@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.
@@ -137,6 +137,9 @@ var LooplayAuth = class {
137
137
  }
138
138
  };
139
139
 
140
+ // src/version.ts
141
+ var LOOPLAY_SDK_VERSION = "0.8.5";
142
+
140
143
  // src/apps/http.ts
141
144
  function getFetchFn(fetchOverride) {
142
145
  if (fetchOverride) {
@@ -164,7 +167,10 @@ var HttpClient = class {
164
167
  constructor(options) {
165
168
  this.baseUrl = options.baseUrl.replace(/\/$/, "");
166
169
  this.fetchFn = getFetchFn(options.fetch);
167
- this.defaultHeaders = options.defaultHeaders ?? {};
170
+ this.defaultHeaders = {
171
+ "x-looplay-sdk-version": LOOPLAY_SDK_VERSION,
172
+ ...options.defaultHeaders ?? {}
173
+ };
168
174
  }
169
175
  async request(method, path, options) {
170
176
  const url = this.buildUrl(path, options?.query);
@@ -267,6 +273,19 @@ var ServiceClient = class {
267
273
  body
268
274
  });
269
275
  }
276
+ /**
277
+ * Re-issues a fresh `playSessionToken` for the SAME `playSessionId` — call
278
+ * this instead of `startPlaySession` when refreshing an about-to-expire
279
+ * (or just-expired, within the server's renewal grace window) token, so
280
+ * long-running sessions keep a single `playSessionId` throughout.
281
+ */
282
+ async renewPlaySession(gameKey, auth = {}, body) {
283
+ return this.http.request("POST", "/sdk/games/play-sessions/renew", {
284
+ ...auth,
285
+ headers: { "x-game-key": gameKey, ...this.trackingHeaders(auth) },
286
+ body
287
+ });
288
+ }
270
289
  async completePlaySession(gameKey, auth, body) {
271
290
  await this.http.request("POST", "/sdk/games/play-sessions/complete", {
272
291
  ...auth,
@@ -436,9 +455,11 @@ var ApiClient = class {
436
455
  getAccessToken;
437
456
  getAnonymousId;
438
457
  trackingSession;
458
+ trackingSessionPromiseKey;
439
459
  trackingSessionPromise;
440
460
  currentPlaySessionId;
441
461
  playSession;
462
+ playSessionPromiseKey;
442
463
  playSessionPromise;
443
464
  constructor(options) {
444
465
  if (!options.baseUrl) throw new MissingBaseUrlError();
@@ -479,7 +500,10 @@ var ApiClient = class {
479
500
  const bearerToken = await this.getAccessToken?.();
480
501
  const anonId = bearerToken ? void 0 : this.getAnonymousId?.();
481
502
  if (!bearerToken && !anonId) throw new NotAuthenticatedError();
482
- const session = await this.ensureTrackingSession(anonId);
503
+ const session = await this.ensureTrackingSession(
504
+ bearerToken ? `bearer:${bearerToken}` : `anon:${anonId}`,
505
+ anonId
506
+ );
483
507
  return {
484
508
  bearerToken,
485
509
  anonId,
@@ -492,14 +516,16 @@ var ApiClient = class {
492
516
  * session. Swallows failures — tracking calls still work without it,
493
517
  * just without unified session tracing.
494
518
  */
495
- async ensureTrackingSession(anonId) {
519
+ async ensureTrackingSession(cacheKey, anonId) {
496
520
  const now = Date.now();
497
- if (this.trackingSession && this.trackingSession.expiresAtMs > now + 3e4) {
521
+ if (this.trackingSession && this.trackingSession.cacheKey === cacheKey && this.trackingSession.expiresAtMs > now + 3e4) {
498
522
  return this.trackingSession;
499
523
  }
500
- if (!this.trackingSessionPromise) {
524
+ if (!this.trackingSessionPromise || this.trackingSessionPromiseKey !== cacheKey) {
525
+ this.trackingSessionPromiseKey = cacheKey;
501
526
  this.trackingSessionPromise = this.raw.bootstrapTrackingSession(anonId).then((dto) => {
502
527
  this.trackingSession = {
528
+ cacheKey,
503
529
  trackingSessionId: dto.trackingSessionId,
504
530
  anonTrackingToken: dto.trackingToken,
505
531
  expiresAtMs: new Date(dto.expiresAt).getTime()
@@ -507,10 +533,18 @@ var ApiClient = class {
507
533
  return this.trackingSession;
508
534
  }).catch(() => void 0).finally(() => {
509
535
  this.trackingSessionPromise = void 0;
536
+ this.trackingSessionPromiseKey = void 0;
510
537
  });
511
538
  }
512
539
  return this.trackingSessionPromise;
513
540
  }
541
+ getTrackingAuthKey(auth) {
542
+ return [
543
+ auth.bearerToken ? `bearer:${auth.bearerToken}` : `anon:${auth.anonId ?? ""}`,
544
+ auth.trackingSessionId ?? "",
545
+ auth.anonTrackingToken ?? ""
546
+ ].join("\n");
547
+ }
514
548
  /**
515
549
  * Signs (and caches until near expiry) the `playSessionToken` required by
516
550
  * `trackPlay`/`trackMatch`/`emit` for the current play session — does not
@@ -519,12 +553,20 @@ var ApiClient = class {
519
553
  */
520
554
  async ensurePlaySession(gameKey, auth, playSessionId) {
521
555
  const now = Date.now();
522
- if (this.playSession && this.playSession.gameKey === gameKey && this.playSession.playSessionId === playSessionId && this.playSession.expiresAtMs > now + 1e4) {
556
+ const cacheKey = [gameKey, playSessionId, this.getTrackingAuthKey(auth)].join("\n");
557
+ if (this.playSession && this.playSession.cacheKey === cacheKey && this.playSession.gameKey === gameKey && this.playSession.playSessionId === playSessionId && this.playSession.expiresAtMs > now + 1e4) {
523
558
  return this.playSession;
524
559
  }
525
- if (!this.playSessionPromise) {
526
- this.playSessionPromise = this.raw.startPlaySession(gameKey, auth, { playSessionId }).then((dto) => {
560
+ const previous = this.playSession && this.playSession.cacheKey === cacheKey && this.playSession.gameKey === gameKey && this.playSession.playSessionId === playSessionId ? this.playSession : void 0;
561
+ if (!this.playSessionPromise || this.playSessionPromiseKey !== cacheKey) {
562
+ this.playSessionPromiseKey = cacheKey;
563
+ const issue = previous ? this.raw.renewPlaySession(gameKey, auth, {
564
+ playSessionId,
565
+ playSessionToken: previous.playSessionToken
566
+ }).catch(() => this.raw.startPlaySession(gameKey, auth, { playSessionId })) : this.raw.startPlaySession(gameKey, auth, { playSessionId });
567
+ this.playSessionPromise = issue.then((dto) => {
527
568
  this.playSession = {
569
+ cacheKey,
528
570
  gameKey,
529
571
  playSessionId: dto.playSessionId,
530
572
  playSessionToken: dto.playSessionToken,
@@ -533,6 +575,7 @@ var ApiClient = class {
533
575
  return this.playSession;
534
576
  }).finally(() => {
535
577
  this.playSessionPromise = void 0;
578
+ this.playSessionPromiseKey = void 0;
536
579
  });
537
580
  }
538
581
  return this.playSessionPromise;
@@ -883,6 +926,7 @@ var LooplayIframeAuth = class {
883
926
  initialized = false;
884
927
  lifecycleTrackingInitialized = false;
885
928
  lastGameStartedKey = null;
929
+ integrationDetectionKeys = /* @__PURE__ */ new Set();
886
930
  client = null;
887
931
  listeners = /* @__PURE__ */ new Set();
888
932
  options;
@@ -984,6 +1028,7 @@ var LooplayIframeAuth = class {
984
1028
  init() {
985
1029
  if (this.initialized || typeof window === "undefined") return;
986
1030
  this.initialized = true;
1031
+ void this.detectIntegrationOnce();
987
1032
  const isEmbedded = window.parent !== window;
988
1033
  this.debug("auth listener initialized", {
989
1034
  isIframe: isEmbedded,
@@ -1034,6 +1079,7 @@ var LooplayIframeAuth = class {
1034
1079
  this.anonymousFallbackTimer = null;
1035
1080
  this.initialized = false;
1036
1081
  this.lifecycleTrackingInitialized = false;
1082
+ this.integrationDetectionKeys.clear();
1037
1083
  this.listeners.clear();
1038
1084
  }
1039
1085
  /** Lazily builds (and rebuilds on auth change) the `ApiClient` bound to the current auth mode. */
@@ -1102,7 +1148,15 @@ var LooplayIframeAuth = class {
1102
1148
  this.debug("emitGameEvent dispatched", { gameId, actionCode, ...opts });
1103
1149
  return this.getClient().emit(gameId, actionCode, opts);
1104
1150
  }
1105
- /** Idempotent; auto-emits GAME_STARTED exactly once per distinct auth session (bearer or anonymous). */
1151
+ /**
1152
+ * Idempotent; auto-emits GAME_STARTED exactly once per distinct auth
1153
+ * session (bearer or anonymous), and self-reports SDK integration the
1154
+ * same way the standalone `LooplaySDK.init()` does — without this, games
1155
+ * embedded via iframe never trigger the active detect call, only the
1156
+ * passive heartbeat that tracking calls leave behind (which doesn't fire
1157
+ * at all while the game is still IN_REVIEW/not LIVE, since tracking is
1158
+ * blocked until then).
1159
+ */
1106
1160
  initLifecycleTracking() {
1107
1161
  if (this.lifecycleTrackingInitialized || typeof window === "undefined")
1108
1162
  return;
@@ -1112,6 +1166,11 @@ var LooplayIframeAuth = class {
1112
1166
  const key = this.getTrackingKey();
1113
1167
  if (!key || this.lastGameStartedKey === key) return;
1114
1168
  this.lastGameStartedKey = key;
1169
+ const gameId = this.getGameId();
1170
+ if (gameId) {
1171
+ this.debug("auto detectIntegration dispatch", { gameId });
1172
+ void this.getClient().detectIntegration(gameId).catch(() => void 0);
1173
+ }
1115
1174
  this.debug("auto GAME_STARTED dispatch");
1116
1175
  void this.emitGameEvent("GAME_STARTED");
1117
1176
  });
@@ -1231,8 +1290,11 @@ ${identity}`;
1231
1290
  }
1232
1291
  }
1233
1292
  setState(next) {
1293
+ const currentApiUrl = this.getApiUrl();
1234
1294
  this.state = next;
1235
- this.client = null;
1295
+ if (this.getApiUrl() !== currentApiUrl) {
1296
+ this.client = null;
1297
+ }
1236
1298
  this.debug("auth state updated", {
1237
1299
  gameId: this.getGameId(),
1238
1300
  apiUrl: this.getApiUrl(),
@@ -1241,6 +1303,28 @@ ${identity}`;
1241
1303
  hasAnonymousId: Boolean(next.anonymousId)
1242
1304
  });
1243
1305
  this.listeners.forEach((listener) => listener(next));
1306
+ void this.detectIntegrationOnce();
1307
+ }
1308
+ /** Detects each resolved game/API pair at most once for this iframe session. */
1309
+ async detectIntegrationOnce() {
1310
+ const gameId = this.getGameId();
1311
+ const apiUrl = this.getApiUrl();
1312
+ if (!gameId || !apiUrl) return;
1313
+ const key = `${apiUrl}
1314
+ ${gameId}`;
1315
+ if (this.integrationDetectionKeys.has(key)) return;
1316
+ this.integrationDetectionKeys.add(key);
1317
+ try {
1318
+ await this.getClient().detectIntegration(gameId);
1319
+ this.debug("integration detected", { gameId, apiUrl });
1320
+ } catch (error) {
1321
+ this.integrationDetectionKeys.delete(key);
1322
+ this.debug("integration detection failed", {
1323
+ gameId,
1324
+ apiUrl,
1325
+ error: error instanceof Error ? error.message : String(error)
1326
+ });
1327
+ }
1244
1328
  }
1245
1329
  resolveParentOrigin() {
1246
1330
  if (this.options.parentOrigin) return this.options.parentOrigin;
@@ -1285,6 +1369,6 @@ ${identity}`;
1285
1369
  }
1286
1370
  };
1287
1371
 
1288
- export { ApiClient, BrowserLocalStorageAuthStorage, HttpClient, HttpError, LooplayAuth, LooplayIframeAuth, LooplaySDK, LooplaySDKError, MemoryAuthStorage, MissingAuthError, MissingBaseUrlError, NotAuthenticatedError, NotInitializedError, ServiceClient, TelegramAuthProvider };
1372
+ export { ApiClient, BrowserLocalStorageAuthStorage, HttpClient, HttpError, LOOPLAY_SDK_VERSION, LooplayAuth, LooplayIframeAuth, LooplaySDK, LooplaySDKError, MemoryAuthStorage, MissingAuthError, MissingBaseUrlError, NotAuthenticatedError, NotInitializedError, ServiceClient, TelegramAuthProvider };
1289
1373
  //# sourceMappingURL=looplay-sdk.esm.js.map
1290
1374
  //# sourceMappingURL=looplay-sdk.esm.js.map