@output.ai/core 0.1.18 → 0.2.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@output.ai/core",
3
- "version": "0.1.18",
3
+ "version": "0.2.1",
4
4
  "description": "The core module of the output framework",
5
5
  "type": "module",
6
6
  "exports": {
@@ -38,6 +38,7 @@
38
38
  "@temporalio/workflow": "1.13.1",
39
39
  "redis": "5.8.3",
40
40
  "stacktrace-parser": "0.1.11",
41
+ "undici": "7.18.2",
41
42
  "zod": "4.1.12"
42
43
  },
43
44
  "license": "Apache-2.0",
package/src/consts.js CHANGED
@@ -1,4 +1,4 @@
1
- export const ACTIVITY_SEND_WEBHOOK = '__internal#sendWebhook';
1
+ export const ACTIVITY_SEND_HTTP_REQUEST = '__internal#sendHttpRequest';
2
2
  export const ACTIVITY_GET_TRACE_DESTINATIONS = '__internal#getTraceDestinations';
3
3
  export const METADATA_ACCESS_SYMBOL = Symbol( '__metadata' );
4
4
  export const SHARED_STEP_PREFIX = '__shared#';
package/src/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { z } from 'zod';
2
2
  import type { ActivityOptions } from '@temporalio/workflow';
3
+ import type { SerializedFetchResponse } from './utils/index.d.ts';
3
4
 
4
5
  /**
5
6
  * Expose z from Zod as a convenience.
@@ -7,6 +8,7 @@ import type { ActivityOptions } from '@temporalio/workflow';
7
8
  export { z } from 'zod';
8
9
 
9
10
  /**
11
+ * Exports Temporal's sleep() function for advanced use cases.
10
12
  * Pause workflow execution for a specified duration.
11
13
  *
12
14
  * Use this for delay-based throttling when calling external APIs.
@@ -21,6 +23,8 @@ export { z } from 'zod';
21
23
  * }
22
24
  * ```
23
25
  *
26
+ * @see {@link https://docs.temporal.io/develop/typescript/timers}
27
+ *
24
28
  * @param ms - Duration to sleep in milliseconds (or a string like '1s', '100ms')
25
29
  * @returns A promise that resolves after the specified duration
26
30
  *
@@ -49,6 +53,11 @@ export { continueAsNew } from '@temporalio/workflow';
49
53
  */
50
54
  export type AnyZodSchema = z.ZodType<any, any, any>; // eslint-disable-line @typescript-eslint/no-explicit-any
51
55
 
56
+ /**
57
+ * Allowed HTTP methods for request helpers.
58
+ */
59
+ export type HttpMethod = 'HEAD' | 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
60
+
52
61
  /**
53
62
  * Native Temporal configurations for activities.
54
63
  *
@@ -163,8 +172,11 @@ export type StepFunctionWrapper<StepFunction> =
163
172
  * - Never call another step from within a step.
164
173
  * - Never call a workflow from within a step.
165
174
  *
166
- * @typeParam InputSchema - Zod schema for the step input
167
- * @typeParam OutputSchema - Zod schema for the step output
175
+ * @typeParam InputSchema - Zod schema of the fn's input.
176
+ * @typeParam OutputSchema - Zod schema of the fn's return.
177
+ *
178
+ * @throws {@link ValidationError}
179
+ * @throws {@link FatalError}
168
180
  *
169
181
  * @param params - Step parameters
170
182
  * @param params.name - Human-readable step name (must start with a letter or underscore, followed by letters, numbers, or underscores)
@@ -402,6 +414,11 @@ export type WorkflowFunctionWrapper<WorkflowFunction> =
402
414
  * }
403
415
  * } )
404
416
  * ```
417
+ * @typeParam InputSchema - Zod schema of the fn's input.
418
+ * @typeParam OutputSchema - Zod schema of the fn's return.
419
+ *
420
+ * @throws {@link ValidationError}
421
+ * @throws {@link FatalError}
405
422
  *
406
423
  * @param params - Workflow parameters
407
424
  * @param params.name - Human-readable workflow name (must start with a letter or underscore, followed by letters, numbers, or underscores).
@@ -460,6 +477,11 @@ export class EvaluationResult {
460
477
  * @returns The evaluation result reasoning
461
478
  */
