diff --git a/dataspace.ts b/dataspace.ts new file mode 100644 index 0000000..ed62b95 --- /dev/null +++ b/dataspace.ts @@ -0,0 +1,78 @@ +import { Assertion, Entity, Handle, Ref, Turn } from 'actor'; +import { Dictionary, IdentityMap, is, Record } 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('Observe', ['label', 'observer']); + +export class Dataspace implements Partial { + readonly handleMap: IdentityMap> = new IdentityMap(); + readonly assertions = new Bag(); + readonly subscriptions: Dictionary>> = new Dictionary(); + + assert(turn: Turn, rec: Assertion, handle: Handle): void { + if (!Record.isRecord(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(); + 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).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); + 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 { + if (!Record.isRecord(rec)) return; + this.subscriptions.get(rec.label)?.forEach((_seen, peer) => turn.message(peer, rec)); + } +} diff --git a/main.ts b/main.ts index 12e40fb..6f2177c 100644 --- a/main.ts +++ b/main.ts @@ -1,7 +1,6 @@ import { Actor, Assertion, - Entity, Handle, Ref, Turn, @@ -9,82 +8,8 @@ import { pdiscard, rfilter, } from './actor.js'; -import { Dictionary, IdentityMap, is, Record } 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. (1) It should be fast to evaluate; ideally, JITtable? (2) It -// should allow patterns on patterns, to some degree. We occasionally -// want to observe observers. As such, it'd be good to choose a -// language that enforced some kind of normal forms for its patterns, -// so observer-observers didn't have to deal with spurious variation. -// -const Observe = Record.makeConstructor('Observe', ['label', 'observer']); - -class Dataspace implements Partial { - readonly handleMap: IdentityMap> = new IdentityMap(); - readonly assertions = new Bag(); - readonly subscriptions: Dictionary>> = new Dictionary(); - - assert(turn: Turn, rec: Assertion, handle: Handle): void { - if (!Record.isRecord(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(); - 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).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); - 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 { - if (!Record.isRecord(rec)) return; - this.subscriptions.get(rec.label)?.forEach((_seen, peer) => turn.message(peer, rec)); - } -} - -//--------------------------------------------------------------------------- +import { Record } from 'preserves'; +import { Dataspace, Observe } from './dataspace.js'; const BoxState = Record.makeConstructor('BoxState', ['value']); const SetBox = Record.makeConstructor('SetBox', ['newValue']);