fix(web): the item pane's save indicator counts the writes outstanding, so "Saved" never shows over a write still in flight (BUG-3044) (#1532)

* fix(web): the item pane's save indicator counts the writes outstanding, so "Saved" never shows over a write still in flight (BUG-3044)

saveStatus was one value written by whichever write reached it last. An older
write's success showed "Saved" while a newer one was in flight (or after the
newest had failed); one field's failure hid another's write; a previous save's
two-second timer fired mid-request. The pane's SSE gates read that value, so
each also admitted an incoming event while a local write was outstanding.

SaveTracker (web/src/lib/items/saveTracker.svelte.ts) counts outstanding
writes: 'saving' while any is out, 'saved' once the last settles only if the
newest-issued one succeeded, 'idle' otherwise. Starting a write clears the
old timer. A reset at load bumps an epoch, so a previous item's late settle is
ignored. saveStatus is now $derived from it, and the ten SSE-gate reads are
unchanged.

All nine save paths begin a token, record success through showSaved(saveTok),
and settle in a finally, so the returns that used to leave the status alone
(a superseded field write, a superseded content write, the collab flush's
force_refresh skip, an item switch) still count down. Without that, a count
would pin 'saving', and every SSE gate with it, until navigation. The raw
drain uses one token for the whole drain.

Identity-fence gate (lead ruling): each settle after an unfenced await is
tabled per row as an allowed commit whose reason cites the tracker's
stale-epoch test. The analysis is not taught to trust the epoch. Rows whose
coverage changed are re-signed. Every changed coverage run traces to this
diff: 25 rows, 285 token-run edits; the 20 not verbatim in it are boundary
splits of the import, the finally blocks, the removed timer declaration and
the helper body. Fixtures are migrated by the same rename. Kill table, old
fixtures on main against migrated fixtures here: 171 fixtures, 0 verdict
flips.

Claude-Session: https://claude.ai/code/session_01UcaMsmoCt8u7VpJhCNxiJJ

* docs(web): the raw drain's one save token is the truthful status, stated on its own terms (BUG-3044)