462
479
  get reasoning(): string;
480
+
481
+ /**
482
+ * @returns A zod schema representing this class
483
+ */
484
+ get schema(): string;
463
485
  }
464
486
 
465
487
  /**
@@ -553,6 +575,12 @@ export type EvaluatorFunctionWrapper<EvaluatorFunction> =
553
575
  *
554
576
  * It is translated to a Temporal Activity.
555
577
  *
578
+ * @typeParam InputSchema - Zod schema of the fn's input.
579
+ * @typeParam Result - Return type of the fn, extends EvaluationResult.
580
+ *
581
+ * @throws {@link ValidationError}
582
+ * @throws {@link FatalError}
583
+ *
556
584
  * @param params - Evaluator parameters
557
585
  * @param params.name - Human-readable evaluator name (must start with a letter or underscore, followed by letters, numbers, or underscores)
558
586
  * @param params.description - Description of the evaluator
@@ -580,17 +608,82 @@ export declare function evaluator<
580
608
  */
581
609
 
582
610
  /**
583
- * Create a webhook call that pauses the workflow until resumed via signal.
611
+ * Send an POST HTTP request to a URL, optionally with a payload, then wait for a webhook response.
612
+ *
613
+ * The "Content-Type" is inferred from the payload type and can be overridden via the `headers` argument.
584
614
  *
585
- * Sends a request via an activity; the workflow will await a corresponding
586
- * resume signal to continue and return the response payload.
615
+ * If the body is not a type natively accepted by the Fetch API, it is serialized to a string: `JSON.stringify()` for objects, or `String()` for primitives.
616
+ *
617
+ * When a body is sent, the payload is wrapped together with the `workflowId` and sent as:
618
+ * @example
619
+ * ```js
620
+ * const finalPayload = {
621
+ * workflowId,
622
+ * payload
623
+ * }
624
+ * ```
587
625
  *
588
- * @param params - Webhook request parameters
589
- * @param params.url - Webhook request URL (POST)
590
- * @param params.payload - Webhook request payload
591
- * @returns Resolves with the response payload when resumed
626
+ * After dispatching the request, the workflow pauses and waits for a POST to `/workflow/:id/feedback` (where `:id` is the `workflowId`). When the API receives that request, its body is delivered back to the workflow and execution resumes.
627
+ *
628
+ * @example
629
+ * ```js
630
+ * const response = await sendPostRequestAndAwaitWebhook( {
631
+ * url: 'https://example.com/integration',
632
+ * payload: {
633
+ * }
634
+ * } );
635
+ *
636
+ * assert( response, 'the value sent back via the api' );
637
+ * ```
638
+ *
639
+ * @remarks
640
+ * - Only callable from within a workflow function; do not use in steps or evaluators.
641
+ * - Steps and evaluators are activity-based and are not designed to be paused.
642
+ * - If used within steps or evaluators, a compilation error will be raised.
643
+ * - Uses a Temporal Activity to dispatch the HTTP request, working around the runtime limitation for workflows.
644
+ * - Uses a Temporal Trigger to pause the workflow.
645
+ * - Uses a Temporal Signal to resume the workflow when the API responds.
646
+ *
647
+ * @param params - Parameters object
648
+ * @param params.url - Request URL
649
+ * @param params.payload - Request payload
650
+ * @param params.headers - Headers for the request
651
+ * @returns Resolves with the payload received by the webhook
652
+ */
653
+ export declare function sendPostRequestAndAwaitWebhook( params: {
654
+ url: string;
655
+ payload?: object;
656
+ headers?: Record<string, string>;
657
+ } ): Promise<unknown>;
658
+
659
+ /**
660
+ * Send an HTTP request to a URL.
661
+ *
662
+ * For POST or PUT requests, an optional payload can be sent as the body.
663
+ *
664
+ * The "Content-Type" is inferred from the payload type and can be overridden via the `headers` argument.
665
+ *
666
+ * If the body is not a type natively accepted by the Fetch API, it is serialized to a string: `JSON.stringify()` for objects, or `String()` for primitives.
667
+ *
668
+ * @remarks
669
+ * - Intended for use within workflow functions; do not use in steps or evaluators.
670
+ * - Steps and evaluators are activity-based and can perform HTTP requests directly.
671
+ * - If used within steps or evaluators, a compilation error will be raised.
672
+ * - Uses a Temporal Activity to dispatch the HTTP request, working around the runtime limitation for workflows.
673
+ *
674
+ * @param params - Parameters object
675
+ * @param params.url - Request URL
676
+ * @param params.method - The HTTP method (default: 'GET')
677
+ * @param params.payload - Request payload (only for POST/PUT)
678
+ * @param params.headers - Headers for the request
679
+ * @returns Resolves with an HTTP response serialized to a plain object
592
680
  */
