@steve31415/baselib 3.36.2 → 3.37.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.
@@ -88,7 +88,7 @@ export function createBrowserLogger(opts = {}) {
88
88
  // keeping are often the ones an offline stretch causes: while the
89
89
  // browser reports itself offline, failed attempts don't count, so a
90
90
  // batch waits out the outage (until the page dies or the buffer cap
91
- // evicts it) instead of being dropped after ~60 s.
91
+ // evicts it) instead of being dropped after its 6 attempts (~5 min).
92
92
  isOffline: () => navigator.onLine === false,
93
93
  onGiveUp: (batch, error, reason) => console.warn(`log ship dropped a batch (${reason}): ${String(error)}`, {
94
94
  batch_id: batch.id,
@@ -54,13 +54,16 @@ export interface ShipperOptions {
54
54
  token: string;
55
55
  batchSize?: number;
56
56
  intervalMs?: number;
57
- /** Total delivery attempts per batch (default 4). */
57
+ /** Total delivery attempts per batch (default 6; see DEFAULT_RETRY_DELAYS_MS). */
58
58
  maxAttempts?: number;
59
- /** Backoff before attempt 2, 3, … (default [5s, 15s, 40s]). On an idle
60
- * Cloud Run instance the CPU is throttled between requests, so these
59
+ /** Backoff before attempt 2, 3, … (default DEFAULT_RETRY_DELAYS_MS). On an
60
+ * idle Cloud Run instance the CPU is throttled between requests, so these
61
61
  * fire at the next CPU window (next request or shutdown), not on the
62
- * wall clock. */
62
+ * wall clock — which only stretches the budget. */
63
63
  retryDelaysMs?: number[];
64
+ /** While isOffline() is true, how long a batch waits before its next
65
+ * (uncounted) attempt (default 40 s). */
66
+ offlineRetryMs?: number;
64
67
  /** Per-attempt fetch timeout (default 10s; 3s aborted most of the fleet's
65
68
  * attempts in the two weeks to 2026-08-28). */
66
69
  attemptTimeoutMs?: number;
@@ -74,8 +77,8 @@ export interface ShipperOptions {
74
77
  /** True while the platform knows it has no network (the browser passes
75
78
  * `navigator.onLine === false`). A failed attempt made while offline says
76
79
  * nothing about the batch or the ingest service, so it is not counted
77
- * toward maxAttempts and not reported: the batch waits and retries at the
78
- * longest backoff delay, or at once on resume(). The signal only decides
80
+ * toward maxAttempts and not reported: the batch waits and retries after
81
+ * offlineRetryMs, or at once on resume(). The signal only decides
79
82
  * what a failure means — attempts still run while "offline". */
80
83
  isOffline?: () => boolean;
81
84
  /** A delivery attempt failed; the batch will be retried. */
@@ -86,6 +89,21 @@ export interface ShipperOptions {
86
89
  * keep, listed in `failures`. Those events are lost; the rest landed. */
87
90
  onPartialFailure?: (batch: ShippedBatch, failed: number, failures: unknown[]) => void;
88
91
  }
92
+ /** The default retry budget: 6 attempts with 5 + 15 + 40 + 90 + 150 s =
93
+ * 5 minutes of backoff between the first and the last.
94
+ *
95
+ * Deliberate choice, 2026-10-15 (watchdog task dc78e9b2): the earlier
96
+ * 4 attempts over 5 + 15 + 40 s (~60 s) dropped a mirror batch on
97
+ * 2026-10-14, when Axiom ingest answered 503 on and off for ~150 s — 2.5×
98
+ * the budget — the only ingest outage in the 4 weeks of markers on record.
99
+ * 5 minutes covers that episode twice over. Headroom: at the fleet's peak
100
+ * five-minute volume (mirror, 1 538 events, ~0.3 MB, 2026-10-08..15) the
101
+ * events held through a 5-minute outage fill ~31% of the 5 000-event /
102
+ * 5 MB buffer cap, so the cap, not the attempt count, bounds a longer
103
+ * outage. Longer delays cost nothing on a throttled instance (they ride the
104
+ * next CPU window either way) and only matter while ingest is failing. */
105
+ export declare const DEFAULT_MAX_ATTEMPTS = 6;
106
+ export declare const DEFAULT_RETRY_DELAYS_MS: readonly number[];
89
107
  export declare class LogShipper {
90
108
  private readonly opts;
91
109
  private queue;
@@ -102,6 +120,7 @@ export declare class LogShipper {
102
120
  private readonly intervalMs;
103
121
  private readonly maxAttempts;
104
122
  private readonly retryDelaysMs;
123
+ private readonly offlineRetryMs;
105
124
  private readonly attemptTimeoutMs;
106
125
  private readonly maxBufferedEvents;
107
126
  private readonly maxBufferedBytes;
package/dist/log-core.js CHANGED
@@ -45,6 +45,21 @@ export function serializeError(err) {
45
45
  export function truncate(s, max) {
46
46
  return s.length <= max ? s : s.slice(0, max) + `…[+${s.length - max}]`;
47
47
  }
48
+ /** The default retry budget: 6 attempts with 5 + 15 + 40 + 90 + 150 s =
49
+ * 5 minutes of backoff between the first and the last.
50
+ *
51
+ * Deliberate choice, 2026-10-15 (watchdog task dc78e9b2): the earlier
52
+ * 4 attempts over 5 + 15 + 40 s (~60 s) dropped a mirror batch on
53
+ * 2026-10-14, when Axiom ingest answered 503 on and off for ~150 s — 2.5×
54
+ * the budget — the only ingest outage in the 4 weeks of markers on record.
55
+ * 5 minutes covers that episode twice over. Headroom: at the fleet's peak
56
+ * five-minute volume (mirror, 1 538 events, ~0.3 MB, 2026-10-08..15) the
57
+ * events held through a 5-minute outage fill ~31% of the 5 000-event /
58
+ * 5 MB buffer cap, so the cap, not the attempt count, bounds a longer
59
+ * outage. Longer delays cost nothing on a throttled instance (they ride the
60
+ * next CPU window either way) and only matter while ingest is failing. */
61
+ export const DEFAULT_MAX_ATTEMPTS = 6;
62
+ export const DEFAULT_RETRY_DELAYS_MS = [5_000, 15_000, 40_000, 90_000, 150_000];
48
63
  const RESPONSE_BODY_MAX = 300;
49
64
  /** A 4xx is a verdict on the bytes, not the moment: retrying the identical
50
65
  * body cannot succeed. 408 (timeout) and 429 (rate limit) are the exceptions. */
@@ -88,6 +103,7 @@ export class LogShipper {
88
103
  intervalMs;
89
104
  maxAttempts;
90
105
  retryDelaysMs;
106
+ offlineRetryMs;
91
107
  attemptTimeoutMs;
92
108
  maxBufferedEvents;
93
109
  maxBufferedBytes;
@@ -95,8 +111,9 @@ export class LogShipper {
95
111
  this.opts = opts;
96
112
  this.batchSize = opts.batchSize ?? 50;
97
113
  this.intervalMs = opts.intervalMs ?? 2000;
98
- this.maxAttempts = opts.maxAttempts ?? 4;
99
- this.retryDelaysMs = opts.retryDelaysMs ?? [5_000, 15_000, 40_000];
114
+ this.maxAttempts = opts.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
115
+ this.retryDelaysMs = opts.retryDelaysMs ?? DEFAULT_RETRY_DELAYS_MS;
116
+ this.offlineRetryMs = opts.offlineRetryMs ?? 40_000;
100
117
  this.attemptTimeoutMs = opts.attemptTimeoutMs ?? 10_000;
101
118
  this.maxBufferedEvents = opts.maxBufferedEvents ?? 5_000;
102
119
  this.maxBufferedBytes = opts.maxBufferedBytes ?? 5 * 1024 * 1024;
@@ -259,7 +276,7 @@ export class LogShipper {
259
276
  if (!outcome.ok && !outcome.terminal && this.opts.isOffline?.()) {
260
277
  // Uncounted: the budget is for failures that could be the batch's
261
278
  // or the service's, and an offline failure is neither.
262
- batch.nextAttemptAt = Date.now() + this.retryDelaysMs[this.retryDelaysMs.length - 1];
279
+ batch.nextAttemptAt = Date.now() + this.offlineRetryMs;
263
280
  this.schedulePump();
264
281
  return;
265
282
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@steve31415/baselib",
3
- "version": "3.36.2",
3
+ "version": "3.37.0",
4
4
  "description": "Plasticine new-world shared platform library: logging, auth, service-to-service auth, db, HTTP, sync, app updates",
5
5
  "type": "module",
6
6
  "license": "MIT",