Skip to content

feat: adds api background-jobs server setup#1833

Merged
stalniy merged 4 commits intomainfrom
feat/background-jobs
Aug 26, 2025
Merged

feat: adds api background-jobs server setup#1833
stalniy merged 4 commits intomainfrom
feat/background-jobs

Conversation

@stalniy
Copy link
Contributor

@stalniy stalniy commented Aug 22, 2025

Why

ref #1735

Summary by CodeRabbit

  • New Features

    • Added a dedicated background-jobs server and option to run REST + background processes.
    • New CLI commands for maintenance (refill-wallets, top-up-deployments, cleanup tasks, gpu-pricing-bot, stale-anonymous-user cleanup).
  • Refactor

    • Centralized startup/bootstrap with a reusable server starter and init hooks.
    • Job-queue now supports lifecycle checks and per-job processing for improved reliability.
    • Notification templates accept optional vars.
  • Chores

    • Updated Docker ports and local/test configs (notifications -> 3090; API exposes REST + background-jobs).

@stalniy stalniy requested a review from a team as a code owner August 22, 2025 14:49
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 22, 2025

Walkthrough

Split the API into rest and background-jobs runtimes, introduce a startServer bootstrap with APP_INITIALIZER lifecycle, refactor job queue startup/health (setup/ping/dispose, per-job workers), add DB/JobQueue healthchecks and Sequelize startup hooks, update ports/configs, and adjust tests/imports.

Changes

