UNPKG

json-joy

Version:

Collection of libraries for building collaborative editing apps.

466 lines 13.3 kB
import { isUint8Array } from '@jsonjoy.com/buffers/lib/isUint8Array'; import { Timestamp } from './clock'; import * as print from '../util/print'; import { printTree } from 'tree-dump'; const maybeConst = (x) => { switch (typeof x) { case 'number': case 'boolean': case 'undefined': return true; case 'object': return x === null || x instanceof Timestamp; default: return false; } }; /** * @category Patch */ export class NodeBuilder { _build; constructor(_build) { this._build = _build; } build(builder) { return this._build?.(builder) ?? builder.con(void 0); } } /** * @category Patch */ export class SchemaNode extends NodeBuilder { toString(tab) { return this.type; } } /** * This namespace contains all the node builders. Each node builder is a * schema for a specific node type. Each node builder has a `build` method * that takes a {@link NodeBuilder} and returns the ID of the node. */ export var nodes; (function (nodes) { /** * The `con` class represents a "con" JSON CRDT node. As the generic type * parameter, it takes the type of the raw value. * * Example: * * ```ts * s.con(0); * s.con(''); * s.con<number>(123); * s.con<0 | 1>(0); * ``` */ class con extends SchemaNode { raw; type = 'con'; constructor(raw) { super(); this.raw = raw; } build(builder) { return builder.con(this.raw); } toString(tab) { return this.type + ' ' + print.con(this.raw); } } nodes.con = con; /** * The `str` class represents a "str" JSON CRDT node. As the generic type * parameter, it takes the type of the raw value. * * Example: * * ```ts * s.str(''); * s.str('hello'); * s.str<string>('world'); * s.str<'' | 'hello' | 'world'>('hello'); * ``` */ class str extends SchemaNode { raw; type = 'str'; constructor(raw) { super(); this.raw = raw; } build(builder) { return builder.json(this.raw); } toString(tab) { return this.type + ' ' + print.con(this.raw); } } nodes.str = str; /** * The `bin` class represents a "bin" JSON CRDT node. */ class bin extends SchemaNode { raw; type = 'bin'; constructor(raw) { super(); this.raw = raw; } build(builder) { return builder.json(this.raw); } toString(tab) { return this.type + ' ' + print.bin(this.raw); } } nodes.bin = bin; /** * The `val` class represents a "val" JSON CRDT node. As the generic type * parameter, it takes the type of the inner node builder. * * Example: * * ```ts * s.val(s.con(0)); * s.val(s.str('')); * s.val(s.str('hello')); * ``` */ class val extends SchemaNode { value; type = 'val'; constructor(value) { super(); this.value = value; } build(builder) { const valId = builder.val(); const valueId = this.value.build(builder); builder.setVal(valId, valueId); return valId; } toString(tab) { return this.type + printTree(tab, [(tab) => this.value.toString(tab)]); } } nodes.val = val; /** * The `vec` class represents a "vec" JSON CRDT node. As the generic type * parameter, it takes a tuple of node builders. * * Example: * * ```ts * s.vec(s.con(0), s.con(1)); * s.vec(s.str(''), s.str('hello')); * ``` */ class vec extends SchemaNode { value; type = 'vec'; constructor(value) { super(); this.value = value; } build(builder) { const vecId = builder.vec(); const value = this.value; const length = value.length; if (length) { const elementPairs = []; for (let i = 0; i < length; i++) { const element = value[i]; if (!element) continue; const elementId = element.build(builder); elementPairs.push([i, elementId]); } builder.insVec(vecId, elementPairs); } return vecId; } toString(tab) { return (this.type + printTree(tab, [ ...this.value.map((child, i) => (tab) => `${i}: ${child ? child.toString(tab) : print.line(child)}`), ])); } } nodes.vec = vec; /** * The `obj` class represents a "obj" JSON CRDT node. As the generic type * parameter, it takes a record of node builders. The optional generic type * parameter is a record of optional keys. * * Example: * * ```ts * s.obj({ * name: s.str(''), * age: s.con(0), * }); * ``` * * Specify optional keys as the second argument: * * ```ts * s.obj( * { * href: s.str('https://example.com'), * }, * { * title: s.str(''), * }, * ) * ``` * * Or, specify only the type, using the `optional` method: * * ```ts * s.obj({ * href: s.str('https://example.com'), * }) * .optional<nodes.obj({ * title: nodes.str, * })>() * ``` */ class obj extends SchemaNode { obj; opt; type = 'obj'; constructor(obj, opt) { super(); this.obj = obj; this.opt = opt; } optional() { return this; } build(builder) { const objId = builder.obj(); const keyValuePairs = []; const merged = { ...this.obj, ...this.opt }; const keys = Object.keys(merged); const length = keys.length; if (length) { for (let i = 0; i < length; i++) { const key = keys[i]; const valueId = merged[key].build(builder); keyValuePairs.push([key, valueId]); } builder.insObj(objId, keyValuePairs); } return objId; } toString(tab = '') { return (this.type + printTree(tab, [ ...[...Object.entries(this.obj)].map(([key, child]) => (tab) => print.line(key) + printTree(tab + ' ', [(tab) => child.toString(tab)])), ...[...Object.entries(this.opt ?? [])].map(([key, child]) => (tab) => print.line(key) + '?' + printTree(tab + ' ', [(tab) => child.toString(tab)])), ])); } } nodes.obj = obj; /** * The `arr` class represents a "arr" JSON CRDT node. As the generic type * parameter, it an array of node builders. * * Example: * * ```ts * s.arr([s.con(0), s.con(1)]); * s.arr([s.str(''), s.str('hello')]); * ``` */ class arr extends SchemaNode { arr; type = 'arr'; constructor(arr) { super(); this.arr = arr; } build(builder) { const arrId = builder.arr(); const arr = this.arr; const length = arr.length; if (length) { const valueIds = []; for (let i = 0; i < length; i++) valueIds.push(arr[i].build(builder)); builder.insArr(arrId, arrId, valueIds); } return arrId; } toString(tab) { return (this.type + printTree(tab, [ ...this.arr.map((child, i) => (tab) => `[${i}]: ${child ? child.toString(tab) : print.line(child)}`), ])); } } nodes.arr = arr; /** * Creates an extension node schema. The extension node is a tuple with a * sentinel header and a data node. The sentinel header is a 3-byte * {@link Uint8Array}, which makes this "vec" node to be treated as an * extension "ext" node. * * The 3-byte header consists of the extension ID, the SID of the tuple ID, * and the time of the tuple ID: * * - 1 byte for the extension id * - 1 byte for the sid of the tuple id, modulo 256 * - 1 byte for the time of the tuple id, modulo 256 */ class ext extends SchemaNode { id; data; type = 'ext'; /** * @param id A unique extension ID. * @param data Schema of the data node of the extension. */ constructor(id, data) { super(); this.id = id; this.data = data; } build(builder) { const buf = new Uint8Array([this.id, 0, 0]); const tupleId = builder.vec(); buf[1] = tupleId.sid % 256; buf[2] = tupleId.time % 256; builder.insVec(tupleId, [ [0, builder.constOrJson(s.con(buf))], [1, this.data.build(builder)], ]); return tupleId; } toString(tab) { return this.type + '(' + this.id + ')' + printTree(tab, [(tab) => this.data.toString(tab)]); } } nodes.ext = ext; })(nodes || (nodes = {})); /** * Schema builder. Use this to create a JSON CRDT model schema and the default * value. * * Example: * * ```typescript * const schema = s.obj({ * name: s.str(''), * age: s.con(0), * tags: s.arr<nodes.con<string>>([]), * }); * ``` */ export const schema = { /** * Creates a "con" node schema and the default value. * * @param raw Raw default value. */ con: (raw) => new nodes.con(raw), /** * Creates a "str" node schema and the default value. * * @param str Default value. */ str: (str) => new nodes.str(str || ''), /** * Creates a "bin" node schema and the default value. * * @param bin Default value. */ bin: (bin) => new nodes.bin(bin), /** * Creates a "val" node schema and the default value. * * @param val Default value. */ val: (val) => new nodes.val(val), /** * Creates a "vec" node schema and the default value. * * @param vec Default value. */ vec: (...vec) => new nodes.vec(vec), /** * Creates a "obj" node schema and the default value. * * @param obj Default value, required object keys. * @param opt Default value of optional object keys. */ obj: (obj, opt) => new nodes.obj(obj, opt), /** * This is an alias for {@link schema.obj}. It creates a "map" node schema, * which is an object where a key can be any string and the value is of the * same type. * * @param obj Default value. */ map: (obj) => schema.obj(obj), /** * Creates an "arr" node schema and the default value. * * @param arr Default value. */ arr: (arr) => new nodes.arr(arr), /** * Recursively creates a node tree from any POJO. */ json: (value) => { switch (typeof value) { case 'object': { if (!value) return s.val(s.con(value)); if (value instanceof NodeBuilder) return value; else if (Array.isArray(value)) return s.arr(value.map((v) => s.json(v))); else if (isUint8Array(value)) return s.bin(value); else if (value instanceof Timestamp) return s.val(s.con(value)); else { const obj = {}; const keys = Object.keys(value); for (const key of keys) obj[key] = s.jsonCon(value[key]); return s.obj(obj); } } case 'string': return s.str(value); default: return s.val(s.con(value)); } }, /** * Recursively creates a schema node tree from any POJO. Same as {@link json}, but * converts constant values to {@link nodes.con} nodes, instead wrapping them into * {@link nodes.val} nodes. * * @todo Remove this once "arr" RGA supports in-place updates. */ jsonCon: (value) => { return maybeConst(value) ? s.con(value) : s.json(value); }, /** * Creates an extension node schema. * * @param id A unique extension ID. * @param data Schema of the data node of the extension. */ ext: (id, data) => new nodes.ext(id, data), }; /** * Schema builder. Use this to create a JSON CRDT model schema and the default * value. Alias for {@link schema}. */ export const s = schema; //# sourceMappingURL=schema.js.map