Claude-Session: https://claude.ai/code/session_01UcaMsmoCt8u7VpJhCNxiJJ
This commit is contained in:
xarmian
2026-09-25 02:40:34 -04:00
committed by GitHub
parent 372f91f03b
commit 326acd29af
9 changed files with 589 additions and 238 deletions
+62 -42
View File
@@ -5,6 +5,7 @@
// Its own statement, so units that only use `api` keep their reviewed hash.
import { isSupersededWriteError } from '$lib/api/client';
import { nextClientWrite } from '$lib/items/clientWrite';
import { SaveTracker, type SaveToken } from '$lib/items/saveTracker.svelte';
import { confirmOpenChildrenOrThrow, isOpenChildrenError } from '$lib/items/openChildrenError';
import { WriteOrder, fieldWriteTarget, rederiveListWrite, submitOrderedOCC } from '$lib/items/fieldWriteOrder';
import { isMultiRelationType } from '$lib/items/relationFieldTypes';
@@ -727,8 +728,12 @@
});
let contentDebounceTimer: ReturnType<typeof setTimeout> | undefined;
let saveStatus = $state<'idle' | 'saving' | 'saved'>('idle');
let saveStatusTimer: ReturnType<typeof setTimeout> | undefined;
// The save indicator counts the writes OUTSTANDING (BUG-3044): every
// `saves.begin()` below is settled in a `finally`, so a superseded or
// fenced-off write still counts down. `saveStatus` stays the name the SSE
// gates read.
const saves = new SaveTracker();
const saveStatus = $derived(saves.status);
// Per-instance shadow of editorStore.dirty/lastSaveTime (PLAN-2154 Phase 0 /
// R4, TASK-2156). editorStore is a module singleton — with a second
@@ -1970,8 +1975,6 @@
// the next item, which then suppresses SSE/sync refreshes
// (the `if (saveStatus === 'saving')` guards above).
// Per Codex review round 12.
clearTimeout(saveStatusTimer);
saveStatusTimer = undefined;
// Cleared BEFORE the status goes idle: a refresh owed to the previous
// item must not fire against this one when the effect below sees the
// status drop (BUG-3036).
@@ -1979,7 +1982,9 @@
// A change recorded for the PREVIOUS load's item is not this load's
// (BUG-3198): the record describes what arrived while this load runs.
changedDuringLoad = emptyChangedDuringLoad();
saveStatus = 'idle';
// Forgets every write still out for the previous item: their settles
// carry the old epoch and are ignored (BUG-3044).
saves.reset();
// Capture the URL parts this load was scoped to. Used in the catch
// path to detect whether the user has navigated away before this
// request rejected — without this the stale catch would clobber a
@@ -3196,10 +3201,13 @@
onBack?.();
}
function showSaved() {
saveStatus = 'saved';
clearTimeout(saveStatusTimer);
saveStatusTimer = setTimeout(() => { saveStatus = 'idle'; }, 2000);
/**
* Records that THIS write succeeded. The indicator shows "Saved" once the
* last outstanding write settles, and only if the newest one issued is this
* kind of success (BUG-3044); see `SaveTracker`.
*/
function showSaved(saveTok: SaveToken) {
saves.succeed(saveTok);
}
async function saveTitle() {
@@ -3231,15 +3239,14 @@
const sent = titleDraft.trim();
const targetItem = item;
const gen = loadGeneration;
saveStatus = 'saving';
const saveTok = saves.begin();
try {
const updated = await api.items.update(wsSlug, targetItem.id, { title: titleDraft.trim() });
if (gen !== loadGeneration || item?.id !== targetItem.id) return;
item = withInflightTags(updated);
showSaved();
showSaved(saveTok);
} catch (err: any) {
if (gen !== loadGeneration || item?.id !== targetItem.id) return;
saveStatus = 'idle';
const reason = err?.message || 'Failed to update title';
// BUG-3115: put the refused text back in the editor, with the reason,
// unless the user has already reopened it and typed something else.
@@ -3255,6 +3262,8 @@
} else {
toastStore.show(reason, 'error');
}
} finally {
saves.settle(saveTok);
}
}
@@ -3326,7 +3335,7 @@
// Taken BEFORE the first send, so a second edit to the same field while
// this one is in flight is visible to every post-await branch below.
const ticket = fieldWrites.take(fieldWriteTarget(targetItem.id, key));
saveStatus = 'saving';
const saveTok = saves.begin();
// One PATCH attempt: single-key merge, OCC-guarded by the caller-supplied
// token. `force` overrides the open-children guard (BUG-1538).
@@ -3404,7 +3413,7 @@
// looks exactly as healthy as the newer one.
if (!fieldWrites.claim(ticket)) return;
item = withInflightTags(fresh);
showSaved();
showSaved(saveTok);
} catch (e) {
// A newer write for this field was dispatched while this one was in
// flight, so this one's outcome is not the one the user is waiting
@@ -3450,7 +3459,6 @@
// question is re-asked here rather than inherited from the
// check at the top of this catch.
if (fieldWrites.superseded(ticket)) return;
saveStatus = 'idle';
const msg = retryErr instanceof Error ? retryErr.message : 'Failed to save';
console.error('Forced field update failed:', retryErr);
toastStore.show(msg, 'error');
@@ -3469,7 +3477,7 @@
// question that matters: has anything newer actually written?
if (!fieldWrites.claim(ticket)) return;
item = withInflightTags(forced);
showSaved();
showSaved(saveTok);
return;
}
// User declined to override. Snap the on-page field back
@@ -3486,7 +3494,6 @@
// claim on return — server changed, pane showing the old value,
// and nothing left to correct it. A branch that asserts no new
// server truth must not take the mark for one.
if (!fieldWrites.superseded(ticket)) saveStatus = 'idle';
item = { ...item };
toastStore.show('Status change cancelled', 'info');
return;
@@ -3498,7 +3505,6 @@
if (isUpdateConflictError(e)) {
if (!stillCurrent() || !item) return;
if (!fieldWrites.claim(ticket)) return;
saveStatus = 'idle';
// `lastServerItem` is only ever assigned inside the submitWithOCC
// closure, so TS's control-flow narrows it to `null` here — the
// assertion restores the real union type it actually holds.
@@ -3513,9 +3519,12 @@
}
// Any other failure mode — network, validation, 500, etc.
if (!stillCurrent()) return;
saveStatus = 'idle';
console.error('Failed to save field:', e);
toastStore.show('Failed to save', 'error');
} finally {
// EVERY path, the superseded early returns included: a superseded
// write that never settled would pin 'saving' for the item (BUG-3044).
saves.settle(saveTok);
}
}
@@ -3637,7 +3646,7 @@
async function flushTagSaver(saver: TagSaver) {
saver.running = true;
saveStatus = 'saving';
const saveTok = saves.begin();
try {
while (saver.pending !== null) {
// NO freeze recheck in the drain (BUG-2263): tag editing is invisible
@@ -3680,7 +3689,7 @@
item = adoptServerItem(fresh);
}
}
if (item && item.id === saver.itemId) showSaved();
if (item && item.id === saver.itemId) showSaved(saveTok);
// A newly-created tag should appear in autocomplete next time.
void loadTagSuggestions(saver.ws);
} catch (e) {
@@ -3690,10 +3699,10 @@
if (item && item.id === saver.itemId) {
// Revert to the last server-confirmed tags, not the optimistic set.
item = { ...item, tags: saver.confirmed };
saveStatus = 'idle';
toastStore.show('Failed to save', 'error');
}
} finally {
saves.settle(saveTok);
saver.running = false;
// Drop the entry once fully drained so the map doesn't accumulate
// stale savers; guard on identity so a newer saver isn't evicted.
@@ -4059,7 +4068,7 @@
// coordinator P1).
const targetItem = item;
const gen = loadGeneration;
saveStatus = 'saving';
const saveTok = saves.begin();
try {
const update: Record<string, any> = {};
if (userId) {
@@ -4070,11 +4079,12 @@
const updated = await api.items.update(wsSlug, targetItem.id, update);
if (gen !== loadGeneration || item?.id !== targetItem.id) return;
item = withInflightTags(updated);
showSaved();
showSaved(saveTok);
} catch {
if (gen !== loadGeneration || item?.id !== targetItem.id) return;
saveStatus = 'idle';
toastStore.show('Failed to update assignment', 'error');
} finally {
saves.settle(saveTok);
}
}
@@ -4085,7 +4095,7 @@
// coordinator P1).
const targetItem = item;
const gen = loadGeneration;
saveStatus = 'saving';
const saveTok = saves.begin();
try {
const update: Record<string, any> = {};
if (roleId) {
@@ -4096,11 +4106,12 @@
const updated = await api.items.update(wsSlug, targetItem.id, update);
if (gen !== loadGeneration || item?.id !== targetItem.id) return;
item = withInflightTags(updated);
showSaved();
showSaved(saveTok);
} catch {
if (gen !== loadGeneration || item?.id !== targetItem.id) return;
saveStatus = 'idle';
toastStore.show('Failed to update role', 'error');
} finally {
saves.settle(saveTok);
}
}
@@ -4157,7 +4168,7 @@
// saved/error feedback onto a switched-in item (Codex).
const reqItem = item;
const gen = loadGeneration;
saveStatus = 'saving';
const saveTok = saves.begin();
// Set lastSaveTime BEFORE the API call so the SSE guard works
// even if the SSE event arrives before the response.
editorStore.setLastSaveTime(Date.now());
@@ -4177,14 +4188,13 @@
localLastSaveTime = Date.now();
editorStore.setDirty(false);
localDirty = false;
showSaved();
showSaved(saveTok);
}).catch((e) => {
if (switchedAway(reqItem, gen)) return;
// A newer write of this tab already landed and owns the outcome.
if (isSupersededWriteError(e)) return;
saveStatus = 'idle';
toastStore.show('Failed to save content', 'error');
});
}).finally(() => saves.settle(saveTok));
}, 1200);
}
@@ -4350,8 +4360,11 @@
const isForegroundCurrent = (): boolean =>
!keepalive && !!item && item.id === itemId && genAtFlush === loadGeneration;
// Only a foreground flush owns the indicator; a keepalive flush after
// navigation begins nothing, so it settles nothing.
let saveTok: SaveToken | null = null;
if (isForegroundCurrent()) {
saveStatus = 'saving';
saveTok = saves.begin();
localLastSaveTime = Date.now();
// Singleton editorStore is owned by the ACTIVE side only: a FROZEN
// (peeking) instance stamps its own shadow above but must not touch the
@@ -4401,7 +4414,7 @@
editorStore.setLastSaveTime(Date.now());
editorStore.setDirty(false);
}
showSaved();
if (saveTok) showSaved(saveTok);
}
return 'flushed';
} catch (e) {
@@ -4413,10 +4426,12 @@
// every arm, foreground or not.
if (e instanceof PadApiError && e.code === 'superseded_write') return 'skipped';
if (isForegroundCurrent()) {
saveStatus = 'idle';
toastStore.show('Failed to save content', 'error');
}
return 'failed';
} finally {
// The force_refresh and superseded returns included (BUG-3044).
if (saveTok) saves.settle(saveTok);
}
},
});
@@ -4486,7 +4501,7 @@
.catch(() => {});
}
// Debounced raw save.
saveStatus = 'saving';
const saveTok = saves.begin();
editorStore.setLastSaveTime(Date.now());
localLastSaveTime = Date.now();
// Raw mode: content is already in storage format (with [[wiki links]])
@@ -4514,7 +4529,7 @@
rawContentSaver.clearPending();
editorStore.setDirty(false);
localDirty = false;
showSaved();
showSaved(saveTok);
} else {
// Newer pending edit; keep local content, adopt
// server-side metadata only. The next debounce
@@ -4525,9 +4540,8 @@
if (!item || item.id !== reqItemId || genAtSave !== loadGeneration) return;
// A newer write of this tab already landed and owns the outcome.
if (isSupersededWriteError(e)) return;
saveStatus = 'idle';
toastStore.show('Failed to save content', 'error');
});
}).finally(() => saves.settle(saveTok));
},
});
@@ -4598,13 +4612,19 @@
const reqItemId = item.id;
const genAtFlush = loadGeneration;
let lastError = false;
// ONE indicator write for the whole drain, begun before its first await
// and settled in the finally below (BUG-3044). That is the truthful
// status for a drain: it IS saving until its last PATCH settles, so the
// indicator stays 'saving' across every pass and shows "Saved" once, when
// the drain empties, as it always did. A token per pass would flicker
// between passes while the drain is still sending.
const saveTok = saves.begin();
try {
for (let i = 0; i < RAW_FLUSH_DRAIN_CAP; i++) {
const markdown: string | null = rawContentSaver.pending;
if (markdown === null) break;
rawContentSaver.cancel();
try {
saveStatus = 'saving';
editorStore.setLastSaveTime(Date.now());
localLastSaveTime = Date.now();
const updated = await api.items.update(wsSlug, reqItemId, { content: markdown, client_write: nextClientWrite() });
@@ -4652,7 +4672,6 @@
if (rawContentSaver.pending === markdown) rawContentSaver.clearPending();
continue;
}
saveStatus = 'idle';
toastStore.show('Failed to save content', 'error');
lastError = true;
break;
@@ -4661,11 +4680,12 @@
if (!lastError && rawContentSaver.pending === null) {
editorStore.setDirty(false);
localDirty = false;
showSaved();
showSaved(saveTok);
return true;
}
return false;
} finally {
saves.settle(saveTok);
rawFlushInFlight = false;
}
}
@@ -6,8 +6,8 @@
"class": 4,
"subs": [
[
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n",
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\twindow.addEventListener('focus', (() => () => {\n\t\t\t\titem = withInflightTags(updated);\n\t\t\t})());\n\t\t\tshowSaved();\n"
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n",
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\twindow.addEventListener('focus', (() => () => {\n\t\t\t\titem = withInflightTags(updated);\n\t\t\t})());\n\t\t\tshowSaved(saveTok);\n"
]
],
"refuses": [
@@ -21,8 +21,8 @@
"class": 4,
"subs": [
[
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n",
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\twindow.addEventListener('focus', (() => {\n\t\t\t\treturn () => {\n\t\t\t\t\titem = withInflightTags(updated);\n\t\t\t\t};\n\t\t\t})());\n\t\t\tshowSaved();\n"
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n",
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\twindow.addEventListener('focus', (() => {\n\t\t\t\treturn () => {\n\t\t\t\t\titem = withInflightTags(updated);\n\t\t\t\t};\n\t\t\t})());\n\t\t\tshowSaved(saveTok);\n"
]
],
"refuses": [
@@ -36,8 +36,8 @@
"class": 4,
"subs": [
[
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n",
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\t(window.onfocus = updated ? () => {\n\t\t\t\titem = withInflightTags(updated);\n\t\t\t} : null, tick)();\n\t\t\tshowSaved();\n"
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n",
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\t(window.onfocus = updated ? () => {\n\t\t\t\titem = withInflightTags(updated);\n\t\t\t} : null, tick)();\n\t\t\tshowSaved(saveTok);\n"
]
],
"refuses": [
@@ -49,8 +49,8 @@
"class": 2,
"subs": [
[
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n",
"{ title: titleDraft.trim() });\n\t\t\tconst applyTitleNow = (next = (item = withInflightTags(updated))) => next;\n\t\t\tapplyTitleNow();\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n"
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n",
"{ title: titleDraft.trim() });\n\t\t\tconst applyTitleNow = (next = (item = withInflightTags(updated))) => next;\n\t\t\tapplyTitleNow();\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n"
]
],
"refuses": [
@@ -62,8 +62,8 @@
"class": 2,
"subs": [
[
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n",
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\twindow.addEventListener('focus', (_e: Event, _n = (item = withInflightTags(updated))) => {});\n\t\t\tshowSaved();\n"
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n",
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\twindow.addEventListener('focus', (_e: Event, _n = (item = withInflightTags(updated))) => {});\n\t\t\tshowSaved(saveTok);\n"
]
],
"refuses": [
@@ -75,8 +75,8 @@
"class": 2,
"subs": [
[
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n",
"{ title: titleDraft.trim() });\n\t\t\tconst [_first = (item = withInflightTags(updated))] = [];\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n"
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n",
"{ title: titleDraft.trim() });\n\t\t\tconst [_first = (item = withInflightTags(updated))] = [];\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n"
]
],
"refuses": [
@@ -88,8 +88,8 @@
"class": 3,
"subs": [
[
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n",
"{ title: titleDraft.trim() });\n\t\t\tfor (item of [updated]);\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n"
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n",
"{ title: titleDraft.trim() });\n\t\t\tfor (item of [updated]);\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n"
]
],
"refuses": [
@@ -101,8 +101,8 @@
"class": 1,
"subs": [
[
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n",
"{ title: titleDraft.trim() });\n\t\t\tswitch ((loadGeneration = gen), 0) {\n\t\t\t\tcase 1:\n\t\t\t\t\tlet loadGeneration = 0;\n\t\t\t\t\tvoid loadGeneration;\n\t\t\t}\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n"
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n",
"{ title: titleDraft.trim() });\n\t\t\tswitch ((loadGeneration = gen), 0) {\n\t\t\t\tcase 1:\n\t\t\t\t\tlet loadGeneration = 0;\n\t\t\t\t\tvoid loadGeneration;\n\t\t\t}\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n"
]
],
"refuses": [
@@ -114,8 +114,8 @@
"class": 5,
"subs": [
[
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n",
"{ title: titleDraft.trim() });\n\t\t\tuntrack(loadData);\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n"
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n",
"{ title: titleDraft.trim() });\n\t\t\tuntrack(loadData);\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n"
]
],
"refuses": [
@@ -128,8 +128,8 @@
"class": 1,
"subs": [
[
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n",
"{ title: titleDraft.trim() });\n\t\t\tconst console = { warn: api.items.update };\n\t\t\tconsole.warn(wsSlug, targetItem.id, { title: 'stale' });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n"
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n",
"{ title: titleDraft.trim() });\n\t\t\tconst console = { warn: api.items.update };\n\t\t\tconsole.warn(wsSlug, targetItem.id, { title: 'stale' });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n"
]
],
"refuses": [
@@ -141,8 +141,8 @@
"class": 1,
"subs": [
[
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n",
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\tconst untrack = window.addEventListener.bind(window, 'focus');\n\t\t\tuntrack(() => {\n\t\t\t\titem = withInflightTags(updated);\n\t\t\t});\n\t\t\tshowSaved();\n"
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n",
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\tconst untrack = window.addEventListener.bind(window, 'focus');\n\t\t\tuntrack(() => {\n\t\t\t\titem = withInflightTags(updated);\n\t\t\t});\n\t\t\tshowSaved(saveTok);\n"
]
],
"refuses": [
@@ -154,8 +154,8 @@
"class": 1,
"subs": [
[
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n",
"{ title: titleDraft.trim() });\n\t\t\tconst identityHeld = (_c: number) => true;\n\t\t\tif (!identityHeld(gen)) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n"
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n",
"{ title: titleDraft.trim() });\n\t\t\tconst identityHeld = (_c: number) => true;\n\t\t\tif (!identityHeld(gen)) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n"
]
],
"refuses": [
@@ -167,8 +167,8 @@
"class": 1,
"subs": [
[
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n",
"{ title: titleDraft.trim() });\n\t\t\tconst loadGeneration = gen;\n\t\t\tif (gen !== loadGeneration) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n"
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n",
"{ title: titleDraft.trim() });\n\t\t\tconst loadGeneration = gen;\n\t\t\tif (gen !== loadGeneration) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n"
]
],
"refuses": [
@@ -180,12 +180,12 @@
"class": 6,
"subs": [
[
"\t\tconst gen = loadGeneration;\n\t\tsaveStatus = 'saving';\n\t\ttry {\n\t\t\tconst updated = await api.items.update(wsSlug, targetItem.id, { title",
"\t\tconst gen = loadGeneration;\n\t\tconst titleStillHere = gen === loadGeneration;\n\t\tsaveStatus = 'saving';\n\t\ttry {\n\t\t\tconst updated = await api.items.update(wsSlug, targetItem.id, { title"
"\t\tconst gen = loadGeneration;\n\t\tconst saveTok = saves.begin();\n\t\ttry {\n\t\t\tconst updated = await api.items.update(wsSlug, targetItem.id, { title",
"\t\tconst gen = loadGeneration;\n\t\tconst titleStillHere = gen === loadGeneration;\n\t\tconst saveTok = saves.begin();\n\t\ttry {\n\t\t\tconst updated = await api.items.update(wsSlug, targetItem.id, { title"
],
[
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n",
"{ title: titleDraft.trim() });\n\t\t\tif (!titleStillHere) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n"
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n",
"{ title: titleDraft.trim() });\n\t\t\tif (!titleStillHere) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n"
]
],
"refuses": [
@@ -198,12 +198,12 @@
"class": 6,
"subs": [
[
"\t\tconst gen = loadGeneration;\n\t\tsaveStatus = 'saving';\n\t\ttry {\n\t\t\tconst updated = await api.items.update(wsSlug, targetItem.id, { title",
"\t\tconst gen = loadGeneration;\n\t\tconst sameGen = (g: number, _other: number) => g === loadGeneration;\n\t\tsaveStatus = 'saving';\n\t\ttry {\n\t\t\tconst updated = await api.items.update(wsSlug, targetItem.id, { title"
"\t\tconst gen = loadGeneration;\n\t\tconst saveTok = saves.begin();\n\t\ttry {\n\t\t\tconst updated = await api.items.update(wsSlug, targetItem.id, { title",
"\t\tconst gen = loadGeneration;\n\t\tconst sameGen = (g: number, _other: number) => g === loadGeneration;\n\t\tconst saveTok = saves.begin();\n\t\ttry {\n\t\t\tconst updated = await api.items.update(wsSlug, targetItem.id, { title"
],
[
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n",
"{ title: titleDraft.trim() });\n\t\t\tif (!sameGen(loadGeneration, gen)) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n"
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n",
"{ title: titleDraft.trim() });\n\t\t\tif (!sameGen(loadGeneration, gen)) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n"
]
],
"refuses": [
@@ -216,8 +216,8 @@
"class": 6,
"subs": [
[
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n",
"{ title: titleDraft.trim() });\n\t\t\tif (!identityHeld(gen)) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n"
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n",
"{ title: titleDraft.trim() });\n\t\t\tif (!identityHeld(gen)) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n"
]
],
"refuses": [
@@ -230,12 +230,12 @@
"class": 6,
"subs": [
[
"\t\tconst gen = loadGeneration;\n\t\tsaveStatus = 'saving';\n\t\ttry {\n\t\t\tconst updated = await api.items.update(wsSlug, targetItem.id, { title",
"\t\tconst gen = loadGeneration;\n\t\tconst identity = captureIdentity();\n\t\tsaveStatus = 'saving';\n\t\ttry {\n\t\t\tconst updated = await api.items.update(wsSlug, targetItem.id, { title"
"\t\tconst gen = loadGeneration;\n\t\tconst saveTok = saves.begin();\n\t\ttry {\n\t\t\tconst updated = await api.items.update(wsSlug, targetItem.id, { title",
"\t\tconst gen = loadGeneration;\n\t\tconst identity = captureIdentity();\n\t\tconst saveTok = saves.begin();\n\t\ttry {\n\t\t\tconst updated = await api.items.update(wsSlug, targetItem.id, { title"
],
[
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n",
"{ title: titleDraft.trim() });\n\t\t\tif (identity !== loadGeneration) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n"
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n",
"{ title: titleDraft.trim() });\n\t\t\tif (identity !== loadGeneration) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n"
]
],
"refuses": [
@@ -248,12 +248,12 @@
"class": 2,
"subs": [
[
"\t\tconst gen = loadGeneration;\n\t\tsaveStatus = 'saving';\n\t\ttry {\n\t\t\tconst updated = await api.items.update(wsSlug, targetItem.id, { title",
"\t\tconst gen = loadGeneration;\n\t\tconst freshTitleGen = (g: number, _h = (g = loadGeneration)) => g === loadGeneration;\n\t\tsaveStatus = 'saving';\n\t\ttry {\n\t\t\tconst updated = await api.items.update(wsSlug, targetItem.id, { title"
"\t\tconst gen = loadGeneration;\n\t\tconst saveTok = saves.begin();\n\t\ttry {\n\t\t\tconst updated = await api.items.update(wsSlug, targetItem.id, { title",
"\t\tconst gen = loadGeneration;\n\t\tconst freshTitleGen = (g: number, _h = (g = loadGeneration)) => g === loadGeneration;\n\t\tconst saveTok = saves.begin();\n\t\ttry {\n\t\t\tconst updated = await api.items.update(wsSlug, targetItem.id, { title"
],
[
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n",
"{ title: titleDraft.trim() });\n\t\t\tif (!freshTitleGen(gen)) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n"
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n",
"{ title: titleDraft.trim() });\n\t\t\tif (!freshTitleGen(gen)) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n"
]
],
"refuses": [
@@ -265,12 +265,12 @@
"class": 6,
"subs": [
[
"\t\tconst gen = loadGeneration;\n\t\tsaveStatus = 'saving';\n\t\ttry {\n\t\t\tconst updated = await api.items.update(wsSlug, targetItem.id, { title",
"\t\tconst gen = loadGeneration;\n\t\tconst titleFresh = () => !!api.items.update(wsSlug, targetItem.id, { title: 'x' }) && gen === loadGeneration;\n\t\tsaveStatus = 'saving';\n\t\ttry {\n\t\t\tconst updated = await api.items.update(wsSlug, targetItem.id, { title"
"\t\tconst gen = loadGeneration;\n\t\tconst saveTok = saves.begin();\n\t\ttry {\n\t\t\tconst updated = await api.items.update(wsSlug, targetItem.id, { title",
"\t\tconst gen = loadGeneration;\n\t\tconst titleFresh = () => !!api.items.update(wsSlug, targetItem.id, { title: 'x' }) && gen === loadGeneration;\n\t\tconst saveTok = saves.begin();\n\t\ttry {\n\t\t\tconst updated = await api.items.update(wsSlug, targetItem.id, { title"
],
[
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n",
"{ title: titleDraft.trim() });\n\t\t\tif (!titleFresh() || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n"
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n",
"{ title: titleDraft.trim() });\n\t\t\tif (!titleFresh() || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n"
]
],
"refuses": [
@@ -284,8 +284,8 @@
"class": 6,
"subs": [
[
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n",
"{ title: titleDraft.trim() });\n\t\t\tif (authStore.identityEpoch !== { epoch: authStore.identityEpoch }.epoch) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n"
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n",
"{ title: titleDraft.trim() });\n\t\t\tif (authStore.identityEpoch !== { epoch: authStore.identityEpoch }.epoch) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n"
]
],
"refuses": [
@@ -298,8 +298,8 @@
"class": 6,
"subs": [
[
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n",
"{ title: titleDraft.trim() });\n\t\t\tif (!identityHeld(captureIdentity())) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n"
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n",
"{ title: titleDraft.trim() });\n\t\t\tif (!identityHeld(captureIdentity())) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n"
]
],
"refuses": [
@@ -312,8 +312,8 @@
"class": 6,
"subs": [
[
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n",
"{ title: titleDraft.trim() });\n\t\t\tif (!identityHeld(authStore.identityEpoch)) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n"
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n",
"{ title: titleDraft.trim() });\n\t\t\tif (!identityHeld(authStore.identityEpoch)) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n"
]
],
"refuses": [
@@ -326,12 +326,12 @@
"class": 6,
"subs": [
[
"\t\tconst gen = loadGeneration;\n\t\tsaveStatus = 'saving';\n\t\ttry {\n\t\t\tconst updated = await api.items.update(wsSlug, targetItem.id, { title",
"\t\tconst gen = loadGeneration;\n\t\tlet titleOk = gen === loadGeneration;\n\t\tsaveStatus = 'saving';\n\t\ttry {\n\t\t\tconst updated = await api.items.update(wsSlug, targetItem.id, { title"
"\t\tconst gen = loadGeneration;\n\t\tconst saveTok = saves.begin();\n\t\ttry {\n\t\t\tconst updated = await api.items.update(wsSlug, targetItem.id, { title",
"\t\tconst gen = loadGeneration;\n\t\tlet titleOk = gen === loadGeneration;\n\t\tconst saveTok = saves.begin();\n\t\ttry {\n\t\t\tconst updated = await api.items.update(wsSlug, targetItem.id, { title"
],
[
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n",
"{ title: titleDraft.trim() });\n\t\t\ttitleOk = true;\n\t\t\tif (!titleOk) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n"
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n",
"{ title: titleDraft.trim() });\n\t\t\ttitleOk = true;\n\t\t\tif (!titleOk) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n"
]
],
"refuses": [
@@ -344,8 +344,8 @@
"class": 6,
"subs": [
[
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n",
"{ title: titleDraft.trim() });\n\t\t\tif (!identityHeld(identityEpochAtLoad)) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n"
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n",
"{ title: titleDraft.trim() });\n\t\t\tif (!identityHeld(identityEpochAtLoad)) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n"
]
],
"refuses": [
@@ -358,12 +358,12 @@
"class": 3,
"subs": [
[
"\t\tconst gen = loadGeneration;\n\t\tsaveStatus = 'saving';\n\t\ttry {\n\t\t\tconst updated = await api.items.update(wsSlug, targetItem.id, { title",
"\t\tlet gen = loadGeneration;\n\t\tsaveStatus = 'saving';\n\t\ttry {\n\t\t\tconst updated = await api.items.update(wsSlug, targetItem.id, { title"
"\t\tconst gen = loadGeneration;\n\t\tconst saveTok = saves.begin();\n\t\ttry {\n\t\t\tconst updated = await api.items.update(wsSlug, targetItem.id, { title",
"\t\tlet gen = loadGeneration;\n\t\tconst saveTok = saves.begin();\n\t\ttry {\n\t\t\tconst updated = await api.items.update(wsSlug, targetItem.id, { title"
],
[
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n",
"{ title: titleDraft.trim() });\n\t\t\tfor (gen of [loadGeneration]);\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n"
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n",
"{ title: titleDraft.trim() });\n\t\t\tfor (gen of [loadGeneration]);\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n"
]
],
"refuses": [
@@ -375,8 +375,8 @@
"class": 7,
"subs": [
[
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n",
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n\t\t\tconst titles = api.items.titleStream(wsSlug, targetItem.id);\n\t\t\ttry {\n\t\t\t\tfor await (const t of titles) void t;\n\t\t\t} catch {\n\t\t\t\titem = null;\n\t\t\t}\n"
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n",
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n\t\t\tconst titles = api.items.titleStream(wsSlug, targetItem.id);\n\t\t\ttry {\n\t\t\t\tfor await (const t of titles) void t;\n\t\t\t} catch {\n\t\t\t\titem = null;\n\t\t\t}\n"
]
],
"refuses": [
@@ -389,8 +389,8 @@
"class": 7,
"subs": [
[
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n",
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n\t\t\tconst titles = api.items.titleStream(wsSlug, targetItem.id);\n\t\t\ttry {\n\t\t\t\tfor await (const t of titles) void t;\n\t\t\t} finally {\n\t\t\t\titem = null;\n\t\t\t}\n"
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n",
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n\t\t\tconst titles = api.items.titleStream(wsSlug, targetItem.id);\n\t\t\ttry {\n\t\t\t\tfor await (const t of titles) void t;\n\t\t\t} finally {\n\t\t\t\titem = null;\n\t\t\t}\n"
]
],
"refuses": [
@@ -403,8 +403,8 @@
"class": 5,
"subs": [
[
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n",
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\tconst applyLater = ((_s: TemplateStringsArray) => () => {\n\t\t\t\titem = withInflightTags(updated);\n\t\t\t})`x`;\n\t\t\twindow.addEventListener('focus', applyLater);\n\t\t\tshowSaved();\n"
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n",
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\tconst applyLater = ((_s: TemplateStringsArray) => () => {\n\t\t\t\titem = withInflightTags(updated);\n\t\t\t})`x`;\n\t\t\twindow.addEventListener('focus', applyLater);\n\t\t\tshowSaved(saveTok);\n"
]
],
"refuses": [
@@ -416,12 +416,12 @@
"class": 5,
"subs": [
[
"\tfunction showSaved() {\n",
"\tlet pendingTitleItem: Item | null = null;\n\tfunction applyPendingTitle() {\n\t\tif (pendingTitleItem) item = pendingTitleItem;\n\t}\n\n\tfunction showSaved() {\n"
"\tfunction showSaved(saveTok: SaveToken) {\n",
"\tlet pendingTitleItem: Item | null = null;\n\tfunction applyPendingTitle() {\n\t\tif (pendingTitleItem) item = pendingTitleItem;\n\t}\n\n\tfunction showSaved(saveTok: SaveToken) {\n"
],
[
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n",
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\tpendingTitleItem = withInflightTags(updated);\n\t\t\twindow.addEventListener('focus', updated ? applyPendingTitle : showSaved, { once: true });\n\t\t\tshowSaved();\n"
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n",
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\tpendingTitleItem = withInflightTags(updated);\n\t\t\twindow.addEventListener('focus', updated ? applyPendingTitle : showSaved, { once: true });\n\t\t\tshowSaved(saveTok);\n"
]
],
"refuses": [
@@ -435,8 +435,8 @@
"class": 5,
"subs": [
[
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n",
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\tconst applyTitleLater = () => {\n\t\t\t\titem = withInflightTags(updated);\n\t\t\t};\n\t\t\twindow.addEventListener('focus', applyTitleLater as EventListener);\n\t\t\tshowSaved();\n"
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n",
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\tconst applyTitleLater = () => {\n\t\t\t\titem = withInflightTags(updated);\n\t\t\t};\n\t\t\twindow.addEventListener('focus', applyTitleLater as EventListener);\n\t\t\tshowSaved(saveTok);\n"
]
],
"refuses": [
@@ -450,8 +450,8 @@
"class": 5,
"subs": [
[
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n",
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\tconst applyTitleLater = () => {\n\t\t\t\titem = withInflightTags(updated);\n\t\t\t};\n\t\t\twindow.addEventListener('focus', applyTitleLater!);\n\t\t\tshowSaved();\n"
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n",
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\tconst applyTitleLater = () => {\n\t\t\t\titem = withInflightTags(updated);\n\t\t\t};\n\t\t\twindow.addEventListener('focus', applyTitleLater!);\n\t\t\tshowSaved(saveTok);\n"
]
],
"refuses": [
@@ -465,8 +465,8 @@
"class": 5,
"subs": [
[
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n",
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\tfunction* applyTitleGen() {\n\t\t\t\titem = withInflightTags(updated);\n\t\t\t}\n\t\t\tconst titleIt = applyTitleGen();\n\t\t\twindow.addEventListener('focus', titleIt.next.bind(titleIt));\n\t\t\tshowSaved();\n"
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n",
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\tfunction* applyTitleGen() {\n\t\t\t\titem = withInflightTags(updated);\n\t\t\t}\n\t\t\tconst titleIt = applyTitleGen();\n\t\t\twindow.addEventListener('focus', titleIt.next.bind(titleIt));\n\t\t\tshowSaved(saveTok);\n"
]
],
"refuses": [
@@ -478,8 +478,8 @@
"class": 1,
"subs": [
[
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n",
"{ title: titleDraft.trim() });\n\t\t\ttry {\n\t\t\t\tthrow loadGeneration;\n\t\t\t} catch (gen) {\n\t\t\t\tif (gen !== loadGeneration) return;\n\t\t\t\titem = updated;\n\t\t\t}\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n"
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n",
"{ title: titleDraft.trim() });\n\t\t\ttry {\n\t\t\t\tthrow loadGeneration;\n\t\t\t} catch (gen) {\n\t\t\t\tif (gen !== loadGeneration) return;\n\t\t\t\titem = updated;\n\t\t\t}\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n"
]
],
"refuses": [
@@ -60,8 +60,8 @@
"class": "F1",
"subs": [
[
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n",
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n\t\t\twindow.addEventListener('focus', async () => {\n\t\t\t\titem = withInflightTags(updated);\n\t\t\t}, { once: true });\n"
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n",
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n\t\t\twindow.addEventListener('focus', async () => {\n\t\t\t\titem = withInflightTags(updated);\n\t\t\t}, { once: true });\n"
]
],
"nested": [
@@ -80,8 +80,8 @@
"class": "F1",
"subs": [
[
"\tfunction showSaved() {\n",
"\tfunction showSaved() {\n\t\twindow.addEventListener('focus', async () => {\n\t\t\tsaveStatus = 'idle';\n\t\t}, { once: true });\n"
"\tfunction showSaved(saveTok: SaveToken) {\n",
"\tfunction showSaved(saveTok: SaveToken) {\n\t\twindow.addEventListener('focus', async () => {\n\t\t\tsaveStatus = 'idle';\n\t\t}, { once: true });\n"
]
],
"nested": [
@@ -158,8 +158,8 @@
"class": "F3",
"subs": [
[
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n",
"{ title: titleDraft.trim() });\n\t\t\tconst current = gen === loadGeneration && item?.id === targetItem.id;\n\t\t\tif (!current) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n\t\t\tfor await (const t of api.items.titleStream(wsSlug, targetItem.id)) {\n\t\t\t\tif (!current) return;\n\t\t\t\ttitleDraft = t;\n\t\t\t}\n"
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n",
"{ title: titleDraft.trim() });\n\t\t\tconst current = gen === loadGeneration && item?.id === targetItem.id;\n\t\t\tif (!current) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n\t\t\tfor await (const t of api.items.titleStream(wsSlug, targetItem.id)) {\n\t\t\t\tif (!current) return;\n\t\t\t\ttitleDraft = t;\n\t\t\t}\n"
]
],
"refuses": [
@@ -172,8 +172,8 @@
"class": "F3",
"subs": [
[
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n",
"{ title: titleDraft.trim() });\n\t\t\tconst current = gen === loadGeneration && item?.id === targetItem.id;\n\t\t\tif (!current) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n\t\t\tconst titles = api.items.titleStream(wsSlug, targetItem.id);\n\t\t\ttry {\n\t\t\t\tfor await (const t of titles) void t;\n\t\t\t} catch {\n\t\t\t\tif (current) toastStore.show('Title stream failed', 'error');\n\t\t\t}\n"
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n",
"{ title: titleDraft.trim() });\n\t\t\tconst current = gen === loadGeneration && item?.id === targetItem.id;\n\t\t\tif (!current) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n\t\t\tconst titles = api.items.titleStream(wsSlug, targetItem.id);\n\t\t\ttry {\n\t\t\t\tfor await (const t of titles) void t;\n\t\t\t} catch {\n\t\t\t\tif (current) toastStore.show('Title stream failed', 'error');\n\t\t\t}\n"
]
],
"refuses": [
@@ -216,12 +216,12 @@
"\t\t\tif (ws === wsSlug && identityHeld(startedAt)) tagSuggestions = [];\n"
],
[
"\t\tconst gen = loadGeneration;\n\t\tsaveStatus = 'saving';\n\t\ttry {\n\t\t\tconst updated = await api.items.update(wsSlug, targetItem.id, { title",
"\t\tconst gen = loadGeneration;\n\t\tconst startedAt = loadGeneration;\n\t\tsaveStatus = 'saving';\n\t\ttry {\n\t\t\tconst updated = await api.items.update(wsSlug, targetItem.id, { title"
"\t\tconst gen = loadGeneration;\n\t\tconst saveTok = saves.begin();\n\t\ttry {\n\t\t\tconst updated = await api.items.update(wsSlug, targetItem.id, { title",
"\t\tconst gen = loadGeneration;\n\t\tconst startedAt = loadGeneration;\n\t\tconst saveTok = saves.begin();\n\t\ttry {\n\t\t\tconst updated = await api.items.update(wsSlug, targetItem.id, { title"
],
[
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n",
"{ title: titleDraft.trim() });\n\t\t\tif (!identityHeld(startedAt)) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n"
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n",
"{ title: titleDraft.trim() });\n\t\t\tif (!identityHeld(startedAt)) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n"
]
],
"refuses": [
@@ -234,12 +234,12 @@
"class": "F8",
"subs": [
[
"\t\tconst gen = loadGeneration;\n\t\tsaveStatus = 'saving';\n\t\ttry {\n\t\t\tconst updated = await api.items.update(wsSlug, targetItem.id, { title",
"\t\tconst gen = loadGeneration;\n\t\tconst sameGen = (g: number) => g === loadGeneration;\n\t\tsaveStatus = 'saving';\n\t\ttry {\n\t\t\tconst updated = await api.items.update(wsSlug, targetItem.id, { title"
"\t\tconst gen = loadGeneration;\n\t\tconst saveTok = saves.begin();\n\t\ttry {\n\t\t\tconst updated = await api.items.update(wsSlug, targetItem.id, { title",
"\t\tconst gen = loadGeneration;\n\t\tconst sameGen = (g: number) => g === loadGeneration;\n\t\tconst saveTok = saves.begin();\n\t\ttry {\n\t\t\tconst updated = await api.items.update(wsSlug, targetItem.id, { title"
],
[
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n",
"{ title: titleDraft.trim() });\n\t\t\tif (!sameGen(loadGeneration)) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n"
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n",
"{ title: titleDraft.trim() });\n\t\t\tif (!sameGen(loadGeneration)) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n"
],
[
"\t\t\tconst fresh = await submitWithOCC(false);\n\t\t\tif (!stillCurrent()) return;\n",
@@ -256,12 +256,12 @@
"class": "F8",
"subs": [
[
"\t\tconst gen = loadGeneration;\n\t\tsaveStatus = 'saving';\n\t\ttry {\n\t\t\tconst updated = await api.items.update(wsSlug, targetItem.id, { title",
"\t\tconst gen = loadGeneration;\n\t\tconst current = () => gen === loadGeneration;\n\t\tsaveStatus = 'saving';\n\t\ttry {\n\t\t\tconst updated = await api.items.update(wsSlug, targetItem.id, { title"
"\t\tconst gen = loadGeneration;\n\t\tconst saveTok = saves.begin();\n\t\ttry {\n\t\t\tconst updated = await api.items.update(wsSlug, targetItem.id, { title",
"\t\tconst gen = loadGeneration;\n\t\tconst current = () => gen === loadGeneration;\n\t\tconst saveTok = saves.begin();\n\t\ttry {\n\t\t\tconst updated = await api.items.update(wsSlug, targetItem.id, { title"
],
[
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n",
"{ title: titleDraft.trim() });\n\t\t\tif (!current) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n"
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n",
"{ title: titleDraft.trim() });\n\t\t\tif (!current) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n"
],
[
"\t\t\tconst fresh = await submitWithOCC(false);\n\t\t\tif (!stillCurrent()) return;\n",
@@ -281,8 +281,8 @@
"model": "shadowing: a callback param named like a capture is read as that capture; params stay exempt from the shadowing refusal because switchedAway(targetItem, gen) and its callers need the name, and reusing a capture's name for a different value is the shadowing the header puts outside the model",
"subs": [
[
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n",
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n\t\t\tdialogs.onGeneration((gen: number) => {\n\t\t\t\tif (gen !== loadGeneration) return;\n\t\t\t\titem = withInflightTags(updated);\n\t\t\t});\n"
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n",
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n\t\t\tdialogs.onGeneration((gen: number) => {\n\t\t\t\tif (gen !== loadGeneration) return;\n\t\t\t\titem = withInflightTags(updated);\n\t\t\t});\n"
]
]
},
@@ -292,12 +292,12 @@
"model": "contrived fence: a helper whose whole body is a fence boolean computed before the await is a fence that cannot lapse; no author trying to comply wraps a pre-await boolean in a zero-argument function to read it later",
"subs": [
[
"\t\tconst gen = loadGeneration;\n\t\tsaveStatus = 'saving';\n\t\ttry {\n\t\t\tconst updated = await api.items.update(wsSlug, targetItem.id, { title",
"\t\tconst gen = loadGeneration;\n\t\tconst startedCurrent = gen === loadGeneration;\n\t\tconst stillHere = () => startedCurrent;\n\t\tsaveStatus = 'saving';\n\t\ttry {\n\t\t\tconst updated = await api.items.update(wsSlug, targetItem.id, { title"
"\t\tconst gen = loadGeneration;\n\t\tconst saveTok = saves.begin();\n\t\ttry {\n\t\t\tconst updated = await api.items.update(wsSlug, targetItem.id, { title",
"\t\tconst gen = loadGeneration;\n\t\tconst startedCurrent = gen === loadGeneration;\n\t\tconst stillHere = () => startedCurrent;\n\t\tconst saveTok = saves.begin();\n\t\ttry {\n\t\t\tconst updated = await api.items.update(wsSlug, targetItem.id, { title"
],
[
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n",
"{ title: titleDraft.trim() });\n\t\t\tif (!stillHere()) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n"
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n",
"{ title: titleDraft.trim() });\n\t\t\tif (!stillHere()) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n"
]
]
}
@@ -19,8 +19,8 @@
"id": "B showSaved re-registers itself by name as a visibilitychange listener",
"subs": [
[
"\tfunction showSaved() {\n\t\tsaveStatus = 'saved';\n",
"\tfunction showSaved() {\n\t\t// Nobody sees the indicator on a hidden tab: show it when they come back.\n\t\tif (document.hidden) {\n\t\t\tdocument.addEventListener('visibilitychange', showSaved, { once: true });\n\t\t\treturn;\n\t\t}\n\t\tsaveStatus = 'saved';\n"
"\tfunction showSaved(saveTok: SaveToken) {\n\t\tsaves.succeed(saveTok);\n",
"\tfunction showSaved(saveTok: SaveToken) {\n\t\t// Nobody sees the indicator on a hidden tab: show it when they come back.\n\t\tif (document.hidden) {\n\t\t\tdocument.addEventListener('visibilitychange', showSaved, { once: true });\n\t\t\treturn;\n\t\t}\n\t\tsaves.succeed(saveTok);\n"
]
],
"refuses": [
@@ -31,8 +31,8 @@
"id": "B2 the same through an arrow that calls showSaved",
"subs": [
[
"\tfunction showSaved() {\n\t\tsaveStatus = 'saved';\n",
"\tfunction showSaved() {\n\t\tif (document.hidden) {\n\t\t\tdocument.addEventListener('visibilitychange', () => showSaved(), { once: true });\n\t\t\treturn;\n\t\t}\n\t\tsaveStatus = 'saved';\n"
"\tfunction showSaved(saveTok: SaveToken) {\n\t\tsaves.succeed(saveTok);\n",
"\tfunction showSaved(saveTok: SaveToken) {\n\t\tif (document.hidden) {\n\t\t\tdocument.addEventListener('visibilitychange', () => showSaved(saveTok), { once: true });\n\t\t\treturn;\n\t\t}\n\t\tsaves.succeed(saveTok);\n"
]
],
"refuses": [
@@ -95,8 +95,8 @@
"id": "G a function-valued let rebound in an $effect, called above the fence",
"subs": [
[
"\tfunction showSaved() {\n",
"\tlet applyTitle = (next: Item): void => {\n\t\tvoid next;\n\t};\n\t$effect(() => {\n\t\tapplyTitle = (next: Item) => {\n\t\t\titem = withInflightTags(next);\n\t\t};\n\t});\n\n\tfunction showSaved() {\n"
"\tfunction showSaved(saveTok: SaveToken) {\n",
"\tlet applyTitle = (next: Item): void => {\n\t\tvoid next;\n\t};\n\t$effect(() => {\n\t\tapplyTitle = (next: Item) => {\n\t\t\titem = withInflightTags(next);\n\t\t};\n\t});\n\n\tfunction showSaved(saveTok: SaveToken) {\n"
],
[
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n",
@@ -152,8 +152,8 @@
"model": "a helper already being inlined is not walked again as the callback it hands itself to",
"subs": [
[
"\tfunction showSaved() {\n\t\tsaveStatus = 'saved';\n",
"\tfunction showSaved() {\n\t\t// Nobody sees the indicator on a hidden tab: show it when they come back.\n\t\tif (document.hidden) {\n\t\t\tdocument.addEventListener('visibilitychange', showSaved, { once: true });\n\t\t\treturn;\n\t\t}\n\t\tsaveStatus = 'saved';\n"
"\tfunction showSaved(saveTok: SaveToken) {\n\t\tsaves.succeed(saveTok);\n",
"\tfunction showSaved(saveTok: SaveToken) {\n\t\t// Nobody sees the indicator on a hidden tab: show it when they come back.\n\t\tif (document.hidden) {\n\t\t\tdocument.addEventListener('visibilitychange', showSaved, { once: true });\n\t\t\treturn;\n\t\t}\n\t\tsaves.succeed(saveTok);\n"
]
]
},
@@ -162,8 +162,8 @@
"model": "the same re-entry through a literal that calls the helper",
"subs": [
[
"\tfunction showSaved() {\n\t\tsaveStatus = 'saved';\n",
"\tfunction showSaved() {\n\t\tif (document.hidden) {\n\t\t\tdocument.addEventListener('visibilitychange', () => showSaved(), { once: true });\n\t\t\treturn;\n\t\t}\n\t\tsaveStatus = 'saved';\n"
"\tfunction showSaved(saveTok: SaveToken) {\n\t\tsaves.succeed(saveTok);\n",
"\tfunction showSaved(saveTok: SaveToken) {\n\t\tif (document.hidden) {\n\t\t\tdocument.addEventListener('visibilitychange', () => showSaved(saveTok), { once: true });\n\t\t\treturn;\n\t\t}\n\t\tsaves.succeed(saveTok);\n"
]
]
},
@@ -216,8 +216,8 @@
"model": "a helper is declared once, not bound once: a let rebound outside every unit is inlined with its declared body",
"subs": [
[
"\tfunction showSaved() {\n",
"\tlet applyTitle = (next: Item): void => {\n\t\tvoid next;\n\t};\n\t$effect(() => {\n\t\tapplyTitle = (next: Item) => {\n\t\t\titem = withInflightTags(next);\n\t\t};\n\t});\n\n\tfunction showSaved() {\n"
"\tfunction showSaved(saveTok: SaveToken) {\n",
"\tlet applyTitle = (next: Item): void => {\n\t\tvoid next;\n\t};\n\t$effect(() => {\n\t\tapplyTitle = (next: Item) => {\n\t\t\titem = withInflightTags(next);\n\t\t};\n\t});\n\n\tfunction showSaved(saveTok: SaveToken) {\n"
],
[
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n",
@@ -85,19 +85,40 @@ interface Row {
reviewed?: string;
}
/**
* Why a save-indicator settle may run after an unfenced await (BUG-3044, lead
* ruling): it MUST run on every path, the item-switch and superseded returns
* included, or the outstanding count pins 'saving' and every SSE gate with it.
* What keeps a stale settle from moving the NEXT item's indicator is the
* tracker's epoch, which `saves.reset()` bumps at load. This analysis cannot
* see that and is deliberately not taught to trust it; the claim is pinned by
* saveTracker.svelte.test.ts 'a settle from before a reset (item switch) is
* ignored', which a reset that does not bump the epoch fails.
*/
const SETTLE_WHY =
"save-indicator settle: runs on every path by design; a stale token is a no-op by the tracker's epoch (saveTracker.svelte.test.ts 'a settle from before a reset (item switch) is ignored')";
/** The same, appended to the reason of a row whose own `finally` settles. */
const SETTLE_NOTE = `; its finally settles the save indicator (${SETTLE_WHY})`;
/** The collab flush's `save` callback, exactly (`codeOf`): the settle allowance covers this code and nothing else. */
const COLLAB_SAVE_CODE =
"async ({ ws, itemId, toSave, keepalive }) => { const genAtFlush = loadGeneration; const isForegroundCurrent = (): boolean => !keepalive && !!item && item.id === itemId && genAtFlush === loadGeneration; let saveTok: SaveToken | null = null; if (isForegroundCurrent()) { saveTok = saves.begin(); localLastSaveTime = Date.now(); // Singleton editorStore is owned by the ACTIVE side only: a FROZEN // (peeking) instance stamps its own shadow above but must not touch the // singleton (PLAN-2179 DR-2 / TASK-2181). `saveStatus` is per-instance. if (!peeking) editorStore.setLastSaveTime(Date.now()); } try { // Pass the provider's per-tab op-log cursor (TASK-1319) so the // server can advance the GC watermark when this tab is caught // up to MAX(op-log.id). When the cursor is below MAX (peer ops // not yet applied here) the server leaves the watermark // untouched — the GC sweeper must not delete rows this markdown // doesn't reflect. The cursor reflects the provider tracking // THIS item; if a navigation has already swapped to another // item the foreground flush path bails earlier on dedupe and // this code doesn't run. const opLogCursor = collabProvider && collabProvider.itemID === itemId ? collabProvider.lastOpLogID : undefined; await api.items.flushCollabContent(ws, itemId, toSave, { keepalive, opLogCursor, // BUG-3080: two teardown flushes are both dispatched before // either answers, so only the server can order them — by // this stamp. Deliberately NO expected_seq here: both would // carry the same one, and the older landing first would get // the NEWER refused. clientWrite: nextClientWrite(), }); // Post-await force_refresh check: a force_refresh frame can // arrive WHILE the PATCH is in flight (server already accepted, // items.content already overwritten — the server-side gate // covers the happens-before case where MIN had advanced past // our cursor by the time the request landed). Refuse to record // this flush as authoritative locally — returning 'skipped' // stops the flusher seeding lastFlushedContent from a // known-stale base and skips the saveStatus update. Per Codex // round 10 [P1]. if (forceRefreshInFlight) return 'skipped'; if (isForegroundCurrent()) { localLastSaveTime = Date.now(); localDirty = false; // Singleton editorStore: active side only (PLAN-2179 / TASK-2181) — // a frozen preview's remote-op flush updates only its shadow above. if (!peeking) { editorStore.setLastSaveTime(Date.now()); editorStore.setDirty(false); } if (saveTok) showSaved(saveTok); } return 'flushed'; } catch (e) { // Superseded (BUG-3080): a newer flush of this tab already // landed. Not an error, and not recorded as the last flushed // content either — this body is not what the server holds. // Classified inline rather than by a call: this catch runs after an // await with no fence of its own, and the answer is needed on // every arm, foreground or not. if (e instanceof PadApiError && e.code === 'superseded_write') return 'skipped'; if (isForegroundCurrent()) { toastStore.show('Failed to save content', 'error'); } return 'failed'; } finally { // The force_refresh and superseded returns included (BUG-3044). if (saveTok) saves.settle(saveTok); } }";
/** Top-level `async function` declarations, by name. */
const ASYNC_FUNCTIONS: Record<string, Row> = {
adoptOrConvergeToLiveCollection: { reviewed: '5a647061f75f', why: 'myGen against loadGeneration before adopting' },
reconcileCollectionSegment: { reviewed: 'dbb36caf0452', why: 'identityHeld after the list fetch; retags and navigates only under it' },
jumpToSection: { reviewed: '17407aeb431b', why: 'switches this instance\'s tab and scrolls to an anchor', may: ['document.getElementById', 'document.getElementById(anchorId).scrollIntoView'] },
ensureGraphComp: { reviewed: 'c1565cfb8a18', why: 'lazy-loads a component module into this instance', may: ['ItemGraphComp', 'graphLoadError'] },
handleCopyRef: { reviewed: 'fb3adcaf167a', why: 'switchedAway before the copied flag' },
loadData: { reviewed: '8ed3c63b1d7d', why: 'IS the load: myGen against loadGeneration after every await; the BUG-3198 re-read of a changed item runs after the install, keyed on the installed item id, and fences itself on itemGen' },
handleCopyRef: { reviewed: 'a5e0a0394fcc', why: 'switchedAway before the copied flag' },
loadData: { reviewed: '8eb393047b3e', why: 'IS the load: myGen against loadGeneration after every await; the BUG-3198 re-read of a changed item runs after the install, keyed on the installed item id, and fences itself on itemGen' },
startEditTitle: { reviewed: '17f04352d420', why: 'focuses and sizes the input it opened synchronously', may: ['el', 'titleInputEl.focus', 'titleInputEl.setSelectionRange'] },
saveTitle: { reviewed: 'ba5a2acf0d81', why: 'gen against loadGeneration on both arms, and again after the tick that resizes a reopened editor (BUG-3115)' },
saveTitle: { reviewed: 'cef019550a0a', why: 'gen against loadGeneration on both arms, and again after the tick that resizes a reopened editor (BUG-3115)' + SETTLE_NOTE, may: ['saves.settle'] },
updateField: {
reviewed: '4ae2212df0a5',
why: 'stillCurrent() on every arm, the OCC refetch and the open-children confirm',
reviewed: '55dd4fdc7824',
why: 'stillCurrent() on every arm, the OCC refetch and the open-children confirm' + SETTLE_NOTE,
may: ['saves.settle'],
callbacks: {
'submitOrderedOCC({send})': {
may: ['api.items.update'],
@@ -117,9 +138,9 @@ const ASYNC_FUNCTIONS: Record<string, Row> = {
},
},
flushTagSaver: {
reviewed: 'c2b3dd381073',
why: 'identityHeld(saver.epoch) before every commit and send (each batch goes through sendTagBatch, which fences its own conflict path); the unfenced writes are to this burst\'s own identity-stamped record, and the finally deletes that record only if the registry still holds it (the get)',
may: ['saver', 'tagSavers.get', 'tagSavers.delete'],
reviewed: '310eef4ca57e',
why: 'identityHeld(saver.epoch) before every commit and send (each batch goes through sendTagBatch, which fences its own conflict path); the unfenced writes are to this burst\'s own identity-stamped record, and the finally deletes that record only if the registry still holds it (the get)' + SETTLE_NOTE,
may: ['saves.settle', 'saver', 'tagSavers.get', 'tagSavers.delete'],
},
// BUG-3143: one tag batch, re-derived and retried on a conflict. Writes
// only this burst's own record (confirmed, token), like flushTagSaver.
@@ -134,12 +155,12 @@ const ASYNC_FUNCTIONS: Record<string, Row> = {
loadTagSuggestions: { reviewed: 'b03a62bf7294', why: 'identityHeld after the fetch; the identity listener re-runs it' },
stampSourceUrl: { reviewed: 'b9d3f9d400ae', why: 'switchedAway on both arms' },
refreshFromSource: { reviewed: '205f77aba079', why: 'switchedAway on every arm; the frozen-editor notice (BUG-2177) is after that check' },
updateAssignedUser: { reviewed: '86f060522de1', why: 'gen against loadGeneration on both arms' },
updateAgentRole: { reviewed: 'ae4f320cdbab', why: 'gen against loadGeneration on both arms' },
updateAssignedUser: { reviewed: '5e4128e45ae7', why: 'gen against loadGeneration on both arms' + SETTLE_NOTE, may: ['saves.settle'] },
updateAgentRole: { reviewed: '9b0a6af92cc2', why: 'gen against loadGeneration on both arms' + SETTLE_NOTE, may: ['saves.settle'] },
flushRawIfPending: {
reviewed: '0420c7e37b44',
why: 'genAtFlush against loadGeneration after each PATCH; the re-entrancy waiter returns state; the finally clears this drain\'s own in-flight flag',
may: ['rawFlushInFlight'],
reviewed: '425804645744',
why: 'genAtFlush against loadGeneration after each PATCH; the re-entrancy waiter returns state; the finally clears this drain\'s own in-flight flag' + SETTLE_NOTE,
may: ['saves.settle', 'rawFlushInFlight'],
bareAwaits: ['await new Promise((r) => setTimeout(r, 50));'],
},
refreshLinksPreservingOnFailure: {
@@ -153,10 +174,10 @@ const ASYNC_FUNCTIONS: Record<string, Row> = {
// BUG-3036: the re-read an SSE event or sync pass deferred while a save was in
// flight. The SSE item_updated shape: itemGen and the item id after each await.
runOwedRefresh: { reviewed: '1ede6aa08b69', why: 'destroyed, itemGen and the item id after each await, as the SSE item_updated re-read it stands in for (destroyed since BUG-3198: the load path calls it too)' },
flushCollabBeforeRestore: { reviewed: '58150a653ea6', why: 'identityHeld before its failure toast' },
flushCollabBeforeRestore: { reviewed: 'f9f7621c1dd2', why: 'identityHeld before its failure toast' },
closeCopyDialog: { reviewed: '1c925f081a26', why: 'restores focus after closing synchronously', may: ['paneMenuTrigger.focus'] },
closePushDialog: { reviewed: 'bda8c7529677', why: 'restores focus after closing synchronously', may: ['paneMenuTrigger.focus'] },
flushContentBeforeCopy: { reviewed: '5bea1cb961b5', why: 'returns a boolean to the dialog' },
flushContentBeforeCopy: { reviewed: 'd7083bf9954c', why: 'returns a boolean to the dialog' },
handleCopied: { reviewed: 'f35fb099838d', why: 'switchedAway before adopting the refreshed item' },
handleDelete: { reviewed: 'b66adcf84794', why: 'switchedAway on both arms' },
handleRestore: { reviewed: 'c3a1732554a4', why: 'switchedAway on every arm' },
@@ -187,15 +208,22 @@ interface SignedRow extends Row {
/** Async functions that are not top-level declarations, in the script. */
const NESTED: SignedRow[] = [
{ body: /event\.type === 'collection_updated'/, why: 'SSE: callbackGen after the collection fetch, itemGen on item branches; a change or a migration arriving before this load installs its item is only RECORDED (BUG-3198), synchronously, before any await', reviewed: 'fd626753eaf9' },
{ body: /result\.type === 'caught_up'/, why: 'sync: callbackGen after the reconciliation, itemGen on item branches; a result arriving before this load installs its item is only RECORDED (BUG-3198), synchronously, before any await', reviewed: '30f73f3a3ece' },
{ body: /flushCollabContent\(/, why: 'collab save: isForegroundCurrent (genAtFlush) before UI feedback', reviewed: '6dba5f66f347' },
{ body: /event\.type === 'collection_updated'/, why: 'SSE: callbackGen after the collection fetch, itemGen on item branches; a change or a migration arriving before this load installs its item is only RECORDED (BUG-3198), synchronously, before any await', reviewed: '87c80cea46db' },
{ body: /result\.type === 'caught_up'/, why: 'sync: callbackGen after the reconciliation, itemGen on item branches; a result arriving before this load installs its item is only RECORDED (BUG-3198), synchronously, before any await', reviewed: 'a7ae81a0da33' },
{
body: /flushCollabContent\(/,
in: '(top level)',
code: COLLAB_SAVE_CODE,
why: 'collab save: isForegroundCurrent (genAtFlush) before UI feedback' + SETTLE_NOTE,
reviewed: 'f197b2c9f0ef',
may: ['saves.settle'],
},
];
/** Async functions in the markup. */
const MARKUP: SignedRow[] = [
{ body: /startGen/, why: 'Rich toggle: startGen against loadGeneration after each await', reviewed: '63a9cce2ed93' },
{ body: /genAtToggle/, why: 'Markdown toggle: genAtToggle against loadGeneration after each await', reviewed: '5c5b8240c566' },
{ body: /genAtToggle/, why: 'Markdown toggle: genAtToggle against loadGeneration after each await', reviewed: '6ad6c1318246' },
];
const collapse = (s: string) => s.replace(/\s+/g, ' ');
@@ -361,28 +389,27 @@ const CONTINUATIONS: SignedRow[] = [
why: 'rename heal failure: clears only the bridge object this heal installed', reviewed: '9a33138e4937',
may: ['renameOverride'],
},
{ call: /^setTimeout\($/, body: /copied = false/, why: 'copy-flag reset: switchedAway', reviewed: '199047839886' },
{ call: /api\.items\.get\(wsSlug, itemSlug\)\.catch\($/, body: /./, why: 'loadData item fetch: sets a flag local to that load and re-throws', reviewed: '1567ed00227b' },
{ call: /^setTimeout\($/, body: /copied = false/, why: 'copy-flag reset: switchedAway', reviewed: '8a8d4754017f' },
{ call: /api\.items\.get\(wsSlug, itemSlug\)\.catch\($/, body: /./, why: 'loadData item fetch: sets a flag local to that load and re-throws', reviewed: 'f2267a80cece' },
{
call: /^setTimeout\($/,
body: /staleConnecting = true/,
in: '$effect(…)',
code: "() => { if (collabProvider?.state === 'connecting' && !hasEverSynced) { staleConnecting = true; } }",
why: 'connection state of this instance\'s own provider', reviewed: 'c4d9f3230509',
why: 'connection state of this instance\'s own provider', reviewed: 'b0f9f4cc5f23',
may: ['staleConnecting'],
},
{ call: /\.get\(refreshCtx\.wsSlug, refreshCtx\.itemId\) \.then\($/, body: /./, why: 'force-refresh fetch: refreshGen against loadGeneration', reviewed: '0a1fffba6d45' },
{ call: /forceRefreshNonce \+= 1; \}\) \.catch\($/, body: /./, why: 'force-refresh failure: refreshGen against loadGeneration', reviewed: 'e54e21b2c59d' },
{ call: /\.get\(refreshCtx\.wsSlug, refreshCtx\.itemId\) \.then\($/, body: /./, why: 'force-refresh fetch: refreshGen against loadGeneration', reviewed: '85c402dd6ca9' },
{ call: /forceRefreshNonce \+= 1; \}\) \.catch\($/, body: /./, why: 'force-refresh failure: refreshGen against loadGeneration', reviewed: 'b34c4479bb79' },
{
call: /^setTimeout\($/,
body: /teardownFlushed/,
in: 'onBeforeUnload',
code: '() => { teardownFlushed = false; }',
why: 're-arms the BUG-3005 teardown latch, itself identity-checked', reviewed: '48cada93d389',
why: 're-arms the BUG-3005 teardown latch, itself identity-checked', reviewed: 'e598de5a2771',
may: ['teardownFlushed'],
},
{ call: /^queueMicrotask\($/, body: /./, why: 'collab lazy seed: refuses a retired or re-identified context first', reviewed: 'bb4b5bef7eec' },
{ call: /^setTimeout\($/, body: /saveStatus/, in: 'showSaved', code: "() => { saveStatus = 'idle'; }", why: 'cosmetic save-indicator reset', reviewed: 'bd7f1439c3d2', may: ['saveStatus'] },
{ call: /^queueMicrotask\($/, body: /./, why: 'collab lazy seed: refuses a retired or re-identified context first', reviewed: '197a4a45d219' },
{ call: /^tick\(\)\.then\($/, body: /./, why: 'schedules a focus frame; commits nothing itself', reviewed: 'b6f7655cb302' },
{
call: /^requestAnimationFrame\($/,
@@ -396,16 +423,34 @@ const CONTINUATIONS: SignedRow[] = [
call: /^setTimeout\($/,
body: /content: toSave, client_write: nextClientWrite\(\) \}\)\.then/,
in: 'handleContentUpdate',
why: 'content debounce: loadData clears this timer before its first await, so the callback never runs across a load', reviewed: '1302904e1805',
why: 'content debounce: loadData clears this timer before its first await, so the callback never runs across a load', reviewed: 'd05baa756ebd',
startSafe: true,
pin: (src, unit) => clearsBeforeFirstAwait(src, 'loadData', 'contentDebounceTimer') ?? assignedTo(src, unit, 'contentDebounceTimer'),
},
{ call: /\{ content: toSave, client_write: nextClientWrite\(\) \}\)\.then\($/, body: /^\(\) =>/, why: 'content save: switchedAway', reviewed: '77120c902e52' },
{ call: /showSaved\(\); \}\)\.catch\($/, body: /./, why: 'content save failure: switchedAway', reviewed: '541348670a2a' },
{ call: /\{ keepalive: true \}\) \.then\($/, body: /./, why: 'raw keepalive save: genAtSave against loadGeneration', reviewed: 'dcdd26c60264' },
{ call: /localDirty = false; \} \}\) \.catch\($/, body: /^\(\) => \{\}$/, why: 'raw keepalive failure: empty', reviewed: '61988786e8bf' },
{ call: /reqItemId, \{ content: toSave, client_write: nextClientWrite\(\) \}\)\.then\($/, body: /./, why: 'raw foreground save: genAtSave against loadGeneration', reviewed: 'ddb4a299f255' },
{ call: /content: item\.content \}\); \} \}\)\.catch\($/, body: /./, why: 'raw foreground failure: genAtSave against loadGeneration', reviewed: '3c3619ac0a37' },
{ call: /\{ content: toSave, client_write: nextClientWrite\(\) \}\)\.then\($/, body: /^\(\) =>/, why: 'content save: switchedAway', reviewed: '160532c3b115' },
{ call: /showSaved\(saveTok\); \}\)\.catch\($/, body: /./, why: 'content save failure: switchedAway', reviewed: '73e9f73724f5' },
{
call: /\.finally\($/,
body: /^\(\) => saves\.settle\(saveTok\)$/,
in: 'setTimeout(…)',
code: '() => saves.settle(saveTok)',
why: SETTLE_WHY,
reviewed: '4fbcd658ebba',
may: ['saves.settle'],
},
{ call: /\{ keepalive: true \}\) \.then\($/, body: /./, why: 'raw keepalive save: genAtSave against loadGeneration', reviewed: '758b235c3705' },
{ call: /localDirty = false; \} \}\) \.catch\($/, body: /^\(\) => \{\}$/, why: 'raw keepalive failure: empty', reviewed: '66b6e4855355' },
{ call: /reqItemId, \{ content: toSave, client_write: nextClientWrite\(\) \}\)\.then\($/, body: /./, why: 'raw foreground save: genAtSave against loadGeneration', reviewed: '109114fbb1b8' },
{ call: /content: item\.content \}\); \} \}\)\.catch\($/, body: /./, why: 'raw foreground failure: genAtSave against loadGeneration', reviewed: 'a47d5c0c6f20' },
{
call: /\.finally\($/,
body: /^\(\) => saves\.settle\(saveTok\)$/,
in: '{save}',
code: '() => saves.settle(saveTok)',
why: SETTLE_WHY,
reviewed: '0ef0106d13cc',
may: ['saves.settle'],
},
];
/**
@@ -415,7 +460,7 @@ const CONTINUATIONS: SignedRow[] = [
* one is an edit to every unit that reaches it.
*/
const HELPERS: Record<string, string> = {
primeCanonicalSeed: 'bd355af5a470',
primeCanonicalSeed: '9d90b6bf0245',
adoptCollection: 'ab38368cd8fb',
adoptServerItem: '198d4de5b450',
applyProgress: 'b28014625819',
@@ -426,11 +471,11 @@ const HELPERS: Record<string, string> = {
identityHeld: '1c5505d51f73',
navigateToCollectionRoot: '64dce75693d3',
refreshPrintMeta: '2312cc481ca5',
runTeardownFlush: '5718e613cefd',
runTeardownFlush: '8251de42632d',
// BUG-3124 unit B: the cursor-advance settle. Synchronous; arms the flusher's
// single timer only for the current, identity-held context.
settleCollabIfCurrent: 'c3f36ad978c9',
showSaved: '3f1ef91fac07',
showSaved: '456dd972dcf5',
settleCollabIfCurrent: '438f4aed6d2f',
// BUG-2367: the move's success toast, naming any not_unique drop.
// Synchronous; reads only its arguments and writes only the toast store.
showMovedToast: '6a32f06fbd9f',
@@ -960,19 +1005,19 @@ const LOAD_DATA_CLEAR = '\t\tclearTimeout(contentDebounceTimer);\n\t\tcontentDeb
/** saveTitle's capture, made reassignable. */
const TITLE_GEN_LET: [string, string] = [
"\t\tconst gen = loadGeneration;\n\t\tsaveStatus = 'saving';\n\t\ttry {\n\t\t\tconst updated = await api.items.update(wsSlug, targetItem.id, { title",
"\t\tlet gen = loadGeneration;\n\t\tsaveStatus = 'saving';\n\t\ttry {\n\t\t\tconst updated = await api.items.update(wsSlug, targetItem.id, { title",
"\t\tconst gen = loadGeneration;\n\t\tconst saveTok = saves.begin();\n\t\ttry {\n\t\t\tconst updated = await api.items.update(wsSlug, targetItem.id, { title",
"\t\tlet gen = loadGeneration;\n\t\tconst saveTok = saves.begin();\n\t\ttry {\n\t\t\tconst updated = await api.items.update(wsSlug, targetItem.id, { title",
];
/** saveTitle records an identity stamp at entry, before its await. */
const TITLE_STAMP_AT_ENTRY: [string, string] = [
"\t\tconst gen = loadGeneration;\n\t\tsaveStatus = 'saving';\n\t\ttry {\n\t\t\tconst updated = await api.items.update(wsSlug, targetItem.id, { title",
"\t\tconst gen = loadGeneration;\n\t\tconst snap = { epoch: captureIdentity() };\n\t\tsaveStatus = 'saving';\n\t\ttry {\n\t\t\tconst updated = await api.items.update(wsSlug, targetItem.id, { title",
"\t\tconst gen = loadGeneration;\n\t\tconst saveTok = saves.begin();\n\t\ttry {\n\t\t\tconst updated = await api.items.update(wsSlug, targetItem.id, { title",
"\t\tconst gen = loadGeneration;\n\t\tconst snap = { epoch: captureIdentity() };\n\t\tconst saveTok = saves.begin();\n\t\ttry {\n\t\t\tconst updated = await api.items.update(wsSlug, targetItem.id, { title",
];
/** saveTitle's fence and the two commits under it: the anchor most round-5 edits rewrite. */
const TITLE_FENCE_AND_COMMITS =
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n";
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n";
describe('ItemDetail AST guard: round 4\'s edits are all refused (lead ruling, condition 1)', () => {
// Without a clean baseline every mutant is "refused" for free.
@@ -1009,17 +1054,17 @@ describe('ItemDetail AST guard: round 4\'s edits are all refused (lead ruling, c
// read as holding after that await.
{
id: 'R5 E1 a fence before an await in one && test',
subs: [[TITLE_FENCE_AND_COMMITS, "{ title: titleDraft.trim() });\n\t\t\tif (gen === loadGeneration && (await dialogs.confirm('Keep the new title?'))) {\n\t\t\t\titem = withInflightTags(updated);\n\t\t\t\tshowSaved();\n\t\t\t}\n"]],
subs: [[TITLE_FENCE_AND_COMMITS, "{ title: titleDraft.trim() });\n\t\t\tif (gen === loadGeneration && (await dialogs.confirm('Keep the new title?'))) {\n\t\t\t\titem = withInflightTags(updated);\n\t\t\t\tshowSaved(saveTok);\n\t\t\t}\n"]],
refuses: ['saveTitle()', 'assigns item after an unfenced await'],
},
{
id: 'R5 E1b a fence boolean whose initialiser awaits after the fence',
subs: [[TITLE_FENCE_AND_COMMITS, "{ title: titleDraft.trim() });\n\t\t\tconst keep = gen === loadGeneration && (await dialogs.confirm('Keep the new title?'));\n\t\t\tif (!keep) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n"]],
subs: [[TITLE_FENCE_AND_COMMITS, "{ title: titleDraft.trim() });\n\t\t\tconst keep = gen === loadGeneration && (await dialogs.confirm('Keep the new title?'));\n\t\t\tif (!keep) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n"]],
refuses: ['saveTitle()', 'assigns item after an unfenced await'],
},
{
id: 'R5 E1c the early-return form: fence, then an await, in one || test',
subs: [[TITLE_FENCE_AND_COMMITS, "{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id || !(await dialogs.confirm('Keep the new title?'))) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n"]],
subs: [[TITLE_FENCE_AND_COMMITS, "{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id || !(await dialogs.confirm('Keep the new title?'))) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n"]],
refuses: ['saveTitle()', 'assigns item after an unfenced await'],
},
// Round 5 P1-2: a callback handed to anything but the deferring names was
@@ -1042,7 +1087,7 @@ describe('ItemDetail AST guard: round 4\'s edits are all refused (lead ruling, c
},
{
id: 'R5 E8 a commit inside a requestIdleCallback callback',
subs: [[TITLE_FENCE_AND_COMMITS, "{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\trequestIdleCallback(() => {\n\t\t\t\titem = withInflightTags(updated);\n\t\t\t\tshowSaved();\n\t\t\t});\n"]],
subs: [[TITLE_FENCE_AND_COMMITS, "{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\trequestIdleCallback(() => {\n\t\t\t\titem = withInflightTags(updated);\n\t\t\t\tshowSaved(saveTok);\n\t\t\t});\n"]],
refuses: ['saveTitle()', 'callback requestIdleCallback(…)', 'assigns item after an unfenced await'],
},
{
@@ -1077,7 +1122,7 @@ describe('ItemDetail AST guard: round 4\'s edits are all refused (lead ruling, c
id: 'R5 P1-2 a function declared under a name used twice, passed by name',
subs: [
[TITLE_FENCE_AND_COMMITS, "{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\tconst onTitleClose = () => {\n\t\t\t\titem = withInflightTags(updated);\n\t\t\t};\n\t\t\twindow.addEventListener('blur', onTitleClose);\n"],
['\tfunction showSaved() {\n', '\tfunction showSaved() {\n\t\tconst onTitleClose = () => {};\n\t\tvoid onTitleClose;\n'],
['\tfunction showSaved(saveTok: SaveToken) {\n', '\tfunction showSaved(saveTok: SaveToken) {\n\t\tconst onTitleClose = () => {};\n\t\tvoid onTitleClose;\n'],
],
refuses: ['saveTitle()', 'assigns item after an unfenced await'],
},
@@ -1092,14 +1137,14 @@ describe('ItemDetail AST guard: round 4\'s edits are all refused (lead ruling, c
// Only an empty Set/Map is an allowed constructor default (lead ruling on
// checkpoint 63, condition 3).
id: 'R6 a default value that constructs something with effects',
subs: [['\tfunction showSaved() {\n', "\tfunction showSaved(stream = new TitleStream()) {\n\t\tvoid stream;\n"]],
subs: [['\tfunction showSaved(saveTok: SaveToken) {\n', "\tfunction showSaved(stream = new TitleStream()) {\n\t\tvoid stream;\n"]],
refuses: ['default value calls or assigns'],
},
{
// A fence boolean stops being one when it is written (round 6 ruling B,
// class 6); here the write, not an await, is what makes it stale.
id: 'R6 a fence boolean overwritten after it is computed',
subs: [[TITLE_FENCE_AND_COMMITS, "{ title: titleDraft.trim() });\n\t\t\tlet titleOk = gen === loadGeneration;\n\t\t\ttitleOk = true;\n\t\t\tif (!titleOk) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n"]],
subs: [[TITLE_FENCE_AND_COMMITS, "{ title: titleDraft.trim() });\n\t\t\tlet titleOk = gen === loadGeneration;\n\t\t\ttitleOk = true;\n\t\t\tif (!titleOk) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n"]],
refuses: ['saveTitle()', 'assigns item after an unfenced await'],
},
{
@@ -1107,7 +1152,7 @@ describe('ItemDetail AST guard: round 4\'s edits are all refused (lead ruling, c
id: 'R6 loadGeneration compared against an identity capture',
subs: [
[TITLE_GEN_LET[0], TITLE_GEN_LET[0].replace('\t\tsaveStatus', '\t\tconst identity = captureIdentity();\n\t\tsaveStatus')],
[TITLE_FENCE_AND_COMMITS, '{ title: titleDraft.trim() });\n\t\t\tif (loadGeneration !== identity) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n'],
[TITLE_FENCE_AND_COMMITS, '{ title: titleDraft.trim() });\n\t\t\tif (loadGeneration !== identity) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n'],
],
refuses: ['saveTitle()', 'assigns item after an unfenced await'],
},
@@ -1160,12 +1205,12 @@ describe('ItemDetail AST guard: round 4\'s edits are all refused (lead ruling, c
},
{
id: 'R5 E3 a commit after a for-of over an awaited list whose body always returns',
subs: [[TITLE_FENCE_AND_COMMITS, "{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\tfor (const newer of await api.items.newerVersions(wsSlug, targetItem.id)) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n"]],
subs: [[TITLE_FENCE_AND_COMMITS, "{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\tfor (const newer of await api.items.newerVersions(wsSlug, targetItem.id)) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n"]],
refuses: ['saveTitle()', 'assigns item after an unfenced await'],
},
{
id: 'R5 P2-2 a commit after a while whose test awaits and whose body always returns',
subs: [[TITLE_FENCE_AND_COMMITS, "{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\twhile (await api.items.hasNewer(wsSlug, targetItem.id)) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n"]],
subs: [[TITLE_FENCE_AND_COMMITS, "{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\twhile (await api.items.hasNewer(wsSlug, targetItem.id)) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n"]],
refuses: ['saveTitle()', 'assigns item after an unfenced await'],
},
{
@@ -1180,7 +1225,7 @@ describe('ItemDetail AST guard: round 4\'s edits are all refused (lead ruling, c
},
{
id: 'R5 P2-3 a commit after a switch with no default whose test awaits',
subs: [[TITLE_FENCE_AND_COMMITS, "{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\tswitch (titleDraft) {\n\t\t\t\tcase await api.items.canonicalTitle(wsSlug):\n\t\t\t\t\treturn;\n\t\t\t}\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n"]],
subs: [[TITLE_FENCE_AND_COMMITS, "{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\tswitch (titleDraft) {\n\t\t\t\tcase await api.items.canonicalTitle(wsSlug):\n\t\t\t\t\treturn;\n\t\t\t}\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n"]],
refuses: ['saveTitle()', 'assigns item after an unfenced await'],
},
// Round 5 P2-4: an identity comparison between two STAMPS proves nothing.
@@ -1267,15 +1312,15 @@ describe('ItemDetail AST guard: round 4\'s edits are all refused (lead ruling, c
// A block's bindings hold only inside that block: a `const item` in
// one branch excuses no `item =` outside it.
id: 'R5 P1-3 a block-scoped declaration excuses a write outside its block',
subs: [[TITLE_FENCE_AND_COMMITS, "{ title: titleDraft.trim() });\n\t\t\tif (updated) {\n\t\t\t\tconst item = updated;\n\t\t\t\tvoid item;\n\t\t\t}\n\t\t\titem = withInflightTags(updated);\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\tshowSaved();\n"]],
subs: [[TITLE_FENCE_AND_COMMITS, "{ title: titleDraft.trim() });\n\t\t\tif (updated) {\n\t\t\t\tconst item = updated;\n\t\t\t\tvoid item;\n\t\t\t}\n\t\t\titem = withInflightTags(updated);\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\tshowSaved(saveTok);\n"]],
refuses: ['saveTitle()', 'assigns item after an unfenced await — item = withInflightTags(updated)'],
},
{
// An inlined helper resolves names in ITS scopes: a caller's local that
// shares a name with the state the helper writes excuses nothing.
id: "R5 P1-3 a caller's local does not excuse the same name inside an inlined helper",
subs: [[TITLE_FENCE_AND_COMMITS, "{ title: titleDraft.trim() });\n\t\t\tconst saveStatus = updated.title;\n\t\t\tshowSaved();\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n"]],
refuses: ['saveTitle()', 'showSaved() -> assigns saveStatus after an unfenced await'],
subs: [[TITLE_FENCE_AND_COMMITS, "{ title: titleDraft.trim() });\n\t\t\tconst saveStatus = updated.title;\n\t\t\tshowSaved(saveTok);\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n"]],
refuses: ['saveTitle()', 'showSaved() -> calls saves.succeed after an unfenced await'],
},
];
it('a callback allowance that names no callback the unit creates is refused', () => {
@@ -1358,7 +1403,7 @@ describe('ItemDetail AST guard: round 4\'s edits are all refused (lead ruling, c
id: 'a stamp minted after the await and compared at once',
model:
'contrived fence: a stamp is trusted to have been recorded earlier; building one from a live read right before comparing it to a live read is a fence that cannot fail, which no author trying to comply writes',
subs: [[TITLE_FENCE_AND_COMMITS, '{ title: titleDraft.trim() });\n\t\t\tconst fresh = { epoch: captureIdentity() };\n\t\t\tif (authStore.identityEpoch !== fresh.epoch) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n']],
subs: [[TITLE_FENCE_AND_COMMITS, '{ title: titleDraft.trim() });\n\t\t\tconst fresh = { epoch: captureIdentity() };\n\t\t\tif (authStore.identityEpoch !== fresh.epoch) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n']],
},
// Round 7 (checkpoint 67): the reviewer's out-of-model findings.
...(round7.gaps as Array<{ id: string; class: string; model: string; subs: string[][] }>).map((g) => ({
@@ -1411,8 +1456,8 @@ describe('ItemDetail AST guard: round 4\'s edits are all refused (lead ruling, c
// A catch param is a local of the handler, even though the handler is
// entered after an await.
id: 'a write to the catch param in an unfenced handler',
old: "\t\t} catch (err: any) {\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\tsaveStatus = 'idle';\n",
new: "\t\t} catch (err: any) {\n\t\t\terr = null;\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\tsaveStatus = 'idle';\n",
old: "\t\t} catch (err: any) {\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n",
new: "\t\t} catch (err: any) {\n\t\t\terr = null;\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n",
},
{
// An inlined helper declared inside a block sees that block's names.
@@ -1428,8 +1473,8 @@ describe('ItemDetail AST guard: round 4\'s edits are all refused (lead ruling, c
},
{
id: 'a commit moved BELOW its fence stays accepted',
old: "{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n",
new: "{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\tshowSaved();\n\t\t\titem = withInflightTags(updated);\n",
old: "{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n",
new: "{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\tshowSaved(saveTok);\n\t\t\titem = withInflightTags(updated);\n",
},
{
// A stamp rooted in a local of the unit is a stamp inside a fence
@@ -1438,14 +1483,14 @@ describe('ItemDetail AST guard: round 4\'s edits are all refused (lead ruling, c
id: 'a fence boolean over a stamp the unit recorded before its await',
old: TITLE_STAMP_AT_ENTRY[0],
new: TITLE_STAMP_AT_ENTRY[1],
also: [[TITLE_FENCE_AND_COMMITS, "{ title: titleDraft.trim() });\n\t\t\tconst held = authStore.identityEpoch === snap.epoch;\n\t\t\tif (!held || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n"]],
also: [[TITLE_FENCE_AND_COMMITS, "{ title: titleDraft.trim() });\n\t\t\tconst held = authStore.identityEpoch === snap.epoch;\n\t\t\tif (!held || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n"]],
},
{
// ...and inside a helper's body, read in the helper's scope.
id: 'a fence helper over a stamp the unit recorded before its await',
old: TITLE_STAMP_AT_ENTRY[0],
new: TITLE_STAMP_AT_ENTRY[1].replace('\t\tsaveStatus', '\t\tconst snapHeld = () => authStore.identityEpoch === snap.epoch;\n\t\tsaveStatus'),
also: [[TITLE_FENCE_AND_COMMITS, "{ title: titleDraft.trim() });\n\t\t\tif (!snapHeld() || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n"]],
new: TITLE_STAMP_AT_ENTRY[1].replace('\t\tconst saveTok', '\t\tconst snapHeld = () => authStore.identityEpoch === snap.epoch;\n\t\tconst saveTok'),
also: [[TITLE_FENCE_AND_COMMITS, "{ title: titleDraft.trim() });\n\t\t\tif (!snapHeld() || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n"]],
},
];
it.each(ACCEPTED.map((c) => [c.id, c] as const))('control accepted: %s', (_id, c) => {
@@ -1606,7 +1651,7 @@ describe('ItemDetail identity gate: a fenced unit cannot change without a re-rea
it('CONTROL: an edit to a fenced unit costs exactly one hash bump', () => {
const code = SOURCE.replace(
TITLE_FENCE_AND_COMMITS,
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\tshowSaved();\n\t\t\titem = withInflightTags(updated);\n"
"{ title: titleDraft.trim() });\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\tshowSaved(saveTok);\n\t\t\titem = withInflightTags(updated);\n"
);
const first = refusals(code);
expect(first).toHaveLength(1);
@@ -1649,10 +1694,10 @@ describe('ItemDetail identity gate: a fenced unit cannot change without a re-rea
it('a function value rebound outside every unit changes the hash of the unit that only NAMES it (round 9 F4)', () => {
// Step 1: a reviewed base where saveTitle hands `applyTitle` to someone
// else rather than calling it — the round 8 G shape with the call removed.
const noop = '\tlet applyTitle = (next: Item): void => {\n\t\tvoid next;\n\t};\n\t$effect(() => {\n\t\tapplyTitle = (next: Item) => {\n\t\t\tvoid next;\n\t\t};\n\t});\n\n\tfunction showSaved() {\n';
const base = SOURCE.replace('\tfunction showSaved() {\n', noop).replace(
const noop = '\tlet applyTitle = (next: Item): void => {\n\t\tvoid next;\n\t};\n\t$effect(() => {\n\t\tapplyTitle = (next: Item) => {\n\t\t\tvoid next;\n\t\t};\n\t});\n\n\tfunction showSaved(saveTok: SaveToken) {\n';
const base = SOURCE.replace('\tfunction showSaved(saveTok: SaveToken) {\n', noop).replace(
TITLE_FENCE_AND_COMMITS,
"{ title: titleDraft.trim() });\n\t\t\t[updated].forEach(applyTitle);\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n"
"{ title: titleDraft.trim() });\n\t\t\t[updated].forEach(applyTitle);\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n"
);
const opts = reviewAs(base);
// Step 2: only the rebinding inside the $effect changes. saveTitle never
@@ -1666,10 +1711,10 @@ describe('ItemDetail identity gate: a fenced unit cannot change without a re-rea
it('a function value rebound outside every unit changes the hash of the unit that calls it (refinement 2)', () => {
// Step 1: a reviewed base where saveTitle calls a no-op `let` helper.
const noop = '\tlet applyTitle = (next: Item): void => {\n\t\tvoid next;\n\t};\n\t$effect(() => {\n\t\tapplyTitle = (next: Item) => {\n\t\t\tvoid next;\n\t\t};\n\t});\n\n\tfunction showSaved() {\n';
const base = SOURCE.replace('\tfunction showSaved() {\n', noop).replace(
const noop = '\tlet applyTitle = (next: Item): void => {\n\t\tvoid next;\n\t};\n\t$effect(() => {\n\t\tapplyTitle = (next: Item) => {\n\t\t\tvoid next;\n\t\t};\n\t});\n\n\tfunction showSaved(saveTok: SaveToken) {\n';
const base = SOURCE.replace('\tfunction showSaved(saveTok: SaveToken) {\n', noop).replace(
TITLE_FENCE_AND_COMMITS,
"{ title: titleDraft.trim() });\n\t\t\tapplyTitle(updated);\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved();\n"
"{ title: titleDraft.trim() });\n\t\t\tapplyTitle(updated);\n\t\t\tif (gen !== loadGeneration || item?.id !== targetItem.id) return;\n\t\t\titem = withInflightTags(updated);\n\t\t\tshowSaved(saveTok);\n"
);
const opts = reviewAs(base);
// Step 2: only the rebinding inside the $effect changes.
@@ -141,7 +141,7 @@ describe('ItemDetail field writes are ordered', () => {
// leaves the server holding the confirmed value and the pane showing the
// old one, with nothing left to correct it. `claim` asks whether anything
// newer actually WROTE, which is the question that matters here.
// Bounded at the branch's own `showSaved()`, not at the next landmark:
// Bounded at the branch's own `showSaved(saveTok)`, not at the next landmark:
// the DECLINED branch below it legitimately mentions supersession (for
// the save indicator), and a slice running into it reports that as a
// violation here.
@@ -151,7 +151,7 @@ describe('ItemDetail field writes are ordered', () => {
// anchored on the branch would miss it — measured, it survived.
const windowAt = UPDATE_FIELD.indexOf('if (!stillCurrent() || !item) return;');
const forcedAt = UPDATE_FIELD.indexOf('if (forced) {', windowAt);
const savedAt = UPDATE_FIELD.indexOf('showSaved();', forcedAt);
const savedAt = UPDATE_FIELD.indexOf('showSaved(saveTok);', forcedAt);
expect(windowAt).toBeGreaterThan(-1);
expect(forcedAt).toBeGreaterThan(windowAt);
expect(savedAt).toBeGreaterThan(forcedAt);
@@ -175,9 +175,12 @@ describe('ItemDetail field writes are ordered', () => {
const afterForced = declined.slice(declined.indexOf('toastStore.show(\'Status change cancelled\''));
expect(declined).toContain("toastStore.show('Status change cancelled'");
// Between the end of the forced branch and the cancel toast there is no
// claim — only the saveStatus guard.
// claim. (It used to hold the save-indicator guard; the indicator now
// settles in updateField's finally, BUG-3044.)
const cancelAt = declined.indexOf("toastStore.show('Status change cancelled'");
const declinedBody = declined.slice(declined.indexOf('}', declined.indexOf('showSaved();')), cancelAt);
const succeedAt = declined.indexOf('showSaved(saveTok);');
expect(succeedAt, 'the forced branch ends at its success record').toBeGreaterThan(-1);
const declinedBody = declined.slice(declined.indexOf('}', succeedAt), cancelAt);
expect(declinedBody).not.toContain('fieldWrites.claim(ticket)');
expect(afterForced.length).toBeGreaterThan(0);
});
@@ -0,0 +1,66 @@
import { describe, it, expect } from 'vitest';
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
/**
* BUG-3044 — every write the pane's save indicator counts is settled on every
* path. A `saves.begin()` whose token is not settled in a `finally` leaves the
* count up after any early return (a superseded write, an item switch, a
* force-refresh skip), and 'saving' then blocks every SSE gate for the item
* until navigation.
*
* A source guard, deliberately narrow: it pairs each begin with a settle of
* the same token inside a `finally` block or a `.finally(` in the SAME
* function. It does not prove the finally is reachable on every path; the
* identity-fence gate (itemDetailIdentityFenceAst.test.ts) hashes each of
* these units, so any edit to one is re-read there.
*/
const SOURCE = readFileSync(fileURLToPath(new URL('./ItemDetail.svelte', import.meta.url)), 'utf-8');
/** The body of the innermost `function name(` / `name: async (` / `setTimeout(() =>` that contains `at`, by brace matching. */
function enclosingBlock(src: string, at: number): string {
let depth = 0;
for (let i = at; i >= 0; i--) {
if (src[i] === '}') depth++;
else if (src[i] === '{') {
if (depth === 0) {
const head = src.slice(Math.max(0, i - 160), i);
if (/(function \w+\([^)]*\)[^{]*|=>\s*|async \([^)]*\)[^{]*=>\s*)$/.test(head)) {
let d = 0;
for (let j = i; j < src.length; j++) {
if (src[j] === '{') d++;
else if (src[j] === '}' && --d === 0) return src.slice(i, j + 1);
}
}
} else depth--;
}
}
return '';
}
describe('ItemDetail settles every save it begins (BUG-3044)', () => {
const begins = [...SOURCE.matchAll(/(?:const |)saveTok = saves\.begin\(\);/g)];
it('the population is the nine save paths', () => {
// Title, field, tags, assignment, role, the legacy content debounce, the
// collab flush, the raw saver and the raw drain.
expect(begins.length).toBe(9);
});
it.each(begins.map((m) => [SOURCE.slice(0, m.index).split('\n').length, m.index!] as const))(
'the begin at line %i is settled in a finally of the same function',
(_line, at) => {
const body = enclosingBlock(SOURCE, at);
expect(body, 'no enclosing function found').not.toBe('');
const settled =
/finally \{[^}]*saves\.settle\(saveTok\)/.test(body) || /\.finally\(\(\) => saves\.settle\(saveTok\)\)/.test(body);
expect(settled, 'no settle of this token in a finally').toBe(true);
}
);
it('writes nothing to the indicator except through the tracker', () => {
// Its one declaration is derived from the tracker; nothing assigns it.
expect(SOURCE).toMatch(/const saveStatus = \$derived\(saves\.status\);/);
expect(SOURCE).not.toMatch(/(?<!const )\bsaveStatus\s*=[^=]/);
});
});
@@ -0,0 +1,117 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { SAVED_MS, SaveTracker } from './saveTracker.svelte';
/**
* BUG-3044 — the pane's save indicator describes the writes outstanding. The
* first three legs are the three filed sequences; each was reachable with the
* single last-writer `saveStatus` it replaces.
*/
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
describe('SaveTracker (BUG-3044)', () => {
it('an older success does not show Saved while a newer write is in flight', () => {
const s = new SaveTracker();
const older = s.begin();
const newer = s.begin();
s.succeed(older);
s.settle(older);
expect(s.status).toBe('saving');
s.succeed(newer);
s.settle(newer);
expect(s.status).toBe('saved');
});
it('an older success settling AFTER the newest write failed does not show Saved', () => {
const s = new SaveTracker();
const older = s.begin();
const newer = s.begin();
s.settle(newer); // failed
expect(s.status).toBe('saving');
s.succeed(older);
s.settle(older);
expect(s.status).toBe('idle');
});
it("one field's failure does not hide another field's write in flight", () => {
const s = new SaveTracker();
const a = s.begin();
const b = s.begin();
s.settle(a); // a failed
expect(s.status).toBe('saving');
s.succeed(b);
s.settle(b);
expect(s.status).toBe('saved');
});
it("a previous save's Saved timer does not fire during a new request", () => {
const s = new SaveTracker();
const first = s.begin();
s.succeed(first);
s.settle(first);
expect(s.status).toBe('saved');
vi.advanceTimersByTime(SAVED_MS - 100);
const second = s.begin();
vi.advanceTimersByTime(500); // the first save's timer would have fired here
expect(s.status).toBe('saving');
s.succeed(second);
s.settle(second);
expect(s.status).toBe('saved');
vi.advanceTimersByTime(SAVED_MS);
expect(s.status).toBe('idle');
});
it('a superseded write that settles in a finally does not pin saving', () => {
// The trap a plain counter walks into: a superseded write returns early
// without touching the status. Settled from a finally, it still counts down.
const s = new SaveTracker();
const superseded = s.begin();
const winner = s.begin();
s.succeed(winner);
s.settle(winner);
expect(s.status).toBe('saving');
s.settle(superseded);
expect(s.status).toBe('saved');
});
it('a settle from before a reset (item switch) is ignored', () => {
const s = new SaveTracker();
const old = s.begin();
s.reset();
expect(s.status).toBe('idle');
const current = s.begin();
s.succeed(old);
s.settle(old);
expect(s.status, 'the previous item cannot end this one').toBe('saving');
s.settle(current);
expect(s.status).toBe('idle');
});
it('settling a token twice counts once', () => {
const s = new SaveTracker();
const a = s.begin();
const b = s.begin();
s.settle(a);
s.settle(a);
expect(s.status).toBe('saving');
s.settle(b);
expect(s.status).toBe('idle');
});
it('reset clears a pending Saved timer', () => {
const s = new SaveTracker();
const t = s.begin();
s.succeed(t);
s.settle(t);
s.reset();
const next = s.begin();
vi.advanceTimersByTime(SAVED_MS + 1);
expect(s.status).toBe('saving');
s.settle(next);
});
});
+100
View File
@@ -0,0 +1,100 @@
/**
* The item pane's save indicator, derived from the writes actually OUTSTANDING
* rather than from whichever write reached a shared variable last (BUG-3044).
*
* `status` is:
* - 'saving' while ANY begun write has not settled;
* - 'saved' (for SAVED_MS) once the last one settles, iff the NEWEST-ISSUED
* write succeeded — an older success cannot announce a value that a newer
* write has since failed to store;
* - 'idle' otherwise.
*
* A single last-writer value got three sequences wrong: an older write's success
* showed "Saved" while a newer write was still in flight; one field's failure
* showed idle while another field was still saving; and a previous "Saved"
* timer fired mid-request, because starting a save never cleared it. The pane's
* SSE gates read `status === 'saving'`, so each of those also admitted an
* incoming event while a local write was outstanding.
*
* Every `begin()` must be `settle()`d, on EVERY return path — including a write
* that was superseded by a newer one for the same field, which otherwise never
* decrements and pins 'saving' (and every SSE gate) until navigation. Call
* `settle` from a `finally`. A token from before the last `reset()` (the pane
* switched items) is ignored, so a late settle cannot move the next item's
* indicator. Settling a token twice is a no-op.
*/
export type SaveStatus = 'idle' | 'saving' | 'saved';
/** How long "Saved" stays up before the indicator goes idle. */
export const SAVED_MS = 2000;
export interface SaveToken {
readonly epoch: number;
readonly n: number;
ok: boolean;
settled: boolean;
}
export class SaveTracker {
status = $state<SaveStatus>('idle');
#epoch = 0;
#outstanding = 0;
#issued = 0;
#newestOk = false;
#timer: ReturnType<typeof setTimeout> | undefined;
/** A write is starting. Clears any pending "Saved" timer. */
begin(): SaveToken {
this.#clearTimer();
this.#outstanding += 1;
this.#issued += 1;
this.status = 'saving';
return { epoch: this.#epoch, n: this.#issued, ok: false, settled: false };
}
/** Record that this write succeeded; it still has to be settled. */
succeed(token: SaveToken): void {
token.ok = true;
}
/** This write is over, whatever happened to it. Call from a `finally`. */
settle(token: SaveToken): void {
if (token.settled) return;
token.settled = true;
if (token.epoch !== this.#epoch) return;
this.#outstanding -= 1;
if (token.n === this.#issued) this.#newestOk = token.ok;
if (this.#outstanding > 0) return;
if (this.#newestOk) {
this.status = 'saved';
this.#timer = setTimeout(() => {
this.#timer = undefined;
this.status = 'idle';
}, SAVED_MS);
} else {
this.status = 'idle';
}
}
/** Forget every outstanding write (the pane moved to another item). */
reset(): void {
this.#epoch += 1;
this.#outstanding = 0;
this.#issued = 0;
this.#newestOk = false;
this.#clearTimer();
this.status = 'idle';
}
/** Stop the timer; for component teardown. */
destroy(): void {
this.#clearTimer();
}
#clearTimer(): void {
clearTimeout(this.#timer);
this.#timer = undefined;
}
}