@convex-dev/workpool 0.1.2 → 0.2.0-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.
Files changed (125) hide show
  1. package/README.md +155 -17
  2. package/dist/commonjs/client/index.d.ts +123 -35
  3. package/dist/commonjs/client/index.d.ts.map +1 -1
  4. package/dist/commonjs/client/index.js +122 -15
  5. package/dist/commonjs/client/index.js.map +1 -1
  6. package/dist/commonjs/client/utils.d.ts +16 -0
  7. package/dist/commonjs/client/utils.d.ts.map +1 -0
  8. package/dist/commonjs/client/utils.js +2 -0
  9. package/dist/commonjs/client/utils.js.map +1 -0
  10. package/dist/commonjs/component/complete.d.ts +89 -0
  11. package/dist/commonjs/component/complete.d.ts.map +1 -0
  12. package/dist/commonjs/component/complete.js +80 -0
  13. package/dist/commonjs/component/complete.js.map +1 -0
  14. package/dist/commonjs/component/convex.config.d.ts.map +1 -1
  15. package/dist/commonjs/component/convex.config.js +0 -2
  16. package/dist/commonjs/component/convex.config.js.map +1 -1
  17. package/dist/commonjs/component/kick.d.ts +9 -0
  18. package/dist/commonjs/component/kick.d.ts.map +1 -0
  19. package/dist/commonjs/component/kick.js +97 -0
  20. package/dist/commonjs/component/kick.js.map +1 -0
  21. package/dist/commonjs/component/lib.d.ts +23 -32
  22. package/dist/commonjs/component/lib.d.ts.map +1 -1
  23. package/dist/commonjs/component/lib.js +91 -563
  24. package/dist/commonjs/component/lib.js.map +1 -1
  25. package/dist/commonjs/component/logging.d.ts +5 -3
  26. package/dist/commonjs/component/logging.d.ts.map +1 -1
  27. package/dist/commonjs/component/logging.js +13 -2
  28. package/dist/commonjs/component/logging.js.map +1 -1
  29. package/dist/commonjs/component/loop.d.ts +13 -0
  30. package/dist/commonjs/component/loop.d.ts.map +1 -0
  31. package/dist/commonjs/component/loop.js +482 -0
  32. package/dist/commonjs/component/loop.js.map +1 -0
  33. package/dist/commonjs/component/recovery.d.ts +24 -0
  34. package/dist/commonjs/component/recovery.d.ts.map +1 -0
  35. package/dist/commonjs/component/recovery.js +94 -0
  36. package/dist/commonjs/component/recovery.js.map +1 -0
  37. package/dist/commonjs/component/schema.d.ts +167 -93
  38. package/dist/commonjs/component/schema.d.ts.map +1 -1
  39. package/dist/commonjs/component/schema.js +56 -65
  40. package/dist/commonjs/component/schema.js.map +1 -1
  41. package/dist/commonjs/component/shared.d.ts +138 -0
  42. package/dist/commonjs/component/shared.d.ts.map +1 -0
  43. package/dist/commonjs/component/shared.js +77 -0
  44. package/dist/commonjs/component/shared.js.map +1 -0
  45. package/dist/commonjs/component/stats.d.ts +6 -3
  46. package/dist/commonjs/component/stats.d.ts.map +1 -1
  47. package/dist/commonjs/component/stats.js +23 -4
  48. package/dist/commonjs/component/stats.js.map +1 -1
  49. package/dist/commonjs/component/worker.d.ts +15 -0
  50. package/dist/commonjs/component/worker.d.ts.map +1 -0
  51. package/dist/commonjs/component/worker.js +73 -0
  52. package/dist/commonjs/component/worker.js.map +1 -0
  53. package/dist/esm/client/index.d.ts +123 -35
  54. package/dist/esm/client/index.d.ts.map +1 -1
  55. package/dist/esm/client/index.js +122 -15
  56. package/dist/esm/client/index.js.map +1 -1
  57. package/dist/esm/client/utils.d.ts +16 -0
  58. package/dist/esm/client/utils.d.ts.map +1 -0
  59. package/dist/esm/client/utils.js +2 -0
  60. package/dist/esm/client/utils.js.map +1 -0
  61. package/dist/esm/component/complete.d.ts +89 -0
  62. package/dist/esm/component/complete.d.ts.map +1 -0
  63. package/dist/esm/component/complete.js +80 -0
  64. package/dist/esm/component/complete.js.map +1 -0
  65. package/dist/esm/component/convex.config.d.ts.map +1 -1
  66. package/dist/esm/component/convex.config.js +0 -2
  67. package/dist/esm/component/convex.config.js.map +1 -1
  68. package/dist/esm/component/kick.d.ts +9 -0
  69. package/dist/esm/component/kick.d.ts.map +1 -0
  70. package/dist/esm/component/kick.js +97 -0
  71. package/dist/esm/component/kick.js.map +1 -0
  72. package/dist/esm/component/lib.d.ts +23 -32
  73. package/dist/esm/component/lib.d.ts.map +1 -1
  74. package/dist/esm/component/lib.js +91 -563
  75. package/dist/esm/component/lib.js.map +1 -1
  76. package/dist/esm/component/logging.d.ts +5 -3
  77. package/dist/esm/component/logging.d.ts.map +1 -1
  78. package/dist/esm/component/logging.js +13 -2
  79. package/dist/esm/component/logging.js.map +1 -1
  80. package/dist/esm/component/loop.d.ts +13 -0
  81. package/dist/esm/component/loop.d.ts.map +1 -0
  82. package/dist/esm/component/loop.js +482 -0
  83. package/dist/esm/component/loop.js.map +1 -0
  84. package/dist/esm/component/recovery.d.ts +24 -0
  85. package/dist/esm/component/recovery.d.ts.map +1 -0
  86. package/dist/esm/component/recovery.js +94 -0
  87. package/dist/esm/component/recovery.js.map +1 -0
  88. package/dist/esm/component/schema.d.ts +167 -93
  89. package/dist/esm/component/schema.d.ts.map +1 -1
  90. package/dist/esm/component/schema.js +56 -65
  91. package/dist/esm/component/schema.js.map +1 -1
  92. package/dist/esm/component/shared.d.ts +138 -0
  93. package/dist/esm/component/shared.d.ts.map +1 -0
  94. package/dist/esm/component/shared.js +77 -0
  95. package/dist/esm/component/shared.js.map +1 -0
  96. package/dist/esm/component/stats.d.ts +6 -3
  97. package/dist/esm/component/stats.d.ts.map +1 -1
  98. package/dist/esm/component/stats.js +23 -4
  99. package/dist/esm/component/stats.js.map +1 -1
  100. package/dist/esm/component/worker.d.ts +15 -0
  101. package/dist/esm/component/worker.d.ts.map +1 -0
  102. package/dist/esm/component/worker.js +73 -0
  103. package/dist/esm/component/worker.js.map +1 -0
  104. package/package.json +6 -5
  105. package/src/client/index.ts +232 -68
  106. package/src/client/utils.ts +45 -0
  107. package/src/component/README.md +73 -0
  108. package/src/component/_generated/api.d.ts +38 -66
  109. package/src/component/complete.test.ts +508 -0
  110. package/src/component/complete.ts +98 -0
  111. package/src/component/convex.config.ts +0 -3
  112. package/src/component/kick.test.ts +285 -0
  113. package/src/component/kick.ts +118 -0
  114. package/src/component/lib.test.ts +448 -0
  115. package/src/component/lib.ts +105 -667
  116. package/src/component/logging.ts +24 -12
  117. package/src/component/loop.test.ts +1204 -0
  118. package/src/component/loop.ts +637 -0
  119. package/src/component/recovery.test.ts +541 -0
  120. package/src/component/recovery.ts +96 -0
  121. package/src/component/schema.ts +61 -77
  122. package/src/component/setup.test.ts +5 -0
  123. package/src/component/shared.ts +141 -0
  124. package/src/component/stats.ts +26 -8
  125. package/src/component/worker.ts +81 -0
