@convex-dev/workpool 0.2.18-alpha.3 → 0.2.19-alpha.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.
@@ -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
  /**
@@ -181,9 +242,13 @@ export class Workpool {
181
242
  *
182
243
  * @param ctx - The mutation or action context that can call ctx.runMutation.
183
244
  */
184
- async cancelAll(ctx: RunMutationCtx): Promise<void> {
245
+ async cancelAll(
246
+ ctx: RunMutationCtx,
247
+ options?: { limit?: number }
248
+ ): Promise<void> {
185
249
  await ctx.runMutation(this.component.lib.cancelAll, {
186
250
  logLevel: this.options.logLevel ?? DEFAULT_LOG_LEVEL,
251
+ ...options,
187
252
  });
188
253
  }
189
254
  /**
@@ -200,6 +265,17 @@ export class Workpool {
200
265
  return ctx.runQuery(this.component.lib.status, { id });
201
266
  }
202
267
 
268
+ /**
269
+ * Gets the status of a batch of work items.
270
+ *
271
+ * @param ctx - The query context that can call ctx.runQuery.
272
+ * @param ids - The IDs of the work to get the status of.
273
+ * @returns The status of the work items.
274
+ */
275
+ async statusBatch(ctx: RunQueryCtx, ids: WorkId[]): Promise<Status[]> {
276
+ return ctx.runQuery(this.component.lib.statusBatch, { ids });
277
+ }
278
+
203
279
  /**
204
280
  * Defines a mutation that will be run after a work item completes.
205
281
  * You can pass this to a call to enqueue* like so:
@@ -269,17 +345,9 @@ export function vOnCompleteArgs<
269
345
  });
270
346
  }
271
347
 
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
348
  export type RetryOption = {
282
349
  /** Whether to retry the action if it fails.
350
+ * If false, the action won’t be retried.
283
351
  * If true, it will use the default retry behavior.
284
352
  * If custom behavior is provided, it will retry using that behavior.
285
353
  * If unset, it will use the Workpool's configured default.
@@ -315,25 +383,14 @@ export type WorkpoolRetryOptions = {
315
383
  */
316
384
  retryActionsByDefault?: boolean;
317
385
  };
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
386
 
336
- export type CallbackOptions = {
387
+ export type EnqueueOptions = {
388
+ /**
389
+ * The name of the function. By default, if you pass in api.foo.bar.baz,
390
+ * it will use "foo/bar:baz" as the name. If you pass in a function handle,
391
+ * it will use the function handle directly.
392
+ */
393
+ name?: string;
337
394
  /**
338
395
  * A mutation to run after the function succeeds, fails, or is canceled.
339
396
  * The context type is for your use, feel free to provide a validator for it.
@@ -368,7 +425,24 @@ export type CallbackOptions = {
368
425
  * Useful for passing data from the enqueue site to the onComplete site.
369
426
  */
370
427
  context?: unknown;
371
- };
428
+ } & (
429
+ | {
430
+ /**
431
+ * The time (ms since epoch) to run the action at.
432
+ * If not provided, the action will be run as soon as possible.
433
+ * Note: this is advisory only. It may run later.
434
+ */
435
+ runAt?: number;
436
+ }
437
+ | {
438
+ /**
439
+ * The number of milliseconds to run the action after.
440
+ * If not provided, the action will be run as soon as possible.
441
+ * Note: this is advisory only. It may run later.
442
+ */
443
+ runAfter?: number;
444
+ }
445
+ );
372
446
 
