// 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 import Debug from 'debug'; import {DeltaFilter} from "./delta"; import {CollapsedDelta, Lossless, LosslessViewMany, LosslessViewOne} from "./lossless"; import {DomainEntityID, PropertyID, PropertyTypes, Timestamp, ViewMany} from "./types"; const debug = Debug('lossy'); type TimestampedProperty = { value: PropertyTypes, timeUpdated: Timestamp }; export type LossyViewOne = { id: DomainEntityID; properties: { [key: PropertyID]: T }; }; export type LossyViewMany = ViewMany>; export type ResolvedViewOne = LossyViewOne; export type ResolvedViewMany = ViewMany; export type Resolver = (losslessView: LosslessViewMany) => T; // Extract a particular value from a delta's pointers export function valueFromCollapsedDelta( delta: CollapsedDelta, key: string ): 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; } } } } // 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; } function defaultResolver(losslessView: LosslessViewMany): ResolvedViewMany { const resolved: ResolvedViewMany = {}; // debug('default resolver, lossless view', JSON.stringify(losslessView)); for (const [id, ent] of Object.entries(losslessView)) { resolved[id] = {id, properties: {}}; for (const key of Object.keys(ent.properties)) { const {value} = lastValueFromLosslessViewOne(ent, key) || {}; // debug(`[ ${key} ] = ${value}`); resolved[id].properties[key] = value; } } return resolved; }; // 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. export class Lossy { lossless: Lossless; constructor(lossless: Lossless) { this.lossless = lossless; } // 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. // TODO: Cache things! resolve(fn?: Resolver | Resolver, entityIds?: DomainEntityID[], deltaFilter?: DeltaFilter): T { if (!fn) { fn = defaultResolver; } const losslessView = this.lossless.view(entityIds, deltaFilter); return fn(losslessView) as T; } } // 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