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
@@ -1,38 +1,126 @@
1
1
  /**
2
- * Placement resolution: each block's candidates parsed from its `at` map,
3
- * and the winning placement of every block (acyclic acceptance in global
4
- * candidate order, a deterministic root fallback). Pure over replicated
5
- * state.
2
+ * Placement reads over the sequences (fork patch YP22): which block a home
3
+ * slot belongs to, each block's display edges (the sequences that can show
4
+ * its slot: its live move records', strongest first, then its parent's), the
5
+ * places of every sequence, and the cycle walk. Which edge a block takes is
6
+ * the index's acceptance (`text/index/placement.ts` `settle`). Pure over
7
+ * replicated state.
6
8
  */
7
- import type { EngineNode } from '../engine-api.js';
9
+ import type { EngineDoc, EngineNode } from '../engine-api.js';
10
+ import { isRecord, type Entry, type SeqItem, type Stamp, type Unit } from './slots.js';
8
11
  import { type Owner } from '../text/model.js';
9
- import type { BlockId, BlockRec, PlacementCand, ResolvedPlacement } from './model.js';
10
- /** Parse the `at` map of a block node into sorted candidates. */
11
- export declare const candidatesOf: (node: EngineNode) => PlacementCand[];
12
- /**
13
- * Resolve every block's winning placement, in global candidate order
14
- * `(seq, clientId, blockId)` descending. A candidate is accepted iff its
15
- * DISPLAY edge cannot reach back to the block through already-accepted
16
- * display edges; rejected candidates fall through to the block's next
17
- * candidate, then to a deterministic root fallback. Pure — reads only
18
- * replicated state, writes nothing.
19
- *
20
- * The relation kept acyclic is the COMPOSED display-parent relation
21
- * `b ↦ owner(parent(b))` (see `displayParentOf`): a placement parent that
22
- * was merged away resolves to its claim owner, so the raw `pl.parent`
23
- * graph being acyclic is NOT sufficient — a merge claim can redirect a
24
- * display edge back into the block's own subtree (e.g. `A` merged into
25
- * `B` while `B` sits under `A`, or `del` resurrection re-arming a claim).
26
- * Because the claim-owner map is computed independently of placements
27
- * (claims live on `claims` lists), the acceptance test composes the
28
- * two: the candidate's tentative display edge is `ownerOf(p)` — a live
29
- * self-owned block or `null` (root). A deleted parent is no sink: its
30
- * unmarked children display in its slot (read-time promotion,
31
- * {@link displaySlotOf}), so the walk passes through it along its own
32
- * accepted placement.
33
- *
34
- * `ownerOf` is injectable so callers can share an ownership context they
35
- * already computed; standalone callers get the map derived from the
36
- * block records (pure over replicated state — claims are `claims` items).
37
- */
38
- export declare const resolvePlacements: (blocks: Map<BlockId, BlockRec>, ownerOf?: (b: BlockId) => Owner) => Map<BlockId, ResolvedPlacement>;
12
+ import type { BlockId } from './model.js';
13
+ /** A block's home slot: the owner of the sequence holding it (`null`: the root) and its unit. */
14
+ export type SlotRef = Unit & {
15
+ parent: BlockId | null;
16
+ };
17
+ /** The stamp of a block with no slot: below every edge (it shows at the root's tail). */
18
+ export declare const NO_SLOT: Stamp;
19
+ /**
20
+ * A block's display edge: the owner of the sequence that shows its slot
21
+ * (`null`: the root; `undefined`: a node that is no block of the document),
22
+ * the record it shows through (`null`: at home), the edge's stamp, and the
23
+ * item of its displaying unit (the slot's, or the record).
24
+ */
25
+ export type Edge = {
26
+ parent: BlockId | null | undefined;
27
+ via: SeqItem | null;
28
+ stamp: Stamp;
29
+ item: SeqItem;
30
+ };
31
+ /**
32
+ * The block a home slot `{ b: id }` written by `client` belongs to: `id`
33
+ * when its registry value is `client`'s, else the losing incarnation
34
+ * `client` wrote (`id.same.concurrent`: each creation wrote its own slot),
35
+ * else `id` (a lone slot).
36
+ */
37
+ export declare const slotOwner: (registry: EngineNode, kept: (item: unknown) => boolean, id: BlockId, client: number) => BlockId;
38
+ /**
39
+ * The block whose sequence `node` is (`null`: the registry root), or
40
+ * `undefined` for a node that is no block of the document (an atom, a data
41
+ * root, a seed's losing incarnation).
42
+ */
43
+ export declare const ownerOfNode: (registry: EngineNode, kept: (item: unknown) => boolean, node: EngineNode) => BlockId | null | undefined;
44
+ /**
45
+ * The owner of the sequence block `home`'s slot shows in as the engine
46
+ * stands — its mover's, else its home's (`undefined`: gone, not arrived, or
47
+ * a node that is no block): the strongest edge's parent without building the
48
+ * edges (`candidatesOf`).
49
+ */
50
+ export declare const edgeParentOf: (doc: EngineDoc, registry: EngineNode, kept: (item: unknown) => boolean, home: SlotRef) => BlockId | null | undefined;
51
+ /**
52
+ * The display edges block `home`'s slot can take, the strongest first: one per
53
+ * live move record naming it, by descending `(priority, client, clock)` (the
54
+ * engine keeps a unit's movers in that order; the first is its mover), then
55
+ * its home slot. A block shows at the
56
+ * first one that closes no cycle (the later move wins); when that is not the
57
+ * first, it is back where it stood before the move a cycle dropped: the
58
+ * record before it, else home. `[]` when the slot is gone or has not arrived.
59
+ * `at`: the slot's item when the caller read it already (a rebuild's
60
+ * collection: no store lookup).
61
+ */
62
+ export declare const candidatesOf: (doc: EngineDoc, registry: EngineNode, kept: (item: unknown) => boolean, home: SlotRef, at?: SeqItem) => Edge[];
63
+ /**
64
+ * The block slots that can show in one sequence, in its order, as the index
65
+ * reads it: a slot item's units (moved away or not: a block a cycle sends back
66
+ * home shows there), a live record's units in the order it names them (a
67
+ * block a cycle sends back to an older record shows at it). Which block shows
68
+ * at a position is the index's acceptance; these are the places.
69
+ */
70
+ export declare const positionsOfItem: (doc: EngineDoc, it: SeqItem) => Entry[];
71
+ /**
72
+ * The places before unit `home` shown at `edge`, nearest first: the units its
73
+ * record names before it (or its slot item's earlier units), then each item to
74
+ * the left, last place first. The index's order keys walk them to the nearest
75
+ * block that shows where it was found.
76
+ */
77
+ export declare function slotsBefore(doc: EngineDoc, edge: Edge, home: Unit): Generator<Entry>;
78
+ /** Where a cycle check walks: the display parent of `at` through the accepted edges. */
79
+ type Accepted = ReadonlyMap<BlockId, {
80
+ parent: BlockId | null;
81
+ }>;
82
+ /**
83
+ * The blocks on the cycle a display edge `block → parent` would close
84
+ * through `accepted` edges and the claim owners (`ownerOf`: a live parent's
85
+ * edge is its owner's, a deleted one is walked itself), or `null` when it
86
+ * closes none. The members are `block` and every block whose edge the walk
87
+ * followed.
88
+ */
89
+ export declare const cycleThrough: (accepted: Accepted, ownerOf: (b: BlockId) => Owner, block: BlockId, parent: BlockId | null) => BlockId[] | null;
90
+ /** The order of the blocks at the root's tail (rejected or slotless): by their edge's stamp, then id. */
91
+ export declare const byTail: (a: {
92
+ id: BlockId;
93
+ stamp: Stamp;
94
+ }, b: {
95
+ id: BlockId;
96
+ stamp: Stamp;
97
+ }) => number;
98
+ /**
99
+ * The home slot of block `id` among the slots naming its registry id
100
+ * (`slots`: every `{ b }` slot by `b`): the one its writer wrote
101
+ * (`slotOwner`) — the lowest `(client, clock)` when several do (a redo's
102
+ * copy beside the original) — or `null`.
103
+ */
104
+ export declare const homeOf: (registry: EngineNode, kept: (item: unknown) => boolean, slots: ReadonlyMap<BlockId, readonly SlotRef[]>, id: BlockId) => SlotRef | null;
105
+ /** The block slots of every sequence: by the id they name (with each one's item), and each sequence's places in order. */
106
+ export type Slots = {
107
+ slots: Map<BlockId, SlotRef[]>;
108
+ items: Map<SlotRef, SeqItem>;
109
+ places: Map<BlockId | null, Entry[]>;
110
+ };
111
+ /** Every registry node and its kept incarnations, with the block id each stands for (the sequences' owners). */
112
+ export declare const registryOwners: (registry: EngineNode, kept: (item: unknown) => boolean) => {
113
+ id: BlockId;
114
+ node: EngineNode;
115
+ }[];
116
+ /**
117
+ * Every block slot of every sequence (the root's, each block's), each item
118
+ * read once: the slots by the id they name (a slot item's units, moved away
119
+ * or not, none gone), and each sequence's places in order
120
+ * (`positionsOfItem`).
121
+ */
122
+ export declare const collectSlots: (doc: EngineDoc, owners: Iterable<{
123
+ id: BlockId;
124
+ node: EngineNode;
125
+ }>) => Slots;
126
+ export { isRecord, type Stamp };
@@ -1,169 +1,203 @@
1
- import { decodeRank, encodeRank, RANK_VMIN } from './rank.js';
2
- import { AT, isNodeLike } from '../schema.js';
3
- import { computeOwners, DEAD } from '../text/model.js';
1
+ import { isNodeLike, REGISTRY_KEY } from '../schema.js';
2
+ import { baseIdOf, incarnationId, incarnationsOf, LIVE_WRITERS } from '../incarnations.js';
3
+ import { attrItems, firstItem, itemOf } from '../structs.js';
4
+ import { cmpStamp, isBlockSlot, isRecord, itemAt, sameUnit, stampOf, unitsOf } from './slots.js';
5
+ import { DEAD } from '../text/model.js';
6
+ /** The stamp of a home slot's edge: below every record's. */
7
+ const HOME = 0;
8
+ /** The stamp of a block with no slot: below every edge (it shows at the root's tail). */
9
+ export const NO_SLOT = [-1, 0, 0];
4
10
  /**
5
- * Tiebreak for rehome ranks — the placement fallback is PURE replicated
6
- * state (identical on every replica), so the minted rank must not depend
7
- * on the local `doc.clientID` or `Math.random`: a fixed value keeps the
8
- * resolution deterministic everywhere. Ordering between rehomed blocks is
9
- * carried by the `v` digit (each successive rehome mints below the last).
11
+ * The block a home slot `{ b: id }` written by `client` belongs to: `id`
12
+ * when its registry value is `client`'s, else the losing incarnation
13
+ * `client` wrote (`id.same.concurrent`: each creation wrote its own slot),
14
+ * else `id` (a lone slot).
10
15
  */
