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
|
|
|
|
|
2024-12-26 15:59:03 -06:00
|
|
|
import Debug from 'debug';
|
2024-12-30 01:23:11 -06:00
|
|
|
import EventEmitter from 'events';
|
2024-12-31 11:35:09 -06:00
|
|
|
import {Delta, DeltaFilter, DeltaNetworkImage} from './delta.js';
|
|
|
|
import {Transactions} from './transactions.js';
|
|
|
|
import {DomainEntityID, PropertyID, PropertyTypes, TransactionID, ViewMany} from "./types.js";
|
2024-12-26 15:59:03 -06:00
|
|
|
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-26 15:59:03 -06:00
|
|
|
|
2024-12-30 01:23:11 -06:00
|
|
|
export type CollapsedDelta = Omit<DeltaNetworkImage, 'pointers'> & {
|
2024-12-23 17:29:38 -06:00
|
|
|
pointers: CollapsedPointer[];
|
|
|
|
};
|
2024-12-26 15:59:03 -06:00
|
|
|
|
2024-12-23 23:30:21 -06:00
|
|
|
export type LosslessViewOne = {
|
|
|
|
referencedAs: string[];
|
|
|
|
properties: {
|
|
|
|
[key: PropertyID]: CollapsedDelta[]
|
|
|
|
}
|
|
|
|
};
|
2024-12-26 15:59:03 -06:00
|
|
|
|
2024-12-29 14:35:30 -06:00
|
|
|
export type LosslessViewMany = ViewMany<LosslessViewOne>;
|
2024-12-23 17:29:38 -06:00
|
|
|
|
2024-12-30 01:23:11 -06:00
|
|
|
class LosslessEntityMap extends Map<DomainEntityID, LosslessEntity> {};
|
2024-12-23 17:29:38 -06:00
|
|
|
|
2024-12-30 01:23:11 -06:00
|
|
|
class LosslessEntity {
|
2024-12-23 17:29:38 -06:00
|
|
|
id: DomainEntityID;
|
2024-12-26 15:59:03 -06:00
|
|
|
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-26 15:59:03 -06:00
|
|
|
|
2024-12-23 17:29:38 -06:00
|
|
|
for (const targetContext of targetContexts) {
|
2024-12-26 15:59:03 -06:00
|
|
|
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
|
|
|
}
|
2024-12-26 15:59:03 -06:00
|
|
|
|
|
|
|
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
|
|
|
}
|
2024-12-26 15:59:03 -06:00
|
|
|
return {
|
|
|
|
id: this.id,
|
|
|
|
properties
|
|
|
|
};
|
2024-12-23 17:29:38 -06:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export class Lossless {
|
2024-12-30 01:23:11 -06:00
|
|
|
domainEntities = new LosslessEntityMap();
|
2024-12-29 17:50:20 -06:00
|
|
|
transactions = new Transactions();
|
|
|
|
referencedAs = new Map<string, Set<DomainEntityID>>();
|
2024-12-30 01:23:11 -06:00
|
|
|
eventStream = new EventEmitter();
|
|
|
|
|
|
|
|
constructor() {
|
|
|
|
this.transactions.eventStream.on("completed", (transactionId) => {
|
|
|
|
debug(`completed transaction ${transactionId}`);
|
|
|
|
const transaction = this.transactions.get(transactionId);
|
|
|
|
if (!transaction) return;
|
|
|
|
for (const id of transaction.entityIds) {
|
|
|
|
this.eventStream.emit("updated", id);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2024-12-23 17:29:38 -06:00
|
|
|
|
2024-12-30 01:23:11 -06:00
|
|
|
ingestDelta(delta: Delta): TransactionID | undefined {
|
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-26 15:59:03 -06:00
|
|
|
|
2024-12-23 17:29:38 -06:00
|
|
|
for (const target of targets) {
|
|
|
|
let ent = this.domainEntities.get(target);
|
2024-12-26 15:59:03 -06:00
|
|
|
|
2024-12-23 17:29:38 -06:00
|
|
|
if (!ent) {
|
2024-12-30 01:23:11 -06:00
|
|
|
ent = new LosslessEntity(target);
|
2024-12-23 17:29:38 -06:00
|
|
|
this.domainEntities.set(target, ent);
|
|
|
|
}
|
2024-12-26 15:59:03 -06:00
|
|
|
|
2024-12-23 17:29:38 -06:00
|
|
|
ent.addDelta(delta);
|
2024-12-29 17:50:20 -06:00
|
|
|
}
|
2024-12-26 15:59:03 -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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-12-30 01:23:11 -06:00
|
|
|
const transactionId = this.transactions.ingestDelta(delta, targets);
|
|
|
|
|
|
|
|
if (!transactionId) {
|
|
|
|
// No transaction -- we can issue an update event immediately
|
|
|
|
for (const id of targets) {
|
|
|
|
this.eventStream.emit("updated", id);
|
2024-12-29 17:50:20 -06:00
|
|
|
}
|
2024-12-23 17:29:38 -06:00
|
|
|
}
|
2024-12-30 01:23:11 -06:00
|
|
|
return transactionId;
|
2024-12-23 17:29:38 -06:00
|
|
|
}
|
|
|
|
|
2024-12-26 15:59:03 -06:00
|
|
|
view(entityIds?: DomainEntityID[], deltaFilter?: DeltaFilter): LosslessViewMany {
|
2024-12-23 23:30:21 -06:00
|
|
|
const view: LosslessViewMany = {};
|
2024-12-26 15:59:03 -06:00
|
|
|
entityIds = entityIds ?? Array.from(this.domainEntities.keys());
|
|
|
|
for (const id of entityIds) {
|
|
|
|
const ent = this.domainEntities.get(id);
|
|
|
|
if (!ent) continue;
|
|
|
|
|
2024-12-23 23:30:21 -06:00
|
|
|
const referencedAs = new Set<string>();
|
2024-12-26 15:59:03 -06:00
|
|
|
const properties: {
|
|
|
|
[key: PropertyID]: CollapsedDelta[]
|
|
|
|
} = {};
|
|
|
|
|
|
|
|
for (const [key, deltas] of ent.properties.entries()) {
|
|
|
|
properties[key] = properties[key] || [];
|
|
|
|
|
|
|
|
for (const delta of deltas) {
|
2024-12-30 01:23:11 -06:00
|
|
|
// If this delta is part of a transaction,
|
|
|
|
// we need to be able to wait for the whole transaction.
|
|
|
|
if (delta.transactionId) {
|
|
|
|
if (!this.transactions.isComplete(delta.transactionId)) {
|
|
|
|
// TODO: Test this condition
|
2024-12-31 11:35:09 -06:00
|
|
|
debug(`excluding delta ${delta.id} because transaction ${delta.transactionId} is not completed`);
|
2024-12-30 01:23:11 -06:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
2024-12-26 15:59:03 -06:00
|
|
|
|
2024-12-23 20:44:54 -06:00
|
|
|
if (deltaFilter) {
|
|
|
|
const include = deltaFilter(delta);
|
|
|
|
if (!include) continue;
|
|
|
|
}
|
2024-12-26 15:59:03 -06:00
|
|
|
|
2024-12-23 23:30:21 -06:00
|
|
|
const pointers: CollapsedPointer[] = [];
|
2024-12-26 15:59:03 -06:00
|
|
|
|
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-26 15:59:03 -06:00
|
|
|
|
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
|
|
|
};
|
2024-12-26 15:59:03 -06:00
|
|
|
|
|
|
|
properties[key].push(collapsedDelta);
|
2024-12-23 17:29:38 -06:00
|
|
|
}
|
|
|
|
}
|
2024-12-26 15:59:03 -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
|
|
|
}
|