593
- export declare function createWebhook( params: { url: string; payload?: object } ): Promise<object>;
681
+ export declare function sendHttpRequest( params: {
682
+ url: string;
683
+ method?: HttpMethod;
684
+ payload?: object;
685
+ headers?: Record<string, string>;
686
+ } ): Promise<SerializedFetchResponse>;
594
687
 
595
688
  /*
596
689
  ╭─────────────╮
package/src/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { evaluator, EvaluationStringResult, EvaluationNumberResult, EvaluationBooleanResult } from './interface/evaluator.js';
2
2
  import { step } from './interface/step.js';
3
3
  import { workflow } from './interface/workflow.js';
4
- import { createWebhook } from './interface/webhook.js';
4
+ import { sendHttpRequest, sendPostRequestAndAwaitWebhook } from './interface/webhook.js';
5
5
  import { FatalError, ValidationError } from './errors.js';
6
6
  export { continueAsNew, sleep } from '@temporalio/workflow';
7
7
  import { z } from 'zod';
@@ -15,8 +15,9 @@ export {
15
15
  EvaluationNumberResult,
16
16
  EvaluationStringResult,
17
17
  EvaluationBooleanResult,
18
- // webhook tool
19
- createWebhook,
18
+ // webhook tools
19
+ sendHttpRequest,
20
+ sendPostRequestAndAwaitWebhook,
20
21
  // errors
21
22
  FatalError,
22
23
  ValidationError,
@@ -34,11 +34,17 @@ export class EvaluationResult {
34
34
  */
35
35
  reasoning = undefined;
36
36
 
37
- static #schema = z.object( {
38
- value: z.any(),
39
- confidence: z.number(),
40
- reasoning: z.string().optional()
41
- } );
37
+ static get valueSchema() {
38
+ return z.any();
39
+ };
40
+
41
+ static get schema() {
42
+ return z.object( {
43
+ value: this.valueSchema,
44
+ confidence: z.number(),
45
+ reasoning: z.string().optional()
46
+ } );
47
+ };
42
48
 
43
49
  /**
44
50
  * @constructor
@@ -48,7 +54,7 @@ export class EvaluationResult {
48
54
  * @param {string} [args.reasoning] - The reasoning behind the result
49
55
  */
