@outputai/core 0.7.1-next.ae5bab4.0 → 0.7.1-next.ba2fb0b.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 (78) hide show
  1. package/bin/worker.sh +6 -0
  2. package/package.json +1 -1
  3. package/src/consts.js +0 -4
  4. package/src/errors.js +6 -2
  5. package/src/hooks/index.d.ts +10 -0
  6. package/src/interface/evaluator.js +7 -20
  7. package/src/interface/evaluator.spec.js +117 -1
  8. package/src/interface/step.js +8 -9
  9. package/src/interface/step.spec.js +124 -0
  10. package/src/interface/validations/index.js +108 -0
  11. package/src/interface/validations/index.spec.js +182 -0
  12. package/src/interface/validations/schemas.js +113 -0
  13. package/src/interface/validations/schemas.spec.js +209 -0
  14. package/src/interface/webhook.js +1 -1
  15. package/src/interface/webhook.spec.js +1 -1
  16. package/src/interface/workflow.d.ts +10 -9
  17. package/src/interface/workflow.js +76 -164
  18. package/src/interface/workflow.spec.js +637 -521
  19. package/src/interface/workflow_activity_options.js +16 -0
  20. package/src/interface/workflow_utils.js +1 -1
  21. package/src/interface/zod_integration.spec.js +2 -2
  22. package/src/internal_utils/aggregations.js +0 -10
  23. package/src/internal_utils/aggregations.spec.js +1 -48
  24. package/src/internal_utils/errors.js +14 -8
  25. package/src/internal_utils/errors.spec.js +73 -27
  26. package/src/utils/index.d.ts +19 -0
  27. package/src/utils/utils.js +53 -0
  28. package/src/utils/utils.spec.js +105 -1
  29. package/src/worker/bundle.js +26 -0
  30. package/src/worker/bundle.spec.js +53 -0
  31. package/src/worker/bundler_options.js +1 -1
  32. package/src/worker/bundler_options.spec.js +1 -1
  33. package/src/worker/catalog_workflow/catalog_job.js +148 -0
  34. package/src/worker/catalog_workflow/catalog_job.spec.js +232 -0
  35. package/src/worker/check.js +24 -0
  36. package/src/worker/connection_monitor.js +112 -0
  37. package/src/worker/connection_monitor.spec.js +199 -0
  38. package/src/worker/index.js +146 -41
  39. package/src/worker/index.spec.js +281 -109
  40. package/src/worker/interceptors/activity.js +7 -24
  41. package/src/worker/interceptors/activity.spec.js +97 -66
  42. package/src/worker/interceptors/index.js +4 -7
  43. package/src/worker/interceptors/modules.js +15 -0
  44. package/src/worker/interceptors/workflow.js +6 -8
  45. package/src/worker/interceptors/workflow.spec.js +49 -42
  46. package/src/worker/interruption.js +33 -0
  47. package/src/worker/interruption.spec.js +98 -0
  48. package/src/worker/loader/activities.js +75 -0
  49. package/src/worker/loader/activities.spec.js +213 -0
  50. package/src/worker/loader/hooks.js +28 -0
  51. package/src/worker/loader/hooks.spec.js +64 -0
  52. package/src/worker/loader/matchers.js +46 -0
  53. package/src/worker/loader/matchers.spec.js +140 -0
  54. package/src/worker/{loader_tools.js → loader/tools.js} +19 -67
  55. package/src/worker/{loader_tools.spec.js → loader/tools.spec.js} +53 -85
  56. package/src/worker/loader/workflows.js +82 -0
  57. package/src/worker/loader/workflows.spec.js +256 -0
  58. package/src/worker/{setup_telemetry.js → telemetry.js} +9 -4
  59. package/src/worker/{setup_telemetry.spec.js → telemetry.spec.js} +3 -3
  60. package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.js +5 -109
  61. package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.spec.js +31 -103
  62. package/src/worker/webpack_loaders/workflow_rewriter/index.mjs +5 -6
  63. package/src/worker/webpack_loaders/workflow_rewriter/index.spec.js +11 -83
  64. package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.js +8 -11
  65. package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.spec.js +9 -9
  66. package/src/interface/validations/runtime.js +0 -20
  67. package/src/interface/validations/runtime.spec.js +0 -29
  68. package/src/interface/validations/schema_utils.js +0 -8
  69. package/src/interface/validations/schema_utils.spec.js +0 -67
  70. package/src/interface/validations/static.js +0 -137
  71. package/src/interface/validations/static.spec.js +0 -397
  72. package/src/interface/workflow.replay_compatibility.spec.js +0 -254
  73. package/src/worker/loader.js +0 -202
  74. package/src/worker/loader.spec.js +0 -498
  75. package/src/worker/shutdown.js +0 -26
  76. package/src/worker/shutdown.spec.js +0 -82
  77. package/src/worker/start_catalog.js +0 -96
  78. package/src/worker/start_catalog.spec.js +0 -179
