edytor 0.1.0-next.21 → 0.1.0-next.23

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 (69) hide show
  1. package/README.md +1 -1
  2. package/dist/cloudflare/DocumentRoom.d.ts +302 -10
  3. package/dist/cloudflare/DocumentRoom.js +965 -88
  4. package/dist/cloudflare/index.d.ts +1 -1
  5. package/dist/cloudflare/index.js +1 -1
  6. package/dist/cloudflare/routeDocumentSocket.js +30 -6
  7. package/dist/collaboration/awarenessSelection.d.ts +7 -1
  8. package/dist/collaboration/awarenessSelection.js +7 -1
  9. package/dist/collaboration/index.d.ts +2 -2
  10. package/dist/collaboration/index.js +2 -2
  11. package/dist/collaboration/providers.d.ts +4 -0
  12. package/dist/collaboration/providers.js +4 -0
  13. package/dist/components/Edytor.svelte +14 -2
  14. package/dist/components/Edytor.svelte.d.ts +8 -1
  15. package/dist/crdt/admission.d.ts +6 -1
  16. package/dist/crdt/admission.js +9 -2
  17. package/dist/crdt/data.d.ts +3 -1
  18. package/dist/crdt/data.js +42 -32
  19. package/dist/crdt/document.d.ts +31 -8
  20. package/dist/crdt/document.js +53 -18
  21. package/dist/crdt/edytor-doc.d.ts +31 -3
  22. package/dist/crdt/edytor-doc.js +131 -14
  23. package/dist/crdt/engine.d.ts +6 -0
  24. package/dist/crdt/engine.js +7 -0
  25. package/dist/crdt/flow.d.ts +2 -0
  26. package/dist/crdt/flow.js +2 -2
  27. package/dist/crdt/index.d.ts +20 -9
  28. package/dist/crdt/index.js +3 -3
  29. package/dist/crdt/migration/migrate.js +9 -3
  30. package/dist/crdt/placement/model.d.ts +9 -1
  31. package/dist/crdt/placement/model.js +29 -10
  32. package/dist/crdt/placement/rank.d.ts +20 -0
  33. package/dist/crdt/placement/rank.js +79 -0
  34. package/dist/crdt/protocols/envelope.d.ts +20 -1
  35. package/dist/crdt/protocols/envelope.js +14 -2
  36. package/dist/crdt/providers/container.d.ts +15 -0
  37. package/dist/crdt/providers/container.js +17 -3
  38. package/dist/crdt/providers/index.d.ts +21 -1
  39. package/dist/crdt/providers/index.js +86 -3
  40. package/dist/crdt/providers/indexeddb.js +60 -40
  41. package/dist/crdt/providers/room.d.ts +18 -2
  42. package/dist/crdt/providers/room.js +29 -5
  43. package/dist/crdt/providers/websocket.d.ts +9 -1
  44. package/dist/crdt/providers/websocket.js +21 -6
  45. package/dist/crdt/storage.d.ts +21 -0
  46. package/dist/crdt/storage.js +37 -0
  47. package/dist/crdt/structs.d.ts +17 -0
  48. package/dist/crdt/structs.js +41 -0
  49. package/dist/crdt/text/model.d.ts +9 -1
  50. package/dist/crdt/text/model.js +24 -14
  51. package/dist/crdt/text/rows.d.ts +94 -0
  52. package/dist/crdt/text/rows.js +193 -0
  53. package/dist/crdt/text/runs.d.ts +30 -10
  54. package/dist/crdt/text/runs.js +1235 -264
  55. package/dist/edytor.svelte.d.ts +3 -1
  56. package/dist/edytor.svelte.js +9 -6
  57. package/dist/index.d.ts +1 -1
  58. package/dist/index.js +1 -1
  59. package/dist/plugins/image/ImageEmpty.svelte +19 -6
  60. package/dist/plugins/image/ImagePlugin.svelte +9 -3
  61. package/dist/plugins/image/ImagePlugin.svelte.d.ts +2 -2
  62. package/dist/plugins/image/image.d.ts +16 -0
  63. package/dist/plugins/image/image.js +19 -0
  64. package/dist/plugins/index.d.ts +1 -1
  65. package/dist/plugins/index.js +1 -1
  66. package/dist/plugins.d.ts +11 -0
  67. package/dist/session/commands.d.ts +1 -0
  68. package/dist/session/commands.js +4 -1
  69. package/package.json +1 -1
