@looplay/sdk 0.8.5 → 0.8.7

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;
@@ -885,6 +928,7 @@ var LooplayIframeAuth = class {
885
928
  initialized = false;
886
929
  lifecycleTrackingInitialized = false;
887
930
  lastGameStartedKey = null;
931
+ integrationDetectionKeys = /* @__PURE__ */ new Set();
888
932
  client = null;
889
933
  listeners = /* @__PURE__ */ new Set();
890
934
  options;
@@ -986,6 +1030,7 @@ var LooplayIframeAuth = class {
986
1030
  init() {
987
1031
  if (this.initialized || typeof window === "undefined") return;
988
1032
  this.initialized = true;
1033
+ void this.detectIntegrationOnce();
989
1034
  const isEmbedded = window.parent !== window;
990
1035
  this.debug("auth listener initialized", {
991
1036
  isIframe: isEmbedded,
@@ -1036,6 +1081,7 @@ var LooplayIframeAuth = class {
1036
1081
  this.anonymousFallbackTimer = null;
1037
1082
  this.initialized = false;
1038
1083
  this.lifecycleTrackingInitialized = false;
1084
+ this.integrationDetectionKeys.clear();
1039
1085
  this.listeners.clear();
1040
1086
  }
1041
1087
  /** Lazily builds (and rebuilds on auth change) the `ApiClient` bound to the current auth mode. */
@@ -1104,7 +1150,15 @@ var LooplayIframeAuth = class {
1104
1150
  this.debug("emitGameEvent dispatched", { gameId, actionCode, ...opts });
1105
1151
  return this.getClient().emit(gameId, actionCode, opts);
1106
1152
  }
1107
- /** Idempotent; auto-emits GAME_STARTED exactly once per distinct auth session (bearer or anonymous). */
1153
+ /**
1154
+ * Idempotent; auto-emits GAME_STARTED exactly once per distinct auth
1155
+ * session (bearer or anonymous), and self-reports SDK integration the
1156
+ * same way the standalone `LooplaySDK.init()` does — without this, games
1157
+ * embedded via iframe never trigger the active detect call, only the
1158
+ * passive heartbeat that tracking calls leave behind (which doesn't fire
1159
+ * at all while the game is still IN_REVIEW/not LIVE, since tracking is
1160
+ * blocked until then).
1161
+ */
1108
1162
  initLifecycleTracking() {
1109
1163
  if (this.lifecycleTrackingInitialized || typeof window === "undefined")
1110
1164
  return;
@@ -1114,6 +1168,11 @@ var LooplayIframeAuth = class {
1114
1168
  const key = this.getTrackingKey();
1115
1169
  if (!key || this.lastGameStartedKey === key) return;
1116
1170
  this.lastGameStartedKey = key;
1171
+ const gameId = this.getGameId();
1172
+ if (gameId) {
1173
+ this.debug("auto detectIntegration dispatch", { gameId });
1174
+ void this.getClient().detectIntegration(gameId).catch(() => void 0);
1175
+ }
1117
1176
  this.debug("auto GAME_STARTED dispatch");
1118
1177
  void this.emitGameEvent("GAME_STARTED");
1119
1178
  });
@@ -1233,8 +1292,11 @@ ${identity}`;
1233
1292
  }
1234
1293
  }
1235
1294
  setState(next) {
1295
+ const currentApiUrl = this.getApiUrl();
1236
1296
  this.state = next;
1237
- this.client = null;
1297
+ if (this.getApiUrl() !== currentApiUrl) {
1298
+ this.client = null;
1299
+ }
1238
1300
  this.debug("auth state updated", {
1239
1301
  gameId: this.getGameId(),
1240
1302
  apiUrl: this.getApiUrl(),
@@ -1243,6 +1305,28 @@ ${identity}`;
1243
1305
  hasAnonymousId: Boolean(next.anonymousId)
1244
1306
  });
1245
1307
  this.listeners.forEach((listener) => listener(next));
1308
+ void this.detectIntegrationOnce();
1309
+ }
1310
+ /** Detects each resolved game/API pair at most once for this iframe session. */
1311
+ async detectIntegrationOnce() {
1312
+ const gameId = this.getGameId();
1313
+ const apiUrl = this.getApiUrl();
1314
+ if (!gameId || !apiUrl) return;
1315
+ const key = `${apiUrl}
1316
+ ${gameId}`;
1317
+ if (this.integrationDetectionKeys.has(key)) return;
1318
+ this.integrationDetectionKeys.add(key);
1319
+ try {
1320
+ await this.getClient().detectIntegration(gameId);
1321
+ this.debug("integration detected", { gameId, apiUrl });
1322
+ } catch (error) {
1323
+ this.integrationDetectionKeys.delete(key);
1324
+ this.debug("integration detection failed", {
1325
+ gameId,
1326
+ apiUrl,
1327
+ error: error instanceof Error ? error.message : String(error)
1328
+ });
1329
+ }
1246
1330
  }
1247
1331
  resolveParentOrigin() {
1248
1332
  if (this.options.parentOrigin) return this.options.parentOrigin;
@@ -1291,6 +1375,7 @@ exports.ApiClient = ApiClient;
1291
1375
  exports.BrowserLocalStorageAuthStorage = BrowserLocalStorageAuthStorage;
1292
1376
  exports.HttpClient = HttpClient;
1293
1377
  exports.HttpError = HttpError;
1378
+ exports.LOOPLAY_SDK_VERSION = LOOPLAY_SDK_VERSION;
1294
1379
  exports.LooplayAuth = LooplayAuth;
1295
1380
  exports.LooplayIframeAuth = LooplayIframeAuth;
1296
1381
  exports.LooplaySDK = LooplaySDK;