edytor 5.0.0 → 7.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 (137) hide show
  1. package/README.md +3 -3
  2. package/dist/cloudflare/DocumentRoom.d.ts +77 -2
  3. package/dist/cloudflare/DocumentRoom.js +37 -1
  4. package/dist/cloudflare/index.d.ts +1 -1
  5. package/dist/cloudflare/index.js +1 -1
  6. package/dist/cloudflare/room/access.d.ts +6 -1
  7. package/dist/cloudflare/room/access.js +30 -4
  8. package/dist/cloudflare/room/admission.d.ts +11 -0
  9. package/dist/cloudflare/room/admission.js +43 -10
  10. package/dist/cloudflare/room/context.d.ts +73 -76
  11. package/dist/cloudflare/room/context.js +17 -6
  12. package/dist/cloudflare/room/forged.d.ts +1 -1
  13. package/dist/cloudflare/room/forged.js +11 -9
  14. package/dist/cloudflare/room/frames.d.ts +2 -0
  15. package/dist/cloudflare/room/frames.js +2 -0
  16. package/dist/cloudflare/room/presence.js +1 -4
  17. package/dist/cloudflare/room/reseed.d.ts +93 -0
  18. package/dist/cloudflare/room/reseed.js +500 -0
  19. package/dist/cloudflare/room/scheduler.d.ts +1 -1
  20. package/dist/cloudflare/room/scheduler.js +9 -1
  21. package/dist/cloudflare/room/shared.d.ts +10 -0
  22. package/dist/cloudflare/room/shared.js +10 -0
  23. package/dist/cloudflare/room/storage.d.ts +7 -0
  24. package/dist/cloudflare/room/storage.js +33 -1
  25. package/dist/components/Edytor.svelte +18 -3
  26. package/dist/components/Edytor.svelte.d.ts +10 -1
  27. package/dist/components/EdytorRoot.svelte +17 -0
  28. package/dist/components/EdytorRoot.svelte.d.ts +4 -0
  29. package/dist/components/Suggestion.svelte +1 -1
  30. package/dist/crdt/attribution/block.d.ts +3 -0
  31. package/dist/crdt/attribution/block.js +3 -2
  32. package/dist/crdt/doc/history.js +2 -3
  33. package/dist/crdt/doc/ops.d.ts +81 -0
  34. package/dist/crdt/doc/ops.js +1898 -0
  35. package/dist/crdt/doc/suggest.d.ts +6 -2
  36. package/dist/crdt/doc/suggest.js +6 -5
  37. package/dist/crdt/edytor-doc.d.ts +157 -157
  38. package/dist/crdt/edytor-doc.js +15 -43
  39. package/dist/crdt/engine-room.d.ts +4 -0
  40. package/dist/crdt/engine-room.js +6 -0
  41. package/dist/crdt/flow.d.ts +4 -46
  42. package/dist/crdt/flow.js +2 -262
  43. package/dist/crdt/index.d.ts +64 -75
  44. package/dist/crdt/operations.d.ts +1 -1
  45. package/dist/crdt/placement/resolve.d.ts +1 -1
  46. package/dist/crdt/protocol.d.ts +2 -2
  47. package/dist/crdt/protocol.js +2 -2
  48. package/dist/crdt/protocols/auth.d.ts +8 -1
  49. package/dist/crdt/protocols/auth.js +14 -1
  50. package/dist/crdt/protocols/awareness.js +46 -81
  51. package/dist/crdt/providers/index.d.ts +11 -22
  52. package/dist/crdt/providers/index.js +22 -3
  53. package/dist/crdt/providers/indexeddb.d.ts +6 -10
  54. package/dist/crdt/providers/indexeddb.js +10 -16
  55. package/dist/crdt/providers/room.d.ts +26 -0
  56. package/dist/crdt/providers/room.js +41 -12
  57. package/dist/crdt/providers/websocket.d.ts +10 -14
  58. package/dist/crdt/providers/websocket.js +43 -23
  59. package/dist/crdt/rangeDelete.d.ts +2 -47
  60. package/dist/crdt/rangeDelete.js +1 -169
  61. package/dist/crdt/reseed.d.ts +127 -0
  62. package/dist/crdt/reseed.js +533 -0
  63. package/dist/crdt/restore.d.ts +13 -1
  64. package/dist/crdt/restore.js +8 -3
  65. package/dist/crdt/schema.d.ts +6 -0
  66. package/dist/crdt/schema.js +6 -0
  67. package/dist/crdt/structs.d.ts +8 -11
  68. package/dist/crdt/structs.js +25 -44
  69. package/dist/crdt/text/index/build.d.ts +119 -0
  70. package/dist/crdt/text/index/build.js +2801 -0
  71. package/dist/crdt/text/index/shared.d.ts +2 -6
  72. package/dist/crdt/text/index/shared.js +2 -5
  73. package/dist/crdt/text/model.js +2 -3
  74. package/dist/crdt/text/runs.d.ts +9 -12
  75. package/dist/crdt/text/runs.js +2 -165
  76. package/dist/crdt/vendor/yjs/UPSTREAM.md +101 -5
  77. package/dist/crdt/vendor/yjs/dts/index.d.ts +1 -1
  78. package/dist/crdt/vendor/yjs/dts/utils/Transaction.d.ts +10 -10
  79. package/dist/crdt/vendor/yjs/dts/utils/encoding.d.ts +1 -0
  80. package/dist/crdt/vendor/yjs/src/index.js +1 -1
  81. package/dist/crdt/vendor/yjs/src/structs/Item.js +24 -13
  82. package/dist/crdt/vendor/yjs/src/utils/Transaction.js +12 -10
  83. package/dist/crdt/vendor/yjs/src/utils/encoding.js +42 -1
  84. package/dist/events/domTextOffset.d.ts +2 -0
  85. package/dist/events/domTextOffset.js +8 -0
  86. package/dist/export/forms.d.ts +50 -50
  87. package/dist/index.d.ts +1 -1
  88. package/dist/index.js +1 -1
  89. package/dist/plugins/richtext/RichTextPlugin.svelte +63 -61
  90. package/dist/plugins/richtext/richTextOperations.js +26 -32
  91. package/dist/selection/selection.svelte.d.ts +1 -1
  92. package/dist/selection/selection.svelte.js +87 -106
  93. package/dist/selection/selection.utils.js +5 -8
  94. package/dist/session/commands.js +49 -51
  95. package/dist/session/handles.js +3 -7
  96. package/dist/surface/attributes.d.ts +3 -3
  97. package/dist/surface/attributes.js +2 -2
  98. package/dist/surface/observer.svelte.d.ts +7 -7
  99. package/dist/surface/observer.svelte.js +225 -336
  100. package/dist/surface/projector.svelte.js +2 -2
  101. package/dist/utils.d.ts +5 -0
  102. package/dist/utils.js +7 -0
  103. package/package.json +1 -1
  104. package/dist/crdt/doc/capability.d.ts +0 -56
  105. package/dist/crdt/doc/capability.js +0 -207
  106. package/dist/crdt/doc/content.d.ts +0 -16
  107. package/dist/crdt/doc/content.js +0 -77
  108. package/dist/crdt/doc/delete.d.ts +0 -15
  109. package/dist/crdt/doc/delete.js +0 -84
  110. package/dist/crdt/doc/layout.d.ts +0 -9
  111. package/dist/crdt/doc/layout.js +0 -178
  112. package/dist/crdt/doc/meta.d.ts +0 -19
  113. package/dist/crdt/doc/meta.js +0 -198
  114. package/dist/crdt/doc/moves.d.ts +0 -21
  115. package/dist/crdt/doc/moves.js +0 -274
  116. package/dist/crdt/doc/split-merge.d.ts +0 -10
  117. package/dist/crdt/doc/split-merge.js +0 -179
  118. package/dist/crdt/doc/steps.d.ts +0 -65
  119. package/dist/crdt/doc/steps.js +0 -311
  120. package/dist/crdt/text/index/cache.d.ts +0 -16
  121. package/dist/crdt/text/index/cache.js +0 -175
  122. package/dist/crdt/text/index/claims.d.ts +0 -18
  123. package/dist/crdt/text/index/claims.js +0 -203
  124. package/dist/crdt/text/index/fold.d.ts +0 -21
  125. package/dist/crdt/text/index/fold.js +0 -656
  126. package/dist/crdt/text/index/layout.d.ts +0 -22
  127. package/dist/crdt/text/index/layout.js +0 -258
  128. package/dist/crdt/text/index/placement.d.ts +0 -47
  129. package/dist/crdt/text/index/placement.js +0 -738
  130. package/dist/crdt/text/index/records.d.ts +0 -13
  131. package/dist/crdt/text/index/records.js +0 -139
  132. package/dist/crdt/text/index/report.d.ts +0 -18
  133. package/dist/crdt/text/index/report.js +0 -337
  134. package/dist/crdt/text/index/state.d.ts +0 -197
  135. package/dist/crdt/text/index/state.js +0 -116
  136. package/dist/crdt/text/index/streams.d.ts +0 -25
  137. package/dist/crdt/text/index/streams.js +0 -248
