@rehers/rehers-roleplay-sdk 2.5.2 → 2.5.4

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.
Files changed (2) hide show
  1. package/package.json +2 -2
  2. package/roleplay-sdk.js +137 -16
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rehers/rehers-roleplay-sdk",
3
- "version": "2.5.2",
3
+ "version": "2.5.4",
4
4
  "description": "Seamless Roleplay SDK — embed roleplay call sessions via a modal + iframe",
5
5
  "main": "roleplay-sdk.js",
6
6
  "types": "index.d.ts",
@@ -32,6 +32,6 @@
32
32
  "license": "UNLICENSED",
33
33
  "repository": {
34
34
  "type": "git",
35
- "url": "https://github.com/rehers/seamless-frontend-independent"
35
+ "url": "git+https://github.com/rehers/seamless-frontend-independent.git"
36
36
  }
37
37
  }
package/roleplay-sdk.js CHANGED
@@ -28,6 +28,8 @@
28
28
  var sessionExpiresAt = 0; // epoch ms
29
29
  var refreshTimer = null;
30
30
  var fetchingSession = null; // single-flight Promise
31
+ var activeSessionXhr = null;
32
+ var activeInitVersion = 0;
31
33
 
32
34
  var initCallbacks = { onReady: null, onError: null };
33
35
  var initCalled = false;
@@ -48,6 +50,17 @@
48
50
  var dialogAddToScenarioPendingContacts = null;
49
51
  var dialogListener = null;
50
52
  var dialogCloseTeardownTimer = null;
53
+ var dialogRevealTimer = null;
54
+ var mountRevealTimer = null;
55
+
56
+ // Max wait before we reveal the iframe even if it never sends ROLEPLAY_READY.
57
+ // Prevents users from staring at an empty container if the hosted app is slow
58
+ // or fails to signal. 3s is conservative; the handshake normally lands in < 500ms.
59
+ var IFRAME_REVEAL_FALLBACK_MS = 3000;
60
+
61
+ function revealIframe(iframeEl) {
62
+ if (iframeEl) iframeEl.style.opacity = "1";
63
+ }
51
64
 
52
65
  // ── Safe logging ──────────────────────────────────────────────────
53
66
 
@@ -93,24 +106,54 @@
93
106
 
94
107
  // ── Session management ────────────────────────────────────────────
95
108
 
