From 42d3820917d44372af6119c3fdb9fd4d024dd48f Mon Sep 17 00:00:00 2001 From: streamich Date: Fri, 19 Apr 2024 12:55:46 +0200 Subject: [PATCH 01/22] =?UTF-8?q?feat(json-crdt-extensions):=20?= =?UTF-8?q?=F0=9F=8E=B8=20improve=20Slice=20and=20PersistedSlice=20interfa?= =?UTF-8?q?ces?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../peritext/slice/PersistedSlice.ts | 27 +++++++++++++------ .../peritext/slice/Slices.ts | 9 ++++--- .../peritext/slice/types.ts | 27 +++++++++++++++++-- 3 files changed, 49 insertions(+), 14 deletions(-) diff --git a/src/json-crdt-extensions/peritext/slice/PersistedSlice.ts b/src/json-crdt-extensions/peritext/slice/PersistedSlice.ts index 0bab0ea42b..07eaa3e32a 100644 --- a/src/json-crdt-extensions/peritext/slice/PersistedSlice.ts +++ b/src/json-crdt-extensions/peritext/slice/PersistedSlice.ts @@ -10,23 +10,25 @@ import type {Peritext} from '../Peritext'; import type {SliceDto, SliceType, Stateful} from '../types'; import type {Printable} from '../../../util/print/types'; import type {JsonNode, VecNode} from '../../../json-crdt/nodes'; +import type {AbstractRga} from '../../../json-crdt/nodes/rga'; -export class PersistedSlice extends Range implements Slice, Printable, Stateful { - public readonly id: ITimestampStruct; - +export class PersistedSlice extends Range implements Slice, Printable, Stateful { constructor( protected readonly txt: Peritext, + protected readonly rga: AbstractRga, protected readonly chunk: ArrChunk, public readonly tuple: VecNode, - public behavior: SliceBehavior, + behavior: SliceBehavior, /** @todo Rename to x1? */ - public start: Point, + public start: Point, /** @todo Rename to x2? */ - public end: Point, - public type: SliceType, + public end: Point, + type: SliceType, ) { - super(txt.str, start, end); + super(rga, start, end); this.id = this.chunk.id; + this.behavior = behavior; + this.type = type; } protected tagNode(): JsonNode | undefined { @@ -34,6 +36,12 @@ export class PersistedSlice extends Range implements Slice, Printable, Stateful return this.tuple.get(3); } + // -------------------------------------------------------------------- Slice + + public readonly id: ITimestampStruct; + public behavior: SliceBehavior; + public type: SliceType; + public data(): unknown | undefined { return this.tuple.get(4)?.view(); } @@ -58,6 +66,9 @@ export class PersistedSlice extends Range implements Slice, Printable, Stateful public refresh(): number { const hash = hashNode(this.tuple); const changed = hash !== this.hash; + + // TODO: Add .refresh() to Range. + this.hash = hash; if (changed) { const tuple = this.tuple; diff --git a/src/json-crdt-extensions/peritext/slice/Slices.ts b/src/json-crdt-extensions/peritext/slice/Slices.ts index 67b80a4386..5311095499 100644 --- a/src/json-crdt-extensions/peritext/slice/Slices.ts +++ b/src/json-crdt-extensions/peritext/slice/Slices.ts @@ -52,10 +52,11 @@ export class Slices implements Stateful, Printable { const tuple = model.index.get(tupleId) as VecNode; const chunk = set.findById(chunkId)!; // TODO: Need to check if split slice text was deleted + const txt = this.txt; const slice = behavior === SliceBehavior.Split - ? new SplitSlice(this.txt, chunk, tuple, behavior, start, end, type) - : new PersistedSlice(this.txt, chunk, tuple, behavior, start, end, type); + ? new SplitSlice(txt, txt.str, chunk, tuple, behavior, start, end, type) + : new PersistedSlice(txt, txt.str, chunk, tuple, behavior, start, end, type); this.list.set(chunk, slice); return slice; } @@ -80,8 +81,8 @@ export class Slices implements Stateful, Printable { const type = tuple.get(3)!.view() as SliceType; const slice = behavior === SliceBehavior.Split - ? new SplitSlice(this.txt, chunk, tuple, behavior, p1, p2, type) - : new PersistedSlice(this.txt, chunk, tuple, behavior, p1, p2, type); + ? new SplitSlice(txt, txt.str, chunk, tuple, behavior, p1, p2, type) + : new PersistedSlice(txt, txt.str, chunk, tuple, behavior, p1, p2, type); return slice; } diff --git a/src/json-crdt-extensions/peritext/slice/types.ts b/src/json-crdt-extensions/peritext/slice/types.ts index 04ef95a9e9..1bbc912d43 100644 --- a/src/json-crdt-extensions/peritext/slice/types.ts +++ b/src/json-crdt-extensions/peritext/slice/types.ts @@ -3,11 +3,34 @@ import type {SliceType, Stateful} from '../types'; import type {ITimestampStruct} from '../../../json-crdt-patch/clock'; import type {SliceBehavior} from '../constants'; -export interface Slice extends Range, Stateful { - /** ID used for layer sorting. */ +export interface Slice extends Range, Stateful { + /** + * ID of the slice. ID is used for layer sorting. + */ id: ITimestampStruct; + + /** + * The low-level behavior of the slice. Specifies whether the slice is a split, + * i.e. a "marker" for a block split, in which case it represents a single + * place in the text where text is split into blocks. Otherwise, specifies + * the low-level behavior or the rich-text formatting of the slice. + */ behavior: SliceBehavior; + + /** + * The high-level behavior of the slice. Specifies the user-defined type of the + * slice, e.g. paragraph, heading, blockquote, etc. + */ type: SliceType; + + /** + * High-level user-defined metadata of the slice, which accompanies the slice + * type. + */ data(): unknown; + + /** + * Whether the slice is deleted. + */ del(): boolean; } From 4d553173155248d3eaee41f49fe587305f84e33d Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Fri, 19 Apr 2024 13:10:28 +0200 Subject: [PATCH 02/22] =?UTF-8?q?refactor(json-crdt-extensions):=20?= =?UTF-8?q?=F0=9F=92=A1=20move=20Point=20to=20/rga=20folder?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/json-crdt-extensions/peritext/Peritext.ts | 2 +- src/json-crdt-extensions/peritext/editor/Editor.ts | 2 +- src/json-crdt-extensions/peritext/{point => rga}/Point.ts | 0 .../peritext/{point => rga}/__tests__/Point.spec.ts | 0 src/json-crdt-extensions/peritext/slice/Cursor.ts | 2 +- src/json-crdt-extensions/peritext/slice/PersistedSlice.ts | 2 +- src/json-crdt-extensions/peritext/slice/Range.ts | 2 +- src/json-crdt-extensions/peritext/slice/Slices.ts | 2 +- 8 files changed, 6 insertions(+), 6 deletions(-) rename src/json-crdt-extensions/peritext/{point => rga}/Point.ts (100%) rename src/json-crdt-extensions/peritext/{point => rga}/__tests__/Point.spec.ts (100%) diff --git a/src/json-crdt-extensions/peritext/Peritext.ts b/src/json-crdt-extensions/peritext/Peritext.ts index d9fb75cbe3..e5c7fb07cb 100644 --- a/src/json-crdt-extensions/peritext/Peritext.ts +++ b/src/json-crdt-extensions/peritext/Peritext.ts @@ -1,5 +1,5 @@ import {Anchor, SliceBehavior} from './constants'; -import {Point} from './point/Point'; +import {Point} from './rga/Point'; import {Range} from './slice/Range'; import {Editor} from './editor/Editor'; import {printTree} from '../../util/print/printTree'; diff --git a/src/json-crdt-extensions/peritext/editor/Editor.ts b/src/json-crdt-extensions/peritext/editor/Editor.ts index 058b92264c..8a88a6c293 100644 --- a/src/json-crdt-extensions/peritext/editor/Editor.ts +++ b/src/json-crdt-extensions/peritext/editor/Editor.ts @@ -5,7 +5,7 @@ import {PersistedSlice} from '../slice/PersistedSlice'; import type {Range} from '../slice/Range'; import type {Peritext} from '../Peritext'; import type {Printable} from '../../../util/print/types'; -import type {Point} from '../point/Point'; +import type {Point} from '../rga/Point'; import type {SliceType} from '../types'; export class Editor implements Printable { diff --git a/src/json-crdt-extensions/peritext/point/Point.ts b/src/json-crdt-extensions/peritext/rga/Point.ts similarity index 100% rename from src/json-crdt-extensions/peritext/point/Point.ts rename to src/json-crdt-extensions/peritext/rga/Point.ts diff --git a/src/json-crdt-extensions/peritext/point/__tests__/Point.spec.ts b/src/json-crdt-extensions/peritext/rga/__tests__/Point.spec.ts similarity index 100% rename from src/json-crdt-extensions/peritext/point/__tests__/Point.spec.ts rename to src/json-crdt-extensions/peritext/rga/__tests__/Point.spec.ts diff --git a/src/json-crdt-extensions/peritext/slice/Cursor.ts b/src/json-crdt-extensions/peritext/slice/Cursor.ts index 483b47f1ea..6302c03bc5 100644 --- a/src/json-crdt-extensions/peritext/slice/Cursor.ts +++ b/src/json-crdt-extensions/peritext/slice/Cursor.ts @@ -1,4 +1,4 @@ -import {Point} from '../point/Point'; +import {Point} from '../rga/Point'; import {Anchor, SliceBehavior, Tags} from '../constants'; import {Range} from './Range'; import {printTree} from '../../../util/print/printTree'; diff --git a/src/json-crdt-extensions/peritext/slice/PersistedSlice.ts b/src/json-crdt-extensions/peritext/slice/PersistedSlice.ts index 07eaa3e32a..30fcb307de 100644 --- a/src/json-crdt-extensions/peritext/slice/PersistedSlice.ts +++ b/src/json-crdt-extensions/peritext/slice/PersistedSlice.ts @@ -1,4 +1,4 @@ -import {Point} from '../point/Point'; +import {Point} from '../rga/Point'; import {Range} from './Range'; import {hashNode} from '../../../json-crdt/hash'; import {printTree} from '../../../util/print/printTree'; diff --git a/src/json-crdt-extensions/peritext/slice/Range.ts b/src/json-crdt-extensions/peritext/slice/Range.ts index 41f985bc6c..f9cc176db3 100644 --- a/src/json-crdt-extensions/peritext/slice/Range.ts +++ b/src/json-crdt-extensions/peritext/slice/Range.ts @@ -1,4 +1,4 @@ -import {Point} from '../point/Point'; +import {Point} from '../rga/Point'; import {Anchor} from '../constants'; import type {ITimestampStruct} from '../../../json-crdt-patch/clock'; import type {Printable} from '../../../util/print/types'; diff --git a/src/json-crdt-extensions/peritext/slice/Slices.ts b/src/json-crdt-extensions/peritext/slice/Slices.ts index 5311095499..07ef3e7be5 100644 --- a/src/json-crdt-extensions/peritext/slice/Slices.ts +++ b/src/json-crdt-extensions/peritext/slice/Slices.ts @@ -6,7 +6,7 @@ import {CONST, updateNum} from '../../../json-hash'; import {printTree} from '../../../util/print/printTree'; import {Anchor, SliceBehavior, SliceHeaderMask, SliceHeaderShift} from '../constants'; import {SplitSlice} from './SplitSlice'; -import {Point} from '../point/Point'; +import {Point} from '../rga/Point'; import {Slice} from './types'; import {VecNode} from '../../../json-crdt/nodes'; import type {SliceDto, SliceType, Stateful} from '../types'; From c2bde731e65b494e8cb693be61f0d1c5cd71c02a Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Fri, 19 Apr 2024 13:12:06 +0200 Subject: [PATCH 03/22] =?UTF-8?q?refactor(json-crdt-extensions):=20?= =?UTF-8?q?=F0=9F=92=A1=20move=20Range=20to=20/rga=20folder?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/json-crdt-extensions/peritext/Peritext.ts | 2 +- src/json-crdt-extensions/peritext/editor/Editor.ts | 2 +- src/json-crdt-extensions/peritext/{slice => rga}/Range.ts | 2 +- .../peritext/{slice => rga}/__tests__/Range.spec.ts | 0 src/json-crdt-extensions/peritext/slice/Cursor.ts | 2 +- src/json-crdt-extensions/peritext/slice/PersistedSlice.ts | 2 +- src/json-crdt-extensions/peritext/slice/Slices.ts | 2 +- src/json-crdt-extensions/peritext/slice/types.ts | 2 +- 8 files changed, 7 insertions(+), 7 deletions(-) rename src/json-crdt-extensions/peritext/{slice => rga}/Range.ts (99%) rename src/json-crdt-extensions/peritext/{slice => rga}/__tests__/Range.spec.ts (100%) diff --git a/src/json-crdt-extensions/peritext/Peritext.ts b/src/json-crdt-extensions/peritext/Peritext.ts index e5c7fb07cb..cfffaf6b8a 100644 --- a/src/json-crdt-extensions/peritext/Peritext.ts +++ b/src/json-crdt-extensions/peritext/Peritext.ts @@ -1,6 +1,6 @@ import {Anchor, SliceBehavior} from './constants'; import {Point} from './rga/Point'; -import {Range} from './slice/Range'; +import {Range} from './rga/Range'; import {Editor} from './editor/Editor'; import {printTree} from '../../util/print/printTree'; import {ArrNode, StrNode} from '../../json-crdt/nodes'; diff --git a/src/json-crdt-extensions/peritext/editor/Editor.ts b/src/json-crdt-extensions/peritext/editor/Editor.ts index 8a88a6c293..3c8cc2c14e 100644 --- a/src/json-crdt-extensions/peritext/editor/Editor.ts +++ b/src/json-crdt-extensions/peritext/editor/Editor.ts @@ -2,7 +2,7 @@ import {Cursor} from '../slice/Cursor'; import {Anchor, SliceBehavior} from '../constants'; import {tick, type ITimestampStruct} from '../../../json-crdt-patch/clock'; import {PersistedSlice} from '../slice/PersistedSlice'; -import type {Range} from '../slice/Range'; +import type {Range} from '../rga/Range'; import type {Peritext} from '../Peritext'; import type {Printable} from '../../../util/print/types'; import type {Point} from '../rga/Point'; diff --git a/src/json-crdt-extensions/peritext/slice/Range.ts b/src/json-crdt-extensions/peritext/rga/Range.ts similarity index 99% rename from src/json-crdt-extensions/peritext/slice/Range.ts rename to src/json-crdt-extensions/peritext/rga/Range.ts index f9cc176db3..1121167e05 100644 --- a/src/json-crdt-extensions/peritext/slice/Range.ts +++ b/src/json-crdt-extensions/peritext/rga/Range.ts @@ -1,4 +1,4 @@ -import {Point} from '../rga/Point'; +import {Point} from './Point'; import {Anchor} from '../constants'; import type {ITimestampStruct} from '../../../json-crdt-patch/clock'; import type {Printable} from '../../../util/print/types'; diff --git a/src/json-crdt-extensions/peritext/slice/__tests__/Range.spec.ts b/src/json-crdt-extensions/peritext/rga/__tests__/Range.spec.ts similarity index 100% rename from src/json-crdt-extensions/peritext/slice/__tests__/Range.spec.ts rename to src/json-crdt-extensions/peritext/rga/__tests__/Range.spec.ts diff --git a/src/json-crdt-extensions/peritext/slice/Cursor.ts b/src/json-crdt-extensions/peritext/slice/Cursor.ts index 6302c03bc5..46eda15407 100644 --- a/src/json-crdt-extensions/peritext/slice/Cursor.ts +++ b/src/json-crdt-extensions/peritext/slice/Cursor.ts @@ -1,6 +1,6 @@ import {Point} from '../rga/Point'; import {Anchor, SliceBehavior, Tags} from '../constants'; -import {Range} from './Range'; +import {Range} from '../rga/Range'; import {printTree} from '../../../util/print/printTree'; import type {ITimestampStruct} from '../../../json-crdt-patch/clock'; import type {Peritext} from '../Peritext'; diff --git a/src/json-crdt-extensions/peritext/slice/PersistedSlice.ts b/src/json-crdt-extensions/peritext/slice/PersistedSlice.ts index 30fcb307de..88806d7ec7 100644 --- a/src/json-crdt-extensions/peritext/slice/PersistedSlice.ts +++ b/src/json-crdt-extensions/peritext/slice/PersistedSlice.ts @@ -1,5 +1,5 @@ import {Point} from '../rga/Point'; -import {Range} from './Range'; +import {Range} from '../rga/Range'; import {hashNode} from '../../../json-crdt/hash'; import {printTree} from '../../../util/print/printTree'; import {Anchor, SliceHeaderMask, SliceHeaderShift, SliceBehavior} from '../constants'; diff --git a/src/json-crdt-extensions/peritext/slice/Slices.ts b/src/json-crdt-extensions/peritext/slice/Slices.ts index 07ef3e7be5..5e166d2755 100644 --- a/src/json-crdt-extensions/peritext/slice/Slices.ts +++ b/src/json-crdt-extensions/peritext/slice/Slices.ts @@ -1,6 +1,6 @@ import {PersistedSlice} from './PersistedSlice'; import {ITimespanStruct, ITimestampStruct, Timespan, Timestamp, compare, tss} from '../../../json-crdt-patch/clock'; -import {Range} from './Range'; +import {Range} from '../rga/Range'; import {updateRga} from '../../../json-crdt/hash'; import {CONST, updateNum} from '../../../json-hash'; import {printTree} from '../../../util/print/printTree'; diff --git a/src/json-crdt-extensions/peritext/slice/types.ts b/src/json-crdt-extensions/peritext/slice/types.ts index 1bbc912d43..3197ba32c1 100644 --- a/src/json-crdt-extensions/peritext/slice/types.ts +++ b/src/json-crdt-extensions/peritext/slice/types.ts @@ -1,4 +1,4 @@ -import type {Range} from './Range'; +import type {Range} from '../rga/Range'; import type {SliceType, Stateful} from '../types'; import type {ITimestampStruct} from '../../../json-crdt-patch/clock'; import type {SliceBehavior} from '../constants'; From a0332b04fd891951fe10569f2445d9c7ace27482 Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Fri, 19 Apr 2024 14:49:49 +0200 Subject: [PATCH 04/22] =?UTF-8?q?feat(json-crdt-extensions):=20?= =?UTF-8?q?=F0=9F=8E=B8=20improve=20unserialization=20of=20slices?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../peritext/constants.ts | 2 + .../peritext/rga/Range.ts | 12 +- .../peritext/slice/PersistedSlice.ts | 121 ++++++++++++------ .../peritext/slice/Slices.ts | 31 ++--- .../peritext/slice/SplitSlice.ts | 4 + 5 files changed, 107 insertions(+), 63 deletions(-) diff --git a/src/json-crdt-extensions/peritext/constants.ts b/src/json-crdt-extensions/peritext/constants.ts index f91ba633f6..a767f1da5c 100644 --- a/src/json-crdt-extensions/peritext/constants.ts +++ b/src/json-crdt-extensions/peritext/constants.ts @@ -7,6 +7,8 @@ export const enum Tags { Cursor = 0, } +// TODO: MOVE to /slices + export const enum SliceHeaderMask { X1Anchor = 0b1, X2Anchor = 0b10, diff --git a/src/json-crdt-extensions/peritext/rga/Range.ts b/src/json-crdt-extensions/peritext/rga/Range.ts index 1121167e05..38221b985b 100644 --- a/src/json-crdt-extensions/peritext/rga/Range.ts +++ b/src/json-crdt-extensions/peritext/rga/Range.ts @@ -1,15 +1,17 @@ import {Point} from './Point'; import {Anchor} from '../constants'; +import {updateNum} from '../../../json-hash'; import type {ITimestampStruct} from '../../../json-crdt-patch/clock'; import type {Printable} from '../../../util/print/types'; import type {AbstractRga, Chunk} from '../../../json-crdt/nodes/rga'; +import type {Stateful} from '../types'; /** * A range is a pair of points that represent a selection in the text. A range * can be collapsed to a single point, then it is called a *marker* * (if it is stored in the text), or *caret* (if it is a cursor position). */ -export class Range implements Printable { +export class Range implements Pick, Printable { /** * Creates a range from two points. The points are ordered so that the * start point is before or equal to the end point. @@ -206,6 +208,14 @@ export class Range implements Printable { return result; } + // ----------------------------------------------------------------- Stateful + + public refresh(): number { + let state = this.start.refresh(); + state = updateNum(state, this.end.refresh()); + return state; + } + // ---------------------------------------------------------------- Printable public toString(tab: string = '', lite: boolean = true): string { diff --git a/src/json-crdt-extensions/peritext/slice/PersistedSlice.ts b/src/json-crdt-extensions/peritext/slice/PersistedSlice.ts index 88806d7ec7..a20703de48 100644 --- a/src/json-crdt-extensions/peritext/slice/PersistedSlice.ts +++ b/src/json-crdt-extensions/peritext/slice/PersistedSlice.ts @@ -1,36 +1,89 @@ import {Point} from '../rga/Point'; import {Range} from '../rga/Range'; -import {hashNode} from '../../../json-crdt/hash'; +import {updateNode} from '../../../json-crdt/hash'; import {printTree} from '../../../util/print/printTree'; import {Anchor, SliceHeaderMask, SliceHeaderShift, SliceBehavior} from '../constants'; -import {ArrChunk} from '../../../json-crdt/nodes'; -import {type ITimestampStruct, Timestamp} from '../../../json-crdt-patch/clock'; +import {CONST} from '../../../json-hash'; +import {Timestamp} from '../../../json-crdt-patch/clock'; +import {VecNode} from '../../../json-crdt/nodes'; +import type {JsonNode} from '../../../json-crdt/nodes'; +import type {ITimestampStruct} from '../../../json-crdt-patch/clock'; +import type {ArrChunk} from '../../../json-crdt/nodes'; import type {Slice} from './types'; import type {Peritext} from '../Peritext'; import type {SliceDto, SliceType, Stateful} from '../types'; import type {Printable} from '../../../util/print/types'; -import type {JsonNode, VecNode} from '../../../json-crdt/nodes'; import type {AbstractRga} from '../../../json-crdt/nodes/rga'; -export class PersistedSlice extends Range implements Slice, Printable, Stateful { +export const validateType = (type: SliceType) => { + switch (typeof type) { + case 'string': + case 'number': + return; + case 'object': { + if (!(type instanceof Array)) throw new Error('INVALID_TYPE'); + if (type.length > 32) throw new Error('INVALID_TYPE'); + const length = type.length; + LOOP: for (let i = 0; i < length; i++) { + const step = type[i]; + switch (typeof step) { + case 'string': + case 'number': + continue LOOP; + default: + throw new Error('INVALID_TYPE'); + } + } + return; + } + default: + throw new Error('INVALID_TYPE'); + } +}; + +export class PersistedSlice extends Range implements Slice, Stateful, Printable { + public static deserialize(txt: Peritext, rga: AbstractRga, chunk: ArrChunk, tuple: VecNode): PersistedSlice { + const header = +(tuple.get(0)!.view() as SliceDto[0]); + const id1 = tuple.get(1)!.view() as ITimestampStruct; + const id2 = (tuple.get(2)!.view() || id1) as ITimestampStruct; + const type = tuple.get(3)!.view() as SliceType; + if (typeof header !== 'number') throw new Error('INVALID_HEADER'); + if (!(id1 instanceof Timestamp)) throw new Error('INVALID_ID'); + if (!(id2 instanceof Timestamp)) throw new Error('INVALID_ID'); + validateType(type); + const anchor1: Anchor = (header & SliceHeaderMask.X1Anchor) >>> SliceHeaderShift.X1Anchor; + const anchor2: Anchor = (header & SliceHeaderMask.X2Anchor) >>> SliceHeaderShift.X2Anchor; + const behavior: SliceBehavior = (header & SliceHeaderMask.Behavior) >>> SliceHeaderShift.Behavior; + const p1 = new Point(rga, id1, anchor1); + const p2 = new Point(rga, id2, anchor2); + const slice = new PersistedSlice(txt, rga, chunk, tuple, behavior, type, p1, p2); + return slice; + } + constructor( + /** The Peritext context. */ protected readonly txt: Peritext, + /** The text RGA. */ protected readonly rga: AbstractRga, + /** The `arr` chunk of `arr` where the slice is stored. */ protected readonly chunk: ArrChunk, + /** The `vec` node which stores the serialized contents of this slice. */ public readonly tuple: VecNode, behavior: SliceBehavior, - /** @todo Rename to x1? */ + type: SliceType, public start: Point, - /** @todo Rename to x2? */ public end: Point, - type: SliceType, ) { super(rga, start, end); - this.id = this.chunk.id; + this.id = chunk.id; this.behavior = behavior; this.type = type; } + public isSplit(): boolean { + return this.behavior === SliceBehavior.Split; + } + protected tagNode(): JsonNode | undefined { // TODO: Normalize `.get()` and `.getNode()` methods across VecNode and ArrNode. return this.tuple.get(3); @@ -50,46 +103,32 @@ export class PersistedSlice extends Range implements Slice, Pr return this.chunk.del; } - // ---------------------------------------------------------------- Printable - - public toString(tab: string = ''): string { - const tagNode = this.tagNode(); - const range = `${this.start.toString('', true)} ↔ ${this.end.toString('', true)}`; - const header = `${this.constructor.name} ${range}`; - return header + printTree(tab, [!tagNode ? null : (tab) => tagNode.toString(tab)]); - } - // ----------------------------------------------------------------- Stateful public hash: number = 0; public refresh(): number { - const hash = hashNode(this.tuple); - const changed = hash !== this.hash; - - // TODO: Add .refresh() to Range. - - this.hash = hash; + let state = CONST.START_STATE; + state = updateNode(state, this.tuple); + const changed = state !== this.hash; + this.hash = state; if (changed) { const tuple = this.tuple; - const header = +(tuple.get(0)!.view() as SliceDto[0]); - const anchor1: Anchor = (header & SliceHeaderMask.X1Anchor) >>> SliceHeaderShift.X1Anchor; - const anchor2: Anchor = (header & SliceHeaderMask.X2Anchor) >>> SliceHeaderShift.X2Anchor; - const type: SliceBehavior = (header & SliceHeaderMask.Behavior) >>> SliceHeaderShift.Behavior; - const id1 = tuple.get(1)!.view() as ITimestampStruct; - const id2 = (tuple.get(2)!.view() || id1) as ITimestampStruct; - if (!(id1 instanceof Timestamp)) throw new Error('INVALID_ID'); - if (!(id2 instanceof Timestamp)) throw new Error('INVALID_ID'); - const subtype = tuple.get(3)!.view() as SliceType; - this.behavior = type; - this.type = subtype; - const x1 = this.start; - const x2 = this.end; - x1.id = id1; - x1.anchor = anchor1; - x2.id = id2; - x2.anchor = anchor2; + const slice = PersistedSlice.deserialize(this.txt, this.rga, this.chunk, tuple); + this.behavior = slice.behavior; + this.type = slice.type; + this.start = slice.start; + this.end = slice.end; } return this.hash; } + + // ---------------------------------------------------------------- Printable + + public toString(tab: string = ''): string { + const tagNode = this.tagNode(); + const range = `${this.start.toString('', true)} ↔ ${this.end.toString('', true)}`; + const header = `${this.constructor.name} ${range}`; + return header + printTree(tab, [!tagNode ? null : (tab) => tagNode.toString(tab)]); + } } diff --git a/src/json-crdt-extensions/peritext/slice/Slices.ts b/src/json-crdt-extensions/peritext/slice/Slices.ts index 5e166d2755..39b516a188 100644 --- a/src/json-crdt-extensions/peritext/slice/Slices.ts +++ b/src/json-crdt-extensions/peritext/slice/Slices.ts @@ -1,15 +1,15 @@ import {PersistedSlice} from './PersistedSlice'; -import {ITimespanStruct, ITimestampStruct, Timespan, Timestamp, compare, tss} from '../../../json-crdt-patch/clock'; +import {Timespan, compare, tss} from '../../../json-crdt-patch/clock'; import {Range} from '../rga/Range'; import {updateRga} from '../../../json-crdt/hash'; import {CONST, updateNum} from '../../../json-hash'; import {printTree} from '../../../util/print/printTree'; -import {Anchor, SliceBehavior, SliceHeaderMask, SliceHeaderShift} from '../constants'; +import {SliceBehavior, SliceHeaderShift} from '../constants'; import {SplitSlice} from './SplitSlice'; -import {Point} from '../rga/Point'; import {Slice} from './types'; import {VecNode} from '../../../json-crdt/nodes'; -import type {SliceDto, SliceType, Stateful} from '../types'; +import type {ITimespanStruct, ITimestampStruct} from '../../../json-crdt-patch/clock'; +import type {SliceType, Stateful} from '../types'; import type {Peritext} from '../Peritext'; import type {Printable} from '../../../util/print/types'; import type {ArrChunk, ArrNode} from '../../../json-crdt/nodes'; @@ -55,34 +55,23 @@ export class Slices implements Stateful, Printable { const txt = this.txt; const slice = behavior === SliceBehavior.Split - ? new SplitSlice(txt, txt.str, chunk, tuple, behavior, start, end, type) - : new PersistedSlice(txt, txt.str, chunk, tuple, behavior, start, end, type); + ? new SplitSlice(txt, txt.str, chunk, tuple, behavior, type, start, end) + : new PersistedSlice(txt, txt.str, chunk, tuple, behavior, type, start, end); this.list.set(chunk, slice); return slice; } protected unpack(chunk: ArrChunk): PersistedSlice { const txt = this.txt; + const rga = txt.str; const model = txt.model; const tupleId = chunk.data ? chunk.data[0] : undefined; if (!tupleId) throw new Error('MARKER_NOT_FOUND'); const tuple = model.index.get(tupleId); if (!(tuple instanceof VecNode)) throw new Error('NOT_TUPLE'); - const header = +(tuple.get(0)!.view() as SliceDto[0]); - const anchor1: Anchor = (header & SliceHeaderMask.X1Anchor) >>> SliceHeaderShift.X1Anchor; - const anchor2: Anchor = (header & SliceHeaderMask.X2Anchor) >>> SliceHeaderShift.X2Anchor; - const behavior: SliceBehavior = (header & SliceHeaderMask.Behavior) >>> SliceHeaderShift.Behavior; - const id1 = tuple.get(1)!.view() as ITimestampStruct; - const id2 = (tuple.get(2)!.view() || id1) as ITimestampStruct; - if (!(id1 instanceof Timestamp)) throw new Error('INVALID_ID'); - if (!(id2 instanceof Timestamp)) throw new Error('INVALID_ID'); - const p1 = new Point(txt.str, id1, anchor1); - const p2 = new Point(txt.str, id2, anchor2); - const type = tuple.get(3)!.view() as SliceType; - const slice = - behavior === SliceBehavior.Split - ? new SplitSlice(txt, txt.str, chunk, tuple, behavior, p1, p2, type) - : new PersistedSlice(txt, txt.str, chunk, tuple, behavior, p1, p2, type); + let slice = PersistedSlice.deserialize(txt, rga, chunk, tuple); + // TODO: Simplify, remove `SplitSlice` class. + if (slice.isSplit()) slice = new SplitSlice(txt, rga, chunk, tuple, slice.behavior, slice.type, slice.start, slice.end); return slice; } diff --git a/src/json-crdt-extensions/peritext/slice/SplitSlice.ts b/src/json-crdt-extensions/peritext/slice/SplitSlice.ts index 9c3b1ac3b7..ced358a8e2 100644 --- a/src/json-crdt-extensions/peritext/slice/SplitSlice.ts +++ b/src/json-crdt-extensions/peritext/slice/SplitSlice.ts @@ -1,3 +1,7 @@ import {PersistedSlice} from './PersistedSlice'; +/** + * A *split slice* represents a block split in the text, i.e. it is a *marker* + * that shows where a block was split. + */ export class SplitSlice extends PersistedSlice {} From fde38b3874c8f8d3a355d9e091380c7520b88d61 Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Fri, 19 Apr 2024 15:01:48 +0200 Subject: [PATCH 05/22] =?UTF-8?q?refactor(json-crdt-extensions):=20?= =?UTF-8?q?=F0=9F=92=A1=20improve=20constants?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/json-crdt-extensions/peritext/Peritext.ts | 3 ++- .../peritext/editor/Editor.ts | 3 ++- .../peritext/rga/Point.ts | 2 +- .../peritext/rga/Range.ts | 2 +- .../peritext/rga/__tests__/Point.spec.ts | 2 +- .../peritext/rga/__tests__/Range.spec.ts | 2 +- .../peritext/rga/constants.ts | 4 ++++ .../peritext/slice/Cursor.ts | 22 +++++++++---------- .../peritext/slice/PersistedSlice.ts | 3 ++- .../peritext/slice/Slices.ts | 5 +++-- .../peritext/slice/SplitSlice.ts | 2 ++ .../peritext/{ => slice}/constants.ts | 12 +++++----- .../peritext/slice/types.ts | 2 +- 13 files changed, 37 insertions(+), 27 deletions(-) create mode 100644 src/json-crdt-extensions/peritext/rga/constants.ts rename src/json-crdt-extensions/peritext/{ => slice}/constants.ts (85%) diff --git a/src/json-crdt-extensions/peritext/Peritext.ts b/src/json-crdt-extensions/peritext/Peritext.ts index cfffaf6b8a..fbb18ced70 100644 --- a/src/json-crdt-extensions/peritext/Peritext.ts +++ b/src/json-crdt-extensions/peritext/Peritext.ts @@ -1,4 +1,5 @@ -import {Anchor, SliceBehavior} from './constants'; +import {Anchor} from './rga/constants'; +import {SliceBehavior} from './slice/constants'; import {Point} from './rga/Point'; import {Range} from './rga/Range'; import {Editor} from './editor/Editor'; diff --git a/src/json-crdt-extensions/peritext/editor/Editor.ts b/src/json-crdt-extensions/peritext/editor/Editor.ts index 3c8cc2c14e..56dfe1e9e1 100644 --- a/src/json-crdt-extensions/peritext/editor/Editor.ts +++ b/src/json-crdt-extensions/peritext/editor/Editor.ts @@ -1,5 +1,6 @@ import {Cursor} from '../slice/Cursor'; -import {Anchor, SliceBehavior} from '../constants'; +import {Anchor} from '../rga/constants'; +import {SliceBehavior} from '../slice/constants'; import {tick, type ITimestampStruct} from '../../../json-crdt-patch/clock'; import {PersistedSlice} from '../slice/PersistedSlice'; import type {Range} from '../rga/Range'; diff --git a/src/json-crdt-extensions/peritext/rga/Point.ts b/src/json-crdt-extensions/peritext/rga/Point.ts index aa0821745f..30eb117b30 100644 --- a/src/json-crdt-extensions/peritext/rga/Point.ts +++ b/src/json-crdt-extensions/peritext/rga/Point.ts @@ -1,5 +1,5 @@ import {compare, type ITimestampStruct, toDisplayString, equal, tick, containsId} from '../../../json-crdt-patch/clock'; -import {Anchor} from '../constants'; +import {Anchor} from './constants'; import {ChunkSlice} from '../util/ChunkSlice'; import {updateId} from '../../../json-crdt/hash'; import type {AbstractRga, Chunk} from '../../../json-crdt/nodes/rga'; diff --git a/src/json-crdt-extensions/peritext/rga/Range.ts b/src/json-crdt-extensions/peritext/rga/Range.ts index 38221b985b..faa56f503e 100644 --- a/src/json-crdt-extensions/peritext/rga/Range.ts +++ b/src/json-crdt-extensions/peritext/rga/Range.ts @@ -1,5 +1,5 @@ import {Point} from './Point'; -import {Anchor} from '../constants'; +import {Anchor} from './constants'; import {updateNum} from '../../../json-hash'; import type {ITimestampStruct} from '../../../json-crdt-patch/clock'; import type {Printable} from '../../../util/print/types'; diff --git a/src/json-crdt-extensions/peritext/rga/__tests__/Point.spec.ts b/src/json-crdt-extensions/peritext/rga/__tests__/Point.spec.ts index e20a8ae5f4..163c0b761c 100644 --- a/src/json-crdt-extensions/peritext/rga/__tests__/Point.spec.ts +++ b/src/json-crdt-extensions/peritext/rga/__tests__/Point.spec.ts @@ -1,6 +1,6 @@ import {Model} from '../../../../json-crdt/model'; import {Peritext} from '../../Peritext'; -import {Anchor} from '../../constants'; +import {Anchor} from '../constants'; import {tick} from '../../../../json-crdt-patch/clock'; const setup = () => { diff --git a/src/json-crdt-extensions/peritext/rga/__tests__/Range.spec.ts b/src/json-crdt-extensions/peritext/rga/__tests__/Range.spec.ts index e08800c7dc..29bf962b4d 100644 --- a/src/json-crdt-extensions/peritext/rga/__tests__/Range.spec.ts +++ b/src/json-crdt-extensions/peritext/rga/__tests__/Range.spec.ts @@ -1,6 +1,6 @@ import {Model} from '../../../../json-crdt/model'; import {Peritext} from '../../Peritext'; -import {Anchor} from '../../constants'; +import {Anchor} from '../constants'; const setup = (insert: (peritext: Peritext) => void = (peritext) => peritext.strApi().ins(0, 'Hello world!')) => { const model = Model.withLogicalClock(); diff --git a/src/json-crdt-extensions/peritext/rga/constants.ts b/src/json-crdt-extensions/peritext/rga/constants.ts new file mode 100644 index 0000000000..2b03ccef96 --- /dev/null +++ b/src/json-crdt-extensions/peritext/rga/constants.ts @@ -0,0 +1,4 @@ +export const enum Anchor { + Before = 0, + After = 1, +} diff --git a/src/json-crdt-extensions/peritext/slice/Cursor.ts b/src/json-crdt-extensions/peritext/slice/Cursor.ts index 46eda15407..8fe3a10131 100644 --- a/src/json-crdt-extensions/peritext/slice/Cursor.ts +++ b/src/json-crdt-extensions/peritext/slice/Cursor.ts @@ -1,5 +1,5 @@ import {Point} from '../rga/Point'; -import {Anchor, SliceBehavior, Tags} from '../constants'; +import {CursorAnchor, SliceBehavior, Tags} from './constants'; import {Range} from '../rga/Range'; import {printTree} from '../../../util/print/printTree'; import type {ITimestampStruct} from '../../../json-crdt-patch/clock'; @@ -15,10 +15,8 @@ export class Cursor extends Range implements Slice { * the end which does not move when user changes selection. The other * end is free to move, the moving end of the cursor is "focus". By default * "anchor" is the start of the cursor. - * - * @todo Create a custom enum for this, instead of using `Anchor`. */ - public base: Anchor = Anchor.Before; + public anchorSide: CursorAnchor = CursorAnchor.Start; constructor( public readonly id: ITimestampStruct, @@ -30,17 +28,17 @@ export class Cursor extends Range implements Slice { } public anchor(): Point { - return this.base === Anchor.Before ? this.start : this.end; + return this.anchorSide === CursorAnchor.Start ? this.start : this.end; } public focus(): Point { - return this.base === Anchor.Before ? this.end : this.start; + return this.anchorSide === CursorAnchor.Start ? this.end : this.start; } - public set(start: Point, end?: Point, base: Anchor = Anchor.Before): void { + public set(start: Point, end?: Point, base: CursorAnchor = CursorAnchor.Start): void { if (!end || end === start) end = start.clone(); super.set(start, end); - this.base = base; + this.anchorSide = base; } public setAt(start: number, length: number = 0): void { @@ -51,7 +49,7 @@ export class Cursor extends Range implements Slice { len = -len; } super.setAt(at, len); - this.base = length < 0 ? Anchor.After : Anchor.Before; + this.anchorSide = length < 0 ? CursorAnchor.End : CursorAnchor.Start; } /** @@ -67,11 +65,11 @@ export class Cursor extends Range implements Slice { if (edge === 0) focus = point; else anchor = point; if (focus.cmpSpatial(anchor) < 0) { - this.base = Anchor.After; + this.anchorSide = CursorAnchor.End; this.start = focus; this.end = anchor; } else { - this.base = Anchor.Before; + this.anchorSide = CursorAnchor.Start; this.start = anchor; this.end = focus; } @@ -95,7 +93,7 @@ export class Cursor extends Range implements Slice { public toString(tab: string = ''): string { const text = JSON.stringify(this.text()); - const focusIcon = this.base === Anchor.Before ? '.⇨|' : '|⇦.'; + const focusIcon = this.anchorSide === CursorAnchor.Start ? '.⇨|' : '|⇦.'; const main = `${this.constructor.name} ${super.toString(tab + ' ', true)} ${focusIcon}`; return main + printTree(tab, [() => text]); } diff --git a/src/json-crdt-extensions/peritext/slice/PersistedSlice.ts b/src/json-crdt-extensions/peritext/slice/PersistedSlice.ts index a20703de48..c18af384c0 100644 --- a/src/json-crdt-extensions/peritext/slice/PersistedSlice.ts +++ b/src/json-crdt-extensions/peritext/slice/PersistedSlice.ts @@ -2,7 +2,8 @@ import {Point} from '../rga/Point'; import {Range} from '../rga/Range'; import {updateNode} from '../../../json-crdt/hash'; import {printTree} from '../../../util/print/printTree'; -import {Anchor, SliceHeaderMask, SliceHeaderShift, SliceBehavior} from '../constants'; +import {Anchor} from '../rga/constants'; +import {SliceHeaderMask, SliceHeaderShift, SliceBehavior} from './constants'; import {CONST} from '../../../json-hash'; import {Timestamp} from '../../../json-crdt-patch/clock'; import {VecNode} from '../../../json-crdt/nodes'; diff --git a/src/json-crdt-extensions/peritext/slice/Slices.ts b/src/json-crdt-extensions/peritext/slice/Slices.ts index 39b516a188..852bf0fd3b 100644 --- a/src/json-crdt-extensions/peritext/slice/Slices.ts +++ b/src/json-crdt-extensions/peritext/slice/Slices.ts @@ -4,7 +4,7 @@ import {Range} from '../rga/Range'; import {updateRga} from '../../../json-crdt/hash'; import {CONST, updateNum} from '../../../json-hash'; import {printTree} from '../../../util/print/printTree'; -import {SliceBehavior, SliceHeaderShift} from '../constants'; +import {SliceBehavior, SliceHeaderShift} from './constants'; import {SplitSlice} from './SplitSlice'; import {Slice} from './types'; import {VecNode} from '../../../json-crdt/nodes'; @@ -71,7 +71,8 @@ export class Slices implements Stateful, Printable { if (!(tuple instanceof VecNode)) throw new Error('NOT_TUPLE'); let slice = PersistedSlice.deserialize(txt, rga, chunk, tuple); // TODO: Simplify, remove `SplitSlice` class. - if (slice.isSplit()) slice = new SplitSlice(txt, rga, chunk, tuple, slice.behavior, slice.type, slice.start, slice.end); + if (slice.isSplit()) + slice = new SplitSlice(txt, rga, chunk, tuple, slice.behavior, slice.type, slice.start, slice.end); return slice; } diff --git a/src/json-crdt-extensions/peritext/slice/SplitSlice.ts b/src/json-crdt-extensions/peritext/slice/SplitSlice.ts index ced358a8e2..05eefe6ca3 100644 --- a/src/json-crdt-extensions/peritext/slice/SplitSlice.ts +++ b/src/json-crdt-extensions/peritext/slice/SplitSlice.ts @@ -3,5 +3,7 @@ import {PersistedSlice} from './PersistedSlice'; /** * A *split slice* represents a block split in the text, i.e. it is a *marker* * that shows where a block was split. + * + * @deprecated */ export class SplitSlice extends PersistedSlice {} diff --git a/src/json-crdt-extensions/peritext/constants.ts b/src/json-crdt-extensions/peritext/slice/constants.ts similarity index 85% rename from src/json-crdt-extensions/peritext/constants.ts rename to src/json-crdt-extensions/peritext/slice/constants.ts index a767f1da5c..39cb16fa7a 100644 --- a/src/json-crdt-extensions/peritext/constants.ts +++ b/src/json-crdt-extensions/peritext/slice/constants.ts @@ -1,14 +1,16 @@ -export const enum Anchor { - Before = 0, - After = 1, +/** + * Specifies which cursor end is the "anchor", e.g. the end which does not move + * when user changes selection. + */ +export const enum CursorAnchor { + Start = 0, + End = 1, } export const enum Tags { Cursor = 0, } -// TODO: MOVE to /slices - export const enum SliceHeaderMask { X1Anchor = 0b1, X2Anchor = 0b10, diff --git a/src/json-crdt-extensions/peritext/slice/types.ts b/src/json-crdt-extensions/peritext/slice/types.ts index 3197ba32c1..c617d8a575 100644 --- a/src/json-crdt-extensions/peritext/slice/types.ts +++ b/src/json-crdt-extensions/peritext/slice/types.ts @@ -1,7 +1,7 @@ import type {Range} from '../rga/Range'; import type {SliceType, Stateful} from '../types'; import type {ITimestampStruct} from '../../../json-crdt-patch/clock'; -import type {SliceBehavior} from '../constants'; +import type {SliceBehavior} from './constants'; export interface Slice extends Range, Stateful { /** From dfc3e031dbbe2761c12a5f60c4c3af40236d2ce4 Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Fri, 19 Apr 2024 15:31:37 +0200 Subject: [PATCH 06/22] =?UTF-8?q?chore(json-crdt-extensions):=20?= =?UTF-8?q?=F0=9F=A4=96=20update=20error=20message?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/json-crdt-extensions/peritext/slice/Slices.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/json-crdt-extensions/peritext/slice/Slices.ts b/src/json-crdt-extensions/peritext/slice/Slices.ts index 852bf0fd3b..7e69f125df 100644 --- a/src/json-crdt-extensions/peritext/slice/Slices.ts +++ b/src/json-crdt-extensions/peritext/slice/Slices.ts @@ -6,8 +6,8 @@ import {CONST, updateNum} from '../../../json-hash'; import {printTree} from '../../../util/print/printTree'; import {SliceBehavior, SliceHeaderShift} from './constants'; import {SplitSlice} from './SplitSlice'; -import {Slice} from './types'; import {VecNode} from '../../../json-crdt/nodes'; +import type {Slice} from './types'; import type {ITimespanStruct, ITimestampStruct} from '../../../json-crdt-patch/clock'; import type {SliceType, Stateful} from '../types'; import type {Peritext} from '../Peritext'; @@ -66,7 +66,7 @@ export class Slices implements Stateful, Printable { const rga = txt.str; const model = txt.model; const tupleId = chunk.data ? chunk.data[0] : undefined; - if (!tupleId) throw new Error('MARKER_NOT_FOUND'); + if (!tupleId) throw new Error('SLICE_NOT_FOUND'); const tuple = model.index.get(tupleId); if (!(tuple instanceof VecNode)) throw new Error('NOT_TUPLE'); let slice = PersistedSlice.deserialize(txt, rga, chunk, tuple); From c99fa958959a82ba8a613d70a293d0ac1171f49e Mon Sep 17 00:00:00 2001 From: streamich Date: Fri, 19 Apr 2024 17:35:14 +0200 Subject: [PATCH 07/22] =?UTF-8?q?perf(util):=20=E2=9A=A1=EF=B8=8F=20walk?= =?UTF-8?q?=20tree=20less=20when=20computing=20its=20size?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/util/trees/avl/AvlMap.ts | 9 ++------- src/util/trees/util.ts | 13 +++++++------ 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/src/util/trees/avl/AvlMap.ts b/src/util/trees/avl/AvlMap.ts index e7df5adc9b..1e48c0183d 100644 --- a/src/util/trees/avl/AvlMap.ts +++ b/src/util/trees/avl/AvlMap.ts @@ -1,6 +1,6 @@ import {insert, insertLeft, remove, insertRight, print} from './util'; import {printTree} from '../../print/printTree'; -import {findOrNextLower, first, next} from '../util'; +import {findOrNextLower, first, next, size} from '../util'; import type {Printable} from '../../print/types'; import type {Comparator, HeadlessNode} from '../types'; import type {AvlNodeReference, IAvlTreeNode} from './types'; @@ -81,12 +81,7 @@ export class AvlMap implements Printable { } public size(): number { - const root = this.root; - if (!root) return 0; - let curr = first(root); - let size = 1; - while ((curr = next(curr as HeadlessNode) as AvlNode | undefined)) size++; - return size; + return size(this.root); } public isEmpty(): boolean { diff --git a/src/util/trees/util.ts b/src/util/trees/util.ts index 9894abb2c6..0ef39d42d3 100644 --- a/src/util/trees/util.ts +++ b/src/util/trees/util.ts @@ -44,13 +44,14 @@ export const prev = (curr: N): N | undefined => { return p; }; +const size_ = (root: N): number => { + const l = root.l; + const r = root.r; + return 1 + (l ? size_(l) : 0) + (r ? size_(r) : 0); +}; + export const size = (root: N | undefined): number => { - if (!root) return 0; - const start = first(root)!; - let curr: N | undefined = start; - let result = 1; - while ((curr = next(curr))) result++; - return result; + return root ? size_(root) : 0; }; export const find = ( From 690f91a429bd0ef1d28b13391b44e80672829566 Mon Sep 17 00:00:00 2001 From: streamich Date: Fri, 19 Apr 2024 18:09:16 +0200 Subject: [PATCH 08/22] =?UTF-8?q?perf(util):=20=E2=9A=A1=EF=B8=8F=20store?= =?UTF-8?q?=20size=20of=20the=20map=20in=20AvlMap?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../__bench__/bench.map.insert.nums.native.ts | 15 +++++++++++++++ src/util/trees/__bench__/payloads.ts | 16 ++++++++-------- src/util/trees/avl/AvlMap.ts | 12 +++++++++--- 3 files changed, 32 insertions(+), 11 deletions(-) diff --git a/src/util/trees/__bench__/bench.map.insert.nums.native.ts b/src/util/trees/__bench__/bench.map.insert.nums.native.ts index 0c3214118a..32a1b16511 100644 --- a/src/util/trees/__bench__/bench.map.insert.nums.native.ts +++ b/src/util/trees/__bench__/bench.map.insert.nums.native.ts @@ -3,6 +3,7 @@ import {runBenchmark, IBenchmark} from '../../../__bench__/runBenchmark'; import {Tree} from '../Tree'; import {AvlBstNumNumMap} from '../avl/AvlBstNumNumMap'; +import {AvlMap} from '../avl/AvlMap'; import {RadixTree} from '../radix/RadixTree'; import * as payloads from './payloads'; @@ -67,6 +68,20 @@ const benchmark: IBenchmark = { }; }, }, + { + name: 'json-joy AvlMap', + setup: () => { + return (num: unknown) => { + const map = new AvlMap(); + const numbers = num as number[]; + const length = numbers.length; + for (let i = 0; i < length; i++) { + const key = numbers[i]; + map.set(key, key); + } + }; + }, + }, { name: 'json-joy Tree', setup: () => { diff --git a/src/util/trees/__bench__/payloads.ts b/src/util/trees/__bench__/payloads.ts index e826e07cfd..aeac0d016d 100644 --- a/src/util/trees/__bench__/payloads.ts +++ b/src/util/trees/__bench__/payloads.ts @@ -8,6 +8,14 @@ const naturalNumbersRandomSmallList = Array.from({length: 100}, (_, i) => Math.f const naturalNumbersRandom = Array.from({length: 1000}, (_, i) => Math.floor(Math.random() * 1000)); export const numbers = [ + { + name: (json: any) => `Random ${naturalNumbersRandomSmallList.length} numbers`, + data: naturalNumbersRandomSmallList, + }, + { + name: (json: any) => `Random ${naturalNumbersRandom.length} numbers`, + data: naturalNumbersRandom, + }, { name: (json: any) => `${(json as any).length} natural numbers from ${(json as any)[0]} to ${(json as any)[(json as any).length - 1]}`, @@ -38,12 +46,4 @@ export const numbers = [ `${(json as any).length} natural numbers from ${(json as any)[0]} to ${(json as any)[(json as any).length - 1]}`, data: naturalNumbersReverse, }, - { - name: (json: any) => `Random ${naturalNumbersRandomSmallList.length} numbers`, - data: naturalNumbersRandomSmallList, - }, - { - name: (json: any) => `Random ${naturalNumbersRandom.length} numbers`, - data: naturalNumbersRandom, - }, ]; diff --git a/src/util/trees/avl/AvlMap.ts b/src/util/trees/avl/AvlMap.ts index 1e48c0183d..c5e2ffc1db 100644 --- a/src/util/trees/avl/AvlMap.ts +++ b/src/util/trees/avl/AvlMap.ts @@ -1,6 +1,6 @@ import {insert, insertLeft, remove, insertRight, print} from './util'; import {printTree} from '../../print/printTree'; -import {findOrNextLower, first, next, size} from '../util'; +import {findOrNextLower, first, next} from '../util'; import type {Printable} from '../../print/types'; import type {Comparator, HeadlessNode} from '../types'; import type {AvlNodeReference, IAvlTreeNode} from './types'; @@ -29,6 +29,7 @@ export class AvlMap implements Printable { public insert(k: K, v: V): AvlNodeReference> { const item = new AvlNode(k, v); this.root = insert(this.root, item, this.comparator); + this._size++; return item; } @@ -47,6 +48,7 @@ export class AvlMap implements Printable { const node = new AvlNode(k, v); this.root = cmp < 0 ? (insertLeft(root, node, curr) as AvlNode) : (insertRight(root, node, curr) as AvlNode); + this._size++; return node; } @@ -69,19 +71,23 @@ export class AvlMap implements Printable { const node = this.find(k); if (!node) return false; this.root = remove(this.root, node as IAvlTreeNode); + this._size--; return true; } public clear(): void { + this._size = 0; this.root = undefined; } - + public has(k: K): boolean { return !!this.find(k); } + public _size: number = 0; + public size(): number { - return size(this.root); + return this._size; } public isEmpty(): boolean { From 97b60c2e30bcf1150502687d038af860c26eed0b Mon Sep 17 00:00:00 2001 From: streamich Date: Fri, 19 Apr 2024 18:30:37 +0200 Subject: [PATCH 09/22] =?UTF-8?q?feat(json-crdt-extensions):=20?= =?UTF-8?q?=F0=9F=8E=B8=20improve=20.toString()=20presentation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/json-crdt-extensions/peritext/slice/Cursor.ts | 2 +- src/json-crdt-extensions/peritext/slice/PersistedSlice.ts | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/json-crdt-extensions/peritext/slice/Cursor.ts b/src/json-crdt-extensions/peritext/slice/Cursor.ts index 8fe3a10131..710e11e03b 100644 --- a/src/json-crdt-extensions/peritext/slice/Cursor.ts +++ b/src/json-crdt-extensions/peritext/slice/Cursor.ts @@ -93,7 +93,7 @@ export class Cursor extends Range implements Slice { public toString(tab: string = ''): string { const text = JSON.stringify(this.text()); - const focusIcon = this.anchorSide === CursorAnchor.Start ? '.⇨|' : '|⇦.'; + const focusIcon = this.anchorSide === CursorAnchor.Start ? '.→|' : '|←.'; const main = `${this.constructor.name} ${super.toString(tab + ' ', true)} ${focusIcon}`; return main + printTree(tab, [() => text]); } diff --git a/src/json-crdt-extensions/peritext/slice/PersistedSlice.ts b/src/json-crdt-extensions/peritext/slice/PersistedSlice.ts index c18af384c0..ffd4e52b9b 100644 --- a/src/json-crdt-extensions/peritext/slice/PersistedSlice.ts +++ b/src/json-crdt-extensions/peritext/slice/PersistedSlice.ts @@ -128,8 +128,7 @@ export class PersistedSlice extends Range implements Slice, St public toString(tab: string = ''): string { const tagNode = this.tagNode(); - const range = `${this.start.toString('', true)} ↔ ${this.end.toString('', true)}`; - const header = `${this.constructor.name} ${range}`; + const header = `${this.constructor.name} ${super.toString(tab)}`; return header + printTree(tab, [!tagNode ? null : (tab) => tagNode.toString(tab)]); } } From 5acce0ff4e3f88de6a4a7819a75754cfb5c7181b Mon Sep 17 00:00:00 2001 From: streamich Date: Fri, 19 Apr 2024 18:43:09 +0200 Subject: [PATCH 10/22] =?UTF-8?q?perf(json-crdt-extensions):=20=E2=9A=A1?= =?UTF-8?q?=EF=B8=8F=20store=20slice=20list=20in=20AvlMap?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../peritext/slice/Slices.ts | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/src/json-crdt-extensions/peritext/slice/Slices.ts b/src/json-crdt-extensions/peritext/slice/Slices.ts index 7e69f125df..3c49b65374 100644 --- a/src/json-crdt-extensions/peritext/slice/Slices.ts +++ b/src/json-crdt-extensions/peritext/slice/Slices.ts @@ -7,6 +7,7 @@ import {printTree} from '../../../util/print/printTree'; import {SliceBehavior, SliceHeaderShift} from './constants'; import {SplitSlice} from './SplitSlice'; import {VecNode} from '../../../json-crdt/nodes'; +import {AvlMap} from '../../../util/trees/avl/AvlMap'; import type {Slice} from './types'; import type {ITimespanStruct, ITimestampStruct} from '../../../json-crdt-patch/clock'; import type {SliceType, Stateful} from '../types'; @@ -15,7 +16,7 @@ import type {Printable} from '../../../util/print/types'; import type {ArrChunk, ArrNode} from '../../../json-crdt/nodes'; export class Slices implements Stateful, Printable { - private list = new Map(); + private list = new AvlMap(compare); constructor( public readonly txt: Peritext, @@ -57,7 +58,7 @@ export class Slices implements Stateful, Printable { behavior === SliceBehavior.Split ? new SplitSlice(txt, txt.str, chunk, tuple, behavior, type, start, end) : new PersistedSlice(txt, txt.str, chunk, tuple, behavior, type, start, end); - this.list.set(chunk, slice); + this.list.set(chunk.id, slice); return slice; } @@ -76,6 +77,10 @@ export class Slices implements Stateful, Printable { return slice; } + public get(id: ITimestampStruct): PersistedSlice | undefined { + return this.list.get(id); + } + public del(id: ITimestampStruct): void { const api = this.txt.model.api; api.builder.del(this.set.id, [tss(id.sid, id.time, 1)]); @@ -98,11 +103,11 @@ export class Slices implements Stateful, Printable { } public size(): number { - return this.list.size; + return this.list._size; } public forEach(callback: (item: PersistedSlice) => void): void { - this.list.forEach(callback); + this.list.forEach((node) => callback(node.v)); } // ----------------------------------------------------------------- Stateful @@ -116,16 +121,16 @@ export class Slices implements Stateful, Printable { this._topologyHash = topologyHash; let chunk: ArrChunk | undefined; for (const iterator = this.set.iterator(); (chunk = iterator()); ) { - const item = this.list.get(chunk); + const item = this.list.get(chunk.id); if (chunk.del) { - if (item) this.list.delete(chunk); + if (item) this.list.del(chunk.id); } else { - if (!item) this.list.set(chunk, this.unpack(chunk)); + if (!item) this.list.set(chunk.id, this.unpack(chunk)); } } } let hash: number = topologyHash; - this.list.forEach((item) => { + this.list.forEach(({v: item}) => { item.refresh(); hash = updateNum(hash, item.hash); }); @@ -139,10 +144,10 @@ export class Slices implements Stateful, Printable { this.constructor.name + printTree( tab, - [...this.list].map( - ([, slice]) => + [...this.list.entries()].map( + ({v}) => (tab) => - slice.toString(tab), + v.toString(tab), ), ) ); From abd04a68b8ea49de8557deed8d134617c4b43614 Mon Sep 17 00:00:00 2001 From: streamich Date: Fri, 19 Apr 2024 18:43:35 +0200 Subject: [PATCH 11/22] =?UTF-8?q?test(json-crdt-extensions):=20?= =?UTF-8?q?=F0=9F=92=8D=20improve=20Slices.ins()=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../peritext/slice/__tests__/Slices.spec.ts | 69 +++++++++++++++---- 1 file changed, 54 insertions(+), 15 deletions(-) diff --git a/src/json-crdt-extensions/peritext/slice/__tests__/Slices.spec.ts b/src/json-crdt-extensions/peritext/slice/__tests__/Slices.spec.ts index 83da192c01..55ed32f3f8 100644 --- a/src/json-crdt-extensions/peritext/slice/__tests__/Slices.spec.ts +++ b/src/json-crdt-extensions/peritext/slice/__tests__/Slices.spec.ts @@ -1,5 +1,7 @@ import {Model} from '../../../../json-crdt/model'; import {Peritext} from '../../Peritext'; +import {Anchor} from '../../rga/constants'; +import {SliceBehavior} from '../constants'; const setup = () => { const model = Model.withLogicalClock(); @@ -13,7 +15,8 @@ const setup = () => { model.api.str(['text']).del(7, 1); model.api.str(['text']).ins(11, ' this game is awesome'); const peritext = new Peritext(model, model.api.str(['text']).node, model.api.arr(['slices']).node); - return {model, peritext}; + const slices = peritext.slices; + return {model, peritext, slices}; }; test('initially slice list is empty', () => { @@ -23,25 +26,25 @@ test('initially slice list is empty', () => { expect(peritext.slices.size()).toBe(0); }); -describe('inserts', () => { +describe('.ins()', () => { test('can insert a slice', () => { - const {peritext} = setup(); - const {editor} = peritext; - editor.setCursor(12, 7); - const slice = editor.insertSlice('b', {bold: true}); - peritext.refresh(); + const {peritext, slices} = setup(); + const range = peritext.rangeAt(12, 7); + const slice = slices.ins(range, SliceBehavior.Stack, 'b', {bold: true}); expect(peritext.slices.size()).toBe(1); - expect(slice.start).toStrictEqual(editor.cursor.start); - expect(slice.end).toStrictEqual(editor.cursor.end); + expect(slice.start).toStrictEqual(range.start); + expect(slice.end).toStrictEqual(range.end); + expect(slice.behavior).toBe(SliceBehavior.Stack); + expect(slice.type).toBe('b'); expect(slice.data()).toStrictEqual({bold: true}); }); test('can insert two slices', () => { const {peritext} = setup(); const {editor} = peritext; - editor.setCursor(6, 5); + editor.cursor.setAt(6, 5); const slice1 = editor.insertSlice('strong', {bold: true}); - editor.setCursor(12, 4); + editor.cursor.setAt(12, 4); const slice2 = editor.insertSlice('i', {italic: true}); peritext.refresh(); expect(peritext.slices.size()).toBe(2); @@ -59,7 +62,7 @@ describe('inserts', () => { expect(changed1).toBe(true); expect(changed2).toBe(false); expect(hash1).toBe(hash2); - editor.setCursor(12, 7); + editor.cursor.setAt(12, 7); editor.insertSlice('b', {bold: true}); const changed3 = peritext.slices.hash !== peritext.slices.refresh(); const hash3 = peritext.slices.hash; @@ -69,7 +72,7 @@ describe('inserts', () => { expect(changed4).toBe(false); expect(hash1).not.toStrictEqual(hash3); expect(hash3).toBe(hash4); - editor.setCursor(12, 4); + editor.cursor.setAt(12, 4); editor.insertSlice('em', {italic: true}); const changed5 = peritext.slices.hash !== peritext.slices.refresh(); const hash5 = peritext.slices.hash; @@ -80,13 +83,49 @@ describe('inserts', () => { expect(hash3).not.toBe(hash5); expect(hash5).toBe(hash6); }); + + test('can store all different slice range and metadata combinations', () => { + const {peritext, model} = setup(); + const r1 = peritext.range(peritext.pointAt(4, Anchor.Before), peritext.pointAt(6, Anchor.After)); + const r2 = peritext.range(peritext.pointAt(2, Anchor.After), peritext.pointAt(8, Anchor.After)); + const r3 = peritext.range(peritext.pointAt(2, Anchor.After), peritext.pointAt(8, Anchor.Before)); + const r4 = peritext.range(peritext.pointAt(0, Anchor.Before), peritext.pointAt(8, Anchor.Before)); + const ranges = [r1, r2, r3, r4]; + const types = ['b', ['li', 'ul'], 0, 123, [1, 2, 3]]; + const datas = [{bold: true}, {list: 'ul'}, 0, 123, [1, 2, 3], null, undefined]; + const behaviors = [SliceBehavior.Stack, SliceBehavior.Erase, SliceBehavior.Overwrite, SliceBehavior.Split]; + for (const range of ranges) { + for (const type of types) { + for (const data of datas) { + for (const behavior of behaviors) { + const slice = peritext.slices.ins(range, behavior, type, data); + expect(slice.start.cmp(range.start)).toBe(0); + expect(slice.end.cmp(range.end)).toBe(0); + expect(slice.behavior).toBe(behavior); + expect(slice.type).toStrictEqual(type); + expect(slice.data()).toStrictEqual(data); + const buf = model.toBinary(); + const model2 = Model.fromBinary(buf); + const peritext2 = new Peritext(model2, model2.api.str(['text']).node, model2.api.arr(['slices']).node); + peritext2.refresh(); + const slice2 = peritext2.slices.get(slice.id)!; + expect(slice2.start.cmp(range.start)).toBe(0); + expect(slice2.end.cmp(range.end)).toBe(0); + expect(slice2.behavior).toBe(behavior); + expect(slice2.type).toStrictEqual(type); + expect(slice2.data()).toStrictEqual(data); + } + } + } + } + }); }); describe('deletes', () => { test('can delete a slice', () => { const {peritext} = setup(); const {editor} = peritext; - editor.setCursor(6, 5); + editor.cursor.setAt(6, 5); const slice1 = editor.insertSlice('b', {bold: true}); peritext.refresh(); const hash1 = peritext.slices.hash; @@ -103,7 +142,7 @@ describe('tag changes', () => { test('recomputes hash on tag change', () => { const {peritext} = setup(); const {editor} = peritext; - editor.setCursor(6, 5); + editor.cursor.setAt(6, 5); const slice1 = editor.insertSlice('b', {bold: true}); peritext.refresh(); const hash1 = peritext.slices.hash; From 12e4129b151e3dd93e9bb45c0f2d0b6e3a4b5d48 Mon Sep 17 00:00:00 2001 From: streamich Date: Fri, 19 Apr 2024 19:51:56 +0200 Subject: [PATCH 12/22] =?UTF-8?q?style:=20=F0=9F=92=84=20run=20Prettier?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/json-crdt-extensions/peritext/slice/SplitSlice.ts | 2 +- src/util/trees/avl/AvlMap.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/json-crdt-extensions/peritext/slice/SplitSlice.ts b/src/json-crdt-extensions/peritext/slice/SplitSlice.ts index 05eefe6ca3..a66c0d267d 100644 --- a/src/json-crdt-extensions/peritext/slice/SplitSlice.ts +++ b/src/json-crdt-extensions/peritext/slice/SplitSlice.ts @@ -3,7 +3,7 @@ import {PersistedSlice} from './PersistedSlice'; /** * A *split slice* represents a block split in the text, i.e. it is a *marker* * that shows where a block was split. - * + * * @deprecated */ export class SplitSlice extends PersistedSlice {} diff --git a/src/util/trees/avl/AvlMap.ts b/src/util/trees/avl/AvlMap.ts index c5e2ffc1db..700eac2a2e 100644 --- a/src/util/trees/avl/AvlMap.ts +++ b/src/util/trees/avl/AvlMap.ts @@ -79,7 +79,7 @@ export class AvlMap implements Printable { this._size = 0; this.root = undefined; } - + public has(k: K): boolean { return !!this.find(k); } From c482e640657bf3acc5ac9d806c6ebb0fac7766f8 Mon Sep 17 00:00:00 2001 From: streamich Date: Fri, 19 Apr 2024 21:55:56 +0200 Subject: [PATCH 13/22] =?UTF-8?q?feat(json-crdt-extensions):=20?= =?UTF-8?q?=F0=9F=8E=B8=20improve=20slice=20state=20refresh=20and=20print?= =?UTF-8?q?=20layout?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/json-crdt-extensions/peritext/Peritext.ts | 6 --- .../peritext/slice/Cursor.ts | 27 +++++++------ .../peritext/slice/PersistedSlice.ts | 13 ++++--- .../peritext/slice/Slices.ts | 2 +- .../peritext/slice/__tests__/Slices.spec.ts | 39 ++++++++++++++----- .../peritext/slice/types.ts | 3 +- 6 files changed, 56 insertions(+), 34 deletions(-) diff --git a/src/json-crdt-extensions/peritext/Peritext.ts b/src/json-crdt-extensions/peritext/Peritext.ts index fbb18ced70..72bb9fe4b7 100644 --- a/src/json-crdt-extensions/peritext/Peritext.ts +++ b/src/json-crdt-extensions/peritext/Peritext.ts @@ -160,12 +160,6 @@ export class Peritext implements Printable { return slice; } - // ---------------------------------------------------------------- Deletions - - public delSlice(sliceId: ITimestampStruct): void { - this.slices.del(sliceId); - } - /** Select a single character before a point. */ public findCharBefore(point: Point): Range | undefined { if (point.anchor === Anchor.After) { diff --git a/src/json-crdt-extensions/peritext/slice/Cursor.ts b/src/json-crdt-extensions/peritext/slice/Cursor.ts index 710e11e03b..9f016ef8ca 100644 --- a/src/json-crdt-extensions/peritext/slice/Cursor.ts +++ b/src/json-crdt-extensions/peritext/slice/Cursor.ts @@ -5,6 +5,7 @@ import {printTree} from '../../../util/print/printTree'; import type {ITimestampStruct} from '../../../json-crdt-patch/clock'; import type {Peritext} from '../Peritext'; import type {Slice} from './types'; +import {updateNum} from '../../../json-hash'; export class Cursor extends Range implements Slice { public readonly behavior = SliceBehavior.Overwrite; @@ -80,8 +81,8 @@ export class Cursor extends Range implements Slice { return false; } - public data(): unknown { - return 1; + public data() { + return undefined; } public move(move: number): void { @@ -91,19 +92,23 @@ export class Cursor extends Range implements Slice { end.move(move); } - public toString(tab: string = ''): string { - const text = JSON.stringify(this.text()); - const focusIcon = this.anchorSide === CursorAnchor.Start ? '.→|' : '|←.'; - const main = `${this.constructor.name} ${super.toString(tab + ' ', true)} ${focusIcon}`; - return main + printTree(tab, [() => text]); - } - // ----------------------------------------------------------------- Stateful public hash: number = 0; public refresh(): number { - // TODO: implement this ... - return this.hash; + let state = super.refresh(); + state = updateNum(state, this.anchorSide); + this.hash = state; + return state; + } + + // ---------------------------------------------------------------- Printable + + public toString(tab: string = ''): string { + const text = JSON.stringify(this.text()); + const focusIcon = this.anchorSide === CursorAnchor.Start ? '.→|' : '|←.'; + const main = `${this.constructor.name} ${super.toString(tab + ' ', true)} ${focusIcon}`; + return main + printTree(tab, [() => text]); } } diff --git a/src/json-crdt-extensions/peritext/slice/PersistedSlice.ts b/src/json-crdt-extensions/peritext/slice/PersistedSlice.ts index ffd4e52b9b..c5a9d51384 100644 --- a/src/json-crdt-extensions/peritext/slice/PersistedSlice.ts +++ b/src/json-crdt-extensions/peritext/slice/PersistedSlice.ts @@ -96,8 +96,8 @@ export class PersistedSlice extends Range implements Slice, St public behavior: SliceBehavior; public type: SliceType; - public data(): unknown | undefined { - return this.tuple.get(4)?.view(); + public data(): JsonNode | undefined { + return this.tuple.get(4); } public del(): boolean { @@ -127,8 +127,11 @@ export class PersistedSlice extends Range implements Slice, St // ---------------------------------------------------------------- Printable public toString(tab: string = ''): string { - const tagNode = this.tagNode(); - const header = `${this.constructor.name} ${super.toString(tab)}`; - return header + printTree(tab, [!tagNode ? null : (tab) => tagNode.toString(tab)]); + const data = this.data(); + const header = `${this.constructor.name} ${super.toString(tab)}, ${this.behavior}, ${JSON.stringify(this.type)}`; + return header + printTree(tab, [ + // !data ? null : (tab) => JSON.stringify(data.view()).replace(/([\{\[\:])/g, '$1 ').replace(/([\}\]])/g, ' $1'), + !data ? null : (tab) => JSON.stringify(data.view()), + ]); } } diff --git a/src/json-crdt-extensions/peritext/slice/Slices.ts b/src/json-crdt-extensions/peritext/slice/Slices.ts index 3c49b65374..c623f49c84 100644 --- a/src/json-crdt-extensions/peritext/slice/Slices.ts +++ b/src/json-crdt-extensions/peritext/slice/Slices.ts @@ -87,7 +87,7 @@ export class Slices implements Stateful, Printable { api.apply(); } - public delMany(slices: Slice[]): void { + public delSlices(slices: Slice[]): void { const api = this.txt.model.api; const spans: ITimespanStruct[] = []; const length = slices.length; diff --git a/src/json-crdt-extensions/peritext/slice/__tests__/Slices.spec.ts b/src/json-crdt-extensions/peritext/slice/__tests__/Slices.spec.ts index 55ed32f3f8..34e0a5327d 100644 --- a/src/json-crdt-extensions/peritext/slice/__tests__/Slices.spec.ts +++ b/src/json-crdt-extensions/peritext/slice/__tests__/Slices.spec.ts @@ -4,7 +4,7 @@ import {Anchor} from '../../rga/constants'; import {SliceBehavior} from '../constants'; const setup = () => { - const model = Model.withLogicalClock(); + const model = Model.withLogicalClock(12345678); model.api.root({ text: '', slices: [], @@ -36,7 +36,7 @@ describe('.ins()', () => { expect(slice.end).toStrictEqual(range.end); expect(slice.behavior).toBe(SliceBehavior.Stack); expect(slice.type).toBe('b'); - expect(slice.data()).toStrictEqual({bold: true}); + expect(slice.data()!.view()).toStrictEqual({bold: true}); }); test('can insert two slices', () => { @@ -48,8 +48,8 @@ describe('.ins()', () => { const slice2 = editor.insertSlice('i', {italic: true}); peritext.refresh(); expect(peritext.slices.size()).toBe(2); - expect(slice1.data()).toStrictEqual({bold: true}); - expect(slice2.data()).toStrictEqual({italic: true}); + expect(slice1.data()!.view()).toStrictEqual({bold: true}); + expect(slice2.data()!.view()).toStrictEqual({italic: true}); }); test('updates hash on slice insert', () => { @@ -85,7 +85,7 @@ describe('.ins()', () => { }); test('can store all different slice range and metadata combinations', () => { - const {peritext, model} = setup(); + const {peritext} = setup(); const r1 = peritext.range(peritext.pointAt(4, Anchor.Before), peritext.pointAt(6, Anchor.After)); const r2 = peritext.range(peritext.pointAt(2, Anchor.After), peritext.pointAt(8, Anchor.After)); const r3 = peritext.range(peritext.pointAt(2, Anchor.After), peritext.pointAt(8, Anchor.Before)); @@ -98,12 +98,13 @@ describe('.ins()', () => { for (const type of types) { for (const data of datas) { for (const behavior of behaviors) { + const {peritext, model} = setup(); const slice = peritext.slices.ins(range, behavior, type, data); expect(slice.start.cmp(range.start)).toBe(0); expect(slice.end.cmp(range.end)).toBe(0); expect(slice.behavior).toBe(behavior); expect(slice.type).toStrictEqual(type); - expect(slice.data()).toStrictEqual(data); + expect(slice.data()?.view()).toStrictEqual(data); const buf = model.toBinary(); const model2 = Model.fromBinary(buf); const peritext2 = new Peritext(model2, model2.api.str(['text']).node, model2.api.arr(['slices']).node); @@ -113,7 +114,7 @@ describe('.ins()', () => { expect(slice2.end.cmp(range.end)).toBe(0); expect(slice2.behavior).toBe(behavior); expect(slice2.type).toStrictEqual(type); - expect(slice2.data()).toStrictEqual(data); + expect(slice2.data()?.view()).toStrictEqual(data); } } } @@ -121,16 +122,34 @@ describe('.ins()', () => { }); }); -describe('deletes', () => { +describe('.del()', () => { + test('can delete a slice', () => { + const {peritext} = setup(); + const {editor} = peritext; + editor.cursor.setAt(6, 5); + const slice1 = editor.insertSlice('b', {bold: true}); + peritext.refresh(); + const hash1 = peritext.slices.hash; + expect(peritext.slices.size()).toBe(1); + peritext.slices.del(slice1.id); + peritext.refresh(); + const hash2 = peritext.slices.hash; + expect(peritext.slices.size()).toBe(0); + expect(hash1).not.toBe(hash2); + }); +}); + +describe('.delSlices()', () => { test('can delete a slice', () => { const {peritext} = setup(); const {editor} = peritext; editor.cursor.setAt(6, 5); const slice1 = editor.insertSlice('b', {bold: true}); + console.log(slice1 + ''); peritext.refresh(); const hash1 = peritext.slices.hash; expect(peritext.slices.size()).toBe(1); - peritext.delSlice(slice1.id); + peritext.slices.delSlices([slice1]); peritext.refresh(); const hash2 = peritext.slices.hash; expect(peritext.slices.size()).toBe(0); @@ -138,7 +157,7 @@ describe('deletes', () => { }); }); -describe('tag changes', () => { +describe('.refresh()', () => { test('recomputes hash on tag change', () => { const {peritext} = setup(); const {editor} = peritext; diff --git a/src/json-crdt-extensions/peritext/slice/types.ts b/src/json-crdt-extensions/peritext/slice/types.ts index c617d8a575..e7189158a9 100644 --- a/src/json-crdt-extensions/peritext/slice/types.ts +++ b/src/json-crdt-extensions/peritext/slice/types.ts @@ -2,6 +2,7 @@ import type {Range} from '../rga/Range'; import type {SliceType, Stateful} from '../types'; import type {ITimestampStruct} from '../../../json-crdt-patch/clock'; import type {SliceBehavior} from './constants'; +import type {JsonNode} from '../../../json-crdt/nodes'; export interface Slice extends Range, Stateful { /** @@ -27,7 +28,7 @@ export interface Slice extends Range, Stateful { * High-level user-defined metadata of the slice, which accompanies the slice * type. */ - data(): unknown; + data(): JsonNode | undefined; /** * Whether the slice is deleted. From 423fe16c9237cc8212f03d22a973ceef16d189f2 Mon Sep 17 00:00:00 2001 From: streamich Date: Sat, 20 Apr 2024 08:28:32 +0200 Subject: [PATCH 14/22] =?UTF-8?q?feat(json-crdt-extensions):=20?= =?UTF-8?q?=F0=9F=8E=B8=20make=20Cursor=20generic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../peritext/slice/Cursor.ts | 23 ++++++++----------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/src/json-crdt-extensions/peritext/slice/Cursor.ts b/src/json-crdt-extensions/peritext/slice/Cursor.ts index 9f016ef8ca..0213522bf8 100644 --- a/src/json-crdt-extensions/peritext/slice/Cursor.ts +++ b/src/json-crdt-extensions/peritext/slice/Cursor.ts @@ -2,12 +2,12 @@ import {Point} from '../rga/Point'; import {CursorAnchor, SliceBehavior, Tags} from './constants'; import {Range} from '../rga/Range'; import {printTree} from '../../../util/print/printTree'; +import {updateNum} from '../../../json-hash'; import type {ITimestampStruct} from '../../../json-crdt-patch/clock'; import type {Peritext} from '../Peritext'; import type {Slice} from './types'; -import {updateNum} from '../../../json-hash'; -export class Cursor extends Range implements Slice { +export class Cursor extends Range implements Slice { public readonly behavior = SliceBehavior.Overwrite; public readonly type = Tags.Cursor; @@ -22,21 +22,21 @@ export class Cursor extends Range implements Slice { constructor( public readonly id: ITimestampStruct, protected readonly txt: Peritext, - public start: Point, - public end: Point, + public start: Point, + public end: Point, ) { - super(txt.str, start, end); + super(txt.str as any, start, end); } - public anchor(): Point { + public anchor(): Point { return this.anchorSide === CursorAnchor.Start ? this.start : this.end; } - public focus(): Point { + public focus(): Point { return this.anchorSide === CursorAnchor.Start ? this.end : this.start; } - public set(start: Point, end?: Point, base: CursorAnchor = CursorAnchor.Start): void { + public set(start: Point, end?: Point, base: CursorAnchor = CursorAnchor.Start): void { if (!end || end === start) end = start.clone(); super.set(start, end); this.anchorSide = base; @@ -59,7 +59,7 @@ export class Cursor extends Range implements Slice { * @param point Point to set the edge to. * @param edge 0 for "focus", 1 for "anchor." */ - public setEdge(point: Point, edge: 0 | 1 = 0): void { + public setEdge(point: Point, edge: 0 | 1 = 0): void { if (this.start === this.end) this.end = this.end.clone(); let anchor = this.anchor(); let focus = this.focus(); @@ -76,11 +76,6 @@ export class Cursor extends Range implements Slice { } } - /** @todo Maybe move it to another interface? */ - public del(): boolean { - return false; - } - public data() { return undefined; } From 894cddde90f3945d544708a3bdc59b07e2d5d403 Mon Sep 17 00:00:00 2001 From: streamich Date: Sat, 20 Apr 2024 08:30:47 +0200 Subject: [PATCH 15/22] =?UTF-8?q?feat(json-crdt-extensions):=20?= =?UTF-8?q?=F0=9F=8E=B8=20pretty=20print=20one-line=20JSON?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/json-crdt-extensions/peritext/slice/PersistedSlice.ts | 3 +-- src/json-pretty/index.ts | 7 +++++++ 2 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 src/json-pretty/index.ts diff --git a/src/json-crdt-extensions/peritext/slice/PersistedSlice.ts b/src/json-crdt-extensions/peritext/slice/PersistedSlice.ts index c5a9d51384..f0107d8b6c 100644 --- a/src/json-crdt-extensions/peritext/slice/PersistedSlice.ts +++ b/src/json-crdt-extensions/peritext/slice/PersistedSlice.ts @@ -130,8 +130,7 @@ export class PersistedSlice extends Range implements Slice, St const data = this.data(); const header = `${this.constructor.name} ${super.toString(tab)}, ${this.behavior}, ${JSON.stringify(this.type)}`; return header + printTree(tab, [ - // !data ? null : (tab) => JSON.stringify(data.view()).replace(/([\{\[\:])/g, '$1 ').replace(/([\}\]])/g, ' $1'), - !data ? null : (tab) => JSON.stringify(data.view()), + !data ? null : (tab) => prettyOneLine(data.view()), ]); } } diff --git a/src/json-pretty/index.ts b/src/json-pretty/index.ts new file mode 100644 index 0000000000..f07429d6d2 --- /dev/null +++ b/src/json-pretty/index.ts @@ -0,0 +1,7 @@ +export const prettyOneLine = (value: unknown): string => { + let json = JSON.stringify(value); + json = json + .replace(/([\{\[\:\,])/g, '$1 ') + .replace(/([\}\]])/g, ' $1'); + return json; +}; From 1ed13b8236f3efbd6e266749bffa83bcd8a12094 Mon Sep 17 00:00:00 2001 From: streamich Date: Sat, 20 Apr 2024 09:11:03 +0200 Subject: [PATCH 16/22] =?UTF-8?q?feat(json-crdt-extensions):=20?= =?UTF-8?q?=F0=9F=8E=B8=20improve=20slices=20interface?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../peritext/slice/PersistedSlice.ts | 61 +++++++++---------- .../peritext/slice/types.ts | 19 +++++- .../peritext/slice/util.ts | 27 ++++++++ 3 files changed, 72 insertions(+), 35 deletions(-) create mode 100644 src/json-crdt-extensions/peritext/slice/util.ts diff --git a/src/json-crdt-extensions/peritext/slice/PersistedSlice.ts b/src/json-crdt-extensions/peritext/slice/PersistedSlice.ts index f0107d8b6c..07ac114a1c 100644 --- a/src/json-crdt-extensions/peritext/slice/PersistedSlice.ts +++ b/src/json-crdt-extensions/peritext/slice/PersistedSlice.ts @@ -3,46 +3,23 @@ import {Range} from '../rga/Range'; import {updateNode} from '../../../json-crdt/hash'; import {printTree} from '../../../util/print/printTree'; import {Anchor} from '../rga/constants'; -import {SliceHeaderMask, SliceHeaderShift, SliceBehavior} from './constants'; +import {SliceHeaderMask, SliceHeaderShift, SliceBehavior, SliceTupleIndex} from './constants'; import {CONST} from '../../../json-hash'; import {Timestamp} from '../../../json-crdt-patch/clock'; import {VecNode} from '../../../json-crdt/nodes'; +import {prettyOneLine} from '../../../json-pretty'; +import {validateType} from './util'; +import {s} from '../../../json-crdt-patch'; import type {JsonNode} from '../../../json-crdt/nodes'; import type {ITimestampStruct} from '../../../json-crdt-patch/clock'; import type {ArrChunk} from '../../../json-crdt/nodes'; -import type {Slice} from './types'; +import type {MutableSlice} from './types'; import type {Peritext} from '../Peritext'; import type {SliceDto, SliceType, Stateful} from '../types'; import type {Printable} from '../../../util/print/types'; import type {AbstractRga} from '../../../json-crdt/nodes/rga'; -export const validateType = (type: SliceType) => { - switch (typeof type) { - case 'string': - case 'number': - return; - case 'object': { - if (!(type instanceof Array)) throw new Error('INVALID_TYPE'); - if (type.length > 32) throw new Error('INVALID_TYPE'); - const length = type.length; - LOOP: for (let i = 0; i < length; i++) { - const step = type[i]; - switch (typeof step) { - case 'string': - case 'number': - continue LOOP; - default: - throw new Error('INVALID_TYPE'); - } - } - return; - } - default: - throw new Error('INVALID_TYPE'); - } -}; - -export class PersistedSlice extends Range implements Slice, Stateful, Printable { +export class PersistedSlice extends Range implements MutableSlice, Stateful, Printable { public static deserialize(txt: Peritext, rga: AbstractRga, chunk: ArrChunk, tuple: VecNode): PersistedSlice { const header = +(tuple.get(0)!.view() as SliceDto[0]); const id1 = tuple.get(1)!.view() as ITimestampStruct; @@ -90,17 +67,35 @@ export class PersistedSlice extends Range implements Slice, St return this.tuple.get(3); } + protected tupleApi() { + return this.txt.model.api.wrap(this.tuple); + } + // -------------------------------------------------------------------- Slice public readonly id: ITimestampStruct; public behavior: SliceBehavior; public type: SliceType; - public data(): JsonNode | undefined { - return this.tuple.get(4); + public setType(type: SliceType): void { + this.type = type; + this.tupleApi().set([[SliceTupleIndex.Subtype, s.con(type)]]); + } + + public data(): unknown | undefined { + return this.tuple.get(4)?.view(); + } + + public setData(data: unknown): void { + this.tupleApi().set([[SliceTupleIndex.Data, data]]); + } + + public dataNode() { + const node = this.tuple.get(SliceTupleIndex.Data); + return node && this.txt.model.api.wrap(node); } - public del(): boolean { + public isDel(): boolean { return this.chunk.del; } @@ -130,7 +125,7 @@ export class PersistedSlice extends Range implements Slice, St const data = this.data(); const header = `${this.constructor.name} ${super.toString(tab)}, ${this.behavior}, ${JSON.stringify(this.type)}`; return header + printTree(tab, [ - !data ? null : (tab) => prettyOneLine(data.view()), + !data ? null : (tab) => prettyOneLine(data), ]); } } diff --git a/src/json-crdt-extensions/peritext/slice/types.ts b/src/json-crdt-extensions/peritext/slice/types.ts index e7189158a9..f3ebf8ba64 100644 --- a/src/json-crdt-extensions/peritext/slice/types.ts +++ b/src/json-crdt-extensions/peritext/slice/types.ts @@ -28,10 +28,25 @@ export interface Slice extends Range, Stateful { * High-level user-defined metadata of the slice, which accompanies the slice * type. */ - data(): JsonNode | undefined; + data(): unknown | undefined; +} + +export interface MutableSlice extends Slice { + /** + * Sets the type of the slice. + * + * @param type The new type of the slice. + */ + setType(type: SliceType): void; + + // /** + // * High-level user-defined metadata of the slice, which accompanies the slice + // * type. + // */ + // dataNode(): JsonNode | undefined; /** * Whether the slice is deleted. */ - del(): boolean; + isDel(): boolean; } diff --git a/src/json-crdt-extensions/peritext/slice/util.ts b/src/json-crdt-extensions/peritext/slice/util.ts new file mode 100644 index 0000000000..21978d0b32 --- /dev/null +++ b/src/json-crdt-extensions/peritext/slice/util.ts @@ -0,0 +1,27 @@ +import type {SliceType} from "../types"; + +export const validateType = (type: SliceType) => { + switch (typeof type) { + case 'string': + case 'number': + return; + case 'object': { + if (!(type instanceof Array)) throw new Error('INVALID_TYPE'); + if (type.length > 32) throw new Error('INVALID_TYPE'); + const length = type.length; + LOOP: for (let i = 0; i < length; i++) { + const step = type[i]; + switch (typeof step) { + case 'string': + case 'number': + continue LOOP; + default: + throw new Error('INVALID_TYPE'); + } + } + return; + } + default: + throw new Error('INVALID_TYPE'); + } +}; From ba77d200dc03b49c9b61dddd605971c1d7c76841 Mon Sep 17 00:00:00 2001 From: streamich Date: Sat, 20 Apr 2024 10:24:47 +0200 Subject: [PATCH 17/22] =?UTF-8?q?feat(json-crdt-extensions):=20?= =?UTF-8?q?=F0=9F=8E=B8=20add=20slice=20update=20method?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../peritext/slice/PersistedSlice.ts | 49 +++-- .../peritext/slice/__tests__/Slices.spec.ts | 167 +++++++++++++++++- .../peritext/slice/types.ts | 21 +-- 3 files changed, 207 insertions(+), 30 deletions(-) diff --git a/src/json-crdt-extensions/peritext/slice/PersistedSlice.ts b/src/json-crdt-extensions/peritext/slice/PersistedSlice.ts index 07ac114a1c..cc45e70230 100644 --- a/src/json-crdt-extensions/peritext/slice/PersistedSlice.ts +++ b/src/json-crdt-extensions/peritext/slice/PersistedSlice.ts @@ -5,7 +5,7 @@ import {printTree} from '../../../util/print/printTree'; import {Anchor} from '../rga/constants'; import {SliceHeaderMask, SliceHeaderShift, SliceBehavior, SliceTupleIndex} from './constants'; import {CONST} from '../../../json-hash'; -import {Timestamp} from '../../../json-crdt-patch/clock'; +import {Timestamp, compare} from '../../../json-crdt-patch/clock'; import {VecNode} from '../../../json-crdt/nodes'; import {prettyOneLine} from '../../../json-pretty'; import {validateType} from './util'; @@ -13,7 +13,7 @@ import {s} from '../../../json-crdt-patch'; import type {JsonNode} from '../../../json-crdt/nodes'; import type {ITimestampStruct} from '../../../json-crdt-patch/clock'; import type {ArrChunk} from '../../../json-crdt/nodes'; -import type {MutableSlice} from './types'; +import type {MutableSlice, SliceUpdateParams} from './types'; import type {Peritext} from '../Peritext'; import type {SliceDto, SliceType, Stateful} from '../types'; import type {Printable} from '../../../util/print/types'; @@ -71,30 +71,59 @@ export class PersistedSlice extends Range implements MutableSlice return this.txt.model.api.wrap(this.tuple); } - // -------------------------------------------------------------------- Slice + // ------------------------------------------------------------- MutableSlice public readonly id: ITimestampStruct; public behavior: SliceBehavior; public type: SliceType; - public setType(type: SliceType): void { - this.type = type; - this.tupleApi().set([[SliceTupleIndex.Subtype, s.con(type)]]); + public update(params: SliceUpdateParams): void { + let updateHeader = false; + let {start, end} = this; + const changes: [number, unknown][] = []; + if (params.behavior !== undefined) { + this.behavior = params.behavior; + updateHeader = true; + } + if (params.range) { + const range = params.range; + if (range.start.anchor !== start.anchor) updateHeader = true; + if (range.end.anchor !== end.anchor) updateHeader = true; + if (compare(range.start.id, start.id) !== 0) + changes.push([SliceTupleIndex.X1, s.con(range.start.id)]); + if (compare(range.end.id, end.id) !== 0) + changes.push([SliceTupleIndex.X2, s.con(range.end.id)]); + this.setRange(range); + } + if (params.type !== undefined) { + this.type = params.type; + changes.push([SliceTupleIndex.Type, s.con(this.type)]); + } + if (params.data !== undefined) + changes.push([SliceTupleIndex.Data, s.con(params.data)]); + if (updateHeader) { + const header = + (this.behavior << SliceHeaderShift.Behavior) + + (this.start.anchor << SliceHeaderShift.X1Anchor) + + (this.end.anchor << SliceHeaderShift.X2Anchor); + changes.push([SliceTupleIndex.Header, s.con(header)]); + } + this.tupleApi().set(changes); } public data(): unknown | undefined { return this.tuple.get(4)?.view(); } - public setData(data: unknown): void { - this.tupleApi().set([[SliceTupleIndex.Data, data]]); - } - public dataNode() { const node = this.tuple.get(SliceTupleIndex.Data); return node && this.txt.model.api.wrap(node); } + public del(): void { + this.txt.slices.del(this.id); + } + public isDel(): boolean { return this.chunk.del; } diff --git a/src/json-crdt-extensions/peritext/slice/__tests__/Slices.spec.ts b/src/json-crdt-extensions/peritext/slice/__tests__/Slices.spec.ts index 34e0a5327d..9e090ebc39 100644 --- a/src/json-crdt-extensions/peritext/slice/__tests__/Slices.spec.ts +++ b/src/json-crdt-extensions/peritext/slice/__tests__/Slices.spec.ts @@ -1,4 +1,4 @@ -import {Model} from '../../../../json-crdt/model'; +import {Model, ObjApi} from '../../../../json-crdt/model'; import {Peritext} from '../../Peritext'; import {Anchor} from '../../rga/constants'; import {SliceBehavior} from '../constants'; @@ -16,7 +16,13 @@ const setup = () => { model.api.str(['text']).ins(11, ' this game is awesome'); const peritext = new Peritext(model, model.api.str(['text']).node, model.api.arr(['slices']).node); const slices = peritext.slices; - return {model, peritext, slices}; + const encodeAndDecode = () => { + const buf = model.toBinary(); + const model2 = Model.fromBinary(buf); + const peritext2 = new Peritext(model2, model2.api.str(['text']).node, model2.api.arr(['slices']).node); + return {model2, peritext2}; + }; + return {model, peritext, slices, encodeAndDecode}; }; test('initially slice list is empty', () => { @@ -36,7 +42,7 @@ describe('.ins()', () => { expect(slice.end).toStrictEqual(range.end); expect(slice.behavior).toBe(SliceBehavior.Stack); expect(slice.type).toBe('b'); - expect(slice.data()!.view()).toStrictEqual({bold: true}); + expect(slice.data()).toStrictEqual({bold: true}); }); test('can insert two slices', () => { @@ -48,8 +54,8 @@ describe('.ins()', () => { const slice2 = editor.insertSlice('i', {italic: true}); peritext.refresh(); expect(peritext.slices.size()).toBe(2); - expect(slice1.data()!.view()).toStrictEqual({bold: true}); - expect(slice2.data()!.view()).toStrictEqual({italic: true}); + expect(slice1.data()).toStrictEqual({bold: true}); + expect(slice2.data()).toStrictEqual({italic: true}); }); test('updates hash on slice insert', () => { @@ -104,7 +110,7 @@ describe('.ins()', () => { expect(slice.end.cmp(range.end)).toBe(0); expect(slice.behavior).toBe(behavior); expect(slice.type).toStrictEqual(type); - expect(slice.data()?.view()).toStrictEqual(data); + expect(slice.data()).toStrictEqual(data); const buf = model.toBinary(); const model2 = Model.fromBinary(buf); const peritext2 = new Peritext(model2, model2.api.str(['text']).node, model2.api.arr(['slices']).node); @@ -114,7 +120,7 @@ describe('.ins()', () => { expect(slice2.end.cmp(range.end)).toBe(0); expect(slice2.behavior).toBe(behavior); expect(slice2.type).toStrictEqual(type); - expect(slice2.data()?.view()).toStrictEqual(data); + expect(slice2.data()).toStrictEqual(data); } } } @@ -122,6 +128,16 @@ describe('.ins()', () => { }); }); +describe('.get()', () => { + test('can retrieve slice by id', () => { + const {peritext} = setup(); + const range = peritext.rangeAt(6, 5); + const slice = peritext.slices.insOverwrite(range, 'italic'); + const slice2 = peritext.slices.get(slice.id); + expect(slice2).toBe(slice); + }); +}); + describe('.del()', () => { test('can delete a slice', () => { const {peritext} = setup(); @@ -145,7 +161,6 @@ describe('.delSlices()', () => { const {editor} = peritext; editor.cursor.setAt(6, 5); const slice1 = editor.insertSlice('b', {bold: true}); - console.log(slice1 + ''); peritext.refresh(); const hash1 = peritext.slices.hash; expect(peritext.slices.size()).toBe(1); @@ -158,6 +173,142 @@ describe('.delSlices()', () => { }); describe('.refresh()', () => { + test('changes hash on slice behavior change', () => { + const {peritext, encodeAndDecode} = setup(); + const range = peritext.rangeAt(6, 5); + const slice = peritext.slices.insOverwrite(range, 'b', {howBold: 'very'}); + const hash1 = peritext.slices.refresh(); + const hash2 = peritext.slices.refresh(); + expect(hash1).toBe(hash2); + expect(slice.behavior).toBe(SliceBehavior.Overwrite); + slice.update({behavior: SliceBehavior.Stack}); + expect(slice.behavior).toBe(SliceBehavior.Stack); + const hash3 = peritext.slices.refresh(); + const hash4 = peritext.slices.refresh(); + expect(hash3).not.toBe(hash2); + expect(hash4).toBe(hash3); + const {peritext2} = encodeAndDecode(); + peritext2.refresh(); + const slice2 = peritext2.slices.get(slice.id)!; + expect(slice2.cmp(slice)).toBe(0); + }); + + test('changes hash on slice subtype change', () => { + const {peritext, encodeAndDecode} = setup(); + const range = peritext.rangeAt(6, 5); + const slice = peritext.slices.insOverwrite(range, 'b', {howBold: 'very'}); + const hash1 = peritext.slices.refresh(); + const hash2 = peritext.slices.refresh(); + expect(hash1).toBe(hash2); + expect(slice.type).toBe('b'); + slice.update({type: 123}) + expect(slice.type).toBe(123); + const hash3 = peritext.slices.refresh(); + const hash4 = peritext.slices.refresh(); + expect(hash3).not.toBe(hash2); + expect(hash4).toBe(hash3); + const {peritext2} = encodeAndDecode(); + peritext2.refresh(); + const slice2 = peritext2.slices.get(slice.id)!; + expect(slice2.cmp(slice)).toBe(0); + }); + + test('changes hash on slice data overwrite', () => { + const {peritext, encodeAndDecode} = setup(); + const range = peritext.rangeAt(6, 5); + const slice = peritext.slices.insOverwrite(range, 'b', {howBold: 'very'}); + const hash1 = peritext.slices.refresh(); + const hash2 = peritext.slices.refresh(); + expect(hash1).toBe(hash2); + expect(slice.data()).toEqual({howBold: 'very'}); + slice.update({data: 'the data'}); + expect(slice.data()).toEqual('the data'); + const hash3 = peritext.slices.refresh(); + const hash4 = peritext.slices.refresh(); + expect(hash3).not.toBe(hash2); + expect(hash4).toBe(hash3); + const {peritext2} = encodeAndDecode(); + peritext2.refresh(); + const slice2 = peritext2.slices.get(slice.id)!; + expect(slice2.cmp(slice)).toBe(0); + }); + + test('changes hash on start anchor change', () => { + const {peritext, encodeAndDecode} = setup(); + const range = peritext.rangeAt(6, 5); + const slice = peritext.slices.insOverwrite(range, 'b', {howBold: 'very'}); + const hash1 = peritext.slices.refresh(); + const hash2 = peritext.slices.refresh(); + expect(hash1).toBe(hash2); + range.start.anchor = Anchor.After; + slice.update({range}); + const hash3 = peritext.slices.refresh(); + const hash4 = peritext.slices.refresh(); + expect(hash3).not.toBe(hash2); + expect(hash4).toBe(hash3); + const {peritext2} = encodeAndDecode(); + peritext2.refresh(); + const slice2 = peritext2.slices.get(slice.id)!; + expect(slice2.cmp(slice)).toBe(0); + }); + + test('changes hash on end anchor change', () => { + const {peritext, encodeAndDecode} = setup(); + const range = peritext.rangeAt(6, 5); + const slice = peritext.slices.insOverwrite(range, 'b', {howBold: 'very'}); + const hash1 = peritext.slices.refresh(); + const hash2 = peritext.slices.refresh(); + expect(hash1).toBe(hash2); + range.end.anchor = Anchor.Before; + slice.update({range}); + const hash3 = peritext.slices.refresh(); + const hash4 = peritext.slices.refresh(); + expect(hash3).not.toBe(hash2); + expect(hash4).toBe(hash3); + const {peritext2} = encodeAndDecode(); + peritext2.refresh(); + const slice2 = peritext2.slices.get(slice.id)!; + expect(slice2.cmp(slice)).toBe(0); + }); + + test('changes hash on start position change', () => { + const {peritext, encodeAndDecode} = setup(); + const range = peritext.rangeAt(6, 5); + const slice = peritext.slices.insOverwrite(range, 'b', {howBold: 'very'}); + const hash1 = peritext.slices.refresh(); + const hash2 = peritext.slices.refresh(); + expect(hash1).toBe(hash2); + range.start.id = range.start.nextId()!; + slice.update({range}); + const hash3 = peritext.slices.refresh(); + const hash4 = peritext.slices.refresh(); + expect(hash3).not.toBe(hash2); + expect(hash4).toBe(hash3); + const {peritext2} = encodeAndDecode(); + peritext2.refresh(); + const slice2 = peritext2.slices.get(slice.id)!; + expect(slice2.cmp(slice)).toBe(0); + }); + + test('changes hash on end position change', () => { + const {peritext, encodeAndDecode} = setup(); + const range = peritext.rangeAt(6, 5); + const slice = peritext.slices.insOverwrite(range, 'b', {howBold: 'very'}); + const hash1 = peritext.slices.refresh(); + const hash2 = peritext.slices.refresh(); + expect(hash1).toBe(hash2); + range.end.id = range.start.prevId()!; + slice.update({range}); + const hash3 = peritext.slices.refresh(); + const hash4 = peritext.slices.refresh(); + expect(hash3).not.toBe(hash2); + expect(hash4).toBe(hash3); + const {peritext2} = encodeAndDecode(); + peritext2.refresh(); + const slice2 = peritext2.slices.get(slice.id)!; + expect(slice2.cmp(slice)).toBe(0); + }); + test('recomputes hash on tag change', () => { const {peritext} = setup(); const {editor} = peritext; diff --git a/src/json-crdt-extensions/peritext/slice/types.ts b/src/json-crdt-extensions/peritext/slice/types.ts index f3ebf8ba64..6afdd511de 100644 --- a/src/json-crdt-extensions/peritext/slice/types.ts +++ b/src/json-crdt-extensions/peritext/slice/types.ts @@ -2,7 +2,6 @@ import type {Range} from '../rga/Range'; import type {SliceType, Stateful} from '../types'; import type {ITimestampStruct} from '../../../json-crdt-patch/clock'; import type {SliceBehavior} from './constants'; -import type {JsonNode} from '../../../json-crdt/nodes'; export interface Slice extends Range, Stateful { /** @@ -32,21 +31,19 @@ export interface Slice extends Range, Stateful { } export interface MutableSlice extends Slice { - /** - * Sets the type of the slice. - * - * @param type The new type of the slice. - */ - setType(type: SliceType): void; + update(params: SliceUpdateParams): void; - // /** - // * High-level user-defined metadata of the slice, which accompanies the slice - // * type. - // */ - // dataNode(): JsonNode | undefined; + del(): void; /** * Whether the slice is deleted. */ isDel(): boolean; } + +export interface SliceUpdateParams { + behavior?: SliceBehavior; + type?: SliceType; + data?: unknown; + range?: Range; +} From 3c50c725356715ec82ac1cac45f548c394363a8c Mon Sep 17 00:00:00 2001 From: streamich Date: Sat, 20 Apr 2024 10:25:31 +0200 Subject: [PATCH 18/22] =?UTF-8?q?feat(json-crdt-extensions):=20?= =?UTF-8?q?=F0=9F=8E=B8=20improve=20method=20setup=20and=20constants?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/json-crdt-extensions/peritext/Peritext.ts | 15 --------- .../peritext/editor/Editor.ts | 6 ++-- .../peritext/rga/Range.ts | 8 +++++ .../peritext/slice/Slices.ts | 32 ++++++++++++++----- .../peritext/slice/constants.ts | 8 +++++ 5 files changed, 43 insertions(+), 26 deletions(-) diff --git a/src/json-crdt-extensions/peritext/Peritext.ts b/src/json-crdt-extensions/peritext/Peritext.ts index 72bb9fe4b7..2005c7e81b 100644 --- a/src/json-crdt-extensions/peritext/Peritext.ts +++ b/src/json-crdt-extensions/peritext/Peritext.ts @@ -1,5 +1,4 @@ import {Anchor} from './rga/constants'; -import {SliceBehavior} from './slice/constants'; import {Point} from './rga/Point'; import {Range} from './rga/Range'; import {Editor} from './editor/Editor'; @@ -9,8 +8,6 @@ import {Slices} from './slice/Slices'; import {type ITimestampStruct} from '../../json-crdt-patch/clock'; import type {Model} from '../../json-crdt/model'; import type {Printable} from '../../util/print/types'; -import type {SliceType} from './types'; -import type {PersistedSlice} from './slice/PersistedSlice'; /** * Context for a Peritext instance. Contains all the data and methods needed to @@ -148,18 +145,6 @@ export class Peritext implements Printable { return textId; } - public insSlice( - range: Range, - behavior: SliceBehavior, - type: SliceType, - data?: unknown | ITimestampStruct, - ): PersistedSlice { - // if (range.isCollapsed()) throw new Error('INVALID_RANGE'); - // TODO: If range is not collapsed, check if there are any visible characters in the range. - const slice = this.slices.ins(range, behavior, type, data); - return slice; - } - /** Select a single character before a point. */ public findCharBefore(point: Point): Range | undefined { if (point.anchor === Anchor.After) { diff --git a/src/json-crdt-extensions/peritext/editor/Editor.ts b/src/json-crdt-extensions/peritext/editor/Editor.ts index 56dfe1e9e1..de975f2133 100644 --- a/src/json-crdt-extensions/peritext/editor/Editor.ts +++ b/src/json-crdt-extensions/peritext/editor/Editor.ts @@ -122,14 +122,14 @@ export class Editor implements Printable { } public insertSlice(type: SliceType, data?: unknown | ITimestampStruct): PersistedSlice { - return this.txt.insSlice(this.cursor, SliceBehavior.Stack, type, data); + return this.txt.slices.ins(this.cursor, SliceBehavior.Stack, type, data); } public insertOverwriteSlice(type: SliceType, data?: unknown | ITimestampStruct): PersistedSlice { - return this.txt.insSlice(this.cursor, SliceBehavior.Overwrite, type, data); + return this.txt.slices.ins(this.cursor, SliceBehavior.Overwrite, type, data); } public insertEraseSlice(type: SliceType, data?: unknown | ITimestampStruct): PersistedSlice { - return this.txt.insSlice(this.cursor, SliceBehavior.Erase, type, data); + return this.txt.slices.ins(this.cursor, SliceBehavior.Erase, type, data); } } diff --git a/src/json-crdt-extensions/peritext/rga/Range.ts b/src/json-crdt-extensions/peritext/rga/Range.ts index faa56f503e..3759215c35 100644 --- a/src/json-crdt-extensions/peritext/rga/Range.ts +++ b/src/json-crdt-extensions/peritext/rga/Range.ts @@ -94,6 +94,14 @@ export class Range implements Pick, Printable { return new Range(this.rga, this.start.clone(), this.end.clone()); } + public cmp(range: Range): -1 | 0 | 1 { + return this.start.cmp(range.start) || this.end.cmp(range.end); + } + + public cmpSpatial(range: Range): number { + return this.start.cmpSpatial(range.start) || this.end.cmpSpatial(range.end); + } + /** * Determines if the range is collapsed to a single point. Handles special * cases where the range is collapsed, but the points are not equal, for diff --git a/src/json-crdt-extensions/peritext/slice/Slices.ts b/src/json-crdt-extensions/peritext/slice/Slices.ts index c623f49c84..e46d830164 100644 --- a/src/json-crdt-extensions/peritext/slice/Slices.ts +++ b/src/json-crdt-extensions/peritext/slice/Slices.ts @@ -4,7 +4,7 @@ import {Range} from '../rga/Range'; import {updateRga} from '../../../json-crdt/hash'; import {CONST, updateNum} from '../../../json-hash'; import {printTree} from '../../../util/print/printTree'; -import {SliceBehavior, SliceHeaderShift} from './constants'; +import {SliceBehavior, SliceHeaderShift, SliceTupleIndex} from './constants'; import {SplitSlice} from './SplitSlice'; import {VecNode} from '../../../json-crdt/nodes'; import {AvlMap} from '../../../util/trees/avl/AvlMap'; @@ -30,8 +30,8 @@ export class Slices implements Stateful, Printable { const api = model.api; const builder = api.builder; const tupleId = builder.vec(); - const start = range.start; - const end = range.end; + const start = range.start.clone(); + const end = range.end.clone(); const header = (behavior << SliceHeaderShift.Behavior) + (start.anchor << SliceHeaderShift.X1Anchor) + @@ -41,12 +41,12 @@ export class Slices implements Stateful, Printable { const x2Id = builder.const(compare(start.id, end.id) === 0 ? 0 : end.id); const subtypeId = builder.const(type); const tupleKeysUpdate: [key: number, value: ITimestampStruct][] = [ - [0, headerId], - [1, x1Id], - [2, x2Id], - [3, subtypeId], + [SliceTupleIndex.Header, headerId], + [SliceTupleIndex.X1, x1Id], + [SliceTupleIndex.X2, x2Id], + [SliceTupleIndex.Type, subtypeId], ]; - if (data !== undefined) tupleKeysUpdate.push([4, builder.json(data)]); + if (data !== undefined) tupleKeysUpdate.push([SliceTupleIndex.Data, builder.json(data)]); builder.insVec(tupleId, tupleKeysUpdate); const chunkId = builder.insArr(set.id, set.id, [tupleId]); api.apply(); @@ -62,6 +62,22 @@ export class Slices implements Stateful, Printable { return slice; } + public insSplit(range: Range, type: SliceType, data?: unknown): SplitSlice { + return this.ins(range, SliceBehavior.Split, type, data) as SplitSlice; + } + + public insStack(range: Range, type: SliceType, data?: unknown): PersistedSlice { + return this.ins(range, SliceBehavior.Stack, type, data); + } + + public insOverwrite(range: Range, type: SliceType, data?: unknown): PersistedSlice { + return this.ins(range, SliceBehavior.Overwrite, type, data); + } + + public insErase(range: Range, type: SliceType, data?: unknown): PersistedSlice { + return this.ins(range, SliceBehavior.Erase, type, data); + } + protected unpack(chunk: ArrChunk): PersistedSlice { const txt = this.txt; const rga = txt.str; diff --git a/src/json-crdt-extensions/peritext/slice/constants.ts b/src/json-crdt-extensions/peritext/slice/constants.ts index 39cb16fa7a..cbba411386 100644 --- a/src/json-crdt-extensions/peritext/slice/constants.ts +++ b/src/json-crdt-extensions/peritext/slice/constants.ts @@ -49,3 +49,11 @@ export const enum SliceBehavior { */ Erase = 0b011, } + +export const enum SliceTupleIndex { + Header = 0, + X1 = 1, + X2 = 2, + Type = 3, + Data = 4, +} From bebfbbb8a058cbbdeb686bc534c92acf1014f00e Mon Sep 17 00:00:00 2001 From: streamich Date: Sat, 20 Apr 2024 10:26:16 +0200 Subject: [PATCH 19/22] =?UTF-8?q?style(json-crdt-extensions):=20?= =?UTF-8?q?=F0=9F=92=84=20run=20Prettier?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../peritext/slice/PersistedSlice.ts | 13 ++++--------- .../peritext/slice/__tests__/Slices.spec.ts | 2 +- src/json-crdt-extensions/peritext/slice/util.ts | 2 +- src/json-pretty/index.ts | 4 +--- 4 files changed, 7 insertions(+), 14 deletions(-) diff --git a/src/json-crdt-extensions/peritext/slice/PersistedSlice.ts b/src/json-crdt-extensions/peritext/slice/PersistedSlice.ts index cc45e70230..295bb98716 100644 --- a/src/json-crdt-extensions/peritext/slice/PersistedSlice.ts +++ b/src/json-crdt-extensions/peritext/slice/PersistedSlice.ts @@ -89,18 +89,15 @@ export class PersistedSlice extends Range implements MutableSlice const range = params.range; if (range.start.anchor !== start.anchor) updateHeader = true; if (range.end.anchor !== end.anchor) updateHeader = true; - if (compare(range.start.id, start.id) !== 0) - changes.push([SliceTupleIndex.X1, s.con(range.start.id)]); - if (compare(range.end.id, end.id) !== 0) - changes.push([SliceTupleIndex.X2, s.con(range.end.id)]); + if (compare(range.start.id, start.id) !== 0) changes.push([SliceTupleIndex.X1, s.con(range.start.id)]); + if (compare(range.end.id, end.id) !== 0) changes.push([SliceTupleIndex.X2, s.con(range.end.id)]); this.setRange(range); } if (params.type !== undefined) { this.type = params.type; changes.push([SliceTupleIndex.Type, s.con(this.type)]); } - if (params.data !== undefined) - changes.push([SliceTupleIndex.Data, s.con(params.data)]); + if (params.data !== undefined) changes.push([SliceTupleIndex.Data, s.con(params.data)]); if (updateHeader) { const header = (this.behavior << SliceHeaderShift.Behavior) + @@ -153,8 +150,6 @@ export class PersistedSlice extends Range implements MutableSlice public toString(tab: string = ''): string { const data = this.data(); const header = `${this.constructor.name} ${super.toString(tab)}, ${this.behavior}, ${JSON.stringify(this.type)}`; - return header + printTree(tab, [ - !data ? null : (tab) => prettyOneLine(data), - ]); + return header + printTree(tab, [!data ? null : (tab) => prettyOneLine(data)]); } } diff --git a/src/json-crdt-extensions/peritext/slice/__tests__/Slices.spec.ts b/src/json-crdt-extensions/peritext/slice/__tests__/Slices.spec.ts index 9e090ebc39..7640883e1c 100644 --- a/src/json-crdt-extensions/peritext/slice/__tests__/Slices.spec.ts +++ b/src/json-crdt-extensions/peritext/slice/__tests__/Slices.spec.ts @@ -201,7 +201,7 @@ describe('.refresh()', () => { const hash2 = peritext.slices.refresh(); expect(hash1).toBe(hash2); expect(slice.type).toBe('b'); - slice.update({type: 123}) + slice.update({type: 123}); expect(slice.type).toBe(123); const hash3 = peritext.slices.refresh(); const hash4 = peritext.slices.refresh(); diff --git a/src/json-crdt-extensions/peritext/slice/util.ts b/src/json-crdt-extensions/peritext/slice/util.ts index 21978d0b32..0a4ad670e3 100644 --- a/src/json-crdt-extensions/peritext/slice/util.ts +++ b/src/json-crdt-extensions/peritext/slice/util.ts @@ -1,4 +1,4 @@ -import type {SliceType} from "../types"; +import type {SliceType} from '../types'; export const validateType = (type: SliceType) => { switch (typeof type) { diff --git a/src/json-pretty/index.ts b/src/json-pretty/index.ts index f07429d6d2..24e2d5f013 100644 --- a/src/json-pretty/index.ts +++ b/src/json-pretty/index.ts @@ -1,7 +1,5 @@ export const prettyOneLine = (value: unknown): string => { let json = JSON.stringify(value); - json = json - .replace(/([\{\[\:\,])/g, '$1 ') - .replace(/([\}\]])/g, ' $1'); + json = json.replace(/([\{\[\:\,])/g, '$1 ').replace(/([\}\]])/g, ' $1'); return json; }; From f6c55b3d436457d76989695f494360efdbf51ea9 Mon Sep 17 00:00:00 2001 From: streamich Date: Sat, 20 Apr 2024 11:02:23 +0200 Subject: [PATCH 20/22] =?UTF-8?q?test(json-crdt-extensions):=20?= =?UTF-8?q?=F0=9F=92=8D=20improve=20slice=20refresh=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../peritext/slice/__tests__/Slices.spec.ts | 141 ++++-------------- 1 file changed, 31 insertions(+), 110 deletions(-) diff --git a/src/json-crdt-extensions/peritext/slice/__tests__/Slices.spec.ts b/src/json-crdt-extensions/peritext/slice/__tests__/Slices.spec.ts index 7640883e1c..27f21fa7eb 100644 --- a/src/json-crdt-extensions/peritext/slice/__tests__/Slices.spec.ts +++ b/src/json-crdt-extensions/peritext/slice/__tests__/Slices.spec.ts @@ -1,6 +1,8 @@ import {Model, ObjApi} from '../../../../json-crdt/model'; import {Peritext} from '../../Peritext'; +import {Range} from '../../rga/Range'; import {Anchor} from '../../rga/constants'; +import {PersistedSlice} from '../PersistedSlice'; import {SliceBehavior} from '../constants'; const setup = () => { @@ -173,150 +175,69 @@ describe('.delSlices()', () => { }); describe('.refresh()', () => { - test('changes hash on slice behavior change', () => { - const {peritext, encodeAndDecode} = setup(); - const range = peritext.rangeAt(6, 5); - const slice = peritext.slices.insOverwrite(range, 'b', {howBold: 'very'}); - const hash1 = peritext.slices.refresh(); - const hash2 = peritext.slices.refresh(); - expect(hash1).toBe(hash2); - expect(slice.behavior).toBe(SliceBehavior.Overwrite); + const testSliceUpdate = (name: string, update: (controls: {range: Range, slice: PersistedSlice}) => void) => { + test('changes hash on: ' + name, () => { + const {peritext, encodeAndDecode} = setup(); + const range = peritext.rangeAt(6, 5); + const slice = peritext.slices.insOverwrite(range, 'b', {howBold: 'very'}); + const hash1 = peritext.slices.refresh(); + const hash2 = peritext.slices.refresh(); + expect(hash1).toBe(hash2); + expect(slice.type).toBe('b'); + update({range, slice}); + const hash3 = peritext.slices.refresh(); + const hash4 = peritext.slices.refresh(); + expect(hash3).not.toBe(hash2); + expect(hash4).toBe(hash3); + const {peritext2} = encodeAndDecode(); + peritext2.refresh(); + const slice2 = peritext2.slices.get(slice.id)!; + expect(slice2.cmp(slice)).toBe(0); + }); + }; + + testSliceUpdate('slice behavior change', ({slice}) => { slice.update({behavior: SliceBehavior.Stack}); expect(slice.behavior).toBe(SliceBehavior.Stack); - const hash3 = peritext.slices.refresh(); - const hash4 = peritext.slices.refresh(); - expect(hash3).not.toBe(hash2); - expect(hash4).toBe(hash3); - const {peritext2} = encodeAndDecode(); - peritext2.refresh(); - const slice2 = peritext2.slices.get(slice.id)!; - expect(slice2.cmp(slice)).toBe(0); }); - test('changes hash on slice subtype change', () => { - const {peritext, encodeAndDecode} = setup(); - const range = peritext.rangeAt(6, 5); - const slice = peritext.slices.insOverwrite(range, 'b', {howBold: 'very'}); - const hash1 = peritext.slices.refresh(); - const hash2 = peritext.slices.refresh(); - expect(hash1).toBe(hash2); - expect(slice.type).toBe('b'); + testSliceUpdate('slice type change', ({slice}) => { slice.update({type: 123}); expect(slice.type).toBe(123); - const hash3 = peritext.slices.refresh(); - const hash4 = peritext.slices.refresh(); - expect(hash3).not.toBe(hash2); - expect(hash4).toBe(hash3); - const {peritext2} = encodeAndDecode(); - peritext2.refresh(); - const slice2 = peritext2.slices.get(slice.id)!; - expect(slice2.cmp(slice)).toBe(0); }); - test('changes hash on slice data overwrite', () => { - const {peritext, encodeAndDecode} = setup(); - const range = peritext.rangeAt(6, 5); - const slice = peritext.slices.insOverwrite(range, 'b', {howBold: 'very'}); - const hash1 = peritext.slices.refresh(); - const hash2 = peritext.slices.refresh(); - expect(hash1).toBe(hash2); - expect(slice.data()).toEqual({howBold: 'very'}); + testSliceUpdate('slice data overwrite', ({slice}) => { slice.update({data: 'the data'}); expect(slice.data()).toEqual('the data'); - const hash3 = peritext.slices.refresh(); - const hash4 = peritext.slices.refresh(); - expect(hash3).not.toBe(hash2); - expect(hash4).toBe(hash3); - const {peritext2} = encodeAndDecode(); - peritext2.refresh(); - const slice2 = peritext2.slices.get(slice.id)!; - expect(slice2.cmp(slice)).toBe(0); }); - test('changes hash on start anchor change', () => { - const {peritext, encodeAndDecode} = setup(); - const range = peritext.rangeAt(6, 5); - const slice = peritext.slices.insOverwrite(range, 'b', {howBold: 'very'}); - const hash1 = peritext.slices.refresh(); - const hash2 = peritext.slices.refresh(); - expect(hash1).toBe(hash2); + testSliceUpdate('slice start anchor change', ({range, slice}) => { range.start.anchor = Anchor.After; slice.update({range}); - const hash3 = peritext.slices.refresh(); - const hash4 = peritext.slices.refresh(); - expect(hash3).not.toBe(hash2); - expect(hash4).toBe(hash3); - const {peritext2} = encodeAndDecode(); - peritext2.refresh(); - const slice2 = peritext2.slices.get(slice.id)!; - expect(slice2.cmp(slice)).toBe(0); }); - test('changes hash on end anchor change', () => { - const {peritext, encodeAndDecode} = setup(); - const range = peritext.rangeAt(6, 5); - const slice = peritext.slices.insOverwrite(range, 'b', {howBold: 'very'}); - const hash1 = peritext.slices.refresh(); - const hash2 = peritext.slices.refresh(); - expect(hash1).toBe(hash2); + testSliceUpdate('slice end anchor change', ({range, slice}) => { range.end.anchor = Anchor.Before; slice.update({range}); - const hash3 = peritext.slices.refresh(); - const hash4 = peritext.slices.refresh(); - expect(hash3).not.toBe(hash2); - expect(hash4).toBe(hash3); - const {peritext2} = encodeAndDecode(); - peritext2.refresh(); - const slice2 = peritext2.slices.get(slice.id)!; - expect(slice2.cmp(slice)).toBe(0); }); - test('changes hash on start position change', () => { - const {peritext, encodeAndDecode} = setup(); - const range = peritext.rangeAt(6, 5); - const slice = peritext.slices.insOverwrite(range, 'b', {howBold: 'very'}); - const hash1 = peritext.slices.refresh(); - const hash2 = peritext.slices.refresh(); - expect(hash1).toBe(hash2); + testSliceUpdate('slice start position', ({range, slice}) => { range.start.id = range.start.nextId()!; slice.update({range}); - const hash3 = peritext.slices.refresh(); - const hash4 = peritext.slices.refresh(); - expect(hash3).not.toBe(hash2); - expect(hash4).toBe(hash3); - const {peritext2} = encodeAndDecode(); - peritext2.refresh(); - const slice2 = peritext2.slices.get(slice.id)!; - expect(slice2.cmp(slice)).toBe(0); }); - test('changes hash on end position change', () => { - const {peritext, encodeAndDecode} = setup(); - const range = peritext.rangeAt(6, 5); - const slice = peritext.slices.insOverwrite(range, 'b', {howBold: 'very'}); - const hash1 = peritext.slices.refresh(); - const hash2 = peritext.slices.refresh(); - expect(hash1).toBe(hash2); + testSliceUpdate('slice end position', ({range, slice}) => { range.end.id = range.start.prevId()!; slice.update({range}); - const hash3 = peritext.slices.refresh(); - const hash4 = peritext.slices.refresh(); - expect(hash3).not.toBe(hash2); - expect(hash4).toBe(hash3); - const {peritext2} = encodeAndDecode(); - peritext2.refresh(); - const slice2 = peritext2.slices.get(slice.id)!; - expect(slice2.cmp(slice)).toBe(0); }); - test('recomputes hash on tag change', () => { + test('recomputes hash on inline data change', () => { const {peritext} = setup(); const {editor} = peritext; editor.cursor.setAt(6, 5); const slice1 = editor.insertSlice('b', {bold: true}); peritext.refresh(); const hash1 = peritext.slices.hash; - const tag = slice1.data()!; peritext.model.api.obj(['slices', 0, 4]).set({bold: false}); peritext.refresh(); const hash2 = peritext.slices.hash; From db7f10e52e0594fbe45e1b9cc2acdb54681d5d11 Mon Sep 17 00:00:00 2001 From: streamich Date: Sat, 20 Apr 2024 11:39:03 +0200 Subject: [PATCH 21/22] =?UTF-8?q?fix(json-crdt-extensions):=20=F0=9F=90=9B?= =?UTF-8?q?=20delete=20slices=20from=20index=20on=20deletion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../peritext/slice/PersistedSlice.ts | 7 +- .../peritext/slice/Slices.ts | 1 + .../slice/__tests__/PersistedSlice.spec.ts | 76 +++++++++++++++++++ .../peritext/slice/__tests__/Slices.spec.ts | 25 +----- .../peritext/slice/__tests__/setup.ts | 24 ++++++ 5 files changed, 104 insertions(+), 29 deletions(-) create mode 100644 src/json-crdt-extensions/peritext/slice/__tests__/PersistedSlice.spec.ts create mode 100644 src/json-crdt-extensions/peritext/slice/__tests__/setup.ts diff --git a/src/json-crdt-extensions/peritext/slice/PersistedSlice.ts b/src/json-crdt-extensions/peritext/slice/PersistedSlice.ts index 295bb98716..4f7a1ac4e2 100644 --- a/src/json-crdt-extensions/peritext/slice/PersistedSlice.ts +++ b/src/json-crdt-extensions/peritext/slice/PersistedSlice.ts @@ -62,11 +62,6 @@ export class PersistedSlice extends Range implements MutableSlice return this.behavior === SliceBehavior.Split; } - protected tagNode(): JsonNode | undefined { - // TODO: Normalize `.get()` and `.getNode()` methods across VecNode and ArrNode. - return this.tuple.get(3); - } - protected tupleApi() { return this.txt.model.api.wrap(this.tuple); } @@ -109,7 +104,7 @@ export class PersistedSlice extends Range implements MutableSlice } public data(): unknown | undefined { - return this.tuple.get(4)?.view(); + return this.tuple.get(SliceTupleIndex.Data)?.view(); } public dataNode() { diff --git a/src/json-crdt-extensions/peritext/slice/Slices.ts b/src/json-crdt-extensions/peritext/slice/Slices.ts index e46d830164..7ab183ae73 100644 --- a/src/json-crdt-extensions/peritext/slice/Slices.ts +++ b/src/json-crdt-extensions/peritext/slice/Slices.ts @@ -98,6 +98,7 @@ export class Slices implements Stateful, Printable { } public del(id: ITimestampStruct): void { + this.list.del(id); const api = this.txt.model.api; api.builder.del(this.set.id, [tss(id.sid, id.time, 1)]); api.apply(); diff --git a/src/json-crdt-extensions/peritext/slice/__tests__/PersistedSlice.spec.ts b/src/json-crdt-extensions/peritext/slice/__tests__/PersistedSlice.spec.ts new file mode 100644 index 0000000000..7d595dd708 --- /dev/null +++ b/src/json-crdt-extensions/peritext/slice/__tests__/PersistedSlice.spec.ts @@ -0,0 +1,76 @@ +import {SliceBehavior} from '../constants'; +import {setup} from './setup'; + +const setupSlice = () => { + const deps = setup(); + const range = deps.peritext.rangeAt(2, 3); + const slice = deps.peritext.slices.insSplit(range, 0); + return {...deps, range, slice}; +}; + +test('can read slice data', () => { + const {range, slice} = setupSlice(); + expect(slice.isSplit()).toBe(true); + expect(slice.behavior).toBe(SliceBehavior.Split); + expect(slice.type).toBe(0); + expect(slice.data()).toBe(undefined); + expect(slice.start).not.toBe(range.start); + expect(slice.start.cmp(range.start)).toBe(0); + expect(slice.end).not.toBe(range.end); + expect(slice.end.cmp(range.end)).toBe(0); +}); + +describe('.update()', () => { + const testUpdate = (name: string, update: (deps: ReturnType) => void) => { + test('can update: ' + name, () => { + const deps = setupSlice(); + const {slice} = deps; + const hash1 = slice.refresh(); + const hash2 = slice.refresh(); + expect(hash1).toBe(hash2); + update(deps); + const hash3 = slice.refresh(); + expect(hash3).not.toBe(hash2); + }); + }; + + testUpdate('behavior', ({slice}) => { + slice.update({behavior: SliceBehavior.Erase}); + expect(slice.behavior).toBe(SliceBehavior.Erase); + }); + + testUpdate('type', ({slice}) => { + slice.update({type: 1}); + expect(slice.type).toBe(1); + }); + + testUpdate('data', ({slice}) => { + slice.update({data: 123}); + expect(slice.data()).toBe(123); + }); + + testUpdate('range', ({peritext, slice}) => { + const range2 = peritext.rangeAt(0, 1); + slice.update({range: range2}); + expect(slice.cmp(range2)).toBe(0); + }); +}); + +describe('.del() and .isDel()', () => { + test('can delete a slice', () => { + const {peritext, slice} = setupSlice(); + expect(peritext.model.view().slices.length).toBe(1); + expect(slice.isDel()).toBe(false); + const slice2 = peritext.slices.get(slice.id)!; + expect(peritext.model.view().slices.length).toBe(1); + expect(slice2.isDel()).toBe(false); + expect(slice2).toBe(slice); + slice.del(); + expect(peritext.model.view().slices.length).toBe(0); + expect(slice.isDel()).toBe(true); + expect(slice2.isDel()).toBe(true); + const slice3 = peritext.slices.get(slice.id); + expect(slice3).toBe(undefined); + }); +}); + diff --git a/src/json-crdt-extensions/peritext/slice/__tests__/Slices.spec.ts b/src/json-crdt-extensions/peritext/slice/__tests__/Slices.spec.ts index 27f21fa7eb..7144ef64c1 100644 --- a/src/json-crdt-extensions/peritext/slice/__tests__/Slices.spec.ts +++ b/src/json-crdt-extensions/peritext/slice/__tests__/Slices.spec.ts @@ -1,31 +1,10 @@ -import {Model, ObjApi} from '../../../../json-crdt/model'; +import {Model} from '../../../../json-crdt/model'; import {Peritext} from '../../Peritext'; import {Range} from '../../rga/Range'; import {Anchor} from '../../rga/constants'; import {PersistedSlice} from '../PersistedSlice'; import {SliceBehavior} from '../constants'; - -const setup = () => { - const model = Model.withLogicalClock(12345678); - model.api.root({ - text: '', - slices: [], - }); - model.api.str(['text']).ins(0, 'wworld'); - model.api.str(['text']).ins(0, 'helo '); - model.api.str(['text']).ins(2, 'l'); - model.api.str(['text']).del(7, 1); - model.api.str(['text']).ins(11, ' this game is awesome'); - const peritext = new Peritext(model, model.api.str(['text']).node, model.api.arr(['slices']).node); - const slices = peritext.slices; - const encodeAndDecode = () => { - const buf = model.toBinary(); - const model2 = Model.fromBinary(buf); - const peritext2 = new Peritext(model2, model2.api.str(['text']).node, model2.api.arr(['slices']).node); - return {model2, peritext2}; - }; - return {model, peritext, slices, encodeAndDecode}; -}; +import {setup} from './setup'; test('initially slice list is empty', () => { const {peritext} = setup(); diff --git a/src/json-crdt-extensions/peritext/slice/__tests__/setup.ts b/src/json-crdt-extensions/peritext/slice/__tests__/setup.ts new file mode 100644 index 0000000000..f31bb4c04b --- /dev/null +++ b/src/json-crdt-extensions/peritext/slice/__tests__/setup.ts @@ -0,0 +1,24 @@ +import {Model} from '../../../../json-crdt/model'; +import {Peritext} from '../../Peritext'; + +export const setup = () => { + const model = Model.withLogicalClock(12345678); + model.api.root({ + text: '', + slices: [], + }); + model.api.str(['text']).ins(0, 'wworld'); + model.api.str(['text']).ins(0, 'helo '); + model.api.str(['text']).ins(2, 'l'); + model.api.str(['text']).del(7, 1); + model.api.str(['text']).ins(11, ' this game is awesome'); + const peritext = new Peritext(model, model.api.str(['text']).node, model.api.arr(['slices']).node); + const slices = peritext.slices; + const encodeAndDecode = () => { + const buf = model.toBinary(); + const model2 = Model.fromBinary(buf); + const peritext2 = new Peritext(model2, model2.api.str(['text']).node, model2.api.arr(['slices']).node); + return {model2, peritext2}; + }; + return {model, peritext, slices, encodeAndDecode}; +}; From 7c270aa4d1da1e3384392b46cf9a5fc2d4a83b88 Mon Sep 17 00:00:00 2001 From: streamich Date: Sat, 20 Apr 2024 11:40:29 +0200 Subject: [PATCH 22/22] =?UTF-8?q?style(json-crdt-extensions):=20?= =?UTF-8?q?=F0=9F=92=84=20fix=20linter=20errors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/json-crdt-extensions/peritext/slice/PersistedSlice.ts | 2 +- .../peritext/slice/__tests__/PersistedSlice.spec.ts | 1 - .../peritext/slice/__tests__/Slices.spec.ts | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/json-crdt-extensions/peritext/slice/PersistedSlice.ts b/src/json-crdt-extensions/peritext/slice/PersistedSlice.ts index 4f7a1ac4e2..66fa218fb4 100644 --- a/src/json-crdt-extensions/peritext/slice/PersistedSlice.ts +++ b/src/json-crdt-extensions/peritext/slice/PersistedSlice.ts @@ -74,7 +74,7 @@ export class PersistedSlice extends Range implements MutableSlice public update(params: SliceUpdateParams): void { let updateHeader = false; - let {start, end} = this; + const {start, end} = this; const changes: [number, unknown][] = []; if (params.behavior !== undefined) { this.behavior = params.behavior; diff --git a/src/json-crdt-extensions/peritext/slice/__tests__/PersistedSlice.spec.ts b/src/json-crdt-extensions/peritext/slice/__tests__/PersistedSlice.spec.ts index 7d595dd708..08d75e9dad 100644 --- a/src/json-crdt-extensions/peritext/slice/__tests__/PersistedSlice.spec.ts +++ b/src/json-crdt-extensions/peritext/slice/__tests__/PersistedSlice.spec.ts @@ -73,4 +73,3 @@ describe('.del() and .isDel()', () => { expect(slice3).toBe(undefined); }); }); - diff --git a/src/json-crdt-extensions/peritext/slice/__tests__/Slices.spec.ts b/src/json-crdt-extensions/peritext/slice/__tests__/Slices.spec.ts index 7144ef64c1..f2f29c0e6a 100644 --- a/src/json-crdt-extensions/peritext/slice/__tests__/Slices.spec.ts +++ b/src/json-crdt-extensions/peritext/slice/__tests__/Slices.spec.ts @@ -154,7 +154,7 @@ describe('.delSlices()', () => { }); describe('.refresh()', () => { - const testSliceUpdate = (name: string, update: (controls: {range: Range, slice: PersistedSlice}) => void) => { + const testSliceUpdate = (name: string, update: (controls: {range: Range; slice: PersistedSlice}) => void) => { test('changes hash on: ' + name, () => { const {peritext, encodeAndDecode} = setup(); const range = peritext.rangeAt(6, 5);