@loggerjs/browser 0.5.1 → 0.5.3
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 +9 -2
- package/dist/index.cjs +478 -29
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +478 -29
- package/dist/index.js.map +1 -1
- package/dist/indexeddb-transport.cjs +366 -15
- package/dist/indexeddb-transport.cjs.map +1 -1
- package/dist/indexeddb-transport.d.ts +41 -0
- package/dist/indexeddb-transport.d.ts.map +1 -1
- package/dist/indexeddb-transport.js +367 -16
- package/dist/indexeddb-transport.js.map +1 -1
- package/dist/offline-first-transport.cjs +369 -16
- package/dist/offline-first-transport.cjs.map +1 -1
- package/dist/offline-first-transport.d.ts.map +1 -1
- package/dist/offline-first-transport.js +370 -17
- package/dist/offline-first-transport.js.map +1 -1
- package/dist/zip-export.cjs +109 -13
- package/dist/zip-export.cjs.map +1 -1
- package/dist/zip-export.d.ts +24 -0
- package/dist/zip-export.d.ts.map +1 -1
- package/dist/zip-export.js +109 -13
- package/dist/zip-export.js.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -894,12 +894,96 @@ function indexedDbBrowserHttpOfflineQueue(options = {}) {
|
|
|
894
894
|
//#region src/indexeddb-transport.ts
|
|
895
895
|
const DEFAULT_DB_NAME = "loggerjs";
|
|
896
896
|
const DEFAULT_STORE_NAME = "logs";
|
|
897
|
-
const
|
|
897
|
+
const DEFAULT_SESSION_CONTEXT_KEY$1 = "sessionId";
|
|
898
|
+
const DEFAULT_SPILL_MAX_ENTRIES = 200;
|
|
899
|
+
const DEFAULT_SPILL_MAX_BYTES = 512 * 1024;
|
|
900
|
+
const DEFAULT_SPILL_MAX_ORIGIN_BYTES = 5 * 1024 * 1024;
|
|
901
|
+
const DB_VERSION = 3;
|
|
898
902
|
const ORDER_INDEX_NAME = "createdAtSeq";
|
|
903
|
+
const SESSION_INDEX_NAME = "sessionId";
|
|
904
|
+
const SESSION_ORDER_INDEX_NAME = "sessionIdCreatedAtSeq";
|
|
905
|
+
const SPILL_SCHEMA = "loggerjs.indexeddb-spill.v1";
|
|
899
906
|
function normalizePositiveInteger(value, fallback) {
|
|
900
907
|
if (value === void 0 || !Number.isFinite(value)) return fallback;
|
|
901
908
|
return Math.max(0, Math.floor(value));
|
|
902
909
|
}
|
|
910
|
+
let fallbackSessionSeq = 0;
|
|
911
|
+
function defaultPageSessionId() {
|
|
912
|
+
const randomUUID = globalThis.crypto?.randomUUID;
|
|
913
|
+
if (randomUUID) return randomUUID.call(globalThis.crypto);
|
|
914
|
+
return `${Date.now().toString(36)}-${(fallbackSessionSeq++).toString(36)}`;
|
|
915
|
+
}
|
|
916
|
+
function normalizeSessionId(value) {
|
|
917
|
+
if (typeof value !== "string") return void 0;
|
|
918
|
+
const normalized = value.trim();
|
|
919
|
+
return normalized.length > 0 ? normalized : void 0;
|
|
920
|
+
}
|
|
921
|
+
function normalizeSessionOptions(session) {
|
|
922
|
+
if (session === false) return void 0;
|
|
923
|
+
if (typeof session === "string") return {
|
|
924
|
+
contextKey: DEFAULT_SESSION_CONTEXT_KEY$1,
|
|
925
|
+
id: normalizeSessionId(session) ?? defaultPageSessionId()
|
|
926
|
+
};
|
|
927
|
+
return {
|
|
928
|
+
contextKey: session?.contextKey ?? DEFAULT_SESSION_CONTEXT_KEY$1,
|
|
929
|
+
getId: session?.getId,
|
|
930
|
+
id: normalizeSessionId(session?.id) ?? defaultPageSessionId()
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
function resolveLocalStorage(storage) {
|
|
934
|
+
if (storage) return storage;
|
|
935
|
+
try {
|
|
936
|
+
return globalThis.localStorage;
|
|
937
|
+
} catch {
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
function normalizeLocalStorageSpillOptions(spill, dbName, storeName) {
|
|
942
|
+
if (!spill) return void 0;
|
|
943
|
+
const storage = resolveLocalStorage(spill.storage);
|
|
944
|
+
if (!storage) return void 0;
|
|
945
|
+
const namespace = spill.namespace ?? `${dbName}:${storeName}`;
|
|
946
|
+
return {
|
|
947
|
+
drainOnCreate: spill.drainOnCreate ?? true,
|
|
948
|
+
key: `loggerjs:spill:v1:${namespace}`,
|
|
949
|
+
maxBytes: normalizePositiveInteger(spill.maxBytes, DEFAULT_SPILL_MAX_BYTES),
|
|
950
|
+
maxEntries: normalizePositiveInteger(spill.maxEntries, DEFAULT_SPILL_MAX_ENTRIES),
|
|
951
|
+
maxOriginBytes: normalizePositiveInteger(spill.maxOriginBytes, DEFAULT_SPILL_MAX_ORIGIN_BYTES),
|
|
952
|
+
minLevelValue: spill.minLevel === void 0 ? void 0 : (0, _loggerjs_core.toLevelValue)(spill.minLevel),
|
|
953
|
+
onDrop: spill.onDrop,
|
|
954
|
+
spillOnPageHide: spill.spillOnPageHide ?? true,
|
|
955
|
+
storage
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
function storageByteLength(key, value) {
|
|
959
|
+
return 2 * (key.length + value.length);
|
|
960
|
+
}
|
|
961
|
+
function estimateLocalStorageBytes(storage) {
|
|
962
|
+
try {
|
|
963
|
+
let total = 0;
|
|
964
|
+
for (let index = 0; index < storage.length; index += 1) {
|
|
965
|
+
const key = storage.key(index);
|
|
966
|
+
if (key === null) continue;
|
|
967
|
+
total += storageByteLength(key, storage.getItem(key) ?? "");
|
|
968
|
+
}
|
|
969
|
+
return total;
|
|
970
|
+
} catch {
|
|
971
|
+
return;
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
function isQuotaExceededError(error) {
|
|
975
|
+
return typeof DOMException !== "undefined" && error instanceof DOMException && (error.name === "QuotaExceededError" || error.name === "NS_ERROR_DOM_QUOTA_REACHED");
|
|
976
|
+
}
|
|
977
|
+
function isSpillLogEvent(value) {
|
|
978
|
+
if (!value || typeof value !== "object") return false;
|
|
979
|
+
const event = value;
|
|
980
|
+
return typeof event.id === "string" && typeof event.time === "number" && typeof event.seq === "number" && typeof event.level === "number" && typeof event.levelName === "string" && typeof event.logger === "string" && typeof event.message === "string";
|
|
981
|
+
}
|
|
982
|
+
function parseSpillPayload(raw) {
|
|
983
|
+
const parsed = JSON.parse(raw);
|
|
984
|
+
if (parsed.schema !== SPILL_SCHEMA || !Array.isArray(parsed.entries)) return void 0;
|
|
985
|
+
return parsed.entries.filter(isSpillLogEvent);
|
|
986
|
+
}
|
|
903
987
|
function nowMs$1() {
|
|
904
988
|
return typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
|
|
905
989
|
}
|
|
@@ -965,6 +1049,7 @@ function decodeEntry(entry, codec) {
|
|
|
965
1049
|
function shouldKeepEntry(entry, options) {
|
|
966
1050
|
if (options.from !== void 0 && entry.createdAt < options.from) return false;
|
|
967
1051
|
if (options.to !== void 0 && entry.createdAt > options.to) return false;
|
|
1052
|
+
if (options.sessionId !== void 0 && entry.sessionId !== options.sessionId) return false;
|
|
968
1053
|
if (options.minLevel !== void 0 && entry.level < (0, _loggerjs_core.toLevelValue)(options.minLevel)) return false;
|
|
969
1054
|
if (options.logger !== void 0 && entry.logger !== options.logger) return false;
|
|
970
1055
|
if (options.type !== void 0 && entry.type !== options.type) return false;
|
|
@@ -972,6 +1057,21 @@ function shouldKeepEntry(entry, options) {
|
|
|
972
1057
|
}
|
|
973
1058
|
function orderRangeForQuery(options) {
|
|
974
1059
|
if (typeof IDBKeyRange === "undefined") return void 0;
|
|
1060
|
+
if (options.sessionId !== void 0) {
|
|
1061
|
+
const lower = [
|
|
1062
|
+
options.sessionId,
|
|
1063
|
+
options.from ?? Number.MIN_SAFE_INTEGER,
|
|
1064
|
+
Number.MIN_SAFE_INTEGER,
|
|
1065
|
+
""
|
|
1066
|
+
];
|
|
1067
|
+
const upper = [
|
|
1068
|
+
options.sessionId,
|
|
1069
|
+
options.to ?? Number.MAX_SAFE_INTEGER,
|
|
1070
|
+
Number.MAX_SAFE_INTEGER,
|
|
1071
|
+
""
|
|
1072
|
+
];
|
|
1073
|
+
return IDBKeyRange.bound(lower, upper);
|
|
1074
|
+
}
|
|
975
1075
|
const lower = options.from === void 0 ? void 0 : [
|
|
976
1076
|
options.from,
|
|
977
1077
|
Number.MIN_SAFE_INTEGER,
|
|
@@ -1020,12 +1120,17 @@ function indexedDbTransport(options = {}) {
|
|
|
1020
1120
|
const flushOnPageHide = options.flushOnPageHide ?? true;
|
|
1021
1121
|
const durability = options.durability ?? "default";
|
|
1022
1122
|
const rootIndexedDB = options.indexedDB ?? globalThis.indexedDB;
|
|
1123
|
+
const sessionOptions = normalizeSessionOptions(options.session);
|
|
1124
|
+
const spillOptions = normalizeLocalStorageSpillOptions(options.localStorageSpill, dbName, storeName);
|
|
1023
1125
|
const transactionOptions = durability === "default" ? void 0 : { durability };
|
|
1024
1126
|
const buffer = [];
|
|
1025
1127
|
let dbPromise;
|
|
1026
1128
|
let indexedDBFactoryPromise;
|
|
1027
1129
|
let timer;
|
|
1028
1130
|
let flushPromise;
|
|
1131
|
+
let pendingFlushBatch;
|
|
1132
|
+
let spillDrainPromise;
|
|
1133
|
+
let localStorageSpillClearCandidate;
|
|
1029
1134
|
let lastContext;
|
|
1030
1135
|
let closed = false;
|
|
1031
1136
|
const statsState = {
|
|
@@ -1052,13 +1157,23 @@ function indexedDbTransport(options = {}) {
|
|
|
1052
1157
|
storageBucketFallbacks: 0,
|
|
1053
1158
|
transactionOptionFallbacks: 0,
|
|
1054
1159
|
errors: 0,
|
|
1055
|
-
errorsByOperation: {}
|
|
1160
|
+
errorsByOperation: {},
|
|
1161
|
+
localStorageSpillWrites: 0,
|
|
1162
|
+
localStorageSpillEntries: 0,
|
|
1163
|
+
localStorageSpillDrains: 0,
|
|
1164
|
+
localStorageSpillDrainedEntries: 0,
|
|
1165
|
+
localStorageSpillDropped: 0,
|
|
1166
|
+
localStorageSpillDroppedByReason: {},
|
|
1167
|
+
localStorageSpillErrors: 0,
|
|
1168
|
+
localStorageSpillErrorsByOperation: {}
|
|
1056
1169
|
};
|
|
1057
1170
|
const snapshotStats = () => ({
|
|
1058
1171
|
...statsState,
|
|
1059
1172
|
bufferDepth: buffer.length,
|
|
1060
1173
|
droppedByReason: { ...statsState.droppedByReason },
|
|
1061
1174
|
errorsByOperation: { ...statsState.errorsByOperation },
|
|
1175
|
+
localStorageSpillDroppedByReason: { ...statsState.localStorageSpillDroppedByReason },
|
|
1176
|
+
localStorageSpillErrorsByOperation: { ...statsState.localStorageSpillErrorsByOperation },
|
|
1062
1177
|
pendingFlush: flushPromise !== void 0,
|
|
1063
1178
|
persistedDroppedByReason: { ...statsState.persistedDroppedByReason }
|
|
1064
1179
|
});
|
|
@@ -1085,6 +1200,18 @@ function indexedDbTransport(options = {}) {
|
|
|
1085
1200
|
(0, _loggerjs_core.incrementLoggerMetaCounter)(`transport.indexeddb.persisted.dropped.${reason}`);
|
|
1086
1201
|
options.onPersistedDrop?.(entry, reason);
|
|
1087
1202
|
};
|
|
1203
|
+
const reportSpillError = (error, operation) => {
|
|
1204
|
+
statsState.localStorageSpillErrors += 1;
|
|
1205
|
+
incrementRecord(statsState.localStorageSpillErrorsByOperation, operation);
|
|
1206
|
+
reportInternalError(error, `localstorage-spill-${operation}`);
|
|
1207
|
+
};
|
|
1208
|
+
const dropSpillEvent = (event, reason) => {
|
|
1209
|
+
statsState.localStorageSpillDropped += 1;
|
|
1210
|
+
incrementRecord(statsState.localStorageSpillDroppedByReason, reason);
|
|
1211
|
+
(0, _loggerjs_core.incrementLoggerMetaCounter)("transport.indexeddb.localstorage_spill.dropped");
|
|
1212
|
+
(0, _loggerjs_core.incrementLoggerMetaCounter)(`transport.indexeddb.localstorage_spill.dropped.${reason}`);
|
|
1213
|
+
spillOptions?.onDrop?.(event, reason);
|
|
1214
|
+
};
|
|
1088
1215
|
const resolveIndexedDBFactory = async () => {
|
|
1089
1216
|
if (options.indexedDB || !options.storageBucketName) return rootIndexedDB;
|
|
1090
1217
|
indexedDBFactoryPromise ??= (async () => {
|
|
@@ -1124,6 +1251,13 @@ function indexedDbTransport(options = {}) {
|
|
|
1124
1251
|
"seq",
|
|
1125
1252
|
"id"
|
|
1126
1253
|
]);
|
|
1254
|
+
ensureIndex(store, SESSION_INDEX_NAME, "sessionId");
|
|
1255
|
+
ensureIndex(store, SESSION_ORDER_INDEX_NAME, [
|
|
1256
|
+
"sessionId",
|
|
1257
|
+
"createdAt",
|
|
1258
|
+
"seq",
|
|
1259
|
+
"id"
|
|
1260
|
+
]);
|
|
1127
1261
|
});
|
|
1128
1262
|
request.addEventListener("success", () => {
|
|
1129
1263
|
statsState.databaseOpenCount += 1;
|
|
@@ -1214,7 +1348,7 @@ function indexedDbTransport(options = {}) {
|
|
|
1214
1348
|
return total;
|
|
1215
1349
|
});
|
|
1216
1350
|
const queryEntriesByCursor = async (queryOptions) => withStore("readonly", async (store) => {
|
|
1217
|
-
const index = getStoreIndex(store, ORDER_INDEX_NAME);
|
|
1351
|
+
const index = getStoreIndex(store, queryOptions.sessionId === void 0 ? ORDER_INDEX_NAME : SESSION_ORDER_INDEX_NAME);
|
|
1218
1352
|
if (!index) return void 0;
|
|
1219
1353
|
const entries = [];
|
|
1220
1354
|
const limit = queryOptions.limit;
|
|
@@ -1227,6 +1361,31 @@ function indexedDbTransport(options = {}) {
|
|
|
1227
1361
|
});
|
|
1228
1362
|
return entries;
|
|
1229
1363
|
});
|
|
1364
|
+
const listSessions = async (sessionQueryOptions = {}) => {
|
|
1365
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
1366
|
+
for (const entry of await getEntries()) {
|
|
1367
|
+
if (!entry.sessionId) continue;
|
|
1368
|
+
const current = sessions.get(entry.sessionId);
|
|
1369
|
+
if (current) {
|
|
1370
|
+
current.firstSeen = Math.min(current.firstSeen, entry.createdAt);
|
|
1371
|
+
current.lastSeen = Math.max(current.lastSeen, entry.createdAt);
|
|
1372
|
+
current.count += 1;
|
|
1373
|
+
current.byteLength += entry.byteLength;
|
|
1374
|
+
} else sessions.set(entry.sessionId, {
|
|
1375
|
+
byteLength: entry.byteLength,
|
|
1376
|
+
count: 1,
|
|
1377
|
+
firstSeen: entry.createdAt,
|
|
1378
|
+
lastSeen: entry.createdAt,
|
|
1379
|
+
sessionId: entry.sessionId
|
|
1380
|
+
});
|
|
1381
|
+
}
|
|
1382
|
+
const ordered = [...sessions.values()].sort((left, right) => {
|
|
1383
|
+
if (left.lastSeen !== right.lastSeen) return left.lastSeen - right.lastSeen;
|
|
1384
|
+
return left.sessionId < right.sessionId ? -1 : left.sessionId > right.sessionId ? 1 : 0;
|
|
1385
|
+
});
|
|
1386
|
+
if (sessionQueryOptions.order !== "asc") ordered.reverse();
|
|
1387
|
+
return ordered.slice(0, sessionQueryOptions.limit);
|
|
1388
|
+
};
|
|
1230
1389
|
const pruneWithEntries = async () => {
|
|
1231
1390
|
const now = Date.now();
|
|
1232
1391
|
let entries = await getEntries();
|
|
@@ -1282,10 +1441,34 @@ function indexedDbTransport(options = {}) {
|
|
|
1282
1441
|
statsState.lastPruneDurationMs = nowMs$1() - startedAt;
|
|
1283
1442
|
}
|
|
1284
1443
|
};
|
|
1444
|
+
const sessionIdForEvent = (event) => {
|
|
1445
|
+
if (!sessionOptions) return void 0;
|
|
1446
|
+
return normalizeSessionId(sessionOptions.getId?.(event)) ?? normalizeSessionId(event.context?.[sessionOptions.contextKey]) ?? sessionOptions.id;
|
|
1447
|
+
};
|
|
1448
|
+
const eventWithSessionContext = (event, sessionId) => {
|
|
1449
|
+
if (!sessionId || !sessionOptions?.contextKey) return event;
|
|
1450
|
+
if (event.context?.[sessionOptions.contextKey] === sessionId) return event;
|
|
1451
|
+
return {
|
|
1452
|
+
...event,
|
|
1453
|
+
context: {
|
|
1454
|
+
...event.context,
|
|
1455
|
+
[sessionOptions.contextKey]: sessionId
|
|
1456
|
+
}
|
|
1457
|
+
};
|
|
1458
|
+
};
|
|
1459
|
+
const eventWithResolvedSession = (event) => {
|
|
1460
|
+
const sessionId = sessionIdForEvent(event);
|
|
1461
|
+
return {
|
|
1462
|
+
event: eventWithSessionContext(event, sessionId),
|
|
1463
|
+
sessionId
|
|
1464
|
+
};
|
|
1465
|
+
};
|
|
1285
1466
|
const eventToEntry = (event) => {
|
|
1286
|
-
const
|
|
1467
|
+
const resolved = eventWithResolvedSession(event);
|
|
1468
|
+
const payload = codec.encode(resolved.event);
|
|
1287
1469
|
return {
|
|
1288
1470
|
id: event.id,
|
|
1471
|
+
sessionId: resolved.sessionId,
|
|
1289
1472
|
seq: event.seq,
|
|
1290
1473
|
createdAt: event.time,
|
|
1291
1474
|
level: event.level,
|
|
@@ -1296,6 +1479,30 @@ function indexedDbTransport(options = {}) {
|
|
|
1296
1479
|
payload
|
|
1297
1480
|
};
|
|
1298
1481
|
};
|
|
1482
|
+
const clearLocalStorageSpill = (expectedValue) => {
|
|
1483
|
+
if (!spillOptions) return false;
|
|
1484
|
+
try {
|
|
1485
|
+
if (expectedValue !== void 0 && spillOptions.storage.getItem(spillOptions.key) !== expectedValue) return false;
|
|
1486
|
+
spillOptions.storage.removeItem(spillOptions.key);
|
|
1487
|
+
if (expectedValue === void 0 || localStorageSpillClearCandidate?.value === expectedValue) localStorageSpillClearCandidate = void 0;
|
|
1488
|
+
return true;
|
|
1489
|
+
} catch (error) {
|
|
1490
|
+
reportSpillError(error, "clear");
|
|
1491
|
+
return false;
|
|
1492
|
+
}
|
|
1493
|
+
};
|
|
1494
|
+
const rememberLocalStorageSpill = (value, events) => {
|
|
1495
|
+
localStorageSpillClearCandidate = {
|
|
1496
|
+
pendingIds: new Set(events.map((event) => event.id)),
|
|
1497
|
+
value
|
|
1498
|
+
};
|
|
1499
|
+
};
|
|
1500
|
+
const markLocalStorageSpillPersisted = (events) => {
|
|
1501
|
+
const candidate = localStorageSpillClearCandidate;
|
|
1502
|
+
if (!candidate || events.length === 0) return;
|
|
1503
|
+
for (const event of events) candidate.pendingIds.delete(event.id);
|
|
1504
|
+
if (candidate.pendingIds.size === 0) clearLocalStorageSpill(candidate.value);
|
|
1505
|
+
};
|
|
1299
1506
|
const writeBatch = async (events) => {
|
|
1300
1507
|
if (events.length === 0) return;
|
|
1301
1508
|
const startedAt = nowMs$1();
|
|
@@ -1307,6 +1514,7 @@ function indexedDbTransport(options = {}) {
|
|
|
1307
1514
|
});
|
|
1308
1515
|
statsState.persisted += entries.length;
|
|
1309
1516
|
(0, _loggerjs_core.incrementLoggerMetaCounter)("transport.indexeddb.persisted", entries.length);
|
|
1517
|
+
markLocalStorageSpillPersisted(events);
|
|
1310
1518
|
await prune();
|
|
1311
1519
|
statsState.flushes += 1;
|
|
1312
1520
|
} catch (error) {
|
|
@@ -1316,6 +1524,124 @@ function indexedDbTransport(options = {}) {
|
|
|
1316
1524
|
statsState.lastFlushDurationMs = nowMs$1() - startedAt;
|
|
1317
1525
|
}
|
|
1318
1526
|
};
|
|
1527
|
+
const stringifySpillPayload = (events) => (0, _loggerjs_core.safeJsonStringify)({
|
|
1528
|
+
createdAt: Date.now(),
|
|
1529
|
+
entries: events,
|
|
1530
|
+
schema: SPILL_SCHEMA
|
|
1531
|
+
}, { maxArrayLength: spillOptions?.maxEntries ?? DEFAULT_SPILL_MAX_ENTRIES });
|
|
1532
|
+
const spillStorageBudget = (currentValue) => {
|
|
1533
|
+
if (!spillOptions) return 0;
|
|
1534
|
+
const ownBytes = currentValue ? storageByteLength(spillOptions.key, currentValue) : 0;
|
|
1535
|
+
const originBytes = estimateLocalStorageBytes(spillOptions.storage);
|
|
1536
|
+
if (originBytes === void 0) return spillOptions.maxBytes;
|
|
1537
|
+
return Math.max(0, Math.min(spillOptions.maxBytes, spillOptions.maxOriginBytes - originBytes + ownBytes));
|
|
1538
|
+
};
|
|
1539
|
+
const pendingEventsForSpill = () => {
|
|
1540
|
+
const events = [...pendingFlushBatch ?? [], ...buffer];
|
|
1541
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1542
|
+
const deduped = [];
|
|
1543
|
+
for (const event of events) {
|
|
1544
|
+
if (seen.has(event.id)) continue;
|
|
1545
|
+
seen.add(event.id);
|
|
1546
|
+
if (spillOptions?.minLevelValue !== void 0 && event.level < spillOptions.minLevelValue) continue;
|
|
1547
|
+
deduped.push(eventWithResolvedSession(event).event);
|
|
1548
|
+
}
|
|
1549
|
+
return deduped;
|
|
1550
|
+
};
|
|
1551
|
+
const trimSpillEventsToBudget = (events, budget) => {
|
|
1552
|
+
if (!spillOptions) return void 0;
|
|
1553
|
+
while (events.length > spillOptions.maxEntries) {
|
|
1554
|
+
const dropped = events.shift();
|
|
1555
|
+
if (dropped) dropSpillEvent(dropped, "max-entries");
|
|
1556
|
+
}
|
|
1557
|
+
while (events.length > 0) {
|
|
1558
|
+
const value = stringifySpillPayload(events);
|
|
1559
|
+
if (storageByteLength(spillOptions.key, value) <= budget) return value;
|
|
1560
|
+
const dropped = events.shift();
|
|
1561
|
+
if (dropped) dropSpillEvent(dropped, "max-bytes");
|
|
1562
|
+
}
|
|
1563
|
+
};
|
|
1564
|
+
const writeLocalStorageSpill = () => {
|
|
1565
|
+
if (!spillOptions) return;
|
|
1566
|
+
const pendingEvents = pendingEventsForSpill();
|
|
1567
|
+
if (pendingEvents.length === 0) return;
|
|
1568
|
+
try {
|
|
1569
|
+
const currentValue = spillOptions.storage.getItem(spillOptions.key);
|
|
1570
|
+
let events = pendingEvents;
|
|
1571
|
+
if (currentValue) {
|
|
1572
|
+
const merged = /* @__PURE__ */ new Map();
|
|
1573
|
+
try {
|
|
1574
|
+
for (const event of parseSpillPayload(currentValue) ?? []) merged.set(event.id, event);
|
|
1575
|
+
} catch {}
|
|
1576
|
+
for (const event of pendingEvents) merged.set(event.id, event);
|
|
1577
|
+
events = [...merged.values()];
|
|
1578
|
+
}
|
|
1579
|
+
const budget = spillStorageBudget(currentValue);
|
|
1580
|
+
const value = trimSpillEventsToBudget(events, budget);
|
|
1581
|
+
if (!value) return;
|
|
1582
|
+
try {
|
|
1583
|
+
spillOptions.storage.setItem(spillOptions.key, value);
|
|
1584
|
+
} catch (error) {
|
|
1585
|
+
if (!isQuotaExceededError(error)) throw error;
|
|
1586
|
+
while (events.length > 0) {
|
|
1587
|
+
const dropped = events.shift();
|
|
1588
|
+
if (dropped) dropSpillEvent(dropped, "quota");
|
|
1589
|
+
const retryValue = trimSpillEventsToBudget(events, budget);
|
|
1590
|
+
if (!retryValue) return;
|
|
1591
|
+
try {
|
|
1592
|
+
spillOptions.storage.setItem(spillOptions.key, retryValue);
|
|
1593
|
+
rememberLocalStorageSpill(retryValue, events);
|
|
1594
|
+
statsState.localStorageSpillWrites += 1;
|
|
1595
|
+
statsState.localStorageSpillEntries += events.length;
|
|
1596
|
+
return;
|
|
1597
|
+
} catch (retryError) {
|
|
1598
|
+
if (!isQuotaExceededError(retryError)) throw retryError;
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
return;
|
|
1602
|
+
}
|
|
1603
|
+
rememberLocalStorageSpill(value, events);
|
|
1604
|
+
statsState.localStorageSpillWrites += 1;
|
|
1605
|
+
statsState.localStorageSpillEntries += events.length;
|
|
1606
|
+
} catch (error) {
|
|
1607
|
+
reportSpillError(error, "write");
|
|
1608
|
+
}
|
|
1609
|
+
};
|
|
1610
|
+
const drainLocalStorageSpill = async () => {
|
|
1611
|
+
if (!spillOptions) return;
|
|
1612
|
+
let raw;
|
|
1613
|
+
try {
|
|
1614
|
+
raw = spillOptions.storage.getItem(spillOptions.key);
|
|
1615
|
+
} catch (error) {
|
|
1616
|
+
reportSpillError(error, "read");
|
|
1617
|
+
return;
|
|
1618
|
+
}
|
|
1619
|
+
if (!raw) return;
|
|
1620
|
+
let events;
|
|
1621
|
+
try {
|
|
1622
|
+
events = parseSpillPayload(raw);
|
|
1623
|
+
} catch (error) {
|
|
1624
|
+
reportSpillError(error, "parse");
|
|
1625
|
+
clearLocalStorageSpill();
|
|
1626
|
+
return;
|
|
1627
|
+
}
|
|
1628
|
+
if (!events || events.length === 0) {
|
|
1629
|
+
clearLocalStorageSpill();
|
|
1630
|
+
return;
|
|
1631
|
+
}
|
|
1632
|
+
await writeBatch(events);
|
|
1633
|
+
clearLocalStorageSpill(raw);
|
|
1634
|
+
statsState.localStorageSpillDrains += 1;
|
|
1635
|
+
statsState.localStorageSpillDrainedEntries += events.length;
|
|
1636
|
+
};
|
|
1637
|
+
const ensureSpillDrained = async () => {
|
|
1638
|
+
const promise = spillDrainPromise;
|
|
1639
|
+
if (!promise) return;
|
|
1640
|
+
await promise;
|
|
1641
|
+
};
|
|
1642
|
+
if (spillOptions?.drainOnCreate) spillDrainPromise = drainLocalStorageSpill().catch((error) => reportSpillError(error, "drain")).finally(() => {
|
|
1643
|
+
spillDrainPromise = void 0;
|
|
1644
|
+
});
|
|
1319
1645
|
const clearTimer = () => {
|
|
1320
1646
|
if (timer) clearTimeout(timer);
|
|
1321
1647
|
timer = void 0;
|
|
@@ -1323,7 +1649,13 @@ function indexedDbTransport(options = {}) {
|
|
|
1323
1649
|
const flushPending = async () => {
|
|
1324
1650
|
if (flushPromise) return flushPromise;
|
|
1325
1651
|
clearTimer();
|
|
1326
|
-
|
|
1652
|
+
const batch = buffer.splice(0, buffer.length);
|
|
1653
|
+
pendingFlushBatch = batch;
|
|
1654
|
+
flushPromise = (async () => {
|
|
1655
|
+
await ensureSpillDrained();
|
|
1656
|
+
await writeBatch(batch);
|
|
1657
|
+
})().finally(() => {
|
|
1658
|
+
if (pendingFlushBatch === batch) pendingFlushBatch = void 0;
|
|
1327
1659
|
flushPromise = void 0;
|
|
1328
1660
|
if (buffer.length > 0 && !closed) schedule();
|
|
1329
1661
|
});
|
|
@@ -1336,14 +1668,28 @@ function indexedDbTransport(options = {}) {
|
|
|
1336
1668
|
}, flushIntervalMs);
|
|
1337
1669
|
};
|
|
1338
1670
|
const onPageHide = () => {
|
|
1339
|
-
|
|
1671
|
+
if (spillOptions?.spillOnPageHide) writeLocalStorageSpill();
|
|
1672
|
+
if (flushOnPageHide) flushPending().catch((error) => reportInternalError(error, "pagehide-flush"));
|
|
1340
1673
|
};
|
|
1341
1674
|
const onVisibilityChange = () => {
|
|
1342
|
-
if (typeof document !== "undefined" && document.visibilityState === "hidden")
|
|
1675
|
+
if (typeof document !== "undefined" && document.visibilityState === "hidden") {
|
|
1676
|
+
if (spillOptions?.spillOnPageHide) writeLocalStorageSpill();
|
|
1677
|
+
if (flushOnPageHide) flushPending().catch((error) => reportInternalError(error, "visibility-flush"));
|
|
1678
|
+
}
|
|
1679
|
+
};
|
|
1680
|
+
const addVisibilityChangeListener = () => {
|
|
1681
|
+
if (typeof document !== "undefined" && typeof document.addEventListener === "function") {
|
|
1682
|
+
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
1683
|
+
return () => document.removeEventListener("visibilitychange", onVisibilityChange);
|
|
1684
|
+
}
|
|
1685
|
+
if (typeof globalThis.addEventListener !== "function") return void 0;
|
|
1686
|
+
globalThis.addEventListener("visibilitychange", onVisibilityChange);
|
|
1687
|
+
return () => globalThis.removeEventListener?.("visibilitychange", onVisibilityChange);
|
|
1343
1688
|
};
|
|
1344
|
-
|
|
1689
|
+
let removeVisibilityChangeListener;
|
|
1690
|
+
if (flushOnPageHide || spillOptions?.spillOnPageHide) {
|
|
1345
1691
|
globalThis.addEventListener?.("pagehide", onPageHide);
|
|
1346
|
-
|
|
1692
|
+
removeVisibilityChangeListener = addVisibilityChangeListener();
|
|
1347
1693
|
}
|
|
1348
1694
|
return {
|
|
1349
1695
|
name: options.name ?? "indexeddb",
|
|
@@ -1376,6 +1722,7 @@ function indexedDbTransport(options = {}) {
|
|
|
1376
1722
|
async clear() {
|
|
1377
1723
|
buffer.length = 0;
|
|
1378
1724
|
clearTimer();
|
|
1725
|
+
clearLocalStorageSpill();
|
|
1379
1726
|
await withStore("readwrite", (store) => requestToPromise(store.clear()));
|
|
1380
1727
|
},
|
|
1381
1728
|
async remove(ids) {
|
|
@@ -1399,6 +1746,10 @@ function indexedDbTransport(options = {}) {
|
|
|
1399
1746
|
if (event) yield event;
|
|
1400
1747
|
}
|
|
1401
1748
|
},
|
|
1749
|
+
async sessions(sessionQueryOptions = {}) {
|
|
1750
|
+
await flushPending();
|
|
1751
|
+
return listSessions(sessionQueryOptions);
|
|
1752
|
+
},
|
|
1402
1753
|
stats() {
|
|
1403
1754
|
return snapshotStats();
|
|
1404
1755
|
},
|
|
@@ -1406,12 +1757,12 @@ function indexedDbTransport(options = {}) {
|
|
|
1406
1757
|
closed = true;
|
|
1407
1758
|
clearTimer();
|
|
1408
1759
|
globalThis.removeEventListener?.("pagehide", onPageHide);
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
promise?.then((db) => db.close());
|
|
1760
|
+
removeVisibilityChangeListener?.();
|
|
1761
|
+
removeVisibilityChangeListener = void 0;
|
|
1762
|
+
return flushPending().finally(() => {
|
|
1763
|
+
const promise = dbPromise;
|
|
1764
|
+
dbPromise = void 0;
|
|
1765
|
+
return promise?.then((db) => db.close());
|
|
1415
1766
|
});
|
|
1416
1767
|
}
|
|
1417
1768
|
};
|
|
@@ -1438,12 +1789,14 @@ function isBrowserOnline(options) {
|
|
|
1438
1789
|
function offlineFirstTransport(remote, options = {}) {
|
|
1439
1790
|
const name = options.name ?? `offline-first(${remote.name ?? "transport"})`;
|
|
1440
1791
|
const replayBatchSize = normalizeBatchSize(options.replayBatchSize);
|
|
1792
|
+
const queueOptions = options.queueOptions;
|
|
1441
1793
|
const queue = options.queue ?? indexedDbTransport({
|
|
1442
1794
|
dbName: "loggerjs-offline",
|
|
1443
1795
|
flushIntervalMs: 0,
|
|
1444
1796
|
name: `${name}:queue`,
|
|
1445
1797
|
storeName: "offline-events",
|
|
1446
|
-
...
|
|
1798
|
+
...queueOptions,
|
|
1799
|
+
session: queueOptions?.session ?? false
|
|
1447
1800
|
});
|
|
1448
1801
|
const reliableRemote = (0, _loggerjs_core.retryTransport)(remote, {
|
|
1449
1802
|
maxRetries: 2,
|
|
@@ -1570,6 +1923,7 @@ const ZIP_MAX_UINT32 = 4294967295;
|
|
|
1570
1923
|
const LOCAL_FILE_HEADER_SIGNATURE = 67324752;
|
|
1571
1924
|
const CENTRAL_DIRECTORY_SIGNATURE = 33639248;
|
|
1572
1925
|
const END_OF_CENTRAL_DIRECTORY_SIGNATURE = 101010256;
|
|
1926
|
+
const DEFAULT_SESSION_CONTEXT_KEY = "sessionId";
|
|
1573
1927
|
let crc32Table;
|
|
1574
1928
|
function getCrc32Table() {
|
|
1575
1929
|
if (crc32Table) return crc32Table;
|
|
@@ -1709,6 +2063,56 @@ function normalizeMaxEvents(value) {
|
|
|
1709
2063
|
if (!Number.isFinite(value)) return void 0;
|
|
1710
2064
|
return Math.max(0, Math.floor(value));
|
|
1711
2065
|
}
|
|
2066
|
+
function normalizeSessionExportOptions(value, logFileName) {
|
|
2067
|
+
if (!value) return void 0;
|
|
2068
|
+
const options = value === true ? {} : value;
|
|
2069
|
+
return {
|
|
2070
|
+
contextKey: options.contextKey ?? DEFAULT_SESSION_CONTEXT_KEY,
|
|
2071
|
+
directory: options.directory ?? "sessions",
|
|
2072
|
+
fallbackSessionId: options.fallbackSessionId ?? "unknown",
|
|
2073
|
+
logFileName: options.logFileName ?? logFileName
|
|
2074
|
+
};
|
|
2075
|
+
}
|
|
2076
|
+
function normalizeRecentExportOptions(value, format) {
|
|
2077
|
+
if (!value) return void 0;
|
|
2078
|
+
const options = value === true ? {} : value;
|
|
2079
|
+
return {
|
|
2080
|
+
logFileName: options.logFileName ?? (format === "json" ? "recent.json" : "recent.ndjson"),
|
|
2081
|
+
maxEvents: normalizeMaxEvents(options.maxEvents) ?? 100
|
|
2082
|
+
};
|
|
2083
|
+
}
|
|
2084
|
+
function logContentForLines(lines, format) {
|
|
2085
|
+
if (format === "json") return `[${lines.join(",")}]`;
|
|
2086
|
+
return lines.length > 0 ? `${lines.join("\n")}\n` : "";
|
|
2087
|
+
}
|
|
2088
|
+
function eventTimeRange(items) {
|
|
2089
|
+
let from;
|
|
2090
|
+
let to;
|
|
2091
|
+
for (const item of items) {
|
|
2092
|
+
from = from === void 0 ? item.event.time : Math.min(from, item.event.time);
|
|
2093
|
+
to = to === void 0 ? item.event.time : Math.max(to, item.event.time);
|
|
2094
|
+
}
|
|
2095
|
+
return from === void 0 || to === void 0 ? void 0 : {
|
|
2096
|
+
from,
|
|
2097
|
+
to
|
|
2098
|
+
};
|
|
2099
|
+
}
|
|
2100
|
+
function sessionIdForEvent(event, options) {
|
|
2101
|
+
const value = event.context?.[options.contextKey];
|
|
2102
|
+
return typeof value === "string" && value.length > 0 ? value : options.fallbackSessionId;
|
|
2103
|
+
}
|
|
2104
|
+
function safeZipPathSegment(value) {
|
|
2105
|
+
const encoded = encodeURIComponent(value).replace(/%/g, "_");
|
|
2106
|
+
if (!encoded || encoded === "." || encoded === "..") return "_";
|
|
2107
|
+
return encoded;
|
|
2108
|
+
}
|
|
2109
|
+
function sortByEventTime(items) {
|
|
2110
|
+
return [...items].sort((left, right) => {
|
|
2111
|
+
if (left.event.time !== right.event.time) return left.event.time - right.event.time;
|
|
2112
|
+
if (left.event.seq !== right.event.seq) return left.event.seq - right.event.seq;
|
|
2113
|
+
return left.event.id < right.event.id ? -1 : left.event.id > right.event.id ? 1 : 0;
|
|
2114
|
+
});
|
|
2115
|
+
}
|
|
1712
2116
|
function createLogZipBlob(files, options = {}) {
|
|
1713
2117
|
const normalizedFiles = createNormalizedZipFiles(files, options);
|
|
1714
2118
|
const parts = [];
|
|
@@ -1731,27 +2135,72 @@ async function exportLogsToZip(source, options = {}) {
|
|
|
1731
2135
|
const createdAt = options.createdAt ?? Date.now();
|
|
1732
2136
|
const logFileName = options.logFileName ?? (format === "json" ? "logs.json" : "logs.ndjson");
|
|
1733
2137
|
const maxEvents = normalizeMaxEvents(options.maxEvents);
|
|
2138
|
+
const sessionExportOptions = normalizeSessionExportOptions(options.groupBySession, logFileName);
|
|
2139
|
+
const recentExportOptions = normalizeRecentExportOptions(options.includeRecent, format);
|
|
1734
2140
|
const serialize = options.serializeEvent ?? ((event) => (0, _loggerjs_core.safeJsonStringify)(event, options.stringify ?? {}));
|
|
1735
|
-
const
|
|
2141
|
+
const exported = [];
|
|
1736
2142
|
let skippedCount = 0;
|
|
1737
|
-
let from;
|
|
1738
|
-
let to;
|
|
1739
2143
|
for await (const event of resolveSource(source, options.query)) {
|
|
1740
|
-
if (maxEvents !== void 0 &&
|
|
2144
|
+
if (maxEvents !== void 0 && exported.length >= maxEvents) break;
|
|
1741
2145
|
const mapped = options.mapEvent ? options.mapEvent(event) : event;
|
|
1742
2146
|
if (!mapped) {
|
|
1743
2147
|
skippedCount += 1;
|
|
1744
2148
|
continue;
|
|
1745
2149
|
}
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
2150
|
+
exported.push({
|
|
2151
|
+
event: mapped,
|
|
2152
|
+
line: serialize(mapped)
|
|
2153
|
+
});
|
|
1749
2154
|
}
|
|
1750
2155
|
const files = [{
|
|
1751
2156
|
name: logFileName,
|
|
1752
|
-
content:
|
|
2157
|
+
content: logContentForLines(exported.map((item) => item.line), format),
|
|
1753
2158
|
lastModified: createdAt
|
|
1754
2159
|
}];
|
|
2160
|
+
const sessionManifests = [];
|
|
2161
|
+
if (sessionExportOptions) {
|
|
2162
|
+
const groups = /* @__PURE__ */ new Map();
|
|
2163
|
+
for (const item of exported) {
|
|
2164
|
+
const sessionId = sessionIdForEvent(item.event, sessionExportOptions);
|
|
2165
|
+
const group = groups.get(sessionId) ?? [];
|
|
2166
|
+
group.push(item);
|
|
2167
|
+
groups.set(sessionId, group);
|
|
2168
|
+
}
|
|
2169
|
+
const usedFileNames = new Set(files.map((file) => normalizeZipFileName(file.name)));
|
|
2170
|
+
const sortedGroups = [...groups.entries()];
|
|
2171
|
+
sortedGroups.sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0);
|
|
2172
|
+
for (const [sessionId, group] of sortedGroups) {
|
|
2173
|
+
let fileName = normalizeZipFileName(`${sessionExportOptions.directory}/${safeZipPathSegment(sessionId)}/${sessionExportOptions.logFileName}`);
|
|
2174
|
+
let suffix = 1;
|
|
2175
|
+
while (usedFileNames.has(fileName)) {
|
|
2176
|
+
fileName = normalizeZipFileName(`${sessionExportOptions.directory}/${safeZipPathSegment(sessionId)}-${suffix}/${sessionExportOptions.logFileName}`);
|
|
2177
|
+
suffix += 1;
|
|
2178
|
+
}
|
|
2179
|
+
usedFileNames.add(fileName);
|
|
2180
|
+
const orderedGroup = sortByEventTime(group);
|
|
2181
|
+
files.push({
|
|
2182
|
+
name: fileName,
|
|
2183
|
+
content: logContentForLines(orderedGroup.map((item) => item.line), format),
|
|
2184
|
+
lastModified: createdAt
|
|
2185
|
+
});
|
|
2186
|
+
sessionManifests.push({
|
|
2187
|
+
logCount: orderedGroup.length,
|
|
2188
|
+
logFileName: fileName,
|
|
2189
|
+
sessionId,
|
|
2190
|
+
timeRange: eventTimeRange(orderedGroup)
|
|
2191
|
+
});
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
let recentLogFileName;
|
|
2195
|
+
if (recentExportOptions) {
|
|
2196
|
+
const recentItems = recentExportOptions.maxEvents <= 0 ? [] : sortByEventTime(exported).slice(-recentExportOptions.maxEvents);
|
|
2197
|
+
recentLogFileName = normalizeZipFileName(recentExportOptions.logFileName);
|
|
2198
|
+
files.push({
|
|
2199
|
+
name: recentLogFileName,
|
|
2200
|
+
content: logContentForLines(recentItems.map((item) => item.line), format),
|
|
2201
|
+
lastModified: createdAt
|
|
2202
|
+
});
|
|
2203
|
+
}
|
|
1755
2204
|
if (options.includeManifest ?? true) {
|
|
1756
2205
|
const manifest = {
|
|
1757
2206
|
schema: "loggerjs.log-export.v1",
|
|
@@ -1759,13 +2208,13 @@ async function exportLogsToZip(source, options = {}) {
|
|
|
1759
2208
|
source: options.source,
|
|
1760
2209
|
format,
|
|
1761
2210
|
logFileName: normalizeZipFileName(logFileName),
|
|
1762
|
-
logCount:
|
|
2211
|
+
logCount: exported.length,
|
|
1763
2212
|
skippedCount,
|
|
1764
2213
|
query: options.query,
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
2214
|
+
recentLogFileName,
|
|
2215
|
+
sessionCount: sessionExportOptions ? sessionManifests.length : void 0,
|
|
2216
|
+
sessions: sessionExportOptions ? sessionManifests : void 0,
|
|
2217
|
+
timeRange: eventTimeRange(exported)
|
|
1769
2218
|
};
|
|
1770
2219
|
files.push({
|
|
1771
2220
|
name: options.manifestFileName ?? "manifest.json",
|