edytor 0.1.0-next.24 → 0.1.0-next.26

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 (81) hide show
  1. package/README.md +1 -1
  2. package/dist/cloudflare/DocumentRoom.d.ts +194 -5
  3. package/dist/cloudflare/DocumentRoom.js +686 -45
  4. package/dist/cloudflare/history.d.ts +144 -14
  5. package/dist/cloudflare/history.js +225 -6
  6. package/dist/cloudflare/index.d.ts +3 -1
  7. package/dist/cloudflare/index.js +3 -1
  8. package/dist/cloudflare/locks.d.ts +31 -0
  9. package/dist/cloudflare/locks.js +31 -0
  10. package/dist/cloudflare/move.d.ts +159 -0
  11. package/dist/cloudflare/move.js +169 -0
  12. package/dist/cloudflare/routeDocumentSocket.js +18 -11
  13. package/dist/collaboration/index.d.ts +1 -1
  14. package/dist/collaboration/index.js +1 -1
  15. package/dist/collaboration/providers.d.ts +2 -0
  16. package/dist/collaboration/providers.js +2 -0
  17. package/dist/collaboration/remoteSelection.js +10 -1
  18. package/dist/components/Edytor.svelte +26 -0
  19. package/dist/components/Edytor.svelte.d.ts +9 -0
  20. package/dist/crdt/data.js +2 -1
  21. package/dist/crdt/document.d.ts +151 -0
  22. package/dist/crdt/document.js +11 -146
  23. package/dist/crdt/edytor-doc.d.ts +121 -8
  24. package/dist/crdt/edytor-doc.js +5 -2
  25. package/dist/crdt/engine.d.ts +4 -1
  26. package/dist/crdt/engine.js +6 -1
  27. package/dist/crdt/incarnations.d.ts +69 -0
  28. package/dist/crdt/incarnations.js +51 -0
  29. package/dist/crdt/index.d.ts +26 -8
  30. package/dist/crdt/index.js +25 -1
  31. package/dist/crdt/migration/generation.d.ts +58 -0
  32. package/dist/crdt/migration/generation.js +68 -0
  33. package/dist/crdt/migration/migrate.js +1 -1
  34. package/dist/crdt/placement/model.d.ts +32 -0
  35. package/dist/crdt/placement/model.js +165 -10
  36. package/dist/crdt/placement/rank.d.ts +38 -24
  37. package/dist/crdt/placement/rank.js +147 -93
  38. package/dist/crdt/protocols/envelope.d.ts +9 -3
  39. package/dist/crdt/protocols/envelope.js +9 -3
  40. package/dist/crdt/protocols/sync.d.ts +13 -6
  41. package/dist/crdt/protocols/sync.js +31 -9
  42. package/dist/crdt/providers/index.d.ts +2 -0
  43. package/dist/crdt/providers/index.js +23 -1
  44. package/dist/crdt/providers/indexeddb.d.ts +10 -0
  45. package/dist/crdt/providers/indexeddb.js +53 -6
  46. package/dist/crdt/providers/room.js +16 -1
  47. package/dist/crdt/purge.js +27 -2
  48. package/dist/crdt/restore.js +6 -2
  49. package/dist/crdt/schema.d.ts +1 -1
  50. package/dist/crdt/schema.js +4 -1
  51. package/dist/crdt/semantics.d.ts +46 -1
  52. package/dist/crdt/semantics.js +32 -3
  53. package/dist/crdt/text/deletes.js +102 -18
  54. package/dist/crdt/text/marks.d.ts +58 -0
  55. package/dist/crdt/text/marks.js +85 -0
  56. package/dist/crdt/text/model.d.ts +37 -5
  57. package/dist/crdt/text/model.js +147 -22
  58. package/dist/crdt/text/runs.js +413 -29
  59. package/dist/crdt/vendor/yjs/UPSTREAM.md +75 -0
  60. package/dist/crdt/vendor/yjs/dts/index.d.ts +1 -1
  61. package/dist/crdt/vendor/yjs/dts/utils/Doc.d.ts +19 -0
  62. package/dist/crdt/vendor/yjs/dts/utils/marks.d.ts +83 -0
  63. package/dist/crdt/vendor/yjs/dts/utils/transaction-helpers.d.ts +2 -1
  64. package/dist/crdt/vendor/yjs/dts/ynode.d.ts +25 -0
  65. package/dist/crdt/vendor/yjs/src/index.js +1 -1
  66. package/dist/crdt/vendor/yjs/src/structs/Item.js +19 -5
  67. package/dist/crdt/vendor/yjs/src/utils/Doc.js +20 -0
  68. package/dist/crdt/vendor/yjs/src/utils/RangeCursor.js +2 -0
  69. package/dist/crdt/vendor/yjs/src/utils/marks.js +149 -0
  70. package/dist/crdt/vendor/yjs/src/utils/transaction-helpers.js +30 -5
  71. package/dist/crdt/vendor/yjs/src/ynode.js +226 -2
  72. package/dist/edytor.svelte.js +19 -2
  73. package/dist/index.d.ts +1 -1
  74. package/dist/index.js +1 -1
  75. package/dist/plugins/blockHandles/BlockHandles.svelte +9 -2
  76. package/dist/plugins/richtext/RichTextPlugin.svelte +2 -2
  77. package/dist/session/editing/text.d.ts +6 -2
  78. package/dist/surface/overlay.d.ts +11 -0
  79. package/dist/surface/overlay.js +16 -0
  80. package/dist/themes/notion.css +18 -0
  81. package/package.json +1 -1
