@typeberry/lib 0.9.0-3f2c45b → 0.9.0-59ce083

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typeberry/lib",
3
- "version": "0.9.0-3f2c45b",
3
+ "version": "0.9.0-59ce083",
4
4
  "description": "Typeberry Library",
5
5
  "main": "./bin/lib/index.js",
6
6
  "types": "./bin/lib/index.d.ts",
@@ -18,5 +18,6 @@ export * from "./opaque.js";
18
18
  export { name, version } from "./package.js";
19
19
  export * from "./result.js";
20
20
  export * from "./safe-alloc-uint8array.js";
21
+ export * from "./shutdown.js";
21
22
  export * from "./test.js";
22
23
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../packages/core/utils/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,cAAc,oBAAoB,CAAC;AACnC,cAAc,YAAY,CAAC;AAC3B,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC;AACzB,cAAc,aAAa,CAAC;AAC5B,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC7C,cAAc,aAAa,CAAC;AAC5B,cAAc,4BAA4B,CAAC;AAC3C,cAAc,WAAW,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../packages/core/utils/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,cAAc,oBAAoB,CAAC;AACnC,cAAc,YAAY,CAAC;AAC3B,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC;AACzB,cAAc,aAAa,CAAC;AAC5B,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC7C,cAAc,aAAa,CAAC;AAC5B,cAAc,4BAA4B,CAAC;AAC3C,cAAc,eAAe,CAAC;AAC9B,cAAc,WAAW,CAAC"}
@@ -18,4 +18,5 @@ export * from "./opaque.js";
18
18
  export { name, version } from "./package.js";
19
19
  export * from "./result.js";
20
20
  export * from "./safe-alloc-uint8array.js";
21
+ export * from "./shutdown.js";
21
22
  export * from "./test.js";
