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.
- 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/package.json +1 -1
- package/src/nodeBuiltinsAreProbedByCalling.spec.ts +233 -0
- package/src/publicSurface.spec.ts +88 -0
- 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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Database } from 'bun:sqlite';
|
|
2
2
|
import { beforeEach, describe, expect, test } from 'bun:test';
|
|
3
3
|
import { createFakeD1Binding } from './fakeD1';
|
|
4
|
-
import { perInvocation } from './invocation';
|
|
4
|
+
import { createPendingWrites, perInvocation } from './invocation';
|
|
5
5
|
import { LIMITS } from './limits';
|
|
6
6
|
import { createLocalD1 } from './local';
|
|
7
7
|
import { createRemoteD1 } from './remote';
|
|
@@ -172,3 +172,67 @@ describe('the budget counts the REMOTE driver identically', () => {
|
|
|
172
172
|
expect(db.used).toBe(2);
|
|
173
173
|
});
|
|
174
174
|
});
|
|
175
|
+
|
|
176
|
+
describe('the pending writes of one invocation', () => {
|
|
177
|
+
test('settle() waits for every collected write before it resolves', async () => {
|
|
178
|
+
const landed: number[] = [];
|
|
179
|
+
const writes = createPendingWrites();
|
|
180
|
+
const slow = (n: number, ms: number) =>
|
|
181
|
+
new Promise<void>((resolve) =>
|
|
182
|
+
setTimeout(() => {
|
|
183
|
+
landed.push(n);
|
|
184
|
+
resolve();
|
|
185
|
+
}, ms),
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
writes.add(slow(1, 20));
|
|
189
|
+
writes.add(slow(2, 5));
|
|
190
|
+
// Nothing has landed yet: this is the state a response sent without settling escapes in.
|
|
191
|
+
expect(landed).toEqual([]);
|
|
192
|
+
|
|
193
|
+
await writes.settle();
|
|
194
|
+
expect(landed.sort()).toEqual([1, 2]);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test('🔴 a failed write REJECTS settle() — `all`, not `allSettled`', async () => {
|
|
198
|
+
// The failure path this primitive exists for. `allSettled` would swallow the rejection
|
|
199
|
+
// and the entry point would answer 200 to a write that never landed, which reads to the
|
|
200
|
+
// person as the sign-out having taken.
|
|
201
|
+
const writes = createPendingWrites();
|
|
202
|
+
writes.add(Promise.resolve('this one is fine'));
|
|
203
|
+
writes.add(Promise.reject(new Error('D1_ERROR: no such table: sessions')));
|
|
204
|
+
await expect(writes.settle()).rejects.toThrow('no such table: sessions');
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test('it collects the real statements a store queues, and they are durable after settle()', async () => {
|
|
208
|
+
// The whole shape in miniature: a synchronous caller queues D1 work it cannot await,
|
|
209
|
+
// and the row exists only because somebody settled.
|
|
210
|
+
const sqlite = seeded();
|
|
211
|
+
const db = perInvocation(createLocalD1(sqlite));
|
|
212
|
+
const writes = createPendingWrites();
|
|
213
|
+
|
|
214
|
+
const revoke = (id: number, name: string): void => {
|
|
215
|
+
writes.add(db.prepare('INSERT INTO people (id, name) VALUES (?, ?)').bind(id, name).run());
|
|
216
|
+
};
|
|
217
|
+
revoke(90, 'p90');
|
|
218
|
+
revoke(91, 'p91');
|
|
219
|
+
|
|
220
|
+
await writes.settle();
|
|
221
|
+
const rows = await db.prepare('SELECT name FROM people WHERE id >= 90 ORDER BY id').all<{ name: string }>();
|
|
222
|
+
expect(rows.results.map((r) => r.name)).toEqual(['p90', 'p91']);
|
|
223
|
+
// Two writes plus the read back — the collector adds no queries of its own.
|
|
224
|
+
expect(db.used).toBe(3);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test('an invocation that wrote nothing settles clean', async () => {
|
|
228
|
+
await expect(createPendingWrites().settle()).resolves.toBeUndefined();
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test('it satisfies the two-method collector a store is handed', async () => {
|
|
232
|
+
// `cursedauth/d1-stores` declares this shape under its own name and may not import it
|
|
233
|
+
// from here. Structural, in both directions — this is that assertion at compile time.
|
|
234
|
+
const collector: { add(promise: Promise<unknown>): void } = createPendingWrites();
|
|
235
|
+
collector.add(Promise.resolve());
|
|
236
|
+
expect(typeof collector.add).toBe('function');
|
|
237
|
+
});
|
|
238
|
+
});
|
|
@@ -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
|
|
|
44
52
|
import { LIMITS } from './limits';
|
|
@@ -205,3 +213,81 @@ export function perInvocation(db: D1LikeDatabase, opts: { max?: number } = {}):
|
|
|
205
213
|
},
|
|
206
214
|
};
|
|
207
215
|
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Where a durable write goes on its way out of one invocation.
|
|
219
|
+
*
|
|
220
|
+
* Deliberately two members, and the narrower `add`-only half is what a STORE takes. A store
|
|
221
|
+
* that also held `settle` could end the request's writes from inside the request, which is
|
|
222
|
+
* the entry point's job and nobody else's. `cursedauth/d1-stores` declares exactly this
|
|
223
|
+
* two-method shape under its own name (`WriteCollector`) because that package sits UNDER
|
|
224
|
+
* this one and may not import it — structural typing means an object made here satisfies it
|
|
225
|
+
* verbatim, with no dependency edge in either direction.
|
|
226
|
+
*/
|
|
227
|
+
export interface WriteCollector {
|
|
228
|
+
add(promise: Promise<unknown>): void;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** A write that must land before the response is returned. See {@link createPendingWrites}. */
|
|
232
|
+
export interface PendingWrites extends WriteCollector {
|
|
233
|
+
/** Await every collected write. Called BEFORE the response is returned, never after. */
|
|
234
|
+
settle(): Promise<void>;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Collect the durable writes of ONE invocation, to be awaited before the response leaves.
|
|
239
|
+
*
|
|
240
|
+
* ## Why this is the Worker-write primitive and not a convenience
|
|
241
|
+
*
|
|
242
|
+
* A ported app's synchronous stores cannot await — their signatures are somebody else's
|
|
243
|
+
* published contract — so the shape every port lands on is: one read fills a snapshot, every
|
|
244
|
+
* sync call answers from it, and every mutation both updates the snapshot and pushes its
|
|
245
|
+
* statement's promise in here. The entry point then does the awaiting:
|
|
246
|
+
*
|
|
247
|
+
* ```ts
|
|
248
|
+
* export default {
|
|
249
|
+
* async fetch(req: Request, env: Env) {
|
|
250
|
+
* const db = perInvocation(createRemoteD1(env.DB));
|
|
251
|
+
* const writes = createPendingWrites();
|
|
252
|
+
* const res = await app.fetch(req, { db, writes });
|
|
253
|
+
* await writes.settle(); // 🔴 BEFORE the response, never after
|
|
254
|
+
* return res;
|
|
255
|
+
* },
|
|
256
|
+
* };
|
|
257
|
+
* ```
|
|
258
|
+
*
|
|
259
|
+
* 🔴 **`settle()` before the response, never `ctx.waitUntil`.** A Worker isolate is per-colo
|
|
260
|
+
* and short-lived; a write acknowledged by a 200 and then dropped means "sign out" reported
|
|
261
|
+
* success and left the credential live — a failure invisible to any single-process test and
|
|
262
|
+
* indistinguishable, to the person, from the feature being broken.
|
|
263
|
+
*
|
|
264
|
+
* 🔴 **`Promise.all`, not `allSettled`.** A failed durable write must not be reported as a
|
|
265
|
+
* success. It surfaces as a 500 from the entry point, which is the honest answer to "did my
|
|
266
|
+
* sign-out take". `allSettled` here would convert every lost write into a silent 200 — the
|
|
267
|
+
* same outcome as not collecting them at all, reached by a road that looks careful.
|
|
268
|
+
*
|
|
269
|
+
* ## Why it lives here
|
|
270
|
+
*
|
|
271
|
+
* It was written three times before it was published once — `apps/patterns/worker/stores.ts`,
|
|
272
|
+
* `apps/collections/worker/stores.ts` and `libs/cursedauth/src/d1Stores.ts`, byte-identical
|
|
273
|
+
* but for a comment, with six more ports queued behind them. It is a D1-INVOCATION primitive
|
|
274
|
+
* rather than an auth one, so it belongs beside {@link perInvocation}: both are constructed
|
|
275
|
+
* per request, both describe what is true for the span of one, and neither knows anything
|
|
276
|
+
* about who is signed in.
|
|
277
|
+
*
|
|
278
|
+
* Ordering is NOT what this gives you. Everything added is already in flight, so two writes
|
|
279
|
+
* that must land in order go in one `batch()`, never in two `add`s — a DELETE that must
|
|
280
|
+
* precede its INSERT will otherwise race and trip a unique index.
|
|
281
|
+
*/
|
|
282
|
+
export function createPendingWrites(): PendingWrites {
|
|
283
|
+
const pending: Promise<unknown>[] = [];
|
|
284
|
+
return {
|
|
285
|
+
add: (promise) => void pending.push(promise),
|
|
286
|
+
async settle() {
|
|
287
|
+
// `all`, not `allSettled`: a failed durable write must not be reported as a
|
|
288
|
+
// success. It surfaces as a 500 from the entry point, which is the honest answer
|
|
289
|
+
// to "did my sign-out take".
|
|
290
|
+
await Promise.all(pending);
|
|
291
|
+
},
|
|
292
|
+
};
|
|
293
|
+
}
|