@@ -1,121 +1,285 @@
1
1
  import {
2
2
  createFunctionHandle,
3
3
  DefaultFunctionArgs,
4
- Expand,
5
4
  FunctionReference,
6
5
  FunctionVisibility,
7
- GenericDataModel,
8
- GenericMutationCtx,
9
- GenericQueryCtx,
10
6
  getFunctionName,
11
7
  } from "convex/server";
12
- import { GenericId } from "convex/values";
13
- import { api } from "../component/_generated/api";
14
- import { LogLevel } from "../component/logging";
15
- import { completionStatus, type CompletionStatus } from "../component/schema";
16
- export { completionStatus, type CompletionStatus };
8
+ import { v, VString } from "convex/values";
9
+ import { Mounts } from "../component/_generated/api.js";
10
+ import {
11
+ OnComplete,
12
+ runResult as runResultValidator,
13
+ RunResult,
14
+ type RetryBehavior,
15
+ OnCompleteArgs as SharedOnCompleteArgs,
16
+ Status,
17
+ Config,
18
+ } from "../component/shared.js";
19
+ import { type LogLevel, logLevel } from "../component/logging.js";
20
+ 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 };
17
24
 
18
- export type WorkId = string;
25
+ // Attempts will run with delay [0, 250, 500, 1000, 2000] (ms)
26
+ export const DEFAULT_RETRY_BEHAVIOR: RetryBehavior = {
27
+ maxAttempts: 5,
28
+ initialBackoffMs: 250,
29
+ base: 2,
30
+ };
31
+ export type WorkId = string & { __isWorkId: true };
32
+ export const workIdValidator = v.string() as VString<WorkId>;
19
33
 