@@ -0,0 +1,18 @@
1
+ export type Closer = () => Promise<void>;
2
+ export type ShutdownSignal = string;
3
+ export interface ShutdownLogger {
4
+ info: (msg: string) => void;
5
+ error: (msg: string) => void;
6
+ }
7
+ export interface ShutdownOptions {
8
+ /** Max ms allowed for `close` before forced exit(1). Default 10_000. */
9
+ timeoutMs?: number;
10
+ /** Signals to listen for. Default ["SIGTERM", "SIGINT"]. */
11
+ signals?: readonly ShutdownSignal[];
12
+ /** Logger; defaults to a no-op logger. */
13
+ log?: ShutdownLogger;
14
+ /** Test hook used instead of process.exit. */
15
+ exit?: (code: number) => never;
16
+ }
17
+ export declare function installShutdownHandlers(_close: Closer, _options?: ShutdownOptions): () => void;
18
+ //# sourceMappingURL=shutdown.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shutdown.d.ts","sourceRoot":"","sources":["../../../../../packages/core/utils/shutdown.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;AAEzC,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC;AAEpC,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5B,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC9B;AAED,MAAM,WAAW,eAAe;IAC9B,wEAAwE;IACxE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4DAA4D;IAC5D,OAAO,CAAC,EAAE,SAAS,cAAc,EAAE,CAAC;IACpC,0CAA0C;IAC1C,GAAG,CAAC,EAAE,cAAc,CAAC;IACrB,8CAA8C;IAC9C,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,KAAK,CAAC;CAChC;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,GAAE,eAAoB,GAAG,MAAM,IAAI,CAElG"}
@@ -0,0 +1,3 @@
1
+ export function installShutdownHandlers(_close, _options = {}) {
2
+ return () => { };
3
+ }
@@ -0,0 +1,5 @@
1
+ import type { Closer, ShutdownOptions } from "./shutdown.js";
2
+ export type { Closer, ShutdownLogger, ShutdownOptions, ShutdownSignal } from "./shutdown.js";
3
+ /** Returns an uninstall function (used by tests). */
4
+ export declare function installShutdownHandlers(close: Closer, options?: ShutdownOptions): () => void;
5
+ //# sourceMappingURL=shutdown.node.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shutdown.node.d.ts","sourceRoot":"","sources":["../../../../../packages/core/utils/shutdown.node.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAE7D,YAAY,EAAE,MAAM,EAAE,cAAc,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAK7F,qDAAqD;AACrD,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,GAAE,eAAoB,GAAG,MAAM,IAAI,CA+EhG"}
@@ -0,0 +1,76 @@
1
+ const DEFAULT_SIGNALS = ["SIGTERM", "SIGINT"];
2
+ const DEFAULT_TIMEOUT_MS = 10_000;
3
+ /** Returns an uninstall function (used by tests). */
4
+ export function installShutdownHandlers(close, options = {}) {
5
+ const signals = (options.signals ?? DEFAULT_SIGNALS);
6
+ const exit = (options.exit ?? process.exit);
7
+ const log = options.log;
8
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
9
+ let state = "idle";
10
+ const forceExit = (signal) => {
11
+ log?.error(`Received ${signal} during shutdown, forcing exit.`);
12
+ try {
13
+ exit(1);
14
+ }
15
+ catch {
16
+ // test-time exit throws; swallow.
17
+ }
18
+ };
19
+ const handler = (signal) => {
20
+ if (state === "closing" || state === "done") {
21
+ forceExit(signal);
22
+ return;
23
+ }
24
+ state = "closing";
25
+ log?.info(`Received ${signal}, draining...`);
26
+ void (async () => {
27
+ const TIMED_OUT = Symbol("timed-out");
28
+ let timeoutHandle;
29
+ const timeoutPromise = new Promise((resolve) => {
30
+ timeoutHandle = setTimeout(() => resolve(TIMED_OUT), timeoutMs);
31
+ });
32
+ timeoutHandle?.unref?.();
33
+ let code = 0;
34
+ try {
35
+ const outcome = await Promise.race([
36
+ Promise.resolve()
37
+ .then(close)
38
+ .then(() => "ok", (e) => ({ err: e })),
39
+ timeoutPromise,
40
+ ]);
41
+ if (outcome === TIMED_OUT) {
42
+ code = 1;
43
+ log?.error(`Shutdown cleanup exceeded ${timeoutMs}ms timeout, forcing exit.`);
44
+ }
45
+ else if (typeof outcome === "object" && "err" in outcome) {
46
+ code = 1;
47
+ const e = outcome.err;
48
+ log?.error(`Shutdown close threw: ${e instanceof Error ? (e.stack ?? e.message) : String(e)}`);
49
+ }
50
+ else {
51
+ log?.info("Shutdown complete.");
52
+ }
53
+ }
54
+ finally {
55
+ clearTimeout(timeoutHandle);
56
+ state = "done";
57
+ }
58
+ try {
59
+ exit(code);
60
+ }
61
+ catch {
62
+ // test-time exit throws; swallow.
63
+ }
64
+ })();
65
+ };
66
+ // Node passes the signal name to the listener, so a single handler works
67
+ // for every signal.
68
+ for (const sig of signals) {
69
+ process.on(sig, handler);
70
+ }
71
+ return () => {
72
+ for (const sig of signals) {
73
+ process.off(sig, handler);
74
+ }
75
+ };
76
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=shutdown.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shutdown.test.d.ts","sourceRoot":"","sources":["../../../../../packages/core/utils/shutdown.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,164 @@
1
+ import assert from "node:assert";
2
+ import { afterEach, describe, it } from "node:test";
3
+ import { installShutdownHandlers as installGenericShutdownHandlers } from "./shutdown.js";
4
+ import { installShutdownHandlers } from "./shutdown.node.js";
5
+ function makeExitRecorder() {
6
+ let resolveExited;
7
+ const exited = new Promise((resolve) => {
8
+ resolveExited = resolve;
9
+ });
10
+ const recorder = {
11
+ exitCode: null,
12
+ exited,
13
+ exit: ((code) => {
14
+ recorder.exitCode = code;
15
+ resolveExited();
16
+ // Satisfy `never` and abort the calling promise chain.
17
+ throw new Error(`__test_exit_${code}`);
18
+ }),
19
+ };
20
+ return recorder;
21
+ }
22
+ const silentLog = { info: () => { }, error: () => { } };
23
+ describe("utils::installShutdownHandlers", () => {
24
+ const uninstallers = [];
25
+ afterEach(() => {
26
+ while (uninstallers.length > 0) {
27
+ uninstallers.pop()?.();
28
+ }
29
+ });
30
+ it("calls close and exits 0 on first SIGTERM", async () => {
31
+ const exitRec = makeExitRecorder();
32
+ let closeCalled = false;
33
+ uninstallers.push(installShutdownHandlers(async () => {
34
+ closeCalled = true;
35
+ }, { exit: exitRec.exit, log: silentLog }));
36
+ process.emit("SIGTERM", "SIGTERM");
37
+ await exitRec.exited;
38
+ assert.strictEqual(closeCalled, true);
39
+ assert.strictEqual(exitRec.exitCode, 0);
40
+ });
41
+ it("exits with code 1 when cleanup exceeds timeoutMs", async () => {
42
+ const exitRec = makeExitRecorder();
43
+ let closeStarted = false;
44
+ let errorLogged = null;
45
+ let releaseClose;
46
+ const closeReleased = new Promise((resolve) => {
47
+ releaseClose = resolve;
48
+ });
49
+ uninstallers.push(installShutdownHandlers(async () => {
50
+ closeStarted = true;
51
+ await closeReleased;
52
+ }, {
53
+ exit: exitRec.exit,
54
+ timeoutMs: 5,
55
+ log: {
56
+ info: () => { },
57
+ error: (msg) => {
58
+ errorLogged = msg;
59
+ },
60
+ },
61
+ }));
62
+ process.emit("SIGTERM", "SIGTERM");
63
+ await exitRec.exited;
64
+ assert.strictEqual(closeStarted, true, "close should have been invoked");
65
+ assert.strictEqual(exitRec.exitCode, 1);
66
+ assert.ok(errorLogged !== null, "expected timeout to be logged");
67
+ assert.match(errorLogged, /timeout|exceed/i);
68
+ // Allow the in-flight close promise to settle so the test doesn't leak.
69
+ releaseClose();
70
+ });
71
+ it("forces exit 1 when a second signal arrives during cleanup", async () => {
72
+ const exitRec = makeExitRecorder();
73
+ let releaseClose;
74
+ const closeReleased = new Promise((resolve) => {
75
+ releaseClose = resolve;
76
+ });
77
+ let errorLogged = null;
78
+ uninstallers.push(installShutdownHandlers(async () => {
79
+ await closeReleased;
80
+ }, {
81
+ // Large timeout so this test can't pass via the timeout path.
82
+ timeoutMs: 60_000,
83
+ exit: exitRec.exit,
84
+ log: {
85
+ info: () => { },
86
+ error: (msg) => {
87
+ errorLogged = msg;
88
+ },
89
+ },
90
+ }));
91
+ process.emit("SIGTERM", "SIGTERM");
92
+ await new Promise((resolve) => setImmediate(resolve));
93
+ process.emit("SIGINT", "SIGINT");
94
+ await exitRec.exited;
95
+ assert.strictEqual(exitRec.exitCode, 1);
96
+ assert.ok(errorLogged !== null, "expected force-exit message to be logged");
97
+ assert.match(errorLogged, /SIGINT/);
98
+ releaseClose();
99
+ });
100
+ it("does nothing after uninstall", async () => {
101
+ const exitRec = makeExitRecorder();
102
+ let closeCalled = false;
103
+ const uninstall = installShutdownHandlers(async () => {
104
+ closeCalled = true;
105
+ }, { exit: exitRec.exit, log: silentLog });
106
+ uninstall();
107
+ process.emit("SIGTERM", "SIGTERM");
108
+ // Give any (incorrectly registered) handler a tick to fire.
109
+ await new Promise((resolve) => setImmediate(resolve));
110
+ assert.strictEqual(closeCalled, false);
111
+ assert.strictEqual(exitRec.exitCode, null);
112
+ });
113
+ it("is a no-op from the generic shutdown module", async () => {
114
+ const exitRec = makeExitRecorder();
115
+ let closeCalled = false;
116
+ const uninstall = installGenericShutdownHandlers(async () => {
117
+ closeCalled = true;
118
+ }, { exit: exitRec.exit, log: silentLog });
119
+ uninstall();
120
+ assert.strictEqual(typeof uninstall, "function");
121
+ assert.strictEqual(closeCalled, false);
122
+ assert.strictEqual(exitRec.exitCode, null);
123
+ });
124
+ it("exits with code 1 when close rejects", async () => {
125
+ const exitRec = makeExitRecorder();
126
+ let errorLogged = null;
127
+ uninstallers.push(installShutdownHandlers(async () => {
128
+ throw new Error("boom");
129
+ }, {
130
+ exit: exitRec.exit,
131
+ log: {
132
+ info: () => { },
133
+ error: (msg) => {
134
+ errorLogged = msg;
135
+ },
136
+ },
137
+ }));
138
+ process.emit("SIGTERM", "SIGTERM");
139
+ await exitRec.exited;
140
+ assert.strictEqual(exitRec.exitCode, 1);
141
+ assert.ok(errorLogged !== null, "expected error to be logged");
142
+ assert.match(errorLogged, /boom/);
143
+ });
144
+ it("exits with code 1 when close throws synchronously", async () => {
145
+ const exitRec = makeExitRecorder();
146
+ let errorLogged = null;
147
+ uninstallers.push(installShutdownHandlers(() => {
148
+ throw new Error("sync boom");
149
+ }, {
150
+ exit: exitRec.exit,
151
+ log: {
152
+ info: () => { },
153
+ error: (msg) => {
154
+ errorLogged = msg;
155
+ },
156
+ },
157
+ }));
158
+ process.emit("SIGTERM", "SIGTERM");
159
+ await exitRec.exited;
160
+ assert.strictEqual(exitRec.exitCode, 1);
161
+ assert.ok(errorLogged !== null, "expected error to be logged");
162
+ assert.match(errorLogged, /sync boom/);
163
+ });
164
+ });
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../../../../packages/extensions/ipc/server.ts"],"names":[],"mappings":"AACA,OAAO,EAAgB,KAAK,MAAM,EAAE,MAAM,UAAU,CAAC;AAIrD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAIxD,4CAA4C;AAC5C,qBAAa,SAAS;IAKA,OAAO,CAAC,QAAQ,CAAC,MAAM;IAJ3C,MAAM,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM;IAIzB,OAAO;IAEP,+CAA+C;IAC/C,IAAI,CAAC,IAAI,EAAE,SAAS,GAAG,IAAI;IAI3B,wBAAwB;IACxB,KAAK,IAAI,IAAI;CAGd;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,iBAAiB,EAAE,CAAC,MAAM,EAAE,SAAS,KAAK,UAAU,cAiFhG"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../../../../packages/extensions/ipc/server.ts"],"names":[],"mappings":"AACA,OAAO,EAAgB,KAAK,MAAM,EAAE,MAAM,UAAU,CAAC;AAIrD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAIxD,4CAA4C;AAC5C,qBAAa,SAAS;IAKA,OAAO,CAAC,QAAQ,CAAC,MAAM;IAJ3C,MAAM,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM;IAIzB,OAAO;IAEP,+CAA+C;IAC/C,IAAI,CAAC,IAAI,EAAE,SAAS,GAAG,IAAI;IAI3B,wBAAwB;IACxB,KAAK,IAAI,IAAI;CAGd;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,iBAAiB,EAAE,CAAC,MAAM,EAAE,SAAS,KAAK,UAAU,cAuFhG"}
@@ -87,6 +87,13 @@ export function startIpcServer(name, newMessageHandler) {
87
87
  controller.abort();
88
88
  // unrefing
89
89
  server.unref();
90
+ // Windows named pipes are cleaned up by the OS; Unix sockets are not.
91
+ if (!isWindows) {
92
+ try {
93
+ fs.unlinkSync(socketPath);
94
+ }
95
+ catch { }
96
+ }
90
97
  };
