diff --git a/frontend/src/lib/components/ScriptBuilder.svelte b/frontend/src/lib/components/ScriptBuilder.svelte index 5080a05736..5842f4be1e 100644 --- a/frontend/src/lib/components/ScriptBuilder.svelte +++ b/frontend/src/lib/components/ScriptBuilder.svelte @@ -345,6 +345,12 @@ let pathError = $state('') let loadingSave = $state(false) + console.log('[draft-sync] ScriptBuilder script-tag: enter', { + userDraftPath, + initialPath, + contentLen: script.content?.length ?? 0, + isEmpty: script.content == '' + }) if (script.content == '') { // Suspend autosave around the bootstrap mutations — seeding the // editor with the template's `initialCode` is a programmatic @@ -355,7 +361,7 @@ // new drafts). Resumed in the async `.finally` so language // switches AFTER bootstrap (which also call `initContent`) sync // normally. - console.log('Suspending autosave for script bootstrap', userDraftPath) + console.log('[draft-sync] ScriptBuilder: bootstrap branch START', userDraftPath) UserDraft.stopSync('script', userDraftPath) if (template === 'wac_python') { script.modules = { @@ -372,9 +378,13 @@ } } } + console.log('[draft-sync] ScriptBuilder: calling initContent', userDraftPath) initContent(script.language, script.kind, template).finally(() => { + console.log('[draft-sync] ScriptBuilder: initContent finally → restartSync', userDraftPath) UserDraft.restartSync('script', userDraftPath) }) + } else { + console.log('[draft-sync] ScriptBuilder: bootstrap SKIPPED (content non-empty)', userDraftPath) } async function isTemplateScript() { @@ -419,8 +429,16 @@ // doesn't run `inferArgs` on an empty `script.content` and toast // "Could not parse code". If a template script is then loaded // below we re-seed with the `templateScript=true` variant. + console.log('[draft-sync] initContent: SYNC seed content', userDraftPath, { + language, + template + }) script.content = initialCode(language, kind, template, false) + console.log('[draft-sync] initContent: awaiting isTemplateScript', userDraftPath) const templateScript = await isTemplateScript() + console.log('[draft-sync] initContent: post-await', userDraftPath, { + hasTemplateScript: !!templateScript + }) if (templateScript) { script.content = initialCode(language, kind, template, true) } diff --git a/frontend/src/lib/userDraft.svelte.ts b/frontend/src/lib/userDraft.svelte.ts index ee6255a9e3..dd8585657c 100644 --- a/frontend/src/lib/userDraft.svelte.ts +++ b/frontend/src/lib/userDraft.svelte.ts @@ -444,8 +444,13 @@ export const UserDraft = { const ws = resolveWorkspace(opts) const mk = mapKey(ws, itemKind, path) const entry = entries.get(mk) - if (entry) entry.syncSuspended = true - else pendingSuspensions.add(mk) + if (entry) { + entry.syncSuspended = true + console.log('[draft-sync] stopSync (entry live)', mk) + } else { + pendingSuspensions.add(mk) + console.log('[draft-sync] stopSync (queued, no entry yet)', mk) + } }, /** @@ -458,9 +463,13 @@ export const UserDraft = { restartSync(itemKind: UserDraftItemKind, path: string, opts?: UserDraftOptions): void { const ws = resolveWorkspace(opts) const mk = mapKey(ws, itemKind, path) - pendingSuspensions.delete(mk) + const hadPending = pendingSuspensions.delete(mk) const entry = entries.get(mk) if (entry) entry.syncSuspended = false + console.log('[draft-sync] restartSync', mk, { + entryLive: !!entry, + clearedPending: hadPending + }) }, /** @@ -642,8 +651,13 @@ function acquireEntry( const existing = entries.get(mk) if (existing) { existing.count++ + console.log('[draft-sync] acquireEntry: reuse', mk, { newCount: existing.count }) return } + console.log('[draft-sync] acquireEntry: NEW', mk, { + hasSeed: defaultValue !== undefined, + pendingSuspended: pendingSuspensions.has(mk) + }) // Seed the cell with the caller's `defaultValue` (deep-cloned so the // cell owns its copy and the caller's baseline can't alias it). This is // how editors report the deployed/draft state until the user edits — @@ -691,22 +705,42 @@ function acquireEntry( const stored = cell.val if (stored !== undefined) readFieldsRecursively(stored.value) const next = stored === undefined ? undefined : JSON.stringify(stored) - if (next === lastSerialized) return + if (next === lastSerialized) { + console.log('[draft-sync] effect: no change', mk, { + nextLen: next?.length ?? 0 + }) + return + } + const nextLen = next?.length ?? 0 + const diff = nextLen - (lastSerialized?.length ?? 0) lastSerialized = next if (skipNextWrite) { skipNextWrite = false + console.log('[draft-sync] effect: SWALLOW (skipNextWrite seed)', mk, { + nextLen, + diff + }) return } const entry = entries.get(mk) if (entry?.skipNextSync) { entry.skipNextSync = false + console.log('[draft-sync] effect: SWALLOW (skipNextSync)', mk, { nextLen, diff }) return } // `syncSuspended` swallows the POST but still advances // `lastSerialized` (above) so when sync resumes the next // real change is detected as a change — only the writes // made during suspension are dropped from the server's view. - if (entry?.syncSuspended) return + if (entry?.syncSuspended) { + console.log('[draft-sync] effect: SWALLOW (syncSuspended)', mk, { nextLen, diff }) + return + } + console.log('[draft-sync] effect: POST', mk, { + nextLen, + diff, + stack: new Error().stack?.split('\n').slice(1, 6).join('\n') + }) void UserDraftDbSyncer.save({ workspace, itemKind, diff --git a/frontend/src/lib/userDraftDbSyncer.svelte.ts b/frontend/src/lib/userDraftDbSyncer.svelte.ts index e2e33ae524..0fcee34861 100644 --- a/frontend/src/lib/userDraftDbSyncer.svelte.ts +++ b/frontend/src/lib/userDraftDbSyncer.svelte.ts @@ -147,6 +147,9 @@ const pendingSaveOpts = new Map() async function postSave(opts: UserDraftDbSyncerSaveOpts): Promise { const key = draftKey(opts.workspace, opts.itemKind, opts.path) + console.log('[draft-sync] postSave START (sending POST)', key, { + valueIsNull: opts.value === null + }) try { const resp = await DraftService.saveDraft({ workspace: opts.workspace, @@ -169,8 +172,9 @@ async function postSave(opts: UserDraftDbSyncerSaveOpts): Promise { // newer `save()` that arrived during the POST replaces the entry // and must survive for the next flush / debouncer round. if (pendingSaveOpts.get(key) === opts) pendingSaveOpts.delete(key) + console.log('[draft-sync] postSave SUCCESS', key) } catch (e) { - console.error('UserDraftDbSyncer.save failed', e) + console.error('[draft-sync] postSave FAILED', key, e) } } @@ -284,6 +288,10 @@ export const UserDraftDbSyncer = { async save(opts: UserDraftDbSyncerSaveOpts): Promise { const key = draftKey(opts.workspace, opts.itemKind, opts.path) + console.log('[draft-sync] UserDraftDbSyncer.save called', key, { + immediate: !!opts.immediate, + valueIsNull: opts.value === null + }) // Track the latest unconfirmed save BEFORE entering the pipeline // so the unload flush has something to send even if the page // hides before the debouncer fires. diff --git a/frontend/src/routes/(root)/(logged)/scripts/edit/[...path]/+page.svelte b/frontend/src/routes/(root)/(logged)/scripts/edit/[...path]/+page.svelte index b344741eab..05a87f2c59 100644 --- a/frontend/src/routes/(root)/(logged)/scripts/edit/[...path]/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/scripts/edit/[...path]/+page.svelte @@ -150,6 +150,7 @@ // fires. `initialPath = ''` also makes ScriptBuilder open the // metadata drawer on mount. Strip the single-use flag last. if (page.url.searchParams.get('new_draft') === 'true') { + console.log('[draft-sync] route: new_draft branch START', draftPath) // Suspend autosave for the whole new-draft bootstrap: the seed // `setDraftAndMeta` AND ScriptBuilder's `initContent` (which // fills `script.content` from a template) are both @@ -171,9 +172,11 @@ } as unknown as EditableScript initialPath = '' savedScript = structuredClone(empty) + console.log('[draft-sync] route: about to setDraftAndMeta(empty)', draftPath) scriptHandle.setDraftAndMeta(empty, {}) fullyLoaded = true renderEditor = true + console.log('[draft-sync] route: new_draft branch DONE', draftPath) return } if (hash) {