@@ -63,6 +63,12 @@ export const messageChunk = 5;
63
63
  * room sends it only to a socket that subscribed.
64
64
  */
65
65
  export const messageComments = 6;
66
+ /**
67
+ * Client → server, after a re-seed (`room.reseed.protocol`): the client's
68
+ * whole old document, its unsaved edits in it, for the room to rebase onto
69
+ * the current epoch. Sent only when the room asks (its epoch notice).
70
+ */
71
+ export const messageRebase = 7;
66
72
  /** A WebSocket message limit on Cloudflare (32 MiB): a larger frame is sent as chunks. */
67
73
  export const MAX_FRAME_BYTES = 32 * 1024 * 1024;
68
74
  const chunkStart = 0;
@@ -293,6 +299,10 @@ export const markSynced = (host) => {
293
299
  host._settleSynced[0](host);
294
300
  return true;
295
301
  };
302
+ /** Report `error` on `provider`'s error channel (`'message-error'`): a frame, a payload or a storage failure. */
303
+ export const fault = (provider, error) => provider.emit?.('message-error', [error, provider]);
304
+ /** A message type no handler claims inside a valid envelope: protocol skew, reported, never dropped silently. */
305
+ export const unknownType = (provider, what, type) => fault(provider, new Error(`Unknown ${what} message type ${type}`));
296
306
  /**
297
307
  * Terminal `'failed'` emission (the sync-failure contract): fires at
298
308
  * most once per provider, only while it has never synced — destroy before
@@ -330,6 +340,15 @@ export const CLOSE = {
330
340
  denied: 4403,
331
341
  /** The dialed replica is bound to another user. */
