edytor 4.0.0 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (193) 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 +10 -3
  6. package/dist/cloudflare/index.d.ts +1 -1
  7. package/dist/cloudflare/room/access.d.ts +8 -3
  8. package/dist/cloudflare/room/access.js +44 -20
  9. package/dist/cloudflare/room/admission.js +28 -5
  10. package/dist/cloudflare/room/comments.js +28 -1
  11. package/dist/cloudflare/room/context.d.ts +106 -109
  12. package/dist/cloudflare/room/context.js +10 -3
  13. package/dist/cloudflare/room/forged.d.ts +14 -0
  14. package/dist/cloudflare/room/forged.js +30 -2
  15. package/dist/cloudflare/room/frames.d.ts +4 -0
  16. package/dist/cloudflare/room/frames.js +5 -1
  17. package/dist/cloudflare/room/purge.js +2 -1
  18. package/dist/cloudflare/room/replicas.d.ts +4 -2
  19. package/dist/cloudflare/room/replicas.js +8 -3
  20. package/dist/cloudflare/room/suggest.d.ts +7 -0
  21. package/dist/cloudflare/room/suggest.js +143 -0
  22. package/dist/cloudflare/routeDocumentSocket.d.ts +10 -1
  23. package/dist/cloudflare/routeDocumentSocket.js +5 -3
  24. package/dist/components/Content.svelte +9 -1
  25. package/dist/crdt/anchors.js +3 -2
  26. package/dist/crdt/attribution/attribution.d.ts +3 -0
  27. package/dist/crdt/attribution/attribution.js +3 -3
  28. package/dist/crdt/doc/funnel.d.ts +1 -1
  29. package/dist/crdt/doc/funnel.js +5 -2
  30. package/dist/crdt/doc/gate.d.ts +1 -1
  31. package/dist/crdt/doc/history.d.ts +6 -0
  32. package/dist/crdt/doc/history.js +79 -59
  33. package/dist/crdt/doc/ops.d.ts +81 -0
  34. package/dist/crdt/doc/ops.js +1898 -0
  35. package/dist/crdt/doc/reads.d.ts +2 -2
  36. package/dist/crdt/doc/reads.js +18 -28
  37. package/dist/crdt/doc/suggest.d.ts +167 -0
  38. package/dist/crdt/doc/suggest.js +579 -0
  39. package/dist/crdt/document.d.ts +10 -1
  40. package/dist/crdt/document.js +19 -11
  41. package/dist/crdt/edytor-doc.d.ts +294 -272
  42. package/dist/crdt/edytor-doc.js +123 -108
  43. package/dist/crdt/engine-api.d.ts +8 -2
  44. package/dist/crdt/engine.d.ts +2 -1
  45. package/dist/crdt/engine.js +3 -1
  46. package/dist/crdt/flow.d.ts +5 -47
  47. package/dist/crdt/flow.js +3 -269
  48. package/dist/crdt/index.d.ts +102 -108
  49. package/dist/crdt/index.js +3 -0
  50. package/dist/crdt/operations.d.ts +13 -4
  51. package/dist/crdt/parts.d.ts +43 -0
  52. package/dist/crdt/parts.js +44 -0
  53. package/dist/crdt/placement/display.d.ts +1 -1
  54. package/dist/crdt/placement/display.js +3 -3
  55. package/dist/crdt/placement/keys.d.ts +5 -0
  56. package/dist/crdt/placement/keys.js +5 -0
  57. package/dist/crdt/placement/resolve.d.ts +1 -1
  58. package/dist/crdt/placement/resolve.js +2 -1
  59. package/dist/crdt/protocol.d.ts +1 -1
  60. package/dist/crdt/protocol.js +1 -1
  61. package/dist/crdt/protocols/auth.d.ts +11 -1
  62. package/dist/crdt/protocols/auth.js +17 -1
  63. package/dist/crdt/protocols/awareness.js +46 -81
  64. package/dist/crdt/protocols/comments.d.ts +16 -1
  65. package/dist/crdt/protocols/comments.js +9 -1
  66. package/dist/crdt/providers/index.d.ts +12 -22
  67. package/dist/crdt/providers/index.js +3 -1
  68. package/dist/crdt/providers/indexeddb.d.ts +6 -10
  69. package/dist/crdt/providers/indexeddb.js +10 -16
  70. package/dist/crdt/providers/room.d.ts +11 -0
  71. package/dist/crdt/providers/room.js +26 -12
  72. package/dist/crdt/providers/websocket.d.ts +8 -12
  73. package/dist/crdt/providers/websocket.js +17 -19
  74. package/dist/crdt/purge.d.ts +7 -1
  75. package/dist/crdt/purge.js +32 -2
  76. package/dist/crdt/rangeDelete.d.ts +2 -47
  77. package/dist/crdt/rangeDelete.js +1 -169
  78. package/dist/crdt/restore.d.ts +2 -1
  79. package/dist/crdt/restore.js +9 -0
  80. package/dist/crdt/schema.d.ts +9 -1
  81. package/dist/crdt/schema.js +13 -3
  82. package/dist/crdt/structs.d.ts +2 -0
  83. package/dist/crdt/structs.js +2 -0
  84. package/dist/crdt/text/index/build.d.ts +125 -0
  85. package/dist/crdt/text/index/build.js +2895 -0
  86. package/dist/crdt/text/index/shared.d.ts +2 -6
  87. package/dist/crdt/text/index/shared.js +19 -19
  88. package/dist/crdt/text/model.d.ts +10 -0
  89. package/dist/crdt/text/model.js +10 -4
  90. package/dist/crdt/text/runs.d.ts +43 -42
  91. package/dist/crdt/text/runs.js +2 -165
  92. package/dist/crdt/vendor/yjs/UPSTREAM.md +83 -0
  93. package/dist/crdt/vendor/yjs/dts/index.d.ts +1 -1
  94. package/dist/crdt/vendor/yjs/dts/structs/Item.d.ts +6 -3
  95. package/dist/crdt/vendor/yjs/dts/ynode.d.ts +13 -2
  96. package/dist/crdt/vendor/yjs/src/index.js +1 -1
  97. package/dist/crdt/vendor/yjs/src/structs/Item.js +44 -5
  98. package/dist/crdt/vendor/yjs/src/ynode.js +29 -7
  99. package/dist/edytor.svelte.d.ts +1 -1
  100. package/dist/edytor.svelte.js +100 -75
  101. package/dist/events/beforeInputCommands.d.ts +2 -2
  102. package/dist/events/domTextOffset.d.ts +2 -0
  103. package/dist/events/domTextOffset.js +8 -0
  104. package/dist/events/nativeInteractiveControl.d.ts +11 -1
  105. package/dist/events/nativeInteractiveControl.js +41 -1
  106. package/dist/events/onBeforeInput.js +3 -3
  107. package/dist/events/onFocus.js +9 -11
  108. package/dist/export/forms.d.ts +75 -75
  109. package/dist/index.d.ts +4 -2
  110. package/dist/index.js +1 -1
  111. package/dist/labels.d.ts +8 -0
  112. package/dist/labels.js +6 -1
  113. package/dist/plugins/comments/CommentsController.svelte.d.ts +5 -1
  114. package/dist/plugins/comments/CommentsController.svelte.js +14 -4
  115. package/dist/plugins/find/search.js +2 -2
  116. package/dist/plugins/index.d.ts +1 -0
  117. package/dist/plugins/index.js +1 -0
  118. package/dist/plugins/richtext/RichTextPlugin.svelte +63 -61
  119. package/dist/plugins/richtext/richTextOperations.js +26 -32
  120. package/dist/plugins/suggestions/SuggestionBars.svelte +40 -23
  121. package/dist/plugins/suggestions/SuggestionBars.svelte.d.ts +3 -1
  122. package/dist/plugins/suggestions/SuggestionRuns.svelte +77 -0
  123. package/dist/plugins/suggestions/SuggestionRuns.svelte.d.ts +9 -0
  124. package/dist/plugins/suggestions/shared.svelte.d.ts +58 -0
  125. package/dist/plugins/suggestions/shared.svelte.js +421 -0
  126. package/dist/plugins/suggestions/suggestionsPlugin.d.ts +15 -4
  127. package/dist/selection/domSelection.d.ts +2 -0
  128. package/dist/selection/domSelection.js +6 -2
  129. package/dist/selection/pointer.svelte.js +75 -48
  130. package/dist/selection/replaceSelection.js +2 -6
  131. package/dist/selection/selection.svelte.d.ts +3 -3
  132. package/dist/selection/selection.svelte.js +39 -32
  133. package/dist/selection/selection.utils.d.ts +14 -0
  134. package/dist/selection/selection.utils.js +33 -11
  135. package/dist/session/commands.js +21 -11
  136. package/dist/session/composition.svelte.d.ts +5 -3
  137. package/dist/session/composition.svelte.js +21 -17
  138. package/dist/session/handles.d.ts +4 -17
  139. package/dist/session/handles.js +14 -44
  140. package/dist/session/navigation.js +6 -9
  141. package/dist/session/selection.d.ts +11 -5
  142. package/dist/session/selection.js +14 -48
  143. package/dist/session/suggestions.svelte.d.ts +38 -9
  144. package/dist/session/suggestions.svelte.js +37 -18
  145. package/dist/session/virtual.d.ts +1 -1
  146. package/dist/surface/attributes.d.ts +5 -0
  147. package/dist/surface/attributes.js +2 -2
  148. package/dist/surface/cells.d.ts +2 -26
  149. package/dist/surface/cells.js +2 -33
  150. package/dist/surface/observer.svelte.d.ts +6 -1
  151. package/dist/surface/observer.svelte.js +143 -111
  152. package/dist/surface/pin.svelte.d.ts +9 -9
  153. package/dist/surface/pin.svelte.js +15 -22
  154. package/dist/surface/projector.svelte.js +10 -16
  155. package/dist/text/text.svelte.js +14 -19
  156. package/dist/themes/notion.css +29 -0
  157. package/dist/utils.d.ts +5 -0
  158. package/dist/utils.js +7 -0
  159. package/package.json +3 -1
  160. package/dist/crdt/doc/capability.d.ts +0 -56
  161. package/dist/crdt/doc/capability.js +0 -211
  162. package/dist/crdt/doc/content.d.ts +0 -16
  163. package/dist/crdt/doc/content.js +0 -77
  164. package/dist/crdt/doc/delete.d.ts +0 -15
  165. package/dist/crdt/doc/delete.js +0 -84
  166. package/dist/crdt/doc/layout.d.ts +0 -9
  167. package/dist/crdt/doc/layout.js +0 -178
  168. package/dist/crdt/doc/meta.d.ts +0 -19
  169. package/dist/crdt/doc/meta.js +0 -199
  170. package/dist/crdt/doc/moves.d.ts +0 -21
  171. package/dist/crdt/doc/moves.js +0 -274
  172. package/dist/crdt/doc/split-merge.d.ts +0 -10
  173. package/dist/crdt/doc/split-merge.js +0 -179
  174. package/dist/crdt/doc/steps.d.ts +0 -65
  175. package/dist/crdt/doc/steps.js +0 -311
  176. package/dist/crdt/text/index/cache.d.ts +0 -16
  177. package/dist/crdt/text/index/cache.js +0 -175
  178. package/dist/crdt/text/index/claims.d.ts +0 -18
  179. package/dist/crdt/text/index/claims.js +0 -225
  180. package/dist/crdt/text/index/fold.d.ts +0 -21
  181. package/dist/crdt/text/index/fold.js +0 -656
  182. package/dist/crdt/text/index/layout.d.ts +0 -24
  183. package/dist/crdt/text/index/layout.js +0 -271
  184. package/dist/crdt/text/index/placement.d.ts +0 -47
  185. package/dist/crdt/text/index/placement.js +0 -741
  186. package/dist/crdt/text/index/records.d.ts +0 -13
  187. package/dist/crdt/text/index/records.js +0 -139
  188. package/dist/crdt/text/index/report.d.ts +0 -18
  189. package/dist/crdt/text/index/report.js +0 -337
  190. package/dist/crdt/text/index/state.d.ts +0 -197
  191. package/dist/crdt/text/index/state.js +0 -116
  192. package/dist/crdt/text/index/streams.d.ts +0 -25
  193. package/dist/crdt/text/index/streams.js +0 -248
