@output.ai/core 0.0.10 → 0.0.11
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 +1 -1
- package/package.json +1 -1
- package/src/configs.js +43 -4
- package/src/configs.spec.js +480 -0
- package/src/consts.js +7 -0
- package/src/interface/step.js +5 -5
- package/src/interface/utils.js +0 -27
- package/src/interface/utils.spec.js +2 -38
- package/src/interface/workflow.js +29 -12
- package/src/internal_activities/index.js +30 -3
- package/src/worker/interceptors/activity.js +1 -2
- package/src/worker/interceptors/workflow.js +2 -21
- package/src/worker/loader.js +3 -2
- package/src/worker/loader.spec.js +5 -1
- package/src/worker/tracer/tracer_tree.js +1 -2
- package/src/worker/tracer/tracer_tree.test.js +1 -2
- package/src/worker/tracer/types.js +0 -6
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
|
|
141
|
+
- `TRACING_ENABLED`: A "stringbool" value indicating if traces should be generated or not;
|
package/package.json
CHANGED
package/src/configs.js
CHANGED
|
@@ -2,21 +2,60 @@ import * as z from 'zod';
|
|
|
2
2
|
|
|
3
3
|
class InvalidEnvVarsErrors extends Error {}
|
|
4
4
|
|
|
5
|
+
const ipv4Validator = z.ipv4();
|
|
6
|
+
const ipv6Validator = z.ipv6();
|
|
7
|
+
const portValidator = z.number().int().min( 10 ).max( 65535 ).optional();
|
|
8
|
+
|
|
9
|
+
const ipValidator = z.union( [ ipv4Validator, ipv6Validator ] );
|
|
10
|
+
|
|
11
|
+
const likelyIpv6 = val => val.startsWith( '[' );
|
|
12
|
+
|
|
13
|
+
const splitIpWithPort = val => {
|
|
14
|
+
// ipv6
|
|
15
|
+
if ( likelyIpv6( val ) ) {
|
|
16
|
+
const address = val.slice( 1, val.lastIndexOf( ']' ) );
|
|
17
|
+
const portString = val.split( ']' )[1].slice( 1 ); // remove the :
|
|
18
|
+
const port = portString ? parseInt( portString, 10 ) : undefined;
|
|
19
|
+
return { address, port };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ipv4
|
|
23
|
+
const parts = val.split( ':' );
|
|
24
|
+
if ( parts.length !== 2 ) {
|
|
25
|
+
return { address: val, port: undefined };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const [ address, portString ] = parts;
|
|
29
|
+
const port = portString ? parseInt( portString, 10 ) : undefined;
|
|
30
|
+
return { address, port };
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const ipWithOptionalPort = z.string().refine(
|
|
34
|
+
val => {
|
|
35
|
+
const { address, port } = splitIpWithPort( val );
|
|
36
|
+
return ipValidator.safeParse( address ).success && portValidator.safeParse( port ).success;
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
message: 'Must be a valid IP address'
|
|
40
|
+
}
|
|
41
|
+
);
|
|
42
|
+
|
|
5
43
|
const envVarSchema = z.object( {
|
|
6
44
|
TEMPORAL_ADDRESS: z.union( [
|
|
7
|
-
z.
|
|
8
|
-
z.string().regex( /^[a-z0-9_-]+:\d{2,5}$/i ) // local docker container name like worker:7233
|
|
45
|
+
z.string().regex( /^https?:\/\/.+/ ), // HTTP or HTTPS URLs
|
|
46
|
+
z.string().regex( /^[a-z0-9_-]+:\d{2,5}$/i ), // local docker container name like worker:7233
|
|
47
|
+
ipWithOptionalPort // IP address (IPv4 or IPv6) with port
|
|
9
48
|
] ),
|
|
10
49
|
TEMPORAL_NAMESPACE: z.string().optional().default( 'default' ),
|
|
11
50
|
TEMPORAL_API_KEY: z.string().optional(),
|
|
12
51
|
CATALOG_ID: z.string().regex( /^[a-z0-9_.@-]+$/i ),
|
|
13
52
|
API_AUTH_KEY: z.string().optional(),
|
|
14
|
-
TRACING_ENABLED: z.
|
|
53
|
+
TRACING_ENABLED: z.enum( [ 'true', 'false' ] ).optional()
|
|
15
54
|
} );
|
|
16
55
|
|
|
17
56
|
const { data: safeEnvVar, error } = envVarSchema.safeParse( process.env );
|
|
18
57
|
if ( error ) {
|
|
19
|
-
throw new InvalidEnvVarsErrors(
|
|
58
|
+
throw new InvalidEnvVarsErrors( JSON.stringify( error.format(), null, 2 ) );
|
|
20
59
|
}
|
|
21
60
|
|
|
22
61
|
export const worker = {
|
|
@@ -0,0 +1,480 @@
|
|
|
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 valid http URLs', async () => {
|
|
18
|
+
process.env = {
|
|
19
|
+
TEMPORAL_ADDRESS: 'http://localhost:7233',
|
|
20
|
+
CATALOG_ID: 'test-catalog'
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const { worker } = await import( './configs.js' );
|
|
24
|
+
expect( worker.address ).toBe( 'http://localhost:7233' );
|
|
25
|
+
} );
|
|
26
|
+
|
|
27
|
+
it( 'should accept valid https URLs', async () => {
|
|
28
|
+
process.env = {
|
|
29
|
+
TEMPORAL_ADDRESS: 'https://temporal.example.com',
|
|
30
|
+
CATALOG_ID: 'test-catalog'
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const { worker } = await import( './configs.js' );
|
|
34
|
+
expect( worker.address ).toBe( 'https://temporal.example.com' );
|
|
35
|
+
} );
|
|
36
|
+
|
|
37
|
+
it( 'should accept docker container names with ports', async () => {
|
|
38
|
+
process.env = {
|
|
39
|
+
TEMPORAL_ADDRESS: 'temporal-server:7233',
|
|
40
|
+
CATALOG_ID: 'test-catalog'
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const { worker } = await import( './configs.js' );
|
|
44
|
+
expect( worker.address ).toBe( 'temporal-server:7233' );
|
|
45
|
+
} );
|
|
46
|
+
|
|
47
|
+
it( 'should accept container names with underscores and hyphens', async () => {
|
|
48
|
+
process.env = {
|
|
49
|
+
TEMPORAL_ADDRESS: 'my_temporal-server:7233',
|
|
50
|
+
CATALOG_ID: 'test-catalog'
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const { worker } = await import( './configs.js' );
|
|
54
|
+
expect( worker.address ).toBe( 'my_temporal-server:7233' );
|
|
55
|
+
} );
|
|
56
|
+
|
|
57
|
+
it( 'should accept IP addresses with HTTP', async () => {
|
|
58
|
+
process.env = {
|
|
59
|
+
TEMPORAL_ADDRESS: 'http://192.168.1.100:7233',
|
|
60
|
+
CATALOG_ID: 'test-catalog'
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const { worker } = await import( './configs.js' );
|
|
64
|
+
expect( worker.address ).toBe( 'http://192.168.1.100:7233' );
|
|
65
|
+
} );
|
|
66
|
+
|
|
67
|
+
it( 'should accept IP addresses with HTTPS', async () => {
|
|
68
|
+
process.env = {
|
|
69
|
+
TEMPORAL_ADDRESS: 'https://10.0.0.1:7233',
|
|
70
|
+
CATALOG_ID: 'test-catalog'
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const { worker } = await import( './configs.js' );
|
|
74
|
+
expect( worker.address ).toBe( 'https://10.0.0.1:7233' );
|
|
75
|
+
} );
|
|
76
|
+
|
|
77
|
+
it( 'should accept IP addresses without protocol for docker format', async () => {
|
|
78
|
+
process.env = {
|
|
79
|
+
TEMPORAL_ADDRESS: '172.17.0.2:7233',
|
|
80
|
+
CATALOG_ID: 'test-catalog'
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const { worker } = await import( './configs.js' );
|
|
84
|
+
expect( worker.address ).toBe( '172.17.0.2:7233' );
|
|
85
|
+
} );
|
|
86
|
+
|
|
87
|
+
it( 'should accept IPv6 addresses with brackets and port', async () => {
|
|
88
|
+
process.env = {
|
|
89
|
+
TEMPORAL_ADDRESS: '[2001:db8::1]:7233',
|
|
90
|
+
CATALOG_ID: 'test-catalog'
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const { worker } = await import( './configs.js' );
|
|
94
|
+
expect( worker.address ).toBe( '[2001:db8::1]:7233' );
|
|
95
|
+
} );
|
|
96
|
+
|
|
97
|
+
it( 'should accept IPv6 addresses with HTTP', async () => {
|
|
98
|
+
process.env = {
|
|
99
|
+
TEMPORAL_ADDRESS: 'http://[2001:db8::1]:7233',
|
|
100
|
+
CATALOG_ID: 'test-catalog'
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const { worker } = await import( './configs.js' );
|
|
104
|
+
expect( worker.address ).toBe( 'http://[2001:db8::1]:7233' );
|
|
105
|
+
} );
|
|
106
|
+
|
|
107
|
+
it( 'should accept IPv6 addresses with HTTPS', async () => {
|
|
108
|
+
process.env = {
|
|
109
|
+
TEMPORAL_ADDRESS: 'https://[::1]:7233',
|
|
110
|
+
CATALOG_ID: 'test-catalog'
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const { worker } = await import( './configs.js' );
|
|
114
|
+
expect( worker.address ).toBe( 'https://[::1]:7233' );
|
|
115
|
+
} );
|
|
116
|
+
|
|
117
|
+
it( 'should reject invalid addresses', async () => {
|
|
118
|
+
process.env = {
|
|
119
|
+
TEMPORAL_ADDRESS: 'not-a-valid-address',
|
|
120
|
+
CATALOG_ID: 'test-catalog'
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
await expect( import( './configs.js' ) ).rejects.toThrow();
|
|
124
|
+
} );
|
|
125
|
+
|
|
126
|
+
it( 'should reject addresses without ports in container format', async () => {
|
|
127
|
+
process.env = {
|
|
128
|
+
TEMPORAL_ADDRESS: 'temporal-server',
|
|
129
|
+
CATALOG_ID: 'test-catalog'
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
await expect( import( './configs.js' ) ).rejects.toThrow();
|
|
133
|
+
} );
|
|
134
|
+
|
|
135
|
+
it( 'should reject addresses with single digit port numbers', async () => {
|
|
136
|
+
process.env = {
|
|
137
|
+
TEMPORAL_ADDRESS: 'temporal-server:1',
|
|
138
|
+
CATALOG_ID: 'test-catalog'
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
await expect( import( './configs.js' ) ).rejects.toThrow();
|
|
142
|
+
} );
|
|
143
|
+
|
|
144
|
+
it( 'should reject when TEMPORAL_ADDRESS is missing', async () => {
|
|
145
|
+
process.env = {
|
|
146
|
+
CATALOG_ID: 'test-catalog'
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
await expect( import( './configs.js' ) ).rejects.toThrow();
|
|
150
|
+
} );
|
|
151
|
+
} );
|
|
152
|
+
|
|
153
|
+
describe( 'CATALOG_ID', () => {
|
|
154
|
+
it( 'should accept valid catalog IDs with letters and numbers', async () => {
|
|
155
|
+
process.env = {
|
|
156
|
+
TEMPORAL_ADDRESS: 'http://localhost:7233',
|
|
157
|
+
CATALOG_ID: 'catalog123'
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const { worker } = await import( './configs.js' );
|
|
161
|
+
expect( worker.catalogId ).toBe( 'catalog123' );
|
|
162
|
+
expect( worker.taskQueue ).toBe( 'catalog123' );
|
|
163
|
+
} );
|
|
164
|
+
|
|
165
|
+
it( 'should accept catalog IDs with dots and hyphens', async () => {
|
|
166
|
+
process.env = {
|
|
167
|
+
TEMPORAL_ADDRESS: 'http://localhost:7233',
|
|
168
|
+
CATALOG_ID: 'my.catalog-id'
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const { worker } = await import( './configs.js' );
|
|
172
|
+
expect( worker.catalogId ).toBe( 'my.catalog-id' );
|
|
173
|
+
} );
|
|
174
|
+
|
|
175
|
+
it( 'should accept catalog IDs with underscores', async () => {
|
|
176
|
+
process.env = {
|
|
177
|
+
TEMPORAL_ADDRESS: 'http://localhost:7233',
|
|
178
|
+
CATALOG_ID: 'my_catalog_id'
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const { worker } = await import( './configs.js' );
|
|
182
|
+
expect( worker.catalogId ).toBe( 'my_catalog_id' );
|
|
183
|
+
} );
|
|
184
|
+
|
|
185
|
+
it( 'should accept catalog IDs with @ symbol', async () => {
|
|
186
|
+
process.env = {
|
|
187
|
+
TEMPORAL_ADDRESS: 'http://localhost:7233',
|
|
188
|
+
CATALOG_ID: '@my-catalog'
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const { worker } = await import( './configs.js' );
|
|
192
|
+
expect( worker.catalogId ).toBe( '@my-catalog' );
|
|
193
|
+
} );
|
|
194
|
+
|
|
195
|
+
it( 'should reject catalog IDs with special characters', async () => {
|
|
196
|
+
process.env = {
|
|
197
|
+
TEMPORAL_ADDRESS: 'http://localhost:7233',
|
|
198
|
+
CATALOG_ID: 'catalog!@#$'
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
await expect( import( './configs.js' ) ).rejects.toThrow();
|
|
202
|
+
} );
|
|
203
|
+
|
|
204
|
+
it( 'should reject when CATALOG_ID is missing', async () => {
|
|
205
|
+
process.env = {
|
|
206
|
+
TEMPORAL_ADDRESS: 'http://localhost:7233'
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
await expect( import( './configs.js' ) ).rejects.toThrow();
|
|
210
|
+
} );
|
|
211
|
+
|
|
212
|
+
it( 'should reject empty CATALOG_ID', async () => {
|
|
213
|
+
process.env = {
|
|
214
|
+
TEMPORAL_ADDRESS: 'http://localhost:7233',
|
|
215
|
+
CATALOG_ID: ''
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
await expect( import( './configs.js' ) ).rejects.toThrow();
|
|
219
|
+
} );
|
|
220
|
+
} );
|
|
221
|
+
|
|
222
|
+
describe( 'Optional Fields', () => {
|
|
223
|
+
it( 'should use default namespace when TEMPORAL_NAMESPACE is not provided', async () => {
|
|
224
|
+
process.env = {
|
|
225
|
+
TEMPORAL_ADDRESS: 'http://localhost:7233',
|
|
226
|
+
CATALOG_ID: 'test-catalog'
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const { worker } = await import( './configs.js' );
|
|
230
|
+
expect( worker.namespace ).toBe( 'default' );
|
|
231
|
+
} );
|
|
232
|
+
|
|
233
|
+
it( 'should use custom namespace when provided', async () => {
|
|
234
|
+
process.env = {
|
|
235
|
+
TEMPORAL_ADDRESS: 'http://localhost:7233',
|
|
236
|
+
CATALOG_ID: 'test-catalog',
|
|
237
|
+
TEMPORAL_NAMESPACE: 'custom-namespace'
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const { worker } = await import( './configs.js' );
|
|
241
|
+
expect( worker.namespace ).toBe( 'custom-namespace' );
|
|
242
|
+
} );
|
|
243
|
+
|
|
244
|
+
it( 'should handle TEMPORAL_API_KEY when provided', async () => {
|
|
245
|
+
process.env = {
|
|
246
|
+
TEMPORAL_ADDRESS: 'http://localhost:7233',
|
|
247
|
+
CATALOG_ID: 'test-catalog',
|
|
248
|
+
TEMPORAL_API_KEY: 'secret-api-key'
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const { worker } = await import( './configs.js' );
|
|
252
|
+
expect( worker.apiKey ).toBe( 'secret-api-key' );
|
|
253
|
+
} );
|
|
254
|
+
|
|
255
|
+
it( 'should handle missing TEMPORAL_API_KEY', async () => {
|
|
256
|
+
process.env = {
|
|
257
|
+
TEMPORAL_ADDRESS: 'http://localhost:7233',
|
|
258
|
+
CATALOG_ID: 'test-catalog'
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const { worker } = await import( './configs.js' );
|
|
262
|
+
expect( worker.apiKey ).toBeUndefined();
|
|
263
|
+
} );
|
|
264
|
+
|
|
265
|
+
it( 'should handle API_AUTH_KEY when provided', async () => {
|
|
266
|
+
process.env = {
|
|
267
|
+
TEMPORAL_ADDRESS: 'http://localhost:7233',
|
|
268
|
+
CATALOG_ID: 'test-catalog',
|
|
269
|
+
API_AUTH_KEY: 'api-secret-key'
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
const { api } = await import( './configs.js' );
|
|
273
|
+
expect( api.authKey ).toBe( 'api-secret-key' );
|
|
274
|
+
} );
|
|
275
|
+
|
|
276
|
+
it( 'should handle missing API_AUTH_KEY', async () => {
|
|
277
|
+
process.env = {
|
|
278
|
+
TEMPORAL_ADDRESS: 'http://localhost:7233',
|
|
279
|
+
CATALOG_ID: 'test-catalog'
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const { api } = await import( './configs.js' );
|
|
283
|
+
expect( api.authKey ).toBeUndefined();
|
|
284
|
+
} );
|
|
285
|
+
|
|
286
|
+
it( 'should handle TRACING_ENABLED when true', async () => {
|
|
287
|
+
process.env = {
|
|
288
|
+
TEMPORAL_ADDRESS: 'http://localhost:7233',
|
|
289
|
+
CATALOG_ID: 'test-catalog',
|
|
290
|
+
TRACING_ENABLED: 'true'
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
const { tracing } = await import( './configs.js' );
|
|
294
|
+
expect( tracing.enabled ).toBe( 'true' );
|
|
295
|
+
} );
|
|
296
|
+
|
|
297
|
+
it( 'should handle TRACING_ENABLED when false', async () => {
|
|
298
|
+
process.env = {
|
|
299
|
+
TEMPORAL_ADDRESS: 'http://localhost:7233',
|
|
300
|
+
CATALOG_ID: 'test-catalog',
|
|
301
|
+
TRACING_ENABLED: 'false'
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
const { tracing } = await import( './configs.js' );
|
|
305
|
+
expect( tracing.enabled ).toBe( 'false' );
|
|
306
|
+
} );
|
|
307
|
+
|
|
308
|
+
it( 'should handle missing TRACING_ENABLED', async () => {
|
|
309
|
+
process.env = {
|
|
310
|
+
TEMPORAL_ADDRESS: 'http://localhost:7233',
|
|
311
|
+
CATALOG_ID: 'test-catalog'
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
const { tracing } = await import( './configs.js' );
|
|
315
|
+
expect( tracing.enabled ).toBeUndefined();
|
|
316
|
+
} );
|
|
317
|
+
} );
|
|
318
|
+
} );
|
|
319
|
+
|
|
320
|
+
describe( 'Exported Config Objects', () => {
|
|
321
|
+
it( 'should export worker config with all properties', async () => {
|
|
322
|
+
process.env = {
|
|
323
|
+
TEMPORAL_ADDRESS: 'http://localhost:7233',
|
|
324
|
+
CATALOG_ID: 'test-catalog',
|
|
325
|
+
TEMPORAL_NAMESPACE: 'test-namespace',
|
|
326
|
+
TEMPORAL_API_KEY: 'test-api-key'
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
const { worker } = await import( './configs.js' );
|
|
330
|
+
|
|
331
|
+
expect( worker ).toEqual( {
|
|
332
|
+
address: 'http://localhost:7233',
|
|
333
|
+
apiKey: 'test-api-key',
|
|
334
|
+
executionTimeout: '1m',
|
|
335
|
+
maxActivities: 100,
|
|
336
|
+
maxWorkflows: 100,
|
|
337
|
+
namespace: 'test-namespace',
|
|
338
|
+
taskQueue: 'test-catalog',
|
|
339
|
+
catalogId: 'test-catalog'
|
|
340
|
+
} );
|
|
341
|
+
} );
|
|
342
|
+
|
|
343
|
+
it( 'should export api config', async () => {
|
|
344
|
+
process.env = {
|
|
345
|
+
TEMPORAL_ADDRESS: 'http://localhost:7233',
|
|
346
|
+
CATALOG_ID: 'test-catalog',
|
|
347
|
+
API_AUTH_KEY: 'test-auth-key'
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
const { api } = await import( './configs.js' );
|
|
351
|
+
|
|
352
|
+
expect( api ).toEqual( {
|
|
353
|
+
authKey: 'test-auth-key'
|
|
354
|
+
} );
|
|
355
|
+
} );
|
|
356
|
+
|
|
357
|
+
it( 'should export tracing config', async () => {
|
|
358
|
+
process.env = {
|
|
359
|
+
TEMPORAL_ADDRESS: 'http://localhost:7233',
|
|
360
|
+
CATALOG_ID: 'test-catalog',
|
|
361
|
+
TRACING_ENABLED: 'true'
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
const { tracing } = await import( './configs.js' );
|
|
365
|
+
|
|
366
|
+
expect( tracing ).toEqual( {
|
|
367
|
+
enabled: 'true'
|
|
368
|
+
} );
|
|
369
|
+
} );
|
|
370
|
+
|
|
371
|
+
it( 'should have correct static worker config values', async () => {
|
|
372
|
+
process.env = {
|
|
373
|
+
TEMPORAL_ADDRESS: 'http://localhost:7233',
|
|
374
|
+
CATALOG_ID: 'test-catalog'
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
const { worker } = await import( './configs.js' );
|
|
378
|
+
|
|
379
|
+
expect( worker.executionTimeout ).toBe( '1m' );
|
|
380
|
+
expect( worker.maxActivities ).toBe( 100 );
|
|
381
|
+
expect( worker.maxWorkflows ).toBe( 100 );
|
|
382
|
+
} );
|
|
383
|
+
} );
|
|
384
|
+
|
|
385
|
+
describe( 'Error Handling', () => {
|
|
386
|
+
it( 'should throw InvalidEnvVarsErrors for invalid configuration', async () => {
|
|
387
|
+
process.env = {
|
|
388
|
+
TEMPORAL_ADDRESS: 'invalid',
|
|
389
|
+
CATALOG_ID: 'test-catalog'
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
await expect( import( './configs.js' ) ).rejects.toThrow();
|
|
393
|
+
} );
|
|
394
|
+
|
|
395
|
+
it( 'should handle multiple validation errors', async () => {
|
|
396
|
+
process.env = {
|
|
397
|
+
TEMPORAL_ADDRESS: 'invalid',
|
|
398
|
+
CATALOG_ID: 'invalid!@#'
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
await expect( import( './configs.js' ) ).rejects.toThrow();
|
|
402
|
+
} );
|
|
403
|
+
} );
|
|
404
|
+
|
|
405
|
+
describe( 'Edge Cases', () => {
|
|
406
|
+
it( 'should handle environment variables with spaces', async () => {
|
|
407
|
+
process.env = {
|
|
408
|
+
TEMPORAL_ADDRESS: 'http://localhost:7233',
|
|
409
|
+
CATALOG_ID: 'test-catalog',
|
|
410
|
+
TEMPORAL_NAMESPACE: ' custom-namespace '
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
const { worker } = await import( './configs.js' );
|
|
414
|
+
expect( worker.namespace ).toBe( ' custom-namespace ' );
|
|
415
|
+
} );
|
|
416
|
+
|
|
417
|
+
it( 'should handle very long catalog IDs', async () => {
|
|
418
|
+
process.env = {
|
|
419
|
+
TEMPORAL_ADDRESS: 'http://localhost:7233',
|
|
420
|
+
CATALOG_ID: 'a'.repeat( 100 )
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
const { worker } = await import( './configs.js' );
|
|
424
|
+
expect( worker.catalogId ).toBe( 'a'.repeat( 100 ) );
|
|
425
|
+
} );
|
|
426
|
+
|
|
427
|
+
it( 'should handle URLs with ports outside typical range', async () => {
|
|
428
|
+
process.env = {
|
|
429
|
+
TEMPORAL_ADDRESS: 'http://localhost:65535',
|
|
430
|
+
CATALOG_ID: 'test-catalog'
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
const { worker } = await import( './configs.js' );
|
|
434
|
+
expect( worker.address ).toBe( 'http://localhost:65535' );
|
|
435
|
+
} );
|
|
436
|
+
|
|
437
|
+
it( 'should handle container names with numbers', async () => {
|
|
438
|
+
process.env = {
|
|
439
|
+
TEMPORAL_ADDRESS: 'temporal123:7233',
|
|
440
|
+
CATALOG_ID: 'test-catalog'
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
const { worker } = await import( './configs.js' );
|
|
444
|
+
expect( worker.address ).toBe( 'temporal123:7233' );
|
|
445
|
+
} );
|
|
446
|
+
|
|
447
|
+
it( 'should handle mixed case in catalog ID', async () => {
|
|
448
|
+
process.env = {
|
|
449
|
+
TEMPORAL_ADDRESS: 'http://localhost:7233',
|
|
450
|
+
CATALOG_ID: 'Test.Catalog-ID_123'
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
const { worker } = await import( './configs.js' );
|
|
454
|
+
expect( worker.catalogId ).toBe( 'Test.Catalog-ID_123' );
|
|
455
|
+
} );
|
|
456
|
+
} );
|
|
457
|
+
|
|
458
|
+
describe( 'Complete Valid Configuration', () => {
|
|
459
|
+
it( 'should handle a complete valid configuration with all optional fields', async () => {
|
|
460
|
+
process.env = {
|
|
461
|
+
TEMPORAL_ADDRESS: 'https://temporal.cloud.example.com',
|
|
462
|
+
TEMPORAL_NAMESPACE: 'production',
|
|
463
|
+
TEMPORAL_API_KEY: 'prod-api-key-123',
|
|
464
|
+
CATALOG_ID: 'prod.catalog@v1',
|
|
465
|
+
API_AUTH_KEY: 'secure-auth-key',
|
|
466
|
+
TRACING_ENABLED: 'true'
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
const { worker, api, tracing } = await import( './configs.js' );
|
|
470
|
+
|
|
471
|
+
expect( worker.address ).toBe( 'https://temporal.cloud.example.com' );
|
|
472
|
+
expect( worker.namespace ).toBe( 'production' );
|
|
473
|
+
expect( worker.apiKey ).toBe( 'prod-api-key-123' );
|
|
474
|
+
expect( worker.catalogId ).toBe( 'prod.catalog@v1' );
|
|
475
|
+
expect( worker.taskQueue ).toBe( 'prod.catalog@v1' );
|
|
476
|
+
expect( api.authKey ).toBe( 'secure-auth-key' );
|
|
477
|
+
expect( tracing.enabled ).toBe( 'true' );
|
|
478
|
+
} );
|
|
479
|
+
} );
|
|
480
|
+
} );
|
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
|
+
};
|
package/src/interface/step.js
CHANGED
|
@@ -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
|
-
|
|
13
|
-
|
|
11
|
+
const output = await fn( input );
|
|
12
|
+
if ( outputSchema ) {
|
|
13
|
+
validateStepOutput( name, outputSchema, output );
|
|
14
14
|
}
|
|
15
|
-
return
|
|
15
|
+
return output;
|
|
16
16
|
};
|
|
17
17
|
|
|
18
18
|
setMetadata( wrapper, { name, description, inputSchema, outputSchema } );
|
package/src/interface/utils.js
CHANGED
|
@@ -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
|
|
2
|
-
import { getInvocationDir
|
|
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
|
|
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
|
-
|
|
42
|
+
validateWorkflowOutput( name, outputSchema, output );
|
|
36
43
|
}
|
|
37
|
-
return
|
|
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
|
|
52
|
+
const output = await fn.call( {
|
|
44
53
|
invokeStep: async ( stepName, input ) => steps[`${workflowPath}#${stepName}`]( input ),
|
|
45
54
|
|
|
46
|
-
startWorkflow: async (
|
|
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:
|
|
61
|
+
{ parentWorkflowId: workflowId, rootWorkflowId: workflowId, rootWorkflowType: name };
|
|
54
62
|
|
|
55
|
-
return executeChild(
|
|
63
|
+
return executeChild( childName, { args: input ? [ input ] : [], memo: workflowMemory } );
|
|
56
64
|
}
|
|
57
|
-
} );
|
|
65
|
+
}, input );
|
|
58
66
|
|
|
59
67
|
if ( outputSchema ) {
|
|
60
|
-
|
|
68
|
+
validateWorkflowOutput( name, outputSchema, output );
|
|
61
69
|
}
|
|
62
|
-
|
|
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 {
|
|
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
|
} );
|
package/src/worker/loader.js
CHANGED
|
@@ -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 '
|
|
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 '
|
|
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
|
|