novy-syndicate/src/dataspace.ts

83 lines
4.1 KiB
TypeScript

import { Assertion, Entity, Handle, Ref, Turn } from 'actor';
import { Dictionary, IdentityMap, is, preserves, Record, Tuple } from 'preserves';
import { Bag, ChangeDescription } from './bag';
// Q. Why keep "Observe"? Why not do the clever trick of asserting the
// observer, and having the dataspace read the implicit pattern it's
// interested in off its attenuator?
//
// A. (1) Because we want to have the possibility of more than one
// variety of pattern language. For example, here we have a simple
// label match, but we'll quickly want something about as rich as the
// pattern language in attenuators. And later, we could easily want
// something with, perhaps, as much power as RELAX-NG or similar. (2)
// Because we want to have onlookers have some hope of seeing whether
// a pattern of interest to them is being observed, and if we used
// attenuators to match, we'd have to expose visibility into
// attenuators into the pattern language. See next question.
//
// Q. What kinds of constraints on the pattern language are there?
//
// A. It should be fast to evaluate; ideally, JITtable? It should
// allow patterns on patterns, to some degree. This is for two
// reasons: we occasionally want to observe observers, and we
// frequently want to use attenuators limit the kinds of patterns that
// a principal may observe. As such, it's good to choose a language
// that enforced some kind of normal forms for its patterns, so
// observer-observers and attenuator patterns don't have to deal with
// spurious variation.
//
export const Observe = Record.makeConstructor<{label: Assertion, observer: Ref}, Ref>()(
Symbol.for('Observe'), ['label', 'observer']);
export class Dataspace implements Partial<Entity> {
readonly handleMap: IdentityMap<Handle, Record<Assertion, any, Ref>> = new IdentityMap();
readonly assertions = new Bag<Ref>();
readonly subscriptions: Dictionary<Map<Ref, Dictionary<Handle>>> = new Dictionary();
assert(turn: Turn, rec: Assertion, handle: Handle): void {
// console.log(preserves`ds ${turn.actor.id} assert ${rec} ${handle}`);
if (!Record.isRecord<Assertion, Tuple<Assertion>, Ref>(rec)) return;
this.handleMap.set(handle, rec);
if (this.assertions.change(rec, +1) !== ChangeDescription.ABSENT_TO_PRESENT) return;
if (Observe.isClassOf(rec)) {
const label = Observe._.label(rec)!;
const observer = Observe._.observer(rec) as Ref;
const seen = new Dictionary<Handle>();
if (!this.subscriptions.has(label)) this.subscriptions.set(label, new Map());
this.subscriptions.get(label)!.set(observer, seen);
this.assertions.forEach((_count, prev) =>
is((prev as Record<Assertion, any, Ref>).label, label)
&& seen.set(prev, turn.assert(observer, prev)));
}
this.subscriptions.get(rec.label)?.forEach((seen, peer) =>
seen.has(rec) || seen.set(rec, turn.assert(peer, rec)));
}
retract(turn: Turn, upstreamHandle: Handle): void {
const rec = this.handleMap.get(upstreamHandle);
// console.log(preserves`ds ${turn.actor.id} retract ${rec} ${upstreamHandle}`);
if (rec === void 0) return;
this.handleMap.delete(upstreamHandle);
if (this.assertions.change(rec, -1) !== ChangeDescription.PRESENT_TO_ABSENT) return;
this.subscriptions.get(rec.label)?.forEach((seen, _peer) => {
const downstreamHandle = seen.get(rec);
if (downstreamHandle !== void 0) {
turn.retract(downstreamHandle);
seen.delete(rec);
}
});
if (Observe.isClassOf(rec)) {
let peerMap = this.subscriptions.get(Observe._.label(rec)!)!;
peerMap.delete(Observe._.observer(rec) as Ref);
if (peerMap.size === 0) this.subscriptions.delete(Observe._.label(rec)!);
}
}
message(turn: Turn, rec: Assertion): void {
// console.log(preserves`ds ${turn.actor.id} message ${rec}`);
if (!Record.isRecord<Assertion, Tuple<Assertion>, Ref>(rec)) return;
this.subscriptions.get(rec.label)?.forEach((_seen, peer) => turn.message(peer, rec));
}
}