cursedbelt-server 4.9.0 โ†’ 4.11.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.
@@ -0,0 +1,88 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+ import { run } from '../scripts/publicSurface';
3
+
4
+ /**
5
+ * The public-surface ratchet, wired into `bun run verify` at its cheapest correct
6
+ * home: `verify` ends in `bun test src`, so this needs no new script and cannot be a
7
+ * script somebody forgot to chain.
8
+ *
9
+ * WHY it exists, WHAT a symbol count includes and excludes, why the dependency list
10
+ * and the used-vs-exported ratio are deliberately NOT computed here, and what
11
+ * `--prune` may and may not do are all in the header of `scripts/publicSurface.ts` โ€”
12
+ * one check, one baseline (`publicSurface.baseline`), one place that explains itself.
13
+ * This file is only the wiring.
14
+ *
15
+ * ๐Ÿ”ด Verified failing, 2026-09-19, before it was trusted โ€” every branch, not just the
16
+ * easy one. Each probe was applied, observed, and removed again:
17
+ *
18
+ * ยท `export const ratchetProbe = 1` appended to `src/server/errors.ts`
19
+ * โ†’ this file's "ceiling" test red โ€” `3 pass, 1 fail`, "exports no more symbols per
20
+ * subpath than the baseline allows" โ€” and the CLI:
21
+ * "1 subpath(s) that gained exported symbols ยท ./errors โ€” baseline allows 14,
22
+ * exports 15 now"
23
+ * ยท the same line appended to `src/server/guard/revocationStore.ts`, which is where the
24
+ * transitive `export *` walk is actually visible: `./guard/revocations` points at that
25
+ * file and `./guard`'s barrel does `export * from './revocationStore'`
26
+ * โ†’ "2 subpath(s) that gained exported symbols ยท ./guard โ€” baseline allows 25,
27
+ * exports 26 now ยท ./guard/revocations โ€” baseline allows 7, exports 8 now".
28
+ * ๐Ÿ”ด TWO from one line, which is the walk the header promises, seen working. Note
29
+ * which probe does NOT do this: `.` is a barrel of 219 NAMED re-exports, so a new
30
+ * symbol in `errors.ts` does not reach it. A probe chosen in the wrong module
31
+ * would have "proved" the transitive walk by never exercising it.
32
+ * ยท a `"./probe"` subpath added to `package.json` `exports`, pointed at
33
+ * `./src/server/errors.ts`
34
+ * โ†’ "1 export subpath(s) the baseline does not know ยท ./probe โ€” NEW subpath,
35
+ * 14 exported symbol(s)"
36
+ * ยท `VALIDATION_STATUS` removed from the re-export list in `src/server/errors.ts`, no prune
37
+ * โ†’ "1 baseline entr(y|ies) that no longer match `exports` ยท ./errors โ€” baseline
38
+ * says 14, exports 13"
39
+ * ยท a `5 ./gone` line added to the baseline by hand
40
+ * โ†’ same failure, "./gone โ€” baseline says 5, the subpath is gone". An entry
41
+ * matching nothing is the half that stops a deleted module leaving an allowance.
42
+ * ยท `--prune` run against the grown surface
43
+ * โ†’ "refuses while the surface has GROWN โ€” it may only lower the baseline", exit 1
44
+ * ยท `--prune` run against the shrunk surface
45
+ * โ†’ "pruned to 34 subpath(s), 0 removed, 990 symbols", exit 0, and `14 ./errors`
46
+ * became `13 ./errors` in the file โ€” the burn-down path
47
+ * ยท `--seed` run over the seeded baseline
48
+ * โ†’ "refuses: โ€ฆ already records 34 subpath(s)", exit 1
49
+ *
50
+ * Every probe was reverted with `git checkout -- <path>`; the tree afterwards held only
51
+ * this file, `scripts/publicSurface.ts` and `publicSurface.baseline`.
52
+ *
53
+ * A ratchet nobody has seen red is a ratchet nobody knows is wired up.
54
+ */
55
+ const result = await run();
56
+
57
+ describe('public surface', () => {
58
+ it('has measured a real surface', () => {
59
+ // Guards the three assertions below: if `exports` were ever read as empty, every
60
+ // one of them passes vacuously and the ratchet quietly dies.
61
+ // ๐Ÿ”ด 20, not 34: a vacuity guard set at the real surface fails for the one reason
62
+ // a vacuity guard must never fail โ€” the surface being healthy and simply smaller.
63
+ expect(result.surface.counts.size).toBeGreaterThan(20);
64
+ });
65
+
66
+ it('publishes no export subpath the baseline does not know', () => {
67
+ expect(
68
+ result.fresh,
69
+ `new public subpath(s) โ€” a permanent promise. Add the line to publicSurface.baseline and say why:\n ${result.fresh.join('\n ')}`,
70
+ ).toEqual([]);
71
+ });
72
+
73
+ it('exports no more symbols per subpath than the baseline allows', () => {
74
+ expect(
75
+ result.grown,
76
+ `a baseline entry is a ceiling, not an amnesty:\n ${result.grown.join('\n ')}`,
77
+ ).toEqual([]);
78
+ });
79
+
80
+ it('has no baseline entry that outlived the surface it recorded', () => {
81
+ // The half that makes it a RATCHET: a deleted module may not leave an allowance
82
+ // behind for the next one to grow into.
83
+ expect(
84
+ result.stale,
85
+ `surface came off and the ratchet was not tightened โ€” run \`bun scripts/publicSurface.ts --prune\`:\n ${result.stale.join('\n ')}`,
86
+ ).toEqual([]);
87
+ });
88
+ });
@@ -10,7 +10,7 @@ import {
10
10
  workerCpuClock,
11
11
  } from './cpuClock';
