2024-12-22 14:17:44 -06:00
|
|
|
// A basic collection of entities
|
|
|
|
// This may be extended to house a collection of objects that all follow a common schema.
|
2024-12-21 21:16:18 -06:00
|
|
|
// It should enable operations like removing a property removes the value from the entities in the collection
|
|
|
|
// It could then be further extended with e.g. table semantics like filter, sort, join
|
|
|
|
|
2024-12-26 15:59:03 -06:00
|
|
|
import Debug from 'debug';
|
2024-12-25 16:13:48 -06:00
|
|
|
import {randomUUID} from "node:crypto";
|
2024-12-22 09:13:44 -06:00
|
|
|
import EventEmitter from "node:events";
|
2024-12-31 11:35:09 -06:00
|
|
|
import {Delta, DeltaFilter} from "./delta.js";
|
|
|
|
import {Entity, EntityProperties} from "./entity.js";
|
|
|
|
import {Lossy, ResolvedViewOne, Resolver} from "./lossy.js";
|
|
|
|
import {RhizomeNode} from "./node.js";
|
|
|
|
import {DomainEntityID} from "./types.js";
|
2024-12-26 15:59:03 -06:00
|
|
|
const debug = Debug('collection');
|
2024-12-22 09:13:44 -06:00
|
|
|
|
|
|
|
export class Collection {
|
2024-12-25 16:13:48 -06:00
|
|
|
rhizomeNode?: RhizomeNode;
|
|
|
|
name: string;
|
2024-12-22 09:13:44 -06:00
|
|
|
eventStream = new EventEmitter();
|
2024-12-30 01:23:11 -06:00
|
|
|
lossy?: Lossy;
|
2024-12-25 16:13:48 -06:00
|
|
|
|
|
|
|
constructor(name: string) {
|
|
|
|
this.name = name;
|
2024-12-27 13:43:43 -06:00
|
|
|
}
|
|
|
|
|
2024-12-26 15:59:03 -06:00
|
|
|
// Instead of trying to update our final view of the entity with every incoming delta,
|
|
|
|
// let's try this:
|
|
|
|
// - keep a lossless view (of everything)
|
|
|
|
// - build a lossy view when needed
|
|
|
|
// This approach is simplistic, but can then be optimized and enhanced.
|
|
|
|
|
2024-12-25 16:13:48 -06:00
|
|
|
rhizomeConnect(rhizomeNode: RhizomeNode) {
|
|
|
|
this.rhizomeNode = rhizomeNode;
|
|
|
|
|
2024-12-30 01:23:11 -06:00
|
|
|
this.lossy = new Lossy(this.rhizomeNode.lossless);
|
|
|
|
|
|
|
|
// Listen for completed transactions, and emit updates to event stream
|
|
|
|
this.rhizomeNode.lossless.eventStream.on("updated", (id) => {
|
|
|
|
// TODO: Filter so we only get members of our collection
|
|
|
|
|
|
|
|
// TODO: Reslover / Delta Filter?
|
|
|
|
const res = this.resolve(id);
|
|
|
|
this.eventStream.emit("update", res);
|
2024-12-22 09:13:44 -06:00
|
|
|
});
|
2024-12-25 16:13:48 -06:00
|
|
|
|
2024-12-27 13:43:43 -06:00
|
|
|
rhizomeNode.httpServer.httpApi.serveCollection(this);
|
2024-12-26 15:59:03 -06:00
|
|
|
|
|
|
|
debug(`connected ${this.name} to rhizome`);
|
2024-12-22 09:13:44 -06:00
|
|
|
}
|
|
|
|
|
2024-12-27 13:43:43 -06:00
|
|
|
// Applies the javascript rules for updating object values,
|
2024-12-29 14:35:30 -06:00
|
|
|
// e.g. set to `undefined` to delete a property.
|
|
|
|
// This function is here instead of Entity so that it can:
|
|
|
|
// - read the current state in order to build its delta
|
|
|
|
// - include the collection name in the delta it produces
|
2024-12-27 13:43:43 -06:00
|
|
|
generateDeltas(
|
|
|
|
entityId: DomainEntityID,
|
|
|
|
newProperties: EntityProperties,
|
2024-12-29 17:50:20 -06:00
|
|
|
creator: string,
|
|
|
|
host: string,
|
|
|
|
resolver?: Resolver
|
2024-12-30 01:23:11 -06:00
|
|
|
): {
|
|
|
|
transactionDelta: Delta | undefined,
|
|
|
|
deltas: Delta[]
|
|
|
|
} {
|
2024-12-22 09:13:44 -06:00
|
|
|
const deltas: Delta[] = [];
|
2024-12-27 13:43:43 -06:00
|
|
|
let oldProperties: EntityProperties = {};
|
|
|
|
|
|
|
|
if (entityId) {
|
2024-12-29 14:35:30 -06:00
|
|
|
const entity = this.resolve(entityId, resolver);
|
2024-12-27 13:43:43 -06:00
|
|
|
if (entity) {
|
|
|
|
oldProperties = entity.properties;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-12-29 17:50:20 -06:00
|
|
|
// Generate a transaction ID
|
|
|
|
const transactionId = `transaction-${randomUUID()}`;
|
|
|
|
|
2024-12-27 13:43:43 -06:00
|
|
|
// Generate a delta for each changed property
|
|
|
|
Object.entries(newProperties).forEach(([key, value]) => {
|
2024-12-29 17:50:20 -06:00
|
|
|
// Disallow property named "id"
|
2024-12-27 13:43:43 -06:00
|
|
|
if (key === 'id') return;
|
|
|
|
|
|
|
|
if (oldProperties[key] !== value && host && creator) {
|
|
|
|
deltas.push(new Delta({
|
|
|
|
creator,
|
|
|
|
host,
|
|
|
|
pointers: [{
|
|
|
|
localContext: this.name,
|
|
|
|
target: entityId,
|
|
|
|
targetContext: key
|
|
|
|
}, {
|
|
|
|
localContext: key,
|
|
|
|
target: value
|
|
|
|
}]
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2024-12-30 01:23:11 -06:00
|
|
|
let transactionDelta: Delta | undefined;
|
|
|
|
|
|
|
|
if (deltas.length > 1) {
|
|
|
|
// We can generate a separate delta describing this transaction
|
|
|
|
transactionDelta = new Delta({
|
|
|
|
creator,
|
|
|
|
host,
|
|
|
|
pointers: [{
|
|
|
|
localContext: "_transaction",
|
|
|
|
target: transactionId,
|
|
|
|
targetContext: "size"
|
|
|
|
}, {
|
|
|
|
localContext: "size",
|
|
|
|
target: deltas.length
|
|
|
|
}]
|
|
|
|
});
|
|
|
|
|
|
|
|
// Also need to annotate the deltas with the transactionId
|
|
|
|
for (const delta of deltas) {
|
|
|
|
delta.pointers.unshift({
|
|
|
|
localContext: "_transaction",
|
|
|
|
target: transactionId,
|
|
|
|
targetContext: "deltas"
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
2024-12-29 17:50:20 -06:00
|
|
|
|
2024-12-30 01:23:11 -06:00
|
|
|
return {transactionDelta, deltas};
|
2024-12-27 13:43:43 -06:00
|
|
|
}
|
|
|
|
|
2024-12-29 14:35:30 -06:00
|
|
|
onCreate(cb: (entity: Entity) => void) {
|
|
|
|
// TODO: Trigger for changes received from peers
|
|
|
|
this.eventStream.on('create', (entity: Entity) => {
|
|
|
|
cb(entity);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
onUpdate(cb: (entity: Entity) => void) {
|
|
|
|
// TODO: Trigger for changes received from peers
|
|
|
|
this.eventStream.on('update', (entity: Entity) => {
|
|
|
|
cb(entity);
|
|
|
|
});
|
|
|
|
}
|
2024-12-27 13:43:43 -06:00
|
|
|
|
2024-12-29 14:35:30 -06:00
|
|
|
getIds(): string[] {
|
|
|
|
if (!this.rhizomeNode) return [];
|
2024-12-29 17:50:20 -06:00
|
|
|
const set = this.rhizomeNode.lossless.referencedAs.get(this.name);
|
|
|
|
if (!set) return [];
|
|
|
|
return Array.from(set.values());
|
2024-12-29 14:35:30 -06:00
|
|
|
}
|
2024-12-27 13:43:43 -06:00
|
|
|
|
2024-12-29 14:35:30 -06:00
|
|
|
// THIS PUT SHOULD CORRESOND TO A PARTICULAR MATERIALIZED VIEW...
|
|
|
|
// How can we encode that?
|
|
|
|
// Well, we have a way to do that, we just need the same particular inputs.
|
|
|
|
// We take a resolver as an optional argument.
|
|
|
|
async put(
|
|
|
|
entityId: DomainEntityID | undefined,
|
|
|
|
properties: EntityProperties,
|
|
|
|
resolver?: Resolver
|
|
|
|
): Promise<ResolvedViewOne> {
|
2024-12-29 17:50:20 -06:00
|
|
|
if (!this.rhizomeNode) throw new Error('collection not connecte to rhizome');
|
|
|
|
|
2024-12-29 14:35:30 -06:00
|
|
|
// For convenience, we allow setting id via properties.id
|
|
|
|
if (!entityId && !!properties.id && typeof properties.id === 'string') {
|
|
|
|
entityId = properties.id;
|
|
|
|
}
|
|
|
|
// Generate an ID if none is provided
|
2024-12-27 13:43:43 -06:00
|
|
|
if (!entityId) {
|
|
|
|
entityId = randomUUID();
|
|
|
|
}
|
|
|
|
|
2024-12-30 01:23:11 -06:00
|
|
|
const {transactionDelta, deltas} = this.generateDeltas(
|
2024-12-27 13:43:43 -06:00
|
|
|
entityId,
|
|
|
|
properties,
|
|
|
|
this.rhizomeNode?.config.creator,
|
|
|
|
this.rhizomeNode?.config.peerId,
|
2024-12-29 17:50:20 -06:00
|
|
|
resolver,
|
2024-12-27 13:43:43 -06:00
|
|
|
);
|
2024-12-26 15:59:03 -06:00
|
|
|
|
2024-12-30 01:23:11 -06:00
|
|
|
const ingested = new Promise<boolean>((resolve) => {
|
|
|
|
this.rhizomeNode!.lossless.eventStream.on("updated", (id: DomainEntityID) => {
|
|
|
|
if (id === entityId) resolve(true);
|
2024-12-27 13:43:43 -06:00
|
|
|
})
|
|
|
|
});
|
|
|
|
|
2024-12-30 01:23:11 -06:00
|
|
|
if (transactionDelta) {
|
|
|
|
deltas.unshift(transactionDelta);
|
|
|
|
}
|
2024-12-26 15:59:03 -06:00
|
|
|
|
2024-12-30 01:23:11 -06:00
|
|
|
deltas.forEach(async (delta: Delta) => {
|
2024-12-26 15:59:03 -06:00
|
|
|
// record this delta just as if we had received it from a peer
|
2024-12-25 16:13:48 -06:00
|
|
|
delta.receivedFrom = this.rhizomeNode!.myRequestAddr;
|
|
|
|
this.rhizomeNode!.deltaStream.deltasAccepted.push(delta);
|
2024-12-22 14:17:44 -06:00
|
|
|
|
2024-12-26 15:59:03 -06:00
|
|
|
// publish the delta to our subscribed peers
|
2024-12-25 16:13:48 -06:00
|
|
|
await this.rhizomeNode!.deltaStream.publishDelta(delta);
|
2024-12-26 15:59:03 -06:00
|
|
|
|
|
|
|
// ingest the delta as though we had received it from a peer
|
2024-12-30 01:23:11 -06:00
|
|
|
this.rhizomeNode!.lossless.ingestDelta(delta);
|
2024-12-22 09:13:44 -06:00
|
|
|
});
|
2024-12-27 13:43:43 -06:00
|
|
|
|
|
|
|
// Return updated view of this entity
|
|
|
|
// Let's wait for an event notifying us that the entity has been updated.
|
|
|
|
// This means all of our deltas have been applied.
|
|
|
|
|
2024-12-30 01:23:11 -06:00
|
|
|
await ingested;
|
2024-12-27 13:43:43 -06:00
|
|
|
|
2024-12-29 14:35:30 -06:00
|
|
|
const res = this.resolve(entityId, resolver);
|
2024-12-27 13:43:43 -06:00
|
|
|
if (!res) throw new Error("could not get what we just put!");
|
|
|
|
return res;
|
2024-12-22 09:13:44 -06:00
|
|
|
}
|
2024-12-22 14:17:44 -06:00
|
|
|
|
2024-12-30 01:23:11 -06:00
|
|
|
resolve<T = ResolvedViewOne>(
|
|
|
|
id: string,
|
|
|
|
resolver?: Resolver,
|
|
|
|
deltaFilter?: DeltaFilter
|
|
|
|
): T | undefined {
|
2024-12-31 11:35:09 -06:00
|
|
|
if (!this.rhizomeNode) throw new Error('collection not connected to rhizome');
|
|
|
|
if (!this.lossy) throw new Error('lossy view not initialized');
|
2024-12-29 14:35:30 -06:00
|
|
|
|
2024-12-31 11:35:09 -06:00
|
|
|
const res = this.lossy.resolve(resolver, [id], deltaFilter) || {};
|
2024-12-22 14:17:44 -06:00
|
|
|
|
2024-12-29 17:50:20 -06:00
|
|
|
return res[id] as T;
|
2024-12-22 09:13:44 -06:00
|
|
|
}
|
2024-12-21 21:16:18 -06:00
|
|
|
}
|