cursedbelt-server 4.17.0 → 4.18.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/server/d1/pullD1.js +10 -0
- 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 +11 -0
- package/src/server/telemetry.ts +27 -0
- package/src/server/telemetryIsWorkerSafe.spec.ts +42 -0
package/dist/server/d1/pullD1.js
CHANGED
|
@@ -139,6 +139,16 @@ export async function pullD1ToSqlite(options) {
|
|
|
139
139
|
// `exec`, not `run`: each is many statements and `run` would execute only the first,
|
|
140
140
|
// leaving a file with a schema and no rows that looks perfectly valid.
|
|
141
141
|
db.exec(options.schemaSql);
|
|
142
|
+
// 🔴 A table D1 holds that the schema does not declare is created from D1's OWN DDL, never
|
|
143
|
+
// dropped: measured on music's first post-cutover pull (2026-09-23), `art_lookups` — a
|
|
144
|
+
// cache its art job creates where it first needs it — made the whole pull refuse with
|
|
145
|
+
// `no such table`, because its rows were exported and its table was not in the schema.
|
|
146
|
+
const have = new Set(db.query("SELECT name FROM sqlite_master WHERE type = 'table'").all().map((r) => r.name));
|
|
147
|
+
for (const row of rows) {
|
|
148
|
+
if (!tables.includes(row.name) || have.has(row.name) || !row.sql)
|
|
149
|
+
continue;
|
|
150
|
+
db.exec(row.sql.replace(/^\s*CREATE\s+TABLE\s+(?!IF\s+NOT\s+EXISTS)/i, 'CREATE TABLE IF NOT EXISTS '));
|
|
151
|
+
}
|
|
142
152
|
db.exec(sql);
|
|
143
153
|
if (options.afterLoadSql)
|
|
144
154
|
db.exec(options.afterLoadSql);
|
|
@@ -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.0",
|
|
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",
|
|
@@ -144,6 +144,26 @@ describe('pulling D1 down — table by table, because a whole-database export re
|
|
|
144
144
|
expect(hits.n).toBe(1);
|
|
145
145
|
});
|
|
146
146
|
|
|
147
|
+
test('🔴 a table D1 holds that the schema does not declare is created from D1\'s own DDL', async () => {
|
|
148
|
+
const dir = scratch();
|
|
149
|
+
const master = [...MASTER, { name: 'art_lookups', sql: 'CREATE TABLE art_lookups (key TEXT PRIMARY KEY, art_key TEXT)' }];
|
|
150
|
+
const run: Wrangler = (args) => {
|
|
151
|
+
if (args[1] === 'execute') return { code: 0, stdout: JSON.stringify([{ results: master }]), stderr: '' };
|
|
152
|
+
const table = args[args.indexOf('--table') + 1] as string;
|
|
153
|
+
const out = args[args.indexOf('--output') + 1] as string;
|
|
154
|
+
writeFileSync(
|
|
155
|
+
out,
|
|
156
|
+
table === 'items' ? "INSERT INTO items (id) VALUES ('i1');" : table === 'art_lookups' ? "INSERT INTO art_lookups VALUES ('k', 'art/1');" : '',
|
|
157
|
+
);
|
|
158
|
+
return { code: 0, stdout: '', stderr: '' };
|
|
159
|
+
};
|
|
160
|
+
await pull(dir, run);
|
|
161
|
+
const db = new Database(join(dir, 'pulled.sqlite'), { readonly: true });
|
|
162
|
+
const row = db.query('select art_key from art_lookups').get() as { art_key: string } | null;
|
|
163
|
+
db.close();
|
|
164
|
+
expect(row?.art_key).toBe('art/1');
|
|
165
|
+
});
|
|
166
|
+
|
|
147
167
|
test('🔴 a database without the required table is refused as the wrong one', async () => {
|
|
148
168
|
const dir = scratch();
|
|
149
169
|
const { run } = fake(() => '');
|
package/src/server/d1/pullD1.ts
CHANGED
|
@@ -176,6 +176,17 @@ export async function pullD1ToSqlite(options: PullD1Options): Promise<void> {
|
|
|
176
176
|
// `exec`, not `run`: each is many statements and `run` would execute only the first,
|
|
177
177
|
// leaving a file with a schema and no rows that looks perfectly valid.
|
|
178
178
|
db.exec(options.schemaSql);
|
|
179
|
+
// 🔴 A table D1 holds that the schema does not declare is created from D1's OWN DDL, never
|
|
180
|
+
// dropped: measured on music's first post-cutover pull (2026-09-23), `art_lookups` — a
|
|
181
|
+
// cache its art job creates where it first needs it — made the whole pull refuse with
|
|
182
|
+
// `no such table`, because its rows were exported and its table was not in the schema.
|
|
183
|
+
const have = new Set(
|
|
184
|
+
(db.query("SELECT name FROM sqlite_master WHERE type = 'table'").all() as { name: string }[]).map((r) => r.name),
|
|
185
|
+
);
|
|
186
|
+
for (const row of rows) {
|
|
187
|
+
if (!tables.includes(row.name) || have.has(row.name) || !row.sql) continue;
|
|
188
|
+
db.exec(row.sql.replace(/^\s*CREATE\s+TABLE\s+(?!IF\s+NOT\s+EXISTS)/i, 'CREATE TABLE IF NOT EXISTS '));
|
|
189
|
+
}
|
|
179
190
|
db.exec(sql);
|
|
180
191
|
if (options.afterLoadSql) db.exec(options.afterLoadSql);
|
|
181
192
|
} finally {
|
|
@@ -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
|
+
});
|