373
447
  export type OnCompleteArgs = {
374
448
  /**
@@ -412,36 +486,105 @@ function getRetryBehavior(
412
486
  return retryOverride ?? (retryByDefault ? defaultRetry : undefined);
413
487
  }
414
488
 
415
- async function defaultEnqueueArgs(
489
+ async function enqueueArgs(
416
490
  fn:
417
491
  | FunctionReference<FunctionType, FunctionVisibility>
418
492
  | FunctionHandle<FunctionType, DefaultFunctionArgs>,
419
- name: string | undefined,
420
- { logLevel, maxParallelism }: Partial<Config>
493
+ opts:
494
+ | (EnqueueOptions & Partial<Config> & { retryBehavior?: RetryBehavior })
495
+ | undefined
421
496
  ) {
422
497
  const [fnHandle, fnName] =
423
498
  typeof fn === "string" && fn.startsWith("function://")
424
- ? [fn, name ?? fn]
425
- : [await createFunctionHandle(fn), name ?? safeFunctionName(fn)];
499
+ ? [fn, opts?.name ?? fn]
500
+ : [await createFunctionHandle(fn), opts?.name ?? safeFunctionName(fn)];
501
+ const onComplete: OnComplete | undefined = opts?.onComplete
502
+ ? {
503
+ fnHandle: await createFunctionHandle(opts.onComplete),
504
+ context: opts.context,
505
+ }
506
+ : undefined;
426
507
  return {
427
508
  fnHandle,
428
509
  fnName,
510
+ onComplete,
511
+ runAt: getRunAt(opts),
512
+ retryBehavior: opts?.retryBehavior,
429
513
  config: {
430
- logLevel: logLevel ?? DEFAULT_LOG_LEVEL,
431
- maxParallelism: maxParallelism ?? DEFAULT_MAX_PARALLELISM,
514
+ logLevel: opts?.logLevel ?? DEFAULT_LOG_LEVEL,
515
+ maxParallelism: opts?.maxParallelism ?? DEFAULT_MAX_PARALLELISM,
432
516
  },
433
517
  };
434
518
  }
435
519
 
436
- function getRunAt(options?: SchedulerOptions): number {
520
+ function getRunAt(
521
+ options:
522
+ | {
523
+ runAt?: number;
524
+ runAfter?: number;
525
+ }
526
+ | undefined
527
+ ): number {
437
528
  if (!options) {
438
529
  return Date.now();
439
530
  }
440
- if ("runAt" in options && options.runAt !== undefined) {
531
+ if (options.runAt !== undefined) {
441
532
  return options.runAt;
442
533
  }
443
- if ("runAfter" in options && options.runAfter !== undefined) {
534
+ if (options.runAfter !== undefined) {
444
535
  return Date.now() + options.runAfter;
445
536
  }
446
537
  return Date.now();
447
538
  }
539
+
540
+ export async function enqueueBatch<
541
+ FnType extends FunctionType,
542
+ Args extends DefaultFunctionArgs,
543
+ ReturnType,
544
+ >(
545
+ component: UseApi<Mounts>,
546
+ ctx: RunMutationCtx,
547
+ fnType: FnType,
548
+ fn: FunctionReference<FnType, FunctionVisibility, Args, ReturnType>,
549
+ fnArgsArray: Array<Args>,
550
+ options: EnqueueOptions & {
551
+ retryBehavior?: RetryBehavior;
552
+ maxParallelism?: number;
553
+ logLevel?: LogLevel;
554
+ }
555
+ ): Promise<WorkId[]> {
556
+ const { config, ...defaults } = await enqueueArgs(fn, options);
557
+ const ids = await ctx.runMutation(component.lib.enqueueBatch, {
558
+ items: fnArgsArray.map((fnArgs) => ({
559
+ ...defaults,
560
+ fnArgs,
561
+ fnType,
562
+ })),
563
+ config,
564
+ });
565
+ return ids as WorkId[];
566
+ }
567
+
568
+ export async function enqueue<
569
+ FnType extends FunctionType,
570
+ Args extends DefaultFunctionArgs,
571
+ ReturnType,
572
+ >(
573
+ component: UseApi<Mounts>,
574
+ ctx: RunMutationCtx,
575
+ fnType: FnType,
576
+ fn: FunctionReference<FnType, FunctionVisibility, Args, ReturnType>,
577
+ fnArgs: Args,
578
+ options: EnqueueOptions & {
579
+ retryBehavior?: RetryBehavior;
580
+ maxParallelism?: number;
581
+ logLevel?: LogLevel;
582
+ }
583
+ ): Promise<WorkId> {
584
+ const id = await ctx.runMutation(component.lib.enqueue, {
585
+ ...(await enqueueArgs(fn, options)),
586
+ fnArgs,
587
+ fnType,
588
+ });
589
+ return id as WorkId;
590
+ }
@@ -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
  );