edytor 3.0.0 → 5.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 (231) hide show
  1. package/README.md +2 -2
  2. package/dist/block/block.svelte.js +6 -21
  3. package/dist/block/inlineBlock.svelte.js +4 -6
  4. package/dist/clipboard/moveText.js +6 -8
  5. package/dist/cloudflare/DocumentRoom.d.ts +23 -12
  6. package/dist/cloudflare/DocumentRoom.js +10 -6
  7. package/dist/cloudflare/index.d.ts +1 -1
  8. package/dist/cloudflare/room/access.d.ts +8 -3
  9. package/dist/cloudflare/room/access.js +44 -20
  10. package/dist/cloudflare/room/admission.js +28 -5
  11. package/dist/cloudflare/room/comments.js +28 -1
  12. package/dist/cloudflare/room/context.d.ts +139 -129
  13. package/dist/cloudflare/room/context.js +11 -4
  14. package/dist/cloudflare/room/forged.d.ts +14 -0
  15. package/dist/cloudflare/room/forged.js +30 -2
  16. package/dist/cloudflare/room/frames.d.ts +4 -0
  17. package/dist/cloudflare/room/frames.js +5 -1
  18. package/dist/cloudflare/room/purge.d.ts +6 -3
  19. package/dist/cloudflare/room/purge.js +45 -19
  20. package/dist/cloudflare/room/replicas.d.ts +4 -2
  21. package/dist/cloudflare/room/replicas.js +8 -3
  22. package/dist/cloudflare/room/suggest.d.ts +7 -0
  23. package/dist/cloudflare/room/suggest.js +143 -0
  24. package/dist/cloudflare/room/updates.d.ts +3 -4
  25. package/dist/cloudflare/room/updates.js +6 -8
  26. package/dist/cloudflare/routeDocumentSocket.d.ts +10 -1
  27. package/dist/cloudflare/routeDocumentSocket.js +5 -3
  28. package/dist/components/Content.svelte +9 -1
  29. package/dist/crdt/anchors.js +3 -2
  30. package/dist/crdt/attribution/attribution.d.ts +34 -12
  31. package/dist/crdt/attribution/attribution.js +59 -21
  32. package/dist/crdt/attribution/block.d.ts +3 -3
  33. package/dist/crdt/attribution/block.js +7 -8
  34. package/dist/crdt/attribution/index.d.ts +1 -1
  35. package/dist/crdt/attribution/index.js +1 -1
  36. package/dist/crdt/data.d.ts +62 -31
  37. package/dist/crdt/data.js +211 -103
  38. package/dist/crdt/doc/capability.js +9 -13
  39. package/dist/crdt/doc/delete.d.ts +1 -1
  40. package/dist/crdt/doc/delete.js +13 -30
  41. package/dist/crdt/doc/funnel.d.ts +1 -1
  42. package/dist/crdt/doc/funnel.js +34 -26
  43. package/dist/crdt/doc/gate.d.ts +3 -3
  44. package/dist/crdt/doc/history.d.ts +31 -9
  45. package/dist/crdt/doc/history.js +204 -73
  46. package/dist/crdt/doc/layout.js +16 -27
  47. package/dist/crdt/doc/meta.js +17 -23
  48. package/dist/crdt/doc/moves.d.ts +1 -1
  49. package/dist/crdt/doc/moves.js +52 -32
  50. package/dist/crdt/doc/reads.d.ts +2 -2
  51. package/dist/crdt/doc/reads.js +20 -30
  52. package/dist/crdt/doc/seed.js +2 -2
  53. package/dist/crdt/doc/split-merge.js +12 -15
  54. package/dist/crdt/doc/steps.d.ts +25 -13
  55. package/dist/crdt/doc/steps.js +55 -125
  56. package/dist/crdt/doc/suggest.d.ts +167 -0
  57. package/dist/crdt/doc/suggest.js +579 -0
  58. package/dist/crdt/doc/types.d.ts +21 -9
  59. package/dist/crdt/document.d.ts +26 -6
  60. package/dist/crdt/document.js +40 -15
  61. package/dist/crdt/edytor-doc.d.ts +410 -390
  62. package/dist/crdt/edytor-doc.js +143 -95
  63. package/dist/crdt/engine-api.d.ts +39 -1
  64. package/dist/crdt/engine.d.ts +2 -2
  65. package/dist/crdt/engine.js +4 -5
  66. package/dist/crdt/flow.d.ts +2 -2
  67. package/dist/crdt/flow.js +29 -37
  68. package/dist/crdt/index.d.ts +134 -127
  69. package/dist/crdt/index.js +4 -1
  70. package/dist/crdt/operations.d.ts +12 -3
  71. package/dist/crdt/parts.d.ts +43 -0
  72. package/dist/crdt/parts.js +44 -0
  73. package/dist/crdt/placement/display.d.ts +6 -31
  74. package/dist/crdt/placement/display.js +41 -146
  75. package/dist/crdt/placement/keys.d.ts +63 -0
  76. package/dist/crdt/placement/keys.js +95 -0
  77. package/dist/crdt/placement/model.d.ts +119 -118
  78. package/dist/crdt/placement/model.js +242 -142
  79. package/dist/crdt/placement/resolve.d.ts +123 -35
  80. package/dist/crdt/placement/resolve.js +188 -153
  81. package/dist/crdt/placement/slots.d.ts +134 -0
  82. package/dist/crdt/placement/slots.js +152 -0
  83. package/dist/crdt/protocol.d.ts +1 -1
  84. package/dist/crdt/protocol.js +1 -1
  85. package/dist/crdt/protocols/auth.d.ts +11 -1
  86. package/dist/crdt/protocols/auth.js +17 -1
  87. package/dist/crdt/protocols/comments.d.ts +16 -1
  88. package/dist/crdt/protocols/comments.js +9 -1
  89. package/dist/crdt/providers/index.d.ts +6 -4
  90. package/dist/crdt/providers/index.js +3 -1
  91. package/dist/crdt/providers/room.d.ts +16 -2
  92. package/dist/crdt/providers/room.js +64 -17
  93. package/dist/crdt/providers/websocket.d.ts +6 -4
  94. package/dist/crdt/providers/websocket.js +14 -7
  95. package/dist/crdt/purge.d.ts +24 -9
  96. package/dist/crdt/purge.js +266 -126
  97. package/dist/crdt/restore.d.ts +4 -3
  98. package/dist/crdt/restore.js +27 -34
  99. package/dist/crdt/schema.d.ts +30 -21
  100. package/dist/crdt/schema.js +39 -23
  101. package/dist/crdt/storage.d.ts +1 -1
  102. package/dist/crdt/storage.js +1 -1
  103. package/dist/crdt/structs.d.ts +17 -15
  104. package/dist/crdt/structs.js +10 -22
  105. package/dist/crdt/text/index/cache.js +4 -0
  106. package/dist/crdt/text/index/claims.js +13 -23
  107. package/dist/crdt/text/index/fold.d.ts +2 -3
  108. package/dist/crdt/text/index/fold.js +194 -44
  109. package/dist/crdt/text/index/layout.d.ts +2 -4
  110. package/dist/crdt/text/index/layout.js +14 -30
  111. package/dist/crdt/text/index/placement.d.ts +26 -18
  112. package/dist/crdt/text/index/placement.js +420 -157
  113. package/dist/crdt/text/index/records.d.ts +3 -3
  114. package/dist/crdt/text/index/records.js +24 -31
  115. package/dist/crdt/text/index/shared.d.ts +11 -3
  116. package/dist/crdt/text/index/shared.js +29 -17
  117. package/dist/crdt/text/index/state.d.ts +21 -17
  118. package/dist/crdt/text/index/state.js +15 -17
  119. package/dist/crdt/text/index/streams.d.ts +10 -5
  120. package/dist/crdt/text/index/streams.js +78 -27
  121. package/dist/crdt/text/items.d.ts +10 -3
  122. package/dist/crdt/text/items.js +7 -0
  123. package/dist/crdt/text/model.d.ts +87 -62
  124. package/dist/crdt/text/model.js +91 -169
  125. package/dist/crdt/text/ranges.d.ts +1 -1
  126. package/dist/crdt/text/ranges.js +3 -3
  127. package/dist/crdt/text/rows.d.ts +13 -7
  128. package/dist/crdt/text/rows.js +24 -7
  129. package/dist/crdt/text/runs.d.ts +51 -42
  130. package/dist/crdt/text/runs.js +24 -7
  131. package/dist/crdt/text/streams.d.ts +26 -13
  132. package/dist/crdt/text/streams.js +41 -14
  133. package/dist/crdt/vendor/yjs/UPSTREAM.md +651 -21
  134. package/dist/crdt/vendor/yjs/dts/global.d.ts +2 -0
  135. package/dist/crdt/vendor/yjs/dts/index.d.ts +1 -1
  136. package/dist/crdt/vendor/yjs/dts/structs/AbstractStruct.d.ts +7 -0
  137. package/dist/crdt/vendor/yjs/dts/structs/Item.d.ts +197 -10
  138. package/dist/crdt/vendor/yjs/dts/utils/Doc.d.ts +6 -3
  139. package/dist/crdt/vendor/yjs/dts/utils/StructStore.d.ts +5 -18
  140. package/dist/crdt/vendor/yjs/dts/utils/Transaction.d.ts +15 -1
  141. package/dist/crdt/vendor/yjs/dts/utils/UpdateDecoder.d.ts +14 -6
  142. package/dist/crdt/vendor/yjs/dts/utils/UpdateEncoder.d.ts +33 -6
  143. package/dist/crdt/vendor/yjs/dts/utils/ids.d.ts +1 -65
  144. package/dist/crdt/vendor/yjs/dts/utils/marks.d.ts +6 -1
  145. package/dist/crdt/vendor/yjs/dts/ynode.d.ts +54 -7
  146. package/dist/crdt/vendor/yjs/global.d.ts +2 -0
  147. package/dist/crdt/vendor/yjs/src/index.js +1 -1
  148. package/dist/crdt/vendor/yjs/src/structs/AbstractStruct.js +10 -0
  149. package/dist/crdt/vendor/yjs/src/structs/Item.js +649 -51
  150. package/dist/crdt/vendor/yjs/src/utils/BlockSet.js +18 -9
  151. package/dist/crdt/vendor/yjs/src/utils/Doc.js +5 -3
  152. package/dist/crdt/vendor/yjs/src/utils/StructStore.js +5 -0
  153. package/dist/crdt/vendor/yjs/src/utils/Transaction.js +36 -13
  154. package/dist/crdt/vendor/yjs/src/utils/UpdateDecoder.js +88 -18
  155. package/dist/crdt/vendor/yjs/src/utils/UpdateEncoder.js +119 -32
  156. package/dist/crdt/vendor/yjs/src/utils/encoding.js +13 -1
  157. package/dist/crdt/vendor/yjs/src/utils/ids.js +7 -7
  158. package/dist/crdt/vendor/yjs/src/utils/marks.js +193 -8
  159. package/dist/crdt/vendor/yjs/src/utils/redo.js +1 -1
  160. package/dist/crdt/vendor/yjs/src/utils/transaction-helpers.js +1 -1
  161. package/dist/crdt/vendor/yjs/src/utils/updates.js +3 -3
  162. package/dist/crdt/vendor/yjs/src/ynode.js +199 -12
  163. package/dist/edytor.svelte.d.ts +1 -1
  164. package/dist/edytor.svelte.js +103 -75
  165. package/dist/events/beforeInputCommands.d.ts +2 -2
  166. package/dist/events/beforeInputCommands.js +2 -2
  167. package/dist/events/nativeInteractiveControl.d.ts +11 -1
  168. package/dist/events/nativeInteractiveControl.js +41 -1
  169. package/dist/events/onBeforeInput.js +3 -3
  170. package/dist/events/onFocus.js +9 -11
  171. package/dist/events/onPageHide.d.ts +3 -0
  172. package/dist/events/onPageHide.js +16 -0
  173. package/dist/export/forms.d.ts +50 -50
  174. package/dist/index.d.ts +4 -2
  175. package/dist/index.js +1 -1
  176. package/dist/labels.d.ts +8 -0
  177. package/dist/labels.js +6 -1
  178. package/dist/plugins/comments/CommentsController.svelte.d.ts +5 -1
  179. package/dist/plugins/comments/CommentsController.svelte.js +14 -4
  180. package/dist/plugins/find/search.js +2 -2
  181. package/dist/plugins/index.d.ts +1 -0
  182. package/dist/plugins/index.js +1 -0
  183. package/dist/plugins/suggestions/SuggestionBars.svelte +40 -23
  184. package/dist/plugins/suggestions/SuggestionBars.svelte.d.ts +3 -1
  185. package/dist/plugins/suggestions/SuggestionRuns.svelte +77 -0
  186. package/dist/plugins/suggestions/SuggestionRuns.svelte.d.ts +9 -0
  187. package/dist/plugins/suggestions/shared.svelte.d.ts +58 -0
  188. package/dist/plugins/suggestions/shared.svelte.js +421 -0
  189. package/dist/plugins/suggestions/suggestionsPlugin.d.ts +15 -4
  190. package/dist/selection/domSelection.d.ts +2 -0
  191. package/dist/selection/domSelection.js +6 -2
  192. package/dist/selection/pointer.svelte.js +75 -48
  193. package/dist/selection/replaceSelection.js +2 -6
  194. package/dist/selection/selection.svelte.d.ts +3 -3
  195. package/dist/selection/selection.svelte.js +39 -32
  196. package/dist/selection/selection.utils.d.ts +14 -0
  197. package/dist/selection/selection.utils.js +28 -3
  198. package/dist/session/commands.js +21 -11
  199. package/dist/session/composition.svelte.d.ts +5 -3
  200. package/dist/session/composition.svelte.js +21 -17
  201. package/dist/session/handles.d.ts +4 -17
  202. package/dist/session/handles.js +11 -37
  203. package/dist/session/navigation.js +6 -9
  204. package/dist/session/selection.d.ts +11 -5
  205. package/dist/session/selection.js +14 -48
  206. package/dist/session/suggestions.svelte.d.ts +38 -9
  207. package/dist/session/suggestions.svelte.js +37 -18
  208. package/dist/session/virtual.d.ts +1 -1
  209. package/dist/surface/attributes.d.ts +5 -0
  210. package/dist/surface/attributes.js +2 -2
  211. package/dist/surface/cells.d.ts +2 -26
  212. package/dist/surface/cells.js +2 -33
  213. package/dist/surface/observer.svelte.d.ts +6 -1
  214. package/dist/surface/observer.svelte.js +130 -83
  215. package/dist/surface/pin.svelte.d.ts +9 -9
  216. package/dist/surface/pin.svelte.js +15 -22
  217. package/dist/surface/projector.svelte.d.ts +15 -3
  218. package/dist/surface/projector.svelte.js +67 -22
  219. package/dist/text/text.svelte.js +14 -19
  220. package/dist/themes/notion.css +29 -0
  221. package/package.json +4 -2
  222. package/dist/crdt/placement/rank.d.ts +0 -87
  223. package/dist/crdt/placement/rank.js +0 -332
  224. package/dist/crdt/placement/source.d.ts +0 -43
  225. package/dist/crdt/placement/source.js +0 -66
  226. package/dist/crdt/text/delete-index.d.ts +0 -129
  227. package/dist/crdt/text/delete-index.js +0 -365
  228. package/dist/crdt/text/deletes.d.ts +0 -76
  229. package/dist/crdt/text/deletes.js +0 -362
  230. package/dist/crdt/text/index/anchored.d.ts +0 -17
  231. package/dist/crdt/text/index/anchored.js +0 -197
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Slots and move records — where a block or an array item stands (fork
3
+ * patch YP22).
4
+ *
5
+ * Every data owner keeps ONE order sequence, its node's list part (a block's
6
+ * node, an atom's node, the registry root for the top-level blocks, the
7
+ * `docdata` root for the document's data). The sequence holds one countable
8
+ * unit per thing it orders:
9
+ *
10
+ * - a block's **home slot** `{ b: id }`, written once at the block's creation
11
+ * in its parent's sequence, right after its left sibling's unit (YATA's
12
+ * origin rule: later inserts there stay together, `order.insert.run`);
13
+ * - an array item's **slot** `{ p: path, i: id }` (`crdt/data.ts`): the
14
+ * item exists while its slot does;
15
+ * - a **move record** (the engine's `ContentMove`): one unit at the
16
+ * destination naming the units it moves, in order. Each named unit shows
17
+ * at the record that wins its movers — the greatest `(priority, client,
18
+ * clock)` — and reads as deleted at home, so a sequence walk sees it gone
19
+ * from there. Deleting a record (an undo) hands its units to the next
20
+ * record, else home.
21
+ *
22
+ * A reader of a sequence (`entriesOf`) walks its live items: a slot at home,
23
+ * or a record expanded to the units it shows. A writer places a new slot or a
24
+ * record after a displayed entry (`placeAfter`): when that entry is a unit in
25
+ * the middle of a record's display, the new record also carries the record's
26
+ * later units, so the new thing shows between them.
27
+ *
28
+ * The engine is read through `engine.js` (structural types, `engine-api.ts`).
29
+ */
30
+ import type { EngineDoc, EngineNode } from '../engine-api.js';
31
+ import { type StoreStruct } from '../structs.js';
32
+ export type BlockId = string;
33
+ /**
34
+ * A block's home slot in its parent's sequence; `s`, when the block was made
35
+ * by a split (Enter, a paste of lines): the id (`client, clock`) of the
36
+ * split's boundary item in the text, the engine's sort key among the slots
37
+ * of one origin (the boundaries' order in the text, so two peers' splits of
38
+ * one block keep its text's order, `order.split.text`).
39
+ */
40
+ export type BlockSlot = {
41
+ b: BlockId;
42
+ s?: [client: number, clock: number];
43
+ };
44
+ /** An array item's slot: the array's path (`crdt/data.ts`) and the item's id. */
45
+ export type DataSlot = {
46
+ p: string;
47
+ i: string;
48
+ };
49
+ /** One unit of a sequence: its engine id. */
50
+ export type Unit = {
51
+ c: number;
52
+ k: number;
53
+ };
54
+ export declare const isBlockSlot: (v: unknown) => v is BlockSlot;
55
+ export declare const isDataSlot: (v: unknown) => v is DataSlot;
56
+ /** A sequence item as the slot reads walk it. */
57
+ export type SeqItem = StoreStruct & {
58
+ left: SeqItem | null;
59
+ right: SeqItem | null;
60
+ content: {
61
+ arr?: unknown[];
62
+ priority?: number;
63
+ side?: number;
64
+ part?: number;
65
+ at?: unknown;
66
+ ids?: number[];
67
+ };
68
+ /** The record that shows this unit (fork patch YP22), `null` at home. */
69
+ mover?: SeqItem | null;
70
+ movers?: SeqItem[] | null;
71
+ /** Removed or held: shown nowhere. */
72
+ gone?: boolean;
73
+ };
74
+ /** Whether `it` is a move record. */
75
+ export declare const isRecord: (it: SeqItem) => boolean;
76
+ /** A record's stamp, the order its wins follow: `(priority, client, clock)`. */
77
+ export type Stamp = readonly [priority: number, client: number, clock: number];
78
+ export declare const stampOf: (r: SeqItem) => Stamp;
79
+ export declare const cmpStamp: (a: Stamp, b: Stamp) => number;
80
+ /** One displayed entry of a sequence: its value, its unit, and the record it shows through (`null`: at home; `at`: its place among the units read with it). */
81
+ export type Entry = {
82
+ value: unknown;
83
+ unit: Unit;
84
+ via: SeqItem | null;
85
+ at: number;
86
+ };
87
+ /** Whether `a` and `b` are one unit. */
88
+ export declare const sameUnit: (a: Unit, b: Unit) => boolean;
89
+ /** The item holding unit `u`, or `null` when `doc` lacks it (no value read: the fold's hot path). */
90
+ export declare const itemAt: (doc: EngineDoc, u: Unit) => SeqItem | null;
91
+ /** The item holding unit `u` and the value at it, or `null` when `doc` lacks it (a GC struct, not arrived). */
92
+ export declare const unitOf: (doc: EngineDoc, u: Unit) => {
93
+ item: SeqItem;
94
+ value: unknown;
95
+ } | null;
96
+ /**
97
+ * The units item `it` can show, in order, those `keep` takes: a live
98
+ * record's named units (present, not gone; `via` the record), a slot item's
99
+ * units, moved away or not (none when it is gone; `via` `null`). THE walk
100
+ * every reader of a sequence's units takes: the engine's (`shownBy`), the
101
+ * index's places (`placement/resolve.ts`) and the writers' view of them
102
+ * (`placement/model.ts`).
103
+ */
104
+ export declare const unitsOf: (doc: EngineDoc, it: SeqItem, keep: (item: SeqItem, value: unknown, unit: Unit) => boolean) => Entry[];
105
+ /** The units the engine shows at item `it`, in order: a live record's, those it wins; a live slot item's, all. */
106
+ export declare const shownBy: (doc: EngineDoc, it: SeqItem) => Entry[];
107
+ /** The displayed entries of `node`'s sequence, in order. */
108
+ export declare const entriesOf: (doc: EngineDoc, node: EngineNode) => Entry[];
109
+ /**
110
+ * The entry unit `home` displays as: at home when no live record wins it,
111
+ * else at its mover (`via`, `at`); `null` when it shows nowhere (gone).
112
+ */
113
+ export declare const anchorOf: (doc: EngineDoc, home: Unit) => Entry | null;
114
+ /** The sequence node that holds `e`'s displaying item (its record's, or its slot's). */
115
+ export declare const nodeOf: (doc: EngineDoc, e: Entry) => EngineNode;
116
+ /** `units` as `client, clock, len` triples (one unit each). */
117
+ export declare const triples: (units: readonly Unit[]) => number[];
118
+ /**
119
+ * Write new slots (`payloads`) and/or a move of `moved` (unit triples) into
120
+ * `node`'s sequence right after the displayed entry `left` (at the start when
121
+ * `null`; `right` the entry that follows, if any). When `left` is a unit in
122
+ * the middle of a record's display, the record written here carries that
123
+ * record's later units too, so the new entries show between them. `side` is
124
+ * the record's class among the items of its origin (`0` after, `1` before);
125
+ * `chain` the units the record stands at for the engine's sort of two
126
+ * records at one origin, given its first unit — the home slots from the root
127
+ * sequence down to the unit it stands at (`null`: the first unit alone) — and
128
+ * `part` its order among the records standing there. `shown` reads the units
129
+ * a record shows, as the reader sees them (the index's: a unit a cycle sent
130
+ * back elsewhere is not carried).
131
+ */
132
+ export declare const placeAfter: (doc: EngineDoc, node: EngineNode, left: Entry | null, right: Entry | null, payloads: readonly unknown[] | null, moved?: readonly number[] | null, side?: number, part?: number, chain?: ((first: Unit) => readonly Unit[]) | null, shown?: (r: SeqItem) => Entry[]) => void;
133
+ /** Remove unit `u` from its sequence (the engine's delete: it shows nowhere any more). */
134
+ export declare const removeUnit: (doc: EngineDoc, u: Unit) => void;
@@ -0,0 +1,152 @@
1
+ import { Y } from '../engine.js';
2
+ import { asYDoc, clientsOf, engineOps, firstItem, storeOf, structAt } from '../structs.js';
3
+ export const isBlockSlot = (v) => v != null && typeof v === 'object' && typeof v.b === 'string';
4
+ export const isDataSlot = (v) => v != null &&
5
+ typeof v === 'object' &&
6
+ typeof v.p === 'string' &&
7
+ typeof v.i === 'string';
8
+ /** Whether `it` is a move record. */
9
+ export const isRecord = (it) => it.content.priority !== undefined && it.content.ids !== undefined;
10
+ export const stampOf = (r) => [r.content.priority, r.id.client, r.id.clock];
11
+ export const cmpStamp = (a, b) => a[0] - b[0] || a[1] - b[1] || a[2] - b[2];
12
+ /** Whether `a` and `b` are one unit. */
13
+ export const sameUnit = (a, b) => a.c === b.c && a.k === b.k;
14
+ const ops = engineOps(Y);
15
+ /** The item holding unit `u`, or `null` when `doc` lacks it (no value read: the fold's hot path). */
16
+ export const itemAt = (doc, u) => {
17
+ const structs = clientsOf(doc).get(u.c);
18
+ if (structs === undefined || u.k >= storeOf(doc).getClock(u.c))
19
+ return null;
20
+ const item = structAt(Y, structs, u.k);
21
+ return item === null || !ops.isItem(item) ? null : item;
22
+ };
23
+ /** The item holding unit `u` and the value at it, or `null` when `doc` lacks it (a GC struct, not arrived). */
24
+ export const unitOf = (doc, u) => {
25
+ const item = itemAt(doc, u);
26
+ return item === null ? null : { item, value: item.content.arr?.[u.k - item.id.clock] };
27
+ };
28
+ /**
29
+ * The units item `it` can show, in order, those `keep` takes: a live
30
+ * record's named units (present, not gone; `via` the record), a slot item's
31
+ * units, moved away or not (none when it is gone; `via` `null`). THE walk
32
+ * every reader of a sequence's units takes: the engine's (`shownBy`), the
33
+ * index's places (`placement/resolve.ts`) and the writers' view of them
34
+ * (`placement/model.ts`).
35
+ */
36
+ export const unitsOf = (doc, it, keep) => {
37
+ const out = [];
38
+ const add = (item, value, unit, via) => {
39
+ if (item.gone !== true && keep(item, value, unit))
40
+ out.push({ value, unit, via, at: out.length });
41
+ };
42
+ if (!isRecord(it))
43
+ it.content.arr?.forEach((value, j) => add(it, value, { c: it.id.client, k: it.id.clock + j }, null));
44
+ else if (!it.deleted) {
45
+ const ids = it.content.ids;
46
+ for (let i = 0; i < ids.length; i += 3)
47
+ for (let k = 0; k < ids[i + 2]; k++) {
48
+ const unit = { c: ids[i], k: ids[i + 1] + k };
49
+ const u = unitOf(doc, unit);
50
+ if (u !== null)
51
+ add(u.item, u.value, unit, it);
52
+ }
53
+ }
54
+ return out;
55
+ };
56
+ /** The units the engine shows at item `it`, in order: a live record's, those it wins; a live slot item's, all. */
57
+ export const shownBy = (doc, it) => {
58
+ const via = isRecord(it) ? it : null;
59
+ return it.deleted ? [] : unitsOf(doc, it, (item) => item.mover === via);
60
+ };
61
+ /** The displayed entries of `node`'s sequence, in order. */
62
+ export const entriesOf = (doc, node) => {
63
+ const out = [];
64
+ for (let it = firstItem(node); it !== null; it = it.right)
65
+ for (const e of shownBy(doc, it))
66
+ out.push(e);
67
+ return out;
68
+ };
69
+ /**
70
+ * The entry unit `home` displays as: at home when no live record wins it,
71
+ * else at its mover (`via`, `at`); `null` when it shows nowhere (gone).
72
+ */
73
+ export const anchorOf = (doc, home) => {
74
+ const u = unitOf(doc, home);
75
+ if (u === null || u.item.gone === true)
76
+ return null;
77
+ const r = u.item.mover ?? null;
78
+ if (r === null || r.deleted)
79
+ return { value: u.value, unit: home, via: null, at: 0 };
80
+ return shownBy(doc, r).find((e) => sameUnit(e.unit, home)) ?? null;
81
+ };
82
+ /** The sequence node that holds `e`'s displaying item (its record's, or its slot's). */
83
+ export const nodeOf = (doc, e) => (e.via !== null ? e.via.parent : unitOf(doc, e.unit).item.parent);
84
+ /** The live index of the item that displays `e` (its record, or its slot unit). */
85
+ const liveIndexOf = (doc, e) => {
86
+ const id = e.via === null ? Y.createID(e.unit.c, e.unit.k) : Y.createID(e.via.id.client, e.via.id.clock);
87
+ return Y.indexOfId(asYDoc(doc), id).index;
88
+ };
89
+ /** `units` as `client, clock, len` triples (one unit each). */
90
+ export const triples = (units) => units.flatMap((u) => [u.c, u.k, 1]);
91
+ /**
92
+ * Write new slots (`payloads`) and/or a move of `moved` (unit triples) into
93
+ * `node`'s sequence right after the displayed entry `left` (at the start when
94
+ * `null`; `right` the entry that follows, if any). When `left` is a unit in
95
+ * the middle of a record's display, the record written here carries that
96
+ * record's later units too, so the new entries show between them. `side` is
97
+ * the record's class among the items of its origin (`0` after, `1` before);
98
+ * `chain` the units the record stands at for the engine's sort of two
99
+ * records at one origin, given its first unit — the home slots from the root
100
+ * sequence down to the unit it stands at (`null`: the first unit alone) — and
101
+ * `part` its order among the records standing there. `shown` reads the units
102
+ * a record shows, as the reader sees them (the index's: a unit a cycle sent
103
+ * back elsewhere is not carried).
104
+ */
105
+ export const placeAfter = (doc, node, left, right, payloads, moved = null, side = 0, part = 0, chain = null, shown = (r) => shownBy(doc, r)) => {
106
+ let index;
107
+ let carry = [];
108
+ if (left === null)
109
+ index = right === null ? node.length : liveIndexOf(doc, right);
110
+ else {
111
+ if (left.via !== null)
112
+ carry = shown(left.via)
113
+ .slice(left.at + 1)
114
+ .map((e) => e.unit);
115
+ // A unit shown at home though its item moved away (a cycle sent it back)
116
+ // is no live unit: the gap after it starts at its live index.
117
+ const live = left.via !== null || unitOf(doc, left.unit)?.item.deleted !== true;
118
+ index = liveIndexOf(doc, left) + (live ? 1 : 0);
119
+ }
120
+ const units = moved === null ? [] : [...moved];
121
+ if (payloads !== null && payloads.length > 0) {
122
+ // The new slots' first unit is this writer's next clock.
123
+ if (carry.length > 0)
124
+ units.push(doc.clientID, storeOf(doc).getClock(doc.clientID), payloads.length);
125
+ node.insert(index, payloads);
126
+ }
127
+ // A carried unit that moves here anyway is named once.
128
+ const named = new Set();
129
+ for (let i = 0; i < units.length; i += 3)
130
+ for (let k = 0; k < units[i + 2]; k++)
131
+ named.add(`${units[i]}:${units[i + 1] + k}`);
132
+ for (const u of carry)
133
+ if (!named.has(`${u.c}:${u.k}`))
134
+ units.push(u.c, u.k, 1);
135
+ if (units.length > 0 && (moved !== null || carry.length > 0)) {
136
+ const first = { c: units[0], k: units[1] };
137
+ const at = chain === null ? null : chain(first);
138
+ // A chain of the first unit alone is the engine's default: not written.
139
+ const own = at !== null && at.length === 1 && sameUnit(at[0], first);
140
+ node.move(index, units, side, part, at === null || own ? null : at.map((u) => Y.createID(u.c, u.k)));
141
+ }
142
+ };
143
+ /** Remove unit `u` from its sequence (the engine's delete: it shows nowhere any more). */
144
+ export const removeUnit = (doc, u) => {
145
+ Y.transact(asYDoc(doc), (tr) => {
146
+ const it = Y.getItemCleanStart(tr, Y.createID(u.c, u.k));
147
+ if (it.length > 1)
148
+ Y.getItemCleanStart(tr, Y.createID(u.c, u.k + 1));
149
+ if (!it.removed)
150
+ ops.remove(tr, it);
151
+ });
152
+ };
@@ -29,7 +29,7 @@ export { SCHEMA_VERSION, META_KEY } from './edytor-doc.js';
29
29
  export { GENERATION, generationWord, frame, PROTOCOL_VERSION, GENERATION_RECORD, GenerationMismatchError, readProtocolVersion, type GenerationRecord } from './protocols/envelope.js';
30
30
  export { messageSync, messageAwareness, messageAuth, messageQueryAwareness, messageSaved, messageChunk, messageComments, MAX_FRAME_BYTES, chunkFrame, createChunkReader, ChunkError, type ChunkReader, CLOSE } from './providers/room.js';
31
31
  export { messageYjsSyncStep1, messageYjsSyncStep2, messageYjsUpdate, type SyncProtocol } from './protocols/sync.js';
32
- export { messagePermissionDenied, messageReadOnly, writePermissionDenied, writeReadOnly } from './protocols/auth.js';
32
+ export { messageAccess, messagePermissionDenied, messageReadOnly, writeAccess, writePermissionDenied, writeReadOnly } from './protocols/auth.js';
33
33
  export { commentsSubscribe, commentsSnapshot, commentsChange, writeCommentsSubscribe, writeCommentsSnapshot, writeCommentsChange, readCommentsMessage, type CommentChange, type CommentMessage, type CommentSnapshot, type CommentThread, type ThreadComment } from './protocols/comments.js';
34
34
  export { applyAwarenessUpdate, encodeAwarenessUpdate, readAwarenessEntries, writeAwarenessEntries, type AwarenessEntry } from './protocols/awareness.js';
35
35
  export { createDecoder, readVarUint, readVarUint8Array, type Decoder } from 'lib0-v14/decoding';
@@ -32,7 +32,7 @@ export { GENERATION, generationWord, frame, PROTOCOL_VERSION, GENERATION_RECORD,
32
32
  // ── Message types, acknowledgements and chunks ─────────────────────────
33
33
  export { messageSync, messageAwareness, messageAuth, messageQueryAwareness, messageSaved, messageChunk, messageComments, MAX_FRAME_BYTES, chunkFrame, createChunkReader, ChunkError, CLOSE } from './providers/room.js';
34
34
  export { messageYjsSyncStep1, messageYjsSyncStep2, messageYjsUpdate } from './protocols/sync.js';
35
- export { messagePermissionDenied, messageReadOnly, writePermissionDenied, writeReadOnly } from './protocols/auth.js';
35
+ export { messageAccess, messagePermissionDenied, messageReadOnly, writeAccess, writePermissionDenied, writeReadOnly } from './protocols/auth.js';
36
36
  export { commentsSubscribe, commentsSnapshot, commentsChange, writeCommentsSubscribe, writeCommentsSnapshot, writeCommentsChange, readCommentsMessage } from './protocols/comments.js';
37
37
  export { applyAwarenessUpdate, encodeAwarenessUpdate, readAwarenessEntries, writeAwarenessEntries } from './protocols/awareness.js';
38
38
  // Minimal wire codec for frame bodies (lib0 — the codec the frames are
@@ -14,10 +14,20 @@ import * as decoding from 'lib0-v14/decoding';
14
14
  export declare const messagePermissionDenied = 0;
15
15
  /** The denial of a read-only socket's write: that socket still syncs (not a terminal refusal). */
16
16
  export declare const READ_ONLY_DENIAL = "read-only";
17
+ /** The denial of what a suggest-only socket wrote that is no suggestion's: it still syncs. */
18
+ export declare const SUGGEST_ONLY_DENIAL = "suggest-only";
17
19
  export declare const writePermissionDenied: (encoder: encoding.Encoder, reason: string) => void;
18
20
  /** The socket may read but not write: sent when it joins, before any write is refused. */
19
21
  export declare const messageReadOnly = 1;
20
22
  export declare const writeReadOnly: (encoder: encoding.Encoder) => void;
23
+ /**
24
+ * The socket's access (the string that follows: `write`, `suggest` or
25
+ * `comment`), sent when it joins, before anything else it reads; a
26
+ * `comment` one's after its read-only notice (a `read` one has only the
27
+ * notice).
28
+ */
29
+ export declare const messageAccess = 2;
30
+ export declare const writeAccess: (encoder: encoding.Encoder, access: string) => void;
21
31
  export type PermissionDeniedHandler = (ydoc: unknown, reason: string) => void;
22
32
  /**
23
33
  * Decode one auth payload. Returns the auth message subtype so the caller
@@ -25,4 +35,4 @@ export type PermissionDeniedHandler = (ydoc: unknown, reason: string) => void;
25
35
  * do not speak is protocol skew, and this stack's contract is that skew is
26
36
  * reported, not silently dropped (same rule as unknown sync subtypes).
27
37
  */
28
- export declare const readAuthMessage: (decoder: decoding.Decoder, y: unknown, permissionDeniedHandler: PermissionDeniedHandler, readOnlyHandler?: (ydoc: unknown) => void) => number;
38
+ export declare const readAuthMessage: (decoder: decoding.Decoder, y: unknown, permissionDeniedHandler: PermissionDeniedHandler, readOnlyHandler?: (ydoc: unknown) => void, accessHandler?: (access: string) => void) => number;
@@ -14,6 +14,8 @@ import * as decoding from 'lib0-v14/decoding';
14
14
  export const messagePermissionDenied = 0;
15
15
  /** The denial of a read-only socket's write: that socket still syncs (not a terminal refusal). */
16
16
  export const READ_ONLY_DENIAL = 'read-only';
17
+ /** The denial of what a suggest-only socket wrote that is no suggestion's: it still syncs. */
18
+ export const SUGGEST_ONLY_DENIAL = 'suggest-only';
17
19
  export const writePermissionDenied = (encoder, reason) => {
18
20
  encoding.writeVarUint(encoder, messagePermissionDenied);
19
21
  encoding.writeVarString(encoder, reason);
@@ -23,13 +25,24 @@ export const messageReadOnly = 1;
23
25
  export const writeReadOnly = (encoder) => {
24
26
  encoding.writeVarUint(encoder, messageReadOnly);
25
27
  };
28
+ /**
29
+ * The socket's access (the string that follows: `write`, `suggest` or
30
+ * `comment`), sent when it joins, before anything else it reads; a
31
+ * `comment` one's after its read-only notice (a `read` one has only the
32
+ * notice).
33
+ */
34
+ export const messageAccess = 2;
35
+ export const writeAccess = (encoder, access) => {
36
+ encoding.writeVarUint(encoder, messageAccess);
37
+ encoding.writeVarString(encoder, access);
38
+ };
26
39
  /**
27
40
  * Decode one auth payload. Returns the auth message subtype so the caller
28
41
  * can surface unknown subtypes — a valid envelope carrying an auth type we
29
42
  * do not speak is protocol skew, and this stack's contract is that skew is
30
43
  * reported, not silently dropped (same rule as unknown sync subtypes).
31
44
  */
32
- export const readAuthMessage = (decoder, y, permissionDeniedHandler, readOnlyHandler) => {
45
+ export const readAuthMessage = (decoder, y, permissionDeniedHandler, readOnlyHandler, accessHandler) => {
33
46
  const authMessageType = decoding.readVarUint(decoder);
34
47
  switch (authMessageType) {
35
48
  case messagePermissionDenied:
@@ -38,6 +51,9 @@ export const readAuthMessage = (decoder, y, permissionDeniedHandler, readOnlyHan
38
51
  case messageReadOnly:
39
52
  // Unclaimed, it is returned like any subtype the caller does not speak.
40
53
  readOnlyHandler?.(y);
54
+ break;
55
+ case messageAccess:
56
+ accessHandler?.(decoding.readVarString(decoder));
41
57
  }
42
58
  return authMessageType;
43
59
  };
@@ -67,6 +67,16 @@ export type CommentRequest = {
67
67
  block?: string | null;
68
68
  /** The first comment's id (default: one the store makes). */
69
69
  comment?: string;
70
+ /**
71
+ * The text it is made on, as anchors (`DocAnchor`s, each span's
72
+ * ends): the room writes its anchor mark over them for a user who
73
+ * may only comment (`room.access.comment`); a writer's view writes
74
+ * it itself.
75
+ */
76
+ anchors?: {
77
+ start: unknown;
78
+ end: unknown;
79
+ }[];
70
80
  } | {
71
81
  op: 'reply';
72
82
  thread: string;
@@ -85,11 +95,16 @@ export type CommentRequest = {
85
95
  thread: string;
86
96
  comment?: string;
87
97
  };
88
- /** Who asks: a verified user, read-only or not; a moderator may delete anyone's comment. */
98
+ /**
99
+ * Who asks: a verified user, read-only or not; a moderator may delete
100
+ * anyone's comment; `anchors`: the store writes a new thread's anchor (a
101
+ * user who may only comment).
102
+ */
89
103
  export type CommentActor = {
90
104
  user: string;
91
105
  readOnly?: boolean;
92
106
  moderator?: boolean;
107
+ anchors?: boolean;
93
108
  };
94
109
  /** What one request changed (the room broadcasts it, `onComment` receives it). */
95
110
  export type CommentChange = {
@@ -62,13 +62,21 @@ export const parseCommentRequest = (value) => {
62
62
  return null;
63
63
  if (v.block !== undefined && v.block !== null && typeof v.block !== 'string')
64
64
  return null;
65
+ if (v.anchors !== undefined &&
66
+ (!Array.isArray(v.anchors) ||
67
+ v.anchors.length > 64 ||
68
+ v.anchors.some((a) => typeof a?.start !== 'object' || typeof a?.end !== 'object')))
69
+ return null;
65
70
  return {
66
71
  op: 'add',
67
72
  thread: v.thread,
68
73
  body: v.body,
69
74
  ...(v.quote === undefined ? {} : { quote: v.quote }),
70
75
  ...(v.block === undefined ? {} : { block: v.block }),
71
- ...(v.comment === undefined ? {} : { comment: v.comment })
76
+ ...(v.comment === undefined ? {} : { comment: v.comment }),
77
+ ...(v.anchors === undefined
78
+ ? {}
79
+ : { anchors: v.anchors })
72
80
  };
73
81
  case 'reply':
74
82
  if (typeof v.body !== 'string')
@@ -47,6 +47,8 @@ export type EdytorSyncPayload = {
47
47
  * websocket sync's local store. `document.attachSync` passes it.
48
48
  */
49
49
  attach?: (sync: EdytorSync) => EdytorSyncCleanup | void;
50
+ /** What the room says the document's user may do (`EdytorDocument.access`). */
51
+ access?: (access: 'write' | 'suggest' | 'comment' | 'read') => void;
50
52
  };
51
53
  export type EdytorSyncCleanup = () => void | Promise<void>;
52
54
  export type EdytorSync = ((payload: EdytorSyncPayload) => void | EdytorSyncCleanup) & {
@@ -273,11 +275,11 @@ export declare const bindProviders: (Y: EngineApi) => {
273
275
  destroy(): void;
274
276
  disconnect(): void;
275
277
  connect(): void;
276
- emit<N extends "refused" | "status" | "failed" | "schema-mismatch" | "message-error" | "protocol-mismatch" | "synced" | "connection-close" | "connection-error" | "saved" | "permission-denied" | "expired" | "unreachable">(name: N, args: Parameters<WebsocketProviderEvents[N]>): void;
278
+ emit<N extends "refused" | "status" | "failed" | "schema-mismatch" | "message-error" | "protocol-mismatch" | "synced" | "access" | "connection-close" | "connection-error" | "saved" | "permission-denied" | "expired" | "unreachable">(name: N, args: Parameters<WebsocketProviderEvents[N]>): void;
277
279
  _observers: Map<string, Set<any>>;
278
- on<NAME extends "refused" | "status" | "failed" | "schema-mismatch" | "message-error" | "protocol-mismatch" | "synced" | "connection-close" | "connection-error" | "saved" | "permission-denied" | "expired" | "unreachable">(name: NAME, f: WebsocketProviderEvents[NAME]): WebsocketProviderEvents[NAME];
279
- once<NAME extends "refused" | "status" | "failed" | "schema-mismatch" | "message-error" | "protocol-mismatch" | "synced" | "connection-close" | "connection-error" | "saved" | "permission-denied" | "expired" | "unreachable">(name: NAME, f: WebsocketProviderEvents[NAME]): void;
280
- off<NAME extends "refused" | "status" | "failed" | "schema-mismatch" | "message-error" | "protocol-mismatch" | "synced" | "connection-close" | "connection-error" | "saved" | "permission-denied" | "expired" | "unreachable">(name: NAME, f: WebsocketProviderEvents[NAME]): void;
280
+ on<NAME extends "refused" | "status" | "failed" | "schema-mismatch" | "message-error" | "protocol-mismatch" | "synced" | "access" | "connection-close" | "connection-error" | "saved" | "permission-denied" | "expired" | "unreachable">(name: NAME, f: WebsocketProviderEvents[NAME]): WebsocketProviderEvents[NAME];
281
+ once<NAME extends "refused" | "status" | "failed" | "schema-mismatch" | "message-error" | "protocol-mismatch" | "synced" | "access" | "connection-close" | "connection-error" | "saved" | "permission-denied" | "expired" | "unreachable">(name: NAME, f: WebsocketProviderEvents[NAME]): void;
282
+ off<NAME extends "refused" | "status" | "failed" | "schema-mismatch" | "message-error" | "protocol-mismatch" | "synced" | "access" | "connection-close" | "connection-error" | "saved" | "permission-denied" | "expired" | "unreachable">(name: NAME, f: WebsocketProviderEvents[NAME]): void;
281
283
  };
282
284
  };
283
285
  watchComments: (server: string | null, roomName: string, listener: (message: import("../protocol.js").CommentMessage) => void) => (() => void);
@@ -38,7 +38,7 @@ export const bindProviders = (Y) => {
38
38
  const room = `${serverUrl}/${roomName}`;
39
39
  const persistName = options.persist === false ? undefined : (options.persistName ?? `edytor:${room}`);
40
40
  const sync = (payload) => {
41
- const { doc, awareness, synced, failed, attach, armBound, holdBound } = payload;
41
+ const { doc, awareness, synced, failed, attach, armBound, holdBound, access } = payload;
42
42
  const { params, WebSocketPolyfill, maxBackoffTime, connectTimeout, disableBc, onExpired, maxFrameBytes } = options;
43
43
  const local = persistName !== undefined && typeof indexedDB !== 'undefined'
44
44
  ? localSync(persistName, { disableBc })
@@ -54,6 +54,8 @@ export const bindProviders = (Y) => {
54
54
  });
55
55
  if (onExpired)
56
56
  provider.on('expired', (state) => onExpired(state));
57
+ if (access)
58
+ provider.on('access', access);
57
59
  provider.on('synced', (isSynced) => {
58
60
  if (isSynced)
59
61
  synced(provider);
@@ -139,13 +139,27 @@ export type LifecycleHost = {
139
139
  _leave: () => void;
140
140
  emit?(event: 'failed', args: [unknown, unknown]): void;
141
141
  };
142
+ /** When a subscriber of {@link onPageLeave} runs: a session's last writes, then the transports. */
143
+ export type PageLeavePhase = 'write' | 'transport';
144
+ /**
145
+ * Be told when the page leaves for good: a `pagehide` of a page the
146
+ * back/forward cache keeps nothing of (a kept page may come back as it was),
147
+ * or, in Node, the process's `exit`. One listener for the page; its
148
+ * subscribers run in two phases whatever order they subscribed in — a
149
+ * session's last writes (`write`: a history's release, `hist.release.end`)
150
+ * before the transports' teardown (`transport`: each provider), so those
151
+ * writes go out while the sockets and stores are still open (listener order
152
+ * on the window is registration order in Chromium, capturing or not).
153
+ * Returns the unsubscribe.
154
+ */
155
+ export declare const onPageLeave: (fn: () => void, phase: PageLeavePhase) => (() => void);
142
156
  /**
143
157
  * Install the lifecycle on a provider: `hasSynced`, `whenSynced`, and the
144
158
  * departure announcement. `beforeunload` only announces it (`depart`): an
145
159
  * app's unsaved-changes prompt fires it and the user may stay, so the
146
160
  * provider keeps running and its presence returns at the next renewal.
147
- * Leaving the page (`pagehide` of a page not kept in the back/forward
148
- * cache; Node: `exit`) destroys the provider, whose teardown announces it.
161
+ * Leaving the page for good ({@link onPageLeave}'s `transport` phase)
162
+ * destroys the provider, whose teardown announces it.
149
163
  */
150
164
  export declare const initLifecycle: (host: LifecycleHost, destroy: () => unknown, depart?: () => void) => void;
151
165
  /** The first sync — lifetime. Returns `true` only the first time. */
@@ -45,6 +45,7 @@ import * as encoding from 'lib0-v14/encoding';
45
45
  import * as decoding from 'lib0-v14/decoding';
46
46
  import * as promise from 'lib0-v14/promise';
47
47
  import { applyAwarenessUpdate, encodeAwarenessUpdate } from '../protocols/awareness.js';
48
+ import { callEach } from '../protocols/observable.js';
48
49
  import { BARE_REPLY_LENGTH, frame, GENERATION, writeProtocolVersion } from '../protocols/envelope.js';
49
50
  import { checkSchema, SchemaMismatchError } from '../admission.js';
50
51
  import { asEngineDoc } from '../structs.js';
@@ -205,13 +206,68 @@ export const tabs = {
205
206
  };
206
207
  /** The outbound quarantine: a read-only document does not spread. */
207
208
  export const quarantined = (doc) => checkSchema(asEngineDoc(doc)) !== null;
209
+ /**
210
+ * The departures installed, by the page's `addEventListener` (one per realm
211
+ * in a browser; a harness simulating several pages in one realm gives each
212
+ * its own) or, in Node, by the process.
213
+ */
214
+ const departures = new WeakMap();
215
+ /** The page (Node: the process) leaves for good: the writes first, then the transports, each isolated. */
216
+ const leave = (departure) => {
217
+ for (const phase of ['write', 'transport'])
218
+ callEach(`[edytor] page leave (${phase})`, [...departure.leaving[phase]]);
219
+ };
220
+ /**
221
+ * Be told when the page leaves for good: a `pagehide` of a page the
222
+ * back/forward cache keeps nothing of (a kept page may come back as it was),
223
+ * or, in Node, the process's `exit`. One listener for the page; its
224
+ * subscribers run in two phases whatever order they subscribed in — a
225
+ * session's last writes (`write`: a history's release, `hist.release.end`)
226
+ * before the transports' teardown (`transport`: each provider), so those
227
+ * writes go out while the sockets and stores are still open (listener order
228
+ * on the window is registration order in Chromium, capturing or not).
229
+ * Returns the unsubscribe.
230
+ */
231
+ export const onPageLeave = (fn, phase) => {
232
+ const page = globalThis;
233
+ const proc = globalThis.process ?? {};
234
+ const key = page.addEventListener ?? proc;
235
+ let departure = departures.get(key);
236
+ if (departure === undefined) {
237
+ const created = {
238
+ leaving: { write: new Set(), transport: new Set() },
239
+ uninstall: () => { }
240
+ };
241
+ if (page.addEventListener) {
242
+ const hide = (event) => void (event.persisted || leave(created));
243
+ const remove = page.removeEventListener;
244
+ page.addEventListener('pagehide', hide);
245
+ created.uninstall = () => remove?.call(page, 'pagehide', hide);
246
+ }
247
+ else {
248
+ const exit = () => leave(created);
249
+ proc.on?.('exit', exit);
250
+ created.uninstall = () => proc.off?.('exit', exit);
251
+ }
252
+ departures.set(key, (departure = created));
253
+ }
254
+ const own = departure;
255
+ own.leaving[phase].add(fn);
256
+ return () => {
257
+ own.leaving[phase].delete(fn);
258
+ if (own.leaving.write.size + own.leaving.transport.size === 0 && departures.get(key) === own) {
259
+ own.uninstall();
260
+ departures.delete(key);
261
+ }
262
+ };
263
+ };
208
264
  /**
209
265
  * Install the lifecycle on a provider: `hasSynced`, `whenSynced`, and the
210
266
  * departure announcement. `beforeunload` only announces it (`depart`): an
211
267
  * app's unsaved-changes prompt fires it and the user may stay, so the
212
268
  * provider keeps running and its presence returns at the next renewal.
213
- * Leaving the page (`pagehide` of a page not kept in the back/forward
214
- * cache; Node: `exit`) destroys the provider, whose teardown announces it.
269
+ * Leaving the page for good ({@link onPageLeave}'s `transport` phase)
270
+ * destroys the provider, whose teardown announces it.
215
271
  */
216
272
  export const initLifecycle = (host, destroy, depart = () => { }) => {
217
273
  host.hasSynced = false;
@@ -222,21 +278,12 @@ export const initLifecycle = (host, destroy, depart = () => { }) => {
222
278
  // A consumer that never attaches a catch must not crash the process.
223
279
  host.whenSynced.catch(() => { });
224
280
  const page = globalThis;
225
- if (page.addEventListener) {
226
- const leave = (event) => void (event.persisted || destroy());
227
- page.addEventListener('beforeunload', depart);
228
- page.addEventListener('pagehide', leave);
229
- host._leave = () => {
230
- page.removeEventListener?.('beforeunload', depart);
231
- page.removeEventListener?.('pagehide', leave);
232
- };
233
- }
234
- else {
235
- const leave = () => void destroy();
236
- const proc = globalThis.process ?? {};
237
- proc.on?.('exit', leave);
238
- host._leave = () => proc.off?.('exit', leave);
239
- }
281
+ page.addEventListener?.('beforeunload', depart);
282
+ const off = onPageLeave(() => void destroy(), 'transport');
283
+ host._leave = () => {
284
+ page.removeEventListener?.('beforeunload', depart);
285
+ off();
286
+ };
240
287
  };
241
288
  /** The first sync — lifetime. Returns `true` only the first time. */
242
289
  export const markSynced = (host) => {