edytor 4.0.0 → 5.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 (147) hide show
  1. package/README.md +2 -2
  2. package/dist/block/block.svelte.js +6 -21
  3. package/dist/block/inlineBlock.svelte.js +4 -6
  4. package/dist/clipboard/moveText.js +6 -8
  5. package/dist/cloudflare/DocumentRoom.d.ts +10 -3
  6. package/dist/cloudflare/index.d.ts +1 -1
  7. package/dist/cloudflare/room/access.d.ts +8 -3
  8. package/dist/cloudflare/room/access.js +44 -20
  9. package/dist/cloudflare/room/admission.js +28 -5
  10. package/dist/cloudflare/room/comments.js +28 -1
  11. package/dist/cloudflare/room/context.d.ts +115 -106
  12. package/dist/cloudflare/room/context.js +10 -3
  13. package/dist/cloudflare/room/forged.d.ts +14 -0
  14. package/dist/cloudflare/room/forged.js +30 -2
  15. package/dist/cloudflare/room/frames.d.ts +4 -0
  16. package/dist/cloudflare/room/frames.js +5 -1
  17. package/dist/cloudflare/room/purge.js +2 -1
  18. package/dist/cloudflare/room/replicas.d.ts +4 -2
  19. package/dist/cloudflare/room/replicas.js +8 -3
  20. package/dist/cloudflare/room/suggest.d.ts +7 -0
  21. package/dist/cloudflare/room/suggest.js +143 -0
  22. package/dist/cloudflare/routeDocumentSocket.d.ts +10 -1
  23. package/dist/cloudflare/routeDocumentSocket.js +5 -3
  24. package/dist/components/Content.svelte +9 -1
  25. package/dist/crdt/anchors.js +3 -2
  26. package/dist/crdt/attribution/attribution.d.ts +3 -0
  27. package/dist/crdt/attribution/attribution.js +3 -3
  28. package/dist/crdt/doc/capability.js +9 -13
  29. package/dist/crdt/doc/funnel.d.ts +1 -1
  30. package/dist/crdt/doc/funnel.js +5 -2
  31. package/dist/crdt/doc/gate.d.ts +1 -1
  32. package/dist/crdt/doc/history.d.ts +6 -0
  33. package/dist/crdt/doc/history.js +77 -56
  34. package/dist/crdt/doc/layout.js +5 -5
  35. package/dist/crdt/doc/meta.js +6 -7
  36. package/dist/crdt/doc/reads.d.ts +2 -2
  37. package/dist/crdt/doc/reads.js +18 -28
  38. package/dist/crdt/doc/steps.js +4 -4
  39. package/dist/crdt/doc/suggest.d.ts +167 -0
  40. package/dist/crdt/doc/suggest.js +579 -0
  41. package/dist/crdt/document.d.ts +10 -1
  42. package/dist/crdt/document.js +19 -11
  43. package/dist/crdt/edytor-doc.d.ts +355 -333
  44. package/dist/crdt/edytor-doc.js +125 -82
  45. package/dist/crdt/engine-api.d.ts +8 -2
  46. package/dist/crdt/engine.d.ts +2 -1
  47. package/dist/crdt/engine.js +3 -1
  48. package/dist/crdt/flow.d.ts +1 -1
  49. package/dist/crdt/flow.js +4 -10
  50. package/dist/crdt/index.d.ts +111 -105
  51. package/dist/crdt/index.js +3 -0
  52. package/dist/crdt/operations.d.ts +12 -3
  53. package/dist/crdt/parts.d.ts +43 -0
  54. package/dist/crdt/parts.js +44 -0
  55. package/dist/crdt/placement/display.d.ts +1 -1
  56. package/dist/crdt/placement/display.js +3 -3
  57. package/dist/crdt/placement/keys.d.ts +5 -0
  58. package/dist/crdt/placement/keys.js +5 -0
  59. package/dist/crdt/placement/resolve.js +2 -1
  60. package/dist/crdt/protocol.d.ts +1 -1
  61. package/dist/crdt/protocol.js +1 -1
  62. package/dist/crdt/protocols/auth.d.ts +11 -1
  63. package/dist/crdt/protocols/auth.js +17 -1
  64. package/dist/crdt/protocols/comments.d.ts +16 -1
  65. package/dist/crdt/protocols/comments.js +9 -1
  66. package/dist/crdt/providers/index.d.ts +6 -4
  67. package/dist/crdt/providers/index.js +3 -1
  68. package/dist/crdt/providers/websocket.d.ts +6 -4
  69. package/dist/crdt/providers/websocket.js +9 -4
  70. package/dist/crdt/purge.d.ts +7 -1
  71. package/dist/crdt/purge.js +32 -2
  72. package/dist/crdt/restore.d.ts +2 -1
  73. package/dist/crdt/restore.js +9 -0
  74. package/dist/crdt/schema.d.ts +9 -1
  75. package/dist/crdt/schema.js +13 -3
  76. package/dist/crdt/structs.d.ts +2 -0
  77. package/dist/crdt/structs.js +2 -0
  78. package/dist/crdt/text/index/claims.js +1 -23
  79. package/dist/crdt/text/index/layout.d.ts +2 -4
  80. package/dist/crdt/text/index/layout.js +9 -22
  81. package/dist/crdt/text/index/placement.js +21 -24
  82. package/dist/crdt/text/index/shared.js +17 -14
  83. package/dist/crdt/text/model.d.ts +10 -0
  84. package/dist/crdt/text/model.js +8 -1
  85. package/dist/crdt/text/runs.d.ts +34 -30
  86. package/dist/crdt/vendor/yjs/UPSTREAM.md +83 -0
  87. package/dist/crdt/vendor/yjs/dts/index.d.ts +1 -1
  88. package/dist/crdt/vendor/yjs/dts/structs/Item.d.ts +6 -3
  89. package/dist/crdt/vendor/yjs/dts/ynode.d.ts +13 -2
  90. package/dist/crdt/vendor/yjs/src/index.js +1 -1
  91. package/dist/crdt/vendor/yjs/src/structs/Item.js +44 -5
  92. package/dist/crdt/vendor/yjs/src/ynode.js +29 -7
  93. package/dist/edytor.svelte.d.ts +1 -1
  94. package/dist/edytor.svelte.js +100 -75
  95. package/dist/events/beforeInputCommands.d.ts +2 -2
  96. package/dist/events/nativeInteractiveControl.d.ts +11 -1
  97. package/dist/events/nativeInteractiveControl.js +41 -1
  98. package/dist/events/onBeforeInput.js +3 -3
  99. package/dist/events/onFocus.js +9 -11
  100. package/dist/export/forms.d.ts +25 -25
  101. package/dist/index.d.ts +4 -2
  102. package/dist/index.js +1 -1
  103. package/dist/labels.d.ts +8 -0
  104. package/dist/labels.js +6 -1
  105. package/dist/plugins/comments/CommentsController.svelte.d.ts +5 -1
  106. package/dist/plugins/comments/CommentsController.svelte.js +14 -4
  107. package/dist/plugins/find/search.js +2 -2
  108. package/dist/plugins/index.d.ts +1 -0
  109. package/dist/plugins/index.js +1 -0
  110. package/dist/plugins/suggestions/SuggestionBars.svelte +40 -23
  111. package/dist/plugins/suggestions/SuggestionBars.svelte.d.ts +3 -1
  112. package/dist/plugins/suggestions/SuggestionRuns.svelte +77 -0
  113. package/dist/plugins/suggestions/SuggestionRuns.svelte.d.ts +9 -0
  114. package/dist/plugins/suggestions/shared.svelte.d.ts +58 -0
  115. package/dist/plugins/suggestions/shared.svelte.js +421 -0
  116. package/dist/plugins/suggestions/suggestionsPlugin.d.ts +15 -4
  117. package/dist/selection/domSelection.d.ts +2 -0
  118. package/dist/selection/domSelection.js +6 -2
  119. package/dist/selection/pointer.svelte.js +75 -48
  120. package/dist/selection/replaceSelection.js +2 -6
  121. package/dist/selection/selection.svelte.d.ts +3 -3
  122. package/dist/selection/selection.svelte.js +39 -32
  123. package/dist/selection/selection.utils.d.ts +14 -0
  124. package/dist/selection/selection.utils.js +28 -3
  125. package/dist/session/commands.js +21 -11
  126. package/dist/session/composition.svelte.d.ts +5 -3
  127. package/dist/session/composition.svelte.js +21 -17
  128. package/dist/session/handles.d.ts +4 -17
  129. package/dist/session/handles.js +11 -37
  130. package/dist/session/navigation.js +6 -9
  131. package/dist/session/selection.d.ts +11 -5
  132. package/dist/session/selection.js +14 -48
  133. package/dist/session/suggestions.svelte.d.ts +38 -9
  134. package/dist/session/suggestions.svelte.js +37 -18
  135. package/dist/session/virtual.d.ts +1 -1
  136. package/dist/surface/attributes.d.ts +5 -0
  137. package/dist/surface/attributes.js +2 -2
  138. package/dist/surface/cells.d.ts +2 -26
  139. package/dist/surface/cells.js +2 -33
  140. package/dist/surface/observer.svelte.d.ts +6 -1
  141. package/dist/surface/observer.svelte.js +128 -81
  142. package/dist/surface/pin.svelte.d.ts +9 -9
  143. package/dist/surface/pin.svelte.js +15 -22
  144. package/dist/surface/projector.svelte.js +8 -14
  145. package/dist/text/text.svelte.js +14 -19
  146. package/dist/themes/notion.css +29 -0
  147. package/package.json +3 -1
