@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.
- package/CHANGELOG.md +20 -0
- package/LICENSE +21 -0
- package/README.md +65 -2
- package/dist/frame.cjs +1 -0
- package/dist/frame.js +78 -0
- package/dist/index.cjs +1 -0
- package/dist/index.js +10 -0
- package/dist/learning.cjs +1 -0
- package/dist/learning.js +43 -0
- package/dist/plugin.cjs +1 -0
- package/dist/plugin.js +52 -0
- package/dist/relay/host.cjs +1 -0
- package/dist/relay/host.js +542 -0
- package/dist/relay/messages.cjs +1 -0
- package/dist/relay/messages.js +27 -0
- package/dist/relay/player.cjs +1 -0
- package/dist/relay/player.js +327 -0
- package/dist/relay/socket.cjs +1 -0
- package/dist/relay/socket.js +74 -0
- package/dist/session.cjs +1 -0
- package/dist/session.js +548 -0
- package/dist/src/frame.d.ts +43 -0
- package/dist/src/index.d.ts +18 -0
- package/dist/src/learning.d.ts +18 -0
- package/dist/src/plugin.d.ts +51 -0
- package/dist/src/relay/host.d.ts +156 -0
- package/dist/src/relay/messages.d.ts +137 -0
- package/dist/src/relay/player.d.ts +127 -0
- package/dist/src/relay/socket.d.ts +34 -0
- package/dist/src/session.d.ts +248 -0
- package/dist/src/storage.d.ts +27 -0
- package/dist/src/version.d.ts +2 -0
- package/dist/storage.cjs +1 -0
- package/dist/storage.js +40 -0
- package/dist/version.cjs +1 -0
- package/dist/version.js +4 -0
- package/package.json +62 -4
|
@@ -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;
|