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.
Files changed (34) hide show
  1. package/README.md +331 -0
  2. package/dist/module.d.mts +73 -0
  3. package/dist/module.json +9 -0
  4. package/dist/module.mjs +380 -0
  5. package/dist/runtime/server/app.d.ts +80 -0
  6. package/dist/runtime/server/app.js +81 -0
  7. package/dist/runtime/server/d1.d.ts +119 -0
  8. package/dist/runtime/server/d1.js +259 -0
  9. package/dist/runtime/server/dev.d.ts +39 -0
  10. package/dist/runtime/server/dev.js +93 -0
  11. package/dist/runtime/server/dispatch.d.ts +25 -0
  12. package/dist/runtime/server/dispatch.js +66 -0
  13. package/dist/runtime/server/index.d.ts +12 -0
  14. package/dist/runtime/server/index.js +12 -0
  15. package/dist/runtime/server/outbox.d.ts +220 -0
  16. package/dist/runtime/server/outbox.js +246 -0
  17. package/dist/runtime/server/payload.d.ts +3 -0
  18. package/dist/runtime/server/payload.js +3 -0
  19. package/dist/runtime/server/plugins/dev-queues.d.ts +2 -0
  20. package/dist/runtime/server/plugins/dev-queues.js +25 -0
  21. package/dist/runtime/server/policy.d.ts +10 -0
  22. package/dist/runtime/server/policy.js +49 -0
  23. package/dist/runtime/server/queue.d.ts +211 -0
  24. package/dist/runtime/server/queue.js +495 -0
  25. package/dist/runtime/server/registry.d.ts +79 -0
  26. package/dist/runtime/server/registry.js +82 -0
  27. package/dist/runtime/server/schema.d.ts +965 -0
  28. package/dist/runtime/server/schema.js +80 -0
  29. package/dist/runtime/server/testing.d.ts +34 -0
  30. package/dist/runtime/server/testing.js +61 -0
  31. package/dist/runtime/server/types.d.ts +123 -0
  32. package/dist/runtime/server/types.js +0 -0
  33. package/dist/types.d.mts +3 -0
  34. package/package.json +109 -0
