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.
Files changed (143) hide show
  1. package/README.md +2 -2
  2. package/dist/cloudflare/DocumentRoom.d.ts +13 -9
  3. package/dist/cloudflare/DocumentRoom.js +10 -6
  4. package/dist/cloudflare/room/context.d.ts +27 -26
  5. package/dist/cloudflare/room/context.js +2 -2
  6. package/dist/cloudflare/room/purge.d.ts +6 -3
  7. package/dist/cloudflare/room/purge.js +44 -19
  8. package/dist/cloudflare/room/updates.d.ts +3 -4
  9. package/dist/cloudflare/room/updates.js +6 -8
  10. package/dist/crdt/attribution/attribution.d.ts +31 -12
  11. package/dist/crdt/attribution/attribution.js +56 -18
  12. package/dist/crdt/attribution/block.d.ts +3 -3
  13. package/dist/crdt/attribution/block.js +7 -8
  14. package/dist/crdt/attribution/index.d.ts +1 -1
  15. package/dist/crdt/attribution/index.js +1 -1
  16. package/dist/crdt/data.d.ts +62 -31
  17. package/dist/crdt/data.js +211 -103
  18. package/dist/crdt/doc/delete.d.ts +1 -1
  19. package/dist/crdt/doc/delete.js +13 -30
  20. package/dist/crdt/doc/funnel.js +29 -24
  21. package/dist/crdt/doc/gate.d.ts +3 -3
  22. package/dist/crdt/doc/history.d.ts +25 -9
  23. package/dist/crdt/doc/history.js +150 -40
  24. package/dist/crdt/doc/layout.js +11 -22
  25. package/dist/crdt/doc/meta.js +12 -17
  26. package/dist/crdt/doc/moves.d.ts +1 -1
  27. package/dist/crdt/doc/moves.js +52 -32
  28. package/dist/crdt/doc/reads.js +2 -2
  29. package/dist/crdt/doc/seed.js +2 -2
  30. package/dist/crdt/doc/split-merge.js +12 -15
  31. package/dist/crdt/doc/steps.d.ts +25 -13
  32. package/dist/crdt/doc/steps.js +52 -122
  33. package/dist/crdt/doc/types.d.ts +21 -9
  34. package/dist/crdt/document.d.ts +16 -5
  35. package/dist/crdt/document.js +21 -4
  36. package/dist/crdt/edytor-doc.d.ts +62 -64
  37. package/dist/crdt/edytor-doc.js +18 -13
  38. package/dist/crdt/engine-api.d.ts +33 -1
  39. package/dist/crdt/engine.d.ts +0 -1
  40. package/dist/crdt/engine.js +1 -4
  41. package/dist/crdt/flow.d.ts +1 -1
  42. package/dist/crdt/flow.js +25 -27
  43. package/dist/crdt/index.d.ts +26 -25
  44. package/dist/crdt/index.js +1 -1
  45. package/dist/crdt/placement/display.d.ts +5 -30
  46. package/dist/crdt/placement/display.js +39 -144
  47. package/dist/crdt/placement/keys.d.ts +58 -0
  48. package/dist/crdt/placement/keys.js +90 -0
  49. package/dist/crdt/placement/model.d.ts +119 -118
  50. package/dist/crdt/placement/model.js +242 -142
  51. package/dist/crdt/placement/resolve.d.ts +123 -35
  52. package/dist/crdt/placement/resolve.js +187 -153
  53. package/dist/crdt/placement/slots.d.ts +134 -0
  54. package/dist/crdt/placement/slots.js +152 -0
  55. package/dist/crdt/providers/room.d.ts +16 -2
  56. package/dist/crdt/providers/room.js +64 -17
  57. package/dist/crdt/providers/websocket.js +5 -3
  58. package/dist/crdt/purge.d.ts +17 -8
  59. package/dist/crdt/purge.js +236 -126
  60. package/dist/crdt/restore.d.ts +2 -2
  61. package/dist/crdt/restore.js +18 -34
  62. package/dist/crdt/schema.d.ts +22 -21
  63. package/dist/crdt/schema.js +29 -23
  64. package/dist/crdt/storage.d.ts +1 -1
  65. package/dist/crdt/storage.js +1 -1
  66. package/dist/crdt/structs.d.ts +15 -15
  67. package/dist/crdt/structs.js +8 -22
  68. package/dist/crdt/text/index/cache.js +4 -0
  69. package/dist/crdt/text/index/claims.js +12 -0
  70. package/dist/crdt/text/index/fold.d.ts +2 -3
  71. package/dist/crdt/text/index/fold.js +194 -44
  72. package/dist/crdt/text/index/layout.js +5 -8
  73. package/dist/crdt/text/index/placement.d.ts +26 -18
  74. package/dist/crdt/text/index/placement.js +404 -138
  75. package/dist/crdt/text/index/records.d.ts +3 -3
  76. package/dist/crdt/text/index/records.js +24 -31
  77. package/dist/crdt/text/index/shared.d.ts +11 -3
  78. package/dist/crdt/text/index/shared.js +12 -3
  79. package/dist/crdt/text/index/state.d.ts +21 -17
  80. package/dist/crdt/text/index/state.js +15 -17
  81. package/dist/crdt/text/index/streams.d.ts +10 -5
  82. package/dist/crdt/text/index/streams.js +78 -27
  83. package/dist/crdt/text/items.d.ts +10 -3
  84. package/dist/crdt/text/items.js +7 -0
  85. package/dist/crdt/text/model.d.ts +77 -62
  86. package/dist/crdt/text/model.js +84 -169
  87. package/dist/crdt/text/ranges.d.ts +1 -1
  88. package/dist/crdt/text/ranges.js +3 -3
  89. package/dist/crdt/text/rows.d.ts +13 -7
  90. package/dist/crdt/text/rows.js +24 -7
  91. package/dist/crdt/text/runs.d.ts +17 -12
  92. package/dist/crdt/text/runs.js +24 -7
  93. package/dist/crdt/text/streams.d.ts +26 -13
  94. package/dist/crdt/text/streams.js +41 -14
  95. package/dist/crdt/vendor/yjs/UPSTREAM.md +568 -21
  96. package/dist/crdt/vendor/yjs/dts/global.d.ts +2 -0
  97. package/dist/crdt/vendor/yjs/dts/index.d.ts +1 -1
  98. package/dist/crdt/vendor/yjs/dts/structs/AbstractStruct.d.ts +7 -0
  99. package/dist/crdt/vendor/yjs/dts/structs/Item.d.ts +194 -10
  100. package/dist/crdt/vendor/yjs/dts/utils/Doc.d.ts +6 -3
  101. package/dist/crdt/vendor/yjs/dts/utils/StructStore.d.ts +5 -18
  102. package/dist/crdt/vendor/yjs/dts/utils/Transaction.d.ts +15 -1
  103. package/dist/crdt/vendor/yjs/dts/utils/UpdateDecoder.d.ts +14 -6
  104. package/dist/crdt/vendor/yjs/dts/utils/UpdateEncoder.d.ts +33 -6
  105. package/dist/crdt/vendor/yjs/dts/utils/ids.d.ts +1 -65
  106. package/dist/crdt/vendor/yjs/dts/utils/marks.d.ts +6 -1
  107. package/dist/crdt/vendor/yjs/dts/ynode.d.ts +43 -7
  108. package/dist/crdt/vendor/yjs/global.d.ts +2 -0
  109. package/dist/crdt/vendor/yjs/src/index.js +1 -1
  110. package/dist/crdt/vendor/yjs/src/structs/AbstractStruct.js +10 -0
  111. package/dist/crdt/vendor/yjs/src/structs/Item.js +610 -51
  112. package/dist/crdt/vendor/yjs/src/utils/BlockSet.js +18 -9
  113. package/dist/crdt/vendor/yjs/src/utils/Doc.js +5 -3
  114. package/dist/crdt/vendor/yjs/src/utils/StructStore.js +5 -0
  115. package/dist/crdt/vendor/yjs/src/utils/Transaction.js +36 -13
  116. package/dist/crdt/vendor/yjs/src/utils/UpdateDecoder.js +88 -18
  117. package/dist/crdt/vendor/yjs/src/utils/UpdateEncoder.js +119 -32
  118. package/dist/crdt/vendor/yjs/src/utils/encoding.js +13 -1
  119. package/dist/crdt/vendor/yjs/src/utils/ids.js +7 -7
  120. package/dist/crdt/vendor/yjs/src/utils/marks.js +193 -8
  121. package/dist/crdt/vendor/yjs/src/utils/redo.js +1 -1
  122. package/dist/crdt/vendor/yjs/src/utils/transaction-helpers.js +1 -1
  123. package/dist/crdt/vendor/yjs/src/utils/updates.js +3 -3
  124. package/dist/crdt/vendor/yjs/src/ynode.js +177 -12
  125. package/dist/edytor.svelte.js +4 -1
  126. package/dist/events/beforeInputCommands.js +2 -2
  127. package/dist/events/onPageHide.d.ts +3 -0
  128. package/dist/events/onPageHide.js +16 -0
  129. package/dist/export/forms.d.ts +25 -25
  130. package/dist/surface/observer.svelte.js +2 -2
  131. package/dist/surface/projector.svelte.d.ts +15 -3
  132. package/dist/surface/projector.svelte.js +59 -8
  133. package/package.json +2 -2
  134. package/dist/crdt/placement/rank.d.ts +0 -87
  135. package/dist/crdt/placement/rank.js +0 -332
  136. package/dist/crdt/placement/source.d.ts +0 -43
  137. package/dist/crdt/placement/source.js +0 -66
  138. package/dist/crdt/text/delete-index.d.ts +0 -129
  139. package/dist/crdt/text/delete-index.js +0 -365
  140. package/dist/crdt/text/deletes.d.ts +0 -76
  141. package/dist/crdt/text/deletes.js +0 -362
  142. package/dist/crdt/text/index/anchored.d.ts +0 -17
  143. 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) and YP19 (only what edytor calls)
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 `bench/lib/interop.mjs` (the fork and the newer engine syncing over the
59
- wire) and the baseline leg of `src/tests/crdt/hardening/search-marker-format.test.ts`
60
- (byte-identical stores after every operation) run against it. A red run
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 those two checks, 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"); the engine needs two hooks for it, and nothing
557
- else changes.
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 reads the fork's bytes with its `…V2`
836
- functions (`bench/lib/interop.mjs`, the baseline legs of
837
- `search-marker-format.test.ts` and `yp16-writes.test.ts` compare against
838
- upstream's `encodeStateAsUpdateV2`). Vendored execution lines 5,123 →
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: `src/tests/crdt/harness/undo.js` is the engine's `UndoManager`
873
- as it stood before YP18 (`undoManagerOf(Y)`, over the fork or a copy of its
874
- tree), for the upstream suite, the replica harness's `raw` adapter, the
875
- engine probes and the differentials. Removed with their subject:
876
- `yp11-undo-hooks.test.ts`, `yp12-undo-withdraw.test.ts` (the hooks' rows;
877
- the rules are the facade history's `hist.undo.*` rows) and the upstream row
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` derives
958
- `changedParentTypes` from `changed`. Removed with their subject: the
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