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
1 change: 1 addition & 0 deletions .git-cz.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"json-crdt-patch",
"json-equal",
"json-expression",
"json-ot",
"json-pack",
"json-patch",
"json-patch-ot",
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@
"messagepack": "^1.1.12",
"msgpack-lite": "^0.1.26",
"msgpack5": "^5.3.2",
"ot-text": "^1.0.2",
"pako": "^2.0.4",
"prettier": "^2.5.1",
"pretty-quick": "^3.1.3",
Expand Down
10 changes: 4 additions & 6 deletions src/__tests__/msgpack-documents.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {JsonPackExtension, JsonPackValue} from "../json-pack";
import {encodeFull} from "../json-pack/util";
import {JsonPackExtension, JsonPackValue} from '../json-pack';
import {encodeFull} from '../json-pack/util';

export interface JsonDocument {
name: string;
Expand All @@ -24,9 +24,7 @@ export const msgPackDocuments: JsonDocument[] = [
},
{
name: 'MessagePack value in array',
json: [
new JsonPackValue(encodeFull(null)),
],
json: [new JsonPackValue(encodeFull(null))],
},
{
name: 'MessagePack extension',
Expand All @@ -36,7 +34,7 @@ export const msgPackDocuments: JsonDocument[] = [
name: 'MessagePack extension in object',
json: {
foo: new JsonPackExtension(1, new Uint8Array([1, 2, 3])),
}
},
},
{
name: 'MessagePack extension in array',
Expand Down
21 changes: 21 additions & 0 deletions src/demo/ot-text.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/* tslint:disable no-console */

/**
* Run this demo with:
*
* npx ts-node src/demo/ot-text.ts
*/

const {type} = require('ot-text');

const op1 = [1, 'a'];
const op2 = [3, 'b'];

// const op1 = [3, 'a'];
// const op2 = [3, 'b'];

const op3 = type.transform(op1, op2, 'left');
const op4 = type.transform(op2, op1, 'right');

console.log('op3', op3);
console.log('op4', op4);
10 changes: 6 additions & 4 deletions src/json-binary/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ const unwrapBinary = (value: unknown): unknown => {
}
case 'string': {
if (item.length < minDataUri) continue;
if (item.substring(0, binUriStartLength) === binUriStart) value[i] = fromBase64(item.substring(binUriStartLength));
if (item.substring(0, binUriStartLength) === binUriStart)
value[i] = fromBase64(item.substring(binUriStartLength));
else if (item.substring(0, msgPackUriStartLength) === msgPackUriStart)
value[i] = new JsonPackValue(fromBase64(item.substring(msgPackUriStartLength)));
else if (item.substring(0, msgPackExtStartLength) === msgPackExtStart) value[i] = parseExtDataUri(item);
Expand All @@ -57,8 +58,8 @@ const unwrapBinary = (value: unknown): unknown => {
(value as any)[key] = buf;
} else if (item.substring(0, msgPackUriStartLength) === msgPackUriStart) {
(value as any)[key] = new JsonPackValue(fromBase64(item.substring(msgPackUriStartLength)));
}
else if (item.substring(0, msgPackExtStartLength) === msgPackExtStart) (value as any)[key] = parseExtDataUri(item);
} else if (item.substring(0, msgPackExtStartLength) === msgPackExtStart)
(value as any)[key] = parseExtDataUri(item);
}
}
}
Expand All @@ -67,7 +68,8 @@ const unwrapBinary = (value: unknown): unknown => {
if (typeof value === 'string') {
if (value.length < minDataUri) return value;
if (value.substring(0, binUriStartLength) === binUriStart) return fromBase64(value.substring(binUriStartLength));
if (value.substring(0, msgPackUriStartLength) === msgPackUriStart) return new JsonPackValue(fromBase64(value.substring(msgPackUriStartLength)));
if (value.substring(0, msgPackUriStartLength) === msgPackUriStart)
return new JsonPackValue(fromBase64(value.substring(msgPackUriStartLength)));
if (value.substring(0, msgPackExtStartLength) === msgPackExtStart) return parseExtDataUri(value);
else return value;
}
Expand Down
28 changes: 8 additions & 20 deletions src/json-block/Block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,12 @@ export class BasicBlock<Data, Patch> {

public apply(patch: Patch): void {
this.model.apply(patch);
this.v$.next(this.v$.getValue() + 1);
this.v$.next(this.v$.getValue());
}
}

export type PatchResponse<Patch> = true | false | Patch;

export interface BranchDependencies<Patch> {
readonly merge: (baseVersion: number, patches: Patch[]) => Promise<BranchMergeResponse<Patch>>;
}

export interface BranchMergeResponse<Patch> {
version: number;
batches: PatchResponse<Patch>[][];
Expand All @@ -66,31 +62,23 @@ export class Branch<Data, Patch> {
protected readonly head$: BehaviorSubject<BasicBlock<Data, Patch>>;

/**
* List of patches applied locally to the head, but not saved
* on the server. This is delta between the base and the head.
* List of patches applied locally to the head, but not yet
* added to the `batches` list.
*/
protected patches: Patch[];

/** Number of patches currently being merged to the server. */
protected merging: number = 0;
/** List of batches to be persisted on the server. */
protected batches: Patch[][] = [];

constructor(base: BasicBlock<Data, Patch>) {
this.base$ = new BehaviorSubject(base);
this.patches = [];
this.head$ = new BehaviorSubject(base.fork());
}

public async merge(opts: BranchDependencies<Patch>): Promise<void> {
try {
const base = this.base$.getValue();
const baseVersion = base.v$.getValue();
const batch = [...this.patches];
this.merging = batch.length;
const res = await opts.merge(baseVersion, batch);

} catch (error) {
this.merging = 0;
}
public cutBatch(): void {
this.batches.push(this.patches);
this.patches = [];
}

/** Apply a patch locally to the head. */
Expand Down
6 changes: 3 additions & 3 deletions src/json-block/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type {Observable} from "rxjs";
import type {Observable} from 'rxjs';

export interface BlockServerApi {
create: () => Promise<void>;
Expand All @@ -11,14 +11,14 @@ export interface BlockClientApiMergeRequest {
/** Last known batch ID by the client. */
v: number;
/** List of patches serialized in block-specific codec. */
batch: unknown[];
b: unknown[];
}

/**
* There are a number of scenarios that can happen
* when merging changes. The possible scenarios also depend
* on the collaborative editing algorithm used for the block.
*
*
* 1. The batch is accepted without any conflicts (changes).
* 2. The batch is accepted with conflicts, hence the batch may be
* be modified to resolve the conflicts. This is relevant for
Expand Down
8 changes: 3 additions & 5 deletions src/json-crdt/codec/binary/Encoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export class Encoder extends CrdtEncoder {
delete this.literals;
return this.flush();
}

protected encodeLiteralsTable(model: Model) {
const literalFrequencies = new Map<string | number, number>();
for (const node of model.nodes.iterate()) {
Expand Down Expand Up @@ -75,11 +75,9 @@ export class Encoder extends CrdtEncoder {
}
}
const literals: unknown[] = [];
for (const [literal, frequency] of literalFrequencies.entries())
if (frequency > 1) literals.push(literal);
for (const [literal, frequency] of literalFrequencies.entries()) if (frequency > 1) literals.push(literal);
this.encodeArray(literals);
for (let i = 0; i < literals.length; i++)
this.literals!.set(literals[i], i);
for (let i = 0; i < literals.length; i++) this.literals!.set(literals[i], i);
}

protected encodeClockTable(data: Uint8Array) {
Expand Down
5 changes: 4 additions & 1 deletion src/json-crdt/codec/binary/__tests__/ViewDecoder.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ describe('logical', () => {
doc1.api.root([1, 'asdf', false, {}, {foo: 'bar'}]).commit();
doc1.api.str([1]).ins(4, '!').commit();
doc1.api.str([4, 'foo']).del(1, 1).commit();
doc1.api.arr([]).ins(1, [new Uint8Array([1, 2, 3])]).commit();
doc1.api
.arr([])
.ins(1, [new Uint8Array([1, 2, 3])])
.commit();
doc1.api.bin([1]).del(2, 1).commit();
doc1.api.arr([]).ins(6, ['a', 'b', 'c']).commit();
doc1.api.arr([]).del(7, 1).commit();
Expand Down
24 changes: 14 additions & 10 deletions src/json-crdt/codec/binary/__tests__/literals.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,21 @@ import {Encoder} from '../Encoder';
test('encodes repeating object keys into literals table', () => {
const encoder = new Encoder();
const model1 = Model.withLogicalClock();
model1.api.root({
foooo: {
baaar: 0,
}
}).commit();
model1.api
.root({
foooo: {
baaar: 0,
},
})
.commit();
const model2 = Model.withLogicalClock();
model2.api.root({
foooo: {
foooo: 0,
}
}).commit();
model2.api
.root({
foooo: {
foooo: 0,
},
})
.commit();
const encoded1 = encoder.encode(model1);
const encoded2 = encoder.encode(model2);
expect(encoded1.byteLength > encoded2.byteLength).toBe(true);
Expand Down
49 changes: 38 additions & 11 deletions src/json-crdt/json-patch/JsonPatchDraft.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,17 @@ import {ObjectType} from '../types/lww-object/ObjectType';
import {UNDEFINED_ID} from '../../json-crdt-patch/constants';
import {toPath} from '../../json-pointer/util';
import type {Model} from '../model';
import type {Operation, OperationAdd, OperationRemove, OperationReplace, OperationMove, OperationCopy, OperationTest, OperationStrIns, OperationStrDel} from '../../json-patch';
import type {
Operation,
OperationAdd,
OperationRemove,
OperationReplace,
OperationMove,
OperationCopy,
OperationTest,
OperationStrIns,
OperationStrDel,
} from '../../json-patch';

export class JsonPatchDraft {
public readonly draft = new Draft();
Expand All @@ -18,16 +28,33 @@ export class JsonPatchDraft {
}

public applyOp(op: Operation): void {
switch(op.op) {
case 'add': this.applyOpAdd(op); break;
case 'remove': this.applyRemove(op); break;
case 'replace': this.applyReplace(op); break;
case 'move': this.applyMove(op); break;
case 'copy': this.applyCopy(op); break;
case 'test': this.applyTest(op); break;
case 'str_ins': this.applyStrIns(op); break;
case 'str_del': this.applyStrDel(op); break;
default: throw new Error('UNKNOWN_OP');
switch (op.op) {
case 'add':
this.applyOpAdd(op);
break;
case 'remove':
this.applyRemove(op);
break;
case 'replace':
this.applyReplace(op);
break;
case 'move':
this.applyMove(op);
break;
case 'copy':
this.applyCopy(op);
break;
case 'test':
this.applyTest(op);
break;
case 'str_ins':
this.applyStrIns(op);
break;
case 'str_del':
this.applyStrDel(op);
break;
default:
throw new Error('UNKNOWN_OP');
}
}

Expand Down
37 changes: 10 additions & 27 deletions src/json-crdt/json-patch/__tests__/JsonPatch.str.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ const testCases: TestCase[] = [
doc1: null,
patches: [
[{op: 'add', path: '', value: {foo: [{bar: 'baz'}]}}],
[{op: 'str_ins', path: '/foo/0/bar', pos: 3, str: '!'}]
[{op: 'str_ins', path: '/foo/0/bar', pos: 3, str: '!'}],
],
doc2: {foo: [{bar: 'baz!'}]},
},
Expand All @@ -62,73 +62,56 @@ const testCases: TestCase[] = [
doc1: null,
patches: [
[{op: 'add', path: '', value: {foo: [{bar: 'baz'}]}}],
[{op: 'str_ins', path: ['foo', 0, 'bar'], pos: 3, str: '!'}]
[{op: 'str_ins', path: ['foo', 0, 'bar'], pos: 3, str: '!'}],
],
doc2: {foo: [{bar: 'baz!'}]},
},
{
name: 'can delete a single char',
doc1: 'a',
patches: [
[{op: 'str_del', path: [], pos: 0, len: 1}]
],
patches: [[{op: 'str_del', path: [], pos: 0, len: 1}]],
doc2: '',
},
{
name: 'can delete from already empty string',
doc1: '',
patches: [
[{op: 'str_del', path: [], pos: 0, len: 1}]
],
patches: [[{op: 'str_del', path: [], pos: 0, len: 1}]],
doc2: '',
},
{
name: 'can delete at the end of string',
doc1: 'ab',
patches: [
[{op: 'str_del', path: [], pos: 1, len: 1}]
],
patches: [[{op: 'str_del', path: [], pos: 1, len: 1}]],
doc2: 'a',
},
{
name: 'can delete at the beginning of string',
doc1: 'ab',
patches: [
[{op: 'str_del', path: [], pos: 0, len: 1}]
],
patches: [[{op: 'str_del', path: [], pos: 0, len: 1}]],
doc2: 'b',
},
{
name: 'can delete in the middle of string',
doc1: 'abc',
patches: [
[{op: 'str_del', path: [], pos: 1, len: 1}]
],
patches: [[{op: 'str_del', path: [], pos: 1, len: 1}]],
doc2: 'ac',
},
{
name: 'can delete multiple chars',
doc1: '1234',
patches: [
[{op: 'str_del', path: [], pos: 1, len: 2}],
[{op: 'str_del', path: [], pos: 1, len: 5}],
],
patches: [[{op: 'str_del', path: [], pos: 1, len: 2}], [{op: 'str_del', path: [], pos: 1, len: 5}]],
doc2: '1',
},
{
name: 'handles deletion beyond end of string',
doc1: '1234',
patches: [
[{op: 'str_del', path: [], pos: 1111, len: 2}],
],
patches: [[{op: 'str_del', path: [], pos: 1111, len: 2}]],
doc2: '1234',
},
{
name: 'can delete a string in object',
doc1: {foo: '123'},
patches: [
[{op: 'str_del', path: '/foo', pos: 1, len: 2}],
],
patches: [[{op: 'str_del', path: '/foo', pos: 1, len: 2}]],
doc2: {foo: '1'},
},
];
Expand Down
Loading