edytor 5.0.0 → 6.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 (75) hide show
  1. package/README.md +2 -2
  2. package/dist/cloudflare/room/context.d.ts +62 -74
  3. package/dist/crdt/doc/history.js +2 -3
  4. package/dist/crdt/doc/ops.d.ts +81 -0
  5. package/dist/crdt/doc/ops.js +1898 -0
  6. package/dist/crdt/edytor-doc.d.ts +156 -156
  7. package/dist/crdt/edytor-doc.js +14 -42
  8. package/dist/crdt/flow.d.ts +4 -46
  9. package/dist/crdt/flow.js +2 -262
  10. package/dist/crdt/index.d.ts +62 -74
  11. package/dist/crdt/operations.d.ts +1 -1
  12. package/dist/crdt/placement/resolve.d.ts +1 -1
  13. package/dist/crdt/protocols/awareness.js +46 -81
  14. package/dist/crdt/providers/index.d.ts +10 -22
  15. package/dist/crdt/providers/indexeddb.d.ts +6 -10
  16. package/dist/crdt/providers/indexeddb.js +10 -16
  17. package/dist/crdt/providers/room.d.ts +11 -0
  18. package/dist/crdt/providers/room.js +26 -12
  19. package/dist/crdt/providers/websocket.d.ts +6 -12
  20. package/dist/crdt/providers/websocket.js +8 -15
  21. package/dist/crdt/rangeDelete.d.ts +2 -47
  22. package/dist/crdt/rangeDelete.js +1 -169
  23. package/dist/crdt/text/index/build.d.ts +125 -0
  24. package/dist/crdt/text/index/build.js +2895 -0
  25. package/dist/crdt/text/index/shared.d.ts +2 -6
  26. package/dist/crdt/text/index/shared.js +2 -5
  27. package/dist/crdt/text/model.js +2 -3
  28. package/dist/crdt/text/runs.d.ts +9 -12
  29. package/dist/crdt/text/runs.js +2 -165
  30. package/dist/events/domTextOffset.d.ts +2 -0
  31. package/dist/events/domTextOffset.js +8 -0
  32. package/dist/export/forms.d.ts +50 -50
  33. package/dist/plugins/richtext/RichTextPlugin.svelte +63 -61
  34. package/dist/plugins/richtext/richTextOperations.js +26 -32
  35. package/dist/selection/selection.utils.js +5 -8
  36. package/dist/session/handles.js +3 -7
  37. package/dist/surface/observer.svelte.js +15 -30
  38. package/dist/surface/projector.svelte.js +2 -2
  39. package/dist/utils.d.ts +5 -0
  40. package/dist/utils.js +7 -0
  41. package/package.json +1 -1
  42. package/dist/crdt/doc/capability.d.ts +0 -56
  43. package/dist/crdt/doc/capability.js +0 -207
  44. package/dist/crdt/doc/content.d.ts +0 -16
  45. package/dist/crdt/doc/content.js +0 -77
  46. package/dist/crdt/doc/delete.d.ts +0 -15
  47. package/dist/crdt/doc/delete.js +0 -84
  48. package/dist/crdt/doc/layout.d.ts +0 -9
  49. package/dist/crdt/doc/layout.js +0 -178
  50. package/dist/crdt/doc/meta.d.ts +0 -19
  51. package/dist/crdt/doc/meta.js +0 -198
  52. package/dist/crdt/doc/moves.d.ts +0 -21
  53. package/dist/crdt/doc/moves.js +0 -274
  54. package/dist/crdt/doc/split-merge.d.ts +0 -10
  55. package/dist/crdt/doc/split-merge.js +0 -179
  56. package/dist/crdt/doc/steps.d.ts +0 -65
  57. package/dist/crdt/doc/steps.js +0 -311
  58. package/dist/crdt/text/index/cache.d.ts +0 -16
  59. package/dist/crdt/text/index/cache.js +0 -175
  60. package/dist/crdt/text/index/claims.d.ts +0 -18
  61. package/dist/crdt/text/index/claims.js +0 -203
  62. package/dist/crdt/text/index/fold.d.ts +0 -21
  63. package/dist/crdt/text/index/fold.js +0 -656
  64. package/dist/crdt/text/index/layout.d.ts +0 -22
  65. package/dist/crdt/text/index/layout.js +0 -258
  66. package/dist/crdt/text/index/placement.d.ts +0 -47
  67. package/dist/crdt/text/index/placement.js +0 -738
  68. package/dist/crdt/text/index/records.d.ts +0 -13
  69. package/dist/crdt/text/index/records.js +0 -139
  70. package/dist/crdt/text/index/report.d.ts +0 -18
  71. package/dist/crdt/text/index/report.js +0 -337
  72. package/dist/crdt/text/index/state.d.ts +0 -197
  73. package/dist/crdt/text/index/state.js +0 -116
  74. package/dist/crdt/text/index/streams.d.ts +0 -25
  75. package/dist/crdt/text/index/streams.js +0 -248
