refactoring in preparation for adding more resolvers

This commit is contained in:
Ladd Hoffman 2025-01-02 20:29:26 -06:00
parent e684eac932
commit c6f6ece504
5 changed files with 111 additions and 97 deletions

View File

@ -1,8 +1,8 @@
import Debug from 'debug'; import Debug from 'debug';
import {Delta, PointerTarget} from "../src/delta"; import {Delta, PointerTarget} from "../src/delta";
import {lastValueFromDeltas} from "../src/last-write-wins"; import {lastValueFromDeltas, valueFromCollapsedDelta} from "../src/last-write-wins";
import {Lossless, LosslessViewOne} from "../src/lossless"; import {Lossless, LosslessViewOne} from "../src/lossless";
import {Lossy, valueFromCollapsedDelta} from "../src/lossy"; import {Lossy} from "../src/lossy";
import {RhizomeNode} from "../src/node"; import {RhizomeNode} from "../src/node";
const debug = Debug('test:lossy'); const debug = Debug('test:lossy');
@ -16,18 +16,18 @@ type Summary = {
roles: Role[]; roles: Role[];
}; };
class Summarizer extends Lossy<Summary, Summary> {
function initializer(): Summary { initializer(): Summary {
return { return {
roles: [] roles: []
}; };
} }
// TODO: Add more rigor to this example approach to generating a summary. // TODO: Add more rigor to this example approach to generating a summary.
// it's really not CRDT, it likely depends on the order of the pointers. // it's really not CRDT, it likely depends on the order of the pointers.
// TODO: Prove with failing test // TODO: Prove with failing test
const reducer = (acc: Summary, cur: LosslessViewOne): Summary => { reducer(acc: Summary, cur: LosslessViewOne): Summary {
if (cur.referencedAs.includes("role")) { if (cur.referencedAs.includes("role")) {
const {delta, value: actor} = lastValueFromDeltas("actor", cur.propertyDeltas["actor"]) ?? {}; const {delta, value: actor} = lastValueFromDeltas("actor", cur.propertyDeltas["actor"]) ?? {};
if (!delta) throw new Error('expected to find delta'); if (!delta) throw new Error('expected to find delta');
@ -42,19 +42,19 @@ const reducer = (acc: Summary, cur: LosslessViewOne): Summary => {
} }
return acc; return acc;
} }
const resolver = (acc: Summary): Summary => { resolver(acc: Summary): Summary {
return acc; return acc;
}
} }
describe('Lossy', () => { describe('Lossy', () => {
describe('use a provided initializer, reducer, and resolver to resolve entity views', () => { describe('use a provided initializer, reducer, and resolver to resolve entity views', () => {
const node = new RhizomeNode(); const node = new RhizomeNode();
const lossless = new Lossless(node); const lossless = new Lossless(node);
const lossy = new Lossy(lossless, initializer, reducer, resolver); const lossy = new Summarizer(lossless);
beforeAll(() => { beforeAll(() => {
lossless.ingestDelta(new Delta({ lossless.ingestDelta(new Delta({

13
__tests__/relational.ts Normal file
View File

@ -0,0 +1,13 @@
describe('Relational', () => {
it.skip('Allows expressing a domain ontology as a relational schema', async () => {});
// Deltas can be filtered at time of view resolution, and
// excluded if they violate schema constraints;
// Ideally the sender minimizes this by locally validating against the constraints.
// For cases where deltas conflict, there can be a resolution process,
// with configurable parameters such as duration, quorum, and so on;
// or a deterministic algorithm can be applied.
it.skip('Can validate a delta against a relational constraint', async () => {});
it.skip('Can validate a delta against a set of relational constraints', async () => {});
});

View File

@ -1,7 +1,7 @@
// import Debug from 'debug'; // import Debug from 'debug';
import {EntityProperties} from "./entity"; import {EntityProperties} from "./entity";
import {CollapsedDelta, Lossless, LosslessViewOne} from "./lossless"; import {CollapsedDelta, LosslessViewOne} from "./lossless";
import {Lossy, valueFromCollapsedDelta} from './lossy'; import {Lossy} from './lossy';
import {DomainEntityID, PropertyID, PropertyTypes, Timestamp, ViewMany} from "./types"; import {DomainEntityID, PropertyID, PropertyTypes, Timestamp, ViewMany} from "./types";
// const debug = Debug('rz:lossy:last-write-wins'); // const debug = Debug('rz:lossy:last-write-wins');
@ -27,7 +27,21 @@ export type ResolvedViewMany = ViewMany<ResolvedViewOne>;
type Accumulator = LossyViewMany<TimestampedProperties>; type Accumulator = LossyViewMany<TimestampedProperties>;
type Result = LossyViewMany<EntityProperties>; type Result = LossyViewMany<EntityProperties>;
// Function for resolving a value for an entity by last write wins // Extract a particular value from a delta's pointers
export function valueFromCollapsedDelta(
key: string,
delta: CollapsedDelta
): string | number | undefined {
for (const pointer of delta.pointers) {
for (const [k, value] of Object.entries(pointer)) {
if (k === key && (typeof value === "string" || typeof value === "number")) {
return value;
}
}
}
}
// Resolve a value for an entity by last write wins
export function lastValueFromDeltas( export function lastValueFromDeltas(
key: string, key: string,
deltas?: CollapsedDelta[] deltas?: CollapsedDelta[]
@ -55,11 +69,12 @@ export function lastValueFromDeltas(
return res; return res;
} }
function initializer(): Accumulator { export class LastWriteWins extends Lossy<Accumulator, Result> {
initializer(): Accumulator {
return {}; return {};
}; }
function reducer(acc: Accumulator, cur: LosslessViewOne): Accumulator { reducer(acc: Accumulator, cur: LosslessViewOne): Accumulator {
if (!acc[cur.id]) { if (!acc[cur.id]) {
acc[cur.id] = {id: cur.id, properties: {}}; acc[cur.id] = {id: cur.id, properties: {}};
} }
@ -76,9 +91,9 @@ function reducer(acc: Accumulator, cur: LosslessViewOne): Accumulator {
} }
} }
return acc; return acc;
}; };
function resolver(cur: Accumulator): Result { resolver(cur: Accumulator): Result {
const res: Result = {}; const res: Result = {};
for (const [id, ent] of Object.entries(cur)) { for (const [id, ent] of Object.entries(cur)) {
@ -89,12 +104,6 @@ function resolver(cur: Accumulator): Result {
} }
return res; return res;
}; };
export class LastWriteWins extends Lossy<Accumulator, Result> {
constructor(
readonly lossless: Lossless,
) {
super(lossless, initializer, reducer, resolver);
}
} }

View File

@ -4,38 +4,21 @@
import Debug from 'debug'; import Debug from 'debug';
import {DeltaFilter, DeltaID} from "./delta"; import {DeltaFilter, DeltaID} from "./delta";
import {CollapsedDelta, Lossless, LosslessViewOne} from "./lossless"; import {Lossless, LosslessViewOne} from "./lossless";
import {DomainEntityID} from "./types"; import {DomainEntityID} from "./types";
const debug = Debug('rz:lossy'); const debug = Debug('rz:lossy');
export type Initializer<Accumulator> = (v: LosslessViewOne) => Accumulator;
export type Reducer<Accumulator> = (acc: Accumulator, cur: LosslessViewOne) => Accumulator;
export type Resolver<Accumulator, Result> = (cur: Accumulator) => Result;
// Extract a particular value from a delta's pointers
export function valueFromCollapsedDelta(
key: string,
delta: CollapsedDelta
): string | number | undefined {
for (const pointer of delta.pointers) {
for (const [k, value] of Object.entries(pointer)) {
if (k === key && (typeof value === "string" || typeof value === "number")) {
return value;
}
}
}
}
// We support incremental updates of lossy models. // We support incremental updates of lossy models.
export class Lossy<Accumulator, Result> { export abstract class Lossy<Accumulator, Result> {
deltaFilter?: DeltaFilter; deltaFilter?: DeltaFilter;
accumulator?: Accumulator; accumulator?: Accumulator;
abstract initializer(v: LosslessViewOne): Accumulator;
abstract reducer(acc: Accumulator, cur: LosslessViewOne): Accumulator;
abstract resolver(cur: Accumulator): Result;
constructor( constructor(
readonly lossless: Lossless, readonly lossless: Lossless,
readonly initializer: Initializer<Accumulator>,
readonly reducer: Reducer<Accumulator>,
readonly resolver: Resolver<Accumulator, Result>,
) { ) {
this.lossless.eventStream.on("updated", (id, deltaIds) => { this.lossless.eventStream.on("updated", (id, deltaIds) => {
debug(`[${this.lossless.rhizomeNode.config.peerId}] entity ${id} updated, deltaIds:`, debug(`[${this.lossless.rhizomeNode.config.peerId}] entity ${id} updated, deltaIds:`,

9
src/relational.ts Normal file
View File

@ -0,0 +1,9 @@
import {Collection} from "./collection";
export class RelationalCollection extends Collection {
// lossy?:
}