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,2399 @@
1
+ // compiler/runtime/rt-from-context.mjs — Runtime helpers (context → rt)
2
+ import { validationExceptionWithMessages, sessionErrorBag, DEFAULT_DONT_FLASH } from '../../kernel/validation.mjs';
3
+ import { Validator } from '../../kernel/validator.mjs';
4
+ import * as hashMod from '../../kernel/hash.mjs';
5
+ import { str as strHelpers } from '../../kernel/str.mjs';
6
+ import { LengthAwarePaginator } from '../../kernel/paginator.mjs';
7
+ import { phpJsonEncode } from '../../kernel/php-json.mjs';
8
+ import { Mailable } from '../../kernel/mailable.mjs';
9
+ import { setModelRuntime, setModelTimezone, phpDateString } from '../../kernel/model.mjs';
10
+ import { FormRequest } from '../../kernel/form-request.mjs';
11
+ import { JsonResource, resourceResponse } from '../../kernel/resources.mjs';
12
+ import { Collection } from '../../kernel/collections.mjs';
13
+
14
+ // The Collection marker: the runtime's Collection class is created per rt (a
15
+ // view renders with its OWN rt - buildRt() runs again for it), so an
16
+ // `instanceof` check cannot see a Collection that came from another rt (the
17
+ // controller's). The marker travels with the instance.
18
+ // The registry symbol: the kernel's Collection (kernel/collections.mjs) marks
19
+ // its instances with the SAME symbol, so a collection built by the controller's
20
+ // rt is still recognised inside a view's own rt.
21
+ const IS_COLLECTION = Symbol.for('lw.collection');
22
+
23
+ // env() -> the config key: the build-time config dump is the padanan for env()
24
+ // at the edge (there is no .env there). Keep in sync with tools/config-dump.php.
25
+ const ENV_CONFIG_KEYS = {
26
+ APP_ENV: 'app.env',
27
+ APP_DEBUG: 'app.debug',
28
+ APP_KEY: 'app.key',
29
+ APP_URL: 'app.url',
30
+ };
31
+ export function buildRt(context, opts = {}) {
32
+ const strict = !!(opts && opts.strict);
33
+ const ctx = context || {};
34
+ const warnings = [];
35
+
36
+ const rt = { __warnings: warnings, __strict: strict, __output: '' };
37
+
38
+ // ---- helpers for legacy/enriched compat ----
39
+ const has = (k) => Object.prototype.hasOwnProperty.call(ctx, k) && ctx[k] != null;
40
+
41
+ // app
42
+ let appCtx = has('app') ? ctx.app : null;
43
+ if (!appCtx && has('app_version')) {
44
+ appCtx = { version: ctx.app_version, locale: 'en' };
45
+ warnings.push('app: derived from app_version (legacy shape)');
46
+ }
47
+ if (!appCtx) {
48
+ appCtx = { version: '', locale: 'en' };
49
+ warnings.push('app: missing, using empty fallback');
50
+ }
51
+
52
+ // files / vfs
53
+ let filesCtx = has('files') ? ctx.files : null;
54
+ if (!filesCtx && has('vfs')) {
55
+ filesCtx = ctx.vfs;
56
+ warnings.push('files: derived from vfs (legacy shape)');
57
+ }
58
+ if (!filesCtx) {
59
+ filesCtx = {};
60
+ warnings.push('files: missing, using empty fallback');
61
+ }
62
+
63
+ // vite
64
+ let viteCtx = has('vite') ? ctx.vite : null;
65
+ if (!viteCtx) {
66
+ viteCtx = { fonts: '', invoke: '' };
67
+ warnings.push('vite: missing, fonts/invoke default to empty');
68
+ }
69
+
70
+ // auth
71
+ let authCtx = has('auth') ? ctx.auth : null;
72
+ if (!authCtx) {
73
+ authCtx = { check: false };
74
+ warnings.push('auth: missing, check defaults to false');
75
+ }
76
+
77
+ // urls
78
+ let urlsCtx = has('urls') ? ctx.urls : null;
79
+ if (!urlsCtx) {
80
+ urlsCtx = {};
81
+ warnings.push('urls: missing, url()/route() will throw when called');
82
+ }
83
+
84
+ // route_names
85
+ let routeNames = has('route_names') ? ctx.route_names : null;
86
+ if (!routeNames && Array.isArray(ctx.routes)) {
87
+ routeNames = ctx.routes.map((r) => r && r.name).filter((n) => n != null);
88
+ warnings.push('route_names: derived from routes (legacy shape)');
89
+ }
90
+ if (!Array.isArray(routeNames)) {
91
+ routeNames = [];
92
+ warnings.push('route_names: missing, using empty');
93
+ }
94
+
95
+ // config
96
+ // A compiled controller calls buildRt(null, { ... http: ctx }) - the config
97
+ // lives on the HTTP context the kernel hands down (the flat dotted map).
98
+ let configCtx = has('config') ? ctx.config : null;
99
+ if (!configCtx && opts && opts.http && opts.http.config) configCtx = opts.http.config;
100
+ // The parent runtime hands its config down (a view renders with the app's
101
+ // config — the translations, the pagination view).
102
+ if (opts && opts.config) configCtx = { ...(configCtx || {}), ...opts.config };
103
+ if (!configCtx) configCtx = {};
104
+
105
+ // ---- phpNumStr (PHP-style float formatting) ----
106
+ const phpNumStr = (v) => {
107
+ if (Number.isInteger(v)) return String(v);
108
+ const s = v.toPrecision(14);
109
+ return s.includes('.') ? s.replace(/0+$/, '').replace(/\.$/, '') : s;
110
+ };
111
+
112
+ // ---- rt.echo (PHP echo semantics) ----
113
+ rt.echo = (v) => {
114
+ if (v instanceof Promise) {
115
+ throw new Error('unresolved promise rendered — await the query in a single chain (Post::where(...)->get())');
116
+ }
117
+ let s;
118
+ if (v === null || v === undefined) s = '';
119
+ else if (v === true) s = '1';
120
+ else if (v === false) s = '';
121
+ else if (typeof v === 'number') s = phpNumStr(v);
122
+ else s = String(v);
123
+ // A view that is capturing a section (@section) sends its output into the
124
+ // section, not into the page.
125
+ if (bladeEnv.capturing()) {
126
+ bladeEnv.capture(s);
127
+ return s;
128
+ }
129
+ rt.__output += s;
130
+ return s;
131
+ };
132
+
133
+ // ---- rt.e (htmlspecialchars) ----
134
+ rt.e = (v) => {
135
+ let s;
136
+ if (v === null || v === undefined) s = '';
137
+ else if (v === true) s = '1';
138
+ else if (v === false) s = '';
139
+ else if (typeof v === 'number') s = phpNumStr(v);
140
+ else if (Array.isArray(v)) s = 'Array';
141
+ else if (typeof v === 'object') s = 'Object';
142
+ else s = String(v);
143
+ return s
144
+ .replace(/&/g, '&')
145
+ .replace(/</g, '&lt;')
146
+ .replace(/>/g, '&gt;')
147
+ .replace(/"/g, '&quot;')
148
+ .replace(/'/g, '&#039;');
149
+ };
150
+
151
+ // ---- rt.str_replace ----
152
+ rt.str_replace = (a, b, s) => {
153
+ const needle = String(a);
154
+ const subject = String(s);
155
+ if (needle === '') return subject;
156
+ return subject.split(needle).join(String(b));
157
+ };
158
+
159
+ // ---- rt.add (PHP arithmetic +) ----
160
+ // PHP 8's arithmetic: a numeric string is a number; a string with a leading
161
+ // numeric part is that part (PHP raises a warning); a non-numeric string (or
162
+ // an array) in an arithmetic operation is a TypeError; a division by zero is
163
+ // a DivisionByZeroError; the modulo truncates its operands to int. The
164
+ // compiled side must not answer something else (a silent wrong answer).
165
+ const phpNum = (v) => {
166
+ if (v === null || v === undefined) return 0;
167
+ if (typeof v === 'boolean') return v ? 1 : 0;
168
+ if (typeof v === 'number') return v;
169
+ if (typeof v === 'string') {
170
+ if (isNumericStr(v)) return Number(v.trim());
171
+ const lead = /^\s*[+-]?\d+(\.\d*)?([eE][+-]?\d+)?/.exec(v);
172
+ return lead ? Number(lead[0].trim()) : NaN;
173
+ }
174
+ return NaN;
175
+ };
176
+ // A PHP array: a list or a plain object (the compiled side represents an
177
+ // array as an object literal; a model instance is not one).
178
+ const isPhpArray = (v) => {
179
+ if (Array.isArray(v)) return true;
180
+ if (v === null || typeof v !== 'object') return false;
181
+ const proto = Object.getPrototypeOf(v);
182
+ return proto === Object.prototype || proto === null;
183
+ };
184
+ const phpTypeName = (v) => {
185
+ if (v === null || v === undefined) return 'null';
186
+ if (typeof v === 'boolean') return 'bool';
187
+ if (typeof v === 'number') return Number.isInteger(v) ? 'int' : 'float';
188
+ if (typeof v === 'string') return 'string';
189
+ if (isPhpArray(v)) return 'array';
190
+ return 'object';
191
+ };
192
+ const arithOperands = (op, a, b) => {
193
+ const na = phpNum(a);
194
+ const nb = phpNum(b);
195
+ if (Number.isNaN(na) || Number.isNaN(nb)) {
196
+ throw new TypeError(
197
+ 'Unsupported operand types: ' + phpTypeName(a) + ' ' + op + ' ' + phpTypeName(b)
198
+ );
199
+ }
200
+ return [na, nb];
201
+ };
202
+ rt.add = (a, b) => {
203
+ // PHP 8: array + array is the union (the left keys win); every other
204
+ // combination follows the arithmetic rules. A PHP array is a plain object
205
+ // (the compiled side) or a list.
206
+ if (isPhpArray(a) && isPhpArray(b)) {
207
+ const out = {};
208
+ for (const k of Object.keys(a)) out[k] = a[k];
209
+ for (const k of Object.keys(b)) if (!Object.prototype.hasOwnProperty.call(out, k)) out[k] = b[k];
210
+ return out;
211
+ }
212
+ const p = arithOperands('+', a, b);
213
+ return p[0] + p[1];
214
+ };
215
+
216
+ // ---- rt.concat (PHP string cast + concat) ----
217
+ const phpToStr = (v) => {
218
+ if (v === null || v === undefined) return '';
219
+ if (v === true) return '1';
220
+ if (v === false) return '';
221
+ if (typeof v === 'number') return phpNumStr(v);
222
+ if (Array.isArray(v)) return 'Array';
223
+ if (typeof v === 'object') return 'Object';
224
+ return String(v);
225
+ };
226
+ rt.concat = (a, b) => phpToStr(a) + phpToStr(b);
227
+
228
+ // ---- rt.config ----
229
+ rt.config = (k, d) => {
230
+ if (Object.prototype.hasOwnProperty.call(configCtx, k)) return configCtx[k];
231
+ if (arguments.length >= 2) return d;
232
+ throw new Error('config: missing key ' + JSON.stringify(k) + ' with no default');
233
+ };
234
+
235
+ // ---- rt.app ----
236
+ rt.app = (name) => {
237
+ if (name === undefined) {
238
+ return {
239
+ version: () => appCtx.version,
240
+ getLocale: () => appCtx.locale,
241
+ };
242
+ }
243
+ const key = String(name);
244
+ if (key.indexOf('Illuminate\\Foundation\\Vite') !== -1 || key.indexOf('Illuminate/Foundation/Vite') !== -1) {
245
+ const invoke = viteCtx.invoke;
246
+ const fonts = viteCtx.fonts;
247
+ const shim = (entries) => invoke;
248
+ shim.fonts = () => fonts;
249
+ return shim;
250
+ }
251
+ throw new Error('app: unsupported service ' + JSON.stringify(name));
252
+ };
253
+
254
+ // ---- rt.public_path ----
255
+ rt.public_path = (p) => p;
256
+
257
+ // ---- rt.file_exists ----
258
+ rt.file_exists = (p) => {
259
+ const key = String(p);
260
+ if (Object.prototype.hasOwnProperty.call(filesCtx, key)) return !!filesCtx[key];
261
+ throw new Error('file_exists: unknown path ' + JSON.stringify(key));
262
+ };
263
+
264
+ // ---- rt.Route ----
265
+ rt.Route = { has: (n) => routeNames.indexOf(n) !== -1 };
266
+
267
+ // ---- rt.session / rt.request / rt.auth (compiled HTTP context) ----
268
+ // `opts.http` is the request context handed down by the HTTP kernel
269
+ // (`{ request, session, auth, user, route, routeParams }`). When it is
270
+ // present these helpers read the live session/guard; when it is absent
271
+ // (static snapshot or build-time evaluation) they fall back to the captured
272
+ // context and the session/request helpers fail loud rather than invent state.
273
+ const liveSession = (opts && opts.http && opts.http.session) ? opts.http.session : null;
274
+ const liveRequest = (opts && opts.http && opts.http.request) ? opts.http.request : null;
275
+ // The D1 binding, captured WITHOUT throwing: `rt.db()` raises when it is
276
+ // missing, and the FormRequest lifecycle must not raise for a request whose
277
+ // rules never touch the database. `unique`/`exists` check it themselves
278
+ // (kernel/validator.mjs).
279
+ const liveDb = (opts && opts.db) || null;
280
+ const liveAuth = (opts && opts.http && opts.http.auth) ? opts.http.auth : null;
281
+ const liveCache = (opts && opts.http && opts.http.cache) ? opts.http.cache : null;
282
+ // The request clock (integer seconds) — the model timestamps follow it.
283
+ const requestClock =
284
+ opts && opts.http && typeof opts.http.clock === 'function' ? opts.http.clock : null;
285
+ // The service adapters the worker hands down (storage/mail/queue).
286
+ const liveStorage = (opts && opts.http && opts.http.storage) ? opts.http.storage : null;
287
+ // The log sink the platform hands down (the worker: the Workers console).
288
+ const liveLog =
289
+ (opts && opts.http && typeof opts.http.log === 'function') ? opts.http.log : null;
290
+ const liveMail = (opts && opts.http && opts.http.mail) ? opts.http.mail : null;
291
+ const liveQueue = (opts && opts.http && opts.http.queue) ? opts.http.queue : null;
292
+
293
+ // ---- the service adapters: the framework services a class can inject ----
294
+ // The edge hands the live service over (the worker: the platform log, the
295
+ // mailer, the queue). The shape is Laravel's, so the compiled class calls
296
+ // it the way the app does.
297
+ const logAdapter = () => {
298
+ if (!liveLog) return null;
299
+ const write = (level) => (message, context) => liveLog(level, message, context);
300
+ return {
301
+ log: (level, message, context) => liveLog(level, message, context),
302
+ debug: write('debug'),
303
+ info: write('info'),
304
+ notice: write('notice'),
305
+ warning: write('warning'),
306
+ warn: write('warning'),
307
+ error: write('error'),
308
+ critical: write('critical'),
309
+ alert: write('alert'),
310
+ emergency: write('emergency'),
311
+ };
312
+ };
313
+ // The queue: Laravel's Queue facade. The edge has no consumer for the
314
+ // binding (the worker exports no queue handler), and the binding carries a
315
+ // JSON body, not a PHP job — so a queued job would never run. It fails loud
316
+ // instead of vanishing.
317
+ const queueAdapter = () => {
318
+ if (!liveQueue) return null;
319
+ const fail = (method) => {
320
+ throw new Error(
321
+ 'Queue::' + method + '(): the job would never run at the edge — the worker has no queue handler, so ' +
322
+ 'nothing consumes the queue binding, and the binding carries a JSON body, not a PHP job. The paths that ' +
323
+ 'work: a Cloudflare Queues binding with a consumer you write (a limited version is on the Workers free plan; the paid plan charges per operation), or a D1 table the cron trigger reads.'
324
+ );
325
+ };
326
+ return {
327
+ push: () => fail('push'),
328
+ pushOn: () => fail('pushOn'),
329
+ later: () => fail('later'),
330
+ bulk: () => fail('bulk'),
331
+ };
332
+ };
333
+ // The service an injected dependency asks for.
334
+ const liveAdapter = (kind) => {
335
+ if (kind === 'mail') return liveMail || null;
336
+ if (kind === 'log') return logAdapter();
337
+ if (kind === 'queue') return queueAdapter();
338
+ return null;
339
+ };
340
+
341
+ const noHttp = (fn) => {
342
+ throw new Error(fn + '(): no HTTP context (static snapshot / build-time evaluation)');
343
+ };
344
+ rt.request = () => (liveRequest || noHttp('request'));
345
+ rt.session = () => (liveSession || noHttp('session'));
346
+ // The app's validation messages (the app's own lang file wins over the
347
+ // framework's copy, exactly like Laravel's translator - the same helper the
348
+ // request's validate() uses, kernel/http.mjs:validationMessagesOf).
349
+ const defaultValidationMessages = () => {
350
+ const locale = String(configCtx['app.locale'] || 'en');
351
+ const base = translations.en && translations.en.validation;
352
+ const local = translations[locale] && translations[locale].validation;
353
+ if (!base && !local) return null;
354
+ return Object.assign({}, base || {}, local || {});
355
+ };
356
+ // The FormRequest lifecycle (Illuminate/Validation/ValidatesWhenResolvedTrait
357
+ // ::validateResolved + Illuminate/Foundation/Http/FormRequest
358
+ // ::getValidatorInstance). The order is Laravel's:
359
+ // 1. prepareForValidation() - the input it prepares is validated
360
+ // 2. authorize() - false raises the 403
361
+ // (failedAuthorization -> UnauthorizedException)
362
+ // 3. the validator over validationData() with the app's rules
363
+ // 4. withValidator($validator) - the app registers on the validator
364
+ // 5. after() - the callback it RETURNS is registered
365
+ // 6. the rules run; a failure raises the 422 (failedValidation)
366
+ // 7. passedValidation() - the work after the rules passed
367
+ //
368
+ // The hooks are the COMPILED class's own methods (the class compiler emits
369
+ // them), so the lifecycle calls them on the instance. The validator is the
370
+ // kernel's (the edge has one factory).
371
+ rt.formRequest = async (fqcn, opts = {}) => {
372
+ const hooks = Array.isArray(opts.hooks) ? opts.hooks : [];
373
+ const instance = rt.newClass(fqcn, []);
374
+ if (hooks.includes('prepareForValidation')) {
375
+ await instance.prepareForValidation(rt);
376
+ }
377
+ if (hooks.includes('authorize')) {
378
+ const allowed = await instance.authorize(rt);
379
+ if (!allowed) {
380
+ // Illuminate\Validation\ValidatesWhenResolvedTrait::failedAuthorization
381
+ // throws the UnauthorizedException, and Laravel's Handler turns it into
382
+ // the 403 (AccessDeniedHttpException - "This action is unauthorized.").
383
+ // The error carries the status, so the compiled handler renders it
384
+ // (an expectsJson request gets Laravel's pretty JSON body).
385
+ throw rt.statusErr(403, 'This action is unauthorized.');
386
+ }
387
+ }
388
+ // The data the rules see: the app's validationData() hook, or the whole
389
+ // request (Laravel's default - FormRequest.php:197).
390
+ const data = hooks.includes('validationData')
391
+ ? await instance.validationData(rt)
392
+ : instance.all();
393
+ // The connection is OPTIONAL: only `unique`/`exists` read it, and the
394
+ // validator raises its own precise error for them when it is missing
395
+ // (kernel/validator.mjs - "validation rule unique needs opts.db"). Calling
396
+ // rt.db() here threw for EVERY FormRequest, so a route whose rules are all
397
+ // `required`/`string`/`max` answered 500 on a worker without a D1 binding
398
+ // where Laravel answers 200.
399
+ const validator = new Validator(data, opts.rules || {}, opts.messages || {}, {
400
+ db: liveDb,
401
+ attributes: opts.attributes || {},
402
+ defaultMessages: defaultValidationMessages(),
403
+ });
404
+ if (hooks.includes('withValidator')) {
405
+ await instance.withValidator(rt, validator);
406
+ }
407
+ if (hooks.includes('after')) {
408
+ // Laravel registers the hook's RETURN VALUE on the validator
409
+ // (FormRequest.php:114): `$validator->after($this->after(...))`, and it
410
+ // calls the hook through the container with `['validator' => $validator]`
411
+ // (FormRequest.php:112-115) - so `after(Validator $v)` receives it.
412
+ // Without the argument the compiled hook's `$v` was undefined and the
413
+ // request died with a TypeError where Laravel answers 422.
414
+ const callback = await instance.after(rt, validator);
415
+ if (callback) validator.after(callback);
416
+ }
417
+ instance.setValidator(validator);
418
+ // The rules run (and the after callbacks - the validator runs them at the
419
+ // end of the pass, Validator.php:509-511).
420
+ const ok = await validator.passes();
421
+ if (!ok) {
422
+ instance.failedValidation(validator);
423
+ }
424
+ if (hooks.includes('passedValidation')) {
425
+ await instance.passedValidation(rt);
426
+ }
427
+ return validator.validated();
428
+ };
429
+ // Illuminate\Support\Str - the string helpers (PHP-exact, kernel/str.mjs).
430
+ // (`rt.str` is the PHP (string) cast, so the facade goes to rt.Str.)
431
+ rt.Str = strHelpers;
432
+ // back(): the session's url.previous, else the Referer, else the fallback.
433
+ rt.back = (fallback = '/') => {
434
+ const previous =
435
+ liveSession && typeof liveSession.previousUrl === 'function' ? liveSession.previousUrl() : null;
436
+ const referer =
437
+ liveRequest && typeof liveRequest.header === 'function' ? liveRequest.header('referer') : null;
438
+ return rt.redirect(previous || referer || fallback);
439
+ };
440
+ // data_get(): the dot/array access (the `*` wildcard is not ported).
441
+ rt.dataGet = (target, key, def = null) => {
442
+ if (key === null || key === undefined) return target;
443
+ const parts = String(key).split('.');
444
+ let cur = target;
445
+ for (const part of parts) {
446
+ if (cur === null || cur === undefined) return def;
447
+ if (Array.isArray(cur)) {
448
+ const idx = Number(part);
449
+ if (!Number.isInteger(idx)) return def;
450
+ cur = cur[idx];
451
+ continue;
452
+ }
453
+ if (typeof cur !== 'object') return def;
454
+ if (!Object.prototype.hasOwnProperty.call(cur, part)) return def;
455
+ cur = cur[part];
456
+ }
457
+ return cur === undefined ? def : cur;
458
+ };
459
+ // tap(): the value through a closure (Laravel returns the value).
460
+ rt.tap = (value, callback) => {
461
+ if (typeof callback === 'function') callback(value);
462
+ return value;
463
+ };
464
+ // report(): the platform log (it does not answer the exception).
465
+ rt.report = (error) => {
466
+ const message = error && error.message ? String(error.message) : String(error);
467
+ if (liveLog) liveLog('error', message);
468
+ return null;
469
+ };
470
+ // env(): the build-time config dump is the padanan (there is no .env at the edge).
471
+ rt.env = (key, def = null) => {
472
+ const cfgKey = ENV_CONFIG_KEYS[String(key)];
473
+ if (!cfgKey) {
474
+ throw new Error(
475
+ 'env(' + key + ') has no compiled counterpart - use config() (the build-time config dump is the padanan)'
476
+ );
477
+ }
478
+ const value = rt.config(cfgKey);
479
+ return value === undefined || value === null ? def : value;
480
+ };
481
+ // ---- rt.__ : the translation helper (Illuminate\Translation) ----
482
+ // The strings are dumped at build time (the app's lang/ files + the
483
+ // framework's fallback). Laravel returns the key itself when a string is
484
+ // missing.
485
+ const translations = configCtx.translations && typeof configCtx.translations === 'object'
486
+ ? configCtx.translations
487
+ : {};
488
+ const translateLine = (key, locale) => {
489
+ const locales = [locale];
490
+ const fallback = configCtx['app.fallback_locale'];
491
+ if (fallback && locales.indexOf(fallback) === -1) locales.push(fallback);
492
+ for (const loc of locales) {
493
+ const groups = translations[loc];
494
+ if (!groups) continue;
495
+ const dot = String(key).indexOf('.');
496
+ const group = dot === -1 ? '*' : String(key).slice(0, dot);
497
+ const item = dot === -1 ? String(key) : String(key).slice(dot + 1);
498
+ const groupStrings = group === '*' ? groups : groups[group];
499
+ if (!groupStrings) continue;
500
+ const line = rt.dataGet(groupStrings, item, null);
501
+ if (line !== null && line !== undefined) return String(line);
502
+ }
503
+ return null;
504
+ };
505
+ // Illuminate\Translation\Translator::makeReplacements(): `:key`, `:KEY`,
506
+ // `:Key` (the longest keys first).
507
+ const makeReplacements = (line, replace) => {
508
+ if (!replace || typeof replace !== 'object') return line;
509
+ const keys = Object.keys(replace).sort((a, b) => b.length - a.length);
510
+ let out = line;
511
+ for (const key of keys) {
512
+ const value = replace[key] === null || replace[key] === undefined ? '' : String(replace[key]);
513
+ const upper = key.toUpperCase();
514
+ const ucfirst = upper.charAt(0) + key.slice(1);
515
+ out = out
516
+ .split(':' + key).join(value)
517
+ .split(':' + upper).join(value.toUpperCase())
518
+ .split(':' + ucfirst).join(value.charAt(0).toUpperCase() + value.slice(1));
519
+ }
520
+ return out;
521
+ };
522
+ rt.__ = (key, replace) => {
523
+ const locale = configCtx['app.locale'] || 'en';
524
+ const line = translateLine(String(key), locale);
525
+ // Laravel applies the replacements to the key too, when the line is
526
+ // missing (`makeReplacements($line ?: $key, $replace)`).
527
+ return makeReplacements(line === null ? String(key) : line, replace);
528
+ };
529
+ rt.cache = () => {
530
+ if (liveCache) return liveCache;
531
+ if (opts && opts.http) throw new Error('cache(): no cache store configured (cache.driver !== "database" or no D1 binding)');
532
+ return noHttp('cache');
533
+ };
534
+ // Storage:: — the object store (the padanan for the app's local disk): R2
535
+ // when the app binds it, else the D1 blob store. Without either this fails
536
+ // loud: a silent in-memory store would lose the writes at the end of the
537
+ // isolate.
538
+ rt.storage = () => {
539
+ if (liveStorage) return liveStorage;
540
+ if (opts && opts.http) throw new Error('storage(): no object store — bind R2 (env.R2) or D1 (env.DB); Storage:: needs object storage at the edge');
541
+ return noHttp('storage');
542
+ };
543
+ // `new SomeException('msg')` - the PHP built-in exceptions. The compiled
544
+ // side throws an error with the same class name and message; the handler
545
+ // answers the same status (500) as Laravel.
546
+ rt.newError = (className, message, code) => {
547
+ const e = new Error(message === undefined || message === null ? '' : String(message));
548
+ e.name = String(className);
549
+ e.phpClass = String(className);
550
+ if (code !== undefined && code !== null) e.code = Number(code);
551
+ return e;
552
+ };
553
+ // throw_if()/throw_unless(): they throw the given error when the condition
554
+ // says so, and answer null otherwise (like Laravel).
555
+ rt.throwIf = (condition, error) => {
556
+ if (condition) throw error;
557
+ return null;
558
+ };
559
+ rt.throwUnless = (condition, error) => {
560
+ if (!condition) throw error;
561
+ return null;
562
+ };
563
+ // Log:: — the padanan is the platform log (the Workers console); the sink
564
+ // comes from the HTTP context.
565
+ rt.log = (level, message, context) => {
566
+ if (liveLog) return liveLog(level, message, context);
567
+ return null;
568
+ };
569
+ // Mail:: — the mailer (log/noop/resend).
570
+ rt.mail = () => {
571
+ if (liveMail) return liveMail;
572
+ if (opts && opts.http) throw new Error('mail(): no mailer configured');
573
+ return noHttp('mail');
574
+ };
575
+ // Queue:: — the queue facade (it fails loud: nothing consumes the binding).
576
+ rt.queue = () => {
577
+ const adapter = queueAdapter();
578
+ if (adapter) return adapter;
579
+ if (opts && opts.http) throw new Error('queue(): no queue binding configured');
580
+ return noHttp('queue');
581
+ };
582
+ // Laravel's old($key, $default) -> session()->getOldInput($key, $default).
583
+ rt.old = (key, def = null) => (liveSession ? liveSession.getOldInput(key, def) : noHttp('old'));
584
+ rt.csrf_token = () => rt.session().token();
585
+ // Illuminate\Foundation\helpers.php csrf_field(): no escaping (the token is
586
+ // Str::random(40), alphanumeric) — kept byte-exact.
587
+ rt.csrf_field = () =>
588
+ '<input type="hidden" name="_token" value="' + String(rt.session().token()) + '" autocomplete="off">';
589
+
590
+ // Illuminate\Validation\ValidationException::withMessages([...]) — the compiled
591
+ // kernel catches the same class instance (kernel/validation.mjs) and renders the
592
+ // 302-back / 422 response, so `throw` works exactly like the PHP throw.
593
+ rt.validationExceptionWithMessages = (messages, o) => validationExceptionWithMessages(messages, o);
594
+
595
+ rt.auth = (name) => {
596
+ if (liveAuth) {
597
+ return {
598
+ check: (n) => liveAuth.check(n ?? name),
599
+ guest: (n) => liveAuth.guest(n ?? name),
600
+ user: (n) => liveAuth.user(n ?? name),
601
+ id: (n) => liveAuth.id(n ?? name),
602
+ guard: (n) => liveAuth.guard(n ?? name),
603
+ attempt: (c, r, n) => liveAuth.guard(n ?? name).attempt(c, r),
604
+ once: (c, n) => liveAuth.guard(n ?? name).once(c),
605
+ login: (u, r, n) => liveAuth.guard(n ?? name).login(u, r),
606
+ logout: (n) => liveAuth.guard(n ?? name).logout(),
607
+ };
608
+ }
609
+ const unsupported = (m) => () => {
610
+ throw new Error(
611
+ 'auth()->' + m + '() needs the request context (compiled controller) — not available during static/build-time evaluation'
612
+ );
613
+ };
614
+ return {
615
+ check: () => !!authCtx.check,
616
+ guest: () => !authCtx.check,
617
+ user: unsupported('user'),
618
+ id: unsupported('id'),
619
+ attempt: unsupported('attempt'),
620
+ once: unsupported('once'),
621
+ login: unsupported('login'),
622
+ logout: unsupported('logout'),
623
+ guard: () => ({ check: () => !!authCtx.check, user: unsupported('guard()->user') }),
624
+ };
625
+ };
626
+
627
+ // ---- rt.newClass / rt.event: the compiled classes (the event listeners) ----
628
+ // The classes are compiled at build time (dist/src/classes.generated.mjs):
629
+ // the constructor (the promoted properties) and the public methods. Laravel
630
+ // dispatches an event synchronously, so the edge calls the listeners in the
631
+ // registration order. Laravel's Dispatcher::dispatch(): every listener runs, only
632
+ // an answer of false stops the chain, and the dispatch answers the array of the
633
+ // responses.
634
+ const classesMap = (opts && opts.classes && typeof opts.classes === 'object') ? opts.classes : {};
635
+ const eventListenersMap =
636
+ (opts && opts.eventListeners && typeof opts.eventListeners === 'object') ? opts.eventListeners : {};
637
+ // The dependencies of a constructor: the framework service the edge adapts
638
+ // (the mailer, the logger, the queue) or the app service the edge builds
639
+ // (the compiled class).
640
+ // The classes whose dependencies are being resolved right now: a service A
641
+ // that needs B that needs A would recurse forever (a stack overflow with no
642
+ // cause), so the cycle is named (Laravel: 'Circular dependency detected').
643
+ const resolving = new Set();
644
+ const resolveDeps = (deps, fqcn) => {
645
+ const out = {};
646
+ for (const d of deps || []) {
647
+ if (d.kind === 'adapter') {
648
+ const live = liveAdapter(d.adapter);
649
+ if (!live) {
650
+ throw new Error(
651
+ 'newClass: the service ' + d.type + ' is not available at the edge — the request context has no ' +
652
+ d.adapter + ' (the worker hands it down)'
653
+ );
654
+ }
655
+ out[d.param] = live;
656
+ continue;
657
+ }
658
+ out[d.param] = rt.newClass(d.type, []);
659
+ }
660
+ return out;
661
+ };
662
+ rt.newClass = (fqcn, args) => {
663
+ const entry = classesMap[String(fqcn)];
664
+ if (!entry) {
665
+ throw new Error('newClass: the class ' + fqcn + ' is not compiled — the build did not dump it');
666
+ }
667
+ if (resolving.has(String(fqcn))) {
668
+ throw new Error(
669
+ 'newClass: circular dependency detected [' + [...resolving, String(fqcn)].join(' -> ') + ']'
670
+ );
671
+ }
672
+ // The dependencies of the constructor: the framework service the edge
673
+ // adapts (the mailer, the logger, the queue) or the app service the edge
674
+ // builds (the compiled class).
675
+ resolving.add(String(fqcn));
676
+ let deps;
677
+ try {
678
+ deps = resolveDeps(entry.deps || [], fqcn);
679
+ } finally {
680
+ resolving.delete(String(fqcn));
681
+ }
682
+ // A mailable extends Laravel's Mailable: the compiled class gets the
683
+ // kernel's base (the fluent surface + the payload).
684
+ const built = entry.ctor(Array.isArray(args) ? args : [], deps);
685
+ const obj =
686
+ entry.base === 'mailable'
687
+ ? Object.assign(new Mailable(), built)
688
+ : entry.base === 'formRequest'
689
+ ? Object.assign(new FormRequest(rt.request()), built)
690
+ : entry.base === 'resource'
691
+ ? // `Illuminate\Http\Resources\Json\JsonResource::__construct($resource)`
692
+ // is INHERITED by the app's resource (a resource declares only
693
+ // toArray()), so the wrapped value is the first constructor
694
+ // argument - the compiled class has no constructor of its own.
695
+ Object.assign(new JsonResource(Array.isArray(args) ? args[0] : undefined), built)
696
+ : built;
697
+ // The runtime: a mailable renders its view through it (`__lwMail`).
698
+ Object.defineProperty(obj, '__rt', { value: rt, enumerable: false, configurable: true });
699
+ // The class of the instance (the dispatch needs it) — not enumerable, so a
700
+ // JSON answer does not show it.
701
+ Object.defineProperty(obj, '__lwClass', { value: String(fqcn), enumerable: false });
702
+ for (const [name, fn] of Object.entries(entry.methods || {})) {
703
+ // A RESOURCE's methods are called by the base (JsonResource::resolve ->
704
+ // toArray($request)), so the runtime binds `rt` itself: the compiled
705
+ // signature is `(rt, ...args)` and the caller only supplies the args.
706
+ // A resource's `with($request)` is a METHOD in Laravel while the base
707
+ // carries the `with` ARRAY (the extra top-level keys); JS cannot keep the
708
+ // two apart under one name, so the compiled method lands on `withData` -
709
+ // the name the payload already reads (`resource.withData()`).
710
+ const key = entry.base === 'resource' && name === 'with' ? 'withData' : name;
711
+ obj[key] = entry.base === 'resource' ? (...a) => fn.call(obj, rt, ...a) : fn.bind(obj);
712
+ }
713
+ return obj;
714
+ };
715
+ rt.event = {
716
+ dispatch: async (event) => {
717
+ const fqcn = event && event.__lwClass;
718
+ if (!fqcn) {
719
+ throw new Error('event.dispatch: the event is not an instance of a compiled event class');
720
+ }
721
+ const listeners = eventListenersMap[fqcn] || [];
722
+ // Laravel's Dispatcher::dispatch(): every listener runs, a listener that answers
723
+ // false stops the chain (and its answer is discarded), and the return value is
724
+ // the ARRAY of the responses (not the first answer). Only dispatchUntil() (the
725
+ // halt flag) stops at the first non-null answer.
726
+ const responses = [];
727
+ for (const listenerFqcn of listeners) {
728
+ const entry = classesMap[listenerFqcn];
729
+ if (!entry) {
730
+ throw new Error('event.dispatch: the listener ' + listenerFqcn + ' is not compiled');
731
+ }
732
+ const handle = entry.methods && entry.methods.handle;
733
+ if (!handle) {
734
+ throw new Error('event.dispatch: the listener ' + listenerFqcn + ' has no handle() method');
735
+ }
736
+ const listener = rt.newClass(listenerFqcn, []);
737
+ const answer = await handle.call(listener, rt, event);
738
+ if (answer === false) break;
739
+ responses.push(answer === undefined ? null : answer);
740
+ }
741
+ return responses;
742
+ },
743
+ };
744
+
745
+ // ---- rt.gate: the compiled policy check (Illuminate\Auth\Access\Gate) ----
746
+ // The policies are compiled at build time (dist/src/policies.generated.mjs):
747
+ // the abilities, the guest rules and the policy `before()` hook. The guest
748
+ // rules are Laravel's own: a guest the policy does not allow is denied — the
749
+ // method is not called at all (Gate::canBeCalledWithUser).
750
+ const policiesMap = (opts && opts.policies && typeof opts.policies === 'object') ? opts.policies : {};
751
+ rt.gate = async (ability, subject, modelFqcn, deny) => {
752
+ const key = String(modelFqcn);
753
+ const entry = policiesMap[key];
754
+ if (!entry) {
755
+ throw new Error('gate: no compiled policy for ' + key + ' — the build did not dump it');
756
+ }
757
+ const user = (await rt.auth().user()) ?? null;
758
+ const m = (entry.methods || {})[String(ability)];
759
+ let result;
760
+ if (!m) {
761
+ // Laravel skips the whole policy callback when the ability method is not
762
+ // callable — the check is denied and the policy's before() does not run.
763
+ result = false;
764
+ } else {
765
+ if (entry.before && (user !== null || entry.before.guest)) {
766
+ // Laravel hands before() the raw arguments: the ability and the
767
+ // subject (a class-level check passes the class name).
768
+ const beforeArgs =
769
+ subject === null || subject === undefined ? [ability, key] : [ability, subject];
770
+ const r = await entry.before.fn(rt, user, ...beforeArgs);
771
+ if (r !== null && r !== undefined) result = r;
772
+ }
773
+ if (result === undefined) {
774
+ if (user === null && !m.guest) {
775
+ result = false; // a guest the policy does not allow: denied
776
+ } else {
777
+ result = await m.fn(rt, user, ...(subject === null || subject === undefined ? [] : [subject]));
778
+ }
779
+ }
780
+ }
781
+ const allowed = rt.truthy(result);
782
+ return deny ? !allowed : allowed;
783
+ };
784
+
785
+ // ---- the queue: `Job::dispatch()` writes a D1 row ----
786
+ //
787
+ // Laravel's `database` queue driver writes a row into `jobs` and a worker
788
+ // consumes it later; `sync` runs the job inline. The edge has no long-lived
789
+ // worker, so the row goes into `lw_jobs` and the CRON TRIGGER drains it
790
+ // (`scheduled()` in src/worker.mjs) - the job runs AFTER the response, which
791
+ // is the `database` behaviour. The build refuses the push when the worker has
792
+ // no cron trigger (the job would never run).
793
+ const QUEUE_TABLE = 'lw_jobs';
794
+ const queueReady = new WeakMap();
795
+ async function ensureQueueTable(db) {
796
+ let ready = queueReady.get(db);
797
+ if (!ready) {
798
+ ready = (async () => {
799
+ try {
800
+ await db.prepare('SELECT 1 FROM "' + QUEUE_TABLE + '" LIMIT 1').first();
801
+ } catch {
802
+ await db
803
+ .prepare(
804
+ 'CREATE TABLE IF NOT EXISTS "' + QUEUE_TABLE + '" (' +
805
+ '"id" INTEGER PRIMARY KEY AUTOINCREMENT, "job" TEXT NOT NULL, ' +
806
+ '"payload" TEXT NOT NULL, "attempts" INTEGER NOT NULL DEFAULT 0, ' +
807
+ '"available_at" INTEGER NOT NULL, "created_at" TEXT NOT NULL)'
808
+ )
809
+ .run();
810
+ }
811
+ })();
812
+ queueReady.set(db, ready);
813
+ }
814
+ return ready;
815
+ }
816
+ rt.queuePush = async (fqcn, args) => {
817
+ const db = opts && opts.db ? opts.db : null;
818
+ if (!db) throw new Error('Job::dispatch(): no database binding (env.DB) — the queue row has nowhere to go');
819
+ await ensureQueueTable(db);
820
+ const now = clockDate();
821
+ const res = await db
822
+ .prepare(
823
+ 'INSERT INTO "' + QUEUE_TABLE + '" ("job", "payload", "attempts", "available_at", "created_at") ' +
824
+ 'VALUES (?, ?, 0, ?, ?)'
825
+ )
826
+ .bind(String(fqcn), JSON.stringify(args || []), Math.floor(now.getTime() / 1000), phpDateString(now))
827
+ .run();
828
+ // Laravel's `dispatch()` answers the pending dispatch (an object), not a
829
+ // boolean; the compiled call sites only ever chain `->onQueue()` etc.,
830
+ // which the build refuses, so the id is the honest answer.
831
+ return Number((res && res.meta && res.meta.last_row_id) || 0);
832
+ };
833
+ // The consumer: run up to `limit` due rows through the compiled job classes.
834
+ // `handlers` maps a job FQCN to `{ ctor, handle }` (the compiled class).
835
+ rt.queueDrain = async (db, handlers, limit = 10) => {
836
+ if (!db || !handlers) return { ran: 0, failed: 0 };
837
+ await ensureQueueTable(db);
838
+ const nowS = Math.floor(clockDate().getTime() / 1000);
839
+ const rows = await db
840
+ .prepare('SELECT "id", "job", "payload", "attempts" FROM "' + QUEUE_TABLE + '" WHERE "available_at" <= ? ORDER BY "id" LIMIT ?')
841
+ .bind(nowS, Number(limit) || 10)
842
+ .all();
843
+ let ran = 0;
844
+ let failed = 0;
845
+ for (const row of (rows && rows.results) || []) {
846
+ const entry = handlers[String(row.job)];
847
+ if (!entry) {
848
+ // An unknown job cannot run: drop it (a row that never runs would block
849
+ // the queue forever) and count it as failed.
850
+ await db.prepare('DELETE FROM "' + QUEUE_TABLE + '" WHERE "id" = ?').bind(row.id).run();
851
+ failed++;
852
+ continue;
853
+ }
854
+ try {
855
+ const args = JSON.parse(String(row.payload || '[]'));
856
+ const instance = await entry.build(rt, args);
857
+ await instance.handle();
858
+ await db.prepare('DELETE FROM "' + QUEUE_TABLE + '" WHERE "id" = ?').bind(row.id).run();
859
+ ran++;
860
+ } catch (e) {
861
+ failed++;
862
+ const attempts = Number(row.attempts || 0) + 1;
863
+ if (attempts >= 3) {
864
+ await db.prepare('DELETE FROM "' + QUEUE_TABLE + '" WHERE "id" = ?').bind(row.id).run();
865
+ } else {
866
+ // The retry backoff is Laravel's default (0): the row is due again
867
+ // immediately, with the attempt counted.
868
+ await db
869
+ .prepare('UPDATE "' + QUEUE_TABLE + '" SET "attempts" = ?, "available_at" = ? WHERE "id" = ?')
870
+ .bind(attempts, nowS, row.id)
871
+ .run();
872
+ }
873
+ }
874
+ }
875
+ return { ran, failed };
876
+ };
877
+
878
+ // ---- rt.gateGlobal: a Gate::define('x', 'Class@method') ability ----
879
+ // The dump records the definition (the class + method + the guest rule), and
880
+ // the build compiled the class; the kernel's Gate runs it in Laravel's order
881
+ // (before -> the ability -> after, Gate.php:428-453). A CLOSURE never
882
+ // reaches here: the build refuses it.
883
+ const gateDefinitions = (opts && opts.gateDefinitions && typeof opts.gateDefinitions === 'object')
884
+ ? opts.gateDefinitions
885
+ : {};
886
+ rt.gateGlobal = async (ability, subject) => {
887
+ const def = gateDefinitions[String(ability)];
888
+ if (!def) {
889
+ throw new Error('gate: no definition for the ability ' + String(ability) + ' — the build did not dump it');
890
+ }
891
+ const user = (await rt.auth().user()) ?? null;
892
+ // canBeCalledWithUser(): a guest reaches the callback only when its first
893
+ // parameter allows it.
894
+ if (user === null && def.guestAllowed === false) return false;
895
+ const [cls, method] = String(def.callback).split('@');
896
+ // The compiled class registry (`opts.classes`): the same map `newClass`
897
+ // reads. The instance is built through it (the ctor's deps included).
898
+ if (!classesMap[String(cls)]) {
899
+ throw new Error('gate: the class ' + cls + ' is not compiled — the build did not include it');
900
+ }
901
+ const instance = await rt.newClass(cls, []);
902
+ const fn = instance[method || '__invoke'];
903
+ if (typeof fn !== 'function') {
904
+ throw new Error('gate: ' + cls + ' has no method ' + (method || '__invoke') + '()');
905
+ }
906
+ // The compiled methods take `rt` as their FIRST argument (the class
907
+ // compiler emits `async function (rt, $user, ...)`), so the gate hands it
908
+ // over before the user - otherwise `rt` is the user and every `rt.*` call
909
+ // inside the ability throws.
910
+ const result = await fn.call(
911
+ instance,
912
+ rt,
913
+ user,
914
+ ...(subject === null || subject === undefined ? [] : [subject])
915
+ );
916
+ return rt.truthy(result);
917
+ };
918
+
919
+ // ---- rt.url / rt.route ----
920
+ const runtime = !!(opts && opts.runtime);
921
+ const origin = (opts && typeof opts.origin === 'string' && opts.origin) ? String(opts.origin).replace(/\/+$/, '') : '';
922
+ const routeMap = (opts && opts.routeMap && typeof opts.routeMap === 'object') ? opts.routeMap : null;
923
+ const modelRouteKey = (v) => {
924
+ if (v == null) return '';
925
+ if (typeof v === 'object') {
926
+ if (typeof v.get === 'function' && v.attributes) return v.get('id');
927
+ if (v.id !== undefined) return v.id;
928
+ }
929
+ return v;
930
+ };
931
+ // Runtime route() generation: name → uri pattern (built at build time). Only used
932
+ // when args are passed or when the captured build-time value does not apply.
933
+ const genUrl = (name, args) => {
934
+ if (!routeMap || !Object.prototype.hasOwnProperty.call(routeMap, name)) return null;
935
+ const uri = String(routeMap[name]);
936
+ let i = 0;
937
+ const path = uri.split('/').map((seg) => {
938
+ const m = /^\{([A-Za-z_][A-Za-z0-9_]*)\??\}$/.exec(seg);
939
+ if (!m) return seg;
940
+ const raw = modelRouteKey(args[i++]);
941
+ return encodeURIComponent(raw == null ? '' : String(raw));
942
+ }).join('/');
943
+ return origin + '/' + path;
944
+ };
945
+ const lookupUrl = (key) => {
946
+ if (!Object.prototype.hasOwnProperty.call(urlsCtx, key)) {
947
+ throw new Error('url/route: uncaptured ' + JSON.stringify(key));
948
+ }
949
+ const v = urlsCtx[key];
950
+ if (v && typeof v === 'object' && v.error) throw new Error('url/route: ' + v.error);
951
+ return v;
952
+ };
953
+ rt.url = (p) => {
954
+ const s = String(p);
955
+ if (runtime && s.startsWith('/')) return origin + s;
956
+ return lookupUrl(s);
957
+ };
958
+ rt.route = (n, ...rest) => {
959
+ const key = String(n);
960
+ if (rest.length > 0) {
961
+ const g = genUrl(key, rest);
962
+ if (g != null) return g;
963
+ throw new Error('route: uncaptured extra args');
964
+ }
965
+ if (runtime) {
966
+ const g = genUrl(key, []);
967
+ if (g != null) return g;
968
+ }
969
+ return lookupUrl(key);
970
+ };
971
+
972
+ // ---- builtins (flat on rt) ----
973
+ rt.isset = (...args) => {
974
+ // isset(var) — for our purposes, check args defined & not null
975
+ if (args.length === 0) return false;
976
+ for (const a of args) if (a === undefined || a === null) return false;
977
+ return true;
978
+ };
979
+ rt.empty = (v) => {
980
+ if (v === null || v === undefined) return true;
981
+ if (typeof v === 'boolean') return v === false;
982
+ if (typeof v === 'number') return v === 0;
983
+ if (typeof v === 'string') return v === '' || v === '0';
984
+ if (Array.isArray(v)) return v.length === 0;
985
+ return false;
986
+ };
987
+ rt.unset = () => {}; // no-op in emitted code (variables are lets; M1 has none)
988
+ rt.count = (v) => (Array.isArray(v) ? v.length : (v && typeof v === 'object' ? Object.keys(v).length : 0));
989
+ rt.in_array = (needle, haystack, strict) => {
990
+ // A PHP array is a list OR an assoc array (a plain object on the compiled
991
+ // side) - in_array walks the VALUES of both.
992
+ const arr = Array.isArray(haystack)
993
+ ? haystack
994
+ : haystack !== null && typeof haystack === 'object'
995
+ ? Object.values(haystack)
996
+ : [];
997
+ if (strict) return arr.some((x) => x === needle && typeof x === typeof needle);
998
+ return arr.some((x) => rt.looseEq(x, needle));
999
+ };
1000
+ const isObj = (v) => v !== null && typeof v === 'object';
1001
+ const arrValues = (v) => (Array.isArray(v) ? v.slice() : (isObj(v) ? rt.phpKeys(v).map((k) => v[k]) : []));
1002
+ rt.implode = (glue, arr) => arrValues(arr).map((x) => (x == null ? '' : String(x))).join(glue == null ? '' : String(glue));
1003
+ rt.explode = (delim, str, limit) => String(str).split(String(delim));
1004
+ rt.array_key_exists = (k, arr) => (isObj(arr) ? Object.prototype.hasOwnProperty.call(arr, k) : false);
1005
+ // PHP preserves an array's insertion order; a JS object does not (an
1006
+ // integer-like key enumerates first). A literal that mixes both kinds is
1007
+ // built by `rt.assoc()`, which records the order in a non-enumerable marker
1008
+ // - every key iterator below reads it first.
1009
+ rt.phpKeys = (v) =>
1010
+ v && typeof v === 'object' && Array.isArray(v.__lwOrder) ? v.__lwOrder : Object.keys(v || {});
1011
+ rt.assoc = (pairs) => {
1012
+ const out = {};
1013
+ const order = [];
1014
+ for (const pair of pairs) {
1015
+ const key = String(pair[0]);
1016
+ if (!Object.prototype.hasOwnProperty.call(out, key)) order.push(key);
1017
+ out[key] = pair[1];
1018
+ }
1019
+ Object.defineProperty(out, '__lwOrder', { value: order, enumerable: false });
1020
+ return out;
1021
+ };
1022
+ rt.array_keys = (arr) => (isObj(arr) ? rt.phpKeys(arr).map((k) => (/^\d+$/.test(k) ? Number(k) : k)) : []);
1023
+ rt.array_values = (arr) => arrValues(arr);
1024
+ rt.array_merge = (...arrs) => {
1025
+ if (arrs.every((a) => Array.isArray(a))) {
1026
+ const out = [];
1027
+ for (const a of arrs) for (const v of a) out.push(v);
1028
+ return out;
1029
+ }
1030
+ const out = {};
1031
+ for (const a of arrs) if (isObj(a)) for (const k of Object.keys(a)) out[k] = a[k];
1032
+ return out;
1033
+ };
1034
+ // `$arr[] = $x` — the PHP append (the next free integer key). It used to
1035
+ // compile to `$arr[null] = $x` — a *silent* wrong answer.
1036
+ rt.arrayPush = (arr, value) => {
1037
+ if (!Array.isArray(arr)) {
1038
+ throw new Error(
1039
+ 'arrayPush: the target is not a list (PHP $a[] = … on a string-keyed array)'
1040
+ );
1041
+ }
1042
+ arr.push(value);
1043
+ return arr;
1044
+ };
1045
+ rt.sprintf = (fmt, ...args) => {
1046
+ let i = 0;
1047
+ // collapse %% -> % first (PHP: literal percent)
1048
+ const withPlaceholders = String(fmt).replace(/%%/g, '\u0000');
1049
+ const out = withPlaceholders.replace(/%[sdu]/g, (m) => {
1050
+ const a = args[i++];
1051
+ if (m === '%d' || m === '%u') return String(parseInt(a, 10));
1052
+ return String(a == null ? '' : a);
1053
+ });
1054
+ return out.replace(/\u0000/g, '%');
1055
+ };
1056
+ rt.strtolower = (s) => String(s).toLowerCase();
1057
+ rt.strtoupper = (s) => String(s).toUpperCase();
1058
+ rt.trim = (s) => String(s).trim();
1059
+
1060
+ // ---- byte-oriented string helpers (PHP length semantics) ----
1061
+ const utf8 = new TextEncoder();
1062
+ const utf8d = new TextDecoder();
1063
+ const byteLen = (s) => utf8.encode(String(s)).length;
1064
+ rt.strlen = (s) => (s == null ? 0 : byteLen(s));
1065
+ rt.substr = (s, start, len) => {
1066
+ const bytes = utf8.encode(s == null ? '' : String(s));
1067
+ const n = bytes.length;
1068
+ let st = Number(start) | 0;
1069
+ if (st < 0) st = Math.max(n + st, 0);
1070
+ if (st > n) st = n;
1071
+ let end = n;
1072
+ if (len !== undefined && len !== null) {
1073
+ const l = Number(len) | 0;
1074
+ end = l < 0 ? n + l : st + l;
1075
+ }
1076
+ if (end < st) end = st;
1077
+ if (end > n) end = n;
1078
+ return utf8d.decode(bytes.slice(st, end));
1079
+ };
1080
+ rt.str_repeat = (s, n) => String(s == null ? '' : s).repeat(Math.max(0, Number(n) | 0));
1081
+ rt.str_contains = (h, n, ci) => {
1082
+ const hay = String(h == null ? '' : h);
1083
+ const needle = String(n);
1084
+ if (needle === '') return true;
1085
+ if (ci) return hay.toLowerCase().includes(needle.toLowerCase());
1086
+ return hay.includes(needle);
1087
+ };
1088
+ rt.str_starts_with = (h, n, ci) => {
1089
+ const hay = String(h == null ? '' : h);
1090
+ const needle = String(n);
1091
+ if (needle === '') return true;
1092
+ if (ci) return hay.toLowerCase().startsWith(needle.toLowerCase());
1093
+ return hay.startsWith(needle);
1094
+ };
1095
+ rt.str_ends_with = (h, n, ci) => {
1096
+ const hay = String(h == null ? '' : h);
1097
+ const needle = String(n);
1098
+ if (needle === '') return true;
1099
+ if (ci) return hay.toLowerCase().endsWith(needle.toLowerCase());
1100
+ return hay.endsWith(needle);
1101
+ };
1102
+ rt.str_is = (pattern, value) => {
1103
+ const p = String(pattern == null ? '' : pattern);
1104
+ const v = String(value == null ? '' : value);
1105
+ // Laravel Str::is: * wildcard, case-sensitive, full-string match
1106
+ // Escape regex specials except *, then * -> .*
1107
+ const escaped = p.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*');
1108
+ return new RegExp('^' + escaped + '$').test(v);
1109
+ };
1110
+ rt.ucfirst = (s) => {
1111
+ const x = String(s == null ? '' : s);
1112
+ return x === '' ? '' : x[0].toUpperCase() + x.slice(1);
1113
+ };
1114
+ rt.lcfirst = (s) => {
1115
+ const x = String(s == null ? '' : s);
1116
+ return x === '' ? '' : x[0].toLowerCase() + x.slice(1);
1117
+ };
1118
+ rt.str_pad = (s, len, pad, type) => {
1119
+ const x = String(s == null ? '' : s);
1120
+ const target = Math.max(0, Number(len) | 0);
1121
+ const cur = byteLen(x);
1122
+ if (cur >= target) return x;
1123
+ let p = pad === undefined || pad === null || pad === '' ? ' ' : String(pad);
1124
+ const need = target - cur;
1125
+ const fill = (count) => p.repeat(Math.ceil(count / p.length)).slice(0, count);
1126
+ const t = type === undefined ? 1 : Number(type);
1127
+ if (t === 0) return fill(need) + x; // STR_PAD_LEFT
1128
+ if (t === 2) {
1129
+ const left = Math.floor(need / 2);
1130
+ return fill(left) + x + fill(need - left); // STR_PAD_BOTH
1131
+ }
1132
+ return x + fill(need); // STR_PAD_RIGHT (default)
1133
+ };
1134
+
1135
+ // ---- PHP truthiness / logic ----
1136
+ // The elvis operator (`$a ?: $b`): the test is evaluated once, and the
1137
+ // else branch runs only when the test is falsy (PHP's semantics).
1138
+ rt.elvis = (test, other) => {
1139
+ const v = test();
1140
+ return rt.truthy(v) ? v : other();
1141
+ };
1142
+ // PHP 8's increment/decrement: a number grows, a numeric string becomes a
1143
+ // number ("19"++ => 20, "19"-- => 18), an alnum string bumps its last byte
1144
+ // ("a" -> "b", "z" -> "aa") - and a decrement on a non-numeric string has
1145
+ // NO effect (PHP 8 deprecated it).
1146
+ rt.inc = (v) => {
1147
+ if (typeof v === 'number') return v + 1;
1148
+ if (typeof v === 'boolean') return v ? 2 : 1;
1149
+ if (v === null || v === undefined) return 1;
1150
+ if (typeof v === 'string') {
1151
+ if (isNumericStr(v)) return Number(v.trim()) + 1;
1152
+ const chars = [...v];
1153
+ const last = chars[chars.length - 1];
1154
+ const isDigit = last >= '0' && last <= '9';
1155
+ const isLower = last >= 'a' && last <= 'z';
1156
+ const isUpper = last >= 'A' && last <= 'Z';
1157
+ if (!isDigit && !isLower && !isUpper) return v;
1158
+ if (isDigit) {
1159
+ if (last === '9') {
1160
+ let carry = true;
1161
+ const out = [...v];
1162
+ for (let i = out.length - 1; i >= 0 && carry; i--) {
1163
+ const c = out[i];
1164
+ if (c >= '0' && c <= '8') { out[i] = String(Number(c) + 1); carry = false; }
1165
+ else if (c === '9') { out[i] = '0'; }
1166
+ else { return v; }
1167
+ }
1168
+ return (carry ? '1' : '') + out.join('');
1169
+ }
1170
+ chars[chars.length - 1] = String(Number(last) + 1);
1171
+ return chars.join('');
1172
+ }
1173
+ if (isLower) {
1174
+ if (last === 'z') {
1175
+ let i = chars.length - 1;
1176
+ while (i >= 0 && chars[i] === 'z') { chars[i] = 'a'; i--; }
1177
+ if (i < 0) return 'a'.repeat(chars.length + 1);
1178
+ chars[i] = String.fromCharCode(chars[i].charCodeAt(0) + 1);
1179
+ return chars.join('');
1180
+ }
1181
+ chars[chars.length - 1] = String.fromCharCode(last.charCodeAt(0) + 1);
1182
+ return chars.join('');
1183
+ }
1184
+ if (last === 'Z') {
1185
+ let i = chars.length - 1;
1186
+ while (i >= 0 && chars[i] === 'Z') { chars[i] = 'A'; i--; }
1187
+ if (i < 0) return 'A'.repeat(chars.length + 1);
1188
+ chars[i] = String.fromCharCode(chars[i].charCodeAt(0) + 1);
1189
+ return chars.join('');
1190
+ }
1191
+ chars[chars.length - 1] = String.fromCharCode(last.charCodeAt(0) + 1);
1192
+ return chars.join('');
1193
+ }
1194
+ return v;
1195
+ };
1196
+ rt.dec = (v) => {
1197
+ if (typeof v === 'number') return v - 1;
1198
+ if (typeof v === 'boolean') return v ? 0 : -1;
1199
+ if (v === null || v === undefined) return -1;
1200
+ if (typeof v === 'string') {
1201
+ // A numeric string becomes a number ("19"-- => 18); a non-numeric one
1202
+ // has NO effect (PHP 8 deprecated the decrement on it).
1203
+ if (isNumericStr(v)) return Number(v.trim()) - 1;
1204
+ return v;
1205
+ }
1206
+ return v;
1207
+ };
1208
+ rt.truthy = (v) => {
1209
+ if (v === null || v === undefined) return false;
1210
+ if (typeof v === 'boolean') return v;
1211
+ if (typeof v === 'number') return v !== 0;
1212
+ if (typeof v === 'string') return v !== '' && v !== '0';
1213
+ if (Array.isArray(v)) return v.length !== 0;
1214
+ if (typeof v === 'object') return Object.keys(v).length !== 0;
1215
+ return true;
1216
+ };
1217
+ rt.not = (v) => !rt.truthy(v);
1218
+ rt.and = (fa, fb) => (rt.truthy(fa()) ? rt.truthy(fb()) : false);
1219
+ rt.or = (fa, fb) => (rt.truthy(fa()) ? true : rt.truthy(fb()));
1220
+
1221
+ // ---- PHP comparison semantics ----
1222
+ const isNumericStr = (s) => /^\s*[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?\s*$/.test(s);
1223
+ const tagOf = (v) => {
1224
+ if (v === null || v === undefined) return 'null';
1225
+ if (typeof v === 'boolean') return 'bool';
1226
+ if (typeof v === 'number') return 'num';
1227
+ if (typeof v === 'string') return 'str';
1228
+ return 'arr';
1229
+ };
1230
+ const arrEmpty = (v) => (Array.isArray(v) ? v.length === 0 : (v && typeof v === 'object' ? Object.keys(v).length === 0 : true));
1231
+
1232
+ // Two numeric strings, compared with the int64 precision PHP uses.
1233
+ const numericStringEq = (a, b) => {
1234
+ const norm = (s) => {
1235
+ const t = s.trim();
1236
+ const m = /^([+-]?)(\d*)(?:\.(\d*))?(?:[eE]([+-]?\d+))?$/.exec(t);
1237
+ if (!m) return null;
1238
+ const sign = m[1] === '-' ? -1n : 1n;
1239
+ const int = m[2] || '0';
1240
+ const frac = m[3] || '';
1241
+ const exp = m[4] ? parseInt(m[4], 10) : 0;
1242
+ return { sign, int, frac, exp };
1243
+ };
1244
+ const x = norm(a);
1245
+ const y = norm(b);
1246
+ if (!x || !y) return false;
1247
+ // The same value in decimal: align the exponents, then compare the digits.
1248
+ const toDecimal = (n) => {
1249
+ const digits = n.int + (n.frac || '');
1250
+ const point = n.int.length + n.exp;
1251
+ if (point <= 0) return { sign: n.sign, int: '0', frac: '0'.repeat(-point) + digits };
1252
+ if (point >= digits.length) return { sign: n.sign, int: digits + '0'.repeat(point - digits.length), frac: '' };
1253
+ return { sign: n.sign, int: digits.slice(0, point), frac: digits.slice(point) };
1254
+ };
1255
+ const dx = toDecimal(x);
1256
+ const dy = toDecimal(y);
1257
+ const strip = (v) => {
1258
+ let int = v.int.replace(/^0+(?=\d)/, '');
1259
+ let frac = v.frac.replace(/0+$/, '');
1260
+ return { sign: v.sign, int: int || '0', frac };
1261
+ };
1262
+ const sx = strip(dx);
1263
+ const sy = strip(dy);
1264
+ if (sx.sign !== sy.sign) return false;
1265
+ return sx.int === sy.int && sx.frac === sy.frac;
1266
+ };
1267
+ rt.looseEq = (a, b) => {
1268
+ const ta = tagOf(a), tb = tagOf(b);
1269
+ if (ta === 'null' && tb === 'null') return true;
1270
+ if (ta === 'bool' || tb === 'bool') return rt.truthy(a) === rt.truthy(b);
1271
+ if (ta === 'null') {
1272
+ if (tb === 'num') return Number(b) === 0;
1273
+ if (tb === 'str') return b === '';
1274
+ if (tb === 'arr') return arrEmpty(b);
1275
+ return false;
1276
+ }
1277
+ if (tb === 'null') return rt.looseEq(b, a);
1278
+ if (ta === 'num' && tb === 'num') return a === b;
1279
+ if (ta === 'str' && tb === 'str') {
1280
+ // Two numeric strings are compared as numbers - with the full int64
1281
+ // precision (JS's Number() rounds beyond 2^53, so '9007199254740993' ==
1282
+ // '9007199254740992' would answer true where PHP says false).
1283
+ if (isNumericStr(a) && isNumericStr(b)) return numericStringEq(a, b);
1284
+ return a === b;
1285
+ }
1286
+ if (ta === 'num' && tb === 'str') return isNumericStr(b) ? a === Number(b) : String(a) === String(b);
1287
+ if (ta === 'str' && tb === 'num') return isNumericStr(a) ? Number(a) === b : String(a) === String(b);
1288
+ return a === b;
1289
+ };
1290
+ rt.looseNe = (a, b) => !rt.looseEq(a, b);
1291
+
1292
+ rt.strictEq = (a, b, la, lb) => {
1293
+ const na = a === undefined ? null : a;
1294
+ const nb = b === undefined ? null : b;
1295
+ const ta = tagOf(na), tb = tagOf(nb);
1296
+ if (ta !== tb) return false;
1297
+ if (ta === 'num' && (!!la !== !!lb)) return false;
1298
+ return na === nb;
1299
+ };
1300
+ rt.strictNe = (a, b, la, lb) => !rt.strictEq(a, b, la, lb);
1301
+
1302
+ const numVal = (v) => (typeof v === 'number' ? v : (typeof v === 'string' && isNumericStr(v) ? Number(v) : null));
1303
+ const relPair = (a, b) => {
1304
+ const na = numVal(a), nb = numVal(b);
1305
+ if (na !== null && nb !== null) return [na, nb];
1306
+ return [phpToStr(a), phpToStr(b)];
1307
+ };
1308
+ rt.lt = (a, b) => { const p = relPair(a, b); return p[0] < p[1]; };
1309
+ rt.gt = (a, b) => { const p = relPair(a, b); return p[0] > p[1]; };
1310
+ rt.lte = (a, b) => { const p = relPair(a, b); return p[0] <= p[1]; };
1311
+ rt.gte = (a, b) => { const p = relPair(a, b); return p[0] >= p[1]; };
1312
+
1313
+ // ---- PHP arithmetic ----
1314
+ rt.sub = (a, b) => {
1315
+ const p = arithOperands('-', a, b);
1316
+ return p[0] - p[1];
1317
+ };
1318
+ rt.mul = (a, b) => {
1319
+ const p = arithOperands('*', a, b);
1320
+ return p[0] * p[1];
1321
+ };
1322
+ rt.div = (a, b) => {
1323
+ const p = arithOperands('/', a, b);
1324
+ // PHP's DivisionByZeroError (the class name reaches the 500).
1325
+ if (p[1] === 0) throw rt.newError('DivisionByZeroError', 'Division by zero');
1326
+ return p[0] / p[1];
1327
+ };
1328
+ rt.mod = (a, b) => {
1329
+ const p = arithOperands('%', a, b);
1330
+ if (p[1] === 0) throw rt.newError('DivisionByZeroError', 'Modulo by zero');
1331
+ return Math.trunc(p[0]) % Math.trunc(p[1]);
1332
+ };
1333
+ rt.pow = (a, b) => {
1334
+ const p = arithOperands('**', a, b);
1335
+ return Math.pow(p[0], p[1]);
1336
+ };
1337
+
1338
+ // ---- null coalescing ----
1339
+ rt.coalesce = (a, b) => (a === null || a === undefined ? b : a);
1340
+ rt.coalesceAssign = (get, set, val) => { const cur = get(); if (cur === null || cur === undefined) set(val); return get(); };
1341
+
1342
+ // ---- match (strict) ----
1343
+ rt.match = (v, ...arms) => {
1344
+ let dflt;
1345
+ for (const a of arms) {
1346
+ const conds = a[0], body = a[1];
1347
+ if (conds === null) { dflt = body; continue; }
1348
+ for (const c of conds) if (rt.strictEq(v, c, false, false)) return body();
1349
+ }
1350
+ if (dflt) return dflt();
1351
+ throw new Error('match: unhandled value');
1352
+ };
1353
+
1354
+ // ---- foreach over PHP arrays/objects ----
1355
+ // A Collection (Illuminate\Collections\Collection): the items live in
1356
+ // `items` and the iteration yields them (Collection::getIterator()).
1357
+ const isCollection = (v) => v !== null && typeof v === 'object' && v[IS_COLLECTION] === true;
1358
+ // The spread (`[...$a]`): the VALUES of the array, in order (PHP drops the
1359
+ // keys - the spread builds a list).
1360
+ rt.spread = (src) => {
1361
+ if (src === null || src === undefined) return [];
1362
+ if (Array.isArray(src)) return src.slice();
1363
+ if (typeof src === 'object') {
1364
+ if (Array.isArray(src.__items)) return src.__items.slice();
1365
+ // A Collection spreads its ITEMS (PHP: [...collect([1,2])] => [1,2] -
1366
+ // Illuminate\Collections\Collection::getIterator()).
1367
+ if (isCollection(src)) return src.items.slice();
1368
+ return Object.keys(src).map((k) => src[k]);
1369
+ }
1370
+ return [src];
1371
+ };
1372
+ // A string offset (`$s[-1]`): PHP 7.1+ counts from the end (a negative
1373
+ // offset), the positive ones from the start.
1374
+ rt.strOffset = (s, offset) => {
1375
+ const str = String(s == null ? '' : s);
1376
+ const off = Number(offset) || 0;
1377
+ const at = off < 0 ? str.length + off : off;
1378
+ if (at < 0 || at >= str.length) return '';
1379
+ return str[at];
1380
+ };
1381
+ rt.entries = (src) => {
1382
+ // A paginator (and a Collection) iterate their items, not their fields.
1383
+ if (src && typeof src === 'object' && Array.isArray(src.__items)) {
1384
+ return src.__items.map((v, i) => [i, v]);
1385
+ }
1386
+ // A Collection (Illuminate\Collections\Collection::getIterator()): the
1387
+ // ITEMS, keyed by their position - not the fields of the wrapper.
1388
+ if (isCollection(src)) {
1389
+ return src.items.map((v, i) => [i, v]);
1390
+ }
1391
+ if (src && typeof src === 'object') {
1392
+ const keyOf = (k) => (/^\d+$/.test(k) ? Number(k) : k);
1393
+ return rt.phpKeys(src).map((k) => [keyOf(k), src[k]]);
1394
+ }
1395
+ return [];
1396
+ };
1397
+
1398
+ // ---- type predicates ----
1399
+ const isNum = (v) => (typeof v === 'number' && !Number.isNaN(v)) || (typeof v === 'string' && v.trim() !== '' && !Number.isNaN(Number(v)));
1400
+ rt.is_null = (v) => v === null || v === undefined;
1401
+ rt.is_bool = (v) => typeof v === 'boolean';
1402
+ rt.is_int = (v) => typeof v === 'number' && Number.isInteger(v);
1403
+ rt.is_float = (v) => typeof v === 'number' && !Number.isInteger(v);
1404
+ rt.is_string = (v) => typeof v === 'string';
1405
+ rt.is_array = (v) => Array.isArray(v) || isObj(v);
1406
+ rt.is_object = (v) => isObj(v) && !Array.isArray(v);
1407
+ rt.is_numeric = isNum;
1408
+ rt.is_callable = (v) => typeof v === 'function';
1409
+ rt.is_countable = (v) => Array.isArray(v) || isObj(v);
1410
+ rt.is_boolean = rt.is_bool;
1411
+ rt.is_integer = rt.is_int;
1412
+ rt.is_long = rt.is_int;
1413
+ rt.is_double = rt.is_float;
1414
+ rt.is_real = rt.is_float;
1415
+
1416
+ // ---- numbers ----
1417
+ rt.abs = (v) => Math.abs(Number(v));
1418
+ const phpRound = (x, p) => {
1419
+ const m = Math.pow(10, p);
1420
+ const v = x * m;
1421
+ return (v < 0 ? -Math.round(-v) : Math.round(v)) / m;
1422
+ };
1423
+ rt.round = (v, p) => phpRound(Number(v), p == null ? 0 : Number(p) | 0);
1424
+ rt.floor = (v) => Math.floor(Number(v));
1425
+ rt.ceil = (v) => Math.ceil(Number(v));
1426
+ rt.intval = (v, base) => {
1427
+ if (typeof v === 'number') return Math.trunc(v);
1428
+ if (typeof v === 'boolean') return v ? 1 : 0;
1429
+ const s = String(v == null ? '' : v).trim();
1430
+ const b = base == null ? 10 : Number(base) || 10;
1431
+ const re = b === 16 ? /^[+-]?(0x)?[0-9a-f]+/i : b === 8 ? /^[+-]?[0-7]+/ : /^[+-]?\d+/;
1432
+ const m = s.match(re);
1433
+ return m ? parseInt(m[0], b) : 0;
1434
+ };
1435
+ rt.floatval = (v) => {
1436
+ if (typeof v === 'number') return v;
1437
+ const m = String(v == null ? '' : v).trim().match(/^[+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?/);
1438
+ return m ? parseFloat(m[0]) : 0;
1439
+ };
1440
+ const maxMin = (dir, a) => {
1441
+ const vals = a.length === 1 && Array.isArray(a[0]) ? a[0] : a;
1442
+ let best = vals[0];
1443
+ for (const v of vals) if (dir > 0 ? Number(v) > Number(best) : Number(v) < Number(best)) best = v;
1444
+ return best;
1445
+ };
1446
+ rt.max = (...a) => maxMin(1, a);
1447
+ rt.min = (...a) => maxMin(-1, a);
1448
+
1449
+ // ---- strings (continued) ----
1450
+ const PHP_WS = ' \t\n\r\u0000\u000B';
1451
+ const trimChars = (s, chars, dir) => {
1452
+ const set = new Set(chars.split(''));
1453
+ const a = String(s == null ? '' : s).split('');
1454
+ let i = 0, j = a.length;
1455
+ if (dir !== 'r') while (i < j && set.has(a[i])) i++;
1456
+ if (dir !== 'l') while (j > i && set.has(a[j - 1])) j--;
1457
+ return a.slice(i, j).join('');
1458
+ };
1459
+ rt.ltrim = (s, chars) => trimChars(s, chars == null ? PHP_WS : String(chars), 'l');
1460
+ rt.rtrim = (s, chars) => trimChars(s, chars == null ? PHP_WS : String(chars), 'r');
1461
+ rt.ucwords = (s) => String(s == null ? '' : s).replace(/(^|[ \t\r\n\f\u000B])(\S)/g, (m, p, c) => p + c.toUpperCase());
1462
+ rt.strrev = (s) => String(s == null ? '' : s).split('').reverse().join('');
1463
+ rt.strpos = (h, n, off) => {
1464
+ const i = String(h == null ? '' : h).indexOf(String(n), off == null ? 0 : Number(off) | 0);
1465
+ return i < 0 ? false : i;
1466
+ };
1467
+ rt.strrpos = (h, n, off) => {
1468
+ const s = String(h == null ? '' : h);
1469
+ const from = off == null ? s.length : Number(off) | 0;
1470
+ const i = s.lastIndexOf(String(n), from);
1471
+ return i < 0 ? false : i;
1472
+ };
1473
+ rt.substr_count = (h, n, off, len) => {
1474
+ const s = String(h == null ? '' : h).slice(off == null ? 0 : Number(off) | 0);
1475
+ const needle = String(n);
1476
+ if (needle === '') return 0;
1477
+ let count = 0, i = 0;
1478
+ for (;;) {
1479
+ const at = s.indexOf(needle, i);
1480
+ if (at < 0) break;
1481
+ count++;
1482
+ i = at + needle.length;
1483
+ }
1484
+ return count;
1485
+ };
1486
+ rt.strip_tags = (s) => String(s == null ? '' : s).replace(/<[^>]*>/g, '');
1487
+ rt.htmlspecialchars = (s) => (s == null ? '' : rt.e(s));
1488
+
1489
+ // ---- json ----
1490
+ // Illuminate\View\Compilers\Concerns\CompilesJson:12 - Blade's `@json($x)`
1491
+ // compiles to `json_encode($x, JSON_HEX_TAG|JSON_HEX_APOS|JSON_HEX_AMP|JSON_HEX_QUOT, 512)`
1492
+ // (CompilesJson:28): the four HEX flags escape `<`, `>`, `&`, `'` and `"` as
1493
+ // \uXXXX (PHP writes the hex in upper case) and the depth bounds the nesting
1494
+ // (PHP's json_encode() returns false past it).
1495
+ // The kernel's encoder (kernel/php-json.mjs) writes the DEFAULT flags, so the
1496
+ // runtime applies the HEX ones here. The other flags are not ported: they
1497
+ // would answer a different body, so they fail loud.
1498
+ const JSON_HEX_TAG = 1;
1499
+ const JSON_HEX_AMP = 2;
1500
+ const JSON_HEX_APOS = 4;
1501
+ const JSON_HEX_QUOT = 8;
1502
+ const JSON_HEX_FLAGS = JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT;
1503
+ // The HEX flags rewrite the characters INSIDE the encoded JSON: `<`, `>`,
1504
+ // `&` and `'` are not JSON syntax, so a literal one in the output can only
1505
+ // come from a string. The `"` is the ESCAPED one (`\"` inside a string) -
1506
+ // the string delimiters are bare quotes.
1507
+ const jsonHexEncode = (json, flags) => {
1508
+ if (!flags) return json;
1509
+ let out = '';
1510
+ let i = 0;
1511
+ while (i < json.length) {
1512
+ const c = json[i];
1513
+ if (c !== '"') {
1514
+ out += c;
1515
+ i++;
1516
+ continue;
1517
+ }
1518
+ out += '"';
1519
+ i++;
1520
+ while (i < json.length) {
1521
+ const ch = json[i];
1522
+ if (ch === '\\') {
1523
+ const next = json[i + 1];
1524
+ if (next === '"' && flags & JSON_HEX_QUOT) {
1525
+ out += '\\u0022';
1526
+ i += 2;
1527
+ continue;
1528
+ }
1529
+ // \\ \/ \b \t \n \f \r \u - the hex digits of a \u escape are copied
1530
+ // as they are (they are never one of the flagged characters).
1531
+ out += ch + next;
1532
+ i += 2;
1533
+ continue;
1534
+ }
1535
+ if (ch === '"') {
1536
+ out += '"';
1537
+ i++;
1538
+ break;
1539
+ }
1540
+ if (flags & JSON_HEX_TAG) {
1541
+ if (ch === '<') {
1542
+ out += '\\u003C';
1543
+ i++;
1544
+ continue;
1545
+ }
1546
+ if (ch === '>') {
1547
+ out += '\\u003E';
1548
+ i++;
1549
+ continue;
1550
+ }
1551
+ }
1552
+ if (flags & JSON_HEX_AMP && ch === '&') {
1553
+ out += '\\u0026';
1554
+ i++;
1555
+ continue;
1556
+ }
1557
+ if (flags & JSON_HEX_APOS && ch === "'") {
1558
+ out += '\\u0027';
1559
+ i++;
1560
+ continue;
1561
+ }
1562
+ out += ch;
1563
+ i++;
1564
+ }
1565
+ }
1566
+ return out;
1567
+ };
1568
+ // PHP's json_encode() depth: the nesting of the arrays/objects (a scalar is
1569
+ // no level). PHP returns false past it ("Maximum stack depth exceeded").
1570
+ const jsonDepth = (v) => {
1571
+ if (Array.isArray(v)) {
1572
+ let deepest = 0;
1573
+ for (const item of v) {
1574
+ const d = jsonDepth(item);
1575
+ if (d > deepest) deepest = d;
1576
+ }
1577
+ return 1 + deepest;
1578
+ }
1579
+ if (isPhpArray(v)) {
1580
+ let deepest = 0;
1581
+ for (const k of Object.keys(v)) {
1582
+ const d = jsonDepth(v[k]);
1583
+ if (d > deepest) deepest = d;
1584
+ }
1585
+ return 1 + deepest;
1586
+ }
1587
+ return 0;
1588
+ };
1589
+ rt.json_encode = (v, flags, depth) => {
1590
+ const f = flags == null ? 0 : Number(flags) | 0;
1591
+ const d = depth == null ? 512 : Number(depth) | 0;
1592
+ if (f & ~JSON_HEX_FLAGS) {
1593
+ throw new Error(
1594
+ 'json_encode() flags ' + f + ' are not supported at the edge — PHP would write a different ' +
1595
+ 'body with them (the ported ones are JSON_HEX_TAG|JSON_HEX_AMP|JSON_HEX_APOS|JSON_HEX_QUOT) — ' +
1596
+ 'drop the flags or write a port'
1597
+ );
1598
+ }
1599
+ const value = v == null ? null : v;
1600
+ if (jsonDepth(value) > d) return false;
1601
+ return jsonHexEncode(phpJsonEncode(value), f);
1602
+ };
1603
+ rt.json_decode = (s, assoc) => JSON.parse(String(s));
1604
+
1605
+ // ---- more array helpers ----
1606
+ rt.array_sum = (arr) => arrValues(arr).reduce((s, x) => s + Number(x), 0);
1607
+ rt.array_slice = (arr, offset, length) => {
1608
+ const a = arrValues(arr);
1609
+ const off = Number(offset) | 0;
1610
+ const o = off < 0 ? Math.max(a.length + off, 0) : off;
1611
+ if (length === undefined || length === null) return a.slice(o);
1612
+ return a.slice(o, o + Math.max(0, Number(length) | 0));
1613
+ };
1614
+ rt.array_reverse = (arr) => arrValues(arr).slice().reverse();
1615
+ rt.array_unique = (arr) => {
1616
+ const out = [];
1617
+ for (const v of arrValues(arr)) if (!out.some((x) => phpToStr(x) === phpToStr(v))) out.push(v);
1618
+ return out;
1619
+ };
1620
+ rt.array_count_values = (arr) => {
1621
+ const o = {};
1622
+ for (const v of arrValues(arr)) {
1623
+ const k = phpToStr(v);
1624
+ o[k] = (o[k] || 0) + 1;
1625
+ }
1626
+ return o;
1627
+ };
1628
+ rt.array_flip = (arr) => {
1629
+ const o = {};
1630
+ if (isObj(arr)) for (const k of Object.keys(arr)) o[phpToStr(arr[k])] = /^\d+$/.test(k) ? Number(k) : k;
1631
+ return o;
1632
+ };
1633
+ rt.array_filter = (arr, cb) => arrValues(arr).filter((v) => (cb ? !!cb(v) : !!v));
1634
+ rt.array_map = (cb, arr) => arrValues(arr).map((v) => cb(v));
1635
+
1636
+ rt.str = phpToStr;
1637
+ rt.optional = (value, cb = null) => {
1638
+ if (typeof cb === 'function') return value == null ? null : cb(value);
1639
+ if (value == null) {
1640
+ const fn = () => null;
1641
+ fn.toString = () => '';
1642
+ fn.valueOf = () => null;
1643
+ return new Proxy(fn, {
1644
+ get(t, prop) {
1645
+ if (prop === 'toString' || prop === 'valueOf' || prop === Symbol.toPrimitive) return t[prop];
1646
+ return fn;
1647
+ },
1648
+ });
1649
+ }
1650
+ return value;
1651
+ };
1652
+
1653
+ rt.number_format = (n, dec = 0, dp = '.', ts = ',') => {
1654
+ const x = Number(n);
1655
+ const s = Math.abs(x).toFixed(dec);
1656
+ const [ip, fp] = s.split('.');
1657
+ const grouped = ip.replace(/\B(?=(\d{3})+(?!\d))/g, ts);
1658
+ return (x < 0 ? '-' : '') + grouped + (fp ? dp + fp : '');
1659
+ };
1660
+ rt.nl2br = (s) => String(s == null ? '' : s).replace(/\r\n|\r|\n/g, '<br />\n');
1661
+
1662
+ // ---- Collection wrapper (rt.collect) ----
1663
+ // ONE Collection for the whole edge: the kernel's (probed against Laravel,
1664
+ // kernel/collections.mjs). The runtime used to carry a private subset here,
1665
+ // so `collect()` inside a compiled route and inside a view were two
1666
+ // different classes with two different answers (the inline one had no
1667
+ // groupBy/keyBy/where, and its implode()/chunk() disagreed with Laravel).
1668
+ rt.collect = (arr) => new Collection(arr == null ? [] : arr);
1669
+ // `Collection::wrap($value)` is NOT `collect($value)`: wrap() applies
1670
+ // Arr::wrap (a non-array becomes ONE item), while collect() runs
1671
+ // getArrayableItems() (an Arrayable becomes its attributes).
1672
+ rt.collectionWrap = (value) => Collection.wrap(value);
1673
+ rt.Collection = Collection;
1674
+
1675
+ // ---- rt.now() minimal Carbon stub ----
1676
+ //
1677
+ // Laravel sets PHP's default timezone from `config('app.timezone')` and every
1678
+ // Carbon date renders in it, so a date in a body is the APP's local time -
1679
+ // not UTC. The stub renders in the same zone: `Intl` is the only timezone
1680
+ // database a Worker has (it ships the full IANA set), and the parts are
1681
+ // cached per zone. A UTC app takes the fast path (the `getUTC*` accessors).
1682
+ const pad2 = (n) => String(n).padStart(2, '0');
1683
+ const pad4 = (n) => String(n).padStart(4, '0');
1684
+ const APP_TZ = String(configCtx['app.timezone'] || 'UTC');
1685
+ const TZ_IS_UTC = APP_TZ === 'UTC' || APP_TZ === 'Etc/UTC' || APP_TZ === 'GMT';
1686
+ const tzFormatters = new Map();
1687
+ function zoneFormatter(tz) {
1688
+ let f = tzFormatters.get(tz);
1689
+ if (!f) {
1690
+ f = new Intl.DateTimeFormat('en-US', {
1691
+ timeZone: tz,
1692
+ hour12: false,
1693
+ year: 'numeric',
1694
+ month: '2-digit',
1695
+ day: '2-digit',
1696
+ hour: '2-digit',
1697
+ minute: '2-digit',
1698
+ second: '2-digit',
1699
+ });
1700
+ tzFormatters.set(tz, f);
1701
+ }
1702
+ return f;
1703
+ }
1704
+ /** The wall-clock parts of `d` in `tz` (a timezone the app configured). */
1705
+ function zoneParts(d, tz) {
1706
+ if (tz === null || tz === undefined || TZ_IS_UTC) {
1707
+ return {
1708
+ Y: d.getUTCFullYear(),
1709
+ m: d.getUTCMonth() + 1,
1710
+ d: d.getUTCDate(),
1711
+ H: d.getUTCHours(),
1712
+ i: d.getUTCMinutes(),
1713
+ s: d.getUTCSeconds(),
1714
+ };
1715
+ }
1716
+ const parts = {};
1717
+ for (const part of zoneFormatter(tz).formatToParts(d)) {
1718
+ if (part.type !== 'literal') parts[part.type] = part.value;
1719
+ }
1720
+ // `hour12: false` renders midnight as 24 in some engines; PHP's `H` is 00.
1721
+ const hour = Number(parts.hour) % 24;
1722
+ return {
1723
+ Y: Number(parts.year),
1724
+ m: Number(parts.month),
1725
+ d: Number(parts.day),
1726
+ H: hour,
1727
+ i: Number(parts.minute),
1728
+ s: Number(parts.second),
1729
+ };
1730
+ }
1731
+ function makeCarbon(date, tz = APP_TZ) {
1732
+ const d = new Date(date.getTime());
1733
+ const api = {
1734
+ _d: d,
1735
+ _tz: tz,
1736
+ format(fmt) {
1737
+ const p = zoneParts(d, tz);
1738
+ const Y = pad4(p.Y);
1739
+ const m = pad2(p.m);
1740
+ const dd = pad2(p.d);
1741
+ const H = pad2(p.H);
1742
+ const i = pad2(p.i);
1743
+ const s = pad2(p.s);
1744
+ const n = String(p.m);
1745
+ const j = String(p.d);
1746
+ const G = String(p.H);
1747
+ const g0 = p.H % 12 || 12;
1748
+ const g = String(g0);
1749
+ const a = p.H < 12 ? 'am' : 'pm';
1750
+ const A = p.H < 12 ? 'AM' : 'PM';
1751
+ const U = String(Math.floor(d.getTime() / 1000));
1752
+ return String(fmt).replace(/Y|m|d|H|i|s|n|j|G|g|a|A|U/g, (ch) => {
1753
+ switch (ch) {
1754
+ case 'Y': return Y;
1755
+ case 'm': return m;
1756
+ case 'd': return dd;
1757
+ case 'H': return H;
1758
+ case 'i': return i;
1759
+ case 's': return s;
1760
+ case 'n': return n;
1761
+ case 'j': return j;
1762
+ case 'G': return G;
1763
+ case 'g': return g;
1764
+ case 'a': return a;
1765
+ case 'A': return A;
1766
+ case 'U': return U;
1767
+ default: return ch;
1768
+ }
1769
+ });
1770
+ },
1771
+ // The date in the APP's timezone (Carbon renders in its own zone, and
1772
+ // Laravel's is `config('app.timezone')`).
1773
+ toDateString() {
1774
+ const p = zoneParts(d, tz);
1775
+ return pad4(p.Y) + '-' + pad2(p.m) + '-' + pad2(p.d);
1776
+ },
1777
+ toDateTimeString() {
1778
+ const p = zoneParts(d, tz);
1779
+ return pad4(p.Y) + '-' + pad2(p.m) + '-' + pad2(p.d) + ' ' + pad2(p.H) + ':' + pad2(p.i) + ':' + pad2(p.s);
1780
+ },
1781
+ get timezoneName() { return tz; },
1782
+ getTimezone() { return tz; },
1783
+ get timestamp() { return Math.floor(d.getTime() / 1000); },
1784
+ getTimestamp() { return Math.floor(d.getTime() / 1000); },
1785
+ addDays(n) { const nd = new Date(d.getTime()); nd.setUTCDate(nd.getUTCDate() + Number(n)); return makeCarbon(nd, tz); },
1786
+ subDays(n) { const nd = new Date(d.getTime()); nd.setUTCDate(nd.getUTCDate() - Number(n)); return makeCarbon(nd, tz); },
1787
+ addHours(n) { const nd = new Date(d.getTime()); nd.setUTCHours(nd.getUTCHours() + Number(n)); return makeCarbon(nd, tz); },
1788
+ subHours(n) { const nd = new Date(d.getTime()); nd.setUTCHours(nd.getUTCHours() - Number(n)); return makeCarbon(nd, tz); },
1789
+ copy() { return makeCarbon(new Date(d.getTime()), tz); },
1790
+ clone() { return makeCarbon(new Date(d.getTime()), tz); },
1791
+ toString() { return api.toDateTimeString(); },
1792
+ valueOf() { return d.getTime(); },
1793
+ };
1794
+ return api;
1795
+ }
1796
+ // The request clock (integer seconds): the oracle freezes Carbon, so the
1797
+ // compiled side must read the same instant - the wall clock diverges.
1798
+ const clockDate = () => (requestClock ? new Date(requestClock() * 1000) : new Date());
1799
+ rt.now = () => makeCarbon(clockDate());
1800
+ // `today()` is midnight in the APP's timezone (Carbon::today() uses the
1801
+ // default zone), so the UTC instant is the zone's midnight converted back.
1802
+ rt.today = () => {
1803
+ const d = clockDate();
1804
+ const p = zoneParts(d, APP_TZ);
1805
+ const utcMidnight = Date.UTC(p.Y, p.m - 1, p.d, 0, 0, 0);
1806
+ // The zone's offset at that instant: the parts of the UTC midnight tell it.
1807
+ const asUtc = Date.UTC(p.Y, p.m - 1, p.d, p.H, p.i, p.s);
1808
+ const offset = asUtc - Math.floor(d.getTime() / 1000) * 1000;
1809
+ return makeCarbon(new Date(utcMidnight - offset));
1810
+ };
1811
+
1812
+ // ---- controller runtime (view data, models, responses) ----
1813
+ // rt.var reads runtime view data (injected by controller renders). When no data is
1814
+ // present the value is `undefined` — byte-identical to pre-data-injection behavior.
1815
+ const dataCtx = (opts && opts.data && typeof opts.data === 'object') ? opts.data : {};
1816
+
1817
+ // Blade's compiled @foreach bookkeeping calls $__env->addLoop() /
1818
+ // incrementLoopIndices() / getLastLoop() / popLoop(). Provide a per-rt loop
1819
+ // stack that mirrors Illuminate\View\Concerns\ManagesLoops so compiled views
1820
+ // with loops render at runtime.
1821
+ //
1822
+ // The same object carries the **sections** (Illuminate\View\Concerns\ManagesLayouts):
1823
+ // a layout is a compiled view that yields the sections of the view that
1824
+ // extends it. The env is SHARED with the nested renders (the layout is
1825
+ // rendered with the same env), so the sections it yields are the ones the
1826
+ // child view pushed.
1827
+ const loopsStack = [];
1828
+ const sections = {};
1829
+ const sectionStack = [];
1830
+ const ownEnv = {
1831
+ addLoop(dataSrc) {
1832
+ let length = null;
1833
+ if (Array.isArray(dataSrc)) length = dataSrc.length;
1834
+ else if (dataSrc instanceof Map) length = dataSrc.size;
1835
+ // A Collection is countable (Illuminate\View\Concerns\ManagesLoops:25 -
1836
+ // count($collection) is the number of the items).
1837
+ else if (isCollection(dataSrc)) length = dataSrc.items.length;
1838
+ else if (dataSrc && typeof dataSrc === 'object') {
1839
+ length = typeof dataSrc.length === 'number' ? dataSrc.length : Object.keys(dataSrc).length;
1840
+ }
1841
+ const parent = loopsStack.length ? { ...loopsStack[loopsStack.length - 1] } : null;
1842
+ loopsStack.push({
1843
+ iteration: 0,
1844
+ index: 0,
1845
+ remaining: length,
1846
+ count: length,
1847
+ first: true,
1848
+ last: length === null ? null : length === 1,
1849
+ odd: false,
1850
+ even: true,
1851
+ depth: loopsStack.length + 1,
1852
+ parent,
1853
+ });
1854
+ },
1855
+ incrementLoopIndices() {
1856
+ const loop = loopsStack[loopsStack.length - 1];
1857
+ if (!loop) return;
1858
+ const iteration = loop.iteration;
1859
+ loop.iteration = iteration + 1;
1860
+ loop.index = iteration;
1861
+ loop.first = iteration === 0;
1862
+ loop.odd = !loop.odd;
1863
+ loop.even = !loop.even;
1864
+ const hasCount = loop.count !== null && loop.count !== undefined;
1865
+ loop.remaining = hasCount ? loop.remaining - 1 : null;
1866
+ loop.last = hasCount ? iteration === loop.count - 1 : null;
1867
+ },
1868
+ popLoop() {
1869
+ loopsStack.pop();
1870
+ },
1871
+ getLastLoop() {
1872
+ return loopsStack.length ? loopsStack[loopsStack.length - 1] : null;
1873
+ },
1874
+ getLoopStack() {
1875
+ return loopsStack;
1876
+ },
1877
+ // ---- the sections (a layout yields them) ----
1878
+ // Illuminate\View\Concerns\ManagesLayouts: startSection($name, $content)
1879
+ // with a content sets the section; without one it captures the output
1880
+ // until stopSection().
1881
+ startSection(name, content) {
1882
+ const key = String(name);
1883
+ if (content !== null && content !== undefined) {
1884
+ // Illuminate\View\Concerns\ManagesLayouts:53 - the inline content is
1885
+ // escaped (e($content)). The captured block (@section ... @endsection)
1886
+ // is raw (stopSection() keeps the output as it is).
1887
+ sections[key] = rt.e(content);
1888
+ return;
1889
+ }
1890
+ sectionStack.push({ name: key, buffer: '' });
1891
+ },
1892
+ stopSection() {
1893
+ const s = sectionStack.pop();
1894
+ if (s) sections[s.name] = s.buffer;
1895
+ },
1896
+ yieldContent(name, fallback) {
1897
+ const key = String(name);
1898
+ if (Object.prototype.hasOwnProperty.call(sections, key)) return sections[key];
1899
+ // Illuminate\View\Concerns\ManagesLayouts:157 - the default is escaped
1900
+ // (e($default)).
1901
+ return fallback === null || fallback === undefined ? '' : rt.e(fallback);
1902
+ },
1903
+ hasSection(name) {
1904
+ return Object.prototype.hasOwnProperty.call(sections, String(name));
1905
+ },
1906
+ // The echo of a view that is capturing a section goes into the section.
1907
+ capturing() {
1908
+ return sectionStack.length > 0;
1909
+ },
1910
+ capture(text) {
1911
+ if (!sectionStack.length) return;
1912
+ sectionStack[sectionStack.length - 1].buffer += String(text);
1913
+ },
1914
+ // `make($view, $data)->render()` — the layout the view extends.
1915
+ make(name, data) {
1916
+ return {
1917
+ render: () => {
1918
+ const rendered = rt.view(String(name), data || {});
1919
+ return rendered && rendered.body != null ? String(rendered.body) : '';
1920
+ },
1921
+ };
1922
+ },
1923
+ };
1924
+
1925
+ // The env is shared with the nested renders (a layout is rendered with the
1926
+ // env of the view that extends it) — the sections travel with it.
1927
+ const bladeEnv = (opts && opts.env) || ownEnv;
1928
+
1929
+ rt.var = (k) => {
1930
+ if (Object.prototype.hasOwnProperty.call(dataCtx, k)) return dataCtx[k];
1931
+ if (k === '__env') return bladeEnv;
1932
+ return undefined;
1933
+ };
1934
+
1935
+ // rt.vars(): the variables a compiled view sees (PHP's get_defined_vars()):
1936
+ // the data it was given + the locals the view set itself.
1937
+ rt.vars = (locals) => {
1938
+ const out = Object.assign({}, dataCtx);
1939
+ if (locals && typeof locals === 'object') {
1940
+ for (const k of Object.keys(locals)) {
1941
+ if (locals[k] !== undefined) out[k] = locals[k];
1942
+ }
1943
+ }
1944
+ return out;
1945
+ };
1946
+
1947
+ // rt.array_diff_key($a, $b) — the keys of $a that $b does not have.
1948
+ rt.array_diff_key = (a, b) => {
1949
+ const out = {};
1950
+ if (!a || typeof a !== 'object') return out;
1951
+ for (const k of Object.keys(a)) {
1952
+ if (b && Object.prototype.hasOwnProperty.call(b, k)) continue;
1953
+ out[k] = a[k];
1954
+ }
1955
+ return out;
1956
+ };
1957
+
1958
+ // rt.prop: PHP `->` property access on models (attributes via get()), plain
1959
+ // objects and arrays. Missing values become null (PHP null-ish read).
1960
+ rt.prop = (obj, key) => {
1961
+ if (obj === null || obj === undefined) return null;
1962
+ if (obj instanceof Promise) {
1963
+ throw new Error('unresolved promise — await the query in a single chain before reading ->' + key);
1964
+ }
1965
+ if (typeof obj.get === 'function' && obj.attributes && typeof obj.attributes === 'object') {
1966
+ const v = obj.get(key);
1967
+ return v === undefined ? null : v;
1968
+ }
1969
+ if (typeof obj === 'object') return obj[key] === undefined ? null : obj[key];
1970
+ return null;
1971
+ };
1972
+
1973
+ // rt.propSet: PHP `$model->attribute = $value` (Eloquent::setAttribute). The
1974
+ // Model#set() path applies the casts (and the `hashed` cast hashes at
1975
+ // set-time, like Eloquent); plain objects/arrays get a plain assignment.
1976
+ rt.propSet = (obj, key, value) => {
1977
+ if (obj === null || obj === undefined) {
1978
+ throw new Error('cannot set ->' + key + ' on null');
1979
+ }
1980
+ if (typeof obj.set === 'function' && obj.attributes && typeof obj.attributes === 'object') {
1981
+ obj.set(key, value);
1982
+ return value;
1983
+ }
1984
+ if (typeof obj === 'object') {
1985
+ obj[key] = value;
1986
+ return value;
1987
+ }
1988
+ throw new Error('cannot set ->' + key + ' on ' + typeof obj);
1989
+ };
1990
+
1991
+ const statusErr = (status, message) => {
1992
+ const e = new Error(message == null ? '' : String(message));
1993
+ e.status = Number(status) || 500;
1994
+ return e;
1995
+ };
1996
+ rt.statusErr = statusErr;
1997
+ rt.abort = (code, message) => { throw statusErr(code, message || ('HTTP ' + code)); };
1998
+ // Laravel's abort_if()/abort_unless(): they answer null unless they abort.
1999
+ rt.abortIf = (condition, code, message) => {
2000
+ if (condition) rt.abort(code, message);
2001
+ return null;
2002
+ };
2003
+ rt.abortUnless = (condition, code, message) => {
2004
+ if (!condition) rt.abort(code, message);
2005
+ return null;
2006
+ };
2007
+ rt.errorResponse = (e) => {
2008
+ const status = (e && e.status) || 500;
2009
+ const message = e && e.message ? String(e.message) : 'HTTP ' + status;
2010
+ // Illuminate\Foundation\Exceptions\Handler::prepareJsonResponse(): an
2011
+ // expectsJson request gets the exception as JSON, and Laravel encodes it
2012
+ // with JSON_PRETTY_PRINT (4 spaces) - convertExceptionToArray() answers
2013
+ // `['message' => $e->getMessage()]` for an HTTP exception. The bytes are
2014
+ // PHP's (the slashes escaped), so the encoder is the kernel's.
2015
+ if (liveRequest && typeof liveRequest.expectsJson === 'function' && liveRequest.expectsJson()) {
2016
+ return {
2017
+ status,
2018
+ headers: { 'content-type': 'application/json' },
2019
+ body: '{\n "message": ' + phpJsonEncode(message) + '\n}',
2020
+ };
2021
+ }
2022
+ return {
2023
+ status,
2024
+ headers: { 'content-type': 'text/plain; charset=utf-8' },
2025
+ body: message,
2026
+ };
2027
+ };
2028
+ // PHP's json_encode() honours JsonSerializable::jsonSerialize(): a Model
2029
+ // answers its toArray() (`{"id":1,"name":"a"}`) and an Eloquent Collection
2030
+ // answers an ARRAY of them. Without this the raw object reached the encoder
2031
+ // and threw "unsupported value (Function)" - a 500 where Laravel answers the
2032
+ // model's JSON.
2033
+ const jsonSerializable = (value) => {
2034
+ if (value === null || typeof value !== 'object') return value;
2035
+ if (Array.isArray(value)) return value.map(jsonSerializable);
2036
+ // A kernel Collection: `toArray()` (its `toJSON()` is a JSON STRING, like
2037
+ // Laravel's toJson(), so it must not be used here).
2038
+ if (typeof value.toArray === 'function' && typeof value.all === 'function') {
2039
+ return value.toArray();
2040
+ }
2041
+ // A model: `toJSON()` is its toArray() (the kernel's own shape).
2042
+ if (typeof value.toJSON === 'function') return value.toJSON();
2043
+ // Any other JsonSerializable (`jsonSerialize()` - a paginator answers its
2044
+ // toArray(), which is the body `response()->json($paginator)` sends) or
2045
+ // Arrayable (`toArray()`).
2046
+ if (typeof value.jsonSerialize === 'function') return value.jsonSerialize();
2047
+ if (typeof value.toArray === 'function') return value.toArray();
2048
+ // A plain PHP array (an assoc array is a JS object here): PHP's
2049
+ // json_encode() recurses into it, so a Collection or a model nested in it
2050
+ // is serialised too. Without this `response()->json(['data' => $posts])` -
2051
+ // the most common JSON shape there is - threw "unsupported value".
2052
+ const out = {};
2053
+ for (const key of Object.keys(value)) out[key] = jsonSerializable(value[key]);
2054
+ return out;
2055
+ };
2056
+ rt.json = function (value, status) {
2057
+ // `json($data = [])`: no argument at all is an empty ARRAY ("[]"), while an
2058
+ // explicit null is Symfony's `$data ??= new \ArrayObject()` -> "{}"
2059
+ // (JsonResponse.php:47).
2060
+ const noArg = arguments.length === 0;
2061
+ return {
2062
+ status: Number(status) || 200,
2063
+ headers: { 'content-type': 'application/json' },
2064
+ // PHP's json_encode bytes: the slashes are escaped ("a\/b"), the
2065
+ // non-ASCII as \uXXXX (the surrogate pairs too). A plain JSON.stringify
2066
+ // would answer a different body (Laravel's bytes are the parity).
2067
+ body: noArg ? '[]' : value == null ? '{}' : phpJsonEncode(jsonSerializable(value)),
2068
+ };
2069
+ };
2070
+ // Laravel's Router::toResponse (Illuminate/Routing/Router.php:929-953): the
2071
+ // value a controller returns becomes the response - a string is the body
2072
+ // (text/html), an array or an object is JSON, null is an empty 200, and a
2073
+ // model that was just created answers 201.
2074
+ rt.toResponse = (value) => {
2075
+ // A response the controller already built (`view()`, `response()->json()`,
2076
+ // `redirect()`) passes through untouched - Laravel's prepareResponse does
2077
+ // the same with a Response instance.
2078
+ if (
2079
+ value && typeof value === 'object' && !Array.isArray(value) &&
2080
+ typeof value.status === 'number' && value.body !== undefined &&
2081
+ typeof value.toJSON !== 'function'
2082
+ ) {
2083
+ return value;
2084
+ }
2085
+ if (value === null || value === undefined) {
2086
+ return { status: 200, headers: { 'content-type': 'text/html; charset=utf-8' }, body: '' };
2087
+ }
2088
+ if (typeof value === 'string') {
2089
+ return { status: 200, headers: { 'content-type': 'text/html; charset=utf-8' }, body: value };
2090
+ }
2091
+ // A resource returned from a controller: Laravel's Router::prepareResponse
2092
+ // wraps it in a ResourceResponse (the `data` key, the `with` and the
2093
+ // `additional` of the resource - ResourceResponse.php:31-52). The marker
2094
+ // (`__lwResource`) is what identifies it: the kernel's base replaces the
2095
+ // instance's prototype with a property proxy, so `instanceof` is false.
2096
+ if (value && value.__lwResource === true) return resourceResponse(value, rt.request());
2097
+ if (Array.isArray(value)) return rt.json(value);
2098
+ if (typeof value === 'object') {
2099
+ // A model that was just created: Laravel answers 201 (a JsonResponse).
2100
+ if (value.wasRecentlyCreated && typeof value.toJSON === 'function') {
2101
+ return rt.json(value.toJSON(), 201);
2102
+ }
2103
+ // Laravel's Model is Stringable (`__toString()` is `toJson()`), so a
2104
+ // model that is returned is its JSON with the text/html type.
2105
+ if (typeof value.toJSON === 'function') {
2106
+ return {
2107
+ status: 200,
2108
+ headers: { 'content-type': 'text/html; charset=utf-8' },
2109
+ body: phpJsonEncode(value.toJSON()),
2110
+ };
2111
+ }
2112
+ return rt.json(value);
2113
+ }
2114
+ // A number or a boolean: Laravel's Response with the value as the body.
2115
+ return { status: 200, headers: { 'content-type': 'text/html; charset=utf-8' }, body: String(value) };
2116
+ };
2117
+ // The redirect, with Laravel's flash modifiers. The flash lives in the
2118
+ // session (it is read by the *next* request), exactly like Laravel's
2119
+ // RedirectResponse::with()/withInput()/withErrors().
2120
+ rt.redirect = (location, status) => {
2121
+ const res = {
2122
+ status: Number(status) || 302,
2123
+ headers: { location: String(location == null ? '' : location) },
2124
+ body: '',
2125
+ };
2126
+ res.with = (key, value) => {
2127
+ const session = liveSession;
2128
+ if (session) {
2129
+ if (key !== null && typeof key === 'object' && !Array.isArray(key)) {
2130
+ for (const [k, v] of Object.entries(key)) session.flash(k, v);
2131
+ } else {
2132
+ session.flash(key, value === undefined ? true : value);
2133
+ }
2134
+ }
2135
+ return res;
2136
+ };
2137
+ res.withInput = (input) => {
2138
+ const session = liveSession;
2139
+ if (session) {
2140
+ const source =
2141
+ input !== undefined && input !== null
2142
+ ? input
2143
+ : liveRequest && typeof liveRequest.all === 'function'
2144
+ ? liveRequest.all()
2145
+ : {};
2146
+ const clean = Object.assign({}, source);
2147
+ for (const key of DEFAULT_DONT_FLASH) delete clean[key];
2148
+ if (typeof session.flashInput === 'function') session.flashInput(clean);
2149
+ else session.flash('_old_input', clean);
2150
+ }
2151
+ return res;
2152
+ };
2153
+ res.withErrors = (errors) => {
2154
+ const session = liveSession;
2155
+ if (session) {
2156
+ const bag =
2157
+ liveRequest && typeof liveRequest.input === 'function'
2158
+ ? liveRequest.input('_error_bag', 'default')
2159
+ : 'default';
2160
+ session.flash('errors', sessionErrorBag(bag, errors || {}));
2161
+ }
2162
+ return res;
2163
+ };
2164
+ return res;
2165
+ };
2166
+
2167
+ // rt.param: route parameter → PHP method argument coercion (Laravel matches by position;
2168
+ // controllers bind after checking the route segment name).
2169
+ // PHP 8's coercive typing for a SCALAR parameter (a route segment arrives as
2170
+ // a string): a numeric string converts, and anything else is a TypeError -
2171
+ // a 500 - not a silent 0. Probed against PHP 8.3:
2172
+ // int('12') = 12, int('12.5') = 12 (a deprecation, not an error),
2173
+ // int(' 7 ') = 7, int('abc')/int('12abc')/int('') = TypeError,
2174
+ // float('abc') = TypeError, string('abc') = 'abc'.
2175
+ const PHP_NUMERIC_STRING = /^\s*[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?\s*$/;
2176
+ const phpTypeError = (name, type, value) =>
2177
+ new TypeError(
2178
+ 'Argument #1 ($' + name + ') must be of type ' + type + ', string given: the route segment ' +
2179
+ JSON.stringify(value) + ' is not ' + (type === 'int' ? 'an integer' : 'a number') +
2180
+ ' (PHP 8 throws a TypeError here, so Laravel answers 500)'
2181
+ );
2182
+ rt.param = (params, name, type, hasDefault, def, nullable) => {
2183
+ const v = params && typeof params === 'object' ? params[name] : undefined;
2184
+ // A missing segment takes the default (Laravel's own rule: the route would
2185
+ // not match without the segment). A NULL value is different: PHP passes it
2186
+ // to the parameter, so a non-nullable scalar type throws a TypeError
2187
+ // (`f(null)` for `int $n` is "must be of type int, null given") - the
2188
+ // compiled rung used to answer null, a 200 where Laravel answers 500.
2189
+ if (v === undefined) return hasDefault ? def : null;
2190
+ if (v === null) {
2191
+ if (nullable) return null;
2192
+ if (type === 'int' || type === 'float' || type === 'string') {
2193
+ throw new TypeError(
2194
+ 'Argument #1 ($' + name + ') must be of type ' + type + ', null given (PHP 8 throws a ' +
2195
+ 'TypeError here, so Laravel answers 500)'
2196
+ );
2197
+ }
2198
+ return hasDefault ? def : null;
2199
+ }
2200
+ if (type === 'int' || type === 'float') {
2201
+ if (typeof v === 'number') return type === 'int' ? Math.trunc(v) : v;
2202
+ const s = String(v);
2203
+ if (!PHP_NUMERIC_STRING.test(s)) throw phpTypeError(name, type, v);
2204
+ const n = Number(s);
2205
+ return type === 'int' ? Math.trunc(n) : n;
2206
+ }
2207
+ if (type === 'bool') return rt.truthy(v);
2208
+ if (type === 'raw') return v;
2209
+ return String(v);
2210
+ };
2211
+
2212
+ // rt.db: the D1 binding the compiled controller writes through
2213
+ // (Eloquent's model writes need the connection).
2214
+ rt.db = () => {
2215
+ const binding = (opts && opts.db) || null;
2216
+ if (!binding) throw new Error('rt.db: no database binding (env.DB) available');
2217
+ return binding;
2218
+ };
2219
+
2220
+ // rt.hash: the Hash facade (bcrypt). The rounds come from the app config
2221
+ // (hashing.bcrypt.rounds), baked in at build time.
2222
+ const hashRounds = Number((opts && opts.hashRounds) || 12);
2223
+ rt.hash = {
2224
+ make: (value) => hashMod.make(String(value), { rounds: hashRounds }),
2225
+ check: (value, hashed) => hashMod.check(String(value), hashed),
2226
+ needsRehash: (hashed) => hashMod.needsRehash(hashed, { rounds: hashRounds }),
2227
+ isHashed: (value) => hashMod.isHashed(value),
2228
+ };
2229
+
2230
+ // ---- model facades (rt.model) ----
2231
+ const modelsMap = (opts && opts.models && typeof opts.models === 'object') ? opts.models : {};
2232
+ const viewsMap = (opts && opts.views && typeof opts.views === 'object') ? opts.views : {};
2233
+ const db = (opts && opts.db) || null;
2234
+
2235
+ const wrapBuilder = (b) => {
2236
+ if (b && typeof b.firstOrFail !== 'function') {
2237
+ b.firstOrFail = async () => {
2238
+ const r = await b.first();
2239
+ if (r === null || r === undefined) throw statusErr(404, 'not found');
2240
+ return r;
2241
+ };
2242
+ }
2243
+ return b;
2244
+ };
2245
+ const facadeCache = new Map();
2246
+ const makeFacade = (M) => {
2247
+ if (facadeCache.has(M)) return facadeCache.get(M);
2248
+ if (!db) throw new Error('rt.model: no database binding (env.DB) available');
2249
+ // The model timestamps follow the request clock (a frozen clock in the
2250
+ // parity tests), so a write carries the same `created_at` as the PHP side.
2251
+ if (requestClock && typeof M.clock !== 'undefined') M.clock = requestClock;
2252
+ // The app's timezone: the model writes/renders its timestamps in it
2253
+ // (Laravel sets PHP's default from config('app.timezone') and
2254
+ // freshTimestampString()/serializeDate() follow it).
2255
+ if (typeof setModelTimezone === 'function') setModelTimezone(APP_TZ);
2256
+ const name = M.name || 'Model';
2257
+ const facade = {
2258
+ find: (id) => M.find(db, id),
2259
+ findOrFail: async (id) => {
2260
+ const row = await M.find(db, id);
2261
+ if (row === null || row === undefined) throw statusErr(404, name + ' not found');
2262
+ return row;
2263
+ },
2264
+ all: () => M.all(db),
2265
+ where: (...a) => wrapBuilder(M.query(db).where(...a)),
2266
+ whereIn: (...a) => wrapBuilder(M.query(db).whereIn(...a)),
2267
+ // Eloquent::create() — a new model, filled (fillable-guarded) and saved.
2268
+ create: async (attributes) => {
2269
+ const m = new M();
2270
+ if (attributes && typeof attributes === 'object') m.fill(attributes);
2271
+ await m.save(db);
2272
+ return m;
2273
+ },
2274
+ // Eloquent::paginate() — the LengthAwarePaginator (the page comes from
2275
+ // the request, the path from the request's URL).
2276
+ paginate: async (perPage, pageName, page) =>
2277
+ rt.paginate(M.query(db), perPage, pageName, page),
2278
+ };
2279
+ facadeCache.set(M, facade);
2280
+ return facade;
2281
+ };
2282
+ // A DECLARED relation read as a property (`$post->comments`): Eloquent's
2283
+ // Model::getRelationValue() answers the CACHED relation and otherwise queries
2284
+ // the database - the kernel's Model::related() does exactly that (it loads
2285
+ // through the model's own `static relations` and caches the result).
2286
+ rt.modelRelation = async (model, name) => {
2287
+ if (!model || typeof model.related !== 'function') {
2288
+ throw new Error('rt.modelRelation: ' + JSON.stringify(name) + ' needs a model instance');
2289
+ }
2290
+ if (!db) {
2291
+ throw new Error(
2292
+ 'rt.modelRelation: the relation "' +
2293
+ String(name) +
2294
+ '" needs the database binding (env.DB) — the edge cannot load it'
2295
+ );
2296
+ }
2297
+ return model.related(db, name);
2298
+ };
2299
+
2300
+ // The runtime the compiled model ACCESSORS run against (`getXAttribute` is a
2301
+ // compiled `function (rt, value)`; the kernel's Model::get() calls it).
2302
+ setModelRuntime(rt);
2303
+
2304
+ rt.model = (name) => {
2305
+ const key = String(name);
2306
+ const M = modelsMap[key];
2307
+ if (!M) throw new Error('rt.model: unknown model ' + JSON.stringify(key));
2308
+ return makeFacade(M);
2309
+ };
2310
+
2311
+ // ---- rt.paginate: the LengthAwarePaginator (Illuminate\Pagination) ----
2312
+ // Laravel's PaginationState: the page comes from the request's input (a
2313
+ // valid integer >= 1, else 1), the path from the request's URL (without the
2314
+ // query string) and the query string from the request (withQueryString()).
2315
+ const phpFilterInt = (v) => {
2316
+ if (typeof v === 'number') return Number.isInteger(v) ? v : null;
2317
+ if (v === null || v === undefined) return null;
2318
+ const s = String(v).trim();
2319
+ if (!/^[+-]?\d+$/.test(s)) return null;
2320
+ return parseInt(s, 10);
2321
+ };
2322
+ rt.paginate = async (builder, perPage, pageName, page) => {
2323
+ const name = pageName ? String(pageName) : 'page';
2324
+ const size = Number(perPage) || 15;
2325
+ let current = page ? Number(page) : 1;
2326
+ if (!page && liveRequest && typeof liveRequest.input === 'function') {
2327
+ const fromRequest = phpFilterInt(liveRequest.input(name));
2328
+ if (fromRequest !== null && fromRequest >= 1) current = fromRequest;
2329
+ }
2330
+ const res = await builder.paginate(size, current);
2331
+ const path =
2332
+ liveRequest && typeof liveRequest.url === 'function' ? liveRequest.url() : '/';
2333
+ const query =
2334
+ liveRequest && typeof liveRequest.query === 'function' ? liveRequest.query() : {};
2335
+ const paginator = new LengthAwarePaginator(res.data, res.total, res.per_page, res.current_page, {
2336
+ path,
2337
+ pageName: name,
2338
+ });
2339
+ // withQueryString(): the request's query string (Laravel's resolver).
2340
+ paginator.__requestQuery = query && typeof query === 'object' ? { ...query } : {};
2341
+ // links(): the pagination view (the compiled `pagination::tailwind`).
2342
+ const paginationView = configCtx['pagination.view'] || 'pagination::tailwind';
2343
+ paginator.links = () => {
2344
+ const rendered = rt.view(paginationView, {
2345
+ paginator,
2346
+ elements: paginator.elements(),
2347
+ });
2348
+ return rendered && rendered.body != null ? String(rendered.body) : '';
2349
+ };
2350
+ return paginator;
2351
+ };
2352
+
2353
+ // rt.view: render a compiled Blade view with runtime data.
2354
+ rt.view = (name, data, opts) => {
2355
+ const key = String(name);
2356
+ const entry = viewsMap[key];
2357
+ if (!entry || typeof entry.main !== 'function') {
2358
+ throw new Error('view not compiled: ' + JSON.stringify(key) + ' — write a port for this route');
2359
+ }
2360
+ const d = data == null ? {} : data;
2361
+ for (const k of Object.keys(d)) {
2362
+ if (d[k] instanceof Promise) {
2363
+ throw new Error('view data ' + JSON.stringify(k) + ' is an unresolved promise — await the query in a single chain');
2364
+ }
2365
+ }
2366
+ const sub = buildRt(entry.context || {}, {
2367
+ strict: strict,
2368
+ runtime: runtime,
2369
+ data: d,
2370
+ db: db,
2371
+ models: modelsMap,
2372
+ views: viewsMap,
2373
+ origin: origin,
2374
+ routeMap: routeMap,
2375
+ // The app's config (the translations, the pagination view): the view
2376
+ // renders with the same config as the controller.
2377
+ config: configCtx,
2378
+ // The same env: a layout yields the sections of the view that extends it.
2379
+ env: bladeEnv,
2380
+ });
2381
+ entry.main(sub);
2382
+ // Illuminate\View\Engines\PhpEngine::evaluatePath() returns
2383
+ // ltrim(ob_get_clean()) — a view's output never starts with whitespace.
2384
+ return {
2385
+ status: 200,
2386
+ headers: { 'content-type': 'text/html; charset=utf-8' },
2387
+ // The response path trims (Laravel's HTTP response); the MAIL path does
2388
+ // not (Laravel's View::render() returns the raw output) - so the caller
2389
+ // asks for the raw body.
2390
+ body:
2391
+ opts && opts.raw
2392
+ ? String(sub.__output)
2393
+ : String(sub.__output).replace(/^[ \t\n\r\0\x0B]+/, ''),
2394
+ };
2395
+ };
2396
+
2397
+ return rt;
2398
+ }
2399
+ export default buildRt;