cursedbelt-server 4.9.0 → 4.11.1
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/server/analytics/webAnalytics.d.ts +32 -3
- package/dist/server/analytics/webAnalytics.js +40 -6
- package/dist/server/bench/cpuClock.d.ts +4 -0
- package/dist/server/bench/cpuClock.js +57 -3
- package/dist/server/bench/runBench.d.ts +27 -0
- package/dist/server/bench/runBench.js +42 -2
- package/dist/server/d1/index.d.ts +1 -1
- package/dist/server/d1/index.js +1 -1
- package/dist/server/d1/invocation.d.ts +72 -0
- package/dist/server/d1/invocation.js +65 -0
- package/dist/server/master-lock/guard.d.ts +8 -3
- package/dist/server/master-lock/guard.js +8 -3
- package/package.json +1 -1
- package/src/nodeBuiltinsAreProbedByCalling.spec.ts +233 -0
- package/src/publicSurface.spec.ts +88 -0
- package/src/server/analytics/injectedScripts.spec.ts +11 -7
- package/src/server/analytics/webAnalytics.spec.ts +26 -7
- package/src/server/analytics/webAnalytics.ts +43 -6
- package/src/server/bench/cpuBudget.spec.ts +191 -1
- package/src/server/bench/cpuClock.spec.ts +141 -0
- package/src/server/bench/cpuClock.ts +55 -2
- package/src/server/bench/runBench.ts +73 -2
- package/src/server/d1/index.ts +1 -1
- package/src/server/d1/invocation.spec.ts +65 -1
- package/src/server/d1/invocation.ts +86 -0
- package/src/server/incidents/incidents.spec.ts +5 -1
- package/src/server/master-lock/guard.ts +8 -3
|
@@ -48,7 +48,29 @@ export declare const WEB_ANALYTICS_SITE_TAG = "d1bea8562544421fbd2db5f11b74d1cc"
|
|
|
48
48
|
/** The `cursedalchemy.com` site's `data-cf-beacon` token. This is what the tag carries. */
|
|
49
49
|
export declare const WEB_ANALYTICS_SITE_TOKEN = "bd88d63678b64bc89f9702f48846bc88";
|
|
50
50
|
/**
|
|
51
|
-
* The
|
|
51
|
+
* 🔴 The `version` the beacon's config must carry, or NOTHING is recorded.
|
|
52
|
+
*
|
|
53
|
+
* Measured 2026-09-22, after the RUM API showed every manual-snippet shell recording ZERO
|
|
54
|
+
* pageviews from 2026-09-19 on — collections, music, roms, flix, station and desk, which had
|
|
55
|
+
* been 130–290 a day between them — while the hosts still on the edge's injection kept
|
|
56
|
+
* recording. The beacon copies this value into its payload as `versions.fl`, and
|
|
57
|
+
* `cloudflareinsights.com/cdn-cgi/rum` answers a payload without `fl` with a **404 that carries
|
|
58
|
+
* no CORS header**, which a browser prints as a CORS refusal and nobody reads as "recorded
|
|
59
|
+
* nothing". Same token, same src, driven in a real browser from a `*.cursedalchemy.com` origin:
|
|
60
|
+
* `{"token": …}` → 404 every time; `{"version": "2024.11.0", "token": …}` → 204 every time,
|
|
61
|
+
* whether the script path was pinned or not. Any value was accepted; this one is what the
|
|
62
|
+
* edge's own injection carries, so it is the value Cloudflare is known to send.
|
|
63
|
+
*
|
|
64
|
+
* Cloudflare's own `site_info` snippet OMITS it, so "exactly the vendor's text" was the
|
|
65
|
+
* defect, not the safety it looked like. `tools/check-web-analytics.ts` reds a hand copy
|
|
66
|
+
* without it.
|
|
67
|
+
*/
|
|
68
|
+
export declare const WEB_ANALYTICS_BEACON_VERSION = "2024.11.0";
|
|
69
|
+
/** The `data-cf-beacon` value — one spelling for the tag, the bootstrap and every hand copy. */
|
|
70
|
+
export declare function webAnalyticsBeaconConfig(token?: string): string;
|
|
71
|
+
/**
|
|
72
|
+
* The snippet as Cloudflare's own `site_info` endpoint spells it — plus the `version` key it
|
|
73
|
+
* leaves out, without which nothing is recorded (see {@link WEB_ANALYTICS_BEACON_VERSION}).
|
|
52
74
|
*
|
|
53
75
|
* 🔴 The single-quoted attributes and the space after `"token":` are Cloudflare's, not a
|
|
54
76
|
* style choice, and they are kept because this string is COMPARED against what an app's
|
|
@@ -104,7 +126,14 @@ export declare function webAnalyticsBootstrap(token?: string): string;
|
|
|
104
126
|
*
|
|
105
127
|
* Deliberately NOT a substring test against {@link WEB_ANALYTICS_TAG}: a shell is authored by
|
|
106
128
|
* hand and a formatter is allowed to re-wrap the tag across lines or re-order its attributes.
|
|
107
|
-
* What must be true is that the beacon's source is there
|
|
108
|
-
* token — the
|
|
129
|
+
* What must be true is that the beacon's source is there, that it is carrying the right
|
|
130
|
+
* token, and that its config carries a `version` — the three things that decide whether a
|
|
131
|
+
* pageview is recorded at all. The third was missed until 2026-09-22 and cost four days of
|
|
132
|
+
* every manual shell's data; see {@link WEB_ANALYTICS_BEACON_VERSION}.
|
|
109
133
|
*/
|
|
110
134
|
export declare function htmlCarriesWebAnalytics(html: string, token?: string): boolean;
|
|
135
|
+
/**
|
|
136
|
+
* Every `data-cf-beacon` config in `html` names a `version` — and there is at least one.
|
|
137
|
+
* Read up to the config's closing brace, so key order and a formatter's spacing do not matter.
|
|
138
|
+
*/
|
|
139
|
+
export declare function beaconConfigsCarryVersion(html: string): boolean;
|
|
@@ -48,7 +48,31 @@ export const WEB_ANALYTICS_SITE_TAG = "d1bea8562544421fbd2db5f11b74d1cc";
|
|
|
48
48
|
/** The `cursedalchemy.com` site's `data-cf-beacon` token. This is what the tag carries. */
|
|
49
49
|
export const WEB_ANALYTICS_SITE_TOKEN = "bd88d63678b64bc89f9702f48846bc88";
|
|
50
50
|
/**
|
|
51
|
-
* The
|
|
51
|
+
* 🔴 The `version` the beacon's config must carry, or NOTHING is recorded.
|
|
52
|
+
*
|
|
53
|
+
* Measured 2026-09-22, after the RUM API showed every manual-snippet shell recording ZERO
|
|
54
|
+
* pageviews from 2026-09-19 on — collections, music, roms, flix, station and desk, which had
|
|
55
|
+
* been 130–290 a day between them — while the hosts still on the edge's injection kept
|
|
56
|
+
* recording. The beacon copies this value into its payload as `versions.fl`, and
|
|
57
|
+
* `cloudflareinsights.com/cdn-cgi/rum` answers a payload without `fl` with a **404 that carries
|
|
58
|
+
* no CORS header**, which a browser prints as a CORS refusal and nobody reads as "recorded
|
|
59
|
+
* nothing". Same token, same src, driven in a real browser from a `*.cursedalchemy.com` origin:
|
|
60
|
+
* `{"token": …}` → 404 every time; `{"version": "2024.11.0", "token": …}` → 204 every time,
|
|
61
|
+
* whether the script path was pinned or not. Any value was accepted; this one is what the
|
|
62
|
+
* edge's own injection carries, so it is the value Cloudflare is known to send.
|
|
63
|
+
*
|
|
64
|
+
* Cloudflare's own `site_info` snippet OMITS it, so "exactly the vendor's text" was the
|
|
65
|
+
* defect, not the safety it looked like. `tools/check-web-analytics.ts` reds a hand copy
|
|
66
|
+
* without it.
|
|
67
|
+
*/
|
|
68
|
+
export const WEB_ANALYTICS_BEACON_VERSION = "2024.11.0";
|
|
69
|
+
/** The `data-cf-beacon` value — one spelling for the tag, the bootstrap and every hand copy. */
|
|
70
|
+
export function webAnalyticsBeaconConfig(token = WEB_ANALYTICS_SITE_TOKEN) {
|
|
71
|
+
return `{"version": "${WEB_ANALYTICS_BEACON_VERSION}", "token": "${token}"}`;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The snippet as Cloudflare's own `site_info` endpoint spells it — plus the `version` key it
|
|
75
|
+
* leaves out, without which nothing is recorded (see {@link WEB_ANALYTICS_BEACON_VERSION}).
|
|
52
76
|
*
|
|
53
77
|
* 🔴 The single-quoted attributes and the space after `"token":` are Cloudflare's, not a
|
|
54
78
|
* style choice, and they are kept because this string is COMPARED against what an app's
|
|
@@ -62,7 +86,7 @@ export const WEB_ANALYTICS_SITE_TOKEN = "bd88d63678b64bc89f9702f48846bc88";
|
|
|
62
86
|
export function webAnalyticsTag(token = WEB_ANALYTICS_SITE_TOKEN) {
|
|
63
87
|
return ("<!-- Cloudflare Web Analytics -->" +
|
|
64
88
|
`<script type='module' src='${WEB_ANALYTICS_BEACON_SRC}' ` +
|
|
65
|
-
`data-cf-beacon='
|
|
89
|
+
`data-cf-beacon='${webAnalyticsBeaconConfig(token)}'></script>` +
|
|
66
90
|
"<!-- End Cloudflare Web Analytics -->");
|
|
67
91
|
}
|
|
68
92
|
/** The fleet's tag, for the one site this generation serves. */
|
|
@@ -111,7 +135,7 @@ export function webAnalyticsBootstrap(token = WEB_ANALYTICS_SITE_TOKEN) {
|
|
|
111
135
|
`\t\tvar beacon = document.createElement("script");`,
|
|
112
136
|
`\t\tbeacon.type = "module";`,
|
|
113
137
|
`\t\tbeacon.src = "${WEB_ANALYTICS_BEACON_SRC}";`,
|
|
114
|
-
`\t\tbeacon.setAttribute("data-cf-beacon", '
|
|
138
|
+
`\t\tbeacon.setAttribute("data-cf-beacon", '${webAnalyticsBeaconConfig(token)}');`,
|
|
115
139
|
`\t\tdocument.head.appendChild(beacon);`,
|
|
116
140
|
"\t}",
|
|
117
141
|
"</script>",
|
|
@@ -122,9 +146,19 @@ export function webAnalyticsBootstrap(token = WEB_ANALYTICS_SITE_TOKEN) {
|
|
|
122
146
|
*
|
|
123
147
|
* Deliberately NOT a substring test against {@link WEB_ANALYTICS_TAG}: a shell is authored by
|
|
124
148
|
* hand and a formatter is allowed to re-wrap the tag across lines or re-order its attributes.
|
|
125
|
-
* What must be true is that the beacon's source is there
|
|
126
|
-
* token — the
|
|
149
|
+
* What must be true is that the beacon's source is there, that it is carrying the right
|
|
150
|
+
* token, and that its config carries a `version` — the three things that decide whether a
|
|
151
|
+
* pageview is recorded at all. The third was missed until 2026-09-22 and cost four days of
|
|
152
|
+
* every manual shell's data; see {@link WEB_ANALYTICS_BEACON_VERSION}.
|
|
127
153
|
*/
|
|
128
154
|
export function htmlCarriesWebAnalytics(html, token = WEB_ANALYTICS_SITE_TOKEN) {
|
|
129
|
-
return html.includes(WEB_ANALYTICS_BEACON_SRC) && html.includes(token);
|
|
155
|
+
return html.includes(WEB_ANALYTICS_BEACON_SRC) && html.includes(token) && beaconConfigsCarryVersion(html);
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Every `data-cf-beacon` config in `html` names a `version` — and there is at least one.
|
|
159
|
+
* Read up to the config's closing brace, so key order and a formatter's spacing do not matter.
|
|
160
|
+
*/
|
|
161
|
+
export function beaconConfigsCarryVersion(html) {
|
|
162
|
+
const configs = [...html.matchAll(/data-cf-beacon[^{]{0,24}(\{[^}]*\})/g)].map((m) => m[1] ?? "");
|
|
163
|
+
return configs.length > 0 && configs.every((c) => /"version"\s*:\s*"[^"]+"/.test(c));
|
|
130
164
|
}
|
|
@@ -40,6 +40,10 @@ export interface CpuClock {
|
|
|
40
40
|
* That is why {@link runCpuBench} drives its cases SERIALLY — a serial driver is what
|
|
41
41
|
* makes this proxy sound enough to gate on. Mounted in production it is advisory: useful
|
|
42
42
|
* for ranking routes, not for a billing claim.
|
|
43
|
+
*
|
|
44
|
+
* Where the runtime has no such reading, this returns {@link unavailableCpuClock} — a
|
|
45
|
+
* clock, never a throw. {@link probeProcessCpuUsage} is what decides, and it decides by
|
|
46
|
+
* CALLING.
|
|
43
47
|
*/
|
|
44
48
|
export declare function processCpuClock(): CpuClock;
|
|
45
49
|
/**
|
|
@@ -26,11 +26,15 @@
|
|
|
26
26
|
* That is why {@link runCpuBench} drives its cases SERIALLY — a serial driver is what
|
|
27
27
|
* makes this proxy sound enough to gate on. Mounted in production it is advisory: useful
|
|
28
28
|
* for ranking routes, not for a billing claim.
|
|
29
|
+
*
|
|
30
|
+
* Where the runtime has no such reading, this returns {@link unavailableCpuClock} — a
|
|
31
|
+
* clock, never a throw. {@link probeProcessCpuUsage} is what decides, and it decides by
|
|
32
|
+
* CALLING.
|
|
29
33
|
*/
|
|
30
34
|
export function processCpuClock() {
|
|
31
|
-
const
|
|
32
|
-
if (!
|
|
33
|
-
return unavailableCpuClock(
|
|
35
|
+
const probe = probeProcessCpuUsage();
|
|
36
|
+
if (!probe.ok)
|
|
37
|
+
return unavailableCpuClock(`process.cpuUsage (${probe.why})`);
|
|
34
38
|
return {
|
|
35
39
|
source: 'process.cpuUsage',
|
|
36
40
|
proxy: true,
|
|
@@ -44,6 +48,56 @@ export function processCpuClock() {
|
|
|
44
48
|
},
|
|
45
49
|
};
|
|
46
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* 🔴 **Is `process.cpuUsage()` a clock here? Answered by CALLING it, never by `typeof`.**
|
|
53
|
+
*
|
|
54
|
+
* Measured 2026-09-19 on the first real deploy of a Worker mounting `cpuBudget()`: under
|
|
55
|
+
* `nodejs_compat`, workerd's strategy for the parts of `node:process` it does not implement
|
|
56
|
+
* is to PROVIDE the method and throw when it is called —
|
|
57
|
+
*
|
|
58
|
+
* ```
|
|
59
|
+
* Error [ERR_METHOD_NOT_IMPLEMENTED]: The process.cpuUsage method is not implemented
|
|
60
|
+
* at process.cpuUsage (node-internal:public_process:235:11)
|
|
61
|
+
* ```
|
|
62
|
+
*
|
|
63
|
+
* — so `typeof process.cpuUsage === 'function'` answers *yes* about the one runtime the
|
|
64
|
+
* probe exists to answer *no* about. The clock that came back threw on first use, and
|
|
65
|
+
* because `cpuBudget()` is mounted FIRST by design, the throw landed in front of the whole
|
|
66
|
+
* app: every path 500, with 843 green tests behind it, because `bun test` runs where the
|
|
67
|
+
* method works.
|
|
68
|
+
*
|
|
69
|
+
* This is a capability CLASS, not one method: `nodejs_compat` stubs a great deal of
|
|
70
|
+
* `node:process`, `node:os` and `node:v8` the same way. `src/nodeBuiltinsAreProbedByCalling.spec.ts`
|
|
71
|
+
* is the check that keeps a `typeof` gate from coming back anywhere in this library.
|
|
72
|
+
*
|
|
73
|
+
* Both call shapes are exercised, because the span uses both and only one of them is
|
|
74
|
+
* probed by the first: `cpuUsage()` for the baseline and `cpuUsage(before)` for the delta.
|
|
75
|
+
* That costs two calls at wiring time — the same trade {@link workerCpuClock} argues for on
|
|
76
|
+
* the other side, and the alternative is a gate that reads zero for ever.
|
|
77
|
+
*/
|
|
78
|
+
function probeProcessCpuUsage() {
|
|
79
|
+
// Present-but-throwing and absent are both unavailable; the distinction only LABELS the
|
|
80
|
+
// clock, and is never the decision — which is why this line may carry the excuse that
|
|
81
|
+
// `src/nodeBuiltinsAreProbedByCalling.spec.ts` refuses everywhere else.
|
|
82
|
+
// probe-by-calling:ignore — label only; the call below is what decides.
|
|
83
|
+
const present = typeof process !== 'undefined' && typeof process.cpuUsage === 'function';
|
|
84
|
+
let reading;
|
|
85
|
+
try {
|
|
86
|
+
// A bare `process` that does not exist throws a ReferenceError in here, which is the
|
|
87
|
+
// same answer as a method that throws: not a clock.
|
|
88
|
+
const before = process.cpuUsage();
|
|
89
|
+
const delta = process.cpuUsage(before);
|
|
90
|
+
reading = before.user + before.system + delta.user + delta.system;
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return { ok: false, why: present ? 'throws' : 'absent' };
|
|
94
|
+
}
|
|
95
|
+
// A stub that returns something non-numeric is no more usable than one that throws, and
|
|
96
|
+
// would otherwise reach the recorder as a confident 0.00 for every route.
|
|
97
|
+
if (!Number.isFinite(reading))
|
|
98
|
+
return { ok: false, why: 'non-numeric' };
|
|
99
|
+
return { ok: true };
|
|
100
|
+
}
|
|
47
101
|
/**
|
|
48
102
|
* A clock over a CPU-ms reader the runtime supplies. `proxy: false` — this is the real
|
|
49
103
|
* quantity Cloudflare bills, so a report built on it may be quoted as one.
|
|
@@ -31,6 +31,33 @@ export interface BenchCase {
|
|
|
31
31
|
* that never ran — a green budget over a broken route.
|
|
32
32
|
*/
|
|
33
33
|
expectStatus?: number | number[] | ((status: number) => boolean);
|
|
34
|
+
/**
|
|
35
|
+
* Called before EVERY iteration of this case — warm-up included. For a **destructive**
|
|
36
|
+
* route, rebuild the fixture here; without it the second call measures an empty database.
|
|
37
|
+
*
|
|
38
|
+
* 🔴 This is the hook a destructive route cannot be benched without. `runCpuBench` fires a
|
|
39
|
+
* case `warmup + iterations` times against ONE app instance, which is exactly right for a
|
|
40
|
+
* read and a lie for a purge: the first call runs against a populated library and the next
|
|
41
|
+
* twenty-nine against an empty one, so the p99 is a small number for the wrong reason and
|
|
42
|
+
* the budget it satisfies means nothing. `expectStatus` cannot catch it either — the second
|
|
43
|
+
* call legitimately answers 200 with `{ deleted: 0 }`.
|
|
44
|
+
*
|
|
45
|
+
* It is **per case**, not run-wide, so the cheap read cases pay nothing for it — neither
|
|
46
|
+
* the rebuild nor the accounting {@link runCpuBench} does around it.
|
|
47
|
+
*
|
|
48
|
+
* 🔴 **Its cost is not measured, and that is checked rather than assumed.** The recorder is
|
|
49
|
+
* written through by the `cpuBudget()` middleware inside `app.fetch`, so a hook that runs
|
|
50
|
+
* outside that call is already excluded by ordering — but ordering is an accident, and a
|
|
51
|
+
* fixture rebuild that cost 200 ms and got attributed to the route would make this feature
|
|
52
|
+
* a worse lie than the hole it closes. So {@link runCpuBench} refuses a hook that drives
|
|
53
|
+
* the app under bench (see the error it throws), and `cpuBudget.spec.ts` proves a hook that
|
|
54
|
+
* burns real CPU leaves the route's number alone.
|
|
55
|
+
*
|
|
56
|
+
* `iteration` is 0-based and monotonic across the WHOLE case, warm-up first: warm-up gets
|
|
57
|
+
* `0 … warmup-1` and the measured pass continues from `warmup`. A hook that seeds ids from
|
|
58
|
+
* it therefore never collides between the two passes.
|
|
59
|
+
*/
|
|
60
|
+
beforeEach?: (c: BenchCase, iteration: number) => void | Promise<void>;
|
|
34
61
|
}
|
|
35
62
|
export interface RunCpuBenchOpts {
|
|
36
63
|
/** Anything with Hono's `fetch` shape. */
|
|
@@ -25,6 +25,39 @@ function statusAllowed(c, status) {
|
|
|
25
25
|
function describe(c) {
|
|
26
26
|
return `${(c.method ?? 'GET').toUpperCase()} ${c.path}`;
|
|
27
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Every request the recorder has attributed to any route so far. `samples` counts what was
|
|
30
|
+
* SEEN rather than what the reservoir retained, so this only ever goes up within a phase.
|
|
31
|
+
*/
|
|
32
|
+
function totalSamples(recorder) {
|
|
33
|
+
let n = 0;
|
|
34
|
+
for (const r of recorder.report().routes)
|
|
35
|
+
n += r.samples;
|
|
36
|
+
return n;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Run a case's {@link BenchCase.beforeEach}, and prove it stayed out of the histogram.
|
|
40
|
+
*
|
|
41
|
+
* 🔴 The obvious way to rebuild a fixture is to drive the app that already has the routes —
|
|
42
|
+
* `POST /api/upload` thirty times to re-seed a library. Those requests go through
|
|
43
|
+
* `cpuBudget()` like any other, so the rebuild lands in the report as real samples on real
|
|
44
|
+
* routes, and the destructive route this hook exists to measure honestly is now benched
|
|
45
|
+
* beside rows nobody asked for. Refusing it here is cheap and only costs a case that has a
|
|
46
|
+
* hook at all.
|
|
47
|
+
*/
|
|
48
|
+
async function prepare(opts, c, iteration) {
|
|
49
|
+
if (!c.beforeEach)
|
|
50
|
+
return;
|
|
51
|
+
const before = totalSamples(opts.recorder);
|
|
52
|
+
await c.beforeEach(c, iteration);
|
|
53
|
+
const after = totalSamples(opts.recorder);
|
|
54
|
+
if (after !== before) {
|
|
55
|
+
throw new Error(`cpu-bench: ${describe(c)}'s beforeEach drove ${after - before} request(s) through the ` +
|
|
56
|
+
'app under bench, so the fixture rebuild is now IN the measurement it exists to keep ' +
|
|
57
|
+
'out of it. Rebuild the fixture directly — against the database, the store, the ' +
|
|
58
|
+
'seed helper — never through app.fetch.');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
28
61
|
async function fire(opts, c) {
|
|
29
62
|
const method = (c.method ?? c.init?.method ?? 'GET').toUpperCase();
|
|
30
63
|
const req = new Request(`${opts.origin ?? BENCH_ORIGIN}${c.path}`, { ...c.init, method });
|
|
@@ -46,16 +79,23 @@ export async function runCpuBench(opts) {
|
|
|
46
79
|
const warmup = opts.warmup ?? 5;
|
|
47
80
|
if (!(iterations > 0))
|
|
48
81
|
throw new Error('runCpuBench: iterations must be > 0');
|
|
82
|
+
// `beforeEach` runs over the warm-up too, deliberately. Warm-up exists to reach a handler's
|
|
83
|
+
// steady state, and a destructive route warmed against the empty database it emptied warms
|
|
84
|
+
// the wrong branch — the loop the p99 is about never runs.
|
|
49
85
|
for (const c of opts.cases) {
|
|
50
|
-
for (let i = 0; i < warmup; i += 1)
|
|
86
|
+
for (let i = 0; i < warmup; i += 1) {
|
|
87
|
+
await prepare(opts, c, i);
|
|
51
88
|
await fire(opts, c);
|
|
89
|
+
}
|
|
52
90
|
}
|
|
53
91
|
// 🔴 Everything above was warm-up. Discard it — measuring it is the bug this guards.
|
|
54
92
|
opts.recorder.reset();
|
|
55
93
|
for (const c of opts.cases) {
|
|
56
94
|
const n = c.iterations ?? iterations;
|
|
57
|
-
for (let i = 0; i < n; i += 1)
|
|
95
|
+
for (let i = 0; i < n; i += 1) {
|
|
96
|
+
await prepare(opts, c, warmup + i);
|
|
58
97
|
await fire(opts, c);
|
|
98
|
+
}
|
|
59
99
|
}
|
|
60
100
|
return opts.recorder.report();
|
|
61
101
|
}
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* ```
|
|
16
16
|
*/
|
|
17
17
|
export { type BackupPoint, createTimeTravelBackup, type DatabaseBackup, type TimeTravelOpts, } from './backup';
|
|
18
|
-
export { type InvocationD1, perInvocation } from './invocation';
|
|
18
|
+
export { createPendingWrites, type InvocationD1, type PendingWrites, perInvocation, type WriteCollector } from './invocation';
|
|
19
19
|
export { assertBatchSize, assertWithinLimits, chunkForBind, LIMITS } from './limits';
|
|
20
20
|
export { createLocalD1, refuseInteractiveTransaction } from './local';
|
|
21
21
|
export { createRemoteD1, type D1BindingLike, type D1BindingMeta, type D1BindingResult, type D1BindingStatement, } from './remote';
|
package/dist/server/d1/index.js
CHANGED
|
@@ -33,7 +33,7 @@ export { createTimeTravelBackup, } from './backup';
|
|
|
33
33
|
// that does not use the query builder — which per `../db/kysely.ts`'s own header is most
|
|
34
34
|
// of them, since business tables stay on raw statements. Measured 2026-09-18 against the
|
|
35
35
|
// published 4.3.0 tarball. `barrelsReachNoOptionalPeer.spec.ts` is what keeps it out.
|
|
36
|
-
export { perInvocation } from './invocation';
|
|
36
|
+
export { createPendingWrites, perInvocation } from './invocation';
|
|
37
37
|
export { assertBatchSize, assertWithinLimits, chunkForBind, LIMITS } from './limits';
|
|
38
38
|
export { createLocalD1, refuseInteractiveTransaction } from './local';
|
|
39
39
|
export { createRemoteD1, } from './remote';
|
|
@@ -39,6 +39,14 @@
|
|
|
39
39
|
* It wraps either driver, because it sits ABOVE the seam and only counts. That is what
|
|
40
40
|
* makes the local gate able to prove a production-only limit: wrap a local database in a
|
|
41
41
|
* test, run the loop, and the laptop fails exactly where the Worker would.
|
|
42
|
+
*
|
|
43
|
+
* ## The other thing that is true for the span of one request: {@link createPendingWrites}
|
|
44
|
+
*
|
|
45
|
+
* A Worker port's synchronous stores answer from a snapshot and queue their durable writes
|
|
46
|
+
* as promises, which the entry point awaits BEFORE the response leaves. That collector has
|
|
47
|
+
* the same lifetime as the budget above — one per invocation, constructed by the caller in
|
|
48
|
+
* the same breath — so it lives in the same file. See its own header for why it was moved
|
|
49
|
+
* here on 2026-09-19.
|
|
42
50
|
*/
|
|
43
51
|
import { type D1LikeDatabase } from './types';
|
|
44
52
|
/**
|
|
@@ -70,3 +78,67 @@ export interface InvocationD1 extends D1LikeDatabase {
|
|
|
70
78
|
export declare function perInvocation(db: D1LikeDatabase, opts?: {
|
|
71
79
|
max?: number;
|
|
72
80
|
}): InvocationD1;
|
|
81
|
+
/**
|
|
82
|
+
* Where a durable write goes on its way out of one invocation.
|
|
83
|
+
*
|
|
84
|
+
* Deliberately two members, and the narrower `add`-only half is what a STORE takes. A store
|
|
85
|
+
* that also held `settle` could end the request's writes from inside the request, which is
|
|
86
|
+
* the entry point's job and nobody else's. `cursedauth/d1-stores` declares exactly this
|
|
87
|
+
* two-method shape under its own name (`WriteCollector`) because that package sits UNDER
|
|
88
|
+
* this one and may not import it — structural typing means an object made here satisfies it
|
|
89
|
+
* verbatim, with no dependency edge in either direction.
|
|
90
|
+
*/
|
|
91
|
+
export interface WriteCollector {
|
|
92
|
+
add(promise: Promise<unknown>): void;
|
|
93
|
+
}
|
|
94
|
+
/** A write that must land before the response is returned. See {@link createPendingWrites}. */
|
|
95
|
+
export interface PendingWrites extends WriteCollector {
|
|
96
|
+
/** Await every collected write. Called BEFORE the response is returned, never after. */
|
|
97
|
+
settle(): Promise<void>;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Collect the durable writes of ONE invocation, to be awaited before the response leaves.
|
|
101
|
+
*
|
|
102
|
+
* ## Why this is the Worker-write primitive and not a convenience
|
|
103
|
+
*
|
|
104
|
+
* A ported app's synchronous stores cannot await — their signatures are somebody else's
|
|
105
|
+
* published contract — so the shape every port lands on is: one read fills a snapshot, every
|
|
106
|
+
* sync call answers from it, and every mutation both updates the snapshot and pushes its
|
|
107
|
+
* statement's promise in here. The entry point then does the awaiting:
|
|
108
|
+
*
|
|
109
|
+
* ```ts
|
|
110
|
+
* export default {
|
|
111
|
+
* async fetch(req: Request, env: Env) {
|
|
112
|
+
* const db = perInvocation(createRemoteD1(env.DB));
|
|
113
|
+
* const writes = createPendingWrites();
|
|
114
|
+
* const res = await app.fetch(req, { db, writes });
|
|
115
|
+
* await writes.settle(); // 🔴 BEFORE the response, never after
|
|
116
|
+
* return res;
|
|
117
|
+
* },
|
|
118
|
+
* };
|
|
119
|
+
* ```
|
|
120
|
+
*
|
|
121
|
+
* 🔴 **`settle()` before the response, never `ctx.waitUntil`.** A Worker isolate is per-colo
|
|
122
|
+
* and short-lived; a write acknowledged by a 200 and then dropped means "sign out" reported
|
|
123
|
+
* success and left the credential live — a failure invisible to any single-process test and
|
|
124
|
+
* indistinguishable, to the person, from the feature being broken.
|
|
125
|
+
*
|
|
126
|
+
* 🔴 **`Promise.all`, not `allSettled`.** A failed durable write must not be reported as a
|
|
127
|
+
* success. It surfaces as a 500 from the entry point, which is the honest answer to "did my
|
|
128
|
+
* sign-out take". `allSettled` here would convert every lost write into a silent 200 — the
|
|
129
|
+
* same outcome as not collecting them at all, reached by a road that looks careful.
|
|
130
|
+
*
|
|
131
|
+
* ## Why it lives here
|
|
132
|
+
*
|
|
133
|
+
* It was written three times before it was published once — `apps/patterns/worker/stores.ts`,
|
|
134
|
+
* `apps/collections/worker/stores.ts` and `libs/cursedauth/src/d1Stores.ts`, byte-identical
|
|
135
|
+
* but for a comment, with six more ports queued behind them. It is a D1-INVOCATION primitive
|
|
136
|
+
* rather than an auth one, so it belongs beside {@link perInvocation}: both are constructed
|
|
137
|
+
* per request, both describe what is true for the span of one, and neither knows anything
|
|
138
|
+
* about who is signed in.
|
|
139
|
+
*
|
|
140
|
+
* Ordering is NOT what this gives you. Everything added is already in flight, so two writes
|
|
141
|
+
* that must land in order go in one `batch()`, never in two `add`s — a DELETE that must
|
|
142
|
+
* precede its INSERT will otherwise race and trip a unique index.
|
|
143
|
+
*/
|
|
144
|
+
export declare function createPendingWrites(): PendingWrites;
|
|
@@ -39,6 +39,14 @@
|
|
|
39
39
|
* It wraps either driver, because it sits ABOVE the seam and only counts. That is what
|
|
40
40
|
* makes the local gate able to prove a production-only limit: wrap a local database in a
|
|
41
41
|
* test, run the loop, and the laptop fails exactly where the Worker would.
|
|
42
|
+
*
|
|
43
|
+
* ## The other thing that is true for the span of one request: {@link createPendingWrites}
|
|
44
|
+
*
|
|
45
|
+
* A Worker port's synchronous stores answer from a snapshot and queue their durable writes
|
|
46
|
+
* as promises, which the entry point awaits BEFORE the response leaves. That collector has
|
|
47
|
+
* the same lifetime as the budget above — one per invocation, constructed by the caller in
|
|
48
|
+
* the same breath — so it lives in the same file. See its own header for why it was moved
|
|
49
|
+
* here on 2026-09-19.
|
|
42
50
|
*/
|
|
43
51
|
import { LIMITS } from './limits';
|
|
44
52
|
import { D1LimitError } from './types';
|
|
@@ -164,3 +172,60 @@ export function perInvocation(db, opts = {}) {
|
|
|
164
172
|
},
|
|
165
173
|
};
|
|
166
174
|
}
|
|
175
|
+
/**
|
|
176
|
+
* Collect the durable writes of ONE invocation, to be awaited before the response leaves.
|
|
177
|
+
*
|
|
178
|
+
* ## Why this is the Worker-write primitive and not a convenience
|
|
179
|
+
*
|
|
180
|
+
* A ported app's synchronous stores cannot await — their signatures are somebody else's
|
|
181
|
+
* published contract — so the shape every port lands on is: one read fills a snapshot, every
|
|
182
|
+
* sync call answers from it, and every mutation both updates the snapshot and pushes its
|
|
183
|
+
* statement's promise in here. The entry point then does the awaiting:
|
|
184
|
+
*
|
|
185
|
+
* ```ts
|
|
186
|
+
* export default {
|
|
187
|
+
* async fetch(req: Request, env: Env) {
|
|
188
|
+
* const db = perInvocation(createRemoteD1(env.DB));
|
|
189
|
+
* const writes = createPendingWrites();
|
|
190
|
+
* const res = await app.fetch(req, { db, writes });
|
|
191
|
+
* await writes.settle(); // 🔴 BEFORE the response, never after
|
|
192
|
+
* return res;
|
|
193
|
+
* },
|
|
194
|
+
* };
|
|
195
|
+
* ```
|
|
196
|
+
*
|
|
197
|
+
* 🔴 **`settle()` before the response, never `ctx.waitUntil`.** A Worker isolate is per-colo
|
|
198
|
+
* and short-lived; a write acknowledged by a 200 and then dropped means "sign out" reported
|
|
199
|
+
* success and left the credential live — a failure invisible to any single-process test and
|
|
200
|
+
* indistinguishable, to the person, from the feature being broken.
|
|
201
|
+
*
|
|
202
|
+
* 🔴 **`Promise.all`, not `allSettled`.** A failed durable write must not be reported as a
|
|
203
|
+
* success. It surfaces as a 500 from the entry point, which is the honest answer to "did my
|
|
204
|
+
* sign-out take". `allSettled` here would convert every lost write into a silent 200 — the
|
|
205
|
+
* same outcome as not collecting them at all, reached by a road that looks careful.
|
|
206
|
+
*
|
|
207
|
+
* ## Why it lives here
|
|
208
|
+
*
|
|
209
|
+
* It was written three times before it was published once — `apps/patterns/worker/stores.ts`,
|
|
210
|
+
* `apps/collections/worker/stores.ts` and `libs/cursedauth/src/d1Stores.ts`, byte-identical
|
|
211
|
+
* but for a comment, with six more ports queued behind them. It is a D1-INVOCATION primitive
|
|
212
|
+
* rather than an auth one, so it belongs beside {@link perInvocation}: both are constructed
|
|
213
|
+
* per request, both describe what is true for the span of one, and neither knows anything
|
|
214
|
+
* about who is signed in.
|
|
215
|
+
*
|
|
216
|
+
* Ordering is NOT what this gives you. Everything added is already in flight, so two writes
|
|
217
|
+
* that must land in order go in one `batch()`, never in two `add`s — a DELETE that must
|
|
218
|
+
* precede its INSERT will otherwise race and trip a unique index.
|
|
219
|
+
*/
|
|
220
|
+
export function createPendingWrites() {
|
|
221
|
+
const pending = [];
|
|
222
|
+
return {
|
|
223
|
+
add: (promise) => void pending.push(promise),
|
|
224
|
+
async settle() {
|
|
225
|
+
// `all`, not `allSettled`: a failed durable write must not be reported as a
|
|
226
|
+
// success. It surfaces as a 500 from the entry point, which is the honest answer
|
|
227
|
+
// to "did my sign-out take".
|
|
228
|
+
await Promise.all(pending);
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
}
|
|
@@ -51,9 +51,14 @@ export interface MasterLockGuardOptions extends LockPageOptions {
|
|
|
51
51
|
* loosening it: nothing loads from anywhere but this origin, the form posts nowhere (the
|
|
52
52
|
* script does the POST), and the page cannot be framed.
|
|
53
53
|
*
|
|
54
|
-
* 🔴 **EXPORTED so that nothing has to copy it
|
|
55
|
-
*
|
|
56
|
-
*
|
|
54
|
+
* 🔴 **EXPORTED so that nothing has to copy it** — and exporting it is not the same as nothing
|
|
55
|
+
* copying it. This sentence used to read *"`apps/collections` had a hand-typed duplicate … until
|
|
56
|
+
* 2026-09-18"*, and it was false the day it was written: the export landed, and BOTH re-typed
|
|
57
|
+
* copies stayed — one in `apps/collections/scripts/injectedScripts.test.ts` and one three
|
|
58
|
+
* directories from here in `../analytics/injectedScripts.spec.ts`. They were collapsed onto this
|
|
59
|
+
* constant on 2026-09-21. A policy that is typed twice can be loosened in one place and stay
|
|
60
|
+
* green in the other, which is a wall that no longer refuses what its own test says it does.
|
|
61
|
+
* It is also what
|
|
57
62
|
* `beaconNeverReachesTheWall.spec.ts` measures the beacon against: the wall refuses
|
|
58
63
|
* `static.cloudflareinsights.com`, so the analytics tag and this response are mutually
|
|
59
64
|
* exclusive BY CONSTRUCTION rather than by anyone remembering. The owner's reason, 2026-09-18:
|
|
@@ -20,9 +20,14 @@ const noStore = { "cache-control": "no-store, no-cache, must-revalidate", pragma
|
|
|
20
20
|
* loosening it: nothing loads from anywhere but this origin, the form posts nowhere (the
|
|
21
21
|
* script does the POST), and the page cannot be framed.
|
|
22
22
|
*
|
|
23
|
-
* 🔴 **EXPORTED so that nothing has to copy it
|
|
24
|
-
*
|
|
25
|
-
*
|
|
23
|
+
* 🔴 **EXPORTED so that nothing has to copy it** — and exporting it is not the same as nothing
|
|
24
|
+
* copying it. This sentence used to read *"`apps/collections` had a hand-typed duplicate … until
|
|
25
|
+
* 2026-09-18"*, and it was false the day it was written: the export landed, and BOTH re-typed
|
|
26
|
+
* copies stayed — one in `apps/collections/scripts/injectedScripts.test.ts` and one three
|
|
27
|
+
* directories from here in `../analytics/injectedScripts.spec.ts`. They were collapsed onto this
|
|
28
|
+
* constant on 2026-09-21. A policy that is typed twice can be loosened in one place and stay
|
|
29
|
+
* green in the other, which is a wall that no longer refuses what its own test says it does.
|
|
30
|
+
* It is also what
|
|
26
31
|
* `beaconNeverReachesTheWall.spec.ts` measures the beacon against: the wall refuses
|
|
27
32
|
* `static.cloudflareinsights.com`, so the analytics tag and this response are mutually
|
|
28
33
|
* exclusive BY CONSTRUCTION rather than by anyone remembering. The owner's reason, 2026-09-18:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursedbelt-server",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.11.1",
|
|
4
4
|
"license": "ISC",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "The app-facing Bun/Hono server tier of the cursedbelt split \u2014 storage, sharing, activity, guard, sync. React-free; cursedbelt-core below it.",
|