Skip to content

Commit

Permalink
Merge pull request #540 from streamich/extensions-3
Browse files Browse the repository at this point in the history
Extensions 3
  • Loading branch information
streamich authored Mar 7, 2024
2 parents 2741edc + f17efa2 commit 8dbf09f
Show file tree
Hide file tree
Showing 10 changed files with 164 additions and 40 deletions.
34 changes: 34 additions & 0 deletions src/json-crdt-extensions/cnt/__demos__/docs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/* tslint:disable no-console */

/**
* Run this demo with:
*
* npx nodemon -q -x ts-node src/json-crdt-extensions/cnt/__demos__/docs.ts
*/

import {Model, s} from '../../../json-crdt';
import {CntExt} from '..';

console.clear();

const model = Model.withLogicalClock(1234);

model.ext.register(CntExt);

model.api.root({
counter: CntExt.new(1),
});
console.log(model + '');

// Excess use only ...
// 2-3 days for finding damages ...
// ..

const api = model.api.in(['counter']).asExt(CntExt);
const values = api.view();

console.log(values);

api.inc(10);

console.log(model + '');
2 changes: 2 additions & 0 deletions src/json-crdt-extensions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './mval';
export * from './cnt';
30 changes: 30 additions & 0 deletions src/json-crdt-extensions/mval/__demos__/docs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/* tslint:disable no-console */

/**
* Run this demo with:
*
* npx nodemon -q -x ts-node src/json-crdt-extensions/mval/__demos__/docs.ts
*/

import {Model, s} from '../../../json-crdt';
import {MvalExt} from '..';

console.clear();

const model = Model.withLogicalClock(1234);

model.ext.register(MvalExt);

model.api.root({
score: MvalExt.new(1),
});
console.log(model + '');

const api = model.api.in(['score']).asExt(MvalExt);
const values = api.view();

console.log(values);

api.set(s.con(2));

console.log(model + '');
11 changes: 6 additions & 5 deletions src/json-crdt-extensions/mval/__demos__/usage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,26 @@
*/

import {Model, s} from '../../../json-crdt';
import {ValueMvExt} from '..';
import {MvalExt} from '..';

console.clear();

const model = Model.withLogicalClock(1234);

model.ext.register(ValueMvExt);
model.ext.register(MvalExt);

model.api.root({
obj: {
mv: ValueMvExt.new(s.con(1)),
name: s.con('John'),
score: MvalExt.new(s.con(1)),
},
});

console.log('');
console.log('Initial value:');
console.log(model + '');

const api = model.api.in(['obj', 'mv']).asExt(ValueMvExt);
const api = model.api.in(['obj', 'score']).asExt(MvalExt);

api.set(s.con(5));

Expand All @@ -35,7 +36,7 @@ console.log(model + '');

const model2 = model.fork();

const api2 = model2.api.in(['obj', 'mv']).asExt(ValueMvExt);
const api2 = model2.api.in(['obj', 'score']).asExt(MvalExt);

api.set(s.con(10));
api2.set(s.con(20));
Expand Down
28 changes: 28 additions & 0 deletions src/json-crdt-extensions/mval/__demos__/view.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/* tslint:disable no-console */

/**
* Run this demo with:
*
* npx nodemon -q -x ts-node src/json-crdt-extensions/mval/__demos__/view.ts
*/

import {Model, s} from '../../../json-crdt';
import {MvalExt} from '..';

console.clear();

const model = Model.withLogicalClock(1234);

model.ext.register(MvalExt);

model.api.root(MvalExt.new(s.con(1)));

console.log('');
console.log('Model with extension:');
console.log(model + '');

const model2 = Model.fromBinary(model.toBinary());

console.log('');
console.log('Model not aware of extension:');
console.log(model2 + '');
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import {ValueMvExt} from '..';
import {MvalExt} from '..';
import {Model} from '../../../json-crdt/model';

