diff --git a/src/json-crdt-extensions/cnt/__demos__/docs.ts b/src/json-crdt-extensions/cnt/__demos__/docs.ts new file mode 100644 index 0000000000..aff2ecb0cd --- /dev/null +++ b/src/json-crdt-extensions/cnt/__demos__/docs.ts @@ -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 + ''); diff --git a/src/json-crdt-extensions/index.ts b/src/json-crdt-extensions/index.ts new file mode 100644 index 0000000000..745c3b1ab5 --- /dev/null +++ b/src/json-crdt-extensions/index.ts @@ -0,0 +1,2 @@ +export * from './mval'; +export * from './cnt'; diff --git a/src/json-crdt-extensions/mval/__demos__/docs.ts b/src/json-crdt-extensions/mval/__demos__/docs.ts new file mode 100644 index 0000000000..95d3d86463 --- /dev/null +++ b/src/json-crdt-extensions/mval/__demos__/docs.ts @@ -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 + ''); diff --git a/src/json-crdt-extensions/mval/__demos__/usage.ts b/src/json-crdt-extensions/mval/__demos__/usage.ts index 638c5814d7..11d6b58cfc 100644 --- a/src/json-crdt-extensions/mval/__demos__/usage.ts +++ b/src/json-crdt-extensions/mval/__demos__/usage.ts @@ -7,17 +7,18 @@ */ 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)), }, }); @@ -25,7 +26,7 @@ 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)); @@ -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)); diff --git a/src/json-crdt-extensions/mval/__demos__/view.ts b/src/json-crdt-extensions/mval/__demos__/view.ts new file mode 100644 index 0000000000..bd2f26f497 --- /dev/null +++ b/src/json-crdt-extensions/mval/__demos__/view.ts @@ -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 + ''); diff --git a/src/json-crdt-extensions/mval/__tests__/ValueMv.spec.ts b/src/json-crdt-extensions/mval/__tests__/MvalExt.spec.ts similarity index 74% rename from src/json-crdt-extensions/mval/__tests__/ValueMv.spec.ts rename to src/json-crdt-extensions/mval/__tests__/MvalExt.spec.ts index 97cffa0eb9..d94e8bc45e 100644 --- a/src/json-crdt-extensions/mval/__tests__/ValueMv.spec.ts +++ b/src/json-crdt-extensions/mval/__tests__/MvalExt.spec.ts @@ -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); @@ -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); @@ -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]}); @@ -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()); diff --git a/src/json-crdt-extensions/mval/index.ts b/src/json-crdt-extensions/mval/index.ts index 5e1c7a074e..b1e64acc9f 100644 --- a/src/json-crdt-extensions/mval/index.ts +++ b/src/json-crdt-extensions/mval/index.ts @@ -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 = { +export const MvalExt: ExtensionDefinition = { id: ExtensionId.mval, name: 'mval', new: (value: unknown | ITimestampStruct) => diff --git a/src/json-crdt/extensions/Extensions.ts b/src/json-crdt/extensions/Extensions.ts index e3fd7e4ebb..65143f027b 100644 --- a/src/json-crdt/extensions/Extensions.ts +++ b/src/json-crdt/extensions/Extensions.ts @@ -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}`), diff --git a/src/json-crdt/extensions/README.md b/src/json-crdt/extensions/README.md new file mode 100644 index 0000000000..eee667078c --- /dev/null +++ b/src/json-crdt/extensions/README.md @@ -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 { , , } +└─ 1: any +``` diff --git a/src/json-crdt/nodes/vec/__tests__/extension.spec.ts b/src/json-crdt/nodes/vec/__tests__/extension.spec.ts index 8e6ae482cc..d5f4a78ac7 100644 --- a/src/json-crdt/nodes/vec/__tests__/extension.spec.ts +++ b/src/json-crdt/nodes/vec/__tests__/extension.spec.ts @@ -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: [], @@ -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'}], @@ -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'], @@ -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); @@ -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; @@ -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;