@loggerjs/browser 0.5.2 → 0.5.4

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
@@ -894,12 +894,98 @@ 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 DB_VERSION = 2;
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 = 4;
898
902
  const ORDER_INDEX_NAME = "createdAtSeq";
903
+ const SESSION_INDEX_NAME = "sessionId";
904
+ const SESSION_ORDER_INDEX_NAME = "sessionIdCreatedAtSeq";
905
+ const SESSION_STORE_SUFFIX = "__sessions";
906
+ const SESSION_LAST_SEEN_INDEX_NAME = "lastSeenSessionId";
907
+ const SPILL_SCHEMA = "loggerjs.indexeddb-spill.v1";
899
908
  function normalizePositiveInteger(value, fallback) {
900
909
  if (value === void 0 || !Number.isFinite(value)) return fallback;
901
910
  return Math.max(0, Math.floor(value));
902
911
  }
912
+ let fallbackSessionSeq = 0;
913
+ function defaultPageSessionId() {
914
+ const randomUUID = globalThis.crypto?.randomUUID;
915
+ if (randomUUID) return randomUUID.call(globalThis.crypto);
916
+ return `${Date.now().toString(36)}-${(fallbackSessionSeq++).toString(36)}`;
917
+ }
918
+ function normalizeSessionId(value) {
919
+ if (typeof value !== "string") return void 0;
920
+ const normalized = value.trim();
921
+ return normalized.length > 0 ? normalized : void 0;
922
+ }
923
+ function normalizeSessionOptions(session) {
924
+ if (session === false) return void 0;
925
+ if (typeof session === "string") return {
926
+ contextKey: DEFAULT_SESSION_CONTEXT_KEY$1,
927
+ id: normalizeSessionId(session) ?? defaultPageSessionId()
928
+ };
929
+ return {
930
+ contextKey: session?.contextKey ?? DEFAULT_SESSION_CONTEXT_KEY$1,
931
+ getId: session?.getId,
932
+ id: normalizeSessionId(session?.id) ?? defaultPageSessionId()
933
+ };
934
+ }
935
+ function resolveLocalStorage(storage) {
936
+ if (storage) return storage;
937
+ try {
938
+ return globalThis.localStorage;
939
+ } catch {
940
+ return;
941
+ }
942
+ }
943
+ function normalizeLocalStorageSpillOptions(spill, dbName, storeName) {
944
+ if (!spill) return void 0;
945
+ const storage = resolveLocalStorage(spill.storage);
946
+ if (!storage) return void 0;
947
+ const namespace = spill.namespace ?? `${dbName}:${storeName}`;
948
+ return {
949
+ drainOnCreate: spill.drainOnCreate ?? true,
950
+ key: `loggerjs:spill:v1:${namespace}`,
951
+ maxBytes: normalizePositiveInteger(spill.maxBytes, DEFAULT_SPILL_MAX_BYTES),
952
+ maxEntries: normalizePositiveInteger(spill.maxEntries, DEFAULT_SPILL_MAX_ENTRIES),
953
+ maxOriginBytes: normalizePositiveInteger(spill.maxOriginBytes, DEFAULT_SPILL_MAX_ORIGIN_BYTES),
954
+ minLevelValue: spill.minLevel === void 0 ? void 0 : (0, _loggerjs_core.toLevelValue)(spill.minLevel),
955
+ onDrop: spill.onDrop,
956
+ spillOnPageHide: spill.spillOnPageHide ?? true,
957
+ storage
958
+ };
959
+ }
960
+ function storageByteLength(key, value) {
961
+ return 2 * (key.length + value.length);
962
+ }
963
+ function estimateLocalStorageBytes(storage) {
964
+ try {
965
+ let total = 0;
966
+ for (let index = 0; index < storage.length; index += 1) {
967
+ const key = storage.key(index);
968
+ if (key === null) continue;
969
+ total += storageByteLength(key, storage.getItem(key) ?? "");
970
+ }
971
+ return total;
972
+ } catch {
973
+ return;
974
+ }
975
+ }
976
+ function isQuotaExceededError(error) {
977
+ return typeof DOMException !== "undefined" && error instanceof DOMException && (error.name === "QuotaExceededError" || error.name === "NS_ERROR_DOM_QUOTA_REACHED");
978
+ }
979
+ function isSpillLogEvent(value) {
980
+ if (!value || typeof value !== "object") return false;
981
+ const event = value;
982
+ 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";
983
+ }
984
+ function parseSpillPayload(raw) {
985
+ const parsed = JSON.parse(raw);
986
+ if (parsed.schema !== SPILL_SCHEMA || !Array.isArray(parsed.entries)) return void 0;
987
+ return parsed.entries.filter(isSpillLogEvent);
988
+ }
903
989
  function nowMs$1() {
904
990
  return typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
905
991
  }