@@ -293,6 +293,10 @@ export const markSynced = (host) => {
293
293
  host._settleSynced[0](host);
294
294
  return true;
295
295
  };
296
+ /** Report `error` on `provider`'s error channel (`'message-error'`): a frame, a payload or a storage failure. */
297
+ export const fault = (provider, error) => provider.emit?.('message-error', [error, provider]);
298
+ /** A message type no handler claims inside a valid envelope: protocol skew, reported, never dropped silently. */
299
+ export const unknownType = (provider, what, type) => fault(provider, new Error(`Unknown ${what} message type ${type}`));
296
300
  /**
297
301
  * Terminal `'failed'` emission (the sync-failure contract): fires at
298
302
  * most once per provider, only while it has never synced — destroy before
@@ -425,13 +429,13 @@ export const bindRoomProtocol = (syncProtocol, behavior) => {
425
429
  // (`discarded`) and reported the same way.
426
430
  const update = decoding.readVarUint8Array(decoder);
427
431
  const origin = emitSynced ? provider : (behavior.tabOrigin?.(provider) ?? provider);
428
- const onError = (error) => provider.emit?.('message-error', [error, provider]);
432
+ const onError = (error) => fault(provider, error);
429
433
  const { applied, problem, discarded } = syncProtocol.applyRemote(provider.doc, update, origin, onError);
430
434
  const refused = problem ?? discarded;
431
435
  if (refused) {
432
436
  const docName = behavior.docName(provider);
433
437
  provider.emit?.('schema-mismatch', [{ docName, problem: refused }, provider]);
434
- provider.emit?.('message-error', [new SchemaMismatchError(docName, refused), provider]);
438
+ fault(provider, new SchemaMismatchError(docName, refused));
435
439
  }
436
440
  // Only an ACCEPTED Step2 is a held member state — a refused
437
441
  // one never claims the handshake.
@@ -441,10 +445,7 @@ export const bindRoomProtocol = (syncProtocol, behavior) => {
441
445
  return;
442
446
  }
443
447
  // Unknown sync subtype inside a valid envelope — observable.
444
- provider.emit?.('message-error', [
445
- new Error(`Unknown sync message type ${syncMessageType}`),
446
- provider
447
- ]);
448
+ unknownType(provider, 'sync', syncMessageType);
448
449
  },
449
450
  [messageQueryAwareness]: (encoder, _decoder, provider) => {
450
451
  encoding.writeVarUint(encoder, messageAwareness);
@@ -477,10 +478,7 @@ export const bindRoomProtocol = (syncProtocol, behavior) => {
477
478
  if (!messageHandler) {
478
479
  // Fail closed + observable: a VALID envelope carrying a message
479
480
  // type no handler claims is protocol skew — never silent.
480
- provider.emit?.('message-error', [
481
- new Error(`Unknown v14 message type ${messageType}`),
482
- provider
483
- ]);
481
+ unknownType(provider, 'v14', messageType);
484
482
  return encoder;
485
483
  }
486
484
  const followUp = messageHandler(encoder, decoder, provider, emitSynced);
@@ -503,7 +501,7 @@ export const bindRoomProtocol = (syncProtocol, behavior) => {
503
501
  catch (error) {
504
502
  // same-tab publishes are delivered synchronously — a malformed
505
503
  // message must not propagate into the publisher's call stack.
506
- provider.emit?.('message-error', [error, provider]);
504
+ fault(provider, error);
507
505
  }
508
506
  };
509
507
  };
@@ -554,6 +552,21 @@ export const bindRoomProtocol = (syncProtocol, behavior) => {
554
552
  };
555
553
  /** The departure announcement — our presence, removed — on every transport the provider speaks. */
