edytor 6.0.0 → 7.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 +3 -3
- package/dist/cloudflare/DocumentRoom.d.ts +77 -2
- package/dist/cloudflare/DocumentRoom.js +37 -1
- package/dist/cloudflare/index.d.ts +1 -1
- package/dist/cloudflare/index.js +1 -1
- package/dist/cloudflare/room/access.d.ts +6 -1
- package/dist/cloudflare/room/access.js +30 -4
- package/dist/cloudflare/room/admission.d.ts +11 -0
- package/dist/cloudflare/room/admission.js +43 -10
- package/dist/cloudflare/room/context.d.ts +11 -2
- package/dist/cloudflare/room/context.js +17 -6
- package/dist/cloudflare/room/forged.d.ts +1 -1
- package/dist/cloudflare/room/forged.js +11 -9
- package/dist/cloudflare/room/frames.d.ts +2 -0
- package/dist/cloudflare/room/frames.js +2 -0
- package/dist/cloudflare/room/presence.js +1 -4
- package/dist/cloudflare/room/reseed.d.ts +93 -0
- package/dist/cloudflare/room/reseed.js +500 -0
- package/dist/cloudflare/room/scheduler.d.ts +1 -1
- package/dist/cloudflare/room/scheduler.js +9 -1
- package/dist/cloudflare/room/shared.d.ts +10 -0
- package/dist/cloudflare/room/shared.js +10 -0
- package/dist/cloudflare/room/storage.d.ts +7 -0
- package/dist/cloudflare/room/storage.js +33 -1
- package/dist/components/Edytor.svelte +18 -3
- package/dist/components/Edytor.svelte.d.ts +10 -1
- package/dist/components/EdytorRoot.svelte +17 -0
- package/dist/components/EdytorRoot.svelte.d.ts +4 -0
- package/dist/components/Suggestion.svelte +1 -1
- package/dist/crdt/attribution/block.d.ts +3 -0
- package/dist/crdt/attribution/block.js +3 -2
- package/dist/crdt/doc/suggest.d.ts +6 -2
- package/dist/crdt/doc/suggest.js +6 -5
- package/dist/crdt/edytor-doc.d.ts +1 -1
- package/dist/crdt/edytor-doc.js +1 -1
- package/dist/crdt/engine-room.d.ts +4 -0
- package/dist/crdt/engine-room.js +6 -0
- package/dist/crdt/index.d.ts +2 -1
- package/dist/crdt/protocol.d.ts +2 -2
- package/dist/crdt/protocol.js +2 -2
- package/dist/crdt/protocols/auth.d.ts +8 -1
- package/dist/crdt/protocols/auth.js +14 -1
- package/dist/crdt/providers/index.d.ts +1 -0
- package/dist/crdt/providers/index.js +22 -3
- package/dist/crdt/providers/room.d.ts +15 -0
- package/dist/crdt/providers/room.js +15 -0
- package/dist/crdt/providers/websocket.d.ts +4 -2
- package/dist/crdt/providers/websocket.js +36 -9
- package/dist/crdt/reseed.d.ts +127 -0
- package/dist/crdt/reseed.js +533 -0
- package/dist/crdt/restore.d.ts +13 -1
- package/dist/crdt/restore.js +8 -3
- package/dist/crdt/schema.d.ts +6 -0
- package/dist/crdt/schema.js +6 -0
- package/dist/crdt/structs.d.ts +8 -11
- package/dist/crdt/structs.js +25 -44
- package/dist/crdt/text/index/build.d.ts +3 -9
- package/dist/crdt/text/index/build.js +87 -181
- package/dist/crdt/vendor/yjs/UPSTREAM.md +101 -5
- package/dist/crdt/vendor/yjs/dts/index.d.ts +1 -1
- package/dist/crdt/vendor/yjs/dts/utils/Transaction.d.ts +10 -10
- package/dist/crdt/vendor/yjs/dts/utils/encoding.d.ts +1 -0
- package/dist/crdt/vendor/yjs/src/index.js +1 -1
- package/dist/crdt/vendor/yjs/src/structs/Item.js +24 -13
- package/dist/crdt/vendor/yjs/src/utils/Transaction.js +12 -10
- package/dist/crdt/vendor/yjs/src/utils/encoding.js +42 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/selection/selection.svelte.d.ts +1 -1
- package/dist/selection/selection.svelte.js +87 -106
- package/dist/session/commands.js +49 -51
- package/dist/surface/attributes.d.ts +3 -3
- package/dist/surface/attributes.js +2 -2
- package/dist/surface/observer.svelte.d.ts +7 -7
- package/dist/surface/observer.svelte.js +213 -309
- package/package.json +1 -1
|
@@ -193,6 +193,8 @@
|
|
|
193
193
|
params,
|
|
194
194
|
onSyncExpired,
|
|
195
195
|
onSyncRefused,
|
|
196
|
+
rebuild,
|
|
197
|
+
adopt,
|
|
196
198
|
requireHydration,
|
|
197
199
|
snapshot,
|
|
198
200
|
actor,
|
|
@@ -208,7 +210,15 @@
|
|
|
208
210
|
autocapitalize = 'none',
|
|
209
211
|
inputmode,
|
|
210
212
|
enterkeyhint
|
|
211
|
-
}: EdytorProps
|
|
213
|
+
}: EdytorProps & {
|
|
214
|
+
/** The view's own rebuild (`EdytorRoot`): a new view on a new document. */
|
|
215
|
+
rebuild?: () => void;
|
|
216
|
+
/**
|
|
217
|
+
* Hands the view to `EdytorRoot`, which a binding cannot: on the server a
|
|
218
|
+
* bound prop renders the tree twice, and the discarded view never ends.
|
|
219
|
+
*/
|
|
220
|
+
adopt?: (view: EdytorClass) => void;
|
|
221
|
+
} = $props();
|
|
212
222
|
|
|
213
223
|
const initialEdytorOptions = untrack(() => ({
|
|
214
224
|
readonly,
|
|
@@ -227,6 +237,7 @@
|
|
|
227
237
|
}));
|
|
228
238
|
|
|
229
239
|
edytor = new EdytorClass(initialEdytorOptions);
|
|
240
|
+
untrack(() => adopt)?.(edytor);
|
|
230
241
|
|
|
231
242
|
// ONE attach path for owned and injected documents: `attachSync`
|
|
232
243
|
// tracks the provider on the DOCUMENT's lifetime (one provider per
|
|
@@ -276,7 +287,11 @@
|
|
|
276
287
|
}
|
|
277
288
|
// Subscribe first, then replay the standing refusal isolated like the
|
|
278
289
|
// later ones: a throwing callback still hears the next refusal.
|
|
279
|
-
offRefused = document.onSyncRefused((next) =>
|
|
290
|
+
offRefused = document.onSyncRefused((next) => {
|
|
291
|
+
onSyncRefused?.(next);
|
|
292
|
+
// A re-seed (`4410`): a view that owns its document builds itself again on a new one.
|
|
293
|
+
if (next.code === CLOSE.reseeded && !edytorDocument) rebuild?.();
|
|
294
|
+
});
|
|
280
295
|
if (refusal) {
|
|
281
296
|
try {
|
|
282
297
|
onSyncRefused?.(refusal);
|
|
@@ -379,7 +394,7 @@
|
|
|
379
394
|
contenteditable="false"
|
|
380
395
|
aria-hidden="true"
|
|
381
396
|
style="display: none"
|
|
382
|
-
use:edytor.surface.
|
|
397
|
+
use:edytor.surface.own
|
|
383
398
|
></span>
|
|
384
399
|
</div>
|
|
385
400
|
{:else if snapshot}
|
|
@@ -113,6 +113,15 @@ export type EdytorProps = {
|
|
|
113
113
|
import type { JSONDoc } from '../utils/json.js';
|
|
114
114
|
import type { HotKey, HotKeyCombination } from '../session/keymap.js';
|
|
115
115
|
import type { EdytorSelection } from '../selection/selection.svelte.js';
|
|
116
|
-
|
|
116
|
+
type $$ComponentProps = EdytorProps & {
|
|
117
|
+
/** The view's own rebuild (`EdytorRoot`): a new view on a new document. */
|
|
118
|
+
rebuild?: () => void;
|
|
119
|
+
/**
|
|
120
|
+
* Hands the view to `EdytorRoot`, which a binding cannot: on the server a
|
|
121
|
+
* bound prop renders the tree twice, and the discarded view never ends.
|
|
122
|
+
*/
|
|
123
|
+
adopt?: (view: EdytorClass) => void;
|
|
124
|
+
};
|
|
125
|
+
declare const Edytor: import("svelte").Component<$$ComponentProps, {}, "edytor">;
|
|
117
126
|
type Edytor = ReturnType<typeof Edytor>;
|
|
118
127
|
export default Edytor;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// The public `<Edytor>`: the view, built again on a new document when the
|
|
3
|
+
// room re-seeds the one it owns (`room.reseed.protocol`).
|
|
4
|
+
import Edytor, { type EdytorProps } from './Edytor.svelte';
|
|
5
|
+
|
|
6
|
+
// eslint-disable-next-line no-useless-assignment -- bindable: `adopt` assigns it
|
|
7
|
+
let { edytor = $bindable(), ...props }: EdytorProps = $props();
|
|
8
|
+
let epoch = $state(0);
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<!-- A view built again loads the room's document: it never seeds its value over it. -->
|
|
12
|
+
{#key epoch}<Edytor
|
|
13
|
+
{...props}
|
|
14
|
+
requireHydration={epoch > 0 || props.requireHydration}
|
|
15
|
+
adopt={(view) => (edytor = view)}
|
|
16
|
+
rebuild={() => epoch++}
|
|
17
|
+
/>{/key}
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
contenteditable="false"
|
|
32
32
|
aria-label={suggestion.label ?? labelsOf(edytor, 'editor', editorEnglish).suggestion}
|
|
33
33
|
role="group"
|
|
34
|
-
use:edytor.surface.
|
|
34
|
+
use:edytor.surface.own={suggestion.id}
|
|
35
35
|
onmousedown={cancel}
|
|
36
36
|
>
|
|
37
37
|
{#each cells as cell (cell.id)}<Block kid={cell} />{/each}
|
|
@@ -78,6 +78,9 @@ export type BlockAttribution = {
|
|
|
78
78
|
lastChangedBy?: ActorId;
|
|
79
79
|
};
|
|
80
80
|
export { BLOCK_ATTR_ROOT, LAST_CHANGED_ATTR };
|
|
81
|
+
/** A record's `createdBy` key, and the prefix of its contributors' (a re-seed copies them: `crdt/reseed.ts`). */
|
|
82
|
+
export declare const CREATED_KEY = "c";
|
|
83
|
+
export declare const CONTRIBUTOR_PREFIX = "k/";
|
|
81
84
|
/** The `k/` member set on a `b/` record node. */
|
|
82
85
|
export declare const contributorsOfRecord: (rec: EngineNode | null) => Set<ActorId>;
|
|
83
86
|
/**
|
|
@@ -5,8 +5,9 @@ import { attrItems, newNode as makeNode } from '../structs.js';
|
|
|
5
5
|
// `../schema.js` leaf — re-exported so existing import sites stay.
|
|
6
6
|
export { BLOCK_ATTR_ROOT, LAST_CHANGED_ATTR };
|
|
7
7
|
const REC_NODE = 'brec';
|
|
8
|
-
|
|
9
|
-
const
|
|
8
|
+
/** A record's `createdBy` key, and the prefix of its contributors' (a re-seed copies them: `crdt/reseed.ts`). */
|
|
9
|
+
export const CREATED_KEY = 'c';
|
|
10
|
+
export const CONTRIBUTOR_PREFIX = 'k/';
|
|
10
11
|
/** Incarnation stamp on a record: the nonce `n` of the node it belongs to. */
|
|
11
12
|
const INCARNATION_KEY = 'i';
|
|
12
13
|
const recordOf = (doc, id) => {
|
|
@@ -89,8 +89,12 @@ export type SuggestContext = DocBase & DocReads & {
|
|
|
89
89
|
create: (doc: EngineDoc, config: EdytorDocConfig) => SuggestFacade;
|
|
90
90
|
/** Its changes no index fact shows: counted in the facade's `version`. */
|
|
91
91
|
v: number;
|
|
92
|
-
/**
|
|
93
|
-
|
|
92
|
+
/**
|
|
93
|
+
* The document without its pending suggestions, read by `read` (the
|
|
94
|
+
* export `toJSON()` gives unless asked for the suggestions);
|
|
95
|
+
* `undefined` while none is pending.
|
|
96
|
+
*/
|
|
97
|
+
base: (<T>(read: (other: SuggestFacade) => T) => T | undefined) | undefined;
|
|
94
98
|
};
|
|
95
99
|
/** The facade the part is built over: its `part`, and the operations it writes through. */
|
|
96
100
|
export type SuggestFacade = {
|
package/dist/crdt/doc/suggest.js
CHANGED
|
@@ -526,10 +526,11 @@ const docSuggest = (f) => {
|
|
|
526
526
|
return [...map.keys()].sort(c.compare).flatMap((b) => map.get(b));
|
|
527
527
|
};
|
|
528
528
|
/**
|
|
529
|
-
* The document without its pending suggestions (`sugg.json.base`)
|
|
530
|
-
* copy, every pending one rejected; `undefined` while
|
|
529
|
+
* The document without its pending suggestions (`sugg.json.base`), read
|
|
530
|
+
* by `read`: on a copy, every pending one rejected; `undefined` while
|
|
531
|
+
* none is pending.
|
|
531
532
|
*/
|
|
532
|
-
const base = () => {
|
|
533
|
+
const base = (read) => {
|
|
533
534
|
if (root().attrKeys().next().done || !changes().length)
|
|
534
535
|
return;
|
|
535
536
|
const { Y } = c;
|
|
@@ -538,11 +539,11 @@ const docSuggest = (f) => {
|
|
|
538
539
|
const other = c.create(copy, { ...c.config, actor: undefined });
|
|
539
540
|
const all = suggestionsOf(other);
|
|
540
541
|
all.run(all.decide(all.changes(), false));
|
|
541
|
-
const
|
|
542
|
+
const out = read(other);
|
|
542
543
|
all.dispose();
|
|
543
544
|
other.dispose();
|
|
544
545
|
copy.destroy();
|
|
545
|
-
return
|
|
546
|
+
return out;
|
|
546
547
|
};
|
|
547
548
|
c.base = base;
|
|
548
549
|
return {
|
|
@@ -1032,7 +1032,7 @@ export declare const bindEdytorDoc: (Y: EngineApi) => {
|
|
|
1032
1032
|
* Make the visible document `doc` equal `json`, keeping the ids the
|
|
1033
1033
|
* registry holds and writing only what differs (`room.history.restore`), inside the caller's transaction.
|
|
1034
1034
|
*/
|
|
1035
|
-
restoreTo: <Plan>(doc: EngineDoc, facade: import("./restore.js").RestoreFacade<Plan>, json: JSONDoc) => import("./restore.js").RestoreReport;
|
|
1035
|
+
restoreTo: <Plan>(doc: EngineDoc, facade: import("./restore.js").RestoreFacade<Plan>, json: JSONDoc, { rewrite, suggestions }?: import("./restore.js").RestoreOptions<Plan>) => import("./restore.js").RestoreReport;
|
|
1036
1036
|
model: {
|
|
1037
1037
|
REGISTRY_KEY: string;
|
|
1038
1038
|
registryOf: (doc: EngineDoc) => EngineNode;
|
package/dist/crdt/edytor-doc.js
CHANGED
|
@@ -220,7 +220,7 @@ export const docFacade = (Y, layers) => {
|
|
|
220
220
|
*/
|
|
221
221
|
const toJSON = (options) => {
|
|
222
222
|
// A part's export first (the shared suggestions': without the pending ones, `sugg.json.base`).
|
|
223
|
-
const base = !options?.suggestions && part.base?.();
|
|
223
|
+
const base = !options?.suggestions && part.base?.((other) => other.toJSON({ suggestions: true }));
|
|
224
224
|
if (base)
|
|
225
225
|
return base;
|
|
226
226
|
const data = docData();
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The engine calls only the room makes (a re-seed's cut and rebase,
|
|
3
|
+
* `crdt/reseed.ts`): apart from the engine object (`engine.js`), which every
|
|
4
|
+
* app keeps whole, so an app's bundle holds none of them.
|
|
5
|
+
*/
|
|
6
|
+
export { clipUpdate, diffUpdate } from './vendor/yjs/src/index.js';
|
package/dist/crdt/index.d.ts
CHANGED
|
@@ -360,7 +360,7 @@ export declare const bindCrdt: (Y: EngineApi) => {
|
|
|
360
360
|
text: import("./text/model.js").TextEngine;
|
|
361
361
|
}, horizon: import("./purge.js").Horizon | null) => import("./purge.js").PurgeReport;
|
|
362
362
|
horizonOf: (doc: import("./engine-api.js").EngineDoc) => import("./purge.js").Horizon | null;
|
|
363
|
-
restoreTo: <Plan>(doc: import("./engine-api.js").EngineDoc, facade: import("./restore.js").RestoreFacade<Plan>, json: import("./index.js").JSONDoc) => import("./restore.js").RestoreReport;
|
|
363
|
+
restoreTo: <Plan>(doc: import("./engine-api.js").EngineDoc, facade: import("./restore.js").RestoreFacade<Plan>, json: import("./index.js").JSONDoc, { rewrite, suggestions }?: import("./restore.js").RestoreOptions<Plan>) => import("./restore.js").RestoreReport;
|
|
364
364
|
model: {
|
|
365
365
|
REGISTRY_KEY: string;
|
|
366
366
|
registryOf: (doc: import("./engine-api.js").EngineDoc) => import("./engine-api.js").EngineNode;
|
|
@@ -551,6 +551,7 @@ export declare const bindCrdt: (Y: EngineApi) => {
|
|
|
551
551
|
deny(): void;
|
|
552
552
|
allow(): void;
|
|
553
553
|
};
|
|
554
|
+
_rebasing: boolean;
|
|
554
555
|
messageHandlers: Record<number, import("./providers/room.js").RoomMessageHandler</*elided*/ any>>;
|
|
555
556
|
hasSynced: boolean;
|
|
556
557
|
whenSynced: Promise<unknown>;
|
package/dist/crdt/protocol.d.ts
CHANGED
|
@@ -27,9 +27,9 @@
|
|
|
27
27
|
export { bindCrdt, type Crdt, type EngineApi, type EngineDoc, type EngineNode, type EngineTransaction, type EngineItemRef, type YNode, type YItem, type YDoc, type YUndoManager, type YTransaction } from './index.js';
|
|
28
28
|
export { SCHEMA_VERSION, META_KEY } from './edytor-doc.js';
|
|
29
29
|
export { GENERATION, generationWord, frame, PROTOCOL_VERSION, GENERATION_RECORD, GenerationMismatchError, readProtocolVersion, type GenerationRecord } from './protocols/envelope.js';
|
|
30
|
-
export { messageSync, messageAwareness, messageAuth, messageQueryAwareness, messageSaved, messageChunk, messageComments, MAX_FRAME_BYTES, chunkFrame, createChunkReader, ChunkError, type ChunkReader, CLOSE } from './providers/room.js';
|
|
30
|
+
export { messageSync, messageAwareness, messageAuth, messageQueryAwareness, messageSaved, messageChunk, messageComments, messageRebase, MAX_FRAME_BYTES, chunkFrame, createChunkReader, ChunkError, type ChunkReader, CLOSE } from './providers/room.js';
|
|
31
31
|
export { messageYjsSyncStep1, messageYjsSyncStep2, messageYjsUpdate, type SyncProtocol } from './protocols/sync.js';
|
|
32
|
-
export { messageAccess, messagePermissionDenied, messageReadOnly, writeAccess, writePermissionDenied, writeReadOnly } from './protocols/auth.js';
|
|
32
|
+
export { messageAccess, messageEpoch, messagePermissionDenied, messageReadOnly, writeAccess, writeEpoch, writePermissionDenied, writeReadOnly } from './protocols/auth.js';
|
|
33
33
|
export { commentsSubscribe, commentsSnapshot, commentsChange, writeCommentsSubscribe, writeCommentsSnapshot, writeCommentsChange, readCommentsMessage, type CommentChange, type CommentMessage, type CommentSnapshot, type CommentThread, type ThreadComment } from './protocols/comments.js';
|
|
34
34
|
export { applyAwarenessUpdate, encodeAwarenessUpdate, readAwarenessEntries, writeAwarenessEntries, type AwarenessEntry } from './protocols/awareness.js';
|
|
35
35
|
export { createDecoder, readVarUint, readVarUint8Array, type Decoder } from 'lib0-v14/decoding';
|
package/dist/crdt/protocol.js
CHANGED
|
@@ -30,9 +30,9 @@ export { bindCrdt } from './index.js';
|
|
|
30
30
|
export { SCHEMA_VERSION, META_KEY } from './edytor-doc.js';
|
|
31
31
|
export { GENERATION, generationWord, frame, PROTOCOL_VERSION, GENERATION_RECORD, GenerationMismatchError, readProtocolVersion } from './protocols/envelope.js';
|
|
32
32
|
// ── Message types, acknowledgements and chunks ─────────────────────────
|
|
33
|
-
export { messageSync, messageAwareness, messageAuth, messageQueryAwareness, messageSaved, messageChunk, messageComments, MAX_FRAME_BYTES, chunkFrame, createChunkReader, ChunkError, CLOSE } from './providers/room.js';
|
|
33
|
+
export { messageSync, messageAwareness, messageAuth, messageQueryAwareness, messageSaved, messageChunk, messageComments, messageRebase, MAX_FRAME_BYTES, chunkFrame, createChunkReader, ChunkError, CLOSE } from './providers/room.js';
|
|
34
34
|
export { messageYjsSyncStep1, messageYjsSyncStep2, messageYjsUpdate } from './protocols/sync.js';
|
|
35
|
-
export { messageAccess, messagePermissionDenied, messageReadOnly, writeAccess, writePermissionDenied, writeReadOnly } from './protocols/auth.js';
|
|
35
|
+
export { messageAccess, messageEpoch, messagePermissionDenied, messageReadOnly, writeAccess, writeEpoch, writePermissionDenied, writeReadOnly } from './protocols/auth.js';
|
|
36
36
|
export { commentsSubscribe, commentsSnapshot, commentsChange, writeCommentsSubscribe, writeCommentsSnapshot, writeCommentsChange, readCommentsMessage } from './protocols/comments.js';
|
|
37
37
|
export { applyAwarenessUpdate, encodeAwarenessUpdate, readAwarenessEntries, writeAwarenessEntries } from './protocols/awareness.js';
|
|
38
38
|
// Minimal wire codec for frame bodies (lib0 — the codec the frames are
|
|
@@ -28,6 +28,13 @@ export declare const writeReadOnly: (encoder: encoding.Encoder) => void;
|
|
|
28
28
|
*/
|
|
29
29
|
export declare const messageAccess = 2;
|
|
30
30
|
export declare const writeAccess: (encoder: encoding.Encoder, access: string) => void;
|
|
31
|
+
/**
|
|
32
|
+
* The room's epoch (`room.reseed.protocol`), sent to a socket that dialed an
|
|
33
|
+
* older one, instead of anything else: the room asks it for its old
|
|
34
|
+
* document (`messageRebase`), then closes it `4410`.
|
|
35
|
+
*/
|
|
36
|
+
export declare const messageEpoch = 3;
|
|
37
|
+
export declare const writeEpoch: (encoder: encoding.Encoder, epoch: number) => void;
|
|
31
38
|
export type PermissionDeniedHandler = (ydoc: unknown, reason: string) => void;
|
|
32
39
|
/**
|
|
33
40
|
* Decode one auth payload. Returns the auth message subtype so the caller
|
|
@@ -35,4 +42,4 @@ export type PermissionDeniedHandler = (ydoc: unknown, reason: string) => void;
|
|
|
35
42
|
* do not speak is protocol skew, and this stack's contract is that skew is
|
|
36
43
|
* reported, not silently dropped (same rule as unknown sync subtypes).
|
|
37
44
|
*/
|
|
38
|
-
export declare const readAuthMessage: (decoder: decoding.Decoder, y: unknown, permissionDeniedHandler: PermissionDeniedHandler, readOnlyHandler?: (ydoc: unknown) => void, accessHandler?: (access: string) => void) => number;
|
|
45
|
+
export declare const readAuthMessage: (decoder: decoding.Decoder, y: unknown, permissionDeniedHandler: PermissionDeniedHandler, readOnlyHandler?: (ydoc: unknown) => void, accessHandler?: (access: string) => void, epochHandler?: (epoch: number) => void) => number;
|
|
@@ -36,13 +36,23 @@ export const writeAccess = (encoder, access) => {
|
|
|
36
36
|
encoding.writeVarUint(encoder, messageAccess);
|
|
37
37
|
encoding.writeVarString(encoder, access);
|
|
38
38
|
};
|
|
39
|
+
/**
|
|
40
|
+
* The room's epoch (`room.reseed.protocol`), sent to a socket that dialed an
|
|
41
|
+
* older one, instead of anything else: the room asks it for its old
|
|
42
|
+
* document (`messageRebase`), then closes it `4410`.
|
|
43
|
+
*/
|
|
44
|
+
export const messageEpoch = 3;
|
|
45
|
+
export const writeEpoch = (encoder, epoch) => {
|
|
46
|
+
encoding.writeVarUint(encoder, messageEpoch);
|
|
47
|
+
encoding.writeVarUint(encoder, epoch);
|
|
48
|
+
};
|
|
39
49
|
/**
|
|
40
50
|
* Decode one auth payload. Returns the auth message subtype so the caller
|
|
41
51
|
* can surface unknown subtypes — a valid envelope carrying an auth type we
|
|
42
52
|
* do not speak is protocol skew, and this stack's contract is that skew is
|
|
43
53
|
* reported, not silently dropped (same rule as unknown sync subtypes).
|
|
44
54
|
*/
|
|
45
|
-
export const readAuthMessage = (decoder, y, permissionDeniedHandler, readOnlyHandler, accessHandler) => {
|
|
55
|
+
export const readAuthMessage = (decoder, y, permissionDeniedHandler, readOnlyHandler, accessHandler, epochHandler) => {
|
|
46
56
|
const authMessageType = decoding.readVarUint(decoder);
|
|
47
57
|
switch (authMessageType) {
|
|
48
58
|
case messagePermissionDenied:
|
|
@@ -54,6 +64,9 @@ export const readAuthMessage = (decoder, y, permissionDeniedHandler, readOnlyHan
|
|
|
54
64
|
break;
|
|
55
65
|
case messageAccess:
|
|
56
66
|
accessHandler?.(decoding.readVarString(decoder));
|
|
67
|
+
break;
|
|
68
|
+
case messageEpoch:
|
|
69
|
+
epochHandler?.(decoding.readVarUint(decoder));
|
|
57
70
|
}
|
|
58
71
|
return authMessageType;
|
|
59
72
|
};
|
|
@@ -243,6 +243,7 @@ export declare const bindProviders: (Y: EngineApi) => {
|
|
|
243
243
|
deny(): void;
|
|
244
244
|
allow(): void;
|
|
245
245
|
};
|
|
246
|
+
_rebasing: boolean;
|
|
246
247
|
messageHandlers: Record<number, import("./room.js").RoomMessageHandler</*elided*/ any>>;
|
|
247
248
|
hasSynced: boolean;
|
|
248
249
|
whenSynced: Promise<unknown>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { keepingReplaced } from '../incarnations.js';
|
|
2
2
|
import { bindIndexeddbProvider } from './indexeddb.js';
|
|
3
|
-
import { assertRoomId, SyncRefusedError } from './room.js';
|
|
3
|
+
import { assertRoomId, CLOSE, SyncRefusedError } from './room.js';
|
|
4
4
|
import { bindWebsocketProvider } from './websocket.js';
|
|
5
5
|
export const bindProviders = (Y) => {
|
|
6
6
|
const idb = bindIndexeddbProvider(Y);
|
|
@@ -50,7 +50,18 @@ export const bindProviders = (Y) => {
|
|
|
50
50
|
maxBackoffTime,
|
|
51
51
|
connectTimeout,
|
|
52
52
|
disableBc: disableBc || local !== undefined,
|
|
53
|
-
maxFrameBytes
|
|
53
|
+
maxFrameBytes,
|
|
54
|
+
// With a local copy, the dial waits for it: the epoch it names is the copy's.
|
|
55
|
+
connect: local === undefined
|
|
56
|
+
});
|
|
57
|
+
const dial = () => provider._destroyed || provider.connect();
|
|
58
|
+
// A re-seed (`room.reseed.protocol`, a `4410` refusal): the local copy goes,
|
|
59
|
+
// its deletion waiting for every tab's connection to close.
|
|
60
|
+
provider.on('refused', ({ code }) => {
|
|
61
|
+
if (code !== CLOSE.reseeded || !loaded)
|
|
62
|
+
return;
|
|
63
|
+
void release?.();
|
|
64
|
+
void idb.clearDocument(persistName);
|
|
54
65
|
});
|
|
55
66
|
if (onExpired)
|
|
56
67
|
provider.on('expired', (state) => onExpired(state));
|
|
@@ -94,9 +105,17 @@ export const bindProviders = (Y) => {
|
|
|
94
105
|
});
|
|
95
106
|
}
|
|
96
107
|
let release = undefined;
|
|
108
|
+
// The store's load (or its failure) opens the dial.
|
|
109
|
+
const loaded = local &&
|
|
110
|
+
Object.assign((p) => local({
|
|
111
|
+
...p,
|
|
112
|
+
synced: (x) => (p.synced(x), dial()),
|
|
113
|
+
failed: (error, x) => (p.failed?.(error, x), dial())
|
|
114
|
+
}), local);
|
|
97
115
|
try {
|
|
98
116
|
// Called directly (no `attach`), the store only persists: readiness is the socket's.
|
|
99
|
-
release =
|
|
117
|
+
release =
|
|
118
|
+
loaded && (attach ? attach(loaded) : loaded({ doc, awareness, synced: () => { } }));
|
|
100
119
|
}
|
|
101
120
|
catch (error) {
|
|
102
121
|
provider.destroy();
|
|
@@ -61,6 +61,12 @@ export declare const messageChunk = 5;
|
|
|
61
61
|
* room sends it only to a socket that subscribed.
|
|
62
62
|
*/
|
|
63
63
|
export declare const messageComments = 6;
|
|
64
|
+
/**
|
|
65
|
+
* Client → server, after a re-seed (`room.reseed.protocol`): the client's
|
|
66
|
+
* whole old document, its unsaved edits in it, for the room to rebase onto
|
|
67
|
+
* the current epoch. Sent only when the room asks (its epoch notice).
|
|
68
|
+
*/
|
|
69
|
+
export declare const messageRebase = 7;
|
|
64
70
|
/** A WebSocket message limit on Cloudflare (32 MiB): a larger frame is sent as chunks. */
|
|
65
71
|
export declare const MAX_FRAME_BYTES: number;
|
|
66
72
|
/**
|
|
@@ -203,6 +209,15 @@ export declare const CLOSE: {
|
|
|
203
209
|
readonly denied: 4403;
|
|
204
210
|
/** The dialed replica is bound to another user. */
|
|
205
211
|
readonly replicaTaken: 4409;
|
|
212
|
+
/**
|
|
213
|
+
* The room re-seeded the document (`room.reseed.protocol`; the reason is
|
|
214
|
+
* the room's epoch): a refusal of this document, once a client with
|
|
215
|
+
* unsaved edits redialed and handed them over. The new epoch is a new
|
|
216
|
+
* document's. Also the answer to a copy the room cannot rebase (an
|
|
217
|
+
* epoch past the cuts it keeps, or a later one) and to an older epoch's
|
|
218
|
+
* content on a socket of the current one.
|
|
219
|
+
*/
|
|
220
|
+
readonly reseeded: 4410;
|
|
206
221
|
/**
|
|
207
222
|
* A room quota refused the socket's write (`quota: document`,
|
|
208
223
|
* `quota: rate`, `quota: frame`): the frame was not applied, and a
|
|
@@ -63,6 +63,12 @@ export const messageChunk = 5;
|
|
|
63
63
|
* room sends it only to a socket that subscribed.
|
|
64
64
|
*/
|
|
65
65
|
export const messageComments = 6;
|
|
66
|
+
/**
|
|
67
|
+
* Client → server, after a re-seed (`room.reseed.protocol`): the client's
|
|
68
|
+
* whole old document, its unsaved edits in it, for the room to rebase onto
|
|
69
|
+
* the current epoch. Sent only when the room asks (its epoch notice).
|
|
70
|
+
*/
|
|
71
|
+
export const messageRebase = 7;
|
|
66
72
|
/** A WebSocket message limit on Cloudflare (32 MiB): a larger frame is sent as chunks. */
|
|
67
73
|
export const MAX_FRAME_BYTES = 32 * 1024 * 1024;
|
|
68
74
|
const chunkStart = 0;
|
|
@@ -334,6 +340,15 @@ export const CLOSE = {
|
|
|
334
340
|
denied: 4403,
|
|
335
341
|
/** The dialed replica is bound to another user. */
|
|
336
342
|
replicaTaken: 4409,
|
|
343
|
+
/**
|
|
344
|
+
* The room re-seeded the document (`room.reseed.protocol`; the reason is
|
|
345
|
+
* the room's epoch): a refusal of this document, once a client with
|
|
346
|
+
* unsaved edits redialed and handed them over. The new epoch is a new
|
|
347
|
+
* document's. Also the answer to a copy the room cannot rebase (an
|
|
348
|
+
* epoch past the cuts it keeps, or a later one) and to an older epoch's
|
|
349
|
+
* content on a socket of the current one.
|
|
350
|
+
*/
|
|
351
|
+
reseeded: 4410,
|
|
337
352
|
/**
|
|
338
353
|
* A room quota refused the socket's write (`quota: document`,
|
|
339
354
|
* `quota: rate`, `quota: frame`): the frame was not applied, and a
|
|
@@ -174,6 +174,8 @@ export declare const bindWebsocketProvider: (Y: EngineApi) => {
|
|
|
174
174
|
/** The room asks a read-only socket for its state again: what this actor wrote is unsaved until stored. */
|
|
175
175
|
allow(): void;
|
|
176
176
|
};
|
|
177
|
+
/** @internal The room re-seeded: this document's unsaved edits are being handed over (`room.reseed.protocol`). */
|
|
178
|
+
_rebasing: boolean;
|
|
177
179
|
messageHandlers: Record<number, RoomMessageHandler</*elided*/ any>>;
|
|
178
180
|
hasSynced: boolean;
|
|
179
181
|
whenSynced: Promise<unknown>;
|
|
@@ -193,8 +195,8 @@ export declare const bindWebsocketProvider: (Y: EngineApi) => {
|
|
|
193
195
|
* decodes it). `''`, `.`, `..`, an id over 256 characters and one with a
|
|
194
196
|
* lone surrogate are refused at construction ({@link assertRoomId}). `params` are read at every dial (a refreshed token
|
|
195
197
|
* reaches the next connection), with `replica` = this document's
|
|
196
|
-
* client id unless `params` names one
|
|
197
|
-
* writes to it.
|
|
198
|
+
* client id unless `params` names one (the room binds the socket's
|
|
199
|
+
* writes to it), and `epoch`, the document's, which no param replaces.
|
|
198
200
|
*/
|
|
199
201
|
get url(): string;
|
|
200
202
|
/** Updates of this document's actor the room has not acknowledged as persisted yet. */
|
|
@@ -47,18 +47,19 @@
|
|
|
47
47
|
* there (TCP never does).
|
|
48
48
|
*/
|
|
49
49
|
import * as decoding from 'lib0-v14/decoding';
|
|
50
|
+
import * as encoding from 'lib0-v14/encoding';
|
|
50
51
|
import * as time from 'lib0-v14/time';
|
|
51
52
|
import * as math from 'lib0-v14/math';
|
|
52
53
|
import * as url from 'lib0-v14/url';
|
|
53
54
|
import { Awareness, removeAwarenessStates } from '../protocols/awareness.js';
|
|
54
55
|
import { IsolatedObservable } from '../protocols/observable.js';
|
|
55
|
-
import { messagePermissionDenied, messageAccess, messageReadOnly, READ_ONLY_DENIAL, SUGGEST_ONLY_DENIAL, readAuthMessage } from '../protocols/auth.js';
|
|
56
|
+
import { messagePermissionDenied, messageAccess, messageEpoch, messageReadOnly, READ_ONLY_DENIAL, SUGGEST_ONLY_DENIAL, readAuthMessage } from '../protocols/auth.js';
|
|
56
57
|
import { bindSync } from '../protocols/sync.js';
|
|
57
58
|
import { readCommentsMessage, writeCommentsSubscribe } from '../protocols/comments.js';
|
|
58
59
|
import { frame } from '../protocols/envelope.js';
|
|
59
|
-
import { assertRoomId, beginDestroy, bindRoomProtocol, tabs, chunkFrame, CLOSE, createChunkReader, emitFailed, fault, initLifecycle, isRefusal, unknownType, LOCAL_PRESENCE_LOSS, markSynced, messageAuth, messageChunk, messageComments, messageSaved, messageSync, MAX_FRAME_BYTES, SyncRefusedError } from './room.js';
|
|
60
|
+
import { assertRoomId, beginDestroy, bindRoomProtocol, tabs, chunkFrame, CLOSE, createChunkReader, emitFailed, fault, initLifecycle, isRefusal, unknownType, LOCAL_PRESENCE_LOSS, markSynced, messageAuth, messageChunk, messageComments, messageRebase, messageSaved, messageSync, MAX_FRAME_BYTES, SyncRefusedError } from './room.js';
|
|
60
61
|
import { asEngineDoc } from '../structs.js';
|
|
61
|
-
import { ATTRIBUTION_ROOT } from '../schema.js';
|
|
62
|
+
import { ATTRIBUTION_ROOT, EPOCH_ROOT } from '../schema.js';
|
|
62
63
|
import { ATTRIBUTION_ORIGIN, writerOf } from '../attribution/attribution.js';
|
|
63
64
|
// @todo - this should depend on awareness.outdatedTime
|
|
64
65
|
const messageReconnectTimeout = 30000;
|
|
@@ -129,10 +130,17 @@ export const bindWebsocketProvider = (Y) => {
|
|
|
129
130
|
// The room's notice at the join: a state, not a refusal (no event).
|
|
130
131
|
() => provider._writes.deny(),
|
|
131
132
|
// The room's word at the join, and at a change in place (`read`).
|
|
132
|
-
(access) => provider.emit('access', [access, provider])
|
|
133
|
+
(access) => provider.emit('access', [access, provider]),
|
|
134
|
+
// The room re-seeded since this document's epoch: it asks for the
|
|
135
|
+
// document, its unsaved edits in it (`room.reseed.protocol`).
|
|
136
|
+
() => {
|
|
137
|
+
provider._rebasing = true;
|
|
138
|
+
send(provider, frame(messageRebase, (e) => encoding.writeVarUint8Array(e, Y.encodeStateAsUpdate(provider.doc))));
|
|
139
|
+
});
|
|
133
140
|
if (authType !== messagePermissionDenied &&
|
|
134
141
|
authType !== messageReadOnly &&
|
|
135
|
-
authType !== messageAccess
|
|
142
|
+
authType !== messageAccess &&
|
|
143
|
+
authType !== messageEpoch) {
|
|
136
144
|
// An auth type we do not speak inside a valid v14 envelope is
|
|
137
145
|
// protocol skew — reported, never dropped silently.
|
|
138
146
|
unknownType(provider, 'auth', authType);
|
|
@@ -400,6 +408,13 @@ export const bindWebsocketProvider = (Y) => {
|
|
|
400
408
|
provider.awareness.meta.delete(client);
|
|
401
409
|
provider.emit('status', [{ status: 'disconnected' }]);
|
|
402
410
|
}
|
|
411
|
+
// A re-seed (`room.reseed.protocol`): unsaved edits are handed over first,
|
|
412
|
+
// by a dial of this document's epoch, older now, which the room asks for
|
|
413
|
+
// them. Then, or with none, it is a refusal: the new epoch is a new document's.
|
|
414
|
+
if (event?.code === CLOSE.reseeded && provider.unsaved > 0 && !provider._rebasing) {
|
|
415
|
+
provider._rebasing = true;
|
|
416
|
+
return setupWS(provider);
|
|
417
|
+
}
|
|
403
418
|
if (event && isRefusal(event.code)) {
|
|
404
419
|
// Terminal: the next dial would be refused the same way.
|
|
405
420
|
provider.shouldConnect = false;
|
|
@@ -511,6 +526,8 @@ export const bindWebsocketProvider = (Y) => {
|
|
|
511
526
|
_chunks = createChunkReader();
|
|
512
527
|
/** This actor's writes pending the room's acknowledgement. */
|
|
513
528
|
_writes;
|
|
529
|
+
/** @internal The room re-seeded: this document's unsaved edits are being handed over (`room.reseed.protocol`). */
|
|
530
|
+
_rebasing = false;
|
|
514
531
|
messageHandlers = room.messageHandlers;
|
|
515
532
|
// The room lifecycle — installed by `initLifecycle`.
|
|
516
533
|
hasSynced;
|
|
@@ -596,12 +613,22 @@ export const bindWebsocketProvider = (Y) => {
|
|
|
596
613
|
* decodes it). `''`, `.`, `..`, an id over 256 characters and one with a
|
|
597
614
|
* lone surrogate are refused at construction ({@link assertRoomId}). `params` are read at every dial (a refreshed token
|
|
598
615
|
* reaches the next connection), with `replica` = this document's
|
|
599
|
-
* client id unless `params` names one
|
|
600
|
-
* writes to it.
|
|
616
|
+
* client id unless `params` names one (the room binds the socket's
|
|
617
|
+
* writes to it), and `epoch`, the document's, which no param replaces.
|
|
601
618
|
*/
|
|
602
619
|
get url() {
|
|
603
|
-
|
|
604
|
-
|
|
620
|
+
// The document's epoch (`new`: it holds nothing yet), which the room checks (`room.reseed.protocol`).
|
|
621
|
+
const epoch = this.doc.store.clients.size
|
|
622
|
+
? String(asEngineDoc(this.doc).get(EPOCH_ROOT).getAttr('n') ?? 0)
|
|
623
|
+
: 'new';
|
|
624
|
+
const query = {
|
|
625
|
+
replica: String(this.doc.clientID),
|
|
626
|
+
epoch,
|
|
627
|
+
...this.params
|
|
628
|
+
};
|
|
629
|
+
// In its place, but no param of the app's names it instead.
|
|
630
|
+
query.epoch = epoch;
|
|
631
|
+
return `${this.serverUrl}/${encodeURIComponent(this.roomname)}?${url.encodeQueryParams(query)}`;
|
|
605
632
|
}
|
|
606
633
|
/** Updates of this document's actor the room has not acknowledged as persisted yet. */
|
|
607
634
|
get unsaved() {
|