stitchkit 0.48.1 → 0.49.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/llms-full.txt CHANGED
@@ -712,8 +712,9 @@ The registry is intentionally flat: every key must point to one concrete
712
712
 
713
713
  ## `createServer`
714
714
 
715
- `createServer(config)` builds the router and starts `Bun.serve()`. It returns
716
- the Bun server instance.
715
+ `createServer(config)` builds the router and starts `Bun.serve()`. It returns a
716
+ managed handle with `url`, `port`, the concrete server under `runtime`, live
717
+ `status`, and one idempotent `shutdown()` lifecycle.
717
718
 
718
719
  ```ts
719
720
  import { createServer } from 'stitchkit/server'
@@ -746,8 +747,34 @@ server. See [Testing & deployment](./testing-and-deployment.md).
746
747
  | `logging` | `true` for built-in request logs, or a `LoggingConfig` (see below) |
747
748
  | `traceId` | override per-request trace-id resolution — may return `undefined` to fall back |
748
749
  | `wrapFetch` | compose wrappers around the finished handler (request context, audit) |
749
- | `websocket` | Bun WebSocket handlers e.g. from `createSocketIOServer` |
750
- | `routes` / `development` / `bun` | passthrough to `Bun.serve` |
750
+ | `socket` | full Stitchkit Socket.IO handle; route, default WebSocket handler and shutdown are mounted once |
751
+ | `websocket` | custom Bun WebSocket handler; with `socket`, this is the explicit composed handler |
752
+ | `development` / `bun` | passthrough to `Bun.serve` |
753
+
754
+ Native Bun `routes` are intentionally not accepted: Bun matches them before
755
+ `fetch`, so they could bypass shutdown admission. Use `rawRoutes`; they retain
756
+ the Fetch `Request → Response` model and participate in lifecycle tracking.
757
+
758
+ ### Managed shutdown
759
+
760
+ ```ts
761
+ const server = createServer({ services, socket })
762
+
763
+ const result = await server.shutdown({
764
+ gracePeriodMs: 30_000,
765
+ retryAfterSeconds: 5,
766
+ signal: shutdownController.signal,
767
+ })
768
+ ```
769
+
770
+ The first call closes HTTP and Socket.IO admission, drains accepted application
771
+ requests, closes realtime transports and stops the runtime within one total
772
+ budget. Repeated calls return the same Promise; the first options win. New
773
+ ordinary HTTP work receives `503`, `Retry-After` and `Connection: close` outside
774
+ `wrapFetch`. `result.outcome` is `clean` or `forced`; a forced result preserves
775
+ the pending snapshot and reason while final pending counters describe the
776
+ post-close transport state. `runtime` is a diagnostics escape hatch, not a
777
+ second canonical stop path.
751
778
 
752
779
  ### Trusted HTTPS in development
753
780
 
@@ -1561,6 +1588,15 @@ attempt, so the default `2` permits at most three total attempts. The default
1561
1588
  `statusCodes: []` does not replay HTTP responses; explicit `methods` or
1562
1589
  `statusCodes` expand that policy when a project has a proven idempotent case.
1563
1590
 
1591
+ Inside Next.js 16 server rendering, the first attempt still uses Next's normal
1592
+ request memoization. If Ky authorizes a retry after a network rejection,
1593
+ Stitchkit passes that retry's current `Request.signal` in the second fetch
1594
+ argument and materializes the current Request as URL + init. Next 16.3 otherwise
1595
+ merges `init` into a Request before its dedupe layer and loses the explicit
1596
+ signal opt-out. The retry therefore performs a new network attempt instead of
1597
+ returning the cached rejection. This adapter does not broaden retry policy: POST, unconfigured HTTP
1598
+ statuses, cancellation and exhausted budgets retain the rules above.
1599
+
1564
1600
  ## `createClient`
1565
1601
 
1566
1602
  ```ts
@@ -3103,13 +3139,12 @@ realtime.onConnection(({ raw, events, to }) => {
3103
3139
  })
