edytor 0.1.0-next.20 → 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/columns/resize.svelte.js +6 -1
- package/dist/plugins.d.ts +11 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
Edytor aims to be for Svelte what Slate.js is for React: a heavily customizable editor with an API to build any kind of collaborative rich text editor.
|
|
17
17
|
|
|
18
|
-
> **Work in progress.** Edytor is a pre-release (`0.1.0-next.
|
|
18
|
+
> **Work in progress.** Edytor is a pre-release (`0.1.0-next.22`, `edytor@next` on npm) and not ready for production; the API changes between releases without a compatibility layer. The untagged `edytor@0.0.11` on npm predates the current API. Issues and PRs are welcome: when you report a bug, include the document's JSON value.
|
|
19
19
|
|
|
20
20
|
## Features
|
|
21
21
|
|
|
@@ -68,10 +68,16 @@ export declare const mintPresenceKey: () => string;
|
|
|
68
68
|
export declare const publishPresence: (awareness: AwarenessLike, key: string, payload: PresenceSelection | null) => boolean;
|
|
69
69
|
/** What a view shares of its selection: the selection itself, the block holding its focus, or nothing. */
|
|
70
70
|
export type PresenceShare = 'caret' | 'block' | 'none';
|
|
71
|
+
/**
|
|
72
|
+
* The least ms between two presence writes of a view by default (P9): a
|
|
73
|
+
* caret held down or a drag publishes about 20 writes a second instead of
|
|
74
|
+
* one per selection change, which every peer and the room relay.
|
|
75
|
+
*/
|
|
76
|
+
export declare const DEFAULT_PRESENCE_THROTTLE = 50;
|
|
71
77
|
export type PresenceOptions = {
|
|
72
78
|
/** `'caret'` (default): the selection; `'block'`: the focused block only, so moving inside it publishes nothing; `'none'`: nothing. */
|
|
73
79
|
share?: PresenceShare;
|
|
74
|
-
/** Minimum ms between two presence writes (default `0
|
|
80
|
+
/** Minimum ms between two presence writes (default {@link DEFAULT_PRESENCE_THROTTLE}, 50; `0`: every change). The first goes at once; later ones collapse into one write of the newest at the window's end. */
|
|
75
81
|
throttle?: number;
|
|
76
82
|
};
|
|
77
83
|
/**
|
|
@@ -28,6 +28,12 @@ export const publishPresence = (awareness, key, payload) => {
|
|
|
28
28
|
awareness.setLocalState(Object.keys(next).length > 0 ? { ...state, selections: next } : state);
|
|
29
29
|
return true;
|
|
30
30
|
};
|
|
31
|
+
/**
|
|
32
|
+
* The least ms between two presence writes of a view by default (P9): a
|
|
33
|
+
* caret held down or a drag publishes about 20 writes a second instead of
|
|
34
|
+
* one per selection change, which every peer and the room relay.
|
|
35
|
+
*/
|
|
36
|
+
export const DEFAULT_PRESENCE_THROTTLE = 50;
|
|
31
37
|
/**
|
|
32
38
|
* One view's presence writer (R1): the only writer of its key, under the
|
|
33
39
|
* view's `share` and `throttle`. `select()` hands it the full payload; the
|
|
@@ -44,7 +50,7 @@ export class PresenceWriter {
|
|
|
44
50
|
#held;
|
|
45
51
|
/** The last payload and focused block handed in, kept so a share change republishes at once. */
|
|
46
52
|
#latest = [null, null];
|
|
47
|
-
constructor(awareness, key, { share = 'caret', throttle =
|
|
53
|
+
constructor(awareness, key, { share = 'caret', throttle = DEFAULT_PRESENCE_THROTTLE } = {}) {
|
|
48
54
|
this.awareness = awareness;
|
|
49
55
|
this.key = key;
|
|
50
56
|
this.#share = share;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { freshestPublishedSelection, publishPresence, PresenceWriter, type PresenceOptions, type PresenceShare, type EdytorAwarenessSelection, type EdytorAwarenessState, type EdytorAwarenessUser, type EdytorAwarenessViewSelection } from './awarenessSelection.js';
|
|
1
|
+
export { freshestPublishedSelection, publishPresence, PresenceWriter, DEFAULT_PRESENCE_THROTTLE, type PresenceOptions, type PresenceShare, type EdytorAwarenessSelection, type EdytorAwarenessState, type EdytorAwarenessUser, type EdytorAwarenessViewSelection } from './awarenessSelection.js';
|
|
2
2
|
export { whenDocumentReady } from './documentSync.js';
|
|
3
3
|
export { createIndexeddbSync, createWebsocketSync, clearDocument, IndexeddbPersistence, storeState, WebsocketProvider, type EdytorSync, type EdytorSyncCleanup, type EdytorSyncPayload, type IndexeddbSyncOptions, type WebsocketSync, type WebsocketSyncOptions } from './providers.js';
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { freshestPublishedSelection, publishPresence, PresenceWriter } from './awarenessSelection.js';
|
|
1
|
+
export { freshestPublishedSelection, publishPresence, PresenceWriter, DEFAULT_PRESENCE_THROTTLE } from './awarenessSelection.js';
|
|
2
2
|
export { whenDocumentReady } from './documentSync.js';
|
|
3
3
|
export { createIndexeddbSync, createWebsocketSync, clearDocument, IndexeddbPersistence, storeState, WebsocketProvider } from './providers.js';
|
|
@@ -12,7 +12,10 @@
|
|
|
12
12
|
} from '../crdt/index.js';
|
|
13
13
|
import { CLOSE, validRoomId } from '../crdt/providers/room.js';
|
|
14
14
|
import type { EdytorSync, WebsocketSyncOptions } from '../collaboration/index.js';
|
|
15
|
-
import
|
|
15
|
+
import {
|
|
16
|
+
DEFAULT_PRESENCE_THROTTLE,
|
|
17
|
+
type PresenceOptions
|
|
18
|
+
} from '../collaboration/awarenessSelection.js';
|
|
16
19
|
import { createIndexeddbSync, createWebsocketSync } from '../collaboration/providers.js';
|
|
17
20
|
export { EdytorClass as EdytorContext, useEdytor };
|
|
18
21
|
import type { Placeholder, Plugin } from '../plugins.js';
|
|
@@ -273,7 +276,7 @@
|
|
|
273
276
|
edytor.readonly = readonly;
|
|
274
277
|
});
|
|
275
278
|
$effect(() => {
|
|
276
|
-
edytor.presence.throttle = presence?.throttle ??
|
|
279
|
+
edytor.presence.throttle = presence?.throttle ?? DEFAULT_PRESENCE_THROTTLE;
|
|
277
280
|
edytor.presence.share = presence?.share ?? 'caret';
|
|
278
281
|
});
|
|
279
282
|
// A readonly change shows or hides chrome: the overlay repositions it.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Edytor as EdytorClass, useEdytor, type Snippets } from '../edytor.svelte.js';
|
|
2
2
|
import { SyncRefusedError, type Awareness, type DocumentActor, type EdytorDocument, type YDoc } from '../crdt/index.js';
|
|
3
3
|
import type { EdytorSync, WebsocketSyncOptions } from '../collaboration/index.js';
|
|
4
|
-
import type
|
|
4
|
+
import { type PresenceOptions } from '../collaboration/awarenessSelection.js';
|
|
5
5
|
export { EdytorClass as EdytorContext, useEdytor };
|
|
6
6
|
import type { Placeholder, Plugin } from '../plugins.js';
|
|
7
7
|
import { type BlockHandlesOptions } from '../plugins/blockHandles/blockHandlesPlugin.js';
|
package/dist/crdt/data.d.ts
CHANGED
|
@@ -57,6 +57,8 @@ export declare const keySegment: (k: string) => string;
|
|
|
57
57
|
export declare const segmentKey: (s: string) => string;
|
|
58
58
|
/** A new node's data, as leaves. */
|
|
59
59
|
export declare const dataLeaves: (data: unknown) => Map<string, unknown>;
|
|
60
|
+
/** A path of the data as a leaf key (`d/` and its pointer). */
|
|
61
|
+
export declare const leafKey: (path: readonly string[]) => string;
|
|
60
62
|
/** `node`'s data (`undefined` when it has none). */
|
|
61
63
|
export declare const readData: (node: EngineNode) => JsonObj | undefined;
|
|
62
64
|
/**
|
|
@@ -75,7 +77,7 @@ export declare const applyPatch: (data: JsonObj, patches: readonly DataPatch[])
|
|
|
75
77
|
* they still read as the patched value, else it is exploded: every leaf
|
|
76
78
|
* written, the attr deleted. An unchanged value writes nothing.
|
|
77
79
|
*/
|
|
78
|
-
export declare const patchWrites: (node: EngineNode, patches: readonly DataPatch[], client?: number, rand?: () => number) => LeafWrite[] | null;
|
|
80
|
+
export declare const patchWrites: (node: EngineNode, patches: readonly DataPatch[], client?: number, rand?: () => number, atomic?: readonly string[]) => LeafWrite[] | null;
|
|
79
81
|
/** Write `writes` on `node` (a detached one too: a block or atom being created). */
|
|
80
82
|
export declare const writeLeaves: (node: EngineNode, writes: Iterable<LeafWrite>) => void;
|
|
81
83
|
export {};
|
package/dist/crdt/data.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DATA, DATA_LEAF_PREFIX } from './schema.js';
|
|
2
|
-
import {
|
|
2
|
+
import { rankAfter } from './placement/rank.js';
|
|
3
3
|
import { hash32 } from './rand.js';
|
|
4
4
|
import { jsonEquals } from '../utils/json.js';
|
|
5
5
|
export const isObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
@@ -17,13 +17,6 @@ export const keySegment = (k) => (k.startsWith('~') ? `~0${k.slice(1)}` : k);
|
|
|
17
17
|
export const segmentKey = (s) => (s.startsWith('~0') ? `~${s.slice(2)}` : s);
|
|
18
18
|
const RANK = /^(?:[-\w]{16})+$/;
|
|
19
19
|
const index = (k) => (/^(0|[1-9]\d*)$/.test(k) ? Number(k) : -1);
|
|
20
|
-
/**
|
|
21
|
-
* The digit that opens a client's run after an item it made (`between`):
|
|
22
|
-
* below every digit a plain insert extends a rank with (`0`, a gap's), so
|
|
23
|
-
* the run sorts right after its item, and far above the digit minimum, so
|
|
24
|
-
* the inserts before it (each one digit lower) never run out of room.
|
|
25
|
-
*/
|
|
26
|
-
const RUN = -(2 ** 39);
|
|
27
20
|
const rank = (v) => (typeof v === 'string' && RANK.test(v) ? v : undefined);
|
|
28
21
|
/** Item `id`'s place: where a move put it (`id>`), else where it was made (`id#`). */
|
|
29
22
|
const placeOf = (t, id) => rank(t.k.get(`${id}>`)?.v) ?? rank(t.k.get(`${id}#`)?.v);
|
|
@@ -103,28 +96,11 @@ const edits = (rand, client = 0) => {
|
|
|
103
96
|
const remove = (t, id) => [id, `${id}#`, `${id}>`].forEach((s) => t.k.delete(s));
|
|
104
97
|
/**
|
|
105
98
|
* A rank in `(left, right)`. After an item this client placed, it is in
|
|
106
|
-
* the client's run there (`
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
* meanwhile. Elsewhere a plain rank between the two.
|
|
99
|
+
* the client's run there (`rankAfter`): what it inserts after its own
|
|
100
|
+
* items stays together, as `Y.Array` keeps an insert after its origin,
|
|
101
|
+
* whatever a peer inserts in that gap meanwhile. Elsewhere a plain rank.
|
|
110
102
|
*/
|
|
111
|
-
const between = (left, right) =>
|
|
112
|
-
const plain = () => rankBetween(left, right, client, pick);
|
|
113
|
-
if (left === undefined)
|
|
114
|
-
return plain();
|
|
115
|
-
const segs = decodeRank(left);
|
|
116
|
-
const k = segs.findLastIndex((g) => g.v === RUN);
|
|
117
|
-
const run = k >= 0 && segs[k].t === client
|
|
118
|
-
? encodeRank(segs.slice(0, k + 1))
|
|
119
|
-
: segs.at(-1).t === client
|
|
120
|
-
? left + encodeRank([{ v: RUN, t: client }])
|
|
121
|
-
: undefined;
|
|
122
|
-
if (run === undefined)
|
|
123
|
-
return plain();
|
|
124
|
-
const tail = (r) => r && r.length > run.length && r.startsWith(run) ? r.slice(run.length) : undefined;
|
|
125
|
-
const rank = run + rankBetween(tail(left), tail(right), client, pick);
|
|
126
|
-
return right === undefined || rank < right ? rank : plain();
|
|
127
|
-
};
|
|
103
|
+
const between = (left, right) => rankAfter(left, right, client, pick);
|
|
128
104
|
/** Give `entries` (kept items, moved items, new values) their places in `t`, in order. */
|
|
129
105
|
const lay = (t, entries) => {
|
|
130
106
|
let left;
|
|
@@ -314,9 +290,35 @@ const effective = (base, leaves) => {
|
|
|
314
290
|
under(`${key}/`, v);
|
|
315
291
|
out.set(key, []);
|
|
316
292
|
}
|
|
293
|
+
else if (isObject(v) && Object.keys(v).length) {
|
|
294
|
+
// An object written as one leaf (an atomic path, `data.atomic`) reads as its keys.
|
|
295
|
+
under(`${key}/`, v);
|
|
296
|
+
out.set(key, {});
|
|
297
|
+
}
|
|
317
298
|
}
|
|
318
299
|
return out;
|
|
319
300
|
};
|
|
301
|
+
/** A path of the data as a leaf key (`d/` and its pointer). */
|
|
302
|
+
export const leafKey = (path) => DATA_LEAF_PREFIX + path.map((k) => enc(k)).join('/');
|
|
303
|
+
/**
|
|
304
|
+
* `leaves` with every atomic path (`data.atomic`, H8: a leaf key a kind
|
|
305
|
+
* declares) holding one leaf: the value of everything at and under it.
|
|
306
|
+
*/
|
|
307
|
+
const collapse = (leaves, atomic) => {
|
|
308
|
+
if (atomic.length === 0)
|
|
309
|
+
return leaves;
|
|
310
|
+
const root = treeOf(leaves);
|
|
311
|
+
for (const key of atomic) {
|
|
312
|
+
let t = root;
|
|
313
|
+
for (const s of key.slice(DATA_LEAF_PREFIX.length).split('/'))
|
|
314
|
+
t = t?.k.get(s);
|
|
315
|
+
if (t === undefined || t.k.size === 0)
|
|
316
|
+
continue;
|
|
317
|
+
t.v = valueOf(t);
|
|
318
|
+
t.k.clear();
|
|
319
|
+
}
|
|
320
|
+
return leavesFrom(root);
|
|
321
|
+
};
|
|
320
322
|
const read = (base, leaves) => valueOf(treeOf(effective(base, leaves)));
|
|
321
323
|
/** `node`'s data (`undefined` when it has none). */
|
|
322
324
|
export const readData = (node) => read(node.getAttr(DATA), leavesOf(node));
|
|
@@ -351,7 +353,7 @@ export const applyPatch = (data, patches) => {
|
|
|
351
353
|
* they still read as the patched value, else it is exploded: every leaf
|
|
352
354
|
* written, the attr deleted. An unchanged value writes nothing.
|
|
353
355
|
*/
|
|
354
|
-
export const patchWrites = (node, patches, client = 0, rand) => {
|
|
356
|
+
export const patchWrites = (node, patches, client = 0, rand, atomic = []) => {
|
|
355
357
|
const [base, now] = [node.getAttr(DATA), leavesOf(node)];
|
|
356
358
|
const was = effective(base, now);
|
|
357
359
|
const root = treeOf(was);
|
|
@@ -359,13 +361,21 @@ export const patchWrites = (node, patches, client = 0, rand) => {
|
|
|
359
361
|
const { apply } = edits(rand, client);
|
|
360
362
|
if (!patches.every((p) => apply(root, p)))
|
|
361
363
|
return null;
|
|
362
|
-
const
|
|
364
|
+
const value = valueOf(root);
|
|
363
365
|
if (jsonEquals(before, value))
|
|
364
366
|
return [];
|
|
367
|
+
// An atomic path is written as one leaf, and every leaf stored under it goes (H8).
|
|
368
|
+
const want = collapse(leavesFrom(root), atomic);
|
|
369
|
+
const under = (key) => atomic.some((a) => key.startsWith(`${a}/`));
|
|
365
370
|
const diff = (from) => [...new Set([...from.keys(), ...want.keys()])]
|
|
366
371
|
.filter((key) => !jsonEquals(from.get(key), want.get(key)))
|
|
367
372
|
.map((key) => [key, want.get(key)]);
|
|
368
|
-
const own =
|
|
373
|
+
const own = [
|
|
374
|
+
...diff(collapse(was, atomic)),
|
|
375
|
+
...[...now.keys()]
|
|
376
|
+
.filter((key) => under(key) && !want.has(key))
|
|
377
|
+
.map((key) => [key, undefined])
|
|
378
|
+
];
|
|
369
379
|
const after = new Map(now);
|
|
370
380
|
for (const [key, value] of own)
|
|
371
381
|
value === undefined ? after.delete(key) : after.set(key, value);
|
package/dist/crdt/document.d.ts
CHANGED
|
@@ -52,11 +52,14 @@ export type DocumentOptions = {
|
|
|
52
52
|
* History tuning — applied when the document's undo manager attaches.
|
|
53
53
|
* `captureTimeout` is the merge window in ms (engine default 500):
|
|
54
54
|
* commits closer than this fuse into one undo step; `0` makes every
|
|
55
|
-
* commit its own step.
|
|
56
|
-
* {@link
|
|
55
|
+
* commit its own step. `limit` is the number of undo steps kept (default
|
|
56
|
+
* {@link DEFAULT_HISTORY_LIMIT}, 200; `Infinity` keeps every step): an
|
|
57
|
+
* older step is dropped and the deleted content it kept is collected.
|
|
58
|
+
* {@link EdytorDocument.clearHistory} drops both stacks.
|
|
57
59
|
*/
|
|
58
60
|
history?: {
|
|
59
61
|
captureTimeout?: number;
|
|
62
|
+
limit?: number;
|
|
60
63
|
};
|
|
61
64
|
/**
|
|
62
65
|
* Opt-in per-block lineage ring — `depth > 0` captures a subtree
|
|
@@ -158,6 +161,7 @@ export type EdytorDocumentInit = {
|
|
|
158
161
|
semantics?: DocumentSemanticsConfig;
|
|
159
162
|
history?: {
|
|
160
163
|
captureTimeout?: number;
|
|
164
|
+
limit?: number;
|
|
161
165
|
};
|
|
162
166
|
lineage?: {
|
|
163
167
|
depth?: number;
|
|
@@ -299,6 +303,12 @@ export declare class EdytorDocument {
|
|
|
299
303
|
* @internal
|
|
300
304
|
*/
|
|
301
305
|
get historyCaptureTimeout(): number | undefined;
|
|
306
|
+
/**
|
|
307
|
+
* The undo steps this document's history keeps (P6): the configured
|
|
308
|
+
* `history.limit`, else {@link DEFAULT_HISTORY_LIMIT}. The `attachDocument`
|
|
309
|
+
* dedupe check reads it (a reattach naming another limit is a conflict).
|
|
310
|
+
*/
|
|
311
|
+
get historyLimit(): number;
|
|
302
312
|
/**
|
|
303
313
|
* The lineage ring depth this document was configured with — read by
|
|
304
314
|
* the `attachDocument` dedupe compatibility check (a reattach naming
|
|
@@ -406,12 +416,11 @@ export declare class EdytorDocument {
|
|
|
406
416
|
untrackOrigin: (origin: unknown) => void;
|
|
407
417
|
private _attachHistory;
|
|
408
418
|
/**
|
|
409
|
-
*
|
|
410
|
-
*
|
|
411
|
-
*
|
|
412
|
-
*
|
|
413
|
-
*
|
|
414
|
-
* nothing to clear — it does NOT force attach or throw on pending).
|
|
419
|
+
* Drop the undo/redo stacks. The undo stack already keeps only its
|
|
420
|
+
* newest `history.limit` steps (P6); call this where undo no longer makes
|
|
421
|
+
* sense, e.g. after a save checkpoint. No-op while history has not
|
|
422
|
+
* attached (there is nothing to clear — it does NOT force attach or throw
|
|
423
|
+
* on pending).
|
|
415
424
|
*/
|
|
416
425
|
clearHistory: () => void;
|
|
417
426
|
/** Local document transaction — headless edits, one undo step per call. */
|
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
|
});
|
|
@@ -418,6 +429,14 @@ export class EdytorDocument {
|
|
|
418
429
|
get historyCaptureTimeout() {
|
|
419
430
|
return this._historyOptions?.captureTimeout;
|
|
420
431
|
}
|
|
432
|
+
/**
|
|
433
|
+
* The undo steps this document's history keeps (P6): the configured
|
|
434
|
+
* `history.limit`, else {@link DEFAULT_HISTORY_LIMIT}. The `attachDocument`
|
|
435
|
+
* dedupe check reads it (a reattach naming another limit is a conflict).
|
|
436
|
+
*/
|
|
437
|
+
get historyLimit() {
|
|
438
|
+
return this._historyOptions?.limit ?? DEFAULT_HISTORY_LIMIT;
|
|
439
|
+
}
|
|
421
440
|
/**
|
|
422
441
|
* The lineage ring depth this document was configured with — read by
|
|
423
442
|
* the `attachDocument` dedupe compatibility check (a reattach naming
|
|
@@ -657,17 +676,17 @@ export class EdytorDocument {
|
|
|
657
676
|
this._history = this.facade.createUndoManager({
|
|
658
677
|
trackedOrigins: this._trackedOrigins,
|
|
659
678
|
captureTimeout: this._historyOptions?.captureTimeout,
|
|
679
|
+
limit: this.historyLimit,
|
|
660
680
|
captureTransaction: (transaction) => transaction.local !== false
|
|
661
681
|
});
|
|
662
682
|
return this._history;
|
|
663
683
|
};
|
|
664
684
|
/**
|
|
665
|
-
*
|
|
666
|
-
*
|
|
667
|
-
*
|
|
668
|
-
*
|
|
669
|
-
*
|
|
670
|
-
* nothing to clear — it does NOT force attach or throw on pending).
|
|
685
|
+
* Drop the undo/redo stacks. The undo stack already keeps only its
|
|
686
|
+
* newest `history.limit` steps (P6); call this where undo no longer makes
|
|
687
|
+
* sense, e.g. after a save checkpoint. No-op while history has not
|
|
688
|
+
* attached (there is nothing to clear — it does NOT force attach or throw
|
|
689
|
+
* on pending).
|
|
671
690
|
*/
|
|
672
691
|
clearHistory = () => {
|
|
673
692
|
if (this._destroyed) {
|
|
@@ -1017,6 +1036,9 @@ export const bindDocument = (Y) => {
|
|
|
1017
1036
|
options.history.captureTimeout !== existing.historyCaptureTimeout) {
|
|
1018
1037
|
throw new SemanticConflictError('history.captureTimeout', existing.historyCaptureTimeout, options.history.captureTimeout);
|
|
1019
1038
|
}
|
|
1039
|
+
if (options.history?.limit !== undefined && options.history.limit !== existing.historyLimit) {
|
|
1040
|
+
throw new SemanticConflictError('history.limit', existing.historyLimit, options.history.limit);
|
|
1041
|
+
}
|
|
1020
1042
|
if (options.lineage?.depth !== undefined && options.lineage.depth !== existing.lineageDepth) {
|
|
1021
1043
|
throw new SemanticConflictError('lineage.depth', existing.lineageDepth, options.lineage.depth);
|
|
1022
1044
|
}
|
|
@@ -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,9 @@ 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;
|
|
621
633
|
project: () => import("./index.js").ProjectedDoc;
|
|
622
634
|
toJSON: () => JSONDoc;
|
|
623
635
|
blockJSON: (id: string) => JSONBlock;
|
|
@@ -712,7 +724,7 @@ export declare const bindEdytorDoc: (Y: EngineApi) => {
|
|
|
712
724
|
isSelfOrDescendant: (placements: Map<BlockId, import("./placement/model.js").ResolvedPlacement>, own: import("./text/model.js").Ownership, id: BlockId, maybeAncestor: BlockId) => boolean;
|
|
713
725
|
ranksAt: (siblings: readonly {
|
|
714
726
|
rank: string;
|
|
715
|
-
}[], index: number, count: number, clientId: number, rand?: () => number) => string[];
|
|
727
|
+
}[], index: number, count: number, clientId: number, rand?: () => number, run?: boolean) => string[];
|
|
716
728
|
writePlacement: (doc: EngineDoc, node: EngineNode, p: BlockId | null, r: string) => void;
|
|
717
729
|
withdrawOnUndo: (doc: EngineDoc) => (item: import("./engine-api.js").EngineItemRef, stackItem: {
|
|
718
730
|
inserts: {
|