@finesoft/front 0.1.76 → 0.1.78

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/docs/01-getting-started.md +230 -0
  2. package/docs/02-routing-and-controllers.md +203 -0
  3. package/docs/03-middleware.md +220 -0
  4. package/docs/04-rendering-and-hydration.md +271 -0
  5. package/docs/05-i18n.md +243 -0
  6. package/docs/06-http-client.md +286 -0
  7. package/docs/07-di-container.md +264 -0
  8. package/docs/08-observability.md +290 -0
  9. package/docs/09-server-and-deployment.md +242 -0
  10. package/docs/10-features-platform-pwa.md +238 -0
  11. package/docs/README.md +72 -0
  12. package/docs/advanced/custom-action-handler.md +248 -0
  13. package/docs/advanced/custom-adapter.md +264 -0
  14. package/docs/advanced/custom-event-recorder.md +318 -0
  15. package/docs/advanced/inline-proxy-codegen.md +200 -0
  16. package/docs/advanced/multi-tenant-scopes.md +330 -0
  17. package/docs/engineering/ci-release-flow.md +244 -0
  18. package/docs/engineering/project-structure.md +296 -0
  19. package/docs/engineering/testing.md +317 -0
  20. package/docs/pitfalls/container-scope-leak.md +215 -0
  21. package/docs/pitfalls/i18n-bundle-size.md +182 -0
  22. package/docs/pitfalls/proxy-binary-payloads.md +133 -0
  23. package/docs/pitfalls/redirect-vs-rewrite.md +147 -0
  24. package/docs/pitfalls/ssr-hydration-mismatch.md +163 -0
  25. package/docs/pitfalls/ssr-vs-csr-globals.md +176 -0
  26. package/docs/zh/01-getting-started.md +230 -0
  27. package/docs/zh/02-routing-and-controllers.md +203 -0
  28. package/docs/zh/03-middleware.md +220 -0
  29. package/docs/zh/04-rendering-and-hydration.md +271 -0
  30. package/docs/zh/05-i18n.md +243 -0
  31. package/docs/zh/06-http-client.md +286 -0
  32. package/docs/zh/07-di-container.md +264 -0
  33. package/docs/zh/08-observability.md +287 -0
  34. package/docs/zh/09-server-and-deployment.md +242 -0
  35. package/docs/zh/10-features-platform-pwa.md +238 -0
  36. package/docs/zh/README.md +72 -0
  37. package/docs/zh/advanced/custom-action-handler.md +248 -0
  38. package/docs/zh/advanced/custom-adapter.md +264 -0
  39. package/docs/zh/advanced/custom-event-recorder.md +318 -0
  40. package/docs/zh/advanced/inline-proxy-codegen.md +200 -0
  41. package/docs/zh/advanced/multi-tenant-scopes.md +330 -0
  42. package/docs/zh/engineering/ci-release-flow.md +244 -0
  43. package/docs/zh/engineering/project-structure.md +296 -0
  44. package/docs/zh/engineering/testing.md +317 -0
  45. package/docs/zh/pitfalls/container-scope-leak.md +215 -0
  46. package/docs/zh/pitfalls/i18n-bundle-size.md +182 -0
  47. package/docs/zh/pitfalls/proxy-binary-payloads.md +133 -0
  48. package/docs/zh/pitfalls/redirect-vs-rewrite.md +147 -0
  49. package/docs/zh/pitfalls/ssr-hydration-mismatch.md +163 -0
  50. package/docs/zh/pitfalls/ssr-vs-csr-globals.md +176 -0
  51. package/package.json +2 -1