332
342
  replicaTaken: 4409,
343
+ /**
344
+ * The room re-seeded the document (`room.reseed.protocol`; the reason is
345
+ * the room's epoch): a refusal of this document, once a client with
346
+ * unsaved edits redialed and handed them over. The new epoch is a new
347
+ * document's. Also the answer to a copy the room cannot rebase (an
348
+ * epoch past the cuts it keeps, or a later one) and to an older epoch's
349
+ * content on a socket of the current one.
350
+ */
351
+ reseeded: 4410,
333
352
  /**
334
353
  * A room quota refused the socket's write (`quota: document`,
335
354
  * `quota: rate`, `quota: frame`): the frame was not applied, and a
@@ -425,13 +444,13 @@ export const bindRoomProtocol = (syncProtocol, behavior) => {
425
444
  // (`discarded`) and reported the same way.
426
445
  const update = decoding.readVarUint8Array(decoder);
427
446
  const origin = emitSynced ? provider : (behavior.tabOrigin?.(provider) ?? provider);
428
- const onError = (error) => provider.emit?.('message-error', [error, provider]);
447
+ const onError = (error) => fault(provider, error);
429
448
  const { applied, problem, discarded } = syncProtocol.applyRemote(provider.doc, update, origin, onError);
430
449
  const refused = problem ?? discarded;
431
450
  if (refused) {
432
451
  const docName = behavior.docName(provider);
433
452
  provider.emit?.('schema-mismatch', [{ docName, problem: refused }, provider]);
434
- provider.emit?.('message-error', [new SchemaMismatchError(docName, refused), provider]);
453
+ fault(provider, new SchemaMismatchError(docName, refused));
435
454
  }
436
455
  // Only an ACCEPTED Step2 is a held member state — a refused
437
456
  // one never claims the handshake.
@@ -441,10 +460,7 @@ export const bindRoomProtocol = (syncProtocol, behavior) => {
441
460
  return;
442
461
  }
443
462
  // Unknown sync subtype inside a valid envelope — observable.
444
- provider.emit?.('message-error', [
445
- new Error(`Unknown sync message type ${syncMessageType}`),
446
- provider
447
- ]);
463
+ unknownType(provider, 'sync', syncMessageType);
448
464
  },
449
465
  [messageQueryAwareness]: (encoder, _decoder, provider) => {
450
466
  encoding.writeVarUint(encoder, messageAwareness);
@@ -477,10 +493,7 @@ export const bindRoomProtocol = (syncProtocol, behavior) => {
477
493
  if (!messageHandler) {
478
494
  // Fail closed + observable: a VALID envelope carrying a message
479
495
  // 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
- ]);
496
+ unknownType(provider, 'v14', messageType);
484
497
  return encoder;
485
498
  }
486
499
  const followUp = messageHandler(encoder, decoder, provider, emitSynced);
@@ -503,7 +516,7 @@ export const bindRoomProtocol = (syncProtocol, behavior) => {
503
516
  catch (error) {
504
517
  // same-tab publishes are delivered synchronously — a malformed
505
518
  // message must not propagate into the publisher's call stack.
506
- provider.emit?.('message-error', [error, provider]);
519
+ fault(provider, error);
507
520
  }
508
521
  };
509
522
  };
@@ -554,6 +567,21 @@ export const bindRoomProtocol = (syncProtocol, behavior) => {
554
567
  };
555
568
  /** The departure announcement — our presence, removed — on every transport the provider speaks. */
