2024-12-23 23:30:54 -06:00
|
|
|
// We have the lossless transformation of the delta stream.
|
|
|
|
// We want to enable transformations from the lossless view,
|
|
|
|
// into various possible "lossy" views that combine or exclude some information.
|
|
|
|
//
|
|
|
|
// We can achieve this via functional expression, encoded as JSON-Logic.
|
|
|
|
// Fields in the output can be described as transformations
|
|
|
|
|
2024-12-26 15:59:03 -06:00
|
|
|
import Debug from 'debug';
|
2024-12-27 13:43:43 -06:00
|
|
|
import {DeltaFilter} from "./delta";
|
2024-12-29 14:35:30 -06:00
|
|
|
import {CollapsedDelta, Lossless, LosslessViewMany, LosslessViewOne} from "./lossless";
|
|
|
|
import {DomainEntityID, PropertyID, PropertyTypes, Timestamp, ViewMany} from "./types";
|
2024-12-26 15:59:03 -06:00
|
|
|
const debug = Debug('lossy');
|
2024-12-23 23:30:54 -06:00
|
|
|
|
2024-12-29 14:35:30 -06:00
|
|
|
type TimestampedProperty = {
|
|
|
|
value: PropertyTypes,
|
|
|
|
timeUpdated: Timestamp
|
2024-12-26 15:59:03 -06:00
|
|
|
};
|
|
|
|
|
2024-12-29 14:35:30 -06:00
|
|
|
export type LossyViewOne<T = TimestampedProperty> = {
|
|
|
|
id: DomainEntityID;
|
|
|
|
properties: {
|
|
|
|
[key: PropertyID]: T
|
|
|
|
};
|
2024-12-26 15:59:03 -06:00
|
|
|
};
|
|
|
|
|
2024-12-29 14:35:30 -06:00
|
|
|
export type LossyViewMany<T> = ViewMany<LossyViewOne<T>>;
|
|
|
|
|
|
|
|
export type ResolvedViewOne = LossyViewOne<PropertyTypes>;
|
|
|
|
export type ResolvedViewMany = ViewMany<ResolvedViewOne>;
|
|
|
|
|
|
|
|
export type Resolver<T = ResolvedViewMany> =
|
|
|
|
(losslessView: LosslessViewMany) => T;
|
2024-12-23 23:30:54 -06:00
|
|
|
|
2024-12-23 23:34:28 -06:00
|
|
|
// Extract a particular value from a delta's pointers
|
2024-12-27 13:43:43 -06:00
|
|
|
export function valueFromCollapsedDelta(
|
|
|
|
delta: CollapsedDelta,
|
|
|
|
key: string
|
|
|
|
): string | number | undefined {
|
2024-12-26 15:59:03 -06:00
|
|
|
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;
|
|
|
|
}
|
2024-12-23 23:30:54 -06:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2024-12-29 14:35:30 -06:00
|
|
|
// Function for resolving a value for an entity by last write wins
|
|
|
|
export function lastValueFromLosslessViewOne(
|
|
|
|
ent: LosslessViewOne,
|
|
|
|
key: string
|
|
|
|
): {
|
|
|
|
delta?: CollapsedDelta,
|
|
|
|
value?: string | number,
|
|
|
|
timeUpdated?: number
|
|
|
|
} | undefined {
|
|
|
|
const res: {
|
|
|
|
delta?: CollapsedDelta,
|
|
|
|
value?: string | number,
|
|
|
|
timeUpdated?: number
|
|
|
|
} = {};
|
|
|
|
res.timeUpdated = 0;
|
|
|
|
|
|
|
|
for (const delta of ent.properties[key] || []) {
|
|
|
|
const value = valueFromCollapsedDelta(delta, key);
|
|
|
|
if (value === undefined) continue;
|
|
|
|
if (delta.timeCreated < res.timeUpdated) continue;
|
|
|
|
res.delta = delta;
|
|
|
|
res.value = value;
|
|
|
|
res.timeUpdated = delta.timeCreated;
|
|
|
|
}
|
|
|
|
|
|
|
|
return res;
|
|
|
|
}
|
|
|
|
|
2024-12-29 17:50:20 -06:00
|
|
|
function defaultResolver(losslessView: LosslessViewMany): ResolvedViewMany {
|
2024-12-30 01:23:11 -06:00
|
|
|
const resolved: ResolvedViewMany = {};
|
2024-12-29 17:50:20 -06:00
|
|
|
|
2024-12-30 01:23:11 -06:00
|
|
|
// debug('default resolver, lossless view', JSON.stringify(losslessView));
|
|
|
|
for (const [id, ent] of Object.entries(losslessView)) {
|
|
|
|
resolved[id] = {id, properties: {}};
|
2024-12-29 17:50:20 -06:00
|
|
|
|
2024-12-30 01:23:11 -06:00
|
|
|
for (const key of Object.keys(ent.properties)) {
|
|
|
|
const {value} = lastValueFromLosslessViewOne(ent, key) || {};
|
2024-12-29 17:50:20 -06:00
|
|
|
|
2024-12-30 01:23:11 -06:00
|
|
|
// debug(`[ ${key} ] = ${value}`);
|
|
|
|
resolved[id].properties[key] = value;
|
2024-12-29 17:50:20 -06:00
|
|
|
}
|
2024-12-30 01:23:11 -06:00
|
|
|
}
|
|
|
|
return resolved;
|
|
|
|
};
|
2024-12-29 17:50:20 -06:00
|
|
|
|
2024-12-30 01:23:11 -06:00
|
|
|
// TODO: Incremental updates of lossy models. For example, with last-write-wins,
|
|
|
|
// we keep the timeUpdated for each field. A second stage resolver can rearrange
|
|
|
|
// the data structure to a preferred shape and may discard the timeUpdated info.
|
2024-12-23 23:30:54 -06:00
|
|
|
export class Lossy {
|
|
|
|
lossless: Lossless;
|
|
|
|
|
|
|
|
constructor(lossless: Lossless) {
|
|
|
|
this.lossless = lossless;
|
|
|
|
}
|
|
|
|
|
2024-12-27 13:43:43 -06:00
|
|
|
// Using the lossless view of some given domain entities,
|
|
|
|
// apply a filter to the deltas composing that lossless view,
|
|
|
|
// and then apply a supplied resolver function which receives
|
|
|
|
// the filtered lossless view as input.
|
2024-12-30 01:23:11 -06:00
|
|
|
// TODO: Cache things!
|
2024-12-29 17:50:20 -06:00
|
|
|
resolve<T = ResolvedViewOne>(fn?: Resolver<T> | Resolver, entityIds?: DomainEntityID[], deltaFilter?: DeltaFilter): T {
|
|
|
|
if (!fn) {
|
|
|
|
fn = defaultResolver;
|
|
|
|
}
|
2024-12-27 13:43:43 -06:00
|
|
|
const losslessView = this.lossless.view(entityIds, deltaFilter);
|
2024-12-29 17:50:20 -06:00
|
|
|
return fn(losslessView) as T;
|
2024-12-23 23:30:54 -06:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-12-25 00:42:16 -06:00
|
|
|
// Generate a rule
|
|
|
|
// Apply the rule -- When?
|
|
|
|
// - Maybe we shard a set of deltas and map/reduce the results --
|
|
|
|
// We are trying to implement CRDT, so the results
|
|
|
|
// must be composable to preserve that feature.
|
|
|
|
// That also seems to imply we want to stick with
|
|
|
|
// the lossless view until the delta set is chosen
|
|
|
|
// - So, in general on a set of deltas
|
|
|
|
// at times which seem opportune
|
|
|
|
// the results of which can be recorded
|
|
|
|
// and indexed such that the results can be reused
|
|
|
|
// i.e. you want to compute the result of a set which
|
|
|
|
// contains a prior one
|
|
|
|
|