20
34
  export class Workpool {
35
+ /**
36
+ * Initializes a Workpool.
37
+ *
38
+ * Note: if you want different pools, you need to *create different instances*
39
+ * of Workpool in convex.config.ts. It isn't sufficient to have different
40
+ * instances of this class.
41
+ *
42
+ * @param component - The component to use, like `components.workpool` from
43
+ * `./_generated/api.ts`.
44
+ * @param options - The options for the Workpool.
45
+ */
21
46
  constructor(
22
- private component: UseApi<typeof api>,
47
+ private component: UseApi<Mounts>, // UseApi<api> for jump to definition
23
48
  private options: {
24
49
  /** How many actions/mutations can be running at once within this pool.
25
50
  * Min 1, Max 300.
26
51
  */
27
- maxParallelism: number;
28
- /** How much to log.
29
- * Default WARN.
52
+ maxParallelism?: number;
53
+ /** How much to log. This is updated on each call to `enqueue*`,
54
+ * `status`, or `cancel*`.
55
+ * Default is WARN.
30
56
  * With INFO, you can see events for started and completed work, which can
31
- * be parsed.
57
+ * be parsed by tools like [Axiom](https://axiom.co) for monitoring.
32
58
  * With DEBUG, you can see timers and internal events for work being
33
59
  * scheduled.
34
60
  */
35
61
  logLevel?: LogLevel;
36
- /** How long to keep completed work in the database, for access by `status`.
37
- * Default 1 day.
62
+ /** Default retry behavior for enqueued actions. */
63
+ defaultRetryBehavior?: RetryBehavior;
64
+ /** Whether to retry actions that fail by default. Default: false.
65
+ * NOTE: Only do this if your actions are idempotent.
66
+ * See the docs (README.md) for more details.
38
67
  */
39
- statusTtl?: number;
68
+ retryActionsByDefault?: boolean;
40
69
  }
41
70
  ) {}
71
+ /**
72
+ * Enqueues an action to be run.
73
+ *
74
+ * @param ctx - The mutation or action context that can call ctx.runMutation.
75
+ * @param fn - The action to run, like `internal.example.myAction`.
76
+ * @param fnArgs - The arguments to pass to the action.
77
+ * @param options - The options for the action to specify retry behavior,
78
+ * onComplete handling, and scheduling via `runAt` or `runAfter`.
79
+ * @returns The ID of the work that was enqueued.
80
+ */
42
81
  async enqueueAction<Args extends DefaultFunctionArgs, ReturnType>(
43
82
  ctx: RunMutationCtx,
44
83
  fn: FunctionReference<"action", FunctionVisibility, Args, ReturnType>,
45
- fnArgs: Args
84
+ fnArgs: Args,
85
+ options?: {
86
+ /** Whether to retry the action if it fails.
87
+ * If true, it will use the default retry behavior.
88
+ * If custom behavior is provided, it will retry using that behavior.
89
+ * If unset, it will use the Workpool's configured default.
90
+ */
91
+ retry?: boolean | RetryBehavior;
92
+ } & CallbackOptions &
93
+ SchedulerOptions
46
94
  ): Promise<WorkId> {
47
- const fnHandle = await createFunctionHandle(fn);
95
+ const retryBehavior = getRetryBehavior(
96
+ this.options.defaultRetryBehavior,
97
+ this.options.retryActionsByDefault,
98
+ options?.retry
99
+ );
100
+ const onComplete: OnComplete | undefined = options?.onComplete
101
+ ? {
102
+ fnHandle: await createFunctionHandle(options.onComplete),
103
+ context: options.context,
104
+ }
105
+ : undefined;
48
106
  const id = await ctx.runMutation(this.component.lib.enqueue, {
49
- fnHandle,
50
- fnName: getFunctionName(fn),
107
+ ...(await defaultEnqueueArgs(fn, this.options)),
51
108
  fnArgs,
52
109
  fnType: "action",
53
- options: this.options,
110
+ runAt: getRunAt(options),
111
+ onComplete,
112
+ retryBehavior,
54
113
  });
55
114
  return id as WorkId;
56
115
  }
116
+
117
+ /**
118
+ * Enqueues a mutation to be run.
119
+ *
120
+ * Note: mutations are not retried by the workpool. Convex automatically
121
+ * retries them on database conflicts and transient failures.
122
+ * Because they're deterministic, external retries don't provide any benefit.
123
+ *
124
+ * @param ctx - The mutation or action context that can call ctx.runMutation.
125
+ * @param fn - The mutation to run, like `internal.example.myMutation`.
126
+ * @param fnArgs - The arguments to pass to the mutation.
127
+ * @param options - The options for the mutation to specify onComplete handling
128
+ * and scheduling via `runAt` or `runAfter`.
129
+ */
57
130
  async enqueueMutation<Args extends DefaultFunctionArgs, ReturnType>(
58
131
  ctx: RunMutationCtx,
59
132
  fn: FunctionReference<"mutation", FunctionVisibility, Args, ReturnType>,
60
- fnArgs: Args
133
+ fnArgs: Args,
134
+ options?: CallbackOptions & SchedulerOptions
61
135
  ): Promise<WorkId> {
62
- const fnHandle = await createFunctionHandle(fn);
63
136
  const id = await ctx.runMutation(this.component.lib.enqueue, {
64
- fnHandle,
65
- fnName: getFunctionName(fn),
137
+ ...(await defaultEnqueueArgs(fn, this.options)),
66
138
  fnArgs,
67
139
  fnType: "mutation",
68
- options: this.options,
140
+ runAt: getRunAt(options),
69
141
  });
70
142
  return id as WorkId;
71
143
  }
72
144
  async cancel(ctx: RunMutationCtx, id: WorkId): Promise<void> {
73
- await ctx.runMutation(this.component.lib.cancel, { id });
145
+ await ctx.runMutation(this.component.lib.cancel, {
146
+ id,
147
+ logLevel: this.options.logLevel ?? getDefaultLogLevel(),
148
+ });
149
+ }
150
+ async cancelAll(ctx: RunMutationCtx): Promise<void> {
151
+ await ctx.runMutation(this.component.lib.cancelAll, {
152
+ logLevel: this.options.logLevel ?? getDefaultLogLevel(),
153
+ });
74
154
  }
75
- async status(
76
- ctx: RunQueryCtx,
77
- id: WorkId
78
- ): Promise<
79
- | { kind: "pending" }
80
- | { kind: "inProgress" }
81
- | { kind: "completed"; completionStatus: CompletionStatus }
82
- > {
83
- return await ctx.runQuery(this.component.lib.status, { id });
155
+ async status(ctx: RunQueryCtx, id: WorkId): Promise<Status> {
156
+ return ctx.runQuery(this.component.lib.status, { id });
84
157
  }
85
158
  }
86
159
 
87
- /* Type utils follow */
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
+ }
88
175
 
