Skip to content

Commit

Permalink
Merge pull request #559 from streamich/revert
Browse files Browse the repository at this point in the history
Revert patches, support undo/redo stacks
  • Loading branch information
streamich authored Mar 29, 2024
2 parents 0a1e5be + 0723a09 commit 8829f98
Show file tree
Hide file tree
Showing 8 changed files with 264 additions and 21 deletions.
41 changes: 41 additions & 0 deletions src/json-crdt/history/UndoRedoStack.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
export interface UndoItem {
undo(): RedoItem;
}

export interface RedoItem {
redo(): UndoItem;
}

export class UndoRedoStack {
private undoStack: UndoItem[] = [];
private redoStack: RedoItem[] = [];

public undoLength(): number {
return this.undoStack.length;
}

public redoLength(): number {
return this.redoStack.length;
}

public push(undo: UndoItem): RedoItem[] {
const redoStack = this.redoStack;
this.redoStack = [];
this.undoStack.push(undo);
return redoStack;
}

public undo(): void {
const undo = this.undoStack.pop();
if (!undo) return;
const redo = undo.undo();
this.redoStack.push(redo);
}

public redo(): void {
const redo = this.redoStack.pop();
if (!redo) return;
const undo = redo.redo();
this.undoStack.push(undo);
}
}
5 changes: 3 additions & 2 deletions src/json-crdt/model/Model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import {JsonCrdtPatchOperation, Patch} from '../../json-crdt-patch/Patch';
import {ModelApi} from './api/ModelApi';
import {ORIGIN, SESSION, SYSTEM_SESSION_TIME} from '../../json-crdt-patch/constants';
import {randomSessionId} from './util';
import {RootNode, ValNode, VecNode, ObjNode, StrNode, BinNode, ArrNode, BuilderNodeToJsonNode} from '../nodes';
import {RootNode, ValNode, VecNode, ObjNode, StrNode, BinNode, ArrNode} from '../nodes';
import {SchemaToJsonNode} from '../schema/types';
import {printTree} from '../../util/print/printTree';
import {Extensions} from '../extensions/Extensions';
import {AvlMap} from '../../util/trees/avl/AvlMap';
Expand Down Expand Up @@ -378,7 +379,7 @@ export class Model<N extends JsonNode = JsonNode<any>> implements Printable {
* @param schema The schema to set for this model.
* @returns Strictly typed model.
*/
public setSchema<S extends NodeBuilder>(schema: S): Model<BuilderNodeToJsonNode<S>> {
public setSchema<S extends NodeBuilder>(schema: S): Model<SchemaToJsonNode<S>> {
if (this.clock.time < 2) this.api.root(schema);
return <any>this;
}
Expand Down
2 changes: 2 additions & 0 deletions src/json-crdt/model/api/ModelApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,8 @@ export class ModelApi<N extends JsonNode = JsonNode> implements SyncStore<JsonNo
* Locates a `con` node and returns a local changes API for it. If the node
* doesn't exist or the node at the path is not a `con` node, throws an error.
*
* @todo Rename to `con`.
*
* @param path Path at which to locate a node.
* @returns A local changes API for a `con` node.
*/
Expand Down
19 changes: 0 additions & 19 deletions src/json-crdt/nodes/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import type {nodes as builder} from '../../json-crdt-patch';
import type * as nodes from './nodes';
import type {Identifiable} from '../../json-crdt-patch/types';

/**
Expand Down Expand Up @@ -47,20 +45,3 @@ export interface JsonNode<View = unknown> extends Identifiable {
}

export type JsonNodeView<N> = N extends JsonNode<infer V> ? V : {[K in keyof N]: JsonNodeView<N[K]>};

// prettier-ignore
export type BuilderNodeToJsonNode<S> = S extends builder.str<infer T>
? nodes.StrNode<T>
: S extends builder.bin
? nodes.BinNode
: S extends builder.con<infer T>
? nodes.ConNode<T>
: S extends builder.val<infer T>
? nodes.ValNode<BuilderNodeToJsonNode<T>>
: S extends builder.vec<infer T>
? nodes.VecNode<{[K in keyof T]: BuilderNodeToJsonNode<T[K]>}>
: S extends builder.obj<infer T>
? nodes.ObjNode<{[K in keyof T]: BuilderNodeToJsonNode<T[K]>}>
: S extends builder.arr<infer T>
? nodes.ArrNode<BuilderNodeToJsonNode<T>>
: JsonNode;
89 changes: 89 additions & 0 deletions src/json-crdt/schema/__tests__/toSchema.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import {NodeBuilder, s, nodes} from '../../../json-crdt-patch';
import {deepEqual} from '../../../json-equal/deepEqual';
import {cmpUint8Array} from '../../../util/buffers/cmpUint8Array';
import {Model} from '../../model';
import {toSchema} from '../toSchema';

const cmp = (a: NodeBuilder, b: NodeBuilder): boolean => {
if (a instanceof nodes.con && b instanceof nodes.con) return deepEqual(a.raw, b.raw);
else if (a instanceof nodes.val && b instanceof nodes.val) return cmp(a.value, b.value);
else if (a instanceof nodes.obj && b instanceof nodes.obj) {
const objAKeys = Object.keys(a.obj);
const objBKeys = Object.keys(a.obj);
const objALen = objAKeys.length;
const objBLen = objBKeys.length;
if (objALen !== objBLen) return false;
const optAKeys = Object.keys(a.opt || {});
const optBKeys = Object.keys(b.opt || {});
const optALen = optAKeys.length;
const optBLen = optBKeys.length;
if (optALen !== optBLen) return false;
for (let i = 0; i < objALen; i++) {
const key = objAKeys[i];
if (!cmp(a.obj[key], b.obj[key])) return false;
}
for (let i = 0; i < optALen; i++) {
const key = optAKeys[i];
if (!cmp(a.opt![key], b.opt![key])) return false;
}
return true;
} else if (a instanceof nodes.vec && b instanceof nodes.vec) {
const vecA = a.value;
const vecB = b.value;
const len = vecA.length;
if (len !== vecB.length) return false;
for (let i = 0; i < len; i++) if (!cmp(vecA[i], vecA[i])) return false;
return true;
} else if (a instanceof nodes.str && b instanceof nodes.str) return a.raw === b.raw;
else if (a instanceof nodes.bin && b instanceof nodes.bin) return cmpUint8Array(a.raw, b.raw);
else if (a instanceof nodes.arr && b instanceof nodes.arr) {
const arrA = a.arr;
const arrB = b.arr;
const len = arrA.length;
if (len !== arrB.length) return false;
for (let i = 0; i < len; i++) if (!cmp(arrA[i], arrB[i])) return false;
return true;
}
return false;
};

test('can infer schema of a document nodes', () => {
const con = s.con('con');
const str = s.str('hello');
const obj = s.obj({
id: s.con('id'),
val: s.val(s.str('world')),
});
const schema = s.obj({
con,
str,
bin: s.bin(new Uint8Array([1, 2, 3])),
obj,
vec: s.vec(s.con(1), s.con({foo: 'bar'})),
arr: s.arr([s.con(1), s.con({foo: 'bar'})]),
});
const model = Model.withLogicalClock().setSchema(schema);
const node = model.root.node();
const schema2 = toSchema(node);
expect(cmp(schema, schema2)).toBe(true);
const conSchema = toSchema(model.api.const('con').node);
expect(cmp(con, conSchema)).toBe(true);
expect(cmp(str, conSchema)).toBe(false);
const strSchema = toSchema(model.api.str('str').node);
expect(cmp(str, strSchema)).toBe(true);
expect(cmp(con, strSchema)).toBe(false);
const objSchema = toSchema(model.api.obj('obj').node);
expect(cmp(obj, objSchema)).toBe(true);
expect(cmp(con, objSchema)).toBe(false);
});

test('can infer schema of a typed model', () => {
const schema = s.obj({
id: s.con('id'),
val: s.val(s.str('world')),
});
const model = Model.withLogicalClock().setSchema(schema);
const schema2 = toSchema(model.root.node());
expect(schema2.obj.id).toBeInstanceOf(nodes.con);
expect(schema2.obj.val).toBeInstanceOf(nodes.val);
});
57 changes: 57 additions & 0 deletions src/json-crdt/schema/__tests__/types.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import {s} from '../../../json-crdt-patch';
import {Model} from '../../model';
import {JsonNodeToSchema, SchemaToJsonNode} from '../types';

describe('can infer schema of JSON CRDT nodes', () => {
test('con', () => {
const schema1 = s.con(123);
const schema2: JsonNodeToSchema<SchemaToJsonNode<typeof schema1>> = schema1;
});

test('val', () => {
const schema1 = s.val(s.con(true));
const schema2: JsonNodeToSchema<SchemaToJsonNode<typeof schema1>> = schema1;
});

test('obj', () => {
const schema1 = s.obj({
hello: s.con('world'),
});
const schema2: JsonNodeToSchema<SchemaToJsonNode<typeof schema1>> = schema1;
});

test('vec', () => {
const schema1 = s.vec(s.con(1), s.val(s.con(2)));
const schema2: JsonNodeToSchema<SchemaToJsonNode<typeof schema1>> = schema1;
});

test('str', () => {
const schema1 = s.str('asdf');
const schema2: JsonNodeToSchema<SchemaToJsonNode<typeof schema1>> = schema1;
});

test('bin', () => {
const schema1 = s.bin(new Uint8Array([1, 2, 3]));
const schema2: JsonNodeToSchema<SchemaToJsonNode<typeof schema1>> = schema1;
});

test('arr', () => {
const schema1 = s.arr([s.con(1), s.val(s.con(2))]);
const schema2: JsonNodeToSchema<SchemaToJsonNode<typeof schema1>> = schema1;
});

test('from typed model', () => {
const model = Model.withLogicalClock().setSchema(
s.obj({
id: s.con('asdf'),
age: s.val(s.con(42)),
}),
);
type Node = ReturnType<typeof model.root.node>;
type Schema = JsonNodeToSchema<Node>;
const schema: Schema = s.obj({
id: s.con('asdf'),
age: s.val(s.con(42)),
});
});
});
36 changes: 36 additions & 0 deletions src/json-crdt/schema/toSchema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import {JsonNode, ConNode, ValNode, ObjNode, VecNode, StrNode, BinNode, ArrNode} from '../nodes';
import {NodeBuilder, s} from '../../json-crdt-patch';
import type {JsonNodeToSchema} from './types';

/**
* Converts any JSON CRDT node to a schema representation. The schema can be
* used to copy the structure of the JSON CRDT node to another document or
* another location in the same document.
*
* @param node JSON CRDT node to recursively convert to schema.
* @returns Schema representation of the JSON CRDT node.
*/
export const toSchema = <N extends JsonNode<any>>(node: N): JsonNodeToSchema<N> => {
if (node instanceof ConNode) return s.con(node.val) as any;
if (node instanceof ValNode) return s.val(toSchema(node.node())) as any;
if (node instanceof ObjNode) {
const obj: Record<string, NodeBuilder> = {};
node.nodes((child, key) => (obj[key] = toSchema(child)));
return s.obj(obj) as any;
}
if (node instanceof VecNode) {
const arr: NodeBuilder[] = [];
node.children((child) => arr.push(toSchema(child)));
return s.vec(...arr) as any;
}
if (node instanceof StrNode) return s.str(node.view()) as any;
if (node instanceof BinNode) return s.bin(node.view()) as any;
if (node instanceof ArrNode) {
const arr: NodeBuilder[] = [];
node.children((child) => {
if (child) arr.push(toSchema(child));
});
return s.arr(arr) as any;
}
return s.con(undefined) as any;
};
36 changes: 36 additions & 0 deletions src/json-crdt/schema/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type {nodes as builder} from '../../json-crdt-patch';
import type * as nodes from '../nodes';

// prettier-ignore
export type SchemaToJsonNode<S> = S extends builder.str<infer T>
? nodes.StrNode<T>
: S extends builder.bin
? nodes.BinNode
: S extends builder.con<infer T>
? nodes.ConNode<T>
: S extends builder.val<infer T>
? nodes.ValNode<SchemaToJsonNode<T>>
: S extends builder.vec<infer T>
? nodes.VecNode<{[K in keyof T]: SchemaToJsonNode<T[K]>}>
: S extends builder.obj<infer T>
? nodes.ObjNode<{[K in keyof T]: SchemaToJsonNode<T[K]>}>
: S extends builder.arr<infer T>
? nodes.ArrNode<SchemaToJsonNode<T>>
: nodes.JsonNode;

// prettier-ignore
export type JsonNodeToSchema<N> = N extends nodes.StrNode<infer T>
? builder.str<T>
: N extends nodes.BinNode
? builder.bin
: N extends nodes.ConNode<infer T>
? builder.con<T>
: N extends nodes.ValNode<infer T>
? builder.val<JsonNodeToSchema<T>>
: N extends nodes.VecNode<infer T>
? builder.vec<{[K in keyof T]: JsonNodeToSchema<T[K]>}>
: N extends nodes.ObjNode<infer T>
? builder.obj<{[K in keyof T]: JsonNodeToSchema<T[K]>}>
: N extends nodes.ArrNode<infer T>
? builder.arr<JsonNodeToSchema<T>>
: builder.con<undefined>;

0 comments on commit 8829f98

Please sign in to comment.