@fuzdev/fuz_app 0.67.0 → 0.67.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.
@@ -1 +1 @@
1
- {"version":3,"file":"spawn_backend.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/spawn_backend.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,EAAQ,KAAK,YAAY,EAAC,MAAM,oBAAoB,CAAC;AAI5D,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,qBAAqB,CAAC;AAIvD,6EAA6E;AAC7E,MAAM,WAAW,aAAa;IAC7B,yFAAyF;IACzF,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAC/B,qEAAqE;IACrE,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC;IAC7B;;;;;OAKG;IACH,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B;;;OAGG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACvC;AAmID;;;;;;GAMG;AACH,eAAO,MAAM,aAAa,GAAU,QAAQ,aAAa,KAAG,OAAO,CAAC,aAAa,CA8FhF,CAAC"}
1
+ {"version":3,"file":"spawn_backend.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/spawn_backend.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,EAAQ,KAAK,YAAY,EAAC,MAAM,oBAAoB,CAAC;AAI5D,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,qBAAqB,CAAC;AAIvD,6EAA6E;AAC7E,MAAM,WAAW,aAAa;IAC7B,yFAAyF;IACzF,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAC/B,qEAAqE;IACrE,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC;IAC7B;;;;;OAKG;IACH,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B;;;OAGG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACvC;AA4ID;;;;;;GAMG;AACH,eAAO,MAAM,aAAa,GAAU,QAAQ,aAAa,KAAG,OAAO,CAAC,aAAa,CAiHhF,CAAC"}
@@ -31,6 +31,14 @@ import { readFile, writeFile, mkdir } from 'node:fs/promises';
31
31
  import { dirname } from 'node:path';
32
32
  /** Number of ms between health-probe attempts. Tuned to be cheap on busy CI runners. */
33
33
  const HEALTH_PROBE_INTERVAL_MS = 100;
34
+ /**
35
+ * Grace window after teardown's SIGTERM before escalating to SIGKILL on the
36
+ * process group. A well-behaved backend exits within milliseconds; this only
37
+ * fires for one that ignores SIGTERM or whose graceful shutdown never
38
+ * completes (e.g. a runtime whose `server.stop()` promise never resolves), so
39
+ * the await below can't strand the whole run forever.
40
+ */
41
+ const TEARDOWN_SIGKILL_GRACE_MS = 3_000;
34
42
  /**
35
43
  * Sleep helper for the probe loop. Resolves after `ms`.
36
44
  */
@@ -208,10 +216,30 @@ export const spawn_backend = async (config) => {
208
216
  live_teardowns.delete(teardown_sync);
209
217
  if (exit_info !== null)
210
218
  return;
211
- // Wait for the child to actually exit so callers can be sure the
212
- // port is free.
219
+ // Wait for the child to actually exit so callers can be sure the port
220
+ // is free. A backend that ignores SIGTERM — or whose graceful shutdown
221
+ // wedges (e.g. a runtime whose `server.stop()` never resolves) — would
222
+ // otherwise hang this await forever and strand the run, so escalate to
223
+ // SIGKILL on the process group after a grace window. SIGKILL is
224
+ // uncatchable, so the child then exits and the `'exit'` listener
225
+ // resolves. Defense-in-depth: the per-runtime adapter shutdown is the
226
+ // primary fix; this guarantees teardown completes regardless.
213
227
  await new Promise((resolve) => {
214
- child.once('exit', () => resolve());
228
+ const kill_timer = setTimeout(() => {
229
+ if (child.pid !== undefined && exit_info === null) {
230
+ try {
231
+ // Negative pid → process group.
232
+ process.kill(-child.pid, 'SIGKILL');
233
+ }
234
+ catch {
235
+ // Already dead; ignore.
236
+ }
237
+ }
238
+ }, TEARDOWN_SIGKILL_GRACE_MS);
239
+ child.once('exit', () => {
240
+ clearTimeout(kill_timer);
241
+ resolve();
242
+ });
215
243
  });
216
244
  };
