@shivaedev/platform 0.2.3 → 0.3.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/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.3.0 - 2026-08-03
6
+
7
+ ### Added
8
+
9
+ - Add a Node/Bun subscription signal that combines Web and procedure aborts
10
+ with request and socket close events for reliable long-lived response cleanup.
11
+
5
12
  ## 0.2.3 - 2026-08-03
6
13
 
7
14
  ### Fixed
package/README.md CHANGED
@@ -23,6 +23,32 @@ The optional cache key keeps one runtime across development module reloads.
23
23
  The runtime also carries transaction- and request-scoped service overrides
24
24
  across promise boundaries.
25
25
 
26
+ ## Node HTTP
27
+
28
+ Long-lived responses under Bun's `node:http` compatibility layer may not abort
29
+ their Web request signal when the underlying socket disappears. Combine the
30
+ available Web, procedure, request, and socket signals at the subscription
31
+ boundary:
32
+
33
+ ```ts
34
+ import { nodeSubscriptionSignal } from "@shivaedev/platform/node-http"
35
+
36
+ const subscription = nodeSubscriptionSignal({
37
+ request,
38
+ nodeRequest,
39
+ signals: [procedureSignal],
40
+ })
41
+
42
+ try {
43
+ await consume(subscription.signal)
44
+ } finally {
45
+ subscription.dispose()
46
+ }
47
+ ```
48
+
49
+ The node request may be supplied directly or carried by an srvx Web request.
50
+ Applications still own subscription limits, event buses, and transport policy.
51
+
26
52
  ## Better Auth
27
53
 
28
54
  `effectPrismaAdapter` stores Better Auth data through the application's Effect