3104
3140
  ```
3105
3141
 
3106
- It returns a handle with three pieces, all wired into `createServer`:
3142
+ Pass the full handle to `createServer`; it mounts and owns the transport once:
3107
3143
 
3108
3144
  ```ts
3109
3145
  createServer({
3110
3146
  services,
3111
- websocket: socket.websocket, // → Bun.serve websocket handlers
3112
- rawRoutes: [socket.route], // ready-made /socket.io/*socketPath route
3147
+ socket,
3113
3148
  })
3114
3149
 
3115
3150
  // elsewhere — validated broadcast:
@@ -3129,11 +3164,15 @@ export function publishExampleNote(realtime: ExampleRealtimePublisher): void {
3129
3164
  | Handle field | Purpose |
3130
3165
  |--------------|---------|
3131
3166
  | `io` | raw Socket.IO server for middleware, handshake auth and transport ownership |
3132
- | `websocket` | Bun WebSocket handlers pass to `createServer({ websocket })` |
3133
- | `route` | the `/socket.io/*socketPath` raw route pass to `createServer({ rawRoutes })` |
3134
-
3135
- `SocketIOServerConfig` also takes `path`, `transports`, `pingTimeout` and
3136
- `pingInterval`. For anything else socket.io's `ServerOptions` exposes, use the
3167
+ | `websocket` | Bun handlers used directly only in explicit raw-lane composition |
3168
+ | `route` | `/socket.io/*socketPath`, mounted automatically by `createServer({ socket })` |
3169
+ | `close()` | idempotent standalone close for CLI/tools with no HTTP server |
3170
+ | `beginShutdown()` / `connections()` | lifecycle surface consumed by the managed server |
3171
+
3172
+ `SocketIOServerConfig` also takes `path`, `transports`, `pingTimeout`,
3173
+ `pingInterval` and a runtime-neutral `allowRequest(Request)` handshake policy.
3174
+ The policy is composed with managed-shutdown admission on both Bun and Node.
3175
+ For anything else socket.io's `ServerOptions` exposes, use the
3137
3176
  typed **`serverOptions`** passthrough — most often `maxHttpBufferSize` to lift the
3138
3177
  1 MB default for large emits:
3139
3178
 
@@ -3144,7 +3183,8 @@ await createSocketIOServer({
3144
3183
  })
3145
3184
  ```
3146
3185
 
3147
- The wrapper-owned fields (`cors` / `path` / `transports` / `ping*`) take
3186
+ The wrapper-owned fields (`cors` / `path` / `transports` / `ping*` /
3187
+ `allowRequest`) take
3148
3188
  precedence over the same keys in `serverOptions`. On Bun the engine-level options
3149
3189
  (`maxHttpBufferSize`, the ping heartbeat, `upgradeTimeout`) are forwarded to
3150
3190
  `@socket.io/bun-engine` too — so a configured `maxHttpBufferSize` actually applies
@@ -3441,8 +3481,9 @@ const websocket = composeWebSocketHandlers(
3441
3481
 
3442
3482
  createServer({
3443
3483
  services,
3484
+ socket,
3444
3485
  websocket,
3445
- rawRoutes: [socket.route, pcmRoute],
3486
+ rawRoutes: [pcmRoute],
3446
3487
  })
3447
3488
  ```
3448
3489
 
@@ -4590,8 +4631,34 @@ createServer({
4590
4631
  })
4591
4632
  ```
4592
4633
 
4593
- `createServer` returns the `Bun.serve` instance keep the reference if you need
4594
- `.stop()` for a graceful shutdown.
4634
+ Keep the managed handle and wire process policy explicitly:
4635
+
4636
+ ```ts
4637
+ const server = createServer({ services, socket })
4638
+ const force = new AbortController()
4639
+ let closing: Promise<void> | undefined
4640
+
4641
+ function shutdown() {
4642
+ if (closing) {
4643
+ force.abort() // a later signal shortens the same shutdown, not a second chain
4644
+ return closing
4645
+ }
4646
+ closing = server.shutdown({ gracePeriodMs: 30_000, signal: force.signal }).then(async result => {
4647
+ await mcp.close()
4648
+ await prisma.$disconnect()
4649
+ console.log(result)
4650
+ })
4651
+ return closing
4652
+ }
4653
+
4654
+ process.on('SIGTERM', () => void shutdown())
4655
+ process.on('SIGINT', () => void shutdown())
4656
+ ```
4657
+
4658
+ The server owns HTTP/Socket.IO transport resources. MCP, databases, queues and
4659
+ domain run-state remain application resources and close explicitly after server
4660
+ drain. Do not call `runtime.stop()` or `socket.io.close()` in parallel with
4661
+ `shutdown()`.
4595
4662
 
4596
4663
  ### Deploy on Node
4597
4664
 
@@ -4602,10 +4669,13 @@ the listener differs: replace `createServer` with **`serveNode`** (from
4602
4669
  ```ts
4603
4670
  import { serveNode } from 'stitchkit/node'
4604
4671
 
4605
- serveNode({
4672
+ const server = await serveNode({
4606
4673
  services,
4674
+ socket,
4607
4675
  port: Number(process.env.PORT ?? 3000),
4608
4676
  })
4677
+
4678
+ await server.shutdown({ gracePeriodMs: 30_000 })
4609
4679
  ```
4610
4680
 
4611
4681
  Notes for a Node host:
@@ -4614,9 +4684,6 @@ Notes for a Node host:
4614
4684
  raw routes use `RawRoute<TServer = unknown>`; supply a host server generic
4615
4685
  only when an embedding adapter passes one to `createHandler`.
4616
4686
 
4617
- - Add **`@types/bun`** as a dev dependency — it is an optional peer that types the
4618
- shared `stitchkit/server` surface (without it `tsc` reports a missing `Bun`
4619
- namespace).
4620
4687
  - **Socket.IO** attaches to the Node HTTP server via `serveNode({ socket })`, and
4621
4688
  on Node the default transport is `['websocket']` — set the client to match
4622
4689
  (`transports: ['websocket']`). See [realtime](./realtime.md).
@@ -5626,9 +5693,68 @@ socket.io
5626
5693
  socket.attach(nodeHttpServer)
5627
5694
  ```
5628
5695
 
5629
- Inline Bun routes passed to `createServer` continue to infer `BunServer`; no
5630
- annotation is needed. Node consumers can remove `@types/bun` unless another
5631
- dependency independently requires it.
5696
+ Node consumers can remove `@types/bun` unless another dependency independently
5697
+ requires it.
5698
+
5699
+ ### Managed server shutdown
5700
+
5701
+ `createServer()` and `serveNode()` now return the same structural managed
5702
+ lifecycle. Replace every direct runtime stop and parallel Socket.IO close:
5703
+
5704
+ ```ts
5705
+ // before — split ownership
5706
+ const socket = await createSocketIOServer(config)
5707
+ const server = createServer({
5708
+ services,
5709
+ websocket: socket.websocket,
5710
+ rawRoutes: [socket.route],
5711
+ })
5712
+ server.stop()
5713
+ await socket.io.close()
5714
+
5715
+ // after — one owner and one total deadline
5716
+ const socket = await createSocketIOServer(config)
5717
+ const server = createServer({ services, socket })
5718
+ const result = await server.shutdown({ gracePeriodMs: 30_000 })
5719
+ ```
5720
+
5721
+ On Node, keep the same `socket` field and replace `handle.close()` with
5722
+ `handle.shutdown()`. Runtime-specific diagnostics move under `handle.runtime`;
5723
+ do not use it as a second shutdown path. Standalone CLI/tools that create a
5724
+ Socket.IO handle without an HTTP server call `await socket.close()`.
5725
+
5726
+ If Bun Socket.IO shares the port with a raw lane, keep the explicit composition
5727
+ but let the server mount the Socket.IO route:
5728
+
5729
+ ```ts
5730
+ createServer({
5731
+ services,
5732
+ socket,
5733
+ websocket: composeWebSocketHandlers([
5734
+ webSocketLane({ match: isRaw, handlers: rawHandlers }),
5735
+ socketIoLane(socket.websocket),
5736
+ ]),
5737
+ rawRoutes: [rawUpgradeRoute],
5738
+ })
5739
+ ```
5740
+
5741
+ Move native Bun `routes` entries to `rawRoutes`. Native routes run before the
5742
+ Fetch handler and therefore cannot participate in admission or drain. Wire
5743
+ `SIGTERM`/`SIGINT` in the application; the first signal starts `shutdown()`, and
5744
+ a later signal may abort the same controller. Close MCP, databases and queues
5745
+ after the server result—those resources remain application-owned.
5746
+
5747
+ Move a handshake policy from the Node-only callback shape inside
5748
+ `serverOptions` to the runtime-neutral top-level policy. It receives a Web
5749
+ `Request`, may be async, and returns whether to admit the handshake:
5750
+
5751
+ ```ts
5752
+ // before
5753
+ serverOptions: { allowRequest: (request, done) => done(null, allowed(request)) }
5754
+
5755
+ // after
5756
+ allowRequest: (request) => allowed(request)
5757
+ ```
5632
5758
 
5633
5759
  ## Your handlers may be returning more than the contract declares
5634
5760
 
@@ -5718,7 +5844,7 @@ The browser-and-server entrypoint. Re-exports everything from
5718
5844
  | `ContractClientConfig` | _type_ | per-tenant / resource-scoped client config — dynamic `pathPrefix` + `stripPrefixKeys` ([guide](../guide/client.md#contractclientconfig--per-tenant--resource-scoped-clients)) |
5719
5845
  | `contractEndpointMatchers` | function | compile exact pathname matchers for selected HTTP contract operations and expected-401 policy |
5720
5846
  | `PathPrefixArgs` | _type_ | required string-valued keys exposed to a typed dynamic `pathPrefix` callback |
5721
- | `createHttpClient` | function | the Ky-based HTTP transport — [guide](../guide/client.md#createhttpclient) |
5847
+ | `createHttpClient` | function | the Ky-based HTTP transport; on Next.js SSR its first attempt stays request-memoizable while every retry is a distinct transport attempt — [guide](../guide/client.md#createhttpclient) |
5722
5848
  | `ApiError` | class | a non-2xx response, with `code` / `status` / `details` / `hint` and optional readonly `traceId` from `x-request-id` |
5723
5849
  | `HttpClient` | _type_ | the transport interface `createClient` builds on |
5724
5850
  | `ConfiguredHttpClient` | _type_ | a framework-created `HttpClient` carrying its readonly `baseUrl` for URL builders |
@@ -5897,6 +6023,12 @@ Also re-exports the error helpers from `stitchkit/contract`.
5897
6023
  | `parseBody` | function | parse + Zod-validate a JSON body → `data` or `null` (no throw) |
5898
6024
  | `HandlerConfig` | _type_ | config for `createHandler`, including optional `maxJsonBodyBytes`; bound to `BunServer` on this entrypoint |
5899
6025
  | `BunServerConfig` | _type_ | config for `createServer` (Bun) |
6026
+ | `BunServerHandle` | _type_ | managed Bun handle (`url`, `port`, `runtime`, `status`, `shutdown`) |
6027
+ | `ManagedServerHandle` | _type_ | shared lifecycle shape generic over the runtime escape hatch |
6028
+ | `ShutdownOptionsSchema` / `ShutdownOptions` | schema / _type_ | one grace budget, retry hint and optional external abort signal |
6029
+ | `ShutdownStatusSchema` / `ShutdownStatus` | schema / _type_ | live state and request/WebSocket counters |
6030
+ | `ShutdownResultSchema` / `ShutdownResult` | schema / _type_ | clean/forced result with final counters and at-force snapshots |
6031
+ | `ShutdownStateSchema` / `ShutdownState` | schema / _type_ | managed lifecycle state machine |
5900
6032
  | `ServiceDef` | _type_ | the result of `implement` |
5901
6033
  | `MethodDef` | _type_ | one resolved endpoint inside a service |
5902
6034
  | `OperationIdentity` | _type_ | path-free service/action/scope/method identity shared by contract and native tool operations |
@@ -5962,8 +6094,10 @@ Also re-exports the error helpers from `stitchkit/contract`.
5962
6094
  | `RealtimeServer` | _type_ | validated broadcast and connection API inferred from a realtime contract |
5963
6095
  | `RealtimeServerConnection` | _type_ | one validated connection with raw socket access for auth and rooms |
5964
6096
  | `RealtimeServerHandle` | _type_ | minimal Socket.IO server handle accepted by `bindRealtimeServer` |
6097
+ | `SocketIORequestPolicy` | _type_ | runtime-neutral async-capable Web `Request` handshake admission policy |
5965
6098
  | `SocketIOServerConfig` | _type_ | config for `createSocketIOServer` |
5966
- | `SocketIOServerHandle` | _type_ | the `{ io, websocket, route }` handle |
6099
+ | `SocketIOServerHandle` | _type_ | typed Socket.IO server plus Bun mount fields and idempotent lifecycle |
6100
+ | `SocketIOServerLifecycle` | _type_ | non-generic Bun mount/shutdown portion accepted by `createServer` |
5967
6101
  | `composeWebSocketHandlers` | function | compose one Bun `websocket` from N lanes — a raw binary lane beside Socket.IO ([guide](../guide/realtime.md#raw-binary-lane-bun)) |
5968
6102
  | `webSocketLane` | function | a typed, cast-free lane for `composeWebSocketHandlers` |
5969
6103
  | `socketIoLane` | function | the Socket.IO catch-all lane for `composeWebSocketHandlers` |
@@ -6292,9 +6426,11 @@ runtime-agnostic pieces of `stitchkit/server` and the error helpers.
6292
6426
  | `createSocketIOServer` | function | the typed Node Socket.IO server (`io` + `attach`; no Bun engine declarations) |
6293
6427
  | `implement` / `createImplement` | function | bind a contract to typed handlers (same as `/server`) |
6294
6428
  | `NodeServerConfig` | _type_ | config for `serveNode` |
6295
- | `NodeServerHandle` | _type_ | the `serveNode` handle (`{ port, stop }`) |
6429
+ | `NodeServerHandle` | _type_ | managed Node handle (`url`, `port`, `runtime`, `status`, `shutdown`) |
6430
+ | `NodeRuntimeServer` | _type_ | concrete `srvx/node` runtime escape hatch |
6431
+ | `NodeSocketLifecycle` | _type_ | Bun-free Socket.IO lifecycle accepted by `serveNode` |
6296
6432
  | `HandlerConfig` / `ServiceDef` / `RawRoute` / `RawRouteContext` | _type_ | runtime-neutral handler types; raw routes default their host server to `unknown` |
6297
- | `SocketIOServerConfig` / `SocketIOServerHandle` | _type_ | shared config and the Node-only `{ io, attach }` handle |
6433
+ | `SocketIORequestPolicy` / `SocketIOServerConfig` / `SocketIOServerHandle` | _type_ | runtime-neutral handshake policy, config and Bun-free Node handle with `io`, `attach` and lifecycle |
6298
6434
  | `AppError` + `appError` / `badRequest` / `unauthorized` / `forbidden` / `notFound` / `conflict` / `rateLimited` | — | error helpers (same as `/contract`) |
6299
6435
 
6300
6436
  ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stitchkit",
3
- "version": "0.48.1",
3
+ "version": "0.49.0",
4
4
  "description": "Contract-first backend framework — one defineContract() into an HTTP API, MCP tools, AI-agent tools and a typed client. Bun and Node.",
5
5
  "keywords": [
6
6
  "bun",
@@ -95,6 +95,7 @@
95
95
  "prepack": "cp ../../README.md ./README.md && bun ../../scripts/gen-llms.ts && bun run build",
96
96
  "test": "bun test",
97
97
  "smoke:node": "node scripts/node-smoke.mjs",
98
+ "smoke:next-ssr": "node scripts/next-ssr-retry-smoke.mjs",
98
99
  "consumer-lane": "bun scripts/consumer-lane/run.mjs",
99
100
  "bench:mcp-preparation": "bun scripts/benchmark-mcp-preparation.ts"
100
101
  },