@loggerjs/browser 0.5.5 → 0.6.0

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
@@ -172,6 +172,7 @@ function browserHttpTransport(options) {
172
172
  const beaconCodec = options.beaconCodec ?? codec;
173
173
  const queue = [];
174
174
  const maxBatchSize = options.maxBatchSize ?? 50;
175
+ if (!Number.isSafeInteger(maxBatchSize) || maxBatchSize <= 0) throw new RangeError("maxBatchSize must be a positive safe integer");
175
176
  const flushIntervalMs = options.flushIntervalMs ?? 2e3;
176
177
  const maxQueueSize = options.maxQueueSize ?? 1e3;
177
178
  const dropPolicy = options.dropPolicy ?? "drop-oldest";
@@ -184,8 +185,9 @@ function browserHttpTransport(options) {
184
185
  const fetchFn = options.fetchFn ?? globalThis.fetch?.bind(globalThis);
185
186
  let offlineEntrySeq = 0;
186
187
  let timer;
187
- let flushing = false;
188
- let replayingOffline = false;
188
+ let activeFlush;
189
+ let flushingBeacon = false;
190
+ let replayPromise;
189
191
  let lastContext;
190
192
  const headers = (payloadHeaders, contentType = codec.contentType) => ({
191
193
  "content-type": contentType,
@@ -210,9 +212,18 @@ function browserHttpTransport(options) {
210
212
  };
211
213
  const createBeaconChunks = (batch) => {
212
214
  const chunks = [];
215
+ const oversized = [];
213
216
  let currentEvents = [];
214
217
  let currentPayload;
215
218
  for (const event of batch) {
219
+ if (currentEvents.length === maxBatchSize && currentPayload !== void 0) {
220
+ chunks.push({
221
+ events: currentEvents,
222
+ payload: currentPayload
223
+ });
224
+ currentEvents = [];
225
+ currentPayload = void 0;
226
+ }
216
227
  const candidateEvents = [...currentEvents, event];
217
228
  const candidatePayload = beaconCodec.encode(candidateEvents);
218
229
  if (payloadByteLength$1(candidatePayload) <= beaconMaxBytes) {
@@ -220,7 +231,7 @@ function browserHttpTransport(options) {
220
231
  currentPayload = candidatePayload;
221
232
  continue;
222
233
  }
223
- if (currentEvents.length > 0 && currentPayload) chunks.push({
234
+ if (currentEvents.length > 0 && currentPayload !== void 0) chunks.push({
224
235
  events: currentEvents,
225
236
  payload: currentPayload
226
237
  });
@@ -231,33 +242,32 @@ function browserHttpTransport(options) {
231
242
  } else {
232
243
  currentEvents = [];
233
244
  currentPayload = void 0;
234
- reportDrop(event, "beacon-too-large");
245
+ oversized.push(event);
235
246
  }
236
247
  }
237
- if (currentEvents.length > 0 && currentPayload) chunks.push({
248
+ if (currentEvents.length > 0 && currentPayload !== void 0) chunks.push({
238
249
  events: currentEvents,
239
250
  payload: currentPayload
240
251
  });
252
+ for (const event of oversized) reportDrop(event, "beacon-too-large");
241
253
  return chunks;
242
254
  };
243
255
  const sendBeaconBatch = (batch) => {
244
- if (typeof navigator === "undefined" || !navigator.sendBeacon) return {
245
- ok: false,
246
- remaining: batch
247
- };
256
+ if (typeof navigator === "undefined" || !navigator.sendBeacon) return { remaining: batch };
248
257
  const chunks = createBeaconChunks(batch);
249
258
  for (let index = 0; index < chunks.length; index++) {
250
259
  const chunk = chunks[index];
251
260
  if (!chunk) continue;
252
- if (!navigator.sendBeacon(options.url, payloadToBeaconBody(chunk.payload, beaconCodec.contentType))) return {
253
- ok: false,
254
- remaining: remainingEvents(chunks, index)
255
- };
261
+ try {
262
+ if (!navigator.sendBeacon(options.url, payloadToBeaconBody(chunk.payload, beaconCodec.contentType))) return { remaining: remainingEvents(chunks, index) };
263
+ } catch (error) {
264
+ return {
265
+ remaining: remainingEvents(chunks, index),
266
+ failure: { error }
267
+ };
268
+ }
256
269
  }
257
- return {
258
- ok: true,
259
- remaining: []
260
- };
270
+ return { remaining: [] };
261
271
  };
262
272
  const sendPayload = async (entry) => {
263
273
  if (!fetchFn) throw new Error("fetch is not available for browserHttpTransport");
@@ -293,17 +303,18 @@ function browserHttpTransport(options) {
293
303
  incrementLoggerMetaCounter("transport.offline.queued");
294
304
  return true;
295
305
  };
306
+ const isOnline = () => typeof navigator === "undefined" || navigator.onLine !== false;
296
307
  const sendFetchBatch = async (batch) => {
297
- if (batch.length === 0) return;
298
308
  const transformed = await encodeTransformedPayload(batch);
299
- if (offlineQueue && typeof navigator !== "undefined" && navigator.onLine === false) {
309
+ if (offlineQueue && !isOnline()) {
300
310
  await enqueueOfflinePayload(transformed.payload, transformed.headers, transformed.contentType);
301
- return;
311
+ return "queued";
302
312
  }
303
313
  try {
304
314
  await sendPayload(createOfflineEntry(transformed.payload, transformed.headers, transformed.contentType));
315
+ return "sent";
305
316
  } catch (error) {
306
- if (await enqueueOfflinePayload(transformed.payload, transformed.headers, transformed.contentType)) return;
317
+ if (await enqueueOfflinePayload(transformed.payload, transformed.headers, transformed.contentType)) return "queued";
307
318
  throw error;
308
319
  }
309
320
  };
@@ -325,40 +336,88 @@ function browserHttpTransport(options) {
325
336
  await sleep(replayRetryDelay(attempt));
326
337
  }
327
338
  };
328
- const replayOfflineQueue = async () => {
329
- if (!offlineQueue || replayingOffline) return;
330
- replayingOffline = true;
339
+ const replayOfflineQueue = () => {
340
+ if (!offlineQueue) return Promise.resolve();
341
+ replayPromise ??= (async () => {
342
+ try {
343
+ await offlineQueue.replay(sendOfflineEntryWithRetry);
344
+ } finally {
345
+ replayPromise = void 0;
346
+ }
347
+ })();
348
+ return replayPromise;
349
+ };
350
+ const replayOfflineQueueSafely = async (operation) => {
351
+ if (!offlineQueue || !isOnline()) return;
331
352
  try {
332
- await offlineQueue.replay(sendOfflineEntryWithRetry);
333
- } finally {
334
- replayingOffline = false;
353
+ await replayOfflineQueue();
354
+ } catch (error) {
355
+ reportInternalError(error, operation);
335
356
  }
336
357
  };
337
- const flush = async (preferBeacon = false) => {
338
- if (flushing || queue.length === 0) return;
339
- flushing = true;
340
- clearTimer();
341
- const batch = queue.splice(0, queue.length);
342
- let pendingBatch = batch;
358
+ const flushBeaconQueue = () => {
359
+ if (flushingBeacon || options.transformPayload || queue.length === 0) return;
360
+ flushingBeacon = true;
361
+ let pending = queue.splice(0, queue.length);
343
362
  try {
344
- if (preferBeacon && !options.transformPayload) {
345
- const beaconResult = sendBeaconBatch(batch);
346
- if (beaconResult.ok || beaconResult.remaining.length === 0) return;
347
- pendingBatch = beaconResult.remaining;
348
- }
349
- await sendFetchBatch(pendingBatch);
350
- pendingBatch = [];
351
- } catch (error) {
352
- if (pendingBatch.length > 0) queue.unshift(...pendingBatch);
353
- throw error;
363
+ const result = sendBeaconBatch(pending);
364
+ pending = result.remaining;
365
+ if (result.failure) throw result.failure.error;
354
366
  } finally {
355
- flushing = false;
356
- if (queue.length > 0) schedule();
367
+ if (pending.length > 0) queue.unshift(...pending);
368
+ flushingBeacon = false;
357
369
  }
358
370
  };
371
+ const flush = (preferBeacon = false) => {
372
+ if (activeFlush) {
373
+ if (preferBeacon) try {
374
+ flushBeaconQueue();
375
+ } catch (error) {
376
+ return activeFlush.then(() => {
377
+ throw error;
378
+ });
379
+ }
380
+ return activeFlush;
381
+ }
382
+ if (queue.length === 0) return preferBeacon ? Promise.resolve() : replayOfflineQueueSafely("replay");
383
+ clearTimer();
384
+ let resolve;
385
+ let reject;
386
+ const task = new Promise((onResolve, onReject) => {
387
+ resolve = onResolve;
388
+ reject = onReject;
389
+ });
390
+ activeFlush = task;
391
+ (async () => {
392
+ try {
393
+ if (preferBeacon) flushBeaconQueue();
394
+ let sent = false;
395
+ let queued = false;
396
+ while (queue.length > 0) {
397
+ const batch = queue.splice(0, maxBatchSize);
398
+ try {
399
+ if (await sendFetchBatch(batch) === "sent") sent = true;
400
+ else queued = true;
401
+ } catch (error) {
402
+ queue.unshift(...batch);
403
+ throw error;
404
+ }
405
+ }
406
+ if (!preferBeacon && sent && !queued) await replayOfflineQueueSafely("replay");
407
+ resolve();
408
+ } catch (error) {
409
+ reject(error);
410
+ } finally {
411
+ activeFlush = void 0;
412
+ if (queue.length > 0) schedule();
413
+ }
414
+ })();
415
+ return task;
416
+ };
359
417
  const schedule = () => {
360
- if (timer || flushIntervalMs <= 0) return;
418
+ if (activeFlush || timer || flushIntervalMs <= 0) return;
361
419
  timer = setTimeout(() => {
420
+ timer = void 0;
362
421
  flush(false).catch((error) => reportInternalError(error, "flush"));
363
422
  }, flushIntervalMs);
364
423
  };
@@ -375,7 +434,10 @@ function browserHttpTransport(options) {
375
434
  globalThis.addEventListener?.("pagehide", onPageHide);
376
435
  globalThis.addEventListener?.("visibilitychange", onVisibilityChange);
377
436
  }
378
- if (offlineQueue) globalThis.addEventListener?.("online", onOnline);
437
+ if (offlineQueue) {
438
+ globalThis.addEventListener?.("online", onOnline);
439
+ if (options.offlineReplayOnStart ?? true) setTimeout(() => void replayOfflineQueueSafely("startup-replay"), 0);
440
+ }
379
441
  return {
380
442
  name: options.name ?? "browser-http",
381
443
  minLevel: options.minLevel,
@@ -391,6 +453,7 @@ function browserHttpTransport(options) {
391
453
  if (dropped) reportDrop(dropped, "queue-full");
392
454
  }
393
455
  queue.push(event);
456
+ if (activeFlush) return;
394
457
  if (queue.length >= maxBatchSize) flush(false).catch((error) => reportInternalError(error, "flush"));
395
458
  else schedule();
396
459
  },
@@ -2028,6 +2091,7 @@ function offlineFirstTransport(remote, options = {}) {
2028
2091
  name,
2029
2092
  minLevel: options.minLevel ?? remote.minLevel,
2030
2093
  queue,
2094
+ ready: remote.ready && (() => remote.ready?.()),
2031
2095
  write(record, context) {
2032
2096
  return sendOrQueue(context.toEvent(record), context);
2033
2097
  },