package/README.md CHANGED
@@ -15,7 +15,7 @@
15
15
 
16
16
  Edytor aims to be for Svelte what Slate.js is for React: a heavily customizable editor with an API to build any kind of collaborative rich text editor.
17
17
 
18
- > **Work in progress.** Edytor is a pre-release (`0.1.0-next.24`, `edytor@next` on npm) and not ready for production; the API changes between releases without a compatibility layer. The untagged `edytor@0.0.11` on npm predates the current API. Issues and PRs are welcome: when you report a bug, include the document's JSON value.
18
+ > **Work in progress.** Edytor is a pre-release (`0.1.0-next.26`, `edytor@next` on npm) and not ready for production; the API changes between releases without a compatibility layer. The untagged `edytor@0.0.11` on npm predates the current API. Issues and PRs are welcome: when you report a bug, include the document's JSON value.
19
19
 
20
20
  ## Features
21
21
 
@@ -96,7 +96,9 @@
96
96
  import { DurableObject } from 'cloudflare:workers';
97
97
  import * as E from '../crdt/index.js';
98
98
  import type { PurgeReport } from '../crdt/purge.js';
99
- import { type HistoryEntry, type HistoryOptions, type KVLike } from './history.js';
99
+ import { type HistoryEntry, type HistoryOptions, type KVLike, type R2BucketLike } from './history.js';
100
+ import { type LockOptions } from './locks.js';
101
+ import { type CommitResult, type ExportedBlocks, type ImportReceipt, type ImportRequest, type LateEditBatch, type MoveNamespace } from './move.js';
100
102
  import type { AwarenessEntry, DocumentSemanticsConfig, EdytorDoc, JSONBlock, JSONDoc, YDoc } from '../crdt/index.js';
101
103
  /** Under SQLite-backed Durable Objects' 2 MB row cap, with room for the other columns. */
102
104
  export declare const DEFAULT_MAX_ROW_BYTES: number;
@@ -162,7 +164,7 @@ export type SavedDocument = {
162
164
  };
163
165
  /** What `onLoad` may return: JSON, a bare v14 update, or `{ update, replicas }` (a `SavedDocument`). */
164
166
  export type LoadedDocument = JSONDoc | Uint8Array | Pick<SavedDocument, 'update' | 'replicas'>;
165
- /** The header `routeDocumentSocket` sets on an authorized `lastUpdated` probe (H12). */
167
+ /** The header `routeDocumentSocket` sets on an authorized probe: `lastUpdated` (H12) or `snapshot` (P8). */
166
168
  export declare const PROBE_HEADER = "X-Edytor-Probe";
167
169
  /** The transaction origin of the room's own edits (`transact`, `onLoad` seeds). */
168
170
  export declare const ROOM_ORIGIN: unique symbol;
@@ -204,12 +206,24 @@ export type DocumentRoomEnv = {
204
206
  * retention, else 30); `off` never purges.
205
207
  */
206
208
  EDYTOR_PURGE_AFTER_DAYS?: string | number;
207
- /** A KV namespace binding: the room keeps its version history there (H11, `history()`). */
208
- EDYTOR_HISTORY?: KVLike;
209
+ /**
210
+ * Where the room keeps its version history (H11, `history()`): a KV
211
+ * namespace or an R2 bucket binding, or the string `room` (the room's own
212
+ * storage, {@link roomHistory}). Unset: no history.
213
+ */
214
+ EDYTOR_HISTORY?: KVLike | R2BucketLike | 'room';
209
215
  /** Days a version is kept (default 30). */
210
216
  EDYTOR_HISTORY_RETENTION_DAYS?: string | number;
211
217
  /** The IANA time zone of the history's half-day slots (default `UTC`). */