@@ -1,125 +1,232 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ const {
4
+ catalogJobInstance,
5
+ configValues,
6
+ connectionMonitorInstance,
7
+ createCatalogMock,
8
+ initInterceptorsMock,
9
+ messageBusMock,
10
+ mockConnection,
11
+ mockLog,
12
+ mockWorker,
13
+ promises,
14
+ resetPromises,
15
+ setupInterruptionHandlerMock,
16
+ setupTelemetryMock
17
+ } = vi.hoisted( () => {
18
+ const createDeferred = () => {
19
+ const state = {};
20
+ state.promise = new Promise( ( resolve, reject ) => {
21
+ state.resolve = resolve;
22
+ state.reject = reject;
23
+ } );
24
+ return state;
25
+ };
26
+
27
+ const promises = {};
28
+ const resetPromises = () => {
29
+ promises.workerRun = createDeferred();
30
+ promises.connectionMonitor = createDeferred();
31
+ promises.catalogJob = createDeferred();
32
+ };
33
+ resetPromises();
34
+
35
+ const configValues = {
36
+ address: 'localhost:7233',
37
+ apiKey: undefined,
38
+ namespace: 'default',
39
+ taskQueue: 'test-queue',
40
+ catalogId: 'test-catalog',
41
+ grpcProxy: undefined,
42
+ maxConcurrentWorkflowTaskExecutions: 200,
43
+ maxConcurrentActivityTaskExecutions: 40,
44
+ maxCachedWorkflows: 1000,
45
+ maxConcurrentActivityTaskPolls: 5,
46
+ maxConcurrentWorkflowTaskPolls: 5,
47
+ processFailureShutdownDelay: 0
48
+ };
49
+
50
+ const connectionMonitorInstance = {
51
+ running: false,
52
+ connectionLossError: null,
53
+ onConnectionLost: vi.fn( cb => {
54
+ connectionMonitorInstance.connectionLostCb = cb;
55
+ } ),
56
+ start: vi.fn( () => {
57
+ connectionMonitorInstance.running = true;
58
+ return promises.connectionMonitor.promise.finally( () => {
59
+ connectionMonitorInstance.running = false;
60
+ } );
61
+ } ),
62
+ stop: vi.fn( () => {
63
+ connectionMonitorInstance.running = false;
64
+ promises.connectionMonitor.resolve();
65
+ return promises.connectionMonitor.promise;
66
+ } ),
67
+ connectionLostCb: null
68
+ };
69
+
70
+ const catalogJobInstance = {
71
+ running: false,
72
+ error: null,
73
+ onError: vi.fn( cb => {
74
+ catalogJobInstance.errorCb = cb;
75
+ } ),
76
+ run: vi.fn( () => {
77
+ catalogJobInstance.running = true;
78
+ return promises.catalogJob.promise.finally( () => {
79
+ catalogJobInstance.running = false;
80
+ } );
81
+ } ),
82
+ interrupt: vi.fn( () => {
83
+ catalogJobInstance.running = false;
84
+ promises.catalogJob.resolve();
85
+ return promises.catalogJob.promise;
86
+ } ),
87
+ errorCb: null
88
+ };
89
+
90
+ const mockWorker = {
91
+ getStatus: vi.fn( () => ( { runState: mockWorker.runState } ) ),
92
+ run: vi.fn( () => promises.workerRun.promise ),
93
+ runState: 'RUNNING',
94
+ shutdown: vi.fn()
95
+ };
96
+
97
+ return {
98
+ catalogJobInstance,
99
+ configValues,
100
+ connectionMonitorInstance,
101
+ createCatalogMock: vi.fn().mockReturnValue( { workflows: [], activities: {} } ),
102
+ createWorkflowsEntryPointMock: vi.fn().mockReturnValue( '/fake/workflows/path.js' ),
103
+ hashSourceCodeMock: vi.fn().mockResolvedValue( 'catalog-hash' ),
104
+ initInterceptorsMock: vi.fn().mockReturnValue( [] ),
105
+ loadActivitiesMock: vi.fn().mockResolvedValue( {} ),
106
+ loadHooksMock: vi.fn().mockResolvedValue( undefined ),
107
+ loadWorkflowsMock: vi.fn().mockResolvedValue( [] ),
108
+ messageBusMock: { emit: vi.fn(), on: vi.fn() },
109
+ mockConnection: { close: vi.fn().mockResolvedValue( undefined ) },
110
+ mockLog: { error: vi.fn(), info: vi.fn(), warn: vi.fn() },
111
+ mockWorker,
112
+ promises,
113
+ resetPromises,
114
+ setupInterruptionHandlerMock: vi.fn(),
115
+ setupTelemetryMock: vi.fn()
116
+ };
117
+ } );
2
118
 
