@usero/sdk 1.1.0 → 1.1.1

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.
@@ -625,12 +625,15 @@ function showMuteToast(store) {
625
625
  toast.setAttribute("role", "status");
626
626
  toast.innerHTML = `<strong>Mic off.</strong> Screen is still recording. Tap to unmute.`;
627
627
  slot.appendChild(toast);
628
- window.setTimeout(() => {
628
+ const outer = window.setTimeout(() => {
629
+ if (!toast.isConnected) return;
629
630
  toast.setAttribute("data-leaving", "true");
630
- window.setTimeout(() => {
631
- toast.remove();
631
+ const inner = window.setTimeout(() => {
632
+ if (toast.isConnected) toast.remove();
632
633
  }, 260);
634
+ store.muteToastTimers.push(inner);
633
635
  }, 3e3);
636
+ store.muteToastTimers.push(outer);
634
637
  }
635
638
  function openNotePopover(store, onSave, onCancel) {
636
639
  const root = store.indicatorRoot;
@@ -758,6 +761,17 @@ function showThanksScreen(root, opts) {
758
761
  sent.textContent = message;
759
762
  card.appendChild(sent);
760
763
  };
764
+ const ERROR_CLASS = "end-error";
765
+ const showError = (message) => {
766
+ const prior = form.querySelector(`.${ERROR_CLASS}`);
767
+ if (prior) prior.remove();
768
+ const err = document.createElement("p");
769
+ err.className = ERROR_CLASS;
770
+ err.textContent = message;
771
+ err.setAttribute("role", "alert");
772
+ err.style.cssText = "margin:10px 0 0;font-size:12.5px;color:#b91c1c;text-align:center;";
773
+ form.appendChild(err);
774
+ };
761
775
  const submit = async () => {
762
776
  const text = ta.value.trim();
763
777
  ta.disabled = true;
@@ -765,10 +779,21 @@ function showThanksScreen(root, opts) {
765
779
  const submitBtn = form.querySelector("button.primary");
766
780
  if (submitBtn) submitBtn.disabled = true;
767
781
  if (text) {
768
- await opts.onSubmitNote(text);
769
- swapToSent("Thanks. You can close this tab.");
782
+ try {
783
+ await Promise.race([
784
+ Promise.resolve(opts.onSubmitNote(text)),
785
+ new Promise((_, reject) => {
786
+ window.setTimeout(() => reject(new Error("timeout")), 3e4);
787
+ })
788
+ ]);
789
+ swapToSent("Thanks. You can close this tab.");
790
+ } catch {
791
+ ta.disabled = false;
792
+ skipBtn.disabled = false;
793
+ if (submitBtn) submitBtn.disabled = false;
794
+ showError("Couldn't save your note. Try again?");
795
+ }
770
796
  } else {
771
- opts.onSkip();
772
797
  swapToSent("All good. You can close this tab.");
773
798
  }
774
799
  };
@@ -825,6 +850,12 @@ async function finaliseSession(apiUrl, sessionId, durationSeconds, extras = {})
825
850
  }
826
851
  const trimmedEndNote = extras.endNote?.trim();
827
852
  if (trimmedEndNote) body.endNote = trimmedEndNote;
853
+ if (extras.notes && extras.notes.length > 0) {
854
+ body.notes = extras.notes.slice(0, 200).map((n) => ({
855
+ atMs: Math.max(0, Math.round(n.atMs)),
856
+ text: n.text
857
+ }));
858
+ }
828
859
  const res = await fetch(`${apiUrl.replace(/\/$/, "")}/api/user-test-sessions/${encodeURIComponent(sessionId)}/finalise`, {
829
860
  method: "POST",
830
861
  headers: { "Content-Type": "application/json" },
@@ -836,7 +867,7 @@ async function finaliseSession(apiUrl, sessionId, durationSeconds, extras = {})
836
867
  return false;
837
868
  }
838
869
  }
839
- async function postNote(apiUrl, sessionId, atMs, text, logger) {
870
+ async function postNoteOnce(apiUrl, sessionId, atMs, text, logger) {
840
871
  try {
841
872
  const res = await fetch(`${apiUrl.replace(/\/$/, "")}/api/user-test-sessions/${encodeURIComponent(sessionId)}/notes`, {
842
873
  method: "POST",
@@ -846,14 +877,26 @@ async function postNote(apiUrl, sessionId, atMs, text, logger) {
846
877
  });
847
878
  if (!res.ok) {
848
879
  logger.warn(`note POST rejected with ${res.status}`);
849
- return false;
880
+ return { ok: false, transient: res.status >= 500 || res.status === 408 || res.status === 429 };
850
881
  }
851
- return true;
882
+ let id;
883
+ try {
884
+ const json = await res.json();
885
+ if (typeof json.id === "string") id = json.id;
886
+ } catch {
887
+ }
888
+ return { ok: true, id, transient: false };
852
889
  } catch (err) {
853
890
  logger.warn("note POST failed", err);
854
- return false;
891
+ return { ok: false, transient: true };
855
892
  }
856
893
  }
894
+ async function postNoteWithRetry(apiUrl, sessionId, atMs, text, logger) {
895
+ const first = await postNoteOnce(apiUrl, sessionId, atMs, text, logger);
896
+ if (first.ok || !first.transient) return first;
897
+ await new Promise((resolve) => setTimeout(resolve, 400 + Math.floor(Math.random() * 200)));
898
+ return postNoteOnce(apiUrl, sessionId, atMs, text, logger);
899
+ }
857
900
  async function flushPendingFromIdb(store, ctx) {
858
901
  if (!store.sessionId) return;
859
902
  const pending = await idbListChunks(store.sessionId);
@@ -974,7 +1017,9 @@ function stopRecording(store) {
974
1017
  }
975
1018
  async function finishFlow(store, ctx, opts) {
976
1019
  if (store.cancelled) return;
1020
+ if (store.finishFlowRan) return;
977
1021
  if (store.indicatorState === "finishing" || store.indicatorState === "done") return;
1022
+ store.finishFlowRan = true;
978
1023
  store.indicatorState = "finishing";
979
1024
  flushMuteIfActive(store);
980
1025
  renderIndicatorState(store);
@@ -983,9 +1028,16 @@ async function finishFlow(store, ctx, opts) {
983
1028
  await flushPendingFromIdb(store, ctx);
984
1029
  const durationSeconds = (Date.now() - store.startedAt) / 1e3;
985
1030
  if (store.sessionId) {
1031
+ const unackedNotes = store.notes.filter((n) => !n.acked).map((n) => ({ atMs: n.atMs, text: n.text }));
986
1032
  const ok = await finaliseSession(store.options.apiUrl, store.sessionId, durationSeconds, {
987
- mutedSegments: store.mutedSegments
1033
+ mutedSegments: store.mutedSegments,
1034
+ notes: unackedNotes
988
1035
  });
1036
+ if (ok) {
1037
+ for (const n of store.notes) {
1038
+ if (!n.acked) n.acked = true;
1039
+ }
1040
+ }
989
1041
  store.indicatorState = ok ? "done" : "error";
990
1042
  } else {
991
1043
  store.indicatorState = "error";
@@ -996,10 +1048,15 @@ async function finishFlow(store, ctx, opts) {
996
1048
  onSubmitNote: async (text) => {
997
1049
  if (!store.sessionId) return;
998
1050
  store.endNote = text;
999
- await finaliseSession(store.options.apiUrl, store.sessionId, durationSeconds, {
1000
- mutedSegments: store.mutedSegments,
1001
- endNote: text
1051
+ const stillUnacked = store.notes.filter((n) => !n.acked).map((n) => ({ atMs: n.atMs, text: n.text }));
1052
+ const ok = await finaliseSession(store.options.apiUrl, store.sessionId, durationSeconds, {
1053
+ endNote: text,
1054
+ notes: stillUnacked
1002
1055
  });
1056
+ if (!ok) throw new Error("finalise failed");
1057
+ for (const n of store.notes) {
1058
+ if (!n.acked) n.acked = true;
1059
+ }
1003
1060
  },
1004
1061
  onSkip: () => {
1005
1062
  }
@@ -1046,10 +1103,12 @@ function userTest(options = {}) {
1046
1103
  mutedSinceMs: null,
1047
1104
  mutedSegments: [],
1048
1105
  muteToastShown: false,
1106
+ muteToastTimers: [],
1049
1107
  notes: [],
1050
1108
  notesPopoverOpen: false,
1051
1109
  notePopoverAtMs: null,
1052
- endNote: ""
1110
+ endNote: "",
1111
+ finishFlowRan: false
1053
1112
  };
1054
1113
  ctx.setStore(store);
1055
1114
  const onFinish = () => {
@@ -1079,11 +1138,19 @@ function userTest(options = {}) {
1079
1138
  store,
1080
1139
  (text) => {
1081
1140
  const atMs = store.notePopoverAtMs ?? Math.max(0, Date.now() - store.startedAt);
1082
- store.notes.push({ atMs, text });
1141
+ const note = { atMs, text, acked: false };
1142
+ store.notes.push(note);
1083
1143
  closeNote();
1084
1144
  renderNotesCount(store);
1085
1145
  if (store.sessionId) {
1086
- void postNote(store.options.apiUrl, store.sessionId, atMs, text, ctx.logger);
1146
+ const sessionId = store.sessionId;
1147
+ void (async () => {
1148
+ const result = await postNoteWithRetry(store.options.apiUrl, sessionId, atMs, text, ctx.logger);
1149
+ if (result.ok) {
1150
+ note.acked = true;
1151
+ if (result.id) note.serverId = result.id;
1152
+ }
1153
+ })();
1087
1154
  }
1088
1155
  },
1089
1156
  () => closeNote()
@@ -1166,6 +1233,13 @@ function userTest(options = {}) {
1166
1233
  document.removeEventListener("keydown", store.keydownHandler);
1167
1234
  store.keydownHandler = null;
1168
1235
  }
1236
+ for (const id of store.muteToastTimers) {
1237
+ try {
1238
+ window.clearTimeout(id);
1239
+ } catch {
1240
+ }
1241
+ }
1242
+ store.muteToastTimers = [];
1169
1243
  if (store.indicator && store.indicator.parentNode) {
1170
1244
  store.indicator.parentNode.removeChild(store.indicator);
1171
1245
  }