Skip to content

Commit 514947e

Browse files
committed
stateContext param
1 parent c30299a commit 514947e

File tree

4 files changed

+226
-13
lines changed

4 files changed

+226
-13
lines changed

packages/core/src/destination-kit/__tests__/multi-async-action.test.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,16 @@ describe('Multi Async Actions', () => {
3636
async pollMultiple(_request: any, data: MultiPollInput<unknown, unknown>) {
3737
// Simulate polling multiple operations using context data
3838
const results = data.operationIds.map((operationId, index) => {
39-
const context = data.contexts?.[operationId]
39+
// Get context from stateContext instead of a non-existent contexts property
40+
const contextData = data.stateContext?.getRequestContext(`operation_${operationId}`)
41+
const context = contextData && typeof contextData === 'string' ? JSON.parse(contextData) : {}
42+
const priority = context?.priority
4043
return {
4144
operationId,
4245
status: 'completed' as const,
4346
result: {
4447
processedName: `processed-${index}`,
45-
priority: context?.priority,
48+
priority: priority,
4649
originalIndex: context?.payloadIndex
4750
}
4851
}

packages/core/src/destination-kit/action.ts

Lines changed: 54 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -153,16 +153,31 @@ export interface ActionDefinition<
153153
* The operation to poll for the status of an async operation initiated by performBatch.
154154
* This method is only required if performBatch returns AsyncOperationResponse.
155155
*/
156-
poll?: (request: RequestClient, data: PollInput<Settings, AudienceSettings>) => MaybePromise<PollResponse>
156+
poll?: (
157+
request: RequestClient,
158+
data: PollInput<Settings, AudienceSettings>,
159+
context: {
160+
stateContext?: StateContext
161+
logger?: Logger
162+
features?: Features
163+
statsContext?: StatsContext
164+
}
165+
) => MaybePromise<PollResponse>
157166

158167
/**
159168
* The operation to poll for the status of multiple async operations initiated by performBatch.
160169
* This method is only required if performBatch returns MultiAsyncOperationResponse.
161170
*/
162171
pollMultiple?: (
163172
request: RequestClient,
164-
data: MultiPollInput<Settings, AudienceSettings>
165-
) => MaybePromise<MultiPollResponse>
173+
data: MultiPollInput<Settings, AudienceSettings>,
174+
context: {
175+
stateContext?: StateContext
176+
logger?: Logger
177+
features?: Features
178+
statsContext?: StatsContext
179+
}
180+
) => MaybePromise<PollResponse[]>
166181

167182
/** Hooks are triggered at some point in a mappings lifecycle. They may perform a request with the
168183
* destination using the provided inputs and return a response. The response may then optionally be stored
@@ -757,8 +772,16 @@ export class Action<Settings, Payload extends JSONLikeObject, AudienceSettings =
757772
// Create request client for the poll operation
758773
const requestClient = this.createPollRequestClient(pollInput)
759774

775+
// Extract context for poll method
776+
const context = {
777+
stateContext: pollInput.stateContext,
778+
logger: pollInput.logger,
779+
features: pollInput.features,
780+
statsContext: pollInput.statsContext
781+
}
782+
760783
// Execute the poll function
761-
const response = await this.definition.poll(requestClient, pollInput)
784+
const response = await this.definition.poll(requestClient, pollInput, context)
762785
return response
763786
}
764787

@@ -777,9 +800,34 @@ export class Action<Settings, Payload extends JSONLikeObject, AudienceSettings =
777800
// Create request client for the poll operation
778801
const requestClient = this.createPollRequestClient(pollInput)
779802

803+
// Extract context for poll method
804+
const context = {
805+
stateContext: pollInput.stateContext,
806+
logger: pollInput.logger,
807+
features: pollInput.features,
808+
statsContext: pollInput.statsContext
809+
}
810+
780811
// Execute the pollMultiple function
781-
const response = await this.definition.pollMultiple(requestClient, pollInput)
782-
return response
812+
const responses = await this.definition.pollMultiple(requestClient, pollInput, context)
813+
814+
// Transform array of PollResponse to MultiPollResponse
815+
const results = responses.map((response, index) => ({
816+
operationId: pollInput.operationIds[index],
817+
status: response.status,
818+
message: response.message,
819+
error: response.error
820+
}))
821+
822+
// Determine overall status
823+
const allCompleted = results.every((r) => r.status === 'completed')
824+
const anyFailed = results.some((r) => r.status === 'failed')
825+
const overallStatus = anyFailed ? 'failed' : allCompleted ? 'completed' : 'partial'
826+
827+
return {
828+
results,
829+
overallStatus
830+
}
783831
}
784832

785833
/**

packages/core/src/destination-kit/examples/async-action-example.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -113,11 +113,9 @@ const exampleAsyncAction: ActionDefinition<ExampleSettings, ExamplePayload> = {
113113
const asyncResponse: AsyncOperationResponse = {
114114
operationId: responseData.operationId,
115115
status: 'pending',
116-
message: `Batch operation initiated for ${payload.length} records`,
117-
data: {
118-
recordCount: payload.length,
119-
estimatedDuration: responseData.estimatedDuration || '5-10 minutes'
120-
}
116+
message: `Batch operation initiated for ${payload.length} records (estimated duration: ${
117+
responseData.estimatedDuration || '5-10 minutes'
118+
})`
121119
}
122120

123121
return asyncResponse
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
/**
2+
* Test example demonstrating context passing from performBatch to poll methods
3+
* using stateContext pattern inspired by Braze cohorts destination
4+
*/
5+
6+
import { ActionDefinition, ExecuteInput } from '../'
7+
import type { RequestClient } from '../../create-request-client'
8+
9+
interface Settings {
10+
apiKey: string
11+
region: string
12+
}
13+
14+
interface Payload {
15+
userId: string
16+
attributes: Record<string, unknown>
17+
}
18+
19+
const testAction: ActionDefinition<Settings, Payload> = {
20+
title: 'Context Passing Test Action',
21+
description: 'Test action to verify context passing between performBatch and poll methods',
22+
fields: {
23+
userId: {
24+
label: 'User ID',
25+
description: 'Unique identifier for the user',
26+
type: 'string',
27+
required: true
28+
},
29+
attributes: {
30+
label: 'User Attributes',
31+
description: 'Additional attributes for the user',
32+
type: 'object',
33+
required: false
34+
}
35+
},
36+
37+
async perform(request: RequestClient, data: ExecuteInput<Settings, Payload>) {
38+
// Single record operation (not async)
39+
const { payload, settings } = data
40+
41+
const response = await request('https://api.example.com/single', {
42+
method: 'POST',
43+
json: {
44+
apiKey: settings.apiKey,
45+
region: settings.region,
46+
user: payload
47+
}
48+
})
49+
50+
return await response.json()
51+
},
52+
53+
async performBatch(request: RequestClient, data: ExecuteInput<Settings, Payload[]>) {
54+
const { payload, settings, stateContext } = data
55+
56+
// Simulate making an API call that returns an operation ID
57+
const response = await request('https://api.example.com/batch', {
58+
method: 'POST',
59+
json: {
60+
apiKey: settings.apiKey,
61+
region: settings.region,
62+
users: payload
63+
}
64+
})
65+
66+
const responseData = await response.json()
67+
68+
// Store additional context that poll method will need
69+
// This follows the pattern from Braze cohorts destination
70+
stateContext?.setResponseContext(
71+
'batchMetadata',
72+
JSON.stringify({
73+
submittedAt: new Date().toISOString(),
74+
totalRecords: payload.length,
75+
region: settings.region,
76+
requestId: responseData.requestId
77+
}),
78+
{}
79+
)
80+
81+
stateContext?.setResponseContext(
82+
'apiCredentials',
83+
JSON.stringify({
84+
apiKey: settings.apiKey,
85+
endpoint: 'https://api.example.com'
86+
}),
87+
{}
88+
)
89+
90+
return {
91+
operationId: responseData.operationId,
92+
status: 'pending' as const,
93+
message: `Batch operation initiated for ${payload.length} records`
94+
}
95+
},
96+
97+
async poll(request: RequestClient, data, context) {
98+
const { operationId, settings } = data
99+
const { stateContext } = context
100+
101+
// Retrieve context set by performBatch method
102+
const batchMetadataStr = stateContext?.getRequestContext('batchMetadata')
103+
const apiCredentialsStr = stateContext?.getRequestContext('apiCredentials')
104+
105+
const batchMetadata = batchMetadataStr && typeof batchMetadataStr === 'string' ? JSON.parse(batchMetadataStr) : null
106+
const apiCredentials =
107+
apiCredentialsStr && typeof apiCredentialsStr === 'string' ? JSON.parse(apiCredentialsStr) : null
108+
109+
// Use the context data in the poll request
110+
const response = await request(`${apiCredentials?.endpoint || 'https://api.example.com'}/status/${operationId}`, {
111+
method: 'GET',
112+
headers: {
113+
Authorization: `Bearer ${apiCredentials?.apiKey || settings.apiKey}`,
114+
'X-Request-ID': batchMetadata?.requestId || 'unknown'
115+
}
116+
})
117+
118+
const statusData = await response.json()
119+
120+
return {
121+
status: (statusData.completed ? 'completed' : 'pending') as 'pending' | 'completed' | 'failed',
122+
message: batchMetadata
123+
? `Operation for ${batchMetadata.totalRecords} records (submitted at ${batchMetadata.submittedAt}): ${statusData.message}`
124+
: statusData.message
125+
}
126+
},
127+
128+
async pollMultiple(request: RequestClient, data, context) {
129+
const { operationIds, settings } = data
130+
const { stateContext } = context
131+
132+
// Retrieve context set by performBatch method
133+
const batchMetadataStr = stateContext?.getRequestContext('batchMetadata')
134+
const apiCredentialsStr = stateContext?.getRequestContext('apiCredentials')
135+
136+
const batchMetadata = batchMetadataStr && typeof batchMetadataStr === 'string' ? JSON.parse(batchMetadataStr) : null
137+
const apiCredentials =
138+
apiCredentialsStr && typeof apiCredentialsStr === 'string' ? JSON.parse(apiCredentialsStr) : null
139+
140+
// Poll multiple operations using context data
141+
const pollPromises = operationIds.map(async (operationId) => {
142+
const response = await request(`${apiCredentials?.endpoint || 'https://api.example.com'}/status/${operationId}`, {
143+
method: 'GET',
144+
headers: {
145+
Authorization: `Bearer ${apiCredentials?.apiKey || settings.apiKey}`,
146+
'X-Request-ID': batchMetadata?.requestId || 'unknown'
147+
}
148+
})
149+
150+
const statusData = await response.json()
151+
152+
return {
153+
status: (statusData.completed ? 'completed' : 'pending') as 'pending' | 'completed' | 'failed',
154+
message: batchMetadata
155+
? `Operation ${operationId} for batch submitted at ${batchMetadata.submittedAt}: ${statusData.message}`
156+
: statusData.message
157+
}
158+
})
159+
160+
return Promise.all(pollPromises)
161+
}
162+
}
163+
164+
export default testAction

0 commit comments

Comments
 (0)