@output.ai/core 0.0.10 → 0.0.12

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/README.md CHANGED
@@ -138,4 +138,4 @@ Necessary env variables to run the worker locally:
138
138
  - `TEMPORAL_API_KEY`: The API key to access remote temporal. If using local temporal, leave it blank;
139
139
  - `CATALOG_ID`: The name of the local catalog, always set this. Use your email;
140
140
  - `API_AUTH_KEY`: The API key to access the Framework API. Local can be blank, remote use the proper API Key;
141
- - `TRACING_ENABLED`: A "stringbool" value indicating if traces should be generated or note;
141
+ - `TRACING_ENABLED`: A "stringbool" value indicating if traces should be generated or not;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@output.ai/core",
3
- "version": "0.0.10",
3
+ "version": "0.0.12",
4
4
  "description": "The core module of the output framework",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/configs.js CHANGED
@@ -1,22 +1,19 @@
1
1
  import * as z from 'zod';
2
2
 
3
- class InvalidEnvVarsErrors extends Error {}
3
+ class InvalidEnvVarsErrors extends Error { }
4
4
 
5
5
  const envVarSchema = z.object( {
6
- TEMPORAL_ADDRESS: z.union( [
7
- z.httpUrl(),
8
- z.string().regex( /^[a-z0-9_-]+:\d{2,5}$/i ) // local docker container name like worker:7233
9
- ] ),
6
+ TEMPORAL_ADDRESS: z.string().optional().default( 'localhost:7233' ),
10
7
  TEMPORAL_NAMESPACE: z.string().optional().default( 'default' ),
11
8
  TEMPORAL_API_KEY: z.string().optional(),
12
9
  CATALOG_ID: z.string().regex( /^[a-z0-9_.@-]+$/i ),
13
10
  API_AUTH_KEY: z.string().optional(),
14
- TRACING_ENABLED: z.stringbool().optional()
11
+ TRACING_ENABLED: z.enum( [ 'true', 'false' ] ).optional()
15
12
  } );
16
13
 
17
14
  const { data: safeEnvVar, error } = envVarSchema.safeParse( process.env );
18
15
  if ( error ) {
19
- throw new InvalidEnvVarsErrors( z.prettifyError( error ) );
16
+ throw new InvalidEnvVarsErrors( JSON.stringify( error, null, 2 ) );
20
17
  }
21
18
 
