@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,13 +8,19 @@ import {
8
8
  test,
9
9
  vi,
10
10
  } from "vitest";
11
- import schema from "./schema.js";
12
- import { modules } from "./setup.test.js";
13
- import { DEFAULT_MAX_PARALLELISM, kickMainLoop } from "./kick.js";
14
- import { DEFAULT_LOG_LEVEL } from "./logging.js";
15
11
  import { internal } from "./_generated/api";
16
- import { toSegment, fromSegment, nextSegment } from "./shared";
17
12
  import { Id } from "./_generated/dataModel.js";
13
+ import { kickMainLoop } from "./kick.js";
14
+ import { DEFAULT_LOG_LEVEL } from "./logging.js";
15
+ import schema from "./schema.js";
16
+ import { modules } from "./setup.test.js";
17
+ import {
18
+ fromSegment,
19
+ getCurrentSegment,
20
+ getNextSegment,
21
+ toSegment,
22
+ } from "./shared";
23
+ import { DEFAULT_MAX_PARALLELISM } from "./shared.js";
18
24
 
19
25
  describe("kickMainLoop", () => {
20
26
  beforeEach(() => {
@@ -53,13 +59,13 @@ describe("kickMainLoop", () => {
53
59
  expect(globals.logLevel).toBe(DEFAULT_LOG_LEVEL);
54
60
  await kickMainLoop(ctx, "enqueue", {
55
61
  maxParallelism: DEFAULT_MAX_PARALLELISM + 1,
56
- logLevel: "DEBUG",
62
+ logLevel: "ERROR",
57
63
  });
58
64
  const after = await ctx.db.query("globals").unique();
59
65
  expect(after).not.toBeNull();
60
66
  assert(after);
61
67
  expect(after.maxParallelism).toBe(DEFAULT_MAX_PARALLELISM + 1);
62
- expect(after.logLevel).toBe("DEBUG");
68
+ expect(after.logLevel).toBe("ERROR");
63
69
  });
64
70
  });
65
71
 
@@ -73,11 +79,12 @@ describe("kickMainLoop", () => {
73
79
  expect(runStatus.state.kind).toBe("running");
74
80
 
75
81
  // Second kick should not change state
76
- await kickMainLoop(ctx, "enqueue");
82
+ const segment = await kickMainLoop(ctx, "enqueue");
77
83
  const afterStatus = await ctx.db.query("runStatus").unique();
78
84
  assert(afterStatus);
79
85
  expect(afterStatus.state.kind).toBe("running");
80
86
  expect(afterStatus._id).toBe(runStatus._id);
87
+ expect(segment).toBe(getNextSegment());
81
88
  });
82
89
  });
83
90
 
@@ -114,7 +121,8 @@ describe("kickMainLoop", () => {
114
121
  });
115
122
 
116
123
  // Kick should reschedule to run sooner
117
- await kickMainLoop(ctx, "enqueue");
124
+ const segment = await kickMainLoop(ctx, "enqueue");
125
+ expect(segment).toBe(getCurrentSegment());
118
126
 
119
127
  const afterStatus = await ctx.db.query("runStatus").unique();
120
128
  assert(afterStatus);
@@ -155,7 +163,8 @@ describe("kickMainLoop", () => {
155
163
  });
156
164
 
157
165
  // Kick should not change state when saturated
158
- await kickMainLoop(ctx, "enqueue");
166
+ const segment = await kickMainLoop(ctx, "enqueue");
167
+ expect(segment).toBe(getNextSegment());
159
168
  const afterStatus = await ctx.db.query("runStatus").unique();
160
169
  assert(afterStatus);
161
170
  expect(afterStatus.state.kind).toBe("scheduled");
