-
-
Notifications
You must be signed in to change notification settings - Fork 18
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(json-crdt-extensions): 🎸 add slices manager
- Loading branch information
Showing
6 changed files
with
410 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
import {Point} from '../point/Point'; | ||
import {Range} from './Range'; | ||
import {hashNode} 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 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'; | ||
|
||
export class PersistedSlice extends Range implements Slice, Printable, Stateful { | ||
public readonly id: ITimestampStruct; | ||
|
||
constructor( | ||
protected readonly txt: Peritext, | ||
protected readonly chunk: ArrChunk, | ||
public readonly tuple: VecNode, | ||
public behavior: SliceBehavior, | ||
/** @todo Rename to x1? */ | ||
public start: Point, | ||
/** @todo Rename to x2? */ | ||
public end: Point, | ||
public type: SliceType, | ||
) { | ||
super(txt, start, end); | ||
this.id = this.chunk.id; | ||
} | ||
|
||
protected tagNode(): JsonNode | undefined { | ||
// TODO: Normalize `.get()` and `.getNode()` methods across VecNode and ArrNode. | ||
return this.tuple.get(3); | ||
} | ||
|
||
public data(): unknown | undefined { | ||
return this.tuple.get(4)?.view(); | ||
} | ||
|
||
public del(): boolean { | ||
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; | ||
this.hash = hash; | ||
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; | ||
} | ||
return this.hash; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,156 @@ | ||
import {PersistedSlice} from './PersistedSlice'; | ||
import {ITimespanStruct, ITimestampStruct, Timespan, Timestamp, compare, tss} from '../../../json-crdt-patch/clock'; | ||
import {Range} from './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 {SplitSlice} from './SplitSlice'; | ||
import {Point} from '../point/Point'; | ||
import {Slice} from './types'; | ||
import {VecNode} from '../../../json-crdt/nodes'; | ||
import type {SliceDto, SliceType, Stateful} from '../types'; | ||
import type {Peritext} from '../Peritext'; | ||
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<ArrChunk, PersistedSlice>(); | ||
|
||
constructor(public readonly txt: Peritext, public readonly set: ArrNode) {} | ||
|
||
public ins(range: Range, behavior: SliceBehavior, type: SliceType, data?: unknown): PersistedSlice { | ||
const peritext = this.txt; | ||
const model = peritext.model; | ||
const set = this.set; | ||
const api = model.api; | ||
const builder = api.builder; | ||
const tupleId = builder.vec(); | ||
const start = range.start; | ||
const end = range.end; | ||
const header = | ||
(behavior << SliceHeaderShift.Behavior) + | ||
(start.anchor << SliceHeaderShift.X1Anchor) + | ||
(end.anchor << SliceHeaderShift.X2Anchor); | ||
const headerId = builder.const(header); | ||
const x1Id = builder.const(start.id); | ||
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], | ||
]; | ||
if (data !== undefined) tupleKeysUpdate.push([4, builder.json(data)]); | ||
builder.insVec(tupleId, tupleKeysUpdate); | ||
const chunkId = builder.insArr(set.id, set.id, [tupleId]); | ||
api.apply(); | ||
const tuple = model.index.get(tupleId) as VecNode; | ||
const chunk = set.findById(chunkId)!; | ||
// TODO: Need to check if split slice text was deleted | ||
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); | ||
this.list.set(chunk, slice); | ||
return slice; | ||
} | ||
|
||
protected unpack(chunk: ArrChunk): PersistedSlice { | ||
const txt = this.txt; | ||
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, id1, anchor1); | ||
const p2 = new Point(txt, id2, anchor2); | ||
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); | ||
return slice; | ||
} | ||
|
||
public del(id: ITimestampStruct): void { | ||
const api = this.txt.model.api; | ||
api.builder.del(this.set.id, [tss(id.sid, id.time, 1)]); | ||
api.apply(); | ||
} | ||
|
||
public delMany(slices: Slice[]): void { | ||
const api = this.txt.model.api; | ||
const spans: ITimespanStruct[] = []; | ||
const length = slices.length; | ||
for (let i = 0; i < length; i++) { | ||
const slice = slices[i]; | ||
if (slice instanceof PersistedSlice) { | ||
const id = slice.id; | ||
spans.push(new Timespan(id.sid, id.time, 1)); | ||
} | ||
} | ||
api.builder.del(this.set.id, spans); | ||
api.apply(); | ||
} | ||
|
||
public size(): number { | ||
return this.list.size; | ||
} | ||
|
||
public forEach(callback: (item: PersistedSlice) => void): void { | ||
this.list.forEach(callback); | ||
} | ||
|
||
// ----------------------------------------------------------------- Stateful | ||
|
||
private _topologyHash: number = 0; | ||
public hash: number = 0; | ||
|
||
public refresh(): number { | ||
const topologyHash = updateRga(CONST.START_STATE, this.set); | ||
if (topologyHash !== this._topologyHash) { | ||
this._topologyHash = topologyHash; | ||
let chunk: ArrChunk | undefined; | ||
for (const iterator = this.set.iterator(); (chunk = iterator()); ) { | ||
const item = this.list.get(chunk); | ||
if (chunk.del) { | ||
if (item) this.list.delete(chunk); | ||
} else { | ||
if (!item) this.list.set(chunk, this.unpack(chunk)); | ||
} | ||
} | ||
} | ||
let hash: number = topologyHash; | ||
this.list.forEach((item) => { | ||
item.refresh(); | ||
hash = updateNum(hash, item.hash); | ||
}); | ||
return (this.hash = hash); | ||
} | ||
|
||
// ---------------------------------------------------------------- Printable | ||
|
||
public toString(tab: string = ''): string { | ||
return ( | ||
this.constructor.name + | ||
printTree( | ||
tab, | ||
[...this.list].map( | ||
([, slice]) => | ||
(tab) => | ||
slice.toString(tab), | ||
), | ||
) | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import {PersistedSlice} from './PersistedSlice'; | ||
|
||
export class SplitSlice extends PersistedSlice {} |
Oops, something went wrong.