@open-mercato/core 0.6.5-develop.4384.1.ce2ec6eaaa → 0.6.5-develop.4393.1.de282b5dfd
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +2 -2
- package/dist/generated/entities/channel_ingest_dead_letter/index.js +25 -0
- package/dist/generated/entities/channel_ingest_dead_letter/index.js.map +7 -0
- package/dist/generated/entities/channel_thread_mapping/index.js +25 -0
- package/dist/generated/entities/channel_thread_mapping/index.js.map +7 -0
- package/dist/generated/entities/channel_thread_token/index.js +17 -0
- package/dist/generated/entities/channel_thread_token/index.js.map +7 -0
- package/dist/generated/entities/communication_channel/index.js +43 -0
- package/dist/generated/entities/communication_channel/index.js.map +7 -0
- package/dist/generated/entities/customer_interaction/index.js +4 -0
- package/dist/generated/entities/customer_interaction/index.js.map +2 -2
- package/dist/generated/entities/external_conversation/index.js +25 -0
- package/dist/generated/entities/external_conversation/index.js.map +7 -0
- package/dist/generated/entities/external_message/index.js +25 -0
- package/dist/generated/entities/external_message/index.js.map +7 -0
- package/dist/generated/entities/integration_credentials/index.js +3 -1
- package/dist/generated/entities/integration_credentials/index.js.map +2 -2
- package/dist/generated/entities/message/index.js +2 -0
- package/dist/generated/entities/message/index.js.map +2 -2
- package/dist/generated/entities/message_channel_link/index.js +33 -0
- package/dist/generated/entities/message_channel_link/index.js.map +7 -0
- package/dist/generated/entities/message_reaction/index.js +25 -0
- package/dist/generated/entities/message_reaction/index.js.map +7 -0
- package/dist/generated/entities.ids.generated.js +11 -0
- package/dist/generated/entities.ids.generated.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +117 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/helpers/integration/authFixtures.js +2 -1
- package/dist/helpers/integration/authFixtures.js.map +2 -2
- package/dist/helpers/integration/communicationChannelsFixtures.js +58 -0
- package/dist/helpers/integration/communicationChannelsFixtures.js.map +7 -0
- package/dist/modules/communication_channels/acl.js +47 -0
- package/dist/modules/communication_channels/acl.js.map +7 -0
- package/dist/modules/communication_channels/api/delete/channels/[id]/route.js +133 -0
- package/dist/modules/communication_channels/api/delete/channels/[id]/route.js.map +7 -0
- package/dist/modules/communication_channels/api/delete/messages/[messageId]/reactions/[reactionId]/route.js +113 -0
- package/dist/modules/communication_channels/api/delete/messages/[messageId]/reactions/[reactionId]/route.js.map +7 -0
- package/dist/modules/communication_channels/api/get/channels/[id]/health/route.js +138 -0
- package/dist/modules/communication_channels/api/get/channels/[id]/health/route.js.map +7 -0
- package/dist/modules/communication_channels/api/get/channels/[id]/route.js +93 -0
- package/dist/modules/communication_channels/api/get/channels/[id]/route.js.map +7 -0
- package/dist/modules/communication_channels/api/get/channels/route.js +96 -0
- package/dist/modules/communication_channels/api/get/channels/route.js.map +7 -0
- package/dist/modules/communication_channels/api/get/me/channels/route.js +82 -0
- package/dist/modules/communication_channels/api/get/me/channels/route.js.map +7 -0
- package/dist/modules/communication_channels/api/get/oauth/[provider]/callback/route.js +274 -0
- package/dist/modules/communication_channels/api/get/oauth/[provider]/callback/route.js.map +7 -0
- package/dist/modules/communication_channels/api/post/channels/[id]/import-history/route.js +168 -0
- package/dist/modules/communication_channels/api/post/channels/[id]/import-history/route.js.map +7 -0
- package/dist/modules/communication_channels/api/post/channels/[id]/poll-now/route.js +143 -0
- package/dist/modules/communication_channels/api/post/channels/[id]/poll-now/route.js.map +7 -0
- package/dist/modules/communication_channels/api/post/channels/[id]/push/register/route.js +127 -0
- package/dist/modules/communication_channels/api/post/channels/[id]/push/register/route.js.map +7 -0
- package/dist/modules/communication_channels/api/post/channels/[id]/set-primary/route.js +99 -0
- package/dist/modules/communication_channels/api/post/channels/[id]/set-primary/route.js.map +7 -0
- package/dist/modules/communication_channels/api/post/channels/[id]/test-send/route.js +197 -0
- package/dist/modules/communication_channels/api/post/channels/[id]/test-send/route.js.map +7 -0
- package/dist/modules/communication_channels/api/post/channels/connect/credentials/route.js +124 -0
- package/dist/modules/communication_channels/api/post/channels/connect/credentials/route.js.map +7 -0
- package/dist/modules/communication_channels/api/post/messages/[messageId]/reactions/route.js +120 -0
- package/dist/modules/communication_channels/api/post/messages/[messageId]/reactions/route.js.map +7 -0
- package/dist/modules/communication_channels/api/post/oauth/[provider]/initiate/route.js +157 -0
- package/dist/modules/communication_channels/api/post/oauth/[provider]/initiate/route.js.map +7 -0
- package/dist/modules/communication_channels/api/post/send-as-user/route.js +115 -0
- package/dist/modules/communication_channels/api/post/send-as-user/route.js.map +7 -0
- package/dist/modules/communication_channels/api/post/test-seed/route.js +217 -0
- package/dist/modules/communication_channels/api/post/test-seed/route.js.map +7 -0
- package/dist/modules/communication_channels/api/post/webhook/[provider]/route.js +175 -0
- package/dist/modules/communication_channels/api/post/webhook/[provider]/route.js.map +7 -0
- package/dist/modules/communication_channels/api/post/webhooks/gmail/route.js +123 -0
- package/dist/modules/communication_channels/api/post/webhooks/gmail/route.js.map +7 -0
- package/dist/modules/communication_channels/api/put/threads/[threadId]/assign/route.js +117 -0
- package/dist/modules/communication_channels/api/put/threads/[threadId]/assign/route.js.map +7 -0
- package/dist/modules/communication_channels/backend/communication_channels/channels/[id]/page.js +180 -0
- package/dist/modules/communication_channels/backend/communication_channels/channels/[id]/page.js.map +7 -0
- package/dist/modules/communication_channels/backend/communication_channels/channels/[id]/page.meta.js +36 -0
- package/dist/modules/communication_channels/backend/communication_channels/channels/[id]/page.meta.js.map +7 -0
- package/dist/modules/communication_channels/backend/communication_channels/channels/page.js +107 -0
- package/dist/modules/communication_channels/backend/communication_channels/channels/page.js.map +7 -0
- package/dist/modules/communication_channels/backend/communication_channels/channels/page.meta.js +38 -0
- package/dist/modules/communication_channels/backend/communication_channels/channels/page.meta.js.map +7 -0
- package/dist/modules/communication_channels/backend/profile/communication-channels/page.js +727 -0
- package/dist/modules/communication_channels/backend/profile/communication-channels/page.js.map +7 -0
- package/dist/modules/communication_channels/backend/profile/communication-channels/page.meta.js +38 -0
- package/dist/modules/communication_channels/backend/profile/communication-channels/page.meta.js.map +7 -0
- package/dist/modules/communication_channels/commands/connect-credential-channel.js +154 -0
- package/dist/modules/communication_channels/commands/connect-credential-channel.js.map +7 -0
- package/dist/modules/communication_channels/commands/delete-channel.js +137 -0
- package/dist/modules/communication_channels/commands/delete-channel.js.map +7 -0
- package/dist/modules/communication_channels/commands/deliver-outbound-message.js +400 -0
- package/dist/modules/communication_channels/commands/deliver-outbound-message.js.map +7 -0
- package/dist/modules/communication_channels/commands/disconnect-channel.js +163 -0
- package/dist/modules/communication_channels/commands/disconnect-channel.js.map +7 -0
- package/dist/modules/communication_channels/commands/ingest-inbound-message.js +413 -0
- package/dist/modules/communication_channels/commands/ingest-inbound-message.js.map +7 -0
- package/dist/modules/communication_channels/commands/interceptors.js +68 -0
- package/dist/modules/communication_channels/commands/interceptors.js.map +7 -0
- package/dist/modules/communication_channels/commands/process-inbound-reaction.js +198 -0
- package/dist/modules/communication_channels/commands/process-inbound-reaction.js.map +7 -0
- package/dist/modules/communication_channels/commands/push-register.js +146 -0
- package/dist/modules/communication_channels/commands/push-register.js.map +7 -0
- package/dist/modules/communication_channels/commands/push-renew.js +23 -0
- package/dist/modules/communication_channels/commands/push-renew.js.map +7 -0
- package/dist/modules/communication_channels/commands/push-unregister.js +108 -0
- package/dist/modules/communication_channels/commands/push-unregister.js.map +7 -0
- package/dist/modules/communication_channels/commands/queue-import-history.js +113 -0
- package/dist/modules/communication_channels/commands/queue-import-history.js.map +7 -0
- package/dist/modules/communication_channels/commands/reassign-conversation.js +193 -0
- package/dist/modules/communication_channels/commands/reassign-conversation.js.map +7 -0
- package/dist/modules/communication_channels/commands/set-primary-channel.js +114 -0
- package/dist/modules/communication_channels/commands/set-primary-channel.js.map +7 -0
- package/dist/modules/communication_channels/commands/toggle-outbound-reaction.js +260 -0
- package/dist/modules/communication_channels/commands/toggle-outbound-reaction.js.map +7 -0
- package/dist/modules/communication_channels/data/enrichers.js +286 -0
- package/dist/modules/communication_channels/data/enrichers.js.map +7 -0
- package/dist/modules/communication_channels/data/entities.js +447 -0
- package/dist/modules/communication_channels/data/entities.js.map +7 -0
- package/dist/modules/communication_channels/data/extensions.js +67 -0
- package/dist/modules/communication_channels/data/extensions.js.map +7 -0
- package/dist/modules/communication_channels/data/validators.js +123 -0
- package/dist/modules/communication_channels/data/validators.js.map +7 -0
- package/dist/modules/communication_channels/di.js +35 -0
- package/dist/modules/communication_channels/di.js.map +7 -0
- package/dist/modules/communication_channels/encryption.js +12 -0
- package/dist/modules/communication_channels/encryption.js.map +7 -0
- package/dist/modules/communication_channels/events.js +124 -0
- package/dist/modules/communication_channels/events.js.map +7 -0
- package/dist/modules/communication_channels/index.js +20 -0
- package/dist/modules/communication_channels/index.js.map +7 -0
- package/dist/modules/communication_channels/lib/access-control.js +43 -0
- package/dist/modules/communication_channels/lib/access-control.js.map +7 -0
- package/dist/modules/communication_channels/lib/adapter-compat.js +36 -0
- package/dist/modules/communication_channels/lib/adapter-compat.js.map +7 -0
- package/dist/modules/communication_channels/lib/adapter-registry-singleton.js +22 -0
- package/dist/modules/communication_channels/lib/adapter-registry-singleton.js.map +7 -0
- package/dist/modules/communication_channels/lib/adapter.js +1 -0
- package/dist/modules/communication_channels/lib/adapter.js.map +7 -0
- package/dist/modules/communication_channels/lib/connect-channel.js +95 -0
- package/dist/modules/communication_channels/lib/connect-channel.js.map +7 -0
- package/dist/modules/communication_channels/lib/contact-resolver.js +79 -0
- package/dist/modules/communication_channels/lib/contact-resolver.js.map +7 -0
- package/dist/modules/communication_channels/lib/credential-refresh.js +97 -0
- package/dist/modules/communication_channels/lib/credential-refresh.js.map +7 -0
- package/dist/modules/communication_channels/lib/dead-letter.js +62 -0
- package/dist/modules/communication_channels/lib/dead-letter.js.map +7 -0
- package/dist/modules/communication_channels/lib/email-capabilities.js +47 -0
- package/dist/modules/communication_channels/lib/email-capabilities.js.map +7 -0
- package/dist/modules/communication_channels/lib/email-contact.js +14 -0
- package/dist/modules/communication_channels/lib/email-contact.js.map +7 -0
- package/dist/modules/communication_channels/lib/email-mime.js +259 -0
- package/dist/modules/communication_channels/lib/email-mime.js.map +7 -0
- package/dist/modules/communication_channels/lib/error-classification.js +101 -0
- package/dist/modules/communication_channels/lib/error-classification.js.map +7 -0
- package/dist/modules/communication_channels/lib/gmail-pubsub-jwt.js +185 -0
- package/dist/modules/communication_channels/lib/gmail-pubsub-jwt.js.map +7 -0
- package/dist/modules/communication_channels/lib/mutation-guards.js +114 -0
- package/dist/modules/communication_channels/lib/mutation-guards.js.map +7 -0
- package/dist/modules/communication_channels/lib/oauth-client-config.js +32 -0
- package/dist/modules/communication_channels/lib/oauth-client-config.js.map +7 -0
- package/dist/modules/communication_channels/lib/oauth-state.js +128 -0
- package/dist/modules/communication_channels/lib/oauth-state.js.map +7 -0
- package/dist/modules/communication_channels/lib/oauth-token.js +45 -0
- package/dist/modules/communication_channels/lib/oauth-token.js.map +7 -0
- package/dist/modules/communication_channels/lib/pg-errors.js +11 -0
- package/dist/modules/communication_channels/lib/pg-errors.js.map +7 -0
- package/dist/modules/communication_channels/lib/provider-health.js +24 -0
- package/dist/modules/communication_channels/lib/provider-health.js.map +7 -0
- package/dist/modules/communication_channels/lib/push-state.js +19 -0
- package/dist/modules/communication_channels/lib/push-state.js.map +7 -0
- package/dist/modules/communication_channels/lib/queue.js +54 -0
- package/dist/modules/communication_channels/lib/queue.js.map +7 -0
- package/dist/modules/communication_channels/lib/reaction-processor-types.js +5 -0
- package/dist/modules/communication_channels/lib/reaction-processor-types.js.map +7 -0
- package/dist/modules/communication_channels/lib/reaction-semantics.js +11 -0
- package/dist/modules/communication_channels/lib/reaction-semantics.js.map +7 -0
- package/dist/modules/communication_channels/lib/registry.js +67 -0
- package/dist/modules/communication_channels/lib/registry.js.map +7 -0
- package/dist/modules/communication_channels/lib/route-mutation-guard.js +43 -0
- package/dist/modules/communication_channels/lib/route-mutation-guard.js.map +7 -0
- package/dist/modules/communication_channels/lib/sanitize-channel-html.js +96 -0
- package/dist/modules/communication_channels/lib/sanitize-channel-html.js.map +7 -0
- package/dist/modules/communication_channels/lib/send-as-user.js +194 -0
- package/dist/modules/communication_channels/lib/send-as-user.js.map +7 -0
- package/dist/modules/communication_channels/lib/system-user.js +22 -0
- package/dist/modules/communication_channels/lib/system-user.js.map +7 -0
- package/dist/modules/communication_channels/lib/test-seed.js +68 -0
- package/dist/modules/communication_channels/lib/test-seed.js.map +7 -0
- package/dist/modules/communication_channels/lib/thread-matcher.js +263 -0
- package/dist/modules/communication_channels/lib/thread-matcher.js.map +7 -0
- package/dist/modules/communication_channels/lib/thread-token.js +219 -0
- package/dist/modules/communication_channels/lib/thread-token.js.map +7 -0
- package/dist/modules/communication_channels/lib/use-connect-channel.js +61 -0
- package/dist/modules/communication_channels/lib/use-connect-channel.js.map +7 -0
- package/dist/modules/communication_channels/migrations/Migration20260526134719_communication_channels.js +50 -0
- package/dist/modules/communication_channels/migrations/Migration20260526134719_communication_channels.js.map +7 -0
- package/dist/modules/communication_channels/migrations/Migration20260527195446_communication_channels.js +19 -0
- package/dist/modules/communication_channels/migrations/Migration20260527195446_communication_channels.js.map +7 -0
- package/dist/modules/communication_channels/migrations/Migration20260529231848_communication_channels.js +13 -0
- package/dist/modules/communication_channels/migrations/Migration20260529231848_communication_channels.js.map +7 -0
- package/dist/modules/communication_channels/migrations/Migration20260531120000_communication_channels.js +17 -0
- package/dist/modules/communication_channels/migrations/Migration20260531120000_communication_channels.js.map +7 -0
- package/dist/modules/communication_channels/notifications.client.js +51 -0
- package/dist/modules/communication_channels/notifications.client.js.map +7 -0
- package/dist/modules/communication_channels/notifications.handlers.js +53 -0
- package/dist/modules/communication_channels/notifications.handlers.js.map +7 -0
- package/dist/modules/communication_channels/notifications.js +56 -0
- package/dist/modules/communication_channels/notifications.js.map +7 -0
- package/dist/modules/communication_channels/setup.js +105 -0
- package/dist/modules/communication_channels/setup.js.map +7 -0
- package/dist/modules/communication_channels/subscribers/channel-requires-reauth-notification.js +71 -0
- package/dist/modules/communication_channels/subscribers/channel-requires-reauth-notification.js.map +7 -0
- package/dist/modules/communication_channels/subscribers/outbound-bridge.js +103 -0
- package/dist/modules/communication_channels/subscribers/outbound-bridge.js.map +7 -0
- package/dist/modules/communication_channels/subscribers/user-deleted-cascade.js +51 -0
- package/dist/modules/communication_channels/subscribers/user-deleted-cascade.js.map +7 -0
- package/dist/modules/communication_channels/widgets/components.js +7 -0
- package/dist/modules/communication_channels/widgets/components.js.map +7 -0
- package/dist/modules/communication_channels/widgets/injection/channel-badge/widget.client.js +18 -0
- package/dist/modules/communication_channels/widgets/injection/channel-badge/widget.client.js.map +7 -0
- package/dist/modules/communication_channels/widgets/injection/channel-badge/widget.js +30 -0
- package/dist/modules/communication_channels/widgets/injection/channel-badge/widget.js.map +7 -0
- package/dist/modules/communication_channels/widgets/injection/channel-info-panel/widget.client.js +185 -0
- package/dist/modules/communication_channels/widgets/injection/channel-info-panel/widget.client.js.map +7 -0
- package/dist/modules/communication_channels/widgets/injection/channel-info-panel/widget.js +17 -0
- package/dist/modules/communication_channels/widgets/injection/channel-info-panel/widget.js.map +7 -0
- package/dist/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.client.js +44 -0
- package/dist/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.client.js.map +7 -0
- package/dist/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.js +17 -0
- package/dist/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.js.map +7 -0
- package/dist/modules/communication_channels/widgets/injection/profile-channels-menu/widget.js +23 -0
- package/dist/modules/communication_channels/widgets/injection/profile-channels-menu/widget.js.map +7 -0
- package/dist/modules/communication_channels/widgets/injection/reaction-bar/widget.client.js +141 -0
- package/dist/modules/communication_channels/widgets/injection/reaction-bar/widget.client.js.map +7 -0
- package/dist/modules/communication_channels/widgets/injection/reaction-bar/widget.js +17 -0
- package/dist/modules/communication_channels/widgets/injection/reaction-bar/widget.js.map +7 -0
- package/dist/modules/communication_channels/widgets/injection-table.js +38 -0
- package/dist/modules/communication_channels/widgets/injection-table.js.map +7 -0
- package/dist/modules/communication_channels/widgets/notifications/ChannelRequiresReauthRenderer.js +25 -0
- package/dist/modules/communication_channels/widgets/notifications/ChannelRequiresReauthRenderer.js.map +7 -0
- package/dist/modules/communication_channels/widgets/notifications/MessageReceivedRenderer.js +19 -0
- package/dist/modules/communication_channels/widgets/notifications/MessageReceivedRenderer.js.map +7 -0
- package/dist/modules/communication_channels/widgets/notifications/index.js +7 -0
- package/dist/modules/communication_channels/widgets/notifications/index.js.map +7 -0
- package/dist/modules/communication_channels/workers/channel-import-history.js +185 -0
- package/dist/modules/communication_channels/workers/channel-import-history.js.map +7 -0
- package/dist/modules/communication_channels/workers/gmail-history-sync.js +154 -0
- package/dist/modules/communication_channels/workers/gmail-history-sync.js.map +7 -0
- package/dist/modules/communication_channels/workers/gmail-renew-watch.js +95 -0
- package/dist/modules/communication_channels/workers/gmail-renew-watch.js.map +7 -0
- package/dist/modules/communication_channels/workers/inbound-processor.js +56 -0
- package/dist/modules/communication_channels/workers/inbound-processor.js.map +7 -0
- package/dist/modules/communication_channels/workers/outbound-delivery.js +85 -0
- package/dist/modules/communication_channels/workers/outbound-delivery.js.map +7 -0
- package/dist/modules/communication_channels/workers/poll-channel.js +240 -0
- package/dist/modules/communication_channels/workers/poll-channel.js.map +7 -0
- package/dist/modules/communication_channels/workers/poll-tick.js +132 -0
- package/dist/modules/communication_channels/workers/poll-tick.js.map +7 -0
- package/dist/modules/communication_channels/workers/reaction-processor.js +192 -0
- package/dist/modules/communication_channels/workers/reaction-processor.js.map +7 -0
- package/dist/modules/customers/acl.js +18 -0
- package/dist/modules/customers/acl.js.map +2 -2
- package/dist/modules/customers/api/activities/route.js +9 -0
- package/dist/modules/customers/api/activities/route.js.map +2 -2
- package/dist/modules/customers/api/companies/[id]/route.js +18 -7
- package/dist/modules/customers/api/companies/[id]/route.js.map +2 -2
- package/dist/modules/customers/api/interactions/[id]/visibility/route.js +151 -0
- package/dist/modules/customers/api/interactions/[id]/visibility/route.js.map +7 -0
- package/dist/modules/customers/api/interactions/counts/route.js +6 -0
- package/dist/modules/customers/api/interactions/counts/route.js.map +2 -2
- package/dist/modules/customers/api/interactions/route.js +26 -7
- package/dist/modules/customers/api/interactions/route.js.map +2 -2
- package/dist/modules/customers/api/people/[id]/email-threads/route.js +82 -0
- package/dist/modules/customers/api/people/[id]/email-threads/route.js.map +7 -0
- package/dist/modules/customers/api/people/[id]/emails/route.js +157 -0
- package/dist/modules/customers/api/people/[id]/emails/route.js.map +7 -0
- package/dist/modules/customers/api/people/[id]/route.js +12 -4
- package/dist/modules/customers/api/people/[id]/route.js.map +2 -2
- package/dist/modules/customers/backend/customers/people-v2/[id]/page.js +10 -0
- package/dist/modules/customers/backend/customers/people-v2/[id]/page.js.map +2 -2
- package/dist/modules/customers/commands/deals.js +46 -5
- package/dist/modules/customers/commands/deals.js.map +2 -2
- package/dist/modules/customers/commands/interactions.js +16 -0
- package/dist/modules/customers/commands/interactions.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivityCard.js +32 -0
- package/dist/modules/customers/components/detail/ActivityCard.js.map +2 -2
- package/dist/modules/customers/components/detail/ComposeEmailDialog.js +242 -0
- package/dist/modules/customers/components/detail/ComposeEmailDialog.js.map +7 -0
- package/dist/modules/customers/components/detail/DealForm.js +2 -1
- package/dist/modules/customers/components/detail/DealForm.js.map +2 -2
- package/dist/modules/customers/components/detail/DealsSection.js +10 -0
- package/dist/modules/customers/components/detail/DealsSection.js.map +2 -2
- package/dist/modules/customers/components/detail/EmailCardActions.js +179 -0
- package/dist/modules/customers/components/detail/EmailCardActions.js.map +7 -0
- package/dist/modules/customers/components/detail/EmailReplyForwardActions.js +52 -0
- package/dist/modules/customers/components/detail/EmailReplyForwardActions.js.map +7 -0
- package/dist/modules/customers/components/detail/PersonDetailTabs.js +7 -1
- package/dist/modules/customers/components/detail/PersonDetailTabs.js.map +2 -2
- package/dist/modules/customers/components/detail/PersonEmailThreadsTab.js +366 -0
- package/dist/modules/customers/components/detail/PersonEmailThreadsTab.js.map +7 -0
- package/dist/modules/customers/data/enrichers.js +133 -2
- package/dist/modules/customers/data/enrichers.js.map +2 -2
- package/dist/modules/customers/data/entities.js +18 -0
- package/dist/modules/customers/data/entities.js.map +2 -2
- package/dist/modules/customers/data/extensions.js +16 -0
- package/dist/modules/customers/data/extensions.js.map +7 -0
- package/dist/modules/customers/encryption.js +11 -0
- package/dist/modules/customers/encryption.js.map +2 -2
- package/dist/modules/customers/events.js +4 -1
- package/dist/modules/customers/events.js.map +2 -2
- package/dist/modules/customers/lib/findPeopleByAddresses.js +64 -0
- package/dist/modules/customers/lib/findPeopleByAddresses.js.map +7 -0
- package/dist/modules/customers/lib/kysely.js.map +2 -2
- package/dist/modules/customers/lib/link-channel-message-handler.js +303 -0
- package/dist/modules/customers/lib/link-channel-message-handler.js.map +7 -0
- package/dist/modules/customers/lib/personEmailThreads.js +205 -0
- package/dist/modules/customers/lib/personEmailThreads.js.map +7 -0
- package/dist/modules/customers/lib/visibilityFilter.js +51 -0
- package/dist/modules/customers/lib/visibilityFilter.js.map +7 -0
- package/dist/modules/customers/migrations/Migration20260527012240_customers.js +20 -0
- package/dist/modules/customers/migrations/Migration20260527012240_customers.js.map +7 -0
- package/dist/modules/customers/setup.js +2 -1
- package/dist/modules/customers/setup.js.map +2 -2
- package/dist/modules/customers/subscribers/link-channel-message-received.js +12 -0
- package/dist/modules/customers/subscribers/link-channel-message-received.js.map +7 -0
- package/dist/modules/customers/subscribers/link-channel-message-sent.js +12 -0
- package/dist/modules/customers/subscribers/link-channel-message-sent.js.map +7 -0
- package/dist/modules/integrations/data/entities.js +8 -1
- package/dist/modules/integrations/data/entities.js.map +2 -2
- package/dist/modules/integrations/lib/credentials-service.js +29 -14
- package/dist/modules/integrations/lib/credentials-service.js.map +2 -2
- package/dist/modules/integrations/migrations/Migration20260526154136_integrations.js +15 -0
- package/dist/modules/integrations/migrations/Migration20260526154136_integrations.js.map +7 -0
- package/dist/modules/messages/commands/messages.js +70 -8
- package/dist/modules/messages/commands/messages.js.map +2 -2
- package/dist/modules/messages/components/ComposeMessagePageClient.js +24 -13
- package/dist/modules/messages/components/ComposeMessagePageClient.js.map +2 -2
- package/dist/modules/messages/components/MessageDetailPageClient.js +39 -2
- package/dist/modules/messages/components/MessageDetailPageClient.js.map +2 -2
- package/dist/modules/messages/components/MessagesInboxPageClient.js +1 -0
- package/dist/modules/messages/components/MessagesInboxPageClient.js.map +2 -2
- package/dist/modules/messages/data/entities.js +8 -1
- package/dist/modules/messages/data/entities.js.map +2 -2
- package/dist/modules/messages/migrations/Migration20260531130000.js +15 -0
- package/dist/modules/messages/migrations/Migration20260531130000.js.map +7 -0
- package/dist/modules/messages/widgets/injection-table.js +7 -0
- package/dist/modules/messages/widgets/injection-table.js.map +7 -0
- package/generated/entities/channel_ingest_dead_letter/index.ts +11 -0
- package/generated/entities/channel_thread_mapping/index.ts +11 -0
- package/generated/entities/channel_thread_token/index.ts +7 -0
- package/generated/entities/communication_channel/index.ts +20 -0
- package/generated/entities/customer_interaction/index.ts +2 -0
- package/generated/entities/external_conversation/index.ts +11 -0
- package/generated/entities/external_message/index.ts +11 -0
- package/generated/entities/integration_credentials/index.ts +1 -0
- package/generated/entities/message/index.ts +1 -0
- package/generated/entities/message_channel_link/index.ts +15 -0
- package/generated/entities/message_reaction/index.ts +11 -0
- package/generated/entities.ids.generated.ts +11 -0
- package/generated/entity-fields-registry.ts +117 -0
- package/package.json +9 -7
- package/src/helpers/integration/authFixtures.ts +4 -1
- package/src/helpers/integration/communicationChannelsFixtures.ts +124 -0
- package/src/modules/communication_channels/acl.ts +43 -0
- package/src/modules/communication_channels/api/delete/channels/[id]/route.ts +163 -0
- package/src/modules/communication_channels/api/delete/messages/[messageId]/reactions/[reactionId]/route.ts +143 -0
- package/src/modules/communication_channels/api/get/channels/[id]/health/route.ts +173 -0
- package/src/modules/communication_channels/api/get/channels/[id]/route.ts +111 -0
- package/src/modules/communication_channels/api/get/channels/route.ts +109 -0
- package/src/modules/communication_channels/api/get/me/channels/route.ts +100 -0
- package/src/modules/communication_channels/api/get/oauth/[provider]/callback/route.ts +355 -0
- package/src/modules/communication_channels/api/post/channels/[id]/import-history/route.ts +206 -0
- package/src/modules/communication_channels/api/post/channels/[id]/poll-now/route.ts +174 -0
- package/src/modules/communication_channels/api/post/channels/[id]/push/register/route.ts +158 -0
- package/src/modules/communication_channels/api/post/channels/[id]/set-primary/route.ts +114 -0
- package/src/modules/communication_channels/api/post/channels/[id]/test-send/route.ts +241 -0
- package/src/modules/communication_channels/api/post/channels/connect/credentials/route.ts +134 -0
- package/src/modules/communication_channels/api/post/messages/[messageId]/reactions/route.ts +143 -0
- package/src/modules/communication_channels/api/post/oauth/[provider]/initiate/route.ts +192 -0
- package/src/modules/communication_channels/api/post/send-as-user/route.ts +125 -0
- package/src/modules/communication_channels/api/post/test-seed/route.ts +267 -0
- package/src/modules/communication_channels/api/post/webhook/[provider]/route.ts +227 -0
- package/src/modules/communication_channels/api/post/webhooks/gmail/route.ts +161 -0
- package/src/modules/communication_channels/api/put/threads/[threadId]/assign/route.ts +132 -0
- package/src/modules/communication_channels/backend/communication_channels/channels/[id]/page.meta.ts +34 -0
- package/src/modules/communication_channels/backend/communication_channels/channels/[id]/page.tsx +250 -0
- package/src/modules/communication_channels/backend/communication_channels/channels/page.meta.ts +36 -0
- package/src/modules/communication_channels/backend/communication_channels/channels/page.tsx +137 -0
- package/src/modules/communication_channels/backend/profile/communication-channels/page.meta.ts +36 -0
- package/src/modules/communication_channels/backend/profile/communication-channels/page.tsx +907 -0
- package/src/modules/communication_channels/commands/connect-credential-channel.ts +243 -0
- package/src/modules/communication_channels/commands/delete-channel.ts +193 -0
- package/src/modules/communication_channels/commands/deliver-outbound-message.ts +579 -0
- package/src/modules/communication_channels/commands/disconnect-channel.ts +241 -0
- package/src/modules/communication_channels/commands/ingest-inbound-message.ts +602 -0
- package/src/modules/communication_channels/commands/interceptors.ts +104 -0
- package/src/modules/communication_channels/commands/process-inbound-reaction.ts +265 -0
- package/src/modules/communication_channels/commands/push-register.ts +203 -0
- package/src/modules/communication_channels/commands/push-renew.ts +49 -0
- package/src/modules/communication_channels/commands/push-unregister.ts +168 -0
- package/src/modules/communication_channels/commands/queue-import-history.ts +180 -0
- package/src/modules/communication_channels/commands/reassign-conversation.ts +273 -0
- package/src/modules/communication_channels/commands/set-primary-channel.ts +154 -0
- package/src/modules/communication_channels/commands/toggle-outbound-reaction.ts +347 -0
- package/src/modules/communication_channels/data/enrichers.ts +413 -0
- package/src/modules/communication_channels/data/entities.ts +546 -0
- package/src/modules/communication_channels/data/extensions.ts +76 -0
- package/src/modules/communication_channels/data/validators.ts +138 -0
- package/src/modules/communication_channels/di.ts +40 -0
- package/src/modules/communication_channels/encryption.ts +44 -0
- package/src/modules/communication_channels/events.ts +122 -0
- package/src/modules/communication_channels/i18n/de.json +138 -0
- package/src/modules/communication_channels/i18n/en.json +138 -0
- package/src/modules/communication_channels/i18n/es.json +138 -0
- package/src/modules/communication_channels/i18n/pl.json +138 -0
- package/src/modules/communication_channels/index.ts +19 -0
- package/src/modules/communication_channels/lib/access-control.ts +110 -0
- package/src/modules/communication_channels/lib/adapter-compat.ts +57 -0
- package/src/modules/communication_channels/lib/adapter-registry-singleton.ts +35 -0
- package/src/modules/communication_channels/lib/adapter.ts +605 -0
- package/src/modules/communication_channels/lib/connect-channel.ts +163 -0
- package/src/modules/communication_channels/lib/contact-resolver.ts +162 -0
- package/src/modules/communication_channels/lib/credential-refresh.ts +197 -0
- package/src/modules/communication_channels/lib/dead-letter.ts +87 -0
- package/src/modules/communication_channels/lib/email-capabilities.ts +60 -0
- package/src/modules/communication_channels/lib/email-contact.ts +17 -0
- package/src/modules/communication_channels/lib/email-mime.ts +425 -0
- package/src/modules/communication_channels/lib/error-classification.ts +144 -0
- package/src/modules/communication_channels/lib/gmail-pubsub-jwt.ts +278 -0
- package/src/modules/communication_channels/lib/mutation-guards.ts +215 -0
- package/src/modules/communication_channels/lib/oauth-client-config.ts +79 -0
- package/src/modules/communication_channels/lib/oauth-state.ts +228 -0
- package/src/modules/communication_channels/lib/oauth-token.ts +81 -0
- package/src/modules/communication_channels/lib/pg-errors.ts +12 -0
- package/src/modules/communication_channels/lib/provider-health.ts +47 -0
- package/src/modules/communication_channels/lib/push-state.ts +38 -0
- package/src/modules/communication_channels/lib/queue.ts +66 -0
- package/src/modules/communication_channels/lib/reaction-processor-types.ts +51 -0
- package/src/modules/communication_channels/lib/reaction-semantics.ts +48 -0
- package/src/modules/communication_channels/lib/registry.ts +99 -0
- package/src/modules/communication_channels/lib/route-mutation-guard.ts +68 -0
- package/src/modules/communication_channels/lib/sanitize-channel-html.ts +129 -0
- package/src/modules/communication_channels/lib/send-as-user.ts +284 -0
- package/src/modules/communication_channels/lib/system-user.ts +74 -0
- package/src/modules/communication_channels/lib/test-seed.ts +140 -0
- package/src/modules/communication_channels/lib/thread-matcher.ts +430 -0
- package/src/modules/communication_channels/lib/thread-token.ts +355 -0
- package/src/modules/communication_channels/lib/use-connect-channel.ts +73 -0
- package/src/modules/communication_channels/migrations/.snapshot-open-mercato.json +2142 -0
- package/src/modules/communication_channels/migrations/Migration20260526134719_communication_channels.ts +55 -0
- package/src/modules/communication_channels/migrations/Migration20260527195446_communication_channels.ts +20 -0
- package/src/modules/communication_channels/migrations/Migration20260529231848_communication_channels.ts +13 -0
- package/src/modules/communication_channels/migrations/Migration20260531120000_communication_channels.ts +24 -0
- package/src/modules/communication_channels/notifications.client.ts +50 -0
- package/src/modules/communication_channels/notifications.handlers.ts +86 -0
- package/src/modules/communication_channels/notifications.ts +52 -0
- package/src/modules/communication_channels/setup.ts +158 -0
- package/src/modules/communication_channels/subscribers/channel-requires-reauth-notification.ts +118 -0
- package/src/modules/communication_channels/subscribers/outbound-bridge.ts +175 -0
- package/src/modules/communication_channels/subscribers/user-deleted-cascade.ts +100 -0
- package/src/modules/communication_channels/widgets/components.ts +36 -0
- package/src/modules/communication_channels/widgets/injection/channel-badge/widget.client.tsx +38 -0
- package/src/modules/communication_channels/widgets/injection/channel-badge/widget.ts +51 -0
- package/src/modules/communication_channels/widgets/injection/channel-info-panel/widget.client.tsx +278 -0
- package/src/modules/communication_channels/widgets/injection/channel-info-panel/widget.ts +24 -0
- package/src/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.client.tsx +63 -0
- package/src/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.ts +29 -0
- package/src/modules/communication_channels/widgets/injection/profile-channels-menu/widget.ts +34 -0
- package/src/modules/communication_channels/widgets/injection/reaction-bar/widget.client.tsx +177 -0
- package/src/modules/communication_channels/widgets/injection/reaction-bar/widget.ts +26 -0
- package/src/modules/communication_channels/widgets/injection-table.ts +47 -0
- package/src/modules/communication_channels/widgets/notifications/ChannelRequiresReauthRenderer.tsx +48 -0
- package/src/modules/communication_channels/widgets/notifications/MessageReceivedRenderer.tsx +45 -0
- package/src/modules/communication_channels/widgets/notifications/index.ts +2 -0
- package/src/modules/communication_channels/workers/channel-import-history.ts +252 -0
- package/src/modules/communication_channels/workers/gmail-history-sync.ts +223 -0
- package/src/modules/communication_channels/workers/gmail-renew-watch.ts +141 -0
- package/src/modules/communication_channels/workers/inbound-processor.ts +114 -0
- package/src/modules/communication_channels/workers/outbound-delivery.ts +155 -0
- package/src/modules/communication_channels/workers/poll-channel.ts +391 -0
- package/src/modules/communication_channels/workers/poll-tick.ts +210 -0
- package/src/modules/communication_channels/workers/reaction-processor.ts +264 -0
- package/src/modules/customers/acl.ts +18 -0
- package/src/modules/customers/api/activities/route.ts +13 -0
- package/src/modules/customers/api/companies/[id]/route.ts +21 -1
- package/src/modules/customers/api/interactions/[id]/visibility/route.ts +179 -0
- package/src/modules/customers/api/interactions/counts/route.ts +10 -0
- package/src/modules/customers/api/interactions/route.ts +51 -5
- package/src/modules/customers/api/people/[id]/email-threads/route.ts +92 -0
- package/src/modules/customers/api/people/[id]/emails/route.ts +184 -0
- package/src/modules/customers/api/people/[id]/route.ts +17 -2
- package/src/modules/customers/backend/customers/people-v2/[id]/page.tsx +11 -1
- package/src/modules/customers/commands/deals.ts +65 -6
- package/src/modules/customers/commands/interactions.ts +30 -0
- package/src/modules/customers/components/detail/ActivityCard.tsx +48 -0
- package/src/modules/customers/components/detail/ComposeEmailDialog.tsx +329 -0
- package/src/modules/customers/components/detail/DealForm.tsx +2 -1
- package/src/modules/customers/components/detail/DealsSection.tsx +26 -0
- package/src/modules/customers/components/detail/EmailCardActions.tsx +258 -0
- package/src/modules/customers/components/detail/EmailReplyForwardActions.tsx +53 -0
- package/src/modules/customers/components/detail/PersonDetailTabs.tsx +8 -1
- package/src/modules/customers/components/detail/PersonEmailThreadsTab.tsx +448 -0
- package/src/modules/customers/data/enrichers.ts +252 -1
- package/src/modules/customers/data/entities.ts +46 -1
- package/src/modules/customers/data/extensions.ts +26 -0
- package/src/modules/customers/encryption.ts +11 -0
- package/src/modules/customers/events.ts +4 -0
- package/src/modules/customers/i18n/de.json +41 -0
- package/src/modules/customers/i18n/en.json +41 -0
- package/src/modules/customers/i18n/es.json +41 -0
- package/src/modules/customers/i18n/pl.json +41 -0
- package/src/modules/customers/lib/findPeopleByAddresses.ts +107 -0
- package/src/modules/customers/lib/kysely.ts +16 -0
- package/src/modules/customers/lib/link-channel-message-handler.ts +571 -0
- package/src/modules/customers/lib/personEmailThreads.ts +325 -0
- package/src/modules/customers/lib/visibilityFilter.ts +152 -0
- package/src/modules/customers/migrations/.snapshot-open-mercato.json +61 -0
- package/src/modules/customers/migrations/Migration20260527012240_customers.ts +23 -0
- package/src/modules/customers/setup.ts +1 -0
- package/src/modules/customers/subscribers/link-channel-message-received.ts +21 -0
- package/src/modules/customers/subscribers/link-channel-message-sent.ts +21 -0
- package/src/modules/integrations/AGENTS.md +9 -0
- package/src/modules/integrations/data/entities.ts +21 -1
- package/src/modules/integrations/lib/credentials-service.ts +49 -13
- package/src/modules/integrations/migrations/.snapshot-open-mercato.json +26 -1
- package/src/modules/integrations/migrations/Migration20260526154136_integrations.ts +15 -0
- package/src/modules/messages/commands/messages.ts +101 -8
- package/src/modules/messages/components/ComposeMessagePageClient.tsx +17 -0
- package/src/modules/messages/components/MessageDetailPageClient.tsx +43 -0
- package/src/modules/messages/components/MessagesInboxPageClient.tsx +4 -0
- package/src/modules/messages/data/entities.ts +11 -0
- package/src/modules/messages/migrations/.snapshot-open-mercato.json +18 -0
- package/src/modules/messages/migrations/Migration20260531130000.ts +15 -0
- package/src/modules/messages/widgets/injection-table.ts +29 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
2
|
+
import { CommunicationChannel } from "../data/entities.js";
|
|
3
|
+
import {
|
|
4
|
+
COMMUNICATION_CHANNELS_INGEST_INBOUND_COMMAND_ID
|
|
5
|
+
} from "../commands/ingest-inbound-message.js";
|
|
6
|
+
import { COMMUNICATION_CHANNELS_QUEUES, getCommunicationChannelsQueue } from "../lib/queue.js";
|
|
7
|
+
import { preservePushState } from "../lib/push-state.js";
|
|
8
|
+
import { writeIngestDeadLetter } from "../lib/dead-letter.js";
|
|
9
|
+
import { classifyOutboundError, computeBackoffMs, isReauthError } from "../lib/error-classification.js";
|
|
10
|
+
import { refreshCredentialsIfNeeded } from "../lib/credential-refresh.js";
|
|
11
|
+
import { emitCommunicationChannelsEvent } from "../events.js";
|
|
12
|
+
const POLL_CHANNEL_MAX_ATTEMPTS = 3;
|
|
13
|
+
const MAX_DRAIN_PAGES = 100;
|
|
14
|
+
const metadata = {
|
|
15
|
+
queue: COMMUNICATION_CHANNELS_QUEUES.poll,
|
|
16
|
+
id: "communication_channels:poll-channel",
|
|
17
|
+
concurrency: 10
|
|
18
|
+
};
|
|
19
|
+
async function handle(job, ctx) {
|
|
20
|
+
const { channelId, scope, attempt = 1, drainPage = 0 } = job.payload;
|
|
21
|
+
const em = ctx.resolve("em").fork();
|
|
22
|
+
const adapterRegistry = ctx.resolve("channelAdapterRegistry");
|
|
23
|
+
const channel = await findOneWithDecryption(
|
|
24
|
+
em,
|
|
25
|
+
CommunicationChannel,
|
|
26
|
+
{
|
|
27
|
+
id: channelId,
|
|
28
|
+
tenantId: scope.tenantId,
|
|
29
|
+
organizationId: scope.organizationId ?? null,
|
|
30
|
+
deletedAt: null
|
|
31
|
+
},
|
|
32
|
+
void 0,
|
|
33
|
+
scope
|
|
34
|
+
);
|
|
35
|
+
if (!channel) {
|
|
36
|
+
console.warn(
|
|
37
|
+
`[communication_channels:poll-channel] channel ${channelId} not found (skipping)`
|
|
38
|
+
);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (!channel.isActive) return;
|
|
42
|
+
if (channel.status !== "connected" && channel.status !== "error") return;
|
|
43
|
+
const adapter = adapterRegistry?.get(channel.providerKey);
|
|
44
|
+
if (!adapter) {
|
|
45
|
+
console.warn(
|
|
46
|
+
`[communication_channels:poll-channel] no adapter for provider '${channel.providerKey}' (channel ${channelId})`
|
|
47
|
+
);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const capabilities = channel.capabilities ?? null;
|
|
51
|
+
if (capabilities?.realtimePush !== false) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (typeof adapter.fetchHistory !== "function") {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
let credentialsService = null;
|
|
58
|
+
try {
|
|
59
|
+
credentialsService = ctx.resolve("integrationCredentialsService");
|
|
60
|
+
} catch {
|
|
61
|
+
credentialsService = null;
|
|
62
|
+
}
|
|
63
|
+
const credentialsScope = {
|
|
64
|
+
tenantId: scope.tenantId,
|
|
65
|
+
organizationId: scope.organizationId ?? scope.tenantId,
|
|
66
|
+
userId: channel.userId ?? null
|
|
67
|
+
};
|
|
68
|
+
let credentials = {};
|
|
69
|
+
if (channel.credentialsRef && credentialsService) {
|
|
70
|
+
try {
|
|
71
|
+
credentials = await credentialsService.resolve(`channel_${channel.providerKey}`, credentialsScope) ?? {};
|
|
72
|
+
} catch {
|
|
73
|
+
credentials = {};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const refreshed = await refreshCredentialsIfNeeded(
|
|
77
|
+
{
|
|
78
|
+
adapter,
|
|
79
|
+
channelId: channel.id,
|
|
80
|
+
credentials,
|
|
81
|
+
scope: credentialsScope
|
|
82
|
+
},
|
|
83
|
+
{ credentialsService }
|
|
84
|
+
);
|
|
85
|
+
credentials = refreshed.credentials;
|
|
86
|
+
let normalized = [];
|
|
87
|
+
let nextCursor;
|
|
88
|
+
let hasMore = false;
|
|
89
|
+
try {
|
|
90
|
+
const result = await adapter.fetchHistory({
|
|
91
|
+
conversationId: channel.externalIdentifier ?? channel.id,
|
|
92
|
+
credentials,
|
|
93
|
+
cursor: channel.lastPolledAt ? channel.lastPolledAt.toISOString() : void 0,
|
|
94
|
+
channelState: channel.channelState ?? void 0,
|
|
95
|
+
scope: {
|
|
96
|
+
tenantId: scope.tenantId,
|
|
97
|
+
organizationId: scope.organizationId ?? scope.tenantId
|
|
98
|
+
},
|
|
99
|
+
// `contactFilter.sinceDays` is a hint to provider adapters about how far
|
|
100
|
+
// back to look on first-poll bootstrap. For UID-incremental polls (every
|
|
101
|
+
// tick after the first), adapters ignore this and just fetch new mail
|
|
102
|
+
// since the persisted cursor.
|
|
103
|
+
contactFilter: { addresses: [], sinceDays: 7 }
|
|
104
|
+
});
|
|
105
|
+
normalized = Array.isArray(result?.messages) ? result.messages : [];
|
|
106
|
+
nextCursor = result?.nextCursor;
|
|
107
|
+
hasMore = result?.hasMore === true;
|
|
108
|
+
} catch (err) {
|
|
109
|
+
await handlePollError(err, em, channel, scope, attempt, job.payload);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const commandBus = ctx.resolve("commandBus");
|
|
113
|
+
const containerProxy = { resolve: ctx.resolve.bind(ctx) };
|
|
114
|
+
const commandCtx = {
|
|
115
|
+
container: containerProxy,
|
|
116
|
+
auth: null,
|
|
117
|
+
organizationScope: null,
|
|
118
|
+
selectedOrganizationId: scope.organizationId ?? null,
|
|
119
|
+
organizationIds: scope.organizationId ? [scope.organizationId] : null
|
|
120
|
+
};
|
|
121
|
+
let transientIngestAbort = false;
|
|
122
|
+
for (const message of normalized) {
|
|
123
|
+
try {
|
|
124
|
+
const input = {
|
|
125
|
+
channelId: channel.id,
|
|
126
|
+
providerKey: channel.providerKey,
|
|
127
|
+
channelType: channel.channelType,
|
|
128
|
+
scope: {
|
|
129
|
+
tenantId: scope.tenantId,
|
|
130
|
+
organizationId: scope.organizationId ?? null
|
|
131
|
+
},
|
|
132
|
+
message
|
|
133
|
+
};
|
|
134
|
+
await commandBus.execute(COMMUNICATION_CHANNELS_INGEST_INBOUND_COMMAND_ID, {
|
|
135
|
+
input,
|
|
136
|
+
ctx: commandCtx
|
|
137
|
+
});
|
|
138
|
+
} catch (err) {
|
|
139
|
+
const classification = classifyOutboundError(err);
|
|
140
|
+
if (classification.transient) {
|
|
141
|
+
console.warn(
|
|
142
|
+
`[communication_channels:poll-channel] transient ingest failure for channel ${channel.id}; aborting page so cursor is NOT advanced. Reason: ${classification.message}`
|
|
143
|
+
);
|
|
144
|
+
transientIngestAbort = true;
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
await writeIngestDeadLetter({
|
|
148
|
+
em,
|
|
149
|
+
scope,
|
|
150
|
+
channel,
|
|
151
|
+
message,
|
|
152
|
+
err,
|
|
153
|
+
errorMessage: classification.message
|
|
154
|
+
});
|
|
155
|
+
console.warn(
|
|
156
|
+
`[communication_channels:poll-channel] permanent ingest failure for channel ${channel.id}; recorded in dead-letter and advancing cursor past message ${message.externalMessageId}. Reason: ${classification.message}`
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (transientIngestAbort) {
|
|
161
|
+
channel.lastError = "transient_ingest_failure";
|
|
162
|
+
await em.flush();
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
channel.lastPolledAt = /* @__PURE__ */ new Date();
|
|
166
|
+
if (channel.lastError) channel.lastError = null;
|
|
167
|
+
if (channel.status === "error") {
|
|
168
|
+
channel.status = "connected";
|
|
169
|
+
}
|
|
170
|
+
if (typeof nextCursor === "string" && nextCursor.length > 0) {
|
|
171
|
+
const decoded = decodeChannelStateCursor(nextCursor);
|
|
172
|
+
if (decoded) {
|
|
173
|
+
channel.channelState = preservePushState(channel.channelState, decoded);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
await em.flush();
|
|
177
|
+
if (hasMore) {
|
|
178
|
+
if (drainPage < MAX_DRAIN_PAGES) {
|
|
179
|
+
const queue = getCommunicationChannelsQueue(COMMUNICATION_CHANNELS_QUEUES.poll);
|
|
180
|
+
await queue.enqueue(
|
|
181
|
+
{ channelId: channel.id, scope, attempt: 1, drainPage: drainPage + 1 },
|
|
182
|
+
{ delayMs: 250 }
|
|
183
|
+
);
|
|
184
|
+
} else {
|
|
185
|
+
console.warn(
|
|
186
|
+
`[communication_channels:poll-channel] drain page cap (${MAX_DRAIN_PAGES}) reached for channel ${channel.id}; stopping re-enqueue until the next scheduled tick`
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function decodeChannelStateCursor(cursor) {
|
|
192
|
+
try {
|
|
193
|
+
const decoded = Buffer.from(cursor, "base64").toString("utf8");
|
|
194
|
+
const parsed = JSON.parse(decoded);
|
|
195
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
196
|
+
return parsed;
|
|
197
|
+
}
|
|
198
|
+
return null;
|
|
199
|
+
} catch {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
async function handlePollError(err, em, channel, scope, attempt, payload) {
|
|
204
|
+
const classification = classifyOutboundError(err);
|
|
205
|
+
channel.lastError = classification.message;
|
|
206
|
+
if (isReauthError(classification)) {
|
|
207
|
+
channel.status = "requires_reauth";
|
|
208
|
+
await em.flush();
|
|
209
|
+
await emitCommunicationChannelsEvent(
|
|
210
|
+
"communication_channels.channel.requires_reauth",
|
|
211
|
+
{
|
|
212
|
+
channelId: channel.id,
|
|
213
|
+
providerKey: channel.providerKey,
|
|
214
|
+
channelType: channel.channelType,
|
|
215
|
+
reason: classification.message,
|
|
216
|
+
tenantId: scope.tenantId,
|
|
217
|
+
organizationId: scope.organizationId ?? null
|
|
218
|
+
},
|
|
219
|
+
{ persistent: true }
|
|
220
|
+
);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
if (classification.transient && attempt < POLL_CHANNEL_MAX_ATTEMPTS) {
|
|
224
|
+
await em.flush();
|
|
225
|
+
const next = { ...payload, attempt: attempt + 1 };
|
|
226
|
+
const queue = getCommunicationChannelsQueue(COMMUNICATION_CHANNELS_QUEUES.poll);
|
|
227
|
+
await queue.enqueue(next, {
|
|
228
|
+
delayMs: computeBackoffMs(attempt)
|
|
229
|
+
});
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
channel.status = "error";
|
|
233
|
+
await em.flush();
|
|
234
|
+
}
|
|
235
|
+
export {
|
|
236
|
+
POLL_CHANNEL_MAX_ATTEMPTS,
|
|
237
|
+
handle as default,
|
|
238
|
+
metadata
|
|
239
|
+
};
|
|
240
|
+
//# sourceMappingURL=poll-channel.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/communication_channels/workers/poll-channel.ts"],
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { JobContext, QueuedJob, WorkerMeta } from '@open-mercato/queue'\nimport type { CommandBus } from '@open-mercato/shared/lib/commands'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { CommunicationChannel } from '../data/entities'\nimport {\n COMMUNICATION_CHANNELS_INGEST_INBOUND_COMMAND_ID,\n type IngestInboundMessageInput,\n} from '../commands/ingest-inbound-message'\nimport { COMMUNICATION_CHANNELS_QUEUES, getCommunicationChannelsQueue } from '../lib/queue'\nimport { preservePushState } from '../lib/push-state'\nimport { writeIngestDeadLetter } from '../lib/dead-letter'\nimport { classifyOutboundError, computeBackoffMs, isReauthError } from '../lib/error-classification'\nimport { refreshCredentialsIfNeeded } from '../lib/credential-refresh'\nimport { emitCommunicationChannelsEvent } from '../events'\nimport type { ChannelAdapterRegistry } from '../lib/registry'\nimport type { NormalizedInboundMessage } from '../lib/adapter'\n\n/**\n * Job payload for the `communication-channels-poll` queue.\n *\n * One job per channel per scheduler tick. The poll-tick worker (sibling file)\n * enumerates due channels and enqueues these jobs.\n */\nexport type PollChannelJobPayload = {\n channelId: string\n scope: {\n tenantId: string\n organizationId: string | null\n }\n /** Attempt count, 1-based; used for retry-backoff decisions. */\n attempt?: number\n /** Self-re-enqueue drain counter (bounds the multi-page `hasMore` drain loop). */\n drainPage?: number\n}\n\nexport const POLL_CHANNEL_MAX_ATTEMPTS = 3\n\n/**\n * Hard cap on `hasMore` self-re-enqueue drain pages \u2014 guards against an adapter\n * that returns `hasMore: true` with a non-advancing (pinned) cursor, which would\n * otherwise spin a tight, unthrottled re-enqueue loop. Mirrors the same guard in\n * `gmail-history-sync`.\n */\nconst MAX_DRAIN_PAGES = 100\n\nexport const metadata: WorkerMeta = {\n queue: COMMUNICATION_CHANNELS_QUEUES.poll,\n id: 'communication_channels:poll-channel',\n concurrency: 10,\n}\n\ntype HandlerContext = JobContext & {\n resolve: <T = unknown>(name: string) => T\n}\n\ntype CredentialsServiceLike = {\n resolve: (\n integrationId: string,\n scope: { organizationId: string; tenantId: string; userId?: string | null },\n ) => Promise<Record<string, unknown> | null>\n save?: (\n integrationId: string,\n credentials: Record<string, unknown>,\n scope: { organizationId: string; tenantId: string; userId?: string | null },\n ) => Promise<void>\n}\n\n/**\n * Poll a single channel for inbound messages.\n *\n * Per SPEC-045d \u00A7 6 (with email-spec \u00A7 Hub Deltas \u2192 Delta 6 polling extensions):\n * 1. Load the channel; skip when `is_active === false` or `status !== 'connected'`.\n * 2. Skip when the adapter declares `realtimePush !== false` (it doesn't want polling).\n * 3. Refresh credentials if OAuth and within the expiry window.\n * 4. Call `adapter.fetchHistory({ channelId, credentials, since: lastPolledAt })`.\n * 5. For each normalized message, dispatch `ingest_inbound_message` (idempotent on\n * `(channel_id, external_message_id)`).\n * 6. Update `channel.lastPolledAt = NOW()` on success.\n * 7. On error: classify, set `channel.status` accordingly, set `last_error`, emit\n * `channel.requires_reauth` on 401, retry transient failures up to MAX.\n *\n * The hub doesn't drain a remote mailbox indefinitely \u2014 `fetchHistory` returns a\n * single page (provider decides the size). If the provider has more, the next tick\n * picks it up after the configured `poll_interval_seconds`.\n */\nexport default async function handle(\n job: QueuedJob<PollChannelJobPayload>,\n ctx: HandlerContext,\n): Promise<void> {\n const { channelId, scope, attempt = 1, drainPage = 0 } = job.payload\n const em = (ctx.resolve('em') as EntityManager).fork()\n const adapterRegistry = ctx.resolve<ChannelAdapterRegistry>('channelAdapterRegistry')\n\n const channel = await findOneWithDecryption(\n em,\n CommunicationChannel,\n {\n id: channelId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId ?? null,\n deletedAt: null,\n },\n undefined,\n scope,\n )\n if (!channel) {\n console.warn(\n `[communication_channels:poll-channel] channel ${channelId} not found (skipping)`,\n )\n return\n }\n if (!channel.isActive) return\n // Allow `connected` (normal poll) and `error` (Spec B \u00A7 B5 auto-recovery\n // sweep enqueues these intentionally \u2014 on a successful poll below we\n // flip them back to `connected`). `requires_reauth` and `disconnected`\n // are owned by the credential-refresh and disconnect flows.\n if (channel.status !== 'connected' && channel.status !== 'error') return\n\n const adapter = adapterRegistry?.get(channel.providerKey)\n if (!adapter) {\n console.warn(\n `[communication_channels:poll-channel] no adapter for provider '${channel.providerKey}' (channel ${channelId})`,\n )\n return\n }\n // Adapter opted out of polling \u2014 webhook providers.\n const capabilities = (channel.capabilities as { realtimePush?: boolean } | null) ?? null\n if (capabilities?.realtimePush !== false) {\n // realtimePush is `true` (default for back-compat) \u2014 don't poll push providers.\n return\n }\n if (typeof adapter.fetchHistory !== 'function') {\n // Adapter doesn't implement history fetching \u2014 nothing we can do.\n return\n }\n\n // Credentials.\n let credentialsService: CredentialsServiceLike | null = null\n try {\n credentialsService = ctx.resolve<CredentialsServiceLike>('integrationCredentialsService')\n } catch {\n credentialsService = null\n }\n // Per-user credentials scope: pass `channel.userId` so the credentials\n // service returns this user's row, not whoever connected the provider last.\n // See review R2-C1 / N1 (2026-05-26).\n const credentialsScope = {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId ?? scope.tenantId,\n userId: channel.userId ?? null,\n }\n let credentials: Record<string, unknown> = {}\n if (channel.credentialsRef && credentialsService) {\n try {\n credentials =\n (await credentialsService.resolve(`channel_${channel.providerKey}`, credentialsScope)) ?? {}\n } catch {\n credentials = {}\n }\n }\n const refreshed = await refreshCredentialsIfNeeded(\n {\n adapter,\n channelId: channel.id,\n credentials,\n scope: credentialsScope,\n },\n { credentialsService },\n )\n credentials = refreshed.credentials\n\n // Fetch a single page of history.\n // `channelState` is the provider-specific resumption cursor \u2014 Gmail historyId,\n // IMAP UIDVALIDITY+UIDNEXT, etc. We persist it across\n // ticks on `channel.channelState` so each poll resumes from the prior one\n // instead of running a full mailbox resync. Empty / NULL = \"first poll;\n // bootstrap the cursor from the provider\".\n let normalized: NormalizedInboundMessage[] = []\n let nextCursor: string | undefined\n let hasMore = false\n try {\n const result = await adapter.fetchHistory({\n conversationId: channel.externalIdentifier ?? channel.id,\n credentials,\n cursor: channel.lastPolledAt ? channel.lastPolledAt.toISOString() : undefined,\n channelState: (channel.channelState as Record<string, unknown> | null) ?? undefined,\n scope: {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId ?? scope.tenantId,\n },\n // `contactFilter.sinceDays` is a hint to provider adapters about how far\n // back to look on first-poll bootstrap. For UID-incremental polls (every\n // tick after the first), adapters ignore this and just fetch new mail\n // since the persisted cursor.\n contactFilter: { addresses: [], sinceDays: 7 },\n })\n normalized = Array.isArray(result?.messages) ? result.messages : []\n nextCursor = result?.nextCursor\n hasMore = result?.hasMore === true\n } catch (err) {\n await handlePollError(err, em, channel, scope, attempt, job.payload)\n return\n }\n\n // Dispatch ingest commands for each message.\n const commandBus = ctx.resolve<CommandBus>('commandBus')\n const containerProxy = { resolve: ctx.resolve.bind(ctx) }\n const commandCtx = {\n container: containerProxy as never,\n auth: null,\n organizationScope: null,\n selectedOrganizationId: scope.organizationId ?? null,\n organizationIds: scope.organizationId ? [scope.organizationId] : null,\n }\n // Spec B \u00A7 Per-message commit + dead-letter:\n // - Permanent failure (malformed MIME, schema, contract violation):\n // write to channel_ingest_dead_letter, log, advance cursor anyway so\n // the bad blob never stalls the channel again.\n // - Transient failure (DB drop, network blip): abort the loop without\n // advancing the cursor. The next tick re-fetches the same page;\n // idempotency via the (channel_id, external_message_id) unique\n // constraint means already-ingested messages no-op on the retry.\n let transientIngestAbort = false\n for (const message of normalized) {\n try {\n const input: IngestInboundMessageInput = {\n channelId: channel.id,\n providerKey: channel.providerKey,\n channelType: channel.channelType,\n scope: {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId ?? null,\n },\n message,\n }\n await commandBus.execute(COMMUNICATION_CHANNELS_INGEST_INBOUND_COMMAND_ID, {\n input,\n ctx: commandCtx as never,\n })\n } catch (err) {\n const classification = classifyOutboundError(err)\n if (classification.transient) {\n console.warn(\n `[communication_channels:poll-channel] transient ingest failure for channel ${channel.id}; aborting page so cursor is NOT advanced. Reason: ${classification.message}`,\n )\n transientIngestAbort = true\n break\n }\n // Permanent \u2014 write to dead-letter so an operator can replay later.\n // The shared helper is best-effort (never throws) and idempotent on\n // `(channelId, externalMessageId)`, so a replayed page that fails the\n // same message again does not insert a duplicate row.\n await writeIngestDeadLetter({\n em,\n scope,\n channel,\n message,\n err,\n errorMessage: classification.message,\n })\n console.warn(\n `[communication_channels:poll-channel] permanent ingest failure for channel ${channel.id}; recorded in dead-letter and advancing cursor past message ${message.externalMessageId}. Reason: ${classification.message}`,\n )\n }\n }\n\n // Transient abort: keep prior cursor + lastPolledAt so the next tick\n // re-fetches the same page (idempotent at the DB layer).\n if (transientIngestAbort) {\n channel.lastError = 'transient_ingest_failure'\n await em.flush()\n return\n }\n\n // Update poll cursor + clear any stale error state.\n channel.lastPolledAt = new Date()\n if (channel.lastError) channel.lastError = null\n // Recover the channel from any prior non-fatal error state. The previous\n // poll(s) may have set status='error' after exhausting transient-retry\n // attempts, but a fresh successful poll means the upstream is healthy\n // again \u2014 flip it back to 'connected' so the scheduler keeps it in\n // rotation and the user doesn't have to manually reconnect.\n // We DON'T touch 'requires_reauth' here (that lifecycle state is owned\n // by the credential-refresh / OAuth flow) or 'disconnected' (owned by\n // the cascade-on-user-delete subscriber).\n if (channel.status === 'error') {\n channel.status = 'connected'\n }\n // Providers encode their cursor as base64-encoded JSON in `nextCursor`; we\n // decode it back to an object so the next tick can pass it straight into\n // `fetchHistory` as `channelState`. Decode failures fall back to the prior\n // state (next tick bootstraps from there).\n //\n // Push-delivery state (Spec C) \u2014 watch/subscription identifiers and expiry \u2014 is\n // owned by the push register/renew commands, not the sync cursor. A provider's\n // `fetchHistory` returns only sync-cursor fields, so persisting the decoded\n // cursor as a full replace would silently wipe push state and stop\n // `gmail-renew-watch` from renewing it. Carry\n // the hub-owned push keys forward whenever the new cursor omits them.\n if (typeof nextCursor === 'string' && nextCursor.length > 0) {\n const decoded = decodeChannelStateCursor(nextCursor)\n if (decoded) {\n channel.channelState = preservePushState(channel.channelState, decoded)\n }\n }\n await em.flush()\n\n // Drain contract (review H1, 2026-05-26): adapters that have additional\n // pages beyond this tick's batch (large mailboxes, mid-deltaLink walks,\n // UID overflows) signal `hasMore: true`. The persisted `channelState`\n // already encodes the mid-drain resumption token (Gmail `pendingHistoryPageToken`,\n // IMAP non-terminal `uidNext`). Re-enqueue with\n // a small delay so we keep draining without overrunning rate limits.\n if (hasMore) {\n // Bound the drain: an adapter that returns `hasMore: true` with a\n // non-advancing cursor (e.g. a persistently-failing message that pins the\n // Gmail cursor via `hardFailed`) must not spin an unthrottled loop. Stop\n // at MAX_DRAIN_PAGES; the next scheduled poll tick re-checks the channel.\n if (drainPage < MAX_DRAIN_PAGES) {\n const queue = getCommunicationChannelsQueue(COMMUNICATION_CHANNELS_QUEUES.poll)\n await queue.enqueue(\n { channelId: channel.id, scope, attempt: 1, drainPage: drainPage + 1 } as unknown as Record<string, unknown>,\n { delayMs: 250 },\n )\n } else {\n console.warn(\n `[communication_channels:poll-channel] drain page cap (${MAX_DRAIN_PAGES}) reached for channel ${channel.id}; stopping re-enqueue until the next scheduled tick`,\n )\n }\n }\n}\n\nfunction decodeChannelStateCursor(cursor: string): Record<string, unknown> | null {\n try {\n const decoded = Buffer.from(cursor, 'base64').toString('utf8')\n const parsed = JSON.parse(decoded) as unknown\n if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {\n return parsed as Record<string, unknown>\n }\n return null\n } catch {\n return null\n }\n}\n\nasync function handlePollError(\n err: unknown,\n em: EntityManager,\n channel: CommunicationChannel,\n scope: PollChannelJobPayload['scope'],\n attempt: number,\n payload: PollChannelJobPayload,\n): Promise<void> {\n const classification = classifyOutboundError(err)\n channel.lastError = classification.message\n\n if (isReauthError(classification)) {\n channel.status = 'requires_reauth'\n await em.flush()\n await emitCommunicationChannelsEvent(\n 'communication_channels.channel.requires_reauth',\n {\n channelId: channel.id,\n providerKey: channel.providerKey,\n channelType: channel.channelType,\n reason: classification.message,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId ?? null,\n },\n { persistent: true },\n )\n return\n }\n\n if (classification.transient && attempt < POLL_CHANNEL_MAX_ATTEMPTS) {\n // Transient error \u2014 re-enqueue with backoff. Channel status stays\n // `connected` so the scheduler keeps it in rotation.\n await em.flush()\n const next: PollChannelJobPayload = { ...payload, attempt: attempt + 1 }\n const queue = getCommunicationChannelsQueue(COMMUNICATION_CHANNELS_QUEUES.poll)\n await queue.enqueue(next as unknown as Record<string, unknown>, {\n delayMs: computeBackoffMs(attempt),\n })\n return\n }\n\n // Permanent or attempts exhausted \u2014 mark channel as error and stop the loop.\n channel.status = 'error'\n await em.flush()\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
2
|
+
import { CommunicationChannel } from "../data/entities.js";
|
|
3
|
+
import { COMMUNICATION_CHANNELS_QUEUES, getCommunicationChannelsQueue } from "../lib/queue.js";
|
|
4
|
+
const POLL_ENUMERATION_CAP = Math.max(
|
|
5
|
+
1,
|
|
6
|
+
Number.parseInt(process.env.COMMUNICATION_CHANNELS_POLL_ENUMERATION_CAP ?? "500", 10) || 500
|
|
7
|
+
);
|
|
8
|
+
const metadata = {
|
|
9
|
+
queue: COMMUNICATION_CHANNELS_QUEUES.pollTick,
|
|
10
|
+
id: "communication_channels:poll-tick",
|
|
11
|
+
concurrency: 1
|
|
12
|
+
// single-flight per tenant — one tick at a time
|
|
13
|
+
};
|
|
14
|
+
async function handle(job, ctx) {
|
|
15
|
+
const raw = job?.payload ?? {};
|
|
16
|
+
const tenantId = raw.scope?.tenantId ?? raw.tenantId ?? null;
|
|
17
|
+
const organizationId = raw.scope?.organizationId ?? raw.organizationId ?? null;
|
|
18
|
+
if (!tenantId) {
|
|
19
|
+
console.warn(
|
|
20
|
+
"[communication_channels:poll-tick] skipping tick \u2014 payload has no tenantId",
|
|
21
|
+
{ payload: raw }
|
|
22
|
+
);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const scope = { tenantId, organizationId };
|
|
26
|
+
const em = ctx.resolve("em").fork();
|
|
27
|
+
const now = /* @__PURE__ */ new Date();
|
|
28
|
+
const connectedCandidates = await findWithDecryption(
|
|
29
|
+
em,
|
|
30
|
+
CommunicationChannel,
|
|
31
|
+
{
|
|
32
|
+
tenantId: scope.tenantId,
|
|
33
|
+
organizationId: scope.organizationId ?? null,
|
|
34
|
+
isActive: true,
|
|
35
|
+
deletedAt: null,
|
|
36
|
+
status: "connected",
|
|
37
|
+
pollIntervalSeconds: { $ne: null }
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
limit: POLL_ENUMERATION_CAP,
|
|
41
|
+
orderBy: { lastPolledAt: "asc" }
|
|
42
|
+
},
|
|
43
|
+
scope
|
|
44
|
+
);
|
|
45
|
+
const recoverMinutesRaw = Number.parseInt(
|
|
46
|
+
process.env.OM_CHANNEL_AUTO_RECOVER_MINUTES ?? "",
|
|
47
|
+
10
|
|
48
|
+
);
|
|
49
|
+
const recoverMinutes = Number.isFinite(recoverMinutesRaw) && recoverMinutesRaw >= 0 ? recoverMinutesRaw : 30;
|
|
50
|
+
const recoverCutoff = new Date(now.getTime() - recoverMinutes * 60 * 1e3);
|
|
51
|
+
const errorCandidates = await findWithDecryption(
|
|
52
|
+
em,
|
|
53
|
+
CommunicationChannel,
|
|
54
|
+
{
|
|
55
|
+
tenantId: scope.tenantId,
|
|
56
|
+
organizationId: scope.organizationId ?? null,
|
|
57
|
+
isActive: true,
|
|
58
|
+
deletedAt: null,
|
|
59
|
+
status: "error",
|
|
60
|
+
// Polling channels only. Push-only channels (Gmail) have
|
|
61
|
+
// `pollIntervalSeconds = null` and are intentionally excluded: poll-channel
|
|
62
|
+
// returns early for push providers, so their recovery is owner-driven
|
|
63
|
+
// (re-register push / reconnect), not this poll-recovery sweep.
|
|
64
|
+
pollIntervalSeconds: { $ne: null },
|
|
65
|
+
// `lastPolledAt` advances only on a SUCCESSFUL poll (poll-channel does
|
|
66
|
+
// not touch it in `handlePollError`). To keep recovery to one retry per
|
|
67
|
+
// window — rather than re-enqueuing a persistently-failing channel every
|
|
68
|
+
// tick — the recovery-enqueue loop below bumps `lastPolledAt` to `now`
|
|
69
|
+
// when it schedules a recovery job, so the channel only re-enters this
|
|
70
|
+
// pool after another `recoverMinutes` have elapsed.
|
|
71
|
+
//
|
|
72
|
+
// A channel that fails its FIRST poll (before any success) still has
|
|
73
|
+
// `lastPolledAt = null`; a bare `$lt` would exclude it forever (SQL
|
|
74
|
+
// `NULL < ts` is NULL, not true), stranding it in `error`. Include the
|
|
75
|
+
// null case so never-polled error channels get their first recovery
|
|
76
|
+
// attempt on the next tick (the enqueue bump below then throttles it).
|
|
77
|
+
$or: [{ lastPolledAt: null }, { lastPolledAt: { $lt: recoverCutoff } }]
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
limit: POLL_ENUMERATION_CAP,
|
|
81
|
+
orderBy: { lastPolledAt: "asc" }
|
|
82
|
+
},
|
|
83
|
+
scope
|
|
84
|
+
);
|
|
85
|
+
const queue = getCommunicationChannelsQueue(COMMUNICATION_CHANNELS_QUEUES.poll);
|
|
86
|
+
let enqueued = 0;
|
|
87
|
+
let recovered = 0;
|
|
88
|
+
for (const channel of connectedCandidates) {
|
|
89
|
+
const intervalSeconds = channel.pollIntervalSeconds;
|
|
90
|
+
if (!intervalSeconds || intervalSeconds <= 0) continue;
|
|
91
|
+
if (!isDue(channel.lastPolledAt ?? null, intervalSeconds, now)) continue;
|
|
92
|
+
const payload = {
|
|
93
|
+
channelId: channel.id,
|
|
94
|
+
scope: {
|
|
95
|
+
tenantId: channel.tenantId,
|
|
96
|
+
organizationId: channel.organizationId ?? scope.organizationId ?? null
|
|
97
|
+
},
|
|
98
|
+
attempt: 1
|
|
99
|
+
};
|
|
100
|
+
await queue.enqueue(payload);
|
|
101
|
+
enqueued += 1;
|
|
102
|
+
}
|
|
103
|
+
for (const channel of errorCandidates) {
|
|
104
|
+
const payload = {
|
|
105
|
+
channelId: channel.id,
|
|
106
|
+
scope: {
|
|
107
|
+
tenantId: channel.tenantId,
|
|
108
|
+
organizationId: channel.organizationId ?? scope.organizationId ?? null
|
|
109
|
+
},
|
|
110
|
+
attempt: 1
|
|
111
|
+
};
|
|
112
|
+
await queue.enqueue(payload);
|
|
113
|
+
channel.lastPolledAt = now;
|
|
114
|
+
recovered += 1;
|
|
115
|
+
}
|
|
116
|
+
if (recovered > 0) await em.flush();
|
|
117
|
+
if (enqueued > 0 || recovered > 0) {
|
|
118
|
+
console.log(
|
|
119
|
+
`[communication_channels:poll-tick] enqueued ${enqueued} normal + ${recovered} auto-recover poll job(s) for tenant ${scope.tenantId}`
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function isDue(lastPolledAt, intervalSeconds, now) {
|
|
124
|
+
if (!lastPolledAt) return true;
|
|
125
|
+
const dueAt = new Date(lastPolledAt.getTime() + intervalSeconds * 1e3);
|
|
126
|
+
return now >= dueAt;
|
|
127
|
+
}
|
|
128
|
+
export {
|
|
129
|
+
handle as default,
|
|
130
|
+
metadata
|
|
131
|
+
};
|
|
132
|
+
//# sourceMappingURL=poll-tick.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/communication_channels/workers/poll-tick.ts"],
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { JobContext, QueuedJob, WorkerMeta } from '@open-mercato/queue'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { CommunicationChannel } from '../data/entities'\nimport { COMMUNICATION_CHANNELS_QUEUES, getCommunicationChannelsQueue } from '../lib/queue'\nimport type { PollChannelJobPayload } from './poll-channel'\n\n/**\n * Scheduler tick payload. Fired by the `@open-mercato/scheduler` cron entry\n * registered in the hub's `setup.ts` (`communication_channels:poll-tick` schedule).\n */\nexport type PollTickPayload = {\n scope: {\n tenantId: string\n organizationId: string | null\n }\n}\n\nconst POLL_ENUMERATION_CAP = Math.max(\n 1,\n Number.parseInt(process.env.COMMUNICATION_CHANNELS_POLL_ENUMERATION_CAP ?? '500', 10) || 500,\n)\n\nexport const metadata: WorkerMeta = {\n queue: COMMUNICATION_CHANNELS_QUEUES.pollTick,\n id: 'communication_channels:poll-tick',\n concurrency: 1, // single-flight per tenant \u2014 one tick at a time\n}\n\ntype HandlerContext = JobContext & {\n resolve: <T = unknown>(name: string) => T\n}\n\n/**\n * Enumerate channels due for polling and enqueue per-channel jobs.\n *\n * Per email integration spec \u00A7 Hub Deltas \u2192 Delta 6:\n * SELECT id FROM communication_channels\n * WHERE is_active = true\n * AND deleted_at IS NULL\n * AND status = 'connected'\n * AND poll_interval_seconds IS NOT NULL\n * AND (last_polled_at IS NULL\n * OR last_polled_at + poll_interval_seconds * '1 sec' <= NOW())\n * ORDER BY last_polled_at NULLS FIRST\n * LIMIT 500\n *\n * Implementation note: MikroORM's QueryBuilder is the canonical entry point;\n * the raw SQL above is the conceptual query. We use the entity-level filter to\n * keep things portable and let MikroORM compile it.\n */\nexport default async function handle(\n job: QueuedJob<PollTickPayload>,\n ctx: HandlerContext,\n): Promise<void> {\n // The scheduler module (`@open-mercato/scheduler`) spreads the configured\n // `targetPayload` and then adds `tenantId` / `organizationId` at the TOP\n // level of the enqueued payload (see\n // packages/scheduler/.../execute-schedule.worker.ts). Our setup.ts originally\n // stored `{ scope: { tenantId, organizationId } }` under targetPayload, so\n // at runtime the payload looks like:\n // { scope: { tenantId, organizationId }, tenantId, organizationId, _idempotencyKey }\n // Accept either path so the handler is robust to both how operators originally\n // configured the schedule (nested `scope`) and how the scheduler flattens it.\n const raw = (job?.payload ?? {}) as Partial<PollTickPayload> & {\n tenantId?: string | null\n organizationId?: string | null\n }\n const tenantId = raw.scope?.tenantId ?? raw.tenantId ?? null\n const organizationId =\n raw.scope?.organizationId ?? raw.organizationId ?? null\n if (!tenantId) {\n console.warn(\n '[communication_channels:poll-tick] skipping tick \u2014 payload has no tenantId',\n { payload: raw },\n )\n return\n }\n const scope = { tenantId, organizationId }\n const em = (ctx.resolve('em') as EntityManager).fork()\n\n const now = new Date()\n // Find candidate channels \u2014 we enumerate two pools:\n // (1) status='connected' channels due for their normal poll cycle.\n // (2) Spec B \u00A7 Auto-recovery sweep: status='error' channels whose\n // `lastPolledAt` is older than OM_CHANNEL_AUTO_RECOVER_MINUTES\n // (default 30 min). At most one retry per recovery window per\n // channel \u2014 when we enqueue a recovery job below we bump that\n // channel's `lastPolledAt` to `now` so it falls back under the\n // cutoff and is NOT re-selected on the immediately-following ticks\n // (poll-channel only advances `lastPolledAt` on a SUCCESSFUL poll,\n // so without this a persistently-failing channel would re-enqueue\n // every tick). On success `poll-channel` flips the status back to\n // 'connected' so the channel rejoins the normal pool.\n //\n // Due-ness for (1) is computed in JS to avoid cross-DB interval\n // arithmetic; the (2) cutoff is a single timestamp compare.\n const connectedCandidates = await findWithDecryption(\n em,\n CommunicationChannel,\n {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId ?? null,\n isActive: true,\n deletedAt: null,\n status: 'connected',\n pollIntervalSeconds: { $ne: null },\n },\n {\n limit: POLL_ENUMERATION_CAP,\n orderBy: { lastPolledAt: 'asc' },\n },\n scope,\n )\n\n const recoverMinutesRaw = Number.parseInt(\n process.env.OM_CHANNEL_AUTO_RECOVER_MINUTES ?? '',\n 10,\n )\n // `0` is a valid override meaning \"recover on the very next tick\" (used by\n // TC-CHANNEL-EMAIL-027 and operators who want aggressive recovery). Only a\n // negative or non-numeric value falls back to the 30-minute default.\n const recoverMinutes =\n Number.isFinite(recoverMinutesRaw) && recoverMinutesRaw >= 0 ? recoverMinutesRaw : 30\n const recoverCutoff = new Date(now.getTime() - recoverMinutes * 60 * 1000)\n const errorCandidates = await findWithDecryption(\n em,\n CommunicationChannel,\n {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId ?? null,\n isActive: true,\n deletedAt: null,\n status: 'error',\n // Polling channels only. Push-only channels (Gmail) have\n // `pollIntervalSeconds = null` and are intentionally excluded: poll-channel\n // returns early for push providers, so their recovery is owner-driven\n // (re-register push / reconnect), not this poll-recovery sweep.\n pollIntervalSeconds: { $ne: null },\n // `lastPolledAt` advances only on a SUCCESSFUL poll (poll-channel does\n // not touch it in `handlePollError`). To keep recovery to one retry per\n // window \u2014 rather than re-enqueuing a persistently-failing channel every\n // tick \u2014 the recovery-enqueue loop below bumps `lastPolledAt` to `now`\n // when it schedules a recovery job, so the channel only re-enters this\n // pool after another `recoverMinutes` have elapsed.\n //\n // A channel that fails its FIRST poll (before any success) still has\n // `lastPolledAt = null`; a bare `$lt` would exclude it forever (SQL\n // `NULL < ts` is NULL, not true), stranding it in `error`. Include the\n // null case so never-polled error channels get their first recovery\n // attempt on the next tick (the enqueue bump below then throttles it).\n $or: [{ lastPolledAt: null }, { lastPolledAt: { $lt: recoverCutoff } }],\n },\n {\n limit: POLL_ENUMERATION_CAP,\n orderBy: { lastPolledAt: 'asc' },\n },\n scope,\n )\n\n const queue = getCommunicationChannelsQueue(COMMUNICATION_CHANNELS_QUEUES.poll)\n let enqueued = 0\n let recovered = 0\n for (const channel of connectedCandidates as CommunicationChannel[]) {\n const intervalSeconds = channel.pollIntervalSeconds\n if (!intervalSeconds || intervalSeconds <= 0) continue\n if (!isDue(channel.lastPolledAt ?? null, intervalSeconds, now)) continue\n const payload: PollChannelJobPayload = {\n channelId: channel.id,\n scope: {\n tenantId: channel.tenantId,\n organizationId: channel.organizationId ?? scope.organizationId ?? null,\n },\n attempt: 1,\n }\n await queue.enqueue(payload as unknown as Record<string, unknown>)\n enqueued += 1\n }\n // Auto-recovery: at most one retry per recovery window per error-state\n // channel. We bump `lastPolledAt` to `now` as we enqueue so the same channel\n // drops back under `recoverCutoff` and is NOT re-selected on the next ticks\n // (poll-channel leaves `lastPolledAt` untouched on failure, so without this a\n // persistently-failing channel would be re-enqueued every tick).\n for (const channel of errorCandidates as CommunicationChannel[]) {\n const payload: PollChannelJobPayload = {\n channelId: channel.id,\n scope: {\n tenantId: channel.tenantId,\n organizationId: channel.organizationId ?? scope.organizationId ?? null,\n },\n attempt: 1,\n }\n await queue.enqueue(payload as unknown as Record<string, unknown>)\n channel.lastPolledAt = now\n recovered += 1\n }\n if (recovered > 0) await em.flush()\n\n if (enqueued > 0 || recovered > 0) {\n console.log(\n `[communication_channels:poll-tick] enqueued ${enqueued} normal + ${recovered} auto-recover poll job(s) for tenant ${scope.tenantId}`,\n )\n }\n}\n\nfunction isDue(lastPolledAt: Date | null, intervalSeconds: number, now: Date): boolean {\n if (!lastPolledAt) return true\n const dueAt = new Date(lastPolledAt.getTime() + intervalSeconds * 1000)\n return now >= dueAt\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
2
|
+
import {
|
|
3
|
+
COMMUNICATION_CHANNELS_PROCESS_INBOUND_REACTION_COMMAND_ID
|
|
4
|
+
} from "../commands/process-inbound-reaction.js";
|
|
5
|
+
import { COMMUNICATION_CHANNELS_QUEUES, getCommunicationChannelsQueue } from "../lib/queue.js";
|
|
6
|
+
import { classifyOutboundError, computeBackoffMs } from "../lib/error-classification.js";
|
|
7
|
+
import { CommunicationChannel } from "../data/entities.js";
|
|
8
|
+
import {
|
|
9
|
+
REACTION_PROCESSOR_MAX_ATTEMPTS
|
|
10
|
+
} from "../lib/reaction-processor-types.js";
|
|
11
|
+
import { refreshCredentialsIfNeeded } from "../lib/credential-refresh.js";
|
|
12
|
+
const metadata = {
|
|
13
|
+
queue: COMMUNICATION_CHANNELS_QUEUES.reactions,
|
|
14
|
+
id: "communication_channels:reaction-processor",
|
|
15
|
+
concurrency: 10
|
|
16
|
+
};
|
|
17
|
+
async function handle(job, ctx) {
|
|
18
|
+
switch (job.payload.kind) {
|
|
19
|
+
case "inbound":
|
|
20
|
+
await handleInbound(job.payload, ctx);
|
|
21
|
+
return;
|
|
22
|
+
case "outbound_send":
|
|
23
|
+
await handleOutboundSend(job.payload, ctx);
|
|
24
|
+
return;
|
|
25
|
+
case "outbound_remove":
|
|
26
|
+
await handleOutboundRemove(job.payload, ctx);
|
|
27
|
+
return;
|
|
28
|
+
default: {
|
|
29
|
+
const exhaustive = job.payload;
|
|
30
|
+
throw new Error(`Unknown reaction job kind: ${JSON.stringify(exhaustive)}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
async function handleInbound(payload, ctx) {
|
|
35
|
+
const commandBus = ctx.resolve("commandBus");
|
|
36
|
+
const containerProxy = { resolve: ctx.resolve.bind(ctx) };
|
|
37
|
+
const commandCtx = {
|
|
38
|
+
container: containerProxy,
|
|
39
|
+
auth: null,
|
|
40
|
+
organizationScope: null,
|
|
41
|
+
selectedOrganizationId: payload.scope.organizationId ?? null,
|
|
42
|
+
organizationIds: payload.scope.organizationId ? [payload.scope.organizationId] : null
|
|
43
|
+
};
|
|
44
|
+
const input = {
|
|
45
|
+
channelId: payload.channelId,
|
|
46
|
+
providerKey: payload.providerKey,
|
|
47
|
+
channelType: payload.channelType,
|
|
48
|
+
scope: payload.scope,
|
|
49
|
+
event: payload.event
|
|
50
|
+
};
|
|
51
|
+
await commandBus.execute(
|
|
52
|
+
COMMUNICATION_CHANNELS_PROCESS_INBOUND_REACTION_COMMAND_ID,
|
|
53
|
+
{ input, ctx: commandCtx }
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
async function handleOutboundSend(payload, ctx) {
|
|
57
|
+
const result = await callAdapterOutbound(payload, ctx, "send");
|
|
58
|
+
await maybeRetry(result, payload, ctx);
|
|
59
|
+
}
|
|
60
|
+
async function handleOutboundRemove(payload, ctx) {
|
|
61
|
+
const result = await callAdapterOutbound(payload, ctx, "remove");
|
|
62
|
+
await maybeRetry(result, payload, ctx);
|
|
63
|
+
}
|
|
64
|
+
async function callAdapterOutbound(payload, ctx, action) {
|
|
65
|
+
const adapterRegistry = ctx.resolve("channelAdapterRegistry");
|
|
66
|
+
const adapter = adapterRegistry?.get(payload.providerKey);
|
|
67
|
+
if (!adapter) {
|
|
68
|
+
return { status: "no_adapter", message: `No adapter for provider '${payload.providerKey}'` };
|
|
69
|
+
}
|
|
70
|
+
const em = ctx.resolve("em").fork();
|
|
71
|
+
const channel = await findOneWithDecryption(
|
|
72
|
+
em,
|
|
73
|
+
CommunicationChannel,
|
|
74
|
+
{
|
|
75
|
+
id: payload.channelId,
|
|
76
|
+
tenantId: payload.scope.tenantId,
|
|
77
|
+
organizationId: payload.scope.organizationId ?? null,
|
|
78
|
+
deletedAt: null
|
|
79
|
+
},
|
|
80
|
+
void 0,
|
|
81
|
+
payload.scope
|
|
82
|
+
);
|
|
83
|
+
if (!channel) {
|
|
84
|
+
return { status: "no_adapter", message: `Channel ${payload.channelId} not found` };
|
|
85
|
+
}
|
|
86
|
+
if (!channel.isActive) {
|
|
87
|
+
return { status: "channel_inactive", message: `Channel ${payload.channelId} is inactive` };
|
|
88
|
+
}
|
|
89
|
+
let credentials = {};
|
|
90
|
+
let credentialsService = null;
|
|
91
|
+
try {
|
|
92
|
+
credentialsService = ctx.resolve("integrationCredentialsService");
|
|
93
|
+
} catch {
|
|
94
|
+
credentialsService = null;
|
|
95
|
+
}
|
|
96
|
+
const credentialsScope = {
|
|
97
|
+
tenantId: payload.scope.tenantId,
|
|
98
|
+
organizationId: payload.scope.organizationId ?? payload.scope.tenantId,
|
|
99
|
+
userId: channel.userId ?? null
|
|
100
|
+
};
|
|
101
|
+
if (channel.credentialsRef && credentialsService) {
|
|
102
|
+
try {
|
|
103
|
+
credentials = await credentialsService.resolve(`channel_${channel.providerKey}`, credentialsScope) ?? {};
|
|
104
|
+
} catch {
|
|
105
|
+
credentials = {};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const refreshed = await refreshCredentialsIfNeeded(
|
|
109
|
+
{
|
|
110
|
+
adapter,
|
|
111
|
+
channelId: channel.id,
|
|
112
|
+
credentials,
|
|
113
|
+
scope: credentialsScope
|
|
114
|
+
},
|
|
115
|
+
{ credentialsService }
|
|
116
|
+
);
|
|
117
|
+
credentials = refreshed.credentials;
|
|
118
|
+
try {
|
|
119
|
+
if (action === "send") {
|
|
120
|
+
if (typeof adapter.sendReaction !== "function") {
|
|
121
|
+
return { status: "no_adapter", message: `Adapter '${adapter.providerKey}' has no sendReaction` };
|
|
122
|
+
}
|
|
123
|
+
const sendPayload = payload;
|
|
124
|
+
await adapter.sendReaction({
|
|
125
|
+
externalMessageId: sendPayload.messageId,
|
|
126
|
+
// platform message id; the adapter maps to provider id via its own channel-link lookup if needed
|
|
127
|
+
conversationId: sendPayload.conversationId ?? "",
|
|
128
|
+
emoji: sendPayload.emoji,
|
|
129
|
+
credentials,
|
|
130
|
+
scope: {
|
|
131
|
+
tenantId: payload.scope.tenantId,
|
|
132
|
+
organizationId: payload.scope.organizationId ?? payload.scope.tenantId
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
} else {
|
|
136
|
+
if (typeof adapter.removeReaction !== "function") {
|
|
137
|
+
return { status: "no_adapter", message: `Adapter '${adapter.providerKey}' has no removeReaction` };
|
|
138
|
+
}
|
|
139
|
+
const removePayload = payload;
|
|
140
|
+
await adapter.removeReaction({
|
|
141
|
+
externalMessageId: removePayload.externalReactionId ?? removePayload.messageId,
|
|
142
|
+
conversationId: removePayload.conversationId ?? "",
|
|
143
|
+
emoji: removePayload.emoji,
|
|
144
|
+
credentials,
|
|
145
|
+
scope: {
|
|
146
|
+
tenantId: payload.scope.tenantId,
|
|
147
|
+
organizationId: payload.scope.organizationId ?? payload.scope.tenantId
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
return { status: "ok" };
|
|
152
|
+
} catch (err) {
|
|
153
|
+
const classification = classifyOutboundError(err);
|
|
154
|
+
return {
|
|
155
|
+
status: "failed",
|
|
156
|
+
transient: classification.transient,
|
|
157
|
+
message: classification.message
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
async function maybeRetry(result, payload, ctx) {
|
|
162
|
+
if (result.status === "ok") return;
|
|
163
|
+
if (result.status === "no_adapter" || result.status === "channel_inactive") {
|
|
164
|
+
console.error(
|
|
165
|
+
`[communication_channels:reaction-processor] ${result.status}: ${result.message}`
|
|
166
|
+
);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (!result.transient) {
|
|
170
|
+
console.error(
|
|
171
|
+
`[communication_channels:reaction-processor] permanent failure: ${result.message}`
|
|
172
|
+
);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const attempt = payload.attempt ?? 1;
|
|
176
|
+
if (attempt >= REACTION_PROCESSOR_MAX_ATTEMPTS) {
|
|
177
|
+
console.error(
|
|
178
|
+
`[communication_channels:reaction-processor] giving up after attempt ${attempt}: ${result.message}`
|
|
179
|
+
);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const next = { ...payload, attempt: attempt + 1 };
|
|
183
|
+
const queue = getCommunicationChannelsQueue(COMMUNICATION_CHANNELS_QUEUES.reactions);
|
|
184
|
+
await queue.enqueue(next, {
|
|
185
|
+
delayMs: computeBackoffMs(attempt)
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
export {
|
|
189
|
+
handle as default,
|
|
190
|
+
metadata
|
|
191
|
+
};
|
|
192
|
+
//# sourceMappingURL=reaction-processor.js.map
|