3
- const mockLog = { info: vi.fn(), warn: vi.fn(), error: vi.fn() };
4
119
  vi.mock( '#logger', () => ( { createChildLogger: () => mockLog } ) );
5
-
6
120
  vi.mock( '#consts', async importOriginal => {
7
121
  const actual = await importOriginal();
8
122
  return { ...actual };
9
123
  } );
10
-
11
- vi.mock( '#tracing', () => ( { init: vi.fn().mockResolvedValue( undefined ) } ) );
12
-
13
- const configValues = {
14
- address: 'localhost:7233',
15
- apiKey: undefined,
16
- namespace: 'default',
17
- taskQueue: 'test-queue',
18
- catalogId: 'test-catalog',
19
- grpcProxy: undefined,
20
- maxConcurrentWorkflowTaskExecutions: 200,
21
- maxConcurrentActivityTaskExecutions: 40,
22
- maxCachedWorkflows: 1000,
23
- maxConcurrentActivityTaskPolls: 5,
24
- maxConcurrentWorkflowTaskPolls: 5,
25
- processFailureShutdownDelay: 0
26
- };
27
- vi.mock( './configs.js', () => configValues );
28
-
29
- const messageBusMock = { on: vi.fn(), emit: vi.fn() };
124
+ const initTracing = vi.fn().mockResolvedValue( undefined );
125
+ vi.mock( '#tracing', () => ( { init: initTracing } ) );
30
126
  vi.mock( '#bus', () => ( { messageBus: messageBusMock } ) );
31
127
 
32
- const loadWorkflowsMock = vi.fn().mockResolvedValue( [] );
33
- const loadActivitiesMock = vi.fn().mockResolvedValue( {} );
128
+ const loadWorkflowsMock = vi.fn().mockResolvedValue( { workflows: [], entrypoint: '/fake/workflows/path.js' } );
129
+ const loadActivitiesMock = vi.fn().mockResolvedValue( { activities: {} } );
34
130
  const loadHooksMock = vi.fn().mockResolvedValue( undefined );
35
- const createWorkflowsEntryPointMock = vi.fn().mockReturnValue( '/fake/workflows/path.js' );
36
- vi.mock( './loader.js', () => ( {
37
- loadWorkflows: loadWorkflowsMock,
38
- loadActivities: loadActivitiesMock,
39
- loadHooks: loadHooksMock,
40
- createWorkflowsEntryPoint: createWorkflowsEntryPointMock
41
- } ) );
131
+ vi.mock( './loader/workflows.js', () => ( { loadWorkflows: loadWorkflowsMock } ) );
132
+ vi.mock( './loader/activities.js', () => ( { loadActivities: loadActivitiesMock } ) );
133
+ vi.mock( './loader/hooks.js', () => ( { loadHooks: loadHooksMock } ) );
134
+ vi.mock( './configs.js', () => configValues );
42
135
 
43
136
  const hashSourceCodeMock = vi.fn().mockResolvedValue( 'catalog-hash' );
