@convex-dev/workpool 0.2.14 → 0.2.16

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.
Files changed (39) hide show
  1. package/README.md +23 -13
  2. package/dist/commonjs/client/index.d.ts +191 -56
  3. package/dist/commonjs/client/index.d.ts.map +1 -1
  4. package/dist/commonjs/client/index.js +55 -6
  5. package/dist/commonjs/client/index.js.map +1 -1
  6. package/dist/commonjs/component/complete.js +2 -2
  7. package/dist/commonjs/component/complete.js.map +1 -1
  8. package/dist/commonjs/component/schema.js +2 -2
  9. package/dist/commonjs/component/schema.js.map +1 -1
  10. package/dist/commonjs/component/shared.d.ts +2 -2
  11. package/dist/commonjs/component/shared.d.ts.map +1 -1
  12. package/dist/commonjs/component/shared.js +1 -1
  13. package/dist/commonjs/component/shared.js.map +1 -1
  14. package/dist/commonjs/component/stats.d.ts.map +1 -1
  15. package/dist/commonjs/component/stats.js +7 -25
  16. package/dist/commonjs/component/stats.js.map +1 -1
  17. package/dist/esm/client/index.d.ts +191 -56
  18. package/dist/esm/client/index.d.ts.map +1 -1
  19. package/dist/esm/client/index.js +55 -6
  20. package/dist/esm/client/index.js.map +1 -1
  21. package/dist/esm/component/complete.js +2 -2
  22. package/dist/esm/component/complete.js.map +1 -1
  23. package/dist/esm/component/schema.js +2 -2
  24. package/dist/esm/component/schema.js.map +1 -1
  25. package/dist/esm/component/shared.d.ts +2 -2
  26. package/dist/esm/component/shared.d.ts.map +1 -1
  27. package/dist/esm/component/shared.js +1 -1
  28. package/dist/esm/component/shared.js.map +1 -1
  29. package/dist/esm/component/stats.d.ts.map +1 -1
  30. package/dist/esm/component/stats.js +7 -25
  31. package/dist/esm/component/stats.js.map +1 -1
  32. package/package.json +4 -3
  33. package/src/client/index.ts +145 -59
  34. package/src/component/complete.ts +2 -2
  35. package/src/component/kick.test.ts +9 -2
  36. package/src/component/loop.test.ts +0 -1
  37. package/src/component/schema.ts +2 -2
  38. package/src/component/shared.ts +2 -2
  39. package/src/component/stats.ts +8 -31
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  "email": "support@convex.dev",
8
8
  "url": "https://github.com/get-convex/workpool/issues"
9
9
  },
10
- "version": "0.2.14",
10
+ "version": "0.2.16",
11
11
  "license": "Apache-2.0",