12
12
  import { createCpuRecorder } from './recorder';
13
- import { runCpuBench } from './runBench';
13
+ import { BENCH_ORIGIN, runCpuBench } from './runBench';
14
14
 
15
15
  /**
16
16
  * ๐Ÿ”ด **The assertion must fire in BOTH directions or it is decoration.** A gate that can
@@ -306,6 +306,196 @@ describe('the bench runner', () => {
306
306
  });
307
307
  });
308
308
 
309
+ /**
310
+ * ๐Ÿ”ด **A destructive route benched without a fixture rebuild reports a number for the wrong
311
+ * reason**, and `expectStatus` cannot see it: the second purge legitimately answers 200 with
312
+ * `{ deleted: 0 }`. One call runs against a populated library and the next twenty-nine
313
+ * against an empty one, so the p99 describes the empty case and the budget it satisfies
314
+ * means nothing. The first block below IS that hole, measured; `beforeEach` is what closes
315
+ * it; the last two are the ways closing it could itself become a lie.
316
+ *
317
+ * Filed 2026-09-19 by the `collections` port โ€” the first app to mount this harness โ€” which
318
+ * had to leave `POST /api/duplicates/rescan` and `POST /api/recycle/purge`, the only two
319
+ * routes in it whose cost is O(catalogue), declared but unbenched for exactly this reason.
320
+ */
321
+ describe('a destructive case โ€” the fixture rebuild between iterations', () => {
322
+ /** ~4 CPU-ms per item held, ~0 CPU-ms when the library is empty. */
323
+ const PURGE_ITEM_MS = 4;
324
+ const PURGE_ITEMS = 3;
325
+ const POPULATED_MS = PURGE_ITEM_MS * PURGE_ITEMS;
326
+
327
+ /** A route whose cost is O(library) and which EMPTIES the library โ€” a purge, in miniature. */
328
+ function buildDestructiveApp() {
329
+ const recorder = createCpuRecorder({
330
+ clock: processCpuClock(),
331
+ config: { routes: { 'POST /api/purge': 60 } },
332
+ });
333
+ const app = new Hono();
334
+ app.use('*', cpuBudget({ recorder }));
335
+ let library: string[] = [];
336
+ /** How many items each call actually found to purge โ€” the fixture's state, per iteration. */
337
+ const purged: number[] = [];
338
+ app.post('/api/purge', (c) => {
339
+ let acc = 0;
340
+ for (const item of library) acc += burnCpu(PURGE_ITEM_MS) + item.length;
341
+ purged.push(library.length);
342
+ library = [];
343
+ return c.json({ deleted: purged[purged.length - 1], acc });
344
+ });
345
+ const seed = () => {
346
+ library = Array.from({ length: PURGE_ITEMS }, (_, i) => `item-${i}`);
347
+ };
348
+ return { app, recorder, seed, purged };
349
+ }
350
+
351
+ it('WITHOUT the hook, measures an empty library from the second call on โ€” the hole itself', async () => {
352
+ const d = buildDestructiveApp();
353
+ d.seed();
354
+ // warmup 0 so the one populated call is a measured sample rather than a discarded one โ€”
355
+ // otherwise the trap would be invisible here for a second, different reason.
356
+ const report = await runCpuBench({
357
+ app: d.app,
358
+ recorder: d.recorder,
359
+ cases: [{ method: 'POST', path: '/api/purge' }],
360
+ iterations: 100,
361
+ warmup: 0,
362
+ });
363
+
364
+ // One real purge, ninety-nine against nothing.
365
+ expect(d.purged[0]).toBe(PURGE_ITEMS);
366
+ expect(d.purged.slice(1).every((n) => n === 0)).toBe(true);
367
+
368
+ const row = report.routes[0];
369
+ expect(row.max).toBeGreaterThan(POPULATED_MS * 0.5); // the route really does cost this
370
+ expect(row.p99).toBeLessThan(2); // โ€ฆand the number the budget is read off says it does not
371
+ expect(assertCpuBudgets(report)).toEqual([]); // green, and meaningless. That is the bug.
372
+ });
373
+
374
+ it('WITH the hook, measures the populated library on every single iteration', async () => {
375
+ const d = buildDestructiveApp();
376
+ const seen: number[] = [];
377
+ const report = await runCpuBench({
378
+ app: d.app,
379
+ recorder: d.recorder,
380
+ cases: [
381
+ {
382
+ method: 'POST',
383
+ path: '/api/purge',
384
+ beforeEach: (c, iteration) => {
385
+ expect(c.path).toBe('/api/purge');
386
+ seen.push(iteration);
387
+ d.seed();
388
+ },
389
+ },
390
+ ],
391
+ iterations: 20,
392
+ warmup: 2,
393
+ });
394
+
395
+ // ๐Ÿ”ด EVERY call found a full library, warm-up included โ€” not just the first.
396
+ expect(d.purged).toHaveLength(22);
397
+ expect(d.purged.every((n) => n === PURGE_ITEMS)).toBe(true);
398
+ // The iteration counter is 0-based and monotonic across both passes, so a hook that
399
+ // seeds ids from it cannot collide between the warm-up and the measured run.
400
+ expect(seen).toEqual(Array.from({ length: 22 }, (_, i) => i));
401
+
402
+ const row = report.routes[0];
403
+ expect(row.samples).toBe(20);
404
+ // The p50 โ€” not just the max โ€” now carries the real cost. Before the hook it was ~0.
405
+ expect(row.p50).toBeGreaterThan(POPULATED_MS * 0.5);
406
+ expect(row.p99).toBeGreaterThan(POPULATED_MS * 0.5);
407
+ });
408
+
409
+ it('is PER CASE โ€” a cheap read case beside a destructive one pays nothing for it', async () => {
410
+ const d = buildDestructiveApp();
411
+ d.app.get('/api/cheap', (c) => c.json({ ok: true }));
412
+ let rebuilds = 0;
413
+ const report = await runCpuBench({
414
+ app: d.app,
415
+ recorder: d.recorder,
416
+ cases: [
417
+ { path: '/api/cheap' },
418
+ {
419
+ method: 'POST',
420
+ path: '/api/purge',
421
+ beforeEach: () => {
422
+ rebuilds += 1;
423
+ d.seed();
424
+ },
425
+ },
426
+ ],
427
+ iterations: 20,
428
+ warmup: 1,
429
+ });
430
+
431
+ // 21 for the destructive case and not one for the read โ€” the read case never called it.
432
+ expect(rebuilds).toBe(21);
433
+ expect(d.purged).toHaveLength(21);
434
+ const cheap = report.routes.find((r) => r.route === '/api/cheap');
435
+ const purge = report.routes.find((r) => r.route === '/api/purge');
436
+ expect(cheap?.p99).toBeLessThan(2);
437
+ expect(purge?.p99).toBeGreaterThan(POPULATED_MS * 0.5);
438
+ });
439
+
440
+ /*
441
+ * ๐Ÿ”ด The hook's own CPU must not land in the histogram. It is excluded by ORDERING โ€” the
442
+ * recorder is written through by the middleware inside `app.fetch`, and the hook runs
443
+ * outside it โ€” and ordering is an accident, so it is asserted here rather than relied on.
444
+ * A fixture rebuild that cost 200 ms and got attributed to the route would make this
445
+ * feature a worse lie than the hole it closes.
446
+ */
447
+ it("๐Ÿ”ด does NOT record the hook's own CPU, however expensive the rebuild is", async () => {
448
+ const { app, recorder } = buildApp({ routes: { 'GET /api/cheap': 5 } });
449
+ let rebuilds = 0;
450
+ const report = await runCpuBench({
451
+ app,
452
+ recorder,
453
+ cases: [
454
+ {
455
+ path: '/api/cheap',
456
+ beforeEach: () => {
457
+ rebuilds += 1;
458
+ burnCpu(SLOW_MS); // 30 CPU-ms of rebuild, ~3ร— the default budget, every iteration
459
+ },
460
+ },
461
+ ],
462
+ iterations: 20,
463
+ warmup: 2,
464
+ });
465
+
466
+ expect(rebuilds).toBe(22); // 22 ร— 30 CPU-ms was genuinely burned during this runโ€ฆ
467
+ const row = report.routes[0];
468
+ expect(row.samples).toBe(20);
469
+ // โ€ฆand none of it is in these numbers. Even the MAX stays far under one rebuild.
470
+ expect(row.max).toBeLessThan(SLOW_MS / 3);
471
+ expect(row.p99).toBeLessThan(5);
472
+ expect(assertCpuBudgets(report)).toEqual([]);
473
+ });
474
+
475
+ it('REFUSES a hook that rebuilds the fixture through the app under bench', async () => {
476
+ // The obvious way to re-seed a library is to drive the app that has the routes. Those
477
+ // requests go through `cpuBudget()` like any other, so the rebuild would land in the
478
+ // report as real samples on real routes โ€” the same lie, arriving from the other side.
479
+ const { app, recorder } = buildApp({});
480
+ await expect(
481
+ runCpuBench({
482
+ app,
483
+ recorder,
484
+ cases: [
485
+ {
486
+ path: '/api/cheap',
487
+ beforeEach: async () => {
488
+ await app.request(`${BENCH_ORIGIN}/api/cheap`);
489
+ },
490
+ },
491
+ ],
492
+ iterations: 5,
493
+ warmup: 0,
494
+ }),
495
+ ).rejects.toThrow(/beforeEach drove 1 request\(s\) through the app under bench/);
496
+ });
497
+ });
498
+
309
499
  /**
310
500
  * ๐Ÿ”ด **The third direction: a gate that CANNOT go red.**
311
501
  *
@@ -0,0 +1,141 @@
1
+ import { afterEach, describe, expect, it } from 'bun:test';
2
+ import { Hono } from 'hono';
3
+ import { cpuBudget } from './cpuBudget';
4
+ import { processCpuClock } from './cpuClock';
5
+
6
+ /**
7
+ * ๐Ÿ”ด **The runtime this file exists for is the one `bun test` is not.**
8
+ *
9
+ * Measured 2026-09-19 on the first real deploy of `apps/collections`:
10
+ * `https://collections.curtwphillips.workers.dev/healthz` answered
11
+ * `500 Internal Server Error` on every path, with 843 green tests behind it. Under
12
+ * `nodejs_compat`, workerd PROVIDES `process.cpuUsage` and throws when it is called โ€”
13
+ *
14
+ * ```
15
+ * Error [ERR_METHOD_NOT_IMPLEMENTED]: The process.cpuUsage method is not implemented
16
+ * at process.cpuUsage (node-internal:public_process:235:11)
17
+ * at Object.start (index.js:2772:30) <- cpuBudget's middleware
18
+ * ```
19
+ *
20
+ * โ€” so the old `typeof process.cpuUsage === 'function'` probe said *available*, and
21
+ * `cpuBudget()`, which is mounted FIRST by design, threw in front of the whole app.
22
+ *
23
+ * Every case below stubs a THROWING method, not a missing one; a missing one was always
24
+ * handled and was never the bug.
25
+ *
26
+ * ๐Ÿ”ด Proven red, 2026-09-19, by putting the `typeof` probe back: **`2 pass, 4 fail`**. The
27
+ * second case reproduced the outage exactly, stack and all โ€”
28
+ *
29
+ * ```
30
+ * error: The process.cpuUsage method is not implemented code: "ERR_METHOD_NOT_IMPLEMENTED"
31
+ * at start (src/server/bench/cpuClock.ts:59:30)
32
+ * at <anonymous> (src/server/bench/cpuBudget.ts:54:32)
33
+ * expect(res.status).toBe(200) Expected: 200 Received: 500
34
+ * ```
35
+ *
36
+ * The two that passed against the old code are the two that must: `(absent)`, which the
37
+ * `typeof` probe did answer correctly, and the real-runtime direction.
38
+ */
39
+
40
+ const REAL_CPU_USAGE = process.cpuUsage;
41
+
42
+ /** Replace `process.cpuUsage` for one test. Restored by the `afterEach` below, always. */
43
+ function stubCpuUsage(fn: unknown): void {
44
+ (process as unknown as { cpuUsage: unknown }).cpuUsage = fn;
45
+ }
46
+
47
+ /** workerd's shape: present, callable, and it throws. */
48
+ function methodNotImplemented(): never {
49
+ const err = new Error('The process.cpuUsage method is not implemented');
50
+ (err as Error & { code?: string }).code = 'ERR_METHOD_NOT_IMPLEMENTED';
51
+ throw err;
52
+ }
53
+
54
+ afterEach(() => {
55
+ stubCpuUsage(REAL_CPU_USAGE);
56
+ });
57
+
58
+ describe('processCpuClock() under a runtime that stubs the method and throws', () => {
59
+ it('returns an UNAVAILABLE clock rather than one that throws on first use', () => {
60
+ stubCpuUsage(methodNotImplemented);
61
+
62
+ const clock = processCpuClock();
63
+ expect(clock.available).toBe(false);
64
+ // The label says which of the two failure shapes this was: it was there, and it threw.
65
+ expect(clock.source).toBe('process.cpuUsage (throws)');
66
+ // ๐Ÿ”ด And the span is a NaN, not a throw โ€” NaN is what `createCpuRecorder` drops as
67
+ // "not a sample", where a 0 would be recorded as a confident measurement of no work.
68
+ const span = clock.start();
69
+ expect(Number.isNaN(span())).toBe(true);
70
+ });
71
+
72
+ it('๐Ÿ”ด leaves an app that mounts cpuBudget() SERVING, instead of 500 on every path', async () => {
73
+ stubCpuUsage(methodNotImplemented);
74
+
75
+ // Exactly the production wiring: no clock passed, so the middleware builds its own
76
+ // `processCpuClock()`, and it is mounted before everything else.
77
+ const app = new Hono();
78
+ const mw = cpuBudget();
79
+ app.use('*', mw);
80
+ app.get('/healthz', (c) => c.json({ ok: true }));
81
+
82
+ const res = await app.request('http://cpu-bench.invalid/healthz');
83
+ expect(res.status).toBe(200);
84
+ expect(await res.json()).toEqual({ ok: true });
85
+
86
+ // Nothing measurable was recorded โ€” and that is the honest outcome, not a zero.
87
+ const report = mw.recorder.report();
88
+ expect(report.available).toBe(false);
89
+ expect(report.routes).toHaveLength(0);
90
+ });
91
+
92
+ it('probes the DELTA call too, not just the baseline one', () => {
93
+ // A stub that answers the zero-argument form and throws on `cpuUsage(before)` would
94
+ // pass a one-call probe and then throw at the END of the first request โ€” the same
95
+ // outage, arriving one line later.
96
+ stubCpuUsage((before?: unknown) => {
97
+ if (before !== undefined) methodNotImplemented();
98
+ return { user: 1_000, system: 1_000 };
99
+ });
100
+
101
+ const clock = processCpuClock();
102
+ expect(clock.available).toBe(false);
103
+ expect(clock.source).toBe('process.cpuUsage (throws)');
104
+ expect(Number.isNaN(clock.start()())).toBe(true);
105
+ });
106
+
107
+ it('treats a stub that answers with a non-number as unavailable, never as zero', () => {
108
+ stubCpuUsage(() => ({ user: undefined, system: undefined }));
109
+
110
+ const clock = processCpuClock();
111
+ expect(clock.available).toBe(false);
112
+ expect(clock.source).toBe('process.cpuUsage (non-numeric)');
113
+ expect(Number.isNaN(clock.start()())).toBe(true);
114
+ });
115
+
116
+ it('still says (absent) when the method is genuinely missing', () => {
117
+ stubCpuUsage(undefined);
118
+
119
+ const clock = processCpuClock();
120
+ expect(clock.available).toBe(false);
121
+ expect(clock.source).toBe('process.cpuUsage (absent)');
122
+ });
123
+
124
+ // The direction that stops the fix from being "always unavailable": on this runtime the
125
+ // method works, and the clock must still measure. Without this, every case above passes
126
+ // against a `processCpuClock()` that returned `unavailableCpuClock()` unconditionally.
127
+ it('is AVAILABLE and measures on a runtime where the method really works', () => {
128
+ const clock = processCpuClock();
129
+ expect(clock.available).toBe(true);
130
+ expect(clock.source).toBe('process.cpuUsage');
131
+ expect(clock.proxy).toBe(true);
132
+
133
+ const span = clock.start();
134
+ let acc = 0;
135
+ for (let i = 0; i < 2_000_000; i += 1) acc += Math.sqrt(i % 13);
136
+ expect(acc).toBeGreaterThan(0);
137
+ const cpuMs = span();
138
+ expect(Number.isFinite(cpuMs)).toBe(true);
139
+ expect(cpuMs).toBeGreaterThan(0);
140
+ });
141
+ });
@@ -43,10 +43,14 @@ export interface CpuClock {
43
43
  * That is why {@link runCpuBench} drives its cases SERIALLY โ€” a serial driver is what
44
44
  * makes this proxy sound enough to gate on. Mounted in production it is advisory: useful
45
45
  * for ranking routes, not for a billing claim.
46
+ *
47
+ * Where the runtime has no such reading, this returns {@link unavailableCpuClock} โ€” a
48
+ * clock, never a throw. {@link probeProcessCpuUsage} is what decides, and it decides by
49
+ * CALLING.
46
50
  */
47
51
  export function processCpuClock(): CpuClock {
48
- const usable = typeof process !== 'undefined' && typeof process.cpuUsage === 'function';
49
- if (!usable) return unavailableCpuClock('process.cpuUsage (absent)');
52
+ const probe = probeProcessCpuUsage();
53
+ if (!probe.ok) return unavailableCpuClock(`process.cpuUsage (${probe.why})`);
50
54
  return {
51
55
  source: 'process.cpuUsage',
52
56
  proxy: true,
@@ -61,6 +65,55 @@ export function processCpuClock(): CpuClock {
61
65
  };
62
66
  }
63
67
 
68
+ /**
69
+ * ๐Ÿ”ด **Is `process.cpuUsage()` a clock here? Answered by CALLING it, never by `typeof`.**
70
+ *
71
+ * Measured 2026-09-19 on the first real deploy of a Worker mounting `cpuBudget()`: under
72
+ * `nodejs_compat`, workerd's strategy for the parts of `node:process` it does not implement
73
+ * is to PROVIDE the method and throw when it is called โ€”
74
+ *
75
+ * ```
76
+ * Error [ERR_METHOD_NOT_IMPLEMENTED]: The process.cpuUsage method is not implemented
77
+ * at process.cpuUsage (node-internal:public_process:235:11)
78
+ * ```
79
+ *
80
+ * โ€” so `typeof process.cpuUsage === 'function'` answers *yes* about the one runtime the
81
+ * probe exists to answer *no* about. The clock that came back threw on first use, and
82
+ * because `cpuBudget()` is mounted FIRST by design, the throw landed in front of the whole
83
+ * app: every path 500, with 843 green tests behind it, because `bun test` runs where the
84
+ * method works.
85
+ *
86
+ * This is a capability CLASS, not one method: `nodejs_compat` stubs a great deal of
87
+ * `node:process`, `node:os` and `node:v8` the same way. `src/nodeBuiltinsAreProbedByCalling.spec.ts`
88
+ * is the check that keeps a `typeof` gate from coming back anywhere in this library.
89
+ *
90
+ * Both call shapes are exercised, because the span uses both and only one of them is
91
+ * probed by the first: `cpuUsage()` for the baseline and `cpuUsage(before)` for the delta.
92
+ * That costs two calls at wiring time โ€” the same trade {@link workerCpuClock} argues for on
93
+ * the other side, and the alternative is a gate that reads zero for ever.
94
+ */
95
+ function probeProcessCpuUsage(): { ok: true } | { ok: false; why: string } {
96
+ // Present-but-throwing and absent are both unavailable; the distinction only LABELS the
97
+ // clock, and is never the decision โ€” which is why this line may carry the excuse that
98
+ // `src/nodeBuiltinsAreProbedByCalling.spec.ts` refuses everywhere else.
99
+ // probe-by-calling:ignore โ€” label only; the call below is what decides.
100
+ const present = typeof process !== 'undefined' && typeof process.cpuUsage === 'function';
101
+ let reading: number;
102
+ try {
103
+ // A bare `process` that does not exist throws a ReferenceError in here, which is the
104
+ // same answer as a method that throws: not a clock.
105
+ const before = process.cpuUsage();
106
+ const delta = process.cpuUsage(before);
107
+ reading = before.user + before.system + delta.user + delta.system;
108
+ } catch {
109
+ return { ok: false, why: present ? 'throws' : 'absent' };
110
+ }
111
+ // A stub that returns something non-numeric is no more usable than one that throws, and
112
+ // would otherwise reach the recorder as a confident 0.00 for every route.
113
+ if (!Number.isFinite(reading)) return { ok: false, why: 'non-numeric' };
114
+ return { ok: true };
115
+ }
116
+
64
117
  /**
65
118
  * A clock over a CPU-ms reader the runtime supplies. `proxy: false` โ€” this is the real
66
119
  * quantity Cloudflare bills, so a report built on it may be quoted as one.
@@ -34,6 +34,33 @@ export interface BenchCase {
34
34
  * that never ran โ€” a green budget over a broken route.
35
35
  */
36
36
  expectStatus?: number | number[] | ((status: number) => boolean);
37
+ /**
38
+ * Called before EVERY iteration of this case โ€” warm-up included. For a **destructive**
39
+ * route, rebuild the fixture here; without it the second call measures an empty database.
40
+ *
41
+ * ๐Ÿ”ด This is the hook a destructive route cannot be benched without. `runCpuBench` fires a
42
+ * case `warmup + iterations` times against ONE app instance, which is exactly right for a
43
+ * read and a lie for a purge: the first call runs against a populated library and the next
44
+ * twenty-nine against an empty one, so the p99 is a small number for the wrong reason and
45
+ * the budget it satisfies means nothing. `expectStatus` cannot catch it either โ€” the second
46
+ * call legitimately answers 200 with `{ deleted: 0 }`.
47
+ *
48
+ * It is **per case**, not run-wide, so the cheap read cases pay nothing for it โ€” neither
49
+ * the rebuild nor the accounting {@link runCpuBench} does around it.
50
+ *
51
+ * ๐Ÿ”ด **Its cost is not measured, and that is checked rather than assumed.** The recorder is
52
+ * written through by the `cpuBudget()` middleware inside `app.fetch`, so a hook that runs
53
+ * outside that call is already excluded by ordering โ€” but ordering is an accident, and a
54
+ * fixture rebuild that cost 200 ms and got attributed to the route would make this feature
55
+ * a worse lie than the hole it closes. So {@link runCpuBench} refuses a hook that drives
56
+ * the app under bench (see the error it throws), and `cpuBudget.spec.ts` proves a hook that
57
+ * burns real CPU leaves the route's number alone.
58
+ *
59
+ * `iteration` is 0-based and monotonic across the WHOLE case, warm-up first: warm-up gets
60
+ * `0 โ€ฆ warmup-1` and the measured pass continues from `warmup`. A hook that seeds ids from
61
+ * it therefore never collides between the two passes.
62
+ */
63
+ beforeEach?: (c: BenchCase, iteration: number) => void | Promise<void>;
37
64
  }
38
65
 
39
66
  export interface RunCpuBenchOpts {
@@ -73,6 +100,41 @@ function describe(c: BenchCase): string {
73
100
  return `${(c.method ?? 'GET').toUpperCase()} ${c.path}`;
74
101
  }
75
102
 
103
+ /**
104
+ * Every request the recorder has attributed to any route so far. `samples` counts what was
105
+ * SEEN rather than what the reservoir retained, so this only ever goes up within a phase.
106
+ */
107
+ function totalSamples(recorder: RunCpuBenchOpts['recorder']): number {
108
+ let n = 0;
109
+ for (const r of recorder.report().routes) n += r.samples;
110
+ return n;
111
+ }
112
+
113
+ /**
114
+ * Run a case's {@link BenchCase.beforeEach}, and prove it stayed out of the histogram.
115
+ *
116
+ * ๐Ÿ”ด The obvious way to rebuild a fixture is to drive the app that already has the routes โ€”
117
+ * `POST /api/upload` thirty times to re-seed a library. Those requests go through
118
+ * `cpuBudget()` like any other, so the rebuild lands in the report as real samples on real
119
+ * routes, and the destructive route this hook exists to measure honestly is now benched
120
+ * beside rows nobody asked for. Refusing it here is cheap and only costs a case that has a
121
+ * hook at all.
122
+ */
123
+ async function prepare(opts: RunCpuBenchOpts, c: BenchCase, iteration: number): Promise<void> {
124
+ if (!c.beforeEach) return;
125
+ const before = totalSamples(opts.recorder);
126
+ await c.beforeEach(c, iteration);
127
+ const after = totalSamples(opts.recorder);
128
+ if (after !== before) {
129
+ throw new Error(
130
+ `cpu-bench: ${describe(c)}'s beforeEach drove ${after - before} request(s) through the ` +
131
+ 'app under bench, so the fixture rebuild is now IN the measurement it exists to keep ' +
132
+ 'out of it. Rebuild the fixture directly โ€” against the database, the store, the ' +
133
+ 'seed helper โ€” never through app.fetch.',
134
+ );
135
+ }
136
+ }
137
+
76
138
  async function fire(opts: RunCpuBenchOpts, c: BenchCase): Promise<void> {
77
139
  const method = (c.method ?? c.init?.method ?? 'GET').toUpperCase();
78
140
  const req = new Request(`${opts.origin ?? BENCH_ORIGIN}${c.path}`, { ...c.init, method });
@@ -95,15 +157,24 @@ export async function runCpuBench(opts: RunCpuBenchOpts): Promise<CpuBudgetRepor
95
157
  const warmup = opts.warmup ?? 5;
96
158
  if (!(iterations > 0)) throw new Error('runCpuBench: iterations must be > 0');
97
159
 
160
+ // `beforeEach` runs over the warm-up too, deliberately. Warm-up exists to reach a handler's
161
+ // steady state, and a destructive route warmed against the empty database it emptied warms
162
+ // the wrong branch โ€” the loop the p99 is about never runs.
98
163
  for (const c of opts.cases) {
99
- for (let i = 0; i < warmup; i += 1) await fire(opts, c);
164
+ for (let i = 0; i < warmup; i += 1) {
165
+ await prepare(opts, c, i);
166
+ await fire(opts, c);
167
+ }
100
168
  }
101
169
  // ๐Ÿ”ด Everything above was warm-up. Discard it โ€” measuring it is the bug this guards.
102
170
  opts.recorder.reset();
103
171
 
104
172
  for (const c of opts.cases) {
105
173
  const n = c.iterations ?? iterations;
106
- for (let i = 0; i < n; i += 1) await fire(opts, c);
174
+ for (let i = 0; i < n; i += 1) {
175
+ await prepare(opts, c, warmup + i);
176
+ await fire(opts, c);
177
+ }
107
178
  }
108
179
 
109
180
  return opts.recorder.report();
@@ -39,7 +39,7 @@ export {
39
39
  // that does not use the query builder โ€” which per `../db/kysely.ts`'s own header is most
40
40
  // of them, since business tables stay on raw statements. Measured 2026-09-18 against the
41
41
  // published 4.3.0 tarball. `barrelsReachNoOptionalPeer.spec.ts` is what keeps it out.
42
- export { type InvocationD1, perInvocation } from './invocation';
42
+ export { createPendingWrites, type InvocationD1, type PendingWrites, perInvocation, type WriteCollector } from './invocation';
43
43
  export { assertBatchSize, assertWithinLimits, chunkForBind, LIMITS } from './limits';
44
44
  export { createLocalD1, refuseInteractiveTransaction } from './local';
45
45
  export {