@outputai/core 0.8.2-next.4b5c049.0 → 0.8.2-next.57bc8d6.0

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.
Files changed (73) hide show
  1. package/package.json +4 -8
  2. package/src/bus.js +1 -1
  3. package/src/helpers/component.js +12 -0
  4. package/src/helpers/component.spec.js +54 -0
  5. package/src/helpers/fetch.js +105 -0
  6. package/src/helpers/fetch.spec.js +203 -0
  7. package/src/helpers/function.js +15 -0
  8. package/src/helpers/function.spec.js +48 -0
  9. package/src/helpers/object.js +98 -0
  10. package/src/helpers/object.spec.js +377 -0
  11. package/src/helpers/promise.js +29 -0
  12. package/src/helpers/promise.spec.js +35 -0
  13. package/src/helpers/string.js +30 -0
  14. package/src/helpers/string.spec.js +64 -0
  15. package/src/hooks/index.d.ts +102 -30
  16. package/src/hooks/index.js +16 -1
  17. package/src/hooks/index.spec.js +55 -1
  18. package/src/index.d.ts +2 -2
  19. package/src/interface/evaluator.d.ts +2 -2
  20. package/src/interface/evaluator.js +14 -12
  21. package/src/interface/evaluator.spec.js +10 -6
  22. package/src/interface/index.d.ts +1 -1
  23. package/src/interface/index.js +1 -1
  24. package/src/interface/logger.d.ts +52 -44
  25. package/src/interface/logger.js +17 -12
  26. package/src/interface/logger.spec.js +35 -1
  27. package/src/interface/step.d.ts +1 -1
  28. package/src/interface/step.js +15 -12
  29. package/src/interface/step.spec.js +10 -6
  30. package/src/interface/webhook.d.ts +21 -2
  31. package/src/interface/workflow.d.ts +2 -2
  32. package/src/interface/workflow.js +85 -79
  33. package/src/interface/workflow.spec.js +11 -4
  34. package/src/internal_activities/index.js +38 -36
  35. package/src/internal_activities/index.spec.js +27 -4
  36. package/src/logger/development.js +1 -1
  37. package/src/logger/development.spec.js +1 -1
  38. package/src/sdk/helpers/index.d.ts +1 -0
  39. package/src/sdk/helpers/index.js +1 -0
  40. package/src/sdk/helpers/objects.d.ts +51 -0
  41. package/src/sdk/helpers/objects.js +8 -0
  42. package/src/sdk/helpers/objects.spec.js +16 -0
  43. package/src/tracing/processors/s3/redis_client.spec.js +0 -6
  44. package/src/tracing/processors/s3/s3_client.spec.js +0 -6
  45. package/src/tracing/trace_engine.js +1 -1
  46. package/src/worker/catalog_workflow/catalog_job.js +1 -1
  47. package/src/worker/catalog_workflow/index.spec.js +8 -11
  48. package/src/worker/configs.js +1 -1
  49. package/src/worker/connection_monitor.js +1 -1
  50. package/src/worker/global_functions.js +2 -8
  51. package/src/worker/index.js +1 -1
  52. package/src/worker/interceptors/activity.js +8 -11
  53. package/src/worker/interceptors/activity.spec.js +25 -26
  54. package/src/worker/interceptors/workflow.js +3 -3
  55. package/src/worker/interceptors/workflow.spec.js +1 -1
  56. package/src/worker/loader/matchers.js +1 -1
  57. package/src/worker/sinks.js +1 -1
  58. package/src/worker/sinks.spec.js +1 -1
  59. package/src/internal_utils/component.js +0 -9
  60. package/src/utils/index.d.ts +0 -148
  61. package/src/utils/index.js +0 -1
  62. package/src/utils/utils.js +0 -307
  63. package/src/utils/utils.spec.js +0 -723
  64. /package/src/{internal_utils → helpers}/aggregations.js +0 -0
  65. /package/src/{internal_utils → helpers}/aggregations.spec.js +0 -0
  66. /package/src/{internal_utils → helpers}/errors.js +0 -0
  67. /package/src/{internal_utils → helpers}/errors.spec.js +0 -0
  68. /package/src/{internal_utils → helpers}/temporal_context.js +0 -0
  69. /package/src/{internal_utils → helpers}/temporal_context.spec.ts +0 -0
  70. /package/src/{internal_utils → helpers}/trace_info.js +0 -0
  71. /package/src/{internal_utils → helpers}/trace_info.spec.js +0 -0
  72. /package/src/{internal_utils → helpers}/workflow_context.js +0 -0
  73. /package/src/{internal_utils → helpers}/workflow_context.spec.js +0 -0