Cohort / File(s) Summary
Env config updates
apps/api/env/.env.functional.test, apps/api/env/.env.local.sample, apps/api/env/.env.unit.test
Update NOTIFICATIONS_API_BASE_URL from http://localhost:3081http://localhost:3090.
Entrypoints & multi-process bootstrap
apps/api/src/server.ts, apps/api/src/rest-app.ts, apps/api/src/background-jobs-app.ts, apps/api/src/index.ts
Add server.ts multi-process orchestrator (INTERFACE rest/background-jobs/all), export bootstrap(port) for rest/background jobs, and remove old index.ts bootstrap.
Server start utility & lifecycle
apps/api/src/core/services/start-server/start-server.ts, apps/api/src/core/services/start-server/start-server.spec.ts, apps/api/src/core/services/index.ts, apps/api/src/core/index.ts
Add startServer to run APP_INITIALIZER/ON_APP_START, start HTTP server, register idempotent shutdown/disposal, include tests and re-exports.
Job queue service & provider wiring
apps/api/src/core/services/job-queue/job-queue.service.ts, apps/api/src/core/services/job-queue/job-queue.service.spec.ts, apps/api/src/app/providers/jobs.provider.ts, apps/api/src/app/index.ts
Rename start()setup(), add ping() and dispose(), enforce single handler per queue, switch to per-job workers (batchSize=1) with per-job events; provider now calls setup() then registerHandlers(...).
Job-queue healthcheck & healthz
apps/api/src/core/providers/job-queue-healthcheck.ts, apps/api/src/core/providers/index.ts, apps/api/src/healthz/services/healthz/healthz.service.ts, apps/api/src/healthz/services/healthz/healthz.service.spec.ts, apps/api/src/healthz/routes/healthz.router.ts, apps/api/src/healthz/controllers/healthz/healthz.controller.spec.ts
Add JOB_QUEUE_HEALTHCHECK provider; HealthzService now injects DB and job-queue healthchecks, caches pings, and returns combined { postgres, jobQueue }; schema and tests updated.
Postgres provider & DB healthcheck
apps/api/src/core/providers/postgres.provider.ts
Add DB_HEALTHCHECK token/type that pings DB, register APP_INITIALIZER dispose hook for closing connections, and make closeConnections resolve void.
Sequelize DB connection & lifecycle
apps/api/src/db/dbConnection.ts
Add connectUsingSequelize(logger?) to authenticate and sync DBs; register as APP_INITIALIZER ON_APP_START; adjust closeConnections to resolve void.
Health-check integration & tests
apps/api/src/healthz/services/healthz/healthz.service.ts, apps/api/src/healthz/services/healthz/healthz.service.spec.ts
Replace direct DB usage with injected DbHealthcheck and JobQueueHealthcheck, add TTL-cached Healthcheck wrapper, update public HealthzResult type and tests.
Notifications typing/behavior
apps/api/src/notifications/services/notification-handler/notification.handler.ts
Call templates with payload.vars directly; change TemplateVarsParameter so 0/1-param templates map to { vars?: undefined }, affecting NotificationJob data typing.
Console & CLI
apps/api/src/console.ts
Reorder imports, replace jobs provider import with ./app, remove drain-job-queues command and daemon helper, add new CLI commands wired to controllers.
Shutdown logging
apps/api/src/core/services/shutdown-server/shutdown-server.ts, apps/api/src/core/services/shutdown-server/shutdown-server.spec.ts
Rename on-shutdown error event key CONTAINER_DISPOSE_ERRORON_SHUTDOWN_ERROR; tests updated.
Functional tests import updates
apps/api/test/functional/*
Many functional tests now import app and initDb/connectUsingSequelize from @src/rest-app and @src/db/dbConnection; setups updated to use connectUsingSequelize/closeConnections.
Build & tooling
apps/api/webpack.dev.js, apps/api/webpack.prod.js, apps/api/tsconfig.json
Webpack entries include server.ts, rest-app.ts, background-jobs-app.ts; add resolve.extensionAlias; TypeScript enables allowImportingTsExtensions and rewriteRelativeImportExtensions.
Docker compose ports
packages/docker/docker-compose.prod.yml
Add api background jobs host mapping 3081:3001; change notifications host mapping 3081:30003090:3000.
Core re-exports & config
apps/api/src/core/index.ts, apps/api/src/core/config/env.config.ts
Add side-effect import of ./providers; re-export app-initializer, start-server, and job-queue service; add POSTGRES_BACKGROUND_JOBS_SCHEMA env var defaulting to "pgboss".

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor OS as Process/OS
  participant S as server.ts
  participant Child as Child Process
  participant REST as rest-app.ts
  participant BG as background-jobs-app.ts

  OS->>S: start (PORT, INTERFACE)
  alt INTERFACE = all
    S->>Child: fork (INTERFACE=rest, PORT)
    S->>Child: fork (INTERFACE=background-jobs, PORT+1)
    Child-->>S: send "ready"
  else INTERFACE = rest
    S->>REST: import & bootstrap(PORT)
    REST-->>S: process.send "ready"
  else INTERFACE = background-jobs
    S->>BG: import & bootstrap(PORT)
    BG-->>S: process.send "ready"
  end
  OS->>S: SIGINT/SIGTERM
  S->>Child: forward signal
Loading
sequenceDiagram
  autonumber
  participant App as Hono App
  participant SS as startServer
  participant DI as DI Container
  participant Init as APP_INITIALIZER
  participant SVR as HTTP Server
  participant Proc as process events

  SS->>App: beforeStart?()
  SS->>DI: resolveAll(APP_INITIALIZER)
  loop initializers
    SS->>Init: ON_APP_START()
  end
  SS->>SVR: serve({fetch, port})
  Proc->>SS: on SIGINT/SIGTERM -> shutdown()
  SVR-->>SS: 'close' -> dispose container
Loading
sequenceDiagram
  autonumber
  participant Provider as jobs.provider
  participant JQS as JobQueueService
  participant H1 as Handler A
  participant H2 as Handler B

  Provider->>JQS: setup()
  Provider->>JQS: registerHandlers([H1, H2])
  par per-queue workers
    JQS->>JQS: work(queue, batchSize=1, handler)
    JQS-->>JQS: log JOB_STARTED(jobId)
    alt success
      JQS-->>JQS: log JOB_DONE(jobId)
    else failure
      JQS-->>JQS: log JOB_FAILED(jobId)
    end
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • baktun14

Poem

"I hopped from branch to build with cheer,
Split processes so each job sings clear.
DB pings, queues hum, ports shuffle right,
Bootstrap wakes both day and night.
A rabbit cheers — deploy's in sight! 🐇"

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/background-jobs

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@codecov
Copy link

codecov bot commented Aug 22, 2025

❌ 1 Tests Failed:

Tests completed Failed Passed Skipped
1211 1 1210 0
View the top 1 failed test(s) by shortest run time
Deployment Settings PATCH /v1/deployment-settings/{userId}/{dseq} should return 404 when updating other user's deployment settings
Stack Traces | 0.088s run time
ForbiddenError: Cannot execute "create" on "UserWallet"
    at Function.this (.../console/console/node_modules/@.../ability/src/ForbiddenError.ts:38:7)
    at DrizzleAbility.from [as throwUnlessCanExecute] (.../lib/drizzle-ability/drizzle-ability.ts:41:20)
    at UserWalletRepository.throwUnlessCanExecute [as getOrCreate] (.../repositories/user-wallet/user-wallet.repository.ts:53:19)
    at WalletInitializerService.initializeAndGrantTrialLimits (.../services/wallet-initializer/wallet-initializer.service.ts:22:31)
    at .../test/services/wallet-testing.service.ts:39:15
    at ExecutionContextService.runWithContext (.../services/execution-context/execution-context.service.ts:37:12)

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

@stalniy stalniy force-pushed the feat/background-jobs branch from e55fcf7 to 812c2f0 Compare August 22, 2025 15:04
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (9)
apps/api/test/functional/provider-deployments.spec.ts (1)

135-139: Replace as any with a typed response to satisfy repo TS rules

Repo guidelines prohibit any. Define a response type and use it in both places.

@@
-import { app, initDb } from "@src/rest-app";
+import { app, initDb } from "@src/rest-app";
+
+type ProviderDeploymentsResponse = {
+  total: number;
+  deployments: Deployment[];
+};
@@
-      const data = (await response.json()) as any;
+      const data = (await response.json()) as ProviderDeploymentsResponse;
@@
-      const data = (await response.json()) as any;
+      const data = (await response.json()) as ProviderDeploymentsResponse;

Also applies to: 155-160

apps/api/test/functional/provider-earnings.spec.ts (1)

70-72: Avoid as any in error-path assertion

Use a minimal error shape instead of any to keep tests type-safe.

@@
-      const data = (await res.json()) as any;
+      type ErrorResponse = { message: string };
+      const data = (await res.json()) as ErrorResponse;
apps/api/test/functional/leases-duration.spec.ts (1)

134-140: Replace any with a proper typed response.

Repo guidelines forbid any. Define a response type and use it for JSON parsing.

-    const data = (await response.json()) as any;
+    const data = (await response.json()) as LeasesDurationResponse;

Add this type near the imports to document the contract:

type LeasesDurationResponse = {
  leaseCount: number;
  totalDurationInSeconds: number;
  totalDurationInHours: number;
  leases: Array<{ dseq: string }>;
};
apps/api/test/functional/anonymous-user.spec.ts (2)

13-16: Avoid any when parsing the create-user response.

Use the existing AnonymousUserResponseOutput type for JSON instead of any.

-    const body = (await userResponse.json()) as any;
+    const body = (await userResponse.json()) as AnonymousUserResponseOutput;

55-56: Avoid any when parsing the "different user" response.

Replace any with the response type.

-      const { token: differentUserToken } = (await differentUserResponse.json()) as any;
+      const { token: differentUserToken } = (await differentUserResponse.json()) as AnonymousUserResponseOutput;
apps/api/test/functional/providers.spec.ts (1)

60-65: Await the instance updates to prevent race conditions.

Model.update (instance) returns a Promise; without await, subsequent requests may read stale state.

Apply this diff:

-    providers[0].update({
+    await providers[0].update({
       lastSuccessfulSnapshotId: providerSnapshots[0].id
     });
-    providers[1].update({
+    await providers[1].update({
       lastSuccessfulSnapshotId: providerSnapshots[2].id
     });
apps/api/test/functional/provider-dashboard.spec.ts (1)

39-40: Remove as any casts and type the response shape.
Per guidelines, avoid any. Define a local response type and cast explicitly.

Apply this diff in-place to the two casts:

-      const data = (await response.json()) as any;
+      const data = (await response.json()) as ProviderDashboardResponse;
@@
-      const data = (await response.json()) as any;
+      const data = (await response.json()) as ProviderDashboardResponse;

Add this supporting type near the top of the file (outside the selected range):

type ProviderDashboardResponse = {
  current: { date: string; height: number };
  previous: { date: string; height: number };
};

Also applies to: 53-54

apps/api/src/healthz/controllers/healthz/healthz.controller.spec.ts (1)

51-56: Drop explicit return type from setup() to match test conventions.
Per repo guidelines, don’t specify the return type of setup in specs.

-  function setup(): {
-    controller: HealthzController;
-    service: MockProxy<HealthzService>;
-  } {
+  function setup() {
apps/api/src/core/services/job-queue/job-queue.service.spec.ts (1)

116-120: Replace any types in pgBoss.work mock to comply with “no any” guideline.

Use precise types to avoid any in TS tests.

- jest.spyOn(pgBoss, "work").mockImplementation(async (queueName: string, options: any, processFn: (jobs: any[]) => Promise<void>) => {
-   await processFn(jobs);
+ jest.spyOn(pgBoss, "work").mockImplementation(async (_queueName: string, _options: Parameters<PgBoss["work"]>[1], processFn: (jobs: EnqueuedJob[]) => Promise<void>) => {
+   await processFn(jobs as EnqueuedJob[]);
    return "work-id";
 });

Add once per file (near the bottom) to support the type:

type EnqueuedJob = { id: string; data: TestJob["data"] };
🧹 Nitpick comments (71)
apps/api/src/notifications/services/notification-handler/notification.handler.ts (3)

48-49: Type erasure via as GenericNotificationTemplate hides template-specific var types

Casting each template to GenericNotificationTemplate discards the concrete vars type and lets mismatches slip through compile-time. Prefer localizing the cast behind a small invoker helper (overloaded) to limit the unsafe surface and document intent.

Example helper (keeps casts contained; avoids any):

function invokeTemplate(
  t: (user: UserOutput) => CreateNotificationInput,
  user: UserOutput
): CreateNotificationInput;
function invokeTemplate<V extends Record<string, unknown>>(
  t: (user: UserOutput, vars: V) => CreateNotificationInput,
  user: UserOutput,
  vars: V
): CreateNotificationInput;
function invokeTemplate(
  t: (user: UserOutput, vars?: Record<string, unknown>) => CreateNotificationInput,
  user: UserOutput,
  vars?: Record<string, unknown>
): CreateNotificationInput {
  return t.length <= 1 ? t(user) : t(user, vars ?? {});
}

Then in handle:

const t = notificationTemplates[payload.template];
await this.notificationService.createNotification(
  // TS will pick the correct overload; fallback `{}` remains enforced
  // @ts-expect-error allow union of function types via overload resolution
  invokeTemplate(t as any, user, (payload as any).vars) // if you keep the cast, keep it isolated here
);

If you’d prefer no any at all, keep the existing cast but still call through invokeTemplate so defaulting and arity handling are centralized.


68-72: Typo in event name: “MISTMATCH” → “MISMATCH”

Minor, but it affects alerting/queries consistency.

-        event: "USER_CONDITIONS_MISTMATCH",
+        event: "USER_CONDITIONS_MISMATCH",

37-47: Add tests for both arities (with/without vars) and conditions path

Given the behavior change around vars, add targeted tests to prevent regressions:

  • Template with 2 params: when job omits vars, handler should not throw and should call service with {}.
  • Template with 1 param: handler should ignore vars and still create a notification.
  • Conditions mismatch: ensure early return with the corrected event name.

I can draft unit tests for NotificationHandler.handle using stub templates (1‑arg and 2‑arg) and a spy on NotificationService.createNotification. Want me to open a follow‑up PR or push to this branch?

Also applies to: 57-66, 76-77

apps/api/tsconfig.json (1)

8-10: TS ≥5.3 Verified; No Breaking Imports Detected

  • All workspace packages specify TypeScript ~5.8.2, satisfying the ≥ 5.3 requirement.
  • No .ts-extension imports in apps/api—the new allowImportingTsExtensions and rewriteRelativeImportExtensions flags won’t affect existing code.
  • apps/api and its test/tsconfig.json both omit moduleResolution, so editors and secondary tools default to CommonJS resolution despite "module": "NodeNext".

Optional refactor for consistency across IDEs and tooling:

--- a/apps/api/tsconfig.json
+++ b/apps/api/tsconfig.json
@@ compilerOptions
-   "module": "NodeNext",
+   "module": "NodeNext",
+   "moduleResolution": "NodeNext",
    "allowImportingTsExtensions": true,
    "rewriteRelativeImportExtensions": true
packages/docker/docker-compose.prod.yml (1)

10-10: Background-jobs port mapping & healthcheck setup confirmed

  • The background-jobs server binds to container port 3001 via the backgroundJobsPort = port + 1 logic in apps/api/src/server.ts (line 17), and startServer in apps/api/src/background-jobs-app.ts directly uses that { port } argument.
  • Health endpoints are already exposed by healthzRouter in both the REST and background-jobs apps under /v1/healthz/readiness and /v1/healthz/liveness.

Optional refactor—add a Docker Compose healthcheck so the api service is marked healthy only when both the main API and background-jobs servers are ready:

 services:
   api:
     ports:
       - '3080:3000'
       - '3081:3001' # background jobs server
+    healthcheck:
+      test: ["CMD-SHELL", "curl -fsS http://localhost:3000/v1/healthz/readiness && curl -fsS http://localhost:3001/v1/healthz/readiness"]
+      interval: 10s
+      timeout: 3s
+      retries: 5
apps/api/test/functional/provider-earnings.spec.ts (1)

27-33: Nit: stabilize timestamps in fixtures

Multiple calls to subDays(Date.now(), …) can drift by milliseconds. Prefer a single base time for determinism.

-      createAkashBlock({
-        datetime: subDays(Date.now(), 1).toISOString(),
+      const now = new Date();
+      createAkashBlock({
+        datetime: subDays(now, 1).toISOString(),
@@
-      createAkashBlock({
-        datetime: subDays(Date.now(), 2).toISOString(),
+      createAkashBlock({
+        datetime: subDays(now, 2).toISOString(),
apps/api/test/functional/blocks.spec.ts (1)

15-35: Refactor setup to avoid shared state and meet test guidelines

Current setup depends on outer isDbInitialized and testData, violating the “no shared state in setup” rule; it’s also placed above the root describe body’s end, whereas guidelines require it at the bottom. Consider making setup self-contained, parameterized, and moving it to the bottom of the root describe.

-  const testData: {
-    blocks?: AkashBlock[];
-  } = {};
-  let isDbInitialized = false;
-
-  const setup = async () => {
-    if (isDbInitialized) {
-      return testData;
-    }
-
-    const validator = await createValidator();
-    const baseTime = new Date();
-    testData.blocks = await Promise.all(
-      Array.from({ length: 101 }, (_, i) => {
-        return createAkashBlock({
-          height: i + 1,
-          proposer: validator.hexAddress,
-          datetime: subSeconds(baseTime, (101 - i) * blockFrequency)
-        });
-      })
-    );
-    isDbInitialized = true;
-    return testData;
-  };
+  // (move to bottom of root describe)

And at the bottom of the root describe("Blocks", …):

+  // Setup must be at the bottom of the root describe and avoid shared state
+  const setup = async (_: { seed?: boolean } = { seed: true }) => {
+    const validator = await createValidator();
+    const baseTime = new Date();
+    const blocks = await Promise.all(
+      Array.from({ length: 101 }, (_, i) =>
+        createAkashBlock({
+          height: i + 1,
+          proposer: validator.hexAddress,
+          datetime: subSeconds(baseTime, (101 - i) * blockFrequency)
+        })
+      )
+    );
+    return { blocks };
+  };

Note: If duplicate inserts are a concern due to unique constraints on height, switch to an idempotent seeding strategy (e.g., wrap createAkashBlock with an upsert-by-height).

apps/api/test/functional/certificate.spec.ts (1)

75-76: Fix suite description to match the actual endpoint.

The suite title says "POST /v1/certificate/create" while the test hits "/v1/certificates". Align to reduce confusion in reports.

-describe("POST /v1/certificate/create", () => {
+describe("POST /v1/certificates", () => {
apps/api/test/functional/app.spec.ts (1)

20-21: Tighten version regex to avoid matching arbitrary characters.

The dot (.) matches any character; escape it to match literal dots.

-        version: expect.stringMatching(/^[0-9]+.[0-9]+.[0-9]+(-beta\.[0-9]+)?$/)
+        version: expect.stringMatching(/^[0-9]+\.[0-9]+\.[0-9]+(-beta\.[0-9]+)?$/)
apps/api/test/functional/anonymous-user.spec.ts (1)

8-16: Consider moving setup to a setup() helper per repo test guidelines.

Your beforeEach that creates a user can be converted into a local setup() returning { user, token }, called explicitly in each test. This improves isolation and follows the stated testing convention. Low priority for functional tests if you prefer the current style.

apps/api/test/functional/wallets-refill.spec.ts (2)

22-71: Avoid mutating global BillingConfig without restoring it (test bleed risk).

Mutating DI-provided config in-place can leak into other tests executed in the same Jest worker. Wrap the mutation in try/finally and restore the original value.

Apply this diff to scope and restore the change:

 it("should refill wallets low on fee allowance", async () => {
-  config.FEE_ALLOWANCE_REFILL_THRESHOLD = 2;
+  const originalThreshold = config.FEE_ALLOWANCE_REFILL_THRESHOLD;
+  config.FEE_ALLOWANCE_REFILL_THRESHOLD = 2;
+  try {
     const NUMBER_OF_WALLETS = 5;
     const prepareRecords = Array.from({ length: NUMBER_OF_WALLETS }).map(async (_, index) => {
       const records = await walletService.createUserAndWallet();
       const { user, token } = records;
       const { wallet } = records;
       let walletRecord = await userWalletRepository.findById(wallet.id);
@@
       return { user, token, wallet };
     });
 
     const records = await Promise.all(prepareRecords);
     await walletController.refillWallets();
     const trialingWallet = records.pop()!;
 
     await Promise.all([
       ...records.map(async ({ wallet }) => {
         const walletRecord = await userWalletRepository.findById(wallet.id);
 
         expect(walletRecord?.feeAllowance).toBe(config.FEE_ALLOWANCE_REFILL_AMOUNT);
       }),
       userWalletRepository.findById(trialingWallet.wallet.id).then(walletRecord => {
         expect(walletRecord?.feeAllowance).toBe(config.FEE_ALLOWANCE_REFILL_THRESHOLD);
       })
     ]);
-  });
+  } finally {
+    config.FEE_ALLOWANCE_REFILL_THRESHOLD = originalThreshold;
+  }
+});

50-52: Assert against the updated DB record, not the stale in-memory wallet.

wallet.isTrialing doesn’t reflect the updateById mutation; assert walletRecord.isTrialing against the intended boolean.

Apply this diff:

-        expect(walletRecord.feeAllowance).toBe(config.FEE_ALLOWANCE_REFILL_THRESHOLD);
-        expect(wallet.isTrialing).toBe(true);
+        expect(walletRecord.feeAllowance).toBe(config.FEE_ALLOWANCE_REFILL_THRESHOLD);
+        expect(walletRecord.isTrialing).toBe(index === NUMBER_OF_WALLETS - 1);
apps/api/test/functional/sign-and-broadcast-tx.spec.ts (1)

47-48: Replace as any with a minimal response type.

Comply with the guideline to avoid any in TS tests.

Apply this diff:

-      const { token } = (await differentUserResponse.json()) as any;
+      const { token } = (await differentUserResponse.json()) as { token: string };
apps/api/test/functional/docs.spec.ts (1)

5-16: Ensure timers are restored even on failure.

Wrap the assertions in try/finally so useRealTimers always executes.

Apply this diff:

-    it(`returns docs with all routes expected`, async () => {
-      jest.useFakeTimers();
-      jest.setSystemTime(new Date("2025-07-03T12:00:00.000Z"));
-
-      const response = await app.request(`/v1/doc`);
-
-      expect(response.status).toBe(200);
-      const data = await response.json();
-      expect(data).toMatchSnapshot();
-
-      jest.useRealTimers();
-    });
+    it(`returns docs with all routes expected`, async () => {
+      jest.useFakeTimers();
+      jest.setSystemTime(new Date("2025-07-03T12:00:00.000Z"));
+      try {
+        const response = await app.request(`/v1/doc`);
+        expect(response.status).toBe(200);
+        const data = await response.json();
+        expect(data).toMatchSnapshot();
+      } finally {
+        jest.useRealTimers();
+      }
+    });
apps/api/test/functional/providers.spec.ts (3)

101-105: Avoid as any in test parsing. Type the response.

Use the domain type already imported.

Apply this diff:

-      const data = (await response.json()) as any;
+      const data = (await response.json()) as Provider[];

110-114: Repeat: replace as any with Provider[].

Same reasoning as above.

Apply this diff:

-      const data = (await response.json()) as any;
+      const data = (await response.json()) as Provider[];

130-134: Replace as any with concrete type.

Tighten types to comply with guidelines.

Apply this diff:

-      const data = (await response.json()) as any;
+      const data = (await response.json()) as Provider;
apps/api/test/functional/stripe-webhook.spec.ts (2)

34-47: Make STRIPE_WEBHOOK_SECRET deterministic in tests.

The test fails if the env var isn’t set in CI. Set and restore it within this suite to avoid external coupling.

Apply this diff (inside the describe("Stripe webhook", ...) block, near the top):

 describe("Stripe webhook", () => {
+  const originalStripeWebhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
+  beforeAll(() => {
+    process.env.STRIPE_WEBHOOK_SECRET ??= "whsec_test";
+  });
+  afterAll(() => {
+    if (originalStripeWebhookSecret === undefined) {
+      delete process.env.STRIPE_WEBHOOK_SECRET;
+    } else {
+      process.env.STRIPE_WEBHOOK_SECRET = originalStripeWebhookSecret;
+    }
+  });

This keeps signature generation aligned with the server’s verification logic.


66-82: Optionally assert nock expectations were consumed.

Adds a stronger guarantee that the webhook handler performed the expected Stripe lookup.

Example (at the end of the test body):

expect(nock.isDone()).toBe(true);
apps/api/test/functional/create-deployment.spec.ts (1)

94-98: Avoid live network IO in tests — use the existing api base or stub with nock

The test depends on an external endpoint, which can flake CI or fail offline. Prefer keeping all HTTP under nock control.

Apply this diff to use the existing API base and keep calls mockable by nock:

-    const response = await axios.get(`https://api.sandbox-01.aksh.pw/blocks/latest`);
+    // Keep this under nock's api base to avoid external IO in tests
+    const response = await axios.get(`${apiNodeUrl}/blocks/latest`);

If ${apiNodeUrl}/blocks/latest isn’t the correct path for block height, either:

  • add a dedicated height provider behind the same base, or
  • stub axios: nock(new URL(apiNodeUrl).origin).get('/blocks/latest').reply(200, { block: { header: { height: 123 }}})
apps/api/src/app/index.ts (1)

1-2: Ensure DB init runs before job-queue setup (import order may matter)

If APP_INITIALIZER execution order follows registration/import order, initialize DB before jobs to avoid transient startup failures.

Apply this diff to make the dependency explicit:

-import "./providers/jobs.provider";
-import "./providers/db-init.provider";
+import "./providers/db-init.provider";
+import "./providers/jobs.provider";

If the DI container guarantees initializer ordering independently of import order, ignore the swap. Otherwise, add a small comment in jobs.provider asserting DB dependency or a runtime check in jobs setup.

apps/api/test/functional/templates.spec.ts (2)

14-15: Avoid leaking global env mutations across specs

The test overrides env.GITHUB_PAT but never restores it. Capture and restore to prevent cross-test coupling.

Apply this diff:

-env.GITHUB_PAT = "ghp_1234567890";
+const previousPat = env.GITHUB_PAT;
+env.GITHUB_PAT = "ghp_1234567890";
@@
 describe("Templates API", () => {
-  afterAll(() => {
+  afterAll(() => {
     jest.restoreAllMocks();
+    // restore env to avoid side-effects on other specs
+    env.GITHUB_PAT = previousPat;
   });

Also applies to: 39-42


17-37: Prefer DI-friendly mocking over jest.mock for external clients

Project guidelines discourage jest.mock() in specs. Long-term, consider injecting an Octokit client into the app/module and using jest-mock-extended to provide a mock in tests. For this functional spec, keeping the module mock is understandable; treat this as a backlog item.

apps/api/test/functional/transactions.spec.ts (1)

25-25: Avoid any casts; use concrete types already imported

Casting responses to any violates the typing guideline and loses safety. You already import Transaction; use it for the parsed payloads.

Apply this diff:

-      const transactionsFound = (await response.json()) as any;
+      const transactionsFound = (await response.json()) as Transaction[];
@@
-      const transactionFound = (await response.json()) as any;
+      const transactionFound = (await response.json()) as Transaction;

Also applies to: 49-49

apps/api/test/functional/balances.spec.ts (1)

47-48: Avoid as any in tests; use typed response aliases

Per repo guidelines, don’t use any. Define minimal response types for assertions and assert to those instead of any.

Example:

type BalancesResponse = {
  data: { balance: number; deployments: number; total: number };
};

// ...
const result = (await response.json()) as BalancesResponse;

Also applies to: 76-77

apps/api/test/functional/provider-attributes-schema.spec.ts (1)

8-9: Replace as any with a concrete schema type

Keep types explicit in spec files to follow the “no any” rule.

Example:

type ProviderAttribute = {
  key: string;
  type: string;
  required: boolean;
  description: string;
  values?: unknown[];
};
type ProviderAttributesSchemaResponse = Record<string, ProviderAttribute>;

const data = (await response.json()) as ProviderAttributesSchemaResponse;
apps/api/test/functional/bids.spec.ts (2)

31-60: Refactor from beforeEach to a test-local setup() function

Guidelines recommend a setup function at the bottom of the root describe instead of beforeEach, with no shared state. Move knownUsers/knownApiKeys/knownWallets inside setup() and return helpers like mockUser/mockAdmin. Each test should call await setup() and use the returned context.

Sketch:

// At the bottom of the root describe
function setup({ withAdmin = false }: { withAdmin?: boolean } = {}) {
  const knownUsers: Record<string, UserOutput> = {};
  const knownApiKeys: Record<string, ApiKeyOutput> = {};
  const knownWallets: Record<string, UserWalletOutput[]> = {};

  // create typed mocks via jest-mock-extended if feasible and register via DI
  // provide mockUser, mockAdmin that close over the above maps

  return { knownUsers, knownApiKeys, knownWallets, mockUser, mockAdmin };
}

If the route under test resolves dependencies from the DI container, consider creating typed mocks with jest-mock-extended and registering them via container.registerInstance(...) for stricter typing and isolation.


129-136: Remove as any from parsed JSON

Define a typed response for bids and assert to that type.

Example:

type BidsResponse = { data: Array<{ bid: string; escrow_account: string }> };

const result = (await response.json()) as BidsResponse;
expect(result.data).toEqual([{ bid: "fake-bid", escrow_account: "fake-escrow-account" }]);

Also applies to: 148-155, 168-175

apps/api/test/functional/provider-graph-data.spec.ts (1)

206-206: Replace as any with precise DTO types in tests

Coding guidelines prohibit any in TS. Define lightweight DTO types and use them in assertions.

Apply:

@@
-import { app, initDb } from "@src/rest-app";
+import { app, initDb } from "@src/rest-app";
+
+type ProviderGraphNowCompare = {
+  count: number;
+  cpu: number;
+  gpu: number;
+  memory: number;
+  storage: number;
+};
+type ProviderGraphData = {
+  currentValue: number;
+  compareValue: number;
+  snapshots: { date: string; value: number }[];
+  now: ProviderGraphNowCompare;
+  compare: ProviderGraphNowCompare;
+};
@@
-        const data = (await response.json()) as any;
+        const data = (await response.json()) as ProviderGraphData;
@@
-        const data = (await response.json()) as any;
+        const data = (await response.json()) as ProviderGraphData;

Also applies to: 286-286

apps/api/test/functional/market-data.spec.ts (1)

58-58: Introduce a MarketData DTO in apps/api/test/functional/market-data.spec.ts
To satisfy the “no any” policy, define the exact response shape and replace each as any cast with this type.

Suggested changes:

@@ apps/api/test/functional/market-data.spec.ts
-import { app } from "@src/rest-app";
+import { app } from "@src/rest-app";
+
+type MarketData = {
+  price: number;
+  volume: number;
+  marketCap: number;
+  marketCapRank: number;
+  priceChange24h: number;
+  priceChangePercentage24: number;
+};

@@
-      const data = (await response.json()) as any;
+      const data = (await response.json()) as MarketData;

@@
-        const data = (await response.json()) as any;
+        const data = (await response.json()) as MarketData;

• Both occurrences (lines 57 and 90) now use MarketData instead of any.
• A full-text scan (rg -nP '\bas\s+any\b' apps/api/test/functional) shows 30+ remaining as any casts across other functional tests—consider defining minimal DTOs (or a generic parser helper) for each endpoint to eliminate those as well.

apps/api/test/functional/api-key.spec.ts (4)

149-153: Replace “as any” with a precise response type

Avoid any in tests per guidelines. Narrow to the fields you assert.

Apply:

-      const result = (await response.json()) as any;
+      const result = (await response.json()) as { data: Array<{ keyFormat: string }> };

197-207: Type the GET-by-id response instead of casting to any

Keep types tight and local to what’s asserted.

-      const result = (await response.json()) as any;
+      const result = (await response.json()) as {
+        data: {
+          id: string;
+          name: string;
+          expiresAt: string | null;
+          createdAt: string;
+          updatedAt: string;
+          apiKey?: string;
+        };
+      };

244-251: Type the create response instead of using any

This avoids implicit-any flows in expectations.

-      const result = (await response.json()) as any;
+      const result = (await response.json()) as {
+        data: { id: string; name: string; expiresAt: string; apiKey: string };
+      };

362-369: Type the PATCH response instead of using any

Keep the type only to what you assert.

-      const result = (await response.json()) as any;
+      const result = (await response.json()) as { data: { id: string; name: string } };
apps/api/test/functional/provider-regions.spec.ts (1)

5-5: LGTM on switching to rest-app; mirror teardown like other suites

Import and usage look consistent. Consider closing DB connections here like in dashboard-data.spec.ts to avoid pooled connection leaks when running tests in parallel.

-import { app, initDb } from "@src/rest-app";
+import { app, initDb } from "@src/rest-app";
+import { closeConnections } from "@src/core";
@@
 describe("ProviderRegions", () => {
@@
   beforeAll(async () => {
     await initDb();
@@
   });
+
+  afterAll(async () => {
+    await closeConnections();
+  });

If a global test harness already disposes connections, feel free to skip this.

apps/api/src/app/providers/db-init.provider.ts (1)

11-13: Pass a DB-context logger or rely on the default to keep log context

connectUsingSequelize already defaults to LoggerService.forContext("DB"). Passing a generic container-resolved logger may lose this DB context. Prefer relying on the default or passing the DB-context explicitly.

-        await connectUsingSequelize(c.resolve(LoggerService));
+        // use default DB-context logger
+        await connectUsingSequelize();
+        // or explicitly:
+        // await connectUsingSequelize(LoggerService.forContext("DB"));
apps/api/test/functional/dashboard-data.spec.ts (1)

5-5: Import switch to rest-app is consistent; keep types tight in assertions

The entrypoint change is good. One small clean-up: avoid as any on the parsed JSON.

-      const data = (await response.json()) as any;
+      type BlockStats = {
+        height: number;
+        date: string;
+        activeLeaseCount?: number | null;
+        totalLeaseCount?: number | null;
+        activeCPU?: number | null;
+        activeGPU?: number | null;
+        activeMemory?: number | null;
+        activeEphemeralStorage?: number | null;
+        activePersistentStorage?: number | null;
+        totalUAktSpent?: number | null;
+        totalUUsdSpent?: number | null;
+        totalUUsdcSpent?: number | null;
+        dailyLeaseCount?: number | null;
+        dailyUAktSpent?: number | null;
+        dailyUUsdSpent?: number | null;
+        dailyUUsdcSpent?: number | null;
+      };
+      const data = (await response.json()) as { now: BlockStats; compare: BlockStats };
apps/api/test/functional/provider-dashboard.spec.ts (2)

18-31: Use Date objects in seed data to match model typing and avoid runtime mismatches.
Factories/seeders often persist and return Date instances; passing ISO strings can cause toISOString() calls to fail at runtime if the seeder does not re-hydrate to Date. Prefer passing Date directly.

-      createAkashBlock({
-        datetime: new Date().toISOString(),
+      createAkashBlock({
+        datetime: new Date(),
         isProcessed: true,
         totalUUsdSpent: 1000,
         height: 300
       }),
-      createAkashBlock({
-        datetime: subDays(Date.now(), 1).toISOString(),
+      createAkashBlock({
+        datetime: subDays(Date.now(), 1),
         height: 200
       }),
-      createAkashBlock({
-        datetime: subDays(Date.now(), 2).toISOString(),
+      createAkashBlock({
+        datetime: subDays(Date.now(), 2),
         height: 100
       })

If createAkashBlock expects strings, keep current inputs but ensure it returns Block with datetime: Date. Otherwise, adopt the diff above.


9-33: Optional: adopt a local setup() to align with the repo’s test pattern.
Current use of beforeAll is fine functionally, but tests in this repo generally favor a setup() at the bottom of the root describe. Consider migrating to keep consistency.

apps/api/test/functional/user-init.spec.ts (2)

15-19: Avoid jest.mock() module mocking in specs; prefer DI + jest-mock-extended.
Repo guideline: don’t use jest.mock() in test files. For functional tests, consider exposing an injectable token for user context so the test can register a stub rather than mocking the module.

High-level direction:

  • Introduce a DI token (e.g., USER_CONTEXT) wrapping getCurrentUserId.
  • In production, register the real provider; in tests, register a mock via jest-mock-extended.

Example sketch (not a complete diff):

// production module (e.g., user-context.provider.ts)
export interface UserContext { getCurrentUserId(): string | undefined }
export const USER_CONTEXT = Symbol('USER_CONTEXT');

// wiring (app startup)
container.register<UserContext>(USER_CONTEXT, { useValue: { getCurrentUserId } });

// usage (middlewares)
const { getCurrentUserId } = container.resolve<UserContext>(USER_CONTEXT);
// test
import { mock } from 'jest-mock-extended';
const userCtx = mock<UserContext>();
userCtx.getCurrentUserId.mockReturnValue(auth0Payload.userId);
container.register(USER_CONTEXT, { useValue: userCtx });

I can help draft the provider and swap the middleware to consume it—want me to open a follow-up patch?


89-127: Conform setup() to test conventions (param signature and placement).

  • The setup function should accept a single parameter with an inline type and be placed at the bottom of the root describe (it’s currently nested).
  • Also useful to allow payload overrides.

Minimal delta to the signature and body:

-    function setup() {
+    function setup(options: {
+      auth0PayloadOverride?: Partial<{
+        userId: string;
+        wantedUsername: string;
+        email: string;
+        emailVerified: boolean;
+        subscribedToNewsletter: boolean;
+      }>;
+    } = {}) {
       const auth0Payload = {
         userId: faker.string.alphanumeric(10),
         wantedUsername: faker.internet.userName(),
         email: faker.internet.email(),
         emailVerified: true,
         subscribedToNewsletter: false
       };
+      Object.assign(auth0Payload, options.auth0PayloadOverride);

And move this setup to the bottom of the root describe("User Init", ...) block.

apps/api/src/core/providers/app-initializer.ts (1)

10-10: Good addition: APP_SHUTDOWN token for graceful teardown.
This enables clean disposal of shared resources (DB, queues, etc.). Consider introducing a named type for reuse across providers.

Apply this small refinement:

+export interface AppShutdown {
+  dispose: () => void | Promise<void>;
+}
-export const APP_SHUTDOWN: InjectionToken<{ dispose: () => void | Promise<void> }> = Symbol("APP_SHUTDOWN");
+export const APP_SHUTDOWN: InjectionToken<AppShutdown> = Symbol("APP_SHUTDOWN");
apps/api/src/app/providers/jobs.provider.ts (1)

9-21: Consider registering a shutdown hook to drain/close the job queue.
With APP_SHUTDOWN now available, it’s a good place to ensure workers stop cleanly and transports are closed.

Sketch (if JobQueueService exposes dispose() or close()):

import { APP_SHUTDOWN } from "@src/core/providers/app-initializer";

container.register(APP_SHUTDOWN, {
  useValue: {
    async dispose() {
      const jobQueueManager = container.resolve(JobQueueService);
      await jobQueueManager.dispose?.(); // or close()/stop()
    }
  }
});

If JobQueueService doesn’t yet have a shutdown method, I can add one.

apps/api/src/healthz/controllers/healthz/healthz.controller.spec.ts (1)

12-14: Rename tests to reflect both checks (postgres + jobQueue).
Titles still say “postgres is ready” but now the payload includes jobQueue. Consider making the names precise, e.g., “should return ok if dependencies are healthy”.

Also applies to: 33-35

apps/api/src/background-jobs-app.ts (1)

11-17: Consider initializing prerequisites in beforeStart for atomic boot or via an app initializer.

If workers depend on DB migrations or other prerequisites, prefer moving the worker startup (or its prerequisites) into beforeStart (or an APP_INITIALIZER) so failure aborts the boot atomically and shutdown handlers are registered by startServer.

Example:

-  await startServer(app, LoggerService.forContext("BACKGROUND_JOBS"), process, { port });
-  await container.resolve(JobQueueService).startWorkers();
+  await startServer(app, LoggerService.forContext("BACKGROUND_JOBS"), process, {
+    port,
+    beforeStart: async () => {
+      // e.g. await migratePG(); // if required here as well
+      await container.resolve(JobQueueService).startWorkers();
+    }
+  });
apps/api/src/server.ts (3)

5-5: Add top-level error handling to avoid unhandled rejections on boot failure.

If any boot task throws (e.g., worker startup moved to beforeStart), process will crash with an unhandled rejection. Make it explicit and exit non-zero.

-bootstrap();
+void bootstrap().catch(error => {
+  // Keep this minimal; logging provider may not be ready yet.
+  // eslint-disable-next-line no-console
+  console.error("FATAL_BOOT_ERROR", error);
+  process.exit(1);
+});

9-9: Typo: boostrapListbootstrapList.

Minor readability fix.

-  const boostrapList: Promise<void>[] = [];
+  const bootstrapList: Promise<void>[] = [];

And update subsequent references:

-    boostrapList.push(import("./rest-app.ts").then(m => m.bootstrap(port)));
+    bootstrapList.push(import("./rest-app.ts").then(m => m.bootstrap(port)));
-    boostrapList.push(import("./background-jobs-app.ts").then(m => m.bootstrap(backgroundJobsPort)));
+    bootstrapList.push(import("./background-jobs-app.ts").then(m => m.bootstrap(backgroundJobsPort)));
-  await Promise.all(boostrapList);
+  await Promise.all(bootstrapList);

16-19: Allow an explicit BACKGROUND_JOBS_PORT override (fallback to port + 1).

Helpful in containerized envs where port+1 may already be allocated.

-  if (INTERFACE === "background-jobs" || INTERFACE === "all") {
-    const backgroundJobsPort = INTERFACE === "all" ? port + 1 : port;
+  if (INTERFACE === "background-jobs" || INTERFACE === "all") {
+    const { BACKGROUND_JOBS_PORT } = process.env;
+    const backgroundJobsPort =
+      BACKGROUND_JOBS_PORT ? parseInt(BACKGROUND_JOBS_PORT, 10) : (INTERFACE === "all" ? port + 1 : port);
     boostrapList.push(import("./background-jobs-app.ts").then(m => m.bootstrap(backgroundJobsPort)));
   }
apps/api/test/functional/addresses.spec.ts (2)

214-216: Avoid reconnecting DB on every test run; connect once for speed

Calling connectUsingSequelize() inside setup runs per test invocation and can add noticeable overhead. Prefer connecting once in beforeAll (and keeping your afterAll as-is) or memoize the connection in setup to ensure idempotency.

Example refactor outside this hunk:

describe("Addresses API", () => {
  beforeAll(async () => {
    await connectUsingSequelize();
  });

  // ... keep your tests & setup() without the connectUsingSequelize() call
});

214-535: Minor guideline nit: setup() with a single param

Guidelines say setup should accept a single parameter with inline type. It’s fine functionally as-is, but for consistency you can accept a typed options param (even if unused) and return the created fixtures.

Possible tweak outside this hunk:

const setup = async (_opts: { seed?: boolean } = {}) => {
  // existing body
  return { address, validators, transactions };
};
apps/api/src/core/providers/job-queue-healthcheck.ts (1)

9-17: Prefer a unique Symbol token and add explicit factory type

Using a string token can collide. A Symbol guarantees uniqueness, and typing the factory helps keep the contract tight.

Apply this diff:

-export const JOB_QUEUE_HEALTHCHECK: InjectionToken<JobQueueHealthcheck> = "JOB_QUEUE_HEALTHCHECK";
-container.register(JOB_QUEUE_HEALTHCHECK, {
-  useFactory: c => {
+export const JOB_QUEUE_HEALTHCHECK: InjectionToken<JobQueueHealthcheck> = Symbol("JOB_QUEUE_HEALTHCHECK");
+container.register<JobQueueHealthcheck>(JOB_QUEUE_HEALTHCHECK, {
+  useFactory: (c): JobQueueHealthcheck => {
     const jobQueueService = c.resolve(JobQueueService);
     return {
       ping: () => jobQueueService.ping()
     };
   }
 });
apps/api/src/console.ts (1)

23-23: Run APP_SHUTDOWN disposers on shutdown for symmetry with APP_INITIALIZER

executeCliHandler triggers APP_INITIALIZER on start. For symmetry and future-proof cleanup (other providers may register APP_SHUTDOWN), run all APP_SHUTDOWN.dispose hooks during shutdown before disposing the container.

Apply this diff:

-import { APP_INITIALIZER, ON_APP_START } from "./core/providers/app-initializer";
+import { APP_INITIALIZER, ON_APP_START, APP_SHUTDOWN } from "./core/providers/app-initializer";
 const shutdown = once(async () => {
   // eslint-disable-next-line @typescript-eslint/no-var-requires
   const { closeConnections } = require("./core/providers/postgres.provider");
 
-  await Promise.all([closeConnections(), chainDb.close(), container.dispose()]);
+  const shutdownHooks = container.resolveAll(APP_SHUTDOWN).map(h => h.dispose());
+  await Promise.all([closeConnections(), chainDb.close(), ...shutdownHooks]);
+  await container.dispose();
 });

Also applies to: 133-138

apps/api/src/core/providers/postgres.provider.ts (1)

29-37: DbHealthcheck ping should conform to Promise

The useValue currently returns the result of unsafe("SELECT 1"), which resolves to a result set. Make it explicit Promise for type correctness and to match the JobQueueHealthcheck shape.

Apply this diff:

 container.register(DB_HEALTHCHECK, {
   useValue: {
-    ping: () => appClient.unsafe("SELECT 1")
+    ping: () => appClient.unsafe("SELECT 1").then(() => undefined)
   }
 });
apps/api/src/core/services/job-queue/job-queue.service.ts (3)

28-43: Single-handler enforcement per queue is good; consider early throw before side-effects

The duplicate-handler guard is solid. Minor improvement: detect duplicates before calling createQueue to avoid creating a queue and then failing on a later handler.

Potential refactor sketch:

const names = handlers.map(h => h.accepts[JOB_NAME]);
const dup = names.find((n, i) => names.indexOf(n) !== i);
if (dup) throw new Error(`JobQueue does not support multiple handlers for the same queue: ${dup}`);
// then createQueue in a single pass

144-147: ping() implementation is fine; consider using the public API when available

Using getDb().executeSql("SELECT 1") is pragmatic, but it relies on a non-typed API. If PgBoss exposes a public liveness method in future, prefer it. No change required now.


89-91: Doc: clarify that startWorkers blocks

startWorkers awaits the worker promises, so the returned promise won’t resolve until workers stop. Consider documenting that it’s intended for a long-lived background process and will block.

Suggested doc tweak:

/** Starts jobs processing (long-running; resolves when workers are stopped) */
apps/api/src/db/dbConnection.ts (3)

