[twenty-server] no floating promises lint rule (#20499)
## Introduction
That's an audit + RFC
## Fire-and-forget (`void`) -- Intentional, correct
These are telemetry, metrics, and audit logging in hot paths or
non-critical contexts. `void` is the right choice.
| File | What was voided |
|---|---|
| `sign-in-up.service.ts` | `metricsService.incrementCounter` (sign-up
metric) + `auditService.insertWorkspaceEvent` (workspace created) |
| `use-graphql-error-handler.hook.ts` | 5x
`metricsService.incrementCounter` (GraphQL operation metrics) |
| `bullmq.driver.ts` | 2x `metricsService.incrementCounter` (job
completed/failed metrics) |
| `call-webhook.job.ts` | 2x `auditService.insertWorkspaceEvent` + 1x
`metricsService.incrementCounter` |
| `custom-domain-manager.service.ts` | `analytics.insertWorkspaceEvent`
(domain activation event) |
| `logic-function-executor.service.ts` |
`auditService.insertWorkspaceEvent` (function execution) |
| `workflow-runner.workspace-service.ts` |
`metricsService.incrementCounter` (throttle metric) |
| `cleaner.workspace-service.ts` | `metricsService.incrementCounter`
(deleted workspace metric) |
| `stream-agent-chat.job.ts` | Detached IIFE for streaming chunks
(intentional concurrent pipeline) |
| `workspace-auth-context.middleware.ts` |
`withWorkspaceAuthContext(...)` (AsyncLocalStorage, returns void anyway)
|
## Top-level script entry points (`void bootstrap()`)
These are module-level calls where the promise has no consumer. `void`
makes the lint rule happy and documents the intent.
| File | What changed |
|---|---|
| `main.ts` | `void bootstrap()` |
| `command.ts` | `void bootstrap()` |
| `queue-worker.ts` | `void bootstrap()` |
| `truncate-db.ts` | `void dropSchemasSequentially()` |
| `codegen/index.ts` | `void generateTests(forceArg)` |
## Now properly awaited -- Real bug fixes
These were floating promises that could silently fail, lose data, or
cause race conditions.
| File | What was fixed |
|---|---|
| `billing-sync-plans-data.command.ts` | `meters.map(async ...)` wrapped
in `Promise.all` -- was returning before upserts finished |
| `cache-storage.service.ts` | `setAdd` and `setPop` had `.then()`
chains that weren't returned/awaited |
| `create-audit-log-from-internal-event.ts` | 4x
`auditService.createObjectEvent` now awaited inside a job |
| `cleaner.workspace-service.ts` | 2x `emailService.send(...)` now
awaited -- emails could silently fail |
| `agent-async-executor.service.ts` | `calculateAndBillUsage` +
`billNativeWebSearchUsage` in `finally` block now awaited |
| `repair-tool-call.util.ts` | `calculateAndBillUsage` now awaited |
| `agent-title-generation.service.ts` | `calculateAndBillUsage` now
awaited |
| `chat-execution.service.ts` | `billNativeWebSearchUsage` now awaited |
| `ai-generate-text.controller.ts` | `calculateAndBillUsage` in
`finally` block now awaited |
| `agent-turn.resolver.ts` | `messageQueueService.add(...)` now awaited
|
| `command.ts` | `app.close()` now awaited (was exiting before graceful
shutdown) |
| `i18n.service.ts` | `loadTranslations()` in `onModuleInit` now awaited
|
| `workspace-query-hook.explorer.ts` | `explore()` in `onModuleInit` now
awaited |
| `message-queue.explorer.ts` | `handleProcessorGroupCollection` in
`onModuleInit` now awaited |
| `ai-billing.service.spec.ts` | Test now properly `await`s the async
call |
| `messaging-messages-import.service.spec.ts` | `expect(...)` now
properly `await`ed for async assertion |
| `archive.finalize()` (3 files) | Voided -- promise resolution already
handled by `pipeline()` / `on('end')` |
## Impersonation & security audit trail -- Upgraded from `void` to
`await`
These were previously fire-and-forget but are
security/compliance-critical events that must be reliably persisted.
| File | What was fixed |
|---|---|
| `impersonation.service.ts` | 4x `auditService.insertWorkspaceEvent`
now awaited (impersonation attempt, token generation
attempt/success/failure) |
| `auth.resolver.ts` | 5x `auditService.insertWorkspaceEvent` now
awaited (impersonation token exchange attempt/success/failure at server
and workspace levels) |
| `auth.service.ts` | 2x `analytics.insertWorkspaceEvent` now awaited
(impersonation attempted/issued) |
## Billing audit -- Upgraded from `void` to `await`
Payment events should be reliably persisted for financial/compliance
reporting.
| File | What was fixed |
|---|---|
| `billing-webhook-invoice.service.ts` |
`auditService.insertWorkspaceEvent(PAYMENT_RECEIVED_EVENT)` now awaited
inside Stripe webhook handler |
## Fire-and-forget with proper error handling -- Upgraded from bare
`void`
These remain non-blocking but now catch and log errors instead of
risking unhandled rejections.
| File | What was fixed |
|---|---|
| `logic-function-executor.service.ts` |
`applicationLogsService.writeLogs` now uses `.catch()` instead of bare
`void` -- user-facing logs should surface errors |
## Systemic infrastructure fixes
| File | What was fixed |
|---|---|
| `metrics.service.ts` | `incrementCounter`: Redis cache write
(`metricsCacheService.updateCounter`) now uses `.catch()` internally
instead of raw `await` -- prevents unhandled rejections across all `void
metricsService.incrementCounter(...)` call sites when Redis is unhealthy
|
| `audit.service.ts` | `preventIfDisabled`: made properly `async` with
`await` and consistent `Promise<{ success: boolean }>` return type.
Removed broken `catch` that returned an `AuditException` as a value
(wrong constructor args, unreachable dead code). Removed unused
`AuditException` import |
## Fixed in this session (beyond original PR)
| File | What changed |
|---|---|
| `telemetry.listener.ts` | Removed misleading `Promise.all` + `void`
combo; replaced with simple `for...of` + `void` |
| `message-queue.explorer.ts` | Changed from `void` to `await` so
startup crashes on registration failure |
This commit is contained in:
+3
-3
@@ -92,7 +92,7 @@ export class BullMQDriver
|
||||
]);
|
||||
}
|
||||
|
||||
async work<T>(
|
||||
work<T>(
|
||||
queueName: MessageQueue,
|
||||
handler: (job: MessageQueueJob<T>) => Promise<void>,
|
||||
options?: MessageQueueWorkerOptions,
|
||||
@@ -136,7 +136,7 @@ export class BullMQDriver
|
||||
);
|
||||
|
||||
this.workerMap[queueName].on('completed', (job) => {
|
||||
this.metricsService.incrementCounter({
|
||||
void this.metricsService.incrementCounter({
|
||||
key: MetricsKeys.JobCompleted,
|
||||
attributes: { queue: queueName, job_name: job?.name ?? '' },
|
||||
shouldStoreInCache: false,
|
||||
@@ -148,7 +148,7 @@ export class BullMQDriver
|
||||
return;
|
||||
}
|
||||
|
||||
this.metricsService.incrementCounter({
|
||||
void this.metricsService.incrementCounter({
|
||||
key: MetricsKeys.JobFailed,
|
||||
attributes: {
|
||||
queue: queueName,
|
||||
|
||||
+3
-6
@@ -14,13 +14,11 @@ export interface MessageQueueDriver {
|
||||
data: T,
|
||||
options?: QueueJobOptions,
|
||||
): Promise<void>;
|
||||
// @ts-expect-error legacy noImplicitAny
|
||||
work<T extends MessageQueueJobData>(
|
||||
queueName: MessageQueue,
|
||||
handler: ({ data, id }: { data: T; id: string }) => Promise<void> | void,
|
||||
options?: MessageQueueWorkerOptions,
|
||||
);
|
||||
// @ts-expect-error legacy noImplicitAny
|
||||
): void;
|
||||
addCron<T extends MessageQueueJobData | undefined>({
|
||||
queueName,
|
||||
jobName,
|
||||
@@ -33,8 +31,7 @@ export interface MessageQueueDriver {
|
||||
data: T;
|
||||
options: QueueCronJobOptions;
|
||||
jobId?: string;
|
||||
});
|
||||
// @ts-expect-error legacy noImplicitAny
|
||||
}): Promise<void>;
|
||||
removeCron({
|
||||
queueName,
|
||||
jobName,
|
||||
@@ -43,6 +40,6 @@ export interface MessageQueueDriver {
|
||||
queueName: MessageQueue;
|
||||
jobName: string;
|
||||
jobId?: string;
|
||||
});
|
||||
}): Promise<void>;
|
||||
register?(queueName: MessageQueue): void;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
|
||||
import { type MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
||||
|
||||
// Synchronous driver for tests and local dev
|
||||
export class SyncDriver implements MessageQueueDriver {
|
||||
private readonly logger = new Logger(SyncDriver.name);
|
||||
private workersMap: {
|
||||
@@ -50,7 +51,7 @@ export class SyncDriver implements MessageQueueDriver {
|
||||
work<T extends MessageQueueJobData>(
|
||||
queueName: MessageQueue,
|
||||
handler: (job: MessageQueueJob<T>) => Promise<void> | void,
|
||||
) {
|
||||
): void {
|
||||
this.logger.log(`Registering handler for queue: ${queueName}`);
|
||||
this.workersMap[queueName] = handler;
|
||||
}
|
||||
|
||||
+1
-1
@@ -132,7 +132,7 @@ export class MessageQueueExplorer implements OnModuleInit {
|
||||
}
|
||||
}
|
||||
|
||||
private async handleProcessorGroupCollection(
|
||||
private handleProcessorGroupCollection(
|
||||
processorGroupCollection: ProcessorGroup[],
|
||||
queue: MessageQueueService,
|
||||
options?: MessageQueueWorkerOptions,
|
||||
|
||||
+2
-2
@@ -72,7 +72,7 @@ export class MessageQueueService {
|
||||
work<T extends MessageQueueJobData>(
|
||||
handler: (job: MessageQueueJob<T>) => Promise<void> | void,
|
||||
options?: MessageQueueWorkerOptions,
|
||||
) {
|
||||
return this.driver.work(this.queueName, handler, options);
|
||||
): void {
|
||||
this.driver.work(this.queueName, handler, options);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user