@effectstream/node-sdk 0.200.1 → 0.200.4
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/README.md +7 -0
- package/package.json +13 -13
- package/src/single-file.ts +2 -2
- package/test/fixtures/single-file-sigterm.ts +50 -0
- package/test/single-file-lifecycle.test.ts +46 -0
package/README.md
CHANGED
|
@@ -64,6 +64,13 @@ instead of jumping to a new tip.
|
|
|
64
64
|
The subpaths are thin re-exports - semantics are identical to importing
|
|
65
65
|
from the underlying packages.
|
|
66
66
|
|
|
67
|
+
The re-exported `db/start-pglite` handle uses a bounded, non-destructive
|
|
68
|
+
`close()` by default and an explicit owner-only `close({ force: true })` mode.
|
|
69
|
+
Default close defers PGlite cleanup until the last accepted client socket drains.
|
|
70
|
+
Concurrent calls share the first cleanup promise and the first requested mode
|
|
71
|
+
wins. See the `@effectstream/db` PgLite gateway lifecycle section for the full
|
|
72
|
+
client-ownership, failure, and `0.104.0`→`0.200.1` migration contract.
|
|
73
|
+
|
|
67
74
|
## Inside EffectStream
|
|
68
75
|
|
|
69
76
|
A namespace seam, not a runtime piece. Each subpath simply re-exports
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effectstream/node-sdk",
|
|
3
|
-
"version": "0.200.
|
|
3
|
+
"version": "0.200.4",
|
|
4
4
|
"license": "(MIT OR Apache-2.0)",
|
|
5
5
|
"homepage": "https://effectstream.github.io/docs/",
|
|
6
6
|
"repository": {
|
|
@@ -83,18 +83,18 @@
|
|
|
83
83
|
}
|
|
84
84
|
},
|
|
85
85
|
"dependencies": {
|
|
86
|
-
"@effectstream/runtime": "0.200.
|
|
87
|
-
"@effectstream/sm": "0.200.
|
|
88
|
-
"@effectstream/sync": "0.200.
|
|
89
|
-
"@effectstream/db": "0.200.
|
|
90
|
-
"@effectstream/db-emulator": "0.200.
|
|
91
|
-
"@effectstream/coroutine": "0.200.
|
|
92
|
-
"@effectstream/event-server": "0.200.
|
|
93
|
-
"@effectstream/config": "0.200.
|
|
94
|
-
"@effectstream/chain-types": "0.200.
|
|
95
|
-
"@effectstream/precompile": "0.200.
|
|
96
|
-
"@effectstream/utils": "0.200.
|
|
97
|
-
"@effectstream/concise": "0.200.
|
|
86
|
+
"@effectstream/runtime": "0.200.4",
|
|
87
|
+
"@effectstream/sm": "0.200.4",
|
|
88
|
+
"@effectstream/sync": "0.200.4",
|
|
89
|
+
"@effectstream/db": "0.200.4",
|
|
90
|
+
"@effectstream/db-emulator": "0.200.4",
|
|
91
|
+
"@effectstream/coroutine": "0.200.4",
|
|
92
|
+
"@effectstream/event-server": "0.200.4",
|
|
93
|
+
"@effectstream/config": "0.200.4",
|
|
94
|
+
"@effectstream/chain-types": "0.200.4",
|
|
95
|
+
"@effectstream/precompile": "0.200.4",
|
|
96
|
+
"@effectstream/utils": "0.200.4",
|
|
97
|
+
"@effectstream/concise": "0.200.4",
|
|
98
98
|
"effection": "^3.5.0"
|
|
99
99
|
},
|
|
100
100
|
"description": "Main application node SDK for EffectStream"
|
package/src/single-file.ts
CHANGED
|
@@ -218,7 +218,7 @@ export async function runNode<
|
|
|
218
218
|
|
|
219
219
|
task = run(function* () {
|
|
220
220
|
yield* ensure(function* () {
|
|
221
|
-
yield* call(() => ownedDatabase.close());
|
|
221
|
+
yield* call(() => ownedDatabase.close({ force: true }));
|
|
222
222
|
});
|
|
223
223
|
yield* init();
|
|
224
224
|
yield* withEffectstreamStaticConfig(staticConfig, function* () {
|
|
@@ -252,7 +252,7 @@ export async function runNode<
|
|
|
252
252
|
process.off("SIGINT", signalHandler);
|
|
253
253
|
process.off("SIGTERM", signalHandler);
|
|
254
254
|
}
|
|
255
|
-
if (!task && database) await database.close();
|
|
255
|
+
if (!task && database) await database.close({ force: true });
|
|
256
256
|
restoreEnvironment();
|
|
257
257
|
}
|
|
258
258
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import pg from "../../../db/node_modules/pg";
|
|
2
|
+
import net from "node:net";
|
|
3
|
+
import {
|
|
4
|
+
midnightContract,
|
|
5
|
+
pglite,
|
|
6
|
+
runNode,
|
|
7
|
+
} from "../../src/single-file.ts";
|
|
8
|
+
|
|
9
|
+
const originalPoolEnd = pg.Pool.prototype.end;
|
|
10
|
+
const originalServerClose = net.Server.prototype.close;
|
|
11
|
+
pg.Pool.prototype.end = async function () {
|
|
12
|
+
console.info("SINGLE_FILE_POOL_END");
|
|
13
|
+
return await originalPoolEnd.call(this);
|
|
14
|
+
};
|
|
15
|
+
net.Server.prototype.close = function (...args: Parameters<net.Server["close"]>) {
|
|
16
|
+
const address = this.address();
|
|
17
|
+
if (address && typeof address !== "string") {
|
|
18
|
+
console.info(`SINGLE_FILE_SERVER_CLOSE:${address.port}`);
|
|
19
|
+
}
|
|
20
|
+
return originalServerClose.apply(this, args);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const initialSignalListeners = process.listenerCount("SIGTERM");
|
|
24
|
+
const signalPoll = setInterval(() => {
|
|
25
|
+
if (process.listenerCount("SIGTERM") > initialSignalListeners) {
|
|
26
|
+
clearInterval(signalPoll);
|
|
27
|
+
console.info("SINGLE_FILE_SEND_SIGTERM");
|
|
28
|
+
process.kill(process.pid, "SIGTERM");
|
|
29
|
+
}
|
|
30
|
+
}, 10);
|
|
31
|
+
|
|
32
|
+
await runNode({
|
|
33
|
+
appName: "single-file-sigterm-fixture",
|
|
34
|
+
apiPort: 18999,
|
|
35
|
+
database: pglite({ port: 0 }),
|
|
36
|
+
sources: {
|
|
37
|
+
counter: midnightContract({
|
|
38
|
+
network: "preview",
|
|
39
|
+
address: "a".repeat(64),
|
|
40
|
+
startBlockHeight: 0,
|
|
41
|
+
ledger: { round: "uint128" },
|
|
42
|
+
indexer: "http://127.0.0.1:18998/graphql",
|
|
43
|
+
}),
|
|
44
|
+
},
|
|
45
|
+
transitions: {
|
|
46
|
+
counter: () => {},
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
console.info("SINGLE_FILE_EXIT_ZERO");
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import net from "node:net";
|
|
3
|
+
|
|
4
|
+
async function expectPortClosed(port: number): Promise<void> {
|
|
5
|
+
await expect(new Promise<void>((resolve, reject) => {
|
|
6
|
+
const socket = net.connect({ host: "127.0.0.1", port });
|
|
7
|
+
socket.once("connect", () => {
|
|
8
|
+
socket.destroy();
|
|
9
|
+
resolve();
|
|
10
|
+
});
|
|
11
|
+
socket.once("error", reject);
|
|
12
|
+
})).rejects.toMatchObject({ code: "ECONNREFUSED" });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
test("single-file SIGTERM drains the runtime pool before explicit forced PGlite close", async () => {
|
|
16
|
+
const child = Bun.spawn([
|
|
17
|
+
process.execPath,
|
|
18
|
+
new URL("./fixtures/single-file-sigterm.ts", import.meta.url).pathname,
|
|
19
|
+
], {
|
|
20
|
+
cwd: new URL("../../../..", import.meta.url).pathname,
|
|
21
|
+
stdout: "pipe",
|
|
22
|
+
stderr: "pipe",
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
26
|
+
new Response(child.stdout).text(),
|
|
27
|
+
new Response(child.stderr).text(),
|
|
28
|
+
child.exited,
|
|
29
|
+
]);
|
|
30
|
+
const output = `${stdout}\n${stderr}`;
|
|
31
|
+
|
|
32
|
+
expect(exitCode, output).toBe(0);
|
|
33
|
+
expect(output).toContain("SINGLE_FILE_SEND_SIGTERM");
|
|
34
|
+
expect(output).toContain("SINGLE_FILE_POOL_END");
|
|
35
|
+
expect(output).toContain("SINGLE_FILE_EXIT_ZERO");
|
|
36
|
+
|
|
37
|
+
const match = output.match(/database: server listening on port (\d+)/);
|
|
38
|
+
if (!match) throw new Error(`No reported PGlite port in output:\n${output}`);
|
|
39
|
+
const port = Number(match[1]);
|
|
40
|
+
const gatewayCloseMarker = `SINGLE_FILE_SERVER_CLOSE:${port}`;
|
|
41
|
+
expect(output).toContain(gatewayCloseMarker);
|
|
42
|
+
expect(output.indexOf("SINGLE_FILE_POOL_END")).toBeLessThan(
|
|
43
|
+
output.indexOf(gatewayCloseMarker),
|
|
44
|
+
);
|
|
45
|
+
await expectPortClosed(port);
|
|
46
|
+
}, 30_000);
|