556
569
  const depart = (provider) => behavior.broadcast(provider, awarenessFrame(provider.awareness, [provider.doc.clientID], new Map()));
570
+ /**
571
+ * Wire `provider` to its room: its BroadcastChannel subscriber, `onUpdate`
572
+ * on its document's updates and its presence changes to the room
573
+ * (`awarenessUpdateHandler`). The teardown removes both listeners.
574
+ */
575
+ const listen = (provider, onUpdate) => {
576
+ provider._bcSubscriber = bcSubscriber(provider);
577
+ const onPresence = awarenessUpdateHandler(provider);
578
+ provider.doc.on('update', onUpdate);
579
+ provider.awareness.on('update', onPresence);
580
+ return () => {
581
+ provider.awareness.off('update', onPresence);
582
+ provider.doc.off('update', onUpdate);
583
+ };
584
+ };
557
585
  /** Leave the room: announce the presence removal, then unsubscribe the BC channel. */
558
586
  const disconnectBc = (provider) => {
559
587
  depart(provider);
@@ -572,6 +600,7 @@ export const bindRoomProtocol = (syncProtocol, behavior) => {
572
600
  hello,
573
601
  depart,
574
602
  connectBc,
575
- disconnectBc
603
+ disconnectBc,
604
+ listen
576
605
  };
577
606
  };
@@ -174,6 +174,8 @@ export declare const bindWebsocketProvider: (Y: EngineApi) => {
174
174
  /** The room asks a read-only socket for its state again: what this actor wrote is unsaved until stored. */
175
175
  allow(): void;
176
176
  };
