diff --git a/packages/@ot-doc/document/src/lib2/algebra.ts b/packages/@ot-doc/document/src/lib2/algebra.ts new file mode 100644 index 0000000..3ed9b2e --- /dev/null +++ b/packages/@ot-doc/document/src/lib2/algebra.ts @@ -0,0 +1,18 @@ +import { Maybe } from "./maybe"; + +export type Constant = () => T; +export type UnaryOperator = (a: T) => T; +export type PartialUnaryOperator = (a: T) => Maybe; +export type BinaryOperator = (a: T) => UnaryOperator; +export type PartialBinaryOperator = (a: T) => PartialUnaryOperator; +export type Predicate = (a: T) => boolean; +export type Relation = (a: T) => Predicate; + +// Use $PascalCase to represent a type class +export type $Eq = { + equals: Relation; +}; + +export type $Ord = { + lessThan: Relation; +}; diff --git a/packages/@ot-doc/document/src/lib2/document.ts b/packages/@ot-doc/document/src/lib2/document.ts new file mode 100644 index 0000000..5470bae --- /dev/null +++ b/packages/@ot-doc/document/src/lib2/document.ts @@ -0,0 +1,31 @@ +import { Constant, PartialBinaryOperator, PartialUnaryOperator, UnaryOperator } from "./algebra"; + +export type $Init = { + initial: Constant; +}; + +export const $init = (cp: Cp): $Init => ({ initial: () => cp }); + +export type $Comp = { + compose: (op: Op) => PartialUnaryOperator; +}; + +export type $Inv = { + invert: UnaryOperator; +}; + +export type $Idn = { + identity: Constant; +}; + +export const $idn = (op: Op): $Idn => ({ identity: () => op }); + +export type $Tran = { + transform: PartialBinaryOperator; +}; + +export type $BaseDoc = $Init & $Comp; + +export type $InvDoc = $BaseDoc & $Inv; + +export type $FullDoc = $InvDoc & $Idn & $Tran; diff --git a/packages/@ot-doc/document/src/lib2/maybe.ts b/packages/@ot-doc/document/src/lib2/maybe.ts new file mode 100644 index 0000000..2612eac --- /dev/null +++ b/packages/@ot-doc/document/src/lib2/maybe.ts @@ -0,0 +1,6 @@ +// $: Constructor type +// v: the value +export type Maybe = { $: "Nothing" } | { $: "Just", v: T }; + +export const nothing = (): Maybe => ({ $: "Nothing" }); +export const just = (v: T): Maybe => ({ $: "Just", v }); diff --git a/packages/@ot-doc/document/src/lib2/record.ts b/packages/@ot-doc/document/src/lib2/record.ts new file mode 100644 index 0000000..6bcf0c8 --- /dev/null +++ b/packages/@ot-doc/document/src/lib2/record.ts @@ -0,0 +1,87 @@ +import { $Eq } from "./algebra"; +import { $BaseDoc, $FullDoc, $Idn, $InvDoc } from "./document"; +import { just, nothing } from "./maybe"; + +export const $baseDocRecord = + ({ equals }: $Eq) => + ({ + initial, + compose, + }: $BaseDoc): $BaseDoc, Record> => { + const cpI = initial(); + + return { + initial: () => ({}), + compose: (op) => (cp) => + Object.keys(op).reduce((cpT, key) => { + if (cpT.$ === 'Nothing') { + return cpT; + } + const cpK = cpT.v[key] ?? cpI; + const opK = op[key]; + const mNewCpK = compose(opK)(cpK); + if (mNewCpK.$ === 'Nothing') { + return nothing(); + } + const { v: newCpK } = mNewCpK; + if (!equals(newCpK)(cpK)) { + if (cpT.v === cp) { + cpT.v = { ...cp }; + } + if (equals(newCpK)(cpI)) { + delete cpT.v[key]; + } else { + cpT.v[key] = newCpK; + } + } + return cpT; + }, just(cp)), + }; + }; + +export const $invDocRecord = + ({ equals }: $Eq) => + ({ initial, compose, invert }: $InvDoc): $InvDoc, Record> => { + return { + ...$baseDocRecord({ equals })({ initial, compose }), + invert: (op) => Object.keys(op).reduce((opT, key) => { + opT[key] = invert(op[key]); + return opT; + }, {} as Record), + }; + }; + +export const $fullDocRecord = + (clsOp: $Eq & $Idn) => + (clsCp: $Eq) => + ({ initial, compose, invert, transform }: $FullDoc): $FullDoc, Record> => { + const opI = clsOp.identity(); + + return { + ...$invDocRecord(clsCp)({ initial, compose, invert }), + identity: () => ({}), + transform: (opA) => (opB) => Object.keys(opA).reduce((mOpT, key) => { + if (mOpT.$ === 'Nothing' || !(key in opB)) { + return mOpT; + } + const opKA = mOpT.v[key]; + const opKB = opB[key]; + const mNewOpKA = transform(opKA)(opKB); + if (mNewOpKA.$ === 'Nothing') { + return nothing(); + } + const { v: newOpKA } = mNewOpKA; + if (!clsOp.equals(newOpKA)(opKA)) { + if (mOpT.v === opA) { + mOpT.v = { ...opA }; + } + if (clsOp.equals(newOpKA)(opI)) { + delete mOpT.v[key]; + } else { + mOpT.v[key] = newOpKA; + } + } + return mOpT; + }, just(opA)), + }; + }; diff --git a/packages/@ot-doc/document/src/lib2/singleton.ts b/packages/@ot-doc/document/src/lib2/singleton.ts new file mode 100644 index 0000000..555a728 --- /dev/null +++ b/packages/@ot-doc/document/src/lib2/singleton.ts @@ -0,0 +1,51 @@ +import { $Eq, $Ord } from "./algebra"; +import { $FullDoc, $idn, $Init, $InvDoc } from "./document"; +import { just, nothing } from "./maybe"; + +// f: update from value +// t: update to value +export type Update = { f: A, t: A } | null; + +// Eq a => Eq (Update a) +export const $eqUpdate = ({ equals }: $Eq): $Eq> => ({ + equals: (a) => (b) => { + if (a === b) { + return true; + } + if (!a || !b) { + return false; + } + return (equals(a.f)(b.f) && equals(a.t)(b.t)) + }, +}); + +// Eq a, Initial a => $InvDoc a (Update a) +export const $invDocUpdate = ({ + equals, + initial, +}: $Eq & $Init): $InvDoc> => ({ + initial, + compose: + (op) => op ? ((v) => equals(v)(op.f) ? just(op.t) : nothing()) : just, + invert: op => op ? { f: op.t, t: op.f } : null, +}); + +// Greater Write Win +// Eq a, Initial a, Ord a => $FullDoc a (Update a) +export const $fullDocGww = ({ + equals, + initial, + lessThan, +}: $Eq & $Init & $Ord): $FullDoc> => ({ + ...$invDocUpdate({ equals, initial }), + ...$idn(null), + transform: (opA) => (opB) => { + if (!opA) return just(null); + if (!opB) return just(opA); + const { f: fA, t: tA } = opA; + const { f: fB, t: tB } = opB; + if (!equals(fA)(fB)) return nothing(); + return just(lessThan(tA)(tB) ? { f: tA, t: tB } : null); + }, +}); + diff --git a/packages/@ot-doc/document/src/lib2/struct.ts b/packages/@ot-doc/document/src/lib2/struct.ts new file mode 100644 index 0000000..e74da44 --- /dev/null +++ b/packages/@ot-doc/document/src/lib2/struct.ts @@ -0,0 +1,37 @@ +import { $Eq } from "./algebra"; +import { $BaseDoc, $FullDoc, $InvDoc } from "./document"; + +export type Stt$Eq> = { + [K in keyof T]: $Eq; +}; + +export type Stt$BaseDoc, Op extends Record> = { + [K in keyof Cp]: $BaseDoc; +}; + +export type Stt$InvDoc, Op extends Record> = { + [K in keyof Cp]: $InvDoc; +}; + +export type Stt$FullDoc, Op extends Record> = { + [K in keyof Cp]: $FullDoc; +}; + +export const $baseDocStruct = + >(stt$eq: Stt$Eq) => + >(stt$baseDoc: Stt$BaseDoc): $BaseDoc => { + return {}; + }; + +export const $invDocStruct = + >(stt$eq: Stt$Eq) => + >(stt$invDoc: Stt$InvDoc): $InvDoc => { + return {}; + }; + +export const $fullDocStruct = + >(stt$eq: Stt$Eq) => + >(stt$fullDoc: Stt$FullDoc): $FullDoc => { + return {}; + }; + \ No newline at end of file diff --git a/packages/@ot-doc/document/src/lib2/timestamped.ts b/packages/@ot-doc/document/src/lib2/timestamped.ts new file mode 100644 index 0000000..cf0086d --- /dev/null +++ b/packages/@ot-doc/document/src/lib2/timestamped.ts @@ -0,0 +1,25 @@ +import { $Eq, $Ord } from "./algebra"; +import { $FullDoc, $Init } from "./document"; +import { $fullDocGww, Update } from "./singleton"; + +// t: timestamp +// v: value +export type Timestamped = { t: number, v: T }; + +export const $eqTimestamped = ({ equals }: $Eq): $Eq> => ({ + equals: (a) => (b) => a.t === b.t && equals(a.v)(b.v), +}); + +export const $ordTimestamped = ({ lessThan }: $Ord): $Ord> => ({ + lessThan: (a) => (b) => a.t < b.t || (a.t === b.t && lessThan(a.v)(b.v)), +}); + +export const $fullDocLww = ( + cls: $Eq & $Init & $Ord +): $FullDoc, Update>> => { + const v = cls.initial(); + const initial = () => ({ t: -Infinity, v }); + const { equals } = $eqTimestamped(cls); + const { lessThan } = $ordTimestamped(cls); + return $fullDocGww({ equals, lessThan, initial }); +};