perf: incremental site saves; hoist runtime builds out of the publish transaction#27
Merged
Merged
Conversation
Measures the JSON payload the editor ships to PUT /pages plus the server-side validate + reconcile work, for a one-prop edit. Captures the before/after evidence for the incremental-save change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… transaction
Incremental saves (every autosave/Cmd+S previously shipped and rewrote the
FULL page + component roster, and the server re-validated and DOMPurify-
sanitized every node of every page):
- The editor store derives which pages/VCs changed from the same Mutative
patches that power undo (slices/site/dirtyTracking.ts) — wired into
historic mutations, undo/redo, and lifecycle resets, with a conservative
mark-all fallback for anything unattributable. Autosave snapshots the
marks before the request and merges them back on failure.
- Wire protocol: PUT /pages takes { changedPages, pageIds, baselinePageIds? },
PUT /components takes { changedComponents, componentIds }. Full id rosters
always ship, so reap semantics (incl. ISS-041 baseline) are unchanged.
- Server validates ONLY the changed batch (validatePagesForPartialSave;
slug uniqueness against a light id+slug projection) — outside the
transaction, since the SQLite adapter serializes all transactions through
one chain. Cross-VC rules run on the merged post-save roster
(validateVisualComponentsForPartialWrite); the old full-write validator
is deleted.
- updateDataRowDraftCells writes without the hydrated re-read the roster
reconcilers always discarded; listDataRowIdSlugs replaces full-roster
hydration in both reconcile loops.
- deletePage now splices in place — wholesale array replacement emitted a
patch dirty-tracking couldn't attribute, escalating every post-delete
save to a full save. Page-roster mutations extracted to
page-tree/pageMutations.ts (mutations.ts ratchets 880 → 760 lines).
Publish transaction hoist (verified high-impact):
- ensureRuntimeDependencyCache (bun install), the importmap build, and all
per-page esbuild runs now execute BEFORE the publish transaction; the
transaction is DB-writes-only with an orphan-version guard for pages
reaped between the read and the write. Every concurrent write used to
queue behind multi-second builds on the global SQLite transaction chain.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
90e7cff to
97779cd
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Follow-up to #24 (stacked on
perf/critical-performance-fixes— merge that first; this diff is the last two commits). Fixes the two remaining adversarially-verified server-side findings from the performance audit: the full-roster autosave and the publish transaction blocking every concurrent write.Benchmarks — before / after
Same bench discipline as #24: the
Site save round-tripscenario was committed first and run on unmodified code, then re-run after the fix (full-size: 60 pages × ~300 nodes, one-prop edit — the canonical autosave trigger).The publish-transaction hoist isn't a wall-time win for the publish itself — it removes a stall: with a cold dependency cache,
bun install(60 s timeout) plus one esbuild run per page all executed inside the publish transaction, and the SQLite adapter serializes every transaction in the process through one chain. Every concurrent write — autosaves, row publishes, plugin records, sessions — queued behind it. Now the transaction contains only the DB writes.What changed
Incremental saves (every autosave/Cmd+S used to ship and rewrite the full page + component roster; the server re-validated and DOMPurify-sanitized every node of every page):
slices/site/dirtyTracking.ts) — wired into historic mutations, undo/redo, and lifecycle resets. Anything unattributable marksall(conservative full save); over-marking is safe, under-marking would lose edits. Autosave snapshots the marks before the request and merges them back on failure, so edits landing mid-save are never lost.PUT /pages→{ changedPages, pageIds, baselinePageIds? },PUT /components→{ changedComponents, componentIds }. Full id rosters always ship, so delete-what's-missing reconcile semantics (including the ISS-041 concurrency baseline) are byte-identical.validatePagesForPartialSave; slug uniqueness against a light id+slug projection) — outside the transaction. Cross-VC rules (identity, refs, dependency cycles) are inherently roster-wide, so they run against the merged post-save roster (validateVisualComponentsForPartialWrite); the old full-write validator is deleted.updateDataRowDraftCellsdrops the hydrated per-row re-read both reconcilers always discarded;listDataRowIdSlugsreplaces fullcells_jsonhydration in the reconcile loops (the third of the three wasted full-tree JSON round-trips per page per save).deletePagenow splices in place — the wholesale array replacement it used emitted a patch dirty-tracking couldn't attribute, silently escalating every post-delete save to a full save (found by the new test suite). Page-roster mutations extracted topage-tree/pageMutations.ts;mutations.tsratchets 880 → 760 lines.Publish transaction hoist (verified high/high): dependency-cache priming, the importmap build, and all per-page esbuild runs now execute before the transaction opens; the transaction is writes-only, with an orphan-version guard for pages reaped between the read and the write (
withPublishLockalready serializes publishes, so the read is stable against other publishers).Verification
bun test: 5,156 pass / 0 fail (45 new tests: dirty-collector units + store integration, adapter wire-shape pins, 12 handler-level partial-save cases on real SQLite incl. both ISS-041 directions, slug conflicts vs unchanged rows, and merged-roster VC ref rejection)bun run build+bun run lint: clean. Docs updated (site-shell.mdsave flow,publisher.mdpublish flow).🤖 Generated with Claude Code