edytor 0.1.0-next.21 → 0.1.0-next.22
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/collaboration/awarenessSelection.d.ts +7 -1
- package/dist/collaboration/awarenessSelection.js +7 -1
- package/dist/collaboration/index.d.ts +1 -1
- package/dist/collaboration/index.js +1 -1
- package/dist/components/Edytor.svelte +5 -2
- package/dist/components/Edytor.svelte.d.ts +1 -1
- package/dist/crdt/data.d.ts +3 -1
- package/dist/crdt/data.js +42 -32
- package/dist/crdt/document.d.ts +17 -8
- package/dist/crdt/document.js +35 -13
- package/dist/crdt/edytor-doc.d.ts +15 -3
- package/dist/crdt/edytor-doc.js +107 -14
- package/dist/crdt/flow.d.ts +2 -0
- package/dist/crdt/flow.js +2 -2
- package/dist/crdt/index.d.ts +6 -4
- package/dist/crdt/index.js +1 -1
- 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/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.js +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/plugins.d.ts +11 -0
- package/package.json +1 -1
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.`);
|
|
@@ -1071,10 +1075,11 @@ export const bindEdytorDoc = (Y) => {
|
|
|
1071
1075
|
node.setAttr(key, value);
|
|
1072
1076
|
}
|
|
1073
1077
|
};
|
|
1074
|
-
const createUndoManager = (
|
|
1078
|
+
const createUndoManager = (options = {}) => {
|
|
1075
1079
|
if (disposed) {
|
|
1076
1080
|
throw new EdytorDocDisposedError('createUndoManager');
|
|
1077
1081
|
}
|
|
1082
|
+
const { limit = DEFAULT_HISTORY_LIMIT, ...opts } = options;
|
|
1078
1083
|
write(() => {
|
|
1079
1084
|
if (!isInitialized(doc))
|
|
1080
1085
|
init(doc);
|
|
@@ -1102,6 +1107,43 @@ export const bindEdytorDoc = (Y) => {
|
|
|
1102
1107
|
};
|
|
1103
1108
|
um.on('stack-item-added', skipOwnText);
|
|
1104
1109
|
um.on('stack-item-updated', skipOwnText);
|
|
1110
|
+
// P6: the undo stack keeps its newest `limit` steps. A step that
|
|
1111
|
+
// falls off releases what it kept for its undo (its deleted items),
|
|
1112
|
+
// and the engine collects their content now, as it would have at
|
|
1113
|
+
// the delete without a history (the doc's `gcFilter` still decides:
|
|
1114
|
+
// a text copy another replica may have to copy again stays, P11).
|
|
1115
|
+
// An item is released only once every step that deleted part of it
|
|
1116
|
+
// fell off (the engine merges deleted items across steps): never
|
|
1117
|
+
// split, so the store keeps its merged items. Only the items
|
|
1118
|
+
// themselves are released: a kept container's flag may guard a
|
|
1119
|
+
// newer step's items inside it.
|
|
1120
|
+
const released = Y.createIdSet();
|
|
1121
|
+
const covered = (st) => {
|
|
1122
|
+
const end = st.id.clock + st.length;
|
|
1123
|
+
return (released.clients.get(st.id.client)?.getIds() ?? []).some((r) => r.clock <= st.id.clock && end <= r.clock + r.len);
|
|
1124
|
+
};
|
|
1125
|
+
const trim = ({ type }) => {
|
|
1126
|
+
const over = um.undoStack.length - limit;
|
|
1127
|
+
if (type !== 'undo' || !(over > 0))
|
|
1128
|
+
return;
|
|
1129
|
+
const dropped = um.undoStack.splice(0, over);
|
|
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
|
+
um.on('stack-item-added', trim);
|
|
1105
1147
|
// Lineage for undo/redo (O19, F4): the replay displaces the state
|
|
1106
1148
|
// every block the popped stack item touches, so each one's subtree
|
|
1107
1149
|
// is captured (`force`: lost whoever owns `l`) from the history
|
|
@@ -1226,7 +1268,7 @@ export const bindEdytorDoc = (Y) => {
|
|
|
1226
1268
|
const i = T.resolveAnchor(doc, text, anchor.a);
|
|
1227
1269
|
if (i === null)
|
|
1228
1270
|
return null;
|
|
1229
|
-
const s = own.
|
|
1271
|
+
const s = own.streamAt(anchor.b, i);
|
|
1230
1272
|
const owner = s === undefined ? DEAD : own.ownerOf(s.block);
|
|
1231
1273
|
if (typeof owner !== 'string' || !isLiveIn(view(), owner))
|
|
1232
1274
|
return null;
|
|
@@ -1270,11 +1312,30 @@ export const bindEdytorDoc = (Y) => {
|
|
|
1270
1312
|
});
|
|
1271
1313
|
/** This client's next clock: what its source ranks are tied by (`sourceRank`, DW-05). */
|
|
1272
1314
|
const clock = () => doc.store?.getClock(doc.clientID) ?? 0;
|
|
1273
|
-
/**
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1315
|
+
/**
|
|
1316
|
+
* `count` ranks at `index` among `parent`'s children, the moving
|
|
1317
|
+
* `exclude` left out. `run`: new blocks, which extend this client's run
|
|
1318
|
+
* after a block it ranked (H1, `order.insert.run`); a move never does
|
|
1319
|
+
* (the rank-growth guard).
|
|
1320
|
+
*/
|
|
1321
|
+
const ranksFor = (parent, index, count, exclude = [], run = false) => {
|
|
1322
|
+
const v = view();
|
|
1323
|
+
const all = v.kids.get(parent) ?? [];
|
|
1324
|
+
// The moving blocks' own slots left out: index → the list's index past them.
|
|
1325
|
+
const gone = exclude
|
|
1326
|
+
.map((id) => M.positionInView(v, id))
|
|
1327
|
+
.filter((p) => p !== null && p.parent === parent)
|
|
1328
|
+
.map((p) => p.index)
|
|
1329
|
+
.sort((a, b) => a - b);
|
|
1330
|
+
const raw = (i) => {
|
|
1331
|
+
for (const g of gone)
|
|
1332
|
+
if (g <= i)
|
|
1333
|
+
i++;
|
|
1334
|
+
return i;
|
|
1335
|
+
};
|
|
1336
|
+
const at = Math.max(0, Math.min(index, all.length - gone.length));
|
|
1337
|
+
const pair = [at > 0 ? all[raw(at - 1)] : undefined, all[raw(at)]];
|
|
1338
|
+
return M.ranksAt(pair, 1, count, doc.clientID, randOf(doc), run);
|
|
1278
1339
|
};
|
|
1279
1340
|
/**
|
|
1280
1341
|
* Move `ids` to `ranks` under `parent` (`index`: the slot hooks see).
|
|
@@ -1380,7 +1441,12 @@ export const bindEdytorDoc = (Y) => {
|
|
|
1380
1441
|
const dataSteps = (target, patches) => {
|
|
1381
1442
|
const [id, inlineId] = typeof target === 'object' && target ? [target.block, target.atom] : [target ?? undefined];
|
|
1382
1443
|
const node = dataNode(id, inlineId);
|
|
1383
|
-
|
|
1444
|
+
// A block kind's atomic paths are written as one leaf each (`data.atomic`, H8).
|
|
1445
|
+
const type = inlineId === undefined && id !== undefined ? blockTypeOf(id) : undefined;
|
|
1446
|
+
const atomic = (type === undefined ? undefined : roleOf(type)?.atomic) ?? [];
|
|
1447
|
+
const leaves = node
|
|
1448
|
+
? patchWrites(node, patches, doc.clientID, randOf(doc), atomic.map((p) => leafKey(typeof p === 'string' ? [p] : p)))
|
|
1449
|
+
: [];
|
|
1384
1450
|
if (leaves === null)
|
|
1385
1451
|
return null; // a patch fits no value there
|
|
1386
1452
|
if (leaves.length === 0)
|
|
@@ -1400,6 +1466,26 @@ export const bindEdytorDoc = (Y) => {
|
|
|
1400
1466
|
const replaceData = (data) => [
|
|
1401
1467
|
{ path: [], value: sanitizeWireJson(data) }
|
|
1402
1468
|
];
|
|
1469
|
+
/**
|
|
1470
|
+
* `data`'s leaves as sets over `current` (`data.retype.keep`, H4): a
|
|
1471
|
+
* plain object's keys, recursively where `current` holds an object
|
|
1472
|
+
* there too; anything else (an array, a primitive, an empty object, an
|
|
1473
|
+
* object where `current` holds none) as one value at its path. Nothing
|
|
1474
|
+
* else under the root is touched.
|
|
1475
|
+
*/
|
|
1476
|
+
const leafSets = (data, current) => {
|
|
1477
|
+
const out = [];
|
|
1478
|
+
const plainObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
1479
|
+
const walk = (v, at, path) => {
|
|
1480
|
+
if (plainObject(v) && Object.keys(v).length > 0 && (path.length === 0 || plainObject(at)))
|
|
1481
|
+
for (const [k, x] of Object.entries(v))
|
|
1482
|
+
walk(x, plainObject(at) ? at[k] : undefined, [...path, k]);
|
|
1483
|
+
else if (path.length > 0)
|
|
1484
|
+
out.push({ path, value: v });
|
|
1485
|
+
};
|
|
1486
|
+
walk(sanitizeWireJson(data), current, []);
|
|
1487
|
+
return out;
|
|
1488
|
+
};
|
|
1403
1489
|
/**
|
|
1404
1490
|
* The kind `kid` shows once it leaves `from` for a slot under `parent`
|
|
1405
1491
|
* — the island and container rules, one answer:
|
|
@@ -1602,7 +1688,7 @@ export const bindEdytorDoc = (Y) => {
|
|
|
1602
1688
|
return plan([], []);
|
|
1603
1689
|
if ((parent !== null && !live(parent)) || M.collides(doc, clean))
|
|
1604
1690
|
return REFUSED;
|
|
1605
|
-
const ranks = ranksFor(parent, dest.index, clean.length);
|
|
1691
|
+
const ranks = ranksFor(parent, dest.index, clean.length, [], true);
|
|
1606
1692
|
return plan(clean.map((s) => s.id), [{ op: 'insertBlocks', parent, index: dest.index, specs: clean, ranks }]);
|
|
1607
1693
|
};
|
|
1608
1694
|
/**
|
|
@@ -1700,7 +1786,7 @@ export const bindEdytorDoc = (Y) => {
|
|
|
1700
1786
|
const last = ids.at(-1);
|
|
1701
1787
|
const { parent, index } = positionOf(levels.at(-1) ?? last);
|
|
1702
1788
|
if (levels.length === 0) {
|
|
1703
|
-
const ranks = ranksFor(parent, index + 1, specs.length);
|
|
1789
|
+
const ranks = ranksFor(parent, index + 1, specs.length, [], true);
|
|
1704
1790
|
return plan(placed, [...insert(parent, index + 1, specs, ranks), ...retype]);
|
|
1705
1791
|
}
|
|
1706
1792
|
// Bottom up: what stays before the split at each level (the level
|
|
@@ -2265,8 +2351,11 @@ export const bindEdytorDoc = (Y) => {
|
|
|
2265
2351
|
return steps ? plan(t === null ? [] : [typeof t === 'string' ? t : t.block], steps) : REFUSED;
|
|
2266
2352
|
};
|
|
2267
2353
|
/**
|
|
2268
|
-
* Baseline `setBlock`: `type
|
|
2269
|
-
*
|
|
2354
|
+
* Baseline `setBlock`: `type` updates the block in place and `data`
|
|
2355
|
+
* sets the leaves it names, removing none (`data.retype.keep`, H4: a
|
|
2356
|
+
* retype keeps the block's properties, as Notion's Turn into does;
|
|
2357
|
+
* `setBlockData` replaces them); `content`/`children` REPLACE
|
|
2358
|
+
* wholesale — explicit replacement is a
|
|
2270
2359
|
* new-identity operation; a retype to a void kind without `children`
|
|
2271
2360
|
* unnests the current ones (`retypeSteps`). All-or-nothing (D-12): children
|
|
2272
2361
|
* for a block that is `void` after the write, or a replacement id that
|
|
@@ -2287,13 +2376,16 @@ export const bindEdytorDoc = (Y) => {
|
|
|
2287
2376
|
return REFUSED;
|
|
2288
2377
|
if (children !== undefined && M.collides(doc, children))
|
|
2289
2378
|
return refused('id-collision');
|
|
2379
|
+
const data = value.data === undefined ? [] : dataSteps(id, leafSets(value.data, blockDataOf(id)));
|
|
2380
|
+
if (data === null)
|
|
2381
|
+
return REFUSED;
|
|
2290
2382
|
const writes = [
|
|
2291
2383
|
...(type === undefined
|
|
2292
2384
|
? []
|
|
2293
2385
|
: children === undefined
|
|
2294
2386
|
? retypeSteps(id, type)
|
|
2295
2387
|
: attr(id, TYPE, type)),
|
|
2296
|
-
...
|
|
2388
|
+
...data
|
|
2297
2389
|
];
|
|
2298
2390
|
if (content !== undefined) {
|
|
2299
2391
|
const length = displayLength(id);
|
|
@@ -2451,6 +2543,7 @@ export const bindEdytorDoc = (Y) => {
|
|
|
2451
2543
|
insideItem: (id) => insideItem(id),
|
|
2452
2544
|
tailOf: (id) => ({ type: kindToCopy(id), data: blockDataOf(id) }),
|
|
2453
2545
|
ranksFor,
|
|
2546
|
+
insertRanks: (parent, index, count) => ranksFor(parent, index, count, [], true),
|
|
2454
2547
|
pieceRanks,
|
|
2455
2548
|
redata: (id, data) => dataSteps(id, replaceData(data)) ?? [],
|
|
2456
2549
|
deleteBlocks: (ids, filled) => deleteBlocks(ids, false, filled),
|
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
|
}
|
package/dist/crdt/index.d.ts
CHANGED
|
@@ -168,7 +168,7 @@ export declare const bindCrdt: (Y: EngineApi) => {
|
|
|
168
168
|
isSelfOrDescendant: (placements: Map<import("./index.js").BlockId, import("./placement/model.js").ResolvedPlacement>, own: import("./text/model.js").Ownership, id: import("./index.js").BlockId, maybeAncestor: import("./index.js").BlockId) => boolean;
|
|
169
169
|
ranksAt: (siblings: readonly {
|
|
170
170
|
rank: string;
|
|
171
|
-
}[], index: number, count: number, clientId: number, rand?: () => number) => string[];
|
|
171
|
+
}[], index: number, count: number, clientId: number, rand?: () => number, run?: boolean) => string[];
|
|
172
172
|
writePlacement: (doc: import("./engine-api.js").EngineDoc, node: import("./engine-api.js").EngineNode, p: import("./index.js").BlockId | null, r: string) => void;
|
|
173
173
|
withdrawOnUndo: (doc: import("./engine-api.js").EngineDoc) => (item: import("./engine-api.js").EngineItemRef, stackItem: {
|
|
174
174
|
inserts: {
|
|
@@ -277,7 +277,9 @@ export declare const bindCrdt: (Y: EngineApi) => {
|
|
|
277
277
|
checkSchema: () => import("./edytor-doc.js").SchemaProblem | null;
|
|
278
278
|
assertSchema: () => void;
|
|
279
279
|
dispose: () => void;
|
|
280
|
-
createUndoManager: (
|
|
280
|
+
createUndoManager: (options?: ConstructorParameters<EngineApi["UndoManager"]>[1] & {
|
|
281
|
+
limit?: number;
|
|
282
|
+
}) => import("./engine-api.js").YUndoManager;
|
|
281
283
|
project: () => import("./index.js").ProjectedDoc;
|
|
282
284
|
toJSON: () => import("./index.js").JSONDoc;
|
|
283
285
|
blockJSON: (id: string) => import("./index.js").JSONBlock;
|
|
@@ -360,7 +362,7 @@ export declare const bindCrdt: (Y: EngineApi) => {
|
|
|
360
362
|
isSelfOrDescendant: (placements: Map<import("./index.js").BlockId, import("./placement/model.js").ResolvedPlacement>, own: import("./text/model.js").Ownership, id: import("./index.js").BlockId, maybeAncestor: import("./index.js").BlockId) => boolean;
|
|
361
363
|
ranksAt: (siblings: readonly {
|
|
362
364
|
rank: string;
|
|
363
|
-
}[], index: number, count: number, clientId: number, rand?: () => number) => string[];
|
|
365
|
+
}[], index: number, count: number, clientId: number, rand?: () => number, run?: boolean) => string[];
|
|
364
366
|
writePlacement: (doc: import("./engine-api.js").EngineDoc, node: import("./engine-api.js").EngineNode, p: import("./index.js").BlockId | null, r: string) => void;
|
|
365
367
|
withdrawOnUndo: (doc: import("./engine-api.js").EngineDoc) => (item: import("./engine-api.js").EngineItemRef, stackItem: {
|
|
366
368
|
inserts: {
|
|
@@ -627,7 +629,7 @@ export declare const bindCrdt: (Y: EngineApi) => {
|
|
|
627
629
|
};
|
|
628
630
|
export type Crdt = ReturnType<typeof bindCrdt>;
|
|
629
631
|
export type { EngineApi, EngineDoc, EngineNode, EngineDeepEvent, EngineTransaction, EngineItemRef, YNode, YItem } from './engine-api.js';
|
|
630
|
-
export { SCHEMA_VERSION, META_KEY, EdytorDocDisposedError } from './edytor-doc.js';
|
|
632
|
+
export { SCHEMA_VERSION, META_KEY, EdytorDocDisposedError, DEFAULT_HISTORY_LIMIT } from './edytor-doc.js';
|
|
631
633
|
export { GENERATION, generationWord, frame, PROTOCOL_VERSION, GENERATION_RECORD, GenerationMismatchError, readProtocolVersion, type GenerationRecord } from './protocols/envelope.js';
|
|
632
634
|
export { messageSync, messageAwareness, messageAuth, messageQueryAwareness, messageSaved, messageChunk, MAX_FRAME_BYTES, chunkFrame, createChunkReader } from './providers/room.js';
|
|
633
635
|
export { messageYjsSyncStep1, messageYjsSyncStep2, messageYjsUpdate, type SyncProtocol } from './protocols/sync.js';
|
package/dist/crdt/index.js
CHANGED
|
@@ -155,7 +155,7 @@ export const bindCrdt = (Y) => {
|
|
|
155
155
|
// hibernation), the store-before-ack frame (`messageSaved`) and the bounded
|
|
156
156
|
// catch-up codec (`chunkFrame`/`createChunkReader`, `messageChunk`).
|
|
157
157
|
// `pnpm check:worker` keeps this graph Worker-safe.
|
|
158
|
-
export { SCHEMA_VERSION, META_KEY, EdytorDocDisposedError } from './edytor-doc.js';
|
|
158
|
+
export { SCHEMA_VERSION, META_KEY, EdytorDocDisposedError, DEFAULT_HISTORY_LIMIT } from './edytor-doc.js';
|
|
159
159
|
export { GENERATION, generationWord, frame, PROTOCOL_VERSION, GENERATION_RECORD, GenerationMismatchError, readProtocolVersion } from './protocols/envelope.js';
|
|
160
160
|
export { messageSync, messageAwareness, messageAuth, messageQueryAwareness, messageSaved, messageChunk, MAX_FRAME_BYTES, chunkFrame, createChunkReader } from './providers/room.js';
|
|
161
161
|
export { messageYjsSyncStep1, messageYjsSyncStep2, messageYjsUpdate } from './protocols/sync.js';
|
|
@@ -342,6 +342,14 @@ export declare const displayParentOf: (own: DisplayOwnership, pl: ResolvedPlacem
|
|
|
342
342
|
* promoted one ({@link displaySlotOf}).
|
|
343
343
|
*/
|
|
344
344
|
export declare const childrenIndex: (placements: Map<BlockId, ResolvedPlacement>, own: DisplayOwnership) => Map<BlockId | null, ChildSlot[]>;
|
|
345
|
+
/** The order of a children list: by rank, ties by id. */
|
|
346
|
+
export declare const bySlot: (a: {
|
|
347
|
+
id: BlockId;
|
|
348
|
+
rank: string;
|
|
349
|
+
}, b: {
|
|
350
|
+
id: BlockId;
|
|
351
|
+
rank: string;
|
|
352
|
+
}) => number;
|
|
345
353
|
/**
|
|
346
354
|
* Document order (O7): ONE pre-order over the visible blocks of a children
|
|
347
355
|
* index — `ids` in reading order, `at` the position of each id. Every
|
|
@@ -376,7 +384,7 @@ export declare const bindModel: (Y: EngineApi) => {
|
|
|
376
384
|
isSelfOrDescendant: (placements: Map<BlockId, ResolvedPlacement>, own: Ownership, id: BlockId, maybeAncestor: BlockId) => boolean;
|
|
377
385
|
ranksAt: (siblings: readonly {
|
|
378
386
|
rank: string;
|
|
379
|
-
}[], index: number, count: number, clientId: number, rand?: () => number) => string[];
|
|
387
|
+
}[], index: number, count: number, clientId: number, rand?: () => number, run?: boolean) => string[];
|
|
380
388
|
writePlacement: (doc: EngineDoc, node: EngineNode, p: BlockId | null, r: string) => void;
|
|
381
389
|
withdrawOnUndo: (doc: EngineDoc) => (item: EngineItemRef, stackItem: {
|
|
382
390
|
inserts: {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { decodeRank, encodeRank, rankBetween, RANK_VMIN } from './rank.js';
|
|
1
|
+
import { decodeRank, encodeRank, rankAfter, rankBetween, RANK_VMIN } from './rank.js';
|
|
2
2
|
import { AT, AT_NODE, BLOCK_NODE, CONTENT, CONTENT_NODE, DEL_PREFIX, hasDeleteMark, ID, CLAIMS, CLAIMS_NODE, INLINE_NODE, isNodeLike, NONCE, REGISTRY_KEY, TYPE, WITHDRAW_PREFIX } from '../schema.js';
|
|
3
3
|
import { nonceOf, randOf } from '../rand.js';
|
|
4
4
|
import { bindRuns } from '../text/runs.js';
|
|
@@ -409,11 +409,12 @@ export const childrenIndex = (placements, own) => {
|
|
|
409
409
|
else
|
|
410
410
|
index.set(parent, [slot]);
|
|
411
411
|
}
|
|
412
|
-
for (const bucket of index.values())
|
|
413
|
-
bucket.sort(
|
|
414
|
-
}
|
|
412
|
+
for (const bucket of index.values())
|
|
413
|
+
bucket.sort(bySlot);
|
|
415
414
|
return index;
|
|
416
415
|
};
|
|
416
|
+
/** The order of a children list: by rank, ties by id. */
|
|
417
|
+
export const bySlot = (a, b) => (a.rank === b.rank ? a.id.localeCompare(b.id) : a.rank < b.rank ? -1 : 1);
|
|
417
418
|
export const documentOrder = (kids) => {
|
|
418
419
|
const ids = [];
|
|
419
420
|
const at = new Map();
|
|
@@ -559,9 +560,10 @@ export const bindModel = (Y) => {
|
|
|
559
560
|
* so each new member takes `left` verbatim: it JOINS the tie and the
|
|
560
561
|
* `(rank, id)` display sort orders the whole group deterministically.
|
|
561
562
|
* On a valid seam the emitted chain is `rankBetween(left, right)` then
|
|
562
|
-
* `rankBetween(prev, right)
|
|
563
|
+
* `rankBetween(prev, right)` — with `run`, `rankAfter` (an insert: after a
|
|
564
|
+
* block this client ranked, in its run there, H1).
|
|
563
565
|
*/
|
|
564
|
-
const ranksAt = (siblings, index, count, clientId, rand) => {
|
|
566
|
+
const ranksAt = (siblings, index, count, clientId, rand, run = false) => {
|
|
565
567
|
let left = siblings[index - 1]?.rank;
|
|
566
568
|
const right = siblings[index]?.rank;
|
|
567
569
|
if (left !== undefined && right !== undefined && left >= right) {
|
|
@@ -569,7 +571,10 @@ export const bindModel = (Y) => {
|
|
|
569
571
|
}
|
|
570
572
|
const out = [];
|
|
571
573
|
for (let i = 0; i < count; i++) {
|
|
572
|
-
|
|
574
|
+
// An insert extends this client's run after a block it ranked (H1).
|
|
575
|
+
const r = run
|
|
576
|
+
? rankAfter(left, right, clientId, rand)
|
|
577
|
+
: rankBetween(left, right, clientId, rand);
|
|
573
578
|
out.push(r);
|
|
574
579
|
left = r;
|
|
575
580
|
}
|
|
@@ -749,12 +754,26 @@ export const bindModel = (Y) => {
|
|
|
749
754
|
* slot ({@link displaySlotOf}). Content is each block's display (R2).
|
|
750
755
|
*/
|
|
751
756
|
const project = (doc) => ({ children: R.attach(doc).project() });
|
|
752
|
-
/**
|
|
757
|
+
/**
|
|
758
|
+
* `positionOf` in a view: the display parent and the index among its
|
|
759
|
+
* visible children — a binary search by the block's display rank (the
|
|
760
|
+
* list's own order), not a scan of its siblings.
|
|
761
|
+
*/
|
|
753
762
|
const positionInView = (v, id) => {
|
|
754
763
|
if (!isLiveIn(v, id))
|
|
755
764
|
return null;
|
|
756
|
-
const
|
|
757
|
-
const
|
|
765
|
+
const slot = displaySlotOf(v.own, v.placements, v.placements.get(id), id);
|
|
766
|
+
const dp = slot.parent;
|
|
767
|
+
const list = v.kids.get(dp) ?? [];
|
|
768
|
+
let [lo, hi] = [0, list.length];
|
|
769
|
+
while (lo < hi) {
|
|
770
|
+
const mid = (lo + hi) >> 1;
|
|
771
|
+
if (bySlot(list[mid], { id, rank: slot.rank }) < 0)
|
|
772
|
+
lo = mid + 1;
|
|
773
|
+
else
|
|
774
|
+
hi = mid;
|
|
775
|
+
}
|
|
776
|
+
const index = list[lo]?.id === id ? lo : list.findIndex((s) => s.id === id);
|
|
758
777
|
return index < 0 ? null : { parent: dp, index };
|
|
759
778
|
};
|
|
760
779
|
/**
|
|
@@ -55,3 +55,23 @@ export declare const decodeRank: (rank: string) => RankSeg[];
|
|
|
55
55
|
/** Encode segments back to the persisted string form. */
|
|
56
56
|
export declare const encodeRank: (segs: readonly RankSeg[]) => string;
|
|
57
57
|
export declare const rankBetween: (left: string | undefined, right: string | undefined, clientId: number, rand?: () => number) => string;
|
|
58
|
+
/**
|
|
59
|
+
* The digit of the run marker data arrays wrote up to 0.1.0-next.21 (a run
|
|
60
|
+
* of that form is the marker segment, then ranks of the client's own): still
|
|
61
|
+
* read as a run, so what a client inserts there stays in it.
|
|
62
|
+
*/
|
|
63
|
+
export declare const RANK_RUN: number;
|
|
64
|
+
/**
|
|
65
|
+
* A rank in `(left, right)` for an insert right after `left` — one rule for
|
|
66
|
+
* array items (`crdt/data.ts`) and inserted blocks (H1, `order.insert.run`).
|
|
67
|
+
* When `client` made `left`, the rank is in the client's run after it: `left`
|
|
68
|
+
* itself as the prefix, then one segment of the run band — or, when `left`
|
|
69
|
+
* is already a member of the client's run, that run's prefix and a member
|
|
70
|
+
* between `left` and the next one. So what one client inserts after its own
|
|
71
|
+
* items stays together whatever a peer inserts in that gap meanwhile: a
|
|
72
|
+
* peer's rank there is either no extension of the run's prefix (it sorts
|
|
73
|
+
* after the whole run) or one whose next digit is above the band (YATA's
|
|
74
|
+
* origin rule, as `Y.Array` keeps an insert after its origin). Elsewhere, or
|
|
75
|
+
* when the run rank would not sort below `right`, a plain {@link rankBetween}.
|
|
76
|
+
*/
|
|
77
|
+
export declare const rankAfter: (left: string | undefined, right: string | undefined, client: number, rand?: () => number) => string;
|
|
@@ -207,3 +207,82 @@ export const rankBetween = (left, right, clientId, rand = Math.random) => {
|
|
|
207
207
|
path.push(lSeg);
|
|
208
208
|
}
|
|
209
209
|
};
|
|
210
|
+
/**
|
|
211
|
+
* The digit of the run marker data arrays wrote up to 0.1.0-next.21 (a run
|
|
212
|
+
* of that form is the marker segment, then ranks of the client's own): still
|
|
213
|
+
* read as a run, so what a client inserts there stays in it.
|
|
214
|
+
*/
|
|
215
|
+
export const RANK_RUN = -(2 ** 39);
|
|
216
|
+
/**
|
|
217
|
+
* A run member's digit lies in `(RANK_RUN, RANK_RUN + RUN_BAND)`: below every
|
|
218
|
+
* digit a plain rank extends a prefix with (`0`, an append's), far above the
|
|
219
|
+
* digit minimum. A run opens at {@link RUN_START}, the middle of the band, so
|
|
220
|
+
* its members can be inserted before and after one another.
|
|
221
|
+
*/
|
|
222
|
+
const RUN_BAND = 2 ** 38;
|
|
223
|
+
const RUN_START = RANK_RUN + 2 ** 37;
|
|
224
|
+
const inRun = (v) => v > RANK_RUN && v < RANK_RUN + RUN_BAND;
|
|
225
|
+
/**
|
|
226
|
+
* A rank in `(left, right)` for an insert right after `left` — one rule for
|
|
227
|
+
* array items (`crdt/data.ts`) and inserted blocks (H1, `order.insert.run`).
|
|
228
|
+
* When `client` made `left`, the rank is in the client's run after it: `left`
|
|
229
|
+
* itself as the prefix, then one segment of the run band — or, when `left`
|
|
230
|
+
* is already a member of the client's run, that run's prefix and a member
|
|
231
|
+
* between `left` and the next one. So what one client inserts after its own
|
|
232
|
+
* items stays together whatever a peer inserts in that gap meanwhile: a
|
|
233
|
+
* peer's rank there is either no extension of the run's prefix (it sorts
|
|
234
|
+
* after the whole run) or one whose next digit is above the band (YATA's
|
|
235
|
+
* origin rule, as `Y.Array` keeps an insert after its origin). Elsewhere, or
|
|
236
|
+
* when the run rank would not sort below `right`, a plain {@link rankBetween}.
|
|
237
|
+
*/
|
|
238
|
+
export const rankAfter = (left, right, client, rand = Math.random) => {
|
|
239
|
+
const plain = () => rankBetween(left, right, client, rand);
|
|
240
|
+
if (left === undefined)
|
|
241
|
+
return plain();
|
|
242
|
+
const segs = decodeRank(left);
|
|
243
|
+
const last = segs.at(-1);
|
|
244
|
+
const bottom = encodeRank([{ v: RANK_RUN, t: 0 }]);
|
|
245
|
+
const top = encodeRank([{ v: RANK_RUN + RUN_BAND, t: 0 }]);
|
|
246
|
+
/**
|
|
247
|
+
* `prefix` and a rank after `after` (`undefined`: the run opens) in the
|
|
248
|
+
* run: inside the band when `band`. Opening, it takes the band's middle,
|
|
249
|
+
* or a digit below the run's first member; at the run's end it appends as
|
|
250
|
+
* an open end does (a window, not half the band); else between the two.
|
|
251
|
+
*/
|
|
252
|
+
const own = (prefix, after, band) => {
|
|
253
|
+
const tail = (r) => r && r.length > prefix.length && r.startsWith(prefix) ? r.slice(prefix.length) : undefined;
|
|
254
|
+
let next = tail(right);
|
|
255
|
+
if (band && next !== undefined && next > top)
|
|
256
|
+
next = undefined;
|
|
257
|
+
let mine;
|
|
258
|
+
if (after === undefined) {
|
|
259
|
+
if (next === undefined)
|
|
260
|
+
mine = encodeRank([{ v: RUN_START, t: client }]);
|
|
261
|
+
else if (band && next > bottom)
|
|
262
|
+
mine = rankBetween(bottom, next, client, rand);
|
|
263
|
+
else
|
|
264
|
+
return plain();
|
|
265
|
+
}
|
|
266
|
+
else if (next === undefined && band) {
|
|
267
|
+
const append = rankBetween(after, undefined, client, rand);
|
|
268
|
+
mine = append < top ? append : rankBetween(after, top, client, rand);
|
|
269
|
+
}
|
|
270
|
+
else
|
|
271
|
+
mine = rankBetween(after, next, client, rand);
|
|
272
|
+
const rank = prefix + mine;
|
|
273
|
+
return right === undefined || rank < right ? rank : plain();
|
|
274
|
+
};
|
|
275
|
+
// A member of the client's run: the next member, inside the band.
|
|
276
|
+
if (last.t === client && inRun(last.v))
|
|
277
|
+
return own(encodeRank(segs.slice(0, -1)), encodeRank([last]), true);
|
|
278
|
+
// A run of the earlier form (the marker, then the client's ranks): stays in it.
|
|
279
|
+
const k = segs.findLastIndex((g) => g.v === RANK_RUN);
|
|
280
|
+
if (k >= 0 && segs[k].t === client) {
|
|
281
|
+
const prefix = encodeRank(segs.slice(0, k + 1));
|
|
282
|
+
return own(prefix, left.length > prefix.length ? left.slice(prefix.length) : undefined, false);
|
|
283
|
+
}
|
|
284
|
+
// A rank the client made: its run opens right after it.
|
|
285
|
+
if (last.t === client)
|
|
286
|
+
return own(left, undefined, true);
|
|
287
|
+
return plain();
|
|
288
|
+
};
|
package/dist/crdt/structs.d.ts
CHANGED
|
@@ -25,7 +25,11 @@ export type StoreStruct = {
|
|
|
25
25
|
/** `null` for sequence items; an attr name for map-entry items. */
|
|
26
26
|
parentSub: string | null;
|
|
27
27
|
parent: unknown;
|
|
28
|
+
left?: StoreStruct | null;
|
|
28
29
|
right?: StoreStruct | null;
|
|
30
|
+
content?: {
|
|
31
|
+
arr?: unknown[];
|
|
32
|
+
};
|
|
29
33
|
countable?: boolean;
|
|
30
34
|
keep?: boolean;
|
|
31
35
|
redone?: {
|
|
@@ -75,6 +79,12 @@ export declare const structAt: (Y: EngineApi, structs: readonly StoreStruct[], c
|
|
|
75
79
|
* partial walk. `clientsOf` throws first when the store itself is absent.
|
|
76
80
|
*/
|
|
77
81
|
export declare const walkIdSetStructs: (Y: EngineApi, doc: EngineDoc, idSet: IdSetLike, fn: (struct: StoreStruct) => boolean | void) => boolean;
|
|
82
|
+
/**
|
|
83
|
+
* {@link walkIdSetStructs}, clipped: `fn(struct, from, to)` gets the clock
|
|
84
|
+
* range `[from, to)` of the struct the id set covers (a struct can reach past
|
|
85
|
+
* a range a diff of two id sets cut).
|
|
86
|
+
*/
|
|
87
|
+
export declare const walkIdSetRanges: (Y: EngineApi, doc: EngineDoc, idSet: IdSetLike, fn: (struct: StoreStruct, from: number, to: number) => void) => boolean;
|
|
78
88
|
/**
|
|
79
89
|
* The id the local `redone` chain leads `id` to: the copy an undo or redo
|
|
80
90
|
* on THIS replica re-created of a deleted item (redo copies chain), or `id`
|
|
@@ -104,3 +114,10 @@ export declare const nextClock: (doc: EngineDoc, client: number) => number;
|
|
|
104
114
|
* undefined until the missing update arrives.
|
|
105
115
|
*/
|
|
106
116
|
export declare const holdsPending: (doc: EngineDoc) => boolean;
|
|
117
|
+
/**
|
|
118
|
+
* The transactions the engine has opened and not finished cleaning up
|
|
119
|
+
* (vendor-internal `doc._transactionCleanups`), in order: the open one, and
|
|
120
|
+
* those started during an earlier one's cleanup, whose bodies already ran
|
|
121
|
+
* and whose observers have not.
|
|
122
|
+
*/
|
|
123
|
+
export declare const queuedTransactions: (doc: EngineDoc) => readonly unknown[];
|
package/dist/crdt/structs.js
CHANGED
|
@@ -75,6 +75,40 @@ export const walkIdSetStructs = (Y, doc, idSet, fn) => {
|
|
|
75
75
|
});
|
|
76
76
|
return complete;
|
|
77
77
|
};
|
|
78
|
+
/**
|
|
79
|
+
* {@link walkIdSetStructs}, clipped: `fn(struct, from, to)` gets the clock
|
|
80
|
+
* range `[from, to)` of the struct the id set covers (a struct can reach past
|
|
81
|
+
* a range a diff of two id sets cut).
|
|
82
|
+
*/
|
|
83
|
+
export const walkIdSetRanges = (Y, doc, idSet, fn) => {
|
|
84
|
+
const clients = clientsOf(doc);
|
|
85
|
+
let complete = true;
|
|
86
|
+
idSet.clients.forEach((ranges, client) => {
|
|
87
|
+
const structs = clients.get(client);
|
|
88
|
+
if (structs === undefined) {
|
|
89
|
+
complete = false;
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
for (const r of ranges.getIds()) {
|
|
93
|
+
const end = r.clock + r.len;
|
|
94
|
+
let i;
|
|
95
|
+
try {
|
|
96
|
+
i = Y.findIndexSS(structs, r.clock);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
complete = false;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
for (; i < structs.length; i++) {
|
|
103
|
+
const s = structs[i];
|
|
104
|
+
if (s.id.clock >= end)
|
|
105
|
+
break;
|
|
106
|
+
fn(s, Math.max(r.clock, s.id.clock), Math.min(end, s.id.clock + s.length));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
return complete;
|
|
111
|
+
};
|
|
78
112
|
/**
|
|
79
113
|
* The id the local `redone` chain leads `id` to: the copy an undo or redo
|
|
80
114
|
* on THIS replica re-created of a deleted item (redo copies chain), or `id`
|
|
@@ -117,3 +151,10 @@ export const holdsPending = (doc) => {
|
|
|
117
151
|
const store = doc.store;
|
|
118
152
|
return (store?.pendingStructs ?? null) !== null || (store?.pendingDs ?? null) !== null;
|
|
119
153
|
};
|
|
154
|
+
/**
|
|
155
|
+
* The transactions the engine has opened and not finished cleaning up
|
|
156
|
+
* (vendor-internal `doc._transactionCleanups`), in order: the open one, and
|
|
157
|
+
* those started during an earlier one's cleanup, whose bodies already ran
|
|
158
|
+
* and whose observers have not.
|
|
159
|
+
*/
|
|
160
|
+
export const queuedTransactions = (doc) => doc._transactionCleanups ?? [];
|
|
@@ -195,9 +195,15 @@ export type Stream = {
|
|
|
195
195
|
};
|
|
196
196
|
/** Scan `text` (live space, renderer-free) for its boundary items. */
|
|
197
197
|
export declare const scanText: (home: BlockId, text: EngineNode) => TextRow;
|
|
198
|
+
export declare const byId: (a: string, b: string) => number;
|
|
198
199
|
/** Each block's delimiting boundary: the lowest-id live boundary whose nonce matches its record. */
|
|
199
200
|
export declare const delimiters: (blocks: ReadonlyMap<BlockId, Pick<TextBlockRec, "n">>, rows: Iterable<TextRow>) => Map<BlockId, string>;
|
|
200
|
-
/**
|
|
201
|
+
/**
|
|
202
|
+
* The streams of one text, in text order: the home's head (unless delimited
|
|
203
|
+
* elsewhere), then one per delimiting boundary — one sweep over the
|
|
204
|
+
* boundaries (each non-delimiting one is an inert boundary of the stream it
|
|
205
|
+
* sits in).
|
|
206
|
+
*/
|
|
201
207
|
export declare const placeText: (row: TextRow, delim: ReadonlyMap<BlockId, string>) => Stream[];
|
|
202
208
|
/** A displayed piece: `[i0, i1)` of `text` (home `t`) holds content only; `block` owns its stream. */
|
|
203
209
|
export type Seg = {
|
|
@@ -221,6 +227,8 @@ export type Ownership = {
|
|
|
221
227
|
streamOf: (b: BlockId) => Stream | undefined;
|
|
222
228
|
/** The streams of `home`'s text, in text order. */
|
|
223
229
|
streamsIn: (home: BlockId) => readonly Stream[];
|
|
230
|
+
/** The stream of `home`'s text whose `[start, end]` holds live index `i` (the first, at a seam). */
|
|
231
|
+
streamAt: (home: BlockId, i: number) => Stream | undefined;
|
|
224
232
|
/** `display(b)` as boundary-free pieces in reading order; `null` when hidden or dead. */
|
|
225
233
|
display: (b: BlockId) => Seg[] | null;
|
|
226
234
|
};
|