44
- vi.mock( './loader_tools.js', () => ( { hashSourceCode: hashSourceCodeMock } ) );
137
+ vi.mock( './loader/tools.js', () => ( { hashSourceCode: hashSourceCodeMock } ) );
45
138
 
46
139
  vi.mock( './sinks.js', () => ( { sinks: {} } ) );
47
-
48
- const createCatalogMock = vi.fn().mockReturnValue( { workflows: [], activities: {} } );
49
140
  vi.mock( './catalog_workflow/index.js', () => ( { createCatalog: createCatalogMock } ) );
50
-
51
141
  vi.mock( './bundler_options.js', () => ( { webpackConfigHook: vi.fn() } ) );
52
-
53
- const initInterceptorsMock = vi.fn().mockReturnValue( [] );
54
142
  vi.mock( './interceptors/index.js', () => ( { initInterceptors: initInterceptorsMock } ) );
55
-
56
- const startCatalogMock = vi.fn().mockResolvedValue( undefined );
57
- vi.mock( './start_catalog.js', () => ( { startCatalog: startCatalogMock } ) );
58
-
59
- const bootstrapFetchProxyMock = vi.fn();
60
- vi.mock( './proxy.js', () => ( { bootstrapFetchProxy: bootstrapFetchProxyMock } ) );
61
-
62
- const registerShutdownMock = vi.fn();
63
- vi.mock( './shutdown.js', () => ( { registerShutdown: registerShutdownMock } ) );
64
-
65
- const setupTelemetryMock = vi.fn();
66
- vi.mock( './setup_telemetry.js', () => ( { setupTelemetry: setupTelemetryMock } ) );
67
-
143
+ vi.mock( './proxy.js', () => ( { bootstrapFetchProxy: vi.fn() } ) );
144
+ vi.mock( './telemetry.js', () => ( { setupTelemetry: setupTelemetryMock } ) );
145
+ vi.mock( './interruption.js', () => ( { setupInterruptionHandler: setupInterruptionHandlerMock } ) );
146
+ vi.mock( './connection_monitor.js', () => ( {
147
+ TemporalConnectionMonitor: vi.fn( function () {
148
+ return connectionMonitorInstance;
149
+ } )
150
+ } ) );
151
+ vi.mock( './catalog_workflow/catalog_job.js', () => ( {
152
+ CatalogJob: vi.fn( function () {
153
+ return catalogJobInstance;
154
+ } )
155
+ } ) );
68
156
  vi.mock( './log_hooks.js', () => ( {} ) );
69
-
70
- const runState = { resolve: null };
71
- const runPromise = new Promise( r => {
72
- runState.resolve = r;
73
- } );
74
- const shutdownMock = vi.fn();
75
- const mockConnection = { close: vi.fn().mockResolvedValue( undefined ) };
76
- const mockWorker = { run: () => runPromise, shutdown: shutdownMock };
77
-
78
157
  vi.mock( '@temporalio/worker', () => ( {
79
- Worker: { create: vi.fn().mockResolvedValue( mockWorker ) },
80
- NativeConnection: { connect: vi.fn().mockResolvedValue( mockConnection ) }
158
+ NativeConnection: { connect: vi.fn().mockResolvedValue( mockConnection ) },
159
+ Worker: { create: vi.fn().mockResolvedValue( mockWorker ) }
81
160
  } ) );
82
161
 
