noteloom 0.3.3 → 0.5.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 (104) hide show
  1. package/README.md +173 -808
  2. package/bin/noteloom.mjs +139 -0
  3. package/dist/canvas.cjs +2 -0
  4. package/dist/canvas.cjs.map +1 -0
  5. package/dist/canvas.d.ts +2 -0
  6. package/dist/canvas.js +5 -0
  7. package/dist/canvas.js.map +1 -0
  8. package/dist/collab.cjs +2 -0
  9. package/dist/collab.cjs.map +1 -0
  10. package/dist/collab.d.ts +19 -0
  11. package/dist/collab.js +379 -0
  12. package/dist/collab.js.map +1 -0
  13. package/dist/comments.cjs +2 -0
  14. package/dist/comments.cjs.map +1 -0
  15. package/dist/comments.d.ts +17 -0
  16. package/dist/comments.js +19 -0
  17. package/dist/comments.js.map +1 -0
  18. package/dist/index.cjs +2 -0
  19. package/dist/index.cjs.map +1 -0
  20. package/dist/index.d.ts +374 -34
  21. package/dist/index.js +3983 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/persistence.cjs +2 -0
  24. package/dist/persistence.cjs.map +1 -0
  25. package/dist/persistence.d.ts +11 -0
  26. package/dist/persistence.js +12 -0
  27. package/dist/persistence.js.map +1 -0
  28. package/dist/shared/CommentAvatar-TKqsM8vG.js +53 -0
  29. package/dist/shared/CommentAvatar-TKqsM8vG.js.map +1 -0
  30. package/dist/shared/CommentAvatar-V457IgoQ.cjs +2 -0
  31. package/dist/shared/CommentAvatar-V457IgoQ.cjs.map +1 -0
  32. package/dist/shared/CommentThreadCard-CwKd6fp3.js +213 -0
  33. package/dist/shared/CommentThreadCard-CwKd6fp3.js.map +1 -0
  34. package/dist/shared/CommentThreadCard-YziIa8My.cjs +2 -0
  35. package/dist/shared/CommentThreadCard-YziIa8My.cjs.map +1 -0
  36. package/dist/shared/CommentsPanel-DezejDdE.cjs +2 -0
  37. package/dist/shared/CommentsPanel-DezejDdE.cjs.map +1 -0
  38. package/dist/shared/CommentsPanel-dx7IaoqS.js +76 -0
  39. package/dist/shared/CommentsPanel-dx7IaoqS.js.map +1 -0
  40. package/dist/shared/VersionHistory-DhYb8C2e.js +1120 -0
  41. package/dist/shared/VersionHistory-DhYb8C2e.js.map +1 -0
  42. package/dist/shared/VersionHistory-DuSxis_7.cjs +12 -0
  43. package/dist/shared/VersionHistory-DuSxis_7.cjs.map +1 -0
  44. package/dist/shared/VoiceListeningIndicator-BXByLZwO.cjs +2 -0
  45. package/dist/shared/VoiceListeningIndicator-BXByLZwO.cjs.map +1 -0
  46. package/dist/shared/VoiceListeningIndicator-bv285FSY.js +338 -0
  47. package/dist/shared/VoiceListeningIndicator-bv285FSY.js.map +1 -0
  48. package/dist/shared/fieldRegistry-BoYyW3GH.cjs +2 -0
  49. package/dist/shared/fieldRegistry-BoYyW3GH.cjs.map +1 -0
  50. package/dist/shared/fieldRegistry-PjJWXFoB.js +223 -0
  51. package/dist/shared/fieldRegistry-PjJWXFoB.js.map +1 -0
  52. package/dist/shared/icons-BCseh2pt.cjs +3531 -0
  53. package/dist/shared/icons-BCseh2pt.cjs.map +1 -0
  54. package/dist/shared/icons-D5Ctow8R.js +4279 -0
  55. package/dist/shared/icons-D5Ctow8R.js.map +1 -0
  56. package/dist/shared/index-CbIBsFmI.cjs +2 -0
  57. package/dist/shared/index-CbIBsFmI.cjs.map +1 -0
  58. package/dist/shared/index-D_8hJpI7.js +2145 -0
  59. package/dist/shared/index-D_8hJpI7.js.map +1 -0
  60. package/dist/shared/indexedDbPersistence-BKlGPFTP.js +112 -0
  61. package/dist/shared/indexedDbPersistence-BKlGPFTP.js.map +1 -0
  62. package/dist/shared/indexedDbPersistence-BWJ1h5Vp.cjs +2 -0
  63. package/dist/shared/indexedDbPersistence-BWJ1h5Vp.cjs.map +1 -0
  64. package/dist/shared/leafBlockFactory-CaLWUcfa.cjs +2 -0
  65. package/dist/shared/leafBlockFactory-CaLWUcfa.cjs.map +1 -0
  66. package/dist/shared/leafBlockFactory-DSa1XCox.js +335 -0
  67. package/dist/shared/leafBlockFactory-DSa1XCox.js.map +1 -0
  68. package/dist/shared/serialize-BqKU9EZy.js +86 -0
  69. package/dist/shared/serialize-BqKU9EZy.js.map +1 -0
  70. package/dist/shared/serialize-CqwX55uK.cjs +3 -0
  71. package/dist/shared/serialize-CqwX55uK.cjs.map +1 -0
  72. package/dist/shared/starter-kit-DyX1U84W.cjs +27 -0
  73. package/dist/shared/starter-kit-DyX1U84W.cjs.map +1 -0
  74. package/dist/shared/starter-kit-mfeNxqFn.js +4980 -0
  75. package/dist/shared/starter-kit-mfeNxqFn.js.map +1 -0
  76. package/dist/shared/usePopoverEdgeClamp-CPRWQtZV.cjs +2 -0
  77. package/dist/shared/usePopoverEdgeClamp-CPRWQtZV.cjs.map +1 -0
  78. package/dist/shared/usePopoverEdgeClamp-D_hB5t60.js +109 -0
  79. package/dist/shared/usePopoverEdgeClamp-D_hB5t60.js.map +1 -0
  80. package/dist/shared/useServiceWorkerUpdate-BduabXv6.js +94 -0
  81. package/dist/shared/useServiceWorkerUpdate-BduabXv6.js.map +1 -0
  82. package/dist/shared/useServiceWorkerUpdate-DQboGesi.cjs +2 -0
  83. package/dist/shared/useServiceWorkerUpdate-DQboGesi.cjs.map +1 -0
  84. package/dist/starter-kit.cjs +2 -0
  85. package/dist/starter-kit.cjs.map +1 -0
  86. package/dist/starter-kit.d.ts +9 -0
  87. package/dist/starter-kit.js +10 -0
  88. package/dist/starter-kit.js.map +1 -0
  89. package/dist/style.css +3529 -3283
  90. package/dist/versions.cjs +2 -0
  91. package/dist/versions.cjs.map +1 -0
  92. package/dist/versions.d.ts +11 -0
  93. package/dist/versions.js +13 -0
  94. package/dist/versions.js.map +1 -0
  95. package/dist/voice.cjs +2 -0
  96. package/dist/voice.cjs.map +1 -0
  97. package/dist/voice.d.ts +7 -0
  98. package/dist/voice.js +8 -0
  99. package/dist/voice.js.map +1 -0
  100. package/package.json +159 -92
  101. package/dist/noteloom.cjs +0 -3292
  102. package/dist/noteloom.cjs.map +0 -1
  103. package/dist/noteloom.es.js +0 -15047
  104. package/dist/noteloom.es.js.map +0 -1
