@primero-ai/temporal-graph-tools 1.0.0 → 1.0.2
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/README.md +5 -5
- package/dist/examples/bundle/activities.d.ts +34 -0
- package/dist/examples/bundle/activities.d.ts.map +1 -0
- package/dist/examples/bundle/activities.js +35 -0
- package/dist/examples/bundle/workflows.d.ts +3 -0
- package/dist/examples/bundle/workflows.d.ts.map +1 -0
- package/dist/examples/bundle/workflows.js +14 -0
- package/dist/examples/trigger-workflows.d.ts +2 -0
- package/dist/examples/trigger-workflows.d.ts.map +1 -0
- package/dist/examples/trigger-workflows.js +43 -0
- package/dist/examples/worker.d.ts +3 -0
- package/dist/examples/worker.d.ts.map +1 -0
- package/dist/examples/worker.js +29 -0
- package/dist/src/bundler.d.ts +25 -0
- package/dist/src/bundler.d.ts.map +1 -0
- package/dist/src/bundler.js +202 -0
- package/dist/src/index.d.ts +6 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +5 -0
- package/dist/src/types.d.ts +45 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +64 -0
- package/dist/src/utils/deep-equal.d.ts +2 -0
- package/dist/src/utils/deep-equal.d.ts.map +1 -0
- package/dist/src/utils/deep-equal.js +54 -0
- package/dist/src/workflow/builder.d.ts +27 -0
- package/dist/src/workflow/builder.d.ts.map +1 -0
- package/dist/src/workflow/builder.js +230 -0
- package/dist/src/workflow/collection.d.ts +3 -0
- package/dist/src/workflow/collection.d.ts.map +1 -0
- package/dist/src/workflow/collection.js +39 -0
- package/dist/src/workflow-bundler.d.ts +6 -0
- package/dist/src/workflow-bundler.d.ts.map +1 -0
- package/dist/src/workflow-bundler.js +163 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# @
|
|
1
|
+
# @primero-ai/temporal-graph-tools
|
|
2
2
|
|
|
3
3
|
TypeScript utilities for assembling Temporal workflows from plain activity
|
|
4
4
|
functions. Build a workflow plan, capture the generated source code, hydrate
|
|
@@ -21,11 +21,11 @@ hand-writing workflow files.
|
|
|
21
21
|
## Installation
|
|
22
22
|
|
|
23
23
|
```bash
|
|
24
|
-
npm install @
|
|
24
|
+
npm install @primero-ai/temporal-graph-tools
|
|
25
25
|
# or
|
|
26
|
-
pnpm add @
|
|
26
|
+
pnpm add @primero-ai/temporal-graph-tools
|
|
27
27
|
# or
|
|
28
|
-
bun add @
|
|
28
|
+
bun add @primero-ai/temporal-graph-tools
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
The package targets Node.js 18+ and ships ESM builds.
|
|
@@ -37,7 +37,7 @@ import {
|
|
|
37
37
|
bundleWorkflows,
|
|
38
38
|
createActivity,
|
|
39
39
|
createWorkflowBuilder,
|
|
40
|
-
} from '@
|
|
40
|
+
} from '@primero-ai/temporal-graph-tools'
|
|
41
41
|
|
|
42
42
|
type FetchUserInput = { userId: string }
|
|
43
43
|
type FetchUserOutput = { profile: { id: string; name: string } }
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export type FetchUserInput = {
|
|
2
|
+
userId: string;
|
|
3
|
+
};
|
|
4
|
+
export declare const fetchUserProfile: import("@primero-ai/temporal-graph-tools").ConfiguredActivityReference<FetchUserInput, FetchUserOutput, "fetchUserProfile">;
|
|
5
|
+
export declare const sendWelcomeEmail: import("@primero-ai/temporal-graph-tools").ConfiguredActivityReference<FetchUserOutput, {
|
|
6
|
+
sent: boolean;
|
|
7
|
+
recipientSlug: string;
|
|
8
|
+
}, "sendWelcomeEmail">;
|
|
9
|
+
export type UserProfile = {
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
slug: string;
|
|
13
|
+
};
|
|
14
|
+
export type FetchUserOutput = {
|
|
15
|
+
profile: UserProfile;
|
|
16
|
+
};
|
|
17
|
+
export declare const syncCrmRecord: import("@primero-ai/temporal-graph-tools").ConfiguredActivityReference<FetchUserOutput, {
|
|
18
|
+
synced: boolean;
|
|
19
|
+
recordSlug: string;
|
|
20
|
+
}, "syncCrmRecord">;
|
|
21
|
+
export type ParallelResult = {
|
|
22
|
+
sendWelcomeEmail: {
|
|
23
|
+
sent: boolean;
|
|
24
|
+
recipientSlug: string;
|
|
25
|
+
};
|
|
26
|
+
syncCrmRecord: {
|
|
27
|
+
synced: boolean;
|
|
28
|
+
recordSlug: string;
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
export declare const logCompletion: import("@primero-ai/temporal-graph-tools").ConfiguredActivityReference<ParallelResult, void, "logCompletion">;
|
|
32
|
+
export declare const startGreet: import("@primero-ai/temporal-graph-tools").ConfiguredActivityReference<unknown, void, "startGreet">;
|
|
33
|
+
export declare const endGreet: import("@primero-ai/temporal-graph-tools").ConfiguredActivityReference<unknown, void, "endGreet">;
|
|
34
|
+
//# sourceMappingURL=activities.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"activities.d.ts","sourceRoot":"","sources":["../../../examples/bundle/activities.ts"],"names":[],"mappings":"AAOA,MAAM,MAAM,cAAc,GAAG;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,CAAA;AAE/C,eAAO,MAAM,gBAAgB,6HAiB5B,CAAA;AAED,eAAO,MAAM,gBAAgB;UAC2B,OAAO;mBAAiB,MAAM;sBAOrF,CAAA;AAED,MAAM,MAAM,WAAW,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAA;AACpE,MAAM,MAAM,eAAe,GAAG;IAAE,OAAO,EAAE,WAAW,CAAA;CAAE,CAAA;AAEtD,eAAO,MAAM,aAAa;YACgC,OAAO;gBAAc,MAAM;mBAOpF,CAAA;AAED,MAAM,MAAM,cAAc,GAAG;IAC3B,gBAAgB,EAAE;QAAE,IAAI,EAAE,OAAO,CAAC;QAAC,aAAa,EAAE,MAAM,CAAA;KAAE,CAAA;IAC1D,aAAa,EAAE;QAAE,MAAM,EAAE,OAAO,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAA;CACvD,CAAA;AAED,eAAO,MAAM,aAAa,+GASzB,CAAA;AACD,eAAO,MAAM,UAAU,qGAA0E,CAAA;AACjG,eAAO,MAAM,QAAQ,mGAAuE,CAAA"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import kebabCase from 'lodash/kebabCase.js';
|
|
2
|
+
import { createActivity, } from '@primero-ai/temporal-graph-tools';
|
|
3
|
+
export const fetchUserProfile = createActivity(async (input) => {
|
|
4
|
+
const profileName = `User ${input.userId.slice(0, 6)}`;
|
|
5
|
+
const slug = kebabCase(profileName);
|
|
6
|
+
console.log(`[fetchUserProfile] fetching ${input.userId} as ${slug}`);
|
|
7
|
+
return {
|
|
8
|
+
profile: {
|
|
9
|
+
id: input.userId,
|
|
10
|
+
name: profileName,
|
|
11
|
+
slug,
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
}, {
|
|
15
|
+
id: 'fetchUserProfile',
|
|
16
|
+
});
|
|
17
|
+
export const sendWelcomeEmail = createActivity(async ({ profile }) => {
|
|
18
|
+
console.log(`[sendWelcomeEmail] sent email to ${profile.name} (${profile.slug})`);
|
|
19
|
+
return { sent: true, recipientSlug: profile.slug };
|
|
20
|
+
}, {
|
|
21
|
+
id: 'sendWelcomeEmail',
|
|
22
|
+
});
|
|
23
|
+
export const syncCrmRecord = createActivity(async ({ profile }) => {
|
|
24
|
+
console.log(`[syncCrmRecord] synced record ${profile.id} (${profile.slug})`);
|
|
25
|
+
return { synced: true, recordSlug: profile.slug };
|
|
26
|
+
}, {
|
|
27
|
+
id: 'syncCrmRecord',
|
|
28
|
+
});
|
|
29
|
+
export const logCompletion = createActivity(async (result) => {
|
|
30
|
+
console.log(`[logCompletion] email sent=${result.sendWelcomeEmail.sent} (slug=${result.sendWelcomeEmail.recipientSlug}), crm synced=${result.syncCrmRecord.synced}`);
|
|
31
|
+
}, {
|
|
32
|
+
id: 'logCompletion',
|
|
33
|
+
});
|
|
34
|
+
export const startGreet = createActivity(async () => console.log('Hello '), { id: 'startGreet' });
|
|
35
|
+
export const endGreet = createActivity(async () => console.log('World'), { id: 'endGreet' });
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workflows.d.ts","sourceRoot":"","sources":["../../../examples/bundle/workflows.ts"],"names":[],"mappings":"AAWA,eAAO,MAAM,iBAAiB,gEAOnB,CAAA;AAEX,eAAO,MAAM,iBAAiB,gEAGnB,CAAA"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { createWorkflowBuilder } from '@primero-ai/temporal-graph-tools';
|
|
2
|
+
import { endGreet, fetchUserProfile, logCompletion, sendWelcomeEmail, startGreet, syncCrmRecord, } from './activities';
|
|
3
|
+
export const builderOnboarding = createWorkflowBuilder({
|
|
4
|
+
workflowName: 'customerOnboardingWorkflow',
|
|
5
|
+
proxyOptions: { startToCloseTimeout: '2 minutes' },
|
|
6
|
+
})
|
|
7
|
+
.then(fetchUserProfile)
|
|
8
|
+
.parallel([sendWelcomeEmail, syncCrmRecord])
|
|
9
|
+
.then(logCompletion)
|
|
10
|
+
.commit();
|
|
11
|
+
export const builderHelloWorld = createWorkflowBuilder({ workflowName: 'greetWorkflow' })
|
|
12
|
+
.then(startGreet)
|
|
13
|
+
.then(endGreet)
|
|
14
|
+
.commit();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"trigger-workflows.d.ts","sourceRoot":"","sources":["../../examples/trigger-workflows.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Connection, WorkflowClient } from '@temporalio/client';
|
|
2
|
+
import { builderHelloWorld, builderOnboarding } from './bundle/workflows';
|
|
3
|
+
const DEFAULT_NAMESPACE = 'default';
|
|
4
|
+
const DEFAULT_TASK_QUEUE = 'default';
|
|
5
|
+
async function run() {
|
|
6
|
+
var _a, _b, _c, _d;
|
|
7
|
+
const connection = await Connection.connect({
|
|
8
|
+
address: (_a = process.env.TEMPORAL_ADDRESS) !== null && _a !== void 0 ? _a : 'localhost:7233',
|
|
9
|
+
tls: process.env.TEMPORAL_API_KEY ? {} : undefined,
|
|
10
|
+
apiKey: process.env.TEMPORAL_API_KEY,
|
|
11
|
+
});
|
|
12
|
+
const client = new WorkflowClient({
|
|
13
|
+
connection,
|
|
14
|
+
namespace: (_b = process.env.TEMPORAL_NAMESPACE) !== null && _b !== void 0 ? _b : DEFAULT_NAMESPACE,
|
|
15
|
+
});
|
|
16
|
+
const [onboardingHandle, greetHandle] = await Promise.all([
|
|
17
|
+
client.start(builderOnboarding.workflowName, {
|
|
18
|
+
taskQueue: (_c = process.env.TEMPORAL_TASK_QUEUE) !== null && _c !== void 0 ? _c : DEFAULT_TASK_QUEUE,
|
|
19
|
+
workflowId: `customer-onboarding-${Date.now()}`,
|
|
20
|
+
args: [{ userId: 'user-123' }],
|
|
21
|
+
}),
|
|
22
|
+
client.start(builderHelloWorld.workflowName, {
|
|
23
|
+
taskQueue: (_d = process.env.TEMPORAL_TASK_QUEUE) !== null && _d !== void 0 ? _d : DEFAULT_TASK_QUEUE,
|
|
24
|
+
workflowId: `greet-${Date.now()}`,
|
|
25
|
+
args: [{ userId: 'user-123' }],
|
|
26
|
+
}),
|
|
27
|
+
]);
|
|
28
|
+
console.log('Onboarding Workflow started:', {
|
|
29
|
+
workflowId: onboardingHandle.workflowId,
|
|
30
|
+
runId: onboardingHandle.firstExecutionRunId,
|
|
31
|
+
});
|
|
32
|
+
console.log('Greet Workflow started:', {
|
|
33
|
+
workflowId: greetHandle.workflowId,
|
|
34
|
+
runId: greetHandle.firstExecutionRunId,
|
|
35
|
+
});
|
|
36
|
+
const result = await Promise.all([onboardingHandle.result(), greetHandle.result()]);
|
|
37
|
+
console.log('Workflows completed with result:', result);
|
|
38
|
+
await connection.close();
|
|
39
|
+
}
|
|
40
|
+
run().catch((error) => {
|
|
41
|
+
console.error('Failed to run workflow example:', error);
|
|
42
|
+
process.exitCode = 1;
|
|
43
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../../examples/worker.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoB,uBAAuB,EAAU,MAAM,oBAAoB,CAAA;AAItF,wBAAgB,oBAAoB,IAAI,uBAAuB,CAM9D"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { NativeConnection, Worker } from '@temporalio/worker';
|
|
2
|
+
import { bundleWorkflows, loadActivitiesFromBundle } from '@primero-ai/temporal-graph-tools';
|
|
3
|
+
import { builderHelloWorld, builderOnboarding } from './bundle/workflows';
|
|
4
|
+
export function getConnectionOptions() {
|
|
5
|
+
return {
|
|
6
|
+
address: process.env.TEMPORAL_ADDRESS,
|
|
7
|
+
apiKey: process.env.TEMPORAL_API_KEY,
|
|
8
|
+
tls: {},
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
async function run() {
|
|
12
|
+
const { workflowBundle, activityBundle } = await bundleWorkflows([builderHelloWorld, builderOnboarding], {
|
|
13
|
+
activityBundle: {},
|
|
14
|
+
});
|
|
15
|
+
const activities = await loadActivitiesFromBundle(activityBundle);
|
|
16
|
+
const connection = await NativeConnection.connect(getConnectionOptions());
|
|
17
|
+
const worker = await Worker.create({
|
|
18
|
+
connection,
|
|
19
|
+
namespace: process.env.TEMPORAL_NAMESPACE,
|
|
20
|
+
taskQueue: process.env.TEMPORAL_TASK_QUEUE || 'default',
|
|
21
|
+
activities,
|
|
22
|
+
workflowBundle,
|
|
23
|
+
});
|
|
24
|
+
await worker.run();
|
|
25
|
+
}
|
|
26
|
+
run().catch((err) => {
|
|
27
|
+
console.error(err);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { WorkflowBuildResult } from './types.js';
|
|
2
|
+
import type { ActivityImplementations } from './workflow-bundler.js';
|
|
3
|
+
export type BundleWorkflowsOptions = {
|
|
4
|
+
filename?: string;
|
|
5
|
+
activityBundle?: BundleActivitiesOptions;
|
|
6
|
+
};
|
|
7
|
+
export type BundleWorkflowsResult = {
|
|
8
|
+
activityBundle: BundledActivitiesArtifact;
|
|
9
|
+
workflowBundle: {
|
|
10
|
+
code: string;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
export type BundleActivitiesOptions = {
|
|
14
|
+
entrypoints?: readonly string[];
|
|
15
|
+
filename?: string;
|
|
16
|
+
externals?: readonly string[];
|
|
17
|
+
};
|
|
18
|
+
export type BundledActivitiesArtifact = {
|
|
19
|
+
filename: string;
|
|
20
|
+
code: string;
|
|
21
|
+
map?: string;
|
|
22
|
+
};
|
|
23
|
+
export declare function loadActivitiesFromBundle(bundle: BundledActivitiesArtifact): Promise<ActivityImplementations>;
|
|
24
|
+
export declare function bundleWorkflows(plans: readonly WorkflowBuildResult[], options?: BundleWorkflowsOptions): Promise<BundleWorkflowsResult>;
|
|
25
|
+
//# sourceMappingURL=bundler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bundler.d.ts","sourceRoot":"","sources":["../../src/bundler.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAkB,mBAAmB,EAA0B,MAAM,YAAY,CAAA;AAC7F,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAA;AAIpE,MAAM,MAAM,sBAAsB,GAAG;IACnC,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,cAAc,CAAC,EAAE,uBAAuB,CAAA;CACzC,CAAA;AAED,MAAM,MAAM,qBAAqB,GAAG;IAClC,cAAc,EAAE,yBAAyB,CAAA;IACzC,cAAc,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAA;CACjC,CAAA;AAED,MAAM,MAAM,uBAAuB,GAAG;IACpC,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAC/B,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CAC9B,CAAA;AAED,MAAM,MAAM,yBAAyB,GAAG;IACtC,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,CAAC,EAAE,MAAM,CAAA;CACb,CAAA;AAoCD,wBAAsB,wBAAwB,CAC5C,MAAM,EAAE,yBAAyB,GAChC,OAAO,CAAC,uBAAuB,CAAC,CA4BlC;AAED,wBAAsB,eAAe,CACnC,KAAK,EAAE,SAAS,mBAAmB,EAAE,EACrC,OAAO,CAAC,EAAE,sBAAsB,GAC/B,OAAO,CAAC,qBAAqB,CAAC,CAqChC"}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
2
|
+
import { buildWorkflowBundleCode } from './workflow-bundler.js';
|
|
3
|
+
import { collectWorkflowBuildResults } from './workflow/collection.js';
|
|
4
|
+
let cachedEsbuild;
|
|
5
|
+
async function loadEsbuild() {
|
|
6
|
+
if (cachedEsbuild) {
|
|
7
|
+
return cachedEsbuild;
|
|
8
|
+
}
|
|
9
|
+
const createLoadError = (error) => {
|
|
10
|
+
if (error instanceof Error) {
|
|
11
|
+
error.message = `Failed to load esbuild. Install it as a dependency before calling bundleWorkflows(). Original error: ${error.message}`;
|
|
12
|
+
return error;
|
|
13
|
+
}
|
|
14
|
+
return new Error(`Failed to load esbuild. Install it as a dependency before calling bundleWorkflows(). Original error: ${String(error)}`);
|
|
15
|
+
};
|
|
16
|
+
try {
|
|
17
|
+
const esbuildModule = await import('esbuild');
|
|
18
|
+
cachedEsbuild = esbuildModule;
|
|
19
|
+
return esbuildModule;
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
throw createLoadError(error);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export async function loadActivitiesFromBundle(bundle) {
|
|
26
|
+
if (!bundle || typeof bundle.code !== 'string' || bundle.code.trim().length === 0) {
|
|
27
|
+
throw new Error('Activity bundle code must be a non-empty string.');
|
|
28
|
+
}
|
|
29
|
+
const source = ensureActivityInlineSourceMap(bundle.code, bundle.map);
|
|
30
|
+
const encoded = Buffer.from(source, 'utf8').toString('base64');
|
|
31
|
+
const moduleUrl = `data:text/javascript;base64,${encoded}`;
|
|
32
|
+
const moduleNamespace = (await import(moduleUrl));
|
|
33
|
+
const activitiesExport = moduleNamespace.activities;
|
|
34
|
+
if (isActivityImplementations(activitiesExport)) {
|
|
35
|
+
return activitiesExport;
|
|
36
|
+
}
|
|
37
|
+
const collected = {};
|
|
38
|
+
Object.entries(moduleNamespace).forEach(([key, value]) => {
|
|
39
|
+
if (typeof value === 'function') {
|
|
40
|
+
collected[key] = value;
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
if (Object.keys(collected).length > 0) {
|
|
44
|
+
return collected;
|
|
45
|
+
}
|
|
46
|
+
throw new Error('Activity bundle did not expose an activities object or callable exports.');
|
|
47
|
+
}
|
|
48
|
+
export async function bundleWorkflows(plans, options) {
|
|
49
|
+
var _a;
|
|
50
|
+
if (plans.length === 0) {
|
|
51
|
+
throw new Error('bundleWorkflows requires at least one workflow plan.');
|
|
52
|
+
}
|
|
53
|
+
const seenWorkflowNames = new Set();
|
|
54
|
+
plans.forEach((plan, index) => {
|
|
55
|
+
const workflowName = typeof plan.workflowName === 'string' ? plan.workflowName.trim() : '';
|
|
56
|
+
if (workflowName.length === 0) {
|
|
57
|
+
throw new Error(`Workflow plan at index ${index} is missing a valid workflowName. Provide workflowName when building workflows.`);
|
|
58
|
+
}
|
|
59
|
+
if (seenWorkflowNames.has(workflowName)) {
|
|
60
|
+
throw new Error(`Duplicate workflow name detected in bundle: '${workflowName}' at index ${index}. Workflow names must be unique.`);
|
|
61
|
+
}
|
|
62
|
+
seenWorkflowNames.add(workflowName);
|
|
63
|
+
});
|
|
64
|
+
const collection = collectWorkflowBuildResults(plans);
|
|
65
|
+
const activityBundle = await bundleActivitiesWithEsbuild(collection.activities, options === null || options === void 0 ? void 0 : options.activityBundle);
|
|
66
|
+
const filename = (_a = options === null || options === void 0 ? void 0 : options.filename) !== null && _a !== void 0 ? _a : createBundleFilename(collection.workflows);
|
|
67
|
+
const code = await buildWorkflowBundleCode(collection.workflows, filename);
|
|
68
|
+
return {
|
|
69
|
+
activityBundle,
|
|
70
|
+
workflowBundle: { code },
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function createBundleFilename(workflows) {
|
|
74
|
+
const baseName = workflows
|
|
75
|
+
.map((workflow) => workflow.workflowName)
|
|
76
|
+
.filter((name) => typeof name === 'string' && name.trim().length > 0)
|
|
77
|
+
.join('-')
|
|
78
|
+
.trim();
|
|
79
|
+
const sanitizedBase = sanitizeFilenameBase(baseName.length > 0 ? baseName : 'workflow');
|
|
80
|
+
const randomSuffix = Math.random().toString(36).slice(2, 8);
|
|
81
|
+
return `${sanitizedBase}-${randomSuffix}.workflow.js`;
|
|
82
|
+
}
|
|
83
|
+
function sanitizeFilenameBase(candidate) {
|
|
84
|
+
const sanitized = candidate.replace(/[^A-Za-z0-9_.-]/g, '_');
|
|
85
|
+
if (sanitized.length === 0 || /^_+$/.test(sanitized)) {
|
|
86
|
+
return 'workflow';
|
|
87
|
+
}
|
|
88
|
+
return sanitized;
|
|
89
|
+
}
|
|
90
|
+
async function bundleActivitiesWithEsbuild(bundles, options = {}) {
|
|
91
|
+
var _a, _b, _c, _d, _e;
|
|
92
|
+
const entrypoints = normalizeActivityEntrypoints((_a = options.entrypoints) !== null && _a !== void 0 ? _a : deriveActivityEntrypoints(bundles));
|
|
93
|
+
if (entrypoints.length === 0) {
|
|
94
|
+
throw new Error('bundleActivities requires at least one entrypoint.');
|
|
95
|
+
}
|
|
96
|
+
const filename = ((_b = options.filename) !== null && _b !== void 0 ? _b : 'activities.bundle.mjs').trim();
|
|
97
|
+
const externals = new Set(['@primero-ai/temporal-graph-tools']);
|
|
98
|
+
((_c = options.externals) !== null && _c !== void 0 ? _c : []).forEach((name) => {
|
|
99
|
+
if (typeof name === 'string' && name.trim().length > 0) {
|
|
100
|
+
externals.add(name.trim());
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
const entrySource = createActivitiesEntrySource(entrypoints);
|
|
104
|
+
const esbuild = await loadEsbuild();
|
|
105
|
+
const buildResult = await esbuild.build({
|
|
106
|
+
stdin: {
|
|
107
|
+
contents: entrySource,
|
|
108
|
+
resolveDir: process.cwd(),
|
|
109
|
+
sourcefile: filename,
|
|
110
|
+
loader: 'ts',
|
|
111
|
+
},
|
|
112
|
+
bundle: true,
|
|
113
|
+
platform: 'node',
|
|
114
|
+
format: 'esm',
|
|
115
|
+
target: ['node18'],
|
|
116
|
+
absWorkingDir: process.cwd(),
|
|
117
|
+
outfile: filename,
|
|
118
|
+
write: false,
|
|
119
|
+
sourcemap: 'inline',
|
|
120
|
+
external: Array.from(externals),
|
|
121
|
+
});
|
|
122
|
+
const jsFile = (_d = buildResult.outputFiles) === null || _d === void 0 ? void 0 : _d.find((file) => file.path.endsWith('.js') || file.path.endsWith('.mjs'));
|
|
123
|
+
const mapFile = (_e = buildResult.outputFiles) === null || _e === void 0 ? void 0 : _e.find((file) => file.path.endsWith('.js.map') || file.path.endsWith('.mjs.map'));
|
|
124
|
+
if (!jsFile) {
|
|
125
|
+
throw new Error('Failed to generate activities bundle.');
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
filename,
|
|
129
|
+
code: jsFile.text,
|
|
130
|
+
...(mapFile ? { map: mapFile.text } : {}),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
function normalizeActivityEntrypoints(entrypoints) {
|
|
134
|
+
return entrypoints
|
|
135
|
+
.map((entry) => entry.trim())
|
|
136
|
+
.filter((entry) => entry.length > 0)
|
|
137
|
+
.map((entry) => (isAbsolute(entry) ? entry : resolve(process.cwd(), entry)));
|
|
138
|
+
}
|
|
139
|
+
function deriveActivityEntrypoints(bundles) {
|
|
140
|
+
const paths = new Set();
|
|
141
|
+
Object.values(bundles).forEach((bundle) => {
|
|
142
|
+
if (bundle === null || bundle === void 0 ? void 0 : bundle.sourceFile) {
|
|
143
|
+
paths.add(bundle.sourceFile);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
if (paths.size === 0) {
|
|
147
|
+
throw new Error('Unable to derive activity sources automatically. Provide activityBundle.entrypoints.');
|
|
148
|
+
}
|
|
149
|
+
return Array.from(paths);
|
|
150
|
+
}
|
|
151
|
+
function createActivitiesEntrySource(entrypoints) {
|
|
152
|
+
const imports = entrypoints
|
|
153
|
+
.map((entry, index) => `import * as module${index} from ${JSON.stringify(entry)};`)
|
|
154
|
+
.join('\n');
|
|
155
|
+
const moduleRefs = entrypoints.map((_, index) => `module${index}`).join(', ');
|
|
156
|
+
return `
|
|
157
|
+
${imports}
|
|
158
|
+
|
|
159
|
+
const merged = {} as Record<string, unknown>;
|
|
160
|
+
for (const mod of [${moduleRefs}]) {
|
|
161
|
+
if (!mod || typeof mod !== 'object') {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const candidate = (mod as { activities?: unknown }).activities;
|
|
165
|
+
if (candidate && typeof candidate === 'object') {
|
|
166
|
+
Object.assign(merged, candidate as Record<string, unknown>);
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
for (const [key, value] of Object.entries(mod)) {
|
|
170
|
+
if (key === 'default' || key === '__esModule') {
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (value && typeof value === 'object' && 'activity' in (value as { activity?: unknown })) {
|
|
174
|
+
const activity = (value as { activity?: unknown }).activity;
|
|
175
|
+
if (typeof activity === 'function') {
|
|
176
|
+
merged[key] = activity;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (typeof value === 'function') {
|
|
181
|
+
merged[key] = value;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export const activities = merged;
|
|
187
|
+
`.trimStart();
|
|
188
|
+
}
|
|
189
|
+
function ensureActivityInlineSourceMap(code, map) {
|
|
190
|
+
const marker = '//# sourceMappingURL=data:application/json;base64,';
|
|
191
|
+
if (!map || code.includes(marker)) {
|
|
192
|
+
return code;
|
|
193
|
+
}
|
|
194
|
+
const base64 = Buffer.from(map, 'utf8').toString('base64');
|
|
195
|
+
return `${code}\n${marker}${base64}`;
|
|
196
|
+
}
|
|
197
|
+
function isActivityImplementations(value) {
|
|
198
|
+
if (typeof value !== 'object' || value === null) {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
return Object.values(value).every((entry) => typeof entry === 'function');
|
|
202
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export * from './types.js';
|
|
2
|
+
export { WorkflowBuilder, createWorkflowBuilder } from './workflow/builder.js';
|
|
3
|
+
export * from './workflow-bundler.js';
|
|
4
|
+
export { collectWorkflowBuildResults } from './workflow/collection.js';
|
|
5
|
+
export * from './bundler.js';
|
|
6
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAA;AAC1B,OAAO,EAAE,eAAe,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAA;AAC9E,cAAc,uBAAuB,CAAA;AACrC,OAAO,EAAE,2BAA2B,EAAE,MAAM,0BAA0B,CAAA;AACtE,cAAc,cAAc,CAAA"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { ActivityOptions } from '@temporalio/workflow';
|
|
2
|
+
export type CreateActivity<TInput = unknown, TOutput = unknown> = (input: TInput) => TOutput | Promise<TOutput>;
|
|
3
|
+
export type ActivityConfig<Id extends string = string> = ActivityOptions & {
|
|
4
|
+
id?: Id;
|
|
5
|
+
};
|
|
6
|
+
export declare const ACTIVITY_SOURCE_SYMBOL: unique symbol;
|
|
7
|
+
export type ConfiguredActivityReference<TInput = unknown, TOutput = unknown, Id extends string = string> = {
|
|
8
|
+
activity: CreateActivity<TInput, TOutput>;
|
|
9
|
+
config: ActivityConfig<Id>;
|
|
10
|
+
};
|
|
11
|
+
export type ActivityReference<TInput = unknown, TOutput = unknown, Id extends string = string> = CreateActivity<TInput, TOutput> | ConfiguredActivityReference<TInput, TOutput, Id>;
|
|
12
|
+
export declare function getActivitySourceFile(activity: CreateActivity<unknown, unknown>): string | undefined;
|
|
13
|
+
export type ActivityBundle = {
|
|
14
|
+
implementation: CreateActivity<unknown, unknown>;
|
|
15
|
+
config?: ActivityConfig;
|
|
16
|
+
name?: string;
|
|
17
|
+
sourceFile?: string;
|
|
18
|
+
};
|
|
19
|
+
export type WorkflowSourceArtifact = {
|
|
20
|
+
workflowName: string;
|
|
21
|
+
workflowSource: string;
|
|
22
|
+
};
|
|
23
|
+
export type WorkflowBuildResult = WorkflowSourceArtifact & {
|
|
24
|
+
activities: Record<string, ActivityBundle>;
|
|
25
|
+
};
|
|
26
|
+
export type TemporalWorkflowBuildOptions = {
|
|
27
|
+
workflowName: string;
|
|
28
|
+
activitiesImportPath?: string;
|
|
29
|
+
proxyOptions?: ActivityOptions | string;
|
|
30
|
+
};
|
|
31
|
+
export declare function createActivity<TInput, TOutput>(activity: CreateActivity<TInput, TOutput>): CreateActivity<TInput, TOutput>;
|
|
32
|
+
export declare function createActivity<TInput, TOutput, Id extends string>(activity: CreateActivity<TInput, TOutput>, config: ActivityConfig<Id>): ConfiguredActivityReference<TInput, TOutput, Id>;
|
|
33
|
+
type TupleToObject<Refs> = Refs extends readonly [infer Head, ...infer Tail] ? (Head extends ConfiguredActivityReference<never, infer Output, infer Id> ? {
|
|
34
|
+
[K in Id]: Output;
|
|
35
|
+
} : Record<never, never>) & TupleToObject<Tail extends readonly ConfiguredActivityReference<never, unknown, string>[] ? Tail : []> : object;
|
|
36
|
+
type Simplify<T> = {
|
|
37
|
+
[K in keyof T]: T[K];
|
|
38
|
+
};
|
|
39
|
+
export type ParallelOutputs<TInput, Refs extends readonly ConfiguredActivityReference<TInput, unknown, string>[]> = Simplify<TupleToObject<Refs>>;
|
|
40
|
+
export type WorkflowCollectionBuildResult = {
|
|
41
|
+
workflows: WorkflowSourceArtifact[];
|
|
42
|
+
activities: Record<string, ActivityBundle>;
|
|
43
|
+
};
|
|
44
|
+
export {};
|
|
45
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAE3D,MAAM,MAAM,cAAc,CAAC,MAAM,GAAG,OAAO,EAAE,OAAO,GAAG,OAAO,IAAI,CAChE,KAAK,EAAE,MAAM,KACV,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;AAE/B,MAAM,MAAM,cAAc,CAAC,EAAE,SAAS,MAAM,GAAG,MAAM,IAAI,eAAe,GAAG;IACzE,EAAE,CAAC,EAAE,EAAE,CAAA;CACR,CAAA;AAID,eAAO,MAAM,sBAAsB,eAAiE,CAAA;AAMpG,MAAM,MAAM,2BAA2B,CACrC,MAAM,GAAG,OAAO,EAChB,OAAO,GAAG,OAAO,EACjB,EAAE,SAAS,MAAM,GAAG,MAAM,IACxB;IACF,QAAQ,EAAE,cAAc,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACzC,MAAM,EAAE,cAAc,CAAC,EAAE,CAAC,CAAA;CAC3B,CAAA;AAED,MAAM,MAAM,iBAAiB,CAAC,MAAM,GAAG,OAAO,EAAE,OAAO,GAAG,OAAO,EAAE,EAAE,SAAS,MAAM,GAAG,MAAM,IACzF,cAAc,CAAC,MAAM,EAAE,OAAO,CAAC,GAC/B,2BAA2B,CAAC,MAAM,EAAE,OAAO,EAAE,EAAE,CAAC,CAAA;AAiEpD,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,cAAc,CAAC,OAAO,EAAE,OAAO,CAAC,GACzC,MAAM,GAAG,SAAS,CAIpB;AAED,MAAM,MAAM,cAAc,GAAG;IAC3B,cAAc,EAAE,cAAc,CAAC,OAAO,EAAE,OAAO,CAAC,CAAA;IAChD,MAAM,CAAC,EAAE,cAAc,CAAA;IACvB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB,CAAA;AAED,MAAM,MAAM,sBAAsB,GAAG;IACnC,YAAY,EAAE,MAAM,CAAA;IACpB,cAAc,EAAE,MAAM,CAAA;CACvB,CAAA;AAED,MAAM,MAAM,mBAAmB,GAAG,sBAAsB,GAAG;IACzD,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAA;CAC3C,CAAA;AAED,MAAM,MAAM,4BAA4B,GAAG;IACzC,YAAY,EAAE,MAAM,CAAA;IACpB,oBAAoB,CAAC,EAAE,MAAM,CAAA;IAC7B,YAAY,CAAC,EAAE,eAAe,GAAG,MAAM,CAAA;CACxC,CAAA;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,OAAO,EAC5C,QAAQ,EAAE,cAAc,CAAC,MAAM,EAAE,OAAO,CAAC,GACxC,cAAc,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;AAClC,wBAAgB,cAAc,CAAC,MAAM,EAAE,OAAO,EAAE,EAAE,SAAS,MAAM,EAC/D,QAAQ,EAAE,cAAc,CAAC,MAAM,EAAE,OAAO,CAAC,EACzC,MAAM,EAAE,cAAc,CAAC,EAAE,CAAC,GACzB,2BAA2B,CAAC,MAAM,EAAE,OAAO,EAAE,EAAE,CAAC,CAAA;AAiBnD,KAAK,aAAa,CAAC,IAAI,IAAI,IAAI,SAAS,SAAS,CAAC,MAAM,IAAI,EAAE,GAAG,MAAM,IAAI,CAAC,GACxE,CAAC,IAAI,SAAS,2BAA2B,CAAC,KAAK,EAAE,MAAM,MAAM,EAAE,MAAM,EAAE,CAAC,GACpE;KAAG,CAAC,IAAI,EAAE,GAAG,MAAM;CAAE,GACrB,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,GACvB,aAAa,CACX,IAAI,SAAS,SAAS,2BAA2B,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,GAAG,IAAI,GAAG,EAAE,CACxF,GACH,MAAM,CAAA;AAEV,KAAK,QAAQ,CAAC,CAAC,IAAI;KAAG,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;CAAE,CAAA;AAE3C,MAAM,MAAM,eAAe,CACzB,MAAM,EACN,IAAI,SAAS,SAAS,2BAA2B,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,IAC1E,QAAQ,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,CAAA;AAEjC,MAAM,MAAM,6BAA6B,GAAG;IAC1C,SAAS,EAAE,sBAAsB,EAAE,CAAA;IACnC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAA;CAC3C,CAAA;AAED,OAAO,EAAE,CAAA"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { fileURLToPath } from 'node:url';
|
|
2
|
+
const ACTIVITY_SELF_SEGMENT = '/temporal-graph-tools/';
|
|
3
|
+
export const ACTIVITY_SOURCE_SYMBOL = Symbol.for('@primero-ai/temporal-graph-tools/activity-source');
|
|
4
|
+
function captureActivitySource() {
|
|
5
|
+
// eslint-disable-next-line @typescript-eslint/unbound-method
|
|
6
|
+
const originalPrepare = Error.prepareStackTrace;
|
|
7
|
+
try {
|
|
8
|
+
Error.prepareStackTrace = (_, structuredStackTrace) => structuredStackTrace;
|
|
9
|
+
const error = new Error();
|
|
10
|
+
Error.captureStackTrace(error, createActivity);
|
|
11
|
+
const callsites = error.stack;
|
|
12
|
+
if (!callsites) {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
for (const site of callsites) {
|
|
16
|
+
const fileName = typeof site.getFileName === 'function' ? site.getFileName.call(site) : undefined;
|
|
17
|
+
const scriptName = typeof site.getScriptNameOrSourceURL === 'function'
|
|
18
|
+
? site.getScriptNameOrSourceURL.call(site)
|
|
19
|
+
: undefined;
|
|
20
|
+
const resolved = fileName !== null && fileName !== void 0 ? fileName : scriptName;
|
|
21
|
+
if (!resolved) {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
const normalized = resolved.startsWith('file:') ? fileURLToPath(resolved) : resolved;
|
|
25
|
+
const comparable = normalized.replace(/\\/g, '/').replace(/^file:\/\//, '');
|
|
26
|
+
if (comparable.includes(ACTIVITY_SELF_SEGMENT)) {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
return normalized;
|
|
30
|
+
}
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
Error.prepareStackTrace = originalPrepare;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function recordActivityMetadata(activity) {
|
|
38
|
+
if (Reflect.get(activity, ACTIVITY_SOURCE_SYMBOL)) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const metadata = {
|
|
42
|
+
sourceFile: captureActivitySource(),
|
|
43
|
+
};
|
|
44
|
+
Reflect.defineProperty(activity, ACTIVITY_SOURCE_SYMBOL, {
|
|
45
|
+
value: metadata,
|
|
46
|
+
configurable: false,
|
|
47
|
+
enumerable: false,
|
|
48
|
+
writable: false,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
export function getActivitySourceFile(activity) {
|
|
52
|
+
const metadata = Reflect.get(activity, ACTIVITY_SOURCE_SYMBOL);
|
|
53
|
+
return metadata === null || metadata === void 0 ? void 0 : metadata.sourceFile;
|
|
54
|
+
}
|
|
55
|
+
export function createActivity(activity, config) {
|
|
56
|
+
recordActivityMetadata(activity);
|
|
57
|
+
if (!config) {
|
|
58
|
+
return activity;
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
activity,
|
|
62
|
+
config,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deep-equal.d.ts","sourceRoot":"","sources":["../../../src/utils/deep-equal.ts"],"names":[],"mappings":"AAWA,wBAAgB,SAAS,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CA2DhE"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
function isPlainObject(value) {
|
|
2
|
+
if (value === null || typeof value !== 'object') {
|
|
3
|
+
return false;
|
|
4
|
+
}
|
|
5
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
6
|
+
const prototype = Object.getPrototypeOf(value);
|
|
7
|
+
return prototype === Object.prototype || prototype === null;
|
|
8
|
+
}
|
|
9
|
+
export function deepEqual(left, right) {
|
|
10
|
+
if (Object.is(left, right)) {
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
if (left === null || right === null) {
|
|
14
|
+
return left === right;
|
|
15
|
+
}
|
|
16
|
+
if (left instanceof Date && right instanceof Date) {
|
|
17
|
+
return left.getTime() === right.getTime();
|
|
18
|
+
}
|
|
19
|
+
if (left instanceof RegExp && right instanceof RegExp) {
|
|
20
|
+
return left.source === right.source && left.flags === right.flags;
|
|
21
|
+
}
|
|
22
|
+
if (Array.isArray(left) || Array.isArray(right)) {
|
|
23
|
+
if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
for (let index = 0; index < left.length; index += 1) {
|
|
27
|
+
if (!deepEqual(left[index], right[index])) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
if (isPlainObject(left) || isPlainObject(right)) {
|
|
34
|
+
if (!isPlainObject(left) || !isPlainObject(right)) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
const leftEntries = Object.entries(left).filter(([, value]) => value !== undefined);
|
|
38
|
+
const rightEntries = Object.entries(right).filter(([, value]) => value !== undefined);
|
|
39
|
+
if (leftEntries.length !== rightEntries.length) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
const sortedLeft = leftEntries.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
43
|
+
const sortedRight = rightEntries.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
44
|
+
for (let index = 0; index < sortedLeft.length; index += 1) {
|
|
45
|
+
const [leftKey, leftValue] = sortedLeft[index];
|
|
46
|
+
const [rightKey, rightValue] = sortedRight[index];
|
|
47
|
+
if (leftKey !== rightKey || !deepEqual(leftValue, rightValue)) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ActivityConfig, ActivityReference, ConfiguredActivityReference, ParallelOutputs, TemporalWorkflowBuildOptions, WorkflowBuildResult } from '../types.js';
|
|
2
|
+
export declare class WorkflowBuilder<TCurrentOutput> {
|
|
3
|
+
private readonly activityBundles;
|
|
4
|
+
private readonly plan;
|
|
5
|
+
private readonly baseOptions;
|
|
6
|
+
private started;
|
|
7
|
+
private autoIncrement;
|
|
8
|
+
constructor(options: TemporalWorkflowBuildOptions);
|
|
9
|
+
then<TNextOutput>(reference: ActivityReference<TCurrentOutput, TNextOutput>, config?: ActivityConfig): WorkflowBuilder<TNextOutput>;
|
|
10
|
+
parallel<const References extends readonly ConfiguredActivityReference<TCurrentOutput, unknown, string>[]>(references: References): WorkflowBuilder<ParallelOutputs<TCurrentOutput, References>>;
|
|
11
|
+
commit(): WorkflowBuildResult;
|
|
12
|
+
private normalizeActivityReference;
|
|
13
|
+
private registerActivity;
|
|
14
|
+
private createActivityBundle;
|
|
15
|
+
private prepareConfig;
|
|
16
|
+
private generateTemporalWorkflowSource;
|
|
17
|
+
private formatProxyOptions;
|
|
18
|
+
private ensureUniqueIdentifier;
|
|
19
|
+
private formatPropertyKey;
|
|
20
|
+
private assignActivityKey;
|
|
21
|
+
private normalizeActivityKey;
|
|
22
|
+
private activityBundlesEqual;
|
|
23
|
+
private deriveActivityKey;
|
|
24
|
+
private assertStarted;
|
|
25
|
+
}
|
|
26
|
+
export declare const createWorkflowBuilder: <TInitialInput>(options: TemporalWorkflowBuildOptions) => WorkflowBuilder<TInitialInput>;
|
|
27
|
+
//# sourceMappingURL=builder.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../../../src/workflow/builder.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAEV,cAAc,EACd,iBAAiB,EACjB,2BAA2B,EAE3B,eAAe,EACf,4BAA4B,EAC5B,mBAAmB,EACpB,MAAM,aAAa,CAAA;AAepB,qBAAa,eAAe,CAAC,cAAc;IACzC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAqC;IAErE,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAc;IAEnC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA8B;IAE1D,OAAO,CAAC,OAAO,CAAQ;IAEvB,OAAO,CAAC,aAAa,CAAI;gBAEb,OAAO,EAAE,4BAA4B;IAejD,IAAI,CAAC,WAAW,EACd,SAAS,EAAE,iBAAiB,CAAC,cAAc,EAAE,WAAW,CAAC,EACzD,MAAM,CAAC,EAAE,cAAc,GACtB,eAAe,CAAC,WAAW,CAAC;IAU/B,QAAQ,CACN,KAAK,CAAC,UAAU,SAAS,SAAS,2BAA2B,CAC3D,cAAc,EACd,OAAO,EACP,MAAM,CACP,EAAE,EACH,UAAU,EAAE,UAAU,GAAG,eAAe,CAAC,eAAe,CAAC,cAAc,EAAE,UAAU,CAAC,CAAC;IAiBvF,MAAM,IAAI,mBAAmB;IAgB7B,OAAO,CAAC,0BAA0B;IAiBlC,OAAO,CAAC,gBAAgB;IAOxB,OAAO,CAAC,oBAAoB;IAa5B,OAAO,CAAC,aAAa;IAUrB,OAAO,CAAC,8BAA8B;IAwFtC,OAAO,CAAC,kBAAkB;IAc1B,OAAO,CAAC,sBAAsB;IAqC9B,OAAO,CAAC,iBAAiB;IAQzB,OAAO,CAAC,iBAAiB;IAkCzB,OAAO,CAAC,oBAAoB;IAU5B,OAAO,CAAC,oBAAoB;IAO5B,OAAO,CAAC,iBAAiB;IAYzB,OAAO,CAAC,aAAa;CAKtB;AAED,eAAO,MAAM,qBAAqB,GAAI,aAAa,EACjD,SAAS,4BAA4B,KACpC,eAAe,CAAC,aAAa,CAE/B,CAAA"}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { getActivitySourceFile } from '../types.js';
|
|
2
|
+
import { deepEqual } from '../utils/deep-equal.js';
|
|
3
|
+
export class WorkflowBuilder {
|
|
4
|
+
constructor(options) {
|
|
5
|
+
this.activityBundles = {};
|
|
6
|
+
this.plan = [];
|
|
7
|
+
this.started = false;
|
|
8
|
+
this.autoIncrement = 0;
|
|
9
|
+
const workflowName = options === null || options === void 0 ? void 0 : options.workflowName;
|
|
10
|
+
if (typeof workflowName !== 'string' || workflowName.trim().length === 0) {
|
|
11
|
+
throw new Error('createWorkflowBuilder requires options.workflowName to be a non-empty string.');
|
|
12
|
+
}
|
|
13
|
+
this.baseOptions = {
|
|
14
|
+
...options,
|
|
15
|
+
workflowName: workflowName.trim(),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
then(reference, config) {
|
|
19
|
+
const entry = this.normalizeActivityReference(reference, config);
|
|
20
|
+
const normalized = this.registerActivity(entry);
|
|
21
|
+
this.plan.push({ type: 'step', step: normalized });
|
|
22
|
+
this.started = true;
|
|
23
|
+
return this;
|
|
24
|
+
}
|
|
25
|
+
parallel(references) {
|
|
26
|
+
this.assertStarted('parallel');
|
|
27
|
+
if (!Array.isArray(references) || references.length === 0) {
|
|
28
|
+
throw new Error('parallel() requires at least one activity reference.');
|
|
29
|
+
}
|
|
30
|
+
const normalized = references.map((ref) => this.registerActivity(this.normalizeActivityReference(ref)));
|
|
31
|
+
this.plan.push({ type: 'parallel', steps: normalized });
|
|
32
|
+
this.started = true;
|
|
33
|
+
return this;
|
|
34
|
+
}
|
|
35
|
+
commit() {
|
|
36
|
+
if (!this.started) {
|
|
37
|
+
throw new Error('Cannot build a workflow without any steps. Call then() before build().');
|
|
38
|
+
}
|
|
39
|
+
const { source: workflowSource, workflowName } = this.generateTemporalWorkflowSource({
|
|
40
|
+
...this.baseOptions,
|
|
41
|
+
});
|
|
42
|
+
return {
|
|
43
|
+
workflowName,
|
|
44
|
+
activities: { ...this.activityBundles },
|
|
45
|
+
workflowSource,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
normalizeActivityReference(reference, inlineConfig) {
|
|
49
|
+
var _a;
|
|
50
|
+
if (typeof reference === 'function') {
|
|
51
|
+
return {
|
|
52
|
+
activity: reference,
|
|
53
|
+
config: { ...(inlineConfig !== null && inlineConfig !== void 0 ? inlineConfig : {}) },
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
activity: reference.activity,
|
|
58
|
+
config: { ...((_a = reference.config) !== null && _a !== void 0 ? _a : {}), ...(inlineConfig !== null && inlineConfig !== void 0 ? inlineConfig : {}) },
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
registerActivity(entry) {
|
|
62
|
+
var _a;
|
|
63
|
+
const baseKey = (_a = entry.config.id) !== null && _a !== void 0 ? _a : this.deriveActivityKey(entry.activity);
|
|
64
|
+
const key = this.assignActivityKey(baseKey, entry);
|
|
65
|
+
return { key };
|
|
66
|
+
}
|
|
67
|
+
createActivityBundle(entry, key) {
|
|
68
|
+
var _a;
|
|
69
|
+
const config = this.prepareConfig(entry.config);
|
|
70
|
+
const name = (_a = entry.activity.name) === null || _a === void 0 ? void 0 : _a.trim();
|
|
71
|
+
const sourceFile = getActivitySourceFile(entry.activity);
|
|
72
|
+
return {
|
|
73
|
+
implementation: entry.activity,
|
|
74
|
+
...(name ? { name } : { name: key }),
|
|
75
|
+
...(config ? { config } : {}),
|
|
76
|
+
...(sourceFile ? { sourceFile } : {}),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
prepareConfig(config) {
|
|
80
|
+
const entries = Object.entries(config).filter(([, value]) => value !== undefined);
|
|
81
|
+
if (entries.length === 0) {
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
return Object.fromEntries(entries);
|
|
85
|
+
}
|
|
86
|
+
generateTemporalWorkflowSource(options) {
|
|
87
|
+
var _a;
|
|
88
|
+
const topLevelIdentifiers = new Set();
|
|
89
|
+
const workflowName = this.ensureUniqueIdentifier(options.workflowName, options.workflowName, topLevelIdentifiers);
|
|
90
|
+
const activitiesIdentifier = this.ensureUniqueIdentifier('activities', 'activities', topLevelIdentifiers);
|
|
91
|
+
const activitiesImportPath = (_a = options.activitiesImportPath) !== null && _a !== void 0 ? _a : './activities';
|
|
92
|
+
const proxyOptionsLiteral = this.formatProxyOptions(options.proxyOptions);
|
|
93
|
+
const lines = [];
|
|
94
|
+
lines.push(`import { proxyActivities } from '@temporalio/workflow'`);
|
|
95
|
+
lines.push(`import type { Activities } from '${activitiesImportPath}'`);
|
|
96
|
+
lines.push('');
|
|
97
|
+
lines.push(`const ${activitiesIdentifier} = proxyActivities<Activities>(${proxyOptionsLiteral})`);
|
|
98
|
+
lines.push('');
|
|
99
|
+
lines.push(`export async function ${workflowName}(input: unknown): Promise<unknown> {`);
|
|
100
|
+
let currentValue = 'input';
|
|
101
|
+
const localIdentifiers = new Set(['input']);
|
|
102
|
+
this.plan.forEach((stage, stageIndex) => {
|
|
103
|
+
if (stage.type === 'step') {
|
|
104
|
+
const resultVar = this.ensureUniqueIdentifier(undefined, `step${stageIndex}`, localIdentifiers);
|
|
105
|
+
lines.push(` const ${resultVar} = await ${activitiesIdentifier}.${stage.step.key}(${currentValue});`);
|
|
106
|
+
lines.push('');
|
|
107
|
+
currentValue = resultVar;
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const individualVars = stage.steps.map((step, index) => this.ensureUniqueIdentifier(undefined, `parallel${stageIndex}_${index}`, localIdentifiers));
|
|
111
|
+
lines.push(` const [${individualVars.join(', ')}] = await Promise.all([`);
|
|
112
|
+
stage.steps.forEach((step, index) => {
|
|
113
|
+
const suffix = index === stage.steps.length - 1 ? '' : ',';
|
|
114
|
+
lines.push(` ${activitiesIdentifier}.${step.key}(${currentValue})${suffix}`);
|
|
115
|
+
});
|
|
116
|
+
lines.push(' ])');
|
|
117
|
+
const aggregateVar = this.ensureUniqueIdentifier(undefined, `parallel${stageIndex}`, localIdentifiers);
|
|
118
|
+
lines.push(` const ${aggregateVar} = {`);
|
|
119
|
+
stage.steps.forEach((step, index) => {
|
|
120
|
+
const propertyKey = this.formatPropertyKey(step.key);
|
|
121
|
+
lines.push(` ${propertyKey}: ${individualVars[index]},`);
|
|
122
|
+
});
|
|
123
|
+
lines.push(' }');
|
|
124
|
+
lines.push('');
|
|
125
|
+
currentValue = aggregateVar;
|
|
126
|
+
});
|
|
127
|
+
lines.push(` return ${currentValue};`);
|
|
128
|
+
lines.push('}');
|
|
129
|
+
return {
|
|
130
|
+
workflowName,
|
|
131
|
+
source: lines.join('\n'),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
formatProxyOptions(options) {
|
|
135
|
+
if (typeof options === 'string' && options.trim().length > 0) {
|
|
136
|
+
return options.trim();
|
|
137
|
+
}
|
|
138
|
+
if (options === undefined) {
|
|
139
|
+
return `{
|
|
140
|
+
startToCloseTimeout: '1 minute',
|
|
141
|
+
}`;
|
|
142
|
+
}
|
|
143
|
+
return JSON.stringify(options, null, 2);
|
|
144
|
+
}
|
|
145
|
+
ensureUniqueIdentifier(raw, fallback, used) {
|
|
146
|
+
const fallbackValue = fallback || 'value';
|
|
147
|
+
const base = raw && raw.trim().length > 0 ? raw.trim() : fallbackValue;
|
|
148
|
+
let sanitized = base.replace(/[^A-Za-z0-9_]/g, '_');
|
|
149
|
+
if (!/^[A-Za-z_]/.test(sanitized)) {
|
|
150
|
+
sanitized = `_${sanitized}`;
|
|
151
|
+
}
|
|
152
|
+
if (sanitized.length === 0 || /^_+$/.test(sanitized)) {
|
|
153
|
+
sanitized = fallbackValue.replace(/[^A-Za-z0-9_]/g, '_');
|
|
154
|
+
if (!/^[A-Za-z_]/.test(sanitized)) {
|
|
155
|
+
sanitized = `_${sanitized}`;
|
|
156
|
+
}
|
|
157
|
+
if (sanitized.length === 0 || /^_+$/.test(sanitized)) {
|
|
158
|
+
sanitized = 'value';
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
let candidate = sanitized;
|
|
162
|
+
let counter = 1;
|
|
163
|
+
while (used.has(candidate)) {
|
|
164
|
+
candidate = `${sanitized}_${counter++}`;
|
|
165
|
+
}
|
|
166
|
+
used.add(candidate);
|
|
167
|
+
return candidate;
|
|
168
|
+
}
|
|
169
|
+
formatPropertyKey(key) {
|
|
170
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
|
|
171
|
+
return key;
|
|
172
|
+
}
|
|
173
|
+
return `'${key.replace(/'/g, "\\'")}'`;
|
|
174
|
+
}
|
|
175
|
+
assignActivityKey(candidate, entry) {
|
|
176
|
+
const normalized = this.normalizeActivityKey(candidate);
|
|
177
|
+
const existing = this.activityBundles[normalized];
|
|
178
|
+
const initialBundle = this.createActivityBundle(entry, normalized);
|
|
179
|
+
if (!existing) {
|
|
180
|
+
this.activityBundles[normalized] = initialBundle;
|
|
181
|
+
return normalized;
|
|
182
|
+
}
|
|
183
|
+
if (this.activityBundlesEqual(existing, initialBundle)) {
|
|
184
|
+
return normalized;
|
|
185
|
+
}
|
|
186
|
+
let counter = 1;
|
|
187
|
+
while (true) {
|
|
188
|
+
const nextKey = `${normalized}_${counter++}`;
|
|
189
|
+
const nextBundle = this.createActivityBundle(entry, nextKey);
|
|
190
|
+
const existingBundle = this.activityBundles[nextKey];
|
|
191
|
+
if (!existingBundle) {
|
|
192
|
+
this.activityBundles[nextKey] = nextBundle;
|
|
193
|
+
return nextKey;
|
|
194
|
+
}
|
|
195
|
+
if (this.activityBundlesEqual(existingBundle, nextBundle)) {
|
|
196
|
+
return nextKey;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
normalizeActivityKey(candidate) {
|
|
201
|
+
const key = candidate.trim();
|
|
202
|
+
if (key.length === 0) {
|
|
203
|
+
throw new Error('Activity keys must be non-empty strings.');
|
|
204
|
+
}
|
|
205
|
+
return key;
|
|
206
|
+
}
|
|
207
|
+
activityBundlesEqual(existing, candidate) {
|
|
208
|
+
var _a, _b, _c, _d;
|
|
209
|
+
const configsEqual = deepEqual((_a = existing.config) !== null && _a !== void 0 ? _a : undefined, (_b = candidate.config) !== null && _b !== void 0 ? _b : undefined);
|
|
210
|
+
const namesEqual = ((_c = existing.name) !== null && _c !== void 0 ? _c : null) === ((_d = candidate.name) !== null && _d !== void 0 ? _d : null);
|
|
211
|
+
return existing.implementation === candidate.implementation && configsEqual && namesEqual;
|
|
212
|
+
}
|
|
213
|
+
deriveActivityKey(activity) {
|
|
214
|
+
var _a;
|
|
215
|
+
const candidate = (_a = activity.name) === null || _a === void 0 ? void 0 : _a.trim();
|
|
216
|
+
if (candidate) {
|
|
217
|
+
return candidate;
|
|
218
|
+
}
|
|
219
|
+
this.autoIncrement += 1;
|
|
220
|
+
return `step_${this.autoIncrement}`;
|
|
221
|
+
}
|
|
222
|
+
assertStarted(method) {
|
|
223
|
+
if (!this.started) {
|
|
224
|
+
throw new Error(`Cannot call ${method}() before defining the first step.`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
export const createWorkflowBuilder = (options) => {
|
|
229
|
+
return new WorkflowBuilder(options);
|
|
230
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"collection.d.ts","sourceRoot":"","sources":["../../../src/workflow/collection.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,mBAAmB,EACnB,6BAA6B,EAE9B,MAAM,aAAa,CAAA;AAGpB,wBAAgB,2BAA2B,CACzC,OAAO,EAAE,SAAS,mBAAmB,EAAE,GACtC,6BAA6B,CAmD/B"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { deepEqual } from '../utils/deep-equal.js';
|
|
2
|
+
export function collectWorkflowBuildResults(results) {
|
|
3
|
+
if (results.length === 0) {
|
|
4
|
+
throw new Error('collectWorkflowBuildResults requires at least one workflow build result.');
|
|
5
|
+
}
|
|
6
|
+
const workflows = [];
|
|
7
|
+
const activities = {};
|
|
8
|
+
const workflowNames = new Set();
|
|
9
|
+
results.forEach((result, index) => {
|
|
10
|
+
if (workflowNames.has(result.workflowName)) {
|
|
11
|
+
throw new Error(`Duplicate workflow name detected at index ${index}: '${result.workflowName}'. Workflow names must be unique.`);
|
|
12
|
+
}
|
|
13
|
+
workflowNames.add(result.workflowName);
|
|
14
|
+
workflows.push({
|
|
15
|
+
workflowName: result.workflowName,
|
|
16
|
+
workflowSource: result.workflowSource,
|
|
17
|
+
});
|
|
18
|
+
Object.entries(result.activities).forEach(([key, bundle]) => {
|
|
19
|
+
var _a, _b, _c, _d, _e, _f;
|
|
20
|
+
const existing = activities[key];
|
|
21
|
+
if (!existing) {
|
|
22
|
+
activities[key] = bundle;
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const configMatches = deepEqual((_a = existing.config) !== null && _a !== void 0 ? _a : undefined, (_b = bundle.config) !== null && _b !== void 0 ? _b : undefined);
|
|
26
|
+
const sourceMatches = ((_c = existing.sourceFile) !== null && _c !== void 0 ? _c : null) === ((_d = bundle.sourceFile) !== null && _d !== void 0 ? _d : null);
|
|
27
|
+
if (existing.implementation !== bundle.implementation ||
|
|
28
|
+
!configMatches ||
|
|
29
|
+
((_e = existing.name) !== null && _e !== void 0 ? _e : null) !== ((_f = bundle.name) !== null && _f !== void 0 ? _f : null) ||
|
|
30
|
+
!sourceMatches) {
|
|
31
|
+
throw new Error(`Activity '${key}' is defined multiple times with conflicting implementations.`);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
return {
|
|
36
|
+
workflows,
|
|
37
|
+
activities,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { WorkflowBuildResult, WorkflowSourceArtifact } from './types.js';
|
|
2
|
+
export type ActivityImplementations = Record<string, (...args: unknown[]) => unknown>;
|
|
3
|
+
export type ActivityBundles = WorkflowBuildResult['activities'];
|
|
4
|
+
export declare function instantiateActivities(bundles: ActivityBundles): Promise<ActivityImplementations>;
|
|
5
|
+
export declare function buildWorkflowBundleCode(workflow: string | WorkflowSourceArtifact | WorkflowSourceArtifact[], filename?: string): Promise<string>;
|
|
6
|
+
//# sourceMappingURL=workflow-bundler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workflow-bundler.d.ts","sourceRoot":"","sources":["../../src/workflow-bundler.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAA;AAE7E,MAAM,MAAM,uBAAuB,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,CAAA;AAErF,MAAM,MAAM,eAAe,GAAG,mBAAmB,CAAC,YAAY,CAAC,CAAA;AAE/D,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,eAAe,GACvB,OAAO,CAAC,uBAAuB,CAAC,CAQlC;AAED,wBAAsB,uBAAuB,CAC3C,QAAQ,EAAE,MAAM,GAAG,sBAAsB,GAAG,sBAAsB,EAAE,EACpE,QAAQ,SAAgB,GACvB,OAAO,CAAC,MAAM,CAAC,CAMjB"}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import * as realFS from 'node:fs';
|
|
2
|
+
import path, { basename } from 'node:path';
|
|
3
|
+
import { WorkflowCodeBundler, } from '@temporalio/worker/lib/workflow/bundler.js';
|
|
4
|
+
import { createFsFromVolume, Volume } from 'memfs';
|
|
5
|
+
import { Union } from 'unionfs';
|
|
6
|
+
export async function instantiateActivities(bundles) {
|
|
7
|
+
const implementations = {};
|
|
8
|
+
Object.entries(bundles).forEach(([key, bundle]) => {
|
|
9
|
+
implementations[key] = bundle.implementation;
|
|
10
|
+
});
|
|
11
|
+
return implementations;
|
|
12
|
+
}
|
|
13
|
+
export async function buildWorkflowBundleCode(workflow, filename = 'workflow.js') {
|
|
14
|
+
const workflows = normalizeWorkflows(workflow);
|
|
15
|
+
const bundler = new VirtualWorkflowCodeBundler(workflows);
|
|
16
|
+
const { code } = await bundler.createBundle();
|
|
17
|
+
return ensureInlineSourceMap(code, filename);
|
|
18
|
+
}
|
|
19
|
+
function normalizeWorkflows(workflow) {
|
|
20
|
+
if (Array.isArray(workflow)) {
|
|
21
|
+
if (workflow.length === 0) {
|
|
22
|
+
throw new Error('buildWorkflowBundleCode requires at least one workflow source.');
|
|
23
|
+
}
|
|
24
|
+
return workflow;
|
|
25
|
+
}
|
|
26
|
+
if (typeof workflow === 'string') {
|
|
27
|
+
return [
|
|
28
|
+
{
|
|
29
|
+
workflowName: 'workflow',
|
|
30
|
+
workflowSource: workflow,
|
|
31
|
+
},
|
|
32
|
+
];
|
|
33
|
+
}
|
|
34
|
+
return [workflow];
|
|
35
|
+
}
|
|
36
|
+
function sanitizeFileName(candidate) {
|
|
37
|
+
const base = candidate.trim().replace(/[^A-Za-z0-9_.-]/g, '_');
|
|
38
|
+
if (base.length > 0 && !/^_+$/.test(base)) {
|
|
39
|
+
return `${base}.ts`;
|
|
40
|
+
}
|
|
41
|
+
return 'workflow.ts';
|
|
42
|
+
}
|
|
43
|
+
function ensureUniqueFileName(baseName, used) {
|
|
44
|
+
let candidate = baseName;
|
|
45
|
+
let counter = 1;
|
|
46
|
+
while (used.has(candidate)) {
|
|
47
|
+
const dotIndex = baseName.lastIndexOf('.');
|
|
48
|
+
const prefix = dotIndex > 0 ? baseName.slice(0, dotIndex) : baseName;
|
|
49
|
+
const extension = dotIndex > 0 ? baseName.slice(dotIndex) : '';
|
|
50
|
+
candidate = `${prefix}_${counter++}${extension}`;
|
|
51
|
+
}
|
|
52
|
+
used.add(candidate);
|
|
53
|
+
return candidate;
|
|
54
|
+
}
|
|
55
|
+
function stripExtension(fileName) {
|
|
56
|
+
const parsed = basename(fileName);
|
|
57
|
+
if (parsed.endsWith('.ts')) {
|
|
58
|
+
return parsed.slice(0, -3);
|
|
59
|
+
}
|
|
60
|
+
return parsed;
|
|
61
|
+
}
|
|
62
|
+
function ensureInlineSourceMap(code, filename) {
|
|
63
|
+
const marker = '//# sourceMappingURL=data:application/json;base64,';
|
|
64
|
+
if (!code.includes(marker)) {
|
|
65
|
+
const stubMap = {
|
|
66
|
+
version: 3,
|
|
67
|
+
file: filename,
|
|
68
|
+
sources: ['workflow.ts'],
|
|
69
|
+
sourcesContent: [code],
|
|
70
|
+
names: [],
|
|
71
|
+
mappings: '',
|
|
72
|
+
};
|
|
73
|
+
const base64 = Buffer.from(JSON.stringify(stubMap)).toString('base64');
|
|
74
|
+
return `${code}\n${marker}${base64}`;
|
|
75
|
+
}
|
|
76
|
+
const markerIndex = code.lastIndexOf(marker);
|
|
77
|
+
const base64 = code.slice(markerIndex + marker.length).trim();
|
|
78
|
+
const decoded = Buffer.from(base64, 'base64').toString('utf8');
|
|
79
|
+
const sourceMap = JSON.parse(decoded);
|
|
80
|
+
if (!sourceMap.file || sourceMap.file.length === 0) {
|
|
81
|
+
sourceMap.file = filename;
|
|
82
|
+
}
|
|
83
|
+
if (!Array.isArray(sourceMap.sources) || sourceMap.sources.length === 0) {
|
|
84
|
+
sourceMap.sources = [filename];
|
|
85
|
+
}
|
|
86
|
+
const updatedBase64 = Buffer.from(JSON.stringify(sourceMap)).toString('base64');
|
|
87
|
+
return code.slice(0, markerIndex + marker.length) + updatedBase64;
|
|
88
|
+
}
|
|
89
|
+
class VirtualWorkflowCodeBundler extends WorkflowCodeBundler {
|
|
90
|
+
constructor(workflows) {
|
|
91
|
+
const entry = path.join(VirtualWorkflowCodeBundler.virtualRoot, 'index.ts');
|
|
92
|
+
super({ workflowsPath: entry });
|
|
93
|
+
this.workflows = workflows;
|
|
94
|
+
this.virtualEntrypoint = entry;
|
|
95
|
+
}
|
|
96
|
+
async createBundle() {
|
|
97
|
+
const volume = new Volume();
|
|
98
|
+
const unionFs = new Union();
|
|
99
|
+
this.populateVirtualWorkflows(volume);
|
|
100
|
+
const readdir = Object.assign((...params) => {
|
|
101
|
+
const args = [...params];
|
|
102
|
+
const callbackCandidate = args.pop();
|
|
103
|
+
if (typeof callbackCandidate !== 'function') {
|
|
104
|
+
return realFS.readdir(...params);
|
|
105
|
+
}
|
|
106
|
+
const callback = callbackCandidate;
|
|
107
|
+
const wrappedCallback = (err, files) => {
|
|
108
|
+
if (err !== null) {
|
|
109
|
+
callback(err, files);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (!Array.isArray(files)) {
|
|
113
|
+
callback(null, files);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (!files.every((entry) => typeof entry === 'string')) {
|
|
117
|
+
callback(null, files);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const typedFiles = files;
|
|
121
|
+
const filtered = typedFiles.filter((file) => /\.[jt]s$/.test(path.extname(file)) && !file.endsWith('.d.ts'));
|
|
122
|
+
callback(null, filtered);
|
|
123
|
+
};
|
|
124
|
+
const patchedArgs = [...args, wrappedCallback];
|
|
125
|
+
return realFS.readdir(...patchedArgs);
|
|
126
|
+
}, { __promisify__: realFS.readdir.__promisify__ });
|
|
127
|
+
const memoryFs = createFsFromVolume(volume);
|
|
128
|
+
const layeredFs = { ...realFS, readdir };
|
|
129
|
+
unionFs.use(memoryFs);
|
|
130
|
+
unionFs.use(layeredFs);
|
|
131
|
+
const distDir = '/dist';
|
|
132
|
+
const entrypointPath = this.makeEntrypointPath(unionFs, this.virtualEntrypoint);
|
|
133
|
+
this.genEntrypoint(volume, entrypointPath);
|
|
134
|
+
const bundleFilePath = await this.bundle(unionFs, memoryFs, entrypointPath, distDir);
|
|
135
|
+
let code = memoryFs.readFileSync(bundleFilePath, 'utf8');
|
|
136
|
+
code = code.replace('var __webpack_module_cache__ = {}', 'var __webpack_module_cache__ = globalThis.__webpack_module_cache__');
|
|
137
|
+
const sizeInMb = `${(code.length / (1024 * 1024)).toFixed(2)}MB`;
|
|
138
|
+
this.logger.info('Workflow bundle created', { size: sizeInMb });
|
|
139
|
+
return {
|
|
140
|
+
sourceMap: 'deprecated: this is no longer in use\n',
|
|
141
|
+
code,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
populateVirtualWorkflows(volume) {
|
|
145
|
+
const usedFileNames = new Set();
|
|
146
|
+
const moduleSpecifiers = [];
|
|
147
|
+
const baseDir = VirtualWorkflowCodeBundler.virtualRoot;
|
|
148
|
+
this.workflows.forEach((entry, index) => {
|
|
149
|
+
var _a;
|
|
150
|
+
const fileName = this.workflows.length === 1
|
|
151
|
+
? 'workflow.ts'
|
|
152
|
+
: ensureUniqueFileName(sanitizeFileName((_a = entry.workflowName) !== null && _a !== void 0 ? _a : `workflow_${index}`), usedFileNames);
|
|
153
|
+
const filePath = path.join(baseDir, fileName);
|
|
154
|
+
volume.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
155
|
+
volume.writeFileSync(filePath, `${entry.workflowSource}\n`);
|
|
156
|
+
moduleSpecifiers.push(`./${stripExtension(fileName)}`);
|
|
157
|
+
});
|
|
158
|
+
const entrySource = moduleSpecifiers.map((specifier) => `export * from '${specifier}'`).join('\n') + '\n';
|
|
159
|
+
volume.mkdirSync(path.dirname(this.virtualEntrypoint), { recursive: true });
|
|
160
|
+
volume.writeFileSync(this.virtualEntrypoint, entrySource);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
VirtualWorkflowCodeBundler.virtualRoot = path.join(process.cwd(), '__temporal_virtual__', 'workflows');
|
package/package.json
CHANGED