mirror of
https://github.com/logseq/logseq.git
synced 2026-05-02 18:06:32 +00:00
7.0 KiB
7.0 KiB
ADR 0010: Canonical Op-Driven Client Rebase With Legacy Tx Surgery Isolation
Date: 2026-03-19 Status: Accepted
Context
Client sync rebase currently relies on custom tx-data surgery to keep pending local changes alive after remote txs are applied.
That approach has several problems:
- the logic is hard to reason about because it edits datoms instead of replaying user intent
- it does not preserve intent well when a local change originated from a higher level outliner action
- the helper set keeps growing with special cases for missing refs, structural conflicts, and deleted entities
- the outliner-op surface is too large to replay directly without first reducing it
At the same time, not every outliner op needs to remain a first-class replay
operation.
Many ops can be safely represented by :transact when their tx-data is already
self-contained and does not depend on rerunning outliner logic.
We also have existing persisted pending rows that only store tx-data and reverse tx-data. Those rows still need a compatibility path, but that path should not define the new rebase architecture.
Decision
- New pending local tx rows will persist semantic
:outliner-opsin addition to their existing tx-data payload. - Client rebase will become op-driven for all new pending rows:
- reverse local txs
- apply remote txs
- transform or drop stored local ops against the post-remote temp db
- replay the surviving ops to regenerate rebased tx-data
- Reduce the replay-visible outliner-op surface to a small canonical set:
:insert-blocks:save-block:move-blocks:delete-blocks:transact
- Normalize higher-level ops into that canonical set before persistence.
Examples:
:indent-outdent-blocksbecomes:move-blocks:move-blocks-up-downbecomes:move-blocks:rename-pagebecomes:save-block
- Introduce an explicit safe-
:transactclassifier:- if replaying tx-data directly is sufficient and does not require rerunning
outliner logic, persist the op as canonical
:transact - otherwise keep it as one of the canonical outliner replay ops
- expected canonical
:transactcases include direct tx replay actions such as undo, redo, import replay, reaction toggles, and property-value updates
- if replaying tx-data directly is sufficient and does not require rerunning
outliner logic, persist the op as canonical
- Treat undo/redo as canonical
:transactactions.- persist the exact tx-data that the successful undo or redo applied
- keep the action atomic as one pending tx row
- do not reconstruct the original higher-level outliner ops that were undone or redone
- Treat
:batch-import-ednas canonical:transactafter successful local execution.- persist the exact tx-data that the import applied
- do not rerun import expansion during rebase
- Keep the following op kinds as replay-visible semantic ops because they must
be reevaluated against current DB state:
:save-block:insert-blocks:move-blocks:delete-blocks:create-page:delete-page:upsert-property
- Stop using raw tx-data surgery in the normal rebase path for new rows.
- Move the current tx-data surgery helpers into a dedicated legacy namespace.
That legacy namespace is only for compatibility handling of old persisted
pending rows that do not have stored
:outliner-ops. - Add explicit owned-block filtering in the new op-driven rebase path. Initially cover:
- reaction blocks owned by
:logseq.property.reaction/target - property history blocks owned by
:logseq.property.history/block - property history blocks whose effective owner disappears through deleted
:logseq.property.history/ref-value
- If a local op creates or updates one of those owned blocks and the owning block was deleted remotely, drop that op.
- Treat each pending tx row as one user action and keep it atomic during rebase.
- If any op in a pending tx becomes invalid during rebase, drop the whole pending tx rather than keeping a partial replay of that user action.
- Keep this refactor client-only for now. The sync wire format and server tx log remain unchanged.
Consequences
- Positive:
- Rebase reasons about user intent instead of datom accidents.
- The number of op kinds that rebase must understand becomes much smaller.
- Safe direct tx replay remains available through canonical
:transactwithout forcing every operation through outliner code. - Undo/redo stays aligned with its real intent: replay the exact applied tx, not rerun a reconstructed higher-level command.
- Import replay stays aligned with its real intent: preserve the exact local import result rather than recomputing import expansion later.
- Legacy compatibility is isolated instead of contaminating the new design.
- Owned-block cleanup becomes an explicit semantic rule rather than another tx-data patch.
- Rebase behavior stays aligned with the mental model that one pending tx is one user action that either survives or is discarded as a whole.
- Negative:
- We must maintain a canonicalization layer from original outliner ops to the reduced replay set.
- Some existing ops need careful classification to decide whether they are
safe
:transactor must stay true outliner replays. - For a transition period, the codebase will contain both the new rebase path and the isolated legacy compatibility path.
- If one invalid op is grouped together with otherwise valid work in the same pending tx, the whole user action will be lost during rebase.
Follow-up Constraints
- New pending tx producers must persist canonical
:outliner-ops. - New pending tx producers must preserve user-action boundaries because rebase will treat each persisted tx row atomically.
- Canonicalization should happen when persisting local pending txs, not lazily during rebase.
- Undo/redo producers should persist canonical
:transactactions using the exact tx-data they applied. - Import producers should persist canonical
:transactactions using the exact tx-data they applied. - The main sync apply namespace should not call legacy tx-surgery helpers for new pending rows.
- The legacy namespace should be clearly named and easy to delete once old pending-row compatibility is no longer needed.
Verification
- Add or update frontend worker db-sync coverage for:
- persistence of canonical
:outliner-ops - canonical reduction of
:indent-outdent-blocksand:move-blocks-up-downinto:move-blocks - safe
:transactreplay versus true outliner replay classification - undo/redo persistence as atomic canonical
:transactactions :batch-import-ednpersistence as atomic canonical:transact:rename-pagecanonicalization to:save-block- op-driven rebase preserving pending tx boundaries
- dropping owned reaction/history ops when their owner was deleted remotely
- dropping the whole pending tx when any op in that user action becomes invalid
- routing legacy pending rows without stored ops through the legacy namespace
- persistence of canonical
- Expected targeted command:
bb dev:test -v frontend.worker.db-sync-test