@convex-dev/workpool 0.2.18-alpha.2 → 0.2.18

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.
@@ -40,7 +40,7 @@ export {
40
40
  /** @deprecated Use `vResultValidator` instead. */
41
41
  vResultValidator as resultValidator,
42
42
  };
43
- /** @deprecated Use `vOnCompleteArgs(<your-context-validator>)` instead. */
43
+ /** Equivalent to `vOnCompleteArgs(<your-context-validator>)`. */
44
44
  export const vOnComplete = vOnCompleteArgs(v.any());
45
45
  /** @deprecated Use `vOnCompleteArgs()` instead. */
46
46
  export const vOnCompleteValidator = vOnCompleteArgs;
@@ -52,6 +52,9 @@ export const DEFAULT_RETRY_BEHAVIOR: RetryBehavior = {
52
52
  base: 2,
53
53
  };
54
54
 
55
+ // UseApi<api> for jump to definition
56
+ export type WorkpoolComponent = UseApi<Mounts>;
57
+
55
58
  export class Workpool {
56
59
  /**
57
60
  * Initializes a Workpool.
@@ -65,9 +68,10 @@ export class Workpool {
65
68
  * @param options - The {@link WorkpoolOptions} for the Workpool.
66
69
  */
67
70
  constructor(
68
- private component: UseApi<Mounts>, // UseApi<api> for jump to definition
71
+ public component: WorkpoolComponent,
69
72
  public options: WorkpoolOptions
70
73
  ) {}
74
+
71
75
  /**
72
76
  * Enqueues an action to be run.
73
77
  *
@@ -82,28 +86,48 @@ export class Workpool {
82
86
  ctx: RunMutationCtx,
83
87
  fn: FunctionReference<"action", FunctionVisibility, Args, ReturnType>,
84
88
  fnArgs: Args,
85
- options?: RetryOption & CallbackOptions & SchedulerOptions & NameOption
89
+ options?: RetryOption & EnqueueOptions
86
90
  ): Promise<WorkId> {
87
91
  const retryBehavior = getRetryBehavior(
88
92
  this.options.defaultRetryBehavior,
89
93
  this.options.retryActionsByDefault,
90
94
  options?.retry
91
95
  );
92
- const onComplete: OnComplete | undefined = options?.onComplete
93
- ? {
94
- fnHandle: await createFunctionHandle(options.onComplete),
95
- context: options.context,
96
- }
97
- : undefined;
98
- const id = await ctx.runMutation(this.component.lib.enqueue, {
99
- ...(await defaultEnqueueArgs(fn, options?.name, this.options)),
100
- fnArgs,
101
- fnType: "action",
102
- runAt: getRunAt(options),
103
- onComplete,
96
+ return enqueue(this.component, ctx, "action", fn, fnArgs, {
97
+ retryBehavior,
98
+ ...this.options,
99
+ ...options,
100
+ });
101
+ }
102
+
103
+ /**
104
+ * Enqueues a batch of actions to be run.
105
+ * Each action will be run independently, and the onComplete handler will
106
+ * be called for each action.
107
+ *
108
+ * @param ctx - The mutation or action ctx that can call ctx.runMutation.
109
+ * @param fn - The action to run, like `internal.example.myAction`.
110
+ * @param argsArray - The arguments to pass to the action.
111
+ * @param options - The options for the actions to specify retry behavior,
112
+ * onComplete handling, and scheduling via `runAt` or `runAfter`.
113
+ * @returns The IDs of the work that was enqueued.
114
+ */
115
+ async enqueueActionBatch<Args extends DefaultFunctionArgs, ReturnType>(
116
+ ctx: RunMutationCtx,
117
+ fn: FunctionReference<"action", FunctionVisibility, Args, ReturnType>,
118
+ argsArray: Array<Args>,
119
+ options?: RetryOption & EnqueueOptions
120
+ ): Promise<WorkId[]> {
121
+ const retryBehavior = getRetryBehavior(
122
+ this.options.defaultRetryBehavior,
123
+ this.options.retryActionsByDefault,
124
+ options?.retry
125
+ );
126
+ return enqueueBatch(this.component, ctx, "action", fn, argsArray, {
104
127
  retryBehavior,
128
+ ...this.options,
129
+ ...options,
105
130
  });
106
- return id as WorkId;
107
131
  }
108
132
 
109
133
  /**
@@ -123,44 +147,81 @@ export class Workpool {
123
147
  ctx: RunMutationCtx,
124
148
  fn: FunctionReference<"mutation", FunctionVisibility, Args, ReturnType>,
125
149
  fnArgs: Args,
126
- options?: CallbackOptions & SchedulerOptions & NameOption
150
+ options?: EnqueueOptions
127
151
  ): Promise<WorkId> {
128
- const onComplete: OnComplete | undefined = options?.onComplete
129
- ? {
130
- fnHandle: await createFunctionHandle(options.onComplete),
131
- context: options.context,
132
- }
133
- : undefined;
134
- const id = await ctx.runMutation(this.component.lib.enqueue, {
135
- ...(await defaultEnqueueArgs(fn, options?.name, this.options)),
136
- fnArgs,
137
- fnType: "mutation",
138
- runAt: getRunAt(options),
139
- onComplete,
152
+ return enqueue(this.component, ctx, "mutation", fn, fnArgs, {
153
+ ...this.options,
154
+ ...options,
155
+ });
156
+ }
157
+ /**
158
+ * Enqueues a batch of mutations to be run.
159
+ * Each mutation will be run independently, and the onComplete handler will
160
+ * be called for each mutation.
161
+ *
162
+ * @param ctx - The mutation or action context that can call ctx.runMutation.
163
+ * @param fn - The mutation to run, like `internal.example.myMutation`.
164
+ * @param argsArray - The arguments to pass to the mutations.
165
+ * @param options - The options for the mutations to specify onComplete handling
166
+ * and scheduling via `runAt` or `runAfter`.
167
+ */
168
+ async enqueueMutationBatch<Args extends DefaultFunctionArgs, ReturnType>(
169
+ ctx: RunMutationCtx,
170
+ fn: FunctionReference<"mutation", FunctionVisibility, Args, ReturnType>,
171
+ argsArray: Array<Args>,
172
+ options?: EnqueueOptions
173
+ ): Promise<WorkId[]> {
174
+ return enqueueBatch(this.component, ctx, "mutation", fn, argsArray, {
175
+ ...this.options,
176
+ ...options,
140
177
  });
141
- return id as WorkId;
142
178
  }
143
179
 
180
+ /**
181
+ * Enqueues a query to be run.
182
+ * Usually not what you want, but it can be useful during workflows.
183
+ * The query is run in a mutation and the result is returned to the caller,
184
+ * so it can conflict if other mutations are writing the value.
185
+ *
186
+ * @param ctx - The mutation or action context that can call ctx.runMutation.
187
+ * @param fn - The query to run, like `internal.example.myQuery`.
188
+ * @param fnArgs - The arguments to pass to the query.
189
+ * @param options - The options for the query to specify onComplete handling
190
+ * and scheduling via `runAt` or `runAfter`.
191
+ */
144
192
  async enqueueQuery<Args extends DefaultFunctionArgs, ReturnType>(
145
193
  ctx: RunMutationCtx,
146
194
  fn: FunctionReference<"query", FunctionVisibility, Args, ReturnType>,
147
195
  fnArgs: Args,
148
- options?: CallbackOptions & SchedulerOptions & NameOption
196
+ options?: EnqueueOptions
149
197
  ): Promise<WorkId> {
150
- const onComplete: OnComplete | undefined = options?.onComplete
151
- ? {
152
- fnHandle: await createFunctionHandle(options.onComplete),
153
- context: options.context,
154
- }
155
- : undefined;
156
- const id = await ctx.runMutation(this.component.lib.enqueue, {
157
- ...(await defaultEnqueueArgs(fn, options?.name, this.options)),
158
- fnArgs,
159
- fnType: "query",
160
- runAt: getRunAt(options),
161
- onComplete,
198
+ return enqueue(this.component, ctx, "query", fn, fnArgs, {
199
+ ...this.options,
200
+ ...options,
201
+ });
202
+ }
203
+
204
+ /**
205
+ * Enqueues a batch of queries to be run.
206
+ * Each query will be run independently, and the onComplete handler will
207
+ * be called for each query.
208
+ *
209
+ * @param ctx - The mutation or action context that can call ctx.runMutation.
210
+ * @param fn - The query to run, like `internal.example.myQuery`.
211
+ * @param argsArray - The arguments to pass to the queries.
212
+ * @param options - The options for the queries to specify onComplete handling
213
+ * and scheduling via `runAt` or `runAfter`.
214
+ */
215
+ async enqueueQueryBatch<Args extends DefaultFunctionArgs, ReturnType>(
216
+ ctx: RunMutationCtx,
217
+ fn: FunctionReference<"query", FunctionVisibility, Args, ReturnType>,
218
+ argsArray: Array<Args>,
219
+ options?: EnqueueOptions
220
+ ): Promise<WorkId[]> {
221
+ return enqueueBatch(this.component, ctx, "query", fn, argsArray, {
222
+ ...this.options,
223
+ ...options,
162
224
  });
163
- return id as WorkId;
164
225
  }
165
226
 
166
227
  /**
@@ -200,6 +261,17 @@ export class Workpool {
200
261
  return ctx.runQuery(this.component.lib.status, { id });
201
262
  }
202
263
 
264
+ /**
265
+ * Gets the status of a batch of work items.
266
+ *
267
+ * @param ctx - The query context that can call ctx.runQuery.
268
+ * @param ids - The IDs of the work to get the status of.
269
+ * @returns The status of the work items.
270
+ */
271
+ async statusBatch(ctx: RunQueryCtx, ids: WorkId[]): Promise<Status[]> {
272
+ return ctx.runQuery(this.component.lib.statusBatch, { ids });
273
+ }
274
+
203
275
  /**
204
276
  * Defines a mutation that will be run after a work item completes.
205
277
  * You can pass this to a call to enqueue* like so:
@@ -264,20 +336,11 @@ export function vOnCompleteArgs<
264
336
  >(context?: V) {
265
337
  return v.object({
266
338
  workId: vWorkIdValidator,
267
- context: context ?? v.optional(v.any()),
339
+ context: (context ?? v.optional(v.any())) as V,
268
340
  result: vResultValidator,
269
341
  });
270
342
  }
271
343
 
272
- export type NameOption = {
273
- /**
274
- * The name of the function. By default, if you pass in api.foo.bar.baz,
275
- * it will use "foo/bar:baz" as the name. If you pass in a function handle,
276
- * it will use the function handle directly.
277
- */
278
- name?: string;
279
- };
280
-
281
344
  export type RetryOption = {
282
345
  /** Whether to retry the action if it fails.
283
346
  * If true, it will use the default retry behavior.
@@ -315,25 +378,14 @@ export type WorkpoolRetryOptions = {
315
378
  */
316
379
  retryActionsByDefault?: boolean;
317
380
  };
318
- export type SchedulerOptions =
319
- | {
320
- /**
321
- * The time (ms since epoch) to run the action at.
322
- * If not provided, the action will be run as soon as possible.
323
- * Note: this is advisory only. It may run later.
324
- */
325
- runAt?: number;
326
- }
327
- | {
328
- /**
329
- * The number of milliseconds to run the action after.
330
- * If not provided, the action will be run as soon as possible.
331
- * Note: this is advisory only. It may run later.
332
- */
333
- runAfter?: number;
334
- };
335
381
 
336
- export type CallbackOptions = {
382
+ export type EnqueueOptions = {
383
+ /**
384
+ * The name of the function. By default, if you pass in api.foo.bar.baz,
385
+ * it will use "foo/bar:baz" as the name. If you pass in a function handle,
386
+ * it will use the function handle directly.
387
+ */
388
+ name?: string;
337
389
  /**
338
390
  * A mutation to run after the function succeeds, fails, or is canceled.
339
391
  * The context type is for your use, feel free to provide a validator for it.
@@ -368,7 +420,24 @@ export type CallbackOptions = {
368
420
  * Useful for passing data from the enqueue site to the onComplete site.
369
421
  */
370
422
  context?: unknown;
371
- };
423
+ } & (
424
+ | {
425
+ /**
426
+ * The time (ms since epoch) to run the action at.
427
+ * If not provided, the action will be run as soon as possible.
428
+ * Note: this is advisory only. It may run later.
429
+ */
430
+ runAt?: number;
431
+ }
432
+ | {
433
+ /**
434
+ * The number of milliseconds to run the action after.
435
+ * If not provided, the action will be run as soon as possible.
436
+ * Note: this is advisory only. It may run later.
437
+ */
438
+ runAfter?: number;
439
+ }
440
+ );
372
441
 
373
442
  export type OnCompleteArgs = {
374
443
  /**
@@ -412,36 +481,105 @@ function getRetryBehavior(
412
481
  return retryOverride ?? (retryByDefault ? defaultRetry : undefined);
413
482
  }
414
483
 
415
- async function defaultEnqueueArgs(
484
+ async function enqueueArgs(
416
485
  fn:
417
486
  | FunctionReference<FunctionType, FunctionVisibility>
418
487
  | FunctionHandle<FunctionType, DefaultFunctionArgs>,
419
- name: string | undefined,
420
- { logLevel, maxParallelism }: Partial<Config>
488
+ opts:
489
+ | (EnqueueOptions & Partial<Config> & { retryBehavior?: RetryBehavior })
490
+ | undefined
421
491
  ) {
422
492
  const [fnHandle, fnName] =
423
493
  typeof fn === "string" && fn.startsWith("function://")
424
- ? [fn, name ?? fn]
425
- : [await createFunctionHandle(fn), name ?? safeFunctionName(fn)];
494
+ ? [fn, opts?.name ?? fn]
495
+ : [await createFunctionHandle(fn), opts?.name ?? safeFunctionName(fn)];
496
+ const onComplete: OnComplete | undefined = opts?.onComplete
497
+ ? {
498
+ fnHandle: await createFunctionHandle(opts.onComplete),
499
+ context: opts.context,
500
+ }
501
+ : undefined;
426
502
  return {
427
503
  fnHandle,
428
504
  fnName,
505
+ onComplete,
506
+ runAt: getRunAt(opts),
507
+ retryBehavior: opts?.retryBehavior,
429
508
  config: {
430
- logLevel: logLevel ?? DEFAULT_LOG_LEVEL,
431
- maxParallelism: maxParallelism ?? DEFAULT_MAX_PARALLELISM,
509
+ logLevel: opts?.logLevel ?? DEFAULT_LOG_LEVEL,
510
+ maxParallelism: opts?.maxParallelism ?? DEFAULT_MAX_PARALLELISM,
432
511
  },
433
512
  };
434
513
  }
435
514
 
436
- function getRunAt(options?: SchedulerOptions): number {
515
+ function getRunAt(
516
+ options:
517
+ | {
518
+ runAt?: number;
519
+ runAfter?: number;
520
+ }
521
+ | undefined
522
+ ): number {
437
523
  if (!options) {
438
524
  return Date.now();
439
525
  }
440
- if ("runAt" in options && options.runAt !== undefined) {
526
+ if (options.runAt !== undefined) {
441
527
  return options.runAt;
442
528
  }
443
- if ("runAfter" in options && options.runAfter !== undefined) {
529
+ if (options.runAfter !== undefined) {
444
530
  return Date.now() + options.runAfter;
445
531
  }
446
532
  return Date.now();
447
533
  }
534
+
535
+ export async function enqueueBatch<
536
+ FnType extends FunctionType,
537
+ Args extends DefaultFunctionArgs,
538
+ ReturnType,
539
+ >(
540
+ component: UseApi<Mounts>,
541
+ ctx: RunMutationCtx,
542
+ fnType: FnType,
543
+ fn: FunctionReference<FnType, FunctionVisibility, Args, ReturnType>,
544
+ fnArgsArray: Array<Args>,
545
+ options: EnqueueOptions & {
546
+ retryBehavior?: RetryBehavior;
547
+ maxParallelism?: number;
548
+ logLevel?: LogLevel;
549
+ }
550
+ ): Promise<WorkId[]> {
551
+ const { config, ...defaults } = await enqueueArgs(fn, options);
552
+ const ids = await ctx.runMutation(component.lib.enqueueBatch, {
553
+ items: fnArgsArray.map((fnArgs) => ({
554
+ ...defaults,
555
+ fnArgs,
556
+ fnType,
557
+ })),
558
+ config,
559
+ });
560
+ return ids as WorkId[];
561
+ }
562
+
563
+ export async function enqueue<
564
+ FnType extends FunctionType,
565
+ Args extends DefaultFunctionArgs,
566
+ ReturnType,
567
+ >(
568
+ component: UseApi<Mounts>,
569
+ ctx: RunMutationCtx,
570
+ fnType: FnType,
571
+ fn: FunctionReference<FnType, FunctionVisibility, Args, ReturnType>,
572
+ fnArgs: Args,
573
+ options: EnqueueOptions & {
574
+ retryBehavior?: RetryBehavior;
575
+ maxParallelism?: number;
576
+ logLevel?: LogLevel;
577
+ }
578
+ ): Promise<WorkId> {
579
+ const id = await ctx.runMutation(component.lib.enqueue, {
580
+ ...(await enqueueArgs(fn, options)),
581
+ fnArgs,
582
+ fnType,
583
+ });
584
+ return id as WorkId;
585
+ }
@@ -89,6 +89,30 @@ export type Mounts = {
89
89
  },
90
90
  string
91
91
  >;
92
+ enqueueBatch: FunctionReference<
93
+ "mutation",
94
+ "public",
95
+ {
96
+ config: {
97
+ logLevel: "DEBUG" | "TRACE" | "INFO" | "REPORT" | "WARN" | "ERROR";
98
+ maxParallelism: number;
99
+ };
100
+ items: Array<{
101
+ fnArgs: any;
102
+ fnHandle: string;
103
+ fnName: string;
104
+ fnType: "action" | "mutation" | "query";
105
+ onComplete?: { context?: any; fnHandle: string };
106
+ retryBehavior?: {
107
+ base: number;
108
+ initialBackoffMs: number;
109
+ maxAttempts: number;
110
+ };
111
+ runAt: number;
112
+ }>;
113
+ },
114
+ Array<string>
115
+ >;
92
116
  status: FunctionReference<
93
117
  "query",
94
118
  "public",
@@ -97,6 +121,16 @@ export type Mounts = {
97
121
  | { previousAttempts: number; state: "running" }
98
122
  | { state: "finished" }
99
123
  >;
124
+ statusBatch: FunctionReference<
125
+ "query",
126
+ "public",
127
+ { ids: Array<string> },
128
+ Array<
129
+ | { previousAttempts: number; state: "pending" }
130
+ | { previousAttempts: number; state: "running" }
131
+ | { state: "finished" }
132
+ >
133
+ >;
100
134
  };
101
135
  };
102
136
  // For now fullApiWithMounts is only fullApi which provides
@@ -9,6 +9,8 @@ import {
9
9
  fromSegment,
10
10
  getCurrentSegment,
11
11
  getNextSegment,
12
+ SECOND,
13
+ toSegment,
12
14
  } from "./shared.js";
13
15
 
14
16
  /**
@@ -40,7 +42,7 @@ export async function kickMainLoop(
40
42
  );
41
43
  return next;
42
44
  }
43
- if (runStatus.state.segment <= next) {
45
+ if (runStatus.state.segment <= toSegment(Date.now() + SECOND)) {
44
46
  console.debug(
45
47
  `[${source}] main is scheduled to run soon enough, so we don't need to kick it`
46
48
  );
@@ -1,10 +1,10 @@
1
- import { v } from "convex/values";
1
+ import { Infer, ObjectType, v } from "convex/values";
2
2
  import { api } from "./_generated/api.js";
3
3
  import { fnType } from "./shared.js";
4
4
  import { Id } from "./_generated/dataModel.js";
5
- import { mutation, MutationCtx, query } from "./_generated/server.js";
5
+ import { mutation, MutationCtx, query, QueryCtx } from "./_generated/server.js";
6
6
  import { kickMainLoop } from "./kick.js";
7
- import { createLogger, LogLevel, logLevel } from "./logging.js";
7
+ import { createLogger, Logger, LogLevel, logLevel } from "./logging.js";
8
8
  import {
9
9
  boundScheduledTime,
10
10
  config,
@@ -20,44 +20,75 @@ import { recordEnqueued } from "./stats.js";
20
20
  const MAX_POSSIBLE_PARALLELISM = 100;
21
21
  const MAX_PARALLELISM_SOFT_LIMIT = 50;
22
22
 
23
+ const itemArgs = {
24
+ fnHandle: v.string(),
25
+ fnName: v.string(),
26
+ fnArgs: v.any(),
27
+ fnType,
28
+ runAt: v.number(),
29
+ // TODO: annotation?
30
+ onComplete: v.optional(onComplete),
31
+ retryBehavior: v.optional(retryBehavior),
32
+ };
33
+ const enqueueArgs = {
34
+ ...itemArgs,
35
+ config,
36
+ };
23
37
  export const enqueue = mutation({
38
+ args: enqueueArgs,
39
+ returns: v.id("work"),
40
+ handler: async (ctx, { config, ...itemArgs }) => {
41
+ validateConfig(config);
42
+ const console = createLogger(config.logLevel);
43
+ const kickSegment = await kickMainLoop(ctx, "enqueue", config);
44
+ return await enqueueHandler(ctx, console, kickSegment, itemArgs);
45
+ },
46
+ });
47
+ async function enqueueHandler(
48
+ ctx: MutationCtx,
49
+ console: Logger,
50
+ kickSegment: bigint,
51
+ { runAt, ...workArgs }: ObjectType<typeof itemArgs>
52
+ ) {
53
+ runAt = boundScheduledTime(runAt, console);
54
+ const workId = await ctx.db.insert("work", {
55
+ ...workArgs,
56
+ attempts: 0,
57
+ });
58
+ await ctx.db.insert("pendingStart", {
59
+ workId,
60
+ segment: max(toSegment(runAt), kickSegment),
61
+ });
62
+ recordEnqueued(console, { workId, fnName: workArgs.fnName, runAt });
63
+ return workId;
64
+ }
65
+
66
+ type Config = Infer<typeof config>;
67
+ function validateConfig(config: Config) {
68
+ if (config.maxParallelism > MAX_POSSIBLE_PARALLELISM) {
69
+ throw new Error(`maxParallelism must be <= ${MAX_PARALLELISM_SOFT_LIMIT}`);
70
+ } else if (config.maxParallelism > MAX_PARALLELISM_SOFT_LIMIT) {
71
+ createLogger(config.logLevel).warn(
72
+ `maxParallelism should be <= ${MAX_PARALLELISM_SOFT_LIMIT}, but is set to ${config.maxParallelism}. This will be an error in a future version.`
73
+ );
74
+ } else if (config.maxParallelism < 1) {
75
+ throw new Error("maxParallelism must be >= 1");
76
+ }
77
+ }
78
+
79
+ export const enqueueBatch = mutation({
24
80
  args: {
25
- fnHandle: v.string(),
26
- fnName: v.string(),
27
- fnArgs: v.any(),
28
- fnType,
29
- runAt: v.number(),
30
- // TODO: annotation?
31
- onComplete: v.optional(onComplete),
32
- retryBehavior: v.optional(retryBehavior),
81
+ items: v.array(v.object(itemArgs)),
33
82
  config,
34
83
  },
35
- returns: v.id("work"),
36
- handler: async (ctx, { config, runAt, ...workArgs }) => {
84
+ returns: v.array(v.id("work")),
85
+ handler: async (ctx, { config, items }) => {
86
+ validateConfig(config);
37
87
  const console = createLogger(config.logLevel);
38
- if (config.maxParallelism > MAX_POSSIBLE_PARALLELISM) {
39
- throw new Error(
40
- `maxParallelism must be <= ${MAX_PARALLELISM_SOFT_LIMIT}`
41
- );
42
- } else if (config.maxParallelism > MAX_PARALLELISM_SOFT_LIMIT) {
43
- console.warn(
44
- `maxParallelism should be <= ${MAX_PARALLELISM_SOFT_LIMIT}, but is set to ${config.maxParallelism}. This will be an error in a future version.`
45
- );
46
- } else if (config.maxParallelism < 1) {
47
- throw new Error("maxParallelism must be >= 1");
48
- }
49
- runAt = boundScheduledTime(runAt, console);
50
- const workId = await ctx.db.insert("work", {
51
- ...workArgs,
52
- attempts: 0,
53
- });
54
- const limit = await kickMainLoop(ctx, "enqueue", config);
55
- await ctx.db.insert("pendingStart", {
56
- workId,
57
- segment: max(toSegment(runAt), limit),
58
- });
59
- recordEnqueued(console, { workId, fnName: workArgs.fnName, runAt });
60
- return workId;
88
+ const kickSegment = await kickMainLoop(ctx, "enqueue", config);
89
+ return Promise.all(
90
+ items.map((item) => enqueueHandler(ctx, console, kickSegment, item))
91
+ );
61
92
  },
62
93
  });
63
94
 
@@ -119,27 +150,38 @@ export const cancelAll = mutation({
119
150
  export const status = query({
120
151
  args: { id: v.id("work") },
121
152
  returns: statusValidator,
122
- handler: async (ctx, { id }) => {
123
- const work = await ctx.db.get(id);
124
- if (!work) {
125
- return { state: "finished" } as const;
126
- }
127
- const pendingStart = await ctx.db
128
- .query("pendingStart")
129
- .withIndex("workId", (q) => q.eq("workId", id))
130
- .unique();
131
- if (pendingStart) {
132
- return { state: "pending", previousAttempts: work.attempts } as const;
133
- }
134
- const pendingCompletion = await ctx.db
135
- .query("pendingCompletion")
136
- .withIndex("workId", (q) => q.eq("workId", id))
137
- .unique();
138
- if (pendingCompletion?.retry) {
139
- return { state: "pending", previousAttempts: work.attempts } as const;
140
- }
141
- // Assume it's in progress. It could be pending cancelation
142
- return { state: "running", previousAttempts: work.attempts } as const;
153
+ handler: statusHandler,
154
+ });
155
+ async function statusHandler(ctx: QueryCtx, { id }: { id: Id<"work"> }) {
156
+ const work = await ctx.db.get(id);
157
+ if (!work) {
158
+ return { state: "finished" } as const;
159
+ }
160
+ const pendingStart = await ctx.db
161
+ .query("pendingStart")
162
+ .withIndex("workId", (q) => q.eq("workId", id))
163
+ .unique();
164
+ if (pendingStart) {
165
+ return { state: "pending", previousAttempts: work.attempts } as const;
166
+ }
167
+ const pendingCompletion = await ctx.db
168
+ .query("pendingCompletion")
169
+ .withIndex("workId", (q) => q.eq("workId", id))
170
+ .unique();
171
+ if (pendingCompletion?.retry) {
172
+ return { state: "pending", previousAttempts: work.attempts } as const;
173
+ }
174
+ // Assume it's in progress. It could be pending cancelation
175
+ return { state: "running", previousAttempts: work.attempts } as const;
176
+ }
177
+
178
+ export const statusBatch = query({
179
+ args: { ids: v.array(v.id("work")) },
180
+ returns: v.array(statusValidator),
181
+ handler: async (ctx, { ids }) => {
182
+ return await Promise.all(
183
+ ids.map(async (id) => await statusHandler(ctx, { id }))
184
+ );
143
185
  },
144
186
  });
145
187