diff --git a/.changeset/long-turkeys-impress.md b/.changeset/long-turkeys-impress.md new file mode 100644 index 00000000..c8e68856 --- /dev/null +++ b/.changeset/long-turkeys-impress.md @@ -0,0 +1,11 @@ +--- +"@blobscan/rest-api-server": minor +"@blobscan/syncers": minor +--- + +Refactored the stats syncer package to support general-purpose synchronization workers/queues. + +Key changes include: + + • Renamed the package to syncers. + • Exported each syncer directly, removing the StatsSyncer managing entity. diff --git a/apps/rest-api-server/package.json b/apps/rest-api-server/package.json index 68af875f..ba869f47 100644 --- a/apps/rest-api-server/package.json +++ b/apps/rest-api-server/package.json @@ -18,8 +18,7 @@ "@blobscan/api": "workspace:^0.9.0", "@blobscan/logger": "workspace:^0.1.0", "@blobscan/open-telemetry": "workspace:^0.0.7", - "@blobscan/stats-syncer": "workspace:^0.1.8", - "@blobscan/swarm-syncer": "workspace:^0.0.1", + "@blobscan/syncers": "workspace:^0.1.9", "@blobscan/zod": "workspace:^0.1.0", "@opentelemetry/instrumentation-express": "^0.33.0", "@sentry/node": "^7.109.0", diff --git a/apps/rest-api-server/src/index.ts b/apps/rest-api-server/src/index.ts index b1f8b35a..e2120081 100644 --- a/apps/rest-api-server/src/index.ts +++ b/apps/rest-api-server/src/index.ts @@ -14,38 +14,16 @@ import { gracefulShutdown as apiGracefulShutdown, } from "@blobscan/api"; import { collectDefaultMetrics } from "@blobscan/open-telemetry"; -import { StatsSyncer } from "@blobscan/stats-syncer"; -import { SwarmStampSyncer } from "@blobscan/swarm-syncer"; import { env } from "./env"; import { logger } from "./logger"; import { morganMiddleware } from "./morgan"; import { openApiDocument } from "./openapi"; -import { getNetworkDencunForkSlot } from "./utils"; +import { setUpSyncers } from "./syncers"; collectDefaultMetrics(); -const statsSyncer = new StatsSyncer({ - redisUri: env.REDIS_URI, - lowestSlot: - env.DENCUN_FORK_SLOT ?? getNetworkDencunForkSlot(env.NETWORK_NAME), -}); - -statsSyncer.start({ - cronPatterns: { - daily: env.STATS_SYNCER_DAILY_CRON_PATTERN, - overall: env.STATS_SYNCER_OVERALL_CRON_PATTERN, - }, -}); - - -if (env.SWARM_STORAGE_ENABLED && env.SWARM_BATCH_ID) { - const swarmSyncer = new SwarmStampSyncer({ - redisUri: env.REDIS_URI, - env.BEE_ENDPOINT, //FIXME - ); - swarmSyncer.start(env.SWARM_SYNCER_CRON); -} +const closeSyncers = setUpSyncers(); const app = express(); @@ -83,7 +61,7 @@ async function gracefulShutdown(signal: string) { logger.debug(`Received ${signal}. Shutting down...`); await apiGracefulShutdown() - .finally(() => statsSyncer.close()) + .finally(() => closeSyncers()) .finally(() => { server.close(() => { logger.debug("Server shut down successfully"); diff --git a/apps/rest-api-server/src/syncers.ts b/apps/rest-api-server/src/syncers.ts new file mode 100644 index 00000000..af080559 --- /dev/null +++ b/apps/rest-api-server/src/syncers.ts @@ -0,0 +1,30 @@ +import { + DailyStatsSyncer, + OverallStatsSyncer, + createRedisConnection, +} from "@blobscan/syncers"; + +import { env } from "./env"; +import { getNetworkDencunForkSlot } from "./utils"; + +export function setUpSyncers() { + const connection = createRedisConnection(env.REDIS_URI); + + const dailyStatsSyncer = new DailyStatsSyncer({ + cronPattern: env.STATS_SYNCER_DAILY_CRON_PATTERN, + redisUriOrConnection: connection, + }); + + const overallStatsSyncer = new OverallStatsSyncer({ + cronPattern: env.STATS_SYNCER_OVERALL_CRON_PATTERN, + redisUriOrConnection: connection, + lowestSlot: + env.DENCUN_FORK_SLOT ?? getNetworkDencunForkSlot(env.NETWORK_NAME), + }); + + Promise.all([dailyStatsSyncer.start(), overallStatsSyncer.start()]); + + return () => { + return dailyStatsSyncer.close().finally(() => overallStatsSyncer.close()); + }; +} diff --git a/packages/stats-syncer/src/PeriodicUpdater.ts b/packages/stats-syncer/src/PeriodicUpdater.ts deleted file mode 100644 index 6ef76c0d..00000000 --- a/packages/stats-syncer/src/PeriodicUpdater.ts +++ /dev/null @@ -1,119 +0,0 @@ -/* eslint-disable @typescript-eslint/no-misused-promises */ -import { Queue, Worker } from "bullmq"; -import type { Redis } from "ioredis"; - -import { createModuleLogger } from "@blobscan/logger"; -import type { Logger } from "@blobscan/logger"; - -import { ErrorException, PeriodicUpdaterError } from "./errors"; -import { createRedisConnection } from "./utils"; - -export type PeriodicUpdaterConfig = { - name: string; - redisUriOrConnection: string | Redis; - updaterFn: () => Promise; -}; - -export class PeriodicUpdater { - name: string; - protected worker: Worker; - protected queue: Queue; - protected updaterFn: () => Promise; - protected logger: Logger; - - constructor({ - name, - redisUriOrConnection, - updaterFn, - }: PeriodicUpdaterConfig) { - const isRedisUri = typeof redisUriOrConnection === "string"; - this.name = name; - this.logger = createModuleLogger("stats-syncer", this.name); - - let connection: Redis; - - if (isRedisUri) { - connection = createRedisConnection(redisUriOrConnection); - - connection.on("error", (err) => { - this.logger.error( - new ErrorException("A Redis connection error ocurred", err) - ); - }); - } else { - connection = redisUriOrConnection; - } - - this.queue = new Queue(this.name, { - connection, - }); - - this.worker = new Worker(this.queue.name, updaterFn, { - connection, - }); - - this.updaterFn = updaterFn; - - this.queue.on("error", (err) => { - this.logger.error(new ErrorException("A queue error occurred", err)); - }); - - this.worker.on("failed", (_, err) => { - this.logger.error(new ErrorException("A worker error ocurred", err)); - }); - } - - async start(cronPattern: string) { - try { - const jobName = `${this.name}-job`; - const repeatableJob = await this.queue.add(jobName, null, { - repeat: { - pattern: cronPattern, - }, - }); - - return repeatableJob; - } catch (err) { - throw new PeriodicUpdaterError( - this.name, - "An error ocurred when starting updater", - err - ); - } - } - - close() { - const teardownPromise: Promise = Promise.resolve(); - - return teardownPromise - .finally(async () => { - await this.#performClosingOperation(() => - this.worker.removeAllListeners().close(true) - ); - }) - .finally(async () => { - await this.#performClosingOperation(() => - this.queue.obliterate({ force: true }) - ); - }) - .finally(async () => { - await this.#performClosingOperation(() => - this.queue.removeAllListeners().close() - ); - }); - } - - async #performClosingOperation(operation: () => Promise) { - try { - await operation(); - } catch (err) { - const err_ = new PeriodicUpdaterError( - this.name, - "An error ocurred when performing closing operation", - err - ); - - throw err_; - } - } -} diff --git a/packages/stats-syncer/src/StatsSyncer.ts b/packages/stats-syncer/src/StatsSyncer.ts deleted file mode 100644 index 54f19dcb..00000000 --- a/packages/stats-syncer/src/StatsSyncer.ts +++ /dev/null @@ -1,84 +0,0 @@ -/* eslint-disable @typescript-eslint/no-misused-promises */ -import type { Redis } from "ioredis"; - -import { ErrorException, StatsSyncerError } from "./errors"; -import { logger } from "./logger"; -import { DailyStatsUpdater } from "./updaters/DailyStatsUpdater"; -import { OverallStatsUpdater } from "./updaters/OverallStatsUpdater"; -import { createRedisConnection } from "./utils"; - -export type StatsSyncerOptions = { - redisUri: string; - lowestSlot?: number; -}; - -export class StatsSyncer { - protected connection: Redis; - protected dailyStatsUpdater: DailyStatsUpdater; - protected overallStatsUpdater: OverallStatsUpdater; - - constructor({ redisUri, lowestSlot }: StatsSyncerOptions) { - const connection = createRedisConnection(redisUri); - - connection.on("error", (err) => { - logger.error(new ErrorException("The Redis connection failed", err)); - }); - - this.connection = connection; - this.dailyStatsUpdater = new DailyStatsUpdater(connection); - this.overallStatsUpdater = new OverallStatsUpdater(connection, { - lowestSlot, - }); - } - - async start(config: { - cronPatterns: { - daily: string; - overall: string; - }; - }) { - try { - const cronPatterns = config.cronPatterns; - - await Promise.all([ - this.dailyStatsUpdater.start(cronPatterns.daily), - this.overallStatsUpdater.start(cronPatterns.overall), - ]); - - logger.info("Stats syncer started successfully."); - } catch (err) { - const err_ = new StatsSyncerError( - "An error occurred when starting syncer", - err - ); - - logger.error(err_); - - throw err_; - } - } - - async close() { - try { - await this.dailyStatsUpdater - .close() - .finally(() => this.overallStatsUpdater.close()) - .finally(() => { - this.connection.removeAllListeners(); - - if (this.connection.status === "ready") this.connection.disconnect(); - }); - - logger.info("Stats syncer closed successfully."); - } catch (err) { - const err_ = new StatsSyncerError( - "An error ocurred when closing syncer", - err - ); - - logger.error(err_); - - throw err_; - } - } -} diff --git a/packages/stats-syncer/src/errors.ts b/packages/stats-syncer/src/errors.ts deleted file mode 100644 index ab95c6e9..00000000 --- a/packages/stats-syncer/src/errors.ts +++ /dev/null @@ -1,21 +0,0 @@ -export class ErrorException extends Error { - constructor(message: string, cause?: unknown) { - super(message, { - cause, - }); - - this.name = this.constructor.name; - } -} - -export class StatsSyncerError extends ErrorException { - constructor(message: string, cause: unknown) { - super(`Stats syncer failed: ${message}`, cause); - } -} - -export class PeriodicUpdaterError extends ErrorException { - constructor(updaterName: string, message: string, cause: unknown) { - super(`Updater "${updaterName}" failed: ${message}`, cause); - } -} diff --git a/packages/stats-syncer/src/index.ts b/packages/stats-syncer/src/index.ts deleted file mode 100644 index fcfe8511..00000000 --- a/packages/stats-syncer/src/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { StatsSyncer } from "./StatsSyncer"; -export type { PeriodicUpdater, PeriodicUpdaterConfig } from "./PeriodicUpdater"; diff --git a/packages/stats-syncer/src/logger.ts b/packages/stats-syncer/src/logger.ts deleted file mode 100644 index 96c6dcf6..00000000 --- a/packages/stats-syncer/src/logger.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { createModuleLogger } from "@blobscan/logger"; - -export const logger = createModuleLogger("stats-syncer"); diff --git a/packages/stats-syncer/test/OverallStatsUpdater.test.ts b/packages/stats-syncer/test/OverallStatsUpdater.test.ts deleted file mode 100644 index bed3404d..00000000 --- a/packages/stats-syncer/test/OverallStatsUpdater.test.ts +++ /dev/null @@ -1,222 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; - -import { prisma } from "@blobscan/db"; -import { fixtures, omitDBTimestampFields } from "@blobscan/test"; - -import { OverallStatsUpdater } from "../src/updaters/OverallStatsUpdater"; - -class OverallStatsUpdaterMock extends OverallStatsUpdater { - constructor( - redisUri = process.env.REDIS_URI ?? "", - config: ConstructorParameters[1] = {} - ) { - const lowestSlot = - config.lowestSlot ?? fixtures.blockchainSyncState[0]?.lastLowerSyncedSlot; - super(redisUri, { - ...config, - lowestSlot, - }); - } - - getWorker() { - return this.worker; - } - - getWorkerProcessor() { - return this.updaterFn; - } - - getQueue() { - return this.queue; - } -} - -function getAllOverallStats() { - const uniqueArgs = { - where: { - id: 1, - }, - }; - - return Promise.all([ - prisma.blobOverallStats.findUnique(uniqueArgs), - prisma.blockOverallStats.findUnique(uniqueArgs), - prisma.transactionOverallStats.findUnique(uniqueArgs), - ]).then((allOverallStats) => - allOverallStats.map((stats) => - stats ? omitDBTimestampFields(stats) : undefined - ) - ); -} - -describe("OverallStatsUpdater", () => { - let overallStatsUpdater: OverallStatsUpdaterMock; - - beforeEach(() => { - overallStatsUpdater = new OverallStatsUpdaterMock(); - - return async () => { - await overallStatsUpdater.close(); - }; - }); - - it("should aggregate all overall stats correctly", async () => { - const workerProcessor = overallStatsUpdater.getWorkerProcessor(); - - const incrementTransactionSpy = vi.spyOn(prisma, "$transaction"); - - await workerProcessor(); - - const [blobOverallStats, blockOverallStats, transactionOverallStats] = - await getAllOverallStats(); - - expect( - incrementTransactionSpy, - "Expect to aggregate overall stats within a transaction" - ).toHaveBeenCalledOnce(); - expect(blobOverallStats, "Incorrect blob overall stats aggregation") - .toMatchInlineSnapshot(` - { - "avgBlobSize": 1175, - "id": 1, - "totalBlobSize": 9400n, - "totalBlobs": 8, - "totalUniqueBlobs": 1, - } - `); - expect(blockOverallStats, "Incorrect block overall stats aggregation") - .toMatchInlineSnapshot(` - { - "avgBlobAsCalldataFee": 5406666.666666667, - "avgBlobFee": 114000000, - "avgBlobGasPrice": 21.33333333333333, - "id": 1, - "totalBlobAsCalldataFee": "16220000", - "totalBlobAsCalldataGasUsed": "760000", - "totalBlobFee": "342000000", - "totalBlobGasUsed": "16000000", - "totalBlocks": 3, - } - `); - expect( - transactionOverallStats, - "Incorrect transaction overall stats aggregation" - ).toMatchInlineSnapshot(` - { - "avgMaxBlobGasFee": 100, - "id": 1, - "totalTransactions": 4, - "totalUniqueReceivers": 0, - "totalUniqueSenders": 0, - } - `); - }); - - it("should aggregate overall stats in batches correctly when there are too many blocks", async () => { - const batchSize = 2; - const workerProcessor = new OverallStatsUpdaterMock(undefined, { - batchSize, - }).getWorkerProcessor(); - const incrementTransactionSpy = vi.spyOn(prisma, "$transaction"); - const blockchainSyncState = fixtures.blockchainSyncState[0]; - const lastAggregatedBlock = blockchainSyncState - ? blockchainSyncState.lastAggregatedBlock + 1 - : 0; - const lastFinalizedBlock = - fixtures.blockchainSyncState[0]?.lastFinalizedBlock ?? 0; - const batches = Math.ceil( - (lastFinalizedBlock - lastAggregatedBlock + 1) / batchSize - ); - - await workerProcessor(); - - expect( - incrementTransactionSpy, - "Incorrect number of stats aggregation calls" - ).toHaveBeenCalledTimes(batches); - }); - - it("should update last aggregated block to last finalized block after aggregation", async () => { - const workerProcessor = overallStatsUpdater.getWorkerProcessor(); - const expectedLastAggregatedBlock = - fixtures.blockchainSyncState[0]?.lastFinalizedBlock; - - await workerProcessor(); - - const lastAggregatedBlock = await prisma.blockchainSyncState - .findUnique({ - select: { - lastAggregatedBlock: true, - }, - where: { - id: 1, - }, - }) - .then((state) => state?.lastAggregatedBlock); - - expect(lastAggregatedBlock).toBe(expectedLastAggregatedBlock); - }); - - it("should skip aggregation when no finalized block has been set", async () => { - const workerProcessor = overallStatsUpdater.getWorkerProcessor(); - - await prisma.blockchainSyncState.update({ - data: { - lastFinalizedBlock: null, - }, - where: { - id: 1, - }, - }); - - await workerProcessor(); - - const allOverallStats = await getAllOverallStats().then((allOverallStats) => - allOverallStats.filter((stats) => !!stats) - ); - - expect(allOverallStats).toEqual([]); - }); - - it("should skip aggregation when no blocks have been indexed yet", async () => { - const workerProcessor = overallStatsUpdater.getWorkerProcessor(); - - vi.spyOn(prisma.block, "findLatest").mockResolvedValueOnce(null); - - await workerProcessor(); - - const allOverallStats = await getAllOverallStats().then((allOverallStats) => - allOverallStats.filter((stats) => !!stats) - ); - - expect(allOverallStats).toEqual([]); - }); - - it("should skip aggregation when the lowest slot hasn't been reached yet", async () => { - const workerProcessor = new OverallStatsUpdaterMock(undefined, { - lowestSlot: 1, - }).getWorkerProcessor(); - - await workerProcessor(); - - const allOverallStats = await getAllOverallStats().then((allOverallStats) => - allOverallStats.filter((stats) => !!stats) - ); - - expect(allOverallStats).toEqual([]); - }); - - it("should skip aggregation when there is no new finalized blocks", async () => { - const workerProcessor = overallStatsUpdater.getWorkerProcessor(); - - await workerProcessor(); - - const allOverallStats = await getAllOverallStats(); - - await workerProcessor(); - - const allOverallStatsAfter = await getAllOverallStats(); - - expect(allOverallStats).toEqual(allOverallStatsAfter); - }); -}); diff --git a/packages/stats-syncer/test/PeriodicUpdater.test.ts b/packages/stats-syncer/test/PeriodicUpdater.test.ts deleted file mode 100644 index 9354c905..00000000 --- a/packages/stats-syncer/test/PeriodicUpdater.test.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; - -import { testValidError } from "@blobscan/test"; - -import { PeriodicUpdater } from "../src/PeriodicUpdater"; -import type { PeriodicUpdaterConfig } from "../src/PeriodicUpdater"; -import { PeriodicUpdaterError } from "../src/errors"; - -class PeriodicUpdaterMock extends PeriodicUpdater { - constructor({ - name, - redisUriOrConnection, - updaterFn, - }: Partial = {}) { - super({ - name: name ?? "test-updater", - redisUriOrConnection: redisUriOrConnection ?? "redis://localhost:6379/1", - updaterFn: updaterFn ?? (() => Promise.resolve()), - }); - } - - getWorker() { - return this.worker; - } - - getQueue() { - return this.queue; - } -} - -describe("PeriodicUpdater", () => { - let periodicUpdater: PeriodicUpdaterMock; - - beforeEach(() => { - periodicUpdater = new PeriodicUpdaterMock(); - - return async () => { - await periodicUpdater.close(); - }; - }); - - it("should create an updater correctly", async () => { - const queue = periodicUpdater.getQueue(); - const worker = periodicUpdater.getWorker(); - const isPaused = await queue.isPaused(); - - expect(worker.isRunning(), "Expected worker to be running").toBeTruthy(); - expect(isPaused, "Expected queue to be running").toBeFalsy(); - }); - - describe("when running an updater", () => { - it("should set up a repeatable job correctly", async () => { - const queue = periodicUpdater.getQueue(); - const cronPattern = "* * * * *"; - - await periodicUpdater.start(cronPattern); - - const jobs = await queue.getRepeatableJobs(); - - expect(jobs.length, "Expected one repeatable job").toBe(1); - expect(jobs[0]?.pattern, "Repetable job cron pattern mismatch").toEqual( - cronPattern - ); - }); - - testValidError( - "should throw a valid error when failing to run", - async () => { - const queue = periodicUpdater.getQueue(); - - vi.spyOn(queue, "add").mockRejectedValueOnce(new Error("Queue error")); - - await periodicUpdater.start("* * * * *"); - }, - PeriodicUpdaterError, - { checkCause: true } - ); - }); - - describe("when closing an updater", () => { - it("should close correctly", async () => { - const queue = periodicUpdater.getQueue(); - const worker = periodicUpdater.getWorker(); - - const queueCloseSpy = vi.spyOn(queue, "close").mockResolvedValueOnce(); - const queueRemoveAllListenersSpy = vi - .spyOn(queue, "removeAllListeners") - .mockReturnValueOnce(queue); - - const workerCloseSpy = vi.spyOn(worker, "close").mockResolvedValueOnce(); - const workerRemoveAllListenersSpy = vi - .spyOn(worker, "removeAllListeners") - .mockReturnValueOnce(worker); - - await periodicUpdater.close(); - - expect(queueCloseSpy).toHaveBeenCalledOnce(); - expect(workerCloseSpy).toHaveBeenCalledOnce(); - - expect(queueRemoveAllListenersSpy).toHaveBeenCalledOnce(); - expect(workerRemoveAllListenersSpy).toHaveBeenCalledOnce(); - }); - }); - - testValidError( - "should throw a valid error when failing to close it", - async () => { - const queue = periodicUpdater.getQueue(); - const worker = periodicUpdater.getWorker(); - - vi.spyOn(queue, "close").mockRejectedValueOnce( - new Error("Queue closing error") - ); - vi.spyOn(worker, "close").mockRejectedValueOnce( - new Error("Worker closing error") - ); - - await periodicUpdater.close(); - }, - PeriodicUpdaterError, - { - checkCause: true, - } - ); -}); diff --git a/packages/stats-syncer/test/StatsSyncer.test.ts b/packages/stats-syncer/test/StatsSyncer.test.ts deleted file mode 100644 index 58619994..00000000 --- a/packages/stats-syncer/test/StatsSyncer.test.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; - -import { testValidError } from "@blobscan/test"; - -import { StatsSyncer } from "../src/StatsSyncer"; -import { StatsSyncerError } from "../src/errors"; - -class StatsSyncerMock extends StatsSyncer { - constructor(redisUri = "redis://localhost:6379/1") { - super({ redisUri }); - } - - getConnection() { - return this.connection; - } - - getDailyStatsUpdater() { - return this.dailyStatsUpdater; - } - - getOverallStatsUpdater() { - return this.overallStatsUpdater; - } -} - -describe("StatsSyncer", () => { - let statsSyncer: StatsSyncerMock; - - beforeEach(() => { - statsSyncer = new StatsSyncerMock(); - - return async () => { - await statsSyncer.close(); - }; - }); - - describe("when running the stats syncer", () => { - it("should start updaters correctly", async () => { - const dailyStatsUpdaterStartSpy = vi.spyOn( - statsSyncer.getDailyStatsUpdater(), - "start" - ); - const overallStatsUpdaterStartSpy = vi.spyOn( - statsSyncer.getOverallStatsUpdater(), - "start" - ); - - await statsSyncer.start({ - cronPatterns: { daily: "* * * * *", overall: "* * * * *" }, - }); - - expect(dailyStatsUpdaterStartSpy).toHaveBeenCalledWith("* * * * *"); - expect(overallStatsUpdaterStartSpy).toHaveBeenCalledWith("* * * * *"); - }); - - testValidError( - "should throw a valid error when failing to start it", - async () => { - const cronPatterns = { - daily: "* * * * *", - overall: "* * * * *", - }; - - vi.spyOn( - statsSyncer.getDailyStatsUpdater(), - "start" - ).mockRejectedValueOnce( - new Error( - "Something happened when trying to start daily stats updater" - ) - ); - - await statsSyncer.start({ - cronPatterns, - }); - }, - StatsSyncerError, - { - checkCause: true, - } - ); - }); - - describe("when closing the stats syncer", () => { - it("should close the updaters", async () => { - const closeStatsSyncer = new StatsSyncerMock(); - const dailyStatsUpdater = closeStatsSyncer.getDailyStatsUpdater(); - const overallStatsUpdater = closeStatsSyncer.getOverallStatsUpdater(); - const connection = closeStatsSyncer.getConnection(); - - const dailyStatsUpdaterCloseSpy = vi.spyOn(dailyStatsUpdater, "close"); - const overallStatsUpdaterCloseSpy = vi.spyOn( - overallStatsUpdater, - "close" - ); - const connectionCloseSpy = vi.spyOn(connection, "disconnect"); - - await closeStatsSyncer.close(); - - expect(dailyStatsUpdaterCloseSpy).toHaveBeenCalledOnce(); - expect(overallStatsUpdaterCloseSpy).toHaveBeenCalledOnce(); - expect(connectionCloseSpy).toHaveBeenCalledOnce(); - }); - - testValidError( - "should throw a valid error when failing to close it", - async () => { - const closeStatsSyncer = new StatsSyncerMock(); - const dailyStatsUpdater = closeStatsSyncer.getDailyStatsUpdater(); - vi.spyOn(dailyStatsUpdater, "close").mockRejectedValueOnce( - new Error("Some daily stats updater closing error") - ); - - await closeStatsSyncer.close(); - }, - StatsSyncerError, - { checkCause: true } - ); - }); -}); diff --git a/packages/swarm-syncer/package.json b/packages/swarm-syncer/package.json deleted file mode 100644 index ef11757e..00000000 --- a/packages/swarm-syncer/package.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "@blobscan/swarm-syncer", - "description": "Blobscan's swarm stamps data synchronizer", - "private": true, - "version": "0.0.1", - "main": "./src/index.ts", - "scripts": { - "clean": "rm -rf .turbo node_modules", - "lint": "eslint .", - "lint:fix": "pnpm lint --fix", - "type-check": "tsc --noEmit", - "test": "pnpm with-env:test vitest", - "test:ui": "pnpm with-env:test vitest --ui", - "with-env:test": ". ../../.env.test --" - }, - "dependencies": { - "@blobscan/dayjs": "workspace:^0.0.2", - "@blobscan/db": "workspace:^0.7.0", - "@blobscan/logger": "workspace:^0.1.0", - "axios": "^1.7.2", - "bullmq": "^4.13.2", - "ioredis": "^5.3.2" - }, - "eslintConfig": { - "root": true, - "extends": [ - "@blobscan/eslint-config/base" - ] - } -} diff --git a/packages/swarm-syncer/src/SwarmStampSyncer.ts b/packages/swarm-syncer/src/SwarmStampSyncer.ts deleted file mode 100644 index b02a204e..00000000 --- a/packages/swarm-syncer/src/SwarmStampSyncer.ts +++ /dev/null @@ -1,67 +0,0 @@ - -import type { Redis } from "ioredis"; - -import { ErrorException, SwarmStampSyncerError } from "./errors"; -import { logger } from "./logger"; -import { SwarmStampUpdater } from "./updaters/SwarmUpdater"; -import { createRedisConnection } from "./utils"; - -export type SwarmStampSyncerOptions = { - redisUri: string; - beeEndpoint: string; -}; - -export class SwarmStampSyncer { - protected connection: Redis; - protected swarmUpdater: SwarmStampUpdater; - - constructor({ redisUri, beeEndpoint }: SwarmStampSyncerOptions) { - const connection = createRedisConnection(redisUri); - - connection.on("error", (err) => { - logger.error(new ErrorException("The Redis connection failed", err)); - }); - - this.connection = connection; - this.swarmUpdater = new SwarmStampUpdater(connection, beeEndpoint); - } - - async start(cronPattern: string) { - try { - await this.swarmUpdater.start(cronPattern); - logger.info("Swarm stamp syncer started successfully."); - } catch (err) { - const err_ = new SwarmStampSyncerError( - "An error occurred when starting swarm stamps syncer", - err - ); - - logger.error(err_); - - throw err_; - } - } - - async close() { - try { - await this.swarmUpdater - .close() - .finally(() => { - this.connection.removeAllListeners(); - - if (this.connection.status === "ready") this.connection.disconnect(); - }); - - logger.info("Stats syncer closed successfully."); - } catch (err) { - const err_ = new SwarmStampSyncerError( - "An error ocurred when closing syncer", - err - ); - - logger.error(err_); - - throw err_; - } - } -} diff --git a/packages/swarm-syncer/src/env.ts b/packages/swarm-syncer/src/env.ts deleted file mode 100644 index 9e4e5a43..00000000 --- a/packages/swarm-syncer/src/env.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { - booleanSchema, - createEnv, - presetEnvOptions, -} from "@blobscan/zod"; - -export const env = createEnv({ - envOptions: { - server: { - SWARM_STORAGE_ENABLED: booleanSchema.default("false"), - }, - - ...presetEnvOptions, - }, -}); - -export type EnvVars = typeof env; diff --git a/packages/swarm-syncer/src/errors.ts b/packages/swarm-syncer/src/errors.ts deleted file mode 100644 index 0c67edd9..00000000 --- a/packages/swarm-syncer/src/errors.ts +++ /dev/null @@ -1,21 +0,0 @@ -export class ErrorException extends Error { - constructor(message: string, cause?: unknown) { - super(message, { - cause, - }); - - this.name = this.constructor.name; - } -} - -export class SwarmStampSyncerError extends ErrorException { - constructor(message: string) { - super(`Stats syncer failed: ${message}`); - } -} - -export class PeriodicUpdaterError extends ErrorException { - constructor(updaterName: string, message: string, cause: unknown) { - super(`Updater "${updaterName}" failed: ${message}`, cause); - } -} diff --git a/packages/swarm-syncer/src/index.ts b/packages/swarm-syncer/src/index.ts deleted file mode 100644 index ab099d4a..00000000 --- a/packages/swarm-syncer/src/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { SwarmStampSyncer } from "./SwarmStampSyncer"; -export type { PeriodicUpdater, PeriodicUpdaterConfig } from "./PeriodicUpdater"; diff --git a/packages/swarm-syncer/src/logger.ts b/packages/swarm-syncer/src/logger.ts deleted file mode 100644 index a7022ae8..00000000 --- a/packages/swarm-syncer/src/logger.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { createModuleLogger } from "@blobscan/logger"; - -export const logger = createModuleLogger("swarm-stamp-syncer"); diff --git a/packages/swarm-syncer/src/updaters/SwarmUpdater.ts b/packages/swarm-syncer/src/updaters/SwarmUpdater.ts deleted file mode 100644 index b0db7de7..00000000 --- a/packages/swarm-syncer/src/updaters/SwarmUpdater.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type { Redis } from "ioredis"; -import { prisma } from "@blobscan/db"; -import { env } from "../env"; -const axios = require('axios'); -import { PeriodicUpdater } from "../PeriodicUpdater"; -import { SwarmStampSyncerError } from "../errors"; - -export class SwarmStampUpdater extends PeriodicUpdater { - constructor( - redisUriOrConnection: string | Redis, - beeEndpoint: string, - ) { - const name = "swarm-stamp"; - super({ - name, - redisUriOrConnection, - updaterFn: async () => { - const batchId = env.SWARM_BATCH_ID; - const url = `${beeEndpoint}/stamps/${batchId}`; - const response = await axios.get(url); - const data = response.data; - - if (response.status != 200) { - throw new SwarmStampSyncerError(`Stamps endpoint returned status ${response.status} for batch id ${batchId}`); - } - - await prisma.blobStoragesState.update({ - data: { - swarmDataTTL: data.batchTTL, - }, - where: { - swarmDataId: batchId, - }, - }); - - this.logger.info(`Updated swarm stamp ${batchId}`); - }, - }); - } -} diff --git a/packages/swarm-syncer/src/utils.ts b/packages/swarm-syncer/src/utils.ts deleted file mode 100644 index 24f5cbbf..00000000 --- a/packages/swarm-syncer/src/utils.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Redis } from "ioredis"; - -import dayjs from "@blobscan/dayjs"; - -export function createRedisConnection(uri: string) { - return new Redis(uri, { - maxRetriesPerRequest: null, - }); -} - -export function formatDate(date: Date | string | dayjs.Dayjs) { - return dayjs(date).format("YYYY-MM-DD"); -} diff --git a/packages/swarm-syncer/test/SwarmStampSyncer.test.ts b/packages/swarm-syncer/test/SwarmStampSyncer.test.ts deleted file mode 100644 index 58619994..00000000 --- a/packages/swarm-syncer/test/SwarmStampSyncer.test.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; - -import { testValidError } from "@blobscan/test"; - -import { StatsSyncer } from "../src/StatsSyncer"; -import { StatsSyncerError } from "../src/errors"; - -class StatsSyncerMock extends StatsSyncer { - constructor(redisUri = "redis://localhost:6379/1") { - super({ redisUri }); - } - - getConnection() { - return this.connection; - } - - getDailyStatsUpdater() { - return this.dailyStatsUpdater; - } - - getOverallStatsUpdater() { - return this.overallStatsUpdater; - } -} - -describe("StatsSyncer", () => { - let statsSyncer: StatsSyncerMock; - - beforeEach(() => { - statsSyncer = new StatsSyncerMock(); - - return async () => { - await statsSyncer.close(); - }; - }); - - describe("when running the stats syncer", () => { - it("should start updaters correctly", async () => { - const dailyStatsUpdaterStartSpy = vi.spyOn( - statsSyncer.getDailyStatsUpdater(), - "start" - ); - const overallStatsUpdaterStartSpy = vi.spyOn( - statsSyncer.getOverallStatsUpdater(), - "start" - ); - - await statsSyncer.start({ - cronPatterns: { daily: "* * * * *", overall: "* * * * *" }, - }); - - expect(dailyStatsUpdaterStartSpy).toHaveBeenCalledWith("* * * * *"); - expect(overallStatsUpdaterStartSpy).toHaveBeenCalledWith("* * * * *"); - }); - - testValidError( - "should throw a valid error when failing to start it", - async () => { - const cronPatterns = { - daily: "* * * * *", - overall: "* * * * *", - }; - - vi.spyOn( - statsSyncer.getDailyStatsUpdater(), - "start" - ).mockRejectedValueOnce( - new Error( - "Something happened when trying to start daily stats updater" - ) - ); - - await statsSyncer.start({ - cronPatterns, - }); - }, - StatsSyncerError, - { - checkCause: true, - } - ); - }); - - describe("when closing the stats syncer", () => { - it("should close the updaters", async () => { - const closeStatsSyncer = new StatsSyncerMock(); - const dailyStatsUpdater = closeStatsSyncer.getDailyStatsUpdater(); - const overallStatsUpdater = closeStatsSyncer.getOverallStatsUpdater(); - const connection = closeStatsSyncer.getConnection(); - - const dailyStatsUpdaterCloseSpy = vi.spyOn(dailyStatsUpdater, "close"); - const overallStatsUpdaterCloseSpy = vi.spyOn( - overallStatsUpdater, - "close" - ); - const connectionCloseSpy = vi.spyOn(connection, "disconnect"); - - await closeStatsSyncer.close(); - - expect(dailyStatsUpdaterCloseSpy).toHaveBeenCalledOnce(); - expect(overallStatsUpdaterCloseSpy).toHaveBeenCalledOnce(); - expect(connectionCloseSpy).toHaveBeenCalledOnce(); - }); - - testValidError( - "should throw a valid error when failing to close it", - async () => { - const closeStatsSyncer = new StatsSyncerMock(); - const dailyStatsUpdater = closeStatsSyncer.getDailyStatsUpdater(); - vi.spyOn(dailyStatsUpdater, "close").mockRejectedValueOnce( - new Error("Some daily stats updater closing error") - ); - - await closeStatsSyncer.close(); - }, - StatsSyncerError, - { checkCause: true } - ); - }); -}); diff --git a/packages/swarm-syncer/test/__snapshots__/StatsSyncer.test.ts.snap b/packages/swarm-syncer/test/__snapshots__/StatsSyncer.test.ts.snap deleted file mode 100644 index 1ab5d442..00000000 --- a/packages/swarm-syncer/test/__snapshots__/StatsSyncer.test.ts.snap +++ /dev/null @@ -1,9 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`StatsSyncer > when closing the stats syncer > should throw a valid error when failing to close it 1`] = `"Stats syncer failed: An error ocurred when closing syncer"`; - -exports[`StatsSyncer > when closing the stats syncer > should throw a valid error when failing to close it 2`] = `[Error: Some daily stats updater closing error]`; - -exports[`StatsSyncer > when running the stats syncer > should throw a valid error when failing to start it 1`] = `"Stats syncer failed: An error occurred when starting syncer"`; - -exports[`StatsSyncer > when running the stats syncer > should throw a valid error when failing to start it 2`] = `[Error: Something happened when trying to start daily stats updater]`; diff --git a/packages/swarm-syncer/tsconfig.json b/packages/swarm-syncer/tsconfig.json deleted file mode 100644 index aff0b2dc..00000000 --- a/packages/swarm-syncer/tsconfig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "@blobscan/tsconfig/base.production.json", - "include": ["src/**/*.ts", "test/**/*.ts", "vitest.config.ts"] -} diff --git a/packages/swarm-syncer/vitest.config.ts b/packages/swarm-syncer/vitest.config.ts deleted file mode 100644 index 8fdeaf1f..00000000 --- a/packages/swarm-syncer/vitest.config.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { defineProject } from "vitest/config"; - -import { sharedProjectConfig } from "../../vitest.shared"; - -export default defineProject(sharedProjectConfig); diff --git a/packages/stats-syncer/CHANGELOG.md b/packages/syncers/CHANGELOG.md similarity index 99% rename from packages/stats-syncer/CHANGELOG.md rename to packages/syncers/CHANGELOG.md index 35e2a92b..f1d175f2 100644 --- a/packages/stats-syncer/CHANGELOG.md +++ b/packages/syncers/CHANGELOG.md @@ -1,4 +1,4 @@ -# @blobscan/stats-syncer +# @blobscan/syncers ## 0.1.9 diff --git a/packages/stats-syncer/package.json b/packages/syncers/package.json similarity index 95% rename from packages/stats-syncer/package.json rename to packages/syncers/package.json index 7bd1d58d..01eafe66 100644 --- a/packages/stats-syncer/package.json +++ b/packages/syncers/package.json @@ -1,5 +1,5 @@ { - "name": "@blobscan/stats-syncer", + "name": "@blobscan/syncers", "description": "Blobscan's stats syncer", "private": true, "version": "0.1.9", diff --git a/packages/swarm-syncer/src/PeriodicUpdater.ts b/packages/syncers/src/BaseSyncer.ts similarity index 55% rename from packages/swarm-syncer/src/PeriodicUpdater.ts rename to packages/syncers/src/BaseSyncer.ts index 03dd4bab..48949a46 100644 --- a/packages/swarm-syncer/src/PeriodicUpdater.ts +++ b/packages/syncers/src/BaseSyncer.ts @@ -5,42 +5,44 @@ import type { Redis } from "ioredis"; import { createModuleLogger } from "@blobscan/logger"; import type { Logger } from "@blobscan/logger"; -import { ErrorException, PeriodicUpdaterError } from "./errors"; +import { ErrorException, SyncerError } from "./errors"; import { createRedisConnection } from "./utils"; -// TODO: Refactor, this file is duplicate (packages: stats-syncer and swarm-syncer) -export type PeriodicUpdaterConfig = { +export interface CommonSyncerConfig { + redisUriOrConnection: Redis | string; + cronPattern: string; +} + +export interface BaseSyncerConfig extends CommonSyncerConfig { name: string; - redisUriOrConnection: string | Redis; - updaterFn: () => Promise; -}; + syncerFn: () => Promise; +} -export class PeriodicUpdater { +export class BaseSyncer { name: string; - protected worker: Worker; - protected queue: Queue; - protected updaterFn: () => Promise; + cronPattern: string; + + protected syncerFn: () => Promise; protected logger: Logger; + protected connection: Redis; + protected worker: Worker | undefined; + protected queue: Queue | undefined; + constructor({ name, + cronPattern, redisUriOrConnection, - updaterFn, - }: PeriodicUpdaterConfig) { - const isRedisUri = typeof redisUriOrConnection === "string"; - this.name = name; - this.logger = createModuleLogger("stats-syncer", this.name); + syncerFn, + }: BaseSyncerConfig) { + this.name = `${name}-syncer`; + this.cronPattern = cronPattern; + this.logger = createModuleLogger(this.name); let connection: Redis; - if (isRedisUri) { + if (typeof redisUriOrConnection === "string") { connection = createRedisConnection(redisUriOrConnection); - - connection.on("error", (err) => { - this.logger.error( - new ErrorException("A Redis connection error ocurred", err) - ); - }); } else { connection = redisUriOrConnection; } @@ -49,12 +51,10 @@ export class PeriodicUpdater { connection, }); - this.worker = new Worker(this.queue.name, updaterFn, { + this.worker = new Worker(this.queue.name, syncerFn, { connection, }); - this.updaterFn = updaterFn; - this.queue.on("error", (err) => { this.logger.error(new ErrorException("A queue error occurred", err)); }); @@ -62,22 +62,27 @@ export class PeriodicUpdater { this.worker.on("failed", (_, err) => { this.logger.error(new ErrorException("A worker error ocurred", err)); }); + + this.connection = connection; + this.syncerFn = syncerFn; } - async start(cronPattern: string) { + async start() { try { const jobName = `${this.name}-job`; - const repeatableJob = await this.queue.add(jobName, null, { + const repeatableJob = await this.queue?.add(jobName, null, { repeat: { - pattern: cronPattern, + pattern: this.cronPattern, }, }); + this.logger.info("Syncer started successfully"); + return repeatableJob; } catch (err) { - throw new PeriodicUpdaterError( + throw new SyncerError( this.name, - "An error ocurred when starting updater", + "An error ocurred when starting syncer", err ); } @@ -89,26 +94,28 @@ export class PeriodicUpdater { return teardownPromise .finally(async () => { await this.#performClosingOperation(() => - this.worker.removeAllListeners().close(true) + this.worker?.removeAllListeners().close(true) ); }) .finally(async () => { await this.#performClosingOperation(() => - this.queue.obliterate({ force: true }) + this.queue?.obliterate({ force: true }) ); }) .finally(async () => { await this.#performClosingOperation(() => - this.queue.removeAllListeners().close() + this.queue?.removeAllListeners().close() ); + + this.logger.info("Syncer closed successfully"); }); } - async #performClosingOperation(operation: () => Promise) { + async #performClosingOperation(operation: () => Promise | undefined) { try { await operation(); } catch (err) { - const err_ = new PeriodicUpdaterError( + const err_ = new SyncerError( this.name, "An error ocurred when performing closing operation", err diff --git a/packages/syncers/src/errors.ts b/packages/syncers/src/errors.ts new file mode 100644 index 00000000..6afc5bf7 --- /dev/null +++ b/packages/syncers/src/errors.ts @@ -0,0 +1,15 @@ +export class ErrorException extends Error { + constructor(message: string, cause?: unknown) { + super(message, { + cause, + }); + + this.name = this.constructor.name; + } +} + +export class SyncerError extends ErrorException { + constructor(syncerName: string, message: string, cause: unknown) { + super(`Syncer "${syncerName}" failed: ${message}`, cause); + } +} diff --git a/packages/syncers/src/index.ts b/packages/syncers/src/index.ts new file mode 100644 index 00000000..1cf74888 --- /dev/null +++ b/packages/syncers/src/index.ts @@ -0,0 +1,2 @@ +export * from "./syncers"; +export { createRedisConnection } from "./utils"; diff --git a/packages/stats-syncer/src/updaters/DailyStatsUpdater.ts b/packages/syncers/src/syncers/DailyStatsSyncer.ts similarity index 88% rename from packages/stats-syncer/src/updaters/DailyStatsUpdater.ts rename to packages/syncers/src/syncers/DailyStatsSyncer.ts index ace23fba..93c4574a 100644 --- a/packages/stats-syncer/src/updaters/DailyStatsUpdater.ts +++ b/packages/syncers/src/syncers/DailyStatsSyncer.ts @@ -1,9 +1,7 @@ -import type { Redis } from "ioredis"; - import { normalizeDailyDate, normalizeDate, prisma } from "@blobscan/db"; import type { PrismaPromise, RawDatePeriod } from "@blobscan/db"; -import { PeriodicUpdater } from "../PeriodicUpdater"; +import { CommonSyncerConfig, BaseSyncer } from "../BaseSyncer"; import { formatDate } from "../utils"; interface DailyStatsModel { @@ -20,13 +18,17 @@ const dailyStatsModels: Record = { transaction: prisma.transactionDailyStats, }; -export class DailyStatsUpdater extends PeriodicUpdater { - constructor(redisUriOrConnection: string | Redis) { - const name = "daily"; +export interface DailyStatsSyncerConfig extends CommonSyncerConfig {} + +export class DailyStatsSyncer extends BaseSyncer { + constructor({ redisUriOrConnection, cronPattern }: DailyStatsSyncerConfig) { + const name = "daily-stats"; + super({ name, redisUriOrConnection, - updaterFn: async () => { + cronPattern, + syncerFn: async () => { const findLatestArgs: { select: { day: boolean; diff --git a/packages/stats-syncer/src/updaters/OverallStatsUpdater.ts b/packages/syncers/src/syncers/OverallStatsSyncer.ts similarity index 87% rename from packages/stats-syncer/src/updaters/OverallStatsUpdater.ts rename to packages/syncers/src/syncers/OverallStatsSyncer.ts index ae3935c3..2d7ee48c 100644 --- a/packages/stats-syncer/src/updaters/OverallStatsUpdater.ts +++ b/packages/syncers/src/syncers/OverallStatsSyncer.ts @@ -1,14 +1,12 @@ -import type { Redis } from "ioredis"; - import { prisma } from "@blobscan/db"; import type { BlockNumberRange, Prisma } from "@blobscan/db"; -import { PeriodicUpdater } from "../PeriodicUpdater"; +import { CommonSyncerConfig, BaseSyncer } from "../BaseSyncer"; -export type OverallStatsUpdaterOptions = { +export interface OverallStatsSyncerConfig extends CommonSyncerConfig { batchSize?: number; lowestSlot?: number; -}; +} const DEFAULT_BATCH_SIZE = 2_000_000; const DEFAULT_INITIAL_SLOT = 0; @@ -17,21 +15,19 @@ function isUnset(value: T | undefined | null): value is null | undefined { return value === undefined || value === null; } -export class OverallStatsUpdater extends PeriodicUpdater { - constructor( - redisUriOrConnection: string | Redis, - options: OverallStatsUpdaterOptions = {} - ) { - const name = "overall"; +export class OverallStatsSyncer extends BaseSyncer { + constructor({ + cronPattern, + redisUriOrConnection, + batchSize = DEFAULT_BATCH_SIZE, + lowestSlot = DEFAULT_INITIAL_SLOT, + }: OverallStatsSyncerConfig) { + const name = "overall-stats"; super({ name, + cronPattern, redisUriOrConnection, - updaterFn: async () => { - const { - batchSize = DEFAULT_BATCH_SIZE, - lowestSlot = DEFAULT_INITIAL_SLOT, - } = options ?? {}; - + syncerFn: async () => { const [blockchainSyncState, latestBlock] = await Promise.all([ prisma.blockchainSyncState.findUnique({ select: { diff --git a/packages/syncers/src/syncers/index.ts b/packages/syncers/src/syncers/index.ts new file mode 100644 index 00000000..4fbba13a --- /dev/null +++ b/packages/syncers/src/syncers/index.ts @@ -0,0 +1,4 @@ +export { DailyStatsSyncer } from "./DailyStatsSyncer"; +export type { DailyStatsSyncerConfig } from "./DailyStatsSyncer"; +export { OverallStatsSyncer } from "./OverallStatsSyncer"; +export type { OverallStatsSyncerConfig } from "./OverallStatsSyncer"; diff --git a/packages/stats-syncer/src/utils.ts b/packages/syncers/src/utils.ts similarity index 100% rename from packages/stats-syncer/src/utils.ts rename to packages/syncers/src/utils.ts diff --git a/packages/swarm-syncer/test/PeriodicUpdater.test.ts b/packages/syncers/test/BaseSyncer.test.ts similarity index 72% rename from packages/swarm-syncer/test/PeriodicUpdater.test.ts rename to packages/syncers/test/BaseSyncer.test.ts index 9354c905..46c7b1c0 100644 --- a/packages/swarm-syncer/test/PeriodicUpdater.test.ts +++ b/packages/syncers/test/BaseSyncer.test.ts @@ -1,29 +1,32 @@ +import { Redis } from "ioredis"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { testValidError } from "@blobscan/test"; -import { PeriodicUpdater } from "../src/PeriodicUpdater"; -import type { PeriodicUpdaterConfig } from "../src/PeriodicUpdater"; -import { PeriodicUpdaterError } from "../src/errors"; +import { BaseSyncer } from "../src/BaseSyncer"; +import type { BaseSyncerConfig } from "../src/BaseSyncer"; +import { SyncerError } from "../src/errors"; +import { createRedisConnection } from "../src/utils"; -class PeriodicUpdaterMock extends PeriodicUpdater { - constructor({ - name, - redisUriOrConnection, - updaterFn, - }: Partial = {}) { +class PeriodicUpdaterMock extends BaseSyncer { + constructor({ name, syncerFn: updaterFn }: Partial = {}) { super({ name: name ?? "test-updater", - redisUriOrConnection: redisUriOrConnection ?? "redis://localhost:6379/1", - updaterFn: updaterFn ?? (() => Promise.resolve()), + redisUriOrConnection: "redis://localhost:6379/1", + cronPattern: "* * * * *", + syncerFn: updaterFn ?? (() => Promise.resolve()), }); } getWorker() { + if (!this.worker) throw new Error("Worker not initialized"); + return this.worker; } getQueue() { + if (!this.queue) throw new Error("Queue not initialized"); + return this.queue; } } @@ -51,15 +54,14 @@ describe("PeriodicUpdater", () => { describe("when running an updater", () => { it("should set up a repeatable job correctly", async () => { const queue = periodicUpdater.getQueue(); - const cronPattern = "* * * * *"; - await periodicUpdater.start(cronPattern); + await periodicUpdater.start(); const jobs = await queue.getRepeatableJobs(); expect(jobs.length, "Expected one repeatable job").toBe(1); expect(jobs[0]?.pattern, "Repetable job cron pattern mismatch").toEqual( - cronPattern + "* * * * *" ); }); @@ -70,17 +72,21 @@ describe("PeriodicUpdater", () => { vi.spyOn(queue, "add").mockRejectedValueOnce(new Error("Queue error")); - await periodicUpdater.start("* * * * *"); + await periodicUpdater.start(); }, - PeriodicUpdaterError, + SyncerError, { checkCause: true } ); }); describe("when closing an updater", () => { it("should close correctly", async () => { - const queue = periodicUpdater.getQueue(); - const worker = periodicUpdater.getWorker(); + const closingPeriodicUpdater = new PeriodicUpdaterMock(); + + await closingPeriodicUpdater.start(); + + const queue = closingPeriodicUpdater.getQueue(); + const worker = closingPeriodicUpdater.getWorker(); const queueCloseSpy = vi.spyOn(queue, "close").mockResolvedValueOnce(); const queueRemoveAllListenersSpy = vi @@ -92,7 +98,7 @@ describe("PeriodicUpdater", () => { .spyOn(worker, "removeAllListeners") .mockReturnValueOnce(worker); - await periodicUpdater.close(); + await closingPeriodicUpdater.close(); expect(queueCloseSpy).toHaveBeenCalledOnce(); expect(workerCloseSpy).toHaveBeenCalledOnce(); @@ -117,7 +123,7 @@ describe("PeriodicUpdater", () => { await periodicUpdater.close(); }, - PeriodicUpdaterError, + SyncerError, { checkCause: true, } diff --git a/packages/stats-syncer/test/DailyStatsUpdater.test.fixtures.ts b/packages/syncers/test/DailyStatsSyncer.test.fixtures.ts similarity index 100% rename from packages/stats-syncer/test/DailyStatsUpdater.test.fixtures.ts rename to packages/syncers/test/DailyStatsSyncer.test.fixtures.ts diff --git a/packages/stats-syncer/test/DailyStatsUpdater.test.ts b/packages/syncers/test/DailyStatsSyncer.test.ts similarity index 78% rename from packages/stats-syncer/test/DailyStatsUpdater.test.ts rename to packages/syncers/test/DailyStatsSyncer.test.ts index de7c2dae..7557a39c 100644 --- a/packages/stats-syncer/test/DailyStatsUpdater.test.ts +++ b/packages/syncers/test/DailyStatsSyncer.test.ts @@ -3,18 +3,18 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { prisma } from "@blobscan/db"; import { fixtures } from "@blobscan/test"; -import { DailyStatsUpdater } from "../src/updaters/DailyStatsUpdater"; +import { DailyStatsSyncer } from "../src/syncers/"; import { formatDate } from "../src/utils"; -import { CURRENT_DAY_DATA } from "./DailyStatsUpdater.test.fixtures"; +import { CURRENT_DAY_DATA } from "./DailyStatsSyncer.test.fixtures"; import { expectDailyStatsDatesToBeEqual, getAllDailyStatsDates, indexNewBlock, -} from "./DailyStatsUpdater.test.utils"; +} from "./DailyStatsSyncer.test.utils"; -class DailyStatsUpdaterMock extends DailyStatsUpdater { +class DailyStatsSyncerMock extends DailyStatsSyncer { constructor(redisUri = process.env.REDIS_URI ?? "") { - super(redisUri); + super({ redisUriOrConnection: redisUri, cronPattern: "* * * * *" }); } getWorker() { @@ -22,7 +22,7 @@ class DailyStatsUpdaterMock extends DailyStatsUpdater { } getWorkerProcessor() { - return this.updaterFn; + return this.syncerFn; } getQueue() { @@ -30,23 +30,23 @@ class DailyStatsUpdaterMock extends DailyStatsUpdater { } } -describe("DailyStatsUpdater", () => { +describe("DailyStatsSyncer", () => { const allExpectedDates = Array.from( new Set(fixtures.blocks.map((block) => formatDate(block.timestamp))) ).sort((a, b) => (a < b ? -1 : 1)); - let dailyStatsUpdater: DailyStatsUpdaterMock; + let dailyStatsSyncer: DailyStatsSyncerMock; beforeEach(() => { - dailyStatsUpdater = new DailyStatsUpdaterMock(); + dailyStatsSyncer = new DailyStatsSyncerMock(); return async () => { - await dailyStatsUpdater.close(); + await dailyStatsSyncer.close(); }; }); it("should aggregate data for all available days", async () => { - const workerProcessor = dailyStatsUpdater.getWorkerProcessor(); + const workerProcessor = dailyStatsSyncer.getWorkerProcessor(); await workerProcessor(); @@ -61,7 +61,7 @@ describe("DailyStatsUpdater", () => { }); it("should skip aggregation if not all blocks have been indexed for the last day", async () => { - const workerProcessor = dailyStatsUpdater.getWorkerProcessor(); + const workerProcessor = dailyStatsSyncer.getWorkerProcessor(); await indexNewBlock(CURRENT_DAY_DATA); @@ -75,7 +75,7 @@ describe("DailyStatsUpdater", () => { }); it("should skip aggregation if no blocks have been indexed yet", async () => { - const workerProcessor = dailyStatsUpdater.getWorkerProcessor(); + const workerProcessor = dailyStatsSyncer.getWorkerProcessor(); const findLatestSpy = vi .spyOn(prisma.block, "findLatest") @@ -92,7 +92,7 @@ describe("DailyStatsUpdater", () => { it("should skip aggregation if already up to date", async () => { await indexNewBlock(CURRENT_DAY_DATA); - const workerProcessor = dailyStatsUpdater.getWorkerProcessor(); + const workerProcessor = dailyStatsSyncer.getWorkerProcessor(); await workerProcessor(); diff --git a/packages/stats-syncer/test/DailyStatsUpdater.test.utils.ts b/packages/syncers/test/DailyStatsSyncer.test.utils.ts similarity index 100% rename from packages/stats-syncer/test/DailyStatsUpdater.test.utils.ts rename to packages/syncers/test/DailyStatsSyncer.test.utils.ts diff --git a/packages/swarm-syncer/test/OverallStatsUpdater.test.ts b/packages/syncers/test/OverallStatsSyncer.test.ts similarity index 92% rename from packages/swarm-syncer/test/OverallStatsUpdater.test.ts rename to packages/syncers/test/OverallStatsSyncer.test.ts index bed3404d..ab0ec58a 100644 --- a/packages/swarm-syncer/test/OverallStatsUpdater.test.ts +++ b/packages/syncers/test/OverallStatsSyncer.test.ts @@ -3,16 +3,18 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { prisma } from "@blobscan/db"; import { fixtures, omitDBTimestampFields } from "@blobscan/test"; -import { OverallStatsUpdater } from "../src/updaters/OverallStatsUpdater"; +import { + OverallStatsSyncer, + OverallStatsSyncerConfig, +} from "../src/syncers/OverallStatsSyncer"; -class OverallStatsUpdaterMock extends OverallStatsUpdater { - constructor( - redisUri = process.env.REDIS_URI ?? "", - config: ConstructorParameters[1] = {} - ) { +class OverallStatsUpdaterMock extends OverallStatsSyncer { + constructor(config: Partial = {}) { const lowestSlot = config.lowestSlot ?? fixtures.blockchainSyncState[0]?.lastLowerSyncedSlot; - super(redisUri, { + super({ + cronPattern: "* * * * *", + redisUriOrConnection: "redis://localhost:6379/1", ...config, lowestSlot, }); @@ -23,7 +25,7 @@ class OverallStatsUpdaterMock extends OverallStatsUpdater { } getWorkerProcessor() { - return this.updaterFn; + return this.syncerFn; } getQueue() { @@ -114,7 +116,7 @@ describe("OverallStatsUpdater", () => { it("should aggregate overall stats in batches correctly when there are too many blocks", async () => { const batchSize = 2; - const workerProcessor = new OverallStatsUpdaterMock(undefined, { + const workerProcessor = new OverallStatsUpdaterMock({ batchSize, }).getWorkerProcessor(); const incrementTransactionSpy = vi.spyOn(prisma, "$transaction"); @@ -193,7 +195,7 @@ describe("OverallStatsUpdater", () => { }); it("should skip aggregation when the lowest slot hasn't been reached yet", async () => { - const workerProcessor = new OverallStatsUpdaterMock(undefined, { + const workerProcessor = new OverallStatsUpdaterMock({ lowestSlot: 1, }).getWorkerProcessor(); diff --git a/packages/swarm-syncer/test/__snapshots__/PeriodicUpdater.test.ts.snap b/packages/syncers/test/__snapshots__/BaseSyncer.test.ts.snap similarity index 64% rename from packages/swarm-syncer/test/__snapshots__/PeriodicUpdater.test.ts.snap rename to packages/syncers/test/__snapshots__/BaseSyncer.test.ts.snap index ad2709e3..dba557d8 100644 --- a/packages/swarm-syncer/test/__snapshots__/PeriodicUpdater.test.ts.snap +++ b/packages/syncers/test/__snapshots__/BaseSyncer.test.ts.snap @@ -1,9 +1,9 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`PeriodicUpdater > should throw a valid error when failing to close it 1`] = `"Updater \\"test-updater\\" failed: An error ocurred when performing closing operation"`; +exports[`PeriodicUpdater > should throw a valid error when failing to close it 1`] = `"Syncer \\"test-updater-syncer\\" failed: An error ocurred when performing closing operation"`; exports[`PeriodicUpdater > should throw a valid error when failing to close it 2`] = `[Error: Queue closing error]`; -exports[`PeriodicUpdater > when running an updater > should throw a valid error when failing to run 1`] = `"Updater \\"test-updater\\" failed: An error ocurred when starting updater"`; +exports[`PeriodicUpdater > when running an updater > should throw a valid error when failing to run 1`] = `"Syncer \\"test-updater-syncer\\" failed: An error ocurred when starting syncer"`; exports[`PeriodicUpdater > when running an updater > should throw a valid error when failing to run 2`] = `[Error: Queue error]`; diff --git a/packages/stats-syncer/test/__snapshots__/PeriodicUpdater.test.ts.snap b/packages/syncers/test/__snapshots__/PeriodicUpdater.test.ts.snap similarity index 73% rename from packages/stats-syncer/test/__snapshots__/PeriodicUpdater.test.ts.snap rename to packages/syncers/test/__snapshots__/PeriodicUpdater.test.ts.snap index ad2709e3..5c860cbe 100644 --- a/packages/stats-syncer/test/__snapshots__/PeriodicUpdater.test.ts.snap +++ b/packages/syncers/test/__snapshots__/PeriodicUpdater.test.ts.snap @@ -1,9 +1,9 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`PeriodicUpdater > should throw a valid error when failing to close it 1`] = `"Updater \\"test-updater\\" failed: An error ocurred when performing closing operation"`; +exports[`PeriodicUpdater > should throw a valid error when failing to close it 1`] = `"Updater \\"test-updater-updater\\" failed: An error ocurred when performing closing operation"`; exports[`PeriodicUpdater > should throw a valid error when failing to close it 2`] = `[Error: Queue closing error]`; -exports[`PeriodicUpdater > when running an updater > should throw a valid error when failing to run 1`] = `"Updater \\"test-updater\\" failed: An error ocurred when starting updater"`; +exports[`PeriodicUpdater > when running an updater > should throw a valid error when failing to run 1`] = `"Updater \\"test-updater-updater\\" failed: An error ocurred when starting updater"`; exports[`PeriodicUpdater > when running an updater > should throw a valid error when failing to run 2`] = `[Error: Queue error]`; diff --git a/packages/stats-syncer/test/__snapshots__/StatsSyncer.test.ts.snap b/packages/syncers/test/__snapshots__/StatsSyncer.test.ts.snap similarity index 100% rename from packages/stats-syncer/test/__snapshots__/StatsSyncer.test.ts.snap rename to packages/syncers/test/__snapshots__/StatsSyncer.test.ts.snap diff --git a/packages/syncers/test/__snapshots__/SyncerManager.test.ts.snap b/packages/syncers/test/__snapshots__/SyncerManager.test.ts.snap new file mode 100644 index 00000000..606d6a8b --- /dev/null +++ b/packages/syncers/test/__snapshots__/SyncerManager.test.ts.snap @@ -0,0 +1,9 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`SyncerManager > when closing the syncer manager > should throw a valid error when failing to close it 1`] = `"Periodic updater manager failed: An error ocurred when closing syncers"`; + +exports[`SyncerManager > when closing the syncer manager > should throw a valid error when failing to close it 2`] = `[Error: Some daily stats updater closing error]`; + +exports[`SyncerManager > when running the syncer manager > should throw a valid error when failing to start it 1`] = `"Periodic updater manager failed: An error occurred when starting syncers"`; + +exports[`SyncerManager > when running the syncer manager > should throw a valid error when failing to start it 2`] = `[Error: Something happened when trying to start daily stats updater]`; diff --git a/packages/stats-syncer/tsconfig.json b/packages/syncers/tsconfig.json similarity index 100% rename from packages/stats-syncer/tsconfig.json rename to packages/syncers/tsconfig.json diff --git a/packages/stats-syncer/vitest.config.ts b/packages/syncers/vitest.config.ts similarity index 100% rename from packages/stats-syncer/vitest.config.ts rename to packages/syncers/vitest.config.ts diff --git a/packages/test/src/fixtures/postgres/data.json b/packages/test/src/fixtures/postgres/data.json index 2158e14f..f62b1561 100644 --- a/packages/test/src/fixtures/postgres/data.json +++ b/packages/test/src/fixtures/postgres/data.json @@ -13,7 +13,8 @@ { "id": 1, "swarmDataId": "batch-1", - "swarmDataTTL": 1000 + "swarmDataTTL": 1000, + "updatedAt": "2023-10-31T12:10:00Z" } ], "blocks": [ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7bd0ff60..68830c84 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -171,12 +171,9 @@ importers: '@blobscan/open-telemetry': specifier: workspace:^0.0.7 version: link:../../packages/open-telemetry - '@blobscan/stats-syncer': - specifier: workspace:^0.1.8 - version: link:../../packages/stats-syncer - '@blobscan/swarm-syncer': - specifier: workspace:^0.0.1 - version: link:../../packages/swarm-syncer + '@blobscan/syncers': + specifier: workspace:^0.1.9 + version: link:../../packages/syncers '@blobscan/zod': specifier: workspace:^0.1.0 version: link:../../packages/zod @@ -590,25 +587,7 @@ importers: specifier: ^14.2.0 version: 14.2.0 - packages/stats-syncer: - dependencies: - '@blobscan/dayjs': - specifier: workspace:^0.0.2 - version: link:../dayjs - '@blobscan/db': - specifier: workspace:^0.7.0 - version: link:../db - '@blobscan/logger': - specifier: workspace:^0.1.0 - version: link:../logger - bullmq: - specifier: ^4.13.2 - version: 4.17.0 - ioredis: - specifier: ^5.3.2 - version: 5.4.1 - - packages/swarm-syncer: + packages/syncers: dependencies: '@blobscan/dayjs': specifier: workspace:^0.0.2 @@ -619,9 +598,6 @@ importers: '@blobscan/logger': specifier: workspace:^0.1.0 version: link:../logger - axios: - specifier: ^1.7.2 - version: 1.7.2 bullmq: specifier: ^4.13.2 version: 4.17.0 @@ -3206,9 +3182,6 @@ packages: axios@1.6.8: resolution: {integrity: sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==} - axios@1.7.2: - resolution: {integrity: sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==} - axobject-query@3.2.1: resolution: {integrity: sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==} @@ -9808,14 +9781,6 @@ snapshots: transitivePeerDependencies: - debug - axios@1.7.2: - dependencies: - follow-redirects: 1.15.6 - form-data: 4.0.0 - proxy-from-env: 1.1.0 - transitivePeerDependencies: - - debug - axobject-query@3.2.1: dependencies: dequal: 2.0.3 @@ -10649,7 +10614,7 @@ snapshots: '@typescript-eslint/parser': 5.62.0(eslint@8.57.0)(typescript@5.4.5) eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0) + eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0) eslint-plugin-import: 2.29.1(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) eslint-plugin-jsx-a11y: 6.8.0(eslint@8.57.0) eslint-plugin-react: 7.32.2(eslint@8.57.0) @@ -10677,12 +10642,12 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0): + eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0): dependencies: debug: 4.3.4 enhanced-resolve: 5.15.0 eslint: 8.57.0 - eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) + eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0) eslint-plugin-import: 2.29.1(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) fast-glob: 3.3.2 get-tsconfig: 4.7.2 @@ -10694,14 +10659,14 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.8.0(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0): + eslint-module-utils@2.8.0(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 5.62.0(eslint@8.57.0)(typescript@5.4.5) eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0) + eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0) transitivePeerDependencies: - supports-color @@ -10715,7 +10680,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) + eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0) hasown: 2.0.2 is-core-module: 2.13.1 is-glob: 4.0.3 @@ -10742,7 +10707,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) + eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0) hasown: 2.0.2 is-core-module: 2.13.1 is-glob: 4.0.3