@aikirun/worker 0.7.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,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.workflowId": workflowRun.workflowId,
274
- "aiki.workflowVersionId": workflowRun.workflowVersionId,
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.params.opts?.workflowRun?.heartbeatIntervalMs ?? 3e4);
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 spinThresholdMs = this.params.opts?.workflowRun?.spinThresholdMs ?? 10;
295
288
  const appContext = this.client[INTERNAL].contextFactory ? await this.client[INTERNAL].contextFactory(workflowRun) : null;
296
289
  await workflowVersion[INTERNAL].handler(
297
- workflowRun.input,
298
290
  {
299
291
  id: workflowRun.id,
300
- workflowId: workflowRun.workflowId,
301
- workflowVersionId: workflowRun.workflowVersionId,
292
+ name: workflowRun.name,
293
+ versionId: workflowRun.versionId,
302
294
  options: workflowRun.options,
303
295
  logger,
304
- sleep: createSleeper(handle, logger, { spinThresholdMs }),
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 || error instanceof TaskFailedError || isServerConflictError(error)) {
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
- handleNotificationError(error) {
341
- this.logger.warn("Notification error, falling back to polling", {
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.7.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.7.0",
22
- "@aikirun/client": "0.7.0",
23
- "@aikirun/workflow": "0.7.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"