edytor 0.1.0-next.40 → 0.1.0-next.41

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
@@ -16,7 +16,7 @@
16
16
 
17
17
  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.
18
18
 
19
- > **Work in progress.** Edytor is a pre-release (`0.1.0-next.40`, `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
+ > **Work in progress.** Edytor is a pre-release (`0.1.0-next.41`, `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.
20
20
 
21
21
  ## Features
22
22
 
@@ -1,6 +1,6 @@
1
1
  import { Text } from '../text/text.svelte.js';
2
2
  import type { Edytor } from '../edytor.svelte.js';
3
- import type { JSONBlock, JSONText, JSONInlineBlock } from '../utils/json.js';
3
+ import type { JSONBlock, JSONInlineBlock } from '../utils/json.js';
4
4
  import { type BlockOperations } from './block.utils.js';
5
5
  import type { BlockDefinition } from '../plugins.js';
6
6
  import { InlineBlock } from './inlineBlock.svelte.js';
@@ -186,16 +186,4 @@ export declare class Block {
186
186
  text: Text;
187
187
  offset: number;
188
188
  } | null;
189
- /**
190
- * Insert children at `index`: a JSON spec is created (its id kept, minted
191
- * when missing); a block handle moves there with its identity.
192
- */
193
- insertChildren: (index: number, blocks: (JSONBlock | Block)[]) => void;
194
- /** Delete `length` visible children from `index`, each with its subtree (a document delete). */
195
- deleteChildren: (index: number, length?: number) => void;
196
- /**
197
- * Insert content before part `index` (a text segment or an inline atom):
198
- * each entry is a text's runs or an atom, as JSON.
199
- */
200
- insertParts: (index: number, parts: (JSONText[] | JSONInlineBlock)[]) => void;
201
189
  }
@@ -355,9 +355,14 @@ export class Block {
355
355
  // ── JSON insertion (specs are data; `new Block({block})` is gone) ──
356
356
  /**
357
357
  * Insert children at `index`: a JSON spec is created (its id kept, minted
358
- * when missing); a block handle moves there with its identity.
358
+ * when missing); a block handle moves there with its identity. A raw
359
+ * write for the editor's own commands: nothing on a view whose dispatcher
360
+ * refuses writes (readonly, a read-only document).
361
+ * @internal
359
362
  */
360
363
  insertChildren = (index, blocks) => {
364
+ if (!this.edytor.dispatcher.permits())
365
+ return;
361
366
  const { facade } = this.edytor;
362
367
  const parent = this.isRoot ? null : this.id;
363
368
  blocks.forEach((block, k) => {
@@ -367,17 +372,26 @@ export class Block {
367
372
  facade.insertBlock({ parent, index: index + k }, jsonBlockToSpec(block));
368
373
  });
369
374
  };
370
- /** Delete `length` visible children from `index`, each with its subtree (a document delete). */
375
+ /**
376
+ * Delete `length` visible children from `index`, each with its subtree (a
377
+ * document delete). Raw, as `insertChildren`.
378
+ * @internal
379
+ */
371
380
  deleteChildren = (index, length = 1) => {
381
+ if (!this.edytor.dispatcher.permits())
382
+ return;
372
383
  const ids = this.edytor.facade.childrenIds(this.isRoot ? null : this.id);
373
384
  for (const id of ids.slice(index, index + length).reverse())
374
385
  this.edytor.facade.block(id).delete({ keepChildren: false });
375
386
  };
376
387
  /**
377
388
  * Insert content before part `index` (a text segment or an inline atom):
378
- * each entry is a text's runs or an atom, as JSON.
389
+ * each entry is a text's runs or an atom, as JSON. Raw, as `insertChildren`.
390
+ * @internal
379
391
  */
380
392
  insertParts = (index, parts) => {
393
+ if (!this.edytor.dispatcher.permits())
394
+ return;
381
395
  const model = this.model;
382
396
  if (!model)
383
397
  return;
@@ -28,7 +28,7 @@ recorded patch and document it here.
28
28
 
29
29
  The v13 provider stack (`y-protocols`, `y-websocket`, `y-indexeddb`, `lib0@0.2`)
30
30
  is no longer a dependency of any kind: `tests/packed-consumer/smoke.js` and
31
- `src/tests/crdt/gate2/public-boundary.test.ts` refuse it.
31
+ `src/tests/crdt/boundary-attacks/public-boundary.test.ts` refuse it.
32
32
 
33
33
  ## Layout
34
34
 
@@ -56,7 +56,7 @@ the run's artifact, says in the summary whether the installed `lib0-v14`
56
56
  satisfies the newer engine's `lib0` range (a mismatch is a run warning: a
57
57
  red check may then be the older lib0, not the fork), then materializes the newer engine as `bench/vendor-baseline/yjs`
58
58
  so `bench/lib/interop.mjs` (the fork and the newer engine syncing over the
59
- wire) and the baseline leg of `src/tests/crdt/hardening/r1-p4-format.test.ts`
59
+ wire) and the baseline leg of `src/tests/crdt/hardening/search-marker-format.test.ts`
60
60
  (byte-identical stores after every operation) run against it. A red run
61
61
  means the re-sync needs care; the re-sync itself is the manual recipe under
62
62
  YP8. Locally: `node scripts/upstream-check.mjs`, then those two checks, then
@@ -204,7 +204,7 @@ added path inert = upstream behavior) and requires **byte-identical**
204
204
  `encodeStateAsUpdate` output plus identical rendered deltas — covering
