@outputai/core 0.2.1-next.6499038.0 → 0.2.1-next.756d32d.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@outputai/core",
3
- "version": "0.2.1-next.6499038.0",
3
+ "version": "0.2.1-next.756d32d.0",
4
4
  "description": "The core module of the output framework",
5
5
  "type": "module",
6
6
  "exports": {
@@ -37,11 +37,11 @@
37
37
  "@babel/parser": "7.29.2",
38
38
  "@babel/traverse": "7.29.0",
39
39
  "@babel/types": "7.29.0",
40
- "@temporalio/activity": "1.15.0",
41
- "@temporalio/client": "1.15.0",
42
- "@temporalio/common": "1.15.0",
43
- "@temporalio/worker": "1.15.0",
44
- "@temporalio/workflow": "1.15.0",
40
+ "@temporalio/activity": "1.17.0",
41
+ "@temporalio/client": "1.17.0",
42
+ "@temporalio/common": "1.17.0",
43
+ "@temporalio/worker": "1.17.0",
44
+ "@temporalio/workflow": "1.17.0",
45
45
  "redis": "5.12.1",
46
46
  "stacktrace-parser": "0.1.11",
47
47
  "undici": "8.1.0",
@@ -12,6 +12,42 @@ export interface ErrorHookPayload {
12
12
  error: Error;
13
13
  }
14
14
 
15
+ /**
16
+ * Payload passed to the onWorkflowStart handler when a workflow run begins.
17
+ */
18
+ export interface WorkflowStartHookPayload {
19
+ /** Identifier of the workflow run. */
20
+ id: string;
21
+ /** Name of the workflow. */
22
+ name: string;
23
+ }
24
+
25
+ /**
26
+ * Payload passed to the onWorkflowEnd handler when a workflow run completes successfully.
27
+ */
28
+ export interface WorkflowEndHookPayload {
29
+ /** Identifier of the workflow run. */
30
+ id: string;
31
+ /** Name of the workflow. */
32
+ name: string;
33
+ /** Duration of the workflow run in milliseconds. */
34
+ duration: number;
35
+ }
36
+
37
+ /**
38
+ * Payload passed to the onWorkflowError handler when a workflow run fails.
39
+ */
40
+ export interface WorkflowErrorHookPayload {
41
+ /** Identifier of the workflow run. */
42
+ id: string;
43
+ /** Name of the workflow. */
44
+ name: string;
45
+ /** Elapsed time before failure in milliseconds. */
46
+ duration: number;
47
+ /** The error thrown. */
48
+ error: Error;
49
+ }
50
+
15
51
  /**
16
52
  * Register a handler to be invoked on workflow, activity or runtime errors.
17
53
  *
@@ -21,11 +57,38 @@ export declare function onError( handler: ( payload: ErrorHookPayload ) => void
21
57
 
22
58
  /**
23
59
  * Register a handler to be invoked once, before the worker starts processing tasks.
24
- * Runs synchronously after activities are loaded and before Worker.create().
60
+ * It is invoked before Worker.create().
25
61
  *
26
62
  * @param handler - Function called with no arguments.
27
63
  */
28
- export declare function onBeforeStart( handler: () => void ): void;
64
+ export declare function onBeforeWorkerStart( handler: () => void ): void;
65
+
66
+ /**
67
+ * Register a handler to be invoked when a workflow run starts.
68
+ *
69
+ * Excludes the $catalog internal workflow.
70
+ *
71
+ * @param handler - Function called with the workflow start payload.
72
+ */
73
+ export declare function onWorkflowStart( handler: ( payload: WorkflowStartHookPayload ) => void ): void;
74
+
75
+ /**
76
+ * Register a handler to be invoked when a workflow run completes successfully.
77
+ *
78
+ * Excludes the $catalog internal workflow.
79
+ *
80
+ * @param handler - Function called with the workflow end payload.
81
+ */
82
+ export declare function onWorkflowEnd( handler: ( payload: WorkflowEndHookPayload ) => void ): void;
83
+
84
+ /**
85
+ * Register a handler to be invoked when a workflow run fails.
86
+ *
87
+ * Excludes the $catalog internal workflow.
88
+ *
89
+ * @param handler - Function called with the workflow error payload.
90
+ */
91
+ export declare function onWorkflowError( handler: ( payload: WorkflowErrorHookPayload ) => void ): void;
29
92
 
30
93
  /**
31
94
  * Register a handler to be invoked when a given event happens
@@ -33,4 +96,4 @@ export declare function onBeforeStart( handler: () => void ): void;
33
96
  * @param eventName - The name of the event to subscribe
34
97
  * @param handler - Function called with the event payload
35
98
  */
36
- export declare function on( eventName: string, handler: ( payload: object ) => void ): void;
99
+ export declare function on( eventName: string, handler: ( payload?: object ) => void ): void;
@@ -1,34 +1,50 @@
1
1
  import { messageBus } from '#bus';
2
- import { BusEventType } from '#consts';
2
+ import { BusEventType, WORKFLOW_CATALOG } from '#consts';
3
3
  import { createChildLogger } from '#logger';
4
4
 
5
5
  const log = createChildLogger( 'Hooks' );
6
6
 
7
+ /**
8
+ * Invokes an external hook handler function with a try catch around it
9
+ *
10
+ * @param {Function} fn
11
+ * @param {any} args - Args to invoke the function with
12
+ * @param {string} hookName - hookName to identify this hook function in the logs
13
+ */
14
+ const safeInvoke = async ( fn, args, hookName ) => {
15
+ try {
16
+ await fn( args );
17
+ } catch ( error ) {
18
+ log.error( `${hookName} hook error`, { message: error.message, stack: error.stack } );
19
+ }
20
+ };
21
+
22
+ /** Triggers on any errors: workflow, activity and runtime */
7
23
  export const onError = handler => {
8
- const invokeHandler = async args => {
9
- try {
10
- await handler( args );
11
- } catch ( error ) {
12
- log.error( 'onError hook error', { error } );
13
- }
14
- };
15
-
16
- messageBus.on( BusEventType.ACTIVITY_ERROR, async ( { name, workflowName, error } ) =>
17
- invokeHandler( { source: 'activity', activityName: name, workflowName, error } ) );
18
- messageBus.on( BusEventType.WORKFLOW_ERROR, async ( { name, error } ) =>
19
- invokeHandler( { source: 'workflow', workflowName: name, error } ) );
24
+ messageBus.on( BusEventType.ACTIVITY_ERROR, async ( { id, name, workflowId, workflowName, error } ) =>
25
+ safeInvoke( handler, { source: 'activity', activityId: id, activityName: name, workflowId, workflowName, error }, 'onError' ) );
26
+ messageBus.on( BusEventType.WORKFLOW_ERROR, async ( { id, name, error } ) =>
27
+ safeInvoke( handler, { source: 'workflow', workflowId: id, workflowName: name, error }, 'onError' ) );
20
28
  messageBus.on( BusEventType.RUNTIME_ERROR, async ( { error } ) =>
21
- invokeHandler( { source: 'runtime', error } ) );
29
+ safeInvoke( handler, { source: 'runtime', error }, 'onError' ) );
22
30
  };
