@mushi-mushi/core 1.27.0 → 1.27.2
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/README.md +1 -1
- package/dist/index.cjs +151 -68
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +151 -68
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -138,4 +138,4 @@ MIT
|
|
|
138
138
|
<!-- mushi-readme-stats-footer -->
|
|
139
139
|
---
|
|
140
140
|
|
|
141
|
-
<sub>Monorepo scale (July 2026): 55 edge functions ·
|
|
141
|
+
<sub>Monorepo scale (July 2026): 55 edge functions · 336 SQL migrations · 13 outbound plugins · 11 inbound adapters · 19 pipeline agents. Canonical counts: <a href="https://github.com/kensaurus/mushi-mushi/blob/master/docs/stats.md">docs/stats.md</a> · <code>pnpm docs-stats</code></sub>
|
package/dist/index.cjs
CHANGED
|
@@ -111,6 +111,7 @@ var DEFAULT_TIMEOUT = 1e4;
|
|
|
111
111
|
var DEFAULT_MAX_RETRIES = 2;
|
|
112
112
|
var DEFAULT_CIRCUIT_THRESHOLD = 4;
|
|
113
113
|
var DEFAULT_CIRCUIT_COOLDOWN_MS = 3e4;
|
|
114
|
+
var MAX_INLINE_RETRY_WAIT_MS = 1e4;
|
|
114
115
|
function createApiClient(options) {
|
|
115
116
|
const {
|
|
116
117
|
projectId,
|
|
@@ -185,11 +186,15 @@ function createApiClient(options) {
|
|
|
185
186
|
`[Mushi] Credentials rejected (HTTP ${response.status}). Check your Project ID and API key scope (must be "report:write"). ` + where
|
|
186
187
|
);
|
|
187
188
|
}
|
|
188
|
-
|
|
189
|
-
|
|
189
|
+
const rateLimited = response.status === 429;
|
|
190
|
+
const unusable = response.status >= 500 || rateLimited;
|
|
191
|
+
const retryAfterMs = rateLimited ? parseRetryAfter(response.headers.get("Retry-After")) : null;
|
|
192
|
+
const canWaitInline = retryAfterMs === null || retryAfterMs <= MAX_INLINE_RETRY_WAIT_MS;
|
|
193
|
+
if (unusable && retries > 0 && canWaitInline) {
|
|
194
|
+
await sleep(retryAfterMs ?? getBackoffDelay(maxRetries - retries));
|
|
190
195
|
return request(method, path, body, retries - 1, internalKind, extraHeaders);
|
|
191
196
|
}
|
|
192
|
-
if (
|
|
197
|
+
if (unusable) cbRecordUnreachable();
|
|
193
198
|
else cbRecordReachable();
|
|
194
199
|
return {
|
|
195
200
|
ok: false,
|
|
@@ -207,7 +212,7 @@ function createApiClient(options) {
|
|
|
207
212
|
clearTimeout(timer);
|
|
208
213
|
if (retries > 0 && isRetryable(error)) {
|
|
209
214
|
await sleep(getBackoffDelay(maxRetries - retries));
|
|
210
|
-
return request(method, path, body, retries - 1, internalKind);
|
|
215
|
+
return request(method, path, body, retries - 1, internalKind, extraHeaders);
|
|
211
216
|
}
|
|
212
217
|
cbRecordUnreachable();
|
|
213
218
|
return {
|
|
@@ -497,6 +502,16 @@ function sleep(ms) {
|
|
|
497
502
|
function getBackoffDelay(attempt) {
|
|
498
503
|
return Math.min(1e3 * 2 ** attempt + Math.random() * 500, 1e4);
|
|
499
504
|
}
|
|
505
|
+
function parseRetryAfter(value, now2 = Date.now()) {
|
|
506
|
+
if (!value) return null;
|
|
507
|
+
const raw = value.trim();
|
|
508
|
+
if (!raw) return null;
|
|
509
|
+
const seconds = Number(raw);
|
|
510
|
+
if (Number.isFinite(seconds)) return seconds < 0 ? null : seconds * 1e3;
|
|
511
|
+
const at = Date.parse(raw);
|
|
512
|
+
if (Number.isNaN(at)) return null;
|
|
513
|
+
return Math.max(at - now2, 0);
|
|
514
|
+
}
|
|
500
515
|
function isRetryable(error) {
|
|
501
516
|
if (error instanceof DOMException && error.name === "AbortError") return true;
|
|
502
517
|
if (error instanceof TypeError) return true;
|
|
@@ -1056,12 +1071,27 @@ function openKeyDb() {
|
|
|
1056
1071
|
return new Promise((resolve, reject) => {
|
|
1057
1072
|
const req = indexedDB.open(KEY_DB, 1);
|
|
1058
1073
|
req.onupgradeneeded = () => {
|
|
1074
|
+
try {
|
|
1075
|
+
const db = req.result;
|
|
1076
|
+
if (!db) return;
|
|
1077
|
+
if (!db.objectStoreNames.contains(KEY_STORE)) {
|
|
1078
|
+
db.createObjectStore(KEY_STORE);
|
|
1079
|
+
}
|
|
1080
|
+
} catch {
|
|
1081
|
+
}
|
|
1082
|
+
};
|
|
1083
|
+
req.onsuccess = () => {
|
|
1059
1084
|
const db = req.result;
|
|
1060
|
-
if (!db.objectStoreNames.contains(KEY_STORE)) {
|
|
1061
|
-
|
|
1085
|
+
if (!db || !db.objectStoreNames.contains(KEY_STORE)) {
|
|
1086
|
+
try {
|
|
1087
|
+
db?.close();
|
|
1088
|
+
} catch {
|
|
1089
|
+
}
|
|
1090
|
+
reject(new Error("key object store unavailable"));
|
|
1091
|
+
return;
|
|
1062
1092
|
}
|
|
1093
|
+
resolve(db);
|
|
1063
1094
|
};
|
|
1064
|
-
req.onsuccess = () => resolve(req.result);
|
|
1065
1095
|
req.onerror = () => reject(req.error);
|
|
1066
1096
|
});
|
|
1067
1097
|
}
|
|
@@ -1151,6 +1181,8 @@ function createOfflineQueue(config = {}) {
|
|
|
1151
1181
|
let syncCleanup = null;
|
|
1152
1182
|
let flushInterval = null;
|
|
1153
1183
|
let backendType = null;
|
|
1184
|
+
let inFlight = null;
|
|
1185
|
+
let followUp = null;
|
|
1154
1186
|
async function wrapForStorage(report) {
|
|
1155
1187
|
const queuedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1156
1188
|
if (!encryptAtRest) {
|
|
@@ -1204,12 +1236,28 @@ function createOfflineQueue(config = {}) {
|
|
|
1204
1236
|
return new Promise((resolve, reject) => {
|
|
1205
1237
|
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
|
1206
1238
|
request.onupgradeneeded = () => {
|
|
1239
|
+
try {
|
|
1240
|
+
const db = request.result;
|
|
1241
|
+
if (!db) return;
|
|
1242
|
+
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
1243
|
+
db.createObjectStore(STORE_NAME, { keyPath: "id" });
|
|
1244
|
+
}
|
|
1245
|
+
} catch {
|
|
1246
|
+
}
|
|
1247
|
+
};
|
|
1248
|
+
request.onsuccess = () => {
|
|
1207
1249
|
const db = request.result;
|
|
1208
|
-
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
1209
|
-
|
|
1250
|
+
if (!db || !db.objectStoreNames.contains(STORE_NAME)) {
|
|
1251
|
+
backendType = "localstorage";
|
|
1252
|
+
try {
|
|
1253
|
+
db?.close();
|
|
1254
|
+
} catch {
|
|
1255
|
+
}
|
|
1256
|
+
reject(new Error("queue object store unavailable"));
|
|
1257
|
+
return;
|
|
1210
1258
|
}
|
|
1259
|
+
resolve(db);
|
|
1211
1260
|
};
|
|
1212
|
-
request.onsuccess = () => resolve(request.result);
|
|
1213
1261
|
request.onerror = () => {
|
|
1214
1262
|
backendType = "localstorage";
|
|
1215
1263
|
reject(request.error);
|
|
@@ -1287,7 +1335,10 @@ function createOfflineQueue(config = {}) {
|
|
|
1287
1335
|
}
|
|
1288
1336
|
async function lsEnqueue(report) {
|
|
1289
1337
|
const rows = lsRead();
|
|
1290
|
-
|
|
1338
|
+
const row = await wrapForStorage(report);
|
|
1339
|
+
const idx = rows.findIndex((r) => r.id === report.id);
|
|
1340
|
+
if (idx >= 0) rows[idx] = row;
|
|
1341
|
+
else rows.push(row);
|
|
1291
1342
|
lsWrite(rows);
|
|
1292
1343
|
}
|
|
1293
1344
|
function lsDelete(id) {
|
|
@@ -1326,26 +1377,43 @@ function createOfflineQueue(config = {}) {
|
|
|
1326
1377
|
return false;
|
|
1327
1378
|
}
|
|
1328
1379
|
}
|
|
1329
|
-
async function
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
if (
|
|
1333
|
-
|
|
1334
|
-
|
|
1380
|
+
async function readRows() {
|
|
1381
|
+
const backend = detectBackend();
|
|
1382
|
+
if (backend === "none") return { backend, rows: [] };
|
|
1383
|
+
if (backend === "indexeddb") {
|
|
1384
|
+
try {
|
|
1385
|
+
return { backend, rows: await idbGetAll() };
|
|
1386
|
+
} catch {
|
|
1387
|
+
backendType = "localstorage";
|
|
1388
|
+
}
|
|
1335
1389
|
}
|
|
1390
|
+
return { backend: "localstorage", rows: lsRead() };
|
|
1391
|
+
}
|
|
1392
|
+
async function writeRow(report) {
|
|
1336
1393
|
const backend = detectBackend();
|
|
1394
|
+
if (backend === "none") {
|
|
1395
|
+
queueLog.debug("Offline queue: no storage backend, dropping report", { id: report.id });
|
|
1396
|
+
return backend;
|
|
1397
|
+
}
|
|
1337
1398
|
if (backend === "indexeddb") {
|
|
1338
1399
|
try {
|
|
1339
1400
|
await idbEnqueue(report);
|
|
1340
|
-
return;
|
|
1401
|
+
return backend;
|
|
1341
1402
|
} catch {
|
|
1342
1403
|
backendType = "localstorage";
|
|
1343
1404
|
}
|
|
1344
1405
|
}
|
|
1345
|
-
|
|
1346
|
-
|
|
1406
|
+
await lsEnqueue(report);
|
|
1407
|
+
return "localstorage";
|
|
1408
|
+
}
|
|
1409
|
+
async function enqueue(report) {
|
|
1410
|
+
if (!enabled) return;
|
|
1411
|
+
const currentSize = await size();
|
|
1412
|
+
if (currentSize >= maxQueueSize) {
|
|
1413
|
+
queueLog.warn("Offline queue full \u2014 dropping report", { maxQueueSize });
|
|
1347
1414
|
return;
|
|
1348
1415
|
}
|
|
1416
|
+
await writeRow(report);
|
|
1349
1417
|
}
|
|
1350
1418
|
function getBackoffDelay2(attempt) {
|
|
1351
1419
|
return Math.min(1e3 * 2 ** attempt + Math.random() * 500, MAX_BACKOFF_MS);
|
|
@@ -1353,19 +1421,9 @@ function createOfflineQueue(config = {}) {
|
|
|
1353
1421
|
function sleep2(ms) {
|
|
1354
1422
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1355
1423
|
}
|
|
1356
|
-
async function
|
|
1357
|
-
|
|
1358
|
-
let rows;
|
|
1359
|
-
const backend = detectBackend();
|
|
1360
|
-
if (backend === "indexeddb") {
|
|
1361
|
-
try {
|
|
1362
|
-
rows = await idbGetAll();
|
|
1363
|
-
} catch {
|
|
1364
|
-
rows = lsRead();
|
|
1365
|
-
}
|
|
1366
|
-
} else {
|
|
1367
|
-
rows = lsRead();
|
|
1368
|
-
}
|
|
1424
|
+
async function runFlush(client) {
|
|
1425
|
+
const { backend, rows: storedRows } = await readRows();
|
|
1426
|
+
let rows = storedRows;
|
|
1369
1427
|
const now2 = Date.now();
|
|
1370
1428
|
const fresh = [];
|
|
1371
1429
|
for (const row of rows) {
|
|
@@ -1394,59 +1452,84 @@ function createOfflineQueue(config = {}) {
|
|
|
1394
1452
|
if (result.ok) {
|
|
1395
1453
|
await removeRow(backend, rowId);
|
|
1396
1454
|
sent++;
|
|
1455
|
+
continue;
|
|
1456
|
+
}
|
|
1457
|
+
failed++;
|
|
1458
|
+
if (result.error?.code === "CIRCUIT_OPEN") {
|
|
1459
|
+
queueLog.debug("Offline queue: circuit open, deferring the rest of the batch", {
|
|
1460
|
+
id: rowId,
|
|
1461
|
+
remaining: batch.length - i
|
|
1462
|
+
});
|
|
1463
|
+
break;
|
|
1464
|
+
}
|
|
1465
|
+
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;
|
|
1466
|
+
// retrying re-serialises the multi-MB body every sync tick and wedges
|
|
1467
|
+
// the queue (it matches neither permanent nor transient otherwise).
|
|
1468
|
+
// SERIALIZE_FAILED (circular ref) is likewise unrecoverable on retry.
|
|
1469
|
+
result.error?.code === "PAYLOAD_TOO_LARGE" || result.error?.code === "SERIALIZE_FAILED" || typeof result.error?.message === "string" && /invalid payload|description must be at least|validation/i.test(
|
|
1470
|
+
result.error.message
|
|
1471
|
+
);
|
|
1472
|
+
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"));
|
|
1473
|
+
if (permanent) {
|
|
1474
|
+
await removeRow(backend, rowId);
|
|
1397
1475
|
} else {
|
|
1398
|
-
const
|
|
1399
|
-
|
|
1400
|
-
// the queue (it matches neither permanent nor transient otherwise).
|
|
1401
|
-
// SERIALIZE_FAILED (circular ref) is likewise unrecoverable on retry.
|
|
1402
|
-
result.error?.code === "PAYLOAD_TOO_LARGE" || result.error?.code === "SERIALIZE_FAILED" || typeof result.error?.message === "string" && /invalid payload|description must be at least|validation/i.test(
|
|
1403
|
-
result.error.message
|
|
1404
|
-
);
|
|
1405
|
-
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"));
|
|
1406
|
-
if (permanent) {
|
|
1476
|
+
const nextAttempts = rowAttempts(row) + 1;
|
|
1477
|
+
if (nextAttempts >= MAX_DELIVERY_ATTEMPTS) {
|
|
1407
1478
|
await removeRow(backend, rowId);
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1479
|
+
queueLog.warn("Offline queue: giving up on report after repeated failures", {
|
|
1480
|
+
id: rowId,
|
|
1481
|
+
attempts: nextAttempts,
|
|
1482
|
+
code: result.error?.code
|
|
1483
|
+
});
|
|
1484
|
+
} else {
|
|
1485
|
+
row.attempts = nextAttempts;
|
|
1486
|
+
const persisted = await persistRow(backend, row);
|
|
1487
|
+
if (persisted) {
|
|
1488
|
+
queueLog.debug("Offline queue: delivery failed, will retry", {
|
|
1413
1489
|
id: rowId,
|
|
1414
1490
|
attempts: nextAttempts,
|
|
1415
|
-
code: result.error?.code
|
|
1491
|
+
code: result.error?.code,
|
|
1492
|
+
classified: transient ? "transient" : "unclassified"
|
|
1416
1493
|
});
|
|
1417
1494
|
} else {
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
attempts: nextAttempts,
|
|
1424
|
-
code: result.error?.code
|
|
1425
|
-
});
|
|
1426
|
-
} else {
|
|
1427
|
-
await removeRow(backend, rowId);
|
|
1428
|
-
queueLog.warn("Offline queue: dropping report (attempt counter unpersistable)", {
|
|
1429
|
-
id: rowId,
|
|
1430
|
-
code: result.error?.code
|
|
1431
|
-
});
|
|
1432
|
-
}
|
|
1495
|
+
await removeRow(backend, rowId);
|
|
1496
|
+
queueLog.warn("Offline queue: dropping report (attempt counter unpersistable)", {
|
|
1497
|
+
id: rowId,
|
|
1498
|
+
code: result.error?.code
|
|
1499
|
+
});
|
|
1433
1500
|
}
|
|
1434
1501
|
}
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
}
|
|
1502
|
+
}
|
|
1503
|
+
if (i < batch.length - 1) {
|
|
1504
|
+
await sleep2(getBackoffDelay2(i));
|
|
1439
1505
|
}
|
|
1440
1506
|
}
|
|
1441
1507
|
return { sent, failed };
|
|
1442
1508
|
}
|
|
1509
|
+
function flush(client) {
|
|
1510
|
+
if (!enabled) return Promise.resolve({ sent: 0, failed: 0 });
|
|
1511
|
+
if (inFlight) {
|
|
1512
|
+
if (!followUp) {
|
|
1513
|
+
followUp = inFlight.catch(() => void 0).then(() => {
|
|
1514
|
+
followUp = null;
|
|
1515
|
+
return flush(client);
|
|
1516
|
+
});
|
|
1517
|
+
}
|
|
1518
|
+
return followUp;
|
|
1519
|
+
}
|
|
1520
|
+
const pass = runFlush(client).finally(() => {
|
|
1521
|
+
if (inFlight === pass) inFlight = null;
|
|
1522
|
+
});
|
|
1523
|
+
inFlight = pass;
|
|
1524
|
+
return pass;
|
|
1525
|
+
}
|
|
1443
1526
|
async function size() {
|
|
1444
1527
|
const backend = detectBackend();
|
|
1445
1528
|
if (backend === "indexeddb") {
|
|
1446
1529
|
try {
|
|
1447
1530
|
return await idbSize();
|
|
1448
1531
|
} catch {
|
|
1449
|
-
|
|
1532
|
+
backendType = "localstorage";
|
|
1450
1533
|
}
|
|
1451
1534
|
}
|
|
1452
1535
|
return lsRead().length;
|