@@ -0,0 +1,22 @@
1
+ import type { IncomingMessage } from "node:http";
2
+ export interface NodeSubscriptionSignalOptions {
3
+ /** A Web request, including its signal and an optional srvx node runtime. */
4
+ readonly request?: Request | undefined;
5
+ /** The node request when the transport exposes it directly. */
6
+ readonly nodeRequest?: IncomingMessage | undefined;
7
+ /** Additional transport or procedure signals to combine. */
8
+ readonly signals?: Iterable<AbortSignal | undefined>;
9
+ }
10
+ export interface NodeSubscriptionSignal {
11
+ /** Detach node listeners when the subscription ends. */
12
+ readonly dispose: () => void;
13
+ /** Aborts when any supplied signal or the underlying connection closes. */
14
+ readonly signal: AbortSignal;
15
+ }
16
+ /**
17
+ * Combine Web/procedure abort signals with node request and socket close events.
18
+ * The node events cover long-lived responses under Bun's node:http compatibility
19
+ * layer, where a Web request signal alone may not report an abandoned socket.
20
+ */
21
+ export declare const nodeSubscriptionSignal: (options: NodeSubscriptionSignalOptions) => NodeSubscriptionSignal;
22
+ //# sourceMappingURL=subscription-signal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"subscription-signal.d.ts","sourceRoot":"","sources":["../../src/node-http/subscription-signal.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAUjD,MAAM,WAAW,6BAA6B;IAC7C,6EAA6E;IAC7E,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACvC,+DAA+D;IAC/D,QAAQ,CAAC,WAAW,CAAC,EAAE,eAAe,GAAG,SAAS,CAAC;IACnD,4DAA4D;IAC5D,QAAQ,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,WAAW,GAAG,SAAS,CAAC,CAAC;CACrD;AAED,MAAM,WAAW,sBAAsB;IACtC,wDAAwD;IACxD,QAAQ,CAAC,OAAO,EAAE,MAAM,IAAI,CAAC;IAC7B,2EAA2E;IAC3E,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;CAC7B;AAOD;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,GAClC,SAAS,6BAA6B,KACpC,sBAgCF,CAAC"}
@@ -0,0 +1,42 @@
1
+ const nodeRequestFrom = (request) => request?.runtime?.node?.req;
2
+ /**
3
+ * Combine Web/procedure abort signals with node request and socket close events.
4
+ * The node events cover long-lived responses under Bun's node:http compatibility
5
+ * layer, where a Web request signal alone may not report an abandoned socket.
6
+ */
7
+ export const nodeSubscriptionSignal = (options) => {
8
+ const controller = new AbortController();
9
+ const signals = [controller.signal];
10
+ if (options.request !== undefined)
11
+ signals.push(options.request.signal);
12
+ for (const signal of options.signals ?? []) {
13
+ if (signal !== undefined)
14
+ signals.push(signal);
15
+ }
16
+ const nodeRequest = options.nodeRequest ?? nodeRequestFrom(options.request);
17
+ if (nodeRequest === undefined) {
18
+ return { dispose: () => { }, signal: AbortSignal.any(signals) };
19
+ }
20
+ const socket = nodeRequest.socket;
21
+ const onRequestClose = () => {
22
+ if (!nodeRequest.complete)
23
+ controller.abort();
24
+ };
25
+ const onSocketClose = () => controller.abort();
26
+ nodeRequest.once("close", onRequestClose);
27
+ socket.once("close", onSocketClose);
28
+ if (nodeRequest.destroyed || socket.destroyed)
29
+ controller.abort();
30
+ let disposed = false;
31
+ return {
32
+ dispose: () => {
33
+ if (disposed)
34
+ return;
35
+ disposed = true;
36
+ nodeRequest.removeListener("close", onRequestClose);
37
+ socket.removeListener("close", onSocketClose);
38
+ },
39
+ signal: AbortSignal.any(signals),
40
+ };
41
+ };
42
+ //# sourceMappingURL=subscription-signal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"subscription-signal.js","sourceRoot":"","sources":["../../src/node-http/subscription-signal.ts"],"names":[],"mappings":"AA0BA,MAAM,eAAe,GAAG,CACvB,OAA4B,EACE,EAAE,CAC/B,OAAsD,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,CAAC;AAE7E;;;;GAIG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,CACrC,OAAsC,EACb,EAAE;IAC3B,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;IACzC,MAAM,OAAO,GAAkB,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;IACnD,IAAI,OAAO,CAAC,OAAO,KAAK,SAAS;QAAE,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IACxE,KAAK,MAAM,MAAM,IAAI,OAAO,CAAC,OAAO,IAAI,EAAE,EAAE,CAAC;QAC5C,IAAI,MAAM,KAAK,SAAS;YAAE,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAChD,CAAC;IAED,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,IAAI,eAAe,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IAC5E,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;QAC/B,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,GAAE,CAAC,EAAE,MAAM,EAAE,WAAW,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC;IAChE,CAAC;IAED,MAAM,MAAM,GAAG,WAAW,CAAC,MAAM,CAAC;IAClC,MAAM,cAAc,GAAG,GAAG,EAAE;QAC3B,IAAI,CAAC,WAAW,CAAC,QAAQ;YAAE,UAAU,CAAC,KAAK,EAAE,CAAC;IAC/C,CAAC,CAAC;IACF,MAAM,aAAa,GAAG,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,CAAC;IAC/C,WAAW,CAAC,IAAI,CAAC,OAAO,EAAE,cAAc,CAAC,CAAC;IAC1C,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,aAAa,CAAC,CAAC;IACpC,IAAI,WAAW,CAAC,SAAS,IAAI,MAAM,CAAC,SAAS;QAAE,UAAU,CAAC,KAAK,EAAE,CAAC;IAElE,IAAI,QAAQ,GAAG,KAAK,CAAC;IACrB,OAAO;QACN,OAAO,EAAE,GAAG,EAAE;YACb,IAAI,QAAQ;gBAAE,OAAO;YACrB,QAAQ,GAAG,IAAI,CAAC;YAChB,WAAW,CAAC,cAAc,CAAC,OAAO,EAAE,cAAc,CAAC,CAAC;YACpD,MAAM,CAAC,cAAc,CAAC,OAAO,EAAE,aAAa,CAAC,CAAC;QAC/C,CAAC;QACD,MAAM,EAAE,WAAW,CAAC,GAAG,CAAC,OAAO,CAAC;KAChC,CAAC;AACH,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export { type NodeSubscriptionSignal, type NodeSubscriptionSignalOptions, nodeSubscriptionSignal, } from "./node-http/subscription-signal.js";
2
+ //# sourceMappingURL=node-http.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"node-http.d.ts","sourceRoot":"","sources":["../src/node-http.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,KAAK,sBAAsB,EAC3B,KAAK,6BAA6B,EAClC,sBAAsB,GACtB,MAAM,oCAAoC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export { nodeSubscriptionSignal, } from "./node-http/subscription-signal.js";
2
+ //# sourceMappingURL=node-http.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"node-http.js","sourceRoot":"","sources":["../src/node-http.ts"],"names":[],"mappings":"AAAA,OAAO,EAGN,sBAAsB,GACtB,MAAM,oCAAoC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shivaedev/platform",
3
- "version": "0.2.3",
3
+ "version": "0.3.0",
4
4
  "description": "Opinionated Effect application infrastructure for ShivaeDev services",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -36,6 +36,12 @@