23
31
 
24
- export const onBeforeStart = handler => messageBus.on( BusEventType.WORKER_BEFORE_START, handler );
32
+ /** Listen to worker before start events */
33
+ export const onBeforeWorkerStart = handler => messageBus.on( BusEventType.WORKER_BEFORE_START, () =>
34
+ safeInvoke( handler, undefined, 'onBeforeWorkerStart' ) );
25
35
 
26
- export const on = ( eventName, handler ) => {
27
- messageBus.on( `external:${eventName}`, async payload => {
28
- try {
29
- await handler( payload );
30
- } catch ( error ) {
31
- log.error( `on(${eventName}) hook error`, { error } );
32
- }
33
- } );
34
- };
36
+ /** Listen to workflow start events, excludes catalog workflow */
37
+ export const onWorkflowStart = handler => messageBus.on( BusEventType.WORKFLOW_START, ( { id, name } ) =>
38
+ WORKFLOW_CATALOG !== name ? safeInvoke( handler, { id, name }, 'onWorkflowStart' ) : null );
39
+
40
+ /** Listen to workflow end events, excludes catalog workflow */
41
+ export const onWorkflowEnd = handler => messageBus.on( BusEventType.WORKFLOW_END, ( { id, name, duration } ) =>
42
+ WORKFLOW_CATALOG !== name ? safeInvoke( handler, { id, name, duration }, 'onWorkflowEnd' ) : null );
43
+
44
+ /** Listen to workflow error events, excludes catalog workflow */
45
+ export const onWorkflowError = handler => messageBus.on( BusEventType.WORKFLOW_ERROR, ( { id, name, duration, error } ) =>
46
+ WORKFLOW_CATALOG !== name ? safeInvoke( handler, { id, name, duration, error }, 'onWorkflowError' ) : null );
47
+
48
+ /** Generic listener for events emitted elsewhere (outside core) */
49
+ export const on = ( eventName, handler ) => messageBus.on( `external:${eventName}`, payload =>
50
+ safeInvoke( handler, payload, eventName ) );
@@ -0,0 +1,176 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { BusEventType, WORKFLOW_CATALOG } from '#consts';
3
+
4
+ const logErrorMock = vi.hoisted( () => vi.fn() );
5
+ const createChildLoggerMock = vi.hoisted( () =>
6
+ vi.fn( () => ( { error: logErrorMock } ) )
7
+ );
8
+
9
+ const onHandlers = vi.hoisted( () => ( {} ) );
10
+ const messageBusMock = vi.hoisted( () => ( {
11
+ on: vi.fn( ( eventType, handler ) => {
12
+ onHandlers[eventType] = handler;
13
+ } )
14
+ } ) );
15
+
16
+ vi.mock( '#logger', () => ( { createChildLogger: createChildLoggerMock } ) );
17
+ vi.mock( '#bus', () => ( { messageBus: messageBusMock } ) );
18
+
19
+ import {
20
+ on,
21
+ onBeforeWorkerStart,
22
+ onError,
23
+ onWorkflowEnd,
24
+ onWorkflowError,
25
+ onWorkflowStart
26
+ } from './index.js';
27
+
28
+ describe( 'hooks/index', () => {
29
+ beforeEach( () => {
30
+ vi.clearAllMocks();
31
+ Object.keys( onHandlers ).forEach( k => {
32
+ delete onHandlers[k];
33
+ } );
34
+ } );
35
+
36
+ describe( 'onError', () => {
37
+ it( 'registers activity, workflow, and runtime error listeners', () => {
38
+ const handler = vi.fn().mockResolvedValue( undefined );
39
+ onError( handler );
40
+
41
+ expect( messageBusMock.on ).toHaveBeenCalledWith( BusEventType.ACTIVITY_ERROR, expect.any( Function ) );
42
+ expect( messageBusMock.on ).toHaveBeenCalledWith( BusEventType.WORKFLOW_ERROR, expect.any( Function ) );
43
+ expect( messageBusMock.on ).toHaveBeenCalledWith( BusEventType.RUNTIME_ERROR, expect.any( Function ) );
44
+ } );
45
+
46
+ it( 'invokes handler with activity-shaped payload', async () => {
47
+ const handler = vi.fn().mockResolvedValue( undefined );
48
+ onError( handler );
49
+
50
+ const err = new Error( 'act-fail' );
51
+ await onHandlers[BusEventType.ACTIVITY_ERROR]( {
52
+ id: 'act-1',
53
+ name: 'wf#step',
54
+ workflowId: 'wf-run-1',
55
+ workflowName: 'wf',
56
+ error: err
57
+ } );
58
+
59
+ expect( handler ).toHaveBeenCalledWith( {
60
+ source: 'activity',
61
+ activityId: 'act-1',
62
+ activityName: 'wf#step',
63
+ workflowId: 'wf-run-1',
64
+ workflowName: 'wf',
65
+ error: err
66
+ } );
67
+ } );
68
+
69
+ it( 'invokes handler with workflow-shaped payload', async () => {
70
+ const handler = vi.fn().mockResolvedValue( undefined );
71
+ onError( handler );
72
+
73
+ const err = new Error( 'wf-fail' );
74
+ await onHandlers[BusEventType.WORKFLOW_ERROR]( {
75
+ id: 'wf-run-2',
76
+ name: 'myWorkflow',
77
+ error: err
78
+ } );
79
+
80
+ expect( handler ).toHaveBeenCalledWith( {
81
+ source: 'workflow',
82
+ workflowId: 'wf-run-2',
83
+ workflowName: 'myWorkflow',
84
+ error: err
85
+ } );
86
+ } );
87
+
88
+ it( 'logs and does not rethrow when handler rejects', async () => {
89
+ const handler = vi.fn().mockRejectedValue( new Error( 'boom' ) );
90
+ onError( handler );
91
+
92
+ const error = new Error( 'rt' );
93
+ await onHandlers[BusEventType.RUNTIME_ERROR]( { error } );
94
+
95
+ expect( handler ).toHaveBeenCalledWith( { source: 'runtime', error } );
96
+ } );
97
+ } );
98
+
99
+ describe( 'onBeforeWorkerStart', () => {
100
+ it( 'registers and invokes handler with undefined payload', async () => {
101
+ const handler = vi.fn().mockResolvedValue( undefined );
102
+ onBeforeWorkerStart( handler );
103
+
104
+ expect( messageBusMock.on ).toHaveBeenCalledWith( BusEventType.WORKER_BEFORE_START, expect.any( Function ) );
105
+ await onHandlers[BusEventType.WORKER_BEFORE_START]();
106
+
107
+ expect( handler ).toHaveBeenCalledWith( undefined );
108
+ } );
109
+ } );
110
+
111
+ describe( 'onWorkflowStart', () => {
112
+ it( 'skips catalog workflow name', async () => {
113
+ const handler = vi.fn().mockResolvedValue( undefined );
114
+ onWorkflowStart( handler );
115
+
116
+ await Promise.resolve( onHandlers[BusEventType.WORKFLOW_START]( { id: '1', name: WORKFLOW_CATALOG } ) );
117
+ expect( handler ).not.toHaveBeenCalled();
118
+
119
+ await Promise.resolve( onHandlers[BusEventType.WORKFLOW_START]( { id: '2', name: 'myWorkflow' } ) );
120
+ expect( handler ).toHaveBeenCalledWith( { id: '2', name: 'myWorkflow' } );
121
+ } );
122
+ } );
123
+
124
+ describe( 'onWorkflowEnd', () => {
125
+ it( 'skips catalog workflow name', async () => {
126
+ const handler = vi.fn().mockResolvedValue( undefined );
127
+ onWorkflowEnd( handler );
128
+
129
+ await Promise.resolve( onHandlers[BusEventType.WORKFLOW_END]( {
130
+ id: '1',
131
+ name: WORKFLOW_CATALOG,
132
+ duration: 10
133
+ } ) );
134
+ expect( handler ).not.toHaveBeenCalled();
135
+
136
+ await Promise.resolve( onHandlers[BusEventType.WORKFLOW_END]( { id: '2', name: 'myWorkflow', duration: 5 } ) );
137
+ expect( handler ).toHaveBeenCalledWith( { id: '2', name: 'myWorkflow', duration: 5 } );
138
+ } );
139
+ } );
140
+
141
+ describe( 'onWorkflowError', () => {
142
+ it( 'skips catalog workflow name', async () => {
143
+ const handler = vi.fn().mockResolvedValue( undefined );
144
+ const err = new Error( 'wf' );
145
+ onWorkflowError( handler );
146
+
147
+ await Promise.resolve( onHandlers[BusEventType.WORKFLOW_ERROR]( {
148
+ id: '1',
149
+ name: WORKFLOW_CATALOG,
150
+ duration: 1,
151
+ error: err
152
+ } ) );
153
+ expect( handler ).not.toHaveBeenCalled();
154
+
155
+ await Promise.resolve( onHandlers[BusEventType.WORKFLOW_ERROR]( {
156
+ id: '2',
157
+ name: 'myWorkflow',
158
+ duration: 2,
159
+ error: err
160
+ } ) );
161
+ expect( handler ).toHaveBeenCalledWith( { id: '2', name: 'myWorkflow', duration: 2, error: err } );
162
+ } );
163
+ } );
164
+
165
+ describe( 'on', () => {
166
+ it( 'subscribes to external event channel and forwards payload', async () => {
167
+ const handler = vi.fn().mockResolvedValue( undefined );
168
+ on( 'myEvent', handler );
169
+
170
+ expect( messageBusMock.on ).toHaveBeenCalledWith( 'external:myEvent', expect.any( Function ) );
171
+ await onHandlers['external:myEvent']( { foo: 1 } );
172
+
173
+ expect( handler ).toHaveBeenCalledWith( { foo: 1 } );
174
+ } );
175
+ } );
176
+ } );