Skip to content

Commit

Permalink
Merge pull request #463 from streamich/api-improvements
Browse files Browse the repository at this point in the history
API improvements
  • Loading branch information
streamich authored Nov 28, 2023
2 parents a63f12b + 7e190c6 commit b02305f
Show file tree
Hide file tree
Showing 23 changed files with 205 additions and 77 deletions.
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

0 comments on commit b02305f

Please sign in to comment.