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