revert: drop unsuccessful path-stabilization heuristics + leftover [draft-sync] logs

This commit is contained in:
Diego Imbert
2026-06-05 22:32:02 +02:00
parent 9bf6a7dbc2
commit 98df2b7552
4 changed files with 15 additions and 120 deletions
@@ -78,7 +78,7 @@
import { writable } from 'svelte/store'
import { defaultScriptLanguages, processLangs } from '$lib/scripts'
import DefaultScripts from './DefaultScripts.svelte'
import { getContext, onMount, setContext, tick, untrack } from 'svelte'
import { getContext, onMount, setContext, untrack } from 'svelte'
import EditorHeader from './EditorHeader.svelte'
import AutosaveIndicator from './AutosaveIndicator.svelte'
import LabelsInput from './LabelsInput.svelte'
@@ -345,12 +345,6 @@
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
@@ -361,7 +355,13 @@
// new drafts). Resumed in the async `.finally` so language
// switches AFTER bootstrap (which also call `initContent`) sync
// normally.
console.log('[draft-sync] ScriptBuilder: bootstrap branch START', userDraftPath)
//
// NOTE: this is a best-effort partial defense — on warm in-app
// navs it suppresses the seed/template writes, but on a HARD
// PAGE RELOAD the Path widget's `$userStore`-gated mutation
// cascade lands AFTER our `restartSync` and still POSTs. Fixing
// that robustly needs a different "first user edit" signal
// (e.g. Monaco input event) that hasn't been wired up yet.
UserDraft.stopSync('script', userDraftPath)
if (template === 'wac_python') {
script.modules = {
@@ -378,50 +378,9 @@
}
}
}
console.log('[draft-sync] ScriptBuilder: calling initContent', userDraftPath)
initContent(script.language, script.kind, template).finally(async () => {
console.log('[draft-sync] ScriptBuilder: initContent finally', userDraftPath, {
pathAlreadySet: !!script.path
})
// The `Path` widget assigns `script.path` from its
// `$effect.pre` gated on `$workspaceStore + $userStore` —
// which on a HARD-REFRESH-then-first-nav can be unset when
// we get here. Worse, even on a warm reload the widget's
// `reset()` mutates `meta` in TWO steps (`{owner: ''}` then
// `meta.owner = username`), producing two distinct path
// writes — exiting on the first non-empty value lets the
// second one POST as the user's "first edit".
//
// Wait for `script.path` to STABILIZE: same value across
// two consecutive ticks. `tick()` returns only after Svelte
// has flushed pending effects, so a stable read means the
// Path widget's cascade settled and any intermediate
// mutations were already swallowed under suspension.
//
// Bounded so a stuck mount can't disable autosave forever.
let waited = 0
let last: string | undefined = undefined
let stable = 0
while (waited < 30) {
await tick()
waited++
const cur = script.path
if (cur && cur === last) {
stable++
if (stable >= 2) break
} else {
last = cur
stable = 0
}
}
console.log('[draft-sync] ScriptBuilder: path stable → restartSync', userDraftPath, {
ticks: waited,
path: script.path
})
initContent(script.language, script.kind, template).finally(() => {
UserDraft.restartSync('script', userDraftPath)
})
} else {
console.log('[draft-sync] ScriptBuilder: bootstrap SKIPPED (content non-empty)', userDraftPath)
}
async function isTemplateScript() {
@@ -466,16 +425,8 @@
// 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)
}
@@ -585,11 +536,6 @@
try {
script.schema = script.schema ?? emptySchema()
try {
console.log('[draft-sync] editScript: inferArgs', {
language: script.language,
contentLen: script.content?.length ?? 0,
contentHead: script.content?.slice(0, 80)
})
const result = await inferArgs(
script.language,
script.content,
@@ -604,12 +550,6 @@
script.has_preprocessor = result?.has_preprocessor || undefined
}
} catch (error) {
console.error('[draft-sync] editScript: inferArgs threw', {
language: script.language,
contentLen: script.content?.length ?? 0,
contentHead: script.content?.slice(0, 80),
error
})
sendUserToast(`Could not parse code, are you sure it is valid?`, true)
}
+5 -39
View File
@@ -444,13 +444,8 @@ export const UserDraft = {
const ws = resolveWorkspace(opts)
const mk = mapKey(ws, itemKind, path)
const entry = entries.get(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)
}
if (entry) entry.syncSuspended = true
else pendingSuspensions.add(mk)
},
/**
@@ -463,13 +458,9 @@ export const UserDraft = {
restartSync(itemKind: UserDraftItemKind, path: string, opts?: UserDraftOptions): void {
const ws = resolveWorkspace(opts)
const mk = mapKey(ws, itemKind, path)
const hadPending = pendingSuspensions.delete(mk)
pendingSuspensions.delete(mk)
const entry = entries.get(mk)
if (entry) entry.syncSuspended = false
console.log('[draft-sync] restartSync', mk, {
entryLive: !!entry,
clearedPending: hadPending
})
},
/**
@@ -651,13 +642,8 @@ 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 —
@@ -705,42 +691,22 @@ function acquireEntry(
const stored = cell.val
if (stored !== undefined) readFieldsRecursively(stored.value)
const next = stored === undefined ? undefined : JSON.stringify(stored)
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)
if (next === lastSerialized) return
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) {
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')
})
if (entry?.syncSuspended) return
void UserDraftDbSyncer.save({
workspace,
itemKind,
+1 -9
View File
@@ -147,9 +147,6 @@ const pendingSaveOpts = new Map<string, UserDraftDbSyncerSaveOpts>()
async function postSave(opts: UserDraftDbSyncerSaveOpts): Promise<void> {
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,
@@ -172,9 +169,8 @@ async function postSave(opts: UserDraftDbSyncerSaveOpts): Promise<void> {
// 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('[draft-sync] postSave FAILED', key, e)
console.error('UserDraftDbSyncer.save failed', e)
}
}
@@ -288,10 +284,6 @@ export const UserDraftDbSyncer = {
async save(opts: UserDraftDbSyncerSaveOpts): Promise<void> {
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.
@@ -150,7 +150,6 @@
// 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
@@ -172,11 +171,9 @@
} 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) {