* feat: implement webhook tasker with async/sync fallback This PR implements a webhook tasker with async/sync fallback architecture to fix failing E2E tests. The solution follows the existing proration tasker pattern and uses Dependency Injection with @evyweb/ioctopus. Key changes: - Create IWebhookTasker interface for webhook delivery - Implement WebhookSyncTasker for immediate execution (E2E tests) - Implement WebhookAsyncTasker for queued execution (production) - Create main WebhookTasker class extending Tasker<IWebhookTasker> - Add DI modules and tokens for all tasker components - Update WebhookTaskerProducerService to use new WebhookTasker - Add unit tests for sync and async taskers The ENABLE_ASYNC_TASKER flag automatically selects the appropriate mode: - Production: Uses WebhookAsyncTasker to queue tasks - E2E Tests: Uses WebhookSyncTasker for immediate execution This ensures webhooks are delivered immediately in E2E tests without requiring the cron job that processes queued tasks. Co-Authored-By: ali@cal.com <alishahbaz7@gmail.com> * fix: update WebhookTaskerProducerService tests for new interface Update tests to use the new deps-based constructor and mockWebhookTasker.deliverWebhook instead of mockTasker.create Co-Authored-By: ali@cal.com <alishahbaz7@gmail.com> * fix: use moduleLoader pattern for WebhookProducerService in container Co-Authored-By: ali@cal.com <alishahbaz7@gmail.com> * refactor: replace InternalTasker with Trigger.dev for webhook delivery This commit refactors the WebhookTasker to use Trigger.dev instead of InternalTasker, following the pattern established in BookingEmailAndSmsTasker and PlatformOrganizationBillingTasker (PR #26803). Changes: - Replace WebhookAsyncTasker with WebhookTriggerTasker that uses trigger.dev - Create trigger.dev task files (deliver-webhook.ts, config.ts, schema.ts) - Update DI modules to use WebhookTriggerTasker - Remove old InternalTasker-based implementation - Update unit tests for new implementation The WebhookSyncTasker continues to execute webhooks immediately for E2E tests where ENABLE_ASYNC_TASKER is automatically false. Co-Authored-By: ali@cal.com <alishahbaz7@gmail.com> * fix: update comments to reflect Trigger.dev usage instead of InternalTasker Co-Authored-By: unknown <> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
133 lines
6.5 KiB
TypeScript
133 lines
6.5 KiB
TypeScript
import type { WebhookFeature } from "@calcom/features/webhooks/lib/facade/WebhookFeature";
|
|
import type { IWebhookRepository } from "@calcom/features/webhooks/lib/interface/IWebhookRepository";
|
|
import type {
|
|
IBookingWebhookService,
|
|
IFormWebhookService,
|
|
IOOOWebhookService,
|
|
IRecordingWebhookService,
|
|
IWebhookService,
|
|
} from "@calcom/features/webhooks/lib/interface/services";
|
|
import type { IWebhookProducerService } from "@calcom/features/webhooks/lib/interface/WebhookProducerService";
|
|
import type { IWebhookNotifier } from "@calcom/features/webhooks/lib/interface/webhook";
|
|
import type { WebhookTaskConsumer } from "@calcom/features/webhooks/lib/service/WebhookTaskConsumer";
|
|
import { type Container, createContainer } from "@evyweb/ioctopus";
|
|
import { moduleLoader as prismaModuleLoader } from "../../modules/Prisma";
|
|
import { moduleLoader as loggerModuleLoader } from "../../shared/services/logger.service";
|
|
import { taskerServiceModule } from "../../shared/services/tasker.service";
|
|
import { SHARED_TOKENS } from "../../shared/shared.tokens";
|
|
import { bookingWebhookDataFetcherModule } from "../modules/BookingWebhookDataFetcher.module";
|
|
import { formWebhookDataFetcherModule } from "../modules/FormWebhookDataFetcher.module";
|
|
import { oooWebhookDataFetcherModule } from "../modules/OOOWebhookDataFetcher.module";
|
|
import { paymentWebhookDataFetcherModule } from "../modules/PaymentWebhookDataFetcher.module";
|
|
import { recordingWebhookDataFetcherModule } from "../modules/RecordingWebhookDataFetcher.module";
|
|
import { webhookModule } from "../modules/Webhook.module";
|
|
import { moduleLoader as webhookProducerServiceModuleLoader } from "../modules/WebhookProducerService.module";
|
|
import { webhookTaskConsumerModule } from "../modules/WebhookTaskConsumer.module";
|
|
import { WEBHOOK_TOKENS } from "../Webhooks.tokens";
|
|
|
|
const webhookContainer: Container = createContainer();
|
|
|
|
// Load shared infrastructure
|
|
loggerModuleLoader.loadModule(webhookContainer);
|
|
prismaModuleLoader.loadModule(webhookContainer);
|
|
webhookContainer.load(SHARED_TOKENS.TASKER, taskerServiceModule);
|
|
|
|
// Load webhook module (includes cross-table repositories + all webhook services)
|
|
webhookContainer.load(WEBHOOK_TOKENS.WEBHOOK_EVENT_TYPE_REPOSITORY, webhookModule);
|
|
webhookContainer.load(WEBHOOK_TOKENS.WEBHOOK_USER_REPOSITORY, webhookModule);
|
|
webhookContainer.load(WEBHOOK_TOKENS.WEBHOOK_REPOSITORY, webhookModule);
|
|
webhookContainer.load(WEBHOOK_TOKENS.WEBHOOK_SERVICE, webhookModule);
|
|
webhookContainer.load(WEBHOOK_TOKENS.BOOKING_WEBHOOK_SERVICE, webhookModule);
|
|
webhookContainer.load(WEBHOOK_TOKENS.FORM_WEBHOOK_SERVICE, webhookModule);
|
|
webhookContainer.load(WEBHOOK_TOKENS.RECORDING_WEBHOOK_SERVICE, webhookModule);
|
|
webhookContainer.load(WEBHOOK_TOKENS.OOO_WEBHOOK_SERVICE, webhookModule);
|
|
webhookContainer.load(WEBHOOK_TOKENS.PAYLOAD_BUILDER_FACTORY, webhookModule);
|
|
webhookContainer.load(WEBHOOK_TOKENS.WEBHOOK_NOTIFICATION_HANDLER, webhookModule);
|
|
webhookContainer.load(WEBHOOK_TOKENS.WEBHOOK_NOTIFIER, webhookModule);
|
|
|
|
// Load Data Fetchers (Strategy Pattern implementations)
|
|
webhookContainer.load(WEBHOOK_TOKENS.BOOKING_DATA_FETCHER, bookingWebhookDataFetcherModule);
|
|
webhookContainer.load(WEBHOOK_TOKENS.PAYMENT_DATA_FETCHER, paymentWebhookDataFetcherModule);
|
|
webhookContainer.load(WEBHOOK_TOKENS.FORM_DATA_FETCHER, formWebhookDataFetcherModule);
|
|
webhookContainer.load(WEBHOOK_TOKENS.RECORDING_DATA_FETCHER, recordingWebhookDataFetcherModule);
|
|
webhookContainer.load(WEBHOOK_TOKENS.OOO_DATA_FETCHER, oooWebhookDataFetcherModule);
|
|
|
|
// Load Producer/Consumer modules
|
|
// Use moduleLoader pattern for producer service to load WebhookTasker dependencies
|
|
webhookProducerServiceModuleLoader.loadModule(webhookContainer);
|
|
webhookContainer.load(WEBHOOK_TOKENS.WEBHOOK_TASK_CONSUMER, webhookTaskConsumerModule);
|
|
|
|
export { webhookContainer };
|
|
|
|
/**
|
|
* Get the Webhook Task Consumer.
|
|
*
|
|
* This is used internally by the tasker handler (`webhookDelivery.ts`).
|
|
* For application code, use `getWebhookFeature().consumer` instead.
|
|
*/
|
|
export function getWebhookTaskConsumer(): WebhookTaskConsumer {
|
|
return webhookContainer.get<WebhookTaskConsumer>(WEBHOOK_TOKENS.WEBHOOK_TASK_CONSUMER);
|
|
}
|
|
|
|
/**
|
|
* Get the complete Webhook Feature facade (RECOMMENDED).
|
|
*
|
|
* This is the primary interface for webhook functionality.
|
|
* It provides access to all webhook services through a unified, type-safe API.
|
|
*
|
|
* Usage:
|
|
* ```typescript
|
|
* import { getWebhookFeature } from "@calcom/features/di/webhooks/containers/webhook";
|
|
*
|
|
* const webhooks = getWebhookFeature();
|
|
*
|
|
* // Queue a webhook (lightweight - Producer pattern)
|
|
* await webhooks.producer.queueBookingCreatedWebhook({ ... });
|
|
*
|
|
* // Or use event-specific services (legacy - will be deprecated in Phase 6)
|
|
* await webhooks.booking.emitBookingCreated({ ... });
|
|
* ```
|
|
*/
|
|
export function getWebhookFeature(): WebhookFeature {
|
|
return {
|
|
producer: webhookContainer.get<IWebhookProducerService>(WEBHOOK_TOKENS.WEBHOOK_PRODUCER_SERVICE),
|
|
consumer: webhookContainer.get<WebhookTaskConsumer>(WEBHOOK_TOKENS.WEBHOOK_TASK_CONSUMER),
|
|
core: webhookContainer.get<IWebhookService>(WEBHOOK_TOKENS.WEBHOOK_SERVICE),
|
|
booking: webhookContainer.get<IBookingWebhookService>(WEBHOOK_TOKENS.BOOKING_WEBHOOK_SERVICE),
|
|
form: webhookContainer.get<IFormWebhookService>(WEBHOOK_TOKENS.FORM_WEBHOOK_SERVICE),
|
|
recording: webhookContainer.get<IRecordingWebhookService>(WEBHOOK_TOKENS.RECORDING_WEBHOOK_SERVICE),
|
|
ooo: webhookContainer.get<IOOOWebhookService>(WEBHOOK_TOKENS.OOO_WEBHOOK_SERVICE),
|
|
notifier: webhookContainer.get<IWebhookNotifier>(WEBHOOK_TOKENS.WEBHOOK_NOTIFIER),
|
|
repository: webhookContainer.get<IWebhookRepository>(WEBHOOK_TOKENS.WEBHOOK_REPOSITORY),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get only the webhook producer service
|
|
*
|
|
* Use this when you only need to queue webhooks, not consume or manage them.
|
|
* Lighter import footprint for better tree-shaking and faster module loading.
|
|
*
|
|
* Benefits:
|
|
* - Interface Segregation Principle: Import only what you need
|
|
* - Smaller bundle size: Avoid loading entire facade
|
|
* - Clearer intent: "I'm only queueing webhooks"
|
|
*
|
|
* Usage:
|
|
* ```typescript
|
|
* import { getWebhookProducer } from "@calcom/features/di/webhooks";
|
|
*
|
|
* const producer = getWebhookProducer();
|
|
* await producer.queueBookingCreatedWebhook({
|
|
* bookingUid: booking.uid,
|
|
* eventTypeId: eventType.id,
|
|
* userId: user.id,
|
|
* });
|
|
* ```
|
|
*
|
|
* @returns Lightweight webhook producer service (no heavy dependencies)
|
|
*/
|
|
export function getWebhookProducer(): IWebhookProducerService {
|
|
return webhookContainer.get<IWebhookProducerService>(WEBHOOK_TOKENS.WEBHOOK_PRODUCER_SERVICE);
|
|
}
|