edytor 3.0.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/README.md +2 -2
  2. package/dist/cloudflare/DocumentRoom.d.ts +13 -9
  3. package/dist/cloudflare/DocumentRoom.js +10 -6
  4. package/dist/cloudflare/room/context.d.ts +27 -26
  5. package/dist/cloudflare/room/context.js +2 -2
  6. package/dist/cloudflare/room/purge.d.ts +6 -3
  7. package/dist/cloudflare/room/purge.js +44 -19
  8. package/dist/cloudflare/room/updates.d.ts +3 -4
  9. package/dist/cloudflare/room/updates.js +6 -8
  10. package/dist/crdt/attribution/attribution.d.ts +31 -12
  11. package/dist/crdt/attribution/attribution.js +56 -18
  12. package/dist/crdt/attribution/block.d.ts +3 -3
  13. package/dist/crdt/attribution/block.js +7 -8
  14. package/dist/crdt/attribution/index.d.ts +1 -1
  15. package/dist/crdt/attribution/index.js +1 -1
  16. package/dist/crdt/data.d.ts +62 -31
  17. package/dist/crdt/data.js +211 -103
  18. package/dist/crdt/doc/delete.d.ts +1 -1
  19. package/dist/crdt/doc/delete.js +13 -30
  20. package/dist/crdt/doc/funnel.js +29 -24
  21. package/dist/crdt/doc/gate.d.ts +3 -3
  22. package/dist/crdt/doc/history.d.ts +25 -9
  23. package/dist/crdt/doc/history.js +150 -40
  24. package/dist/crdt/doc/layout.js +11 -22
  25. package/dist/crdt/doc/meta.js +12 -17
  26. package/dist/crdt/doc/moves.d.ts +1 -1
  27. package/dist/crdt/doc/moves.js +52 -32
  28. package/dist/crdt/doc/reads.js +2 -2
  29. package/dist/crdt/doc/seed.js +2 -2
  30. package/dist/crdt/doc/split-merge.js +12 -15
  31. package/dist/crdt/doc/steps.d.ts +25 -13
  32. package/dist/crdt/doc/steps.js +52 -122
  33. package/dist/crdt/doc/types.d.ts +21 -9
  34. package/dist/crdt/document.d.ts +16 -5
  35. package/dist/crdt/document.js +21 -4
  36. package/dist/crdt/edytor-doc.d.ts +62 -64
  37. package/dist/crdt/edytor-doc.js +18 -13
  38. package/dist/crdt/engine-api.d.ts +33 -1
  39. package/dist/crdt/engine.d.ts +0 -1
  40. package/dist/crdt/engine.js +1 -4
  41. package/dist/crdt/flow.d.ts +1 -1
  42. package/dist/crdt/flow.js +25 -27
  43. package/dist/crdt/index.d.ts +26 -25
  44. package/dist/crdt/index.js +1 -1
  45. package/dist/crdt/placement/display.d.ts +5 -30
  46. package/dist/crdt/placement/display.js +39 -144
  47. package/dist/crdt/placement/keys.d.ts +58 -0
  48. package/dist/crdt/placement/keys.js +90 -0
  49. package/dist/crdt/placement/model.d.ts +119 -118
  50. package/dist/crdt/placement/model.js +242 -142
  51. package/dist/crdt/placement/resolve.d.ts +123 -35
  52. package/dist/crdt/placement/resolve.js +187 -153
  53. package/dist/crdt/placement/slots.d.ts +134 -0
  54. package/dist/crdt/placement/slots.js +152 -0
  55. package/dist/crdt/providers/room.d.ts +16 -2
  56. package/dist/crdt/providers/room.js +64 -17
  57. package/dist/crdt/providers/websocket.js +5 -3
  58. package/dist/crdt/purge.d.ts +17 -8
  59. package/dist/crdt/purge.js +236 -126
  60. package/dist/crdt/restore.d.ts +2 -2
  61. package/dist/crdt/restore.js +18 -34
  62. package/dist/crdt/schema.d.ts +22 -21
  63. package/dist/crdt/schema.js +29 -23
  64. package/dist/crdt/storage.d.ts +1 -1
  65. package/dist/crdt/storage.js +1 -1
  66. package/dist/crdt/structs.d.ts +15 -15
  67. package/dist/crdt/structs.js +8 -22
  68. package/dist/crdt/text/index/cache.js +4 -0
  69. package/dist/crdt/text/index/claims.js +12 -0
  70. package/dist/crdt/text/index/fold.d.ts +2 -3
  71. package/dist/crdt/text/index/fold.js +194 -44
  72. package/dist/crdt/text/index/layout.js +5 -8
  73. package/dist/crdt/text/index/placement.d.ts +26 -18
  74. package/dist/crdt/text/index/placement.js +404 -138
  75. package/dist/crdt/text/index/records.d.ts +3 -3
  76. package/dist/crdt/text/index/records.js +24 -31
  77. package/dist/crdt/text/index/shared.d.ts +11 -3
  78. package/dist/crdt/text/index/shared.js +12 -3
  79. package/dist/crdt/text/index/state.d.ts +21 -17
  80. package/dist/crdt/text/index/state.js +15 -17
  81. package/dist/crdt/text/index/streams.d.ts +10 -5
  82. package/dist/crdt/text/index/streams.js +78 -27
  83. package/dist/crdt/text/items.d.ts +10 -3
  84. package/dist/crdt/text/items.js +7 -0
  85. package/dist/crdt/text/model.d.ts +77 -62
  86. package/dist/crdt/text/model.js +84 -169
  87. package/dist/crdt/text/ranges.d.ts +1 -1
  88. package/dist/crdt/text/ranges.js +3 -3
  89. package/dist/crdt/text/rows.d.ts +13 -7
  90. package/dist/crdt/text/rows.js +24 -7
  91. package/dist/crdt/text/runs.d.ts +17 -12
  92. package/dist/crdt/text/runs.js +24 -7
  93. package/dist/crdt/text/streams.d.ts +26 -13
  94. package/dist/crdt/text/streams.js +41 -14
  95. package/dist/crdt/vendor/yjs/UPSTREAM.md +568 -21
  96. package/dist/crdt/vendor/yjs/dts/global.d.ts +2 -0
  97. package/dist/crdt/vendor/yjs/dts/index.d.ts +1 -1
  98. package/dist/crdt/vendor/yjs/dts/structs/AbstractStruct.d.ts +7 -0
  99. package/dist/crdt/vendor/yjs/dts/structs/Item.d.ts +194 -10
  100. package/dist/crdt/vendor/yjs/dts/utils/Doc.d.ts +6 -3
  101. package/dist/crdt/vendor/yjs/dts/utils/StructStore.d.ts +5 -18
  102. package/dist/crdt/vendor/yjs/dts/utils/Transaction.d.ts +15 -1
  103. package/dist/crdt/vendor/yjs/dts/utils/UpdateDecoder.d.ts +14 -6
  104. package/dist/crdt/vendor/yjs/dts/utils/UpdateEncoder.d.ts +33 -6
  105. package/dist/crdt/vendor/yjs/dts/utils/ids.d.ts +1 -65
  106. package/dist/crdt/vendor/yjs/dts/utils/marks.d.ts +6 -1
  107. package/dist/crdt/vendor/yjs/dts/ynode.d.ts +43 -7
  108. package/dist/crdt/vendor/yjs/global.d.ts +2 -0
  109. package/dist/crdt/vendor/yjs/src/index.js +1 -1
  110. package/dist/crdt/vendor/yjs/src/structs/AbstractStruct.js +10 -0
  111. package/dist/crdt/vendor/yjs/src/structs/Item.js +610 -51
  112. package/dist/crdt/vendor/yjs/src/utils/BlockSet.js +18 -9
  113. package/dist/crdt/vendor/yjs/src/utils/Doc.js +5 -3
  114. package/dist/crdt/vendor/yjs/src/utils/StructStore.js +5 -0
  115. package/dist/crdt/vendor/yjs/src/utils/Transaction.js +36 -13
  116. package/dist/crdt/vendor/yjs/src/utils/UpdateDecoder.js +88 -18
  117. package/dist/crdt/vendor/yjs/src/utils/UpdateEncoder.js +119 -32
  118. package/dist/crdt/vendor/yjs/src/utils/encoding.js +13 -1
  119. package/dist/crdt/vendor/yjs/src/utils/ids.js +7 -7
  120. package/dist/crdt/vendor/yjs/src/utils/marks.js +193 -8
  121. package/dist/crdt/vendor/yjs/src/utils/redo.js +1 -1
  122. package/dist/crdt/vendor/yjs/src/utils/transaction-helpers.js +1 -1
  123. package/dist/crdt/vendor/yjs/src/utils/updates.js +3 -3
  124. package/dist/crdt/vendor/yjs/src/ynode.js +177 -12
  125. package/dist/edytor.svelte.js +4 -1
  126. package/dist/events/beforeInputCommands.js +2 -2
  127. package/dist/events/onPageHide.d.ts +3 -0
  128. package/dist/events/onPageHide.js +16 -0
  129. package/dist/export/forms.d.ts +25 -25
  130. package/dist/surface/observer.svelte.js +2 -2
  131. package/dist/surface/projector.svelte.d.ts +15 -3
  132. package/dist/surface/projector.svelte.js +59 -8
  133. package/package.json +2 -2
  134. package/dist/crdt/placement/rank.d.ts +0 -87
  135. package/dist/crdt/placement/rank.js +0 -332
  136. package/dist/crdt/placement/source.d.ts +0 -43
  137. package/dist/crdt/placement/source.js +0 -66
  138. package/dist/crdt/text/delete-index.d.ts +0 -129
  139. package/dist/crdt/text/delete-index.js +0 -365
  140. package/dist/crdt/text/deletes.d.ts +0 -76
  141. package/dist/crdt/text/deletes.js +0 -362
  142. package/dist/crdt/text/index/anchored.d.ts +0 -17
  143. package/dist/crdt/text/index/anchored.js +0 -197
