UNPKG

@empirica/core

Version:
1,806 lines (1,783 loc) 70.3 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/admin/index.ts var admin_exports = {}; __export(admin_exports, { AdminConnection: () => AdminConnection, AdminContext: () => AdminContext, Attributes: () => Attributes2, EventContext: () => EventContext, Globals: () => Globals2, ListenersCollector: () => ListenersCollector, ListenersCollectorProxy: () => ListenersCollectorProxy, Scope: () => Scope2, Scopes: () => Scopes2, SharedGlobals: () => Globals, TajribaAdminAccess: () => TajribaAdminAccess, TajribaConnection: () => TajribaConnection, TajribaEvent: () => TajribaEvent, participantsSub: () => participantsSub }); module.exports = __toCommonJS(admin_exports); // src/shared/globals.ts var import_rxjs = require("rxjs"); var Globals = class { constructor(globals) { this.attrs = /* @__PURE__ */ new Map(); this.updates = /* @__PURE__ */ new Map(); this.self = new import_rxjs.BehaviorSubject(void 0); globals.subscribe({ next: ({ attribute, done }) => { if (attribute) { let val = void 0; if (attribute.val) { val = JSON.parse(attribute.val); } this.updates.set(attribute.key, val); } if (done) { for (const [key, val] of this.updates) { this.obs(key).next(val); } this.updates.clear(); if (this.self) { this.self.next(this); } } } }); } get(key) { const o = this.attrs.get(key); if (o) { return o.getValue(); } return void 0; } obs(key) { let o = this.attrs.get(key); if (!o) { o = new import_rxjs.BehaviorSubject(void 0); this.attrs.set(key, o); } return o; } }; // src/shared/tajriba_connection.ts var import_tajriba = require("@empirica/tajriba"); // src/utils/console.ts var isBrowser = typeof window !== "undefined" && typeof window.document !== "undefined"; var logsMock; var colorHex = { [1 /* Bold */]: "font-weight: bold", [30 /* Black */]: "color: #000000", [31 /* Red */]: "color: #cc0000", [32 /* Green */]: "color: #4e9a06", [33 /* Yellow */]: "color: #c4a000", [34 /* Blue */]: "color: #729fcf", [35 /* Magenta */]: "color: #75507b", [36 /* Cyan */]: "color: #06989a", [37 /* White */]: "color: #d3d7cf", [90 /* DarkGray */]: "color: #555753" }; var levels = { trace: 0, debug: 1, log: 2, info: 2, warn: 3, error: 4 }; var reversLevels = {}; for (const key in levels) { reversLevels[levels[key]] = key; } var currentLevel = 2; function formatConsoleDate(date, level) { var hour = date.getHours(); var minutes = date.getMinutes(); var seconds = date.getSeconds(); var milliseconds = date.getMilliseconds(); const str = (hour < 10 ? "0" + hour : hour) + ":" + (minutes < 10 ? "0" + minutes : minutes) + ":" + (seconds < 10 ? "0" + seconds : seconds) + "." + ("00" + milliseconds).slice(-3); if (isBrowser) { const ts = colorize(str, 90 /* DarkGray */).concat(level); return [ts[0] + " " + level[0], ts[1], level[1]]; } return colorize(str, 90 /* DarkGray */).concat(level); } var createLogger = (lvl, level) => { return (...args) => { if (lvl < currentLevel) { return; } if (logsMock) { logsMock.log({ level: reversLevels[lvl], args }); return; } if (args.length === 1) { switch (typeof args[0]) { case "string": for (const line of args[0].split("\n")) { console.log(...formatConsoleDate(/* @__PURE__ */ new Date(), level).concat(line)); } return; case "object": if (args[0] instanceof Error) { const error2 = args[0]; const prettyErr = error2.name + ": " + error2.message.replace(new RegExp(`^${error2.name}[: ]*`), "") + "\n" + (error2.stack || "").split("\n").map((line) => line.trim()).map((line) => { if (line.startsWith(error2.name + ": " + error2.message)) return null; if (line.startsWith("at")) { return " " + line; } return line; }).filter(Boolean).join("\n"); for (const line of prettyErr.split("\n")) { console.log(...formatConsoleDate(/* @__PURE__ */ new Date(), level).concat(line)); } return; } } } console.log(...formatConsoleDate(/* @__PURE__ */ new Date(), level).concat(args)); }; }; function colorize(s, ...cc) { if (isBrowser) { const attr = []; for (const c of cc) { attr.push(colorHex[c]); } return [`%c${s}`, attr.join("; ")]; } let out = ""; for (const c of cc) { out += `\x1B[${c}m`; } out += `${s}\x1B[0m`; return [out]; } var trace = createLogger(0, colorize("TRC", 35 /* Magenta */)); var debug = createLogger(1, colorize("DBG", 33 /* Yellow */)); var log = createLogger(2, colorize("LOG", 33 /* Yellow */)); var info = createLogger(2, colorize("INF", 32 /* Green */)); var warn = createLogger(3, colorize("WRN", 36 /* Cyan */)); var error = createLogger(4, colorize("ERR", 31 /* Red */, 1 /* Bold */)); // src/utils/object.ts var import_rxjs2 = require("rxjs"); function bs(init) { return new import_rxjs2.BehaviorSubject(init); } function bsu(init = void 0) { return new import_rxjs2.BehaviorSubject(init); } // src/shared/tajriba_connection.ts var ErrNotConnected = new Error("not connected"); var TajribaConnection = class { constructor(url) { this.url = url; this._connected = bs(false); this._connecting = bs(true); this._stopped = bs(false); this.tajriba = import_tajriba.Tajriba.connect(this.url); this._connected.next(this.tajriba.connected); this.tajriba.on("connected", () => { this._connected.next(true); this._connecting.next(false); }); this.tajriba.on("disconnected", () => { if (this._connected.getValue()) { this._connected.next(false); } if (!this._connecting.getValue()) { this._connecting.next(true); } }); this.tajriba.on("error", (err) => { error("connection error", err); }); } get connecting() { return this._connecting; } get connected() { return this._connected; } get stopped() { return this._stopped; } async sessionParticipant(token, pident) { if (!this._connected.getValue()) { throw ErrNotConnected; } return await this.tajriba.sessionParticipant(token, pident); } async sessionAdmin(token) { if (!this._connected.getValue()) { throw ErrNotConnected; } return await this.tajriba.sessionAdmin(token); } stop() { if (this._stopped.getValue()) { return; } if (this.tajriba) { this.tajriba.removeAllListeners("connected"); this.tajriba.removeAllListeners("disconnected"); this.tajriba.stop(); } this._connecting.next(false); this._connected.next(false); this._stopped.next(true); } }; // src/admin/attributes.ts var import_rxjs4 = require("rxjs"); // src/shared/attributes.ts var import_rxjs3 = require("rxjs"); var Attributes = class { constructor(attributesObs, donesObs, setAttributes) { this.setAttributes = setAttributes; this.attrs = /* @__PURE__ */ new Map(); this.updates = /* @__PURE__ */ new Map(); attributesObs.subscribe({ next: ({ attribute, removed }) => { this.update(attribute, removed); } }); donesObs.subscribe({ next: (scopeIDs) => { this.next(scopeIDs); } }); } attribute(scopeID, key) { let scopeMap = this.attrs.get(scopeID); if (!scopeMap) { scopeMap = /* @__PURE__ */ new Map(); this.attrs.set(scopeID, scopeMap); } let attr = scopeMap.get(key); if (!attr) { attr = new Attribute(this.setAttributes, scopeID, key); scopeMap.set(key, attr); } return attr; } attributes(scopeID) { let scopeMap = this.attrs.get(scopeID); if (!scopeMap) { scopeMap = /* @__PURE__ */ new Map(); this.attrs.set(scopeID, scopeMap); } return Array.from(scopeMap.values()); } attributePeek(scopeID, key) { let scopeUpdateMap = this.updates.get(scopeID); if (scopeUpdateMap) { const updated = scopeUpdateMap.get(key); if (updated) { if (typeof updated === "boolean") { return; } else { if (!updated.val) { return; } else { const attr2 = new Attribute(this.setAttributes, scopeID, key); attr2._update(updated); return attr2; } } } } let scopeMap = this.attrs.get(scopeID); if (!scopeMap) { return; } let attr = scopeMap.get(key); if (!attr) { return; } if (attr.value === void 0) { return; } return attr; } nextAttributeValue(scopeID, key) { const attr = this.attributePeek(scopeID, key); if (!attr) { return; } return attr.value; } update(attr, removed) { let nodeID = attr.nodeID; if (!nodeID) { if (!attr.node?.id) { error(`new attribute without node ID`); return; } nodeID = attr.node.id; } let scopeMap = this.updates.get(nodeID); if (!scopeMap) { scopeMap = /* @__PURE__ */ new Map(); this.updates.set(nodeID, scopeMap); } if (removed) { scopeMap.set(attr.key, true); } else { let key = attr.key; if (attr.index !== void 0 && attr.index !== null) { key = `${key}[${attr.index}]`; } scopeMap.set(key, attr); } } scopeWasUpdated(scopeID) { if (!scopeID) { return false; } return this.updates.has(scopeID); } next(scopeIDs) { for (const [scopeID, attrs] of this.updates) { if (!scopeIDs.includes(scopeID)) { continue; } let scopeMap = this.attrs.get(scopeID); if (!scopeMap) { scopeMap = /* @__PURE__ */ new Map(); this.attrs.set(scopeID, scopeMap); } for (const [key, attrOrDel] of attrs) { if (typeof attrOrDel === "boolean") { let attr = scopeMap.get(key); if (attr) { attr._update(void 0); } } else { let attr = scopeMap.get(attrOrDel.key); if (!attr) { attr = new Attribute(this.setAttributes, scopeID, attrOrDel.key); scopeMap.set(attrOrDel.key, attr); } attr._update(attrOrDel); } } } for (const scopeID of scopeIDs) { this.updates.delete(scopeID); } } }; var Attribute = class { constructor(setAttributes, scopeID, key) { this.setAttributes = setAttributes; this.scopeID = scopeID; this.key = key; this.val = new import_rxjs3.BehaviorSubject(void 0); } get id() { return this.attr?.id; } get createdAt() { return this.attr ? new Date(this.attr.createdAt) : null; } get obs() { return this.val; } get value() { return this.val.getValue(); } get nodeID() { return this.scopeID; } // items returns the attribute changes for the current attribute, if it is a // vector. Otherwise it returns null; get items() { if (!this.attrs) { return null; } return this.attrs; } set(value, ao) { const attrProps = this._prepSet(value, ao); if (!attrProps) { return; } this.setAttributes([attrProps]); trace(`SET ${this.key} = ${value} (${this.scopeID})`); } _prepSet(value, ao, item) { if (ao?.append !== void 0 && ao.index !== void 0) { error(`cannot set both append and index`); throw new Error(`cannot set both append and index`); } const serVal = JSON.stringify(value); if (!item && (ao?.index !== void 0 || ao?.append)) { let index = ao.index || 0; if (ao?.append) { index = this.attrs?.length || 0; } if (!this.attrs) { this.attrs = []; } if (!this.attrs[index]) { this.attrs[index] = new Attribute( this.setAttributes, this.scopeID, this.key ); } else { const existing = this.attrs[index]; if (existing && existing.serVal === serVal) { return; } } this.attrs[index]._prepSet(value, ao, true); const v = this._recalcVectorVal(); this.val.next(v); } else { if (this.serVal === serVal) { return; } this.val.next(value); } this.serVal = serVal; const attrProps = { key: this.key, nodeID: this.scopeID, val: serVal }; 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; } return attrProps; } _recalcVectorVal() { return this.attrs.map( (a) => !a || a.val == void 0 ? null : a.value || null ); } // internal only _update(attr, item) { if (attr && this.attr && this.attr.id === attr.id) { return; } if (attr && attr.vector && !item) { if (attr.index === void 0) { error(`vector attribute missing index`); return; } if (this.attrs == void 0) { this.attrs = []; } while (this.attrs.length < attr.index + 1) { const newAttr2 = new Attribute( this.setAttributes, this.scopeID, this.key ); this.attrs.push(newAttr2); } const newAttr = new Attribute(this.setAttributes, this.scopeID, this.key); newAttr._update(attr, true); this.attrs[attr.index] = newAttr; const value2 = this._recalcVectorVal(); this.val.next(value2); return; } this.attr = attr; this.serVal = attr?.val === void 0 || attr?.val === null ? "" : attr.val; let value = void 0; if (this.attr?.val) { value = JSON.parse(this.attr.val); } this.val.next(value); } }; // src/admin/attributes.ts var Attributes2 = class extends Attributes { constructor() { super(...arguments); this.attrsByKind = /* @__PURE__ */ new Map(); this.attribSubs = /* @__PURE__ */ new Map(); } subscribeAttribute(kind, key) { if (!this.attribSubs.has(kind)) { this.attribSubs.set(kind, /* @__PURE__ */ new Map()); } const keyMap = this.attribSubs.get(kind); let sub = keyMap.get(key); if (!sub) { sub = new import_rxjs4.ReplaySubject(); keyMap.set(key, sub); const attrByScopeID = this.attrsByKind.get(kind); setTimeout(() => { if (!attrByScopeID) { sub.next({ done: true }); return; } let attrs = []; for (const [_, attrByKey] of attrByScopeID?.entries()) { for (const [_2, attr] of attrByKey) { if (attr.key === key) { attrs.push(attr); } } } if (attrs.length > 0) { let count = 0; for (const attr of attrs) { count++; sub.next({ attribute: attr, done: count == attrs.length }); } } else { sub.next({ done: true }); } }, 0); } return sub; } next(scopeIDs) { const byKind = /* @__PURE__ */ new Map(); for (const [scopeID, attrs] of this.updates) { if (!scopeIDs.includes(scopeID)) { continue; } for (const [_, attr] of attrs) { if (typeof attr === "boolean") { continue; } const kind = attr.node?.kind; if (kind) { let kindAttrs = byKind.get(kind); if (!kindAttrs) { kindAttrs = []; byKind.set(kind, kindAttrs); } kindAttrs.push(attr); } } } const updates = []; for (const [kind, attrs] of byKind) { for (const attr of attrs) { if (!attr.nodeID && !attr.node?.id) { warn(`found attribute change without node ID`); continue; } if (!scopeIDs.includes(attr.nodeID || attr.node.id)) { continue; } updates.push([kind, attr.key, attr]); } } super.next(scopeIDs); for (const [kind, key, attrChange] of updates) { const nodeID = attrChange.nodeID || attrChange.node.id; if (!scopeIDs.includes(nodeID)) { continue; } const attr = this.attrs.get(nodeID).get(key); const sub = this.attribSubs.get(kind)?.get(key); if (sub) { sub.next({ attribute: attr, done: true }); } else { let kAttrs = this.attrsByKind.get(kind); if (!kAttrs) { kAttrs = /* @__PURE__ */ new Map(); this.attrsByKind.set(kind, kAttrs); } let kkAttrs = kAttrs.get(nodeID); if (!kkAttrs) { kkAttrs = /* @__PURE__ */ new Map(); kAttrs.set(nodeID, kkAttrs); } kkAttrs.set(key, attr); } } } }; // src/admin/connection.ts var import_rxjs6 = require("rxjs"); // src/admin/observables.ts var import_async_mutex = require("async-mutex"); var import_rxjs5 = require("rxjs"); async function awaitObsValue(obs, value) { let res; const prom = new Promise((r) => { res = r; }); const unsub = obs.subscribe((val2) => { if (val2 === value) { res(val2); } }); const val = await prom; unsub.unsubscribe(); return val; } function lockedAsyncSubscribe(mutex, obs, fn) { return obs.subscribe({ next: async (val) => { try { const release = await mutex.acquire(); try { await fn(val); } catch (err) { console.error("error in async observable subscription"); console.error(err); } finally { release(); } } catch (err) { if (err !== import_async_mutex.E_CANCELED) { console.error( "error acquiring lock in async observable subscription" ); console.error(err); } } } }); } function subscribeAsync(obs, fn) { const cancel = new import_rxjs5.Subject(); obs.pipe((0, import_rxjs5.concatMap)(fn), (0, import_rxjs5.takeUntil)(cancel)).subscribe(); return { closed: false, unsubscribe() { if (this.closed) { warn("closing a closed async observable subscription"); return; } this.closed = true; cancel.next(); cancel.unsubscribe(); } }; } // src/admin/connection.ts var AdminConnection = class { constructor(taj, tokens, resetToken) { this.resetToken = resetToken; this._tajriba = bsu(); this._connected = bs(false); this._connecting = bs(false); this._stopped = bs(false); let token; let connected = false; this.sub = subscribeAsync( (0, import_rxjs6.merge)(taj.connected, tokens), async (tokenOrConnected) => { if (typeof tokenOrConnected === "boolean") { connected = tokenOrConnected; } else { token = tokenOrConnected; } if (!token || !connected) { return; } if (this._connected.getValue()) { return; } this._connecting.next(true); try { const tajAdmin = await taj.sessionAdmin(token); this._tajriba.next(tajAdmin); this._connected.next(true); tajAdmin.on("connected", () => { if (!this._connected.getValue()) { this._connected.next(true); } }); tajAdmin.on("error", (err) => { error("connection error", err); }); tajAdmin.on("disconnected", () => { if (this._connected.getValue()) { this._connected.next(false); } }); tajAdmin.on("accessDenied", () => { if (this._connected.getValue()) { this._connected.next(false); } this.resetToken(); }); } catch (error2) { if (error2 !== ErrNotConnected) { this.resetToken(); } } this._connecting.next(false); } ); } stop() { if (this._stopped.getValue()) { return; } const taj = this._tajriba.getValue(); if (taj) { taj.removeAllListeners("connected"); taj.removeAllListeners("disconnected"); taj.stop(); this._tajriba.next(void 0); } this.sub.unsubscribe(); this._connecting.next(false); this._connected.next(false); this._stopped.next(true); } get connecting() { return this._connecting; } get connected() { return this._connected; } get stopped() { return this._stopped; } get admin() { return this._tajriba; } }; // src/admin/context.ts var import_rxjs11 = require("rxjs"); // src/admin/runloop.ts var import_rxjs9 = require("rxjs"); // src/admin/cake.ts var import_async_mutex2 = require("async-mutex"); // src/admin/events.ts var TajribaEvent = /* @__PURE__ */ ((TajribaEvent2) => { TajribaEvent2["TransitionAdd"] = "TRANSITION_ADD"; TajribaEvent2["ParticipantConnect"] = "PARTICIPANT_CONNECT"; TajribaEvent2["ParticipantDisconnect"] = "PARTICIPANT_DISCONNECT"; return TajribaEvent2; })(TajribaEvent || {}); var placementString = /* @__PURE__ */ new Map(); placementString.set(0 /* Before */, "before"); placementString.set(1 /* None */, "on"); placementString.set(2 /* After */, "after"); function PlacementString(placement) { return placementString.get(placement); } function unique(kind, placement, callback) { return async (ctx, props) => { const attr = props.attribute; const scope = props[kind]; if (!attr.id || scope.get(`ran-${PlacementString(placement)}-${props.attrId}`)) { return; } await callback(ctx, props); scope.set(`ran-${PlacementString(placement)}-${props.attrId}`, true); }; } var ListenersCollector = class { constructor() { /** @internal */ this.starts = []; /** @internal */ this.readys = []; /** @internal */ this.tajEvents = []; /** @internal */ this.kindListeners = []; /** @internal */ this.attributeListeners = []; } /** @internal */ setFlusher(flusher) { this.flusher = flusher; } async flush() { if (!this.flusher) { return; } await this.flusher.flush(); } flushAfter(cb) { if (!this.flusher) { return; } return this.flusher.flushAfter(cb); } get unique() { return new ListenersCollectorProxy(this); } on(kindOrEvent, keyOrNodeIDOrEventOrCallback, callback) { this.registerListerner( 1 /* None */, kindOrEvent, keyOrNodeIDOrEventOrCallback, callback ); } before(kindOrEvent, keyOrNodeIDOrEventOrCallback, callback, uniqueCall) { this.registerListerner( 0 /* Before */, kindOrEvent, keyOrNodeIDOrEventOrCallback, callback, uniqueCall ); } after(kindOrEvent, keyOrNodeIDOrEventOrCallback, callback, uniqueCall) { this.registerListerner( 2 /* After */, kindOrEvent, keyOrNodeIDOrEventOrCallback, callback, uniqueCall ); } registerListerner(placement, kindOrEvent, keyOrNodeIDOrEventOrCallback, callback, uniqueCall = false) { if (kindOrEvent === "start") { if (callback) { throw new Error("start event only accepts 2 arguments"); } if (typeof keyOrNodeIDOrEventOrCallback !== "function") { throw new Error("second argument expected to be a callback"); } this.starts.push({ placement, callback: keyOrNodeIDOrEventOrCallback }); return; } if (kindOrEvent === "ready") { if (callback) { throw new Error("ready event only accepts 2 arguments"); } if (typeof keyOrNodeIDOrEventOrCallback !== "function") { throw new Error("second argument expected to be a callback"); } this.readys.push({ placement, callback: keyOrNodeIDOrEventOrCallback }); return; } if (Object.values(TajribaEvent).includes(kindOrEvent)) { if (typeof keyOrNodeIDOrEventOrCallback !== "function") { throw new Error("second argument expected to be a callback"); } this.tajEvents.push({ placement, event: kindOrEvent, callback: keyOrNodeIDOrEventOrCallback }); return; } if (typeof keyOrNodeIDOrEventOrCallback === "function") { this.kindListeners.push({ placement, kind: kindOrEvent, callback: keyOrNodeIDOrEventOrCallback }); } else { if (typeof keyOrNodeIDOrEventOrCallback !== "string") { throw new Error("second argument expected to be an attribute key"); } if (typeof callback !== "function") { throw new Error("third argument expected to be a callback"); } if (uniqueCall) { callback = unique(kindOrEvent, placement, callback); } this.attributeListeners.push({ placement, kind: kindOrEvent, key: keyOrNodeIDOrEventOrCallback, callback }); } } }; var ListenersCollectorProxy = class extends ListenersCollector { constructor(coll) { super(); this.coll = coll; } registerListerner(placement, kindOrEvent, keyOrNodeIDOrEventOrCallback, callback) { if (kindOrEvent === "start" || kindOrEvent === "ready" || Object.values(TajribaEvent).includes(kindOrEvent) || typeof keyOrNodeIDOrEventOrCallback === "function") { throw new Error("only attribute listeners can be unique"); } super.registerListerner( placement, kindOrEvent, keyOrNodeIDOrEventOrCallback, callback, true ); while (true) { const listener = this.attributeListeners.pop(); if (!listener) { break; } this.coll.attributeListeners.push(listener); } } }; var EventContext = class { constructor(subs, taj, scopes, flusher) { this.subs = subs; this.taj = taj; this.scopes = scopes; this.flusher = flusher; } async flush() { await this.flusher.flush(); } flushAfter(cb) { return this.flusher.flushAfter(cb); } scopesByKind(kind) { return this.scopes.byKind(kind); } scopesByKindID(kind, id) { return this.scopes.byKind(kind).get(id); } scopesByKindMatching(kind, key, val) { const scopes = Array.from(this.scopes.byKind(kind).values()); return scopes.filter((s) => s.get(key) === val); } scopeSub(...inputs) { for (const input of inputs) { this.subs.scopeSub(input); } } participantsSub() { this.subs.participantsSub(); } transitionsSub(stepID) { this.subs.transitionsSub(stepID); } // c8 ignore: the TajribaAdminAccess proxy functions are tested elswhere /* c8 ignore next 3 */ addScopes(input) { return this.taj.addScopes(input); } /* c8 ignore next 3 */ addGroups(input) { return this.taj.addGroups(input); } /* c8 ignore next 3 */ addLinks(input) { return this.taj.addLinks(input); } /* c8 ignore next 3 */ addSteps(input) { return this.taj.addSteps(input); } /* c8 ignore next 3 */ addTransitions(input) { return this.taj.addTransitions(input); } addFinalizer(cb) { this.taj.addFinalizer(cb); } /* c8 ignore next 3 */ get globals() { return this.taj.globals; } }; var Flusher = class { constructor(postCallback) { this.postCallback = postCallback; } async flush() { if (!this.postCallback) { return; } await this.postCallback(); } flushAfter(cb) { if (!this.postCallback) { cb(); return; } return async () => { await cb(); if (this.postCallback) { await this.postCallback(); } }; } }; // src/admin/promises.ts function promiseHandle() { let ret = {}; ret.promise = new Promise((r) => { ret.result = r; }); return ret; } // src/admin/cake.ts 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 import_async_mutex2.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/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/participants.ts var import_tajriba2 = require("@empirica/tajriba"); async function participantsSub(taj, connections, participants) { let handle = promiseHandle(); taj.onEvent({ eventTypes: [import_tajriba2.EventType.ParticipantConnected] }).subscribe({ next({ node, done }) { if (!node) { if (done) { if (handle) { handle?.result(); connections.next({ done: true }); } return; } error(`received no participant on connected`); return; } if (node.__typename !== "Participant") { error(`received non-participant on connected`); return; } const part = { id: node.id, identifier: node.identifier }; participants.set(node.id, part); connections.next({ connection: { participant: part, connected: true }, done }); if (handle && done) { handle.result(); } } }); taj.onEvent({ eventTypes: [import_tajriba2.EventType.ParticipantDisconnect] }).subscribe({ next({ node }) { if (!node) { error(`received no participant on disconnect`); return; } if (node.__typename !== "Participant") { error(`received non-participant on disconnect`); return; } participants.delete(node.id); connections.next({ connection: { participant: { id: node.id, identifier: node.identifier }, connected: false }, done: true }); } }); await handle.promise; handle = void 0; } // src/admin/scopes.ts var import_rxjs8 = require("rxjs"); // src/shared/scopes.ts var import_rxjs7 = require("rxjs"); var Scopes = class { constructor(scopesObs, donesObs, ctx, kinds, attributes) { this.ctx = ctx; this.kinds = kinds; this.attributes = attributes; this.scopes = /* @__PURE__ */ new Map(); // newScopes is used to track scopes that have appeared for the first time. this.newScopes = /* @__PURE__ */ new Map(); this.scopesByKind = /* @__PURE__ */ new Map(); this.kindUpdated = /* @__PURE__ */ new Set(); scopesObs.subscribe({ next: ({ scope, removed }) => { this.update(scope, removed); } }); donesObs.subscribe({ next: (scopeIDs) => { this.next(scopeIDs); } }); } scope(id) { return this.scopes.get(id)?.getValue(); } scopeObs(id) { return this.scopes.get(id); } byKind(kind) { let map = this.scopesByKind.get(kind); if (!map) { map = /* @__PURE__ */ new Map(); this.scopesByKind.set(kind, map); } return map; } kindWasUpdated(kind) { return this.kindUpdated.has(kind); } next(scopeIDs) { this.kindUpdated.clear(); for (const [_, scopeSubject] of this.scopes) { const scope = scopeSubject.getValue(); if ((scope._updated || this.attributes.scopeWasUpdated(scope.id)) && scopeIDs.includes(scope.id)) { scope._updated = false; scopeSubject.next(scope); } } } update(scope, removed) { const existing = this.scopes.get(scope.id)?.getValue(); if (removed) { if (!existing) { warn("scopes: missing scope on removal", scope.id, scope.kind); return; } existing._deleted = true; existing._updated = true; this.scopes.delete(scope.id); if (!scope.kind) { warn("scopes: scope missing kind on scope on removal"); return; } const kind2 = scope.kind; this.scopesByKind.get(kind2).delete(scope.id); this.kindUpdated.add(kind2); return; } if (existing) { existing._deleted = false; return; } if (!scope.kind) { warn("scopes: scope missing kind on scope"); return; } const kind = scope.kind; const scopeClass = this.kinds[kind]; if (!scopeClass) { warn(`scopes: unknown scope kind: ${scope.kind}`); return; } const obj = this.create(scopeClass, scope); const subj = new import_rxjs7.BehaviorSubject(obj); this.scopes.set(scope.id, subj); this.newScopes.set(scope.id, true); let skm = this.scopesByKind.get(kind); if (!skm) { skm = /* @__PURE__ */ new Map(); this.scopesByKind.set(kind, skm); } skm.set(scope.id, obj); obj._updated = true; this.kindUpdated.add(kind); } create(scopeClass, scope) { return new scopeClass(this.ctx, scope, this.attributes); } }; var Scope = class { constructor(ctx, scope, attributes) { this.ctx = ctx; this.scope = scope; this.attributes = attributes; /** * @internal */ this._deleted = false; /** * @internal */ this._updated = false; } get id() { return this.scope.id; } /** * @internal */ get kind() { return this.scope.kind; } get(key) { return this.attributes.attribute(this.scope.id, key).value; } getAttribute(key) { return this.attributes.attribute(this.scope.id, key); } obs(key) { return this.attributes.attribute(this.scope.id, key).obs; } set(keyOrAttributes, value, ao) { if (typeof keyOrAttributes === "string") { if (value === void 0) { value = null; } return this.attributes.attribute(this.scope.id, keyOrAttributes).set(value, ao); } const nextProps = []; for (const attr of keyOrAttributes) { const at = this.attributes.attribute(this.scope.id, attr.key)._prepSet(attr.value, attr.ao); if (!at) { continue; } nextProps.push(at); } if (nextProps.length === 0) { return; } this.attributes.setAttributes(nextProps); } append(key, value, ao) { if (!ao) { ao = {}; } ao.append = true; return this.attributes.attribute(this.scope.id, key).set(value, ao); } inspect() { const attrs = this.attributes.attributes(this.scope.id); const out = {}; for (const attr of attrs) { out[attr.key] = attr.value; } return out; } /** * @internal */ hasUpdated() { return this._updated || this.attributes.scopeWasUpdated(this.id); } }; // src/admin/scopes.ts var Scopes2 = class extends Scopes { constructor(scopesObs, donesObs, ctx, kinds, attributes, taj) { super(scopesObs, donesObs, ctx, kinds, attributes); this.taj = taj; this.kindSubs = /* @__PURE__ */ new Map(); } /** @internal */ subscribeKind(kind) { let sub = this.kindSubs.get(kind); if (!sub) { sub = new import_rxjs8.ReplaySubject(); this.kindSubs.set(kind, sub); const scopes = this.byKind(kind); setTimeout(() => { if (scopes.size === 0) { sub.next({ done: true }); return; } let count = 0; for (const [_, scope] of scopes) { count++; sub.next({ scope, done: scopes.size === count }); } }, 0); } return sub; } next(scopeIDs) { for (const [_, scopeReplaySubject] of this.scopes) { const scope = scopeReplaySubject.getValue(); if (this.newScopes.get(scope.id) && scopeIDs.includes(scope.id)) { const kindSub = this.kindSubs.get(scope.kind); if (kindSub) { kindSub.next({ scope, done: true }); } this.newScopes.set(scope.id, false); } } super.next(scopeIDs); } create(scopeClass, scope) { return new scopeClass(this.ctx, scope, this, this.attributes); } }; var Scope2 = class extends Scope { constructor(ctx, scope, scopes, attributes) { super(ctx, scope, attributes); this.scopes = scopes; this.taj = scopes.taj; } scopeByID(id) { return this.scopes.scope(id); } scopeByKey(key) { const id = this.get(key); if (!id || typeof id !== "string") { return; } return this.scopes.scope(id); } scopesByKind(kind) { return this.scopes.byKind(kind); } scopesByKindID(kind, id) { return this.scopes.byKind(kind).get(id); } scopesByKindMatching(kind, key, val) { const scopes = Array.from(this.scopes.byKind(kind).values()); return scopes.filter((s) => s.get(key) === val); } addScopes(input) { return this.taj.addScopes(input); } addGroups(input) { return this.taj.addGroups(input); } addLinks(input) { return this.taj.addLinks(input); } addSteps(input) { retu