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
@@ -43,6 +43,8 @@ export type EdytorOptions = {
43
43
  awareness?: Awareness;
44
44
  /** The local author of a view-owned document (with `document`, set it there). */
45
45
  actor?: DocumentActor;
46
+ /** A view-owned document's `requireHydration` (with `document`, set it there). */
47
+ requireHydration?: boolean;
46
48
  /** What this view shares of its selection with peers, and how often (`edytor.presence`). */
47
49
  presence?: PresenceOptions;
48
50
  sync?: boolean;
@@ -193,7 +195,7 @@ export declare class Edytor {
193
195
  */
194
196
  historyUndo: () => void;
195
197
  historyRedo: () => void;
196
- constructor({ snippets, readonly, hotKeys, plugins, document, doc, awareness, actor, presence, sync, value, onSelectionChange, placeholder, onChange }: EdytorOptions);
198
+ constructor({ snippets, readonly, hotKeys, plugins, document, doc, awareness, actor, requireHydration, presence, sync, value, onSelectionChange, placeholder, onChange }: EdytorOptions);
197
199
  /** Kinds already reported missing (DEV, once each). */
198
200
  private unknownKinds;
199
201
  /**
@@ -247,10 +247,13 @@ export class Edytor {
247
247
  : 'noop';
248
248
  this.dispatcher.last = { operation: command, status };
249
249
  };
250
- constructor({ snippets, readonly, hotKeys, plugins, document, doc, awareness, actor, presence, sync, value, onSelectionChange, placeholder, onChange }) {
250
+ constructor({ snippets, readonly, hotKeys, plugins, document, doc, awareness, actor, requireHydration, presence, sync, value, onSelectionChange, placeholder, onChange }) {
251
251
  if (document !== undefined) {
252
- if (doc !== undefined || awareness !== undefined || actor !== undefined) {
253
- throw new Error('EdytorOptions: `document` cannot be combined with `doc`/`awareness`/`actor` — ' +
252
+ if (doc !== undefined ||
253
+ awareness !== undefined ||
254
+ actor !== undefined ||
255
+ requireHydration !== undefined) {
256
+ throw new Error('EdytorOptions: `document` cannot be combined with `doc`/`awareness`/`actor`/`requireHydration` — ' +
254
257
  'the document owns them (compose them via attachDocument first).');
255
258
  }
256
259
  this.document = document;
@@ -258,7 +261,7 @@ export class Edytor {
258
261
  else {
259
262
  // Legacy path — the view internally owns a document composed
260
263
  // around the injected (or a fresh) doc/awareness.
261
- this.document = attachDocument(doc ?? new Y.Doc(), { awareness, actor });
264
+ this.document = attachDocument(doc ?? new Y.Doc(), { awareness, actor, requireHydration });
262
265
  this.ownsDocument = true;
263
266
  }
264
267
  this.readonly = readonly || false;
@@ -323,9 +326,9 @@ export class Edytor {
323
326
  // ANYTHING is applied, so a failed view cannot half-seed semantics.
324
327
  const blocks = Array.from(this.blocks);
325
328
  this.document.adoptSemantics({
326
- roles: Object.fromEntries(blocks.map(([type, { void: v, island, lines, layout }]) => [
329
+ roles: Object.fromEntries(blocks.map(([type, { void: v, island, lines, layout, atomic }]) => [
327
330
  type,
328
- { void: v, island, lines, layout }
331
+ { void: v, island, lines, layout, atomic }
329
332
  ])),
330
333
  rendersContent: Object.fromEntries(blocks.map(([type, definition]) => [type, definition.rendersContent !== false])),
331
334
  defaultChild
package/dist/index.d.ts CHANGED
@@ -41,4 +41,4 @@ export type { Suggestion, Suggestions, SuggestionAt, SuggestionContent, Suggesti
41
41
  export { textToBlocks, type TextToBlocksOptions } from './clipboard/textBlocks.js';
42
42
  export * from './plugins/index.js';
43
43
  export * from './crdt/index.js';
44
- export { createIndexeddbSync, createWebsocketSync, clearDocument, storeState, IndexeddbPersistence, WebsocketProvider, PresenceWriter, type PresenceOptions, type PresenceShare } from './collaboration/index.js';
44
+ export { createIndexeddbSync, createWebsocketSync, clearDocument, storeState, IndexeddbPersistence, WebsocketProvider, prefetch, lastUpdated, PresenceWriter, DEFAULT_PRESENCE_THROTTLE, type PresenceOptions, type PresenceShare } from './collaboration/index.js';
package/dist/index.js CHANGED
@@ -39,4 +39,4 @@ export * from './crdt/index.js';
39
39
  // needs no `edytor/crdt` import for the common IndexedDB/websocket cases.
40
40
  // The `EdytorSync*`/`IndexeddbSyncOptions`/`WebsocketSyncOptions` types are
41
41
  // already re-exported through `./crdt/index.js` above.
42
- export { createIndexeddbSync, createWebsocketSync, clearDocument, storeState, IndexeddbPersistence, WebsocketProvider, PresenceWriter } from './collaboration/index.js';
42
+ export { createIndexeddbSync, createWebsocketSync, clearDocument, storeState, IndexeddbPersistence, WebsocketProvider, prefetch, lastUpdated, PresenceWriter, DEFAULT_PRESENCE_THROTTLE } from './collaboration/index.js';
@@ -1,6 +1,6 @@
1
1
  <script lang="ts">
2
2
  import type { Block } from '../../block/block.svelte.js';
3
- import { safeImageSrc } from './image.js';
3
+ import { MAX_INLINE_IMAGE_BYTES, oversizedInlineImage, safeImageSrc } from './image.js';
4
4
 
5
5
  /**
6
6
  * Notion's empty image: "Add an image", then a link field (and Upload with
@@ -12,12 +12,15 @@
12
12
  $props();
13
13
  let draft = $state('');
14
14
  let open = $state(false);
15
- let failed = $state(false);
15
+ /** Why the last link or upload was not embedded (`null`: it was). */
16
+ let failed = $state<'invalid' | 'inline' | null>(null);
17
+ const inlineLimit = `${MAX_INLINE_IMAGE_BYTES / (1024 * 1024)} MB`;
16
18
 
17
19
  const embed = (value: string) => {
18
20
  const src = safeImageSrc(value);
19
- failed = !src;
20
- if (src && block) block.data.src = src;
21
+ // An inline image over the cap is never stored (H6): it would weigh on every sync.
22
+ failed = !src ? 'invalid' : oversizedInlineImage(src) ? 'inline' : null;
23
+ if (src && failed === null && block) block.data.src = src;
21
24
  };
22
25
  </script>
23
26
 
@@ -57,13 +60,23 @@
57
60
  try {
58
61
  embed(await upload(file));
59
62
  } catch {
60
- failed = true;
63
+ failed = 'invalid';
61
64
  }
62
65
  }}