177
+ /** @internal The room re-seeded: this document's unsaved edits are being handed over (`room.reseed.protocol`). */
178
+ _rebasing: boolean;
177
179
  messageHandlers: Record<number, RoomMessageHandler</*elided*/ any>>;
178
180
  hasSynced: boolean;
179
181
  whenSynced: Promise<unknown>;
@@ -185,22 +187,16 @@ export declare const bindWebsocketProvider: (Y: EngineApi) => {
185
187
  /** Closes the current dial if it has not opened in time. */
186
188
  _connectTimer: ReturnType<typeof setTimeout> | undefined;
187
189
  _checkInterval: ReturnType<typeof setInterval>;
188
- _updateHandler: (update: Uint8Array, origin: unknown, doc?: unknown, transaction?: {
189
- local: boolean;
190
- }) => void;
191
- _awarenessUpdateHandler: (updates: {
192
- added: number[];
193
- updated: number[];
194
- removed: number[];
195
- }, origin: unknown) => void;
190
+ /** @internal Removes the document and presence listeners (`room.listen`). */
191
+ _unlisten: () => void;
196
192
  /**
197
193
  * The dial URL: the room name as one encoded path segment (any id —
198
194
  * `/`, `%`, `#`, `?` included — reaches its own room; the server
199
195
  * decodes it). `''`, `.`, `..`, an id over 256 characters and one with a
200
196
  * lone surrogate are refused at construction ({@link assertRoomId}). `params` are read at every dial (a refreshed token
201
197
  * reaches the next connection), with `replica` = this document's
202
- * client id unless `params` names one: the room binds the socket's
203
- * writes to it.
198
+ * client id unless `params` names one (the room binds the socket's
199
+ * writes to it), and `epoch`, the document's, which no param replaces.
204
200
  */
205
201
  get url(): string;
206
202
  /** Updates of this document's actor the room has not acknowledged as persisted yet. */
@@ -223,11 +219,11 @@ export declare const bindWebsocketProvider: (Y: EngineApi) => {
223
219
  destroy(): void;
224
220
  disconnect(): void;
225
221
  connect(): void;
226
- 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;
222
+ 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;
227
223
  _observers: Map<string, Set<any>>;
228
- 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];
229
- 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;
230
- 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;
224
+ 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];
225
+ 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;
226
+ 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;
231
227
  };
232
228
  };
233
229
  watchComments: (server: string | null, roomName: string, listener: (message: CommentMessage) => void) => (() => void);
@@ -47,18 +47,19 @@
47
47
  * there (TCP never does).
48
48
  */
49
49
  import * as decoding from 'lib0-v14/decoding';
50
+ import * as encoding from 'lib0-v14/encoding';
50
51
  import * as time from 'lib0-v14/time';
51
52
  import * as math from 'lib0-v14/math';
52
53
  import * as url from 'lib0-v14/url';
53
54
  import { Awareness, removeAwarenessStates } from '../protocols/awareness.js';
54
55
  import { IsolatedObservable } from '../protocols/observable.js';
55
- import { messagePermissionDenied, messageAccess, messageReadOnly, READ_ONLY_DENIAL, SUGGEST_ONLY_DENIAL, readAuthMessage } from '../protocols/auth.js';
56
+ import { messagePermissionDenied, messageAccess, messageEpoch, messageReadOnly, READ_ONLY_DENIAL, SUGGEST_ONLY_DENIAL, readAuthMessage } from '../protocols/auth.js';
56
57
  import { bindSync } from '../protocols/sync.js';
57
58
  import { readCommentsMessage, writeCommentsSubscribe } from '../protocols/comments.js';
58
59
  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';
60
+ import { assertRoomId, beginDestroy, bindRoomProtocol, tabs, chunkFrame, CLOSE, createChunkReader, emitFailed, fault, initLifecycle, isRefusal, unknownType, LOCAL_PRESENCE_LOSS, markSynced, messageAuth, messageChunk, messageComments, messageRebase, messageSaved, messageSync, MAX_FRAME_BYTES, SyncRefusedError } from './room.js';
60
61
  import { asEngineDoc } from '../structs.js';