212
218
  EDYTOR_HISTORY_TIME_ZONE?: string;
219
+ /** Per-block locks (`locks()`): the data key naming a block's owner, e.g. `lockedBy`. Unset: none. */
220
+ EDYTOR_LOCKS?: string;
221
+ /**
222
+ * The namespace of the rooms blocks move to (`rooms()`, H10): the room
223
+ * forwards late edits of moved blocks there itself. Unset: they wait
224
+ * for the host's `forwardLateEdits`.
225
+ */
226
+ EDYTOR_ROOMS?: MoveNamespace;
213
227
  };
214
228
  /** What a socket is bound to: its verified user, its replica (Yjs client id), its access. */
215
229
  export type SocketIdentity = {
@@ -317,6 +331,25 @@ export type RoomLogEntry = {
317
331
  edytor: 'fault';
318
332
  reason: 'storage' | 'internal';
319
333
  detail: string;
334
+ } | {
335
+ edytor: 'convert';
336
+ from: number;
337
+ to: number;
338
+ blocks: number;
339
+ bytes: number;
340
+ } | {
341
+ edytor: 'move';
342
+ moveId: string;
343
+ role: 'out' | 'in';
344
+ peer: string;
345
+ blocks: number;
346
+ } | {
347
+ edytor: 'late';
348
+ moveId: string;
349
+ seq: number;
350
+ edits: number;
351
+ structural: number;
352
+ forwarded: boolean;
320
353
  };
321
354
  /** `pending`: deletes of items the room does not hold yet (they wait for them). */
322
355
  type RowKind = 'generation' | 'update' | 'snapshot' | 'pending';
@@ -418,6 +451,19 @@ export type AttachDocumentOptions = {
418
451
  * when set (the room then keeps an index of every block's state).
419
452
  */
420
453
  validate?: (frame: FrameValidation) => boolean | void;
454
+ /**
455
+ * Per-block locks (H10, `room.locks`): a block whose data names a user
456
+ * under `key` (default `lockedBy`) is that user's alone — a frame from
457
+ * anyone else touching it is denied and compensated, as `validate`'s
458
+ * denials are ({@link lockedBlocks}). Asked before `validate`.
459
+ */
460
+ locks?: LockOptions;
461
+ /**
462
+ * The rooms blocks move to (H10, `room.move`): with it, the room forwards
463
+ * the late edits of blocks it moved out to their destination itself, on
464
+ * its alarm; without it, they wait for the host's `forwardLateEdits`.
465
+ */
466
+ rooms?: () => MoveNamespace | undefined;
421
467
  /**
422
468
  * The room's log (H14): one entry per compaction, quota hit, denial or
423
469
  * fault. Default: `console.log` of the entry as JSON (Workers Logs and
@@ -481,6 +527,10 @@ export declare class AttachedDocument {
481
527
  } | {
482
528
  kind: 'restored';
483
529
  records: number;
530
+ } | {
531
+ kind: 'converted';
532
+ from: number;
533
+ records: number;
484
534
  };
485
535
  private nextRecord;
486
536
  private updates;
@@ -539,7 +589,13 @@ export declare class AttachedDocument {
539
589
  private readonly epochsTable;
540
590
  private readonly editorsTable;
541
591
  private readonly restoreTable;
592
+ private readonly movesTable;
593
+ private readonly lateTable;
594
+ /** The blocks moved out and watched for late edits, by id → their move (`null`: not read yet). */
595
+ private watched;
542
596
  private _lookups;
597
+ private readonly tablePrefix;
598
+ private _validator;
543
599
  private _history;
544
600
  constructor(ctx: DurableObjectState, options?: AttachDocumentOptions);
545
601
  /**
@@ -651,6 +707,18 @@ export declare class AttachedDocument {
651
707
  * engine refuses) is refused for good.
652
708
  */
653
709
  private load;
710
+ /**
711
+ * The generation cutover at load (`room.generation.convert`): the
712
+ * records of a generation-4 container are read as JSON
713
+ * (`crdt.generations.previousJSON`), which this generation seeds as
714
+ * `onLoad`'s JSON is (deterministic: every replica converting the same
715
+ * state writes the same update), and the container is replaced by it in
716
+ * one storage transaction: rows, replicas, purge epochs, slot editors
717
+ * and the stored restore go; the document data, the alarm's due tasks
718
+ * and `lastUpdated` stay. What a generation-4 client never sent is not
719
+ * in it; history, attribution and CRDT identity do not cross.
720
+ */
721
+ private convert;
654
722
  /** The bytes of the stored `pending` records (uncompressed, as every record but a snapshot). */
655
723
  private pendingBytes;
656
724
  /**
@@ -711,6 +779,11 @@ export declare class AttachedDocument {
711
779
  lastUpdated(): number | null;
712
780
  /** The room's id: its name (`getByName`), else the object's id. */
713
781
  private get roomId();
782
+ /**
783
+ * The frame check, resolved at first use: per-block locks (`locks`),
784
+ * then `validate`; `null` when neither is set (no block index is kept).
785
+ */
786
+ private get validator();
714
787
  /** The history settings, resolved at first use (`null`: none; an unknown time zone is noted `history`). */
715
788
  private get historyConfig();
716
789
  /** Days after which deleted content is purged (`null`: never), `room.purge.timing`. */
@@ -750,6 +823,15 @@ export declare class AttachedDocument {
750
823
  * room keeps no history.
751
824
  */
752
825
  listHistory(): Promise<HistoryEntry[]>;
826
+ /** Every version key the store holds under this room's prefix, page by page. */
827
+ private storedVersions;
828
+ /**
829
+ * The `retention` task (`room.history.retention`), for a store that
830
+ * expires nothing itself: delete every version past its expiry, then
831
+ * re-arm at the next one's. A failure is noted `history` and retried
832
+ * an hour later.
833
+ */
834
+ private expireVersions;
753
835
  /** Version `key` as JSON (a preview), or `null` when this room holds no such version. */
754
836
  readHistory(key: string): Promise<JSONDoc | null>;
755
837
  /**
@@ -782,6 +864,77 @@ export declare class AttachedDocument {
782
864
  * is released and collected (the next compaction stores it collected).
783
865
  */
784
866
  private dropRestore;
867
+ private ensureMoves;
868
+ /** One move's row. */
869
+ private moveRow;
870
+ /** Days a moved-out block is watched for late edits: the purge horizon, else {@link DEFAULT_MOVE_GRACE_DAYS}. */
871
+ private get graceMs();
872
+ /**
873
+ * Export blocks `ids` for a move (`room.move`, step 1): their visible
874
+ * subtrees as JSON, the outermost only, in document order. Writes
875
+ * nothing to the document; records the export (`moveId`) for
876
+ * `commitMove`. Throws when none of them shows.
877
+ */
878
+ exportBlocks(ids: string[]): ExportedBlocks;
879
+ /**
880
+ * Import a move's blocks (`room.move`, step 2) under `dest.parent` at
881
+ * `dest.index`, as one room transaction: each keeps its id unless this
882
+ * document holds it already (then `<id>~<n>`). Idempotent: a second
883
+ * call with the same `moveId` writes nothing and returns the same ids,
884
+ * the acknowledgement `commitMove` needs.
885
+ */
886
+ importBlocks(request: ImportRequest): ImportReceipt;
887
+ /**
888
+ * Commit a move (`room.move`, step 3), with the destination's
889
+ * acknowledgement: delete the exported blocks with their subtrees, as
890
+ * one room transaction, and watch them for late edits (`room.move.late`)
891
+ * for the grace period. Idempotent; `refused` for an unknown or aborted
892
+ * move.
893
+ */
894
+ commitMove(moveId: string, receipt: {
895
+ to: string;
896
+ ids: Record<string, string>;
897
+ }): CommitResult;
898
+ /** Drop an export that was not committed (`room.move`): the blocks stay. */
899
+ abortMove(moveId: string): {
900
+ moveId: string;
901
+ status: 'applied' | 'noop';
902
+ };
903
+ /** The late edits waiting to reach their destination (`room.move.late`), oldest first. */
904
+ lateEdits(): LateEditBatch[];
905
+ /** The destination applied a move's late edits through `seq`: they are dropped here. */
906
+ ackLateEdits(moveId: string, seq: number): void;
907
+ /**
908
+ * Apply a batch of late edits of a move this room imported
909
+ * (`room.move.late`): each block's content and data merged three ways
910
+ * into what it holds now (what was written here since stays), as one
911
+ * room transaction. Idempotent by sequence number; a block that no
912
+ * longer shows is skipped. Returns the last sequence applied.
913
+ */
914
+ applyLateEdits(batch: LateEditBatch): {
915
+ applied: number;
916
+ skipped: string[];
917
+ };
918
+ /** One block's late edit, merged into what it holds (inside a room transaction). */
919
+ private mergeLateEdit;
920
+ /** The registry entries carrying a `del.<marker>` mark written between clocks `c0` and `c1`. */
921
+ private markedBetween;
922
+ /** The moved-out blocks watched for late edits (`room.move.late`), by id → their move. */
923
+ private watchedBlocks;
924
+ /** A transaction that changed a watched block marks its move dirty (inside the `update` emit: no doc write). */
925
+ private noteLateEdits;
926
+ /**
927
+ * The `forward` task (`room.move.late`): turn each dirty move's changes
928
+ * into a batch of late edits — read on a copy of the document with the
929
+ * move's own deletes left out — then forward the waiting batches to
930
+ * their destinations (with `rooms`), and stop watching moves past the
931
+ * grace period. A forward that fails runs again a minute later.
932
+ */
933
+ private forwardMoves;
934
+ /** The document's blocks as they show without the deletes of `moves`' commits: a copy, read once. */
935
+ private withoutMoveDeletes;
936
+ /** One dirty move's changes since its last batch, as a new batch of late edits (or a report). */
937
+ private collectLateEdits;
785
938
  /**
786
939
  * The purge task, also over RPC (`room.purge.timing`): record an epoch
787
940
  * (the state vector, now) when the document changed since the last one,
@@ -1046,10 +1199,23 @@ export declare class DocumentRoom<Env = DocumentRoomEnv> extends DurableObject<E
1046
1199
  protected semantics(): DocumentSemanticsConfig;
1047
1200
  /**
1048
1201
  * Version history — see {@link AttachDocumentOptions.history}. Default:
1049
- * the `EDYTOR_HISTORY` KV binding, with `EDYTOR_HISTORY_RETENTION_DAYS`
1202
+ * `EDYTOR_HISTORY` — a KV namespace binding ({@link kvHistory}), an R2
1203
+ * bucket binding ({@link r2History}) or the string `room` (the room's
1204
+ * own storage, {@link roomHistory}) — with `EDYTOR_HISTORY_RETENTION_DAYS`
1050
1205
  * and `EDYTOR_HISTORY_TIME_ZONE`; none without it. Read once, at first use.
1051
1206
  */
1052
1207
  protected history(): HistoryOptions | undefined;
1208
+ /**
1209
+ * Per-block locks — see {@link AttachDocumentOptions.locks}. Default: the
1210
+ * `EDYTOR_LOCKS` var names the data key (`{ key }`); none without it.
1211
+ * Read once, at the first frame.
1212
+ */
1213
+ protected locks(): LockOptions | undefined;
1214
+ /**
1215
+ * The rooms blocks move to — see {@link AttachDocumentOptions.rooms}.
1216
+ * Default: the `EDYTOR_ROOMS` binding; none without it.
1217
+ */
1218
+ protected rooms(): MoveNamespace | undefined;
1053
1219
  /** The room's clock — see {@link AttachDocumentOptions.now}. */
1054
1220
  protected now(): number;
1055
1221
  fetch(request: Request): Promise<Response>;
@@ -1074,6 +1240,29 @@ export declare class DocumentRoom<Env = DocumentRoomEnv> extends DurableObject<E
1074
1240
  }>;
1075
1241
  /** The version writes in flight, also over RPC — see {@link AttachedDocument.historyWritten}. */
1076
1242
  historyWritten(): Promise<void>;
1243
+ /** Export blocks for a move, also over RPC — see {@link AttachedDocument.exportBlocks}. */
1244
+ exportBlocks(ids: string[]): ExportedBlocks;
1245
+ /** Import a move's blocks, also over RPC — see {@link AttachedDocument.importBlocks}. */
1246
+ importBlocks(request: ImportRequest): ImportReceipt;
1247
+ /** Commit a move, also over RPC — see {@link AttachedDocument.commitMove}. */
1248
+ commitMove(moveId: string, receipt: {
1249
+ to: string;
1250
+ ids: Record<string, string>;
1251
+ }): CommitResult;
1252
+ /** Abort an export, also over RPC — see {@link AttachedDocument.abortMove}. */
1253
+ abortMove(moveId: string): {
1254
+ moveId: string;
1255
+ status: 'applied' | 'noop';
1256
+ };
1257
+ /** The late edits waiting, also over RPC — see {@link AttachedDocument.lateEdits}. */
1258
+ lateEdits(): LateEditBatch[];
1259
+ /** Drop forwarded late edits, also over RPC — see {@link AttachedDocument.ackLateEdits}. */
1260
+ ackLateEdits(moveId: string, seq: number): void;
1261
+ /** Apply late edits, also over RPC — see {@link AttachedDocument.applyLateEdits}. */
1262
+ applyLateEdits(batch: LateEditBatch): {
1263
+ applied: number;
1264
+ skipped: string[];
1265
+ };
1077
1266
  /** Run the purge task now, also over RPC — see {@link AttachedDocument.purge}. */
1078
1267
  purge(): (PurgeReport & {
1079
1268
  horizon: number;