Skip to content

Commit a49af84

Browse files
committed
feat(docs): document worker queue limits and subsystem events
- Add maxQueueDepth and maxQueueWaitMs worker-pool options - Add worker, session, and csrf lifecycle event reference entries - Update taskTimeoutMs default to 5000 and note fault events - Update type definitions for the new options and events
1 parent b587cc3 commit a49af84

11 files changed

Lines changed: 297 additions & 45 deletions

File tree

docs/.vitepress/deserve-types.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,10 @@ export interface WebSocketOptions {
459459

460460
/** Worker pool creation options. */
461461
export interface WorkerPoolOptions {
462+
/** Maximum pending tasks before fast-rejecting */
463+
readonly maxQueueDepth?: number
464+
/** Maximum projected queue wait in ms */
465+
readonly maxQueueWaitMs?: number
462466
/** Number of workers in pool */
463467
readonly poolSize?: number
464468
/** URL to worker script module */
@@ -571,8 +575,12 @@ export interface RouterOptions {
571575
readonly trustProxy?: TrustProxyConfig
572576
/** Directory path for template views */
573577
readonly viewsDir?: string
574-
/** Maximum loop iterations allowed */
578+
/** Maximum loop iterations per #each block */
575579
readonly maxIterations?: number
580+
/** Maximum #each body executions per render */
581+
readonly maxRenderIterations?: number
582+
/** Maximum total output characters per render */
583+
readonly maxOutputSize?: number
576584
/** Worker pool configuration options */
577585
readonly worker?: WorkerPoolOptions
578586
}
@@ -633,6 +641,18 @@ export type EventBase =
633641
error?: Error
634642
}
635643
>
644+
| LifecycleEvent<'worker:timeout', { workerIndex: number; timeoutMs: number; error: Error }>
645+
| LifecycleEvent<'worker:crash', { workerIndex: number; error: Error }>
646+
| LifecycleEvent<'worker:respawn', { workerIndex: number }>
647+
| LifecycleEvent<
648+
'worker:rejected',
649+
{ reason: 'queue-depth' | 'queue-wait'; queueDepth: number; maxQueueDepth: number }
650+
>
651+
| LifecycleEvent<
652+
'session:invalid',
653+
{ cookieName: string; reason: 'tampered' | 'expired' | 'malformed' }
654+
>
655+
| LifecycleEvent<'csrf:rule-error', { rule: 'origin' | 'secFetchSite'; error: Error }>
636656

637657
/** Discriminant value of a lifecycle event. */
638658
export type EventKind = EventBase['kind']

docs/core-concepts/worker-pool.md

Lines changed: 54 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -76,10 +76,21 @@ import type { Context, WorkerRunHandle } from '@neabyte/deserve'
7676
export async function GET(ctx: Context): Promise<Response> {
7777
const worker = ctx.getState<WorkerRunHandle>('worker' as never)
7878
if (!worker) {
79-
return ctx.send.json({ error: 'Worker not enabled' }, { status: 503 })
79+
return ctx.send.json(
80+
{
81+
error: 'Worker not enabled'
82+
},
83+
{
84+
status: 503
85+
}
86+
)
8087
}
81-
const result = await worker.run<{ done: boolean; value: number }>({ iterations: 50_000 })
82-
return ctx.send.json({ value: result?.value })
88+
const result = await worker.run<{ done: boolean; value: number }>({
89+
iterations: 50_000
90+
})
91+
return ctx.send.json({
92+
value: result?.value
93+
})
8394
}
8495
```
8596

@@ -105,7 +116,7 @@ worker: {
105116

106117
### `taskTimeoutMs`
107118

108-
Per-task timeout in milliseconds. Default is **30000**. A task that runs longer rejects with a timeout error and the worker is respawned.
119+
Per-task timeout in milliseconds. Default is **5000**. A task that runs longer rejects with a timeout error, the slot is reclaimed, and the worker is respawned. The reclaim surfaces as a [`worker:timeout`](/middleware/observability/events#workers) event followed by [`worker:respawn`](/middleware/observability/events#workers).
109120

110121
```typescript
111122
worker: {
@@ -114,6 +125,31 @@ worker: {
114125
}
115126
```
116127

128+
### `maxQueueDepth`
129+
130+
Maximum accepted-but-unsettled tasks the pool holds before turning new work away. Default is the worker count times **8**, so a pool of 4 holds up to 32. Once the ceiling is hit a new dispatch is refused immediately rather than queued, which keeps a flood of work from piling up without bound:
131+
132+
```typescript
133+
worker: {
134+
scriptURL: workerScriptUrl,
135+
poolSize: 4,
136+
maxQueueDepth: 64
137+
}
138+
```
139+
140+
### `maxQueueWaitMs`
141+
142+
Maximum projected wait, measured as the chosen slot's pending count times `taskTimeoutMs`, before a dispatch is refused. Default is **2000**. A task that would otherwise sit behind a long backlog is turned away fast instead of waiting:
143+
144+
```typescript
145+
worker: {
146+
scriptURL: workerScriptUrl,
147+
maxQueueWaitMs: 5_000
148+
}
149+
```
150+
151+
A refused dispatch rejects right away and surfaces as a [`worker:rejected`](/middleware/observability/events#workers) event, with `reason` saying whether `maxQueueDepth` or `maxQueueWaitMs` tripped it.
152+
117153
## Complete Example (Inline Worker)
118154

119155
Using an inline worker script with `Blob` and `createObjectURL`:
@@ -127,13 +163,21 @@ self.onmessage = (e) => {
127163
const n = Math.max(0, Number(data.iterations) || 50000)
128164
let value = 0
129165
for (let i = 0; i < n; i++) value += Math.sqrt(i)
130-
self.postMessage({ done: true, value })
166+
self.postMessage({
167+
done: true,
168+
value
169+
})
131170
}
132171
export {}
133172
`
134173

135174
const workerScriptUrl = URL.createObjectURL(
136-
new Blob([workerCode], { type: 'application/javascript' })
175+
new Blob(
176+
[workerCode],
177+
{
178+
type: 'application/javascript'
179+
}
180+
)
137181
)
138182

139183
const router = new Router({
@@ -151,10 +195,11 @@ await router.serve(8000)
151195

152196
- **No pool:** A router created without `worker` leaves `ctx.getState('worker' as never)` undefined. Return 503 or a clear message when the route requires a worker.
153197
- **Worker error:** When the worker calls `postMessage({ error: true, message: '...' })`, `worker.run()` rejects with an `Error` carrying that message. Without a message, the error reads `Worker returned an error with no message`.
154-
- **Worker crash:** When the worker throws or crashes, `run()` rejects with `Worker task failed before responding`.
155-
- **Task timeout:** When a task runs past `taskTimeoutMs` (default 30000), `run()` rejects with `Worker task exceeded <ms>ms timeout`.
198+
- **Worker crash:** When the worker throws or crashes, `run()` rejects with `Worker task failed before responding`, and the slot recovers on its own.
199+
- **Task timeout:** When a task runs past `taskTimeoutMs` (default 5000), `run()` rejects with `Worker task exceeded <ms>ms timeout`.
200+
- **Refused under load:** When the pool is at `maxQueueDepth` or the projected wait passes `maxQueueWaitMs`, `run()` rejects with a queue-full or slot-busy error before the task ever starts.
156201

157-
Catch a rejected task and forward it to the [centralized error handler](/error-handling/object-details):
202+
Every one of these faults also streams through the observability bus as a [worker event](/middleware/observability/events#workers), so a stall, crash, recovery, or refusal is visible without touching the request path. Catch a rejected task and forward it to the [centralized error handler](/error-handling/object-details):
158203

159204
```typescript
160205
try {

docs/id/core-concepts/worker-pool.md

Lines changed: 55 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ self.postMessage({
6767

6868
### 3. Pakai di Rute
6969

70-
Handle worker tinggal di framework state, jadi `ctx.getState` menjangkaunya dengan tipe `WorkerRunHandle`. Router yang dibuat tanpa `worker` membiarkan handle undefined, yang merupakan momen untuk mengembalikan 503:
70+
Handle worker tinggal di framework state, jadi `ctx.getState` menjangkaunya dengan tipe `WorkerRunHandle`. Router yang dibuat tanpa `worker` membiarkan handle undefined, dan di situlah saatnya mengembalikan 503:
7171

7272
```typescript twoslash
7373
// routes/heavy.ts
@@ -76,10 +76,21 @@ import type { Context, WorkerRunHandle } from '@neabyte/deserve'
7676
export async function GET(ctx: Context): Promise<Response> {
7777
const worker = ctx.getState<WorkerRunHandle>('worker' as never)
7878
if (!worker) {
79-
return ctx.send.json({ error: 'Worker not enabled' }, { status: 503 })
79+
return ctx.send.json(
80+
{
81+
error: 'Worker not enabled'
82+
},
83+
{
84+
status: 503
85+
}
86+
)
8087
}
81-
const result = await worker.run<{ done: boolean; value: number }>({ iterations: 50_000 })
82-
return ctx.send.json({ value: result?.value })
88+
const result = await worker.run<{ done: boolean; value: number }>({
89+
iterations: 50_000
90+
})
91+
return ctx.send.json({
92+
value: result?.value
93+
})
8394
}
8495
```
8596

@@ -105,7 +116,7 @@ worker: {
105116

106117
### `taskTimeoutMs`
107118

108-
Timeout per tugas dalam milidetik. Default adalah **30000**. Tugas yang berjalan lebih lama ditolak dengan error timeout dan worker dilahirkan ulang.
119+
Timeout per tugas dalam milidetik. Default adalah **5000**. Tugas yang berjalan lebih lama ditolak dengan error timeout, slot direklaim, dan worker dijalankan ulang. Reklaim ini muncul sebagai event [`worker:timeout`](/id/middleware/observability/events#worker) lalu [`worker:respawn`](/id/middleware/observability/events#worker).
109120

110121
```typescript
111122
worker: {
@@ -114,6 +125,31 @@ worker: {
114125
}
115126
```
116127

128+
### `maxQueueDepth`
129+
130+
Maksimum tugas diterima-tapi-belum-selesai yang ditahan pool sebelum menolak pekerjaan baru. Default adalah jumlah worker dikali **8**, jadi pool 4 menahan hingga 32. Begitu batas tercapai, dispatch baru ditolak langsung alih-alih diantrekan, sehingga banjir pekerjaan tidak menumpuk tanpa batas:
131+
132+
```typescript
133+
worker: {
134+
scriptURL: workerScriptUrl,
135+
poolSize: 4,
136+
maxQueueDepth: 64
137+
}
138+
```
139+
140+
### `maxQueueWaitMs`
141+
142+
Maksimum proyeksi tunggu, diukur sebagai jumlah tugas pending pada slot terpilih dikali `taskTimeoutMs`, sebelum dispatch ditolak. Default adalah **2000**. Tugas yang seharusnya menunggu di belakang antrean panjang ditolak cepat alih-alih menunggu:
143+
144+
```typescript
145+
worker: {
146+
scriptURL: workerScriptUrl,
147+
maxQueueWaitMs: 5_000
148+
}
149+
```
150+
151+
Dispatch yang ditolak langsung gagal dan muncul sebagai event [`worker:rejected`](/id/middleware/observability/events#worker), dengan `reason` menyebut apakah `maxQueueDepth` atau `maxQueueWaitMs` yang memicunya.
152+
117153
## Contoh Lengkap (Worker Inline)
118154

119155
Memakai script worker inline dengan `Blob` dan `createObjectURL`:
@@ -127,13 +163,21 @@ self.onmessage = (e) => {
127163
const n = Math.max(0, Number(data.iterations) || 50000)
128164
let value = 0
129165
for (let i = 0; i < n; i++) value += Math.sqrt(i)
130-
self.postMessage({ done: true, value })
166+
self.postMessage({
167+
done: true,
168+
value
169+
})
131170
}
132171
export {}
133172
`
134173

135174
const workerScriptUrl = URL.createObjectURL(
136-
new Blob([workerCode], { type: 'application/javascript' })
175+
new Blob(
176+
[workerCode],
177+
{
178+
type: 'application/javascript'
179+
}
180+
)
137181
)
138182

139183
const router = new Router({
@@ -151,10 +195,11 @@ await router.serve(8000)
151195

152196
- **Tanpa pool:** Router yang dibuat tanpa `worker` membiarkan `ctx.getState('worker' as never)` undefined. Kembalikan 503 atau pesan jelas ketika rute butuh worker.
153197
- **Error worker:** Ketika worker memanggil `postMessage({ error: true, message: '...' })`, `worker.run()` ditolak dengan `Error` yang membawa pesan itu. Tanpa pesan, error berbunyi `Worker returned an error with no message`.
154-
- **Crash worker:** Ketika worker melempar atau crash, `run()` ditolak dengan `Worker task failed before responding`.
155-
- **Timeout tugas:** Ketika tugas berjalan melewati `taskTimeoutMs` (default 30000), `run()` ditolak dengan `Worker task exceeded <ms>ms timeout`.
198+
- **Crash worker:** Ketika worker melempar atau crash, `run()` ditolak dengan `Worker task failed before responding`, dan slot pulih dengan sendirinya.
199+
- **Timeout tugas:** Ketika tugas berjalan melewati `taskTimeoutMs` (default 5000), `run()` ditolak dengan `Worker task exceeded <ms>ms timeout`.
200+
- **Ditolak di bawah beban:** Ketika pool mencapai `maxQueueDepth` atau proyeksi tunggu melewati `maxQueueWaitMs`, `run()` ditolak dengan error antrean-penuh atau slot-sibuk sebelum tugas sempat mulai.
156201

157-
Tangkap tugas yang ditolak dan teruskan ke [error handler terpusat](/id/error-handling/object-details):
202+
Setiap kesalahan ini juga mengalir lewat bus observability sebagai [event worker](/id/middleware/observability/events#worker), jadi stall, crash, pemulihan, atau penolakan terlihat tanpa menyentuh jalur request. Tangkap tugas yang ditolak dan teruskan ke [error handler terpusat](/id/error-handling/object-details):
158203

159204
```typescript
160205
try {

docs/id/middleware/csrf.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,9 @@ import { Mware, Router } from '@neabyte/deserve'
3535
const router = new Router()
3636
// ---cut---
3737
// Satu origin tepercaya
38-
router.use(Mware.csrf({ origin: 'https://app.example.com' }))
38+
router.use(Mware.csrf({
39+
origin: 'https://app.example.com'
40+
}))
3941

4042
// Daftar origin tepercaya
4143
router.use(
@@ -99,3 +101,5 @@ type CsrfRulePredicate = (value: string, ctx: Context) => boolean
99101
## Penanganan Error
100102
101103
Ketika request diblokir, middleware mengembalikan pesan `Request blocked by CSRF protection` dengan **status code 403**. Untuk membentuk response itu, daftarkan satu handler dengan [`router.catch()`](/id/error-handling/object-details), atau andalkan [perilaku default](/id/error-handling/default-behavior).
104+
105+
Aturan `origin` atau `secFetchSite` kustom yang melempar gagal pemeriksaannya sendiri dan jatuh aman ke penolakan, dan kesalahannya muncul sebagai event [`csrf:rule-error`](/id/middleware/observability/events#middleware) yang menyebut aturan mana yang rusak alih-alih tetap tersembunyi.

docs/id/middleware/observability/errors.md

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ Error muncul di bus [`router.on()`](/id/middleware/observability/overview) yang
1313
```typescript twoslash
1414
import { Router } from '@neabyte/deserve'
1515

16-
const router = new Router({ routesDir: './routes' })
16+
const router = new Router({
17+
routesDir: './routes'
18+
})
1719

1820
// Catat setiap request gagal
1921
router.on((event) => {
@@ -40,7 +42,9 @@ await router.serve(8000)
4042
```typescript twoslash
4143
import { Router } from '@neabyte/deserve'
4244

43-
const router = new Router({ routesDir: './routes' })
45+
const router = new Router({
46+
routesDir: './routes'
47+
})
4448
// ---cut---
4549
router.on((event) => {
4650
if (event.kind === 'process:error') {
@@ -51,9 +55,28 @@ router.on((event) => {
5155
})
5256
```
5357

58+
## Menangkap Kesalahan Subsistem
59+
60+
Listener yang sama menangkap kesalahan dari worker pool dan middleware bawaan. Task yang timeout, worker yang crash, dispatch yang ditolak di bawah beban, cookie session yang gagal didekode, dan aturan CSRF yang melempar masing-masing tiba sebagai event-nya sendiri. Saring berdasarkan kind yang terdaftar di [Referensi Event](/id/middleware/observability/events#worker) untuk merutekannya ke tempat log:
61+
62+
```typescript twoslash
63+
import { Router } from '@neabyte/deserve'
64+
65+
const router = new Router({
66+
routesDir: './routes'
67+
})
68+
// ---cut---
69+
router.on((event) => {
70+
// Bereaksi pada kesalahan worker dan middleware
71+
if (event.kind === 'worker:crash' || event.kind === 'session:invalid') {
72+
console.error(event.kind, event.metadata)
73+
}
74+
})
75+
```
76+
5477
## Memasangkan Dengan Penanganan Error
5578

56-
Dua hook menutup tugas berbeda:
79+
Dua hook menangani tugas berbeda:
5780

5881
- [`router.catch()`](/id/error-handling/object-details) membentuk response yang diterima klien.
5982
- `router.on()` mencatat apa yang terjadi untuk log dan metrik.
@@ -65,11 +88,20 @@ Pakai `catch` untuk mengontrol balasan, dan `on` untuk mengamatinya. Pengaturan
6588
```typescript twoslash
6689
import { Router } from '@neabyte/deserve'
6790

68-
const router = new Router({ routesDir: './routes' })
91+
const router = new Router({
92+
routesDir: './routes'
93+
})
6994
// ---cut---
7095
// Bentuk response klien
7196
router.catch((ctx, info) => {
72-
return ctx.send.json({ error: 'Something went wrong' }, { status: info.statusCode })
97+
return ctx.send.json(
98+
{
99+
error: 'Something went wrong'
100+
},
101+
{
102+
status: info.statusCode
103+
}
104+
)
73105
})
74106

75107
// Catat kegagalan untuk nanti

docs/id/middleware/observability/events.md

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ description: "Referensi semua event siklus hidup dan error yang dipancarkan rout
66

77
Setiap event dari [`router.on()`](/id/middleware/observability/overview) membawa diskriminan `kind` dan objek `metadata`. Halaman ini mendaftar setiap jenis dan field yang disediakannya.
88

9-
![Event request bernilai external secara default tapi jadi internal ketika timeout, error framework, atau context yang hilang yang memicunya, sementara setiap kind non-request selalu internal, jadi merutekan berdasarkan field type menjaga lalu lintas klien normal tetap di luar kanal alert kesalahan](/diagrams/obs-event-channel.png)
9+
![Event request bernilai external secara default tapi jadi internal ketika dipicu oleh timeout, error framework, atau context yang hilang, sementara setiap kind non-request selalu internal, jadi merutekan berdasarkan field type menjaga lalu lintas klien normal tetap di luar kanal alert kesalahan](/diagrams/obs-event-channel.png)
1010

1111
## Server
1212

@@ -15,7 +15,7 @@ Setiap event dari [`router.on()`](/id/middleware/observability/overview) membawa
1515
| `server:listening` | `port`, `hostname` |
1616
| `server:shutdown` | tidak ada |
1717

18-
`server:listening` menyala saat server mengikat port. `server:shutdown` menyala setelah server selesai dikuras.
18+
`server:listening` menyala saat server mengikat port. `server:shutdown` menyala setelah server selesai menuntaskan request berjalan.
1919

2020
## Rute
2121

@@ -41,6 +41,26 @@ Event reload datang dari hot reload saat berkas berubah di disk.
4141

4242
Event view datang dari [mesin rendering DVE](/id/rendering/).
4343

44+
## Worker
45+
46+
| Kind | Metadata |
47+
| ----------------- | ------------------------------------------------- |
48+
| `worker:timeout` | `workerIndex`, `timeoutMs`, `error` |
49+
| `worker:crash` | `workerIndex`, `error` |
50+
| `worker:respawn` | `workerIndex` |
51+
| `worker:rejected` | `reason` (`queue-depth`, `queue-wait`), `queueDepth`, `maxQueueDepth` |
52+
53+
`worker:timeout` menyala saat sebuah task melewati tenggatnya, `worker:crash` saat worker mati di tengah task, dan `worker:respawn` saat slot yang dibebaskan diganti. `worker:rejected` menyala saat sebuah dispatch ditolak di bawah beban, dengan `reason` menyebut apakah kedalaman antrean atau proyeksi tunggu yang memicu batas. Ini datang dari [worker pool](/id/core-concepts/worker-pool).
54+
55+
## Middleware
56+
57+
| Kind | Metadata |
58+
| ----------------- | ------------------------------------------------- |
59+
| `session:invalid` | `cookieName`, `reason` (`tampered`, `expired`, `malformed`) |
60+
| `csrf:rule-error` | `rule` (`origin`, `secFetchSite`), `error` |
61+
62+
`session:invalid` menyala saat cookie bertanda tangan gagal didekode, dengan `reason` menyebut apakah nilainya dirusak, sudah lewat `maxAge`, atau malformed, sementara request lanjut tanpa session terpasang. Ini datang dari [middleware session](/id/middleware/session). `csrf:rule-error` menyala saat aturan CSRF kustom melempar, menyebut aturan mana yang rusak sementara pemeriksaan tetap jatuh aman ke penolakan. Ini datang dari [middleware CSRF](/id/middleware/csrf).
63+
4464
## Request
4565

4666
| Kind | Metadata |

0 commit comments

Comments
 (0)