@workerdeck/queue 2.15.0 → 3.1.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/build/index.d.mts +13 -1
- package/build/index.mjs +190 -59
- package/build/index.mjs.map +1 -1
- package/package.json +3 -3
package/build/index.d.mts
CHANGED
|
@@ -14,6 +14,7 @@ interface QueueAdapter {
|
|
|
14
14
|
prune(olderThanMs: number): Promise<number>;
|
|
15
15
|
addDailyTokens(dayKey: string, tokens: number): Promise<number>;
|
|
16
16
|
dailyTokens(dayKey: string): Promise<number>;
|
|
17
|
+
nextRunAt?(): Promise<number | undefined>;
|
|
17
18
|
onWork?(listener: () => void): () => void;
|
|
18
19
|
}
|
|
19
20
|
declare class InMemoryQueueAdapter implements QueueAdapter {
|
|
@@ -26,6 +27,7 @@ declare class InMemoryQueueAdapter implements QueueAdapter {
|
|
|
26
27
|
prune(olderThanMs: number): Promise<number>;
|
|
27
28
|
addDailyTokens(dayKey: string, tokens: number): Promise<number>;
|
|
28
29
|
dailyTokens(dayKey: string): Promise<number>;
|
|
30
|
+
nextRunAt(): Promise<number | undefined>;
|
|
29
31
|
}
|
|
30
32
|
//#endregion
|
|
31
33
|
//#region src/queue.d.ts
|
|
@@ -48,6 +50,12 @@ type JobQueueOptions = {
|
|
|
48
50
|
webhookAttempts?: number;
|
|
49
51
|
webhookRetryDelayMs?: number;
|
|
50
52
|
onEvent?: (event: JobEvent) => void;
|
|
53
|
+
onError?: (error: unknown, context: JobQueueErrorContext) => void;
|
|
54
|
+
};
|
|
55
|
+
type JobQueueErrorPhase = 'pump' | 'start' | 'event' | 'finalize' | 'park' | 'resume';
|
|
56
|
+
type JobQueueErrorContext = {
|
|
57
|
+
jobId?: string;
|
|
58
|
+
phase: JobQueueErrorPhase;
|
|
51
59
|
};
|
|
52
60
|
declare class JobQueue {
|
|
53
61
|
#private;
|
|
@@ -55,12 +63,16 @@ declare class JobQueue {
|
|
|
55
63
|
submit(request: CreateJobRequest): Promise<JobInfo>;
|
|
56
64
|
get(id: string): Promise<JobInfo | null>;
|
|
57
65
|
list(): Promise<JobInfo[]>;
|
|
66
|
+
canParkSession(sessionId: string): boolean;
|
|
58
67
|
onSessionParking(sessionId: string, executionId: string): boolean;
|
|
59
68
|
onSessionResumed(sessionId: string, runner: Runner): void;
|
|
60
69
|
cancel(id: string): Promise<JobInfo | null>;
|
|
70
|
+
pause(): void;
|
|
71
|
+
resume(): void;
|
|
72
|
+
get paused(): boolean;
|
|
61
73
|
stats(): Promise<QueueStats>;
|
|
62
74
|
close(): void;
|
|
63
75
|
}
|
|
64
76
|
//#endregion
|
|
65
|
-
export { InMemoryQueueAdapter, JobQueue, type JobQueueOptions, type JobRecord, type QueueAdapter };
|
|
77
|
+
export { InMemoryQueueAdapter, JobQueue, type JobQueueErrorContext, type JobQueueErrorPhase, type JobQueueOptions, type JobRecord, type QueueAdapter };
|
|
66
78
|
//# sourceMappingURL=index.d.mts.map
|
package/build/index.mjs
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { pickCreateSessionRequest } from "@workerdeck/protocol";
|
|
3
3
|
//#region src/adapter.ts
|
|
4
|
+
function earliestRunAt(jobs, now) {
|
|
5
|
+
let earliest;
|
|
6
|
+
for (const { info } of jobs) if (info.status === "queued" && info.nextRunAt !== void 0 && info.nextRunAt > now) earliest = earliest === void 0 ? info.nextRunAt : Math.min(earliest, info.nextRunAt);
|
|
7
|
+
return earliest;
|
|
8
|
+
}
|
|
4
9
|
var InMemoryQueueAdapter = class {
|
|
5
10
|
#jobs = /* @__PURE__ */ new Map();
|
|
6
11
|
#dailyTokens = /* @__PURE__ */ new Map();
|
|
@@ -54,9 +59,22 @@ var InMemoryQueueAdapter = class {
|
|
|
54
59
|
dailyTokens(dayKey) {
|
|
55
60
|
return Promise.resolve(this.#dailyTokens.get(dayKey) ?? 0);
|
|
56
61
|
}
|
|
62
|
+
nextRunAt() {
|
|
63
|
+
return Promise.resolve(earliestRunAt(this.#jobs.values(), Date.now()));
|
|
64
|
+
}
|
|
57
65
|
};
|
|
58
66
|
//#endregion
|
|
59
67
|
//#region src/queue.ts
|
|
68
|
+
const MAX_TIMER_MS = 2 ** 31 - 1;
|
|
69
|
+
const PUMP_RETRY_MS = 5e3;
|
|
70
|
+
function nextUtcMidnight(epochMs) {
|
|
71
|
+
const now = new Date(epochMs);
|
|
72
|
+
return Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() + 1);
|
|
73
|
+
}
|
|
74
|
+
function reportToConsole(error, context) {
|
|
75
|
+
const job = context.jobId ? ` (job ${context.jobId})` : "";
|
|
76
|
+
console.warn(`[workerdeck] queue ${context.phase} failed${job}: ${error instanceof Error ? error.message : String(error)}`);
|
|
77
|
+
}
|
|
60
78
|
function dayKey(epochMs) {
|
|
61
79
|
return new Date(epochMs).toISOString().slice(0, 10);
|
|
62
80
|
}
|
|
@@ -94,15 +112,20 @@ var JobQueue = class {
|
|
|
94
112
|
#adapter;
|
|
95
113
|
#running = /* @__PURE__ */ new Map();
|
|
96
114
|
#parked = /* @__PURE__ */ new Map();
|
|
115
|
+
#starting = /* @__PURE__ */ new Map();
|
|
97
116
|
#pumping = false;
|
|
117
|
+
#pumpRequested = false;
|
|
118
|
+
#paused = false;
|
|
98
119
|
#closed = false;
|
|
120
|
+
#wakeTimer;
|
|
121
|
+
#wakeAt = Number.POSITIVE_INFINITY;
|
|
99
122
|
#offWork;
|
|
100
123
|
#sweepTimer;
|
|
101
124
|
#retryTimers = /* @__PURE__ */ new Set();
|
|
102
125
|
constructor(options) {
|
|
103
126
|
this.#options = options;
|
|
104
127
|
this.#adapter = options.adapter ?? new InMemoryQueueAdapter();
|
|
105
|
-
this.#offWork = this.#adapter.onWork?.(() =>
|
|
128
|
+
this.#offWork = this.#adapter.onWork?.(() => this.#pump());
|
|
106
129
|
const retention = options.retention;
|
|
107
130
|
if (retention) {
|
|
108
131
|
const interval = retention.sweepIntervalMs ?? Math.min(retention.maxAgeMs, 6e4);
|
|
@@ -153,6 +176,10 @@ var JobQueue = class {
|
|
|
153
176
|
async list() {
|
|
154
177
|
return (await this.#adapter.list()).map((j) => j.info);
|
|
155
178
|
}
|
|
179
|
+
canParkSession(sessionId) {
|
|
180
|
+
const job = this.#bySession(this.#running, sessionId);
|
|
181
|
+
return !job || !job.finalized && !job.killReason;
|
|
182
|
+
}
|
|
156
183
|
onSessionParking(sessionId, executionId) {
|
|
157
184
|
const job = this.#bySession(this.#running, sessionId);
|
|
158
185
|
if (!job) return true;
|
|
@@ -166,28 +193,22 @@ var JobQueue = class {
|
|
|
166
193
|
job.parkedExecutionId = executionId;
|
|
167
194
|
this.#running.delete(job.record.info.id);
|
|
168
195
|
this.#parked.set(job.record.info.id, job);
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
job.parkTimer = setTimeout(() => this.#kill(job, `job exceeded max parked duration (${parkedLimit}ms)`), Math.max(0, parkedLimit - job.parkedMs));
|
|
172
|
-
job.parkTimer.unref?.();
|
|
173
|
-
}
|
|
174
|
-
this.#recordPark(job, executionId);
|
|
175
|
-
return true;
|
|
176
|
-
}
|
|
177
|
-
async #recordPark(job, executionId) {
|
|
178
|
-
const updated = await this.#adapter.update(job.record.info.id, {
|
|
196
|
+
this.#armLimit(job, "parkTimer", this.#options.maxParkedDurationMs, job.parkedMs, "max parked duration");
|
|
197
|
+
const patch = {
|
|
179
198
|
status: "parked",
|
|
180
199
|
parkedAt: job.parkedAt,
|
|
181
200
|
parkedExecutionId: executionId
|
|
201
|
+
};
|
|
202
|
+
this.#spawn("park", job.record.info.id, async () => {
|
|
203
|
+
await this.#recordTransition(job, patch, (info) => ({
|
|
204
|
+
type: "job_parked",
|
|
205
|
+
job: info,
|
|
206
|
+
executionId,
|
|
207
|
+
ts: Date.now()
|
|
208
|
+
}));
|
|
209
|
+
this.#pump();
|
|
182
210
|
});
|
|
183
|
-
|
|
184
|
-
this.#emit(job.record, {
|
|
185
|
-
type: "job_parked",
|
|
186
|
-
job: job.record.info,
|
|
187
|
-
executionId,
|
|
188
|
-
ts: Date.now()
|
|
189
|
-
}, job);
|
|
190
|
-
this.#pump();
|
|
211
|
+
return true;
|
|
191
212
|
}
|
|
192
213
|
onSessionResumed(sessionId, runner) {
|
|
193
214
|
const job = this.#bySession(this.#parked, sessionId);
|
|
@@ -203,27 +224,30 @@ var JobQueue = class {
|
|
|
203
224
|
job.runner = runner;
|
|
204
225
|
this.#parked.delete(job.record.info.id);
|
|
205
226
|
this.#running.set(job.record.info.id, job);
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
job.durationTimer.unref?.();
|
|
210
|
-
}
|
|
211
|
-
job.unsubscribe = runner.subscribe((event) => void this.#handleEvent(job, event), job.lastSeq);
|
|
212
|
-
this.#recordResume(job, executionId);
|
|
213
|
-
}
|
|
214
|
-
async #recordResume(job, executionId) {
|
|
215
|
-
const updated = await this.#adapter.update(job.record.info.id, {
|
|
227
|
+
this.#armLimit(job, "durationTimer", this.#effectiveDurationLimit(job.record.request), job.runningMs, "max duration");
|
|
228
|
+
job.unsubscribe = this.#subscribe(job, job.lastSeq);
|
|
229
|
+
const patch = {
|
|
216
230
|
status: "running",
|
|
217
231
|
parkedAt: void 0,
|
|
218
232
|
parkedExecutionId: void 0
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
this.#emit(job.record, {
|
|
233
|
+
};
|
|
234
|
+
this.#spawn("resume", job.record.info.id, () => this.#recordTransition(job, patch, (info) => ({
|
|
222
235
|
type: "job_resumed",
|
|
223
|
-
job:
|
|
236
|
+
job: info,
|
|
224
237
|
executionId,
|
|
225
238
|
ts: Date.now()
|
|
226
|
-
}
|
|
239
|
+
})));
|
|
240
|
+
}
|
|
241
|
+
async #recordTransition(job, patch, event) {
|
|
242
|
+
const updated = await this.#adapter.update(job.record.info.id, patch);
|
|
243
|
+
if (updated) job.record = updated;
|
|
244
|
+
this.#emit(job.record, event(job.record.info), job);
|
|
245
|
+
}
|
|
246
|
+
#armLimit(job, key, limit, spentMs, label) {
|
|
247
|
+
if (limit === void 0) return;
|
|
248
|
+
const timer = setTimeout(() => this.#kill(job, `job exceeded ${label} (${limit}ms)`), Math.max(0, limit - spentMs));
|
|
249
|
+
timer.unref?.();
|
|
250
|
+
job[key] = timer;
|
|
227
251
|
}
|
|
228
252
|
#bySession(jobs, sessionId) {
|
|
229
253
|
for (const job of jobs.values()) if (job.record.info.sessionId === sessionId) return job;
|
|
@@ -238,7 +262,16 @@ var JobQueue = class {
|
|
|
238
262
|
await this.#finalize(running, this.#abortPatch(running, "canceled"));
|
|
239
263
|
return running.record.info;
|
|
240
264
|
}
|
|
265
|
+
const starting = this.#starting.get(id);
|
|
266
|
+
if (starting) {
|
|
267
|
+
if (starting.canceled) return record.info;
|
|
268
|
+
starting.canceled = true;
|
|
269
|
+
return this.#markCanceled(id);
|
|
270
|
+
}
|
|
241
271
|
if (record.info.status !== "queued") return record.info;
|
|
272
|
+
return this.#markCanceled(id);
|
|
273
|
+
}
|
|
274
|
+
async #markCanceled(id) {
|
|
242
275
|
const updated = await this.#adapter.update(id, {
|
|
243
276
|
status: "canceled",
|
|
244
277
|
finishedAt: Date.now(),
|
|
@@ -251,25 +284,38 @@ var JobQueue = class {
|
|
|
251
284
|
});
|
|
252
285
|
return updated?.info ?? null;
|
|
253
286
|
}
|
|
287
|
+
pause() {
|
|
288
|
+
this.#paused = true;
|
|
289
|
+
}
|
|
290
|
+
resume() {
|
|
291
|
+
if (!this.#paused) return;
|
|
292
|
+
this.#paused = false;
|
|
293
|
+
this.#pump();
|
|
294
|
+
}
|
|
295
|
+
get paused() {
|
|
296
|
+
return this.#paused;
|
|
297
|
+
}
|
|
254
298
|
async stats() {
|
|
255
299
|
const jobs = await this.#adapter.list();
|
|
256
300
|
const dailyTokensUsed = await this.#adapter.dailyTokens(dayKey(Date.now()));
|
|
257
301
|
const dailyTokenLimit = this.#options.dailyTokenLimit;
|
|
258
302
|
return {
|
|
259
303
|
maxConcurrency: this.#options.maxConcurrency ?? 1,
|
|
260
|
-
running: this.#running.size,
|
|
304
|
+
running: this.#running.size + this.#starting.size,
|
|
261
305
|
parked: this.#parked.size,
|
|
262
306
|
queued: jobs.filter((j) => j.info.status === "queued").length,
|
|
263
307
|
sessionTokenLimit: this.#options.sessionTokenLimit,
|
|
264
308
|
dailyTokenLimit,
|
|
265
309
|
dailyTokensUsed,
|
|
266
|
-
paused: dailyTokenLimit !== void 0 && dailyTokensUsed >= dailyTokenLimit
|
|
310
|
+
paused: this.#paused || dailyTokenLimit !== void 0 && dailyTokensUsed >= dailyTokenLimit
|
|
267
311
|
};
|
|
268
312
|
}
|
|
269
313
|
close() {
|
|
270
314
|
this.#closed = true;
|
|
271
315
|
this.#offWork?.();
|
|
272
316
|
clearInterval(this.#sweepTimer);
|
|
317
|
+
clearTimeout(this.#wakeTimer);
|
|
318
|
+
this.#wakeTimer = void 0;
|
|
273
319
|
for (const timer of this.#retryTimers) clearTimeout(timer);
|
|
274
320
|
this.#retryTimers.clear();
|
|
275
321
|
}
|
|
@@ -278,24 +324,69 @@ var JobQueue = class {
|
|
|
278
324
|
if (!retention) return;
|
|
279
325
|
this.#adapter.prune(retention.maxAgeMs).catch(() => {});
|
|
280
326
|
}
|
|
281
|
-
|
|
282
|
-
if (this.#
|
|
327
|
+
#pump() {
|
|
328
|
+
if (this.#closed || this.#paused) return;
|
|
329
|
+
if (this.#pumping) {
|
|
330
|
+
this.#pumpRequested = true;
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
283
333
|
this.#pumping = true;
|
|
334
|
+
this.#spawn("pump", void 0, () => this.#drainRequests());
|
|
335
|
+
}
|
|
336
|
+
async #drainRequests() {
|
|
284
337
|
try {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
}
|
|
338
|
+
do {
|
|
339
|
+
this.#pumpRequested = false;
|
|
340
|
+
await this.#fill();
|
|
341
|
+
} while (this.#pumpRequested && !this.#closed && !this.#paused);
|
|
342
|
+
} catch (error) {
|
|
343
|
+
this.#wake(Date.now() + PUMP_RETRY_MS);
|
|
344
|
+
throw error;
|
|
293
345
|
} finally {
|
|
294
346
|
this.#pumping = false;
|
|
295
347
|
}
|
|
296
348
|
}
|
|
349
|
+
async #fill() {
|
|
350
|
+
const maxConcurrency = this.#options.maxConcurrency ?? 1;
|
|
351
|
+
while (this.#running.size + this.#starting.size < maxConcurrency && !this.#closed && !this.#paused) {
|
|
352
|
+
const limit = this.#options.dailyTokenLimit;
|
|
353
|
+
if (limit !== void 0 && await this.#adapter.dailyTokens(dayKey(Date.now())) >= limit) {
|
|
354
|
+
this.#wake(nextUtcMidnight(Date.now()));
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
const record = await this.#adapter.claimNext();
|
|
358
|
+
if (!record) {
|
|
359
|
+
const next = await this.#nextRunAt();
|
|
360
|
+
if (next !== void 0) this.#wake(next);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
if (this.#closed || this.#paused) {
|
|
364
|
+
await this.#adapter.update(record.info.id, { status: "queued" });
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
const id = record.info.id;
|
|
368
|
+
this.#starting.set(id, { canceled: false });
|
|
369
|
+
this.#spawn("start", id, () => this.#start(record));
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
async #nextRunAt() {
|
|
373
|
+
if (this.#adapter.nextRunAt) return this.#adapter.nextRunAt();
|
|
374
|
+
return earliestRunAt(await this.#adapter.list(), Date.now());
|
|
375
|
+
}
|
|
376
|
+
#wake(at) {
|
|
377
|
+
if (this.#closed || this.#wakeTimer !== void 0 && this.#wakeAt <= at) return;
|
|
378
|
+
clearTimeout(this.#wakeTimer);
|
|
379
|
+
this.#wakeAt = at;
|
|
380
|
+
this.#wakeTimer = setTimeout(() => {
|
|
381
|
+
this.#wakeTimer = void 0;
|
|
382
|
+
this.#wakeAt = Number.POSITIVE_INFINITY;
|
|
383
|
+
this.#pump();
|
|
384
|
+
}, Math.min(MAX_TIMER_MS, Math.max(0, at - Date.now())));
|
|
385
|
+
this.#wakeTimer.unref?.();
|
|
386
|
+
}
|
|
297
387
|
async #start(record) {
|
|
298
388
|
const id = record.info.id;
|
|
389
|
+
const starting = this.#starting.get(id) ?? { canceled: false };
|
|
299
390
|
const build = this.#options.buildRunnerConfig ?? ((req) => req);
|
|
300
391
|
let runner;
|
|
301
392
|
try {
|
|
@@ -308,6 +399,9 @@ var JobQueue = class {
|
|
|
308
399
|
};
|
|
309
400
|
runner = await this.#options.createRunner(build(request));
|
|
310
401
|
} catch (error) {
|
|
402
|
+
this.#starting.delete(id);
|
|
403
|
+
this.#pump();
|
|
404
|
+
if (starting.canceled) return;
|
|
311
405
|
const failed = await this.#adapter.update(id, {
|
|
312
406
|
status: "failed",
|
|
313
407
|
finishedAt: Date.now(),
|
|
@@ -320,6 +414,12 @@ var JobQueue = class {
|
|
|
320
414
|
});
|
|
321
415
|
return;
|
|
322
416
|
}
|
|
417
|
+
this.#starting.delete(id);
|
|
418
|
+
if (starting.canceled) {
|
|
419
|
+
runner.close("server");
|
|
420
|
+
this.#pump();
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
323
423
|
const job = {
|
|
324
424
|
record,
|
|
325
425
|
runner,
|
|
@@ -337,6 +437,12 @@ var JobQueue = class {
|
|
|
337
437
|
const updated = await this.#adapter.update(id, {
|
|
338
438
|
startedAt: Date.now(),
|
|
339
439
|
sessionId: runner.id
|
|
440
|
+
}).catch((error) => {
|
|
441
|
+
this.#report(error, {
|
|
442
|
+
jobId: id,
|
|
443
|
+
phase: "start"
|
|
444
|
+
});
|
|
445
|
+
return null;
|
|
340
446
|
});
|
|
341
447
|
if (updated) job.record = updated;
|
|
342
448
|
this.#emit(job.record, {
|
|
@@ -344,12 +450,29 @@ var JobQueue = class {
|
|
|
344
450
|
job: job.record.info,
|
|
345
451
|
ts: Date.now()
|
|
346
452
|
});
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
453
|
+
this.#armLimit(job, "durationTimer", this.#effectiveDurationLimit(record.request), 0, "max duration");
|
|
454
|
+
job.unsubscribe = this.#subscribe(job);
|
|
455
|
+
}
|
|
456
|
+
#subscribe(job, afterSeq) {
|
|
457
|
+
return job.runner.subscribe((event) => this.#spawn("event", job.record.info.id, () => this.#handleEvent(job, event)), afterSeq);
|
|
458
|
+
}
|
|
459
|
+
#spawn(phase, jobId, run) {
|
|
460
|
+
try {
|
|
461
|
+
run().catch((error) => this.#report(error, {
|
|
462
|
+
jobId,
|
|
463
|
+
phase
|
|
464
|
+
}));
|
|
465
|
+
} catch (error) {
|
|
466
|
+
this.#report(error, {
|
|
467
|
+
jobId,
|
|
468
|
+
phase
|
|
469
|
+
});
|
|
351
470
|
}
|
|
352
|
-
|
|
471
|
+
}
|
|
472
|
+
#report(error, context) {
|
|
473
|
+
try {
|
|
474
|
+
(this.#options.onError ?? reportToConsole)(error, context);
|
|
475
|
+
} catch {}
|
|
353
476
|
}
|
|
354
477
|
#abortPatch(job, error) {
|
|
355
478
|
return {
|
|
@@ -365,13 +488,14 @@ var JobQueue = class {
|
|
|
365
488
|
#kill(job, reason) {
|
|
366
489
|
if (job.finalized || job.killReason) return;
|
|
367
490
|
job.killReason = reason;
|
|
491
|
+
const id = job.record.info.id;
|
|
368
492
|
if (job.parkedAt !== void 0) {
|
|
369
|
-
this.#finalize(job, this.#abortPatch(job, reason));
|
|
493
|
+
this.#spawn("finalize", id, () => this.#finalize(job, this.#abortPatch(job, reason)));
|
|
370
494
|
return;
|
|
371
495
|
}
|
|
372
496
|
job.runner.interrupt().catch(() => {});
|
|
373
497
|
job.forceTimer = setTimeout(() => {
|
|
374
|
-
this.#finalize(job, this.#abortPatch(job, reason));
|
|
498
|
+
this.#spawn("finalize", id, () => this.#finalize(job, this.#abortPatch(job, reason)));
|
|
375
499
|
}, this.#options.killGraceMs ?? 5e3);
|
|
376
500
|
job.forceTimer.unref?.();
|
|
377
501
|
}
|
|
@@ -435,15 +559,20 @@ var JobQueue = class {
|
|
|
435
559
|
}
|
|
436
560
|
}
|
|
437
561
|
#effectiveTokenLimit(request) {
|
|
438
|
-
|
|
439
|
-
return limits.length > 0 ? Math.min(...limits) : void 0;
|
|
562
|
+
return tightest(request.maxTokens, this.#options.sessionTokenLimit);
|
|
440
563
|
}
|
|
441
564
|
#effectiveDurationLimit(request) {
|
|
442
|
-
|
|
443
|
-
return limits.length > 0 ? Math.min(...limits) : void 0;
|
|
565
|
+
return tightest(request.maxDurationMs, this.#options.maxJobDurationMs);
|
|
444
566
|
}
|
|
445
567
|
async #finalize(job, patch) {
|
|
446
568
|
if (job.finalized) return;
|
|
569
|
+
try {
|
|
570
|
+
await this.#settle(job, patch);
|
|
571
|
+
} finally {
|
|
572
|
+
this.#pump();
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
async #settle(job, patch) {
|
|
447
576
|
job.finalized = true;
|
|
448
577
|
job.unsubscribe();
|
|
449
578
|
clearTimeout(job.durationTimer);
|
|
@@ -497,7 +626,6 @@ var JobQueue = class {
|
|
|
497
626
|
timer.unref?.();
|
|
498
627
|
this.#retryTimers.add(timer);
|
|
499
628
|
}
|
|
500
|
-
this.#pump();
|
|
501
629
|
return;
|
|
502
630
|
}
|
|
503
631
|
const updated = await this.#adapter.update(job.record.info.id, {
|
|
@@ -515,7 +643,6 @@ var JobQueue = class {
|
|
|
515
643
|
}, job);
|
|
516
644
|
}
|
|
517
645
|
this.#sweep();
|
|
518
|
-
this.#pump();
|
|
519
646
|
}
|
|
520
647
|
#progress(job, progress) {
|
|
521
648
|
const event = {
|
|
@@ -562,6 +689,10 @@ var JobQueue = class {
|
|
|
562
689
|
}
|
|
563
690
|
}
|
|
564
691
|
};
|
|
692
|
+
function tightest(...limits) {
|
|
693
|
+
const set = limits.filter((n) => typeof n === "number");
|
|
694
|
+
return set.length > 0 ? Math.min(...set) : void 0;
|
|
695
|
+
}
|
|
565
696
|
//#endregion
|
|
566
697
|
export { InMemoryQueueAdapter, JobQueue };
|
|
567
698
|
|
package/build/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/adapter.ts","../src/queue.ts"],"sourcesContent":["import type { CreateJobRequest, JobInfo } from '@workerdeck/protocol'\n\nexport type JobRecord = {\n info: JobInfo\n request: CreateJobRequest\n}\n\nexport interface QueueAdapter {\n add(job: JobRecord): Promise<void>\n claimNext(): Promise<JobRecord | null>\n get(id: string): Promise<JobRecord | null>\n list(): Promise<JobRecord[]>\n update(id: string, patch: Partial<JobInfo>): Promise<JobRecord | null>\n prune(olderThanMs: number): Promise<number>\n addDailyTokens(dayKey: string, tokens: number): Promise<number>\n dailyTokens(dayKey: string): Promise<number>\n onWork?(listener: () => void): () => void\n}\n\nexport class InMemoryQueueAdapter implements QueueAdapter {\n #jobs = new Map<string, JobRecord>()\n #dailyTokens = new Map<string, number>()\n\n add(job: JobRecord): Promise<void> {\n this.#jobs.set(job.info.id, job)\n return Promise.resolve()\n }\n\n claimNext(): Promise<JobRecord | null> {\n const now = Date.now()\n for (const job of this.#jobs.values()) {\n if (job.info.status === 'queued' && (job.info.nextRunAt === undefined || job.info.nextRunAt <= now)) {\n job.info = { ...job.info, status: 'running' }\n return Promise.resolve(job)\n }\n }\n return Promise.resolve(null)\n }\n\n get(id: string): Promise<JobRecord | null> {\n return Promise.resolve(this.#jobs.get(id) ?? null)\n }\n\n list(): Promise<JobRecord[]> {\n return Promise.resolve([...this.#jobs.values()])\n }\n\n update(id: string, patch: Partial<JobInfo>): Promise<JobRecord | null> {\n const job = this.#jobs.get(id)\n if (!job) {\n return Promise.resolve(null)\n }\n job.info = { ...job.info, ...patch }\n return Promise.resolve(job)\n }\n\n prune(olderThanMs: number): Promise<number> {\n const cutoff = Date.now() - olderThanMs\n let removed = 0\n for (const [id, job] of this.#jobs) {\n const { status, finishedAt } = job.info\n const terminal = status === 'succeeded' || status === 'failed' || status === 'canceled'\n if (terminal && (finishedAt ?? 0) <= cutoff) {\n this.#jobs.delete(id)\n removed++\n }\n }\n return Promise.resolve(removed)\n }\n\n addDailyTokens(dayKey: string, tokens: number): Promise<number> {\n const next = (this.#dailyTokens.get(dayKey) ?? 0) + tokens\n this.#dailyTokens.set(dayKey, next)\n return Promise.resolve(next)\n }\n\n dailyTokens(dayKey: string): Promise<number> {\n return Promise.resolve(this.#dailyTokens.get(dayKey) ?? 0)\n }\n}\n","import { randomUUID } from 'node:crypto'\nimport type { Runner, SessionRunnerConfig } from '@workerdeck/core'\nimport {\n pickCreateSessionRequest,\n type ApiMessage,\n type CreateJobRequest,\n type CreateSessionRequest,\n type JobEvent,\n type JobInfo,\n type JobProgress,\n type JobUsage,\n type QueueStats,\n type SessionEvent,\n} from '@workerdeck/protocol'\nimport { InMemoryQueueAdapter, type JobRecord, type QueueAdapter } from './adapter.ts'\n\nexport type JobQueueOptions = {\n createRunner: (config: SessionRunnerConfig) => Runner | Promise<Runner>\n adapter?: QueueAdapter\n maxConcurrency?: number\n sessionTokenLimit?: number\n dailyTokenLimit?: number\n maxJobDurationMs?: number\n maxParkedDurationMs?: number\n killGraceMs?: number\n retention?: { maxAgeMs: number; sweepIntervalMs?: number }\n buildRunnerConfig?: (req: CreateSessionRequest) => SessionRunnerConfig\n discardSession?: (sessionId: string) => void | Promise<void>\n fetchImpl?: typeof fetch\n webhookAttempts?: number\n webhookRetryDelayMs?: number\n onEvent?: (event: JobEvent) => void\n}\n\ntype RunningJob = {\n record: JobRecord\n runner: Runner\n unsubscribe: () => void\n lastSeq: number\n estimatedTokens: number\n killReason?: string\n canceled: boolean\n finalized: boolean\n deliveries: Promise<void>\n durationTimer?: ReturnType<typeof setTimeout>\n forceTimer?: ReturnType<typeof setTimeout>\n runningMs: number\n legStartedAt: number\n parkedAt?: number\n parkedExecutionId?: string\n parkTimer?: ReturnType<typeof setTimeout>\n parkedMs: number\n}\n\nfunction dayKey(epochMs: number): string {\n return new Date(epochMs).toISOString().slice(0, 10)\n}\n\n// An attempt that reported no priced figure at all stays unknown rather than banking a zero:\n// only the claude engine reports a cost of its own, and `$0.00` is a claim.\nfunction addOptionalCost(prior: number | undefined, attempt: number | undefined): number | undefined {\n if (prior === undefined && attempt === undefined) {\n return undefined\n }\n return (prior ?? 0) + (attempt ?? 0)\n}\n\nfunction sumUsage(usage: unknown): number {\n if (typeof usage !== 'object' || usage === null) {\n return 0\n }\n const u = usage as Record<string, unknown>\n return (\n (typeof u.input_tokens === 'number' ? u.input_tokens : 0) +\n (typeof u.output_tokens === 'number' ? u.output_tokens : 0) +\n (typeof u.cache_creation_input_tokens === 'number' ? u.cache_creation_input_tokens : 0) +\n (typeof u.cache_read_input_tokens === 'number' ? u.cache_read_input_tokens : 0)\n )\n}\n\nfunction textPreview(message: ApiMessage, max = 140): JobProgress | null {\n const blocks = typeof message.content === 'string' ? [{ type: 'text', text: message.content }] : message.content\n for (const block of blocks) {\n if (block.type === 'tool_use') {\n return { kind: 'tool_use', preview: (block as { name?: string }).name }\n }\n if (block.type === 'text') {\n const text = (block as { text?: string }).text ?? ''\n if (text.trim()) {\n return {\n kind: 'assistant_text',\n preview: text.length > max ? text.slice(0, max - 1) + '…' : text,\n }\n }\n }\n }\n return null\n}\n\nexport class JobQueue {\n #options: JobQueueOptions\n #adapter: QueueAdapter\n #running = new Map<string, RunningJob>()\n #parked = new Map<string, RunningJob>()\n #pumping = false\n #closed = false\n #offWork: (() => void) | undefined\n #sweepTimer: ReturnType<typeof setInterval> | undefined\n #retryTimers = new Set<ReturnType<typeof setTimeout>>()\n\n constructor(options: JobQueueOptions) {\n this.#options = options\n this.#adapter = options.adapter ?? new InMemoryQueueAdapter()\n this.#offWork = this.#adapter.onWork?.(() => void this.#pump())\n const retention = options.retention\n if (retention) {\n const interval = retention.sweepIntervalMs ?? Math.min(retention.maxAgeMs, 60_000)\n this.#sweepTimer = setInterval(() => this.#sweep(), interval)\n this.#sweepTimer.unref?.()\n }\n }\n\n async submit(request: CreateJobRequest): Promise<JobInfo> {\n if (this.#closed) {\n throw new Error('queue is closed')\n }\n if (!request.session?.prompt?.trim()) {\n throw new Error('session.prompt is required')\n }\n if (request.session.resume || request.session.forkSession) {\n throw new Error('resume/forkSession are not supported for queued jobs')\n }\n const attempts = request.attempts ?? 1\n if (!Number.isInteger(attempts) || attempts < 1) {\n throw new Error('attempts must be a positive integer')\n }\n if (request.retryDelayMs !== undefined && !(request.retryDelayMs >= 0)) {\n throw new Error('retryDelayMs must be >= 0')\n }\n const info: JobInfo = {\n id: randomUUID(),\n status: 'queued',\n cwd: request.session.cwd ?? '',\n profile: request.session.profile,\n prompt: request.session.prompt,\n createdAt: Date.now(),\n attempt: 1,\n maxAttempts: attempts,\n usage: { tokens: 0, totalCostUsd: 0, numTurns: 0 },\n meta: request.meta,\n scope: request.session.scope,\n }\n const record: JobRecord = { info, request }\n await this.#adapter.add(record)\n this.#emit(record, { type: 'job_submitted', job: info, ts: Date.now() }, undefined, {\n skipWebhook: true,\n })\n void this.#pump()\n return info\n }\n\n async get(id: string): Promise<JobInfo | null> {\n return (await this.#adapter.get(id))?.info ?? null\n }\n\n async list(): Promise<JobInfo[]> {\n return (await this.#adapter.list()).map((j) => j.info)\n }\n\n onSessionParking(sessionId: string, executionId: string): boolean {\n const job = this.#bySession(this.#running, sessionId)\n if (!job) {\n return true\n }\n if (job.finalized || job.killReason) {\n return false\n }\n const now = Date.now()\n job.unsubscribe()\n job.runningMs += now - job.legStartedAt\n clearTimeout(job.durationTimer)\n job.durationTimer = undefined\n job.parkedAt = now\n job.parkedExecutionId = executionId\n this.#running.delete(job.record.info.id)\n this.#parked.set(job.record.info.id, job)\n const parkedLimit = this.#options.maxParkedDurationMs\n if (parkedLimit !== undefined) {\n job.parkTimer = setTimeout(\n () => this.#kill(job, `job exceeded max parked duration (${parkedLimit}ms)`),\n Math.max(0, parkedLimit - job.parkedMs),\n )\n job.parkTimer.unref?.()\n }\n void this.#recordPark(job, executionId)\n return true\n }\n\n async #recordPark(job: RunningJob, executionId: string): Promise<void> {\n const updated = await this.#adapter.update(job.record.info.id, {\n status: 'parked',\n parkedAt: job.parkedAt,\n parkedExecutionId: executionId,\n })\n if (updated) {\n job.record = updated\n }\n this.#emit(job.record, { type: 'job_parked', job: job.record.info, executionId, ts: Date.now() }, job)\n void this.#pump()\n }\n\n onSessionResumed(sessionId: string, runner: Runner): void {\n const job = this.#bySession(this.#parked, sessionId)\n if (!job || job.finalized) {\n return\n }\n const now = Date.now()\n const executionId = job.parkedExecutionId ?? ''\n clearTimeout(job.parkTimer)\n job.parkTimer = undefined\n job.parkedMs += now - (job.parkedAt ?? now)\n job.parkedAt = undefined\n job.parkedExecutionId = undefined\n job.legStartedAt = now\n job.runner = runner\n this.#parked.delete(job.record.info.id)\n this.#running.set(job.record.info.id, job)\n const durationLimit = this.#effectiveDurationLimit(job.record.request)\n if (durationLimit !== undefined) {\n job.durationTimer = setTimeout(\n () => this.#kill(job, `job exceeded max duration (${durationLimit}ms)`),\n Math.max(0, durationLimit - job.runningMs),\n )\n job.durationTimer.unref?.()\n }\n job.unsubscribe = runner.subscribe((event) => void this.#handleEvent(job, event), job.lastSeq)\n void this.#recordResume(job, executionId)\n }\n\n async #recordResume(job: RunningJob, executionId: string): Promise<void> {\n const updated = await this.#adapter.update(job.record.info.id, {\n status: 'running',\n parkedAt: undefined,\n parkedExecutionId: undefined,\n })\n if (updated) {\n job.record = updated\n }\n this.#emit(job.record, { type: 'job_resumed', job: job.record.info, executionId, ts: Date.now() }, job)\n }\n\n #bySession(jobs: Map<string, RunningJob>, sessionId: string): RunningJob | undefined {\n for (const job of jobs.values()) {\n if (job.record.info.sessionId === sessionId) {\n return job\n }\n }\n return undefined\n }\n\n async cancel(id: string): Promise<JobInfo | null> {\n const record = await this.#adapter.get(id)\n if (!record) {\n return null\n }\n const running = this.#running.get(id) ?? this.#parked.get(id)\n if (running) {\n running.canceled = true\n running.killReason = 'canceled'\n await this.#finalize(running, this.#abortPatch(running, 'canceled'))\n return running.record.info\n }\n if (record.info.status !== 'queued') {\n return record.info\n }\n const updated = await this.#adapter.update(id, {\n status: 'canceled',\n finishedAt: Date.now(),\n error: 'canceled',\n })\n if (updated) {\n this.#emit(updated, { type: 'job_completed', job: updated.info, ts: Date.now() })\n }\n return updated?.info ?? null\n }\n\n async stats(): Promise<QueueStats> {\n const jobs = await this.#adapter.list()\n const dailyTokensUsed = await this.#adapter.dailyTokens(dayKey(Date.now()))\n const dailyTokenLimit = this.#options.dailyTokenLimit\n return {\n maxConcurrency: this.#options.maxConcurrency ?? 1,\n running: this.#running.size,\n parked: this.#parked.size,\n queued: jobs.filter((j) => j.info.status === 'queued').length,\n sessionTokenLimit: this.#options.sessionTokenLimit,\n dailyTokenLimit,\n dailyTokensUsed,\n paused: dailyTokenLimit !== undefined && dailyTokensUsed >= dailyTokenLimit,\n }\n }\n\n close(): void {\n this.#closed = true\n this.#offWork?.()\n clearInterval(this.#sweepTimer)\n for (const timer of this.#retryTimers) {\n clearTimeout(timer)\n }\n this.#retryTimers.clear()\n }\n\n #sweep(): void {\n const retention = this.#options.retention\n if (!retention) {\n return\n }\n this.#adapter.prune(retention.maxAgeMs).catch(() => {})\n }\n\n async #pump(): Promise<void> {\n if (this.#pumping || this.#closed) {\n return\n }\n this.#pumping = true\n try {\n const maxConcurrency = this.#options.maxConcurrency ?? 1\n while (this.#running.size < maxConcurrency) {\n const limit = this.#options.dailyTokenLimit\n if (limit !== undefined && (await this.#adapter.dailyTokens(dayKey(Date.now()))) >= limit) {\n return\n }\n const record = await this.#adapter.claimNext()\n if (!record) {\n return\n }\n await this.#start(record)\n }\n } finally {\n this.#pumping = false\n }\n }\n\n async #start(record: JobRecord): Promise<void> {\n const id = record.info.id\n const build = this.#options.buildRunnerConfig ?? ((req: CreateSessionRequest) => req)\n let runner: Runner\n try {\n // Projected again here, not only at the HTTP door: a durable adapter can hold a record written before the door did.\n const request: CreateSessionRequest = {\n ...pickCreateSessionRequest(record.request.session),\n meta: { ...record.request.session.meta, jobId: id },\n }\n runner = await this.#options.createRunner(build(request))\n } catch (error) {\n const failed = await this.#adapter.update(id, {\n status: 'failed',\n finishedAt: Date.now(),\n error: error instanceof Error ? error.message : String(error),\n })\n if (failed) {\n this.#emit(failed, { type: 'job_completed', job: failed.info, ts: Date.now() })\n }\n return\n }\n const job: RunningJob = {\n record,\n runner,\n unsubscribe: () => {},\n lastSeq: 0,\n estimatedTokens: 0,\n canceled: false,\n finalized: false,\n deliveries: Promise.resolve(),\n runningMs: 0,\n legStartedAt: Date.now(),\n parkedMs: 0,\n }\n this.#running.set(id, job)\n const updated = await this.#adapter.update(id, {\n startedAt: Date.now(),\n sessionId: runner.id,\n })\n if (updated) {\n job.record = updated\n }\n this.#emit(job.record, { type: 'job_started', job: job.record.info, ts: Date.now() })\n const durationLimit = this.#effectiveDurationLimit(record.request)\n if (durationLimit !== undefined) {\n job.durationTimer = setTimeout(() => this.#kill(job, `job exceeded max duration (${durationLimit}ms)`), durationLimit)\n job.durationTimer.unref?.()\n }\n job.unsubscribe = runner.subscribe((event) => void this.#handleEvent(job, event))\n }\n\n // How a run that never produced a `turn_result` is written down. There is no authoritative\n // usage for such a run, so it reports the tokens counted from the message stream, no cost\n // and no turns. `turn_result` is the one path that does NOT come through here - it has real\n // numbers - which is exactly the distinction five inline copies of this used to hide.\n #abortPatch(job: RunningJob, error: string): Partial<JobInfo> {\n return {\n usage: { tokens: job.estimatedTokens, totalCostUsd: 0, numTurns: 0 },\n status: job.canceled ? 'canceled' : 'failed',\n error,\n }\n }\n\n #kill(job: RunningJob, reason: string): void {\n if (job.finalized || job.killReason) {\n return\n }\n job.killReason = reason\n if (job.parkedAt !== undefined) {\n void this.#finalize(job, this.#abortPatch(job, reason))\n return\n }\n void job.runner.interrupt().catch(() => {})\n job.forceTimer = setTimeout(() => {\n void this.#finalize(job, this.#abortPatch(job, reason))\n }, this.#options.killGraceMs ?? 5000)\n job.forceTimer.unref?.()\n }\n\n async #handleEvent(job: RunningJob, event: SessionEvent): Promise<void> {\n if (job.finalized) {\n return\n }\n job.lastSeq = Math.max(job.lastSeq, event.seq)\n switch (event.type) {\n case 'system_init': {\n await this.#adapter.update(job.record.info.id, { sdkSessionId: event.sdkSessionId })\n return\n }\n case 'assistant_message': {\n if (event.replay) {\n return\n }\n job.estimatedTokens += sumUsage(event.message.usage)\n const limit = this.#effectiveTokenLimit(job.record.request)\n if (limit !== undefined && job.estimatedTokens > limit) {\n this.#kill(job, `session token limit exceeded (${job.estimatedTokens} > ${limit})`)\n }\n const progress = textPreview(event.message)\n if (progress) {\n this.#progress(job, progress)\n }\n return\n }\n case 'permission_requested': {\n this.#progress(job, {\n kind: 'permission_requested',\n preview: event.request.title ?? event.request.toolName,\n request: event.request,\n })\n return\n }\n case 'permission_resolved': {\n this.#progress(job, { kind: 'permission_resolved', preview: event.behavior })\n return\n }\n case 'turn_result': {\n const tokens = sumUsage(event.usage) || job.estimatedTokens\n await this.#finalize(job, {\n usage: {\n tokens,\n totalCostUsd: event.totalCostUsd,\n numTurns: event.numTurns,\n costUsd: event.costUsd,\n },\n result: {\n subtype: event.subtype,\n isError: event.isError,\n result: event.result,\n errors: event.errors,\n durationMs: event.durationMs,\n },\n status: job.killReason ? (job.canceled ? 'canceled' : 'failed') : event.isError ? 'failed' : 'succeeded',\n error: job.killReason ?? (event.isError ? event.errors?.join('; ') || event.subtype : undefined),\n })\n return\n }\n case 'session_error': {\n await this.#finalize(job, this.#abortPatch(job, job.killReason ?? event.message))\n return\n }\n case 'session_closed': {\n await this.#finalize(job, this.#abortPatch(job, job.killReason ?? 'session closed before completing'))\n return\n }\n default: {\n return\n }\n }\n }\n\n #effectiveTokenLimit(request: CreateJobRequest): number | undefined {\n const limits = [request.maxTokens, this.#options.sessionTokenLimit].filter((n): n is number => typeof n === 'number')\n return limits.length > 0 ? Math.min(...limits) : undefined\n }\n\n #effectiveDurationLimit(request: CreateJobRequest): number | undefined {\n const limits = [request.maxDurationMs, this.#options.maxJobDurationMs].filter((n): n is number => typeof n === 'number')\n return limits.length > 0 ? Math.min(...limits) : undefined\n }\n\n async #finalize(job: RunningJob, patch: Partial<JobInfo>): Promise<void> {\n if (job.finalized) {\n return\n }\n job.finalized = true\n job.unsubscribe()\n clearTimeout(job.durationTimer)\n clearTimeout(job.forceTimer)\n clearTimeout(job.parkTimer)\n this.#running.delete(job.record.info.id)\n const wasParked = this.#parked.delete(job.record.info.id)\n job.runner.close('server')\n if (wasParked && job.record.info.sessionId) {\n try {\n void Promise.resolve(this.#options.discardSession?.(job.record.info.sessionId)).catch(() => {})\n } catch {}\n }\n const attemptUsage = patch.usage ?? { tokens: 0, totalCostUsd: 0, numTurns: 0 }\n if (attemptUsage.tokens > 0) {\n await this.#adapter.addDailyTokens(dayKey(Date.now()), attemptUsage.tokens)\n }\n const prior = job.record.info.usage\n const usage: JobUsage = {\n tokens: prior.tokens + attemptUsage.tokens,\n totalCostUsd: prior.totalCostUsd + attemptUsage.totalCostUsd,\n numTurns: prior.numTurns + attemptUsage.numTurns,\n costUsd: addOptionalCost(prior.costUsd, attemptUsage.costUsd),\n }\n const attempt = job.record.info.attempt ?? 1\n const maxAttempts = job.record.request.attempts ?? 1\n if (patch.status === 'failed' && attempt < maxAttempts && !this.#closed) {\n const baseDelay = job.record.request.retryDelayMs ?? 5000\n const delay = baseDelay * 2 ** (attempt - 1)\n const updated = await this.#adapter.update(job.record.info.id, {\n status: 'queued',\n attempt: attempt + 1,\n nextRunAt: Date.now() + delay,\n error: patch.error,\n usage,\n sessionId: undefined,\n sdkSessionId: undefined,\n startedAt: undefined,\n result: undefined,\n })\n if (updated) {\n job.record = updated\n this.#emit(updated, { type: 'job_retrying', job: updated.info, ts: Date.now() }, job)\n const timer = setTimeout(() => {\n this.#retryTimers.delete(timer)\n void this.#pump()\n }, delay)\n timer.unref?.()\n this.#retryTimers.add(timer)\n }\n void this.#pump()\n return\n }\n const updated = await this.#adapter.update(job.record.info.id, {\n ...patch,\n usage,\n nextRunAt: undefined,\n finishedAt: Date.now(),\n })\n if (updated) {\n job.record = updated\n this.#emit(job.record, { type: 'job_completed', job: updated.info, ts: Date.now() }, job)\n }\n this.#sweep()\n void this.#pump()\n }\n\n #progress(job: RunningJob, progress: JobProgress): void {\n const event: JobEvent = { type: 'job_progress', job: job.record.info, progress, ts: Date.now() }\n if (job.record.request.webhook?.progress === 'completion') {\n try {\n this.#options.onEvent?.(event)\n } catch {}\n return\n }\n this.#emit(job.record, event, job)\n }\n\n #emit(record: JobRecord, event: JobEvent, chainOwner?: RunningJob, { skipWebhook = false }: { skipWebhook?: boolean } = {}): void {\n try {\n this.#options.onEvent?.(event)\n } catch {}\n const webhook = record.request.webhook\n if (!webhook || skipWebhook) {\n return\n }\n const running = chainOwner ?? this.#running.get(record.info.id)\n const deliver = () => this.#deliver(webhook.url, webhook.headers, event)\n if (running) {\n running.deliveries = running.deliveries.then(deliver)\n } else {\n void deliver()\n }\n }\n\n async #deliver(url: string, headers: Record<string, string> | undefined, event: JobEvent): Promise<void> {\n const fetchImpl = this.#options.fetchImpl ?? fetch\n const attempts = this.#options.webhookAttempts ?? 3\n const baseDelay = this.#options.webhookRetryDelayMs ?? 500\n for (let attempt = 0; attempt < attempts; attempt++) {\n try {\n const res = await fetchImpl(url, {\n method: 'POST',\n headers: { 'content-type': 'application/json', ...headers },\n body: JSON.stringify(event),\n })\n if (res.ok) {\n return\n }\n } catch {}\n if (attempt < attempts - 1) {\n await new Promise((resolve) => setTimeout(resolve, baseDelay * 2 ** attempt))\n }\n }\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/adapter.ts","../src/queue.ts"],"sourcesContent":["import type { CreateJobRequest, JobInfo } from '@workerdeck/protocol'\n\nexport type JobRecord = {\n info: JobInfo\n request: CreateJobRequest\n}\n\nexport interface QueueAdapter {\n add(job: JobRecord): Promise<void>\n claimNext(): Promise<JobRecord | null>\n get(id: string): Promise<JobRecord | null>\n list(): Promise<JobRecord[]>\n update(id: string, patch: Partial<JobInfo>): Promise<JobRecord | null>\n prune(olderThanMs: number): Promise<number>\n addDailyTokens(dayKey: string, tokens: number): Promise<number>\n dailyTokens(dayKey: string): Promise<number>\n nextRunAt?(): Promise<number | undefined>\n onWork?(listener: () => void): () => void\n}\n\nexport function earliestRunAt(jobs: Iterable<JobRecord>, now: number): number | undefined {\n let earliest: number | undefined\n for (const { info } of jobs) {\n if (info.status === 'queued' && info.nextRunAt !== undefined && info.nextRunAt > now) {\n earliest = earliest === undefined ? info.nextRunAt : Math.min(earliest, info.nextRunAt)\n }\n }\n return earliest\n}\n\nexport class InMemoryQueueAdapter implements QueueAdapter {\n #jobs = new Map<string, JobRecord>()\n #dailyTokens = new Map<string, number>()\n\n add(job: JobRecord): Promise<void> {\n this.#jobs.set(job.info.id, job)\n return Promise.resolve()\n }\n\n claimNext(): Promise<JobRecord | null> {\n const now = Date.now()\n for (const job of this.#jobs.values()) {\n if (job.info.status === 'queued' && (job.info.nextRunAt === undefined || job.info.nextRunAt <= now)) {\n job.info = { ...job.info, status: 'running' }\n return Promise.resolve(job)\n }\n }\n return Promise.resolve(null)\n }\n\n get(id: string): Promise<JobRecord | null> {\n return Promise.resolve(this.#jobs.get(id) ?? null)\n }\n\n list(): Promise<JobRecord[]> {\n return Promise.resolve([...this.#jobs.values()])\n }\n\n update(id: string, patch: Partial<JobInfo>): Promise<JobRecord | null> {\n const job = this.#jobs.get(id)\n if (!job) {\n return Promise.resolve(null)\n }\n job.info = { ...job.info, ...patch }\n return Promise.resolve(job)\n }\n\n prune(olderThanMs: number): Promise<number> {\n const cutoff = Date.now() - olderThanMs\n let removed = 0\n for (const [id, job] of this.#jobs) {\n const { status, finishedAt } = job.info\n const terminal = status === 'succeeded' || status === 'failed' || status === 'canceled'\n if (terminal && (finishedAt ?? 0) <= cutoff) {\n this.#jobs.delete(id)\n removed++\n }\n }\n return Promise.resolve(removed)\n }\n\n addDailyTokens(dayKey: string, tokens: number): Promise<number> {\n const next = (this.#dailyTokens.get(dayKey) ?? 0) + tokens\n this.#dailyTokens.set(dayKey, next)\n return Promise.resolve(next)\n }\n\n dailyTokens(dayKey: string): Promise<number> {\n return Promise.resolve(this.#dailyTokens.get(dayKey) ?? 0)\n }\n\n nextRunAt(): Promise<number | undefined> {\n return Promise.resolve(earliestRunAt(this.#jobs.values(), Date.now()))\n }\n}\n","import { randomUUID } from 'node:crypto'\nimport type { Runner, SessionRunnerConfig } from '@workerdeck/core'\nimport {\n pickCreateSessionRequest,\n type ApiMessage,\n type CreateJobRequest,\n type CreateSessionRequest,\n type JobEvent,\n type JobInfo,\n type JobProgress,\n type JobUsage,\n type QueueStats,\n type SessionEvent,\n} from '@workerdeck/protocol'\nimport { earliestRunAt, InMemoryQueueAdapter, type JobRecord, type QueueAdapter } from './adapter.ts'\n\nexport type JobQueueOptions = {\n createRunner: (config: SessionRunnerConfig) => Runner | Promise<Runner>\n adapter?: QueueAdapter\n maxConcurrency?: number\n sessionTokenLimit?: number\n dailyTokenLimit?: number\n maxJobDurationMs?: number\n maxParkedDurationMs?: number\n killGraceMs?: number\n retention?: { maxAgeMs: number; sweepIntervalMs?: number }\n buildRunnerConfig?: (req: CreateSessionRequest) => SessionRunnerConfig\n discardSession?: (sessionId: string) => void | Promise<void>\n fetchImpl?: typeof fetch\n webhookAttempts?: number\n webhookRetryDelayMs?: number\n onEvent?: (event: JobEvent) => void\n onError?: (error: unknown, context: JobQueueErrorContext) => void\n}\n\nexport type JobQueueErrorPhase = 'pump' | 'start' | 'event' | 'finalize' | 'park' | 'resume'\n\nexport type JobQueueErrorContext = { jobId?: string; phase: JobQueueErrorPhase }\n\ntype StartingJob = { canceled: boolean }\n\ntype RunningJob = {\n record: JobRecord\n runner: Runner\n unsubscribe: () => void\n lastSeq: number\n estimatedTokens: number\n killReason?: string\n canceled: boolean\n finalized: boolean\n deliveries: Promise<void>\n durationTimer?: ReturnType<typeof setTimeout>\n forceTimer?: ReturnType<typeof setTimeout>\n runningMs: number\n legStartedAt: number\n parkedAt?: number\n parkedExecutionId?: string\n parkTimer?: ReturnType<typeof setTimeout>\n parkedMs: number\n}\n\nconst MAX_TIMER_MS = 2 ** 31 - 1\n\nconst PUMP_RETRY_MS = 5_000\n\nfunction nextUtcMidnight(epochMs: number): number {\n const now = new Date(epochMs)\n return Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() + 1)\n}\n\nfunction reportToConsole(error: unknown, context: JobQueueErrorContext): void {\n const job = context.jobId ? ` (job ${context.jobId})` : ''\n console.warn(`[workerdeck] queue ${context.phase} failed${job}: ${error instanceof Error ? error.message : String(error)}`)\n}\n\nfunction dayKey(epochMs: number): string {\n return new Date(epochMs).toISOString().slice(0, 10)\n}\n\n// An attempt that reported no priced figure at all stays unknown rather than banking a zero:\n// only the claude engine reports a cost of its own, and `$0.00` is a claim.\nfunction addOptionalCost(prior: number | undefined, attempt: number | undefined): number | undefined {\n if (prior === undefined && attempt === undefined) {\n return undefined\n }\n return (prior ?? 0) + (attempt ?? 0)\n}\n\nfunction sumUsage(usage: unknown): number {\n if (typeof usage !== 'object' || usage === null) {\n return 0\n }\n const u = usage as Record<string, unknown>\n return (\n (typeof u.input_tokens === 'number' ? u.input_tokens : 0) +\n (typeof u.output_tokens === 'number' ? u.output_tokens : 0) +\n (typeof u.cache_creation_input_tokens === 'number' ? u.cache_creation_input_tokens : 0) +\n (typeof u.cache_read_input_tokens === 'number' ? u.cache_read_input_tokens : 0)\n )\n}\n\nfunction textPreview(message: ApiMessage, max = 140): JobProgress | null {\n const blocks = typeof message.content === 'string' ? [{ type: 'text', text: message.content }] : message.content\n for (const block of blocks) {\n if (block.type === 'tool_use') {\n return { kind: 'tool_use', preview: (block as { name?: string }).name }\n }\n if (block.type === 'text') {\n const text = (block as { text?: string }).text ?? ''\n if (text.trim()) {\n return {\n kind: 'assistant_text',\n preview: text.length > max ? text.slice(0, max - 1) + '…' : text,\n }\n }\n }\n }\n return null\n}\n\nexport class JobQueue {\n #options: JobQueueOptions\n #adapter: QueueAdapter\n #running = new Map<string, RunningJob>()\n #parked = new Map<string, RunningJob>()\n #starting = new Map<string, StartingJob>()\n #pumping = false\n #pumpRequested = false\n #paused = false\n #closed = false\n #wakeTimer: ReturnType<typeof setTimeout> | undefined\n #wakeAt = Number.POSITIVE_INFINITY\n #offWork: (() => void) | undefined\n #sweepTimer: ReturnType<typeof setInterval> | undefined\n #retryTimers = new Set<ReturnType<typeof setTimeout>>()\n\n constructor(options: JobQueueOptions) {\n this.#options = options\n this.#adapter = options.adapter ?? new InMemoryQueueAdapter()\n this.#offWork = this.#adapter.onWork?.(() => this.#pump())\n const retention = options.retention\n if (retention) {\n const interval = retention.sweepIntervalMs ?? Math.min(retention.maxAgeMs, 60_000)\n this.#sweepTimer = setInterval(() => this.#sweep(), interval)\n this.#sweepTimer.unref?.()\n }\n }\n\n async submit(request: CreateJobRequest): Promise<JobInfo> {\n if (this.#closed) {\n throw new Error('queue is closed')\n }\n if (!request.session?.prompt?.trim()) {\n throw new Error('session.prompt is required')\n }\n if (request.session.resume || request.session.forkSession) {\n throw new Error('resume/forkSession are not supported for queued jobs')\n }\n const attempts = request.attempts ?? 1\n if (!Number.isInteger(attempts) || attempts < 1) {\n throw new Error('attempts must be a positive integer')\n }\n if (request.retryDelayMs !== undefined && !(request.retryDelayMs >= 0)) {\n throw new Error('retryDelayMs must be >= 0')\n }\n const info: JobInfo = {\n id: randomUUID(),\n status: 'queued',\n cwd: request.session.cwd ?? '',\n profile: request.session.profile,\n prompt: request.session.prompt,\n createdAt: Date.now(),\n attempt: 1,\n maxAttempts: attempts,\n usage: { tokens: 0, totalCostUsd: 0, numTurns: 0 },\n meta: request.meta,\n scope: request.session.scope,\n }\n const record: JobRecord = { info, request }\n await this.#adapter.add(record)\n this.#emit(record, { type: 'job_submitted', job: info, ts: Date.now() }, undefined, {\n skipWebhook: true,\n })\n this.#pump()\n return info\n }\n\n async get(id: string): Promise<JobInfo | null> {\n return (await this.#adapter.get(id))?.info ?? null\n }\n\n async list(): Promise<JobInfo[]> {\n return (await this.#adapter.list()).map((j) => j.info)\n }\n\n canParkSession(sessionId: string): boolean {\n const job = this.#bySession(this.#running, sessionId)\n return !job || (!job.finalized && !job.killReason)\n }\n\n onSessionParking(sessionId: string, executionId: string): boolean {\n const job = this.#bySession(this.#running, sessionId)\n if (!job) {\n return true\n }\n if (job.finalized || job.killReason) {\n return false\n }\n const now = Date.now()\n job.unsubscribe()\n job.runningMs += now - job.legStartedAt\n clearTimeout(job.durationTimer)\n job.durationTimer = undefined\n job.parkedAt = now\n job.parkedExecutionId = executionId\n this.#running.delete(job.record.info.id)\n this.#parked.set(job.record.info.id, job)\n this.#armLimit(job, 'parkTimer', this.#options.maxParkedDurationMs, job.parkedMs, 'max parked duration')\n const patch: Partial<JobInfo> = { status: 'parked', parkedAt: job.parkedAt, parkedExecutionId: executionId }\n this.#spawn('park', job.record.info.id, async () => {\n await this.#recordTransition(job, patch, (info) => ({ type: 'job_parked', job: info, executionId, ts: Date.now() }))\n this.#pump()\n })\n return true\n }\n\n onSessionResumed(sessionId: string, runner: Runner): void {\n const job = this.#bySession(this.#parked, sessionId)\n if (!job || job.finalized) {\n return\n }\n const now = Date.now()\n const executionId = job.parkedExecutionId ?? ''\n clearTimeout(job.parkTimer)\n job.parkTimer = undefined\n job.parkedMs += now - (job.parkedAt ?? now)\n job.parkedAt = undefined\n job.parkedExecutionId = undefined\n job.legStartedAt = now\n job.runner = runner\n this.#parked.delete(job.record.info.id)\n this.#running.set(job.record.info.id, job)\n this.#armLimit(job, 'durationTimer', this.#effectiveDurationLimit(job.record.request), job.runningMs, 'max duration')\n job.unsubscribe = this.#subscribe(job, job.lastSeq)\n const patch: Partial<JobInfo> = { status: 'running', parkedAt: undefined, parkedExecutionId: undefined }\n this.#spawn('resume', job.record.info.id, () =>\n this.#recordTransition(job, patch, (info) => ({ type: 'job_resumed', job: info, executionId, ts: Date.now() })),\n )\n }\n\n async #recordTransition(job: RunningJob, patch: Partial<JobInfo>, event: (info: JobInfo) => JobEvent): Promise<void> {\n const updated = await this.#adapter.update(job.record.info.id, patch)\n if (updated) {\n job.record = updated\n }\n this.#emit(job.record, event(job.record.info), job)\n }\n\n #armLimit(job: RunningJob, key: 'durationTimer' | 'parkTimer', limit: number | undefined, spentMs: number, label: string): void {\n if (limit === undefined) {\n return\n }\n const timer = setTimeout(() => this.#kill(job, `job exceeded ${label} (${limit}ms)`), Math.max(0, limit - spentMs))\n timer.unref?.()\n job[key] = timer\n }\n\n #bySession(jobs: Map<string, RunningJob>, sessionId: string): RunningJob | undefined {\n for (const job of jobs.values()) {\n if (job.record.info.sessionId === sessionId) {\n return job\n }\n }\n return undefined\n }\n\n async cancel(id: string): Promise<JobInfo | null> {\n const record = await this.#adapter.get(id)\n if (!record) {\n return null\n }\n const running = this.#running.get(id) ?? this.#parked.get(id)\n if (running) {\n running.canceled = true\n running.killReason = 'canceled'\n await this.#finalize(running, this.#abortPatch(running, 'canceled'))\n return running.record.info\n }\n const starting = this.#starting.get(id)\n if (starting) {\n if (starting.canceled) {\n return record.info\n }\n starting.canceled = true\n return this.#markCanceled(id)\n }\n if (record.info.status !== 'queued') {\n return record.info\n }\n return this.#markCanceled(id)\n }\n\n async #markCanceled(id: string): Promise<JobInfo | null> {\n const updated = await this.#adapter.update(id, {\n status: 'canceled',\n finishedAt: Date.now(),\n error: 'canceled',\n })\n if (updated) {\n this.#emit(updated, { type: 'job_completed', job: updated.info, ts: Date.now() })\n }\n return updated?.info ?? null\n }\n\n pause(): void {\n this.#paused = true\n }\n\n resume(): void {\n if (!this.#paused) {\n return\n }\n this.#paused = false\n this.#pump()\n }\n\n get paused(): boolean {\n return this.#paused\n }\n\n async stats(): Promise<QueueStats> {\n const jobs = await this.#adapter.list()\n const dailyTokensUsed = await this.#adapter.dailyTokens(dayKey(Date.now()))\n const dailyTokenLimit = this.#options.dailyTokenLimit\n return {\n maxConcurrency: this.#options.maxConcurrency ?? 1,\n running: this.#running.size + this.#starting.size,\n parked: this.#parked.size,\n queued: jobs.filter((j) => j.info.status === 'queued').length,\n sessionTokenLimit: this.#options.sessionTokenLimit,\n dailyTokenLimit,\n dailyTokensUsed,\n paused: this.#paused || (dailyTokenLimit !== undefined && dailyTokensUsed >= dailyTokenLimit),\n }\n }\n\n close(): void {\n this.#closed = true\n this.#offWork?.()\n clearInterval(this.#sweepTimer)\n clearTimeout(this.#wakeTimer)\n this.#wakeTimer = undefined\n for (const timer of this.#retryTimers) {\n clearTimeout(timer)\n }\n this.#retryTimers.clear()\n }\n\n #sweep(): void {\n const retention = this.#options.retention\n if (!retention) {\n return\n }\n this.#adapter.prune(retention.maxAgeMs).catch(() => {})\n }\n\n #pump(): void {\n if (this.#closed || this.#paused) {\n return\n }\n if (this.#pumping) {\n this.#pumpRequested = true\n return\n }\n this.#pumping = true\n this.#spawn('pump', undefined, () => this.#drainRequests())\n }\n\n async #drainRequests(): Promise<void> {\n try {\n do {\n this.#pumpRequested = false\n await this.#fill()\n } while (this.#pumpRequested && !this.#closed && !this.#paused)\n } catch (error) {\n this.#wake(Date.now() + PUMP_RETRY_MS)\n throw error\n } finally {\n this.#pumping = false\n }\n }\n\n async #fill(): Promise<void> {\n const maxConcurrency = this.#options.maxConcurrency ?? 1\n while (this.#running.size + this.#starting.size < maxConcurrency && !this.#closed && !this.#paused) {\n const limit = this.#options.dailyTokenLimit\n if (limit !== undefined && (await this.#adapter.dailyTokens(dayKey(Date.now()))) >= limit) {\n this.#wake(nextUtcMidnight(Date.now()))\n return\n }\n const record = await this.#adapter.claimNext()\n if (!record) {\n const next = await this.#nextRunAt()\n if (next !== undefined) {\n this.#wake(next)\n }\n return\n }\n if (this.#closed || this.#paused) {\n await this.#adapter.update(record.info.id, { status: 'queued' })\n return\n }\n const id = record.info.id\n this.#starting.set(id, { canceled: false })\n this.#spawn('start', id, () => this.#start(record))\n }\n }\n\n async #nextRunAt(): Promise<number | undefined> {\n if (this.#adapter.nextRunAt) {\n return this.#adapter.nextRunAt()\n }\n return earliestRunAt(await this.#adapter.list(), Date.now())\n }\n\n #wake(at: number): void {\n if (this.#closed || (this.#wakeTimer !== undefined && this.#wakeAt <= at)) {\n return\n }\n clearTimeout(this.#wakeTimer)\n this.#wakeAt = at\n this.#wakeTimer = setTimeout(\n () => {\n this.#wakeTimer = undefined\n this.#wakeAt = Number.POSITIVE_INFINITY\n this.#pump()\n },\n Math.min(MAX_TIMER_MS, Math.max(0, at - Date.now())),\n )\n this.#wakeTimer.unref?.()\n }\n\n async #start(record: JobRecord): Promise<void> {\n const id = record.info.id\n const starting = this.#starting.get(id) ?? { canceled: false }\n const build = this.#options.buildRunnerConfig ?? ((req: CreateSessionRequest) => req)\n let runner: Runner\n try {\n // Projected again here, not only at the HTTP door: a durable adapter can hold a record written before the door did.\n const request: CreateSessionRequest = {\n ...pickCreateSessionRequest(record.request.session),\n meta: { ...record.request.session.meta, jobId: id },\n }\n runner = await this.#options.createRunner(build(request))\n } catch (error) {\n this.#starting.delete(id)\n this.#pump()\n if (starting.canceled) {\n return\n }\n const failed = await this.#adapter.update(id, {\n status: 'failed',\n finishedAt: Date.now(),\n error: error instanceof Error ? error.message : String(error),\n })\n if (failed) {\n this.#emit(failed, { type: 'job_completed', job: failed.info, ts: Date.now() })\n }\n return\n }\n this.#starting.delete(id)\n if (starting.canceled) {\n runner.close('server')\n this.#pump()\n return\n }\n const job: RunningJob = {\n record,\n runner,\n unsubscribe: () => {},\n lastSeq: 0,\n estimatedTokens: 0,\n canceled: false,\n finalized: false,\n deliveries: Promise.resolve(),\n runningMs: 0,\n legStartedAt: Date.now(),\n parkedMs: 0,\n }\n this.#running.set(id, job)\n const updated = await this.#adapter.update(id, { startedAt: Date.now(), sessionId: runner.id }).catch((error: unknown) => {\n this.#report(error, { jobId: id, phase: 'start' })\n return null\n })\n if (updated) {\n job.record = updated\n }\n this.#emit(job.record, { type: 'job_started', job: job.record.info, ts: Date.now() })\n this.#armLimit(job, 'durationTimer', this.#effectiveDurationLimit(record.request), 0, 'max duration')\n job.unsubscribe = this.#subscribe(job)\n }\n\n #subscribe(job: RunningJob, afterSeq?: number): () => void {\n return job.runner.subscribe((event) => this.#spawn('event', job.record.info.id, () => this.#handleEvent(job, event)), afterSeq)\n }\n\n #spawn(phase: JobQueueErrorPhase, jobId: string | undefined, run: () => Promise<void>): void {\n try {\n run().catch((error: unknown) => this.#report(error, { jobId, phase }))\n } catch (error) {\n this.#report(error, { jobId, phase })\n }\n }\n\n #report(error: unknown, context: JobQueueErrorContext): void {\n try {\n ;(this.#options.onError ?? reportToConsole)(error, context)\n } catch {}\n }\n\n // How a run that never produced a `turn_result` is written down. There is no authoritative\n // usage for such a run, so it reports the tokens counted from the message stream, no cost\n // and no turns. `turn_result` is the one path that does NOT come through here - it has real\n // numbers - which is exactly the distinction five inline copies of this used to hide.\n #abortPatch(job: RunningJob, error: string): Partial<JobInfo> {\n return {\n usage: { tokens: job.estimatedTokens, totalCostUsd: 0, numTurns: 0 },\n status: job.canceled ? 'canceled' : 'failed',\n error,\n }\n }\n\n #kill(job: RunningJob, reason: string): void {\n if (job.finalized || job.killReason) {\n return\n }\n job.killReason = reason\n const id = job.record.info.id\n if (job.parkedAt !== undefined) {\n this.#spawn('finalize', id, () => this.#finalize(job, this.#abortPatch(job, reason)))\n return\n }\n void job.runner.interrupt().catch(() => {})\n job.forceTimer = setTimeout(() => {\n this.#spawn('finalize', id, () => this.#finalize(job, this.#abortPatch(job, reason)))\n }, this.#options.killGraceMs ?? 5000)\n job.forceTimer.unref?.()\n }\n\n async #handleEvent(job: RunningJob, event: SessionEvent): Promise<void> {\n if (job.finalized) {\n return\n }\n job.lastSeq = Math.max(job.lastSeq, event.seq)\n switch (event.type) {\n case 'system_init': {\n await this.#adapter.update(job.record.info.id, { sdkSessionId: event.sdkSessionId })\n return\n }\n case 'assistant_message': {\n if (event.replay) {\n return\n }\n job.estimatedTokens += sumUsage(event.message.usage)\n const limit = this.#effectiveTokenLimit(job.record.request)\n if (limit !== undefined && job.estimatedTokens > limit) {\n this.#kill(job, `session token limit exceeded (${job.estimatedTokens} > ${limit})`)\n }\n const progress = textPreview(event.message)\n if (progress) {\n this.#progress(job, progress)\n }\n return\n }\n case 'permission_requested': {\n this.#progress(job, {\n kind: 'permission_requested',\n preview: event.request.title ?? event.request.toolName,\n request: event.request,\n })\n return\n }\n case 'permission_resolved': {\n this.#progress(job, { kind: 'permission_resolved', preview: event.behavior })\n return\n }\n case 'turn_result': {\n const tokens = sumUsage(event.usage) || job.estimatedTokens\n await this.#finalize(job, {\n usage: {\n tokens,\n totalCostUsd: event.totalCostUsd,\n numTurns: event.numTurns,\n costUsd: event.costUsd,\n },\n result: {\n subtype: event.subtype,\n isError: event.isError,\n result: event.result,\n errors: event.errors,\n durationMs: event.durationMs,\n },\n status: job.killReason ? (job.canceled ? 'canceled' : 'failed') : event.isError ? 'failed' : 'succeeded',\n error: job.killReason ?? (event.isError ? event.errors?.join('; ') || event.subtype : undefined),\n })\n return\n }\n case 'session_error': {\n await this.#finalize(job, this.#abortPatch(job, job.killReason ?? event.message))\n return\n }\n case 'session_closed': {\n await this.#finalize(job, this.#abortPatch(job, job.killReason ?? 'session closed before completing'))\n return\n }\n default: {\n return\n }\n }\n }\n\n #effectiveTokenLimit(request: CreateJobRequest): number | undefined {\n return tightest(request.maxTokens, this.#options.sessionTokenLimit)\n }\n\n #effectiveDurationLimit(request: CreateJobRequest): number | undefined {\n return tightest(request.maxDurationMs, this.#options.maxJobDurationMs)\n }\n\n async #finalize(job: RunningJob, patch: Partial<JobInfo>): Promise<void> {\n if (job.finalized) {\n return\n }\n try {\n await this.#settle(job, patch)\n } finally {\n this.#pump()\n }\n }\n\n async #settle(job: RunningJob, patch: Partial<JobInfo>): Promise<void> {\n job.finalized = true\n job.unsubscribe()\n clearTimeout(job.durationTimer)\n clearTimeout(job.forceTimer)\n clearTimeout(job.parkTimer)\n this.#running.delete(job.record.info.id)\n const wasParked = this.#parked.delete(job.record.info.id)\n job.runner.close('server')\n if (wasParked && job.record.info.sessionId) {\n try {\n void Promise.resolve(this.#options.discardSession?.(job.record.info.sessionId)).catch(() => {})\n } catch {}\n }\n const attemptUsage = patch.usage ?? { tokens: 0, totalCostUsd: 0, numTurns: 0 }\n if (attemptUsage.tokens > 0) {\n await this.#adapter.addDailyTokens(dayKey(Date.now()), attemptUsage.tokens)\n }\n const prior = job.record.info.usage\n const usage: JobUsage = {\n tokens: prior.tokens + attemptUsage.tokens,\n totalCostUsd: prior.totalCostUsd + attemptUsage.totalCostUsd,\n numTurns: prior.numTurns + attemptUsage.numTurns,\n costUsd: addOptionalCost(prior.costUsd, attemptUsage.costUsd),\n }\n const attempt = job.record.info.attempt ?? 1\n const maxAttempts = job.record.request.attempts ?? 1\n if (patch.status === 'failed' && attempt < maxAttempts && !this.#closed) {\n const baseDelay = job.record.request.retryDelayMs ?? 5000\n const delay = baseDelay * 2 ** (attempt - 1)\n const updated = await this.#adapter.update(job.record.info.id, {\n status: 'queued',\n attempt: attempt + 1,\n nextRunAt: Date.now() + delay,\n error: patch.error,\n usage,\n sessionId: undefined,\n sdkSessionId: undefined,\n startedAt: undefined,\n result: undefined,\n })\n if (updated) {\n job.record = updated\n this.#emit(updated, { type: 'job_retrying', job: updated.info, ts: Date.now() }, job)\n const timer = setTimeout(() => {\n this.#retryTimers.delete(timer)\n this.#pump()\n }, delay)\n timer.unref?.()\n this.#retryTimers.add(timer)\n }\n return\n }\n const updated = await this.#adapter.update(job.record.info.id, {\n ...patch,\n usage,\n nextRunAt: undefined,\n finishedAt: Date.now(),\n })\n if (updated) {\n job.record = updated\n this.#emit(job.record, { type: 'job_completed', job: updated.info, ts: Date.now() }, job)\n }\n this.#sweep()\n }\n\n #progress(job: RunningJob, progress: JobProgress): void {\n const event: JobEvent = { type: 'job_progress', job: job.record.info, progress, ts: Date.now() }\n if (job.record.request.webhook?.progress === 'completion') {\n try {\n this.#options.onEvent?.(event)\n } catch {}\n return\n }\n this.#emit(job.record, event, job)\n }\n\n #emit(record: JobRecord, event: JobEvent, chainOwner?: RunningJob, { skipWebhook = false }: { skipWebhook?: boolean } = {}): void {\n try {\n this.#options.onEvent?.(event)\n } catch {}\n const webhook = record.request.webhook\n if (!webhook || skipWebhook) {\n return\n }\n const running = chainOwner ?? this.#running.get(record.info.id)\n const deliver = () => this.#deliver(webhook.url, webhook.headers, event)\n if (running) {\n running.deliveries = running.deliveries.then(deliver)\n } else {\n void deliver()\n }\n }\n\n async #deliver(url: string, headers: Record<string, string> | undefined, event: JobEvent): Promise<void> {\n const fetchImpl = this.#options.fetchImpl ?? fetch\n const attempts = this.#options.webhookAttempts ?? 3\n const baseDelay = this.#options.webhookRetryDelayMs ?? 500\n for (let attempt = 0; attempt < attempts; attempt++) {\n try {\n const res = await fetchImpl(url, {\n method: 'POST',\n headers: { 'content-type': 'application/json', ...headers },\n body: JSON.stringify(event),\n })\n if (res.ok) {\n return\n }\n } catch {}\n if (attempt < attempts - 1) {\n await new Promise((resolve) => setTimeout(resolve, baseDelay * 2 ** attempt))\n }\n }\n }\n}\n\nfunction tightest(...limits: (number | undefined)[]): number | undefined {\n const set = limits.filter((n): n is number => typeof n === 'number')\n return set.length > 0 ? Math.min(...set) : undefined\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@workerdeck/queue",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Job queue over the WorkerDeck session runner: remote services schedule one-shot runs; the queue executes them as sessions with bounded concurrency and token budgets, delivering progress and completion via webhooks. Pluggable adapter interface (in-memory bundled; redis/bullmq/pubsub adapters can implement the same contract).",
|
|
6
6
|
"license": "MIT",
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
}
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@workerdeck/core": "
|
|
21
|
-
"@workerdeck/protocol": "
|
|
20
|
+
"@workerdeck/core": "3.1.0",
|
|
21
|
+
"@workerdeck/protocol": "3.1.0"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"@anthropic-ai/claude-agent-sdk": "^0.3.280",
|