Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 48 additions & 1 deletion packages/core/src/destination-kit/action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ import type {
DynamicFieldContext,
ActionDestinationSuccessResponseType,
ActionDestinationErrorResponseType,
ResultMultiStatusNode
ResultMultiStatusNode,
AsyncPollResponseType
} from './types'
import { syncModeTypes } from './types'
import { HTTPError, NormalizedOptions } from '../request-client'
Expand Down Expand Up @@ -139,6 +140,9 @@ export interface ActionDefinition<
/** The operation to perform when this action is triggered for a batch of events */
performBatch?: RequestFn<Settings, Payload[], PerformBatchResponse, AudienceSettings>

/** The operation to poll the status of async operation(s) - handles both single and batch operations */
poll?: RequestFn<Settings, Payload, AsyncPollResponseType, AudienceSettings>

/** Hooks are triggered at some point in a mappings lifecycle. They may perform a request with the
* destination using the provided inputs and return a response. The response may then optionally be stored
* in the mapping for later use in the action.
Expand Down Expand Up @@ -260,6 +264,7 @@ export class Action<Settings, Payload extends JSONLikeObject, AudienceSettings =
readonly hookSchemas?: Record<string, JSONSchema4>
readonly hasBatchSupport: boolean
readonly hasHookSupport: boolean
readonly hasPollSupport: boolean
// Payloads may be any type so we use `any` explicitly here.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private extendRequest: RequestExtension<Settings, any> | undefined
Expand All @@ -277,6 +282,7 @@ export class Action<Settings, Payload extends JSONLikeObject, AudienceSettings =
this.extendRequest = extendRequest
this.hasBatchSupport = typeof definition.performBatch === 'function'
this.hasHookSupport = definition.hooks !== undefined
this.hasPollSupport = typeof definition.poll === 'function'
// Generate json schema based on the field definitions
if (Object.keys(definition.fields ?? {}).length) {
this.schema = fieldsToJsonSchema(definition.fields)
Expand Down Expand Up @@ -586,6 +592,42 @@ export class Action<Settings, Payload extends JSONLikeObject, AudienceSettings =
return multiStatusResponse
}

async executePoll(
bundle: ExecuteBundle<Settings, InputData | undefined, AudienceSettings>
): Promise<AsyncPollResponseType> {
if (!this.hasPollSupport) {
throw new IntegrationError('This action does not support polling operations.', 'NotImplemented', 501)
}

// Note: Polling operations typically use data from stateContext rather than transforming the event payload
// Since we're checking the status of async operations, not processing new event data
const payload = {} as Payload

// Construct the data bundle to send to the poll action
const dataBundle = {
rawData: bundle.data,
rawMapping: bundle.mapping,
settings: bundle.settings,
payload,
auth: bundle.auth,
features: bundle.features,
statsContext: bundle.statsContext,
logger: bundle.logger,
engageDestinationCache: bundle.engageDestinationCache,
transactionContext: bundle.transactionContext,
stateContext: bundle.stateContext,
audienceSettings: bundle.audienceSettings,
subscriptionMetadata: bundle.subscriptionMetadata,
signal: bundle?.signal
}

// Construct the request client and perform the poll operation
const requestClient = this.createRequestClient(dataBundle)
const pollResponse = await this.definition.poll!(requestClient, dataBundle)

return pollResponse
}

/*
* Extract the dynamic field context and handler path from a field string. Examples:
* - "structured.first_name" => { dynamicHandlerPath: "structured.first_name" }
Expand Down Expand Up @@ -716,6 +758,11 @@ export class Action<Settings, Payload extends JSONLikeObject, AudienceSettings =
}

private parseResponse(response: unknown): unknown {
// Handle async action responses by returning them as-is
if (response && typeof response === 'object' && (response as any).isAsync === true) {
return response
}

/**
* Try to use the parsed response `.data` or `.content` string
* @see {@link ../middleware/after-response/prepare-response.ts}
Expand Down
52 changes: 50 additions & 2 deletions packages/core/src/destination-kit/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ import type {
Deletion,
DeletionPayload,
DynamicFieldResponse,
ResultMultiStatusNode
ResultMultiStatusNode,
AsyncActionResponseType,
AsyncPollResponseType
} from './types'
import type { AllRequestOptions } from '../request-client'
import { ErrorCodes, IntegrationError, InvalidAuthenticationError, MultiStatusErrorReporter } from '../errors'
Expand All @@ -44,7 +46,9 @@ export type {
ActionHookType,
ExecuteInput,
RequestFn,
Result
Result,
AsyncActionResponseType,
AsyncPollResponseType
}
export { hookTypeStrings }
export type { MinimalInputField }
Expand Down Expand Up @@ -716,6 +720,50 @@ export class Destination<Settings = JSONObject, AudienceSettings = JSONObject> {
return action.executeDynamicField(fieldKey, data, dynamicFn)
}

public async executePoll(
actionSlug: string,
{
event,
mapping,
subscriptionMetadata,
settings,
auth,
features,
statsContext,
logger,
engageDestinationCache,
transactionContext,
stateContext,
signal
}: EventInput<Settings>
): Promise<AsyncPollResponseType> {
const action = this.actions[actionSlug]
if (!action) {
throw new IntegrationError(`Action ${actionSlug} not found`, 'NotImplemented', 404)
}

let audienceSettings = {} as AudienceSettings
if (event.context?.personas) {
audienceSettings = event.context?.personas?.audience_settings as AudienceSettings
}

return action.executePoll({
mapping,
data: event as unknown as InputData,
settings,
audienceSettings,
auth,
features,
statsContext,
logger,
engageDestinationCache,
transactionContext,
stateContext,
subscriptionMetadata,
signal
})
}

private async onSubscription(
subscription: Subscription,
events: SegmentEvent | SegmentEvent[],
Expand Down
34 changes: 34 additions & 0 deletions packages/core/src/destination-kit/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,40 @@ export type ActionDestinationErrorResponseType = {
body?: JSONLikeObject | string
}

export type AsyncActionResponseType = {
/** Indicates this is an async operation */
isAsync: true
/** Optional message about the async operation(s) */
message?: string
/** Initial status code */
status?: number
}

export type AsyncOperationResult = {
/** The current status of this operation */
status: 'pending' | 'completed' | 'failed'
/** Message about current state */
message?: string
/** Final result data when status is 'completed' */
result?: JSONLikeObject
/** Error information when status is 'failed' */
error?: {
code: string
message: string
}
/** Original context for this operation */
context?: JSONLikeObject
}

export type AsyncPollResponseType = {
/** Array of operation results - single element for individual operations, multiple for batch */
results: AsyncOperationResult[]
/** Overall status - completed when all operations are done */
overallStatus: 'pending' | 'completed' | 'failed' | 'partial'
/** Summary message */
message?: string
}

export type ResultMultiStatusNode =
| ActionDestinationSuccessResponseType
| (ActionDestinationErrorResponseType & {
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { Destination, fieldsToJsonSchema } from './destination-kit'
export type { AsyncActionResponseType, AsyncPollResponseType } from './destination-kit'
export { getAuthData } from './destination-kit/parse-settings'
export { transform, Features } from './mapping-kit'
export {
Expand Down
Loading