import type { TriggerDefinition } from "@automate.ax/catalog/authoring" import { planIntegrationAccountUse } from "./account-planning" import { markNonInitiatingContextBoundary } from "./context-boundary-planning" import { serializedIntegrationAccountDefinition } from "./integrations" import { consumeHookLocation, formatHookLocation, getAutomationPlanningContext, getAutomationRuntimeState, } from "./runtime" import type { Signal } from "./signal-protocol" import { createSubscriptionSignal } from "./signal-resolution" /** * Registers a low-level event subscription without public trigger policy. * * @param trigger - Shared trigger definition to subscribe to. * @param config - Configuration for the event type. * @param staticProperties - Values exposed directly instead of as projections. * @param options - Internal planning behavior for the subscription. * @param options.inferActionBoundary - Whether this trigger can anchor an * otherwise dependency-free action. * @throws When an account-backed trigger has invalid or dynamic configuration. */ export function createSubscription< TEvent, const TStatic extends object = object, >( trigger: TriggerDefinition, config: unknown, staticProperties?: TStatic, options: { inferActionBoundary?: boolean } = {}, ): Signal & Readonly { const { scopePath, slot: hookSlot } = consumeHookLocation() const planningContext = getAutomationPlanningContext() let plannedConfig = config if (trigger.account) { if ( typeof config !== "object" || config === null || Array.isArray(config) ) { throw new Error(`Trigger ${trigger.type} requires account configuration.`) } const account = "account" in config ? config.account : undefined const configWithoutAccount = Object.fromEntries( Object.entries(config).filter(([key]) => key !== "account"), ) if (account === undefined && trigger.account.planDefaultBinding === false) { plannedConfig = configWithoutAccount } else { const selection = planIntegrationAccountUse(account, [trigger.account]) if (selection.dynamic) { throw new Error("Trigger account selection must be static.") } plannedConfig = { ...configWithoutAccount, account: { [serializedIntegrationAccountDefinition]: { binding: selection.binding, serviceId: trigger.account.serviceId, }, }, } } } if (planningContext) { planningContext.subscriptions.push({ config: plannedConfig, eventType: trigger.type, hookSlot, ...(scopePath.length > 0 && { scopePath: [...scopePath] }), }) if (options.inferActionBoundary === false) { markNonInitiatingContextBoundary(planningContext, { scopePath, slot: hookSlot, }) } } const signal = createSubscriptionSignal( { scopePath, slot: hookSlot }, staticProperties, ) const state = getAutomationRuntimeState() if (options.inferActionBoundary === false) { state?.nonInitiatingSubscriptionOrigins?.add( `event:${formatHookLocation({ scopePath, slot: hookSlot })}`, ) } else { state?.initiatingSubscriptionSignals?.push(signal) } return signal }