@aikirun/worker 0.7.0 → 0.9.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/README.md +13 -13
- package/dist/index.d.ts +30 -20
- package/dist/index.js +42 -49
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -20,9 +20,9 @@ import { onboardingWorkflowV1 } from "./workflows.ts";
|
|
|
20
20
|
|
|
21
21
|
// Define worker
|
|
22
22
|
const aikiWorker = worker({
|
|
23
|
-
|
|
23
|
+
name: "worker-1",
|
|
24
24
|
workflows: [onboardingWorkflowV1],
|
|
25
|
-
subscriber: { type: "
|
|
25
|
+
subscriber: { type: "redis" },
|
|
26
26
|
opts: {
|
|
27
27
|
maxConcurrentWorkflowRuns: 10,
|
|
28
28
|
},
|
|
@@ -30,7 +30,7 @@ const aikiWorker = worker({
|
|
|
30
30
|
|
|
31
31
|
// Initialize client
|
|
32
32
|
const aikiClient = await client({
|
|
33
|
-
url: "http://localhost:
|
|
33
|
+
url: "http://localhost:9876",
|
|
34
34
|
redis: { host: "localhost", port: 6379 },
|
|
35
35
|
});
|
|
36
36
|
|
|
@@ -68,8 +68,8 @@ Scale workers by creating separate definitions to isolate workflows or shard by
|
|
|
68
68
|
|
|
69
69
|
```typescript
|
|
70
70
|
// Separate workers by workflow type
|
|
71
|
-
const orderWorker = worker({
|
|
72
|
-
const emailWorker = worker({
|
|
71
|
+
const orderWorker = worker({ name: "orders", workflows: [orderWorkflowV1] });
|
|
72
|
+
const emailWorker = worker({ name: "emails", workflows: [emailWorkflowV1] });
|
|
73
73
|
|
|
74
74
|
await orderWorker.spawn(client);
|
|
75
75
|
await emailWorker.spawn(client);
|
|
@@ -77,10 +77,10 @@ await emailWorker.spawn(client);
|
|
|
77
77
|
|
|
78
78
|
```typescript
|
|
79
79
|
// Shard workers by key (reuse base definition with different shards)
|
|
80
|
-
const orderWorker = worker({
|
|
80
|
+
const orderWorker = worker({ name: "order-processor", workflows: [orderWorkflowV1] });
|
|
81
81
|
|
|
82
|
-
await orderWorker.with().opt("
|
|
83
|
-
await orderWorker.with().opt("
|
|
82
|
+
await orderWorker.with().opt("shards", ["us-east", "us-west"]).spawn(client);
|
|
83
|
+
await orderWorker.with().opt("shards", ["eu-west"]).spawn(client);
|
|
84
84
|
```
|
|
85
85
|
|
|
86
86
|
## Worker Configuration
|
|
@@ -89,9 +89,9 @@ await orderWorker.with().opt("shardKeys", ["eu-west"]).spawn(client);
|
|
|
89
89
|
|
|
90
90
|
```typescript
|
|
91
91
|
interface WorkerParams {
|
|
92
|
-
|
|
92
|
+
name: string; // Unique worker name
|
|
93
93
|
workflows: WorkflowVersion[]; // Workflow versions to execute
|
|
94
|
-
subscriber?: SubscriberStrategy; // Message subscriber (default:
|
|
94
|
+
subscriber?: SubscriberStrategy; // Message subscriber (default: redis)
|
|
95
95
|
}
|
|
96
96
|
```
|
|
97
97
|
|
|
@@ -104,7 +104,7 @@ interface WorkerOptions {
|
|
|
104
104
|
heartbeatIntervalMs?: number; // Heartbeat interval (default: 30s)
|
|
105
105
|
};
|
|
106
106
|
gracefulShutdownTimeoutMs?: number; // Shutdown timeout (default: 5s)
|
|
107
|
-
|
|
107
|
+
shards?: string[]; // Optional shards for distributed work
|
|
108
108
|
}
|
|
109
109
|
```
|
|
110
110
|
|
|
@@ -114,9 +114,9 @@ Workers receive workflow versions through the `workflows` param:
|
|
|
114
114
|
|
|
115
115
|
```typescript
|
|
116
116
|
const aikiWorker = worker({
|
|
117
|
-
|
|
117
|
+
name: "worker-1",
|
|
118
118
|
workflows: [workflowV1, workflowV2, anotherWorkflowV1],
|
|
119
|
-
subscriber: { type: "
|
|
119
|
+
subscriber: { type: "redis" },
|
|
120
120
|
});
|
|
121
121
|
```
|
|
122
122
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Client, SubscriberStrategy } from '@aikirun/client';
|
|
2
|
+
import { WorkerName, WorkerId } from '@aikirun/types/worker';
|
|
2
3
|
import { WorkflowVersion } from '@aikirun/workflow';
|
|
3
4
|
|
|
4
5
|
type NonEmptyArray<T> = [T, ...T[]];
|
|
@@ -27,15 +28,15 @@ type TypeOfValueAtPath<T extends object, Path extends PathFromObject<T>> = Path
|
|
|
27
28
|
* execution, which returns a handle for controlling the running worker.
|
|
28
29
|
*
|
|
29
30
|
* @param params - Worker configuration parameters
|
|
30
|
-
* @param params.
|
|
31
|
+
* @param params.name - Unique worker name for identification and monitoring
|
|
31
32
|
* @param params.workflows - Array of workflow versions this worker can execute
|
|
32
|
-
* @param params.subscriber - Message subscriber strategy (default:
|
|
33
|
+
* @param params.subscriber - Message subscriber strategy (default: redis)
|
|
33
34
|
* @returns Worker definition, call spawn(client) to begin execution
|
|
34
35
|
*
|
|
35
36
|
* @example
|
|
36
37
|
* ```typescript
|
|
37
38
|
* export const myWorker = worker({
|
|
38
|
-
*
|
|
39
|
+
* name: "order-worker",
|
|
39
40
|
* workflows: [orderWorkflowV1, paymentWorkflowV1],
|
|
40
41
|
* opts: {
|
|
41
42
|
* maxConcurrentWorkflowRuns: 10,
|
|
@@ -52,44 +53,53 @@ type TypeOfValueAtPath<T extends object, Path extends PathFromObject<T>> = Path
|
|
|
52
53
|
*/
|
|
53
54
|
declare function worker(params: WorkerParams): Worker;
|
|
54
55
|
interface WorkerParams {
|
|
55
|
-
|
|
56
|
+
name: string;
|
|
56
57
|
workflows: WorkflowVersion<any, any, any, any>[];
|
|
57
58
|
subscriber?: SubscriberStrategy;
|
|
58
59
|
opts?: WorkerOptions;
|
|
59
60
|
}
|
|
60
61
|
interface WorkerOptions {
|
|
61
62
|
maxConcurrentWorkflowRuns?: number;
|
|
62
|
-
workflowRun?:
|
|
63
|
-
heartbeatIntervalMs?: number;
|
|
64
|
-
/**
|
|
65
|
-
* Threshold for spinning vs persisting delays (default: 10ms).
|
|
66
|
-
*
|
|
67
|
-
* Delays <= threshold: In-memory wait (fast, no history, not durable)
|
|
68
|
-
* Delays > threshold: Server state transition (history recorded, durable)
|
|
69
|
-
*
|
|
70
|
-
* Set to 0 to record all delays in transition history.
|
|
71
|
-
*/
|
|
72
|
-
spinThresholdMs?: number;
|
|
73
|
-
};
|
|
63
|
+
workflowRun?: WorkflowRunOptions;
|
|
74
64
|
gracefulShutdownTimeoutMs?: number;
|
|
75
65
|
/**
|
|
76
|
-
* Optional array of
|
|
66
|
+
* Optional array of shards this worker should process.
|
|
77
67
|
* When provided, the worker will only subscribe to sharded streams.
|
|
78
68
|
* When omitted, the worker subscribes to default streams.
|
|
79
69
|
*/
|
|
80
|
-
|
|
70
|
+
shards?: string[];
|
|
71
|
+
/**
|
|
72
|
+
* Optional reference for external correlation.
|
|
73
|
+
* Use this to associate the worker with external identifiers.
|
|
74
|
+
*/
|
|
75
|
+
reference?: {
|
|
76
|
+
id: string;
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
interface WorkflowRunOptions {
|
|
80
|
+
heartbeatIntervalMs?: number;
|
|
81
|
+
/**
|
|
82
|
+
* Threshold for spinning vs persisting delays (default: 10ms).
|
|
83
|
+
*
|
|
84
|
+
* Delays <= threshold: In-memory wait (fast, no history, not durable)
|
|
85
|
+
* Delays > threshold: Server state transition (history recorded, durable)
|
|
86
|
+
*
|
|
87
|
+
* Set to 0 to record all delays in transition history.
|
|
88
|
+
*/
|
|
89
|
+
spinThresholdMs?: number;
|
|
81
90
|
}
|
|
82
91
|
interface WorkerBuilder {
|
|
83
92
|
opt<Path extends PathFromObject<WorkerOptions>>(path: Path, value: TypeOfValueAtPath<WorkerOptions, Path>): WorkerBuilder;
|
|
84
93
|
spawn: Worker["spawn"];
|
|
85
94
|
}
|
|
86
95
|
interface Worker {
|
|
87
|
-
|
|
96
|
+
name: WorkerName;
|
|
88
97
|
with(): WorkerBuilder;
|
|
89
98
|
spawn: <AppContext>(client: Client<AppContext>) => Promise<WorkerHandle>;
|
|
90
99
|
}
|
|
91
100
|
interface WorkerHandle {
|
|
92
|
-
id:
|
|
101
|
+
id: WorkerId;
|
|
102
|
+
name: WorkerName;
|
|
93
103
|
stop: () => Promise<void>;
|
|
94
104
|
}
|
|
95
105
|
|
package/dist/index.js
CHANGED
|
@@ -29,26 +29,6 @@ function fireAndForget(promise, onError) {
|
|
|
29
29
|
});
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
// ../../lib/error/conflict.ts
|
|
33
|
-
function isServerConflictError(error) {
|
|
34
|
-
if (error === null || typeof error !== "object") {
|
|
35
|
-
return false;
|
|
36
|
-
}
|
|
37
|
-
if ("code" in error && error.code === "CONFLICT") {
|
|
38
|
-
return true;
|
|
39
|
-
}
|
|
40
|
-
if ("status" in error && error.status === 409) {
|
|
41
|
-
return true;
|
|
42
|
-
}
|
|
43
|
-
if (error instanceof Error && error.name === "ConflictError") {
|
|
44
|
-
return true;
|
|
45
|
-
}
|
|
46
|
-
if ("data" in error && error.data !== null && typeof error.data === "object" && "status" in error.data && error.data.status === 409) {
|
|
47
|
-
return true;
|
|
48
|
-
}
|
|
49
|
-
return false;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
32
|
// ../../lib/object/overrider.ts
|
|
53
33
|
function set(obj, path, value) {
|
|
54
34
|
const keys = path.split(".");
|
|
@@ -80,7 +60,6 @@ var objectOverrider = (defaultObj) => (obj) => {
|
|
|
80
60
|
|
|
81
61
|
// worker.ts
|
|
82
62
|
import { INTERNAL } from "@aikirun/types/symbols";
|
|
83
|
-
import { TaskFailedError } from "@aikirun/types/task";
|
|
84
63
|
import {
|
|
85
64
|
WorkflowRunFailedError,
|
|
86
65
|
WorkflowRunNotExecutableError,
|
|
@@ -98,9 +77,9 @@ function worker(params) {
|
|
|
98
77
|
var WorkerImpl = class _WorkerImpl {
|
|
99
78
|
constructor(params) {
|
|
100
79
|
this.params = params;
|
|
101
|
-
this.
|
|
80
|
+
this.name = params.name;
|
|
102
81
|
}
|
|
103
|
-
|
|
82
|
+
name;
|
|
104
83
|
with() {
|
|
105
84
|
const optsOverrider = objectOverrider(this.params.opts ?? {});
|
|
106
85
|
const createBuilder = (optsBuilder) => ({
|
|
@@ -119,14 +98,24 @@ var WorkerHandleImpl = class {
|
|
|
119
98
|
constructor(client, params) {
|
|
120
99
|
this.client = client;
|
|
121
100
|
this.params = params;
|
|
122
|
-
this.id =
|
|
101
|
+
this.id = crypto.randomUUID();
|
|
102
|
+
this.name = params.name;
|
|
103
|
+
this.workflowRunOpts = {
|
|
104
|
+
heartbeatIntervalMs: this.params.opts?.workflowRun?.heartbeatIntervalMs ?? 3e4,
|
|
105
|
+
spinThresholdMs: this.params.opts?.workflowRun?.spinThresholdMs ?? 10
|
|
106
|
+
};
|
|
123
107
|
this.registry = workflowRegistry().addMany(this.params.workflows);
|
|
108
|
+
const reference = this.params.opts?.reference;
|
|
124
109
|
this.logger = client.logger.child({
|
|
125
110
|
"aiki.component": "worker",
|
|
126
|
-
"aiki.workerId": this.id
|
|
111
|
+
"aiki.workerId": this.id,
|
|
112
|
+
"aiki.workerName": this.name,
|
|
113
|
+
...reference && { "aiki.workerReferenceId": reference.id }
|
|
127
114
|
});
|
|
128
115
|
}
|
|
129
116
|
id;
|
|
117
|
+
name;
|
|
118
|
+
workflowRunOpts;
|
|
130
119
|
registry;
|
|
131
120
|
logger;
|
|
132
121
|
abortController;
|
|
@@ -134,12 +123,12 @@ var WorkerHandleImpl = class {
|
|
|
134
123
|
activeWorkflowRunsById = /* @__PURE__ */ new Map();
|
|
135
124
|
async _start() {
|
|
136
125
|
const subscriberStrategyBuilder = this.client[INTERNAL].subscriber.create(
|
|
137
|
-
this.params.subscriber ?? { type: "
|
|
126
|
+
this.params.subscriber ?? { type: "redis" },
|
|
138
127
|
this.registry.getAll(),
|
|
139
|
-
this.params.opts?.
|
|
128
|
+
this.params.opts?.shards
|
|
140
129
|
);
|
|
141
130
|
this.subscriberStrategy = await subscriberStrategyBuilder.init(this.id, {
|
|
142
|
-
onError: (error) => this.
|
|
131
|
+
onError: (error) => this.handleSubscriberError(error),
|
|
143
132
|
onStop: () => this.stop()
|
|
144
133
|
});
|
|
145
134
|
this.abortController = new AbortController();
|
|
@@ -156,7 +145,9 @@ var WorkerHandleImpl = class {
|
|
|
156
145
|
this.logger.info("Worker stopping");
|
|
157
146
|
this.abortController?.abort();
|
|
158
147
|
const activeWorkflowRuns = Array.from(this.activeWorkflowRunsById.values());
|
|
159
|
-
if (activeWorkflowRuns.length === 0)
|
|
148
|
+
if (activeWorkflowRuns.length === 0) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
160
151
|
const timeoutMs = this.params.opts?.gracefulShutdownTimeoutMs ?? 5e3;
|
|
161
152
|
if (timeoutMs > 0) {
|
|
162
153
|
await Promise.race([Promise.allSettled(activeWorkflowRuns.map((w) => w.executionPromise)), delay(timeoutMs)]);
|
|
@@ -171,15 +162,16 @@ var WorkerHandleImpl = class {
|
|
|
171
162
|
this.activeWorkflowRunsById.clear();
|
|
172
163
|
}
|
|
173
164
|
async poll(abortSignal) {
|
|
174
|
-
this.logger.info("Worker started");
|
|
175
165
|
if (!this.subscriberStrategy) {
|
|
176
166
|
throw new Error("Subscriber strategy not initialized");
|
|
177
167
|
}
|
|
168
|
+
this.logger.info("Worker started");
|
|
169
|
+
const maxConcurrentWorkflowRuns = this.params.opts?.maxConcurrentWorkflowRuns ?? 1;
|
|
178
170
|
let nextDelayMs = this.subscriberStrategy.getNextDelay({ type: "polled", foundWork: false });
|
|
179
171
|
let subscriberFailedAttempts = 0;
|
|
180
172
|
while (!abortSignal.aborted) {
|
|
181
173
|
await delay(nextDelayMs, { abortSignal });
|
|
182
|
-
const availableCapacity =
|
|
174
|
+
const availableCapacity = maxConcurrentWorkflowRuns - this.activeWorkflowRunsById.size;
|
|
183
175
|
if (availableCapacity <= 0) {
|
|
184
176
|
nextDelayMs = this.subscriberStrategy.getNextDelay({ type: "at_capacity" });
|
|
185
177
|
continue;
|
|
@@ -243,13 +235,13 @@ var WorkerHandleImpl = class {
|
|
|
243
235
|
continue;
|
|
244
236
|
}
|
|
245
237
|
const workflowVersion = this.registry.get(
|
|
246
|
-
workflowRun.
|
|
247
|
-
workflowRun.
|
|
238
|
+
workflowRun.name,
|
|
239
|
+
workflowRun.versionId
|
|
248
240
|
);
|
|
249
241
|
if (!workflowVersion) {
|
|
250
242
|
this.logger.warn("Workflow version not found", {
|
|
251
|
-
"aiki.
|
|
252
|
-
"aiki.workflowVersionId": workflowRun.
|
|
243
|
+
"aiki.workflowName": workflowRun.name,
|
|
244
|
+
"aiki.workflowVersionId": workflowRun.versionId,
|
|
253
245
|
"aiki.workflowRunId": workflowRun.id
|
|
254
246
|
});
|
|
255
247
|
if (meta && this.subscriberStrategy?.acknowledge) {
|
|
@@ -258,7 +250,9 @@ var WorkerHandleImpl = class {
|
|
|
258
250
|
}
|
|
259
251
|
continue;
|
|
260
252
|
}
|
|
261
|
-
if (abortSignal.aborted)
|
|
253
|
+
if (abortSignal.aborted) {
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
262
256
|
const workflowExecutionPromise = this.executeWorkflow(workflowRun, workflowVersion, meta);
|
|
263
257
|
this.activeWorkflowRunsById.set(workflowRun.id, {
|
|
264
258
|
run: workflowRun,
|
|
@@ -270,8 +264,8 @@ var WorkerHandleImpl = class {
|
|
|
270
264
|
async executeWorkflow(workflowRun, workflowVersion, meta) {
|
|
271
265
|
const logger = this.logger.child({
|
|
272
266
|
"aiki.component": "workflow-execution",
|
|
273
|
-
"aiki.
|
|
274
|
-
"aiki.workflowVersionId": workflowRun.
|
|
267
|
+
"aiki.workflowName": workflowRun.name,
|
|
268
|
+
"aiki.workflowVersionId": workflowRun.versionId,
|
|
275
269
|
"aiki.workflowRunId": workflowRun.id
|
|
276
270
|
});
|
|
277
271
|
let heartbeatInterval;
|
|
@@ -287,29 +281,28 @@ var WorkerHandleImpl = class {
|
|
|
287
281
|
"aiki.error": error instanceof Error ? error.message : String(error)
|
|
288
282
|
});
|
|
289
283
|
}
|
|
290
|
-
}, this.
|
|
284
|
+
}, this.workflowRunOpts.heartbeatIntervalMs);
|
|
291
285
|
}
|
|
292
286
|
const eventsDefinition = workflowVersion[INTERNAL].eventsDefinition;
|
|
293
287
|
const handle = await workflowRunHandle(this.client, workflowRun, eventsDefinition, logger);
|
|
294
|
-
const
|
|
295
|
-
const appContext = this.client[INTERNAL].contextFactory ? await this.client[INTERNAL].contextFactory(workflowRun) : null;
|
|
288
|
+
const appContext = this.client[INTERNAL].createContext ? await this.client[INTERNAL].createContext(workflowRun) : null;
|
|
296
289
|
await workflowVersion[INTERNAL].handler(
|
|
297
|
-
workflowRun.input,
|
|
298
290
|
{
|
|
299
291
|
id: workflowRun.id,
|
|
300
|
-
|
|
301
|
-
|
|
292
|
+
name: workflowRun.name,
|
|
293
|
+
versionId: workflowRun.versionId,
|
|
302
294
|
options: workflowRun.options,
|
|
303
295
|
logger,
|
|
304
|
-
sleep: createSleeper(handle, logger
|
|
296
|
+
sleep: createSleeper(handle, logger),
|
|
305
297
|
events: createEventWaiters(handle, eventsDefinition, logger),
|
|
306
|
-
[INTERNAL]: { handle, options: { spinThresholdMs } }
|
|
298
|
+
[INTERNAL]: { handle, options: { spinThresholdMs: this.workflowRunOpts.spinThresholdMs } }
|
|
307
299
|
},
|
|
300
|
+
workflowRun.input,
|
|
308
301
|
appContext
|
|
309
302
|
);
|
|
310
303
|
shouldAcknowledge = true;
|
|
311
304
|
} catch (error) {
|
|
312
|
-
if (error instanceof WorkflowRunNotExecutableError || error instanceof WorkflowRunSuspendedError || error instanceof WorkflowRunFailedError
|
|
305
|
+
if (error instanceof WorkflowRunNotExecutableError || error instanceof WorkflowRunSuspendedError || error instanceof WorkflowRunFailedError) {
|
|
313
306
|
shouldAcknowledge = true;
|
|
314
307
|
} else {
|
|
315
308
|
logger.error("Unexpected error during workflow execution", {
|
|
@@ -337,8 +330,8 @@ var WorkerHandleImpl = class {
|
|
|
337
330
|
this.activeWorkflowRunsById.delete(workflowRun.id);
|
|
338
331
|
}
|
|
339
332
|
}
|
|
340
|
-
|
|
341
|
-
this.logger.warn("
|
|
333
|
+
handleSubscriberError(error) {
|
|
334
|
+
this.logger.warn("Subscriber error", {
|
|
342
335
|
"aiki.error": error.message,
|
|
343
336
|
"aiki.stack": error.stack
|
|
344
337
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aikirun/worker",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Worker SDK for Aiki - execute workflows and tasks with durable state management and automatic recovery",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -18,9 +18,9 @@
|
|
|
18
18
|
"build": "tsup"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@aikirun/types": "0.
|
|
22
|
-
"@aikirun/client": "0.
|
|
23
|
-
"@aikirun/workflow": "0.
|
|
21
|
+
"@aikirun/types": "0.9.0",
|
|
22
|
+
"@aikirun/client": "0.9.0",
|
|
23
|
+
"@aikirun/workflow": "0.9.0"
|
|
24
24
|
},
|
|
25
25
|
"publishConfig": {
|
|
26
26
|
"access": "public"
|