@outputai/core 0.8.1-next.e92f632.0 → 0.8.2-dev.e78f6b4.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 (102) hide show
  1. package/package.json +9 -10
  2. package/src/bus.js +1 -1
  3. package/src/consts.js +5 -2
  4. package/src/helpers/component.js +12 -0
  5. package/src/helpers/component.spec.js +54 -0
  6. package/src/helpers/fetch.js +105 -0
  7. package/src/helpers/fetch.spec.js +203 -0
  8. package/src/helpers/function.js +15 -0
  9. package/src/helpers/function.spec.js +48 -0
  10. package/src/helpers/object.js +98 -0
  11. package/src/helpers/object.spec.js +377 -0
  12. package/src/helpers/promise.js +29 -0
  13. package/src/helpers/promise.spec.js +35 -0
  14. package/src/helpers/string.js +30 -0
  15. package/src/helpers/string.spec.js +64 -0
  16. package/src/interface/evaluator.js +14 -12
  17. package/src/interface/evaluator.spec.js +10 -6
  18. package/src/interface/index.d.ts +7 -6
  19. package/src/interface/index.js +2 -0
  20. package/src/interface/logger.d.ts +53 -0
  21. package/src/interface/logger.js +68 -0
  22. package/src/interface/logger.spec.js +138 -0
  23. package/src/interface/step.js +15 -12
  24. package/src/interface/step.spec.js +10 -6
  25. package/src/interface/webhook.d.ts +21 -2
  26. package/src/interface/workflow.js +85 -78
  27. package/src/interface/workflow.spec.js +11 -4
  28. package/src/internal_activities/index.js +38 -35
  29. package/src/internal_activities/index.spec.js +27 -4
  30. package/src/logger/development.js +2 -2
  31. package/src/logger/development.spec.js +19 -2
  32. package/src/logger/production.js +1 -1
  33. package/src/logger/production.spec.js +24 -5
  34. package/src/sdk/README.md +47 -0
  35. package/src/sdk/helpers/component_metadata.d.ts +17 -0
  36. package/src/sdk/helpers/component_metadata.js +6 -0
  37. package/src/sdk/helpers/component_metadata.spec.js +30 -0
  38. package/src/sdk/helpers/index.d.ts +12 -0
  39. package/src/sdk/helpers/index.js +3 -0
  40. package/src/sdk/helpers/objects.d.ts +51 -0
  41. package/src/sdk/helpers/objects.js +8 -0
  42. package/src/sdk/helpers/objects.spec.js +16 -0
  43. package/src/sdk/helpers/path.d.ts +11 -0
  44. package/src/sdk/helpers/path.js +32 -0
  45. package/src/{utils/resolve_invocation_dir.spec.js → sdk/helpers/path.spec.js} +9 -9
  46. package/src/sdk/runtime/context.d.ts +30 -0
  47. package/src/sdk/runtime/context.js +15 -0
  48. package/src/{activity_integration → sdk/runtime}/context.spec.js +5 -5
  49. package/src/sdk/runtime/events.d.ts +15 -0
  50. package/src/sdk/runtime/events.js +18 -0
  51. package/src/{activity_integration → sdk/runtime}/events.spec.js +8 -9
  52. package/src/sdk/runtime/index.d.ts +12 -0
  53. package/src/sdk/runtime/index.js +3 -0
  54. package/src/sdk/runtime/tracing.d.ts +46 -0
  55. package/src/sdk/runtime/tracing.js +11 -0
  56. package/src/tracing/processors/s3/redis_client.spec.js +0 -6
  57. package/src/tracing/processors/s3/s3_client.spec.js +0 -6
  58. package/src/tracing/trace_engine.js +1 -1
  59. package/src/worker/catalog_workflow/catalog_job.js +1 -1
  60. package/src/worker/catalog_workflow/index.spec.js +8 -11
  61. package/src/worker/configs.js +14 -1
  62. package/src/worker/configs.spec.js +34 -1
  63. package/src/worker/connection_monitor.js +3 -14
  64. package/src/worker/connection_monitor.spec.js +4 -21
  65. package/src/worker/global_functions.js +14 -0
  66. package/src/worker/global_functions.spec.js +55 -0
  67. package/src/worker/index.js +11 -3
  68. package/src/worker/index.spec.js +29 -1
  69. package/src/worker/interceptors/activity.js +2 -2
  70. package/src/worker/interceptors/workflow.js +3 -3
  71. package/src/worker/interceptors/workflow.spec.js +1 -1
  72. package/src/worker/loader/matchers.js +1 -1
  73. package/src/worker/loader/tools.js +1 -1
  74. package/src/worker/loader/tools.spec.js +1 -1
  75. package/src/worker/log_hooks.js +14 -0
  76. package/src/worker/log_hooks.spec.js +83 -2
  77. package/src/worker/sinks.js +7 -1
  78. package/src/worker/sinks.spec.js +203 -0
  79. package/src/activity_integration/context.d.ts +0 -23
  80. package/src/activity_integration/context.js +0 -18
  81. package/src/activity_integration/event_id_integration.spec.js +0 -52
  82. package/src/activity_integration/events.d.ts +0 -10
  83. package/src/activity_integration/events.js +0 -15
  84. package/src/activity_integration/index.d.ts +0 -9
  85. package/src/activity_integration/index.js +0 -3
  86. package/src/activity_integration/tracing.d.ts +0 -40
  87. package/src/activity_integration/tracing.js +0 -48
  88. package/src/utils/index.d.ts +0 -180
  89. package/src/utils/index.js +0 -2
  90. package/src/utils/resolve_invocation_dir.js +0 -34
  91. package/src/utils/utils.js +0 -334
  92. package/src/utils/utils.spec.js +0 -723
  93. /package/src/{internal_utils → helpers}/aggregations.js +0 -0
  94. /package/src/{internal_utils → helpers}/aggregations.spec.js +0 -0
  95. /package/src/{internal_utils → helpers}/errors.js +0 -0
  96. /package/src/{internal_utils → helpers}/errors.spec.js +0 -0
  97. /package/src/{internal_utils → helpers}/temporal_context.js +0 -0
  98. /package/src/{internal_utils → helpers}/temporal_context.spec.ts +0 -0
  99. /package/src/{internal_utils → helpers}/trace_info.js +0 -0
  100. /package/src/{internal_utils → helpers}/trace_info.spec.js +0 -0
  101. /package/src/{internal_utils → helpers}/workflow_context.js +0 -0
  102. /package/src/{internal_utils → helpers}/workflow_context.spec.js +0 -0