50
56
  constructor( args ) {
51
- const result = EvaluationResult.#schema.safeParse( args );
57
+ const result = this.constructor.schema.safeParse( args );
52
58
  if ( result.error ) {
53
59
  throw new EvaluationResultValidationError( z.prettifyError( result.error ) );
54
60
  }
@@ -64,7 +70,9 @@ export class EvaluationResult {
64
70
  * @property {string} value - The evaluation result value
65
71
  */
66
72
  export class EvaluationStringResult extends EvaluationResult {
67
- static #valueSchema = z.string();
73
+ static get valueSchema() {
74
+ return z.string();
75
+ };
68
76
 
69
77
  /**
70
78
  * @constructor
@@ -74,10 +82,6 @@ export class EvaluationStringResult extends EvaluationResult {
74
82
  * @param {string} [args.reasoning] - The reasoning behind the result
75
83
  */
76
84
  constructor( args ) {
77
- const result = EvaluationStringResult.#valueSchema.safeParse( args.value );
78
- if ( result.error ) {
79
- throw new EvaluationResultValidationError( z.prettifyError( result.error ) );
80
- }
81
85
  super( args );
82
86
  }
83
87
  };
@@ -88,7 +92,9 @@ export class EvaluationStringResult extends EvaluationResult {
88
92
  * @property {boolean} value - The evaluation result value
89
93
  */
90
94
  export class EvaluationBooleanResult extends EvaluationResult {
91
- static #valueSchema = z.boolean();
95
+ static get valueSchema() {
96
+ return z.boolean();
97
+ };
92
98
 
93
99
  /**
94
100
  * @constructor
@@ -98,10 +104,6 @@ export class EvaluationBooleanResult extends EvaluationResult {
98
104
  * @param {string} [args.reasoning] - The reasoning behind the result
99
105
  */
100
106
  constructor( args ) {
101
- const result = EvaluationBooleanResult.#valueSchema.safeParse( args.value );
102
- if ( result.error ) {
103
- throw new EvaluationResultValidationError( z.prettifyError( result.error ) );
104
- }
105
107
  super( args );
106
108
  }
107
109
  };
@@ -112,7 +114,9 @@ export class EvaluationBooleanResult extends EvaluationResult {
112
114
  * @property {number} value - The evaluation result value
113
115
  */
114
116
  export class EvaluationNumberResult extends EvaluationResult {
115
- static #valueSchema = z.number();
117
+ static get valueSchema() {
118
+ return z.number();
119
+ };
116
120
 
117
121
  /**
118
122
  * @constructor
@@ -122,10 +126,6 @@ export class EvaluationNumberResult extends EvaluationResult {
122
126
  * @param {string} [args.reasoning] - The reasoning behind the result
123
127
  */
124
128
  constructor( args ) {
125
- const result = EvaluationNumberResult.#valueSchema.safeParse( args.value );
126
- if ( result.error ) {
127
- throw new EvaluationResultValidationError( z.prettifyError( result.error ) );
128
- }
129
129
  super( args );
130
130
  }
131
131
  };
@@ -138,7 +138,7 @@ export function evaluator( { name, description, inputSchema, fn, options } ) {
138
138
 
139
139
  const output = await fn( input );
140
140
 
141
- if ( !output instanceof EvaluationResult ) {
141
+ if ( !( output instanceof EvaluationResult ) ) {
142
142
  throw new ValidationError( 'Evaluators must return an EvaluationResult' );
143
143
  }
144
144
 
@@ -0,0 +1,103 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ EvaluationResult,
4
+ EvaluationStringResult,
5
+ EvaluationNumberResult,
6
+ EvaluationBooleanResult
7
+ } from './evaluator.js';
8
+ import { ValidationError } from '#errors';
9
+
10
+ describe( 'interface/evaluator - EvaluationResult classes', () => {
11
+ describe( 'class inheritance', () => {
12
+ it( 'subclasses extend EvaluationResult', () => {
13
+ const s = new EvaluationStringResult( { value: 'ok', confidence: 0.8 } );
14
+ const n = new EvaluationNumberResult( { value: 42, confidence: 1 } );
15
+ const b = new EvaluationBooleanResult( { value: true, confidence: 0.5 } );
16
+
17
+ expect( s ).toBeInstanceOf( EvaluationResult );
18
+ expect( n ).toBeInstanceOf( EvaluationResult );
19
+ expect( b ).toBeInstanceOf( EvaluationResult );
20
+ } );
21
+ } );
22
+
23
+ describe( 'constructor payload validation', () => {
24
+ it( 'base class validates presence and types of common fields', () => {
25
+ // valid
26
+ const base = new EvaluationResult( { value: { any: 'thing' }, confidence: 0.1 } );
27
+ expect( base.value ).toEqual( { any: 'thing' } );
28
+ expect( base.confidence ).toBe( 0.1 );
29
+ expect( base.reasoning ).toBeUndefined();
30
+
31
+ // invalid: missing confidence
32
+ expect( () => new EvaluationResult( { value: 1 } ) ).toThrow( ValidationError );
33
+
34
+ // invalid: confidence wrong type
35
+ expect( () => new EvaluationResult( { value: 'x', confidence: 'nope' } ) ).toThrow( ValidationError );
36
+
37
+ // invalid: reasoning wrong type
38
+ expect( () => new EvaluationResult( { value: 'x', confidence: 1, reasoning: 123 } ) ).toThrow( ValidationError );
39
+ } );
40
+
41
+ it( 'string subclass enforces string value', () => {
42
+ // valid
43
+ const r = new EvaluationStringResult( { value: 'hello', confidence: 0.9 } );
44
+ expect( r.value ).toBe( 'hello' );
45
+
46
+ // invalid
47
+ expect( () => new EvaluationStringResult( { value: 123, confidence: 0.2 } ) ).toThrow( ValidationError );
48
+ } );
49
+
50
+ it( 'number subclass enforces number value', () => {
51
+ // valid
52
+ const r = new EvaluationNumberResult( { value: 123, confidence: 0.2 } );
53
+ expect( r.value ).toBe( 123 );
54
+
55
+ // invalid
56
+ expect( () => new EvaluationNumberResult( { value: 'nope', confidence: 0.2 } ) ).toThrow( ValidationError );
57
+ } );
58
+
59
+ it( 'boolean subclass enforces boolean value', () => {
60
+ // valid
61
+ const r = new EvaluationBooleanResult( { value: true, confidence: 1 } );
62
+ expect( r.value ).toBe( true );
63
+
64
+ // invalid
65
+ expect( () => new EvaluationBooleanResult( { value: 'nope', confidence: 0.2 } ) ).toThrow( ValidationError );
66
+ } );
67
+ } );
68
+
69
+ describe( 'static schema getter', () => {
70
+ it( 'base schema accepts any value and optional reasoning', () => {
71
+ const ok = EvaluationResult.schema.safeParse( { value: 'x', confidence: 0.5, reasoning: 'why' } );
72
+ expect( ok.success ).toBe( true );
73
+
74
+ const ok2 = EvaluationResult.schema.safeParse( { value: 123, confidence: 0.5 } );
75
+ expect( ok2.success ).toBe( true );
76
+ } );
77
+
78
+ it( 'string schema requires value to be string', () => {
79
+ const ok = EvaluationStringResult.schema.safeParse( { value: 'x', confidence: 1 } );
80
+ expect( ok.success ).toBe( true );
81
+
82
+ const bad = EvaluationStringResult.schema.safeParse( { value: 123, confidence: 1 } );
83
+ expect( bad.success ).toBe( false );
84
+ } );
85
+
86
+ it( 'number schema requires value to be number', () => {
87
+ const ok = EvaluationNumberResult.schema.safeParse( { value: 10, confidence: 1 } );
88
+ expect( ok.success ).toBe( true );
89
+
90
+ const bad = EvaluationNumberResult.schema.safeParse( { value: '10', confidence: 1 } );
91
+ expect( bad.success ).toBe( false );
92
+ } );
93
+
94
+ it( 'boolean schema requires value to be boolean', () => {
95
+ const ok = EvaluationBooleanResult.schema.safeParse( { value: false, confidence: 1 } );
96
+ expect( ok.success ).toBe( true );
97
+
98
+ const bad = EvaluationBooleanResult.schema.safeParse( { value: 'false', confidence: 1 } );
99
+ expect( bad.success ).toBe( false );
100
+ } );
101
+ } );
102
+ } );
103
+
@@ -55,9 +55,11 @@ const stepAndWorkflowSchema = z.strictObject( {
55
55
 
56
56
  const evaluatorSchema = stepAndWorkflowSchema.omit( { outputSchema: true } );
57
57
 
58
- const webhookSchema = z.object( {
58
+ const httpRequestSchema = z.object( {
59
59
  url: z.url( { protocol: /^https?$/ } ),
60
- payload: z.any().optional()
60
+ method: z.enum( [ 'GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE' ] ),
61
+ payload: z.any().optional(),
62
+ headers: z.record( z.string(), z.string() ).optional()
61
63
  } );
62
64
 
63
65
  const validateAgainstSchema = ( schema, args ) => {
@@ -98,11 +100,11 @@ export function validateWorkflow( args ) {
98
100
  };
99
101
 
100
102
  /**
101
- * Validate createWebhook payload
103
+ * Validate request payload
102
104
  *
103
- * @param {object} args - The createWebhook arguments
105
+ * @param {object} args - The request arguments
104
106
  * @throws {StaticValidationError} Throws if args are invalid
105
107
  */
106
- export function validateCreateWebhook( args ) {
107
- validateAgainstSchema( webhookSchema, args );
108
+ export function validateRequestPayload( args ) {
109
+ validateAgainstSchema( httpRequestSchema, args );
108
110
  };
@@ -1,6 +1,6 @@
1
1
  import { describe, it, expect } from 'vitest';
2
2
  import { z } from 'zod';
3
- import { validateStep, validateWorkflow, validateCreateWebhook, validateEvaluator, StaticValidationError } from './static.js';
3
+ import { validateStep, validateWorkflow, validateRequestPayload, validateEvaluator, StaticValidationError } from './static.js';
4
4
 
5
5
  const validArgs = Object.freeze( {
6
6
  name: 'valid_name',
@@ -186,28 +186,77 @@ describe( 'interface/validator', () => {
186
186
  } );
187
187
  } );
188
188
 
189
- describe( 'validate webhook', () => {
189
+ describe( 'validate request', () => {
190
190
  it( 'passes with valid http url', () => {
191
- expect( () => validateCreateWebhook( { url: 'http://example.com' } ) ).not.toThrow();
191
+ expect( () => validateRequestPayload( { url: 'http://example.com', method: 'GET' } ) ).not.toThrow();
192
192
  } );
193
193
 
194
194
  it( 'passes with valid https url', () => {
195
- expect( () => validateCreateWebhook( { url: 'https://example.com/path?q=1' } ) ).not.toThrow();
195
+ expect( () => validateRequestPayload( { url: 'https://example.com/path?q=1', method: 'GET' } ) ).not.toThrow();
196
196
  } );
197
197
 
198
198
  it( 'rejects missing url', () => {
199
199
  const error = new StaticValidationError( '✖ Invalid input: expected string, received undefined\n → at url' );
200
- expect( () => validateCreateWebhook( { } ) ).toThrow( error );
200
+ expect( () => validateRequestPayload( { method: 'GET' } ) ).toThrow( error );
201
201
  } );
202
202
 
203
203
  it( 'rejects invalid scheme', () => {
204
204
  const error = new StaticValidationError( '✖ Invalid URL\n → at url' );
205
- expect( () => validateCreateWebhook( { url: 'ftp://example.com' } ) ).toThrow( error );
205
+ expect( () => validateRequestPayload( { url: 'ftp://example.com', method: 'GET' } ) ).toThrow( error );
206
206
  } );
207
207
 
208
208
  it( 'rejects malformed url', () => {
209
209
  const error = new StaticValidationError( '✖ Invalid URL\n → at url' );
210
- expect( () => validateCreateWebhook( { url: 'http:////' } ) ).toThrow( error );
210
+ expect( () => validateRequestPayload( { url: 'http:////', method: 'GET' } ) ).toThrow( error );
211
+ } );
212
+
213
+ it( 'rejects missing method', () => {
214
+ expect( () => validateRequestPayload( { url: 'https://example.com' } ) ).toThrow( StaticValidationError );
215
+ } );
216
+
217
+ it( 'passes with headers as string map', () => {
218
+ const request = {
219
+ url: 'https://example.com',
220
+ method: 'GET',
221
+ headers: { 'x-api-key': 'abc', accept: 'application/json' }
222
+ };
223
+ expect( () => validateRequestPayload( request ) ).not.toThrow();
224
+ } );
225
+
226
+ it( 'rejects non-object headers', () => {
227
+ const request = {
228
+ url: 'https://example.com',
229
+ method: 'GET',
230
+ headers: 5
231
+ };
232
+ expect( () => validateRequestPayload( request ) ).toThrow( StaticValidationError );
233
+ } );
234
+
235
+ it( 'rejects headers with non-string values', () => {
236
+ const request = {
237
+ url: 'https://example.com',
238
+ method: 'GET',
239
+ headers: { 'x-num': 123 }
240
+ };
241
+ expect( () => validateRequestPayload( request ) ).toThrow( StaticValidationError );
242
+ } );
243
+
244
+ it( 'passes with payload object', () => {
245
+ const request = {
246
+ url: 'https://example.com/api',
247
+ method: 'POST',
248
+ payload: { a: 1, b: 'two' }
249
+ };
250
+ expect( () => validateRequestPayload( request ) ).not.toThrow();
251
+ } );
252
+
253
+ it( 'passes with payload string', () => {
254
+ const request = {
255
+ url: 'https://example.com/upload',
256
+ method: 'POST',
257
+ payload: 'raw-body'
258
+ };
259
+ expect( () => validateRequestPayload( request ) ).not.toThrow();
211
260
  } );
212
261
  } );
213
262
  } );
@@ -1,31 +1,64 @@
1
1
  // THIS RUNS IN THE TEMPORAL'S SANDBOX ENVIRONMENT
2
- import { defineSignal, setHandler, proxyActivities, workflowInfo, proxySinks, uuid4 } from '@temporalio/workflow';
3
- import { ACTIVITY_SEND_WEBHOOK } from '#consts';
2
+ import { defineSignal, setHandler, proxyActivities, workflowInfo, proxySinks, uuid4, Trigger } from '@temporalio/workflow';
3
+ import { ACTIVITY_SEND_HTTP_REQUEST } from '#consts';
4
4
  import { FatalError } from '#errors';
5
- import { validateCreateWebhook } from './validations/static.js';
5
+ import { validateRequestPayload } from './validations/static.js';
6
6
 
7
- export async function createWebhook( { url, payload } ) {
8
- validateCreateWebhook( { url, payload } );
9
- const { workflowId } = workflowInfo();
10
-
11
- await proxyActivities( {
7
+ /**
8
+ * Call the internal activity to make a HTTP request and returns its response.
9
+ *
10
+ * @param {Object} parameters
11
+ * @param {string} url
12
+ * @param {string} method
13
+ * @param {unknown} [payload]
14
+ * @param {object} [headers]
15
+ * @returns {Promise<object>} The serialized HTTP response
16
+ */
17
+ export async function sendHttpRequest( { url, method = 'GET', payload = undefined, headers = undefined } ) {
18
+ validateRequestPayload( { method, url, payload, headers } );
19
+ const res = await proxyActivities( {
12
20
  startToCloseTimeout: '3m',
13
21
  retry: {
14
22
  initialInterval: '15s',
15
- maximumAttempts: 5,
23
+ maximumAttempts: 3,
16
24
  nonRetryableErrorTypes: [ FatalError.name ]
17
25
  }
18
- } )[ACTIVITY_SEND_WEBHOOK]( { url, workflowId, payload } );
26
+ } )[ACTIVITY_SEND_HTTP_REQUEST]( { method, url, payload, headers } );
27
+ return res;
28
+ };
29
+
30
+ /**
31
+ * Call the internal activity to make a POST request sending a payload to a given url.
32
+ *
33
+ * After the request succeeds, pause the code using Trigger and wait for a Signal to un-pause it.
34
+ *
35
+ * The signal will be sent by the API when a response is sent to its webhook url.
36
+ *
37
+ * @param {Object} parameters
38
+ * @param {string} url
39
+ * @param {unknown} [payload]
40
+ * @param {object} [headers]
41
+ * @returns {Promise<unknown>} The response received by the webhook
42
+ */
43
+ export async function sendPostRequestAndAwaitWebhook( { url, payload = undefined, headers = undefined } ) {
44
+ const { workflowId } = workflowInfo();
45
+ const wrappedPayload = { workflowId, payload };
46
+
47
+ await sendHttpRequest( { method: 'POST', url, payload: wrappedPayload, headers } );
19
48
 
20
49
  const sinks = await proxySinks();
50
+ const resumeTrigger = new Trigger();
21
51
  const resumeSignal = defineSignal( 'resume' );
22
52
 
23
- const traceId = `${workflowId}-${url}-${uuid4}`;
53
+ const traceId = `${workflowId}-${url}-${uuid4()}`;
24
54
  sinks.trace.addEventStart( { id: traceId, name: 'resume', kind: 'webhook' } );
25
- return new Promise( resolve =>
26
- setHandler( resumeSignal, responsePayload => {
27
- sinks.trace.addEventEnd( { id: traceId, details: responsePayload } );
28
- resolve( responsePayload );
29
- } )
30
- );
55
+
56
+ setHandler( resumeSignal, webhookPayload => {
57
+ if ( !resumeTrigger.resolved ) {
58
+ sinks.trace.addEventEnd( { id: traceId, details: webhookPayload } );
59
+ resumeTrigger.resolve( webhookPayload );
60
+ }
61
+ } );
62
+
63
+ return resumeTrigger;
31
64
  };