@@ -176,7 +185,7 @@ describe("kickMainLoop", () => {
176
185
  await ctx.db.delete(runStatus._id);
177
186
 
178
187
  // Kick should recreate runStatus
179
- await kickMainLoop(ctx, "recovery");
188
+ await kickMainLoop(ctx, "complete");
180
189
  const newRunStatus = await ctx.db.query("runStatus").unique();
181
190
  expect(newRunStatus).not.toBeNull();
182
191
  assert(newRunStatus);
@@ -196,7 +205,7 @@ describe("kickMainLoop", () => {
196
205
  await ctx.db.delete(globals._id);
197
206
 
198
207
  // Kick should recreate globals
199
- await kickMainLoop(ctx, "recovery");
208
+ await kickMainLoop(ctx, "complete");
200
209
  const newGlobals = await ctx.db.query("globals").unique();
201
210
  expect(newGlobals).not.toBeNull();
202
211
  assert(newGlobals);
@@ -208,13 +217,15 @@ describe("kickMainLoop", () => {
208
217
  test("handles race conditions between multiple kicks", async () => {
209
218
  const t = convexTest(schema, modules);
210
219
  // Run kicks in separate transactions to simulate concurrent access
211
- await Promise.all(
220
+ const segments = await Promise.all(
212
221
  Array.from({ length: 10 }, () =>
213
222
  t.run(async (ctx) => {
214
- await kickMainLoop(ctx, "enqueue");
223
+ const segment = await kickMainLoop(ctx, "enqueue");
224
+ return segment;
215
225
  })
216
226
  )
217
227
  );
228
+ expect(segments.filter((s) => s === getCurrentSegment())).toHaveLength(1);
218
229
 
219
230
  // Check final state in a new transaction
220
231
  await t.run(async (ctx) => {
@@ -242,20 +253,19 @@ describe("kickMainLoop", () => {
242
253
  // Initial kick with custom config
243
254
  await kickMainLoop(ctx, "enqueue", {
244
255
  maxParallelism: 5,
245
- logLevel: "DEBUG",
256
+ logLevel: "ERROR",
246
257
  });
247
258
 
248
259
  // Kick from different sources
249
260
  await kickMainLoop(ctx, "cancel");
250
- await kickMainLoop(ctx, "saveResult");
251
- await kickMainLoop(ctx, "recovery");
261
+ await kickMainLoop(ctx, "complete");
252
262
 
253
263
  // Config should be preserved
254
264
  const globals = await ctx.db.query("globals").unique();
255
265
  expect(globals).not.toBeNull();
256
266
  assert(globals);
257
267
  expect(globals.maxParallelism).toBe(5);
258
- expect(globals.logLevel).toBe("DEBUG");
268
+ expect(globals.logLevel).toBe("ERROR");
259
269
  });
260
270
  });
261
271
 
@@ -265,7 +275,7 @@ describe("kickMainLoop", () => {
265
275
  await kickMainLoop(ctx, "enqueue");
266
276
  const runStatus = await ctx.db.query("runStatus").unique();
267
277
  assert(runStatus);
268
- const segment = nextSegment() + 10n;
278
+ const segment = getNextSegment() + 10n;
269
279
  await ctx.db.patch(runStatus._id, {
270
280
  state: {
271
281
  generation: 0n,
@@ -2,43 +2,49 @@ import { internal } from "./_generated/api.js";
2
2
  import { internalMutation, MutationCtx } from "./_generated/server.js";
3
3
  import { createLogger, DEFAULT_LOG_LEVEL } from "./logging.js";
4
4
  import { INITIAL_STATE } from "./loop.js";
5
- import { Config, nextSegment } from "./shared.js";
5
+ import {
6
+ boundScheduledTime,
7
+ Config,
8
+ DEFAULT_MAX_PARALLELISM,
9
+ fromSegment,
10
+ getCurrentSegment,
11
+ getNextSegment,
12
+ } from "./shared.js";
6
13
 
7
- export const DEFAULT_MAX_PARALLELISM = 10;
8
14
  /**
9
- * Called from outside the loop:
15
+ * Called from outside the loop.
16
+ * Returns the soonest segment to enqueue work for the main loop.
10
17
  */
11
-
12
18
  export async function kickMainLoop(
13
19
  ctx: MutationCtx,
14
- source: "enqueue" | "cancel" | "saveResult" | "recovery",
20
+ source: "enqueue" | "cancel" | "complete",
15
21
  config?: Partial<Config>
16
- ): Promise<void> {
22
+ ): Promise<bigint> {
17
23
  const globals = await getOrUpdateGlobals(ctx, config);
18
24
  const console = createLogger(globals.logLevel);
19
25
  const runStatus = await getOrCreateRunStatus(ctx);
26
+ const next = getNextSegment();
20
27
 
21
28
  // Only kick to run now if we're scheduled or idle.
22
29
  if (runStatus.state.kind === "running") {
23
30
  console.debug(
24
31
  `[${source}] main is actively running, so we don't need to kick it`
25
32
  );
26
- return;
33
+ return next;
27
34
  }
28
- const segment = nextSegment();
29
35
  // main is scheduled to run later, so we should cancel it and reschedule.
30
36
  if (runStatus.state.kind === "scheduled") {
31
37
  if (source === "enqueue" && runStatus.state.saturated) {
32
38
  console.debug(
33
39
  `[${source}] main is saturated, so we don't need to kick it`
34
40
  );
35
- return;
41
+ return next;
36
42
  }
37
- if (runStatus.state.segment <= segment) {
43
+ if (runStatus.state.segment <= next) {
38
44
  console.debug(
39
45
  `[${source}] main is scheduled to run soon enough, so we don't need to kick it`
40
46
  );
41
- return;
47
+ return next;
42
48
  }
43
49
  console.debug(
44
50
  `[${source}] main is scheduled to run later, so reschedule it to run now`
@@ -51,15 +57,17 @@ export async function kickMainLoop(
51
57
  `[${source}] main is marked as scheduled, but it's status is ${scheduled?.state.kind}`
52
58
  );
53
59
  }
60
+ } else if (runStatus.state.kind === "idle") {
61
+ console.debug(`[${source}] main was idle, so run it now`);
54
62
  }
55
- console.debug(
56
- `[${source}] main was scheduled later, so reschedule it to run now`
57
- );
58
63
  await ctx.db.patch(runStatus._id, { state: { kind: "running" } });
59
- await ctx.scheduler.runAfter(0, internal.loop.main, {
64
+ const current = getCurrentSegment();
65
+ const scheduledTime = boundScheduledTime(fromSegment(current), console);
66
+ await ctx.scheduler.runAt(scheduledTime, internal.loop.main, {
60
67
  generation: runStatus.state.generation,
61
- segment,
68
+ segment: current,
62
69
  });
70
+ return current;
63
71
  }
64
72
 
65
73
  export const forceKick = internalMutation({
@@ -67,7 +75,7 @@ export const forceKick = internalMutation({
67
75
  handler: async (ctx) => {
68
76
  const runStatus = await getOrCreateRunStatus(ctx);
69
77
  await ctx.db.delete(runStatus._id);
70
- await kickMainLoop(ctx, "recovery");
78
+ await kickMainLoop(ctx, "complete");
71
79
  },
72
80
  });
73
81
 
@@ -1,5 +1,13 @@
1
1
  import { convexTest } from "convex-test";
2
- import { describe, expect, it, beforeEach, afterEach, vi } from "vitest";
2
+ import {
3
+ describe,
4
+ expect,
5
+ it,
6
+ beforeEach,
7
+ afterEach,
8
+ vi,
9
+ assert,
10
+ } from "vitest";
3
11
  import { Id } from "./_generated/dataModel";
4
12
  import schema from "./schema";
5
13
  import { api } from "./_generated/api";
@@ -36,13 +44,13 @@ describe("lib", () => {
36
44
  runAt: Date.now(),
37
45
  config: {
38
46
  maxParallelism: 10,
39
- logLevel: "INFO",
47
+ logLevel: "WARN",
40
48
  },
41
49
  });
42
50
 
43
51
  expect(id).toBeDefined();
44
52
  const status = await t.query(api.lib.status, { id });
45
- expect(status).toEqual({ state: "pending", attempt: 0 });
53
+ expect(status).toEqual({ state: "pending", previousAttempts: 0 });
46
54
  });
47
55
 
48
56
  it("should throw error if maxParallelism is too high", async () => {
@@ -55,11 +63,27 @@ describe("lib", () => {
55
63
  runAt: Date.now(),
56
64
  config: {
57
65
  maxParallelism: 101, // More than MAX_POSSIBLE_PARALLELISM
58
- logLevel: "INFO",
66
+ logLevel: "WARN",
59
67
  },
60
68
  })
61
69
  ).rejects.toThrow("maxParallelism must be <= 100");
62
70
  });
71
+
72
+ it("should throw error if maxParallelism is too low", async () => {
73
+ await expect(
74
+ t.mutation(api.lib.enqueue, {
75
+ fnHandle: "testHandle",
76
+ fnName: "testFunction",
77
+ fnArgs: { test: true },
78
+ fnType: "mutation",
79
+ runAt: Date.now(),
80
+ config: {
81
+ maxParallelism: 0, // Less than minimum
82
+ logLevel: "WARN",
83
+ },
84
+ })
85
+ ).rejects.toThrow("maxParallelism must be >= 1");
86
+ });
63
87
  });
64
88
 
65
89
  describe("cancel", () => {
@@ -72,13 +96,13 @@ describe("lib", () => {
72
96
  runAt: Date.now(),
73
97
  config: {
74
98
  maxParallelism: 10,
75
- logLevel: "INFO",
99
+ logLevel: "WARN",
76
100
  },
77
101
  });
78
102
 
79
103
  await t.mutation(api.lib.cancel, {
80
104
  id,
81
- logLevel: "INFO",
105
+ logLevel: "WARN",
82
106
  });
83
107
 
84
108
  // Verify a pending cancelation was created
@@ -90,6 +114,74 @@ describe("lib", () => {
90
114
  expect(pendingCancelations[0].workId).toBe(id);
91
115
  });
92
116
  });
117
+
118
+ it("should not create duplicate cancelation requests", async () => {
119
+ const id = await t.mutation(api.lib.enqueue, {
120
+ fnHandle: "testHandle",
121
+ fnName: "testFunction",
122
+ fnArgs: { test: true },
123
+ fnType: "mutation",
124
+ runAt: Date.now(),
125
+ config: {
126
+ maxParallelism: 10,
127
+ logLevel: "WARN",
128
+ },
129
+ });
130
+
131
+ // Cancel the first time
132
+ await t.mutation(api.lib.cancel, {
133
+ id,
134
+ logLevel: "WARN",
135
+ });
136
+
137
+ // Cancel the second time
138
+ await t.mutation(api.lib.cancel, {
139
+ id,
140
+ logLevel: "WARN",
141
+ });
142
+
143
+ // Verify only one pending cancelation was created
144
+ await t.run(async (ctx) => {
145
+ const pendingCancelations = await ctx.db
146
+ .query("pendingCancelation")
147
+ .collect();
148
+ expect(pendingCancelations).toHaveLength(1);
149
+ expect(pendingCancelations[0].workId).toBe(id);
150
+ });
151
+ });
152
+
153
+ it("should not create cancelation for non-existent work", async () => {
154
+ const id = await t.mutation(api.lib.enqueue, {
155
+ fnHandle: "testHandle",
156
+ fnName: "testFunction",
157
+ fnArgs: { test: true },
158
+ fnType: "mutation",
159
+ runAt: Date.now(),
160
+ config: {
161
+ maxParallelism: 10,
162
+ logLevel: "WARN",
163
+ },
164
+ });
165
+
166
+ // Delete the work item
167
+ await t.run(async (ctx) => {
168
+ await ctx.db.delete(id);
169
+ });
170
+
171
+ // Try to cancel the deleted work
172
+ await t.mutation(api.lib.cancel, {
173
+ id,
174
+ logLevel: "WARN",
175
+ });
176
+
177
+ // Verify no pending cancelation was created
178
+ await t.run(async (ctx) => {
179
+ const pendingCancelations = await ctx.db
180
+ .query("pendingCancelation")
181
+ .collect();
182
+ expect(pendingCancelations).toHaveLength(0);
183
+ });
184
+ });
93
185
  });
94
186
 
95
187
  describe("cancelAll", () => {
@@ -101,17 +193,17 @@ describe("lib", () => {
101
193
  fnName: "testFunction",
102
194
  fnArgs: { test: i },
103
195
  fnType: "mutation",
104
- runAt: Date.now(),
196
+ runAt: Date.now() + 5 * 60 * 1000,
105
197
  config: {
106
198
  maxParallelism: 10,
107
- logLevel: "INFO",
199
+ logLevel: "WARN",
108
200
  },
109
201
  });
110
202
  ids.push(id);
111
203
  }
112
204
 
113
205
  await t.mutation(api.lib.cancelAll, {
114
- logLevel: "INFO",
206
+ logLevel: "WARN",
115
207
  before: Date.now() + 1000,
116
208
  });
117
209
 
@@ -125,6 +217,54 @@ describe("lib", () => {
125
217
  expect(canceledIds).toEqual(expect.arrayContaining(ids));
126
218
  });
127
219
  });
220
+
221
+ it("should process work items in batches for cancelAll", async () => {
222
+ const PAGE_SIZE = 64; // Same as in lib.ts
223
+
224
+ // Create PAGE_SIZE + 1 work items to trigger pagination
225
+ for (let i = 0; i < PAGE_SIZE + 1; i++) {
226
+ await t.mutation(api.lib.enqueue, {
227
+ fnHandle: "testHandle",
228
+ fnName: "testFunction",
229
+ fnArgs: { test: i },
230
+ fnType: "mutation",
231
+ runAt: Date.now(),
232
+ config: {
233
+ maxParallelism: 10,
234
+ logLevel: "WARN",
235
+ },
236
+ });
237
+ }
238
+
239
+ await t.mutation(api.lib.cancelAll, {
240
+ logLevel: "WARN",
241
+ before: Date.now() + 1000,
242
+ });
243
+
244
+ // assert that cancelAll was scheduled
245
+ await t.run(async (ctx) => {
246
+ const scheduledFunctions = await ctx.db.system
247
+ .query("_scheduled_functions")
248
+ .collect();
249
+ expect(scheduledFunctions.length).toBeGreaterThan(0);
250
+ // check that one of the scheduled functions is cancelAll
251
+ const cancelAllScheduledFunction = scheduledFunctions.find(
252
+ (sf) => sf.name === "lib:cancelAll"
253
+ );
254
+ expect(cancelAllScheduledFunction).toBeDefined();
255
+ assert(cancelAllScheduledFunction);
256
+ });
257
+
258
+ // Verify the first page of cancelations was created
259
+ await t.run(async (ctx) => {
260
+ const pendingCancelations = await ctx.db
261
+ .query("pendingCancelation")
262
+ .collect();
263
+
264
+ // We should have at least PAGE_SIZE cancelations
265
+ expect(pendingCancelations.length).toEqual(PAGE_SIZE);
266
+ });
267
+ });
128
268
  });
129
269
 
130
270
  describe("status", () => {
@@ -137,7 +277,7 @@ describe("lib", () => {
137
277
  runAt: Date.now(),
138
278
  config: {
139
279
  maxParallelism: 10,
140
- logLevel: "INFO",
280
+ logLevel: "WARN",
141
281
  },
142
282
  });
143
283
  await t.run(async (ctx) => {
@@ -157,7 +297,7 @@ describe("lib", () => {
157
297
  runAt: Date.now(),
158
298
  config: {
159
299
  maxParallelism: 10,
160
- logLevel: "INFO",
300
+ logLevel: "WARN",
161
301
  },
162
302
  });
163
303
 
@@ -171,7 +311,7 @@ describe("lib", () => {
171
311
  });
172
312
 
173
313
  const status = await t.query(api.lib.status, { id });
174
- expect(status).toEqual({ state: "pending", attempt: 0 });
314
+ expect(status).toEqual({ state: "pending", previousAttempts: 0 });
175
315
  });
176
316
 
177
317
  it("should return running state when work is in progress", async () => {
@@ -183,7 +323,37 @@ describe("lib", () => {
183
323
  runAt: Date.now(),
184
324
  config: {
185
325
  maxParallelism: 10,
186
- logLevel: "INFO",
326
+ logLevel: "WARN",
327
+ },
328
+ });
329
+
330
+ // Delete the pendingStart to simulate work in progress
331
+ await t.run(async (ctx) => {
332
+ const pendingStart = await ctx.db.query("pendingStart").first();
333
+ expect(pendingStart).toBeDefined();
334
+ assert(pendingStart);
335
+ await ctx.db.delete(pendingStart._id);
336
+ });
337
+
338
+ const status = await t.query(api.lib.status, { id });
339
+ expect(status).toEqual({ state: "running", previousAttempts: 0 });
340
+ });
341
+
342
+ it("should return pending state for work pending retry", async () => {
343
+ const id = await t.mutation(api.lib.enqueue, {
344
+ fnHandle: "testHandle",
345
+ fnName: "testFunction",
346
+ fnArgs: { test: true },
347
+ fnType: "mutation",
348
+ runAt: Date.now(),
349
+ retryBehavior: {
350
+ maxAttempts: 3,
351
+ initialBackoffMs: 100,
352
+ base: 2,
353
+ },
354
+ config: {
355
+ maxParallelism: 10,
356
+ logLevel: "WARN",
187
357
  },
188
358
  });
189
359
 
@@ -191,13 +361,88 @@ describe("lib", () => {
191
361
  await t.run(async (ctx) => {
192
362
  const pendingStart = await ctx.db.query("pendingStart").first();
193
363
  expect(pendingStart).toBeDefined();
194
- if (pendingStart) {
195
- await ctx.db.delete(pendingStart._id);
196
- }
364
+ assert(pendingStart);
365
+ await ctx.db.delete(pendingStart._id);
366
+
367
+ // Create a pendingCompletion with retry=true to simulate a failed job that will be retried
368
+ await ctx.db.insert("pendingCompletion", {
369
+ workId: id,
370
+ segment: 1n, // Using a simple segment value for testing
371
+ runResult: { kind: "failed", error: "Test error" },
372
+ retry: true,
373
+ });
374
+ });
375
+
376
+ const status = await t.query(api.lib.status, { id });
377
+ expect(status).toEqual({ state: "pending", previousAttempts: 0 });
378
+ });
379
+
380
+ it("should return running state for work with pendingCancelation", async () => {
381
+ const id = await t.mutation(api.lib.enqueue, {
382
+ fnHandle: "testHandle",
383
+ fnName: "testFunction",
384
+ fnArgs: { test: true },
385
+ fnType: "mutation",
386
+ runAt: Date.now(),
387
+ config: {
388
+ maxParallelism: 10,
389
+ logLevel: "WARN",
390
+ },
391
+ });
392
+
393
+ // Delete the pendingStart and add pendingCancelation to simulate cancellation in progress
394
+ await t.run(async (ctx) => {
395
+ const pendingStart = await ctx.db.query("pendingStart").first();
396
+ expect(pendingStart).toBeDefined();
397
+ assert(pendingStart);
398
+ await ctx.db.delete(pendingStart._id);
399
+
400
+ // Create a pendingCancelation
401
+ await ctx.db.insert("pendingCancelation", {
402
+ workId: id,
403
+ segment: 1n, // Using a simple segment value for testing
404
+ });
405
+ });
406
+
407
+ // According to the implementation, a job with pendingCancelation but no pendingStart
408
+ // or pendingCompletion with retry=true is considered "running"
409
+ const status = await t.query(api.lib.status, { id });
410
+ expect(status).toEqual({ state: "running", previousAttempts: 0 });
411
+ });
412
+
413
+ it("should return running state for work with pendingCompletion but retry=false", async () => {
414
+ const id = await t.mutation(api.lib.enqueue, {
415
+ fnHandle: "testHandle",
416
+ fnName: "testFunction",
417
+ fnArgs: { test: true },
418
+ fnType: "mutation",
419
+ runAt: Date.now(),
420
+ config: {
421
+ maxParallelism: 10,
422
+ logLevel: "WARN",
423
+ },
424
+ });
425
+
426
+ // Delete the pendingStart and add pendingCompletion with retry=false
427
+ await t.run(async (ctx) => {
428
+ const pendingStart = await ctx.db.query("pendingStart").first();
429
+ expect(pendingStart).toBeDefined();
430
+ assert(pendingStart);
431
+ await ctx.db.delete(pendingStart._id);
432
+
433
+ // Create a pendingCompletion with retry=false
434
+ await ctx.db.insert("pendingCompletion", {
435
+ workId: id,
436
+ segment: 1n, // Using a simple segment value for testing
437
+ runResult: { kind: "failed", error: "Test error" },
438
+ retry: false,
439
+ });
197
440
  });
198
441
 
442
+ // According to the implementation, a job with pendingCompletion but retry=false
443
+ // is considered "running"
199
444
  const status = await t.query(api.lib.status, { id });
200
- expect(status).toEqual({ state: "running", attempt: 0 });
445
+ expect(status).toEqual({ state: "running", previousAttempts: 0 });
201
446
  });
202
447
  });
203
448
  });