test('can set new values in single fork', () => {
const model = Model.withLogicalClock();
model.ext.register(ValueMvExt);
model.ext.register(MvalExt);
model.api.root({
mv: ValueMvExt.new(1),
mv: MvalExt.new(1),
});
expect(model.view()).toEqual({mv: [1]});
const register = model.api.in(['mv']).asExt(ValueMvExt);
const register = model.api.in(['mv']).asExt(MvalExt);
register.set(2);
expect(model.view()).toEqual({mv: [2]});
register.set(3);
Expand All @@ -17,11 +17,11 @@ test('can set new values in single fork', () => {

test('removes tombstones on insert', () => {
const model = Model.withLogicalClock();
model.ext.register(ValueMvExt);
model.ext.register(MvalExt);
model.api.root({
mv: ValueMvExt.new(1),
mv: MvalExt.new(1),
});
const register = model.api.in(['mv']).asExt(ValueMvExt);
const register = model.api.in(['mv']).asExt(MvalExt);
expect(register.node.data.size()).toBe(1);
register.set(2);
expect(register.node.data.size()).toBe(1);
Expand All @@ -33,13 +33,13 @@ test('removes tombstones on insert', () => {

test('contains two values when two forks set value concurrently', () => {
const model1 = Model.withLogicalClock();
model1.ext.register(ValueMvExt);
model1.ext.register(MvalExt);
model1.api.root({
mv: ValueMvExt.new(1),
mv: MvalExt.new(1),
});
const model2 = model1.fork();
const register1 = model1.api.in(['mv']).asExt(ValueMvExt);
const register2 = model2.api.in(['mv']).asExt(ValueMvExt);
const register1 = model1.api.in(['mv']).asExt(MvalExt);
const register2 = model2.api.in(['mv']).asExt(MvalExt);
register1.set(2);
register2.set(3);
expect(model1.view()).toEqual({mv: [2]});
Expand All @@ -56,13 +56,13 @@ test('contains two values when two forks set value concurrently', () => {

test('contains one value when a fork overwrites a register', () => {
const model1 = Model.withLogicalClock();
model1.ext.register(ValueMvExt);
model1.ext.register(MvalExt);
model1.api.root({
mv: ValueMvExt.new(1),
mv: MvalExt.new(1),
});
const model2 = model1.fork();
const register1 = model1.api.in(['mv']).asExt(ValueMvExt);
const register2 = model2.api.in(['mv']).asExt(ValueMvExt);
const register1 = model1.api.in(['mv']).asExt(MvalExt);
const register2 = model2.api.in(['mv']).asExt(MvalExt);
register1.set(2);
register2.set(3);
model1.applyPatch(model2.api.flush());
Expand Down
2 changes: 1 addition & 1 deletion src/json-crdt-extensions/mval/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type {ITimestampStruct} from '../../json-crdt-patch/clock';
import type {ArrNode} from '../../json-crdt/nodes/arr/ArrNode';
import type {ExtensionDefinition} from '../../json-crdt';

export const ValueMvExt: ExtensionDefinition<ArrNode, ValueMv, ValueMvApi> = {
export const MvalExt: ExtensionDefinition<ArrNode, ValueMv, ValueMvApi> = {
id: ExtensionId.mval,
name: 'mval',
new: (value: unknown | ITimestampStruct) =>
Expand Down
2 changes: 1 addition & 1 deletion src/json-crdt/extensions/Extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export class Extensions implements Printable {
.map((k) => +k)
.sort();
return (
this.constructor.name +
'extensions' +
printTree(
tab,
keys.map((k) => (tab) => `${k}: ${this.ext[k].name}`),
Expand Down
29 changes: 29 additions & 0 deletions src/json-crdt/extensions/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Extensions

Extensions allow to create new node types out of the existing built-in types:
`con`, `val`, `obj`, `vec`, `str`, `bin`, `arr`.

Each extension has a globally unique ID, which is an 8-bit unsigned integer.
Thus, only 256 extensions can be defined at the same time.

Extensions do not modify in any shape the JSON CRDT, nor JSON CRDT Patch
protocols, instead they build on top of the `vec` node type. An extension node
is a `vec` node with a specific structure, and a specific interpretation of the
elements of the `vec` node.

An extension `vec` node follows the following structure: it is a 2-tuple, where
the first element in the extension *header* and the second element is the
extension *payload*.

The extension *header* is a `con` node, which holds a 3 byte `Uint8Array` with
the following octets: (1) the extension ID, (2) the session ID modulo 256, and
(3) the time sequence modulo 256.

The extension *payload* is any JSON CRDT node with any value, which is specific
to the extension.

```
vec
├─ 0: con Uin8Array { <ext_id>, <sid_mod_256>, <time_mod_256> }
└─ 1: any
```
36 changes: 18 additions & 18 deletions src/json-crdt/nodes/vec/__tests__/extension.spec.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import {ValueMvExt} from '../../../../json-crdt-extensions/mval';
import {MvalExt} from '../../../../json-crdt-extensions/mval';
import {konst} from '../../../../json-crdt-patch/builder/Konst';
import {Model} from '../../../../json-crdt/model';

test('can specify extension name', () => {
expect(ValueMvExt.name).toBe('mval');
expect(MvalExt.name).toBe('mval');
});

test('can create a new multi-value register', () => {
const model = Model.withLogicalClock();
model.ext.register(ValueMvExt);
model.ext.register(MvalExt);
model.api.root({
mv: ValueMvExt.new(),
mv: MvalExt.new(),
});
expect(model.view()).toEqual({
mv: [],
Expand All @@ -19,9 +19,9 @@ test('can create a new multi-value register', () => {

test('can provide initial value', () => {
const model = Model.withLogicalClock();
model.ext.register(ValueMvExt);
model.ext.register(MvalExt);
model.api.root({
mv: ValueMvExt.new({foo: 'bar'}),
mv: MvalExt.new({foo: 'bar'}),
});
expect(model.view()).toEqual({
mv: [{foo: 'bar'}],
Expand All @@ -30,22 +30,22 @@ test('can provide initial value', () => {

test('can read view from node or API node', () => {
const model = Model.withLogicalClock();
model.ext.register(ValueMvExt);
model.ext.register(MvalExt);
model.api.root({
mv: ValueMvExt.new('foo'),
mv: MvalExt.new('foo'),
});
const api = model.api.in('mv').asExt(ValueMvExt);
const api = model.api.in('mv').asExt(MvalExt);
expect(api.view()).toEqual(['foo']);
expect(api.node.view()).toEqual(['foo']);
});

test('exposes API to edit extension data', () => {
const model = Model.withLogicalClock();
model.ext.register(ValueMvExt);
model.ext.register(MvalExt);
model.api.root({
mv: ValueMvExt.new(),
mv: MvalExt.new(),
});
const nodeApi = model.api.in('mv').asExt(ValueMvExt);
const nodeApi = model.api.in('mv').asExt(MvalExt);
nodeApi.set(konst('lol'));
expect(model.view()).toEqual({
mv: ['lol'],
Expand All @@ -55,9 +55,9 @@ test('exposes API to edit extension data', () => {
describe('extension validity checks', () => {
test('does not treat ArrNode as extension if header is too long', () => {
const model = Model.withLogicalClock();
model.ext.register(ValueMvExt);
model.ext.register(MvalExt);
model.api.root({
mv: ValueMvExt.new(),
mv: MvalExt.new(),
});
const buf = new Uint8Array(4);
buf.set(model.api.const(['mv', 0]).node.view() as Uint8Array, 0);
Expand All @@ -70,9 +70,9 @@ describe('extension validity checks', () => {

test('does not treat ArrNode as extension if header sid is wrong', () => {
const model = Model.withLogicalClock();
model.ext.register(ValueMvExt);
model.ext.register(MvalExt);
model.api.root({
mv: ValueMvExt.new(),
mv: MvalExt.new(),
});
const buf = model.api.const(['mv', 0]).node.view() as Uint8Array;
buf[1] += 1;
Expand All @@ -84,9 +84,9 @@ describe('extension validity checks', () => {

test('does not treat ArrNode as extension if header time is wrong', () => {
const model = Model.withLogicalClock();
model.ext.register(ValueMvExt);
model.ext.register(MvalExt);
model.api.root({
mv: ValueMvExt.new(),
mv: MvalExt.new(),
});
const buf = model.api.const(['mv', 0]).node.view() as Uint8Array;
buf[2] += 1;
Expand Down

0 comments on commit 8dbf09f

Please sign in to comment.