61
- import { ATTRIBUTION_ROOT } from '../schema.js';
62
+ import { ATTRIBUTION_ROOT, EPOCH_ROOT } from '../schema.js';
62
63
  import { ATTRIBUTION_ORIGIN, writerOf } from '../attribution/attribution.js';
63
64
  // @todo - this should depend on awareness.outdatedTime
64
65
  const messageReconnectTimeout = 30000;
@@ -129,16 +130,20 @@ export const bindWebsocketProvider = (Y) => {
129
130
  // The room's notice at the join: a state, not a refusal (no event).
130
131
  () => provider._writes.deny(),
131
132
  // The room's word at the join, and at a change in place (`read`).
132
- (access) => provider.emit('access', [access, provider]));
133
+ (access) => provider.emit('access', [access, provider]),
134
+ // The room re-seeded since this document's epoch: it asks for the
135
+ // document, its unsaved edits in it (`room.reseed.protocol`).
136
+ () => {
137
+ provider._rebasing = true;
138
+ send(provider, frame(messageRebase, (e) => encoding.writeVarUint8Array(e, Y.encodeStateAsUpdate(provider.doc))));
139
+ });
133
140
  if (authType !== messagePermissionDenied &&
134
141
  authType !== messageReadOnly &&
135
- authType !== messageAccess) {
142
+ authType !== messageAccess &&
143
+ authType !== messageEpoch) {
136
144
  // An auth type we do not speak inside a valid v14 envelope is
137
145
  // protocol skew — reported, never dropped silently.
138
- provider.emit('message-error', [
139
- new Error(`Unknown auth message type ${authType}`),
140
- provider
141
- ]);
146
+ unknownType(provider, 'auth', authType);
142
147
  }
143
148
  }
144
149
  },
@@ -403,6 +408,13 @@ export const bindWebsocketProvider = (Y) => {
403
408
  provider.awareness.meta.delete(client);
404
409
  provider.emit('status', [{ status: 'disconnected' }]);
405
410
  }
411
+ // A re-seed (`room.reseed.protocol`): unsaved edits are handed over first,
412
+ // by a dial of this document's epoch, older now, which the room asks for
413
+ // them. Then, or with none, it is a refusal: the new epoch is a new document's.
414
+ if (event?.code === CLOSE.reseeded && provider.unsaved > 0 && !provider._rebasing) {
415
+ provider._rebasing = true;
416
+ return setupWS(provider);
417
+ }
406
418
  if (event && isRefusal(event.code)) {
407
419
  // Terminal: the next dial would be refused the same way.
408
420
  provider.shouldConnect = false;
@@ -459,7 +471,7 @@ export const bindWebsocketProvider = (Y) => {
459
471
  room.readMessage(provider, new Uint8Array(event.data), true, (buf) => sendOn(provider, websocket, buf));
460
472
  }
461
473
  catch (error) {
462
- provider.emit('message-error', [error, provider]);
474
+ fault(provider, error);
463
475
  }
464
476
  };
