@mushi-mushi/core 1.17.0 → 1.19.0

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.
package/dist/index.cjs CHANGED
@@ -842,6 +842,8 @@ var LS_KEY = "mushi_offline_queue";
842
842
  var BATCH_SIZE = 10;
843
843
  var MAX_BACKOFF_MS = 6e4;
844
844
  var AUTO_FLUSH_INTERVAL_MS = 3e4;
845
+ var MAX_DELIVERY_ATTEMPTS = 8;
846
+ var MAX_QUEUE_AGE_MS = 24 * 60 * 60 * 1e3;
845
847
  function createOfflineQueue(config = {}) {
846
848
  const { enabled = true, maxQueueSize = 50, syncOnReconnect = true, encryptAtRest = true } = config;
847
849
  let syncCleanup = null;
@@ -874,6 +876,17 @@ function createOfflineQueue(config = {}) {
874
876
  function isEncryptedRecord(row) {
875
877
  return !!row.payload && isEncryptedPayload(row.payload);
876
878
  }
879
+ function rowAttempts(row) {
880
+ const n = row.attempts;
881
+ return typeof n === "number" && Number.isFinite(n) ? n : 0;
882
+ }
883
+ function isExpired(row, now) {
884
+ const queuedAt = row.queuedAt;
885
+ if (!queuedAt) return false;
886
+ const ts = Date.parse(queuedAt);
887
+ if (Number.isNaN(ts)) return false;
888
+ return now - ts > MAX_QUEUE_AGE_MS;
889
+ }
877
890
  function detectBackend() {
878
891
  if (backendType) return backendType;
879
892
  if (typeof indexedDB !== "undefined") {
@@ -929,6 +942,15 @@ function createOfflineQueue(config = {}) {
929
942
  tx.onerror = () => reject(tx.error);
930
943
  });
931
944
  }
945
+ async function idbPutRow(row) {
946
+ const db = await openDb();
947
+ return new Promise((resolve, reject) => {
948
+ const tx = db.transaction(STORE_NAME, "readwrite");
949
+ tx.objectStore(STORE_NAME).put(row);
950
+ tx.oncomplete = () => resolve();
951
+ tx.onerror = () => reject(tx.error);
952
+ });
953
+ }
932
954
  async function idbSize() {
933
955
  const db = await openDb();
934
956
  return new Promise((resolve, reject) => {
@@ -970,6 +992,38 @@ function createOfflineQueue(config = {}) {
970
992
  const rows = lsRead().filter((r) => r.id !== id);
971
993
  lsWrite(rows);
972
994
  }
995
+ function lsUpdateRow(row) {
996
+ const rows = lsRead();
997
+ const idx = rows.findIndex((r) => r.id === row.id);
998
+ if (idx >= 0) {
999
+ rows[idx] = row;
1000
+ lsWrite(rows);
1001
+ }
1002
+ }
1003
+ async function removeRow(backend, rowId) {
1004
+ try {
1005
+ if (backend === "indexeddb") await idbDelete(rowId);
1006
+ else lsDelete(rowId);
1007
+ } catch (err) {
1008
+ queueLog.debug("Offline queue: row removal failed (will age out)", {
1009
+ id: rowId,
1010
+ err: String(err)
1011
+ });
1012
+ }
1013
+ }
1014
+ async function persistRow(backend, row) {
1015
+ try {
1016
+ if (backend === "indexeddb") await idbPutRow(row);
1017
+ else lsUpdateRow(row);
1018
+ return true;
1019
+ } catch (err) {
1020
+ queueLog.debug("Offline queue: row persist failed", {
1021
+ id: row.id,
1022
+ err: String(err)
1023
+ });
1024
+ return false;
1025
+ }
1026
+ }
973
1027
  async function enqueue(report) {
974
1028
  if (!enabled) return;
975
1029
  const currentSize = await size();
@@ -1010,6 +1064,18 @@ function createOfflineQueue(config = {}) {
1010
1064
  } else {
1011
1065
  rows = lsRead();
1012
1066
  }
1067
+ const now = Date.now();
1068
+ const fresh = [];
1069
+ for (const row of rows) {
1070
+ if (isExpired(row, now)) {
1071
+ const rowId = row.id;
1072
+ await removeRow(backend, rowId);
1073
+ queueLog.debug("Offline queue: evicting stale report", { id: rowId });
1074
+ } else {
1075
+ fresh.push(row);
1076
+ }
1077
+ }
1078
+ rows = fresh;
1013
1079
  const batch = rows.slice(0, BATCH_SIZE);
1014
1080
  let sent = 0;
1015
1081
  let failed = 0;
@@ -1018,23 +1084,13 @@ function createOfflineQueue(config = {}) {
1018
1084
  const rowId = row.id;
1019
1085
  const report = await unwrapForSend(row);
1020
1086
  if (!report) {
1021
- try {
1022
- if (backend === "indexeddb") await idbDelete(rowId);
1023
- else lsDelete(rowId);
1024
- } catch {
1025
- lsDelete(rowId);
1026
- }
1087
+ await removeRow(backend, rowId);
1027
1088
  failed++;
1028
1089
  continue;
1029
1090
  }
1030
1091
  const result = await client.submitReport(report);
1031
1092
  if (result.ok) {
1032
- try {
1033
- if (backend === "indexeddb") await idbDelete(rowId);
1034
- else lsDelete(rowId);
1035
- } catch {
1036
- lsDelete(rowId);
1037
- }
1093
+ await removeRow(backend, rowId);
1038
1094
  sent++;
1039
1095
  } else {
1040
1096
  const permanent = result.error?.code === "HTTP_400" || result.error?.code === "HTTP_413" || result.error?.code === "HTTP_422" || result.error?.code === "INGEST_ERROR" || result.error?.code === "VALIDATION_ERROR" || // A payload that exceeds the size guard will never shrink on its own;
@@ -1046,17 +1102,33 @@ function createOfflineQueue(config = {}) {
1046
1102
  );
1047
1103
  const transient = !permanent && (result.error?.code === "NETWORK_ERROR" || result.error?.code === "HTTP_403" || result.error?.code === "HTTP_429" || result.error?.code === "HTTP_502" || result.error?.code === "HTTP_503" || result.error?.code === "HTTP_504" || typeof result.error?.code === "string" && result.error.code.startsWith("HTTP_5"));
1048
1104
  if (permanent) {
1049
- try {
1050
- if (backend === "indexeddb") await idbDelete(rowId);
1051
- else lsDelete(rowId);
1052
- } catch {
1053
- lsDelete(rowId);
1054
- }
1105
+ await removeRow(backend, rowId);
1055
1106
  } else if (transient) {
1056
- queueLog.debug("Offline queue: transient failure, will retry", {
1057
- id: rowId,
1058
- code: result.error?.code
1059
- });
1107
+ const nextAttempts = rowAttempts(row) + 1;
1108
+ if (nextAttempts >= MAX_DELIVERY_ATTEMPTS) {
1109
+ await removeRow(backend, rowId);
1110
+ queueLog.warn("Offline queue: giving up on report after repeated failures", {
1111
+ id: rowId,
1112
+ attempts: nextAttempts,
1113
+ code: result.error?.code
1114
+ });
1115
+ } else {
1116
+ row.attempts = nextAttempts;
1117
+ const persisted = await persistRow(backend, row);
1118
+ if (persisted) {
1119
+ queueLog.debug("Offline queue: transient failure, will retry", {
1120
+ id: rowId,
1121
+ attempts: nextAttempts,
1122
+ code: result.error?.code
1123
+ });
1124
+ } else {
1125
+ await removeRow(backend, rowId);
1126
+ queueLog.warn("Offline queue: dropping report (attempt counter unpersistable)", {
1127
+ id: rowId,
1128
+ code: result.error?.code
1129
+ });
1130
+ }
1131
+ }
1060
1132
  }
1061
1133
  failed++;
1062
1134
  if (i < batch.length - 1) {