stitchkit 0.48.1 → 0.49.1

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,39 @@ 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
+ forceTimeoutMs: 5_000,
766
+ retryAfterSeconds: 5,
767
+ signal: shutdownController.signal,
768
+ })
769
+ ```
770
+
771
+ The first call closes HTTP and Socket.IO admission, then gives the complete
772
+ graceful request/realtime/runtime chain one `gracePeriodMs` budget. If that
773
+ budget or the external signal forces destructive teardown, `forceTimeoutMs`
774
+ bounds physical completion separately. Repeated calls return the same Promise;
775
+ the first options win. New
776
+ ordinary HTTP work receives `503`, `Retry-After` and `Connection: close` outside
777
+ `wrapFetch`. `result.outcome` is `clean` or `forced`; a forced result preserves
778
+ the pending snapshot and reason while final pending counters describe the
779
+ post-close transport state. A graceful phase error still runs forced cleanup and
780
+ then rejects with the original error; a forced transport that cannot confirm
781
+ completion before `forceTimeoutMs` rejects instead of reporting a false zero.
782
+ `runtime` is a diagnostics escape hatch, not a second canonical stop path.
751
783
 
752
784
  ### Trusted HTTPS in development
753
785
 
@@ -1561,6 +1593,15 @@ attempt, so the default `2` permits at most three total attempts. The default
1561
1593
  `statusCodes: []` does not replay HTTP responses; explicit `methods` or
1562
1594
  `statusCodes` expand that policy when a project has a proven idempotent case.
1563
1595
 
1596
+ Inside Next.js 16 server rendering, the first attempt still uses Next's normal
1597
+ request memoization. If Ky authorizes a retry after a network rejection,
1598
+ Stitchkit passes that retry's current `Request.signal` in the second fetch
1599
+ argument and materializes the current Request as URL + init. Next 16.3 otherwise
1600
+ merges `init` into a Request before its dedupe layer and loses the explicit
1601
+ signal opt-out. The retry therefore performs a new network attempt instead of
1602
+ returning the cached rejection. This adapter does not broaden retry policy: POST, unconfigured HTTP
1603
+ statuses, cancellation and exhausted budgets retain the rules above.
1604
+
1564
1605
  ## `createClient`
1565
1606
 
1566
1607
  ```ts
@@ -3103,13 +3144,12 @@ realtime.onConnection(({ raw, events, to }) => {
3103
3144
  })
3104
3145
  ```
3105
3146
 
3106
- It returns a handle with three pieces, all wired into `createServer`:
3147
+ Pass the full handle to `createServer`; it mounts and owns the transport once:
3107
3148
 
3108
3149
  ```ts
3109
3150
  createServer({
3110
3151
  services,
3111
- websocket: socket.websocket, // → Bun.serve websocket handlers
3112
- rawRoutes: [socket.route], // ready-made /socket.io/*socketPath route
3152
+ socket,
3113
3153
  })
3114
3154
 
3115
3155
  // elsewhere — validated broadcast:
