@loggerjs/browser 0.5.3 → 0.5.5

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.js CHANGED
@@ -169,6 +169,7 @@ function remainingEvents(chunks, startIndex) {
169
169
  }
170
170
  function browserHttpTransport(options) {
171
171
  const codec = options.codec ?? safeJsonCodec();
172
+ const beaconCodec = options.beaconCodec ?? codec;
172
173
  const queue = [];
173
174
  const maxBatchSize = options.maxBatchSize ?? 50;
174
175
  const flushIntervalMs = options.flushIntervalMs ?? 2e3;
@@ -213,7 +214,7 @@ function browserHttpTransport(options) {
213
214
  let currentPayload;
214
215
  for (const event of batch) {
215
216
  const candidateEvents = [...currentEvents, event];
216
- const candidatePayload = codec.encode(candidateEvents);
217
+ const candidatePayload = beaconCodec.encode(candidateEvents);
217
218
  if (payloadByteLength$1(candidatePayload) <= beaconMaxBytes) {
218
219
  currentEvents = candidateEvents;
219
220
  currentPayload = candidatePayload;
@@ -223,7 +224,7 @@ function browserHttpTransport(options) {
223
224
  events: currentEvents,
224
225
  payload: currentPayload
225
226
  });
226
- const singlePayload = codec.encode([event]);
227
+ const singlePayload = beaconCodec.encode([event]);
227
228
  if (payloadByteLength$1(singlePayload) <= beaconMaxBytes) {
228
229
  currentEvents = [event];
229
230
  currentPayload = singlePayload;
@@ -248,7 +249,7 @@ function browserHttpTransport(options) {
248
249
  for (let index = 0; index < chunks.length; index++) {
249
250
  const chunk = chunks[index];
250
251
  if (!chunk) continue;
251
- if (!navigator.sendBeacon(options.url, payloadToBeaconBody(chunk.payload, codec.contentType))) return {
252
+ if (!navigator.sendBeacon(options.url, payloadToBeaconBody(chunk.payload, beaconCodec.contentType))) return {
252
253
  ok: false,
253
254
  remaining: remainingEvents(chunks, index)
254
255
  };
@@ -898,10 +899,12 @@ const DEFAULT_SESSION_CONTEXT_KEY$1 = "sessionId";
898
899
  const DEFAULT_SPILL_MAX_ENTRIES = 200;
899
900
  const DEFAULT_SPILL_MAX_BYTES = 512 * 1024;
900
901
  const DEFAULT_SPILL_MAX_ORIGIN_BYTES = 5 * 1024 * 1024;
901
- const DB_VERSION = 3;
902
+ const DB_VERSION = 4;
902
903
  const ORDER_INDEX_NAME = "createdAtSeq";
903
904
  const SESSION_INDEX_NAME = "sessionId";
904
905
  const SESSION_ORDER_INDEX_NAME = "sessionIdCreatedAtSeq";
906
+ const SESSION_STORE_SUFFIX = "__sessions";
907
+ const SESSION_LAST_SEEN_INDEX_NAME = "lastSeenSessionId";
905
908
  const SPILL_SCHEMA = "loggerjs.indexeddb-spill.v1";
906
909
  function normalizePositiveInteger(value, fallback) {
907
910
  if (value === void 0 || !Number.isFinite(value)) return fallback;
@@ -1026,6 +1029,9 @@ function indexExists(store, name) {
1026
1029
  function ensureIndex(store, name, keyPath) {
1027
1030
  if (!indexExists(store, name)) store.createIndex(name, keyPath);
1028
1031
  }
1032
+ function sessionStoreNameFor(storeName) {
1033
+ return `${storeName}${SESSION_STORE_SUFFIX}`;
1034
+ }
1029
1035
  function getStoreIndex(store, name) {
1030
1036
  const maybeStore = store;
1031
1037
  if (typeof maybeStore.index !== "function" || !indexExists(store, name)) return void 0;
@@ -1055,6 +1061,44 @@ function shouldKeepEntry(entry, options) {
1055
1061
  if (options.type !== void 0 && entry.type !== options.type) return false;
1056
1062
  return true;
1057
1063
  }
1064
+ function addSessionSummary(sessions, entry) {
1065
+ if (!entry.sessionId) return;
1066
+ const current = sessions.get(entry.sessionId);
1067
+ if (current) {
1068
+ current.firstSeen = Math.min(current.firstSeen, entry.createdAt);
1069
+ current.lastSeen = Math.max(current.lastSeen, entry.createdAt);
1070
+ current.count += 1;
1071
+ current.byteLength += entry.byteLength;
1072
+ return;
1073
+ }
1074
+ sessions.set(entry.sessionId, {
1075
+ byteLength: entry.byteLength,
1076
+ count: 1,
1077
+ firstSeen: entry.createdAt,
1078
+ lastSeen: entry.createdAt,
1079
+ sessionId: entry.sessionId
1080
+ });
1081
+ }
1082
+ function mergeSessionSummary(current, incoming) {
1083
+ if (!current) return incoming;
1084
+ return {
1085
+ byteLength: current.byteLength + incoming.byteLength,
1086
+ count: current.count + incoming.count,
1087
+ firstSeen: Math.min(current.firstSeen, incoming.firstSeen),
1088
+ lastSeen: Math.max(current.lastSeen, incoming.lastSeen),
1089
+ sessionId: incoming.sessionId
1090
+ };
1091
+ }
1092
+ function orderSessions(sessions, order = "desc") {
1093
+ const sorted = [...sessions].sort((left, right) => {
1094
+ if (left.lastSeen !== right.lastSeen) return left.lastSeen - right.lastSeen;
1095
+ return left.sessionId < right.sessionId ? -1 : left.sessionId > right.sessionId ? 1 : 0;
1096
+ });
1097
+ if (order === "asc") return sorted;
1098
+ const reversed = [];
1099
+ for (let index = sorted.length - 1; index >= 0; index -= 1) reversed.push(sorted[index]);
1100
+ return reversed;
1101
+ }
1058
1102
  function orderRangeForQuery(options) {
1059
1103
  if (typeof IDBKeyRange === "undefined") return void 0;
1060
1104
  if (options.sessionId !== void 0) {
@@ -1086,6 +1130,33 @@ function orderRangeForQuery(options) {
1086
1130
  if (lower) return IDBKeyRange.lowerBound(lower);
1087
1131
  if (upper) return IDBKeyRange.upperBound(upper);
1088
1132
  }
1133
+ function orderRangeForSessionId(sessionId) {
1134
+ if (typeof IDBKeyRange === "undefined") return void 0;
1135
+ return IDBKeyRange.bound([
1136
+ sessionId,
1137
+ Number.MIN_SAFE_INTEGER,
1138
+ Number.MIN_SAFE_INTEGER,
1139
+ ""
1140
+ ], [
1141
+ sessionId,
1142
+ Number.MAX_SAFE_INTEGER,
1143
+ Number.MAX_SAFE_INTEGER,
1144
+ "￿"
1145
+ ]);
1146
+ }
1147
+ function migrateSessionSummaries(logStore, sessionStore) {
1148
+ const summaries = /* @__PURE__ */ new Map();
1149
+ const request = logStore.openCursor();
1150
+ request.addEventListener("success", () => {
1151
+ const cursor = request.result;
1152
+ if (!cursor) {
1153
+ for (const summary of summaries.values()) sessionStore.put(summary);
1154
+ return;
1155
+ }
1156
+ addSessionSummary(summaries, cursor.value);
1157
+ cursor.continue();
1158
+ });
1159
+ }
1089
1160
  function iterateCursor(request, visit) {
1090
1161
  return new Promise((resolve, reject) => {
1091
1162
  request.addEventListener("success", () => {
@@ -1110,6 +1181,7 @@ function iterateCursor(request, visit) {
1110
1181
  function indexedDbTransport(options = {}) {
1111
1182
  const dbName = options.dbName ?? DEFAULT_DB_NAME;
1112
1183
  const storeName = options.storeName ?? DEFAULT_STORE_NAME;
1184
+ const sessionStoreName = sessionStoreNameFor(storeName);
1113
1185
  const codec = options.codec ?? safeJsonCodec();
1114
1186
  const batchSize = normalizePositiveInteger(options.batchSize, 100);
1115
1187
  const flushIntervalMs = normalizePositiveInteger(options.flushIntervalMs, 1e3);
@@ -1128,6 +1200,7 @@ function indexedDbTransport(options = {}) {
1128
1200
  let indexedDBFactoryPromise;
1129
1201
  let timer;
1130
1202
  let flushPromise;
1203
+ let clearPromise;
1131
1204
  let pendingFlushBatch;
1132
1205
  let spillDrainPromise;
1133
1206
  let localStorageSpillClearCandidate;
@@ -1238,7 +1311,7 @@ function indexedDbTransport(options = {}) {
1238
1311
  if (!idb) throw new Error("IndexedDB is not available for indexedDbTransport");
1239
1312
  return new Promise((resolve, reject) => {
1240
1313
  const request = idb.open(dbName, DB_VERSION);
1241
- request.addEventListener("upgradeneeded", () => {
1314
+ request.addEventListener("upgradeneeded", (event) => {
1242
1315
  const db = request.result;
1243
1316
  const store = db.objectStoreNames.contains(storeName) ? request.transaction?.objectStore(storeName) : db.createObjectStore(storeName, { keyPath: "id" });
1244
1317
  if (!store) return;
@@ -1258,6 +1331,10 @@ function indexedDbTransport(options = {}) {
1258
1331
  "seq",
1259
1332
  "id"
1260
1333
  ]);
1334
+ const sessionStore = db.objectStoreNames.contains(sessionStoreName) ? request.transaction?.objectStore(sessionStoreName) : db.createObjectStore(sessionStoreName, { keyPath: "sessionId" });
1335
+ if (!sessionStore) return;
1336
+ ensureIndex(sessionStore, SESSION_LAST_SEEN_INDEX_NAME, ["lastSeen", "sessionId"]);
1337
+ if (event.oldVersion < 4) migrateSessionSummaries(store, sessionStore);
1261
1338
  });
1262
1339
  request.addEventListener("success", () => {
1263
1340
  statsState.databaseOpenCount += 1;
@@ -1268,13 +1345,13 @@ function indexedDbTransport(options = {}) {
1268
1345
  });
1269
1346
  return dbPromise;
1270
1347
  };
1271
- const createTransaction = (db, mode) => {
1272
- if (mode !== "readwrite" || !transactionOptions) return db.transaction(storeName, mode);
1348
+ const createTransaction = (db, mode, stores = storeName) => {
1349
+ if (mode !== "readwrite" || !transactionOptions) return db.transaction(stores, mode);
1273
1350
  try {
1274
- return db.transaction(storeName, mode, transactionOptions);
1351
+ return db.transaction(stores, mode, transactionOptions);
1275
1352
  } catch {
1276
1353
  statsState.transactionOptionFallbacks += 1;
1277
- return db.transaction(storeName, mode);
1354
+ return db.transaction(stores, mode);
1278
1355
  }
1279
1356
  };
1280
1357
  const withStore = async (mode, run) => {
@@ -1291,6 +1368,57 @@ function indexedDbTransport(options = {}) {
1291
1368
  throw error;
1292
1369
  }
1293
1370
  };
1371
+ const withLogAndSessionStores = async (mode, run) => {
1372
+ const tx = createTransaction(await openDb(), mode, [storeName, sessionStoreName]);
1373
+ const settled = transactionToPromise(tx);
1374
+ try {
1375
+ const result = await run(tx.objectStore(storeName), tx.objectStore(sessionStoreName), tx);
1376
+ await settled;
1377
+ return result;
1378
+ } catch (error) {
1379
+ if (tx.error === null) try {
1380
+ tx.abort();
1381
+ } catch {}
1382
+ throw error;
1383
+ }
1384
+ };
1385
+ const withSessionStore = async (mode, run) => {
1386
+ const tx = createTransaction(await openDb(), mode, sessionStoreName);
1387
+ const settled = transactionToPromise(tx);
1388
+ try {
1389
+ const result = await run(tx.objectStore(sessionStoreName), tx);
1390
+ await settled;
1391
+ return result;
1392
+ } catch (error) {
1393
+ if (tx.error === null) try {
1394
+ tx.abort();
1395
+ } catch {}
1396
+ throw error;
1397
+ }
1398
+ };
1399
+ const mergeSessionSummaries = async (sessionStore, entries) => {
1400
+ const summaries = /* @__PURE__ */ new Map();
1401
+ for (const entry of entries) addSessionSummary(summaries, entry);
1402
+ for (const summary of summaries.values()) {
1403
+ const current = await requestToPromise(sessionStore.get(summary.sessionId));
1404
+ await requestToPromise(sessionStore.put(mergeSessionSummary(current, summary)));
1405
+ }
1406
+ };
1407
+ const rebuildSessionSummaries = async (store, sessionStore, sessionIds) => {
1408
+ if (sessionIds.size === 0) return;
1409
+ const index = getStoreIndex(store, SESSION_ORDER_INDEX_NAME);
1410
+ for (const sessionId of sessionIds) {
1411
+ const summaries = /* @__PURE__ */ new Map();
1412
+ const range = orderRangeForSessionId(sessionId);
1413
+ await iterateCursor(index && range ? index.openCursor(range) : store.openCursor(), (cursor) => {
1414
+ const entry = cursor.value;
1415
+ if (entry.sessionId === sessionId) addSessionSummary(summaries, entry);
1416
+ });
1417
+ const summary = summaries.get(sessionId);
1418
+ if (summary) await requestToPromise(sessionStore.put(summary));
1419
+ else await requestToPromise(sessionStore.delete(sessionId));
1420
+ }
1421
+ };
1294
1422
  const getEntries = async () => orderEntries(await withStore("readonly", (store) => requestToPromise(store.getAll())));
1295
1423
  const countEntries = async () => withStore("readonly", (store) => {
1296
1424
  const maybeStore = store;
@@ -1299,25 +1427,31 @@ function indexedDbTransport(options = {}) {
1299
1427
  });
1300
1428
  const deleteEntries = async (entries, reason) => {
1301
1429
  if (entries.length === 0) return;
1302
- await withStore("readwrite", async (store) => {
1430
+ await withLogAndSessionStores("readwrite", async (store, sessionStore) => {
1431
+ const affectedSessionIds = /* @__PURE__ */ new Set();
1303
1432
  for (const entry of entries) {
1304
1433
  await requestToPromise(store.delete(entry.id));
1434
+ if (entry.sessionId) affectedSessionIds.add(entry.sessionId);
1305
1435
  dropPersistedEntry(entry, reason);
1306
1436
  }
1437
+ await rebuildSessionSummaries(store, sessionStore, affectedSessionIds);
1307
1438
  });
1308
1439
  };
1309
- const deleteByCursor = async (reason, openCursor, shouldDelete) => withStore("readwrite", async (store) => {
1440
+ const deleteByCursor = async (reason, openCursor, shouldDelete) => withLogAndSessionStores("readwrite", async (store, sessionStore) => {
1310
1441
  const request = openCursor(store);
1311
1442
  if (!request) return void 0;
1312
1443
  let deleted = 0;
1444
+ const affectedSessionIds = /* @__PURE__ */ new Set();
1313
1445
  await iterateCursor(request, (cursor) => {
1314
1446
  const entry = cursor.value;
1315
1447
  if (!shouldDelete(entry, deleted)) return false;
1316
1448
  cursor.delete();
1317
1449
  deleted += 1;
1450
+ if (entry.sessionId) affectedSessionIds.add(entry.sessionId);
1318
1451
  dropPersistedEntry(entry, reason);
1319
1452
  return true;
1320
1453
  });
1454
+ await rebuildSessionSummaries(store, sessionStore, affectedSessionIds);
1321
1455
  return deleted;
1322
1456
  });
1323
1457
  const deleteExpiredByCursor = async (cutoff) => {
@@ -1362,29 +1496,18 @@ function indexedDbTransport(options = {}) {
1362
1496
  return entries;
1363
1497
  });
1364
1498
  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
1499
+ const limit = sessionQueryOptions.limit;
1500
+ if (limit !== void 0 && limit <= 0) return [];
1501
+ return withSessionStore("readonly", async (sessionStore) => {
1502
+ const sessions = [];
1503
+ const index = getStoreIndex(sessionStore, SESSION_LAST_SEEN_INDEX_NAME);
1504
+ if (!index) return orderSessions(await requestToPromise(sessionStore.getAll()), sessionQueryOptions.order).slice(0, limit);
1505
+ await iterateCursor(index.openCursor(void 0, sessionQueryOptions.order === "asc" ? "next" : "prev"), (cursor) => {
1506
+ sessions.push(cursor.value);
1507
+ return limit === void 0 || sessions.length < limit;
1380
1508
  });
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;
1509
+ return sessions;
1385
1510
  });
1386
- if (sessionQueryOptions.order !== "asc") ordered.reverse();
1387
- return ordered.slice(0, sessionQueryOptions.limit);
1388
1511
  };
1389
1512
  const pruneWithEntries = async () => {
1390
1513
  const now = Date.now();
@@ -1509,8 +1632,16 @@ function indexedDbTransport(options = {}) {
1509
1632
  statsState.lastFlushBatchSize = events.length;
1510
1633
  const entries = events.map(eventToEntry);
1511
1634
  try {
1512
- await withStore("readwrite", (store) => {
1513
- for (const entry of entries) store.put(entry);
1635
+ await withLogAndSessionStores("readwrite", async (store, sessionStore) => {
1636
+ const rebuildSessionIds = /* @__PURE__ */ new Set();
1637
+ for (const entry of entries) {
1638
+ const existing = await requestToPromise(store.get(entry.id));
1639
+ if (existing?.sessionId) rebuildSessionIds.add(existing.sessionId);
1640
+ if (existing && entry.sessionId) rebuildSessionIds.add(entry.sessionId);
1641
+ await requestToPromise(store.put(entry));
1642
+ }
1643
+ await rebuildSessionSummaries(store, sessionStore, rebuildSessionIds);
1644
+ await mergeSessionSummaries(sessionStore, entries.filter((entry) => !entry.sessionId || !rebuildSessionIds.has(entry.sessionId)));
1514
1645
  });
1515
1646
  statsState.persisted += entries.length;
1516
1647
  incrementLoggerMetaCounter("transport.indexeddb.persisted", entries.length);
@@ -1651,7 +1782,9 @@ function indexedDbTransport(options = {}) {
1651
1782
  clearTimer();
1652
1783
  const batch = buffer.splice(0, buffer.length);
1653
1784
  pendingFlushBatch = batch;
1785
+ const clearBarrier = clearPromise;
1654
1786
  flushPromise = (async () => {
1787
+ await clearBarrier;
1655
1788
  await ensureSpillDrained();
1656
1789
  await writeBatch(batch);
1657
1790
  })().finally(() => {
@@ -1720,17 +1853,36 @@ function indexedDbTransport(options = {}) {
1720
1853
  return await countEntries() ?? (await getEntries()).length;
1721
1854
  },
1722
1855
  async clear() {
1856
+ if (clearPromise) return clearPromise;
1857
+ const inFlightFlush = flushPromise;
1858
+ const inFlightDrain = spillDrainPromise;
1723
1859
  buffer.length = 0;
1724
1860
  clearTimer();
1725
1861
  clearLocalStorageSpill();
1726
- await withStore("readwrite", (store) => requestToPromise(store.clear()));
1862
+ clearPromise = (async () => {
1863
+ await inFlightFlush;
1864
+ await inFlightDrain;
1865
+ await withLogAndSessionStores("readwrite", async (store, sessionStore) => {
1866
+ await requestToPromise(store.clear());
1867
+ await requestToPromise(sessionStore.clear());
1868
+ });
1869
+ })().finally(() => {
1870
+ clearPromise = void 0;
1871
+ });
1872
+ return clearPromise;
1727
1873
  },
1728
1874
  async remove(ids) {
1729
1875
  await flushPending();
1730
1876
  const items = typeof ids === "string" ? [ids] : ids;
1731
1877
  if (items.length === 0) return;
1732
- await withStore("readwrite", async (store) => {
1733
- for (const id of items) await requestToPromise(store.delete(id));
1878
+ await withLogAndSessionStores("readwrite", async (store, sessionStore) => {
1879
+ const affectedSessionIds = /* @__PURE__ */ new Set();
1880
+ for (const id of items) {
1881
+ const entry = await requestToPromise(store.get(id));
1882
+ if (entry?.sessionId) affectedSessionIds.add(entry.sessionId);
1883
+ await requestToPromise(store.delete(id));
1884
+ }
1885
+ await rebuildSessionSummaries(store, sessionStore, affectedSessionIds);
1734
1886
  });
1735
1887
  },
1736
1888
  async *query(queryOptions = {}) {