@@ -67,6 +67,16 @@ export type CommentRequest = {
67
67
  block?: string | null;
68
68
  /** The first comment's id (default: one the store makes). */
69
69
  comment?: string;
70
+ /**
71
+ * The text it is made on, as anchors (`DocAnchor`s, each span's
72
+ * ends): the room writes its anchor mark over them for a user who
73
+ * may only comment (`room.access.comment`); a writer's view writes
74
+ * it itself.
75
+ */
76
+ anchors?: {
77
+ start: unknown;
78
+ end: unknown;
79
+ }[];
70
80
  } | {
71
81
  op: 'reply';
72
82
  thread: string;
@@ -85,11 +95,16 @@ export type CommentRequest = {
85
95
  thread: string;
86
96
  comment?: string;
87
97
  };
88
- /** Who asks: a verified user, read-only or not; a moderator may delete anyone's comment. */
98
+ /**
99
+ * Who asks: a verified user, read-only or not; a moderator may delete
100
+ * anyone's comment; `anchors`: the store writes a new thread's anchor (a
101
+ * user who may only comment).
102
+ */
89
103
  export type CommentActor = {
90
104
  user: string;
91
105
  readOnly?: boolean;
92
106
  moderator?: boolean;
107
+ anchors?: boolean;
93
108
  };
94
109
  /** What one request changed (the room broadcasts it, `onComment` receives it). */
95
110
  export type CommentChange = {
@@ -62,13 +62,21 @@ export const parseCommentRequest = (value) => {
62
62
  return null;
63
63
  if (v.block !== undefined && v.block !== null && typeof v.block !== 'string')
64
64
  return null;
65
+ if (v.anchors !== undefined &&
66
+ (!Array.isArray(v.anchors) ||
67
+ v.anchors.length > 64 ||
68
+ v.anchors.some((a) => typeof a?.start !== 'object' || typeof a?.end !== 'object')))
69
+ return null;
65
70
  return {
66
71
  op: 'add',
67
72
  thread: v.thread,
68
73
  body: v.body,
69
74
  ...(v.quote === undefined ? {} : { quote: v.quote }),
70
75
  ...(v.block === undefined ? {} : { block: v.block }),
71
- ...(v.comment === undefined ? {} : { comment: v.comment })
76
+ ...(v.comment === undefined ? {} : { comment: v.comment }),
77
+ ...(v.anchors === undefined
78
+ ? {}
79
+ : { anchors: v.anchors })
72
80
  };
73
81
  case 'reply':
74
82
  if (typeof v.body !== 'string')
@@ -47,6 +47,8 @@ export type EdytorSyncPayload = {
47
47
  * websocket sync's local store. `document.attachSync` passes it.
48
48
  */
49
49
  attach?: (sync: EdytorSync) => EdytorSyncCleanup | void;
50
+ /** What the room says the document's user may do (`EdytorDocument.access`). */
51
+ access?: (access: 'write' | 'suggest' | 'comment' | 'read') => void;
50
52
  };
51
53
  export type EdytorSyncCleanup = () => void | Promise<void>;
52
54
  export type EdytorSync = ((payload: EdytorSyncPayload) => void | EdytorSyncCleanup) & {
@@ -273,11 +275,11 @@ export declare const bindProviders: (Y: EngineApi) => {
273
275
  destroy(): void;
274
276
  disconnect(): void;
275
277
  connect(): void;
276
- emit<N extends "refused" | "status" | "failed" | "schema-mismatch" | "message-error" | "protocol-mismatch" | "synced" | "connection-close" | "connection-error" | "saved" | "permission-denied" | "expired" | "unreachable">(name: N, args: Parameters<WebsocketProviderEvents[N]>): void;
278
+ 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;
277
279
  _observers: Map<string, Set<any>>;
278
- on<NAME extends "refused" | "status" | "failed" | "schema-mismatch" | "message-error" | "protocol-mismatch" | "synced" | "connection-close" | "connection-error" | "saved" | "permission-denied" | "expired" | "unreachable">(name: NAME, f: WebsocketProviderEvents[NAME]): WebsocketProviderEvents[NAME];
279
- once<NAME extends "refused" | "status" | "failed" | "schema-mismatch" | "message-error" | "protocol-mismatch" | "synced" | "connection-close" | "connection-error" | "saved" | "permission-denied" | "expired" | "unreachable">(name: NAME, f: WebsocketProviderEvents[NAME]): void;
280
- off<NAME extends "refused" | "status" | "failed" | "schema-mismatch" | "message-error" | "protocol-mismatch" | "synced" | "connection-close" | "connection-error" | "saved" | "permission-denied" | "expired" | "unreachable">(name: NAME, f: WebsocketProviderEvents[NAME]): void;
280
+ 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];
281
+ 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;
282
+ 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;
281
283
  };
282
284
  };
283
285
  watchComments: (server: string | null, roomName: string, listener: (message: import("../protocol.js").CommentMessage) => void) => (() => void);
@@ -38,7 +38,7 @@ export const bindProviders = (Y) => {
38
38
  const room = `${serverUrl}/${roomName}`;
39
39
  const persistName = options.persist === false ? undefined : (options.persistName ?? `edytor:${room}`);
40
40
  const sync = (payload) => {
41
- const { doc, awareness, synced, failed, attach, armBound, holdBound } = payload;
41
+ const { doc, awareness, synced, failed, attach, armBound, holdBound, access } = payload;
42
42
  const { params, WebSocketPolyfill, maxBackoffTime, connectTimeout, disableBc, onExpired, maxFrameBytes } = options;
43
43
  const local = persistName !== undefined && typeof indexedDB !== 'undefined'
44
44
  ? localSync(persistName, { disableBc })
@@ -54,6 +54,8 @@ export const bindProviders = (Y) => {
54
54
  });
55
55
  if (onExpired)
56
56
  provider.on('expired', (state) => onExpired(state));
57
+ if (access)
58
+ provider.on('access', access);
57
59
  provider.on('synced', (isSynced) => {
58
60
  if (isSynced)
59
61
  synced(provider);
@@ -7,6 +7,8 @@ export type WebsocketProviderEvents = {
7
7
  status: (event: {
8
8
  status: 'connected' | 'disconnected' | 'connecting';
9
9
  }) => void;
10
+ /** What the room says this connection's user may do, as it joins (and at a change in place). */
11
+ access: (access: 'write' | 'suggest' | 'comment' | 'read', provider: unknown) => void;
10
12
  synced: (state: boolean) => void;
11
13
  'connection-close': (event: CloseEvent | null, provider: unknown) => void;
12
14
  'connection-error': (event: Event, provider: unknown) => void;
@@ -221,11 +223,11 @@ export declare const bindWebsocketProvider: (Y: EngineApi) => {
221
223
  destroy(): void;
222
224
  disconnect(): void;
223
225
  connect(): void;
224
- emit<N extends "refused" | "status" | "failed" | "schema-mismatch" | "message-error" | "protocol-mismatch" | "synced" | "connection-close" | "connection-error" | "saved" | "permission-denied" | "expired" | "unreachable">(name: N, args: Parameters<WebsocketProviderEvents[N]>): void;
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;
225
227
  _observers: Map<string, Set<any>>;
226
- on<NAME extends "refused" | "status" | "failed" | "schema-mismatch" | "message-error" | "protocol-mismatch" | "synced" | "connection-close" | "connection-error" | "saved" | "permission-denied" | "expired" | "unreachable">(name: NAME, f: WebsocketProviderEvents[NAME]): WebsocketProviderEvents[NAME];
227
- once<NAME extends "refused" | "status" | "failed" | "schema-mismatch" | "message-error" | "protocol-mismatch" | "synced" | "connection-close" | "connection-error" | "saved" | "permission-denied" | "expired" | "unreachable">(name: NAME, f: WebsocketProviderEvents[NAME]): void;
228
- off<NAME extends "refused" | "status" | "failed" | "schema-mismatch" | "message-error" | "protocol-mismatch" | "synced" | "connection-close" | "connection-error" | "saved" | "permission-denied" | "expired" | "unreachable">(name: NAME, f: WebsocketProviderEvents[NAME]): void;
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;
229
231
  };
230
232
  };
231
233
  watchComments: (server: string | null, roomName: string, listener: (message: CommentMessage) => void) => (() => void);
@@ -52,7 +52,7 @@ import * as math from 'lib0-v14/math';
52
52
  import * as url from 'lib0-v14/url';
53
53
  import { Awareness, removeAwarenessStates } from '../protocols/awareness.js';
54
54
  import { IsolatedObservable } from '../protocols/observable.js';
55
- import { messagePermissionDenied, messageReadOnly, READ_ONLY_DENIAL, readAuthMessage } from '../protocols/auth.js';
55
+ import { messagePermissionDenied, messageAccess, messageReadOnly, READ_ONLY_DENIAL, SUGGEST_ONLY_DENIAL, readAuthMessage } from '../protocols/auth.js';
56
56
  import { bindSync } from '../protocols/sync.js';
57
57
  import { readCommentsMessage, writeCommentsSubscribe } from '../protocols/comments.js';
58
58
  import { frame } from '../protocols/envelope.js';
@@ -122,12 +122,17 @@ export const bindWebsocketProvider = (Y) => {
122
122
  if (readOnly)
123
123
  provider._writes.deny();
124
124
  provider.emit('permission-denied', [reason, provider]);
125
- if (!readOnly)
125
+ // What a suggest-only socket may not write is stripped: the socket stays.
126
+ if (!readOnly && reason !== SUGGEST_ONLY_DENIAL)
126
127
  emitFailed(provider, new Error(`permission denied: ${reason}`));
127
128
  },
128
129
  // The room's notice at the join: a state, not a refusal (no event).
129
- () => provider._writes.deny());
130
- if (authType !== messagePermissionDenied && authType !== messageReadOnly) {
130
+ () => provider._writes.deny(),
131
+ // The room's word at the join, and at a change in place (`read`).
132
+ (access) => provider.emit('access', [access, provider]));
133
+ if (authType !== messagePermissionDenied &&
134
+ authType !== messageReadOnly &&
135
+ authType !== messageAccess) {
131
136
  // An auth type we do not speak inside a valid v14 envelope is
132
137
  // protocol skew — reported, never dropped silently.
133
138
  provider.emit('message-error', [
@@ -18,6 +18,7 @@
18
18
  import type { EngineApi, EngineDoc } from './engine-api.js';
19
19
  import { type PlacementModel } from './placement/model.js';
20
20
  import { type TextEngine } from './text/model.js';
21
+ import { type SuggestFacade } from './doc/suggest.js';
21
22
  /** The horizon: the time and encoded state vector of the room's epoch (`room.purge.timing`). */
22
23
  export type Horizon = {
23
24
  at: number;
@@ -43,6 +44,11 @@ export type PurgeReport = {
43
44
  merged: number;
44
45
  /** Release records the horizon covers (`hist.release.end`): deleted. */
45
46
  released: number;
47
+ /**
48
+ * Suggestions decided before the horizon (`sugg.purge`): nothing of them
49
+ * pending, their records' entries older than it: their records deleted.
50
+ */
51
+ suggestions: number;
46
52
  };
47
53
  /**
48
54
  * The ended histories a document holds (`hist.release.end`): per writer,
@@ -53,7 +59,7 @@ export declare const releasesOf: (doc: EngineDoc) => Map<number, number>;
53
59
  /** The horizon a document holds (`null`: never purged). */
54
60
  export declare const readHorizon: (doc: EngineDoc) => Horizon | null;
55
61
  export declare const bindPurge: (Y: EngineApi) => {
56
- purge: (doc: EngineDoc, facade: {
62
+ purge: (doc: EngineDoc, facade: Partial<SuggestFacade> & {
57
63
  model: PlacementModel;
58
64
  text: TextEngine;
59
65
  }, horizon: Horizon | null) => PurgeReport;
@@ -1,10 +1,13 @@
1
1
  import { recordsOf, registryOf, view as modelView } from './placement/model.js';
2
2
  import { removeUnit, unitOf } from './placement/slots.js';
3
3
  import { deleteClaims, purgeStreams, scanText } from './text/model.js';
4
- import { BLOCK_ATTR_ROOT, DATA, DATA_LEAF_PREFIX, DEL_PREFIX, HORIZON_ATTR, HORIZON_ROOT, REGISTRY_KEY, RELEASE_ROOT, TEXT_DELETES_ROOT, WITHDRAW_PREFIX } from './schema.js';
4
+ import { BLOCK_ATTR_ROOT, DATA, DATA_LEAF_PREFIX, DEL_PREFIX, HORIZON_ATTR, HORIZON_ROOT, REGISTRY_KEY, RELEASE_ROOT, SUGGEST_ROOT, TEXT_DELETES_ROOT, WITHDRAW_PREFIX } from './schema.js';
5
5
  import { baseIdOf, isIncarnationId } from './incarnations.js';
6
+ import { suggestionsOf } from './doc/suggest.js';
6
7
  import { attrItems, dropSearchMarkers, engineOps, firstItem, holdsIds } from './structs.js';
7
8
  const mapOf = (node) => attrItems(node);
9
+ /** The clock a suggestion's record opened at (its writer's clocks it owns start there). */
10
+ const clockOf = (rec) => rec.start ?? 0;
8
11
  /**
9
12
  * The ended histories a document holds (`hist.release.end`): per writer,
10
13
  * the clock its latest live release record ended at (what it wrote below
@@ -107,7 +110,8 @@ export const bindPurge = (Y) => {
107
110
  records: 0,
108
111
  claims: 0,
109
112
  merged: 0,
110
- released: 0
113
+ released: 0,
114
+ suggestions: 0
111
115
  };
112
116
  const vector = horizon === null
113
117
  ? new Map()
@@ -425,6 +429,32 @@ export const bindPurge = (Y) => {
425
429
  report.released++;
426
430
  }
427
431
  }
432
+ // Suggestions decided before the horizon (`sugg.purge`): the decision
433
+ // trail goes with them. A writer's oldest record first, so no record's
434
+ // clocks pass to another.
435
+ const suggest = facade.part && suggestionsOf(facade);
436
+ const records = suggest?.records() ?? [];
437
+ if (records.length > 0) {
438
+ const pending = new Set(suggest.changes().map((c) => c.suggestion));
439
+ const root = doc.get(SUGGEST_ROOT);
440
+ const seen = new Set();
441
+ for (const rec of [...records].sort((a, b) => clockOf(a) - clockOf(b))) {
442
+ if (seen.has(rec.writer))
443
+ continue;
444
+ seen.add(rec.writer);
445
+ const node = root.getAttr(rec.id);
446
+ const items = [node._item];
447
+ for (let it = firstItem(node); it; it = it.right)
448
+ if (!it.deleted)
449
+ items.push(it);
450
+ if (pending.has(rec.id) ||
451
+ !items.every((it) => old(it.id.client, it.id.clock + it.length - 1)))
452
+ continue;
453
+ root.deleteAttr(rec.id);
454
+ seen.delete(rec.writer);
455
+ report.suggestions++;
456
+ }
457
+ }
428
458
  // The horizon record, last.
429
459
  if (horizon !== null)
430
460
  doc.get(HORIZON_ROOT).setAttr(HORIZON_ATTR, { at: horizon.at, sv: horizon.sv });
@@ -26,6 +26,7 @@
26
26
  import type { EngineDoc } from './engine-api.js';
27
27
  import { type BlockId, type ContentItem, type InlineSpec, type PlacementModel } from './placement/model.js';
28
28
  import type { DataPatch } from './data.js';
29
+ import { type SuggestFacade } from './doc/suggest.js';
29
30
  import type { JSONDoc } from '../utils/json.js';
30
31
  /** One display unit: a character (a code point) with its marks, or an inline atom, keyed for comparison. */
31
32
  export type Unit = {
@@ -60,7 +61,7 @@ export type RestoreReport = {
60
61
  deleted: number;
61
62
  };
62
63
  /** What a restore reads and writes through: the facade over the document (structurally). */
63
- export type RestoreFacade<Plan> = {
64
+ export type RestoreFacade<Plan> = Partial<SuggestFacade> & {
64
65
  model: PlacementModel;
65
66
  childSlots(parent: BlockId | null): readonly {
66
67
  id: BlockId;
@@ -1,4 +1,5 @@
1
1
  import { blockNodeOf, insertSpecsAt, moveBlocksTo, view as modelView } from './placement/model.js';
2
+ import { suggestionsOf } from './doc/suggest.js';
2
3
  import { DEL_PREFIX, TYPE, WITHDRAW_PREFIX } from './schema.js';
3
4
  import { common, patchWrites, writeData } from './data.js';
4
5
  import { canonKey, deleteClaims } from './text/model.js';
@@ -196,5 +197,13 @@ export const restoreDocument = (doc, facade, json) => {
196
197
  }
197
198
  // 6 · The document's data.
198
199
  facade.apply(facade.prepare.patchData(null, [{ path: [], value: json.data ?? {} }]));
200
+ // 7 · Pending suggestions are dropped (`sugg.restore`): the version holds
201
+ // none (its document without them), so what the restore left of them —
202
+ // their proposals — is rejected, in the restore's step (its undo brings
203
+ // them back).
204
+ // (Built over the facade, its shared suggestions: `doc/suggest.ts`.)
205
+ const suggest = facade.part && suggestionsOf(facade);
206
+ if (suggest)
207
+ suggest.run(suggest.decide(suggest.changes(), false));
199
208
  return report;
200
209
  };
@@ -49,6 +49,14 @@ export declare const HORIZON_ATTR = "h";
49
49
  * horizon covers.
50
50
  */
51
51
  export declare const RELEASE_ROOT = "released";
52
+ /**
53
+ * The shared suggestions (`crdt/doc/suggest.ts`): attr `<id>` is one
54
+ * suggestion's record node, written by its writer (a client id bound to its
55
+ * author): its creation time `at`, `via` and `lbl`, and a list of id lists
56
+ * (fork patch YP31: its writer's proposed deletions, anyone else's settled
57
+ * ranges) and proposed operations. In the history's scope.
58
+ */
59
+ export declare const SUGGEST_ROOT = "suggest";
52
60
  /** Named node roles (`SCHEMA.nodes`). */
53
61
  export declare const BLOCK_NODE = "block";
54
62
  export declare const CONTENT_NODE = "content";
@@ -92,7 +100,7 @@ export declare const LAST_CHANGED_ATTR = "l";
92
100
  export declare const CONTENT = "content";
93
101
  /** The schema generation stamped on the `meta` root (`v`) and its manifest name (`schema`). */
94
102
  export declare const SCHEMA: {
95
- readonly version: 10;
103
+ readonly version: 11;
96
104
  readonly name: "edytor-doc";
97
105
  readonly roots: {
98
106
  readonly registry: "blocks";
@@ -42,6 +42,14 @@ export const HORIZON_ATTR = 'h';
42
42
  * horizon covers.
43
43
  */
44
44
  export const RELEASE_ROOT = 'released';
45
+ /**
46
+ * The shared suggestions (`crdt/doc/suggest.ts`): attr `<id>` is one
47
+ * suggestion's record node, written by its writer (a client id bound to its
48
+ * author): its creation time `at`, `via` and `lbl`, and a list of id lists
49
+ * (fork patch YP31: its writer's proposed deletions, anyone else's settled
50
+ * ranges) and proposed operations. In the history's scope.
51
+ */
52
+ export const SUGGEST_ROOT = 'suggest';
45
53
  /** Named node roles (`SCHEMA.nodes`). */
46
54
  export const BLOCK_NODE = 'block';
47
55
  export const CONTENT_NODE = 'content';
@@ -86,9 +94,11 @@ export const SCHEMA = {
86
94
  // YP21), a block has no `claims` list; 9: a block's place is its home slot
87
95
  // in its parent's sequence and a move is a move record (fork patch YP22),
88
96
  // a block has no `at` map and an array item no rank; 10: the update encoding
89
- // writes each distinct string once (fork patch YP24). A container or frame
90
- // of an earlier generation is refused.
91
- version: 10,
97
+ // writes each distinct string once (fork patch YP24); 11: a hold record
98
+ // inside a nested node holds nothing (fork patch YP31: a suggestion's
99
+ // proposals and decisions). A container or frame of an earlier generation
100
+ // is refused.
101
+ version: 11,
92
102
  name: 'edytor-doc',
93
103
  roots: { registry: REGISTRY_KEY, meta: META_ROOT_KEY },
94
104
  metaAttrs: { version: 'v', schema: 'schema' }
@@ -212,6 +212,8 @@ export declare const engineOps: (Y: EngineApi) => {
212
212
  merge: (sets: IdSetLike[]) => IdSetOf;
213
213
  /** Whether a store struct is an item (not a GC or a Skip). */
214
214
  isItem: (struct: unknown) => struct is StoreStruct;
215
+ /** Show again what a suggestion's deletes in `tr` proposed (fork patch YP32). */
216
+ unhide: (tr: unknown) => void;
215
217
  /** Whether `item` sits in `node`'s subtree. */
216
218
  inside: (node: EngineNode, item: StoreStruct) => boolean;
217
219
  /** Keep `item` (and its parents) from collection (`keepItem`). */
@@ -238,6 +238,8 @@ export const engineOps = (Y) => {
238
238
  merge: (sets) => Y.mergeIdSets(sets),
239
239
  /** Whether a store struct is an item (not a GC or a Skip). */
240
240
  isItem: (struct) => struct instanceof Y.Item,
241
+ /** Show again what a suggestion's deletes in `tr` proposed (fork patch YP32). */
242
+ unhide: (tr) => Y.unhide(tr),
241
243
  /** Whether `item` sits in `node`'s subtree. */
242
244
  inside: (node, item) => Y.isParentOf(asYNode(node), item),
243
245
  /** Keep `item` (and its parents) from collection (`keepItem`). */
@@ -1,4 +1,4 @@
1
- import { DEAD, claimGraph, cmpStamp } from '../model.js';
1
+ import { DEAD, cmpStamp } from '../model.js';
2
2
  import { addTo, dropFrom } from './shared.js';
3
3
  /** The claim graph of one doc's index, over its state. */
4
4
  export const indexClaims = (ix) => {
@@ -16,7 +16,6 @@ export const indexClaims = (ix) => {
16
16
  const topInv = new Map();
17
17
  /** Claimed block → the blocks whose claims list names it (live or not). */
18
18
  const claimersOf = new Map();
19
- let ownersBuilt = false;
20
19
  /** The max-stamp claim on `m` held by a live block. */
21
20
  const topClaim = (m) => {
22
21
  let best;
@@ -91,27 +90,6 @@ export const indexClaims = (ix) => {
91
90
  addTo(displaysMap, to, b);
92
91
  };
93
92
  const ensureOwners = () => {
94
- if (!ownersBuilt) {
95
- ownersBuilt = true;
96
- ownerSeeds.clear();
97
- const graph = claimGraph(blocks);
98
- owners.clear();
99
- for (const [b, o] of graph.owners)
100
- if (blocks.has(b))
101
- owners.set(b, o);
102
- topOf.clear();
103
- topInv.clear();
104
- claimersOf.clear();
105
- for (const [holder, rec] of blocks)
106
- for (const c of rec.claims)
107
- addTo(claimersOf, c.m, holder);
108
- for (const m of claimersOf.keys())
109
- setTop(m, topClaim(m));
110
- displaysMap.clear();
111
- for (const b of blocks.keys())
112
- setDisplay(b, undefined, owners.get(b));
113
- return;
114
- }
115
93
  if (ownerSeeds.size === 0)
116
94
  return;
117
95
  // The tops a seed's claims (now, and those it dropped: noted in `claimersOf` changes) decide.
@@ -4,20 +4,18 @@
4
4
  * they read.
5
5
  */
6
6
  import type { BlockId, ChildSlot } from '../../placement/model.js';
7
- import type { DisplayRoles } from '../runs.js';
7
+ import type { KindRole } from '../runs.js';
8
8
  import type { IndexState } from './state.js';
9
9
  /** The layout rules of one doc's index, over its state. */
10
10
  export declare const indexLayout: (ix: IndexState) => {
11
- role: <T>(b: BlockId, ask: (r: DisplayRoles, type: string) => T) => T | undefined;
11
+ kindOf: (b: BlockId) => KindRole | undefined;
12
12
  lineKind: (b: BlockId) => string | undefined;
13
13
  itemKind: (b: BlockId) => string | undefined;
14
14
  rowKind: (b: BlockId) => string | undefined;
15
15
  rowOfTable: (table: string) => [string, string] | undefined;
16
- declaredRows: () => Map<string, string>;
17
16
  cellKind: (b: BlockId) => string | undefined;
18
17
  cellRank: (row: BlockId, id: BlockId) => string | undefined;
19
18
  tableFacts: (b: BlockId, data: unknown) => string;
20
- declaredItems: () => Set<string>;
21
19
  dissolve: (kids: Map<BlockId | null, ChildSlot[]>) => Set<BlockId>;
22
20
  redissolve: (changed: Map<BlockId, [BlockId | null | undefined, BlockId | null | undefined]>, kinds: Iterable<BlockId>) => Set<BlockId>;
23
21
  };
@@ -3,33 +3,22 @@ import { listedColumns, unlistedRank } from '../../tables.js';
3
3
  /** The layout rules of one doc's index, over its state. */
4
4
  export const indexLayout = (ix) => {
5
5
  const { blocks, kindsOf } = ix;
6
- /** `ask` of `b`'s stored kind; `undefined` without roles or a record. */
7
- const role = (b, ask) => {
6
+ /** The role of `b`'s stored kind; `undefined` without roles or a record. */
7
+ const kindOf = (b) => {
8
8
  const type = blocks.get(b)?.type;
9
- return ix.roles === null || type === undefined ? undefined : ask(ix.roles, type);
9
+ return ix.roles === null || type === undefined ? undefined : ix.roles.of(type);
10
10
  };
11
11
  /** The line kind of `b` when it is an island declared `lines`. */
12
- const lineKind = (b) => role(b, (r, type) => r.line(type));
12
+ const lineKind = (b) => kindOf(b)?.line;
13
13
  /** The item kind of `b` when it is a layout. */
14
- const itemKind = (b) => role(b, (r, type) => r.layout(type));
15
- /** The item kinds the roles declare. */
16
- const declaredItems = () => {
17
- const items = new Set();
18
- for (const type of ix.roles?.layoutKinds() ?? [])
19
- items.add(ix.roles.layout(type));
20
- return items;
21
- };
14
+ const itemKind = (b) => kindOf(b)?.layout;
22
15
  /** The row kind of `b` when it is a table (`table.*`). */
23
- const rowKind = (b) => role(b, (r, type) => r.table(type));
16
+ const rowKind = (b) => kindOf(b)?.table;
24
17
  /** `table`'s row kind and that row kind's cell kind, as `[row, cell]`. */
25
18
  const rowOfTable = (table) => {
26
- const row = ix.roles?.table(table);
27
- return row === undefined ? undefined : [row, ix.roles.defaultChild(row)];
19
+ const row = ix.roles?.of(table).table;
20
+ return row === undefined ? undefined : [row, ix.roles.of(row).child];
28
21
  };
29
- /** The row kinds (with their cell kinds) the roles declare. */
30
- const declaredRows = () => new Map([...(ix.roles?.tableKinds() ?? [])]
31
- .flatMap((type) => rowOfTable(type) ?? [])
32
- .map(([row, cell]) => [row, cell]));
33
22
  /** The cell kind of `b` when it is a row of a table kind. */
34
23
  const cellKind = (b) => ix.rowKinds.get(blocks.get(b)?.type ?? '');
35
24
  /**
@@ -255,16 +244,14 @@ export const indexLayout = (ix) => {
255
244
  return flips;
256
245
  };
257
246
  return {
258
- role,
247
+ kindOf,
259
248
  lineKind,
260
249
  itemKind,
261
250
  rowKind,
262
251
  rowOfTable,
263
- declaredRows,
264
252
  cellKind,
265
253
  cellRank,
266
254
  tableFacts,
267
- declaredItems,
268
255
  dissolve,
269
256
  redissolve
270
257
  };
@@ -31,7 +31,7 @@ import { engineOps, firstItem } from '../../structs.js';
31
31
  import { DEAD, displayOf } from '../model.js';
32
32
  /** Placement maintenance of one doc's index, over its state, claims, streams and layout rules. */
33
33
  export const indexPlacement = (ix) => {
34
- const { doc, registry, blocks, displaysMap, ownerChanged, placementsMap, following, dirtyLists, leftLists, placementSeeds, stateSeeds, kindsOf, slotsByBlock, rawOrder, edges, fallen, rejected, ensureOwners, ownerOf, top, streamOf, streamsIn, streamAt, role, lineKind, itemKind, rowKind, rowOfTable, declaredRows, cellKind, cellRank, declaredItems, dissolve, redissolve } = ix;
34
+ const { doc, registry, blocks, displaysMap, ownerChanged, placementsMap, following, dirtyLists, leftLists, placementSeeds, stateSeeds, kindsOf, slotsByBlock, rawOrder, edges, fallen, rejected, ensureOwners, ownerOf, top, streamOf, streamsIn, streamAt, kindOf, lineKind, itemKind, rowKind, rowOfTable, cellKind, cellRank, dissolve, redissolve } = ix;
35
35
  const kept = engineOps(Y).isKeptReplaced;
36
36
  // ── placements and the children index (maintained) ────────────
37
37
  /** The document holds a layout or an item kind: `kids0` and `kidsMap` differ. */
@@ -47,9 +47,9 @@ export const indexPlacement = (ix) => {
47
47
  const ownShim = {
48
48
  ownerOf,
49
49
  hidden: (b) => ownerOf(b) !== b || ix.dissolved.has(b),
50
- childless: (b) => role(b, (r, type) => r.childless(type)) === true,
51
- island: (b) => role(b, (r, type) => r.island(type)) === true,
52
- container: (b) => role(b, (r, type) => r.container(type)) === true,
50
+ childless: (b) => kindOf(b)?.childless === true,
51
+ island: (b) => kindOf(b)?.island === true,
52
+ container: (b) => kindOf(b)?.container === true,
53
53
  lined: (b) => lineKind(b) !== undefined,
54
54
  passes: (b) => ix.dissolved.has(b),
55
55
  sheds: (owner, child) => {
@@ -500,14 +500,15 @@ export const indexPlacement = (ix) => {
500
500
  placementSeeds.clear();
501
501
  stateSeeds.clear();
502
502
  ownerChanged.clear();
503
- ix.itemKinds = declaredItems();
503
+ const declared = ix.roles?.declared();
504
+ ix.itemKinds = new Set(declared?.items);
504
505
  for (const b of blocks.keys()) {
505
506
  const item = itemKind(b);
506
507
  if (item !== undefined)
507
508
  ix.itemKinds.add(item);
508
509
  }
509
- lineKinds = new Set(ix.roles?.lineKinds());
510
- ix.rowKinds = declaredRows();
510
+ lineKinds = new Set(declared?.lines);
511
+ ix.rowKinds = new Map(declared?.rows);
511
512
  for (const b of blocks.keys()) {
512
513
  const type = blocks.get(b)?.type;
513
514
  const rows = type === undefined ? undefined : rowOfTable(type);
@@ -670,10 +671,8 @@ export const indexPlacement = (ix) => {
670
671
  */
671
672
  const itemOf = (under, plain) => {
672
673
  const parent = typeOf(under);
673
- if (!ix.roles.container(parent))
674
- return plain;
675
- const item = ix.roles.defaultChild(parent);
676
- return ix.roles.rendersContent(item) ? item : plain;
674
+ const { container, child } = ix.roles.of(parent);
675
+ return container && ix.roles.of(child).rendersContent ? child : plain;
677
676
  };
678
677
  /**
679
678
  * The kind `id` displays as — THE shown-kind rule, read from its slot
@@ -694,7 +693,8 @@ export const indexPlacement = (ix) => {
694
693
  const typeOf = (id) => {
695
694
  const stored = blocks.get(id)?.type ?? 'unknown';
696
695
  const slot = ix.slots.get(id);
697
- if (ix.roles === null || slot === undefined)
696
+ const roles = ix.roles;
697
+ if (roles === null || slot === undefined)
698
698
  return stored;
699
699
  const { parent: under, reset } = slot;
700
700
  const line = under === null || lineKinds.size === 0 ? undefined : lineKind(under);
@@ -702,26 +702,23 @@ export const indexPlacement = (ix) => {
702
702
  return line;
703
703
  const lined = lineKinds.has(stored);
704
704
  const from = reset === undefined ? undefined : (blocks.get(reset)?.type ?? null);
705
- if (!lined && (from === undefined || stored !== ix.roles.defaultChild(from)))
706
- return stored === ix.roles.defaultChild(null) && under !== null
707
- ? itemOf(under, stored)
708
- : stored;
705
+ const plain = roles.of(null).child;
706
+ if (!lined && (from === undefined || stored !== roles.of(from).child))
707
+ return stored === plain && under !== null ? itemOf(under, stored) : stored;
709
708
  // Out of a removed list, inside an outer list of its kind: still an item. A
710
709
  // container whose item is the document's default kind (a column) holds no
711
710
  // items of its own: a paragraph under one is no list's item.
712
- if (!lined && typeof from === 'string' && ix.roles.container(from))
711
+ if (!lined && typeof from === 'string' && roles.of(from).container)
713
712
  for (let u = under; u !== null; u = ix.slots.get(u)?.parent ?? null) {
714
- const t = typeOf(u);
715
- if (ix.roles.container(t) &&
716
- ix.roles.defaultChild(t) === stored &&
717
- stored !== ix.roles.defaultChild(null))
713
+ const t = roles.of(typeOf(u));
714
+ if (t.container && t.child === stored && stored !== plain)
718
715
  return stored;
719
716
  }
720
- const kind = ix.roles.defaultChild(under === null ? null : typeOf(under));
721
- if (ix.roles.rendersContent(kind) || !ix.roles.rendersContent(stored))
717
+ const kind = roles.of(under === null ? null : typeOf(under)).child;
718
+ if (roles.of(kind).rendersContent || !roles.of(stored).rendersContent)
722
719
  return kind;
723
720
  // Never a line kind outside its island, even where the slot's kind shows nothing.
724
- return lined ? ix.roles.defaultChild(null) : stored;
721
+ return lined ? plain : stored;
725
722
  };
726
723
  /** The document order, cached until a child list changes. */
727
724
  const order = () => (orderCache ??= documentOrder(ix.kidsMap));
@@ -67,20 +67,23 @@ export const EMPTY_IDS = Object.freeze([]);
67
67
  * its children.
68
68
  */
69
69
  export const sameShape = (roles, a, b) => {
70
- const lines = new Set(roles.lineKinds());
71
- const items = new Set([...roles.layoutKinds()].map((type) => roles.layout(type)));
72
- const rows = new Set([...roles.tableKinds()].flatMap((type) => roles.table(type) ?? []));
73
- const cells = new Set([...rows].map((row) => roles.defaultChild(row)));
74
- return (roles.childless(a) === roles.childless(b) &&
75
- roles.island(a) === roles.island(b) &&
76
- roles.container(a) === roles.container(b) &&
77
- roles.line(a) === roles.line(b) &&
78
- lines.has(a) === lines.has(b) &&
79
- roles.layout(a) === roles.layout(b) &&
80
- items.has(a) === items.has(b) &&
81
- roles.table(a) === roles.table(b) &&
82
- rows.has(a) === rows.has(b) &&
83
- cells.has(a) === cells.has(b));
70
+ const { lines, items, rows, cells } = roles.declared();
71
+ const shape = (t) => {
72
+ const k = roles.of(t);
73
+ return JSON.stringify([
74
+ k.childless,
75
+ k.island,
76
+ k.container,
77
+ k.line,
78
+ lines.has(t),
79
+ k.layout,
80
+ items.has(t),
81
+ k.table,
82
+ rows.has(t),
83
+ cells.has(t)
84
+ ]);
85
+ };
86
+ return shape(a) === shape(b);
84
87
  };
85
88
  /**
86
89
  * The index's self-checks, off in production: a test lane's setup installs