@aikirun/worker 0.6.0 → 0.8.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 CHANGED
@@ -20,7 +20,7 @@ import { onboardingWorkflowV1 } from "./workflows.ts";
20
20
 
21
21
  // Define worker
22
22
  const aikiWorker = worker({
23
- id: "worker-1",
23
+ name: "worker-1",
24
24
  workflows: [onboardingWorkflowV1],
25
25
  subscriber: { type: "redis_streams" },
26
26
  opts: {
@@ -30,7 +30,7 @@ const aikiWorker = worker({
30
30
 
31
31
  // Initialize client
32
32
  const aikiClient = await client({
33
- url: "http://localhost:9090",
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({ id: "orders", workflows: [orderWorkflowV1] });
72
- const emailWorker = worker({ id: "emails", workflows: [emailWorkflowV1] });
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({ id: "order-processor", workflows: [orderWorkflowV1] });
80
+ const orderWorker = worker({ name: "order-processor", workflows: [orderWorkflowV1] });
81
81
 
82
- await orderWorker.with().opt("shardKeys", ["us-east", "us-west"]).spawn(client);
83
- await orderWorker.with().opt("shardKeys", ["eu-west"]).spawn(client);
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,7 +89,7 @@ await orderWorker.with().opt("shardKeys", ["eu-west"]).spawn(client);
89
89
 
90
90
  ```typescript
91
91
  interface WorkerParams {
92
- id: string; // Unique worker ID
92
+ name: string; // Unique worker name
93
93
  workflows: WorkflowVersion[]; // Workflow versions to execute
94
94
  subscriber?: SubscriberStrategy; // Message subscriber (default: redis_streams)
95
95
  }
@@ -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
- shardKeys?: string[]; // Optional shard keys for distributed work
107
+ shards?: string[]; // Optional shards for distributed work
108
108
  }
109
109
  ```
110
110
 
@@ -114,7 +114,7 @@ Workers receive workflow versions through the `workflows` param:
114
114
 
115
115
  ```typescript
116
116
  const aikiWorker = worker({
117
- id: "worker-1",
117
+ name: "worker-1",
118
118
  workflows: [workflowV1, workflowV2, anotherWorkflowV1],
119
119
  subscriber: { type: "redis_streams" },
120
120
  });
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,7 +28,7 @@ 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.id - Unique worker ID for identification and monitoring
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
33
  * @param params.subscriber - Message subscriber strategy (default: redis_streams)
33
34
  * @returns Worker definition, call spawn(client) to begin execution
@@ -35,7 +36,7 @@ type TypeOfValueAtPath<T extends object, Path extends PathFromObject<T>> = Path
35
36
  * @example
36
37
  * ```typescript
37
38
  * export const myWorker = worker({
38
- * id: "order-worker",
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
- id: string;
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 shardKeys this worker should process.
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
- shardKeys?: string[];
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
- id: string;
96
+ name: WorkerName;
88
97
  with(): WorkerBuilder;
89
98
  spawn: <AppContext>(client: Client<AppContext>) => Promise<WorkerHandle>;
90
99
  }
91
100
  interface WorkerHandle {
92
- id: string;
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.id = params.id;
80
+ this.name = params.name;
102
81
  }
103
- id;
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 = params.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;
@@ -136,10 +125,10 @@ var WorkerHandleImpl = class {
136
125
  const subscriberStrategyBuilder = this.client[INTERNAL].subscriber.create(
137
126
  this.params.subscriber ?? { type: "redis_streams" },
138
127
  this.registry.getAll(),
139
- this.params.opts?.shardKeys
128
+ this.params.opts?.shards
140
129
  );
141
130
  this.subscriberStrategy = await subscriberStrategyBuilder.init(this.id, {
142
- onError: (error) => this.handleNotificationError(error),
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) return;
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 = (this.params.opts?.maxConcurrentWorkflowRuns ?? 1) - this.activeWorkflowRunsById.size;
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.workflowId,
247
- workflowRun.workflowVersionId
238
+ workflowRun.name,
239
+ workflowRun.versionId
248
240
  );
249
241
  if (!workflowVersion) {
250
242
  this.logger.warn("Workflow version not found", {
251
- "aiki.workflowId": workflowRun.workflowId,
252
- "aiki.workflowVersionId": workflowRun.workflowVersionId,
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) break;
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,12 +264,9 @@ 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.workflowId": workflowRun.workflowId,
274
- "aiki.workflowVersionId": workflowRun.workflowVersionId,
275
- "aiki.workflowRunId": workflowRun.id,
276
- ...meta && {
277
- "aiki.messageId": meta.messageId
278
- }
267
+ "aiki.workflowName": workflowRun.name,
268
+ "aiki.workflowVersionId": workflowRun.versionId,
269
+ "aiki.workflowRunId": workflowRun.id
279
270
  });
280
271
  let heartbeatInterval;
281
272
  let shouldAcknowledge = false;
@@ -290,29 +281,28 @@ var WorkerHandleImpl = class {
290
281
  "aiki.error": error instanceof Error ? error.message : String(error)
291
282
  });
292
283
  }
293
- }, this.params.opts?.workflowRun?.heartbeatIntervalMs ?? 3e4);
284
+ }, this.workflowRunOpts.heartbeatIntervalMs);
294
285
  }
295
286
  const eventsDefinition = workflowVersion[INTERNAL].eventsDefinition;
296
287
  const handle = await workflowRunHandle(this.client, workflowRun, eventsDefinition, logger);
297
- const spinThresholdMs = this.params.opts?.workflowRun?.spinThresholdMs ?? 10;
298
288
  const appContext = this.client[INTERNAL].contextFactory ? await this.client[INTERNAL].contextFactory(workflowRun) : null;
299
289
  await workflowVersion[INTERNAL].handler(
300
- workflowRun.input,
301
290
  {
302
291
  id: workflowRun.id,
303
- workflowId: workflowRun.workflowId,
304
- workflowVersionId: workflowRun.workflowVersionId,
292
+ name: workflowRun.name,
293
+ versionId: workflowRun.versionId,
305
294
  options: workflowRun.options,
306
295
  logger,
307
- sleep: createSleeper(handle, logger, { spinThresholdMs }),
296
+ sleep: createSleeper(handle, logger),
308
297
  events: createEventWaiters(handle, eventsDefinition, logger),
309
- [INTERNAL]: { handle, options: { spinThresholdMs } }
298
+ [INTERNAL]: { handle, options: { spinThresholdMs: this.workflowRunOpts.spinThresholdMs } }
310
299
  },
300
+ workflowRun.input,
311
301
  appContext
312
302
  );
313
303
  shouldAcknowledge = true;
314
304
  } catch (error) {
315
- if (error instanceof WorkflowRunNotExecutableError || error instanceof WorkflowRunSuspendedError || error instanceof WorkflowRunFailedError || error instanceof TaskFailedError || isServerConflictError(error)) {
305
+ if (error instanceof WorkflowRunNotExecutableError || error instanceof WorkflowRunSuspendedError || error instanceof WorkflowRunFailedError) {
316
306
  shouldAcknowledge = true;
317
307
  } else {
318
308
  logger.error("Unexpected error during workflow execution", {
@@ -340,8 +330,8 @@ var WorkerHandleImpl = class {
340
330
  this.activeWorkflowRunsById.delete(workflowRun.id);
341
331
  }
342
332
  }
343
- handleNotificationError(error) {
344
- this.logger.warn("Notification error, falling back to polling", {
333
+ handleSubscriberError(error) {
334
+ this.logger.warn("Subscriber error", {
345
335
  "aiki.error": error.message,
346
336
  "aiki.stack": error.stack
347
337
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aikirun/worker",
3
- "version": "0.6.0",
3
+ "version": "0.8.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.6.0",
22
- "@aikirun/client": "0.6.0",
23
- "@aikirun/workflow": "0.6.0"
21
+ "@aikirun/types": "0.8.0",
22
+ "@aikirun/client": "0.8.0",
23
+ "@aikirun/workflow": "0.8.0"
24
24
  },
25
25
  "publishConfig": {
26
26
  "access": "public"