@convex-dev/workpool 0.2.18-alpha.3 → 0.2.19-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.
- package/README.md +83 -76
- package/dist/commonjs/client/index.d.ts +98 -31
- package/dist/commonjs/client/index.d.ts.map +1 -1
- package/dist/commonjs/client/index.js +124 -49
- package/dist/commonjs/client/index.js.map +1 -1
- package/dist/commonjs/component/kick.d.ts.map +1 -1
- package/dist/commonjs/component/kick.js +2 -2
- package/dist/commonjs/component/kick.js.map +1 -1
- package/dist/commonjs/component/lib.d.ts +39 -1
- package/dist/commonjs/component/lib.d.ts.map +1 -1
- package/dist/commonjs/component/lib.js +92 -56
- package/dist/commonjs/component/lib.js.map +1 -1
- package/dist/esm/client/index.d.ts +98 -31
- package/dist/esm/client/index.d.ts.map +1 -1
- package/dist/esm/client/index.js +124 -49
- package/dist/esm/client/index.js.map +1 -1
- package/dist/esm/component/kick.d.ts.map +1 -1
- package/dist/esm/component/kick.js +2 -2
- package/dist/esm/component/kick.js.map +1 -1
- package/dist/esm/component/lib.d.ts +39 -1
- package/dist/esm/component/lib.d.ts.map +1 -1
- package/dist/esm/component/lib.js +92 -56
- package/dist/esm/component/lib.js.map +1 -1
- package/package.json +15 -10
- package/src/client/index.ts +225 -82
- package/src/component/_generated/api.d.ts +34 -0
- package/src/component/kick.ts +3 -1
- package/src/component/lib.ts +108 -61
package/src/component/lib.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { v } from "convex/values";
|
|
1
|
+
import { Infer, ObjectType, v } from "convex/values";
|
|
2
2
|
import { api } from "./_generated/api.js";
|
|
3
3
|
import { fnType } from "./shared.js";
|
|
4
4
|
import { Id } from "./_generated/dataModel.js";
|
|
5
|
-
import { mutation, MutationCtx, query } from "./_generated/server.js";
|
|
5
|
+
import { mutation, MutationCtx, query, QueryCtx } from "./_generated/server.js";
|
|
6
6
|
import { kickMainLoop } from "./kick.js";
|
|
7
|
-
import { createLogger, LogLevel, logLevel } from "./logging.js";
|
|
7
|
+
import { createLogger, Logger, LogLevel, logLevel } from "./logging.js";
|
|
8
8
|
import {
|
|
9
9
|
boundScheduledTime,
|
|
10
10
|
config,
|
|
@@ -20,44 +20,75 @@ import { recordEnqueued } from "./stats.js";
|
|
|
20
20
|
const MAX_POSSIBLE_PARALLELISM = 100;
|
|
21
21
|
const MAX_PARALLELISM_SOFT_LIMIT = 50;
|
|
22
22
|
|
|
23
|
+
const itemArgs = {
|
|
24
|
+
fnHandle: v.string(),
|
|
25
|
+
fnName: v.string(),
|
|
26
|
+
fnArgs: v.any(),
|
|
27
|
+
fnType,
|
|
28
|
+
runAt: v.number(),
|
|
29
|
+
// TODO: annotation?
|
|
30
|
+
onComplete: v.optional(onComplete),
|
|
31
|
+
retryBehavior: v.optional(retryBehavior),
|
|
32
|
+
};
|
|
33
|
+
const enqueueArgs = {
|
|
34
|
+
...itemArgs,
|
|
35
|
+
config,
|
|
36
|
+
};
|
|
23
37
|
export const enqueue = mutation({
|
|
38
|
+
args: enqueueArgs,
|
|
39
|
+
returns: v.id("work"),
|
|
40
|
+
handler: async (ctx, { config, ...itemArgs }) => {
|
|
41
|
+
validateConfig(config);
|
|
42
|
+
const console = createLogger(config.logLevel);
|
|
43
|
+
const kickSegment = await kickMainLoop(ctx, "enqueue", config);
|
|
44
|
+
return await enqueueHandler(ctx, console, kickSegment, itemArgs);
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
async function enqueueHandler(
|
|
48
|
+
ctx: MutationCtx,
|
|
49
|
+
console: Logger,
|
|
50
|
+
kickSegment: bigint,
|
|
51
|
+
{ runAt, ...workArgs }: ObjectType<typeof itemArgs>
|
|
52
|
+
) {
|
|
53
|
+
runAt = boundScheduledTime(runAt, console);
|
|
54
|
+
const workId = await ctx.db.insert("work", {
|
|
55
|
+
...workArgs,
|
|
56
|
+
attempts: 0,
|
|
57
|
+
});
|
|
58
|
+
await ctx.db.insert("pendingStart", {
|
|
59
|
+
workId,
|
|
60
|
+
segment: max(toSegment(runAt), kickSegment),
|
|
61
|
+
});
|
|
62
|
+
recordEnqueued(console, { workId, fnName: workArgs.fnName, runAt });
|
|
63
|
+
return workId;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
type Config = Infer<typeof config>;
|
|
67
|
+
function validateConfig(config: Config) {
|
|
68
|
+
if (config.maxParallelism > MAX_POSSIBLE_PARALLELISM) {
|
|
69
|
+
throw new Error(`maxParallelism must be <= ${MAX_PARALLELISM_SOFT_LIMIT}`);
|
|
70
|
+
} else if (config.maxParallelism > MAX_PARALLELISM_SOFT_LIMIT) {
|
|
71
|
+
createLogger(config.logLevel).warn(
|
|
72
|
+
`maxParallelism should be <= ${MAX_PARALLELISM_SOFT_LIMIT}, but is set to ${config.maxParallelism}. This will be an error in a future version.`
|
|
73
|
+
);
|
|
74
|
+
} else if (config.maxParallelism < 1) {
|
|
75
|
+
throw new Error("maxParallelism must be >= 1");
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export const enqueueBatch = mutation({
|
|
24
80
|
args: {
|
|
25
|
-
|
|
26
|
-
fnName: v.string(),
|
|
27
|
-
fnArgs: v.any(),
|
|
28
|
-
fnType,
|
|
29
|
-
runAt: v.number(),
|
|
30
|
-
// TODO: annotation?
|
|
31
|
-
onComplete: v.optional(onComplete),
|
|
32
|
-
retryBehavior: v.optional(retryBehavior),
|
|
81
|
+
items: v.array(v.object(itemArgs)),
|
|
33
82
|
config,
|
|
34
83
|
},
|
|
35
|
-
returns: v.id("work"),
|
|
36
|
-
handler: async (ctx, { config,
|
|
84
|
+
returns: v.array(v.id("work")),
|
|
85
|
+
handler: async (ctx, { config, items }) => {
|
|
86
|
+
validateConfig(config);
|
|
37
87
|
const console = createLogger(config.logLevel);
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
} else if (config.maxParallelism > MAX_PARALLELISM_SOFT_LIMIT) {
|
|
43
|
-
console.warn(
|
|
44
|
-
`maxParallelism should be <= ${MAX_PARALLELISM_SOFT_LIMIT}, but is set to ${config.maxParallelism}. This will be an error in a future version.`
|
|
45
|
-
);
|
|
46
|
-
} else if (config.maxParallelism < 1) {
|
|
47
|
-
throw new Error("maxParallelism must be >= 1");
|
|
48
|
-
}
|
|
49
|
-
runAt = boundScheduledTime(runAt, console);
|
|
50
|
-
const workId = await ctx.db.insert("work", {
|
|
51
|
-
...workArgs,
|
|
52
|
-
attempts: 0,
|
|
53
|
-
});
|
|
54
|
-
const limit = await kickMainLoop(ctx, "enqueue", config);
|
|
55
|
-
await ctx.db.insert("pendingStart", {
|
|
56
|
-
workId,
|
|
57
|
-
segment: max(toSegment(runAt), limit),
|
|
58
|
-
});
|
|
59
|
-
recordEnqueued(console, { workId, fnName: workArgs.fnName, runAt });
|
|
60
|
-
return workId;
|
|
88
|
+
const kickSegment = await kickMainLoop(ctx, "enqueue", config);
|
|
89
|
+
return Promise.all(
|
|
90
|
+
items.map((item) => enqueueHandler(ctx, console, kickSegment, item))
|
|
91
|
+
);
|
|
61
92
|
},
|
|
62
93
|
});
|
|
63
94
|
|
|
@@ -80,14 +111,19 @@ export const cancel = mutation({
|
|
|
80
111
|
|
|
81
112
|
const PAGE_SIZE = 64;
|
|
82
113
|
export const cancelAll = mutation({
|
|
83
|
-
args: {
|
|
84
|
-
|
|
114
|
+
args: {
|
|
115
|
+
logLevel,
|
|
116
|
+
before: v.optional(v.number()),
|
|
117
|
+
limit: v.optional(v.number()),
|
|
118
|
+
},
|
|
119
|
+
handler: async (ctx, { logLevel, before, limit }) => {
|
|
85
120
|
const beforeTime = before ?? Date.now();
|
|
121
|
+
const pageSize = limit ?? PAGE_SIZE;
|
|
86
122
|
const pageOfWork = await ctx.db
|
|
87
123
|
.query("work")
|
|
88
124
|
.withIndex("by_creation_time", (q) => q.lte("_creationTime", beforeTime))
|
|
89
125
|
.order("desc")
|
|
90
|
-
.take(
|
|
126
|
+
.take(pageSize);
|
|
91
127
|
const shouldCancel = await Promise.all(
|
|
92
128
|
pageOfWork.map(async ({ _id }) =>
|
|
93
129
|
shouldCancelWorkItem(ctx, _id, logLevel)
|
|
@@ -107,7 +143,7 @@ export const cancelAll = mutation({
|
|
|
107
143
|
}
|
|
108
144
|
})
|
|
109
145
|
);
|
|
110
|
-
if (pageOfWork.length ===
|
|
146
|
+
if (pageOfWork.length === pageSize) {
|
|
111
147
|
await ctx.scheduler.runAfter(0, api.lib.cancelAll, {
|
|
112
148
|
logLevel,
|
|
113
149
|
before: pageOfWork[pageOfWork.length - 1]._creationTime,
|
|
@@ -119,27 +155,38 @@ export const cancelAll = mutation({
|
|
|
119
155
|
export const status = query({
|
|
120
156
|
args: { id: v.id("work") },
|
|
121
157
|
returns: statusValidator,
|
|
122
|
-
handler:
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
158
|
+
handler: statusHandler,
|
|
159
|
+
});
|
|
160
|
+
async function statusHandler(ctx: QueryCtx, { id }: { id: Id<"work"> }) {
|
|
161
|
+
const work = await ctx.db.get(id);
|
|
162
|
+
if (!work) {
|
|
163
|
+
return { state: "finished" } as const;
|
|
164
|
+
}
|
|
165
|
+
const pendingStart = await ctx.db
|
|
166
|
+
.query("pendingStart")
|
|
167
|
+
.withIndex("workId", (q) => q.eq("workId", id))
|
|
168
|
+
.unique();
|
|
169
|
+
if (pendingStart) {
|
|
170
|
+
return { state: "pending", previousAttempts: work.attempts } as const;
|
|
171
|
+
}
|
|
172
|
+
const pendingCompletion = await ctx.db
|
|
173
|
+
.query("pendingCompletion")
|
|
174
|
+
.withIndex("workId", (q) => q.eq("workId", id))
|
|
175
|
+
.unique();
|
|
176
|
+
if (pendingCompletion?.retry) {
|
|
177
|
+
return { state: "pending", previousAttempts: work.attempts } as const;
|
|
178
|
+
}
|
|
179
|
+
// Assume it's in progress. It could be pending cancelation
|
|
180
|
+
return { state: "running", previousAttempts: work.attempts } as const;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export const statusBatch = query({
|
|
184
|
+
args: { ids: v.array(v.id("work")) },
|
|
185
|
+
returns: v.array(statusValidator),
|
|
186
|
+
handler: async (ctx, { ids }) => {
|
|
187
|
+
return await Promise.all(
|
|
188
|
+
ids.map(async (id) => await statusHandler(ctx, { id }))
|
|
189
|
+
);
|
|
143
190
|
},
|
|
144
191
|
});
|
|
145
192
|
|