109
+ function clearSessionRequest() {
110
+ if (activeSessionXhr) {
111
+ try {
112
+ activeSessionXhr.abort();
113
+ } catch (_) {}
114
+ activeSessionXhr = null;
115
+ }
116
+ fetchingSession = null;
117
+ }
118
+
96
119
  function fetchSession() {
97
- if (fetchingSession) return fetchingSession;
120
+ var requestInitVersion = activeInitVersion;
121
+
122
+ if (fetchingSession && fetchingSession.initVersion === requestInitVersion) {
123
+ return fetchingSession.promise;
124
+ }
98
125
 
99
- fetchingSession = new Promise(function (resolve, reject) {
126
+ var requestPromise = new Promise(function (resolve, reject) {
100
127
  var url = getApiOrigin() + "/api/sdk/session";
101
128
  var body = userToken
102
129
  ? { userToken: userToken }
103
130
  : { userId: userId, userEmail: userEmail, userRole: userRole };
104
131
 
105
132
  var xhr = new XMLHttpRequest();
133
+ activeSessionXhr = xhr;
106
134
  xhr.open("POST", url, true);
107
135
  xhr.setRequestHeader("Content-Type", "application/json");
108
136
  xhr.setRequestHeader("X-Publishable-Key", publishableKey);
109
137
  xhr.withCredentials = false;
110
138
  xhr.timeout = SESSION_TIMEOUT_MS;
111
139
 
140
+ function cleanupRequest() {
141
+ if (activeSessionXhr === xhr) {
142
+ activeSessionXhr = null;
143
+ }
144
+ if (fetchingSession && fetchingSession.promise === requestPromise) {
145
+ fetchingSession = null;
146
+ }
147
+ }
148
+
112
149
  xhr.onload = function () {
113
- fetchingSession = null;
150
+ cleanupRequest();
151
+
152
+ if (requestInitVersion !== activeInitVersion) {
153
+ reject({ code: "ABORTED", message: "Stale session response ignored" });
154
+ return;
155
+ }
156
+
114
157
  var data;
115
158
  try {
116
159
  data = JSON.parse(xhr.responseText);
@@ -143,24 +186,29 @@
143
186
  };
144
187
 
145
188
  xhr.onerror = function () {
146
- fetchingSession = null;
189
+ cleanupRequest();
147
190
  reject({ code: "NETWORK_ERROR", message: "Network error contacting session endpoint" });
148
191
  };
149
192
 
150
193
  xhr.ontimeout = function () {
151
- fetchingSession = null;
194
+ cleanupRequest();
152
195
  reject({ code: "TIMEOUT", message: "Session request timed out after " + SESSION_TIMEOUT_MS + "ms" });
153
196
  };
154
197
 
155
198
  xhr.onabort = function () {
156
- fetchingSession = null;
199
+ cleanupRequest();
157
200
  reject({ code: "ABORTED", message: "Session request was aborted" });
158
201
  };
159
202
 
160
203
  xhr.send(JSON.stringify(body));
161
204
  });
162
205
 
163
- return fetchingSession;
206
+ fetchingSession = {
207
+ initVersion: requestInitVersion,
208
+ promise: requestPromise,
209
+ };
210
+
211
+ return requestPromise;
164
212
  }
165
213
 
166
214
  function getSessionToken() {
@@ -190,6 +238,10 @@
190
238
  clearTimeout(dialogCloseTeardownTimer);
191
239
  dialogCloseTeardownTimer = null;
192
240
  }
241
+ if (dialogRevealTimer) {
242
+ clearTimeout(dialogRevealTimer);
243
+ dialogRevealTimer = null;
244
+ }
193
245
 
194
246
  if (dialogOverlay && dialogOverlay.parentNode) {
195
247
  dialogOverlay.parentNode.removeChild(dialogOverlay);
@@ -214,6 +266,10 @@
214
266
 
215
267
  function teardownMount() {
216
268
  try {
269
+ if (mountRevealTimer) {
270
+ clearTimeout(mountRevealTimer);
271
+ mountRevealTimer = null;
272
+ }
217
273
  if (mountIframe && mountIframe.parentNode) {
218
274
  mountIframe.parentNode.removeChild(mountIframe);
219
275
  }
@@ -284,6 +340,11 @@
284
340
 
285
341
  switch (data.type) {
286
342
  case "ROLEPLAY_READY":
343
+ if (mountRevealTimer) {
344
+ clearTimeout(mountRevealTimer);
345
+ mountRevealTimer = null;
346
+ }
347
+ revealIframe(mountIframe);
287
348
  getSessionToken()
288
349
  .then(function (token) {
289
350
  dispatchInitToTarget(mountIframe, null, null);
@@ -322,6 +383,11 @@
322
383
 
323
384
  switch (data.type) {
324
385
  case "ROLEPLAY_READY":
386
+ if (dialogRevealTimer) {
387
+ clearTimeout(dialogRevealTimer);
388
+ dialogRevealTimer = null;
389
+ }
390
+ revealIframe(dialogIframe);
325
391
  getSessionToken()
326
392
  .then(function (token) {
327
393
  dispatchInitToTarget(dialogIframe, dialogContactData, dialogAddToScenarioPendingContacts);
@@ -332,9 +398,9 @@
332
398
  break;
333
399
 
334
400
  case "ROLEPLAY_ERROR":
335
- if (dialogCallbacks.onError) {
336
- dialogCallbacks.onError({ code: data.code, message: data.message });
337
- }
401
+ var regularOnError = dialogCallbacks.onError;
402
+ teardownDialog();
403
+ if (regularOnError) regularOnError({ code: data.code, message: data.message });
338
404
  break;
339
405
 
340
406
  case "ROLEPLAY_CLOSED":
@@ -357,9 +423,9 @@
357
423
  break;
358
424
 
359
425
  case "ADD_TO_SCENARIO_ERROR":
360
- if (dialogAddToScenarioCallbacks.onError) {
361
- dialogAddToScenarioCallbacks.onError({ code: data.code, message: data.message });
362
- }
426
+ var atsOnError = dialogAddToScenarioCallbacks.onError;
427
+ teardownDialog();
428
+ if (atsOnError) atsOnError({ code: data.code, message: data.message });
363
429
  break;
364
430
 
365
431
  case "ADD_TO_SCENARIO_CLOSED":
@@ -379,10 +445,30 @@
379
445
  try {
380
446
  sendMsg(dialogIframe, { type: "roleplay-close" });
381
447
  if (dialogCloseTeardownTimer) clearTimeout(dialogCloseTeardownTimer);
382
- dialogCloseTeardownTimer = setTimeout(teardownDialog, 300);
448
+ dialogCloseTeardownTimer = setTimeout(function () {
449
+ // Fallback path: iframe didn't respond with ROLEPLAY_CLOSED or
450
+ // ADD_TO_SCENARIO_CLOSED. Fire the host's onClose ourselves so
451
+ // React state (or any controlled caller) can re-render cleanly
452
+ // and reopen the dialog later.
453
+ var mode = dialogMode;
454
+ var atsClose = dialogAddToScenarioCallbacks.onClose;
455
+ var regularClose = dialogCallbacks.onClose;
456
+ teardownDialog();
457
+ try {
458
+ if (mode === "add-to-scenario" && atsClose) atsClose();
459
+ else if (regularClose) regularClose();
460
+ } catch (_) {}
461
+ }, 300);
383
462
  } catch (e) {
384
463
  logError("close", e);
464
+ var mode2 = dialogMode;
465
+ var atsClose2 = dialogAddToScenarioCallbacks.onClose;
466
+ var regularClose2 = dialogCallbacks.onClose;
385
467
  teardownDialog();
468
+ try {
469
+ if (mode2 === "add-to-scenario" && atsClose2) atsClose2();
470
+ else if (regularClose2) regularClose2();
471
+ } catch (_) {}
386
472
  }
387
473
  }
388
474
 
@@ -396,6 +482,11 @@
396
482
  iframeEl.style.height = "100%";
397
483
  iframeEl.style.border = "none";
398
484
  iframeEl.style.display = "block";
485
+ // Start invisible. The host app flashes a brief unstyled loader before it's
486
+ // ready; we reveal the iframe once ROLEPLAY_READY arrives (or after a
487
+ // fallback timeout). See revealIframe() callers.
488
+ iframeEl.style.opacity = "0";
489
+ iframeEl.style.transition = "opacity 120ms ease-out";
399
490
  return iframeEl;
400
491
  }
401
492
 
@@ -407,6 +498,7 @@
407
498
  */
408
499
  init: function (opts) {
409
500
  try {
501
+ var initVersion = activeInitVersion + 1;
410
502
  var hasUserToken = !!(opts && opts.userToken && String(opts.userToken).trim());
411
503
  var hasLegacyIdentity = !!(opts && opts.userId && opts.userEmail);
412
504
 
@@ -427,7 +519,8 @@
427
519
  clearTimeout(refreshTimer);
428
520
  refreshTimer = null;
429
521
  }
430
- fetchingSession = null;
522
+ activeInitVersion = initVersion;
523
+ clearSessionRequest();
431
524
  sessionToken = null;
432
525
  sessionExpiresAt = 0;
433
526
  paymentLink = null;
@@ -445,6 +538,7 @@
445
538
  // Fetch session immediately
446
539
  fetchSession()
447
540
  .then(function (result) {
541
+ if (initVersion !== activeInitVersion) return;
448
542
  if (result.trialMode && !paymentLink) {
449
543
  // User not found and no payment link from server — still call onReady
450
544
  // The open() will show an error state in the iframe
@@ -452,6 +546,8 @@
452
546
  if (initCallbacks.onReady) initCallbacks.onReady();
453
547
  })
454
548
  .catch(function (err) {
549
+ if (initVersion !== activeInitVersion) return;
550
+ if (err && err.code === "ABORTED") return;
455
551
  if (initCallbacks.onError) {
456
552
  initCallbacks.onError({ code: err.code || "INIT_ERROR", message: err.message || "Initialization failed" });
457
553
  }
@@ -553,6 +649,13 @@
553
649
  dialogOverlay = el;
554
650
  dialogIframe = iframeEl;
555
651
  document.body.appendChild(dialogOverlay);
652
+
653
+ // Fallback: reveal the iframe even if ROLEPLAY_READY never arrives.
654
+ if (dialogRevealTimer) clearTimeout(dialogRevealTimer);
655
+ dialogRevealTimer = setTimeout(function () {
656
+ revealIframe(dialogIframe);
657
+ dialogRevealTimer = null;
658
+ }, IFRAME_REVEAL_FALLBACK_MS);
556
659
  } catch (e) {
557
660
  logError("open", e);
558
661
  teardownDialog();
@@ -589,6 +692,13 @@
589
692
  var iframeEl = createIframe("/");
590
693
  mountIframe = iframeEl;
591
694
  container.appendChild(iframeEl);
695
+
696
+ // Fallback: reveal the iframe even if ROLEPLAY_READY never arrives.
697
+ if (mountRevealTimer) clearTimeout(mountRevealTimer);
698
+ mountRevealTimer = setTimeout(function () {
699
+ revealIframe(mountIframe);
700
+ mountRevealTimer = null;
701
+ }, IFRAME_REVEAL_FALLBACK_MS);
592
702
  } catch (e) {
593
703
  logError("mount", e);
594
704
  teardownMount();
@@ -679,6 +789,9 @@
679
789
  iframeEl.style.top = "0";
680
790
  iframeEl.style.left = "0";
681
791
  iframeEl.style.zIndex = "1";
792
+ // Hide until ROLEPLAY_READY so we don't flash the host app's pre-init state.
793
+ iframeEl.style.opacity = "0";
794
+ iframeEl.style.transition = "opacity 120ms ease-out";
682
795
 
683
796
  var closeBtn = document.createElement("button");
684
797
  closeBtn.type = "button";
@@ -716,6 +829,13 @@
716
829
  dialogOverlay = el;
717
830
  dialogIframe = iframeEl;
718
831
  document.body.appendChild(dialogOverlay);
832
+
833
+ // Fallback: reveal the iframe even if ROLEPLAY_READY never arrives.
834
+ if (dialogRevealTimer) clearTimeout(dialogRevealTimer);
835
+ dialogRevealTimer = setTimeout(function () {
836
+ revealIframe(dialogIframe);
837
+ dialogRevealTimer = null;
838
+ }, IFRAME_REVEAL_FALLBACK_MS);
719
839
  } catch (e) {
720
840
  logError("addToScenario", e);
721
841
  teardownDialog();
@@ -753,10 +873,12 @@
753
873
  */
754
874
  destroy: function () {
755
875
  try {
876
+ activeInitVersion++;
756
877
  if (refreshTimer) {
757
878
  clearTimeout(refreshTimer);
758
879
  refreshTimer = null;
759
880
  }
881
+ clearSessionRequest();
760
882
  teardownDialog();
761
883
  teardownMount();
762
884
  publishableKey = null;
@@ -767,7 +889,6 @@
767
889
  paymentLink = null;
768
890
  sessionToken = null;
769
891
  sessionExpiresAt = 0;
770
- fetchingSession = null;
771
892
  initCallbacks = { onReady: null, onError: null };
772
893
  initCalled = false;
773
894
  } catch (e) {