22
19
  export const worker = {
@@ -0,0 +1,379 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+
3
+ describe( 'configs', () => {
4
+ const originalEnv = process.env;
5
+
6
+ beforeEach( () => {
7
+ vi.resetModules();
8
+ process.env = { ...originalEnv };
9
+ } );
10
+
11
+ afterEach( () => {
12
+ process.env = originalEnv;
13
+ } );
14
+
15
+ describe( 'Environment Variable Validation', () => {
16
+ describe( 'TEMPORAL_ADDRESS', () => {
17
+ it( 'should accept string addresses', async () => {
18
+ process.env = {
19
+ TEMPORAL_ADDRESS: 'localhost:7233',
20
+ CATALOG_ID: 'test-catalog'
21
+ };
22
+
23
+ const { worker } = await import( './configs.js' );
24
+ expect( worker.address ).toBe( 'localhost:7233' );
25
+ } );
26
+
27
+ it( 'should use default when omitted', async () => {
28
+ process.env = {
29
+ CATALOG_ID: 'test-catalog'
30
+ };
31
+
32
+ const { worker } = await import( './configs.js' );
33
+ expect( worker.address ).toBe( 'localhost:7233' );
34
+ } );
35
+
36
+ it( 'should reject non-string values', async () => {
37
+ process.env = {
38
+ TEMPORAL_ADDRESS: '123',
39
+ CATALOG_ID: 'test-catalog'
40
+ };
41
+
42
+ // Convert to number to test non-string
43
+ const originalEnv = process.env.TEMPORAL_ADDRESS;
44
+ process.env.TEMPORAL_ADDRESS = 123;
45
+
46
+ await expect( import( './configs.js' ) ).rejects.toThrow();
47
+
48
+ process.env.TEMPORAL_ADDRESS = originalEnv;
49
+ } );
50
+ } );
51
+
52
+ describe( 'CATALOG_ID', () => {
53
+ it( 'should accept valid catalog IDs with letters and numbers', async () => {
54
+ process.env = {
55
+ TEMPORAL_ADDRESS: 'http://localhost:7233',
56
+ CATALOG_ID: 'catalog123'
57
+ };
58
+
59
+ const { worker } = await import( './configs.js' );
60
+ expect( worker.catalogId ).toBe( 'catalog123' );
61
+ expect( worker.taskQueue ).toBe( 'catalog123' );
62
+ } );
63
+
64
+ it( 'should accept catalog IDs with dots and hyphens', async () => {
65
+ process.env = {
66
+ TEMPORAL_ADDRESS: 'http://localhost:7233',
67
+ CATALOG_ID: 'my.catalog-id'
68
+ };
69
+
70
+ const { worker } = await import( './configs.js' );
71
+ expect( worker.catalogId ).toBe( 'my.catalog-id' );
72
+ } );
73
+
74
+ it( 'should accept catalog IDs with underscores', async () => {
75
+ process.env = {
76
+ TEMPORAL_ADDRESS: 'http://localhost:7233',
77
+ CATALOG_ID: 'my_catalog_id'
78
+ };
79
+
80
+ const { worker } = await import( './configs.js' );
81
+ expect( worker.catalogId ).toBe( 'my_catalog_id' );
82
+ } );
83
+
84
+ it( 'should accept catalog IDs with @ symbol', async () => {
85
+ process.env = {
86
+ TEMPORAL_ADDRESS: 'http://localhost:7233',
87
+ CATALOG_ID: '@my-catalog'
88
+ };
89
+
90
+ const { worker } = await import( './configs.js' );
91
+ expect( worker.catalogId ).toBe( '@my-catalog' );
92
+ } );
93
+
94
+ it( 'should reject catalog IDs with special characters', async () => {
95
+ process.env = {
96
+ TEMPORAL_ADDRESS: 'http://localhost:7233',
97
+ CATALOG_ID: 'catalog!@#$'
98
+ };
99
+
100
+ await expect( import( './configs.js' ) ).rejects.toThrow();
101
+ } );
102
+
103
+ it( 'should reject when CATALOG_ID is missing', async () => {
104
+ process.env = {
105
+ TEMPORAL_ADDRESS: 'http://localhost:7233'
106
+ };
107
+
108
+ await expect( import( './configs.js' ) ).rejects.toThrow();
109
+ } );
110
+
111
+ it( 'should reject empty CATALOG_ID', async () => {
112
+ process.env = {
113
+ TEMPORAL_ADDRESS: 'http://localhost:7233',
114
+ CATALOG_ID: ''
115
+ };
116
+
117
+ await expect( import( './configs.js' ) ).rejects.toThrow();
118
+ } );
119
+ } );
120
+
121
+ describe( 'Optional Fields', () => {
122
+ it( 'should use default namespace when TEMPORAL_NAMESPACE is not provided', async () => {
123
+ process.env = {
124
+ TEMPORAL_ADDRESS: 'http://localhost:7233',
125
+ CATALOG_ID: 'test-catalog'
126
+ };
127
+
128
+ const { worker } = await import( './configs.js' );
129
+ expect( worker.namespace ).toBe( 'default' );
130
+ } );
131
+
132
+ it( 'should use custom namespace when provided', async () => {
133
+ process.env = {
134
+ TEMPORAL_ADDRESS: 'http://localhost:7233',
135
+ CATALOG_ID: 'test-catalog',
136
+ TEMPORAL_NAMESPACE: 'custom-namespace'
137
+ };
138
+
139
+ const { worker } = await import( './configs.js' );
140
+ expect( worker.namespace ).toBe( 'custom-namespace' );
141
+ } );
142
+
143
+ it( 'should handle TEMPORAL_API_KEY when provided', async () => {
144
+ process.env = {
145
+ TEMPORAL_ADDRESS: 'http://localhost:7233',
146
+ CATALOG_ID: 'test-catalog',
147
+ TEMPORAL_API_KEY: 'secret-api-key'
148
+ };
149
+
150
+ const { worker } = await import( './configs.js' );
151
+ expect( worker.apiKey ).toBe( 'secret-api-key' );
152
+ } );
153
+
154
+ it( 'should handle missing TEMPORAL_API_KEY', async () => {
155
+ process.env = {
156
+ TEMPORAL_ADDRESS: 'http://localhost:7233',
157
+ CATALOG_ID: 'test-catalog'
158
+ };
159
+
160
+ const { worker } = await import( './configs.js' );
161
+ expect( worker.apiKey ).toBeUndefined();
162
+ } );
163
+
164
+ it( 'should handle API_AUTH_KEY when provided', async () => {
165
+ process.env = {
166
+ TEMPORAL_ADDRESS: 'http://localhost:7233',
167
+ CATALOG_ID: 'test-catalog',
168
+ API_AUTH_KEY: 'api-secret-key'
169
+ };
170
+
171
+ const { api } = await import( './configs.js' );
172
+ expect( api.authKey ).toBe( 'api-secret-key' );
173
+ } );
174
+
175
+ it( 'should handle missing API_AUTH_KEY', async () => {
176
+ process.env = {
177
+ TEMPORAL_ADDRESS: 'http://localhost:7233',
178
+ CATALOG_ID: 'test-catalog'
179
+ };
180
+
181
+ const { api } = await import( './configs.js' );
182
+ expect( api.authKey ).toBeUndefined();
183
+ } );
184
+
185
+ it( 'should handle TRACING_ENABLED when true', async () => {
186
+ process.env = {
187
+ TEMPORAL_ADDRESS: 'http://localhost:7233',
188
+ CATALOG_ID: 'test-catalog',
189
+ TRACING_ENABLED: 'true'
190
+ };
191
+
192
+ const { tracing } = await import( './configs.js' );
193
+ expect( tracing.enabled ).toBe( 'true' );
194
+ } );
195
+
196
+ it( 'should handle TRACING_ENABLED when false', async () => {
197
+ process.env = {
198
+ TEMPORAL_ADDRESS: 'http://localhost:7233',
199
+ CATALOG_ID: 'test-catalog',
200
+ TRACING_ENABLED: 'false'
201
+ };
202
+
203
+ const { tracing } = await import( './configs.js' );
204
+ expect( tracing.enabled ).toBe( 'false' );
205
+ } );
206
+
207
+ it( 'should handle missing TRACING_ENABLED', async () => {
208
+ process.env = {
209
+ TEMPORAL_ADDRESS: 'http://localhost:7233',
210
+ CATALOG_ID: 'test-catalog'
211
+ };
212
+
213
+ const { tracing } = await import( './configs.js' );
214
+ expect( tracing.enabled ).toBeUndefined();
215
+ } );
216
+ } );
217
+ } );
218
+
219
+ describe( 'Exported Config Objects', () => {
220
+ it( 'should export worker config with all properties', async () => {
221
+ process.env = {
222
+ TEMPORAL_ADDRESS: 'http://localhost:7233',
223
+ CATALOG_ID: 'test-catalog',
224
+ TEMPORAL_NAMESPACE: 'test-namespace',
225
+ TEMPORAL_API_KEY: 'test-api-key'
226
+ };
227
+
228
+ const { worker } = await import( './configs.js' );
229
+
230
+ expect( worker ).toEqual( {
231
+ address: 'http://localhost:7233',
232
+ apiKey: 'test-api-key',
233
+ executionTimeout: '1m',
234
+ maxActivities: 100,
235
+ maxWorkflows: 100,
236
+ namespace: 'test-namespace',
237
+ taskQueue: 'test-catalog',
238
+ catalogId: 'test-catalog'
239
+ } );
240
+ } );
241
+
242
+ it( 'should export api config', async () => {
243
+ process.env = {
244
+ TEMPORAL_ADDRESS: 'http://localhost:7233',
245
+ CATALOG_ID: 'test-catalog',
246
+ API_AUTH_KEY: 'test-auth-key'
247
+ };
248
+
249
+ const { api } = await import( './configs.js' );
250
+
251
+ expect( api ).toEqual( {
252
+ authKey: 'test-auth-key'
253
+ } );
254
+ } );
255
+
256
+ it( 'should export tracing config', async () => {
257
+ process.env = {
258
+ TEMPORAL_ADDRESS: 'http://localhost:7233',
259
+ CATALOG_ID: 'test-catalog',
260
+ TRACING_ENABLED: 'true'
261
+ };
262
+
263
+ const { tracing } = await import( './configs.js' );
264
+
265
+ expect( tracing ).toEqual( {
266
+ enabled: 'true'
267
+ } );
268
+ } );
269
+
270
+ it( 'should have correct static worker config values', async () => {
271
+ process.env = {
272
+ TEMPORAL_ADDRESS: 'http://localhost:7233',
273
+ CATALOG_ID: 'test-catalog'
274
+ };
275
+
276
+ const { worker } = await import( './configs.js' );
277
+
278
+ expect( worker.executionTimeout ).toBe( '1m' );
279
+ expect( worker.maxActivities ).toBe( 100 );
280
+ expect( worker.maxWorkflows ).toBe( 100 );
281
+ } );
282
+ } );
283
+
284
+ describe( 'Error Handling', () => {
285
+ it( 'should throw InvalidEnvVarsErrors for invalid configuration', async () => {
286
+ process.env = {
287
+ TEMPORAL_ADDRESS: 'localhost:7233',
288
+ CATALOG_ID: 'invalid!@#'
289
+ };
290
+
291
+ await expect( import( './configs.js' ) ).rejects.toThrow();
292
+ } );
293
+
294
+ it( 'should handle multiple validation errors', async () => {
295
+ process.env = {
296
+ TEMPORAL_ADDRESS: 'localhost:7233',
297
+ CATALOG_ID: 'invalid!@#'
298
+ };
299
+
300
+ await expect( import( './configs.js' ) ).rejects.toThrow();
301
+ } );
302
+ } );
303
+
304
+ describe( 'Edge Cases', () => {
305
+ it( 'should handle environment variables with spaces', async () => {
306
+ process.env = {
307
+ TEMPORAL_ADDRESS: 'http://localhost:7233',
308
+ CATALOG_ID: 'test-catalog',
309
+ TEMPORAL_NAMESPACE: ' custom-namespace '
310
+ };
311
+
312
+ const { worker } = await import( './configs.js' );
313
+ expect( worker.namespace ).toBe( ' custom-namespace ' );
314
+ } );
315
+
316
+ it( 'should handle very long catalog IDs', async () => {
317
+ process.env = {
318
+ TEMPORAL_ADDRESS: 'http://localhost:7233',
319
+ CATALOG_ID: 'a'.repeat( 100 )
320
+ };
321
+
322
+ const { worker } = await import( './configs.js' );
323
+ expect( worker.catalogId ).toBe( 'a'.repeat( 100 ) );
324
+ } );
325
+
326
+ it( 'should handle URLs with ports outside typical range', async () => {
327
+ process.env = {
328
+ TEMPORAL_ADDRESS: 'http://localhost:65535',
329
+ CATALOG_ID: 'test-catalog'
330
+ };
331
+
332
+ const { worker } = await import( './configs.js' );
333
+ expect( worker.address ).toBe( 'http://localhost:65535' );
334
+ } );
335
+
336
+ it( 'should handle container names with numbers', async () => {
337
+ process.env = {
338
+ TEMPORAL_ADDRESS: 'temporal123:7233',
339
+ CATALOG_ID: 'test-catalog'
340
+ };
341
+
342
+ const { worker } = await import( './configs.js' );
343
+ expect( worker.address ).toBe( 'temporal123:7233' );
344
+ } );
345
+
346
+ it( 'should handle mixed case in catalog ID', async () => {
347
+ process.env = {
348
+ TEMPORAL_ADDRESS: 'http://localhost:7233',
349
+ CATALOG_ID: 'Test.Catalog-ID_123'
350
+ };
351
+
352
+ const { worker } = await import( './configs.js' );
353
+ expect( worker.catalogId ).toBe( 'Test.Catalog-ID_123' );
354
+ } );
355
+ } );
356
+
357
+ describe( 'Complete Valid Configuration', () => {
358
+ it( 'should handle a complete valid configuration with all optional fields', async () => {
359
+ process.env = {
360
+ TEMPORAL_ADDRESS: 'https://temporal.cloud.example.com',
361
+ TEMPORAL_NAMESPACE: 'production',
362
+ TEMPORAL_API_KEY: 'prod-api-key-123',
363
+ CATALOG_ID: 'prod.catalog@v1',
364
+ API_AUTH_KEY: 'secure-auth-key',
365
+ TRACING_ENABLED: 'true'
366
+ };
367
+
368
+ const { worker, api, tracing } = await import( './configs.js' );
369
+
370
+ expect( worker.address ).toBe( 'https://temporal.cloud.example.com' );
371
+ expect( worker.namespace ).toBe( 'production' );
372
+ expect( worker.apiKey ).toBe( 'prod-api-key-123' );
373
+ expect( worker.catalogId ).toBe( 'prod.catalog@v1' );
374
+ expect( worker.taskQueue ).toBe( 'prod.catalog@v1' );
375
+ expect( api.authKey ).toBe( 'secure-auth-key' );
376
+ expect( tracing.enabled ).toBe( 'true' );
377
+ } );
378
+ } );
379
+ } );
package/src/consts.js CHANGED
@@ -1,4 +1,11 @@
1
1
  export const SEND_WEBHOOK_ACTIVITY_NAME = '__internal#sendWebhookPost';
2
+ export const READ_TRACE_FILE = '__internal#readTraceFile';
2
3
  export const METADATA_ACCESS_SYMBOL = Symbol( '__metadata' );
3
4
  export const WORKFLOWS_INDEX_FILENAME = '__workflows_entrypoint.js';
4
5
  export const THIS_LIB_NAME = 'core';
6
+ export const TraceEvent = {
7
+ WORKFLOW_START: 'workflow_start',
8
+ WORKFLOW_END: 'workflow_end',
9
+ STEP_START: 'step_start',
10
+ STEP_END: 'step_end'
11
+ };
@@ -1,18 +1,18 @@
1
1
  import { setMetadata } from './metadata.js';
2
2
  import { validateStep } from './validations/static.js';
3
3
  import { validateStepInput, validateStepOutput } from './validations/runtime.js';
4
- import { invokeFnAndValidateOutputPreservingExecutionModel } from './utils.js';
5
4
 
6
5
  export function step( { name, description, inputSchema, outputSchema, fn } ) {
7
6
  validateStep( { name, description, inputSchema, outputSchema, fn } );
8
- const wrapper = input => {
7
+ const wrapper = async input => {
9
8
  if ( inputSchema ) {
10
9
  validateStepInput( name, inputSchema, input );
11
10
  }
12
- if ( !outputSchema ) {
13
- return fn( input );
11
+ const output = await fn( input );
12
+ if ( outputSchema ) {
13
+ validateStepOutput( name, outputSchema, output );
14
14
  }
15
- return invokeFnAndValidateOutputPreservingExecutionModel( fn, input, validateStepOutput.bind( null, name, outputSchema ) );
15
+ return output;
16
16
  };
17
17
 
18
18
  setMetadata( wrapper, { name, description, inputSchema, outputSchema } );
@@ -17,30 +17,3 @@ export const getInvocationDir = () => new Error()
17
17
  .at( -1 )
18
18
  .replace( /\((.+):\d+:\d+\)/, '$1' )
19
19
  .split( '/' ).slice( 0, -1 ).join( '/' );
20
-
21
- /**
22
- * This mouthful function will invoke a function with given arguments, and validate its return
23
- * using a given validator.
24
- *
25
- * It will preserver the execution model (asynchronous vs synchronous), so if the function is
26
- * sync the validation happens here, if it is async (returns Promise) the validation is attached
27
- * to a .then().
28
- *
29
- *
30
- * @param {Function} fn - The function to execute
31
- * @param {any} input - The payload to call the function
32
- * @param {Function} validate - The validator function
33
- * @returns {any} Function result (Promise or not)
34
- */
35
- export const invokeFnAndValidateOutputPreservingExecutionModel = ( fn, input, validate ) => {
36
- const uniformReturn = output => {
37
- validate( output );
38
- return output;
39
- };
40
-
41
- const output = fn( input );
42
- if ( output?.constructor === Promise ) {
43
- return output.then( resolvedOutput => uniformReturn( resolvedOutput ) );
44
- }
45
- return uniformReturn( output );
46
- };
@@ -1,5 +1,5 @@
1
- import { describe, it, expect, vi } from 'vitest';
2
- import { getInvocationDir, invokeFnAndValidateOutputPreservingExecutionModel } from './utils.js';
1
+ import { describe, it, expect } from 'vitest';
2
+ import { getInvocationDir } from './utils.js';
3
3
 
4
4
  describe( 'interface/utils', () => {
5
5
  describe( 'getInvocationDir', () => {
@@ -31,41 +31,5 @@ describe( 'interface/utils', () => {
31
31
  }
32
32
  } );
33
33
  } );
34
-
35
- describe( 'invokeFnAndValidateOutputPreservingExecutionModel', () => {
36
- it( 'validates and returns sync output', () => {
37
- const fn = vi.fn( x => x * 2 );
38
- const validate = vi.fn();
39
- const result = invokeFnAndValidateOutputPreservingExecutionModel( fn, 3, validate );
40
- expect( result ).toBe( 6 );
41
- expect( validate ).toHaveBeenCalledWith( 6 );
42
- } );
43
-
44
- it( 'validates and returns async output preserving promise', async () => {
45
- const fn = vi.fn( async x => x + 1 );
46
- const validate = vi.fn();
47
- const resultPromise = invokeFnAndValidateOutputPreservingExecutionModel( fn, 4, validate );
48
- expect( resultPromise ).toBeInstanceOf( Promise );
49
- const result = await resultPromise;
50
- expect( result ).toBe( 5 );
51
- expect( validate ).toHaveBeenCalledWith( 5 );
52
- } );
53
-
54
- it( 'propagates validator errors (sync)', () => {
55
- const fn = vi.fn( x => x );
56
- const validate = vi.fn( () => {
57
- throw new Error( 'invalid' );
58
- } );
59
- expect( () => invokeFnAndValidateOutputPreservingExecutionModel( fn, 'a', validate ) ).toThrow( 'invalid' );
60
- } );
61
-
62
- it( 'propagates validator errors (async)', async () => {
63
- const fn = vi.fn( async x => x );
64
- const validate = vi.fn( () => {
65
- throw new Error( 'invalid' );
66
- } );
67
- await expect( invokeFnAndValidateOutputPreservingExecutionModel( fn, 'a', validate ) ).rejects.toThrow( 'invalid' );
68
- } );
69
- } );
70
34
  } );
71
35
 
@@ -1,10 +1,11 @@
1
1
  // THIS RUNS IN THE TEMPORAL'S SANDBOX ENVIRONMENT
2
- import { proxyActivities, inWorkflowContext, executeChild, workflowInfo, ApplicationFailure } from '@temporalio/workflow';
3
- import { getInvocationDir, invokeFnAndValidateOutputPreservingExecutionModel } from './utils.js';
2
+ import { proxyActivities, inWorkflowContext, executeChild, workflowInfo, ApplicationFailure, proxySinks } from '@temporalio/workflow';
3
+ import { getInvocationDir } from './utils.js';
4
4
  import { setMetadata } from './metadata.js';
5
5
  import { FatalError, ValidationError } from '../errors.js';
6
6
  import { validateWorkflow } from './validations/static.js';
7
7
  import { validateWorkflowInput, validateWorkflowOutput } from './validations/runtime.js';
8
+ import { READ_TRACE_FILE, TraceEvent } from '#consts';
8
9
 
9
10
  const temporalActivityConfigs = {
10
11
  startToCloseTimeout: '20 minute',
@@ -22,44 +23,60 @@ export function workflow( { name, description, inputSchema, outputSchema, fn } )
22
23
  const workflowPath = getInvocationDir();
23
24
 
24
25
  const steps = proxyActivities( temporalActivityConfigs );
26
+ const sinks = proxySinks();
25
27
 
26
28
  const wrapper = async input => {
27
29
  try {
30
+ if ( inWorkflowContext() ) {
31
+ sinks.log.trace( { event: TraceEvent.WORKFLOW_START, input } );
32
+ }
33
+
28
34
  if ( inputSchema ) {
29
35
  validateWorkflowInput( name, inputSchema, input );
30
36
  }
31
37
 
32
38
  // this returns a plain function, for example, in unit tests
33
39
  if ( !inWorkflowContext() ) {
40
+ const output = await fn( input );
34
41
  if ( outputSchema ) {
35
- return invokeFnAndValidateOutputPreservingExecutionModel( fn, input, validateWorkflowOutput.bind( null, name, outputSchema ) );
42
+ validateWorkflowOutput( name, outputSchema, output );
36
43
  }
37
- return fn( input );
44
+ return output;
38
45
  }
39
46
 
47
+ const { memo, workflowId } = workflowInfo();
48
+
40
49
  Object.assign( workflowInfo().memo, { workflowPath } );
41
50
 
42
51
  // binds the methods called in the code that Webpack loader will add, they will exposed via "this"
43
- const boundFn = fn.bind( {
52
+ const output = await fn.call( {
44
53
  invokeStep: async ( stepName, input ) => steps[`${workflowPath}#${stepName}`]( input ),
45
54
 
46
- startWorkflow: async ( name, input ) => {
47
- const { memo, workflowId, workflowType } = workflowInfo();
55
+ startWorkflow: async ( childName, input ) => {
48
56
 
49
57
  // Checks if current memo has rootWorkflowId, which means current execution is already a child
50
58
  // Then it sets the memory for the child execution passing along who's the original workflow is and its type
51
59
  const workflowMemory = memo.rootWorkflowId ?
52
60
  { parentWorkflowId: workflowId, rootWorkflowType: memo.rootWorkflowType, rootWorkflowId: memo.rootWorkflowId } :
53
- { parentWorkflowId: workflowId, rootWorkflowId: workflowId, rootWorkflowType: workflowType };
61
+ { parentWorkflowId: workflowId, rootWorkflowId: workflowId, rootWorkflowType: name };
54
62
 
55
- return executeChild( name, { args: input ? [ input ] : [], memo: workflowMemory } );
63
+ return executeChild( childName, { args: input ? [ input ] : [], memo: workflowMemory } );
56
64
  }
57
- } );
65
+ }, input );
58
66
 
59
67
  if ( outputSchema ) {
60
- return invokeFnAndValidateOutputPreservingExecutionModel( boundFn, input, validateWorkflowOutput.bind( null, name, outputSchema ) );
68
+ validateWorkflowOutput( name, outputSchema, output );
61
69
  }
62
- return boundFn( input );
70
+
71
+ sinks.log.trace( { event: TraceEvent.WORKFLOW_END, output } );
72
+
73
+ // add trace if not child
74
+ if ( !memo.rootWorkflowId ) {
75
+ const trace = await steps[READ_TRACE_FILE]( { workflowType: name, workflowId } );
76
+ return { output, trace };
77
+ }
78
+
79
+ return output;
63
80
  } catch ( error ) {
64
81
  /*
65
82
  * Any errors in the workflow will interrupt its execution since the workflow is designed to orchestrate and
@@ -1,12 +1,23 @@
1
- import { api as apiConfig } from '#configs';
2
1
  import { FatalError } from '#errors';
2
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
3
+ import { join } from 'node:path';
3
4
 
5
+ const callerDir = process.argv[2];
6
+
7
+ /**
8
+ * Send a post to a given URL
9
+ *
10
+ * @param {object} options
11
+ * @param {string} options.url - The target url
12
+ * @param {string} options.workflowId - The current workflow id
13
+ * @param {any} options.payload - The payload to send url
14
+ * @throws {FatalError}
15
+ */
4
16
  export const sendWebhookPost = async ( { url, workflowId, payload } ) => {
5
17
  const request = fetch( url, {
6
18
  method: 'POST',
7
19
  headers: {
8
- 'Content-Type': 'application/json',
9
- Authentication: `Basic ${apiConfig.authKey}`
20
+ 'Content-Type': 'application/json'
10
21
  },
11
22
  body: JSON.stringify( { workflowId, payload } ),
12
23
  signal: AbortSignal.timeout( 5000 )
@@ -26,3 +37,19 @@ export const sendWebhookPost = async ( { url, workflowId, payload } ) => {
26
37
  throw new FatalError( `Webhook fail: ${res.status}` );
27
38
  }
28
39
  };
40
+
41
+ /**
42
+ * Read the trace file of a given execution and returns the content
43
+ *
44
+ * @param {object} options
45
+ * @param {string} options.workflowType - The type of the workflow
46
+ * @param {string} options.workflowId - The workflow execution id
47
+ * @returns {string[]} Each line of the trace file
48
+ */
49
+ export const readTraceFile = async ( { workflowType, workflowId } ) => {
50
+ const dir = join( callerDir, 'logs', 'runs', workflowType );
51
+ const suffix = `-${workflowId}.raw`;
52
+ const file = join( dir, readdirSync( dir ).find( f => f.endsWith( suffix ) ) );
53
+
54
+ return existsSync( file ) ? readFileSync( file, 'utf-8' ).split( '\n' ) : null;
55
+ };
@@ -1,9 +1,8 @@
1
1
  import { Context } from '@temporalio/activity';
2
2
  import { Storage } from '../async_storage.js';
3
3
  import { trace } from '../tracer/index.js';
4
- import { TraceEvent } from '../tracer/types.js';
5
4
  import { headersToObject } from '../sandboxed_utils.js';
6
- import { THIS_LIB_NAME } from '#consts';
5
+ import { THIS_LIB_NAME, TraceEvent } from '#consts';
7
6
 
8
7
  /*
9
8
  This interceptor is called for every activity execution
@@ -1,6 +1,5 @@
1
1
  // THIS RUNS IN THE TEMPORAL'S SANDBOX ENVIRONMENT
2
- import { proxySinks, workflowInfo } from '@temporalio/workflow';
3
- import { TraceEvent } from '../tracer/types.js';
2
+ import { workflowInfo } from '@temporalio/workflow';
4
3
  import { memoToHeaders } from '../sandboxed_utils.js';
5
4
 
6
5
  /*
@@ -19,24 +18,6 @@ class HeadersInjectionInterceptor {
19
18
  }
20
19
  };
21
20
 
22
- /*
23
- This interceptor captures the workflow execution start and stop to log these event for the internal tracing
24
- This is not an AI comment!
25
-
26
- It uses sinks to share them.
27
- - https://docs.temporal.io/develop/typescript/observability
28
- */
29
- class WorkflowExecutionInterceptor {
30
- async execute( input, next ) {
31
- const sinks = proxySinks();
32
- sinks.log.trace( { event: TraceEvent.WORKFLOW_START, input: input.args } );
33
- const output = await next( input );
34
- sinks.log.trace( { event: TraceEvent.WORKFLOW_END, output } );
35
- return output;
36
- }
37
- };
38
-
39
21
  export const interceptors = () => ( {
40
- outbound: [ new HeadersInjectionInterceptor( workflowInfo().workflowType ) ],
41
- inbound: [ new WorkflowExecutionInterceptor( workflowInfo().workflowType ) ]
22
+ outbound: [ new HeadersInjectionInterceptor( workflowInfo().workflowType ) ]
42
23
  } );
@@ -1,7 +1,7 @@
1
1
  import { dirname, join } from 'path';
2
2
  import { fileURLToPath } from 'url';
3
- import { sendWebhookPost } from '#internal_activities';
4
- import { SEND_WEBHOOK_ACTIVITY_NAME, WORKFLOWS_INDEX_FILENAME } from '#consts';
3
+ import { sendWebhookPost, readTraceFile } from '#internal_activities';
4
+ import { SEND_WEBHOOK_ACTIVITY_NAME, WORKFLOWS_INDEX_FILENAME, READ_TRACE_FILE } from '#consts';
5
5
  import {
6
6
  iteratorOverImportedComponents,
7
7
  recursiveNavigateWhileCollecting,
@@ -21,6 +21,7 @@ export async function loadActivities( path ) {
21
21
 
22
22
  // system activities
23
23
  activities[SEND_WEBHOOK_ACTIVITY_NAME] = sendWebhookPost;
24
+ activities[READ_TRACE_FILE] = readTraceFile;
24
25
  return activities;
25
26
  };
26
27
 
@@ -4,13 +4,16 @@ const METADATA_ACCESS_SYMBOL = Symbol( '__metadata' );
4
4
 
5
5
  vi.mock( '#consts', () => ( {
6
6
  SEND_WEBHOOK_ACTIVITY_NAME: '__internal#sendWebhookPost',
7
+ READ_TRACE_FILE: '__internal#readTraceFile',
7
8
  WORKFLOWS_INDEX_FILENAME: '__workflows_entrypoint.js',
8
9
  METADATA_ACCESS_SYMBOL
9
10
  } ) );
10
11
 
11
12
  const sendWebhookPostMock = vi.fn();
13
+ const readTraceFileMock = vi.fn();
12
14
  vi.mock( '#internal_activities', () => ( {
13
- sendWebhookPost: sendWebhookPostMock
15
+ sendWebhookPost: sendWebhookPostMock,
16
+ readTraceFile: readTraceFileMock
14
17
  } ) );
15
18
 
16
19
  // Mock internal_utils to control filesystem-independent behavior
@@ -39,6 +42,7 @@ describe( 'worker/loader', () => {
39
42
  const activities = await loadActivities( '/root' );
40
43
  expect( activities['/a#Act1'] ).toBeTypeOf( 'function' );
41
44
  expect( activities['__internal#sendWebhookPost'] ).toBe( sendWebhookPostMock );
45
+ expect( activities['__internal#readTraceFile'] ).toBe( readTraceFileMock );
42
46
  } );
43
47
 
44
48
  it( 'loadWorkflows returns array of workflows with metadata', async () => {
@@ -1,7 +1,6 @@
1
1
  import { readFileSync, writeFileSync } from 'node:fs';
2
2
  import { EOL } from 'os';
3
- import { TraceEvent } from './types.js';
4
- import { THIS_LIB_NAME } from '#consts';
3
+ import { THIS_LIB_NAME, TraceEvent } from '#consts';
5
4
 
6
5
  /**
7
6
  * Sorting function that compares two objects and ASC sort them by either .startedAt or, if not present, .timestamp
@@ -5,8 +5,7 @@ import { tmpdir } from 'node:os';
5
5
  import { join } from 'path';
6
6
  import { EOL } from 'os';
7
7
  import { buildLogTree } from './tracer_tree.js';
8
- import { TraceEvent } from './types.js';
9
- import { THIS_LIB_NAME } from '#consts';
8
+ import { THIS_LIB_NAME, TraceEvent } from '#consts';
10
9
 
11
10
  const createTempDir = () => mkdtempSync( join( tmpdir(), 'flow-sdk-trace-tree-' ) );
12
11
 
@@ -1,6 +0,0 @@
1
- export const TraceEvent = {
2
- WORKFLOW_START: 'workflow_start',
3
- WORKFLOW_END: 'workflow_end',
4
- STEP_START: 'step_start',
5
- STEP_END: 'step_end'
6
- };