@@ -1,13 +0,0 @@
1
- import type { BlockId } from '../../placement/model.js';
2
- import type { IndexClaims } from './claims.js';
3
- import type { IndexPlacement } from './placement.js';
4
- import type { IndexState } from './state.js';
5
- import type { IndexStreams } from './streams.js';
6
- /** The block records of one doc's index. */
7
- export declare const indexRecords: (ix: IndexState & IndexClaims & IndexStreams & IndexPlacement) => {
8
- kept: (item: unknown) => boolean;
9
- updateBlockRec: (id: BlockId) => void;
10
- ensureRec: (id: BlockId) => void;
11
- syncIncarnations: (id: BlockId) => string[];
12
- };
13
- export type IndexRecords = ReturnType<typeof indexRecords>;
@@ -1,139 +0,0 @@
1
- /**
2
- * Block records: each registry entry's record (and the losing
3
- * incarnations a key shows), rebuilt on a structural change, noting what
4
- * the maintained facts must re-decide.
5
- */
6
- import { readData } from '../../data.js';
7
- import { Y } from '../../engine.js';
8
- import { baseIdOf, incarnationNode, incarnationsOf, isIncarnationId } from '../../incarnations.js';
9
- import { engineOps } from '../../structs.js';
10
- import { CONTENT, NONCE, hasDeleteMark, hasWithdrawMark, isNodeLike } from '../../schema.js';
11
- import { sameShape, typeAttr } from './shared.js';
12
- /** The block records of one doc's index. */
13
- export const indexRecords = (ix) => {
14
- const { registry, blocks, shells, reclaim, rawOrder, placementSeeds, stateSeeds, incarnations, noteClaims, claimsOf, noteKind, homeFor, reslot } = ix;
15
- // ── records ──────────────────────────────────────────────────────
16
- /** Fork patch YP14's predicate: the registry values the engine keeps. */
17
- const kept = engineOps(Y).isKeptReplaced;
18
- /** The node of `id`: its registry value, or the losing incarnation a derived id names. */
19
- const nodeAt = (id) => {
20
- const v = registry.getAttr(id);
21
- if (isNodeLike(v) || !isIncarnationId(id))
22
- return v;
23
- return incarnationNode(registry, id, kept) ?? undefined;
24
- };
25
- const buildRec = (id, node) => {
26
- const content = node.getAttr(CONTENT);
27
- const derived = isIncarnationId(id);
28
- // A losing incarnation lives and dies with its key's block: hidden by its
29
- // own delete mark or while that block is deleted (a withdrawn one included).
30
- const keyDeleted = derived ? (blocks.get(baseIdOf(id))?.deleted ?? true) : false;
31
- // The implicit claims stamp below every written one: an explicit claim
32
- // (a split moving them to its tail block) outranks them.
33
- const implicit = derived
34
- ? []
35
- : incarnationsOf(registry, id, kept).map((x, i) => ({
36
- m: x.id,
37
- stamp: { c: -1, k: -1 - i }
38
- }));
39
- return {
40
- id,
41
- node,
42
- type: typeAttr(node),
43
- data: readData(node),
44
- n: node.getAttr(NONCE),
45
- deleted: hasDeleteMark(node) || keyDeleted,
46
- content: isNodeLike(content) ? content : undefined,
47
- implicit,
48
- // Read from its stream once the record is in (`claimsOf`), again after the fold's texts.
49
- claims: [],
50
- home: homeFor(id)
51
- };
52
- };
53
- /**
54
- * Rebuild (or create, or drop) block `id`'s record, and note what the
55
- * maintained facts must re-decide: its owner and the tops of the
56
- * blocks it claims, its placement — and, when its entry came or
57
- * went, the placements of the blocks whose display edge it holds.
58
- */
59
- const updateBlockRec = (id) => {
60
- const old = blocks.get(id);
61
- const node = nodeAt(id);
62
- if (!isNodeLike(node))
63
- blocks.delete(id);
64
- else {
65
- const rec = buildRec(id, node);
66
- blocks.set(id, rec);
67
- rec.claims = claimsOf(id);
68
- reclaim.add(id);
69
- }
70
- const rec = blocks.get(id);
71
- // Another node now stands for `id` (or none): its sequence's slots change owner.
72
- if (old?.node !== rec?.node) {
73
- if (old !== undefined)
74
- reslot(old.node);
75
- if (rec !== undefined)
76
- reslot(rec.node);
77
- }
78
- noteShell(id);
79
- noteClaims(id, old?.claims ?? [], rec?.claims ?? []);
80
- placementSeeds.add(id);
81
- if ((old === undefined) !== (rec === undefined)) {
82
- stateSeeds.add(id);
83
- for (const c of rawOrder.get(id) ?? [])
84
- placementSeeds.add(c.id);
85
- }
86
- if (old?.type !== rec?.type) {
87
- noteKind(id);
88
- if (old === undefined ||
89
- rec === undefined ||
90
- ix.roles === null ||
91
- !sameShape(ix.roles, old.type, rec.type))
92
- stateSeeds.add(id);
93
- }
94
- };
95
- /** A withdrawn block without a delete mark: its `deleted` is settled after each fold. */
96
- const noteShell = (id) => {
97
- const rec = blocks.get(id);
98
- const was = shells.has(id);
99
- // A losing incarnation is never withdrawn on its own: its key's block decides.
100
- if (isIncarnationId(id))
101
- shells.delete(id);
102
- else if (rec !== undefined && !rec.deleted && hasWithdrawMark(rec.node))
103
- shells.add(id);
104
- else
105
- shells.delete(id);
106
- // A withdrawn cell shows in a column its table no longer lists (`table.cell`): re-place it.
107
- if (was !== shells.has(id) && rec !== undefined && ix.cellKinds.has(rec.type))
108
- stateSeeds.add(id);
109
- };
110
- const ensureRec = (id) => {
111
- if (!blocks.has(id))
112
- updateBlockRec(id);
113
- };
114
- /**
115
- * Re-read the losing incarnations of key `id` after its record
116
- * changed: each one shown now or before gets its record rebuilt (its
117
- * liveness is the key's); returns them, for the fold to rescan.
118
- */
119
- const syncIncarnations = (id) => {
120
- if (isIncarnationId(id))
121
- return [];
122
- const before = incarnations.get(id) ?? [];
123
- const now = incarnationsOf(registry, id, kept).map((x) => x.id);
124
- if (now.length === 0)
125
- incarnations.delete(id);
126
- else
127
- incarnations.set(id, now);
128
- const all = [...new Set([...before, ...now])];
129
- for (const v of all)
130
- updateBlockRec(v);
131
- return all;
132
- };
133
- return {
134
- kept,
135
- updateBlockRec,
136
- ensureRec,
137
- syncIncarnations
138
- };
139
- };
@@ -1,18 +0,0 @@
1
- import type { EngineDoc } from '../../engine-api.js';
2
- import type { BlockId, ContentItem, ProjectedBlock } from '../../placement/model.js';
3
- import type { IndexReport } from '../runs.js';
4
- import type { IndexCache } from './cache.js';
5
- import type { IndexFold } from './fold.js';
6
- import type { IndexPlacement } from './placement.js';
7
- import type { IndexState, Published } from './state.js';
8
- /** The change report of one doc's index. */
9
- export declare const indexReporter: (ix: IndexState & IndexPlacement & IndexCache & IndexFold) => {
10
- itemsOf: (id: BlockId) => ContentItem[];
11
- projectBlock: (id: BlockId) => ProjectedBlock;
12
- reachable: (prev?: Published["nodes"]) => Published;
13
- publish: (origin: unknown, local: boolean) => void;
14
- onUpdate: (_u: Uint8Array, origin: unknown, _d: EngineDoc, tr: unknown) => void;
15
- onReport: (cb: (r: IndexReport, origin: unknown, local: boolean) => void) => (() => void);
16
- hasSubscribers: () => boolean;
17
- };
18
- export type IndexReporter = ReturnType<typeof indexReporter>;
@@ -1,337 +0,0 @@
1
- /**
2
- * The change report: the fold against the last published index, one per
3
- * commit that changed the visible document, and the projection it carries.
4
- */
5
- import { cloneJsonSafe, sameIds } from '../../../utils/json.js';
6
- import { readData } from '../../data.js';
7
- import { callEach } from '../../protocols/observable.js';
8
- import { EMPTY_IDS, checksOf, dataOf, indexChecks, keyOf } from './shared.js';
9
- /** The change report of one doc's index. */
10
- export const indexReporter = (ix) => {
11
- const { doc, dataRoot, blocks, following, dirtyLists, leftLists, candidates, ensurePlacements, typeOf, runs, syncAll } = ix;
12
- const docData = () => cloneJsonSafe(readData(dataRoot) ?? {});
13
- let dataChanged = false;
14
- // ── projection ───────────────────────────────────────────────────
15
- /** `id`'s visible content items: its runs, as fresh wrappers over their shared payloads. */
16
- const itemsOf = (id) => runs(id).map((r) => ({ ...r }));
17
- const projectBlock = (id) => {
18
- const rec = blocks.get(id);
19
- const projected = {
20
- id,
21
- type: typeOf(id),
22
- data: dataOf(rec),
23
- content: itemsOf(id),
24
- children: []
25
- };
26
- for (const k of ix.kidsMap.get(id) ?? [])
27
- projected.children.push(projectBlock(k.id));
28
- return projected;
29
- };
30
- // ── the change report: the fold against the last published index ─
31
- let published = null;
32
- const reportSubs = new Set();
33
- /** The reachable tree — nodes carry `prev`'s published payloads where they exist. */
34
- const reachable = (prev) => {
35
- ensurePlacements();
36
- const nodes = new Map();
37
- const order = new Map();
38
- const stack = [null];
39
- for (let parent = stack.pop(); parent !== undefined; parent = stack.pop()) {
40
- const ks = ix.kidsMap.get(parent) ?? [];
41
- if (ks.length === 0)
42
- continue;
43
- order.set(parent, Object.freeze(ks.map((k) => k.id)));
44
- for (let index = ks.length - 1; index >= 0; index--)
45
- stack.push(ks[index].id);
46
- ks.forEach(({ id }, index) => {
47
- const old = prev?.get(id);
48
- const rec = blocks.get(id);
49
- nodes.set(id, old === undefined
50
- ? { parent, index, type: typeOf(id), data: rec.data, runs: runs(id) }
51
- : { ...old, parent, index });
52
- });
53
- }
54
- return { nodes, order, kids: ix.kidsVersion };
55
- };
56
- /**
57
- * The report's structural part from the child lists patched since the
58
- * last report, advancing the published tree in place — what
59
- * `reachable` would give, without walking the lists nothing changed:
60
- * a published block no longer shown leaves (a root when its parent
61
- * stays), its still-shown descendants staying; each patched list's
62
- * new order is news, a block in it at a new parent or index moved, one
63
- * not published before is added with its subtree.
64
- */
65
- const advance = (pub, r, covered, reparented) => {
66
- const { nodes, order } = pub;
67
- const shown = (x) => {
68
- for (let c = x; c !== null;) {
69
- const slot = ix.slots.get(c);
70
- if (slot === undefined)
71
- return false;
72
- c = slot.parent;
73
- }
74
- return true;
75
- };
76
- const lists = [...dirtyLists];
77
- // Leaving: a published block of a patched list that no longer shows.
78
- const drop = (id) => {
79
- const ks = order.get(id);
80
- if (ks !== undefined) {
81
- order.delete(id);
82
- r.order.set(id, EMPTY_IDS);
83
- for (const k of ks)
84
- if (!shown(k))
85
- drop(k);
86
- }
87
- nodes.delete(id);
88
- };
89
- for (const id of leftLists) {
90
- const o = nodes.get(id);
91
- if (o === undefined || shown(id))
92
- continue;
93
- if (o.parent === null || shown(o.parent))
94
- r.removed.add(id);
95
- drop(id);
96
- }
97
- // Each patched list that shows: its order, its moved and new blocks.
98
- const fresh = new Map();
99
- for (const p of lists) {
100
- if (p !== null && !shown(p))
101
- continue;
102
- const ks = ix.kidsMap.get(p) ?? [];
103
- const prev = order.get(p);
104
- if (ks.length === 0) {
105
- if (prev !== undefined) {
106
- order.delete(p);
107
- r.order.set(p, EMPTY_IDS);
108
- }
109
- continue;
110
- }
111
- const ids = ks.map((k) => k.id);
112
- if (prev === undefined || !sameIds(prev, ids)) {
113
- const frozen = Object.freeze(ids);
114
- order.set(p, frozen);
115
- r.order.set(p, frozen);
116
- }
117
- ks.forEach(({ id }, index) => {
118
- const n = nodes.get(id);
119
- if (n === undefined)
120
- fresh.set(id, { parent: p, index });
121
- else if (n.parent !== p || n.index !== index) {
122
- r.moved.add(id);
123
- if (n.parent !== p)
124
- reparented.add(id);
125
- n.parent = p;
126
- n.index = index;
127
- }
128
- });
129
- }
130
- // New roots (no new ancestor) bring their subtree.
131
- const add = (b, parent, index) => {
132
- if (!nodes.has(b.id)) {
133
- covered.add(b.id);
134
- const rec = blocks.get(b.id);
135
- nodes.set(b.id, { parent, index, type: typeOf(b.id), data: rec.data, runs: runs(b.id) });
136
- }
137
- if (b.children.length > 0 && !order.has(b.id)) {
138
- const ids = Object.freeze(b.children.map((c) => c.id));
139
- order.set(b.id, ids);
140
- r.order.set(b.id, ids);
141
- }
142
- b.children.forEach((c, i) => add(c, b.id, i));
143
- };
144
- for (const [id, at] of fresh) {
145
- let under = false;
146
- for (let c = ix.slots.get(id)?.parent ?? null; c !== null && !under; c = ix.slots.get(c)?.parent ?? null)
147
- under = fresh.has(c);
148
- if (under || covered.has(id))
149
- continue;
150
- const b = projectBlock(id);
151
- r.added.set(id, b);
152
- add(b, at.parent, at.index);
153
- }
154
- };
155
- /** The blocks whose shown kind followed their slot at the last report. */
156
- let followingBefore = [];
157
- /** The document data's key at the last report. */
158
- let dataKey = '';
159
- /** Build the commit's report and advance the published index to it. */
160
- const report = () => {
161
- const before = published;
162
- // Fold every queued transaction first: a follow-up a cleanup
163
- // started (a delete-mark repair) has already written, and the
164
- // first content read below would fold it mid-report, after the
165
- // child lists were read (found by the purge fuzz, rich lane, seeds 651 and 1271).
166
- syncAll();
167
- ensurePlacements();
168
- const r = {
169
- added: new Map(),
170
- removed: new Set(),
171
- moved: new Set(),
172
- meta: new Map(),
173
- content: new Map(),
174
- order: new Map()
175
- };
176
- // The document's data is news when it differs from what was published.
177
- if (dataChanged) {
178
- dataChanged = false;
179
- const data = docData();
180
- const key = keyOf(data);
181
- if (key !== dataKey)
182
- [r.data, dataKey] = [data, key];
183
- }
184
- if (ix.kidsVersion === before.kids && candidates.size === 0)
185
- return r.data ? r : null;
186
- // Added subtrees carry their new descendants. A descendant that was
187
- // visible before is reported like any visible block (moved, retyped,
188
- // edited against its published baseline), so consumers keep it.
189
- const covered = new Set();
190
- /** Moved to another display parent: only those can show another kind (an index never does). */
191
- const reparented = new Set();
192
- const lists = ix.kidsVersion !== before.kids && !ix.allListsDirty;
193
- const after = ix.kidsVersion === before.kids || lists ? before : reachable(before.nodes);
194
- if (lists)
195
- advance(before, r, covered, reparented);
196
- else if (after !== before) {
197
- for (const [parent, ids] of after.order) {
198
- const prev = before.order.get(parent);
199
- if (prev === undefined || !sameIds(prev, ids))
200
- r.order.set(parent, ids);
201
- }
202
- for (const parent of before.order.keys()) {
203
- if (!after.order.has(parent))
204
- r.order.set(parent, EMPTY_IDS);
205
- }
206
- const register = (b) => {
207
- if (!before.nodes.has(b.id))
208
- covered.add(b.id);
209
- b.children.forEach(register);
210
- };
211
- for (const [id, n] of after.nodes) {
212
- if (!before.nodes.has(id)) {
213
- if (covered.has(id))
214
- continue;
215
- const b = projectBlock(id);
216
- r.added.set(id, b);
217
- register(b);
218
- }
219
- else {
220
- const o = before.nodes.get(id);
221
- if (o.parent !== n.parent || o.index !== n.index)
222
- r.moved.add(id);
223
- if (o.parent !== n.parent)
224
- reparented.add(id);
225
- }
226
- }
227
- // Removed subtree ROOTS: a removed id whose before-parent stays
228
- // visible (or is the root). One under a removed parent leaves with
229
- // it; one under a surviving child of a removed subtree does not.
230
- for (const [id, o] of before.nodes) {
231
- if (after.nodes.has(id))
232
- continue;
233
- if (o.parent === null || after.nodes.has(o.parent))
234
- r.removed.add(id);
235
- }
236
- }
237
- const meta = (id, n) => {
238
- const rec = blocks.get(id);
239
- const type = typeOf(id);
240
- r.meta.set(id, { type, data: dataOf(rec) });
241
- n.type = type;
242
- n.data = rec.data;
243
- };
244
- for (const id of candidates) {
245
- const n = after.nodes.get(id);
246
- if (n === undefined || covered.has(id))
247
- continue;
248
- const rec = blocks.get(id);
249
- if (typeOf(id) !== n.type || keyOf(rec.data) !== keyOf(n.data))
250
- meta(id, n);
251
- const next = runs(id);
252
- if (next !== n.runs) {
253
- const key = keyOf(next);
254
- if ((n.key ??= keyOf(n.runs)) !== key)
255
- r.content.set(id, next);
256
- n.runs = next;
257
- n.key = key;
258
- }
259
- }
260
- // A block that starts or stops displaying a kind other than its
261
- // stored one moved, or its kind follows (or followed) its slot:
262
- // its kind is news. Without a placement change, only a retype
263
- // changes a shown kind — the retyped block's (a candidate) and the
264
- // kinds derived from it: a promoted or stray line shows its display
265
- // parent's default child.
266
- if (after !== before || lists || ix.retyped) {
267
- const now = [...following];
268
- for (const id of [...reparented, ...now, ...followingBefore]) {
269
- const n = after.nodes.get(id);
270
- if (n !== undefined && !candidates.has(id) && !covered.has(id) && typeOf(id) !== n.type)
271
- meta(id, n);
272
- }
273
- followingBefore = now;
274
- }
275
- // A plain block directly in a list shows as its item (`itemOf`):
276
- // a block whose shown kind changed re-reads its children's (a worklist:
277
- // the map visits the entries added meanwhile).
278
- for (const id of r.meta.keys())
279
- for (const { id: kid } of ix.kidsMap.get(id) ?? []) {
280
- const n = after.nodes.get(kid);
281
- if (n !== undefined && !covered.has(kid) && !r.meta.has(kid) && typeOf(kid) !== n.type)
282
- meta(kid, n);
283
- }
284
- ix.retyped = false;
285
- candidates.clear();
286
- published = after;
287
- after.kids = ix.kidsVersion;
288
- dirtyLists.clear();
289
- leftLists.clear();
290
- ix.allListsDirty = false;
291
- if (indexChecks.on)
292
- checksOf(ix).checkPublished(published);
293
- const empty = r.added.size + r.removed.size + r.moved.size + r.meta.size + r.content.size + r.order.size;
294
- return empty === 0 && !r.data ? null : r;
295
- };
296
- /** Report the commit (or role change) to every subscriber, when it changed the visible document. */
297
- const publish = (origin, local) => {
298
- const r = report();
299
- if (r !== null)
300
- callEach('[edytor-doc] change', [...reportSubs], r, origin, local);
301
- };
302
- const onUpdate = (_u, origin, _d, tr) => {
303
- if (tr.changed?.has(dataRoot))
304
- dataChanged = true;
305
- publish(origin, tr.local === true);
306
- };
307
- /** Subscribe to the change report; the first subscription takes the baseline. */
308
- const onReport = (cb) => {
309
- if (reportSubs.size === 0) {
310
- syncAll();
311
- published = reachable();
312
- dataKey = keyOf(docData());
313
- candidates.clear();
314
- ix.reporting = true;
315
- doc.on('update', onUpdate);
316
- }
317
- reportSubs.add(cb);
318
- return () => {
319
- if (!reportSubs.delete(cb) || reportSubs.size > 0)
320
- return;
321
- doc.off('update', onUpdate);
322
- published = null;
323
- ix.reporting = false;
324
- candidates.clear();
325
- };
326
- };
327
- const hasSubscribers = () => reportSubs.size > 0;
328
- return {
329
- itemsOf,
330
- projectBlock,
331
- reachable,
332
- publish,
333
- onUpdate,
334
- onReport,
335
- hasSubscribers
336
- };
337
- };