@convex-dev/workpool 0.4.6 → 0.4.7-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 (49) hide show
  1. package/dist/component/_generated/api.d.ts +2 -0
  2. package/dist/component/_generated/api.d.ts.map +1 -1
  3. package/dist/component/_generated/api.js.map +1 -1
  4. package/dist/component/complete.d.ts.map +1 -1
  5. package/dist/component/complete.js +8 -7
  6. package/dist/component/complete.js.map +1 -1
  7. package/dist/component/danger.js +7 -7
  8. package/dist/component/danger.js.map +1 -1
  9. package/dist/component/future.d.ts +11 -0
  10. package/dist/component/future.d.ts.map +1 -0
  11. package/dist/component/future.js +21 -0
  12. package/dist/component/future.js.map +1 -0
  13. package/dist/component/kick.d.ts +3 -3
  14. package/dist/component/kick.d.ts.map +1 -1
  15. package/dist/component/kick.js +14 -16
  16. package/dist/component/kick.js.map +1 -1
  17. package/dist/component/lib.d.ts.map +1 -1
  18. package/dist/component/lib.js +13 -13
  19. package/dist/component/lib.js.map +1 -1
  20. package/dist/component/loop.d.ts +44 -1
  21. package/dist/component/loop.d.ts.map +1 -1
  22. package/dist/component/loop.js +171 -217
  23. package/dist/component/loop.js.map +1 -1
  24. package/dist/component/recovery.d.ts.map +1 -1
  25. package/dist/component/recovery.js +2 -2
  26. package/dist/component/recovery.js.map +1 -1
  27. package/dist/component/schema.d.ts.map +1 -1
  28. package/dist/component/schema.js +2 -1
  29. package/dist/component/schema.js.map +1 -1
  30. package/dist/component/worker.js +1 -1
  31. package/dist/component/worker.js.map +1 -1
  32. package/package.json +8 -12
  33. package/src/component/_generated/api.ts +2 -0
  34. package/src/component/complete.test.ts +13 -13
  35. package/src/component/complete.ts +13 -7
  36. package/src/component/danger.ts +7 -7
  37. package/src/component/future.ts +38 -0
  38. package/src/component/kick.test.ts +17 -20
  39. package/src/component/kick.ts +20 -17
  40. package/src/component/lib.test.ts +7 -7
  41. package/src/component/lib.ts +12 -15
  42. package/src/component/loop.test.ts +695 -1127
  43. package/src/component/loop.ts +212 -283
  44. package/src/component/recovery.test.ts +3 -3
  45. package/src/component/recovery.ts +5 -2
  46. package/src/component/schema.ts +2 -1
  47. package/src/component/stateMachine.test.ts +1246 -0
  48. package/src/component/stats.test.ts +4 -4
  49. package/src/component/worker.ts +1 -1
@@ -8,31 +8,29 @@ import {
8
8
  type Config,
9
9
  fromSegment,
10
10
  getCurrentSegment,
11
- getNextSegment,
12
11
  SECOND,
13
12
  toSegment,
14
13
  } from "./shared.js";
15
14
 
16
15
  /**
17
- * Called from outside the loop.
18
- * Returns the soonest segment to enqueue work for the main loop.
16
+ * Wakes the main loop if it isn't already running. No-ops when a wake-up
17
+ * wouldn't be productive (e.g. enqueue while saturated).
19
18
  */
