rhizome/src/lossless.ts

228 lines
5.9 KiB
TypeScript
Raw Normal View History

2024-12-23 17:29:38 -06:00
// Deltas target entities.
// We can maintain a record of all the targeted entities, and the deltas that targeted them
import Debug from 'debug';
2024-12-29 17:50:20 -06:00
import {Delta, DeltaFilter, DeltaID} from './delta';
import {DomainEntityID, PropertyID, PropertyTypes, TransactionID, ViewMany} from "./types";
const debug = Debug('lossless');
2024-12-23 17:29:38 -06:00
2024-12-29 17:50:20 -06:00
export type CollapsedPointer = {[key: PropertyID]: PropertyTypes};
2024-12-23 17:29:38 -06:00
export type CollapsedDelta = Omit<Delta, 'pointers'> & {
pointers: CollapsedPointer[];
};
2024-12-23 23:30:21 -06:00
export type LosslessViewOne = {
referencedAs: string[];
properties: {
[key: PropertyID]: CollapsedDelta[]
}
};
2024-12-29 14:35:30 -06:00
export type LosslessViewMany = ViewMany<LosslessViewOne>;
2024-12-23 17:29:38 -06:00
class DomainEntityMap extends Map<DomainEntityID, DomainEntity> {};
class DomainEntity {
id: DomainEntityID;
properties = new Map<PropertyID, Set<Delta>>();
2024-12-23 17:29:38 -06:00
constructor(id: DomainEntityID) {
this.id = id;
}
addDelta(delta: Delta) {
const targetContexts = delta.pointers
.filter(({target}) => target === this.id)
.map(({targetContext}) => targetContext)
.filter((targetContext) => typeof targetContext === 'string');
2024-12-23 17:29:38 -06:00
for (const targetContext of targetContexts) {
let propertyDeltas = this.properties.get(targetContext);
if (!propertyDeltas) {
propertyDeltas = new Set<Delta>();
this.properties.set(targetContext, propertyDeltas);
2024-12-23 17:29:38 -06:00
}
debug(`adding delta for entity ${this.id}`);
propertyDeltas.add(delta);
}
}
toJSON() {
const properties: {[key: PropertyID]: number} = {};
for (const [key, deltas] of this.properties.entries()) {
properties[key] = deltas.size;
2024-12-23 17:29:38 -06:00
}
return {
id: this.id,
properties
};
2024-12-23 17:29:38 -06:00
}
}
2024-12-29 17:50:20 -06:00
class Transaction {
size?: number;
receivedDeltaIds = new Set<DeltaID>();
}
class Transactions {
transactions = new Map<TransactionID, Transaction>();
getOrInit(id: TransactionID): Transaction {
let t = this.transactions.get(id);
if (!t) {
t = new Transaction();
this.transactions.set(id, t);
}
return t;
}
receivedDelta(id: TransactionID, deltaId: DeltaID) {
const t = this.getOrInit(id);
t.receivedDeltaIds.add(deltaId);
}
isComplete(id: TransactionID) {
const t = this.getOrInit(id);
return t.size !== undefined && t.receivedDeltaIds.size === t.size;
}
setSize(id: TransactionID, size: number) {
const t = this.getOrInit(id);
t.size = size;
}
get ids() {
return Array.from(this.transactions.keys());
}
}
2024-12-23 17:29:38 -06:00
export class Lossless {
domainEntities = new DomainEntityMap();
2024-12-29 17:50:20 -06:00
transactions = new Transactions();
referencedAs = new Map<string, Set<DomainEntityID>>();
// referencingAs = new Map<string, Set<DomainEntityID>>();
2024-12-23 17:29:38 -06:00
2024-12-29 14:35:30 -06:00
ingestDelta(delta: Delta) {
2024-12-23 17:29:38 -06:00
const targets = delta.pointers
.filter(({targetContext}) => !!targetContext)
.map(({target}) => target)
.filter((target) => typeof target === 'string')
2024-12-23 17:29:38 -06:00
for (const target of targets) {
let ent = this.domainEntities.get(target);
2024-12-23 17:29:38 -06:00
if (!ent) {
ent = new DomainEntity(target);
this.domainEntities.set(target, ent);
}
2024-12-23 17:29:38 -06:00
ent.addDelta(delta);
2024-12-29 17:50:20 -06:00
}
2024-12-29 17:50:20 -06:00
for (const {target, localContext} of delta.pointers) {
if (typeof target === "string" && this.domainEntities.has(target)) {
if (this.domainEntities.has(target)) {
let referencedAs = this.referencedAs.get(localContext);
if (!referencedAs) {
referencedAs = new Set<string>();
this.referencedAs.set(localContext, referencedAs);
}
referencedAs.add(target);
}
}
}
const {target: transactionId} = delta.pointers.find(({
localContext,
target,
targetContext
}) =>
localContext === "_transaction" &&
typeof target === "string" &&
targetContext === "deltas"
) || {};
if (transactionId) {
// This delta is part of a transaction
this.transactions.receivedDelta(transactionId as string, delta.id);
} else {
const {target: transactionId} = delta.pointers.find(({
localContext,
target,
targetContext
}) =>
localContext === "_transaction" &&
typeof target === "string" &&
targetContext === "size"
) || {};
if (transactionId) {
// This delta describes a transaction
const {target: size} = delta.pointers.find(({
localContext,
target
}) =>
localContext === "size" &&
typeof target === "number"
) || {};
this.transactions.setSize(transactionId as string, size as number);
}
2024-12-23 17:29:38 -06:00
}
}
view(entityIds?: DomainEntityID[], deltaFilter?: DeltaFilter): LosslessViewMany {
2024-12-23 23:30:21 -06:00
const view: LosslessViewMany = {};
entityIds = entityIds ?? Array.from(this.domainEntities.keys());
for (const id of entityIds) {
const ent = this.domainEntities.get(id);
if (!ent) continue;
debug(`domain entity ${id}`, JSON.stringify(ent));
2024-12-23 23:30:21 -06:00
const referencedAs = new Set<string>();
const properties: {
[key: PropertyID]: CollapsedDelta[]
} = {};
for (const [key, deltas] of ent.properties.entries()) {
properties[key] = properties[key] || [];
for (const delta of deltas) {
if (deltaFilter) {
const include = deltaFilter(delta);
if (!include) continue;
}
2024-12-23 23:30:21 -06:00
const pointers: CollapsedPointer[] = [];
2024-12-23 23:30:21 -06:00
for (const {localContext, target} of delta.pointers) {
pointers.push({[localContext]: target});
if (target === ent.id) {
referencedAs.add(localContext);
}
}
2024-12-23 17:29:38 -06:00
const collapsedDelta: CollapsedDelta = {
...delta,
2024-12-23 23:30:21 -06:00
pointers
2024-12-23 17:29:38 -06:00
};
properties[key].push(collapsedDelta);
2024-12-23 17:29:38 -06:00
}
}
view[ent.id] = {
referencedAs: Array.from(referencedAs.values()),
properties
};
2024-12-23 17:29:38 -06:00
}
return view;
}
2024-12-25 00:42:16 -06:00
// TODO: point-in-time queries
2024-12-23 17:29:38 -06:00
}