@@ -0,0 +1,80 @@
1
+ import { sql } from "drizzle-orm";
2
+ import { index, integer, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core";
3
+ export const cfJobBatches = sqliteTable("job_batches", {
4
+ id: text("id").primaryKey(),
5
+ name: text("name"),
6
+ parentBatchId: text("parent_batch_id"),
7
+ totalJobs: integer("total_jobs").notNull().default(0),
8
+ pendingJobs: integer("pending_jobs").notNull().default(0),
9
+ failedJobs: integer("failed_jobs").notNull().default(0),
10
+ onFinish: text("on_finish"),
11
+ handler: text("handler"),
12
+ allowFailures: integer("allow_failures").default(0),
13
+ siteId: text("site_id"),
14
+ userId: integer("user_id"),
15
+ createdAt: integer("created_at").notNull().default(sql`(unixepoch())`),
16
+ updatedAt: integer("updated_at").notNull().default(sql`(unixepoch())`),
17
+ finishedAt: integer("finished_at")
18
+ }, (t) => [
19
+ index("idx_job_batches_site").on(t.siteId),
20
+ index("idx_job_batches_pending").on(t.pendingJobs),
21
+ index("idx_job_batches_parent").on(t.parentBatchId),
22
+ index("idx_job_batches_finished_at").on(t.finishedAt)
23
+ ]);
24
+ export const cfJobs = sqliteTable("jobs", {
25
+ id: text("id").primaryKey(),
26
+ queue: text("queue").notNull(),
27
+ jobType: text("job_type").notNull(),
28
+ batchId: text("batch_id").references(() => cfJobBatches.id),
29
+ userId: integer("user_id"),
30
+ siteId: text("site_id"),
31
+ partnerId: text("partner_id"),
32
+ traceId: text("trace_id"),
33
+ uniqueKey: text("unique_key"),
34
+ payload: text("payload").notNull(),
35
+ attempts: integer("attempts").default(0),
36
+ maxAttempts: integer("max_attempts").default(3),
37
+ reservedAt: integer("reserved_at"),
38
+ availableAt: integer("available_at").notNull(),
39
+ createdAt: integer("created_at").notNull().default(sql`(unixepoch())`),
40
+ completedAt: integer("completed_at"),
41
+ failedAt: integer("failed_at"),
42
+ lastError: text("last_error"),
43
+ retryReasons: text("retry_reasons"),
44
+ rowsFetched: integer("rows_fetched"),
45
+ rowsInserted: integer("rows_inserted"),
46
+ d1RowsRead: integer("d1_rows_read"),
47
+ d1RowsWritten: integer("d1_rows_written"),
48
+ durationMs: integer("duration_ms")
49
+ }, (t) => [
50
+ index("idx_jobs_claimable").on(t.queue, t.reservedAt, t.availableAt),
51
+ index("idx_jobs_user").on(t.userId),
52
+ index("idx_jobs_site").on(t.siteId),
53
+ index("idx_jobs_partner").on(t.partnerId),
54
+ index("idx_jobs_type").on(t.jobType),
55
+ index("idx_jobs_batch").on(t.batchId),
56
+ index("idx_jobs_trace").on(t.traceId),
57
+ index("idx_jobs_sync_dedup").on(t.siteId, t.jobType),
58
+ uniqueIndex("idx_jobs_unique_active").on(t.uniqueKey).where(sql`unique_key IS NOT NULL AND completed_at IS NULL AND failed_at IS NULL`)
59
+ ]);
60
+ export const cfFailedJobs = sqliteTable("failed_jobs", {
61
+ id: text("id").primaryKey(),
62
+ queue: text("queue").notNull(),
63
+ jobType: text("job_type").notNull(),
64
+ batchId: text("batch_id"),
65
+ userId: integer("user_id"),
66
+ siteId: text("site_id"),
67
+ partnerId: text("partner_id"),
68
+ traceId: text("trace_id"),
69
+ uniqueKey: text("unique_key"),
70
+ payload: text("payload").notNull(),
71
+ exception: text("exception").notNull(),
72
+ attempts: integer("attempts").notNull(),
73
+ maxAttempts: integer("max_attempts").notNull(),
74
+ failedAt: integer("failed_at").notNull()
75
+ }, (t) => [
76
+ index("idx_failed_jobs_queue").on(t.queue),
77
+ index("idx_failed_jobs_site").on(t.siteId),
78
+ index("idx_failed_jobs_trace").on(t.traceId),
79
+ index("idx_failed_jobs_failed_at").on(t.failedAt)
80
+ ]);
@@ -0,0 +1,34 @@
1
+ import type { CloudflareQueue, QueueBatch, QueueMessage } from './types.js';
2
+ export interface FakeQueuedMessage<T> {
3
+ body: T;
4
+ opts?: {
5
+ delaySeconds?: number;
6
+ };
7
+ }
8
+ export declare function createFakeQueue<T = unknown>(): {
9
+ queue: CloudflareQueue<T>;
10
+ messages: FakeQueuedMessage<T>[];
11
+ clear(): void;
12
+ };
13
+ export declare function createFakeQueueEnv<T = unknown>(binding?: string): {
14
+ queue: CloudflareQueue<T>;
15
+ messages: FakeQueuedMessage<T>[];
16
+ clear(): void;
17
+ env: {
18
+ [binding]: CloudflareQueue<T>;
19
+ };
20
+ };
21
+ export declare function createQueueMessage<T>(body: T, attempts?: number, id?: string): QueueMessage<T> & {
22
+ acked: boolean;
23
+ retries: Array<{
24
+ delaySeconds?: number;
25
+ } | undefined>;
26
+ };
27
+ export declare function createQueueBatch<T>(queue: string, bodies: T[], opts?: {
28
+ ids?: string[];
29
+ }): QueueBatch<T> & {
30
+ ackedAll: boolean;
31
+ retriedAll: Array<{
32
+ delaySeconds?: number;
33
+ } | undefined>;
34
+ };
@@ -0,0 +1,61 @@
1
+ export function createFakeQueue() {
2
+ const messages = [];
3
+ const queue = {
4
+ async send(message, opts) {
5
+ messages.push({ body: message, opts });
6
+ },
7
+ async sendBatch(batch, opts) {
8
+ for (const message of batch) {
9
+ const perMessage = message.delaySeconds !== void 0 || message.contentType !== void 0 ? { delaySeconds: message.delaySeconds, contentType: message.contentType } : void 0;
10
+ messages.push({ body: message.body, opts: perMessage ?? opts });
11
+ }
12
+ }
13
+ };
14
+ return {
15
+ queue,
16
+ messages,
17
+ clear() {
18
+ messages.length = 0;
19
+ }
20
+ };
21
+ }
22
+ export function createFakeQueueEnv(binding = "QUEUE") {
23
+ const fake = createFakeQueue();
24
+ return {
25
+ env: { [binding]: fake.queue },
26
+ ...fake
27
+ };
28
+ }
29
+ export function createQueueMessage(body, attempts = 0, id) {
30
+ return {
31
+ id,
32
+ body,
33
+ attempts,
34
+ acked: false,
35
+ retries: [],
36
+ ack() {
37
+ this.acked = true;
38
+ },
39
+ retry(opts) {
40
+ this.retries.push(opts);
41
+ }
42
+ };
43
+ }
44
+ export function createQueueBatch(queue, bodies, opts) {
45
+ const messages = bodies.map((body, i) => createQueueMessage(body, 0, opts?.ids?.[i]));
46
+ const batch = {
47
+ queue,
48
+ messages,
49
+ ackedAll: false,
50
+ retriedAll: [],
51
+ ackAll() {
52
+ batch.ackedAll = true;
53
+ for (const message of messages) message.ack();
54
+ },
55
+ retryAll(retryOpts) {
56
+ batch.retriedAll.push(retryOpts);
57
+ for (const message of messages) message.retry(retryOpts);
58
+ }
59
+ };
60
+ return batch;
61
+ }
@@ -0,0 +1,123 @@
1
+ export type QueueMessageContentType = 'text' | 'json' | 'bytes' | 'v8';
2
+ export interface QueueSendOptions {
3
+ delaySeconds?: number;
4
+ contentType?: QueueMessageContentType;
5
+ }
6
+ export interface QueueRetryOptions {
7
+ delaySeconds?: number;
8
+ }
9
+ export interface QueueMessage<T = unknown> {
10
+ id?: string;
11
+ body: T;
12
+ attempts: number;
13
+ timestamp?: Date | number;
14
+ ack: () => void;
15
+ retry: (opts?: QueueRetryOptions) => void;
16
+ }
17
+ export interface QueueBatch<T = unknown> {
18
+ queue: string;
19
+ messages: Array<QueueMessage<T>>;
20
+ ackAll?: () => void;
21
+ retryAll?: (opts?: QueueRetryOptions) => void;
22
+ }
23
+ export interface QueuePayload<T = unknown, Env extends Record<string, unknown> = Record<string, unknown>> {
24
+ batch: QueueBatch<T>;
25
+ env: Env;
26
+ }
27
+ export interface CloudflareQueueSendBatchMessage<T = unknown> {
28
+ body: T;
29
+ contentType?: QueueMessageContentType;
30
+ delaySeconds?: number;
31
+ }
32
+ export interface CloudflareQueue<T = unknown> {
33
+ send: (message: T, opts?: QueueSendOptions) => Promise<void>;
34
+ sendBatch?: (messages: Array<CloudflareQueueSendBatchMessage<T>>, opts?: QueueSendOptions) => Promise<void>;
35
+ }
36
+ export interface QueueBindingConfig {
37
+ binding: string;
38
+ queueName?: string;
39
+ jobType?: string;
40
+ /** Wrangler `[[queues.consumers]].max_batch_size` (informational; validated against handler expectations). */
41
+ maxBatchSize?: number;
42
+ /** Wrangler `[[queues.consumers]].max_batch_timeout` in seconds. */
43
+ maxBatchTimeout?: number;
44
+ /** Wrangler `[[queues.consumers]].max_concurrency`. */
45
+ maxConcurrency?: number;
46
+ /** Wrangler `[[queues.consumers]].max_retries`. Compared against `definition.tries`. */
47
+ maxRetries?: number;
48
+ /** Wrangler `[[queues.consumers]].retry_delay` in seconds (default retry delay if none supplied per-message). */
49
+ retryDelay?: number;
50
+ /** Wrangler `[[queues.consumers]].dead_letter_queue` — the *Cloudflare* queue name. */
51
+ deadLetterQueue?: string;
52
+ /** Binding name of the DLQ producer, used when this module manually forwards exhausted messages. */
53
+ deadLetterQueueBinding?: string;
54
+ }
55
+ export type QueueBindingsConfig = Record<string, string | QueueBindingConfig>;
56
+ export interface JobControlResult {
57
+ handled: boolean;
58
+ action?: 'released' | 'failed';
59
+ delaySeconds?: number;
60
+ error?: string;
61
+ }
62
+ export interface JobContext<Env, Db, Logger> {
63
+ env: Env;
64
+ jobId: string;
65
+ batchId: string | null;
66
+ attempt: number;
67
+ db: Db;
68
+ log: Logger;
69
+ release: (delaySeconds: number) => Promise<void>;
70
+ fail: (error: string) => Promise<void>;
71
+ }
72
+ export type JobHandler<Payload, Env, Db, Logger> = (payload: Payload, ctx: JobContext<Env, Db, Logger>) => Promise<void>;
73
+ export type JobFailedHandler<Payload, Env, Db, Logger> = (payload: Payload, ctx: JobContext<Env, Db, Logger>, error: unknown) => Promise<void>;
74
+ export type JobNext = () => Promise<void>;
75
+ export type JobMiddleware<Payload, Env, Db, Logger> = (payload: Payload, ctx: JobContext<Env, Db, Logger>, next: JobNext) => Promise<void>;
76
+ export type JobBackoff = number | number[] | ((attempt: number) => number);
77
+ export type JobPayloadParseResult<Payload> = {
78
+ success: true;
79
+ data: Payload;
80
+ } | {
81
+ success: false;
82
+ error: unknown;
83
+ };
84
+ export interface JobPayloadSchema<Payload> {
85
+ safeParse: (payload: unknown) => JobPayloadParseResult<Payload>;
86
+ }
87
+ export interface JobDefinition<Name extends string, Payload, Queue extends string, Env, Db, Logger> {
88
+ name: Name;
89
+ queue: Queue;
90
+ jobType?: string;
91
+ input?: JobPayloadSchema<Payload>;
92
+ handle: JobHandler<Payload, Env, Db, Logger>;
93
+ failed?: JobFailedHandler<Payload, Env, Db, Logger>;
94
+ middleware?: Array<JobMiddleware<Payload, Env, Db, Logger>>;
95
+ tries?: number;
96
+ maxAttempts?: number;
97
+ backoff?: JobBackoff;
98
+ unique?: boolean;
99
+ uniqueId?: (payload: Payload) => string;
100
+ }
101
+ export interface DispatchableJob<Payload = Record<string, unknown>> {
102
+ id: string;
103
+ queue: string;
104
+ payload: Payload;
105
+ attempts: number;
106
+ batchId: string | null;
107
+ siteId?: string | null;
108
+ userId?: number | null;
109
+ }
110
+ export interface DispatchResult {
111
+ success: boolean;
112
+ error?: string;
113
+ handlerNotFound?: boolean;
114
+ invalidPayload?: boolean;
115
+ validationError?: unknown;
116
+ control?: JobControlResult;
117
+ }
118
+ export type QueueHandlerResult = {
119
+ action: 'ack';
120
+ } | {
121
+ action: 'retry';
122
+ delaySeconds?: number;
123
+ };
File without changes
@@ -0,0 +1,3 @@
1
+ export { default } from './module.mjs'
2
+
3
+ export { type ModuleOptions } from './module.mjs'
package/package.json ADDED
@@ -0,0 +1,109 @@
1
+ {
2
+ "name": "nuxt-cf-jobs",
3
+ "type": "module",
4
+ "version": "0.0.2",
5
+ "description": "Nuxt module for typed Cloudflare queue jobs.",
6
+ "author": {
7
+ "name": "Harlan Wilton",
8
+ "email": "harlan@harlanzw.com",
9
+ "url": "https://harlanzw.com/"
10
+ },
11
+ "contributors": [
12
+ "Harlan Wilton <harlan@harlanzw.com>"
13
+ ],
14
+ "license": "MIT",
15
+ "exports": {
16
+ ".": {
17
+ "types": "./dist/types.d.mts",
18
+ "import": "./dist/module.mjs",
19
+ "default": "./dist/module.mjs"
20
+ },
21
+ "./server": {
22
+ "types": "./dist/runtime/server/index.d.ts",
23
+ "import": "./dist/runtime/server/index.js",
24
+ "default": "./dist/runtime/server/index.js"
25
+ },
26
+ "./d1": {
27
+ "types": "./dist/runtime/server/d1.d.ts",
28
+ "import": "./dist/runtime/server/d1.js",
29
+ "default": "./dist/runtime/server/d1.js"
30
+ },
31
+ "./durable": {
32
+ "types": "./dist/runtime/server/outbox.d.ts",
33
+ "import": "./dist/runtime/server/outbox.js",
34
+ "default": "./dist/runtime/server/outbox.js"
35
+ },
36
+ "./queue": {
37
+ "types": "./dist/runtime/server/queue.d.ts",
38
+ "import": "./dist/runtime/server/queue.js",
39
+ "default": "./dist/runtime/server/queue.js"
40
+ },
41
+ "./schema": {
42
+ "types": "./dist/runtime/server/schema.d.ts",
43
+ "import": "./dist/runtime/server/schema.js",
44
+ "default": "./dist/runtime/server/schema.js"
45
+ },
46
+ "./testing": {
47
+ "types": "./dist/runtime/server/testing.d.ts",
48
+ "import": "./dist/runtime/server/testing.js",
49
+ "default": "./dist/runtime/server/testing.js"
50
+ }
51
+ },
52
+ "main": "./dist/module.mjs",
53
+ "types": "./dist/types.d.mts",
54
+ "typesVersions": {
55
+ "*": {
56
+ "server": [
57
+ "dist/runtime/server/index.d.ts"
58
+ ],
59
+ "d1": [
60
+ "dist/runtime/server/d1.d.ts"
61
+ ],
62
+ "durable": [
63
+ "dist/runtime/server/outbox.d.ts"
64
+ ],
65
+ "queue": [
66
+ "dist/runtime/server/queue.d.ts"
67
+ ],
68
+ "schema": [
69
+ "dist/runtime/server/schema.d.ts"
70
+ ],
71
+ "testing": [
72
+ "dist/runtime/server/testing.d.ts"
73
+ ]
74
+ }
75
+ },
76
+ "files": [
77
+ "dist"
78
+ ],
79
+ "peerDependencies": {
80
+ "nuxt": ">=3.0.0 <5.0.0"
81
+ },
82
+ "dependencies": {
83
+ "@nuxt/kit": "^4.4.6",
84
+ "@nuxt/schema": "^4.4.6",
85
+ "drizzle-orm": "0.45.2"
86
+ },
87
+ "devDependencies": {
88
+ "@nuxt/module-builder": "^1.0.2",
89
+ "@types/node": "^25.9.1",
90
+ "@vitest/coverage-v8": "4.1.7",
91
+ "happy-dom": "^20.9.0",
92
+ "nuxt": "^4.4.6",
93
+ "typescript": "^6.0.3",
94
+ "unbuild": "^3.6.1",
95
+ "vitest": "^4.1.7",
96
+ "vue": "^3.5.34",
97
+ "vue-tsc": "^3.3.1",
98
+ "wrangler": "^4.94.0"
99
+ },
100
+ "scripts": {
101
+ "build": "nuxt-module-build build && nuxt-module-build prepare",
102
+ "dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare",
103
+ "test": "vitest run tests/dispatch.test.ts --typecheck=false",
104
+ "test:unit": "vitest run tests/dispatch.test.ts --typecheck=false",
105
+ "test:types": "nuxt prepare tests/fixtures/nuxt-demo && vue-tsc --noEmit -p tests/fixtures/nuxt-demo/tsconfig.json",
106
+ "test:e2e": "CF_JOBS_E2E=1 vitest run --no-file-parallelism tests/wrangler-dev.e2e.test.ts tests/wrangler-d1.e2e.test.ts",
107
+ "typecheck": "tsc --noEmit -p tsconfig.json && pnpm test:types"
108
+ }
109
+ }