@@ -942,6 +1028,9 @@ function indexExists(store, name) {
942
1028
  function ensureIndex(store, name, keyPath) {
943
1029
  if (!indexExists(store, name)) store.createIndex(name, keyPath);
944
1030
  }
1031
+ function sessionStoreNameFor(storeName) {
1032
+ return `${storeName}${SESSION_STORE_SUFFIX}`;
1033
+ }
945
1034
  function getStoreIndex(store, name) {
946
1035
  const maybeStore = store;
947
1036
  if (typeof maybeStore.index !== "function" || !indexExists(store, name)) return void 0;
@@ -965,13 +1054,67 @@ function decodeEntry(entry, codec) {
965
1054
  function shouldKeepEntry(entry, options) {
966
1055
  if (options.from !== void 0 && entry.createdAt < options.from) return false;
967
1056
  if (options.to !== void 0 && entry.createdAt > options.to) return false;
1057
+ if (options.sessionId !== void 0 && entry.sessionId !== options.sessionId) return false;
968
1058
  if (options.minLevel !== void 0 && entry.level < (0, _loggerjs_core.toLevelValue)(options.minLevel)) return false;
969
1059
  if (options.logger !== void 0 && entry.logger !== options.logger) return false;
970
1060
  if (options.type !== void 0 && entry.type !== options.type) return false;
971
1061
  return true;
972
1062
  }
1063
+ function addSessionSummary(sessions, entry) {
1064
+ if (!entry.sessionId) return;
1065
+ const current = sessions.get(entry.sessionId);
1066
+ if (current) {
1067
+ current.firstSeen = Math.min(current.firstSeen, entry.createdAt);
1068
+ current.lastSeen = Math.max(current.lastSeen, entry.createdAt);
1069
+ current.count += 1;
1070
+ current.byteLength += entry.byteLength;
1071
+ return;
1072
+ }
1073
+ sessions.set(entry.sessionId, {
1074
+ byteLength: entry.byteLength,
1075
+ count: 1,
1076
+ firstSeen: entry.createdAt,
1077
+ lastSeen: entry.createdAt,
1078
+ sessionId: entry.sessionId
1079
+ });
1080
+ }
1081
+ function mergeSessionSummary(current, incoming) {
1082
+ if (!current) return incoming;
1083
+ return {
1084
+ byteLength: current.byteLength + incoming.byteLength,
1085
+ count: current.count + incoming.count,
1086
+ firstSeen: Math.min(current.firstSeen, incoming.firstSeen),
1087
+ lastSeen: Math.max(current.lastSeen, incoming.lastSeen),
1088
+ sessionId: incoming.sessionId
1089
+ };
1090
+ }
1091
+ function orderSessions(sessions, order = "desc") {
1092
+ const sorted = [...sessions].sort((left, right) => {
1093
+ if (left.lastSeen !== right.lastSeen) return left.lastSeen - right.lastSeen;
1094
+ return left.sessionId < right.sessionId ? -1 : left.sessionId > right.sessionId ? 1 : 0;
1095
+ });
1096
+ if (order === "asc") return sorted;
1097
+ const reversed = [];
1098
+ for (let index = sorted.length - 1; index >= 0; index -= 1) reversed.push(sorted[index]);
1099
+ return reversed;
1100
+ }
973
1101
  function orderRangeForQuery(options) {
974
1102
  if (typeof IDBKeyRange === "undefined") return void 0;
1103
+ if (options.sessionId !== void 0) {
1104
+ const lower = [
1105
+ options.sessionId,
1106
+ options.from ?? Number.MIN_SAFE_INTEGER,
1107
+ Number.MIN_SAFE_INTEGER,
1108
+ ""
1109
+ ];
1110
+ const upper = [
1111
+ options.sessionId,
1112
+ options.to ?? Number.MAX_SAFE_INTEGER,
1113
+ Number.MAX_SAFE_INTEGER,
1114
+ "￿"
1115
+ ];
1116
+ return IDBKeyRange.bound(lower, upper);
1117
+ }
975
1118
  const lower = options.from === void 0 ? void 0 : [
976
1119
  options.from,
977
1120
  Number.MIN_SAFE_INTEGER,
@@ -986,6 +1129,33 @@ function orderRangeForQuery(options) {
986
1129
  if (lower) return IDBKeyRange.lowerBound(lower);
987
1130
  if (upper) return IDBKeyRange.upperBound(upper);
988
1131
  }
1132
+ function orderRangeForSessionId(sessionId) {
1133
+ if (typeof IDBKeyRange === "undefined") return void 0;
1134
+ return IDBKeyRange.bound([
1135
+ sessionId,
1136
+ Number.MIN_SAFE_INTEGER,
1137
+ Number.MIN_SAFE_INTEGER,
1138
+ ""
1139
+ ], [
1140
+ sessionId,
1141
+ Number.MAX_SAFE_INTEGER,
1142
+ Number.MAX_SAFE_INTEGER,
1143
+ "￿"
1144
+ ]);
1145
+ }
1146
+ function migrateSessionSummaries(logStore, sessionStore) {
1147
+ const summaries = /* @__PURE__ */ new Map();
1148
+ const request = logStore.openCursor();
1149
+ request.addEventListener("success", () => {
1150
+ const cursor = request.result;
1151
+ if (!cursor) {
1152
+ for (const summary of summaries.values()) sessionStore.put(summary);
1153
+ return;
1154
+ }
1155
+ addSessionSummary(summaries, cursor.value);
1156
+ cursor.continue();
1157
+ });
1158
+ }
989
1159
  function iterateCursor(request, visit) {
990
1160
  return new Promise((resolve, reject) => {
991
1161
  request.addEventListener("success", () => {
@@ -1010,6 +1180,7 @@ function iterateCursor(request, visit) {
1010
1180
  function indexedDbTransport(options = {}) {
1011
1181
  const dbName = options.dbName ?? DEFAULT_DB_NAME;
1012
1182
  const storeName = options.storeName ?? DEFAULT_STORE_NAME;
1183
+ const sessionStoreName = sessionStoreNameFor(storeName);
1013
1184
  const codec = options.codec ?? (0, _loggerjs_core.safeJsonCodec)();
1014
1185
  const batchSize = normalizePositiveInteger(options.batchSize, 100);
1015
1186
  const flushIntervalMs = normalizePositiveInteger(options.flushIntervalMs, 1e3);
@@ -1020,12 +1191,18 @@ function indexedDbTransport(options = {}) {
1020
1191
  const flushOnPageHide = options.flushOnPageHide ?? true;
1021
1192
  const durability = options.durability ?? "default";
1022
1193
  const rootIndexedDB = options.indexedDB ?? globalThis.indexedDB;
1194
+ const sessionOptions = normalizeSessionOptions(options.session);
1195
+ const spillOptions = normalizeLocalStorageSpillOptions(options.localStorageSpill, dbName, storeName);
1023
1196
  const transactionOptions = durability === "default" ? void 0 : { durability };
1024
1197
  const buffer = [];
1025
1198
  let dbPromise;
1026
1199
  let indexedDBFactoryPromise;
1027
1200
  let timer;
1028
1201
  let flushPromise;
1202
+ let clearPromise;
1203
+ let pendingFlushBatch;
1204
+ let spillDrainPromise;
1205
+ let localStorageSpillClearCandidate;
1029
1206
  let lastContext;
1030
1207
  let closed = false;
1031
1208
  const statsState = {
@@ -1052,13 +1229,23 @@ function indexedDbTransport(options = {}) {
1052
1229
  storageBucketFallbacks: 0,
1053
1230
  transactionOptionFallbacks: 0,
1054
1231
  errors: 0,
1055
- errorsByOperation: {}
1232
+ errorsByOperation: {},
1233
+ localStorageSpillWrites: 0,
1234
+ localStorageSpillEntries: 0,
1235
+ localStorageSpillDrains: 0,
1236
+ localStorageSpillDrainedEntries: 0,
1237
+ localStorageSpillDropped: 0,
1238
+ localStorageSpillDroppedByReason: {},
1239
+ localStorageSpillErrors: 0,
1240
+ localStorageSpillErrorsByOperation: {}
1056
1241
  };
1057
1242
  const snapshotStats = () => ({
1058
1243
  ...statsState,
1059
1244
  bufferDepth: buffer.length,
1060
1245
  droppedByReason: { ...statsState.droppedByReason },
1061
1246
  errorsByOperation: { ...statsState.errorsByOperation },
1247
+ localStorageSpillDroppedByReason: { ...statsState.localStorageSpillDroppedByReason },
1248
+ localStorageSpillErrorsByOperation: { ...statsState.localStorageSpillErrorsByOperation },
1062
1249
  pendingFlush: flushPromise !== void 0,
1063
1250
  persistedDroppedByReason: { ...statsState.persistedDroppedByReason }
1064
1251
  });
@@ -1085,6 +1272,18 @@ function indexedDbTransport(options = {}) {
1085
1272
  (0, _loggerjs_core.incrementLoggerMetaCounter)(`transport.indexeddb.persisted.dropped.${reason}`);
1086
1273
  options.onPersistedDrop?.(entry, reason);
1087
1274
  };
1275
+ const reportSpillError = (error, operation) => {
1276
+ statsState.localStorageSpillErrors += 1;
1277
+ incrementRecord(statsState.localStorageSpillErrorsByOperation, operation);
1278
+ reportInternalError(error, `localstorage-spill-${operation}`);
1279
+ };
1280
+ const dropSpillEvent = (event, reason) => {
1281
+ statsState.localStorageSpillDropped += 1;
1282
+ incrementRecord(statsState.localStorageSpillDroppedByReason, reason);
1283
+ (0, _loggerjs_core.incrementLoggerMetaCounter)("transport.indexeddb.localstorage_spill.dropped");
1284
+ (0, _loggerjs_core.incrementLoggerMetaCounter)(`transport.indexeddb.localstorage_spill.dropped.${reason}`);
1285
+ spillOptions?.onDrop?.(event, reason);
1286
+ };
1088
1287
  const resolveIndexedDBFactory = async () => {
1089
1288
  if (options.indexedDB || !options.storageBucketName) return rootIndexedDB;
1090
1289
  indexedDBFactoryPromise ??= (async () => {
@@ -1111,7 +1310,7 @@ function indexedDbTransport(options = {}) {
1111
1310
  if (!idb) throw new Error("IndexedDB is not available for indexedDbTransport");
1112
1311
  return new Promise((resolve, reject) => {
1113
1312
  const request = idb.open(dbName, DB_VERSION);
1114
- request.addEventListener("upgradeneeded", () => {
1313
+ request.addEventListener("upgradeneeded", (event) => {
1115
1314
  const db = request.result;
1116
1315
  const store = db.objectStoreNames.contains(storeName) ? request.transaction?.objectStore(storeName) : db.createObjectStore(storeName, { keyPath: "id" });
1117
1316
  if (!store) return;
@@ -1124,6 +1323,17 @@ function indexedDbTransport(options = {}) {
1124
1323
  "seq",
1125
1324
  "id"
1126
1325
  ]);
1326
+ ensureIndex(store, SESSION_INDEX_NAME, "sessionId");
1327
+ ensureIndex(store, SESSION_ORDER_INDEX_NAME, [
1328
+ "sessionId",
1329
+ "createdAt",
1330
+ "seq",
1331
+ "id"
1332
+ ]);
1333
+ const sessionStore = db.objectStoreNames.contains(sessionStoreName) ? request.transaction?.objectStore(sessionStoreName) : db.createObjectStore(sessionStoreName, { keyPath: "sessionId" });
1334
+ if (!sessionStore) return;
1335
+ ensureIndex(sessionStore, SESSION_LAST_SEEN_INDEX_NAME, ["lastSeen", "sessionId"]);
1336
+ if (event.oldVersion < 4) migrateSessionSummaries(store, sessionStore);
1127
1337
  });
1128
1338
  request.addEventListener("success", () => {
1129
1339
  statsState.databaseOpenCount += 1;
@@ -1134,13 +1344,13 @@ function indexedDbTransport(options = {}) {
1134
1344
  });
1135
1345
  return dbPromise;
1136
1346
  };
1137
- const createTransaction = (db, mode) => {
1138
- if (mode !== "readwrite" || !transactionOptions) return db.transaction(storeName, mode);
1347
+ const createTransaction = (db, mode, stores = storeName) => {
1348
+ if (mode !== "readwrite" || !transactionOptions) return db.transaction(stores, mode);
1139
1349
  try {
1140
- return db.transaction(storeName, mode, transactionOptions);
1350
+ return db.transaction(stores, mode, transactionOptions);
1141
1351
  } catch {
1142
1352
  statsState.transactionOptionFallbacks += 1;
1143
- return db.transaction(storeName, mode);
1353
+ return db.transaction(stores, mode);
1144
1354
  }
1145
1355
  };
1146
1356
  const withStore = async (mode, run) => {
@@ -1157,6 +1367,57 @@ function indexedDbTransport(options = {}) {
1157
1367
  throw error;
1158
1368
  }
1159
1369
  };
1370
+ const withLogAndSessionStores = async (mode, run) => {
1371
+ const tx = createTransaction(await openDb(), mode, [storeName, sessionStoreName]);
1372
+ const settled = transactionToPromise(tx);
1373
+ try {
1374
+ const result = await run(tx.objectStore(storeName), tx.objectStore(sessionStoreName), tx);
1375
+ await settled;
1376
+ return result;
1377
+ } catch (error) {
1378
+ if (tx.error === null) try {
1379
+ tx.abort();
1380
+ } catch {}
1381
+ throw error;
1382
+ }
1383
+ };
1384
+ const withSessionStore = async (mode, run) => {
1385
+ const tx = createTransaction(await openDb(), mode, sessionStoreName);
1386
+ const settled = transactionToPromise(tx);
1387
+ try {
1388
+ const result = await run(tx.objectStore(sessionStoreName), tx);
1389
+ await settled;
1390
+ return result;
1391
+ } catch (error) {
1392
+ if (tx.error === null) try {
1393
+ tx.abort();
1394
+ } catch {}
1395
+ throw error;
1396
+ }
1397
+ };
1398
+ const mergeSessionSummaries = async (sessionStore, entries) => {
1399
+ const summaries = /* @__PURE__ */ new Map();
1400
+ for (const entry of entries) addSessionSummary(summaries, entry);
1401
+ for (const summary of summaries.values()) {
1402
+ const current = await requestToPromise(sessionStore.get(summary.sessionId));
1403
+ await requestToPromise(sessionStore.put(mergeSessionSummary(current, summary)));
1404
+ }
1405
+ };
1406
+ const rebuildSessionSummaries = async (store, sessionStore, sessionIds) => {
1407
+ if (sessionIds.size === 0) return;
1408
+ const index = getStoreIndex(store, SESSION_ORDER_INDEX_NAME);
1409
+ for (const sessionId of sessionIds) {
1410
+ const summaries = /* @__PURE__ */ new Map();
1411
+ const range = orderRangeForSessionId(sessionId);
1412
+ await iterateCursor(index && range ? index.openCursor(range) : store.openCursor(), (cursor) => {
1413
+ const entry = cursor.value;
1414
+ if (entry.sessionId === sessionId) addSessionSummary(summaries, entry);
1415
+ });
1416
+ const summary = summaries.get(sessionId);
1417
+ if (summary) await requestToPromise(sessionStore.put(summary));
1418
+ else await requestToPromise(sessionStore.delete(sessionId));
1419
+ }
1420
+ };
1160
1421
  const getEntries = async () => orderEntries(await withStore("readonly", (store) => requestToPromise(store.getAll())));
1161
1422
  const countEntries = async () => withStore("readonly", (store) => {
1162
1423
  const maybeStore = store;
@@ -1165,25 +1426,31 @@ function indexedDbTransport(options = {}) {
1165
1426
  });
1166
1427
  const deleteEntries = async (entries, reason) => {
1167
1428
  if (entries.length === 0) return;
1168
- await withStore("readwrite", async (store) => {
1429
+ await withLogAndSessionStores("readwrite", async (store, sessionStore) => {
1430
+ const affectedSessionIds = /* @__PURE__ */ new Set();
1169
1431
  for (const entry of entries) {
1170
1432
  await requestToPromise(store.delete(entry.id));
1433
+ if (entry.sessionId) affectedSessionIds.add(entry.sessionId);
1171
1434
  dropPersistedEntry(entry, reason);
1172
1435
  }
1436
+ await rebuildSessionSummaries(store, sessionStore, affectedSessionIds);
1173
1437
  });
1174
1438
  };
1175
- const deleteByCursor = async (reason, openCursor, shouldDelete) => withStore("readwrite", async (store) => {
1439
+ const deleteByCursor = async (reason, openCursor, shouldDelete) => withLogAndSessionStores("readwrite", async (store, sessionStore) => {
1176
1440
  const request = openCursor(store);
1177
1441
  if (!request) return void 0;
1178
1442
  let deleted = 0;
1443
+ const affectedSessionIds = /* @__PURE__ */ new Set();
1179
1444
  await iterateCursor(request, (cursor) => {
1180
1445
  const entry = cursor.value;
1181
1446
  if (!shouldDelete(entry, deleted)) return false;
1182
1447
  cursor.delete();
1183
1448
  deleted += 1;
1449
+ if (entry.sessionId) affectedSessionIds.add(entry.sessionId);
1184
1450
  dropPersistedEntry(entry, reason);
1185
1451
  return true;
1186
1452
  });
1453
+ await rebuildSessionSummaries(store, sessionStore, affectedSessionIds);
1187
1454
  return deleted;
1188
1455
  });
1189
1456
  const deleteExpiredByCursor = async (cutoff) => {
@@ -1214,7 +1481,7 @@ function indexedDbTransport(options = {}) {
1214
1481
  return total;
1215
1482
  });
1216
1483
  const queryEntriesByCursor = async (queryOptions) => withStore("readonly", async (store) => {
1217
- const index = getStoreIndex(store, ORDER_INDEX_NAME);
1484
+ const index = getStoreIndex(store, queryOptions.sessionId === void 0 ? ORDER_INDEX_NAME : SESSION_ORDER_INDEX_NAME);
1218
1485
  if (!index) return void 0;
1219
1486
  const entries = [];
1220
1487
  const limit = queryOptions.limit;
@@ -1227,6 +1494,20 @@ function indexedDbTransport(options = {}) {
1227
1494
  });
1228
1495
  return entries;
1229
1496
  });
1497
+ const listSessions = async (sessionQueryOptions = {}) => {
1498
+ const limit = sessionQueryOptions.limit;
1499
+ if (limit !== void 0 && limit <= 0) return [];
1500
+ return withSessionStore("readonly", async (sessionStore) => {
1501
+ const sessions = [];
1502
+ const index = getStoreIndex(sessionStore, SESSION_LAST_SEEN_INDEX_NAME);
1503
+ if (!index) return orderSessions(await requestToPromise(sessionStore.getAll()), sessionQueryOptions.order).slice(0, limit);
1504
+ await iterateCursor(index.openCursor(void 0, sessionQueryOptions.order === "asc" ? "next" : "prev"), (cursor) => {
1505
+ sessions.push(cursor.value);
1506
+ return limit === void 0 || sessions.length < limit;
1507
+ });
1508
+ return sessions;
1509
+ });
1510
+ };
1230
1511
  const pruneWithEntries = async () => {
1231
1512
  const now = Date.now();
1232
1513
  let entries = await getEntries();
@@ -1282,10 +1563,34 @@ function indexedDbTransport(options = {}) {
1282
1563
  statsState.lastPruneDurationMs = nowMs$1() - startedAt;
1283
1564
  }
1284
1565
  };
1566
+ const sessionIdForEvent = (event) => {
1567
+ if (!sessionOptions) return void 0;
1568
+ return normalizeSessionId(sessionOptions.getId?.(event)) ?? normalizeSessionId(event.context?.[sessionOptions.contextKey]) ?? sessionOptions.id;
1569
+ };
1570
+ const eventWithSessionContext = (event, sessionId) => {
1571
+ if (!sessionId || !sessionOptions?.contextKey) return event;
1572
+ if (event.context?.[sessionOptions.contextKey] === sessionId) return event;
1573
+ return {
1574
+ ...event,
1575
+ context: {
1576
+ ...event.context,
1577
+ [sessionOptions.contextKey]: sessionId
1578
+ }
1579
+ };
1580
+ };
1581
+ const eventWithResolvedSession = (event) => {
1582
+ const sessionId = sessionIdForEvent(event);
1583
+ return {
1584
+ event: eventWithSessionContext(event, sessionId),
1585
+ sessionId
1586
+ };
1587
+ };
1285
1588
  const eventToEntry = (event) => {
1286
- const payload = codec.encode(event);
1589
+ const resolved = eventWithResolvedSession(event);
1590
+ const payload = codec.encode(resolved.event);
1287
1591
  return {
1288
1592
  id: event.id,
1593
+ sessionId: resolved.sessionId,
1289
1594
  seq: event.seq,
1290
1595
  createdAt: event.time,
1291
1596
  level: event.level,
@@ -1296,17 +1601,50 @@ function indexedDbTransport(options = {}) {
1296
1601
  payload
1297
1602
  };
1298
1603
  };
1604
+ const clearLocalStorageSpill = (expectedValue) => {
1605
+ if (!spillOptions) return false;
1606
+ try {
1607
+ if (expectedValue !== void 0 && spillOptions.storage.getItem(spillOptions.key) !== expectedValue) return false;
1608
+ spillOptions.storage.removeItem(spillOptions.key);
1609
+ if (expectedValue === void 0 || localStorageSpillClearCandidate?.value === expectedValue) localStorageSpillClearCandidate = void 0;
1610
+ return true;
1611
+ } catch (error) {
1612
+ reportSpillError(error, "clear");
1613
+ return false;
1614
+ }
1615
+ };
1616
+ const rememberLocalStorageSpill = (value, events) => {
1617
+ localStorageSpillClearCandidate = {
1618
+ pendingIds: new Set(events.map((event) => event.id)),
1619
+ value
1620
+ };
1621
+ };
1622
+ const markLocalStorageSpillPersisted = (events) => {
1623
+ const candidate = localStorageSpillClearCandidate;
1624
+ if (!candidate || events.length === 0) return;
1625
+ for (const event of events) candidate.pendingIds.delete(event.id);
1626
+ if (candidate.pendingIds.size === 0) clearLocalStorageSpill(candidate.value);
1627
+ };
1299
1628
  const writeBatch = async (events) => {
1300
1629
  if (events.length === 0) return;
1301
1630
  const startedAt = nowMs$1();
1302
1631
  statsState.lastFlushBatchSize = events.length;
1303
1632
  const entries = events.map(eventToEntry);
1304
1633
  try {
1305
- await withStore("readwrite", (store) => {
1306
- for (const entry of entries) store.put(entry);
1634
+ await withLogAndSessionStores("readwrite", async (store, sessionStore) => {
1635
+ const rebuildSessionIds = /* @__PURE__ */ new Set();
1636
+ for (const entry of entries) {
1637
+ const existing = await requestToPromise(store.get(entry.id));
1638
+ if (existing?.sessionId) rebuildSessionIds.add(existing.sessionId);
1639
+ if (existing && entry.sessionId) rebuildSessionIds.add(entry.sessionId);
1640
+ await requestToPromise(store.put(entry));
1641
+ }
1642
+ await rebuildSessionSummaries(store, sessionStore, rebuildSessionIds);
1643
+ await mergeSessionSummaries(sessionStore, entries.filter((entry) => !entry.sessionId || !rebuildSessionIds.has(entry.sessionId)));
1307
1644
  });
1308
1645
  statsState.persisted += entries.length;
1309
1646
  (0, _loggerjs_core.incrementLoggerMetaCounter)("transport.indexeddb.persisted", entries.length);
1647
+ markLocalStorageSpillPersisted(events);
1310
1648
  await prune();
1311
1649
  statsState.flushes += 1;
1312
1650
  } catch (error) {
@@ -1316,6 +1654,124 @@ function indexedDbTransport(options = {}) {
1316
1654
  statsState.lastFlushDurationMs = nowMs$1() - startedAt;
1317
1655
  }
1318
1656
  };
1657
+ const stringifySpillPayload = (events) => (0, _loggerjs_core.safeJsonStringify)({
1658
+ createdAt: Date.now(),
1659
+ entries: events,
1660
+ schema: SPILL_SCHEMA
1661
+ }, { maxArrayLength: spillOptions?.maxEntries ?? DEFAULT_SPILL_MAX_ENTRIES });
1662
+ const spillStorageBudget = (currentValue) => {
1663
+ if (!spillOptions) return 0;
1664
+ const ownBytes = currentValue ? storageByteLength(spillOptions.key, currentValue) : 0;
1665
+ const originBytes = estimateLocalStorageBytes(spillOptions.storage);
1666
+ if (originBytes === void 0) return spillOptions.maxBytes;
1667
+ return Math.max(0, Math.min(spillOptions.maxBytes, spillOptions.maxOriginBytes - originBytes + ownBytes));
1668
+ };
1669
+ const pendingEventsForSpill = () => {
1670
+ const events = [...pendingFlushBatch ?? [], ...buffer];
1671
+ const seen = /* @__PURE__ */ new Set();
1672
+ const deduped = [];
1673
+ for (const event of events) {
1674
+ if (seen.has(event.id)) continue;
1675
+ seen.add(event.id);
1676
+ if (spillOptions?.minLevelValue !== void 0 && event.level < spillOptions.minLevelValue) continue;
1677
+ deduped.push(eventWithResolvedSession(event).event);
1678
+ }
1679
+ return deduped;
1680
+ };
1681
+ const trimSpillEventsToBudget = (events, budget) => {
1682
+ if (!spillOptions) return void 0;
1683
+ while (events.length > spillOptions.maxEntries) {
1684
+ const dropped = events.shift();
1685
+ if (dropped) dropSpillEvent(dropped, "max-entries");
1686
+ }
1687
+ while (events.length > 0) {
1688
+ const value = stringifySpillPayload(events);
1689
+ if (storageByteLength(spillOptions.key, value) <= budget) return value;
1690
+ const dropped = events.shift();
1691
+ if (dropped) dropSpillEvent(dropped, "max-bytes");
1692
+ }
1693
+ };
1694
+ const writeLocalStorageSpill = () => {
1695
+ if (!spillOptions) return;
1696
+ const pendingEvents = pendingEventsForSpill();
1697
+ if (pendingEvents.length === 0) return;
1698
+ try {
1699
+ const currentValue = spillOptions.storage.getItem(spillOptions.key);
1700
+ let events = pendingEvents;
1701
+ if (currentValue) {
1702
+ const merged = /* @__PURE__ */ new Map();
1703
+ try {
1704
+ for (const event of parseSpillPayload(currentValue) ?? []) merged.set(event.id, event);
1705
+ } catch {}
1706
+ for (const event of pendingEvents) merged.set(event.id, event);
1707
+ events = [...merged.values()];
1708
+ }
1709
+ const budget = spillStorageBudget(currentValue);
1710
+ const value = trimSpillEventsToBudget(events, budget);
1711
+ if (!value) return;
1712
+ try {
1713
+ spillOptions.storage.setItem(spillOptions.key, value);
1714
+ } catch (error) {
1715
+ if (!isQuotaExceededError(error)) throw error;
1716
+ while (events.length > 0) {
1717
+ const dropped = events.shift();
1718
+ if (dropped) dropSpillEvent(dropped, "quota");
1719
+ const retryValue = trimSpillEventsToBudget(events, budget);
1720
+ if (!retryValue) return;
1721
+ try {
1722
+ spillOptions.storage.setItem(spillOptions.key, retryValue);
1723
+ rememberLocalStorageSpill(retryValue, events);
1724
+ statsState.localStorageSpillWrites += 1;
1725
+ statsState.localStorageSpillEntries += events.length;
1726
+ return;
1727
+ } catch (retryError) {
1728
+ if (!isQuotaExceededError(retryError)) throw retryError;
1729
+ }
1730
+ }
1731
+ return;
1732
+ }
1733
+ rememberLocalStorageSpill(value, events);
1734
+ statsState.localStorageSpillWrites += 1;
1735
+ statsState.localStorageSpillEntries += events.length;
1736
+ } catch (error) {
1737
+ reportSpillError(error, "write");
1738
+ }
1739
+ };
1740
+ const drainLocalStorageSpill = async () => {
1741
+ if (!spillOptions) return;
1742
+ let raw;
1743
+ try {
1744
+ raw = spillOptions.storage.getItem(spillOptions.key);
1745
+ } catch (error) {
1746
+ reportSpillError(error, "read");
1747
+ return;
1748
+ }
1749
+ if (!raw) return;
1750
+ let events;
1751
+ try {
1752
+ events = parseSpillPayload(raw);
1753
+ } catch (error) {
1754
+ reportSpillError(error, "parse");
1755
+ clearLocalStorageSpill();
1756
+ return;
1757
+ }
1758
+ if (!events || events.length === 0) {
1759
+ clearLocalStorageSpill();
1760
+ return;
1761
+ }
1762
+ await writeBatch(events);
1763
+ clearLocalStorageSpill(raw);
1764
+ statsState.localStorageSpillDrains += 1;
1765
+ statsState.localStorageSpillDrainedEntries += events.length;
1766
+ };
1767
+ const ensureSpillDrained = async () => {
1768
+ const promise = spillDrainPromise;
1769
+ if (!promise) return;
1770
+ await promise;
1771
+ };
1772
+ if (spillOptions?.drainOnCreate) spillDrainPromise = drainLocalStorageSpill().catch((error) => reportSpillError(error, "drain")).finally(() => {
1773
+ spillDrainPromise = void 0;
1774
+ });
1319
1775
  const clearTimer = () => {
1320
1776
  if (timer) clearTimeout(timer);
1321
1777
  timer = void 0;
@@ -1323,7 +1779,15 @@ function indexedDbTransport(options = {}) {
1323
1779
  const flushPending = async () => {
1324
1780
  if (flushPromise) return flushPromise;
1325
1781
  clearTimer();
1326
- flushPromise = writeBatch(buffer.splice(0, buffer.length)).finally(() => {
1782
+ const batch = buffer.splice(0, buffer.length);
1783
+ pendingFlushBatch = batch;
1784
+ const clearBarrier = clearPromise;
1785
+ flushPromise = (async () => {
1786
+ await clearBarrier;
1787
+ await ensureSpillDrained();
1788
+ await writeBatch(batch);
1789
+ })().finally(() => {
1790
+ if (pendingFlushBatch === batch) pendingFlushBatch = void 0;
1327
1791
  flushPromise = void 0;
1328
1792
  if (buffer.length > 0 && !closed) schedule();
1329
1793
  });
@@ -1336,14 +1800,28 @@ function indexedDbTransport(options = {}) {
1336
1800
  }, flushIntervalMs);
1337
1801
  };
1338
1802
  const onPageHide = () => {
1339
- flushPending().catch((error) => reportInternalError(error, "pagehide-flush"));
1803
+ if (spillOptions?.spillOnPageHide) writeLocalStorageSpill();
1804
+ if (flushOnPageHide) flushPending().catch((error) => reportInternalError(error, "pagehide-flush"));
1340
1805
  };
1341
1806
  const onVisibilityChange = () => {
1342
- if (typeof document !== "undefined" && document.visibilityState === "hidden") flushPending().catch((error) => reportInternalError(error, "visibility-flush"));
1807
+ if (typeof document !== "undefined" && document.visibilityState === "hidden") {
1808
+ if (spillOptions?.spillOnPageHide) writeLocalStorageSpill();
1809
+ if (flushOnPageHide) flushPending().catch((error) => reportInternalError(error, "visibility-flush"));
1810
+ }
1811
+ };
1812
+ const addVisibilityChangeListener = () => {
1813
+ if (typeof document !== "undefined" && typeof document.addEventListener === "function") {
1814
+ document.addEventListener("visibilitychange", onVisibilityChange);
1815
+ return () => document.removeEventListener("visibilitychange", onVisibilityChange);
1816
+ }
1817
+ if (typeof globalThis.addEventListener !== "function") return void 0;
1818
+ globalThis.addEventListener("visibilitychange", onVisibilityChange);
1819
+ return () => globalThis.removeEventListener?.("visibilitychange", onVisibilityChange);
1343
1820
  };
1344
- if (flushOnPageHide) {
1821
+ let removeVisibilityChangeListener;
1822
+ if (flushOnPageHide || spillOptions?.spillOnPageHide) {
1345
1823
  globalThis.addEventListener?.("pagehide", onPageHide);
1346
- globalThis.addEventListener?.("visibilitychange", onVisibilityChange);
1824
+ removeVisibilityChangeListener = addVisibilityChangeListener();
1347
1825
  }
1348
1826
  return {
1349
1827
  name: options.name ?? "indexeddb",
@@ -1374,16 +1852,36 @@ function indexedDbTransport(options = {}) {
1374
1852
  return await countEntries() ?? (await getEntries()).length;
1375
1853
  },
1376
1854
  async clear() {
1855
+ if (clearPromise) return clearPromise;
1856
+ const inFlightFlush = flushPromise;
1857
+ const inFlightDrain = spillDrainPromise;
1377
1858
  buffer.length = 0;
1378
1859
  clearTimer();
1379
- await withStore("readwrite", (store) => requestToPromise(store.clear()));
1860
+ clearLocalStorageSpill();
1861
+ clearPromise = (async () => {
1862
+ await inFlightFlush;
1863
+ await inFlightDrain;
1864
+ await withLogAndSessionStores("readwrite", async (store, sessionStore) => {
1865
+ await requestToPromise(store.clear());
1866
+ await requestToPromise(sessionStore.clear());
1867
+ });
1868
+ })().finally(() => {
1869
+ clearPromise = void 0;
1870
+ });
1871
+ return clearPromise;
1380
1872
  },
1381
1873
  async remove(ids) {
1382
1874
  await flushPending();
1383
1875
  const items = typeof ids === "string" ? [ids] : ids;
1384
1876
  if (items.length === 0) return;
1385
- await withStore("readwrite", async (store) => {
1386
- for (const id of items) await requestToPromise(store.delete(id));
1877
+ await withLogAndSessionStores("readwrite", async (store, sessionStore) => {
1878
+ const affectedSessionIds = /* @__PURE__ */ new Set();
1879
+ for (const id of items) {
1880
+ const entry = await requestToPromise(store.get(id));
1881
+ if (entry?.sessionId) affectedSessionIds.add(entry.sessionId);
1882
+ await requestToPromise(store.delete(id));
1883
+ }
1884
+ await rebuildSessionSummaries(store, sessionStore, affectedSessionIds);
1387
1885
  });
1388
1886
  },
1389
1887
  async *query(queryOptions = {}) {
@@ -1399,6 +1897,10 @@ function indexedDbTransport(options = {}) {
1399
1897
  if (event) yield event;
1400
1898
  }
1401
1899
  },
1900
+ async sessions(sessionQueryOptions = {}) {
1901
+ await flushPending();
1902
+ return listSessions(sessionQueryOptions);
1903
+ },
1402
1904
  stats() {
1403
1905
  return snapshotStats();
1404
1906
  },
@@ -1406,12 +1908,12 @@ function indexedDbTransport(options = {}) {
1406
1908
  closed = true;
1407
1909
  clearTimer();
1408
1910
  globalThis.removeEventListener?.("pagehide", onPageHide);
1409
- globalThis.removeEventListener?.("visibilitychange", onVisibilityChange);
1410
- const flushed = flushPending();
1411
- const promise = dbPromise;
1412
- dbPromise = void 0;
1413
- return flushed.finally(() => {
1414
- promise?.then((db) => db.close());
1911
+ removeVisibilityChangeListener?.();
1912
+ removeVisibilityChangeListener = void 0;
1913
+ return flushPending().finally(() => {
1914
+ const promise = dbPromise;
1915
+ dbPromise = void 0;
1916
+ return promise?.then((db) => db.close());
1415
1917
  });
1416
1918
  }
1417
1919
  };
@@ -1438,12 +1940,14 @@ function isBrowserOnline(options) {
1438
1940
  function offlineFirstTransport(remote, options = {}) {
1439
1941
  const name = options.name ?? `offline-first(${remote.name ?? "transport"})`;
1440
1942
  const replayBatchSize = normalizeBatchSize(options.replayBatchSize);
1943
+ const queueOptions = options.queueOptions;
1441
1944
  const queue = options.queue ?? indexedDbTransport({
1442
1945
  dbName: "loggerjs-offline",
1443
1946
  flushIntervalMs: 0,
1444
1947
  name: `${name}:queue`,
1445
1948
  storeName: "offline-events",
1446
- ...options.queueOptions
1949
+ ...queueOptions,
1950
+ session: queueOptions?.session ?? false
1447
1951
  });
1448
1952
  const reliableRemote = (0, _loggerjs_core.retryTransport)(remote, {
1449
1953
  maxRetries: 2,
@@ -1570,6 +2074,7 @@ const ZIP_MAX_UINT32 = 4294967295;
1570
2074
  const LOCAL_FILE_HEADER_SIGNATURE = 67324752;
1571
2075
  const CENTRAL_DIRECTORY_SIGNATURE = 33639248;
1572
2076
  const END_OF_CENTRAL_DIRECTORY_SIGNATURE = 101010256;
2077
+ const DEFAULT_SESSION_CONTEXT_KEY = "sessionId";
1573
2078
  let crc32Table;
1574
2079
  function getCrc32Table() {
1575
2080
  if (crc32Table) return crc32Table;
@@ -1709,6 +2214,56 @@ function normalizeMaxEvents(value) {
1709
2214
  if (!Number.isFinite(value)) return void 0;
1710
2215
  return Math.max(0, Math.floor(value));
1711
2216
  }
2217
+ function normalizeSessionExportOptions(value, logFileName) {
2218
+ if (!value) return void 0;
2219
+ const options = value === true ? {} : value;
2220
+ return {
2221
+ contextKey: options.contextKey ?? DEFAULT_SESSION_CONTEXT_KEY,
2222
+ directory: options.directory ?? "sessions",
2223
+ fallbackSessionId: options.fallbackSessionId ?? "unknown",
2224
+ logFileName: options.logFileName ?? logFileName
2225
+ };
2226
+ }
2227
+ function normalizeRecentExportOptions(value, format) {
2228
+ if (!value) return void 0;
2229
+ const options = value === true ? {} : value;
2230
+ return {
2231
+ logFileName: options.logFileName ?? (format === "json" ? "recent.json" : "recent.ndjson"),
2232
+ maxEvents: normalizeMaxEvents(options.maxEvents) ?? 100
2233
+ };
2234
+ }
2235
+ function logContentForLines(lines, format) {
2236
+ if (format === "json") return `[${lines.join(",")}]`;
2237
+ return lines.length > 0 ? `${lines.join("\n")}\n` : "";
2238
+ }
2239
+ function eventTimeRange(items) {
2240
+ let from;
2241
+ let to;
2242
+ for (const item of items) {
2243
+ from = from === void 0 ? item.event.time : Math.min(from, item.event.time);
2244
+ to = to === void 0 ? item.event.time : Math.max(to, item.event.time);
2245
+ }
2246
+ return from === void 0 || to === void 0 ? void 0 : {
2247
+ from,
2248
+ to
2249
+ };
2250
+ }
2251
+ function sessionIdForEvent(event, options) {
2252
+ const value = event.context?.[options.contextKey];
2253
+ return typeof value === "string" && value.length > 0 ? value : options.fallbackSessionId;
2254
+ }
2255
+ function safeZipPathSegment(value) {
2256
+ const encoded = encodeURIComponent(value).replace(/%/g, "_");
2257
+ if (!encoded || encoded === "." || encoded === "..") return "_";
2258
+ return encoded;
2259
+ }
2260
+ function sortByEventTime(items) {
2261
+ return [...items].sort((left, right) => {
2262
+ if (left.event.time !== right.event.time) return left.event.time - right.event.time;
2263
+ if (left.event.seq !== right.event.seq) return left.event.seq - right.event.seq;
2264
+ return left.event.id < right.event.id ? -1 : left.event.id > right.event.id ? 1 : 0;
2265
+ });
2266
+ }
1712
2267
  function createLogZipBlob(files, options = {}) {
1713
2268
  const normalizedFiles = createNormalizedZipFiles(files, options);
1714
2269
  const parts = [];
@@ -1731,27 +2286,72 @@ async function exportLogsToZip(source, options = {}) {
1731
2286
  const createdAt = options.createdAt ?? Date.now();
1732
2287
  const logFileName = options.logFileName ?? (format === "json" ? "logs.json" : "logs.ndjson");
1733
2288
  const maxEvents = normalizeMaxEvents(options.maxEvents);
2289
+ const sessionExportOptions = normalizeSessionExportOptions(options.groupBySession, logFileName);
2290
+ const recentExportOptions = normalizeRecentExportOptions(options.includeRecent, format);
1734
2291
  const serialize = options.serializeEvent ?? ((event) => (0, _loggerjs_core.safeJsonStringify)(event, options.stringify ?? {}));
1735
- const lines = [];
2292
+ const exported = [];
1736
2293
  let skippedCount = 0;
1737
- let from;
1738
- let to;
1739
2294
  for await (const event of resolveSource(source, options.query)) {
1740
- if (maxEvents !== void 0 && lines.length >= maxEvents) break;
2295
+ if (maxEvents !== void 0 && exported.length >= maxEvents) break;
1741
2296
  const mapped = options.mapEvent ? options.mapEvent(event) : event;
1742
2297
  if (!mapped) {
1743
2298
  skippedCount += 1;
1744
2299
  continue;
1745
2300
  }
1746
- lines.push(serialize(mapped));
1747
- from = from === void 0 ? mapped.time : Math.min(from, mapped.time);
1748
- to = to === void 0 ? mapped.time : Math.max(to, mapped.time);
2301
+ exported.push({
2302
+ event: mapped,
2303
+ line: serialize(mapped)
2304
+ });
1749
2305
  }
1750
2306
  const files = [{
1751
2307
  name: logFileName,
1752
- content: format === "json" ? `[${lines.join(",")}]` : lines.length > 0 ? `${lines.join("\n")}\n` : "",
2308
+ content: logContentForLines(exported.map((item) => item.line), format),
1753
2309
  lastModified: createdAt
1754
2310
  }];
2311
+ const sessionManifests = [];
2312
+ if (sessionExportOptions) {
2313
+ const groups = /* @__PURE__ */ new Map();
2314
+ for (const item of exported) {
2315
+ const sessionId = sessionIdForEvent(item.event, sessionExportOptions);
2316
+ const group = groups.get(sessionId) ?? [];
2317
+ group.push(item);
2318
+ groups.set(sessionId, group);
2319
+ }
2320
+ const usedFileNames = new Set(files.map((file) => normalizeZipFileName(file.name)));
2321
+ const sortedGroups = [...groups.entries()];
2322
+ sortedGroups.sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0);
2323
+ for (const [sessionId, group] of sortedGroups) {
2324
+ let fileName = normalizeZipFileName(`${sessionExportOptions.directory}/${safeZipPathSegment(sessionId)}/${sessionExportOptions.logFileName}`);
2325
+ let suffix = 1;
2326
+ while (usedFileNames.has(fileName)) {
2327
+ fileName = normalizeZipFileName(`${sessionExportOptions.directory}/${safeZipPathSegment(sessionId)}-${suffix}/${sessionExportOptions.logFileName}`);
2328
+ suffix += 1;
2329
+ }
2330
+ usedFileNames.add(fileName);
2331
+ const orderedGroup = sortByEventTime(group);
2332
+ files.push({
2333
+ name: fileName,
2334
+ content: logContentForLines(orderedGroup.map((item) => item.line), format),
2335
+ lastModified: createdAt
2336
+ });
2337
+ sessionManifests.push({
2338
+ logCount: orderedGroup.length,
2339
+ logFileName: fileName,
2340
+ sessionId,
2341
+ timeRange: eventTimeRange(orderedGroup)
2342
+ });
2343
+ }
2344
+ }
2345
+ let recentLogFileName;
2346
+ if (recentExportOptions) {
2347
+ const recentItems = recentExportOptions.maxEvents <= 0 ? [] : sortByEventTime(exported).slice(-recentExportOptions.maxEvents);
2348
+ recentLogFileName = normalizeZipFileName(recentExportOptions.logFileName);
2349
+ files.push({
2350
+ name: recentLogFileName,
2351
+ content: logContentForLines(recentItems.map((item) => item.line), format),
2352
+ lastModified: createdAt
2353
+ });
2354
+ }
1755
2355
  if (options.includeManifest ?? true) {
1756
2356
  const manifest = {
1757
2357
  schema: "loggerjs.log-export.v1",
@@ -1759,13 +2359,13 @@ async function exportLogsToZip(source, options = {}) {
1759
2359
  source: options.source,
1760
2360
  format,
1761
2361
  logFileName: normalizeZipFileName(logFileName),
1762
- logCount: lines.length,
2362
+ logCount: exported.length,
1763
2363
  skippedCount,
1764
2364
  query: options.query,
1765
- timeRange: from === void 0 || to === void 0 ? void 0 : {
1766
- from,
1767
- to
1768
- }
2365
+ recentLogFileName,
2366
+ sessionCount: sessionExportOptions ? sessionManifests.length : void 0,
2367
+ sessions: sessionExportOptions ? sessionManifests : void 0,
2368
+ timeRange: eventTimeRange(exported)
1769
2369
  };
1770
2370
  files.push({
1771
2371
  name: options.manifestFileName ?? "manifest.json",