@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 +94 -22
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +14 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +94 -22
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
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) {
|