edytor 0.1.0-next.21 → 0.1.0-next.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cloudflare/DocumentRoom.d.ts +302 -10
- package/dist/cloudflare/DocumentRoom.js +965 -88
- package/dist/cloudflare/index.d.ts +1 -1
- package/dist/cloudflare/index.js +1 -1
- package/dist/cloudflare/routeDocumentSocket.js +30 -6
- package/dist/collaboration/awarenessSelection.d.ts +7 -1
- package/dist/collaboration/awarenessSelection.js +7 -1
- package/dist/collaboration/index.d.ts +2 -2
- package/dist/collaboration/index.js +2 -2
- package/dist/collaboration/providers.d.ts +4 -0
- package/dist/collaboration/providers.js +4 -0
- package/dist/components/Edytor.svelte +14 -2
- package/dist/components/Edytor.svelte.d.ts +8 -1
- package/dist/crdt/admission.d.ts +6 -1
- package/dist/crdt/admission.js +9 -2
- package/dist/crdt/data.d.ts +3 -1
- package/dist/crdt/data.js +42 -32
- package/dist/crdt/document.d.ts +31 -8
- package/dist/crdt/document.js +53 -18
- package/dist/crdt/edytor-doc.d.ts +31 -3
- package/dist/crdt/edytor-doc.js +131 -14
- package/dist/crdt/engine.d.ts +6 -0
- package/dist/crdt/engine.js +7 -0
- package/dist/crdt/flow.d.ts +2 -0
- package/dist/crdt/flow.js +2 -2
- package/dist/crdt/index.d.ts +20 -9
- package/dist/crdt/index.js +3 -3
- package/dist/crdt/migration/migrate.js +9 -3
- package/dist/crdt/placement/model.d.ts +9 -1
- package/dist/crdt/placement/model.js +29 -10
- package/dist/crdt/placement/rank.d.ts +20 -0
- package/dist/crdt/placement/rank.js +79 -0
- package/dist/crdt/protocols/envelope.d.ts +20 -1
- package/dist/crdt/protocols/envelope.js +14 -2
- package/dist/crdt/providers/container.d.ts +15 -0
- package/dist/crdt/providers/container.js +17 -3
- package/dist/crdt/providers/index.d.ts +21 -1
- package/dist/crdt/providers/index.js +86 -3
- package/dist/crdt/providers/indexeddb.js +60 -40
- package/dist/crdt/providers/room.d.ts +18 -2
- package/dist/crdt/providers/room.js +29 -5
- package/dist/crdt/providers/websocket.d.ts +9 -1
- package/dist/crdt/providers/websocket.js +21 -6
- package/dist/crdt/storage.d.ts +21 -0
- package/dist/crdt/storage.js +37 -0
- package/dist/crdt/structs.d.ts +17 -0
- package/dist/crdt/structs.js +41 -0
- package/dist/crdt/text/model.d.ts +9 -1
- package/dist/crdt/text/model.js +24 -14
- package/dist/crdt/text/rows.d.ts +94 -0
- package/dist/crdt/text/rows.js +193 -0
- package/dist/crdt/text/runs.d.ts +30 -10
- package/dist/crdt/text/runs.js +1235 -264
- package/dist/edytor.svelte.d.ts +3 -1
- package/dist/edytor.svelte.js +9 -6
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/plugins/image/ImageEmpty.svelte +19 -6
- package/dist/plugins/image/ImagePlugin.svelte +9 -3
- package/dist/plugins/image/ImagePlugin.svelte.d.ts +2 -2
- package/dist/plugins/image/image.d.ts +16 -0
- package/dist/plugins/image/image.js +19 -0
- package/dist/plugins/index.d.ts +1 -1
- package/dist/plugins/index.js +1 -1
- package/dist/plugins.d.ts +11 -0
- package/dist/session/commands.d.ts +1 -0
- package/dist/session/commands.js +4 -1
- package/package.json +1 -1
package/dist/crdt/document.js
CHANGED
|
@@ -143,7 +143,7 @@
|
|
|
143
143
|
import { DEV } from 'esm-env';
|
|
144
144
|
import { Y } from './engine.js';
|
|
145
145
|
import { bindAttribution } from './attribution/index.js';
|
|
146
|
-
import { bindEdytorDoc, isInitialized, lineageDepthOf } from './edytor-doc.js';
|
|
146
|
+
import { bindEdytorDoc, DEFAULT_HISTORY_LIMIT, isInitialized, lineageDepthOf } from './edytor-doc.js';
|
|
147
147
|
import { assertAdmission, assertSchema, bindAdmission, checkSchema } from './admission.js';
|
|
148
148
|
import { Awareness } from './protocols/awareness.js';
|
|
149
149
|
import { callEach } from './protocols/observable.js';
|
|
@@ -199,12 +199,23 @@ export class DocumentDestroyedError extends Error {
|
|
|
199
199
|
* does: from the socket's open, so a slow dial never counts).
|
|
200
200
|
*/
|
|
201
201
|
export const DEFAULT_READINESS_BOUND = 1000;
|
|
202
|
-
const normalizeRole = (role) =>
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
})
|
|
202
|
+
const normalizeRole = (role) => {
|
|
203
|
+
const atomic = [
|
|
204
|
+
...new Map((role?.atomic ?? []).map((p) => {
|
|
205
|
+
const path = typeof p === 'string' ? [p] : [...p];
|
|
206
|
+
return [JSON.stringify(path), path];
|
|
207
|
+
}))
|
|
208
|
+
]
|
|
209
|
+
.sort(([a], [b]) => (a < b ? -1 : 1))
|
|
210
|
+
.map(([, path]) => path);
|
|
211
|
+
return {
|
|
212
|
+
void: role?.void === true,
|
|
213
|
+
island: role?.island === true,
|
|
214
|
+
lines: role?.lines === true,
|
|
215
|
+
...(role?.layout === true && { layout: true }),
|
|
216
|
+
...(atomic.length > 0 && { atomic })
|
|
217
|
+
};
|
|
218
|
+
};
|
|
208
219
|
const anonymousActor = () => ({
|
|
209
220
|
id: `anon-${crypto.randomUUID()}`
|
|
210
221
|
});
|
|
@@ -307,6 +318,8 @@ export class EdytorDocument {
|
|
|
307
318
|
_defaultType;
|
|
308
319
|
_historyOptions;
|
|
309
320
|
_lineageDepth;
|
|
321
|
+
/** {@link DocumentOptions.requireHydration}: an empty document waits for a provider's `synced`. */
|
|
322
|
+
requireHydration;
|
|
310
323
|
/** Attached providers keyed by transport target (O75): one per target. */
|
|
311
324
|
_providers = new Map();
|
|
312
325
|
_pendingSyncs = 0;
|
|
@@ -336,6 +349,7 @@ export class EdytorDocument {
|
|
|
336
349
|
this._ownsAwareness = init.awareness === undefined;
|
|
337
350
|
this._historyOptions = init.history;
|
|
338
351
|
this._lineageDepth = init.lineage?.depth;
|
|
352
|
+
this.requireHydration = init.requireHydration === true;
|
|
339
353
|
this.actor = init.actor ?? anonymousActor();
|
|
340
354
|
// Durable actor identity is separate from the presence profile: the
|
|
341
355
|
// `actor` field carries the stable id (U5/U6 stub seam), `user`
|
|
@@ -418,6 +432,14 @@ export class EdytorDocument {
|
|
|
418
432
|
get historyCaptureTimeout() {
|
|
419
433
|
return this._historyOptions?.captureTimeout;
|
|
420
434
|
}
|
|
435
|
+
/**
|
|
436
|
+
* The undo steps this document's history keeps (P6): the configured
|
|
437
|
+
* `history.limit`, else {@link DEFAULT_HISTORY_LIMIT}. The `attachDocument`
|
|
438
|
+
* dedupe check reads it (a reattach naming another limit is a conflict).
|
|
439
|
+
*/
|
|
440
|
+
get historyLimit() {
|
|
441
|
+
return this._historyOptions?.limit ?? DEFAULT_HISTORY_LIMIT;
|
|
442
|
+
}
|
|
421
443
|
/**
|
|
422
444
|
* The lineage ring depth this document was configured with — read by
|
|
423
445
|
* the `attachDocument` dedupe compatibility check (a reattach naming
|
|
@@ -657,17 +679,17 @@ export class EdytorDocument {
|
|
|
657
679
|
this._history = this.facade.createUndoManager({
|
|
658
680
|
trackedOrigins: this._trackedOrigins,
|
|
659
681
|
captureTimeout: this._historyOptions?.captureTimeout,
|
|
682
|
+
limit: this.historyLimit,
|
|
660
683
|
captureTransaction: (transaction) => transaction.local !== false
|
|
661
684
|
});
|
|
662
685
|
return this._history;
|
|
663
686
|
};
|
|
664
687
|
/**
|
|
665
|
-
*
|
|
666
|
-
*
|
|
667
|
-
*
|
|
668
|
-
*
|
|
669
|
-
*
|
|
670
|
-
* nothing to clear — it does NOT force attach or throw on pending).
|
|
688
|
+
* Drop the undo/redo stacks. The undo stack already keeps only its
|
|
689
|
+
* newest `history.limit` steps (P6); call this where undo no longer makes
|
|
690
|
+
* sense, e.g. after a save checkpoint. No-op while history has not
|
|
691
|
+
* attached (there is nothing to clear — it does NOT force attach or throw
|
|
692
|
+
* on pending).
|
|
671
693
|
*/
|
|
672
694
|
clearHistory = () => {
|
|
673
695
|
if (this._destroyed) {
|
|
@@ -740,9 +762,12 @@ export class EdytorDocument {
|
|
|
740
762
|
* decides again when it turns writable (the refusal propagates to the
|
|
741
763
|
* reporting provider only).
|
|
742
764
|
*/
|
|
743
|
-
_decide = (value, report = false) => {
|
|
765
|
+
_decide = (value, report = false, synced = false) => {
|
|
744
766
|
if (this._destroyed || this.ready)
|
|
745
767
|
return;
|
|
768
|
+
// H12: an empty document waits for a provider that synced.
|
|
769
|
+
if (this.requireHydration && !synced && !isInitialized(this.doc))
|
|
770
|
+
return;
|
|
746
771
|
const waiting = this._pendingSyncs > 0 || this._refusals.size > 0;
|
|
747
772
|
if (waiting && !isInitialized(this.doc))
|
|
748
773
|
return;
|
|
@@ -824,7 +849,7 @@ export class EdytorDocument {
|
|
|
824
849
|
synced: () => {
|
|
825
850
|
settle();
|
|
826
851
|
this._refusals.delete(target);
|
|
827
|
-
this._decide(opts.value, true);
|
|
852
|
+
this._decide(opts.value, true, true);
|
|
828
853
|
},
|
|
829
854
|
failed,
|
|
830
855
|
armBound: () => {
|
|
@@ -1017,9 +1042,16 @@ export const bindDocument = (Y) => {
|
|
|
1017
1042
|
options.history.captureTimeout !== existing.historyCaptureTimeout) {
|
|
1018
1043
|
throw new SemanticConflictError('history.captureTimeout', existing.historyCaptureTimeout, options.history.captureTimeout);
|
|
1019
1044
|
}
|
|
1045
|
+
if (options.history?.limit !== undefined && options.history.limit !== existing.historyLimit) {
|
|
1046
|
+
throw new SemanticConflictError('history.limit', existing.historyLimit, options.history.limit);
|
|
1047
|
+
}
|
|
1020
1048
|
if (options.lineage?.depth !== undefined && options.lineage.depth !== existing.lineageDepth) {
|
|
1021
1049
|
throw new SemanticConflictError('lineage.depth', existing.lineageDepth, options.lineage.depth);
|
|
1022
1050
|
}
|
|
1051
|
+
if (options.requireHydration !== undefined &&
|
|
1052
|
+
options.requireHydration !== existing.requireHydration) {
|
|
1053
|
+
throw new SemanticConflictError('requireHydration', existing.requireHydration, options.requireHydration);
|
|
1054
|
+
}
|
|
1023
1055
|
if (options.semantics !== undefined) {
|
|
1024
1056
|
existing.adoptSemantics(options.semantics);
|
|
1025
1057
|
}
|
|
@@ -1045,7 +1077,8 @@ export const bindDocument = (Y) => {
|
|
|
1045
1077
|
awareness: options.awareness,
|
|
1046
1078
|
semantics: options.semantics,
|
|
1047
1079
|
history: options.history,
|
|
1048
|
-
lineage: options.lineage
|
|
1080
|
+
lineage: options.lineage,
|
|
1081
|
+
requireHydration: options.requireHydration
|
|
1049
1082
|
});
|
|
1050
1083
|
attached.set(document.doc, document);
|
|
1051
1084
|
if (options.value !== undefined) {
|
|
@@ -1082,7 +1115,8 @@ export const bindDocument = (Y) => {
|
|
|
1082
1115
|
awareness: options.awareness,
|
|
1083
1116
|
semantics: options.semantics,
|
|
1084
1117
|
history: options.history,
|
|
1085
|
-
lineage: options.lineage
|
|
1118
|
+
lineage: options.lineage,
|
|
1119
|
+
requireHydration: options.requireHydration
|
|
1086
1120
|
});
|
|
1087
1121
|
attached.set(doc, document);
|
|
1088
1122
|
try {
|
|
@@ -1144,7 +1178,8 @@ export const bindDocument = (Y) => {
|
|
|
1144
1178
|
awareness: options.awareness,
|
|
1145
1179
|
semantics: options.semantics,
|
|
1146
1180
|
history: options.history,
|
|
1147
|
-
lineage: options.lineage
|
|
1181
|
+
lineage: options.lineage,
|
|
1182
|
+
requireHydration: options.requireHydration
|
|
1148
1183
|
});
|
|
1149
1184
|
attached.set(doc, document);
|
|
1150
1185
|
return document;
|
|
@@ -98,6 +98,8 @@ export declare const assertUsableDoc: (doc: EngineDoc) => void;
|
|
|
98
98
|
* counterpart of `DocumentDestroyedError` — kept in this module because a
|
|
99
99
|
* bare `bindEdytorDoc` facade has no document layer above it.
|
|
100
100
|
*/
|
|
101
|
+
/** The undo steps a document's history keeps by default (P6): older ones are released. */
|
|
102
|
+
export declare const DEFAULT_HISTORY_LIMIT = 200;
|
|
101
103
|
export declare class EdytorDocDisposedError extends Error {
|
|
102
104
|
constructor(service?: string);
|
|
103
105
|
}
|
|
@@ -120,6 +122,14 @@ export type BlockRole = {
|
|
|
120
122
|
* a `defaultChild`.
|
|
121
123
|
*/
|
|
122
124
|
layout?: boolean;
|
|
125
|
+
/**
|
|
126
|
+
* Data paths written as one leaf (`data.atomic`, H8): a top-level key, or
|
|
127
|
+
* an array of keys for a nested one (`['link', ['media', 'source']]`). An
|
|
128
|
+
* assignment there, or anywhere under it, writes the whole value as one
|
|
129
|
+
* last-writer-wins leaf, so two concurrent assignments never merge into a
|
|
130
|
+
* value neither wrote: one wins whole.
|
|
131
|
+
*/
|
|
132
|
+
atomic?: readonly (string | readonly string[])[];
|
|
123
133
|
};
|
|
124
134
|
/** Island-sealing policy for a walk in document order (R5; see `next`). */
|
|
125
135
|
export type OrderPolicy = {
|
|
@@ -495,7 +505,7 @@ export declare const bindEdytorDoc: (Y: EngineApi) => {
|
|
|
495
505
|
isSelfOrDescendant: (placements: Map<BlockId, import("./placement/model.js").ResolvedPlacement>, own: import("./text/model.js").Ownership, id: BlockId, maybeAncestor: BlockId) => boolean;
|
|
496
506
|
ranksAt: (siblings: readonly {
|
|
497
507
|
rank: string;
|
|
498
|
-
}[], index: number, count: number, clientId: number, rand?: () => number) => string[];
|
|
508
|
+
}[], index: number, count: number, clientId: number, rand?: () => number, run?: boolean) => string[];
|
|
499
509
|
writePlacement: (doc: EngineDoc, node: EngineNode, p: BlockId | null, r: string) => void;
|
|
500
510
|
withdrawOnUndo: (doc: EngineDoc) => (item: import("./engine-api.js").EngineItemRef, stackItem: {
|
|
501
511
|
inserts: {
|
|
@@ -617,7 +627,17 @@ export declare const bindEdytorDoc: (Y: EngineApi) => {
|
|
|
617
627
|
checkSchema: () => SchemaProblem | null;
|
|
618
628
|
assertSchema: () => void;
|
|
619
629
|
dispose: () => void;
|
|
620
|
-
createUndoManager: (
|
|
630
|
+
createUndoManager: (options?: ConstructorParameters<EngineApi["UndoManager"]>[1] & {
|
|
631
|
+
limit?: number;
|
|
632
|
+
}) => YUndoManager;
|
|
633
|
+
/**
|
|
634
|
+
* Drop every step of a history `createUndoManager` made (undo and
|
|
635
|
+
* redo) and release what they kept for their undo, as the history
|
|
636
|
+
* limit does (P6): the engine collects that content now, the doc's
|
|
637
|
+
* `gcFilter` still deciding. The room's validation history (H2) runs
|
|
638
|
+
* it after each frame.
|
|
639
|
+
*/
|
|
640
|
+
releaseHistory: (um: YUndoManager) => void;
|
|
621
641
|
project: () => import("./index.js").ProjectedDoc;
|
|
622
642
|
toJSON: () => JSONDoc;
|
|
623
643
|
blockJSON: (id: string) => JSONBlock;
|
|
@@ -696,6 +716,14 @@ export declare const bindEdytorDoc: (Y: EngineApi) => {
|
|
|
696
716
|
})[];
|
|
697
717
|
onChange: (cb: (change: DocChange) => void) => (() => void);
|
|
698
718
|
};
|
|
719
|
+
/**
|
|
720
|
+
* Keep, on `doc`, the text a replica may have to copy again (P11): its
|
|
721
|
+
* `gcFilter` then spares a deleted copy's content, as every facade's
|
|
722
|
+
* history does. A doc that only relays updates (the room) calls it
|
|
723
|
+
* before applying anything, so what it collects and encodes matches
|
|
724
|
+
* what an editing replica keeps.
|
|
725
|
+
*/
|
|
726
|
+
keepCopies: (doc: EngineDoc) => void;
|
|
699
727
|
/** The bound engine layers (same instances the facades use). */
|
|
700
728
|
model: {
|
|
701
729
|
REGISTRY_KEY: string;
|
|
@@ -712,7 +740,7 @@ export declare const bindEdytorDoc: (Y: EngineApi) => {
|
|
|
712
740
|
isSelfOrDescendant: (placements: Map<BlockId, import("./placement/model.js").ResolvedPlacement>, own: import("./text/model.js").Ownership, id: BlockId, maybeAncestor: BlockId) => boolean;
|
|
713
741
|
ranksAt: (siblings: readonly {
|
|
714
742
|
rank: string;
|
|
715
|
-
}[], index: number, count: number, clientId: number, rand?: () => number) => string[];
|
|
743
|
+
}[], index: number, count: number, clientId: number, rand?: () => number, run?: boolean) => string[];
|
|
716
744
|
writePlacement: (doc: EngineDoc, node: EngineNode, p: BlockId | null, r: string) => void;
|
|
717
745
|
withdrawOnUndo: (doc: EngineDoc) => (item: import("./engine-api.js").EngineItemRef, stackItem: {
|
|
718
746
|
inserts: {
|
package/dist/crdt/edytor-doc.js
CHANGED
|
@@ -118,7 +118,7 @@ import { bindBlockAttribution, blockAttributionOf } from './attribution/block.js
|
|
|
118
118
|
import { isLegacyDoc } from './migration/legacy-schema.js';
|
|
119
119
|
import { rangeDeleteOps } from './rangeDelete.js';
|
|
120
120
|
import { flowOps } from './flow.js';
|
|
121
|
-
import { dataLeaves, isObject, itemIds, patchWrites, readData, writeLeaves } from './data.js';
|
|
121
|
+
import { dataLeaves, isObject, itemIds, leafKey, patchWrites, readData, writeLeaves } from './data.js';
|
|
122
122
|
import { id as newId } from '../utils.js';
|
|
123
123
|
import { cloneJsonSafe, jsonBlockToSpec, jsonEquals, sanitizeSpec, sanitizeWireJson, sanitizeWireString } from '../utils/json.js';
|
|
124
124
|
// ── schema manifest ─────────────────────────────────────────────────────
|
|
@@ -249,6 +249,10 @@ export const assertUsableDoc = (doc) => {
|
|
|
249
249
|
* counterpart of `DocumentDestroyedError` — kept in this module because a
|
|
250
250
|
* bare `bindEdytorDoc` facade has no document layer above it.
|
|
251
251
|
*/
|
|
252
|
+
/** The undo steps a document's history keeps by default (P6): older ones are released. */
|
|
253
|
+
export const DEFAULT_HISTORY_LIMIT = 200;
|
|
254
|
+
/** The origin of the transaction that releases a dropped history step's content. */
|
|
255
|
+
const HISTORY_TRIM = Symbol('edytor.history.trim');
|
|
252
256
|
export class EdytorDocDisposedError extends Error {
|
|
253
257
|
constructor(service) {
|
|
254
258
|
super(`EdytorDoc${service ? `.${service}` : ''}: facade is disposed.`);
|
|
@@ -341,6 +345,8 @@ export const bindEdytorDoc = (Y) => {
|
|
|
341
345
|
const M = bindModel(Y);
|
|
342
346
|
const T = bindText(Y);
|
|
343
347
|
const D = bindDeletes(Y);
|
|
348
|
+
/** Each history's release of all its steps (`releaseHistory`). */
|
|
349
|
+
const releasers = new WeakMap();
|
|
344
350
|
// U1 — compact per-block attribution writes (`attribution/block.ts`).
|
|
345
351
|
// One bound instance per engine binding; its suppression memory is
|
|
346
352
|
// per-doc (WeakMap-keyed), so facades on the same doc share it.
|
|
@@ -1071,10 +1077,11 @@ export const bindEdytorDoc = (Y) => {
|
|
|
1071
1077
|
node.setAttr(key, value);
|
|
1072
1078
|
}
|
|
1073
1079
|
};
|
|
1074
|
-
const createUndoManager = (
|
|
1080
|
+
const createUndoManager = (options = {}) => {
|
|
1075
1081
|
if (disposed) {
|
|
1076
1082
|
throw new EdytorDocDisposedError('createUndoManager');
|
|
1077
1083
|
}
|
|
1084
|
+
const { limit = DEFAULT_HISTORY_LIMIT, ...opts } = options;
|
|
1078
1085
|
write(() => {
|
|
1079
1086
|
if (!isInitialized(doc))
|
|
1080
1087
|
init(doc);
|
|
@@ -1102,6 +1109,49 @@ export const bindEdytorDoc = (Y) => {
|
|
|
1102
1109
|
};
|
|
1103
1110
|
um.on('stack-item-added', skipOwnText);
|
|
1104
1111
|
um.on('stack-item-updated', skipOwnText);
|
|
1112
|
+
// P6: the undo stack keeps its newest `limit` steps. A step that
|
|
1113
|
+
// falls off releases what it kept for its undo (its deleted items),
|
|
1114
|
+
// and the engine collects their content now, as it would have at
|
|
1115
|
+
// the delete without a history (the doc's `gcFilter` still decides:
|
|
1116
|
+
// a text copy another replica may have to copy again stays, P11).
|
|
1117
|
+
// An item is released only once every step that deleted part of it
|
|
1118
|
+
// fell off (the engine merges deleted items across steps): never
|
|
1119
|
+
// split, so the store keeps its merged items. Only the items
|
|
1120
|
+
// themselves are released: a kept container's flag may guard a
|
|
1121
|
+
// newer step's items inside it.
|
|
1122
|
+
const released = Y.createIdSet();
|
|
1123
|
+
const covered = (st) => {
|
|
1124
|
+
const end = st.id.clock + st.length;
|
|
1125
|
+
return (released.clients.get(st.id.client)?.getIds() ?? []).some((r) => r.clock <= st.id.clock && end <= r.clock + r.len);
|
|
1126
|
+
};
|
|
1127
|
+
const release = (dropped) => {
|
|
1128
|
+
if (dropped.length === 0)
|
|
1129
|
+
return;
|
|
1130
|
+
for (const step of dropped)
|
|
1131
|
+
Y.insertIntoIdSet(released, step.deletes);
|
|
1132
|
+
const gc = doc.gc;
|
|
1133
|
+
const keepIt = doc.gcFilter;
|
|
1134
|
+
doc.transact((tr) => {
|
|
1135
|
+
for (const step of dropped)
|
|
1136
|
+
walkIdSetStructs(Y, doc, step.deletes, (st) => {
|
|
1137
|
+
const it = st;
|
|
1138
|
+
if (it.content === undefined || it.keep !== true || !covered(it))
|
|
1139
|
+
return;
|
|
1140
|
+
it.keep = false;
|
|
1141
|
+
if (gc && it.deleted && keepIt(it))
|
|
1142
|
+
it.gc(tr, false);
|
|
1143
|
+
});
|
|
1144
|
+
}, HISTORY_TRIM);
|
|
1145
|
+
};
|
|
1146
|
+
const trim = ({ type }) => {
|
|
1147
|
+
const over = um.undoStack.length - limit;
|
|
1148
|
+
if (type !== 'undo' || !(over > 0))
|
|
1149
|
+
return;
|
|
1150
|
+
release(um.undoStack.splice(0, over));
|
|
1151
|
+
};
|
|
1152
|
+
um.on('stack-item-added', trim);
|
|
1153
|
+
// `releaseHistory(um)`: every step dropped and released by the same rule.
|
|
1154
|
+
releasers.set(um, () => release([...um.undoStack.splice(0), ...um.redoStack.splice(0)]));
|
|
1105
1155
|
// Lineage for undo/redo (O19, F4): the replay displaces the state
|
|
1106
1156
|
// every block the popped stack item touches, so each one's subtree
|
|
1107
1157
|
// is captured (`force`: lost whoever owns `l`) from the history
|
|
@@ -1226,7 +1276,7 @@ export const bindEdytorDoc = (Y) => {
|
|
|
1226
1276
|
const i = T.resolveAnchor(doc, text, anchor.a);
|
|
1227
1277
|
if (i === null)
|
|
1228
1278
|
return null;
|
|
1229
|
-
const s = own.
|
|
1279
|
+
const s = own.streamAt(anchor.b, i);
|
|
1230
1280
|
const owner = s === undefined ? DEAD : own.ownerOf(s.block);
|
|
1231
1281
|
if (typeof owner !== 'string' || !isLiveIn(view(), owner))
|
|
1232
1282
|
return null;
|
|
@@ -1270,11 +1320,30 @@ export const bindEdytorDoc = (Y) => {
|
|
|
1270
1320
|
});
|
|
1271
1321
|
/** This client's next clock: what its source ranks are tied by (`sourceRank`, DW-05). */
|
|
1272
1322
|
const clock = () => doc.store?.getClock(doc.clientID) ?? 0;
|
|
1273
|
-
/**
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1323
|
+
/**
|
|
1324
|
+
* `count` ranks at `index` among `parent`'s children, the moving
|
|
1325
|
+
* `exclude` left out. `run`: new blocks, which extend this client's run
|
|
1326
|
+
* after a block it ranked (H1, `order.insert.run`); a move never does
|
|
1327
|
+
* (the rank-growth guard).
|
|
1328
|
+
*/
|
|
1329
|
+
const ranksFor = (parent, index, count, exclude = [], run = false) => {
|
|
1330
|
+
const v = view();
|
|
1331
|
+
const all = v.kids.get(parent) ?? [];
|
|
1332
|
+
// The moving blocks' own slots left out: index → the list's index past them.
|
|
1333
|
+
const gone = exclude
|
|
1334
|
+
.map((id) => M.positionInView(v, id))
|
|
1335
|
+
.filter((p) => p !== null && p.parent === parent)
|
|
1336
|
+
.map((p) => p.index)
|
|
1337
|
+
.sort((a, b) => a - b);
|
|
1338
|
+
const raw = (i) => {
|
|
1339
|
+
for (const g of gone)
|
|
1340
|
+
if (g <= i)
|
|
1341
|
+
i++;
|
|
1342
|
+
return i;
|
|
1343
|
+
};
|
|
1344
|
+
const at = Math.max(0, Math.min(index, all.length - gone.length));
|
|
1345
|
+
const pair = [at > 0 ? all[raw(at - 1)] : undefined, all[raw(at)]];
|
|
1346
|
+
return M.ranksAt(pair, 1, count, doc.clientID, randOf(doc), run);
|
|
1278
1347
|
};
|
|
1279
1348
|
/**
|
|
1280
1349
|
* Move `ids` to `ranks` under `parent` (`index`: the slot hooks see).
|
|
@@ -1380,7 +1449,12 @@ export const bindEdytorDoc = (Y) => {
|
|
|
1380
1449
|
const dataSteps = (target, patches) => {
|
|
1381
1450
|
const [id, inlineId] = typeof target === 'object' && target ? [target.block, target.atom] : [target ?? undefined];
|
|
1382
1451
|
const node = dataNode(id, inlineId);
|
|
1383
|
-
|
|
1452
|
+
// A block kind's atomic paths are written as one leaf each (`data.atomic`, H8).
|
|
1453
|
+
const type = inlineId === undefined && id !== undefined ? blockTypeOf(id) : undefined;
|
|
1454
|
+
const atomic = (type === undefined ? undefined : roleOf(type)?.atomic) ?? [];
|
|
1455
|
+
const leaves = node
|
|
1456
|
+
? patchWrites(node, patches, doc.clientID, randOf(doc), atomic.map((p) => leafKey(typeof p === 'string' ? [p] : p)))
|
|
1457
|
+
: [];
|
|
1384
1458
|
if (leaves === null)
|
|
1385
1459
|
return null; // a patch fits no value there
|
|
1386
1460
|
if (leaves.length === 0)
|
|
@@ -1400,6 +1474,26 @@ export const bindEdytorDoc = (Y) => {
|
|
|
1400
1474
|
const replaceData = (data) => [
|
|
1401
1475
|
{ path: [], value: sanitizeWireJson(data) }
|
|
1402
1476
|
];
|
|
1477
|
+
/**
|
|
1478
|
+
* `data`'s leaves as sets over `current` (`data.retype.keep`, H4): a
|
|
1479
|
+
* plain object's keys, recursively where `current` holds an object
|
|
1480
|
+
* there too; anything else (an array, a primitive, an empty object, an
|
|
1481
|
+
* object where `current` holds none) as one value at its path. Nothing
|
|
1482
|
+
* else under the root is touched.
|
|
1483
|
+
*/
|
|
1484
|
+
const leafSets = (data, current) => {
|
|
1485
|
+
const out = [];
|
|
1486
|
+
const plainObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
1487
|
+
const walk = (v, at, path) => {
|
|
1488
|
+
if (plainObject(v) && Object.keys(v).length > 0 && (path.length === 0 || plainObject(at)))
|
|
1489
|
+
for (const [k, x] of Object.entries(v))
|
|
1490
|
+
walk(x, plainObject(at) ? at[k] : undefined, [...path, k]);
|
|
1491
|
+
else if (path.length > 0)
|
|
1492
|
+
out.push({ path, value: v });
|
|
1493
|
+
};
|
|
1494
|
+
walk(sanitizeWireJson(data), current, []);
|
|
1495
|
+
return out;
|
|
1496
|
+
};
|
|
1403
1497
|
/**
|
|
1404
1498
|
* The kind `kid` shows once it leaves `from` for a slot under `parent`
|
|
1405
1499
|
* — the island and container rules, one answer:
|
|
@@ -1602,7 +1696,7 @@ export const bindEdytorDoc = (Y) => {
|
|
|
1602
1696
|
return plan([], []);
|
|
1603
1697
|
if ((parent !== null && !live(parent)) || M.collides(doc, clean))
|
|
1604
1698
|
return REFUSED;
|
|
1605
|
-
const ranks = ranksFor(parent, dest.index, clean.length);
|
|
1699
|
+
const ranks = ranksFor(parent, dest.index, clean.length, [], true);
|
|
1606
1700
|
return plan(clean.map((s) => s.id), [{ op: 'insertBlocks', parent, index: dest.index, specs: clean, ranks }]);
|
|
1607
1701
|
};
|
|
1608
1702
|
/**
|
|
@@ -1700,7 +1794,7 @@ export const bindEdytorDoc = (Y) => {
|
|
|
1700
1794
|
const last = ids.at(-1);
|
|
1701
1795
|
const { parent, index } = positionOf(levels.at(-1) ?? last);
|
|
1702
1796
|
if (levels.length === 0) {
|
|
1703
|
-
const ranks = ranksFor(parent, index + 1, specs.length);
|
|
1797
|
+
const ranks = ranksFor(parent, index + 1, specs.length, [], true);
|
|
1704
1798
|
return plan(placed, [...insert(parent, index + 1, specs, ranks), ...retype]);
|
|
1705
1799
|
}
|
|
1706
1800
|
// Bottom up: what stays before the split at each level (the level
|
|
@@ -2265,8 +2359,11 @@ export const bindEdytorDoc = (Y) => {
|
|
|
2265
2359
|
return steps ? plan(t === null ? [] : [typeof t === 'string' ? t : t.block], steps) : REFUSED;
|
|
2266
2360
|
};
|
|
2267
2361
|
/**
|
|
2268
|
-
* Baseline `setBlock`: `type
|
|
2269
|
-
*
|
|
2362
|
+
* Baseline `setBlock`: `type` updates the block in place and `data`
|
|
2363
|
+
* sets the leaves it names, removing none (`data.retype.keep`, H4: a
|
|
2364
|
+
* retype keeps the block's properties, as Notion's Turn into does;
|
|
2365
|
+
* `setBlockData` replaces them); `content`/`children` REPLACE
|
|
2366
|
+
* wholesale — explicit replacement is a
|
|
2270
2367
|
* new-identity operation; a retype to a void kind without `children`
|
|
2271
2368
|
* unnests the current ones (`retypeSteps`). All-or-nothing (D-12): children
|
|
2272
2369
|
* for a block that is `void` after the write, or a replacement id that
|
|
@@ -2287,13 +2384,16 @@ export const bindEdytorDoc = (Y) => {
|
|
|
2287
2384
|
return REFUSED;
|
|
2288
2385
|
if (children !== undefined && M.collides(doc, children))
|
|
2289
2386
|
return refused('id-collision');
|
|
2387
|
+
const data = value.data === undefined ? [] : dataSteps(id, leafSets(value.data, blockDataOf(id)));
|
|
2388
|
+
if (data === null)
|
|
2389
|
+
return REFUSED;
|
|
2290
2390
|
const writes = [
|
|
2291
2391
|
...(type === undefined
|
|
2292
2392
|
? []
|
|
2293
2393
|
: children === undefined
|
|
2294
2394
|
? retypeSteps(id, type)
|
|
2295
2395
|
: attr(id, TYPE, type)),
|
|
2296
|
-
...
|
|
2396
|
+
...data
|
|
2297
2397
|
];
|
|
2298
2398
|
if (content !== undefined) {
|
|
2299
2399
|
const length = displayLength(id);
|
|
@@ -2451,6 +2551,7 @@ export const bindEdytorDoc = (Y) => {
|
|
|
2451
2551
|
insideItem: (id) => insideItem(id),
|
|
2452
2552
|
tailOf: (id) => ({ type: kindToCopy(id), data: blockDataOf(id) }),
|
|
2453
2553
|
ranksFor,
|
|
2554
|
+
insertRanks: (parent, index, count) => ranksFor(parent, index, count, [], true),
|
|
2454
2555
|
pieceRanks,
|
|
2455
2556
|
redata: (id, data) => dataSteps(id, replaceData(data)) ?? [],
|
|
2456
2557
|
deleteBlocks: (ids, filled) => deleteBlocks(ids, false, filled),
|
|
@@ -2559,6 +2660,14 @@ export const bindEdytorDoc = (Y) => {
|
|
|
2559
2660
|
assertSchema: () => assertSchema(doc),
|
|
2560
2661
|
dispose,
|
|
2561
2662
|
createUndoManager,
|
|
2663
|
+
/**
|
|
2664
|
+
* Drop every step of a history `createUndoManager` made (undo and
|
|
2665
|
+
* redo) and release what they kept for their undo, as the history
|
|
2666
|
+
* limit does (P6): the engine collects that content now, the doc's
|
|
2667
|
+
* `gcFilter` still deciding. The room's validation history (H2) runs
|
|
2668
|
+
* it after each frame.
|
|
2669
|
+
*/
|
|
2670
|
+
releaseHistory: (um) => releasers.get(um)?.(),
|
|
2562
2671
|
// reads — every id argument normalizes at ingress (O1)
|
|
2563
2672
|
project: () => M.project(doc),
|
|
2564
2673
|
toJSON,
|
|
@@ -2679,6 +2788,14 @@ export const bindEdytorDoc = (Y) => {
|
|
|
2679
2788
|
SchemaMismatchError,
|
|
2680
2789
|
/** Attach the per-doc facade. */
|
|
2681
2790
|
create,
|
|
2791
|
+
/**
|
|
2792
|
+
* Keep, on `doc`, the text a replica may have to copy again (P11): its
|
|
2793
|
+
* `gcFilter` then spares a deleted copy's content, as every facade's
|
|
2794
|
+
* history does. A doc that only relays updates (the room) calls it
|
|
2795
|
+
* before applying anything, so what it collects and encodes matches
|
|
2796
|
+
* what an editing replica keeps.
|
|
2797
|
+
*/
|
|
2798
|
+
keepCopies: (doc) => void D.scope(doc),
|
|
2682
2799
|
/** The bound engine layers (same instances the facades use). */
|
|
2683
2800
|
model: M,
|
|
2684
2801
|
text: T,
|
package/dist/crdt/engine.d.ts
CHANGED
|
@@ -17,6 +17,12 @@ export type EngineSymbol =
|
|
|
17
17
|
| 'UpdateEncoderV1'
|
|
18
18
|
| 'writeIdSet'
|
|
19
19
|
| 'mergeUpdates'
|
|
20
|
+
| 'applyUpdateV2'
|
|
21
|
+
| 'encodeStateAsUpdateV2'
|
|
22
|
+
| 'mergeUpdatesV2'
|
|
23
|
+
| 'convertUpdateFormatV2ToV1'
|
|
24
|
+
| 'UpdateDecoderV1'
|
|
25
|
+
| 'UpdateEncoderV2'
|
|
20
26
|
| 'Skip'
|
|
21
27
|
| 'findIndexSS'
|
|
22
28
|
| 'createRelativePositionFromTypeIndex'
|
package/dist/crdt/engine.js
CHANGED
|
@@ -30,6 +30,13 @@ export const Y = {
|
|
|
30
30
|
UpdateEncoderV1: V.UpdateEncoderV1,
|
|
31
31
|
writeIdSet: V.writeIdSet,
|
|
32
32
|
mergeUpdates: V.mergeUpdates,
|
|
33
|
+
// storage in the v2 (columnar) encoding (P5): room rows and the IndexedDB store
|
|
34
|
+
applyUpdateV2: V.applyUpdateV2,
|
|
35
|
+
encodeStateAsUpdateV2: V.encodeStateAsUpdateV2,
|
|
36
|
+
mergeUpdatesV2: V.mergeUpdatesV2,
|
|
37
|
+
convertUpdateFormatV2ToV1: V.convertUpdateFormatV2ToV1,
|
|
38
|
+
UpdateDecoderV1: V.UpdateDecoderV1,
|
|
39
|
+
UpdateEncoderV2: V.UpdateEncoderV2,
|
|
33
40
|
Skip: V.Skip,
|
|
34
41
|
findIndexSS: V.findIndexSS,
|
|
35
42
|
createRelativePositionFromTypeIndex: V.createRelativePositionFromTypeIndex,
|
package/dist/crdt/flow.d.ts
CHANGED
|
@@ -87,6 +87,8 @@ export type FlowContext = RangeDeleteContext & {
|
|
|
87
87
|
insideItem: (id: BlockId) => boolean;
|
|
88
88
|
tailOf: (id: BlockId) => SplitTail;
|
|
89
89
|
ranksFor: (parent: BlockId | null, index: number, count: number) => string[];
|
|
90
|
+
/** `ranksFor` for new blocks: after a block this client ranked, in its run there (H1). */
|
|
91
|
+
insertRanks: (parent: BlockId | null, index: number, count: number) => string[];
|
|
90
92
|
/** Ranks for the `count` blocks a split of `id` at `at` puts after it, by that offset. */
|
|
91
93
|
pieceRanks: (id: BlockId, at: number, count: number) => string[];
|
|
92
94
|
redata: (id: BlockId, data: Record<string, unknown>) => PlanStep[];
|
package/dist/crdt/flow.js
CHANGED
|
@@ -190,13 +190,13 @@ export const flowOps = (c) => ({
|
|
|
190
190
|
// Nothing before the position and the first line stands apart: the lines go before `B`,
|
|
191
191
|
// which keeps its text and takes a joining last line, or goes when empty and not needed.
|
|
192
192
|
if (!joinsHead && o === 0 && !inside) {
|
|
193
|
-
place(index, c.
|
|
193
|
+
place(index, c.insertRanks(parent, index, placed.length));
|
|
194
194
|
if (joinsTail) {
|
|
195
195
|
const at = join(last(), 0);
|
|
196
196
|
// A closed header's taken line's nested lines follow it, the caret ending them.
|
|
197
197
|
const after = shut ? specs(parent, last().children ?? []) : [];
|
|
198
198
|
if (after.length) {
|
|
199
|
-
const ranks = c.
|
|
199
|
+
const ranks = c.insertRanks(parent, index + 1, after.length);
|
|
200
200
|
const slot = index + placed.length + 1;
|
|
201
201
|
writes.push({ op: 'insertBlocks', parent, index: slot, specs: after, ranks });
|
|
202
202
|
}
|