edytor 6.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 (76) 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 +11 -2
  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/suggest.d.ts +6 -2
  33. package/dist/crdt/doc/suggest.js +6 -5
  34. package/dist/crdt/edytor-doc.d.ts +1 -1
  35. package/dist/crdt/edytor-doc.js +1 -1
  36. package/dist/crdt/engine-room.d.ts +4 -0
  37. package/dist/crdt/engine-room.js +6 -0
  38. package/dist/crdt/index.d.ts +2 -1
  39. package/dist/crdt/protocol.d.ts +2 -2
  40. package/dist/crdt/protocol.js +2 -2
  41. package/dist/crdt/protocols/auth.d.ts +8 -1
  42. package/dist/crdt/protocols/auth.js +14 -1
  43. package/dist/crdt/providers/index.d.ts +1 -0
  44. package/dist/crdt/providers/index.js +22 -3
  45. package/dist/crdt/providers/room.d.ts +15 -0
  46. package/dist/crdt/providers/room.js +15 -0
  47. package/dist/crdt/providers/websocket.d.ts +4 -2
  48. package/dist/crdt/providers/websocket.js +36 -9
  49. package/dist/crdt/reseed.d.ts +127 -0
  50. package/dist/crdt/reseed.js +533 -0
  51. package/dist/crdt/restore.d.ts +13 -1
  52. package/dist/crdt/restore.js +8 -3
  53. package/dist/crdt/schema.d.ts +6 -0
  54. package/dist/crdt/schema.js +6 -0
  55. package/dist/crdt/structs.d.ts +8 -11
  56. package/dist/crdt/structs.js +25 -44
  57. package/dist/crdt/text/index/build.d.ts +3 -9
  58. package/dist/crdt/text/index/build.js +87 -181
  59. package/dist/crdt/vendor/yjs/UPSTREAM.md +101 -5
  60. package/dist/crdt/vendor/yjs/dts/index.d.ts +1 -1
  61. package/dist/crdt/vendor/yjs/dts/utils/Transaction.d.ts +10 -10
  62. package/dist/crdt/vendor/yjs/dts/utils/encoding.d.ts +1 -0
  63. package/dist/crdt/vendor/yjs/src/index.js +1 -1
  64. package/dist/crdt/vendor/yjs/src/structs/Item.js +24 -13
  65. package/dist/crdt/vendor/yjs/src/utils/Transaction.js +12 -10
  66. package/dist/crdt/vendor/yjs/src/utils/encoding.js +42 -1
  67. package/dist/index.d.ts +1 -1
  68. package/dist/index.js +1 -1
  69. package/dist/selection/selection.svelte.d.ts +1 -1
  70. package/dist/selection/selection.svelte.js +87 -106
  71. package/dist/session/commands.js +49 -51
  72. package/dist/surface/attributes.d.ts +3 -3
  73. package/dist/surface/attributes.js +2 -2
  74. package/dist/surface/observer.svelte.d.ts +7 -7
  75. package/dist/surface/observer.svelte.js +213 -309
  76. package/package.json +1 -1
@@ -193,6 +193,8 @@
193
193
  params,
194
194
  onSyncExpired,
195
195
  onSyncRefused,
196
+ rebuild,
197
+ adopt,
196
198
  requireHydration,
197
199
  snapshot,
198
200
  actor,
@@ -208,7 +210,15 @@
208
210
  autocapitalize = 'none',
209
211
  inputmode,
210
212
  enterkeyhint
211
- }: EdytorProps = $props();
213
+ }: EdytorProps & {
214
+ /** The view's own rebuild (`EdytorRoot`): a new view on a new document. */
215
+ rebuild?: () => void;
216
+ /**
217
+ * Hands the view to `EdytorRoot`, which a binding cannot: on the server a
218
+ * bound prop renders the tree twice, and the discarded view never ends.
219
+ */
220
+ adopt?: (view: EdytorClass) => void;
221
+ } = $props();
212
222
 
213
223
  const initialEdytorOptions = untrack(() => ({
214
224
  readonly,
@@ -227,6 +237,7 @@
227
237
  }));
228
238
 
229
239
  edytor = new EdytorClass(initialEdytorOptions);
240
+ untrack(() => adopt)?.(edytor);
230
241
 
231
242
  // ONE attach path for owned and injected documents: `attachSync`
