107 lines
4.9 KiB
TypeScript
107 lines
4.9 KiB
TypeScript
import { Assertion, Entity, Handle, LocalAction, Ref, Turn } from 'actor';
|
|
import { Dictionary, IdentityMap, is, preserves, Record, Tuple } from '@preserves/core';
|
|
import { Bag, ChangeDescription } from './bag';
|
|
|
|
import { fromObserve, toObserve, Observe } from './gen/dataspace';
|
|
export * from './gen/dataspace';
|
|
|
|
// 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. (3)
|
|
// Because reflection on attenuators is a big, heavy hammer, and it's
|
|
// better to be explicit about patterns! Also, some attenuations
|
|
// happen behind a veil of secrecy - they're not all open for the
|
|
// world to read about. Actors may proxy communications in arbitrary,
|
|
// secret ways.
|
|
//
|
|
// 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 class Dataspace implements Partial<Entity> {
|
|
readonly handleMap: IdentityMap<Handle, Record<Assertion, any, Ref>> = new IdentityMap();
|
|
readonly assertions = new Bag<Ref>();
|
|
readonly subscriptions = new Dictionary<Ref, Map<Ref, Dictionary<Ref, Handle>>>();
|
|
|
|
assert(turn: Turn, rec: Assertion, handle: Handle): void {
|
|
// console.log(preserves`ds ${turn.activeFacet.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;
|
|
{
|
|
const o = toObserve(rec);
|
|
if (o !== void 0) {
|
|
const seen = new Dictionary<Ref, Handle>();
|
|
if (!this.subscriptions.has(o.label)) this.subscriptions.set(o.label, new Map());
|
|
this.subscriptions.get(o.label)!.set(o.observer, seen);
|
|
this.assertions.forEach((_count, prev) =>
|
|
is((prev as Record<Assertion, any, Ref>).label, o.label)
|
|
&& seen.set(prev, turn.assert(o.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.activeFacet.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) => {
|
|
turn.retract(seen.get(rec));
|
|
seen.delete(rec);
|
|
});
|
|
{
|
|
const o = toObserve(rec);
|
|
if (o !== void 0) {
|
|
let peerMap = this.subscriptions.get(o.label)!;
|
|
peerMap.delete(o.observer);
|
|
if (peerMap.size === 0) this.subscriptions.delete(o.label);
|
|
}
|
|
}
|
|
}
|
|
|
|
message(turn: Turn, rec: Assertion): void {
|
|
// console.log(preserves`ds ${turn.activeFacet.id} message ${rec}`);
|
|
if (!Record.isRecord<Assertion, Tuple<Assertion>, Ref>(rec)) return;
|
|
this.subscriptions.get(rec.label)?.forEach((_seen, peer) => turn.message(peer, rec));
|
|
}
|
|
}
|
|
|
|
export function during(f: (t: Turn, a: Assertion) => (LocalAction | null)): Partial<Entity> {
|
|
const assertionMap = new Map<Handle, LocalAction>();
|
|
return {
|
|
assert(t: Turn, a: Assertion, h: Handle): void {
|
|
const g = f(t, a);
|
|
if (g !== null) assertionMap.set(h, g);
|
|
},
|
|
retract(t: Turn, h: Handle): void {
|
|
assertionMap.get(h)?.(t);
|
|
assertionMap.delete(h);
|
|
},
|
|
};
|
|
}
|
|
|
|
export function observe(t: Turn, ds: Ref, label: symbol, e: Partial<Entity>): Handle {
|
|
return t.assert(ds, fromObserve(Observe({ label, observer: t.ref(e) })));
|
|
}
|