@@ -1,40 +0,0 @@
1
- import type { Attribute } from '#trace_attribute';
2
-
3
- export { Attribute } from '#trace_attribute';
4
- /**
5
- * Creates a new event.
6
- *
7
- * @param args
8
- * @param args.id - A unique id for the Event.
9
- * @param args.kind - The kind of Event, like HTTP, DiskWrite, DBOp, etc.
10
- * @param args.name - The human-friendly name of the Event: query, request, create.
11
- * @param args.details - Arbitrary data to add to this event, it will be used as the "input" field.
12
- */
13
- export declare function addEventStart( args: { id: string; kind: string; name: string; details: unknown } ): void;
14
-
15
- /**
16
- * Concludes an event.
17
- *
18
- * @param args
19
- * @param args.id - The id of the event to conclude.
20
- * @param args.details - Arbitrary data to add to this event, it will be used as the "output" field.
21
- */
22
- export declare function addEventEnd( args: { id: string; details: unknown } ): void;
23
-
24
- /**
25
- * Concludes an event with an error.
26
- *
27
- * @param args
28
- * @param args.id - The id of the event to conclude.
29
- * @param args.details - Arbitrary data to add to this event, it will be used as the "error" field.
30
- */
31
- export declare function addEventError( args: { id: string; details: unknown } ): void;
32
-
33
- /**
34
- * Adds an attribute to an event.
35
- *
36
- * @param args
37
- * @param args.eventId - The id of the event to attach the attribute to.
38
- * @param args.attribute - The attribute to attach to the event.
39
- */
40
- export declare function addEventAttribute( args: { eventId: string; attribute: Attribute.Instance } ): void;
@@ -1,48 +0,0 @@
1
- import { addEventActionWithContext, EventAction } from '#tracing';
2
-
3
- export { Attribute } from '#trace_attribute';
4
-
5
- /**
6
- * Creates a new event.
7
- *
8
- * @param {object} args
9
- * @param {string} args.id - A unique id for the Event.
10
- * @param {string} args.kind - The kind of Event, like HTTP, DiskWrite, DBOp, etc.
11
- * @param {string} args.name - The human-friendly name of the Event: query, request, create.
12
- * @param {object} args.details - Arbitrary data to add to this event, it will be used as the "input" field.
13
- * @returns {void}
14
- */
15
- export const addEventStart = ( { id, kind, name, details } ) =>
16
- addEventActionWithContext( EventAction.START, { kind, name, details, id } );
17
-
18
- /**
19
- * Concludes an event.
20
- *
21
- * @param {object} args
22
- * @param {string} args.id - The id of the event to conclude.
23
- * @param {object} args.details - Arbitrary data to add to this event, it will be used as the "output" field.
24
- * @returns {void}
25
- */
26
- export const addEventEnd = ( { id, details } ) => addEventActionWithContext( EventAction.END, { id, details } );
27
-
28
- /**
29
- * Concludes an event with an error.
30
- *
31
- * @param {object} args
32
- * @param {string} args.id - The id of the event to conclude.
33
- * @param {object} args.details - Arbitrary data to add to this event, it will be used as the "error" field.
34
- * @returns {void}
35
- */
36
- export const addEventError = ( { id, details } ) => addEventActionWithContext( EventAction.ERROR, { id, details } );
37
-
38
- /**
39
- * Adds an attribute to an event.
40
- *
41
- * @param {object} args
42
- * @param {string} args.eventId - The id of the event to attach the attribute to.
43
- * @param {string} args.name - The attribute name
44
- * @param {unknown} args.value - The attribute value
45
- * @returns {void}
46
- */
47
- export const addEventAttribute = ( { eventId, attribute } ) =>
48
- addEventActionWithContext( EventAction.ADD_ATTR, { id: eventId, details: attribute } );
@@ -1,180 +0,0 @@
1
- /**
2
- * > [!WARNING]
3
- * > **Internal use only.** Not part of the public API; may change without notice.
4
- *
5
- * @packageDocumentation
6
- */
7
-
8
- /**
9
- * Return the first immediate directory of the file invoking the code that called this function.
10
- *
11
- * Excludes `@outputai/core`, node, and other internal paths.
12
- */
13
- export function resolveInvocationDir(): string;
14
-
15
- /**
16
- * Node safe clone implementation that doesn't use global structuredClone().
17
- *
18
- * Returns a cloned version of the object.
19
- *
20
- * Only clones static properties. Getters become static properties.
21
- *
22
- * @param object
23
- */
24
- export function clone( object: object ): object;
25
-
26
- /**
27
- * Receives an error as argument and throws it.
28
- *
29
- * @param error
30
- * @throws {Error}
31
- */
32
- export function throws( error: Error ): void;
33
-
34
- /**
35
- * Attach given value to an object with the METADATA_ACCESS_SYMBOL symbol as key.
36
- *
37
- * @param target
38
- * @param value
39
- * @returns
40
- */
41
- export function setMetadata( target: object, value: object ): void;
42
-
43
- /**
44
- * Read metadata previously attached via setMetadata.
45
- *
46
- * @param target - The function or object to read metadata from.
47
- * @returns The metadata object, or null if none is attached.
48
- */
49
- export function getMetadata( target: Function ): { name: string; description?: string; type?: string } | null;
50
-
51
- /** Represents a {Response} serialized to plain object */
52
- export type SerializedFetchResponse = {
53
- /** The response url */
54
- url: string,
55
-
56
- /** The response status code */
57
- status: number,
58
-
59
- /** The response status text */
60
- statusText: string,
61
-
62
- /** Flag indicating if the request succeeded */
63
- ok: boolean,
64
-
65
- /** Object with response headers */
66
- headers: Record<string, string>,
67
-
68
- /** Response body, either JSON, text or arrayBuffer converter to base64 */
69
- body: object | string
70
- };
71
-
72
- /**
73
- * Consumes an HTTP `Response` and serializes it to a plain object.
74
- *
75
- * @param response - The response to serialize.
76
- * @returns SerializedFetchResponse
77
- */
78
- export function serializeFetchResponse( response: Response ): SerializedFetchResponse;
79
-
80
- export type SerializedBodyAndContentType = {
81
- /** The body as a string when possible; otherwise the original value */
82
- body: string | unknown,
83
- /** The inferred `Content-Type` header value, if any */
84
- contentType: string | undefined
85
- };
86
-
87
- /**
88
- * Serializes a payload for use as a fetch POST body and infers its `Content-Type`.
89
- *
90
- * @param body - The payload to serialize.
91
- * @returns The serialized body and inferred `Content-Type`.
92
- */
93
- export function serializeBodyAndInferContentType( body: unknown ): SerializedBodyAndContentType;
94
-
95
- /**
96
- * Returns true if the value is a plain object:
97
- * - `{}`
98
- * - `new Object()`
99
- * - `Object.create(null)`
100
- *
101
- * @param object - The value to check.
102
- * @returns Whether the value is a plain object.
103
- */
104
- export function isPlainObject( object: unknown ): boolean;
105
-
106
- /**
107
- * Returns a copy of an array with its content shuffled.
108
- *
109
- * @param arr - The array to shuffle
110
- * @returns A shuffled array copy
111
- */
112
- export function shuffleArray( arr: unknown[] ): unknown[];
113
-
114
- /**
115
- * Creates a new object by merging object `b` onto object `a`, biased toward `b`:
116
- * - Fields in `b` overwrite fields in `a`.
117
- * - Fields in `b` that don't exist in `a` are created.
118
- * - Fields in `a` that don't exist in `b` are left unchanged.
119
- *
120
- * @param a - The base object.
121
- * @param b - The overriding object.
122
- * @throws {Error} If either `a` or `b` is not a plain object.
123
- * @returns A new merged object.
124
- */
125
- export function deepMerge( a: object, b: object | null | undefined ): object;
126
-
127
- /**
128
- * Creates a new object by merging object `b` onto object `a`, biased toward `b`:
129
- * - Fields in `b` that don't exist in `a` are created.
130
- * - Fields in `a` that don't exist in `b` are left unchanged.
131
- * - Fields in `a` and `b` are passed as arguments to the resolve function (a,b) and its return assigns the new value.
132
- *
133
- * @param a - The base object.
134
- * @param b - The overriding object.
135
- * @param resolver - The resolver function.
136
- * @throws {Error} If either `a` or `b` is not a plain object.
137
- * @returns A new merged object.
138
- */
139
- export function deepMergeWithResolver( a: object, b: object | null | undefined, resolver: function ): object;
140
-
141
- /**
142
- * Shortens a UUID to a url-safe base64-like string (custom 64-char alphabet).
143
- * Temporal-friendly: no Buffer or crypto; safe to use inside workflows.
144
- *
145
- * @param uuid - Standard UUID (e.g. `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`).
146
- * @returns Short string using A–Z, a–z, 0–9, `_`, `-` (typically 21–22 chars).
147
- */
148
- export function toUrlSafeBase64( uuid: string ): string;
149
-
150
- /**
151
- * Similar to native Promise.allSettled, but rejects with `{ isTimeout: true }`
152
- * if the execution exceeds the given timeout.
153
- *
154
- * @param promises - Values or promises to wait for.
155
- * @param timeoutMs - Maximum wait time in milliseconds.
156
- * @returns Native Promise.allSettled-style results.
157
- */
158
- export function allSettledWithTimeout<T>(
159
- promises: Array<T | PromiseLike<T>>,
160
- timeoutMs: number
161
- ): Promise<PromiseSettledResult<Awaited<T>>[]>;
162
-
163
- /**
164
- * Promise wrapper that can be resolved externally.
165
- */
166
- export class CancellablePromise {
167
- /** The internal promise */
168
- readonly promise: Promise<void>;
169
- /** Whether the promise is already resolved or not */
170
- readonly completed: boolean;
171
- /** Resolves the promise */
172
- complete(): void;
173
- }
174
-
175
- /**
176
- * Returns a function that invokes the wrapped function once.
177
- */
178
- export function runOnce<Args extends unknown[], Return>(
179
- fn: ( ...args: Args ) => Return
180
- ): ( ...args: Args ) => Return;
@@ -1,2 +0,0 @@
1
- export { default as resolveInvocationDir } from './resolve_invocation_dir.js';
2
- export * from './utils.js';
@@ -1,34 +0,0 @@
1
- import * as stackTraceParser from 'stacktrace-parser';
2
-
3
- // OS separator, but in a deterministic way, allowing this to work in Temporal's sandbox
4
- // This avoids importing from node:path
5
- const SEP = new Error().stack.includes( '/' ) ? '/' : '\\';
6
-
7
- const transformSeparators = path => path.replaceAll( '/', SEP );
8
- const defaultIgnorePaths = [
9
- '/@outputai/core/',
10
- '/@outputai/llm/',
11
- '/@outputai/evals/',
12
- '/sdk/core/',
13
- '/sdk/llm/',
14
- '/sdk/evals/',
15
- 'node:internal/',
16
- 'evalmachine.',
17
- 'webpack/bootstrap'
18
- ];
19
-
20
- /**
21
- * Return the directory of the file invoking the code that called this function
22
- * Excludes some internal paths and the sdk itself
23
- */
24
- export default ( additionalIgnorePaths = [] ) => {
25
- const stack = new Error().stack;
26
- const lines = stackTraceParser.parse( stack );
27
- const ignorePaths = [ ...additionalIgnorePaths, ...defaultIgnorePaths ].map( transformSeparators );
28
-
29
- const frame = lines.find( l => !ignorePaths.some( p => l.file.includes( p ) ) );
30
- if ( !frame ) {
31
- throw new Error( `Invocation dir resolution via stack trace failed. Stack: ${stack}` );
32
- }
33
- return frame.file.replace( 'file://', '' ).split( SEP ).slice( 0, -1 ).join( SEP );
34
- };
@@ -1,334 +0,0 @@
1
- import { METADATA_ACCESS_SYMBOL } from '#consts';
2
-
3
- /**
4
- * Node safe clone implementation that doesn't use global structuredClone()
5
- * @param {object} v
6
- * @returns {object}
7
- */
8
- export const clone = v => {
9
- try {
10
- return JSON.parse( JSON.stringify( v ) );
11
- } catch {
12
- return v;
13
- }
14
- };
15
-
16
- /**
17
- * Detect a JS plain object.
18
- *
19
- * @param {unknown} v
20
- * @returns {boolean}
21
- */
22
- export const isPlainObject = v =>
23
- typeof v === 'object' &&
24
- !Array.isArray( v ) &&
25
- v !== null &&
26
- [ Object.prototype, null ].includes( Object.getPrototypeOf( v ) );
27
-
28
- /**
29
- * Throw given error
30
- * @param {Error} e
31
- * @throws {e}
32
- */
33
- export const throws = e => {
34
- throw e;
35
- };
36
-
37
- /**
38
- * Add metadata "values" property to a given object
39
- * @param {object} target
40
- * @param {object} values
41
- * @returns
42
- */
43
- export const setMetadata = ( target, values ) =>
44
- Object.defineProperty( target, METADATA_ACCESS_SYMBOL, { value: values, writable: false, enumerable: false, configurable: false } );
45
-
46
- /**
47
- * Read metadata previously attached via setMetadata
48
- * @param {Function} target
49
- * @returns {object|null}
50
- */
51
- export const getMetadata = target => target[METADATA_ACCESS_SYMBOL] ?? null;
52
-
53
- /**
54
- * Returns true if string value is stringbool and true
55
- * @param {string} v
56
- * @returns
57
- */
58
- export const isStringboolTrue = v => [ '1', 'true', 'on' ].includes( v );
59
-
60
- /**
61
- * Consume Fetch's HTTP Response and return a serialized version of it;
62
- *
63
- * @param {Response} response
64
- * @returns {object} Serialized response
65
- */
66
- export const serializeFetchResponse = async response => {
67
- const headers = Object.fromEntries( response.headers );
68
- const contentType = headers['content-type'] || '';
69
-
70
- const body = await ( async () => {
71
- if ( contentType.includes( 'application/json' ) ) {
72
- return response.json();
73
- }
74
- if ( contentType.startsWith( 'text/' ) ) {
75
- return response.text();
76
- }
77
- return response.arrayBuffer().then( buf => Buffer.from( buf ).toString( 'base64' ) );
78
- } )();
79
-
80
- return {
81
- url: response.url,
82
- status: response.status,
83
- statusText: response.statusText,
84
- ok: response.ok,
85
- headers,
86
- body
87
- };
88
- };
89
-
90
- /**
91
- * Duck-typing to detect a Node Readable (Stream) without importing anything
92
- *
93
- * @param {unknown} v
94
- * @returns {boolean}
95
- */
96
- const isReadable = v =>
97
- typeof v === 'object' &&
98
- typeof v?.read === 'function' &&
99
- typeof v?.on === 'function' &&
100
- typeof v?.pipe === 'function' &&
101
- v?.readable !== false;
102
-
103
- /**
104
- * Based on the type of a payload, serialized it to be send as the body of a fetch POST request and also infer its Content Type.
105
- *
106
- * Non serializable types versus Content-Type reference (for Node)
107
- *
108
- * |Type|Is self-describing)|Inferred type by fetch|Defined mime type|
109
- * |-|-|-|-}
110
- * |Blob|yes|`blob.type`||
111
- * |File|yes|`file.type`||
112
- * |FormData|yes|"multipart/form-data; boundary=..."||
113
- * |URLSearchParams|yes|"application/x-www-form-urlencoded;charset=UTF-8"||
114
- * |ArrayBuffer|no||"application/octet-stream"|
115
- * |TypedArray (Uint8Array,Uint16Array)||"application/octet-stream"||
116
- * |DataView|no||"application/octet-stream"|
117
- * |ReadableStream, Readable, AsyncIterator|no||Can't, because stream must be read|
118
- *
119
- * If payload is none of the above types, test it:
120
- * If the it is an object, serialize using JSON.stringify and set content-type to `application/json`;
121
- * Else, it is a JS primitive, serialize using JSON.stringify and set content-type to `text/plain`;
122
- *
123
- * This implementation is overkill for temporal workflows since the only types available there will be:
124
- * - URLSearchParams
125
- * - ArrayBuffer
126
- * - TypedArrays
127
- * - DataView
128
- * - asyncGenerator
129
- * The others are non deterministic and are not available at runtime, but this function was build to be flexible
130
- *
131
- * @see {@link https://fetch.spec.whatwg.org/#bodyinit}
132
- * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch}
133
- *
134
- * @param {unknown} payload
135
- * @returns {object} An object with the serialized body and inferred content-type
136
- */
137
- export const serializeBodyAndInferContentType = payload => {
138
- const dataTypes = [ Blob, File, URLSearchParams, FormData ];
139
-
140
- // empty body
141
- if ( [ null, undefined ].includes( payload ) ) {
142
- return { body: undefined, contentType: undefined };
143
- }
144
-
145
- // Buffer types, covers ArrayBuffer, TypedArrays and DataView
146
- if ( payload instanceof ArrayBuffer || ArrayBuffer.isView( payload ) ) {
147
- return { body: payload, contentType: 'application/octet-stream' };
148
- }
149
-
150
- // These data types auto assigned mime types
151
- if ( dataTypes.some( t => payload instanceof t ) ) {
152
- return { body: payload, contentType: undefined };
153
- }
154
-
155
- // ReadableStream, Readable and Async Iterator mimes cant be determined without reading it
156
- if ( payload instanceof ReadableStream || typeof payload[Symbol.asyncIterator] === 'function' || isReadable( payload ) ) {
157
- return { body: payload, contentType: undefined };
158
- }
159
-
160
- if ( typeof payload === 'object' ) {
161
- return { body: JSON.stringify( payload ), contentType: 'application/json; charset=UTF-8' };
162
- }
163
-
164
- return { body: String( payload ), contentType: 'text/plain; charset=UTF-8' };
165
- };
166
-
167
- /**
168
- * Receives an array and returns a copy of it with the elements shuffled
169
- *
170
- * @param {array} arr
171
- * @returns {array}
172
- */
173
- export const shuffleArray = arr => arr
174
- .map( v => ( { v, sort: Math.random() } ) )
175
- .sort( ( a, b ) => a.sort - b.sort )
176
- .map( ( { v } ) => v );
177
-
178
- /**
179
- * Creates a new object merging object "b" onto object "a", using a resolver function to define the value to keep.
180
- * - Object "b" fields that also exists on "a" will have their value defined by the "resolver" function
181
- * - Object "b" fields that don't exist on object "a" will be added;
182
- * - Object "a" fields that don't exist on object "b" will be preserved;
183
- *
184
- * If "b" isn't an object, a new object equal to "a" is returned
185
- *
186
- * @param {object} a - The base object
187
- * @param {object} b - The target object
188
- * @param {function} resolver - A function that return the value to be kept. First argument is value a, second is value b
189
- * @returns {object} A new object
190
- */
191
- export const deepMergeWithResolver = ( a, b, resolver ) => {
192
- if ( !isPlainObject( a ) ) {
193
- throw new Error( 'Parameter "a" is not an object.' );
194
- }
195
- if ( !isPlainObject( b ) ) {
196
- return clone( a );
197
- }
198
- return Object.entries( b ).reduce( ( obj, [ k, v ] ) =>
199
- Object.assign( obj, {
200
- [k]: ( () => {
201
- if ( isPlainObject( v ) && isPlainObject( a[k] ) ) {
202
- return deepMergeWithResolver( a[k], v, resolver );
203
- }
204
- if ( Object.hasOwn( a, k ) ) {
205
- return resolver( a[k], v );
206
- }
207
- return v;
208
- } )()
209
- } )
210
- , clone( a ) );
211
- };
212
-
213
- /**
214
- * Creates a new object merging object "b" onto object "a" biased to "b":
215
- * - Object "b" will overwrite fields on object "a";
216
- * - Object "b" fields that don't exist on object "a" will be added;
217
- * - Object "a" fields that don't exist on object "b" will be preserved;
218
- *
219
- * If "b" isn't an object, a new object equal to "a" is returned
220
- *
221
- * @param {object} a - The base object
222
- * @param {object} b - The target object
223
- * @returns {object} A new object
224
- */
225
- export const deepMerge = ( a, b ) => deepMergeWithResolver( a, b, ( _, b ) => b );
226
-
227
- /**
228
- * Shortens a UUID by re-encoding it to base62.
229
- *
230
- * This is a Temporal friendly, without crypto or Buffer.
231
- * @param {string} uuid
232
- * @returns {string}
233
- */
234
- export const toUrlSafeBase64 = uuid => {
235
- const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-';
236
- const alphabetLen = alphabet.length;
237
- const base = BigInt( alphabetLen );
238
- const hex = uuid.replace( /-/g, '' );
239
-
240
- const toDigits = n => n <= 0n ? [] : toDigits( n / base ).concat( alphabet[Number( n % base )] );
241
- return toDigits( BigInt( '0x' + hex ) ).join( '' );
242
- };
243
-
244
- /**
245
- * Similar to native Promise.allSettled but throws an Error if the execution exceeds a given time.
246
- *
247
- * The error thrown will have attribute `.isTimeout` as `true`.
248
- *
249
- * @template T
250
- * @param {Array<T | PromiseLike<T>>} promises
251
- * @param {number} timeoutMs
252
- * @returns {Promise<PromiseSettledResult<Awaited<T>>[]>}
253
- * @throws {Error & { isTimeout: true }}
254
- */
255
- export const allSettledWithTimeout = ( () => {
256
- class TimeoutError extends Error {
257
- isTimeout = true;
258
- constructor() {
259
- super( 'Timed out before completing all promises' );
260
- }
261
- }
262
-
263
- return async ( promises, timeoutMs ) => {
264
- if ( promises.length === 0 ) {
265
- return [];
266
- }
267
-
268
- const state = { timeoutMonitor: null };
269
-
270
- try {
271
- return await Promise.race( [
272
- Promise.allSettled( promises ),
273
- new Promise( ( _, reject ) => {
274
- state.timeoutMonitor = setTimeout( () => reject( new TimeoutError() ), timeoutMs );
275
- } )
276
- ] );
277
- } finally {
278
- clearTimeout( state.timeoutMonitor );
279
- }
280
- };
281
- } )();
282
-
283
- /**
284
- * Builds a promise that can be resolved from the outside.
285
- */
286
- export class CancellablePromise {
287
- #promise = null;
288
- #complete = null;
289
- #completed = false;
290
-
291
- constructor() {
292
- this.#promise = new Promise( resolve => {
293
- this.#complete = () => {
294
- resolve();
295
- this.#completed = true;
296
- };
297
- } );
298
- }
299
- /** Retrieves the promise */
300
- get promise() {
301
- return this.#promise;
302
- }
303
- /** Returns whether the promise is resolved or not */
304
- get completed() {
305
- return this.#completed;
306
- }
307
- /** Resolves the promise */
308
- complete() {
309
- this.#complete();
310
- }
311
- };
312
-
313
- /**
314
- * Returns a function that invokes the fn argument when called once, further calls do nothing.
315
- * @param {Function} fn
316
- * @returns {Function}
317
- */
318
- export const runOnce = fn => {
319
- const state = { executed: false, result: undefined };
320
- return ( ...args ) => {
321
- if ( !state.executed ) {
322
- state.executed = true;
323
- return state.result = fn( ...args );
324
- }
325
- return state.result;
326
- };
327
- };
328
-
329
- /**
330
- * Escape regexp characters in a string
331
- * @param {*} value
332
- * @returns
333
- */
334
- export const rxEscape = v => v.replace( /[.*+?^${}()|[\]\\]/g, '\\$&' );