laravel-workers 0.0.0-stage → 0.4.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.
Files changed (136) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +295 -2
  3. package/bin/laravel-workers.mjs +141 -0
  4. package/build.mjs +2599 -0
  5. package/compiler/bin/controller.mjs +92 -0
  6. package/compiler/bin/transpile.mjs +65 -0
  7. package/compiler/runtime/rt-from-context.mjs +2399 -0
  8. package/compiler/src/classes.mjs +1232 -0
  9. package/compiler/src/controller.mjs +1029 -0
  10. package/compiler/src/dist-swap.mjs +76 -0
  11. package/compiler/src/mail-reach.mjs +104 -0
  12. package/compiler/src/policy.mjs +345 -0
  13. package/compiler/src/transpiler.mjs +3094 -0
  14. package/compiler/src/view-name.mjs +21 -0
  15. package/kernel/auth.mjs +1502 -0
  16. package/kernel/cache.mjs +405 -0
  17. package/kernel/collections.mjs +1615 -0
  18. package/kernel/cookies.mjs +474 -0
  19. package/kernel/csrf.mjs +698 -0
  20. package/kernel/db.mjs +703 -0
  21. package/kernel/digest.mjs +360 -0
  22. package/kernel/encrypter.mjs +106 -0
  23. package/kernel/form-request.mjs +106 -0
  24. package/kernel/hash.mjs +113 -0
  25. package/kernel/html-entities.mjs +268 -0
  26. package/kernel/http.mjs +1803 -0
  27. package/kernel/image.mjs +341 -0
  28. package/kernel/mail.mjs +269 -0
  29. package/kernel/mailable.mjs +172 -0
  30. package/kernel/model.mjs +1226 -0
  31. package/kernel/multipart.mjs +161 -0
  32. package/kernel/paginator.mjs +336 -0
  33. package/kernel/php-json.mjs +220 -0
  34. package/kernel/php-serialize.mjs +245 -0
  35. package/kernel/queue.mjs +96 -0
  36. package/kernel/ratelimit.mjs +855 -0
  37. package/kernel/redis.mjs +264 -0
  38. package/kernel/resources.mjs +857 -0
  39. package/kernel/response.mjs +17 -0
  40. package/kernel/router.mjs +276 -0
  41. package/kernel/scheduler.mjs +236 -0
  42. package/kernel/service-adapters.mjs +49 -0
  43. package/kernel/session.mjs +934 -0
  44. package/kernel/storage.mjs +265 -0
  45. package/kernel/str.mjs +445 -0
  46. package/kernel/tests/auth.test.mjs +1074 -0
  47. package/kernel/tests/budget.test.mjs +527 -0
  48. package/kernel/tests/cache.test.mjs +290 -0
  49. package/kernel/tests/collections.test.mjs +534 -0
  50. package/kernel/tests/cookies.test.mjs +302 -0
  51. package/kernel/tests/csrf.test.mjs +597 -0
  52. package/kernel/tests/db.test.mjs +1069 -0
  53. package/kernel/tests/encrypter.test.mjs +90 -0
  54. package/kernel/tests/fixtures/laravel-1337-cookies.json +120 -0
  55. package/kernel/tests/hash.test.mjs +149 -0
  56. package/kernel/tests/http.test.mjs +1182 -0
  57. package/kernel/tests/image.test.mjs +235 -0
  58. package/kernel/tests/login-flow.test.mjs +509 -0
  59. package/kernel/tests/model.test.mjs +324 -0
  60. package/kernel/tests/oracle-app.mjs +32 -0
  61. package/kernel/tests/paginator.test.mjs +132 -0
  62. package/kernel/tests/php-json.test.mjs +263 -0
  63. package/kernel/tests/php-serialize.test.mjs +210 -0
  64. package/kernel/tests/ports.test.mjs +47 -0
  65. package/kernel/tests/ratelimit.test.mjs +975 -0
  66. package/kernel/tests/readiness.test.mjs +155 -0
  67. package/kernel/tests/redis.test.mjs +175 -0
  68. package/kernel/tests/registry-json.test.mjs +28 -0
  69. package/kernel/tests/resources.test.mjs +275 -0
  70. package/kernel/tests/router.test.mjs +281 -0
  71. package/kernel/tests/routes-json.test.mjs +32 -0
  72. package/kernel/tests/services.test.mjs +474 -0
  73. package/kernel/tests/session.test.mjs +709 -0
  74. package/kernel/tests/sqlite-integration.test.mjs +390 -0
  75. package/kernel/tests/storage.test.mjs +133 -0
  76. package/kernel/tests/str.test.mjs +83 -0
  77. package/kernel/tests/upload.test.mjs +193 -0
  78. package/kernel/tests/validation-messages.test.mjs +117 -0
  79. package/kernel/tests/validation.test.mjs +340 -0
  80. package/kernel/tests/validator-hooks.test.mjs +343 -0
  81. package/kernel/tests/validator.test.mjs +805 -0
  82. package/kernel/tests/vendor-lite.test.mjs +39 -0
  83. package/kernel/validation-messages.data.mjs +168 -0
  84. package/kernel/validation-messages.mjs +249 -0
  85. package/kernel/validation.mjs +123 -0
  86. package/kernel/validator.mjs +1538 -0
  87. package/package.json +68 -5
  88. package/shadow/run.mjs +784 -0
  89. package/shadow/scenarios/fresh-app.mjs +60 -0
  90. package/shadow/scenarios/lw-constructs-app.mjs +669 -0
  91. package/src/ports/dbcheck.mjs +22 -0
  92. package/src/worker.mjs +200 -0
  93. package/tools/_root.php +135 -0
  94. package/tools/bench.mjs +129 -0
  95. package/tools/class-dump.php +87 -0
  96. package/tools/compile-view.php +263 -0
  97. package/tools/config-dump.php +192 -0
  98. package/tools/constants-dump.php +46 -0
  99. package/tools/construct-audit.mjs +326 -0
  100. package/tools/constructs.json +1510 -0
  101. package/tools/docs-check.mjs +149 -0
  102. package/tools/event-dump.php +320 -0
  103. package/tools/formrequest-dump.php +229 -0
  104. package/tools/gate.mjs +36 -0
  105. package/tools/http-oracle.php +200 -0
  106. package/tools/init.mjs +73 -0
  107. package/tools/integrations.json +69 -0
  108. package/tools/mailable-views.php +116 -0
  109. package/tools/make-cookie-fixture.mjs +87 -0
  110. package/tools/make-html-entities.mjs +106 -0
  111. package/tools/model-dump.php +198 -0
  112. package/tools/php-auth-oracle.php +701 -0
  113. package/tools/php-cache-oracle.php +166 -0
  114. package/tools/php-crypto-oracle.php +113 -0
  115. package/tools/php-csrf-oracle.php +271 -0
  116. package/tools/php-http-pipeline-oracle.php +358 -0
  117. package/tools/php-json-oracle.php +102 -0
  118. package/tools/php-login-flow-oracle.php +342 -0
  119. package/tools/php-provision.mjs +172 -0
  120. package/tools/php-ratelimit-oracle.php +508 -0
  121. package/tools/php-runtime.mjs +342 -0
  122. package/tools/php-serialize-oracle.php +114 -0
  123. package/tools/php-session-oracle.php +266 -0
  124. package/tools/php-shadow-oracle.php +588 -0
  125. package/tools/policy-dump.php +240 -0
  126. package/tools/ports.mjs +51 -0
  127. package/tools/prerender.php +68 -0
  128. package/tools/readiness.mjs +255 -0
  129. package/tools/registry-json.mjs +22 -0
  130. package/tools/routes-dump.php +27 -0
  131. package/tools/routes-json.mjs +48 -0
  132. package/tools/schedule-dump.php +82 -0
  133. package/tools/schema-dump.php +158 -0
  134. package/tools/validation-messages.php +54 -0
  135. package/tools/vendor-lite.mjs +299 -0
  136. package/tools/verify-s0.php +209 -0
