@outputai/core 0.10.0 → 0.10.1-next.09ed166.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 (45) hide show
  1. package/package.json +1 -1
  2. package/src/bus.js +58 -20
  3. package/src/bus.spec.js +135 -77
  4. package/src/consts.js +2 -1
  5. package/src/helpers/object.js +48 -35
  6. package/src/helpers/object.spec.js +123 -75
  7. package/src/hooks/index.d.ts +48 -45
  8. package/src/hooks/index.js +49 -44
  9. package/src/hooks/index.spec.js +74 -102
  10. package/src/interface/workflow.js +92 -85
  11. package/src/interface/workflow.spec.js +135 -142
  12. package/src/sdk/helpers/objects.d.ts +24 -19
  13. package/src/sdk/runtime/events.d.ts +4 -6
  14. package/src/sdk/runtime/events.js +2 -15
  15. package/src/sdk/runtime/events.spec.js +14 -92
  16. package/src/worker/global_functions.js +2 -2
  17. package/src/worker/global_functions.spec.js +3 -3
  18. package/src/worker/index.js +3 -3
  19. package/src/worker/index.spec.js +6 -6
  20. package/src/worker/interceptors/activity.js +4 -4
  21. package/src/worker/interceptors/activity.spec.js +9 -9
  22. package/src/worker/interceptors/workflow.js +5 -5
  23. package/src/worker/interceptors/workflow.spec.js +7 -7
  24. package/src/worker/loader/activities.js +56 -40
  25. package/src/worker/loader/activities.spec.js +21 -22
  26. package/src/worker/loader/workflows.spec.js +1 -2
  27. package/src/worker/log_hooks.js +9 -9
  28. package/src/worker/log_hooks.spec.js +2 -2
  29. package/src/worker/sinks.js +8 -8
  30. package/src/worker/sinks.spec.js +9 -9
  31. package/src/worker/webpack_loaders/consts.js +6 -0
  32. package/src/worker/webpack_loaders/tools.js +1 -146
  33. package/src/worker/webpack_loaders/tools.spec.js +0 -23
  34. package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.js +24 -32
  35. package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.spec.js +82 -279
  36. package/src/worker/webpack_loaders/workflow_rewriter/index.mjs +6 -7
  37. package/src/worker/webpack_loaders/workflow_rewriter/index.spec.js +57 -119
  38. package/src/worker/webpack_loaders/workflow_rewriter/rewrite_activity_calls.js +88 -0
  39. package/src/worker/webpack_loaders/workflow_rewriter/rewrite_activity_calls.spec.js +165 -0
  40. package/src/worker/webpack_loaders/workflow_validator/index.mjs +96 -29
  41. package/src/worker/webpack_loaders/workflow_validator/index.spec.js +110 -583
  42. package/src/worker/webpack_loaders/{npm_workflow_export_resolve.js → workflow_validator/npm_workflow_export_resolve.js} +1 -1
  43. package/src/worker/webpack_loaders/{npm_workflow_export_resolve.spec.js → workflow_validator/npm_workflow_export_resolve.spec.js} +1 -1
  44. package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.js +0 -193
  45. package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.spec.js +0 -123
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@outputai/core",
3
- "version": "0.10.0",
3
+ "version": "0.10.1-next.09ed166.0",
4
4
  "description": "The core module of the output framework",
5
5
  "type": "module",