@@ -3129,11 +3169,15 @@ export function publishExampleNote(realtime: ExampleRealtimePublisher): void {
3129
3169
  | Handle field | Purpose |
3130
3170
  |--------------|---------|
3131
3171
  | `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
3172
+ | `websocket` | Bun handlers used directly only in explicit raw-lane composition |
3173
+ | `route` | `/socket.io/*socketPath`, mounted automatically by `createServer({ socket })` |
3174
+ | `close()` | idempotent standalone close for CLI/tools with no HTTP server |
3175
+ | `beginShutdown()` / `connections()` | lifecycle surface consumed by the managed server |
3176
+
3177
+ `SocketIOServerConfig` also takes `path`, `transports`, `pingTimeout`,
3178
+ `pingInterval` and a runtime-neutral `allowRequest(Request)` handshake policy.
3179
+ The policy is composed with managed-shutdown admission on both Bun and Node.
3180
+ For anything else socket.io's `ServerOptions` exposes, use the
3137
3181
  typed **`serverOptions`** passthrough — most often `maxHttpBufferSize` to lift the
3138
3182
  1 MB default for large emits:
3139
3183
 
@@ -3144,7 +3188,8 @@ await createSocketIOServer({
3144
3188
  })
3145
3189
  ```
3146
3190
 
3147
- The wrapper-owned fields (`cors` / `path` / `transports` / `ping*`) take
3191
+ The wrapper-owned fields (`cors` / `path` / `transports` / `ping*` /
3192
+ `allowRequest`) take
3148
3193
  precedence over the same keys in `serverOptions`. On Bun the engine-level options
3149
3194
  (`maxHttpBufferSize`, the ping heartbeat, `upgradeTimeout`) are forwarded to
3150
3195
  `@socket.io/bun-engine` too — so a configured `maxHttpBufferSize` actually applies
@@ -3441,8 +3486,9 @@ const websocket = composeWebSocketHandlers(
3441
3486
 
3442
3487
  createServer({
3443
3488
  services,
3489
+ socket,
3444
3490
  websocket,
3445
- rawRoutes: [socket.route, pcmRoute],
3491
+ rawRoutes: [pcmRoute],
3446
3492
  })
3447
3493
  ```
3448
3494
 
@@ -4590,8 +4636,38 @@ createServer({
4590
4636
  })
4591
4637
  ```
4592
4638
 
4593
- `createServer` returns the `Bun.serve` instance keep the reference if you need
4594
- `.stop()` for a graceful shutdown.
4639
+ Keep the managed handle and wire process policy explicitly:
4640
+
4641
+ ```ts
4642
+ const server = createServer({ services, socket })
4643
+ const force = new AbortController()
4644
+ let closing: Promise<void> | undefined
4645
+
4646
+ function shutdown() {
4647
+ if (closing) {
4648
+ force.abort() // a later signal shortens the same shutdown, not a second chain
4649
+ return closing
4650
+ }
4651
+ closing = server.shutdown({
4652
+ gracePeriodMs: 30_000,
4653
+ forceTimeoutMs: 5_000,
4654
+ signal: force.signal,
4655
+ }).then(async result => {
4656
+ await mcp.close()
4657
+ await prisma.$disconnect()
4658
+ console.log(result)
4659
+ })
4660
+ return closing
4661
+ }
4662
+
4663
+ process.on('SIGTERM', () => void shutdown())
4664
+ process.on('SIGINT', () => void shutdown())
4665
+ ```
4666
+
4667
+ The server owns HTTP/Socket.IO transport resources. MCP, databases, queues and
4668
+ domain run-state remain application resources and close explicitly after server
4669
+ drain. Do not call `runtime.stop()` or `socket.io.close()` in parallel with
4670
+ `shutdown()`.
4595
4671
 
4596
4672
  ### Deploy on Node
4597
4673
 
@@ -4602,10 +4678,13 @@ the listener differs: replace `createServer` with **`serveNode`** (from
4602
4678
  ```ts
4603
4679
  import { serveNode } from 'stitchkit/node'
4604
4680
 
4605
- serveNode({
4681
+ const server = await serveNode({
4606
4682
  services,
4683
+ socket,
4607
4684
  port: Number(process.env.PORT ?? 3000),
4608
4685
  })
4686
+
4687
+ await server.shutdown({ gracePeriodMs: 30_000 })
4609
4688
  ```
4610
4689
 
4611
4690
  Notes for a Node host:
@@ -4614,9 +4693,6 @@ Notes for a Node host:
4614
4693
  raw routes use `RawRoute<TServer = unknown>`; supply a host server generic
4615
4694
  only when an embedding adapter passes one to `createHandler`.
4616
4695
 
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
4696
  - **Socket.IO** attaches to the Node HTTP server via `serveNode({ socket })`, and
4621
4697
  on Node the default transport is `['websocket']` — set the client to match
4622
4698
  (`transports: ['websocket']`). See [realtime](./realtime.md).
@@ -5626,9 +5702,68 @@ socket.io
5626
5702
  socket.attach(nodeHttpServer)
5627
5703
  ```
5628
5704
 
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.
5705
+ Node consumers can remove `@types/bun` unless another dependency independently
5706
+ requires it.
5707
+
5708
+ ### Managed server shutdown
5709
+
5710
+ `createServer()` and `serveNode()` now return the same structural managed
5711
+ lifecycle. Replace every direct runtime stop and parallel Socket.IO close:
5712
+
5713
+ ```ts
5714
+ // before — split ownership
5715
+ const socket = await createSocketIOServer(config)
5716
+ const server = createServer({
5717
+ services,
5718
+ websocket: socket.websocket,
5719
+ rawRoutes: [socket.route],
5720
+ })
5721
+ server.stop()
5722
+ await socket.io.close()
5723
+
5724
+ // after — one owner and one total deadline
5725
+ const socket = await createSocketIOServer(config)
5726
+ const server = createServer({ services, socket })
5727
+ const result = await server.shutdown({ gracePeriodMs: 30_000 })
5728
+ ```
5729
+
5730
+ On Node, keep the same `socket` field and replace `handle.close()` with
5731
+ `handle.shutdown()`. Runtime-specific diagnostics move under `handle.runtime`;
5732
+ do not use it as a second shutdown path. Standalone CLI/tools that create a
5733
+ Socket.IO handle without an HTTP server call `await socket.close()`.
5734
+
5735
+ If Bun Socket.IO shares the port with a raw lane, keep the explicit composition
5736
+ but let the server mount the Socket.IO route:
5737
+
5738
+ ```ts
5739
+ createServer({
5740
+ services,
5741
+ socket,
5742
+ websocket: composeWebSocketHandlers([
5743
+ webSocketLane({ match: isRaw, handlers: rawHandlers }),
5744
+ socketIoLane(socket.websocket),
5745
+ ]),
5746
+ rawRoutes: [rawUpgradeRoute],
5747
+ })
5748
+ ```
5749
+
5750
+ Move native Bun `routes` entries to `rawRoutes`. Native routes run before the
5751
+ Fetch handler and therefore cannot participate in admission or drain. Wire
5752
+ `SIGTERM`/`SIGINT` in the application; the first signal starts `shutdown()`, and
5753
+ a later signal may abort the same controller. Close MCP, databases and queues
5754
+ after the server result—those resources remain application-owned.
5755
+
5756
+ Move a handshake policy from the Node-only callback shape inside
5757
+ `serverOptions` to the runtime-neutral top-level policy. It receives a Web
5758
+ `Request`, may be async, and returns whether to admit the handshake:
5759
+
5760
+ ```ts
5761
+ // before
5762
+ serverOptions: { allowRequest: (request, done) => done(null, allowed(request)) }
5763
+
5764
+ // after
5765
+ allowRequest: (request) => allowed(request)
5766
+ ```
5632
5767
 
5633
5768
  ## Your handlers may be returning more than the contract declares
5634
5769
 
@@ -5718,7 +5853,7 @@ The browser-and-server entrypoint. Re-exports everything from
5718
5853
  | `ContractClientConfig` | _type_ | per-tenant / resource-scoped client config — dynamic `pathPrefix` + `stripPrefixKeys` ([guide](../guide/client.md#contractclientconfig--per-tenant--resource-scoped-clients)) |
5719
5854
  | `contractEndpointMatchers` | function | compile exact pathname matchers for selected HTTP contract operations and expected-401 policy |
5720
5855
  | `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) |
5856
+ | `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
5857
  | `ApiError` | class | a non-2xx response, with `code` / `status` / `details` / `hint` and optional readonly `traceId` from `x-request-id` |
5723
5858
  | `HttpClient` | _type_ | the transport interface `createClient` builds on |
5724
5859
  | `ConfiguredHttpClient` | _type_ | a framework-created `HttpClient` carrying its readonly `baseUrl` for URL builders |
@@ -5897,6 +6032,12 @@ Also re-exports the error helpers from `stitchkit/contract`.
5897
6032
  | `parseBody` | function | parse + Zod-validate a JSON body → `data` or `null` (no throw) |
5898
6033
  | `HandlerConfig` | _type_ | config for `createHandler`, including optional `maxJsonBodyBytes`; bound to `BunServer` on this entrypoint |
5899
6034
  | `BunServerConfig` | _type_ | config for `createServer` (Bun) |
6035
+ | `BunServerHandle` | _type_ | managed Bun handle (`url`, `port`, `runtime`, `status`, `shutdown`) |
6036
+ | `ManagedServerHandle` | _type_ | shared lifecycle shape generic over the runtime escape hatch |
6037
+ | `ShutdownOptionsSchema` / `ShutdownOptions` | schema / _type_ | one graceful budget, bounded forced-completion timeout, retry hint and optional external abort signal |
6038
+ | `ShutdownStatusSchema` / `ShutdownStatus` | schema / _type_ | live state and request/WebSocket counters |
6039
+ | `ShutdownResultSchema` / `ShutdownResult` | schema / _type_ | clean/forced result with final counters and at-force snapshots |
6040
+ | `ShutdownStateSchema` / `ShutdownState` | schema / _type_ | managed lifecycle state machine |
5900
6041
  | `ServiceDef` | _type_ | the result of `implement` |
5901
6042
  | `MethodDef` | _type_ | one resolved endpoint inside a service |
5902
6043
  | `OperationIdentity` | _type_ | path-free service/action/scope/method identity shared by contract and native tool operations |
@@ -5962,8 +6103,10 @@ Also re-exports the error helpers from `stitchkit/contract`.
5962
6103
  | `RealtimeServer` | _type_ | validated broadcast and connection API inferred from a realtime contract |
5963
6104
  | `RealtimeServerConnection` | _type_ | one validated connection with raw socket access for auth and rooms |
5964
6105
  | `RealtimeServerHandle` | _type_ | minimal Socket.IO server handle accepted by `bindRealtimeServer` |
6106
+ | `SocketIORequestPolicy` | _type_ | runtime-neutral async-capable Web `Request` handshake admission policy |
5965
6107
  | `SocketIOServerConfig` | _type_ | config for `createSocketIOServer` |
5966
- | `SocketIOServerHandle` | _type_ | the `{ io, websocket, route }` handle |
6108
+ | `SocketIOServerHandle` | _type_ | typed Socket.IO server plus Bun mount fields and idempotent lifecycle |
6109
+ | `SocketIOServerLifecycle` | _type_ | non-generic Bun mount/shutdown portion accepted by `createServer` |
5967
6110
  | `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
6111
  | `webSocketLane` | function | a typed, cast-free lane for `composeWebSocketHandlers` |
5969
6112
  | `socketIoLane` | function | the Socket.IO catch-all lane for `composeWebSocketHandlers` |
@@ -6292,9 +6435,11 @@ runtime-agnostic pieces of `stitchkit/server` and the error helpers.
6292
6435
  | `createSocketIOServer` | function | the typed Node Socket.IO server (`io` + `attach`; no Bun engine declarations) |
6293
6436
  | `implement` / `createImplement` | function | bind a contract to typed handlers (same as `/server`) |
6294
6437
  | `NodeServerConfig` | _type_ | config for `serveNode` |
6295
- | `NodeServerHandle` | _type_ | the `serveNode` handle (`{ port, stop }`) |
6438
+ | `NodeServerHandle` | _type_ | managed Node handle (`url`, `port`, `runtime`, `status`, `shutdown`) |
6439
+ | `NodeRuntimeServer` | _type_ | concrete `srvx/node` runtime escape hatch |
6440
+ | `NodeSocketLifecycle` | _type_ | Bun-free Socket.IO lifecycle accepted by `serveNode` |
6296
6441
  | `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 |
6442
+ | `SocketIORequestPolicy` / `SocketIOServerConfig` / `SocketIOServerHandle` | _type_ | runtime-neutral handshake policy, config and Bun-free Node handle with `io`, `attach` and lifecycle |
6298
6443
  | `AppError` + `appError` / `badRequest` / `unauthorized` / `forbidden` / `notFound` / `conflict` / `rateLimited` | — | error helpers (same as `/contract`) |
6299
6444
 
6300
6445
  ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stitchkit",
3
- "version": "0.48.1",
3
+ "version": "0.49.1",
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
  },