@@ -1,6 +1,5 @@
1
1
  import { StepValidator } from './validations/index.js';
2
- import { setMetadata } from '#internal_utils/component';
3
- import { ComponentType } from '#consts';
2
+ import { createStep } from '#helpers/component';
4
3
 
5
4
  /**
6
5
  * Create a new step (activity flavor) and return a wrapper function around its fn handler
@@ -9,13 +8,17 @@ export function step( { name, description, inputSchema, outputSchema, fn, option
9
8
  StepValidator.validateDefinition( { name, description, inputSchema, outputSchema, fn, options } );
10
9
  const validator = new StepValidator( { name, inputSchema, outputSchema } );
11
10
 
12
- const wrapper = async input => {
13
- validator.validateInput( input );
14
- const output = await fn( input );
15
- validator.validateOutput( output );
16
- return output;
17
- };
18
-
19
- setMetadata( wrapper, { name, description, inputSchema, outputSchema, type: ComponentType.STEP, options } );
20
- return wrapper;
21
- };
11
+ return createStep( {
12
+ name,
13
+ description,
14
+ inputSchema,
15
+ outputSchema,
16
+ options,
17
+ handler: async input => {
18
+ validator.validateInput( input );
19
+ const output = await fn( input );
20
+ validator.validateOutput( output );
21
+ return output;
22
+ }
23
+ } );
24
+ }
@@ -1,11 +1,11 @@
1
1
  import { describe, it, expect, vi, beforeEach } from 'vitest';
2
2
  import { ValidationError } from '#errors';
3
- import { ComponentType } from '#consts';
4
3
 
5
4
  const validateDefinitionMock = vi.hoisted( () => vi.fn() );
6
5
  const validateInputMock = vi.hoisted( () => vi.fn() );
7
6
  const validateOutputMock = vi.hoisted( () => vi.fn() );
8
7
  const validatorConstructorMock = vi.hoisted( () => vi.fn() );
8
+ const createStepMock = vi.hoisted( () => vi.fn( ( { handler } ) => handler ) );
9
9
 
10
10
  vi.mock( './validations/index.js', () => {
11
11
  class StepValidator {
@@ -23,12 +23,16 @@ vi.mock( './validations/index.js', () => {
23
23
  return { StepValidator };
24
24
  } );
25
25
 
26
+ vi.mock( '#helpers/component', () => ( {
27
+ createStep: createStepMock
28
+ } ) );
29
+
26
30
  describe( 'step()', () => {
27
31
  beforeEach( () => {
28
32
  vi.clearAllMocks();
29
33
  } );
30
34
 
31
- it( 'validates the definition, creates a runtime validator, and attaches metadata', async () => {
35
+ it( 'validates the definition, creates a runtime validator, and creates a step component', async () => {
32
36
  const { step } = await import( './step.js' );
33
37
  const inputSchema = { safeParse: vi.fn() };
34
38
  const outputSchema = { safeParse: vi.fn() };
@@ -58,15 +62,15 @@ describe( 'step()', () => {
58
62
  outputSchema
59
63
  } );
60
64
 
61
- const [ metadataSymbol ] = Object.getOwnPropertySymbols( wrapper );
62
- expect( wrapper[metadataSymbol] ).toEqual( {
65
+ expect( createStepMock ).toHaveBeenCalledWith( {
63
66
  name: 'test_step',
64
67
  description: 'Test step',
65
68
  inputSchema,
66
69
  outputSchema,
67
- type: ComponentType.STEP,
68
- options
70
+ options,
71
+ handler: expect.any( Function )
69
72
  } );
73
+ expect( wrapper ).toBe( createStepMock.mock.calls[0][0].handler );
70
74
  } );
71
75
 
72
76
  it( 'validates input and output around the step function', async () => {
@@ -1,10 +1,29 @@
1
- import type { SerializedFetchResponse } from '../utils/index.d.ts';
2
-
3
1
  /**
4
2
  * Allowed HTTP methods for request helpers.
5
3
  */
6
4
  export type HttpMethod = 'HEAD' | 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
7
5
 
