Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
"demo:json-pointer": "ts-node src/json-pointer/__demos__/json-pointer.ts",
"demo:reactive-rpc:server": "ts-node src/reactive-rpc/__demos__/server.ts",
"coverage": "yarn test --collectCoverage",
"typedoc": "typedoc",
"typedoc": "npx typedoc",
"build:pages": "rimraf gh-pages && mkdir -p gh-pages && cp -r typedocs/* gh-pages && cp -r coverage gh-pages/coverage",
"deploy:pages": "gh-pages -d gh-pages",
"publish-coverage-and-typedocs": "yarn typedoc && yarn coverage && yarn build:pages && yarn deploy:pages"
Expand Down Expand Up @@ -152,7 +152,6 @@
"tslib": "^2.5.0",
"tslint": "^6.1.3",
"tslint-config-common": "^1.6.2",
"typedoc": "^0.25.3",
"typescript": "^5.2.2",
"uWebSockets.js": "uNetworking/uWebSockets.js#v20.23.0",
"webpack": "^5.84.1",
Expand Down
1 change: 1 addition & 0 deletions src/json-crdt-patch/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@
*/

export * from './types';
export * from './clock';
export * from './operations';
export * from './Patch';
export * from './PatchBuilder';
Expand Down
3 changes: 3 additions & 0 deletions src/json-crdt/codec/indexed/binary/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './types';
export * from './Encoder';
export * from './Decoder';
2 changes: 2 additions & 0 deletions src/json-crdt/codec/sidecar/binary/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './Encoder';
export * from './Decoder';
4 changes: 4 additions & 0 deletions src/json-crdt/codec/structural/binary/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './constants';
export * from './Encoder';
export * from './Decoder';
export * from './ViewDecoder';
3 changes: 3 additions & 0 deletions src/json-crdt/codec/structural/compact/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './types';
export * from './Encoder';
export * from './Decoder';
3 changes: 3 additions & 0 deletions src/json-crdt/codec/structural/verbose/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './types';
export * from './Encoder';
export * from './Decoder';
2 changes: 2 additions & 0 deletions src/json-crdt/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export * from './nodes';
export * from './extensions/types';
export * from './model';

export * from '../json-crdt-patch';
9 changes: 9 additions & 0 deletions src/json-crdt/model/api/__tests__/ArrayApi.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,12 @@ test('can insert a value and delete all previous ones', () => {
arr.ins(1, [69]);
expect(arr.view()).toEqual([42, 69]);
});

test('.length()', () => {
const doc = Model.withLogicalClock();
doc.api.root({
arr: [1, 2, 3],
});
const arr = doc.api.arr(['arr']);
expect(arr.length()).toBe(3);
});
10 changes: 10 additions & 0 deletions src/json-crdt/model/api/__tests__/BinaryApi.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import {s} from '../../../../json-crdt-patch';
import {Model} from '../../Model';

