@steve31415/baselib 3.36.1 → 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.
- package/dist/log-browser.js +1 -1
- package/dist/log-core.d.ts +25 -6
- package/dist/log-core.js +20 -3
- package/dist/test-owners.js +42 -9
- package/package.json +1 -1
package/dist/log-browser.js
CHANGED
|
@@ -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 ~
|
|
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,
|
package/dist/log-core.d.ts
CHANGED
|
@@ -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
|
|
57
|
+
/** Total delivery attempts per batch (default 6; see DEFAULT_RETRY_DELAYS_MS). */
|
|
58
58
|
maxAttempts?: number;
|
|
59
|
-
/** Backoff before attempt 2, 3, … (default
|
|
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
|
|
78
|
-
*
|
|
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 ??
|
|
99
|
-
this.retryDelaysMs = opts.retryDelaysMs ??
|
|
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.
|
|
279
|
+
batch.nextAttemptAt = Date.now() + this.offlineRetryMs;
|
|
263
280
|
this.schedulePump();
|
|
264
281
|
return;
|
|
265
282
|
}
|
package/dist/test-owners.js
CHANGED
|
@@ -9,32 +9,65 @@
|
|
|
9
9
|
//
|
|
10
10
|
// Scope is `.ts` only; `.tsx` is out of scope by decision (docs/TESTING.md,
|
|
11
11
|
// "The src -> test mapping"). `.d.ts` files are declarations, not modules.
|
|
12
|
+
//
|
|
13
|
+
// Files git ignores are not part of the repo and are skipped in both
|
|
14
|
+
// directions. A long-lived deploy checkout can hold leftovers of a build step
|
|
15
|
+
// that no longer exists — games' clone kept a generated `src/build-version.ts`
|
|
16
|
+
// from 2026-07-15, gitignored once its generator was deleted, and verify
|
|
17
|
+
// failed on it in every deploy while passing in every fresh worktree
|
|
18
|
+
// (2026-10-12, Coder task 37e13f8e). Untracked files that are NOT ignored still
|
|
19
|
+
// count, so a new module is checked before its first `git add`. Outside a git
|
|
20
|
+
// work tree (or without git) nothing is filtered.
|
|
21
|
+
import { execFile } from 'node:child_process';
|
|
12
22
|
import { readdir } from 'node:fs/promises';
|
|
13
23
|
import { join } from 'node:path';
|
|
14
24
|
const MODULE_EXT = '.ts';
|
|
15
25
|
const TEST_EXT = '.test.ts';
|
|
16
|
-
/**
|
|
17
|
-
*
|
|
18
|
-
|
|
26
|
+
/** The subset of `paths` (relative to `root`) that git ignores. Tracked
|
|
27
|
+
* files are never reported, even when a pattern matches them. Empty when
|
|
28
|
+
* `root` is not in a git work tree or git is unavailable. */
|
|
29
|
+
function gitIgnored(root, paths) {
|
|
30
|
+
if (paths.length === 0)
|
|
31
|
+
return Promise.resolve(new Set());
|
|
32
|
+
return new Promise((resolve, reject) => {
|
|
33
|
+
const child = execFile('git', ['check-ignore', '-z', '--stdin'], { cwd: root, maxBuffer: 64 * 1024 * 1024 }, (err, stdout) => {
|
|
34
|
+
// Exit 1 means "none ignored"; 128 (not a repo) or a missing git
|
|
35
|
+
// binary means there is nothing to filter against.
|
|
36
|
+
const code = err?.code;
|
|
37
|
+
if (err && code !== 1 && code !== 128 && code !== 'ENOENT') {
|
|
38
|
+
reject(err);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
resolve(new Set(err ? [] : stdout.split('\0').filter((p) => p !== '')));
|
|
42
|
+
});
|
|
43
|
+
child.stdin?.on('error', () => { }); // git exited before reading: the callback reports it
|
|
44
|
+
child.stdin?.end(paths.join('\0') + '\0');
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
/** Files under `root/dir` (recursively) ending in `ext` and not ignored by
|
|
48
|
+
* git, as paths relative to `dir` with forward slashes. A missing directory
|
|
49
|
+
* yields no files. */
|
|
50
|
+
async function listFiles(root, dir, ext) {
|
|
19
51
|
let entries;
|
|
20
52
|
try {
|
|
21
|
-
entries = await readdir(dir, { recursive: true });
|
|
53
|
+
entries = await readdir(join(root, dir), { recursive: true });
|
|
22
54
|
}
|
|
23
55
|
catch (err) {
|
|
24
56
|
if (err.code === 'ENOENT')
|
|
25
57
|
return [];
|
|
26
58
|
throw err;
|
|
27
59
|
}
|
|
28
|
-
|
|
60
|
+
const files = entries
|
|
29
61
|
.map((e) => e.split('\\').join('/'))
|
|
30
|
-
.filter((e) => e.endsWith(ext) && !e.endsWith('.d.ts'))
|
|
31
|
-
|
|
62
|
+
.filter((e) => e.endsWith(ext) && !e.endsWith('.d.ts'));
|
|
63
|
+
const ignored = await gitIgnored(root, files.map((f) => `${dir}/${f}`));
|
|
64
|
+
return files.filter((f) => !ignored.has(`${dir}/${f}`)).sort();
|
|
32
65
|
}
|
|
33
66
|
export async function checkTestOwners(opts) {
|
|
34
67
|
const srcDir = opts.srcDir ?? 'src';
|
|
35
68
|
const testDir = opts.testDir ?? 'test';
|
|
36
|
-
const modules = (await listFiles(
|
|
37
|
-
const tests = new Set(await listFiles(
|
|
69
|
+
const modules = (await listFiles(opts.root, srcDir, MODULE_EXT)).filter((m) => !m.endsWith(TEST_EXT));
|
|
70
|
+
const tests = new Set(await listFiles(opts.root, testDir, TEST_EXT));
|
|
38
71
|
const missing = [];
|
|
39
72
|
for (const mod of modules) {
|
|
40
73
|
const expected = `${mod.slice(0, -MODULE_EXT.length)}${TEST_EXT}`;
|
package/package.json
CHANGED