6
+ /** Represents a {Response} serialized to plain object */
7
+ export type SerializedFetchResponse = {
8
+ /** The response url */
9
+ url: string,
10
+
11
+ /** The response status code */
12
+ status: number,
13
+
14
+ /** The response status text */
15
+ statusText: string,
16
+
17
+ /** Flag indicating if the request succeeded */
18
+ ok: boolean,
19
+
20
+ /** Object with response headers */
21
+ headers: Record<string, string>,
22
+
23
+ /** Response body, either JSON, text, or an arrayBuffer converted to base64 */
24
+ body: object | string
25
+ };
26
+
8
27
  /**
9
28
  * Send an POST HTTP request to a URL, optionally with a payload, then wait for a webhook response.
10
29
  *
@@ -133,7 +133,7 @@ export type WorkflowFunction<
133
133
  > = InputSchema extends AnyZodSchema ?
134
134
  ( input: z.infer<InputSchema>, context: WorkflowContext<InputSchema, OutputSchema> ) =>
135
135
  Promise<OutputSchema extends AnyZodSchema ? z.infer<OutputSchema> : void> :
136
- ( input?: undefined | null, context: WorkflowContext<InputSchema, OutputSchema> ) =>
136
+ ( input: undefined | null, context: WorkflowContext<InputSchema, OutputSchema> ) =>
137
137
  Promise<OutputSchema extends AnyZodSchema ? z.infer<OutputSchema> : void>;
138
138
 
139
139
  /**
@@ -149,7 +149,7 @@ export type WorkflowFunction<
149
149
  * @param options - Additional options for the invocation.
150
150
  * @returns A value matching the schema defined by `outputSchema`.
151
151
  */
152
- export type WorkflowFunctionWrapper<WorkflowFunction> =
152
+ export type WorkflowFunctionWrapper<WorkflowFunction extends ( ...args: any ) => any> = // eslint-disable-line @typescript-eslint/no-explicit-any
153
153
  [Parameters<WorkflowFunction>[0]] extends [undefined | null] ?
154
154
  ( input?: undefined | null, options?: WorkflowInvocationOptions ) =>
155
155
  ReturnType<WorkflowFunction> :
@@ -1,11 +1,12 @@
1
1
  // THIS RUNS IN THE TEMPORAL'S SANDBOX ENVIRONMENT
2
2
  import { proxyActivities, inWorkflowContext, executeChild, workflowInfo, uuid4, ParentClosePolicy } from '@temporalio/workflow';
3
3
  import { WorkflowValidator } from './validations/index.js';
4
- import { deepMerge, toUrlSafeBase64 } from '#utils';
5
- import { WorkflowContext } from '#internal_utils/workflow_context';
6
- import { setMetadata } from '#internal_utils/component';
7
- import { TraceInfo } from '#internal_utils/trace_info';
4
+ import { toUrlSafeBase64 } from '#helpers/string';
5
+ import { WorkflowContext } from '#helpers/workflow_context';
6
+ import { TraceInfo } from '#helpers/trace_info';
7
+ import { deepMerge } from '#helpers/object';
8
8
  import { defaultOptions } from './workflow_activity_options.js';
