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