added more concise syntax for deltas

This commit is contained in:
Ladd Hoffman 2025-01-02 16:58:51 -06:00
parent b147378bf8
commit 39d70b4680
8 changed files with 181 additions and 80 deletions

53
__tests__/delta.ts Normal file
View File

@ -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'
}]
});
});
});

View File

@ -1,4 +1,4 @@
import {Delta, DeltaFilter} from '../src/delta'; import {Delta, DeltaFilter, DeltaV2} from '../src/delta';
import {Lossless} from '../src/lossless'; import {Lossless} from '../src/lossless';
import {RhizomeNode} from '../src/node'; import {RhizomeNode} from '../src/node';
@ -6,10 +6,19 @@ describe('Lossless', () => {
const node = new RhizomeNode(); const node = new RhizomeNode();
it('creates a lossless view of keanu as neo in the matrix', () => { it('creates a lossless view of keanu as neo in the matrix', () => {
const delta = new Delta({ const delta = new DeltaV2({
creator: 'a', creator: 'a',
host: 'h', 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", localContext: "actor",
target: "keanu", target: "keanu",
targetContext: "roles" targetContext: "roles"
@ -27,8 +36,7 @@ describe('Lossless', () => {
}, { }, {
localContext: "salary_currency", localContext: "salary_currency",
target: "usd" target: "usd"
}] }]);
});
const lossless = new Lossless(node); const lossless = new Lossless(node);

View File

@ -1,7 +1,7 @@
import Debug from 'debug'; import Debug from 'debug';
import EventEmitter from 'node:events'; import EventEmitter from 'node:events';
import objectHash from 'object-hash'; import objectHash from 'object-hash';
import {Delta, DeltaNetworkImage} from './delta'; import {Delta} from './delta';
import {RhizomeNode} from './node'; import {RhizomeNode} from './node';
const debug = Debug('rz:deltas'); const debug = Debug('rz:deltas');
@ -91,12 +91,13 @@ export class DeltaStream {
} }
serializeDelta(delta: Delta): string { serializeDelta(delta: Delta): string {
const deltaNetworkImage = new DeltaNetworkImage(delta); const deltaNetworkImage = delta.toNetworkImage();
return JSON.stringify(deltaNetworkImage); return JSON.stringify(deltaNetworkImage);
} }
deserializeDelta(input: string): Delta { deserializeDelta(input: string): Delta {
// TODO: Input validation // TODO: Input validation
return JSON.parse(input); const parsed = JSON.parse(input);
return Delta.fromNetworkImage(parsed);
} }
} }

View File

@ -1,25 +1,37 @@
import {randomUUID} from "crypto"; import {randomUUID} from "crypto";
import Debug from 'debug';
import microtime from 'microtime'; import microtime from 'microtime';
import {CreatorID, HostID, Timestamp, TransactionID} from "./types";
import {PeerAddress} from "./peers"; import {PeerAddress} from "./peers";
import {CreatorID, DomainEntityID, HostID, PropertyID, Timestamp, TransactionID} from "./types";
const debug = Debug('rz:delta');
export type DeltaID = string; export type DeltaID = string;
export type PointerTarget = string | number | undefined; export type PointerTarget = string | number | null;
export type Pointer = { type PointerV1 = {
localContext: string; localContext: string;
target: PointerTarget; target: PointerTarget;
targetContext?: string; 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; id: DeltaID;
timeCreated: Timestamp; timeCreated: Timestamp;
host: HostID; host: HostID;
creator: CreatorID; creator: CreatorID;
pointers: Pointer[]; pointers: PointerV1[];
constructor({id, timeCreated, host, creator, pointers}: DeltaNetworkImage) {
constructor({id, timeCreated, host, creator, pointers}: DeltaNetworkImageV1) {
this.id = id; this.id = id;
this.host = host; this.host = host;
this.creator = creator; 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; receivedFrom?: PeerAddress;
timeReceived: Timestamp; timeReceived: Timestamp;
transactionId?: TransactionID; transactionId?: TransactionID;
// TODO: Verify the following assumption: constructor({id, timeCreated, host, creator, pointers}: Partial<DeltaNetworkImageV1>) {
// We're assuming that you only call this constructor when id = id ?? randomUUID();
// actually creating a new delta. timeCreated = timeCreated ?? microtime.now();
// 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();
if (!host || !creator || !pointers) throw new Error('uninitializied values'); if (!host || !creator || !pointers) throw new Error('uninitializied values');
super({id, timeCreated, host, creator, pointers}); super({id, timeCreated, host, creator, pointers});
this.timeCreated = timeCreated; this.timeCreated = timeCreated;
this.timeReceived = this.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; export type DeltaFilter = (delta: Delta) => boolean;

View File

@ -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);
});
}

View File

@ -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]
};
};

View File

@ -3,7 +3,7 @@
import Debug from 'debug'; import Debug from 'debug';
import EventEmitter from 'events'; import EventEmitter from 'events';
import {Delta, DeltaFilter, DeltaID, DeltaNetworkImage} from './delta'; import {Delta, DeltaFilter, DeltaID, DeltaNetworkImageV1} from './delta';
import {RhizomeNode} from './node'; import {RhizomeNode} from './node';
import {Transactions} from './transactions'; import {Transactions} from './transactions';
import {DomainEntityID, PropertyID, PropertyTypes, TransactionID, ViewMany} from "./types"; 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 CollapsedPointer = {[key: PropertyID]: PropertyTypes};
export type CollapsedDelta = Omit<DeltaNetworkImage, 'pointers'> & { export type CollapsedDelta = Omit<DeltaNetworkImageV1, 'pointers'> & {
pointers: CollapsedPointer[]; pointers: CollapsedPointer[];
}; };

View File

@ -4,7 +4,7 @@ export type FilterExpr = JSONLogic;
export type FilterGenerator = () => FilterExpr; export type FilterGenerator = () => FilterExpr;
export type PropertyTypes = string | number | undefined; export type PropertyTypes = string | number | null;
export type DomainEntityID = string; export type DomainEntityID = string;
export type PropertyID = string; export type PropertyID = string;