[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:
Paul Rastoin
2026-05-13 12:08:42 +02:00
committed by GitHub
parent 38d9eacff8
commit a159a68e2c
43 changed files with 135 additions and 122 deletions
+4
View File
@@ -2,6 +2,9 @@
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["typescript", "import", "unicorn"],
"jsPlugins": ["../twenty-oxlint-rules/dist/oxlint-plugin.mjs"],
"options": {
"typeAware": true
},
"categories": {
"correctness": "off"
},
@@ -53,6 +56,7 @@
}],
"typescript/no-empty-function": "off",
"typescript/no-explicit-any": "warn",
"typescript/no-floating-promises": "error",
"typescript/no-unused-vars": ["warn", {
"vars": "all",
"varsIgnorePattern": "^_",
@@ -31,6 +31,6 @@ async function bootstrap() {
await CommandFactory.runApplication(app);
app.close();
await app.close();
}
bootstrap();
void bootstrap();
@@ -41,4 +41,4 @@ async function dropSchemasSequentially() {
}
}
dropSchemasSequentially();
void dropSchemasSequentially();
@@ -4,9 +4,9 @@ import { OnCustomBatchEvent } from 'src/engine/api/graphql/graphql-query-runner/
import { USER_SIGNUP_EVENT_NAME } from 'src/engine/api/graphql/workspace-query-runner/constants/user-signup-event-name.constants';
import { AuditService } from 'src/engine/core-modules/audit/services/audit.service';
import { USER_SIGNUP_EVENT } from 'src/engine/core-modules/audit/utils/events/workspace-event/user/user-signup';
import { TelemetryEventType } from 'src/engine/core-modules/telemetry/telemetry-event.type';
import { TelemetryService } from 'src/engine/core-modules/telemetry/telemetry.service';
import { CustomWorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/custom-workspace-batch-event.type';
import { TelemetryEventType } from 'src/engine/core-modules/telemetry/telemetry-event.type';
@Injectable()
export class TelemetryListener {
@@ -20,14 +20,14 @@ export class TelemetryListener {
payload: CustomWorkspaceEventBatch<TelemetryEventType>,
) {
await Promise.all(
payload.events.map(async (eventPayload) => {
payload.events.map(async (eventPayload) =>
this.auditService
.createContext({
userId: eventPayload.userId,
workspaceId: payload.workspaceId,
})
.insertWorkspaceEvent(USER_SIGNUP_EVENT, {});
}),
.insertWorkspaceEvent(USER_SIGNUP_EVENT, {}),
),
);
await this.telemetryService.publish({
@@ -41,8 +41,8 @@ export class WorkspaceQueryHookExplorer implements OnModuleInit {
private readonly workspaceQueryHookStorage: WorkspaceQueryHookStorage,
) {}
onModuleInit() {
this.explore();
async onModuleInit() {
await this.explore();
}
async explore() {
@@ -35,25 +35,25 @@ export class CreateAuditLogFromInternalEvent {
// Since these are object record events, we use createObjectEvent
if (workspaceEventBatch.name.endsWith('.updated')) {
auditService.createObjectEvent(OBJECT_RECORD_UPDATED_EVENT, {
await auditService.createObjectEvent(OBJECT_RECORD_UPDATED_EVENT, {
...eventProperties,
recordId: eventData.recordId,
objectMetadataId: workspaceEventBatch.objectMetadata.id,
});
} else if (workspaceEventBatch.name.endsWith('.created')) {
auditService.createObjectEvent(OBJECT_RECORD_CREATED_EVENT, {
await auditService.createObjectEvent(OBJECT_RECORD_CREATED_EVENT, {
...eventProperties,
recordId: eventData.recordId,
objectMetadataId: workspaceEventBatch.objectMetadata.id,
});
} else if (workspaceEventBatch.name.endsWith('.deleted')) {
auditService.createObjectEvent(OBJECT_RECORD_DELETED_EVENT, {
await auditService.createObjectEvent(OBJECT_RECORD_DELETED_EVENT, {
...eventProperties,
recordId: eventData.recordId,
objectMetadataId: workspaceEventBatch.objectMetadata.id,
});
} else if (workspaceEventBatch.name.endsWith('.upserted')) {
auditService.createObjectEvent(OBJECT_RECORD_UPSERTED_EVENT, {
await auditService.createObjectEvent(OBJECT_RECORD_UPSERTED_EVENT, {
...eventProperties,
recordId: eventData.recordId,
objectMetadataId: workspaceEventBatch.objectMetadata.id,
@@ -1,10 +1,6 @@
import { Injectable } from '@nestjs/common';
import { Injectable, Logger } from '@nestjs/common';
import { ClickHouseService } from 'src/database/clickHouse/clickHouse.service';
import {
AuditException,
AuditExceptionCode,
} from 'src/engine/core-modules/audit/audit.exception';
import {
type TrackEventName,
type TrackEventProperties,
@@ -18,6 +14,8 @@ import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twent
@Injectable()
export class AuditService {
private readonly logger = new Logger(AuditService.name);
constructor(
private readonly twentyConfigService: TwentyConfigService,
private readonly clickHouseService: ClickHouseService,
@@ -82,16 +80,19 @@ export class AuditService {
};
}
private preventIfDisabled(
private async preventIfDisabled(
sendEventOrPageviewFunction: () => Promise<{ success: boolean }>,
) {
): Promise<{ success: boolean }> {
if (!this.twentyConfigService.get('CLICKHOUSE_URL')) {
return { success: true };
}
try {
return sendEventOrPageviewFunction();
} catch (err) {
return new AuditException(err, AuditExceptionCode.INVALID_INPUT);
return await sendEventOrPageviewFunction();
} catch (error) {
this.logger.error('Failed to persist audit event to ClickHouse', error);
return { success: false };
}
}
}
@@ -711,7 +711,7 @@ export class AuthResolver {
userId: impersonatorUserWorkspace.user.id,
});
auditService.insertWorkspaceEvent(MONITORING_EVENT, {
await auditService.insertWorkspaceEvent(MONITORING_EVENT, {
eventName: `${isServerLevelImpersonation ? 'server' : 'workspace'}.impersonation.token_exchange_attempt`,
message: `Impersonation token exchange attempt for ${targetUserEmail} by ${impersonatorUserWorkspace.user.id}`,
});
@@ -722,7 +722,7 @@ export class AuthResolver {
if (isServerLevelImpersonation) {
if (!hasServerLevelImpersonatePermission) {
auditService.insertWorkspaceEvent(MONITORING_EVENT, {
await auditService.insertWorkspaceEvent(MONITORING_EVENT, {
eventName: 'server.impersonation.token_exchange_failed',
message: `Server level impersonation not allowed for ${targetUserEmail} by userId ${impersonatorUserWorkspace.user.id}`,
});
@@ -733,7 +733,7 @@ export class AuthResolver {
);
}
auditService.insertWorkspaceEvent(MONITORING_EVENT, {
await auditService.insertWorkspaceEvent(MONITORING_EVENT, {
eventName: `server.impersonation.token_exchange_success`,
message: `Impersonation token exchanged for ${targetUserEmail} by userId ${impersonatorUserWorkspace.user.id}`,
});
@@ -755,7 +755,7 @@ export class AuthResolver {
});
if (!hasWorkspaceLevelImpersonatePermission) {
auditService.insertWorkspaceEvent(MONITORING_EVENT, {
await auditService.insertWorkspaceEvent(MONITORING_EVENT, {
eventName: 'workspace.impersonation.token_exchange_failed',
message: `Impersonation not allowed for ${targetUserEmail} by userId ${impersonatorUserWorkspace.user.id}`,
});
@@ -765,7 +765,7 @@ export class AuthResolver {
);
}
auditService.insertWorkspaceEvent(MONITORING_EVENT, {
await auditService.insertWorkspaceEvent(MONITORING_EVENT, {
eventName: 'workspace.impersonation.token_exchange_success',
message: `Impersonation token exchanged for ${targetUserEmail} by userId ${impersonatorUserWorkspace.user.id}`,
});
@@ -28,7 +28,7 @@ export class WorkspaceAuthContextMiddleware implements NestMiddleware {
applyWorkspaceSentryContext(authContext);
withWorkspaceAuthContext(authContext, () => {
void withWorkspaceAuthContext(authContext, () => {
next();
});
}
@@ -432,7 +432,7 @@ export class AuthService {
userId: _impersonatorUserId,
});
analytics.insertWorkspaceEvent('Monitoring', {
await analytics.insertWorkspaceEvent('Monitoring', {
eventName: 'workspace.impersonation.attempted',
message: `correlationId=${correlationId}; impersonatorUserWorkspaceId=${impersonatorUserWorkspaceId}; targetUserWorkspaceId=${impersonatedUserWorkspaceId}; workspaceId=${workspaceId}`,
});
@@ -458,7 +458,7 @@ export class AuthService {
true,
);
analytics.insertWorkspaceEvent('Monitoring', {
await analytics.insertWorkspaceEvent('Monitoring', {
eventName: 'workspace.impersonation.issued',
message: `correlationId=${correlationId}; impersonatorUserWorkspaceId=${impersonatorUserWorkspaceId}; targetUserWorkspaceId=${impersonatedUserWorkspaceId}; workspaceId=${workspaceId}`,
});
@@ -384,7 +384,7 @@ export class SignInUpService {
undefined,
);
this.metricsService.incrementCounter({
void this.metricsService.incrementCounter({
key: MetricsKeys.SignUpSuccess,
shouldStoreInCache: false,
});
@@ -597,7 +597,7 @@ export class SignInUpService {
await queryRunner.commitTransaction();
this.auditService
void this.auditService
.createContext({ workspaceId })
.insertWorkspaceEvent(WORKSPACE_CREATED_EVENT, {});
@@ -228,7 +228,7 @@ export class BillingWebhookInvoiceService {
if (isDefined(billingCustomer)) {
await this.delaySuspendedWorkspaceCleanup(billingCustomer);
this.auditService
await this.auditService
.createContext({ workspaceId: billingCustomer.workspaceId })
.insertWorkspaceEvent(PAYMENT_RECEIVED_EVENT, {
amountPaid: data.object.amount_paid,
@@ -46,21 +46,23 @@ export class BillingSyncPlansDataCommand extends MigrationCommandRunner {
meters: Stripe.Billing.Meter[],
options: MigrationCommandOptions,
) {
meters.map(async (meter) => {
try {
if (!options.dryRun) {
await this.billingMeterRepository.upsert(
transformStripeMeterToDatabaseMeter(meter),
{
conflictPaths: ['stripeMeterId'],
},
);
await Promise.all(
meters.map(async (meter) => {
try {
if (!options.dryRun) {
await this.billingMeterRepository.upsert(
transformStripeMeterToDatabaseMeter(meter),
{
conflictPaths: ['stripeMeterId'],
},
);
}
this.logger.log(`Upserted meter: ${meter.id}`);
} catch (error) {
this.logger.error(`Error upserting meter ${meter.id}: ${error}`);
}
this.logger.log(`Upserted meter: ${meter.id}`);
} catch (error) {
this.logger.error(`Error upserting meter ${meter.id}: ${error}`);
}
});
}),
);
}
private async upsertProductRepositoryData(
@@ -97,13 +97,13 @@ export class CacheStorageService {
return;
}
this.get(key).then((res: string[]) => {
if (res) {
this.set(key, [...res, ...value], ttl);
} else {
this.set(key, value, ttl);
}
});
const res = await this.get<string[]>(key);
if (res) {
await this.set(key, [...res, ...value], ttl);
} else {
await this.set(key, value, ttl);
}
}
async setRemove(key: string, values: string[]): Promise<number> {
@@ -146,15 +146,15 @@ export class CacheStorageService {
);
}
return this.get(key).then((res: string[]) => {
if (res) {
this.set(key, res.slice(0, -size));
const res = await this.get<string[]>(key);
return res.slice(-size);
}
if (res) {
await this.set(key, res.slice(0, -size));
return [];
});
return res.slice(-size);
}
return [];
}
async getSetLength(key: string) {
@@ -164,9 +164,9 @@ export class CacheStorageService {
);
}
return this.get(key).then((res: string[]) => {
return res.length;
});
const res = await this.get<string[]>(key);
return res?.length ?? 0;
}
async setMembers(key: string): Promise<string[]> {
@@ -114,7 +114,7 @@ export class CustomDomainManagerService {
workspaceId: workspace.id,
});
analytics.insertWorkspaceEvent(
void analytics.insertWorkspaceEvent(
workspace.isCustomDomainEnabled
? CUSTOM_DOMAIN_ACTIVATED_EVENT
: CUSTOM_DOMAIN_DEACTIVATED_EVENT,
@@ -116,7 +116,7 @@ export const useGraphQLErrorHandlerHook = <
setResult,
}) => {
if (!result.errors || result.errors.length === 0) {
options.metricsService.incrementCounter({
void options.metricsService.incrementCounter({
key: MetricsKeys.GraphqlOperation200,
});
@@ -178,9 +178,11 @@ export const useGraphQLErrorHandlerHook = <
}
if (metricKey) {
options.metricsService.incrementCounter({ key: metricKey });
void options.metricsService.incrementCounter({
key: metricKey,
});
} else {
options.metricsService.incrementCounter({
void options.metricsService.incrementCounter({
key: MetricsKeys.GraphqlOperationUnknown,
});
}
@@ -280,7 +282,7 @@ export const useGraphQLErrorHandlerHook = <
isDefined(currentMetadataVersion) &&
requestMetadataVersion !== `${currentMetadataVersion}`
) {
options.metricsService.incrementCounter({
void options.metricsService.incrementCounter({
key: MetricsKeys.SchemaVersionMismatch,
});
@@ -310,7 +312,7 @@ export const useGraphQLErrorHandlerHook = <
isDefined(backendMajor) &&
frontEndMajor < backendMajor
) {
options.metricsService.incrementCounter({
void options.metricsService.incrementCounter({
key: MetricsKeys.AppVersionMismatch,
});
throw new GraphQLError(APP_VERSION_MISMATCH_ERROR, {
@@ -113,6 +113,6 @@ export class I18nService implements OnModuleInit {
}
async onModuleInit() {
this.loadTranslations();
await this.loadTranslations();
}
}
@@ -159,13 +159,13 @@ export class ImpersonationService {
userId: impersonatorUserWorkspace.userId,
});
auditService.insertWorkspaceEvent(MONITORING_EVENT, {
await auditService.insertWorkspaceEvent(MONITORING_EVENT, {
eventName: `${impersonationLevel}.impersonation.attempt`,
message: `Impersonation attempt: targetUserId=${toImpersonateUserWorkspace.user.id}, workspaceId=${toImpersonateUserWorkspace.workspace.id}, impersonatorUserId=${impersonatorUserWorkspace.user.id}`,
});
try {
auditService.insertWorkspaceEvent(MONITORING_EVENT, {
await auditService.insertWorkspaceEvent(MONITORING_EVENT, {
eventName: `${impersonationLevel}.impersonation.login_token_attempt`,
message: `Impersonation token generation attempt for user ${toImpersonateUserWorkspace.user.id}`,
});
@@ -179,7 +179,7 @@ export class ImpersonationService {
},
);
auditService.insertWorkspaceEvent(MONITORING_EVENT, {
await auditService.insertWorkspaceEvent(MONITORING_EVENT, {
eventName: `${impersonationLevel}.impersonation.login_token_generated`,
message: `Impersonation token generated successfully for user ${toImpersonateUserWorkspace.user.id}`,
});
@@ -194,7 +194,7 @@ export class ImpersonationService {
loginToken,
};
} catch {
auditService.insertWorkspaceEvent(MONITORING_EVENT, {
await auditService.insertWorkspaceEvent(MONITORING_EVENT, {
eventName: `${impersonationLevel}.impersonation.login_token_failed`,
message: `Impersonation token generation failed for targetUserId=${toImpersonateUserWorkspace.user.id}`,
});
@@ -72,7 +72,7 @@ const createZip = async (buildDir, zipPath) => {
const p = pipeline(archive, output);
archive.directory(buildDir, false);
archive.finalize();
void archive.finalize();
return p;
};
@@ -807,7 +807,7 @@ export class LambdaDriver implements LogicFunctionDriver {
await new Promise<void>((resolve, reject) => {
archive.on('end', resolve);
archive.on('error', reject);
archive.finalize();
void archive.finalize();
});
return Buffer.concat(chunks);
@@ -15,7 +15,7 @@ export const createZipFile = async (
const p = pipeline(archive, output);
archive.directory(sourceDir, false);
archive.finalize();
void archive.finalize();
return p;
};
@@ -332,7 +332,9 @@ export class LogicFunctionExecutorService {
executionId,
}));
this.applicationLogsService.writeLogs(logEntries);
void this.applicationLogsService.writeLogs(logEntries).catch((error) => {
this.logger.error('Failed to persist application logs', error);
});
await this.subscriptionService.publish({
channel: SubscriptionChannel.LOGIC_FUNCTION_LOGS_CHANNEL,
@@ -349,7 +351,7 @@ export class LogicFunctionExecutorService {
},
});
this.auditService
void this.auditService
.createContext({
workspaceId,
})
@@ -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,
@@ -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;
}
@@ -132,7 +132,7 @@ export class MessageQueueExplorer implements OnModuleInit {
}
}
private async handleProcessorGroupCollection(
private handleProcessorGroupCollection(
processorGroupCollection: ProcessorGroup[],
queue: MessageQueueService,
options?: MessageQueueWorkerOptions,
@@ -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);
}
}
@@ -98,7 +98,11 @@ export class MetricsService {
counter.add(1, attributes);
if (shouldStoreInCache && eventId) {
await this.metricsCacheService.updateCounter(key, [eventId]);
try {
await this.metricsCacheService.updateCounter(key, [eventId]);
} catch (error) {
this.logger.error(`Failed to update metrics cache for ${key}`, error);
}
}
if (isDefined(debugLog)) {
@@ -307,7 +307,7 @@ export class AgentAsyncExecutorService {
AiExceptionCode.AGENT_EXECUTION_FAILED,
);
} finally {
this.aiBillingService.calculateAndBillUsage(
void this.aiBillingService.calculateAndBillUsage(
agent?.modelId ?? AUTO_SELECT_SMART_MODEL_ID,
{ usage: accumulatedUsage, cacheCreationTokens },
workspaceId,
@@ -316,7 +316,7 @@ export class AgentAsyncExecutorService {
userWorkspaceId,
);
this.aiBillingService.billNativeWebSearchUsage(
void this.aiBillingService.billNativeWebSearchUsage(
nativeWebSearchCallCount,
workspaceId,
userWorkspaceId,
@@ -80,7 +80,7 @@ export class AgentTurnResolver {
});
const savedTurn = await this.turnRepository.save(turn);
this.messageQueueService.add<{
await this.messageQueueService.add<{
turnId: string;
threadId: string;
agentId: string;
@@ -110,7 +110,7 @@ export const repairToolCall = async ({
? extractCacheCreationTokensFromSteps(steps)
: 0;
billingContext.aiBillingService.calculateAndBillUsage(
void billingContext.aiBillingService.calculateAndBillUsage(
billingContext.modelId,
{ usage, cacheCreationTokens },
billingContext.workspaceId,
@@ -350,8 +350,8 @@ describe('AiBillingService', () => {
});
describe('calculateAndBillUsage', () => {
it('should calculate cost and emit billing event when model exists', () => {
service.calculateAndBillUsage(
it('should calculate cost and emit billing event when model exists', async () => {
await service.calculateAndBillUsage(
'gpt-4o',
{ usage: mockTokenUsage },
'workspace-1',
@@ -298,7 +298,7 @@ export class StreamAgentChatJob {
// Publish all chunks first, then signal completion. This guarantees
// message-persisted arrives after every stream-chunk on the client.
(async () => {
void (async () => {
try {
for await (const chunk of uiStream) {
await this.eventPublisherService.publish({
@@ -63,7 +63,7 @@ export class AgentTitleGenerationService {
? extractCacheCreationTokensFromSteps(steps)
: 0;
this.aiBillingService.calculateAndBillUsage(
void this.aiBillingService.calculateAndBillUsage(
defaultModel.modelId,
{ usage, cacheCreationTokens },
workspaceId,
@@ -315,7 +315,7 @@ export class ChatExecutionService {
// billNativeWebSearchUsage short-circuits when count <= 0, so calling
// unconditionally is safe regardless of whether native search fired.
this.aiBillingService.billNativeWebSearchUsage(
void this.aiBillingService.billNativeWebSearchUsage(
countNativeWebSearchCallsFromSteps(steps),
workspace.id,
userWorkspaceId,
@@ -77,7 +77,7 @@ export class AiGenerateTextController {
};
} finally {
if (result) {
this.aiBillingService.calculateAndBillUsage(
void this.aiBillingService.calculateAndBillUsage(
resolvedModelId,
{
usage: result.usage,
@@ -89,13 +89,13 @@ export class CallWebhookJob {
const success = response.status >= 200 && response.status < 300;
auditService.insertWorkspaceEvent(WEBHOOK_RESPONSE_EVENT, {
void auditService.insertWorkspaceEvent(WEBHOOK_RESPONSE_EVENT, {
status: response.status,
success,
...commonPayload,
});
this.metricsService.incrementCounter({
void this.metricsService.incrementCounter({
key: MetricsKeys.JobWebhookCallCompleted,
shouldStoreInCache: false,
});
@@ -105,7 +105,7 @@ export class CallWebhookJob {
err.message.includes('internal IP address') &&
err.message.includes('is not allowed');
auditService.insertWorkspaceEvent(WEBHOOK_RESPONSE_EVENT, {
void auditService.insertWorkspaceEvent(WEBHOOK_RESPONSE_EVENT, {
success: false,
...commonPayload,
...(err.response && { status: err.response.status }),
@@ -134,7 +134,7 @@ export class CleanerWorkspaceService {
throw new Error('Workspace member email is missing');
}
this.emailService.send({
await this.emailService.send({
to: workspaceMember.userEmail,
from: `${this.twentyConfigService.get(
'EMAIL_FROM_NAME',
@@ -177,18 +177,18 @@ export class CleanerWorkspaceService {
if (!dryRun) {
for (const workspaceMember of workspaceMembers) {
await this.sendWarningEmail(
workspaceMember,
workspace.displayName,
daysSinceInactive,
);
await this.userVarsService.set({
userId: workspaceMember.userId,
workspaceId: workspace.id,
key: USER_WORKSPACE_DELETION_WARNING_SENT_KEY,
value: true,
});
await this.sendWarningEmail(
workspaceMember,
workspace.displayName,
daysSinceInactive,
);
}
}
}
@@ -212,7 +212,7 @@ export class CleanerWorkspaceService {
throw new Error('Workspace member email is missing');
}
this.emailService.send({
await this.emailService.send({
to: workspaceMember.userEmail,
from: `${this.twentyConfigService.get(
'EMAIL_FROM_NAME',
@@ -356,7 +356,7 @@ export class CleanerWorkspaceService {
}
await this.workspaceService.deleteWorkspace(workspace.id);
this.metricsService.incrementCounter({
void this.metricsService.incrementCounter({
key: MetricsKeys.CronJobDeletedWorkspace,
shouldStoreInCache: false,
});
+1 -1
View File
@@ -93,4 +93,4 @@ const bootstrap = async () => {
await app.listen(twentyConfigService.get('NODE_PORT'));
};
bootstrap();
void bootstrap();
@@ -246,7 +246,7 @@ describe('MessagingMessagesImportService', () => {
mockMessageChannel.syncStage =
MessageChannelSyncStage.MESSAGES_IMPORT_PENDING;
expect(
await expect(
service.processMessageBatchImport(
mockMessageChannel as MessageChannelEntity,
mockConnectedAccount,
@@ -269,7 +269,7 @@ export class WorkflowRunnerWorkspaceService {
return false;
} catch {
this.metricsService.incrementCounter({
void this.metricsService.incrementCounter({
key: MetricsKeys.WorkflowRunThrottled,
eventId: workspaceId,
});
@@ -30,4 +30,4 @@ async function bootstrap() {
throw err;
}
}
bootstrap();
void bootstrap();
@@ -212,4 +212,4 @@ const generateTests = async (force = false) => {
const forceArg = process.argv.includes('--force');
// Call the function with the parsed argument
generateTests(forceArg);
void generateTests(forceArg);