556
554
  const depart = (provider) => behavior.broadcast(provider, awarenessFrame(provider.awareness, [provider.doc.clientID], new Map()));
555
+ /**
556
+ * Wire `provider` to its room: its BroadcastChannel subscriber, `onUpdate`
557
+ * on its document's updates and its presence changes to the room
558
+ * (`awarenessUpdateHandler`). The teardown removes both listeners.
559
+ */
560
+ const listen = (provider, onUpdate) => {
561
+ provider._bcSubscriber = bcSubscriber(provider);
562
+ const onPresence = awarenessUpdateHandler(provider);
563
+ provider.doc.on('update', onUpdate);
564
+ provider.awareness.on('update', onPresence);
565
+ return () => {
566
+ provider.awareness.off('update', onPresence);
567
+ provider.doc.off('update', onUpdate);
568
+ };
569
+ };
557
570
  /** Leave the room: announce the presence removal, then unsubscribe the BC channel. */
558
571
  const disconnectBc = (provider) => {
559
572
  depart(provider);
@@ -572,6 +585,7 @@ export const bindRoomProtocol = (syncProtocol, behavior) => {
572
585
  hello,
573
586
  depart,
574
587
  connectBc,
575
- disconnectBc
588
+ disconnectBc,
589
+ listen
576
590
  };
577
591
  };