20
19
  export async function kickMainLoop(
21
20
  ctx: MutationCtx,
22
21
  source: "enqueue" | "cancel" | "complete" | "kick",
23
22
  config?: Config,
24
- ): Promise<bigint> {
23
+ ): Promise<void> {
25
24
  const globals = config ?? (await getOrUpdateGlobals(ctx, config));
26
25
  const console = createLogger(globals.logLevel);
27
26
  const runStatus = await getOrCreateRunStatus(ctx);
28
- const next = getNextSegment();
29
27
 
30
28
  // Only kick to run now if we're scheduled or idle.
31
29
  if (runStatus.state.kind === "running") {
32
30
  console.debug(
33
31
  `[${source}] main is actively running, so we don't need to kick it`,
34
32
  );
35
- return next;
33
+ return;
36
34
  }
37
35
  // main is scheduled to run later, so we should cancel it and reschedule.
38
36
  if (runStatus.state.kind === "scheduled") {
@@ -40,24 +38,27 @@ export async function kickMainLoop(
40
38
  console.debug(
41
39
  `[${source}] main is saturated, so we don't need to kick it`,
42
40
  );
43
- return next;
41
+ return;
44
42
  }
45
43
  if (source === "complete" && !runStatus.state.saturated) {
46
44
  console.debug(
47
45
  `[${source}] main is not saturated, so kicking for completion isn't necessary`,
48
46
  );
49
- return next;
47
+ return;
50
48
  }
51
49
  if (runStatus.state.segment <= toSegment(Date.now() + SECOND)) {
52
50
  console.debug(
53
51
  `[${source}] main is scheduled to run soon enough, so we don't need to kick it`,
54
52
  );
55
- return next;
53
+ return;
56
54
  }
57
55
  console.debug(
58
56
  `[${source}] main is scheduled to run later, so reschedule it to run now`,
59
57
  );
60
- const scheduled = await ctx.db.system.get(runStatus.state.scheduledId);
58
+ const scheduled = await ctx.db.system.get(
59
+ "_scheduled_functions",
60
+ runStatus.state.scheduledId,
61
+ );
61
62
  if (scheduled && scheduled.state.kind === "pending") {
62
63
  await ctx.scheduler.cancel(runStatus.state.scheduledId);
63
64
  } else {
@@ -68,21 +69,23 @@ export async function kickMainLoop(
68
69
  } else if (runStatus.state.kind === "idle") {
69
70
  console.debug(`[${source}] main was idle, so run it now`);
70
71
  }
71
- await ctx.db.patch(runStatus._id, { state: { kind: "running" } });
72
- const current = getCurrentSegment();
73
- const scheduledTime = boundScheduledTime(fromSegment(current), console);
72
+ await ctx.db.patch("runStatus", runStatus._id, {
73
+ state: { kind: "running" },
74
+ });
75
+ const scheduledTime = boundScheduledTime(
76
+ fromSegment(getCurrentSegment()),
77
+ console,
78
+ );
74
79
  await ctx.scheduler.runAt(scheduledTime, internal.loop.main, {
75
80
  generation: runStatus.state.generation,
76
- segment: current,
77
81
  });
78
- return current;
79
82
  }
80
83
 
81
84
  export const forceKick = internalMutation({
82
85
  args: {},
83
86
  handler: async (ctx) => {
84
87
  const runStatus = await getOrCreateRunStatus(ctx);
85
- await ctx.db.delete(runStatus._id);
88
+ await ctx.db.delete("runStatus", runStatus._id);
86
89
  await kickMainLoop(ctx, "kick");
87
90
  },
88
91
  });
@@ -97,7 +100,7 @@ async function getOrCreateRunStatus(ctx: MutationCtx) {
97
100
  generation: state?.generation ?? INITIAL_STATE.generation,
98
101
  },
99
102
  });
100
- runStatus = (await ctx.db.get(id))!;
103
+ runStatus = (await ctx.db.get("runStatus", id))!;
101
104
  if (!state) {
102
105
  await ctx.db.insert("internalState", INITIAL_STATE);
103
106
  }
