fix(editor): make session transitions loss-safe - #210
Conversation
📝 WalkthroughWalkthroughThe editor now tracks dirty sessions, gates transitions with Save/Don’t Save/Cancel choices, finalizes active processes, prevents stale feedpak and audio loads, supports Save As through the File System Access API, and adds backend session close and export endpoints. ChangesEditor session lifecycle
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Editor
participant SessionLifecycle
participant Host
participant FileOps
Editor->>SessionLifecycle: Request session transition
SessionLifecycle->>Host: Finalize recording and stop processes
SessionLifecycle-->>Editor: Show save changes prompt
Editor-->>SessionLifecycle: Choose save, discard, or cancel
SessionLifecycle->>Host: Save current session
Host->>FileOps: saveCDLC()
FileOps-->>Host: Return save result
SessionLifecycle-->>Editor: Allow or block transition
Possibly related issues
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/import.js (1)
373-417: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winGuard
editorAddEmptyKeys()against stale sessions.
CaptureS.sessionIdbefore the fetch and skipS.arrangements.push(...)if it changed before the response returns; Open/New can swap sessions while this request is in flight, and the empty Keys arrangement will otherwise land in the wrong session.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/import.js` around lines 373 - 417, Update editorAddEmptyKeys to capture S.sessionId before starting the fetch, then compare it with the current S.sessionId after the response succeeds and before mutating session state. If the session ID changed, skip S.arrangements.push and all related arrangement-selection/UI updates, while preserving the existing request cleanup in finally.
🧹 Nitpick comments (1)
src/file-ops.js (1)
63-69: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winObject URL revoked immediately after the synthetic click — may cancel the download in some browsers.
Several documented browser/extension bugs show that revoking a blob URL right after triggering an anchor's programmatic
click()(even viasetTimeout(..., 0)) can abort the download before the browser finishes reading it; FileSaver.js works around this with a much longer delay (40s).⏳ Suggested widening of the revoke delay
- a.click(); - setTimeout(() => URL.revokeObjectURL(url), 0); + a.click(); + setTimeout(() => URL.revokeObjectURL(url), 30000);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/file-ops.js` around lines 63 - 69, Increase the object URL revocation delay in the download flow after the anchor click, using a substantially longer timeout so browsers can finish consuming the blob before URL.revokeObjectURL runs. Keep the existing URL creation, synthetic click, filename handling, and cleanup behavior unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@routes.py`:
- Around line 3059-3075: The export_editor_session endpoint must support valid
directory-form .feedpak and .sloppak sessions instead of rejecting them via
candidate.is_file(). Preserve the existing path-safety checks, and when
candidate is a directory, zip or otherwise materialize it into a packed file for
export; continue returning the current conflict response only for missing or
unsupported saved paths.
In `@src/file-ops.js`:
- Around line 105-117: Re-check the generation in the load flow after awaiting
disposeBackendSession(oldSessionId) and before applying any new session state
such as S.title, S.sessionId, or S.arrangements. If generation !==
packLoadGeneration, return false so a stale load cannot overwrite newer state;
keep the existing teardown behavior for the current generation.
In `@src/main.js`:
- Around line 608-614: Remove the redundant guard from the New… → Create flow by
updating window.editorShowCreateModal so it directly invokes
editorShowCreateModal without calling guardSessionTransition; keep the existing
guard in window.editorShowNewFormatPicker, which already protects the entry
point.
In `@src/session-lifecycle.js`:
- Around line 87-98: Update disposeBackendSession to bound the session-close
fetch with an AbortController and timeout, aborting the request when the
deadline is reached. Preserve the existing best-effort behavior by swallowing
timeout and fetch errors so session transitions do not wait indefinitely.
---
Outside diff comments:
In `@src/import.js`:
- Around line 373-417: Update editorAddEmptyKeys to capture S.sessionId before
starting the fetch, then compare it with the current S.sessionId after the
response succeeds and before mutating session state. If the session ID changed,
skip S.arrangements.push and all related arrangement-selection/UI updates, while
preserving the existing request cleanup in finally.
---
Nitpick comments:
In `@src/file-ops.js`:
- Around line 63-69: Increase the object URL revocation delay in the download
flow after the anchor click, using a substantially longer timeout so browsers
can finish consuming the blob before URL.revokeObjectURL runs. Keep the existing
URL creation, synthetic click, filename handling, and cleanup behavior
unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 124196c8-440f-48a5-a85b-afa9e333b958
📒 Files selected for processing (16)
CHANGELOG.mdroutes.pysrc/arrangement.jssrc/audio.jssrc/create.jssrc/file-ops.jssrc/history.jssrc/host.jssrc/import.jssrc/main.jssrc/menu-bar.jssrc/midi-record.jssrc/replace-audio.jssrc/session-lifecycle.jssrc/state.jstests/session_lifecycle.test.mjs
| @app.get("/api/plugins/editor/session/export") | ||
| async def export_editor_session(session_id: str): | ||
| session = sessions.get(session_id) | ||
| if not session: | ||
| return JSONResponse({"error": "No active session"}, 404) | ||
| filename = str(session.get("filename") or "") | ||
| dlc_dir = get_dlc_dir() | ||
| if not filename or not dlc_dir: | ||
| return JSONResponse({"error": "Session has no saved feedpak"}, 409) | ||
| root = dlc_dir.resolve() | ||
| candidate = (root / filename).resolve() | ||
| try: | ||
| candidate.relative_to(root) | ||
| except ValueError: | ||
| return JSONResponse({"error": "forbidden"}, 403) | ||
| if not candidate.is_file(): | ||
| return JSONResponse({"error": "Saved feedpak is not a packed file"}, 409) |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Support authoring-form packages in export.
Save As calls this endpoint after saveCDLC(), but directory-form sloppaks intentionally save in place and remain directories. This is_file() check therefore returns 409 for valid .feedpak/ or .sloppak/ sessions, so Save As fails. Zip the directory for export or materialize a packed file before exporting.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@routes.py` around lines 3059 - 3075, The export_editor_session endpoint must
support valid directory-form .feedpak and .sloppak sessions instead of rejecting
them via candidate.is_file(). Preserve the existing path-safety checks, and when
candidate is a directory, zip or otherwise materialize it into a packed file for
export; continue returning the current conflict response only for missing or
unsupported saved paths.
| const data = await resp.json(); | ||
| if (data.error) { setStatus('Error: ' + data.error); return; } | ||
| if (generation !== packLoadGeneration) return false; | ||
| if (data.error) { setStatus('Error: ' + data.error); return false; } | ||
|
|
||
| if (oldSessionId && oldSessionId !== data.session_id) { | ||
| await disposeBackendSession(oldSessionId); | ||
| } | ||
| externalSaveHandle = null; | ||
| // The outgoing decoded buffer is not part of the new job. In | ||
| // particular, an audio-less feedpak must not inherit AUDIO mode or | ||
| // accidentally make the old recording playable again. | ||
| S.audioBuffer = null; | ||
| S.waveformPeaks = null; |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Map the relevant file and nearby symbols first.
ast-grep outline src/file-ops.js --view expanded || true
# Read the relevant section with line numbers.
sed -n '1,240p' src/file-ops.js | cat -n
# Find related functions/usages.
rg -n "disposeBackendSession|packLoadGeneration|loadCDLC|editorSaveAsSloppakConfirm|editorSaveAs|_writeExternalCopy|_mirrorExternalCopy|_exportBlob|setStatus\(" src/file-ops.js src -g '!**/node_modules/**' || trueRepository: got-feedBack/feedBack-plugin-editor
Length of output: 37420
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the backend-session teardown and the rest of loadCDLC.
sed -n '1,220p' src/session-lifecycle.js | cat -n
printf '\n----\n'
sed -n '240,260p' src/file-ops.js | cat -n
# Small static check: confirm there is an await between the generation check and the
# first S.* state mutation, and no re-check in that window.
python3 - <<'PY'
from pathlib import Path
text = Path('src/file-ops.js').read_text()
start = text.index("if (generation !== packLoadGeneration) return false;")
end = text.index("externalSaveHandle = null;")
window = text[start:end]
print("window_has_await_disposeBackendSession:", "await disposeBackendSession(oldSessionId);" in window)
print("window_has_second_generation_check:", window.count("generation !== packLoadGeneration"))
print("window_has_S_mutation_before_recheck:", "S.audioBuffer = null;" in window and "await disposeBackendSession(oldSessionId);" in window)
PYRepository: got-feedBack/feedBack-plugin-editor
Length of output: 5843
Avoid awaiting session teardown before applying the new load state. The generation guard runs before await disposeBackendSession(oldSessionId), so a newer loadCDLC() can start while this call is suspended and the stale invocation will still overwrite S.title, S.sessionId, S.arrangements, etc. Re-check generation === packLoadGeneration immediately after the await, or fire-and-forget the best-effort close.
🧰 Tools
🪛 ast-grep (0.44.1)
[error] 106-106: React's useState should not be directly called
Context: setStatus('Error: ' + data.error)
Note: [CWE-710] Improper Adherence to Coding Standards. Security best practice.
(usestate-direct-usage)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/file-ops.js` around lines 105 - 117, Re-check the generation in the load
flow after awaiting disposeBackendSession(oldSessionId) and before applying any
new session state such as S.title, S.sessionId, or S.arrangements. If generation
!== packLoadGeneration, return false so a stale load cannot overwrite newer
state; keep the existing teardown behavior for the current generation.
| export async function disposeBackendSession(sessionId) { | ||
| if (!sessionId) return; | ||
| try { | ||
| await fetch('/api/plugins/editor/session/close', { | ||
| method: 'POST', | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| body: JSON.stringify({ session_id: sessionId }), | ||
| }); | ||
| } catch (_) { | ||
| // Best effort. The backend also expires abandoned temp sessions. | ||
| } | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Add a timeout/abort to the session-close fetch.
disposeBackendSession is awaited inline in the critical path (e.g. loadCDLC awaits it before applying the newly loaded session's data). With no timeout/AbortController, a slow or hanging /session/close response blocks the entire session-transition flow indefinitely, even though this call is explicitly "best effort."
⏱️ Proposed fix: bound the close request
export async function disposeBackendSession(sessionId) {
if (!sessionId) return;
try {
- await fetch('/api/plugins/editor/session/close', {
+ const controller = typeof AbortController === 'function' ? new AbortController() : null;
+ const timer = controller ? setTimeout(() => controller.abort(), 5000) : null;
+ await fetch('/api/plugins/editor/session/close', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ session_id: sessionId }),
- });
+ ...(controller ? { signal: controller.signal } : {}),
+ });
+ if (timer) clearTimeout(timer);
} catch (_) {
// Best effort. The backend also expires abandoned temp sessions.
}
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export async function disposeBackendSession(sessionId) { | |
| if (!sessionId) return; | |
| try { | |
| await fetch('/api/plugins/editor/session/close', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ session_id: sessionId }), | |
| }); | |
| } catch (_) { | |
| // Best effort. The backend also expires abandoned temp sessions. | |
| } | |
| } | |
| export async function disposeBackendSession(sessionId) { | |
| if (!sessionId) return; | |
| try { | |
| const controller = typeof AbortController === 'function' ? new AbortController() : null; | |
| const timer = controller ? setTimeout(() => controller.abort(), 5000) : null; | |
| await fetch('/api/plugins/editor/session/close', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ session_id: sessionId }), | |
| ...(controller ? { signal: controller.signal } : {}), | |
| }); | |
| if (timer) clearTimeout(timer); | |
| } catch (_) { | |
| // Best effort. The backend also expires abandoned temp sessions. | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/session-lifecycle.js` around lines 87 - 98, Update disposeBackendSession
to bound the session-close fetch with an AbortController and timeout, aborting
the request when the deadline is reached. Preserve the existing best-effort
behavior by swallowing timeout and fetch errors so session transitions do not
wait indefinitely.
Keep-both CHANGELOG resolution: the session-transitions entry joins the merged wave's Added block; the stale-AudioBufferSource fix folds into the existing Unreleased Fixed section. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EBQCHCNA81E9tHmSDHSe2Q
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/audio.js (1)
39-40: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winCancellable load pattern looks correct; consider surfacing HTTP failures explicitly.
The generation-counter +
AbortControllercombo is sound:cancelAudioLoad()bumps the generation and aborts synchronously before the new controller is created (no interleaving window), and the post-decodegeneration !== audioLoadGenerationcheck correctly gates theS.audioBuffer/S.durationcommit so a stale decode can never clobber a newer load — including the case where abort fires whilearrayBuffer()is still reading the body (still rejects withAbortError, so it's caught cleanly).One gap:
resp.okis never checked, so an HTTP error response (404/expired signed URL, etc.) falls through todecodeAudioData, which will fail with a genericEncodingErrorrather than a clear "HTTP xxx" message — logged at Line 76 as just'Audio load error:'. Given this path is now central to session/pack transitions, a clearer failure signal would help debugging in the field.🔧 Suggested check
const resp = await fetch(url, audioLoadController ? { signal: audioLoadController.signal } : undefined); + if (!resp.ok) throw new Error(`HTTP ${resp.status} loading audio`); const buf = await resp.arrayBuffer();Also applies to: 57-87
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/audio.js` around lines 39 - 40, In the audio loading flow around the fetch response handling, check resp.ok before reading or decoding the body and raise an error that includes the HTTP status (and preferably status text) when the request fails. Keep the existing AbortController/generation cancellation behavior and ensure the resulting error reaches the existing “Audio load error:” logging path.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@src/audio.js`:
- Around line 39-40: In the audio loading flow around the fetch response
handling, check resp.ok before reading or decoding the body and raise an error
that includes the HTTP status (and preferably status text) when the request
fails. Keep the existing AbortController/generation cancellation behavior and
ensure the resulting error reaches the existing “Audio load error:” logging
path.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 676e5cb8-dcd1-4b45-a392-a9ac6a0b914d
📒 Files selected for processing (5)
CHANGELOG.mdsrc/audio.jssrc/host.jssrc/main.jssrc/menu-bar.js
✅ Files skipped from review due to trivial changes (1)
- CHANGELOG.md
🚧 Files skipped from review as they are similar to previous changes (3)
- src/host.js
- src/menu-bar.js
- src/main.js
…mpt lifecycle The session-lifecycle work hardened loadCDLC but the GP/EOF import path (editorApplyCreateResult) replaced the job through a different code path and skipped all of it: it never stopped the outgoing playback, cancelled the pending audio load, cleared the decoded buffer, or disposed the old backend session. An audio-less import (the common draft-now case) therefore left the previous recording sounding under the new chart and leaked the old session's sandbox. Route it through the same stopSessionProcesses() + buffer clear + disposeBackendSession(old) preamble loadCDLC uses. Also: - The transition-confirm prompt's Escape listener now rides the screen teardown registry (host.addGlobalListener), and dismissSessionPrompt() detaches it and resolves the pending prompt so a mid-prompt screen re-injection can't strand the awaiting guardSessionTransition. Wired into window.__editorScreenTeardown. host.addGlobalListener now forwards the capture flag. - The "New" format picker no longer double-prompts on a dirty job: its buttons called the guarded window.editorShowCreateModal after the picker had already guarded the transition; they now call the raw opener. Adds tests/import_create_teardown.test.mjs covering the import-create teardown (the gap that hid the bug). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
src/create.js (1)
2142-2158: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winTeardown logic duplicated between
editorApplyCreateResultandloadCDLC.The comment at line 2142 explicitly notes this reimplements the "same outgoing-job teardown loadCDLC performs" (stop processes, clear decoded audio, capture/dispose old session). Having two hand-maintained copies of this sequence (here and in
file-ops.js'sloadCDLC) risks drift if one is updated without the other. Consider extracting a shared helper (e.g.teardownOutgoingSession(oldSessionId)insession-lifecycle.js) that both call sites invoke.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/create.js` around lines 2142 - 2158, Extract the duplicated outgoing-session teardown into a shared helper, such as teardownOutgoingSession, in the session lifecycle module. Move the stopSessionProcesses calls, audio and waveform cleanup, old session capture/disposal, and related teardown behavior from editorApplyCreateResult and loadCDLC into that helper, then update both call sites to invoke it while preserving their existing state updates and session transition behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/create.js`:
- Around line 2142-2158: Remove the blocking await of disposeBackendSession in
the create-result transition after updating S.sessionId, so backend cleanup runs
best-effort without delaying the remaining session state, format, arrangement,
DOM, and audio updates. Preserve the oldSessionId and changed-session guard, and
invoke cleanup without awaiting it.
---
Nitpick comments:
In `@src/create.js`:
- Around line 2142-2158: Extract the duplicated outgoing-session teardown into a
shared helper, such as teardownOutgoingSession, in the session lifecycle module.
Move the stopSessionProcesses calls, audio and waveform cleanup, old session
capture/disposal, and related teardown behavior from editorApplyCreateResult and
loadCDLC into that helper, then update both call sites to invoke it while
preserving their existing state updates and session transition behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: f12c2d36-acdb-41b2-9987-166140fe5976
📒 Files selected for processing (5)
CHANGELOG.mdsrc/create.jssrc/main.jssrc/session-lifecycle.jstests/import_create_teardown.test.mjs
🚧 Files skipped from review as they are similar to previous changes (2)
- src/main.js
- src/session-lifecycle.js
| // Same outgoing-job teardown loadCDLC performs: stop the old playback, | ||
| // the pending audio load and any drag, and drop the decoded buffer. An | ||
| // audio-less import skips the loadAudio() branch below, so without this the | ||
| // previous recording keeps sounding under the new chart and S.audioBuffer | ||
| // stays stale. Dispose the old backend session too so its sandbox isn't leaked. | ||
| const oldSessionId = S.sessionId; | ||
| stopSessionProcesses(); // also cancels the outgoing audio load | ||
| S.audioBuffer = null; | ||
| S.waveformPeaks = null; | ||
| S.title = data.title || ''; | ||
| S.artist = data.artist || ''; | ||
| S.filename = ''; | ||
| S.sessionId = data.session_id; | ||
| if (oldSessionId && oldSessionId !== data.session_id) { | ||
| await disposeBackendSession(oldSessionId); | ||
| } | ||
| markSessionDirty(); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Blocking await on a timeout-less network call mid-transition.
disposeBackendSession (session-lifecycle.js) does a plain await fetch(...) with no AbortController/timeout. Awaiting it here, before the remaining S.format/S.arrangements/DOM updates and host.loadAudio, means a slow or hung backend /session/close call stalls the entire "apply create result" flow — the create modal is already hidden (line 2141) but the new session's UI (title, buttons, arrangement view) won't render until the fetch settles. Since disposeBackendSession already treats this as best-effort and swallows errors, there's no need to block on it here.
🔧 Proposed fix: don't block the UI transition on backend cleanup
S.sessionId = data.session_id;
if (oldSessionId && oldSessionId !== data.session_id) {
- await disposeBackendSession(oldSessionId);
+ disposeBackendSession(oldSessionId); // best-effort, fire-and-forget
}
markSessionDirty();📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Same outgoing-job teardown loadCDLC performs: stop the old playback, | |
| // the pending audio load and any drag, and drop the decoded buffer. An | |
| // audio-less import skips the loadAudio() branch below, so without this the | |
| // previous recording keeps sounding under the new chart and S.audioBuffer | |
| // stays stale. Dispose the old backend session too so its sandbox isn't leaked. | |
| const oldSessionId = S.sessionId; | |
| stopSessionProcesses(); // also cancels the outgoing audio load | |
| S.audioBuffer = null; | |
| S.waveformPeaks = null; | |
| S.title = data.title || ''; | |
| S.artist = data.artist || ''; | |
| S.filename = ''; | |
| S.sessionId = data.session_id; | |
| if (oldSessionId && oldSessionId !== data.session_id) { | |
| await disposeBackendSession(oldSessionId); | |
| } | |
| markSessionDirty(); | |
| // Same outgoing-job teardown loadCDLC performs: stop the old playback, | |
| // the pending audio load and any drag, and drop the decoded buffer. An | |
| // audio-less import skips the loadAudio() branch below, so without this the | |
| // previous recording keeps sounding under the new chart and S.audioBuffer | |
| // stays stale. Dispose the old backend session too so its sandbox isn't leaked. | |
| const oldSessionId = S.sessionId; | |
| stopSessionProcesses(); // also cancels the outgoing audio load | |
| S.audioBuffer = null; | |
| S.waveformPeaks = null; | |
| S.title = data.title || ''; | |
| S.artist = data.artist || ''; | |
| S.filename = ''; | |
| S.sessionId = data.session_id; | |
| if (oldSessionId && oldSessionId !== data.session_id) { | |
| disposeBackendSession(oldSessionId); // best-effort, fire-and-forget | |
| } | |
| markSessionDirty(); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/create.js` around lines 2142 - 2158, Remove the blocking await of
disposeBackendSession in the create-result transition after updating
S.sessionId, so backend cleanup runs best-effort without delaying the remaining
session state, format, arrangement, DOM, and audio updates. Preserve the
oldSessionId and changed-session guard, and invoke cleanup without awaiting it.
Summary
Root cause
loadCDLC()replaced the editor model and audio buffer without first stopping the activeAudioBufferSourceNode. Session replacement also had no centralized dirty guard, explicit backend disposal, or stale-request ownership.Validation
npm test(102/102)npm run lint(0 errors; 3 existing warnings)python -m pytest(230 passed, 2 skipped)Dogfood focus
Summary by CodeRabbit