12
12
  "keywords": [
13
13
  "convex",
@@ -31,7 +31,7 @@
31
31
  "test:debug": "vitest --inspect-brk --no-file-parallelism",
32
32
  "test:coverage": "vitest run --coverage --coverage.reporter=text",
33
33
  "alpha": "rm -rf dist && npm run build && npm run test && npm version prerelease --preid alpha && npm publish --tag alpha && git push --tags",
34
- "release": "rm -rf dist && npm run build && npm run test && npm version patch && npm publish && git push --tags",
34
+ "release": "rm -rf dist && npm run build && npm run test && npm version patch && npm publish && git push --tags && git push",
35
35
  "prepare": "npm run build",
36
36
  "prepack": "node node10stubs.mjs",
37
37
  "postpack": "node node10stubs.mjs --cleanup"
@@ -72,9 +72,10 @@
72
72
  "@eslint/js": "^9.9.1",
73
73
  "@types/node": "^18.17.0",
74
74
  "@vitest/coverage-v8": "^2.1.9",
75
- "convex-test": "^0.0.36-alpha.0",
75
+ "convex-test": "^0.0.38-alpha.0",
76
76
  "eslint": "^9.9.1",
77
77
  "globals": "^15.9.0",
78
+ "pkg-pr-new": "^0.0.54",
78
79
  "prettier": "3.2.5",
79
80
  "typescript": "^5.8.3",
80
81
  "typescript-eslint": "^8.4.0",
@@ -5,29 +5,45 @@ import {
5
5
  FunctionReference,
6
6
  FunctionType,
7
7
  FunctionVisibility,
8
+ GenericDataModel,
9
+ GenericMutationCtx,
8
10
  getFunctionName,
11
+ internalMutationGeneric,
12
+ RegisteredMutation,
9
13
  } from "convex/server";
10
- import { v, VString } from "convex/values";
14
+ import { Infer, v, Validator, VAny, VString } from "convex/values";
11
15
  import { Mounts } from "../component/_generated/api.js";
12
16
  import { DEFAULT_LOG_LEVEL, type LogLevel } from "../component/logging.js";
13
17
  import {
14
18
  Config,
15
19
  DEFAULT_MAX_PARALLELISM,
16
20
  OnComplete,
17
- runResult as resultValidator,
21
+ vResultValidator,
18
22
  type RetryBehavior,
19
23
  RunResult,
20
24
  OnCompleteArgs as SharedOnCompleteArgs,
21
25
  Status,
22
26
  } from "../component/shared.js";
23
27
  import { RunMutationCtx, RunQueryCtx, UseApi } from "./utils.js";
24
- export { resultValidator, type RunResult, type RetryBehavior, type OnComplete };
28
+ export {
29
+ vResultValidator,
30
+ type RunResult,
31
+ type RetryBehavior,
32
+ type OnComplete,
33
+ };
25
34
  export {
26
35
  retryBehavior as vRetryBehavior,
27
36
  onComplete as vOnComplete,
28
37
  } from "../component/shared.js";
29
38
  export { logLevel as vLogLevel, type LogLevel } from "../component/logging.js";
30
- export { resultValidator as vResultValidator };
39
+ export type WorkId = string & { __isWorkId: true };
40
+ export const vWorkIdValidator = v.string() as VString<WorkId>;
41
+ export {
42
+ /** @deprecated Use `vWorkIdValidator` instead. */
43
+ vWorkIdValidator as workIdValidator,
44
+ /** @deprecated Use `vResultValidator` instead. */
45
+ vResultValidator as resultValidator,
46
+ };
31
47
 
32
48
  // Attempts will run with delay [0, 250, 500, 1000, 2000] (ms)
33
49
  export const DEFAULT_RETRY_BEHAVIOR: RetryBehavior = {
@@ -35,56 +51,6 @@ export const DEFAULT_RETRY_BEHAVIOR: RetryBehavior = {
35
51
  initialBackoffMs: 250,
36
52
  base: 2,
37
53
  };
38
- export type WorkId = string & { __isWorkId: true };
39
- export const workIdValidator = v.string() as VString<WorkId>;
40
- export { workIdValidator as vWorkIdValidator };
41
-
42
- export type NameOption = {
43
- /**
44
- * The name of the function. By default, if you pass in api.foo.bar.baz,
45
- * it will use "foo/bar:baz" as the name. If you pass in a function handle,
46
- * it will use the function handle directly.
47
- */
48
- name?: string;
49
- };
50
-
51
- export type RetryOption = {
52
- /** Whether to retry the action if it fails.
53
- * If true, it will use the default retry behavior.
54
- * If custom behavior is provided, it will retry using that behavior.
55
- * If unset, it will use the Workpool's configured default.
56
- */
57
- retry?: boolean | RetryBehavior;
58
- };
59
-
60
- export type WorkpoolOptions = {
61
- /** How many actions/mutations can be running at once within this pool.
62
- * Min 1, Max 300.
63
- */
64
- maxParallelism?: number;
65
- /** How much to log. This is updated on each call to `enqueue*`,
66
- * `status`, or `cancel*`.
67
- * Default is REPORT, which logs warnings, errors, and a periodic report.
68
- * With INFO, you can also see events for started and completed work.
69
- * Stats generated can be parsed by tools like
70
- * [Axiom](https://axiom.co) for monitoring.
71
- * With DEBUG, you can see timers and internal events for work being
72
- * scheduled.
73
- */
74
- logLevel?: LogLevel;
75
- } & WorkpoolRetryOptions;
76
-
77
- export type WorkpoolRetryOptions = {
78
- /** Default retry behavior for enqueued actions.
79
- * See {@link RetryBehavior}.
80
- */
81
- defaultRetryBehavior?: RetryBehavior;
82
- /** Whether to retry actions that fail by default. Default: false.
83
- * NOTE: Only enable this if your actions are idempotent.
84
- * See the docs (README.md) for more details.
85
- */
86
- retryActionsByDefault?: boolean;
87
- };
88
54
 
89
55
  export class Workpool {
90
56
  /**
@@ -233,8 +199,122 @@ export class Workpool {
233
199
  async status(ctx: RunQueryCtx, id: WorkId): Promise<Status> {
234
200
  return ctx.runQuery(this.component.lib.status, { id });
235
201
  }
202
+
203
+ /**
204
+ * Defines a mutation that will be run after a work item completes.
205
+ * You can pass this to a call to enqueue* like so:
206
+ * ```ts
207
+ * export const myOnComplete = workpool.defineOnComplete({
208
+ * context: v.literal("myContextValue"), // optional
209
+ * handler: async (ctx, {workId, context, result}) => {
210
+ * // ... do something with the result
211
+ * },
212
+ * });
213
+ *
214
+ * // in some other function:
215
+ * const workId = await workpool.enqueueAction(ctx, internal.foo.bar, {
216
+ * // ... args to action
217
+ * }, {
218
+ * onComplete: internal.foo.myOnComplete,
219
+ * });
220
+ * ```
221
+ */
222
+ defineOnComplete<
223
+ DataModel extends GenericDataModel,
224
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
225
+ V extends Validator<any, "required", any> = VAny,
226
+ >({
227
+ context,
228
+ handler,
229
+ }: {
230
+ context?: V;
231
+ handler: (
232
+ ctx: GenericMutationCtx<DataModel>,
233
+ args: {
234
+ workId: WorkId;
235
+ context: Infer<V>;
236
+ result: RunResult;
237
+ }
238
+ ) => Promise<void>;
239
+ }): RegisteredMutation<"internal", OnCompleteArgs, null> {
240
+ return internalMutationGeneric({
241
+ args: vOnCompleteValidator(context),
242
+ handler,
243
+ });
244
+ }
245
+ }
246
+
247
+ /**
248
+ * Returns a validator to use for the onComplete mutation.
249
+ * To be used like:
250
+ * ```ts
251
+ * export const myOnComplete = internalMutation({
252
+ * args: vOnCompleteValidator(v.string()),
253
+ * handler: async (ctx, {workId, context, result}) => {
254
+ * // context has been validated as a string
255
+ * // ... do something with the result
256
+ * },
257
+ * });
258
+ * @param context - The context validator. If not provided, it will be `v.any()`.
259
+ * @returns The validator for the onComplete mutation.
260
+ */
261
+ export function vOnCompleteValidator<
262
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
263
+ V extends Validator<any, "required", any> = VAny,
264
+ >(context?: V) {
265
+ return v.object({
266
+ workId: vWorkIdValidator,
267
+ context: context ?? v.any(),
268
+ result: vResultValidator,
269
+ });
236
270
  }
237
271
 
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
+ export type RetryOption = {
282
+ /** Whether to retry the action if it fails.
283
+ * If true, it will use the default retry behavior.
284
+ * If custom behavior is provided, it will retry using that behavior.
285
+ * If unset, it will use the Workpool's configured default.
286
+ */
287
+ retry?: boolean | RetryBehavior;
288
+ };
289
+
290
+ export type WorkpoolOptions = {
291
+ /** How many actions/mutations can be running at once within this pool.
292
+ * Min 1, Max 300.
293
+ */
294
+ maxParallelism?: number;
295
+ /** How much to log. This is updated on each call to `enqueue*`,
296
+ * `status`, or `cancel*`.
297
+ * Default is REPORT, which logs warnings, errors, and a periodic report.
298
+ * With INFO, you can also see events for started and completed work.
299
+ * Stats generated can be parsed by tools like
300
+ * [Axiom](https://axiom.co) for monitoring.
301
+ * With DEBUG, you can see timers and internal events for work being
302
+ * scheduled.
303
+ */
304
+ logLevel?: LogLevel;
305
+ } & WorkpoolRetryOptions;
306
+
307
+ export type WorkpoolRetryOptions = {
308
+ /** Default retry behavior for enqueued actions.
309
+ * See {@link RetryBehavior}.
310
+ */
311
+ defaultRetryBehavior?: RetryBehavior;
312
+ /** Whether to retry actions that fail by default. Default: false.
313
+ * NOTE: Only enable this if your actions are idempotent.
314
+ * See the docs (README.md) for more details.
315
+ */
316
+ retryActionsByDefault?: boolean;
317
+ };
238
318
  export type SchedulerOptions =
239
319
  | {
240
320
  /**
@@ -259,12 +339,18 @@ export type CallbackOptions = {
259
339
  * The context type is for your use, feel free to provide a validator for it.
260
340
  * e.g.
261
341
  * ```ts
342
+ * export const completion = workpool.defineOnComplete({
343
+ * context: v.string(),
344
+ * handler: async (ctx, {workId, context, result}) => {
345
+ * // context has been validated as a string
346
+ * // ... do something with the result
347
+ * },
348
+ * });
349
+ * ```
350
+ * or more manually:
351
+ * ```ts
262
352
  * export const completion = internalMutation({
263
- * args: {
264
- * workId: workIdValidator,
265
- * context: v.any(),
266
- * result: resultValidator,
267
- * },
353
+ * args: vOnCompleteValidator(v.string()),
268
354
  * handler: async (ctx, args) => {
269
355
  * console.log(args.result, "Got Context back -> ", args.context, Date.now() - args.context);
270
356
  * },
@@ -4,7 +4,7 @@ import { Id } from "./_generated/dataModel.js";
4
4
  import { internalMutation, MutationCtx } from "./_generated/server.js";
5
5
  import { kickMainLoop } from "./kick.js";
6
6
  import { createLogger } from "./logging.js";
7
- import { OnCompleteArgs, RunResult, runResult } from "./shared.js";
7
+ import { OnCompleteArgs, RunResult, vResultValidator } from "./shared.js";
8
8
  import { recordCompleted } from "./stats.js";
9
9
 
10
10
  export type CompleteJob = Infer<typeof completeArgs.fields.jobs.element>;
@@ -12,7 +12,7 @@ export type CompleteJob = Infer<typeof completeArgs.fields.jobs.element>;
12
12
  export const completeArgs = v.object({
13
13
  jobs: v.array(
14
14
  v.object({
15
- runResult: runResult,
15
+ runResult: vResultValidator,
16
16
  workId: v.id("work"),
17
17
  attempt: v.number(),
18
18
  })
@@ -9,7 +9,6 @@ import {
9
9
  vi,
10
10
  } from "vitest";
11
11
  import { internal } from "./_generated/api";
12
- import { Id } from "./_generated/dataModel.js";
13
12
  import { kickMainLoop } from "./kick.js";
14
13
  import { DEFAULT_LOG_LEVEL } from "./logging.js";
15
14
  import schema from "./schema.js";
@@ -276,13 +275,18 @@ describe("kickMainLoop", () => {
276
275
  const runStatus = await ctx.db.query("runStatus").unique();
277
276
  assert(runStatus);
278
277
  const segment = getNextSegment() + 10n;
278
+ const scheduledId = await ctx.scheduler.runAfter(
279
+ 10_000,
280
+ internal.loop.main,
281
+ { generation: 0n, segment }
282
+ );
279
283
  await ctx.db.patch(runStatus._id, {
280
284
  state: {
281
285
  generation: 0n,
282
286
  saturated: false,
283
287
  kind: "scheduled",
284
288
  segment,
285
- scheduledId: "" as Id<"_scheduled_functions">,
289
+ scheduledId,
286
290
  },
287
291
  });
288
292
  // await all scheduled functions to run
@@ -291,6 +295,9 @@ describe("kickMainLoop", () => {
291
295
  assert(afterStatus);
292
296
  expect(afterStatus.state.kind).toBe("running");
293
297
  assert(afterStatus.state.kind === "running");
298
+ const scheduledJob = await ctx.db.system.get(scheduledId);
299
+ assert(scheduledJob);
300
+ expect(scheduledJob.state.kind).toBe("canceled");
294
301
  });
295
302
  });
296
303
  });
@@ -17,7 +17,6 @@ import {
17
17
  DEFAULT_MAX_PARALLELISM,
18
18
  getCurrentSegment,
19
19
  getNextSegment,
20
- HOUR,
21
20
  toSegment,
22
21
  } from "./shared";
23
22
  import { DEFAULT_LOG_LEVEL } from "./logging";
@@ -5,7 +5,7 @@ import {
5
5
  config,
6
6
  onComplete,
7
7
  retryBehavior,
8
- runResult,
8
+ vResultValidator,
9
9
  } from "./shared.js";
10
10
 
11
11
  // Represents a slice of time to process work.
@@ -80,7 +80,7 @@ export default defineSchema({
80
80
  // Written by complete, read & deleted by `main`.
81
81
  pendingCompletion: defineTable({
82
82
  segment,
83
- runResult,
83
+ runResult: vResultValidator,
84
84
  workId: v.id("work"),
85
85
  retry: v.boolean(),
86
86
  })
@@ -64,7 +64,7 @@ export type RetryBehavior = {
64
64
  // This ensures that the type satisfies the schema.
65
65
  const _ = {} as RetryBehavior satisfies Infer<typeof retryBehavior>;
66
66
 
67
- export const runResult = v.union(
67
+ export const vResultValidator = v.union(
68
68
  v.object({
69
69
  kind: v.literal("success"),
70
70
  returnValue: v.any(),
@@ -77,7 +77,7 @@ export const runResult = v.union(
77
77
  kind: v.literal("canceled"),
78
78
  })
79
79
  );
80
- export type RunResult = Infer<typeof runResult>;
80
+ export type RunResult = Infer<typeof vResultValidator>;
81
81
 
82
82
  export const onComplete = v.object({
83
83
  fnHandle: v.string(), // mutation
@@ -15,8 +15,6 @@ import { internal } from "./_generated/api.js";
15
15
  import schema from "./schema.js";
16
16
  import { paginator } from "convex-helpers/server/pagination";
17
17
 
18
- const BACKLOG_BATCH_SIZE = 100;
19
-
20
18
  /**
21
19
  * Record stats about work execution. Intended to be queried by Axiom or Datadog.
22
20
  * See the [README](https://github.com/get-convex/workpool) for example queries.
@@ -114,36 +112,15 @@ export const calculateBacklogAndReport = internalMutation({
114
112
  logLevel,
115
113
  },
116
114
  handler: async (ctx, args) => {
117
- const pendingStart = await paginator(ctx.db, schema)
118
- .query("pendingStart")
119
- .withIndex("segment", (q) =>
120
- q.gte("segment", args.startSegment).lt("segment", args.endSegment)
121
- )
122
- .paginate({
123
- numItems: BACKLOG_BATCH_SIZE,
124
- cursor: args.cursor,
125
- });
115
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
116
+ const pendingStart = await (ctx.db.query("pendingStart") as any).count();
117
+
126
118
  const console = createLogger(args.logLevel);
127
- if (pendingStart.isDone) {
128
- recordReport(console, {
129
- ...args.report,
130
- running: args.running,
131
- backlog: pendingStart.page.length,
132
- });
133
- } else {
134
- await ctx.scheduler.runAfter(
135
- 0,
136
- internal.stats.calculateBacklogAndReport,
137
- {
138
- startSegment: args.startSegment,
139
- endSegment: args.endSegment,
140
- cursor: pendingStart.continueCursor,
141
- report: args.report,
142
- running: args.running,
143
- logLevel: args.logLevel,
144
- }
145
- );
146
- }
119
+ recordReport(console, {
120
+ ...args.report,
121
+ running: args.running,
122
+ backlog: pendingStart,
123
+ });
147
124
  },
148
125
  });
149
126