@playcademy/sdk 0.3.1 → 0.3.2
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/dist/index.d.ts +26 -30
- package/dist/index.js +264 -204
- package/dist/internal.d.ts +4347 -4026
- package/dist/internal.js +516 -448
- package/dist/server.d.ts +4 -11
- package/dist/server.js +40 -22
- package/dist/types.d.ts +3077 -3080
- package/package.json +8 -7
package/dist/internal.js
CHANGED
|
@@ -32,15 +32,15 @@ class PlaycademyMessaging {
|
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
listen(type, handler) {
|
|
35
|
-
|
|
35
|
+
function postMessageListener(event) {
|
|
36
36
|
const messageEvent = event;
|
|
37
37
|
if (messageEvent.data?.type === type) {
|
|
38
38
|
handler(messageEvent.data.payload || messageEvent.data);
|
|
39
39
|
}
|
|
40
|
-
}
|
|
41
|
-
|
|
40
|
+
}
|
|
41
|
+
function customEventListener(event) {
|
|
42
42
|
handler(event.detail);
|
|
43
|
-
}
|
|
43
|
+
}
|
|
44
44
|
if (!this.listeners.has(type)) {
|
|
45
45
|
this.listeners.set(type, new Map);
|
|
46
46
|
}
|
|
@@ -50,7 +50,7 @@ class PlaycademyMessaging {
|
|
|
50
50
|
customEvent: customEventListener
|
|
51
51
|
});
|
|
52
52
|
window.addEventListener("message", postMessageListener);
|
|
53
|
-
|
|
53
|
+
globalThis.addEventListener(type, customEventListener);
|
|
54
54
|
}
|
|
55
55
|
unlisten(type, handler) {
|
|
56
56
|
const typeListeners = this.listeners.get(type);
|
|
@@ -59,14 +59,14 @@ class PlaycademyMessaging {
|
|
|
59
59
|
}
|
|
60
60
|
const listeners = typeListeners.get(handler);
|
|
61
61
|
window.removeEventListener("message", listeners.postMessage);
|
|
62
|
-
|
|
62
|
+
globalThis.removeEventListener(type, listeners.customEvent);
|
|
63
63
|
typeListeners.delete(handler);
|
|
64
64
|
if (typeListeners.size === 0) {
|
|
65
65
|
this.listeners.delete(type);
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
getMessagingContext(eventType) {
|
|
69
|
-
const isIframe = typeof window !== "undefined" &&
|
|
69
|
+
const isIframe = typeof globalThis.window !== "undefined" && globalThis.self !== window.top;
|
|
70
70
|
const iframeToParentEvents = [
|
|
71
71
|
"PLAYCADEMY_READY" /* READY */,
|
|
72
72
|
"PLAYCADEMY_EXIT" /* EXIT */,
|
|
@@ -89,18 +89,18 @@ class PlaycademyMessaging {
|
|
|
89
89
|
target.postMessage(messageData, origin);
|
|
90
90
|
}
|
|
91
91
|
sendViaCustomEvent(type, payload) {
|
|
92
|
-
|
|
92
|
+
globalThis.dispatchEvent(new CustomEvent(type, { detail: payload }));
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
95
|
var messaging = new PlaycademyMessaging;
|
|
96
96
|
|
|
97
97
|
// src/core/static/init.ts
|
|
98
98
|
async function getPlaycademyConfig(allowedParentOrigins) {
|
|
99
|
-
const preloaded =
|
|
99
|
+
const preloaded = globalThis.PLAYCADEMY;
|
|
100
100
|
if (preloaded?.token) {
|
|
101
101
|
return preloaded;
|
|
102
102
|
}
|
|
103
|
-
if (
|
|
103
|
+
if (globalThis.self !== window.top) {
|
|
104
104
|
return await waitForPlaycademyInit(allowedParentOrigins);
|
|
105
105
|
} else {
|
|
106
106
|
return createStandaloneConfig();
|
|
@@ -114,13 +114,14 @@ function getReferrerOrigin() {
|
|
|
114
114
|
}
|
|
115
115
|
}
|
|
116
116
|
function buildAllowedOrigins(explicit) {
|
|
117
|
-
if (Array.isArray(explicit) && explicit.length > 0)
|
|
117
|
+
if (Array.isArray(explicit) && explicit.length > 0) {
|
|
118
118
|
return explicit;
|
|
119
|
+
}
|
|
119
120
|
const ref = getReferrerOrigin();
|
|
120
121
|
return ref ? [ref] : [];
|
|
121
122
|
}
|
|
122
123
|
function isOriginAllowed(origin, allowlist) {
|
|
123
|
-
if (
|
|
124
|
+
if (globalThis.location.hostname === "localhost" || globalThis.location.hostname === "127.0.0.1") {
|
|
124
125
|
return true;
|
|
125
126
|
}
|
|
126
127
|
if (!allowlist || allowlist.length === 0) {
|
|
@@ -136,14 +137,16 @@ async function waitForPlaycademyInit(allowedParentOrigins) {
|
|
|
136
137
|
const allowlist = buildAllowedOrigins(allowedParentOrigins);
|
|
137
138
|
let hasWarnedAboutUntrustedOrigin = false;
|
|
138
139
|
function warnAboutUntrustedOrigin(origin) {
|
|
139
|
-
if (hasWarnedAboutUntrustedOrigin)
|
|
140
|
+
if (hasWarnedAboutUntrustedOrigin) {
|
|
140
141
|
return;
|
|
142
|
+
}
|
|
141
143
|
hasWarnedAboutUntrustedOrigin = true;
|
|
142
144
|
console.warn("[Playcademy SDK] Ignoring INIT from untrusted origin:", origin);
|
|
143
145
|
}
|
|
144
|
-
|
|
145
|
-
if (event.data?.type !== "PLAYCADEMY_INIT" /* INIT */)
|
|
146
|
+
function handleMessage(event) {
|
|
147
|
+
if (event.data?.type !== "PLAYCADEMY_INIT" /* INIT */) {
|
|
146
148
|
return;
|
|
149
|
+
}
|
|
147
150
|
if (!isOriginAllowed(event.origin, allowlist)) {
|
|
148
151
|
warnAboutUntrustedOrigin(event.origin);
|
|
149
152
|
return;
|
|
@@ -151,9 +154,9 @@ async function waitForPlaycademyInit(allowedParentOrigins) {
|
|
|
151
154
|
contextReceived = true;
|
|
152
155
|
window.removeEventListener("message", handleMessage);
|
|
153
156
|
clearTimeout(timeoutId);
|
|
154
|
-
|
|
157
|
+
globalThis.PLAYCADEMY = event.data.payload;
|
|
155
158
|
resolve(event.data.payload);
|
|
156
|
-
}
|
|
159
|
+
}
|
|
157
160
|
window.addEventListener("message", handleMessage);
|
|
158
161
|
const timeoutId = setTimeout(() => {
|
|
159
162
|
if (!contextReceived) {
|
|
@@ -167,16 +170,16 @@ function createStandaloneConfig() {
|
|
|
167
170
|
console.debug("[Playcademy SDK] Standalone mode detected, creating mock context for sandbox development");
|
|
168
171
|
const mockConfig = {
|
|
169
172
|
baseUrl: "http://localhost:4321",
|
|
170
|
-
gameUrl:
|
|
173
|
+
gameUrl: globalThis.location.origin,
|
|
171
174
|
token: "mock-game-token-for-local-dev",
|
|
172
175
|
gameId: "mock-game-id-from-template",
|
|
173
176
|
realtimeUrl: undefined
|
|
174
177
|
};
|
|
175
|
-
|
|
178
|
+
globalThis.PLAYCADEMY = mockConfig;
|
|
176
179
|
return mockConfig;
|
|
177
180
|
}
|
|
178
181
|
async function init(options) {
|
|
179
|
-
if (typeof window === "undefined") {
|
|
182
|
+
if (typeof globalThis.window === "undefined") {
|
|
180
183
|
throw new Error("Playcademy SDK must run in a browser context");
|
|
181
184
|
}
|
|
182
185
|
const config = await getPlaycademyConfig(options?.allowedParentOrigins);
|
|
@@ -188,7 +191,7 @@ async function init(options) {
|
|
|
188
191
|
gameUrl: config.gameUrl,
|
|
189
192
|
token: config.token,
|
|
190
193
|
gameId: config.gameId,
|
|
191
|
-
autoStartSession:
|
|
194
|
+
autoStartSession: globalThis.self !== window.top,
|
|
192
195
|
onDisconnect: options?.onDisconnect,
|
|
193
196
|
enableConnectionMonitoring: options?.enableConnectionMonitoring
|
|
194
197
|
});
|
|
@@ -198,23 +201,23 @@ async function init(options) {
|
|
|
198
201
|
return client;
|
|
199
202
|
}
|
|
200
203
|
// ../logger/src/index.ts
|
|
201
|
-
|
|
204
|
+
function isBrowser() {
|
|
202
205
|
const g = globalThis;
|
|
203
206
|
return typeof g.window !== "undefined" && typeof g.document !== "undefined";
|
|
204
|
-
}
|
|
205
|
-
|
|
207
|
+
}
|
|
208
|
+
function isProduction() {
|
|
206
209
|
return typeof process !== "undefined" && false;
|
|
207
|
-
}
|
|
208
|
-
|
|
210
|
+
}
|
|
211
|
+
function isDevelopment() {
|
|
209
212
|
return typeof process !== "undefined" && true;
|
|
210
|
-
}
|
|
211
|
-
|
|
213
|
+
}
|
|
214
|
+
function isInteractiveTTY() {
|
|
212
215
|
return typeof process !== "undefined" && Boolean(process.stdout && process.stdout.isTTY);
|
|
213
|
-
}
|
|
214
|
-
|
|
216
|
+
}
|
|
217
|
+
function isSilent() {
|
|
215
218
|
return typeof process !== "undefined" && process.env.LOG_SILENT === "true";
|
|
216
|
-
}
|
|
217
|
-
|
|
219
|
+
}
|
|
220
|
+
function detectOutputFormat() {
|
|
218
221
|
if (isBrowser()) {
|
|
219
222
|
return "browser";
|
|
220
223
|
}
|
|
@@ -238,7 +241,7 @@ var detectOutputFormat = () => {
|
|
|
238
241
|
return "color-tty";
|
|
239
242
|
}
|
|
240
243
|
return "json-single-line";
|
|
241
|
-
}
|
|
244
|
+
}
|
|
242
245
|
var colors = {
|
|
243
246
|
reset: "\x1B[0m",
|
|
244
247
|
bold: "\x1B[1m",
|
|
@@ -249,21 +252,26 @@ var colors = {
|
|
|
249
252
|
cyan: "\x1B[36m",
|
|
250
253
|
gray: "\x1B[90m"
|
|
251
254
|
};
|
|
252
|
-
|
|
255
|
+
function getLevelColor(level) {
|
|
253
256
|
switch (level) {
|
|
254
|
-
case "debug":
|
|
257
|
+
case "debug": {
|
|
255
258
|
return colors.blue;
|
|
256
|
-
|
|
259
|
+
}
|
|
260
|
+
case "info": {
|
|
257
261
|
return colors.cyan;
|
|
258
|
-
|
|
262
|
+
}
|
|
263
|
+
case "warn": {
|
|
259
264
|
return colors.yellow;
|
|
260
|
-
|
|
265
|
+
}
|
|
266
|
+
case "error": {
|
|
261
267
|
return colors.red;
|
|
262
|
-
|
|
268
|
+
}
|
|
269
|
+
default: {
|
|
263
270
|
return colors.reset;
|
|
271
|
+
}
|
|
264
272
|
}
|
|
265
|
-
}
|
|
266
|
-
|
|
273
|
+
}
|
|
274
|
+
function formatBrowserOutput(level, message, context, scope) {
|
|
267
275
|
const timestamp = new Date().toISOString();
|
|
268
276
|
const levelUpper = level.toUpperCase();
|
|
269
277
|
const consoleMethod = getConsoleMethod(level);
|
|
@@ -273,8 +281,8 @@ var formatBrowserOutput = (level, message, context, scope) => {
|
|
|
273
281
|
} else {
|
|
274
282
|
consoleMethod(`[${timestamp}] ${levelUpper}`, `${scopePrefix}${message}`);
|
|
275
283
|
}
|
|
276
|
-
}
|
|
277
|
-
|
|
284
|
+
}
|
|
285
|
+
function formatColorTTY(level, message, context, scope) {
|
|
278
286
|
const timestamp = new Date().toISOString();
|
|
279
287
|
const levelColor = getLevelColor(level);
|
|
280
288
|
const levelUpper = level.toUpperCase().padEnd(5);
|
|
@@ -286,8 +294,8 @@ var formatColorTTY = (level, message, context, scope) => {
|
|
|
286
294
|
} else {
|
|
287
295
|
consoleMethod(`${coloredPrefix} ${scopePrefix}${message}`);
|
|
288
296
|
}
|
|
289
|
-
}
|
|
290
|
-
|
|
297
|
+
}
|
|
298
|
+
function formatJSONSingleLine(level, message, context, scope) {
|
|
291
299
|
const timestamp = new Date().toISOString();
|
|
292
300
|
const logEntry = {
|
|
293
301
|
timestamp,
|
|
@@ -298,8 +306,8 @@ var formatJSONSingleLine = (level, message, context, scope) => {
|
|
|
298
306
|
};
|
|
299
307
|
const consoleMethod = getConsoleMethod(level);
|
|
300
308
|
consoleMethod(JSON.stringify(logEntry));
|
|
301
|
-
}
|
|
302
|
-
|
|
309
|
+
}
|
|
310
|
+
function formatJSONPretty(level, message, context, scope) {
|
|
303
311
|
const timestamp = new Date().toISOString();
|
|
304
312
|
const logEntry = {
|
|
305
313
|
timestamp,
|
|
@@ -310,65 +318,76 @@ var formatJSONPretty = (level, message, context, scope) => {
|
|
|
310
318
|
};
|
|
311
319
|
const consoleMethod = getConsoleMethod(level);
|
|
312
320
|
consoleMethod(JSON.stringify(logEntry, null, 2));
|
|
313
|
-
}
|
|
314
|
-
|
|
321
|
+
}
|
|
322
|
+
function getConsoleMethod(level) {
|
|
315
323
|
switch (level) {
|
|
316
|
-
case "debug":
|
|
324
|
+
case "debug": {
|
|
317
325
|
return console.debug;
|
|
318
|
-
|
|
326
|
+
}
|
|
327
|
+
case "info": {
|
|
319
328
|
return console.info;
|
|
320
|
-
|
|
329
|
+
}
|
|
330
|
+
case "warn": {
|
|
321
331
|
return console.warn;
|
|
322
|
-
|
|
332
|
+
}
|
|
333
|
+
case "error": {
|
|
323
334
|
return console.error;
|
|
324
|
-
|
|
335
|
+
}
|
|
336
|
+
default: {
|
|
325
337
|
return console.log;
|
|
338
|
+
}
|
|
326
339
|
}
|
|
327
|
-
}
|
|
340
|
+
}
|
|
328
341
|
var levelPriority = {
|
|
329
342
|
debug: 0,
|
|
330
343
|
info: 1,
|
|
331
344
|
warn: 2,
|
|
332
345
|
error: 3
|
|
333
346
|
};
|
|
334
|
-
|
|
347
|
+
function getMinimumLogLevel() {
|
|
335
348
|
const envLevel = typeof process !== "undefined" ? (process.env.LOG_LEVEL ?? "").toLowerCase() : "";
|
|
336
349
|
if (envLevel && ["debug", "info", "warn", "error"].includes(envLevel)) {
|
|
337
350
|
return envLevel;
|
|
338
351
|
}
|
|
339
352
|
return isProduction() ? "info" : "debug";
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
if (isSilent())
|
|
353
|
+
}
|
|
354
|
+
function shouldLog(level) {
|
|
355
|
+
if (isSilent()) {
|
|
343
356
|
return false;
|
|
357
|
+
}
|
|
344
358
|
const minLevel = getMinimumLogLevel();
|
|
345
359
|
return levelPriority[level] >= levelPriority[minLevel];
|
|
346
|
-
}
|
|
360
|
+
}
|
|
347
361
|
var customHandler;
|
|
348
|
-
|
|
349
|
-
if (!shouldLog(level))
|
|
362
|
+
function performLog(level, message, context, scope) {
|
|
363
|
+
if (!shouldLog(level)) {
|
|
350
364
|
return;
|
|
365
|
+
}
|
|
351
366
|
if (customHandler) {
|
|
352
367
|
customHandler(level, message, context, scope);
|
|
353
368
|
return;
|
|
354
369
|
}
|
|
355
370
|
const outputFormat = detectOutputFormat();
|
|
356
371
|
switch (outputFormat) {
|
|
357
|
-
case "browser":
|
|
372
|
+
case "browser": {
|
|
358
373
|
formatBrowserOutput(level, message, context, scope);
|
|
359
374
|
break;
|
|
360
|
-
|
|
375
|
+
}
|
|
376
|
+
case "color-tty": {
|
|
361
377
|
formatColorTTY(level, message, context, scope);
|
|
362
378
|
break;
|
|
363
|
-
|
|
379
|
+
}
|
|
380
|
+
case "json-single-line": {
|
|
364
381
|
formatJSONSingleLine(level, message, context, scope);
|
|
365
382
|
break;
|
|
366
|
-
|
|
383
|
+
}
|
|
384
|
+
case "json-pretty": {
|
|
367
385
|
formatJSONPretty(level, message, context, scope);
|
|
368
386
|
break;
|
|
387
|
+
}
|
|
369
388
|
}
|
|
370
|
-
}
|
|
371
|
-
|
|
389
|
+
}
|
|
390
|
+
function createLogger(scopeName) {
|
|
372
391
|
return {
|
|
373
392
|
debug: (message, context) => performLog("debug", message, context, scopeName),
|
|
374
393
|
info: (message, context) => performLog("info", message, context, scopeName),
|
|
@@ -377,7 +396,7 @@ var createLogger = (scopeName) => {
|
|
|
377
396
|
log: (level, message, context) => performLog(level, message, context, scopeName),
|
|
378
397
|
scope: (name) => createLogger(scopeName ? `${scopeName}.${name}` : name)
|
|
379
398
|
};
|
|
380
|
-
}
|
|
399
|
+
}
|
|
381
400
|
var log = createLogger();
|
|
382
401
|
|
|
383
402
|
// src/core/errors.ts
|
|
@@ -389,14 +408,14 @@ class PlaycademyError extends Error {
|
|
|
389
408
|
}
|
|
390
409
|
|
|
391
410
|
class ApiError extends Error {
|
|
392
|
-
status;
|
|
393
411
|
code;
|
|
394
412
|
details;
|
|
395
413
|
rawBody;
|
|
414
|
+
status;
|
|
396
415
|
constructor(status, code, message, details, rawBody) {
|
|
397
416
|
super(message);
|
|
398
|
-
this.status = status;
|
|
399
417
|
this.name = "ApiError";
|
|
418
|
+
this.status = status;
|
|
400
419
|
this.code = code;
|
|
401
420
|
this.details = details;
|
|
402
421
|
this.rawBody = rawBody;
|
|
@@ -427,38 +446,54 @@ class ApiError extends Error {
|
|
|
427
446
|
}
|
|
428
447
|
function statusCodeToErrorCode(status) {
|
|
429
448
|
switch (status) {
|
|
430
|
-
case 400:
|
|
449
|
+
case 400: {
|
|
431
450
|
return "BAD_REQUEST";
|
|
432
|
-
|
|
451
|
+
}
|
|
452
|
+
case 401: {
|
|
433
453
|
return "UNAUTHORIZED";
|
|
434
|
-
|
|
454
|
+
}
|
|
455
|
+
case 403: {
|
|
435
456
|
return "FORBIDDEN";
|
|
436
|
-
|
|
457
|
+
}
|
|
458
|
+
case 404: {
|
|
437
459
|
return "NOT_FOUND";
|
|
438
|
-
|
|
460
|
+
}
|
|
461
|
+
case 405: {
|
|
439
462
|
return "METHOD_NOT_ALLOWED";
|
|
440
|
-
|
|
463
|
+
}
|
|
464
|
+
case 409: {
|
|
441
465
|
return "CONFLICT";
|
|
442
|
-
|
|
466
|
+
}
|
|
467
|
+
case 410: {
|
|
443
468
|
return "GONE";
|
|
444
|
-
|
|
469
|
+
}
|
|
470
|
+
case 412: {
|
|
445
471
|
return "PRECONDITION_FAILED";
|
|
446
|
-
|
|
472
|
+
}
|
|
473
|
+
case 413: {
|
|
447
474
|
return "PAYLOAD_TOO_LARGE";
|
|
448
|
-
|
|
475
|
+
}
|
|
476
|
+
case 422: {
|
|
449
477
|
return "VALIDATION_FAILED";
|
|
450
|
-
|
|
478
|
+
}
|
|
479
|
+
case 429: {
|
|
451
480
|
return "TOO_MANY_REQUESTS";
|
|
452
|
-
|
|
481
|
+
}
|
|
482
|
+
case 500: {
|
|
453
483
|
return "INTERNAL_ERROR";
|
|
454
|
-
|
|
484
|
+
}
|
|
485
|
+
case 501: {
|
|
455
486
|
return "NOT_IMPLEMENTED";
|
|
456
|
-
|
|
487
|
+
}
|
|
488
|
+
case 503: {
|
|
457
489
|
return "SERVICE_UNAVAILABLE";
|
|
458
|
-
|
|
490
|
+
}
|
|
491
|
+
case 504: {
|
|
459
492
|
return "TIMEOUT";
|
|
460
|
-
|
|
493
|
+
}
|
|
494
|
+
default: {
|
|
461
495
|
return status >= 500 ? "INTERNAL_ERROR" : "BAD_REQUEST";
|
|
496
|
+
}
|
|
462
497
|
}
|
|
463
498
|
}
|
|
464
499
|
function extractApiErrorInfo(error) {
|
|
@@ -476,10 +511,10 @@ function extractApiErrorInfo(error) {
|
|
|
476
511
|
// src/core/static/login.ts
|
|
477
512
|
async function login(baseUrl, email, password) {
|
|
478
513
|
let url = baseUrl;
|
|
479
|
-
if (baseUrl.startsWith("/") && typeof window !== "undefined") {
|
|
480
|
-
url =
|
|
514
|
+
if (baseUrl.startsWith("/") && typeof globalThis.window !== "undefined") {
|
|
515
|
+
url = globalThis.location.origin + baseUrl;
|
|
481
516
|
}
|
|
482
|
-
url
|
|
517
|
+
url += "/auth/login";
|
|
483
518
|
const response = await fetch(url, {
|
|
484
519
|
method: "POST",
|
|
485
520
|
headers: {
|
|
@@ -490,7 +525,7 @@ async function login(baseUrl, email, password) {
|
|
|
490
525
|
if (!response.ok) {
|
|
491
526
|
try {
|
|
492
527
|
const errorData = await response.json();
|
|
493
|
-
const errorMessage = errorData && errorData.message ? String(errorData.message) : response.statusText;
|
|
528
|
+
const errorMessage = errorData && errorData.message !== undefined ? String(errorData.message) : response.statusText;
|
|
494
529
|
throw new PlaycademyError(errorMessage);
|
|
495
530
|
} catch (error) {
|
|
496
531
|
log.error("[Playcademy SDK] Failed to parse error response JSON, using status text instead:", { error });
|
|
@@ -504,7 +539,7 @@ async function generateSecureRandomString(length) {
|
|
|
504
539
|
const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
|
|
505
540
|
const randomValues = new Uint8Array(length);
|
|
506
541
|
globalThis.crypto.getRandomValues(randomValues);
|
|
507
|
-
return
|
|
542
|
+
return [...randomValues].map((byte) => charset[byte % charset.length]).join("");
|
|
508
543
|
}
|
|
509
544
|
|
|
510
545
|
// src/core/auth/oauth.ts
|
|
@@ -546,8 +581,9 @@ function parseOAuthState(state) {
|
|
|
546
581
|
}
|
|
547
582
|
function getOAuthConfig(provider) {
|
|
548
583
|
const configGetter = OAUTH_CONFIGS[provider];
|
|
549
|
-
if (!configGetter)
|
|
584
|
+
if (!configGetter) {
|
|
550
585
|
throw new Error(`Unsupported auth provider: ${provider}`);
|
|
586
|
+
}
|
|
551
587
|
return configGetter();
|
|
552
588
|
}
|
|
553
589
|
|
|
@@ -574,11 +610,11 @@ function openPopupWindow(url, name = "auth-popup", width = 500, height = 600) {
|
|
|
574
610
|
return window.open(url, name, features);
|
|
575
611
|
}
|
|
576
612
|
function isInIframe() {
|
|
577
|
-
if (typeof window === "undefined") {
|
|
613
|
+
if (typeof globalThis.window === "undefined") {
|
|
578
614
|
return false;
|
|
579
615
|
}
|
|
580
616
|
try {
|
|
581
|
-
return
|
|
617
|
+
return globalThis.self !== window.top;
|
|
582
618
|
} catch {
|
|
583
619
|
return true;
|
|
584
620
|
}
|
|
@@ -631,9 +667,10 @@ async function initiatePopupFlow(options) {
|
|
|
631
667
|
async function waitForServerMessage(popup, onStateChange) {
|
|
632
668
|
return new Promise((resolve) => {
|
|
633
669
|
let resolved = false;
|
|
634
|
-
|
|
635
|
-
if (event.origin !==
|
|
670
|
+
function handleMessage(event) {
|
|
671
|
+
if (event.origin !== globalThis.location.origin) {
|
|
636
672
|
return;
|
|
673
|
+
}
|
|
637
674
|
const data = event.data;
|
|
638
675
|
if (data?.type === "PLAYCADEMY_AUTH_STATE_CHANGE") {
|
|
639
676
|
resolved = true;
|
|
@@ -660,7 +697,7 @@ async function waitForServerMessage(popup, onStateChange) {
|
|
|
660
697
|
});
|
|
661
698
|
}
|
|
662
699
|
}
|
|
663
|
-
}
|
|
700
|
+
}
|
|
664
701
|
window.addEventListener("message", handleMessage);
|
|
665
702
|
const checkClosed = setInterval(() => {
|
|
666
703
|
if (popup.closed && !resolved) {
|
|
@@ -722,7 +759,7 @@ async function initiateRedirectFlow(options) {
|
|
|
722
759
|
params.set("scope", config.scope);
|
|
723
760
|
}
|
|
724
761
|
const authUrl = `${config.authorizationEndpoint}?${params.toString()}`;
|
|
725
|
-
|
|
762
|
+
globalThis.location.href = authUrl;
|
|
726
763
|
return new Promise(() => {});
|
|
727
764
|
} catch (error) {
|
|
728
765
|
const errorMessage = error instanceof Error ? error.message : "Authentication failed";
|
|
@@ -738,14 +775,22 @@ async function initiateRedirectFlow(options) {
|
|
|
738
775
|
// src/core/auth/flows/unified.ts
|
|
739
776
|
async function initiateUnifiedFlow(options) {
|
|
740
777
|
const { mode = "auto" } = options;
|
|
741
|
-
|
|
778
|
+
let effectiveMode;
|
|
779
|
+
if (mode === "auto") {
|
|
780
|
+
effectiveMode = isInIframe() ? "popup" : "redirect";
|
|
781
|
+
} else {
|
|
782
|
+
effectiveMode = mode;
|
|
783
|
+
}
|
|
742
784
|
switch (effectiveMode) {
|
|
743
|
-
case "popup":
|
|
785
|
+
case "popup": {
|
|
744
786
|
return initiatePopupFlow(options);
|
|
745
|
-
|
|
787
|
+
}
|
|
788
|
+
case "redirect": {
|
|
746
789
|
return initiateRedirectFlow(options);
|
|
747
|
-
|
|
790
|
+
}
|
|
791
|
+
default: {
|
|
748
792
|
throw new Error(`Unsupported authentication mode: ${effectiveMode}`);
|
|
793
|
+
}
|
|
749
794
|
}
|
|
750
795
|
}
|
|
751
796
|
|
|
@@ -767,7 +812,7 @@ async function login2(client, options) {
|
|
|
767
812
|
provider: options.provider,
|
|
768
813
|
mode: options.mode || "auto",
|
|
769
814
|
callbackUrl: options.callbackUrl,
|
|
770
|
-
hasStateData:
|
|
815
|
+
hasStateData: Boolean(stateData)
|
|
771
816
|
});
|
|
772
817
|
const optionsWithState = {
|
|
773
818
|
...options,
|
|
@@ -802,13 +847,13 @@ function createIdentityNamespace(client) {
|
|
|
802
847
|
// src/namespaces/game/runtime.ts
|
|
803
848
|
function createRuntimeNamespace(client) {
|
|
804
849
|
const eventListeners = new Map;
|
|
805
|
-
|
|
850
|
+
function trackListener(eventType, handler) {
|
|
806
851
|
if (!eventListeners.has(eventType)) {
|
|
807
852
|
eventListeners.set(eventType, new Set);
|
|
808
853
|
}
|
|
809
854
|
eventListeners.get(eventType).add(handler);
|
|
810
|
-
}
|
|
811
|
-
|
|
855
|
+
}
|
|
856
|
+
function untrackListener(eventType, handler) {
|
|
812
857
|
const listeners = eventListeners.get(eventType);
|
|
813
858
|
if (listeners) {
|
|
814
859
|
listeners.delete(handler);
|
|
@@ -816,12 +861,9 @@ function createRuntimeNamespace(client) {
|
|
|
816
861
|
eventListeners.delete(eventType);
|
|
817
862
|
}
|
|
818
863
|
}
|
|
819
|
-
}
|
|
820
|
-
if (typeof window !== "undefined" &&
|
|
821
|
-
|
|
822
|
-
const forwardKeys = Array.isArray(playcademyConfig?.forwardKeys) ? playcademyConfig.forwardKeys : ["Escape"];
|
|
823
|
-
const keySet = new Set(forwardKeys.map((k) => k.toLowerCase()));
|
|
824
|
-
const keyListener = (event) => {
|
|
864
|
+
}
|
|
865
|
+
if (typeof globalThis.window !== "undefined" && globalThis.self !== window.top) {
|
|
866
|
+
let keyListener = function(event) {
|
|
825
867
|
if (keySet.has(event.key?.toLowerCase() ?? "") || keySet.has(event.code?.toLowerCase() ?? "")) {
|
|
826
868
|
messaging.send("PLAYCADEMY_KEY_EVENT" /* KEY_EVENT */, {
|
|
827
869
|
key: event.key,
|
|
@@ -830,11 +872,14 @@ function createRuntimeNamespace(client) {
|
|
|
830
872
|
});
|
|
831
873
|
}
|
|
832
874
|
};
|
|
833
|
-
|
|
834
|
-
|
|
875
|
+
const playcademyConfig = globalThis.PLAYCADEMY;
|
|
876
|
+
const forwardKeys = Array.isArray(playcademyConfig?.forwardKeys) ? playcademyConfig.forwardKeys : ["Escape"];
|
|
877
|
+
const keySet = new Set(forwardKeys.map((k) => k.toLowerCase()));
|
|
878
|
+
globalThis.addEventListener("keydown", keyListener);
|
|
879
|
+
globalThis.addEventListener("keyup", keyListener);
|
|
835
880
|
trackListener("PLAYCADEMY_FORCE_EXIT" /* FORCE_EXIT */, () => {
|
|
836
|
-
|
|
837
|
-
|
|
881
|
+
globalThis.removeEventListener("keydown", keyListener);
|
|
882
|
+
globalThis.removeEventListener("keyup", keyListener);
|
|
838
883
|
});
|
|
839
884
|
}
|
|
840
885
|
return {
|
|
@@ -911,32 +956,30 @@ function createRuntimeNamespace(client) {
|
|
|
911
956
|
};
|
|
912
957
|
}
|
|
913
958
|
function createAssetsNamespace(client) {
|
|
914
|
-
|
|
959
|
+
async function fetchAsset(path, options) {
|
|
915
960
|
const gameUrl = client["initPayload"]?.gameUrl;
|
|
916
961
|
if (!gameUrl) {
|
|
917
|
-
const relativePath = path.startsWith("./") ? path :
|
|
962
|
+
const relativePath = path.startsWith("./") ? path : `./${path}`;
|
|
918
963
|
return fetch(relativePath, options);
|
|
919
964
|
}
|
|
920
965
|
const cleanPath = path.startsWith("./") ? path.slice(2) : path;
|
|
921
|
-
return fetch(gameUrl
|
|
922
|
-
}
|
|
966
|
+
return fetch(`${gameUrl}${cleanPath}`, options);
|
|
967
|
+
}
|
|
923
968
|
return {
|
|
924
969
|
url(pathOrStrings, ...values) {
|
|
925
970
|
const gameUrl = client["initPayload"]?.gameUrl;
|
|
926
971
|
let path;
|
|
927
972
|
if (Array.isArray(pathOrStrings) && "raw" in pathOrStrings) {
|
|
928
973
|
const strings = pathOrStrings;
|
|
929
|
-
path = strings.reduce((acc, str, i) =>
|
|
930
|
-
return acc + str + (values[i] != null ? String(values[i]) : "");
|
|
931
|
-
}, "");
|
|
974
|
+
path = strings.reduce((acc, str, i) => acc + str + (values[i] != null ? String(values[i]) : ""), "");
|
|
932
975
|
} else {
|
|
933
976
|
path = pathOrStrings;
|
|
934
977
|
}
|
|
935
978
|
if (!gameUrl) {
|
|
936
|
-
return path.startsWith("./") ? path :
|
|
979
|
+
return path.startsWith("./") ? path : `./${path}`;
|
|
937
980
|
}
|
|
938
981
|
const cleanPath = path.startsWith("./") ? path.slice(2) : path;
|
|
939
|
-
return gameUrl
|
|
982
|
+
return `${gameUrl}${cleanPath}`;
|
|
940
983
|
},
|
|
941
984
|
fetch: fetchAsset,
|
|
942
985
|
json: async (path) => {
|
|
@@ -958,10 +1001,10 @@ function createAssetsNamespace(client) {
|
|
|
958
1001
|
};
|
|
959
1002
|
}
|
|
960
1003
|
// src/namespaces/game/backend.ts
|
|
1004
|
+
function normalizePath(path) {
|
|
1005
|
+
return path.startsWith("/") ? path : `/${path}`;
|
|
1006
|
+
}
|
|
961
1007
|
function createBackendNamespace(client) {
|
|
962
|
-
function normalizePath(path) {
|
|
963
|
-
return path.startsWith("/") ? path : `/${path}`;
|
|
964
|
-
}
|
|
965
1008
|
return {
|
|
966
1009
|
async get(path, headers) {
|
|
967
1010
|
return client["requestGameBackend"](normalizePath(path), "GET", undefined, headers);
|
|
@@ -987,9 +1030,7 @@ function createBackendNamespace(client) {
|
|
|
987
1030
|
url(pathOrStrings, ...values) {
|
|
988
1031
|
if (Array.isArray(pathOrStrings) && "raw" in pathOrStrings) {
|
|
989
1032
|
const strings = pathOrStrings;
|
|
990
|
-
const path2 = strings.reduce((acc, str, i) =>
|
|
991
|
-
return acc + str + (values[i] != null ? String(values[i]) : "");
|
|
992
|
-
}, "");
|
|
1033
|
+
const path2 = strings.reduce((acc, str, i) => acc + str + (values[i] != null ? String(values[i]) : ""), "");
|
|
993
1034
|
return `${client.gameUrl}/api${path2.startsWith("/") ? path2 : `/${path2}`}`;
|
|
994
1035
|
}
|
|
995
1036
|
const path = pathOrStrings;
|
|
@@ -1003,8 +1044,9 @@ function createPermanentCache(keyPrefix) {
|
|
|
1003
1044
|
async function get(key, loader) {
|
|
1004
1045
|
const fullKey = keyPrefix ? `${keyPrefix}:${key}` : key;
|
|
1005
1046
|
const existing = cache.get(fullKey);
|
|
1006
|
-
if (existing)
|
|
1047
|
+
if (existing) {
|
|
1007
1048
|
return existing;
|
|
1049
|
+
}
|
|
1008
1050
|
const promise = loader().catch((error) => {
|
|
1009
1051
|
cache.delete(fullKey);
|
|
1010
1052
|
throw error;
|
|
@@ -1042,23 +1084,23 @@ function createPermanentCache(keyPrefix) {
|
|
|
1042
1084
|
function createUsersNamespace(client) {
|
|
1043
1085
|
const itemIdCache = createPermanentCache("items");
|
|
1044
1086
|
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1045
|
-
|
|
1046
|
-
if (UUID_REGEX.test(identifier))
|
|
1087
|
+
async function resolveItemId(identifier) {
|
|
1088
|
+
if (UUID_REGEX.test(identifier)) {
|
|
1047
1089
|
return identifier;
|
|
1090
|
+
}
|
|
1048
1091
|
const gameId = client["gameId"];
|
|
1049
1092
|
const cacheKey = gameId ? `${identifier}:${gameId}` : identifier;
|
|
1050
1093
|
return itemIdCache.get(cacheKey, async () => {
|
|
1051
1094
|
const queryParams = new URLSearchParams({ slug: identifier });
|
|
1052
|
-
if (gameId)
|
|
1095
|
+
if (gameId) {
|
|
1053
1096
|
queryParams.append("gameId", gameId);
|
|
1097
|
+
}
|
|
1054
1098
|
const item = await client["request"](`/items/resolve?${queryParams.toString()}`, "GET");
|
|
1055
1099
|
return item.id;
|
|
1056
1100
|
});
|
|
1057
|
-
}
|
|
1101
|
+
}
|
|
1058
1102
|
return {
|
|
1059
|
-
me: async () =>
|
|
1060
|
-
return client["request"]("/users/me", "GET");
|
|
1061
|
-
},
|
|
1103
|
+
me: async () => client["request"]("/users/me", "GET"),
|
|
1062
1104
|
inventory: {
|
|
1063
1105
|
get: async () => client["request"](`/inventory`, "GET"),
|
|
1064
1106
|
add: async (identifier, qty) => {
|
|
@@ -1185,8 +1227,15 @@ var ACHIEVEMENT_DEFINITIONS = [
|
|
|
1185
1227
|
}
|
|
1186
1228
|
];
|
|
1187
1229
|
// ../constants/src/typescript.ts
|
|
1188
|
-
var
|
|
1189
|
-
|
|
1230
|
+
var TypeScriptPackages = {
|
|
1231
|
+
tsc: "tsc",
|
|
1232
|
+
nativePreview: "@typescript/native-preview",
|
|
1233
|
+
nativePreviewPinned: "@typescript/native-preview@7.0.0-dev.20260221.1"
|
|
1234
|
+
};
|
|
1235
|
+
var TYPESCRIPT_RUNNER = {
|
|
1236
|
+
package: TypeScriptPackages.nativePreviewPinned,
|
|
1237
|
+
bin: "tsgo"
|
|
1238
|
+
};
|
|
1190
1239
|
// ../constants/src/overworld.ts
|
|
1191
1240
|
var ITEM_SLUGS = {
|
|
1192
1241
|
PLAYCADEMY_CREDITS: "PLAYCADEMY_CREDITS",
|
|
@@ -1241,7 +1290,7 @@ function createSingletonCache() {
|
|
|
1241
1290
|
// src/namespaces/game/credits.ts
|
|
1242
1291
|
function createCreditsNamespace(client) {
|
|
1243
1292
|
const creditsIdCache = createSingletonCache();
|
|
1244
|
-
|
|
1293
|
+
async function getCreditsItemId() {
|
|
1245
1294
|
return creditsIdCache.get(async () => {
|
|
1246
1295
|
const queryParams = new URLSearchParams({ slug: CURRENCIES.PRIMARY });
|
|
1247
1296
|
const creditsItem = await client["request"](`/items/resolve?${queryParams.toString()}`, "GET");
|
|
@@ -1250,7 +1299,7 @@ function createCreditsNamespace(client) {
|
|
|
1250
1299
|
}
|
|
1251
1300
|
return creditsItem.id;
|
|
1252
1301
|
});
|
|
1253
|
-
}
|
|
1302
|
+
}
|
|
1254
1303
|
return {
|
|
1255
1304
|
balance: async () => {
|
|
1256
1305
|
const inventory = await client["request"]("/inventory", "GET");
|
|
@@ -1298,14 +1347,12 @@ function createCreditsNamespace(client) {
|
|
|
1298
1347
|
// src/namespaces/game/scores.ts
|
|
1299
1348
|
function createScoresNamespace(client) {
|
|
1300
1349
|
return {
|
|
1301
|
-
submit: async (gameId, score, metadata) => {
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
});
|
|
1308
|
-
}
|
|
1350
|
+
submit: async (gameId, score, metadata) => client["request"](`/games/${gameId}/scores`, "POST", {
|
|
1351
|
+
body: {
|
|
1352
|
+
score,
|
|
1353
|
+
metadata
|
|
1354
|
+
}
|
|
1355
|
+
})
|
|
1309
1356
|
};
|
|
1310
1357
|
}
|
|
1311
1358
|
// src/namespaces/game/realtime.ts
|
|
@@ -1379,8 +1426,9 @@ function createTTLCache(options) {
|
|
|
1379
1426
|
function has(key) {
|
|
1380
1427
|
const fullKey = keyPrefix ? `${keyPrefix}:${key}` : key;
|
|
1381
1428
|
const cached = cache.get(fullKey);
|
|
1382
|
-
if (!cached)
|
|
1429
|
+
if (!cached) {
|
|
1383
1430
|
return false;
|
|
1431
|
+
}
|
|
1384
1432
|
const now = Date.now();
|
|
1385
1433
|
if (cached.expiresAt <= now) {
|
|
1386
1434
|
cache.delete(fullKey);
|
|
@@ -1422,7 +1470,9 @@ function createTimebackNamespace(client) {
|
|
|
1422
1470
|
ttl: 5000,
|
|
1423
1471
|
keyPrefix: "game.timeback.xp"
|
|
1424
1472
|
});
|
|
1425
|
-
|
|
1473
|
+
function getTimeback() {
|
|
1474
|
+
return client["initPayload"]?.timeback;
|
|
1475
|
+
}
|
|
1426
1476
|
return {
|
|
1427
1477
|
get user() {
|
|
1428
1478
|
return {
|
|
@@ -1438,21 +1488,19 @@ function createTimebackNamespace(client) {
|
|
|
1438
1488
|
get organizations() {
|
|
1439
1489
|
return getTimeback()?.organizations ?? [];
|
|
1440
1490
|
},
|
|
1441
|
-
fetch: async (options) => {
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
}, options);
|
|
1455
|
-
},
|
|
1491
|
+
fetch: async (options) => userCache.get("current", async () => {
|
|
1492
|
+
const response = await client["request"]("/timeback/user", "GET");
|
|
1493
|
+
const initPayload = client["initPayload"];
|
|
1494
|
+
if (initPayload) {
|
|
1495
|
+
initPayload.timeback = response;
|
|
1496
|
+
}
|
|
1497
|
+
return {
|
|
1498
|
+
id: response.id,
|
|
1499
|
+
role: response.role,
|
|
1500
|
+
enrollments: response.enrollments,
|
|
1501
|
+
organizations: response.organizations
|
|
1502
|
+
};
|
|
1503
|
+
}, options),
|
|
1456
1504
|
xp: {
|
|
1457
1505
|
fetch: async (options) => {
|
|
1458
1506
|
const hasGrade = options?.grade !== undefined;
|
|
@@ -1590,22 +1638,18 @@ function createAuthNamespace(client) {
|
|
|
1590
1638
|
client.setToken(null);
|
|
1591
1639
|
},
|
|
1592
1640
|
apiKeys: {
|
|
1593
|
-
create: async (options) => {
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
dev: ["read", "write"]
|
|
1602
|
-
}
|
|
1641
|
+
create: async (options) => client["request"]("/dev/api-keys", "POST", {
|
|
1642
|
+
body: {
|
|
1643
|
+
name: options?.name || `SDK Key - ${new Date().toISOString()}`,
|
|
1644
|
+
expiresIn: options?.expiresIn !== undefined ? options.expiresIn : null,
|
|
1645
|
+
permissions: options?.permissions || {
|
|
1646
|
+
games: ["read", "write", "delete"],
|
|
1647
|
+
users: ["read:self", "write:self"],
|
|
1648
|
+
dev: ["read", "write"]
|
|
1603
1649
|
}
|
|
1604
|
-
}
|
|
1605
|
-
},
|
|
1606
|
-
list: async () =>
|
|
1607
|
-
return client["request"]("/auth/api-key/list", "GET");
|
|
1608
|
-
},
|
|
1650
|
+
}
|
|
1651
|
+
}),
|
|
1652
|
+
list: async () => client["request"]("/auth/api-key/list", "GET"),
|
|
1609
1653
|
revoke: async (keyId) => {
|
|
1610
1654
|
await client["request"]("/auth/api-key/revoke", "POST", {
|
|
1611
1655
|
body: { id: keyId }
|
|
@@ -1648,19 +1692,139 @@ function createAdminNamespace(client) {
|
|
|
1648
1692
|
}
|
|
1649
1693
|
};
|
|
1650
1694
|
}
|
|
1651
|
-
// src/
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1695
|
+
// src/core/deploy.ts
|
|
1696
|
+
class DeployPipeline {
|
|
1697
|
+
static POLL_INTERVAL_MS = 1000;
|
|
1698
|
+
static INACTIVITY_TIMEOUT_MS = 60 * 1000;
|
|
1699
|
+
static GAME_FETCH_RETRIES = 3;
|
|
1700
|
+
static MAX_INLINE_REQUEST_BYTES = 5.5 * 1024 * 1024;
|
|
1701
|
+
static textEncoder = new TextEncoder;
|
|
1702
|
+
client;
|
|
1703
|
+
constructor(client) {
|
|
1704
|
+
this.client = client;
|
|
1705
|
+
}
|
|
1706
|
+
async uploadFile(file, gameId, hooks) {
|
|
1707
|
+
const fileName = file instanceof File ? file.name : "game.zip";
|
|
1708
|
+
const { presignedUrl, uploadToken } = await this.initiateUpload(fileName, gameId);
|
|
1709
|
+
const contentType = file.type || "application/octet-stream";
|
|
1710
|
+
if (hooks?.onEvent && typeof XMLHttpRequest !== "undefined") {
|
|
1711
|
+
await this.uploadViaXHR(presignedUrl, file, contentType, hooks);
|
|
1712
|
+
} else {
|
|
1713
|
+
await this.uploadViaFetch(presignedUrl, file, contentType);
|
|
1714
|
+
}
|
|
1715
|
+
return uploadToken;
|
|
1716
|
+
}
|
|
1717
|
+
async submit(args) {
|
|
1718
|
+
const { requestBody } = await this.buildRequestBody(args);
|
|
1719
|
+
const { slug, hooks } = args;
|
|
1720
|
+
const job = await this.client["request"](`/games/${slug}/deploy`, "POST", { body: requestBody });
|
|
1721
|
+
const completedJob = await this.poll(slug, job.id, hooks);
|
|
1722
|
+
if (!completedJob.result?.url) {
|
|
1723
|
+
throw new Error("Deployment completed but no deployment URL was recorded");
|
|
1724
|
+
}
|
|
1725
|
+
return this.fetchGameWithRetry(slug);
|
|
1726
|
+
}
|
|
1727
|
+
async buildRequestBody(args) {
|
|
1728
|
+
const game = await this.resolveGame(args.slug, args.game);
|
|
1729
|
+
const requestBody = {};
|
|
1730
|
+
if (args.uploadToken) {
|
|
1731
|
+
requestBody.uploadToken = args.uploadToken;
|
|
1732
|
+
}
|
|
1733
|
+
if (args.metadata) {
|
|
1734
|
+
requestBody.metadata = args.metadata;
|
|
1735
|
+
}
|
|
1736
|
+
if (!args.backend) {
|
|
1737
|
+
return { game, requestBody };
|
|
1738
|
+
}
|
|
1739
|
+
const backendFields = {
|
|
1740
|
+
config: args.backend.config,
|
|
1741
|
+
...args.backend.bindings ? { bindings: args.backend.bindings } : {},
|
|
1742
|
+
...args.backend.schema ? { schema: args.backend.schema } : {}
|
|
1743
|
+
};
|
|
1744
|
+
const inlineBody = {
|
|
1745
|
+
...requestBody,
|
|
1746
|
+
...backendFields,
|
|
1747
|
+
code: args.backend.code
|
|
1748
|
+
};
|
|
1749
|
+
if (this.serializedSize(inlineBody) <= DeployPipeline.MAX_INLINE_REQUEST_BYTES) {
|
|
1750
|
+
return { game, requestBody: inlineBody };
|
|
1751
|
+
}
|
|
1752
|
+
const skeletonBody = {
|
|
1753
|
+
...requestBody,
|
|
1754
|
+
...backendFields,
|
|
1755
|
+
codeUploadToken: "________placeholder________"
|
|
1756
|
+
};
|
|
1757
|
+
if (this.serializedSize(skeletonBody) > DeployPipeline.MAX_INLINE_REQUEST_BYTES) {
|
|
1758
|
+
throw new Error("Deploy request is too large even after uploading backend code");
|
|
1759
|
+
}
|
|
1760
|
+
skeletonBody.codeUploadToken = await this.uploadCode(game.id, args.backend.code);
|
|
1761
|
+
return { game, requestBody: skeletonBody };
|
|
1762
|
+
}
|
|
1763
|
+
serializedSize(body) {
|
|
1764
|
+
return DeployPipeline.textEncoder.encode(JSON.stringify(body)).length;
|
|
1765
|
+
}
|
|
1766
|
+
async initiateUpload(fileName, gameId) {
|
|
1767
|
+
return this.client["request"]("/games/uploads/initiate/", "POST", { body: { fileName, gameId } });
|
|
1768
|
+
}
|
|
1769
|
+
async uploadCode(gameId, code) {
|
|
1770
|
+
const { presignedUrl, uploadToken } = await this.initiateUpload("backend.js", gameId);
|
|
1771
|
+
const res = await fetch(presignedUrl, {
|
|
1772
|
+
method: "PUT",
|
|
1773
|
+
body: code,
|
|
1774
|
+
headers: { "Content-Type": "application/javascript" }
|
|
1775
|
+
});
|
|
1776
|
+
if (!res.ok) {
|
|
1777
|
+
throw new Error(`Backend code upload failed: ${res.status} ${res.statusText}`);
|
|
1778
|
+
}
|
|
1779
|
+
return uploadToken;
|
|
1780
|
+
}
|
|
1781
|
+
async uploadViaFetch(url, body, contentType) {
|
|
1782
|
+
const res = await fetch(url, {
|
|
1783
|
+
method: "PUT",
|
|
1784
|
+
body,
|
|
1785
|
+
headers: { "Content-Type": contentType }
|
|
1786
|
+
});
|
|
1787
|
+
if (!res.ok) {
|
|
1788
|
+
throw new Error(`File upload failed: ${res.status} ${res.statusText}`);
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
uploadViaXHR(url, body, contentType, hooks) {
|
|
1792
|
+
return new Promise((resolve, reject) => {
|
|
1793
|
+
const xhr = new XMLHttpRequest;
|
|
1794
|
+
xhr.open("PUT", url, true);
|
|
1795
|
+
try {
|
|
1796
|
+
xhr.setRequestHeader("Content-Type", contentType);
|
|
1797
|
+
} catch {}
|
|
1798
|
+
xhr.upload.addEventListener("progress", (event) => {
|
|
1799
|
+
if (event.lengthComputable) {
|
|
1800
|
+
hooks.onEvent?.({
|
|
1801
|
+
type: "s3Progress",
|
|
1802
|
+
loaded: event.loaded,
|
|
1803
|
+
total: event.total,
|
|
1804
|
+
percent: event.loaded / event.total
|
|
1805
|
+
});
|
|
1806
|
+
}
|
|
1807
|
+
});
|
|
1808
|
+
xhr.addEventListener("load", () => {
|
|
1809
|
+
if (xhr.status >= 200 && xhr.status < 300) {
|
|
1810
|
+
resolve();
|
|
1811
|
+
} else {
|
|
1812
|
+
reject(new Error(`File upload failed: ${xhr.status} ${xhr.statusText}`));
|
|
1813
|
+
}
|
|
1814
|
+
});
|
|
1815
|
+
xhr.addEventListener("error", () => reject(new Error("File upload failed: network error")));
|
|
1816
|
+
xhr.send(body);
|
|
1817
|
+
});
|
|
1818
|
+
}
|
|
1819
|
+
async poll(slug, jobId, hooks) {
|
|
1656
1820
|
hooks?.onEvent?.({ type: "finalizeStart" });
|
|
1657
1821
|
let seenEvents = 0;
|
|
1658
1822
|
let lastProgressAt = Date.now();
|
|
1659
1823
|
while (true) {
|
|
1660
|
-
if (Date.now() - lastProgressAt >
|
|
1824
|
+
if (Date.now() - lastProgressAt > DeployPipeline.INACTIVITY_TIMEOUT_MS) {
|
|
1661
1825
|
throw new Error("Deployment job timed out after 1 minute without progress");
|
|
1662
1826
|
}
|
|
1663
|
-
const job = await client["request"](`/games/${slug}/deploy?jobId=${encodeURIComponent(jobId)}`, "GET");
|
|
1827
|
+
const job = await this.client["request"](`/games/${slug}/deploy?jobId=${encodeURIComponent(jobId)}`, "GET");
|
|
1664
1828
|
const newEvents = job.events.slice(seenEvents);
|
|
1665
1829
|
seenEvents = job.events.length;
|
|
1666
1830
|
if (newEvents.length > 0) {
|
|
@@ -1679,9 +1843,30 @@ function createDevNamespace(client) {
|
|
|
1679
1843
|
if (job.status === "failed") {
|
|
1680
1844
|
throw new ApiError(job.errorStatus ?? 500, job.errorCode ?? "INTERNAL_ERROR", job.error || "Deployment failed", job);
|
|
1681
1845
|
}
|
|
1682
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
1846
|
+
await new Promise((resolve) => setTimeout(resolve, DeployPipeline.POLL_INTERVAL_MS));
|
|
1683
1847
|
}
|
|
1684
1848
|
}
|
|
1849
|
+
async resolveGame(slug, game) {
|
|
1850
|
+
return game ?? await this.client["request"](`/games/${slug}`, "GET");
|
|
1851
|
+
}
|
|
1852
|
+
async fetchGameWithRetry(slug) {
|
|
1853
|
+
const { GAME_FETCH_RETRIES } = DeployPipeline;
|
|
1854
|
+
for (let attempt = 0;attempt < GAME_FETCH_RETRIES; attempt++) {
|
|
1855
|
+
try {
|
|
1856
|
+
return await this.client["request"](`/games/${slug}`, "GET");
|
|
1857
|
+
} catch {
|
|
1858
|
+
if (attempt < GAME_FETCH_RETRIES - 1) {
|
|
1859
|
+
await new Promise((r) => setTimeout(r, 500 * (attempt + 1)));
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
throw new Error(`Deploy succeeded but failed to fetch updated game after ${GAME_FETCH_RETRIES} attempts`);
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
// src/namespaces/platform/dev.ts
|
|
1868
|
+
function createDevNamespace(client) {
|
|
1869
|
+
const deploy = new DeployPipeline(client);
|
|
1685
1870
|
return {
|
|
1686
1871
|
status: {
|
|
1687
1872
|
apply: () => client["request"]("/dev/apply", "POST"),
|
|
@@ -1703,99 +1888,18 @@ function createDevNamespace(client) {
|
|
|
1703
1888
|
return game;
|
|
1704
1889
|
}
|
|
1705
1890
|
}
|
|
1706
|
-
|
|
1707
|
-
if (file) {
|
|
1708
|
-
const fileName = file instanceof File ? file.name : "game.zip";
|
|
1709
|
-
const initiateResponse = await client["request"]("/games/uploads/initiate/", "POST", {
|
|
1710
|
-
body: {
|
|
1711
|
-
fileName,
|
|
1712
|
-
gameId: game?.id || slug
|
|
1713
|
-
}
|
|
1714
|
-
});
|
|
1715
|
-
uploadToken = initiateResponse.uploadToken;
|
|
1716
|
-
if (hooks?.onEvent && typeof XMLHttpRequest !== "undefined") {
|
|
1717
|
-
await new Promise((resolve, reject) => {
|
|
1718
|
-
const xhr = new XMLHttpRequest;
|
|
1719
|
-
xhr.open("PUT", initiateResponse.presignedUrl, true);
|
|
1720
|
-
const contentType = file.type || "application/octet-stream";
|
|
1721
|
-
try {
|
|
1722
|
-
xhr.setRequestHeader("Content-Type", contentType);
|
|
1723
|
-
} catch {}
|
|
1724
|
-
xhr.upload.onprogress = (event) => {
|
|
1725
|
-
if (event.lengthComputable) {
|
|
1726
|
-
const percent = event.loaded / event.total;
|
|
1727
|
-
hooks.onEvent?.({
|
|
1728
|
-
type: "s3Progress",
|
|
1729
|
-
loaded: event.loaded,
|
|
1730
|
-
total: event.total,
|
|
1731
|
-
percent
|
|
1732
|
-
});
|
|
1733
|
-
}
|
|
1734
|
-
};
|
|
1735
|
-
xhr.onload = () => {
|
|
1736
|
-
if (xhr.status >= 200 && xhr.status < 300)
|
|
1737
|
-
resolve();
|
|
1738
|
-
else
|
|
1739
|
-
reject(new Error(`File upload failed: ${xhr.status} ${xhr.statusText}`));
|
|
1740
|
-
};
|
|
1741
|
-
xhr.onerror = () => reject(new Error("File upload failed: network error"));
|
|
1742
|
-
xhr.send(file);
|
|
1743
|
-
});
|
|
1744
|
-
} else {
|
|
1745
|
-
const uploadResponse = await fetch(initiateResponse.presignedUrl, {
|
|
1746
|
-
method: "PUT",
|
|
1747
|
-
body: file,
|
|
1748
|
-
headers: {
|
|
1749
|
-
"Content-Type": file.type || "application/octet-stream"
|
|
1750
|
-
}
|
|
1751
|
-
});
|
|
1752
|
-
if (!uploadResponse.ok) {
|
|
1753
|
-
throw new Error(`File upload failed: ${uploadResponse.status} ${uploadResponse.statusText}`);
|
|
1754
|
-
}
|
|
1755
|
-
}
|
|
1756
|
-
}
|
|
1891
|
+
const uploadToken = file ? await deploy.uploadFile(file, game?.id || slug, hooks) : undefined;
|
|
1757
1892
|
if (uploadToken || backend) {
|
|
1758
|
-
|
|
1759
|
-
if (uploadToken)
|
|
1760
|
-
requestBody.uploadToken = uploadToken;
|
|
1761
|
-
if (metadata)
|
|
1762
|
-
requestBody.metadata = metadata;
|
|
1763
|
-
if (backend) {
|
|
1764
|
-
requestBody.code = backend.code;
|
|
1765
|
-
requestBody.config = backend.config;
|
|
1766
|
-
if (backend.bindings)
|
|
1767
|
-
requestBody.bindings = backend.bindings;
|
|
1768
|
-
if (backend.schema)
|
|
1769
|
-
requestBody.schema = backend.schema;
|
|
1770
|
-
}
|
|
1771
|
-
const job = await client["request"](`/games/${slug}/deploy`, "POST", {
|
|
1772
|
-
body: requestBody
|
|
1773
|
-
});
|
|
1774
|
-
const completedJob = await pollDeployJob(slug, job.id, hooks);
|
|
1775
|
-
if (!completedJob.result?.url) {
|
|
1776
|
-
throw new Error("Deployment completed but no deployment URL was recorded");
|
|
1777
|
-
}
|
|
1778
|
-
for (let attempt = 0;attempt < 3; attempt++) {
|
|
1779
|
-
try {
|
|
1780
|
-
return await client["request"](`/games/${slug}`, "GET");
|
|
1781
|
-
} catch {
|
|
1782
|
-
if (attempt < 2) {
|
|
1783
|
-
await new Promise((r) => setTimeout(r, 500 * (attempt + 1)));
|
|
1784
|
-
}
|
|
1785
|
-
}
|
|
1786
|
-
}
|
|
1787
|
-
throw new Error("Deploy succeeded but failed to fetch updated game after 3 attempts");
|
|
1893
|
+
return deploy.submit({ slug, game, uploadToken, metadata, backend, hooks });
|
|
1788
1894
|
}
|
|
1789
1895
|
if (game) {
|
|
1790
1896
|
return game;
|
|
1791
1897
|
}
|
|
1792
1898
|
throw new Error("No deployment actions specified (need metadata, file, or backend)");
|
|
1793
1899
|
},
|
|
1794
|
-
seed: async (slug, code, environment, secrets) => {
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
});
|
|
1798
|
-
},
|
|
1900
|
+
seed: async (slug, code, environment, secrets) => client["request"](`/games/${slug}/seed`, "POST", {
|
|
1901
|
+
body: { code, environment, secrets }
|
|
1902
|
+
}),
|
|
1799
1903
|
upsert: async (slug, metadata) => client["request"](`/games/${slug}`, "PUT", { body: metadata }),
|
|
1800
1904
|
delete: (gameId) => client["request"](`/games/${gameId}`, "DELETE"),
|
|
1801
1905
|
secrets: {
|
|
@@ -1812,11 +1916,9 @@ function createDevNamespace(client) {
|
|
|
1812
1916
|
}
|
|
1813
1917
|
},
|
|
1814
1918
|
database: {
|
|
1815
|
-
reset: async (slug, schema) => {
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
});
|
|
1819
|
-
}
|
|
1919
|
+
reset: async (slug, schema) => client["request"](`/games/${slug}/database/reset`, "POST", {
|
|
1920
|
+
body: { schema }
|
|
1921
|
+
})
|
|
1820
1922
|
},
|
|
1821
1923
|
bucket: {
|
|
1822
1924
|
list: async (slug, prefix) => {
|
|
@@ -1868,9 +1970,7 @@ function createDevNamespace(client) {
|
|
|
1868
1970
|
const result = await client["request"](`/games/${slug}/kv/${encodeURIComponent(key)}/metadata`, "GET");
|
|
1869
1971
|
return result.metadata;
|
|
1870
1972
|
},
|
|
1871
|
-
stats: async (slug) => {
|
|
1872
|
-
return client["request"](`/games/${slug}/kv/stats`, "GET");
|
|
1873
|
-
},
|
|
1973
|
+
stats: async (slug) => client["request"](`/games/${slug}/kv/stats`, "GET"),
|
|
1874
1974
|
seed: async (slug, entries) => {
|
|
1875
1975
|
const result = await client["request"](`/games/${slug}/kv/bulk`, "PUT", { body: { entries } });
|
|
1876
1976
|
return result.count;
|
|
@@ -1881,11 +1981,9 @@ function createDevNamespace(client) {
|
|
|
1881
1981
|
}
|
|
1882
1982
|
},
|
|
1883
1983
|
domains: {
|
|
1884
|
-
add: async (slug, hostname) => {
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
});
|
|
1888
|
-
},
|
|
1984
|
+
add: async (slug, hostname) => client["request"](`/games/${slug}/domains`, "POST", {
|
|
1985
|
+
body: { hostname }
|
|
1986
|
+
}),
|
|
1889
1987
|
list: async (slug) => {
|
|
1890
1988
|
const result = await client["request"](`/games/${slug}/domains`, "GET");
|
|
1891
1989
|
return result.domains;
|
|
@@ -1899,9 +1997,7 @@ function createDevNamespace(client) {
|
|
|
1899
1997
|
}
|
|
1900
1998
|
},
|
|
1901
1999
|
logs: {
|
|
1902
|
-
getToken: async (slug, environment) => {
|
|
1903
|
-
return client["request"](`/games/${slug}/logs/token`, "POST", { body: { environment } });
|
|
1904
|
-
}
|
|
2000
|
+
getToken: async (slug, environment) => client["request"](`/games/${slug}/logs/token`, "POST", { body: { environment } })
|
|
1905
2001
|
}
|
|
1906
2002
|
},
|
|
1907
2003
|
items: {
|
|
@@ -1921,35 +2017,27 @@ function createDevNamespace(client) {
|
|
|
1921
2017
|
},
|
|
1922
2018
|
delete: (gameId, itemId) => client["request"](`/games/${gameId}/items/${itemId}`, "DELETE"),
|
|
1923
2019
|
shop: {
|
|
1924
|
-
create: (gameId, itemId, listingData) => {
|
|
1925
|
-
|
|
1926
|
-
},
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
},
|
|
1930
|
-
update: (gameId, itemId, updates) => {
|
|
1931
|
-
return client["request"](`/games/${gameId}/items/${itemId}/shop-listing`, "PATCH", { body: updates });
|
|
1932
|
-
},
|
|
1933
|
-
delete: (gameId, itemId) => {
|
|
1934
|
-
return client["request"](`/games/${gameId}/items/${itemId}/shop-listing`, "DELETE");
|
|
1935
|
-
},
|
|
1936
|
-
list: (gameId) => {
|
|
1937
|
-
return client["request"](`/games/${gameId}/shop-listings`, "GET");
|
|
1938
|
-
}
|
|
2020
|
+
create: (gameId, itemId, listingData) => client["request"](`/games/${gameId}/items/${itemId}/shop-listing`, "POST", { body: listingData }),
|
|
2021
|
+
get: (gameId, itemId) => client["request"](`/games/${gameId}/items/${itemId}/shop-listing`, "GET"),
|
|
2022
|
+
update: (gameId, itemId, updates) => client["request"](`/games/${gameId}/items/${itemId}/shop-listing`, "PATCH", { body: updates }),
|
|
2023
|
+
delete: (gameId, itemId) => client["request"](`/games/${gameId}/items/${itemId}/shop-listing`, "DELETE"),
|
|
2024
|
+
list: (gameId) => client["request"](`/games/${gameId}/shop-listings`, "GET")
|
|
1939
2025
|
}
|
|
1940
2026
|
}
|
|
1941
2027
|
};
|
|
1942
2028
|
}
|
|
1943
2029
|
// src/core/request.ts
|
|
1944
2030
|
function checkDevWarnings(data) {
|
|
1945
|
-
if (!data || typeof data !== "object")
|
|
2031
|
+
if (!data || typeof data !== "object") {
|
|
1946
2032
|
return;
|
|
2033
|
+
}
|
|
1947
2034
|
const response = data;
|
|
1948
2035
|
const warningType = response.__playcademyDevWarning;
|
|
1949
|
-
if (!warningType)
|
|
2036
|
+
if (!warningType) {
|
|
1950
2037
|
return;
|
|
2038
|
+
}
|
|
1951
2039
|
switch (warningType) {
|
|
1952
|
-
case "timeback-not-configured":
|
|
2040
|
+
case "timeback-not-configured": {
|
|
1953
2041
|
console.warn("%c⚠️ TimeBack Not Configured", "background: #f59e0b; color: white; padding: 6px 12px; border-radius: 4px; font-weight: bold; font-size: 13px");
|
|
1954
2042
|
console.log("%cTimeBack is configured in playcademy.config.js but the sandbox does not have TimeBack credentials.", "color: #f59e0b; font-weight: 500");
|
|
1955
2043
|
console.log("To test TimeBack locally:");
|
|
@@ -1960,8 +2048,10 @@ function checkDevWarnings(data) {
|
|
|
1960
2048
|
console.log(" Or deploy your game: %cplaycademy deploy", "color: #10b981; font-weight: 600; font-family: monospace");
|
|
1961
2049
|
console.log(" Or wait for %c@superbuilders/timeback-local%c (coming soon)", "color: #8b5cf6; font-weight: 600; font-family: monospace", "color: inherit");
|
|
1962
2050
|
break;
|
|
1963
|
-
|
|
2051
|
+
}
|
|
2052
|
+
default: {
|
|
1964
2053
|
console.warn(`[Playcademy Dev Warning] ${warningType}`);
|
|
2054
|
+
}
|
|
1965
2055
|
}
|
|
1966
2056
|
}
|
|
1967
2057
|
function prepareRequestBody(body, headers) {
|
|
@@ -2010,18 +2100,20 @@ async function request({
|
|
|
2010
2100
|
})) ?? undefined;
|
|
2011
2101
|
throw ApiError.fromResponse(res.status, res.statusText, errorBody);
|
|
2012
2102
|
}
|
|
2013
|
-
if (res.status === 204)
|
|
2103
|
+
if (res.status === 204) {
|
|
2014
2104
|
return;
|
|
2105
|
+
}
|
|
2015
2106
|
const contentType = res.headers.get("content-type") ?? "";
|
|
2016
2107
|
if (contentType.includes("application/json")) {
|
|
2017
2108
|
try {
|
|
2018
2109
|
const parsed = await res.json();
|
|
2019
2110
|
checkDevWarnings(parsed);
|
|
2020
2111
|
return parsed;
|
|
2021
|
-
} catch (
|
|
2022
|
-
if (
|
|
2112
|
+
} catch (error) {
|
|
2113
|
+
if (error instanceof SyntaxError) {
|
|
2023
2114
|
return;
|
|
2024
|
-
|
|
2115
|
+
}
|
|
2116
|
+
throw error;
|
|
2025
2117
|
}
|
|
2026
2118
|
}
|
|
2027
2119
|
const rawText = await res.text().catch(() => "");
|
|
@@ -2069,12 +2161,8 @@ function createGamesNamespace(client) {
|
|
|
2069
2161
|
return baseGameData;
|
|
2070
2162
|
}, options);
|
|
2071
2163
|
},
|
|
2072
|
-
list: (options) =>
|
|
2073
|
-
|
|
2074
|
-
},
|
|
2075
|
-
getSubjects: () => {
|
|
2076
|
-
return client["request"]("/games/subjects", "GET");
|
|
2077
|
-
},
|
|
2164
|
+
list: (options) => gamesListCache.get("all", () => client["request"]("/games", "GET"), options),
|
|
2165
|
+
getSubjects: () => client["request"]("/games/subjects", "GET"),
|
|
2078
2166
|
startSession: async (gameId) => {
|
|
2079
2167
|
const idToUse = gameId ?? client["_ensureGameId"]();
|
|
2080
2168
|
return client["request"](`/games/${idToUse}/sessions`, "POST", {});
|
|
@@ -2098,10 +2186,12 @@ function createGamesNamespace(client) {
|
|
|
2098
2186
|
leaderboard: {
|
|
2099
2187
|
get: async (gameId, options) => {
|
|
2100
2188
|
const params = new URLSearchParams;
|
|
2101
|
-
if (options?.limit)
|
|
2189
|
+
if (options?.limit) {
|
|
2102
2190
|
params.append("limit", String(options.limit));
|
|
2103
|
-
|
|
2191
|
+
}
|
|
2192
|
+
if (options?.offset) {
|
|
2104
2193
|
params.append("offset", String(options.offset));
|
|
2194
|
+
}
|
|
2105
2195
|
const queryString = params.toString();
|
|
2106
2196
|
const path = queryString ? `/games/${gameId}/leaderboard?${queryString}` : `/games/${gameId}/leaderboard`;
|
|
2107
2197
|
return client["request"](path, "GET");
|
|
@@ -2129,14 +2219,10 @@ function createCharacterNamespace(client) {
|
|
|
2129
2219
|
throw error;
|
|
2130
2220
|
}
|
|
2131
2221
|
},
|
|
2132
|
-
create: async (characterData) => {
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
},
|
|
2137
|
-
update: async (updates) => {
|
|
2138
|
-
return client["request"]("/character", "PATCH", { body: updates });
|
|
2139
|
-
},
|
|
2222
|
+
create: async (characterData) => client["request"]("/character", "POST", {
|
|
2223
|
+
body: characterData
|
|
2224
|
+
}),
|
|
2225
|
+
update: async (updates) => client["request"]("/character", "PATCH", { body: updates }),
|
|
2140
2226
|
components: {
|
|
2141
2227
|
list: async (options) => {
|
|
2142
2228
|
const cacheKey = options?.level === undefined ? "all" : String(options.level);
|
|
@@ -2150,12 +2236,8 @@ function createCharacterNamespace(client) {
|
|
|
2150
2236
|
getCacheKeys: () => componentCache.getKeys()
|
|
2151
2237
|
},
|
|
2152
2238
|
accessories: {
|
|
2153
|
-
equip: async (slot, componentId) => {
|
|
2154
|
-
|
|
2155
|
-
},
|
|
2156
|
-
remove: async (slot) => {
|
|
2157
|
-
return client["request"](`/character/accessories/${slot}`, "DELETE");
|
|
2158
|
-
},
|
|
2239
|
+
equip: async (slot, componentId) => client["request"]("/character/accessories/equip", "POST", { body: { slot, accessoryComponentId: componentId } }),
|
|
2240
|
+
remove: async (slot) => client["request"](`/character/accessories/${slot}`, "DELETE"),
|
|
2159
2241
|
list: async () => {
|
|
2160
2242
|
const character2 = await client.character.get();
|
|
2161
2243
|
return character2?.accessories || [];
|
|
@@ -2174,14 +2256,13 @@ function createAchievementsNamespace(client) {
|
|
|
2174
2256
|
keyPrefix: "achievements.history"
|
|
2175
2257
|
});
|
|
2176
2258
|
return {
|
|
2177
|
-
list: (options) =>
|
|
2178
|
-
return achievementsListCache.get("current", () => client["request"]("/achievements/current", "GET"), options);
|
|
2179
|
-
},
|
|
2259
|
+
list: (options) => achievementsListCache.get("current", () => client["request"]("/achievements/current", "GET"), options),
|
|
2180
2260
|
history: {
|
|
2181
2261
|
list: async (queryOptions, cacheOptions) => {
|
|
2182
2262
|
const params = new URLSearchParams;
|
|
2183
|
-
if (queryOptions?.limit)
|
|
2263
|
+
if (queryOptions?.limit) {
|
|
2184
2264
|
params.append("limit", String(queryOptions.limit));
|
|
2265
|
+
}
|
|
2185
2266
|
const qs = params.toString();
|
|
2186
2267
|
const path = qs ? `/achievements/history?${qs}` : "/achievements/history";
|
|
2187
2268
|
const cacheKey = qs ? `history-${qs}` : "history";
|
|
@@ -2209,9 +2290,7 @@ function createLeaderboardNamespace(client) {
|
|
|
2209
2290
|
}
|
|
2210
2291
|
return client["request"](`/leaderboard?${params}`, "GET");
|
|
2211
2292
|
},
|
|
2212
|
-
getUserRank: async (gameId, userId) => {
|
|
2213
|
-
return client["request"](`/games/${gameId}/users/${userId}/rank`, "GET");
|
|
2214
|
-
}
|
|
2293
|
+
getUserRank: async (gameId, userId) => client["request"](`/games/${gameId}/users/${userId}/rank`, "GET")
|
|
2215
2294
|
};
|
|
2216
2295
|
}
|
|
2217
2296
|
// src/core/cache/cooldown-cache.ts
|
|
@@ -2265,28 +2344,18 @@ function createCooldownCache(defaultCooldownMs) {
|
|
|
2265
2344
|
function createLevelsNamespace(client) {
|
|
2266
2345
|
const progressCache = createCooldownCache(5000);
|
|
2267
2346
|
return {
|
|
2268
|
-
get: async () =>
|
|
2269
|
-
|
|
2270
|
-
},
|
|
2271
|
-
progress: async (options) => {
|
|
2272
|
-
return progressCache.get("user-progress", () => client["request"]("/users/level/progress", "GET"), options);
|
|
2273
|
-
},
|
|
2347
|
+
get: async () => client["request"]("/users/level", "GET"),
|
|
2348
|
+
progress: async (options) => progressCache.get("user-progress", () => client["request"]("/users/level/progress", "GET"), options),
|
|
2274
2349
|
config: {
|
|
2275
|
-
list: async () =>
|
|
2276
|
-
|
|
2277
|
-
},
|
|
2278
|
-
get: async (level) => {
|
|
2279
|
-
return client["request"](`/levels/config/${level}`, "GET");
|
|
2280
|
-
}
|
|
2350
|
+
list: async () => client["request"]("/levels/config", "GET"),
|
|
2351
|
+
get: async (level) => client["request"](`/levels/config/${level}`, "GET")
|
|
2281
2352
|
}
|
|
2282
2353
|
};
|
|
2283
2354
|
}
|
|
2284
2355
|
// src/namespaces/platform/shop.ts
|
|
2285
2356
|
function createShopNamespace(client) {
|
|
2286
2357
|
return {
|
|
2287
|
-
view: () =>
|
|
2288
|
-
return client["request"]("/shop/view", "GET");
|
|
2289
|
-
}
|
|
2358
|
+
view: () => client["request"]("/shop/view", "GET")
|
|
2290
2359
|
};
|
|
2291
2360
|
}
|
|
2292
2361
|
// src/namespaces/platform/notifications.ts
|
|
@@ -2302,14 +2371,18 @@ function createNotificationsNamespace(client) {
|
|
|
2302
2371
|
return {
|
|
2303
2372
|
list: async (queryOptions, cacheOptions) => {
|
|
2304
2373
|
const params = new URLSearchParams;
|
|
2305
|
-
if (queryOptions?.status)
|
|
2374
|
+
if (queryOptions?.status) {
|
|
2306
2375
|
params.append("status", queryOptions.status);
|
|
2307
|
-
|
|
2376
|
+
}
|
|
2377
|
+
if (queryOptions?.type) {
|
|
2308
2378
|
params.append("type", queryOptions.type);
|
|
2309
|
-
|
|
2379
|
+
}
|
|
2380
|
+
if (queryOptions?.limit) {
|
|
2310
2381
|
params.append("limit", String(queryOptions.limit));
|
|
2311
|
-
|
|
2382
|
+
}
|
|
2383
|
+
if (queryOptions?.offset) {
|
|
2312
2384
|
params.append("offset", String(queryOptions.offset));
|
|
2385
|
+
}
|
|
2313
2386
|
const qs = params.toString();
|
|
2314
2387
|
const path = qs ? `/notifications?${qs}` : "/notifications";
|
|
2315
2388
|
const cacheKey = qs ? `list-${qs}` : "list";
|
|
@@ -2365,10 +2438,12 @@ function createNotificationsNamespace(client) {
|
|
|
2365
2438
|
get: async (queryOptions, cacheOptions) => {
|
|
2366
2439
|
const user = await client.users.me();
|
|
2367
2440
|
const params = new URLSearchParams;
|
|
2368
|
-
if (queryOptions?.from)
|
|
2441
|
+
if (queryOptions?.from) {
|
|
2369
2442
|
params.append("from", queryOptions.from);
|
|
2370
|
-
|
|
2443
|
+
}
|
|
2444
|
+
if (queryOptions?.to) {
|
|
2371
2445
|
params.append("to", queryOptions.to);
|
|
2446
|
+
}
|
|
2372
2447
|
const qs = params.toString();
|
|
2373
2448
|
const path = qs ? `/notifications/stats/${user.id}?${qs}` : `/notifications/stats/${user.id}`;
|
|
2374
2449
|
const cacheKey = qs ? `stats-${qs}` : "stats";
|
|
@@ -2405,11 +2480,10 @@ function createSpritesNamespace(client) {
|
|
|
2405
2480
|
return {
|
|
2406
2481
|
templates: {
|
|
2407
2482
|
get: async (slug) => {
|
|
2408
|
-
if (!slug)
|
|
2483
|
+
if (!slug) {
|
|
2409
2484
|
throw new Error("Sprite template slug is required");
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
});
|
|
2485
|
+
}
|
|
2486
|
+
const templateMeta = await templateUrlCache.get(slug, async () => client["request"](`/sprites/templates/${slug}`, "GET"));
|
|
2413
2487
|
if (!templateMeta.url) {
|
|
2414
2488
|
throw new Error(`Template ${slug} has no URL in database`);
|
|
2415
2489
|
}
|
|
@@ -2459,7 +2533,9 @@ function createTimebackNamespace2(client) {
|
|
|
2459
2533
|
ttl: 30 * 1000,
|
|
2460
2534
|
keyPrefix: "platform.timeback.students"
|
|
2461
2535
|
});
|
|
2462
|
-
|
|
2536
|
+
function getTimeback() {
|
|
2537
|
+
return client["initPayload"]?.timeback;
|
|
2538
|
+
}
|
|
2463
2539
|
return {
|
|
2464
2540
|
get user() {
|
|
2465
2541
|
return {
|
|
@@ -2475,26 +2551,22 @@ function createTimebackNamespace2(client) {
|
|
|
2475
2551
|
get organizations() {
|
|
2476
2552
|
return getTimeback()?.organizations ?? [];
|
|
2477
2553
|
},
|
|
2478
|
-
fetch: async (options) => {
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
}, options);
|
|
2492
|
-
}
|
|
2554
|
+
fetch: async (options) => userCache.get("current", async () => {
|
|
2555
|
+
const response = await client["request"]("/timeback/user", "GET");
|
|
2556
|
+
const initPayload = client["initPayload"];
|
|
2557
|
+
if (initPayload) {
|
|
2558
|
+
initPayload.timeback = response;
|
|
2559
|
+
}
|
|
2560
|
+
return {
|
|
2561
|
+
id: response.id,
|
|
2562
|
+
role: response.role,
|
|
2563
|
+
enrollments: response.enrollments,
|
|
2564
|
+
organizations: response.organizations
|
|
2565
|
+
};
|
|
2566
|
+
}, options)
|
|
2493
2567
|
};
|
|
2494
2568
|
},
|
|
2495
|
-
populateStudent: async (names) => {
|
|
2496
|
-
return client["request"]("/timeback/populate-student", "POST", names ? { body: names } : undefined);
|
|
2497
|
-
},
|
|
2569
|
+
populateStudent: async (names) => client["request"]("/timeback/populate-student", "POST", names ? { body: names } : undefined),
|
|
2498
2570
|
startActivity: (_metadata) => {
|
|
2499
2571
|
throw new Error(NOT_SUPPORTED);
|
|
2500
2572
|
},
|
|
@@ -2508,44 +2580,36 @@ function createTimebackNamespace2(client) {
|
|
|
2508
2580
|
throw new Error(NOT_SUPPORTED);
|
|
2509
2581
|
},
|
|
2510
2582
|
management: {
|
|
2511
|
-
setup: (request2) => {
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
},
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
},
|
|
2519
|
-
cleanup: (gameId) => {
|
|
2520
|
-
return client["request"](`/timeback/integrations/${gameId}`, "DELETE");
|
|
2521
|
-
},
|
|
2522
|
-
get: (gameId) => {
|
|
2523
|
-
return client["request"](`/timeback/integrations/${gameId}`, "GET");
|
|
2524
|
-
},
|
|
2525
|
-
getConfig: (gameId) => {
|
|
2526
|
-
return client["request"](`/timeback/config/${gameId}`, "GET");
|
|
2527
|
-
}
|
|
2583
|
+
setup: (request2) => client["request"]("/timeback/setup", "POST", {
|
|
2584
|
+
body: request2
|
|
2585
|
+
}),
|
|
2586
|
+
verify: (gameId) => client["request"](`/timeback/verify/${gameId}`, "GET"),
|
|
2587
|
+
cleanup: (gameId) => client["request"](`/timeback/integrations/${gameId}`, "DELETE"),
|
|
2588
|
+
get: (gameId) => client["request"](`/timeback/integrations/${gameId}`, "GET"),
|
|
2589
|
+
getConfig: (gameId) => client["request"](`/timeback/config/${gameId}`, "GET")
|
|
2528
2590
|
},
|
|
2529
2591
|
xp: {
|
|
2530
2592
|
today: async (options) => {
|
|
2531
2593
|
const params = new URLSearchParams;
|
|
2532
|
-
if (options?.date)
|
|
2594
|
+
if (options?.date) {
|
|
2533
2595
|
params.set("date", options.date);
|
|
2534
|
-
|
|
2596
|
+
}
|
|
2597
|
+
if (options?.timezone) {
|
|
2535
2598
|
params.set("tz", options.timezone);
|
|
2599
|
+
}
|
|
2536
2600
|
const query = params.toString();
|
|
2537
2601
|
const endpoint = query ? `/timeback/xp/today?${query}` : "/timeback/xp/today";
|
|
2538
2602
|
return client["request"](endpoint, "GET");
|
|
2539
2603
|
},
|
|
2540
|
-
total: async () =>
|
|
2541
|
-
return client["request"]("/timeback/xp/total", "GET");
|
|
2542
|
-
},
|
|
2604
|
+
total: async () => client["request"]("/timeback/xp/total", "GET"),
|
|
2543
2605
|
history: async (options) => {
|
|
2544
2606
|
const params = new URLSearchParams;
|
|
2545
|
-
if (options?.startDate)
|
|
2607
|
+
if (options?.startDate) {
|
|
2546
2608
|
params.set("startDate", options.startDate);
|
|
2547
|
-
|
|
2609
|
+
}
|
|
2610
|
+
if (options?.endDate) {
|
|
2548
2611
|
params.set("endDate", options.endDate);
|
|
2612
|
+
}
|
|
2549
2613
|
const query = params.toString();
|
|
2550
2614
|
const endpoint = query ? `/timeback/xp/history?${query}` : "/timeback/xp/history";
|
|
2551
2615
|
return client["request"](endpoint, "GET");
|
|
@@ -2554,10 +2618,12 @@ function createTimebackNamespace2(client) {
|
|
|
2554
2618
|
const [today, total] = await Promise.all([
|
|
2555
2619
|
client["request"]((() => {
|
|
2556
2620
|
const params = new URLSearchParams;
|
|
2557
|
-
if (options?.date)
|
|
2621
|
+
if (options?.date) {
|
|
2558
2622
|
params.set("date", options.date);
|
|
2559
|
-
|
|
2623
|
+
}
|
|
2624
|
+
if (options?.timezone) {
|
|
2560
2625
|
params.set("tz", options.timezone);
|
|
2626
|
+
}
|
|
2561
2627
|
const query = params.toString();
|
|
2562
2628
|
return query ? `/timeback/xp/today?${query}` : "/timeback/xp/today";
|
|
2563
2629
|
})(), "GET"),
|
|
@@ -2567,11 +2633,7 @@ function createTimebackNamespace2(client) {
|
|
|
2567
2633
|
}
|
|
2568
2634
|
},
|
|
2569
2635
|
students: {
|
|
2570
|
-
get: async (timebackId, options) => {
|
|
2571
|
-
return studentCache.get(timebackId, async () => {
|
|
2572
|
-
return client["request"](`/timeback/user/${timebackId}`, "GET");
|
|
2573
|
-
}, options);
|
|
2574
|
-
},
|
|
2636
|
+
get: async (timebackId, options) => studentCache.get(timebackId, async () => client["request"](`/timeback/user/${timebackId}`, "GET"), options),
|
|
2575
2637
|
clearCache: (timebackId) => {
|
|
2576
2638
|
studentCache.clear(timebackId);
|
|
2577
2639
|
}
|
|
@@ -2677,24 +2739,26 @@ class ConnectionMonitor {
|
|
|
2677
2739
|
this._detectInitialState();
|
|
2678
2740
|
}
|
|
2679
2741
|
start() {
|
|
2680
|
-
if (this.isMonitoring)
|
|
2742
|
+
if (this.isMonitoring) {
|
|
2681
2743
|
return;
|
|
2744
|
+
}
|
|
2682
2745
|
this.isMonitoring = true;
|
|
2683
|
-
if (this.config.enableOfflineEvents && typeof window !== "undefined") {
|
|
2684
|
-
|
|
2685
|
-
|
|
2746
|
+
if (this.config.enableOfflineEvents && typeof globalThis.window !== "undefined") {
|
|
2747
|
+
globalThis.addEventListener("online", this._handleOnline);
|
|
2748
|
+
globalThis.addEventListener("offline", this._handleOffline);
|
|
2686
2749
|
}
|
|
2687
2750
|
if (this.config.enableHeartbeat) {
|
|
2688
2751
|
this._startHeartbeat();
|
|
2689
2752
|
}
|
|
2690
2753
|
}
|
|
2691
2754
|
stop() {
|
|
2692
|
-
if (!this.isMonitoring)
|
|
2755
|
+
if (!this.isMonitoring) {
|
|
2693
2756
|
return;
|
|
2757
|
+
}
|
|
2694
2758
|
this.isMonitoring = false;
|
|
2695
|
-
if (typeof window !== "undefined") {
|
|
2696
|
-
|
|
2697
|
-
|
|
2759
|
+
if (typeof globalThis.window !== "undefined") {
|
|
2760
|
+
globalThis.removeEventListener("online", this._handleOnline);
|
|
2761
|
+
globalThis.removeEventListener("offline", this._handleOffline);
|
|
2698
2762
|
}
|
|
2699
2763
|
if (this.heartbeatInterval) {
|
|
2700
2764
|
clearInterval(this.heartbeatInterval);
|
|
@@ -2714,8 +2778,9 @@ class ConnectionMonitor {
|
|
|
2714
2778
|
}
|
|
2715
2779
|
reportRequestFailure(error) {
|
|
2716
2780
|
const isNetworkError = error instanceof TypeError || error instanceof Error && error.message.includes("fetch");
|
|
2717
|
-
if (!isNetworkError)
|
|
2781
|
+
if (!isNetworkError) {
|
|
2718
2782
|
return;
|
|
2783
|
+
}
|
|
2719
2784
|
this.consecutiveFailures++;
|
|
2720
2785
|
if (this.consecutiveFailures >= this.config.failureThreshold) {
|
|
2721
2786
|
this._setState("degraded", "Multiple consecutive request failures");
|
|
@@ -2783,8 +2848,9 @@ class ConnectionMonitor {
|
|
|
2783
2848
|
}
|
|
2784
2849
|
}
|
|
2785
2850
|
_setState(newState, reason) {
|
|
2786
|
-
if (this.state === newState)
|
|
2851
|
+
if (this.state === newState) {
|
|
2787
2852
|
return;
|
|
2853
|
+
}
|
|
2788
2854
|
const oldState = this.state;
|
|
2789
2855
|
this.state = newState;
|
|
2790
2856
|
console.debug(`[ConnectionMonitor] ${oldState} → ${newState}: ${reason}`);
|
|
@@ -2800,14 +2866,15 @@ class ConnectionMonitor {
|
|
|
2800
2866
|
// src/core/connection/utils.ts
|
|
2801
2867
|
function createDisplayAlert(authContext) {
|
|
2802
2868
|
return (message, options) => {
|
|
2803
|
-
if (authContext?.isInIframe && typeof window !== "undefined" && window.parent !== window) {
|
|
2869
|
+
if (authContext?.isInIframe && typeof globalThis.window !== "undefined" && globalThis.window.parent !== globalThis.window) {
|
|
2804
2870
|
window.parent.postMessage({
|
|
2805
2871
|
type: "PLAYCADEMY_DISPLAY_ALERT",
|
|
2806
2872
|
message,
|
|
2807
2873
|
options
|
|
2808
2874
|
}, "*");
|
|
2809
2875
|
} else {
|
|
2810
|
-
const
|
|
2876
|
+
const prefixMap = { error: "❌", warning: "⚠️", info: "ℹ️" };
|
|
2877
|
+
const prefix = (options?.type && prefixMap[options.type]) ?? "ℹ️";
|
|
2811
2878
|
console.log(`${prefix} ${message}`);
|
|
2812
2879
|
}
|
|
2813
2880
|
};
|
|
@@ -2890,12 +2957,8 @@ class PlaycademyBaseClient {
|
|
|
2890
2957
|
initPayload;
|
|
2891
2958
|
connectionManager;
|
|
2892
2959
|
_sessionManager = {
|
|
2893
|
-
startSession: async (gameId) => {
|
|
2894
|
-
|
|
2895
|
-
},
|
|
2896
|
-
endSession: async (sessionId, gameId) => {
|
|
2897
|
-
return this.request(`/games/${gameId}/sessions/${sessionId}`, "DELETE");
|
|
2898
|
-
}
|
|
2960
|
+
startSession: async (gameId) => this.request(`/games/${gameId}/sessions`, "POST"),
|
|
2961
|
+
endSession: async (sessionId, gameId) => this.request(`/games/${gameId}/sessions/${sessionId}`, "DELETE")
|
|
2899
2962
|
};
|
|
2900
2963
|
constructor(config) {
|
|
2901
2964
|
this.baseUrl = config?.baseUrl?.endsWith("/api") ? config.baseUrl : `${config?.baseUrl}/api`;
|
|
@@ -2909,16 +2972,16 @@ class PlaycademyBaseClient {
|
|
|
2909
2972
|
}
|
|
2910
2973
|
getBaseUrl() {
|
|
2911
2974
|
const isRelative = this.baseUrl.startsWith("/");
|
|
2912
|
-
const isBrowser2 = typeof window !== "undefined";
|
|
2913
|
-
return isRelative && isBrowser2 ? `${
|
|
2975
|
+
const isBrowser2 = typeof globalThis.window !== "undefined";
|
|
2976
|
+
return isRelative && isBrowser2 ? `${globalThis.location.origin}${this.baseUrl}` : this.baseUrl;
|
|
2914
2977
|
}
|
|
2915
2978
|
getGameBackendUrl() {
|
|
2916
2979
|
if (!this.gameUrl) {
|
|
2917
2980
|
throw new PlaycademyError("Game backend URL not configured. gameUrl must be set to use game backend features.");
|
|
2918
2981
|
}
|
|
2919
2982
|
const isRelative = this.gameUrl.startsWith("/");
|
|
2920
|
-
const isBrowser2 = typeof window !== "undefined";
|
|
2921
|
-
const effectiveGameUrl = isRelative && isBrowser2 ? `${
|
|
2983
|
+
const isBrowser2 = typeof globalThis.window !== "undefined";
|
|
2984
|
+
const effectiveGameUrl = isRelative && isBrowser2 ? `${globalThis.location.origin}${this.gameUrl}` : this.gameUrl;
|
|
2922
2985
|
return `${effectiveGameUrl}/api`;
|
|
2923
2986
|
}
|
|
2924
2987
|
ping() {
|
|
@@ -2950,8 +3013,9 @@ class PlaycademyBaseClient {
|
|
|
2950
3013
|
return this.connectionManager?.getState() ?? "unknown";
|
|
2951
3014
|
}
|
|
2952
3015
|
async checkConnection() {
|
|
2953
|
-
if (!this.connectionManager)
|
|
3016
|
+
if (!this.connectionManager) {
|
|
2954
3017
|
return "unknown";
|
|
3018
|
+
}
|
|
2955
3019
|
return await this.connectionManager.checkNow();
|
|
2956
3020
|
}
|
|
2957
3021
|
_setAuthContext(context) {
|
|
@@ -3018,11 +3082,13 @@ class PlaycademyBaseClient {
|
|
|
3018
3082
|
this.authContext = { isInIframe: isInIframe() };
|
|
3019
3083
|
}
|
|
3020
3084
|
_initializeConnectionMonitor() {
|
|
3021
|
-
if (typeof window === "undefined")
|
|
3085
|
+
if (typeof globalThis.window === "undefined") {
|
|
3022
3086
|
return;
|
|
3087
|
+
}
|
|
3023
3088
|
const isEnabled = this.config.enableConnectionMonitoring ?? true;
|
|
3024
|
-
if (!isEnabled)
|
|
3089
|
+
if (!isEnabled) {
|
|
3025
3090
|
return;
|
|
3091
|
+
}
|
|
3026
3092
|
try {
|
|
3027
3093
|
this.connectionManager = new ConnectionManager({
|
|
3028
3094
|
baseUrl: this.baseUrl,
|
|
@@ -3037,11 +3103,13 @@ class PlaycademyBaseClient {
|
|
|
3037
3103
|
}
|
|
3038
3104
|
}
|
|
3039
3105
|
async _initializeInternalSession() {
|
|
3040
|
-
if (!this.gameId || this.internalClientSessionId)
|
|
3106
|
+
if (!this.gameId || this.internalClientSessionId) {
|
|
3041
3107
|
return;
|
|
3108
|
+
}
|
|
3042
3109
|
const shouldAutoStart = this.config.autoStartSession ?? true;
|
|
3043
|
-
if (!shouldAutoStart)
|
|
3110
|
+
if (!shouldAutoStart) {
|
|
3044
3111
|
return;
|
|
3112
|
+
}
|
|
3045
3113
|
try {
|
|
3046
3114
|
const response = await this._sessionManager.startSession(this.gameId);
|
|
3047
3115
|
this.internalClientSessionId = response.sessionId;
|