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", diff --git a/packages/api-sync/source/listeners/abstract-listener.ts b/packages/api-sync/source/listeners/abstract-listener.ts index 97e9e71d36..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,7 +61,7 @@ export abstract class AbstractListener imple } catch (ex) { this.logger.error(`#syncToDatabaseTransaction failed: ${ex}`); } finally { - this.#syncTimeout = setTimeout(run, syncInterval); + this.#syncTimeout = setTimeoutAsync(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 { diff --git a/packages/consensus/source/scheduler.ts b/packages/consensus/source/scheduler.ts index 981bc2ff51..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,7 +34,7 @@ export class Scheduler implements Contracts.Consensus.Scheduler { const timeout = Math.max(0, timestamp - dayjs().valueOf()); - this.#timeoutStartRound = setTimeout(async () => { + this.#timeoutStartRound = setTimeoutAsync(async () => { await this.#getConsensus().onTimeoutStartRound(); this.#timeoutStartRound = undefined; }, timeout); @@ -46,7 +47,7 @@ export class Scheduler implements Contracts.Consensus.Scheduler { return false; } - this.#timeoutPropose = setTimeout(async () => { + this.#timeoutPropose = setTimeoutAsync(async () => { await this.#getConsensus().onTimeoutPropose(height, round); this.#timeoutPropose = undefined; }, this.#getTimeout(round)); @@ -59,7 +60,7 @@ export class Scheduler implements Contracts.Consensus.Scheduler { return false; } - this.#timeoutPrevote = setTimeout(async () => { + this.#timeoutPrevote = setTimeoutAsync(async () => { await this.#getConsensus().onTimeoutPrevote(height, round); this.#timeoutPrevote = undefined; }, this.#getTimeout(round)); @@ -72,7 +73,7 @@ export class Scheduler implements Contracts.Consensus.Scheduler { return false; } - this.#timeoutPrecommit = setTimeout(async () => { + this.#timeoutPrecommit = setTimeoutAsync(async () => { await this.#getConsensus().onTimeoutPrecommit(height, round); this.#timeoutPrecommit = undefined; }, this.#getTimeout(round)); 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 { diff --git a/packages/p2p/source/service.ts b/packages/p2p/source/service.ts index b6a9fb5014..2f372a9d8b 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); } } @@ -127,6 +131,8 @@ export class Service implements Contracts.P2P.Service { // 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; @@ -150,6 +156,7 @@ export class Service implements Contracts.P2P.Service { await delay(pingDelay).finally(resolvesFirst); }); + /* eslint-enable */ if (unresponsivePeers > 0) { this.logger.debug(`Removed ${pluralize("peer", unresponsivePeers, true)}`, "p2p"); 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; 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);