217
245
  live_teardowns.add(teardown_sync);
@@ -1 +1 @@
1
- {"version":3,"file":"testing_server_bun.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/testing_server_bun.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA6B9B,OAAO,KAAK,EAAc,oBAAoB,EAAC,MAAM,0BAA0B,CAAC;AAehF,kDAAkD;AAClD,eAAO,MAAM,0BAA0B,QAAO,oBA+B5C,CAAC"}
1
+ {"version":3,"file":"testing_server_bun.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/testing_server_bun.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA6B9B,OAAO,KAAK,EAAc,oBAAoB,EAAC,MAAM,0BAA0B,CAAC;AAehF,kDAAkD;AAClD,eAAO,MAAM,0BAA0B,QAAO,oBA0D5C,CAAC"}
@@ -43,8 +43,35 @@ export const create_bun_testing_adapter = () => ({
43
43
  websocket,
44
44
  });
45
45
  const handle = {
46
- shutdown: async () => {
47
- await server.stop();
46
+ // Bun bug (1.3.14): after a *server-initiated* WebSocket close
47
+ // (`ServerWebSocket.close()` / `hono/bun`'s `WSContext.close()`),
48
+ // `server.stop()` never resolves — Bun doesn't decrement its
49
+ // active-connection count for a server-closed socket, so the stop
50
+ // waits forever for a connection it already closed. The trigger is
51
+ // orthogonal to HTTP load, hono-vs-raw `Bun.serve`, in-vs-cross
52
+ // process, the force flag (`stop()`/`stop(false)`/`stop(true)` all
53
+ // hang), and the client runtime — a single server-closed WS is
54
+ // necessary and sufficient. Client-initiated close or leaving the
55
+ // socket open both stop cleanly in ~0ms. In this suite the trigger is
56
+ // `create_ws_auth_guard` closing the socket on `session_revoke_all`
57
+ // (the `ws.cross.test.ts` close-on-revoke case) — the only
58
+ // server-initiated WS close, which is why teardown hangs there and
59
+ // not under HTTP-only or client-closed WS traffic.
60
+ //
61
+ // So initiate a force-close (`true` drops active connections, no
62
+ // drain) but DON'T await it: awaiting hangs `start_testing_server`'s
63
+ // shutdown forever — `built.close()` and `exit(0)` never run, and the
64
+ // spawning harness blocks on a child that never exits (observed as a
65
+ // multi-minute hang needing SIGKILL). The force-close still tears the
66
+ // live sockets down; the `exit(0)` the core fires immediately after
67
+ // does the real teardown a few ms later. Node/Deno don't need this —
68
+ // their `shutdown()`/`close()` resolve normally. The `.catch` guards a
69
+ // future Bun that rejects rather than hangs; if a future Bun resolves
70
+ // the promise after a server-initiated close, revert to
71
+ // `await server.stop(true)` to mirror the Node/Deno adapters.
72
+ shutdown: () => {
73
+ void Promise.resolve(server.stop(true)).catch(() => { });
74
+ return Promise.resolve();
48
75
  },
49
76
  native: server,
50
77
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fuzdev/fuz_app",
3
- "version": "0.67.0",
3
+ "version": "0.67.1",
4
4
  "description": "fullstack app library",
5
5
  "glyph": "🗝",
6
6
  "logo": "logo.svg",
@@ -14,6 +14,8 @@
14
14
  "build": "gro build",
15
15
  "check": "gro check",
16
16
  "test": "gro test",
17
+ "test:cross": "FUZ_TEST_CROSS_BACKEND=1 vitest run --project cross_backend_ts_node --project cross_backend_ts_deno --project cross_backend_ts_bun",
18
+ "test:cross:spine-stub": "FUZ_TEST_CROSS_BACKEND=1 vitest run --project cross_backend_spine_stub",
17
19
  "benchmark:cross-impl": "gro run src/benchmarks/cross_impl.bench.ts",
18
20
  "preview": "vite preview",
19
21
  "deploy": "gro deploy"