diff --git a/.changeset/cold-terms-accept.md b/.changeset/cold-terms-accept.md new file mode 100644 index 00000000..665d9c17 --- /dev/null +++ b/.changeset/cold-terms-accept.md @@ -0,0 +1,5 @@ +--- +"@blobscan/rest-api-server": minor +--- + +Added Swarm stamp syncer diff --git a/.changeset/fair-rabbits-cross.md b/.changeset/fair-rabbits-cross.md new file mode 100644 index 00000000..e887917d --- /dev/null +++ b/.changeset/fair-rabbits-cross.md @@ -0,0 +1,5 @@ +--- +"@blobscan/syncers": minor +--- + +Added swarm stamp syncer 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/.changeset/moody-fireants-itch.md b/.changeset/moody-fireants-itch.md new file mode 100644 index 00000000..f2d56790 --- /dev/null +++ b/.changeset/moody-fireants-itch.md @@ -0,0 +1,5 @@ +--- +"@blobscan/db": minor +--- + +Added an updated at field to blob storages state model diff --git a/.env.test b/.env.test index b96cb617..3bc0db5e 100644 --- a/.env.test +++ b/.env.test @@ -20,7 +20,7 @@ GOOGLE_STORAGE_ENABLED=true # GOOGLE_SERVICE_KEY= BEE_ENDPOINT=http://localhost:1633 - +SWARM_BATCH_ID=f89e63edf757f06e89933761d6d46592d03026efb9871f9d244f34da86b6c242 FILE_SYSTEM_STORAGE_PATH=test-blobscan-blobs diff --git a/.nvmrc b/.nvmrc index 1e24c021..87834047 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -18.15.0 \ No newline at end of file +20.12.2 diff --git a/apps/docs/src/app/docs/environment/page.md b/apps/docs/src/app/docs/environment/page.md index 8e783981..249f4f19 100644 --- a/apps/docs/src/app/docs/environment/page.md +++ b/apps/docs/src/app/docs/environment/page.md @@ -65,10 +65,13 @@ At the moment Postgres is the default storage and Blobscan won't be able to run **Ethereum Swarm** -| Variable | Description | Required | Default value | -| ----------------------- | -------------------- | -------- | ------------- | -| `SWARM_STORAGE_ENABLED` | Store blobs in Swarm | No | `false` | -| `BEE_ENDPOINT` | Bee endpoint | No | (empty) | +| Variable | Description | Required | Default value | +| -------------------------- | -------------------------- | ------------------------------- | -------------- | +| `SWARM_STORAGE_ENABLED` | Store blobs in Swarm | No | `false` | +| `SWARM_BATCH_ID` | Swarm address of the stamp | If `SWARM_STORAGE_ENABLED=true` | (empty) | +| `SWARM_STAMP_CRON_PATTERN` | Cron pattern for swarm job | No | `*/15 * * * *` | +| `BEE_ENDPOINT` | Bee endpoint | No | (empty) | +| `BEE_DEBUG_ENDPOINT` | Bee debug endpoint | No | (empty) | ## Blob propagator diff --git a/apps/rest-api-server/package.json b/apps/rest-api-server/package.json index cacc1b9a..ba869f47 100644 --- a/apps/rest-api-server/package.json +++ b/apps/rest-api-server/package.json @@ -18,7 +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/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/env.ts b/apps/rest-api-server/src/env.ts index 09b03292..e7a153ae 100644 --- a/apps/rest-api-server/src/env.ts +++ b/apps/rest-api-server/src/env.ts @@ -11,6 +11,9 @@ import { export const env = createEnv({ envOptions: { server: { + // FIXME + // BEE_ENDPOINT: requiredStorageConfigSchema("SWARM", z.string().url()), + BEE_ENDPOINT: z.string().optional(), BLOBSCAN_API_BASE_URL: z .string() .url() @@ -22,8 +25,11 @@ export const env = createEnv({ METRICS_ENABLED: booleanSchema.default("false"), REDIS_URI: z.string().default("redis://localhost:6379"), DENCUN_FORK_SLOT: z.coerce.number().optional(), + SWARM_STAMP_CRON_PATTERN: z.string().default("*/15 * * * *"), STATS_SYNCER_DAILY_CRON_PATTERN: z.string().default("30 0 * * * *"), STATS_SYNCER_OVERALL_CRON_PATTERN: z.string().default("*/15 * * * *"), + SWARM_BATCH_ID: z.string().optional(), + SWARM_STORAGE_ENABLED: booleanSchema.default("false"), SENTRY_DSN_API: z.string().url().optional(), }, diff --git a/apps/rest-api-server/src/index.ts b/apps/rest-api-server/src/index.ts index e62bf6f7..92b844e2 100644 --- a/apps/rest-api-server/src/index.ts +++ b/apps/rest-api-server/src/index.ts @@ -14,28 +14,16 @@ import { gracefulShutdown as apiGracefulShutdown, } from "@blobscan/api"; import { collectDefaultMetrics } from "@blobscan/open-telemetry"; -import { StatsSyncer } from "@blobscan/stats-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, - }, -}); +const closeSyncers = setUpSyncers(); const app = express(); @@ -73,7 +61,9 @@ async function gracefulShutdown(signal: string) { logger.debug(`Received ${signal}. Shutting down...`); await apiGracefulShutdown() - .finally(() => statsSyncer.close()) + .finally(async () => { + await closeSyncers(); + }) .finally(() => { server.close(() => { logger.debug("Server shut down successfully"); @@ -82,7 +72,11 @@ async function gracefulShutdown(signal: string) { } // Listen for TERM signal .e.g. kill -process.on("SIGTERM", () => void gracefulShutdown("SIGTERM")); +process.on("SIGTERM", async () => { + await gracefulShutdown("SIGTERM"); +}); // Listen for INT signal e.g. Ctrl-C -process.on("SIGINT", () => void gracefulShutdown("SIGINT")); +process.on("SIGINT", async () => { + await gracefulShutdown("SIGINT"); +}); diff --git a/apps/rest-api-server/src/syncers.ts b/apps/rest-api-server/src/syncers.ts new file mode 100644 index 00000000..084a0e6c --- /dev/null +++ b/apps/rest-api-server/src/syncers.ts @@ -0,0 +1,61 @@ +import type { BaseSyncer } from "@blobscan/syncers"; +import { + DailyStatsSyncer, + OverallStatsSyncer, + SwarmStampSyncer, + createRedisConnection, +} from "@blobscan/syncers"; + +import { env } from "./env"; +import { logger } from "./logger"; +import { getNetworkDencunForkSlot } from "./utils"; + +export function setUpSyncers() { + const connection = createRedisConnection(env.REDIS_URI); + const syncers: BaseSyncer[] = []; + + if (env.SWARM_STORAGE_ENABLED) { + if (!env.SWARM_BATCH_ID) { + logger.error(`Can't initialize Swarm stamp job: no batch ID provided`); + } else if (!env.BEE_ENDPOINT) { + logger.error("Can't initialize Swarm stamp job: no Bee endpoint provided"); + } else { + syncers.push( + new SwarmStampSyncer({ + cronPattern: env.SWARM_STAMP_CRON_PATTERN, + redisUriOrConnection: connection, + batchId: env.SWARM_BATCH_ID, + beeEndpoint: env.BEE_ENDPOINT, + }) + ); + } + } + + syncers.push( + new DailyStatsSyncer({ + cronPattern: env.STATS_SYNCER_DAILY_CRON_PATTERN, + redisUriOrConnection: connection, + }) + ); + + syncers.push( + new OverallStatsSyncer({ + cronPattern: env.STATS_SYNCER_OVERALL_CRON_PATTERN, + redisUriOrConnection: connection, + lowestSlot: + env.DENCUN_FORK_SLOT ?? getNetworkDencunForkSlot(env.NETWORK_NAME), + }) + ); + + Promise.all(syncers.map((syncer) => syncer.start())); + + return () => { + let teardownPromise = Promise.resolve(); + + for (const syncer of syncers) { + teardownPromise = teardownPromise.finally(() => syncer.close()); + } + + return teardownPromise; + }; +} diff --git a/package.json b/package.json index 53021d87..87753bbd 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "@vitest/coverage-v8": "^0.34.3", "@vitest/ui": "^0.34.1", "dotenv-cli": "^7.2.1", + "msw": "^2.3.1", "prettier": "^2.8.8", "prettier-plugin-tailwindcss": "^0.2.8", "ts-node": "^10.9.1", diff --git a/packages/db/prisma/migrations/20240613112056_add_updated_at_field/migration.sql b/packages/db/prisma/migrations/20240613112056_add_updated_at_field/migration.sql new file mode 100644 index 00000000..e9b6d911 --- /dev/null +++ b/packages/db/prisma/migrations/20240613112056_add_updated_at_field/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "blob_storages_state" ADD COLUMN "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP; diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma index b6b3b93f..4f2d4a97 100644 --- a/packages/db/prisma/schema.prisma +++ b/packages/db/prisma/schema.prisma @@ -53,10 +53,12 @@ model BlockchainSyncState { @@map("blockchain_sync_state") } +//TODO: Rename to SwarmBatchStorage model BlobStoragesState { - id Int @id @default(autoincrement()) - swarmDataId String? @map("swarm_data_id") - swarmDataTTL Int? @map("swarm_data_ttl") + id Int @id @default(autoincrement()) + swarmDataId String? @map("swarm_data_id") // TODO: rename to batchId + swarmDataTTL Int? @map("swarm_data_ttl") // TODO: rename to batchTtl + updatedAt DateTime @default(now()) @map("updated_at") @@map("blob_storages_state") } @@ -259,7 +261,7 @@ model BlobDailyStats { } // NextAuth.js Models -// NOTE: When using postgresql, mysql or sqlserver, +// NOTE: When using postgresql, mysql or sqlserver, // uncomment the @db.Text annotations below // @see https://next-auth.js.org/schemas/models // model Account { 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/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/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 88% rename from packages/stats-syncer/package.json rename to packages/syncers/package.json index 7bd1d58d..039fa09a 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", @@ -17,6 +17,8 @@ "@blobscan/dayjs": "workspace:^0.0.2", "@blobscan/db": "workspace:^0.7.0", "@blobscan/logger": "workspace:^0.1.0", + "@blobscan/zod": "workspace:^0.1.0", + "axios": "^1.7.2", "bullmq": "^4.13.2", "ioredis": "^5.3.2" }, diff --git a/packages/stats-syncer/src/PeriodicUpdater.ts b/packages/syncers/src/BaseSyncer.ts similarity index 55% rename from packages/stats-syncer/src/PeriodicUpdater.ts rename to packages/syncers/src/BaseSyncer.ts index 6ef76c0d..48949a46 100644 --- a/packages/stats-syncer/src/PeriodicUpdater.ts +++ b/packages/syncers/src/BaseSyncer.ts @@ -5,41 +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"; -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; } @@ -48,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)); }); @@ -61,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 ); } @@ -88,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..8967ff33 --- /dev/null +++ b/packages/syncers/src/errors.ts @@ -0,0 +1,50 @@ +import type { AxiosError } from "axios"; + +import { z } from "@blobscan/zod"; + +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); + } +} + +const swarmApiResponseErrorSchema = z.object({ + code: z.number(), + message: z.string(), + reasons: z.array(z.unknown()).optional(), +}); + +export class SwarmNodeError extends ErrorException { + code: number | undefined; + reasons?: unknown[]; + + constructor(error: AxiosError) { + let message: string; + let code: number | undefined; + const result = swarmApiResponseErrorSchema.safeParse(error.response?.data); + let reasons: unknown[] | undefined; + + if (result.success) { + code = result.data.code; + message = result.data.message; + reasons = result.data.reasons; + } else { + message = error.message; + } + + super(message, error.cause); + + this.code = code; + this.reasons = reasons; + } +} diff --git a/packages/syncers/src/index.ts b/packages/syncers/src/index.ts new file mode 100644 index 00000000..2197f0b3 --- /dev/null +++ b/packages/syncers/src/index.ts @@ -0,0 +1,3 @@ +export { BaseSyncer } from "./BaseSyncer"; +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..f07d2df8 100644 --- a/packages/stats-syncer/src/updaters/DailyStatsUpdater.ts +++ b/packages/syncers/src/syncers/DailyStatsSyncer.ts @@ -1,9 +1,8 @@ -import type { Redis } from "ioredis"; - import { normalizeDailyDate, normalizeDate, prisma } from "@blobscan/db"; import type { PrismaPromise, RawDatePeriod } from "@blobscan/db"; -import { PeriodicUpdater } from "../PeriodicUpdater"; +import { BaseSyncer } from "../BaseSyncer"; +import type { CommonSyncerConfig } from "../BaseSyncer"; import { formatDate } from "../utils"; interface DailyStatsModel { @@ -20,13 +19,17 @@ const dailyStatsModels: Record = { transaction: prisma.transactionDailyStats, }; -export class DailyStatsUpdater extends PeriodicUpdater { - constructor(redisUriOrConnection: string | Redis) { - const name = "daily"; +export type DailyStatsSyncerConfig = 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..3720b1bb 100644 --- a/packages/stats-syncer/src/updaters/OverallStatsUpdater.ts +++ b/packages/syncers/src/syncers/OverallStatsSyncer.ts @@ -1,14 +1,13 @@ -import type { Redis } from "ioredis"; - import { prisma } from "@blobscan/db"; import type { BlockNumberRange, Prisma } from "@blobscan/db"; -import { PeriodicUpdater } from "../PeriodicUpdater"; +import { BaseSyncer } from "../BaseSyncer"; +import type { CommonSyncerConfig } 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 +16,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/SwarmStampSyncer.ts b/packages/syncers/src/syncers/SwarmStampSyncer.ts new file mode 100644 index 00000000..25da38b4 --- /dev/null +++ b/packages/syncers/src/syncers/SwarmStampSyncer.ts @@ -0,0 +1,73 @@ +import type { AxiosResponse } from "axios"; +import { AxiosError } from "axios"; +import axios from "axios"; + +import { prisma } from "@blobscan/db"; + +import { BaseSyncer } from "../BaseSyncer"; +import type { CommonSyncerConfig } from "../BaseSyncer"; +import { SwarmNodeError } from "../errors"; + +type BatchData = { + batchID: string; + batchTTL: number; +}; + +export interface SwarmStampSyncerConfig extends CommonSyncerConfig { + beeEndpoint: string; + batchId: string; +} + +export class SwarmStampSyncer extends BaseSyncer { + constructor({ + cronPattern, + redisUriOrConnection, + batchId, + beeEndpoint, + }: SwarmStampSyncerConfig) { + const name = "swarm-stamp"; + super({ + name, + cronPattern, + redisUriOrConnection, + syncerFn: async () => { + let response: AxiosResponse; + + try { + const url = `${beeEndpoint}/stamps/${batchId}`; + + response = await axios.get(url); + } catch (err) { + let cause = err; + + if (err instanceof AxiosError) { + cause = new SwarmNodeError(err); + } + + throw new Error(`Failed to fetch stamp batch "${batchId}"`, { + cause, + }); + } + + const { batchTTL } = response.data; + + await prisma.blobStoragesState.upsert({ + create: { + swarmDataId: batchId, + swarmDataTTL: batchTTL, + }, + update: { + swarmDataTTL: batchTTL, + updatedAt: new Date(), + }, + where: { + id: 1, + swarmDataId: batchId, + }, + }); + + this.logger.info(`Swarm stamp data with batch ID "${batchId}" updated`); + }, + }); + } +} diff --git a/packages/syncers/src/syncers/index.ts b/packages/syncers/src/syncers/index.ts new file mode 100644 index 00000000..660eaf48 --- /dev/null +++ b/packages/syncers/src/syncers/index.ts @@ -0,0 +1,6 @@ +export { DailyStatsSyncer } from "./DailyStatsSyncer"; +export type { DailyStatsSyncerConfig } from "./DailyStatsSyncer"; +export { OverallStatsSyncer } from "./OverallStatsSyncer"; +export type { OverallStatsSyncerConfig } from "./OverallStatsSyncer"; +export { SwarmStampSyncer } from "./SwarmStampSyncer"; +export type { SwarmStampSyncerConfig } from "./SwarmStampSyncer"; 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/stats-syncer/test/PeriodicUpdater.test.ts b/packages/syncers/test/BaseSyncer.test.ts similarity index 74% rename from packages/stats-syncer/test/PeriodicUpdater.test.ts rename to packages/syncers/test/BaseSyncer.test.ts index 9354c905..2d480ddf 100644 --- a/packages/stats-syncer/test/PeriodicUpdater.test.ts +++ b/packages/syncers/test/BaseSyncer.test.ts @@ -2,28 +2,29 @@ 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 = {}) { +import { BaseSyncer } from "../src/BaseSyncer"; +import type { BaseSyncerConfig } from "../src/BaseSyncer"; +import { SyncerError } from "../src/errors"; + +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 +52,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 +70,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 +96,7 @@ describe("PeriodicUpdater", () => { .spyOn(worker, "removeAllListeners") .mockReturnValueOnce(worker); - await periodicUpdater.close(); + await closingPeriodicUpdater.close(); expect(queueCloseSpy).toHaveBeenCalledOnce(); expect(workerCloseSpy).toHaveBeenCalledOnce(); @@ -117,7 +121,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/stats-syncer/test/OverallStatsUpdater.test.ts b/packages/syncers/test/OverallStatsSyncer.test.ts similarity index 91% rename from packages/stats-syncer/test/OverallStatsUpdater.test.ts rename to packages/syncers/test/OverallStatsSyncer.test.ts index bed3404d..fbd13987 100644 --- a/packages/stats-syncer/test/OverallStatsUpdater.test.ts +++ b/packages/syncers/test/OverallStatsSyncer.test.ts @@ -3,16 +3,16 @@ 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 } from "../src/syncers/OverallStatsSyncer"; +import type { 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 +23,7 @@ class OverallStatsUpdaterMock extends OverallStatsUpdater { } getWorkerProcessor() { - return this.updaterFn; + return this.syncerFn; } getQueue() { @@ -114,7 +114,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 +193,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/syncers/test/SwarmStampSyncer.test.ts b/packages/syncers/test/SwarmStampSyncer.test.ts new file mode 100644 index 00000000..84ed0d47 --- /dev/null +++ b/packages/syncers/test/SwarmStampSyncer.test.ts @@ -0,0 +1,179 @@ +/* eslint-disable @typescript-eslint/no-misused-promises */ +import { http, HttpResponse } from "msw"; +import { setupServer } from "msw/node"; +import { beforeAll, beforeEach, describe, expect, it } from "vitest"; + +import type { BlobStoragesState } from "@blobscan/db"; +import { prisma } from "@blobscan/db"; +import { fixtures, testValidError } from "@blobscan/test"; + +import type { SwarmStampSyncerConfig } from "../src/syncers/SwarmStampSyncer"; +import { SwarmStampSyncer } from "../src/syncers/SwarmStampSyncer"; + +const BEE_ENDPOINT = process.env.BEE_ENDPOINT ?? "http://localhost:1633"; + +class SwarmStampSyncerMock extends SwarmStampSyncer { + constructor({ batchId, cronPattern }: Partial = {}) { + super({ + redisUriOrConnection: process.env.REDIS_URI ?? "", + cronPattern: cronPattern ?? "* * * * *", + batchId: batchId ?? process.env.SWARM_BATCH_ID ?? "", + beeEndpoint: BEE_ENDPOINT, + }); + } + + getQueue() { + return this.queue; + } + + getWorkerProcessor() { + return this.syncerFn; + } +} + +describe("SwarmStampSyncer", () => { + const expectedBatchId = fixtures.blobStoragesState[0]?.swarmDataId as string; + const expectedBatchTTL = 1000; + + let swarmStampSyncer: SwarmStampSyncerMock; + + beforeAll(() => { + const baseUrl = `${BEE_ENDPOINT}/stamps`; + const server = setupServer( + ...[ + http.get(`${baseUrl}/:batchId`, ({ request }) => { + const batchId = request.url.split("/").pop(); + + if (!batchId || batchId.length !== 64) { + return HttpResponse.json( + { + code: 400, + message: "invalid path params", + reasons: [ + { + field: "batch_id", + error: "odd length hex string", + }, + ], + }, + { status: 400 } + ); + } + + if (batchId !== expectedBatchId) { + return HttpResponse.json( + { + code: 404, + message: "issuer does not exist", + }, + { status: 404 } + ); + } + + return HttpResponse.json( + { + batchID: expectedBatchId, + batchTTL: expectedBatchTTL, + }, + { + status: 200, + } + ); + }), + ] + ); + + server.listen(); + + return () => { + server.close(); + }; + }); + + beforeEach(() => { + swarmStampSyncer = new SwarmStampSyncerMock(); + + return async () => { + await swarmStampSyncer.close(); + }; + }); + + describe("when creating a new swarm batch data row in the db", async () => { + let blobStorageState: BlobStoragesState | null = null; + + beforeEach(async () => { + await prisma.blobStoragesState.deleteMany(); + + const workerProcessor = swarmStampSyncer.getWorkerProcessor(); + + await workerProcessor().catch((err) => console.log(err)); + + blobStorageState = await prisma.blobStoragesState.findFirst(); + }); + + it("should create it with the correct swarm stamp batch ID", async () => { + expect(blobStorageState?.swarmDataId).toBe(process.env.SWARM_BATCH_ID); + }); + + it("should create it with the correct batch TTL", async () => { + expect(blobStorageState?.swarmDataTTL).toBe(expectedBatchTTL); + }); + }); + + it("should update the batch TTl", async () => { + await prisma.blobStoragesState.update({ + data: { + swarmDataTTL: 99999, + }, + where: { + id: 1, + }, + }); + + const workerProcessor = swarmStampSyncer.getWorkerProcessor(); + await workerProcessor(); + + const blobStorageState = await prisma.blobStoragesState.findFirst(); + + expect(blobStorageState?.swarmDataTTL).toBe(expectedBatchTTL); + }); + + testValidError( + "should fail when trying to fetch a non-existing batch", + async () => { + const failingSwarmStampSyncer = new SwarmStampSyncerMock({ + batchId: + "6b538866048cfb6e9e1d06805374c51572c11219d2d550c03e6e277366cb0371", + }); + const failingWorkerProcessor = + failingSwarmStampSyncer.getWorkerProcessor(); + + await failingWorkerProcessor().finally(async () => { + await failingSwarmStampSyncer.close(); + }); + }, + Error, + { + checkCause: true, + } + ); + + testValidError( + "should fail when trying to fetch an invalid batch", + async () => { + const failingSwarmStampSyncer = new SwarmStampSyncerMock({ + batchId: "invalid-batch", + }); + const failingWorkerProcessor = + failingSwarmStampSyncer.getWorkerProcessor(); + + await failingWorkerProcessor().finally(async () => { + await failingSwarmStampSyncer.close(); + }); + }, + Error, + { + checkCause: true, + } + ); +}); diff --git a/packages/syncers/test/__snapshots__/BaseSyncer.test.ts.snap b/packages/syncers/test/__snapshots__/BaseSyncer.test.ts.snap new file mode 100644 index 00000000..dba557d8 --- /dev/null +++ b/packages/syncers/test/__snapshots__/BaseSyncer.test.ts.snap @@ -0,0 +1,9 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +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`] = `"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__/SwarmStampSyncer.test.ts.snap b/packages/syncers/test/__snapshots__/SwarmStampSyncer.test.ts.snap new file mode 100644 index 00000000..8385c3d7 --- /dev/null +++ b/packages/syncers/test/__snapshots__/SwarmStampSyncer.test.ts.snap @@ -0,0 +1,9 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`SwarmStampSyncer > should fail when trying to fetch a non-existing batch 1`] = `"Failed to fetch stamp batch \\"6b538866048cfb6e9e1d06805374c51572c11219d2d550c03e6e277366cb0371\\""`; + +exports[`SwarmStampSyncer > should fail when trying to fetch a non-existing batch 2`] = `[SwarmNodeError: issuer does not exist]`; + +exports[`SwarmStampSyncer > should fail when trying to fetch an invalid batch 1`] = `"Failed to fetch stamp batch \\"invalid-batch\\""`; + +exports[`SwarmStampSyncer > should fail when trying to fetch an invalid batch 2`] = `[SwarmNodeError: invalid path params]`; 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/syncers/test/helpers.ts b/packages/syncers/test/helpers.ts new file mode 100644 index 00000000..0c79d5a5 --- /dev/null +++ b/packages/syncers/test/helpers.ts @@ -0,0 +1,11 @@ +import { setupServer } from "msw/node"; + +export function createServer(handlers: Parameters[0][]) { + const server = setupServer(...handlers); + + server.listen(); + + return () => { + server.close(); + }; +} 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/index.ts b/packages/test/src/fixtures/index.ts index b96e6fed..5a31ad03 100644 --- a/packages/test/src/fixtures/index.ts +++ b/packages/test/src/fixtures/index.ts @@ -1,9 +1,5 @@ -import { - BlobData, - BlobDataStorageReference, - type PrismaClient, - type Rollup, -} from "@prisma/client"; +import type { BlobData, BlobDataStorageReference } from "@prisma/client"; +import type { PrismaClient, Rollup } from "@prisma/client"; import POSTGRES_DATA from "./postgres/data.json"; diff --git a/packages/test/src/fixtures/postgres/data.json b/packages/test/src/fixtures/postgres/data.json index 2158e14f..a227867c 100644 --- a/packages/test/src/fixtures/postgres/data.json +++ b/packages/test/src/fixtures/postgres/data.json @@ -12,8 +12,9 @@ "blobStoragesState": [ { "id": 1, - "swarmDataId": "batch-1", - "swarmDataTTL": 1000 + "swarmDataId": "f89e63edf757f06e89933761d6d46592d03026efb9871f9d244f34da86b6c242", + "swarmDataTTL": 1000, + "updatedAt": "2023-10-31T12:10:00Z" } ], "blocks": [ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e9cdf671..940d5e13 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -59,6 +59,9 @@ importers: dotenv-cli: specifier: ^7.2.1 version: 7.4.1 + msw: + specifier: ^2.3.1 + version: 2.3.1(typescript@5.4.5) prettier: specifier: ^2.8.8 version: 2.8.8 @@ -103,7 +106,7 @@ importers: version: 2.2.1 '@tailwindcss/typography': specifier: ^0.5.7 - version: 0.5.13(tailwindcss@3.4.3(ts-node@10.9.2(@types/node@20.12.7)(typescript@5.4.5))) + version: 0.5.13(tailwindcss@3.4.3(ts-node@10.9.2(@types/node@20.14.2)(typescript@5.4.5))) autoprefixer: specifier: ^10.4.14 version: 10.4.19(postcss@8.4.38) @@ -142,7 +145,7 @@ importers: version: 1.2.1 tailwindcss: specifier: ^3.3.1 - version: 3.4.3(ts-node@10.9.2(@types/node@20.12.7)(typescript@5.4.5)) + version: 3.4.3(ts-node@10.9.2(@types/node@20.14.2)(typescript@5.4.5)) devDependencies: eslint: specifier: ^8.45.0 @@ -171,9 +174,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/syncers': + specifier: workspace:^0.1.9 + version: link:../../packages/syncers '@blobscan/zod': specifier: workspace:^0.1.0 version: link:../../packages/zod @@ -587,7 +590,7 @@ importers: specifier: ^14.2.0 version: 14.2.0 - packages/stats-syncer: + packages/syncers: dependencies: '@blobscan/dayjs': specifier: workspace:^0.0.2 @@ -598,6 +601,12 @@ importers: '@blobscan/logger': specifier: workspace:^0.1.0 version: link:../logger + '@blobscan/zod': + specifier: workspace:^0.1.0 + version: link:../zod + axios: + specifier: ^1.7.2 + version: 1.7.2 bullmq: specifier: ^4.13.2 version: 4.17.0 @@ -675,7 +684,7 @@ importers: version: 8.4.38 tailwindcss: specifier: ^3.3.1 - version: 3.4.3(ts-node@10.9.2(@types/node@20.12.7)(typescript@5.4.5)) + version: 3.4.3(ts-node@10.9.2(@types/node@20.14.2)(typescript@5.4.5)) tooling/typescript: {} @@ -1391,6 +1400,12 @@ packages: '@bcoe/v8-coverage@0.2.3': resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + '@bundled-es-modules/cookie@2.0.0': + resolution: {integrity: sha512-Or6YHg/kamKHpxULAdSqhGqnWFneIXu1NKvvfBBzKGwpVsYuFIQ5aBPHDnnoR3ghW1nvSkALd+EF9iMtY7Vjxw==} + + '@bundled-es-modules/statuses@1.0.1': + resolution: {integrity: sha512-yn7BklA5acgcBr+7w064fGV+SGIFySjCKpqjcWgBAIfrAkY+4GQTJJHQMeT3V/sgz23VTEVV8TtOmkvJAhFVfg==} + '@changesets/apply-release-plan@7.0.0': resolution: {integrity: sha512-vfi69JR416qC9hWmFGSxj7N6wA5J222XNBmezSVATPWDVPIF7gkd4d8CpbEbXmRWbVrkoli3oerGS6dcL/BGsQ==} @@ -1791,6 +1806,22 @@ packages: '@vue/compiler-sfc': optional: true + '@inquirer/confirm@3.1.9': + resolution: {integrity: sha512-UF09aejxCi4Xqm6N/jJAiFXArXfi9al52AFaSD+2uIHnhZGtd1d6lIGTRMPouVSJxbGEi+HkOWSYaiEY/+szUw==} + engines: {node: '>=18'} + + '@inquirer/core@8.2.2': + resolution: {integrity: sha512-K8SuNX45jEFlX3EBJpu9B+S2TISzMPGXZIuJ9ME924SqbdW6Pt6fIkKvXg7mOEOKJ4WxpQsxj0UTfcL/A434Ww==} + engines: {node: '>=18'} + + '@inquirer/figures@1.0.3': + resolution: {integrity: sha512-ErXXzENMH5pJt5/ssXV0DfWUZqly8nGzf0UcBV9xTnP+KyffE2mqyxIMBrZ8ijQck2nU0TQm40EQB53YreyWHw==} + engines: {node: '>=18'} + + '@inquirer/type@1.3.3': + resolution: {integrity: sha512-xTUt0NulylX27/zMx04ZYar/kr1raaiFTVvQ5feljQsiAgdm0WPj4S73/ye0fbslh+15QrIuDvfCXTek7pMY5A==} + engines: {node: '>=18'} + '@ioredis/commands@1.2.0': resolution: {integrity: sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==} @@ -1898,6 +1929,14 @@ packages: cpu: [x64] os: [win32] + '@mswjs/cookies@1.1.0': + resolution: {integrity: sha512-0ZcCVQxifZmhwNBoQIrystCb+2sWBY2Zw8lpfJBPCHGCA/HWqehITeCRVIv4VMy8MPlaHo2w2pTHFV2pFfqKPw==} + engines: {node: '>=18'} + + '@mswjs/interceptors@0.29.1': + resolution: {integrity: sha512-3rDakgJZ77+RiQUuSK69t1F0m8BQKA8Vh5DCS5V0DWvNY67zob2JhhQrhCO0AKLGINTRSFd1tBaHcJTkhefoSw==} + engines: {node: '>=18'} + '@next-auth/prisma-adapter@1.0.7': resolution: {integrity: sha512-Cdko4KfcmKjsyHFrWwZ//lfLUbcLqlyFqjd/nYE2m3aZ7tjMNUjpks47iw7NTCnXf+5UWz5Ypyt1dSs1EP5QJw==} peerDependencies: @@ -2063,6 +2102,15 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@open-draft/deferred-promise@2.2.0': + resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==} + + '@open-draft/logger@0.3.0': + resolution: {integrity: sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==} + + '@open-draft/until@2.1.0': + resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} + '@opentelemetry/api-logs@0.41.2': resolution: {integrity: sha512-JEV2RAqijAFdWeT6HddYymfnkiRu2ASxoTBr4WsnGJhOjWZkEy6vp+Sx9ozr1NaIODOa2HUyckExIqQjn6qywQ==} engines: {node: '>=14'} @@ -2765,6 +2813,9 @@ packages: '@types/connect@3.4.38': resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + '@types/cookie@0.6.0': + resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + '@types/cors@2.8.17': resolution: {integrity: sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==} @@ -2825,14 +2876,17 @@ packages: '@types/morgan@1.9.9': resolution: {integrity: sha512-iRYSDKVaC6FkGSpEVVIvrRGw0DfJMiQzIn3qr2G5B3C//AWkulhXgaBd7tS9/J79GWSYMTHGs7PfI5b3Y8m+RQ==} + '@types/mute-stream@0.0.4': + resolution: {integrity: sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==} + '@types/node@12.20.55': resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} '@types/node@18.19.31': resolution: {integrity: sha512-ArgCD39YpyyrtFKIqMDvjz79jto5fcI/SVUs2HwB+f0dAzq68yqOdyaSivLiLugSziTpNXLQrVb7RZFmdZzbhA==} - '@types/node@20.12.7': - resolution: {integrity: sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==} + '@types/node@20.14.2': + resolution: {integrity: sha512-xyu6WAMVwv6AKFLB+e/7ySZVr/0zLCzOa7rSpq6jNwpqOrUbcACDWC+53d4n2QHOnDou0fbIsg8wZu/sxrnI4Q==} '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} @@ -2879,6 +2933,9 @@ packages: '@types/shimmer@1.0.5': resolution: {integrity: sha512-9Hp0ObzwwO57DpLFF0InUjUm/II8GmKAvzbefxQTihCb7KI6yc9yzf0nLc4mVdby5N4DRCgQM2wCup9KTieeww==} + '@types/statuses@2.0.5': + resolution: {integrity: sha512-jmIUGWrAiwu3dZpxntxieC+1n/5c3mjrImkmOSQ2NC5uP6cYO4aAZDdSmRcI5C1oiTmqlZGHC+/NmJrKogbP5A==} + '@types/strip-bom@3.0.0': resolution: {integrity: sha512-xevGOReSYGM7g/kUBZzPqCrR/KYAo+F0yiPc85WFTJa0MSLtyFTVTU6cJu/aV4mid7IffDIWqo69THF2o4JiEQ==} @@ -2891,6 +2948,9 @@ packages: '@types/triple-beam@1.3.5': resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==} + '@types/wrap-ansi@3.0.0': + resolution: {integrity: sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==} + '@types/zrender@4.0.6': resolution: {integrity: sha512-1jZ9bJn2BsfmYFPBHtl5o3uV+ILejAtGrDcYSpT4qaVKEI/0YY+arw3XHU04Ebd8Nca3SQ7uNcLaqiL+tTFVMg==} @@ -3061,6 +3121,10 @@ packages: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} + ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -3185,6 +3249,9 @@ 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==} @@ -3400,6 +3467,14 @@ packages: classnames@2.5.1: resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==} + cli-spinners@2.9.2: + resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} + engines: {node: '>=6'} + + cli-width@4.1.0: + resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} + engines: {node: '>= 12'} + client-only@0.0.1: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} @@ -4331,6 +4406,10 @@ packages: graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + graphql@16.8.2: + resolution: {integrity: sha512-cvVIBILwuoSyD54U4cF/UXDh5yAobhNV/tPygI4lZhgOIJQE/WLWC4waBRb4I6bDVYb3OVx3lfHbaQOEoUD5sg==} + engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + gtoken@6.1.2: resolution: {integrity: sha512-4ccGpzz7YAr7lxrT2neugmXQ3hP9ho2gcaityLVkiUecAiwiy60Ii8gRbZeOsXV19fYaRjgBSshs8kXw+NKCPQ==} engines: {node: '>=12.0.0'} @@ -4379,6 +4458,9 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + headers-polyfill@4.0.3: + resolution: {integrity: sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==} + hexer@1.5.0: resolution: {integrity: sha512-dyrPC8KzBzUJ19QTIo1gXNqIISRXQ0NwteW6OeQHRN4ZuZeHkdODfj0zHBdOlHbRY8GqbqK57C9oWSvQZizFsg==} engines: {node: '>= 0.10.x'} @@ -4548,6 +4630,9 @@ packages: resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} engines: {node: '>= 0.4'} + is-node-process@1.2.0: + resolution: {integrity: sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==} + is-number-object@1.0.7: resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} engines: {node: '>= 0.4'} @@ -5052,9 +5137,23 @@ packages: msgpackr@1.10.1: resolution: {integrity: sha512-r5VRLv9qouXuLiIBrLpl2d5ZvPt8svdQTl5/vMvE4nzDMyEX4sgW5yWhuBBj5UmgwOTWj8CIdSXn5sAfsHAWIQ==} + msw@2.3.1: + resolution: {integrity: sha512-ocgvBCLn/5l3jpl1lssIb3cniuACJLoOfZu01e3n5dbJrpA5PeeWn28jCLgQDNt6d7QT8tF2fYRzm9JoEHtiig==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + typescript: '>= 4.7.x' + peerDependenciesMeta: + typescript: + optional: true + multiformats@9.9.0: resolution: {integrity: sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==} + mute-stream@1.0.0: + resolution: {integrity: sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} @@ -5279,6 +5378,9 @@ packages: outdent@0.5.0: resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==} + outvariant@1.4.2: + resolution: {integrity: sha512-Ou3dJ6bA/UJ5GVHxah4LnqDwZRwAmWxrG3wtrHrbGnP4RnLCtA64A4F+ae7Y8ww660JaddSoArUR5HjipWSHAQ==} + p-cancelable@3.0.0: resolution: {integrity: sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==} engines: {node: '>=12.20'} @@ -5354,6 +5456,9 @@ packages: path-to-regexp@0.1.7: resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==} + path-to-regexp@6.2.2: + resolution: {integrity: sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==} + path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} @@ -5897,6 +6002,10 @@ packages: signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + simple-concat@1.0.1: resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} @@ -5997,6 +6106,9 @@ packages: streamx@2.16.1: resolution: {integrity: sha512-m9QYj6WygWyWa3H1YY69amr4nVgy61xfjys7xO7kviL5rfIEc2naf+ewFiOA+aEJD7y0JO3h2GoiUv4TDwEGzQ==} + strict-event-emitter@0.5.1: + resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==} + string-template@0.2.1: resolution: {integrity: sha512-Yptehjogou2xm4UJbxJ4CxgZx12HBfeystp0y3x7s4Dj32ltVVG1Gg8YhKjHZkHicuKpZX/ffilA8505VbUbpw==} @@ -6344,6 +6456,10 @@ packages: resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} engines: {node: '>=10'} + type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + type-fest@0.6.0: resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==} engines: {node: '>=8'} @@ -6356,6 +6472,10 @@ packages: resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} engines: {node: '>=8'} + type-fest@4.20.0: + resolution: {integrity: sha512-MBh+PHUHHisjXf4tlx0CFWoMdjx8zCMLJHOjnV1prABYZFHqtFOyauCIK2/7w4oIfwkF8iNhLtnJEfVY2vn3iw==} + engines: {node: '>=16'} + type-is@1.6.18: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} @@ -7618,6 +7738,14 @@ snapshots: '@bcoe/v8-coverage@0.2.3': {} + '@bundled-es-modules/cookie@2.0.0': + dependencies: + cookie: 0.5.0 + + '@bundled-es-modules/statuses@1.0.1': + dependencies: + statuses: 2.0.1 + '@changesets/apply-release-plan@7.0.0': dependencies: '@babel/runtime': 7.23.2 @@ -8258,6 +8386,31 @@ snapshots: transitivePeerDependencies: - supports-color + '@inquirer/confirm@3.1.9': + dependencies: + '@inquirer/core': 8.2.2 + '@inquirer/type': 1.3.3 + + '@inquirer/core@8.2.2': + dependencies: + '@inquirer/figures': 1.0.3 + '@inquirer/type': 1.3.3 + '@types/mute-stream': 0.0.4 + '@types/node': 20.14.2 + '@types/wrap-ansi': 3.0.0 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + cli-spinners: 2.9.2 + cli-width: 4.1.0 + mute-stream: 1.0.0 + signal-exit: 4.1.0 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + + '@inquirer/figures@1.0.3': {} + + '@inquirer/type@1.3.3': {} + '@ioredis/commands@1.2.0': {} '@istanbuljs/schema@0.1.3': {} @@ -8371,6 +8524,17 @@ snapshots: '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.2': optional: true + '@mswjs/cookies@1.1.0': {} + + '@mswjs/interceptors@0.29.1': + dependencies: + '@open-draft/deferred-promise': 2.2.0 + '@open-draft/logger': 0.3.0 + '@open-draft/until': 2.1.0 + is-node-process: 1.2.0 + outvariant: 1.4.2 + strict-event-emitter: 0.5.1 + '@next-auth/prisma-adapter@1.0.7(@prisma/client@5.13.0(prisma@5.13.0))(next-auth@4.24.7(next@13.5.6(@babel/core@7.24.5)(@opentelemetry/api@1.8.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0))': dependencies: '@prisma/client': 5.13.0(prisma@5.13.0) @@ -8473,6 +8637,15 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.15.0 + '@open-draft/deferred-promise@2.2.0': {} + + '@open-draft/logger@0.3.0': + dependencies: + is-node-process: 1.2.0 + outvariant: 1.4.2 + + '@open-draft/until@2.1.0': {} + '@opentelemetry/api-logs@0.41.2': dependencies: '@opentelemetry/api': 1.8.0 @@ -9180,13 +9353,13 @@ snapshots: mini-svg-data-uri: 1.4.4 tailwindcss: 3.4.3(ts-node@10.9.2(@types/node@18.19.31)(typescript@5.4.5)) - '@tailwindcss/typography@0.5.13(tailwindcss@3.4.3(ts-node@10.9.2(@types/node@20.12.7)(typescript@5.4.5)))': + '@tailwindcss/typography@0.5.13(tailwindcss@3.4.3(ts-node@10.9.2(@types/node@20.14.2)(typescript@5.4.5)))': dependencies: lodash.castarray: 4.4.0 lodash.isplainobject: 4.0.6 lodash.merge: 4.6.2 postcss-selector-parser: 6.0.10 - tailwindcss: 3.4.3(ts-node@10.9.2(@types/node@20.12.7)(typescript@5.4.5)) + tailwindcss: 3.4.3(ts-node@10.9.2(@types/node@20.14.2)(typescript@5.4.5)) '@tanstack/query-core@4.36.1': {} @@ -9261,6 +9434,8 @@ snapshots: dependencies: '@types/node': 18.19.31 + '@types/cookie@0.6.0': {} + '@types/cors@2.8.17': dependencies: '@types/node': 18.19.31 @@ -9333,16 +9508,19 @@ snapshots: dependencies: '@types/node': 18.19.31 + '@types/mute-stream@0.0.4': + dependencies: + '@types/node': 18.19.31 + '@types/node@12.20.55': {} '@types/node@18.19.31': dependencies: undici-types: 5.26.5 - '@types/node@20.12.7': + '@types/node@20.14.2': dependencies: undici-types: 5.26.5 - optional: true '@types/normalize-package-data@2.4.4': {} @@ -9391,6 +9569,8 @@ snapshots: '@types/shimmer@1.0.5': {} + '@types/statuses@2.0.5': {} + '@types/strip-bom@3.0.0': {} '@types/strip-json-comments@0.0.30': {} @@ -9402,6 +9582,8 @@ snapshots: '@types/triple-beam@1.3.5': {} + '@types/wrap-ansi@3.0.0': {} + '@types/zrender@4.0.6': {} '@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5)': @@ -9650,6 +9832,10 @@ snapshots: ansi-colors@4.1.3: {} + ansi-escapes@4.3.2: + dependencies: + type-fest: 0.21.3 + ansi-regex@5.0.1: {} ansi-styles@3.2.1: @@ -9789,6 +9975,14 @@ 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 @@ -10053,6 +10247,10 @@ snapshots: classnames@2.5.1: {} + cli-spinners@2.9.2: {} + + cli-width@4.1.0: {} + client-only@0.0.1: {} cliui@6.0.0: @@ -11240,6 +11438,8 @@ snapshots: graphemer@1.4.0: {} + graphql@16.8.2: {} + gtoken@6.1.2: dependencies: gaxios: 5.1.3 @@ -11293,6 +11493,8 @@ snapshots: dependencies: function-bind: 1.1.2 + headers-polyfill@4.0.3: {} + hexer@1.5.0: dependencies: ansi-color: 0.2.1 @@ -11476,6 +11678,8 @@ snapshots: is-negative-zero@2.0.3: {} + is-node-process@1.2.0: {} + is-number-object@1.0.7: dependencies: has-tostringtag: 1.0.2 @@ -11952,8 +12156,32 @@ snapshots: optionalDependencies: msgpackr-extract: 3.0.2 + msw@2.3.1(typescript@5.4.5): + dependencies: + '@bundled-es-modules/cookie': 2.0.0 + '@bundled-es-modules/statuses': 1.0.1 + '@inquirer/confirm': 3.1.9 + '@mswjs/cookies': 1.1.0 + '@mswjs/interceptors': 0.29.1 + '@open-draft/until': 2.1.0 + '@types/cookie': 0.6.0 + '@types/statuses': 2.0.5 + chalk: 4.1.2 + graphql: 16.8.2 + headers-polyfill: 4.0.3 + is-node-process: 1.2.0 + outvariant: 1.4.2 + path-to-regexp: 6.2.2 + strict-event-emitter: 0.5.1 + type-fest: 4.20.0 + yargs: 17.7.2 + optionalDependencies: + typescript: 5.4.5 + multiformats@9.9.0: {} + mute-stream@1.0.0: {} + mz@2.7.0: dependencies: any-promise: 1.3.0 @@ -12198,6 +12426,8 @@ snapshots: outdent@0.5.0: {} + outvariant@1.4.2: {} + p-cancelable@3.0.0: {} p-filter@2.1.0: @@ -12260,6 +12490,8 @@ snapshots: path-to-regexp@0.1.7: {} + path-to-regexp@6.2.2: {} + path-type@4.0.0: {} pathe@1.1.2: {} @@ -12308,13 +12540,13 @@ snapshots: postcss: 8.4.38 ts-node: 10.9.2(@types/node@18.19.31)(typescript@5.4.5) - postcss-load-config@4.0.2(postcss@8.4.38)(ts-node@10.9.2(@types/node@20.12.7)(typescript@5.4.5)): + postcss-load-config@4.0.2(postcss@8.4.38)(ts-node@10.9.2(@types/node@20.14.2)(typescript@5.4.5)): dependencies: lilconfig: 3.1.1 yaml: 2.4.2 optionalDependencies: postcss: 8.4.38 - ts-node: 10.9.2(@types/node@20.12.7)(typescript@5.4.5) + ts-node: 10.9.2(@types/node@20.14.2)(typescript@5.4.5) postcss-nested@6.0.1(postcss@8.4.38): dependencies: @@ -12827,6 +13059,8 @@ snapshots: signal-exit@3.0.7: {} + signal-exit@4.1.0: {} + simple-concat@1.0.1: {} simple-functional-loader@1.2.1: @@ -12932,6 +13166,8 @@ snapshots: optionalDependencies: bare-events: 2.2.2 + strict-event-emitter@0.5.1: {} + string-template@0.2.1: {} string-width@4.2.3: @@ -13090,7 +13326,7 @@ snapshots: transitivePeerDependencies: - ts-node - tailwindcss@3.4.3(ts-node@10.9.2(@types/node@20.12.7)(typescript@5.4.5)): + tailwindcss@3.4.3(ts-node@10.9.2(@types/node@20.14.2)(typescript@5.4.5)): dependencies: '@alloc/quick-lru': 5.2.0 arg: 5.0.2 @@ -13109,7 +13345,7 @@ snapshots: postcss: 8.4.38 postcss-import: 15.1.0(postcss@8.4.38) postcss-js: 4.0.1(postcss@8.4.38) - postcss-load-config: 4.0.2(postcss@8.4.38)(ts-node@10.9.2(@types/node@20.12.7)(typescript@5.4.5)) + postcss-load-config: 4.0.2(postcss@8.4.38)(ts-node@10.9.2(@types/node@20.14.2)(typescript@5.4.5)) postcss-nested: 6.0.1(postcss@8.4.38) postcss-selector-parser: 6.0.13 resolve: 1.22.8 @@ -13272,14 +13508,14 @@ snapshots: v8-compile-cache-lib: 3.0.1 yn: 3.1.1 - ts-node@10.9.2(@types/node@20.12.7)(typescript@5.4.5): + ts-node@10.9.2(@types/node@20.14.2)(typescript@5.4.5): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.9 '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 - '@types/node': 20.12.7 + '@types/node': 20.14.2 acorn: 8.11.3 acorn-walk: 8.3.2 arg: 4.1.3 @@ -13367,12 +13603,16 @@ snapshots: type-fest@0.20.2: {} + type-fest@0.21.3: {} + type-fest@0.6.0: {} type-fest@0.7.1: {} type-fest@0.8.1: {} + type-fest@4.20.0: {} + type-is@1.6.18: dependencies: media-typer: 0.3.0 diff --git a/turbo.json b/turbo.json index 1624a8ad..1c9611e8 100644 --- a/turbo.json +++ b/turbo.json @@ -104,8 +104,10 @@ "REDIS_URI", "SECRET_KEY", "SENTRY_DSN_API", + "SWARM_STAMP_CRON_PATTERN", "SKIP_ENV_VALIDATION", "SWARM_STORAGE_ENABLED", + "SWARM_BATCH_ID", "VERCEL_URL", "TS_NODE" ]