Skip to content

Commit

Permalink
Merge pull request #535 from streamich/gcnt
Browse files Browse the repository at this point in the history
G-Counter (Grow-only counter) CRDT extension
  • Loading branch information
streamich authored Mar 5, 2024
2 parents 1d3b3e9 + b198c48 commit 4fe604f
Show file tree
Hide file tree
Showing 3 changed files with 117 additions and 2 deletions.
6 changes: 4 additions & 2 deletions src/json-crdt-extensions/constants.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
export const enum ExtensionId {
mval = 0,
Peritext = 1,
QuillDelta = 2,
gcnt = 1,
pncnt = 2,
peritext = 3,
quill = 4,
}
35 changes: 35 additions & 0 deletions src/json-crdt-extensions/gcnt/__tests__/GcntExt.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import {GcntExt} from '..';
import {Model} from '../../../json-crdt/model';

test('can set new values in single fork', () => {
const model = Model.withLogicalClock();
model.ext.register(GcntExt);
model.api.root({
counter: GcntExt.new(24),
});
expect(model.view()).toEqual({counter: 24});
const counter = model.api.in(['counter']).asExt(GcntExt);
expect(counter.view()).toBe(24);
counter.inc(2);
expect(model.view()).toEqual({counter: 26});
counter.inc(-10);
expect(counter.view()).toBe(16);
});

test('two concurrent users can increment the counter', () => {
const model = Model.withLogicalClock();
model.ext.register(GcntExt);
model.api.root({
counter: GcntExt.new(),
});
expect(model.view()).toEqual({counter: 0});
const counter = model.api.in(['counter']).asExt(GcntExt);
expect(counter.view()).toBe(0);
const model2 = model.fork();
const counter2 = model2.api.in(['counter']).asExt(GcntExt);
counter.inc(2);
counter2.inc(3);
model.applyPatch(model2.api.flush());
expect(model.view()).toEqual({counter: 5});
expect(counter.view()).toBe(5);
});
78 changes: 78 additions & 0 deletions src/json-crdt-extensions/gcnt/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import {delayed} from '../../json-crdt-patch/builder/DelayedValueBuilder';
import {ext} from '../../json-crdt/extensions';
import {ExtensionId} from '../constants';
import {printTree} from '../../util/print/printTree';
import {NodeApi} from '../../json-crdt/model/api/nodes';
import type {ExtensionDefinition, ObjNode} from '../../json-crdt';
import type {ITimestampStruct} from '../../json-crdt-patch/clock';
import type {ExtensionJsonNode, JsonNode} from '../../json-crdt';
import type {Printable} from '../../util/print/types';
import type {ExtensionApi} from '../../json-crdt';

const name = 'gcnt';

class GcntNode implements ExtensionJsonNode, Printable {
public readonly id: ITimestampStruct;

constructor(public readonly data: ObjNode) {
this.id = data.id;
}

// -------------------------------------------------------- ExtensionJsonNode

public name(): string {
return name;
}

public view(): number {
const obj = this.data.view();
let sum: number = 0;
for (const key in obj) sum += Number(obj[key]);
return sum;
}

public children(callback: (node: JsonNode) => void): void {}

public child?(): JsonNode | undefined {
return this.data;
}

public container(): JsonNode | undefined {
return this.data.container();
}

public api: undefined | unknown = undefined;

// ---------------------------------------------------------------- Printable

public toString(tab?: string): string {
return `${this.name()} (${this.view()})` + printTree(tab, [(tab) => this.data.toString(tab)]);
}
}

class GcntApi extends NodeApi<GcntNode> implements ExtensionApi<GcntNode> {
public inc(increment: number): this {
const {api, node} = this;
const sid = api.model.clock.sid;
const sidStr = sid.toString(36);
const value = Number(node.data.get(sidStr)?.view() ?? 0);
const newValue = value + increment;
const obj = api.wrap(node.data);
obj.set({
[sidStr]: newValue,
});
return this;
}
}

export const GcntExt: ExtensionDefinition<ObjNode, GcntNode, GcntApi> = {
id: ExtensionId.gcnt,
name,
new: (value?: number, sid: number = 0) =>
ext(
ExtensionId.gcnt,
delayed((builder) => builder.constOrJson(value ? {[sid]: value} : {})),
),
Node: GcntNode,
Api: GcntApi,
};

0 comments on commit 4fe604f

Please sign in to comment.