@loggerjs/browser 0.5.3 → 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.
@@ -7,10 +7,12 @@ const DEFAULT_SESSION_CONTEXT_KEY = "sessionId";
7
7
  const DEFAULT_SPILL_MAX_ENTRIES = 200;
8
8
  const DEFAULT_SPILL_MAX_BYTES = 512 * 1024;
9
9
  const DEFAULT_SPILL_MAX_ORIGIN_BYTES = 5 * 1024 * 1024;
10
- const DB_VERSION = 3;
10
+ const DB_VERSION = 4;
11
11
  const ORDER_INDEX_NAME = "createdAtSeq";
12
12
  const SESSION_INDEX_NAME = "sessionId";
13
13
  const SESSION_ORDER_INDEX_NAME = "sessionIdCreatedAtSeq";
14
+ const SESSION_STORE_SUFFIX = "__sessions";
15
+ const SESSION_LAST_SEEN_INDEX_NAME = "lastSeenSessionId";
14
16
  const SPILL_SCHEMA = "loggerjs.indexeddb-spill.v1";
15
17
  function normalizePositiveInteger(value, fallback) {
16
18
  if (value === void 0 || !Number.isFinite(value)) return fallback;
@@ -135,6 +137,9 @@ function indexExists(store, name) {
135
137
  function ensureIndex(store, name, keyPath) {
136
138
  if (!indexExists(store, name)) store.createIndex(name, keyPath);
137
139
  }
140
+ function sessionStoreNameFor(storeName) {
141
+ return `${storeName}${SESSION_STORE_SUFFIX}`;
142
+ }
138
143
  function getStoreIndex(store, name) {
139
144
  const maybeStore = store;
140
145
  if (typeof maybeStore.index !== "function" || !indexExists(store, name)) return void 0;
@@ -164,6 +169,44 @@ function shouldKeepEntry(entry, options) {
164
169
  if (options.type !== void 0 && entry.type !== options.type) return false;
165
170
  return true;
166
171
  }
172
+ function addSessionSummary(sessions, entry) {
173
+ if (!entry.sessionId) return;
174
+ const current = sessions.get(entry.sessionId);
175
+ if (current) {
176
+ current.firstSeen = Math.min(current.firstSeen, entry.createdAt);
177
+ current.lastSeen = Math.max(current.lastSeen, entry.createdAt);
178
+ current.count += 1;
179
+ current.byteLength += entry.byteLength;
180
+ return;
181
+ }
182
+ sessions.set(entry.sessionId, {
183
+ byteLength: entry.byteLength,
184
+ count: 1,
185
+ firstSeen: entry.createdAt,
186
+ lastSeen: entry.createdAt,
187
+ sessionId: entry.sessionId
188
+ });
189
+ }
190
+ function mergeSessionSummary(current, incoming) {
191
+ if (!current) return incoming;
192
+ return {
193
+ byteLength: current.byteLength + incoming.byteLength,
194
+ count: current.count + incoming.count,
195
+ firstSeen: Math.min(current.firstSeen, incoming.firstSeen),
196
+ lastSeen: Math.max(current.lastSeen, incoming.lastSeen),
197
+ sessionId: incoming.sessionId
198
+ };
199
+ }
200
+ function orderSessions(sessions, order = "desc") {
201
+ const sorted = [...sessions].sort((left, right) => {
202
+ if (left.lastSeen !== right.lastSeen) return left.lastSeen - right.lastSeen;
203
+ return left.sessionId < right.sessionId ? -1 : left.sessionId > right.sessionId ? 1 : 0;
204
+ });
205
+ if (order === "asc") return sorted;
206
+ const reversed = [];
207
+ for (let index = sorted.length - 1; index >= 0; index -= 1) reversed.push(sorted[index]);
208
+ return reversed;
209
+ }
167
210
  function orderRangeForQuery(options) {
168
211
  if (typeof IDBKeyRange === "undefined") return void 0;
169
212
  if (options.sessionId !== void 0) {
@@ -195,6 +238,33 @@ function orderRangeForQuery(options) {
195
238
  if (lower) return IDBKeyRange.lowerBound(lower);
196
239
  if (upper) return IDBKeyRange.upperBound(upper);
197
240
  }
241
+ function orderRangeForSessionId(sessionId) {
242
+ if (typeof IDBKeyRange === "undefined") return void 0;
243
+ return IDBKeyRange.bound([
244
+ sessionId,
245
+ Number.MIN_SAFE_INTEGER,
246
+ Number.MIN_SAFE_INTEGER,
247
+ ""
248
+ ], [
249
+ sessionId,
250
+ Number.MAX_SAFE_INTEGER,
251
+ Number.MAX_SAFE_INTEGER,
252
+ "￿"
253
+ ]);
254
+ }
255
+ function migrateSessionSummaries(logStore, sessionStore) {
256
+ const summaries = /* @__PURE__ */ new Map();
257
+ const request = logStore.openCursor();
258
+ request.addEventListener("success", () => {
259
+ const cursor = request.result;
260
+ if (!cursor) {
261
+ for (const summary of summaries.values()) sessionStore.put(summary);
262
+ return;
263
+ }
264
+ addSessionSummary(summaries, cursor.value);
265
+ cursor.continue();
266
+ });
267
+ }
198
268
  function iterateCursor(request, visit) {
199
269
  return new Promise((resolve, reject) => {
200
270
  request.addEventListener("success", () => {
@@ -219,6 +289,7 @@ function iterateCursor(request, visit) {
219
289
  function indexedDbTransport(options = {}) {
220
290
  const dbName = options.dbName ?? DEFAULT_DB_NAME;
221
291
  const storeName = options.storeName ?? DEFAULT_STORE_NAME;
292
+ const sessionStoreName = sessionStoreNameFor(storeName);
222
293
  const codec = options.codec ?? (0, _loggerjs_core.safeJsonCodec)();
223
294
  const batchSize = normalizePositiveInteger(options.batchSize, 100);
224
295
  const flushIntervalMs = normalizePositiveInteger(options.flushIntervalMs, 1e3);
@@ -237,6 +308,7 @@ function indexedDbTransport(options = {}) {
237
308
  let indexedDBFactoryPromise;
238
309
  let timer;
239
310
  let flushPromise;
311
+ let clearPromise;
240
312
  let pendingFlushBatch;
241
313
  let spillDrainPromise;
242
314
  let localStorageSpillClearCandidate;
@@ -347,7 +419,7 @@ function indexedDbTransport(options = {}) {
347
419
  if (!idb) throw new Error("IndexedDB is not available for indexedDbTransport");
348
420
  return new Promise((resolve, reject) => {
349
421
  const request = idb.open(dbName, DB_VERSION);
350
- request.addEventListener("upgradeneeded", () => {
422
+ request.addEventListener("upgradeneeded", (event) => {
351
423
  const db = request.result;
352
424
  const store = db.objectStoreNames.contains(storeName) ? request.transaction?.objectStore(storeName) : db.createObjectStore(storeName, { keyPath: "id" });
353
425
  if (!store) return;
@@ -367,6 +439,10 @@ function indexedDbTransport(options = {}) {
367
439
  "seq",
368
440
  "id"
369
441
  ]);
442
+ const sessionStore = db.objectStoreNames.contains(sessionStoreName) ? request.transaction?.objectStore(sessionStoreName) : db.createObjectStore(sessionStoreName, { keyPath: "sessionId" });
443
+ if (!sessionStore) return;
444
+ ensureIndex(sessionStore, SESSION_LAST_SEEN_INDEX_NAME, ["lastSeen", "sessionId"]);
445
+ if (event.oldVersion < 4) migrateSessionSummaries(store, sessionStore);
370
446
  });
371
447
  request.addEventListener("success", () => {
372
448
  statsState.databaseOpenCount += 1;
@@ -377,13 +453,13 @@ function indexedDbTransport(options = {}) {
377
453
  });
378
454
  return dbPromise;
379
455
  };
380
- const createTransaction = (db, mode) => {
381
- if (mode !== "readwrite" || !transactionOptions) return db.transaction(storeName, mode);
456
+ const createTransaction = (db, mode, stores = storeName) => {
457
+ if (mode !== "readwrite" || !transactionOptions) return db.transaction(stores, mode);
382
458
  try {
383
- return db.transaction(storeName, mode, transactionOptions);
459
+ return db.transaction(stores, mode, transactionOptions);
384
460
  } catch {
385
461
  statsState.transactionOptionFallbacks += 1;
386
- return db.transaction(storeName, mode);
462
+ return db.transaction(stores, mode);
387
463
  }
388
464
  };
389
465
  const withStore = async (mode, run) => {
@@ -400,6 +476,57 @@ function indexedDbTransport(options = {}) {
400
476
  throw error;
401
477
  }
402
478
  };
479
+ const withLogAndSessionStores = async (mode, run) => {
480
+ const tx = createTransaction(await openDb(), mode, [storeName, sessionStoreName]);
481
+ const settled = transactionToPromise(tx);
482
+ try {
483
+ const result = await run(tx.objectStore(storeName), tx.objectStore(sessionStoreName), tx);
484
+ await settled;
485
+ return result;
486
+ } catch (error) {
487
+ if (tx.error === null) try {
488
+ tx.abort();
489
+ } catch {}
490
+ throw error;
491
+ }
492
+ };
493
+ const withSessionStore = async (mode, run) => {
494
+ const tx = createTransaction(await openDb(), mode, sessionStoreName);
495
+ const settled = transactionToPromise(tx);
496
+ try {
497
+ const result = await run(tx.objectStore(sessionStoreName), tx);
498
+ await settled;
499
+ return result;
500
+ } catch (error) {
501
+ if (tx.error === null) try {
502
+ tx.abort();
503
+ } catch {}
504
+ throw error;
505
+ }
506
+ };
507
+ const mergeSessionSummaries = async (sessionStore, entries) => {
508
+ const summaries = /* @__PURE__ */ new Map();
509
+ for (const entry of entries) addSessionSummary(summaries, entry);
510
+ for (const summary of summaries.values()) {
511
+ const current = await requestToPromise(sessionStore.get(summary.sessionId));
512
+ await requestToPromise(sessionStore.put(mergeSessionSummary(current, summary)));
513
+ }
514
+ };
515
+ const rebuildSessionSummaries = async (store, sessionStore, sessionIds) => {
516
+ if (sessionIds.size === 0) return;
517
+ const index = getStoreIndex(store, SESSION_ORDER_INDEX_NAME);
518
+ for (const sessionId of sessionIds) {
519
+ const summaries = /* @__PURE__ */ new Map();
520
+ const range = orderRangeForSessionId(sessionId);
521
+ await iterateCursor(index && range ? index.openCursor(range) : store.openCursor(), (cursor) => {
522
+ const entry = cursor.value;
523
+ if (entry.sessionId === sessionId) addSessionSummary(summaries, entry);
524
+ });
525
+ const summary = summaries.get(sessionId);
526
+ if (summary) await requestToPromise(sessionStore.put(summary));
527
+ else await requestToPromise(sessionStore.delete(sessionId));
528
+ }
529
+ };
403
530
  const getEntries = async () => orderEntries(await withStore("readonly", (store) => requestToPromise(store.getAll())));
404
531
  const countEntries = async () => withStore("readonly", (store) => {
405
532
  const maybeStore = store;
@@ -408,25 +535,31 @@ function indexedDbTransport(options = {}) {
408
535
  });
409
536
  const deleteEntries = async (entries, reason) => {
410
537
  if (entries.length === 0) return;
411
- await withStore("readwrite", async (store) => {
538
+ await withLogAndSessionStores("readwrite", async (store, sessionStore) => {
539
+ const affectedSessionIds = /* @__PURE__ */ new Set();
412
540
  for (const entry of entries) {
413
541
  await requestToPromise(store.delete(entry.id));
542
+ if (entry.sessionId) affectedSessionIds.add(entry.sessionId);
414
543
  dropPersistedEntry(entry, reason);
415
544
  }
545
+ await rebuildSessionSummaries(store, sessionStore, affectedSessionIds);
416
546
  });
417
547
  };
418
- const deleteByCursor = async (reason, openCursor, shouldDelete) => withStore("readwrite", async (store) => {
548
+ const deleteByCursor = async (reason, openCursor, shouldDelete) => withLogAndSessionStores("readwrite", async (store, sessionStore) => {
419
549
  const request = openCursor(store);
420
550
  if (!request) return void 0;
421
551
  let deleted = 0;
552
+ const affectedSessionIds = /* @__PURE__ */ new Set();
422
553
  await iterateCursor(request, (cursor) => {
423
554
  const entry = cursor.value;
424
555
  if (!shouldDelete(entry, deleted)) return false;
425
556
  cursor.delete();
426
557
  deleted += 1;
558
+ if (entry.sessionId) affectedSessionIds.add(entry.sessionId);
427
559
  dropPersistedEntry(entry, reason);
428
560
  return true;
429
561
  });
562
+ await rebuildSessionSummaries(store, sessionStore, affectedSessionIds);
430
563
  return deleted;
431
564
  });
432
565
  const deleteExpiredByCursor = async (cutoff) => {
@@ -471,29 +604,18 @@ function indexedDbTransport(options = {}) {
471
604
  return entries;
472
605
  });
473
606
  const listSessions = async (sessionQueryOptions = {}) => {
474
- const sessions = /* @__PURE__ */ new Map();
475
- for (const entry of await getEntries()) {
476
- if (!entry.sessionId) continue;
477
- const current = sessions.get(entry.sessionId);
478
- if (current) {
479
- current.firstSeen = Math.min(current.firstSeen, entry.createdAt);
480
- current.lastSeen = Math.max(current.lastSeen, entry.createdAt);
481
- current.count += 1;
482
- current.byteLength += entry.byteLength;
483
- } else sessions.set(entry.sessionId, {
484
- byteLength: entry.byteLength,
485
- count: 1,
486
- firstSeen: entry.createdAt,
487
- lastSeen: entry.createdAt,
488
- sessionId: entry.sessionId
607
+ const limit = sessionQueryOptions.limit;
608
+ if (limit !== void 0 && limit <= 0) return [];
609
+ return withSessionStore("readonly", async (sessionStore) => {
610
+ const sessions = [];
611
+ const index = getStoreIndex(sessionStore, SESSION_LAST_SEEN_INDEX_NAME);
612
+ if (!index) return orderSessions(await requestToPromise(sessionStore.getAll()), sessionQueryOptions.order).slice(0, limit);
613
+ await iterateCursor(index.openCursor(void 0, sessionQueryOptions.order === "asc" ? "next" : "prev"), (cursor) => {
614
+ sessions.push(cursor.value);
615
+ return limit === void 0 || sessions.length < limit;
489
616
  });
490
- }
491
- const ordered = [...sessions.values()].sort((left, right) => {
492
- if (left.lastSeen !== right.lastSeen) return left.lastSeen - right.lastSeen;
493
- return left.sessionId < right.sessionId ? -1 : left.sessionId > right.sessionId ? 1 : 0;
617
+ return sessions;
494
618
  });
495
- if (sessionQueryOptions.order !== "asc") ordered.reverse();
496
- return ordered.slice(0, sessionQueryOptions.limit);
497
619
  };
498
620
  const pruneWithEntries = async () => {
499
621
  const now = Date.now();
@@ -618,8 +740,16 @@ function indexedDbTransport(options = {}) {
618
740
  statsState.lastFlushBatchSize = events.length;
619
741
  const entries = events.map(eventToEntry);
620
742
  try {
621
- await withStore("readwrite", (store) => {
622
- for (const entry of entries) store.put(entry);
743
+ await withLogAndSessionStores("readwrite", async (store, sessionStore) => {
744
+ const rebuildSessionIds = /* @__PURE__ */ new Set();
745
+ for (const entry of entries) {
746
+ const existing = await requestToPromise(store.get(entry.id));
747
+ if (existing?.sessionId) rebuildSessionIds.add(existing.sessionId);
748
+ if (existing && entry.sessionId) rebuildSessionIds.add(entry.sessionId);
749
+ await requestToPromise(store.put(entry));
750
+ }
751
+ await rebuildSessionSummaries(store, sessionStore, rebuildSessionIds);
752
+ await mergeSessionSummaries(sessionStore, entries.filter((entry) => !entry.sessionId || !rebuildSessionIds.has(entry.sessionId)));
623
753
  });
624
754
  statsState.persisted += entries.length;
625
755
  (0, _loggerjs_core.incrementLoggerMetaCounter)("transport.indexeddb.persisted", entries.length);
@@ -760,7 +890,9 @@ function indexedDbTransport(options = {}) {
760
890
  clearTimer();
761
891
  const batch = buffer.splice(0, buffer.length);
762
892
  pendingFlushBatch = batch;
893
+ const clearBarrier = clearPromise;
763
894
  flushPromise = (async () => {
895
+ await clearBarrier;
764
896
  await ensureSpillDrained();
765
897
  await writeBatch(batch);
766
898
  })().finally(() => {
@@ -829,17 +961,36 @@ function indexedDbTransport(options = {}) {
829
961
  return await countEntries() ?? (await getEntries()).length;
830
962
  },
831
963
  async clear() {
964
+ if (clearPromise) return clearPromise;
965
+ const inFlightFlush = flushPromise;
966
+ const inFlightDrain = spillDrainPromise;
832
967
  buffer.length = 0;
833
968
  clearTimer();
834
969
  clearLocalStorageSpill();
835
- await withStore("readwrite", (store) => requestToPromise(store.clear()));
970
+ clearPromise = (async () => {
971
+ await inFlightFlush;
972
+ await inFlightDrain;
973
+ await withLogAndSessionStores("readwrite", async (store, sessionStore) => {
974
+ await requestToPromise(store.clear());
975
+ await requestToPromise(sessionStore.clear());
976
+ });
977
+ })().finally(() => {
978
+ clearPromise = void 0;
979
+ });
980
+ return clearPromise;
836
981
  },
837
982
  async remove(ids) {
838
983
  await flushPending();
839
984
  const items = typeof ids === "string" ? [ids] : ids;
840
985
  if (items.length === 0) return;
841
- await withStore("readwrite", async (store) => {
842
- for (const id of items) await requestToPromise(store.delete(id));
986
+ await withLogAndSessionStores("readwrite", async (store, sessionStore) => {
987
+ const affectedSessionIds = /* @__PURE__ */ new Set();
988
+ for (const id of items) {
989
+ const entry = await requestToPromise(store.get(id));
990
+ if (entry?.sessionId) affectedSessionIds.add(entry.sessionId);
991
+ await requestToPromise(store.delete(id));
992
+ }
993
+ await rebuildSessionSummaries(store, sessionStore, affectedSessionIds);
843
994
  });
844
995
  },
845
996
  async *query(queryOptions = {}) {