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

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.
@@ -0,0 +1,542 @@
1
+ import { RelaySocket as e, relayWebSocketUrl as t } from "./socket.js";
2
+ import { blobToJson as n, serializeError as r, unknownMethod as i } from "./messages.js";
3
+ import { HOST_API_METHODS as a, PLUGIN_API_METHODS as o, RELAY_PROTOCOL as s } from "@memizy/protocol";
4
+ import { RELAY_LIMITS as c, jsonByteSize as l } from "@memizy/protocol/internal";
5
+ //#region src/relay/host.ts
6
+ var u = new Set(a), d = class a {
7
+ pin;
8
+ hostToken;
9
+ players = [];
10
+ state = {
11
+ phase: "lobby",
12
+ appName: null,
13
+ players: [],
14
+ authorityConnected: !0
15
+ };
16
+ options;
17
+ socket;
18
+ listeners = /* @__PURE__ */ new Set();
19
+ channels = /* @__PURE__ */ new Map();
20
+ bundleVersion = 0;
21
+ genCounter = 0;
22
+ callCounter = 0;
23
+ session = null;
24
+ hosting = !1;
25
+ hostingWaiters = [];
26
+ stopSession = null;
27
+ seatOf = /* @__PURE__ */ new Map();
28
+ seatsMode = !1;
29
+ appVersions = /* @__PURE__ */ new Map();
30
+ constructor(n, r) {
31
+ this.options = n, this.pin = r.pin, this.hostToken = r.hostToken, this.socket = new e({
32
+ url: t(n.serverUrl),
33
+ attach: () => ({
34
+ t: "host",
35
+ protocol: s,
36
+ pin: this.pin,
37
+ hostToken: this.hostToken
38
+ }),
39
+ WebSocket: n.WebSocket
40
+ }), this.socket.onStatus((e) => this.emit({
41
+ type: "status",
42
+ status: e
43
+ })), this.socket.onMessage((e) => this.onServer(e));
44
+ }
45
+ static async create(e) {
46
+ let t = await (e.fetch ?? fetch)(new URL("/api/rooms", e.serverUrl), { method: "POST" });
47
+ if (!t.ok) throw Error(`Could not create a room (${t.status}): ${await t.text()}`);
48
+ return new a(e, await t.json());
49
+ }
50
+ static rejoin(e, t) {
51
+ return new a(e, t);
52
+ }
53
+ whenHosting() {
54
+ return this.hosting ? Promise.resolve() : new Promise((e) => this.hostingWaiters.push(e));
55
+ }
56
+ get status() {
57
+ return this.socket.status;
58
+ }
59
+ on(e) {
60
+ return this.listeners.add(e), () => this.listeners.delete(e);
61
+ }
62
+ emit(e) {
63
+ for (let t of this.listeners) t(e);
64
+ }
65
+ async uploadBundle(e, t) {
66
+ let n = t.publicSet, r = await (this.options.fetch ?? fetch)(new URL(`/api/rooms/${this.pin}/bundle`, this.options.serverUrl), {
67
+ method: "PUT",
68
+ headers: {
69
+ Authorization: `Bearer ${this.hostToken}`,
70
+ "Content-Type": "application/json"
71
+ },
72
+ body: JSON.stringify({
73
+ pluginHtml: e,
74
+ set: n
75
+ })
76
+ });
77
+ if (!r.ok) throw Error(`Could not upload the game (${r.status}): ${await r.text()}`);
78
+ return this.bundleVersion = (await r.json()).version, this.bundleVersion;
79
+ }
80
+ setOpen(e) {
81
+ this.socket.send({
82
+ t: "open",
83
+ open: e
84
+ });
85
+ }
86
+ kick(e) {
87
+ this.socket.send({
88
+ t: "kick",
89
+ playerId: e
90
+ });
91
+ }
92
+ close() {
93
+ this.detach(), this.socket.send({ t: "close" }), this.socket.close();
94
+ }
95
+ disconnect() {
96
+ this.detach(), this.socket.close();
97
+ }
98
+ setState(e) {
99
+ this.state = {
100
+ ...this.state,
101
+ ...e
102
+ }, this.broadcast({
103
+ k: "state",
104
+ state: this.state
105
+ });
106
+ }
107
+ attach(e, t = {}) {
108
+ this.detach(), this.session = e, this.seatsMode = t.seats === !0;
109
+ let n = (t) => e.players.some((e) => e.id === t && !e.isHost), r = (e) => this.players.some((t) => t.id === e);
110
+ if (this.seatsMode) for (let [e, i] of Object.entries(t.taken ?? {})) r(e) && n(i) && this.relayIdOf(i) === null && this.seatOf.set(e, i);
111
+ else for (let e of this.players) n(e.id) && this.seatOf.set(e.id, e.id);
112
+ for (let t of e.players) {
113
+ if (t.isHost) continue;
114
+ let n = this.relayIdOf(t.id);
115
+ this.players.find((e) => e.id === n)?.connected || e.setConnected(t.id, !1);
116
+ }
117
+ for (let e of this.seatOf.values()) this.connectRemote(e);
118
+ if (this.stopSession = e.on((e) => this.onSessionEvent(e)), this.setState({
119
+ phase: "loading",
120
+ appName: e.plugin.manifest.appName,
121
+ players: [...e.players],
122
+ authorityConnected: !0,
123
+ result: void 0,
124
+ countdown: void 0,
125
+ ...this.seatState()
126
+ }), this.seatsMode) for (let e of this.players) e.connected && this.sendSeat(e.id);
127
+ }
128
+ detach() {
129
+ this.stopSession?.(), this.stopSession = null, this.session = null;
130
+ for (let [e, t] of this.channels) this.resetChannel(e, t);
131
+ this.seatOf.clear(), this.seatsMode && (this.seatsMode = !1, this.state = {
132
+ ...this.state,
133
+ seats: void 0,
134
+ newPlayers: void 0
135
+ }, this.broadcast({
136
+ k: "state",
137
+ state: this.state
138
+ })), this.broadcast({ k: "unmount" });
139
+ }
140
+ get seats() {
141
+ let e = this.session;
142
+ return !this.seatsMode || !e ? [] : e.players.filter((e) => !e.isHost).map((e) => ({
143
+ id: e.id,
144
+ name: e.name,
145
+ roomPlayerId: this.relayIdOf(e.id)
146
+ }));
147
+ }
148
+ get seatAssignments() {
149
+ return Object.fromEntries(this.seatOf);
150
+ }
151
+ assignSeat(e, t) {
152
+ let n = this.session;
153
+ if (!this.seatsMode || !n) throw Error("Places can be assigned only in a continued game.");
154
+ if (!this.players.some((t) => t.id === e)) throw Error(`Unknown room player "${e}".`);
155
+ if (t !== null && !n.players.some((e) => e.id === t && !e.isHost)) throw Error(`Unknown place "${t}".`);
156
+ if ((this.seatOf.get(e) ?? null) !== t) {
157
+ if (this.unseat(e), t !== null) {
158
+ let n = this.relayIdOf(t);
159
+ n !== null && this.unseat(n), this.seat(e, t);
160
+ }
161
+ this.seatsChanged();
162
+ }
163
+ }
164
+ appVersionOf(e) {
165
+ return this.appVersions.get(e) ?? null;
166
+ }
167
+ relayIdOf(e) {
168
+ for (let [t, n] of this.seatOf) if (n === e) return t;
169
+ return null;
170
+ }
171
+ seatState() {
172
+ let e = this.session;
173
+ if (!this.seatsMode || !e) return {
174
+ seats: void 0,
175
+ newPlayers: void 0
176
+ };
177
+ let t = e.plugin.runtime.multiplayer;
178
+ return {
179
+ seats: this.seats.map((e) => ({
180
+ id: e.id,
181
+ name: e.name,
182
+ taken: e.roomPlayerId !== null
183
+ })),
184
+ newPlayers: !e.ended && !!t?.lateJoin && e.players.length < t.players.max
185
+ };
186
+ }
187
+ seatsChanged() {
188
+ this.seatsMode && (this.setState(this.seatState()), this.emit({
189
+ type: "seats",
190
+ seats: this.seats
191
+ }));
192
+ }
193
+ sendSeat(e) {
194
+ let t = this.seatOf.get(e) ?? null, n = t === null ? void 0 : this.session?.players.find((e) => e.id === t)?.name;
195
+ this.sendTo(e, {
196
+ k: "seat",
197
+ seat: t,
198
+ ...n !== void 0 && { name: n }
199
+ });
200
+ }
201
+ seat(e, t) {
202
+ this.seatOf.set(e, t);
203
+ let n = this.players.find((t) => t.id === e)?.connected ?? !1;
204
+ this.session?.setConnected(t, n), n && this.connectRemote(t), this.sendSeat(e);
205
+ }
206
+ unseat(e) {
207
+ let t = this.seatOf.get(e);
208
+ if (t === void 0) return;
209
+ this.seatOf.delete(e);
210
+ let n = this.channels.get(t);
211
+ n && this.resetChannel(t, n), this.channels.delete(t), this.session?.disconnect(t), this.session?.setConnected(t, !1), this.sendTo(e, { k: "unmount" }), this.sendSeat(e);
212
+ }
213
+ claim(e, t) {
214
+ let n = this.session, r = this.players.find((t) => t.id === e);
215
+ if (!this.seatsMode || !n || n.ended || !r || this.seatOf.has(e)) {
216
+ this.sendSeat(e);
217
+ return;
218
+ }
219
+ if (t === null) {
220
+ try {
221
+ n.addPlayer({
222
+ id: e,
223
+ name: r.name
224
+ });
225
+ } catch {
226
+ this.sendSeat(e);
227
+ return;
228
+ }
229
+ this.seat(e, e);
230
+ } else if (typeof t == "string" && n.players.some((e) => e.id === t && !e.isHost) && this.relayIdOf(t) === null) this.seat(e, t);
231
+ else {
232
+ this.sendSeat(e);
233
+ return;
234
+ }
235
+ this.seatsChanged();
236
+ }
237
+ onSessionEvent(e) {
238
+ switch (e.type) {
239
+ case "countdown":
240
+ this.setState({
241
+ phase: "countdown",
242
+ countdown: e.secondsLeft
243
+ });
244
+ break;
245
+ case "started":
246
+ this.setState({
247
+ phase: "running",
248
+ countdown: void 0
249
+ });
250
+ break;
251
+ case "players":
252
+ this.setState({
253
+ players: e.players,
254
+ ...this.seatState()
255
+ }), this.seatsMode && this.emit({
256
+ type: "seats",
257
+ seats: this.seats
258
+ });
259
+ break;
260
+ case "authority":
261
+ this.setState({ authorityConnected: e.connected });
262
+ break;
263
+ case "result": this.setState({
264
+ phase: "ended",
265
+ result: e.result
266
+ });
267
+ }
268
+ }
269
+ connectRemote(e) {
270
+ let t = this.session;
271
+ t && t.connect(e, this.endpointFor(e)).catch(() => {});
272
+ }
273
+ endpointFor(e) {
274
+ return (t) => new Promise((n, r) => {
275
+ let i = this.channel(e);
276
+ this.resetChannel(e, i);
277
+ let a = ++this.genCounter;
278
+ i.gen = a, i.hostApi = t, i.pendingMount = {
279
+ gen: a,
280
+ resolve: () => n(this.remotePlugin(e, a)),
281
+ reject: r,
282
+ timer: setTimeout(() => {
283
+ i.pendingMount?.gen === a && (i.pendingMount = null, r(/* @__PURE__ */ Error(`The device of player ${e} did not load the game in time.`)));
284
+ }, this.options.mountTimeoutMs ?? 2e4)
285
+ }, this.sendMount(e, a);
286
+ });
287
+ }
288
+ sendMount(e, t) {
289
+ this.sendToSeat(e, {
290
+ k: "mount",
291
+ gen: t,
292
+ bundle: this.bundleVersion,
293
+ locale: this.session?.config.locale ?? "en",
294
+ v: 1
295
+ });
296
+ }
297
+ remotePlugin(e, t) {
298
+ let n = {};
299
+ for (let r of o) n[r] = (...n) => this.callRemote(e, t, r, n);
300
+ return n;
301
+ }
302
+ callRemote(e, t, n, r) {
303
+ let i = this.channels.get(e);
304
+ if (!i || i.gen !== t) return Promise.reject(/* @__PURE__ */ Error("[SESSION_ENDED] The player instance was replaced."));
305
+ let a = this.relayIdOf(e);
306
+ if (!this.players.find((e) => e.id === a)?.connected) return Promise.resolve();
307
+ let o = ++this.callCounter;
308
+ return new Promise((a, s) => {
309
+ let c = setTimeout(() => {
310
+ i.calls.delete(o), s(/* @__PURE__ */ Error(`[INTERNAL_ERROR] ${n} on ${e} timed out.`));
311
+ }, this.options.callTimeoutMs ?? 15e3);
312
+ i.calls.set(o, {
313
+ resolve: a,
314
+ reject: s,
315
+ timer: c
316
+ }), this.sendToSeat(e, {
317
+ k: "call",
318
+ id: o,
319
+ gen: t,
320
+ method: n,
321
+ args: r
322
+ });
323
+ });
324
+ }
325
+ channel(e) {
326
+ let t = this.channels.get(e);
327
+ return t || (t = {
328
+ gen: 0,
329
+ hostApi: null,
330
+ pendingMount: null,
331
+ calls: /* @__PURE__ */ new Map()
332
+ }, this.channels.set(e, t)), t;
333
+ }
334
+ resetChannel(e, t) {
335
+ t.pendingMount &&= (clearTimeout(t.pendingMount.timer), t.pendingMount.reject(/* @__PURE__ */ Error("[SESSION_ENDED] Replaced.")), null), this.settleCalls(t), t.gen = 0, t.hostApi = null;
336
+ }
337
+ settleCalls(e) {
338
+ for (let t of e.calls.values()) clearTimeout(t.timer), t.resolve(void 0);
339
+ e.calls.clear();
340
+ }
341
+ onServer(e) {
342
+ switch (e.t) {
343
+ case "hosting":
344
+ this.hosting = !0, this.hostingWaiters.splice(0).forEach((e) => e()), this.players = e.players, this.emit({
345
+ type: "players",
346
+ players: this.players
347
+ }), this.broadcast({
348
+ k: "state",
349
+ state: this.state
350
+ });
351
+ break;
352
+ case "presence":
353
+ this.onPresence(e.player, e.event);
354
+ break;
355
+ case "from":
356
+ this.onPlayer(e.from, e.data);
357
+ break;
358
+ case "error":
359
+ this.emit({
360
+ type: "error",
361
+ code: e.code,
362
+ message: e.message
363
+ });
364
+ break;
365
+ case "closed": this.emit({
366
+ type: "closed",
367
+ reason: e.reason
368
+ });
369
+ }
370
+ }
371
+ onPresence(e, t) {
372
+ 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({
373
+ type: "players",
374
+ players: this.players
375
+ });
376
+ let n = this.session, r = this.seatOf.get(e.id) ?? null;
377
+ switch (t) {
378
+ case "join":
379
+ if (n && !n.ended && r === null && !this.seatsMode) try {
380
+ n.addPlayer({
381
+ id: e.id,
382
+ name: e.name
383
+ }), this.seatOf.set(e.id, e.id), this.connectRemote(e.id);
384
+ } catch {}
385
+ break;
386
+ case "disconnect": {
387
+ let e = r === null ? void 0 : this.channels.get(r);
388
+ e && this.settleCalls(e), n && r !== null && n.setConnected(r, !1);
389
+ break;
390
+ }
391
+ case "rename":
392
+ n && r !== null && !this.seatsMode && n.renamePlayer(r, e.name);
393
+ break;
394
+ case "leave": if (this.appVersions.delete(e.id), r !== null && this.seatsMode) this.unseat(e.id), this.seatsChanged();
395
+ else if (r !== null) {
396
+ this.seatOf.delete(e.id), this.channels.delete(r);
397
+ try {
398
+ n?.removePlayer(r);
399
+ } catch {}
400
+ }
401
+ }
402
+ }
403
+ async onPlayer(e, t) {
404
+ if (typeof t != "object" || !t) return;
405
+ let a = this.seatOf.get(e) ?? null;
406
+ if (t.k === "hello") this.appVersions.set(e, typeof t.v == "number" && Number.isFinite(t.v) ? t.v : 0), this.sendTo(e, {
407
+ k: "state",
408
+ state: this.state
409
+ }), this.seatsMode && this.sendSeat(e);
410
+ else if (t.k === "claim") {
411
+ this.claim(e, t.seat);
412
+ return;
413
+ }
414
+ if (a === null) {
415
+ if (t.k === "call") {
416
+ let n = u.has(t.method) ? { message: "[SESSION_ENDED] This device has no place in the game." } : i(t.method);
417
+ this.sendTo(e, {
418
+ k: "result",
419
+ id: t.id,
420
+ ok: !1,
421
+ error: n
422
+ });
423
+ }
424
+ return;
425
+ }
426
+ let o = this.channel(a);
427
+ switch (t.k) {
428
+ case "hello": {
429
+ let e = this.session;
430
+ if (!e || !e.players.some((e) => e.id === a)) return;
431
+ if (o.pendingMount) {
432
+ this.sendMount(a, o.pendingMount.gen);
433
+ return;
434
+ }
435
+ e.setConnected(a, !0), (o.gen === 0 || t.gen !== o.gen) && (e.getInstance(a) ? e.reload(a).catch(() => {}) : this.connectRemote(a));
436
+ return;
437
+ }
438
+ case "mounted":
439
+ if (o.pendingMount?.gen === t.gen) {
440
+ clearTimeout(o.pendingMount.timer);
441
+ let e = o.pendingMount;
442
+ o.pendingMount = null, e.resolve();
443
+ }
444
+ return;
445
+ case "mount-failed":
446
+ if (o.pendingMount?.gen === t.gen) {
447
+ clearTimeout(o.pendingMount.timer);
448
+ let e = o.pendingMount;
449
+ o.pendingMount = null, e.reject(Error(t.error?.message ?? "The game could not be loaded on the device."));
450
+ }
451
+ return;
452
+ case "result": {
453
+ let e = o.calls.get(t.id);
454
+ if (!e) return;
455
+ o.calls.delete(t.id), clearTimeout(e.timer), t.ok ? e.resolve(t.value) : e.reject(Error(t.error?.message ?? "Remote call failed"));
456
+ return;
457
+ }
458
+ case "call": {
459
+ let a = (t) => this.sendTo(e, t);
460
+ if (!u.has(t.method)) {
461
+ a({
462
+ k: "result",
463
+ id: t.id,
464
+ ok: !1,
465
+ error: i(t.method)
466
+ });
467
+ return;
468
+ }
469
+ if (t.gen !== o.gen || !o.hostApi || !Array.isArray(t.args)) {
470
+ a({
471
+ k: "result",
472
+ id: t.id,
473
+ ok: !1,
474
+ error: { message: "[SESSION_ENDED] This plugin instance is no longer active." }
475
+ });
476
+ return;
477
+ }
478
+ try {
479
+ let r = o.hostApi[t.method], i = await r(...t.args);
480
+ if (t.method === "hello") {
481
+ i = {
482
+ ...i,
483
+ set: null
484
+ }, a({
485
+ k: "result",
486
+ id: t.id,
487
+ ok: !0,
488
+ value: i,
489
+ hostNow: Date.now()
490
+ });
491
+ return;
492
+ }
493
+ t.method === "getAsset" && i instanceof Blob && (i = await n(i));
494
+ let s = {
495
+ k: "result",
496
+ id: t.id,
497
+ ok: !0,
498
+ value: i ?? null
499
+ };
500
+ if (l({
501
+ t: "to",
502
+ to: e,
503
+ data: s
504
+ }) > c.frameBytes) {
505
+ a({
506
+ k: "result",
507
+ id: t.id,
508
+ ok: !1,
509
+ error: { message: `[DATA_TOO_LARGE] The result of ${t.method} is too large to send to the device.` }
510
+ });
511
+ return;
512
+ }
513
+ a(s);
514
+ } catch (e) {
515
+ a({
516
+ k: "result",
517
+ id: t.id,
518
+ ok: !1,
519
+ error: r(e)
520
+ });
521
+ }
522
+ return;
523
+ }
524
+ }
525
+ }
526
+ sendTo(e, t) {
527
+ this.socket.send({
528
+ t: "to",
529
+ to: e,
530
+ data: t
531
+ });
532
+ }
533
+ sendToSeat(e, t) {
534
+ let n = this.relayIdOf(e);
535
+ n !== null && this.sendTo(n, t);
536
+ }
537
+ broadcast(e) {
538
+ for (let t of this.players) t.connected && this.sendTo(t.id, e);
539
+ }
540
+ };
541
+ //#endregion
542
+ export { d as RelayHost };
@@ -0,0 +1 @@
1
+ var e=1;function t(e){let t=e instanceof Error?e.message:String(e);return e?.code===`METHOD_NOT_FOUND`?{message:`[UNKNOWN_METHOD] ${t}`}:{message:t}}function n(e){return{message:`[UNKNOWN_METHOD] This app does not know the method "${String(e).slice(0,64)}".`}}async function r(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 i(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 a(e){return typeof e==`object`&&!!e&&typeof e.$blob==`string`}exports.RELAY_APP_VERSION=e,exports.blobToJson=r,exports.isSerializedBlob=a,exports.jsonToBlob=i,exports.serializeError=t,exports.unknownMethod=n;
@@ -0,0 +1,27 @@
1
+ //#region src/relay/messages.ts
2
+ var e = 1;
3
+ function t(e) {
4
+ let t = e instanceof Error ? e.message : String(e);
5
+ return e?.code === "METHOD_NOT_FOUND" ? { message: `[UNKNOWN_METHOD] ${t}` } : { message: t };
6
+ }
7
+ function n(e) {
8
+ return { message: `[UNKNOWN_METHOD] This app does not know the method "${String(e).slice(0, 64)}".` };
9
+ }
10
+ async function r(e) {
11
+ let t = new Uint8Array(await e.arrayBuffer()), n = "";
12
+ for (let e = 0; e < t.length; e += 32768) n += String.fromCharCode(...t.subarray(e, e + 32768));
13
+ return {
14
+ $blob: btoa(n),
15
+ type: e.type
16
+ };
17
+ }
18
+ function i(e) {
19
+ let t = atob(e.$blob), n = new Uint8Array(t.length);
20
+ for (let e = 0; e < t.length; e++) n[e] = t.charCodeAt(e);
21
+ return new Blob([n], { type: e.type });
22
+ }
23
+ function a(e) {
24
+ return typeof e == "object" && !!e && typeof e.$blob == "string";
25
+ }
26
+ //#endregion
27
+ export { e as RELAY_APP_VERSION, r as blobToJson, a as isSerializedBlob, i as jsonToBlob, t as serializeError, n as unknownMethod };
@@ -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=1e3,o=class{playerId=null;token;name;state=null;hostConnected=!0;hostAppVersion=null;seat=void 0;options;socket;listeners=new Set;bundle=null;frame=null;plugin=null;gen=0;mountGen=0;callCounter=0;pending=new Map;skewMs=0;hostConfig=null;mountQueue=Promise.resolve();sessionEndedSent=!1;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,...e.auth!==void 0&&{auth:e.auth}}),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)}refreshConfig(){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})}claimSeat(e){this.up({k:`claim`,seat: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,v:1});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,v:1});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`:{let t=e.reason===`kicked`?`kicked`:e.reason===`host`?`closed`:`host-left`;this.mountQueue=this.mountQueue.then(async()=>{await this.endGame(t),this.emit({type:`closed`,reason:e.reason})});break}}}up(e){return this.socket.send({t:`up`,data:e})}async onHost(e){if(typeof e==`object`&&e)switch(e.k){case`state`:e.state.seats||(this.seat=void 0),this.state=e.state,this.emit({type:`state`,state:e.state});return;case`mount`:if(this.hostAppVersion=typeof e.v==`number`?e.v:0,e.gen===this.mountGen){this.mountQueue=this.mountQueue.then(()=>{this.plugin&&this.gen===e.gen&&this.up({k:`mounted`,gen:e.gen})});return}this.mountGen=e.gen,this.mountQueue=this.mountQueue.then(()=>this.mount(e.gen,e.bundle));return;case`unmount`:this.mountGen=0,this.mountQueue=this.mountQueue.then(()=>this.endGame(`closed`));return;case`seat`:if(e.seat!==null&&typeof e.seat!=`string`)return;this.seat=e.seat,e.seat!==null&&typeof e.name==`string`&&e.name&&e.name!==this.name&&this.rename(e.name),this.emit({type:`seat`,seat:e.seat});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(!i.has(e.method)){t({k:`result`,id:e.id,ok:!1,error:n.unknownMethod(e.method)});return}if(e.gen!==this.gen||!this.plugin){t({k:`result`,id:e.id,ok:!1,error:{message:`[SESSION_ENDED] No active plugin instance.`}});return}let r=e.args;if(e.method===`sessionEnded`&&(this.sessionEndedSent=!0),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 endGame(e){let t=this.plugin;if(t&&!this.sessionEndedSent){this.sessionEndedSent=!0;let n;await Promise.race([t.sessionEnded(e).catch(()=>{}),new Promise(e=>n=setTimeout(e,a))]),clearTimeout(n)}this.destroyFrame()}async mount(t,i){this.destroyFrame(),this.sessionEndedSent=!1,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=o;