2021-01-11 22:35:36 +00:00
|
|
|
//---------------------------------------------------------------------------
|
|
|
|
// @syndicate-lang/core, an implementation of Syndicate dataspaces for JS.
|
|
|
|
// Copyright (C) 2016-2021 Tony Garnock-Jones <tonyg@leastfixedpoint.com>
|
|
|
|
//
|
|
|
|
// This program is free software: you can redistribute it and/or modify
|
|
|
|
// it under the terms of the GNU General Public License as published by
|
|
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
|
|
// (at your option) any later version.
|
|
|
|
//
|
|
|
|
// This program is distributed in the hope that it will be useful,
|
|
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
// GNU General Public License for more details.
|
|
|
|
//
|
|
|
|
// You should have received a copy of the GNU General Public License
|
|
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
//---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
import { Value, fromJS, is, Set } from 'preserves';
|
|
|
|
|
|
|
|
import * as Skeleton from './skeleton.js';
|
|
|
|
import { Bag, ChangeDescription } from './bag.js';
|
|
|
|
import { Observe } from './assertions.js';
|
|
|
|
import * as Dataflow from './dataflow.js';
|
|
|
|
import { IdentitySet, IdentityMap } from './idcoll.js';
|
|
|
|
import { Ground } from './ground.js';
|
|
|
|
|
|
|
|
export enum Priority {
|
|
|
|
QUERY_HIGH = 0,
|
|
|
|
QUERY,
|
|
|
|
QUERY_HANDLER,
|
|
|
|
NORMAL,
|
|
|
|
GC,
|
|
|
|
IDLE,
|
|
|
|
_count
|
|
|
|
}
|
|
|
|
|
|
|
|
export type ActorId = number;
|
|
|
|
export type FacetId = ActorId;
|
|
|
|
export type EndpointId = ActorId;
|
|
|
|
|
2021-01-14 13:42:30 +00:00
|
|
|
export type Task<T> = () => T;
|
|
|
|
export type Script<T> = (f: Facet) => T;
|
2021-01-11 22:35:36 +00:00
|
|
|
|
|
|
|
export type MaybeValue = Value | undefined;
|
|
|
|
export type EndpointSpec = { assertion: MaybeValue, analysis: Skeleton.Analysis | null };
|
|
|
|
|
2021-01-14 13:42:30 +00:00
|
|
|
export type ObserverCallback = (facet: Facet, bindings: Array<Value>) => void;
|
2021-01-11 22:35:36 +00:00
|
|
|
|
|
|
|
export type ObserverCallbacks = {
|
|
|
|
add?: ObserverCallback;
|
|
|
|
del?: ObserverCallback;
|
|
|
|
msg?: ObserverCallback;
|
|
|
|
}
|
|
|
|
|
|
|
|
export const DataflowObservableObjectId = Symbol.for('DataflowObservableObjectId');
|
|
|
|
export interface DataflowObservableObject {
|
|
|
|
[DataflowObservableObjectId](): number;
|
|
|
|
}
|
|
|
|
|
|
|
|
export type DataflowObservable = [DataflowObservableObject, string];
|
|
|
|
export function _canonicalizeDataflowObservable(i: DataflowObservable): string {
|
|
|
|
return i[0][DataflowObservableObjectId]() + ',' + i[1];
|
|
|
|
}
|
|
|
|
|
|
|
|
export type DataflowDependent = Endpoint;
|
|
|
|
export function _canonicalizeDataflowDependent(i: DataflowDependent): string {
|
|
|
|
return '' + i.id;
|
|
|
|
}
|
|
|
|
|
2021-01-19 14:13:42 +00:00
|
|
|
export type ActivationScript = Script<void>;
|
|
|
|
|
2021-01-11 22:35:36 +00:00
|
|
|
export abstract class Dataspace {
|
|
|
|
nextId: ActorId = 0;
|
|
|
|
index = new Skeleton.Index();
|
|
|
|
dataflow = new Dataflow.Graph<DataflowDependent, DataflowObservable>(
|
|
|
|
_canonicalizeDataflowDependent,
|
|
|
|
_canonicalizeDataflowObservable);
|
|
|
|
runnable: Array<Actor> = [];
|
|
|
|
pendingTurns: Array<Turn>;
|
|
|
|
actors: IdentityMap<number, Actor> = new IdentityMap();
|
2021-01-19 14:13:42 +00:00
|
|
|
activations: IdentitySet<ActivationScript> = new IdentitySet();
|
2021-01-11 22:35:36 +00:00
|
|
|
|
2021-01-14 13:42:30 +00:00
|
|
|
constructor(bootProc: Script<void>) {
|
2021-01-11 22:35:36 +00:00
|
|
|
this.pendingTurns = [new Turn(null, [new Spawn(null, bootProc, new Set())])];
|
|
|
|
}
|
|
|
|
|
|
|
|
abstract start(): this;
|
|
|
|
abstract ground(): Ground;
|
|
|
|
|
2021-01-14 13:42:30 +00:00
|
|
|
backgroundTask(): () => void {
|
|
|
|
return this.ground().backgroundTask();
|
2021-01-11 22:35:36 +00:00
|
|
|
}
|
|
|
|
|
2021-01-16 16:46:18 +00:00
|
|
|
runTasks(): boolean { // TODO: rename?
|
2021-01-14 13:42:30 +00:00
|
|
|
this.runPendingTasks();
|
2021-01-11 22:35:36 +00:00
|
|
|
this.performPendingActions();
|
|
|
|
return this.runnable.length > 0 || this.pendingTurns.length > 0;
|
|
|
|
}
|
|
|
|
|
2021-01-14 13:42:30 +00:00
|
|
|
runPendingTasks() {
|
2021-01-11 22:35:36 +00:00
|
|
|
let runnable = this.runnable;
|
|
|
|
this.runnable = [];
|
2021-01-14 13:42:30 +00:00
|
|
|
runnable.forEach((ac) => { ac.runPendingTasks(); /* TODO: rename? */ });
|
2021-01-11 22:35:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
performPendingActions() {
|
|
|
|
let turns = this.pendingTurns;
|
|
|
|
this.pendingTurns = [];
|
|
|
|
turns.forEach((turn) => {
|
|
|
|
turn.actions.forEach((action) => {
|
|
|
|
// console.log('[DATASPACE]', group.actor && group.actor.toString(), action);
|
|
|
|
action.perform(this, turn.actor);
|
2021-01-14 13:42:30 +00:00
|
|
|
this.runPendingTasks();
|
2021-01-11 22:35:36 +00:00
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
commitActions(ac: Actor, pending: Array<Action>) {
|
|
|
|
this.pendingTurns.push(new Turn(ac, pending));
|
|
|
|
}
|
|
|
|
|
|
|
|
refreshAssertions() {
|
2021-01-15 12:38:15 +00:00
|
|
|
this.dataflow.repairDamage((ep) => {
|
|
|
|
let facet = ep.facet;
|
|
|
|
if (facet.isLive) { // TODO: necessary test, or tautological?
|
|
|
|
facet.invokeScript(f => f.withNonScriptContext(() => ep.refresh()));
|
|
|
|
}
|
2021-01-11 22:35:36 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2021-01-15 12:38:15 +00:00
|
|
|
addActor(name: any, bootProc: Script<void>, initialAssertions: Set, parentActor: Actor | null) {
|
2021-01-11 22:35:36 +00:00
|
|
|
let ac = new Actor(this, name, initialAssertions, parentActor?.id);
|
|
|
|
// debug('Spawn', ac && ac.toString());
|
|
|
|
this.applyPatch(ac, ac.adhocAssertions);
|
2021-01-14 13:42:30 +00:00
|
|
|
ac.addFacet(null, systemFacet => {
|
2021-01-11 22:35:36 +00:00
|
|
|
// Root facet is a dummy "system" facet that exists to hold
|
|
|
|
// one-or-more "user" "root" facets.
|
2021-01-14 13:42:30 +00:00
|
|
|
ac.addFacet(systemFacet, bootProc);
|
2021-01-11 22:35:36 +00:00
|
|
|
// ^ The "true root", user-visible facet.
|
|
|
|
initialAssertions.forEach((a) => { ac.adhocRetract(a); });
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
applyPatch(ac: Actor, delta: Bag) {
|
|
|
|
// if (!delta.isEmpty()) debug('applyPatch BEGIN', ac && ac.toString());
|
2021-01-15 12:38:15 +00:00
|
|
|
let removals: Array<[number, Value]> = [];
|
2021-01-11 22:35:36 +00:00
|
|
|
delta.forEach((count, a) => {
|
|
|
|
if (count > 0) {
|
|
|
|
// debug('applyPatch +', a && a.toString());
|
|
|
|
this.adjustIndex(a, count);
|
|
|
|
} else {
|
|
|
|
removals.push([count, a]);
|
|
|
|
}
|
|
|
|
if (ac) ac.cleanupChanges.change(a, -count);
|
|
|
|
});
|
|
|
|
removals.forEach(([count, a]) => {
|
|
|
|
// debug('applyPatch -', a && a.toString());
|
|
|
|
this.adjustIndex(a, count);
|
|
|
|
});
|
|
|
|
// if (!delta.isEmpty()) debug('applyPatch END');
|
|
|
|
}
|
|
|
|
|
2021-01-15 12:38:15 +00:00
|
|
|
deliverMessage(m: Value, _sendingActor: Actor | null) {
|
2021-01-14 13:42:30 +00:00
|
|
|
// debug('deliverMessage', sendingActor && sendingActor.toString(), m.toString());
|
|
|
|
this.index.deliverMessage(m);
|
|
|
|
// this.index.deliverMessage(m, (leaf, _m) => {
|
2021-01-11 22:35:36 +00:00
|
|
|
// sendingActor.touchedTopics = sendingActor.touchedTopics.add(leaf);
|
|
|
|
// });
|
|
|
|
}
|
|
|
|
|
|
|
|
adjustIndex(a: Value, count: number) {
|
|
|
|
return this.index.adjustAssertion(a, count);
|
|
|
|
}
|
|
|
|
|
|
|
|
subscribe(handler: Skeleton.Analysis) {
|
2021-01-15 12:38:15 +00:00
|
|
|
this.index.addHandler(handler, handler.callback!);
|
2021-01-11 22:35:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
unsubscribe(handler: Skeleton.Analysis) {
|
2021-01-15 12:38:15 +00:00
|
|
|
this.index.removeHandler(handler, handler.callback!);
|
2021-01-11 22:35:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
endpointHook(_facet: Facet, _endpoint: Endpoint) {
|
|
|
|
// Subclasses may override
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export class Actor {
|
|
|
|
readonly id: ActorId;
|
|
|
|
readonly dataspace: Dataspace;
|
|
|
|
readonly name: any;
|
|
|
|
rootFacet: Facet | null = null;
|
|
|
|
isRunnable: boolean = false;
|
2021-01-14 13:42:30 +00:00
|
|
|
readonly pendingTasks: Array<Array<Task<void>>>;
|
2021-01-11 22:35:36 +00:00
|
|
|
pendingActions: Array<Action>;
|
|
|
|
adhocAssertions: Bag;
|
|
|
|
cleanupChanges = new Bag(); // negative counts allowed!
|
|
|
|
parentId: ActorId | undefined;
|
|
|
|
|
|
|
|
constructor(dataspace: Dataspace,
|
|
|
|
name: any,
|
|
|
|
initialAssertions: Set,
|
|
|
|
parentActorId: ActorId | undefined)
|
|
|
|
{
|
|
|
|
this.id = dataspace.nextId++;
|
|
|
|
this.dataspace = dataspace;
|
|
|
|
this.name = name;
|
|
|
|
this.isRunnable = false;
|
2021-01-14 13:42:30 +00:00
|
|
|
this.pendingTasks = [];
|
|
|
|
for (let i = 0; i < Priority._count; i++) { this.pendingTasks.push([]); }
|
2021-01-11 22:35:36 +00:00
|
|
|
this.pendingActions = [];
|
|
|
|
this.adhocAssertions = new Bag(initialAssertions); // no negative counts allowed
|
|
|
|
this.parentId = parentActorId;
|
|
|
|
dataspace.actors.set(this.id, this);
|
|
|
|
}
|
|
|
|
|
2021-01-14 13:42:30 +00:00
|
|
|
runPendingTasks() {
|
2021-01-11 22:35:36 +00:00
|
|
|
while (true) {
|
2021-01-14 13:42:30 +00:00
|
|
|
let task = this.popNextTask();
|
|
|
|
if (!task) break;
|
|
|
|
task();
|
2021-01-11 22:35:36 +00:00
|
|
|
this.dataspace.refreshAssertions();
|
|
|
|
}
|
|
|
|
|
|
|
|
this.isRunnable = false;
|
|
|
|
let pending = this.pendingActions;
|
|
|
|
if (pending.length > 0) {
|
|
|
|
this.pendingActions = [];
|
|
|
|
this.dataspace.commitActions(this, pending);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-14 13:42:30 +00:00
|
|
|
popNextTask(): Task<void> | null {
|
|
|
|
let tasks = this.pendingTasks;
|
2021-01-11 22:35:36 +00:00
|
|
|
for (let i = 0; i < Priority._count; i++) {
|
2021-01-14 13:42:30 +00:00
|
|
|
let q = tasks[i];
|
2021-01-15 12:38:15 +00:00
|
|
|
if (q.length > 0) return q.shift()!;
|
2021-01-11 22:35:36 +00:00
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
abandonQueuedWork() {
|
|
|
|
this.pendingActions = [];
|
2021-01-14 13:42:30 +00:00
|
|
|
for (let i = 0; i < Priority._count; i++) { this.pendingTasks[i] = []; }
|
2021-01-11 22:35:36 +00:00
|
|
|
}
|
|
|
|
|
2021-01-14 13:42:30 +00:00
|
|
|
scheduleTask(task: Task<void>, priority: Priority = Priority.NORMAL) {
|
2021-01-11 22:35:36 +00:00
|
|
|
if (!this.isRunnable) {
|
|
|
|
this.isRunnable = true;
|
|
|
|
this.dataspace.runnable.push(this);
|
|
|
|
}
|
2021-01-14 13:42:30 +00:00
|
|
|
this.pendingTasks[priority].push(task);
|
2021-01-11 22:35:36 +00:00
|
|
|
}
|
|
|
|
|
2021-01-15 12:38:15 +00:00
|
|
|
addFacet(parentFacet: Facet | null, bootProc: Script<void>, checkInScript: boolean = false) {
|
|
|
|
if (checkInScript && parentFacet && !parentFacet.inScript) {
|
2021-01-11 22:35:36 +00:00
|
|
|
throw new Error("Cannot add facet outside script; are you missing a `react { ... }`?");
|
|
|
|
}
|
|
|
|
let f = new Facet(this, parentFacet);
|
2021-01-15 12:38:15 +00:00
|
|
|
f.invokeScript(f => f.withNonScriptContext(() => bootProc.call(f.fields, f)));
|
2021-01-14 13:42:30 +00:00
|
|
|
this.scheduleTask(() => {
|
2021-01-11 22:35:36 +00:00
|
|
|
if ((parentFacet && !parentFacet.isLive) || f.isInert()) {
|
|
|
|
f._terminate();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
_terminate(emitPatches: boolean) {
|
|
|
|
// Abruptly terminates an entire actor, without running stop-scripts etc.
|
|
|
|
if (emitPatches) {
|
2021-01-14 13:42:30 +00:00
|
|
|
this.scheduleTask(() => {
|
2021-01-11 22:35:36 +00:00
|
|
|
this.adhocAssertions.snapshot().forEach((_count, a) => { this.retract(a); });
|
|
|
|
});
|
|
|
|
}
|
|
|
|
if (this.rootFacet) {
|
|
|
|
this.rootFacet._abort(emitPatches);
|
|
|
|
}
|
2021-01-14 13:42:30 +00:00
|
|
|
this.scheduleTask(() => { this.enqueueScriptAction(new Quit()); });
|
2021-01-11 22:35:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
enqueueScriptAction(action: Action) {
|
|
|
|
this.pendingActions.push(action);
|
|
|
|
}
|
|
|
|
|
|
|
|
pendingPatch(): Patch {
|
|
|
|
if (this.pendingActions.length > 0) {
|
|
|
|
let p = this.pendingActions[this.pendingActions.length - 1];
|
|
|
|
if (p instanceof Patch) return p;
|
|
|
|
}
|
|
|
|
let p = new Patch(new Bag());
|
|
|
|
this.enqueueScriptAction(p);
|
|
|
|
return p;
|
|
|
|
}
|
|
|
|
|
|
|
|
assert(a: Value) { this.pendingPatch().adjust(a, +1); }
|
|
|
|
retract(a: Value) { this.pendingPatch().adjust(a, -1); }
|
|
|
|
|
|
|
|
adhocRetract(a: Value) {
|
|
|
|
a = fromJS(a);
|
|
|
|
if (this.adhocAssertions.change(a, -1, true) === ChangeDescription.PRESENT_TO_ABSENT) {
|
|
|
|
this.retract(a);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
adhocAssert(a: Value) {
|
|
|
|
a = fromJS(a);
|
|
|
|
if (this.adhocAssertions.change(a, +1) === ChangeDescription.ABSENT_TO_PRESENT) {
|
|
|
|
this.assert(a);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
toString(): string {
|
|
|
|
let s = 'Actor(' + this.id;
|
|
|
|
if (this.name !== void 0 && this.name !== null) s = s + ',' + this.name.toString();
|
|
|
|
return s + ')';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
abstract class Action {
|
2021-01-15 12:38:15 +00:00
|
|
|
abstract perform(ds: Dataspace, ac: Actor | null): void;
|
2021-01-11 22:35:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
class Patch extends Action {
|
|
|
|
readonly changes: Bag;
|
|
|
|
|
|
|
|
constructor(changes: Bag) {
|
|
|
|
super();
|
|
|
|
this.changes = changes;
|
|
|
|
}
|
|
|
|
|
2021-01-15 12:38:15 +00:00
|
|
|
perform(ds: Dataspace, ac: Actor | null): void {
|
|
|
|
ds.applyPatch(ac!, this.changes);
|
2021-01-11 22:35:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
adjust(a: Value, count: number) {
|
|
|
|
this.changes.change(fromJS(a), count);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class Message extends Action {
|
|
|
|
readonly body: Value;
|
|
|
|
|
|
|
|
constructor(body: any) {
|
|
|
|
super();
|
|
|
|
this.body = fromJS(body);
|
|
|
|
}
|
|
|
|
|
2021-01-15 12:38:15 +00:00
|
|
|
perform(ds: Dataspace, ac: Actor | null): void {
|
2021-01-14 13:42:30 +00:00
|
|
|
ds.deliverMessage(this.body, ac);
|
2021-01-11 22:35:36 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class Spawn extends Action {
|
|
|
|
readonly name: any;
|
2021-01-14 13:42:30 +00:00
|
|
|
readonly bootProc: Script<void>;
|
2021-01-11 22:35:36 +00:00
|
|
|
readonly initialAssertions: Set;
|
|
|
|
|
2021-01-14 13:42:30 +00:00
|
|
|
constructor(name: any, bootProc: Script<void>, initialAssertions: Set = new Set()) {
|
2021-01-11 22:35:36 +00:00
|
|
|
super();
|
|
|
|
this.name = name;
|
|
|
|
this.bootProc = bootProc;
|
|
|
|
this.initialAssertions = initialAssertions;
|
|
|
|
}
|
|
|
|
|
2021-01-15 12:38:15 +00:00
|
|
|
perform(ds: Dataspace, ac: Actor | null): void {
|
2021-01-11 22:35:36 +00:00
|
|
|
ds.addActor(this.name, this.bootProc, this.initialAssertions, ac);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class Quit extends Action { // TODO: rename? Perhaps to Cleanup?
|
|
|
|
// Pseudo-action - not for userland use.
|
|
|
|
|
2021-01-15 12:38:15 +00:00
|
|
|
perform(ds: Dataspace, ac: Actor | null): void {
|
|
|
|
if (ac === null) throw new Error("Internal error: Quit action with null actor");
|
2021-01-11 22:35:36 +00:00
|
|
|
ds.applyPatch(ac, ac.cleanupChanges);
|
|
|
|
ds.actors.delete(ac.id);
|
|
|
|
// debug('Quit', ac && ac.toString());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class DeferredTurn extends Action {
|
2021-01-14 13:42:30 +00:00
|
|
|
readonly continuation: Task<void>;
|
2021-01-11 22:35:36 +00:00
|
|
|
|
2021-01-14 13:42:30 +00:00
|
|
|
constructor(continuation: Task<void>) {
|
2021-01-11 22:35:36 +00:00
|
|
|
super();
|
|
|
|
this.continuation = continuation;
|
|
|
|
}
|
|
|
|
|
2021-01-15 12:38:15 +00:00
|
|
|
perform(_ds: Dataspace, ac: Actor | null): void {
|
2021-01-11 22:35:36 +00:00
|
|
|
// debug('DeferredTurn', ac && ac.toString());
|
2021-01-15 12:38:15 +00:00
|
|
|
ac!.scheduleTask(this.continuation);
|
2021-01-11 22:35:36 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-19 14:13:42 +00:00
|
|
|
class Activation extends Action {
|
|
|
|
readonly script: ActivationScript;
|
|
|
|
readonly name: any;
|
|
|
|
|
|
|
|
constructor(script: ActivationScript, name: any) {
|
|
|
|
super();
|
|
|
|
this.script = script;
|
|
|
|
this.name = name;
|
|
|
|
}
|
|
|
|
|
|
|
|
perform(ds: Dataspace, ac: Actor | null): void {
|
|
|
|
if (ds.activations.has(this.script)) return;
|
|
|
|
ds.activations.add(this.script);
|
|
|
|
ds.addActor(this.name, rootFacet => rootFacet.addStartScript(this.script), new Set(), ac);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-11 22:35:36 +00:00
|
|
|
export class Turn {
|
|
|
|
readonly actor: Actor | null;
|
|
|
|
readonly actions: Array<Action>;
|
|
|
|
|
|
|
|
constructor(actor: Actor | null, actions: Array<Action> = []) {
|
|
|
|
this.actor = actor;
|
|
|
|
this.actions = actions;
|
|
|
|
}
|
|
|
|
|
|
|
|
enqueueScriptAction(a: Action) {
|
|
|
|
this.actions.push(a);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export class Facet {
|
|
|
|
readonly id: FacetId;
|
|
|
|
isLive = true;
|
|
|
|
readonly actor: Actor;
|
|
|
|
readonly parent: Facet | null;
|
|
|
|
readonly endpoints = new IdentityMap<EndpointId, Endpoint>();
|
2021-01-14 13:42:30 +00:00
|
|
|
readonly stopScripts: Array<Script<void>> = [];
|
2021-01-11 22:35:36 +00:00
|
|
|
readonly children = new IdentitySet<Facet>();
|
|
|
|
readonly fields: any;
|
2021-01-15 12:38:15 +00:00
|
|
|
inScript = true;
|
2021-01-11 22:35:36 +00:00
|
|
|
|
|
|
|
constructor(actor: Actor, parent: Facet | null) {
|
|
|
|
this.id = actor.dataspace.nextId++;
|
|
|
|
this.actor = actor;
|
|
|
|
this.parent = parent;
|
|
|
|
if (parent) {
|
|
|
|
parent.children.add(this);
|
|
|
|
this.fields = Dataflow.Graph.newScope(parent.fields);
|
|
|
|
} else {
|
|
|
|
if (actor.rootFacet) {
|
|
|
|
throw new Error("INVARIANT VIOLATED: Attempt to add second root facet");
|
|
|
|
}
|
|
|
|
actor.rootFacet = this;
|
|
|
|
this.fields = Dataflow.Graph.newScope({});
|
|
|
|
}
|
|
|
|
this.fields[DataflowObservableObjectId] = () => this.id;
|
|
|
|
}
|
|
|
|
|
2021-01-15 12:38:15 +00:00
|
|
|
withNonScriptContext<T>(task: Task<T>): T {
|
|
|
|
let savedInScript = this.inScript;
|
|
|
|
this.inScript = false;
|
|
|
|
try {
|
|
|
|
return task();
|
|
|
|
} finally {
|
|
|
|
this.inScript = savedInScript;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-11 22:35:36 +00:00
|
|
|
_abort(emitPatches: boolean) {
|
|
|
|
this.isLive = false;
|
|
|
|
this.children.forEach(child => child._abort(emitPatches));
|
|
|
|
this.retractAssertionsAndSubscriptions(emitPatches);
|
|
|
|
}
|
|
|
|
|
|
|
|
retractAssertionsAndSubscriptions(emitPatches: boolean) {
|
2021-01-14 13:42:30 +00:00
|
|
|
this.actor.scheduleTask(() => {
|
2021-01-11 22:35:36 +00:00
|
|
|
this.endpoints.forEach((ep) => ep.destroy(emitPatches));
|
|
|
|
this.endpoints.clear();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
isInert(): boolean {
|
|
|
|
return this.endpoints.size === 0 && this.children.size === 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
_terminate() {
|
|
|
|
if (!this.isLive) return;
|
|
|
|
|
|
|
|
let ac = this.actor;
|
|
|
|
let parent = this.parent;
|
|
|
|
if (parent) {
|
|
|
|
parent.children.delete(this);
|
|
|
|
} else {
|
|
|
|
ac.rootFacet = null;
|
|
|
|
}
|
|
|
|
this.isLive = false;
|
|
|
|
|
|
|
|
this.children.forEach((child) => { child._terminate(); });
|
|
|
|
|
|
|
|
// Run stop-scripts after terminating children. This means
|
|
|
|
// that children's stop-scripts run before ours.
|
2021-01-14 13:42:30 +00:00
|
|
|
ac.scheduleTask(() =>
|
|
|
|
this.invokeScript(() =>
|
|
|
|
this.stopScripts.forEach(s =>
|
|
|
|
s.call(this.fields, this))));
|
2021-01-11 22:35:36 +00:00
|
|
|
|
|
|
|
this.retractAssertionsAndSubscriptions(true);
|
2021-01-14 13:42:30 +00:00
|
|
|
ac.scheduleTask(() => {
|
2021-01-11 22:35:36 +00:00
|
|
|
if (parent) {
|
|
|
|
if (parent.isInert()) {
|
|
|
|
parent._terminate();
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
ac._terminate(true);
|
|
|
|
}
|
|
|
|
}, Priority.GC);
|
|
|
|
}
|
|
|
|
|
2021-01-15 12:38:15 +00:00
|
|
|
// This alias exists because of the naive expansion done by the parser.
|
|
|
|
_stop(continuation?: Script<void>) {
|
|
|
|
this.stop(continuation);
|
|
|
|
}
|
|
|
|
|
2021-01-14 13:42:30 +00:00
|
|
|
stop(continuation?: Script<void>) {
|
2021-01-15 12:38:15 +00:00
|
|
|
this.parent!.invokeScript(() => {
|
2021-01-14 13:42:30 +00:00
|
|
|
this.actor.scheduleTask(() => {
|
2021-01-11 22:35:36 +00:00
|
|
|
this._terminate();
|
|
|
|
if (continuation) {
|
2021-01-15 12:38:15 +00:00
|
|
|
this.parent!.scheduleScript(parent => continuation.call(this.fields, parent));
|
2021-01-11 22:35:36 +00:00
|
|
|
// ^ TODO: is this the correct scope to use??
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2021-01-14 13:42:30 +00:00
|
|
|
addStartScript(s: Script<void>) {
|
|
|
|
this.ensureFacetSetup('`on start`');
|
|
|
|
this.scheduleScript(s);
|
2021-01-11 22:35:36 +00:00
|
|
|
}
|
|
|
|
|
2021-01-14 13:42:30 +00:00
|
|
|
addStopScript(s: Script<void>) {
|
|
|
|
this.ensureFacetSetup('`on stop`');
|
2021-01-11 22:35:36 +00:00
|
|
|
this.stopScripts.push(s);
|
|
|
|
}
|
|
|
|
|
2021-01-15 12:38:15 +00:00
|
|
|
addEndpoint(updateFun: Script<EndpointSpec>, isDynamic: boolean = true): Endpoint {
|
2021-01-11 22:35:36 +00:00
|
|
|
const ep = new Endpoint(this, isDynamic, updateFun);
|
|
|
|
this.actor.dataspace.endpointHook(this, ep);
|
|
|
|
return ep;
|
|
|
|
}
|
|
|
|
|
2021-01-14 13:42:30 +00:00
|
|
|
_addRawObserverEndpoint(specScript: Script<MaybeValue>, callbacks: ObserverCallbacks): Endpoint
|
2021-01-11 22:35:36 +00:00
|
|
|
{
|
|
|
|
return this.addEndpoint(() => {
|
2021-01-14 13:42:30 +00:00
|
|
|
const spec = specScript(this);
|
2021-01-11 22:35:36 +00:00
|
|
|
if (spec === void 0) {
|
|
|
|
return { assertion: void 0, analysis: null };
|
|
|
|
} else {
|
|
|
|
const analysis = Skeleton.analyzeAssertion(spec);
|
2021-01-14 13:42:30 +00:00
|
|
|
analysis.callback = this.wrap((facet, evt, vs) => {
|
2021-01-11 22:35:36 +00:00
|
|
|
switch (evt) {
|
2021-01-14 13:42:30 +00:00
|
|
|
case Skeleton.EventType.ADDED: callbacks.add?.(facet, vs); break;
|
|
|
|
case Skeleton.EventType.REMOVED: callbacks.del?.(facet, vs); break;
|
|
|
|
case Skeleton.EventType.MESSAGE: callbacks.msg?.(facet, vs); break;
|
2021-01-11 22:35:36 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
return { assertion: Observe(spec), analysis };
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2021-01-14 13:42:30 +00:00
|
|
|
addObserverEndpoint(specThunk: (facet: Facet) => MaybeValue, callbacks: ObserverCallbacks): Endpoint {
|
2021-01-11 22:35:36 +00:00
|
|
|
const scriptify = (f?: ObserverCallback) =>
|
2021-01-14 13:42:30 +00:00
|
|
|
f && ((facet: Facet, vs: Array<Value>) =>
|
|
|
|
facet.scheduleScript(() => f.call(facet.fields, facet, vs)));
|
2021-01-11 22:35:36 +00:00
|
|
|
return this._addRawObserverEndpoint(specThunk, {
|
|
|
|
add: scriptify(callbacks.add),
|
|
|
|
del: scriptify(callbacks.del),
|
|
|
|
msg: scriptify(callbacks.msg),
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2021-01-14 13:42:30 +00:00
|
|
|
addDataflow(subjectFun: Script<void>, priority?: Priority): Endpoint {
|
2021-01-11 22:35:36 +00:00
|
|
|
return this.addEndpoint(() => {
|
|
|
|
let subjectId = this.actor.dataspace.dataflow.currentSubjectId;
|
2021-01-14 13:42:30 +00:00
|
|
|
this.scheduleScript(() => {
|
2021-01-11 22:35:36 +00:00
|
|
|
if (this.isLive) {
|
|
|
|
this.actor.dataspace.dataflow.withSubject(subjectId, () =>
|
2021-01-15 12:38:15 +00:00
|
|
|
subjectFun.call(this.fields, this));
|
2021-01-11 22:35:36 +00:00
|
|
|
}
|
|
|
|
}, priority);
|
|
|
|
return { assertion: void 0, analysis: null };
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
enqueueScriptAction(action: Action) {
|
|
|
|
this.actor.enqueueScriptAction(action);
|
|
|
|
}
|
|
|
|
|
|
|
|
toString(): string {
|
|
|
|
let s = 'Facet(' + this.actor.id;
|
|
|
|
if (this.actor.name !== void 0 && this.actor.name !== null) {
|
|
|
|
s = s + ',' + this.actor.name.toString();
|
|
|
|
}
|
|
|
|
s = s + ',' + this.id;
|
|
|
|
let f = this.parent;
|
|
|
|
while (f != null) {
|
|
|
|
s = s + ':' + f.id;
|
|
|
|
f = f.parent;
|
|
|
|
}
|
|
|
|
return s + ')';
|
|
|
|
}
|
2021-01-14 13:42:30 +00:00
|
|
|
|
|
|
|
invokeScript<T>(script: Script<T>, propagateErrors = false): T | undefined {
|
|
|
|
try {
|
|
|
|
// console.group('Facet', facet && facet.toString());
|
|
|
|
return script.call(this.fields, this);
|
|
|
|
} catch (e) {
|
|
|
|
let a = this.actor;
|
|
|
|
a.abandonQueuedWork();
|
|
|
|
a._terminate(false);
|
|
|
|
console.error('Actor ' + a.toString() + ' exited with exception:', e);
|
|
|
|
if (propagateErrors) throw e;
|
|
|
|
return undefined;
|
|
|
|
} finally {
|
|
|
|
// console.groupEnd();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
wrap<T extends Array<any>, R>(fn: (f: Facet, ... args: T) => R): (... args: T) => R {
|
2021-01-15 12:38:15 +00:00
|
|
|
return (... actuals) => this.invokeScript(f => fn.call(f.fields, f, ... actuals), true)!;
|
2021-01-14 13:42:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
wrapExternal<T extends Array<any>>(fn: (f: Facet, ... args: T) => void): (... args: T) => void {
|
|
|
|
const ac = this.actor;
|
|
|
|
return (... actuals) => {
|
|
|
|
if (this.isLive) {
|
|
|
|
ac.dataspace.start();
|
|
|
|
ac.scheduleTask(() => this.invokeScript(f => fn.call(f.fields, f, ... actuals)));
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
ensureFacetSetup(what: string) {
|
2021-01-15 12:38:15 +00:00
|
|
|
if (this.inScript) {
|
2021-01-14 13:42:30 +00:00
|
|
|
throw new Error(`Cannot ${what} outside facet setup; are you missing \`react { ... }\`?`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-19 14:13:42 +00:00
|
|
|
ensureNonFacetSetup(what: string) {
|
2021-01-15 12:38:15 +00:00
|
|
|
if (!this.inScript) {
|
2021-01-19 14:13:42 +00:00
|
|
|
throw new Error(`Cannot ${what} during facet setup; are you missing \`on start { ... }\`?`);
|
2021-01-14 13:42:30 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-15 12:38:15 +00:00
|
|
|
// This alias exists because of the naive expansion done by the parser.
|
|
|
|
_send(body: any) {
|
|
|
|
this.send(body);
|
|
|
|
}
|
|
|
|
|
2021-01-14 13:42:30 +00:00
|
|
|
send(body: any) {
|
2021-01-19 14:13:42 +00:00
|
|
|
this.ensureNonFacetSetup('`send`');
|
2021-01-14 13:42:30 +00:00
|
|
|
this.enqueueScriptAction(new Message(body));
|
|
|
|
}
|
|
|
|
|
2021-01-15 12:38:15 +00:00
|
|
|
// This alias exists because of the naive expansion done by the parser.
|
|
|
|
_spawn(name: any, bootProc: Script<void>, initialAssertions?: Set) {
|
|
|
|
this.spawn(name, bootProc, initialAssertions);
|
|
|
|
}
|
|
|
|
|
2021-01-14 13:42:30 +00:00
|
|
|
spawn(name: any, bootProc: Script<void>, initialAssertions?: Set) {
|
2021-01-19 14:13:42 +00:00
|
|
|
this.ensureNonFacetSetup('`spawn`');
|
2021-01-14 13:42:30 +00:00
|
|
|
this.enqueueScriptAction(new Spawn(name, bootProc, initialAssertions));
|
|
|
|
}
|
|
|
|
|
|
|
|
deferTurn(continuation: Script<void>) {
|
2021-01-19 14:13:42 +00:00
|
|
|
this.ensureNonFacetSetup('`deferTurn`');
|
2021-01-14 13:42:30 +00:00
|
|
|
this.enqueueScriptAction(new DeferredTurn(this.wrap(continuation)));
|
|
|
|
}
|
|
|
|
|
2021-01-19 14:13:42 +00:00
|
|
|
activate(script: ActivationScript, name?: any) {
|
|
|
|
this.ensureNonFacetSetup('`activate`');
|
|
|
|
this.enqueueScriptAction(new Activation(script, name ?? null));
|
|
|
|
}
|
|
|
|
|
2021-01-14 13:42:30 +00:00
|
|
|
scheduleScript(script: Script<void>, priority?: Priority) {
|
|
|
|
this.actor.scheduleTask(this.wrap(script), priority);
|
|
|
|
}
|
2021-01-15 12:38:15 +00:00
|
|
|
|
|
|
|
declareField<T extends DataflowObservableObject, K extends keyof T & string>(obj: T, prop: K, init: T[K]) {
|
|
|
|
if (prop in obj) {
|
|
|
|
obj[prop] = init;
|
|
|
|
} else {
|
|
|
|
this.actor.dataspace.dataflow.defineObservableProperty(obj, prop, init, {
|
|
|
|
objectId: [obj, prop],
|
|
|
|
noopGuard: is
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// referenceField(obj: DataflowObservableObject, prop: string) {
|
|
|
|
// if (!(prop in obj)) {
|
|
|
|
// this.actor.dataspace.dataflow.recordObservation([obj, prop]);
|
|
|
|
// }
|
|
|
|
// return obj[prop];
|
|
|
|
// }
|
|
|
|
|
|
|
|
// deleteField(obj: DataflowObservableObject, prop: string) {
|
|
|
|
// this.actor.dataspace.dataflow.recordDamage([obj, prop]);
|
|
|
|
// delete obj[prop];
|
|
|
|
// }
|
|
|
|
|
|
|
|
addChildFacet(bootProc: Script<void>) {
|
|
|
|
this.actor.addFacet(this, bootProc, true);
|
|
|
|
}
|
2021-01-16 16:46:18 +00:00
|
|
|
|
|
|
|
withSelfDo(t: Script<void>) {
|
|
|
|
t(this);
|
|
|
|
}
|
2021-01-11 22:35:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
export class Endpoint {
|
|
|
|
readonly id: EndpointId;
|
|
|
|
readonly facet: Facet;
|
2021-01-14 13:42:30 +00:00
|
|
|
readonly updateFun: Script<EndpointSpec>;
|
2021-01-11 22:35:36 +00:00
|
|
|
spec: EndpointSpec;
|
|
|
|
|
2021-01-14 13:42:30 +00:00
|
|
|
constructor(facet: Facet, isDynamic: boolean, updateFun: Script<EndpointSpec>) {
|
|
|
|
facet.ensureFacetSetup('add endpoint');
|
2021-01-11 22:35:36 +00:00
|
|
|
let ac = facet.actor;
|
|
|
|
let ds = ac.dataspace;
|
|
|
|
this.id = ds.nextId++;
|
|
|
|
this.facet = facet;
|
|
|
|
this.updateFun = updateFun;
|
|
|
|
let initialSpec = ds.dataflow.withSubject(isDynamic ? this : undefined,
|
2021-01-14 13:42:30 +00:00
|
|
|
() => updateFun.call(facet.fields, facet));
|
2021-01-11 22:35:36 +00:00
|
|
|
this._install(initialSpec);
|
2021-01-15 12:38:15 +00:00
|
|
|
this.spec = initialSpec; // keeps TypeScript's undefinedness-checker happy
|
2021-01-11 22:35:36 +00:00
|
|
|
facet.endpoints.set(this.id, this);
|
|
|
|
}
|
|
|
|
|
|
|
|
_install(spec: EndpointSpec) {
|
|
|
|
this.spec = spec;
|
|
|
|
const ac = this.facet.actor;
|
|
|
|
if (this.spec.assertion !== void 0) {
|
|
|
|
ac.assert(this.spec.assertion);
|
|
|
|
}
|
|
|
|
if (this.spec.analysis) ac.dataspace.subscribe(this.spec.analysis);
|
|
|
|
}
|
|
|
|
|
|
|
|
_uninstall(emitPatches: boolean) {
|
|
|
|
if (emitPatches) {
|
|
|
|
if (this.spec.assertion !== void 0) {
|
|
|
|
this.facet.actor.retract(this.spec.assertion);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (this.spec.analysis) this.facet.actor.dataspace.unsubscribe(this.spec.analysis);
|
|
|
|
}
|
|
|
|
|
|
|
|
refresh() {
|
2021-01-14 13:42:30 +00:00
|
|
|
let newSpec = this.updateFun.call(this.facet.fields, this.facet);
|
2021-01-11 22:35:36 +00:00
|
|
|
if (newSpec.assertion !== void 0) newSpec.assertion = fromJS(newSpec.assertion);
|
2021-01-12 13:34:33 +00:00
|
|
|
if (!is(newSpec.assertion, this.spec.assertion)) {
|
2021-01-11 22:35:36 +00:00
|
|
|
this._uninstall(true);
|
|
|
|
this._install(newSpec);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
destroy(emitPatches: boolean) {
|
|
|
|
const facet = this.facet;
|
|
|
|
facet.actor.dataspace.dataflow.forgetSubject(this);
|
|
|
|
// ^ TODO: this won't work because of object identity problems! Why
|
|
|
|
// does the Racket implementation do this, when the old JS
|
|
|
|
// implementation doesn't?
|
|
|
|
facet.endpoints.delete(this.id);
|
|
|
|
this._uninstall(emitPatches);
|
|
|
|
}
|
|
|
|
|
|
|
|
toString(): string {
|
|
|
|
return 'Endpoint(' + this.id + ')';
|
|
|
|
}
|
|
|
|
}
|