@brust/core 2.0.0-alpha.30 → 2.0.0-alpha.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/native/index.d.ts +19 -0
- package/native/index.js +55 -54
- package/package.json +9 -9
- package/src/actions/load.ts +58 -0
- package/src/actions/prefix.ts +20 -0
- package/src/actions-bridge.ts +43 -8
- package/src/build/actions-only.ts +1 -1
- package/src/build/cf-entry.ts +2 -1
- package/src/build/cloudflare.ts +13 -2
- package/src/build/config.ts +55 -2
- package/src/build/index.ts +1 -1
- package/src/build/manifest.ts +6 -0
- package/src/dev/bun.ts +7 -0
- package/src/dev/proxy.ts +20 -0
- package/src/handlers.ts +1 -1
- package/src/native.ts +14 -1
- package/src/run.ts +13 -3
- package/src/worker.ts +8 -1
package/native/index.d.ts
CHANGED
|
@@ -99,6 +99,16 @@ export interface NapiActionsOptions {
|
|
|
99
99
|
maxResponseBytes?: number
|
|
100
100
|
/** `"reject"` (default) or `"allow"` (F87): cross-site mutating action requests. */
|
|
101
101
|
crossSite?: string
|
|
102
|
+
/** F475: `["POST /upload", "PUT /files/*"]`, relative to the prefix: these bodies stream. */
|
|
103
|
+
stream?: Array<string>
|
|
104
|
+
/** F475: the streamed-body ceiling (default 268435456, at most 4294967296). */
|
|
105
|
+
maxStreamBytes?: number
|
|
106
|
+
/** F475: the longest wait for the client's next byte (default 30000). */
|
|
107
|
+
streamIdleMs?: number
|
|
108
|
+
/** F475: the whole streamed call (default 900000). */
|
|
109
|
+
streamTotalMs?: number
|
|
110
|
+
/** F475: streamed bodies at once (default a quarter of the worker slots, at least 1). */
|
|
111
|
+
maxStreams?: number
|
|
102
112
|
}
|
|
103
113
|
|
|
104
114
|
/**
|
|
@@ -163,6 +173,15 @@ export declare function planJobs(slot: number, len: number): string
|
|
|
163
173
|
*/
|
|
164
174
|
export declare function planJobsBuf(slot: number, buf: Uint8Array): string
|
|
165
175
|
|
|
176
|
+
/**
|
|
177
|
+
* F475: the next chunk of the streamed request body of the calling worker's in-flight `fetch`
|
|
178
|
+
* call for `slot`, whose A1 carried `token`. Resolves with the bytes, `null` at the end, or the
|
|
179
|
+
* reason the stream failed (`"max_stream_bytes"`, `"idle"`, `"io"`); never rejects. The stream is
|
|
180
|
+
* looked up here, on the worker's JS thread where `BOUND` is that worker's binding; only the read
|
|
181
|
+
* runs off it.
|
|
182
|
+
*/
|
|
183
|
+
export declare function pullFetchBody(slot: number, token: number): Promise<Uint8Array | string | null>
|
|
184
|
+
|
|
166
185
|
/**
|
|
167
186
|
* F82: hand Rust the response body of the calling worker's in-flight `fetch` call for `slot`
|
|
168
187
|
* (too large for the SAB slot). `false` = declined (over `max_response_bytes`, not in a `fetch`
|
package/native/index.js
CHANGED
|
@@ -89,8 +89,8 @@ function requireNative() {
|
|
|
89
89
|
try {
|
|
90
90
|
const binding = require('@brust/native-android-arm64')
|
|
91
91
|
const bindingPackageVersion = require('@brust/native-android-arm64/package.json').version
|
|
92
|
-
if (bindingPackageVersion !== '2.0.0-alpha.
|
|
93
|
-
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.
|
|
92
|
+
if (bindingPackageVersion !== '2.0.0-alpha.32' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
93
|
+
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.32 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
94
94
|
}
|
|
95
95
|
return binding
|
|
96
96
|
} catch (e) {
|
|
@@ -105,8 +105,8 @@ function requireNative() {
|
|
|
105
105
|
try {
|
|
106
106
|
const binding = require('@brust/native-android-arm-eabi')
|
|
107
107
|
const bindingPackageVersion = require('@brust/native-android-arm-eabi/package.json').version
|
|
108
|
-
if (bindingPackageVersion !== '2.0.0-alpha.
|
|
109
|
-
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.
|
|
108
|
+
if (bindingPackageVersion !== '2.0.0-alpha.32' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
109
|
+
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.32 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
110
110
|
}
|
|
111
111
|
return binding
|
|
112
112
|
} catch (e) {
|
|
@@ -126,8 +126,8 @@ function requireNative() {
|
|
|
126
126
|
try {
|
|
127
127
|
const binding = require('@brust/native-win32-x64-gnu')
|
|
128
128
|
const bindingPackageVersion = require('@brust/native-win32-x64-gnu/package.json').version
|
|
129
|
-
if (bindingPackageVersion !== '2.0.0-alpha.
|
|
130
|
-
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.
|
|
129
|
+
if (bindingPackageVersion !== '2.0.0-alpha.32' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
130
|
+
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.32 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
131
131
|
}
|
|
132
132
|
return binding
|
|
133
133
|
} catch (e) {
|
|
@@ -142,8 +142,8 @@ function requireNative() {
|
|
|
142
142
|
try {
|
|
143
143
|
const binding = require('@brust/native-win32-x64-msvc')
|
|
144
144
|
const bindingPackageVersion = require('@brust/native-win32-x64-msvc/package.json').version
|
|
145
|
-
if (bindingPackageVersion !== '2.0.0-alpha.
|
|
146
|
-
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.
|
|
145
|
+
if (bindingPackageVersion !== '2.0.0-alpha.32' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
146
|
+
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.32 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
147
147
|
}
|
|
148
148
|
return binding
|
|
149
149
|
} catch (e) {
|
|
@@ -159,8 +159,8 @@ function requireNative() {
|
|
|
159
159
|
try {
|
|
160
160
|
const binding = require('@brust/native-win32-ia32-msvc')
|
|
161
161
|
const bindingPackageVersion = require('@brust/native-win32-ia32-msvc/package.json').version
|
|
162
|
-
if (bindingPackageVersion !== '2.0.0-alpha.
|
|
163
|
-
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.
|
|
162
|
+
if (bindingPackageVersion !== '2.0.0-alpha.32' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
163
|
+
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.32 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
164
164
|
}
|
|
165
165
|
return binding
|
|
166
166
|
} catch (e) {
|
|
@@ -175,8 +175,8 @@ function requireNative() {
|
|
|
175
175
|
try {
|
|
176
176
|
const binding = require('@brust/native-win32-arm64-msvc')
|
|
177
177
|
const bindingPackageVersion = require('@brust/native-win32-arm64-msvc/package.json').version
|
|
178
|
-
if (bindingPackageVersion !== '2.0.0-alpha.
|
|
179
|
-
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.
|
|
178
|
+
if (bindingPackageVersion !== '2.0.0-alpha.32' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
179
|
+
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.32 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
180
180
|
}
|
|
181
181
|
return binding
|
|
182
182
|
} catch (e) {
|
|
@@ -194,8 +194,8 @@ function requireNative() {
|
|
|
194
194
|
try {
|
|
195
195
|
const binding = require('@brust/native-darwin-universal')
|
|
196
196
|
const bindingPackageVersion = require('@brust/native-darwin-universal/package.json').version
|
|
197
|
-
if (bindingPackageVersion !== '2.0.0-alpha.
|
|
198
|
-
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.
|
|
197
|
+
if (bindingPackageVersion !== '2.0.0-alpha.32' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
198
|
+
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.32 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
199
199
|
}
|
|
200
200
|
return binding
|
|
201
201
|
} catch (e) {
|
|
@@ -210,8 +210,8 @@ function requireNative() {
|
|
|
210
210
|
try {
|
|
211
211
|
const binding = require('@brust/native-darwin-x64')
|
|
212
212
|
const bindingPackageVersion = require('@brust/native-darwin-x64/package.json').version
|
|
213
|
-
if (bindingPackageVersion !== '2.0.0-alpha.
|
|
214
|
-
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.
|
|
213
|
+
if (bindingPackageVersion !== '2.0.0-alpha.32' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
214
|
+
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.32 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
215
215
|
}
|
|
216
216
|
return binding
|
|
217
217
|
} catch (e) {
|
|
@@ -226,8 +226,8 @@ function requireNative() {
|
|
|
226
226
|
try {
|
|
227
227
|
const binding = require('@brust/native-darwin-arm64')
|
|
228
228
|
const bindingPackageVersion = require('@brust/native-darwin-arm64/package.json').version
|
|
229
|
-
if (bindingPackageVersion !== '2.0.0-alpha.
|
|
230
|
-
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.
|
|
229
|
+
if (bindingPackageVersion !== '2.0.0-alpha.32' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
230
|
+
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.32 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
231
231
|
}
|
|
232
232
|
return binding
|
|
233
233
|
} catch (e) {
|
|
@@ -246,8 +246,8 @@ function requireNative() {
|
|
|
246
246
|
try {
|
|
247
247
|
const binding = require('@brust/native-freebsd-x64')
|
|
248
248
|
const bindingPackageVersion = require('@brust/native-freebsd-x64/package.json').version
|
|
249
|
-
if (bindingPackageVersion !== '2.0.0-alpha.
|
|
250
|
-
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.
|
|
249
|
+
if (bindingPackageVersion !== '2.0.0-alpha.32' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
250
|
+
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.32 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
251
251
|
}
|
|
252
252
|
return binding
|
|
253
253
|
} catch (e) {
|
|
@@ -262,8 +262,8 @@ function requireNative() {
|
|
|
262
262
|
try {
|
|
263
263
|
const binding = require('@brust/native-freebsd-arm64')
|
|
264
264
|
const bindingPackageVersion = require('@brust/native-freebsd-arm64/package.json').version
|
|
265
|
-
if (bindingPackageVersion !== '2.0.0-alpha.
|
|
266
|
-
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.
|
|
265
|
+
if (bindingPackageVersion !== '2.0.0-alpha.32' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
266
|
+
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.32 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
267
267
|
}
|
|
268
268
|
return binding
|
|
269
269
|
} catch (e) {
|
|
@@ -283,8 +283,8 @@ function requireNative() {
|
|
|
283
283
|
try {
|
|
284
284
|
const binding = require('@brust/native-linux-x64-musl')
|
|
285
285
|
const bindingPackageVersion = require('@brust/native-linux-x64-musl/package.json').version
|
|
286
|
-
if (bindingPackageVersion !== '2.0.0-alpha.
|
|
287
|
-
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.
|
|
286
|
+
if (bindingPackageVersion !== '2.0.0-alpha.32' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
287
|
+
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.32 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
288
288
|
}
|
|
289
289
|
return binding
|
|
290
290
|
} catch (e) {
|
|
@@ -299,8 +299,8 @@ function requireNative() {
|
|
|
299
299
|
try {
|
|
300
300
|
const binding = require('@brust/native-linux-x64-gnu')
|
|
301
301
|
const bindingPackageVersion = require('@brust/native-linux-x64-gnu/package.json').version
|
|
302
|
-
if (bindingPackageVersion !== '2.0.0-alpha.
|
|
303
|
-
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.
|
|
302
|
+
if (bindingPackageVersion !== '2.0.0-alpha.32' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
303
|
+
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.32 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
304
304
|
}
|
|
305
305
|
return binding
|
|
306
306
|
} catch (e) {
|
|
@@ -317,8 +317,8 @@ function requireNative() {
|
|
|
317
317
|
try {
|
|
318
318
|
const binding = require('@brust/native-linux-arm64-musl')
|
|
319
319
|
const bindingPackageVersion = require('@brust/native-linux-arm64-musl/package.json').version
|
|
320
|
-
if (bindingPackageVersion !== '2.0.0-alpha.
|
|
321
|
-
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.
|
|
320
|
+
if (bindingPackageVersion !== '2.0.0-alpha.32' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
321
|
+
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.32 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
322
322
|
}
|
|
323
323
|
return binding
|
|
324
324
|
} catch (e) {
|
|
@@ -333,8 +333,8 @@ function requireNative() {
|
|
|
333
333
|
try {
|
|
334
334
|
const binding = require('@brust/native-linux-arm64-gnu')
|
|
335
335
|
const bindingPackageVersion = require('@brust/native-linux-arm64-gnu/package.json').version
|
|
336
|
-
if (bindingPackageVersion !== '2.0.0-alpha.
|
|
337
|
-
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.
|
|
336
|
+
if (bindingPackageVersion !== '2.0.0-alpha.32' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
337
|
+
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.32 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
338
338
|
}
|
|
339
339
|
return binding
|
|
340
340
|
} catch (e) {
|
|
@@ -351,8 +351,8 @@ function requireNative() {
|
|
|
351
351
|
try {
|
|
352
352
|
const binding = require('@brust/native-linux-arm-musleabihf')
|
|
353
353
|
const bindingPackageVersion = require('@brust/native-linux-arm-musleabihf/package.json').version
|
|
354
|
-
if (bindingPackageVersion !== '2.0.0-alpha.
|
|
355
|
-
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.
|
|
354
|
+
if (bindingPackageVersion !== '2.0.0-alpha.32' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
355
|
+
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.32 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
356
356
|
}
|
|
357
357
|
return binding
|
|
358
358
|
} catch (e) {
|
|
@@ -367,8 +367,8 @@ function requireNative() {
|
|
|
367
367
|
try {
|
|
368
368
|
const binding = require('@brust/native-linux-arm-gnueabihf')
|
|
369
369
|
const bindingPackageVersion = require('@brust/native-linux-arm-gnueabihf/package.json').version
|
|
370
|
-
if (bindingPackageVersion !== '2.0.0-alpha.
|
|
371
|
-
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.
|
|
370
|
+
if (bindingPackageVersion !== '2.0.0-alpha.32' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
371
|
+
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.32 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
372
372
|
}
|
|
373
373
|
return binding
|
|
374
374
|
} catch (e) {
|
|
@@ -385,8 +385,8 @@ function requireNative() {
|
|
|
385
385
|
try {
|
|
386
386
|
const binding = require('@brust/native-linux-loong64-musl')
|
|
387
387
|
const bindingPackageVersion = require('@brust/native-linux-loong64-musl/package.json').version
|
|
388
|
-
if (bindingPackageVersion !== '2.0.0-alpha.
|
|
389
|
-
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.
|
|
388
|
+
if (bindingPackageVersion !== '2.0.0-alpha.32' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
389
|
+
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.32 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
390
390
|
}
|
|
391
391
|
return binding
|
|
392
392
|
} catch (e) {
|
|
@@ -401,8 +401,8 @@ function requireNative() {
|
|
|
401
401
|
try {
|
|
402
402
|
const binding = require('@brust/native-linux-loong64-gnu')
|
|
403
403
|
const bindingPackageVersion = require('@brust/native-linux-loong64-gnu/package.json').version
|
|
404
|
-
if (bindingPackageVersion !== '2.0.0-alpha.
|
|
405
|
-
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.
|
|
404
|
+
if (bindingPackageVersion !== '2.0.0-alpha.32' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
405
|
+
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.32 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
406
406
|
}
|
|
407
407
|
return binding
|
|
408
408
|
} catch (e) {
|
|
@@ -419,8 +419,8 @@ function requireNative() {
|
|
|
419
419
|
try {
|
|
420
420
|
const binding = require('@brust/native-linux-riscv64-musl')
|
|
421
421
|
const bindingPackageVersion = require('@brust/native-linux-riscv64-musl/package.json').version
|
|
422
|
-
if (bindingPackageVersion !== '2.0.0-alpha.
|
|
423
|
-
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.
|
|
422
|
+
if (bindingPackageVersion !== '2.0.0-alpha.32' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
423
|
+
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.32 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
424
424
|
}
|
|
425
425
|
return binding
|
|
426
426
|
} catch (e) {
|
|
@@ -435,8 +435,8 @@ function requireNative() {
|
|
|
435
435
|
try {
|
|
436
436
|
const binding = require('@brust/native-linux-riscv64-gnu')
|
|
437
437
|
const bindingPackageVersion = require('@brust/native-linux-riscv64-gnu/package.json').version
|
|
438
|
-
if (bindingPackageVersion !== '2.0.0-alpha.
|
|
439
|
-
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.
|
|
438
|
+
if (bindingPackageVersion !== '2.0.0-alpha.32' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
439
|
+
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.32 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
440
440
|
}
|
|
441
441
|
return binding
|
|
442
442
|
} catch (e) {
|
|
@@ -452,8 +452,8 @@ function requireNative() {
|
|
|
452
452
|
try {
|
|
453
453
|
const binding = require('@brust/native-linux-ppc64-gnu')
|
|
454
454
|
const bindingPackageVersion = require('@brust/native-linux-ppc64-gnu/package.json').version
|
|
455
|
-
if (bindingPackageVersion !== '2.0.0-alpha.
|
|
456
|
-
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.
|
|
455
|
+
if (bindingPackageVersion !== '2.0.0-alpha.32' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
456
|
+
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.32 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
457
457
|
}
|
|
458
458
|
return binding
|
|
459
459
|
} catch (e) {
|
|
@@ -468,8 +468,8 @@ function requireNative() {
|
|
|
468
468
|
try {
|
|
469
469
|
const binding = require('@brust/native-linux-s390x-gnu')
|
|
470
470
|
const bindingPackageVersion = require('@brust/native-linux-s390x-gnu/package.json').version
|
|
471
|
-
if (bindingPackageVersion !== '2.0.0-alpha.
|
|
472
|
-
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.
|
|
471
|
+
if (bindingPackageVersion !== '2.0.0-alpha.32' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
472
|
+
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.32 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
473
473
|
}
|
|
474
474
|
return binding
|
|
475
475
|
} catch (e) {
|
|
@@ -488,8 +488,8 @@ function requireNative() {
|
|
|
488
488
|
try {
|
|
489
489
|
const binding = require('@brust/native-openharmony-arm64')
|
|
490
490
|
const bindingPackageVersion = require('@brust/native-openharmony-arm64/package.json').version
|
|
491
|
-
if (bindingPackageVersion !== '2.0.0-alpha.
|
|
492
|
-
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.
|
|
491
|
+
if (bindingPackageVersion !== '2.0.0-alpha.32' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
492
|
+
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.32 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
493
493
|
}
|
|
494
494
|
return binding
|
|
495
495
|
} catch (e) {
|
|
@@ -504,8 +504,8 @@ function requireNative() {
|
|
|
504
504
|
try {
|
|
505
505
|
const binding = require('@brust/native-openharmony-x64')
|
|
506
506
|
const bindingPackageVersion = require('@brust/native-openharmony-x64/package.json').version
|
|
507
|
-
if (bindingPackageVersion !== '2.0.0-alpha.
|
|
508
|
-
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.
|
|
507
|
+
if (bindingPackageVersion !== '2.0.0-alpha.32' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
508
|
+
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.32 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
509
509
|
}
|
|
510
510
|
return binding
|
|
511
511
|
} catch (e) {
|
|
@@ -520,8 +520,8 @@ function requireNative() {
|
|
|
520
520
|
try {
|
|
521
521
|
const binding = require('@brust/native-openharmony-arm')
|
|
522
522
|
const bindingPackageVersion = require('@brust/native-openharmony-arm/package.json').version
|
|
523
|
-
if (bindingPackageVersion !== '2.0.0-alpha.
|
|
524
|
-
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.
|
|
523
|
+
if (bindingPackageVersion !== '2.0.0-alpha.32' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
524
|
+
throw new Error(`Native binding package version mismatch, expected 2.0.0-alpha.32 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
525
525
|
}
|
|
526
526
|
return binding
|
|
527
527
|
} catch (e) {
|
|
@@ -661,8 +661,8 @@ if (!nativeBinding || forceWasi) {
|
|
|
661
661
|
if (!candidateFailed) {
|
|
662
662
|
if (process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
663
663
|
const bindingPackageVersion = require('@brust/native-wasm32-wasi/package.json').version
|
|
664
|
-
if (bindingPackageVersion !== '2.0.0-alpha.
|
|
665
|
-
throw new Error(`WASI binding package version mismatch, expected 2.0.0-alpha.
|
|
664
|
+
if (bindingPackageVersion !== '2.0.0-alpha.32') {
|
|
665
|
+
throw new Error(`WASI binding package version mismatch, expected 2.0.0-alpha.32 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
666
666
|
}
|
|
667
667
|
}
|
|
668
668
|
wasiBinding = require('@brust/native-wasm32-wasi')
|
|
@@ -789,6 +789,7 @@ module.exports.compileTree = nativeBinding.compileTree
|
|
|
789
789
|
module.exports.localAddr = nativeBinding.localAddr
|
|
790
790
|
module.exports.planJobs = nativeBinding.planJobs
|
|
791
791
|
module.exports.planJobsBuf = nativeBinding.planJobsBuf
|
|
792
|
+
module.exports.pullFetchBody = nativeBinding.pullFetchBody
|
|
792
793
|
module.exports.putFetchReply = nativeBinding.putFetchReply
|
|
793
794
|
module.exports.registerWorker = nativeBinding.registerWorker
|
|
794
795
|
module.exports.scanLoadWrites = nativeBinding.scanLoadWrites
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brust/core",
|
|
3
|
-
"version": "2.0.0-alpha.
|
|
3
|
+
"version": "2.0.0-alpha.32",
|
|
4
4
|
"description": "brust v2 — React components compiled to Rust-rendered templates",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://brust.dev",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
]
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
|
-
"@brust/runtime-dom": "2.0.0-alpha.
|
|
59
|
+
"@brust/runtime-dom": "2.0.0-alpha.32",
|
|
60
60
|
"tsconfig-paths": "4.2.0"
|
|
61
61
|
},
|
|
62
62
|
"peerDependencies": {
|
|
@@ -100,12 +100,12 @@
|
|
|
100
100
|
"access": "public"
|
|
101
101
|
},
|
|
102
102
|
"optionalDependencies": {
|
|
103
|
-
"@brust/native-darwin-x64": "2.0.0-alpha.
|
|
104
|
-
"@brust/native-darwin-arm64": "2.0.0-alpha.
|
|
105
|
-
"@brust/native-linux-x64-gnu": "2.0.0-alpha.
|
|
106
|
-
"@brust/native-linux-arm64-gnu": "2.0.0-alpha.
|
|
107
|
-
"@brust/native-linux-x64-musl": "2.0.0-alpha.
|
|
108
|
-
"@brust/native-linux-arm64-musl": "2.0.0-alpha.
|
|
109
|
-
"@brust/core-cf": "2.0.0-alpha.
|
|
103
|
+
"@brust/native-darwin-x64": "2.0.0-alpha.32",
|
|
104
|
+
"@brust/native-darwin-arm64": "2.0.0-alpha.32",
|
|
105
|
+
"@brust/native-linux-x64-gnu": "2.0.0-alpha.32",
|
|
106
|
+
"@brust/native-linux-arm64-gnu": "2.0.0-alpha.32",
|
|
107
|
+
"@brust/native-linux-x64-musl": "2.0.0-alpha.32",
|
|
108
|
+
"@brust/native-linux-arm64-musl": "2.0.0-alpha.32",
|
|
109
|
+
"@brust/core-cf": "2.0.0-alpha.32"
|
|
110
110
|
}
|
|
111
111
|
}
|
package/src/actions/load.ts
CHANGED
|
@@ -43,3 +43,61 @@ export function shadowedGetActions(app: AnyElysia, patterns: string[], prefix: s
|
|
|
43
43
|
}
|
|
44
44
|
return out
|
|
45
45
|
}
|
|
46
|
+
|
|
47
|
+
/** F475: the route parses nothing (`{ parse: 'none' }`): Elysia 2 keeps `'none'`, Elysia 1 kept `[{ fn: 'none' }]`. */
|
|
48
|
+
function parsesNone(r: { hooks?: unknown }): boolean {
|
|
49
|
+
const p = (r.hooks as { parse?: unknown } | undefined)?.parse
|
|
50
|
+
const none = (x: unknown) => x === 'none' || (x as { fn?: unknown } | null)?.fn === 'none'
|
|
51
|
+
return Array.isArray(p) ? p.some(none) : none(p)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Same route path: segments equal, a parameter (`:x`, `{x}`) matches a parameter, `*` matches `*`. */
|
|
55
|
+
function samePath(a: string, b: string): boolean {
|
|
56
|
+
const x = a.split('/')
|
|
57
|
+
const y = b.split('/')
|
|
58
|
+
const param = (s: string) => /^(:|\{)/.test(s)
|
|
59
|
+
return x.length === y.length && x.every((s, i) => s === y[i] || (param(s) && param(y[i]!)))
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const STREAM_METHODS = new Set(['POST', 'PUT', 'PATCH'])
|
|
63
|
+
/** A route answers `method`: its own, or every method (Elysia 2's `.all()` registers `*`; `ALL` too). */
|
|
64
|
+
const answers = (route: string, method: string) => route === method || route === 'ALL' || route === '*'
|
|
65
|
+
|
|
66
|
+
/** Could one request path match both `listed` (a stream entry, prefix included) and `route` (an Elysia path)? A
|
|
67
|
+
* parameter on either side matches any one segment; a final `*` on either side matches one or more. */
|
|
68
|
+
function overlaps(listed: string, route: string): boolean {
|
|
69
|
+
const a = listed.split('/')
|
|
70
|
+
const b = route.split('/')
|
|
71
|
+
const param = (s: string) => /^(:|\{)/.test(s)
|
|
72
|
+
for (let i = 0; ; i++) {
|
|
73
|
+
const x = a[i]
|
|
74
|
+
const y = b[i]
|
|
75
|
+
if (x === undefined || y === undefined) return x === y
|
|
76
|
+
if ((x === '*' && i === a.length - 1) || (y === '*' && i === b.length - 1)) return x !== '' && y !== ''
|
|
77
|
+
if (x !== y && !param(x) && !param(y)) return false
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* F475: `[actions] stream` against the app. `missing`: listed entries with no `{ parse: 'none' }` route of that method and
|
|
83
|
+
* path. `parsed`: routes that parse their body although a listed entry can match their path (a static route under a
|
|
84
|
+
* `:param` or `*` entry). Both fail boot: Rust would stream those bodies up to `max_stream_bytes` into a route that parses
|
|
85
|
+
* them whole, past `max_body_bytes`. `unlisted`: `{ parse: 'none' }` POST/PUT/PATCH routes not on the list (their bodies
|
|
86
|
+
* stay buffered, capped at `max_body_bytes`).
|
|
87
|
+
*/
|
|
88
|
+
export function streamRouteCheck(app: AnyElysia, entries: string[], prefix: string): { missing: string[]; parsed: string[]; unlisted: string[] } {
|
|
89
|
+
const full = (p: string) => (prefix === '/' ? p : p === '/' ? prefix : prefix + p)
|
|
90
|
+
const listed = entries.map((e) => {
|
|
91
|
+
const [m, p] = e.trim().split(/ +/)
|
|
92
|
+
return { entry: e, method: m!.toUpperCase(), path: full(p!) }
|
|
93
|
+
})
|
|
94
|
+
const routes = app.routes.map((r) => ({ method: r.method.toUpperCase(), path: r.path, none: parsesNone(r) }))
|
|
95
|
+
const missing = listed.filter((l) => !routes.some((r) => r.none && answers(r.method, l.method) && samePath(r.path, l.path))).map((l) => l.entry)
|
|
96
|
+
const parsed = routes
|
|
97
|
+
.filter((r) => !r.none && listed.some((l) => answers(r.method, l.method) && overlaps(l.path, r.path)))
|
|
98
|
+
.map((r) => `${r.method} ${r.path}`)
|
|
99
|
+
const unlisted = routes
|
|
100
|
+
.filter((r) => r.none && (STREAM_METHODS.has(r.method) || answers(r.method, '')) && !listed.some((l) => answers(r.method, l.method) && samePath(r.path, l.path)))
|
|
101
|
+
.map((r) => `${r.method} ${r.path}`)
|
|
102
|
+
return { missing, parsed, unlisted }
|
|
103
|
+
}
|
package/src/actions/prefix.ts
CHANGED
|
@@ -3,6 +3,11 @@ export const DEFAULT_ACTION_PREFIX = '/_brust/action'
|
|
|
3
3
|
export const DEFAULT_MAX_BODY_BYTES = 1_048_576
|
|
4
4
|
export const DEFAULT_MAX_RESPONSE_BYTES = 8_388_608
|
|
5
5
|
export const MAX_ACTION_BYTES = 67_108_864
|
|
6
|
+
/** F475: `max_stream_bytes` default (256 MiB) and hard max (4 GiB); `stream_idle_ms` / `stream_total_ms` defaults. */
|
|
7
|
+
export const DEFAULT_MAX_STREAM_BYTES = 268_435_456
|
|
8
|
+
export const MAX_STREAM_BYTES = 4_294_967_296
|
|
9
|
+
export const DEFAULT_STREAM_IDLE_MS = 30_000
|
|
10
|
+
export const DEFAULT_STREAM_TOTAL_MS = 900_000
|
|
6
11
|
const SEG = /^[A-Za-z0-9._~-]+$/
|
|
7
12
|
|
|
8
13
|
/** Why `p` cannot be the actions prefix, or null. Segment-safe like the server's match. `root`: an actions-only
|
|
@@ -43,3 +48,18 @@ export function baseUrlError(v: unknown): string | null {
|
|
|
43
48
|
/** L16 R16.2: the default `RequestInit.credentials` of the browser client. */
|
|
44
49
|
export const ACTION_CREDENTIALS = ['same-origin', 'include', 'omit'] as const
|
|
45
50
|
export type ActionCredentials = (typeof ACTION_CREDENTIALS)[number]
|
|
51
|
+
|
|
52
|
+
/** F475: why `e` is not a `[actions] stream` entry (`"POST /upload"`, path relative to the prefix, `:name` one segment,
|
|
53
|
+
* a final `*` the rest), or null. Mirrors Rust `StreamRoute::parse`. */
|
|
54
|
+
export function streamEntryError(e: unknown): string | null {
|
|
55
|
+
if (typeof e !== 'string') return `must be a string "<METHOD> <path>" (got ${JSON.stringify(e)})`
|
|
56
|
+
const m = /^\s*(\S+) +(\S+)\s*$/.exec(e)
|
|
57
|
+
if (!m) return `must be "<METHOD> <path>" (got ${JSON.stringify(e)})`
|
|
58
|
+
if (!['POST', 'PUT', 'PATCH'].includes(m[1]!.toUpperCase())) return `method must be POST, PUT or PATCH (got ${JSON.stringify(e)})`
|
|
59
|
+
const path = m[2]!
|
|
60
|
+
const segs = path.split('/').slice(1)
|
|
61
|
+
const badStar = segs.some((s, i) => s.includes('*') && (s !== '*' || i !== segs.length - 1))
|
|
62
|
+
if (!path.startsWith('/') || (path !== '/' && segs.some((s) => s === '')) || badStar)
|
|
63
|
+
return `path must start with '/', have no empty segment, and use '*' only as the last segment (got ${JSON.stringify(e)})`
|
|
64
|
+
return null
|
|
65
|
+
}
|
package/src/actions-bridge.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// The worker side of the `fetch` call kind (design E4/E5, contracts A1/A2). Rust forwards the actions prefix subtree
|
|
2
2
|
// here; the app registered with `setActionsApp` (Elysia 2 via brust() — m3f-actions-elysia) answers a web Request
|
|
3
3
|
// with a web Response, framed into this worker's SAB slot. No Elysia import: the app is anything with `fetch`.
|
|
4
|
-
import { putFetchReply, takeFetchBody } from './native'
|
|
4
|
+
import { pullFetchBody, putFetchReply, takeFetchBody } from './native'
|
|
5
5
|
import type { LoaderReq } from './routes'
|
|
6
6
|
import { runInStoreContext } from './store/server-context'
|
|
7
7
|
|
|
@@ -22,8 +22,10 @@ export interface FetchRequest {
|
|
|
22
22
|
method: string
|
|
23
23
|
url: string
|
|
24
24
|
headers: [string, string][]
|
|
25
|
-
/** `{heap: n}` (F82): more than 262144 bytes (`ACTION_INLINE_BODY_BYTES`, independent of the slot size), fetched with `takeFetchBody(slot)`.
|
|
26
|
-
|
|
25
|
+
/** `{heap: n}` (F82): more than 262144 bytes (`ACTION_INLINE_BODY_BYTES`, independent of the slot size), fetched with `takeFetchBody(slot)`.
|
|
26
|
+
* `{stream}` (F475): a `[actions] stream` route's body, pulled chunk by chunk with `pullFetchBody(slot, token)`; `length` is the
|
|
27
|
+
* declared Content-Length, if any. */
|
|
28
|
+
body: { text: string } | { b64: string } | { heap: number } | { stream: { length: number | null; token: number } } | null
|
|
27
29
|
req: LoaderReq
|
|
28
30
|
}
|
|
29
31
|
/** A2 meta: `status`, `headers`, and (F82) `heap` — the byte length of a response body that crossed by
|
|
@@ -39,8 +41,36 @@ export interface FetchMeta {
|
|
|
39
41
|
export interface HeapIo {
|
|
40
42
|
takeBody(slot: number): Uint8Array | null
|
|
41
43
|
putReply(slot: number, body: Uint8Array): boolean
|
|
44
|
+
/** F475: the next chunk of the slot's streamed body (`pullFetchBody`); only `fetch` calls with A1 `{stream}` use it. */
|
|
45
|
+
pullBody?(slot: number, token: number): Promise<Uint8Array | string | null>
|
|
46
|
+
}
|
|
47
|
+
const nativeHeapIo: HeapIo = {
|
|
48
|
+
takeBody: (slot) => takeFetchBody(slot),
|
|
49
|
+
putReply: (slot, body) => putFetchReply(slot, body),
|
|
50
|
+
pullBody: (slot, token) => pullFetchBody(slot, token),
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** F475: what a failed pull means to the app; Rust answers 413 / 408 / 400 whatever the app then returns. */
|
|
54
|
+
const STREAM_FAILED: Record<string, string> = {
|
|
55
|
+
max_stream_bytes: 'brust: the request body exceeds max_stream_bytes',
|
|
56
|
+
idle: 'brust: the request body stalled longer than stream_idle_ms',
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** F475: the request body as the app reads it: each read pulls one chunk from Rust, and nothing is pulled before a read
|
|
60
|
+
* (`highWaterMark: 0`), so the client is read only as fast as the app consumes. */
|
|
61
|
+
export function pulledBody(pull: () => Promise<Uint8Array | string | null>): ReadableStream<Uint8Array> {
|
|
62
|
+
return new ReadableStream<Uint8Array>(
|
|
63
|
+
{
|
|
64
|
+
async pull(c) {
|
|
65
|
+
const r = await pull()
|
|
66
|
+
if (r === null) c.close()
|
|
67
|
+
else if (typeof r === 'string') c.error(new TypeError(STREAM_FAILED[r] ?? 'brust: the request body broke off or ended short'))
|
|
68
|
+
else c.enqueue(r)
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
{ highWaterMark: 0 },
|
|
72
|
+
)
|
|
42
73
|
}
|
|
43
|
-
const nativeHeapIo: HeapIo = { takeBody: (slot) => takeFetchBody(slot), putReply: (slot, body) => putFetchReply(slot, body) }
|
|
44
74
|
|
|
45
75
|
let maxResponseBytes = 8_388_608
|
|
46
76
|
/** `startWorker` sets the manifest's `actions.maxResponseBytes` (R5). Default 8388608. */
|
|
@@ -68,13 +98,13 @@ const TEXT_PLAIN: [string, string][] = [['content-type', 'text/plain; charset=ut
|
|
|
68
98
|
|
|
69
99
|
/** A1 → web Request (R8 origin: `http://<host>` when the host pair is sane, else `localhost`). Bytes, never a
|
|
70
100
|
* string body, so no content-type is invented. GET/HEAD never carry a body (R4; `new Request` forbids one). */
|
|
71
|
-
export function toRequest(r: FetchRequest, heapBody?: Uint8Array): Request {
|
|
101
|
+
export function toRequest(r: FetchRequest, heapBody?: Uint8Array | ReadableStream<Uint8Array>): Request {
|
|
72
102
|
const host = r.headers.find(([k]) => k === 'host')?.[1]
|
|
73
103
|
const origin = `http://${host && HOST.test(host) ? host : 'localhost'}`
|
|
74
104
|
const bodyless = r.method === 'GET' || r.method === 'HEAD'
|
|
75
105
|
const b = r.body
|
|
76
|
-
const body = bodyless || !b ? undefined : 'heap' in b ? (heapBody as BodyInit | undefined) : 'text' in b ? enc.encode(b.text) : Buffer.from(b.b64, 'base64')
|
|
77
|
-
return new Request(origin + r.url, { method: r.method, headers: r.headers, body })
|
|
106
|
+
const body = bodyless || !b ? undefined : 'heap' in b || 'stream' in b ? (heapBody as BodyInit | undefined) : 'text' in b ? enc.encode(b.text) : Buffer.from(b.b64, 'base64')
|
|
107
|
+
return new Request(origin + r.url, { method: r.method, headers: r.headers, body, ...(body instanceof ReadableStream ? { duplex: 'half' } : {}) } as RequestInit)
|
|
78
108
|
}
|
|
79
109
|
|
|
80
110
|
/** A2 writer at `slot`'s sub-region `[slot*sub, slot*sub+sub)`, `sub = floor(view.byteLength / slots)`:
|
|
@@ -146,11 +176,16 @@ export async function runFetch(requestJson: string, view: Uint8Array, slot: numb
|
|
|
146
176
|
if (!a) return fail(view, slot, slots, 'brust: actions are not configured in this worker')
|
|
147
177
|
try {
|
|
148
178
|
const fr = JSON.parse(requestJson) as FetchRequest
|
|
149
|
-
let heapBody: Uint8Array | undefined
|
|
179
|
+
let heapBody: Uint8Array | ReadableStream<Uint8Array> | undefined
|
|
150
180
|
if (fr.body && 'heap' in fr.body) {
|
|
151
181
|
const got = io.takeBody(slot)
|
|
152
182
|
if (!got || got.length !== fr.body.heap) throw new Error(`heap request body ${got ? `${got.length} != ${fr.body.heap}` : 'missing'} bytes (F82)`)
|
|
153
183
|
heapBody = got
|
|
184
|
+
} else if (fr.body && 'stream' in fr.body) {
|
|
185
|
+
const { pullBody } = io
|
|
186
|
+
if (!pullBody) throw new Error('streamed request body without a pull channel (F475)')
|
|
187
|
+
const { token } = fr.body.stream
|
|
188
|
+
heapBody = pulledBody(() => pullBody(slot, token))
|
|
154
189
|
}
|
|
155
190
|
const req = toRequest(fr, heapBody)
|
|
156
191
|
// R1/E5: one store scope around the handler AND the body read (a lazy body may still touch stores).
|
|
@@ -45,7 +45,7 @@ export async function runActionsOnly(o: {
|
|
|
45
45
|
components: {},
|
|
46
46
|
assets: { runtime: '', features: {} },
|
|
47
47
|
jobs_module: 'jobs.js',
|
|
48
|
-
actions: { entry: await buildActions(tmp, entry), prefix: actions.prefix, maxBodyBytes: actions.maxBodyBytes, maxResponseBytes: actions.maxResponseBytes, crossSite: actions.crossSite },
|
|
48
|
+
actions: { entry: await buildActions(tmp, entry), prefix: actions.prefix, maxBodyBytes: actions.maxBodyBytes, maxResponseBytes: actions.maxResponseBytes, crossSite: actions.crossSite, ...actions.stream },
|
|
49
49
|
actions_only: true,
|
|
50
50
|
}
|
|
51
51
|
mkdirSync(join(tmp, 'jinja'), { recursive: true })
|
package/src/build/cf-entry.ts
CHANGED
|
@@ -8,7 +8,8 @@ export interface CfConfigJson {
|
|
|
8
8
|
stats: boolean
|
|
9
9
|
edgeCache: 'off'
|
|
10
10
|
cors?: unknown
|
|
11
|
-
|
|
11
|
+
/** F475: `stream` + `maxStreamBytes` only when the app lists streamed routes; the idle/total timeouts and `maxStreams` are the platform's on Workers. */
|
|
12
|
+
actions?: { prefix: string; maxBodyBytes: number; maxResponseBytes: number; crossSite: 'reject' | 'allow'; stream?: string[]; maxStreamBytes?: number }
|
|
12
13
|
/** The cache adapter (optional under abi 1, cache adapters R13). */
|
|
13
14
|
cache?: { adapter: string; getTimeoutMs: number; maxBodyBytes: number; namespace: string; buildId: string; epochIntervalMs: number }
|
|
14
15
|
}
|
package/src/build/cloudflare.ts
CHANGED
|
@@ -76,7 +76,7 @@ export interface CfBuildCtx {
|
|
|
76
76
|
/** the FINAL dist dir (`out` is a tmp sibling that is renamed to it): wrangler paths are relative to `<dist>/cf` */
|
|
77
77
|
dist: string
|
|
78
78
|
compiled: Map<string, Compiled>
|
|
79
|
-
manifest: { components: Record<string, { template: string }>; actions?: { prefix: string; maxBodyBytes: number; maxResponseBytes?: number; crossSite?: 'reject' | 'allow' } | null; cache?: CacheRecord }
|
|
79
|
+
manifest: { components: Record<string, { template: string }>; actions?: { prefix: string; maxBodyBytes: number; maxResponseBytes?: number; crossSite?: 'reject' | 'allow'; stream?: string[]; maxStreamBytes?: number } | null; cache?: CacheRecord }
|
|
80
80
|
sourceOf: SourceOf
|
|
81
81
|
actionsEntry: string | null
|
|
82
82
|
/** L16 R16.3: an actions-only Worker: no pages (no routes entry, no jobs, no templates), no Static Assets. */
|
|
@@ -215,6 +215,17 @@ if (bad.length) { console.error(bad.join('\\n')); process.exit(1) }
|
|
|
215
215
|
return { preload, smoke }
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
+
/** The `actions` block of the Worker's CONFIG. F475: the `stream` list and its ceiling only when the app lists streamed routes; the idle and total timeouts and `maxStreams` are the platform's on Workers, so they are not sent. */
|
|
219
|
+
export function cfActions(a: NonNullable<CfBuildCtx['manifest']['actions']>): NonNullable<CfConfigJson['actions']> {
|
|
220
|
+
return {
|
|
221
|
+
prefix: a.prefix,
|
|
222
|
+
maxBodyBytes: a.maxBodyBytes,
|
|
223
|
+
maxResponseBytes: a.maxResponseBytes ?? 8_388_608,
|
|
224
|
+
crossSite: a.crossSite ?? 'reject',
|
|
225
|
+
...(a.stream?.length ? { stream: a.stream, ...(a.maxStreamBytes !== undefined ? { maxStreamBytes: a.maxStreamBytes } : {}) } : {}),
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
218
229
|
export async function buildCloudflare(out: string, ctx: CfBuildCtx, p: CfPrepared): Promise<string[]> {
|
|
219
230
|
const cfDir = join(out, 'cf')
|
|
220
231
|
const stage = join(cfDir, '.stage')
|
|
@@ -250,7 +261,7 @@ export async function buildCloudflare(out: string, ctx: CfBuildCtx, p: CfPrepare
|
|
|
250
261
|
stats: p.cfg.stats,
|
|
251
262
|
edgeCache: p.cfg.edgeCache,
|
|
252
263
|
...(cors ? { cors } : {}),
|
|
253
|
-
...(a ? { actions:
|
|
264
|
+
...(a ? { actions: cfActions(a) } : {}),
|
|
254
265
|
...(cache ? { cache: { adapter: cache.adapter, getTimeoutMs: cache.getTimeoutMs, maxBodyBytes: cache.maxBodyBytes, namespace: cache.namespace, buildId: cache.buildId, epochIntervalMs: cache.epochIntervalMs } } : {}),
|
|
255
266
|
}
|
|
256
267
|
const entryPath = join(stage, 'brust-app.entry.js')
|
package/src/build/config.ts
CHANGED
|
@@ -6,7 +6,21 @@
|
|
|
6
6
|
// such an import in client code, and every browser bundle refuses to include it.
|
|
7
7
|
import { statSync } from 'node:fs'
|
|
8
8
|
import { isAbsolute, join, relative, resolve, sep } from 'node:path'
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
ACTION_CREDENTIALS,
|
|
11
|
+
type ActionCredentials,
|
|
12
|
+
DEFAULT_ACTION_PREFIX,
|
|
13
|
+
DEFAULT_MAX_BODY_BYTES,
|
|
14
|
+
DEFAULT_MAX_RESPONSE_BYTES,
|
|
15
|
+
DEFAULT_MAX_STREAM_BYTES,
|
|
16
|
+
DEFAULT_STREAM_IDLE_MS,
|
|
17
|
+
DEFAULT_STREAM_TOTAL_MS,
|
|
18
|
+
MAX_ACTION_BYTES,
|
|
19
|
+
MAX_STREAM_BYTES,
|
|
20
|
+
baseUrlError,
|
|
21
|
+
prefixError,
|
|
22
|
+
streamEntryError,
|
|
23
|
+
} from '../actions/prefix'
|
|
10
24
|
import { ADAPTER_NAMES, BUILTINS, installHint } from '../cache-adapter/load'
|
|
11
25
|
import { BuildError } from './errors'
|
|
12
26
|
|
|
@@ -15,6 +29,8 @@ import { BuildError } from './errors'
|
|
|
15
29
|
// entry = "actions.ts" prefix = "/_brust/action" max_body_bytes = 1048576 max_response_bytes = 8388608 cross_site = "reject" | "allow" (F87)
|
|
16
30
|
// base_url = "https://api.example.com" credentials = "same-origin" | "include" | "omit" (L16: the browser client's target; runtime settings, validated here too)
|
|
17
31
|
// `prefix = "/"` is accepted only for an actions-only build (L16 R16.4).
|
|
32
|
+
// F475: stream = ["POST /upload", "PUT /files/*"] (relative to the prefix; each a `{ parse: 'none' }` route) streams those bodies
|
|
33
|
+
// max_stream_bytes = 268435456 (<= 4294967296) stream_idle_ms = 30000 stream_total_ms = 900000 max_streams = <workers x slots / 4>
|
|
18
34
|
// `ACTION_INLINE_BODY_BYTES` (Rust): the inline/heap threshold of an action body, independent of the response slot size (`slotBytes`).
|
|
19
35
|
const MIN_RESPONSE_BYTES = 262_144
|
|
20
36
|
|
|
@@ -29,6 +45,17 @@ export interface ActionsBuildConfig {
|
|
|
29
45
|
credentials: ActionCredentials
|
|
30
46
|
/** `entry` was written in brust.toml (a missing file is then an error, never a page-only build). */
|
|
31
47
|
entryExplicit: boolean
|
|
48
|
+
/** F475: the streamed routes and their bounds; absent = no `stream` list (or an empty one). */
|
|
49
|
+
stream?: ActionsStreamConfig
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** F475 `[actions] stream` and its bounds, as the manifest carries them. */
|
|
53
|
+
export interface ActionsStreamConfig {
|
|
54
|
+
stream: string[]
|
|
55
|
+
maxStreamBytes: number
|
|
56
|
+
streamIdleMs: number
|
|
57
|
+
streamTotalMs: number
|
|
58
|
+
maxStreams?: number
|
|
32
59
|
}
|
|
33
60
|
|
|
34
61
|
// [cloudflare] (M4-CF; only read by `--target cloudflare`, validated always so errors surface early):
|
|
@@ -293,7 +320,33 @@ function parseActions(file: string, raw: unknown, actionsOnly: boolean): Actions
|
|
|
293
320
|
throw new BuildError('config', `${file}: actions.credentials must be one of ${ACTION_CREDENTIALS.join(', ')} (got ${JSON.stringify(t.credentials)})`)
|
|
294
321
|
credentials = t.credentials as ActionCredentials
|
|
295
322
|
}
|
|
296
|
-
|
|
323
|
+
const stream = parseStream(file, t)
|
|
324
|
+
return { entry, prefix, maxBodyBytes, maxResponseBytes, crossSite, baseUrl, credentials, entryExplicit: t.entry !== undefined, ...(stream ? { stream } : {}) }
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** F475: `stream` + its bounds; the bounds are validated even without a list, and dropped with it. */
|
|
328
|
+
function parseStream(file: string, t: Record<string, unknown>): ActionsStreamConfig | undefined {
|
|
329
|
+
const int = (key: string, min: number, max: number, dflt: number): number => {
|
|
330
|
+
const n = t[key]
|
|
331
|
+
if (n === undefined) return dflt
|
|
332
|
+
if (typeof n !== 'number' || !Number.isInteger(n) || n < min || n > max) throw new BuildError('config', `${file}: actions.${key} must be an integer in ${min}..${max} (got ${String(n)})`)
|
|
333
|
+
return n
|
|
334
|
+
}
|
|
335
|
+
const maxStreamBytes = int('max_stream_bytes', 1, MAX_STREAM_BYTES, DEFAULT_MAX_STREAM_BYTES)
|
|
336
|
+
const streamIdleMs = int('stream_idle_ms', 1, U32_MAX, DEFAULT_STREAM_IDLE_MS)
|
|
337
|
+
const streamTotalMs = int('stream_total_ms', 1, U32_MAX, DEFAULT_STREAM_TOTAL_MS)
|
|
338
|
+
const maxStreams = t.max_streams === undefined ? undefined : int('max_streams', 1, U32_MAX, 1)
|
|
339
|
+
if (t.stream === undefined) return undefined
|
|
340
|
+
if (!Array.isArray(t.stream)) throw new BuildError('config', `${file}: actions.stream must be an array of "<METHOD> <path>" strings`)
|
|
341
|
+
const stream: string[] = []
|
|
342
|
+
for (const e of t.stream) {
|
|
343
|
+
const why = streamEntryError(e)
|
|
344
|
+
if (why) throw new BuildError('config', `${file}: actions.stream entry ${why}`)
|
|
345
|
+
const [m, p] = (e as string).trim().split(/ +/)
|
|
346
|
+
stream.push(`${m!.toUpperCase()} ${p}`)
|
|
347
|
+
}
|
|
348
|
+
if (stream.length === 0) return undefined
|
|
349
|
+
return { stream, maxStreamBytes, streamIdleMs, streamTotalMs, ...(maxStreams !== undefined ? { maxStreams } : {}) }
|
|
297
350
|
}
|
|
298
351
|
|
|
299
352
|
const DEFAULT_ENTRY = 'actions.ts'
|
package/src/build/index.ts
CHANGED
|
@@ -337,7 +337,7 @@ async function buildInto(
|
|
|
337
337
|
await buildJobs(out, compiled, ctx.sourceOf, appRoot, storeModules(manifest), css.maps, routeRoots, reachedJobs(manifest), writers)
|
|
338
338
|
manifest.actions =
|
|
339
339
|
o.actions && o.actionsEntry
|
|
340
|
-
? { entry: await buildActions(out, o.actionsEntry, css.maps), prefix: o.actions.prefix, maxBodyBytes: o.actions.maxBodyBytes, maxResponseBytes: o.actions.maxResponseBytes, crossSite: o.actions.crossSite }
|
|
340
|
+
? { entry: await buildActions(out, o.actionsEntry, css.maps), prefix: o.actions.prefix, maxBodyBytes: o.actions.maxBodyBytes, maxResponseBytes: o.actions.maxResponseBytes, crossSite: o.actions.crossSite, ...o.actions.stream }
|
|
341
341
|
: null
|
|
342
342
|
if (o.actionsRemote !== null) manifest.actions_remote = { prefix: o.actionsRemote }
|
|
343
343
|
if (o.cache) manifest.cache = await cacheRecord(out, manifest, o.cache, o.cacheModule, css.maps)
|
package/src/build/manifest.ts
CHANGED
|
@@ -69,6 +69,12 @@ export interface ActionsRecord {
|
|
|
69
69
|
maxBodyBytes: number
|
|
70
70
|
maxResponseBytes: number
|
|
71
71
|
crossSite: 'reject' | 'allow'
|
|
72
|
+
/** F475: `[actions] stream` and its bounds; all absent without a `stream` list. */
|
|
73
|
+
stream?: string[]
|
|
74
|
+
maxStreamBytes?: number
|
|
75
|
+
streamIdleMs?: number
|
|
76
|
+
streamTotalMs?: number
|
|
77
|
+
maxStreams?: number
|
|
72
78
|
}
|
|
73
79
|
|
|
74
80
|
/** The manifest format the Rust loader accepts (`MANIFEST_VERSION` in brust-core `manifest.rs`): the one place TS names it. */
|
package/src/dev/bun.ts
CHANGED
|
@@ -150,6 +150,13 @@ export async function runBunDev(o: BunDevOptions): Promise<number> {
|
|
|
150
150
|
diag: () => diag,
|
|
151
151
|
upstream: () => active?.port ?? null,
|
|
152
152
|
meta: devMeta,
|
|
153
|
+
actionsPrefix: () => {
|
|
154
|
+
try {
|
|
155
|
+
return active ? ((JSON.parse(readFileSync(join(active.dir, 'manifest.json'), 'utf8')) as { actions?: { prefix?: string } | null }).actions?.prefix ?? null) : null
|
|
156
|
+
} catch {
|
|
157
|
+
return null
|
|
158
|
+
}
|
|
159
|
+
},
|
|
153
160
|
onRequest(r) {
|
|
154
161
|
tui.log(`${r.method} ${r.path} ${r.status} ${r.ms}ms${r.cache ? ` ${r.cache}` : ''}`)
|
|
155
162
|
if (!r.path.startsWith('/_brust/')) {
|
package/src/dev/proxy.ts
CHANGED
|
@@ -16,6 +16,19 @@ export interface DevView {
|
|
|
16
16
|
onRequest(r: { method: string; path: string; status: number; ms: number; cache: string | null; trace: unknown | null }): void
|
|
17
17
|
/** The active build's `dev.json` (L14), or null before the first build. */
|
|
18
18
|
meta(): unknown | null
|
|
19
|
+
/** F475: the active build's actions prefix, or null (no actions / before the first build). */
|
|
20
|
+
actionsPrefix(): string | null
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** F475: the largest `max_stream_bytes` (4 GiB), what the proxy admits: Bun's own default for `maxRequestBodySize` is 128 MiB,
|
|
24
|
+
* which would refuse a streamed upload the child itself admits. The child enforces the real ceilings (`max_body_bytes`,
|
|
25
|
+
* `max_stream_bytes`), as in production. */
|
|
26
|
+
export const PROXY_MAX_BODY_BYTES = 2 ** 32
|
|
27
|
+
|
|
28
|
+
/** Is `pathname` the actions prefix or under it? (`/` = everything.) */
|
|
29
|
+
export function underPrefix(pathname: string, prefix: string): boolean {
|
|
30
|
+
if (prefix === '/') return true
|
|
31
|
+
return pathname === prefix || pathname.startsWith(`${prefix}/`)
|
|
19
32
|
}
|
|
20
33
|
|
|
21
34
|
/** One entry of the dev trace list (`/_brust/dev/traces`, newest last, at most [`MAX_TRACES`]). */
|
|
@@ -191,8 +204,15 @@ export function startProxy(o: { hostname: string; port: number; allowedHosts?: s
|
|
|
191
204
|
const server = Bun.serve<undefined>({
|
|
192
205
|
hostname: o.hostname,
|
|
193
206
|
port: o.port,
|
|
207
|
+
maxRequestBodySize: PROXY_MAX_BODY_BYTES,
|
|
194
208
|
async fetch(req, srv) {
|
|
195
209
|
const url = new URL(req.url)
|
|
210
|
+
// F475: a body-carrying call under the actions prefix may be a streamed upload that pauses or runs for minutes; Bun's
|
|
211
|
+
// 10 s idle timeout would cut it. The child owns the real bounds (`stream_idle_ms`, `stream_total_ms`).
|
|
212
|
+
if (req.method !== 'GET' && req.method !== 'HEAD' && req.body !== null) {
|
|
213
|
+
const prefix = view.actionsPrefix()
|
|
214
|
+
if (prefix !== null && underPrefix(url.pathname, prefix)) srv.timeout(req, 0)
|
|
215
|
+
}
|
|
196
216
|
// DNS rebinding: a page on evil.example whose name resolves to 127.0.0.1 would otherwise read the app and the
|
|
197
217
|
// source paths in build errors. Only the dev server's own names are answered.
|
|
198
218
|
if (!allowed.has(hostName(req.headers.get('host') ?? ''))) return new Response('brust dev: unexpected Host header (set BRUST_DEV_ALLOWED_HOSTS to allow it)', { status: 421 })
|
package/src/handlers.ts
CHANGED
|
@@ -72,7 +72,7 @@ export interface ManifestLike {
|
|
|
72
72
|
>
|
|
73
73
|
jobs_module?: string
|
|
74
74
|
/** `brust build` (M3-F A3): the actions entry, relative to dist. Absent/null = no actions. */
|
|
75
|
-
actions?: { entry: string; prefix: string; maxBodyBytes: number; maxResponseBytes?: number } | null
|
|
75
|
+
actions?: { entry: string; prefix: string; maxBodyBytes: number; maxResponseBytes?: number; stream?: string[] } | null
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
export function toJson(out: unknown): string {
|
package/src/native.ts
CHANGED
|
@@ -64,7 +64,7 @@ export interface ActionsClientOptions {
|
|
|
64
64
|
credentials?: 'same-origin' | 'include' | 'omit'
|
|
65
65
|
}
|
|
66
66
|
/** napi `NapiActionsOptions`; defaults `/_brust/action`, 1048576, 8388608. */
|
|
67
|
-
export interface ActionsOptions {
|
|
67
|
+
export interface ActionsOptions extends StreamOptions {
|
|
68
68
|
prefix?: string
|
|
69
69
|
maxBodyBytes?: number
|
|
70
70
|
/** F82: response body cap (bytes). Default 8388608. */
|
|
@@ -72,6 +72,15 @@ export interface ActionsOptions {
|
|
|
72
72
|
/** F87: cross-site mutating requests. Default `reject`. */
|
|
73
73
|
crossSite?: 'reject' | 'allow'
|
|
74
74
|
}
|
|
75
|
+
/** F475: the streamed action bodies (napi `NapiActionsOptions`): `["POST /upload"]` relative to the prefix, and the bounds
|
|
76
|
+
* (defaults 268435456 bytes, 30000 ms idle, 900000 ms in all, a quarter of the worker slots at once). */
|
|
77
|
+
export interface StreamOptions {
|
|
78
|
+
stream?: string[]
|
|
79
|
+
maxStreamBytes?: number
|
|
80
|
+
streamIdleMs?: number
|
|
81
|
+
streamTotalMs?: number
|
|
82
|
+
maxStreams?: number
|
|
83
|
+
}
|
|
75
84
|
export type WorkerFn = (kind: 'loader' | 'jobs' | 'fetch' | 'cache', requestJson: string, slot: number) => Promise<number>
|
|
76
85
|
export interface NativeBinding {
|
|
77
86
|
startServer(opts: StartOptions): void
|
|
@@ -89,6 +98,9 @@ export interface NativeBinding {
|
|
|
89
98
|
takeFetchBody(slot: number): Uint8Array | null
|
|
90
99
|
/** F82 — hand Rust a response body too large for the slot; false = declined. Never throws. */
|
|
91
100
|
putFetchReply(slot: number, body: Uint8Array): boolean
|
|
101
|
+
/** F475 — the next chunk of the in-flight `fetch` call's streamed body (A1 `{stream: {token}}`): bytes, `null` at the end,
|
|
102
|
+
* or why it failed (`max_stream_bytes` | `idle` | `io`). Never rejects. */
|
|
103
|
+
pullFetchBody(slot: number, token: number): Promise<Uint8Array | string | null>
|
|
92
104
|
/** cache adapter R10 — a foreign broadcast event: tier 1 + the floor at `seq`; never re-published. */
|
|
93
105
|
cacheReplay(args: InvalidateArgs, seq: number | null): InvalidateResult
|
|
94
106
|
/** cache adapter R9 — bump `stats.l2[name]`; false = unknown name / no adapter / no server. Never throws. */
|
|
@@ -126,6 +138,7 @@ export const planJobs = (slot: number, len: number): string => native().planJobs
|
|
|
126
138
|
export const planJobsBuf = (slot: number, buf: Uint8Array): string => native().planJobsBuf(slot, buf)
|
|
127
139
|
export const takeFetchBody = (slot: number): Uint8Array | null => native().takeFetchBody(slot)
|
|
128
140
|
export const putFetchReply = (slot: number, body: Uint8Array): boolean => native().putFetchReply(slot, body)
|
|
141
|
+
export const pullFetchBody = (slot: number, token: number): Promise<Uint8Array | string | null> => native().pullFetchBody(slot, token)
|
|
129
142
|
export const cacheReplay = (args: InvalidateArgs, seq: number | null): InvalidateResult => native().cacheReplay(args, seq)
|
|
130
143
|
export const cacheL2Count = (name: string): boolean => native().cacheL2Count(name)
|
|
131
144
|
export const cacheClear = (): number => native().cacheClear()
|
package/src/run.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import { existsSync, readFileSync } from 'node:fs'
|
|
12
12
|
import { join, resolve } from 'node:path'
|
|
13
13
|
import { type BrustConfig, type ConfigOverrides, loadConfig } from './config'
|
|
14
|
-
import { beginDrain, localAddr, type StartOptions, startServer, untilReady } from './native'
|
|
14
|
+
import { beginDrain, localAddr, type StartOptions, type StreamOptions, startServer, untilReady } from './native'
|
|
15
15
|
import { DEFAULT_MAX_LOADER_RESPONSE_BYTES } from './worker'
|
|
16
16
|
|
|
17
17
|
export interface RunOptions {
|
|
@@ -27,7 +27,7 @@ export interface RunOptions {
|
|
|
27
27
|
|
|
28
28
|
/** The manifest fields `run` needs (build/manifest.ts owns the full shape). */
|
|
29
29
|
export interface RunManifest {
|
|
30
|
-
actions?: { entry: string; prefix: string; maxBodyBytes: number; maxResponseBytes?: number; crossSite?: 'reject' | 'allow' } | null
|
|
30
|
+
actions?: { entry: string; prefix: string; maxBodyBytes: number; maxResponseBytes?: number; crossSite?: 'reject' | 'allow' } & StreamKeys | null
|
|
31
31
|
/** L16 R16.3: `brust build --actions-only`: no page routes and no routes entry. */
|
|
32
32
|
actions_only?: boolean
|
|
33
33
|
/** L16 R16.4b: a page-only build; the actions app is deployed elsewhere (`[actions] base_url`). */
|
|
@@ -38,6 +38,16 @@ export interface RunManifest {
|
|
|
38
38
|
needs_workers?: boolean
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
/** F475: the `[actions] stream` keys of `manifest.actions`, passed to napi as they are. */
|
|
42
|
+
type StreamKeys = Pick<StreamOptions, 'stream' | 'maxStreamBytes' | 'streamIdleMs' | 'streamTotalMs' | 'maxStreams'>
|
|
43
|
+
|
|
44
|
+
/** F475: the stream keys `a` carries; none without a `stream` list, so an app without one starts as before. */
|
|
45
|
+
function streamOptions(a: StreamKeys): StreamKeys {
|
|
46
|
+
if (!a.stream?.length) return {}
|
|
47
|
+
const { stream, maxStreamBytes, streamIdleMs, streamTotalMs, maxStreams } = a
|
|
48
|
+
return { stream, maxStreamBytes, streamIdleMs, streamTotalMs, ...(maxStreams !== undefined ? { maxStreams } : {}) }
|
|
49
|
+
}
|
|
50
|
+
|
|
41
51
|
/** F165 (R40.1): the pool size `run` spawns: none only when the build said no request needs one. */
|
|
42
52
|
export function workerCount(cfg: BrustConfig, m: RunManifest): number {
|
|
43
53
|
return m.needs_workers === false ? 0 : cfg.workers
|
|
@@ -54,7 +64,7 @@ export function startOptions(cfg: BrustConfig, distDir: string, m: RunManifest,
|
|
|
54
64
|
...(cfg.maxLoaderResponseBytes !== undefined && cfg.maxLoaderResponseBytes !== DEFAULT_MAX_LOADER_RESPONSE_BYTES ? { maxLoaderResponseBytes: cfg.maxLoaderResponseBytes } : {}),
|
|
55
65
|
workerThreads: cfg.ioThreads,
|
|
56
66
|
generator: `brust/${version}`,
|
|
57
|
-
...(m.actions ? { actions: { prefix: m.actions.prefix, maxBodyBytes: m.actions.maxBodyBytes, ...(m.actions.maxResponseBytes !== undefined ? { maxResponseBytes: m.actions.maxResponseBytes } : {}), ...(m.actions.crossSite ? { crossSite: m.actions.crossSite } : {}) } } : {}),
|
|
67
|
+
...(m.actions ? { actions: { prefix: m.actions.prefix, maxBodyBytes: m.actions.maxBodyBytes, ...(m.actions.maxResponseBytes !== undefined ? { maxResponseBytes: m.actions.maxResponseBytes } : {}), ...(m.actions.crossSite ? { crossSite: m.actions.crossSite } : {}), ...streamOptions(m.actions) } } : {}),
|
|
58
68
|
...(actionsClient(cfg, m) ? { actionsClient: actionsClient(cfg, m) } : {}),
|
|
59
69
|
...(cfg.cors ? { cors: cfg.cors } : {}),
|
|
60
70
|
...(devToken ? { devToken } : {}),
|
package/src/worker.ts
CHANGED
|
@@ -199,12 +199,19 @@ export async function startWorker(opts: StartWorkerOptions = {}): Promise<number
|
|
|
199
199
|
const jobs = (jobsMod.default ?? jobsMod) as JobsModule
|
|
200
200
|
if (manifest.actions) {
|
|
201
201
|
// Lazy: elysia is an optional peer, so an app without [actions] must boot without it installed (R7).
|
|
202
|
-
const { loadActionsApp, shadowedGetActions } = await import('./actions/load')
|
|
202
|
+
const { loadActionsApp, shadowedGetActions, streamRouteCheck } = await import('./actions/load')
|
|
203
203
|
const actionsApp = await loadActionsApp(join(dist, manifest.actions.entry), manifest.actions.prefix)
|
|
204
204
|
setActionsApp(actionsApp)
|
|
205
205
|
// F470: a GET under the prefix that a route there matches is the route's; say which GET actions that hides (once)
|
|
206
206
|
const shadowed = env.BRUST_WORKER_ID && env.BRUST_WORKER_ID !== '0' ? [] : shadowedGetActions(actionsApp, flattenRoutes(routes).leaves.map((l) => l.pattern), manifest.actions.prefix)
|
|
207
207
|
if (shadowed.length) console.warn(`[brust] warning: a route under the actions prefix answers GET there, so these GET actions are never reached: ${shadowed.join(', ')}`)
|
|
208
|
+
// F475: a listed route must parse nothing (the app reads the stream itself); an unlisted parse-none route stays buffered.
|
|
209
|
+
const { missing, parsed, unlisted } = streamRouteCheck(actionsApp, manifest.actions.stream ?? [], manifest.actions.prefix)
|
|
210
|
+
if (missing.length) throw new Error(`[actions] stream lists ${missing.join(', ')}, but no { parse: 'none' } action route of that method and path exists`)
|
|
211
|
+
if (parsed.length)
|
|
212
|
+
throw new Error(`[actions] stream covers ${parsed.join(', ')}, which parse their body; give them { parse: 'none' } or narrow the stream entry`)
|
|
213
|
+
if (unlisted.length && (!env.BRUST_WORKER_ID || env.BRUST_WORKER_ID === '0'))
|
|
214
|
+
console.warn(`[brust] warning: these { parse: 'none' } actions are not in [actions] stream, so their bodies are buffered, capped at max_body_bytes: ${unlisted.join(', ')}`)
|
|
208
215
|
setActionsLimits({ maxResponseBytes: manifest.actions.maxResponseBytes ?? DEFAULT_MAX_RESPONSE_BYTES })
|
|
209
216
|
}
|
|
210
217
|
loaderCap = Number.parseInt(env.BRUST_MAX_LOADER_RESPONSE_BYTES ?? '', 10) || DEFAULT_MAX_LOADER_RESPONSE_BYTES
|