diff --git a/__tests__/delta.ts b/__tests__/delta.ts new file mode 100644 index 0000000..b0023e1 --- /dev/null +++ b/__tests__/delta.ts @@ -0,0 +1,53 @@ +import {DeltaV1, DeltaV2} from "../src/delta"; + +describe("Delta", () => { + it("can convert DeltaV1 to DeltaV2", () => { + const deltaV1 = new DeltaV1({ + creator: 'a', + host: 'h', + pointers: [{ + localContext: 'color', + target: 'red' + }, { + localContext: 'furniture', + target: 'chair-1', + targetContext: 'color' + }] + }); + + const deltaV2 = DeltaV2.fromV1(deltaV1); + + expect(deltaV2).toMatchObject({ + ...deltaV1, + pointers: { + color: 'red', + furniture: {'chair-1': 'color'} + } + }); + }); + + it("can convert DeltaV2 to DeltaV1", () => { + const deltaV2 = new DeltaV2({ + creator: 'a', + host: 'h', + pointers: { + color: 'red', + furniture: {'chair-1': 'color'} + } + }); + + const deltaV1 = deltaV2.toV1(); + + expect(deltaV1).toMatchObject({ + ...deltaV2, + pointers: [{ + localContext: 'color', + target: 'red' + }, { + localContext: 'furniture', + target: 'chair-1', + targetContext: 'color' + }] + }); + }); +}); diff --git a/__tests__/lossless.ts b/__tests__/lossless.ts index 3dd81d2..1784e0f 100644 --- a/__tests__/lossless.ts +++ b/__tests__/lossless.ts @@ -1,4 +1,4 @@ -import {Delta, DeltaFilter} from '../src/delta'; +import {Delta, DeltaFilter, DeltaV2} from '../src/delta'; import {Lossless} from '../src/lossless'; import {RhizomeNode} from '../src/node'; @@ -6,10 +6,19 @@ describe('Lossless', () => { const node = new RhizomeNode(); it('creates a lossless view of keanu as neo in the matrix', () => { - const delta = new Delta({ + const delta = new DeltaV2({ creator: 'a', host: 'h', - pointers: [{ + pointers: { + actor: {"keanu": "roles"}, + role: {"neo": "actor"}, + film: {"the_matrix": "cast"}, + base_salary: 1000000, + salary_currency: "usd" + } + }).toV1(); + + expect(delta.pointers).toMatchObject([{ localContext: "actor", target: "keanu", targetContext: "roles" @@ -27,8 +36,7 @@ describe('Lossless', () => { }, { localContext: "salary_currency", target: "usd" - }] - }); + }]); const lossless = new Lossless(node); diff --git a/src/delta-stream.ts b/src/delta-stream.ts index 28f5f44..99b53b3 100644 --- a/src/delta-stream.ts +++ b/src/delta-stream.ts @@ -1,7 +1,7 @@ import Debug from 'debug'; import EventEmitter from 'node:events'; import objectHash from 'object-hash'; -import {Delta, DeltaNetworkImage} from './delta'; +import {Delta} from './delta'; import {RhizomeNode} from './node'; const debug = Debug('rz:deltas'); @@ -91,12 +91,13 @@ export class DeltaStream { } serializeDelta(delta: Delta): string { - const deltaNetworkImage = new DeltaNetworkImage(delta); + const deltaNetworkImage = delta.toNetworkImage(); return JSON.stringify(deltaNetworkImage); } deserializeDelta(input: string): Delta { // TODO: Input validation - return JSON.parse(input); + const parsed = JSON.parse(input); + return Delta.fromNetworkImage(parsed); } } diff --git a/src/delta.ts b/src/delta.ts index 8dce43f..9b1683a 100644 --- a/src/delta.ts +++ b/src/delta.ts @@ -1,25 +1,37 @@ import {randomUUID} from "crypto"; +import Debug from 'debug'; import microtime from 'microtime'; -import {CreatorID, HostID, Timestamp, TransactionID} from "./types"; import {PeerAddress} from "./peers"; +import {CreatorID, DomainEntityID, HostID, PropertyID, Timestamp, TransactionID} from "./types"; +const debug = Debug('rz:delta'); export type DeltaID = string; -export type PointerTarget = string | number | undefined; +export type PointerTarget = string | number | null; -export type Pointer = { +type PointerV1 = { localContext: string; target: PointerTarget; targetContext?: string; }; -export class DeltaNetworkImage { +export type Scalar = string | number | null; +export type Reference = { + [key: PropertyID]: DomainEntityID +}; + +export type PointersV2 = { + [key: PropertyID]: Scalar | Reference +}; + +export class DeltaNetworkImageV1 { id: DeltaID; timeCreated: Timestamp; host: HostID; creator: CreatorID; - pointers: Pointer[]; - constructor({id, timeCreated, host, creator, pointers}: DeltaNetworkImage) { + pointers: PointerV1[]; + + constructor({id, timeCreated, host, creator, pointers}: DeltaNetworkImageV1) { this.id = id; this.host = host; this.creator = creator; @@ -28,26 +40,106 @@ export class DeltaNetworkImage { } }; -export class Delta extends DeltaNetworkImage { +export class DeltaNetworkImageV2 { + id: DeltaID; + timeCreated: Timestamp; + host: HostID; + creator: CreatorID; + pointers: PointersV2; + + constructor({id, timeCreated, host, creator, pointers}: DeltaNetworkImageV2) { + this.id = id; + this.host = host; + this.creator = creator; + this.timeCreated = timeCreated; + this.pointers = pointers; + } +}; + +export class DeltaV1 extends DeltaNetworkImageV1 { receivedFrom?: PeerAddress; timeReceived: Timestamp; transactionId?: TransactionID; - // TODO: Verify the following assumption: - // We're assuming that you only call this constructor when - // actually creating a new delta. - // When receiving one from the network, you can - constructor({host, creator, pointers}: Partial) { - // TODO: Verify that when receiving a delta from the network we can - // retain the delta's id. - const id = randomUUID(); - const timeCreated = microtime.now(); + constructor({id, timeCreated, host, creator, pointers}: Partial) { + id = id ?? randomUUID(); + timeCreated = timeCreated ?? microtime.now(); if (!host || !creator || !pointers) throw new Error('uninitializied values'); super({id, timeCreated, host, creator, pointers}); this.timeCreated = timeCreated; this.timeReceived = this.timeCreated; } + + toNetworkImage() { + return new DeltaNetworkImageV1(this); + } + + static fromNetworkImage(delta: DeltaNetworkImageV1) { + return new DeltaV1(delta); + } } +export class DeltaV2 extends DeltaNetworkImageV2 { + receivedFrom?: PeerAddress; + timeReceived: Timestamp; + transactionId?: TransactionID; + + constructor({id, timeCreated, host, creator, pointers}: Partial) { + id = id ?? randomUUID(); + timeCreated = timeCreated ?? microtime.now(); + if (!host || !creator || !pointers) throw new Error('uninitializied values'); + super({id, timeCreated, host, creator, pointers}); + this.timeCreated = timeCreated; + this.timeReceived = this.timeCreated; + } + + toNetworkImage() { + return new DeltaNetworkImageV2(this); + } + + static fromNetworkImage(delta: DeltaNetworkImageV2) { + return new DeltaV2(delta); + } + + static fromV1(delta: DeltaV1) { + const pointersV2: PointersV2 = {}; + for (const {localContext, target, targetContext} of delta.pointers) { + if (targetContext && typeof target === "string") { + pointersV2[localContext] = {[target]: targetContext}; + } else { + pointersV2[localContext] = target; + } + } + + debug(`fromV1, pointers in: ${JSON.stringify(delta.pointers)}`); + debug(`fromV1, pointers out: ${JSON.stringify(pointersV2)}`); + return DeltaV2.fromNetworkImage({ + ...delta, + pointers: pointersV2 + }); + } + + toV1() { + const pointersV1: PointerV1[] = []; + for (const [localContext, pointerTarget] of Object.entries(this.pointers)) { + if (pointerTarget && typeof pointerTarget === "object") { + const [obj] = Object.entries(pointerTarget) + if (!obj) throw new Error("invalid pointer target"); + const [target, targetContext] = Object.entries(pointerTarget)[0]; + pointersV1.push({localContext, target, targetContext}); + } else { + pointersV1.push({localContext, target: pointerTarget}); + } + } + return new DeltaV1({ + ...this, + pointers: pointersV1 + }); + } +} + +// Alias +export class Delta extends DeltaV1 {} + export type DeltaFilter = (delta: Delta) => boolean; diff --git a/src/filter/index.ts b/src/filter/index.ts deleted file mode 100644 index 38ad0d5..0000000 --- a/src/filter/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { add_operation, apply } from 'json-logic-js'; -import { Delta } from '../delta'; - -type DeltaContext = Delta & { - creatorAddress: string; -}; - -add_operation('in', (needle, haystack) => { - return [...haystack].includes(needle); -}); - -export function applyFilter(deltas: Delta[], filterExpr: JSON): Delta[] { - return deltas.filter(delta => { - const context: DeltaContext = { - ...delta, - creatorAddress: [delta.creator, delta.host].join('@'), - }; - return apply(filterExpr, context); - }); -} diff --git a/src/filter/known.ts b/src/filter/known.ts deleted file mode 100644 index 13b5bd9..0000000 --- a/src/filter/known.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { FilterExpr } from "../types"; -// import { map } from 'radash'; - -// A creator as seen by a host -type OriginPoint = { - creator: string; - host: string; -}; - -class Party { - originPoints: OriginPoint[]; - constructor(og: OriginPoint) { - this.originPoints = [og]; - } - getAddress() { - const { creator, host } = this.originPoints[0]; - return `${creator}@${host}`; - } -} - -const knownParties = new Set(); -export const countKnownParties = () => knownParties.size; - -export function generateFilter(): FilterExpr { -// map(knownParties, (p: Party) => p.address] - // - - const addresses = [...knownParties.values()].map(p => p.getAddress()); - - return { - 'in': ['$creatorAddress', addresses] - }; -}; diff --git a/src/lossless.ts b/src/lossless.ts index 492aac8..ebff7ea 100644 --- a/src/lossless.ts +++ b/src/lossless.ts @@ -3,7 +3,7 @@ import Debug from 'debug'; import EventEmitter from 'events'; -import {Delta, DeltaFilter, DeltaID, DeltaNetworkImage} from './delta'; +import {Delta, DeltaFilter, DeltaID, DeltaNetworkImageV1} from './delta'; import {RhizomeNode} from './node'; import {Transactions} from './transactions'; import {DomainEntityID, PropertyID, PropertyTypes, TransactionID, ViewMany} from "./types"; @@ -11,7 +11,7 @@ const debug = Debug('rz:lossless'); export type CollapsedPointer = {[key: PropertyID]: PropertyTypes}; -export type CollapsedDelta = Omit & { +export type CollapsedDelta = Omit & { pointers: CollapsedPointer[]; }; diff --git a/src/types.ts b/src/types.ts index 3f1faca..9bfe797 100644 --- a/src/types.ts +++ b/src/types.ts @@ -4,7 +4,7 @@ export type FilterExpr = JSONLogic; export type FilterGenerator = () => FilterExpr; -export type PropertyTypes = string | number | undefined; +export type PropertyTypes = string | number | null; export type DomainEntityID = string; export type PropertyID = string;