@empirica/core
Version:
Empirica Core
1,048 lines (1,040 loc) • 28.1 kB
JavaScript
import {
AdminConnection,
Attributes,
EventContext,
Flusher,
ListenersCollector,
Scopes,
Subscriptions,
participantsSub,
promiseHandle,
transitionsSub
} from "./chunk-ATDZK33U.js";
import {
TajribaConnection,
awaitObsValue,
bsu,
lockedAsyncSubscribe,
subscribeAsync
} from "./chunk-WGYNSNUC.js";
import {
Globals
} from "./chunk-2TT3WJZY.js";
import {
debug,
error,
warn
} from "./chunk-TIKLWCJI.js";
// src/admin/globals.ts
var Globals2 = class extends Globals {
constructor(globals, globalScopeID, setAttributes) {
super(globals);
this.globalScopeID = globalScopeID;
this.setAttributes = setAttributes;
}
set(key, value, ao) {
let attr = this.attrs.get(key);
if (!attr) {
attr = bsu();
this.attrs.set(key, attr);
}
attr.next(value);
const attrProps = {
key,
nodeID: this.globalScopeID,
val: JSON.stringify(value)
};
if (ao) {
attrProps.private = ao.private;
attrProps.protected = ao.protected;
attrProps.immutable = ao.immutable;
attrProps.ephemeral = ao.ephemeral;
attrProps.append = ao.append;
attrProps.index = ao.index;
}
this.setAttributes([attrProps]);
}
};
// src/admin/context.ts
import { merge as merge2, Subject as Subject2 } from "rxjs";
// src/admin/runloop.ts
import {
BehaviorSubject,
ReplaySubject,
Subject
} from "rxjs";
// src/admin/cake.ts
import { Mutex } from "async-mutex";
var Cake = class {
constructor(evtctx, scope, kindSubscription, attributeSubscription, connections, transitions) {
this.evtctx = evtctx;
this.scope = scope;
this.kindSubscription = kindSubscription;
this.attributeSubscription = attributeSubscription;
this.connections = connections;
this.transitions = transitions;
this.stopped = false;
this.unsubs = [];
this.mutex = new Mutex();
this.kindListeners = /* @__PURE__ */ new Map();
this.kindLast = /* @__PURE__ */ new Map();
this.prevAttr = promiseHandle();
this.attributeListeners = /* @__PURE__ */ new Map();
this.attributeLast = /* @__PURE__ */ new Map();
this.transitionEvents = [];
this.connectedEvents = [];
this.connectionsMap = /* @__PURE__ */ new Map();
this.disconnectedEvents = [];
}
async stop() {
this.stopped = true;
for (const unsub of this.unsubs) {
unsub.unsubscribe();
}
}
async add(listeners) {
listeners.setFlusher(new Flusher(this.postCallback));
for (const start of listeners.starts) {
debug("start callback");
try {
await start.callback(this.evtctx);
} catch (err) {
prettyPrintError("start", err);
}
if (this.postCallback) {
await this.postCallback();
}
}
if (listeners.kindListeners.length > 0) {
const kindListeners = /* @__PURE__ */ new Map();
for (const listener of listeners.kindListeners) {
const callbacks = kindListeners.get(listener.kind) || [];
callbacks.push(listener);
callbacks.sort(comparePlacement);
kindListeners.set(listener.kind, callbacks);
}
for (const [kind, listeners2] of kindListeners) {
let kl = this.kindListeners.get(kind) || [];
if (this.kindListeners.has(kind)) {
const until = this.kindLast.get(kind);
if (until) {
await this.startKind(kind, () => listeners2, until);
}
kl.push(...listeners2);
kl.sort(comparePlacement);
this.kindListeners.set(kind, kl);
} else {
this.kindListeners.set(kind, listeners2);
await this.startKind(kind, () => this.kindListeners.get(kind) || []);
}
}
}
if (listeners.attributeListeners.length > 0) {
const attributeListeners = /* @__PURE__ */ new Map();
for (const listener of listeners.attributeListeners) {
const key = listener.kind + "-" + listener.key;
const callbacks = attributeListeners.get(key) || [];
callbacks.push(listener);
callbacks.sort(comparePlacement);
attributeListeners.set(key, callbacks);
}
for (const [kkey, listeners2] of attributeListeners) {
const kind = listeners2[0].kind;
const key = listeners2[0].key;
let kl = this.attributeListeners.get(kkey) || [];
if (this.attributeListeners.has(kkey)) {
const until = this.attributeLast.get(kkey);
if (until) {
await this.startAttribute(kind, key, () => listeners2, until);
}
kl.push(...listeners2);
kl.sort(comparePlacement);
this.attributeListeners.set(kkey, kl);
} else {
this.attributeListeners.set(kkey, listeners2);
await this.startAttribute(
kind,
key,
() => this.attributeListeners.get(kkey) || []
);
}
}
}
for (const listener of listeners.tajEvents) {
switch (listener.event) {
case "TRANSITION_ADD" /* TransitionAdd */: {
if (this.transitionEvents.length == 0) {
this.startTransitionAdd();
}
this.transitionEvents.push(listener);
this.transitionEvents.sort(comparePlacement);
break;
}
case "PARTICIPANT_CONNECT" /* ParticipantConnect */: {
if (this.connectedEvents.length == 0) {
this.startConnected();
}
for (const [_, conn] of this.connectionsMap) {
try {
await listener.callback(this.evtctx, {
participant: conn.participant
});
} catch (err) {
prettyPrintError("participant connect", err);
}
if (this.postCallback) {
await this.postCallback();
}
}
this.connectedEvents.push(listener);
this.connectedEvents.sort(comparePlacement);
break;
}
case "PARTICIPANT_DISCONNECT" /* ParticipantDisconnect */: {
if (this.disconnectedEvents.length == 0) {
this.startDisconnected();
}
this.disconnectedEvents.push(listener);
this.disconnectedEvents.sort(comparePlacement);
break;
}
default: {
error(`unsupported tajriba event listener: ${listener.event}`);
}
}
}
for (const ready of listeners.readys) {
debug("ready callback");
try {
await ready.callback(this.evtctx);
} catch (err) {
prettyPrintError("ready", err);
}
}
}
async startKind(kind, callbacks, until) {
let handle = promiseHandle();
const unsub = lockedAsyncSubscribe(
this.mutex,
this.kindSubscription(kind),
async ({ scope, done }) => {
if (this.stopped) {
if (handle) {
handle.result();
}
return;
}
if (scope) {
for (const callback of callbacks()) {
try {
await callback.callback(this.evtctx, { [kind]: scope });
} catch (err) {
prettyPrintError(kind, err);
}
if (this.postCallback) {
await this.postCallback();
}
}
if (until) {
if (scope === until) {
if (handle) {
handle.result();
handle = void 0;
} else {
warn(`until kind without handle`);
}
}
} else {
this.kindLast.set(kind, scope);
}
}
if (!until && done && handle) {
handle.result();
handle = void 0;
}
}
);
if (handle) {
await handle.promise;
}
if (until) {
unsub.unsubscribe();
} else {
this.unsubs.push(unsub);
}
}
async startAttribute(kind, key, callbacks, until) {
let handle = promiseHandle();
const unsub = lockedAsyncSubscribe(
this.mutex,
this.attributeSubscription(kind, key),
async ({ attribute, done }) => {
if (this.stopped) {
if (handle) {
handle.result();
}
return;
}
if (attribute) {
let next = promiseHandle();
if (this.prevAttr) {
const p = this.prevAttr;
this.prevAttr = next;
await p;
} else {
this.prevAttr = next;
}
const k = kind + "-" + key;
const props = {
[key]: attribute.value,
attribute,
attrId: attribute.id
};
if (attribute.nodeID) {
const scope = this.scope(attribute.nodeID);
if (scope) {
props[kind] = scope;
}
}
for (const callback of callbacks()) {
try {
await callback.callback(this.evtctx, props);
} catch (err) {
prettyPrintError(`${kind}.${key}`, err);
}
if (this.stopped) {
return;
}
if (this.postCallback) {
await this.postCallback();
}
if (this.stopped) {
return;
}
}
if (until) {
if (attribute === until) {
if (handle) {
handle.result();
handle = void 0;
} else {
warn(`until attribute without handle`);
}
}
} else {
this.attributeLast.set(k, attribute);
}
}
if (!until && done && handle) {
handle.result();
handle = void 0;
}
}
);
if (handle) {
await handle.promise;
}
if (until) {
unsub.unsubscribe();
} else {
this.unsubs.push(unsub);
}
}
startTransitionAdd() {
const unsub = lockedAsyncSubscribe(
this.mutex,
this.transitions,
async (transition) => {
for (const callback of this.transitionEvents) {
if (this.stopped) {
return;
}
debug(
`transition callback from '${transition.from}' to '${transition.to}'`
);
try {
await callback.callback(this.evtctx, {
transition,
step: transition.step
});
} catch (err) {
prettyPrintError("transition", err);
}
if (this.postCallback) {
await this.postCallback();
}
}
}
);
this.unsubs.push(unsub);
}
async startConnected() {
let handle = promiseHandle();
const unsub = lockedAsyncSubscribe(
this.mutex,
this.connections,
async ({ connection, done }) => {
if (this.stopped) {
if (handle) {
handle.result();
}
return;
}
if (connection) {
if (!connection.connected) {
return;
}
this.connectionsMap.set(connection.participant.id, connection);
for (const callback of this.connectedEvents) {
debug(`connected callback`);
try {
await callback.callback(this.evtctx, {
participant: connection.participant
});
} catch (err) {
prettyPrintError("participant connect", err);
}
if (this.postCallback) {
await this.postCallback();
}
}
}
if (done && handle) {
handle.result();
handle = void 0;
}
}
);
if (handle) {
await handle.promise;
}
this.unsubs.push(unsub);
}
startDisconnected() {
const unsub = lockedAsyncSubscribe(
this.mutex,
this.connections,
async ({ connection }) => {
if (this.stopped) {
return;
}
if (!connection || connection.connected) {
return;
}
this.connectionsMap.delete(connection.participant.id);
for (const callback of this.disconnectedEvents) {
debug(`disconnected callback`);
try {
await callback.callback(this.evtctx, {
participant: connection.participant
});
} catch (err) {
prettyPrintError("participant disconnect", err);
}
if (this.postCallback) {
await this.postCallback();
}
}
}
);
this.unsubs.push(unsub);
}
};
var comparePlacement = (a, b) => a.placement - b.placement;
function prettyPrintError(location, err) {
error(`Error caught in "${location}" callback:`);
error(err);
}
// src/admin/runloop.ts
var Runloop = class {
constructor(conn, ctx, kinds, globalScopeID, subs, stop) {
this.conn = conn;
this.ctx = ctx;
this.kinds = kinds;
this.subs = new Subscriptions();
this.participants = /* @__PURE__ */ new Map();
this.connections = new ReplaySubject();
this.transitions = new Subject();
this.scopesSub = new Subject();
this.attributesSub = new Subject();
this.donesSub = new Subject();
this.finalizers = [];
this.groupPromises = [];
this.stepPromises = [];
this.scopePromises = [];
this.linkPromises = [];
this.transitionPromises = [];
this.attributeInputs = [];
this.running = new BehaviorSubject(false);
this.stopped = false;
this.attributes = new Attributes(
this.attributesSub,
this.donesSub,
this.setAttributes.bind(this)
);
const mut = new TajribaAdminAccess(
this.addFinalizer.bind(this),
this.addScopes.bind(this),
this.addGroups.bind(this),
this.addLinks.bind(this),
this.addSteps.bind(this),
this.addTransitions.bind(this),
new Globals2(
this.taj.globalAttributes(),
globalScopeID,
this.setAttributes.bind(this)
)
);
this.scopes = new Scopes(
this.scopesSub,
this.donesSub,
this.ctx,
this.kinds,
this.attributes,
mut
);
this.evtctx = new EventContext(
this.subs,
mut,
this.scopes,
new Flusher(this.postCallback.bind(this, true))
);
this.cake = new Cake(
this.evtctx,
this.scopes.scope.bind(this.scopes),
this.scopes.subscribeKind.bind(this.scopes),
(kind, key) => this.attributes.subscribeAttribute(kind, key),
this.connections,
this.transitions
);
this.cake.postCallback = this.postCallback.bind(this, true);
const subsSub = subscribeAsync(subs, async (subscriber) => {
let listeners;
if (typeof subscriber === "function") {
listeners = new ListenersCollector();
subscriber(listeners);
} else {
listeners = subscriber;
}
await this.cake.add(listeners);
});
let stopSub;
stopSub = stop.subscribe({
next: () => {
subsSub.unsubscribe();
stopSub.unsubscribe();
}
});
}
/**
* @internal
*
* NOTE: For testing purposes only.
*/
get _attributes() {
return this.attributes;
}
/**
* @internal
*
* NOTE: For testing purposes only.
*/
get _scopes() {
return this.scopes;
}
/**
* @internal
*
* NOTE: For testing purposes only.
*/
async _postCallback() {
return await this.postCallback(true);
}
async postCallback(final) {
if (this.stopped) {
return;
}
this.running.next(true);
const promises = [];
const subs = this.subs.newSubs();
if (subs) {
promises.push(this.processNewSub(subs));
}
promises.push(...this.groupPromises);
this.groupPromises = [];
promises.push(...this.stepPromises);
this.stepPromises = [];
promises.push(...this.scopePromises);
this.scopePromises = [];
promises.push(...this.linkPromises);
this.linkPromises = [];
promises.push(...this.transitionPromises);
this.transitionPromises = [];
if (this.attributeInputs.length > 0) {
const uniqueAttrs = {};
let appendCount = 0;
for (const attr of this.attributeInputs) {
if (!attr.nodeID) {
error(`runloop: attribute without nodeID: ${JSON.stringify(attr)}`);
continue;
}
let key = `${attr.nodeID}-${attr.key}`;
if (attr.append) {
key += `-${appendCount++}`;
}
if (attr.index !== void 0) {
key += `-${attr.index}`;
}
uniqueAttrs[key] = attr;
}
const attrs = Object.values(uniqueAttrs);
promises.push(this.taj.setAttributes(attrs));
this.attributeInputs = [];
}
const res = await Promise.allSettled(promises);
for (const r of res) {
if (r.status === "rejected") {
if (r.reason instanceof String && r.reason.includes("invalid transition")) {
continue;
}
warn(`failed load: ${r.reason}`);
}
}
const finalizer = this.finalizers.shift();
if (finalizer) {
await finalizer();
await this.postCallback(false);
}
if (final) {
this.running.next(false);
}
}
async stop() {
await this.cake.stop();
await awaitObsValue(this.running, false);
this.stopped = true;
}
addFinalizer(cb) {
this.finalizers.push(cb);
}
async addScopes(inputs) {
if (this.stopped) {
return [];
}
const addScopes = this.taj.addScopes(inputs).then((scopes) => {
for (const scope of scopes) {
for (const attrEdge of scope.attributes.edges) {
this.attributesSub.next({
attribute: attrEdge.node,
removed: false
});
}
this.scopesSub.next({
scope,
removed: false
});
}
this.donesSub.next(scopes.map((s) => s.id));
return scopes;
}).catch((err) => {
warn(err.message);
return [];
});
this.scopePromises.push(addScopes);
return addScopes;
}
async addGroups(inputs) {
if (this.stopped) {
return [];
}
const addGroups = this.taj.addGroups(inputs);
this.groupPromises.push(addGroups);
return addGroups;
}
async addLinks(inputs) {
if (this.stopped) {
return [];
}
const proms = [];
for (const input of inputs) {
const linkPromise = this.taj.addLink(input);
this.linkPromises.push(linkPromise);
proms.push(linkPromise);
}
return Promise.all(proms);
}
async addSteps(inputs) {
if (this.stopped) {
return [];
}
const addSteps = this.taj.addSteps(inputs);
this.stepPromises.push(addSteps);
return addSteps;
}
async addTransitions(inputs) {
if (this.stopped) {
return [];
}
const proms = [];
for (const input of inputs) {
const transitionPromise = this.taj.transition(input);
this.transitionPromises.push(transitionPromise);
proms.push(transitionPromise);
}
return Promise.all(proms);
}
async setAttributes(inputs) {
this.attributeInputs.push(...inputs);
}
// TODO ADD iteration attributes per scope, only first 100...
loadAllScopes(filters, after) {
this.taj.scopes({ filter: filters, first: 100, after }).then((conn) => {
const scopes = {};
for (const edge of conn?.edges || []) {
for (const attrEdge of edge.node.attributes.edges || []) {
this.attributesSub.next({
attribute: attrEdge.node,
removed: false
});
}
scopes[edge.node.id] = edge.node;
}
for (const scope of Object.values(scopes)) {
this.scopesSub.next({
scope,
removed: false
});
}
if (conn?.pageInfo.hasNextPage && conn?.pageInfo.endCursor) {
return this.loadAllScopes(filters, conn?.pageInfo.endCursor);
}
});
}
async processNewScopesSub(filters) {
if (filters.length === 0) {
return;
}
let resolve;
const prom = new Promise((r) => resolve = r);
this.taj.scopedAttributes(filters).subscribe({
next: ({ attribute, scopesUpdated, done }) => {
if (attribute) {
if (attribute.node.__typename !== "Scope") {
error(`scoped attribute with non-scope node`);
return;
}
this.attributesSub.next({
attribute,
removed: false
});
this.scopesSub.next({
scope: attribute.node,
removed: false
});
}
if (done) {
resolve();
if (!scopesUpdated) {
error(`scopesUpdated is empty`);
return;
}
this.donesSub.next(scopesUpdated);
}
}
});
await prom;
}
async processNewSub(subs) {
const filters = [];
if (subs.scopes.ids.length > 0) {
filters.push({ ids: subs.scopes.ids });
}
if (subs.scopes.kinds.length > 0) {
filters.push({ kinds: subs.scopes.kinds });
}
if (subs.scopes.names.length > 0) {
filters.push({ names: subs.scopes.names });
}
if (subs.scopes.keys.length > 0) {
filters.push({ keys: subs.scopes.keys });
}
if (subs.scopes.kvs.length > 0) {
filters.push({ kvs: subs.scopes.kvs });
}
if (subs.participants) {
await participantsSub(this.taj, this.connections, this.participants);
}
if (subs.transitions.length > 0) {
for (const id of subs.transitions) {
transitionsSub(this.taj, this.transitions, id);
}
}
await this.processNewScopesSub(filters);
}
get taj() {
return this.conn.admin.getValue();
}
};
// src/admin/token_file.ts
import fs from "fs/promises";
import path from "path";
import { BehaviorSubject as BehaviorSubject2, merge } from "rxjs";
var TokenProvider = class {
constructor(taj, storage, serviceName, serviceRegistrationToken) {
this.tokens = bsu(void 0);
let connected = false;
let token;
this.sub = subscribeAsync(
merge(taj.connected, storage.tokens),
async (tokenOrConnected) => {
if (typeof tokenOrConnected === "boolean") {
connected = tokenOrConnected;
} else {
token = tokenOrConnected;
}
if (token) {
this.tokens.next(token);
return;
}
if (!connected) {
return;
}
if (token === void 0) {
return;
}
try {
const t = await taj.tajriba.registerService(
serviceName,
serviceRegistrationToken
);
if (t) {
storage.updateToken(t);
}
} catch (err) {
error(`token: register service ${err.message}`);
return;
}
}
);
}
get token() {
return this.tokens.getValue();
}
// When stopped, cannot be restarted
stop() {
this.sub?.unsubscribe();
this.sub = void 0;
}
};
var MemTokenStorage = class {
constructor() {
this.tokens = new BehaviorSubject2(null);
}
async updateToken(token) {
this.tokens.next(token);
}
async clearToken() {
this.tokens.next(void 0);
}
};
var FileTokenStorage = class {
constructor(serviceTokenFile, resetToken) {
this.serviceTokenFile = serviceTokenFile;
this._tokens = bsu(null);
resetToken.subscribe({
next: () => {
this.clearToken();
}
});
}
static async init(serviceTokenFile, resetToken) {
const p = new this(serviceTokenFile, resetToken);
const token = await p.readToken();
if (token) {
p._tokens.next(token);
}
return p;
}
async readToken() {
try {
const data = await fs.readFile(this.serviceTokenFile, {
encoding: "utf8"
});
if (data.length > 0) {
return data;
}
} catch (err) {
const e = err;
if (e.code !== "ENOENT") {
error(`token: read token file ${e.message}`);
}
}
return;
}
async writeToken(token) {
try {
const dir = path.dirname(this.serviceTokenFile);
await fs.mkdir(dir, { recursive: true });
await fs.writeFile(this.serviceTokenFile, token);
} catch (err) {
error(`token: write token file ${err.message}`);
}
}
async deleteTokenFile() {
try {
await fs.unlink(this.serviceTokenFile);
} catch (err) {
error(`token: delete token file ${err.message}`);
}
}
get tokens() {
return this._tokens;
}
get token() {
return this._tokens.getValue();
}
async updateToken(token) {
if (token === this._tokens.getValue()) {
return;
}
this._tokens.next(token);
await this.writeToken(token);
}
async clearToken() {
await this.deleteTokenFile();
if (this.token) {
this._tokens.next(void 0);
}
}
};
// src/admin/context.ts
var AdminContext = class {
constructor(url, ctx, kinds) {
this.ctx = ctx;
this.kinds = kinds;
this.adminSubs = new Subject2();
this.adminStop = new Subject2();
this.subs = [];
this.tajriba = new TajribaConnection(url);
}
/**
* @internal
*
* NOTE: For testing purposes only.
*/
get _runloop() {
return this.runloop;
}
static async init(url, tokenFile, serviceName, serviceRegistrationToken, ctx, kinds) {
const adminContext = new this(url, ctx, kinds);
const reset = new Subject2();
let strg;
if (tokenFile === ":mem:") {
strg = new MemTokenStorage();
} else {
strg = await FileTokenStorage.init(tokenFile, reset);
}
const tp = new TokenProvider(
adminContext.tajriba,
strg,
serviceName,
serviceRegistrationToken
);
adminContext.adminConn = new AdminConnection(
adminContext.tajriba,
tp.tokens,
reset.next.bind(reset)
);
adminContext.sub = subscribeAsync(
merge2(adminContext.tajriba.connected, adminContext.adminConn.connected),
async () => {
await adminContext.initOrStop();
}
);
return adminContext;
}
async stop() {
this.sub?.unsubscribe();
delete this.sub;
await this.stopSubs();
this.tajriba.stop();
this.adminConn?.stop();
}
register(subscriber) {
this.subs.push(subscriber);
if (this.runloop) {
this.adminSubs.next(subscriber);
}
}
async initOrStop() {
if (this.tajriba.connected.getValue() && this.adminConn.connected.getValue()) {
await this.initSubs();
} else {
await this.stopSubs();
}
}
async initSubs() {
if (this.runloop) {
return;
}
if (!this.adminConn) {
warn("context: admin not connected");
return;
}
const tajAdmin = this.adminConn.admin.getValue();
if (!tajAdmin) {
warn("context: admin not connected");
return;
}
let globalScopeID;
try {
const scopes = await tajAdmin.scopes({
filter: { kinds: ["global"] },
first: 100
});
globalScopeID = scopes.edges[0]?.node.id;
if (!globalScopeID) {
warn("context: global scopeID not found");
return;
}
} catch (err) {
error(`context: global scopeID not fetched: ${err}`);
return;
}
this.runloop = new Runloop(
this.adminConn,
this.ctx,
this.kinds,
globalScopeID,
this.adminSubs,
this.adminStop
);
for (const sub of this.subs) {
this.adminSubs.next(sub);
}
}
async stopSubs() {
this.adminStop.next();
if (this.runloop) {
await this.runloop.stop();
this.runloop = void 0;
}
}
};
var TajribaAdminAccess = class {
constructor(addFinalizer, addScopes, addGroups, addLinks, addSteps, addTransitions, globals) {
this.addFinalizer = addFinalizer;
this.addScopes = addScopes;
this.addGroups = addGroups;
this.addLinks = addLinks;
this.addSteps = addSteps;
this.addTransitions = addTransitions;
this.globals = globals;
}
};
export {
Globals2 as Globals,
AdminContext,
TajribaAdminAccess
};
//# sourceMappingURL=chunk-LPBU7J6R.js.map