@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/README.md +11 -14
- package/dist/http-transport.cjs +110 -47
- package/dist/http-transport.cjs.map +1 -1
- package/dist/http-transport.d.ts +1 -0
- package/dist/http-transport.d.ts.map +1 -1
- package/dist/http-transport.js +110 -47
- package/dist/http-transport.js.map +1 -1
- package/dist/index.cjs +111 -47
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +111 -47
- package/dist/index.js.map +1 -1
- package/dist/indexeddb-transport.cjs.map +1 -1
- package/dist/indexeddb-transport.js.map +1 -1
- package/dist/offline-first-transport.cjs +1 -0
- package/dist/offline-first-transport.cjs.map +1 -1
- package/dist/offline-first-transport.d.ts.map +1 -1
- package/dist/offline-first-transport.js +1 -0
- package/dist/offline-first-transport.js.map +1 -1
- package/package.json +2 -2
package/dist/http-transport.js
CHANGED
|
@@ -61,6 +61,7 @@ function browserHttpTransport(options) {
|
|
|
61
61
|
const beaconCodec = options.beaconCodec ?? codec;
|
|
62
62
|
const queue = [];
|
|
63
63
|
const maxBatchSize = options.maxBatchSize ?? 50;
|
|
64
|
+
if (!Number.isSafeInteger(maxBatchSize) || maxBatchSize <= 0) throw new RangeError("maxBatchSize must be a positive safe integer");
|
|
64
65
|
const flushIntervalMs = options.flushIntervalMs ?? 2e3;
|
|
65
66
|
const maxQueueSize = options.maxQueueSize ?? 1e3;
|
|
66
67
|
const dropPolicy = options.dropPolicy ?? "drop-oldest";
|
|
@@ -73,8 +74,9 @@ function browserHttpTransport(options) {
|
|
|
73
74
|
const fetchFn = options.fetchFn ?? globalThis.fetch?.bind(globalThis);
|
|
74
75
|
let offlineEntrySeq = 0;
|
|
75
76
|
let timer;
|
|
76
|
-
let
|
|
77
|
-
let
|
|
77
|
+
let activeFlush;
|
|
78
|
+
let flushingBeacon = false;
|
|
79
|
+
let replayPromise;
|
|
78
80
|
let lastContext;
|
|
79
81
|
const headers = (payloadHeaders, contentType = codec.contentType) => ({
|
|
80
82
|
"content-type": contentType,
|
|
@@ -99,9 +101,18 @@ function browserHttpTransport(options) {
|
|
|
99
101
|
};
|
|
100
102
|
const createBeaconChunks = (batch) => {
|
|
101
103
|
const chunks = [];
|
|
104
|
+
const oversized = [];
|
|
102
105
|
let currentEvents = [];
|
|
103
106
|
let currentPayload;
|
|
104
107
|
for (const event of batch) {
|
|
108
|
+
if (currentEvents.length === maxBatchSize && currentPayload !== void 0) {
|
|
109
|
+
chunks.push({
|
|
110
|
+
events: currentEvents,
|
|
111
|
+
payload: currentPayload
|
|
112
|
+
});
|
|
113
|
+
currentEvents = [];
|
|
114
|
+
currentPayload = void 0;
|
|
115
|
+
}
|
|
105
116
|
const candidateEvents = [...currentEvents, event];
|
|
106
117
|
const candidatePayload = beaconCodec.encode(candidateEvents);
|
|
107
118
|
if (payloadByteLength(candidatePayload) <= beaconMaxBytes) {
|
|
@@ -109,7 +120,7 @@ function browserHttpTransport(options) {
|
|
|
109
120
|
currentPayload = candidatePayload;
|
|
110
121
|
continue;
|
|
111
122
|
}
|
|
112
|
-
if (currentEvents.length > 0 && currentPayload) chunks.push({
|
|
123
|
+
if (currentEvents.length > 0 && currentPayload !== void 0) chunks.push({
|
|
113
124
|
events: currentEvents,
|
|
114
125
|
payload: currentPayload
|
|
115
126
|
});
|
|
@@ -120,33 +131,32 @@ function browserHttpTransport(options) {
|
|
|
120
131
|
} else {
|
|
121
132
|
currentEvents = [];
|
|
122
133
|
currentPayload = void 0;
|
|
123
|
-
|
|
134
|
+
oversized.push(event);
|
|
124
135
|
}
|
|
125
136
|
}
|
|
126
|
-
if (currentEvents.length > 0 && currentPayload) chunks.push({
|
|
137
|
+
if (currentEvents.length > 0 && currentPayload !== void 0) chunks.push({
|
|
127
138
|
events: currentEvents,
|
|
128
139
|
payload: currentPayload
|
|
129
140
|
});
|
|
141
|
+
for (const event of oversized) reportDrop(event, "beacon-too-large");
|
|
130
142
|
return chunks;
|
|
131
143
|
};
|
|
132
144
|
const sendBeaconBatch = (batch) => {
|
|
133
|
-
if (typeof navigator === "undefined" || !navigator.sendBeacon) return {
|
|
134
|
-
ok: false,
|
|
135
|
-
remaining: batch
|
|
136
|
-
};
|
|
145
|
+
if (typeof navigator === "undefined" || !navigator.sendBeacon) return { remaining: batch };
|
|
137
146
|
const chunks = createBeaconChunks(batch);
|
|
138
147
|
for (let index = 0; index < chunks.length; index++) {
|
|
139
148
|
const chunk = chunks[index];
|
|
140
149
|
if (!chunk) continue;
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
150
|
+
try {
|
|
151
|
+
if (!navigator.sendBeacon(options.url, payloadToBeaconBody(chunk.payload, beaconCodec.contentType))) return { remaining: remainingEvents(chunks, index) };
|
|
152
|
+
} catch (error) {
|
|
153
|
+
return {
|
|
154
|
+
remaining: remainingEvents(chunks, index),
|
|
155
|
+
failure: { error }
|
|
156
|
+
};
|
|
157
|
+
}
|
|
145
158
|
}
|
|
146
|
-
return {
|
|
147
|
-
ok: true,
|
|
148
|
-
remaining: []
|
|
149
|
-
};
|
|
159
|
+
return { remaining: [] };
|
|
150
160
|
};
|
|
151
161
|
const sendPayload = async (entry) => {
|
|
152
162
|
if (!fetchFn) throw new Error("fetch is not available for browserHttpTransport");
|
|
@@ -182,17 +192,18 @@ function browserHttpTransport(options) {
|
|
|
182
192
|
incrementLoggerMetaCounter("transport.offline.queued");
|
|
183
193
|
return true;
|
|
184
194
|
};
|
|
195
|
+
const isOnline = () => typeof navigator === "undefined" || navigator.onLine !== false;
|
|
185
196
|
const sendFetchBatch = async (batch) => {
|
|
186
|
-
if (batch.length === 0) return;
|
|
187
197
|
const transformed = await encodeTransformedPayload(batch);
|
|
188
|
-
if (offlineQueue &&
|
|
198
|
+
if (offlineQueue && !isOnline()) {
|
|
189
199
|
await enqueueOfflinePayload(transformed.payload, transformed.headers, transformed.contentType);
|
|
190
|
-
return;
|
|
200
|
+
return "queued";
|
|
191
201
|
}
|
|
192
202
|
try {
|
|
193
203
|
await sendPayload(createOfflineEntry(transformed.payload, transformed.headers, transformed.contentType));
|
|
204
|
+
return "sent";
|
|
194
205
|
} catch (error) {
|
|
195
|
-
if (await enqueueOfflinePayload(transformed.payload, transformed.headers, transformed.contentType)) return;
|
|
206
|
+
if (await enqueueOfflinePayload(transformed.payload, transformed.headers, transformed.contentType)) return "queued";
|
|
196
207
|
throw error;
|
|
197
208
|
}
|
|
198
209
|
};
|
|
@@ -214,40 +225,88 @@ function browserHttpTransport(options) {
|
|
|
214
225
|
await sleep(replayRetryDelay(attempt));
|
|
215
226
|
}
|
|
216
227
|
};
|
|
217
|
-
const replayOfflineQueue =
|
|
218
|
-
if (!offlineQueue
|
|
219
|
-
|
|
228
|
+
const replayOfflineQueue = () => {
|
|
229
|
+
if (!offlineQueue) return Promise.resolve();
|
|
230
|
+
replayPromise ??= (async () => {
|
|
231
|
+
try {
|
|
232
|
+
await offlineQueue.replay(sendOfflineEntryWithRetry);
|
|
233
|
+
} finally {
|
|
234
|
+
replayPromise = void 0;
|
|
235
|
+
}
|
|
236
|
+
})();
|
|
237
|
+
return replayPromise;
|
|
238
|
+
};
|
|
239
|
+
const replayOfflineQueueSafely = async (operation) => {
|
|
240
|
+
if (!offlineQueue || !isOnline()) return;
|
|
220
241
|
try {
|
|
221
|
-
await
|
|
222
|
-
}
|
|
223
|
-
|
|
242
|
+
await replayOfflineQueue();
|
|
243
|
+
} catch (error) {
|
|
244
|
+
reportInternalError(error, operation);
|
|
224
245
|
}
|
|
225
246
|
};
|
|
226
|
-
const
|
|
227
|
-
if (
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
const batch = queue.splice(0, queue.length);
|
|
231
|
-
let pendingBatch = batch;
|
|
247
|
+
const flushBeaconQueue = () => {
|
|
248
|
+
if (flushingBeacon || options.transformPayload || queue.length === 0) return;
|
|
249
|
+
flushingBeacon = true;
|
|
250
|
+
let pending = queue.splice(0, queue.length);
|
|
232
251
|
try {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
pendingBatch = beaconResult.remaining;
|
|
237
|
-
}
|
|
238
|
-
await sendFetchBatch(pendingBatch);
|
|
239
|
-
pendingBatch = [];
|
|
240
|
-
} catch (error) {
|
|
241
|
-
if (pendingBatch.length > 0) queue.unshift(...pendingBatch);
|
|
242
|
-
throw error;
|
|
252
|
+
const result = sendBeaconBatch(pending);
|
|
253
|
+
pending = result.remaining;
|
|
254
|
+
if (result.failure) throw result.failure.error;
|
|
243
255
|
} finally {
|
|
244
|
-
|
|
245
|
-
|
|
256
|
+
if (pending.length > 0) queue.unshift(...pending);
|
|
257
|
+
flushingBeacon = false;
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
const flush = (preferBeacon = false) => {
|
|
261
|
+
if (activeFlush) {
|
|
262
|
+
if (preferBeacon) try {
|
|
263
|
+
flushBeaconQueue();
|
|
264
|
+
} catch (error) {
|
|
265
|
+
return activeFlush.then(() => {
|
|
266
|
+
throw error;
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
return activeFlush;
|
|
246
270
|
}
|
|
271
|
+
if (queue.length === 0) return preferBeacon ? Promise.resolve() : replayOfflineQueueSafely("replay");
|
|
272
|
+
clearTimer();
|
|
273
|
+
let resolve;
|
|
274
|
+
let reject;
|
|
275
|
+
const task = new Promise((onResolve, onReject) => {
|
|
276
|
+
resolve = onResolve;
|
|
277
|
+
reject = onReject;
|
|
278
|
+
});
|
|
279
|
+
activeFlush = task;
|
|
280
|
+
(async () => {
|
|
281
|
+
try {
|
|
282
|
+
if (preferBeacon) flushBeaconQueue();
|
|
283
|
+
let sent = false;
|
|
284
|
+
let queued = false;
|
|
285
|
+
while (queue.length > 0) {
|
|
286
|
+
const batch = queue.splice(0, maxBatchSize);
|
|
287
|
+
try {
|
|
288
|
+
if (await sendFetchBatch(batch) === "sent") sent = true;
|
|
289
|
+
else queued = true;
|
|
290
|
+
} catch (error) {
|
|
291
|
+
queue.unshift(...batch);
|
|
292
|
+
throw error;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
if (!preferBeacon && sent && !queued) await replayOfflineQueueSafely("replay");
|
|
296
|
+
resolve();
|
|
297
|
+
} catch (error) {
|
|
298
|
+
reject(error);
|
|
299
|
+
} finally {
|
|
300
|
+
activeFlush = void 0;
|
|
301
|
+
if (queue.length > 0) schedule();
|
|
302
|
+
}
|
|
303
|
+
})();
|
|
304
|
+
return task;
|
|
247
305
|
};
|
|
248
306
|
const schedule = () => {
|
|
249
|
-
if (timer || flushIntervalMs <= 0) return;
|
|
307
|
+
if (activeFlush || timer || flushIntervalMs <= 0) return;
|
|
250
308
|
timer = setTimeout(() => {
|
|
309
|
+
timer = void 0;
|
|
251
310
|
flush(false).catch((error) => reportInternalError(error, "flush"));
|
|
252
311
|
}, flushIntervalMs);
|
|
253
312
|
};
|
|
@@ -264,7 +323,10 @@ function browserHttpTransport(options) {
|
|
|
264
323
|
globalThis.addEventListener?.("pagehide", onPageHide);
|
|
265
324
|
globalThis.addEventListener?.("visibilitychange", onVisibilityChange);
|
|
266
325
|
}
|
|
267
|
-
if (offlineQueue)
|
|
326
|
+
if (offlineQueue) {
|
|
327
|
+
globalThis.addEventListener?.("online", onOnline);
|
|
328
|
+
if (options.offlineReplayOnStart ?? true) setTimeout(() => void replayOfflineQueueSafely("startup-replay"), 0);
|
|
329
|
+
}
|
|
268
330
|
return {
|
|
269
331
|
name: options.name ?? "browser-http",
|
|
270
332
|
minLevel: options.minLevel,
|
|
@@ -280,6 +342,7 @@ function browserHttpTransport(options) {
|
|
|
280
342
|
if (dropped) reportDrop(dropped, "queue-full");
|
|
281
343
|
}
|
|
282
344
|
queue.push(event);
|
|
345
|
+
if (activeFlush) return;
|
|
283
346
|
if (queue.length >= maxBatchSize) flush(false).catch((error) => reportInternalError(error, "flush"));
|
|
284
347
|
else schedule();
|
|
285
348
|
},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http-transport.js","names":[],"sources":["../src/http-transport.ts"],"sourcesContent":["import {\n applyPayloadTransforms,\n incrementLoggerMetaCounter,\n safeJsonCodec,\n toLevelValue,\n type Codec,\n type EncodedPayload,\n type LogEvent,\n type LoggerLevel,\n type PayloadTransform,\n type Transport,\n type TransportContext,\n} from \"@loggerjs/core\";\n\nexport type BrowserHttpDropPolicy = \"drop-oldest\" | \"drop-newest\";\n\nconst DEFAULT_BEACON_MAX_BYTES = 60 * 1024;\n\nconst sleep = (delayMs: number) =>\n delayMs <= 0 ? Promise.resolve() : new Promise<void>((resolve) => setTimeout(resolve, delayMs));\n\nexport interface BrowserHttpOfflineEntry {\n id: string;\n url: string;\n method: \"POST\" | \"PUT\";\n headers: Record<string, string>;\n body: string | Uint8Array;\n credentials?: RequestCredentials;\n keepalive: boolean;\n createdAt: number;\n}\n\nexport interface BrowserHttpOfflineQueue {\n enqueue: (entry: BrowserHttpOfflineEntry) => void | Promise<void>;\n replay: (send: (entry: BrowserHttpOfflineEntry) => Promise<void>) => void | Promise<void>;\n}\n\nexport interface MemoryBrowserHttpOfflineQueueOptions {\n maxEntries?: number;\n dropPolicy?: BrowserHttpDropPolicy;\n onDrop?: (entry: BrowserHttpOfflineEntry, reason: string) => void;\n}\n\nexport interface BrowserHttpTransportOptions {\n url: string;\n name?: string;\n method?: \"POST\" | \"PUT\";\n headers?: Record<string, string>;\n credentials?: RequestCredentials;\n keepalive?: boolean;\n codec?: Codec<string | Uint8Array>;\n beaconCodec?: Codec<string | Uint8Array>;\n minLevel?: LoggerLevel;\n maxBatchSize?: number;\n flushIntervalMs?: number;\n maxQueueSize?: number;\n dropPolicy?: BrowserHttpDropPolicy;\n useBeaconOnPageHide?: boolean;\n beaconMaxBytes?: number;\n offlineQueue?: BrowserHttpOfflineQueue;\n offlineReplayMaxRetries?: number;\n offlineReplayBaseDelayMs?: number;\n offlineReplayMaxDelayMs?: number;\n random?: () => number;\n fetchFn?: typeof fetch;\n transformPayload?: PayloadTransform | readonly PayloadTransform[];\n onDrop?: (event: LogEvent, reason: string) => void;\n}\n\nexport function memoryBrowserHttpOfflineQueue(\n options: MemoryBrowserHttpOfflineQueueOptions = {},\n): BrowserHttpOfflineQueue & { size: () => number } {\n const entries: BrowserHttpOfflineEntry[] = [];\n const maxEntries = options.maxEntries ?? 1000;\n const dropPolicy = options.dropPolicy ?? \"drop-oldest\";\n\n const drop = (entry: BrowserHttpOfflineEntry, reason: string) => {\n incrementLoggerMetaCounter(\"transport.offline.dropped\");\n incrementLoggerMetaCounter(`transport.offline.dropped.${reason}`);\n options.onDrop?.(entry, reason);\n };\n\n return {\n enqueue(entry) {\n if (entries.length >= maxEntries) {\n if (dropPolicy === \"drop-newest\") {\n drop(entry, \"queue-full\");\n return;\n }\n const dropped = entries.shift();\n if (dropped) drop(dropped, \"queue-full\");\n }\n entries.push(entry);\n },\n async replay(send) {\n while (entries.length > 0) {\n const entry = entries[0];\n if (!entry) return;\n // oxlint-disable-next-line no-await-in-loop -- Replay must remove entries only after each send succeeds.\n await send(entry);\n entries.shift();\n }\n },\n size() {\n return entries.length;\n },\n };\n}\n\nfunction payloadToBody(payload: EncodedPayload): BodyInit {\n if (typeof payload === \"string\") return payload;\n return Uint8Array.from(payload);\n}\n\nfunction payloadByteLength(payload: EncodedPayload): number {\n if (typeof payload !== \"string\") return payload.byteLength;\n if (typeof TextEncoder !== \"undefined\") return new TextEncoder().encode(payload).byteLength;\n return new Blob([payload]).size;\n}\n\nfunction payloadToBeaconBody(payload: EncodedPayload, contentType: string): BodyInit {\n return new Blob([typeof payload === \"string\" ? payload : Uint8Array.from(payload)], {\n type: contentType,\n });\n}\n\ninterface BeaconChunk {\n events: LogEvent[];\n payload: string | Uint8Array;\n}\n\nfunction remainingEvents(chunks: BeaconChunk[], startIndex: number): LogEvent[] {\n const events: LogEvent[] = [];\n for (let index = startIndex; index < chunks.length; index++) {\n const chunk = chunks[index];\n if (chunk) events.push(...chunk.events);\n }\n return events;\n}\n\nexport function browserHttpTransport(options: BrowserHttpTransportOptions): Transport {\n const codec = options.codec ?? safeJsonCodec();\n const beaconCodec = options.beaconCodec ?? codec;\n const queue: LogEvent[] = [];\n const maxBatchSize = options.maxBatchSize ?? 50;\n const flushIntervalMs = options.flushIntervalMs ?? 2000;\n const maxQueueSize = options.maxQueueSize ?? 1000;\n const dropPolicy = options.dropPolicy ?? \"drop-oldest\";\n const beaconMaxBytes = options.beaconMaxBytes ?? DEFAULT_BEACON_MAX_BYTES;\n const offlineQueue = options.offlineQueue;\n const offlineReplayMaxRetries = options.offlineReplayMaxRetries ?? 3;\n const offlineReplayBaseDelayMs = options.offlineReplayBaseDelayMs ?? 250;\n const offlineReplayMaxDelayMs = options.offlineReplayMaxDelayMs ?? 5000;\n const random = options.random ?? Math.random;\n const fetchFn = options.fetchFn ?? globalThis.fetch?.bind(globalThis);\n let offlineEntrySeq = 0;\n let timer: ReturnType<typeof setTimeout> | undefined;\n let flushing = false;\n let replayingOffline = false;\n let lastContext: TransportContext | undefined;\n\n const headers = (payloadHeaders?: Record<string, string>, contentType = codec.contentType) => ({\n \"content-type\": contentType,\n ...payloadHeaders,\n ...options.headers,\n });\n\n const clearTimer = () => {\n if (timer) clearTimeout(timer);\n timer = undefined;\n };\n\n const reportDrop = (event: LogEvent, reason: string) => {\n incrementLoggerMetaCounter(\"transport.dropped\");\n incrementLoggerMetaCounter(`transport.dropped.${reason}`);\n options.onDrop?.(event, reason);\n };\n\n const reportInternalError = (error: unknown, operation: string) => {\n lastContext?.reportInternalError(error, {\n phase: \"transport\",\n transport: options.name ?? \"browser-http\",\n operation,\n });\n };\n\n const createBeaconChunks = (batch: LogEvent[]): BeaconChunk[] => {\n const chunks: BeaconChunk[] = [];\n let currentEvents: LogEvent[] = [];\n let currentPayload: string | Uint8Array | undefined;\n\n for (const event of batch) {\n const candidateEvents = [...currentEvents, event];\n const candidatePayload = beaconCodec.encode(candidateEvents);\n if (payloadByteLength(candidatePayload) <= beaconMaxBytes) {\n currentEvents = candidateEvents;\n currentPayload = candidatePayload;\n continue;\n }\n\n if (currentEvents.length > 0 && currentPayload) {\n chunks.push({ events: currentEvents, payload: currentPayload });\n }\n\n const singlePayload = beaconCodec.encode([event]);\n if (payloadByteLength(singlePayload) <= beaconMaxBytes) {\n currentEvents = [event];\n currentPayload = singlePayload;\n } else {\n currentEvents = [];\n currentPayload = undefined;\n reportDrop(event, \"beacon-too-large\");\n }\n }\n\n if (currentEvents.length > 0 && currentPayload) {\n chunks.push({ events: currentEvents, payload: currentPayload });\n }\n\n return chunks;\n };\n\n const sendBeaconBatch = (batch: LogEvent[]): { ok: boolean; remaining: LogEvent[] } => {\n if (typeof navigator === \"undefined\" || !navigator.sendBeacon) {\n return { ok: false, remaining: batch };\n }\n\n const chunks = createBeaconChunks(batch);\n for (let index = 0; index < chunks.length; index++) {\n const chunk = chunks[index];\n if (!chunk) continue;\n const ok = navigator.sendBeacon(\n options.url,\n payloadToBeaconBody(chunk.payload, beaconCodec.contentType),\n );\n if (!ok) return { ok: false, remaining: remainingEvents(chunks, index) };\n }\n\n return { ok: true, remaining: [] };\n };\n\n const sendPayload = async (entry: BrowserHttpOfflineEntry) => {\n if (!fetchFn) throw new Error(\"fetch is not available for browserHttpTransport\");\n const response = await fetchFn(entry.url, {\n method: entry.method,\n headers: entry.headers,\n body: payloadToBody(entry.body),\n credentials: entry.credentials,\n keepalive: entry.keepalive,\n });\n if (!response.ok) throw new Error(`browserHttpTransport failed with status ${response.status}`);\n };\n\n const encodeTransformedPayload = async (batch: LogEvent[]) => {\n const encoded = codec.encode(batch);\n return applyPayloadTransforms(\n encoded,\n {\n contentType: codec.contentType,\n events: batch,\n transport: options.name ?? \"browser-http\",\n },\n options.transformPayload,\n );\n };\n\n const createOfflineEntry = (\n payload: string | Uint8Array,\n payloadHeaders?: Record<string, string>,\n contentType?: string,\n ): BrowserHttpOfflineEntry => ({\n id: `${Date.now().toString(36)}-${(offlineEntrySeq++).toString(36)}`,\n url: options.url,\n method: options.method ?? \"POST\",\n headers: headers(payloadHeaders, contentType),\n body: payload,\n credentials: options.credentials,\n keepalive: options.keepalive ?? true,\n createdAt: Date.now(),\n });\n\n const enqueueOfflinePayload = async (\n payload: string | Uint8Array,\n payloadHeaders?: Record<string, string>,\n contentType?: string,\n ) => {\n if (!offlineQueue) return false;\n await offlineQueue.enqueue(createOfflineEntry(payload, payloadHeaders, contentType));\n incrementLoggerMetaCounter(\"transport.offline.queued\");\n return true;\n };\n\n const sendFetchBatch = async (batch: LogEvent[]) => {\n if (batch.length === 0) return;\n const transformed = await encodeTransformedPayload(batch);\n if (offlineQueue && typeof navigator !== \"undefined\" && navigator.onLine === false) {\n await enqueueOfflinePayload(\n transformed.payload,\n transformed.headers,\n transformed.contentType,\n );\n return;\n }\n try {\n await sendPayload(\n createOfflineEntry(transformed.payload, transformed.headers, transformed.contentType),\n );\n } catch (error) {\n if (\n await enqueueOfflinePayload(\n transformed.payload,\n transformed.headers,\n transformed.contentType,\n )\n ) {\n return;\n }\n throw error;\n }\n };\n\n const replayRetryDelay = (attempt: number): number => {\n const cap = Math.min(offlineReplayMaxDelayMs, offlineReplayBaseDelayMs * 2 ** attempt);\n return cap <= 0 ? 0 : random() * cap;\n };\n\n const sendOfflineEntryWithRetry = async (entry: BrowserHttpOfflineEntry) => {\n for (let attempt = 0; ; attempt++) {\n try {\n // oxlint-disable-next-line no-await-in-loop -- Retry attempts must run sequentially.\n await sendPayload(entry);\n incrementLoggerMetaCounter(\"transport.offline.replayed\");\n return;\n } catch (error) {\n if (attempt >= offlineReplayMaxRetries) {\n incrementLoggerMetaCounter(\"transport.offline.replay.failed\");\n throw error;\n }\n incrementLoggerMetaCounter(\"transport.offline.retry\");\n // oxlint-disable-next-line no-await-in-loop -- Backoff must complete before the next retry.\n await sleep(replayRetryDelay(attempt));\n }\n }\n };\n\n const replayOfflineQueue = async () => {\n if (!offlineQueue || replayingOffline) return;\n replayingOffline = true;\n try {\n await offlineQueue.replay(sendOfflineEntryWithRetry);\n } finally {\n replayingOffline = false;\n }\n };\n\n const flush = async (preferBeacon = false) => {\n if (flushing || queue.length === 0) return;\n flushing = true;\n clearTimer();\n const batch = queue.splice(0, queue.length);\n let pendingBatch = batch;\n\n try {\n if (preferBeacon && !options.transformPayload) {\n const beaconResult = sendBeaconBatch(batch);\n if (beaconResult.ok || beaconResult.remaining.length === 0) return;\n pendingBatch = beaconResult.remaining;\n }\n\n await sendFetchBatch(pendingBatch);\n pendingBatch = [];\n } catch (error) {\n if (pendingBatch.length > 0) queue.unshift(...pendingBatch);\n throw error;\n } finally {\n flushing = false;\n if (queue.length > 0) schedule();\n }\n };\n\n const schedule = () => {\n if (timer || flushIntervalMs <= 0) return;\n timer = setTimeout(() => {\n void flush(false).catch((error: unknown) => reportInternalError(error, \"flush\"));\n }, flushIntervalMs);\n };\n\n const onPageHide = () => {\n void flush(true).catch((error: unknown) => reportInternalError(error, \"pagehide-flush\"));\n };\n const onVisibilityChange = () => {\n if (typeof document !== \"undefined\" && document.visibilityState === \"hidden\") {\n void flush(true).catch((error: unknown) => reportInternalError(error, \"visibility-flush\"));\n }\n };\n const onOnline = () => {\n void replayOfflineQueue().catch((error: unknown) => reportInternalError(error, \"replay\"));\n };\n\n if (options.useBeaconOnPageHide ?? true) {\n globalThis.addEventListener?.(\"pagehide\", onPageHide);\n globalThis.addEventListener?.(\"visibilitychange\", onVisibilityChange);\n }\n if (offlineQueue) globalThis.addEventListener?.(\"online\", onOnline);\n\n return {\n name: options.name ?? \"browser-http\",\n minLevel: options.minLevel,\n log(event, context) {\n if (options.minLevel !== undefined && event.level < toLevelValue(options.minLevel)) return;\n lastContext = context;\n if (queue.length >= maxQueueSize) {\n if (dropPolicy === \"drop-newest\") {\n reportDrop(event, \"queue-full\");\n return;\n }\n const dropped = queue.shift();\n if (dropped) reportDrop(dropped, \"queue-full\");\n }\n queue.push(event);\n if (queue.length >= maxBatchSize) {\n void flush(false).catch((error: unknown) => reportInternalError(error, \"flush\"));\n } else schedule();\n },\n flush() {\n return flush(false);\n },\n close() {\n globalThis.removeEventListener?.(\"pagehide\", onPageHide);\n globalThis.removeEventListener?.(\"visibilitychange\", onVisibilityChange);\n globalThis.removeEventListener?.(\"online\", onOnline);\n return flush(true);\n },\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"http-transport.js","names":[],"sources":["../src/http-transport.ts"],"sourcesContent":["import {\n applyPayloadTransforms,\n incrementLoggerMetaCounter,\n safeJsonCodec,\n toLevelValue,\n type Codec,\n type EncodedPayload,\n type LogEvent,\n type LoggerLevel,\n type PayloadTransform,\n type Transport,\n type TransportContext,\n} from \"@loggerjs/core\";\n\nexport type BrowserHttpDropPolicy = \"drop-oldest\" | \"drop-newest\";\n\nconst DEFAULT_BEACON_MAX_BYTES = 60 * 1024;\n\nconst sleep = (delayMs: number) =>\n delayMs <= 0 ? Promise.resolve() : new Promise<void>((resolve) => setTimeout(resolve, delayMs));\n\nexport interface BrowserHttpOfflineEntry {\n id: string;\n url: string;\n method: \"POST\" | \"PUT\";\n headers: Record<string, string>;\n body: string | Uint8Array;\n credentials?: RequestCredentials;\n keepalive: boolean;\n createdAt: number;\n}\n\nexport interface BrowserHttpOfflineQueue {\n enqueue: (entry: BrowserHttpOfflineEntry) => void | Promise<void>;\n replay: (send: (entry: BrowserHttpOfflineEntry) => Promise<void>) => void | Promise<void>;\n}\n\nexport interface MemoryBrowserHttpOfflineQueueOptions {\n maxEntries?: number;\n dropPolicy?: BrowserHttpDropPolicy;\n onDrop?: (entry: BrowserHttpOfflineEntry, reason: string) => void;\n}\n\nexport interface BrowserHttpTransportOptions {\n url: string;\n name?: string;\n method?: \"POST\" | \"PUT\";\n headers?: Record<string, string>;\n credentials?: RequestCredentials;\n keepalive?: boolean;\n codec?: Codec<string | Uint8Array>;\n beaconCodec?: Codec<string | Uint8Array>;\n minLevel?: LoggerLevel;\n maxBatchSize?: number;\n flushIntervalMs?: number;\n maxQueueSize?: number;\n dropPolicy?: BrowserHttpDropPolicy;\n useBeaconOnPageHide?: boolean;\n beaconMaxBytes?: number;\n offlineQueue?: BrowserHttpOfflineQueue;\n offlineReplayMaxRetries?: number;\n offlineReplayBaseDelayMs?: number;\n offlineReplayMaxDelayMs?: number;\n offlineReplayOnStart?: boolean;\n random?: () => number;\n fetchFn?: typeof fetch;\n transformPayload?: PayloadTransform | readonly PayloadTransform[];\n onDrop?: (event: LogEvent, reason: string) => void;\n}\n\nexport function memoryBrowserHttpOfflineQueue(\n options: MemoryBrowserHttpOfflineQueueOptions = {},\n): BrowserHttpOfflineQueue & { size: () => number } {\n const entries: BrowserHttpOfflineEntry[] = [];\n const maxEntries = options.maxEntries ?? 1000;\n const dropPolicy = options.dropPolicy ?? \"drop-oldest\";\n\n const drop = (entry: BrowserHttpOfflineEntry, reason: string) => {\n incrementLoggerMetaCounter(\"transport.offline.dropped\");\n incrementLoggerMetaCounter(`transport.offline.dropped.${reason}`);\n options.onDrop?.(entry, reason);\n };\n\n return {\n enqueue(entry) {\n if (entries.length >= maxEntries) {\n if (dropPolicy === \"drop-newest\") {\n drop(entry, \"queue-full\");\n return;\n }\n const dropped = entries.shift();\n if (dropped) drop(dropped, \"queue-full\");\n }\n entries.push(entry);\n },\n async replay(send) {\n while (entries.length > 0) {\n const entry = entries[0];\n if (!entry) return;\n // oxlint-disable-next-line no-await-in-loop -- Replay must remove entries only after each send succeeds.\n await send(entry);\n entries.shift();\n }\n },\n size() {\n return entries.length;\n },\n };\n}\n\nfunction payloadToBody(payload: EncodedPayload): BodyInit {\n if (typeof payload === \"string\") return payload;\n return Uint8Array.from(payload);\n}\n\nfunction payloadByteLength(payload: EncodedPayload): number {\n if (typeof payload !== \"string\") return payload.byteLength;\n if (typeof TextEncoder !== \"undefined\") return new TextEncoder().encode(payload).byteLength;\n return new Blob([payload]).size;\n}\n\nfunction payloadToBeaconBody(payload: EncodedPayload, contentType: string): BodyInit {\n return new Blob([typeof payload === \"string\" ? payload : Uint8Array.from(payload)], {\n type: contentType,\n });\n}\n\ninterface BeaconChunk {\n events: LogEvent[];\n payload: string | Uint8Array;\n}\n\nfunction remainingEvents(chunks: BeaconChunk[], startIndex: number): LogEvent[] {\n const events: LogEvent[] = [];\n for (let index = startIndex; index < chunks.length; index++) {\n const chunk = chunks[index];\n if (chunk) events.push(...chunk.events);\n }\n return events;\n}\n\nexport function browserHttpTransport(options: BrowserHttpTransportOptions): Transport {\n const codec = options.codec ?? safeJsonCodec();\n const beaconCodec = options.beaconCodec ?? codec;\n const queue: LogEvent[] = [];\n const maxBatchSize = options.maxBatchSize ?? 50;\n if (!Number.isSafeInteger(maxBatchSize) || maxBatchSize <= 0) {\n throw new RangeError(\"maxBatchSize must be a positive safe integer\");\n }\n const flushIntervalMs = options.flushIntervalMs ?? 2000;\n const maxQueueSize = options.maxQueueSize ?? 1000;\n const dropPolicy = options.dropPolicy ?? \"drop-oldest\";\n const beaconMaxBytes = options.beaconMaxBytes ?? DEFAULT_BEACON_MAX_BYTES;\n const offlineQueue = options.offlineQueue;\n const offlineReplayMaxRetries = options.offlineReplayMaxRetries ?? 3;\n const offlineReplayBaseDelayMs = options.offlineReplayBaseDelayMs ?? 250;\n const offlineReplayMaxDelayMs = options.offlineReplayMaxDelayMs ?? 5000;\n const random = options.random ?? Math.random;\n const fetchFn = options.fetchFn ?? globalThis.fetch?.bind(globalThis);\n let offlineEntrySeq = 0;\n let timer: ReturnType<typeof setTimeout> | undefined;\n let activeFlush: Promise<void> | undefined;\n let flushingBeacon = false;\n let replayPromise: Promise<void> | undefined;\n let lastContext: TransportContext | undefined;\n\n const headers = (payloadHeaders?: Record<string, string>, contentType = codec.contentType) => ({\n \"content-type\": contentType,\n ...payloadHeaders,\n ...options.headers,\n });\n\n const clearTimer = () => {\n if (timer) clearTimeout(timer);\n timer = undefined;\n };\n\n const reportDrop = (event: LogEvent, reason: string) => {\n incrementLoggerMetaCounter(\"transport.dropped\");\n incrementLoggerMetaCounter(`transport.dropped.${reason}`);\n options.onDrop?.(event, reason);\n };\n\n const reportInternalError = (error: unknown, operation: string) => {\n lastContext?.reportInternalError(error, {\n phase: \"transport\",\n transport: options.name ?? \"browser-http\",\n operation,\n });\n };\n\n const createBeaconChunks = (batch: LogEvent[]): BeaconChunk[] => {\n const chunks: BeaconChunk[] = [];\n const oversized: LogEvent[] = [];\n let currentEvents: LogEvent[] = [];\n let currentPayload: string | Uint8Array | undefined;\n\n for (const event of batch) {\n if (currentEvents.length === maxBatchSize && currentPayload !== undefined) {\n chunks.push({ events: currentEvents, payload: currentPayload });\n currentEvents = [];\n currentPayload = undefined;\n }\n const candidateEvents = [...currentEvents, event];\n const candidatePayload = beaconCodec.encode(candidateEvents);\n if (payloadByteLength(candidatePayload) <= beaconMaxBytes) {\n currentEvents = candidateEvents;\n currentPayload = candidatePayload;\n continue;\n }\n\n if (currentEvents.length > 0 && currentPayload !== undefined) {\n chunks.push({ events: currentEvents, payload: currentPayload });\n }\n\n const singlePayload = beaconCodec.encode([event]);\n if (payloadByteLength(singlePayload) <= beaconMaxBytes) {\n currentEvents = [event];\n currentPayload = singlePayload;\n } else {\n currentEvents = [];\n currentPayload = undefined;\n oversized.push(event);\n }\n }\n\n if (currentEvents.length > 0 && currentPayload !== undefined) {\n chunks.push({ events: currentEvents, payload: currentPayload });\n }\n\n for (const event of oversized) reportDrop(event, \"beacon-too-large\");\n return chunks;\n };\n\n const sendBeaconBatch = (\n batch: LogEvent[],\n ): { remaining: LogEvent[]; failure?: { error: unknown } } => {\n if (typeof navigator === \"undefined\" || !navigator.sendBeacon) {\n return { remaining: batch };\n }\n\n const chunks = createBeaconChunks(batch);\n for (let index = 0; index < chunks.length; index++) {\n const chunk = chunks[index];\n if (!chunk) continue;\n try {\n const ok = navigator.sendBeacon(\n options.url,\n payloadToBeaconBody(chunk.payload, beaconCodec.contentType),\n );\n if (!ok) return { remaining: remainingEvents(chunks, index) };\n } catch (error) {\n return { remaining: remainingEvents(chunks, index), failure: { error } };\n }\n }\n\n return { remaining: [] };\n };\n\n const sendPayload = async (entry: BrowserHttpOfflineEntry) => {\n if (!fetchFn) throw new Error(\"fetch is not available for browserHttpTransport\");\n const response = await fetchFn(entry.url, {\n method: entry.method,\n headers: entry.headers,\n body: payloadToBody(entry.body),\n credentials: entry.credentials,\n keepalive: entry.keepalive,\n });\n if (!response.ok) throw new Error(`browserHttpTransport failed with status ${response.status}`);\n };\n\n const encodeTransformedPayload = async (batch: LogEvent[]) => {\n const encoded = codec.encode(batch);\n return applyPayloadTransforms(\n encoded,\n {\n contentType: codec.contentType,\n events: batch,\n transport: options.name ?? \"browser-http\",\n },\n options.transformPayload,\n );\n };\n\n const createOfflineEntry = (\n payload: string | Uint8Array,\n payloadHeaders?: Record<string, string>,\n contentType?: string,\n ): BrowserHttpOfflineEntry => ({\n id: `${Date.now().toString(36)}-${(offlineEntrySeq++).toString(36)}`,\n url: options.url,\n method: options.method ?? \"POST\",\n headers: headers(payloadHeaders, contentType),\n body: payload,\n credentials: options.credentials,\n keepalive: options.keepalive ?? true,\n createdAt: Date.now(),\n });\n\n const enqueueOfflinePayload = async (\n payload: string | Uint8Array,\n payloadHeaders?: Record<string, string>,\n contentType?: string,\n ) => {\n if (!offlineQueue) return false;\n await offlineQueue.enqueue(createOfflineEntry(payload, payloadHeaders, contentType));\n incrementLoggerMetaCounter(\"transport.offline.queued\");\n return true;\n };\n\n const isOnline = () => typeof navigator === \"undefined\" || navigator.onLine !== false;\n\n // Resolves to \"sent\" when the collector accepted the batch and \"queued\" when\n // the offline queue took it instead.\n const sendFetchBatch = async (batch: LogEvent[]): Promise<\"sent\" | \"queued\"> => {\n const transformed = await encodeTransformedPayload(batch);\n if (offlineQueue && !isOnline()) {\n await enqueueOfflinePayload(\n transformed.payload,\n transformed.headers,\n transformed.contentType,\n );\n return \"queued\";\n }\n try {\n await sendPayload(\n createOfflineEntry(transformed.payload, transformed.headers, transformed.contentType),\n );\n return \"sent\";\n } catch (error) {\n if (\n await enqueueOfflinePayload(\n transformed.payload,\n transformed.headers,\n transformed.contentType,\n )\n ) {\n return \"queued\";\n }\n throw error;\n }\n };\n\n const replayRetryDelay = (attempt: number): number => {\n const cap = Math.min(offlineReplayMaxDelayMs, offlineReplayBaseDelayMs * 2 ** attempt);\n return cap <= 0 ? 0 : random() * cap;\n };\n\n const sendOfflineEntryWithRetry = async (entry: BrowserHttpOfflineEntry) => {\n for (let attempt = 0; ; attempt++) {\n try {\n // oxlint-disable-next-line no-await-in-loop -- Retry attempts must run sequentially.\n await sendPayload(entry);\n incrementLoggerMetaCounter(\"transport.offline.replayed\");\n return;\n } catch (error) {\n if (attempt >= offlineReplayMaxRetries) {\n incrementLoggerMetaCounter(\"transport.offline.replay.failed\");\n throw error;\n }\n incrementLoggerMetaCounter(\"transport.offline.retry\");\n // oxlint-disable-next-line no-await-in-loop -- Backoff must complete before the next retry.\n await sleep(replayRetryDelay(attempt));\n }\n }\n };\n\n const replayOfflineQueue = (): Promise<void> => {\n if (!offlineQueue) return Promise.resolve();\n replayPromise ??= (async () => {\n try {\n await offlineQueue.replay(sendOfflineEntryWithRetry);\n } finally {\n replayPromise = undefined;\n }\n })();\n return replayPromise;\n };\n\n // Stored entries stay in the offline queue when replay fails, so a failed\n // replay is reported instead of failing the flush that triggered it.\n const replayOfflineQueueSafely = async (operation: string) => {\n if (!offlineQueue || !isOnline()) return;\n try {\n await replayOfflineQueue();\n } catch (error) {\n reportInternalError(error, operation);\n }\n };\n\n // Beacon submission is synchronous, even while an earlier Fetch is pending.\n // Only queued events are eligible; the active Fetch batch keeps its ownership.\n const flushBeaconQueue = () => {\n if (flushingBeacon || options.transformPayload || queue.length === 0) return;\n flushingBeacon = true;\n let pending = queue.splice(0, queue.length);\n try {\n const result = sendBeaconBatch(pending);\n pending = result.remaining;\n if (result.failure) throw result.failure.error;\n } finally {\n if (pending.length > 0) queue.unshift(...pending);\n flushingBeacon = false;\n }\n };\n\n const flush = (preferBeacon = false): Promise<void> => {\n if (activeFlush) {\n if (preferBeacon) {\n try {\n flushBeaconQueue();\n } catch (error) {\n // close() must still wait for the active delivery before reporting a Beacon error.\n return activeFlush.then(() => {\n throw error;\n });\n }\n }\n return activeFlush;\n }\n if (queue.length === 0) {\n // An explicit flush with nothing live still retries stored payloads, so\n // entries queued during a server outage do not wait for an `online` event.\n return preferBeacon ? Promise.resolve() : replayOfflineQueueSafely(\"replay\");\n }\n clearTimer();\n\n // Publish the task before invoking user codecs/transforms, which may reenter log().\n let resolve!: () => void;\n let reject!: (error: unknown) => void;\n const task = new Promise<void>((onResolve, onReject) => {\n resolve = onResolve;\n reject = onReject;\n });\n activeFlush = task;\n void (async () => {\n try {\n if (preferBeacon) flushBeaconQueue();\n let sent = false;\n let queued = false;\n while (queue.length > 0) {\n const batch = queue.splice(0, maxBatchSize);\n try {\n // oxlint-disable-next-line no-await-in-loop -- Preserve batch order and stop on the first unhandled failure.\n const outcome = await sendFetchBatch(batch);\n if (outcome === \"sent\") sent = true;\n else queued = true;\n } catch (error) {\n queue.unshift(...batch);\n throw error;\n }\n }\n // A live batch reached the collector and none fell back to the offline\n // queue, so the collector is reachable again: replay stored payloads.\n if (!preferBeacon && sent && !queued) await replayOfflineQueueSafely(\"replay\");\n resolve();\n } catch (error) {\n reject(error);\n } finally {\n activeFlush = undefined;\n if (queue.length > 0) schedule();\n }\n })();\n return task;\n };\n\n const schedule = () => {\n if (activeFlush || timer || flushIntervalMs <= 0) return;\n timer = setTimeout(() => {\n timer = undefined;\n void flush(false).catch((error: unknown) => reportInternalError(error, \"flush\"));\n }, flushIntervalMs);\n };\n\n const onPageHide = () => {\n void flush(true).catch((error: unknown) => reportInternalError(error, \"pagehide-flush\"));\n };\n const onVisibilityChange = () => {\n if (typeof document !== \"undefined\" && document.visibilityState === \"hidden\") {\n void flush(true).catch((error: unknown) => reportInternalError(error, \"visibility-flush\"));\n }\n };\n const onOnline = () => {\n void replayOfflineQueue().catch((error: unknown) => reportInternalError(error, \"replay\"));\n };\n\n if (options.useBeaconOnPageHide ?? true) {\n globalThis.addEventListener?.(\"pagehide\", onPageHide);\n globalThis.addEventListener?.(\"visibilitychange\", onVisibilityChange);\n }\n if (offlineQueue) {\n globalThis.addEventListener?.(\"online\", onOnline);\n // Payloads persisted by an earlier page load (for example in IndexedDB)\n // would otherwise wait for an `online` event that may never fire.\n if (options.offlineReplayOnStart ?? true) {\n setTimeout(() => void replayOfflineQueueSafely(\"startup-replay\"), 0);\n }\n }\n\n return {\n name: options.name ?? \"browser-http\",\n minLevel: options.minLevel,\n log(event, context) {\n if (options.minLevel !== undefined && event.level < toLevelValue(options.minLevel)) return;\n lastContext = context;\n if (queue.length >= maxQueueSize) {\n if (dropPolicy === \"drop-newest\") {\n reportDrop(event, \"queue-full\");\n return;\n }\n const dropped = queue.shift();\n if (dropped) reportDrop(dropped, \"queue-full\");\n }\n queue.push(event);\n if (activeFlush) return;\n if (queue.length >= maxBatchSize) {\n void flush(false).catch((error: unknown) => reportInternalError(error, \"flush\"));\n } else schedule();\n },\n flush() {\n return flush(false);\n },\n close() {\n globalThis.removeEventListener?.(\"pagehide\", onPageHide);\n globalThis.removeEventListener?.(\"visibilitychange\", onVisibilityChange);\n globalThis.removeEventListener?.(\"online\", onOnline);\n return flush(true);\n },\n };\n}\n"],"mappings":"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"}
|
package/dist/index.cjs
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
|
|
188
|
-
let
|
|
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
|
-
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
(0, _loggerjs_core.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 &&
|
|
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 =
|
|
329
|
-
if (!offlineQueue
|
|
330
|
-
|
|
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
|
|
333
|
-
}
|
|
334
|
-
|
|
353
|
+
await replayOfflineQueue();
|
|
354
|
+
} catch (error) {
|
|
355
|
+
reportInternalError(error, operation);
|
|
335
356
|
}
|
|
336
357
|
};
|
|
337
|
-
const
|
|
338
|
-
if (
|
|
339
|
-
|
|
340
|
-
|
|
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
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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
|
-
|
|
356
|
-
|
|
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)
|
|
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
|
},
|