package/README.md CHANGED
@@ -17,7 +17,7 @@
17
17
 
18
18
  Edytor aims to be for Svelte what Slate.js is for React: a heavily customizable editor with an API to build any kind of collaborative rich text editor.
19
19
 
20
- > **3.0.** Edytor `3.0.0` is on npm as `edytor`: its API follows semantic versioning, and every change is listed in the changelog. It has no production track record yet: read the [limitations](https://edytor.dev/docs/reference/limitations) before you build on it. Issues and PRs are welcome: when you report a bug, include the document's JSON value.
20
+ > **4.0.** Edytor `4.0.0` is on npm as `edytor`: its API follows semantic versioning, and every change is listed in the changelog. It has no production track record yet: read the [limitations](https://edytor.dev/docs/reference/limitations) before you build on it. Issues and PRs are welcome: when you report a bug, include the document's JSON value.
21
21
 
22
22
  ## Features
23
23
 
@@ -40,7 +40,7 @@ Edytor aims to be for Svelte what Slate.js is for React: a heavily customizable
40
40
 
41
41
  ## Install
42
42
 
43
- Edytor `3.0.0` is on npm under `latest`:
43
+ Edytor `4.0.0` is on npm under `latest`:
44
44
 
45
45
  ```bash
46
46
  pnpm add edytor
@@ -121,9 +121,11 @@ export declare const DEFAULT_SAVE_AFTER = 2000;
121
121
  * Room quotas, each refused with `4413` (`quota: <name>`). The
122
122
  * document's size: what its records hold (uncompressed) and the updates the
123
123
  * engine holds waiting. An isolate has 128 MB, and the live document takes
124
- * 18 to 46 bytes of heap per stored byte (prose to one short line per
125
- * block, its index included; `bench/room-memory.mjs`): 2 MiB is
126
- * about 100 MB at worst. Raise it for documents of long text.
124
+ * 15 to 77 bytes of heap per stored byte in Node (prose to one short line
125
+ * per block, its index included; `bench/room-memory.mjs`; a Worker takes
126
+ * less): 2 MiB is about 150 to 160 MB at worst, a list of some 23,000 short
127
+ * blocks (1.25 MiB keeps that near 100 MB). Raise it for documents of long
128
+ * text, lower it for long lists of short blocks.
127
129
  */
128
130
  export declare const DEFAULT_MAX_DOCUMENT_BYTES: number;
129
131
  /** The share of the document quota past which the room logs its size, by default. */
@@ -364,7 +366,7 @@ export type RoomLogEntry = {
364
366
  user: string;
365
367
  } | ({
366
368
  edytor: 'purge';
367
- horizon: number;
369
+ horizon: number | null;
368
370
  bytes: number;
369
371
  } & PurgeReport) | {
370
372
  edytor: 'quota';
@@ -801,12 +803,14 @@ export declare class AttachedDocument {
801
803
  * The purge task, also over RPC (`room.purge.timing`): record an epoch
802
804
  * (the state vector, now) when the document changed since the last one,
803
805
  * purge what was deleted before the horizon — the newest epoch at least
804
- * `purgeAfterDays` old — when it is newer than the last purged, and
805
- * re-arm for the next epoch to pass it. Returns what the purge wrote, or
806
- * `null` when nothing was past the horizon.
806
+ * `purgeAfterDays` old — when it is newer than the last purged, with what
807
+ * the sessions whose history ended deleted (`hist.release.end`; with no
808
+ * epoch due, those alone, `horizon: null`), and re-arm for the next epoch
809
+ * to pass it. Returns what the purge wrote, or `null` when nothing was
810
+ * past the horizon and no session had ended.
807
811
  */
808
812
  purge(): (PurgeReport & {
809
- horizon: number;
813
+ horizon: number | null;
810
814
  }) | null;
811
815
  /** The room's counters, also over RPC — see {@link RoomMetrics}. */
812
816
  metrics(): RoomMetrics;
@@ -968,7 +972,7 @@ export declare class DocumentRoom<Env = DocumentRoomEnv> extends DurableObject<E
968
972
  };
969
973
  /** Run the purge task now, also over RPC — see {@link AttachedDocument.purge}. */
970
974
  purge(): (PurgeReport & {
971
- horizon: number;
975
+ horizon: number | null;
972
976
  }) | null;
973
977
  /** Every comment thread, also over RPC — see {@link AttachedDocument.listComments}. */
974
978
  listComments(): CommentSnapshot;
@@ -130,9 +130,11 @@ export const DEFAULT_SAVE_AFTER = 2000;
130
130
  * Room quotas, each refused with `4413` (`quota: <name>`). The
131
131
  * document's size: what its records hold (uncompressed) and the updates the
132
132
  * engine holds waiting. An isolate has 128 MB, and the live document takes
133
- * 18 to 46 bytes of heap per stored byte (prose to one short line per
134
- * block, its index included; `bench/room-memory.mjs`): 2 MiB is
135
- * about 100 MB at worst. Raise it for documents of long text.
133
+ * 15 to 77 bytes of heap per stored byte in Node (prose to one short line
134
+ * per block, its index included; `bench/room-memory.mjs`; a Worker takes
135
+ * less): 2 MiB is about 150 to 160 MB at worst, a list of some 23,000 short
136
+ * blocks (1.25 MiB keeps that near 100 MB). Raise it for documents of long
137
+ * text, lower it for long lists of short blocks.
136
138
  */
137
139
  export const DEFAULT_MAX_DOCUMENT_BYTES = 2 * 1024 * 1024;
138
140
  /** The share of the document quota past which the room logs its size, by default. */
@@ -480,9 +482,11 @@ export class AttachedDocument {
480
482
  * The purge task, also over RPC (`room.purge.timing`): record an epoch
481
483
  * (the state vector, now) when the document changed since the last one,
482
484
  * purge what was deleted before the horizon — the newest epoch at least
483
- * `purgeAfterDays` old — when it is newer than the last purged, and
484
- * re-arm for the next epoch to pass it. Returns what the purge wrote, or
485
- * `null` when nothing was past the horizon.
485
+ * `purgeAfterDays` old — when it is newer than the last purged, with what
486
+ * the sessions whose history ended deleted (`hist.release.end`; with no
487
+ * epoch due, those alone, `horizon: null`), and re-arm for the next epoch
488
+ * to pass it. Returns what the purge wrote, or `null` when nothing was
489
+ * past the horizon and no session had ended.
486
490
  */
487
491
  purge() {
488
492
  return this.room.purge.tick();
@@ -28,7 +28,7 @@ export declare const crdt: {
28
28
  Awareness: typeof import("../../index.js").Awareness;
29
29
  doc: {
30
30
  SCHEMA: {
31
- readonly version: 6;
31
+ readonly version: 10;
32
32
  readonly name: "edytor-doc";
33
33
  readonly roots: {
34
34
  readonly registry: "blocks";
@@ -39,7 +39,7 @@ export declare const crdt: {
39
39
  readonly schema: "schema";
40
40
  };
41
41
  };
42
- SCHEMA_VERSION: 6;
42
+ SCHEMA_VERSION: 10;
43
43
  SCHEMA_NAME: "edytor-doc";
44
44
  META_KEY: "meta";
45
45
  init: (doc: E.EngineDoc, opts?: {
@@ -103,29 +103,30 @@ export declare const crdt: {
103
103
  blockNodeOf: (doc: E.EngineDoc, id: import("../../index.js").BlockId) => E.EngineNode | null;
104
104
  liveNodeOf: (doc: E.EngineDoc, id: import("../../index.js").BlockId) => E.EngineNode | null;
105
105
  isLive: (doc: E.EngineDoc, id: import("../../index.js").BlockId) => boolean;
106
- candidatesOf: (node: E.EngineNode) => import("../../crdt/placement/model.js").PlacementCand[];
107
106
  view: (doc: E.EngineDoc) => import("../../crdt/placement/model.js").ModelView;
108
- resolvePlacements: (blocks: Map<import("../../index.js").BlockId, import("../../crdt/placement/model.js").BlockRec>, ownerOf?: (b: import("../../index.js").BlockId) => import("../../crdt/text/model.js").Owner) => Map<import("../../index.js").BlockId, import("../../crdt/placement/model.js").ResolvedPlacement>;
109
107
  childrenIndex: (placements: Map<import("../../index.js").BlockId, import("../../crdt/placement/model.js").ResolvedPlacement>, own: import("../../crdt/placement/model.js").DisplayOwnership) => Map<import("../../index.js").BlockId | null, import("../../crdt/placement/model.js").ChildSlot[]>;
110
108
  positionInView: (v: import("../../crdt/placement/model.js").ModelView, id: import("../../index.js").BlockId) => import("../../index.js").Destination | null;
111
109
  buildInline: (atom: import("../../index.js").InlineSpec) => E.EngineNode;
112
110
  isSelfOrDescendant: (placements: Map<import("../../index.js").BlockId, import("../../crdt/placement/model.js").ResolvedPlacement>, own: import("../../crdt/text/model.js").Ownership, id: import("../../index.js").BlockId, maybeAncestor: import("../../index.js").BlockId) => boolean;
113
- ranksAt: (siblings: readonly {
114
- rank: string;
115
- }[], index: number, count: number, clientId: number, rand?: () => number, run?: boolean) => string[];
116
- writePlacement: (doc: E.EngineDoc, node: E.EngineNode, p: import("../../index.js").BlockId | null, r: string) => void;
111
+ placeBlocks: (doc: E.EngineDoc, v: import("../../crdt/placement/model.js").ModelView, parent: import("../../index.js").BlockId | null, left: import("../../index.js").BlockId | null, right: import("../../index.js").BlockId | null, slots: readonly import("../../crdt/placement/slots.js").BlockSlot[] | null, moved?: readonly number[] | null, side?: number, part?: number, at?: import("../../crdt/placement/slots.js").Unit | null) => void;
112
+ moveBlocksTo: (doc: E.EngineDoc, v: import("../../crdt/placement/model.js").ModelView, ids: readonly import("../../index.js").BlockId[], parent: import("../../index.js").BlockId | null, index: number, side?: number, part?: number, at?: import("../../index.js").BlockId | null) => void;
113
+ insertSpecsAt: (doc: E.EngineDoc, v: import("../../crdt/placement/model.js").ModelView, parent: import("../../index.js").BlockId | null, index: number, specs: readonly import("../../index.js").BlockSpec[], split?: import("../../crdt/placement/slots.js").Unit | null) => void;
117
114
  withdrawOnUndo: (doc: E.EngineDoc) => (item: E.EngineItemRef, stackItem: {
118
115
  inserts: {
119
116
  has(client: number, clock: number): boolean;
120
117
  };
121
118
  }) => boolean;
122
- materializeSpec: (doc: E.EngineDoc, sp: import("../../index.js").BlockSpec, parent: import("../../index.js").BlockId | null, rank: string) => void;
123
119
  collides: (doc: E.EngineDoc, specs: readonly import("../../index.js").BlockSpec[]) => boolean;
124
- writeSplit: (doc: E.EngineDoc, id: import("../../index.js").BlockId, offset: number, newId: import("../../index.js").BlockId, tail: import("../../index.js").SplitTail, place: import("../../crdt/placement/model.js").PlacementValue) => void;
120
+ writeSplit: (doc: E.EngineDoc, id: import("../../index.js").BlockId, offset: number, newId: import("../../index.js").BlockId, tail: import("../../index.js").SplitTail, parent: import("../../index.js").BlockId | null, index: number) => import("../../crdt/placement/slots.js").Unit;
125
121
  ownText: (doc: E.EngineDoc, id: import("../../index.js").BlockId) => {
126
122
  from: unknown;
127
123
  to: number;
128
124
  };
125
+ recordsOf: (doc: E.EngineDoc, node: E.EngineNode, v?: import("../../crdt/placement/model.js").ModelView) => {
126
+ item: import("../../crdt/placement/slots.js").SeqItem;
127
+ stamp: import("../../crdt/placement/slots.js").Stamp;
128
+ wins: boolean;
129
+ }[];
129
130
  insertBlocks: (doc: E.EngineDoc, dest: import("../../index.js").Destination, specs: import("../../index.js").BlockSpec[]) => boolean;
130
131
  insertBlock: (doc: E.EngineDoc, dest: import("../../index.js").Destination, spec: import("../../index.js").BlockSpec) => boolean;
131
132
  restoreBlocks: (doc: E.EngineDoc, specs: import("../../index.js").BlockSpec[]) => void;
@@ -157,8 +158,8 @@ export declare const crdt: {
157
158
  at: number;
158
159
  node: E.EngineNode;
159
160
  } | null;
160
- splitAt: (blocks: ReadonlyMap<import("../../crdt/text/model.js").BlockId, import("../../crdt/text/model.js").TextBlockRec>, own: import("../../crdt/text/model.js").Ownership, b: import("../../crdt/text/model.js").BlockId, offset: number, newId: import("../../crdt/text/model.js").BlockId, n: number) => import("../../crdt/text/model.js").MergeClaim[];
161
- claimInto: (blocks: ReadonlyMap<import("../../crdt/text/model.js").BlockId, import("../../crdt/text/model.js").TextBlockRec>, own: Pick<import("../../crdt/text/model.js").Ownership, "streamOf">, from: import("../../crdt/text/model.js").BlockId, into: import("../../crdt/text/model.js").BlockId) => void;
161
+ splitAt: (doc: E.EngineDoc, blocks: ReadonlyMap<import("../../crdt/text/model.js").BlockId, import("../../crdt/text/model.js").TextBlockRec>, own: import("../../crdt/text/model.js").Ownership, b: import("../../crdt/text/model.js").BlockId, offset: number, newId: import("../../crdt/text/model.js").BlockId, n: number) => void;
162
+ claimInto: (own: Pick<import("../../crdt/text/model.js").Ownership, "streamOf">, from: import("../../crdt/text/model.js").BlockId, into: import("../../crdt/text/model.js").BlockId) => void;
162
163
  ownText: (doc: E.EngineDoc, rec: import("../../crdt/text/model.js").TextBlockRec) => {
163
164
  from: unknown;
164
165
  to: number;
@@ -178,7 +179,7 @@ export declare const crdt: {
178
179
  keep?: boolean;
179
180
  after?: readonly import("../../index.js").BlockSpec[];
180
181
  } | undefined) => import("../../index.js").OpResult;
181
- placeBeside: (ids: readonly string[], target: string, side: "right" | "left", kind?: string | undefined) => import("../../index.js").OpResult;
182
+ placeBeside: (ids: readonly string[], target: string, side: "left" | "right", kind?: string | undefined) => import("../../index.js").OpResult;
182
183
  wrapInLayout: (ids: readonly string[], kind?: string | undefined, columns?: number | undefined) => import("../../index.js").OpResult;
183
184
  splitBlock: (id: string, offset: number, newId: string, tail?: import("../../index.js").SplitTail | undefined) => import("../../index.js").OpResult;
184
185
  mergeBlocks: (fromId: string, intoId: string) => import("../../index.js").OpResult;
@@ -301,11 +302,10 @@ export declare const crdt: {
301
302
  onChange: (cb: (change: import("../../index.js").DocChange) => void) => (() => void);
302
303
  };
303
304
  newDoc: (opts?: ConstructorParameters<E.EngineApi["Doc"]>[0]) => YDoc;
304
- keepCopies: (doc: E.EngineDoc) => void;
305
305
  purge: (doc: E.EngineDoc, facade: {
306
306
  model: import("../../crdt/placement/model.js").PlacementModel;
307
307
  text: import("../../crdt/text/model.js").TextEngine;
308
- }, horizon: import("../../crdt/purge.js").Horizon) => import("../index.js").PurgeReport;
308
+ }, horizon: import("../../crdt/purge.js").Horizon | null) => import("../index.js").PurgeReport;
309
309
  horizonOf: (doc: E.EngineDoc) => import("../../crdt/purge.js").Horizon | null;
310
310
  restoreTo: <Plan>(doc: E.EngineDoc, facade: import("../../crdt/restore.js").RestoreFacade<Plan>, json: JSONDoc) => import("../../crdt/restore.js").RestoreReport;
311
311
  model: {
@@ -314,29 +314,30 @@ export declare const crdt: {
314
314
  blockNodeOf: (doc: E.EngineDoc, id: import("../../index.js").BlockId) => E.EngineNode | null;
315
315
  liveNodeOf: (doc: E.EngineDoc, id: import("../../index.js").BlockId) => E.EngineNode | null;
316
316
  isLive: (doc: E.EngineDoc, id: import("../../index.js").BlockId) => boolean;
317
- candidatesOf: (node: E.EngineNode) => import("../../crdt/placement/model.js").PlacementCand[];
318
317
  view: (doc: E.EngineDoc) => import("../../crdt/placement/model.js").ModelView;
319
- resolvePlacements: (blocks: Map<import("../../index.js").BlockId, import("../../crdt/placement/model.js").BlockRec>, ownerOf?: (b: import("../../index.js").BlockId) => import("../../crdt/text/model.js").Owner) => Map<import("../../index.js").BlockId, import("../../crdt/placement/model.js").ResolvedPlacement>;
320
318
  childrenIndex: (placements: Map<import("../../index.js").BlockId, import("../../crdt/placement/model.js").ResolvedPlacement>, own: import("../../crdt/placement/model.js").DisplayOwnership) => Map<import("../../index.js").BlockId | null, import("../../crdt/placement/model.js").ChildSlot[]>;
321
319
  positionInView: (v: import("../../crdt/placement/model.js").ModelView, id: import("../../index.js").BlockId) => import("../../index.js").Destination | null;
322
320
  buildInline: (atom: import("../../index.js").InlineSpec) => E.EngineNode;
323
321
  isSelfOrDescendant: (placements: Map<import("../../index.js").BlockId, import("../../crdt/placement/model.js").ResolvedPlacement>, own: import("../../crdt/text/model.js").Ownership, id: import("../../index.js").BlockId, maybeAncestor: import("../../index.js").BlockId) => boolean;
324
- ranksAt: (siblings: readonly {
325
- rank: string;
326
- }[], index: number, count: number, clientId: number, rand?: () => number, run?: boolean) => string[];
327
- writePlacement: (doc: E.EngineDoc, node: E.EngineNode, p: import("../../index.js").BlockId | null, r: string) => void;
322
+ placeBlocks: (doc: E.EngineDoc, v: import("../../crdt/placement/model.js").ModelView, parent: import("../../index.js").BlockId | null, left: import("../../index.js").BlockId | null, right: import("../../index.js").BlockId | null, slots: readonly import("../../crdt/placement/slots.js").BlockSlot[] | null, moved?: readonly number[] | null, side?: number, part?: number, at?: import("../../crdt/placement/slots.js").Unit | null) => void;
323
+ moveBlocksTo: (doc: E.EngineDoc, v: import("../../crdt/placement/model.js").ModelView, ids: readonly import("../../index.js").BlockId[], parent: import("../../index.js").BlockId | null, index: number, side?: number, part?: number, at?: import("../../index.js").BlockId | null) => void;
324
+ insertSpecsAt: (doc: E.EngineDoc, v: import("../../crdt/placement/model.js").ModelView, parent: import("../../index.js").BlockId | null, index: number, specs: readonly import("../../index.js").BlockSpec[], split?: import("../../crdt/placement/slots.js").Unit | null) => void;
328
325
  withdrawOnUndo: (doc: E.EngineDoc) => (item: E.EngineItemRef, stackItem: {
329
326
  inserts: {
330
327
  has(client: number, clock: number): boolean;
331
328
  };
332
329
  }) => boolean;
333
- materializeSpec: (doc: E.EngineDoc, sp: import("../../index.js").BlockSpec, parent: import("../../index.js").BlockId | null, rank: string) => void;
334
330
  collides: (doc: E.EngineDoc, specs: readonly import("../../index.js").BlockSpec[]) => boolean;
335
- writeSplit: (doc: E.EngineDoc, id: import("../../index.js").BlockId, offset: number, newId: import("../../index.js").BlockId, tail: import("../../index.js").SplitTail, place: import("../../crdt/placement/model.js").PlacementValue) => void;
331
+ writeSplit: (doc: E.EngineDoc, id: import("../../index.js").BlockId, offset: number, newId: import("../../index.js").BlockId, tail: import("../../index.js").SplitTail, parent: import("../../index.js").BlockId | null, index: number) => import("../../crdt/placement/slots.js").Unit;
336
332
  ownText: (doc: E.EngineDoc, id: import("../../index.js").BlockId) => {
337
333
  from: unknown;
338
334
  to: number;
339
335
  };
336
+ recordsOf: (doc: E.EngineDoc, node: E.EngineNode, v?: import("../../crdt/placement/model.js").ModelView) => {
337
+ item: import("../../crdt/placement/slots.js").SeqItem;
338
+ stamp: import("../../crdt/placement/slots.js").Stamp;
339
+ wins: boolean;
340
+ }[];
340
341
  insertBlocks: (doc: E.EngineDoc, dest: import("../../index.js").Destination, specs: import("../../index.js").BlockSpec[]) => boolean;
341
342
  insertBlock: (doc: E.EngineDoc, dest: import("../../index.js").Destination, spec: import("../../index.js").BlockSpec) => boolean;
342
343
  restoreBlocks: (doc: E.EngineDoc, specs: import("../../index.js").BlockSpec[]) => void;
@@ -368,8 +369,8 @@ export declare const crdt: {
368
369
  at: number;
369
370
  node: E.EngineNode;
370
371
  } | null;
371
- splitAt: (blocks: ReadonlyMap<import("../../crdt/text/model.js").BlockId, import("../../crdt/text/model.js").TextBlockRec>, own: import("../../crdt/text/model.js").Ownership, b: import("../../crdt/text/model.js").BlockId, offset: number, newId: import("../../crdt/text/model.js").BlockId, n: number) => import("../../crdt/text/model.js").MergeClaim[];
372
- claimInto: (blocks: ReadonlyMap<import("../../crdt/text/model.js").BlockId, import("../../crdt/text/model.js").TextBlockRec>, own: Pick<import("../../crdt/text/model.js").Ownership, "streamOf">, from: import("../../crdt/text/model.js").BlockId, into: import("../../crdt/text/model.js").BlockId) => void;
372
+ splitAt: (doc: E.EngineDoc, blocks: ReadonlyMap<import("../../crdt/text/model.js").BlockId, import("../../crdt/text/model.js").TextBlockRec>, own: import("../../crdt/text/model.js").Ownership, b: import("../../crdt/text/model.js").BlockId, offset: number, newId: import("../../crdt/text/model.js").BlockId, n: number) => void;
373
+ claimInto: (own: Pick<import("../../crdt/text/model.js").Ownership, "streamOf">, from: import("../../crdt/text/model.js").BlockId, into: import("../../crdt/text/model.js").BlockId) => void;
373
374
  ownText: (doc: E.EngineDoc, rec: import("../../crdt/text/model.js").TextBlockRec) => {
374
375
  from: unknown;
375
376
  to: number;
@@ -691,9 +692,9 @@ export declare class RoomContext {
691
692
  emptied: number;
692
693
  marks: number;
693
694
  records: number;
694
- candidates: number;
695
695
  claims: number;
696
696
  merged: number;
697
+ released: number;
697
698
  };
698
699
  since: number;
699
700
  };
@@ -51,9 +51,9 @@ const counters = () => ({
51
51
  emptied: 0,
52
52
  marks: 0,
53
53
  records: 0,
54
- candidates: 0,
55
54
  claims: 0,
56
- merged: 0
55
+ merged: 0,
56
+ released: 0
57
57
  },
58
58
  since: Date.now()
59
59
  });
@@ -1,4 +1,4 @@
1
- import type { PurgeReport } from '../../crdt/purge.js';
1
+ import { type PurgeReport } from '../../crdt/purge.js';
2
2
  import { type RoomContext } from './context.js';
3
3
  export declare class RoomPurge {
4
4
  #private;
@@ -8,8 +8,11 @@ export declare class RoomPurge {
8
8
  get days(): number | null;
9
9
  /** A change was stored at `at`: the purge tick runs a day later at the latest. */
10
10
  noteChange(at: number): void;
11
- /** The purge task (`AttachedDocument.purge`). */
11
+ /**
12
+ * The purge task (`AttachedDocument.purge`): its report, `horizon` the
13
+ * epoch it purged to (`null`: only ended sessions' deletes).
14
+ */
12
15
  tick(): (PurgeReport & {
13
- horizon: number;
16
+ horizon: number | null;
14
17
  }) | null;
15
18
  }
@@ -1,10 +1,13 @@
1
1
  /**
2
2
  * The purge (`room.purge.*`): the daily tick records an epoch (the state
3
3
  * vector) when the document changed, and purges what was deleted before
4
- * the horizon — the newest epoch at least `purgeAfterDays` old — as the
5
- * room's own transaction (`crdt/purge.ts`), then compacts.
4
+ * the horizon — the newest epoch at least `purgeAfterDays` old — and what
5
+ * the sessions whose history ended deleted (`hist.release.end`), as the
6
+ * room's own transaction (`crdt/purge.ts`), then compacts. With no epoch
7
+ * due, a tick still purges the ended sessions' deletes.
6
8
  */
7
9
  import { Y } from '../../crdt/engine.js';
10
+ import { releasesOf } from '../../crdt/purge.js';
8
11
  import { asEngineDoc } from '../../crdt/structs.js';
9
12
  import { PURGE_ORIGIN, noTimers } from './shared.js';
10
13
  import { DEFAULT_RETENTION_DAYS } from '../history.js';
@@ -27,7 +30,10 @@ export class RoomPurge {
27
30
  if (this.days !== null)
28
31
  this.room.scheduler.schedule('purge', at + DAY, 'earlier');
29
32
  }
30
- /** The purge task (`AttachedDocument.purge`). */
33
+ /**
34
+ * The purge task (`AttachedDocument.purge`): its report, `horizon` the
35
+ * epoch it purged to (`null`: only ended sessions' deletes).
36
+ */
31
37
  tick() {
32
38
  const room = this.room;
33
39
  const { scheduler, sql, tables } = room;
@@ -61,6 +67,10 @@ export class RoomPurge {
61
67
  if (horizon !== undefined && horizon.at > purged) {
62
68
  result = this.#purgeTo({ at: horizon.at, sv: new Uint8Array(horizon.sv) });
63
69
  }
70
+ else if (releasesOf(asEngineDoc(doc)).size > 0) {
71
+ // No epoch is due: the ended sessions' deletes go all the same.
72
+ result = this.#purgeTo(null);
73
+ }
64
74
  // The next epoch to pass the horizon, or the next change, arms it again.
65
75
  const next = sql
66
76
  .exec(`SELECT at FROM ${tables.epochs} WHERE at > ? ORDER BY at LIMIT 1`, Math.max(purged, horizon?.at ?? -1))
@@ -72,18 +82,27 @@ export class RoomPurge {
72
82
  return result;
73
83
  });
74
84
  }
75
- /** Purge what was deleted before `horizon`, as the room's own transaction, then compact. */
85
+ /**
86
+ * Purge what was deleted before `horizon` and what ended sessions deleted
87
+ * (`null`: only those), as the room's own transaction, then compact. A
88
+ * purge of ended sessions' deletes that finds none left compacts nothing
89
+ * and reports `null`.
90
+ */
76
91
  #purgeTo(horizon) {
77
92
  const room = this.room;
78
93
  const doc = room.requireDoc();
79
94
  // A restore past the horizon can no longer be undone (`room.history.undo`).
80
- room.history.expireRestore(horizon.at);
95
+ if (horizon !== null)
96
+ room.history.expireRestore(horizon.at);
81
97
  room.history.closeSlotIfPast();
82
98
  let report;
99
+ let wrote = true;
83
100
  room.transacting = true;
84
101
  try {
85
102
  room.facade.transact(() => {
86
103
  report = crdt.doc.purge(asEngineDoc(doc), room.facade, horizon);
104
+ const tr = asEngineDoc(doc)._transaction;
105
+ wrote = !tr.insertSet.isEmpty() || !tr.deleteSet.isEmpty();
87
106
  }, PURGE_ORIGIN);
88
107
  }
89
108
  finally {
@@ -94,30 +113,36 @@ export class RoomPurge {
94
113
  room.storage.heal();
95
114
  throw error;
96
115
  }
97
- room.ctx.storage.transactionSync(() => {
98
- room.sql.exec(`INSERT OR REPLACE INTO ${room.tables.meta} (key, value) VALUES ('purged', ?)`, horizon.at);
99
- room.sql.exec(`DELETE FROM ${room.tables.epochs} WHERE at < ?`, horizon.at);
100
- });
101
- room.storage.compact();
102
- const counters = room.counters.purge;
103
- counters.runs++;
104
- counters.horizon = horizon.at;
105
- for (const key of [
116
+ if (horizon !== null)
117
+ room.ctx.storage.transactionSync(() => {
118
+ room.sql.exec(`INSERT OR REPLACE INTO ${room.tables.meta} (key, value) VALUES ('purged', ?)`, horizon.at);
119
+ room.sql.exec(`DELETE FROM ${room.tables.epochs} WHERE at < ?`, horizon.at);
120
+ });
121
+ const keys = [
106
122
  'removed',
107
123
  'emptied',
108
124
  'marks',
109
125
  'records',
110
- 'candidates',
111
126
  'claims',
112
- 'merged'
113
- ])
127
+ 'merged',
128
+ 'released'
129
+ ];
130
+ // Nothing an ended session deleted was left: no compaction, no run.
131
+ if (horizon === null && !wrote)
132
+ return null;
133
+ room.storage.compact();
134
+ const counters = room.counters.purge;
135
+ counters.runs++;
136
+ if (horizon !== null)
137
+ counters.horizon = horizon.at;
138
+ for (const key of keys)
114
139
  counters[key] += report[key];
115
140
  room.log({
116
141
  edytor: 'purge',
117
- horizon: horizon.at,
142
+ horizon: horizon?.at ?? null,
118
143
  bytes: room.counters.compaction.lastBytes,
119
144
  ...report
120
145
  });
121
- return { ...report, horizon: horizon.at };
146
+ return { ...report, horizon: horizon?.at ?? null };
122
147
  }
123
148
  }
@@ -77,10 +77,9 @@ export declare const heldDeletes: (doc: YDoc, ds: Decoded["ds"]) => Decoded["ds"
77
77
  export declare const withoutPending: <T>(doc: YDoc, read: () => T) => T;
78
78
  /**
79
79
  * The live document as one update: its healed state — the engine
80
- * merged what each keystroke wrote and collected deleted content, sparing
81
- * the text a replica may copy again (`keepCopies`) —, without what
82
- * waits (`withoutPending`). Memory never runs ahead of storage, so it
83
- * holds exactly what the stored records hold, collected.
80
+ * merged what each keystroke wrote and collected deleted content —,
81
+ * without what waits (`withoutPending`). Memory never runs ahead of
82
+ * storage, so it holds exactly what the stored records hold, collected.
84
83
  */
85
84
  export declare const liveState: (doc: YDoc) => Uint8Array;
86
85
  /** A fresh room document (`prepareRoomDoc`'s collection rules). */
@@ -5,7 +5,7 @@
5
5
  */
6
6
  import * as encoding from 'lib0-v14/encoding';
7
7
  import { Y } from '../../crdt/engine.js';
8
- import { asEngineDoc, keepFromCollection, structAt as storeStructAt } from '../../crdt/structs.js';
8
+ import { keepFromCollection, structAt as storeStructAt } from '../../crdt/structs.js';
9
9
  import { ROOM_ORIGIN } from './shared.js';
10
10
  import { crdt } from './context.js';
11
11
  export const stateVector = (doc) => Y.decodeStateVector(Y.encodeStateVector(doc));
@@ -295,19 +295,17 @@ export const withoutPending = (doc, read) => {
295
295
  };
296
296
  /**
297
297
  * The live document as one update: its healed state — the engine
298
- * merged what each keystroke wrote and collected deleted content, sparing
299
- * the text a replica may copy again (`keepCopies`) —, without what
300
- * waits (`withoutPending`). Memory never runs ahead of storage, so it
301
- * holds exactly what the stored records hold, collected.
298
+ * merged what each keystroke wrote and collected deleted content —,
299
+ * without what waits (`withoutPending`). Memory never runs ahead of
300
+ * storage, so it holds exactly what the stored records hold, collected.
302
301
  */
303
302
  export const liveState = (doc) => withoutPending(doc, () => Y.encodeStateAsUpdate(doc));
304
303
  /**
305
304
  * A room document's collection rules, before any update applies: it keeps
306
- * what an editing replica keeps, and the content the last restore
307
- * deleted while its undo stands (`keep`, `room.history.undo`).
305
+ * the content the last restore deleted while its undo stands (`keep`,
306
+ * `room.history.undo`).
308
307
  */
309
308
  const prepareRoomDoc = (doc, keep) => {
310
- crdt.doc.keepCopies(asEngineDoc(doc));
311
309
  keepFromCollection(doc, (it) => !(keep()?.intersects(it.id.client, it.id.clock, it.length) ?? false));
312
310
  };
313
311
  /** A fresh room document (`prepareRoomDoc`'s collection rules). */
@@ -17,10 +17,13 @@
17
17
  * `u/<actorId>` → profile `{name?, color?}` — one record per
18
18
  * stable application actor, referenced by id.
19
19
  *
20
- * Published once per attach (inside an `ATTRIBUTION_ORIGIN`
21
- * transaction) and readable through `actors`/`actorOf`. These records
22
- * also travel inside `encode()` output, so `loadDocument` restores the
23
- * dictionary without republishing.
20
+ * Published with the replica's first write: inside the first local
21
+ * transaction that writes under its client id (the engine's
22
+ * `beforeCommit`, fork patch YP30), so a replica that only reads writes
23
+ * nothing, and every struct it writes travels with its binding. Readable
24
+ * through `actors`/`actorOf`. These records also travel inside `encode()`
25
+ * output, so `loadDocument` restores the dictionary without
26
+ * republishing.
24
27
  *
25
28
  * LEGACY RECORDS — documents saved by early builds carry `a/` per-edit
26
29
  * records under the same root. They are plain replicated state, preserved
@@ -35,9 +38,10 @@ import { type BlockAttribution } from './block.js';
35
38
  /** Reserved replicated root for all attribution metadata (`SCHEMA.roots.attribution`). */
36
39
  export { ATTRIBUTION_ROOT };
37
40
  /**
38
- * Origin of every metadata write this module performs (`u/`/`c/`
39
- * dictionary publishes only — per-edit record writes are retired). Kept
40
- * out of undo capture; tooling classifies it as bookkeeping.
41
+ * Origin of the profile writes this module performs on its own
42
+ * (`setProfile`; the binding joins the transaction of the replica's first
43
+ * write, whatever its origin). Kept out of undo capture; tooling classifies
44
+ * it as bookkeeping.
41
45
  */
42
46
  export declare const ATTRIBUTION_ORIGIN: unique symbol;
43
47
  /** The stable application-level author identity (mirrors `DocumentActor`). */
@@ -57,14 +61,23 @@ export type ActorProfile = {
57
61
  * records identically and stay correct as old replicas append records.
58
62
  */
59
63
  export type DocumentAttribution = {
60
- /** actorId → profile — the replicated actor dictionary (`u/` records). */
64
+ /**
65
+ * actorId → profile — the replicated actor dictionary (`u/` records): the
66
+ * actors whose replicas wrote, each published with its replica's first
67
+ * write (or `setProfile`).
68
+ */
61
69
  readonly actors: ReadonlyMap<string, ActorProfile>;
62
- /** Resolve an item's inserting replica (clientID) to an actor id (`c/` records). */
70
+ /**
71
+ * Resolve an item's inserting replica (clientID) to an actor id (`c/`
72
+ * records). A replica's binding is written with its first write, so every
73
+ * client id that wrote a struct resolves; one that never wrote does not.
74
+ */
63
75
  actorOf(client: number): string | undefined;
64
76
  /**
65
- * Re-publish the local actor's profile record (`u/<actorId>` — LWW).
66
- * Attribution is never relabeled: historical records keep the stable
67
- * actor id; only the resolvable profile changes.
77
+ * Re-publish the local actor's profile record (`u/<actorId>` — LWW), a
78
+ * write of this replica (its binding goes with it, as with any first
79
+ * write). Attribution is never relabeled: historical records keep the
80
+ * stable actor id; only the resolvable profile changes.
68
81
  */
69
82
  setProfile(profile: ActorProfile): void;
70
83
  /**
@@ -94,6 +107,12 @@ export type AttributionController = {
94
107
  type AttachOptions = {
95
108
  actor: AttributionActor;
96
109
  };
110
+ /**
111
+ * The actor `doc`'s own replica writes for: its binding (`c/<clientID>`),
112
+ * else, before its first write, the actor its attribution controller was
113
+ * attached with (`undefined`: a raw doc). The provider's `saved` reads it.
114
+ */
115
+ export declare const writerOf: (doc: EngineDoc) => string | undefined;
97
116
  /** The display profile of `actor`: its name and color, those it has. */
98
117
  export declare const profileOf: (actor: AttributionActor) => ActorProfile;
99
118
  /**