@aikirun/client 0.23.0 → 0.24.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
@@ -14,10 +14,9 @@ npm install @aikirun/client
14
14
  import { client } from "@aikirun/client";
15
15
  import { orderWorkflowV1 } from "./workflows.ts";
16
16
 
17
- // Set AIKI_API_KEY env variable or pass apiKey option
18
17
  const aikiClient = client({
19
18
  url: "http://localhost:9850",
20
- redis: { host: "localhost", port: 6379 },
19
+ apiKey: "your-api-key",
21
20
  });
22
21
 
23
22
  // Start a workflow
@@ -27,16 +26,12 @@ const handle = await orderWorkflowV1.start(aikiClient, {
27
26
 
28
27
  // Wait for completion
29
28
  const result = await handle.waitForStatus("completed");
30
-
31
- // Close when done
32
- await aikiClient.close();
33
29
  ```
34
30
 
35
31
  ## Features
36
32
 
37
33
  - **Server Connection** - Connect to the Aiki server via HTTP
38
34
  - **Workflow Management** - Start workflows with type-safe inputs
39
- - **Redis Integration** - Distributed state and message streaming
40
35
  - **Context Injection** - Pass application context to workflows
41
36
  - **Custom Logging** - Plug in your own logger
42
37
 
package/dist/index.d.ts CHANGED
@@ -1,20 +1,15 @@
1
- import { ClientParams, Client, Logger } from '@aikirun/types/client';
2
- export { ApiClient, Client, ClientParams, DbSubscriberStrategy, Logger, RedisConfig, RedisStreamsSubscriberStrategy, ResolvedSubscriberStrategy, SubscriberStrategy, WorkflowRunBatch } from '@aikirun/types/client';
1
+ import { ClientParams, Client } from '@aikirun/types/client';
3
2
 
4
3
  /**
5
4
  * Creates an Aiki client for starting and managing workflows.
6
5
  *
7
- * The client connects to the Aiki server via HTTP and Redis for state management.
6
+ * The client connects to the Aiki server via HTTP.
8
7
  * It provides methods to start workflows and monitor their execution.
9
8
  *
10
9
  * @template AppContext - Type of application context passed to workflows (default: null)
11
10
  * @param params - Client configuration parameters
12
11
  * @param params.url - HTTP URL of the Aiki server (e.g., "http://localhost:9850")
13
- * @param params.apiKey - API key for authentication. Falls back to AIKI_API_KEY env variable if not provided.
14
- * @param params.redis - Redis connection configuration
15
- * @param params.redis.host - Redis server hostname
16
- * @param params.redis.port - Redis server port
17
- * @param params.redis.password - Optional Redis password
12
+ * @param params.apiKey - API key for authentication
18
13
  * @param params.createContext - Optional function to create context for each workflow run
19
14
  * @param params.logger - Optional custom logger (defaults to ConsoleLogger)
20
15
  * @returns Promise resolving to a configured Client instance
@@ -23,8 +18,7 @@ export { ApiClient, Client, ClientParams, DbSubscriberStrategy, Logger, RedisCon
23
18
  * ```typescript
24
19
  * const aikiClient = client({
25
20
  * url: "http://localhost:9850",
26
- * apiKey: "yourApiKey", // or omit to use AIKI_API_KEY env variable
27
- * redis: { host: "localhost", port: 6379 },
21
+ * apiKey: "yourApiKey",
28
22
  * createContext: (run) => ({
29
23
  * traceId: generateTraceId(),
30
24
  * userId: extractUserId(run),
@@ -39,29 +33,8 @@ export { ApiClient, Client, ClientParams, DbSubscriberStrategy, Logger, RedisCon
39
33
  * { type: "status", status: "completed" },
40
34
  * { maxDurationMs: 60_000 }
41
35
  * );
42
- *
43
- * // Cleanup
44
- * await aikiClient.close();
45
36
  * ```
46
37
  */
47
38
  declare function client<AppContext = null>(params: ClientParams<AppContext>): Client<AppContext>;
48
39
 
49
- type LogLevel = "TRACE" | "DEBUG" | "INFO" | "WARN" | "ERROR";
50
- interface ConsoleLoggerOptions {
51
- level?: LogLevel;
52
- context?: Record<string, unknown>;
53
- }
54
- declare class ConsoleLogger implements Logger {
55
- private readonly level;
56
- private readonly context;
57
- constructor(options?: ConsoleLoggerOptions);
58
- trace(message: string, metadata?: Record<string, unknown>): void;
59
- debug(message: string, metadata?: Record<string, unknown>): void;
60
- info(message: string, metadata?: Record<string, unknown>): void;
61
- warn(message: string, metadata?: Record<string, unknown>): void;
62
- error(message: string, metadata?: Record<string, unknown>): void;
63
- child(bindings: Record<string, unknown>): Logger;
64
- private format;
65
- }
66
-
67
- export { ConsoleLogger, client };
40
+ export { client };
package/dist/index.js CHANGED
@@ -1,11 +1,4 @@
1
- // client.ts
2
- import process from "process";
3
- import { INTERNAL as INTERNAL2 } from "@aikirun/types/symbols";
4
- import { createORPCClient } from "@orpc/client";
5
- import { RPCLink } from "@orpc/client/fetch";
6
- import { Redis } from "ioredis";
7
-
8
- // logger/console-logger.ts
1
+ // ../../lib/logger/console-logger.ts
9
2
  var colors = {
10
3
  reset: "\x1B[0m",
11
4
  dim: "\x1B[2m",
@@ -83,572 +76,10 @@ var ConsoleLogger = class _ConsoleLogger {
83
76
  }
84
77
  };
85
78
 
86
- // ../../lib/array/utils.ts
87
- function isNonEmptyArray(value) {
88
- return value !== void 0 && value.length > 0;
89
- }
90
- function shuffleArray(array) {
91
- const shuffledArray = Array.from(array);
92
- for (let i = shuffledArray.length - 1; i > 0; i--) {
93
- const j = Math.floor(Math.random() * (i + 1));
94
- [shuffledArray[i], shuffledArray[j]] = [shuffledArray[j], shuffledArray[i]];
95
- }
96
- return shuffledArray;
97
- }
98
- function distributeRoundRobin(totalSize, itemCount) {
99
- if (itemCount <= 0) return [];
100
- const distribution = Array(itemCount).fill(0);
101
- for (let i = 0; i < totalSize; i++) {
102
- distribution[i % itemCount]++;
103
- }
104
- return distribution;
105
- }
106
-
107
- // ../../lib/crypto/hash.ts
108
- import { createHash } from "crypto";
109
-
110
- // ../../lib/duration/convert.ts
111
- var MS_PER_SECOND = 1e3;
112
- var MS_PER_MINUTE = 60 * MS_PER_SECOND;
113
- var MS_PER_HOUR = 60 * MS_PER_MINUTE;
114
- var MS_PER_DAY = 24 * MS_PER_HOUR;
115
-
116
- // ../../lib/retry/strategy.ts
117
- function getRetryParams(attempts, strategy) {
118
- const strategyType = strategy.type;
119
- switch (strategyType) {
120
- case "never":
121
- return {
122
- retriesLeft: false
123
- };
124
- case "fixed":
125
- if (attempts >= strategy.maxAttempts) {
126
- return {
127
- retriesLeft: false
128
- };
129
- }
130
- return {
131
- retriesLeft: true,
132
- delayMs: strategy.delayMs
133
- };
134
- case "exponential": {
135
- if (attempts >= strategy.maxAttempts) {
136
- return {
137
- retriesLeft: false
138
- };
139
- }
140
- const delayMs = strategy.baseDelayMs * (strategy.factor ?? 2) ** (attempts - 1);
141
- return {
142
- retriesLeft: true,
143
- delayMs: Math.min(delayMs, strategy.maxDelayMs ?? Number.POSITIVE_INFINITY)
144
- };
145
- }
146
- case "jittered": {
147
- if (attempts >= strategy.maxAttempts) {
148
- return {
149
- retriesLeft: false
150
- };
151
- }
152
- const base = strategy.baseDelayMs * (strategy.jitterFactor ?? 2) ** (attempts - 1);
153
- const delayMs = Math.random() * base;
154
- return {
155
- retriesLeft: true,
156
- delayMs: Math.min(delayMs, strategy.maxDelayMs ?? Number.POSITIVE_INFINITY)
157
- };
158
- }
159
- default:
160
- return strategyType;
161
- }
162
- }
163
-
164
- // subscribers/db.ts
165
- function createDbStrategy(client2, strategy, workflows, workerShards) {
166
- const logger = client2.logger.child({
167
- "aiki.component": "db-subscriber"
168
- });
169
- const intervalMs = strategy.intervalMs ?? 1e3;
170
- const maxRetryIntervalMs = strategy.maxRetryIntervalMs ?? 3e4;
171
- const atCapacityIntervalMs = strategy.atCapacityIntervalMs ?? 500;
172
- const claimMinIdleTimeMs = strategy.claimMinIdleTimeMs ?? 9e4;
173
- const workflowFilters = !isNonEmptyArray(workerShards) ? workflows.map((workflow) => ({ name: workflow.name, versionId: workflow.versionId })) : workflows.flatMap(
174
- (workflow) => workerShards.map((shard) => ({ name: workflow.name, versionId: workflow.versionId, shard }))
175
- );
176
- const getNextDelay = (params) => {
177
- switch (params.type) {
178
- case "polled":
179
- case "heartbeat":
180
- return intervalMs;
181
- case "at_capacity":
182
- return atCapacityIntervalMs;
183
- case "retry": {
184
- const retryParams = getRetryParams(params.attemptNumber, {
185
- type: "jittered",
186
- maxAttempts: Number.POSITIVE_INFINITY,
187
- baseDelayMs: intervalMs,
188
- maxDelayMs: maxRetryIntervalMs
189
- });
190
- if (!retryParams.retriesLeft) {
191
- return maxRetryIntervalMs;
192
- }
193
- return retryParams.delayMs;
194
- }
195
- default:
196
- return params;
197
- }
198
- };
199
- return {
200
- async init(workerId, _callbacks) {
201
- return {
202
- type: strategy.type,
203
- getNextDelay,
204
- getNextBatch: async (size) => {
205
- const response = await client2.api.workflowRun.claimReadyV1({
206
- workerId,
207
- workflows: workflowFilters,
208
- limit: size,
209
- claimMinIdleTimeMs
210
- });
211
- return response.runs.map((run) => ({
212
- data: { workflowRunId: run.id }
213
- }));
214
- },
215
- heartbeat: async (workflowRunId) => {
216
- try {
217
- await client2.api.workflowRun.heartbeatV1({ id: workflowRunId });
218
- logger.debug("Heartbeat sent", {
219
- "aiki.workerId": workerId,
220
- "aiki.workflowRunId": workflowRunId
221
- });
222
- } catch (error) {
223
- logger.warn("Heartbeat failed", {
224
- "aiki.workerId": workerId,
225
- "aiki.workflowRunId": workflowRunId,
226
- "aiki.error": error instanceof Error ? error.message : String(error)
227
- });
228
- }
229
- },
230
- acknowledge: async () => {
231
- }
232
- };
233
- }
234
- };
235
- }
236
-
237
- // ../../lib/address/index.ts
238
- function getWorkflowStreamName(name, versionId, shard) {
239
- return shard ? `workflow:${name}:${versionId}:${shard}` : `workflow:${name}:${versionId}`;
240
- }
241
- function getWorkerConsumerGroupName(workflowName, workflowVersionId, shard) {
242
- return shard ? `worker:${workflowName}:${workflowVersionId}:${shard}` : `worker:${workflowName}:${workflowVersionId}`;
243
- }
244
-
245
- // subscribers/redis-streams.ts
246
- import { INTERNAL } from "@aikirun/types/symbols";
247
- import { type } from "arktype";
248
- var streamEntriesSchema = type(["string", type(["string", "unknown[]"]).array()]).array();
249
- var rawStreamMessageFieldsToRecord = (rawFields) => {
250
- const data = {};
251
- for (let i = 0; i < rawFields.length; i += 2) {
252
- if (i + 1 < rawFields.length) {
253
- const key = rawFields[i];
254
- if (typeof key === "string") {
255
- data[key] = rawFields[i + 1];
256
- }
257
- }
258
- }
259
- return data;
260
- };
261
- var streamMessageDataSchema = type({
262
- type: "'workflow_run_ready'",
263
- workflowRunId: "string > 0"
264
- });
265
- var streamPendingMessagesSchema = type(["string", "string", "number", "number"]).array();
266
- function createRedisStreamsStrategy(client2, strategy, workflows, workerShards) {
267
- const redis = client2[INTERNAL].redis.getConnection();
268
- const logger = client2.logger.child({
269
- "aiki.component": "redis-subscriber"
270
- });
271
- const streamConsumerGroupMap = getRedisStreamConsumerGroupMap(workflows, workerShards);
272
- const streams = Array.from(streamConsumerGroupMap.keys());
273
- const intervalMs = strategy.intervalMs ?? 50;
274
- const maxRetryIntervalMs = strategy.maxRetryIntervalMs ?? 3e4;
275
- const atCapacityIntervalMs = strategy.atCapacityIntervalMs ?? 50;
276
- const blockTimeMs = strategy.blockTimeMs ?? 1e3;
277
- const claimMinIdleTimeMs = strategy.claimMinIdleTimeMs ?? 9e4;
278
- const getNextDelay = (params) => {
279
- switch (params.type) {
280
- case "polled":
281
- case "heartbeat":
282
- return intervalMs;
283
- case "retry": {
284
- const retryParams = getRetryParams(params.attemptNumber, {
285
- type: "jittered",
286
- maxAttempts: Number.POSITIVE_INFINITY,
287
- baseDelayMs: intervalMs,
288
- maxDelayMs: maxRetryIntervalMs
289
- });
290
- if (!retryParams.retriesLeft) {
291
- return maxRetryIntervalMs;
292
- }
293
- return retryParams.delayMs;
294
- }
295
- case "at_capacity":
296
- return atCapacityIntervalMs;
297
- default:
298
- return params;
299
- }
300
- };
301
- return {
302
- async init(workerId, _callbacks) {
303
- for (const [stream, consumerGroup] of streamConsumerGroupMap) {
304
- try {
305
- await redis.xgroup("CREATE", stream, consumerGroup, "0", "MKSTREAM");
306
- } catch (error) {
307
- if (!error.message?.includes("BUSYGROUP")) {
308
- throw error;
309
- }
310
- }
311
- }
312
- const pendingMessageMetaByWorkflowRunId = /* @__PURE__ */ new Map();
313
- return {
314
- type: strategy.type,
315
- getNextDelay,
316
- getNextBatch: async (size) => {
317
- const messages = await fetchRedisStreamMessages(
318
- redis,
319
- logger.child({ "aiki.workerId": workerId }),
320
- streams,
321
- streamConsumerGroupMap,
322
- workerId,
323
- size,
324
- blockTimeMs,
325
- claimMinIdleTimeMs
326
- );
327
- const batches = [];
328
- for (const message of messages) {
329
- pendingMessageMetaByWorkflowRunId.set(message.data.workflowRunId, message.meta);
330
- batches.push({ data: message.data });
331
- }
332
- return batches;
333
- },
334
- heartbeat: async (workflowRunId) => {
335
- const meta = pendingMessageMetaByWorkflowRunId.get(workflowRunId);
336
- if (!meta) {
337
- return;
338
- }
339
- try {
340
- await redis.xclaim(meta.stream, meta.consumerGroup, workerId, 0, meta.messageId, "JUSTID");
341
- logger.debug("Heartbeat sent", {
342
- "aiki.workerId": workerId,
343
- "aiki.workflowRunId": workflowRunId,
344
- "aiki.messageId": meta.messageId
345
- });
346
- } catch (error) {
347
- logger.warn("Heartbeat failed", {
348
- "aiki.workerId": workerId,
349
- "aiki.workflowRunId": workflowRunId,
350
- "aiki.error": error instanceof Error ? error.message : String(error)
351
- });
352
- }
353
- },
354
- acknowledge: async (workflowRunId) => {
355
- const meta = pendingMessageMetaByWorkflowRunId.get(workflowRunId);
356
- if (!meta) {
357
- return;
358
- }
359
- try {
360
- const result = await redis.xack(meta.stream, meta.consumerGroup, meta.messageId);
361
- if (result === 0) {
362
- logger.warn("Message already acknowledged", {
363
- "aiki.workerId": workerId,
364
- "aiki.workflowRunId": workflowRunId,
365
- "aiki.messageId": meta.messageId
366
- });
367
- } else {
368
- logger.debug("Message acknowledged", {
369
- "aiki.workerId": workerId,
370
- "aiki.workflowRunId": workflowRunId,
371
- "aiki.messageId": meta.messageId
372
- });
373
- }
374
- } catch (error) {
375
- logger.error("Failed to acknowledge message", {
376
- "aiki.workerId": workerId,
377
- "aiki.workflowRunId": workflowRunId,
378
- "aiki.messageId": meta.messageId,
379
- "aiki.error": error instanceof Error ? error.message : String(error)
380
- });
381
- throw error;
382
- } finally {
383
- pendingMessageMetaByWorkflowRunId.delete(workflowRunId);
384
- }
385
- }
386
- };
387
- }
388
- };
389
- }
390
- function getRedisStreamConsumerGroupMap(workflows, shards) {
391
- if (!shards || !isNonEmptyArray(shards)) {
392
- return new Map(
393
- workflows.map((workflow) => [
394
- getWorkflowStreamName(workflow.name, workflow.versionId),
395
- getWorkerConsumerGroupName(workflow.name, workflow.versionId)
396
- ])
397
- );
398
- }
399
- return new Map(
400
- workflows.flatMap(
401
- (workflow) => shards.map((shard) => [
402
- getWorkflowStreamName(workflow.name, workflow.versionId, shard),
403
- getWorkerConsumerGroupName(workflow.name, workflow.versionId, shard)
404
- ])
405
- )
406
- );
407
- }
408
- async function fetchRedisStreamMessages(redis, logger, streams, streamConsumerGroupMap, workerId, size, blockTimeMs, claimMinIdleTimeMs) {
409
- if (!isNonEmptyArray(streams)) {
410
- return [];
411
- }
412
- const perStreamBlockTimeMs = Math.max(50, Math.floor(blockTimeMs / streams.length));
413
- const batchSizePerStream = distributeRoundRobin(size, streams.length);
414
- const shuffledStreams = shuffleArray(streams);
415
- const streamEntries = [];
416
- for (let i = 0; i < shuffledStreams.length; i++) {
417
- const stream = shuffledStreams[i];
418
- if (!stream) {
419
- continue;
420
- }
421
- const streamBatchSize = batchSizePerStream[i];
422
- if (!streamBatchSize || streamBatchSize === 0) {
423
- continue;
424
- }
425
- const consumerGroup = streamConsumerGroupMap.get(stream);
426
- if (!consumerGroup) {
427
- continue;
428
- }
429
- try {
430
- const result = await redis.xreadgroup(
431
- "GROUP",
432
- consumerGroup,
433
- workerId,
434
- "COUNT",
435
- streamBatchSize,
436
- "BLOCK",
437
- perStreamBlockTimeMs,
438
- "STREAMS",
439
- stream,
440
- ">"
441
- );
442
- if (result) {
443
- streamEntries.push(result);
444
- }
445
- } catch (error) {
446
- logger.error("XREADGROUP failed", {
447
- "aiki.stream": stream,
448
- "aiki.error": error instanceof Error ? error.message : String(error)
449
- });
450
- }
451
- }
452
- const workflowRuns = isNonEmptyArray(streamEntries) ? await processRedisStreamMessages(redis, logger, streamConsumerGroupMap, streamEntries) : [];
453
- const remainingCapacity = size - workflowRuns.length;
454
- if (remainingCapacity > 0 && claimMinIdleTimeMs > 0) {
455
- const claimedWorkflowRuns = await claimStuckRedisStreamMessages(
456
- redis,
457
- logger,
458
- shuffledStreams,
459
- streamConsumerGroupMap,
460
- workerId,
461
- remainingCapacity,
462
- claimMinIdleTimeMs
463
- );
464
- for (const workflowRun of claimedWorkflowRuns) {
465
- workflowRuns.push(workflowRun);
466
- }
467
- }
468
- return workflowRuns;
469
- }
470
- async function processRedisStreamMessages(redis, logger, streamConsumerGroupMap, streamsEntries) {
471
- const workflowRuns = [];
472
- for (const streamEntriesRaw of streamsEntries) {
473
- logger.debug("Raw stream entries", { "aiki.entries": streamEntriesRaw });
474
- const streamEntriesResult = streamEntriesSchema(streamEntriesRaw);
475
- if (streamEntriesResult instanceof type.errors) {
476
- logger.error("Invalid stream entries format", {
477
- "aiki.error": streamEntriesResult.summary
478
- });
479
- continue;
480
- }
481
- for (const streamEntry of streamEntriesResult) {
482
- const [stream, messages] = streamEntry;
483
- const consumerGroup = streamConsumerGroupMap.get(stream);
484
- if (!consumerGroup) {
485
- logger.error("No consumer group found for stream", {
486
- "aiki.stream": stream
487
- });
488
- continue;
489
- }
490
- for (const [messageId, rawFields] of messages) {
491
- const rawMessageData = rawStreamMessageFieldsToRecord(rawFields);
492
- const messageData = streamMessageDataSchema(rawMessageData);
493
- if (messageData instanceof type.errors) {
494
- logger.warn("Invalid message structure", {
495
- "aiki.stream": stream,
496
- "aiki.messageId": messageId,
497
- "aiki.error": messageData.summary
498
- });
499
- await redis.xack(stream, consumerGroup, messageId);
500
- continue;
501
- }
502
- switch (messageData.type) {
503
- case "workflow_run_ready": {
504
- const workflowRunId = messageData.workflowRunId;
505
- workflowRuns.push({
506
- data: { workflowRunId },
507
- meta: {
508
- stream,
509
- messageId,
510
- consumerGroup
511
- }
512
- });
513
- break;
514
- }
515
- default:
516
- messageData.type;
517
- continue;
518
- }
519
- }
520
- }
521
- }
522
- return workflowRuns;
523
- }
524
- async function claimStuckRedisStreamMessages(redis, logger, shuffledStreams, streamConsumerGroupMap, workerId, maxClaim, minIdleMs) {
525
- if (maxClaim <= 0 || minIdleMs <= 0) {
526
- return [];
527
- }
528
- const claimaibleMessagesByStream = await findClaimableMessagesByStream(
529
- redis,
530
- logger,
531
- shuffledStreams,
532
- streamConsumerGroupMap,
533
- workerId,
534
- maxClaim,
535
- minIdleMs
536
- );
537
- if (!claimaibleMessagesByStream.size) {
538
- return [];
539
- }
540
- const claimPromises = Array.from(claimaibleMessagesByStream.entries()).map(async ([stream, messageIds]) => {
541
- if (!messageIds.length) {
542
- return null;
543
- }
544
- const consumerGroup = streamConsumerGroupMap.get(stream);
545
- if (!consumerGroup) {
546
- return null;
547
- }
548
- try {
549
- const claimedMessages = await redis.xclaim(stream, consumerGroup, workerId, minIdleMs, ...messageIds);
550
- return { stream, claimedMessages };
551
- } catch (error) {
552
- const errorMessage = error instanceof Error ? error.message : String(error);
553
- if (errorMessage.includes("NOGROUP")) {
554
- logger.warn("Consumer group does not exist for stream, skipping claim operation", {
555
- "aiki.stream": stream,
556
- "aiki.consumerGroup": consumerGroup
557
- });
558
- } else if (errorMessage.includes("BUSYGROUP")) {
559
- logger.warn("Consumer group busy for stream, skipping claim operation", {
560
- "aiki.stream": stream,
561
- "aiki.consumerGroup": consumerGroup
562
- });
563
- } else if (errorMessage.includes("NOSCRIPT")) {
564
- logger.warn("Redis script not loaded for stream, skipping claim operation", {
565
- "aiki.stream": stream,
566
- "aiki.consumerGroup": consumerGroup
567
- });
568
- } else {
569
- logger.error("Failed to claim messages from stream", {
570
- "aiki.stream": stream,
571
- "aiki.consumerGroup": consumerGroup,
572
- "aiki.messageIds": messageIds.length,
573
- "aiki.error": errorMessage
574
- });
575
- }
576
- return null;
577
- }
578
- });
579
- const claimResults = await Promise.allSettled(claimPromises);
580
- const claimedStreamEntries = [];
581
- for (const result of claimResults) {
582
- if (result.status === "fulfilled" && result.value !== null) {
583
- const { stream, claimedMessages } = result.value;
584
- claimedStreamEntries.push([stream, claimedMessages]);
585
- }
586
- }
587
- if (!isNonEmptyArray(claimedStreamEntries)) {
588
- return [];
589
- }
590
- return processRedisStreamMessages(redis, logger, streamConsumerGroupMap, [claimedStreamEntries]);
591
- }
592
- async function findClaimableMessagesByStream(redis, logger, shuffledStreams, streamConsumerGroupMap, workerId, maxClaim, minIdleMs) {
593
- const claimSizePerStream = distributeRoundRobin(maxClaim, shuffledStreams.length);
594
- const pendingPromises = [];
595
- for (let i = 0; i < shuffledStreams.length; i++) {
596
- const stream = shuffledStreams[i];
597
- if (!stream) {
598
- continue;
599
- }
600
- const claimSize = claimSizePerStream[i];
601
- if (!claimSize || claimSize === 0) {
602
- continue;
603
- }
604
- const consumerGroup = streamConsumerGroupMap.get(stream);
605
- if (!consumerGroup) {
606
- continue;
607
- }
608
- const readPromise = redis.xpending(stream, consumerGroup, "IDLE", minIdleMs, "-", "+", claimSize).then((result) => ({ stream, result }));
609
- pendingPromises.push(readPromise);
610
- }
611
- const pendingResults = await Promise.allSettled(pendingPromises);
612
- const claimableMessagesByStream = /* @__PURE__ */ new Map();
613
- for (const pendingResult of pendingResults) {
614
- if (pendingResult.status !== "fulfilled") {
615
- continue;
616
- }
617
- const { stream, result } = pendingResult.value;
618
- const parsedResult = streamPendingMessagesSchema(result);
619
- if (parsedResult instanceof type.errors) {
620
- logger.error("Invalid XPENDING response", {
621
- "aiki.stream": stream,
622
- "aiki.error": parsedResult.summary
623
- });
624
- continue;
625
- }
626
- const claimableStreamMessages = claimableMessagesByStream.get(stream) ?? [];
627
- for (const [messageId, consumerName, _idleTimeMs, _deliveryCount] of parsedResult) {
628
- if (consumerName !== workerId) {
629
- claimableStreamMessages.push(messageId);
630
- }
631
- }
632
- if (claimableStreamMessages.length) {
633
- claimableMessagesByStream.set(stream, claimableStreamMessages);
634
- }
635
- }
636
- return claimableMessagesByStream;
637
- }
638
-
639
- // subscribers/strategy-resolver.ts
640
- function resolveSubscriberStrategy(client2, strategy, workflows, workerShards) {
641
- switch (strategy.type) {
642
- case "redis":
643
- return createRedisStreamsStrategy(client2, strategy, workflows, workerShards);
644
- case "db":
645
- return createDbStrategy(client2, strategy, workflows, workerShards);
646
- default:
647
- return strategy;
648
- }
649
- }
650
-
651
79
  // client.ts
80
+ import { INTERNAL } from "@aikirun/types/symbols";
81
+ import { createORPCClient } from "@orpc/client";
82
+ import { RPCLink } from "@orpc/client/fetch";
652
83
  function client(params) {
653
84
  return new ClientImpl(params);
654
85
  }
@@ -656,10 +87,7 @@ var ClientImpl = class {
656
87
  constructor(params) {
657
88
  this.params = params;
658
89
  this.logger = params.logger ?? new ConsoleLogger();
659
- const apiKey = params.apiKey ?? process.env.AIKI_API_KEY;
660
- if (!apiKey) {
661
- throw new Error(`API key is required. Provide it via 'apiKey' param or AIKI_API_KEY env variable`);
662
- }
90
+ const { apiKey } = params;
663
91
  const rpcLink = new RPCLink({
664
92
  url: `${params.url}/api`,
665
93
  headers: () => ({
@@ -670,42 +98,14 @@ var ClientImpl = class {
670
98
  this.logger.info("Aiki client initialized", {
671
99
  "aiki.url": params.url
672
100
  });
673
- this[INTERNAL2] = {
674
- subscriber: {
675
- create: (strategy, workflows, workerShards) => resolveSubscriberStrategy(this, strategy, workflows, workerShards)
676
- },
677
- redis: {
678
- getConnection: () => this.getRedisConnection(),
679
- closeConnection: () => this.closeRedisConnection()
680
- },
101
+ this[INTERNAL] = {
681
102
  createContext: this.params.createContext
682
103
  };
683
104
  }
684
105
  api;
685
- [INTERNAL2];
106
+ [INTERNAL];
686
107
  logger;
687
- redisConnection;
688
- async close() {
689
- this.logger.info("Closing Aiki client");
690
- await this.closeRedisConnection();
691
- }
692
- getRedisConnection() {
693
- if (!this.redisConnection) {
694
- if (!this.params.redis) {
695
- throw new Error("Redis configuration not provided to client. Add 'redis' to ClientParams.");
696
- }
697
- this.redisConnection = new Redis(this.params.redis);
698
- }
699
- return this.redisConnection;
700
- }
701
- async closeRedisConnection() {
702
- if (this.redisConnection) {
703
- this.redisConnection.disconnect();
704
- this.redisConnection = void 0;
705
- }
706
- }
707
108
  };
708
109
  export {
709
- ConsoleLogger,
710
110
  client
711
111
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aikirun/client",
3
- "version": "0.23.0",
3
+ "version": "0.24.0",
4
4
  "description": "Client SDK for Aiki - connect to the server, start workflows, and manage execution",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -18,10 +18,8 @@
18
18
  "build": "tsup"
19
19
  },
20
20
  "dependencies": {
21
- "@aikirun/types": "0.23.0",
22
- "@orpc/client": "^1.9.3",
23
- "ioredis": "^5.4.1",
24
- "arktype": "^2.1.29"
21
+ "@aikirun/types": "0.24.0",
22
+ "@orpc/client": "^1.9.3"
25
23
  },
26
24
  "publishConfig": {
27
25
  "access": "public"