9
+ import { createWorkflow } from '#helpers/component';
9
10
  import {
10
11
  ACTIVITY_WRAPPER_VERSION_FIELD,
11
12
  ACTIVITY_GET_TRACE_DESTINATIONS,
@@ -35,84 +36,89 @@ export function workflow( { name, description, inputSchema, outputSchema, fn, op
35
36
  const { disableTrace, activityOptions } = deepMerge( defaultOptions, options );
36
37
  const validator = new WorkflowValidator( { name, inputSchema, outputSchema } );
37
38
 
38
- const wrapper = async ( input, extra = {} ) => {
39
- validator.validateInvocationOptions( extra );
40
-
41
- // this returns a plain function, for example, in unit tests
42
- if ( !inWorkflowContext() ) {
43
- validator.validateInput( input );
44
- const output = await fn( input, deepMerge( WorkflowContext.build(), extra?.context ) );
45
- validator.validateOutput( output );
46
- return output;
47
- }
39
+ return createWorkflow( {
40
+ name,
41
+ description,
42
+ inputSchema,
43
+ outputSchema,
44
+ options,
45
+ aliases,
46
+ handler: async ( input, extra = {} ) => {
47
+ validator.validateInvocationOptions( extra );
48
+
49
+ // this returns a plain function, for example, in unit tests
50
+ if ( !inWorkflowContext() ) {
51
+ validator.validateInput( input );
52
+ const output = await fn( input, deepMerge( WorkflowContext.build(), extra?.context ) );
53
+ validator.validateOutput( output );
54
+ return output;
55
+ }
48
56
 
49
- const { workflowId, workflowType, memo, root } = workflowInfo();
50
-
51
- // if the stack already includes this workflowId, means the workflow() function was called
52
- // from within a running workflow, meaning it is suppose to start a child workflow
53
- const isChild = Array.isArray( memo.stack ) ? memo.stack.includes( workflowId ) :
54
- checkChildFallback( { workflowType, aliases, name } );
55
-
56
- if ( isChild ) {
57
- const result = await executeChild( name, {
58
- args: undefined === input ? [] : [ input ],
59
- workflowId: `${workflowId}-${toUrlSafeBase64( uuid4() )}`,
60
- parentClosePolicy: ParentClosePolicy[extra?.detached ? 'ABANDON' : 'TERMINATE'],
61
- memo: {
62
- ...memo, // Preserve memo and mix activityOptions, if provided
63
- ...( extra?.activityOptions && {
64
- activityOptions: deepMerge( memo?.activityOptions ?? {}, extra?.activityOptions )
65
- } )
66
- }
67
- } );
68
- return result.output;
69
- }
57
+ const { workflowId, workflowType, memo, root } = workflowInfo();
58
+
59
+ // if the stack already includes this workflowId, means the workflow() function was called
60
+ // from within a running workflow, meaning it is suppose to start a child workflow
61
+ const isChild = Array.isArray( memo.stack ) ? memo.stack.includes( workflowId ) :
62
+ checkChildFallback( { workflowType, aliases, name } );
63
+
64
+ if ( isChild ) {
65
+ const result = await executeChild( name, {
66
+ args: undefined === input ? [] : [ input ],
67
+ workflowId: `${workflowId}-${toUrlSafeBase64( uuid4() )}`,
68
+ parentClosePolicy: ParentClosePolicy[extra?.detached ? 'ABANDON' : 'TERMINATE'],
69
+ memo: {
70
+ ...memo, // Preserve memo and mix activityOptions, if provided
71
+ ...( extra?.activityOptions && {
72
+ activityOptions: deepMerge( memo?.activityOptions ?? {}, extra?.activityOptions )
73
+ } )
74
+ }
75
+ } );
76
+ return result.output;
77
+ }
70
78
 
71
- const isRoot = !root;
79
+ const isRoot = !root;
72
80
 
73
- memo.stack = [ ...memo.stack ?? [], workflowId ];
74
- // Parent options have prevalence on nested calls, child will be overwritten
75
- memo.activityOptions = deepMerge( activityOptions, memo.activityOptions );
76
- // Trace info is only added in the root workflow
77
- if ( isRoot ) {
78
- memo.traceInfo = TraceInfo.build( { disableTrace } );
79
- }
81
+ memo.stack = [ ...memo.stack ?? [], workflowId ];
82
+ // Parent options have prevalence on nested calls, child will be overwritten
83
+ memo.activityOptions = deepMerge( activityOptions, memo.activityOptions );
84
+ // Trace info is only added in the root workflow
85
+ if ( isRoot ) {
86
+ memo.traceInfo = TraceInfo.build( { disableTrace } );
87
+ }
80
88
 
81
- const steps = proxyActivities( memo.activityOptions );
82
- const traceDest = isRoot && parseActivityOutput( await steps[ACTIVITY_GET_TRACE_DESTINATIONS]( memo.traceInfo ) );
83
-
84
- try {
85
- validator.validateInput( input );
86
-
87
- // Creates an activity caller based on a prefix
88
- const createCaller = prefix => async ( t, ...args ) => parseActivityOutput( await steps[`${prefix}#${t}`]( ...args ) );
89
-
90
- // This are functions used by the AST to replace direct activity (step/evaluator) calls
91
- const dispatchers = {
92
- invokeStep: createCaller( name ),
93
- invokeSharedStep: createCaller( SHARED_STEP_PREFIX ),
94
- invokeEvaluator: createCaller( name ),
95
- invokeSharedEvaluator: createCaller( SHARED_STEP_PREFIX )
96
- };
97
-
98
- // The workflow function execution with "this" set with the dispatchers
99
- const output = await fn.call( dispatchers, input, WorkflowContext.build() );
100
- validator.validateOutput( output );
101
-
102
- return {
103
- [WORKFLOW_WRAPPER_VERSION_FIELD]: 1,
104
- output,
105
- ...( traceDest && { trace: { destinations: traceDest } } )
106
- };
107
- } catch ( error ) {
108
- if ( isRoot && traceDest ) {
109
- // Append the trace destinations so it is carried to interceptor
110
- error[METADATA_ACCESS_SYMBOL] = { trace: { destinations: traceDest } };
89
+ const steps = proxyActivities( memo.activityOptions );
90
+ const traceDest = isRoot && parseActivityOutput( await steps[ACTIVITY_GET_TRACE_DESTINATIONS]( memo.traceInfo ) );
91
+
92
+ try {
93
+ validator.validateInput( input );
94
+
95
+ // Creates an activity caller based on a prefix
96
+ const createCaller = prefix => async ( t, ...args ) => parseActivityOutput( await steps[`${prefix}#${t}`]( ...args ) );
97
+
98
+ // This are functions used by the AST to replace direct activity (step/evaluator) calls
99
+ const dispatchers = {
100
+ invokeStep: createCaller( name ),
101
+ invokeSharedStep: createCaller( SHARED_STEP_PREFIX ),
102
+ invokeEvaluator: createCaller( name ),
103
+ invokeSharedEvaluator: createCaller( SHARED_STEP_PREFIX )
104
+ };
105
+
106
+ // The workflow function execution with "this" set with the dispatchers
107
+ const output = await fn.call( dispatchers, input, WorkflowContext.build() );
108
+ validator.validateOutput( output );
109
+
110
+ return {
111
+ [WORKFLOW_WRAPPER_VERSION_FIELD]: 1,
112
+ output,
113
+ ...( traceDest && { trace: { destinations: traceDest } } )
114
+ };
115
+ } catch ( error ) {
116
+ if ( isRoot && traceDest ) {
117
+ // Append the trace destinations so it is carried to interceptor
118
+ error[METADATA_ACCESS_SYMBOL] = { trace: { destinations: traceDest } };
119
+ }
120
+ throw error;
111
121
  }
112
- throw error;
113
122
  }
114
- };
115
-
116
- setMetadata( wrapper, { name, description, inputSchema, outputSchema, aliases } );
117
- return wrapper;
118
- };
123
+ } );
124
+ }
@@ -19,6 +19,7 @@ const validateInputMock = vi.hoisted( () => vi.fn() );
19
19
  const validateOutputMock = vi.hoisted( () => vi.fn() );
20
20
  const validateInvocationOptionsMock = vi.hoisted( () => vi.fn() );
21
21
  const validatorConstructorMock = vi.hoisted( () => vi.fn() );
22
+ const createWorkflowMock = vi.hoisted( () => vi.fn( ( { handler } ) => handler ) );
22
23
 
23
24
  vi.mock( './validations/index.js', () => {
24
25
  class WorkflowValidator {
@@ -37,6 +38,10 @@ vi.mock( './validations/index.js', () => {
37
38
  return { WorkflowValidator };
38
39
  } );
39
40
 
41
+ vi.mock( '#helpers/component', () => ( {
42
+ createWorkflow: createWorkflowMock
43
+ } ) );
44
+
40
45
  vi.mock( '@temporalio/workflow', async importOriginal => {
41
46
  const actual = await importOriginal();
42
47
  return {
@@ -151,7 +156,7 @@ describe( 'workflow()', () => {
151
156
  expect( () => workflow( workflowDefinition( { name: 'invalid_name' } ) ) ).toThrow( error );
152
157
  } );
153
158
 
154
- it( 'attaches workflow metadata to the wrapper', async () => {
159
+ it( 'creates a workflow component with definition metadata', async () => {
155
160
  const { workflow } = await import( './workflow.js' );
156
161
  const inputSchema = z.object( { value: z.string() } );
157
162
  const outputSchema = z.object( { ok: z.boolean() } );
@@ -165,14 +170,16 @@ describe( 'workflow()', () => {
165
170
  fn: async () => ( { ok: true } )
166
171
  } ) );
167
172
 
168
- const [ metadataSymbol ] = Object.getOwnPropertySymbols( wf );
169
- expect( wf[metadataSymbol] ).toEqual( {
173
+ expect( createWorkflowMock ).toHaveBeenCalledWith( {
170
174
  name: 'metadata_wf',
171
175
  description: 'Metadata workflow',
172
176
  inputSchema,
173
177
  outputSchema,
174
- aliases: [ 'metadata_alias' ]
178
+ options: {},
179
+ aliases: [ 'metadata_alias' ],
180
+ handler: expect.any( Function )
175
181
  } );
182
+ expect( wf ).toBe( createWorkflowMock.mock.calls[0][0].handler );
176
183
  } );
177
184
 
178
185
  describe( 'outside Temporal workflow context', () => {
@@ -1,10 +1,10 @@
1
1
  import { FatalError } from '#errors';
2
2
  import { fetch } from 'undici';
3
- import { serializeFetchResponse, serializeBodyAndInferContentType } from '#utils';
4
- import { setMetadata } from '#internal_utils/component';
5
- import { ComponentType } from '#consts';
3
+ import { serializeFetchResponse, serializeBodyAndInferContentType } from '#helpers/fetch';
6
4
  import { createChildLogger } from '#logger';
7
5
  import { getDestinations } from '#tracing';
6
+ import { createInternalStep } from '#helpers/component';
7
+ import { ACTIVITY_GET_TRACE_DESTINATIONS, ACTIVITY_SEND_HTTP_REQUEST } from '#consts';
8
8
 
9
9
  const log = createChildLogger( 'HttpClient' );
10
10
 
@@ -20,41 +20,42 @@ const log = createChildLogger( 'HttpClient' );
20
20
  * @returns {object} The serialized HTTP response
21
21
  * @throws {FatalError}
22
22
  */
23
- export const sendHttpRequest = async ( { url, method, payload = undefined, headers = undefined, timeout = 30_000 } ) => {
24
- const args = {
25
- method,
26
- headers: new Headers( headers ?? {} ),
27
- signal: AbortSignal.timeout( timeout )
28
- };
23
+ export const sendHttpRequest = createInternalStep( {
24
+ name: ACTIVITY_SEND_HTTP_REQUEST,
25
+ handler: async ( { url, method, payload = undefined, headers = undefined, timeout = 30_000 } ) => {
26
+ const args = {
27
+ method,
28
+ headers: new Headers( headers ?? {} ),
29
+ signal: AbortSignal.timeout( timeout )
30
+ };
29
31
 
30
- const methodsWithBody = [ 'DELETE', 'PATCH', 'POST', 'PUT', 'OPTIONS' ];
31
- const hasBodyPayload = ![ undefined, null ].includes( payload );
32
- if ( methodsWithBody.includes( method ) && hasBodyPayload ) {
33
- const { body, contentType } = serializeBodyAndInferContentType( payload );
34
- if ( contentType && !args.headers.has( 'content-type' ) ) {
35
- args.headers.set( 'Content-Type', contentType );
36
- }
37
- Object.assign( args, { body } );
38
- };
39
-
40
- const response = await ( async () => {
41
- try {
42
- return await fetch( url, args );
43
- } catch ( e ) {
44
- throw new FatalError( `${method} ${url} ${e.cause ?? e.message}` );
45
- }
46
- } )();
32
+ const methodsWithBody = [ 'DELETE', 'PATCH', 'POST', 'PUT', 'OPTIONS' ];
33
+ const hasBodyPayload = ![ undefined, null ].includes( payload );
34
+ if ( methodsWithBody.includes( method ) && hasBodyPayload ) {
35
+ const { body, contentType } = serializeBodyAndInferContentType( payload );
36
+ if ( contentType && !args.headers.has( 'content-type' ) ) {
37
+ args.headers.set( 'Content-Type', contentType );
38
+ }
39
+ Object.assign( args, { body } );
40
+ };
47
41
 
48
- log.info( 'HTTP request completed', { url, method, status: response.status, statusText: response.statusText } );
42
+ const response = await ( async () => {
43
+ try {
44
+ return await fetch( url, args );
45
+ } catch ( e ) {
46
+ throw new FatalError( `${method} ${url} ${e.cause ?? e.message}` );
47
+ }
48
+ } )();
49
49
 
50
- if ( !response.ok ) {
51
- throw new FatalError( `${method} ${url} ${response.status}` );
52
- }
50
+ log.info( 'HTTP request completed', { url, method, status: response.status, statusText: response.statusText } );
53
51
 
54
- return serializeFetchResponse( response );
55
- };
52
+ if ( !response.ok ) {
53
+ throw new FatalError( `${method} ${url} ${response.status}` );
54
+ }
56
55
 
57
- setMetadata( sendHttpRequest, { type: ComponentType.INTERNAL_STEP } );
56
+ return serializeFetchResponse( response );
57
+ }
58
+ } );
58
59
 
59
60
  /**
60
61
  * Invokes a trace method that resolves all trace output paths based on the traceInfo
@@ -62,6 +63,7 @@ setMetadata( sendHttpRequest, { type: ComponentType.INTERNAL_STEP } );
62
63
  * @param {object} traceInfo
63
64
  * @returns {object} Information about enabled destinations
64
65
  */
65
- export const getTraceDestinations = traceInfo => getDestinations( traceInfo );
66
-
67
- setMetadata( getTraceDestinations, { type: ComponentType.INTERNAL_STEP } );
66
+ export const getTraceDestinations = createInternalStep( {
67
+ name: ACTIVITY_GET_TRACE_DESTINATIONS,
68
+ handler: traceInfo => getDestinations( traceInfo )
69
+ } );
@@ -1,10 +1,12 @@
1
1
  import { describe, it, expect, vi, beforeEach } from 'vitest';
2
2
  import { MockAgent, setGlobalDispatcher } from 'undici';
3
3
  import { FatalError } from '#errors';
4
- import { serializeBodyAndInferContentType, serializeFetchResponse } from '#utils';
4
+ import { ACTIVITY_GET_TRACE_DESTINATIONS, ACTIVITY_SEND_HTTP_REQUEST } from '#consts';
5
+ import { serializeBodyAndInferContentType, serializeFetchResponse } from '#helpers/fetch';
5
6
  import { getTraceDestinations, sendHttpRequest } from './index.js';
6
7
 
7
8
  const getDestinationsMock = vi.hoisted( () => vi.fn() );
9
+ const createInternalStepMock = vi.hoisted( () => vi.fn( ( { handler } ) => handler ) );
8
10
 
9
11
  vi.mock( '#tracing', () => ( {
10
12
  getDestinations: getDestinationsMock
@@ -15,9 +17,15 @@ vi.mock( '#logger', () => {
15
17
  return { createChildLogger: vi.fn( () => log ) };
16
18
  } );
17
19
 
18
- vi.mock( '#utils', () => ( {
19
- setMetadata: vi.fn(),
20
- isStringboolTrue: vi.fn( () => false ),
20
+ vi.mock( '#helpers/component', () => ( {
21
+ createInternalStep: createInternalStepMock
22
+ } ) );
23
+
24
+ vi.mock( '#helpers/string', () => ( {
25
+ isStringboolTrue: vi.fn( () => false )
26
+ } ) );
27
+
28
+ vi.mock( '#helpers/fetch', () => ( {
21
29
  serializeBodyAndInferContentType: vi.fn(),
22
30
  serializeFetchResponse: vi.fn()
23
31
  } ) );
@@ -30,6 +38,21 @@ setGlobalDispatcher( mockAgent );
30
38
  const url = 'https://growthx.ai';
31
39
  const method = 'GET';
32
40
 
41
+ describe( 'internal_activities component registration', () => {
42
+ it( 'creates internal step components for exported activities', () => {
43
+ expect( createInternalStepMock ).toHaveBeenNthCalledWith( 1, {
44
+ name: ACTIVITY_SEND_HTTP_REQUEST,
45
+ handler: expect.any( Function )
46
+ } );
47
+ expect( createInternalStepMock ).toHaveBeenNthCalledWith( 2, {
48
+ name: ACTIVITY_GET_TRACE_DESTINATIONS,
49
+ handler: expect.any( Function )
50
+ } );
51
+ expect( sendHttpRequest ).toBe( createInternalStepMock.mock.calls[0][0].handler );
52
+ expect( getTraceDestinations ).toBe( createInternalStepMock.mock.calls[1][0].handler );
53
+ } );
54
+ } );
55
+
33
56
  describe( 'internal_activities/sendHttpRequest', () => {
34
57
  beforeEach( async () => {
35
58
  vi.restoreAllMocks();
@@ -1,5 +1,5 @@
1
1
  import { format, transports } from 'winston';
2
- import { isPlainObject, shuffleArray } from '#utils';
2
+ import { shuffleArray, isPlainObject } from '#helpers/object';
3
3
 
4
4
  /** Available colors enum */
5
5
  const Color = {
@@ -3,7 +3,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest';
3
3
  const LEVEL = Symbol.for( 'level' );
4
4
  const MESSAGE = Symbol.for( 'message' );
5
5
 
6
- vi.mock( '#utils', () => ( {
6
+ vi.mock( '#helpers/object', () => ( {
7
7
  isPlainObject: v => Object.prototype.toString.call( v ) === '[object Object]',
8
8
  shuffleArray: v => v
9
9
  } ) );
@@ -9,3 +9,4 @@
9
9
  */
10
10
  export * from './component_metadata.js';
11
11
  export * from './path.js';
12
+ export * from './objects.js';
@@ -1,2 +1,3 @@
1
1
  export * from './component_metadata.js';
2
2
  export * from './path.js';
3
+ export * from './objects.js';
@@ -0,0 +1,51 @@
1
+ /** Tools to manipulate JS Objects */
2
+ export declare const Objects: {
3
+ /**
4
+ * Node safe clone implementation that doesn't use global structuredClone().
5
+ *
6
+ * Returns a cloned version of the object.
7
+ *
8
+ * Only clones static properties. Getters become static properties.
9
+ *
10
+ * @param object
11
+ */
12
+ clone( object: object ): object,
13
+
14
+ /**
15
+ * Returns true if the value is a plain object:
16
+ * - `{}`
17
+ * - `new Object()`
18
+ * - `Object.create(null)`
19
+ *
20
+ * @param object - The value to check.
21
+ * @returns Whether the value is a plain object.
22
+ */
23
+ isPlainObject( object: unknown ): boolean,
24
+
25
+ /**
26
+ * Creates a new object by merging object `b` onto object `a`, biased toward `b`:
27
+ * - Fields in `b` overwrite fields in `a`.
28
+ * - Fields in `b` that don't exist in `a` are created.
29
+ * - Fields in `a` that don't exist in `b` are left unchanged.
30
+ *
31
+ * @param a - The base object.
32
+ * @param b - The overriding object.
33
+ * @throws {Error} If either `a` or `b` is not a plain object.
34
+ * @returns A new merged object.
35
+ */
36
+ deepMerge( a: object, b: object | null | undefined ): object,
37
+
38
+ /**
39
+ * Creates a new object by merging object `b` onto object `a`, biased toward `b`:
40
+ * - Fields in `b` that don't exist in `a` are created.
41
+ * - Fields in `a` that don't exist in `b` are left unchanged.
42
+ * - Fields in `a` and `b` are passed as arguments to the resolve function (a,b) and its return assigns the new value.
43
+ *
44
+ * @param a - The base object.
45
+ * @param b - The overriding object.
46
+ * @param resolver - The resolver function.
47
+ * @throws {Error} If either `a` or `b` is not a plain object.
48
+ * @returns A new merged object.
49
+ */
50
+ deepMergeWithResolver( a: object, b: object | null | undefined, resolver: ( a: unknown, b: unknown ) => unknown ): object
51
+ };
@@ -0,0 +1,8 @@
1
+ import { clone, deepMerge, deepMergeWithResolver, isPlainObject } from '#helpers/object';
2
+
3
+ export const Objects = {
4
+ clone,
5
+ deepMerge,
6
+ deepMergeWithResolver,
7
+ isPlainObject
8
+ };
@@ -0,0 +1,16 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { Objects } from './objects.js';
3
+ import { Objects as ObjectsFromIndex } from './index.js';
4
+ import { clone, deepMerge, deepMergeWithResolver, isPlainObject } from '#helpers/object';
5
+
6
+ describe( 'Objects', () => {
7
+ it( 'exports the same functions from the object helper module', () => {
8
+ expect( Objects ).toBe( ObjectsFromIndex );
9
+ expect( Objects ).toEqual( {
10
+ clone,
11
+ deepMerge,
12
+ deepMergeWithResolver,
13
+ isPlainObject
14
+ } );
15
+ } );
16
+ } );
@@ -1,11 +1,5 @@
1
1
  import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
2
 
3
- vi.mock( '#utils', () => ( {
4
- throws: e => {
5
- throw e;
6
- }
7
- } ) );
8
-
9
3
  const logCalls = { warn: [], error: [] };
10
4
  vi.mock( '#logger', () => ( {
11
5
  createChildLogger: () => ( {
@@ -1,11 +1,5 @@
1
1
  import { describe, it, expect, vi, beforeEach } from 'vitest';
2
2
 
3
- vi.mock( '#utils', () => ( {
4
- throws: e => {
5
- throw e;
6
- }
7
- } ) );
8
-
9
3
  const getVarsMock = vi.fn();
10
4
  vi.mock( './configs', () => ( { getVars: () => getVarsMock() } ) );
11
5