6
6
  "engines": {
package/src/bus.js CHANGED
@@ -1,30 +1,68 @@
1
1
  import { EventEmitter } from 'node:events';
2
2
  import { randomUUID } from 'node:crypto';
3
- import { isPlainObject } from '#helpers/object';
3
+ import { Storage } from '#async_storage';
4
+ import { WORKFLOW_CATALOG } from '#consts';
4
5
 
5
- const emitter = new EventEmitter();
6
+ const mainEventBus = new EventEmitter();
7
+ const stepEventBus = new EventEmitter();
8
+
9
+ const getContext = () => {
10
+ const ctx = Storage.load();
11
+ if ( ctx ) {
12
+ const { outputActivityKind, activityInfo, workflowDetails } = ctx;
13
+ return { outputActivityKind, activityInfo, workflowDetails };
14
+ }
15
+ return {};
16
+ };
6
17
 
7
18
  /**
8
- * Every object payload emitted through `messageBus` is stamped with a UUID v4 `eventId`.
19
+ * Attaches information and filter out events from the bus by proxying .emit().
20
+ *
21
+ * If createEnvelope=true, wrap payload:
22
+ * ```js
23
+ * {
24
+ * eventId,
25
+ * eventDate,
26
+ * outputActivityKind,
27
+ * activityInfo,
28
+ * workflowDetails,
29
+ * payload: <original payload>
30
+ * }
31
+ * ```
32
+ *
33
+ * if createEnvelope=false, adds eventId and eventDate to the original payload object
34
+ *
35
+ * @param {object} args
36
+ * @param {object} args.bus The event emitter to proxy
37
+ * @param {boolean} args.createEnvelope Whether to wrap event around an envelope
38
+ * @param {function} args.filter Filter function to drop events
9
39
  */
10
- emitter.emit = new Proxy( emitter.emit, {
11
- apply( target, thisArg, args ) {
12
- const [ eventName, payload, ...extras ] = args;
13
- const newArguments = [];
14
-
15
- // do now push arguments that dont exist. if payload is undefined with len=1, means it was never defined
16
- // if payload is undefined with len>1, means user passed 'undefined' as argument
17
- if ( args.length > 1 ) {
18
- newArguments.push( isPlainObject( payload ) ? {
40
+ const proxyBus = ( { bus, createEnvelope = false, filter } ) => {
41
+ bus.emit = new Proxy( bus.emit, {
42
+ apply( target, thisArg, args ) {
43
+ const [ eventName, payload, ...rest ] = args;
44
+
45
+ const shouldEmit = filter?.( eventName, payload ) ?? true;
46
+ if ( !shouldEmit ) {
47
+ return false;
48
+ }
49
+
50
+ const eventFields = {
19
51
  eventId: randomUUID(),
20
- eventDate: Date.now(),
21
- ...payload
22
- } : payload );
23
- newArguments.push( ...extras );
52
+ eventDate: Date.now()
53
+ };
54
+
55
+ const newPayload = createEnvelope ? { ...eventFields, ...getContext(), payload } : { ...eventFields, ...payload };
56
+ return Reflect.apply( target, thisArg, [ eventName, newPayload, ...rest ] );
24
57
  }
58
+ } );
59
+ };
25
60
 
26
- return Reflect.apply( target, thisArg, [ eventName, ...newArguments ] );
27
- }
28
- } );
61
+ const catalogWfFilter = ( _, payload ) => payload?.workflowDetails?.workflowType !== WORKFLOW_CATALOG;
62
+
63
+ // Main bus is not used inside steps, so it doesn't need context
64
+ proxyBus( { bus: mainEventBus, createEnvelope: false, filter: catalogWfFilter } );
65
+ // This receives SDK and user events from within steps, so add context to it
66
+ proxyBus( { bus: stepEventBus, createEnvelope: true } );
29
67
 
30
- export const messageBus = emitter;
68
+ export { mainEventBus, stepEventBus };
package/src/bus.spec.js CHANGED
@@ -1,135 +1,193 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import { messageBus } from './bus.js';
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+
3
+ const loadMock = vi.hoisted( () => vi.fn() );
4
+
5
+ vi.mock( '#async_storage', () => ( {
6
+ Storage: { load: loadMock }
7
+ } ) );
8
+
9
+ import { mainEventBus, stepEventBus } from './bus.js';
3
10
 
4
11
  const UUID_V4_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
12
+ const ACTIVITY_CONTEXT = {
13
+ activityInfo: { activityId: 'activity-id' },
14
+ workflowDetails: { workflowId: 'workflow-id' },
15
+ outputActivityKind: 'step'
16
+ };
5
17
 
6
- describe( 'messageBus', () => {
18
+ describe( 'event buses', () => {
7
19
  beforeEach( () => {
8
- messageBus.removeAllListeners();
20
+ mainEventBus.removeAllListeners();
21
+ stepEventBus.removeAllListeners();
22
+ loadMock.mockReset();
9
23
  } );
10
24
 
11
- describe( 'eventId stamping', () => {
12
- it( 'stamps a UUID v4 eventId on every object payload', () => {
25
+ afterEach( () => {
26
+ vi.useRealTimers();
27
+ } );
28
+
29
+ describe( 'mainEventBus', () => {
30
+ it( 'adds event metadata to payload fields', () => {
13
31
  const handler = vi.fn();
14
- messageBus.on( 'test:event', handler );
32
+ const now = new Date( '2026-06-02T12:00:00.000Z' );
33
+ vi.useFakeTimers();
34
+ vi.setSystemTime( now );
35
+ mainEventBus.on( 'test:event', handler );
15
36
 
16
- messageBus.emit( 'test:event', { foo: 'bar' } );
37
+ mainEventBus.emit( 'test:event', { foo: 'bar' } );
17
38
 
18
- expect( handler ).toHaveBeenCalledWith( expect.objectContaining( {
19
- foo: 'bar',
20
- eventId: expect.stringMatching( UUID_V4_REGEX )
21
- } ) );
39
+ expect( handler ).toHaveBeenCalledWith( {
40
+ eventId: expect.stringMatching( UUID_V4_REGEX ),
41
+ eventDate: now.getTime(),
42
+ foo: 'bar'
43
+ } );
22
44
  } );
23
45
 
24
- it( 'gives distinct emits distinct eventIds', () => {
46
+ it( 'preserves caller-supplied event metadata', () => {
25
47
  const handler = vi.fn();
26
- messageBus.on( 'test:event', handler );
48
+ mainEventBus.on( 'test:event', handler );
27
49
 
28
- messageBus.emit( 'test:event', { i: 1 } );
29
- messageBus.emit( 'test:event', { i: 2 } );
50
+ mainEventBus.emit( 'test:event', { eventId: 'fixed-id', eventDate: 1234 } );
30
51
 
31
- const first = handler.mock.calls[0][0].eventId;
32
- const second = handler.mock.calls[1][0].eventId;
33
- expect( first ).toMatch( UUID_V4_REGEX );
34
- expect( second ).toMatch( UUID_V4_REGEX );
35
- expect( first ).not.toBe( second );
52
+ expect( handler ).toHaveBeenCalledWith( {
53
+ eventId: 'fixed-id',
54
+ eventDate: 1234
55
+ } );
36
56
  } );
37
57
 
38
- it( 'preserves a caller-supplied eventId (deterministic retry case)', () => {
58
+ it( 'does not mutate the caller-supplied payload', () => {
39
59
  const handler = vi.fn();
40
- messageBus.on( 'test:event', handler );
60
+ const payload = { foo: 'bar' };
61
+ mainEventBus.on( 'test:event', handler );
41
62
 
42
- messageBus.emit( 'test:event', { eventId: 'fixed-id', foo: 'bar' } );
63
+ mainEventBus.emit( 'test:event', payload );
43
64
 
44
- expect( handler ).toHaveBeenCalledWith( expect.objectContaining( {
45
- eventId: 'fixed-id',
46
- foo: 'bar'
65
+ expect( handler.mock.calls[0][0] ).not.toBe( payload );
66
+ expect( payload ).toEqual( { foo: 'bar' } );
67
+ } );
68
+
69
+ it( 'does not attach activity context', () => {
70
+ const handler = vi.fn();
71
+ loadMock.mockReturnValue( {
72
+ activityInfo: { activityId: 'activity-id' },
73
+ workflowDetails: { workflowId: 'workflow-id' },
74
+ outputActivityKind: 'step'
75
+ } );
76
+ mainEventBus.on( 'test:event', handler );
77
+
78
+ mainEventBus.emit( 'test:event', { foo: 'bar' } );
79
+
80
+ expect( handler ).toHaveBeenCalledWith( expect.not.objectContaining( {
81
+ activityInfo: expect.anything(),
82
+ workflowDetails: expect.anything(),
83
+ outputActivityKind: expect.anything()
47
84
  } ) );
85
+ expect( loadMock ).not.toHaveBeenCalled();
48
86
  } );
49
87
 
50
- it( 'stamps eventDate on every object payload', () => {
88
+ it( 'filters catalog workflow events', () => {
51
89
  const handler = vi.fn();
52
- const now = new Date( '2026-06-02T12:00:00.000Z' );
53
- vi.useFakeTimers();
54
- vi.setSystemTime( now );
55
- messageBus.on( 'test:event', handler );
90
+ mainEventBus.on( 'workflow:start', handler );
56
91
 
57
- messageBus.emit( 'test:event', { foo: 'bar' } );
92
+ const emitted = mainEventBus.emit( 'workflow:start', {
93
+ workflowDetails: { workflowType: '$catalog' }
94
+ } );
58
95
 
59
- expect( handler ).toHaveBeenCalledWith( expect.objectContaining( {
60
- eventDate: now.getTime(),
61
- foo: 'bar'
62
- } ) );
96
+ expect( emitted ).toBe( false );
97
+ expect( handler ).not.toHaveBeenCalled();
98
+ } );
99
+ } );
63
100
 
64
- vi.useRealTimers();
101
+ describe( 'stepEventBus', () => {
102
+ beforeEach( () => {
103
+ loadMock.mockReturnValue( ACTIVITY_CONTEXT );
65
104
  } );
66
105
 
67
- it( 'preserves a caller-supplied eventDate', () => {
106
+ it( 'wraps payloads with event metadata and activity context', () => {
68
107
  const handler = vi.fn();
69
- messageBus.on( 'test:event', handler );
108
+ const payload = { foo: 'bar' };
109
+ stepEventBus.on( 'test:event', handler );
70
110
 
71
- messageBus.emit( 'test:event', { eventDate: 1234, foo: 'bar' } );
111
+ stepEventBus.emit( 'test:event', payload );
72
112
 
73
- expect( handler ).toHaveBeenCalledWith( expect.objectContaining( {
74
- eventDate: 1234,
75
- foo: 'bar'
76
- } ) );
113
+ expect( handler ).toHaveBeenCalledWith( {
114
+ eventId: expect.stringMatching( UUID_V4_REGEX ),
115
+ eventDate: expect.any( Number ),
116
+ ...ACTIVITY_CONTEXT,
117
+ payload
118
+ } );
77
119
  } );
78
120
 
79
- it( 'does not mutate the caller-supplied payload object', () => {
121
+ it( 'omits activity fields outside activity context', () => {
80
122
  const handler = vi.fn();
81
- messageBus.on( 'test:event', handler );
123
+ loadMock.mockReturnValue( undefined );
124
+ stepEventBus.on( 'test:event', handler );
82
125
 
83
- const payload = { foo: 'bar' };
84
- messageBus.emit( 'test:event', payload );
126
+ stepEventBus.emit( 'test:event', { foo: 'bar' } );
85
127
 
86
- expect( payload ).not.toHaveProperty( 'eventId' );
87
- expect( payload ).not.toHaveProperty( 'eventDate' );
128
+ expect( handler ).toHaveBeenCalledWith( {
129
+ eventId: expect.stringMatching( UUID_V4_REGEX ),
130
+ eventDate: expect.any( Number ),
131
+ payload: { foo: 'bar' }
132
+ } );
88
133
  } );
89
- } );
90
134
 
91
- describe( 'pass-through behavior', () => {
92
- it( 'passes primitive payloads through unchanged', () => {
135
+ it( 'preserves arbitrary payloads unchanged inside the envelope', () => {
93
136
  const handler = vi.fn();
94
- messageBus.on( 'test:event', handler );
95
-
96
- messageBus.emit( 'test:event', 'a-string' );
97
- messageBus.emit( 'test:event', 42 );
98
- messageBus.emit( 'test:event', true );
99
-
100
- expect( handler ).toHaveBeenNthCalledWith( 1, 'a-string' );
101
- expect( handler ).toHaveBeenNthCalledWith( 2, 42 );
102
- expect( handler ).toHaveBeenNthCalledWith( 3, true );
137
+ const array = [ 1, 2, 3 ];
138
+ const instance = new Date();
139
+ stepEventBus.on( 'test:event', handler );
140
+
141
+ stepEventBus.emit( 'test:event', 'value' );
142
+ stepEventBus.emit( 'test:event', array );
143
+ stepEventBus.emit( 'test:event', instance );
144
+ stepEventBus.emit( 'test:event' );
145
+
146
+ expect( handler.mock.calls[0][0].payload ).toBe( 'value' );
147
+ expect( handler.mock.calls[1][0].payload ).toBe( array );
148
+ expect( handler.mock.calls[2][0].payload ).toBe( instance );
149
+ expect( handler.mock.calls[3][0] ).toHaveProperty( 'payload', undefined );
103
150
  } );
104
151
 
105
- it( 'passes null and undefined payloads through unchanged', () => {
152
+ it( 'keeps payload metadata separate from envelope metadata', () => {
106
153
  const handler = vi.fn();
107
- messageBus.on( 'test:event', handler );
154
+ const payload = { eventId: 'payload-id', eventDate: 1234 };
155
+ stepEventBus.on( 'test:event', handler );
108
156
 
109
- messageBus.emit( 'test:event', null );
110
- messageBus.emit( 'test:event' );
157
+ stepEventBus.emit( 'test:event', payload );
111
158
 
112
- expect( handler ).toHaveBeenNthCalledWith( 1, null );
113
- expect( handler ).toHaveBeenNthCalledWith( 2 );
159
+ const envelope = handler.mock.calls[0][0];
160
+ expect( envelope.eventId ).toMatch( UUID_V4_REGEX );
161
+ expect( envelope.eventId ).not.toBe( payload.eventId );
162
+ expect( envelope.eventDate ).not.toBe( payload.eventDate );
163
+ expect( envelope.payload ).toBe( payload );
114
164
  } );
115
165
 
116
- it( 'passes array payloads through unchanged (no key injection)', () => {
166
+ it( 'gives distinct emits distinct eventIds', () => {
117
167
  const handler = vi.fn();
118
- messageBus.on( 'test:event', handler );
168
+ stepEventBus.on( 'test:event', handler );
119
169
 
120
- messageBus.emit( 'test:event', [ 1, 2, 3 ] );
170
+ stepEventBus.emit( 'test:event', { i: 1 } );
171
+ stepEventBus.emit( 'test:event', { i: 2 } );
121
172
 
122
- expect( handler ).toHaveBeenCalledWith( [ 1, 2, 3 ] );
173
+ const first = handler.mock.calls[0][0].eventId;
174
+ const second = handler.mock.calls[1][0].eventId;
175
+ expect( first ).toMatch( UUID_V4_REGEX );
176
+ expect( second ).toMatch( UUID_V4_REGEX );
177
+ expect( first ).not.toBe( second );
123
178
  } );
124
179
 
125
- it( 'forwards additional positional args untouched', () => {
180
+ it( 'forwards additional positional arguments unchanged', () => {
126
181
  const handler = vi.fn();
127
- messageBus.on( 'test:event', handler );
182
+ stepEventBus.on( 'test:event', handler );
128
183
 
129
- messageBus.emit( 'test:event', { foo: 'bar' }, 'extra', 99 );
184
+ stepEventBus.emit( 'test:event', { foo: 'bar' }, 'extra', 99 );
130
185
 
131
186
  expect( handler ).toHaveBeenCalledWith(
132
- expect.objectContaining( { foo: 'bar', eventId: expect.stringMatching( UUID_V4_REGEX ) } ),
187
+ expect.objectContaining( {
188
+ eventId: expect.stringMatching( UUID_V4_REGEX ),
189
+ payload: { foo: 'bar' }
190
+ } ),
133
191
  'extra',
134
192
  99
135
193
  );
package/src/consts.js CHANGED
@@ -4,11 +4,12 @@ export const ACTIVITY_SEND_HTTP_REQUEST = '__internal#sendHttpRequest';
4
4
  export const ACTIVITY_WRAPPER_VERSION_FIELD = '__output_activity_wrapper_version';
5
5
  export const ACTIVITY_LOGGER_SYMBOL = Symbol.for( '__activity_logger' );
6
6
  export const METADATA_ACCESS_SYMBOL = Symbol( '__metadata' );
7
- export const SHARED_STEP_PREFIX = '$shared';
8
7
  export const WORKFLOW_CATALOG = '$catalog';
9
8
  export const WORKFLOWS_INDEX_FILENAME = '__workflows_entrypoint.js';
10
9
  export const WORKFLOW_WRAPPER_VERSION_FIELD = '__output_workflow_wrapper_version';
11
10
 
11
+ export const INVOKE_ACTIVITY_SYMBOL = Symbol.for( '@outputai/core:__invoke_activity' );
12
+
12
13
  export const ComponentType = {
13
14
  EVALUATOR: 'evaluator',
14
15
  INTERNAL_STEP: 'internal_step',
@@ -24,58 +24,71 @@ export const clone = v => {
24
24
  };
25
25
 
26
26
  /**
27
- * Creates a new object merging object "b" onto object "a", using a resolver function to define the value to keep.
28
- * - Object "b" fields that also exists on "a" will have their value defined by the "resolver" function
29
- * - Object "b" fields that don't exist on object "a" will be added;
30
- * - Object "a" fields that don't exist on object "b" will be preserved;
27
+ * Creates a new object recursively merging the rightmost object over the previous one using a resolver function.
28
+ * Give two objects, (L)eft and (R)right
29
+ * - Object "R" will overwrite fields on object "L" based on the result of the resolver function;
30
+ * - Object "R" fields that don't exist on object "L" will be added;
31
+ * - Object "L" fields that don't exist on object "R" will be preserved;
31
32
  *
32
- * If "b" isn't an object, a new object equal to "a" is returned
33
+ * If "R" isn't an object, a new object equal to "L" is returned.
33
34
  *
34
- * @param {object} a - The base object
35
- * @param {object} b - The target object
36
- * @param {function} resolver - A function that return the value to be kept. First argument is value a, second is value b
35
+ * The resolver function will define the final value of the merge, it receives two args value "L" and "R".
36
+ *
37
+ *
38
+ * @param {object} base - The base object
39
+ * @param {...(object|function)} args - Target objects followed by the resolver function
37
40
  * @returns {object} A new object
38
41
  */
39
- export const deepMergeWithResolver = ( a, b, resolver ) => {
40
- if ( !isPlainObject( a ) ) {
41
- throw new Error( 'Parameter "a" is not an object.' );
42
+ export const deepMergeWithResolver = ( base, ...args ) => {
43
+ const objects = args.slice( 0, -1 );
44
+ const resolver = args.at( -1 );
45
+
46
+ if ( !isPlainObject( base ) ) {
47
+ throw new Error( 'First argument (base object) is not an object.' );
42
48
  }
43
- if ( !isPlainObject( b ) ) {
44
- return clone( a );
49
+
50
+ if ( typeof resolver !== 'function' ) {
51
+ throw new Error( 'Last argument (resolver) is not a function.' );
45
52
  }
46
- return Object.entries( b ).reduce( ( obj, [ k, v ] ) =>
47
- Object.assign( obj, {
48
- [k]: ( () => {
49
- if ( isPlainObject( v ) && isPlainObject( a[k] ) ) {
50
- return deepMergeWithResolver( a[k], v, resolver );
51
- }
52
- if ( Object.hasOwn( a, k ) ) {
53
- return resolver( a[k], v );
54
- }
55
- return v;
56
- } )()
57
- } )
58
- , clone( a ) );
53
+
54
+ return objects.reduce( ( merged, object ) => {
55
+ if ( !isPlainObject( object ) ) {
56
+ return merged;
57
+ }
58
+
59
+ for ( const [ k, v ] of Object.entries( object ) ) {
60
+ if ( isPlainObject( v ) && isPlainObject( merged[k] ) ) {
61
+ merged[k] = deepMergeWithResolver( merged[k], v, resolver );
62
+ } else if ( Object.hasOwn( merged, k ) ) {
63
+ merged[k] = resolver( merged[k], v );
64
+ } else {
65
+ merged[k] = v;
66
+ }
67
+ }
68
+ return merged;
69
+ }, clone( base ) );
59
70
  };
60
71
 
61
72
  /**
62
- * Creates a new object merging object "b" onto object "a" biased to "b":
63
- * - Object "b" will overwrite fields on object "a";
64
- * - Object "b" fields that don't exist on object "a" will be added;
65
- * - Object "a" fields that don't exist on object "b" will be preserved;
73
+ * Creates a new object recursively merging the rightmost object over the previous one.
74
+ * Give two objects, (L)eft and (R)right
75
+ * - Object "R" will overwrite fields on object "L";
76
+ * - Object "R" fields that don't exist on object "L" will be added;
77
+ * - Object "L" fields that don't exist on object "R" will be preserved;
66
78
  *
67
- * If "b" isn't an object, a new object equal to "a" is returned
79
+ * If "R" isn't an object, a new object equal to "L" is returned.
68
80
  *
69
- * @param {object} a - The base object
70
- * @param {object} b - The target object
81
+ * @param {object} base - The base object
82
+ * @param {...object} rest - The target objects
71
83
  * @returns {object} A new object
72
84
  */
73
- export const deepMerge = ( a, b ) => deepMergeWithResolver( a, b, ( _, b ) => b );
85
+ export const deepMerge = ( base, ...rest ) =>
86
+ deepMergeWithResolver( base, ...rest, ( _, b ) => b );
74
87
 
75
88
  /**
76
89
  * Adds an non-writable, non-configurable and non-enumerable property to an object
77
90
  * @param {object} obj
78
- * @param {string} key
91
+ * @param {string|Symbol} key
79
92
  * @param {any} value
80
93
  * @returns
81
94
  */