@memizy/host-sdk 0.0.0-stage → 1.0.0-rc.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,32 @@
1
- # Temporary Holding Version
1
+ # @memizy/host-sdk
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Run Memizy plugins in a host application (the Memizy app, the Plugin Lab): load and validate plugins, mount them in sandboxed iframes, enforce the [Memizy Plugin Protocol](../protocol/SPEC.md) and run game sessions.
4
+
5
+ ```ts
6
+ import { loadOQSEFile } from '@memizy/oqse';
7
+ import { loadPluginFromHtml, LocalSession, mountPlugin } from '@memizy/host-sdk';
8
+
9
+ const loaded = loadPluginFromHtml(html); // manifest validated without running the plugin
10
+ if (!loaded.success) throw new Error(loaded.errors.join('\n'));
11
+
12
+ const session = new LocalSession({
13
+ plugin: loaded.plugin,
14
+ set: loadOQSEFile(setJson).data!,
15
+ mode: 'multiplayer',
16
+ hostAs: 'presenter',
17
+ players: [{ id: 'anna', name: 'Anna' }, { id: 'ben', name: 'Ben' }],
18
+ });
19
+ session.on((event) => console.log(event)); // ready, countdown, answer, ended, traffic, rejected…
20
+
21
+ await mountPlugin(session, 'board', boardElement); // sandboxed iframe + Penpal
22
+ await mountPlugin(session, 'anna', annaElement);
23
+ await mountPlugin(session, 'ben', benElement);
24
+ await session.start(); // waits for everyone, countdown, start
25
+ ```
26
+
27
+ * **`LocalSession`** – all instances in one page (Lab, solo games in the app). Validates every plugin call (schemas, size and rate limits, views, authority), routes messages, runs the start barrier and countdown, handles authority outages (`setConnected`), reloads (`reload`), late joining (`addPlayer`), snapshots, learning progress and plugin data. Multiplayer across devices will use the same API with a relay transport.
28
+ * **`HostStorage`** – plug in your persistence (`MemoryStorage` for the Lab and tests).
29
+ * **`LearningAlgorithm`** – how answers change learning progress (`leitner` by default).
30
+ * **`prepareSetForPlugin` / `checkCompatibility`** – keep only item types the plugin supports, decide which plugins can play a set.
31
+
32
+ Iframes are sandboxed with `allow-scripts allow-forms allow-modals allow-pointer-lock` and **never** `allow-same-origin`.
package/dist/frame.cjs ADDED
@@ -0,0 +1 @@
1
+ let e=require("@memizy/protocol"),t=require("penpal");var n=`allow-scripts allow-forms allow-modals allow-pointer-lock`;function r(t){let n={};for(let r of e.PLUGIN_API_METHODS)n[r]=async(...n)=>{try{return await t[r](...n)}catch(t){throw(0,e.toProtocolError)(t)}};return n}function i(i,a,o,s=document){let c=s.createElement(`iframe`);c.setAttribute(`sandbox`,n),c.setAttribute(`title`,a),c.style.cssText=`width:100%;height:100%;border:0;display:block;touch-action:manipulation;touch-action:pan-x pan-y;`;let l=(0,e.readPluginManifestFromHtml)(i),u=l.success?l.runtime:null,d=(0,e.pluginAllowAttribute)(u);d&&c.setAttribute(`allow`,d);let f=s.defaultView?.location?.origin??null;c.srcdoc=(0,e.injectContentPolicy)(i,(0,e.pluginContentPolicy)(u,f));let p=null;return{iframe:c,plugin:new Promise((n,i)=>{queueMicrotask(()=>{if(!c.contentWindow){i(Error(`The plugin iframe is not in the document.`));return}(0,e.ensureConnectionPolyfills)(),p=(0,t.connect)({messenger:new t.WindowMessenger({remoteWindow:c.contentWindow,allowedOrigins:[`*`]}),methods:o,timeout:e.LIMITS.helloTimeoutMs}),p.promise.then(e=>n(r(e)),i)})}),destroy:()=>p?.destroy()}}async function a(e,t,n,r={}){let a=n.ownerDocument,o=e.config.locale.startsWith(`cs`),s={waitingForHost:r.texts?.waitingForHost??(o?`Čekáme na hostitele…`:`Waiting for the host…`),countdown:r.texts?.countdown??(e=>String(e))},c=a.createElement(`div`);c.className=`mz-host-frame`,c.style.cssText=`position:relative;width:100%;height:100%;`;let l=a.createElement(`div`);l.className=`mz-host-overlay`,l.style.cssText=`position:absolute;inset:0;display:none;place-items:center;background:rgba(0,0,0,.55);color:#fff;font:600 1.5rem system-ui,sans-serif;text-align:center;padding:16px;`;let u=null,d=null;n.appendChild(c);let f=e=>{r.overlays!==!1&&(l.textContent=e??``,l.style.display=e?`grid`:`none`)},p=e.on(n=>{n.type===`countdown`?f(s.countdown(n.secondsLeft)):n.type===`started`?f(null):n.type===`authority`&&t!==e.authority?f(n.connected?null:s.waitingForHost):n.type===`resize`&&n.address===t&&(c.style.height=n.height===`auto`?``:`${n.height}px`)});return{instance:await e.connect(t,async t=>{d?.(),u?.remove();let n=i(e.plugin.html,e.plugin.manifest.appName,t,a);return u=n.iframe,c.replaceChildren(u,l),d=n.destroy,n.plugin},{onDispose:()=>{d?.(),d=null}}),get iframe(){return u},unmount(){p(),e.disconnect(t),c.remove()}}}exports.PLUGIN_SANDBOX=n,exports.createPluginFrame=i,exports.mountPlugin=a;
package/dist/frame.js ADDED
@@ -0,0 +1,77 @@
1
+ import { LIMITS as e, PLUGIN_API_METHODS as t, ensureConnectionPolyfills as n, injectContentPolicy as r, pluginAllowAttribute as i, pluginContentPolicy as a, readPluginManifestFromHtml as o, toProtocolError as s } from "@memizy/protocol";
2
+ import { WindowMessenger as c, connect as l } from "penpal";
3
+ //#region src/frame.ts
4
+ var u = "allow-scripts allow-forms allow-modals allow-pointer-lock";
5
+ function d(e) {
6
+ let n = {};
7
+ for (let r of t) n[r] = async (...t) => {
8
+ try {
9
+ return await e[r](...t);
10
+ } catch (e) {
11
+ throw s(e);
12
+ }
13
+ };
14
+ return n;
15
+ }
16
+ function f(t, s, f, p = document) {
17
+ let m = p.createElement("iframe");
18
+ m.setAttribute("sandbox", u), m.setAttribute("title", s), m.style.cssText = "width:100%;height:100%;border:0;display:block;touch-action:manipulation;touch-action:pan-x pan-y;";
19
+ let h = o(t), g = h.success ? h.runtime : null, _ = i(g);
20
+ _ && m.setAttribute("allow", _);
21
+ let v = p.defaultView?.location?.origin ?? null;
22
+ m.srcdoc = r(t, a(g, v));
23
+ let y = null;
24
+ return {
25
+ iframe: m,
26
+ plugin: new Promise((t, r) => {
27
+ queueMicrotask(() => {
28
+ if (!m.contentWindow) {
29
+ r(/* @__PURE__ */ Error("The plugin iframe is not in the document."));
30
+ return;
31
+ }
32
+ n(), y = l({
33
+ messenger: new c({
34
+ remoteWindow: m.contentWindow,
35
+ allowedOrigins: ["*"]
36
+ }),
37
+ methods: f,
38
+ timeout: e.helloTimeoutMs
39
+ }), y.promise.then((e) => t(d(e)), r);
40
+ });
41
+ }),
42
+ destroy: () => y?.destroy()
43
+ };
44
+ }
45
+ async function p(e, t, n, r = {}) {
46
+ let i = n.ownerDocument, a = e.config.locale.startsWith("cs"), o = {
47
+ waitingForHost: r.texts?.waitingForHost ?? (a ? "Čekáme na hostitele…" : "Waiting for the host…"),
48
+ countdown: r.texts?.countdown ?? ((e) => String(e))
49
+ }, s = i.createElement("div");
50
+ s.className = "mz-host-frame", s.style.cssText = "position:relative;width:100%;height:100%;";
51
+ let c = i.createElement("div");
52
+ c.className = "mz-host-overlay", c.style.cssText = "position:absolute;inset:0;display:none;place-items:center;background:rgba(0,0,0,.55);color:#fff;font:600 1.5rem system-ui,sans-serif;text-align:center;padding:16px;";
53
+ let l = null, u = null;
54
+ n.appendChild(s);
55
+ let d = (e) => {
56
+ r.overlays !== !1 && (c.textContent = e ?? "", c.style.display = e ? "grid" : "none");
57
+ }, p = e.on((n) => {
58
+ n.type === "countdown" ? d(o.countdown(n.secondsLeft)) : n.type === "started" ? d(null) : n.type === "authority" && t !== e.authority ? d(n.connected ? null : o.waitingForHost) : n.type === "resize" && n.address === t && (s.style.height = n.height === "auto" ? "" : `${n.height}px`);
59
+ });
60
+ return {
61
+ instance: await e.connect(t, async (t) => {
62
+ u?.(), l?.remove();
63
+ let n = f(e.plugin.html, e.plugin.manifest.appName, t, i);
64
+ return l = n.iframe, s.replaceChildren(l, c), u = n.destroy, n.plugin;
65
+ }, { onDispose: () => {
66
+ u?.(), u = null;
67
+ } }),
68
+ get iframe() {
69
+ return l;
70
+ },
71
+ unmount() {
72
+ p(), e.disconnect(t), s.remove();
73
+ }
74
+ };
75
+ }
76
+ //#endregion
77
+ export { u as PLUGIN_SANDBOX, f as createPluginFrame, p as mountPlugin };
package/dist/index.cjs ADDED
@@ -0,0 +1 @@
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./plugin.cjs"),t=require("./learning.cjs"),n=require("./storage.cjs"),r=require("./session.cjs"),i=require("./frame.cjs"),a=require("./relay/socket.cjs"),o=require("./relay/messages.cjs"),s=require("./relay/host.cjs"),c=require("./relay/player.cjs");exports.LEITNER_INTERVAL_DAYS=t.LEITNER_INTERVAL_DAYS,exports.LocalSession=r.LocalSession,exports.MemoryStorage=n.MemoryStorage,exports.PLUGIN_SANDBOX=i.PLUGIN_SANDBOX,exports.RelayHost=s.RelayHost,exports.RelayPlayer=c.RelayPlayer,exports.RelaySocket=a.RelaySocket,exports.SETTINGS_ADDRESS=r.SETTINGS_ADDRESS,exports.blobToJson=o.blobToJson,exports.createEmptySet=e.createEmptySet,exports.createPluginFrame=i.createPluginFrame,exports.isSerializedBlob=o.isSerializedBlob,exports.jsonToBlob=o.jsonToBlob,exports.leitner=t.leitner,exports.loadPluginFromHtml=e.loadPluginFromHtml,exports.loadPluginFromUrl=e.loadPluginFromUrl,exports.mountPlugin=i.mountPlugin,exports.prepareSetForPlugin=e.prepareSetForPlugin,exports.relayWebSocketUrl=a.relayWebSocketUrl,exports.serializeError=o.serializeError;
package/dist/index.js ADDED
@@ -0,0 +1,10 @@
1
+ import { createEmptySet as e, loadPluginFromHtml as t, loadPluginFromUrl as n, prepareSetForPlugin as r } from "./plugin.js";
2
+ import { LEITNER_INTERVAL_DAYS as i, leitner as a } from "./learning.js";
3
+ import { MemoryStorage as o } from "./storage.js";
4
+ import { LocalSession as s, SETTINGS_ADDRESS as c } from "./session.js";
5
+ import { PLUGIN_SANDBOX as l, createPluginFrame as u, mountPlugin as d } from "./frame.js";
6
+ import { RelaySocket as f, relayWebSocketUrl as p } from "./relay/socket.js";
7
+ import { blobToJson as m, isSerializedBlob as h, jsonToBlob as g, serializeError as _ } from "./relay/messages.js";
8
+ import { RelayHost as v } from "./relay/host.js";
9
+ import { RelayPlayer as y } from "./relay/player.js";
10
+ export { i as LEITNER_INTERVAL_DAYS, s as LocalSession, o as MemoryStorage, l as PLUGIN_SANDBOX, v as RelayHost, y as RelayPlayer, f as RelaySocket, c as SETTINGS_ADDRESS, m as blobToJson, e as createEmptySet, u as createPluginFrame, h as isSerializedBlob, g as jsonToBlob, a as leitner, t as loadPluginFromHtml, n as loadPluginFromUrl, d as mountPlugin, r as prepareSetForPlugin, p as relayWebSocketUrl, _ as serializeError };
@@ -0,0 +1 @@
1
+ var e=[0,1,3,7,21],t={id:`leitner`,apply(t,n,r){let i=t?.stats??{attempts:0,incorrect:0,streak:0},a={isCorrect:!n.isSkipped&&n.isCorrect,answeredAt:r.toISOString(),...n.confidence!==void 0&&{confidence:n.confidence},...n.timeSpentMs!==void 0&&{timeSpent:n.timeSpentMs},...n.hintsUsed!==void 0&&{hintsUsed:n.hintsUsed},...n.isSkipped&&{isSkipped:!0}};if(n.isSkipped)return{...t??{bucket:0},stats:i,lastAnswer:a};let o=n.isCorrect?Math.min(4,Math.max(1,t?.bucket??0)+1):1,s=new Date(r.getTime()+e[o]*24*60*60*1e3);return{...t,bucket:o,nextReviewAt:s.toISOString(),stats:{attempts:i.attempts+1,incorrect:i.incorrect+ +!n.isCorrect,streak:n.isCorrect?i.streak+1:0},lastAnswer:a}}};exports.LEITNER_INTERVAL_DAYS=e,exports.leitner=t;
@@ -0,0 +1,43 @@
1
+ //#region src/learning.ts
2
+ var e = [
3
+ 0,
4
+ 1,
5
+ 3,
6
+ 7,
7
+ 21
8
+ ], t = {
9
+ id: "leitner",
10
+ apply(t, n, r) {
11
+ let i = t?.stats ?? {
12
+ attempts: 0,
13
+ incorrect: 0,
14
+ streak: 0
15
+ }, a = {
16
+ isCorrect: !n.isSkipped && n.isCorrect,
17
+ answeredAt: r.toISOString(),
18
+ ...n.confidence !== void 0 && { confidence: n.confidence },
19
+ ...n.timeSpentMs !== void 0 && { timeSpent: n.timeSpentMs },
20
+ ...n.hintsUsed !== void 0 && { hintsUsed: n.hintsUsed },
21
+ ...n.isSkipped && { isSkipped: !0 }
22
+ };
23
+ if (n.isSkipped) return {
24
+ ...t ?? { bucket: 0 },
25
+ stats: i,
26
+ lastAnswer: a
27
+ };
28
+ let o = n.isCorrect ? Math.min(4, Math.max(1, t?.bucket ?? 0) + 1) : 1, s = new Date(r.getTime() + e[o] * 24 * 60 * 60 * 1e3);
29
+ return {
30
+ ...t,
31
+ bucket: o,
32
+ nextReviewAt: s.toISOString(),
33
+ stats: {
34
+ attempts: i.attempts + 1,
35
+ incorrect: i.incorrect + +!n.isCorrect,
36
+ streak: n.isCorrect ? i.streak + 1 : 0
37
+ },
38
+ lastAnswer: a
39
+ };
40
+ }
41
+ };
42
+ //#endregion
43
+ export { e as LEITNER_INTERVAL_DAYS, t as leitner };
@@ -0,0 +1 @@
1
+ let e=require("@memizy/protocol"),t=require("@memizy/oqse");function n(t){let n=(0,e.readPluginManifestFromHtml)(t);return n.success?(0,e.isProtocolSupported)(n.runtime.protocol)?{success:!0,plugin:{html:t,manifest:n.manifest,runtime:n.runtime}}:{success:!1,errors:[`The plugin needs Memizy Plugin Protocol ${n.runtime.protocol}, which this host does not support.`]}:n}async function r(e,t=fetch){let r=await t(e);return r.ok?n(await r.text()):{success:!1,errors:[`Could not download the plugin (${r.status} ${r.statusText}).`]}}function i(n,r,i={}){let a=r.capabilities?.types??[],o=a.includes(`*`)?n.items:n.items.filter(e=>a.includes(e.type)),s=(0,e.prepareDisplaySet)({meta:n.meta,items:o},i.seed);return{set:s.set,publicSet:s.publicSet,compatibility:(0,t.checkCompatibility)(n,r),skippedItems:n.items.length-o.length}}function a(e){let n=new Date().toISOString();return{version:t.OQSE_VERSION,meta:{id:(0,t.generateUUID)(),title:e.title,language:e.language,createdAt:n,updatedAt:n},items:[]}}exports.createEmptySet=a,exports.loadPluginFromHtml=n,exports.loadPluginFromUrl=r,exports.prepareSetForPlugin=i;
package/dist/plugin.js ADDED
@@ -0,0 +1,52 @@
1
+ import { isProtocolSupported as e, prepareDisplaySet as t, readPluginManifestFromHtml as n } from "@memizy/protocol";
2
+ import { OQSE_VERSION as r, checkCompatibility as i, generateUUID as a } from "@memizy/oqse";
3
+ //#region src/plugin.ts
4
+ function o(t) {
5
+ let r = n(t);
6
+ return r.success ? e(r.runtime.protocol) ? {
7
+ success: !0,
8
+ plugin: {
9
+ html: t,
10
+ manifest: r.manifest,
11
+ runtime: r.runtime
12
+ }
13
+ } : {
14
+ success: !1,
15
+ errors: [`The plugin needs Memizy Plugin Protocol ${r.runtime.protocol}, which this host does not support.`]
16
+ } : r;
17
+ }
18
+ async function s(e, t = fetch) {
19
+ let n = await t(e);
20
+ return n.ok ? o(await n.text()) : {
21
+ success: !1,
22
+ errors: [`Could not download the plugin (${n.status} ${n.statusText}).`]
23
+ };
24
+ }
25
+ function c(e, n, r = {}) {
26
+ let a = n.capabilities?.types ?? [], o = a.includes("*") ? e.items : e.items.filter((e) => a.includes(e.type)), s = t({
27
+ meta: e.meta,
28
+ items: o
29
+ }, r.seed);
30
+ return {
31
+ set: s.set,
32
+ publicSet: s.publicSet,
33
+ compatibility: i(e, n),
34
+ skippedItems: e.items.length - o.length
35
+ };
36
+ }
37
+ function l(e) {
38
+ let t = (/* @__PURE__ */ new Date()).toISOString();
39
+ return {
40
+ version: r,
41
+ meta: {
42
+ id: a(),
43
+ title: e.title,
44
+ language: e.language,
45
+ createdAt: t,
46
+ updatedAt: t
47
+ },
48
+ items: []
49
+ };
50
+ }
51
+ //#endregion
52
+ export { l as createEmptySet, o as loadPluginFromHtml, s as loadPluginFromUrl, c as prepareSetForPlugin };
@@ -0,0 +1 @@
1
+ const e=require("./socket.cjs"),t=require("./messages.cjs");let n=require("@memizy/protocol");var r=new Set(n.HOST_API_METHODS),i=class i{pin;hostToken;players=[];state={phase:`lobby`,appName:null,players:[],authorityConnected:!0};options;socket;listeners=new Set;channels=new Map;bundleVersion=0;genCounter=0;callCounter=0;session=null;hosting=!1;hostingWaiters=[];stopSession=null;constructor(t,r){this.options=t,this.pin=r.pin,this.hostToken=r.hostToken,this.socket=new e.RelaySocket({url:e.relayWebSocketUrl(t.serverUrl),attach:()=>({t:`host`,protocol:n.RELAY_PROTOCOL,pin:this.pin,hostToken:this.hostToken}),WebSocket:t.WebSocket}),this.socket.onStatus(e=>this.emit({type:`status`,status:e})),this.socket.onMessage(e=>this.onServer(e))}static async create(e){let t=await(e.fetch??fetch)(new URL(`/api/rooms`,e.serverUrl),{method:`POST`});if(!t.ok)throw Error(`Could not create a room (${t.status}): ${await t.text()}`);return new i(e,await t.json())}static resume(e,t){return new i(e,t)}whenHosting(){return this.hosting?Promise.resolve():new Promise(e=>this.hostingWaiters.push(e))}get status(){return this.socket.status}on(e){return this.listeners.add(e),()=>this.listeners.delete(e)}emit(e){for(let t of this.listeners)t(e)}async uploadBundle(e,t){let n=await(this.options.fetch??fetch)(new URL(`/api/rooms/${this.pin}/bundle`,this.options.serverUrl),{method:`PUT`,headers:{Authorization:`Bearer ${this.hostToken}`,"Content-Type":`application/json`},body:JSON.stringify({pluginHtml:e,set:t})});if(!n.ok)throw Error(`Could not upload the game (${n.status}): ${await n.text()}`);return this.bundleVersion=(await n.json()).version,this.bundleVersion}setOpen(e){this.socket.send({t:`open`,open:e})}kick(e){this.socket.send({t:`kick`,playerId:e})}close(){this.detach(),this.socket.send({t:`close`}),this.socket.close()}disconnect(){this.detach(),this.socket.close()}setState(e){this.state={...this.state,...e},this.broadcast({k:`state`,state:this.state})}attach(e){this.detach(),this.session=e;let t=new Set(this.players.map(e=>e.id));for(let t of this.players)!t.connected&&e.players.some(e=>e.id===t.id)&&e.setConnected(t.id,!1);for(let n of e.players)t.has(n.id)&&this.connectRemote(n.id);this.stopSession=e.on(e=>this.onSessionEvent(e)),this.setState({phase:`loading`,appName:e.plugin.manifest.appName,players:e.players,authorityConnected:!0,result:void 0,countdown:void 0})}detach(){this.stopSession?.(),this.stopSession=null,this.session=null;for(let[e,t]of this.channels)this.resetChannel(e,t);this.broadcast({k:`unmount`})}onSessionEvent(e){switch(e.type){case`countdown`:this.setState({phase:`countdown`,countdown:e.secondsLeft});break;case`started`:this.setState({phase:`running`,countdown:void 0});break;case`players`:this.setState({players:e.players});break;case`authority`:this.setState({authorityConnected:e.connected});break;case`ended`:this.setState({phase:`ended`,result:e.result})}}connectRemote(e){let t=this.session;t&&t.connect(e,this.endpointFor(e)).catch(()=>{})}endpointFor(e){return t=>new Promise((n,r)=>{let i=this.channel(e);this.resetChannel(e,i);let a=++this.genCounter;i.gen=a,i.hostApi=t,i.pendingMount={gen:a,resolve:()=>n(this.remotePlugin(e,a)),reject:r,timer:setTimeout(()=>{i.pendingMount?.gen===a&&(i.pendingMount=null,r(Error(`The device of player ${e} did not load the game in time.`)))},this.options.mountTimeoutMs??2e4)},this.sendMount(e,a)})}sendMount(e,t){this.sendTo(e,{k:`mount`,gen:t,bundle:this.bundleVersion,locale:this.session?.config.locale??`cs`})}remotePlugin(e,t){let r={};for(let i of n.PLUGIN_API_METHODS)r[i]=(...n)=>this.callRemote(e,t,i,n);return r}callRemote(e,t,n,r){let i=this.channels.get(e);if(!i||i.gen!==t)return Promise.reject(Error(`[SESSION_ENDED] The player instance was replaced.`));if(!this.players.find(t=>t.id===e)?.connected)return Promise.resolve();let a=++this.callCounter;return new Promise((o,s)=>{let c=setTimeout(()=>{i.calls.delete(a),s(Error(`[INTERNAL_ERROR] ${n} on ${e} timed out.`))},this.options.callTimeoutMs??15e3);i.calls.set(a,{resolve:o,reject:s,timer:c}),this.sendTo(e,{k:`call`,id:a,gen:t,method:n,args:r})})}channel(e){let t=this.channels.get(e);return t||(t={gen:0,hostApi:null,pendingMount:null,calls:new Map},this.channels.set(e,t)),t}resetChannel(e,t){t.pendingMount&&=(clearTimeout(t.pendingMount.timer),t.pendingMount.reject(Error(`[SESSION_ENDED] Replaced.`)),null),this.settleCalls(t),t.gen=0,t.hostApi=null}settleCalls(e){for(let t of e.calls.values())clearTimeout(t.timer),t.resolve(void 0);e.calls.clear()}onServer(e){switch(e.t){case`hosting`:this.hosting=!0,this.hostingWaiters.splice(0).forEach(e=>e()),this.players=e.players,this.emit({type:`players`,players:this.players}),this.broadcast({k:`state`,state:this.state});break;case`presence`:this.onPresence(e.player,e.event);break;case`from`:this.onPlayer(e.from,e.data);break;case`error`:this.emit({type:`error`,code:e.code,message:e.message});break;case`closed`:this.emit({type:`closed`,reason:e.reason})}}onPresence(e,t){this.players=t===`leave`?this.players.filter(t=>t.id!==e.id):this.players.some(t=>t.id===e.id)?this.players.map(t=>t.id===e.id?e:t):[...this.players,e],this.emit({type:`players`,players:this.players});let n=this.session,r=n?.players.some(t=>t.id===e.id)??!1;switch(t){case`join`:if(n&&!n.ended&&!r)try{n.addPlayer({id:e.id,name:e.name}),this.connectRemote(e.id)}catch{}break;case`disconnect`:{let t=this.channels.get(e.id);t&&this.settleCalls(t),r&&n.setConnected(e.id,!1);break}case`rename`:r&&n.renamePlayer(e.id,e.name);break;case`leave`:if(this.channels.delete(e.id),r)try{n.removePlayer(e.id)}catch{}}}async onPlayer(e,n){if(typeof n!=`object`||!n)return;let i=this.channel(e);switch(n.k){case`hello`:{this.sendTo(e,{k:`state`,state:this.state});let t=this.session;if(!t||!t.players.some(t=>t.id===e))return;if(i.pendingMount){this.sendMount(e,i.pendingMount.gen);return}t.setConnected(e,!0),(i.gen===0||n.gen!==i.gen)&&(t.getInstance(e)?t.reload(e).catch(()=>{}):this.connectRemote(e));return}case`mounted`:if(i.pendingMount?.gen===n.gen){clearTimeout(i.pendingMount.timer);let e=i.pendingMount;i.pendingMount=null,e.resolve()}return;case`mount-failed`:if(i.pendingMount?.gen===n.gen){clearTimeout(i.pendingMount.timer);let e=i.pendingMount;i.pendingMount=null,e.reject(Error(n.error?.message??`The game could not be loaded on the device.`))}return;case`result`:{let e=i.calls.get(n.id);if(!e)return;i.calls.delete(n.id),clearTimeout(e.timer),n.ok?e.resolve(n.value):e.reject(Error(n.error?.message??`Remote call failed`));return}case`call`:{let a=t=>this.sendTo(e,t);if(n.gen!==i.gen||!i.hostApi||!r.has(n.method)||!Array.isArray(n.args)){a({k:`result`,id:n.id,ok:!1,error:{message:`[SESSION_ENDED] This plugin instance is no longer active.`}});return}try{let e=i.hostApi[n.method],r=await e(...n.args);if(n.method===`hello`){r={...r,set:null},a({k:`result`,id:n.id,ok:!0,value:r,hostNow:Date.now()});return}n.method===`getAsset`&&r instanceof Blob&&(r=await t.blobToJson(r)),a({k:`result`,id:n.id,ok:!0,value:r??null})}catch(e){a({k:`result`,id:n.id,ok:!1,error:t.serializeError(e)})}return}}}sendTo(e,t){this.socket.send({t:`to`,to:e,data:t})}broadcast(e){for(let t of this.players)t.connected&&this.sendTo(t.id,e)}};exports.RelayHost=i;
@@ -0,0 +1,370 @@
1
+ import { RelaySocket as e, relayWebSocketUrl as t } from "./socket.js";
2
+ import { blobToJson as n, serializeError as r } from "./messages.js";
3
+ import { HOST_API_METHODS as i, PLUGIN_API_METHODS as a, RELAY_PROTOCOL as o } from "@memizy/protocol";
4
+ //#region src/relay/host.ts
5
+ var s = new Set(i), c = class i {
6
+ pin;
7
+ hostToken;
8
+ players = [];
9
+ state = {
10
+ phase: "lobby",
11
+ appName: null,
12
+ players: [],
13
+ authorityConnected: !0
14
+ };
15
+ options;
16
+ socket;
17
+ listeners = /* @__PURE__ */ new Set();
18
+ channels = /* @__PURE__ */ new Map();
19
+ bundleVersion = 0;
20
+ genCounter = 0;
21
+ callCounter = 0;
22
+ session = null;
23
+ hosting = !1;
24
+ hostingWaiters = [];
25
+ stopSession = null;
26
+ constructor(n, r) {
27
+ this.options = n, this.pin = r.pin, this.hostToken = r.hostToken, this.socket = new e({
28
+ url: t(n.serverUrl),
29
+ attach: () => ({
30
+ t: "host",
31
+ protocol: o,
32
+ pin: this.pin,
33
+ hostToken: this.hostToken
34
+ }),
35
+ WebSocket: n.WebSocket
36
+ }), this.socket.onStatus((e) => this.emit({
37
+ type: "status",
38
+ status: e
39
+ })), this.socket.onMessage((e) => this.onServer(e));
40
+ }
41
+ static async create(e) {
42
+ let t = await (e.fetch ?? fetch)(new URL("/api/rooms", e.serverUrl), { method: "POST" });
43
+ if (!t.ok) throw Error(`Could not create a room (${t.status}): ${await t.text()}`);
44
+ return new i(e, await t.json());
45
+ }
46
+ static resume(e, t) {
47
+ return new i(e, t);
48
+ }
49
+ whenHosting() {
50
+ return this.hosting ? Promise.resolve() : new Promise((e) => this.hostingWaiters.push(e));
51
+ }
52
+ get status() {
53
+ return this.socket.status;
54
+ }
55
+ on(e) {
56
+ return this.listeners.add(e), () => this.listeners.delete(e);
57
+ }
58
+ emit(e) {
59
+ for (let t of this.listeners) t(e);
60
+ }
61
+ async uploadBundle(e, t) {
62
+ let n = await (this.options.fetch ?? fetch)(new URL(`/api/rooms/${this.pin}/bundle`, this.options.serverUrl), {
63
+ method: "PUT",
64
+ headers: {
65
+ Authorization: `Bearer ${this.hostToken}`,
66
+ "Content-Type": "application/json"
67
+ },
68
+ body: JSON.stringify({
69
+ pluginHtml: e,
70
+ set: t
71
+ })
72
+ });
73
+ if (!n.ok) throw Error(`Could not upload the game (${n.status}): ${await n.text()}`);
74
+ return this.bundleVersion = (await n.json()).version, this.bundleVersion;
75
+ }
76
+ setOpen(e) {
77
+ this.socket.send({
78
+ t: "open",
79
+ open: e
80
+ });
81
+ }
82
+ kick(e) {
83
+ this.socket.send({
84
+ t: "kick",
85
+ playerId: e
86
+ });
87
+ }
88
+ close() {
89
+ this.detach(), this.socket.send({ t: "close" }), this.socket.close();
90
+ }
91
+ disconnect() {
92
+ this.detach(), this.socket.close();
93
+ }
94
+ setState(e) {
95
+ this.state = {
96
+ ...this.state,
97
+ ...e
98
+ }, this.broadcast({
99
+ k: "state",
100
+ state: this.state
101
+ });
102
+ }
103
+ attach(e) {
104
+ this.detach(), this.session = e;
105
+ let t = new Set(this.players.map((e) => e.id));
106
+ for (let t of this.players) !t.connected && e.players.some((e) => e.id === t.id) && e.setConnected(t.id, !1);
107
+ for (let n of e.players) t.has(n.id) && this.connectRemote(n.id);
108
+ this.stopSession = e.on((e) => this.onSessionEvent(e)), this.setState({
109
+ phase: "loading",
110
+ appName: e.plugin.manifest.appName,
111
+ players: e.players,
112
+ authorityConnected: !0,
113
+ result: void 0,
114
+ countdown: void 0
115
+ });
116
+ }
117
+ detach() {
118
+ this.stopSession?.(), this.stopSession = null, this.session = null;
119
+ for (let [e, t] of this.channels) this.resetChannel(e, t);
120
+ this.broadcast({ k: "unmount" });
121
+ }
122
+ onSessionEvent(e) {
123
+ switch (e.type) {
124
+ case "countdown":
125
+ this.setState({
126
+ phase: "countdown",
127
+ countdown: e.secondsLeft
128
+ });
129
+ break;
130
+ case "started":
131
+ this.setState({
132
+ phase: "running",
133
+ countdown: void 0
134
+ });
135
+ break;
136
+ case "players":
137
+ this.setState({ players: e.players });
138
+ break;
139
+ case "authority":
140
+ this.setState({ authorityConnected: e.connected });
141
+ break;
142
+ case "ended": this.setState({
143
+ phase: "ended",
144
+ result: e.result
145
+ });
146
+ }
147
+ }
148
+ connectRemote(e) {
149
+ let t = this.session;
150
+ t && t.connect(e, this.endpointFor(e)).catch(() => {});
151
+ }
152
+ endpointFor(e) {
153
+ return (t) => new Promise((n, r) => {
154
+ let i = this.channel(e);
155
+ this.resetChannel(e, i);
156
+ let a = ++this.genCounter;
157
+ i.gen = a, i.hostApi = t, i.pendingMount = {
158
+ gen: a,
159
+ resolve: () => n(this.remotePlugin(e, a)),
160
+ reject: r,
161
+ timer: setTimeout(() => {
162
+ i.pendingMount?.gen === a && (i.pendingMount = null, r(/* @__PURE__ */ Error(`The device of player ${e} did not load the game in time.`)));
163
+ }, this.options.mountTimeoutMs ?? 2e4)
164
+ }, this.sendMount(e, a);
165
+ });
166
+ }
167
+ sendMount(e, t) {
168
+ this.sendTo(e, {
169
+ k: "mount",
170
+ gen: t,
171
+ bundle: this.bundleVersion,
172
+ locale: this.session?.config.locale ?? "cs"
173
+ });
174
+ }
175
+ remotePlugin(e, t) {
176
+ let n = {};
177
+ for (let r of a) n[r] = (...n) => this.callRemote(e, t, r, n);
178
+ return n;
179
+ }
180
+ callRemote(e, t, n, r) {
181
+ let i = this.channels.get(e);
182
+ if (!i || i.gen !== t) return Promise.reject(/* @__PURE__ */ Error("[SESSION_ENDED] The player instance was replaced."));
183
+ if (!this.players.find((t) => t.id === e)?.connected) return Promise.resolve();
184
+ let a = ++this.callCounter;
185
+ return new Promise((o, s) => {
186
+ let c = setTimeout(() => {
187
+ i.calls.delete(a), s(/* @__PURE__ */ Error(`[INTERNAL_ERROR] ${n} on ${e} timed out.`));
188
+ }, this.options.callTimeoutMs ?? 15e3);
189
+ i.calls.set(a, {
190
+ resolve: o,
191
+ reject: s,
192
+ timer: c
193
+ }), this.sendTo(e, {
194
+ k: "call",
195
+ id: a,
196
+ gen: t,
197
+ method: n,
198
+ args: r
199
+ });
200
+ });
201
+ }
202
+ channel(e) {
203
+ let t = this.channels.get(e);
204
+ return t || (t = {
205
+ gen: 0,
206
+ hostApi: null,
207
+ pendingMount: null,
208
+ calls: /* @__PURE__ */ new Map()
209
+ }, this.channels.set(e, t)), t;
210
+ }
211
+ resetChannel(e, t) {
212
+ t.pendingMount &&= (clearTimeout(t.pendingMount.timer), t.pendingMount.reject(/* @__PURE__ */ Error("[SESSION_ENDED] Replaced.")), null), this.settleCalls(t), t.gen = 0, t.hostApi = null;
213
+ }
214
+ settleCalls(e) {
215
+ for (let t of e.calls.values()) clearTimeout(t.timer), t.resolve(void 0);
216
+ e.calls.clear();
217
+ }
218
+ onServer(e) {
219
+ switch (e.t) {
220
+ case "hosting":
221
+ this.hosting = !0, this.hostingWaiters.splice(0).forEach((e) => e()), this.players = e.players, this.emit({
222
+ type: "players",
223
+ players: this.players
224
+ }), this.broadcast({
225
+ k: "state",
226
+ state: this.state
227
+ });
228
+ break;
229
+ case "presence":
230
+ this.onPresence(e.player, e.event);
231
+ break;
232
+ case "from":
233
+ this.onPlayer(e.from, e.data);
234
+ break;
235
+ case "error":
236
+ this.emit({
237
+ type: "error",
238
+ code: e.code,
239
+ message: e.message
240
+ });
241
+ break;
242
+ case "closed": this.emit({
243
+ type: "closed",
244
+ reason: e.reason
245
+ });
246
+ }
247
+ }
248
+ onPresence(e, t) {
249
+ this.players = t === "leave" ? this.players.filter((t) => t.id !== e.id) : this.players.some((t) => t.id === e.id) ? this.players.map((t) => t.id === e.id ? e : t) : [...this.players, e], this.emit({
250
+ type: "players",
251
+ players: this.players
252
+ });
253
+ let n = this.session, r = n?.players.some((t) => t.id === e.id) ?? !1;
254
+ switch (t) {
255
+ case "join":
256
+ if (n && !n.ended && !r) try {
257
+ n.addPlayer({
258
+ id: e.id,
259
+ name: e.name
260
+ }), this.connectRemote(e.id);
261
+ } catch {}
262
+ break;
263
+ case "disconnect": {
264
+ let t = this.channels.get(e.id);
265
+ t && this.settleCalls(t), r && n.setConnected(e.id, !1);
266
+ break;
267
+ }
268
+ case "rename":
269
+ r && n.renamePlayer(e.id, e.name);
270
+ break;
271
+ case "leave": if (this.channels.delete(e.id), r) try {
272
+ n.removePlayer(e.id);
273
+ } catch {}
274
+ }
275
+ }
276
+ async onPlayer(e, t) {
277
+ if (typeof t != "object" || !t) return;
278
+ let i = this.channel(e);
279
+ switch (t.k) {
280
+ case "hello": {
281
+ this.sendTo(e, {
282
+ k: "state",
283
+ state: this.state
284
+ });
285
+ let n = this.session;
286
+ if (!n || !n.players.some((t) => t.id === e)) return;
287
+ if (i.pendingMount) {
288
+ this.sendMount(e, i.pendingMount.gen);
289
+ return;
290
+ }
291
+ n.setConnected(e, !0), (i.gen === 0 || t.gen !== i.gen) && (n.getInstance(e) ? n.reload(e).catch(() => {}) : this.connectRemote(e));
292
+ return;
293
+ }
294
+ case "mounted":
295
+ if (i.pendingMount?.gen === t.gen) {
296
+ clearTimeout(i.pendingMount.timer);
297
+ let e = i.pendingMount;
298
+ i.pendingMount = null, e.resolve();
299
+ }
300
+ return;
301
+ case "mount-failed":
302
+ if (i.pendingMount?.gen === t.gen) {
303
+ clearTimeout(i.pendingMount.timer);
304
+ let e = i.pendingMount;
305
+ i.pendingMount = null, e.reject(Error(t.error?.message ?? "The game could not be loaded on the device."));
306
+ }
307
+ return;
308
+ case "result": {
309
+ let e = i.calls.get(t.id);
310
+ if (!e) return;
311
+ i.calls.delete(t.id), clearTimeout(e.timer), t.ok ? e.resolve(t.value) : e.reject(Error(t.error?.message ?? "Remote call failed"));
312
+ return;
313
+ }
314
+ case "call": {
315
+ let a = (t) => this.sendTo(e, t);
316
+ if (t.gen !== i.gen || !i.hostApi || !s.has(t.method) || !Array.isArray(t.args)) {
317
+ a({
318
+ k: "result",
319
+ id: t.id,
320
+ ok: !1,
321
+ error: { message: "[SESSION_ENDED] This plugin instance is no longer active." }
322
+ });
323
+ return;
324
+ }
325
+ try {
326
+ let e = i.hostApi[t.method], r = await e(...t.args);
327
+ if (t.method === "hello") {
328
+ r = {
329
+ ...r,
330
+ set: null
331
+ }, a({
332
+ k: "result",
333
+ id: t.id,
334
+ ok: !0,
335
+ value: r,
336
+ hostNow: Date.now()
337
+ });
338
+ return;
339
+ }
340
+ t.method === "getAsset" && r instanceof Blob && (r = await n(r)), a({
341
+ k: "result",
342
+ id: t.id,
343
+ ok: !0,
344
+ value: r ?? null
345
+ });
346
+ } catch (e) {
347
+ a({
348
+ k: "result",
349
+ id: t.id,
350
+ ok: !1,
351
+ error: r(e)
352
+ });
353
+ }
354
+ return;
355
+ }
356
+ }
357
+ }
358
+ sendTo(e, t) {
359
+ this.socket.send({
360
+ t: "to",
361
+ to: e,
362
+ data: t
363
+ });
364
+ }
365
+ broadcast(e) {
366
+ for (let t of this.players) t.connected && this.sendTo(t.id, e);
367
+ }
368
+ };
369
+ //#endregion
370
+ export { c as RelayHost };
@@ -0,0 +1 @@
1
+ function e(e){return{message:e instanceof Error?e.message:String(e)}}async function t(e){let t=new Uint8Array(await e.arrayBuffer()),n=``;for(let e=0;e<t.length;e+=32768)n+=String.fromCharCode(...t.subarray(e,e+32768));return{$blob:btoa(n),type:e.type}}function n(e){let t=atob(e.$blob),n=new Uint8Array(t.length);for(let e=0;e<t.length;e++)n[e]=t.charCodeAt(e);return new Blob([n],{type:e.type})}function r(e){return typeof e==`object`&&!!e&&typeof e.$blob==`string`}exports.blobToJson=t,exports.isSerializedBlob=r,exports.jsonToBlob=n,exports.serializeError=e;
@@ -0,0 +1,22 @@
1
+ //#region src/relay/messages.ts
2
+ function e(e) {
3
+ return { message: e instanceof Error ? e.message : String(e) };
4
+ }
5
+ async function t(e) {
6
+ let t = new Uint8Array(await e.arrayBuffer()), n = "";
7
+ for (let e = 0; e < t.length; e += 32768) n += String.fromCharCode(...t.subarray(e, e + 32768));
8
+ return {
9
+ $blob: btoa(n),
10
+ type: e.type
11
+ };
12
+ }
13
+ function n(e) {
14
+ let t = atob(e.$blob), n = new Uint8Array(t.length);
15
+ for (let e = 0; e < t.length; e++) n[e] = t.charCodeAt(e);
16
+ return new Blob([n], { type: e.type });
17
+ }
18
+ function r(e) {
19
+ return typeof e == "object" && !!e && typeof e.$blob == "string";
20
+ }
21
+ //#endregion
22
+ export { t as blobToJson, r as isSerializedBlob, n as jsonToBlob, e as serializeError };
@@ -0,0 +1 @@
1
+ const e=require("../frame.cjs"),t=require("./socket.cjs"),n=require("./messages.cjs");let r=require("@memizy/protocol");var i=new Set(r.PLUGIN_API_METHODS),a=class{playerId=null;token;name;state=null;hostConnected=!0;options;socket;listeners=new Set;bundle=null;frame=null;plugin=null;gen=0;callCounter=0;pending=new Map;skewMs=0;hostConfig=null;mountQueue=Promise.resolve();constructor(e){this.options=e,this.token=e.token??null,this.name=e.name,this.socket=new t.RelaySocket({url:t.relayWebSocketUrl(e.serverUrl),attach:()=>({t:`join`,protocol:r.RELAY_PROTOCOL,pin:e.pin,name:this.name,token:this.token??void 0}),WebSocket:e.WebSocket}),this.socket.onStatus(e=>this.emit({type:`status`,status:e})),this.socket.onMessage(e=>this.onServer(e))}get status(){return this.socket.status}on(e){return this.listeners.add(e),()=>this.listeners.delete(e)}emit(e){for(let t of this.listeners)t(e)}updateConfig(){this.plugin&&this.hostConfig&&this.plugin.configChanged(this.localConfig(this.hostConfig)).catch(()=>{})}localConfig(e){return{...e,...this.options.config?.()}}rename(e){this.name=e,this.socket.send({t:`rename`,name:e})}reconnectNow(){this.socket.reconnectNow()}leave(){this.destroyFrame(),this.socket.close()}onServer(e){switch(e.t){case`joined`:this.playerId=e.playerId,this.token=e.token,this.name=e.name,this.hostConnected=e.hostConnected,this.emit({type:`joined`,playerId:e.playerId,token:e.token,name:e.name}),this.up({k:`hello`,gen:this.plugin?this.gen:0});break;case`host-presence`:this.hostConnected=e.connected,this.emit({type:`host`,connected:e.connected}),e.connected&&this.up({k:`hello`,gen:this.plugin?this.gen:0});break;case`data`:this.onHost(e.data);break;case`error`:this.emit({type:`error`,code:e.code,message:e.message,fatal:e.fatal});break;case`closed`:this.destroyFrame(),this.emit({type:`closed`,reason:e.reason})}}up(e){return this.socket.send({t:`up`,data:e})}async onHost(e){if(typeof e==`object`&&e)switch(e.k){case`state`:this.state=e.state,this.emit({type:`state`,state:e.state});return;case`mount`:this.mountQueue=this.mountQueue.then(()=>this.mount(e.gen,e.bundle));return;case`unmount`:this.destroyFrame();return;case`result`:{let t=this.pending.get(e.id);if(!t)return;if(this.pending.delete(e.id),clearTimeout(t.timer),!e.ok){t.reject(Error(e.error?.message??`Host call failed`));return}let r=e.value;if(t.method===`hello`&&this.bundle){if(e.hostNow!==void 0){let n=Date.now();this.skewMs=e.hostNow+(n-t.sentAt)/2-n}let n=r;this.hostConfig=n.config,r={...n,set:structuredClone(this.bundle.data.set),clock:{...n.clock,offsetMs:n.clock.offsetMs+this.skewMs},config:this.localConfig(n.config)}}else n.isSerializedBlob(r)&&(r=n.jsonToBlob(r));t.resolve(r);return}case`call`:{let t=e=>this.up(e);if(e.gen!==this.gen||!this.plugin||!i.has(e.method)){t({k:`result`,id:e.id,ok:!1,error:{message:`[SESSION_ENDED] No active plugin instance.`}});return}let r=e.args;if(e.method===`configChanged`&&(this.hostConfig=r[0],r=[this.localConfig(this.hostConfig)]),e.method===`clockChanged`){let e=r[0];r=[{...e,offsetMs:e.offsetMs+this.skewMs}]}try{let n=this.plugin[e.method],i=await n(...r);t({k:`result`,id:e.id,ok:!0,value:i??null})}catch(r){t({k:`result`,id:e.id,ok:!1,error:n.serializeError(r)})}return}}}async ensureBundle(e){if(this.bundle?.version===e)return this.bundle.data;let t=await(this.options.fetch??fetch)(new URL(`/api/rooms/${this.options.pin}/bundle`,this.options.serverUrl));if(!t.ok)throw Error(`Could not download the game (${t.status}).`);let n=await t.json();return this.bundle={version:Number(t.headers.get(`X-Bundle-Version`)??e),data:n},n}async mount(t,i){this.destroyFrame(),this.gen=t;try{let n=await this.ensureBundle(i);if(this.gen!==t)return;let a=this.options.container();if(!a)throw Error(`No place for the game on this page.`);let o=(0,r.extractManifestFromHtml)(n.pluginHtml),s=o.success?String(o.data.appName??`Game`):`Game`,c=this.options.transformHtml?this.options.transformHtml(n.pluginHtml):n.pluginHtml,l=(this.options.createFrame??e.createPluginFrame)(c,s,this.hostApiFor(t),a.ownerDocument);this.frame=l,a.replaceChildren(l.iframe),this.emit({type:`frame`,active:!0});let u=await l.plugin;if(this.gen!==t)return;this.plugin=u,this.up({k:`mounted`,gen:t})}catch(e){if(this.gen!==t)return;this.emit({type:`error`,code:`MOUNT_FAILED`,message:e.message,fatal:!1}),this.up({k:`mount-failed`,gen:t,error:n.serializeError(e)})}}hostApiFor(e){let t={};for(let n of r.HOST_API_METHODS)t[n]=(...t)=>{if(this.gen!==e)return Promise.reject(Error(`[SESSION_ENDED] This plugin instance was replaced.`));let r=++this.callCounter;return new Promise((i,a)=>{let o=setTimeout(()=>{this.pending.delete(r),a(Error(`[INTERNAL_ERROR] The host did not answer ${n} in time.`))},this.options.callTimeoutMs??15e3);this.pending.set(r,{method:n,sentAt:Date.now(),resolve:i,reject:a,timer:o}),this.up({k:`call`,id:r,gen:e,method:n,args:t})||(clearTimeout(o),this.pending.delete(r),n===`hello`?a(Error(`[INTERNAL_ERROR] Offline.`)):i(void 0))})};return t}destroyFrame(){let e=this.frame!==null;this.frame?.destroy(),this.frame?.iframe.remove(),this.frame=null,this.plugin=null,this.gen=0;for(let e of this.pending.values())clearTimeout(e.timer),e.reject(Error(`[SESSION_ENDED] The plugin instance was closed.`));this.pending.clear(),e&&this.emit({type:`frame`,active:!1})}};exports.RelayPlayer=a;