11
- const REHOME_TIE = 0;
16
+ export const slotOwner = (registry, kept, id, client) => {
17
+ const current = attrItems(registry).get(id);
18
+ if (current === undefined || current.id.client === client)
19
+ return id;
20
+ for (let it = current.left; it !== null && it.parentSub === id; it = it.left)
21
+ if (it.id.client === client && it.id.client >= LIVE_WRITERS && kept(it))
22
+ return incarnationId(id, it);
23
+ return id;
24
+ };
12
25
  /**
13
- * Deterministic rank strictly below `min` — the candidate-less rehome
14
- * fallback. The old floor sentinel `encodeRank([{v: RANK_VMIN,
15
- * t: 0}])` sorted first but was un-insertable-above: `rankBetween(
16
- * undefined, that)` hits the `rSeg.v <= RANK_VMIN` guard and throws
17
- * `RankSpaceExhausted` — reachable with ZERO boundary inserts (corpus
18
- * seed 96). Minting `min[0].v - 1` keeps the documented "orphans rehome
19
- * to the FRONT of the root" behavior while leaving digit headroom for
20
- * legal inserts above the rehomed block.
21
- *
22
- * `min` = the current minimum rank among placements displaying at the
23
- * root, `undefined` when the root list is empty (mint `{v:0}` — the same
24
- * canonical first key `rankBetween(undefined, undefined)` emits). At the
25
- * absolute floor (`min[0].v` already `RANK_VMIN` — unreachable through
26
- * any legal write; nothing sorts below it) join `min`'s tie instead of
27
- * minting an unencodable digit.
26
+ * The block whose sequence `node` is (`null`: the registry root), or
27
+ * `undefined` for a node that is no block of the document (an atom, a data
28
+ * root, a seed's losing incarnation).
28
29
  */
