edytor 5.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 +73 -76
- 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/history.js +2 -3
- package/dist/crdt/doc/ops.d.ts +81 -0
- package/dist/crdt/doc/ops.js +1898 -0
- package/dist/crdt/doc/suggest.d.ts +6 -2
- package/dist/crdt/doc/suggest.js +6 -5
- package/dist/crdt/edytor-doc.d.ts +157 -157
- package/dist/crdt/edytor-doc.js +15 -43
- package/dist/crdt/engine-room.d.ts +4 -0
- package/dist/crdt/engine-room.js +6 -0
- package/dist/crdt/flow.d.ts +4 -46
- package/dist/crdt/flow.js +2 -262
- package/dist/crdt/index.d.ts +64 -75
- package/dist/crdt/operations.d.ts +1 -1
- package/dist/crdt/placement/resolve.d.ts +1 -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/protocols/awareness.js +46 -81
- package/dist/crdt/providers/index.d.ts +11 -22
- package/dist/crdt/providers/index.js +22 -3
- package/dist/crdt/providers/indexeddb.d.ts +6 -10
- package/dist/crdt/providers/indexeddb.js +10 -16
- package/dist/crdt/providers/room.d.ts +26 -0
- package/dist/crdt/providers/room.js +41 -12
- package/dist/crdt/providers/websocket.d.ts +10 -14
- package/dist/crdt/providers/websocket.js +43 -23
- package/dist/crdt/rangeDelete.d.ts +2 -47
- package/dist/crdt/rangeDelete.js +1 -169
- 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 +119 -0
- package/dist/crdt/text/index/build.js +2801 -0
- package/dist/crdt/text/index/shared.d.ts +2 -6
- package/dist/crdt/text/index/shared.js +2 -5
- package/dist/crdt/text/model.js +2 -3
- package/dist/crdt/text/runs.d.ts +9 -12
- package/dist/crdt/text/runs.js +2 -165
- 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/events/domTextOffset.d.ts +2 -0
- package/dist/events/domTextOffset.js +8 -0
- package/dist/export/forms.d.ts +50 -50
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/plugins/richtext/RichTextPlugin.svelte +63 -61
- package/dist/plugins/richtext/richTextOperations.js +26 -32
- package/dist/selection/selection.svelte.d.ts +1 -1
- package/dist/selection/selection.svelte.js +87 -106
- package/dist/selection/selection.utils.js +5 -8
- package/dist/session/commands.js +49 -51
- package/dist/session/handles.js +3 -7
- 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 +225 -336
- package/dist/surface/projector.svelte.js +2 -2
- package/dist/utils.d.ts +5 -0
- package/dist/utils.js +7 -0
- package/package.json +1 -1
- package/dist/crdt/doc/capability.d.ts +0 -56
- package/dist/crdt/doc/capability.js +0 -207
- package/dist/crdt/doc/content.d.ts +0 -16
- package/dist/crdt/doc/content.js +0 -77
- package/dist/crdt/doc/delete.d.ts +0 -15
- package/dist/crdt/doc/delete.js +0 -84
- package/dist/crdt/doc/layout.d.ts +0 -9
- package/dist/crdt/doc/layout.js +0 -178
- package/dist/crdt/doc/meta.d.ts +0 -19
- package/dist/crdt/doc/meta.js +0 -198
- package/dist/crdt/doc/moves.d.ts +0 -21
- package/dist/crdt/doc/moves.js +0 -274
- package/dist/crdt/doc/split-merge.d.ts +0 -10
- package/dist/crdt/doc/split-merge.js +0 -179
- package/dist/crdt/doc/steps.d.ts +0 -65
- package/dist/crdt/doc/steps.js +0 -311
- package/dist/crdt/text/index/cache.d.ts +0 -16
- package/dist/crdt/text/index/cache.js +0 -175
- package/dist/crdt/text/index/claims.d.ts +0 -18
- package/dist/crdt/text/index/claims.js +0 -203
- package/dist/crdt/text/index/fold.d.ts +0 -21
- package/dist/crdt/text/index/fold.js +0 -656
- package/dist/crdt/text/index/layout.d.ts +0 -22
- package/dist/crdt/text/index/layout.js +0 -258
- package/dist/crdt/text/index/placement.d.ts +0 -47
- package/dist/crdt/text/index/placement.js +0 -738
- package/dist/crdt/text/index/records.d.ts +0 -13
- package/dist/crdt/text/index/records.js +0 -139
- package/dist/crdt/text/index/report.d.ts +0 -18
- package/dist/crdt/text/index/report.js +0 -337
- package/dist/crdt/text/index/state.d.ts +0 -197
- package/dist/crdt/text/index/state.js +0 -116
- package/dist/crdt/text/index/streams.d.ts +0 -25
- package/dist/crdt/text/index/streams.js +0 -248
|
@@ -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;
|
|
@@ -293,6 +299,10 @@ export const markSynced = (host) => {
|
|
|
293
299
|
host._settleSynced[0](host);
|
|
294
300
|
return true;
|
|
295
301
|
};
|
|
302
|
+
/** Report `error` on `provider`'s error channel (`'message-error'`): a frame, a payload or a storage failure. */
|
|
303
|
+
export const fault = (provider, error) => provider.emit?.('message-error', [error, provider]);
|
|
304
|
+
/** A message type no handler claims inside a valid envelope: protocol skew, reported, never dropped silently. */
|
|
305
|
+
export const unknownType = (provider, what, type) => fault(provider, new Error(`Unknown ${what} message type ${type}`));
|
|
296
306
|
/**
|
|
297
307
|
* Terminal `'failed'` emission (the sync-failure contract): fires at
|
|
298
308
|
* most once per provider, only while it has never synced — destroy before
|
|
@@ -330,6 +340,15 @@ export const CLOSE = {
|
|
|
330
340
|
denied: 4403,
|
|
331
341
|
/** The dialed replica is bound to another user. */
|
|
332
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,
|
|
333
352
|
/**
|
|
334
353
|
* A room quota refused the socket's write (`quota: document`,
|
|
335
354
|
* `quota: rate`, `quota: frame`): the frame was not applied, and a
|
|
@@ -425,13 +444,13 @@ export const bindRoomProtocol = (syncProtocol, behavior) => {
|
|
|
425
444
|
// (`discarded`) and reported the same way.
|
|
426
445
|
const update = decoding.readVarUint8Array(decoder);
|
|
427
446
|
const origin = emitSynced ? provider : (behavior.tabOrigin?.(provider) ?? provider);
|
|
428
|
-
const onError = (error) => provider
|
|
447
|
+
const onError = (error) => fault(provider, error);
|
|
429
448
|
const { applied, problem, discarded } = syncProtocol.applyRemote(provider.doc, update, origin, onError);
|
|
430
449
|
const refused = problem ?? discarded;
|
|
431
450
|
if (refused) {
|
|
432
451
|
const docName = behavior.docName(provider);
|
|
433
452
|
provider.emit?.('schema-mismatch', [{ docName, problem: refused }, provider]);
|
|
434
|
-
provider
|
|
453
|
+
fault(provider, new SchemaMismatchError(docName, refused));
|
|
435
454
|
}
|
|
436
455
|
// Only an ACCEPTED Step2 is a held member state — a refused
|
|
437
456
|
// one never claims the handshake.
|
|
@@ -441,10 +460,7 @@ export const bindRoomProtocol = (syncProtocol, behavior) => {
|
|
|
441
460
|
return;
|
|
442
461
|
}
|
|
443
462
|
// Unknown sync subtype inside a valid envelope — observable.
|
|
444
|
-
provider
|
|
445
|
-
new Error(`Unknown sync message type ${syncMessageType}`),
|
|
446
|
-
provider
|
|
447
|
-
]);
|
|
463
|
+
unknownType(provider, 'sync', syncMessageType);
|
|
448
464
|
},
|
|
449
465
|
[messageQueryAwareness]: (encoder, _decoder, provider) => {
|
|
450
466
|
encoding.writeVarUint(encoder, messageAwareness);
|
|
@@ -477,10 +493,7 @@ export const bindRoomProtocol = (syncProtocol, behavior) => {
|
|
|
477
493
|
if (!messageHandler) {
|
|
478
494
|
// Fail closed + observable: a VALID envelope carrying a message
|
|
479
495
|
// type no handler claims is protocol skew — never silent.
|
|
480
|
-
provider
|
|
481
|
-
new Error(`Unknown v14 message type ${messageType}`),
|
|
482
|
-
provider
|
|
483
|
-
]);
|
|
496
|
+
unknownType(provider, 'v14', messageType);
|
|
484
497
|
return encoder;
|
|
485
498
|
}
|
|
486
499
|
const followUp = messageHandler(encoder, decoder, provider, emitSynced);
|
|
@@ -503,7 +516,7 @@ export const bindRoomProtocol = (syncProtocol, behavior) => {
|
|
|
503
516
|
catch (error) {
|
|
504
517
|
// same-tab publishes are delivered synchronously — a malformed
|
|
505
518
|
// message must not propagate into the publisher's call stack.
|
|
506
|
-
provider
|
|
519
|
+
fault(provider, error);
|
|
507
520
|
}
|
|
508
521
|
};
|
|
509
522
|
};
|
|
@@ -554,6 +567,21 @@ export const bindRoomProtocol = (syncProtocol, behavior) => {
|
|
|
554
567
|
};
|
|
555
568
|
/** The departure announcement — our presence, removed — on every transport the provider speaks. */
|
|
556
569
|
const depart = (provider) => behavior.broadcast(provider, awarenessFrame(provider.awareness, [provider.doc.clientID], new Map()));
|
|
570
|
+
/**
|
|
571
|
+
* Wire `provider` to its room: its BroadcastChannel subscriber, `onUpdate`
|
|
572
|
+
* on its document's updates and its presence changes to the room
|
|
573
|
+
* (`awarenessUpdateHandler`). The teardown removes both listeners.
|
|
574
|
+
*/
|
|
575
|
+
const listen = (provider, onUpdate) => {
|
|
576
|
+
provider._bcSubscriber = bcSubscriber(provider);
|
|
577
|
+
const onPresence = awarenessUpdateHandler(provider);
|
|
578
|
+
provider.doc.on('update', onUpdate);
|
|
579
|
+
provider.awareness.on('update', onPresence);
|
|
580
|
+
return () => {
|
|
581
|
+
provider.awareness.off('update', onPresence);
|
|
582
|
+
provider.doc.off('update', onUpdate);
|
|
583
|
+
};
|
|
584
|
+
};
|
|
557
585
|
/** Leave the room: announce the presence removal, then unsubscribe the BC channel. */
|
|
558
586
|
const disconnectBc = (provider) => {
|
|
559
587
|
depart(provider);
|
|
@@ -572,6 +600,7 @@ export const bindRoomProtocol = (syncProtocol, behavior) => {
|
|
|
572
600
|
hello,
|
|
573
601
|
depart,
|
|
574
602
|
connectBc,
|
|
575
|
-
disconnectBc
|
|
603
|
+
disconnectBc,
|
|
604
|
+
listen
|
|
576
605
|
};
|
|
577
606
|
};
|
|
@@ -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>;
|
|
@@ -185,22 +187,16 @@ export declare const bindWebsocketProvider: (Y: EngineApi) => {
|
|
|
185
187
|
/** Closes the current dial if it has not opened in time. */
|
|
186
188
|
_connectTimer: ReturnType<typeof setTimeout> | undefined;
|
|
187
189
|
_checkInterval: ReturnType<typeof setInterval>;
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
}) => void;
|
|
191
|
-
_awarenessUpdateHandler: (updates: {
|
|
192
|
-
added: number[];
|
|
193
|
-
updated: number[];
|
|
194
|
-
removed: number[];
|
|
195
|
-
}, origin: unknown) => void;
|
|
190
|
+
/** @internal Removes the document and presence listeners (`room.listen`). */
|
|
191
|
+
_unlisten: () => void;
|
|
196
192
|
/**
|
|
197
193
|
* The dial URL: the room name as one encoded path segment (any id —
|
|
198
194
|
* `/`, `%`, `#`, `?` included — reaches its own room; the server
|
|
199
195
|
* decodes it). `''`, `.`, `..`, an id over 256 characters and one with a
|
|
200
196
|
* lone surrogate are refused at construction ({@link assertRoomId}). `params` are read at every dial (a refreshed token
|
|
201
197
|
* reaches the next connection), with `replica` = this document's
|
|
202
|
-
* client id unless `params` names one
|
|
203
|
-
* 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.
|
|
204
200
|
*/
|
|
205
201
|
get url(): string;
|
|
206
202
|
/** Updates of this document's actor the room has not acknowledged as persisted yet. */
|
|
@@ -223,11 +219,11 @@ export declare const bindWebsocketProvider: (Y: EngineApi) => {
|
|
|
223
219
|
destroy(): void;
|
|
224
220
|
disconnect(): void;
|
|
225
221
|
connect(): void;
|
|
226
|
-
emit<N extends "refused" | "status" | "failed" | "
|
|
222
|
+
emit<N extends "refused" | "status" | "failed" | "message-error" | "schema-mismatch" | "protocol-mismatch" | "synced" | "access" | "connection-close" | "connection-error" | "saved" | "permission-denied" | "expired" | "unreachable">(name: N, args: Parameters<WebsocketProviderEvents[N]>): void;
|
|
227
223
|
_observers: Map<string, Set<any>>;
|
|
228
|
-
on<NAME extends "refused" | "status" | "failed" | "
|
|
229
|
-
once<NAME extends "refused" | "status" | "failed" | "
|
|
230
|
-
off<NAME extends "refused" | "status" | "failed" | "
|
|
224
|
+
on<NAME extends "refused" | "status" | "failed" | "message-error" | "schema-mismatch" | "protocol-mismatch" | "synced" | "access" | "connection-close" | "connection-error" | "saved" | "permission-denied" | "expired" | "unreachable">(name: NAME, f: WebsocketProviderEvents[NAME]): WebsocketProviderEvents[NAME];
|
|
225
|
+
once<NAME extends "refused" | "status" | "failed" | "message-error" | "schema-mismatch" | "protocol-mismatch" | "synced" | "access" | "connection-close" | "connection-error" | "saved" | "permission-denied" | "expired" | "unreachable">(name: NAME, f: WebsocketProviderEvents[NAME]): void;
|
|
226
|
+
off<NAME extends "refused" | "status" | "failed" | "message-error" | "schema-mismatch" | "protocol-mismatch" | "synced" | "access" | "connection-close" | "connection-error" | "saved" | "permission-denied" | "expired" | "unreachable">(name: NAME, f: WebsocketProviderEvents[NAME]): void;
|
|
231
227
|
};
|
|
232
228
|
};
|
|
233
229
|
watchComments: (server: string | null, roomName: string, listener: (message: CommentMessage) => void) => (() => void);
|
|
@@ -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, initLifecycle, isRefusal, 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,16 +130,20 @@ 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
|
-
provider
|
|
139
|
-
new Error(`Unknown auth message type ${authType}`),
|
|
140
|
-
provider
|
|
141
|
-
]);
|
|
146
|
+
unknownType(provider, 'auth', authType);
|
|
142
147
|
}
|
|
143
148
|
}
|
|
144
149
|
},
|
|
@@ -403,6 +408,13 @@ export const bindWebsocketProvider = (Y) => {
|
|
|
403
408
|
provider.awareness.meta.delete(client);
|
|
404
409
|
provider.emit('status', [{ status: 'disconnected' }]);
|
|
405
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
|
+
}
|
|
406
418
|
if (event && isRefusal(event.code)) {
|
|
407
419
|
// Terminal: the next dial would be refused the same way.
|
|
408
420
|
provider.shouldConnect = false;
|
|
@@ -459,7 +471,7 @@ export const bindWebsocketProvider = (Y) => {
|
|
|
459
471
|
room.readMessage(provider, new Uint8Array(event.data), true, (buf) => sendOn(provider, websocket, buf));
|
|
460
472
|
}
|
|
461
473
|
catch (error) {
|
|
462
|
-
provider
|
|
474
|
+
fault(provider, error);
|
|
463
475
|
}
|
|
464
476
|
};
|
|
465
477
|
websocket.onerror = (event) => {
|
|
@@ -514,6 +526,8 @@ export const bindWebsocketProvider = (Y) => {
|
|
|
514
526
|
_chunks = createChunkReader();
|
|
515
527
|
/** This actor's writes pending the room's acknowledgement. */
|
|
516
528
|
_writes;
|
|
529
|
+
/** @internal The room re-seeded: this document's unsaved edits are being handed over (`room.reseed.protocol`). */
|
|
530
|
+
_rebasing = false;
|
|
517
531
|
messageHandlers = room.messageHandlers;
|
|
518
532
|
// The room lifecycle — installed by `initLifecycle`.
|
|
519
533
|
hasSynced;
|
|
@@ -526,8 +540,8 @@ export const bindWebsocketProvider = (Y) => {
|
|
|
526
540
|
/** Closes the current dial if it has not opened in time. */
|
|
527
541
|
_connectTimer;
|
|
528
542
|
_checkInterval;
|
|
529
|
-
|
|
530
|
-
|
|
543
|
+
/** @internal Removes the document and presence listeners (`room.listen`). */
|
|
544
|
+
_unlisten;
|
|
531
545
|
constructor(serverUrl, roomname, doc, { connect = true, awareness = new Awareness(doc), params = {}, WebSocketPolyfill = WebSocket, resyncInterval = 0, maxBackoffTime = 2500, connectTimeout = defaultConnectTimeout, disableBc = false, maxFrameBytes = MAX_FRAME_BYTES } = {}) {
|
|
532
546
|
assertRoomId(roomname);
|
|
533
547
|
super();
|
|
@@ -548,14 +562,13 @@ export const bindWebsocketProvider = (Y) => {
|
|
|
548
562
|
if (resyncInterval > 0) {
|
|
549
563
|
this._resync = setInterval(() => send(this, room.step1(this)), resyncInterval);
|
|
550
564
|
}
|
|
551
|
-
this._bcSubscriber = room.bcSubscriber(this);
|
|
552
565
|
providers.add(this);
|
|
553
566
|
// Local doc updates go to the room; another tab's go to the server
|
|
554
567
|
// only (`broadcastUpdate` quarantines a read-only document). A
|
|
555
568
|
// socket the room made read-only gets this document's own edits
|
|
556
569
|
// alone (each is denied): what a tab or a store delivered, and the
|
|
557
570
|
// actor records, would draw denials nobody made.
|
|
558
|
-
|
|
571
|
+
const onUpdate = (update, origin, _doc, transaction) => {
|
|
559
572
|
if (origin === this)
|
|
560
573
|
return;
|
|
561
574
|
const heard = origin === this._fromTab;
|
|
@@ -573,9 +586,7 @@ export const bindWebsocketProvider = (Y) => {
|
|
|
573
586
|
};
|
|
574
587
|
this._writes = new OwnWrites(doc, (unsaved) => this.emit('saved', [{ saved: unsaved === 0, unsaved }, this]));
|
|
575
588
|
this._writes.trackDocument();
|
|
576
|
-
this.
|
|
577
|
-
this._awarenessUpdateHandler = room.awarenessUpdateHandler(this);
|
|
578
|
-
awareness.on('update', this._awarenessUpdateHandler);
|
|
589
|
+
this._unlisten = room.listen(this, onUpdate);
|
|
579
590
|
this._checkInterval = setInterval(() => {
|
|
580
591
|
if (!this.wsconnected)
|
|
581
592
|
return;
|
|
@@ -602,12 +613,22 @@ export const bindWebsocketProvider = (Y) => {
|
|
|
602
613
|
* decodes it). `''`, `.`, `..`, an id over 256 characters and one with a
|
|
603
614
|
* lone surrogate are refused at construction ({@link assertRoomId}). `params` are read at every dial (a refreshed token
|
|
604
615
|
* reaches the next connection), with `replica` = this document's
|
|
605
|
-
* client id unless `params` names one
|
|
606
|
-
* 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.
|
|
607
618
|
*/
|
|
608
619
|
get url() {
|
|
609
|
-
|
|
610
|
-
|
|
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)}`;
|
|
611
632
|
}
|
|
612
633
|
/** Updates of this document's actor the room has not acknowledged as persisted yet. */
|
|
613
634
|
get unsaved() {
|
|
@@ -651,8 +672,7 @@ export const bindWebsocketProvider = (Y) => {
|
|
|
651
672
|
clearInterval(this._checkInterval);
|
|
652
673
|
providers.delete(this);
|
|
653
674
|
this.disconnect();
|
|
654
|
-
this.
|
|
655
|
-
this.doc.off('update', this._updateHandler);
|
|
675
|
+
this._unlisten();
|
|
656
676
|
super.destroy();
|
|
657
677
|
}
|
|
658
678
|
disconnect() {
|
|
@@ -1,48 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* `del.range.*` rows of `docs/editor-delete-contract.md`:
|
|
5
|
-
*
|
|
6
|
-
* - the head keeps `[0, start)`, the tail keeps `[end, len)`, and every
|
|
7
|
-
* block strictly between them in document order dies;
|
|
8
|
-
* - the head dies iff its prefix is empty (never for a replacement,
|
|
9
|
-
* `del.range.replace`, nor when nothing else would be left to hold the
|
|
10
|
-
* caret, `del.range.whole-doc`: the head is kept, emptied, with its id,
|
|
11
|
-
* type and data, so peers deleting the same range keep the same block)
|
|
12
|
-
* and the tail then keeps its id; the tail dies iff
|
|
13
|
-
* its suffix is empty; otherwise the tail's suffix merges into the head
|
|
14
|
-
* when `canMerge(tail, head)` allows it (`del.range.island-seal`) — an end
|
|
15
|
-
* at the tail's start included: the seam between them is deleted;
|
|
16
|
-
* - what follows the range end survives: the dying tail's children and the
|
|
17
|
-
* later siblings inside every container the range dies through take the
|
|
18
|
-
* topmost such container's slot, unless that would cross an island seal —
|
|
19
|
-
* then those containers stay (`del.range.outside-survives`);
|
|
20
|
-
* - a container (a list: the document's `isContainer`) never dies because
|
|
21
|
-
* the range starts before it: it keeps what follows the range end, like
|
|
22
|
-
* the keys (`del.merge.container`); it dies only when it loses every
|
|
23
|
-
* child and holds no text of its own — the document's `emptiable`, the
|
|
24
|
-
* keys' rule (`del.range.empty-container`). An island that renders
|
|
25
|
-
* no content dies when it loses every child too, except a `lines` island
|
|
26
|
-
* (a code block) the range lies in: its head is then kept, emptied
|
|
27
|
-
* (`del.range.island-kept`);
|
|
28
|
-
* - what a range rescues takes the kind it shows in its new slot (the
|
|
29
|
-
* document's `settle`: a paragraph rescued into a list is its item);
|
|
30
|
-
* - blocks the view hides (a closed toggle's body) are not in the range:
|
|
31
|
-
* they go only with a block that goes, so a surviving head keeps its
|
|
32
|
-
* hidden children, unless it is kept only to hold the caret; a dying
|
|
33
|
-
* closed tail's body takes its place, shown (`del.range.hidden-body`);
|
|
34
|
-
* - a layout the range leaves with one column dissolves, in the same plan
|
|
35
|
-
* (`layout.dissolving`);
|
|
36
|
-
* - a range with an end in a table's cell keeps every cell and row: each
|
|
37
|
-
* cell it crosses loses the text it covers, nothing merges across a
|
|
38
|
-
* cell's edge, the blocks outside the table between the ends go, a
|
|
39
|
-
* table wholly between them goes whole, and the caret lands at the start
|
|
40
|
-
* (`table.range`);
|
|
41
|
-
* - the plan's `at` is where the caret lands (`del.range.caret`).
|
|
2
|
+
* The types range deletion (`deleteRange`, `replaceRange`: the `del.range.*`
|
|
3
|
+
* rows, prepared in `doc/ops.ts`) reads.
|
|
42
4
|
*/
|
|
43
5
|
import type { BlockId } from './placement/model.js';
|
|
44
|
-
import type { Prepared } from './edytor-doc.js';
|
|
45
|
-
import type { OpsContext } from './doc/steps.js';
|
|
46
6
|
/** A position in one document version: a block and a display offset. */
|
|
47
7
|
export type DocPosition = {
|
|
48
8
|
block: BlockId;
|
|
@@ -56,8 +16,3 @@ export type DocPosition = {
|
|
|
56
16
|
export type RangeView = {
|
|
57
17
|
hidden?: (id: BlockId, removed?: ReadonlySet<BlockId>) => boolean;
|
|
58
18
|
};
|
|
59
|
-
/** `deleteRange` and `replaceRange` (the head kept), prepared. */
|
|
60
|
-
export declare const rangeDeleteOps: (c: OpsContext) => {
|
|
61
|
-
deleteRange: (from: DocPosition, to: DocPosition, view?: RangeView) => Prepared;
|
|
62
|
-
replaceRange: (from: DocPosition, to: DocPosition, view?: RangeView) => Prepared;
|
|
63
|
-
};
|
package/dist/crdt/rangeDelete.js
CHANGED
|
@@ -1,169 +1 @@
|
|
|
1
|
-
|
|
2
|
-
/** `deleteRange` and `replaceRange` (the head kept), prepared. */
|
|
3
|
-
export const rangeDeleteOps = (c) => {
|
|
4
|
-
// `headSpent`: a whole-doc retry keeps the head, but what it hid goes as if it went.
|
|
5
|
-
const prepare = (keepHead, headSpent = false) => (from, to, view = {}) => {
|
|
6
|
-
// A position names shown text: a live block that renders its own content.
|
|
7
|
-
const holds = (id) => c.contentTarget(id) && c.rendersContent(id);
|
|
8
|
-
const pos = (p) => {
|
|
9
|
-
const block = ref(p.block);
|
|
10
|
-
const len = holds(block) ? c.displayLength(block) : -1;
|
|
11
|
-
return { block, offset: Math.max(0, Math.min(p.offset, len)) };
|
|
12
|
-
};
|
|
13
|
-
let [s, e] = [pos(from), pos(to)];
|
|
14
|
-
if (!holds(s.block) || !holds(e.block))
|
|
15
|
-
return c.REFUSED;
|
|
16
|
-
const { ids, at } = c.view().order;
|
|
17
|
-
if (at.get(s.block) > at.get(e.block) || (s.block === e.block && s.offset > e.offset))
|
|
18
|
-
[s, e] = [e, s];
|
|
19
|
-
const S = s.block;
|
|
20
|
-
const E = e.block;
|
|
21
|
-
if (S === E) {
|
|
22
|
-
const length = e.offset - s.offset;
|
|
23
|
-
const writes = length > 0 ? [{ op: 'deleteText', id: S, offset: s.offset, length }] : [];
|
|
24
|
-
return { ...c.plan([S], writes), at: s };
|
|
25
|
-
}
|
|
26
|
-
const parent = (id) => c.ancestorsOf(id)[0] ?? null;
|
|
27
|
-
const chain = c.ancestorsOf(E);
|
|
28
|
-
if (c.isTableCell(S) || c.isTableCell(E))
|
|
29
|
-
return inTables(s, e, view);
|
|
30
|
-
// What the view hides once `removed` go (nothing: as it is now).
|
|
31
|
-
const hiddenOnce = (removed) => {
|
|
32
|
-
const set = new Set(removed);
|
|
33
|
-
return (id) => !!view.hidden?.(id, set);
|
|
34
|
-
};
|
|
35
|
-
const headDies = !keepHead && s.offset === 0;
|
|
36
|
-
// What the view hides is not in the range: it goes only with a block that goes,
|
|
37
|
-
// a dying head's body included.
|
|
38
|
-
const hidden = hiddenOnce(headDies || headSpent ? [S] : []);
|
|
39
|
-
const between = ids.slice(at.get(S) + 1, at.get(E)).filter((id) => !hidden(id));
|
|
40
|
-
const lenE = c.displayLength(E);
|
|
41
|
-
const tailDies = e.offset === lenE;
|
|
42
|
-
const merges = !headDies && !tailDies && c.canMerge(E, S);
|
|
43
|
-
const tailGone = tailDies || merges;
|
|
44
|
-
// E's ancestors the range starts before die unless the rescue would cross an island.
|
|
45
|
-
// A container (a list) is not one: it dies only when the range empties it, and
|
|
46
|
-
// keeps its later items like the keys do (`del.merge.container`).
|
|
47
|
-
const partial = chain.filter((a) => (a === (headDies ? S : null) || between.includes(a)) && !c.isContainer(a));
|
|
48
|
-
const top = partial.at(-1);
|
|
49
|
-
const sealed = top !== undefined && chain.slice(0, chain.indexOf(top) + 1).some(c.isIsland);
|
|
50
|
-
const spared = new Set(sealed ? chain : []);
|
|
51
|
-
const doomed = new Set(between.filter((id) => !spared.has(id) && !chain.includes(id)));
|
|
52
|
-
if (!sealed)
|
|
53
|
-
partial.forEach((a) => doomed.add(a));
|
|
54
|
-
if (headDies && !spared.has(S))
|
|
55
|
-
doomed.add(S);
|
|
56
|
-
if (tailDies)
|
|
57
|
-
doomed.add(E);
|
|
58
|
-
// Rescue what follows the range end right after the topmost dying container's slot
|
|
59
|
-
// (after, not at it: a concurrent delete of S revives a merged E above them).
|
|
60
|
-
const home = sealed || top === undefined ? (tailGone ? E : undefined) : top;
|
|
61
|
-
const rescued = home === undefined ? [] : tailGone ? c.childrenIds(E) : [E];
|
|
62
|
-
for (let cur = E; home !== undefined && cur !== home; cur = parent(cur)) {
|
|
63
|
-
const sibs = c.childrenIds(parent(cur));
|
|
64
|
-
rescued.push(...sibs.slice(sibs.indexOf(cur) + 1));
|
|
65
|
-
}
|
|
66
|
-
const dest = home === undefined || rescued.length === 0 ? null : c.positionOf(home);
|
|
67
|
-
// A container (or an island that renders nothing) losing every child dies too.
|
|
68
|
-
const gone = (id) => doomed.has(id) || (merges && id === E);
|
|
69
|
-
const dies = (id) => c.emptiable(id) || (c.isIsland(id) && !c.rendersContent(id));
|
|
70
|
-
const emptied = (id) => {
|
|
71
|
-
if (id === null || gone(id) || !dies(id) || id === dest?.parent)
|
|
72
|
-
return;
|
|
73
|
-
if (!c.childrenIds(id).every(gone))
|
|
74
|
-
return;
|
|
75
|
-
doomed.add(id);
|
|
76
|
-
emptied(parent(id));
|
|
77
|
-
};
|
|
78
|
-
[...doomed, ...(merges ? [E] : [])].forEach((id) => emptied(parent(id)));
|
|
79
|
-
// A range inside one `lines` island never removes it: the head stays, emptied
|
|
80
|
-
// (a code block's Mod+A, `del.range.island-kept`).
|
|
81
|
-
const island = c.ancestorsOf(S).find(c.isIsland);
|
|
82
|
-
const linesIsland = island !== undefined && c.isLines(island);
|
|
83
|
-
if (!keepHead && linesIsland && doomed.has(island) && chain.includes(island))
|
|
84
|
-
return prepare(true, headDies)(from, to, view);
|
|
85
|
-
const lenS = c.displayLength(S);
|
|
86
|
-
const writes = [];
|
|
87
|
-
if (!doomed.has(S) && lenS > s.offset)
|
|
88
|
-
writes.push({ op: 'deleteText', id: S, offset: s.offset, length: lenS - s.offset });
|
|
89
|
-
if (!gone(E) && e.offset > 0)
|
|
90
|
-
writes.push({ op: 'deleteText', id: E, offset: 0, length: e.offset });
|
|
91
|
-
if (dest !== null) {
|
|
92
|
-
writes.push(...c.move(rescued, dest.parent, dest.index + 1));
|
|
93
|
-
// Each shows the kind of its new slot (an island's line, a list's item leave theirs).
|
|
94
|
-
for (const id of rescued)
|
|
95
|
-
writes.push(...c.settle(parent(id), [id], dest.parent));
|
|
96
|
-
}
|
|
97
|
-
if (merges) {
|
|
98
|
-
// The whole tail merges, then its cut prefix goes: E's own text is never written.
|
|
99
|
-
writes.push({ op: 'mergeBlocks', from: E, into: S, at: s.offset, length: lenE });
|
|
100
|
-
if (e.offset > 0)
|
|
101
|
-
writes.push({ op: 'deleteText', id: S, offset: s.offset, length: e.offset });
|
|
102
|
-
}
|
|
103
|
-
const kept = [...rescued, ...(merges ? [E] : [])];
|
|
104
|
-
for (const id of doomed)
|
|
105
|
-
if (!doomed.has(parent(id)))
|
|
106
|
-
writes.push(c.remove(id, kept));
|
|
107
|
-
// A layout the range leaves with one column dissolves (`layout.dissolving`).
|
|
108
|
-
writes.push(...c.dissolving(new Set([...doomed, ...(merges ? [E] : [])]), rescued, writes));
|
|
109
|
-
const caret = (() => {
|
|
110
|
-
if (!doomed.has(S))
|
|
111
|
-
return s;
|
|
112
|
-
if (!gone(E))
|
|
113
|
-
return { block: E, offset: 0 };
|
|
114
|
-
const survives = (id) => id === null || rescued.includes(id) || (!gone(id) && survives(parent(id)));
|
|
115
|
-
// Shown once the plan runs: a dying closed tail's rescued body is.
|
|
116
|
-
const hiddenAfter = hiddenOnce([...doomed, ...(merges ? [E] : [])]);
|
|
117
|
-
const shown = (id) => survives(id) && holds(id) && !hiddenAfter(id);
|
|
118
|
-
// Before the tail: blocks a sealed rescue spared between S and E count too.
|
|
119
|
-
const prev = ids.slice(0, at.get(E)).findLast(shown);
|
|
120
|
-
if (prev !== undefined)
|
|
121
|
-
return { block: prev, offset: c.displayLength(prev) };
|
|
122
|
-
const next = ids.slice(at.get(E) + 1).find(shown);
|
|
123
|
-
return next === undefined ? null : { block: next, offset: 0 };
|
|
124
|
-
})();
|
|
125
|
-
// Nothing else left to hold the caret: the head stays, emptied (whole-doc).
|
|
126
|
-
if (caret === null)
|
|
127
|
-
return prepare(true, headDies)(from, to, view);
|
|
128
|
-
return { ...c.plan([caret.block], writes), at: caret };
|
|
129
|
-
};
|
|
130
|
-
/**
|
|
131
|
-
* `table.range`: a range with an end in a cell. Every cell it crosses
|
|
132
|
-
* loses the text it covers and stays, with its row; the ends keep what
|
|
133
|
-
* lies outside the range, merging nothing; any other block between the
|
|
134
|
-
* ends goes (a table wholly between them whole), but the ancestors of
|
|
135
|
-
* the end, which stay and lose their text (the range covers it: they
|
|
136
|
-
* come after the start in reading order); the caret lands at the start.
|
|
137
|
-
*/
|
|
138
|
-
const inTables = (s, e, view) => {
|
|
139
|
-
const { ids, at } = c.view().order;
|
|
140
|
-
const [S, E] = [s.block, e.block];
|
|
141
|
-
const kept = new Set(c.ancestorsOf(E));
|
|
142
|
-
const between = ids.slice(at.get(S) + 1, at.get(E)).filter((id) => !view.hidden?.(id));
|
|
143
|
-
const writes = [];
|
|
144
|
-
const lenS = c.displayLength(S);
|
|
145
|
-
if (lenS > s.offset)
|
|
146
|
-
writes.push({ op: 'deleteText', id: S, offset: s.offset, length: lenS - s.offset });
|
|
147
|
-
const [tableS, tableE] = [c.tableOf(S), c.tableOf(E)];
|
|
148
|
-
const doomed = new Set();
|
|
149
|
-
for (const id of between) {
|
|
150
|
-
const table = c.tableOf(id);
|
|
151
|
-
if (kept.has(id) || (table !== null && (table === tableS || table === tableE))) {
|
|
152
|
-
const len = c.isTableCell(id) || kept.has(id) ? c.displayLength(id) : 0;
|
|
153
|
-
if (len > 0)
|
|
154
|
-
writes.push({ op: 'deleteText', id, offset: 0, length: len });
|
|
155
|
-
}
|
|
156
|
-
else
|
|
157
|
-
doomed.add(id);
|
|
158
|
-
}
|
|
159
|
-
if (e.offset > 0)
|
|
160
|
-
writes.push({ op: 'deleteText', id: E, offset: 0, length: e.offset });
|
|
161
|
-
const parentOf = (id) => c.ancestorsOf(id)[0] ?? null;
|
|
162
|
-
const roots = [...doomed].filter((id) => !doomed.has(parentOf(id)));
|
|
163
|
-
for (const id of roots)
|
|
164
|
-
writes.push(c.remove(id, []));
|
|
165
|
-
writes.push(...c.dissolving(new Set(roots), [], writes));
|
|
166
|
-
return { ...c.plan([S], writes), at: s };
|
|
167
|
-
};
|
|
168
|
-
return { deleteRange: prepare(false), replaceRange: prepare(true) };
|
|
169
|
-
};
|
|
1
|
+
export {};
|