edytor 4.0.0 → 6.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 +10 -3
- 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 +106 -109
- package/dist/cloudflare/room/context.js +10 -3
- 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.js +2 -1
- 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/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 +3 -0
- package/dist/crdt/attribution/attribution.js +3 -3
- package/dist/crdt/doc/funnel.d.ts +1 -1
- package/dist/crdt/doc/funnel.js +5 -2
- package/dist/crdt/doc/gate.d.ts +1 -1
- package/dist/crdt/doc/history.d.ts +6 -0
- package/dist/crdt/doc/history.js +79 -59
- package/dist/crdt/doc/ops.d.ts +81 -0
- package/dist/crdt/doc/ops.js +1898 -0
- package/dist/crdt/doc/reads.d.ts +2 -2
- package/dist/crdt/doc/reads.js +18 -28
- package/dist/crdt/doc/suggest.d.ts +167 -0
- package/dist/crdt/doc/suggest.js +579 -0
- package/dist/crdt/document.d.ts +10 -1
- package/dist/crdt/document.js +19 -11
- package/dist/crdt/edytor-doc.d.ts +294 -272
- package/dist/crdt/edytor-doc.js +123 -108
- package/dist/crdt/engine-api.d.ts +8 -2
- package/dist/crdt/engine.d.ts +2 -1
- package/dist/crdt/engine.js +3 -1
- package/dist/crdt/flow.d.ts +5 -47
- package/dist/crdt/flow.js +3 -269
- package/dist/crdt/index.d.ts +102 -108
- package/dist/crdt/index.js +3 -0
- package/dist/crdt/operations.d.ts +13 -4
- package/dist/crdt/parts.d.ts +43 -0
- package/dist/crdt/parts.js +44 -0
- package/dist/crdt/placement/display.d.ts +1 -1
- package/dist/crdt/placement/display.js +3 -3
- package/dist/crdt/placement/keys.d.ts +5 -0
- package/dist/crdt/placement/keys.js +5 -0
- package/dist/crdt/placement/resolve.d.ts +1 -1
- package/dist/crdt/placement/resolve.js +2 -1
- 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/awareness.js +46 -81
- package/dist/crdt/protocols/comments.d.ts +16 -1
- package/dist/crdt/protocols/comments.js +9 -1
- package/dist/crdt/providers/index.d.ts +12 -22
- package/dist/crdt/providers/index.js +3 -1
- package/dist/crdt/providers/indexeddb.d.ts +6 -10
- package/dist/crdt/providers/indexeddb.js +10 -16
- package/dist/crdt/providers/room.d.ts +11 -0
- package/dist/crdt/providers/room.js +26 -12
- package/dist/crdt/providers/websocket.d.ts +8 -12
- package/dist/crdt/providers/websocket.js +17 -19
- package/dist/crdt/purge.d.ts +7 -1
- package/dist/crdt/purge.js +32 -2
- package/dist/crdt/rangeDelete.d.ts +2 -47
- package/dist/crdt/rangeDelete.js +1 -169
- package/dist/crdt/restore.d.ts +2 -1
- package/dist/crdt/restore.js +9 -0
- package/dist/crdt/schema.d.ts +9 -1
- package/dist/crdt/schema.js +13 -3
- package/dist/crdt/structs.d.ts +2 -0
- package/dist/crdt/structs.js +2 -0
- package/dist/crdt/text/index/build.d.ts +125 -0
- package/dist/crdt/text/index/build.js +2895 -0
- package/dist/crdt/text/index/shared.d.ts +2 -6
- package/dist/crdt/text/index/shared.js +19 -19
- package/dist/crdt/text/model.d.ts +10 -0
- package/dist/crdt/text/model.js +10 -4
- package/dist/crdt/text/runs.d.ts +43 -42
- package/dist/crdt/text/runs.js +2 -165
- package/dist/crdt/vendor/yjs/UPSTREAM.md +83 -0
- package/dist/crdt/vendor/yjs/dts/index.d.ts +1 -1
- package/dist/crdt/vendor/yjs/dts/structs/Item.d.ts +6 -3
- package/dist/crdt/vendor/yjs/dts/ynode.d.ts +13 -2
- package/dist/crdt/vendor/yjs/src/index.js +1 -1
- package/dist/crdt/vendor/yjs/src/structs/Item.js +44 -5
- package/dist/crdt/vendor/yjs/src/ynode.js +29 -7
- package/dist/edytor.svelte.d.ts +1 -1
- package/dist/edytor.svelte.js +100 -75
- package/dist/events/beforeInputCommands.d.ts +2 -2
- package/dist/events/domTextOffset.d.ts +2 -0
- package/dist/events/domTextOffset.js +8 -0
- 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/export/forms.d.ts +75 -75
- 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/richtext/RichTextPlugin.svelte +63 -61
- package/dist/plugins/richtext/richTextOperations.js +26 -32
- 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 +33 -11
- 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 +14 -44
- 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 +143 -111
- package/dist/surface/pin.svelte.d.ts +9 -9
- package/dist/surface/pin.svelte.js +15 -22
- package/dist/surface/projector.svelte.js +10 -16
- package/dist/text/text.svelte.js +14 -19
- package/dist/themes/notion.css +29 -0
- package/dist/utils.d.ts +5 -0
- package/dist/utils.js +7 -0
- package/package.json +3 -1
- package/dist/crdt/doc/capability.d.ts +0 -56
- package/dist/crdt/doc/capability.js +0 -211
- 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 -199
- 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 -225
- 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 -24
- package/dist/crdt/text/index/layout.js +0 -271
- package/dist/crdt/text/index/placement.d.ts +0 -47
- package/dist/crdt/text/index/placement.js +0 -741
- 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
|
@@ -293,6 +293,10 @@ export const markSynced = (host) => {
|
|
|
293
293
|
host._settleSynced[0](host);
|
|
294
294
|
return true;
|
|
295
295
|
};
|
|
296
|
+
/** Report `error` on `provider`'s error channel (`'message-error'`): a frame, a payload or a storage failure. */
|
|
297
|
+
export const fault = (provider, error) => provider.emit?.('message-error', [error, provider]);
|
|
298
|
+
/** A message type no handler claims inside a valid envelope: protocol skew, reported, never dropped silently. */
|
|
299
|
+
export const unknownType = (provider, what, type) => fault(provider, new Error(`Unknown ${what} message type ${type}`));
|
|
296
300
|
/**
|
|
297
301
|
* Terminal `'failed'` emission (the sync-failure contract): fires at
|
|
298
302
|
* most once per provider, only while it has never synced — destroy before
|
|
@@ -425,13 +429,13 @@ export const bindRoomProtocol = (syncProtocol, behavior) => {
|
|
|
425
429
|
// (`discarded`) and reported the same way.
|
|
426
430
|
const update = decoding.readVarUint8Array(decoder);
|
|
427
431
|
const origin = emitSynced ? provider : (behavior.tabOrigin?.(provider) ?? provider);
|
|
428
|
-
const onError = (error) => provider
|
|
432
|
+
const onError = (error) => fault(provider, error);
|
|
429
433
|
const { applied, problem, discarded } = syncProtocol.applyRemote(provider.doc, update, origin, onError);
|
|
430
434
|
const refused = problem ?? discarded;
|
|
431
435
|
if (refused) {
|
|
432
436
|
const docName = behavior.docName(provider);
|
|
433
437
|
provider.emit?.('schema-mismatch', [{ docName, problem: refused }, provider]);
|
|
434
|
-
provider
|
|
438
|
+
fault(provider, new SchemaMismatchError(docName, refused));
|
|
435
439
|
}
|
|
436
440
|
// Only an ACCEPTED Step2 is a held member state — a refused
|
|
437
441
|
// one never claims the handshake.
|
|
@@ -441,10 +445,7 @@ export const bindRoomProtocol = (syncProtocol, behavior) => {
|
|
|
441
445
|
return;
|
|
442
446
|
}
|
|
443
447
|
// Unknown sync subtype inside a valid envelope — observable.
|
|
444
|
-
provider
|
|
445
|
-
new Error(`Unknown sync message type ${syncMessageType}`),
|
|
446
|
-
provider
|
|
447
|
-
]);
|
|
448
|
+
unknownType(provider, 'sync', syncMessageType);
|
|
448
449
|
},
|
|
449
450
|
[messageQueryAwareness]: (encoder, _decoder, provider) => {
|
|
450
451
|
encoding.writeVarUint(encoder, messageAwareness);
|
|
@@ -477,10 +478,7 @@ export const bindRoomProtocol = (syncProtocol, behavior) => {
|
|
|
477
478
|
if (!messageHandler) {
|
|
478
479
|
// Fail closed + observable: a VALID envelope carrying a message
|
|
479
480
|
// type no handler claims is protocol skew — never silent.
|
|
480
|
-
provider
|
|
481
|
-
new Error(`Unknown v14 message type ${messageType}`),
|
|
482
|
-
provider
|
|
483
|
-
]);
|
|
481
|
+
unknownType(provider, 'v14', messageType);
|
|
484
482
|
return encoder;
|
|
485
483
|
}
|
|
486
484
|
const followUp = messageHandler(encoder, decoder, provider, emitSynced);
|
|
@@ -503,7 +501,7 @@ export const bindRoomProtocol = (syncProtocol, behavior) => {
|
|
|
503
501
|
catch (error) {
|
|
504
502
|
// same-tab publishes are delivered synchronously — a malformed
|
|
505
503
|
// message must not propagate into the publisher's call stack.
|
|
506
|
-
provider
|
|
504
|
+
fault(provider, error);
|
|
507
505
|
}
|
|
508
506
|
};
|
|
509
507
|
};
|
|
@@ -554,6 +552,21 @@ export const bindRoomProtocol = (syncProtocol, behavior) => {
|
|
|
554
552
|
};
|
|
555
553
|
/** The departure announcement — our presence, removed — on every transport the provider speaks. */
|
|
556
554
|
const depart = (provider) => behavior.broadcast(provider, awarenessFrame(provider.awareness, [provider.doc.clientID], new Map()));
|
|
555
|
+
/**
|
|
556
|
+
* Wire `provider` to its room: its BroadcastChannel subscriber, `onUpdate`
|
|
557
|
+
* on its document's updates and its presence changes to the room
|
|
558
|
+
* (`awarenessUpdateHandler`). The teardown removes both listeners.
|
|
559
|
+
*/
|
|
560
|
+
const listen = (provider, onUpdate) => {
|
|
561
|
+
provider._bcSubscriber = bcSubscriber(provider);
|
|
562
|
+
const onPresence = awarenessUpdateHandler(provider);
|
|
563
|
+
provider.doc.on('update', onUpdate);
|
|
564
|
+
provider.awareness.on('update', onPresence);
|
|
565
|
+
return () => {
|
|
566
|
+
provider.awareness.off('update', onPresence);
|
|
567
|
+
provider.doc.off('update', onUpdate);
|
|
568
|
+
};
|
|
569
|
+
};
|
|
557
570
|
/** Leave the room: announce the presence removal, then unsubscribe the BC channel. */
|
|
558
571
|
const disconnectBc = (provider) => {
|
|
559
572
|
depart(provider);
|
|
@@ -572,6 +585,7 @@ export const bindRoomProtocol = (syncProtocol, behavior) => {
|
|
|
572
585
|
hello,
|
|
573
586
|
depart,
|
|
574
587
|
connectBc,
|
|
575
|
-
disconnectBc
|
|
588
|
+
disconnectBc,
|
|
589
|
+
listen
|
|
576
590
|
};
|
|
577
591
|
};
|
|
@@ -7,6 +7,8 @@ export type WebsocketProviderEvents = {
|
|
|
7
7
|
status: (event: {
|
|
8
8
|
status: 'connected' | 'disconnected' | 'connecting';
|
|
9
9
|
}) => void;
|
|
10
|
+
/** What the room says this connection's user may do, as it joins (and at a change in place). */
|
|
11
|
+
access: (access: 'write' | 'suggest' | 'comment' | 'read', provider: unknown) => void;
|
|
10
12
|
synced: (state: boolean) => void;
|
|
11
13
|
'connection-close': (event: CloseEvent | null, provider: unknown) => void;
|
|
12
14
|
'connection-error': (event: Event, provider: unknown) => void;
|
|
@@ -183,14 +185,8 @@ export declare const bindWebsocketProvider: (Y: EngineApi) => {
|
|
|
183
185
|
/** Closes the current dial if it has not opened in time. */
|
|
184
186
|
_connectTimer: ReturnType<typeof setTimeout> | undefined;
|
|
185
187
|
_checkInterval: ReturnType<typeof setInterval>;
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}) => void;
|
|
189
|
-
_awarenessUpdateHandler: (updates: {
|
|
190
|
-
added: number[];
|
|
191
|
-
updated: number[];
|
|
192
|
-
removed: number[];
|
|
193
|
-
}, origin: unknown) => void;
|
|
188
|
+
/** @internal Removes the document and presence listeners (`room.listen`). */
|
|
189
|
+
_unlisten: () => void;
|
|
194
190
|
/**
|
|
195
191
|
* The dial URL: the room name as one encoded path segment (any id —
|
|
196
192
|
* `/`, `%`, `#`, `?` included — reaches its own room; the server
|
|
@@ -221,11 +217,11 @@ export declare const bindWebsocketProvider: (Y: EngineApi) => {
|
|
|
221
217
|
destroy(): void;
|
|
222
218
|
disconnect(): void;
|
|
223
219
|
connect(): void;
|
|
224
|
-
emit<N extends "refused" | "status" | "failed" | "
|
|
220
|
+
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;
|
|
225
221
|
_observers: Map<string, Set<any>>;
|
|
226
|
-
on<NAME extends "refused" | "status" | "failed" | "
|
|
227
|
-
once<NAME extends "refused" | "status" | "failed" | "
|
|
228
|
-
off<NAME extends "refused" | "status" | "failed" | "
|
|
222
|
+
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];
|
|
223
|
+
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;
|
|
224
|
+
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;
|
|
229
225
|
};
|
|
230
226
|
};
|
|
231
227
|
watchComments: (server: string | null, roomName: string, listener: (message: CommentMessage) => void) => (() => void);
|
|
@@ -52,11 +52,11 @@ import * as math from 'lib0-v14/math';
|
|
|
52
52
|
import * as url from 'lib0-v14/url';
|
|
53
53
|
import { Awareness, removeAwarenessStates } from '../protocols/awareness.js';
|
|
54
54
|
import { IsolatedObservable } from '../protocols/observable.js';
|
|
55
|
-
import { messagePermissionDenied, messageReadOnly, READ_ONLY_DENIAL, readAuthMessage } from '../protocols/auth.js';
|
|
55
|
+
import { messagePermissionDenied, messageAccess, messageReadOnly, READ_ONLY_DENIAL, SUGGEST_ONLY_DENIAL, readAuthMessage } from '../protocols/auth.js';
|
|
56
56
|
import { bindSync } from '../protocols/sync.js';
|
|
57
57
|
import { readCommentsMessage, writeCommentsSubscribe } from '../protocols/comments.js';
|
|
58
58
|
import { frame } from '../protocols/envelope.js';
|
|
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';
|
|
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
60
|
import { asEngineDoc } from '../structs.js';
|
|
61
61
|
import { ATTRIBUTION_ROOT } from '../schema.js';
|
|
62
62
|
import { ATTRIBUTION_ORIGIN, writerOf } from '../attribution/attribution.js';
|
|
@@ -122,18 +122,20 @@ export const bindWebsocketProvider = (Y) => {
|
|
|
122
122
|
if (readOnly)
|
|
123
123
|
provider._writes.deny();
|
|
124
124
|
provider.emit('permission-denied', [reason, provider]);
|
|
125
|
-
|
|
125
|
+
// What a suggest-only socket may not write is stripped: the socket stays.
|
|
126
|
+
if (!readOnly && reason !== SUGGEST_ONLY_DENIAL)
|
|
126
127
|
emitFailed(provider, new Error(`permission denied: ${reason}`));
|
|
127
128
|
},
|
|
128
129
|
// The room's notice at the join: a state, not a refusal (no event).
|
|
129
|
-
() => provider._writes.deny()
|
|
130
|
-
|
|
130
|
+
() => provider._writes.deny(),
|
|
131
|
+
// The room's word at the join, and at a change in place (`read`).
|
|
132
|
+
(access) => provider.emit('access', [access, provider]));
|
|
133
|
+
if (authType !== messagePermissionDenied &&
|
|
134
|
+
authType !== messageReadOnly &&
|
|
135
|
+
authType !== messageAccess) {
|
|
131
136
|
// An auth type we do not speak inside a valid v14 envelope is
|
|
132
137
|
// protocol skew — reported, never dropped silently.
|
|
133
|
-
provider
|
|
134
|
-
new Error(`Unknown auth message type ${authType}`),
|
|
135
|
-
provider
|
|
136
|
-
]);
|
|
138
|
+
unknownType(provider, 'auth', authType);
|
|
137
139
|
}
|
|
138
140
|
}
|
|
139
141
|
},
|
|
@@ -454,7 +456,7 @@ export const bindWebsocketProvider = (Y) => {
|
|
|
454
456
|
room.readMessage(provider, new Uint8Array(event.data), true, (buf) => sendOn(provider, websocket, buf));
|
|
455
457
|
}
|
|
456
458
|
catch (error) {
|
|
457
|
-
provider
|
|
459
|
+
fault(provider, error);
|
|
458
460
|
}
|
|
459
461
|
};
|
|
460
462
|
websocket.onerror = (event) => {
|
|
@@ -521,8 +523,8 @@ export const bindWebsocketProvider = (Y) => {
|
|
|
521
523
|
/** Closes the current dial if it has not opened in time. */
|
|
522
524
|
_connectTimer;
|
|
523
525
|
_checkInterval;
|
|
524
|
-
|
|
525
|
-
|
|
526
|
+
/** @internal Removes the document and presence listeners (`room.listen`). */
|
|
527
|
+
_unlisten;
|
|
526
528
|
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 } = {}) {
|
|
527
529
|
assertRoomId(roomname);
|
|
528
530
|
super();
|
|
@@ -543,14 +545,13 @@ export const bindWebsocketProvider = (Y) => {
|
|
|
543
545
|
if (resyncInterval > 0) {
|
|
544
546
|
this._resync = setInterval(() => send(this, room.step1(this)), resyncInterval);
|
|
545
547
|
}
|
|
546
|
-
this._bcSubscriber = room.bcSubscriber(this);
|
|
547
548
|
providers.add(this);
|
|
548
549
|
// Local doc updates go to the room; another tab's go to the server
|
|
549
550
|
// only (`broadcastUpdate` quarantines a read-only document). A
|
|
550
551
|
// socket the room made read-only gets this document's own edits
|
|
551
552
|
// alone (each is denied): what a tab or a store delivered, and the
|
|
552
553
|
// actor records, would draw denials nobody made.
|
|
553
|
-
|
|
554
|
+
const onUpdate = (update, origin, _doc, transaction) => {
|
|
554
555
|
if (origin === this)
|
|
555
556
|
return;
|
|
556
557
|
const heard = origin === this._fromTab;
|
|
@@ -568,9 +569,7 @@ export const bindWebsocketProvider = (Y) => {
|
|
|
568
569
|
};
|
|
569
570
|
this._writes = new OwnWrites(doc, (unsaved) => this.emit('saved', [{ saved: unsaved === 0, unsaved }, this]));
|
|
570
571
|
this._writes.trackDocument();
|
|
571
|
-
this.
|
|
572
|
-
this._awarenessUpdateHandler = room.awarenessUpdateHandler(this);
|
|
573
|
-
awareness.on('update', this._awarenessUpdateHandler);
|
|
572
|
+
this._unlisten = room.listen(this, onUpdate);
|
|
574
573
|
this._checkInterval = setInterval(() => {
|
|
575
574
|
if (!this.wsconnected)
|
|
576
575
|
return;
|
|
@@ -646,8 +645,7 @@ export const bindWebsocketProvider = (Y) => {
|
|
|
646
645
|
clearInterval(this._checkInterval);
|
|
647
646
|
providers.delete(this);
|
|
648
647
|
this.disconnect();
|
|
649
|
-
this.
|
|
650
|
-
this.doc.off('update', this._updateHandler);
|
|
648
|
+
this._unlisten();
|
|
651
649
|
super.destroy();
|
|
652
650
|
}
|
|
653
651
|
disconnect() {
|
package/dist/crdt/purge.d.ts
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
import type { EngineApi, EngineDoc } from './engine-api.js';
|
|
19
19
|
import { type PlacementModel } from './placement/model.js';
|
|
20
20
|
import { type TextEngine } from './text/model.js';
|
|
21
|
+
import { type SuggestFacade } from './doc/suggest.js';
|
|
21
22
|
/** The horizon: the time and encoded state vector of the room's epoch (`room.purge.timing`). */
|
|
22
23
|
export type Horizon = {
|
|
23
24
|
at: number;
|
|
@@ -43,6 +44,11 @@ export type PurgeReport = {
|
|
|
43
44
|
merged: number;
|
|
44
45
|
/** Release records the horizon covers (`hist.release.end`): deleted. */
|
|
45
46
|
released: number;
|
|
47
|
+
/**
|
|
48
|
+
* Suggestions decided before the horizon (`sugg.purge`): nothing of them
|
|
49
|
+
* pending, their records' entries older than it: their records deleted.
|
|
50
|
+
*/
|
|
51
|
+
suggestions: number;
|
|
46
52
|
};
|
|
47
53
|
/**
|
|
48
54
|
* The ended histories a document holds (`hist.release.end`): per writer,
|
|
@@ -53,7 +59,7 @@ export declare const releasesOf: (doc: EngineDoc) => Map<number, number>;
|
|
|
53
59
|
/** The horizon a document holds (`null`: never purged). */
|
|
54
60
|
export declare const readHorizon: (doc: EngineDoc) => Horizon | null;
|
|
55
61
|
export declare const bindPurge: (Y: EngineApi) => {
|
|
56
|
-
purge: (doc: EngineDoc, facade: {
|
|
62
|
+
purge: (doc: EngineDoc, facade: Partial<SuggestFacade> & {
|
|
57
63
|
model: PlacementModel;
|
|
58
64
|
text: TextEngine;
|
|
59
65
|
}, horizon: Horizon | null) => PurgeReport;
|
package/dist/crdt/purge.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { recordsOf, registryOf, view as modelView } from './placement/model.js';
|
|
2
2
|
import { removeUnit, unitOf } from './placement/slots.js';
|
|
3
3
|
import { deleteClaims, purgeStreams, scanText } from './text/model.js';
|
|
4
|
-
import { BLOCK_ATTR_ROOT, DATA, DATA_LEAF_PREFIX, DEL_PREFIX, HORIZON_ATTR, HORIZON_ROOT, REGISTRY_KEY, RELEASE_ROOT, TEXT_DELETES_ROOT, WITHDRAW_PREFIX } from './schema.js';
|
|
4
|
+
import { BLOCK_ATTR_ROOT, DATA, DATA_LEAF_PREFIX, DEL_PREFIX, HORIZON_ATTR, HORIZON_ROOT, REGISTRY_KEY, RELEASE_ROOT, SUGGEST_ROOT, TEXT_DELETES_ROOT, WITHDRAW_PREFIX } from './schema.js';
|
|
5
5
|
import { baseIdOf, isIncarnationId } from './incarnations.js';
|
|
6
|
+
import { suggestionsOf } from './doc/suggest.js';
|
|
6
7
|
import { attrItems, dropSearchMarkers, engineOps, firstItem, holdsIds } from './structs.js';
|
|
7
8
|
const mapOf = (node) => attrItems(node);
|
|
9
|
+
/** The clock a suggestion's record opened at (its writer's clocks it owns start there). */
|
|
10
|
+
const clockOf = (rec) => rec.start ?? 0;
|
|
8
11
|
/**
|
|
9
12
|
* The ended histories a document holds (`hist.release.end`): per writer,
|
|
10
13
|
* the clock its latest live release record ended at (what it wrote below
|
|
@@ -107,7 +110,8 @@ export const bindPurge = (Y) => {
|
|
|
107
110
|
records: 0,
|
|
108
111
|
claims: 0,
|
|
109
112
|
merged: 0,
|
|
110
|
-
released: 0
|
|
113
|
+
released: 0,
|
|
114
|
+
suggestions: 0
|
|
111
115
|
};
|
|
112
116
|
const vector = horizon === null
|
|
113
117
|
? new Map()
|
|
@@ -425,6 +429,32 @@ export const bindPurge = (Y) => {
|
|
|
425
429
|
report.released++;
|
|
426
430
|
}
|
|
427
431
|
}
|
|
432
|
+
// Suggestions decided before the horizon (`sugg.purge`): the decision
|
|
433
|
+
// trail goes with them. A writer's oldest record first, so no record's
|
|
434
|
+
// clocks pass to another.
|
|
435
|
+
const suggest = facade.part && suggestionsOf(facade);
|
|
436
|
+
const records = suggest?.records() ?? [];
|
|
437
|
+
if (records.length > 0) {
|
|
438
|
+
const pending = new Set(suggest.changes().map((c) => c.suggestion));
|
|
439
|
+
const root = doc.get(SUGGEST_ROOT);
|
|
440
|
+
const seen = new Set();
|
|
441
|
+
for (const rec of [...records].sort((a, b) => clockOf(a) - clockOf(b))) {
|
|
442
|
+
if (seen.has(rec.writer))
|
|
443
|
+
continue;
|
|
444
|
+
seen.add(rec.writer);
|
|
445
|
+
const node = root.getAttr(rec.id);
|
|
446
|
+
const items = [node._item];
|
|
447
|
+
for (let it = firstItem(node); it; it = it.right)
|
|
448
|
+
if (!it.deleted)
|
|
449
|
+
items.push(it);
|
|
450
|
+
if (pending.has(rec.id) ||
|
|
451
|
+
!items.every((it) => old(it.id.client, it.id.clock + it.length - 1)))
|
|
452
|
+
continue;
|
|
453
|
+
root.deleteAttr(rec.id);
|
|
454
|
+
seen.delete(rec.writer);
|
|
455
|
+
report.suggestions++;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
428
458
|
// The horizon record, last.
|
|
429
459
|
if (horizon !== null)
|
|
430
460
|
doc.get(HORIZON_ROOT).setAttr(HORIZON_ATTR, { at: horizon.at, sv: horizon.sv });
|
|
@@ -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 {};
|
package/dist/crdt/restore.d.ts
CHANGED
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
import type { EngineDoc } from './engine-api.js';
|
|
27
27
|
import { type BlockId, type ContentItem, type InlineSpec, type PlacementModel } from './placement/model.js';
|
|
28
28
|
import type { DataPatch } from './data.js';
|
|
29
|
+
import { type SuggestFacade } from './doc/suggest.js';
|
|
29
30
|
import type { JSONDoc } from '../utils/json.js';
|
|
30
31
|
/** One display unit: a character (a code point) with its marks, or an inline atom, keyed for comparison. */
|
|
31
32
|
export type Unit = {
|
|
@@ -60,7 +61,7 @@ export type RestoreReport = {
|
|
|
60
61
|
deleted: number;
|
|
61
62
|
};
|
|
62
63
|
/** What a restore reads and writes through: the facade over the document (structurally). */
|
|
63
|
-
export type RestoreFacade<Plan> = {
|
|
64
|
+
export type RestoreFacade<Plan> = Partial<SuggestFacade> & {
|
|
64
65
|
model: PlacementModel;
|
|
65
66
|
childSlots(parent: BlockId | null): readonly {
|
|
66
67
|
id: BlockId;
|
package/dist/crdt/restore.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { blockNodeOf, insertSpecsAt, moveBlocksTo, view as modelView } from './placement/model.js';
|
|
2
|
+
import { suggestionsOf } from './doc/suggest.js';
|
|
2
3
|
import { DEL_PREFIX, TYPE, WITHDRAW_PREFIX } from './schema.js';
|
|
3
4
|
import { common, patchWrites, writeData } from './data.js';
|
|
4
5
|
import { canonKey, deleteClaims } from './text/model.js';
|
|
@@ -196,5 +197,13 @@ export const restoreDocument = (doc, facade, json) => {
|
|
|
196
197
|
}
|
|
197
198
|
// 6 · The document's data.
|
|
198
199
|
facade.apply(facade.prepare.patchData(null, [{ path: [], value: json.data ?? {} }]));
|
|
200
|
+
// 7 · Pending suggestions are dropped (`sugg.restore`): the version holds
|
|
201
|
+
// none (its document without them), so what the restore left of them —
|
|
202
|
+
// their proposals — is rejected, in the restore's step (its undo brings
|
|
203
|
+
// them back).
|
|
204
|
+
// (Built over the facade, its shared suggestions: `doc/suggest.ts`.)
|
|
205
|
+
const suggest = facade.part && suggestionsOf(facade);
|
|
206
|
+
if (suggest)
|
|
207
|
+
suggest.run(suggest.decide(suggest.changes(), false));
|
|
199
208
|
return report;
|
|
200
209
|
};
|
package/dist/crdt/schema.d.ts
CHANGED
|
@@ -49,6 +49,14 @@ export declare const HORIZON_ATTR = "h";
|
|
|
49
49
|
* horizon covers.
|
|
50
50
|
*/
|
|
51
51
|
export declare const RELEASE_ROOT = "released";
|
|
52
|
+
/**
|
|
53
|
+
* The shared suggestions (`crdt/doc/suggest.ts`): attr `<id>` is one
|
|
54
|
+
* suggestion's record node, written by its writer (a client id bound to its
|
|
55
|
+
* author): its creation time `at`, `via` and `lbl`, and a list of id lists
|
|
56
|
+
* (fork patch YP31: its writer's proposed deletions, anyone else's settled
|
|
57
|
+
* ranges) and proposed operations. In the history's scope.
|
|
58
|
+
*/
|
|
59
|
+
export declare const SUGGEST_ROOT = "suggest";
|
|
52
60
|
/** Named node roles (`SCHEMA.nodes`). */
|
|
53
61
|
export declare const BLOCK_NODE = "block";
|
|
54
62
|
export declare const CONTENT_NODE = "content";
|
|
@@ -92,7 +100,7 @@ export declare const LAST_CHANGED_ATTR = "l";
|
|
|
92
100
|
export declare const CONTENT = "content";
|
|
93
101
|
/** The schema generation stamped on the `meta` root (`v`) and its manifest name (`schema`). */
|
|
94
102
|
export declare const SCHEMA: {
|
|
95
|
-
readonly version:
|
|
103
|
+
readonly version: 11;
|
|
96
104
|
readonly name: "edytor-doc";
|
|
97
105
|
readonly roots: {
|
|
98
106
|
readonly registry: "blocks";
|
package/dist/crdt/schema.js
CHANGED
|
@@ -42,6 +42,14 @@ export const HORIZON_ATTR = 'h';
|
|
|
42
42
|
* horizon covers.
|
|
43
43
|
*/
|
|
44
44
|
export const RELEASE_ROOT = 'released';
|
|
45
|
+
/**
|
|
46
|
+
* The shared suggestions (`crdt/doc/suggest.ts`): attr `<id>` is one
|
|
47
|
+
* suggestion's record node, written by its writer (a client id bound to its
|
|
48
|
+
* author): its creation time `at`, `via` and `lbl`, and a list of id lists
|
|
49
|
+
* (fork patch YP31: its writer's proposed deletions, anyone else's settled
|
|
50
|
+
* ranges) and proposed operations. In the history's scope.
|
|
51
|
+
*/
|
|
52
|
+
export const SUGGEST_ROOT = 'suggest';
|
|
45
53
|
/** Named node roles (`SCHEMA.nodes`). */
|
|
46
54
|
export const BLOCK_NODE = 'block';
|
|
47
55
|
export const CONTENT_NODE = 'content';
|
|
@@ -86,9 +94,11 @@ export const SCHEMA = {
|
|
|
86
94
|
// YP21), a block has no `claims` list; 9: a block's place is its home slot
|
|
87
95
|
// in its parent's sequence and a move is a move record (fork patch YP22),
|
|
88
96
|
// a block has no `at` map and an array item no rank; 10: the update encoding
|
|
89
|
-
// writes each distinct string once (fork patch YP24)
|
|
90
|
-
//
|
|
91
|
-
|
|
97
|
+
// writes each distinct string once (fork patch YP24); 11: a hold record
|
|
98
|
+
// inside a nested node holds nothing (fork patch YP31: a suggestion's
|
|
99
|
+
// proposals and decisions). A container or frame of an earlier generation
|
|
100
|
+
// is refused.
|
|
101
|
+
version: 11,
|
|
92
102
|
name: 'edytor-doc',
|
|
93
103
|
roots: { registry: REGISTRY_KEY, meta: META_ROOT_KEY },
|
|
94
104
|
metaAttrs: { version: 'v', schema: 'schema' }
|