205
205
  distant inserts, formatted retains, format-marker inserts, deletes, undo/
206
206
  redo, remote apply, GC, nested modify ops, merge/split cycles.
207
- `src/tests/crdt/hardening/r1-p4-format.test.ts` pins the same-key boundary
207
+ `src/tests/crdt/hardening/search-marker-format.test.ts` pins the same-key boundary
208
208
  rule (the R1 review's YP4 corruption), runs randomized + structured
209
209
  differential replays (patched vs disabled vs materialized pre-P4 baseline)
210
210
  with per-op `toDelta` + store-byte equality across insert/delete/format/
@@ -291,7 +291,7 @@ purity (no updates/splits/undo/renderer changes; marker perturbation
291
291
  invisible to mutations), traversal bounds, and the
292
292
  `a[b]b[b=null]c` read-seed-vs-mutation-seed counterexample.
293
293
  `src/tests/crdt/runs/range-reads.test.ts` +
294
- `gateF2/wu8-range-reads.test.ts` cover the Edytor projection: parity vs
294
+ `text-model-probes/range-read-hazards.test.ts` cover the Edytor projection: parity vs
295
295
  the pre-WU8 `toDelta().toJSON()` oracle at every range, marker pinning
296
296
  (`index` = anchor's left edge, `formats` = fold at that edge), the
297
297
  marks-alias boundary (corruption is confined to one cursor — marker
@@ -486,10 +486,10 @@ Correctness oracle:
486
486
  every top-level vendored declaration outside the allowlist above.
487
487
  - `bench/lib/mk-baseline.sh` now materializes the PRISTINE upstream engine
488
488
  (`@y/y@14.0.0-rc.26`, installed as the `@y/protocols` peer, + YP1), and the
489
- baseline leg of `src/tests/crdt/hardening/r1-p4-format.test.ts` (patched vs
489
+ baseline leg of `src/tests/crdt/hardening/search-marker-format.test.ts` (patched vs
490
490
  upstream, per-op `toDelta` + byte-identical `encodeStateAsUpdate`) and
491
491
  `bench/lib/interop.mjs` pass against it; the YP4/YP7 differentials
492
- (`r1-p4-format`, `marker-seed`, `yp7-gap-end`) stay green.
492
+ (`search-marker-format`, `marker-seed`, `yp7-gap-end`) stay green.
493
493
 
494
494
  Re-syncing with upstream: take the new upstream `src/`, apply YP1, re-apply
495
495
  the YP4/YP5/YP7 hunks (diff this tree against the pinned upstream as in
@@ -507,7 +507,7 @@ triggers `readUpdateV2`'s retry, and the document holds a pending update it
507
507
  could integrate — forever, until some unrelated update from the recorded
508
508
  client arrives. Reproduced on the unmodified `@y/y@14.0.0-rc.26` with four
509
509
  updates (A1 ← B1 ← A3, A2 independent) delivered as A3, B1, A1, A2; found by
510
- the architecture-v2 phase-2 fuzz (`src/tests/crdt/arch-v2/p1-fuzz.test.ts`, an
510
+ the architecture-v2 phase-2 fuzz (`src/tests/crdt/arch-v2/replica-fuzz.test.ts`, an
511
511
  observer fed every update in reverse order stayed pending in 74/1,500 seeds).
512
512
 
513
513
  Patch (all hunks marked `// YP9` or delimited by `// YP9 begin` / `// YP9 end`):
@@ -648,7 +648,7 @@ fork adds paired marks (Peritext / Loro anchor pairing), schema generation 5:
648
648
  Plain formats (every upstream test) are untouched: the class of a plain
649
649
  item is content's, so the comparator reduces to upstream's.
650
650
 
651
- Oracle: `src/tests/crdt/arch-v2/h5-marks.test.ts` (the Peritext cases on
651
+ Oracle: `src/tests/crdt/arch-v2/paired-marks.test.ts` (the Peritext cases on
652
652
  every swept client-id pair and delivery order; 7 of its 17 rows fail on
653
653
  the tree without YP13), the upstream suite unchanged.
654
654
 
@@ -687,7 +687,7 @@ key (`deleteAttr`) delete their subtree as before. `YNode#applyDelta`
687
687
  still writes nothing to a deleted node, so the kept node's own attrs are
688
688
  frozen; its children (a text, a list, a map) are ordinary live types.
689
689
 
690
- Oracle: `src/tests/crdt/phase5/h13-same-id.test.ts` (the losing text shows
690
+ Oracle: `src/tests/crdt/placement/same-id-concurrent.test.ts` (the losing text shows
691
691
  on every replica and after a reload; two fuzzes), the upstream suite
692
692
  unchanged.
693
693
 
@@ -39,12 +39,6 @@ export declare class Text {
39
39
  * the caret is a render the projector displays after (`mounted`).
40
40
  */
41
41
  refreshFromModel: () => void;
42
- /** Insert `text` (optionally marked) at segment-local `offset`. */
43
- insertAt: (offset: number, text: string, marks?: Record<string, unknown> | null) => boolean;
44
- /** Delete `length` atoms at segment-local `offset`. */
45
- deleteAt: (offset: number, length: number) => boolean;
46
- /** Multi-mark format over `[offset, offset+length)` — `null` values remove the mark. */
47
- formatAt: (offset: number, length: number, attributes: Record<string, unknown>) => boolean;
48
42
  private batch;
49
43
  getMarksAtRange: (yStart: number, yEnd: number) => JSONText[];
50
44
  insertText: (args_0: {
@@ -87,12 +87,18 @@ export class Text {
87
87
  // are SEGMENT-LOCAL display atoms (UTF-16 units); `segStart` maps them into
88
88
  // the block's display space (read from the index, so a write after another
89
89
  // in the same transaction lands right).
90
- /** Insert `text` (optionally marked) at segment-local `offset`. */
91
- insertAt = (offset, text, marks) => accepted(this.parent.model?.insertText(this.segStart + offset, text, marks ?? undefined));
92
- /** Delete `length` atoms at segment-local `offset`. */
93
- deleteAt = (offset, length) => accepted(this.parent.model?.deleteText(this.segStart + offset, length));
94
- /** Multi-mark format over `[offset, offset+length)` — `null` values remove the mark. */
95
- formatAt = (offset, length, attributes) => accepted(this.parent.model?.format(this.segStart + offset, length, attributes));
90
+ // They write raw (no hooks, no undo policy of their own): the commands
91
+ // that call them passed the dispatcher's admission, and a call a readonly
92
+ // view or a read-only document would refuse writes nothing.
93
+ /** Insert `text` (optionally marked) at segment-local `offset`. @internal */
94
+ insertAt = (offset, text, marks) => this.edytor.dispatcher.permits() &&
95
+ accepted(this.parent.model?.insertText(this.segStart + offset, text, marks ?? undefined));
96
+ /** Delete `length` atoms at segment-local `offset`. @internal */
97
+ deleteAt = (offset, length) => this.edytor.dispatcher.permits() &&
98
+ accepted(this.parent.model?.deleteText(this.segStart + offset, length));
99
+ /** Multi-mark format over `[offset, offset+length)` — `null` values remove the mark. @internal */
100
+ formatAt = (offset, length, attributes) => this.edytor.dispatcher.permits() &&
101
+ accepted(this.parent.model?.format(this.segStart + offset, length, attributes));
96
102
  batch = batch.bind(this);
97
103
  getMarksAtRange = getMarksAtRange.bind(this);
98
104
  insertText = this.batch('insertText', insertText.bind(this));
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "edytor",
3
3
  "license": "MIT",
4
- "version": "0.1.0-next.40",
4
+ "version": "0.1.0-next.41",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/beynar/edytor.git"