@empirica/core
Version:
Empirica Core
1,806 lines (1,783 loc) • 70.3 kB
JavaScript
"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