@empirica/core
Version:
Empirica Core
814 lines (804 loc) • 21.1 kB
JavaScript
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