@convex-dev/workpool 0.2.0-beta.0 → 0.2.1

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 (110) hide show
  1. package/README.md +87 -18
  2. package/dist/commonjs/client/index.d.ts +33 -8
  3. package/dist/commonjs/client/index.d.ts.map +1 -1
  4. package/dist/commonjs/client/index.js +37 -7
  5. package/dist/commonjs/client/index.js.map +1 -1
  6. package/dist/commonjs/component/complete.d.ts +89 -0
  7. package/dist/commonjs/component/complete.d.ts.map +1 -0
  8. package/dist/commonjs/component/complete.js +82 -0
  9. package/dist/commonjs/component/complete.js.map +1 -0
  10. package/dist/commonjs/component/kick.d.ts +3 -3
  11. package/dist/commonjs/component/kick.d.ts.map +1 -1
  12. package/dist/commonjs/component/kick.js +17 -12
  13. package/dist/commonjs/component/kick.js.map +1 -1
  14. package/dist/commonjs/component/lib.d.ts +6 -6
  15. package/dist/commonjs/component/lib.d.ts.map +1 -1
  16. package/dist/commonjs/component/lib.js +53 -24
  17. package/dist/commonjs/component/lib.js.map +1 -1
  18. package/dist/commonjs/component/logging.d.ts +3 -2
  19. package/dist/commonjs/component/logging.d.ts.map +1 -1
  20. package/dist/commonjs/component/logging.js +34 -16
  21. package/dist/commonjs/component/logging.js.map +1 -1
  22. package/dist/commonjs/component/loop.d.ts +1 -14
  23. package/dist/commonjs/component/loop.d.ts.map +1 -1
  24. package/dist/commonjs/component/loop.js +216 -179
  25. package/dist/commonjs/component/loop.js.map +1 -1
  26. package/dist/commonjs/component/recovery.d.ts +45 -0
  27. package/dist/commonjs/component/recovery.d.ts.map +1 -1
  28. package/dist/commonjs/component/recovery.js +88 -65
  29. package/dist/commonjs/component/recovery.js.map +1 -1
  30. package/dist/commonjs/component/schema.d.ts +17 -13
  31. package/dist/commonjs/component/schema.d.ts.map +1 -1
  32. package/dist/commonjs/component/schema.js +5 -3
  33. package/dist/commonjs/component/schema.js.map +1 -1
  34. package/dist/commonjs/component/shared.d.ts +24 -15
  35. package/dist/commonjs/component/shared.d.ts.map +1 -1
  36. package/dist/commonjs/component/shared.js +20 -7
  37. package/dist/commonjs/component/shared.js.map +1 -1
  38. package/dist/commonjs/component/stats.d.ts +36 -29
  39. package/dist/commonjs/component/stats.d.ts.map +1 -1
  40. package/dist/commonjs/component/stats.js +110 -52
  41. package/dist/commonjs/component/stats.js.map +1 -1
  42. package/dist/commonjs/component/worker.d.ts +4 -14
  43. package/dist/commonjs/component/worker.d.ts.map +1 -1
  44. package/dist/commonjs/component/worker.js +23 -36
  45. package/dist/commonjs/component/worker.js.map +1 -1
  46. package/dist/esm/client/index.d.ts +33 -8
  47. package/dist/esm/client/index.d.ts.map +1 -1
  48. package/dist/esm/client/index.js +37 -7
  49. package/dist/esm/client/index.js.map +1 -1
  50. package/dist/esm/component/complete.d.ts +89 -0
  51. package/dist/esm/component/complete.d.ts.map +1 -0
  52. package/dist/esm/component/complete.js +82 -0
  53. package/dist/esm/component/complete.js.map +1 -0
  54. package/dist/esm/component/kick.d.ts +3 -3
  55. package/dist/esm/component/kick.d.ts.map +1 -1
  56. package/dist/esm/component/kick.js +17 -12
  57. package/dist/esm/component/kick.js.map +1 -1
  58. package/dist/esm/component/lib.d.ts +6 -6
  59. package/dist/esm/component/lib.d.ts.map +1 -1
  60. package/dist/esm/component/lib.js +53 -24
  61. package/dist/esm/component/lib.js.map +1 -1
  62. package/dist/esm/component/logging.d.ts +3 -2
  63. package/dist/esm/component/logging.d.ts.map +1 -1
  64. package/dist/esm/component/logging.js +34 -16
  65. package/dist/esm/component/logging.js.map +1 -1
  66. package/dist/esm/component/loop.d.ts +1 -14
  67. package/dist/esm/component/loop.d.ts.map +1 -1
  68. package/dist/esm/component/loop.js +216 -179
  69. package/dist/esm/component/loop.js.map +1 -1
  70. package/dist/esm/component/recovery.d.ts +45 -0
  71. package/dist/esm/component/recovery.d.ts.map +1 -1
  72. package/dist/esm/component/recovery.js +88 -65
  73. package/dist/esm/component/recovery.js.map +1 -1
  74. package/dist/esm/component/schema.d.ts +17 -13
  75. package/dist/esm/component/schema.d.ts.map +1 -1
  76. package/dist/esm/component/schema.js +5 -3
  77. package/dist/esm/component/schema.js.map +1 -1
  78. package/dist/esm/component/shared.d.ts +24 -15
  79. package/dist/esm/component/shared.d.ts.map +1 -1
  80. package/dist/esm/component/shared.js +20 -7
  81. package/dist/esm/component/shared.js.map +1 -1
  82. package/dist/esm/component/stats.d.ts +36 -29
  83. package/dist/esm/component/stats.d.ts.map +1 -1
  84. package/dist/esm/component/stats.js +110 -52
  85. package/dist/esm/component/stats.js.map +1 -1
  86. package/dist/esm/component/worker.d.ts +4 -14
  87. package/dist/esm/component/worker.d.ts.map +1 -1
  88. package/dist/esm/component/worker.js +23 -36
  89. package/dist/esm/component/worker.js.map +1 -1
  90. package/package.json +12 -12
  91. package/src/client/index.ts +82 -43
  92. package/src/component/README.md +15 -15
  93. package/src/component/_generated/api.d.ts +10 -5
  94. package/src/component/complete.test.ts +508 -0
  95. package/src/component/complete.ts +109 -0
  96. package/src/component/kick.test.ts +29 -19
  97. package/src/component/kick.ts +25 -17
  98. package/src/component/lib.test.ts +262 -17
  99. package/src/component/lib.ts +68 -30
  100. package/src/component/logging.test.ts +16 -0
  101. package/src/component/logging.ts +45 -24
  102. package/src/component/loop.test.ts +1158 -0
  103. package/src/component/loop.ts +292 -224
  104. package/src/component/recovery.test.ts +536 -0
  105. package/src/component/recovery.ts +100 -75
  106. package/src/component/schema.ts +6 -4
  107. package/src/component/shared.ts +23 -8
  108. package/src/component/stats.test.ts +345 -0
  109. package/src/component/stats.ts +149 -56
  110. package/src/component/worker.ts +25 -38
