@playcademy/sdk 0.1.13 → 0.1.15

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.js CHANGED
@@ -123,9 +123,13 @@ var isBrowser = () => {
123
123
  return false;
124
124
  const minLevel = getMinimumLogLevel();
125
125
  return levelPriority[level] >= levelPriority[minLevel];
126
- }, performLog = (level, message, context) => {
126
+ }, customHandler, performLog = (level, message, context) => {
127
127
  if (!shouldLog(level))
128
128
  return;
129
+ if (customHandler) {
130
+ customHandler(level, message, context);
131
+ return;
132
+ }
129
133
  const outputFormat = detectOutputFormat();
130
134
  switch (outputFormat) {
131
135
  case "browser":
@@ -277,7 +281,356 @@ function isInIframe() {
277
281
  }
278
282
  }
279
283
 
284
+ // src/core/connection/monitor.ts
285
+ class ConnectionMonitor {
286
+ state = "online";
287
+ callbacks = new Set;
288
+ heartbeatInterval;
289
+ consecutiveFailures = 0;
290
+ isMonitoring = false;
291
+ config;
292
+ constructor(config) {
293
+ this.config = {
294
+ baseUrl: config.baseUrl,
295
+ heartbeatInterval: config.heartbeatInterval ?? 1e4,
296
+ heartbeatTimeout: config.heartbeatTimeout ?? 5000,
297
+ failureThreshold: config.failureThreshold ?? 2,
298
+ enableHeartbeat: config.enableHeartbeat ?? true,
299
+ enableOfflineEvents: config.enableOfflineEvents ?? true
300
+ };
301
+ this._detectInitialState();
302
+ }
303
+ start() {
304
+ if (this.isMonitoring)
305
+ return;
306
+ this.isMonitoring = true;
307
+ if (this.config.enableOfflineEvents && typeof window !== "undefined") {
308
+ window.addEventListener("online", this._handleOnline);
309
+ window.addEventListener("offline", this._handleOffline);
310
+ }
311
+ if (this.config.enableHeartbeat) {
312
+ this._startHeartbeat();
313
+ }
314
+ }
315
+ stop() {
316
+ if (!this.isMonitoring)
317
+ return;
318
+ this.isMonitoring = false;
319
+ if (typeof window !== "undefined") {
320
+ window.removeEventListener("online", this._handleOnline);
321
+ window.removeEventListener("offline", this._handleOffline);
322
+ }
323
+ if (this.heartbeatInterval) {
324
+ clearInterval(this.heartbeatInterval);
325
+ this.heartbeatInterval = undefined;
326
+ }
327
+ }
328
+ onChange(callback) {
329
+ this.callbacks.add(callback);
330
+ return () => this.callbacks.delete(callback);
331
+ }
332
+ getState() {
333
+ return this.state;
334
+ }
335
+ async checkNow() {
336
+ await this._performHeartbeat();
337
+ return this.state;
338
+ }
339
+ reportRequestFailure(error) {
340
+ const isNetworkError = error instanceof TypeError || error instanceof Error && error.message.includes("fetch");
341
+ if (!isNetworkError)
342
+ return;
343
+ this.consecutiveFailures++;
344
+ if (this.consecutiveFailures >= this.config.failureThreshold) {
345
+ this._setState("degraded", "Multiple consecutive request failures");
346
+ }
347
+ }
348
+ reportRequestSuccess() {
349
+ if (this.consecutiveFailures > 0) {
350
+ this.consecutiveFailures = 0;
351
+ if (this.state === "degraded") {
352
+ this._setState("online", "Requests succeeding again");
353
+ }
354
+ }
355
+ }
356
+ _detectInitialState() {
357
+ if (typeof navigator !== "undefined" && !navigator.onLine) {
358
+ this.state = "offline";
359
+ }
360
+ }
361
+ _handleOnline = () => {
362
+ this.consecutiveFailures = 0;
363
+ this._setState("online", "Browser online event");
364
+ };
365
+ _handleOffline = () => {
366
+ this._setState("offline", "Browser offline event");
367
+ };
368
+ _startHeartbeat() {
369
+ this._performHeartbeat();
370
+ this.heartbeatInterval = setInterval(() => {
371
+ this._performHeartbeat();
372
+ }, this.config.heartbeatInterval);
373
+ }
374
+ async _performHeartbeat() {
375
+ if (typeof navigator !== "undefined" && !navigator.onLine) {
376
+ return;
377
+ }
378
+ try {
379
+ const controller = new AbortController;
380
+ const timeoutId = setTimeout(() => controller.abort(), this.config.heartbeatTimeout);
381
+ const response = await fetch(`${this.config.baseUrl}/ping`, {
382
+ method: "GET",
383
+ signal: controller.signal,
384
+ cache: "no-store"
385
+ });
386
+ clearTimeout(timeoutId);
387
+ if (response.ok) {
388
+ this.consecutiveFailures = 0;
389
+ if (this.state !== "online") {
390
+ this._setState("online", "Heartbeat successful");
391
+ }
392
+ } else {
393
+ this._handleHeartbeatFailure("Heartbeat returned non-OK status");
394
+ }
395
+ } catch (error) {
396
+ this._handleHeartbeatFailure(error instanceof Error ? error.message : "Heartbeat failed");
397
+ }
398
+ }
399
+ _handleHeartbeatFailure(reason) {
400
+ this.consecutiveFailures++;
401
+ if (this.consecutiveFailures >= this.config.failureThreshold) {
402
+ if (typeof navigator !== "undefined" && !navigator.onLine) {
403
+ this._setState("offline", reason);
404
+ } else {
405
+ this._setState("degraded", reason);
406
+ }
407
+ }
408
+ }
409
+ _setState(newState, reason) {
410
+ if (this.state === newState)
411
+ return;
412
+ const oldState = this.state;
413
+ this.state = newState;
414
+ console.debug(`[ConnectionMonitor] ${oldState} → ${newState}: ${reason}`);
415
+ this.callbacks.forEach((callback) => {
416
+ try {
417
+ callback(newState, reason);
418
+ } catch (error) {
419
+ console.error("[ConnectionMonitor] Error in callback:", error);
420
+ }
421
+ });
422
+ }
423
+ }
424
+
425
+ // src/messaging.ts
426
+ class PlaycademyMessaging {
427
+ listeners = new Map;
428
+ send(type, payload, options) {
429
+ if (options?.target) {
430
+ this.sendViaPostMessage(type, payload, options.target, options.origin || "*");
431
+ return;
432
+ }
433
+ const context = this.getMessagingContext(type);
434
+ if (context.shouldUsePostMessage) {
435
+ this.sendViaPostMessage(type, payload, context.target, context.origin);
436
+ } else {
437
+ this.sendViaCustomEvent(type, payload);
438
+ }
439
+ }
440
+ listen(type, handler) {
441
+ const postMessageListener = (event) => {
442
+ const messageEvent = event;
443
+ if (messageEvent.data?.type === type) {
444
+ handler(messageEvent.data.payload || messageEvent.data);
445
+ }
446
+ };
447
+ const customEventListener = (event) => {
448
+ handler(event.detail);
449
+ };
450
+ if (!this.listeners.has(type)) {
451
+ this.listeners.set(type, new Map);
452
+ }
453
+ const listenerMap = this.listeners.get(type);
454
+ listenerMap.set(handler, {
455
+ postMessage: postMessageListener,
456
+ customEvent: customEventListener
457
+ });
458
+ window.addEventListener("message", postMessageListener);
459
+ window.addEventListener(type, customEventListener);
460
+ }
461
+ unlisten(type, handler) {
462
+ const typeListeners = this.listeners.get(type);
463
+ if (!typeListeners || !typeListeners.has(handler)) {
464
+ return;
465
+ }
466
+ const listeners = typeListeners.get(handler);
467
+ window.removeEventListener("message", listeners.postMessage);
468
+ window.removeEventListener(type, listeners.customEvent);
469
+ typeListeners.delete(handler);
470
+ if (typeListeners.size === 0) {
471
+ this.listeners.delete(type);
472
+ }
473
+ }
474
+ getMessagingContext(eventType) {
475
+ const isIframe = typeof window !== "undefined" && window.self !== window.top;
476
+ const iframeToParentEvents = [
477
+ "PLAYCADEMY_READY" /* READY */,
478
+ "PLAYCADEMY_EXIT" /* EXIT */,
479
+ "PLAYCADEMY_TELEMETRY" /* TELEMETRY */,
480
+ "PLAYCADEMY_KEY_EVENT" /* KEY_EVENT */,
481
+ "PLAYCADEMY_DISPLAY_ALERT" /* DISPLAY_ALERT */
482
+ ];
483
+ const shouldUsePostMessage = isIframe && iframeToParentEvents.includes(eventType);
484
+ return {
485
+ shouldUsePostMessage,
486
+ target: shouldUsePostMessage ? window.parent : undefined,
487
+ origin: "*"
488
+ };
489
+ }
490
+ sendViaPostMessage(type, payload, target = window.parent, origin = "*") {
491
+ const messageData = { type };
492
+ if (payload !== undefined) {
493
+ messageData.payload = payload;
494
+ }
495
+ target.postMessage(messageData, origin);
496
+ }
497
+ sendViaCustomEvent(type, payload) {
498
+ window.dispatchEvent(new CustomEvent(type, { detail: payload }));
499
+ }
500
+ }
501
+ var MessageEvents, messaging;
502
+ var init_messaging = __esm(() => {
503
+ ((MessageEvents2) => {
504
+ MessageEvents2["INIT"] = "PLAYCADEMY_INIT";
505
+ MessageEvents2["TOKEN_REFRESH"] = "PLAYCADEMY_TOKEN_REFRESH";
506
+ MessageEvents2["PAUSE"] = "PLAYCADEMY_PAUSE";
507
+ MessageEvents2["RESUME"] = "PLAYCADEMY_RESUME";
508
+ MessageEvents2["FORCE_EXIT"] = "PLAYCADEMY_FORCE_EXIT";
509
+ MessageEvents2["OVERLAY"] = "PLAYCADEMY_OVERLAY";
510
+ MessageEvents2["CONNECTION_STATE"] = "PLAYCADEMY_CONNECTION_STATE";
511
+ MessageEvents2["READY"] = "PLAYCADEMY_READY";
512
+ MessageEvents2["EXIT"] = "PLAYCADEMY_EXIT";
513
+ MessageEvents2["TELEMETRY"] = "PLAYCADEMY_TELEMETRY";
514
+ MessageEvents2["KEY_EVENT"] = "PLAYCADEMY_KEY_EVENT";
515
+ MessageEvents2["DISPLAY_ALERT"] = "PLAYCADEMY_DISPLAY_ALERT";
516
+ MessageEvents2["AUTH_STATE_CHANGE"] = "PLAYCADEMY_AUTH_STATE_CHANGE";
517
+ MessageEvents2["AUTH_CALLBACK"] = "PLAYCADEMY_AUTH_CALLBACK";
518
+ })(MessageEvents ||= {});
519
+ messaging = new PlaycademyMessaging;
520
+ });
521
+
522
+ // src/core/connection/utils.ts
523
+ function createDisplayAlert(authContext) {
524
+ return (message, options) => {
525
+ if (authContext?.isInIframe && typeof window !== "undefined" && window.parent !== window) {
526
+ window.parent.postMessage({
527
+ type: "PLAYCADEMY_DISPLAY_ALERT",
528
+ message,
529
+ options
530
+ }, "*");
531
+ } else {
532
+ const prefix = options?.type === "error" ? "❌" : options?.type === "warning" ? "⚠️" : "ℹ️";
533
+ console.log(`${prefix} ${message}`);
534
+ }
535
+ };
536
+ }
537
+
538
+ // src/core/connection/manager.ts
539
+ class ConnectionManager {
540
+ monitor;
541
+ authContext;
542
+ disconnectHandler;
543
+ connectionChangeCallback;
544
+ currentState = "online";
545
+ additionalDisconnectHandlers = new Set;
546
+ constructor(config) {
547
+ this.authContext = config.authContext;
548
+ this.disconnectHandler = config.onDisconnect;
549
+ this.connectionChangeCallback = config.onConnectionChange;
550
+ if (config.authContext?.isInIframe) {
551
+ this._setupPlatformListener();
552
+ }
553
+ }
554
+ getState() {
555
+ return this.monitor?.getState() ?? this.currentState;
556
+ }
557
+ async checkNow() {
558
+ if (!this.monitor) {
559
+ return this.currentState;
560
+ }
561
+ return await this.monitor.checkNow();
562
+ }
563
+ reportRequestSuccess() {
564
+ this.monitor?.reportRequestSuccess();
565
+ }
566
+ reportRequestFailure(error) {
567
+ this.monitor?.reportRequestFailure(error);
568
+ }
569
+ onDisconnect(callback) {
570
+ this.additionalDisconnectHandlers.add(callback);
571
+ return () => {
572
+ this.additionalDisconnectHandlers.delete(callback);
573
+ };
574
+ }
575
+ stop() {
576
+ this.monitor?.stop();
577
+ }
578
+ _setupPlatformListener() {
579
+ messaging.listen("PLAYCADEMY_CONNECTION_STATE" /* CONNECTION_STATE */, ({ state, reason }) => {
580
+ this.currentState = state;
581
+ this._handleConnectionChange(state, reason);
582
+ });
583
+ }
584
+ _handleConnectionChange(state, reason) {
585
+ this.connectionChangeCallback?.(state, reason);
586
+ if (state === "offline" || state === "degraded") {
587
+ const context = {
588
+ state,
589
+ reason,
590
+ timestamp: Date.now(),
591
+ displayAlert: createDisplayAlert(this.authContext)
592
+ };
593
+ if (this.disconnectHandler) {
594
+ this.disconnectHandler(context);
595
+ }
596
+ this.additionalDisconnectHandlers.forEach((handler) => {
597
+ handler(context);
598
+ });
599
+ }
600
+ }
601
+ }
602
+ var init_manager = __esm(() => {
603
+ init_messaging();
604
+ });
605
+
606
+ // src/core/connection/index.ts
607
+ var init_connection = __esm(() => {
608
+ init_manager();
609
+ });
610
+
280
611
  // src/core/errors.ts
612
+ function extractApiErrorInfo(error) {
613
+ if (!(error instanceof ApiError)) {
614
+ return null;
615
+ }
616
+ const info = {
617
+ status: error.status,
618
+ statusText: error.message
619
+ };
620
+ if (error.details && typeof error.details === "object") {
621
+ const details = error.details;
622
+ if ("error" in details && typeof details.error === "string") {
623
+ info.error = details.error;
624
+ }
625
+ if ("message" in details && typeof details.message === "string") {
626
+ info.message = details.message;
627
+ }
628
+ if (!info.error && !info.message) {
629
+ info.details = error.details;
630
+ }
631
+ }
632
+ return info;
633
+ }
281
634
  var PlaycademyError, ApiError;
282
635
  var init_errors = __esm(() => {
283
636
  PlaycademyError = class PlaycademyError extends Error {
@@ -638,100 +991,6 @@ var init_identity = __esm(() => {
638
991
  init_login();
639
992
  });
640
993
 
641
- // src/messaging.ts
642
- class PlaycademyMessaging {
643
- listeners = new Map;
644
- send(type, payload, options) {
645
- if (options?.target) {
646
- this.sendViaPostMessage(type, payload, options.target, options.origin || "*");
647
- return;
648
- }
649
- const context = this.getMessagingContext(type);
650
- if (context.shouldUsePostMessage) {
651
- this.sendViaPostMessage(type, payload, context.target, context.origin);
652
- } else {
653
- this.sendViaCustomEvent(type, payload);
654
- }
655
- }
656
- listen(type, handler) {
657
- const postMessageListener = (event) => {
658
- const messageEvent = event;
659
- if (messageEvent.data?.type === type) {
660
- handler(messageEvent.data.payload || messageEvent.data);
661
- }
662
- };
663
- const customEventListener = (event) => {
664
- handler(event.detail);
665
- };
666
- if (!this.listeners.has(type)) {
667
- this.listeners.set(type, new Map);
668
- }
669
- const listenerMap = this.listeners.get(type);
670
- listenerMap.set(handler, {
671
- postMessage: postMessageListener,
672
- customEvent: customEventListener
673
- });
674
- window.addEventListener("message", postMessageListener);
675
- window.addEventListener(type, customEventListener);
676
- }
677
- unlisten(type, handler) {
678
- const typeListeners = this.listeners.get(type);
679
- if (!typeListeners || !typeListeners.has(handler)) {
680
- return;
681
- }
682
- const listeners = typeListeners.get(handler);
683
- window.removeEventListener("message", listeners.postMessage);
684
- window.removeEventListener(type, listeners.customEvent);
685
- typeListeners.delete(handler);
686
- if (typeListeners.size === 0) {
687
- this.listeners.delete(type);
688
- }
689
- }
690
- getMessagingContext(eventType) {
691
- const isIframe = typeof window !== "undefined" && window.self !== window.top;
692
- const iframeToParentEvents = [
693
- "PLAYCADEMY_READY" /* READY */,
694
- "PLAYCADEMY_EXIT" /* EXIT */,
695
- "PLAYCADEMY_TELEMETRY" /* TELEMETRY */,
696
- "PLAYCADEMY_KEY_EVENT" /* KEY_EVENT */
697
- ];
698
- const shouldUsePostMessage = isIframe && iframeToParentEvents.includes(eventType);
699
- return {
700
- shouldUsePostMessage,
701
- target: shouldUsePostMessage ? window.parent : undefined,
702
- origin: "*"
703
- };
704
- }
705
- sendViaPostMessage(type, payload, target = window.parent, origin = "*") {
706
- const messageData = { type };
707
- if (payload !== undefined) {
708
- messageData.payload = payload;
709
- }
710
- target.postMessage(messageData, origin);
711
- }
712
- sendViaCustomEvent(type, payload) {
713
- window.dispatchEvent(new CustomEvent(type, { detail: payload }));
714
- }
715
- }
716
- var MessageEvents, messaging;
717
- var init_messaging = __esm(() => {
718
- ((MessageEvents2) => {
719
- MessageEvents2["INIT"] = "PLAYCADEMY_INIT";
720
- MessageEvents2["TOKEN_REFRESH"] = "PLAYCADEMY_TOKEN_REFRESH";
721
- MessageEvents2["PAUSE"] = "PLAYCADEMY_PAUSE";
722
- MessageEvents2["RESUME"] = "PLAYCADEMY_RESUME";
723
- MessageEvents2["FORCE_EXIT"] = "PLAYCADEMY_FORCE_EXIT";
724
- MessageEvents2["OVERLAY"] = "PLAYCADEMY_OVERLAY";
725
- MessageEvents2["READY"] = "PLAYCADEMY_READY";
726
- MessageEvents2["EXIT"] = "PLAYCADEMY_EXIT";
727
- MessageEvents2["TELEMETRY"] = "PLAYCADEMY_TELEMETRY";
728
- MessageEvents2["KEY_EVENT"] = "PLAYCADEMY_KEY_EVENT";
729
- MessageEvents2["AUTH_STATE_CHANGE"] = "PLAYCADEMY_AUTH_STATE_CHANGE";
730
- MessageEvents2["AUTH_CALLBACK"] = "PLAYCADEMY_AUTH_CALLBACK";
731
- })(MessageEvents ||= {});
732
- messaging = new PlaycademyMessaging;
733
- });
734
-
735
994
  // src/core/namespaces/runtime.ts
736
995
  function createRuntimeNamespace(client) {
737
996
  const eventListeners = new Map;
@@ -755,7 +1014,7 @@ function createRuntimeNamespace(client) {
755
1014
  const forwardKeys = Array.isArray(playcademyConfig?.forwardKeys) ? playcademyConfig.forwardKeys : ["Escape"];
756
1015
  const keySet = new Set(forwardKeys.map((k) => k.toLowerCase()));
757
1016
  const keyListener = (event) => {
758
- if (keySet.has(event.key.toLowerCase()) || keySet.has(event.code.toLowerCase())) {
1017
+ if (keySet.has(event.key?.toLowerCase() ?? "") || keySet.has(event.code?.toLowerCase() ?? "")) {
759
1018
  messaging.send("PLAYCADEMY_KEY_EVENT" /* KEY_EVENT */, {
760
1019
  key: event.key,
761
1020
  code: event.code,
@@ -842,7 +1101,7 @@ function createRuntimeNamespace(client) {
842
1101
  },
843
1102
  cdn: {
844
1103
  url(pathOrStrings, ...values) {
845
- const cdnBase = client["initPayload"]?.cdnBase;
1104
+ const gameUrl = client["initPayload"]?.gameUrl;
846
1105
  let path;
847
1106
  if (Array.isArray(pathOrStrings) && "raw" in pathOrStrings) {
848
1107
  const strings = pathOrStrings;
@@ -852,20 +1111,20 @@ function createRuntimeNamespace(client) {
852
1111
  } else {
853
1112
  path = pathOrStrings;
854
1113
  }
855
- if (!cdnBase) {
1114
+ if (!gameUrl) {
856
1115
  return path.startsWith("./") ? path : "./" + path;
857
1116
  }
858
1117
  const cleanPath = path.startsWith("./") ? path.slice(2) : path;
859
- return cdnBase + cleanPath;
1118
+ return gameUrl + cleanPath;
860
1119
  },
861
1120
  fetch: async (path, options) => {
862
- const cdnBase = client["initPayload"]?.cdnBase;
863
- if (!cdnBase) {
1121
+ const gameUrl = client["initPayload"]?.gameUrl;
1122
+ if (!gameUrl) {
864
1123
  const relativePath = path.startsWith("./") ? path : "./" + path;
865
1124
  return fetch(relativePath, options);
866
1125
  }
867
1126
  const cleanPath = path.startsWith("./") ? path.slice(2) : path;
868
- return fetch(cdnBase + cleanPath, options);
1127
+ return fetch(gameUrl + cleanPath, options);
869
1128
  },
870
1129
  json: async (path) => {
871
1130
  const response = await client.runtime.cdn.fetch(path);
@@ -1047,8 +1306,8 @@ async function request({
1047
1306
  const rawText = await res.text().catch(() => "");
1048
1307
  return rawText && rawText.length > 0 ? rawText : undefined;
1049
1308
  }
1050
- async function fetchManifest(assetBundleBase) {
1051
- const manifestUrl = `${assetBundleBase.replace(/\/$/, "")}/playcademy.manifest.json`;
1309
+ async function fetchManifest(deploymentUrl) {
1310
+ const manifestUrl = `${deploymentUrl.replace(/\/$/, "")}/playcademy.manifest.json`;
1052
1311
  try {
1053
1312
  const response = await fetch(manifestUrl);
1054
1313
  if (!response.ok) {
@@ -1086,8 +1345,8 @@ function createGamesNamespace(client) {
1086
1345
  const promise = client["request"](`/games/${gameIdOrSlug}`, "GET");
1087
1346
  return gameFetchCache.get(gameIdOrSlug, async () => {
1088
1347
  const baseGameData = await promise;
1089
- if (baseGameData.gameType === "hosted" && baseGameData.assetBundleBase !== null && baseGameData.assetBundleBase !== "") {
1090
- const manifestData = await fetchManifest(baseGameData.assetBundleBase);
1348
+ if (baseGameData.gameType === "hosted" && baseGameData.deploymentUrl !== null && baseGameData.deploymentUrl !== "") {
1349
+ const manifestData = await fetchManifest(baseGameData.deploymentUrl);
1091
1350
  return { ...baseGameData, manifest: manifestData };
1092
1351
  }
1093
1352
  return baseGameData;
@@ -1276,22 +1535,28 @@ function createDevNamespace(client) {
1276
1535
  }
1277
1536
  },
1278
1537
  games: {
1279
- deploy: {
1280
- frontend: async (slug, metadata, file, hooks) => {
1281
- hooks?.onEvent?.({ type: "init" });
1282
- const game = await client["request"](`/games/${slug}`, "PUT", {
1538
+ deploy: async (slug, options) => {
1539
+ const { metadata, file, backend, hooks } = options;
1540
+ hooks?.onEvent?.({ type: "init" });
1541
+ let game;
1542
+ if (metadata) {
1543
+ game = await client["request"](`/games/${slug}`, "PUT", {
1283
1544
  body: metadata
1284
1545
  });
1285
- if (metadata.gameType === "external" || file === null) {
1546
+ if (metadata.gameType === "external" && !file && !backend) {
1286
1547
  return game;
1287
1548
  }
1549
+ }
1550
+ let uploadToken;
1551
+ if (file) {
1288
1552
  const fileName = file instanceof File ? file.name : "game.zip";
1289
1553
  const initiateResponse = await client["request"]("/games/uploads/initiate/", "POST", {
1290
1554
  body: {
1291
1555
  fileName,
1292
- gameId: game.id
1556
+ gameId: game?.id || slug
1293
1557
  }
1294
1558
  });
1559
+ uploadToken = initiateResponse.uploadToken;
1295
1560
  if (hooks?.onEvent && typeof XMLHttpRequest !== "undefined") {
1296
1561
  await new Promise((resolve, reject) => {
1297
1562
  const xhr = new XMLHttpRequest;
@@ -1332,7 +1597,9 @@ function createDevNamespace(client) {
1332
1597
  throw new Error(`File upload failed: ${uploadResponse.status} ${uploadResponse.statusText}`);
1333
1598
  }
1334
1599
  }
1335
- const finalizeUrl = `${client.baseUrl}/games/uploads/finalize/`;
1600
+ }
1601
+ if (uploadToken || backend) {
1602
+ const deployUrl = `${client.baseUrl}/games/${slug}/deploy`;
1336
1603
  const authToken = client.getToken();
1337
1604
  const tokenType = client.getTokenType();
1338
1605
  const headers = {
@@ -1345,25 +1612,33 @@ function createDevNamespace(client) {
1345
1612
  headers["Authorization"] = `Bearer ${authToken}`;
1346
1613
  }
1347
1614
  }
1348
- const finalizeResponse = await fetch(finalizeUrl, {
1615
+ const requestBody = {};
1616
+ if (uploadToken)
1617
+ requestBody.uploadToken = uploadToken;
1618
+ if (metadata)
1619
+ requestBody.metadata = metadata;
1620
+ if (backend) {
1621
+ requestBody.code = backend.code;
1622
+ requestBody.config = backend.config;
1623
+ if (backend.bindings)
1624
+ requestBody.bindings = backend.bindings;
1625
+ if (backend.schema)
1626
+ requestBody.schema = backend.schema;
1627
+ if (backend.secrets)
1628
+ requestBody.secrets = backend.secrets;
1629
+ }
1630
+ const finalizeResponse = await fetch(deployUrl, {
1349
1631
  method: "POST",
1350
1632
  headers,
1351
- body: JSON.stringify({
1352
- tempS3Key: initiateResponse.tempS3Key,
1353
- gameId: initiateResponse.gameId,
1354
- version: initiateResponse.version,
1355
- slug,
1356
- metadata,
1357
- originalFileName: fileName
1358
- }),
1633
+ body: JSON.stringify(requestBody),
1359
1634
  credentials: "omit"
1360
1635
  });
1361
1636
  if (!finalizeResponse.ok) {
1362
1637
  const errText = await finalizeResponse.text().catch(() => "");
1363
- throw new Error(`Finalize request failed: ${finalizeResponse.status} ${finalizeResponse.statusText}${errText ? ` - ${errText}` : ""}`);
1638
+ throw new Error(`Deploy request failed: ${finalizeResponse.status} ${finalizeResponse.statusText}${errText ? ` - ${errText}` : ""}`);
1364
1639
  }
1365
1640
  if (!finalizeResponse.body) {
1366
- throw new Error("Finalize response body missing");
1641
+ throw new Error("Deploy response body missing");
1367
1642
  }
1368
1643
  hooks?.onEvent?.({ type: "finalizeStart" });
1369
1644
  let sawAnyServerEvent = false;
@@ -1418,20 +1693,19 @@ function createDevNamespace(client) {
1418
1693
  }
1419
1694
  }
1420
1695
  }
1421
- throw new Error("Upload completed but no final game data received");
1422
- },
1423
- backend: async (slug, bundle) => {
1424
- return client["request"](`/games/${slug}/backend/deploy`, "POST", { body: bundle });
1425
- },
1426
- seed: async (slug, code, environment) => {
1427
- return client["request"](`/games/${slug}/seed`, "POST", {
1428
- body: { code, environment }
1429
- });
1696
+ throw new Error("Deployment completed but no final game data received");
1697
+ }
1698
+ if (game) {
1699
+ return game;
1430
1700
  }
1701
+ throw new Error("No deployment actions specified (need metadata, file, or backend)");
1431
1702
  },
1432
- upsert: async (slug, metadata) => {
1433
- return client.dev.games.deploy.frontend(slug, metadata, null);
1703
+ seed: async (slug, code, environment) => {
1704
+ return client["request"](`/games/${slug}/seed`, "POST", {
1705
+ body: { code, environment }
1706
+ });
1434
1707
  },
1708
+ upsert: async (slug, metadata) => client["request"](`/games/${slug}`, "PUT", { body: metadata }),
1435
1709
  delete: (gameId) => client["request"](`/games/${gameId}`, "DELETE"),
1436
1710
  secrets: {
1437
1711
  set: async (slug, secrets) => {
@@ -1488,6 +1762,24 @@ function createDevNamespace(client) {
1488
1762
  delete: async (slug, key) => {
1489
1763
  await client["request"](`/games/${slug}/bucket/${encodeURIComponent(key)}`, "DELETE");
1490
1764
  }
1765
+ },
1766
+ domains: {
1767
+ add: async (slug, hostname) => {
1768
+ return client["request"](`/games/${slug}/domains`, "POST", {
1769
+ body: { hostname }
1770
+ });
1771
+ },
1772
+ list: async (slug) => {
1773
+ const result = await client["request"](`/games/${slug}/domains`, "GET");
1774
+ return result.domains;
1775
+ },
1776
+ status: async (slug, hostname, refresh) => {
1777
+ const params = refresh ? "?refresh=true" : "";
1778
+ return client["request"](`/games/${slug}/domains/${hostname}${params}`, "GET");
1779
+ },
1780
+ delete: async (slug, hostname) => {
1781
+ await client["request"](`/games/${slug}/domains/${hostname}`, "DELETE");
1782
+ }
1491
1783
  }
1492
1784
  },
1493
1785
  items: {
@@ -2534,10 +2826,10 @@ async function waitForPlaycademyInit(allowedParentOrigins) {
2534
2826
  });
2535
2827
  }
2536
2828
  function createStandaloneConfig() {
2537
- console.warn("[Playcademy SDK] Standalone mode detected, creating mock context for local development");
2829
+ console.debug("[Playcademy SDK] Standalone mode detected, creating mock context for sandbox development");
2538
2830
  const mockConfig = {
2539
2831
  baseUrl: "http://localhost:4321",
2540
- gameUrl: "http://localhost:8788",
2832
+ gameUrl: window.location.origin,
2541
2833
  token: "mock-game-token-for-local-dev",
2542
2834
  gameId: "mock-game-id-from-template",
2543
2835
  realtimeUrl: undefined
@@ -2559,7 +2851,9 @@ async function init(options) {
2559
2851
  gameUrl: config.gameUrl,
2560
2852
  token: config.token,
2561
2853
  gameId: config.gameId,
2562
- autoStartSession: window.self !== window.top
2854
+ autoStartSession: window.self !== window.top,
2855
+ onDisconnect: options?.onDisconnect,
2856
+ enableConnectionMonitoring: options?.enableConnectionMonitoring
2563
2857
  });
2564
2858
  client["initPayload"] = config;
2565
2859
  messaging.listen("PLAYCADEMY_TOKEN_REFRESH" /* TOKEN_REFRESH */, ({ token }) => client.setToken(token));
@@ -2628,6 +2922,7 @@ __export(exports_client, {
2628
2922
  var PlaycademyClient;
2629
2923
  var init_client = __esm(() => {
2630
2924
  init_src();
2925
+ init_connection();
2631
2926
  init_errors();
2632
2927
  init_namespaces();
2633
2928
  init_request();
@@ -2642,6 +2937,7 @@ var init_client = __esm(() => {
2642
2937
  internalClientSessionId;
2643
2938
  authContext;
2644
2939
  initPayload;
2940
+ connectionManager;
2645
2941
  constructor(config) {
2646
2942
  this.baseUrl = config?.baseUrl?.endsWith("/api") ? config.baseUrl : `${config?.baseUrl}/api`;
2647
2943
  this.gameUrl = config?.gameUrl;
@@ -2650,6 +2946,7 @@ var init_client = __esm(() => {
2650
2946
  this.authStrategy = createAuthStrategy(config?.token ?? null, config?.tokenType);
2651
2947
  this._detectAuthContext();
2652
2948
  this._initializeInternalSession().catch(() => {});
2949
+ this._initializeConnectionMonitor();
2653
2950
  }
2654
2951
  getBaseUrl() {
2655
2952
  const isRelative = this.baseUrl.startsWith("/");
@@ -2684,6 +2981,20 @@ var init_client = __esm(() => {
2684
2981
  onAuthChange(callback) {
2685
2982
  this.on("authChange", (payload) => callback(payload.token));
2686
2983
  }
2984
+ onDisconnect(callback) {
2985
+ if (!this.connectionManager) {
2986
+ return () => {};
2987
+ }
2988
+ return this.connectionManager.onDisconnect(callback);
2989
+ }
2990
+ getConnectionState() {
2991
+ return this.connectionManager?.getState() ?? "unknown";
2992
+ }
2993
+ async checkConnection() {
2994
+ if (!this.connectionManager)
2995
+ return "unknown";
2996
+ return await this.connectionManager.checkNow();
2997
+ }
2687
2998
  _setAuthContext(context) {
2688
2999
  this.authContext = context;
2689
3000
  }
@@ -2701,28 +3012,42 @@ var init_client = __esm(() => {
2701
3012
  ...options?.headers,
2702
3013
  ...this.authStrategy.getHeaders()
2703
3014
  };
2704
- return request({
2705
- path,
2706
- method,
2707
- body: options?.body,
2708
- baseUrl: this.baseUrl,
2709
- extraHeaders: effectiveHeaders,
2710
- raw: options?.raw
2711
- });
3015
+ try {
3016
+ const result = await request({
3017
+ path,
3018
+ method,
3019
+ body: options?.body,
3020
+ baseUrl: this.baseUrl,
3021
+ extraHeaders: effectiveHeaders,
3022
+ raw: options?.raw
3023
+ });
3024
+ this.connectionManager?.reportRequestSuccess();
3025
+ return result;
3026
+ } catch (error) {
3027
+ this.connectionManager?.reportRequestFailure(error);
3028
+ throw error;
3029
+ }
2712
3030
  }
2713
3031
  async requestGameBackend(path, method, body, headers, raw) {
2714
3032
  const effectiveHeaders = {
2715
3033
  ...headers,
2716
3034
  ...this.authStrategy.getHeaders()
2717
3035
  };
2718
- return request({
2719
- path,
2720
- method,
2721
- body,
2722
- baseUrl: this.getGameBackendUrl(),
2723
- extraHeaders: effectiveHeaders,
2724
- raw
2725
- });
3036
+ try {
3037
+ const result = await request({
3038
+ path,
3039
+ method,
3040
+ body,
3041
+ baseUrl: this.getGameBackendUrl(),
3042
+ extraHeaders: effectiveHeaders,
3043
+ raw
3044
+ });
3045
+ this.connectionManager?.reportRequestSuccess();
3046
+ return result;
3047
+ } catch (error) {
3048
+ this.connectionManager?.reportRequestFailure(error);
3049
+ throw error;
3050
+ }
2726
3051
  }
2727
3052
  _ensureGameId() {
2728
3053
  if (!this.gameId) {
@@ -2733,6 +3058,25 @@ var init_client = __esm(() => {
2733
3058
  _detectAuthContext() {
2734
3059
  this.authContext = { isInIframe: isInIframe() };
2735
3060
  }
3061
+ _initializeConnectionMonitor() {
3062
+ if (typeof window === "undefined")
3063
+ return;
3064
+ const isEnabled = this.config.enableConnectionMonitoring ?? true;
3065
+ if (!isEnabled)
3066
+ return;
3067
+ try {
3068
+ this.connectionManager = new ConnectionManager({
3069
+ baseUrl: this.baseUrl,
3070
+ authContext: this.authContext,
3071
+ onDisconnect: this.config.onDisconnect,
3072
+ onConnectionChange: (state, reason) => {
3073
+ this.emit("connectionChange", { state, reason });
3074
+ }
3075
+ });
3076
+ } catch (error) {
3077
+ log.error("[Playcademy SDK] Failed to initialize connection manager:", { error });
3078
+ }
3079
+ }
2736
3080
  async _initializeInternalSession() {
2737
3081
  if (!this.gameId || this.internalClientSessionId)
2738
3082
  return;
@@ -2782,9 +3126,16 @@ var init_client = __esm(() => {
2782
3126
 
2783
3127
  // src/index.ts
2784
3128
  init_client();
3129
+ init_errors();
3130
+ init_connection();
2785
3131
  init_messaging();
2786
3132
  export {
2787
3133
  messaging,
3134
+ extractApiErrorInfo,
3135
+ PlaycademyError,
2788
3136
  PlaycademyClient,
2789
- MessageEvents
3137
+ MessageEvents,
3138
+ ConnectionMonitor,
3139
+ ConnectionManager,
3140
+ ApiError
2790
3141
  };