66-73: Disambiguate success logs for chain vs user DB.

Both success logs say “Connection has been established successfully.” Consider making them specific to avoid ambiguity in logs.

-  logger.debug("Connection has been established successfully.");
+  logger.debug("Chain DB connection established.");
@@
-  logger.debug("Connection has been established successfully.");
+  logger.debug("User DB connection established.");

Also applies to: 70-73


59-65: Docblock over-promises relative to current implementation.

The comment mentions populate, per-version backups, and loading from backups; the function currently authenticates and syncs user schema only. Align doc with reality or implement the missing steps.

 /**
- * Initialize database schema
- * Populate db
- * Create backups per version
- * Load from backup if exists for current version
+ * Initialize database connections and sync user schema.
+ * Note: population and backups are out of scope here and handled elsewhere.
  */

21-21: Risk: pg int8 parsing to JS number can lose precision.

pg.defaults.parseInt8 = true coerces int8 to Number, which is unsafe for values > 2^53-1. If any int8 columns (e.g., heights, IDs) can exceed this, prefer parsing to BigInt or string.

Example:

import pg from "pg";
// 20 = INT8 OID
pg.types.setTypeParser(20, (val: string) => BigInt(val));

Check whether any int8 columns in chainModels/userModels can exceed JS safe integer range.

apps/api/src/core/services/start-server/start-server.ts (2)

53-56: Prefer beforeExit over exit for async cleanup or drop the exit hook.

The exit event doesn’t allow async work; your shutdown() awaits async tasks. Use beforeExit or rely on SIGTERM/SIGINT + server close.

-    processEvents.on("exit", shutdown);
+    processEvents.on("beforeExit", shutdown);

47-52: Add a “SERVER_STARTED” log with the actual bound port/host.

When port: 0, SERVER_STARTING logs localhost:0. Log once the server is listening with the real address.

     server = serve({
       fetch: app.fetch,
       port: options.port
     });
 
+    // Log actual address once the server is listening
+    server.once("listening", () => {
+      const addr = server.address();
+      const host = typeof addr === "string" ? addr : addr?.address ?? "0.0.0.0";
+      const port = typeof addr === "string" ? options.port : addr?.port;
+      logger.info({ event: "SERVER_STARTED", url: `http://${host}:${port}` });
+    });
apps/api/src/core/services/start-server/start-server.spec.ts (2)

14-16: Await server close in afterEach to prevent race conditions between tests.

Ensures the port is released and listeners are cleaned up before the next test runs.

-  afterEach(() => {
-    startedServer?.close();
-  });
+  afterEach(async () => {
+    if (startedServer) {
+      await new Promise<void>(resolve => startedServer!.close(() => resolve()));
+      startedServer = undefined;
+    }
+  });

127-135: Optional: assert SERVER_START_ERROR is logged on failures.

You already assert disposal on errors. Adding an assertion on logger.error({ event: "SERVER_START_ERROR", ... }) would fully validate the error path.

Also applies to: 136-144

apps/api/src/healthz/services/healthz/healthz.service.spec.ts (1)

103-126: Stabilize TTL test by enabling fake timers before scheduling.

Switching to fake timers mid-test can be flaky if the cache uses timers scheduled earlier. Enable fake timers at the start, then restore at the end.

-    // First call - both should be called
-    await service.getLivenessStatus();
+    jest.useFakeTimers();
+    // First call - both should be called
+    await service.getLivenessStatus();
@@
-    // Advance time beyond TTL
-    jest.useFakeTimers();
+    // Advance time beyond TTL
     jest.advanceTimersByTime(millisecondsInMinute + 1);
@@
-    jest.useRealTimers();
+    jest.useRealTimers();
apps/api/src/rest-app.ts (3)

15-19: Ensure error handler retains class context when passed to Hono

You resolve HonoErrorHandlerService and later pass .handle to appHono.onError. If handle uses this, it will lose context when passed unbound. Prefer binding the instance before registering, or wrap in a lambda.

Example (outside this hunk):

const errorHandler = container.resolve(HonoErrorHandlerService);
appHono.onError(errorHandler.handle.bind(errorHandler));
// or
appHono.onError((err, c) => errorHandler.handle(err, c));

186-192: Make scheduler start resilient and observable

Starting the scheduler during APP_INITIALIZER will fail the whole startup if it throws. If that’s intended — fine. Otherwise, guard it and add structured logs for operability.

 container.register(APP_INITIALIZER, {
   useValue: {
     async [ON_APP_START]() {
-      scheduler.start();
+      try {
+        const logger = LoggerService.forContext("Scheduler");
+        logger.info({ event: "SCHEDULER_STARTING" });
+        scheduler.start();
+        logger.info({ event: "SCHEDULER_STARTED" });
+      } catch (error) {
+        // If the API should still boot without background jobs, demote to warning.
+        LoggerService.forContext("Scheduler").error({ event: "SCHEDULER_START_ERROR", error });
+        // Optionally rethrow to keep current fail-fast behavior.
+        // throw error;
+      }
     }
   } satisfies AppInitializer
 });

196-201: Return the server from bootstrap for testability and graceful shutdown

startServer returns ServerType | undefined but bootstrap discards it. Returning it enables tests and CLIs to close the server and await shutdown.

-export async function bootstrap(port: number) {
-  await startServer(appHono, LoggerService.forContext("APP"), process, {
-    port,
-    beforeStart: migratePG
-  });
-}
+export async function bootstrap(port: number) {
+  return startServer(appHono, LoggerService.forContext("APP"), process, {
+    port,
+    beforeStart: migratePG
+  });
+}
apps/api/src/healthz/services/healthz/healthz.service.ts (4)

30-40: Revisit liveness semantics: avoid coupling to external dependencies

Liveness typically answers “is the process healthy” and should be cheap and independent of downstreams; readiness should gate dependencies. Making liveness depend on DB and job queue can cause restart flaps during transient outages.

Options:

  • Make getLivenessStatus() always return ok (plus simple self-checks like event loop lag, heap thresholds).
  • Keep dependency checks only in readiness, or make them best-effort/soft-fail for liveness.
    Would you like a patch moving DB/JobQueue checks to readiness only?

42-50: Don’t cache failures (or shorten TTL) for liveness paths

Memoizing liveness for 60s delays recovery reporting: after DB/queue recovers, pods may stay “error” for up to 60s. Either:

  • Cache only success, or
  • Use a much smaller TTL for liveness (e.g., 5–10s), or
  • Drop caching for liveness entirely.

Minimal change:

-@Memoize({ ttlInSeconds: secondsInMinute })
+@Memoize({ ttlInSeconds: 10 })
 private async isPostgresAlive() {
   return this.isPostgresHealthy();
 }
 
-@Memoize({ ttlInSeconds: secondsInMinute })
+@Memoize({ ttlInSeconds: 10 })
 private async isJobQueueAlive() {
   return this.isJobQueueHealthy();
 }

If you prefer “cache only success,” I can draft a MemoizeOnSuccess decorator that bypasses caching on false.


52-63: DB health ping with structured error logging: LGTM

Clear error event and boolean collapse are appropriate. Consider including a short error class/name to ease alert rule matching, but current shape is fine.


65-76: Add explicit return type for consistency and clarity

isJobQueueHealthy returns a boolean but lacks an explicit return type, unlike the DB counterpart.