@@ -0,0 +1,223 @@
1
+ class n {
2
+ constructor(e) {
3
+ this.peerId = e, this.wallTime = 0, this.counter = 0;
4
+ }
5
+ /** Local event (a user's own edit). Returns the stamped timestamp. */
6
+ tick() {
7
+ const e = Date.now();
8
+ return e > this.wallTime ? (this.wallTime = e, this.counter = 0) : this.counter += 1, { wallTime: this.wallTime, counter: this.counter, peerId: this.peerId };
9
+ }
10
+ /**
11
+ * Merges in a timestamp observed from a remote peer, advancing this
12
+ * clock so subsequent local ticks are guaranteed to sort after
13
+ * anything just received.
14
+ */
15
+ receive(e) {
16
+ const t = Date.now(), s = Math.max(t, this.wallTime, e.wallTime);
17
+ return s === this.wallTime && s === e.wallTime ? this.counter = Math.max(this.counter, e.counter) + 1 : s === this.wallTime ? this.counter += 1 : s === e.wallTime ? this.counter = e.counter + 1 : this.counter = 0, this.wallTime = s, { wallTime: this.wallTime, counter: this.counter, peerId: this.peerId };
18
+ }
19
+ /** Total order: wallTime, then counter, then peerId. */
20
+ static compare(e, t) {
21
+ return e.wallTime !== t.wallTime ? e.wallTime - t.wallTime : e.counter !== t.counter ? e.counter - t.counter : e.peerId === t.peerId ? 0 : e.peerId < t.peerId ? -1 : 1;
22
+ }
23
+ }
24
+ function d() {
25
+ return typeof crypto < "u" && typeof crypto.randomUUID == "function" ? crypto.randomUUID() : `peer-${Date.now()}-${Math.random().toString(36).slice(2)}`;
26
+ }
27
+ class u {
28
+ constructor() {
29
+ this.slots = /* @__PURE__ */ new Map();
30
+ }
31
+ /** Builds CRDT state from an existing plain-array order (e.g. loading a v1 document with no CRDT history). */
32
+ static fromArray(e, { peerId: t = "legacy" } = {}) {
33
+ const s = new u();
34
+ let o = null;
35
+ for (const r of e)
36
+ s.slots.set(r, {
37
+ id: r,
38
+ originId: o,
39
+ peerId: t,
40
+ clock: { wallTime: 0, counter: 0, peerId: t },
41
+ deleted: !1
42
+ }), o = r;
43
+ return s;
44
+ }
45
+ has(e) {
46
+ return this.slots.has(e);
47
+ }
48
+ /** The raw slot for `id` ({id, originId, peerId, clock, deleted}), or undefined — used to build a wire envelope for a change that was just made. */
49
+ getSlot(e) {
50
+ return this.slots.get(e);
51
+ }
52
+ isDeleted(e) {
53
+ const t = this.slots.get(e);
54
+ return !t || t.deleted;
55
+ }
56
+ /**
57
+ * Inserts `id` immediately after `afterId` (null = at the very start).
58
+ * Idempotent: re-inserting an id that already has a slot is a no-op,
59
+ * which guards against duplicate delivery over an unreliable transport.
60
+ */
61
+ insert(e, t, s, o) {
62
+ this.slots.has(e) || this.slots.set(e, { id: e, originId: t, peerId: o, clock: s, deleted: !1 });
63
+ }
64
+ /**
65
+ * `clock` records WHEN the delete happened (used by pruneTombstones to
66
+ * judge tombstone age) — deliberately a separate value from whatever
67
+ * clock the slot already had from insertion/move, which recorded a
68
+ * different thing (when its POSITION was last established). Without
69
+ * this, an old-but-just-deleted slot would look ancient from the
70
+ * moment it's deleted (inheriting its original insertion time, however
71
+ * long ago that was) and become immediately GC-eligible.
72
+ */
73
+ delete(e, t) {
74
+ const s = this.slots.get(e);
75
+ s && (s.deleted = !0, t && (s.deletedClock = t));
76
+ }
77
+ /** Undoes a local delete in place, preserving the slot's original position. */
78
+ restore(e) {
79
+ const t = this.slots.get(e);
80
+ t && (t.deleted = !1);
81
+ }
82
+ /**
83
+ * Repositions an existing slot to a new anchor in place, rather than
84
+ * tombstoning it and creating a new one — there is only ever one slot
85
+ * per id in a given list, so a same-list reorder (or moving a block
86
+ * back to a parent it was in before) must reuse it. Local-only for now:
87
+ * concurrent moves of the same id by two peers have no defined merge
88
+ * rule yet (last local move simply wins), since there's no remote path
89
+ * exercising this until the transport layer exists.
90
+ *
91
+ * Splices the slot out of the chain BEFORE re-anchoring it: anything
92
+ * currently anchored directly after `id` gets reparented to `id`'s old
93
+ * origin first. Without this, moving a node to an anchor that's
94
+ * currently one of its own (transitive) successors creates a cycle —
95
+ * e.g. a->b->c, "move a to after c" naively sets a.originId='c', but
96
+ * c.originId is (transitively) 'a', so the walk from the list start can
97
+ * no longer reach any of them. Splicing `id` out first means whatever
98
+ * anchor is chosen can never depend on `id` still being where it was.
99
+ */
100
+ move(e, t, s, o) {
101
+ if (t === e) return;
102
+ const r = this.slots.get(e);
103
+ if (!r) return;
104
+ const i = r.originId;
105
+ for (const c of this.slots.values())
106
+ c.originId === e && (c.originId = i);
107
+ r.originId = t, r.peerId = o, r.clock = s, r.deleted = !1;
108
+ }
109
+ /**
110
+ * Merges another peer's slot set into this one. Safe to call with
111
+ * slots in any order, any number of times — idempotent, commutative,
112
+ * and associative — which is what makes convergence hold regardless
113
+ * of network delivery order.
114
+ */
115
+ merge(e) {
116
+ for (const t of e) {
117
+ const s = this.slots.get(t.id);
118
+ s ? t.deleted && !s.deleted && (s.deleted = !0) : this.slots.set(t.id, { ...t });
119
+ }
120
+ }
121
+ /** Every slot as a plain array, for persistence/transport. */
122
+ toSlotArray() {
123
+ return Array.from(this.slots.values());
124
+ }
125
+ /** How many tombstoned (deleted but still retained) slots this list is currently carrying — for deciding whether GC is worth running, or just observing growth. */
126
+ tombstoneCount() {
127
+ let e = 0;
128
+ for (const t of this.slots.values())
129
+ t.deleted && (e += 1);
130
+ return e;
131
+ }
132
+ /**
133
+ * Permanently removes tombstoned slots whose delete happened before
134
+ * `beforeClock` — the actual fix for the "deleted content is never
135
+ * garbage-collected" limitation. Splices each removed slot out of the
136
+ * chain first (children reparented to its own origin, same technique
137
+ * as `move()`) so removal never orphans a slot anchored to it. Returns
138
+ * the number of slots actually removed.
139
+ *
140
+ * Safety of this is specifically tied to how this package's transport
141
+ * layer reconnects: `CollabSession` resyncs a reconnecting peer via a
142
+ * full document snapshot (see syncResponse in CollabSession.js), never
143
+ * by replaying a log of historical ops — so a peer that was offline
144
+ * longer than any reasonable GC threshold never needs an old tombstone
145
+ * to resolve a stale anchor; it just adopts the current state directly.
146
+ * The one theoretical residual risk is a single already-open connection
147
+ * somehow stalling for exactly as long as the GC threshold and then
148
+ * delivering an old queued message afterward — vanishingly unlikely for
149
+ * a live, ordered, reliable data channel (which disconnects long before
150
+ * that under any real network interruption), but not mathematically
151
+ * impossible, which is why this is opt-in rather than automatic.
152
+ */
153
+ pruneTombstones(e) {
154
+ let t = 0;
155
+ for (const [s, o] of this.slots) {
156
+ if (!o.deleted) continue;
157
+ const r = o.deletedClock ?? o.clock;
158
+ if (!(n.compare(r, e) >= 0)) {
159
+ for (const i of this.slots.values())
160
+ i.originId === s && (i.originId = o.originId);
161
+ this.slots.delete(s), t += 1;
162
+ }
163
+ }
164
+ return t;
165
+ }
166
+ /**
167
+ * Materializes the currently-visible order as a plain array of ids
168
+ * (tombstoned ids excluded) — this is what gets projected onto
169
+ * Block.contentIds after every structural change.
170
+ */
171
+ toArray() {
172
+ const e = /* @__PURE__ */ new Map();
173
+ for (const o of this.slots.values()) {
174
+ const r = e.get(o.originId);
175
+ r ? r.push(o) : e.set(o.originId, [o]);
176
+ }
177
+ for (const o of e.values())
178
+ o.sort((r, i) => n.compare(i.clock, r.clock));
179
+ const t = [], s = [...e.get(null) ?? []].reverse();
180
+ for (; s.length > 0; ) {
181
+ const o = s.pop();
182
+ o.deleted || t.push(o.id);
183
+ const r = e.get(o.id);
184
+ if (r)
185
+ for (let i = r.length - 1; i >= 0; i -= 1) s.push(r[i]);
186
+ }
187
+ return t;
188
+ }
189
+ }
190
+ class l {
191
+ constructor() {
192
+ this.clocks = /* @__PURE__ */ new Map();
193
+ }
194
+ static compositeKey(e, t) {
195
+ return `${e}:${t}`;
196
+ }
197
+ get(e, t) {
198
+ return this.clocks.get(l.compositeKey(e, t)) ?? null;
199
+ }
200
+ /** Local write: always wins outright — it just happened, nothing to compare against. */
201
+ recordLocal(e, t, s) {
202
+ this.clocks.set(l.compositeKey(e, t), s);
203
+ }
204
+ /**
205
+ * Whether a remote write to this field should be applied. Callers must
206
+ * check this before mutating so a stale remote write can be discarded
207
+ * without ever touching local state.
208
+ */
209
+ shouldApplyRemote(e, t, s) {
210
+ const o = this.get(e, t);
211
+ return o ? n.compare(s, o) > 0 : !0;
212
+ }
213
+ recordRemote(e, t, s) {
214
+ this.clocks.set(l.compositeKey(e, t), s);
215
+ }
216
+ }
217
+ export {
218
+ l as F,
219
+ n as H,
220
+ u as L,
221
+ d as g
222
+ };
223
+ //# sourceMappingURL=fieldRegistry-PjJWXFoB.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fieldRegistry-PjJWXFoB.js","sources":["../../src/crdt/clock.js","../../src/crdt/listCrdt.js","../../src/crdt/fieldRegistry.js"],"sourcesContent":["/**\n * Hybrid Logical Clock — a (wallTime, counter, peerId) timestamp used to\n * totally order concurrent edits across peers without a central server.\n * wallTime keeps timestamps meaningful as real time (e.g. \"edited 2 min\n * ago\"); counter breaks ties when multiple local events share a wallTime;\n * peerId breaks ties deterministically when both wallTime and counter\n * match across peers.\n */\nexport class HLC {\n constructor(peerId) {\n this.peerId = peerId;\n this.wallTime = 0;\n this.counter = 0;\n }\n\n /** Local event (a user's own edit). Returns the stamped timestamp. */\n tick() {\n const now = Date.now();\n if (now > this.wallTime) {\n this.wallTime = now;\n this.counter = 0;\n } else {\n this.counter += 1;\n }\n return { wallTime: this.wallTime, counter: this.counter, peerId: this.peerId };\n }\n\n /**\n * Merges in a timestamp observed from a remote peer, advancing this\n * clock so subsequent local ticks are guaranteed to sort after\n * anything just received.\n */\n receive(remote) {\n const now = Date.now();\n const maxWall = Math.max(now, this.wallTime, remote.wallTime);\n if (maxWall === this.wallTime && maxWall === remote.wallTime) {\n this.counter = Math.max(this.counter, remote.counter) + 1;\n } else if (maxWall === this.wallTime) {\n this.counter += 1;\n } else if (maxWall === remote.wallTime) {\n this.counter = remote.counter + 1;\n } else {\n this.counter = 0;\n }\n this.wallTime = maxWall;\n return { wallTime: this.wallTime, counter: this.counter, peerId: this.peerId };\n }\n\n /** Total order: wallTime, then counter, then peerId. */\n static compare(a, b) {\n if (a.wallTime !== b.wallTime) return a.wallTime - b.wallTime;\n if (a.counter !== b.counter) return a.counter - b.counter;\n if (a.peerId === b.peerId) return 0;\n return a.peerId < b.peerId ? -1 : 1;\n }\n}\n\nexport function genPeerId() {\n if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {\n return crypto.randomUUID();\n }\n return `peer-${Date.now()}-${Math.random().toString(36).slice(2)}`;\n}\n","import { HLC } from './clock.js';\n\n/**\n * Replicated ordered list, keyed by externally-owned stable ids (Noteloom's\n * existing block/run ids — this module never generates ids, only orders\n * and tombstones the ones it's given). Each item's position is defined\n * relative to a neighbor id (\"insert after X\"), not an array index, so\n * concurrent inserts/deletes at different local positions merge\n * deterministically regardless of what order the operations are applied\n * in on a given peer.\n *\n * Concurrent inserts anchored to the exact same origin are ordered\n * newest-first (by clock, then peerId) so a live edit lands as close to\n * its anchor as possible. Sequential inserts from one peer/batch (each\n * anchored to the id it just inserted, not re-anchored to the original\n * origin — the natural way typing and multi-block paste already work)\n * are never split apart by a concurrent peer's insert at the same\n * origin. A concurrent *non-chained* multi-item insert at the same\n * anchor by two peers can still interleave with this simpler\n * (RGA-style) scheme; convergence still holds, only interleave-avoidance\n * is partial rather than the full Fugue tree guarantee.\n */\nexport class ListCrdtState {\n constructor() {\n this.slots = new Map();\n }\n\n /** Builds CRDT state from an existing plain-array order (e.g. loading a v1 document with no CRDT history). */\n static fromArray(ids, { peerId = 'legacy' } = {}) {\n const state = new ListCrdtState();\n let prev = null;\n for (const id of ids) {\n state.slots.set(id, {\n id,\n originId: prev,\n peerId,\n clock: { wallTime: 0, counter: 0, peerId },\n deleted: false,\n });\n prev = id;\n }\n return state;\n }\n\n has(id) {\n return this.slots.has(id);\n }\n\n /** The raw slot for `id` ({id, originId, peerId, clock, deleted}), or undefined — used to build a wire envelope for a change that was just made. */\n getSlot(id) {\n return this.slots.get(id);\n }\n\n isDeleted(id) {\n const slot = this.slots.get(id);\n return !slot || slot.deleted;\n }\n\n /**\n * Inserts `id` immediately after `afterId` (null = at the very start).\n * Idempotent: re-inserting an id that already has a slot is a no-op,\n * which guards against duplicate delivery over an unreliable transport.\n */\n insert(id, afterId, clock, peerId) {\n if (this.slots.has(id)) return;\n this.slots.set(id, { id, originId: afterId, peerId, clock, deleted: false });\n }\n\n /**\n * `clock` records WHEN the delete happened (used by pruneTombstones to\n * judge tombstone age) — deliberately a separate value from whatever\n * clock the slot already had from insertion/move, which recorded a\n * different thing (when its POSITION was last established). Without\n * this, an old-but-just-deleted slot would look ancient from the\n * moment it's deleted (inheriting its original insertion time, however\n * long ago that was) and become immediately GC-eligible.\n */\n delete(id, clock) {\n const slot = this.slots.get(id);\n if (!slot) return;\n slot.deleted = true;\n if (clock) slot.deletedClock = clock;\n }\n\n /** Undoes a local delete in place, preserving the slot's original position. */\n restore(id) {\n const slot = this.slots.get(id);\n if (!slot) return;\n slot.deleted = false;\n }\n\n /**\n * Repositions an existing slot to a new anchor in place, rather than\n * tombstoning it and creating a new one — there is only ever one slot\n * per id in a given list, so a same-list reorder (or moving a block\n * back to a parent it was in before) must reuse it. Local-only for now:\n * concurrent moves of the same id by two peers have no defined merge\n * rule yet (last local move simply wins), since there's no remote path\n * exercising this until the transport layer exists.\n *\n * Splices the slot out of the chain BEFORE re-anchoring it: anything\n * currently anchored directly after `id` gets reparented to `id`'s old\n * origin first. Without this, moving a node to an anchor that's\n * currently one of its own (transitive) successors creates a cycle —\n * e.g. a->b->c, \"move a to after c\" naively sets a.originId='c', but\n * c.originId is (transitively) 'a', so the walk from the list start can\n * no longer reach any of them. Splicing `id` out first means whatever\n * anchor is chosen can never depend on `id` still being where it was.\n */\n move(id, afterId, clock, peerId) {\n if (afterId === id) return;\n const slot = this.slots.get(id);\n if (!slot) return;\n const oldOriginId = slot.originId;\n for (const other of this.slots.values()) {\n if (other.originId === id) other.originId = oldOriginId;\n }\n slot.originId = afterId;\n slot.peerId = peerId;\n slot.clock = clock;\n slot.deleted = false;\n }\n\n /**\n * Merges another peer's slot set into this one. Safe to call with\n * slots in any order, any number of times — idempotent, commutative,\n * and associative — which is what makes convergence hold regardless\n * of network delivery order.\n */\n merge(remoteSlots) {\n for (const remote of remoteSlots) {\n const local = this.slots.get(remote.id);\n if (!local) {\n this.slots.set(remote.id, { ...remote });\n } else if (remote.deleted && !local.deleted) {\n local.deleted = true;\n }\n // originId/peerId/clock never change after a slot is created —\n // a slot's structural identity is fixed at insert time; `deleted`\n // is the only field that can change, and only monotonically\n // (existence-wins: once deleted, always deleted).\n }\n }\n\n /** Every slot as a plain array, for persistence/transport. */\n toSlotArray() {\n return Array.from(this.slots.values());\n }\n\n /** How many tombstoned (deleted but still retained) slots this list is currently carrying — for deciding whether GC is worth running, or just observing growth. */\n tombstoneCount() {\n let count = 0;\n for (const slot of this.slots.values()) {\n if (slot.deleted) count += 1;\n }\n return count;\n }\n\n /**\n * Permanently removes tombstoned slots whose delete happened before\n * `beforeClock` — the actual fix for the \"deleted content is never\n * garbage-collected\" limitation. Splices each removed slot out of the\n * chain first (children reparented to its own origin, same technique\n * as `move()`) so removal never orphans a slot anchored to it. Returns\n * the number of slots actually removed.\n *\n * Safety of this is specifically tied to how this package's transport\n * layer reconnects: `CollabSession` resyncs a reconnecting peer via a\n * full document snapshot (see syncResponse in CollabSession.js), never\n * by replaying a log of historical ops — so a peer that was offline\n * longer than any reasonable GC threshold never needs an old tombstone\n * to resolve a stale anchor; it just adopts the current state directly.\n * The one theoretical residual risk is a single already-open connection\n * somehow stalling for exactly as long as the GC threshold and then\n * delivering an old queued message afterward — vanishingly unlikely for\n * a live, ordered, reliable data channel (which disconnects long before\n * that under any real network interruption), but not mathematically\n * impossible, which is why this is opt-in rather than automatic.\n */\n pruneTombstones(beforeClock) {\n let removed = 0;\n for (const [id, slot] of this.slots) {\n if (!slot.deleted) continue;\n // deletedClock (when it was actually deleted) is what age means\n // here -- falling back to the slot's own clock (when its position\n // was established) only for a slot deleted before this field\n // existed, which otherwise has no recorded deletion time at all.\n const age = slot.deletedClock ?? slot.clock;\n if (HLC.compare(age, beforeClock) >= 0) continue; // not old enough yet\n for (const other of this.slots.values()) {\n if (other.originId === id) other.originId = slot.originId;\n }\n this.slots.delete(id);\n removed += 1;\n }\n return removed;\n }\n\n /**\n * Materializes the currently-visible order as a plain array of ids\n * (tombstoned ids excluded) — this is what gets projected onto\n * Block.contentIds after every structural change.\n */\n toArray() {\n const childrenByOrigin = new Map();\n for (const slot of this.slots.values()) {\n const bucket = childrenByOrigin.get(slot.originId);\n if (bucket) bucket.push(slot);\n else childrenByOrigin.set(slot.originId, [slot]);\n }\n for (const bucket of childrenByOrigin.values()) {\n bucket.sort((a, b) => HLC.compare(b.clock, a.clock));\n }\n\n const result = [];\n // Explicit-stack depth-first walk — a naive recursive walk's depth\n // equals the list length on a long simple chain (the common case for\n // typed/sequential content), which risks a stack overflow on large\n // documents.\n const stack = [...(childrenByOrigin.get(null) ?? [])].reverse();\n while (stack.length > 0) {\n const slot = stack.pop();\n if (!slot.deleted) result.push(slot.id);\n const children = childrenByOrigin.get(slot.id);\n if (children) {\n for (let i = children.length - 1; i >= 0; i -= 1) stack.push(children[i]);\n }\n }\n return result;\n }\n}\n","import { HLC } from './clock.js';\n\n/**\n * Tracks the logical-clock high-water mark for individual mutable fields\n * (a block's type, one prop key, a run's value, ...) so that when a\n * remote peer's write arrives it's only applied if it's actually newer\n * than whatever's already there — last-write-wins, scoped per field\n * rather than per whole block/run, so unrelated concurrent edits (peer A\n * changes the type, peer B changes a prop) both survive.\n */\nexport class FieldClockRegistry {\n constructor() {\n this.clocks = new Map();\n }\n\n static compositeKey(id, field) {\n return `${id}:${field}`;\n }\n\n get(id, field) {\n return this.clocks.get(FieldClockRegistry.compositeKey(id, field)) ?? null;\n }\n\n /** Local write: always wins outright — it just happened, nothing to compare against. */\n recordLocal(id, field, clock) {\n this.clocks.set(FieldClockRegistry.compositeKey(id, field), clock);\n }\n\n /**\n * Whether a remote write to this field should be applied. Callers must\n * check this before mutating so a stale remote write can be discarded\n * without ever touching local state.\n */\n shouldApplyRemote(id, field, remoteClock) {\n const current = this.get(id, field);\n if (!current) return true;\n return HLC.compare(remoteClock, current) > 0;\n }\n\n recordRemote(id, field, remoteClock) {\n this.clocks.set(FieldClockRegistry.compositeKey(id, field), remoteClock);\n }\n}\n"],"names":["HLC","peerId","now","remote","maxWall","a","b","genPeerId","ListCrdtState","ids","state","prev","id","slot","afterId","clock","oldOriginId","other","remoteSlots","local","count","beforeClock","removed","age","childrenByOrigin","bucket","result","stack","children","FieldClockRegistry","field","remoteClock","current"],"mappings":"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"}