rhizome/src/lossless.ts

200 lines
6.3 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-30 01:23:11 -06:00
import EventEmitter from 'events';
import {Delta, DeltaFilter, DeltaID, DeltaNetworkImage} from './delta.js';
import {RhizomeNode} from './node.js';
import {Transactions} from './transactions.js';
import {DomainEntityID, PropertyID, PropertyTypes, TransactionID, ViewMany} from "./types.js";
2024-12-31 12:40:26 -06:00
const debug = Debug('rz: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-30 01:23:11 -06:00
export type CollapsedDelta = Omit<DeltaNetworkImage, 'pointers'> & {
2024-12-23 17:29:38 -06:00
pointers: CollapsedPointer[];
};
2024-12-23 23:30:21 -06:00
export type LosslessViewOne = {
id: DomainEntityID,
2024-12-23 23:30:21 -06:00
referencedAs: string[];
propertyDeltas: {
2024-12-23 23:30:21 -06:00
[key: PropertyID]: CollapsedDelta[]
}
};
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 {
properties = new Map<PropertyID, Set<Delta>>();
2024-12-23 17:29:38 -06:00
constructor(readonly lossless: Lossless, readonly id: DomainEntityID) {}
2024-12-23 17:29:38 -06:00
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
}
propertyDeltas.add(delta);
debug(`[${this.lossless.rhizomeNode.config.peerId}]`, `entity ${this.id} added delta:`, JSON.stringify(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
}
}
export class Lossless {
2024-12-30 01:23:11 -06:00
domainEntities = new LosslessEntityMap();
2024-12-31 12:28:24 -06:00
transactions: Transactions;
2024-12-29 17:50:20 -06:00
referencedAs = new Map<string, Set<DomainEntityID>>();
2024-12-30 01:23:11 -06:00
eventStream = new EventEmitter();
2024-12-31 12:28:24 -06:00
constructor(readonly rhizomeNode: RhizomeNode) {
this.transactions = new Transactions(this);
this.transactions.eventStream.on("completed", (transactionId, deltaIds) => {
2024-12-31 14:58:28 -06:00
debug(`[${this.rhizomeNode.config.peerId}]`, `Completed transaction ${transactionId}`);
2024-12-30 01:23:11 -06:00
const transaction = this.transactions.get(transactionId);
if (!transaction) return;
for (const id of transaction.entityIds) {
this.eventStream.emit("updated", id, deltaIds);
2024-12-30 01:23:11 -06:00
}
});
}
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-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 LosslessEntity(this, target);
2024-12-23 17:29:38 -06:00
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);
}
}
}
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, [delta.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
}
viewSpecific(entityId: DomainEntityID, deltaIds: DeltaID[], deltaFilter?: DeltaFilter): LosslessViewOne | undefined {
debug(`[${this.rhizomeNode.config.peerId}]`, `viewSpecific, deltaIds:`, JSON.stringify(deltaIds));
const combinedFilter = (delta: Delta) => {
debug(`[${this.rhizomeNode.config.peerId}]`, `combinedFilter, deltaIds:`, JSON.stringify(deltaIds));
if (!deltaIds.includes(delta.id)) {
debug(`[${this.rhizomeNode.config.peerId}]`, `Excluding delta ${delta.id} because it's not in the requested list of deltas`);
return false;
}
if (!deltaFilter) return true;
return deltaFilter(delta);
};
const res = this.view([entityId], (delta) => combinedFilter(delta));
return res[entityId];
}
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;
2024-12-23 23:30:21 -06:00
const referencedAs = new Set<string>();
const propertyDeltas: {
[key: PropertyID]: CollapsedDelta[]
} = {};
for (const [key, deltas] of ent.properties.entries()) {
propertyDeltas[key] = propertyDeltas[key] || [];
for (const delta of deltas) {
if (deltaFilter && !deltaFilter(delta)) {
continue;
}
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 14:58:28 -06:00
debug(`[${this.rhizomeNode.config.peerId}]`, `Excluding delta ${delta.id} because transaction ${delta.transactionId} is not completed`);
2024-12-30 01:23:11 -06:00
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);
}
}
propertyDeltas[key].push({
2024-12-23 17:29:38 -06:00
...delta,
2024-12-23 23:30:21 -06:00
pointers
});
2024-12-23 17:29:38 -06:00
}
}
view[ent.id] = {
id: ent.id,
referencedAs: Array.from(referencedAs.values()),
propertyDeltas
};
2024-12-23 17:29:38 -06:00
}
debug(`[${this.rhizomeNode.config.peerId}]`, `Returning view:`, JSON.stringify(view, null, 2));
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
}