29
- const rehomeRankBelow = (min) => {
30
- if (min === undefined)
31
- return encodeRank([{ v: 0, t: REHOME_TIE }]);
32
- const v = decodeRank(min)[0].v;
33
- if (v <= RANK_VMIN)
34
- return min;
35
- return encodeRank([{ v: v - 1, t: REHOME_TIE }]);
30
+ export const ownerOfNode = (registry, kept, node) => {
31
+ if (node === registry)
32
+ return null;
33
+ const it = itemOf(node);
34
+ if (it === null || it.parent !== registry || typeof it.parentSub !== 'string')
35
+ return undefined;
36
+ if (!it.deleted)
37
+ return it.parentSub;
38
+ if (!kept(it) || it.id.client < LIVE_WRITERS)
39
+ return undefined;
40
+ return incarnationId(it.parentSub, it);
36
41
  };
37
- /** Total order on candidate stamps — matches the ADR's conflict order. */
38
- const cmpStamp = (aSeq, aClient, bSeq, bClient) => aSeq - bSeq || aClient - bClient;
39
- /** Parse the `at` map of a block node into sorted candidates. */
40
- export const candidatesOf = (node) => {
41
- const at = node.getAttr(AT);
42
- const cands = [];
43
- if (isNodeLike(at)) {
44
- at.forEachAttr((v, key) => {
45
- const dot = key.indexOf('.');
46
- const val = v;
47
- if (dot <= 0 || val == null || typeof val !== 'object' || typeof val.r !== 'string')
48
- return;
49
- cands.push({
50
- key,
51
- seq: Number(key.slice(0, dot)),
52
- client: Number(key.slice(dot + 1)),
53
- p: val.p ?? null,
54
- r: val.r
55
- });
56
- });
57
- }
58
- cands.sort((a, b) => cmpStamp(b.seq, b.client, a.seq, a.client));
59
- return cands;
42
+ /**
43
+ * The owner of the sequence block `home`'s slot shows in as the engine
44
+ * stands — its mover's, else its home's (`undefined`: gone, not arrived, or
45
+ * a node that is no block): the strongest edge's parent without building the
46
+ * edges (`candidatesOf`).
47
+ */
48
+ export const edgeParentOf = (doc, registry, kept, home) => {
49
+ const item = itemAt(doc, home);
50
+ if (item === null || item.gone === true)
51
+ return undefined;
52
+ const r = item.mover ?? null;
53
+ return r === null || r.deleted
54
+ ? home.parent
55
+ : ownerOfNode(registry, kept, r.parent);
56
+ };
57
+ /**
58
+ * The display edges block `home`'s slot can take, the strongest first: one per
59
+ * live move record naming it, by descending `(priority, client, clock)` (the
60
+ * engine keeps a unit's movers in that order; the first is its mover), then
61
+ * its home slot. A block shows at the
62
+ * first one that closes no cycle (the later move wins); when that is not the
63
+ * first, it is back where it stood before the move a cycle dropped: the
64
+ * record before it, else home. `[]` when the slot is gone or has not arrived.
65
+ * `at`: the slot's item when the caller read it already (a rebuild's
66
+ * collection: no store lookup).
67
+ */
68
+ export const candidatesOf = (doc, registry, kept, home, at) => {
69
+ const item = at ?? itemAt(doc, home);
70
+ if (item === null || item.gone === true)
71
+ return [];
72
+ const out = item.movers
73
+ ?.filter((r) => !r.deleted)
74
+ .map((r) => ({
75
+ parent: ownerOfNode(registry, kept, r.parent),
76
+ via: r,
77
+ stamp: stampOf(r),
78
+ item: r
79
+ })) ?? [];
80
+ out.push({ parent: home.parent, via: null, stamp: [HOME, home.c, home.k], item });
81
+ return out;
60
82
  };
61
83
  /**
62
- * Resolve every block's winning placement, in global candidate order
63
- * `(seq, clientId, blockId)` descending. A candidate is accepted iff its
64
- * DISPLAY edge cannot reach back to the block through already-accepted
65
- * display edges; rejected candidates fall through to the block's next
66
- * candidate, then to a deterministic root fallback. Pure — reads only
67
- * replicated state, writes nothing.
68
- *
69
- * The relation kept acyclic is the COMPOSED display-parent relation
70
- * `b ↦ owner(parent(b))` (see `displayParentOf`): a placement parent that
71
- * was merged away resolves to its claim owner, so the raw `pl.parent`
72
- * graph being acyclic is NOT sufficient — a merge claim can redirect a
73
- * display edge back into the block's own subtree (e.g. `A` merged into
74
- * `B` while `B` sits under `A`, or `del` resurrection re-arming a claim).
75
- * Because the claim-owner map is computed independently of placements
76
- * (claims live on `claims` lists), the acceptance test composes the
77
- * two: the candidate's tentative display edge is `ownerOf(p)` — a live
78
- * self-owned block or `null` (root). A deleted parent is no sink: its
79
- * unmarked children display in its slot (read-time promotion,
80
- * {@link displaySlotOf}), so the walk passes through it along its own
81
- * accepted placement.
82
- *
83
- * `ownerOf` is injectable so callers can share an ownership context they
84
- * already computed; standalone callers get the map derived from the
85
- * block records (pure over replicated state — claims are `claims` items).
84
+ * The block slots that can show in one sequence, in its order, as the index
85
+ * reads it: a slot item's units (moved away or not: a block a cycle sends back
86
+ * home shows there), a live record's units in the order it names them (a
87
+ * block a cycle sends back to an older record shows at it). Which block shows
88
+ * at a position is the index's acceptance; these are the places.
86
89
  */
87
- export const resolvePlacements = (blocks, ownerOf) => {
88
- let owner = ownerOf;
89
- if (!owner) {
90
- const owners = computeOwners(blocks);
91
- owner = (b) => owners.get(b) ?? DEAD;
90
+ export const positionsOfItem = (doc, it) => unitsOf(doc, it, (_, value) => isBlockSlot(value));
91
+ /**
92
+ * The places before unit `home` shown at `edge`, nearest first: the units its
93
+ * record names before it (or its slot item's earlier units), then each item to
94
+ * the left, last place first. The index's order keys walk them to the nearest
95
+ * block that shows where it was found.
96
+ */
97
+ export function* slotsBefore(doc, edge, home) {
98
+ const own = positionsOfItem(doc, edge.item);
99
+ let at = own.findIndex((e) => sameUnit(e.unit, home));
100
+ if (at < 0)
101
+ at = edge.via === null ? 0 : own.length;
102
+ for (let j = at - 1; j >= 0; j--)
103
+ yield own[j];
104
+ for (let p = edge.item.left; p !== null; p = p.left) {
105
+ const list = positionsOfItem(doc, p);
106
+ for (let j = list.length - 1; j >= 0; j--)
107
+ yield list[j];
92
108
  }
93
- const ordered = [];
94
- for (const [id, rec] of blocks) {
95
- for (const c of rec.cands)
96
- ordered.push({ ...c, blockId: id });
109
+ }
110
+ /**
111
+ * The blocks on the cycle a display edge `block → parent` would close
112
+ * through `accepted` edges and the claim owners (`ownerOf`: a live parent's
113
+ * edge is its owner's, a deleted one is walked itself), or `null` when it
114
+ * closes none. The members are `block` and every block whose edge the walk
115
+ * followed.
116
+ */
117
+ export const cycleThrough = (accepted, ownerOf, block, parent) => {
118
+ const members = [block];
119
+ for (let cur = parent, steps = 0; cur !== null; steps++) {
120
+ const o = ownerOf(cur);
121
+ const at = o === DEAD ? cur : o;
122
+ if (at === block)
123
+ return members;
124
+ const next = accepted.get(at);
125
+ if (next === undefined || steps > accepted.size)
126
+ return null;
127
+ members.push(at);
128
+ cur = next.parent;
97
129
  }
98
- ordered.sort((a, b) => cmpStamp(b.seq, b.client, a.seq, a.client) || b.blockId.localeCompare(a.blockId));
99
- const accepted = new Map();
100
- for (const cand of ordered) {
101
- if (accepted.has(cand.blockId))
130
+ return null;
131
+ };
132
+ /** The order of the blocks at the root's tail (rejected or slotless): by their edge's stamp, then id. */
133
+ export const byTail = (a, b) => cmpStamp(a.stamp, b.stamp) || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
134
+ /**
135
+ * The home slot of block `id` among the slots naming its registry id
136
+ * (`slots`: every `{ b }` slot by `b`): the one its writer wrote
137
+ * (`slotOwner`) — the lowest `(client, clock)` when several do (a redo's
138
+ * copy beside the original) — or `null`.
139
+ */
140
+ export const homeOf = (registry, kept, slots, id) => {
141
+ const base = baseIdOf(id);
142
+ let best = null;
143
+ for (const ref of slots.get(base) ?? []) {
144
+ if (slotOwner(registry, kept, base, ref.c) !== id)
102
145
  continue;
103
- let p = cand.p;
104
- if (p !== null && !blocks.has(p))
105
- p = null; // parent never integrated → root
106
- if (p !== null) {
107
- // Reject the candidate iff the block already is a display
108
- // ancestor of `p` through accepted edges: a live parent's edge
109
- // is its owner's, a deleted one is walked itself (its children
110
- // take its slot). Accepted edges are acyclic by induction, so
111
- // the walk always terminates.
112
- let cyclic = false;
113
- for (let cur = p; cur !== null;) {
114
- const o = owner(cur);
115
- const at = o === DEAD ? cur : o;
116
- if (at === cand.blockId) {
117
- cyclic = true;
118
- break;
119
- }
120
- const cp = accepted.get(at);
121
- if (cp === undefined)
122
- break; // edge not yet accepted → cannot cycle
123
- cur = cp.parent;
124
- }
125
- if (cyclic)
126
- continue; // try this block's next candidate
127
- }
128
- accepted.set(cand.blockId, { parent: p, rank: cand.r });
146
+ if (best === null || ref.c < best.c || (ref.c === best.c && ref.k < best.k))
147
+ best = ref;
129
148
  }
130
- // Fallback: blocks whose candidates were all cycle-rejected are rehomed
131
- // at the root under their argmax rank; a block with NO surviving
132
- // candidate mints a deterministic rank strictly below the current root
133
- // minimum (rehomeRankBelow; see its comment). The mint
134
- // must stay deterministic across replicas: it depends only on the
135
- // accepted map + block ids, never on clientID/Math.random.
136
- const rehomed = [];
137
- for (const [id, rec] of blocks) {
138
- if (accepted.has(id))
149
+ return best;
150
+ };
151
+ /** Every registry node and its kept incarnations, with the block id each stands for (the sequences' owners). */
152
+ export const registryOwners = (registry, kept) => {
153
+ const owners = [];
154
+ // The registry's live entries, read from its items (one pass, no attr lookups).
155
+ for (const [id, item] of attrItems(registry)) {
156
+ if (item.deleted)
139
157
  continue;
140
- if (rec.cands.length > 0) {
141
- accepted.set(id, { parent: null, rank: rec.cands[0].r });
142
- }
143
- else {
144
- rehomed.push(rec);
145
- }
158
+ const node = item.content.type;
159
+ if (isNodeLike(node))
160
+ owners.push({ id, node });
161
+ owners.push(...incarnationsOf(registry, id, kept));
146
162
  }
147
- if (rehomed.length > 0) {
148
- // Minimum rank among placements that DISPLAY at the root (a `null`
149
- // parent edge is the only way a display edge is `null` — `owner()`
150
- // never returns null). Superset-of-visible is fine: minting below
151
- // even an invisible sibling still sorts the rehome first among the
152
- // visible list.
153
- let min;
154
- for (const pl of accepted.values()) {
155
- if (pl.parent === null && (min === undefined || pl.rank < min))
156
- min = pl.rank;
157
- }
158
- // Descending-id mint order → ascending-id display order, the same
159
- // (rank, id)-tie order the old shared MIN_RANK produced. Each mint
160
- // goes below the running minimum, so stacked rehomes always get
161
- // distinct deterministic ranks.
162
- rehomed.sort((a, b) => b.id.localeCompare(a.id));
163
- for (const rec of rehomed) {
164
- min = rehomeRankBelow(min);
165
- accepted.set(rec.id, { parent: null, rank: min });
163
+ return owners;
164
+ };
165
+ /**
166
+ * Every block slot of every sequence (the root's, each block's), each item
167
+ * read once: the slots by the id they name (a slot item's units, moved away
168
+ * or not, none gone), and each sequence's places in order
169
+ * (`positionsOfItem`).
170
+ */
171
+ export const collectSlots = (doc, owners) => {
172
+ const slots = new Map();
173
+ const items = new Map();
174
+ const places = new Map();
175
+ const read = (owner, node) => {
176
+ let list;
177
+ for (let it = firstItem(node); it !== null; it = it.right) {
178
+ const here = positionsOfItem(doc, it);
179
+ if (here.length === 0)
180
+ continue;
181
+ if (list === undefined)
182
+ places.set(owner, (list = []));
183
+ for (const e of here) {
184
+ list.push(e);
185
+ if (e.via !== null)
186
+ continue;
187
+ const ref = { parent: owner, c: e.unit.c, k: e.unit.k };
188
+ const b = e.value.b;
189
+ const named = slots.get(b);
190
+ if (named === undefined)
191
+ slots.set(b, [ref]);
192
+ else
193
+ named.push(ref);
194
+ items.set(ref, it);
195
+ }
166
196
  }
167
- }
168
- return accepted;
197
+ };
198
+ read(null, doc.get(REGISTRY_KEY));
199
+ for (const { id, node } of owners)
200
+ read(id, node);
201
+ return { slots, items, places };
169
202
  };
203
+ export { isRecord };