36
36
  "import": "./dist/runtime.js",
37
37
  "default": "./dist/runtime.js"
38
38
  },
39
+ "./node-http": {
40
+ "types": "./dist/node-http.d.ts",
41
+ "source": "./src/node-http.ts",
42
+ "import": "./dist/node-http.js",
43
+ "default": "./dist/node-http.js"
44
+ },
39
45
  "./testing": {
40
46
  "types": "./dist/testing.d.ts",
41
47
  "source": "./src/testing.ts",
@@ -0,0 +1,71 @@
1
+ import type { IncomingMessage } from "node:http";
2
+
3
+ interface NodeRequestCarrier {
4
+ readonly runtime?: {
5
+ readonly node?: {
6
+ readonly req?: IncomingMessage;
7
+ };
8
+ };
9
+ }
10
+
11
+ export interface NodeSubscriptionSignalOptions {
12
+ /** A Web request, including its signal and an optional srvx node runtime. */
13
+ readonly request?: Request | undefined;
14
+ /** The node request when the transport exposes it directly. */
15
+ readonly nodeRequest?: IncomingMessage | undefined;
16
+ /** Additional transport or procedure signals to combine. */
17
+ readonly signals?: Iterable<AbortSignal | undefined>;
18
+ }
19
+
20
+ export interface NodeSubscriptionSignal {
21
+ /** Detach node listeners when the subscription ends. */
22
+ readonly dispose: () => void;
23
+ /** Aborts when any supplied signal or the underlying connection closes. */
24
+ readonly signal: AbortSignal;
25
+ }
26
+
27
+ const nodeRequestFrom = (
28
+ request: Request | undefined,
29
+ ): IncomingMessage | undefined =>
30
+ (request as (Request & NodeRequestCarrier) | undefined)?.runtime?.node?.req;
31
+
32
+ /**
33
+ * Combine Web/procedure abort signals with node request and socket close events.
34
+ * The node events cover long-lived responses under Bun's node:http compatibility
35
+ * layer, where a Web request signal alone may not report an abandoned socket.
36
+ */
37
+ export const nodeSubscriptionSignal = (
38
+ options: NodeSubscriptionSignalOptions,
39
+ ): NodeSubscriptionSignal => {
40
+ const controller = new AbortController();
41
+ const signals: AbortSignal[] = [controller.signal];
42
+ if (options.request !== undefined) signals.push(options.request.signal);
43
+ for (const signal of options.signals ?? []) {
44
+ if (signal !== undefined) signals.push(signal);
45
+ }
46
+
47
+ const nodeRequest = options.nodeRequest ?? nodeRequestFrom(options.request);
48
+ if (nodeRequest === undefined) {
49
+ return { dispose: () => {}, signal: AbortSignal.any(signals) };
50
+ }
51
+
52
+ const socket = nodeRequest.socket;
53
+ const onRequestClose = () => {
54
+ if (!nodeRequest.complete) controller.abort();
55
+ };
56
+ const onSocketClose = () => controller.abort();
57
+ nodeRequest.once("close", onRequestClose);
58
+ socket.once("close", onSocketClose);
59
+ if (nodeRequest.destroyed || socket.destroyed) controller.abort();
60
+
61
+ let disposed = false;
62
+ return {
63
+ dispose: () => {
64
+ if (disposed) return;
65
+ disposed = true;
66
+ nodeRequest.removeListener("close", onRequestClose);
67
+ socket.removeListener("close", onSocketClose);
68
+ },
69
+ signal: AbortSignal.any(signals),
70
+ };
71
+ };
@@ -0,0 +1,5 @@
1
+ export {
2
+ type NodeSubscriptionSignal,
3
+ type NodeSubscriptionSignalOptions,
4
+ nodeSubscriptionSignal,
5
+ } from "./node-http/subscription-signal.js";