465
477
  websocket.onerror = (event) => {
@@ -514,6 +526,8 @@ export const bindWebsocketProvider = (Y) => {
514
526
  _chunks = createChunkReader();
515
527
  /** This actor's writes pending the room's acknowledgement. */
516
528
  _writes;
529
+ /** @internal The room re-seeded: this document's unsaved edits are being handed over (`room.reseed.protocol`). */
530
+ _rebasing = false;
517
531
  messageHandlers = room.messageHandlers;
518
532
  // The room lifecycle — installed by `initLifecycle`.
519
533
  hasSynced;
@@ -526,8 +540,8 @@ export const bindWebsocketProvider = (Y) => {
526
540
  /** Closes the current dial if it has not opened in time. */
527
541
  _connectTimer;
528
542
  _checkInterval;
529
- _updateHandler;
530
- _awarenessUpdateHandler;
543
+ /** @internal Removes the document and presence listeners (`room.listen`). */
544
+ _unlisten;
531
545
  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 } = {}) {
532
546
  assertRoomId(roomname);
533
547
  super();
@@ -548,14 +562,13 @@ export const bindWebsocketProvider = (Y) => {
548
562
  if (resyncInterval > 0) {
549
563
  this._resync = setInterval(() => send(this, room.step1(this)), resyncInterval);
550
564
  }
551
- this._bcSubscriber = room.bcSubscriber(this);
552
565
  providers.add(this);
553
566
  // Local doc updates go to the room; another tab's go to the server
554
567
  // only (`broadcastUpdate` quarantines a read-only document). A
555
568
  // socket the room made read-only gets this document's own edits
556
569
  // alone (each is denied): what a tab or a store delivered, and the
557
570
  // actor records, would draw denials nobody made.
558
- this._updateHandler = (update, origin, _doc, transaction) => {
571
+ const onUpdate = (update, origin, _doc, transaction) => {
559
572
  if (origin === this)
560
573
  return;
561
574
  const heard = origin === this._fromTab;
@@ -573,9 +586,7 @@ export const bindWebsocketProvider = (Y) => {
573
586
  };
574
587
  this._writes = new OwnWrites(doc, (unsaved) => this.emit('saved', [{ saved: unsaved === 0, unsaved }, this]));
575
588
  this._writes.trackDocument();
576
- this.doc.on('update', this._updateHandler);
577
- this._awarenessUpdateHandler = room.awarenessUpdateHandler(this);
578
- awareness.on('update', this._awarenessUpdateHandler);
589
+ this._unlisten = room.listen(this, onUpdate);
579
590
  this._checkInterval = setInterval(() => {
580
591
  if (!this.wsconnected)
581
592
  return;
@@ -602,12 +613,22 @@ export const bindWebsocketProvider = (Y) => {
602
613
  * decodes it). `''`, `.`, `..`, an id over 256 characters and one with a
603
614
  * lone surrogate are refused at construction ({@link assertRoomId}). `params` are read at every dial (a refreshed token
604
615
  * reaches the next connection), with `replica` = this document's
605
- * client id unless `params` names one: the room binds the socket's
606
- * writes to it.
616
+ * client id unless `params` names one (the room binds the socket's
617
+ * writes to it), and `epoch`, the document's, which no param replaces.
607
618
  */
608
619
  get url() {
609
- const query = url.encodeQueryParams({ replica: String(this.doc.clientID), ...this.params });
610
- return `${this.serverUrl}/${encodeURIComponent(this.roomname)}?${query}`;
620
+ // The document's epoch (`new`: it holds nothing yet), which the room checks (`room.reseed.protocol`).
621
+ const epoch = this.doc.store.clients.size
622
+ ? String(asEngineDoc(this.doc).get(EPOCH_ROOT).getAttr('n') ?? 0)
623
+ : 'new';
624
+ const query = {
625
+ replica: String(this.doc.clientID),
626
+ epoch,
627
+ ...this.params
628
+ };
629
+ // In its place, but no param of the app's names it instead.
630
+ query.epoch = epoch;
631
+ return `${this.serverUrl}/${encodeURIComponent(this.roomname)}?${url.encodeQueryParams(query)}`;
611
632
  }
612
633
  /** Updates of this document's actor the room has not acknowledged as persisted yet. */
613
634
  get unsaved() {
@@ -651,8 +672,7 @@ export const bindWebsocketProvider = (Y) => {
651
672
  clearInterval(this._checkInterval);
652
673
  providers.delete(this);
653
674
  this.disconnect();
654
- this.awareness.off('update', this._awarenessUpdateHandler);
655
- this.doc.off('update', this._updateHandler);
675
+ this._unlisten();
656
676
  super.destroy();
657
677
  }
658
678
  disconnect() {
@@ -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 {};