63
66
  />
64
67
  </label>
65
68
  {/if}
66
- {#if failed}<small>That doesn't look like an image link or upload.</small>{/if}
69
+ {#if failed === 'inline'}
70
+ <small data-edytor-image-error="inline"
71
+ >Inline images are limited to {inlineLimit}: {upload
72
+ ? 'upload the file instead'
73
+ : 'host the image and paste its link'}.</small
74
+ >
75
+ {:else if failed}
76
+ <small data-edytor-image-error="invalid"
77
+ >That doesn't look like an image link or upload.</small
78
+ >
79
+ {/if}
67
80
  </div>
68
81
  {/if}
69
82
  {/if}
@@ -3,9 +3,14 @@
3
3
  import type { Block } from '../../block/block.svelte.js';
4
4
  import ImageEmpty from './ImageEmpty.svelte';
5
5
  import { imageKinds } from '../../crdt/semantics.js';
6
- import { safeImageSrc } from './image.js';
6
+ import {
7
+ MAX_INLINE_IMAGE_BYTES,
8
+ oversizedInlineImage,
9
+ safeImageSrc,
10
+ storableImageSrc
11
+ } from './image.js';
7
12
 
8
- export { safeImageSrc };
13
+ export { MAX_INLINE_IMAGE_BYTES, oversizedInlineImage, safeImageSrc, storableImageSrc };
9
14
 
10
15
  export type ImagePluginOptions = {
11
16
  /** Upload a picked file and answer its URL; without it only links are embedded. */
@@ -48,10 +53,11 @@
48
53
  const src = safeImageSrc(block.data?.src);
49
54
  return `<figure>${src ? `<img src="${escape(src)}" alt="">` : ''}<figcaption>${caption}</figcaption></figure>`;
50
55
  },
56
+ // A pasted inline image over the cap is not imported (H6).
51
57
  parse: (el) => {
52
58
  const src =
53
59
  el.localName === 'figure' &&
54
- safeImageSrc(el.querySelector('img')?.getAttribute('src'));
60
+ storableImageSrc(el.querySelector('img')?.getAttribute('src'));
55
61
  return src ? { src } : undefined;
56
62
  }
57
63
  }
@@ -1,6 +1,6 @@
1
1
  import type { Plugin } from '../../plugins.js';
2
- import { safeImageSrc } from './image.js';
3
- export { safeImageSrc };
2
+ import { MAX_INLINE_IMAGE_BYTES, oversizedInlineImage, safeImageSrc, storableImageSrc } from './image.js';
3
+ export { MAX_INLINE_IMAGE_BYTES, oversizedInlineImage, safeImageSrc, storableImageSrc };
4
4
  export type ImagePluginOptions = {
5
5
  /** Upload a picked file and answer its URL; without it only links are embedded. */
6
6
  upload?: (file: File) => Promise<string>;
@@ -1,2 +1,18 @@
1
1
  /** An image source the block may render: http(s), blob, or an inline image. */
2
2
  export declare const safeImageSrc: (value: unknown) => string | null;
3
+ /**
4
+ * The largest inline image (`data:` URL) an image block takes: 1 MiB of its
5
+ * URL (H6). An inline image is stored in the document and sent in every
6
+ * update and catch-up that carries the block, so a larger one would bloat
7
+ * the room's frames and storage: host the file instead (the plugin's
8
+ * `upload` option) and embed its URL.
9
+ */
10
+ export declare const MAX_INLINE_IMAGE_BYTES: number;
11
+ /** Is `src` an inline (`data:`) image larger than {@link MAX_INLINE_IMAGE_BYTES}? */
12
+ export declare const oversizedInlineImage: (src: string) => boolean;
13
+ /**
14
+ * An image source a block may store: {@link safeImageSrc}'s, but no
15
+ * inline image over {@link MAX_INLINE_IMAGE_BYTES}. Rendering still takes
16
+ * any safe source, so a document that holds a larger one shows it.
17
+ */
18
+ export declare const storableImageSrc: (value: unknown) => string | null;
@@ -5,3 +5,22 @@ export const safeImageSrc = (value) => {
5
5
  const src = value.trim();
6
6
  return /^(https?:|blob:)/i.test(src) || /^data:image\/[a-z0-9.+-]+[;,]/i.test(src) ? src : null;
7
7
  };
8
+ /**
9
+ * The largest inline image (`data:` URL) an image block takes: 1 MiB of its
10
+ * URL (H6). An inline image is stored in the document and sent in every
11
+ * update and catch-up that carries the block, so a larger one would bloat
12
+ * the room's frames and storage: host the file instead (the plugin's
13
+ * `upload` option) and embed its URL.
14
+ */
15
+ export const MAX_INLINE_IMAGE_BYTES = 1024 * 1024;
16
+ /** Is `src` an inline (`data:`) image larger than {@link MAX_INLINE_IMAGE_BYTES}? */
17
+ export const oversizedInlineImage = (src) => /^data:/i.test(src) && src.length > MAX_INLINE_IMAGE_BYTES;
18
+ /**
19
+ * An image source a block may store: {@link safeImageSrc}'s, but no
20
+ * inline image over {@link MAX_INLINE_IMAGE_BYTES}. Rendering still takes
21
+ * any safe source, so a document that holds a larger one shows it.
22
+ */
23
+ export const storableImageSrc = (value) => {
24
+ const src = safeImageSrc(value);
25
+ return src !== null && !oversizedInlineImage(src) ? src : null;
26
+ };
@@ -12,6 +12,6 @@ export type { ToolbarController } from './toolbar/ToolbarController.svelte.js';
12
12
  export { blockMenuPlugin, createBlockMenuPlugin } from './blockMenu/blockMenuPlugin.js';
13
13
  export { suggestionsPlugin, createSuggestionsPlugin, isSuggestionsPlugin, type SuggestionsOptions, type SuggestionBarPayload } from './suggestions/suggestionsPlugin.js';
14
14
  export type { BlockMenuOptions, BlockMenuController, BlockMenuAction } from './blockMenu/BlockMenuController.svelte.js';
15
- export { imagePlugin, createImagePlugin, isImagePlugin, safeImageSrc, type ImagePluginOptions } from './image/ImagePlugin.svelte';
15
+ export { imagePlugin, createImagePlugin, isImagePlugin, safeImageSrc, storableImageSrc, oversizedInlineImage, MAX_INLINE_IMAGE_BYTES, type ImagePluginOptions } from './image/ImagePlugin.svelte';
16
16
  export type { BlockHandleController } from './blockHandles/BlockHandleController.svelte.js';
17
17
  export { columnsPlugin, createColumnsPlugin, isColumnsPlugin, type ColumnsPluginOptions } from './columns/ColumnsPlugin.svelte';
@@ -9,5 +9,5 @@ export { slashMenuPlugin, createSlashMenuPlugin } from './slashMenu/slashMenuPlu
9
9
  export { toolbarPlugin, createToolbarPlugin } from './toolbar/toolbarPlugin.js';
10
10
  export { blockMenuPlugin, createBlockMenuPlugin } from './blockMenu/blockMenuPlugin.js';
11
11
  export { suggestionsPlugin, createSuggestionsPlugin, isSuggestionsPlugin } from './suggestions/suggestionsPlugin.js';
12
- export { imagePlugin, createImagePlugin, isImagePlugin, safeImageSrc } from './image/ImagePlugin.svelte';
12
+ export { imagePlugin, createImagePlugin, isImagePlugin, safeImageSrc, storableImageSrc, oversizedInlineImage, MAX_INLINE_IMAGE_BYTES } from './image/ImagePlugin.svelte';
13
13
  export { columnsPlugin, createColumnsPlugin, isColumnsPlugin } from './columns/ColumnsPlugin.svelte';
package/dist/plugins.d.ts CHANGED
@@ -246,6 +246,17 @@ export type BlockDefinition = {
246
246
  * in the delete contract). Adopted by the document as a role.
247
247
  */
248
248
  layout?: boolean;
249
+ /**
250
+ * Data paths written as one value (`data.atomic`): a top-level key, or an
251
+ * array of keys for a nested one. Setting `block.data.link`, or a key
252
+ * inside it, writes the whole `link` as one property, so two people
253
+ * setting it at once never merge into a value neither wrote (one wins
254
+ * whole); elsewhere each key merges on its own. Adopted by the document as
255
+ * part of the kind's role: every view of a document, a headless one and
256
+ * the room must declare the same paths (`semanticsOf({ embed: { atomic:
257
+ * ['link'] } })`).
258
+ */
259
+ atomic?: readonly (string | readonly string[])[];
249
260
  /**
250
261
  * Whether the snippet renders the block's own content (`content()`).
251
262
  * Defaults to `true`; containers that render only their children
@@ -57,6 +57,7 @@ export declare class Dispatcher {
57
57
  /** The plan `lead` holds for the next dispatched operation (a slash trigger's removal), if any. */
58
58
  get pendingLead(): Plan | null;
59
59
  /** Admission: a readonly view or a read-only document refuses every mutating command. */
60
+ /** Admission: not readonly, a writable document, and one decided when it requires hydration (H12). */
60
61
  permits: () => boolean;
61
62
  /** Apply the undo policy's cut before a `kind` command writes. */
62
63
  cut: (kind: string, phase?: "before" | "after") => void;
@@ -163,7 +163,10 @@ export class Dispatcher {
163
163
  return this.leading;
164
164
  }
165
165
  /** Admission: a readonly view or a read-only document refuses every mutating command. */
166
- permits = () => !this.edytor.readonly && this.edytor.document.writable;
166
+ /** Admission: not readonly, a writable document, and one decided when it requires hydration (H12). */
167
+ permits = () => !this.edytor.readonly &&
168
+ this.edytor.document.writable &&
169
+ (this.edytor.document.ready || !this.edytor.document.requireHydration);
167
170
  /** Apply the undo policy's cut before a `kind` command writes. */
168
171
  cut = (kind, phase = 'before') => {
169
172
  const policy = this.decide(policyOf(kind));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "edytor",
3
- "version": "0.1.0-next.21",
3
+ "version": "0.1.0-next.23",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/beynar/edytor.git"