89
- type RunQueryCtx = {
90
- runQuery: GenericQueryCtx<GenericDataModel>["runQuery"];
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
+ export type SchedulerOptions =
191
+ | {
192
+ /**
193
+ * The time (ms since epoch) to run the action at.
194
+ * If not provided, the action will be run as soon as possible.
195
+ * Note: this is advisory only. It may run later.
196
+ */
197
+ runAt?: number;
198
+ }
199
+ | {
200
+ /**
201
+ * The number of milliseconds to run the action after.
202
+ * If not provided, the action will be run as soon as possible.
203
+ * Note: this is advisory only. It may run later.
204
+ */
205
+ runAfter?: number;
206
+ };
207
+
208
+ export type CallbackOptions = {
209
+ /**
210
+ * A mutation to run after the function succeeds, fails, or is canceled.
211
+ * The context type is for your use, feel free to provide a validator for it.
212
+ * e.g.
213
+ * ```ts
214
+ * export const completion = internalMutation({
215
+ * args: {
216
+ * workId: workIdValidator,
217
+ * context: v.any(),
218
+ * result: runResult,
219
+ * },
220
+ * handler: async (ctx, args) => {
221
+ * console.log(args.result, "Got Context back -> ", args.context, Date.now() - args.context);
222
+ * },
223
+ * });
224
+ * ```
225
+ */
226
+ onComplete?: FunctionReference<
227
+ "mutation",
228
+ FunctionVisibility,
229
+ OnCompleteArgs
230
+ > | null;
231
+
232
+ /**
233
+ * A context object to pass to the `onComplete` mutation.
234
+ * Useful for passing data from the enqueue site to the onComplete site.
235
+ */
236
+ context?: unknown;
91
237
  };
92
- type RunMutationCtx = {
93
- runMutation: GenericMutationCtx<GenericDataModel>["runMutation"];
238
+
239
+ export type OnCompleteArgs = {
240
+ /**
241
+ * The ID of the work that completed.
242
+ */
243
+ workId: WorkId;
244
+ /**
245
+ * The context object passed when enqueuing the work.
246
+ * Useful for passing data from the enqueue site to the onComplete site.
247
+ */
248
+ context: unknown;
249
+ /**
250
+ * The result of the run that completed.
251
+ */
252
+ result: RunResult;
94
253
  };
254
+ // ensure OnCompleteArgs satisfies SharedOnCompleteArgs
255
+ const _ = {} as OnCompleteArgs satisfies SharedOnCompleteArgs;
95
256
 
96
- export type OpaqueIds<T> =
97
- T extends GenericId<infer _T>
98
- ? string
99
- : T extends (infer U)[]
100
- ? OpaqueIds<U>[]
101
- : T extends object
102
- ? { [K in keyof T]: OpaqueIds<T[K]> }
103
- : T;
257
+ function getRunAt(options?: SchedulerOptions): number {
258
+ if (!options) {
259
+ return Date.now();
260
+ }
261
+ if ("runAt" in options && options.runAt !== undefined) {
262
+ return options.runAt;
263
+ }
264
+ if ("runAfter" in options && options.runAfter !== undefined) {
265
+ return Date.now() + options.runAfter;
266
+ }
267
+ return Date.now();
268
+ }
104
269
 
105
- export type UseApi<API> = Expand<{
106
- [mod in keyof API]: API[mod] extends FunctionReference<
107
- infer FType,
108
- "public",
109
- infer FArgs,
110
- infer FReturnType,
111
- infer FComponentPath
112
- >
113
- ? FunctionReference<
114
- FType,
115
- "internal",
116
- OpaqueIds<FArgs>,
117
- OpaqueIds<FReturnType>,
118
- FComponentPath
119
- >
120
- : UseApi<API[mod]>;
121
- }>;
270
+ function getDefaultLogLevel(): LogLevel {
271
+ if (process.env.WORKPOOL_LOG_LEVEL) {
272
+ if (
273
+ !logLevel.members
274
+ .map((m) => m.value as string)
275
+ .includes(process.env.WORKPOOL_LOG_LEVEL)
276
+ ) {
277
+ console.warn(
278
+ `Invalid log level (${process.env.WORKPOOL_LOG_LEVEL}), defaulting to "INFO"`
279
+ );
280
+ } else {
281
+ return process.env.WORKPOOL_LOG_LEVEL as LogLevel;
282
+ }
283
+ }
284
+ return DEFAULT_LOG_LEVEL;
285
+ }
@@ -0,0 +1,45 @@
1
+ import {
2
+ Expand,
3
+ FunctionReference,
4
+ GenericMutationCtx,
5
+ GenericQueryCtx,
6
+ } from "convex/server";
7
+ import { GenericId } from "convex/values";
8
+
9
+ import { GenericDataModel } from "convex/server";
10
+
11
+ /* Type utils follow */
12
+
13
+ export type RunQueryCtx = {
14
+ runQuery: GenericQueryCtx<GenericDataModel>["runQuery"];
15
+ };
16
+ export type RunMutationCtx = {
17
+ runMutation: GenericMutationCtx<GenericDataModel>["runMutation"];
18
+ };
19
+
20
+ export type OpaqueIds<T> =
21
+ T extends GenericId<infer _T>
22
+ ? string
23
+ : T extends (infer U)[]
24
+ ? OpaqueIds<U>[]
25
+ : T extends object
26
+ ? { [K in keyof T]: OpaqueIds<T[K]> }
27
+ : T;
28
+
29
+ export type UseApi<API> = Expand<{
30
+ [mod in keyof API]: API[mod] extends FunctionReference<
31
+ infer FType,
32
+ "public",
33
+ infer FArgs,
34
+ infer FReturnType,
35
+ infer FComponentPath
36
+ >
37
+ ? FunctionReference<
38
+ FType,
39
+ "internal",
40
+ OpaqueIds<FArgs>,
41
+ OpaqueIds<FReturnType>,
42
+ FComponentPath
43
+ >
44
+ : UseApi<API[mod]>;
45
+ }>;
@@ -0,0 +1,73 @@
1
+ # Workpool: implementation notes and high-level architecture
2
+
3
+ Concepts:
4
+
5
+ - `segment`: A slice of time to process work. All work is bucketed into one.
6
+ This enables us to batch work and avoid database conflicts.
7
+ - `generation`: A monotonically increasing counter to ensure the loop is
8
+ only running one instance. If two loops start with the same generation,
9
+ one will successfully increase it, the other will retry and find that the
10
+ generation has changed and fail out.
11
+ - "Retention" is used to refer to situations where a query might have to read
12
+ over a lot of "tombstones" - deleted data that hasn't been vacuumed from the
13
+ underlying database yet. If there are frequent deletions, scanning across them
14
+ can delay a query. Because of our delete-heavy queuing strategy, we have to be
15
+ careful. Strategies are below.
16
+ - Cursors: A pointer to the last processed place in a table. In our case, they
17
+ might allow data to be written before them if out-of-order writes happen, so
18
+ we need to account for finding those "missed" writes on some granularity. We
19
+ choose to wait until there isn't any immediate work to do before those scans.
20
+ They help avoid retention issues.
21
+
22
+ ## Data state machine
23
+
24
+ ```mermaid
25
+ flowchart LR
26
+ Client -->|enqueue| pendingStart
27
+ Client -->|cancel| pendingCancelation
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
+ ```
37
+
38
+ Notably:
39
+
40
+ - The pending\* states are written by outside sources.
41
+ - The main loop federates changes to/from "running"
42
+ - Canceling only impacts pending and retrying jobs.
43
+
44
+ ## Loop state machine
45
+
46
+ ```mermaid
47
+ flowchart TD
48
+ idle -->|enqueue| running
49
+ running-->|"all started, leftover capacity"| scheduled
50
+ scheduled -->|"enqueue, cancel, saveResult, recovery"| running
51
+ running -->|"maxed out"| saturated
52
+ saturated -->|"cancel, saveResult, recovery"| running
53
+ running-->|"all done"| idle
54
+ ```
55
+
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
+
63
+ ## Retention optimization strategy
64
+
65
+ - Producers (Client, Worker, Recovery) write to a future "segment".
66
+ - Consumers (main) read the current segment.
67
+ - On conflicts, producers will write to progressively higher segments, while
68
+ the main loop will continue to read the segment originally called with.
69
+ This means conflicts are less likely on each retry.
70
+ - Patch singletons to avoid tombstones.
71
+ - Use segements & cursors to bound reads to latest data.
72
+ - Do scans outside of the critical path (during load).
73
+ - Do point reads otherwise.
@@ -8,9 +8,15 @@
8
8
  * @module
9
9
  */
10
10
 
11
+ import type * as complete from "../complete.js";
12
+ import type * as kick from "../kick.js";
11
13
  import type * as lib from "../lib.js";
12
14
  import type * as logging from "../logging.js";
15
+ import type * as loop from "../loop.js";
16
+ import type * as recovery from "../recovery.js";
17
+ import type * as shared from "../shared.js";
13
18
  import type * as stats from "../stats.js";
19
+ import type * as worker from "../worker.js";
14
20
 
15
21
  import type {
16
22
  ApiFromModules,
@@ -26,27 +32,49 @@ import type {
26
32
  * ```
27
33
  */
28
34
  declare const fullApi: ApiFromModules<{
35
+ complete: typeof complete;
36
+ kick: typeof kick;
29
37
  lib: typeof lib;
30
38
  logging: typeof logging;
39
+ loop: typeof loop;
40
+ recovery: typeof recovery;
41
+ shared: typeof shared;
31
42
  stats: typeof stats;
43
+ worker: typeof worker;
32
44
  }>;
33
45
  export type Mounts = {
34
46
  lib: {
35
- cancel: FunctionReference<"mutation", "public", { id: string }, any>;
36
- cleanup: FunctionReference<"mutation", "public", { maxAgeMs: number }, any>;
47
+ cancel: FunctionReference<
48
+ "mutation",
49
+ "public",
50
+ { id: string; logLevel: "DEBUG" | "INFO" | "WARN" | "ERROR" },
51
+ any
52
+ >;
53
+ cancelAll: FunctionReference<
54
+ "mutation",
55
+ "public",
56
+ { before?: number; logLevel: "DEBUG" | "INFO" | "WARN" | "ERROR" },
57
+ any
58
+ >;
37
59
  enqueue: FunctionReference<
38
60
  "mutation",
39
61
  "public",
40
62
  {
63
+ config: {
64
+ logLevel: "DEBUG" | "INFO" | "WARN" | "ERROR";
65
+ maxParallelism: number;
66
+ };
41
67
  fnArgs: any;
42
68
  fnHandle: string;
43
69
  fnName: string;
44
70
  fnType: "action" | "mutation";
45
- options: {
46
- logLevel?: "DEBUG" | "INFO" | "WARN" | "ERROR";
47
- maxParallelism: number;
48
- statusTtl?: number;
71
+ onComplete?: { context?: any; fnHandle: string };
72
+ retryBehavior?: {
73
+ base: number;
74
+ initialBackoffMs: number;
75
+ maxAttempts: number;
49
76
  };
77
+ runAt: number;
50
78
  },
51
79
  string
52
80
  >;
@@ -54,14 +82,10 @@ export type Mounts = {
54
82
  "query",
55
83
  "public",
56
84
  { id: string },
57
- | { kind: "pending" }
58
- | { kind: "inProgress" }
59
- | {
60
- completionStatus: "success" | "error" | "canceled" | "timeout";
61
- kind: "completed";
62
- }
85
+ | { previousAttempts: number; state: "pending" }
86
+ | { previousAttempts: number; state: "running" }
87
+ | { state: "finished" }
63
88
  >;
64
- stopCleanup: FunctionReference<"mutation", "public", {}, any>;
65
89
  };
66
90
  };
67
91
  // For now fullApiWithMounts is only fullApi which provides
@@ -78,56 +102,4 @@ export declare const internal: FilterApi<
78
102
  FunctionReference<any, "internal">
79
103
  >;
80
104
 
81
- export declare const components: {
82
- crons: {
83
- public: {
84
- del: FunctionReference<
85
- "mutation",
86
- "internal",
87
- { identifier: { id: string } | { name: string } },
88
- null
89
- >;
90
- get: FunctionReference<
91
- "query",
92
- "internal",
93
- { identifier: { id: string } | { name: string } },
94
- {
95
- args: Record<string, any>;
96
- functionHandle: string;
97
- id: string;
98
- name?: string;
99
- schedule:
100
- | { kind: "interval"; ms: number }
101
- | { cronspec: string; kind: "cron" };
102
- } | null
103
- >;
104
- list: FunctionReference<
105
- "query",
106
- "internal",
107
- {},
108
- Array<{
109
- args: Record<string, any>;
110
- functionHandle: string;
111
- id: string;
112
- name?: string;
113
- schedule:
114
- | { kind: "interval"; ms: number }
115
- | { cronspec: string; kind: "cron" };
116
- }>
117
- >;
118
- register: FunctionReference<
119
- "mutation",
120
- "internal",
121
- {
122
- args: Record<string, any>;
123
- functionHandle: string;
124
- name?: string;
125
- schedule:
126
- | { kind: "interval"; ms: number }
127
- | { cronspec: string; kind: "cron" };
128
- },
129
- string
130
- >;
131
- };
132
- };
133
- };
105
+ export declare const components: {};