162
+ const importWorker = async () => {
163
+ vi.resetModules();
164
+ await import( './index.js' );
165
+ };
166
+
167
+ const settleWorker = async () => {
168
+ promises.catalogJob.resolve();
169
+ promises.connectionMonitor.resolve();
170
+ promises.workerRun.resolve();
171
+ await vi.waitFor( () => expect( mockConnection.close ).toHaveBeenCalled() );
172
+ };
173
+
83
174
  describe( 'worker/index', () => {
84
175
  const exitMock = vi.fn();
85
176
  const originalArgv = process.argv;
86
- const originalExit = process.exit;
87
177
 
88
178
  beforeEach( () => {
89
179
  vi.clearAllMocks();
180
+ resetPromises();
181
+ configValues.apiKey = undefined;
182
+ configValues.grpcProxy = undefined;
183
+ catalogJobInstance.error = null;
184
+ catalogJobInstance.errorCb = null;
185
+ catalogJobInstance.running = false;
186
+ connectionMonitorInstance.connectionLossError = null;
187
+ connectionMonitorInstance.connectionLostCb = null;
188
+ connectionMonitorInstance.running = false;
189
+ mockConnection.close.mockResolvedValue( undefined );
190
+ mockWorker.runState = 'RUNNING';
90
191
  process.argv = [ ...originalArgv.slice( 0, 2 ), '/test/caller/dir' ];
91
- process.exit = exitMock;
192
+ vi.spyOn( process, 'exit' ).mockImplementation( exitMock );
92
193
  } );
93
194
 
94
195
  afterEach( () => {
95
196
  process.argv = originalArgv;
96
- process.exit = originalExit;
97
- configValues.apiKey = undefined;
197
+ vi.restoreAllMocks();
98
198
  } );
99
199
 
100
- it( 'loads configs, workflows, activities and creates worker with correct options', async () => {
101
- const { Worker, NativeConnection } = await import( '@temporalio/worker' );
102
- const { init: initTracing } = await import( '#tracing' );
200
+ it( 'creates the worker lifecycle jobs with expected dependencies', async () => {
201
+ const { NativeConnection, Worker } = await import( '@temporalio/worker' );
202
+ const { TemporalConnectionMonitor } = await import( './connection_monitor.js' );
203
+ const { CatalogJob } = await import( './catalog_workflow/catalog_job.js' );
103
204
 
104
- import( './index.js' );
205
+ await importWorker();
105
206
 
106
- await vi.waitFor( () => {
107
- expect( loadHooksMock ).toHaveBeenCalledWith( '/test/caller/dir' );
108
- } );
207
+ await vi.waitFor( () => expect( Worker.create ).toHaveBeenCalled() );
208
+
209
+ expect( loadHooksMock ).toHaveBeenCalledWith( '/test/caller/dir' );
109
210
  expect( loadWorkflowsMock ).toHaveBeenCalledWith( '/test/caller/dir' );
110
211
  expect( loadActivitiesMock ).toHaveBeenCalledWith( '/test/caller/dir', [] );
111
- expect( createWorkflowsEntryPointMock ).toHaveBeenCalledWith( [] );
112
212
  expect( initTracing ).toHaveBeenCalled();
113
213
  expect( createCatalogMock ).toHaveBeenCalledWith( { workflows: [], activities: {} } );
114
214
  expect( hashSourceCodeMock ).toHaveBeenCalledWith( '/test/caller/dir' );
115
- expect( bootstrapFetchProxyMock ).toHaveBeenCalled();
116
215
  expect( NativeConnection.connect ).toHaveBeenCalledWith( {
117
216
  address: configValues.address,
118
217
  tls: false,
119
218
  apiKey: undefined,
120
219
  proxy: undefined
121
220
  } );
221
+ expect( TemporalConnectionMonitor ).toHaveBeenCalledWith( mockConnection );
222
+ expect( CatalogJob ).toHaveBeenCalledWith( {
223
+ connection: mockConnection,
224
+ namespace: configValues.namespace,
225
+ catalog: { workflows: [], activities: {} },
226
+ catalogHash: 'catalog-hash'
227
+ } );
122
228
  expect( Worker.create ).toHaveBeenCalledWith( expect.objectContaining( {
229
+ connection: mockConnection,
123
230
  namespace: configValues.namespace,
124
231
  taskQueue: configValues.taskQueue,
125
232
  workflowsPath: '/fake/workflows/path.js',
@@ -130,63 +237,128 @@ describe( 'worker/index', () => {
130
237
  maxConcurrentActivityTaskPolls: configValues.maxConcurrentActivityTaskPolls,
131
238
  maxConcurrentWorkflowTaskPolls: configValues.maxConcurrentWorkflowTaskPolls
132
239
  } ) );
133
- expect( initInterceptorsMock ).toHaveBeenCalledWith( { activities: {}, workflows: [], connection: mockConnection } );
134
- expect( registerShutdownMock ).toHaveBeenCalledWith( { worker: mockWorker, log: mockLog } );
240
+ expect( initInterceptorsMock ).toHaveBeenCalledWith( { activities: {}, workflows: [] } );
135
241
  expect( setupTelemetryMock ).toHaveBeenCalledWith( { worker: mockWorker } );
136
- expect( startCatalogMock ).toHaveBeenCalledWith( {
137
- connection: mockConnection,
138
- namespace: configValues.namespace,
139
- catalog: { workflows: [], activities: {} },
140
- catalogHash: 'catalog-hash'
141
- } );
142
-
143
- runState.resolve();
144
- await vi.waitFor( () => {
145
- expect( mockConnection.close ).toHaveBeenCalled();
146
- } );
147
- expect( exitMock ).toHaveBeenCalledWith( 0 );
242
+ expect( setupInterruptionHandlerMock ).toHaveBeenCalledWith( expect.any( Function ) );
243
+ expect( connectionMonitorInstance.onConnectionLost ).toHaveBeenCalledWith( expect.any( Function ) );
244
+ expect( catalogJobInstance.onError ).toHaveBeenCalledWith( expect.any( Function ) );
245
+ expect( mockWorker.run ).toHaveBeenCalled();
246
+ expect( connectionMonitorInstance.start ).toHaveBeenCalled();
247
+ expect( catalogJobInstance.run ).toHaveBeenCalled();
248
+
249
+ await settleWorker();
250
+ expect( mockLog.info ).toHaveBeenCalledWith( 'Bye' );
148
251
  } );
149
252
 
150
253
  it( 'enables TLS when apiKey is set', async () => {
151
254
  configValues.apiKey = 'secret';
152
- vi.resetModules();
153
-
154
255
  const { NativeConnection } = await import( '@temporalio/worker' );
155
- import( './index.js' );
256
+
257
+ await importWorker();
258
+
259
+ await vi.waitFor( () => expect( NativeConnection.connect ).toHaveBeenCalledWith( expect.objectContaining( {
260
+ apiKey: 'secret',
261
+ tls: true
262
+ } ) ) );
263
+
264
+ await settleWorker();
265
+ } );
266
+
267
+ it( 'runs graceful shutdown when interrupted', async () => {
268
+ await importWorker();
269
+
270
+ await vi.waitFor( () => expect( setupInterruptionHandlerMock ).toHaveBeenCalled() );
271
+ const [ shutdown ] = setupInterruptionHandlerMock.mock.calls[0];
272
+
273
+ shutdown();
274
+
275
+ expect( mockWorker.shutdown ).toHaveBeenCalledOnce();
276
+ expect( connectionMonitorInstance.stop ).toHaveBeenCalledOnce();
277
+ expect( catalogJobInstance.interrupt ).toHaveBeenCalledOnce();
278
+
279
+ promises.workerRun.resolve();
280
+ await vi.waitFor( () => expect( mockConnection.close ).toHaveBeenCalled() );
281
+ expect( mockLog.info ).toHaveBeenCalledWith( 'Bye' );
282
+ } );
283
+
284
+ it( 'does not call worker.shutdown when worker has already failed', async () => {
285
+ const error = new Error( 'Big Failure' );
286
+
287
+ await importWorker();
288
+ await vi.waitFor( () => expect( mockWorker.run ).toHaveBeenCalled() );
289
+
290
+ mockWorker.runState = 'FAILED';
291
+ promises.workerRun.reject( error );
292
+
293
+ await vi.waitFor( () => expect( connectionMonitorInstance.stop ).toHaveBeenCalled() );
294
+ expect( mockWorker.shutdown ).not.toHaveBeenCalled();
295
+
296
+ promises.connectionMonitor.resolve();
297
+ promises.catalogJob.resolve();
156
298
 
157
299
  await vi.waitFor( () => {
158
- expect( NativeConnection.connect ).toHaveBeenCalledWith( expect.objectContaining( {
159
- tls: true,
160
- apiKey: 'secret'
300
+ expect( mockLog.error ).toHaveBeenCalledWith( 'Fatal error', expect.objectContaining( {
301
+ error: 'Big Failure'
161
302
  } ) );
162
303
  } );
163
- runState.resolve();
164
- await vi.waitFor( () => expect( exitMock ).toHaveBeenCalled() );
304
+ expect( messageBusMock.emit ).toHaveBeenCalledWith( expect.any( String ), { error } );
305
+ await vi.waitFor( () => expect( exitMock ).toHaveBeenCalledWith( 1 ) );
165
306
  } );
166
307
 
167
- it( 'calls registerShutdown with worker and log', async () => {
168
- vi.resetModules();
308
+ it( 'throws connection monitor errors after graceful shutdown', async () => {
309
+ const error = new Error( 'connection lost' );
310
+
311
+ await importWorker();
312
+ await vi.waitFor( () => expect( connectionMonitorInstance.onConnectionLost ).toHaveBeenCalled() );
169
313
 
170
- import( './index.js' );
314
+ connectionMonitorInstance.connectionLossError = error;
315
+ connectionMonitorInstance.connectionLostCb( error );
316
+ promises.workerRun.resolve();
171
317
 
172
318
  await vi.waitFor( () => {
173
- expect( registerShutdownMock ).toHaveBeenCalledWith( { worker: mockWorker, log: mockLog } );
319
+ expect( mockLog.error ).toHaveBeenCalledWith( 'Fatal error', expect.objectContaining( {
320
+ error: 'connection lost'
321
+ } ) );
174
322
  } );
175
- runState.resolve();
176
- await vi.waitFor( () => expect( exitMock ).toHaveBeenCalled() );
323
+ expect( mockWorker.shutdown ).toHaveBeenCalledOnce();
324
+ expect( catalogJobInstance.interrupt ).toHaveBeenCalledOnce();
325
+ expect( messageBusMock.emit ).toHaveBeenCalledWith( expect.any( String ), { error } );
177
326
  } );
178
327
 
179
- it( 'calls process.exit(1) on fatal error', async () => {
180
- loadWorkflowsMock.mockRejectedValueOnce( new Error( 'load failed' ) );
181
- vi.resetModules();
328
+ it( 'throws catalog job errors after graceful shutdown', async () => {
329
+ const error = new Error( 'catalog failed' );
330
+
331
+ await importWorker();
332
+ await vi.waitFor( () => expect( catalogJobInstance.onError ).toHaveBeenCalled() );
182
333
 
183
- import( './index.js' );
334
+ catalogJobInstance.error = error;
335
+ catalogJobInstance.errorCb( error );
336
+ promises.workerRun.resolve();
184
337
 
185
338
  await vi.waitFor( () => {
186
- expect( mockLog.error ).toHaveBeenCalledWith( 'Fatal error', expect.any( Object ) );
339
+ expect( mockLog.error ).toHaveBeenCalledWith( 'Fatal error', expect.objectContaining( {
340
+ error: 'catalog failed'
341
+ } ) );
187
342
  } );
343
+ expect( mockWorker.shutdown ).toHaveBeenCalledOnce();
344
+ expect( connectionMonitorInstance.stop ).toHaveBeenCalledOnce();
345
+ expect( messageBusMock.emit ).toHaveBeenCalledWith( expect.any( String ), { error } );
346
+ } );
347
+
348
+ it( 'cleans up partial startup failures after connecting', async () => {
349
+ const { Worker } = await import( '@temporalio/worker' );
350
+ const error = new Error( 'worker create failed' );
351
+ Worker.create.mockRejectedValueOnce( error );
352
+
353
+ await importWorker();
354
+
355
+ await vi.waitFor( () => expect( mockConnection.close ).toHaveBeenCalled() );
356
+ expect( connectionMonitorInstance.stop ).not.toHaveBeenCalled();
357
+ expect( catalogJobInstance.interrupt ).not.toHaveBeenCalled();
188
358
  await vi.waitFor( () => {
189
- expect( exitMock ).toHaveBeenCalledWith( 1 );
359
+ expect( mockLog.error ).toHaveBeenCalledWith( 'Fatal error', expect.objectContaining( {
360
+ error: 'worker create failed'
361
+ } ) );
190
362
  } );
191
363
  } );
192
364
  } );
@@ -2,14 +2,11 @@ import { Context, activityInfo as activityInfoFn } from '@temporalio/activity';
2
2
  import { Storage } from '#async_storage';
3
3
  import * as Tracing from '#tracing';
4
4
  import { headersToObject } from './headers.js';
5
- import { ACTIVITY_WRAPPER_VERSION_FIELD, BusEventType, METADATA_ACCESS_SYMBOL, Signal } from '#consts';
6
- import { activityHeartbeatEnabled, activityHeartbeatIntervalMs, namespace } from '../configs.js';
5
+ import { ACTIVITY_WRAPPER_VERSION_FIELD, BusEventType, METADATA_ACCESS_SYMBOL } from '#consts';
6
+ import { activityHeartbeatEnabled, activityHeartbeatIntervalMs } from '../configs.js';
7
7
  import { messageBus } from '#bus';
8
- import { Client } from '@temporalio/client';
9
- import { createChildLogger } from '#logger';
10
8
  import { aggregateAttributes } from '#internal_utils/aggregations';
11
-
12
- const log = createChildLogger( 'ActivityInterceptor' );
9
+ import { buildApplicationFailureWithDetails } from '#internal_utils/errors';
13
10
 
14
11
  /*
15
12
  This interceptor wraps every activity execution with cross-cutting concerns:
@@ -28,7 +25,7 @@ const log = createChildLogger( 'ActivityInterceptor' );
28
25
  - Headers injected by the workflow interceptor
29
26
  */
30
27
  export class ActivityExecutionInterceptor {
31
- constructor( { activities, workflows, connection } ) {
28
+ constructor( { activities, workflows } ) {
32
29
  // convert activities{} object to a map: activityType:kind
33
30
  this.activityKindMap = new Map( Object.entries( activities )
34
31
  .map( ( [ type, fn ] ) => ( [ type, fn[METADATA_ACCESS_SYMBOL].type ] ) ) );
@@ -37,12 +34,11 @@ export class ActivityExecutionInterceptor {
37
34
  this.workflowsPathMap = new Map( workflows.flatMap( ( { name, aliases, path } ) =>
38
35
  [ name, ...aliases ?? [] ].map( a => ( [ a, path ] ) )
39
36
  ) );
40
- this.connection = connection;
41
37
  };
42
38
 
43
39
  async execute( input, next ) {
44
40
  const activityInfo = activityInfoFn();
45
- const { workflowExecution: { workflowId, runId }, activityId, activityType, workflowType } = activityInfo;
41
+ const { workflowExecution: { runId }, activityId, activityType, workflowType } = activityInfo;
46
42
  const { traceInfo, workflowDetails } = headersToObject( input.headers );
47
43
  const outputActivityKind = this.activityKindMap.get( activityType );
48
44
  const workflowFilename = this.workflowsPathMap.get( workflowType );
@@ -61,19 +57,6 @@ export class ActivityExecutionInterceptor {
61
57
 
62
58
  const addAttribute = attribute => state.attributes.push( attribute );
63
59
 
64
- const sendAggregationsViaSignal = async () => {
65
- if ( state.attributes.length > 0 ) {
66
- try {
67
- const client = new Client( { connection: this.connection, namespace } );
68
- const workflowHandle = client.workflow.getHandle( workflowId );
69
- await workflowHandle.signal( Signal.SEND_AGGREGATIONS, aggregateAttributes( state.attributes ) );
70
- } catch ( error ) {
71
- const errorContext = { message: error.message, stack: error.stack, activityId, activityType, workflowId, workflowType, runId };
72
- log.warn( `Signal "${Signal.SEND_AGGREGATIONS}" failed`, errorContext );
73
- }
74
- }
75
- };
76
-
77
60
  // Adds context accessible information
78
61
  const storageContext = {
79
62
  parentId: activityId,
@@ -107,9 +90,9 @@ export class ActivityExecutionInterceptor {
107
90
  messageBus.emit( BusEventType.ACTIVITY_ERROR, { activityInfo, workflowDetails, outputActivityKind, error } );
108
91
  Tracing.addEventError( { id: activityId, details: error, traceInfo } );
109
92
 
110
- await sendAggregationsViaSignal();
93
+ const aggregations = state.attributes.length > 0 ? aggregateAttributes( state.attributes ) : null;
111
94
 
112
- throw error;
95
+ throw aggregations ? buildApplicationFailureWithDetails( error, { aggregations } ) : error;
113
96
  } finally {
114
97
  clearInterval( state.heartbeat );
115
98
  }