@looplay/sdk 0.8.6 → 0.8.8

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.
@@ -139,6 +139,9 @@ var LooplayAuth = class {
139
139
  }
140
140
  };
141
141
 
142
+ // src/version.ts
143
+ var LOOPLAY_SDK_VERSION = "0.8.5";
144
+
142
145
  // src/apps/http.ts
143
146
  function getFetchFn(fetchOverride) {
144
147
  if (fetchOverride) {
@@ -166,7 +169,10 @@ var HttpClient = class {
166
169
  constructor(options) {
167
170
  this.baseUrl = options.baseUrl.replace(/\/$/, "");
168
171
  this.fetchFn = getFetchFn(options.fetch);
169
- this.defaultHeaders = options.defaultHeaders ?? {};
172
+ this.defaultHeaders = {
173
+ "x-looplay-sdk-version": LOOPLAY_SDK_VERSION,
174
+ ...options.defaultHeaders ?? {}
175
+ };
170
176
  }
171
177
  async request(method, path, options) {
172
178
  const url = this.buildUrl(path, options?.query);
@@ -269,6 +275,19 @@ var ServiceClient = class {
269
275
  body
270
276
  });
271
277
  }
278
+ /**
279
+ * Re-issues a fresh `playSessionToken` for the SAME `playSessionId` — call
280
+ * this instead of `startPlaySession` when refreshing an about-to-expire
281
+ * (or just-expired, within the server's renewal grace window) token, so
282
+ * long-running sessions keep a single `playSessionId` throughout.
283
+ */
284
+ async renewPlaySession(gameKey, auth = {}, body) {
285
+ return this.http.request("POST", "/sdk/games/play-sessions/renew", {
286
+ ...auth,
287
+ headers: { "x-game-key": gameKey, ...this.trackingHeaders(auth) },
288
+ body
289
+ });
290
+ }
272
291
  async completePlaySession(gameKey, auth, body) {
273
292
  await this.http.request("POST", "/sdk/games/play-sessions/complete", {
274
293
  ...auth,
@@ -438,9 +457,11 @@ var ApiClient = class {
438
457
  getAccessToken;
439
458
  getAnonymousId;
440
459
  trackingSession;
460
+ trackingSessionPromiseKey;
441
461
  trackingSessionPromise;
442
462
  currentPlaySessionId;
443
463
  playSession;
464
+ playSessionPromiseKey;
444
465
  playSessionPromise;
445
466
  constructor(options) {
446
467
  if (!options.baseUrl) throw new MissingBaseUrlError();
@@ -481,7 +502,10 @@ var ApiClient = class {
481
502
  const bearerToken = await this.getAccessToken?.();
482
503
  const anonId = bearerToken ? void 0 : this.getAnonymousId?.();
483
504
  if (!bearerToken && !anonId) throw new NotAuthenticatedError();
484
- const session = await this.ensureTrackingSession(anonId);
505
+ const session = await this.ensureTrackingSession(
506
+ bearerToken ? `bearer:${bearerToken}` : `anon:${anonId}`,
507
+ anonId
508
+ );
485
509
  return {
486
510
  bearerToken,
487
511
  anonId,
@@ -494,14 +518,16 @@ var ApiClient = class {
494
518
  * session. Swallows failures — tracking calls still work without it,
495
519
  * just without unified session tracing.
496
520
  */
497
- async ensureTrackingSession(anonId) {
521
+ async ensureTrackingSession(cacheKey, anonId) {
498
522
  const now = Date.now();
499
- if (this.trackingSession && this.trackingSession.expiresAtMs > now + 3e4) {
523
+ if (this.trackingSession && this.trackingSession.cacheKey === cacheKey && this.trackingSession.expiresAtMs > now + 3e4) {
500
524
  return this.trackingSession;
501
525
  }
502
- if (!this.trackingSessionPromise) {
526
+ if (!this.trackingSessionPromise || this.trackingSessionPromiseKey !== cacheKey) {
527
+ this.trackingSessionPromiseKey = cacheKey;
503
528
  this.trackingSessionPromise = this.raw.bootstrapTrackingSession(anonId).then((dto) => {
504
529
  this.trackingSession = {
530
+ cacheKey,
505
531
  trackingSessionId: dto.trackingSessionId,
506
532
  anonTrackingToken: dto.trackingToken,
507
533
  expiresAtMs: new Date(dto.expiresAt).getTime()
@@ -509,10 +535,18 @@ var ApiClient = class {
509
535
  return this.trackingSession;
510
536
  }).catch(() => void 0).finally(() => {
511
537
  this.trackingSessionPromise = void 0;
538
+ this.trackingSessionPromiseKey = void 0;
512
539
  });
513
540
  }
514
541
  return this.trackingSessionPromise;
515
542
  }
543
+ getTrackingAuthKey(auth) {
544
+ return [
545
+ auth.bearerToken ? `bearer:${auth.bearerToken}` : `anon:${auth.anonId ?? ""}`,
546
+ auth.trackingSessionId ?? "",
547
+ auth.anonTrackingToken ?? ""
548
+ ].join("\n");
549
+ }
516
550
  /**
517
551
  * Signs (and caches until near expiry) the `playSessionToken` required by
518
552
  * `trackPlay`/`trackMatch`/`emit` for the current play session — does not
@@ -521,12 +555,20 @@ var ApiClient = class {
521
555
  */
522
556
  async ensurePlaySession(gameKey, auth, playSessionId) {
523
557
  const now = Date.now();
524
- if (this.playSession && this.playSession.gameKey === gameKey && this.playSession.playSessionId === playSessionId && this.playSession.expiresAtMs > now + 1e4) {
558
+ const cacheKey = [gameKey, playSessionId, this.getTrackingAuthKey(auth)].join("\n");
559
+ if (this.playSession && this.playSession.cacheKey === cacheKey && this.playSession.gameKey === gameKey && this.playSession.playSessionId === playSessionId && this.playSession.expiresAtMs > now + 1e4) {
525
560
  return this.playSession;
526
561
  }
527
- if (!this.playSessionPromise) {
528
- this.playSessionPromise = this.raw.startPlaySession(gameKey, auth, { playSessionId }).then((dto) => {
562
+ const previous = this.playSession && this.playSession.cacheKey === cacheKey && this.playSession.gameKey === gameKey && this.playSession.playSessionId === playSessionId ? this.playSession : void 0;
563
+ if (!this.playSessionPromise || this.playSessionPromiseKey !== cacheKey) {
564
+ this.playSessionPromiseKey = cacheKey;
565
+ const issue = previous ? this.raw.renewPlaySession(gameKey, auth, {
566
+ playSessionId,
567
+ playSessionToken: previous.playSessionToken
568
+ }).catch(() => this.raw.startPlaySession(gameKey, auth, { playSessionId })) : this.raw.startPlaySession(gameKey, auth, { playSessionId });
569
+ this.playSessionPromise = issue.then((dto) => {
529
570
  this.playSession = {
571
+ cacheKey,
530
572
  gameKey,
531
573
  playSessionId: dto.playSessionId,
532
574
  playSessionToken: dto.playSessionToken,
@@ -535,6 +577,7 @@ var ApiClient = class {
535
577
  return this.playSession;
536
578
  }).finally(() => {
537
579
  this.playSessionPromise = void 0;
580
+ this.playSessionPromiseKey = void 0;
538
581
  });
539
582
  }
540
583
  return this.playSessionPromise;
@@ -733,9 +776,23 @@ var LooplaySDK = class {
733
776
  if (!this.api) throw new MissingBaseUrlError();
734
777
  await this.api.listGameAssets(params.gameId);
735
778
  }
779
+ /**
780
+ * Best-effort self-report — must never block init(). A missing/misconfigured
781
+ * whitelist (a separate setup step from SDK integration) makes the backend
782
+ * reject this call; if that rejection propagated, the whole init() would
783
+ * reject too and every other SDK method would throw NotInitializedError,
784
+ * breaking a game over what's really just an unfinished onboarding step.
785
+ */
736
786
  async detectIntegration(params) {
737
787
  if (!this.api) return;
738
- await this.api.detectIntegration(params.gameId);
788
+ try {
789
+ await this.api.detectIntegration(params.gameId);
790
+ } catch (error) {
791
+ console.warn(
792
+ "[LooplaySDK] detectIntegration failed (SDK will still initialize) \u2014 check that this game has a whitelisted origin/domain configured:",
793
+ error
794
+ );
795
+ }
739
796
  }
740
797
  };
741
798
 
@@ -1107,7 +1164,16 @@ var LooplayIframeAuth = class {
1107
1164
  this.debug("emitGameEvent dispatched", { gameId, actionCode, ...opts });
1108
1165
  return this.getClient().emit(gameId, actionCode, opts);
1109
1166
  }
1110
- /** Idempotent; auto-emits GAME_STARTED exactly once per distinct auth session (bearer or anonymous). */
1167
+ /**
1168
+ * Idempotent; auto-emits GAME_STARTED exactly once per distinct auth
1169
+ * session (bearer or anonymous). SDK integration self-reporting for the
1170
+ * iframe path lives in `detectIntegrationOnce()` (fired from `setState()`
1171
+ * on every auth/config change, not gated behind lifecycle tracking being
1172
+ * enabled at all) — without it, games embedded via iframe would never
1173
+ * trigger the active detect call, only the passive heartbeat that
1174
+ * tracking calls leave behind (which doesn't fire while the game is still
1175
+ * IN_REVIEW/not LIVE, since tracking is blocked until then).
1176
+ */
1111
1177
  initLifecycleTracking() {
1112
1178
  if (this.lifecycleTrackingInitialized || typeof window === "undefined")
1113
1179
  return;
@@ -1236,8 +1302,11 @@ ${identity}`;
1236
1302
  }
1237
1303
  }
1238
1304
  setState(next) {
1305
+ const currentApiUrl = this.getApiUrl();
1239
1306
  this.state = next;
1240
- this.client = null;
1307
+ if (this.getApiUrl() !== currentApiUrl) {
1308
+ this.client = null;
1309
+ }
1241
1310
  this.debug("auth state updated", {
1242
1311
  gameId: this.getGameId(),
1243
1312
  apiUrl: this.getApiUrl(),
@@ -1316,6 +1385,7 @@ exports.ApiClient = ApiClient;
1316
1385
  exports.BrowserLocalStorageAuthStorage = BrowserLocalStorageAuthStorage;
1317
1386
  exports.HttpClient = HttpClient;
1318
1387
  exports.HttpError = HttpError;
1388
+ exports.LOOPLAY_SDK_VERSION = LOOPLAY_SDK_VERSION;
1319
1389
  exports.LooplayAuth = LooplayAuth;
1320
1390
  exports.LooplayIframeAuth = LooplayIframeAuth;
1321
1391
  exports.LooplaySDK = LooplaySDK;