91
98
  }
92
99
  /**
@@ -1,5 +1,6 @@
1
1
  import { type FuzzVersion } from "#@typeberry/ext-ipc";
2
2
  import { v1 as fuzzV1 } from "#@typeberry/fuzz-proto";
3
+ import { type Closer } from "#@typeberry/utils";
3
4
  import type { JamConfig } from "./jam-config.js";
4
5
  export type FuzzConfig = {
5
6
  version: FuzzVersion;
@@ -24,5 +25,7 @@ export declare function getFuzzDetails(): {
24
25
  nodeVersion: fuzzV1.Version;
25
26
  gpVersion: fuzzV1.Version;
26
27
  };
27
- export declare function mainFuzz(fuzzConfig: FuzzConfig, withRelPath: (v: string) => string): Promise<() => void>;
28
+ export declare function mainFuzz(fuzzConfig: FuzzConfig, withRelPath: (v: string) => string): Promise<{
29
+ close: Closer;
30
+ }>;
28
31
  //# sourceMappingURL=main-fuzz.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"main-fuzz.d.ts","sourceRoot":"","sources":["../../../../../packages/jam/node/main-fuzz.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,KAAK,WAAW,EAAmB,MAAM,oBAAoB,CAAC;AACvE,OAAO,EAAE,EAAE,IAAI,MAAM,EAAE,MAAM,uBAAuB,CAAC;AAOrD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAIjD,MAAM,MAAM,UAAU,GAAG;IACvB,OAAO,EAAE,WAAW,CAAC;IACrB,aAAa,EAAE,SAAS,CAAC;IACzB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,uBAAuB,EAAE,OAAO,CAAC;CAClC,CAAC;AAoBF;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CASpF;AAED,iFAAiF;AACjF,wBAAsB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE5D;AAED,wBAAgB,cAAc;;;;EAM7B;AAED,wBAAsB,QAAQ,CAAC,UAAU,EAAE,UAAU,EAAE,WAAW,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,uBAuJxF"}
1
+ {"version":3,"file":"main-fuzz.d.ts","sourceRoot":"","sources":["../../../../../packages/jam/node/main-fuzz.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,KAAK,WAAW,EAAmB,MAAM,oBAAoB,CAAC;AACvE,OAAO,EAAE,EAAE,IAAI,MAAM,EAAE,MAAM,uBAAuB,CAAC;AAIrD,OAAO,EAAE,KAAK,MAAM,EAAoC,MAAM,kBAAkB,CAAC;AAGjF,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAIjD,MAAM,MAAM,UAAU,GAAG;IACvB,OAAO,EAAE,WAAW,CAAC;IACrB,aAAa,EAAE,SAAS,CAAC;IACzB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,uBAAuB,EAAE,OAAO,CAAC;CAClC,CAAC;AAoBF;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CASpF;AAED,iFAAiF;AACjF,wBAAsB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE5D;AAED,wBAAgB,cAAc;;;;EAM7B;AAED,wBAAsB,QAAQ,CAAC,UAAU,EAAE,UAAU,EAAE,WAAW,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,GAAG,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAgLrH"}
@@ -75,6 +75,10 @@ export async function mainFuzz(fuzzConfig, withRelPath) {
75
75
  // every reset, because opening it is the slow part. Only the in-memory blocks
76
76
  // and leaf sets are rebuilt for each vector. fjall-hybrid only.
77
77
  let fjallSession = null;
78
+ // Set when close() starts. Guards resetState so a fuzz command arriving
79
+ // mid-shutdown can't build a fresh node that close() then orphans.
80
+ let isClosing = false;
81
+ let activeReset = null;
78
82
  const chainSpec = getChainSpec(config.node.flavor);
79
83
  const closeFuzzTarget = startFuzzTarget(fuzzConfig.version, fuzzConfig.socket, {
80
84
  ...getFuzzDetails(),
@@ -98,97 +102,122 @@ export async function mainFuzz(fuzzConfig, withRelPath) {
98
102
  }
99
103
  return runningNode.getStateEntries(hash);
100
104
  },
101
- resetState: async (header, state, ancestry) => {
102
- if (runningNode !== null) {
103
- const finish = runningNode.close();
104
- runningNode = null;
105
- await finish;
106
- }
107
- const buildNode = (databaseBasePath) => {
108
- const isPersistent = databaseBasePath !== undefined;
109
- return mainImporter({
110
- ...config,
111
- node: {
112
- ...config.node,
113
- databaseBasePath,
114
- chainSpec: {
115
- ...config.node.chainSpec,
116
- genesisHeader: Encoder.encodeObject(Header.Codec, header, chainSpec),
117
- genesisState: new Map(state),
105
+ resetState: (header, state, ancestry) => {
106
+ const reset = (async () => {
107
+ if (isClosing) {
108
+ return Bytes.zero(HASH_SIZE).asOpaque();
109
+ }
110
+ if (runningNode !== null) {
111
+ const finish = runningNode.close();
112
+ runningNode = null;
113
+ await finish;
114
+ }
115
+ const buildNode = (databaseBasePath) => {
116
+ const isPersistent = databaseBasePath !== undefined;
117
+ return mainImporter({
118
+ ...config,
119
+ node: {
120
+ ...config.node,
121
+ databaseBasePath,
122
+ chainSpec: {
123
+ ...config.node.chainSpec,
124
+ genesisHeader: Encoder.encodeObject(Header.Codec, header, chainSpec),
125
+ genesisState: new Map(state),
126
+ },
118
127
  },
119
- },
120
- ancestry,
121
- network: null,
122
- }, withRelPath, {
123
- initGenesisFromAncestry: fuzzConfig.initGenesisFromAncestry,
124
- // Hybrid keeps leaf sets in RAM, so they must be windowed exactly
125
- // like the in-memory backend; only the large values live on disk.
126
- dummyFinalityDepth: 20,
127
- pruneBlocks: true,
128
- // The on-disk fuzz db is throwaway (we wipe it), so open it ephemeral and
129
- // skip the fsync, we do not need durability here. On full spec ephemeral
130
- // also turns on compression further down, so the big values do not grow the
131
- // db too much. Tiny stays uncompressed, its db is small and speed matters more.
132
- ephemeral: isPersistent,
133
- stateBackend: isPersistent ? hybridStateBackend : "lmdb",
134
- // Reuse the session keyspace (fjall-hybrid only, other backends
135
- // ignore it). Nothing to pass for the in-memory fallback.
136
- sharedFjallSession: isPersistent ? (fjallSession ?? undefined) : undefined,
137
- });
138
- };
139
- if (fuzzDbBase !== undefined) {
140
- try {
141
- if (hybridStateBackend === FUZZ_DB_FJALL) {
142
- // fjall-hybrid: open the values keyspace once and reuse it on every
143
- // reset. The values partition is content-addressed and immutable, so
144
- // it is fine that values pile up across resets, the unreferenced ones
145
- // just sit there. `initializeDatabase` decides whether the db is
146
- // already initialized from the in-memory blocks, which we rebuild on
147
- // every reset, not from the values store, so reusing it does not
148
- // resume the previous run.
149
- if (fjallSession === null) {
150
- // Start from a clean slate once, then keep the keyspace open.
128
+ ancestry,
129
+ network: null,
130
+ }, withRelPath, {
131
+ initGenesisFromAncestry: fuzzConfig.initGenesisFromAncestry,
132
+ // Hybrid keeps leaf sets in RAM, so they must be windowed exactly
133
+ // like the in-memory backend; only the large values live on disk.
134
+ dummyFinalityDepth: 20,
135
+ pruneBlocks: true,
136
+ // The on-disk fuzz db is throwaway (we wipe it), so open it ephemeral and
137
+ // skip the fsync, we do not need durability here. On full spec ephemeral
138
+ // also turns on compression further down, so the big values do not grow the
139
+ // db too much. Tiny stays uncompressed, its db is small and speed matters more.
140
+ ephemeral: isPersistent,
141
+ stateBackend: isPersistent ? hybridStateBackend : "lmdb",
142
+ // Reuse the session keyspace (fjall-hybrid only, other backends
143
+ // ignore it). Nothing to pass for the in-memory fallback.
144
+ sharedFjallSession: isPersistent ? (fjallSession ?? undefined) : undefined,
145
+ });
146
+ };
147
+ if (fuzzDbBase !== undefined) {
148
+ try {
149
+ if (hybridStateBackend === FUZZ_DB_FJALL) {
150
+ // fjall-hybrid: open the values keyspace once and reuse it on every
151
+ // reset. The values partition is content-addressed and immutable, so
152
+ // it is fine that values pile up across resets, the unreferenced ones
153
+ // just sit there. `initializeDatabase` decides whether the db is
154
+ // already initialized from the in-memory blocks, which we rebuild on
155
+ // every reset, not from the values store, so reusing it does not
156
+ // resume the previous run.
157
+ if (fjallSession === null) {
158
+ // Start from a clean slate once, then keep the keyspace open.
159
+ await wipeFuzzDb(fuzzDbBase);
160
+ fjallSession = await FjallValuesSession.open(`${withRelPath(fuzzDbBase)}/${FUZZ_FJALL_VALUES_SUBDIR}`, {
161
+ ephemeral: true,
162
+ cacheSizeBytes: FUZZ_FJALL_CACHE_BYTES,
163
+ });
164
+ logger.info `🗄️ Opened reusable fjall values session at ${withRelPath(fuzzDbBase)}/${FUZZ_FJALL_VALUES_SUBDIR}`;
165
+ }
166
+ }
167
+ else {
168
+ // lmdb-hybrid: keep the old behaviour, wipe and reopen on every
169
+ // reset. A fresh db each reset makes `initializeDatabase` set up
170
+ // genesis again instead of resuming the previous run.
151
171
  await wipeFuzzDb(fuzzDbBase);
152
- fjallSession = await FjallValuesSession.open(`${withRelPath(fuzzDbBase)}/${FUZZ_FJALL_VALUES_SUBDIR}`, {
153
- ephemeral: true,
154
- cacheSizeBytes: FUZZ_FJALL_CACHE_BYTES,
155
- });
156
- logger.info `🗄️ Opened reusable fjall values session at ${withRelPath(fuzzDbBase)}/${FUZZ_FJALL_VALUES_SUBDIR}`;
157
172
  }
173
+ runningNode = await buildNode(fuzzDbBase);
174
+ return await runningNode.getBestStateRootHash();
158
175
  }
159
- else {
160
- // lmdb-hybrid: keep the old behaviour, wipe and reopen on every
161
- // reset. A fresh db each reset makes `initializeDatabase` set up
162
- // genesis again instead of resuming the previous run.
163
- await wipeFuzzDb(fuzzDbBase);
176
+ catch (e) {
177
+ // A partially-opened db may leak on failure; acceptable for this degraded fallback (proper cleanup belongs in mainImporter).
178
+ logger.warn `Failed to open persistent fuzz db at ${fuzzDbBase}, falling back to in-memory: ${e}`;
179
+ runningNode = null;
164
180
  }
165
- runningNode = await buildNode(fuzzDbBase);
166
- return await runningNode.getBestStateRootHash();
167
181
  }
168
- catch (e) {
169
- // A partially-opened db may leak on failure; acceptable for this degraded fallback (proper cleanup belongs in mainImporter).
170
- logger.warn `Failed to open persistent fuzz db at ${fuzzDbBase}, falling back to in-memory: ${e}`;
171
- runningNode = null;
182
+ runningNode = await buildNode(undefined);
183
+ return await runningNode.getBestStateRootHash();
184
+ })();
185
+ activeReset = reset;
186
+ const clearActiveReset = () => {
187
+ if (activeReset === reset) {
188
+ activeReset = null;
172
189
  }
173
- }
174
- runningNode = await buildNode(undefined);
175
- return await runningNode.getBestStateRootHash();
190
+ };
191
+ reset.then(clearActiveReset, clearActiveReset);
192
+ return reset;
176
193
  },
177
194
  });
178
- return () => {
179
- closeFuzzTarget();
180
- // Close the reused fjall values session (if any) before wiping its files, so
181
- // the keyspace handle is released first.
182
- const closed = fjallSession?.close() ?? Promise.resolve();
183
- fjallSession = null;
184
- if (fuzzDbBase !== undefined) {
185
- // best-effort cleanup on shutdown; ignore failures (dir may already be gone).
186
- closed
187
- .catch(() => { })
188
- .finally(() => {
189
- wipeFuzzDb(fuzzDbBase).catch(() => { });
190
- });
191
- }
195
+ return {
196
+ close: async () => {
197
+ isClosing = true;
198
+ // Stop accepting connections + unlink the socket.
199
+ closeFuzzTarget();
200
+ // Drain the active session (flush + close DB). Swallow errors so a
201
+ // failing close still lets the process exit 0; the db is wiped next.
202
+ // The node references the shared fjall session, so it must close first.
203
+ if (activeReset !== null) {
204
+ await activeReset.catch((e) => logger.error `Error waiting for fuzz reset: ${e}`);
205
+ }
206
+ if (runningNode !== null) {
207
+ const node = runningNode;
208
+ runningNode = null;
209
+ await node.close().catch((e) => logger.error `Error closing fuzz node: ${e}`);
210
+ }
211
+ // Release the reused fjall values keyspace before wiping its files.
212
+ if (fjallSession !== null) {
213
+ const session = fjallSession;
214
+ fjallSession = null;
215
+ await session.close().catch((e) => logger.error `Error closing fjall session: ${e}`);
216
+ }
217
+ if (fuzzDbBase !== undefined) {
218
+ await wipeFuzzDb(fuzzDbBase).catch(() => { });
219
+ }
220
+ },
192
221
  };
193
222
  }
194
223
  function isValidStateBackend(val) {