@@ -57,7 +57,7 @@ export { type BlockId, type Destination, type ContentItem, type BlockSpec, type
57
57
  export { type DocBlock } from './nodes.js';
58
58
  export { type ContentRun } from './text/runs.js';
59
59
  export type { YDoc, YUndoManager, YTransaction } from './engine-api.js';
60
- export { type EdytorSync, type EdytorSyncPayload, type EdytorSyncCleanup, type IndexeddbSyncOptions, type WebsocketSync, type WebsocketSyncOptions, type ProviderStack } from './providers/index.js';
60
+ export { type EdytorSync, type EdytorSyncPayload, type EdytorSyncCleanup, type IndexeddbSyncOptions, type WebsocketSync, type WebsocketSyncOptions, type ProviderStack, type PrefetchOptions, type PrefetchResult, type LastUpdatedOptions } from './providers/index.js';
61
61
  export { type IndexeddbPersistenceApi, type IndexeddbPersistenceOptions, type ProtocolMismatch, type SchemaMismatchDetail } from './providers/indexeddb.js';
62
62
  export { type WebsocketProviderOptions, type WebsocketProviderEvents, type WebsocketPolyfill } from './providers/websocket.js';
63
63
  export { SyncRefusedError } from './providers/room.js';
@@ -168,7 +168,7 @@ export declare const bindCrdt: (Y: EngineApi) => {
168
168
  isSelfOrDescendant: (placements: Map<import("./index.js").BlockId, import("./placement/model.js").ResolvedPlacement>, own: import("./text/model.js").Ownership, id: import("./index.js").BlockId, maybeAncestor: import("./index.js").BlockId) => boolean;
169
169
  ranksAt: (siblings: readonly {
170
170
  rank: string;
171
- }[], index: number, count: number, clientId: number, rand?: () => number) => string[];
171
+ }[], index: number, count: number, clientId: number, rand?: () => number, run?: boolean) => string[];
172
172
  writePlacement: (doc: import("./engine-api.js").EngineDoc, node: import("./engine-api.js").EngineNode, p: import("./index.js").BlockId | null, r: string) => void;
