edytor 3.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.
- package/README.md +2 -2
- package/dist/block/block.svelte.js +6 -21
- package/dist/block/inlineBlock.svelte.js +4 -6
- package/dist/clipboard/moveText.js +6 -8
- package/dist/cloudflare/DocumentRoom.d.ts +23 -12
- package/dist/cloudflare/DocumentRoom.js +10 -6
- package/dist/cloudflare/index.d.ts +1 -1
- package/dist/cloudflare/room/access.d.ts +8 -3
- package/dist/cloudflare/room/access.js +44 -20
- package/dist/cloudflare/room/admission.js +28 -5
- package/dist/cloudflare/room/comments.js +28 -1
- package/dist/cloudflare/room/context.d.ts +139 -129
- package/dist/cloudflare/room/context.js +11 -4
- package/dist/cloudflare/room/forged.d.ts +14 -0
- package/dist/cloudflare/room/forged.js +30 -2
- package/dist/cloudflare/room/frames.d.ts +4 -0
- package/dist/cloudflare/room/frames.js +5 -1
- package/dist/cloudflare/room/purge.d.ts +6 -3
- package/dist/cloudflare/room/purge.js +45 -19
- package/dist/cloudflare/room/replicas.d.ts +4 -2
- package/dist/cloudflare/room/replicas.js +8 -3
- package/dist/cloudflare/room/suggest.d.ts +7 -0
- package/dist/cloudflare/room/suggest.js +143 -0
- package/dist/cloudflare/room/updates.d.ts +3 -4
- package/dist/cloudflare/room/updates.js +6 -8
- package/dist/cloudflare/routeDocumentSocket.d.ts +10 -1
- package/dist/cloudflare/routeDocumentSocket.js +5 -3
- package/dist/components/Content.svelte +9 -1
- package/dist/crdt/anchors.js +3 -2
- package/dist/crdt/attribution/attribution.d.ts +34 -12
- package/dist/crdt/attribution/attribution.js +59 -21
- package/dist/crdt/attribution/block.d.ts +3 -3
- package/dist/crdt/attribution/block.js +7 -8
- package/dist/crdt/attribution/index.d.ts +1 -1
- package/dist/crdt/attribution/index.js +1 -1
- package/dist/crdt/data.d.ts +62 -31
- package/dist/crdt/data.js +211 -103
- package/dist/crdt/doc/capability.js +9 -13
- package/dist/crdt/doc/delete.d.ts +1 -1
- package/dist/crdt/doc/delete.js +13 -30
- package/dist/crdt/doc/funnel.d.ts +1 -1
- package/dist/crdt/doc/funnel.js +34 -26
- package/dist/crdt/doc/gate.d.ts +3 -3
- package/dist/crdt/doc/history.d.ts +31 -9
- package/dist/crdt/doc/history.js +204 -73
- package/dist/crdt/doc/layout.js +16 -27
- package/dist/crdt/doc/meta.js +17 -23
- package/dist/crdt/doc/moves.d.ts +1 -1
- package/dist/crdt/doc/moves.js +52 -32
- package/dist/crdt/doc/reads.d.ts +2 -2
- package/dist/crdt/doc/reads.js +20 -30
- package/dist/crdt/doc/seed.js +2 -2
- package/dist/crdt/doc/split-merge.js +12 -15
- package/dist/crdt/doc/steps.d.ts +25 -13
- package/dist/crdt/doc/steps.js +55 -125
- package/dist/crdt/doc/suggest.d.ts +167 -0
- package/dist/crdt/doc/suggest.js +579 -0
- package/dist/crdt/doc/types.d.ts +21 -9
- package/dist/crdt/document.d.ts +26 -6
- package/dist/crdt/document.js +40 -15
- package/dist/crdt/edytor-doc.d.ts +410 -390
- package/dist/crdt/edytor-doc.js +143 -95
- package/dist/crdt/engine-api.d.ts +39 -1
- package/dist/crdt/engine.d.ts +2 -2
- package/dist/crdt/engine.js +4 -5
- package/dist/crdt/flow.d.ts +2 -2
- package/dist/crdt/flow.js +29 -37
- package/dist/crdt/index.d.ts +134 -127
- package/dist/crdt/index.js +4 -1
- package/dist/crdt/operations.d.ts +12 -3
- package/dist/crdt/parts.d.ts +43 -0
- package/dist/crdt/parts.js +44 -0
- package/dist/crdt/placement/display.d.ts +6 -31
- package/dist/crdt/placement/display.js +41 -146
- package/dist/crdt/placement/keys.d.ts +63 -0
- package/dist/crdt/placement/keys.js +95 -0
- package/dist/crdt/placement/model.d.ts +119 -118
- package/dist/crdt/placement/model.js +242 -142
- package/dist/crdt/placement/resolve.d.ts +123 -35
- package/dist/crdt/placement/resolve.js +188 -153
- package/dist/crdt/placement/slots.d.ts +134 -0
- package/dist/crdt/placement/slots.js +152 -0
- package/dist/crdt/protocol.d.ts +1 -1
- package/dist/crdt/protocol.js +1 -1
- package/dist/crdt/protocols/auth.d.ts +11 -1
- package/dist/crdt/protocols/auth.js +17 -1
- package/dist/crdt/protocols/comments.d.ts +16 -1
- package/dist/crdt/protocols/comments.js +9 -1
- package/dist/crdt/providers/index.d.ts +6 -4
- package/dist/crdt/providers/index.js +3 -1
- package/dist/crdt/providers/room.d.ts +16 -2
- package/dist/crdt/providers/room.js +64 -17
- package/dist/crdt/providers/websocket.d.ts +6 -4
- package/dist/crdt/providers/websocket.js +14 -7
- package/dist/crdt/purge.d.ts +24 -9
- package/dist/crdt/purge.js +266 -126
- package/dist/crdt/restore.d.ts +4 -3
- package/dist/crdt/restore.js +27 -34
- package/dist/crdt/schema.d.ts +30 -21
- package/dist/crdt/schema.js +39 -23
- package/dist/crdt/storage.d.ts +1 -1
- package/dist/crdt/storage.js +1 -1
- package/dist/crdt/structs.d.ts +17 -15
- package/dist/crdt/structs.js +10 -22
- package/dist/crdt/text/index/cache.js +4 -0
- package/dist/crdt/text/index/claims.js +13 -23
- package/dist/crdt/text/index/fold.d.ts +2 -3
- package/dist/crdt/text/index/fold.js +194 -44
- package/dist/crdt/text/index/layout.d.ts +2 -4
- package/dist/crdt/text/index/layout.js +14 -30
- package/dist/crdt/text/index/placement.d.ts +26 -18
- package/dist/crdt/text/index/placement.js +420 -157
- package/dist/crdt/text/index/records.d.ts +3 -3
- package/dist/crdt/text/index/records.js +24 -31
- package/dist/crdt/text/index/shared.d.ts +11 -3
- package/dist/crdt/text/index/shared.js +29 -17
- package/dist/crdt/text/index/state.d.ts +21 -17
- package/dist/crdt/text/index/state.js +15 -17
- package/dist/crdt/text/index/streams.d.ts +10 -5
- package/dist/crdt/text/index/streams.js +78 -27
- package/dist/crdt/text/items.d.ts +10 -3
- package/dist/crdt/text/items.js +7 -0
- package/dist/crdt/text/model.d.ts +87 -62
- package/dist/crdt/text/model.js +91 -169
- package/dist/crdt/text/ranges.d.ts +1 -1
- package/dist/crdt/text/ranges.js +3 -3
- package/dist/crdt/text/rows.d.ts +13 -7
- package/dist/crdt/text/rows.js +24 -7
- package/dist/crdt/text/runs.d.ts +51 -42
- package/dist/crdt/text/runs.js +24 -7
- package/dist/crdt/text/streams.d.ts +26 -13
- package/dist/crdt/text/streams.js +41 -14
- package/dist/crdt/vendor/yjs/UPSTREAM.md +651 -21
- package/dist/crdt/vendor/yjs/dts/global.d.ts +2 -0
- package/dist/crdt/vendor/yjs/dts/index.d.ts +1 -1
- package/dist/crdt/vendor/yjs/dts/structs/AbstractStruct.d.ts +7 -0
- package/dist/crdt/vendor/yjs/dts/structs/Item.d.ts +197 -10
- package/dist/crdt/vendor/yjs/dts/utils/Doc.d.ts +6 -3
- package/dist/crdt/vendor/yjs/dts/utils/StructStore.d.ts +5 -18
- package/dist/crdt/vendor/yjs/dts/utils/Transaction.d.ts +15 -1
- package/dist/crdt/vendor/yjs/dts/utils/UpdateDecoder.d.ts +14 -6
- package/dist/crdt/vendor/yjs/dts/utils/UpdateEncoder.d.ts +33 -6
- package/dist/crdt/vendor/yjs/dts/utils/ids.d.ts +1 -65
- package/dist/crdt/vendor/yjs/dts/utils/marks.d.ts +6 -1
- package/dist/crdt/vendor/yjs/dts/ynode.d.ts +54 -7
- package/dist/crdt/vendor/yjs/global.d.ts +2 -0
- package/dist/crdt/vendor/yjs/src/index.js +1 -1
- package/dist/crdt/vendor/yjs/src/structs/AbstractStruct.js +10 -0
- package/dist/crdt/vendor/yjs/src/structs/Item.js +649 -51
- package/dist/crdt/vendor/yjs/src/utils/BlockSet.js +18 -9
- package/dist/crdt/vendor/yjs/src/utils/Doc.js +5 -3
- package/dist/crdt/vendor/yjs/src/utils/StructStore.js +5 -0
- package/dist/crdt/vendor/yjs/src/utils/Transaction.js +36 -13
- package/dist/crdt/vendor/yjs/src/utils/UpdateDecoder.js +88 -18
- package/dist/crdt/vendor/yjs/src/utils/UpdateEncoder.js +119 -32
- package/dist/crdt/vendor/yjs/src/utils/encoding.js +13 -1
- package/dist/crdt/vendor/yjs/src/utils/ids.js +7 -7
- package/dist/crdt/vendor/yjs/src/utils/marks.js +193 -8
- package/dist/crdt/vendor/yjs/src/utils/redo.js +1 -1
- package/dist/crdt/vendor/yjs/src/utils/transaction-helpers.js +1 -1
- package/dist/crdt/vendor/yjs/src/utils/updates.js +3 -3
- package/dist/crdt/vendor/yjs/src/ynode.js +199 -12
- package/dist/edytor.svelte.d.ts +1 -1
- package/dist/edytor.svelte.js +103 -75
- package/dist/events/beforeInputCommands.d.ts +2 -2
- package/dist/events/beforeInputCommands.js +2 -2
- package/dist/events/nativeInteractiveControl.d.ts +11 -1
- package/dist/events/nativeInteractiveControl.js +41 -1
- package/dist/events/onBeforeInput.js +3 -3
- package/dist/events/onFocus.js +9 -11
- package/dist/events/onPageHide.d.ts +3 -0
- package/dist/events/onPageHide.js +16 -0
- package/dist/export/forms.d.ts +50 -50
- package/dist/index.d.ts +4 -2
- package/dist/index.js +1 -1
- package/dist/labels.d.ts +8 -0
- package/dist/labels.js +6 -1
- package/dist/plugins/comments/CommentsController.svelte.d.ts +5 -1
- package/dist/plugins/comments/CommentsController.svelte.js +14 -4
- package/dist/plugins/find/search.js +2 -2
- package/dist/plugins/index.d.ts +1 -0
- package/dist/plugins/index.js +1 -0
- package/dist/plugins/suggestions/SuggestionBars.svelte +40 -23
- package/dist/plugins/suggestions/SuggestionBars.svelte.d.ts +3 -1
- package/dist/plugins/suggestions/SuggestionRuns.svelte +77 -0
- package/dist/plugins/suggestions/SuggestionRuns.svelte.d.ts +9 -0
- package/dist/plugins/suggestions/shared.svelte.d.ts +58 -0
- package/dist/plugins/suggestions/shared.svelte.js +421 -0
- package/dist/plugins/suggestions/suggestionsPlugin.d.ts +15 -4
- package/dist/selection/domSelection.d.ts +2 -0
- package/dist/selection/domSelection.js +6 -2
- package/dist/selection/pointer.svelte.js +75 -48
- package/dist/selection/replaceSelection.js +2 -6
- package/dist/selection/selection.svelte.d.ts +3 -3
- package/dist/selection/selection.svelte.js +39 -32
- package/dist/selection/selection.utils.d.ts +14 -0
- package/dist/selection/selection.utils.js +28 -3
- package/dist/session/commands.js +21 -11
- package/dist/session/composition.svelte.d.ts +5 -3
- package/dist/session/composition.svelte.js +21 -17
- package/dist/session/handles.d.ts +4 -17
- package/dist/session/handles.js +11 -37
- package/dist/session/navigation.js +6 -9
- package/dist/session/selection.d.ts +11 -5
- package/dist/session/selection.js +14 -48
- package/dist/session/suggestions.svelte.d.ts +38 -9
- package/dist/session/suggestions.svelte.js +37 -18
- package/dist/session/virtual.d.ts +1 -1
- package/dist/surface/attributes.d.ts +5 -0
- package/dist/surface/attributes.js +2 -2
- package/dist/surface/cells.d.ts +2 -26
- package/dist/surface/cells.js +2 -33
- package/dist/surface/observer.svelte.d.ts +6 -1
- package/dist/surface/observer.svelte.js +130 -83
- package/dist/surface/pin.svelte.d.ts +9 -9
- package/dist/surface/pin.svelte.js +15 -22
- package/dist/surface/projector.svelte.d.ts +15 -3
- package/dist/surface/projector.svelte.js +67 -22
- package/dist/text/text.svelte.js +14 -19
- package/dist/themes/notion.css +29 -0
- package/package.json +4 -2
- package/dist/crdt/placement/rank.d.ts +0 -87
- package/dist/crdt/placement/rank.js +0 -332
- package/dist/crdt/placement/source.d.ts +0 -43
- package/dist/crdt/placement/source.js +0 -66
- package/dist/crdt/text/delete-index.d.ts +0 -129
- package/dist/crdt/text/delete-index.js +0 -365
- package/dist/crdt/text/deletes.d.ts +0 -76
- package/dist/crdt/text/deletes.js +0 -362
- package/dist/crdt/text/index/anchored.d.ts +0 -17
- package/dist/crdt/text/index/anchored.js +0 -197
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
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.
|
|
19
19
|
|
|
20
|
-
> **
|
|
20
|
+
> **5.0.** Edytor `5.0.0` is on npm as `edytor`: its API follows semantic versioning, and every change is listed in the changelog. It has no production track record yet: read the [limitations](https://edytor.dev/docs/reference/limitations) before you build on it. Issues and PRs are welcome: when you report a bug, include the document's JSON value.
|
|
21
21
|
|
|
22
22
|
## Features
|
|
23
23
|
|
|
@@ -40,7 +40,7 @@ Edytor aims to be for Svelte what Slate.js is for React: a heavily customizable
|
|
|
40
40
|
|
|
41
41
|
## Install
|
|
42
42
|
|
|
43
|
-
Edytor `
|
|
43
|
+
Edytor `5.0.0` is on npm under `latest`:
|
|
44
44
|
|
|
45
45
|
```bash
|
|
46
46
|
pnpm add edytor
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { cleanups } from '../utils.js';
|
|
2
2
|
import { Text } from '../text/text.svelte.js';
|
|
3
|
+
import { segmentAt } from '../crdt/parts.js';
|
|
3
4
|
/** Mark an element inside a block's markup as non-editable chrome (`block.void`, a preview's too). */
|
|
4
5
|
export const voidChrome = (node) => {
|
|
5
6
|
node.setAttribute('data-edytor-void', `true`);
|
|
@@ -63,12 +64,9 @@ export class Block {
|
|
|
63
64
|
get content() {
|
|
64
65
|
this.#track();
|
|
65
66
|
const handles = this.edytor.idToBlock;
|
|
66
|
-
let ordinal = 0;
|
|
67
67
|
return handles
|
|
68
68
|
.parts(this.id)
|
|
69
|
-
.map((part) => part.kind === 'text'
|
|
70
|
-
? handles.text(this.id, ordinal++)
|
|
71
|
-
: handles.atom(this.id, part.item.id));
|
|
69
|
+
.map((part) => part.kind === 'text' ? handles.text(this.id, part.segOrd) : handles.atom(this.id, part.id));
|
|
72
70
|
}
|
|
73
71
|
get type() {
|
|
74
72
|
if (this.isRoot)
|
|
@@ -299,23 +297,10 @@ export class Block {
|
|
|
299
297
|
* last text's end).
|
|
300
298
|
*/
|
|
301
299
|
textAtOffset = (offset) => {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
if (offset <= at)
|
|
307
|
-
return hit;
|
|
308
|
-
at += 1;
|
|
309
|
-
}
|
|
310
|
-
else if (offset <= at + part.length) {
|
|
311
|
-
return { text: part, offset: Math.max(0, offset - at) };
|
|
312
|
-
}
|
|
313
|
-
else {
|
|
314
|
-
hit = { text: part, offset: part.length };
|
|
315
|
-
at += part.length;
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
return hit;
|
|
300
|
+
this.#track();
|
|
301
|
+
const { start, end, segOrd } = segmentAt(this.edytor.idToBlock.parts(this.id), offset);
|
|
302
|
+
const text = this.edytor.idToBlock.text(this.id, segOrd);
|
|
303
|
+
return { text, offset: Math.min(Math.max(0, offset - start), end - start) };
|
|
319
304
|
};
|
|
320
305
|
/** The declared view state (`open` on a `details`) the block's last element held. */
|
|
321
306
|
#viewState = [];
|
|
@@ -27,9 +27,9 @@ export class InlineBlock {
|
|
|
27
27
|
get #item() {
|
|
28
28
|
if ($effect.tracking())
|
|
29
29
|
this.edytor.cells?.get(this.blockId);
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
return this.edytor.idToBlock
|
|
31
|
+
.parts(this.blockId)
|
|
32
|
+
.find((part) => part.kind === 'inline' && part.id === this.id);
|
|
33
33
|
}
|
|
34
34
|
get selected() {
|
|
35
35
|
return this.edytor.selection.selectedInlineBlock.has(this);
|
|
@@ -66,9 +66,8 @@ export class InlineBlock {
|
|
|
66
66
|
node.dataset.edytorId = this.id;
|
|
67
67
|
node.dataset.edytorInlineBlock = this.type;
|
|
68
68
|
this.#attachedNodes.add(node);
|
|
69
|
-
this.edytor.nodeToInlineBlock.set(node, this);
|
|
70
69
|
this.node = node;
|
|
71
|
-
const release = this.edytor.surface.register(node, 'atom', this.blockId);
|
|
70
|
+
const release = this.edytor.surface.register(node, 'atom', this.blockId, undefined, undefined, this);
|
|
72
71
|
const selectInlineBlock = (event) => {
|
|
73
72
|
if (event.button !== 0)
|
|
74
73
|
return;
|
|
@@ -100,7 +99,6 @@ export class InlineBlock {
|
|
|
100
99
|
if (this.node === node) {
|
|
101
100
|
this.node = this.#attachedNodes.values().next().value;
|
|
102
101
|
}
|
|
103
|
-
this.edytor.nodeToInlineBlock.delete(node);
|
|
104
102
|
release();
|
|
105
103
|
}
|
|
106
104
|
};
|
|
@@ -74,14 +74,12 @@ export const dropText = (edytor, drag, to) => {
|
|
|
74
74
|
/** The text and offset an anchor resolves to. */
|
|
75
75
|
const point = (anchor) => {
|
|
76
76
|
const hit = anchor && facade.resolveAnchor(anchor);
|
|
77
|
-
|
|
78
|
-
|
|
77
|
+
return hit && edytor.idToBlock.has(hit.blockId)
|
|
78
|
+
? { block: hit.blockId, offset: hit.offset }
|
|
79
|
+
: null;
|
|
79
80
|
};
|
|
80
|
-
const [
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
edytor.selection.selectText(startText, startOffset, endText, endOffset);
|
|
84
|
-
else if (endText)
|
|
85
|
-
edytor.selection.selectText(endText, endOffset);
|
|
81
|
+
const [from, at] = [point(placed.from), point(placed.to)];
|
|
82
|
+
if (at)
|
|
83
|
+
edytor.selection.selectAt(from ?? at, at);
|
|
86
84
|
return true;
|
|
87
85
|
};
|
|
@@ -106,6 +106,7 @@ import * as E from '../crdt/protocol.js';
|
|
|
106
106
|
import type { PurgeReport } from '../crdt/purge.js';
|
|
107
107
|
import { type HistoryEntry, type HistoryOptions, type KVLike, type R2BucketLike } from './history.js';
|
|
108
108
|
import type { LockOptions } from './locks.js';
|
|
109
|
+
import type { DocumentAccessLevel } from './routeDocumentSocket.js';
|
|
109
110
|
import type { CommitResult, ExportedBlocks, ImportReceipt, ImportRequest, LateEditBatch, MoveNamespace } from './move.js';
|
|
110
111
|
import type { AwarenessEntry } from '../crdt/protocol.js';
|
|
111
112
|
import type { DocumentSemanticsConfig, DocumentOperations, JSONBlock, JSONDoc, YDoc } from '../crdt/index.js';
|
|
@@ -121,9 +122,11 @@ export declare const DEFAULT_SAVE_AFTER = 2000;
|
|
|
121
122
|
* Room quotas, each refused with `4413` (`quota: <name>`). The
|
|
122
123
|
* document's size: what its records hold (uncompressed) and the updates the
|
|
123
124
|
* engine holds waiting. An isolate has 128 MB, and the live document takes
|
|
124
|
-
*
|
|
125
|
-
* block, its index included; `bench/room-memory.mjs
|
|
126
|
-
* about
|
|
125
|
+
* 15 to 77 bytes of heap per stored byte in Node (prose to one short line
|
|
126
|
+
* per block, its index included; `bench/room-memory.mjs`; a Worker takes
|
|
127
|
+
* less): 2 MiB is about 150 to 160 MB at worst, a list of some 23,000 short
|
|
128
|
+
* blocks (1.25 MiB keeps that near 100 MB). Raise it for documents of long
|
|
129
|
+
* text, lower it for long lists of short blocks.
|
|
127
130
|
*/
|
|
128
131
|
export declare const DEFAULT_MAX_DOCUMENT_BYTES: number;
|
|
129
132
|
/** The share of the document quota past which the room logs its size, by default. */
|
|
@@ -256,7 +259,13 @@ export type SocketIdentity = {
|
|
|
256
259
|
user: string;
|
|
257
260
|
/** `null` until bound — by `authorize`, or by the socket's first presence entry. */
|
|
258
261
|
replica: number | null;
|
|
262
|
+
/** It writes nothing to the document (a `read` or `comment` access). */
|
|
259
263
|
readOnly: boolean;
|
|
264
|
+
/**
|
|
265
|
+
* Less than `write`, more than `read`: `suggest` writes only suggestions
|
|
266
|
+
* (`room.access.suggest`), `comment` only comments (`room.access.comment`).
|
|
267
|
+
*/
|
|
268
|
+
access?: 'suggest' | 'comment';
|
|
260
269
|
/**
|
|
261
270
|
* When its credential expires (ms since the epoch, the room's clock):
|
|
262
271
|
* the room closes it `4401` then (`room.access`). `null` or absent: never.
|
|
@@ -272,7 +281,7 @@ export type Attachment = SocketIdentity & {
|
|
|
272
281
|
comments?: boolean;
|
|
273
282
|
};
|
|
274
283
|
export type Refusal = {
|
|
275
|
-
reason: 'generation' | 'schema' | 'malformed' | 'container' | 'identity' | 'replica' | 'read-only' | 'waiting' | 'storage' | 'internal' | 'quota' | 'access' | 'presence' | 'comments' | 'mark' | 'forged' | 'denied' | 'orphan' | 'relayed' | 'history';
|
|
284
|
+
reason: 'generation' | 'schema' | 'malformed' | 'container' | 'identity' | 'replica' | 'read-only' | 'waiting' | 'storage' | 'internal' | 'quota' | 'access' | 'presence' | 'comments' | 'mark' | 'suggest-only' | 'forged' | 'denied' | 'orphan' | 'relayed' | 'history';
|
|
276
285
|
detail: unknown;
|
|
277
286
|
};
|
|
278
287
|
/** A duration series: how many, their total and longest (ms; see {@link RoomMetrics}). */
|
|
@@ -364,7 +373,7 @@ export type RoomLogEntry = {
|
|
|
364
373
|
user: string;
|
|
365
374
|
} | ({
|
|
366
375
|
edytor: 'purge';
|
|
367
|
-
horizon: number;
|
|
376
|
+
horizon: number | null;
|
|
368
377
|
bytes: number;
|
|
369
378
|
} & PurgeReport) | {
|
|
370
379
|
edytor: 'quota';
|
|
@@ -801,12 +810,14 @@ export declare class AttachedDocument {
|
|
|
801
810
|
* The purge task, also over RPC (`room.purge.timing`): record an epoch
|
|
802
811
|
* (the state vector, now) when the document changed since the last one,
|
|
803
812
|
* purge what was deleted before the horizon — the newest epoch at least
|
|
804
|
-
* `purgeAfterDays` old — when it is newer than the last purged,
|
|
805
|
-
*
|
|
806
|
-
* `null`
|
|
813
|
+
* `purgeAfterDays` old — when it is newer than the last purged, with what
|
|
814
|
+
* the sessions whose history ended deleted (`hist.release.end`; with no
|
|
815
|
+
* epoch due, those alone, `horizon: null`), and re-arm for the next epoch
|
|
816
|
+
* to pass it. Returns what the purge wrote, or `null` when nothing was
|
|
817
|
+
* past the horizon and no session had ended.
|
|
807
818
|
*/
|
|
808
819
|
purge(): (PurgeReport & {
|
|
809
|
-
horizon: number;
|
|
820
|
+
horizon: number | null;
|
|
810
821
|
}) | null;
|
|
811
822
|
/** The room's counters, also over RPC — see {@link RoomMetrics}. */
|
|
812
823
|
metrics(): RoomMetrics;
|
|
@@ -845,7 +856,7 @@ export declare class AttachedDocument {
|
|
|
845
856
|
* socket `4403` (`access revoked`), as {@link closeUser}. Your `authorize` must decide the
|
|
846
857
|
* same from then on. Returns how many sockets it changed.
|
|
847
858
|
*/
|
|
848
|
-
setAccess(userId: string, access:
|
|
859
|
+
setAccess(userId: string, access: DocumentAccessLevel | 'none'): {
|
|
849
860
|
sockets: number;
|
|
850
861
|
};
|
|
851
862
|
/** Is `ws` one of this document's sockets? */
|
|
@@ -968,7 +979,7 @@ export declare class DocumentRoom<Env = DocumentRoomEnv> extends DurableObject<E
|
|
|
968
979
|
};
|
|
969
980
|
/** Run the purge task now, also over RPC — see {@link AttachedDocument.purge}. */
|
|
970
981
|
purge(): (PurgeReport & {
|
|
971
|
-
horizon: number;
|
|
982
|
+
horizon: number | null;
|
|
972
983
|
}) | null;
|
|
973
984
|
/** Every comment thread, also over RPC — see {@link AttachedDocument.listComments}. */
|
|
974
985
|
listComments(): CommentSnapshot;
|
|
@@ -979,7 +990,7 @@ export declare class DocumentRoom<Env = DocumentRoomEnv> extends DurableObject<E
|
|
|
979
990
|
sockets: number;
|
|
980
991
|
};
|
|
981
992
|
/** Change a user's access on its sockets, also over RPC — see {@link AttachedDocument.setAccess}. */
|
|
982
|
-
setAccess(userId: string, access:
|
|
993
|
+
setAccess(userId: string, access: DocumentAccessLevel | 'none'): {
|
|
983
994
|
sockets: number;
|
|
984
995
|
};
|
|
985
996
|
/** Compaction, also over RPC. */
|
|
@@ -130,9 +130,11 @@ export const DEFAULT_SAVE_AFTER = 2000;
|
|
|
130
130
|
* Room quotas, each refused with `4413` (`quota: <name>`). The
|
|
131
131
|
* document's size: what its records hold (uncompressed) and the updates the
|
|
132
132
|
* engine holds waiting. An isolate has 128 MB, and the live document takes
|
|
133
|
-
*
|
|
134
|
-
* block, its index included; `bench/room-memory.mjs
|
|
135
|
-
* about
|
|
133
|
+
* 15 to 77 bytes of heap per stored byte in Node (prose to one short line
|
|
134
|
+
* per block, its index included; `bench/room-memory.mjs`; a Worker takes
|
|
135
|
+
* less): 2 MiB is about 150 to 160 MB at worst, a list of some 23,000 short
|
|
136
|
+
* blocks (1.25 MiB keeps that near 100 MB). Raise it for documents of long
|
|
137
|
+
* text, lower it for long lists of short blocks.
|
|
136
138
|
*/
|
|
137
139
|
export const DEFAULT_MAX_DOCUMENT_BYTES = 2 * 1024 * 1024;
|
|
138
140
|
/** The share of the document quota past which the room logs its size, by default. */
|
|
@@ -480,9 +482,11 @@ export class AttachedDocument {
|
|
|
480
482
|
* The purge task, also over RPC (`room.purge.timing`): record an epoch
|
|
481
483
|
* (the state vector, now) when the document changed since the last one,
|
|
482
484
|
* purge what was deleted before the horizon — the newest epoch at least
|
|
483
|
-
* `purgeAfterDays` old — when it is newer than the last purged,
|
|
484
|
-
*
|
|
485
|
-
* `null`
|
|
485
|
+
* `purgeAfterDays` old — when it is newer than the last purged, with what
|
|
486
|
+
* the sessions whose history ended deleted (`hist.release.end`; with no
|
|
487
|
+
* epoch due, those alone, `horizon: null`), and re-arm for the next epoch
|
|
488
|
+
* to pass it. Returns what the purge wrote, or `null` when nothing was
|
|
489
|
+
* past the horizon and no session had ended.
|
|
486
490
|
*/
|
|
487
491
|
purge() {
|
|
488
492
|
return this.room.purge.tick();
|
|
@@ -10,4 +10,4 @@ export { type PurgeReport } from '../crdt/purge.js';
|
|
|
10
10
|
export { COMMENT_ROW_BYTES, COMMENT_STATUS, MAX_COMMENT_LENGTH, MAX_COMMENTS_PER_THREAD, MAX_THREADS, commentAnchors, commentBytes, decideComment, type ThreadComment, type CommentActor, type CommentChange, type CommentOutcome, type CommentRefusal, type CommentRequest, type CommentRun, type CommentSnapshot, type CommentThread } from '../crdt/protocols/comments.js';
|
|
11
11
|
export { lockedBlocks, type LockOptions } from './locks.js';
|
|
12
12
|
export { moveBlocksBetweenRooms, forwardLateEdits, DEFAULT_MOVE_GRACE_DAYS, type CommitResult, type ExportedBlocks, type ImportReceipt, type ImportRequest, type LateEdit, type LateEditBatch, type MoveDestination, type MovedState, type MoveNamespace, type MoveRoom } from './move.js';
|
|
13
|
-
export { routeDocumentSocket, routeDocumentHistory, routeDocumentComments, requestedReplica, type AuthorizeDocumentSocket, type ExpiredCredential, type DocumentNamespace, type DocumentIdentity, type RouteDocumentOptions } from './routeDocumentSocket.js';
|
|
13
|
+
export { routeDocumentSocket, routeDocumentHistory, routeDocumentComments, requestedReplica, type AuthorizeDocumentSocket, type ExpiredCredential, type DocumentNamespace, type DocumentAccessLevel, type DocumentIdentity, type RouteDocumentOptions } from './routeDocumentSocket.js';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type RoomContext } from './context.js';
|
|
2
|
+
import type { DocumentAccessLevel } from '../routeDocumentSocket.js';
|
|
2
3
|
/** A Yjs client id (a non-negative safe integer), or `null`. */
|
|
3
4
|
export declare const parseReplica: (raw: unknown) => number | null;
|
|
4
5
|
export declare class RoomAccess {
|
|
@@ -11,15 +12,19 @@ export declare class RoomAccess {
|
|
|
11
12
|
/** The open sockets of `user`. */
|
|
12
13
|
private socketsOf;
|
|
13
14
|
/** Close `sockets` with `code`, their departure announced, the change logged (`access`). */
|
|
14
|
-
endAccess(sockets: WebSocket[], access: 'expired' | 'closed' | '
|
|
15
|
+
endAccess(sockets: WebSocket[], access: 'expired' | 'closed' | 'none' | DocumentAccessLevel, code: number, reason: string): void;
|
|
15
16
|
/** Revoke: close every socket of `userId` (`AttachedDocument.closeUser`). */
|
|
16
17
|
closeUser(userId: string, code?: number, reason?: string): {
|
|
17
18
|
sockets: number;
|
|
18
19
|
};
|
|
19
20
|
/** Close every socket of `userId`, logged as `access`. */
|
|
20
21
|
private revoke;
|
|
21
|
-
/**
|
|
22
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Change `userId`'s access on its open sockets (`AttachedDocument.setAccess`):
|
|
24
|
+
* a downgrade to `read` in place, any other change by a `1012` close
|
|
25
|
+
* the provider redials (so `authorize` grants the new access).
|
|
26
|
+
*/
|
|
27
|
+
setAccess(userId: string, access: DocumentAccessLevel | 'none'): {
|
|
23
28
|
sockets: number;
|
|
24
29
|
};
|
|
25
30
|
/**
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { CLOSE } from '../../crdt/providers/room.js';
|
|
9
9
|
import { COMMENTS_HEADER, HISTORY_HEADER, HISTORY_KEY_HEADER, IDENTITY_HEADERS, PROBE_HEADER, SOCKET_TAG, closedSocket, noTimers } from './shared.js';
|
|
10
10
|
import { STORAGE_FAILURE } from './context.js';
|
|
11
|
-
import { readOnlyFrame, step1Frame } from './frames.js';
|
|
11
|
+
import { accessFrame, readOnlyFrame, step1Frame } from './frames.js';
|
|
12
12
|
import { StorageFault } from './replicas.js';
|
|
13
13
|
import { stateVector } from './updates.js';
|
|
14
14
|
/** The longest close reason the WebSocket API accepts, in UTF-8 bytes. */
|
|
@@ -22,6 +22,14 @@ export const parseReplica = (raw) => {
|
|
|
22
22
|
const n = typeof raw === 'string' && /^\d{1,16}$/.test(raw) ? Number(raw) : raw;
|
|
23
23
|
return typeof n === 'number' && Number.isSafeInteger(n) && n >= 0 ? n : null;
|
|
24
24
|
};
|
|
25
|
+
const LEVELS = ['read', 'comment', 'suggest', 'write'];
|
|
26
|
+
/** An access level as a socket's identity carries it (`readOnly`, and `access` between read and write). */
|
|
27
|
+
const levelOf = (level) => ({
|
|
28
|
+
readOnly: level === 'read' || level === 'comment',
|
|
29
|
+
...((level === 'suggest' || level === 'comment') && { access: level })
|
|
30
|
+
});
|
|
31
|
+
/** A socket identity's level. */
|
|
32
|
+
const levelIn = (identity) => identity.access ?? (identity.readOnly ? 'read' : 'write');
|
|
25
33
|
/** The user id `routeDocumentSocket` encoded into its header, or `null`. */
|
|
26
34
|
const readUser = (raw) => {
|
|
27
35
|
try {
|
|
@@ -40,14 +48,16 @@ const readIdentity = (headers) => {
|
|
|
40
48
|
const expiresAt = rawExpires === null ? null : Number(rawExpires);
|
|
41
49
|
if (!user || user.length > 256 || (rawReplica && replica === null))
|
|
42
50
|
return null;
|
|
43
|
-
if (access
|
|
51
|
+
if (!LEVELS.includes(access))
|
|
44
52
|
return null;
|
|
45
53
|
if (expiresAt !== null && !Number.isFinite(expiresAt))
|
|
46
54
|
return null;
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
55
|
+
return {
|
|
56
|
+
user,
|
|
57
|
+
replica,
|
|
58
|
+
...levelOf(access),
|
|
59
|
+
...(expiresAt !== null && { expiresAt })
|
|
60
|
+
};
|
|
51
61
|
};
|
|
52
62
|
export class RoomAccess {
|
|
53
63
|
room;
|
|
@@ -131,10 +141,20 @@ export class RoomAccess {
|
|
|
131
141
|
room.refuseContainer(server);
|
|
132
142
|
}
|
|
133
143
|
else {
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
|
|
144
|
+
// Every socket is told its access as it joins (`room.access.suggest`),
|
|
145
|
+
// before anything else it reads. A read-only socket is never asked
|
|
146
|
+
// for its state: it has nothing to give. It gets the read-only
|
|
147
|
+
// notice instead (not a refusal), so its provider tracks nothing,
|
|
148
|
+
// and its level after it.
|
|
149
|
+
const level = levelIn(identity);
|
|
150
|
+
if (identity.readOnly) {
|
|
151
|
+
room.send(server, readOnlyFrame());
|
|
152
|
+
room.send(server, accessFrame(level));
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
room.send(server, accessFrame(level));
|
|
156
|
+
room.send(server, step1Frame(doc));
|
|
157
|
+
}
|
|
138
158
|
if (room.state.presence.size > 0)
|
|
139
159
|
room.send(server, room.presence.snapshot());
|
|
140
160
|
}
|
|
@@ -183,26 +203,30 @@ export class RoomAccess {
|
|
|
183
203
|
return { sockets: sockets.length };
|
|
184
204
|
});
|
|
185
205
|
}
|
|
186
|
-
/**
|
|
206
|
+
/**
|
|
207
|
+
* Change `userId`'s access on its open sockets (`AttachedDocument.setAccess`):
|
|
208
|
+
* a downgrade to `read` in place, any other change by a `1012` close
|
|
209
|
+
* the provider redials (so `authorize` grants the new access).
|
|
210
|
+
*/
|
|
187
211
|
setAccess(userId, access) {
|
|
188
212
|
const room = this.room;
|
|
189
|
-
if (access !== '
|
|
213
|
+
if (access !== 'none' && !LEVELS.includes(access)) {
|
|
190
214
|
throw new RangeError(`setAccess: unknown access ${String(access)}`);
|
|
191
215
|
}
|
|
192
216
|
if (access === 'none')
|
|
193
217
|
return this.revoke(userId, 'none', CLOSE.denied, 'access revoked');
|
|
194
218
|
return noTimers(() => {
|
|
195
|
-
const sockets = this.socketsOf(userId).filter(({ attachment }) => attachment
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
}
|
|
200
|
-
for (const { ws, attachment } of sockets) {
|
|
219
|
+
const sockets = this.socketsOf(userId).filter(({ attachment }) => levelIn(attachment) !== access);
|
|
220
|
+
const inPlace = sockets.filter(({ attachment }) => access === 'read' && levelIn(attachment) === 'write');
|
|
221
|
+
const closed = sockets.filter((socket) => !inPlace.includes(socket));
|
|
222
|
+
this.endAccess(closed.map(({ ws }) => ws), access, CLOSE.accessChanged, 'access changed');
|
|
223
|
+
for (const { ws, attachment } of inPlace) {
|
|
201
224
|
ws.serializeAttachment({ ...attachment, readOnly: true });
|
|
202
225
|
room.send(ws, readOnlyFrame());
|
|
226
|
+
room.send(ws, accessFrame('read'));
|
|
203
227
|
}
|
|
204
|
-
if (
|
|
205
|
-
room.note({ reason: 'access', detail: { user: userId, access, sockets:
|
|
228
|
+
if (inPlace.length > 0) {
|
|
229
|
+
room.note({ reason: 'access', detail: { user: userId, access, sockets: inPlace.length } });
|
|
206
230
|
}
|
|
207
231
|
return { sockets: sockets.length };
|
|
208
232
|
});
|
|
@@ -14,8 +14,9 @@ import * as E from '../../crdt/protocol.js';
|
|
|
14
14
|
import { ChunkError, CLOSE } from '../../crdt/providers/room.js';
|
|
15
15
|
import { MAX_WAITING_DELETES, noTimers } from './shared.js';
|
|
16
16
|
import { STORAGE_FAILURE, now, sync, tally } from './context.js';
|
|
17
|
-
import { attributionWrites, BINDING_PREFIX, forgedAttributionDeletes, forgedDeletes, forgedWriters } from './forged.js';
|
|
18
|
-
import {
|
|
17
|
+
import { attributionWrites, BINDING_PREFIX, forgedAttributionDeletes, forgedDeletes, forgedWriters, selfBound } from './forged.js';
|
|
18
|
+
import { suggestOnly } from './suggest.js';
|
|
19
|
+
import { MalformedFrame, decode, readOnlyDenialFrame, savedFrame, suggestOnlyDenialFrame, step1Frame, storedStep2, updateFrame } from './frames.js';
|
|
19
20
|
import { StorageFault } from './replicas.js';
|
|
20
21
|
import { addIds, deletesUpdate, forgetWaiting, freedBy, heldDeletes, newWriters, pendingDeletes, rangeCount, releasedWaiting, replacedEntries, stateVector, storedStruct, unheldDeletes, withoutClients } from './updates.js';
|
|
21
22
|
/**
|
|
@@ -350,7 +351,8 @@ export class Admission {
|
|
|
350
351
|
}
|
|
351
352
|
const sv = stateVector(doc);
|
|
352
353
|
const orphans = new Set();
|
|
353
|
-
const
|
|
354
|
+
const bound = selfBound(doc, decoded.structs, attachment.user);
|
|
355
|
+
const stripped = replicas.attribute(attachment, newWriters(decoded, sv), sv, orphans, bound);
|
|
354
356
|
// Per-writer block marks: only `n` writes or deletes `del.<n>` /
|
|
355
357
|
// `wd.<n>`. A client writing another's is stripped whole, as a client
|
|
356
358
|
// under another user's id is; a delete of another's mark is dropped.
|
|
@@ -374,6 +376,23 @@ export class Admission {
|
|
|
374
376
|
detail: { user: attachment.user, writers: [...forgers], ranges: rangeCount(forgedMarks) }
|
|
375
377
|
});
|
|
376
378
|
}
|
|
379
|
+
// A user who may only suggest writes only suggestions (`room.access.suggest`).
|
|
380
|
+
const suggest = attachment.access === 'suggest'
|
|
381
|
+
? suggestOnly(doc, decoded, stripped, (client) => mayDelete(client), attachment.replica)
|
|
382
|
+
: null;
|
|
383
|
+
if (suggest && (suggest.strip.size > 0 || !suggest.deletes.isEmpty())) {
|
|
384
|
+
for (const client of suggest.strip)
|
|
385
|
+
stripped.add(client);
|
|
386
|
+
room.note({
|
|
387
|
+
reason: 'suggest-only',
|
|
388
|
+
detail: {
|
|
389
|
+
user: attachment.user,
|
|
390
|
+
writers: [...suggest.strip],
|
|
391
|
+
ranges: rangeCount(suggest.deletes)
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
room.send(ws, suggestOnlyDenialFrame());
|
|
395
|
+
}
|
|
377
396
|
// Attribution (`room.attribution.trust`): a replica binds only
|
|
378
397
|
// itself, to its verified user, and a user writes only its own
|
|
379
398
|
// profile. Another's entry is collected (its clock kept as a GC), a
|
|
@@ -405,9 +424,13 @@ export class Admission {
|
|
|
405
424
|
detail: { user: attachment.user, ranges: rangeCount(overflow) }
|
|
406
425
|
});
|
|
407
426
|
}
|
|
408
|
-
const
|
|
427
|
+
const suggestDeletes = suggest?.deletes ?? Y.createIdSet();
|
|
428
|
+
const dropped = overflow === null &&
|
|
429
|
+
forgedMarks.isEmpty() &&
|
|
430
|
+
forgedEntries.ids.isEmpty() &&
|
|
431
|
+
suggestDeletes.isEmpty()
|
|
409
432
|
? null
|
|
410
|
-
: addIds(addIds(addIds(Y.createIdSet(), overflow ?? Y.createIdSet()), forgedMarks), forgedEntries.ids);
|
|
433
|
+
: addIds(addIds(addIds(addIds(Y.createIdSet(), overflow ?? Y.createIdSet()), forgedMarks), forgedEntries.ids), suggestDeletes);
|
|
411
434
|
const admitted = stripped.size === 0 && dropped === null && attribution.collected.size === 0
|
|
412
435
|
? update
|
|
413
436
|
: withoutClients(decoded, stripped, doc, dropped ?? undefined, attribution.collected);
|
|
@@ -213,6 +213,9 @@ export class RoomComments {
|
|
|
213
213
|
this.broadcast(decided.change);
|
|
214
214
|
if (decided.change.type === 'removed')
|
|
215
215
|
this.#removeAnchors(decided.change.thread.id);
|
|
216
|
+
// A user who may comment but not write: the room writes the thread's anchor (`room.access.comment`).
|
|
217
|
+
if (decided.change.type === 'added' && actor.anchors && request.op === 'add')
|
|
218
|
+
this.#writeAnchors(decided.change.thread.id, request.anchors ?? []);
|
|
216
219
|
room.log({
|
|
217
220
|
edytor: 'comment',
|
|
218
221
|
type: decided.change.type,
|
|
@@ -232,6 +235,27 @@ export class RoomComments {
|
|
|
232
235
|
}
|
|
233
236
|
return outcome;
|
|
234
237
|
}
|
|
238
|
+
/** A new thread's anchor over `spans` (anchors), written by the room: one room transaction. */
|
|
239
|
+
#writeAnchors(id, spans) {
|
|
240
|
+
const room = this.room;
|
|
241
|
+
if (room.live === null || room.busy || spans.length === 0)
|
|
242
|
+
return;
|
|
243
|
+
try {
|
|
244
|
+
const mark = `${COMMENT_MARK}:${id}`;
|
|
245
|
+
room.transact((facade) => {
|
|
246
|
+
for (const { start, end } of spans) {
|
|
247
|
+
const [from, to] = [start, end].map((a) => facade.resolveAnchor(a));
|
|
248
|
+
if (from && to && from.blockId === to.blockId && to.offset > from.offset)
|
|
249
|
+
facade.formatRange(from.blockId, from.offset, to.offset - from.offset, {
|
|
250
|
+
[mark]: { id }
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
catch (error) {
|
|
256
|
+
room.log({ edytor: 'fault', reason: 'internal', detail: `comment anchor: ${String(error)}` });
|
|
257
|
+
}
|
|
258
|
+
}
|
|
235
259
|
/** A removed thread's anchor: its marks leave the document, as one room transaction. */
|
|
236
260
|
#removeAnchors(id) {
|
|
237
261
|
const room = this.room;
|
|
@@ -322,9 +346,12 @@ export class RoomComments {
|
|
|
322
346
|
const request = parseCommentRequest(json);
|
|
323
347
|
if (request === null)
|
|
324
348
|
return new Response('invalid request', { status: 400 });
|
|
349
|
+
// A user who may comment but not write: the room writes what its view may not (the anchor).
|
|
350
|
+
const anchors = identity.access === 'comment' || identity.access === 'suggest';
|
|
325
351
|
const outcome = await this.apply(request, {
|
|
326
352
|
user: identity.user,
|
|
327
|
-
readOnly: identity.readOnly
|
|
353
|
+
readOnly: identity.readOnly && !anchors,
|
|
354
|
+
...(anchors && { anchors })
|
|
328
355
|
});
|
|
329
356
|
if (outcome.status === 'refused')
|
|
330
357
|
return new Response(outcome.message, { status: COMMENT_STATUS[outcome.reason] });
|