232
243
  // tracks the provider on the DOCUMENT's lifetime (one provider per
@@ -276,7 +287,11 @@
276
287
  }
277
288
  // Subscribe first, then replay the standing refusal isolated like the
278
289
  // later ones: a throwing callback still hears the next refusal.
279
- offRefused = document.onSyncRefused((next) => onSyncRefused?.(next));
290
+ offRefused = document.onSyncRefused((next) => {
291
+ onSyncRefused?.(next);
292
+ // A re-seed (`4410`): a view that owns its document builds itself again on a new one.
293
+ if (next.code === CLOSE.reseeded && !edytorDocument) rebuild?.();
294
+ });
280
295
  if (refusal) {
281
296
  try {
282
297
  onSyncRefused?.(refusal);
@@ -379,7 +394,7 @@
379
394
  contenteditable="false"
380
395
  aria-hidden="true"
381
396
  style="display: none"
382
- use:edytor.surface.anchor
397
+ use:edytor.surface.own
383
398
  ></span>
384
399
  </div>
385
400
  {:else if snapshot}
@@ -113,6 +113,15 @@ export type EdytorProps = {
113
113
  import type { JSONDoc } from '../utils/json.js';
114
114
  import type { HotKey, HotKeyCombination } from '../session/keymap.js';
115
115
  import type { EdytorSelection } from '../selection/selection.svelte.js';
116
- declare const Edytor: import("svelte").Component<EdytorProps, {}, "edytor">;
116
+ type $$ComponentProps = EdytorProps & {
117
+ /** The view's own rebuild (`EdytorRoot`): a new view on a new document. */
118
+ rebuild?: () => void;
119
+ /**
120
+ * Hands the view to `EdytorRoot`, which a binding cannot: on the server a
121
+ * bound prop renders the tree twice, and the discarded view never ends.
122
+ */
123
+ adopt?: (view: EdytorClass) => void;
124
+ };
125
+ declare const Edytor: import("svelte").Component<$$ComponentProps, {}, "edytor">;
117
126
  type Edytor = ReturnType<typeof Edytor>;
118
127
  export default Edytor;
@@ -0,0 +1,17 @@
1
+ <script lang="ts">
2
+ // The public `<Edytor>`: the view, built again on a new document when the
3
+ // room re-seeds the one it owns (`room.reseed.protocol`).
4
+ import Edytor, { type EdytorProps } from './Edytor.svelte';
5
+
6
+ // eslint-disable-next-line no-useless-assignment -- bindable: `adopt` assigns it
7
+ let { edytor = $bindable(), ...props }: EdytorProps = $props();
8
+ let epoch = $state(0);
9
+ </script>
10
+
11
+ <!-- A view built again loads the room's document: it never seeds its value over it. -->
12
+ {#key epoch}<Edytor
13
+ {...props}
14
+ requireHydration={epoch > 0 || props.requireHydration}
15
+ adopt={(view) => (edytor = view)}
16
+ rebuild={() => epoch++}
17
+ />{/key}
@@ -0,0 +1,4 @@
1
+ import { type EdytorProps } from './Edytor.svelte';
2
+ declare const EdytorRoot: import("svelte").Component<EdytorProps, {}, "edytor">;
3
+ type EdytorRoot = ReturnType<typeof EdytorRoot>;
4
+ export default EdytorRoot;
@@ -31,7 +31,7 @@
31
31
  contenteditable="false"
32
32
  aria-label={suggestion.label ?? labelsOf(edytor, 'editor', editorEnglish).suggestion}
33
33
  role="group"
34
- use:edytor.surface.preview={suggestion.id}
34
+ use:edytor.surface.own={suggestion.id}
35
35
  onmousedown={cancel}
36
36
  >
37
37
  {#each cells as cell (cell.id)}<Block kid={cell} />{/each}
@@ -78,6 +78,9 @@ export type BlockAttribution = {
78
78
  lastChangedBy?: ActorId;
79
79
  };
80
80
  export { BLOCK_ATTR_ROOT, LAST_CHANGED_ATTR };
81
+ /** A record's `createdBy` key, and the prefix of its contributors' (a re-seed copies them: `crdt/reseed.ts`). */
82
+ export declare const CREATED_KEY = "c";
83
+ export declare const CONTRIBUTOR_PREFIX = "k/";
81
84
  /** The `k/` member set on a `b/` record node. */
82
85
  export declare const contributorsOfRecord: (rec: EngineNode | null) => Set<ActorId>;
83
86
  /**
@@ -5,8 +5,9 @@ import { attrItems, newNode as makeNode } from '../structs.js';
5
5
  // `../schema.js` leaf — re-exported so existing import sites stay.
6
6
  export { BLOCK_ATTR_ROOT, LAST_CHANGED_ATTR };
7
7
  const REC_NODE = 'brec';
8
- const CREATED_KEY = 'c';
9
- const CONTRIBUTOR_PREFIX = 'k/';
8
+ /** A record's `createdBy` key, and the prefix of its contributors' (a re-seed copies them: `crdt/reseed.ts`). */
9
+ export const CREATED_KEY = 'c';
10
+ export const CONTRIBUTOR_PREFIX = 'k/';
10
11
  /** Incarnation stamp on a record: the nonce `n` of the node it belongs to. */
11
12
  const INCARNATION_KEY = 'i';
12
13
  const recordOf = (doc, id) => {
@@ -89,8 +89,12 @@ export type SuggestContext = DocBase & DocReads & {
89
89
  create: (doc: EngineDoc, config: EdytorDocConfig) => SuggestFacade;
90
90
  /** Its changes no index fact shows: counted in the facade's `version`. */
91
91
  v: number;
92
- /** The export `toJSON()` gives unless asked for the suggestions. */
93
- base: (() => JSONDoc | undefined) | undefined;
92
+ /**
93
+ * The document without its pending suggestions, read by `read` (the
94
+ * export `toJSON()` gives unless asked for the suggestions);
95
+ * `undefined` while none is pending.
96
+ */
97
+ base: (<T>(read: (other: SuggestFacade) => T) => T | undefined) | undefined;
94
98
  };
95
99
  /** The facade the part is built over: its `part`, and the operations it writes through. */
96
100
  export type SuggestFacade = {
@@ -526,10 +526,11 @@ const docSuggest = (f) => {
526
526
  return [...map.keys()].sort(c.compare).flatMap((b) => map.get(b));
527
527
  };
528
528
  /**
529
- * The document without its pending suggestions (`sugg.json.base`): on a
530
- * copy, every pending one rejected; `undefined` while none is pending.
529
+ * The document without its pending suggestions (`sugg.json.base`), read
530
+ * by `read`: on a copy, every pending one rejected; `undefined` while
531
+ * none is pending.
531
532
  */
532
- const base = () => {
533
+ const base = (read) => {
533
534
  if (root().attrKeys().next().done || !changes().length)
534
535
  return;
535
536
  const { Y } = c;
@@ -538,11 +539,11 @@ const docSuggest = (f) => {
538
539
  const other = c.create(copy, { ...c.config, actor: undefined });
539
540
  const all = suggestionsOf(other);
540
541
  all.run(all.decide(all.changes(), false));
541
- const json = other.toJSON({ suggestions: true });
542
+ const out = read(other);
542
543
  all.dispose();
543
544
  other.dispose();
544
545
  copy.destroy();
545
- return json;
546
+ return out;
546
547
  };
547
548
  c.base = base;
548
549
  return {
@@ -1032,7 +1032,7 @@ export declare const bindEdytorDoc: (Y: EngineApi) => {
1032
1032
  * Make the visible document `doc` equal `json`, keeping the ids the
1033
1033
  * registry holds and writing only what differs (`room.history.restore`), inside the caller's transaction.
1034
1034
  */
1035
- restoreTo: <Plan>(doc: EngineDoc, facade: import("./restore.js").RestoreFacade<Plan>, json: JSONDoc) => import("./restore.js").RestoreReport;
1035
+ restoreTo: <Plan>(doc: EngineDoc, facade: import("./restore.js").RestoreFacade<Plan>, json: JSONDoc, { rewrite, suggestions }?: import("./restore.js").RestoreOptions<Plan>) => import("./restore.js").RestoreReport;
1036
1036
  model: {
1037
1037
  REGISTRY_KEY: string;
1038
1038
  registryOf: (doc: EngineDoc) => EngineNode;
@@ -220,7 +220,7 @@ export const docFacade = (Y, layers) => {
220
220
  */
221
221
  const toJSON = (options) => {
222
222
  // A part's export first (the shared suggestions': without the pending ones, `sugg.json.base`).
223
- const base = !options?.suggestions && part.base?.();
223
+ const base = !options?.suggestions && part.base?.((other) => other.toJSON({ suggestions: true }));
224
224
  if (base)
225
225
  return base;
226
226
  const data = docData();
@@ -0,0 +1,4 @@
1
+ import type * as V from './vendor/yjs/dts/index.js';
2
+
3
+ export declare const clipUpdate: typeof V.clipUpdate;
4
+ export declare const diffUpdate: typeof V.diffUpdate;
@@ -0,0 +1,6 @@
1
+ /**
2
+ * The engine calls only the room makes (a re-seed's cut and rebase,
3
+ * `crdt/reseed.ts`): apart from the engine object (`engine.js`), which every
4
+ * app keeps whole, so an app's bundle holds none of them.
5
+ */
6
+ export { clipUpdate, diffUpdate } from './vendor/yjs/src/index.js';
@@ -360,7 +360,7 @@ export declare const bindCrdt: (Y: EngineApi) => {
360
360
  text: import("./text/model.js").TextEngine;
361
361
  }, horizon: import("./purge.js").Horizon | null) => import("./purge.js").PurgeReport;
362
362
  horizonOf: (doc: import("./engine-api.js").EngineDoc) => import("./purge.js").Horizon | null;
363
- restoreTo: <Plan>(doc: import("./engine-api.js").EngineDoc, facade: import("./restore.js").RestoreFacade<Plan>, json: import("./index.js").JSONDoc) => import("./restore.js").RestoreReport;
363
+ restoreTo: <Plan>(doc: import("./engine-api.js").EngineDoc, facade: import("./restore.js").RestoreFacade<Plan>, json: import("./index.js").JSONDoc, { rewrite, suggestions }?: import("./restore.js").RestoreOptions<Plan>) => import("./restore.js").RestoreReport;
364
364
  model: {
365
365
  REGISTRY_KEY: string;
366
366
  registryOf: (doc: import("./engine-api.js").EngineDoc) => import("./engine-api.js").EngineNode;
@@ -551,6 +551,7 @@ export declare const bindCrdt: (Y: EngineApi) => {
551
551
  deny(): void;
552
552
  allow(): void;
553
553
  };
554
+ _rebasing: boolean;
554
555
  messageHandlers: Record<number, import("./providers/room.js").RoomMessageHandler</*elided*/ any>>;
555
556
  hasSynced: boolean;
556
557
  whenSynced: Promise<unknown>;
@@ -27,9 +27,9 @@
27
27
  export { bindCrdt, type Crdt, type EngineApi, type EngineDoc, type EngineNode, type EngineTransaction, type EngineItemRef, type YNode, type YItem, type YDoc, type YUndoManager, type YTransaction } from './index.js';
28
28
  export { SCHEMA_VERSION, META_KEY } from './edytor-doc.js';
29
29
  export { GENERATION, generationWord, frame, PROTOCOL_VERSION, GENERATION_RECORD, GenerationMismatchError, readProtocolVersion, type GenerationRecord } from './protocols/envelope.js';
30
- export { messageSync, messageAwareness, messageAuth, messageQueryAwareness, messageSaved, messageChunk, messageComments, MAX_FRAME_BYTES, chunkFrame, createChunkReader, ChunkError, type ChunkReader, CLOSE } from './providers/room.js';
30
+ export { messageSync, messageAwareness, messageAuth, messageQueryAwareness, messageSaved, messageChunk, messageComments, messageRebase, MAX_FRAME_BYTES, chunkFrame, createChunkReader, ChunkError, type ChunkReader, CLOSE } from './providers/room.js';
31
31
  export { messageYjsSyncStep1, messageYjsSyncStep2, messageYjsUpdate, type SyncProtocol } from './protocols/sync.js';
32
- export { messageAccess, messagePermissionDenied, messageReadOnly, writeAccess, writePermissionDenied, writeReadOnly } from './protocols/auth.js';
32
+ export { messageAccess, messageEpoch, messagePermissionDenied, messageReadOnly, writeAccess, writeEpoch, writePermissionDenied, writeReadOnly } from './protocols/auth.js';
33
33
  export { commentsSubscribe, commentsSnapshot, commentsChange, writeCommentsSubscribe, writeCommentsSnapshot, writeCommentsChange, readCommentsMessage, type CommentChange, type CommentMessage, type CommentSnapshot, type CommentThread, type ThreadComment } from './protocols/comments.js';
34
34
  export { applyAwarenessUpdate, encodeAwarenessUpdate, readAwarenessEntries, writeAwarenessEntries, type AwarenessEntry } from './protocols/awareness.js';
35
35
  export { createDecoder, readVarUint, readVarUint8Array, type Decoder } from 'lib0-v14/decoding';
@@ -30,9 +30,9 @@ export { bindCrdt } from './index.js';
30
30
  export { SCHEMA_VERSION, META_KEY } from './edytor-doc.js';
31
31
  export { GENERATION, generationWord, frame, PROTOCOL_VERSION, GENERATION_RECORD, GenerationMismatchError, readProtocolVersion } from './protocols/envelope.js';
32
32
  // ── Message types, acknowledgements and chunks ─────────────────────────
33
- export { messageSync, messageAwareness, messageAuth, messageQueryAwareness, messageSaved, messageChunk, messageComments, MAX_FRAME_BYTES, chunkFrame, createChunkReader, ChunkError, CLOSE } from './providers/room.js';
33
+ export { messageSync, messageAwareness, messageAuth, messageQueryAwareness, messageSaved, messageChunk, messageComments, messageRebase, MAX_FRAME_BYTES, chunkFrame, createChunkReader, ChunkError, CLOSE } from './providers/room.js';
34
34
  export { messageYjsSyncStep1, messageYjsSyncStep2, messageYjsUpdate } from './protocols/sync.js';
35
- export { messageAccess, messagePermissionDenied, messageReadOnly, writeAccess, writePermissionDenied, writeReadOnly } from './protocols/auth.js';
35
+ export { messageAccess, messageEpoch, messagePermissionDenied, messageReadOnly, writeAccess, writeEpoch, writePermissionDenied, writeReadOnly } from './protocols/auth.js';
36
36
  export { commentsSubscribe, commentsSnapshot, commentsChange, writeCommentsSubscribe, writeCommentsSnapshot, writeCommentsChange, readCommentsMessage } from './protocols/comments.js';
37
37
  export { applyAwarenessUpdate, encodeAwarenessUpdate, readAwarenessEntries, writeAwarenessEntries } from './protocols/awareness.js';
38
38
  // Minimal wire codec for frame bodies (lib0 — the codec the frames are
@@ -28,6 +28,13 @@ export declare const writeReadOnly: (encoder: encoding.Encoder) => void;
28
28
  */
29
29
  export declare const messageAccess = 2;
30
30
  export declare const writeAccess: (encoder: encoding.Encoder, access: string) => void;
31
+ /**
32
+ * The room's epoch (`room.reseed.protocol`), sent to a socket that dialed an
33
+ * older one, instead of anything else: the room asks it for its old
34
+ * document (`messageRebase`), then closes it `4410`.
35
+ */
36
+ export declare const messageEpoch = 3;
37
+ export declare const writeEpoch: (encoder: encoding.Encoder, epoch: number) => void;
31
38
  export type PermissionDeniedHandler = (ydoc: unknown, reason: string) => void;
32
39
  /**
33
40
  * Decode one auth payload. Returns the auth message subtype so the caller
@@ -35,4 +42,4 @@ export type PermissionDeniedHandler = (ydoc: unknown, reason: string) => void;
35
42
  * do not speak is protocol skew, and this stack's contract is that skew is
36
43
  * reported, not silently dropped (same rule as unknown sync subtypes).
37
44
  */
38
- export declare const readAuthMessage: (decoder: decoding.Decoder, y: unknown, permissionDeniedHandler: PermissionDeniedHandler, readOnlyHandler?: (ydoc: unknown) => void, accessHandler?: (access: string) => void) => number;
45
+ export declare const readAuthMessage: (decoder: decoding.Decoder, y: unknown, permissionDeniedHandler: PermissionDeniedHandler, readOnlyHandler?: (ydoc: unknown) => void, accessHandler?: (access: string) => void, epochHandler?: (epoch: number) => void) => number;
@@ -36,13 +36,23 @@ export const writeAccess = (encoder, access) => {
36
36
  encoding.writeVarUint(encoder, messageAccess);
37
37
  encoding.writeVarString(encoder, access);
38
38
  };
39
+ /**
40
+ * The room's epoch (`room.reseed.protocol`), sent to a socket that dialed an
41
+ * older one, instead of anything else: the room asks it for its old
42
+ * document (`messageRebase`), then closes it `4410`.
43
+ */
44
+ export const messageEpoch = 3;
45
+ export const writeEpoch = (encoder, epoch) => {
46
+ encoding.writeVarUint(encoder, messageEpoch);
47
+ encoding.writeVarUint(encoder, epoch);
48
+ };
39
49
  /**
40
50
  * Decode one auth payload. Returns the auth message subtype so the caller
41
51
  * can surface unknown subtypes — a valid envelope carrying an auth type we
42
52
  * do not speak is protocol skew, and this stack's contract is that skew is
43
53
  * reported, not silently dropped (same rule as unknown sync subtypes).
44
54
  */
45
- export const readAuthMessage = (decoder, y, permissionDeniedHandler, readOnlyHandler, accessHandler) => {
55
+ export const readAuthMessage = (decoder, y, permissionDeniedHandler, readOnlyHandler, accessHandler, epochHandler) => {
46
56
  const authMessageType = decoding.readVarUint(decoder);
47
57
  switch (authMessageType) {
48
58
  case messagePermissionDenied:
@@ -54,6 +64,9 @@ export const readAuthMessage = (decoder, y, permissionDeniedHandler, readOnlyHan
54
64
  break;
55
65
  case messageAccess:
56
66
  accessHandler?.(decoding.readVarString(decoder));
67
+ break;
68
+ case messageEpoch:
69
+ epochHandler?.(decoding.readVarUint(decoder));
57
70
  }
58
71
  return authMessageType;
59
72
  };
@@ -243,6 +243,7 @@ export declare const bindProviders: (Y: EngineApi) => {
243
243
  deny(): void;
244
244
  allow(): void;
245
245
  };
246
+ _rebasing: boolean;
246
247
  messageHandlers: Record<number, import("./room.js").RoomMessageHandler</*elided*/ any>>;
247
248
  hasSynced: boolean;
248
249
  whenSynced: Promise<unknown>;
@@ -1,6 +1,6 @@
1
1
  import { keepingReplaced } from '../incarnations.js';
2
2
  import { bindIndexeddbProvider } from './indexeddb.js';
3
- import { assertRoomId, SyncRefusedError } from './room.js';
3
+ import { assertRoomId, CLOSE, SyncRefusedError } from './room.js';
4
4
  import { bindWebsocketProvider } from './websocket.js';
5
5
  export const bindProviders = (Y) => {
6
6
  const idb = bindIndexeddbProvider(Y);
@@ -50,7 +50,18 @@ export const bindProviders = (Y) => {
50
50
  maxBackoffTime,
51
51
  connectTimeout,
52
52
  disableBc: disableBc || local !== undefined,
53
- maxFrameBytes
53
+ maxFrameBytes,
54
+ // With a local copy, the dial waits for it: the epoch it names is the copy's.
55
+ connect: local === undefined
56
+ });
57
+ const dial = () => provider._destroyed || provider.connect();
58
+ // A re-seed (`room.reseed.protocol`, a `4410` refusal): the local copy goes,
59
+ // its deletion waiting for every tab's connection to close.
60
+ provider.on('refused', ({ code }) => {
61
+ if (code !== CLOSE.reseeded || !loaded)
62
+ return;
63
+ void release?.();
64
+ void idb.clearDocument(persistName);
54
65
  });
55
66
  if (onExpired)
56
67
  provider.on('expired', (state) => onExpired(state));
@@ -94,9 +105,17 @@ export const bindProviders = (Y) => {
94
105
  });
95
106
  }
96
107
  let release = undefined;
108
+ // The store's load (or its failure) opens the dial.
109
+ const loaded = local &&
110
+ Object.assign((p) => local({
111
+ ...p,
112
+ synced: (x) => (p.synced(x), dial()),
113
+ failed: (error, x) => (p.failed?.(error, x), dial())
114
+ }), local);
97
115
  try {
98
116
  // Called directly (no `attach`), the store only persists: readiness is the socket's.
99
- release = local && (attach ? attach(local) : local({ doc, awareness, synced: () => { } }));
117
+ release =
118
+ loaded && (attach ? attach(loaded) : loaded({ doc, awareness, synced: () => { } }));
100
119
  }
101
120
  catch (error) {
102
121
  provider.destroy();
@@ -61,6 +61,12 @@ export declare const messageChunk = 5;
61
61
  * room sends it only to a socket that subscribed.
62
62
  */
63
63
  export declare const messageComments = 6;
64
+ /**
65
+ * Client → server, after a re-seed (`room.reseed.protocol`): the client's
66
+ * whole old document, its unsaved edits in it, for the room to rebase onto
67
+ * the current epoch. Sent only when the room asks (its epoch notice).
68
+ */
69
+ export declare const messageRebase = 7;
64
70
  /** A WebSocket message limit on Cloudflare (32 MiB): a larger frame is sent as chunks. */
65
71
  export declare const MAX_FRAME_BYTES: number;
66
72
  /**
@@ -203,6 +209,15 @@ export declare const CLOSE: {
203
209
  readonly denied: 4403;
204
210
  /** The dialed replica is bound to another user. */
205
211
  readonly replicaTaken: 4409;
212
+ /**
213
+ * The room re-seeded the document (`room.reseed.protocol`; the reason is
214
+ * the room's epoch): a refusal of this document, once a client with
215
+ * unsaved edits redialed and handed them over. The new epoch is a new
216
+ * document's. Also the answer to a copy the room cannot rebase (an
217
+ * epoch past the cuts it keeps, or a later one) and to an older epoch's
218
+ * content on a socket of the current one.
219
+ */
220
+ readonly reseeded: 4410;
206
221
  /**
207
222
  * A room quota refused the socket's write (`quota: document`,
208
223
  * `quota: rate`, `quota: frame`): the frame was not applied, and a
@@ -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;
@@ -334,6 +340,15 @@ export const CLOSE = {
334
340
  denied: 4403,
335
341
  /** The dialed replica is bound to another user. */
336
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,
337
352
  /**
338
353
  * A room quota refused the socket's write (`quota: document`,
339
354
  * `quota: rate`, `quota: frame`): the frame was not applied, and a
@@ -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>;
@@ -193,8 +195,8 @@ export declare const bindWebsocketProvider: (Y: EngineApi) => {
193
195
  * decodes it). `''`, `.`, `..`, an id over 256 characters and one with a
194
196
  * lone surrogate are refused at construction ({@link assertRoomId}). `params` are read at every dial (a refreshed token
195
197
  * reaches the next connection), with `replica` = this document's
196
- * client id unless `params` names one: the room binds the socket's
197
- * 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.
198
200
  */
199
201
  get url(): string;
200
202
  /** Updates of this document's actor the room has not acknowledged as persisted yet. */
@@ -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, fault, initLifecycle, isRefusal, unknownType, 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,10 +130,17 @@ 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
146
  unknownType(provider, 'auth', authType);
@@ -400,6 +408,13 @@ export const bindWebsocketProvider = (Y) => {
400
408
  provider.awareness.meta.delete(client);
401
409
  provider.emit('status', [{ status: 'disconnected' }]);
402
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
+ }
403
418
  if (event && isRefusal(event.code)) {
404
419
  // Terminal: the next dial would be refused the same way.
405
420
  provider.shouldConnect = false;
@@ -511,6 +526,8 @@ export const bindWebsocketProvider = (Y) => {
511
526
  _chunks = createChunkReader();
512
527
  /** This actor's writes pending the room's acknowledgement. */
513
528
  _writes;
529
+ /** @internal The room re-seeded: this document's unsaved edits are being handed over (`room.reseed.protocol`). */
530
+ _rebasing = false;
514
531
  messageHandlers = room.messageHandlers;
515
532
  // The room lifecycle — installed by `initLifecycle`.
516
533
  hasSynced;
@@ -596,12 +613,22 @@ export const bindWebsocketProvider = (Y) => {
596
613
  * decodes it). `''`, `.`, `..`, an id over 256 characters and one with a
597
614
  * lone surrogate are refused at construction ({@link assertRoomId}). `params` are read at every dial (a refreshed token
598
615
  * reaches the next connection), with `replica` = this document's
599
- * client id unless `params` names one: the room binds the socket's
600
- * 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.
601
618
  */
602
619
  get url() {
603
- const query = url.encodeQueryParams({ replica: String(this.doc.clientID), ...this.params });
604
- 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)}`;
605
632
  }
606
633
  /** Updates of this document's actor the room has not acknowledged as persisted yet. */
607
634
  get unsaved() {