173
173
  withdrawOnUndo: (doc: import("./engine-api.js").EngineDoc) => (item: import("./engine-api.js").EngineItemRef, stackItem: {
174
174
  inserts: {
@@ -277,7 +277,10 @@ export declare const bindCrdt: (Y: EngineApi) => {
277
277
  checkSchema: () => import("./edytor-doc.js").SchemaProblem | null;
278
278
  assertSchema: () => void;
279
279
  dispose: () => void;
280
- createUndoManager: (opts?: ConstructorParameters<EngineApi["UndoManager"]>[1]) => import("./engine-api.js").YUndoManager;
280
+ createUndoManager: (options?: ConstructorParameters<EngineApi["UndoManager"]>[1] & {
281
+ limit?: number;
282
+ }) => import("./engine-api.js").YUndoManager;
283
+ releaseHistory: (um: import("./engine-api.js").YUndoManager) => void;
281
284
  project: () => import("./index.js").ProjectedDoc;
282
285
  toJSON: () => import("./index.js").JSONDoc;
283
286
  blockJSON: (id: string) => import("./index.js").JSONBlock;
@@ -345,6 +348,7 @@ export declare const bindCrdt: (Y: EngineApi) => {
345
348
  })[];
346
349
  onChange: (cb: (change: import("./edytor-doc.js").DocChange) => void) => (() => void);
347
350
  };
351
+ keepCopies: (doc: import("./engine-api.js").EngineDoc) => void;
348
352
  model: {
349
353
  REGISTRY_KEY: string;
350
354
  registryOf: (doc: import("./engine-api.js").EngineDoc) => import("./engine-api.js").EngineNode;
@@ -360,7 +364,7 @@ export declare const bindCrdt: (Y: EngineApi) => {
360
364
  isSelfOrDescendant: (placements: Map<import("./index.js").BlockId, import("./placement/model.js").ResolvedPlacement>, own: import("./text/model.js").Ownership, id: import("./index.js").BlockId, maybeAncestor: import("./index.js").BlockId) => boolean;
361
365
  ranksAt: (siblings: readonly {
362
366
  rank: string;
363
- }[], index: number, count: number, clientId: number, rand?: () => number) => string[];
367
+ }[], index: number, count: number, clientId: number, rand?: () => number, run?: boolean) => string[];
364
368
  writePlacement: (doc: import("./engine-api.js").EngineDoc, node: import("./engine-api.js").EngineNode, p: import("./index.js").BlockId | null, r: string) => void;
365
369
  withdrawOnUndo: (doc: import("./engine-api.js").EngineDoc) => (item: import("./engine-api.js").EngineItemRef, stackItem: {
366
370
  inserts: {
@@ -416,6 +420,8 @@ export declare const bindCrdt: (Y: EngineApi) => {
416
420
  };
417
421
  };
418
422
  providers: {
423
+ prefetch: (options: import("./index.js").PrefetchOptions) => Promise<import("./index.js").PrefetchResult>;
424
+ lastUpdated: (options: import("./index.js").LastUpdatedOptions) => Promise<number | null>;
419
425
  IndexeddbPersistence: {
420
426
  new (name: string, doc: YDoc, options?: import("./index.js").IndexeddbPersistenceOptions): {
421
427
  doc: YDoc;
@@ -495,7 +501,7 @@ export declare const bindCrdt: (Y: EngineApi) => {
495
501
  };
496
502
  };
497
503
  WebsocketProvider: {
498
- new (serverUrl: string, roomname: string, doc: YDoc, { connect, awareness, params, WebSocketPolyfill, resyncInterval, maxBackoffTime, connectTimeout, disableBc }?: import("./index.js").WebsocketProviderOptions): {
504
+ new (serverUrl: string, roomname: string, doc: YDoc, { connect, awareness, params, WebSocketPolyfill, resyncInterval, maxBackoffTime, connectTimeout, disableBc, maxFrameBytes }?: import("./index.js").WebsocketProviderOptions): {
499
505
  serverUrl: string;
500
506
  roomname: string;
501
507
  doc: YDoc;
@@ -503,6 +509,7 @@ export declare const bindCrdt: (Y: EngineApi) => {
503
509
  params: Record<string, string>;
504
510
  maxBackoffTime: number;
505
511
  connectTimeout: number;
512
+ maxFrameBytes: number;
506
513
  _WS: import("./index.js").WebsocketPolyfill;
507
514
  shouldConnect: boolean;
508
515
  ws: WebSocket | null;
@@ -616,7 +623,11 @@ export declare const bindCrdt: (Y: EngineApi) => {
616
623
  };
617
624
  /** U8 — the document-admission boundary (`admitUpdate` staged restore + the shared gate vocabulary). */
618
625
  admission: {
619
- admitUpdate: (update: Uint8Array, docName?: string) => YDoc;
626
+ admitUpdate: (update: Uint8Array | ReadonlyArray<Uint8Array | {
627
+ v2: Uint8Array;
628
+ }>, docName?: string, options?: {
629
+ prepare?: (doc: YDoc) => void;
630
+ }) => YDoc;
620
631
  };
621
632
  /** Bind the attribution service to this engine — actor dictionary + legacy `a/` reads + block attribution (documents attach it automatically). */
622
633
  attribution: {
@@ -627,9 +638,9 @@ export declare const bindCrdt: (Y: EngineApi) => {
627
638
  };
628
639
  export type Crdt = ReturnType<typeof bindCrdt>;
629
640
  export type { EngineApi, EngineDoc, EngineNode, EngineDeepEvent, EngineTransaction, EngineItemRef, YNode, YItem } from './engine-api.js';
630
- export { SCHEMA_VERSION, META_KEY, EdytorDocDisposedError } from './edytor-doc.js';
631
- export { GENERATION, generationWord, frame, PROTOCOL_VERSION, GENERATION_RECORD, GenerationMismatchError, readProtocolVersion, type GenerationRecord } from './protocols/envelope.js';
632
- export { messageSync, messageAwareness, messageAuth, messageQueryAwareness, messageSaved, messageChunk, MAX_FRAME_BYTES, chunkFrame, createChunkReader } from './providers/room.js';
641
+ export { SCHEMA_VERSION, META_KEY, EdytorDocDisposedError, DEFAULT_HISTORY_LIMIT } from './edytor-doc.js';
642
+ export { GENERATION, generationWord, frame, PROTOCOL_VERSION, GENERATION_RECORD, STORED_GENERATION_RECORD, STORAGE_FORMAT, GenerationMismatchError, readProtocolVersion, type GenerationRecord, type StorageFormat } from './protocols/envelope.js';
643
+ export { messageSync, messageAwareness, messageAuth, messageQueryAwareness, messageSaved, messageChunk, MAX_FRAME_BYTES, chunkFrame, createChunkReader, ChunkLimitError, CLOSE } from './providers/room.js';
633
644
  export { messageYjsSyncStep1, messageYjsSyncStep2, messageYjsUpdate, type SyncProtocol } from './protocols/sync.js';
634
645
  export { messagePermissionDenied, messageReadOnly, writePermissionDenied, writeReadOnly } from './protocols/auth.js';
635
646
  export { applyAwarenessUpdate, encodeAwarenessUpdate, modifyAwarenessUpdate, readAwarenessEntries, writeAwarenessEntries, type AwarenessEntry } from './protocols/awareness.js';
@@ -155,9 +155,9 @@ export const bindCrdt = (Y) => {
155
155
  // hibernation), the store-before-ack frame (`messageSaved`) and the bounded
156
156
  // catch-up codec (`chunkFrame`/`createChunkReader`, `messageChunk`).
157
157
  // `pnpm check:worker` keeps this graph Worker-safe.
158
- export { SCHEMA_VERSION, META_KEY, EdytorDocDisposedError } from './edytor-doc.js';
159
- export { GENERATION, generationWord, frame, PROTOCOL_VERSION, GENERATION_RECORD, GenerationMismatchError, readProtocolVersion } from './protocols/envelope.js';
160
- export { messageSync, messageAwareness, messageAuth, messageQueryAwareness, messageSaved, messageChunk, MAX_FRAME_BYTES, chunkFrame, createChunkReader } from './providers/room.js';
158
+ export { SCHEMA_VERSION, META_KEY, EdytorDocDisposedError, DEFAULT_HISTORY_LIMIT } from './edytor-doc.js';
159
+ export { GENERATION, generationWord, frame, PROTOCOL_VERSION, GENERATION_RECORD, STORED_GENERATION_RECORD, STORAGE_FORMAT, GenerationMismatchError, readProtocolVersion } from './protocols/envelope.js';
160
+ export { messageSync, messageAwareness, messageAuth, messageQueryAwareness, messageSaved, messageChunk, MAX_FRAME_BYTES, chunkFrame, createChunkReader, ChunkLimitError, CLOSE } from './providers/room.js';
161
161
  export { messageYjsSyncStep1, messageYjsSyncStep2, messageYjsUpdate } from './protocols/sync.js';
162
162
  export { messagePermissionDenied, messageReadOnly, writePermissionDenied, writeReadOnly } from './protocols/auth.js';
163
163
  export { applyAwarenessUpdate, encodeAwarenessUpdate, modifyAwarenessUpdate, readAwarenessEntries, writeAwarenessEntries } from './protocols/awareness.js';
@@ -33,7 +33,7 @@ import * as f from 'lib0-v14/function';
33
33
  import { bindEdytorDoc } from '../edytor-doc.js';
34
34
  import { bindLegacyReader } from './legacy-schema.js';
35
35
  import { generationDbName } from '../protocols/envelope.js';
36
- import { CUSTOM, decodeRow, encodeRow, openContainer, openIfExists, UPDATES, verifyOrStamp } from '../providers/container.js';
36
+ import { CUSTOM, decodeRow, encodeRow, readRow, openContainer, openIfExists, UPDATES, verifyOrStamp } from '../providers/container.js';
37
37
  import { jsonBlockToSpec, sanitizeWireJson } from '../../utils/json.js';
38
38
  const MIGRATION_KEY = 'migration';
39
39
  /** The name of the migration lock of logical document `name`. */
@@ -170,8 +170,14 @@ export const bindMigration = (Y) => {
170
170
  if (force) {
171
171
  const [updates, custom] = idb.transact(db, [UPDATES, CUSTOM]);
172
172
  await verifyOrStamp(name, updates, custom);
173
- for (const row of await idb.getAll(updates))
174
- Y.applyUpdate(doc, decodeRow(row));
173
+ // Rows first (one transaction), then inflate: a snapshot row is v2, maybe gzip (P5).
174
+ for (const row of await idb.getAll(updates)) {
175
+ const update = await readRow(row);
176
+ if (update instanceof Uint8Array)
177
+ Y.applyUpdate(doc, update);
178
+ else
179
+ Y.applyUpdateV2(doc, update.v2);
180
+ }
175
181
  }
176
182
  const base = Y.encodeStateVector(doc);
177
183
  edytorDoc.restore(doc, json.children.map((b) => jsonBlockToSpec(b)));
@@ -342,6 +342,14 @@ export declare const displayParentOf: (own: DisplayOwnership, pl: ResolvedPlacem
342
342
  * promoted one ({@link displaySlotOf}).
343
343
  */
344
344
  export declare const childrenIndex: (placements: Map<BlockId, ResolvedPlacement>, own: DisplayOwnership) => Map<BlockId | null, ChildSlot[]>;
345
+ /** The order of a children list: by rank, ties by id. */
346
+ export declare const bySlot: (a: {
347
+ id: BlockId;
348
+ rank: string;
349
+ }, b: {
350
+ id: BlockId;
351
+ rank: string;
352
+ }) => number;
345
353
  /**
346
354
  * Document order (O7): ONE pre-order over the visible blocks of a children
347
355
  * index — `ids` in reading order, `at` the position of each id. Every
@@ -376,7 +384,7 @@ export declare const bindModel: (Y: EngineApi) => {
376
384
  isSelfOrDescendant: (placements: Map<BlockId, ResolvedPlacement>, own: Ownership, id: BlockId, maybeAncestor: BlockId) => boolean;
377
385
  ranksAt: (siblings: readonly {
378
386
  rank: string;
379
- }[], index: number, count: number, clientId: number, rand?: () => number) => string[];
387
+ }[], index: number, count: number, clientId: number, rand?: () => number, run?: boolean) => string[];
380
388
  writePlacement: (doc: EngineDoc, node: EngineNode, p: BlockId | null, r: string) => void;
381
389
  withdrawOnUndo: (doc: EngineDoc) => (item: EngineItemRef, stackItem: {
382
390
  inserts: {
@@ -1,4 +1,4 @@
1
- import { decodeRank, encodeRank, rankBetween, RANK_VMIN } from './rank.js';
1
+ import { decodeRank, encodeRank, rankAfter, rankBetween, RANK_VMIN } from './rank.js';
2
2
  import { AT, AT_NODE, BLOCK_NODE, CONTENT, CONTENT_NODE, DEL_PREFIX, hasDeleteMark, ID, CLAIMS, CLAIMS_NODE, INLINE_NODE, isNodeLike, NONCE, REGISTRY_KEY, TYPE, WITHDRAW_PREFIX } from '../schema.js';
3
3
  import { nonceOf, randOf } from '../rand.js';
4
4
  import { bindRuns } from '../text/runs.js';
@@ -409,11 +409,12 @@ export const childrenIndex = (placements, own) => {
409
409
  else
410
410
  index.set(parent, [slot]);
411
411
  }
412
- for (const bucket of index.values()) {
413
- bucket.sort((a, b) => a.rank === b.rank ? a.id.localeCompare(b.id) : a.rank < b.rank ? -1 : 1);
414
- }
412
+ for (const bucket of index.values())
413
+ bucket.sort(bySlot);
415
414
  return index;
416
415
  };
416
+ /** The order of a children list: by rank, ties by id. */
417
+ export const bySlot = (a, b) => (a.rank === b.rank ? a.id.localeCompare(b.id) : a.rank < b.rank ? -1 : 1);
417
418
  export const documentOrder = (kids) => {
418
419
  const ids = [];
419
420
  const at = new Map();
@@ -559,9 +560,10 @@ export const bindModel = (Y) => {
559
560
  * so each new member takes `left` verbatim: it JOINS the tie and the
560
561
  * `(rank, id)` display sort orders the whole group deterministically.
561
562
  * On a valid seam the emitted chain is `rankBetween(left, right)` then
562
- * `rankBetween(prev, right)`.
563
+ * `rankBetween(prev, right)` — with `run`, `rankAfter` (an insert: after a
564
+ * block this client ranked, in its run there, H1).
563
565
  */
564
- const ranksAt = (siblings, index, count, clientId, rand) => {
566
+ const ranksAt = (siblings, index, count, clientId, rand, run = false) => {
565
567
  let left = siblings[index - 1]?.rank;
566
568
  const right = siblings[index]?.rank;
567
569
  if (left !== undefined && right !== undefined && left >= right) {
@@ -569,7 +571,10 @@ export const bindModel = (Y) => {
569
571
  }
570
572
  const out = [];
571
573
  for (let i = 0; i < count; i++) {
572
- const r = rankBetween(left, right, clientId, rand);
574
+ // An insert extends this client's run after a block it ranked (H1).
575
+ const r = run
576
+ ? rankAfter(left, right, clientId, rand)
577
+ : rankBetween(left, right, clientId, rand);
573
578
  out.push(r);
574
579
  left = r;
575
580
  }
@@ -749,12 +754,26 @@ export const bindModel = (Y) => {
749
754
  * slot ({@link displaySlotOf}). Content is each block's display (R2).
750
755
  */
751
756
  const project = (doc) => ({ children: R.attach(doc).project() });
752
- /** `positionOf` in a view: the display parent and the index among its visible children. */
757
+ /**
758
+ * `positionOf` in a view: the display parent and the index among its
759
+ * visible children — a binary search by the block's display rank (the
760
+ * list's own order), not a scan of its siblings.
761
+ */
753
762
  const positionInView = (v, id) => {
754
763
  if (!isLiveIn(v, id))
755
764
  return null;
756
- const dp = displayParentOf(v.own, v.placements.get(id), v.placements, id);
757
- const index = (v.kids.get(dp) ?? []).findIndex((s) => s.id === id);
765
+ const slot = displaySlotOf(v.own, v.placements, v.placements.get(id), id);
766
+ const dp = slot.parent;
767
+ const list = v.kids.get(dp) ?? [];
768
+ let [lo, hi] = [0, list.length];
769
+ while (lo < hi) {
770
+ const mid = (lo + hi) >> 1;
771
+ if (bySlot(list[mid], { id, rank: slot.rank }) < 0)
772
+ lo = mid + 1;
773
+ else
774
+ hi = mid;
775
+ }
776
+ const index = list[lo]?.id === id ? lo : list.findIndex((s) => s.id === id);
758
777
  return index < 0 ? null : { parent: dp, index };
759
778
  };
760
779
  /**
@@ -55,3 +55,23 @@ export declare const decodeRank: (rank: string) => RankSeg[];
55
55
  /** Encode segments back to the persisted string form. */
56
56
  export declare const encodeRank: (segs: readonly RankSeg[]) => string;
57
57
  export declare const rankBetween: (left: string | undefined, right: string | undefined, clientId: number, rand?: () => number) => string;
58
+ /**
59
+ * The digit of the run marker data arrays wrote up to 0.1.0-next.21 (a run
60
+ * of that form is the marker segment, then ranks of the client's own): still
61
+ * read as a run, so what a client inserts there stays in it.
62
+ */
63
+ export declare const RANK_RUN: number;
64
+ /**
65
+ * A rank in `(left, right)` for an insert right after `left` — one rule for
66
+ * array items (`crdt/data.ts`) and inserted blocks (H1, `order.insert.run`).
67
+ * When `client` made `left`, the rank is in the client's run after it: `left`
68
+ * itself as the prefix, then one segment of the run band — or, when `left`
69
+ * is already a member of the client's run, that run's prefix and a member
70
+ * between `left` and the next one. So what one client inserts after its own
71
+ * items stays together whatever a peer inserts in that gap meanwhile: a
72
+ * peer's rank there is either no extension of the run's prefix (it sorts
73
+ * after the whole run) or one whose next digit is above the band (YATA's
74
+ * origin rule, as `Y.Array` keeps an insert after its origin). Elsewhere, or
75
+ * when the run rank would not sort below `right`, a plain {@link rankBetween}.
76
+ */
77
+ export declare const rankAfter: (left: string | undefined, right: string | undefined, client: number, rand?: () => number) => string;
@@ -207,3 +207,82 @@ export const rankBetween = (left, right, clientId, rand = Math.random) => {
207
207
  path.push(lSeg);
208
208
  }
209
209
  };
210
+ /**
211
+ * The digit of the run marker data arrays wrote up to 0.1.0-next.21 (a run
212
+ * of that form is the marker segment, then ranks of the client's own): still
213
+ * read as a run, so what a client inserts there stays in it.
214
+ */
215
+ export const RANK_RUN = -(2 ** 39);
216
+ /**
217
+ * A run member's digit lies in `(RANK_RUN, RANK_RUN + RUN_BAND)`: below every
218
+ * digit a plain rank extends a prefix with (`0`, an append's), far above the
219
+ * digit minimum. A run opens at {@link RUN_START}, the middle of the band, so
220
+ * its members can be inserted before and after one another.
221
+ */
222
+ const RUN_BAND = 2 ** 38;
223
+ const RUN_START = RANK_RUN + 2 ** 37;
224
+ const inRun = (v) => v > RANK_RUN && v < RANK_RUN + RUN_BAND;
225
+ /**
226
+ * A rank in `(left, right)` for an insert right after `left` — one rule for
227
+ * array items (`crdt/data.ts`) and inserted blocks (H1, `order.insert.run`).
228
+ * When `client` made `left`, the rank is in the client's run after it: `left`
229
+ * itself as the prefix, then one segment of the run band — or, when `left`
230
+ * is already a member of the client's run, that run's prefix and a member
231
+ * between `left` and the next one. So what one client inserts after its own
232
+ * items stays together whatever a peer inserts in that gap meanwhile: a
233
+ * peer's rank there is either no extension of the run's prefix (it sorts
234
+ * after the whole run) or one whose next digit is above the band (YATA's
235
+ * origin rule, as `Y.Array` keeps an insert after its origin). Elsewhere, or
236
+ * when the run rank would not sort below `right`, a plain {@link rankBetween}.
237
+ */
238
+ export const rankAfter = (left, right, client, rand = Math.random) => {
239
+ const plain = () => rankBetween(left, right, client, rand);
240
+ if (left === undefined)
241
+ return plain();
242
+ const segs = decodeRank(left);
243
+ const last = segs.at(-1);
244
+ const bottom = encodeRank([{ v: RANK_RUN, t: 0 }]);
245
+ const top = encodeRank([{ v: RANK_RUN + RUN_BAND, t: 0 }]);
246
+ /**
247
+ * `prefix` and a rank after `after` (`undefined`: the run opens) in the
248
+ * run: inside the band when `band`. Opening, it takes the band's middle,
249
+ * or a digit below the run's first member; at the run's end it appends as
250
+ * an open end does (a window, not half the band); else between the two.
251
+ */
252
+ const own = (prefix, after, band) => {
253
+ const tail = (r) => r && r.length > prefix.length && r.startsWith(prefix) ? r.slice(prefix.length) : undefined;
254
+ let next = tail(right);
255
+ if (band && next !== undefined && next > top)
256
+ next = undefined;
257
+ let mine;
258
+ if (after === undefined) {
259
+ if (next === undefined)
260
+ mine = encodeRank([{ v: RUN_START, t: client }]);
261
+ else if (band && next > bottom)
262
+ mine = rankBetween(bottom, next, client, rand);
263
+ else
264
+ return plain();
265
+ }
266
+ else if (next === undefined && band) {
267
+ const append = rankBetween(after, undefined, client, rand);
268
+ mine = append < top ? append : rankBetween(after, top, client, rand);
269
+ }
270
+ else
271
+ mine = rankBetween(after, next, client, rand);
272
+ const rank = prefix + mine;
273
+ return right === undefined || rank < right ? rank : plain();
274
+ };
275
+ // A member of the client's run: the next member, inside the band.
276
+ if (last.t === client && inRun(last.v))
277
+ return own(encodeRank(segs.slice(0, -1)), encodeRank([last]), true);
278
+ // A run of the earlier form (the marker, then the client's ranks): stays in it.
279
+ const k = segs.findLastIndex((g) => g.v === RANK_RUN);
280
+ if (k >= 0 && segs[k].t === client) {
281
+ const prefix = encodeRank(segs.slice(0, k + 1));
282
+ return own(prefix, left.length > prefix.length ? left.slice(prefix.length) : undefined, false);
283
+ }
284
+ // A rank the client made: its run opens right after it.
285
+ if (last.t === client)
286
+ return own(left, undefined, true);
287
+ return plain();
288
+ };
@@ -37,20 +37,39 @@ export declare const GENERATION: number;
37
37
  export declare const GENERATION_PREFIX = "edytor-v14:";
38
38
  /** The IndexedDB name a v14 provider uses for logical document `name`. */
39
39
  export declare const generationDbName: (name: string) => string;
40
+ /**
41
+ * How a container stores its snapshots (P5, 0.1.0-next.23): `'v1'` (the
42
+ * update encoding, every container before it) or `'v2'` (the columnar
43
+ * encoding, gzip-compressed where the platform has `CompressionStream`;
44
+ * a compressed snapshot starts with gzip's `1f 8b`, a v2 update with
45
+ * `00`). Update rows stay v1 in both: a keystroke's update is smaller in
46
+ * v1 (24 bytes against 28, a one-character delete 13 against 24).
47
+ */
48
+ export type StorageFormat = 'v1' | 'v2';
40
49
  /** Record written to the `custom` store marking a DB as this generation's. */
41
50
  export type GenerationRecord = {
42
51
  engine: 'yjs-v14';
43
52
  protocol: number;
44
53
  schema: number;
54
+ /** Absent: `'v1'` (written before 0.1.0-next.23). */
55
+ storage?: StorageFormat;
45
56
  };
46
57
  export declare const GENERATION_KEY = "generation";
58
+ /** This generation's wire and schema (the storage format aside: {@link STORED_GENERATION_RECORD}). */
47
59
  export declare const GENERATION_RECORD: GenerationRecord;
60
+ /** The storage format this build writes its snapshots in. */
61
+ export declare const STORAGE_FORMAT: StorageFormat;
62
+ /** The record this build stamps a container with (its generation and its storage format). */
63
+ export declare const STORED_GENERATION_RECORD: GenerationRecord;
48
64
  /**
49
65
  * Is `v` this generation's container record? A record without `schema` was
50
66
  * written before the schema joined the generation, by builds that spoke
51
- * schema 1 only — it is a schema-1 record.
67
+ * schema 1 only — it is a schema-1 record. A storage format this build
68
+ * does not read (a later one) is another generation's.
52
69
  */
53
70
  export declare const isGenerationRecord: (v: unknown) => v is GenerationRecord;
71
+ /** The storage format of a container record ({@link isGenerationRecord} first). */
72
+ export declare const storageOf: (record: GenerationRecord) => StorageFormat;
54
73
  export declare class GenerationMismatchError extends Error {
55
74
  readonly dbName: string;
56
75
  readonly found: unknown;
@@ -39,21 +39,33 @@ export const GENERATION_PREFIX = 'edytor-v14:';
39
39
  /** The IndexedDB name a v14 provider uses for logical document `name`. */
40
40
  export const generationDbName = (name) => GENERATION_PREFIX + name;
41
41
  export const GENERATION_KEY = 'generation';
42
+ /** This generation's wire and schema (the storage format aside: {@link STORED_GENERATION_RECORD}). */
42
43
  export const GENERATION_RECORD = {
43
44
  engine: 'yjs-v14',
44
45
  protocol: PROTOCOL_VERSION,
45
46
  schema: SCHEMA_VERSION
46
47
  };
48
+ /** The storage format this build writes its snapshots in. */
49
+ export const STORAGE_FORMAT = 'v2';
50
+ /** The record this build stamps a container with (its generation and its storage format). */
51
+ export const STORED_GENERATION_RECORD = {
52
+ ...GENERATION_RECORD,
53
+ storage: STORAGE_FORMAT
54
+ };
47
55
  /**
48
56
  * Is `v` this generation's container record? A record without `schema` was
49
57
  * written before the schema joined the generation, by builds that spoke
50
- * schema 1 only — it is a schema-1 record.
58
+ * schema 1 only — it is a schema-1 record. A storage format this build
59
+ * does not read (a later one) is another generation's.
51
60
  */
52
61
  export const isGenerationRecord = (v) => typeof v === 'object' &&
53
62
  v !== null &&
54
63
  v.engine === GENERATION_RECORD.engine &&
55
64
  v.protocol === GENERATION_RECORD.protocol &&
56
- (v.schema ?? 1) === SCHEMA_VERSION;
65
+ (v.schema ?? 1) === SCHEMA_VERSION &&
66
+ [undefined, 'v1', 'v2'].includes(v.storage);
67
+ /** The storage format of a container record ({@link isGenerationRecord} first). */
68
+ export const storageOf = (record) => record.storage ?? 'v1';
57
69
  export class GenerationMismatchError extends Error {
58
70
  dbName;
59
71
  found;
@@ -5,6 +5,21 @@ export declare const openContainer: (dbName: string) => Promise<IDBDatabase>;
5
5
  /** A row is the update's bytes in a buffer of their own. */
6
6
  export declare const encodeRow: (update: Uint8Array) => ArrayBuffer;
7
7
  export declare const decodeRow: (row: unknown) => Uint8Array;
8
+ /**
9
+ * A snapshot row (P5, storage `'v2'`): the document in the v2 encoding,
10
+ * gzip-compressed where the platform has `CompressionStream` (and that
11
+ * makes it smaller). Update rows stay v1 `ArrayBuffer`s. A build before
12
+ * 0.1.0-next.23 reads such a row as no update at all (`decodeRow` throws),
13
+ * so it fails to load the store instead of misreading it.
14
+ */
15
+ export type SnapshotRow = {
16
+ v2: ArrayBuffer;
17
+ };
18
+ export declare const snapshotRow: (updateV2: Uint8Array) => Promise<SnapshotRow>;
19
+ /** A stored row as the update to apply: `{ v2 }` for a snapshot row (inflated). */
20
+ export declare const readRow: (row: unknown) => Promise<Uint8Array | {
21
+ v2: Uint8Array;
22
+ }>;
8
23
  /**
9
24
  * The one verify-or-stamp rule: a container carrying this generation's
10
25
  * record passes; an EMPTY container without a record is stamped; anything
@@ -1,12 +1,13 @@
1
1
  /**
2
2
  * The v14 IndexedDB container (L58) — one owner for the store names, the row
3
- * codec, the open, the non-creating probe and the ONE verify-or-stamp rule,
3
+ * codec (v1 update rows, v2 snapshot rows: P5), the open, the non-creating probe and the ONE verify-or-stamp rule,
4
4
  * shared by the provider and the migrator. Rows are append-only for every
5
5
  * writer (a provider's compaction appends its snapshot before deleting the
6
6
  * rows it subsumes).
7
7
  */
8
8
  import * as idb from 'lib0-v14/indexeddb';
9
- import { GENERATION_KEY, GENERATION_RECORD, GenerationMismatchError, isGenerationRecord } from '../protocols/envelope.js';
9
+ import { GENERATION_KEY, GenerationMismatchError, isGenerationRecord, STORED_GENERATION_RECORD } from '../protocols/envelope.js';
10
+ import { gunzip, packed } from '../storage.js';
10
11
  export const UPDATES = 'updates';
11
12
  export const CUSTOM = 'custom';
12
13
  /** Open (creating when absent) a container with its two stores. */
@@ -20,6 +21,19 @@ export const decodeRow = (row) => {
20
21
  return row;
21
22
  throw new TypeError('Stored Yjs update is not binary data');
22
23
  };
24
+ export const snapshotRow = async (updateV2) => ({
25
+ v2: (await packed(updateV2)).slice().buffer
26
+ });
27
+ /** A stored row as the update to apply: `{ v2 }` for a snapshot row (inflated). */
28
+ export const readRow = async (row) => {
29
+ if (typeof row === 'object' && row !== null && 'v2' in row) {
30
+ const bytes = row.v2;
31
+ if (!(bytes instanceof ArrayBuffer))
32
+ throw new TypeError('Stored snapshot is not binary data');
33
+ return { v2: await gunzip(new Uint8Array(bytes)) };
34
+ }
35
+ return decodeRow(row);
36
+ };
23
37
  /**
24
38
  * The one verify-or-stamp rule: a container carrying this generation's
25
39
  * record passes; an EMPTY container without a record is stamped; anything
@@ -29,7 +43,7 @@ export const decodeRow = (row) => {
29
43
  export const verifyOrStamp = async (name, updates, custom) => {
30
44
  const found = await idb.get(custom, GENERATION_KEY);
31
45
  if (found === undefined && (await idb.count(updates)) === 0) {
32
- await idb.rtop(custom.put({ ...GENERATION_RECORD }, GENERATION_KEY));
46
+ await idb.rtop(custom.put({ ...STORED_GENERATION_RECORD }, GENERATION_KEY));
33
47
  }
34
48
  else if (!isGenerationRecord(found)) {
35
49
  throw new GenerationMismatchError(name, found);
@@ -99,6 +99,8 @@ export type WebsocketSyncOptions = WebsocketTarget & {
99
99
  onExpired?: (state: Parameters<WebsocketProviderEvents['expired']>[0]) => void;
100
100
  /** Opt out of cross-tab sync over the BroadcastChannel (on by default). */
101
101
  disableBc?: boolean;
102
+ /** Largest frame sent whole (default 32 MiB); larger ones are chunked (H6). */
103
+ maxFrameBytes?: number;
102
104
  /**
103
105
  * Keep a local copy in IndexedDB (default `true`; skipped where there is
104
106
  * no `indexedDB`). Edits survive offline reloads and reach the server on
@@ -112,8 +114,25 @@ export type WebsocketSyncOptions = WebsocketTarget & {
112
114
  export type WebsocketSync = EdytorSync & {
113
115
  persistName?: string;
114
116
  };
117
+ /** What {@link bindProviders}' `prefetch` takes: a websocket sync's target and options. */
118
+ export type PrefetchOptions = (WebsocketSyncOptions extends infer O ? O extends unknown ? Omit<O, 'onExpired' | 'persist' | 'disableBc'> : never : never) & {
119
+ /** ms before it gives up (default 30 000). */
120
+ timeout?: number;
121
+ };
122
+ /** What `prefetch` found: whether the room sent the store anything it lacked. */
123
+ export type PrefetchResult = {
124
+ updated: boolean;
125
+ };
126
+ /** What `lastUpdated` takes: the room it asks, and the `params` (a token) its dial would carry. */
127
+ export type LastUpdatedOptions = WebsocketTarget & {
128
+ params?: Record<string, string>;
129
+ /** The fetch to use (default the global one). */
130
+ fetch?: typeof fetch;
131
+ };
115
132
  export type ProviderStack = ReturnType<typeof bindProviders>;
116
133
  export declare const bindProviders: (Y: EngineApi) => {
134
+ prefetch: (options: PrefetchOptions) => Promise<PrefetchResult>;
135
+ lastUpdated: (options: LastUpdatedOptions) => Promise<number | null>;
117
136
  IndexeddbPersistence: {
118
137
  new (name: string, doc: YDoc, options?: import("./indexeddb.js").IndexeddbPersistenceOptions): {
119
138
  doc: YDoc;
@@ -193,7 +212,7 @@ export declare const bindProviders: (Y: EngineApi) => {
193
212
  };
194
213
  };
195
214
  WebsocketProvider: {
196
- new (serverUrl: string, roomname: string, doc: YDoc, { connect, awareness, params, WebSocketPolyfill, resyncInterval, maxBackoffTime, connectTimeout, disableBc }?: import("./websocket.js").WebsocketProviderOptions): {
215
+ new (serverUrl: string, roomname: string, doc: YDoc, { connect, awareness, params, WebSocketPolyfill, resyncInterval, maxBackoffTime, connectTimeout, disableBc, maxFrameBytes }?: import("./websocket.js").WebsocketProviderOptions): {
197
216
  serverUrl: string;
198
217
  roomname: string;
199
218
  doc: YDoc;
@@ -201,6 +220,7 @@ export declare const bindProviders: (Y: EngineApi) => {
201
220
  params: Record<string, string>;
202
221
  maxBackoffTime: number;
203
222
  connectTimeout: number;
223
+ maxFrameBytes: number;
204
224
  _WS: import("./websocket.js").WebsocketPolyfill;
205
225
  shouldConnect: boolean;
206
226
  ws: WebSocket | null;