nuxt-cf-jobs 0.0.2
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 +331 -0
- package/dist/module.d.mts +73 -0
- package/dist/module.json +9 -0
- package/dist/module.mjs +380 -0
- package/dist/runtime/server/app.d.ts +80 -0
- package/dist/runtime/server/app.js +81 -0
- package/dist/runtime/server/d1.d.ts +119 -0
- package/dist/runtime/server/d1.js +259 -0
- package/dist/runtime/server/dev.d.ts +39 -0
- package/dist/runtime/server/dev.js +93 -0
- package/dist/runtime/server/dispatch.d.ts +25 -0
- package/dist/runtime/server/dispatch.js +66 -0
- package/dist/runtime/server/index.d.ts +12 -0
- package/dist/runtime/server/index.js +12 -0
- package/dist/runtime/server/outbox.d.ts +220 -0
- package/dist/runtime/server/outbox.js +246 -0
- package/dist/runtime/server/payload.d.ts +3 -0
- package/dist/runtime/server/payload.js +3 -0
- package/dist/runtime/server/plugins/dev-queues.d.ts +2 -0
- package/dist/runtime/server/plugins/dev-queues.js +25 -0
- package/dist/runtime/server/policy.d.ts +10 -0
- package/dist/runtime/server/policy.js +49 -0
- package/dist/runtime/server/queue.d.ts +211 -0
- package/dist/runtime/server/queue.js +495 -0
- package/dist/runtime/server/registry.d.ts +79 -0
- package/dist/runtime/server/registry.js +82 -0
- package/dist/runtime/server/schema.d.ts +965 -0
- package/dist/runtime/server/schema.js +80 -0
- package/dist/runtime/server/testing.d.ts +34 -0
- package/dist/runtime/server/testing.js +61 -0
- package/dist/runtime/server/types.d.ts +123 -0
- package/dist/runtime/server/types.js +0 -0
- package/dist/types.d.mts +3 -0
- package/package.json +109 -0
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
export const d1DurableJobMigrationSql = [
|
|
2
|
+
"CREATE TABLE IF NOT EXISTS job_batches (id text PRIMARY KEY, name text, parent_batch_id text, total_jobs integer NOT NULL DEFAULT 0, pending_jobs integer NOT NULL DEFAULT 0, failed_jobs integer NOT NULL DEFAULT 0, on_finish text, handler text, allow_failures integer DEFAULT 0, site_id text, user_id integer, created_at integer NOT NULL DEFAULT (unixepoch()), updated_at integer NOT NULL DEFAULT (unixepoch()), finished_at integer)",
|
|
3
|
+
"CREATE TABLE IF NOT EXISTS jobs (id text PRIMARY KEY, queue text NOT NULL, job_type text NOT NULL, batch_id text REFERENCES job_batches(id), user_id integer, site_id text, partner_id text, trace_id text, unique_key text, payload text NOT NULL, attempts integer DEFAULT 0, max_attempts integer DEFAULT 3, reserved_at integer, available_at integer NOT NULL, created_at integer NOT NULL DEFAULT (unixepoch()), completed_at integer, failed_at integer, last_error text, retry_reasons text, rows_fetched integer, rows_inserted integer, d1_rows_read integer, d1_rows_written integer, duration_ms integer)",
|
|
4
|
+
"CREATE TABLE IF NOT EXISTS failed_jobs (id text PRIMARY KEY, queue text NOT NULL, job_type text NOT NULL, batch_id text, user_id integer, site_id text, partner_id text, trace_id text, unique_key text, payload text NOT NULL, exception text NOT NULL, attempts integer NOT NULL, max_attempts integer NOT NULL, failed_at integer NOT NULL)",
|
|
5
|
+
"CREATE INDEX IF NOT EXISTS idx_job_batches_site ON job_batches (site_id)",
|
|
6
|
+
"CREATE INDEX IF NOT EXISTS idx_job_batches_pending ON job_batches (pending_jobs)",
|
|
7
|
+
"CREATE INDEX IF NOT EXISTS idx_job_batches_parent ON job_batches (parent_batch_id)",
|
|
8
|
+
"CREATE INDEX IF NOT EXISTS idx_job_batches_finished_at ON job_batches (finished_at)",
|
|
9
|
+
"CREATE INDEX IF NOT EXISTS idx_jobs_claimable ON jobs (queue, reserved_at, available_at)",
|
|
10
|
+
"CREATE INDEX IF NOT EXISTS idx_jobs_user ON jobs (user_id)",
|
|
11
|
+
"CREATE INDEX IF NOT EXISTS idx_jobs_site ON jobs (site_id)",
|
|
12
|
+
"CREATE INDEX IF NOT EXISTS idx_jobs_partner ON jobs (partner_id)",
|
|
13
|
+
"CREATE INDEX IF NOT EXISTS idx_jobs_type ON jobs (job_type)",
|
|
14
|
+
"CREATE INDEX IF NOT EXISTS idx_jobs_batch ON jobs (batch_id)",
|
|
15
|
+
"CREATE INDEX IF NOT EXISTS idx_jobs_trace ON jobs (trace_id)",
|
|
16
|
+
"CREATE INDEX IF NOT EXISTS idx_jobs_sync_dedup ON jobs (site_id, job_type)",
|
|
17
|
+
"CREATE UNIQUE INDEX IF NOT EXISTS idx_jobs_unique_active ON jobs (unique_key) WHERE unique_key IS NOT NULL AND completed_at IS NULL AND failed_at IS NULL",
|
|
18
|
+
"CREATE INDEX IF NOT EXISTS idx_failed_jobs_queue ON failed_jobs (queue)",
|
|
19
|
+
"CREATE INDEX IF NOT EXISTS idx_failed_jobs_site ON failed_jobs (site_id)",
|
|
20
|
+
"CREATE INDEX IF NOT EXISTS idx_failed_jobs_trace ON failed_jobs (trace_id)",
|
|
21
|
+
"CREATE INDEX IF NOT EXISTS idx_failed_jobs_failed_at ON failed_jobs (failed_at)"
|
|
22
|
+
];
|
|
23
|
+
export function createD1DurableJobRepository(db, opts = {}) {
|
|
24
|
+
const jobsTable = opts.jobsTable ?? "jobs";
|
|
25
|
+
const failedJobsTable = opts.failedJobsTable ?? "failed_jobs";
|
|
26
|
+
const insertJobSql = `
|
|
27
|
+
INSERT OR IGNORE INTO ${jobsTable} (
|
|
28
|
+
id, queue, job_type, batch_id, user_id, site_id, partner_id, trace_id, unique_key, payload,
|
|
29
|
+
attempts, max_attempts, available_at, created_at
|
|
30
|
+
)
|
|
31
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
32
|
+
`;
|
|
33
|
+
function bindInsertJob(record) {
|
|
34
|
+
return db.prepare(insertJobSql).bind(
|
|
35
|
+
record.id,
|
|
36
|
+
record.queue,
|
|
37
|
+
record.jobType,
|
|
38
|
+
record.batchId ?? null,
|
|
39
|
+
record.userId ?? null,
|
|
40
|
+
record.siteId ?? null,
|
|
41
|
+
record.partnerId ?? null,
|
|
42
|
+
record.traceId,
|
|
43
|
+
record.uniqueKey ?? null,
|
|
44
|
+
record.payload,
|
|
45
|
+
record.attempts,
|
|
46
|
+
record.maxAttempts,
|
|
47
|
+
record.availableAt,
|
|
48
|
+
record.createdAt
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
async migrate() {
|
|
53
|
+
for (const statement of d1DurableJobMigrationSql)
|
|
54
|
+
await db.exec(statement);
|
|
55
|
+
},
|
|
56
|
+
async insertJob(record) {
|
|
57
|
+
const result = await bindInsertJob(record).run();
|
|
58
|
+
return typeof result.meta?.changes === "number" ? result.meta.changes > 0 : result.success === true;
|
|
59
|
+
},
|
|
60
|
+
async insertJobs(records, insertOpts) {
|
|
61
|
+
if (records.length === 0)
|
|
62
|
+
return { inserted: [], chunks: [] };
|
|
63
|
+
const batchSize = Math.max(1, Math.min(insertOpts?.batchSize ?? 90, 100));
|
|
64
|
+
const chunks = [];
|
|
65
|
+
const inserted = [];
|
|
66
|
+
for (let i = 0; i < records.length; i += batchSize) {
|
|
67
|
+
const slice = records.slice(i, i + batchSize);
|
|
68
|
+
const stmts = slice.map(bindInsertJob);
|
|
69
|
+
try {
|
|
70
|
+
const results = typeof db.batch === "function" ? await db.batch(stmts) : await Promise.all(stmts.map((s) => s.run()));
|
|
71
|
+
const changes = results.reduce((sum, r) => sum + (r.meta?.changes ?? (r.success ? 1 : 0)), 0);
|
|
72
|
+
chunks.push({ ok: true, ids: slice.map((r) => r.id), changes });
|
|
73
|
+
if (changes === slice.length)
|
|
74
|
+
inserted.push(...slice);
|
|
75
|
+
else if (changes > 0 && typeof db.batch !== "function") {
|
|
76
|
+
for (let j = 0; j < slice.length; j++)
|
|
77
|
+
if ((results[j]?.meta?.changes ?? 0) > 0)
|
|
78
|
+
inserted.push(slice[j]);
|
|
79
|
+
}
|
|
80
|
+
} catch (error) {
|
|
81
|
+
chunks.push({ ok: false, ids: slice.map((r) => r.id), changes: 0, error });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (chunks.length > 0 && chunks.every((c) => !c.ok))
|
|
85
|
+
throw chunks[0].error ?? new Error("All insertJobs chunks failed");
|
|
86
|
+
return { inserted, chunks };
|
|
87
|
+
},
|
|
88
|
+
async claimJob(id) {
|
|
89
|
+
const now = currentUnixSeconds();
|
|
90
|
+
const job = await db.prepare(`
|
|
91
|
+
UPDATE ${jobsTable}
|
|
92
|
+
SET reserved_at = ?, attempts = attempts + 1
|
|
93
|
+
WHERE id = ?
|
|
94
|
+
AND reserved_at IS NULL
|
|
95
|
+
AND available_at <= ?
|
|
96
|
+
AND completed_at IS NULL
|
|
97
|
+
AND failed_at IS NULL
|
|
98
|
+
RETURNING *
|
|
99
|
+
`).bind(now, id, now).first();
|
|
100
|
+
if (job)
|
|
101
|
+
fireHook(() => opts.onJobClaimed?.({ job }));
|
|
102
|
+
return job;
|
|
103
|
+
},
|
|
104
|
+
async resolveClaimMiss(id) {
|
|
105
|
+
const job = await db.prepare(`
|
|
106
|
+
SELECT reserved_at, completed_at, failed_at
|
|
107
|
+
FROM ${jobsTable}
|
|
108
|
+
WHERE id = ?
|
|
109
|
+
`).bind(id).first();
|
|
110
|
+
if (!job)
|
|
111
|
+
return "not-found";
|
|
112
|
+
if (job.completed_at || job.failed_at)
|
|
113
|
+
return "already-resolved";
|
|
114
|
+
return "in-flight";
|
|
115
|
+
},
|
|
116
|
+
async completeJob(job, result) {
|
|
117
|
+
const durationMs = result && typeof result === "object" && "durationMs" in result && typeof result.durationMs === "number" ? result.durationMs : null;
|
|
118
|
+
await db.prepare(`
|
|
119
|
+
UPDATE ${jobsTable}
|
|
120
|
+
SET completed_at = unixepoch(), reserved_at = NULL, duration_ms = COALESCE(?, duration_ms)
|
|
121
|
+
WHERE id = ?
|
|
122
|
+
`).bind(durationMs, job.id).run();
|
|
123
|
+
fireHook(() => opts.onJobCompleted?.({ job, durationMs, result }));
|
|
124
|
+
},
|
|
125
|
+
async failJob(job, error) {
|
|
126
|
+
await db.prepare(`
|
|
127
|
+
INSERT OR REPLACE INTO ${failedJobsTable} (
|
|
128
|
+
id, queue, job_type, batch_id, user_id, site_id, partner_id, trace_id, unique_key, payload,
|
|
129
|
+
exception, attempts, max_attempts, failed_at
|
|
130
|
+
)
|
|
131
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, unixepoch())
|
|
132
|
+
`).bind(
|
|
133
|
+
job.id,
|
|
134
|
+
job.queue,
|
|
135
|
+
job.job_type,
|
|
136
|
+
job.batch_id,
|
|
137
|
+
job.user_id,
|
|
138
|
+
job.site_id,
|
|
139
|
+
job.partner_id,
|
|
140
|
+
job.trace_id,
|
|
141
|
+
job.unique_key,
|
|
142
|
+
job.payload,
|
|
143
|
+
error,
|
|
144
|
+
job.attempts,
|
|
145
|
+
job.max_attempts
|
|
146
|
+
).run();
|
|
147
|
+
await db.prepare(`DELETE FROM ${jobsTable} WHERE id = ?`).bind(job.id).run();
|
|
148
|
+
fireHook(() => opts.onJobFailed?.({ job, error }));
|
|
149
|
+
},
|
|
150
|
+
async releaseJob(job, releaseOpts) {
|
|
151
|
+
await db.prepare(`
|
|
152
|
+
UPDATE ${jobsTable}
|
|
153
|
+
SET reserved_at = NULL, available_at = ?, last_error = COALESCE(?, last_error)
|
|
154
|
+
WHERE id = ?
|
|
155
|
+
`).bind(resolveAvailableAt(releaseOpts), releaseOpts?.error ?? null, job.id).run();
|
|
156
|
+
fireHook(() => opts.onJobReleased?.({ job, opts: releaseOpts }));
|
|
157
|
+
},
|
|
158
|
+
async recordFailure(input) {
|
|
159
|
+
await db.prepare(`
|
|
160
|
+
INSERT OR REPLACE INTO ${failedJobsTable} (
|
|
161
|
+
id, queue, job_type, batch_id, user_id, site_id, partner_id, trace_id, unique_key, payload,
|
|
162
|
+
exception, attempts, max_attempts, failed_at
|
|
163
|
+
)
|
|
164
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, unixepoch())
|
|
165
|
+
`).bind(
|
|
166
|
+
input.id ?? crypto.randomUUID(),
|
|
167
|
+
input.queue,
|
|
168
|
+
input.jobType,
|
|
169
|
+
input.batchId ?? null,
|
|
170
|
+
input.userId ?? null,
|
|
171
|
+
input.siteId ?? null,
|
|
172
|
+
input.partnerId ?? null,
|
|
173
|
+
input.traceId ?? null,
|
|
174
|
+
input.uniqueKey ?? null,
|
|
175
|
+
input.payload,
|
|
176
|
+
input.exception,
|
|
177
|
+
input.attempts,
|
|
178
|
+
input.maxAttempts ?? input.attempts
|
|
179
|
+
).run();
|
|
180
|
+
},
|
|
181
|
+
async findDispatchableJobs(query = {}) {
|
|
182
|
+
return await all(db.prepare(`
|
|
183
|
+
SELECT *
|
|
184
|
+
FROM ${jobsTable}
|
|
185
|
+
WHERE reserved_at IS NULL
|
|
186
|
+
AND available_at <= ?
|
|
187
|
+
AND completed_at IS NULL
|
|
188
|
+
AND failed_at IS NULL
|
|
189
|
+
ORDER BY available_at ASC
|
|
190
|
+
LIMIT ?
|
|
191
|
+
`).bind(query.now ?? currentUnixSeconds(), query.limit ?? 100));
|
|
192
|
+
},
|
|
193
|
+
async findStaleReservedJobs(query) {
|
|
194
|
+
return await all(db.prepare(`
|
|
195
|
+
SELECT *
|
|
196
|
+
FROM ${jobsTable}
|
|
197
|
+
WHERE reserved_at IS NOT NULL
|
|
198
|
+
AND reserved_at <= ?
|
|
199
|
+
AND completed_at IS NULL
|
|
200
|
+
AND failed_at IS NULL
|
|
201
|
+
ORDER BY reserved_at ASC
|
|
202
|
+
LIMIT ?
|
|
203
|
+
`).bind(query.staleBefore, query.limit ?? 100));
|
|
204
|
+
},
|
|
205
|
+
async releaseStaleReservedJobs(query) {
|
|
206
|
+
const result = await db.prepare(`
|
|
207
|
+
UPDATE ${jobsTable}
|
|
208
|
+
SET reserved_at = NULL, available_at = ?, last_error = COALESCE(?, last_error)
|
|
209
|
+
WHERE id IN (
|
|
210
|
+
SELECT id
|
|
211
|
+
FROM ${jobsTable}
|
|
212
|
+
WHERE reserved_at IS NOT NULL
|
|
213
|
+
AND reserved_at <= ?
|
|
214
|
+
AND completed_at IS NULL
|
|
215
|
+
AND failed_at IS NULL
|
|
216
|
+
LIMIT ?
|
|
217
|
+
)
|
|
218
|
+
`).bind(
|
|
219
|
+
query.availableAt ?? query.now ?? currentUnixSeconds(),
|
|
220
|
+
query.error ?? null,
|
|
221
|
+
query.staleBefore,
|
|
222
|
+
query.limit ?? 100
|
|
223
|
+
).run();
|
|
224
|
+
return result.meta?.changes ?? 0;
|
|
225
|
+
},
|
|
226
|
+
toDispatchableJob(job) {
|
|
227
|
+
return {
|
|
228
|
+
id: job.id,
|
|
229
|
+
queue: job.queue,
|
|
230
|
+
payload: JSON.parse(job.payload),
|
|
231
|
+
attempts: job.attempts,
|
|
232
|
+
batchId: job.batch_id,
|
|
233
|
+
siteId: job.site_id,
|
|
234
|
+
userId: job.user_id
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
function currentUnixSeconds() {
|
|
240
|
+
return Math.floor(Date.now() / 1e3);
|
|
241
|
+
}
|
|
242
|
+
function fireHook(fn) {
|
|
243
|
+
try {
|
|
244
|
+
const result = fn();
|
|
245
|
+
if (result && typeof result.then === "function")
|
|
246
|
+
result.catch(() => {
|
|
247
|
+
});
|
|
248
|
+
} catch {
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
function resolveAvailableAt(opts) {
|
|
252
|
+
if (typeof opts?.availableAt === "number")
|
|
253
|
+
return opts.availableAt;
|
|
254
|
+
return currentUnixSeconds() + (opts?.delaySeconds ?? 0);
|
|
255
|
+
}
|
|
256
|
+
async function all(statement) {
|
|
257
|
+
const result = await statement.all?.();
|
|
258
|
+
return result?.results ?? [];
|
|
259
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { QueueBindingsConfig } from './types.js';
|
|
2
|
+
export interface DevQueueMessage {
|
|
3
|
+
id?: string;
|
|
4
|
+
body: Record<string, unknown>;
|
|
5
|
+
attempts: number;
|
|
6
|
+
timestamp?: Date | number;
|
|
7
|
+
ack: () => void;
|
|
8
|
+
retry: (opts?: {
|
|
9
|
+
delaySeconds?: number;
|
|
10
|
+
}) => void;
|
|
11
|
+
}
|
|
12
|
+
export interface DevQueueBatch {
|
|
13
|
+
queue: string;
|
|
14
|
+
messages: DevQueueMessage[];
|
|
15
|
+
ackAll: () => void;
|
|
16
|
+
retryAll: (opts?: {
|
|
17
|
+
delaySeconds?: number;
|
|
18
|
+
}) => void;
|
|
19
|
+
}
|
|
20
|
+
export interface DevQueueHookPayload {
|
|
21
|
+
batch: DevQueueBatch;
|
|
22
|
+
env: Record<string, unknown>;
|
|
23
|
+
}
|
|
24
|
+
export interface DevQueueRuntime {
|
|
25
|
+
env: Record<string, unknown>;
|
|
26
|
+
enqueue: (binding: string, body: Record<string, unknown>, opts?: {
|
|
27
|
+
delaySeconds?: number;
|
|
28
|
+
attempts?: number;
|
|
29
|
+
}) => void;
|
|
30
|
+
dispose: () => void;
|
|
31
|
+
}
|
|
32
|
+
export interface DevQueueRuntimeOptions {
|
|
33
|
+
queues: QueueBindingsConfig;
|
|
34
|
+
baseEnv?: Record<string, unknown>;
|
|
35
|
+
onBatch: (payload: DevQueueHookPayload) => Promise<void> | void;
|
|
36
|
+
onError?: (error: unknown) => void;
|
|
37
|
+
maxAttempts?: number;
|
|
38
|
+
}
|
|
39
|
+
export declare function createDevQueueRuntime(opts: DevQueueRuntimeOptions): DevQueueRuntime;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { resolveCloudflareQueueName } from "./queue.js";
|
|
2
|
+
export function createDevQueueRuntime(opts) {
|
|
3
|
+
const env = { ...opts.baseEnv ?? {} };
|
|
4
|
+
const bindingToLogical = /* @__PURE__ */ new Map();
|
|
5
|
+
const timers = /* @__PURE__ */ new Set();
|
|
6
|
+
const maxAttempts = opts.maxAttempts ?? 10;
|
|
7
|
+
let disposed = false;
|
|
8
|
+
for (const [logicalName, config] of Object.entries(opts.queues)) {
|
|
9
|
+
const binding = typeof config === "string" ? config : config?.binding;
|
|
10
|
+
if (!binding)
|
|
11
|
+
continue;
|
|
12
|
+
bindingToLogical.set(binding, logicalName);
|
|
13
|
+
const queue = {
|
|
14
|
+
async send(message, sendOpts) {
|
|
15
|
+
enqueue(binding, message, { delaySeconds: sendOpts?.delaySeconds });
|
|
16
|
+
},
|
|
17
|
+
async sendBatch(messages, sendOpts) {
|
|
18
|
+
for (const message of messages) {
|
|
19
|
+
enqueue(binding, message.body, {
|
|
20
|
+
delaySeconds: message.delaySeconds ?? sendOpts?.delaySeconds
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
env[binding] = queue;
|
|
26
|
+
}
|
|
27
|
+
function enqueue(binding, body, eopts) {
|
|
28
|
+
if (disposed)
|
|
29
|
+
return;
|
|
30
|
+
const logical = bindingToLogical.get(binding);
|
|
31
|
+
if (!logical)
|
|
32
|
+
return;
|
|
33
|
+
const cfQueueName = resolveCloudflareQueueName(opts.queues, logical);
|
|
34
|
+
const attempts = (eopts?.attempts ?? 0) + 1;
|
|
35
|
+
const delayMs = Math.max(0, (eopts?.delaySeconds ?? 0) * 1e3);
|
|
36
|
+
const id = eopts?.id ?? (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID() : `${Date.now()}-${Math.random()}`);
|
|
37
|
+
const fire = () => {
|
|
38
|
+
let settled = false;
|
|
39
|
+
const message = {
|
|
40
|
+
id,
|
|
41
|
+
body,
|
|
42
|
+
attempts,
|
|
43
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
44
|
+
ack() {
|
|
45
|
+
settled = true;
|
|
46
|
+
},
|
|
47
|
+
retry(retryOpts) {
|
|
48
|
+
if (settled)
|
|
49
|
+
return;
|
|
50
|
+
settled = true;
|
|
51
|
+
if (attempts >= maxAttempts)
|
|
52
|
+
return;
|
|
53
|
+
enqueue(binding, body, { delaySeconds: retryOpts?.delaySeconds, attempts, id });
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
const batch = {
|
|
57
|
+
queue: cfQueueName,
|
|
58
|
+
messages: [message],
|
|
59
|
+
ackAll() {
|
|
60
|
+
settled = true;
|
|
61
|
+
},
|
|
62
|
+
retryAll(retryOpts) {
|
|
63
|
+
if (settled)
|
|
64
|
+
return;
|
|
65
|
+
settled = true;
|
|
66
|
+
if (attempts >= maxAttempts)
|
|
67
|
+
return;
|
|
68
|
+
enqueue(binding, body, { delaySeconds: retryOpts?.delaySeconds, attempts, id });
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
Promise.resolve().then(() => opts.onBatch({ batch, env })).catch((error) => opts.onError?.(error));
|
|
72
|
+
};
|
|
73
|
+
if (delayMs === 0) {
|
|
74
|
+
queueMicrotask(fire);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const timer = setTimeout(() => {
|
|
78
|
+
timers.delete(timer);
|
|
79
|
+
fire();
|
|
80
|
+
}, delayMs);
|
|
81
|
+
timers.add(timer);
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
env,
|
|
85
|
+
enqueue,
|
|
86
|
+
dispose() {
|
|
87
|
+
disposed = true;
|
|
88
|
+
for (const timer of timers)
|
|
89
|
+
clearTimeout(timer);
|
|
90
|
+
timers.clear();
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { DispatchableJob, DispatchResult, JobContext, JobControlResult, JobDefinition, JobHandler, JobMiddleware } from './types.js';
|
|
2
|
+
export interface JobRegistryLike<Env, Db, Logger> {
|
|
3
|
+
getHandler: (name: string) => JobHandler<unknown, Env, Db, Logger> | undefined;
|
|
4
|
+
getJobDefinition?: (name: string) => JobDefinition<string, unknown, string, Env, Db, Logger> | undefined;
|
|
5
|
+
}
|
|
6
|
+
export interface DispatchContextInput<Job extends DispatchableJob> {
|
|
7
|
+
job: Job;
|
|
8
|
+
taskName: string;
|
|
9
|
+
payload: Record<string, unknown>;
|
|
10
|
+
control: JobControlResult;
|
|
11
|
+
}
|
|
12
|
+
export interface DispatchRegisteredJobOptions<Job extends DispatchableJob, Env, Db, Logger> {
|
|
13
|
+
registry: JobRegistryLike<Env, Db, Logger>;
|
|
14
|
+
job: Job;
|
|
15
|
+
createContext: (input: DispatchContextInput<Job>) => JobContext<Env, Db, Logger> | Promise<JobContext<Env, Db, Logger>>;
|
|
16
|
+
onHandledThrow?: (input: DispatchContextInput<Job> & {
|
|
17
|
+
error: unknown;
|
|
18
|
+
}) => void | Promise<void>;
|
|
19
|
+
onUnhandledThrow?: (input: DispatchContextInput<Job> & {
|
|
20
|
+
error: unknown;
|
|
21
|
+
}) => void | Promise<void>;
|
|
22
|
+
onComplete?: (input: DispatchContextInput<Job>) => void | Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
export declare function dispatchRegisteredJob<Job extends DispatchableJob, Env, Db, Logger>(opts: DispatchRegisteredJobOptions<Job, Env, Db, Logger>): Promise<DispatchResult>;
|
|
25
|
+
export declare function runJobThroughMiddleware<Payload, Env, Db, Logger>(payload: Payload, ctx: JobContext<Env, Db, Logger>, middleware: Array<JobMiddleware<Payload, Env, Db, Logger>>, destination: () => Promise<void>): Promise<void>;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { parseJobInput } from "./registry.js";
|
|
2
|
+
export async function dispatchRegisteredJob(opts) {
|
|
3
|
+
const payload = opts.job.payload;
|
|
4
|
+
const taskName = payload._task;
|
|
5
|
+
if (typeof taskName !== "string" || taskName.length === 0) {
|
|
6
|
+
return { success: false, error: "No _task in payload", handlerNotFound: true };
|
|
7
|
+
}
|
|
8
|
+
const definition = opts.registry.getJobDefinition?.(taskName);
|
|
9
|
+
const handler = definition?.handle ?? opts.registry.getHandler(taskName);
|
|
10
|
+
if (!handler) {
|
|
11
|
+
return { success: false, error: `No handler for task: ${taskName}`, handlerNotFound: true };
|
|
12
|
+
}
|
|
13
|
+
const { _task, _continuations, ...cleanPayload } = payload;
|
|
14
|
+
const parsedPayload = parseJobInput(definition, cleanPayload);
|
|
15
|
+
if (!parsedPayload.success) {
|
|
16
|
+
return {
|
|
17
|
+
success: false,
|
|
18
|
+
error: `Invalid payload for task: ${taskName}`,
|
|
19
|
+
invalidPayload: true,
|
|
20
|
+
validationError: parsedPayload.error
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
const control = { handled: false };
|
|
24
|
+
const input = {
|
|
25
|
+
job: opts.job,
|
|
26
|
+
taskName,
|
|
27
|
+
payload: parsedPayload.data,
|
|
28
|
+
control
|
|
29
|
+
};
|
|
30
|
+
const ctx = await opts.createContext(input);
|
|
31
|
+
try {
|
|
32
|
+
await runJobThroughMiddleware(
|
|
33
|
+
parsedPayload.data,
|
|
34
|
+
ctx,
|
|
35
|
+
definition?.middleware ?? [],
|
|
36
|
+
() => handler(parsedPayload.data, ctx)
|
|
37
|
+
);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
if (control.handled) {
|
|
40
|
+
await opts.onHandledThrow?.({ ...input, error });
|
|
41
|
+
return { success: true, control };
|
|
42
|
+
}
|
|
43
|
+
if (definition?.failed) {
|
|
44
|
+
await Promise.resolve(definition.failed(parsedPayload.data, ctx, error)).catch((failedError) => {
|
|
45
|
+
opts.onUnhandledThrow?.({ ...input, error: failedError });
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
await opts.onUnhandledThrow?.({ ...input, error });
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
await opts.onComplete?.(input);
|
|
52
|
+
return { success: true, control: control.handled ? control : void 0 };
|
|
53
|
+
}
|
|
54
|
+
export async function runJobThroughMiddleware(payload, ctx, middleware, destination) {
|
|
55
|
+
let index = -1;
|
|
56
|
+
async function run(i) {
|
|
57
|
+
if (i <= index)
|
|
58
|
+
throw new Error("Job middleware called next() multiple times");
|
|
59
|
+
index = i;
|
|
60
|
+
const layer = middleware[i];
|
|
61
|
+
if (!layer)
|
|
62
|
+
return destination();
|
|
63
|
+
await layer(payload, ctx, () => run(i + 1));
|
|
64
|
+
}
|
|
65
|
+
await run(0);
|
|
66
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export * from './app.js';
|
|
2
|
+
export * from './d1.js';
|
|
3
|
+
export * from './dev.js';
|
|
4
|
+
export * from './dispatch.js';
|
|
5
|
+
export * from './outbox.js';
|
|
6
|
+
export * from './payload.js';
|
|
7
|
+
export * from './policy.js';
|
|
8
|
+
export * from './queue.js';
|
|
9
|
+
export * from './registry.js';
|
|
10
|
+
export * from './schema.js';
|
|
11
|
+
export * from './testing.js';
|
|
12
|
+
export * from './types.js';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export * from "./app.js";
|
|
2
|
+
export * from "./d1.js";
|
|
3
|
+
export * from "./dev.js";
|
|
4
|
+
export * from "./dispatch.js";
|
|
5
|
+
export * from "./outbox.js";
|
|
6
|
+
export * from "./payload.js";
|
|
7
|
+
export * from "./policy.js";
|
|
8
|
+
export * from "./queue.js";
|
|
9
|
+
export * from "./registry.js";
|
|
10
|
+
export * from "./schema.js";
|
|
11
|
+
export * from "./testing.js";
|
|
12
|
+
export * from "./types.js";
|