mirror of
https://github.com/logseq/logseq.git
synced 2026-05-19 18:32:41 +00:00
148 lines
7.0 KiB
Markdown
148 lines
7.0 KiB
Markdown
# 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
|
|
1. New pending local tx rows will persist semantic `:outliner-ops` in addition
|
|
to their existing tx-data payload.
|
|
2. 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
|
|
3. Reduce the replay-visible outliner-op surface to a small canonical set:
|
|
- `:insert-blocks`
|
|
- `:save-block`
|
|
- `:move-blocks`
|
|
- `:delete-blocks`
|
|
- `:transact`
|
|
4. Normalize higher-level ops into that canonical set before persistence.
|
|
Examples:
|
|
- `:indent-outdent-blocks` becomes `:move-blocks`
|
|
- `:move-blocks-up-down` becomes `:move-blocks`
|
|
- `:rename-page` becomes `:save-block`
|
|
5. Introduce an explicit safe-`:transact` classifier:
|
|
- 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 `:transact` cases include direct tx replay actions such
|
|
as undo, redo, import replay, reaction toggles, and property-value updates
|
|
6. Treat undo/redo as canonical `:transact` actions.
|
|
- 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
|
|
7. Treat `:batch-import-edn` as canonical `:transact` after successful local
|
|
execution.
|
|
- persist the exact tx-data that the import applied
|
|
- do not rerun import expansion during rebase
|
|
8. 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`
|
|
9. Stop using raw tx-data surgery in the normal rebase path for new rows.
|
|
10. 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`.
|
|
11. 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`
|
|
12. If a local op creates or updates one of those owned blocks and the owning
|
|
block was deleted remotely, drop that op.
|
|
13. Treat each pending tx row as one user action and keep it atomic during
|
|
rebase.
|
|
14. 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.
|
|
15. 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 `:transact`
|
|
without 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 `:transact` or 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 `:transact` actions using the
|
|
exact tx-data they applied.
|
|
- Import producers should persist canonical `:transact` actions 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-blocks` and
|
|
`:move-blocks-up-down` into `:move-blocks`
|
|
- safe `:transact` replay versus true outliner replay classification
|
|
- undo/redo persistence as atomic canonical `:transact` actions
|
|
- `:batch-import-edn` persistence as atomic canonical `:transact`
|
|
- `:rename-page` canonicalization 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
|
|
- Expected targeted command:
|
|
- `bb dev:test -v frontend.worker.db-sync-test`
|