-private async isJobQueueHealthy() {
+private async isJobQueueHealthy(): Promise<boolean> {
   try {
     await this.jobQueueHealthcheck.ping();
     return true;
   } catch (error) {
     this.logger.error({
       event: "JOB_QUEUE_HEALTHCHECK_ERROR",
       error
     });
     return false;
   }
 }

@stalniy stalniy force-pushed the feat/background-jobs branch from 812c2f0 to e569b3c Compare August 22, 2025 21:47
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/api/src/core/services/job-queue/job-queue.service.ts (1)

76-87: Avoid logging entire job payload — potential PII leakage. Log only non-sensitive metadata.

job may include emails or other PII (see Notification jobs). Logging the full payload at INFO risks sensitive data exposure.

Apply this diff to log minimal fields after send:

-  async enqueue(job: Job, options?: EnqueueOptions): Promise<string | null> {
-    this.logger.info({
-      event: "JOB_ENQUEUED",
-      job
-    });
-
-    return await this.pgBoss.send({
+  async enqueue(job: Job, options?: EnqueueOptions): Promise<string | null> {
+    const jobId = await this.pgBoss.send({
       name: job.name,
       data: { ...job.data, version: job.version },
       options
     });
+    this.logger.info({
+      event: "JOB_ENQUEUED",
+      queue: job.name,
+      jobVersion: job.version,
+      jobId
+    });
+    return jobId;
   }
♻️ Duplicate comments (5)
apps/api/src/core/services/job-queue/job-queue.service.ts (1)

99-121: Handler callback destructures as array while forcing batchSize=1 — will throw at runtime. Normalize payload shape.

pg-boss calls the handler with a single job when batchSize=1, not an array. Destructuring ([job]) risks “job is not iterable”. Normalize to both shapes.

Apply this diff:

-      const workersPromises = Array.from({ length: options.batchSize ?? 10 }).map(() =>
-        this.pgBoss.work<Job["data"]>(queueName, workerOptions, async ([job]) => {
+      const workersPromises = Array.from({ length: options.batchSize ?? 10 }).map(() =>
+        this.pgBoss.work<Job["data"]>(queueName, workerOptions, async (jobOrJobs) => {
+          const [job] = Array.isArray(jobOrJobs) ? jobOrJobs : [jobOrJobs];
           this.logger.info({
             event: "JOB_STARTED",
             jobId: job.id
           });
           try {
             await handler.handle(job.data);
             this.logger.info({
               event: "JOB_DONE",
               jobId: job.id
             });
           } catch (error) {
             this.logger.error({
               event: "JOB_FAILED",
               jobId: job.id,
               error
             });
             throw error;
           }
-        })
+        })
       );
 
       await Promise.all(workersPromises);
apps/api/src/core/services/job-queue/job-queue.service.spec.ts (3)

109-136: Type-safe pgBoss.work mock and job payload; remove anys.

Replace any usages in the mock with concrete types to comply with repo guidelines and improve safety.

Apply this diff:

-      const jobs = [{ id: "1", data: { message: "Job 1", userId: "user-1" } }];
-
-      jest.spyOn(pgBoss, "work").mockImplementation(async (queueName: string, options: any, processFn: (jobs: any[]) => Promise<void>) => {
-        await processFn(jobs);
+      const jobs: EnqueuedJob[] = [{ id: "1", data: { message: "Job 1", userId: "user-1" } }];
+
+      jest.spyOn(pgBoss, "work").mockImplementation(async (_queueName: string, _options: Parameters<PgBoss["work"]>[1], processFn: (jobs: EnqueuedJob[]) => Promise<void>) => {
+        await processFn(jobs as EnqueuedJob[]);
         return "work-id";
       });

Add this at the top-level of the file (outside tests) to define EnqueuedJob:

type EnqueuedJob = { id: string; data: TestJob["data"] };

142-166: Type-safe failure path mock; remove anys.

Mirror the typing fix in the failure path.

Apply this diff:

-      const jobs = [{ id: "1", data: { message: "Job 1", userId: "user-1" } }];
-
-      jest.spyOn(pgBoss, "work").mockImplementation(async (queueName: string, options: any, processFn: (jobs: any[]) => Promise<void>) => {
-        await processFn(jobs);
+      const jobs: EnqueuedJob[] = [{ id: "1", data: { message: "Job 1", userId: "user-1" } }];
+
+      jest.spyOn(pgBoss, "work").mockImplementation(async (_queueName: string, _options: Parameters<PgBoss["work"]>[1], processFn: (jobs: EnqueuedJob[]) => Promise<void>) => {
+        await processFn(jobs as EnqueuedJob[]);
         return "work-id";
       });

168-183: Type-safe default-options mock; remove anys.

Keep mocks consistently typed.

Apply this diff:

-      jest.spyOn(pgBoss, "work").mockImplementation(async (queueName: string, options: any, processFn: (jobs: any[]) => Promise<void>) => {
-        await processFn([{ id: "1", data: { message: "Job 1", userId: "user-1" } }]);
+      jest.spyOn(pgBoss, "work").mockImplementation(async (_queueName: string, _options: Parameters<PgBoss["work"]>[1], processFn: (jobs: EnqueuedJob[]) => Promise<void>) => {
+        await processFn([{ id: "1", data: { message: "Job 1", userId: "user-1" } }] as EnqueuedJob[]);
         return "work-id";
       });
apps/api/src/rest-app.ts (1)

194-194: Alias export initDbconnectUsingSequelize: ensure callers use the public alias

Good transitional surface. As noted previously, please replace any lingering direct imports of connectUsingSequelize with initDb unless a custom logger is required.

#!/bin/bash
# Find any direct imports/usages bypassing the alias
rg -nP --type ts -C2 "\b(connectUsingSequelize)\b" apps/api
🧹 Nitpick comments (15)
apps/api/env/.env.unit.test (1)

28-28: dotenv-linter UnorderedKey warning — optional cleanup

The linter flags key ordering; consider alphabetizing keys in this file (or all env.test files) to silence the warning. Keep it as a separate housekeeping commit to avoid noisy diffs.

Minimal change (move the key near the top before POSTGRES_DB_URI):

-NOTIFICATIONS_API_BASE_URL=http://localhost:3090
+NOTIFICATIONS_API_BASE_URL=http://localhost:3090

Alternatively, standardize on global alphabetical ordering across the file(s) in a follow-up.

apps/api/src/core/services/shutdown-server/shutdown-server.spec.ts (5)

7-101: Adopt the repository’s setup() testing pattern to reduce duplication and align with guidelines.

Per the project’s spec guidelines, use a setup function at the bottom of the root describe to construct typed mocks and parameterize server behavior. This removes repeated mock wiring and improves readability.

Example refactor (sketch; keep existing imports):

describe(shutdownServer.name, () => {
  it("closes the server", async () => {
    const { server, appLogger, onShutdown } = setup();
    await shutdownServer(server, appLogger, onShutdown);
    expect(server.close).toHaveBeenCalledTimes(1);
    expect(onShutdown).toHaveBeenCalledTimes(1);
    expect(appLogger.error).not.toHaveBeenCalled();
  });

  it("logs error if server close fails", async () => {
    const error = new Error("Failed to close server");
    const { server, appLogger, onShutdown } = setup({ closeBehavior: "callbackError", error });
    await shutdownServer(server, appLogger, onShutdown);
    expect(appLogger.error).toHaveBeenCalledWith({ event: "SERVER_CLOSE_ERROR", error });
    expect(onShutdown).toHaveBeenCalled();
  });

  it("logs error if server close throws", async () => {
    const error = new Error("Failed to close server");
    const { server, appLogger, onShutdown } = setup({ closeBehavior: "throw", error });
    await shutdownServer(server, appLogger, onShutdown);
    expect(appLogger.error).toHaveBeenCalledWith({ event: "SERVER_CLOSE_ERROR", error });
    expect(onShutdown).toHaveBeenCalled();
  });

  it("logs error if onShutdown callback fails", async () => {
    const error = new Error("Failed to dispose container");
    const { server, appLogger } = setup();
    const onShutdown = jest.fn().mockRejectedValue(error);
    await shutdownServer(server, appLogger, onShutdown);
    expect(appLogger.error).toHaveBeenCalledWith({ event: "ON_SHUTDOWN_ERROR", error });
  });

  it("calls shutdown directly if server is not listening", async () => {
    const { server, appLogger, onShutdown } = setup({ listening: false });
    await shutdownServer(server, appLogger, onShutdown);
    expect(server.close).not.toHaveBeenCalled();
    expect(onShutdown).toHaveBeenCalled();
  });

  it("calls server.close if server is listening", async () => {
    const { server, appLogger, onShutdown } = setup({ listening: true });
    await shutdownServer(server, appLogger, onShutdown);
    expect(server.close).toHaveBeenCalled();
    expect(onShutdown).toHaveBeenCalled();
  });

  function setup({
    listening = true,
    closeBehavior = "success" as "success" | "callbackError" | "throw",
    error = new Error("close failed")
  }: {
    listening?: boolean;
    closeBehavior?: "success" | "callbackError" | "throw";
    error?: Error;
  } = {}) {
    const appLogger = mock<Logger>();
    const onShutdown = jest.fn();
    const server = mock<ServerType>({
      listening,
      close:
        closeBehavior === "throw"
          ? (jest.fn().mockImplementation(() => {
              throw error;
            }) as unknown as ServerType["close"])
          : jest
              .fn()
              .mockImplementation((cb: (err?: Error) => void) =>
                cb(closeBehavior === "callbackError" ? error : undefined)
              )
    });
    return { server, appLogger, onShutdown, error };
  }
});

32-35: De-duplicate event key literals (SERVER_CLOSE_ERROR).

To avoid future drift when event keys change, reference a constant instead of repeating string literals in assertions.

Apply within this range:

-    expect(appLogger.error).toHaveBeenCalledWith({
-      event: "SERVER_CLOSE_ERROR",
-      error
-    });
+    expect(appLogger.error).toHaveBeenCalledWith({
+      event: EVENTS.SERVER_CLOSE_ERROR,
+      error
+    });

Add once near the imports (outside this range):

const EVENTS = {
  SERVER_CLOSE_ERROR: "SERVER_CLOSE_ERROR",
  ON_SHUTDOWN_ERROR: "ON_SHUTDOWN_ERROR"
} as const;

51-54: Keep using constants for event keys here as well.

-    expect(appLogger.error).toHaveBeenCalledWith({
-      event: "SERVER_CLOSE_ERROR",
-      error
-    });
+    expect(appLogger.error).toHaveBeenCalledWith({
+      event: EVENTS.SERVER_CLOSE_ERROR,
+      error
+    });

68-71: Likewise, use a constant for ON_SHUTDOWN_ERROR.

-    expect(appLogger.error).toHaveBeenCalledWith({
-      event: "ON_SHUTDOWN_ERROR",
-      error
-    });
+    expect(appLogger.error).toHaveBeenCalledWith({
+      event: EVENTS.ON_SHUTDOWN_ERROR,
+      error
+    });

42-45: Tighten typing of the “throws” mock to avoid broad jest.Mock cast.

Prefer casting to the precise function type (ServerType["close"]) rather than generic jest.Mock.

-      close: jest.fn().mockImplementation(() => {
-        throw error;
-      }) as jest.Mock
+      close: (jest.fn().mockImplementation(() => {
+        throw error;
+      }) as unknown) as ServerType["close"]
apps/api/webpack.dev.js (1)

11-18: Refactor webpack entry configuration to a single orchestrator

Verification confirms that neither rest-app.ts nor background-jobs-app.ts invoke any bootstrap or startServer calls at module load—only server.ts does (via bootstrap(process.env))—so the current multi-entry array won’t inadvertently spin up multiple servers. However, bundling all three modules as a single “multi-main” entry:

• duplicates imports (server.ts already imports both sub-apps)
• forces webpack to include and execute every top-level import in each file
• adds cognitive overhead and potential for subtle side-effects (e.g., duplicate DI provider registrations)

To simplify and future-proof:

• Replace the array entry with a single orchestrator entry in apps/api/webpack.dev.js (lines 11–18):

- entry: ["./src/server.ts", "./src/rest-app.ts", "./src/background-jobs-app.ts"],
+ entry: { server: "./src/server.ts" },-   filename: "server.js"
+   filename: "[name].js"

• If you genuinely need separate bundles for each sub-app (but only run the “server” bundle in development), use named entries instead:

- entry: ["./src/server.ts", "./src/rest-app.ts", "./src/background-jobs-app.ts"],
+ entry: {
+   server: "./src/server.ts",
+   rest:   "./src/rest-app.ts",
+   jobs:   "./src/background-jobs-app.ts"
+ },-   filename: "server.js"
+   filename: "[name].js"

This refactor removes redundant entry points, clarifies execution order, and keeps your bundle aligned with the single-orchestrator pattern.

apps/api/src/core/services/job-queue/job-queue.service.spec.ts (2)

121-136: Assertion will need updating if you adopt explicit concurrency.

If you switch startWorkers to use { concurrency }, update this test to call startWorkers({ concurrency: 5 }) and keep workerOptions.batchSize at 1.

I can provide the exact diff for all affected tests if you accept the concurrency change in the service.


230-274: Place setup() at the bottom of the root describe per test guidelines.

Your setup function is above class helpers; move it to be the last item inside the root describe.

apps/api/test/functional/user-init.spec.ts (1)

11-19: Avoid jest.mock in spec files; prefer dependency injection or explicit test composition.

Guidelines: don’t use jest.mock() in spec files. Instead, pass mocks via DI or a test composition function. Since this is a functional test, two options:

  • Expose an overridable user context provider (e.g., token via tsyringe) that your user middleware consumes; in tests, register a mock with container.registerInstance(...).
  • Expose a createRestApp({ userContextProvider }) factory for tests to inject a stub without module-level mocking.

I can sketch a minimal UserContextProvider token and wire it through the middleware so this test can replace the dependency without jest.mock().

apps/api/src/core/providers/postgres.provider.ts (1)

33-33: Prefer Symbol tokens to minimize collision risk across containers/modules

String tokens work but are easier to collide with. Consider switching to Symbol("DB_HEALTHCHECK") (as is already done elsewhere for APP_INITIALIZER/ON_APP_START).

-export const DB_HEALTHCHECK: InjectionToken<DbHealthcheck> = "DB_HEALTHCHECK";
+export const DB_HEALTHCHECK: InjectionToken<DbHealthcheck> = Symbol("DB_HEALTHCHECK");
apps/api/src/server.ts (3)

33-37: Remove debug console.log or route through LoggerService

Leaking process.send in logs is noisy and not actionable in production.

-  console.log("process.send", process.send, INTERFACE);
+  // If logging is needed here, prefer the project logger:
+  // LoggerService.forContext("BOOTSTRAP").debug({ hasIPC: Boolean(process.send), interface: INTERFACE });

15-16: Typo: boostrapList → bootstrapList

Minor readability/polish.

-    const boostrapList = SUPPORTED_INTERFACES.map((interfaceName, index) => bootstrapInChildProcess({ PORT: String(port + index), INTERFACE: interfaceName }));
-    await Promise.all(boostrapList);
+    const bootstrapList = SUPPORTED_INTERFACES.map((interfaceName, index) =>
+      bootstrapInChildProcess({ PORT: String(port + index), INTERFACE: interfaceName })
+    );
+    await Promise.all(bootstrapList);

7-8: Narrow interface types for better checks and autocomplete

Lock SUPPORTED_INTERFACES to a literal tuple and derive a union for INTERFACE. This catches typos at compile time and improves DX.

-const SUPPORTED_INTERFACES = ["rest", "background-jobs"];
+const SUPPORTED_INTERFACES = ["rest", "background-jobs"] as const;
+type InterfaceName = typeof SUPPORTED_INTERFACES[number];
+
+type BootEnv = {
+  PORT?: string;
+  INTERFACE?: "all" | InterfaceName;
+};

And update bootstrap signature:

-async function bootstrap({ PORT = "3080", INTERFACE = "all" }: Record<string, string | undefined>): Promise<void> {
+async function bootstrap({ PORT = "3080", INTERFACE = "all" }: BootEnv): Promise<void> {
apps/api/src/db/dbConnection.ts (1)

71-83: Avoid variable shadowing and make logs unambiguous

The parameter logger shadows the module-scoped logger (PostgresLoggerService), which hampers readability. Rename the parameter to avoid confusion.

-export async function connectUsingSequelize(logger: LoggerService = LoggerService.forContext("DB")): Promise<void> {
-  logger.debug(`Connecting to chain database (${chainDb.config.host}/${chainDb.config.database})...`);
+export async function connectUsingSequelize(appLogger: LoggerService = LoggerService.forContext("DB")): Promise<void> {
+  appLogger.debug(`Connecting to chain database (${chainDb.config.host}/${chainDb.config.database})...`);
   await chainDb.authenticate();
-  logger.debug("Connection has been established successfully.");
+  appLogger.debug("Chain DB connection established.");
 
-  logger.debug(`Connecting to user database (${userDb.config.host}/${userDb.config.database})...`);
+  appLogger.debug(`Connecting to user database (${userDb.config.host}/${userDb.config.database})...`);
   await userDb.authenticate();
-  logger.debug("Connection has been established successfully.");
+  appLogger.debug("User DB connection established.");
 
-  logger.debug("Sync user schema...");
+  appLogger.debug("Sync user schema...");
   await syncUserSchema();
-  logger.debug("User schema synced.");
+  appLogger.debug("User schema synced.");
 }

If startup latency matters, consider authenticating both connections concurrently and adjusting logs accordingly.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 812c2f0 and e569b3c.

📒 Files selected for processing (72)
  • apps/api/env/.env.functional.test (1 hunks)
  • apps/api/env/.env.local.sample (1 hunks)
  • apps/api/env/.env.unit.test (1 hunks)
  • apps/api/src/app/index.ts (1 hunks)
  • apps/api/src/app/providers/jobs.provider.ts (1 hunks)
  • apps/api/src/background-jobs-app.ts (1 hunks)
  • apps/api/src/console.ts (1 hunks)
  • apps/api/src/core/index.ts (1 hunks)
  • apps/api/src/core/providers/index.ts (1 hunks)
  • apps/api/src/core/providers/job-queue-healthcheck.ts (1 hunks)
  • apps/api/src/core/providers/postgres.provider.ts (4 hunks)
  • apps/api/src/core/services/index.ts (1 hunks)
  • apps/api/src/core/services/job-queue/job-queue.service.spec.ts (4 hunks)
  • apps/api/src/core/services/job-queue/job-queue.service.ts (4 hunks)
  • apps/api/src/core/services/shutdown-server/shutdown-server.spec.ts (1 hunks)
  • apps/api/src/core/services/shutdown-server/shutdown-server.ts (1 hunks)
  • apps/api/src/core/services/start-server/start-server.spec.ts (1 hunks)
  • apps/api/src/core/services/start-server/start-server.ts (1 hunks)
  • apps/api/src/db/dbConnection.ts (2 hunks)
  • apps/api/src/healthz/controllers/healthz/healthz.controller.spec.ts (2 hunks)
  • apps/api/src/healthz/routes/healthz.router.ts (1 hunks)
  • apps/api/src/healthz/services/healthz/healthz.service.spec.ts (1 hunks)
  • apps/api/src/healthz/services/healthz/healthz.service.ts (1 hunks)
  • apps/api/src/index.ts (0 hunks)
  • apps/api/src/notifications/services/notification-handler/notification.handler.ts (1 hunks)
  • apps/api/src/rest-app.ts (4 hunks)
  • apps/api/src/server.ts (1 hunks)
  • apps/api/test/functional/addresses.spec.ts (2 hunks)
  • apps/api/test/functional/anonymous-user.spec.ts (1 hunks)
  • apps/api/test/functional/api-key.spec.ts (1 hunks)
  • apps/api/test/functional/app.spec.ts (1 hunks)
  • apps/api/test/functional/auditors.spec.ts (1 hunks)
  • apps/api/test/functional/balances.spec.ts (1 hunks)
  • apps/api/test/functional/bids.spec.ts (1 hunks)
  • apps/api/test/functional/blocks.spec.ts (1 hunks)
  • apps/api/test/functional/certificate.spec.ts (1 hunks)
  • apps/api/test/functional/create-deployment.spec.ts (1 hunks)
  • apps/api/test/functional/dashboard-data.spec.ts (1 hunks)
  • apps/api/test/functional/deployment-setting.spec.ts (1 hunks)
  • apps/api/test/functional/deployments.spec.ts (1 hunks)
  • apps/api/test/functional/docs.spec.ts (1 hunks)
  • apps/api/test/functional/gpu.spec.ts (1 hunks)
  • apps/api/test/functional/graph-data.spec.ts (1 hunks)
  • apps/api/test/functional/lease-flow.spec.ts (1 hunks)
  • apps/api/test/functional/leases-duration.spec.ts (1 hunks)
  • apps/api/test/functional/market-data.spec.ts (1 hunks)
  • apps/api/test/functional/network-capacity.spec.ts (1 hunks)
  • apps/api/test/functional/nodes-v1.spec.ts (1 hunks)
  • apps/api/test/functional/pricing.spec.ts (1 hunks)
  • apps/api/test/functional/proposals.spec.ts (1 hunks)
  • apps/api/test/functional/provider-attributes-schema.spec.ts (1 hunks)
  • apps/api/test/functional/provider-dashboard.spec.ts (1 hunks)
  • apps/api/test/functional/provider-deployments.spec.ts (1 hunks)
  • apps/api/test/functional/provider-earnings.spec.ts (1 hunks)
  • apps/api/test/functional/provider-graph-data.spec.ts (1 hunks)
  • apps/api/test/functional/provider-regions.spec.ts (1 hunks)
  • apps/api/test/functional/provider-versions.spec.ts (1 hunks)
  • apps/api/test/functional/providers.spec.ts (1 hunks)
  • apps/api/test/functional/sign-and-broadcast-tx.spec.ts (1 hunks)
  • apps/api/test/functional/stale-anonymous-users-cleanup.spec.ts (1 hunks)
  • apps/api/test/functional/start-trial.spec.ts (1 hunks)
  • apps/api/test/functional/stripe-webhook.spec.ts (1 hunks)
  • apps/api/test/functional/templates.spec.ts (1 hunks)
  • apps/api/test/functional/transactions.spec.ts (1 hunks)
  • apps/api/test/functional/usage.spec.ts (1 hunks)
  • apps/api/test/functional/user-init.spec.ts (1 hunks)
  • apps/api/test/functional/validators.spec.ts (1 hunks)
  • apps/api/test/functional/wallets-refill.spec.ts (1 hunks)
  • apps/api/tsconfig.json (1 hunks)
  • apps/api/webpack.dev.js (2 hunks)
  • apps/api/webpack.prod.js (2 hunks)
  • packages/docker/docker-compose.prod.yml (2 hunks)
💤 Files with no reviewable changes (1)
  • apps/api/src/index.ts
✅ Files skipped from review due to trivial changes (5)
  • apps/api/test/functional/blocks.spec.ts
  • apps/api/env/.env.functional.test
  • apps/api/test/functional/auditors.spec.ts
  • apps/api/test/functional/create-deployment.spec.ts
  • apps/api/test/functional/dashboard-data.spec.ts
🚧 Files skipped from review as they are similar to previous changes (54)
  • apps/api/test/functional/gpu.spec.ts
  • apps/api/test/functional/network-capacity.spec.ts
  • apps/api/test/functional/sign-and-broadcast-tx.spec.ts
  • apps/api/test/functional/graph-data.spec.ts
  • apps/api/test/functional/provider-deployments.spec.ts
  • apps/api/test/functional/certificate.spec.ts
  • apps/api/test/functional/wallets-refill.spec.ts
  • apps/api/tsconfig.json
  • apps/api/test/functional/docs.spec.ts
  • apps/api/test/functional/validators.spec.ts
  • apps/api/test/functional/lease-flow.spec.ts
  • apps/api/test/functional/start-trial.spec.ts
  • apps/api/src/app/providers/jobs.provider.ts
  • apps/api/webpack.prod.js
  • apps/api/src/core/services/index.ts
  • apps/api/test/functional/provider-graph-data.spec.ts
  • apps/api/test/functional/pricing.spec.ts
  • packages/docker/docker-compose.prod.yml
  • apps/api/test/functional/transactions.spec.ts
  • apps/api/src/background-jobs-app.ts
  • apps/api/test/functional/provider-earnings.spec.ts
  • apps/api/test/functional/providers.spec.ts
  • apps/api/test/functional/stripe-webhook.spec.ts
  • apps/api/test/functional/app.spec.ts
  • apps/api/src/core/services/shutdown-server/shutdown-server.ts
  • apps/api/src/healthz/routes/healthz.router.ts
  • apps/api/env/.env.local.sample
  • apps/api/test/functional/market-data.spec.ts
  • apps/api/src/healthz/controllers/healthz/healthz.controller.spec.ts
  • apps/api/test/functional/balances.spec.ts
  • apps/api/test/functional/provider-attributes-schema.spec.ts
  • apps/api/test/functional/anonymous-user.spec.ts
  • apps/api/test/functional/deployments.spec.ts
  • apps/api/test/functional/nodes-v1.spec.ts
  • apps/api/test/functional/stale-anonymous-users-cleanup.spec.ts
  • apps/api/src/healthz/services/healthz/healthz.service.spec.ts
  • apps/api/src/core/services/start-server/start-server.spec.ts
  • apps/api/test/functional/api-key.spec.ts
  • apps/api/test/functional/deployment-setting.spec.ts
  • apps/api/test/functional/bids.spec.ts
  • apps/api/src/console.ts
  • apps/api/test/functional/templates.spec.ts
  • apps/api/test/functional/addresses.spec.ts
  • apps/api/test/functional/usage.spec.ts
  • apps/api/test/functional/leases-duration.spec.ts
  • apps/api/src/core/providers/job-queue-healthcheck.ts
  • apps/api/src/notifications/services/notification-handler/notification.handler.ts
  • apps/api/test/functional/provider-versions.spec.ts
  • apps/api/test/functional/provider-dashboard.spec.ts
  • apps/api/src/core/services/start-server/start-server.ts
  • apps/api/src/app/index.ts
  • apps/api/test/functional/provider-regions.spec.ts
  • apps/api/test/functional/proposals.spec.ts
  • apps/api/src/healthz/services/healthz/healthz.service.ts
🧰 Additional context used
📓 Path-based instructions (3)
**/*.spec.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/no-jest-mock.mdc)

Don't use jest.mock() to mock dependencies in test files. Instead, use jest-mock-extended to create mocks and pass mocks as dependencies to the service under test.

**/*.spec.{ts,tsx}: Use setup function instead of beforeEach in test files
setup function must be at the bottom of the root describe block in test files
setup function creates an object under test and returns it
setup function should accept a single parameter with inline type definition
Don't use shared state in setup function
Don't specify return type of setup function

Files:

  • apps/api/src/core/services/shutdown-server/shutdown-server.spec.ts
  • apps/api/test/functional/user-init.spec.ts
  • apps/api/src/core/services/job-queue/job-queue.service.spec.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

Never use type any or cast to type any. Always define the proper TypeScript types.

Files:

  • apps/api/src/core/services/shutdown-server/shutdown-server.spec.ts
  • apps/api/src/core/providers/postgres.provider.ts
  • apps/api/src/db/dbConnection.ts
  • apps/api/test/functional/user-init.spec.ts
  • apps/api/src/core/services/job-queue/job-queue.service.spec.ts
  • apps/api/src/core/services/job-queue/job-queue.service.ts
  • apps/api/src/core/index.ts
  • apps/api/src/server.ts
  • apps/api/src/rest-app.ts
  • apps/api/src/core/providers/index.ts
**/*.{js,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

**/*.{js,ts,tsx}: Never use deprecated methods from libraries.
Don't add unnecessary comments to the code

Files:

  • apps/api/src/core/services/shutdown-server/shutdown-server.spec.ts
  • apps/api/src/core/providers/postgres.provider.ts
  • apps/api/src/db/dbConnection.ts
  • apps/api/test/functional/user-init.spec.ts
  • apps/api/src/core/services/job-queue/job-queue.service.spec.ts
  • apps/api/src/core/services/job-queue/job-queue.service.ts
  • apps/api/src/core/index.ts
  • apps/api/webpack.dev.js
  • apps/api/src/server.ts
  • apps/api/src/rest-app.ts
  • apps/api/src/core/providers/index.ts
🧠 Learnings (2)
📚 Learning: 2025-07-21T08:24:24.269Z
Learnt from: CR
PR: akash-network/console#0
File: .cursor/rules/no-jest-mock.mdc:0-0
Timestamp: 2025-07-21T08:24:24.269Z
Learning: Applies to **/*.spec.{ts,tsx} : Don't use `jest.mock()` to mock dependencies in test files. Instead, use `jest-mock-extended` to create mocks and pass mocks as dependencies to the service under test.

Applied to files:

  • apps/api/src/core/services/job-queue/job-queue.service.spec.ts
📚 Learning: 2025-08-21T04:03:23.490Z
Learnt from: stalniy
PR: akash-network/console#1827
File: apps/api/src/console.ts:146-154
Timestamp: 2025-08-21T04:03:23.490Z
Learning: In the Akash Network console project, services like JobQueueService implement dispose() methods for resource cleanup, and the tsyringe container has been extended with a dispose() method that iterates over all registered instances and calls their dispose() methods, enabling proper shutdown orchestration.

Applied to files:

  • apps/api/src/core/services/job-queue/job-queue.service.ts
🧬 Code graph analysis (6)
apps/api/src/core/providers/postgres.provider.ts (5)
apps/api/src/db/dbConnection.ts (2)
  • closeConnections (54-54)
  • ON_APP_START (58-60)
apps/api/src/core/providers/app-initializer.ts (3)
  • APP_INITIALIZER (3-3)
  • ON_APP_START (4-4)
  • AppInitializer (6-8)
apps/api/src/app/providers/jobs.provider.ts (1)
  • ON_APP_START (11-19)
apps/api/src/rest-app.ts (1)
  • ON_APP_START (188-190)
apps/api/src/core/services/feature-flags/feature-flags.service.ts (1)
  • ON_APP_START (80-82)
apps/api/src/db/dbConnection.ts (4)
apps/api/src/core/providers/postgres.provider.ts (1)
  • closeConnections (54-54)
apps/api/src/core/providers/app-initializer.ts (2)
  • APP_INITIALIZER (3-3)
  • AppInitializer (6-8)
apps/api/src/app/providers/jobs.provider.ts (1)
  • ON_APP_START (11-19)
apps/api/src/rest-app.ts (2)
  • ON_APP_START (188-190)
  • connectUsingSequelize (194-194)
apps/api/src/core/services/job-queue/job-queue.service.spec.ts (2)
apps/api/src/core/services/job-queue/job-queue.service.ts (1)
  • setup (135-142)
packages/logging/src/servicies/logger/logger.service.ts (1)
  • error (107-109)
apps/api/src/core/services/job-queue/job-queue.service.ts (5)
apps/api/src/notifications/services/notification-handler/notification.handler.ts (1)
  • singleton (37-78)
apps/api/src/app/services/trial-started/trial-started.handler.ts (1)
  • singleton (13-94)
apps/api/src/core/services/domain-events/domain-events.service.ts (1)
  • singleton (10-17)
apps/api/src/notifications/services/notification/notification.service.ts (1)
  • singleton (13-67)
packages/logging/src/servicies/logger/logger.service.ts (1)
  • error (107-109)
apps/api/src/server.ts (1)
apps/api/src/rest-app.ts (1)
  • bootstrap (196-201)
apps/api/src/rest-app.ts (5)
apps/api/src/core/providers/app-initializer.ts (2)
  • APP_INITIALIZER (3-3)
  • AppInitializer (6-8)
apps/api/src/app/providers/jobs.provider.ts (1)
  • ON_APP_START (11-19)
apps/api/src/db/dbConnection.ts (1)
  • ON_APP_START (58-60)
apps/api/src/core/services/start-server/start-server.ts (1)
  • startServer (19-64)
apps/api/src/core/providers/postgres.provider.ts (1)
  • migratePG (26-26)
🪛 dotenv-linter (3.3.0)
apps/api/env/.env.unit.test

[warning] 28-28: [UnorderedKey] The NOTIFICATIONS_API_BASE_URL key should go before the POSTGRES_DB_URI key

(UnorderedKey)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: test-build
  • GitHub Check: test-build
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: test-build
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: test-build
  • GitHub Check: Validate local packages
🔇 Additional comments (14)
apps/api/src/core/services/shutdown-server/shutdown-server.spec.ts (1)

68-71: Updated log event key is correct.

Replacing CONTAINER_DISPOSE_ERROR with ON_SHUTDOWN_ERROR in the expectation aligns with the shutdown lifecycle changes. Test intent and coverage remain intact.

apps/api/webpack.dev.js (1)

21-23: TS-first resolution looks good.

extensionAlias ensures bare .js imports resolve to .ts first in dev. Matches prod practice and avoids fragile path rewrites.

apps/api/src/core/services/job-queue/job-queue.service.ts (2)

144-147: pg-boss private API usage in ping(); keep a fallback or assert version compatibility.

getDb() is not part of the public types; relying on it can break across versions. Consider a guarded call or a light-weight public operation (e.g., a trivial send/cancel to a health queue), or document the version constraint.

Proposed guarded approach:

// inside ping()
const bossAny = this.pgBoss as unknown as { getDb?: () => { executeSql: (sql: string) => Promise<any> } };
if (!bossAny.getDb) {
  this.logger.warn({ event: "JOB_QUEUE_PING_UNSUPPORTED" });
  return;
}
// @ts-expect-error - non-public method
await bossAny.getDb().executeSql("SELECT 1");

If you prefer to avoid private APIs entirely, I can provide an alternative ping using a dedicated transient queue.


28-43: Duplicate handler detection looks good.

Set-based guard prevents multiple handlers for the same queue; fail-fast error message is clear.

apps/api/test/functional/user-init.spec.ts (1)

29-31: Functional test composition looks solid.

Using WalletTestingService with app from rest-app maintains the new startup split and keeps the test clear.

apps/api/src/core/index.ts (1)

1-7: Side-effect provider import and explicit re-exports look consistent with the new bootstrap architecture.

This ensures providers register with DI on core import and surfaces start-server and job-queue from a single barrel.

apps/api/src/core/providers/postgres.provider.ts (4)

54-55: LGTM: dispose returns void-compatible promise

Returning undefined post-Promise.all is consistent with Disposable expectations and makes disposal sequencing predictable.


56-62: APP_INITIALIZER + Disposable is a solid pattern for graceful shutdown

Registering a disposable initializer ensures connections are closed via container.dispose() after initializers have been resolved by the server bootstrap. This fits the lifecycle used in startServer.


6-6: Typed import is appropriate

Importing Disposable/InjectionToken as types keeps runtime clean.


15-17: Explicitly scoping initializer tokens keeps coupling low

Importing tokens/interfaces from app-initializer here is correct. No action needed.

apps/api/src/db/dbConnection.ts (2)

54-63: Good lifecycle wiring: init on start, dispose on shutdown

Registering an APP_INITIALIZER with an [ON_APP_START] hook and Disposable.dispose cleanly integrates Sequelize init and teardown with server lifecycle.


10-13: Centralizing LoggerService usage in DB init is consistent and clear

Importing LoggerService here avoids leaking logging concerns to callers.

apps/api/src/rest-app.ts (2)

186-192: Scheduler startup via APP_INITIALIZER fits the new lifecycle

Starting the Scheduler in an APP_INITIALIZER keeps side effects centralized and ordered with other initializers.


196-200: Bootstrap delegates correctly to startServer with migrations hook

Hooking migratePG into beforeStart ensures schema is up-to-date prior to serving. Looks good.

baktun14
baktun14 previously approved these changes Aug 22, 2025
Copy link
Contributor

@baktun14 baktun14 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/api/src/core/services/job-queue/job-queue.service.spec.ts (1)

116-119: Replace any in pgBoss.work mock with precise types; also type the jobs fixture.

Repo guideline forbids any. Use the actual parameter types and a small local job type for the test payload.

Apply this diff:

-      const jobs = [{ id: "1", data: { message: "Job 1", userId: "user-1" } }];
+      const jobs: EnqueuedJob[] = [{ id: "1", data: { message: "Job 1", userId: "user-1" } }];

-      jest.spyOn(pgBoss, "work").mockImplementation(async (queueName: string, options: any, processFn: (jobs: any[]) => Promise<void>) => {
-        await processFn(jobs);
+      jest.spyOn(pgBoss, "work").mockImplementation(async (_queueName: string, _options: Parameters<PgBoss["work"]>[1], processFn: (jobs: EnqueuedJob[]) => Promise<void>) => {
+        await processFn(jobs);
         return "work-id";
       });

Add this helper type near the top of the file (or just above this test):

type EnqueuedJob = { id: string; data: TestJob["data"] };

Also applies to: 114-114

♻️ Duplicate comments (3)
apps/api/src/core/services/job-queue/job-queue.service.spec.ts (2)

149-153: Avoid any in failure-path work mock; mirror the typed signature used above.

Same issue as the success-path mock; keep mocks consistently typed and prefix unused params.

-      const jobs = [{ id: "1", data: { message: "Job 1", userId: "user-1" } }];
+      const jobs: EnqueuedJob[] = [{ id: "1", data: { message: "Job 1", userId: "user-1" } }];

-      jest.spyOn(pgBoss, "work").mockImplementation(async (queueName: string, options: any, processFn: (jobs: any[]) => Promise<void>) => {
-        await processFn(jobs);
+      jest.spyOn(pgBoss, "work").mockImplementation(async (_queueName: string, _options: Parameters<PgBoss["work"]>[1], processFn: (jobs: EnqueuedJob[]) => Promise<void>) => {
+        await processFn(jobs);
         return "work-id";
       });

173-176: Avoid any in default-options work mock; keep the signature typed.

Mirror the same typed signature here; also mark unused params with underscores.

-      jest.spyOn(pgBoss, "work").mockImplementation(async (queueName: string, options: any, processFn: (jobs: any[]) => Promise<void>) => {
-        await processFn([{ id: "1", data: { message: "Job 1", userId: "user-1" } }]);
+      jest.spyOn(pgBoss, "work").mockImplementation(async (_queueName: string, _options: Parameters<PgBoss["work"]>[1], processFn: (jobs: EnqueuedJob[]) => Promise<void>) => {
+        await processFn([{ id: "1", data: { message: "Job 1", userId: "user-1" } }] as EnqueuedJob[]);
         return "work-id";
       });
apps/api/src/core/providers/postgres.provider.ts (1)

30-41: DB_HEALTHCHECK.ping now correctly resolves to Promise — nice cleanup

The healthcheck implementation now awaits the SELECT 1 probe and returns no value, matching the declared Promise<void> type. This prevents accidental reliance on driver return shapes and aligns with the token’s contract.

🧹 Nitpick comments (12)
apps/api/src/core/services/job-queue/job-queue.service.spec.ts (2)

242-262: Move setup() helper to the bottom of the root describe to follow our test convention.

Our test guideline says the setup function must be at the bottom of the root describe; currently, class helpers come after it.

Apply this move (remove here, re-add after the helper classes and just before the closing });):

-  function setup(input?: { pgBoss?: PgBoss; postgresDbUri?: string }) {
-    const mocks = {
-      logger: mock<LoggerService>(),
-      coreConfig: mock<CoreConfigService>({
-        get: jest.fn().mockReturnValue(input?.postgresDbUri ?? "postgresql://localhost:5432/test")
-      }),
-      pgBoss:
-        input?.pgBoss ??
-        mockDeep<PgBoss>({
-          createQueue: jest.fn().mockResolvedValue(undefined),
-          send: jest.fn().mockResolvedValue("job-id"),
-          work: jest.fn().mockResolvedValue(undefined),
-          start: jest.fn().mockResolvedValue(undefined),
-          stop: jest.fn().mockResolvedValue(undefined)
-        })
-    };
-
-    const service = new JobQueueService(mocks.logger, mocks.coreConfig, input && Object.hasOwn(input, "pgBoss") ? input?.pgBoss : mocks.pgBoss);
-
-    return { service, ...mocks };
-  }

Then insert the same function after class TestHandler { ... }:

// Keep setup at the very bottom of the root describe (per repo guideline)
function setup(input?: { pgBoss?: PgBoss; postgresDbUri?: string }) {
  const mocks = {
    logger: mock<LoggerService>(),
    coreConfig: mock<CoreConfigService>({
      get: jest.fn().mockReturnValue(input?.postgresDbUri ?? "postgresql://localhost:5432/test")
    }),
    pgBoss:
      input?.pgBoss ??
      mockDeep<PgBoss>({
        createQueue: jest.fn().mockResolvedValue(undefined),
        send: jest.fn().mockResolvedValue("job-id"),
        work: jest.fn().mockResolvedValue(undefined),
        start: jest.fn().mockResolvedValue(undefined),
        stop: jest.fn().mockResolvedValue(undefined)
      })
  };
  const service = new JobQueueService(mocks.logger, mocks.coreConfig, input && Object.hasOwn(input, "pgBoss") ? input?.pgBoss : mocks.pgBoss);
  return { service, ...mocks };
}

109-140: Optional: DRY the repeated pgBoss.work mock with a tiny helper.

Three tests re-declare the same mock shape. Collapsing into a helper reduces duplication and keeps typing in one place.

Example helper you can place near the bottom:

function mockWorkToProcess(pgBoss: PgBoss, jobs: EnqueuedJob[] | (() => EnqueuedJob[])) {
  return jest.spyOn(pgBoss, "work").mockImplementation(async (_q: string, _o: Parameters<PgBoss["work"]>[1], processFn: (batch: EnqueuedJob[]) => Promise<void>) => {
    const batch = typeof jobs === "function" ? jobs() : jobs;
    await processFn(batch);
    return "work-id";
  });
}

Then in tests:

mockWorkToProcess(pgBoss, jobs);
// or
mockWorkToProcess(pgBoss, () => [{ id: "1", data: { message: "Job 1", userId: "user-1" } }]);

Also applies to: 142-168, 168-184

apps/api/src/core/providers/postgres.provider.ts (3)

33-33: Prefer Symbol-based DI tokens for consistency and collision safety

Elsewhere (e.g., APP_INITIALIZER/ON_APP_START) tokens are Symbols. Consider switching DB_HEALTHCHECK to a Symbol for consistency and to avoid accidental string token collisions.

Proposed change:

-export const DB_HEALTHCHECK: InjectionToken<DbHealthcheck> = "DB_HEALTHCHECK";
+export const DB_HEALTHCHECK: InjectionToken<DbHealthcheck> = Symbol("DB_HEALTHCHECK");

Please confirm all imports use this constant (no raw string-based injections). If any third-party wiring expects the string token, this would be a breaking change.


34-41: Nit: tighten container.register typing

While satisfies DbHealthcheck is good, adding the generic improves readability and editor tooling.

-container.register(DB_HEALTHCHECK, {
+container.register<DbHealthcheck>(DB_HEALTHCHECK, {
   useValue: {
     async ping() {
       await appClient.unsafe("SELECT 1");
     }
-  } satisfies DbHealthcheck
+  } satisfies DbHealthcheck
 });

56-64: Shutdown wiring: verify who calls dispose() and Disposable contract

Registering an APP_INITIALIZER that also exposes dispose: closeConnections is solid. Two checks:

  • Ensure your bootstrap (e.g., startServer) resolves this initializer and calls dispose() on shutdown signals.
  • Confirm the Disposable interface you import from tsyringe allows dispose(): Promise<void> (some versions define void | Promise<void>, others just void).

Minor simplification: you don’t need .then(() => undefined) to coerce the return type.

-export const closeConnections = async () => await Promise.all([migrationClient.end(), appClient.end()]).then(() => undefined);
+export const closeConnections = async () => {
+  await Promise.all([migrationClient.end(), appClient.end()]);
+}
apps/api/src/healthz/services/healthz/healthz.service.spec.ts (4)

10-12: Consider removing cacheEngine cleanup or centralize timer reset in afterEach

The service under test no longer uses cacheEngine; clearing it here may be unnecessary noise. If other specs rely on it, fine—otherwise consider dropping it. Also consider adding jest.useRealTimers() in afterEach to avoid timer leaks from tests that enable fake timers.

 afterEach(() => {
-  cacheEngine.clearAllKeyInCache();
+  cacheEngine.clearAllKeyInCache(); // remove if no longer needed
+  jest.useRealTimers(); // ensures clean timer state between tests
 });

87-111: Strengthen assertion: ensure exactly two error logs (and in any order)

You verify both structured error events; also assert they’re logged exactly twice to prevent duplicate/error spam.

 expect(dbHealthcheck.ping).toHaveBeenCalled();
 expect(jobQueueHealthcheck.ping).toHaveBeenCalled();
 expect(logger.error).toHaveBeenCalledWith({
   event: "POSTGRES_HEALTHCHECK_ERROR",
   error: dbError
 });
 expect(logger.error).toHaveBeenCalledWith({
   event: "JOBQUEUE_HEALTHCHECK_ERROR",
   error: jobQueueError
 });
+expect(logger.error).toHaveBeenCalledTimes(2);

153-198: Flaky-test guard (optional): pin the fake system time

If CI ever flakes on the TTL boundary assertions, consider pinning time at test start with jest.setSystemTime(new Date()). Not required now, but helps eliminate platform clock variance.


201-213: Align setup() with repo testing guidelines (optional)

Guidelines suggest setup accepts a single parameter with an inline type. You can keep defaults but allow per-test overrides without shared state.

-function setup() {
+function setup({ dbOk = true, jobsOk = true }: { dbOk?: boolean; jobsOk?: boolean } = {}) {
   const logger = mock<LoggerService>();
-  const dbHealthcheck = mock<DbHealthcheck>({ ping: jest.fn().mockResolvedValue(undefined) });
-  const jobQueueHealthcheck = mock<JobQueueHealthcheck>({ ping: jest.fn().mockResolvedValue(undefined) });
+  const dbHealthcheck = mock<DbHealthcheck>({
+    ping: jest.fn()[dbOk ? "mockResolvedValue" : "mockRejectedValue"](dbOk ? undefined : new Error("Postgres is not ready"))
+  });
+  const jobQueueHealthcheck = mock<JobQueueHealthcheck>({
+    ping: jest.fn()[jobsOk ? "mockResolvedValue" : "mockRejectedValue"](jobsOk ? undefined : new Error("JobsQueue is not ready"))
+  });
   const healthzService = new HealthzService(dbHealthcheck, jobQueueHealthcheck, logger);
   return { logger, service: healthzService, dbHealthcheck, jobQueueHealthcheck };
}
apps/api/src/healthz/services/healthz/healthz.service.ts (3)

39-49: Avoid type assertion by deriving keys explicitly (optional)

Relying on as keyof HealthzResult["data"] works but can mask drift if a new healthcheck name is added. Construct the result by key to retain type-safety.

-  private buildResult(results: boolean[]): HealthzResult {
-    return {
-      status: results.every(Boolean) ? "ok" : "error",
-      data: results.reduce(
-        (acc, result, index) => {
-          acc[this.healthchecks[index].name as keyof HealthzResult["data"]] = result;
-          return acc;
-        },
-        {} as HealthzResult["data"]
-      )
-    };
-  }
+  private buildResult(results: boolean[]): HealthzResult {
+    const data: HealthzResult["data"] = {
+      postgres: results[this.healthchecks.findIndex(h => h.name === "postgres")] ?? false,
+      jobQueue: results[this.healthchecks.findIndex(h => h.name === "jobQueue")] ?? false
+    };
+    return {
+      status: data.postgres && data.jobQueue ? "ok" : "error",
+      data
+    };
+  }

This keeps the current public shape and avoids unsafe casting.


61-74: Simplify the healthchecker type

Using a union inside Pick<...,"ping"> is equivalent to a structural { ping(): Promise<void> }. Simplify for readability.

-  constructor(
-    public readonly name: string,
-    private readonly healthchecker: Pick<DbHealthcheck | JobQueueHealthcheck, "ping">,
+  constructor(
+    public readonly name: string,
+    private readonly healthchecker: { ping(): Promise<void> },

100-107: Nit: annotate return type of check for clarity

Explicitly returning Promise<void> clarifies intent and helps future refactors.

-  private check() {
+  private check(): Promise<void> {
     this.inflightPing ??= this.healthchecker.ping().finally(() => {
       this.checkedAt = new Date();
       this.inflightPing = undefined;
     });
     return this.inflightPing;
   }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between e569b3c and e5884c8.

📒 Files selected for processing (5)
  • apps/api/src/core/providers/postgres.provider.ts (4 hunks)
  • apps/api/src/core/services/job-queue/job-queue.service.spec.ts (5 hunks)
  • apps/api/src/core/services/start-server/start-server.ts (1 hunks)
  • apps/api/src/healthz/services/healthz/healthz.service.spec.ts (1 hunks)
  • apps/api/src/healthz/services/healthz/healthz.service.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/api/src/core/services/start-server/start-server.ts
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

Never use type any or cast to type any. Always define the proper TypeScript types.

Files:

  • apps/api/src/healthz/services/healthz/healthz.service.ts
  • apps/api/src/core/providers/postgres.provider.ts
  • apps/api/src/healthz/services/healthz/healthz.service.spec.ts
  • apps/api/src/core/services/job-queue/job-queue.service.spec.ts
**/*.{js,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

**/*.{js,ts,tsx}: Never use deprecated methods from libraries.
Don't add unnecessary comments to the code

Files:

  • apps/api/src/healthz/services/healthz/healthz.service.ts
  • apps/api/src/core/providers/postgres.provider.ts
  • apps/api/src/healthz/services/healthz/healthz.service.spec.ts
  • apps/api/src/core/services/job-queue/job-queue.service.spec.ts
**/*.spec.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/no-jest-mock.mdc)

Don't use jest.mock() to mock dependencies in test files. Instead, use jest-mock-extended to create mocks and pass mocks as dependencies to the service under test.

**/*.spec.{ts,tsx}: Use setup function instead of beforeEach in test files
setup function must be at the bottom of the root describe block in test files
setup function creates an object under test and returns it
setup function should accept a single parameter with inline type definition
Don't use shared state in setup function
Don't specify return type of setup function

Files:

  • apps/api/src/healthz/services/healthz/healthz.service.spec.ts
  • apps/api/src/core/services/job-queue/job-queue.service.spec.ts
🧠 Learnings (1)
📚 Learning: 2025-07-21T08:24:24.269Z
Learnt from: CR
PR: akash-network/console#0
File: .cursor/rules/no-jest-mock.mdc:0-0
Timestamp: 2025-07-21T08:24:24.269Z
Learning: Applies to **/*.spec.{ts,tsx} : Don't use `jest.mock()` to mock dependencies in test files. Instead, use `jest-mock-extended` to create mocks and pass mocks as dependencies to the service under test.

Applied to files:

  • apps/api/src/core/services/job-queue/job-queue.service.spec.ts
🧬 Code graph analysis (4)
apps/api/src/healthz/services/healthz/healthz.service.ts (5)
apps/api/src/core/providers/logging.provider.ts (1)
  • injectable (7-8)
apps/api/src/healthz/controllers/healthz/healthz.controller.ts (1)
  • injectable (6-17)
apps/api/src/core/providers/postgres.provider.ts (2)
  • DB_HEALTHCHECK (33-33)
  • DbHealthcheck (30-32)
apps/api/src/core/providers/job-queue-healthcheck.ts (2)
  • JOB_QUEUE_HEALTHCHECK (9-9)
  • JobQueueHealthcheck (6-8)
packages/logging/src/servicies/logger/logger.service.ts (1)
  • error (107-109)
apps/api/src/core/providers/postgres.provider.ts (2)
apps/api/src/db/dbConnection.ts (2)
  • closeConnections (54-54)
  • ON_APP_START (58-60)
apps/api/src/core/providers/app-initializer.ts (3)
  • APP_INITIALIZER (3-3)
  • ON_APP_START (4-4)
  • AppInitializer (6-8)
apps/api/src/healthz/services/healthz/healthz.service.spec.ts (4)
apps/api/src/caching/helpers.ts (1)
  • cacheEngine (8-8)
packages/logging/src/servicies/logger/logger.service.ts (1)
  • error (107-109)
apps/api/src/core/providers/postgres.provider.ts (1)
  • DbHealthcheck (30-32)
apps/api/src/core/providers/job-queue-healthcheck.ts (1)
  • JobQueueHealthcheck (6-8)
apps/api/src/core/services/job-queue/job-queue.service.spec.ts (2)
apps/api/src/core/services/job-queue/job-queue.service.ts (1)
  • setup (135-142)
packages/logging/src/servicies/logger/logger.service.ts (1)
  • error (107-109)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (12)
  • GitHub Check: validate (apps/provider-proxy) / validate-unsafe
  • GitHub Check: validate (apps/api) / validate-unsafe
  • GitHub Check: test-build
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: test-build
  • GitHub Check: test-build
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: test-build
🔇 Additional comments (8)
apps/api/src/core/services/job-queue/job-queue.service.spec.ts (5)

128-135: LGTM: updated expectations for multi-worker + batchSize=1 contract.

The assertions align with the new per-job logging (JOB_STARTED/JOB_DONE), 5 workers, and single-job batches.


156-166: Good negative-path coverage and logging assertions.

Capturing the rejection via Promise.allSettled and asserting JOB_FAILED payload (including jobId and error) is solid.


186-196: LGTM: setup() lifecycle test matches service implementation.

Verifies JOB_QUEUE_STARTING/STARTED, error wiring, and start invocation; consistent with JobQueueService.setup().


210-217: LGTM: PgBoss error handler wiring is validated.

The test spies on on('error', ...) and asserts JOB_QUEUE_ERROR logging with the original error object.


230-240: LGTM: ping() test is pragmatic given pg-boss typings.

Using @ts-expect-error with a reference to the upstream typing gap is acceptable here and keeps the test intention clear.

apps/api/src/healthz/services/healthz/healthz.service.spec.ts (1)

113-137: TTL and inflight assertions look solid

Good use of fake timers and call-count checks to validate caching and deduplication. This should guard against regressions in the TTL logic.

apps/api/src/healthz/services/healthz/healthz.service.ts (2)

12-27: Constructor wiring and TTL setup look good

Clear separation of concerns: injecting healthcheck providers, setting context, and registering TTL-configured probes. Using one-minute TTL is a reasonable default.


75-99: Healthy fallback logic and caching semantics are well thought-out

  • First failure after a success is tolerated until TTL expiry.
  • Failures before any success are surfaced immediately.
  • ignoreCache forces a fresh probe and strict error reporting.

This aligns with the liveness/readiness expectations encoded in tests.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
apps/api/src/core/config/env.config.ts (1)

13-13: Validate schema name to a safe Postgres identifier; optionally normalize case.

To avoid accidental quoted identifiers and runtime surprises, enforce a conservative identifier pattern. Optionally lowercase to match unquoted identifier semantics.

Option A (validation only):

-    POSTGRES_BACKGROUND_JOBS_SCHEMA: z.string().optional().default("pgboss"),
+    POSTGRES_BACKGROUND_JOBS_SCHEMA: z
+      .string()
+      .optional()
+      .default("pgboss")
+      .refine(s => /^[a-z_][a-z0-9_]*$/.test(s), {
+        message: "POSTGRES_BACKGROUND_JOBS_SCHEMA must match ^[a-z_][a-z0-9_]*$",
+      }),

Option B (also normalize to lowercase; use with care if someone intends a quoted/mixed-case schema):

-    POSTGRES_BACKGROUND_JOBS_SCHEMA: z.string().optional().default("pgboss"),
+    POSTGRES_BACKGROUND_JOBS_SCHEMA: z
+      .string()
+      .optional()
+      .transform(v => (v ?? "").trim())
+      .transform(s => (s.length ? s.toLowerCase() : "pgboss"))
+      .refine(s => /^[a-z_][a-z0-9_]*$/.test(s), {
+        message: "POSTGRES_BACKGROUND_JOBS_SCHEMA must match ^[a-z_][a-z0-9_]*$",
+      }),
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between e5884c8 and bd610d9.

📒 Files selected for processing (1)
  • apps/api/src/core/config/env.config.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

Never use type any or cast to type any. Always define the proper TypeScript types.

Files:

  • apps/api/src/core/config/env.config.ts
**/*.{js,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

**/*.{js,ts,tsx}: Never use deprecated methods from libraries.
Don't add unnecessary comments to the code

Files:

  • apps/api/src/core/config/env.config.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (13)
  • GitHub Check: validate (apps/notifications) / validate-unsafe
  • GitHub Check: validate (apps/provider-proxy) / validate-unsafe
  • GitHub Check: validate (apps/indexer) / validate-unsafe
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: test-build
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: test-build
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: test-build
  • GitHub Check: Validate local packages

await jobQueueManager.start();
await jobQueueManager.setup();
await jobQueueManager.registerHandlers([
// keep new lines
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

non-blocking. isn't there a way to configure prettier for this?

Copy link
Contributor Author

@stalniy stalniy Aug 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No to my knowledge, prettier likes people to struggle. Their philosophy is either accept what it does or don't use it 😄 because formatting is not important, it just must be universal to eliminate stylistic holy wars. We could play with line width but it will break other things.

Refs:

We just need to switch to https://eslint.style/ at some point. I've already used it in chain sdk project, as a replacement to prettier.

Copy link
Contributor

@ygrishajev ygrishajev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

amazing ❤️ 💪

@stalniy stalniy force-pushed the feat/background-jobs branch from 8eb2d5e to 22d400f Compare August 26, 2025 04:44
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
apps/api/src/core/services/shutdown-server/shutdown-server.spec.ts (1)

7-101: Adopt the required setup() pattern in spec files (reduce duplication, align with repo testing guidelines)

Per project guidelines for **/*.spec.ts, tests should use a setup function (at the bottom of the root describe) to build SUT/dependencies, avoid shared state, and return the object under test. This also eliminates duplicated mock wiring across tests and makes behavior toggles explicit (listening vs not, close callback variations, onShutdown rejection, etc.).

Apply this refactor within the describe block:

 describe(shutdownServer.name, () => {
-  it("closes the server ", async () => {
-    const server = mock<ServerType>({
-      close: jest.fn().mockImplementation(cb => cb())
-    });
-    const appLogger = mock<Logger>();
-    const onShutdown = jest.fn();
-
-    await shutdownServer(server, appLogger, onShutdown);
-
-    expect(server.close).toHaveBeenCalledTimes(1);
-    expect(onShutdown).toHaveBeenCalledTimes(1);
-    expect(appLogger.error).not.toHaveBeenCalled();
-  });
+  it("closes the server", async () => {
+    const { server, appLogger, onShutdown, run } = setup({ listening: true, closeBehavior: "cb-ok" });
+    await run();
+    expect(server.close).toHaveBeenCalledTimes(1);
+    expect(onShutdown).toHaveBeenCalledTimes(1);
+    expect(appLogger.error).not.toHaveBeenCalled();
+  });

   it("logs error if server close fails", async () => {
     const error = new Error("Failed to close server");
-    const server = mock<ServerType>({
-      close: jest.fn().mockImplementation(cb => cb(error))
-    });
-    const appLogger = mock<Logger>();
-    const onShutdown = jest.fn();
-
-    await shutdownServer(server, appLogger, onShutdown);
-
-    expect(appLogger.error).toHaveBeenCalledWith({
-      event: "SERVER_CLOSE_ERROR",
-      error
-    });
+    const { appLogger, onShutdown, run } = setup({ listening: true, closeBehavior: "cb-error", closeError: error });
+    await run();
+    expect(appLogger.error).toHaveBeenCalledWith(expect.objectContaining({ event: "SERVER_CLOSE_ERROR", error }));
     expect(onShutdown).toHaveBeenCalled();
   });

   it("logs error if server close throws", async () => {
     const error = new Error("Failed to close server");
-    const server = mock<ServerType>({
-      close: jest.fn().mockImplementation(() => {
-        throw error;
-      }) as jest.Mock
-    });
-    const appLogger = mock<Logger>();
-    const onShutdown = jest.fn();
-
-    await shutdownServer(server, appLogger, onShutdown);
-
-    expect(appLogger.error).toHaveBeenCalledWith({
-      event: "SERVER_CLOSE_ERROR",
-      error
-    });
+    const { appLogger, onShutdown, run } = setup({ listening: true, closeBehavior: "throws", closeError: error });
+    await run();
+    expect(appLogger.error).toHaveBeenCalledWith(expect.objectContaining({ event: "SERVER_CLOSE_ERROR", error }));
     expect(onShutdown).toHaveBeenCalled();
   });

   it("logs error if onShutdown callback fails", async () => {
     const error = new Error("Failed to dispose container");
-    const server = mock<ServerType>({
-      close: jest.fn().mockImplementation(cb => cb())
-    });
-    const appLogger = mock<Logger>();
-    const onShutdown = jest.fn().mockRejectedValue(error);
-
-    await shutdownServer(server, appLogger, onShutdown);
-
-    expect(appLogger.error).toHaveBeenCalledWith({
-      event: "ON_SHUTDOWN_ERROR",
-      error
-    });
+    const { appLogger, run } = setup({ listening: true, closeBehavior: "cb-ok", onShutdownRejected: true, onShutdownError: error });
+    await run();
+    expect(appLogger.error).toHaveBeenCalledWith(expect.objectContaining({ event: "ON_SHUTDOWN_ERROR", error }));
   });

   it("calls shutdown directly if server is not listening", async () => {
-    const server = mock<ServerType>({
-      listening: false,
-      close: jest.fn()
-    });
-    const appLogger = mock<Logger>();
-    const onShutdown = jest.fn();
-
-    await shutdownServer(server, appLogger, onShutdown);
-
-    expect(server.close).not.toHaveBeenCalled();
-    expect(onShutdown).toHaveBeenCalled();
+    const { server, onShutdown, run } = setup({ listening: false });
+    await run();
+    expect(server.close).not.toHaveBeenCalled();
+    expect(onShutdown).toHaveBeenCalled();
   });

   it("calls server.close if server is listening", async () => {
-    const server = mock<ServerType>({
-      listening: true,
-      close: jest.fn().mockImplementation(cb => cb())
-    });
-    const appLogger = mock<Logger>();
-    const onShutdown = jest.fn();
-
-    await shutdownServer(server, appLogger, onShutdown);
-
-    expect(server.close).toHaveBeenCalled();
-    expect(onShutdown).toHaveBeenCalled();
+    const { server, onShutdown, run } = setup({ listening: true, closeBehavior: "cb-ok" });
+    await run();
+    expect(server.close).toHaveBeenCalled();
+    expect(onShutdown).toHaveBeenCalled();
   });
-});
+  
+  // setup must be at the bottom of root describe (hoisted in JS, used above)
+  function setup({
+    listening = true,
+    closeBehavior = "cb-ok",
+    closeError,
+    onShutdownRejected = false,
+    onShutdownError
+  }: {
+    listening?: boolean;
+    closeBehavior?: "cb-ok" | "cb-error" | "throws";
+    closeError?: Error;
+    onShutdownRejected?: boolean;
+    onShutdownError?: Error;
+  }) {
+    const appLogger = mock<Logger>();
+    const onShutdown = onShutdownRejected
+      ? jest.fn().mockRejectedValue(onShutdownError ?? new Error("onShutdown failed"))
+      : jest.fn().mockResolvedValue(undefined);
+
+    const closeCbOk = jest.fn().mockImplementation((cb?: (err?: Error) => void) => setImmediate(() => cb?.()));
+    const closeCbErr = jest.fn().mockImplementation((cb?: (err?: Error) => void) =>
+      setImmediate(() => cb?.(closeError ?? new Error("Failed to close server")))
+    );
+    const closeThrows = jest
+      .fn<(cb: (err?: Error) => void) => void>()
+      .mockImplementation(() => {
+        throw closeError ?? new Error("Failed to close server");
+      });
+
+    const server = mock<ServerType>({
+      listening,
+      close: closeBehavior === "cb-error" ? closeCbErr : closeBehavior === "throws" ? closeThrows : closeCbOk
+    });
+
+    const run = () => shutdownServer(server, appLogger, onShutdown);
+    return { server, appLogger, onShutdown, run };
+  }
+});
apps/api/src/core/services/job-queue/job-queue.service.spec.ts (1)

116-119: Remove any from pgBoss.work mock; use library-derived types.

Tests must not use any per guidelines. Type the options from the actual PgBoss overload and the job shape from the TestJob data.

Apply this diff:

-jest.spyOn(pgBoss, "work").mockImplementation(async (queueName: string, options: any, processFn: (jobs: any[]) => Promise<void>) => {
-  await processFn(jobs);
-  return "work-id";
-});
+jest
+  .spyOn(pgBoss, "work")
+  .mockImplementation(
+    async (
+      _queueName: string,
+      _options: Parameters<PgBoss["work"]>[1],
+      processFn: (jobs: Array<{ id: string; data: TestJob["data"] }>) => Promise<void>
+    ) => {
+      await processFn(jobs);
+      return "work-id";
+    }
+  );

Optionally, DRY the job type:

type EnqueuedJob = { id: string; data: TestJob["data"] };

and use EnqueuedJob[] in the mock signature.

apps/api/src/console.ts (3)

42-42: Fix concurrency option default — Zod default is never reached when the flag is omitted

Commander only calls the parser function when the option is provided. Using Zod’s default inside the parser won’t apply when users omit -c, leaving concurrency as undefined downstream.

Set the default via Commander’s defaultValue argument and keep Zod for coercion/validation.

Apply these diffs:

-  .option("-c, --concurrency <number>", "How many wallets are processed concurrently", value => z.number({ coerce: true }).optional().default(10).parse(value))
+  .option(
+    "-c, --concurrency <number>",
+    "How many wallets are processed concurrently",
+    v => z.coerce.number().int().positive().parse(v),
+    10
+  )
-  .option("-c, --concurrency <number>", "How many wallets is processed concurrently", value => z.number({ coerce: true }).optional().default(10).parse(value))
+  .option(
+    "-c, --concurrency <number>",
+    "How many wallets are processed concurrently",
+    v => z.coerce.number().int().positive().parse(v),
+    10
+  )
-  .option("-c, --concurrency <number>", "How many wallets are processed concurrently", value => z.number({ coerce: true }).optional().default(10).parse(value))
+  .option(
+    "-c, --concurrency <number>",
+    "How many wallets are processed concurrently",
+    v => z.coerce.number().int().positive().parse(v),
+    10
+  )
-  .option("-c, --concurrency <number>", "How many wallets are processed concurrently", value => z.number({ coerce: true }).optional().default(10).parse(value))
+  .option(
+    "-c, --concurrency <number>",
+    "How many wallets are processed concurrently",
+    v => z.coerce.number().int().positive().parse(v),
+    10
+  )
-  .option("-c, --concurrency <number>", "How many users are processed concurrently", value => z.number({ coerce: true }).optional().default(10).parse(value))
+  .option(
+    "-c, --concurrency <number>",
+    "How many users are processed concurrently",
+    v => z.coerce.number().int().positive().parse(v),
+    10
+  )

Also applies to: 53-53, 63-63, 73-73, 95-95


71-80: Make provider address required and validate non-empty input

This command appears to require a provider address. As written, the Zod parser is only invoked when the option is present, so the command can run with provider = undefined.

Apply this diff:

-  .option("-p, --provider <string>", "Provider address", value => z.string().parse(value))
+  .requiredOption(
+    "-p, --provider <string>",
+    "Provider address",
+    v => z.string().min(1, "Provider address is required").parse(v)
+  )

(Optional) If you want to validate Akash-style bech32 addresses, add a stricter regex/refinement later.


105-131: End the OpenTelemetry span; current code leaks spans

tracer.startSpan(name) is called but the span is never ended, which will skew traces and can leak memory/resources. Wrap with a finally and call span.end() after the handler completes. While here, guard APP_INITIALIZER calls to avoid calling undefined.

Apply this diff:

-async function executeCliHandler(name: string, handler: () => Promise<unknown>, options?: { type?: "action" | "daemon" }) {
-  await context.with(trace.setSpan(context.active(), tracer.startSpan(name)), async () => {
-    logger.info({ event: "COMMAND_START", name });
-    // eslint-disable-next-line @typescript-eslint/no-var-requires
-    const { migratePG } = require("./core/providers/postgres.provider");
-
-    try {
-      await Promise.all([migratePG(), chainDb.authenticate(), ...container.resolveAll(APP_INITIALIZER).map(initializer => initializer[ON_APP_START]())]);
-
-      const result = await handler();
-
-      if (result && result instanceof Err) {
-        logger.error({ event: "COMMAND_ERROR", name, result: result.val });
-        process.exitCode = 1;
-      } else {
-        logger.info({ event: "COMMAND_END", name });
-      }
-    } catch (error) {
-      logger.error({ event: "COMMAND_ERROR", name, error });
-      process.exitCode = 1;
-    } finally {
-      if (options?.type !== "daemon") {
-        await shutdown();
-      }
-    }
-  });
-}
+async function executeCliHandler(
+  name: string,
+  handler: () => Promise<unknown>,
+  options?: { type?: "action" | "daemon" },
+) {
+  const span = tracer.startSpan(name);
+  try {
+    await context.with(trace.setSpan(context.active(), span), async () => {
+      logger.info({ event: "COMMAND_START", name });
+      // eslint-disable-next-line @typescript-eslint/no-var-requires
+      const { migratePG } = require("./core/providers/postgres.provider");
+
+      try {
+        await Promise.all([
+          migratePG(),
+          chainDb.authenticate(),
+          ...container.resolveAll(APP_INITIALIZER).map(initializer => initializer[ON_APP_START]?.()),
+        ]);
+
+        const result = await handler();
+
+        if (result && result instanceof Err) {
+          logger.error({ event: "COMMAND_ERROR", name, result: result.val });
+          process.exitCode = 1;
+        } else {
+          logger.info({ event: "COMMAND_END", name });
+        }
+      } catch (error) {
+        logger.error({ event: "COMMAND_ERROR", name, error });
+        process.exitCode = 1;
+      } finally {
+        if (options?.type !== "daemon") {
+          await shutdown();
+        }
+      }
+    });
+  } finally {
+    span.end();
+  }
+}
♻️ Duplicate comments (7)
apps/api/env/.env.unit.test (2)

28-28: Align docker-compose host port to 3090 (duplicate of prior note) and re-verify for lingering 3081 references

A previous review found a lingering host port mapping '3081:3001' in packages/docker/docker-compose.prod.yml and advised updating dev compose too. Please ensure all compose files and local scripts map host 3090 for the Notifications container.

Run the script below to surface stragglers and context around usages:

#!/bin/bash
set -euo pipefail

echo "== Compose mappings still using 3081:3001 =="
rg -nP -S --glob '!**/node_modules/**' --glob '!**/dist/**' --glob '!**/build/**' "(^|\s)3081:3001\b" -g '*compose*.yml' -g '*compose*.yaml' -n -C2

echo
echo "== Any hardcoded 3081 references across repo =="
rg -nP -S --glob '!**/node_modules/**' --glob '!**/dist/**' --glob '!**/build/**' '\b3081\b' -C2

echo
echo "== Usages of NOTIFICATIONS_API_BASE_URL (to confirm 3090 in tests/mocks) =="
rg -nP --glob '!**/node_modules/**' --glob '!**/dist/**' --glob '!**/build/**' '\bNOTIFICATIONS_API_BASE_URL\b' -C3

28-28: dotenv-linter UnorderedKey warning: optional reordering

dotenv-linter flags NOTIFICATIONS_API_BASE_URL as out of order relative to POSTGRES_DB_URI. If you keep lint clean, move NOTIFICATIONS_API_BASE_URL earlier (before POSTGRES_DB_URI) or adopt a consistent alphabetical/grouped ordering for keys. Otherwise, consider relaxing that rule for test env files.

apps/api/src/core/services/shutdown-server/shutdown-server.spec.ts (1)

1-101: Sanity-check the repo for leftover CONTAINER_DISPOSE_ERROR usages

To ensure consistency across test and implementation code, run this quick audit. Similar check was suggested earlier; keeping it here for convenience.

#!/bin/bash
set -euo pipefail

echo "Occurrences of old event key (should be 0):"
rg -nP 'CONTAINER_DISPOSE_ERROR' -C2 || true

echo
echo "ON_SHUTDOWN_ERROR call sites for context:"
rg -nP --type=ts -C2 'ON_SHUTDOWN_ERROR' || true
apps/api/src/core/services/job-queue/job-queue.service.spec.ts (2)

150-153: Avoid any in pgBoss.work mock (failure path).

Mirror the typed mock used above for consistency and to satisfy the no-any rule.

Apply this diff:

-jest.spyOn(pgBoss, "work").mockImplementation(async (queueName: string, options: any, processFn: (jobs: any[]) => Promise<void>) => {
-  await processFn(jobs);
-  return "work-id";
-});
+jest
+  .spyOn(pgBoss, "work")
+  .mockImplementation(
+    async (
+      _queueName: string,
+      _options: Parameters<PgBoss["work"]>[1],
+      processFn: (jobs: Array<{ id: string; data: TestJob["data"] }>) => Promise<void>
+    ) => {
+      await processFn(jobs);
+      return "work-id";
+    }
+  );

173-176: Avoid any in pgBoss.work mock (default-options path).

Keep mocks consistently typed and compliant with the no-any guideline.

Apply this diff:

-jest.spyOn(pgBoss, "work").mockImplementation(async (queueName: string, options: any, processFn: (jobs: any[]) => Promise<void>) => {
-  await processFn([{ id: "1", data: { message: "Job 1", userId: "user-1" } }]);
-  return "work-id";
-});
+jest
+  .spyOn(pgBoss, "work")
+  .mockImplementation(
+    async (
+      _queueName: string,
+      _options: Parameters<PgBoss["work"]>[1],
+      processFn: (jobs: Array<{ id: string; data: TestJob["data"] }>) => Promise<void>
+    ) => {
+      await processFn([{ id: "1", data: { message: "Job 1", userId: "user-1" } }]);
+      return "work-id";
+    }
+  );
apps/api/src/rest-app.ts (1)

194-194: Alias export initDb is helpful; ensure all call sites use it (no direct connectUsingSequelize imports).

Keeps the public surface small and swappable. Please verify remaining call sites don’t bypass the alias.

Run:

#!/bin/bash
# Find any lingering direct imports/usages of connectUsingSequelize outside rest-app
rg -nP --type ts -C2 '\bconnectUsingSequelize\b' | rg -v 'src/rest-app\.ts'
apps/api/src/core/providers/index.ts (1)

5-8: Barrel looks good; re-exports provide a clean public surface (plus job-queue-healthcheck)

The exposed tokens/types look coherent for DI. Please ensure no modules still import "./postgres.provider" directly, which would bypass the barrel and risk initialization-order discrepancies.

Run to spot direct imports and potential cycles:

#!/bin/bash
# 1) No direct postgres.provider imports outside the barrel
rg -nP --type ts -C2 "(from|import)\\s+['\"][^'\"]*core/providers/postgres\\.provider['\"]"

# 2) Prefer barrel usage everywhere
rg -nP --type ts -C2 "(from|import)\\s+['\"][^'\"]*core/providers(?!/index)['\"]"

# 3) Sanity-check that providers index is imported before any healthcheck resolution
rg -nP --type ts -C2 "\\b(DB_HEALTHCHECK|JOB_QUEUE_HEALTHCHECK)\\b"
🧹 Nitpick comments (33)
apps/api/src/core/services/index.ts (2)

2-2: Prefer named re-exports over export * to avoid leaking internals

Star re-exports can unintentionally widen the public API (including helper types/constants). Consider re-exporting only what you intend to support long-term.

Apply this diff to narrow the surface:

-export * from "@src/core/services/start-server/start-server";
+export { startServer } from "@src/core/services/start-server/start-server";

If you need to expose types, add explicit type-only re-exports next to this line.


2-2: Verification: no direct circular deps; alias mapping present for build

  • apps/api/src/core/services/start-server/start-server.ts contains no imports from @src/core or @src/core/services, so no immediate re-export cycle detected.
  • Duplicate re-export of start-server found in two barrels:
    • apps/api/src/core/index.ts:6
    • apps/api/src/core/services/index.ts:2
      Consider removing one to avoid redundancy (e.g. drop the root-level export in apps/api/src/core/index.ts).
  • The @src/* alias is correctly configured in apps/api/tsconfig.build.json (baseUrl: ".", paths: ["./src/*"]), so your package build will resolve it.
    • Note: apps/api/tsconfig.json (editor config) has no paths mapping—add the following if you need IDE support:
       {
         "compilerOptions": {
      +    "baseUrl": ".",
      +    "paths": {
      +      "@src/*": ["./src/*"]
      +    }
         }
       }

No blocking issues found; duplicate export cleanup and editor-side alias mapping are optional refinements.

apps/api/src/core/services/shutdown-server/shutdown-server.spec.ts (4)

41-45: Avoid unsafe type cast; type your jest.fn instead of as jest.Mock

Casting to jest.Mock hides type mismatches and can mask regressions. Prefer a typed jest.fn signature that matches ServerType["close"].

-    const server = mock<ServerType>({
-      close: jest.fn().mockImplementation(() => {
-        throw error;
-      }) as jest.Mock
-    });
+    const server = mock<ServerType>({
+      close: jest
+        .fn<(cb: (err?: Error) => void) => void>()
+        .mockImplementation(() => {
+          throw error;
+        })
+    });

32-36: Make logger expectations resilient to extra log fields

Use expect.objectContaining so future additions to the log payload (e.g., requestId, tags) don’t break tests.

-    expect(appLogger.error).toHaveBeenCalledWith({
-      event: "SERVER_CLOSE_ERROR",
-      error
-    });
+    expect(appLogger.error).toHaveBeenCalledWith(expect.objectContaining({ event: "SERVER_CLOSE_ERROR", error }));
-    expect(appLogger.error).toHaveBeenCalledWith({
-      event: "SERVER_CLOSE_ERROR",
-      error
-    });
+    expect(appLogger.error).toHaveBeenCalledWith(expect.objectContaining({ event: "SERVER_CLOSE_ERROR", error }));
-    expect(appLogger.error).toHaveBeenCalledWith({
-      event: "ON_SHUTDOWN_ERROR",
-      error
-    });
+    expect(appLogger.error).toHaveBeenCalledWith(expect.objectContaining({ event: "ON_SHUTDOWN_ERROR", error }));

Also applies to: 51-55, 68-71


8-8: Remove trailing whitespace in test name

Minor nit; keeps diffs clean and avoids noisy lint failures.

-it("closes the server ", async () => {
+it("closes the server", async () => {

10-11: Simulate async server.close callback scheduling

In Node, server.close(cb) invokes the callback on a future tick. Scheduling with setImmediate (or setTimeout(…, 0)) makes tests closer to real behavior and catches accidental sync-only code paths.

If you don’t adopt the setup() refactor yet, tweak the inline fakes:

-      close: jest.fn().mockImplementation(cb => cb())
+      close: jest.fn().mockImplementation(cb => setImmediate(() => cb()))
-      close: jest.fn().mockImplementation(cb => cb())
+      close: jest.fn().mockImplementation(cb => setImmediate(() => cb()))

Also applies to: 91-92

apps/api/src/core/services/job-queue/job-queue.service.spec.ts (3)

156-160: Simplify error assertion; avoid Promise.allSettled for a single promise.

Use Jest’s rejects helper and assert the logged error directly. This keeps the test tighter and avoids type narrowing on PromiseSettledResult.

Apply this diff:

-const [result] = await Promise.allSettled([service.startWorkers({ batchSize: 1 })]);
-
-expect(result.status).toBe("rejected");
-expect((result as PromiseRejectedResult).reason).toBe(error);
+await expect(service.startWorkers({ batchSize: 1 })).rejects.toBe(error);
@@
-expect(logger.error).toHaveBeenCalledWith({
-  event: "JOB_FAILED",
-  jobId: jobs[0].id,
-  error: (result as PromiseRejectedResult).reason
-});
+expect(logger.error).toHaveBeenCalledWith({
+  event: "JOB_FAILED",
+  jobId: jobs[0].id,
+  error
+});

Also applies to: 164-164


233-239: Remove ts-expect-error by typing the getDb mock once.

Avoid repeating ts-expect-error; use a minimal local type for the DB handle and assert on the mock function directly.

Apply this diff:

-// @ts-expect-error - getDb is not typed, see https://github.com/timgit/pg-boss/issues/552#issuecomment-3213043039
-jest.spyOn(pgBoss, "getDb").mockReturnValue({ executeSql: jest.fn().mockResolvedValue(undefined) });
-await service.ping();
-
-// @ts-expect-error - getDb is not typed, see https://github.com/timgit/pg-boss/issues/552#issuecomment-3213043039
-expect(pgBoss.getDb().executeSql).toHaveBeenCalledWith("SELECT 1");
+type PGBossDb = { executeSql: (sql: string) => Promise<unknown> };
+const executeSql = jest.fn().mockResolvedValue(undefined);
+jest.spyOn(pgBoss, "getDb").mockReturnValue({ executeSql } as unknown as PGBossDb);
+await service.ping();
+expect(executeSql).toHaveBeenCalledWith("SELECT 1");

259-261: Minor: simplify pgBoss injection check.

Object.hasOwn is fine, but in guards like this the in operator is clearer.

Apply this diff:

-const service = new JobQueueService(mocks.logger, mocks.coreConfig, input && Object.hasOwn(input, "pgBoss") ? input?.pgBoss : mocks.pgBoss);
+const service = new JobQueueService(mocks.logger, mocks.coreConfig, input && "pgBoss" in input ? input.pgBoss : mocks.pgBoss);
apps/api/src/console.ts (6)

83-84: Polish command description (grammar/casing)

Minor wording improvement.

-  .description("Create deployments for every gpu models to get up to date pricing information")
+  .description("Create deployments for every GPU model to get up-to-date pricing information")

51-53: Polish description wording

Minor wording improvement.

-  .description("Close deployments without leases created at least 10min ago")
+  .description("Close deployments without leases created at least 10 min ago")

91-96: Avoid resolving DI at module load for help text (defers side effects)

Resolving UserConfigService at module scope may trigger DI construction before APP_INITIALIZER runs. If UserConfigService has any I/O, this can fail or slow --help. Consider using a static description and logging the actual value at runtime, or compute the description lazily inside the action.

If you want to avoid module-scope DI while keeping a dynamic help string, an acceptable compromise is a static description:

-  .description(`Remove users that have been inactive for ${userConfig.get("STALE_ANONYMOUS_USERS_LIVE_IN_DAYS")} days`)
+  .description("Remove users that have been inactive for the configured number of days")

…and log the resolved days at command start.


133-138: Verify tsyringe supports container.dispose() and make shutdown more robust

Two points:

  • Some tsyringe versions don’t expose container.dispose(). If unavailable, prefer container.reset() or container.clearInstances().
  • Use Promise.allSettled so one failure (e.g., DB close) doesn’t prevent other cleanups.

Proposed improvement (after verifying available API):

-  await Promise.all([closeConnections(), chainDb.close(), container.dispose()]);
+  await Promise.allSettled([closeConnections(), chainDb.close(), container.dispose?.() ?? Promise.resolve()]);

If dispose is not available, replace with container.reset() (synchronous) or container.clearInstances() as appropriate.


141-141: Prefer parseAsync() for async actions

Your actions are async and you do internal error handling, but using Commander’s async entry improves consistency and future-proofs error propagation.

-program.parse();
+void program.parseAsync();

53-53: Minor grammar nit

“wallets are processed” reads better.

-  .option("-c, --concurrency <number>", "How many wallets is processed concurrently", …
+  .option("-c, --concurrency <number>", "How many wallets are processed concurrently", …
apps/api/src/core/providers/job-queue-healthcheck.ts (1)

9-17: Prefer a Symbol token and the project’s root DI container to avoid token collisions and container drift.

  • Use Symbol("JOB_QUEUE_HEALTHCHECK") for the token (consistent with APP_INITIALIZER) to prevent accidental string token clashes.
  • Register against the same root DI container used by startServer (rootContainer), not the global tsyringe container, to avoid surprises when an alternate container is supplied in tests/server bootstrap.

Apply:

- import type { InjectionToken } from "tsyringe";
- import { container } from "tsyringe";
+ import type { InjectionToken } from "tsyringe";
+ import { rootContainer as container } from "@src/core/di";
@@
-export const JOB_QUEUE_HEALTHCHECK: InjectionToken<JobQueueHealthcheck> = "JOB_QUEUE_HEALTHCHECK";
+export const JOB_QUEUE_HEALTHCHECK: InjectionToken<JobQueueHealthcheck> = Symbol("JOB_QUEUE_HEALTHCHECK");
 container.register(JOB_QUEUE_HEALTHCHECK, {
   useFactory: c => {
     const jobQueueService = c.resolve(JobQueueService);
     return {
       ping: () => jobQueueService.ping()
     };
   }
 });
apps/api/src/db/dbConnection.ts (3)

55-63: Register DB shutdown under APP_SHUTDOWN as well to guarantee pool closure in all shutdown paths.

Relying solely on the initializer instance’s Disposable.dispose being called by container.dispose can be fragile if the initializer wasn’t resolved or if shutdown logic changes. Also exposing closeConnections via APP_SHUTDOWN makes intent explicit and aligns with the broader shutdown orchestration.

Proposed change:

- import { APP_INITIALIZER, ON_APP_START } from "@src/core/providers/app-initializer";
+ import { APP_INITIALIZER, APP_SHUTDOWN, ON_APP_START } from "@src/core/providers/app-initializer";
@@
 container.register(APP_INITIALIZER, {
   useFactory: c =>
     ({
       async [ON_APP_START]() {
         await connectUsingSequelize(c.resolve(LoggerService));
       },
       dispose: closeConnections
     }) satisfies AppInitializer & Disposable
 });
+
+// Ensure DB pools are closed even if initializers weren’t resolved or disposal strategy changes
+container.register(APP_SHUTDOWN, {
+  useValue: {
+    dispose: closeConnections
+  }
+});

If you adopt this, please ensure start-server invokes APP_SHUTDOWN disposers before container.dispose(). If that enhancement already landed in this PR, we’re covered. Otherwise, I can provide a small patch for start-server.ts.


65-70: Docblock overpromises vs current implementation.

The comment lists “Populate db”, “Create backups per version”, “Load from backup” which aren’t implemented here. Tighten the doc to reflect actual behavior to avoid misleading readers.

-/**
- * Initialize database schema
- * Populate db
- * Create backups per version
- * Load from backup if exists for current version
- */
+/**
+ * Establish connections to chainDb and userDb, then sync user schema.
+ * NOTE: Seeding/backups are handled elsewhere and are not part of this function.
+ */

71-83: Optionally gate syncUserSchema behind config to avoid unintended DDL in prod.

If Drizzle migrations (migratePG) are the primary schema mechanism in production, consider making Sequelize sync opt-in via config. Keeps tests/dev happy while preventing accidental DDL in prod.

Example:

 export async function connectUsingSequelize(logger: LoggerService = LoggerService.forContext("DB")): Promise<void> {
   logger.debug(`Connecting to chain database (${chainDb.config.host}/${chainDb.config.database})...`);
   await chainDb.authenticate();
   logger.debug("Connection has been established successfully.");

   logger.debug(`Connecting to user database (${userDb.config.host}/${userDb.config.database})...`);
   await userDb.authenticate();
   logger.debug("Connection has been established successfully.");

-  logger.debug("Sync user schema...");
-  await syncUserSchema();
-  logger.debug("User schema synced.");
+  const shouldSync = coreConfig.get("SEQUELIZE_SYNC_ON_START") === "true";
+  if (shouldSync) {
+    logger.debug("Sync user schema...");
+    await syncUserSchema();
+    logger.debug("User schema synced.");
+  } else {
+    logger.debug("Skipping user schema sync (SEQUELIZE_SYNC_ON_START !== 'true').");
+  }
 }

If some tests rely on sync always running, we can set SEQUELIZE_SYNC_ON_START="true" in the test env.

apps/api/src/rest-app.ts (2)

186-192: Add a disposer for Scheduler to avoid dangling timers on shutdown.

startServer now coordinates graceful shutdown; add a dispose hook so timers/intervals are cleared even if the server fails to start or exits quickly.

- container.register(APP_INITIALIZER, {
-   useValue: {
-     async [ON_APP_START]() {
-       scheduler.start();
-     }
-   } satisfies AppInitializer
- });
+import type { Disposable } from "tsyringe";
+container.register(APP_INITIALIZER, {
+  useValue: {
+    async [ON_APP_START]() {
+      scheduler.start();
+    },
+    // Prefer an explicit stop if available
+    dispose: () => (scheduler.stop?.() ?? scheduler.dispose?.())
+  } satisfies AppInitializer & Disposable
+});

If Scheduler lacks stop/dispose, I recommend adding one. Otherwise we risk leaked intervals in long-lived processes or tests.


196-201: bootstrap: consider returning the server instance (optional).

Returning ServerType | undefined helps callers/tests to close or introspect the server without re-wiring shutdown hooks.

-export async function bootstrap(port: number) {
-  await startServer(appHono, LoggerService.forContext("APP"), process, {
-    port,
-    beforeStart: migratePG
-  });
-}
+import type { ServerType } from "./core/types";
+export async function bootstrap(port: number): Promise<ServerType | undefined> {
+  return startServer(appHono, LoggerService.forContext("APP"), process, {
+    port,
+    beforeStart: migratePG
+  });
+}
apps/api/test/functional/docs.spec.ts (1)

6-7: Optional: adopt the repo’s setup() pattern for tests.

Guidelines recommend a setup function over Jest lifecycle calls. Here, timers are set inline; consider moving them into a local setup() near the bottom of the root describe for consistency.

Example:

// at bottom of the root describe
function setup({ now = new Date("2025-07-03T12:00:00.000Z") }: { now?: Date } = {}) {
  jest.useFakeTimers();
  jest.setSystemTime(now);
  return {};
}

Then call setup() at the start of the test.

apps/api/src/core/providers/index.ts (1)

3-3: Remove redundant side‑effect import of postgres.provider

Since you already re-export from "./postgres.provider", the module is evaluated once via that export; an extra side-effect import is unnecessary.

-import "./postgres.provider";
apps/api/src/core/providers/postgres.provider.ts (3)

56-57: Optional: make closeConnections idempotent and resilient

If APP_INITIALIZER gets resolved/disposed more than once (tests or hot-reloads), double-closing could throw. Consider guarding and logging on failure so shutdown never hangs the process.

Example (outside the selected range, for illustration only):

let pgClosed = false;
export const closeConnections = async () => {
  if (pgClosed) return;
  pgClosed = true;
  try {
    await Promise.all([migrationClient.end(), appClient.end()]);
  } catch (err) {
    logger.warn({ event: "POSTGRES_CLOSE_ERROR", err });
  }
};

6-6: Register APP_INITIALIZER as a singleton to avoid multiple disposables

By default, tsyringe uses transient lifecycle; registering as a singleton prevents multiple instances (and multiple dispose calls) if resolved more than once.

-import type { Disposable, InjectionToken } from "tsyringe";
+import { Lifecycle, type Disposable, type InjectionToken } from "tsyringe";

58-64: Add singleton lifecycle to APP_INITIALIZER registration

Prevents duplicate initializers and duplicate dispose calls across multiple resolutions.

 container.register(APP_INITIALIZER, {
   useFactory: () =>
     ({
       [ON_APP_START]: () => Promise.resolve(),
       dispose: closeConnections
     }) satisfies AppInitializer & Disposable
-});
+  , lifecycle: Lifecycle.Singleton
+});
apps/api/src/healthz/services/healthz/healthz.service.spec.ts (2)

73-111: Liveness covers happy path and dual failures; consider verifying cache miss/hit counters if available

Coverage is strong. If the health service exposes metrics/counters, asserting cache hit/miss would further harden expectations.


201-206: Setup helper is clear and colocated; optional: accept a param for overrides

The setup() meets internal guidelines (bottom of the root describe, no shared state). Optionally accept a single param for overrides (e.g., pre-set ping behaviors or custom TTL) to improve flexibility in future tests.

apps/api/src/healthz/services/healthz/healthz.service.ts (5)

100-107: Make check() return type explicit and guarantee a non-undefined Promise.

As written, TypeScript can infer Promise | undefined because you return a possibly-undefined field. It works at runtime, but explicit typing avoids surprises under strict settings.

-  private check() {
-    this.inflightPing ??= this.healthchecker.ping().finally(() => {
-      this.checkedAt = new Date();
-      this.inflightPing = undefined;
-    });
-
-    return this.inflightPing;
-  }
+  private check(): Promise<void> {
+    const ping =
+      this.inflightPing ??
+      (this.inflightPing = this.healthchecker.ping().finally(() => {
+        this.checkedAt = new Date();
+        this.inflightPing = undefined;
+      }));
+    return ping;
+  }

68-69: Narrow the healthchecker type to a simple “Pingable” interface.

Union-in-Pick is needlessly complex and can widen types. A tiny local interface makes intent clear and keeps compile-time checks tight.

+type Pingable = { ping: () => Promise<void> };
@@
-    private readonly healthchecker: Pick<DbHealthcheck | JobQueueHealthcheck, "ping">,
+    private readonly healthchecker: Pingable,

12-26: Make TTL configurable; default to a constant.

Hard-coding millisecondsInMinute is fine for now, but making it a named constant (and optionally overridable) eases tuning and testing.

+const DEFAULT_HEALTHCHECK_TTL_MS = millisecondsInMinute;
@@
-      new Healthcheck("postgres", dbHealthcheck, logger, {
-        cacheTTL: millisecondsInMinute
-      })
+      new Healthcheck("postgres", dbHealthcheck, logger, {
+        cacheTTL: DEFAULT_HEALTHCHECK_TTL_MS
+      })
@@
-      new Healthcheck("jobQueue", jobQueueHealthcheck, logger, {
-        cacheTTL: millisecondsInMinute
-      })
+      new Healthcheck("jobQueue", jobQueueHealthcheck, logger, {
+        cacheTTL: DEFAULT_HEALTHCHECK_TTL_MS
+      })

If you already have a config provider, consider injecting the TTL instead of a constant.


39-49: Avoid dynamic key casts when building the data object.

The reduce + type-assertion works but obscures the data shape and relies on casts. Building the map in a type-safe way improves maintainability.

Option A (explicit keys; simplest and most type-safe for fixed checks):

-  private buildResult(results: boolean[]): HealthzResult {
-    return {
-      status: results.every(Boolean) ? "ok" : "error",
-      data: results.reduce(
-        (acc, result, index) => {
-          acc[this.healthchecks[index].name as keyof HealthzResult["data"]] = result;
-          return acc;
-        },
-        {} as HealthzResult["data"]
-      )
-    };
-  }
+  private buildResult(results: boolean[]): HealthzResult {
+    const data: HealthzResult["data"] = {
+      postgres: results[this.healthchecks.findIndex(h => h.name === "postgres")],
+      jobQueue: results[this.healthchecks.findIndex(h => h.name === "jobQueue")]
+    };
+    return {
+      status: Object.values(data).every(Boolean) ? "ok" : "error",
+      data
+    };
+  }

Option B (keep dynamic but improve typing): define type HealthcheckName = keyof HealthzResult["data"] and type name: HealthcheckName in Healthcheck.


86-90: Consider logging normalized error details.

Passing raw error is fine if your logger handles unknown objects. If you want uniformity, log message and stack explicitly.

-      this.logger.error({
-        event: `${this.name.toUpperCase()}_HEALTHCHECK_ERROR`,
-        error
-      });
+      const err = error as unknown as { message?: string; stack?: string };
+      this.logger.error({
+        event: `${this.name.toUpperCase()}_HEALTHCHECK_ERROR`,
+        message: err?.message,
+        stack: err?.stack
+      });
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 8eb2d5e and 22d400f.

📒 Files selected for processing (73)
  • apps/api/env/.env.functional.test (1 hunks)
  • apps/api/env/.env.local.sample (1 hunks)
  • apps/api/env/.env.unit.test (1 hunks)
  • apps/api/src/app/index.ts (1 hunks)
  • apps/api/src/app/providers/jobs.provider.ts (1 hunks)
  • apps/api/src/background-jobs-app.ts (1 hunks)
  • apps/api/src/console.ts (1 hunks)
  • apps/api/src/core/config/env.config.ts (1 hunks)
  • apps/api/src/core/index.ts (1 hunks)
  • apps/api/src/core/providers/index.ts (1 hunks)
  • apps/api/src/core/providers/job-queue-healthcheck.ts (1 hunks)
  • apps/api/src/core/providers/postgres.provider.ts (4 hunks)
  • apps/api/src/core/services/index.ts (1 hunks)
  • apps/api/src/core/services/job-queue/job-queue.service.spec.ts (5 hunks)
  • apps/api/src/core/services/job-queue/job-queue.service.ts (4 hunks)
  • apps/api/src/core/services/shutdown-server/shutdown-server.spec.ts (1 hunks)
  • apps/api/src/core/services/shutdown-server/shutdown-server.ts (1 hunks)
  • apps/api/src/core/services/start-server/start-server.spec.ts (1 hunks)
  • apps/api/src/core/services/start-server/start-server.ts (1 hunks)
  • apps/api/src/db/dbConnection.ts (2 hunks)
  • apps/api/src/healthz/controllers/healthz/healthz.controller.spec.ts (2 hunks)
  • apps/api/src/healthz/routes/healthz.router.ts (1 hunks)
  • apps/api/src/healthz/services/healthz/healthz.service.spec.ts (1 hunks)
  • apps/api/src/healthz/services/healthz/healthz.service.ts (1 hunks)
  • apps/api/src/index.ts (0 hunks)
  • apps/api/src/notifications/services/notification-handler/notification.handler.ts (1 hunks)
  • apps/api/src/rest-app.ts (4 hunks)
  • apps/api/src/server.ts (1 hunks)
  • apps/api/test/functional/addresses.spec.ts (2 hunks)
  • apps/api/test/functional/anonymous-user.spec.ts (1 hunks)
  • apps/api/test/functional/api-key.spec.ts (1 hunks)
  • apps/api/test/functional/app.spec.ts (1 hunks)
  • apps/api/test/functional/auditors.spec.ts (1 hunks)
  • apps/api/test/functional/balances.spec.ts (1 hunks)
  • apps/api/test/functional/bids.spec.ts (1 hunks)
  • apps/api/test/functional/blocks.spec.ts (1 hunks)
  • apps/api/test/functional/certificate.spec.ts (1 hunks)
  • apps/api/test/functional/create-deployment.spec.ts (1 hunks)
  • apps/api/test/functional/dashboard-data.spec.ts (1 hunks)
  • apps/api/test/functional/deployment-setting.spec.ts (1 hunks)
  • apps/api/test/functional/deployments.spec.ts (1 hunks)
  • apps/api/test/functional/docs.spec.ts (1 hunks)
  • apps/api/test/functional/gpu.spec.ts (1 hunks)
  • apps/api/test/functional/graph-data.spec.ts (1 hunks)
  • apps/api/test/functional/lease-flow.spec.ts (1 hunks)
  • apps/api/test/functional/leases-duration.spec.ts (1 hunks)
  • apps/api/test/functional/market-data.spec.ts (1 hunks)
  • apps/api/test/functional/network-capacity.spec.ts (1 hunks)
  • apps/api/test/functional/nodes-v1.spec.ts (1 hunks)
  • apps/api/test/functional/pricing.spec.ts (1 hunks)
  • apps/api/test/functional/proposals.spec.ts (1 hunks)
  • apps/api/test/functional/provider-attributes-schema.spec.ts (1 hunks)
  • apps/api/test/functional/provider-dashboard.spec.ts (1 hunks)
  • apps/api/test/functional/provider-deployments.spec.ts (1 hunks)
  • apps/api/test/functional/provider-earnings.spec.ts (1 hunks)
  • apps/api/test/functional/provider-graph-data.spec.ts (1 hunks)
  • apps/api/test/functional/provider-regions.spec.ts (1 hunks)
  • apps/api/test/functional/provider-versions.spec.ts (1 hunks)
  • apps/api/test/functional/providers.spec.ts (1 hunks)
  • apps/api/test/functional/sign-and-broadcast-tx.spec.ts (1 hunks)
  • apps/api/test/functional/stale-anonymous-users-cleanup.spec.ts (1 hunks)
  • apps/api/test/functional/start-trial.spec.ts (1 hunks)
  • apps/api/test/functional/stripe-webhook.spec.ts (1 hunks)
  • apps/api/test/functional/templates.spec.ts (1 hunks)
  • apps/api/test/functional/transactions.spec.ts (1 hunks)
  • apps/api/test/functional/usage.spec.ts (1 hunks)
  • apps/api/test/functional/user-init.spec.ts (1 hunks)
  • apps/api/test/functional/validators.spec.ts (1 hunks)
  • apps/api/test/functional/wallets-refill.spec.ts (1 hunks)
  • apps/api/tsconfig.json (1 hunks)
  • apps/api/webpack.dev.js (2 hunks)
  • apps/api/webpack.prod.js (2 hunks)
  • packages/docker/docker-compose.prod.yml (2 hunks)
💤 Files with no reviewable changes (1)
  • apps/api/src/index.ts
🚧 Files skipped from review as they are similar to previous changes (56)
  • apps/api/test/functional/stripe-webhook.spec.ts
  • apps/api/src/core/services/shutdown-server/shutdown-server.ts
  • apps/api/test/functional/app.spec.ts
  • apps/api/env/.env.local.sample
  • apps/api/test/functional/gpu.spec.ts
  • apps/api/test/functional/network-capacity.spec.ts
  • apps/api/test/functional/lease-flow.spec.ts
  • apps/api/test/functional/provider-graph-data.spec.ts
  • apps/api/test/functional/balances.spec.ts
  • apps/api/test/functional/providers.spec.ts
  • apps/api/test/functional/provider-versions.spec.ts
  • apps/api/src/background-jobs-app.ts
  • apps/api/test/functional/leases-duration.spec.ts
  • apps/api/webpack.prod.js
  • apps/api/test/functional/blocks.spec.ts
  • apps/api/src/core/config/env.config.ts
  • apps/api/src/app/index.ts
  • apps/api/test/functional/proposals.spec.ts
  • apps/api/test/functional/certificate.spec.ts
  • apps/api/test/functional/start-trial.spec.ts
  • apps/api/tsconfig.json
  • apps/api/test/functional/pricing.spec.ts
  • apps/api/src/app/providers/jobs.provider.ts
  • apps/api/src/core/services/start-server/start-server.spec.ts
  • apps/api/test/functional/provider-regions.spec.ts
  • apps/api/test/functional/anonymous-user.spec.ts
  • apps/api/test/functional/create-deployment.spec.ts
  • apps/api/test/functional/provider-attributes-schema.spec.ts
  • apps/api/env/.env.functional.test
  • packages/docker/docker-compose.prod.yml
  • apps/api/test/functional/graph-data.spec.ts
  • apps/api/test/functional/provider-dashboard.spec.ts
  • apps/api/test/functional/deployment-setting.spec.ts
  • apps/api/test/functional/auditors.spec.ts
  • apps/api/src/healthz/controllers/healthz/healthz.controller.spec.ts
  • apps/api/test/functional/bids.spec.ts
  • apps/api/test/functional/provider-earnings.spec.ts
  • apps/api/test/functional/templates.spec.ts
  • apps/api/webpack.dev.js
  • apps/api/test/functional/deployments.spec.ts
  • apps/api/test/functional/dashboard-data.spec.ts
  • apps/api/test/functional/transactions.spec.ts
  • apps/api/test/functional/usage.spec.ts
  • apps/api/test/functional/provider-deployments.spec.ts
  • apps/api/src/server.ts
  • apps/api/src/healthz/routes/healthz.router.ts
  • apps/api/test/functional/nodes-v1.spec.ts
  • apps/api/test/functional/api-key.spec.ts
  • apps/api/test/functional/market-data.spec.ts
  • apps/api/src/core/index.ts
  • apps/api/src/core/services/start-server/start-server.ts
  • apps/api/test/functional/user-init.spec.ts
  • apps/api/src/core/services/job-queue/job-queue.service.ts
  • apps/api/src/notifications/services/notification-handler/notification.handler.ts
  • apps/api/test/functional/wallets-refill.spec.ts
  • apps/api/test/functional/addresses.spec.ts
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

Never use type any or cast to type any. Always define the proper TypeScript types.

Files:

  • apps/api/src/core/services/index.ts
  • apps/api/src/core/services/shutdown-server/shutdown-server.spec.ts
  • apps/api/src/core/providers/job-queue-healthcheck.ts
  • apps/api/src/core/providers/postgres.provider.ts
  • apps/api/src/healthz/services/healthz/healthz.service.ts
  • apps/api/test/functional/validators.spec.ts
  • apps/api/test/functional/sign-and-broadcast-tx.spec.ts
  • apps/api/test/functional/docs.spec.ts
  • apps/api/test/functional/stale-anonymous-users-cleanup.spec.ts
  • apps/api/src/healthz/services/healthz/healthz.service.spec.ts
  • apps/api/src/db/dbConnection.ts
  • apps/api/src/core/providers/index.ts
  • apps/api/src/console.ts
  • apps/api/src/rest-app.ts
  • apps/api/src/core/services/job-queue/job-queue.service.spec.ts
**/*.{js,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

**/*.{js,ts,tsx}: Never use deprecated methods from libraries.
Don't add unnecessary comments to the code

Files:

  • apps/api/src/core/services/index.ts
  • apps/api/src/core/services/shutdown-server/shutdown-server.spec.ts
  • apps/api/src/core/providers/job-queue-healthcheck.ts
  • apps/api/src/core/providers/postgres.provider.ts
  • apps/api/src/healthz/services/healthz/healthz.service.ts
  • apps/api/test/functional/validators.spec.ts
  • apps/api/test/functional/sign-and-broadcast-tx.spec.ts
  • apps/api/test/functional/docs.spec.ts
  • apps/api/test/functional/stale-anonymous-users-cleanup.spec.ts
  • apps/api/src/healthz/services/healthz/healthz.service.spec.ts
  • apps/api/src/db/dbConnection.ts
  • apps/api/src/core/providers/index.ts
  • apps/api/src/console.ts
  • apps/api/src/rest-app.ts
  • apps/api/src/core/services/job-queue/job-queue.service.spec.ts
**/*.spec.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/no-jest-mock.mdc)

Don't use jest.mock() to mock dependencies in test files. Instead, use jest-mock-extended to create mocks and pass mocks as dependencies to the service under test.

**/*.spec.{ts,tsx}: Use setup function instead of beforeEach in test files
setup function must be at the bottom of the root describe block in test files
setup function creates an object under test and returns it
setup function should accept a single parameter with inline type definition
Don't use shared state in setup function
Don't specify return type of setup function

Files:

  • apps/api/src/core/services/shutdown-server/shutdown-server.spec.ts
  • apps/api/test/functional/validators.spec.ts
  • apps/api/test/functional/sign-and-broadcast-tx.spec.ts
  • apps/api/test/functional/docs.spec.ts
  • apps/api/test/functional/stale-anonymous-users-cleanup.spec.ts
  • apps/api/src/healthz/services/healthz/healthz.service.spec.ts
  • apps/api/src/core/services/job-queue/job-queue.service.spec.ts
🧠 Learnings (1)
📚 Learning: 2025-07-21T08:24:24.269Z
Learnt from: CR
PR: akash-network/console#0
File: .cursor/rules/no-jest-mock.mdc:0-0
Timestamp: 2025-07-21T08:24:24.269Z
Learning: Applies to **/*.spec.{ts,tsx} : Don't use `jest.mock()` to mock dependencies in test files. Instead, use `jest-mock-extended` to create mocks and pass mocks as dependencies to the service under test.

Applied to files:

  • apps/api/src/core/services/job-queue/job-queue.service.spec.ts
🧬 Code graph analysis (6)
apps/api/src/core/providers/postgres.provider.ts (5)
apps/api/src/db/dbConnection.ts (2)
  • closeConnections (54-54)
  • ON_APP_START (58-60)
apps/api/src/core/providers/app-initializer.ts (3)
  • APP_INITIALIZER (3-3)
  • ON_APP_START (4-4)
  • AppInitializer (6-8)
apps/api/src/app/providers/jobs.provider.ts (1)
  • ON_APP_START (11-19)
apps/api/src/rest-app.ts (1)
  • ON_APP_START (188-190)
apps/api/src/core/services/feature-flags/feature-flags.service.ts (1)
  • ON_APP_START (80-82)
apps/api/src/healthz/services/healthz/healthz.service.ts (5)
apps/api/src/core/providers/logging.provider.ts (1)
  • injectable (7-8)
apps/api/src/healthz/controllers/healthz/healthz.controller.ts (1)
  • injectable (6-17)
apps/api/src/core/providers/postgres.provider.ts (2)
  • DB_HEALTHCHECK (33-33)
  • DbHealthcheck (30-32)
apps/api/src/core/providers/job-queue-healthcheck.ts (2)
  • JOB_QUEUE_HEALTHCHECK (9-9)
  • JobQueueHealthcheck (6-8)
packages/logging/src/servicies/logger/logger.service.ts (1)
  • error (107-109)
apps/api/src/healthz/services/healthz/healthz.service.spec.ts (4)
apps/api/src/caching/helpers.ts (1)
  • cacheEngine (8-8)
packages/logging/src/servicies/logger/logger.service.ts (1)
  • error (107-109)
apps/api/src/core/providers/postgres.provider.ts (1)
  • DbHealthcheck (30-32)
apps/api/src/core/providers/job-queue-healthcheck.ts (1)
  • JobQueueHealthcheck (6-8)
apps/api/src/db/dbConnection.ts (4)
apps/api/src/core/providers/postgres.provider.ts (1)
  • closeConnections (56-56)
apps/api/src/core/providers/app-initializer.ts (2)
  • APP_INITIALIZER (3-3)
  • AppInitializer (6-8)
apps/api/src/app/providers/jobs.provider.ts (1)
  • ON_APP_START (11-19)
apps/api/src/rest-app.ts (2)
  • ON_APP_START (188-190)
  • connectUsingSequelize (194-194)
apps/api/src/rest-app.ts (6)
apps/api/src/core/providers/app-initializer.ts (2)
  • APP_INITIALIZER (3-3)
  • AppInitializer (6-8)
apps/api/src/app/providers/jobs.provider.ts (1)
  • ON_APP_START (11-19)
apps/api/src/db/dbConnection.ts (1)
  • ON_APP_START (58-60)
apps/api/src/background-jobs-app.ts (1)
  • bootstrap (9-17)
apps/api/src/core/services/start-server/start-server.ts (1)
  • startServer (18-63)
apps/api/src/core/providers/postgres.provider.ts (1)
  • migratePG (26-26)
apps/api/src/core/services/job-queue/job-queue.service.spec.ts (1)
apps/api/src/core/services/job-queue/job-queue.service.ts (1)
  • setup (135-142)
🪛 dotenv-linter (3.3.0)
apps/api/env/.env.unit.test

[warning] 28-28: [UnorderedKey] The NOTIFICATIONS_API_BASE_URL key should go before the POSTGRES_DB_URI key

(UnorderedKey)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: test-build
  • GitHub Check: test-build
  • GitHub Check: test-build
  • GitHub Check: validate / validate-app
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: test-build
  • GitHub Check: test-build
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: Validate local packages

Comment on lines +53 to +59
export interface HealthzResult {
status: "ok" | "error";
data: {
postgres: boolean;
jobQueue: boolean;
};
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Align controller types with the new HealthzResult.

Controller currently advertises HealthzResponse & { status: ... }. To avoid structural mismatches, update it to return HealthzResult and import the type from this service.

Proposed change in apps/api/src/healthz/controllers/healthz/healthz.controller.ts:

-  async getReadinessStatus(): Promise<HealthzResponse & { status: "ok" | "error" }> {
+  async getReadinessStatus(): Promise<HealthzResult> {
     return this.healthzService.getReadinessStatus();
   }
 
-  async getLivenessStatus(): Promise<HealthzResponse & { status: "ok" | "error" }> {
+  async getLivenessStatus(): Promise<HealthzResult> {
     return this.healthzService.getLivenessStatus();
   }

To quickly verify repo-wide alignment and provider bindings, run:


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

# 1) Where HealthzResponse is defined/used and whether jobQueue is included
rg -n --pretty '\bHealthzResponse\b' -C3

# 2) Ensure controller methods are typed as HealthzResult
rg -n --pretty 'class HealthzController' -C3 apps/api/src
rg -n --pretty '\bHealthzResult\b' -C2 apps/api/src/healthz

# 3) Check that DB_HEALTHCHECK and JOB_QUEUE_HEALTHCHECK are registered/bound
rg -nP --pretty 'register\((DB_HEALTHCHECK|JOB_QUEUE_HEALTHCHECK)\b' -C3 apps/api/src || true

# 4) Confirm LoggerService has setContext
rg -n --pretty '\bclass LoggerService\b' -C3
rg -n --pretty '\bsetContext\s*\(' -C2 || true

Length of output: 286919


Update HealthzController to return HealthzResult (service type).

Short check: healthz.service.ts defines/returns HealthzResult; controller currently types methods as HealthzResponse & { status: "ok" | "error" } (imported from the router). DB/JOB healthcheck providers and LoggerService usage were found—no blocking issues.

Files to change:

  • apps/api/src/healthz/controllers/healthz/healthz.controller.ts — replace HealthzResponse-based signatures and import with HealthzResult from the service.
  • (note) apps/api/src/healthz/services/healthz/healthz.service.ts defines HealthzResult and implements getReadinessStatus/getLivenessStatus.

Suggested diffs:

Import change:

- import type { HealthzResponse } from "@src/healthz/routes/healthz.router";
+ import type { HealthzResult } from "@src/healthz/services/healthz/healthz.service";

Method signatures:

-  async getReadinessStatus(): Promise<HealthzResponse & { status: "ok" | "error" }> {
+  async getReadinessStatus(): Promise<HealthzResult> {
     return this.healthzService.getReadinessStatus();
   }

-  async getLivenessStatus(): Promise<HealthzResponse & { status: "ok" | "error" }> {
+  async getLivenessStatus(): Promise<HealthzResult> {
     return this.healthzService.getLivenessStatus();
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export interface HealthzResult {
status: "ok" | "error";
data: {
postgres: boolean;
jobQueue: boolean;
};
}
// apps/api/src/healthz/controllers/healthz/healthz.controller.ts
import type { HealthzResult } from "@src/healthz/services/healthz/healthz.service";
export class HealthzController {
constructor(private readonly healthzService: HealthzService) {}
@Get("readiness")
async getReadinessStatus(): Promise<HealthzResult> {
return this.healthzService.getReadinessStatus();
}
@Get("liveness")
async getLivenessStatus(): Promise<HealthzResult> {
return this.healthzService.getLivenessStatus();
}
}
🤖 Prompt for AI Agents
In apps/api/src/healthz/controllers/healthz/healthz.controller.ts (replace the
HealthzResponse-based signatures and import), import HealthzResult from
apps/api/src/healthz/services/healthz/healthz.service.ts instead of
HealthzResponse, then update the controller methods' return type annotations
from HealthzResponse & { status: "ok" | "error" } to HealthzResult; ensure any
uses of the previous HealthzResponse shape are adjusted to match HealthzResult's
data structure (postgres: boolean, jobQueue: boolean) and remove the now-unused
HealthzResponse import.

Comment on lines +75 to 97
const now = Date.now();

return dbFailingFor < threshold;
}

private async isPostgresConnected(): Promise<boolean> {
try {
await this.pg.execute(sql`SELECT 1`);
this.dbFailedAt = undefined;
if (options?.ignoreCache || !this.checkedAt || now - this.checkedAt.getTime() > this.options.cacheTTL) {
await this.check();
this.isFailed = false;
}

return true;
} catch (error) {
this.dbFailedAt = this.dbFailedAt || new Date();
this.logger.error(error);
this.logger.error({
event: `${this.name.toUpperCase()}_HEALTHCHECK_ERROR`,
error
});

return false;
const prevIsFailed = this.isFailed;
if (this.isFailed === null || prevIsFailed || options?.ignoreCache) return false;

this.isFailed = true;
// tolerate failure for the 1st time and wait for the cache to expire until the next check
return true;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix first-failure tolerance: liveness currently fails on initial ping failure (isFailed === null).

The catch-branch contradicts the comment “tolerate failure for the 1st time”. If the very first check fails (isFailed === null), the function returns false, which can make liveness flap on cold start. Readiness already uses ignoreCache: true, so it remains strict.

Apply this diff to align behavior: tolerate the first failure for liveness, fail on consecutive failures or when ignoreCache is true.

-      const prevIsFailed = this.isFailed;
-      if (this.isFailed === null || prevIsFailed || options?.ignoreCache) return false;
-
-      this.isFailed = true;
-      // tolerate failure for the 1st time and wait for the cache to expire until the next check
-      return true;
+      const hadPreviousFailure = this.isFailed === true;
+      if (options?.ignoreCache || hadPreviousFailure) return false;
+
+      this.isFailed = true;
+      // tolerate the first failure for liveness; readiness still fails via ignoreCache
+      return true;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async isHealthy(options?: { ignoreCache?: boolean }): Promise<boolean> {
const now = Date.now();
return dbFailingFor < threshold;
}
private async isPostgresConnected(): Promise<boolean> {
try {
await this.pg.execute(sql`SELECT 1`);
this.dbFailedAt = undefined;
if (options?.ignoreCache || !this.checkedAt || now - this.checkedAt.getTime() > this.options.cacheTTL) {
await this.check();
this.isFailed = false;
}
return true;
} catch (error) {
this.dbFailedAt = this.dbFailedAt || new Date();
this.logger.error(error);
this.logger.error({
event: `${this.name.toUpperCase()}_HEALTHCHECK_ERROR`,
error
});
return false;
const prevIsFailed = this.isFailed;
if (this.isFailed === null || prevIsFailed || options?.ignoreCache) return false;
this.isFailed = true;
// tolerate failure for the 1st time and wait for the cache to expire until the next check
return true;
}
async isHealthy(options?: { ignoreCache?: boolean }): Promise<boolean> {
const now = Date.now();
try {
if (
options?.ignoreCache ||
!this.checkedAt ||
now - this.checkedAt.getTime() > this.options.cacheTTL
) {
await this.check();
this.isFailed = false;
}
return true;
} catch (error) {
this.logger.error({
event: `${this.name.toUpperCase()}_HEALTHCHECK_ERROR`,
error
});
const hadPreviousFailure = this.isFailed === true;
if (options?.ignoreCache || hadPreviousFailure) return false;
this.isFailed = true;
// tolerate the first failure for liveness; readiness still fails via ignoreCache
return true;
}
🤖 Prompt for AI Agents
In apps/api/src/healthz/services/healthz/healthz.service.ts around lines 75–97,
the catch branch currently treats isFailed === null as an immediate failure
which violates the "tolerate first failure" behavior; change the logic so that
if options?.ignoreCache is true we fail immediately, if prevIsFailed === true (a
consecutive failure) we fail, otherwise treat this as the first failure: set
this.isFailed = true and return true to tolerate it. Concretely, compute const
prevIsFailed = this.isFailed first, then if (options?.ignoreCache) return false;
if (prevIsFailed === true) return false; set this.isFailed = true and return
true.

@stalniy stalniy merged commit d3e6214 into main Aug 26, 2025
61 of 63 checks passed
@stalniy stalniy deleted the feat/background-jobs branch August 26, 2025 05:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

Comments