@@ -0,0 +1,290 @@
1
+ # 8. Observability
2
+
3
+ Three primitives, each composable, each replaceable:
4
+
5
+ - **`Logger`** — line-oriented (debug/info/warn/error)
6
+ - **`EventRecorder`** — structured event records (`{ name, fields }`)
7
+ - **`ReportCallback`** — fan-out of `warn`/`error` logs to external monitoring
8
+
9
+ Plus impression tracking via `IntersectionImpressionObserver`.
10
+
11
+ ## Logger
12
+
13
+ The base `ConsoleLogger` writes to `console.{debug,info,warn,error}` with a category prefix. Scoped loggers add nested prefixes.
14
+
15
+ ```ts
16
+ import { ConsoleLogger } from "@finesoft/front";
17
+
18
+ const logger = new ConsoleLogger("app");
19
+ logger.info("hello"); // [app] hello
20
+
21
+ const auth = logger.scope("auth");
22
+ auth.warn("token expired"); // [app:auth] token expired
23
+ ```
24
+
25
+ Levels: `debug`, `info`, `warn`, `error`. The framework calls `warn`/`error` on its own logger for internal failures (failed guards, dispatch errors).
26
+
27
+ ### Replacing the framework logger
28
+
29
+ ```ts
30
+ import { DEP_KEYS } from "@finesoft/front";
31
+
32
+ framework.container.register(DEP_KEYS.LOGGER, () => myLogger);
33
+ framework.container.register(DEP_KEYS.LOGGER_FACTORY, () => ({
34
+ create(category) {
35
+ return myLoggerImpl.scope(category);
36
+ },
37
+ }));
38
+ ```
39
+
40
+ ## `ReportCallback` — forwarding logs to monitoring
41
+
42
+ The cleanest way to ship `warn`/`error` logs to Sentry / Datadog / your own collector:
43
+
44
+ ```ts
45
+ import { Framework, type ReportCallback } from "@finesoft/front";
46
+
47
+ const reportCallback: ReportCallback = (level, category, args) => {
48
+ Sentry.captureMessage(`[${category}] ${args.join(" ")}`, level);
49
+ };
50
+
51
+ const framework = Framework.create({
52
+ reportCallback,
53
+ // ...
54
+ });
55
+ ```
56
+
57
+ How it works:
58
+
59
+ - `Framework.create({ reportCallback })` installs `ReportingLoggerFactory` over `ConsoleLogger`
60
+ - All `logger.warn(...)` and `logger.error(...)` calls fire the callback **and** continue to the console
61
+ - `debug` and `info` are not forwarded (avoid burying signal in volume)
62
+ - The callback runs inside `try/catch` — a failure in the reporter cannot crash the framework
63
+
64
+ ### Categories
65
+
66
+ Each `logger.scope("auth")` becomes a category passed to the callback. Use them for routing in your monitoring system:
67
+
68
+ ```ts
69
+ reportCallback(level, category, args) {
70
+ if (category.startsWith("auth")) {
71
+ sentry.captureMessage(/* go to auth project */);
72
+ } else {
73
+ sentry.captureMessage(/* default project */);
74
+ }
75
+ }
76
+ ```
77
+
78
+ ### Anti-spam
79
+
80
+ `ReportCallback` runs every time. If you have a hot loop logging the same warning a thousand times, you'll send a thousand events. Add deduplication in the callback itself:
81
+
82
+ ```ts
83
+ const seen = new Map<string, number>();
84
+ const reportCallback: ReportCallback = (level, category, args) => {
85
+ const key = `${category}:${args[0]}`;
86
+ const now = Date.now();
87
+ const last = seen.get(key) ?? 0;
88
+ if (now - last < 60_000) return; // 1 per minute per (category, message)
89
+ seen.set(key, now);
90
+ Sentry.captureMessage(/* ... */);
91
+ };
92
+ ```
93
+
94
+ ## `EventRecorder` — structured events
95
+
96
+ When you want events with fields, not free-form log lines, use `EventRecorder`.
97
+
98
+ ```ts
99
+ import { Framework, ConsoleEventRecorder, type EventRecorder } from "@finesoft/front";
100
+
101
+ const recorder: EventRecorder = new ConsoleEventRecorder();
102
+ const framework = Framework.create({ eventRecorder: recorder });
103
+
104
+ // Anywhere:
105
+ recorder.record({
106
+ name: "PageView",
107
+ fields: { url: "/products/42", referrer: "/search?q=widget" },
108
+ });
109
+ ```
110
+
111
+ ### Built-in events
112
+
113
+ The framework records `PageView` automatically via `framework.didEnterPage(page)`. This fires:
114
+
115
+ - After every successful navigation (SSR + CSR)
116
+ - With fields: `{ intentId, url, renderMode }`
117
+
118
+ ### Composing recorders
119
+
120
+ `CompositeEventRecorder` fans events to multiple sinks:
121
+
122
+ ```ts
123
+ import { ConsoleEventRecorder, CompositeEventRecorder } from "@finesoft/front";
124
+
125
+ const recorder = new CompositeEventRecorder([
126
+ new ConsoleEventRecorder(), // dev visibility
127
+ productionAnalyticsRecorder, // ship to backend
128
+ ]);
129
+ ```
130
+
131
+ If one recorder throws, the others still receive the event.
132
+
133
+ ### Adding common fields
134
+
135
+ `WithFieldsRecorder` decorates another recorder, prepending fields to every event:
136
+
137
+ ```ts
138
+ import { WithFieldsRecorder, ConsoleEventRecorder } from "@finesoft/front";
139
+
140
+ const recorder = new WithFieldsRecorder(new ConsoleEventRecorder(), [
141
+ { getFields: () => ({ app: "myApp", version: "1.0.0" }) },
142
+ { getFields: () => ({ userId: getCurrentUserId() }) },
143
+ ]);
144
+
145
+ recorder.record({ name: "Click", fields: { id: "buy" } });
146
+ // → { app, version, userId, id }
147
+ ```
148
+
149
+ Use this for session-scoped fields you don't want to repeat in every `record()` call.
150
+
151
+ ### Building your own recorder
152
+
153
+ Implement the `EventRecorder` interface:
154
+
155
+ ```ts
156
+ import type { EventRecorder, EventRecord } from "@finesoft/front";
157
+
158
+ class HttpEventRecorder implements EventRecorder {
159
+ constructor(private endpoint: string) {}
160
+ record(event: EventRecord): void {
161
+ // fire-and-forget; do NOT await — record() shouldn't block UI
162
+ navigator.sendBeacon(this.endpoint, JSON.stringify(event));
163
+ }
164
+ destroy(): void {
165
+ // flush, close connections, etc.
166
+ }
167
+ }
168
+ ```
169
+
170
+ See [advanced/custom-event-recorder](./advanced/custom-event-recorder.md) for batching, retries, and lifecycle handling.
171
+
172
+ ## Impression tracking
173
+
174
+ Track when elements enter the viewport using `IntersectionObserver` plumbing:
175
+
176
+ ```ts
177
+ import { IntersectionImpressionObserver, type EventRecorder } from "@finesoft/front";
178
+
179
+ const observer = new IntersectionImpressionObserver((entries) => {
180
+ for (const entry of entries) {
181
+ recorder.record({
182
+ name: "Impression",
183
+ fields: { id: entry.id, ...entry.metadata },
184
+ });
185
+ }
186
+ });
187
+
188
+ // Attach to an element when it mounts
189
+ observer.observe(productCardElement, "product-card-123", {
190
+ category: "featured",
191
+ position: 3,
192
+ });
193
+
194
+ // Detach when it unmounts
195
+ observer.unobserve(productCardElement);
196
+
197
+ // On app teardown
198
+ observer.destroy();
199
+ ```
200
+
201
+ The observer fires the callback once per element per visibility transition. It does not deduplicate across observe/unobserve cycles — your callback should decide whether to record again.
202
+
203
+ ## Error handling via `fallback`
204
+
205
+ A controller error becomes a recorded event automatically:
206
+
207
+ ```ts
208
+ class HomeController extends BaseController<{}, HomePage> {
209
+ readonly intentId = "home";
210
+
211
+ async execute(_params, container) {
212
+ const api = container.resolve<UserApi>("userApi");
213
+ return { kind: "home", users: await api.list() };
214
+ }
215
+
216
+ fallback(_params, error) {
217
+ // 1. framework logs the error via its logger (→ reportCallback)
218
+ // 2. you return a degraded page
219
+ return { kind: "home", users: [], degraded: true };
220
+ }
221
+ }
222
+ ```
223
+
224
+ Order of operations on error:
225
+
226
+ 1. `execute()` throws.
227
+ 2. `BaseController` catches, calls `framework.logger.error("[controller:home]", error)`.
228
+ 3. Logger fires `reportCallback("error", "controller:home", [error])` → Sentry.
229
+ 4. `fallback()` returns a `Page` of your design.
230
+ 5. Render proceeds with the fallback page (HTTP 200 by default).
231
+
232
+ To set a non-200 status, return a page with an "error" marker and check it in an `afterLoad` guard:
233
+
234
+ ```ts
235
+ afterLoad: [
236
+ (ctx) => {
237
+ if ("degraded" in ctx.page && ctx.page.degraded) {
238
+ return deny(503, "Service degraded");
239
+ }
240
+ return next();
241
+ },
242
+ ],
243
+ ```
244
+
245
+ ## Metrics (counters / gauges / timing)
246
+
247
+ The framework's `MetricsClient` is minimal:
248
+
249
+ ```ts
250
+ import { DEP_KEYS } from "@finesoft/front";
251
+
252
+ const metrics = framework.container.resolve(DEP_KEYS.METRICS);
253
+
254
+ metrics.increment("requests.total");
255
+ metrics.increment("login.failed", { reason: "wrong_password" });
256
+ metrics.gauge("queue.depth", 17);
257
+ metrics.timing("render.duration", 142);
258
+ ```
259
+
260
+ The default implementation is a no-op. Register your own:
261
+
262
+ ```ts
263
+ framework.container.register(DEP_KEYS.METRICS, () => new StatsdMetrics(/* ... */));
264
+ ```
265
+
266
+ For most apps, `EventRecorder` is enough — your analytics backend already aggregates events into metrics.
267
+
268
+ ## Trace IDs and request correlation
269
+
270
+ A common pattern: tag every log and event with a per-request trace id.
271
+
272
+ ```ts
273
+ // beforeLoad guard
274
+ function traceIdGuard(ctx) {
275
+ const traceId = ctx.getHeader("x-request-id") ?? crypto.randomUUID();
276
+ ctx.container.register("traceId", () => traceId);
277
+ ctx.container.register(
278
+ DEP_KEYS.EVENT_RECORDER,
279
+ () => new WithFieldsRecorder(baseRecorder, [{ getFields: () => ({ traceId }) }]),
280
+ );
281
+ return next();
282
+ }
283
+ ```
284
+
285
+ Register the scoped recorder via `WithFieldsRecorder` so every event in this request automatically carries the trace id.
286
+
287
+ ## Next
288
+
289
+ - [Server & deployment](./09-server-and-deployment.md) — where these primitives meet HTTP
290
+ - [Advanced: custom event recorder](./advanced/custom-event-recorder.md) — production-grade implementation
@@ -0,0 +1,242 @@
1
+ # 9. Server & deployment
2
+
3
+ The server side of the framework. This chapter covers:
4
+
5
+ - The Vite plugin (`finesoftFrontViteConfig`) — dev server, build config, code generation
6
+ - `createServer` — the standalone Hono server
7
+ - The proxy router — declarative API forwarding with SSRF / binary integrity guards
8
+ - Adapters — Node, Vercel, Cloudflare, Netlify, static
9
+
10
+ ## The Vite plugin
11
+
12
+ ```ts
13
+ // vite.config.ts
14
+ import { finesoftFrontViteConfig } from "@finesoft/front";
15
+ import { defineConfig } from "vite";
16
+
17
+ export default defineConfig({
18
+ plugins: [
19
+ // ... view layer plugin (Vue/React/Svelte)
20
+ finesoftFrontViteConfig({
21
+ ssr: { entry: "src/ssr.ts" },
22
+ i18n: { messagesDir: "src/locales" },
23
+ proxies: [{ prefix: "/api", target: "https://upstream.example" }],
24
+ adapter: "auto",
25
+ isr: { routes: ["/blog/*"], ttl: 300 },
26
+ }),
27
+ ],
28
+ });
29
+ ```
30
+
31
+ ### Options
32
+
33
+ | Option | Type | Notes |
34
+ | ------------------ | ------------------------- | ------------------------------------------------------- |
35
+ | `ssr.entry` | `string` | Path to your SSR entry (default `src/ssr.ts`). |
36
+ | `i18n.messagesDir` | `string` | Folder with `{locale}.json` files (default off). |
37
+ | `proxies` | `ProxyRouteConfig[]` | Declarative API forwarding. See below. |
38
+ | `adapter` | `"auto" \| "node" \| ...` | Target platform. `"auto"` detects from env vars. |
39
+ | `isr` | `{ routes, ttl }` | Incremental Static Regeneration for prerendered routes. |
40
+
41
+ ### What it does
42
+
43
+ In dev:
44
+
45
+ - Starts a Hono server that runs your SSR entry on every request
46
+ - Hot-reloads SSR code via Vite's module graph
47
+ - Serves the proxy routes locally so client-side `fetch("/api/...")` works
48
+
49
+ In build:
50
+
51
+ - Bundles the client bundle with Vite's standard pipeline
52
+ - Bundles the SSR entry as a separate module
53
+ - Generates an adapter-specific entry file (`vercel.func`, `_worker.js`, `node-server.js`, etc.)
54
+ - Prerenders any `renderMode: "prerender"` routes to static HTML
55
+
56
+ ## `createServer` — the standalone Hono server
57
+
58
+ For Node deployments and tests, the framework exports a function that gives you a ready-to-run Hono app:
59
+
60
+ ```ts
61
+ import { createServer } from "@finesoft/front";
62
+
63
+ const app = createServer({
64
+ ssrEntry: "./dist/server/ssr.js",
65
+ proxies: [{ prefix: "/api", target: "https://upstream.example" }],
66
+ staticDir: "./dist/client",
67
+ isr: { routes: ["/blog/*"], ttl: 300 },
68
+ });
69
+
70
+ // app is a Hono instance — mount it however your runtime expects
71
+ import { serve } from "@hono/node-server";
72
+ serve({ fetch: app.fetch, port: 3000 });
73
+ ```
74
+
75
+ ### What it includes
76
+
77
+ - Static file serving for the client bundle
78
+ - All your proxy routes (registered via `registerProxyRoutes`)
79
+ - SSR rendering with full middleware pipeline
80
+ - ISR cache for prerendered routes
81
+ - Locale resolution from `Accept-Language`
82
+
83
+ ## Proxy routes
84
+
85
+ Declarative API forwarding with built-in SSRF protection, binary-safe forwarding, and configurable auth/cache.
86
+
87
+ ### Basic config
88
+
89
+ ```ts
90
+ proxies: [
91
+ {
92
+ prefix: "/api", // must start with /
93
+ target: "https://api.example.com", // must be https:// or http://
94
+ },
95
+ ],
96
+ ```
97
+
98
+ Now `GET /api/users/42` → `GET https://api.example.com/users/42`. Query params and request headers are forwarded.
99
+
100
+ ### Full options
101
+
102
+ ```ts
103
+ {
104
+ prefix: "/api/apple",
105
+ target: "https://api.music.apple.com",
106
+ methods: ["get", "post"], // default ["all"]
107
+ headers: { "X-App": "finesoft" }, // injected per request
108
+ auth: { type: "bearer", envKey: "APPLE_TOKEN" }, // reads process.env.APPLE_TOKEN
109
+ cache: "public, max-age=60", // Cache-Control on response
110
+ followRedirects: false, // default false (redirect: "manual")
111
+ }
112
+ ```
113
+
114
+ `auth.type`: `"bearer"` → `Authorization: Bearer <token>`. `"basic"` → `Authorization: Basic <token>`. The `envKey` is read at request time, so changing it (or unsetting it) does not require a restart.
115
+
116
+ ### What the framework enforces
117
+
118
+ - **SSRF protection**: path is rejected if URL-encoded (any `%`-encoded char), starts with `//`, or contains characters outside the allowed set (`[/\w.\-~%:@!$&'()*+,;=]`). Decoded ≠ raw also rejected (prevents `%2F` smuggling).
119
+ - **Open-redirect protection**: the constructed target URL must have the same `origin` as the configured `target`. Different origin → `400 Invalid proxy target`.
120
+ - **Binary integrity**: response body forwarded via `arrayBuffer()`, not `text()` — preserves bytes exactly. PDF, image, protobuf responses are byte-identical to the upstream response.
121
+ - **Size limit**: 10 MB. `Content-Length` header is checked first for fast rejection; actual body byte length is checked after fetch.
122
+ - **HTTP warning**: any `http://` target logs a warning at startup. Use HTTPS in production.
123
+
124
+ ### Generated proxy code (serverless / edge)
125
+
126
+ For serverless functions, the proxy logic is inlined into the deployed function bundle instead of relying on `registerProxyRoutes` at runtime. See [advanced/inline-proxy-codegen](./advanced/inline-proxy-codegen.md).
127
+
128
+ ## Adapters
129
+
130
+ | Adapter | Target | Build output |
131
+ | -------------- | -------------------------- | ------------------------------------------------------ |
132
+ | `"node"` | Standalone Node.js server | `dist/server/index.js` — `serve({ fetch: app.fetch })` |
133
+ | `"vercel"` | Vercel Build Output API v3 | `.vercel/output/` with `functions/` and `static/` |
134
+ | `"cloudflare"` | Cloudflare Workers | `dist/_worker.js` + `dist/_routes.json` |
135
+ | `"netlify"` | Netlify Functions v2 | `netlify/functions/` + `_redirects` |
136
+ | `"static"` | Pre-rendered static files | `dist/client/` only (no server) |
137
+ | `"auto"` | Auto-detect at build time | Picks one of the above by environment variable |
138
+
139
+ ### Auto-detection
140
+
141
+ `adapter: "auto"` checks (in order):
142
+
143
+ 1. `VERCEL=1` → vercel
144
+ 2. `CF_PAGES=1` → cloudflare
145
+ 3. `NETLIFY=1` → netlify
146
+ 4. otherwise → node
147
+
148
+ This works for most CI environments — Vercel / Cloudflare / Netlify all set these automatically during their build.
149
+
150
+ ## ISR (Incremental Static Regeneration)
151
+
152
+ ```ts
153
+ isr: {
154
+ routes: ["/blog/*", "/products/*"],
155
+ ttl: 300, // seconds
156
+ }
157
+ ```
158
+
159
+ How it works:
160
+
161
+ 1. First request to `/blog/hello-world`: render fully, cache the HTML, set expiry to now + 300s
162
+ 2. Subsequent requests within TTL: serve cached HTML directly
163
+ 3. After expiry: next request triggers re-render; concurrent requests get stale HTML until re-render finishes
164
+
165
+ The cache is in-memory per server instance. For multi-instance deployments where consistency matters, put a CDN in front and use HTTP `Cache-Control` headers instead.
166
+
167
+ Routes not matched by `isr.routes` always render fresh.
168
+
169
+ ### Cache invalidation
170
+
171
+ Programmatic invalidation is not exposed in the public API. To force a refresh:
172
+
173
+ - Restart the server (loses entire cache)
174
+ - Wait for TTL
175
+ - Add a cache-busting query param the controller can ignore but that bypasses the cache key
176
+
177
+ For production, push invalidation up to CDN level — the framework's in-memory cache is for single-instance serving.
178
+
179
+ ## Custom Hono middleware
180
+
181
+ If you need server logic outside the proxy and SSR (e.g., a webhook endpoint, a health check), mount it on the same Hono app:
182
+
183
+ ```ts
184
+ const app = createServer({ ssrEntry: "./dist/server/ssr.js" });
185
+
186
+ app.get("/health", (c) => c.json({ status: "ok" }));
187
+ app.post("/webhook", async (c) => {
188
+ const body = await c.req.json();
189
+ await handleWebhook(body);
190
+ return c.json({ ok: true });
191
+ });
192
+
193
+ // SSR catch-all is registered last by createServer — your routes win.
194
+ ```
195
+
196
+ ## Environment variables
197
+
198
+ The framework reads:
199
+
200
+ - `NODE_ENV` — `"production"` enables prod-only optimizations
201
+ - `PROXY_TOKEN` / `BASIC_TOKEN` / any `auth.envKey` — proxy auth secrets
202
+ - `VERCEL`, `CF_PAGES`, `NETLIFY` — adapter auto-detection
203
+
204
+ Anything else is yours. Access via `process.env` directly or by registering a config object in the DI container:
205
+
206
+ ```ts
207
+ framework.container.register("config", () => ({
208
+ upstreamUrl: process.env.UPSTREAM_URL ?? "https://api.example.com",
209
+ sessionSecret: requireEnv("SESSION_SECRET"),
210
+ }));
211
+ ```
212
+
213
+ ## Health checks and graceful shutdown
214
+
215
+ For Node deployments behind a load balancer:
216
+
217
+ ```ts
218
+ import { serve } from "@hono/node-server";
219
+
220
+ const app = createServer({
221
+ /* ... */
222
+ });
223
+ app.get("/health", (c) => c.json({ ok: true }));
224
+
225
+ const server = serve({ fetch: app.fetch, port: 3000 });
226
+
227
+ process.on("SIGTERM", () => {
228
+ server.close(() => {
229
+ // dispose Framework if you held a reference
230
+ framework.dispose();
231
+ process.exit(0);
232
+ });
233
+ });
234
+ ```
235
+
236
+ `framework.dispose()` recursively disposes the container, calls `destroy()` on registered recorders/loggers, and unregisters all routes.
237
+
238
+ ## Next
239
+
240
+ - [Features, platform, PWA](./10-features-platform-pwa.md) — feature flags, platform detection
241
+ - [Engineering: CI & release flow](./engineering/ci-release-flow.md) — automating releases
242
+ - [Pitfalls: proxy binary payloads](./pitfalls/proxy-binary-payloads.md) — why `arrayBuffer` matters