UNPKG

@empirica/core

Version:
1,048 lines (1,040 loc) 28.1 kB
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