@outputai/core 0.8.2-next.42a0ddf.0 → 0.8.2-next.57bc8d6.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 +11 -11
- package/src/bus.js +1 -1
- package/src/consts.js +5 -2
- package/src/helpers/component.js +12 -0
- package/src/helpers/component.spec.js +54 -0
- package/src/helpers/fetch.js +105 -0
- package/src/helpers/fetch.spec.js +203 -0
- package/src/helpers/function.js +15 -0
- package/src/helpers/function.spec.js +48 -0
- package/src/helpers/object.js +98 -0
- package/src/helpers/object.spec.js +377 -0
- package/src/helpers/promise.js +29 -0
- package/src/helpers/promise.spec.js +35 -0
- package/src/helpers/string.js +30 -0
- package/src/helpers/string.spec.js +64 -0
- package/src/hooks/index.d.ts +102 -30
- package/src/hooks/index.js +16 -1
- package/src/hooks/index.spec.js +55 -1
- package/src/index.d.ts +2 -2
- package/src/interface/evaluator.d.ts +2 -2
- package/src/interface/evaluator.js +14 -12
- package/src/interface/evaluator.spec.js +10 -6
- package/src/interface/index.d.ts +7 -6
- package/src/interface/index.js +2 -0
- package/src/interface/logger.d.ts +61 -0
- package/src/interface/logger.js +73 -0
- package/src/interface/logger.spec.js +172 -0
- package/src/interface/step.d.ts +1 -1
- package/src/interface/step.js +15 -12
- package/src/interface/step.spec.js +10 -6
- package/src/interface/webhook.d.ts +21 -2
- package/src/interface/workflow.d.ts +2 -2
- package/src/interface/workflow.js +85 -78
- package/src/interface/workflow.spec.js +11 -4
- package/src/internal_activities/index.js +38 -35
- package/src/internal_activities/index.spec.js +27 -4
- package/src/logger/development.js +2 -2
- package/src/logger/development.spec.js +19 -2
- package/src/logger/production.js +1 -1
- package/src/logger/production.spec.js +24 -5
- package/src/sdk/README.md +47 -0
- package/src/sdk/helpers/component_metadata.d.ts +17 -0
- package/src/sdk/helpers/component_metadata.js +6 -0
- package/src/sdk/helpers/component_metadata.spec.js +30 -0
- package/src/sdk/helpers/index.d.ts +12 -0
- package/src/sdk/helpers/index.js +3 -0
- package/src/sdk/helpers/objects.d.ts +51 -0
- package/src/sdk/helpers/objects.js +8 -0
- package/src/sdk/helpers/objects.spec.js +16 -0
- package/src/sdk/helpers/path.d.ts +11 -0
- package/src/sdk/helpers/path.js +32 -0
- package/src/{utils/resolve_invocation_dir.spec.js → sdk/helpers/path.spec.js} +9 -9
- package/src/sdk/runtime/context.d.ts +30 -0
- package/src/sdk/runtime/context.js +15 -0
- package/src/{activity_integration → sdk/runtime}/context.spec.js +5 -5
- package/src/sdk/runtime/events.d.ts +15 -0
- package/src/sdk/runtime/events.js +18 -0
- package/src/{activity_integration → sdk/runtime}/events.spec.js +8 -9
- package/src/sdk/runtime/index.d.ts +12 -0
- package/src/sdk/runtime/index.js +3 -0
- package/src/sdk/runtime/tracing.d.ts +46 -0
- package/src/sdk/runtime/tracing.js +11 -0
- package/src/tracing/processors/s3/redis_client.spec.js +0 -6
- package/src/tracing/processors/s3/s3_client.spec.js +0 -6
- package/src/tracing/trace_engine.js +1 -1
- package/src/worker/catalog_workflow/catalog_job.js +1 -1
- package/src/worker/catalog_workflow/index.spec.js +8 -11
- package/src/worker/configs.js +1 -1
- package/src/worker/connection_monitor.js +1 -1
- package/src/worker/global_functions.js +14 -0
- package/src/worker/global_functions.spec.js +55 -0
- package/src/worker/index.js +4 -1
- package/src/worker/index.spec.js +7 -0
- package/src/worker/interceptors/activity.js +8 -11
- package/src/worker/interceptors/activity.spec.js +25 -26
- package/src/worker/interceptors/workflow.js +3 -3
- package/src/worker/interceptors/workflow.spec.js +1 -1
- package/src/worker/loader/matchers.js +1 -1
- package/src/worker/log_hooks.js +14 -0
- package/src/worker/log_hooks.spec.js +83 -2
- package/src/worker/sinks.js +7 -1
- package/src/worker/sinks.spec.js +203 -0
- package/src/activity_integration/context.d.ts +0 -23
- package/src/activity_integration/context.js +0 -18
- package/src/activity_integration/event_id_integration.spec.js +0 -52
- package/src/activity_integration/events.d.ts +0 -10
- package/src/activity_integration/events.js +0 -15
- package/src/activity_integration/index.d.ts +0 -9
- package/src/activity_integration/index.js +0 -3
- package/src/activity_integration/tracing.d.ts +0 -40
- package/src/activity_integration/tracing.js +0 -48
- package/src/utils/index.d.ts +0 -180
- package/src/utils/index.js +0 -2
- package/src/utils/resolve_invocation_dir.js +0 -34
- package/src/utils/utils.js +0 -334
- package/src/utils/utils.spec.js +0 -723
- /package/src/{internal_utils → helpers}/aggregations.js +0 -0
- /package/src/{internal_utils → helpers}/aggregations.spec.js +0 -0
- /package/src/{internal_utils → helpers}/errors.js +0 -0
- /package/src/{internal_utils → helpers}/errors.spec.js +0 -0
- /package/src/{internal_utils → helpers}/temporal_context.js +0 -0
- /package/src/{internal_utils → helpers}/temporal_context.spec.ts +0 -0
- /package/src/{internal_utils → helpers}/trace_info.js +0 -0
- /package/src/{internal_utils → helpers}/trace_info.spec.js +0 -0
- /package/src/{internal_utils → helpers}/workflow_context.js +0 -0
- /package/src/{internal_utils → helpers}/workflow_context.spec.js +0 -0
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { assignImmutableProperty, clone, deepMerge, deepMergeWithResolver, isPlainObject, shuffleArray } from './object.js';
|
|
3
|
+
|
|
4
|
+
describe( 'clone', () => {
|
|
5
|
+
it( 'produces a deep copy without shared references', () => {
|
|
6
|
+
const original = { a: 1, nested: { b: 2 } };
|
|
7
|
+
const copied = clone( original );
|
|
8
|
+
|
|
9
|
+
copied.nested.b = 3;
|
|
10
|
+
|
|
11
|
+
expect( original.nested.b ).toBe( 2 );
|
|
12
|
+
expect( copied.nested.b ).toBe( 3 );
|
|
13
|
+
expect( copied ).not.toBe( original );
|
|
14
|
+
} );
|
|
15
|
+
|
|
16
|
+
it( 'deep copies JSON-compatible arrays and objects', () => {
|
|
17
|
+
const original = {
|
|
18
|
+
arr: [ 1, { nested: true } ],
|
|
19
|
+
str: 'value',
|
|
20
|
+
bool: false,
|
|
21
|
+
nil: null
|
|
22
|
+
};
|
|
23
|
+
const copied = clone( original );
|
|
24
|
+
|
|
25
|
+
copied.arr[1].nested = false;
|
|
26
|
+
|
|
27
|
+
expect( copied ).toEqual( {
|
|
28
|
+
arr: [ 1, { nested: false } ],
|
|
29
|
+
str: 'value',
|
|
30
|
+
bool: false,
|
|
31
|
+
nil: null
|
|
32
|
+
} );
|
|
33
|
+
expect( original.arr[1].nested ).toBe( true );
|
|
34
|
+
expect( copied ).not.toBe( original );
|
|
35
|
+
expect( copied.arr ).not.toBe( original.arr );
|
|
36
|
+
} );
|
|
37
|
+
|
|
38
|
+
it( 'returns primitive JSON values when they can be parsed', () => {
|
|
39
|
+
expect( clone( null ) ).toBeNull();
|
|
40
|
+
expect( clone( true ) ).toBe( true );
|
|
41
|
+
expect( clone( false ) ).toBe( false );
|
|
42
|
+
expect( clone( 123 ) ).toBe( 123 );
|
|
43
|
+
expect( clone( 'hello' ) ).toBe( 'hello' );
|
|
44
|
+
} );
|
|
45
|
+
|
|
46
|
+
it( 'returns original values when JSON serialization produces no parseable payload', () => {
|
|
47
|
+
const sym = Symbol( 'x' );
|
|
48
|
+
const fn = () => {};
|
|
49
|
+
class Foo {}
|
|
50
|
+
|
|
51
|
+
expect( clone( undefined ) ).toBeUndefined();
|
|
52
|
+
expect( clone( sym ) ).toBe( sym );
|
|
53
|
+
expect( clone( fn ) ).toBe( fn );
|
|
54
|
+
expect( clone( Foo ) ).toBe( Foo );
|
|
55
|
+
expect( clone( Date ) ).toBe( Date );
|
|
56
|
+
expect( clone( Object ) ).toBe( Object );
|
|
57
|
+
expect( clone( Number ) ).toBe( Number );
|
|
58
|
+
} );
|
|
59
|
+
|
|
60
|
+
it( 'returns original values when JSON serialization throws', () => {
|
|
61
|
+
const circular = { name: 'circular' };
|
|
62
|
+
circular.self = circular;
|
|
63
|
+
const bigint = 1n;
|
|
64
|
+
|
|
65
|
+
expect( clone( circular ) ).toBe( circular );
|
|
66
|
+
expect( clone( bigint ) ).toBe( bigint );
|
|
67
|
+
} );
|
|
68
|
+
|
|
69
|
+
it( 'keeps JSON.stringify semantics for special numeric values', () => {
|
|
70
|
+
expect( clone( NaN ) ).toBeNull();
|
|
71
|
+
expect( clone( Infinity ) ).toBeNull();
|
|
72
|
+
expect( clone( -Infinity ) ).toBeNull();
|
|
73
|
+
} );
|
|
74
|
+
|
|
75
|
+
it( 'keeps JSON.stringify semantics for non-plain object instances', () => {
|
|
76
|
+
const date = new Date( '2025-01-01T00:00:00.000Z' );
|
|
77
|
+
|
|
78
|
+
expect( clone( date ) ).toBe( '2025-01-01T00:00:00.000Z' );
|
|
79
|
+
expect( clone( /abc/ ) ).toEqual( {} );
|
|
80
|
+
expect( clone( new Map( [ [ 'a', 1 ] ] ) ) ).toEqual( {} );
|
|
81
|
+
expect( clone( new Set( [ 1, 2 ] ) ) ).toEqual( {} );
|
|
82
|
+
} );
|
|
83
|
+
|
|
84
|
+
it( 'drops object properties that JSON.stringify omits', () => {
|
|
85
|
+
const sym = Symbol( 'x' );
|
|
86
|
+
const original = {
|
|
87
|
+
kept: 'yes',
|
|
88
|
+
missing: undefined,
|
|
89
|
+
fn: () => {},
|
|
90
|
+
sym
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
expect( clone( original ) ).toEqual( { kept: 'yes' } );
|
|
94
|
+
} );
|
|
95
|
+
} );
|
|
96
|
+
|
|
97
|
+
describe( 'deepMerge', () => {
|
|
98
|
+
it( 'Overwrites properties in object "a"', () => {
|
|
99
|
+
const a = {
|
|
100
|
+
a: 1,
|
|
101
|
+
b: {
|
|
102
|
+
c: 2
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
const b = {
|
|
106
|
+
a: false,
|
|
107
|
+
b: {
|
|
108
|
+
c: true
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
expect( deepMerge( a, b ) ).toEqual( {
|
|
112
|
+
a: false,
|
|
113
|
+
b: {
|
|
114
|
+
c: true
|
|
115
|
+
}
|
|
116
|
+
} );
|
|
117
|
+
} );
|
|
118
|
+
|
|
119
|
+
it( 'Adds properties existing in "b" but absent in "a"', () => {
|
|
120
|
+
const a = {
|
|
121
|
+
a: 1
|
|
122
|
+
};
|
|
123
|
+
const b = {
|
|
124
|
+
a: false,
|
|
125
|
+
b: true
|
|
126
|
+
};
|
|
127
|
+
expect( deepMerge( a, b ) ).toEqual( {
|
|
128
|
+
a: false,
|
|
129
|
+
b: true
|
|
130
|
+
} );
|
|
131
|
+
} );
|
|
132
|
+
|
|
133
|
+
it( 'Keep extra properties in "a"', () => {
|
|
134
|
+
const a = {
|
|
135
|
+
a: 1
|
|
136
|
+
};
|
|
137
|
+
const b = {
|
|
138
|
+
b: true
|
|
139
|
+
};
|
|
140
|
+
expect( deepMerge( a, b ) ).toEqual( {
|
|
141
|
+
a: 1,
|
|
142
|
+
b: true
|
|
143
|
+
} );
|
|
144
|
+
} );
|
|
145
|
+
|
|
146
|
+
it( 'Merge object is a clone', () => {
|
|
147
|
+
const a = {
|
|
148
|
+
a: 1
|
|
149
|
+
};
|
|
150
|
+
const b = {
|
|
151
|
+
b: 1
|
|
152
|
+
};
|
|
153
|
+
const result = deepMerge( a, b );
|
|
154
|
+
a.a = 2;
|
|
155
|
+
b.b = 2;
|
|
156
|
+
expect( result.a ).toEqual( 1 );
|
|
157
|
+
} );
|
|
158
|
+
|
|
159
|
+
it( 'Returns copy of "a" if "b" is not an object', () => {
|
|
160
|
+
const a = {
|
|
161
|
+
a: 1
|
|
162
|
+
};
|
|
163
|
+
expect( deepMerge( a, null ) ).toEqual( { a: 1 } );
|
|
164
|
+
expect( deepMerge( a, undefined ) ).toEqual( { a: 1 } );
|
|
165
|
+
} );
|
|
166
|
+
|
|
167
|
+
it( 'Copy of object "a" is a clone', () => {
|
|
168
|
+
const a = {
|
|
169
|
+
a: 1
|
|
170
|
+
};
|
|
171
|
+
const result = deepMerge( a, null );
|
|
172
|
+
a.a = 2;
|
|
173
|
+
expect( result.a ).toEqual( 1 );
|
|
174
|
+
} );
|
|
175
|
+
|
|
176
|
+
it( 'Throws when first argument is not a plain object', () => {
|
|
177
|
+
expect( () => deepMerge( Function ) ).toThrow( Error );
|
|
178
|
+
expect( () => deepMerge( () => {} ) ).toThrow( Error );
|
|
179
|
+
expect( () => deepMerge( 'a' ) ).toThrow( Error );
|
|
180
|
+
expect( () => deepMerge( true ) ).toThrow( Error );
|
|
181
|
+
expect( () => deepMerge( /a/ ) ).toThrow( Error );
|
|
182
|
+
expect( () => deepMerge( [] ) ).toThrow( Error );
|
|
183
|
+
expect( () => deepMerge( class Foo {}, class Foo {} ) ).toThrow( Error );
|
|
184
|
+
expect( () => deepMerge( Number.constructor, Number.constructor ) ).toThrow( Error );
|
|
185
|
+
expect( () => deepMerge( Number.constructor.prototype, Number.constructor.prototype ) ).toThrow( Error );
|
|
186
|
+
} );
|
|
187
|
+
} );
|
|
188
|
+
|
|
189
|
+
describe( 'deepMergeWithResolver', () => {
|
|
190
|
+
it( 'uses resolver for existing leaf values, including nested leaves', () => {
|
|
191
|
+
const a = {
|
|
192
|
+
cost: { total: 1 },
|
|
193
|
+
tokens: { total: 2, input: 3 }
|
|
194
|
+
};
|
|
195
|
+
const b = {
|
|
196
|
+
cost: { total: 4 },
|
|
197
|
+
tokens: { total: 5, input: 6, output: 7 }
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
expect( deepMergeWithResolver( a, b, ( x, y ) => x + y ) ).toEqual( {
|
|
201
|
+
cost: { total: 5 },
|
|
202
|
+
tokens: { total: 7, input: 9, output: 7 }
|
|
203
|
+
} );
|
|
204
|
+
} );
|
|
205
|
+
|
|
206
|
+
it( 'copies values from "b" when they do not exist in "a"', () => {
|
|
207
|
+
const resolver = vi.fn( ( x, y ) => x + y );
|
|
208
|
+
|
|
209
|
+
expect( deepMergeWithResolver( { a: 1 }, { b: 2, nested: { c: 3 } }, resolver ) ).toEqual( {
|
|
210
|
+
a: 1,
|
|
211
|
+
b: 2,
|
|
212
|
+
nested: { c: 3 }
|
|
213
|
+
} );
|
|
214
|
+
expect( resolver ).not.toHaveBeenCalled();
|
|
215
|
+
} );
|
|
216
|
+
|
|
217
|
+
it( 'keeps extra values from "a" when absent from "b"', () => {
|
|
218
|
+
expect( deepMergeWithResolver( { a: 1, nested: { kept: 2 } }, { b: 3 }, ( x, y ) => x + y ) ).toEqual( {
|
|
219
|
+
a: 1,
|
|
220
|
+
nested: { kept: 2 },
|
|
221
|
+
b: 3
|
|
222
|
+
} );
|
|
223
|
+
} );
|
|
224
|
+
|
|
225
|
+
it( 'returns a clone of "a" when "b" is not an object', () => {
|
|
226
|
+
const a = { nested: { value: 1 } };
|
|
227
|
+
const result = deepMergeWithResolver( a, null, ( x, y ) => x + y );
|
|
228
|
+
|
|
229
|
+
a.nested.value = 2;
|
|
230
|
+
expect( result ).toEqual( { nested: { value: 1 } } );
|
|
231
|
+
} );
|
|
232
|
+
|
|
233
|
+
it( 'throws when first argument is not a plain object', () => {
|
|
234
|
+
expect( () => deepMergeWithResolver( null, {}, ( x, y ) => x + y ) ).toThrow( Error );
|
|
235
|
+
expect( () => deepMergeWithResolver( [], {}, ( x, y ) => x + y ) ).toThrow( Error );
|
|
236
|
+
expect( () => deepMergeWithResolver( 'a', {}, ( x, y ) => x + y ) ).toThrow( Error );
|
|
237
|
+
} );
|
|
238
|
+
} );
|
|
239
|
+
|
|
240
|
+
describe( 'isPlainObject', () => {
|
|
241
|
+
it( 'Detects plain objects', () => {
|
|
242
|
+
expect( isPlainObject( {} ) ).toBe( true );
|
|
243
|
+
expect( isPlainObject( { a: 1 } ) ).toBe( true );
|
|
244
|
+
expect( isPlainObject( new Object() ) ).toBe( true );
|
|
245
|
+
expect( isPlainObject( new Object( { foo: 'bar' } ) ) ).toBe( true );
|
|
246
|
+
expect( isPlainObject( Object.create( {}.constructor.prototype ) ) ).toBe( true );
|
|
247
|
+
expect( isPlainObject( Object.create( Object.prototype ) ) ).toBe( true );
|
|
248
|
+
} );
|
|
249
|
+
|
|
250
|
+
it( 'Detects plain objects with different prototypes than Object.prototype', () => {
|
|
251
|
+
// Object with null prototype
|
|
252
|
+
expect( isPlainObject( Object.create( null ) ) ).toBe( true );
|
|
253
|
+
} );
|
|
254
|
+
|
|
255
|
+
it( 'Detects non plain objects that had their __proto__ mutated to Object.prototype or null', () => {
|
|
256
|
+
class Foo {}
|
|
257
|
+
const x = new Foo();
|
|
258
|
+
x.__proto__ = Object.prototype;
|
|
259
|
+
expect( isPlainObject( x ) ).toBe( true );
|
|
260
|
+
|
|
261
|
+
const y = new Foo();
|
|
262
|
+
y.__proto__ = null;
|
|
263
|
+
expect( isPlainObject( y ) ).toBe( true );
|
|
264
|
+
} );
|
|
265
|
+
|
|
266
|
+
it( 'Returns false for object which the prototype is not Object.prototype or null', () => {
|
|
267
|
+
// Object which the prototype is a plain {}
|
|
268
|
+
expect( isPlainObject( Object.create( {} ) ) ).toBe( false );
|
|
269
|
+
// Object which prototype is a another object with null prototype
|
|
270
|
+
expect( isPlainObject( Object.create( Object.create( null ) ) ) ).toBe( false );
|
|
271
|
+
} );
|
|
272
|
+
|
|
273
|
+
it( 'Returns false for functions', () => {
|
|
274
|
+
expect( isPlainObject( Function ) ).toBe( false );
|
|
275
|
+
expect( isPlainObject( () => {} ) ).toBe( false );
|
|
276
|
+
expect( isPlainObject( class Foo {} ) ).toBe( false );
|
|
277
|
+
expect( isPlainObject( Number.constructor ) ).toBe( false );
|
|
278
|
+
expect( isPlainObject( Number.constructor.prototype ) ).toBe( false );
|
|
279
|
+
} );
|
|
280
|
+
|
|
281
|
+
it( 'Returns false for arrays', () => {
|
|
282
|
+
expect( isPlainObject( [ 1, 2, 3 ] ) ).toBe( false );
|
|
283
|
+
expect( isPlainObject( [] ) ).toBe( false );
|
|
284
|
+
expect( isPlainObject( Array( 3 ) ) ).toBe( false );
|
|
285
|
+
} );
|
|
286
|
+
|
|
287
|
+
it( 'Returns false for primitives', () => {
|
|
288
|
+
expect( isPlainObject( null ) ).toBe( false );
|
|
289
|
+
expect( isPlainObject( undefined ) ).toBe( false );
|
|
290
|
+
expect( isPlainObject( false ) ).toBe( false );
|
|
291
|
+
expect( isPlainObject( true ) ).toBe( false );
|
|
292
|
+
expect( isPlainObject( 1 ) ).toBe( false );
|
|
293
|
+
expect( isPlainObject( 0 ) ).toBe( false );
|
|
294
|
+
expect( isPlainObject( '' ) ).toBe( false );
|
|
295
|
+
expect( isPlainObject( 'foo' ) ).toBe( false );
|
|
296
|
+
expect( isPlainObject( Symbol( 'foo' ) ) ).toBe( false );
|
|
297
|
+
expect( isPlainObject( Symbol.for( 'foo' ) ) ).toBe( false );
|
|
298
|
+
} );
|
|
299
|
+
|
|
300
|
+
it( 'Returns true for built in objects', () => {
|
|
301
|
+
expect( isPlainObject( Math ) ).toBe( true );
|
|
302
|
+
expect( isPlainObject( JSON ) ).toBe( true );
|
|
303
|
+
} );
|
|
304
|
+
|
|
305
|
+
it( 'Returns false for built in types', () => {
|
|
306
|
+
expect( isPlainObject( String ) ).toBe( false );
|
|
307
|
+
expect( isPlainObject( Number ) ).toBe( false );
|
|
308
|
+
expect( isPlainObject( Date ) ).toBe( false );
|
|
309
|
+
} );
|
|
310
|
+
|
|
311
|
+
it( 'Returns false for other instance where prototype is not object or null', () => {
|
|
312
|
+
expect( isPlainObject( /foo/ ) ).toBe( false );
|
|
313
|
+
expect( isPlainObject( new RegExp( 'foo' ) ) ).toBe( false );
|
|
314
|
+
expect( isPlainObject( new Date() ) ).toBe( false );
|
|
315
|
+
class Foo {}
|
|
316
|
+
expect( isPlainObject( new Foo() ) ).toBe( false );
|
|
317
|
+
expect( isPlainObject( Object.create( ( class Foo {} ).prototype ) ) ).toBe( false );
|
|
318
|
+
} );
|
|
319
|
+
|
|
320
|
+
it( 'Returns false if tries to change the prototype to simulate an object', () => {
|
|
321
|
+
function Bar() {}
|
|
322
|
+
Bar.prototype = Object.create( null );
|
|
323
|
+
expect( isPlainObject( new Bar() ) ).toBe( false );
|
|
324
|
+
} );
|
|
325
|
+
|
|
326
|
+
it( 'Returns false if object proto was mutated to anything else than object or null', () => {
|
|
327
|
+
const zum = {};
|
|
328
|
+
zum.__proto__ = Number.prototype;
|
|
329
|
+
expect( isPlainObject( zum ) ).toBe( false );
|
|
330
|
+
} );
|
|
331
|
+
} );
|
|
332
|
+
|
|
333
|
+
describe( 'assignImmutableProperty', () => {
|
|
334
|
+
it( 'defines a non-writable, non-configurable, non-enumerable property', () => {
|
|
335
|
+
const obj = {};
|
|
336
|
+
const key = Symbol( 'metadata' );
|
|
337
|
+
const value = { name: 'test' };
|
|
338
|
+
|
|
339
|
+
expect( assignImmutableProperty( obj, key, value ) ).toBe( obj );
|
|
340
|
+
expect( obj[key] ).toBe( value );
|
|
341
|
+
expect( Object.getOwnPropertyDescriptor( obj, key ) ).toEqual( {
|
|
342
|
+
value,
|
|
343
|
+
writable: false,
|
|
344
|
+
configurable: false,
|
|
345
|
+
enumerable: false
|
|
346
|
+
} );
|
|
347
|
+
expect( Object.keys( obj ) ).toEqual( [] );
|
|
348
|
+
} );
|
|
349
|
+
|
|
350
|
+
it( 'prevents reassignment and redefinition', () => {
|
|
351
|
+
const obj = {};
|
|
352
|
+
const key = 'metadata';
|
|
353
|
+
|
|
354
|
+
assignImmutableProperty( obj, key, 'original' );
|
|
355
|
+
|
|
356
|
+
expect( () => {
|
|
357
|
+
obj[key] = 'updated';
|
|
358
|
+
} ).toThrow( TypeError );
|
|
359
|
+
expect( () => {
|
|
360
|
+
Object.defineProperty( obj, key, { value: 'updated' } );
|
|
361
|
+
} ).toThrow( TypeError );
|
|
362
|
+
expect( obj[key] ).toBe( 'original' );
|
|
363
|
+
} );
|
|
364
|
+
} );
|
|
365
|
+
|
|
366
|
+
describe( 'shuffleArray', () => {
|
|
367
|
+
it( 'returns a shuffled copy based on random sort keys', () => {
|
|
368
|
+
vi.spyOn( Math, 'random' )
|
|
369
|
+
.mockReturnValueOnce( 0.4 )
|
|
370
|
+
.mockReturnValueOnce( 0.1 )
|
|
371
|
+
.mockReturnValueOnce( 0.3 );
|
|
372
|
+
const arr = [ 'a', 'b', 'c' ];
|
|
373
|
+
|
|
374
|
+
expect( shuffleArray( arr ) ).toEqual( [ 'b', 'c', 'a' ] );
|
|
375
|
+
expect( arr ).toEqual( [ 'a', 'b', 'c' ] );
|
|
376
|
+
} );
|
|
377
|
+
} );
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds a promise that can be resolved from the outside.
|
|
3
|
+
*/
|
|
4
|
+
export class CancellablePromise {
|
|
5
|
+
#promise = null;
|
|
6
|
+
#complete = null;
|
|
7
|
+
#completed = false;
|
|
8
|
+
|
|
9
|
+
constructor() {
|
|
10
|
+
this.#promise = new Promise( resolve => {
|
|
11
|
+
this.#complete = () => {
|
|
12
|
+
resolve();
|
|
13
|
+
this.#completed = true;
|
|
14
|
+
};
|
|
15
|
+
} );
|
|
16
|
+
}
|
|
17
|
+
/** Retrieves the promise */
|
|
18
|
+
get promise() {
|
|
19
|
+
return this.#promise;
|
|
20
|
+
}
|
|
21
|
+
/** Returns whether the promise is resolved or not */
|
|
22
|
+
get completed() {
|
|
23
|
+
return this.#completed;
|
|
24
|
+
}
|
|
25
|
+
/** Resolves the promise */
|
|
26
|
+
complete() {
|
|
27
|
+
this.#complete();
|
|
28
|
+
}
|
|
29
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { CancellablePromise } from './promise.js';
|
|
3
|
+
|
|
4
|
+
describe( 'CancellablePromise', () => {
|
|
5
|
+
it( 'exposes a pending promise until it is completed', async () => {
|
|
6
|
+
const cancellable = new CancellablePromise();
|
|
7
|
+
const onComplete = vi.fn();
|
|
8
|
+
|
|
9
|
+
cancellable.promise.then( onComplete );
|
|
10
|
+
await Promise.resolve();
|
|
11
|
+
|
|
12
|
+
expect( cancellable.completed ).toBe( false );
|
|
13
|
+
expect( onComplete ).not.toHaveBeenCalled();
|
|
14
|
+
|
|
15
|
+
cancellable.complete();
|
|
16
|
+
await cancellable.promise;
|
|
17
|
+
|
|
18
|
+
expect( cancellable.completed ).toBe( true );
|
|
19
|
+
expect( onComplete ).toHaveBeenCalledOnce();
|
|
20
|
+
} );
|
|
21
|
+
|
|
22
|
+
it( 'can be completed multiple times without resolving again', async () => {
|
|
23
|
+
const cancellable = new CancellablePromise();
|
|
24
|
+
const onComplete = vi.fn();
|
|
25
|
+
|
|
26
|
+
cancellable.promise.then( onComplete );
|
|
27
|
+
cancellable.complete();
|
|
28
|
+
cancellable.complete();
|
|
29
|
+
await cancellable.promise;
|
|
30
|
+
await Promise.resolve();
|
|
31
|
+
|
|
32
|
+
expect( cancellable.completed ).toBe( true );
|
|
33
|
+
expect( onComplete ).toHaveBeenCalledOnce();
|
|
34
|
+
} );
|
|
35
|
+
} );
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns true if string value is stringbool and true
|
|
3
|
+
* @param {string} v
|
|
4
|
+
* @returns
|
|
5
|
+
*/
|
|
6
|
+
export const isStringboolTrue = v => [ '1', 'true', 'on' ].includes( v );
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Shortens a UUID by re-encoding it to base62.
|
|
10
|
+
*
|
|
11
|
+
* This is a Temporal friendly, without crypto or Buffer.
|
|
12
|
+
* @param {string} uuid
|
|
13
|
+
* @returns {string}
|
|
14
|
+
*/
|
|
15
|
+
export const toUrlSafeBase64 = uuid => {
|
|
16
|
+
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-';
|
|
17
|
+
const alphabetLen = alphabet.length;
|
|
18
|
+
const base = BigInt( alphabetLen );
|
|
19
|
+
const hex = uuid.replace( /-/g, '' );
|
|
20
|
+
|
|
21
|
+
const toDigits = n => n <= 0n ? [] : toDigits( n / base ).concat( alphabet[Number( n % base )] );
|
|
22
|
+
return toDigits( BigInt( '0x' + hex ) ).join( '' );
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Escape regexp characters in a string
|
|
27
|
+
* @param {*} value
|
|
28
|
+
* @returns
|
|
29
|
+
*/
|
|
30
|
+
export const rxEscape = v => v.replace( /[.*+?^${}()|[\]\\]/g, '\\$&' );
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { toUrlSafeBase64, rxEscape } from './string.js';
|
|
3
|
+
|
|
4
|
+
describe( 'toUrlSafeBase64', () => {
|
|
5
|
+
const urlSafeAlphabet = /^[A-Za-z0-9_-]+$/;
|
|
6
|
+
|
|
7
|
+
it( 'returns a string for a valid UUID', () => {
|
|
8
|
+
const uuid = '550e8400-e29b-41d4-a716-446655440000';
|
|
9
|
+
expect( typeof toUrlSafeBase64( uuid ) ).toBe( 'string' );
|
|
10
|
+
expect( toUrlSafeBase64( uuid ).length ).toBeGreaterThan( 0 );
|
|
11
|
+
} );
|
|
12
|
+
|
|
13
|
+
it( 'output length is 21 or 22 for a standard UUID', () => {
|
|
14
|
+
const uuid = '550e8400-e29b-41d4-a716-446655440000';
|
|
15
|
+
const out = toUrlSafeBase64( uuid );
|
|
16
|
+
expect( out.length ).toBeGreaterThanOrEqual( 21 );
|
|
17
|
+
expect( out.length ).toBeLessThanOrEqual( 22 );
|
|
18
|
+
} );
|
|
19
|
+
|
|
20
|
+
it( 'output contains only url-safe alphabet characters', () => {
|
|
21
|
+
const uuid = '550e8400-e29b-41d4-a716-446655440000';
|
|
22
|
+
const out = toUrlSafeBase64( uuid );
|
|
23
|
+
expect( out ).toMatch( urlSafeAlphabet );
|
|
24
|
+
} );
|
|
25
|
+
|
|
26
|
+
it( 'is deterministic for the same UUID', () => {
|
|
27
|
+
const uuid = '6ba7b810-9dad-11d1-80b4-00c04fd430c8';
|
|
28
|
+
expect( toUrlSafeBase64( uuid ) ).toBe( toUrlSafeBase64( uuid ) );
|
|
29
|
+
} );
|
|
30
|
+
|
|
31
|
+
it( 'different UUIDs produce different strings', () => {
|
|
32
|
+
const a = toUrlSafeBase64( '550e8400-e29b-41d4-a716-446655440000' );
|
|
33
|
+
const b = toUrlSafeBase64( '6ba7b810-9dad-11d1-80b4-00c04fd430c8' );
|
|
34
|
+
expect( a ).not.toBe( b );
|
|
35
|
+
} );
|
|
36
|
+
|
|
37
|
+
it( 'strips hyphens and encodes hex (same as 32-char hex)', () => {
|
|
38
|
+
const withHyphens = '550e8400-e29b-41d4-a716-446655440000';
|
|
39
|
+
const hexOnly = '550e8400e29b41d4a716446655440000';
|
|
40
|
+
expect( toUrlSafeBase64( withHyphens ) ).toBe( toUrlSafeBase64( hexOnly ) );
|
|
41
|
+
} );
|
|
42
|
+
} );
|
|
43
|
+
|
|
44
|
+
describe( 'rxEscape', () => {
|
|
45
|
+
it( 'escapes all regexp metacharacters', () => {
|
|
46
|
+
expect( rxEscape( '.*+?^${}()|[]\\' ) ).toBe( '\\.\\*\\+\\?\\^\\$\\{\\}\\(\\)\\|\\[\\]\\\\' );
|
|
47
|
+
} );
|
|
48
|
+
|
|
49
|
+
it( 'keeps file URL paths matchable as literal regexp input', () => {
|
|
50
|
+
const path = 'file://foo/bar';
|
|
51
|
+
const rx = new RegExp( `^${rxEscape( path )}$` );
|
|
52
|
+
|
|
53
|
+
expect( rx.test( path ) ).toBe( true );
|
|
54
|
+
expect( rx.test( 'file://foo/bar/baz' ) ).toBe( false );
|
|
55
|
+
} );
|
|
56
|
+
|
|
57
|
+
it( 'keeps Windows paths matchable as literal regexp input', () => {
|
|
58
|
+
const path = String.raw`C:\foo\bar`;
|
|
59
|
+
const rx = new RegExp( `^${rxEscape( path )}$` );
|
|
60
|
+
|
|
61
|
+
expect( rx.test( path ) ).toBe( true );
|
|
62
|
+
expect( rx.test( String.raw`C:\foo\bar\baz` ) ).toBe( false );
|
|
63
|
+
} );
|
|
64
|
+
} );
|