@output.ai/core 0.4.7 → 0.4.9-dev.pr326-21ac1cf
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 +1 -1
- package/src/interface/workflow.js +2 -2
- package/src/tracing/processors/s3/configs.js +3 -1
- package/src/tracing/processors/s3/index.js +6 -0
- package/src/tracing/processors/s3/index.spec.js +15 -6
- package/src/utils/index.d.ts +9 -0
- package/src/utils/utils.js +17 -0
- package/src/utils/utils.spec.js +49 -1
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@ import { proxyActivities, inWorkflowContext, executeChild, workflowInfo, uuid4,
|
|
|
3
3
|
import { validateWorkflow } from './validations/static.js';
|
|
4
4
|
import { validateWithSchema } from './validations/runtime.js';
|
|
5
5
|
import { SHARED_STEP_PREFIX, ACTIVITY_GET_TRACE_DESTINATIONS, METADATA_ACCESS_SYMBOL } from '#consts';
|
|
6
|
-
import { deepMerge, mergeActivityOptions, setMetadata } from '#utils';
|
|
6
|
+
import { deepMerge, mergeActivityOptions, setMetadata, toUrlSafeBase64 } from '#utils';
|
|
7
7
|
import { FatalError, ValidationError } from '#errors';
|
|
8
8
|
|
|
9
9
|
/**
|
|
@@ -121,7 +121,7 @@ export function workflow( { name, description, inputSchema, outputSchema, fn, op
|
|
|
121
121
|
startWorkflow: async ( childName, input, extra = {} ) =>
|
|
122
122
|
executeChild( childName, {
|
|
123
123
|
args: input ? [ input ] : [],
|
|
124
|
-
workflowId: `${workflowId}-${
|
|
124
|
+
workflowId: `${workflowId}-${toUrlSafeBase64( uuid4() )}`,
|
|
125
125
|
parentClosePolicy: ParentClosePolicy[extra?.detached ? 'ABANDON' : 'TERMINATE'],
|
|
126
126
|
memo: {
|
|
127
127
|
executionContext,
|
|
@@ -6,7 +6,8 @@ const envVarSchema = z.object( {
|
|
|
6
6
|
OUTPUT_AWS_SECRET_ACCESS_KEY: z.string(),
|
|
7
7
|
OUTPUT_TRACE_REMOTE_S3_BUCKET: z.string(),
|
|
8
8
|
OUTPUT_REDIS_URL: z.string(),
|
|
9
|
-
OUTPUT_REDIS_TRACE_TTL: z.coerce.number().int().positive().default( 60 * 60 * 24 * 7 ) // 7 days
|
|
9
|
+
OUTPUT_REDIS_TRACE_TTL: z.coerce.number().int().positive().default( 60 * 60 * 24 * 7 ), // 7 days
|
|
10
|
+
OUTPUT_TRACE_UPLOAD_DELAY_MS: z.coerce.number().int().nonnegative().default( 10_000 ) // 10s
|
|
10
11
|
} );
|
|
11
12
|
|
|
12
13
|
const env = {};
|
|
@@ -19,6 +20,7 @@ export const loadEnv = () => {
|
|
|
19
20
|
env.remoteS3Bucket = parsedFields.OUTPUT_TRACE_REMOTE_S3_BUCKET;
|
|
20
21
|
env.redisUrl = parsedFields.OUTPUT_REDIS_URL;
|
|
21
22
|
env.redisIncompleteWorkflowsTTL = parsedFields.OUTPUT_REDIS_TRACE_TTL;
|
|
23
|
+
env.traceUploadDelayMs = parsedFields.OUTPUT_TRACE_UPLOAD_DELAY_MS;
|
|
22
24
|
};
|
|
23
25
|
|
|
24
26
|
export const getVars = () => {
|
|
@@ -83,6 +83,12 @@ export const exec = async ( { entry, executionContext } ) => {
|
|
|
83
83
|
return;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
// Wait for straggler entries from other workers to land in Redis before uploading
|
|
87
|
+
const delayMs = getVars().traceUploadDelayMs;
|
|
88
|
+
if ( delayMs > 0 ) {
|
|
89
|
+
await new Promise( resolve => setTimeout( resolve, delayMs ) );
|
|
90
|
+
}
|
|
91
|
+
|
|
86
92
|
const content = buildTraceTree( await getEntries( cacheKey ) );
|
|
87
93
|
// if the trace tree is incomplete it will return null, in this case we can safely discard
|
|
88
94
|
if ( !content ) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
2
|
|
|
3
3
|
const loadEnvMock = vi.fn();
|
|
4
4
|
const getVarsMock = vi.fn( () => ( {
|
|
@@ -29,8 +29,13 @@ vi.mock( '../../tools/build_trace_tree.js', () => ( { default: buildTraceTreeMoc
|
|
|
29
29
|
|
|
30
30
|
describe( 'tracing/processors/s3', () => {
|
|
31
31
|
beforeEach( () => {
|
|
32
|
+
vi.useFakeTimers();
|
|
32
33
|
vi.clearAllMocks();
|
|
33
|
-
getVarsMock.mockReturnValue( { remoteS3Bucket: 'bkt', redisIncompleteWorkflowsTTL: 3600 } );
|
|
34
|
+
getVarsMock.mockReturnValue( { remoteS3Bucket: 'bkt', redisIncompleteWorkflowsTTL: 3600, traceUploadDelayMs: 10_000 } );
|
|
35
|
+
} );
|
|
36
|
+
|
|
37
|
+
afterEach( () => {
|
|
38
|
+
vi.useRealTimers();
|
|
34
39
|
} );
|
|
35
40
|
|
|
36
41
|
it( 'init(): loads config and ensures redis client is created', async () => {
|
|
@@ -57,8 +62,10 @@ describe( 'tracing/processors/s3', () => {
|
|
|
57
62
|
|
|
58
63
|
await exec( { ...ctx, entry: { name: 'A', phase: 'start', timestamp: startTime, parentId: 'root' } } );
|
|
59
64
|
await exec( { ...ctx, entry: { name: 'A', phase: 'tick', timestamp: startTime + 1, parentId: 'root' } } );
|
|
60
|
-
// Root end: no parentId and not start
|
|
61
|
-
|
|
65
|
+
// Root end: no parentId and not start — triggers the 10s delay before upload
|
|
66
|
+
const endPromise = exec( { ...ctx, entry: { name: 'A', phase: 'end', timestamp: startTime + 2 } } );
|
|
67
|
+
await vi.advanceTimersByTimeAsync( 10_000 );
|
|
68
|
+
await endPromise;
|
|
62
69
|
|
|
63
70
|
// Accumulation happened 3 times
|
|
64
71
|
expect( redisMulti.zAdd ).toHaveBeenCalledTimes( 3 );
|
|
@@ -78,7 +85,7 @@ describe( 'tracing/processors/s3', () => {
|
|
|
78
85
|
} );
|
|
79
86
|
|
|
80
87
|
it( 'getDestination(): returns S3 URL using bucket and key from getVars', async () => {
|
|
81
|
-
getVarsMock.mockReturnValue( { remoteS3Bucket: 'my-bucket', redisIncompleteWorkflowsTTL: 3600 } );
|
|
88
|
+
getVarsMock.mockReturnValue( { remoteS3Bucket: 'my-bucket', redisIncompleteWorkflowsTTL: 3600, traceUploadDelayMs: 10_000 } );
|
|
82
89
|
const { getDestination } = await import( './index.js' );
|
|
83
90
|
const startTime = Date.parse( '2020-01-02T03:04:05.678Z' );
|
|
84
91
|
const url = getDestination( { workflowId: 'id1', workflowName: 'WF', startTime } );
|
|
@@ -111,7 +118,9 @@ describe( 'tracing/processors/s3', () => {
|
|
|
111
118
|
zRangeMock.mockResolvedValue( [ JSON.stringify( { id: 'wf', phase: 'end', timestamp: startTime } ) ] );
|
|
112
119
|
buildTraceTreeMock.mockReturnValueOnce( null );
|
|
113
120
|
|
|
114
|
-
|
|
121
|
+
const endPromise = exec( { ...ctx, entry: { id: 'wf', phase: 'end', timestamp: startTime } } );
|
|
122
|
+
await vi.advanceTimersByTimeAsync( 10_000 );
|
|
123
|
+
await endPromise;
|
|
115
124
|
|
|
116
125
|
expect( buildTraceTreeMock ).toHaveBeenCalledTimes( 1 );
|
|
117
126
|
expect( uploadMock ).not.toHaveBeenCalled();
|
package/src/utils/index.d.ts
CHANGED
|
@@ -116,3 +116,12 @@ export function shuffleArray( arr: array ): array;
|
|
|
116
116
|
* @returns A new merged object.
|
|
117
117
|
*/
|
|
118
118
|
export function deepMerge( a: object, b: object ): object;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Shortens a UUID to a url-safe base64-like string (custom 64-char alphabet).
|
|
122
|
+
* Temporal-friendly: no Buffer or crypto; safe to use inside workflows.
|
|
123
|
+
*
|
|
124
|
+
* @param uuid - Standard UUID (e.g. `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`).
|
|
125
|
+
* @returns Short string using A–Z, a–z, 0–9, `_`, `-` (typically 21–22 chars).
|
|
126
|
+
*/
|
|
127
|
+
export function toUrlSafeBase64( uuid: string ): string;
|
package/src/utils/utils.js
CHANGED
|
@@ -195,3 +195,20 @@ export const deepMerge = ( a, b ) => {
|
|
|
195
195
|
Object.assign( obj, { [k]: isPlainObject( v ) && isPlainObject( a[k] ) ? deepMerge( a[k], v ) : v } )
|
|
196
196
|
, clone( a ) );
|
|
197
197
|
};
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Shortens a UUID by re-encoding it to base62.
|
|
201
|
+
*
|
|
202
|
+
* This is a Temporal friendly, without crypto or Buffer.
|
|
203
|
+
* @param {string} uuid
|
|
204
|
+
* @returns {string}
|
|
205
|
+
*/
|
|
206
|
+
export const toUrlSafeBase64 = uuid => {
|
|
207
|
+
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-';
|
|
208
|
+
const alphabetLen = alphabet.length;
|
|
209
|
+
const base = BigInt( alphabetLen );
|
|
210
|
+
const hex = uuid.replace( /-/g, '' );
|
|
211
|
+
|
|
212
|
+
const toDigits = n => n <= 0n ? [] : toDigits( n / base ).concat( alphabet[Number( n % base )] );
|
|
213
|
+
return toDigits( BigInt( '0x' + hex ) ).join( '' );
|
|
214
|
+
};
|
package/src/utils/utils.spec.js
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
2
|
import { Readable } from 'node:stream';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
clone,
|
|
5
|
+
mergeActivityOptions,
|
|
6
|
+
serializeBodyAndInferContentType,
|
|
7
|
+
serializeFetchResponse,
|
|
8
|
+
deepMerge,
|
|
9
|
+
isPlainObject,
|
|
10
|
+
toUrlSafeBase64
|
|
11
|
+
} from './utils.js';
|
|
4
12
|
|
|
5
13
|
describe( 'clone', () => {
|
|
6
14
|
it( 'produces a deep copy without shared references', () => {
|
|
@@ -411,3 +419,43 @@ describe( 'isPlainObject', () => {
|
|
|
411
419
|
expect( isPlainObject( zum ) ).toBe( false );
|
|
412
420
|
} );
|
|
413
421
|
} );
|
|
422
|
+
|
|
423
|
+
describe( 'toUrlSafeBase64', () => {
|
|
424
|
+
const urlSafeAlphabet = /^[A-Za-z0-9_-]+$/;
|
|
425
|
+
|
|
426
|
+
it( 'returns a string for a valid UUID', () => {
|
|
427
|
+
const uuid = '550e8400-e29b-41d4-a716-446655440000';
|
|
428
|
+
expect( typeof toUrlSafeBase64( uuid ) ).toBe( 'string' );
|
|
429
|
+
expect( toUrlSafeBase64( uuid ).length ).toBeGreaterThan( 0 );
|
|
430
|
+
} );
|
|
431
|
+
|
|
432
|
+
it( 'output length is 21 or 22 for a standard UUID', () => {
|
|
433
|
+
const uuid = '550e8400-e29b-41d4-a716-446655440000';
|
|
434
|
+
const out = toUrlSafeBase64( uuid );
|
|
435
|
+
expect( out.length ).toBeGreaterThanOrEqual( 21 );
|
|
436
|
+
expect( out.length ).toBeLessThanOrEqual( 22 );
|
|
437
|
+
} );
|
|
438
|
+
|
|
439
|
+
it( 'output contains only url-safe alphabet characters', () => {
|
|
440
|
+
const uuid = '550e8400-e29b-41d4-a716-446655440000';
|
|
441
|
+
const out = toUrlSafeBase64( uuid );
|
|
442
|
+
expect( out ).toMatch( urlSafeAlphabet );
|
|
443
|
+
} );
|
|
444
|
+
|
|
445
|
+
it( 'is deterministic for the same UUID', () => {
|
|
446
|
+
const uuid = '6ba7b810-9dad-11d1-80b4-00c04fd430c8';
|
|
447
|
+
expect( toUrlSafeBase64( uuid ) ).toBe( toUrlSafeBase64( uuid ) );
|
|
448
|
+
} );
|
|
449
|
+
|
|
450
|
+
it( 'different UUIDs produce different strings', () => {
|
|
451
|
+
const a = toUrlSafeBase64( '550e8400-e29b-41d4-a716-446655440000' );
|
|
452
|
+
const b = toUrlSafeBase64( '6ba7b810-9dad-11d1-80b4-00c04fd430c8' );
|
|
453
|
+
expect( a ).not.toBe( b );
|
|
454
|
+
} );
|
|
455
|
+
|
|
456
|
+
it( 'strips hyphens and encodes hex (same as 32-char hex)', () => {
|
|
457
|
+
const withHyphens = '550e8400-e29b-41d4-a716-446655440000';
|
|
458
|
+
const hexOnly = '550e8400e29b41d4a716446655440000';
|
|
459
|
+
expect( toUrlSafeBase64( withHyphens ) ).toBe( toUrlSafeBase64( hexOnly ) );
|
|
460
|
+
} );
|
|
461
|
+
} );
|