From 39d70b4680c47546d8c3700b9bf9186afdefc879 Mon Sep 17 00:00:00 2001
From: Ladd <ladd@dgov.io>
Date: Thu, 2 Jan 2025 16:58:51 -0600
Subject: [PATCH] added more concise syntax for deltas

---
 __tests__/delta.ts    |  53 ++++++++++++++++++
 __tests__/lossless.ts |  18 ++++--
 src/delta-stream.ts   |   7 ++-
 src/delta.ts          | 124 ++++++++++++++++++++++++++++++++++++------
 src/filter/index.ts   |  20 -------
 src/filter/known.ts   |  33 -----------
 src/lossless.ts       |   4 +-
 src/types.ts          |   2 +-
 8 files changed, 181 insertions(+), 80 deletions(-)
 create mode 100644 __tests__/delta.ts
 delete mode 100644 src/filter/index.ts
 delete mode 100644 src/filter/known.ts

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<DeltaNetworkImage>) {
-    // 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<DeltaNetworkImageV1>) {
+    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<DeltaNetworkImageV2>) {
+    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<Party>();
-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<DeltaNetworkImage, 'pointers'> & {
+export type CollapsedDelta = Omit<DeltaNetworkImageV1, 'pointers'> & {
   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;