test('can edit a simple binary', () => {
Expand Down Expand Up @@ -25,3 +26,12 @@ test('can delete across two chunks', () => {
bin.del(1, 7);
expect(bin.view()).toEqual(new Uint8Array([3, 1]));
});

test('.length()', () => {
const doc = Model.withLogicalClock().setSchema(
s.obj({
bin: s.bin(new Uint8Array([1, 2, 3])),
}),
);
expect(doc.find.val.bin.toApi().length()).toBe(3);
});
4 changes: 2 additions & 2 deletions src/json-crdt/model/api/__tests__/ModelApi.proxy.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ describe('supports all node types', () => {
const objApi: ObjApi = obj.toApi();
expect(objApi).toBeInstanceOf(ObjApi);
expect(objApi.node).toBeInstanceOf(ObjNode);
const keys = new Set(Object.keys(objApi.view()));
const keys = new Set(Object.keys(objApi.view() as any));
expect(keys.has('obj')).toBe(true);
expect(keys.has('vec')).toBe(true);
});
Expand All @@ -79,7 +79,7 @@ describe('supports all node types', () => {
const objApi: ObjApi = obj.toApi();
expect(objApi).toBeInstanceOf(ObjApi);
expect(objApi.node).toBeInstanceOf(ObjNode);
const keys = new Set(Object.keys(objApi.view()));
const keys = new Set(Object.keys(objApi.view() as any));
expect(keys.has('str')).toBe(true);
expect(keys.has('num')).toBe(true);
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import {s} from '../../../../json-crdt-patch';
import {ITimestampStruct} from '../../../../json-crdt-patch/clock';
import {Model} from '../../Model';

test('can edit a simple string', () => {
Expand Down Expand Up @@ -25,6 +27,44 @@ test('can delete across two chunks', () => {
expect(str.view()).toEqual('ca');
});

test('.length()', () => {
const doc = Model.withLogicalClock().setSchema(
s.obj({
str: s.str('hello world'),
}),
);
expect(doc.find.val.str.toApi().length()).toBe(11);
});

describe('position tracking', () => {
test('can convert position into global coordinates and back', () => {
const doc = Model.withLogicalClock().setSchema(
s.obj({
str: s.str('hello world'),
}),
);
const str = doc.find.val.str.toApi();
for (let i = -1; i < str.length(); i++) {
const id = str.findId(i);
expect(str.findPos(id)).toBe(i);
}
});

test('shifts position when text is inserted in the middle', () => {
const doc = Model.withLogicalClock().setSchema(
s.obj({
str: s.str('123456'),
}),
);
const str = doc.find.val.str.toApi();
const ids: ITimestampStruct[] = [];
for (let i = -1; i < str.length(); i++) ids.push(str.findId(i));
str.ins(3, 'abc');
for (let i = 0; i <= 3; i++) expect(str.findPos(ids[i])).toBe(i - 1);
for (let i = 4; i < ids.length; i++) expect(str.findPos(ids[i])).toBe(i + 3 - 1);
});
});

describe('events', () => {
test('can subscribe to "view" events', async () => {
const doc = Model.withLogicalClock();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {vec} from '../../../../json-crdt-patch';
import {s, vec} from '../../../../json-crdt-patch';
import {Model} from '../../Model';

test('can edit a tuple', () => {
Expand All @@ -9,6 +9,38 @@ test('can edit a tuple', () => {
expect(api.vec([]).view()).toEqual([undefined, 'a']);
});

test('.length()', () => {
const doc = Model.withLogicalClock().setSchema(
s.obj({
vec: s.vec(s.con(1), s.con(2)),
}),
);
expect(doc.find.val.vec.toApi().length()).toBe(2);
});

test('.push()', () => {
const doc = Model.withLogicalClock().setSchema(
s.obj({
vec: s.vec(s.con(1), s.con(2)),
}),
);
expect(doc.view().vec).toEqual([1, 2]);
doc.find.val.vec.toApi().push(3);
expect(doc.view().vec).toEqual([1, 2, 3]);
doc.find.val.vec.toApi().push(4, 5, '6');
expect(doc.view().vec).toEqual([1, 2, 3, 4, 5, '6']);
});

test('.view() is not readonly', () => {
const doc = Model.withLogicalClock().setSchema(
s.obj({
vec: s.vec(s.con(1), s.con(2)),
}),
);
const view = doc.find.val.vec.toApi().view();
view[1] = 12;
});

describe('events', () => {
test('can subscribe and un-subscribe to "view" events', async () => {
const doc = Model.withLogicalClock();
Expand Down
69 changes: 68 additions & 1 deletion src/json-crdt/model/api/nodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,21 @@ export class VecApi<N extends VecNode<any> = VecNode<any>> extends NodeApi<N> {
entries.map(([index, json]) => [index, builder.constOrJson(json)]),
);
api.apply();
return this;
return this; // TODO: remove this ...?
}

public push(...values: unknown[]): void {
const length = this.length();
this.set(values.map((value, index) => [length + index, value]));
}

/**
* Get the length of the vector without materializing it to a view.
*
* @returns Length of the vector.
*/
public length(): number {
return this.node.elements.length;
}

/**
Expand Down Expand Up @@ -395,6 +409,50 @@ export class StrApi extends NodeApi<StrNode> {
return this;
}

/**
* Given a character index in local coordinates, find the ID of the character
* in the global coordinates.
*
* @param index Index of the character or `-1` for before the first character.
* @returns ID of the character after which the given position is located.
*/
public findId(index: number | -1): ITimestampStruct {
const node = this.node;
const length = node.length();
const max = length - 1;
if (index > max) index = max;
if (index < 0) return node.id;
const id = node.find(index);
return id || node.id;
}

/**
* Given a position in global coordinates, find the position in local
* coordinates.
*
* @param id ID of the character.
* @returns Index of the character in local coordinates. Returns -1 if the
* the position refers to the beginning of the string.
*/
public findPos(id: ITimestampStruct): number | -1 {
const node = this.node;
const nodeId = node.id;
if (nodeId.sid === id.sid && nodeId.time === id.time) return -1;
const chunk = node.findById(id);
if (!chunk) return -1;
const pos = node.pos(chunk);
return pos + (chunk.del ? 0 : id.time - chunk.id.time);
}

/**
* Get the length of the string without materializing it to a view.
*
* @returns Length of the string.
*/
public length(): number {
return this.node.length();
}

/**
* Returns a proxy object for this node.
*/
Expand Down Expand Up @@ -445,6 +503,15 @@ export class BinApi extends NodeApi<BinNode> {
return this;
}

/**
* Get the length of the binary blob without materializing it to a view.
*
* @returns Length of the binary blob.
*/
public length(): number {
return this.node.length();
}

/**
* Returns a proxy object for this node.
*/
Expand Down
4 changes: 2 additions & 2 deletions src/json-crdt/nodes/arr/ArrNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export class ArrChunk implements Chunk<E[]> {
*/
export class ArrNode<Element extends JsonNode = JsonNode>
extends AbstractRga<E[]>
implements JsonNode<Readonly<JsonNodeView<Element>[]>>, Printable
implements JsonNode<JsonNodeView<Element>[]>, Printable
{
constructor(public readonly doc: Model<any>, id: ITimestampStruct) {
super(id);
Expand Down Expand Up @@ -144,7 +144,7 @@ export class ArrNode<Element extends JsonNode = JsonNode>
private _tick: number = 0;
/** @ignore */
private _view = Empty;
public view(): Readonly<JsonNodeView<Element>[]> {
public view(): JsonNodeView<Element>[] {
const doc = this.doc;
const tick = doc.clock.time + doc.tick;
const _view = this._view;
Expand Down
2 changes: 1 addition & 1 deletion src/json-crdt/nodes/bin/BinNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export class BinNode extends AbstractRga<Uint8Array> implements JsonNode<Uint8Ar

/** @ignore */
private _view: null | Uint8Array = null;
public view(): Readonly<Uint8Array> {
public view(): Uint8Array {
if (this._view) return this._view;
const res = new Uint8Array(this.length());
let offset = 0;
Expand Down
2 changes: 1 addition & 1 deletion src/json-crdt/nodes/con/ConNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export class ConNode<View = unknown | ITimestampStruct> implements JsonNode<View
return undefined;
}

public view(): Readonly<View> {
public view(): View {
return this.val;
}

Expand Down
8 changes: 4 additions & 4 deletions src/json-crdt/nodes/obj/ObjNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import type {JsonNode, JsonNodeView} from '..';
*/

export class ObjNode<Value extends Record<string, JsonNode> = Record<string, JsonNode>>
implements JsonNode<Readonly<JsonNodeView<Value>>>, Printable
implements JsonNode<JsonNodeView<Value>>, Printable
{
/**
* @ignore
Expand Down Expand Up @@ -97,17 +97,17 @@ export class ObjNode<Value extends Record<string, JsonNode> = Record<string, Jso
/**
* @ignore
*/
private _view = {} as Readonly<JsonNodeView<Value>>;
private _view = {} as JsonNodeView<Value>;

/**
* @ignore
*/
public view(): Readonly<JsonNodeView<Value>> {
public view(): JsonNodeView<Value> {
const doc = this.doc;
const tick = doc.clock.time + doc.tick;
const _view = this._view;
if (this._tick === tick) return _view;
const view = {} as Readonly<JsonNodeView<Value>>;
const view = {} as JsonNodeView<Value>;
const index = doc.index;
let useCache = true;
this.keys.forEach((id, key) => {
Expand Down
6 changes: 4 additions & 2 deletions src/json-crdt/nodes/rga/AbstractRga.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ export abstract class AbstractRga<T> {
if (last) this.mergeTombstones2(start, last);
}

public find(position: number): void | ITimestampStruct {
public find(position: number): undefined | ITimestampStruct {
let curr = this.root;
while (curr) {
const l = curr.l;
Expand All @@ -311,9 +311,10 @@ export abstract class AbstractRga<T> {
curr = curr.r;
}
}
return;
}

public findChunk(position: number): void | [chunk: Chunk<T>, offset: number] {
public findChunk(position: number): undefined | [chunk: Chunk<T>, offset: number] {
let curr = this.root;
while (curr) {
const l = curr.l;
Expand All @@ -330,6 +331,7 @@ export abstract class AbstractRga<T> {
curr = curr.r;
}
}
return;
}

public findInterval(position: number, length: number): ITimespanStruct[] {
Expand Down
Loading