stitchkit 0.49.0 → 0.49.2

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.
@@ -1409,6 +1409,7 @@ var ShutdownStateSchema = z.enum([
1409
1409
  ]);
1410
1410
  var ShutdownOptionsSchema = z.object({
1411
1411
  gracePeriodMs: z.number().int().nonnegative().default(30000),
1412
+ forceTimeoutMs: z.number().int().nonnegative().default(5000),
1412
1413
  retryAfterSeconds: z.number().int().nonnegative().default(5),
1413
1414
  signal: z.custom((value) => typeof value === "object" && value !== null && ("aborted" in value) && ("addEventListener" in value), "Expected an AbortSignal").optional()
1414
1415
  });
@@ -1461,6 +1462,18 @@ function untilAbort(signal) {
1461
1462
  return Promise.resolve();
1462
1463
  return new Promise((resolve) => signal.addEventListener("abort", () => resolve(), { once: true }));
1463
1464
  }
1465
+ function withTimeout(promise, timeoutMs, message) {
1466
+ return new Promise((resolve, reject) => {
1467
+ const timer = setTimeout(() => reject(new Error(message)), timeoutMs);
1468
+ promise.then((value) => {
1469
+ clearTimeout(timer);
1470
+ resolve(value);
1471
+ }, (error) => {
1472
+ clearTimeout(timer);
1473
+ reject(error);
1474
+ });
1475
+ });
1476
+ }
1464
1477
  function createServerLifecycle(getAdapter) {
1465
1478
  let state = "running";
1466
1479
  let acceptedRequests = 0;
@@ -1504,6 +1517,7 @@ function createServerLifecycle(getAdapter) {
1504
1517
  shutdownPromise = new Promise((resolve, reject) => {
1505
1518
  const phaseAbort = new AbortController;
1506
1519
  let forcedReason;
1520
+ let phaseError;
1507
1521
  const force = (reason) => {
1508
1522
  if (forcedReason)
1509
1523
  return;
@@ -1520,28 +1534,21 @@ function createServerLifecycle(getAdapter) {
1520
1534
  parsed.signal?.removeEventListener("abort", onExternalAbort);
1521
1535
  };
1522
1536
  (async () => {
1523
- await waitForZero(() => pendingApplicationRequests, phaseAbort.signal);
1524
- if (!forcedReason) {
1525
- state = "closing-realtime";
1526
- const closeRealtime = adapter.closeRealtime();
1527
- await Promise.race([
1528
- closeRealtime.catch((error) => {
1529
- if (!forcedReason)
1530
- throw error;
1531
- }),
1532
- untilAbort(phaseAbort.signal)
1533
- ]);
1534
- }
1535
- if (!forcedReason) {
1536
- state = "stopping-runtime";
1537
- const stopGracefully = adapter.stopGracefully();
1538
- await Promise.race([
1539
- stopGracefully.catch((error) => {
1540
- if (!forcedReason)
1541
- throw error;
1542
- }),
1543
- untilAbort(phaseAbort.signal)
1544
- ]);
1537
+ try {
1538
+ await waitForZero(() => pendingApplicationRequests, phaseAbort.signal);
1539
+ if (!forcedReason) {
1540
+ state = "closing-realtime";
1541
+ await Promise.race([adapter.closeRealtime(), untilAbort(phaseAbort.signal)]);
1542
+ }
1543
+ if (!forcedReason) {
1544
+ state = "stopping-runtime";
1545
+ await Promise.race([adapter.stopGracefully(), untilAbort(phaseAbort.signal)]);
1546
+ }
1547
+ } catch (error) {
1548
+ if (!forcedReason) {
1549
+ phaseError = error;
1550
+ force("error");
1551
+ }
1545
1552
  }
1546
1553
  let pendingRequestsAtForce = 0;
1547
1554
  let pendingWebSocketsAtForce = 0;
@@ -1549,8 +1556,24 @@ function createServerLifecycle(getAdapter) {
1549
1556
  state = "stopping-runtime";
1550
1557
  pendingRequestsAtForce = adapter.pendingRequests();
1551
1558
  pendingWebSocketsAtForce = adapter.pendingWebSockets();
1552
- await adapter.forceStop();
1553
- state = "forced";
1559
+ let forceError;
1560
+ let forceFailed = false;
1561
+ try {
1562
+ await withTimeout(adapter.forceStop(), parsed.forceTimeoutMs, `[stitchkit] forced shutdown did not complete within ${parsed.forceTimeoutMs}ms`);
1563
+ } catch (error) {
1564
+ forceFailed = true;
1565
+ forceError = error;
1566
+ } finally {
1567
+ state = "forced";
1568
+ }
1569
+ if (forcedReason === "error") {
1570
+ if (forceFailed) {
1571
+ throw new AggregateError([phaseError, forceError], "[stitchkit] graceful shutdown failed and forced cleanup also failed", { cause: phaseError });
1572
+ }
1573
+ throw phaseError;
1574
+ }
1575
+ if (forceFailed)
1576
+ throw forceError;
1554
1577
  } else {
1555
1578
  state = "clean";
1556
1579
  }
package/dist/node.js CHANGED
@@ -11,7 +11,7 @@ import {
11
11
  createSocketIOServer,
12
12
  implement,
13
13
  implementRegistry
14
- } from "./index-fjfzsq6y.js";
14
+ } from "./index-z992nfbx.js";
15
15
  import"./index-r1qp4rve.js";
16
16
  import {
17
17
  AppError,
@@ -1 +1 @@
1
- {"version":3,"file":"bun.d.ts","sourceRoot":"","sources":["../../src/server/bun.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,mBAAmB,EAEzB,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AAC3D,OAAO,KAAK,EACV,gBAAgB,EAChB,YAAY,EACZ,aAAa,EACb,QAAQ,EACR,eAAe,EAChB,MAAM,SAAS,CAAC;AAEjB,yEAAyE;AACzE,MAAM,MAAM,SAAS,GAAG,UAAU,CAAC,OAAO,GAAG,CAAC,KAAK,CAAC,CAAC;AACrD,MAAM,MAAM,WAAW,GAAG,QAAQ,CAAC,SAAS,CAAC,CAAC;AAC9C,MAAM,MAAM,kBAAkB,GAAG,eAAe,CAAC,SAAS,CAAC,CAAC;AAC5D,MAAM,MAAM,eAAe,GAAG,YAAY,CAAC,SAAS,CAAC,CAAC;AACtD,MAAM,MAAM,mBAAmB,GAAG,gBAAgB,CAAC,SAAS,CAAC,CAAC;AAC9D,MAAM,MAAM,gBAAgB,GAAG,aAAa,CAAC,SAAS,CAAC,CAAC;AAExD,KAAK,eAAe,GAAG,UAAU,CAAC,OAAO,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;AACvD,KAAK,oBAAoB,GAAG,GAAG,CAAC,gBAAgB,CAAC,OAAO,CAAC,CAAC;AAC1D,KAAK,qBAAqB,GAAG,eAAe,SAAS;IAAE,WAAW,CAAC,EAAE,MAAM,CAAC,CAAA;CAAE,GAAG,CAAC,GAAG,KAAK,CAAC;AAE3F,MAAM,MAAM,iBAAiB,GAAG,IAAI,CAClC,eAAe,EACf,OAAO,GAAG,MAAM,GAAG,UAAU,GAAG,MAAM,GAAG,QAAQ,GAAG,WAAW,GAAG,aAAa,CAChF,CAAC;AAEF,8EAA8E;AAC9E,MAAM,WAAW,eAAgB,SAAQ,gBAAgB,EAAE,mBAAmB;IAC5E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,oBAAoB,CAAC;IACjC,iGAAiG;IACjG,MAAM,CAAC,EAAE,uBAAuB,CAAC;IACjC,WAAW,CAAC,EAAE,qBAAqB,CAAC;IACpC,GAAG,CAAC,EAAE,iBAAiB,CAAC;CACzB;AAED,MAAM,MAAM,eAAe,GAAG,mBAAmB,CAAC,SAAS,CAAC,CAAC;AAE7D,qDAAqD;AACrD,wBAAgB,YAAY,CAAC,MAAM,EAAE,eAAe,GAAG,eAAe,CAqIrE"}
1
+ {"version":3,"file":"bun.d.ts","sourceRoot":"","sources":["../../src/server/bun.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,mBAAmB,EAEzB,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AAC3D,OAAO,KAAK,EACV,gBAAgB,EAChB,YAAY,EACZ,aAAa,EACb,QAAQ,EACR,eAAe,EAChB,MAAM,SAAS,CAAC;AAEjB,yEAAyE;AACzE,MAAM,MAAM,SAAS,GAAG,UAAU,CAAC,OAAO,GAAG,CAAC,KAAK,CAAC,CAAC;AACrD,MAAM,MAAM,WAAW,GAAG,QAAQ,CAAC,SAAS,CAAC,CAAC;AAC9C,MAAM,MAAM,kBAAkB,GAAG,eAAe,CAAC,SAAS,CAAC,CAAC;AAC5D,MAAM,MAAM,eAAe,GAAG,YAAY,CAAC,SAAS,CAAC,CAAC;AACtD,MAAM,MAAM,mBAAmB,GAAG,gBAAgB,CAAC,SAAS,CAAC,CAAC;AAC9D,MAAM,MAAM,gBAAgB,GAAG,aAAa,CAAC,SAAS,CAAC,CAAC;AAExD,KAAK,eAAe,GAAG,UAAU,CAAC,OAAO,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;AACvD,KAAK,oBAAoB,GAAG,GAAG,CAAC,gBAAgB,CAAC,OAAO,CAAC,CAAC;AAC1D,KAAK,qBAAqB,GAAG,eAAe,SAAS;IAAE,WAAW,CAAC,EAAE,MAAM,CAAC,CAAA;CAAE,GAAG,CAAC,GAAG,KAAK,CAAC;AAE3F,MAAM,MAAM,iBAAiB,GAAG,IAAI,CAClC,eAAe,EACf,OAAO,GAAG,MAAM,GAAG,UAAU,GAAG,MAAM,GAAG,QAAQ,GAAG,WAAW,GAAG,aAAa,CAChF,CAAC;AAEF,8EAA8E;AAC9E,MAAM,WAAW,eAAgB,SAAQ,gBAAgB,EAAE,mBAAmB;IAC5E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,oBAAoB,CAAC;IACjC,iGAAiG;IACjG,MAAM,CAAC,EAAE,uBAAuB,CAAC;IACjC,WAAW,CAAC,EAAE,qBAAqB,CAAC;IACpC,GAAG,CAAC,EAAE,iBAAiB,CAAC;CACzB;AAED,MAAM,MAAM,eAAe,GAAG,mBAAmB,CAAC,SAAS,CAAC,CAAC;AAE7D,qDAAqD;AACrD,wBAAgB,YAAY,CAAC,MAAM,EAAE,eAAe,GAAG,eAAe,CAyIrE"}
@@ -16,7 +16,7 @@ import {
16
16
  parseMultipart,
17
17
  socketIoLane,
18
18
  webSocketLane
19
- } from "../index-fjfzsq6y.js";
19
+ } from "../index-z992nfbx.js";
20
20
  import {
21
21
  createAuthHook,
22
22
  createBearerResolver,
@@ -83,7 +83,20 @@ function createServer(config) {
83
83
  const socketRoutePrefix = socket?.route.path.replace(/\*socketPath$/, "");
84
84
  const websocket = configuredWebSocket ?? socket?.websocket;
85
85
  const openSockets = new Set;
86
+ const socketDrainWaiters = new Set;
86
87
  let hadWebSockets = false;
88
+ const resolveSocketDrain = () => {
89
+ if (openSockets.size !== 0)
90
+ return;
91
+ for (const resolve of socketDrainWaiters)
92
+ resolve();
93
+ socketDrainWaiters.clear();
94
+ };
95
+ const waitForSocketDrain = () => {
96
+ if (openSockets.size === 0)
97
+ return Promise.resolve();
98
+ return new Promise((resolve) => socketDrainWaiters.add(resolve));
99
+ };
87
100
  const trackedWebSocket = websocket ? {
88
101
  ...websocket,
89
102
  open(ws) {
@@ -93,6 +106,7 @@ function createServer(config) {
93
106
  },
94
107
  close(ws, code, reason) {
95
108
  openSockets.delete(ws);
109
+ resolveSocketDrain();
96
110
  websocket.close?.(ws, code, reason);
97
111
  }
98
112
  } : undefined;
@@ -110,21 +124,7 @@ function createServer(config) {
110
124
  const logicalClose = socket?.close();
111
125
  for (const ws of openSockets)
112
126
  ws.close(1001, "Server shutting down");
113
- await Promise.all([
114
- logicalClose,
115
- new Promise((resolve) => {
116
- if (openSockets.size === 0) {
117
- resolve();
118
- return;
119
- }
120
- const timer = setInterval(() => {
121
- if (openSockets.size === 0) {
122
- clearInterval(timer);
123
- resolve();
124
- }
125
- }, 5);
126
- })
127
- ]);
127
+ await Promise.all([logicalClose, waitForSocketDrain()]);
128
128
  },
129
129
  async stopGracefully() {
130
130
  await new Promise((resolve) => setTimeout(resolve, 0));
@@ -142,9 +142,10 @@ function createServer(config) {
142
142
  await server2.stop(false);
143
143
  },
144
144
  async forceStop() {
145
- for (const ws of openSockets)
145
+ const logicalClose = socket?.close();
146
+ const physicalClose = waitForSocketDrain();
147
+ for (const ws of [...openSockets])
146
148
  ws.terminate();
147
- openSockets.clear();
148
149
  const stopping = requireRuntime().stop(true);
149
150
  if (!hadWebSockets)
150
151
  await stopping;
@@ -152,6 +153,7 @@ function createServer(config) {
152
153
  stopping.catch(() => {
153
154
  return;
154
155
  });
156
+ await Promise.all([logicalClose, physicalClose]);
155
157
  }
156
158
  };
157
159
  const lifecycle = createServerLifecycle(() => adapter);
@@ -11,6 +11,7 @@ export declare const ShutdownStateSchema: z.ZodEnum<{
11
11
  export type ShutdownState = z.infer<typeof ShutdownStateSchema>;
12
12
  export declare const ShutdownOptionsSchema: z.ZodObject<{
13
13
  gracePeriodMs: z.ZodDefault<z.ZodNumber>;
14
+ forceTimeoutMs: z.ZodDefault<z.ZodNumber>;
14
15
  retryAfterSeconds: z.ZodDefault<z.ZodNumber>;
15
16
  signal: z.ZodOptional<z.ZodCustom<AbortSignal, AbortSignal>>;
16
17
  }, z.core.$strip>;
@@ -1 +1 @@
1
- {"version":3,"file":"shutdown.d.ts","sourceRoot":"","sources":["../../src/server/shutdown.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAE5C,eAAO,MAAM,mBAAmB;;;;;;;EAO9B,CAAC;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEhE,eAAO,MAAM,qBAAqB;;;;iBAahC,CAAC;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAEpE,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;iBAM/B,CAAC;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAElE,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;iBAY/B,CAAC;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAElE,6EAA6E;AAC7E,MAAM,WAAW,mBAAmB,CAAC,QAAQ;IAC3C,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;IAChC,QAAQ,CAAC,OAAO,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;CAC9D;AAED,MAAM,WAAW,eAAe;IAC9B,aAAa,CAAC,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/C,eAAe,IAAI,MAAM,CAAC;IAC1B,iBAAiB,IAAI,MAAM,CAAC;IAC5B,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/B,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAChC,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5B;AAED,UAAU,eAAe;IACvB,SAAS,CAAC,OAAO,EAAE,OAAO,EAAE,YAAY,CAAC,OAAO,CAAC,GAAG,YAAY,CAAC,OAAO,CAAC,CAAC;IAC1E,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;IAChC,QAAQ,CAAC,OAAO,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;CAC9D;AAqCD,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,MAAM,eAAe,GAAG,eAAe,CA8HxF"}
1
+ {"version":3,"file":"shutdown.d.ts","sourceRoot":"","sources":["../../src/server/shutdown.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAE5C,eAAO,MAAM,mBAAmB;;;;;;;EAO9B,CAAC;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEhE,eAAO,MAAM,qBAAqB;;;;;iBAchC,CAAC;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAEpE,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;iBAM/B,CAAC;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAElE,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;iBAY/B,CAAC;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAElE,6EAA6E;AAC7E,MAAM,WAAW,mBAAmB,CAAC,QAAQ;IAC3C,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;IAChC,QAAQ,CAAC,OAAO,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;CAC9D;AAED,MAAM,WAAW,eAAe;IAC9B,aAAa,CAAC,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/C,eAAe,IAAI,MAAM,CAAC;IAC1B,iBAAiB,IAAI,MAAM,CAAC;IAC5B,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/B,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAChC,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5B;AAED,UAAU,eAAe;IACvB,SAAS,CAAC,OAAO,EAAE,OAAO,EAAE,YAAY,CAAC,OAAO,CAAC,GAAG,YAAY,CAAC,OAAO,CAAC,CAAC;IAC1E,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;IAChC,QAAQ,CAAC,OAAO,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;CAC9D;AAqDD,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,MAAM,eAAe,GAAG,eAAe,CAiJxF"}
@@ -66,7 +66,7 @@ export declare function webSocketLane<TData>(lane: WebSocketLane<TData>): Compos
66
66
  * [webSocketLane({ match: isPcmSocket, handlers: pcmHandlers }), socketIoLane(socket.websocket)],
67
67
  * { maxPayloadLength: 16 * 1024 * 1024 },
68
68
  * )
69
- * createServer({ websocket: ws, rawRoutes: [socket.route, pcmUpgradeRoute] })
69
+ * createServer({ socket, websocket: ws, rawRoutes: [pcmUpgradeRoute] })
70
70
  * ```
71
71
  */
72
72
  export declare function composeWebSocketHandlers(lanes: ComposedLane[], config?: WebSocketComposeConfig): WebSocketHandler<unknown>;
package/llms-full.txt CHANGED
@@ -762,19 +762,24 @@ const server = createServer({ services, socket })
762
762
 
763
763
  const result = await server.shutdown({
764
764
  gracePeriodMs: 30_000,
765
+ forceTimeoutMs: 5_000,
765
766
  retryAfterSeconds: 5,
766
767
  signal: shutdownController.signal,
767
768
  })
768
769
  ```
769
770
 
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
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
773
776
  ordinary HTTP work receives `503`, `Retry-After` and `Connection: close` outside
774
777
  `wrapFetch`. `result.outcome` is `clean` or `forced`; a forced result preserves
775
778
  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.
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.
778
783
 
779
784
  ### Trusted HTTPS in development
780
785
 
@@ -4607,8 +4612,9 @@ expect(res.status).toBe(404)
4607
4612
  expect(await res.json()).toEqual({ error: { code: 'NOT_FOUND', message: 'Note not found' } })
4608
4613
  ```
4609
4614
 
4610
- stitchkit's own suite — 143 tests in `packages/core/tests` is the working
4611
- reference for testing each piece.
4615
+ The suite in `packages/core/tests` is the working reference for testing each
4616
+ piece. Its size changes with the public surface, so the guide does not pin a
4617
+ count that can drift independently from the test runner.
4612
4618
 
4613
4619
  ## Deployment
4614
4620
 
@@ -4643,7 +4649,11 @@ function shutdown() {
4643
4649
  force.abort() // a later signal shortens the same shutdown, not a second chain
4644
4650
  return closing
4645
4651
  }
4646
- closing = server.shutdown({ gracePeriodMs: 30_000, signal: force.signal }).then(async result => {
4652
+ closing = server.shutdown({
4653
+ gracePeriodMs: 30_000,
4654
+ forceTimeoutMs: 5_000,
4655
+ signal: force.signal,
4656
+ }).then(async result => {
4647
4657
  await mcp.close()
4648
4658
  await prisma.$disconnect()
4649
4659
  console.log(result)
@@ -6025,7 +6035,7 @@ Also re-exports the error helpers from `stitchkit/contract`.
6025
6035
  | `BunServerConfig` | _type_ | config for `createServer` (Bun) |
6026
6036
  | `BunServerHandle` | _type_ | managed Bun handle (`url`, `port`, `runtime`, `status`, `shutdown`) |
6027
6037
  | `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 |
6038
+ | `ShutdownOptionsSchema` / `ShutdownOptions` | schema / _type_ | one graceful budget, bounded forced-completion timeout, retry hint and optional external abort signal |
6029
6039
  | `ShutdownStatusSchema` / `ShutdownStatus` | schema / _type_ | live state and request/WebSocket counters |
6030
6040
  | `ShutdownResultSchema` / `ShutdownResult` | schema / _type_ | clean/forced result with final counters and at-force snapshots |
6031
6041
  | `ShutdownStateSchema` / `ShutdownState` | schema / _type_ | managed lifecycle state machine |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stitchkit",
3
- "version": "0.49.0",
3
+ "version": "0.49.2",
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",