@@ -0,0 +1,527 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { performance } from 'node:perf_hooks';
4
+
5
+ function stats(samples) {
6
+ const s = [...samples].sort((a, b) => a - b);
7
+ const avg = s.reduce((a, b) => a + b, 0) / s.length;
8
+ const p95 = s[Math.floor(s.length * 0.95)];
9
+ return { avg, p95 };
10
+ }
11
+
12
+ test('budget: router/db/validator/scheduler/cache/storage within 5ms p95', async () => {
13
+ const N = 2000;
14
+
15
+ const { normalizeTable: norm, match: mfn } = await import('../router.mjs');
16
+ const rtTable = norm([{ uri: 'u/{id}', methods: ['GET'] }]);
17
+ {
18
+ const a = [];
19
+ for (let i = 0; i < N; i++) { const t0 = performance.now(); mfn(rtTable, { method: 'GET', path: '/u/' + (i % 50) }); a.push(performance.now() - t0); }
20
+ const { avg, p95 } = stats(a);
21
+ assert.ok(avg <= 2, `router avg ${avg}`);
22
+ assert.ok(p95 <= 5, `router p95 ${p95}`);
23
+ }
24
+
25
+ // The method-not-allowed path (Laravel's automatic OPTIONS response and the
26
+ // 405): the verbs the URI accepts (the route table scan) + the Allow header.
27
+ {
28
+ const { allowedMethods } = await import('../router.mjs');
29
+ const table2 = norm([
30
+ { uri: '/posts', methods: ['GET', 'HEAD'] },
31
+ { uri: '/posts', methods: ['POST'] },
32
+ { uri: '/posts/{post}', methods: ['GET', 'HEAD', 'PUT', 'DELETE'] },
33
+ ]);
34
+ const a = [];
35
+ for (let i = 0; i < N; i++) {
36
+ const t0 = performance.now();
37
+ allowedMethods(table2, { method: 'OPTIONS', path: '/posts' });
38
+ a.push(performance.now() - t0);
39
+ }
40
+ const { avg, p95 } = stats(a);
41
+ assert.ok(avg <= 2, `allowedMethods avg ${avg}`);
42
+ assert.ok(p95 <= 5, `allowedMethods p95 ${p95}`);
43
+ }
44
+
45
+ const { table } = await import('../db.mjs');
46
+ const fake = { prepare: () => ({ bind() { return this; }, async all() { return { results: [] }; } }) };
47
+ {
48
+ const a = [];
49
+ for (let i = 0; i < N; i++) { const t0 = performance.now(); table(fake, 'config').where('k', '=', 'x').toSql(); a.push(performance.now() - t0); }
50
+ const { avg, p95 } = stats(a);
51
+ assert.ok(avg <= 2, `db avg ${avg}`);
52
+ assert.ok(p95 <= 5, `db p95 ${p95}`);
53
+ }
54
+
55
+ const { Validator } = await import('../validator.mjs');
56
+ {
57
+ const a = [];
58
+ for (let i = 0; i < N; i++) { const t0 = performance.now(); const v = new Validator({ email: 'a@b.co' }, { email: 'required|email' }); await v.passes(); a.push(performance.now() - t0); }
59
+ const { avg, p95 } = stats(a);
60
+ assert.ok(avg <= 2, `validator avg ${avg}`);
61
+ assert.ok(p95 <= 5, `validator p95 ${p95}`);
62
+ }
63
+
64
+ // The model attribute read (with the not-compiled guard: a relation or an
65
+ // accessor is rejected instead of answering null).
66
+ {
67
+ const { Model } = await import('../model.mjs');
68
+ class M extends Model {}
69
+ M.notCompiled = ['comments', 'notifications'];
70
+ M.table = 'posts';
71
+ const m = new M({ id: 1, title: 'x', body: 'y' });
72
+ const a = [];
73
+ for (let i = 0; i < N; i++) {
74
+ const t0 = performance.now();
75
+ m.get('title');
76
+ m.get('body');
77
+ a.push(performance.now() - t0);
78
+ }
79
+ const { avg, p95 } = stats(a);
80
+ assert.ok(avg <= 2, `model get avg ${avg}`);
81
+ assert.ok(p95 <= 5, `model get p95 ${p95}`);
82
+ }
83
+
84
+ const { defineSchedule } = await import('../scheduler.mjs');
85
+ const s = defineSchedule(api => { api.call('a', '*/15 * * * *', async () => {}); });
86
+ const d = new Date('2026-10-18T09:15:00Z');
87
+ {
88
+ const a = [];
89
+ for (let i = 0; i < N; i++) { const t0 = performance.now(); s._match(d, '*/15 * * * *'); a.push(performance.now() - t0); }
90
+ const { avg, p95 } = stats(a);
91
+ assert.ok(avg <= 2, `scheduler avg ${avg}`);
92
+ assert.ok(p95 <= 5, `scheduler p95 ${p95}`);
93
+ }
94
+
95
+ const { createCache } = await import('../cache.mjs');
96
+ const c = createCache({});
97
+ await c.put('k', 1, 60000);
98
+ {
99
+ const a = [];
100
+ for (let i = 0; i < N; i++) { const t0 = performance.now(); await c.get('k'); a.push(performance.now() - t0); }
101
+ const { avg, p95 } = stats(a);
102
+ assert.ok(avg <= 2, `cache avg ${avg}`);
103
+ assert.ok(p95 <= 5, `cache p95 ${p95}`);
104
+ }
105
+
106
+ const { createStorage } = await import('../storage.mjs');
107
+ const st = createStorage({});
108
+ await st.put('k', 'v');
109
+ {
110
+ const a = [];
111
+ for (let i = 0; i < N; i++) { const t0 = performance.now(); await st.get('k'); a.push(performance.now() - t0); }
112
+ const { avg, p95 } = stats(a);
113
+ assert.ok(avg <= 2, `storage avg ${avg}`);
114
+ assert.ok(p95 <= 5, `storage p95 ${p95}`);
115
+ }
116
+
117
+ // The upload path: the multipart body is parsed and the file is written to
118
+ // the object store (the D1/R2 round-trip is measured in the shadow harness;
119
+ // here the store is the in-memory one).
120
+ {
121
+ const { buildRequest } = await import('../http.mjs');
122
+ const { createStorage } = await import('../storage.mjs');
123
+ const storage = createStorage({});
124
+ const boundary = '----lwBudgetBoundary';
125
+ const body =
126
+ `--${boundary}\r\n` +
127
+ 'Content-Disposition: form-data; name="title"\r\n\r\n' +
128
+ 'budget\r\n' +
129
+ `--${boundary}\r\n` +
130
+ 'Content-Disposition: form-data; name="avatar"; filename="a.txt"\r\n' +
131
+ 'Content-Type: text/plain\r\n\r\n' +
132
+ 'hello file\r\n' +
133
+ `--${boundary}--\r\n`;
134
+ const raw = {
135
+ method: 'POST',
136
+ url: 'http://localhost/upload',
137
+ headers: { 'content-type': `multipart/form-data; boundary=${boundary}` },
138
+ body: new TextEncoder().encode(body),
139
+ };
140
+ const a = [];
141
+ for (let i = 0; i < N; i++) {
142
+ const t0 = performance.now();
143
+ const req = buildRequest(raw, { storage, defaultDisk: 'local' });
144
+ const file = req.file('avatar');
145
+ file.getClientOriginalName();
146
+ file.getSize();
147
+ await file.storeAs('uploads', 'a.txt');
148
+ a.push(performance.now() - t0);
149
+ }
150
+ const { avg, p95 } = stats(a);
151
+ assert.ok(avg <= 2, `upload avg ${avg}`);
152
+ assert.ok(p95 <= 5, `upload p95 ${p95}`);
153
+ }
154
+
155
+ // The event dispatch (the compiled classes): the listeners are called in the
156
+ // registration order, like Laravel (EVERY listener runs; only an answer of
157
+ // false stops the chain).
158
+ {
159
+ const { buildRt } = await import('../../compiler/runtime/rt-from-context.mjs');
160
+ const rt = buildRt(
161
+ {},
162
+ {
163
+ http: {},
164
+ classes: {
165
+ 'App\\Events\\PostPublished': { ctor: (args) => ({ postId: args[0] }), methods: {} },
166
+ 'App\\Listeners\\MarkPostNotified': {
167
+ ctor: () => ({}),
168
+ methods: {
169
+ handle: async function () {
170
+ this.seen = 1;
171
+ return null;
172
+ },
173
+ },
174
+ },
175
+ },
176
+ eventListeners: { 'App\\Events\\PostPublished': ['App\\Listeners\\MarkPostNotified'] },
177
+ }
178
+ );
179
+ const a = [];
180
+ for (let i = 0; i < N; i++) {
181
+ const t0 = performance.now();
182
+ await rt.event.dispatch(rt.newClass('App\\Events\\PostPublished', [i]));
183
+ a.push(performance.now() - t0);
184
+ }
185
+ const { avg, p95 } = stats(a);
186
+ assert.ok(avg <= 2, `event dispatch avg ${avg}`);
187
+ assert.ok(p95 <= 5, `event dispatch p95 ${p95}`);
188
+ }
189
+
190
+ // The mail (a compiled mailable + the kernel mailer): the payload is built
191
+ // and the mail goes out - the new runtime feature needs its own budget row.
192
+ {
193
+ const { buildRt } = await import('../../compiler/runtime/rt-from-context.mjs');
194
+ const { createMailer } = await import('../mail.mjs');
195
+ const mailer = createMailer({ transport: 'log', from: 'hello@example.com' });
196
+ const rt = buildRt(
197
+ {},
198
+ {
199
+ http: { mail: mailer },
200
+ classes: {
201
+ 'App\\Mail\\OrderMail': {
202
+ base: 'mailable',
203
+ ctor: () => ({}),
204
+ deps: [],
205
+ methods: {
206
+ build: async function () {
207
+ this.subject('Order shipped');
208
+ this.view('mail.order');
209
+ this.with({ id: 7 });
210
+ return this;
211
+ },
212
+ },
213
+ },
214
+ },
215
+ views: {
216
+ 'mail.order': { context: {}, main: (r) => { r.echo('<p>Order ' + r.var('id') + '</p>'); } },
217
+ },
218
+ }
219
+ );
220
+ const a = [];
221
+ for (let i = 0; i < N; i++) {
222
+ const t0 = performance.now();
223
+ const mailable = rt.newClass('App\\Mail\\OrderMail', []);
224
+ await rt.mail().to('buyer@example.com').send(mailable);
225
+ a.push(performance.now() - t0);
226
+ }
227
+ const { avg, p95 } = stats(a);
228
+ assert.ok(avg <= 2, `mail avg ${avg}`);
229
+ assert.ok(p95 <= 5, `mail p95 ${p95}`);
230
+ }
231
+
232
+ // rt.cache() facade (controller-facing Cache::* surface over the D1 store).
233
+ const { buildRt } = await import('../../compiler/runtime/rt-from-context.mjs');
234
+ const fakeCache = { async get() { return 1; }, async put() {}, async forget() {} };
235
+ const rt = buildRt({}, { http: { cache: fakeCache } });
236
+ {
237
+ const a = [];
238
+ for (let i = 0; i < N; i++) { const t0 = performance.now(); rt.cache(); a.push(performance.now() - t0); }
239
+ const { avg, p95 } = stats(a);
240
+ assert.ok(avg <= 2, `rt.cache avg ${avg}`);
241
+ assert.ok(p95 <= 5, `rt.cache p95 ${p95}`);
242
+ }
243
+
244
+ // Eloquent `hashed` cast. The bcrypt work itself is deliberately measured at
245
+ // cost 4 here; the free-plan cost of the default cost 12 is recorded in
246
+ // COMPILED.md (measured ~679ms CPU on Workers, matching Laravel).
247
+ const { Model } = await import('../model.mjs');
248
+ const { makeSync } = await import('../hash.mjs');
249
+ class BudgetHashed extends Model {}
250
+ BudgetHashed.table = 'users';
251
+ BudgetHashed.casts = { password: 'hashed' };
252
+ BudgetHashed.hashRounds = 4;
253
+ const hashed = makeSync('secret', { rounds: 4 });
254
+ {
255
+ const m = new BudgetHashed();
256
+ const a = [];
257
+ for (let i = 0; i < N; i++) { const t0 = performance.now(); m.set('password', hashed); a.push(performance.now() - t0); }
258
+ const { avg, p95 } = stats(a);
259
+ // Already-hashed passthrough is regex-only (no bcrypt); must stay cheap.
260
+ assert.ok(avg <= 2, `hashed-cast passthrough avg ${avg}`);
261
+ assert.ok(p95 <= 5, `hashed-cast passthrough p95 ${p95}`);
262
+ }
263
+ {
264
+ const m = new BudgetHashed();
265
+ const a = [];
266
+ for (let i = 0; i < 20; i++) { const t0 = performance.now(); m.set('password', 'secret-' + i); a.push(performance.now() - t0); }
267
+ const { avg, p95 } = stats(a);
268
+ assert.ok(avg <= 25, `hashed-cast bcrypt cost4 avg ${avg}`);
269
+ assert.ok(p95 <= 40, `hashed-cast bcrypt cost4 p95 ${p95}`);
270
+ }
271
+
272
+ // The `Hash` facade surface (rt.hash) — the facade call itself is cheap; the
273
+ // bcrypt work it triggers is the hashed-cast row above.
274
+ {
275
+ const a = [];
276
+ for (let i = 0; i < N; i++) { const t0 = performance.now(); rt.hash.isHashed('x'); a.push(performance.now() - t0); }
277
+ const { avg, p95 } = stats(a);
278
+ assert.ok(avg <= 2, `rt.hash avg ${avg}`);
279
+ assert.ok(p95 <= 5, `rt.hash p95 ${p95}`);
280
+ }
281
+
282
+ // The model write path: rt.db() (the D1 binding) + rt.propSet() (the
283
+ // `$model->attr = $value` assignment) + Model#set() with the cast applied.
284
+ {
285
+ const fakeDb = { prepare: () => ({ bind() { return this; }, async run() { return { changes: 1, lastInsertRowid: 1 }; }, async all() { return { results: [] }; } }) };
286
+ const rt2 = buildRt({}, { db: fakeDb, hashRounds: 4 });
287
+ const a = [];
288
+ for (let i = 0; i < N; i++) {
289
+ const t0 = performance.now();
290
+ rt2.db();
291
+ rt2.propSet(new BudgetHashed(), 'title', 'x');
292
+ a.push(performance.now() - t0);
293
+ }
294
+ const { avg, p95 } = stats(a);
295
+ assert.ok(avg <= 2, `rt.db+rt.propSet avg ${avg}`);
296
+ assert.ok(p95 <= 5, `rt.db+rt.propSet p95 ${p95}`);
297
+ }
298
+
299
+ // The `auth.basic` middleware stage: Basic header parse + guard lookup.
300
+ {
301
+ const header = 'Basic ' + Buffer.from('a@b.co:secret').toString('base64');
302
+ const a = [];
303
+ for (let i = 0; i < N; i++) {
304
+ const t0 = performance.now();
305
+ const m = /^\s*Basic\s+(.+)$/i.exec(header);
306
+ const decoded = atob(m[1].trim());
307
+ decoded.indexOf(':');
308
+ a.push(performance.now() - t0);
309
+ }
310
+ const { avg, p95 } = stats(a);
311
+ assert.ok(avg <= 2, `auth.basic header parse avg ${avg}`);
312
+ assert.ok(p95 <= 5, `auth.basic header parse p95 ${p95}`);
313
+ }
314
+
315
+ // The service adapters (storage/mail/queue) — the *local* part of the call
316
+ // (the facade lookup + the payload shape). The network cost (R2 / Resend / CF
317
+ // Queues) is not measurable here; it is a round trip per call.
318
+ {
319
+ const { createStorage } = await import('../storage.mjs');
320
+ const { createMailer } = await import('../mail.mjs');
321
+ const { createQueue } = await import('../queue.mjs');
322
+ const storage = createStorage({});
323
+ const mailer = createMailer({ transport: 'log' });
324
+ const queue = createQueue({});
325
+ const a = [];
326
+ for (let i = 0; i < N; i++) {
327
+ const t0 = performance.now();
328
+ await storage.put('k', 'v');
329
+ await storage.exists('k');
330
+ await mailer.send({ to: 'a@b.co', subject: 's', text: 't' });
331
+ await queue.push('default', { i });
332
+ a.push(performance.now() - t0);
333
+ }
334
+ const { avg, p95 } = stats(a);
335
+ assert.ok(avg <= 2, `services (storage/mail/queue) avg ${avg}`);
336
+ assert.ok(p95 <= 5, `services (storage/mail/queue) p95 ${p95}`);
337
+ }
338
+
339
+ // The cron evaluation the edge does for the dumped schedule (the Cloudflare
340
+ // Cron Trigger path): matching a handful of expressions.
341
+ {
342
+ const { dueEvents, cronMatches } = await import('../scheduler.mjs');
343
+ const events = [
344
+ { name: 'a', expression: '*/5 * * * *' },
345
+ { name: 'b', expression: '0 9 * * *' },
346
+ { name: 'c', expression: '0 0 1 * *' },
347
+ ];
348
+ const now = new Date('2026-10-18T12:15:00Z');
349
+ const a = [];
350
+ for (let i = 0; i < N; i++) {
351
+ const t0 = performance.now();
352
+ cronMatches('*/5 * * * *', now);
353
+ dueEvents(events, now);
354
+ a.push(performance.now() - t0);
355
+ }
356
+ const { avg, p95 } = stats(a);
357
+ assert.ok(avg <= 2, `schedule cron evaluation avg ${avg}`);
358
+ assert.ok(p95 <= 5, `schedule cron evaluation p95 ${p95}`);
359
+ }
360
+
361
+ // The `new` expression (the built-in exceptions) + throw_if/throw_unless.
362
+ {
363
+ const a = [];
364
+ for (let i = 0; i < N; i++) {
365
+ const t0 = performance.now();
366
+ rt.newError('RuntimeException', 'budget row');
367
+ rt.throwIf(false, rt.newError('RuntimeException', 'never'));
368
+ a.push(performance.now() - t0);
369
+ }
370
+ const { avg, p95 } = stats(a);
371
+ assert.ok(avg <= 2, `new exception avg ${avg}`);
372
+ assert.ok(p95 <= 5, `new exception p95 ${p95}`);
373
+ }
374
+
375
+ // The helper verbs: rt.abortIf/rt.abortUnless/rt.today/rt.log (the request
376
+ // clock, the platform log) - the local cost only.
377
+ {
378
+ const a = [];
379
+ for (let i = 0; i < N; i++) {
380
+ const t0 = performance.now();
381
+ rt.abortIf(false, 404, 'never');
382
+ rt.abortUnless(true, 500, 'never');
383
+ rt.today().toDateString();
384
+ rt.log('info', 'budget row');
385
+ a.push(performance.now() - t0);
386
+ }
387
+ const { avg, p95 } = stats(a);
388
+ assert.ok(avg <= 2, `helper verbs avg ${avg}`);
389
+ assert.ok(p95 <= 5, `helper verbs p95 ${p95}`);
390
+ }
391
+
392
+ // The helper surface: rt.back/rt.dataGet/rt.tap/rt.report/rt.env.
393
+ {
394
+ const a = [];
395
+ for (let i = 0; i < N; i++) {
396
+ const t0 = performance.now();
397
+ rt.back('/fallback');
398
+ rt.dataGet({ a: { b: 1 } }, 'a.b');
399
+ rt.tap('v', () => {});
400
+ rt.report(new Error('budget row'));
401
+ rt.env('APP_ENV');
402
+ a.push(performance.now() - t0);
403
+ }
404
+ const { avg, p95 } = stats(a);
405
+ assert.ok(avg <= 2, `helper surface avg ${avg}`);
406
+ assert.ok(p95 <= 5, `helper surface p95 ${p95}`);
407
+ }
408
+
409
+ // The Gate check (rt.gate): the local cost of a policy decision (the
410
+ // registry lookup, the guest rule, the compiled policy call). The policy
411
+ // itself is compiled - there is no PHP at the edge.
412
+ {
413
+ const policies = {
414
+ 'App\\Models\\Post': {
415
+ policy: 'App\\Policies\\PostPolicy',
416
+ before: null,
417
+ methods: {
418
+ update: {
419
+ guest: false,
420
+ fn: async (rt, v_user, v_post) =>
421
+ rt.strictEq(rt.prop(v_user, 'id'), rt.prop(v_post, 'user_id'), false, false),
422
+ },
423
+ },
424
+ },
425
+ };
426
+ const rt3 = buildRt({}, { http: { auth: { user: async () => ({ id: 1 }) } }, policies });
427
+ const post = { user_id: 1 };
428
+ const a = [];
429
+ for (let i = 0; i < N; i++) {
430
+ const t0 = performance.now();
431
+ await rt3.gate('update', post, 'App\\Models\\Post', false);
432
+ a.push(performance.now() - t0);
433
+ }
434
+ const { avg, p95 } = stats(a);
435
+ assert.ok(avg <= 2, `rt.gate avg ${avg}`);
436
+ assert.ok(p95 <= 5, `rt.gate p95 ${p95}`);
437
+ }
438
+
439
+ // The paginator (the LengthAwarePaginator): the local cost of the pages and
440
+ // the URLs (the query string + the URL window). The D1 round trips (the
441
+ // count + the page) are not measurable here.
442
+ {
443
+ const { LengthAwarePaginator } = await import('../paginator.mjs');
444
+ const items = [{ id: 1 }, { id: 2 }];
445
+ const a = [];
446
+ for (let i = 0; i < N; i++) {
447
+ const t0 = performance.now();
448
+ const p = new LengthAwarePaginator(items, 60, 2, 10, { path: 'http://localhost/posts' });
449
+ p.total();
450
+ p.currentPage();
451
+ p.nextPageUrl();
452
+ p.elements();
453
+ a.push(performance.now() - t0);
454
+ }
455
+ const { avg, p95 } = stats(a);
456
+ assert.ok(avg <= 2, `paginator avg ${avg}`);
457
+ assert.ok(p95 <= 5, `paginator p95 ${p95}`);
458
+ }
459
+
460
+ // The translation helper (__()): the local cost of the lookup (the dumped
461
+ // strings) + the place-holder replacements.
462
+ {
463
+ const rt4 = buildRt(
464
+ {},
465
+ {
466
+ config: {
467
+ 'app.locale': 'en',
468
+ 'app.fallback_locale': 'en',
469
+ translations: { en: { pagination: { previous: '&laquo; Previous' } } },
470
+ },
471
+ }
472
+ );
473
+ const a = [];
474
+ for (let i = 0; i < N; i++) {
475
+ const t0 = performance.now();
476
+ rt4.__('pagination.previous');
477
+ rt4.__('Go to page :page', { page: i });
478
+ a.push(performance.now() - t0);
479
+ }
480
+ const { avg, p95 } = stats(a);
481
+ assert.ok(avg <= 2, `rt.__ avg ${avg}`);
482
+ assert.ok(p95 <= 5, `rt.__ p95 ${p95}`);
483
+ }
484
+
485
+ // The D1 blob store (the Storage facade on D1 - no R2, no card): the local
486
+ // part of one put + one get (the SQL text, the size check, the row shape).
487
+ // The D1 round trip itself is not measurable here; on the free plan a blob
488
+ // write counts as one D1 row write and a blob read as one row read
489
+ // (100k writes/day, 5M reads/day).
490
+ {
491
+ const rows = new Map();
492
+ const fakeD1 = {
493
+ prepare(sql) {
494
+ const stmt = {
495
+ params: [],
496
+ bind(...p) { this.params = p; return this; },
497
+ async first() {
498
+ if (/SELECT 1/.test(sql)) return { '1': 1 };
499
+ const row = rows.get(this.params[0]);
500
+ return row === undefined ? null : { body: row.body };
501
+ },
502
+ async run() {
503
+ if (/^INSERT OR REPLACE/.test(sql)) {
504
+ rows.set(this.params[0], { body: this.params[1] });
505
+ return { success: true, meta: { changes: 1 } };
506
+ }
507
+ return { success: true };
508
+ },
509
+ };
510
+ return stmt;
511
+ },
512
+ };
513
+ const { createStorage: createStore } = await import('../storage.mjs');
514
+ const blob = createStore({ d1: fakeD1 });
515
+ await blob.put('k', 'v');
516
+ const a = [];
517
+ for (let i = 0; i < N; i++) {
518
+ const t0 = performance.now();
519
+ await blob.put('k', 'v');
520
+ await blob.get('k');
521
+ a.push(performance.now() - t0);
522
+ }
523
+ const { avg, p95 } = stats(a);
524
+ assert.ok(avg <= 2, `storage d1 blob store avg ${avg}`);
525
+ assert.ok(p95 <= 5, `storage d1 blob store p95 ${p95}`);
526
+ }
527
+ });