From 6ed435b824ae3205c1c4325609fdc3a65a640d7a Mon Sep 17 00:00:00 2001 From: oXtxNt9U <120286271+oXtxNt9U@users.noreply.github.com> Date: Mon, 16 Mar 2026 11:23:16 +0900 Subject: [PATCH 1/9] enable no-misused-promise --- eslint.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eslint.config.js b/eslint.config.js index 4dd118a33c..176259658e 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -57,7 +57,7 @@ export default [ "@typescript-eslint/no-empty-object": "off", "@typescript-eslint/no-explicit-any": "error", "@typescript-eslint/no-floating-promises": "error", - "@typescript-eslint/no-misused-promises": "warn", + "@typescript-eslint/no-misused-promises": "error", "@typescript-eslint/no-non-null-asserted-optional-chain": "off", "@typescript-eslint/no-non-null-assertion": "warn", "@typescript-eslint/no-redundant-type-constituents": "warn", From 865a299312302c8ce0ef99d88f7d6a4a823eb6c6 Mon Sep 17 00:00:00 2001 From: oXtxNt9U <120286271+oXtxNt9U@users.noreply.github.com> Date: Mon, 16 Mar 2026 11:23:32 +0900 Subject: [PATCH 2/9] fix @mainsail/test-runner --- packages/test-runner/source/describe.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/test-runner/source/describe.ts b/packages/test-runner/source/describe.ts index c362b8ffa9..d0d1e23a99 100644 --- a/packages/test-runner/source/describe.ts +++ b/packages/test-runner/source/describe.ts @@ -17,11 +17,11 @@ type ContextFunction = () => T; type ContextCallback = (context: T) => Promise | void; interface CallbackArguments { - afterAll: (callback_: ContextCallback) => void; - afterEach: (callback_: ContextCallback) => void; + afterAll: (callback_: ContextCallback) => Promise; + afterEach: (callback_: ContextCallback) => Promise; assert: typeof assert; - beforeAll: (callback_: ContextCallback) => void; - beforeEach: (callback_: ContextCallback) => void; + beforeAll: (callback_: ContextCallback) => Promise; + beforeEach: (callback_: ContextCallback) => Promise; clock: (config?: number | Date | { now?: number | Date | undefined }) => sinon.SinonFakeTimers; dataset: unknown; From 0bcec465d9ad423d3b806cca1fd71e75a3730cb1 Mon Sep 17 00:00:00 2001 From: oXtxNt9U <120286271+oXtxNt9U@users.noreply.github.com> Date: Mon, 16 Mar 2026 11:23:43 +0900 Subject: [PATCH 3/9] fix @mainsail/p2p --- packages/p2p/source/service.ts | 34 +++++++++++++--------------------- 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/packages/p2p/source/service.ts b/packages/p2p/source/service.ts index b6a9fb5014..836e8061a3 100644 --- a/packages/p2p/source/service.ts +++ b/packages/p2p/source/service.ts @@ -64,7 +64,9 @@ export class Service implements Contracts.P2P.Service { await this.#checkReceivedMessages(); if (!this.#disposed) { - this.#mainLoopTimeout = setTimeout(() => this.mainLoop(), 2000); + this.#mainLoopTimeout = setTimeout(() => { + void this.mainLoop(); + }, 2000); } } @@ -108,7 +110,9 @@ export class Service implements Contracts.P2P.Service { if (!this.#disposed) { const nextTimeout = randomNumber(10, 20) * 60 * 1000; - this.#apiNodeCheckLoopTimeout = setTimeout(() => this.#checkApiNodes(), nextTimeout); + this.#apiNodeCheckLoopTimeout = setTimeout(() => { + void this.#checkApiNodes(); + }, nextTimeout); } } @@ -125,31 +129,19 @@ export class Service implements Contracts.P2P.Service { this.logger.info(`Checking ${pluralize("peer", max, true)}`, "p2p"); - // we use Promise.race to cut loose in case some communicator.ping() does not resolve within the delay - // in that case we want to keep on with our program execution while ping promises can finish in the background - await new Promise(async (resolve) => { - let isResolved = false; - - // Simulates Promise.race, but doesn't cause "multipleResolvers" process error - const resolvesFirst = () => { - if (!isResolved) { - isResolved = true; - resolve(); - } - }; - - await Promise.all( + // Wait until either all pings finish or pingDelay elapses. + // If the delay wins, continue program execution and let the remaining ping promises finish in the background. + await Promise.race([ + Promise.all( peers.map(async (peer) => { if (!(await this.peerVerifier.verify(peer))) { unresponsivePeers++; - this.peerDisposer.disposePeer(peer.ip); } }), - ).then(resolvesFirst); - - await delay(pingDelay).finally(resolvesFirst); - }); + ), + delay(pingDelay), + ]); if (unresponsivePeers > 0) { this.logger.debug(`Removed ${pluralize("peer", unresponsivePeers, true)}`, "p2p"); From c8ecf1817916e33712bf0ae1f4140b51f0054382 Mon Sep 17 00:00:00 2001 From: oXtxNt9U <120286271+oXtxNt9U@users.noreply.github.com> Date: Mon, 16 Mar 2026 11:24:06 +0900 Subject: [PATCH 4/9] fix @mainsail/kernel --- .../bootstrap/listen-to-shutdown-signals.ts | 8 ++++-- packages/kernel/source/ipc/handler.ts | 26 +++++++++++-------- .../kernel/source/services/config/watcher.ts | 21 ++++++++------- .../services/filesystem/drivers/local.ts | 4 +-- .../source/services/queue/drivers/memory.ts | 2 +- 5 files changed, 36 insertions(+), 25 deletions(-) diff --git a/packages/kernel/source/bootstrap/listen-to-shutdown-signals.ts b/packages/kernel/source/bootstrap/listen-to-shutdown-signals.ts index 9f4e579dd0..7ec5d545cc 100644 --- a/packages/kernel/source/bootstrap/listen-to-shutdown-signals.ts +++ b/packages/kernel/source/bootstrap/listen-to-shutdown-signals.ts @@ -9,9 +9,13 @@ export class ListenToShutdownSignals implements Contracts.Kernel.Bootstrapper { public async bootstrap(): Promise { for (const signal in Enums.Kernel.ShutdownSignal) { - process.on(signal, async (code) => { - await this.app.terminate(signal); + process.on(signal, (_) => { + void this.#onSignal(signal); }); } } + + async #onSignal(signal: string) { + await this.app.terminate(signal); + } } diff --git a/packages/kernel/source/ipc/handler.ts b/packages/kernel/source/ipc/handler.ts index 221d857b83..929efaadd1 100644 --- a/packages/kernel/source/ipc/handler.ts +++ b/packages/kernel/source/ipc/handler.ts @@ -11,17 +11,21 @@ export class Handler implements Contracts.Kernel.IPC.Handler { - if (this.handler[message.method] === undefined) { - throw new Error(`Method ${message.method} is not defined on the handler`); - } - - try { - const result = await this.handler[message.method](...message.args); - parentPort?.postMessage({ id: message.id, result }); - } catch (error) { - parentPort?.postMessage({ error: error.message, id: message.id }); - } + parentPort?.on("message", (message) => { + void this.#onMessage(message); }); } + + async #onMessage(message: { method: string; id: string; args: [] }) { + if (this.handler[message.method] === undefined) { + throw new Error(`Method ${message.method} is not defined on the handler`); + } + + try { + const result = await this.handler[message.method](...message.args); + parentPort?.postMessage({ id: message.id, result }); + } catch (error) { + parentPort?.postMessage({ error: error.message, id: message.id }); + } + } } diff --git a/packages/kernel/source/services/config/watcher.ts b/packages/kernel/source/services/config/watcher.ts index ce4a4c470c..df9cd07e26 100644 --- a/packages/kernel/source/services/config/watcher.ts +++ b/packages/kernel/source/services/config/watcher.ts @@ -9,22 +9,25 @@ export class Watcher { private readonly app!: Contracts.Kernel.Application; #watcher!: nsfw.NSFW; + #configFiles = new Set([".env", "validators.json", "peers.json", "plugins.js", "plugins.json"]); public async boot(): Promise { - const configFiles = new Set([".env", "validators.json", "peers.json", "plugins.js", "plugins.json"]); - - this.#watcher = await nsfw(this.app.configPath(), async (events) => { - for (const event of events) { - if (event.action === nsfw.ActionType.MODIFIED && configFiles.has(event.file)) { - await this.app.reboot(); - break; - } - } + this.#watcher = await nsfw(this.app.configPath(), (events) => { + void this.#handleEvents(events); }); await this.#watcher.start(); } + async #handleEvents(events: nsfw.FileChangeEvent[]) { + for (const event of events) { + if (event.action === nsfw.ActionType.MODIFIED && this.#configFiles.has(event.file)) { + await this.app.reboot(); + break; + } + } + } + public async dispose(): Promise { return this.#watcher.stop(); } diff --git a/packages/kernel/source/services/filesystem/drivers/local.ts b/packages/kernel/source/services/filesystem/drivers/local.ts index a19c847e02..8a06043b02 100644 --- a/packages/kernel/source/services/filesystem/drivers/local.ts +++ b/packages/kernel/source/services/filesystem/drivers/local.ts @@ -80,7 +80,7 @@ export class LocalFilesystem implements Contracts.Kernel.Filesystem { return this.fs .readdirSync(directory) .map((item: string) => `${directory}/${item}`) - .filter(async (item: string) => this.fs.lstatSync(item).isFile()); + .filter((item: string) => this.fs.lstatSync(item).isFile()); } public async directories(directory: string): Promise { @@ -89,7 +89,7 @@ export class LocalFilesystem implements Contracts.Kernel.Filesystem { return this.fs .readdirSync(directory) .map((item: string) => `${directory}/${item}`) - .filter(async (item: string) => this.fs.lstatSync(item).isDirectory()); + .filter((item: string) => this.fs.lstatSync(item).isDirectory()); } public async makeDirectory(path: string): Promise { diff --git a/packages/kernel/source/services/queue/drivers/memory.ts b/packages/kernel/source/services/queue/drivers/memory.ts index aa89bc6314..122f272983 100644 --- a/packages/kernel/source/services/queue/drivers/memory.ts +++ b/packages/kernel/source/services/queue/drivers/memory.ts @@ -77,7 +77,7 @@ export class MemoryQueue extends EventEmitter implements Contracts.Kernel.Queue } public async later(delay: number, job: Contracts.Kernel.QueueJob): Promise { - setTimeout(() => this.push(job), delay); + setTimeout(() => void this.push(job), delay); } public async bulk(jobs: Contracts.Kernel.QueueJob[]): Promise { From 8967d34c060e0164339af37eae65a8756e4c1c4e Mon Sep 17 00:00:00 2001 From: oXtxNt9U <120286271+oXtxNt9U@users.noreply.github.com> Date: Mon, 16 Mar 2026 11:24:23 +0900 Subject: [PATCH 5/9] fix @mainsail/consensus --- packages/consensus/source/scheduler.ts | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/packages/consensus/source/scheduler.ts b/packages/consensus/source/scheduler.ts index 981bc2ff51..967a88df27 100644 --- a/packages/consensus/source/scheduler.ts +++ b/packages/consensus/source/scheduler.ts @@ -33,9 +33,13 @@ export class Scheduler implements Contracts.Consensus.Scheduler { const timeout = Math.max(0, timestamp - dayjs().valueOf()); - this.#timeoutStartRound = setTimeout(async () => { + const run = async () => { await this.#getConsensus().onTimeoutStartRound(); this.#timeoutStartRound = undefined; + }; + + this.#timeoutStartRound = setTimeout(() => { + void run(); }, timeout); return true; @@ -46,9 +50,13 @@ export class Scheduler implements Contracts.Consensus.Scheduler { return false; } - this.#timeoutPropose = setTimeout(async () => { + const run = async () => { await this.#getConsensus().onTimeoutPropose(height, round); this.#timeoutPropose = undefined; + }; + + this.#timeoutPropose = setTimeout(() => { + void run(); }, this.#getTimeout(round)); return true; @@ -59,9 +67,13 @@ export class Scheduler implements Contracts.Consensus.Scheduler { return false; } - this.#timeoutPrevote = setTimeout(async () => { + const run = async () => { await this.#getConsensus().onTimeoutPrevote(height, round); this.#timeoutPrevote = undefined; + }; + + this.#timeoutPrevote = setTimeout(() => { + void run(); }, this.#getTimeout(round)); return true; @@ -72,9 +84,13 @@ export class Scheduler implements Contracts.Consensus.Scheduler { return false; } - this.#timeoutPrecommit = setTimeout(async () => { + const run = async () => { await this.#getConsensus().onTimeoutPrecommit(height, round); this.#timeoutPrecommit = undefined; + }; + + this.#timeoutPrecommit = setTimeout(() => { + void run(); }, this.#getTimeout(round)); return true; From 04f5edd32366d70f35af9c83719c533fdf214c04 Mon Sep 17 00:00:00 2001 From: oXtxNt9U <120286271+oXtxNt9U@users.noreply.github.com> Date: Mon, 16 Mar 2026 11:24:36 +0900 Subject: [PATCH 6/9] fix @mainsail/api-sync --- .../source/listeners/abstract-listener.ts | 4 +- packages/api-sync/source/tokens/whitelist.ts | 38 ++++++++----------- 2 files changed, 19 insertions(+), 23 deletions(-) diff --git a/packages/api-sync/source/listeners/abstract-listener.ts b/packages/api-sync/source/listeners/abstract-listener.ts index 97e9e71d36..19943b3583 100644 --- a/packages/api-sync/source/listeners/abstract-listener.ts +++ b/packages/api-sync/source/listeners/abstract-listener.ts @@ -60,7 +60,9 @@ export abstract class AbstractListener imple } catch (ex) { this.logger.error(`#syncToDatabaseTransaction failed: ${ex}`); } finally { - this.#syncTimeout = setTimeout(run, syncInterval); + this.#syncTimeout = setTimeout(() => { + void run(); + }, syncInterval); } }; diff --git a/packages/api-sync/source/tokens/whitelist.ts b/packages/api-sync/source/tokens/whitelist.ts index 5f318626a3..934b3afcc2 100644 --- a/packages/api-sync/source/tokens/whitelist.ts +++ b/packages/api-sync/source/tokens/whitelist.ts @@ -31,38 +31,32 @@ export class TokenWhitelist { @inject(Identifiers.Cryptography.Identity.Address.Factory) private readonly addressFactory!: Contracts.Crypto.AddressFactory; - #syncInterval?: NodeJS.Timeout; + #syncTimeout?: NodeJS.Timeout; public async bootstrap(): Promise { const syncInterval = this.#getTokenWhitelistRefreshIntervalMs(); - let running = false; - this.logger.info(`Starting TokenWhitelist using remote: ${this.#getTokenWhitelistRemoteUrl()}`); - this.#syncWhitelist() - .catch((error) => this.logger.error(`#syncWhitelist failed: ${error}`)) - .finally(() => { - this.#syncInterval = setInterval(async () => { - if (running) { - return; - } - - running = true; - - try { - await this.#syncWhitelist(); - } catch (ex) { - this.logger.error(`#syncWhitelist failed: ${ex}`); - } finally { - running = false; - } + const run = async () => { + try { + await this.#syncWhitelist(); + } catch (ex) { + this.logger.error(`#syncWhitelist failed: ${ex}`); + } finally { + this.#syncTimeout = setTimeout(() => { + void run(); }, syncInterval); - }); + } + }; + + void run(); } public async dispose(): Promise { - clearInterval(this.#syncInterval); + if (this.#syncTimeout) { + clearTimeout(this.#syncTimeout); + } } async #syncWhitelist(): Promise { From b91d9980291e0b857b4d974e70948cd847980914 Mon Sep 17 00:00:00 2001 From: oXtxNt9U <120286271+oXtxNt9U@users.noreply.github.com> Date: Tue, 17 Mar 2026 09:14:47 +0900 Subject: [PATCH 7/9] revert p2p --- packages/p2p/source/service.ts | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/packages/p2p/source/service.ts b/packages/p2p/source/service.ts index 836e8061a3..2f372a9d8b 100644 --- a/packages/p2p/source/service.ts +++ b/packages/p2p/source/service.ts @@ -129,19 +129,34 @@ export class Service implements Contracts.P2P.Service { this.logger.info(`Checking ${pluralize("peer", max, true)}`, "p2p"); - // Wait until either all pings finish or pingDelay elapses. - // If the delay wins, continue program execution and let the remaining ping promises finish in the background. - await Promise.race([ - Promise.all( + // we use Promise.race to cut loose in case some communicator.ping() does not resolve within the delay + // in that case we want to keep on with our program execution while ping promises can finish in the background + // TODO: revisit + /* eslint-disable @typescript-eslint/no-misused-promises */ + await new Promise(async (resolve) => { + let isResolved = false; + + // Simulates Promise.race, but doesn't cause "multipleResolvers" process error + const resolvesFirst = () => { + if (!isResolved) { + isResolved = true; + resolve(); + } + }; + + await Promise.all( peers.map(async (peer) => { if (!(await this.peerVerifier.verify(peer))) { unresponsivePeers++; + this.peerDisposer.disposePeer(peer.ip); } }), - ), - delay(pingDelay), - ]); + ).then(resolvesFirst); + + await delay(pingDelay).finally(resolvesFirst); + }); + /* eslint-enable */ if (unresponsivePeers > 0) { this.logger.debug(`Removed ${pluralize("peer", unresponsivePeers, true)}`, "p2p"); From 4aac661fda1f9f5e59a7cea56da95d75e95863fd Mon Sep 17 00:00:00 2001 From: oXtxNt9U <120286271+oXtxNt9U@users.noreply.github.com> Date: Tue, 17 Mar 2026 09:27:40 +0900 Subject: [PATCH 8/9] add setTimeoutAsync util --- packages/utils/source/index.ts | 1 + .../utils/source/set-timeout-async.test.ts | 22 +++++++++++++++++++ packages/utils/source/set-timeout-async.ts | 6 +++++ 3 files changed, 29 insertions(+) create mode 100644 packages/utils/source/set-timeout-async.test.ts create mode 100644 packages/utils/source/set-timeout-async.ts diff --git a/packages/utils/source/index.ts b/packages/utils/source/index.ts index 6a07c29047..98399a5c25 100644 --- a/packages/utils/source/index.ts +++ b/packages/utils/source/index.ts @@ -169,6 +169,7 @@ export * from "./reverse.js"; export * from "./sample.js"; export * from "./semver.js"; export * from "./set.js"; +export * from "./set-timeout-async.js"; export * from "./shuffle.js"; export * from "./sleep.js"; export * from "./snake-case.js"; diff --git a/packages/utils/source/set-timeout-async.test.ts b/packages/utils/source/set-timeout-async.test.ts new file mode 100644 index 0000000000..0f49a38923 --- /dev/null +++ b/packages/utils/source/set-timeout-async.test.ts @@ -0,0 +1,22 @@ +import { describe } from "@mainsail/test-runner"; +import { setTimeoutAsync } from "./set-timeout-async"; + +describe("setTimeoutAsync", async ({ assert, it }) => { + it("should be ok", async () => { + const events: string[] = []; + + const done = new Promise((resolve) => { + const timeout = setTimeoutAsync(async () => { + events.push("callback"); + await Promise.resolve(); + resolve(); + }, 100); + }); + + events.push("after-call"); + + await done; + + assert.equal(events, ["after-call", "callback"]); + }); +}); diff --git a/packages/utils/source/set-timeout-async.ts b/packages/utils/source/set-timeout-async.ts new file mode 100644 index 0000000000..57d53bf830 --- /dev/null +++ b/packages/utils/source/set-timeout-async.ts @@ -0,0 +1,6 @@ +export const setTimeoutAsync = (callback: () => Promise, delay: number): NodeJS.Timeout => + setTimeout(() => { + void (async () => { + await callback(); + })(); + }, delay); From bc8bdc15b67f98eda85db531e1dc1874b6379cb5 Mon Sep 17 00:00:00 2001 From: oXtxNt9U <120286271+oXtxNt9U@users.noreply.github.com> Date: Tue, 17 Mar 2026 09:28:00 +0900 Subject: [PATCH 9/9] use new helper --- .../source/listeners/abstract-listener.ts | 5 ++-- packages/consensus/source/scheduler.ts | 25 ++++--------------- 2 files changed, 7 insertions(+), 23 deletions(-) diff --git a/packages/api-sync/source/listeners/abstract-listener.ts b/packages/api-sync/source/listeners/abstract-listener.ts index 19943b3583..6a98c96173 100644 --- a/packages/api-sync/source/listeners/abstract-listener.ts +++ b/packages/api-sync/source/listeners/abstract-listener.ts @@ -7,6 +7,7 @@ import { Identifiers } from "@mainsail/constants"; import { inject, injectable, tagged } from "@mainsail/container"; import type { Contracts } from "@mainsail/contracts"; import { NotImplemented } from "@mainsail/exceptions"; +import { setTimeoutAsync } from "@mainsail/utils"; import { EventListener } from "../contracts.js"; @@ -60,9 +61,7 @@ export abstract class AbstractListener imple } catch (ex) { this.logger.error(`#syncToDatabaseTransaction failed: ${ex}`); } finally { - this.#syncTimeout = setTimeout(() => { - void run(); - }, syncInterval); + this.#syncTimeout = setTimeoutAsync(run, syncInterval); } }; diff --git a/packages/consensus/source/scheduler.ts b/packages/consensus/source/scheduler.ts index 967a88df27..b94a77bdfc 100644 --- a/packages/consensus/source/scheduler.ts +++ b/packages/consensus/source/scheduler.ts @@ -1,6 +1,7 @@ import { Identifiers } from "@mainsail/constants"; import { inject, injectable } from "@mainsail/container"; import type { Contracts } from "@mainsail/contracts"; +import { setTimeoutAsync } from "@mainsail/utils"; import dayjs from "dayjs"; @injectable() @@ -33,13 +34,9 @@ export class Scheduler implements Contracts.Consensus.Scheduler { const timeout = Math.max(0, timestamp - dayjs().valueOf()); - const run = async () => { + this.#timeoutStartRound = setTimeoutAsync(async () => { await this.#getConsensus().onTimeoutStartRound(); this.#timeoutStartRound = undefined; - }; - - this.#timeoutStartRound = setTimeout(() => { - void run(); }, timeout); return true; @@ -50,13 +47,9 @@ export class Scheduler implements Contracts.Consensus.Scheduler { return false; } - const run = async () => { + this.#timeoutPropose = setTimeoutAsync(async () => { await this.#getConsensus().onTimeoutPropose(height, round); this.#timeoutPropose = undefined; - }; - - this.#timeoutPropose = setTimeout(() => { - void run(); }, this.#getTimeout(round)); return true; @@ -67,13 +60,9 @@ export class Scheduler implements Contracts.Consensus.Scheduler { return false; } - const run = async () => { + this.#timeoutPrevote = setTimeoutAsync(async () => { await this.#getConsensus().onTimeoutPrevote(height, round); this.#timeoutPrevote = undefined; - }; - - this.#timeoutPrevote = setTimeout(() => { - void run(); }, this.#getTimeout(round)); return true; @@ -84,13 +73,9 @@ export class Scheduler implements Contracts.Consensus.Scheduler { return false; } - const run = async () => { + this.#timeoutPrecommit = setTimeoutAsync(async () => { await this.#getConsensus().onTimeoutPrecommit(height, round); this.#timeoutPrecommit = undefined; - }; - - this.#timeoutPrecommit = setTimeout(() => { - void run(); }, this.#getTimeout(round)); return true;