@@ -7,6 +7,8 @@ export type WebsocketProviderEvents = {
7
7
  status: (event: {
8
8
  status: 'connected' | 'disconnected' | 'connecting';
9
9
  }) => void;
10
+ /** What the room says this connection's user may do, as it joins (and at a change in place). */
11
+ access: (access: 'write' | 'suggest' | 'comment' | 'read', provider: unknown) => void;
10
12
  synced: (state: boolean) => void;
11
13
  'connection-close': (event: CloseEvent | null, provider: unknown) => void;
12
14
  'connection-error': (event: Event, provider: unknown) => void;
@@ -183,14 +185,8 @@ export declare const bindWebsocketProvider: (Y: EngineApi) => {
183
185
  /** Closes the current dial if it has not opened in time. */
184
186
  _connectTimer: ReturnType<typeof setTimeout> | undefined;
185
187
  _checkInterval: ReturnType<typeof setInterval>;
186
- _updateHandler: (update: Uint8Array, origin: unknown, doc?: unknown, transaction?: {
187
- local: boolean;
188
- }) => void;
189
- _awarenessUpdateHandler: (updates: {
190
- added: number[];
191
- updated: number[];
192
- removed: number[];
193
- }, origin: unknown) => void;
188
+ /** @internal Removes the document and presence listeners (`room.listen`). */
189
+ _unlisten: () => void;
194
190
  /**
195
191
  * The dial URL: the room name as one encoded path segment (any id —
196
192
  * `/`, `%`, `#`, `?` included — reaches its own room; the server
@@ -221,11 +217,11 @@ export declare const bindWebsocketProvider: (Y: EngineApi) => {
221
217
  destroy(): void;
222
218
  disconnect(): void;
223
219
  connect(): void;
224
- 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;
220
+ emit<N extends "refused" | "status" | "failed" | "message-error" | "schema-mismatch" | "protocol-mismatch" | "synced" | "access" | "connection-close" | "connection-error" | "saved" | "permission-denied" | "expired" | "unreachable">(name: N, args: Parameters<WebsocketProviderEvents[N]>): void;
225
221
  _observers: Map<string, Set<any>>;
226
- 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];
227
- 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;
228
- 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;
222
+ on<NAME extends "refused" | "status" | "failed" | "message-error" | "schema-mismatch" | "protocol-mismatch" | "synced" | "access" | "connection-close" | "connection-error" | "saved" | "permission-denied" | "expired" | "unreachable">(name: NAME, f: WebsocketProviderEvents[NAME]): WebsocketProviderEvents[NAME];
223
+ once<NAME extends "refused" | "status" | "failed" | "message-error" | "schema-mismatch" | "protocol-mismatch" | "synced" | "access" | "connection-close" | "connection-error" | "saved" | "permission-denied" | "expired" | "unreachable">(name: NAME, f: WebsocketProviderEvents[NAME]): void;
224
+ off<NAME extends "refused" | "status" | "failed" | "message-error" | "schema-mismatch" | "protocol-mismatch" | "synced" | "access" | "connection-close" | "connection-error" | "saved" | "permission-denied" | "expired" | "unreachable">(name: NAME, f: WebsocketProviderEvents[NAME]): void;
229
225
  };
230
226
  };
231
227
  watchComments: (server: string | null, roomName: string, listener: (message: CommentMessage) => void) => (() => void);
@@ -52,11 +52,11 @@ import * as math from 'lib0-v14/math';
52
52
  import * as url from 'lib0-v14/url';
53
53
  import { Awareness, removeAwarenessStates } from '../protocols/awareness.js';
54
54
  import { IsolatedObservable } from '../protocols/observable.js';
55
- import { messagePermissionDenied, messageReadOnly, READ_ONLY_DENIAL, readAuthMessage } from '../protocols/auth.js';
55
+ import { messagePermissionDenied, messageAccess, messageReadOnly, READ_ONLY_DENIAL, SUGGEST_ONLY_DENIAL, readAuthMessage } from '../protocols/auth.js';
56
56
  import { bindSync } from '../protocols/sync.js';
57
57
  import { readCommentsMessage, writeCommentsSubscribe } from '../protocols/comments.js';
58
58
  import { frame } from '../protocols/envelope.js';
59
- import { assertRoomId, beginDestroy, bindRoomProtocol, tabs, chunkFrame, CLOSE, createChunkReader, emitFailed, initLifecycle, isRefusal, LOCAL_PRESENCE_LOSS, markSynced, messageAuth, messageChunk, messageComments, messageSaved, messageSync, MAX_FRAME_BYTES, SyncRefusedError } from './room.js';
59
+ import { assertRoomId, beginDestroy, bindRoomProtocol, tabs, chunkFrame, CLOSE, createChunkReader, emitFailed, fault, initLifecycle, isRefusal, unknownType, LOCAL_PRESENCE_LOSS, markSynced, messageAuth, messageChunk, messageComments, messageSaved, messageSync, MAX_FRAME_BYTES, SyncRefusedError } from './room.js';
60
60
  import { asEngineDoc } from '../structs.js';
61
61
  import { ATTRIBUTION_ROOT } from '../schema.js';
62
62
  import { ATTRIBUTION_ORIGIN, writerOf } from '../attribution/attribution.js';
@@ -122,18 +122,20 @@ export const bindWebsocketProvider = (Y) => {
122
122
  if (readOnly)
123
123
  provider._writes.deny();
124
124
  provider.emit('permission-denied', [reason, provider]);
125
- if (!readOnly)
125
+ // What a suggest-only socket may not write is stripped: the socket stays.
126
+ if (!readOnly && reason !== SUGGEST_ONLY_DENIAL)
126
127
  emitFailed(provider, new Error(`permission denied: ${reason}`));
127
128
  },
128
129
  // The room's notice at the join: a state, not a refusal (no event).
129
- () => provider._writes.deny());
130
- if (authType !== messagePermissionDenied && authType !== messageReadOnly) {
130
+ () => provider._writes.deny(),
131
+ // The room's word at the join, and at a change in place (`read`).
132
+ (access) => provider.emit('access', [access, provider]));
133
+ if (authType !== messagePermissionDenied &&
134
+ authType !== messageReadOnly &&
135
+ authType !== messageAccess) {
131
136
  // An auth type we do not speak inside a valid v14 envelope is
132
137
  // protocol skew — reported, never dropped silently.
133
- provider.emit('message-error', [
134
- new Error(`Unknown auth message type ${authType}`),
135
- provider
136
- ]);
138
+ unknownType(provider, 'auth', authType);
137
139
  }
138
140
  }
139
141
  },
@@ -454,7 +456,7 @@ export const bindWebsocketProvider = (Y) => {
454
456
  room.readMessage(provider, new Uint8Array(event.data), true, (buf) => sendOn(provider, websocket, buf));
455
457
  }
456
458
  catch (error) {
457
- provider.emit('message-error', [error, provider]);
459
+ fault(provider, error);
458
460
  }
459
461
  };
460
462
  websocket.onerror = (event) => {
@@ -521,8 +523,8 @@ export const bindWebsocketProvider = (Y) => {
521
523
  /** Closes the current dial if it has not opened in time. */
522
524
  _connectTimer;
523
525
  _checkInterval;
524
- _updateHandler;
525
- _awarenessUpdateHandler;
526
+ /** @internal Removes the document and presence listeners (`room.listen`). */
527
+ _unlisten;
526
528
  constructor(serverUrl, roomname, doc, { connect = true, awareness = new Awareness(doc), params = {}, WebSocketPolyfill = WebSocket, resyncInterval = 0, maxBackoffTime = 2500, connectTimeout = defaultConnectTimeout, disableBc = false, maxFrameBytes = MAX_FRAME_BYTES } = {}) {
527
529
  assertRoomId(roomname);
528
530
  super();
@@ -543,14 +545,13 @@ export const bindWebsocketProvider = (Y) => {
543
545
  if (resyncInterval > 0) {
544
546
  this._resync = setInterval(() => send(this, room.step1(this)), resyncInterval);
545
547
  }
546
- this._bcSubscriber = room.bcSubscriber(this);
547
548
  providers.add(this);
548
549
  // Local doc updates go to the room; another tab's go to the server
549
550
  // only (`broadcastUpdate` quarantines a read-only document). A
550
551
  // socket the room made read-only gets this document's own edits
551
552
  // alone (each is denied): what a tab or a store delivered, and the
552
553
  // actor records, would draw denials nobody made.
553
- this._updateHandler = (update, origin, _doc, transaction) => {
554
+ const onUpdate = (update, origin, _doc, transaction) => {
554
555
  if (origin === this)
555
556
  return;
556
557
  const heard = origin === this._fromTab;
@@ -568,9 +569,7 @@ export const bindWebsocketProvider = (Y) => {
568
569
  };
569
570
  this._writes = new OwnWrites(doc, (unsaved) => this.emit('saved', [{ saved: unsaved === 0, unsaved }, this]));
570
571
  this._writes.trackDocument();
571
- this.doc.on('update', this._updateHandler);
572
- this._awarenessUpdateHandler = room.awarenessUpdateHandler(this);
573
- awareness.on('update', this._awarenessUpdateHandler);
572
+ this._unlisten = room.listen(this, onUpdate);
574
573
  this._checkInterval = setInterval(() => {
575
574
  if (!this.wsconnected)
576
575
  return;
@@ -646,8 +645,7 @@ export const bindWebsocketProvider = (Y) => {
646
645
  clearInterval(this._checkInterval);
647
646
  providers.delete(this);
648
647
  this.disconnect();
649
- this.awareness.off('update', this._awarenessUpdateHandler);
650
- this.doc.off('update', this._updateHandler);
648
+ this._unlisten();
651
649
  super.destroy();
652
650
  }
653
651
  disconnect() {
@@ -18,6 +18,7 @@
18
18
  import type { EngineApi, EngineDoc } from './engine-api.js';
19
19
  import { type PlacementModel } from './placement/model.js';
20
20
  import { type TextEngine } from './text/model.js';
21
+ import { type SuggestFacade } from './doc/suggest.js';
21
22
  /** The horizon: the time and encoded state vector of the room's epoch (`room.purge.timing`). */
22
23
  export type Horizon = {
23
24
  at: number;
@@ -43,6 +44,11 @@ export type PurgeReport = {
43
44
  merged: number;
44
45
  /** Release records the horizon covers (`hist.release.end`): deleted. */
45
46
  released: number;
47
+ /**
48
+ * Suggestions decided before the horizon (`sugg.purge`): nothing of them
49
+ * pending, their records' entries older than it: their records deleted.
50
+ */
51
+ suggestions: number;
46
52
  };
47
53
  /**
48
54
  * The ended histories a document holds (`hist.release.end`): per writer,
@@ -53,7 +59,7 @@ export declare const releasesOf: (doc: EngineDoc) => Map<number, number>;
53
59
  /** The horizon a document holds (`null`: never purged). */
54
60
  export declare const readHorizon: (doc: EngineDoc) => Horizon | null;
55
61
  export declare const bindPurge: (Y: EngineApi) => {
56
- purge: (doc: EngineDoc, facade: {
62
+ purge: (doc: EngineDoc, facade: Partial<SuggestFacade> & {
57
63
  model: PlacementModel;
58
64
  text: TextEngine;
59
65
  }, horizon: Horizon | null) => PurgeReport;
@@ -1,10 +1,13 @@
1
1
  import { recordsOf, registryOf, view as modelView } from './placement/model.js';
2
2
  import { removeUnit, unitOf } from './placement/slots.js';
3
3
  import { deleteClaims, purgeStreams, scanText } from './text/model.js';
4
- import { BLOCK_ATTR_ROOT, DATA, DATA_LEAF_PREFIX, DEL_PREFIX, HORIZON_ATTR, HORIZON_ROOT, REGISTRY_KEY, RELEASE_ROOT, TEXT_DELETES_ROOT, WITHDRAW_PREFIX } from './schema.js';
4
+ import { BLOCK_ATTR_ROOT, DATA, DATA_LEAF_PREFIX, DEL_PREFIX, HORIZON_ATTR, HORIZON_ROOT, REGISTRY_KEY, RELEASE_ROOT, SUGGEST_ROOT, TEXT_DELETES_ROOT, WITHDRAW_PREFIX } from './schema.js';
5
5
  import { baseIdOf, isIncarnationId } from './incarnations.js';
6
+ import { suggestionsOf } from './doc/suggest.js';
6
7
  import { attrItems, dropSearchMarkers, engineOps, firstItem, holdsIds } from './structs.js';
7
8
  const mapOf = (node) => attrItems(node);
9
+ /** The clock a suggestion's record opened at (its writer's clocks it owns start there). */
10
+ const clockOf = (rec) => rec.start ?? 0;
8
11
  /**
9
12
  * The ended histories a document holds (`hist.release.end`): per writer,
10
13
  * the clock its latest live release record ended at (what it wrote below
@@ -107,7 +110,8 @@ export const bindPurge = (Y) => {
107
110
  records: 0,
108
111
  claims: 0,
109
112
  merged: 0,
110
- released: 0
113
+ released: 0,
114
+ suggestions: 0
111
115
  };
112
116
  const vector = horizon === null
113
117
  ? new Map()
@@ -425,6 +429,32 @@ export const bindPurge = (Y) => {
425
429
  report.released++;
426
430
  }
427
431
  }
432
+ // Suggestions decided before the horizon (`sugg.purge`): the decision
433
+ // trail goes with them. A writer's oldest record first, so no record's
434
+ // clocks pass to another.
435
+ const suggest = facade.part && suggestionsOf(facade);
436
+ const records = suggest?.records() ?? [];
437
+ if (records.length > 0) {
438
+ const pending = new Set(suggest.changes().map((c) => c.suggestion));
439
+ const root = doc.get(SUGGEST_ROOT);
440
+ const seen = new Set();
441
+ for (const rec of [...records].sort((a, b) => clockOf(a) - clockOf(b))) {
442
+ if (seen.has(rec.writer))
443
+ continue;
444
+ seen.add(rec.writer);
445
+ const node = root.getAttr(rec.id);
446
+ const items = [node._item];
447
+ for (let it = firstItem(node); it; it = it.right)
448
+ if (!it.deleted)
449
+ items.push(it);
450
+ if (pending.has(rec.id) ||
451
+ !items.every((it) => old(it.id.client, it.id.clock + it.length - 1)))
452
+ continue;
453
+ root.deleteAttr(rec.id);
454
+ seen.delete(rec.writer);
455
+ report.suggestions++;
456
+ }
457
+ }
428
458
  // The horizon record, last.
429
459
  if (horizon !== null)
430
460
  doc.get(HORIZON_ROOT).setAttr(HORIZON_ATTR, { at: horizon.at, sv: horizon.sv });
@@ -1,48 +1,8 @@
1
1
  /**
2
- * Range deletion between two positions — one prepared document operation
3
- * whose merge is the document's `canMerge`. The rules are the
4
- * `del.range.*` rows of `docs/editor-delete-contract.md`:
5
- *
6
- * - the head keeps `[0, start)`, the tail keeps `[end, len)`, and every
7
- * block strictly between them in document order dies;
8
- * - the head dies iff its prefix is empty (never for a replacement,
9
- * `del.range.replace`, nor when nothing else would be left to hold the
10
- * caret, `del.range.whole-doc`: the head is kept, emptied, with its id,
11
- * type and data, so peers deleting the same range keep the same block)
12
- * and the tail then keeps its id; the tail dies iff
13
- * its suffix is empty; otherwise the tail's suffix merges into the head
14
- * when `canMerge(tail, head)` allows it (`del.range.island-seal`) — an end
15
- * at the tail's start included: the seam between them is deleted;
16
- * - what follows the range end survives: the dying tail's children and the
17
- * later siblings inside every container the range dies through take the
18
- * topmost such container's slot, unless that would cross an island seal —
19
- * then those containers stay (`del.range.outside-survives`);
20
- * - a container (a list: the document's `isContainer`) never dies because
21
- * the range starts before it: it keeps what follows the range end, like
22
- * the keys (`del.merge.container`); it dies only when it loses every
23
- * child and holds no text of its own — the document's `emptiable`, the
24
- * keys' rule (`del.range.empty-container`). An island that renders
25
- * no content dies when it loses every child too, except a `lines` island
26
- * (a code block) the range lies in: its head is then kept, emptied
27
- * (`del.range.island-kept`);
28
- * - what a range rescues takes the kind it shows in its new slot (the
29
- * document's `settle`: a paragraph rescued into a list is its item);
30
- * - blocks the view hides (a closed toggle's body) are not in the range:
31
- * they go only with a block that goes, so a surviving head keeps its
32
- * hidden children, unless it is kept only to hold the caret; a dying
33
- * closed tail's body takes its place, shown (`del.range.hidden-body`);
34
- * - a layout the range leaves with one column dissolves, in the same plan
35
- * (`layout.dissolving`);
36
- * - a range with an end in a table's cell keeps every cell and row: each
37
- * cell it crosses loses the text it covers, nothing merges across a
38
- * cell's edge, the blocks outside the table between the ends go, a
39
- * table wholly between them goes whole, and the caret lands at the start
40
- * (`table.range`);
41
- * - the plan's `at` is where the caret lands (`del.range.caret`).
2
+ * The types range deletion (`deleteRange`, `replaceRange`: the `del.range.*`
3
+ * rows, prepared in `doc/ops.ts`) reads.
42
4
  */
43
5
  import type { BlockId } from './placement/model.js';
44
- import type { Prepared } from './edytor-doc.js';
45
- import type { OpsContext } from './doc/steps.js';
46
6
  /** A position in one document version: a block and a display offset. */
47
7
  export type DocPosition = {
48
8
  block: BlockId;
@@ -56,8 +16,3 @@ export type DocPosition = {
56
16
  export type RangeView = {
57
17
  hidden?: (id: BlockId, removed?: ReadonlySet<BlockId>) => boolean;
58
18
  };
59
- /** `deleteRange` and `replaceRange` (the head kept), prepared. */
60
- export declare const rangeDeleteOps: (c: OpsContext) => {
61
- deleteRange: (from: DocPosition, to: DocPosition, view?: RangeView) => Prepared;
62
- replaceRange: (from: DocPosition, to: DocPosition, view?: RangeView) => Prepared;
63
- };
@@ -1,169 +1 @@
1
- import { ref } from './doc/plan.js';
2
- /** `deleteRange` and `replaceRange` (the head kept), prepared. */
3
- export const rangeDeleteOps = (c) => {
4
- // `headSpent`: a whole-doc retry keeps the head, but what it hid goes as if it went.
5
- const prepare = (keepHead, headSpent = false) => (from, to, view = {}) => {
6
- // A position names shown text: a live block that renders its own content.
7
- const holds = (id) => c.contentTarget(id) && c.rendersContent(id);
8
- const pos = (p) => {
9
- const block = ref(p.block);
10
- const len = holds(block) ? c.displayLength(block) : -1;
11
- return { block, offset: Math.max(0, Math.min(p.offset, len)) };
12
- };
13
- let [s, e] = [pos(from), pos(to)];
14
- if (!holds(s.block) || !holds(e.block))
15
- return c.REFUSED;
16
- const { ids, at } = c.view().order;
17
- if (at.get(s.block) > at.get(e.block) || (s.block === e.block && s.offset > e.offset))
18
- [s, e] = [e, s];
19
- const S = s.block;
20
- const E = e.block;
21
- if (S === E) {
22
- const length = e.offset - s.offset;
23
- const writes = length > 0 ? [{ op: 'deleteText', id: S, offset: s.offset, length }] : [];
24
- return { ...c.plan([S], writes), at: s };
25
- }
26
- const parent = (id) => c.ancestorsOf(id)[0] ?? null;
27
- const chain = c.ancestorsOf(E);
28
- if (c.isTableCell(S) || c.isTableCell(E))
29
- return inTables(s, e, view);
30
- // What the view hides once `removed` go (nothing: as it is now).
31
- const hiddenOnce = (removed) => {
32
- const set = new Set(removed);
33
- return (id) => !!view.hidden?.(id, set);
34
- };
35
- const headDies = !keepHead && s.offset === 0;
36
- // What the view hides is not in the range: it goes only with a block that goes,
37
- // a dying head's body included.
38
- const hidden = hiddenOnce(headDies || headSpent ? [S] : []);
39
- const between = ids.slice(at.get(S) + 1, at.get(E)).filter((id) => !hidden(id));
40
- const lenE = c.displayLength(E);
41
- const tailDies = e.offset === lenE;
42
- const merges = !headDies && !tailDies && c.canMerge(E, S);
43
- const tailGone = tailDies || merges;
44
- // E's ancestors the range starts before die unless the rescue would cross an island.
45
- // A container (a list) is not one: it dies only when the range empties it, and
46
- // keeps its later items like the keys do (`del.merge.container`).
47
- const partial = chain.filter((a) => (a === (headDies ? S : null) || between.includes(a)) && !c.isContainer(a));
48
- const top = partial.at(-1);
49
- const sealed = top !== undefined && chain.slice(0, chain.indexOf(top) + 1).some(c.isIsland);
50
- const spared = new Set(sealed ? chain : []);
51
- const doomed = new Set(between.filter((id) => !spared.has(id) && !chain.includes(id)));
52
- if (!sealed)
53
- partial.forEach((a) => doomed.add(a));
54
- if (headDies && !spared.has(S))
55
- doomed.add(S);
56
- if (tailDies)
57
- doomed.add(E);
58
- // Rescue what follows the range end right after the topmost dying container's slot
59
- // (after, not at it: a concurrent delete of S revives a merged E above them).
60
- const home = sealed || top === undefined ? (tailGone ? E : undefined) : top;
61
- const rescued = home === undefined ? [] : tailGone ? c.childrenIds(E) : [E];
62
- for (let cur = E; home !== undefined && cur !== home; cur = parent(cur)) {
63
- const sibs = c.childrenIds(parent(cur));
64
- rescued.push(...sibs.slice(sibs.indexOf(cur) + 1));
65
- }
66
- const dest = home === undefined || rescued.length === 0 ? null : c.positionOf(home);
67
- // A container (or an island that renders nothing) losing every child dies too.
68
- const gone = (id) => doomed.has(id) || (merges && id === E);
69
- const dies = (id) => c.emptiable(id) || (c.isIsland(id) && !c.rendersContent(id));
70
- const emptied = (id) => {
71
- if (id === null || gone(id) || !dies(id) || id === dest?.parent)
72
- return;
73
- if (!c.childrenIds(id).every(gone))
74
- return;
75
- doomed.add(id);
76
- emptied(parent(id));
77
- };
78
- [...doomed, ...(merges ? [E] : [])].forEach((id) => emptied(parent(id)));
79
- // A range inside one `lines` island never removes it: the head stays, emptied
80
- // (a code block's Mod+A, `del.range.island-kept`).
81
- const island = c.ancestorsOf(S).find(c.isIsland);
82
- const linesIsland = island !== undefined && c.isLines(island);
83
- if (!keepHead && linesIsland && doomed.has(island) && chain.includes(island))
84
- return prepare(true, headDies)(from, to, view);
85
- const lenS = c.displayLength(S);
86
- const writes = [];
87
- if (!doomed.has(S) && lenS > s.offset)
88
- writes.push({ op: 'deleteText', id: S, offset: s.offset, length: lenS - s.offset });
89
- if (!gone(E) && e.offset > 0)
90
- writes.push({ op: 'deleteText', id: E, offset: 0, length: e.offset });
91
- if (dest !== null) {
92
- writes.push(...c.move(rescued, dest.parent, dest.index + 1));
93
- // Each shows the kind of its new slot (an island's line, a list's item leave theirs).
94
- for (const id of rescued)
95
- writes.push(...c.settle(parent(id), [id], dest.parent));
96
- }
97
- if (merges) {
98
- // The whole tail merges, then its cut prefix goes: E's own text is never written.
99
- writes.push({ op: 'mergeBlocks', from: E, into: S, at: s.offset, length: lenE });
100
- if (e.offset > 0)
101
- writes.push({ op: 'deleteText', id: S, offset: s.offset, length: e.offset });
102
- }
103
- const kept = [...rescued, ...(merges ? [E] : [])];
104
- for (const id of doomed)
105
- if (!doomed.has(parent(id)))
106
- writes.push(c.remove(id, kept));
107
- // A layout the range leaves with one column dissolves (`layout.dissolving`).
108
- writes.push(...c.dissolving(new Set([...doomed, ...(merges ? [E] : [])]), rescued, writes));
109
- const caret = (() => {
110
- if (!doomed.has(S))
111
- return s;
112
- if (!gone(E))
113
- return { block: E, offset: 0 };
114
- const survives = (id) => id === null || rescued.includes(id) || (!gone(id) && survives(parent(id)));
115
- // Shown once the plan runs: a dying closed tail's rescued body is.
116
- const hiddenAfter = hiddenOnce([...doomed, ...(merges ? [E] : [])]);
117
- const shown = (id) => survives(id) && holds(id) && !hiddenAfter(id);
118
- // Before the tail: blocks a sealed rescue spared between S and E count too.
119
- const prev = ids.slice(0, at.get(E)).findLast(shown);
120
- if (prev !== undefined)
121
- return { block: prev, offset: c.displayLength(prev) };
122
- const next = ids.slice(at.get(E) + 1).find(shown);
123
- return next === undefined ? null : { block: next, offset: 0 };
124
- })();
125
- // Nothing else left to hold the caret: the head stays, emptied (whole-doc).
126
- if (caret === null)
127
- return prepare(true, headDies)(from, to, view);
128
- return { ...c.plan([caret.block], writes), at: caret };
129
- };
130
- /**
131
- * `table.range`: a range with an end in a cell. Every cell it crosses
132
- * loses the text it covers and stays, with its row; the ends keep what
133
- * lies outside the range, merging nothing; any other block between the
134
- * ends goes (a table wholly between them whole), but the ancestors of
135
- * the end, which stay and lose their text (the range covers it: they
136
- * come after the start in reading order); the caret lands at the start.
137
- */
138
- const inTables = (s, e, view) => {
139
- const { ids, at } = c.view().order;
140
- const [S, E] = [s.block, e.block];
141
- const kept = new Set(c.ancestorsOf(E));
142
- const between = ids.slice(at.get(S) + 1, at.get(E)).filter((id) => !view.hidden?.(id));
143
- const writes = [];
144
- const lenS = c.displayLength(S);
145
- if (lenS > s.offset)
146
- writes.push({ op: 'deleteText', id: S, offset: s.offset, length: lenS - s.offset });
147
- const [tableS, tableE] = [c.tableOf(S), c.tableOf(E)];
148
- const doomed = new Set();
149
- for (const id of between) {
150
- const table = c.tableOf(id);
151
- if (kept.has(id) || (table !== null && (table === tableS || table === tableE))) {
152
- const len = c.isTableCell(id) || kept.has(id) ? c.displayLength(id) : 0;
153
- if (len > 0)
154
- writes.push({ op: 'deleteText', id, offset: 0, length: len });
155
- }
156
- else
157
- doomed.add(id);
158
- }
159
- if (e.offset > 0)
160
- writes.push({ op: 'deleteText', id: E, offset: 0, length: e.offset });
161
- const parentOf = (id) => c.ancestorsOf(id)[0] ?? null;
162
- const roots = [...doomed].filter((id) => !doomed.has(parentOf(id)));
163
- for (const id of roots)
164
- writes.push(c.remove(id, []));
165
- writes.push(...c.dissolving(new Set(roots), [], writes));
166
- return { ...c.plan([S], writes), at: s };
167
- };
168
- return { deleteRange: prepare(false), replaceRange: prepare(true) };
169
- };
1
+ export {};
@@ -26,6 +26,7 @@
26
26
  import type { EngineDoc } from './engine-api.js';
27
27
  import { type BlockId, type ContentItem, type InlineSpec, type PlacementModel } from './placement/model.js';
28
28
  import type { DataPatch } from './data.js';
29
+ import { type SuggestFacade } from './doc/suggest.js';
29
30
  import type { JSONDoc } from '../utils/json.js';
30
31
  /** One display unit: a character (a code point) with its marks, or an inline atom, keyed for comparison. */
31
32
  export type Unit = {
@@ -60,7 +61,7 @@ export type RestoreReport = {
60
61
  deleted: number;
61
62
  };
62
63
  /** What a restore reads and writes through: the facade over the document (structurally). */
63
- export type RestoreFacade<Plan> = {
64
+ export type RestoreFacade<Plan> = Partial<SuggestFacade> & {
64
65
  model: PlacementModel;
65
66
  childSlots(parent: BlockId | null): readonly {
66
67
  id: BlockId;
@@ -1,4 +1,5 @@
1
1
  import { blockNodeOf, insertSpecsAt, moveBlocksTo, view as modelView } from './placement/model.js';
2
+ import { suggestionsOf } from './doc/suggest.js';
2
3
  import { DEL_PREFIX, TYPE, WITHDRAW_PREFIX } from './schema.js';
3
4
  import { common, patchWrites, writeData } from './data.js';
4
5
  import { canonKey, deleteClaims } from './text/model.js';
@@ -196,5 +197,13 @@ export const restoreDocument = (doc, facade, json) => {
196
197
  }
197
198
  // 6 · The document's data.
198
199
  facade.apply(facade.prepare.patchData(null, [{ path: [], value: json.data ?? {} }]));
200
+ // 7 · Pending suggestions are dropped (`sugg.restore`): the version holds
201
+ // none (its document without them), so what the restore left of them —
202
+ // their proposals — is rejected, in the restore's step (its undo brings
203
+ // them back).
204
+ // (Built over the facade, its shared suggestions: `doc/suggest.ts`.)
205
+ const suggest = facade.part && suggestionsOf(facade);
206
+ if (suggest)
207
+ suggest.run(suggest.decide(suggest.changes(), false));
199
208
  return report;
200
209
  };
@@ -49,6 +49,14 @@ export declare const HORIZON_ATTR = "h";
49
49
  * horizon covers.
50
50
  */
51
51
  export declare const RELEASE_ROOT = "released";
52
+ /**
53
+ * The shared suggestions (`crdt/doc/suggest.ts`): attr `<id>` is one
54
+ * suggestion's record node, written by its writer (a client id bound to its
55
+ * author): its creation time `at`, `via` and `lbl`, and a list of id lists
56
+ * (fork patch YP31: its writer's proposed deletions, anyone else's settled
57
+ * ranges) and proposed operations. In the history's scope.
58
+ */
59
+ export declare const SUGGEST_ROOT = "suggest";
52
60
  /** Named node roles (`SCHEMA.nodes`). */
53
61
  export declare const BLOCK_NODE = "block";
54
62
  export declare const CONTENT_NODE = "content";
@@ -92,7 +100,7 @@ export declare const LAST_CHANGED_ATTR = "l";
92
100
  export declare const CONTENT = "content";
93
101
  /** The schema generation stamped on the `meta` root (`v`) and its manifest name (`schema`). */
94
102
  export declare const SCHEMA: {
95
- readonly version: 10;
103
+ readonly version: 11;
96
104
  readonly name: "edytor-doc";
97
105
  readonly roots: {
98
106
  readonly registry: "blocks";
@@ -42,6 +42,14 @@ export const HORIZON_ATTR = 'h';
42
42
  * horizon covers.
43
43
  */
44
44
  export const RELEASE_ROOT = 'released';
45
+ /**
46
+ * The shared suggestions (`crdt/doc/suggest.ts`): attr `<id>` is one
47
+ * suggestion's record node, written by its writer (a client id bound to its
48
+ * author): its creation time `at`, `via` and `lbl`, and a list of id lists
49
+ * (fork patch YP31: its writer's proposed deletions, anyone else's settled
50
+ * ranges) and proposed operations. In the history's scope.
51
+ */
52
+ export const SUGGEST_ROOT = 'suggest';
45
53
  /** Named node roles (`SCHEMA.nodes`). */
46
54
  export const BLOCK_NODE = 'block';
47
55
  export const CONTENT_NODE = 'content';
@@ -86,9 +94,11 @@ export const SCHEMA = {
86
94
  // YP21), a block has no `claims` list; 9: a block's place is its home slot
87
95
  // in its parent's sequence and a move is a move record (fork patch YP22),
88
96
  // a block has no `at` map and an array item no rank; 10: the update encoding
89
- // writes each distinct string once (fork patch YP24). A container or frame
90
- // of an earlier generation is refused.
91
- version: 10,
97
+ // writes each distinct string once (fork patch YP24); 11: a hold record
98
+ // inside a nested node holds nothing (fork patch YP31: a suggestion's
99
+ // proposals and decisions). A container or frame of an earlier generation
100
+ // is refused.
101
+ version: 11,
92
102
  name: 'edytor-doc',
93
103
  roots: { registry: REGISTRY_KEY, meta: META_ROOT_KEY },
94
104
  metaAttrs: { version: 'v', schema: 'schema' }