UNPKG

@empirica/core

Version:
814 lines (804 loc) 21.1 kB
import { ErrNotConnected, bs, bsu, subscribeAsync } from "./chunk-WGYNSNUC.js"; import { Attributes, Scope, Scopes } from "./chunk-RXGVZSIF.js"; import { error, warn } from "./chunk-TIKLWCJI.js"; // src/admin/attributes.ts import { ReplaySubject } from "rxjs"; 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 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 import { merge } from "rxjs"; 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( 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/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/participants.ts import { EventType } from "@empirica/tajriba"; // src/admin/promises.ts function promiseHandle() { let ret = {}; ret.promise = new Promise((r) => { ret.result = r; }); return ret; } // src/admin/participants.ts async function participantsSub(taj, connections, participants) { let handle = promiseHandle(); taj.onEvent({ eventTypes: [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: [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 import { ReplaySubject as ReplaySubject2 } from "rxjs"; 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 ReplaySubject2(); 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) { return this.taj.addSteps(input); } addTransitions(input) { return this.taj.addTransitions(input); } addFinalizer(cb) { this.taj.addFinalizer(cb); } /** * @internal */ get globals() { return this.taj.globals; } }; // src/admin/subscriptions.ts function kvstr(kv) { return kv.key + "-" + kv.val; } var Subscriptions = class { constructor() { this.scopeKinds = /* @__PURE__ */ new Set(); this.scopeIDs = /* @__PURE__ */ new Set(); this.scopeNames = /* @__PURE__ */ new Set(); this.scopeKeys = /* @__PURE__ */ new Set(); this.scopeKVSet = /* @__PURE__ */ new Set(); this.scopeKVs = []; this.participantSub = false; this.transitionsSubs = /* @__PURE__ */ new Set(); this.dirty = false; this.last = { participants: false, scopes: { ids: [], kinds: [], names: [], keys: [], kvs: [] }, transitions: [] }; } get subs() { return { participants: this.participantSub, scopes: { kinds: Array.from(this.scopeKinds.values()), ids: Array.from(this.scopeIDs.values()), names: Array.from(this.scopeNames.values()), keys: Array.from(this.scopeKeys.values()), kvs: [...this.scopeKVs] }, transitions: Array.from(this.transitionsSubs.values()) }; } // newSubs will return only new subs since the last call. newSubs() { if (!this.dirty) { return; } const current = this.subs; const { scopes: { ids, kinds, names, keys, kvs }, participants, transitions } = this.last; const kvsstrs = kvs.map((kv) => kvstr(kv)); const next = { participants: this.participantSub && !participants, scopes: { ids: current.scopes.ids.filter((id) => !ids.includes(id)), kinds: current.scopes.kinds.filter((kind) => !kinds.includes(kind)), names: current.scopes.names.filter((name) => !names.includes(name)), keys: current.scopes.keys.filter((key) => !keys.includes(key)), kvs: current.scopes.kvs.filter((kv) => !kvsstrs.includes(kvstr(kv))) }, transitions: current.transitions.filter( (id) => !transitions.includes(id) ) }; this.last = current; this.dirty = false; return next; } scopeSub(input) { if (input.ids) { for (const id of input.ids) { if (!this.scopeIDs.has(id)) { this.scopeIDs.add(id); this.dirty = true; } } } if (input.kinds) { for (const id of input.kinds) { if (!this.scopeKinds.has(id)) { this.scopeKinds.add(id); this.dirty = true; } } } if (input.names) { for (const name of input.names) { if (!this.scopeNames.has(name)) { this.scopeNames.add(name); this.dirty = true; } } } if (input.keys) { for (const key of input.keys) { if (!this.scopeKeys.has(key)) { this.scopeKeys.add(key); this.dirty = true; } } } if (input.kvs) { for (const kv of input.kvs) { const kvKey = kvstr(kv); if (!this.scopeKVSet.has(kvKey)) { this.scopeKVSet.add(kvKey); this.scopeKVs.push(kv); this.dirty = true; } } } } participantsSub() { if (!this.participantSub) { this.dirty = true; this.participantSub = true; } } transitionsSub(nodeID) { if (!this.transitionsSubs.has(nodeID)) { this.transitionsSubs.add(nodeID); this.dirty = true; } } }; // src/admin/transitions.ts import { EventType as EventType2 } from "@empirica/tajriba"; function transitionsSub(taj, transitions, nodeID) { taj.onEvent({ eventTypes: [EventType2.TransitionAdd], nodeID }).subscribe({ next({ node }) { if (!node) { return; } if (node.__typename !== "Transition") { error(`received non-transition`); return; } if (node.node.__typename !== "Step") { error(`received non-step transition`, node.node); return; } transitions.next({ id: node.id, to: node.to, from: node.from, step: { id: node.node.id, duration: node.node.duration, state: node.node.state } }); } }); } export { Attributes2 as Attributes, AdminConnection, TajribaEvent, ListenersCollector, ListenersCollectorProxy, EventContext, Flusher, promiseHandle, participantsSub, Scopes2 as Scopes, Scope2 as Scope, Subscriptions, transitionsSub }; //# sourceMappingURL=chunk-ATDZK33U.js.map