@@ -8,19 +8,22 @@ import {
8
8
  import { v, VString } from "convex/values";
9
9
  import { Mounts } from "../component/_generated/api.js";
10
10
  import {
11
+ DEFAULT_LOG_LEVEL,
12
+ type LogLevel,
13
+ logLevel,
14
+ } from "../component/logging.js";
15
+ import {
16
+ Config,
17
+ DEFAULT_MAX_PARALLELISM,
11
18
  OnComplete,
12
- runResult as runResultValidator,
13
- RunResult,
19
+ runResult as resultValidator,
14
20
  type RetryBehavior,
21
+ RunResult,
15
22
  OnCompleteArgs as SharedOnCompleteArgs,
16
23
  Status,
17
- Config,
18
24
  } from "../component/shared.js";
19
- import { type LogLevel, logLevel } from "../component/logging.js";
20
25
  import { RunMutationCtx, RunQueryCtx, UseApi } from "./utils.js";
21
- import { DEFAULT_LOG_LEVEL } from "../component/logging.js";
22
- import { DEFAULT_MAX_PARALLELISM } from "../component/kick.js";
23
- export { runResultValidator, type RunResult };
26
+ export { resultValidator, type RunResult };
24
27
 
25
28
  // Attempts will run with delay [0, 250, 500, 1000, 2000] (ms)
26
29
  export const DEFAULT_RETRY_BEHAVIOR: RetryBehavior = {
@@ -52,17 +55,20 @@ export class Workpool {
52
55
  maxParallelism?: number;
53
56
  /** How much to log. This is updated on each call to `enqueue*`,
54
57
  * `status`, or `cancel*`.
55
- * Default is WARN.
56
- * With INFO, you can see events for started and completed work, which can
57
- * be parsed by tools like [Axiom](https://axiom.co) for monitoring.
58
+ * Default is REPORT, which logs warnings, errors, and a periodic report.
59
+ * With INFO, you can also see events for started and completed work.
60
+ * Stats generated can be parsed by tools like
61
+ * [Axiom](https://axiom.co) for monitoring.
58
62
  * With DEBUG, you can see timers and internal events for work being
59
63
  * scheduled.
60
64
  */
61
65
  logLevel?: LogLevel;
62
- /** Default retry behavior for enqueued actions. */
66
+ /** Default retry behavior for enqueued actions.
67
+ * See {@link RetryBehavior}.
68
+ */
63
69
  defaultRetryBehavior?: RetryBehavior;
64
70
  /** Whether to retry actions that fail by default. Default: false.
65
- * NOTE: Only do this if your actions are idempotent.
71
+ * NOTE: Only enable this if your actions are idempotent.
66
72
  * See the docs (README.md) for more details.
67
73
  */
68
74
  retryActionsByDefault?: boolean;
@@ -133,60 +139,59 @@ export class Workpool {
133
139
  fnArgs: Args,
134
140
  options?: CallbackOptions & SchedulerOptions
135
141
  ): Promise<WorkId> {
142
+ const onComplete: OnComplete | undefined = options?.onComplete
143
+ ? {
144
+ fnHandle: await createFunctionHandle(options.onComplete),
145
+ context: options.context,
146
+ }
147
+ : undefined;
136
148
  const id = await ctx.runMutation(this.component.lib.enqueue, {
137
149
  ...(await defaultEnqueueArgs(fn, this.options)),
138
150
  fnArgs,
139
151
  fnType: "mutation",
140
152
  runAt: getRunAt(options),
153
+ onComplete,
141
154
  });
142
155
  return id as WorkId;
143
156
  }
157
+ /**
158
+ * Cancels a work item. If it's already started, it will be allowed to finish
159
+ * but will not be retried.
160
+ *
161
+ * @param ctx - The mutation or action context that can call ctx.runMutation.
162
+ * @param id - The ID of the work to cancel.
163
+ */
144
164
  async cancel(ctx: RunMutationCtx, id: WorkId): Promise<void> {
145
165
  await ctx.runMutation(this.component.lib.cancel, {
146
166
  id,
147
167
  logLevel: this.options.logLevel ?? getDefaultLogLevel(),
148
168
  });
149
169
  }
170
+ /**
171
+ * Cancels all pending work items. See {@link cancel}.
172
+ *
173
+ * @param ctx - The mutation or action context that can call ctx.runMutation.
174
+ */
150
175
  async cancelAll(ctx: RunMutationCtx): Promise<void> {
151
176
  await ctx.runMutation(this.component.lib.cancelAll, {
152
177
  logLevel: this.options.logLevel ?? getDefaultLogLevel(),
153
178
  });
154
179
  }
180
+ /**
181
+ * Gets the status of a work item.
182
+ *
183
+ * @param ctx - The query context that can call ctx.runQuery.
184
+ * @param id - The ID of the work to get the status of.
185
+ * @returns The status of the work item. One of:
186
+ * - `{ state: "pending", previousAttempts: number }`
187
+ * - `{ state: "running", previousAttempts: number }`
188
+ * - `{ state: "finished" }`
189
+ */
155
190
  async status(ctx: RunQueryCtx, id: WorkId): Promise<Status> {
156
191
  return ctx.runQuery(this.component.lib.status, { id });
157
192
  }
158
193
  }
159
194
 
160
- function getRetryBehavior(
161
- defaultRetryBehavior: RetryBehavior | undefined,
162
- retryActionsByDefault: boolean | undefined,
163
- retryOverride: boolean | RetryBehavior | undefined
164
- ): RetryBehavior | undefined {
165
- const defaultRetry = defaultRetryBehavior ?? DEFAULT_RETRY_BEHAVIOR;
166
- const retryByDefault = retryActionsByDefault ?? false;
167
- if (retryOverride === true) {
168
- return defaultRetry;
169
- }
170
- if (retryOverride === false) {
171
- return undefined;
172
- }
173
- return retryOverride ?? (retryByDefault ? defaultRetry : undefined);
174
- }
175
-
176
- async function defaultEnqueueArgs(
177
- fn: FunctionReference<"action" | "mutation", FunctionVisibility>,
178
- { logLevel, maxParallelism }: Partial<Config>
179
- ) {
180
- return {
181
- fnHandle: await createFunctionHandle(fn),
182
- fnName: getFunctionName(fn),
183
- config: {
184
- logLevel: logLevel ?? getDefaultLogLevel(),
185
- maxParallelism: maxParallelism ?? DEFAULT_MAX_PARALLELISM,
186
- },
187
- };
188
- }
189
-
190
195
  export type SchedulerOptions =
191
196
  | {
192
197
  /**
@@ -215,7 +220,7 @@ export type CallbackOptions = {
215
220
  * args: {
216
221
  * workId: workIdValidator,
217
222
  * context: v.any(),
218
- * result: runResult,
223
+ * result: resultValidator,
219
224
  * },
220
225
  * handler: async (ctx, args) => {
221
226
  * console.log(args.result, "Got Context back -> ", args.context, Date.now() - args.context);
@@ -254,6 +259,40 @@ export type OnCompleteArgs = {
254
259
  // ensure OnCompleteArgs satisfies SharedOnCompleteArgs
255
260
  const _ = {} as OnCompleteArgs satisfies SharedOnCompleteArgs;
256
261
 
262
+ //
263
+ // Helper functions
264
+ //
265
+
266
+ function getRetryBehavior(
267
+ defaultRetryBehavior: RetryBehavior | undefined,
268
+ retryActionsByDefault: boolean | undefined,
269
+ retryOverride: boolean | RetryBehavior | undefined
270
+ ): RetryBehavior | undefined {
271
+ const defaultRetry = defaultRetryBehavior ?? DEFAULT_RETRY_BEHAVIOR;
272
+ const retryByDefault = retryActionsByDefault ?? false;
273
+ if (retryOverride === true) {
274
+ return defaultRetry;
275
+ }
276
+ if (retryOverride === false) {
277
+ return undefined;
278
+ }
279
+ return retryOverride ?? (retryByDefault ? defaultRetry : undefined);
280
+ }
281
+
282
+ async function defaultEnqueueArgs(
283
+ fn: FunctionReference<"action" | "mutation", FunctionVisibility>,
284
+ { logLevel, maxParallelism }: Partial<Config>
285
+ ) {
286
+ return {
287
+ fnHandle: await createFunctionHandle(fn),
288
+ fnName: getFunctionName(fn),
289
+ config: {
290
+ logLevel: logLevel ?? getDefaultLogLevel(),
291
+ maxParallelism: maxParallelism ?? DEFAULT_MAX_PARALLELISM,
292
+ },
293
+ };
294
+ }
295
+
257
296
  function getRunAt(options?: SchedulerOptions): number {
258
297
  if (!options) {
259
298
  return Date.now();
@@ -25,19 +25,19 @@ Concepts:
25
25
  flowchart LR
26
26
  Client -->|enqueue| pendingStart
27
27
  Client -->|cancel| pendingCancelation
28
- Recovery-->|recover| pendingCancelation
29
- Recovery-->|recover| pendingCompletion
30
- Worker-->|"saveResult"| pendingCompletion
31
- pendingStart -->|main| workerRunning["internalState.running"]
32
- workerRunning-->|"main(pendingCompletion)"| Retry{"Needs retry?"}
33
- Retry-->|"no / canceled"| complete
34
- Retry-->|yes| pendingStart
35
- pendingStart-->|"main(pendingCancelation)"| complete
28
+ complete --> |success or failure| pendingCompletion
29
+ pendingCompletion -->|retry| pendingStart
30
+ pendingStart --> workerRunning["worker running"]
31
+ workerRunning -->|worker finished| complete
32
+ workerRunning --> |recovery| complete
33
+ successfulCancel["AND"]@{shape: delay} --> |canceled| complete
34
+ pendingStart --> successfulCancel
35
+ pendingCancelation --> successfulCancel
36
36
  ```
37
37
 
38
38
  Notably:
39
39
 
40
- - The pending\* states are only written by other sources.
40
+ - The pending\* states are written by outside sources.
41
41
  - The main loop federates changes to/from "running"
42
42
  - Canceling only impacts pending and retrying jobs.
43
43
 
@@ -53,12 +53,12 @@ flowchart TD
53
53
  running-->|"all done"| idle
54
54
  ```
55
55
 
56
- - While the loop is running, clients won't see database conflicts with the
57
- state changing.
58
- - The "saturated" state is concretely "running" or "scheduled" with a boolean
59
- set, to avoid clients from kicking the main loop on enqueueing, which is
60
- unlikely to be productive, since the next action needs to be something
61
- terminating.
56
+ - While the loop is running, the runStatus doesn't change, making it safer to
57
+ read from clients without database conflicts.
58
+ - The "saturated" state is concretely "running" or "scheduled" at max
59
+ parallelism. There is a boolean set on "scheduled" to avoid clients from
60
+ kicking the main loop on enqueueing, which is unlikely to be productive, since
61
+ the next action needs to be something terminating.
62
62
 
63
63
  ## Retention optimization strategy
64
64
 
@@ -8,6 +8,7 @@
8
8
  * @module
9
9
  */
10
10
 
11
+ import type * as complete from "../complete.js";
11
12
  import type * as kick from "../kick.js";
12
13
  import type * as lib from "../lib.js";
13
14
  import type * as logging from "../logging.js";
@@ -31,6 +32,7 @@ import type {
31
32
  * ```
32
33
  */
33
34
  declare const fullApi: ApiFromModules<{
35
+ complete: typeof complete;
34
36
  kick: typeof kick;
35
37
  lib: typeof lib;
36
38
  logging: typeof logging;
@@ -45,13 +47,16 @@ export type Mounts = {
45
47
  cancel: FunctionReference<
46
48
  "mutation",
47
49
  "public",
48
- { id: string; logLevel: "DEBUG" | "INFO" | "WARN" | "ERROR" },
50
+ { id: string; logLevel: "DEBUG" | "INFO" | "REPORT" | "WARN" | "ERROR" },
49
51
  any
50
52
  >;
51
53
  cancelAll: FunctionReference<
52
54
  "mutation",
53
55
  "public",
54
- { before?: number; logLevel: "DEBUG" | "INFO" | "WARN" | "ERROR" },
56
+ {
57
+ before?: number;
58
+ logLevel: "DEBUG" | "INFO" | "REPORT" | "WARN" | "ERROR";
59
+ },
55
60
  any
56
61
  >;
57
62
  enqueue: FunctionReference<
@@ -59,7 +64,7 @@ export type Mounts = {
59
64
  "public",
60
65
  {
61
66
  config: {
62
- logLevel: "DEBUG" | "INFO" | "WARN" | "ERROR";
67
+ logLevel: "DEBUG" | "INFO" | "REPORT" | "WARN" | "ERROR";
63
68
  maxParallelism: number;
64
69
  };
65
70
  fnArgs: any;
@@ -80,8 +85,8 @@ export type Mounts = {
80
85
  "query",
81
86
  "public",
82
87
  { id: string },
83
- | { attempt: number; state: "pending" }
84
- | { attempt: number; state: "running" }
88
+ | { previousAttempts: number; state: "pending" }
89
+ | { previousAttempts: number; state: "running" }
85
90
  | { state: "finished" }
86
91
  >;
87
92
  };