@@ -165,7 +165,7 @@ describe("lib", () => {
165
165
 
166
166
  // Delete the work item
167
167
  await t.run(async (ctx) => {
168
- await ctx.db.delete(id);
168
+ await ctx.db.delete("work", id);
169
169
  });
170
170
 
171
171
  // Try to cancel the deleted work
@@ -281,7 +281,7 @@ describe("lib", () => {
281
281
  },
282
282
  });
283
283
  await t.run(async (ctx) => {
284
- await ctx.db.delete(id);
284
+ await ctx.db.delete("work", id);
285
285
  });
286
286
 
287
287
  const status = await t.query(api.lib.status, { id });
@@ -303,7 +303,7 @@ describe("lib", () => {
303
303
 
304
304
  // Verify work item and pending start were created
305
305
  await t.run(async (ctx) => {
306
- const work = await ctx.db.get(id);
306
+ const work = await ctx.db.get("work", id);
307
307
  expect(work).toBeDefined();
308
308
  const pendingStarts = await ctx.db.query("pendingStart").collect();
309
309
  expect(pendingStarts).toHaveLength(1);
@@ -332,7 +332,7 @@ describe("lib", () => {
332
332
  const pendingStart = await ctx.db.query("pendingStart").first();
333
333
  expect(pendingStart).toBeDefined();
334
334
  assert(pendingStart);
335
- await ctx.db.delete(pendingStart._id);
335
+ await ctx.db.delete("pendingStart", pendingStart._id);
336
336
  });
337
337
 
338
338
  const status = await t.query(api.lib.status, { id });
@@ -362,7 +362,7 @@ describe("lib", () => {
362
362
  const pendingStart = await ctx.db.query("pendingStart").first();
363
363
  expect(pendingStart).toBeDefined();
364
364
  assert(pendingStart);
365
- await ctx.db.delete(pendingStart._id);
365
+ await ctx.db.delete("pendingStart", pendingStart._id);
366
366
 
367
367
  // Create a pendingCompletion with retry=true to simulate a failed job that will be retried
368
368
  await ctx.db.insert("pendingCompletion", {
@@ -395,7 +395,7 @@ describe("lib", () => {
395
395
  const pendingStart = await ctx.db.query("pendingStart").first();
396
396
  expect(pendingStart).toBeDefined();
397
397
  assert(pendingStart);
398
- await ctx.db.delete(pendingStart._id);
398
+ await ctx.db.delete("pendingStart", pendingStart._id);
399
399
 
400
400
  // Create a pendingCancelation
401
401
  await ctx.db.insert("pendingCancelation", {
@@ -428,7 +428,7 @@ describe("lib", () => {
428
428
  const pendingStart = await ctx.db.query("pendingStart").first();
429
429
  expect(pendingStart).toBeDefined();
430
430
  assert(pendingStart);
431
- await ctx.db.delete(pendingStart._id);
431
+ await ctx.db.delete("pendingStart", pendingStart._id);
432
432
 
433
433
  // Create a pendingCompletion with retry=false
434
434
  await ctx.db.insert("pendingCompletion", {
@@ -19,7 +19,7 @@ import {
19
19
  boundScheduledTime,
20
20
  vConfig,
21
21
  fnType,
22
- getNextSegment,
22
+ getCurrentSegment,
23
23
  max,
24
24
  vOnCompleteFnContext,
25
25
  retryBehavior,
@@ -53,14 +53,13 @@ export const enqueue = mutation({
53
53
  handler: async (ctx, { config, ...itemArgs }) => {
54
54
  const globals = await getOrUpdateGlobals(ctx, config);
55
55
  const console = createLogger(globals.logLevel);
56
- const kickSegment = await kickMainLoop(ctx, "enqueue", globals);
57
- return await enqueueHandler(ctx, console, kickSegment, itemArgs);
56
+ await kickMainLoop(ctx, "enqueue", globals);
57
+ return await enqueueHandler(ctx, console, itemArgs);
58
58
  },
59
59
  });
60
60
  async function enqueueHandler(
61
61
  ctx: MutationCtx,
62
62
  console: Logger,
63
- kickSegment: bigint,
64
63
  { runAt, ...workArgs }: ObjectType<typeof itemArgs>,
65
64
  ) {
66
65
  runAt = boundScheduledTime(runAt, console);
@@ -115,7 +114,7 @@ async function enqueueHandler(
115
114
 
116
115
  await ctx.db.insert("pendingStart", {
117
116
  workId,
118
- segment: max(toSegment(runAt), kickSegment),
117
+ segment: max(toSegment(runAt), getCurrentSegment()),
119
118
  });
120
119
  recordEnqueued(console, { workId, fnName: workArgs.fnName, runAt });
121
120
  return workId;
@@ -130,10 +129,8 @@ export const enqueueBatch = mutation({
130
129
  handler: async (ctx, { config, items }) => {
131
130
  const globals = await getOrUpdateGlobals(ctx, config);
132
131
  const console = createLogger(globals.logLevel);
133
- const kickSegment = await kickMainLoop(ctx, "enqueue", globals);
134
- return Promise.all(
135
- items.map((item) => enqueueHandler(ctx, console, kickSegment, item)),
136
- );
132
+ await kickMainLoop(ctx, "enqueue", globals);
133
+ return Promise.all(items.map((item) => enqueueHandler(ctx, console, item)));
137
134
  },
138
135
  });
139
136
 
@@ -146,10 +143,10 @@ export const cancel = mutation({
146
143
  const globals = await getOrUpdateGlobals(ctx, { logLevel });
147
144
  const shouldCancel = await shouldCancelWorkItem(ctx, id, globals.logLevel);
148
145
  if (shouldCancel) {
149
- const segment = await kickMainLoop(ctx, "cancel", globals);
146
+ await kickMainLoop(ctx, "cancel", globals);
150
147
  await ctx.db.insert("pendingCancelation", {
151
148
  workId: id,
152
- segment,
149
+ segment: getCurrentSegment(),
153
150
  });
154
151
  }
155
152
  },
@@ -176,10 +173,10 @@ export const cancelAll = mutation({
176
173
  shouldCancelWorkItem(ctx, _id, globals.logLevel),
177
174
  ),
178
175
  );
179
- let segment = getNextSegment();
180
176
  if (shouldCancel.some((c) => c)) {
181
- segment = await kickMainLoop(ctx, "cancel", globals);
177
+ await kickMainLoop(ctx, "cancel", globals);
182
178
  }
179
+ const segment = getCurrentSegment();
183
180
  await Promise.all(
184
181
  pageOfWork.map(({ _id }, index) => {
185
182
  if (shouldCancel[index]) {
@@ -206,7 +203,7 @@ export const status = query({
206
203
  handler: statusHandler,
207
204
  });
208
205
  async function statusHandler(ctx: QueryCtx, { id }: { id: Id<"work"> }) {
209
- const work = await ctx.db.get(id);
206
+ const work = await ctx.db.get("work", id);
210
207
  if (!work) {
211
208
  return { state: "finished" } as const;
212
209
  }
@@ -245,7 +242,7 @@ async function shouldCancelWorkItem(
245
242
  ) {
246
243
  const console = createLogger(logLevel);
247
244
  // No-op if the work doesn't exist or has completed.
248
- const work = await ctx.db.get(workId);
245
+ const work = await ctx.db.get("work", workId);
249
246
  if (!work) {
250
247
  console.warn(`[cancel] work ${workId} doesn't exist`);
251
248
  return false;