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 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.20`, `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.
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`). The first goes at once; later ones collapse into one write of the newest at the window's end. */
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 = 0 } = {}) {
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 type { PresenceOptions } from '../collaboration/awarenessSelection.js';
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 ?? 0;
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 { PresenceOptions } from '../collaboration/awarenessSelection.js';
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';
@@ -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 { decodeRank, encodeRank, rankBetween } from './placement/rank.js';
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 (`left`, the `RUN` segment, then ranks of its
107
- * own): what it inserts after its own items stays together, as `Y.Array`
108
- * keeps an insert after its origin, whatever a peer inserts in that gap
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 [want, value] = [leavesFrom(root), valueOf(root)];
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 = diff(was);
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);
@@ -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. Stacks are unbounded regardless — see
56
- * {@link EdytorDocument.clearHistory} for the pruning surface.
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
- * Prune the undo/redo stacks — the document's retention surface. The
410
- * engine keeps unbounded stacks: nothing is pruned implicitly (the
411
- * `captureTimeout` merge window only fuses adjacent commits into one
412
- * step). Call this to bound memory on long-lived documents, e.g. after
413
- * a save checkpoint. No-op while history has not attached (there is
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. */
@@ -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
- void: role?.void === true,
204
- island: role?.island === true,
205
- lines: role?.lines === true,
206
- ...(role?.layout === true && { layout: true })
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
- * Prune the undo/redo stacks — the document's retention surface. The
666
- * engine keeps unbounded stacks: nothing is pruned implicitly (the
667
- * `captureTimeout` merge window only fuses adjacent commits into one
668
- * step). Call this to bound memory on long-lived documents, e.g. after
669
- * a save checkpoint. No-op while history has not attached (there is
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: (opts?: ConstructorParameters<EngineApi["UndoManager"]>[1]) => YUndoManager;
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: {