@shivaedev/platform 0.2.2 → 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 +14 -0
- package/README.md +26 -0
- package/dist/node-http/subscription-signal.d.ts +22 -0
- package/dist/node-http/subscription-signal.d.ts.map +1 -0
- package/dist/node-http/subscription-signal.js +42 -0
- package/dist/node-http/subscription-signal.js.map +1 -0
- package/dist/node-http.d.ts +2 -0
- package/dist/node-http.d.ts.map +1 -0
- package/dist/node-http.js +2 -0
- package/dist/node-http.js.map +1 -0
- package/package.json +9 -3
- package/src/node-http/subscription-signal.ts +71 -0
- package/src/node-http.ts +5 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
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
|
+
|
|
12
|
+
## 0.2.3 - 2026-08-03
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- Require Effect Prisma 0.4.3 so generated timestamp codec references normalize
|
|
17
|
+
to `Date`.
|
|
18
|
+
|
|
5
19
|
## 0.2.2 - 2026-08-03
|
|
6
20
|
|
|
7
21
|
### 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 @@
|
|
|
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 @@
|
|
|
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.
|
|
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",
|
|
@@ -50,7 +56,7 @@
|
|
|
50
56
|
},
|
|
51
57
|
"peerDependencies": {
|
|
52
58
|
"@effect/vitest": "4.0.0-beta.102",
|
|
53
|
-
"@shivaedev/effect-prisma": "0.4.
|
|
59
|
+
"@shivaedev/effect-prisma": "0.4.3",
|
|
54
60
|
"@shivaedev/effect-trpc": "0.3.0",
|
|
55
61
|
"@trpc/server": "11.18.0",
|
|
56
62
|
"better-auth": "1.6.25",
|
|
@@ -68,7 +74,7 @@
|
|
|
68
74
|
"@prisma-next/postgres": "0.15.0",
|
|
69
75
|
"@prisma-next/sql-contract": "0.15.0",
|
|
70
76
|
"@prisma-next/target-postgres": "0.15.0",
|
|
71
|
-
"@shivaedev/effect-prisma": "0.4.
|
|
77
|
+
"@shivaedev/effect-prisma": "0.4.3",
|
|
72
78
|
"@shivaedev/effect-trpc": "0.3.0",
|
|
73
79
|
"@trpc/server": "11.18.0",
|
|
74
80
|
"@types/node": "24.10.1",
|
|
@@ -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
|
+
};
|