Skip to content

Commit d98465c

Browse files
committed
docs(recipes): add object storage, graceful shutdown, and deploy guides
- Add a Graceful Shutdown recipe covering signal handling and drain - Add an Object Storage recipe using the staticHandler hook - Add a Production Deploy recipe covering permission flags and binaries - Register the three recipes in the sidebar nav for both locales
1 parent b4405d0 commit d98465c

7 files changed

Lines changed: 626 additions & 2 deletions

File tree

docs/.vitepress/config.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,10 @@ export default withMermaid(
245245
collapsed: true,
246246
items: [
247247
{ text: 'File Uploads', link: '/recipes/file-upload' },
248-
{ text: 'Streaming Data', link: '/recipes/streaming-data' }
248+
{ text: 'Streaming Data', link: '/recipes/streaming-data' },
249+
{ text: 'Object Storage', link: '/recipes/object-storage' },
250+
{ text: 'Graceful Shutdown', link: '/recipes/graceful-shutdown' },
251+
{ text: 'Production Deploy', link: '/recipes/production-deploy' }
249252
]
250253
}
251254
]
@@ -432,7 +435,10 @@ export default withMermaid(
432435
collapsed: true,
433436
items: [
434437
{ text: 'Upload File', link: '/id/recipes/file-upload' },
435-
{ text: 'Streaming Data', link: '/id/recipes/streaming-data' }
438+
{ text: 'Streaming Data', link: '/id/recipes/streaming-data' },
439+
{ text: 'Object Storage', link: '/id/recipes/object-storage' },
440+
{ text: 'Graceful Shutdown', link: '/id/recipes/graceful-shutdown' },
441+
{ text: 'Production Deploy', link: '/id/recipes/production-deploy' }
436442
]
437443
}
438444
]
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
---
2+
description: 'Hentikan server Deserve dengan bersih pada SIGINT atau SIGTERM, kuras request yang sedang berjalan, dan jalankan kerja shutdown dengan AbortSignal.'
3+
---
4+
5+
# Graceful Shutdown
6+
7+
Graceful shutdown menghentikan server menerima koneksi baru sambil membiarkan request yang sedang berjalan selesai, jadi deploy atau restart kontainer tidak pernah memotong response di tengah jalan. Deserve menangani ini secara bawaan, dan sebuah [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) membuka jalan untuk memicunya dari kode.
8+
9+
## Penanganan Sinyal Bawaan
10+
11+
`router.serve()` polos sudah mendengarkan sinyal yang dikirim process manager saat berhenti. Pada `SIGINT` (`Ctrl+C` di terminal) atau `SIGTERM` (yang dikirim Docker dan kebanyakan orchestrator), server berhenti menerima request baru, menguras yang masih berjalan, lalu meresolusi promise `serve()`:
12+
13+
```typescript twoslash
14+
import { Router } from '@neabyte/deserve'
15+
16+
const router = new Router({
17+
routesDir: './routes'
18+
})
19+
20+
// Resolusi setelah pengurasan selesai
21+
await router.serve(8000)
22+
23+
// Tercapai hanya setelah shutdown bersih
24+
console.log('Server stopped')
25+
```
26+
27+
Windows mendengarkan `SIGINT` saja, karena `SIGTERM` tidak dikirim di sana. Tidak ada penyetelan yang diperlukan untuk jalur ini, jadi server dalam kontainer sudah keluar dengan bersih saat `docker stop`.
28+
29+
## Memicu Shutdown Dari Kode
30+
31+
Meneruskan `AbortSignal` sebagai argumen ketiga menyerahkan pemicu ke aplikasi, yang cocok untuk test yang perlu menghentikan server atau route admin yang mengakhiri proses. Membatalkan controller menguras server dengan cara yang sama seperti sinyal:
32+
33+
```typescript twoslash
34+
import { Router } from '@neabyte/deserve'
35+
36+
const router = new Router({
37+
routesDir: './routes'
38+
})
39+
// ---cut---
40+
const controller = new AbortController()
41+
42+
// Hentikan server setelah tiga puluh detik
43+
setTimeout(() => controller.abort(), 30_000)
44+
45+
// Abort menguras, lalu serve resolusi
46+
await router.serve(8000, '0.0.0.0', controller.signal)
47+
```
48+
49+
Menyerahkan `AbortSignal` mengambil alih pemicu berhenti, jadi listener bawaan `SIGINT` dan `SIGTERM` tetap mati dan controller menjadi satu-satunya cara berhenti. Saat keduanya diinginkan, sambungkan dengan membatalkan controller dari dalam listener sinyal.
50+
51+
## Menjalankan Kerja Saat Shutdown
52+
53+
Pembersihan seperti menutup pool basis data atau membilas buffer berada setelah pengurasan, bukan di dalamnya. Event [`server:shutdown`](/id/middleware/observability/events#server) menyala setelah server selesai dikuras, jadi satu listener [observability](/id/middleware/observability/overview) menjaga kerja shutdown tetap di satu tempat:
54+
55+
```typescript twoslash
56+
import { Router } from '@neabyte/deserve'
57+
58+
const router = new Router({
59+
routesDir: './routes'
60+
})
61+
// ---cut---
62+
router.on((event) => {
63+
// Berjalan setelah pengurasan selesai
64+
if (event.kind === 'server:shutdown') {
65+
console.log('Closing resources')
66+
}
67+
})
68+
69+
await router.serve(8000)
70+
```
71+
72+
Event pasangannya [`server:listening`](/id/middleware/observability/events#server) menyala saat server mengikat port, jadi kait startup dan shutdown berdampingan di bus yang sama.
73+
74+
## Arti Pengurasan Bagi Sebuah Request
75+
76+
Request yang sedang berjalan saat pengurasan mulai akan tuntas sampai selesai, dan response-nya tetap terkirim. Koneksi yang tiba setelah pengurasan dimulai ditolak, karena listener sudah berhenti menerima. Response berumur panjang adalah satu hal yang perlu diperhatikan, karena [stream](/id/recipes/streaming-data) atau [WebSocket](/id/middleware/websocket) yang terbuka menahan pengurasan sampai koneksi itu ditutup. Membatasi berapa lama satu request boleh berjalan dengan [`requestTimeoutMs`](/id/getting-started/routes-configuration#requesttimeoutms) menjaga pengurasan tidak menunggu selamanya pada handler yang lambat.

docs/id/recipes/object-storage.md

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
---
2+
description: 'Sajikan berkas dari S3, R2, atau object storage apa pun di Deserve lewat hook staticHandler atau route handler.'
3+
---
4+
5+
# Object Storage
6+
7+
[Static serving](/id/static-file/basic) bawaan membaca dari filesystem lokal, jadi `router.static()` saja tidak bisa menjangkau bucket di S3, Cloudflare R2, atau Google Cloud Storage. Jembatannya adalah opsi [`staticHandler`](/id/getting-started/routes-configuration#statichandler), sebuah hook yang mempertahankan route static yang familiar sambil menukar pembacaan berkas dengan fetch ke object storage. Route tetap terdaftar lewat `router.static()`, dan handler menjawab tiap request dari bucket alih-alih dari disk.
8+
9+
## Kenapa Hook dan Bukan Path
10+
11+
Opsi `path` pada [static serving](/id/static-file/basic#path) memetakan prefix URL ke folder yang bisa diresolusi `Deno.stat` dan `Deno.realPath`, yang merupakan kontrak disk lokal. Object storage tidak punya path nyata di disk, jadi pemeriksaan traversal yang aman dan streaming lewat file handle tidak berlaku. Hook `staticHandler` menyerahkan seluruh langkah serve, jadi bucket menjadi sumber kebenaran sementara permukaan route tetap sama.
12+
13+
## Menyajikan Dari Bucket
14+
15+
Sebagian besar object store mengekspos endpoint HTTPS per objek, jadi `fetch` ke `${endpoint}/${key}` menarik byte-nya. Handler memotong prefix URL dari `ctx.pathname` untuk memulihkan kunci objek, lalu mengalirkan body response langsung lewat [`ctx.send.stream`](/id/response/stream):
16+
17+
```typescript twoslash
18+
import { Router, type Context, type ServeOptions } from '@neabyte/deserve'
19+
20+
// Endpoint dasar bucket
21+
const endpoint = 'https://my-bucket.s3.amazonaws.com'
22+
23+
const router = new Router({
24+
routesDir: 'routes',
25+
staticHandler: {
26+
// Sajikan tiap objek dari bucket
27+
async serve(ctx: Context, options: ServeOptions, urlPath: string) {
28+
// Pulihkan kunci objek dari path
29+
const key = ctx.pathname.slice(urlPath.length).replace(/^\//, '')
30+
const object = await fetch(`${endpoint}/${key}`)
31+
if (!object.ok || !object.body) {
32+
return ctx.handleError(404, new Deno.errors.NotFound('Object not found'))
33+
}
34+
// Alirkan body bucket ke klien
35+
const contentType = object.headers.get('content-type') ?? 'application/octet-stream'
36+
return ctx.send.stream(object.body, undefined, contentType)
37+
}
38+
}
39+
})
40+
41+
// Daftarkan route yang dipenuhi handler
42+
router.static(
43+
'/assets',
44+
{
45+
path: 's3'
46+
}
47+
)
48+
49+
await router.serve(8000)
50+
```
51+
52+
Nilai `path` tetap harus diset pada [`router.static()`](/id/static-file/basic) karena wajib, namun handler mengabaikannya di sini karena bucket menggantikan folder lokal. Request ke `/assets/logo.png` menjadi fetch untuk kunci `logo.png`.
53+
54+
## Meneruskan Byte Range
55+
56+
Static serving menjawab [byte range](/id/static-file/basic#permintaan-byte-range) sendiri, tapi handler kustom kini memegang tugas itu. Meneruskan header `Range` yang masuk ke bucket membiarkan store mengembalikan konten parsial, dan meneruskan kembali status serta header range menjaga penggeser video atau unduhan yang bisa dilanjutkan tetap bekerja:
57+
58+
```typescript twoslash
59+
import type { Context, ServeOptions } from '@neabyte/deserve'
60+
declare const endpoint: string
61+
// ---cut---
62+
async function serve(ctx: Context, options: ServeOptions, urlPath: string) {
63+
const key = ctx.pathname.slice(urlPath.length).replace(/^\//, '')
64+
const range = ctx.header('range')
65+
66+
// Teruskan header Range bila ada
67+
const object = await fetch(`${endpoint}/${key}`, {
68+
headers: range ? { Range: range } : {}
69+
})
70+
if (!object.ok || !object.body) {
71+
return ctx.handleError(404, new Deno.errors.NotFound('Object not found'))
72+
}
73+
74+
// Cerminkan header range ke klien
75+
const contentType = object.headers.get('content-type') ?? 'application/octet-stream'
76+
const contentRange = object.headers.get('content-range')
77+
if (contentRange) {
78+
ctx.setHeader('Content-Range', contentRange)
79+
ctx.setHeader('Accept-Ranges', 'bytes')
80+
}
81+
return ctx.send.custom(object.body, {
82+
status: object.status,
83+
headers: {
84+
'Content-Type': contentType
85+
}
86+
})
87+
}
88+
```
89+
90+
Sebuah `206 Partial Content` dari bucket mengalir balik tanpa berubah, karena `ctx.send.custom` mempertahankan status yang dipilih bucket.
91+
92+
## Memakai Route Handler Sebagai Ganti
93+
94+
Hook `staticHandler` mencakup satu prefix URL utuh, yang cocok untuk folder aset publik. Satu unduhan di balik auth atau logika bisnis lebih cocok dengan [route handler](/id/core-concepts/file-based-routing) biasa, tempat middleware berjalan lebih dulu dan kunci datang dari [route param](/id/core-concepts/route-patterns):
95+
96+
```typescript twoslash
97+
import type { Context } from '@neabyte/deserve'
98+
declare const endpoint: string
99+
// ---cut---
100+
// routes/files/[key].ts
101+
export async function GET(ctx: Context): Promise<Response> {
102+
const key = ctx.param('key')
103+
const object = await fetch(`${endpoint}/${key}`)
104+
if (!object.ok || !object.body) {
105+
return ctx.handleError(404, new Deno.errors.NotFound('Object not found'))
106+
}
107+
// Alirkan objek langsung apa adanya
108+
const contentType = object.headers.get('content-type') ?? 'application/octet-stream'
109+
return ctx.send.stream(object.body, undefined, contentType)
110+
}
111+
```
112+
113+
Jalur ini menjalankan rantai middleware penuh, jadi menjaganya dengan [basic auth](/id/middleware/basic-auth) atau pemeriksaan [session](/id/middleware/session) terjadi sebelum bucket disentuh sama sekali.
114+
115+
## Menandatangani Request
116+
117+
Bucket privat butuh request yang ditandatangani, bukan `fetch` polos. Dua jalur cocok:
118+
119+
- **URL presigned** - SDK menandatangani URL berumur pendek, dan handler bisa mengalihkan dengan [`ctx.redirect`](/id/response/redirect) atau mengambilnya sisi server.
120+
- **SDK sisi server** - klien resmi menandatangani tiap request, misalnya [AWS SDK for JavaScript](https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/welcome.html) untuk S3 atau binding Cloudflare R2 untuk [Workers](https://developers.cloudflare.com/r2/api/workers/workers-api-reference/).
121+
122+
Jalur mana pun yang menandatangani request, body response tetap mengalir lewat `ctx.send.stream`, jadi bentuk penyajiannya tetap sama.
123+
124+
## Menangani Kegagalan
125+
126+
Object storage menambah panggilan jaringan yang bisa timeout atau ditolak, jadi tiap fetch meneruskan kegagalannya ke [penanganan error terpusat](/id/error-handling/object-details) alih-alih membocorkan error mentah. Objek yang hilang dipetakan ke **404**, sementara gangguan upstream dipetakan ke **502** agar penyebabnya tetap terbaca:
127+
128+
```typescript twoslash
129+
import type { Context } from '@neabyte/deserve'
130+
declare const endpoint: string
131+
// ---cut---
132+
export async function GET(ctx: Context): Promise<Response> {
133+
const key = ctx.param('key')
134+
try {
135+
const object = await fetch(`${endpoint}/${key}`)
136+
if (object.status === 404) {
137+
return await ctx.handleError(404, new Deno.errors.NotFound('Object not found'))
138+
}
139+
// Petakan kegagalan upstream ke 502
140+
if (!object.ok || !object.body) {
141+
return await ctx.handleError(502, new Error('Object storage unavailable'))
142+
}
143+
return ctx.send.stream(object.body, undefined, 'application/octet-stream')
144+
} catch (error) {
145+
// Rutekan tiap gangguan jaringan ke error handling
146+
return await ctx.handleError(502, error as Error)
147+
}
148+
}
149+
```
150+
151+
Membentuk ini menjadi satu response klien tinggal di [Penanganan Error](/id/error-handling/object-details), dan menangkapnya untuk log tinggal di [Pelaporan Error](/id/middleware/observability/errors).
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
---
2+
description: 'Kirim server Deserve ke produksi dengan flag permission Deno yang tepat, perintah run yang terkunci, dan binary terkompilasi mandiri.'
3+
---
4+
5+
# Production Deploy
6+
7+
Deno berjalan [tanpa permission secara bawaan](https://docs.deno.com/runtime/fundamentals/security/), jadi server produksi hanya mendapat akses yang diserahkan lewat command line. Server Deserve butuh sekumpulan kecil flag yang bisa diprediksi, dan [Deno CLI](https://docs.deno.com/runtime/reference/cli/) yang sama yang menjalankannya secara lokal juga mengompilasinya menjadi satu binary mandiri untuk deploy.
8+
9+
## Daftar Periksa Permission
10+
11+
Server Deserve menyentuh jaringan untuk mengikat port dan disk untuk membaca rute, view, dan berkas statis. Itu memetakan ke tiga flag, sisanya tetap mati kecuali sebuah app membutuhkannya:
12+
13+
| Flag | Kenapa Deserve membutuhkannya | Wajib |
14+
| ---------------- | ------------------------------------------------------------ | ----------------- |
15+
| `--allow-net` | Mengikat port lewat `Deno.serve` dan menggerakkan `fetch` | Ya |
16+
| `--allow-read` | Meresolusi folder rute, view, dan berkas statis di disk | Ya |
17+
| `--allow-env` | Membaca variabel `PORT` saat port datang dari host | Hanya dengan env |
18+
| `--allow-write` | Tidak dipakai framework, hanya oleh app yang menyimpan berkas | Hanya saat menulis |
19+
20+
Permission write tetap mati untuk server biasa. Route yang menyimpan unggahan ke disk adalah alasan umum menambahkannya, dibatasi ke satu folder seperti pada [Upload File](/id/recipes/file-upload#menyimpan-ke-disk).
21+
22+
## Mengunci Permission
23+
24+
Permission `*` berguna saat membangun, namun produksi lebih terbaca saat tiap flag menyebut persis apa yang boleh disentuhnya. Membatasi `--allow-read` ke folder aset dan `--allow-env` ke satu variabel menjaga permukaan tetap kecil:
25+
26+
```bash
27+
# Batasi tiap permission ke kebutuhannya
28+
deno run \
29+
--allow-net \
30+
--allow-read=./routes,./views,./public \
31+
--allow-env=PORT \
32+
main.ts
33+
```
34+
35+
Sebuah [`deno task`](https://docs.deno.com/runtime/reference/cli/task/) di `deno.json` menyimpan perintah panjang itu dan memberi deploy satu nama untuk dipanggil:
36+
37+
```json
38+
{
39+
"tasks": {
40+
"start": "deno run --allow-net --allow-read=./routes,./views,./public --allow-env=PORT main.ts"
41+
}
42+
}
43+
```
44+
45+
Menjalankan `deno task start` lalu meluncurkan server dengan flag terkunci tiap kali.
46+
47+
## Membaca Port Dari Environment
48+
49+
Host biasanya menetapkan port lewat variabel `PORT`. Memanggil `serve()` tanpa port membaca `PORT` lebih dulu dan kembali ke `8000` sebagai default, jadi binary yang sama cocok untuk run lokal dan host terkelola:
50+
51+
```typescript twoslash
52+
import { Router } from '@neabyte/deserve'
53+
54+
const router = new Router({
55+
routesDir: './routes'
56+
})
57+
// ---cut---
58+
// Baca env PORT, fallback ke 8000
59+
await router.serve()
60+
```
61+
62+
Membaca variabel itulah yang dicakup `--allow-env=PORT`. Memberi angka eksplisit seperti `serve(8000)` melewati pencarian, jadi flag env jadi tidak relevan saat port di-hardcode. Argumen host dan sinyal lengkap ada di [Graceful Shutdown](/id/recipes/graceful-shutdown).
63+
64+
## Mengompilasi Binary Mandiri
65+
66+
[`deno compile`](https://docs.deno.com/runtime/reference/cli/compile/) menyatukan server dan permission-nya menjadi satu executable yang berjalan tanpa Deno terpasang, yang cocok untuk kontainer ramping atau host polos. Flag permission berada pada perintah compile agar binary membawanya:
67+
68+
```bash
69+
# Satukan server dan flag-nya jadi binary
70+
deno compile \
71+
--allow-net \
72+
--allow-read=./routes,./views,./public \
73+
--allow-env=PORT \
74+
--output server \
75+
main.ts
76+
```
77+
78+
Hasilnya berjalan langsung dari `./server` dengan flag sudah di dalam. Satu catatan cocok untuk Deserve, karena [hot reload](/id/core-concepts/hot-reload) memantau berkas di disk dan binary terkompilasi menyajikan snapshot tetap, jadi suntingan setelah build perlu compile baru alih-alih pertukaran langsung.
79+
80+
## Mengawasinya Berjalan
81+
82+
Produksi butuh mata pada server tanpa membanjiri konsol dengan cetakan, yang merupakan tujuan [bus event observability](/id/middleware/observability/overview). Satu listener [`router.on()`](/id/middleware/observability/events) meneruskan event lifecycle, request, dan kesalahan ke apa pun yang mengumpulkan log, dan [pelaporan error](/id/middleware/observability/errors) merutekan kegagalan ke tempat yang sama. Berhenti bersih saat deploy dicakup oleh [Graceful Shutdown](/id/recipes/graceful-shutdown), dan memindahkan kerja berat tanpa memblokir server dicakup oleh [worker pool](/id/core-concepts/worker-pool).

0 commit comments

Comments
 (0)