cursedbelt-server 4.17.1 → 4.18.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/d1/pullD1.js +16 -3
- package/dist/server/telemetry.d.ts +19 -0
- package/dist/server/telemetry.js +19 -0
- package/package.json +7 -1
- package/src/server/d1/pullD1.spec.ts +20 -0
- package/src/server/d1/pullD1.ts +18 -2
- package/src/server/telemetry.ts +27 -0
- package/src/server/telemetryIsWorkerSafe.spec.ts +42 -0
package/dist/server/d1/pullD1.js
CHANGED
|
@@ -82,6 +82,8 @@ export async function servedRuntime(url, { attempts = 3, sleep = (ms) => new Pro
|
|
|
82
82
|
}
|
|
83
83
|
return null;
|
|
84
84
|
}
|
|
85
|
+
/** How many times one table's export is asked for when wrangler says success and writes nothing. */
|
|
86
|
+
const EXPORT_ATTEMPTS = 3;
|
|
85
87
|
/**
|
|
86
88
|
* The tables a snapshot carries, from D1's own `sqlite_master` rows — every real table, and
|
|
87
89
|
* none of what cannot or must not be re-inserted: a virtual table and its shadows (`_config`,
|
|
@@ -123,12 +125,23 @@ export async function pullD1ToSqlite(options) {
|
|
|
123
125
|
let sql = '';
|
|
124
126
|
for (const table of tables) {
|
|
125
127
|
const dump = `${destination}.${table}.sql`;
|
|
126
|
-
|
|
128
|
+
// 🔴 `wrangler d1 export` has been measured answering SUCCESS and writing no file —
|
|
129
|
+
// collections' nightly of 2026-09-23T15:35Z refused on `derivative_retries`, and the same
|
|
130
|
+
// command wrote the file (32 bytes, an empty table) an hour later. So a missing file is
|
|
131
|
+
// asked for again, twice, before it is a refusal; a failure that persists still refuses,
|
|
132
|
+
// because a snapshot missing a table is not a backup.
|
|
133
|
+
let exported = { code: 1, stdout: '', stderr: 'not attempted' };
|
|
134
|
+
for (let attempt = 1; attempt <= EXPORT_ATTEMPTS; attempt++) {
|
|
135
|
+
exported = wrangler(['d1', 'export', database, '--remote', '--env=', '--table', table, '--no-schema', '--output', dump, '-y']);
|
|
136
|
+
if (exported.code !== 0 || existsSync(dump))
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
127
139
|
if (exported.code !== 0) {
|
|
128
140
|
throw new Error(`wrangler d1 export --table ${table} failed: ${exported.stderr.trim() || exported.stdout.trim()}`);
|
|
129
141
|
}
|
|
130
|
-
if (!existsSync(dump))
|
|
131
|
-
throw new Error(`wrangler d1 export --table ${table} reported success and wrote no file at ${dump}`);
|
|
142
|
+
if (!existsSync(dump)) {
|
|
143
|
+
throw new Error(`wrangler d1 export --table ${table} reported success ${EXPORT_ATTEMPTS} times and wrote no file at ${dump}`);
|
|
144
|
+
}
|
|
132
145
|
sql += `${readFileSync(dump, 'utf8')}\n`;
|
|
133
146
|
}
|
|
134
147
|
if (!/INSERT INTO/i.test(sql)) {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cursedbelt-server/telemetry` — the telemetry seam, from a WORKER.
|
|
3
|
+
*
|
|
4
|
+
* `requestLogger` and `createTelemetrySink` are built for exactly one host that most needs them:
|
|
5
|
+
* a Worker, where a row per request in D1 is the one line on Cloudflare's price list with a
|
|
6
|
+
* dollar-scale overage and Workers Analytics Engine is the purpose-built sink
|
|
7
|
+
* (`./metrics/telemetrySink.ts`). Until 2026-09-23 the only door to them was
|
|
8
|
+
* `cursedbelt-server/middleware`, whose export map sends every non-Bun importer to
|
|
9
|
+
* `_bunOnly.js` because the barrel carries Bun-only middleware beside them — so `wrangler`
|
|
10
|
+
* refused the import (`No matching export … for import "requestLogger"`, measured on `music`'s
|
|
11
|
+
* first attempt to mount it) and the seam could reach every host except the one it was for.
|
|
12
|
+
*
|
|
13
|
+
* Everything here is runtime-safe on workerd: `bun:sqlite` appears only as a TYPE (a Mac host
|
|
14
|
+
* passes its `Database`; a Worker passes `null` and an `analytics` binding).
|
|
15
|
+
* `telemetryIsWorkerSafe.spec.ts` walks this file's runtime import graph and fails on any
|
|
16
|
+
* `bun:` import, so it stays that way.
|
|
17
|
+
*/
|
|
18
|
+
export { type RequestLoggerOpts, requestLogger } from './middleware/requestLogger.js';
|
|
19
|
+
export { type AnalyticsEngineDataset, createTelemetrySink, TELEMETRY_POINT_VERSION, type TelemetryEvent, type TelemetrySink, type TelemetrySinkOptions, toDataPoint, } from './metrics/telemetrySink.js';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cursedbelt-server/telemetry` — the telemetry seam, from a WORKER.
|
|
3
|
+
*
|
|
4
|
+
* `requestLogger` and `createTelemetrySink` are built for exactly one host that most needs them:
|
|
5
|
+
* a Worker, where a row per request in D1 is the one line on Cloudflare's price list with a
|
|
6
|
+
* dollar-scale overage and Workers Analytics Engine is the purpose-built sink
|
|
7
|
+
* (`./metrics/telemetrySink.ts`). Until 2026-09-23 the only door to them was
|
|
8
|
+
* `cursedbelt-server/middleware`, whose export map sends every non-Bun importer to
|
|
9
|
+
* `_bunOnly.js` because the barrel carries Bun-only middleware beside them — so `wrangler`
|
|
10
|
+
* refused the import (`No matching export … for import "requestLogger"`, measured on `music`'s
|
|
11
|
+
* first attempt to mount it) and the seam could reach every host except the one it was for.
|
|
12
|
+
*
|
|
13
|
+
* Everything here is runtime-safe on workerd: `bun:sqlite` appears only as a TYPE (a Mac host
|
|
14
|
+
* passes its `Database`; a Worker passes `null` and an `analytics` binding).
|
|
15
|
+
* `telemetryIsWorkerSafe.spec.ts` walks this file's runtime import graph and fails on any
|
|
16
|
+
* `bun:` import, so it stays that way.
|
|
17
|
+
*/
|
|
18
|
+
export { requestLogger } from './middleware/requestLogger.js';
|
|
19
|
+
export { createTelemetrySink, TELEMETRY_POINT_VERSION, toDataPoint, } from './metrics/telemetrySink.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursedbelt-server",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.18.1",
|
|
4
4
|
"license": "ISC",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "The app-facing Bun/Hono server tier of the cursedbelt split — storage, sharing, activity, guard, sync. React-free; cursedbelt-core below it.",
|
|
@@ -163,6 +163,12 @@
|
|
|
163
163
|
"source": "./src/server/master-lock/index.ts",
|
|
164
164
|
"import": "./dist/server/master-lock/index.js"
|
|
165
165
|
},
|
|
166
|
+
"./telemetry": {
|
|
167
|
+
"types": "./dist/server/telemetry.d.ts",
|
|
168
|
+
"bun": "./src/server/telemetry.ts",
|
|
169
|
+
"source": "./src/server/telemetry.ts",
|
|
170
|
+
"import": "./dist/server/telemetry.js"
|
|
171
|
+
},
|
|
166
172
|
"./middleware": {
|
|
167
173
|
"types": "./dist/server/middleware/index.d.ts",
|
|
168
174
|
"bun": "./src/server/middleware/index.ts",
|
|
@@ -172,6 +172,26 @@ describe('pulling D1 down — table by table, because a whole-database export re
|
|
|
172
172
|
).rejects.toThrow(/lists no `tracks` table/);
|
|
173
173
|
});
|
|
174
174
|
|
|
175
|
+
test('an export that says success and writes NO file is asked again — and refuses if it never does', async () => {
|
|
176
|
+
const dir = scratch();
|
|
177
|
+
let flaky = 1;
|
|
178
|
+
const run: Wrangler = (args) => {
|
|
179
|
+
if (args[1] === 'execute') return { code: 0, stdout: JSON.stringify([{ results: MASTER }]), stderr: '' };
|
|
180
|
+
const table = args[args.indexOf('--table') + 1] as string;
|
|
181
|
+
const out = args[args.indexOf('--output') + 1] as string;
|
|
182
|
+
// `meta` "succeeds" once without writing — measured on collections, 2026-09-23.
|
|
183
|
+
if (table === 'meta' && flaky-- > 0) return { code: 0, stdout: 'Downloaded successfully', stderr: '' };
|
|
184
|
+
writeFileSync(out, table === 'items' ? "INSERT INTO items (id) VALUES ('i1');" : '');
|
|
185
|
+
return { code: 0, stdout: '', stderr: '' };
|
|
186
|
+
};
|
|
187
|
+
await pull(dir, run);
|
|
188
|
+
const never: Wrangler = (args) =>
|
|
189
|
+
args[1] === 'execute'
|
|
190
|
+
? { code: 0, stdout: JSON.stringify([{ results: MASTER }]), stderr: '' }
|
|
191
|
+
: { code: 0, stdout: 'Downloaded successfully', stderr: '' };
|
|
192
|
+
await expect(pull(scratch(), never)).rejects.toThrow(/reported success 3 times and wrote no file/);
|
|
193
|
+
});
|
|
194
|
+
|
|
175
195
|
test('🔴 one table failing to export THROWS — never a snapshot missing that table', async () => {
|
|
176
196
|
const dir = scratch();
|
|
177
197
|
const { run } = fake((table) => (table === 'meta' ? null : "INSERT INTO items (id) VALUES ('x');"));
|
package/src/server/d1/pullD1.ts
CHANGED
|
@@ -97,6 +97,9 @@ export async function servedRuntime(
|
|
|
97
97
|
return null;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
/** How many times one table's export is asked for when wrangler says success and writes nothing. */
|
|
101
|
+
const EXPORT_ATTEMPTS = 3;
|
|
102
|
+
|
|
100
103
|
/** One `wrangler` invocation, as {@link pullD1ToSqlite} needs it. Injected so a spec runs none. */
|
|
101
104
|
export type Wrangler = (args: string[]) => { code: number; stdout: string; stderr: string };
|
|
102
105
|
|
|
@@ -160,11 +163,24 @@ export async function pullD1ToSqlite(options: PullD1Options): Promise<void> {
|
|
|
160
163
|
let sql = '';
|
|
161
164
|
for (const table of tables) {
|
|
162
165
|
const dump = `${destination}.${table}.sql`;
|
|
163
|
-
|
|
166
|
+
// 🔴 `wrangler d1 export` has been measured answering SUCCESS and writing no file —
|
|
167
|
+
// collections' nightly of 2026-09-23T15:35Z refused on `derivative_retries`, and the same
|
|
168
|
+
// command wrote the file (32 bytes, an empty table) an hour later. So a missing file is
|
|
169
|
+
// asked for again, twice, before it is a refusal; a failure that persists still refuses,
|
|
170
|
+
// because a snapshot missing a table is not a backup.
|
|
171
|
+
let exported = { code: 1, stdout: '', stderr: 'not attempted' };
|
|
172
|
+
for (let attempt = 1; attempt <= EXPORT_ATTEMPTS; attempt++) {
|
|
173
|
+
exported = wrangler(['d1', 'export', database, '--remote', '--env=', '--table', table, '--no-schema', '--output', dump, '-y']);
|
|
174
|
+
if (exported.code !== 0 || existsSync(dump)) break;
|
|
175
|
+
}
|
|
164
176
|
if (exported.code !== 0) {
|
|
165
177
|
throw new Error(`wrangler d1 export --table ${table} failed: ${exported.stderr.trim() || exported.stdout.trim()}`);
|
|
166
178
|
}
|
|
167
|
-
if (!existsSync(dump))
|
|
179
|
+
if (!existsSync(dump)) {
|
|
180
|
+
throw new Error(
|
|
181
|
+
`wrangler d1 export --table ${table} reported success ${EXPORT_ATTEMPTS} times and wrote no file at ${dump}`,
|
|
182
|
+
);
|
|
183
|
+
}
|
|
168
184
|
sql += `${readFileSync(dump, 'utf8')}\n`;
|
|
169
185
|
}
|
|
170
186
|
if (!/INSERT INTO/i.test(sql)) {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cursedbelt-server/telemetry` — the telemetry seam, from a WORKER.
|
|
3
|
+
*
|
|
4
|
+
* `requestLogger` and `createTelemetrySink` are built for exactly one host that most needs them:
|
|
5
|
+
* a Worker, where a row per request in D1 is the one line on Cloudflare's price list with a
|
|
6
|
+
* dollar-scale overage and Workers Analytics Engine is the purpose-built sink
|
|
7
|
+
* (`./metrics/telemetrySink.ts`). Until 2026-09-23 the only door to them was
|
|
8
|
+
* `cursedbelt-server/middleware`, whose export map sends every non-Bun importer to
|
|
9
|
+
* `_bunOnly.js` because the barrel carries Bun-only middleware beside them — so `wrangler`
|
|
10
|
+
* refused the import (`No matching export … for import "requestLogger"`, measured on `music`'s
|
|
11
|
+
* first attempt to mount it) and the seam could reach every host except the one it was for.
|
|
12
|
+
*
|
|
13
|
+
* Everything here is runtime-safe on workerd: `bun:sqlite` appears only as a TYPE (a Mac host
|
|
14
|
+
* passes its `Database`; a Worker passes `null` and an `analytics` binding).
|
|
15
|
+
* `telemetryIsWorkerSafe.spec.ts` walks this file's runtime import graph and fails on any
|
|
16
|
+
* `bun:` import, so it stays that way.
|
|
17
|
+
*/
|
|
18
|
+
export { type RequestLoggerOpts, requestLogger } from './middleware/requestLogger.js';
|
|
19
|
+
export {
|
|
20
|
+
type AnalyticsEngineDataset,
|
|
21
|
+
createTelemetrySink,
|
|
22
|
+
TELEMETRY_POINT_VERSION,
|
|
23
|
+
type TelemetryEvent,
|
|
24
|
+
type TelemetrySink,
|
|
25
|
+
type TelemetrySinkOptions,
|
|
26
|
+
toDataPoint,
|
|
27
|
+
} from './metrics/telemetrySink.js';
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, join, resolve } from 'node:path';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `cursedbelt-server/telemetry` must be importable by a Worker: nothing reachable from it at
|
|
7
|
+
* RUNTIME may import `bun:*`. A `import type` / `export type` is erased and does not count.
|
|
8
|
+
*/
|
|
9
|
+
const RUNTIME_IMPORT = /^\s*(?:import|export)\s+(?!type\b)(?:[^'"]*?from\s+)?['"]([^'"]+)['"]/gm;
|
|
10
|
+
|
|
11
|
+
function runtimeGraph(entry: string): { files: string[]; bunImports: string[] } {
|
|
12
|
+
const seen = new Set<string>();
|
|
13
|
+
const bunImports: string[] = [];
|
|
14
|
+
const walk = (file: string): void => {
|
|
15
|
+
if (seen.has(file)) return;
|
|
16
|
+
seen.add(file);
|
|
17
|
+
const source = readFileSync(file, 'utf8');
|
|
18
|
+
for (const match of source.matchAll(RUNTIME_IMPORT)) {
|
|
19
|
+
const spec = match[1] as string;
|
|
20
|
+
if (spec.startsWith('bun:')) bunImports.push(`${file.split('/src/')[1]} → ${spec}`);
|
|
21
|
+
if (spec.startsWith('.')) walk(resolve(dirname(file), spec.replace(/\.js$/, '.ts')));
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
walk(entry);
|
|
25
|
+
return { files: [...seen], bunImports };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe('cursedbelt-server/telemetry', () => {
|
|
29
|
+
test('🔴 reaches no `bun:` module at runtime — a Worker can import it', () => {
|
|
30
|
+
const { files, bunImports } = runtimeGraph(join(import.meta.dir, 'telemetry.ts'));
|
|
31
|
+
expect(bunImports).toEqual([]);
|
|
32
|
+
// Not vacuous: the logger and the sink are both in the graph.
|
|
33
|
+
expect(files.some((f) => f.endsWith('requestLogger.ts'))).toBe(true);
|
|
34
|
+
expect(files.some((f) => f.endsWith('telemetrySink.ts'))).toBe(true);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('the walk goes red on the shape it exists for', () => {
|
|
38
|
+
const regex = new RegExp(RUNTIME_IMPORT.source, RUNTIME_IMPORT.flags);
|
|
39
|
+
const found = [..."import { Database } from 'bun:sqlite';\nimport type { X } from 'bun:sqlite';".matchAll(regex)].map((m) => m[1]);
|
|
40
|
+
expect(found).toEqual(['bun:sqlite']);
|
|
41
|
+
});
|
|
42
|
+
});
|