edytor 3.0.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/cloudflare/DocumentRoom.d.ts +13 -9
- package/dist/cloudflare/DocumentRoom.js +10 -6
- package/dist/cloudflare/room/context.d.ts +27 -26
- package/dist/cloudflare/room/context.js +2 -2
- package/dist/cloudflare/room/purge.d.ts +6 -3
- package/dist/cloudflare/room/purge.js +44 -19
- package/dist/cloudflare/room/updates.d.ts +3 -4
- package/dist/cloudflare/room/updates.js +6 -8
- package/dist/crdt/attribution/attribution.d.ts +31 -12
- package/dist/crdt/attribution/attribution.js +56 -18
- package/dist/crdt/attribution/block.d.ts +3 -3
- package/dist/crdt/attribution/block.js +7 -8
- package/dist/crdt/attribution/index.d.ts +1 -1
- package/dist/crdt/attribution/index.js +1 -1
- package/dist/crdt/data.d.ts +62 -31
- package/dist/crdt/data.js +211 -103
- package/dist/crdt/doc/delete.d.ts +1 -1
- package/dist/crdt/doc/delete.js +13 -30
- package/dist/crdt/doc/funnel.js +29 -24
- package/dist/crdt/doc/gate.d.ts +3 -3
- package/dist/crdt/doc/history.d.ts +25 -9
- package/dist/crdt/doc/history.js +150 -40
- package/dist/crdt/doc/layout.js +11 -22
- package/dist/crdt/doc/meta.js +12 -17
- package/dist/crdt/doc/moves.d.ts +1 -1
- package/dist/crdt/doc/moves.js +52 -32
- package/dist/crdt/doc/reads.js +2 -2
- package/dist/crdt/doc/seed.js +2 -2
- package/dist/crdt/doc/split-merge.js +12 -15
- package/dist/crdt/doc/steps.d.ts +25 -13
- package/dist/crdt/doc/steps.js +52 -122
- package/dist/crdt/doc/types.d.ts +21 -9
- package/dist/crdt/document.d.ts +16 -5
- package/dist/crdt/document.js +21 -4
- package/dist/crdt/edytor-doc.d.ts +62 -64
- package/dist/crdt/edytor-doc.js +18 -13
- package/dist/crdt/engine-api.d.ts +33 -1
- package/dist/crdt/engine.d.ts +0 -1
- package/dist/crdt/engine.js +1 -4
- package/dist/crdt/flow.d.ts +1 -1
- package/dist/crdt/flow.js +25 -27
- package/dist/crdt/index.d.ts +26 -25
- package/dist/crdt/index.js +1 -1
- package/dist/crdt/placement/display.d.ts +5 -30
- package/dist/crdt/placement/display.js +39 -144
- package/dist/crdt/placement/keys.d.ts +58 -0
- package/dist/crdt/placement/keys.js +90 -0
- package/dist/crdt/placement/model.d.ts +119 -118
- package/dist/crdt/placement/model.js +242 -142
- package/dist/crdt/placement/resolve.d.ts +123 -35
- package/dist/crdt/placement/resolve.js +187 -153
- package/dist/crdt/placement/slots.d.ts +134 -0
- package/dist/crdt/placement/slots.js +152 -0
- package/dist/crdt/providers/room.d.ts +16 -2
- package/dist/crdt/providers/room.js +64 -17
- package/dist/crdt/providers/websocket.js +5 -3
- package/dist/crdt/purge.d.ts +17 -8
- package/dist/crdt/purge.js +236 -126
- package/dist/crdt/restore.d.ts +2 -2
- package/dist/crdt/restore.js +18 -34
- package/dist/crdt/schema.d.ts +22 -21
- package/dist/crdt/schema.js +29 -23
- package/dist/crdt/storage.d.ts +1 -1
- package/dist/crdt/storage.js +1 -1
- package/dist/crdt/structs.d.ts +15 -15
- package/dist/crdt/structs.js +8 -22
- package/dist/crdt/text/index/cache.js +4 -0
- package/dist/crdt/text/index/claims.js +12 -0
- package/dist/crdt/text/index/fold.d.ts +2 -3
- package/dist/crdt/text/index/fold.js +194 -44
- package/dist/crdt/text/index/layout.js +5 -8
- package/dist/crdt/text/index/placement.d.ts +26 -18
- package/dist/crdt/text/index/placement.js +404 -138
- package/dist/crdt/text/index/records.d.ts +3 -3
- package/dist/crdt/text/index/records.js +24 -31
- package/dist/crdt/text/index/shared.d.ts +11 -3
- package/dist/crdt/text/index/shared.js +12 -3
- package/dist/crdt/text/index/state.d.ts +21 -17
- package/dist/crdt/text/index/state.js +15 -17
- package/dist/crdt/text/index/streams.d.ts +10 -5
- package/dist/crdt/text/index/streams.js +78 -27
- package/dist/crdt/text/items.d.ts +10 -3
- package/dist/crdt/text/items.js +7 -0
- package/dist/crdt/text/model.d.ts +77 -62
- package/dist/crdt/text/model.js +84 -169
- package/dist/crdt/text/ranges.d.ts +1 -1
- package/dist/crdt/text/ranges.js +3 -3
- package/dist/crdt/text/rows.d.ts +13 -7
- package/dist/crdt/text/rows.js +24 -7
- package/dist/crdt/text/runs.d.ts +17 -12
- package/dist/crdt/text/runs.js +24 -7
- package/dist/crdt/text/streams.d.ts +26 -13
- package/dist/crdt/text/streams.js +41 -14
- package/dist/crdt/vendor/yjs/UPSTREAM.md +568 -21
- package/dist/crdt/vendor/yjs/dts/global.d.ts +2 -0
- package/dist/crdt/vendor/yjs/dts/index.d.ts +1 -1
- package/dist/crdt/vendor/yjs/dts/structs/AbstractStruct.d.ts +7 -0
- package/dist/crdt/vendor/yjs/dts/structs/Item.d.ts +194 -10
- package/dist/crdt/vendor/yjs/dts/utils/Doc.d.ts +6 -3
- package/dist/crdt/vendor/yjs/dts/utils/StructStore.d.ts +5 -18
- package/dist/crdt/vendor/yjs/dts/utils/Transaction.d.ts +15 -1
- package/dist/crdt/vendor/yjs/dts/utils/UpdateDecoder.d.ts +14 -6
- package/dist/crdt/vendor/yjs/dts/utils/UpdateEncoder.d.ts +33 -6
- package/dist/crdt/vendor/yjs/dts/utils/ids.d.ts +1 -65
- package/dist/crdt/vendor/yjs/dts/utils/marks.d.ts +6 -1
- package/dist/crdt/vendor/yjs/dts/ynode.d.ts +43 -7
- package/dist/crdt/vendor/yjs/global.d.ts +2 -0
- package/dist/crdt/vendor/yjs/src/index.js +1 -1
- package/dist/crdt/vendor/yjs/src/structs/AbstractStruct.js +10 -0
- package/dist/crdt/vendor/yjs/src/structs/Item.js +610 -51
- package/dist/crdt/vendor/yjs/src/utils/BlockSet.js +18 -9
- package/dist/crdt/vendor/yjs/src/utils/Doc.js +5 -3
- package/dist/crdt/vendor/yjs/src/utils/StructStore.js +5 -0
- package/dist/crdt/vendor/yjs/src/utils/Transaction.js +36 -13
- package/dist/crdt/vendor/yjs/src/utils/UpdateDecoder.js +88 -18
- package/dist/crdt/vendor/yjs/src/utils/UpdateEncoder.js +119 -32
- package/dist/crdt/vendor/yjs/src/utils/encoding.js +13 -1
- package/dist/crdt/vendor/yjs/src/utils/ids.js +7 -7
- package/dist/crdt/vendor/yjs/src/utils/marks.js +193 -8
- package/dist/crdt/vendor/yjs/src/utils/redo.js +1 -1
- package/dist/crdt/vendor/yjs/src/utils/transaction-helpers.js +1 -1
- package/dist/crdt/vendor/yjs/src/utils/updates.js +3 -3
- package/dist/crdt/vendor/yjs/src/ynode.js +177 -12
- package/dist/edytor.svelte.js +4 -1
- package/dist/events/beforeInputCommands.js +2 -2
- package/dist/events/onPageHide.d.ts +3 -0
- package/dist/events/onPageHide.js +16 -0
- package/dist/export/forms.d.ts +25 -25
- package/dist/surface/observer.svelte.js +2 -2
- package/dist/surface/projector.svelte.d.ts +15 -3
- package/dist/surface/projector.svelte.js +59 -8
- package/package.json +2 -2
- package/dist/crdt/placement/rank.d.ts +0 -87
- package/dist/crdt/placement/rank.js +0 -332
- package/dist/crdt/placement/source.d.ts +0 -43
- package/dist/crdt/placement/source.js +0 -66
- package/dist/crdt/text/delete-index.d.ts +0 -129
- package/dist/crdt/text/delete-index.js +0 -365
- package/dist/crdt/text/deletes.d.ts +0 -76
- package/dist/crdt/text/deletes.js +0 -362
- package/dist/crdt/text/index/anchored.d.ts +0 -17
- package/dist/crdt/text/index/anchored.js +0 -197
|
@@ -33,7 +33,7 @@ is no longer a dependency of any kind: `tests/packed-consumer/smoke.js` and
|
|
|
33
33
|
## Layout
|
|
34
34
|
|
|
35
35
|
```text
|
|
36
|
-
src/ upstream src/, plus patches YP1 (import specifiers), YP4, YP5, YP7, YP8 (pruning), YP9–YP15, YP16 (no delta layer), YP17 (one update encoding), YP18 (no undo manager)
|
|
36
|
+
src/ upstream src/, plus patches YP1 (import specifiers), YP4, YP5, YP7, YP8 (pruning), YP9–YP15, YP16 (no delta layer), YP17 (one update encoding), YP18 (no undo manager), YP19 (only what edytor calls), YP20 (hold records), YP21 (claim items), YP22 (move records), YP23 (a block set's trim), YP24 (a string table per update), YP25 (paired marks written in parts), YP26 (a client table per update), YP27 (the parent kind in the info byte), YP28 (large integers in content values), YP29 (a collected mark or claim keeps its class) and YP30 (a transaction's `beforeCommit`)
|
|
37
37
|
global.d.ts upstream global.d.ts, plus patches YP1, YP8 and YP19
|
|
38
38
|
dts/ generated TypeScript declarations (not upstream source — see below)
|
|
39
39
|
LICENSE upstream MIT license, verbatim
|
|
@@ -55,11 +55,12 @@ applied to both) and `fork.diff` (newer → this tree, both carrying YP1) into
|
|
|
55
55
|
the run's artifact, says in the summary whether the installed `lib0-v14`
|
|
56
56
|
satisfies the newer engine's `lib0` range (a mismatch is a run warning: a
|
|
57
57
|
red check may then be the older lib0, not the fork), then materializes the newer engine as `bench/vendor-baseline/yjs`
|
|
58
|
-
so `
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
so the baseline leg of `src/tests/crdt/hardening/search-marker-format.test.ts`
|
|
59
|
+
(identical stores after every operation, each read back from its own
|
|
60
|
+
engine's encoding) runs against it. There is no wire interop to check since
|
|
61
|
+
YP24: the fork's update encoding is its own. A red run
|
|
61
62
|
means the re-sync needs care; the re-sync itself is the manual recipe under
|
|
62
|
-
YP8. Locally: `node scripts/upstream-check.mjs`, then
|
|
63
|
+
YP8. Locally: `node scripts/upstream-check.mjs`, then that check, then
|
|
63
64
|
`bench/lib/mk-baseline.sh` to restore the pinned baseline.
|
|
64
65
|
|
|
65
66
|
## Local patches
|
|
@@ -553,8 +554,9 @@ each undo therefore brought it back twice (`abbc`), and the first undo brought
|
|
|
553
554
|
it back although the other peer's delete still held it (arch-v2 contract
|
|
554
555
|
program `history.concurrent-double-delete`). Edytor fixes this with per-writer
|
|
555
556
|
text delete marks (`src/lib/crdt/text/deletes.ts`, execution ledger row
|
|
556
|
-
"Text double-delete undo"
|
|
557
|
-
else
|
|
557
|
+
"Text double-delete undo"; since YP20 the engine's hold records, and that
|
|
558
|
+
module is gone); the engine needs two hooks for it, and nothing else
|
|
559
|
+
changes.
|
|
558
560
|
|
|
559
561
|
Patch (hunks marked `// YP11` or delimited by `// YP11 begin` / `// YP11 end`):
|
|
560
562
|
|
|
@@ -832,10 +834,11 @@ Patch:
|
|
|
832
834
|
- `edytor/crdt` exports `UpdateEncoder`, `UpdateDecoder` and `diffUpdate`
|
|
833
835
|
in place of the V1/V2 pairs (see `src/tests/crdt/yp8-surface.test.ts`).
|
|
834
836
|
|
|
835
|
-
Interop: an upstream engine
|
|
836
|
-
functions (
|
|
837
|
-
`search-marker-format.test.ts` and
|
|
838
|
-
|
|
837
|
+
Interop: an upstream engine read the fork's bytes with its `…V2`
|
|
838
|
+
functions until YP24 (a string table per update) made the encoding the
|
|
839
|
+
fork's own; the baseline legs of `search-marker-format.test.ts` and
|
|
840
|
+
`yp16-writes.test.ts` now compare the stores each engine reads back from
|
|
841
|
+
its own encoding. Vendored execution lines 5,123 →
|
|
839
842
|
4,872. Test side: `compatibility.tests.js` (v1 documents of Yjs 13) is
|
|
840
843
|
removed; the upstream suite's helpers run every program in the one
|
|
841
844
|
encoding (`testHelper.js`, `updates.tests.js`).
|
|
@@ -869,13 +872,17 @@ Vendored execution lines 4,872 → 4,630; an app bundle 546.4 → 545.0 KB min
|
|
|
869
872
|
(minimal), 745.4 → 744.0 (every plugin): edytor's manager is 4.3 KB where
|
|
870
873
|
the engine's (without `redoItem`) and the hooks' driver were 5.6.
|
|
871
874
|
|
|
872
|
-
Test side:
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
`yp11-undo-hooks.test.ts`, `yp12-undo-withdraw.test.ts` (the
|
|
877
|
-
the rules are the facade history's `hist.undo.*` rows)
|
|
878
|
-
on `ignoreRemoteAttributeChanges
|
|
875
|
+
Test side: the tests keep no undo manager of their own. Every undo a test
|
|
876
|
+
runs is edytor's (`createUndoManager`, `document.history`, or over a raw
|
|
877
|
+
document `src/tests/crdt/harness/history.ts`: the same class with the same
|
|
878
|
+
rules, `historyRules`, its scope narrowed to the probed nodes). Removed with
|
|
879
|
+
their subject: `yp11-undo-hooks.test.ts`, `yp12-undo-withdraw.test.ts` (the
|
|
880
|
+
hooks' rows; the rules are the facade history's `hist.undo.*` rows), the
|
|
881
|
+
upstream rows on `ignoreRemoteAttributeChanges`, and later (the review of
|
|
882
|
+
PR #29) the test copy of the engine's `UndoManager` as it stood before YP18
|
|
883
|
+
(`harness/undo.js`, 528 lines), upstream's `undo-redo.tests.js` (20 rows of
|
|
884
|
+
that class) and its `testSubdocsUndo`; the undo/redo steps of the YP16
|
|
885
|
+
writes differential (an engine write path no more).
|
|
879
886
|
|
|
880
887
|
### YP19 — the engine ships what edytor calls: no plain formats, no observers, no subdocuments, no relative-position objects (`src/ynode.js`, `src/structs/Item.js`, `src/utils/Doc.js`, `src/utils/Transaction.js`, `src/utils/transaction-helpers.js`, `src/utils/encoding.js`, `src/utils/ids.js`, `src/utils/StructStore.js`, `src/utils/positions.js` (new), `src/index.js`, `global.d.ts`)
|
|
881
888
|
|
|
@@ -954,8 +961,8 @@ prototype, from `src/tests/setup/engine-reads.ts` in every vitest lane (and
|
|
|
954
961
|
on a stripped copy's node where a differential loads one). The harness's
|
|
955
962
|
`applyDelta` writes a formatted op as paired mark operations (an insert takes
|
|
956
963
|
exactly its formats, a retain sets the keys it names); the raw adapter marks
|
|
957
|
-
with `mark`; the harness's pre-YP18 `UndoManager`
|
|
958
|
-
`changedParentTypes` from `changed
|
|
964
|
+
with `mark`; the harness's pre-YP18 `UndoManager` derived
|
|
965
|
+
`changedParentTypes` from `changed` (the copy is gone, YP18's test side). Removed with their subject: the
|
|
959
966
|
upstream tests of observers and events, subdocuments, relative positions,
|
|
960
967
|
plain formatting and its cleanup (`y-text`, `y-map`, `y-array`, `doc`,
|
|
961
968
|
`delta`, `undo-redo`, `attribution`, `IdSet`, `y-xml`;
|
|
@@ -968,6 +975,546 @@ cleanup's delete-set walk). Ported: `v14-smoke` (marks, the `commit` event,
|
|
|
968
975
|
probes (planting through `findMarker`), `testStructReferences`,
|
|
969
976
|
`testKeyEncoding`. Upstream suite: 197 → 126 tests.
|
|
970
977
|
|
|
978
|
+
### YP20 — hold records: an attributed, reversible delete (`src/structs/Item.js`, `src/structs/AbstractStruct.js`, `src/ynode.js`, `src/utils/encoding.js`, `src/utils/ids.js`, `src/utils/StructStore.js`, `src/utils/Transaction.js`, `src/utils/transaction-helpers.js`, `src/utils/redo.js`)
|
|
979
|
+
|
|
980
|
+
Reason: the engine's delete is anonymous (a delete set names ids, not who
|
|
981
|
+
deleted them) and irreversible (a deleted item is deleted forever; an undo
|
|
982
|
+
re-creates COPIES). Edytor's product rules need a per-writer, reversible
|
|
983
|
+
text delete — a character two writers deleted shows again only once both
|
|
984
|
+
undid, an undo restores the same text in the same order, text another
|
|
985
|
+
author added is kept — so edytor carried a layer over the engine
|
|
986
|
+
(`crdt/text/deletes.ts`, `crdt/text/delete-index.ts`, 10.3 KB minified in
|
|
987
|
+
every app): per-writer delete marks, restoration records, copy lineage
|
|
988
|
+
(root, member, leaf), pending restorations, a hold rule for a peer's
|
|
989
|
+
concurrent restoration, a leftmost-wins dedup of concurrent copies, a
|
|
990
|
+
`gcFilter` keeping every deleted copy's content on every replica (the room
|
|
991
|
+
too), and follow-up transactions after remote changes. The fork now has the
|
|
992
|
+
delete, and the layer is gone. Design: `docs/archive/engine-delete/design.md`.
|
|
993
|
+
Edytor's schema generation 7 is the cutover (no conversion).
|
|
994
|
+
|
|
995
|
+
Patch (new code delimited by `// YP20 begin` / `// YP20 end`, edits marked `// YP20`):
|
|
996
|
+
|
|
997
|
+
- **`ContentHold`** (`Item.js`, content ref 11, `readContentHold` in
|
|
998
|
+
`ynode.js`'s `contentRefs`): one unit per held id range
|
|
999
|
+
(`client, clock, len`, written as a left id and a length); `copy`,
|
|
1000
|
+
`splice`, `mergeWith` (consecutive records of one writer merge, as a
|
|
1001
|
+
typed run does), `write`. Integrating it holds what it names; deleting it
|
|
1002
|
+
(an ordinary delete of the record item) releases. Records are immutable:
|
|
1003
|
+
the state that decides visibility is the items, the delete set, the
|
|
1004
|
+
records and the deleted records, so replicas converge in any order.
|
|
1005
|
+
- **`Item`**: `holds` (local, never encoded: how many live records name
|
|
1006
|
+
the item), `removed` (bit 3, the delete set's fact; `AbstractStruct`
|
|
1007
|
+
answers `deleted` for a GC struct or a Skip), and `deleted` = `removed ||
|
|
1008
|
+
holds > 0`, so every reader of visibility (cursors, positions, the range
|
|
1009
|
+
cursor, lengths, edytor's index) sees a held item as deleted. The paths
|
|
1010
|
+
that mean the engine's delete read `removed`: `Item#delete`'s guard (a
|
|
1011
|
+
held item is removed too, and logs a give-back: the delete set counts it
|
|
1012
|
+
hidden again), `Item#gc`, `Item#split` (the right part copies `holds`),
|
|
1013
|
+
`Item#mergeWith` (equal `removed` and `holds`), `Item#integrate`'s
|
|
1014
|
+
deleted-parent rule (a held atom's attrs integrate), `ContentType#delete`
|
|
1015
|
+
(a held child goes with its node), `createDeleteSetFromStructStore`,
|
|
1016
|
+
`readAndApplyDeleteSet`, `tryGcDeleteSet`, `redoItem`'s parent rule.
|
|
1017
|
+
- **Holding** (`holdRange`): the items present take the hold now, split at
|
|
1018
|
+
the range's edges; a range this replica lacks (past its clock, or a Skip)
|
|
1019
|
+
waits in `store.pendingHolds` (`client → [clock, len, delta, …]`), applied
|
|
1020
|
+
by `readUpdate` once an update of that client integrated
|
|
1021
|
+
(`applyPendingHolds`): a record never waits for its items, so a delete of
|
|
1022
|
+
text a room lost is stored and acknowledged at once. A GC struct takes
|
|
1023
|
+
nothing. An item that starts or stops being held (and is not removed)
|
|
1024
|
+
moves its parent's length, drops its search markers, joins
|
|
1025
|
+
`transaction.changed` and is logged in `transaction.holds`
|
|
1026
|
+
(`client, clock, ±len`, in order: the index's edits).
|
|
1027
|
+
- **`YNode#delete(index, length, into, fresh)`**: with `into` (the records
|
|
1028
|
+
list) the live units are held by one record appended there instead of
|
|
1029
|
+
removed, except those `fresh` names (the open undo step's inserts:
|
|
1030
|
+
removed, so that step's undo changes nothing); the search markers move as
|
|
1031
|
+
for a delete. **`YNode#hold(ids)`**: append a record holding `ids`.
|
|
1032
|
+
|
|
1033
|
+
Plain deletes (every upstream test) are untouched: with no record, `holds`
|
|
1034
|
+
is 0 everywhere and every changed path reduces to upstream's.
|
|
1035
|
+
|
|
1036
|
+
Edytor side: a text delete passes the records list (`crdt/text/model.ts`
|
|
1037
|
+
`deleteRange`); the history's undo deletes the undoer's record, a redo
|
|
1038
|
+
copies the record (never the text), an undo of typing holds the typed text,
|
|
1039
|
+
a dropped step removes what this writer's records of it hold
|
|
1040
|
+
(`hist.delete-marks.release`); the purge removes what a record older than
|
|
1041
|
+
the horizon holds; the index folds `transaction.holds`. Removed:
|
|
1042
|
+
`bindCrdt(Y).doc.keepCopies`, `PurgeReport.records`, the restoration
|
|
1043
|
+
records root, the engine object's `ContentAny`.
|
|
1044
|
+
|
|
1045
|
+
Measured: vendored execution lines 3,392 → 3,548 (+156); edytor's
|
|
1046
|
+
`text/deletes.ts` + `text/delete-index.ts` (699) removed. App bundle
|
|
1047
|
+
(`scripts/bundle-size.mjs`): minimal 423.8 → 417.0 KB own (470.6 → 463.7
|
|
1048
|
+
total, 162.0 → 159.4 KB gzip), every plugin 653.2 → 646.4 (701.8 → 695.0),
|
|
1049
|
+
`edytor/export` unchanged. `bench:crdt` (one machine, two runs each): a
|
|
1050
|
+
keystroke, a load and an encode unchanged within noise, a backspace
|
|
1051
|
+
(`scale`) 0.15 → 0.08 ms at 1k blocks, an undo (`history5000`) 0.72 →
|
|
1052
|
+
0.42 ms. A room's state after 1,100 text deletes in separate steps over
|
|
1053
|
+
200 blocks: 94,584 → 88,431 bytes (83,826 once the writer's history
|
|
1054
|
+
releases them).
|
|
1055
|
+
|
|
1056
|
+
Oracle: `src/tests/crdt/yp20-holds.test.ts` (a hold and its release show the
|
|
1057
|
+
same item; two writers in both orders; overlapping records; a peer's insert
|
|
1058
|
+
inside a held run; a record whose items arrive later; merged, diffed and
|
|
1059
|
+
reloaded updates; collection; `fresh`; three seeded differentials of held
|
|
1060
|
+
deletes, releases and inserts with search markers on and off, byte-identical
|
|
1061
|
+
after every step), the edytor rows `arch-v2/text-delete-marks.test.ts`,
|
|
1062
|
+
`delete-records-fold.test.ts`, `history-limit.test.ts` and the native
|
|
1063
|
+
contract programs (all 12 hold, `split.range-one-undo` included), the random
|
|
1064
|
+
corpus (every adapter), the upstream suite unchanged (`encoding.tests.js`
|
|
1065
|
+
pins ref 11).
|
|
1066
|
+
|
|
1067
|
+
### YP21 — claim items sort after content and are never collected (`src/utils/marks.js`, `src/structs/Item.js`)
|
|
1068
|
+
|
|
1069
|
+
Reason: a merge is a claim, "the display of this block continues with that
|
|
1070
|
+
block's". Edytor stored it in a `claims` list on the holder's node, so the
|
|
1071
|
+
claim had no place in the holder's text: a split its writer did not see could
|
|
1072
|
+
not tell which piece keeps it, and edytor carried a layer that anchored each
|
|
1073
|
+
claim to the end of its holder's stream as its writer saw it and moved it to
|
|
1074
|
+
the piece that ends that region after every fold (`crdt/text/index/anchored.ts`
|
|
1075
|
+
and the claim anchoring in `text/model.ts`, 2.4 KB minified, plus a `claims`
|
|
1076
|
+
node per block, 13 B stored). A claim is now one countable unit `{ m }` in the
|
|
1077
|
+
holder's backing text, at the end of the gap at the end of its stream (YP7's
|
|
1078
|
+
`insertAtGapEnd`), and YATA decides which piece of a concurrent split holds it.
|
|
1079
|
+
For that to give the serial outcome the engine only needs one sort rule:
|
|
1080
|
+
|
|
1081
|
+
- `markClass` (YP13's integration class) answers `3` for a claim item: a
|
|
1082
|
+
sequence item (`parentSub === null`) whose content's first value is an
|
|
1083
|
+
object with a string `m`. Among the items of one origin, left-side mark
|
|
1084
|
+
items, then content (boundaries included), then right-side mark items, then
|
|
1085
|
+
claim items. So a boundary a concurrent split writes at the claim's origin
|
|
1086
|
+
goes before it (the claim follows the split's last piece), and so does text
|
|
1087
|
+
a concurrent writer types there (it stays in the holder, before the claimed
|
|
1088
|
+
text); a writer who saw the claim writes before it anyway (its insert's
|
|
1089
|
+
right origin is the claim).
|
|
1090
|
+
- `Item#gc` keeps the content of every item whose class is not `1` (paired
|
|
1091
|
+
mark items, YP13, and claim items): the class orders concurrent inserts on
|
|
1092
|
+
every replica, collected or not. The check was YP13's paired-mark test; it
|
|
1093
|
+
is now `markClass(this) === 1`, the same answer for every format item.
|
|
1094
|
+
(YP29: a collected mark or claim keeps only its class.)
|
|
1095
|
+
|
|
1096
|
+
Plain items (every upstream test) are untouched: a sequence of strings,
|
|
1097
|
+
nodes or JSON values without an `m` key, and every map value, read class `1`.
|
|
1098
|
+
|
|
1099
|
+
Edytor side: schema generation 8 (no conversion). A merge writes the claim
|
|
1100
|
+
item (`claimInto`; a streamless block gets its own text first); a block's
|
|
1101
|
+
claims are its implicit ones (`id.same.concurrent`), then the claim items of
|
|
1102
|
+
its stream, which the index's rows track as they track boundaries (a claim
|
|
1103
|
+
item is a row bound that never delimits, so no read shows it); a split writes
|
|
1104
|
+
the claims that follow its point again at the end of the new block's stream
|
|
1105
|
+
and deletes their items. Removed: the `claims` node, `MergeClaim.a`/`r`,
|
|
1106
|
+
`Claim.seqIndex`/`holder`, `TextBlockRec.claimsNode`/`listClaims`,
|
|
1107
|
+
`readClaims`, the index's anchored part.
|
|
1108
|
+
|
|
1109
|
+
Oracle: `src/tests/crdt/text/merge-claim-anchor.test.ts` (48 client-id pairs,
|
|
1110
|
+
both delivery orders, duplicate delivery, a reload: the serial outcomes),
|
|
1111
|
+
`src/tests/crdt/yp21-claims.test.ts` (the sort rule against content,
|
|
1112
|
+
boundaries and marks on every pair of client ids, collection), the index
|
|
1113
|
+
self-checks (each record's claims equal a fresh scan's), the random corpus,
|
|
1114
|
+
the upstream suite unchanged.
|
|
1115
|
+
|
|
1116
|
+
### YP22 — move records: an attributed, reversible, order-independent move (`src/structs/Item.js`, `src/ynode.js`, `src/utils/marks.js`, `src/utils/Doc.js` (`_clock`), `src/utils/Transaction.js`, `src/utils/StructStore.js`, `src/utils/encoding.js`, `src/index.js`, `global.d.ts`)
|
|
1117
|
+
|
|
1118
|
+
Reason: the engine has no move. Edytor placed each block by a map of
|
|
1119
|
+
placement candidates (`at`: `{p: parent, r: rank}` under an ever-growing
|
|
1120
|
+
per-writer key) and ordered an array's items by a rank attr per item
|
|
1121
|
+
(`~id#`, `~id>`), so it carried a dense-key codec (`placement/rank.ts`,
|
|
1122
|
+
2.7 KB), source ranks, a candidate argmax with a runner-up fallback, a
|
|
1123
|
+
global cycle pass, and ranks that grew with every insert at one spot (39
|
|
1124
|
+
characters after 300 Enters) and a 37.7 B `at` node per block. The fork now
|
|
1125
|
+
has the move, built like YP20's hold record (Kleppmann's list move: a
|
|
1126
|
+
position is a YATA item, an element its original item), and the layer is
|
|
1127
|
+
gone. Design: `docs/archive/engine-move/design.md`. Edytor's schema
|
|
1128
|
+
generation 9 is the cutover (no conversion).
|
|
1129
|
+
|
|
1130
|
+
Patch (new code delimited by `// YP22 begin` / `// YP22 end`, edits marked `// YP22`):
|
|
1131
|
+
|
|
1132
|
+
- **`ContentMove`** (`Item.js`, content ref 12, `readContentMove` in
|
|
1133
|
+
`ynode.js`'s `contentRefs`): one countable unit at its destination
|
|
1134
|
+
holding a Lamport `priority`, a `side` (`0` after, `1` before), the
|
|
1135
|
+
ordered id ranges (`client, clock, len`) of the units it moves and, for
|
|
1136
|
+
the sort of two records at one origin, the chain it stands at (`at`: the
|
|
1137
|
+
ids of the units from the root sequence down to the unit it stands at,
|
|
1138
|
+
outermost first — for a block, the home slots of its ancestors, then its
|
|
1139
|
+
own; `null`: the first unit it names alone) and its `part` there (0–7,
|
|
1140
|
+
the writer's order among the records standing at one unit); `copy` keeps
|
|
1141
|
+
them all (a redo copies the record, never the units); its one unit's
|
|
1142
|
+
value (`getContent`) is the record itself; `write` puts
|
|
1143
|
+
priority, whether a chain follows, part and side in one varint
|
|
1144
|
+
(`priority · 32 + 16 · anchored + part · 2 + side`), then the chain (its
|
|
1145
|
+
length, then its ids), then the ranges as a hold record's (one writer,
|
|
1146
|
+
`writeIds`, and one reader, `readIds`, for both). `Item#delete` hands
|
|
1147
|
+
the deleted item to its content's `delete`, so a record drops itself
|
|
1148
|
+
from its units' movers without keeping its item.
|
|
1149
|
+
A record never moves a record (such ids are skipped). Records are
|
|
1150
|
+
immutable and deleted at most once: the state that decides a unit's place
|
|
1151
|
+
is the items, the records and the deleted records, so replicas converge
|
|
1152
|
+
in any order — no cleanup message, no replay (what sank upstream's
|
|
1153
|
+
`ContentMove`, PR #357).
|
|
1154
|
+
- **`Item`**: `movers` (local, never encoded: the live records naming the
|
|
1155
|
+
item, kept strongest first by `(priority, client, clock)`), `mover` (the
|
|
1156
|
+
first of them), `deleted` =
|
|
1157
|
+
`removed || holds > 0 || movers !== null` (hidden at home), `gone` =
|
|
1158
|
+
`removed || holds > 0` (shown nowhere: a delete or a hold beats a move,
|
|
1159
|
+
Collabs' and Liveblocks' rule, not Loro's resurrection). `Item#delete`
|
|
1160
|
+
of a moved item logs it (`tr.moves`) and moves no length; `Item#split`
|
|
1161
|
+
copies `movers`; `Item#mergeWith` requires both unmoved; `Item#gc` keeps
|
|
1162
|
+
a record's content (its class orders concurrent inserts, as YP21's).
|
|
1163
|
+
- **Moving** (YP20's walk: `holdRange`/`holdIds`/`holdItem` take the record,
|
|
1164
|
+
`null` for a hold): the items a record names take
|
|
1165
|
+
it when it integrates, split at the range's edges. A record waits for
|
|
1166
|
+
its units and its chain as an item waits for its origin (`getMissing` in
|
|
1167
|
+
`encoding.js`: a named id past a client's clock, or one a Skip stands
|
|
1168
|
+
for, is the missing dependency — a skip counts in the clock but holds no
|
|
1169
|
+
item to sort by), so they are present whenever its place is decided and
|
|
1170
|
+
whenever another record at its origin is sorted against it
|
|
1171
|
+
(`sourceOrder`), in one update or in many; `applyPending` applies YP20's
|
|
1172
|
+
pending holds only. A
|
|
1173
|
+
unit that starts or stops being moved (and is not gone) moves
|
|
1174
|
+
its home sequence's length and drops its search markers; one whose mover
|
|
1175
|
+
changed is logged in `transaction.moves` (`client, clock, len`, in
|
|
1176
|
+
order), and its home and its new mover's sequences are marked changed.
|
|
1177
|
+
- **`doc._clock`** (was YP13's `_markClock`): the greatest Lamport
|
|
1178
|
+
timestamp of a mark or a move this document integrated; a local mark or
|
|
1179
|
+
move takes `_clock + 1`, so a move made after seeing another wins over it
|
|
1180
|
+
(Yjs ids are not Lamport timestamps). A move record raises `_clock` only
|
|
1181
|
+
when it comes from a peer (`!transaction.local`): a writer's own records
|
|
1182
|
+
keep one priority until it sees a peer's, so its gestures — one plan or
|
|
1183
|
+
several between syncs — win or lose together against a concurrent peer's
|
|
1184
|
+
(between two records of one writer at one priority the later clock wins),
|
|
1185
|
+
and two peers splitting one list at once keep each block where one of them
|
|
1186
|
+
put it, never half of a head's items with each.
|
|
1187
|
+
- **`YNode#move(index, ids, side, part, at)`**: one record at live index
|
|
1188
|
+
`index` (in the gap there, `insertInGapHelper`).
|
|
1189
|
+
- **Sort classes** (`marks.js` `markClass`, `sortsBefore`): among the items
|
|
1190
|
+
of one origin, a move record sorts after every slot (class `4 + side`:
|
|
1191
|
+
a split's pieces first, then the after-records, then the before-records
|
|
1192
|
+
— the order every serial run of the gestures gives, DR-crdt-6); two
|
|
1193
|
+
records of one class sort by the chains they stand at (`sourceOrder`),
|
|
1194
|
+
level by level from the root: two units of one sequence by
|
|
1195
|
+
`sequenceOrder` (the walk from each item leftward until one
|
|
1196
|
+
meets the other, the distance between them; one item: by clock), a unit
|
|
1197
|
+
before the units inside its block (the shorter chain first); chains that
|
|
1198
|
+
meet in no sequence (or reach a unit a GC struct replaced) order nothing,
|
|
1199
|
+
and the part, then the stronger record decide — so two peers that outdent
|
|
1200
|
+
or lift blocks out of one list, or of nested lists, into one gap at once
|
|
1201
|
+
keep the list's order there whatever their client ids (CW-01). The
|
|
1202
|
+
record names everything its sort reads, so the order needs no document
|
|
1203
|
+
hook (the prototype's `_unitOrder` read the chains from the sequences at
|
|
1204
|
+
integration time, and a replica that took a record before an ancestor's
|
|
1205
|
+
slot, inside one update, compared it by client id); and among block slots (`{ b }` units)
|
|
1206
|
+
the split key first (`s`: the id of the boundary item of the split that
|
|
1207
|
+
made the block; two such slots sort by their boundaries' order in the
|
|
1208
|
+
text, `sequenceOrder`, and before a slot no split made, so two peers'
|
|
1209
|
+
splits of one block, with the lines a paste adds at its split, read in
|
|
1210
|
+
the text's order whatever each typed meanwhile, `order.split.text`), then
|
|
1211
|
+
the origin writer's own slots first (`extendsRun`): the lines one writer
|
|
1212
|
+
types after its own line stay together around a peer's line inserted
|
|
1213
|
+
there (`order.insert.run`). The two rules read a unit one way (`unitAt`:
|
|
1214
|
+
an id's item and clock, `null` for a GC struct) and order two units one
|
|
1215
|
+
way (`unitOrder`: by clock in one item, else `sequenceOrder`). Each is a
|
|
1216
|
+
total order per origin
|
|
1217
|
+
that every replica computes alike — `sourceOrder` because a record
|
|
1218
|
+
integrates only once its units and its chain have (above), and a unit's
|
|
1219
|
+
place in its sequence never changes.
|
|
1220
|
+
|
|
1221
|
+
Plain items (every upstream test) are untouched: with no record, `movers`
|
|
1222
|
+
is `null` everywhere, `extendsRun` is false for every array or text item,
|
|
1223
|
+
and every changed path reduces to upstream's.
|
|
1224
|
+
|
|
1225
|
+
Edytor side: each data owner keeps one order sequence, its node's list
|
|
1226
|
+
part (`placement/slots.ts`): a block's children are home slots `{ b }`
|
|
1227
|
+
there (the root's in the registry root's list part), an array's items slots
|
|
1228
|
+
`{ p, i }`, and a move one record naming the moved units in order. The
|
|
1229
|
+
index reads the sequences, keeps an in-memory order key per child
|
|
1230
|
+
(`placement/keys.ts`), re-places only the blocks whose slot, record or
|
|
1231
|
+
mover changed (`transaction.moves`, the slots and records a fold touched)
|
|
1232
|
+
and resolves a cycle locally: a block's candidate places are its live
|
|
1233
|
+
records, strongest first, then its home slot, and the block whose move is
|
|
1234
|
+
the oldest edge of a cycle is back where it stood (its next record, else
|
|
1235
|
+
home; the root's tail only when every place closes a cycle,
|
|
1236
|
+
`placement/resolve.ts`); writers name each record's chain (the home slots
|
|
1237
|
+
above the unit it stands at) and anchor and carry where the index shows a
|
|
1238
|
+
block; the purge drops the old records no block shows at and the slots of
|
|
1239
|
+
removed blocks.
|
|
1240
|
+
Removed: `placement/rank.ts`, `placement/source.ts`, the `at` nodes, the
|
|
1241
|
+
candidate argmax and runner-up, the global resolution pass, the ranks of
|
|
1242
|
+
data arrays.
|
|
1243
|
+
|
|
1244
|
+
Measured: see `docs/archive/engine-move/design.md` ("Prototype: measured").
|
|
1245
|
+
|
|
1246
|
+
Oracle: `src/tests/crdt/yp22-moves.test.ts` (two writers move one unit, in
|
|
1247
|
+
both orders; a group move overlapping a single move; undo, redo and a
|
|
1248
|
+
fallback; a record whose units arrive later; merged, diffed and reloaded
|
|
1249
|
+
updates; the sort classes; three seeded differentials with search markers
|
|
1250
|
+
on and off, byte-identical after every step; three replicas syncing in
|
|
1251
|
+
random orders), the placement rows (MV01–MV10, the order sweeps,
|
|
1252
|
+
`display-cycle`, `reachability`, `same-id-concurrent`), the index
|
|
1253
|
+
self-checks (each placement's parent and each parent's order equal the
|
|
1254
|
+
rebuild from the sequences), the random corpus (every adapter), the
|
|
1255
|
+
upstream suite unchanged (`encoding.tests.js` pins ref 12).
|
|
1256
|
+
|
|
1257
|
+
### YP23 — an update's block set is trimmed to every known range (`src/utils/BlockSet.js`)
|
|
1258
|
+
|
|
1259
|
+
Reason: before an update integrates, `readUpdate` removes from its block set
|
|
1260
|
+
what the store already has (`BlockSet.exclude`: the state below the client's
|
|
1261
|
+
clock, minus the store's skips). Upstream read a client's first and last
|
|
1262
|
+
struct once, before the loop over the known ranges; trimming the first range
|
|
1263
|
+
splits a struct in place (`findIndexCleanStart`), so for the next range the
|
|
1264
|
+
bounds were stale and the range was skipped. A struct spanning a skip, known
|
|
1265
|
+
ids and the end of the state — a GC of a whole removed subtree, after a
|
|
1266
|
+
dropped message left a gap — integrated whole over the known middle: its
|
|
1267
|
+
delete-set entry named ids the store never had, and the transaction's
|
|
1268
|
+
cleanup threw in `tryMerge` (`findIndexSS`: "Unexpected case", or reading
|
|
1269
|
+
`id` of `undefined`). The corpus's `seed-140` raw repro was this
|
|
1270
|
+
(`upstream-engine-crash`, tracked since the v14 cutover); the move records
|
|
1271
|
+
(YP22) made the raw adapter's runs reach it in a dozen more seeds.
|
|
1272
|
+
|
|
1273
|
+
Patch (edits marked `// YP23`): `exclude` reads `firstStruct` and
|
|
1274
|
+
`lastStruct` per range, and a client without structs or ranges is skipped
|
|
1275
|
+
(`continue`), not the whole exclusion (`return`).
|
|
1276
|
+
|
|
1277
|
+
Oracle: `src/tests/crdt/yp23-exclude.test.ts` (a block set holding one GC
|
|
1278
|
+
over 1..59 trimmed by the known ranges 0..5 and 9..34 keeps a skip per range
|
|
1279
|
+
and the structs only where the store has nothing; a replica that
|
|
1280
|
+
integrated around a gap takes the full state of the writer that removed the
|
|
1281
|
+
subtree without throwing, and ends at its state vector), the random corpus's
|
|
1282
|
+
raw adapter (seed 140 passes: its pin is gone).
|
|
1283
|
+
|
|
1284
|
+
### YP24 — one string table per update (`src/utils/UpdateEncoder.js`, `src/utils/UpdateDecoder.js`, `src/structs/Item.js`, `src/utils/updates.js`, `src/utils/BlockSet.js`)
|
|
1285
|
+
|
|
1286
|
+
Reason: the v2 encoding writes every map key, root name and string value
|
|
1287
|
+
in full, every time. Upstream's `writeKey` has a cache, commented out
|
|
1288
|
+
(`keyMap.set`) because decoders older than its own read format keys with
|
|
1289
|
+
`readString`, and map keys and root names do not go through it anyway
|
|
1290
|
+
(`writeString` in `Item#write`, `readString` in both struct readers). In an
|
|
1291
|
+
edytor document a block's id (12 characters) was written five times (its
|
|
1292
|
+
registry key, its `id` attr, its home slot `{ b }`, its record key, the
|
|
1293
|
+
boundary of the split that made it), and `"blocks"`, `"blockattr"`, `"id"`,
|
|
1294
|
+
`"type"`, `"paragraph"`, an actor's id, once per block each: about a fifth
|
|
1295
|
+
of a stored document. Design and measurements:
|
|
1296
|
+
`docs/archive/storage-diet/design.md`. Edytor's schema generation 10 is the
|
|
1297
|
+
cutover (no conversion).
|
|
1298
|
+
|
|
1299
|
+
Patch (new code delimited by `// YP24 begin` / `// YP24 end`, edits marked `// YP24`):
|
|
1300
|
+
|
|
1301
|
+
- **The string table.** `UpdateEncoder#writeKey` keeps its cache: a string
|
|
1302
|
+
the update has written is its index. The key column (`keyClockEncoder`)
|
|
1303
|
+
is a `UintOptRleEncoder` saying `0` for a new string (the next in the
|
|
1304
|
+
string column) and the index plus one for a known one, so a key repeated
|
|
1305
|
+
block after block is the same small number each time (gzip finds it; a
|
|
1306
|
+
running difference, upstream's `IntDiffOptRle`, did not).
|
|
1307
|
+
`UpdateDecoder#readKey` reads it back (an index past the table is
|
|
1308
|
+
refused).
|
|
1309
|
+
- **Keys and root names through it.** `Item#write` writes a map entry's
|
|
1310
|
+
key, a root parent's name and a string parent with `writeKey`; both
|
|
1311
|
+
struct readers (`updates.js`, `BlockSet.js`) read them with `readKey`.
|
|
1312
|
+
Node names and format keys already went through `writeKey`.
|
|
1313
|
+
- **Content values through it.** `writeAny`/`writeJSON` write lib0's
|
|
1314
|
+
`writeAny` layout with every string and object key through the table
|
|
1315
|
+
(`writeValue`); `readAny`/`readJSON` read it back (`readValue`). Numbers,
|
|
1316
|
+
booleans, `null`, `undefined` and binary keep lib0's codec. Text
|
|
1317
|
+
(`ContentString`) stays in the string column, never in the table.
|
|
1318
|
+
- **The encoding's name.** The update's first varuint, upstream's reserved
|
|
1319
|
+
feature flag (`0`), says `1` (`UPDATE_ENCODING`); the decoder refuses an
|
|
1320
|
+
update of another encoding before it reads anything (`an update of
|
|
1321
|
+
another encoding`): an upstream update, or one of edytor's generations 6
|
|
1322
|
+
to 9, is refused, never mis-read.
|
|
1323
|
+
|
|
1324
|
+
The tables are built in write order, so an update is still a function of
|
|
1325
|
+
the store (byte-identical for identical stores, as the fork's differential
|
|
1326
|
+
rows check), and every path that re-encodes (`mergeUpdates`, `diffUpdate`,
|
|
1327
|
+
a block set's re-write, the room's stripped frames) writes through the
|
|
1328
|
+
encoder's own methods.
|
|
1329
|
+
|
|
1330
|
+
Plain programs (every upstream test) are untouched in what they store; their
|
|
1331
|
+
bytes differ from upstream's.
|
|
1332
|
+
|
|
1333
|
+
Measured: see `docs/archive/storage-diet/design.md` ("Prototype: measured").
|
|
1334
|
+
|
|
1335
|
+
Oracle: the upstream suite (`updates.tests.js` merges, diffs and re-encodes
|
|
1336
|
+
every program), `src/tests/crdt/yp24-values.test.ts` (every value shape), `src/tests/crdt/yp16-writes.test.ts` and the baseline leg
|
|
1337
|
+
of `search-marker-format.test.ts` (the stores each engine reads back from
|
|
1338
|
+
its own encoding, `src/tests/crdt/harness/stored.js`, after every
|
|
1339
|
+
operation), `generation.test.ts` (an earlier encoding is refused before
|
|
1340
|
+
decode), `boundary-attacks/envelope.test.ts` (a v13 update inside a v14
|
|
1341
|
+
frame no longer integrates), the random corpus (every adapter).
|
|
1342
|
+
|
|
1343
|
+
### YP25 — a paired mark's value is written in parts (`src/structs/Item.js`, `src/ynode.js`, `src/utils/updates.js`, `src/utils/BlockSet.js`)
|
|
1344
|
+
|
|
1345
|
+
Reason: a paired mark (YP13) is a format item whose value is a JSON array —
|
|
1346
|
+
a start's `[v, l, c, k, side]` (its value, Lamport stamp, operation id and
|
|
1347
|
+
side), an end's `[c, k, side]` (its start's id) — written through
|
|
1348
|
+
`writeAny`, so a 53-bit client was a 9-byte float64 in every one, a start's
|
|
1349
|
+
operation id repeated the item's own id, and an end's repeated its start's,
|
|
1350
|
+
right before it: 33 bytes an item, 66 a mark. Design and measurements:
|
|
1351
|
+
`docs/archive/storage-diet/design.md`. Schema generation 10 (YP24's).
|
|
1352
|
+
|
|
1353
|
+
Patch (new code delimited by `// YP25 begin` / `// YP25 end`, edits marked `// YP25`):
|
|
1354
|
+
|
|
1355
|
+
- `ContentFormat#write` takes the item (`Item#write` passes itself to its
|
|
1356
|
+
content's `write`) and writes, after its key, `writeFormatValue`: a flags
|
|
1357
|
+
length — bit 0 the side; bit 1, for a start, its operation id is the
|
|
1358
|
+
item's own (a redo's copy keeps the original's: then the id follows), for
|
|
1359
|
+
an end, its start is the item's client's (the distance back to its clock
|
|
1360
|
+
follows), else the start's id; bit 2 a start's value is `true`, bit 3
|
|
1361
|
+
`null` — then a start's stamp (a length) and, unless implied, its id and
|
|
1362
|
+
value. Any other value (a plain format, YP19's inert one; a value of
|
|
1363
|
+
another shape) is flagged `16` and written whole.
|
|
1364
|
+
- `readContentFormat(decoder, id)` reads it back (`readFormatValue`, given
|
|
1365
|
+
the item's id): both struct readers pass `readItemContent` the id they
|
|
1366
|
+
create the item with.
|
|
1367
|
+
|
|
1368
|
+
The value read back is the array written, so the mark rules (`foldPaired`,
|
|
1369
|
+
`markClass`) see what they saw. Plain programs keep their stores.
|
|
1370
|
+
|
|
1371
|
+
Oracle: `src/tests/crdt/yp25-marks.test.ts` (every shape — own and copied
|
|
1372
|
+
ids, an end of another writer's start, `true`, `null`, string and object
|
|
1373
|
+
values, plain formats, values of another shape — read back through
|
|
1374
|
+
`applyUpdate`, `decodeUpdate`, `mergeUpdates` and `diffUpdate`, the same
|
|
1375
|
+
store giving the same bytes; a mark's cost), the mark rows (`mark.*`), the
|
|
1376
|
+
random corpus (every adapter).
|
|
1377
|
+
|
|
1378
|
+
### YP26 — one client table per update (`src/utils/UpdateEncoder.js`, `src/utils/UpdateDecoder.js`, `src/utils/ids.js`)
|
|
1379
|
+
|
|
1380
|
+
Reason: a client id (53 bits since v14: an 8-byte varint) was written in
|
|
1381
|
+
full wherever an update names it — each client's struct section, every
|
|
1382
|
+
origin, right origin and parent id, the delete set — and the client
|
|
1383
|
+
column's run-length code only helps while consecutive structs name one
|
|
1384
|
+
client. A document edited over many sessions (each page load is a client)
|
|
1385
|
+
names another session's text at most of its origins: 10 to 12 % of its
|
|
1386
|
+
bytes were client ids. Design and measurements:
|
|
1387
|
+
`docs/archive/storage-diet/design.md`. Schema generation 10 (YP24's).
|
|
1388
|
+
|
|
1389
|
+
Patch (new code delimited by `// YP26 begin` / `// YP26 end`, edits marked `// YP26`):
|
|
1390
|
+
|
|
1391
|
+
- `UpdateEncoder` keeps a client table: `clientIndex(client)` writes a
|
|
1392
|
+
client's index in it, in first-use order, to the client column, and a new
|
|
1393
|
+
client's index (the table's size) is followed there by its id, once;
|
|
1394
|
+
`writeClient`, `writeLeftID` and `writeRightID` write through it.
|
|
1395
|
+
- `UpdateDecoder#readClient` (and `readLeftID`, `readRightID`) reads an
|
|
1396
|
+
index: the table's size is a new client, whose id follows; one past the
|
|
1397
|
+
table is refused (`an unknown client`).
|
|
1398
|
+
- The delete set's client goes through the same table: `writeIdSet` writes
|
|
1399
|
+
it with `writeDsClient`, `readIdSet` and `readAndApplyDeleteSet` read it
|
|
1400
|
+
with `readDsClient` (an `IdSetEncoder`/`IdSetDecoder` alone keeps the
|
|
1401
|
+
plain varint).
|
|
1402
|
+
|
|
1403
|
+
Every path that writes an update writes its clients through these methods
|
|
1404
|
+
(the struct sections, `writeStructs`, the lazy writer of `mergeUpdates` and
|
|
1405
|
+
`diffUpdate`, a block set's re-write, the room's stripped frames, the
|
|
1406
|
+
delete-only updates), in the order a reader reads them, so an update stays
|
|
1407
|
+
a function of the store. The state vector keeps its own encoding.
|
|
1408
|
+
|
|
1409
|
+
Oracle: `src/tests/crdt/yp26-clients.test.ts` (twelve 53-bit sessions
|
|
1410
|
+
typing, deleting and syncing: their store reads back through `applyUpdate`,
|
|
1411
|
+
`decodeUpdate`, `mergeUpdates` and `diffUpdate`; two interleaved writers
|
|
1412
|
+
cost their ids once; an index past the table is refused), the upstream
|
|
1413
|
+
suite, the random corpus (every adapter).
|
|
1414
|
+
|
|
1415
|
+
### YP27 — the parent kind in the info byte (`src/structs/Item.js`, `src/utils/UpdateEncoder.js`, `src/utils/UpdateDecoder.js`, `src/utils/updates.js`, `src/utils/BlockSet.js`, `src/ynode.js`)
|
|
1416
|
+
|
|
1417
|
+
Reason: an item written with its parent (no origin, no right origin: a map
|
|
1418
|
+
entry's first value, a list's first item) said in a column of its own,
|
|
1419
|
+
`parentInfo` (a run-length code of a flag), whether the parent is a root
|
|
1420
|
+
(its name follows) or an item (its id follows). In an edytor document the
|
|
1421
|
+
flag alternates block after block (a registry entry under a root, then its
|
|
1422
|
+
attrs under the block's node), so the run-length code wrote two bytes a
|
|
1423
|
+
change: 8 bytes a block. Content refs fit four bits (0 to 12), so the
|
|
1424
|
+
info byte has room. Schema generation 10 (YP24's).
|
|
1425
|
+
|
|
1426
|
+
Patch (edits marked `// YP27`): `Item#write` sets bit 5 (`BIT5`) of the
|
|
1427
|
+
info byte when the parent it writes is a root's name (or a string parent,
|
|
1428
|
+
the differential updates' edge case) and masks the content ref with
|
|
1429
|
+
`BITS4`; both struct readers read the bit instead of the column, and
|
|
1430
|
+
`readItemContent` masks with `BITS4`. `UpdateEncoder#writeParentInfo`,
|
|
1431
|
+
`UpdateDecoder#readParentInfo` and the `parentInfo` column are removed.
|
|
1432
|
+
|
|
1433
|
+
Oracle: the upstream suite, every engine row reading stores back
|
|
1434
|
+
(`yp16-writes`, `yp25-marks`, `yp26-clients`), the random corpus (every
|
|
1435
|
+
adapter).
|
|
1436
|
+
|
|
1437
|
+
### YP28 — large integers in content values (`src/utils/UpdateEncoder.js`, `src/utils/UpdateDecoder.js`)
|
|
1438
|
+
|
|
1439
|
+
Reason: lib0's `writeAny` writes an integer past 31 bits as a float64 (9
|
|
1440
|
+
bytes). Edytor's values hold two kinds of them: a block's incarnation nonce
|
|
1441
|
+
(32 bits, written three times a block: the node's `n`, its record's `i`, the
|
|
1442
|
+
`n` of the boundary of the split that made it; past 2³¹ half the time) and
|
|
1443
|
+
a block slot's split key (`s: [client, clock]`, the id of that boundary: a
|
|
1444
|
+
53-bit client). Schema generation 10 (YP24's).
|
|
1445
|
+
|
|
1446
|
+
Patch (edits marked `// YP28`, in YP24's value codec): an integer past 31
|
|
1447
|
+
bits a varint holds exactly is tag `115` and a varint; a two-element array
|
|
1448
|
+
of such an integer and a non-negative integer (an item id) is tag `114`,
|
|
1449
|
+
its first through the client table (YP26) and its second a varint. Both
|
|
1450
|
+
read back as the same number and the same array, whatever the value means.
|
|
1451
|
+
|
|
1452
|
+
Oracle: `src/tests/crdt/yp24-values.test.ts` (every value shape — strings,
|
|
1453
|
+
nested objects and arrays, integers either side of 31 bits and of the safe
|
|
1454
|
+
range, floats, booleans, `null`, binary, pairs of every kind — reads back
|
|
1455
|
+
through `applyUpdate`, `mergeUpdates` and `diffUpdate`; a repeated string
|
|
1456
|
+
costs one table entry), the random corpus (every adapter).
|
|
1457
|
+
|
|
1458
|
+
### YP29 — a collected mark or claim keeps only its class (`src/structs/Item.js`, `src/utils/marks.js`, `src/ynode.js`)
|
|
1459
|
+
|
|
1460
|
+
Reason: YP13, YP21 and YP22 keep the content of every collected item whose
|
|
1461
|
+
sort class (`markClass`) is not content's: the class orders concurrent
|
|
1462
|
+
inserts at the item's origin on every replica, collected or not. A paired
|
|
1463
|
+
mark item's or a claim item's class is all a sort reads of it (two marks or
|
|
1464
|
+
two claims of one class at one origin sort by client id), yet each kept its
|
|
1465
|
+
key and value. Edytor's purge now deletes the marks of the streams it
|
|
1466
|
+
empties (`room.purge.what`), and an undone format operation's marks are
|
|
1467
|
+
collected too. A move record still keeps its content: the sort of two
|
|
1468
|
+
records at one origin reads their chains. Schema generation 10 (YP24's).
|
|
1469
|
+
|
|
1470
|
+
Patch (new code delimited by `// YP29 begin` / `// YP29 end`, edits marked `// YP29`):
|
|
1471
|
+
|
|
1472
|
+
- **A collected item's class** (`Item.js`): `ContentDeleted` takes an
|
|
1473
|
+
optional `cls` (`0` a left-side mark, `2` a right-side one, `3` a claim)
|
|
1474
|
+
for each of its units; with one it is content ref 13
|
|
1475
|
+
(`readContentClass` in `ynode.js`'s `contentRefs`, its length and class
|
|
1476
|
+
in one length word: `len · 4 + cls`), countable as the claim it was, not
|
|
1477
|
+
as a mark; it splits and merges with its class, and integrates deleted as
|
|
1478
|
+
any `ContentDeleted`.
|
|
1479
|
+
- `Item#gc` (an item of a live parent) gives a collected item of class 0 or
|
|
1480
|
+
2, or a claim item every unit of which is a claim, a `ContentDeleted` of
|
|
1481
|
+
its class; class 1 (content, a block slot's split key) and the move
|
|
1482
|
+
records (4, 5) keep what they kept.
|
|
1483
|
+
- `markClass` reads a collected item's class first.
|
|
1484
|
+
|
|
1485
|
+
Every reader of a mark or a claim reads live items only (`forward`,
|
|
1486
|
+
`findMarker`'s walk, `RangeCursor#_foldItem`, edytor's rows and scans), so
|
|
1487
|
+
a collected one's content was never read but by the sort.
|
|
1488
|
+
|
|
1489
|
+
Oracle: `src/tests/crdt/yp21-claims.test.ts` (a collected claim item keeps
|
|
1490
|
+
its class, and a replica that receives it integrates it deleted, with its
|
|
1491
|
+
class), `purge-before-horizon.test.ts` (the marks of a stream the purge
|
|
1492
|
+
empties go; a live block keeps its own), the upstream suite
|
|
1493
|
+
(`encoding.tests.js` pins ref 13), the mark rows, the random corpus (every
|
|
1494
|
+
adapter).
|
|
1495
|
+
|
|
1496
|
+
### YP30 — a transaction's `beforeCommit` (`src/utils/Transaction.js`, `src/utils/Doc.js`)
|
|
1497
|
+
|
|
1498
|
+
Reason: edytor writes a replica's actor binding (`c/<clientID>`,
|
|
1499
|
+
`attr.binding.first-write`) with the replica's first write, inside that
|
|
1500
|
+
write's transaction, so a replica that only reads writes nothing and every
|
|
1501
|
+
struct a replica writes travels in the update that binds it. No event ran
|
|
1502
|
+
inside a transaction after its function: `beforeTransaction` comes before
|
|
1503
|
+
anything is written, `commit` after the transaction closed (a write there is
|
|
1504
|
+
a transaction of its own, a second update).
|
|
1505
|
+
|
|
1506
|
+
Patch (new code delimited by `// YP30 begin` / `// YP30 end`): `transact`,
|
|
1507
|
+
for the outermost call, emits `beforeCommit` (transaction, doc) after the
|
|
1508
|
+
function returned or threw, while `doc._transaction` is still set, so a
|
|
1509
|
+
listener's writes join the transaction (its insert and delete sets, its one
|
|
1510
|
+
update, the `commit` fold); then the cleanup runs as before, also when a
|
|
1511
|
+
listener throws. Emitted only when the document has a listener. Not
|
|
1512
|
+
emitted for a nested call.
|
|
1513
|
+
|
|
1514
|
+
Oracle: `src/tests/crdt/attribution/first-write-binding.test.ts` (the first
|
|
1515
|
+
local edit's one update carries the binding; a remote update writes none),
|
|
1516
|
+
the upstream suite.
|
|
1517
|
+
|
|
971
1518
|
## Generated declarations (`dts/`)
|
|
972
1519
|
|
|
973
1520
|
`svelte-package` copies JS verbatim but emits no `.d.ts` for JS inputs, so
|
|
@@ -1019,7 +1566,7 @@ diff -r "$UP/tests" /tmp/yjs-tests-normalized
|
|
|
1019
1566
|
Runner: `vendor-tests/yjs/upstream.test.js` registers every `testXxx(tc)`
|
|
1020
1567
|
export as a vitest test with a real `lib0-v14/testing` `TestCase`.
|
|
1021
1568
|
|
|
1022
|
-
- `pnpm test:crdt` — **120 pass / 6 skipped / 0 failed** since YP19 (196/6
|
|
1569
|
+
- `pnpm test:crdt` — **120 pass / 6 skipped / 0 failed** since YP19, unchanged by YP20 (196/6
|
|
1023
1570
|
since YP16, 216/6 since YP8, 325/6
|
|
1024
1571
|
before; upstream standard tier; the 6 skips are upstream's own
|
|
1025
1572
|
`t.skip(!t.production)` extensive-tier gates: 3× y-array, 3× y-map random
|