@happyvertical/jobs 0.74.8

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 (46) hide show
  1. package/AGENT.md +33 -0
  2. package/LICENSE +7 -0
  3. package/dist/adapters/bull.d.ts +103 -0
  4. package/dist/adapters/bull.d.ts.map +1 -0
  5. package/dist/adapters/bull.js +349 -0
  6. package/dist/adapters/bull.js.map +1 -0
  7. package/dist/adapters/bullmq.d.ts +85 -0
  8. package/dist/adapters/bullmq.d.ts.map +1 -0
  9. package/dist/adapters/bullmq.js +391 -0
  10. package/dist/adapters/bullmq.js.map +1 -0
  11. package/dist/adapters/cloud-tasks.d.ts +110 -0
  12. package/dist/adapters/cloud-tasks.d.ts.map +1 -0
  13. package/dist/adapters/cloud-tasks.js +336 -0
  14. package/dist/adapters/cloud-tasks.js.map +1 -0
  15. package/dist/adapters/postgres.d.ts +55 -0
  16. package/dist/adapters/postgres.d.ts.map +1 -0
  17. package/dist/adapters/postgres.js +437 -0
  18. package/dist/adapters/postgres.js.map +1 -0
  19. package/dist/adapters/sqlite.d.ts +44 -0
  20. package/dist/adapters/sqlite.d.ts.map +1 -0
  21. package/dist/adapters/sqlite.js +323 -0
  22. package/dist/adapters/sqlite.js.map +1 -0
  23. package/dist/adapters/sqs.d.ts +112 -0
  24. package/dist/adapters/sqs.d.ts.map +1 -0
  25. package/dist/adapters/sqs.js +411 -0
  26. package/dist/adapters/sqs.js.map +1 -0
  27. package/dist/base-store.d.ts +69 -0
  28. package/dist/base-store.d.ts.map +1 -0
  29. package/dist/chunks/base-store-DlNksWvQ.js +324 -0
  30. package/dist/chunks/base-store-DlNksWvQ.js.map +1 -0
  31. package/dist/cli/claude-context.d.ts +3 -0
  32. package/dist/cli/claude-context.d.ts.map +1 -0
  33. package/dist/cli/claude-context.js +21 -0
  34. package/dist/cli/claude-context.js.map +1 -0
  35. package/dist/index.d.ts +16 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +252 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/retry.d.ts +84 -0
  40. package/dist/retry.d.ts.map +1 -0
  41. package/dist/types.d.ts +311 -0
  42. package/dist/types.d.ts.map +1 -0
  43. package/dist/worker.d.ts +74 -0
  44. package/dist/worker.d.ts.map +1 -0
  45. package/metadata.json +34 -0
  46. package/package.json +114 -0
@@ -0,0 +1,336 @@
1
+ import { createId } from "@happyvertical/utils";
2
+ import { B as BaseJobStore, p as priorityToNumber } from "../chunks/base-store-DlNksWvQ.js";
3
+ class CloudTasksJobStore extends BaseJobStore {
4
+ config;
5
+ client = null;
6
+ // biome-ignore lint/style/useNamingConvention: Google Cloud Tasks module reference
7
+ cloudTasksModule = null;
8
+ jobStates = /* @__PURE__ */ new Map();
9
+ constructor(config) {
10
+ super();
11
+ this.config = {
12
+ queuePrefix: "",
13
+ defaultRetry: {
14
+ maxAttempts: 3,
15
+ minBackoff: "1s",
16
+ maxBackoff: "300s"
17
+ },
18
+ ...config
19
+ };
20
+ }
21
+ /**
22
+ * Initialize the store - dynamically imports Google Cloud Tasks
23
+ */
24
+ async initialize() {
25
+ if (this.initialized) return;
26
+ try {
27
+ this.cloudTasksModule = await import("@google-cloud/tasks");
28
+ } catch {
29
+ throw new Error(
30
+ "Google Cloud Tasks is required for CloudTasksJobStore. Install it with: npm install @google-cloud/tasks"
31
+ );
32
+ }
33
+ this.client = new this.cloudTasksModule.CloudTasksClient();
34
+ this.initialized = true;
35
+ }
36
+ /**
37
+ * Get the full queue path
38
+ */
39
+ getQueuePath(queueName) {
40
+ return `projects/${this.config.projectId}/locations/${this.config.location}/queues/${this.config.queuePrefix}${queueName}`;
41
+ }
42
+ /**
43
+ * Get the full task path
44
+ */
45
+ getTaskPath(queueName, taskId) {
46
+ return `${this.getQueuePath(queueName)}/tasks/${taskId}`;
47
+ }
48
+ /**
49
+ * Enqueue a new job
50
+ */
51
+ async enqueue(options) {
52
+ if (!this.initialized || !this.client) {
53
+ throw new Error("CloudTasksJobStore not initialized");
54
+ }
55
+ const queueName = options.queue ?? "default";
56
+ const now = /* @__PURE__ */ new Date();
57
+ const jobId = createId();
58
+ const jobData = {
59
+ id: jobId,
60
+ queue: queueName,
61
+ payload: options.payload,
62
+ priority: priorityToNumber(options.priority),
63
+ maxAttempts: options.maxAttempts ?? 3,
64
+ timeout: options.timeout ?? 3e5,
65
+ timeoutBehavior: options.timeoutBehavior ?? "fail",
66
+ retryStrategy: options.retryStrategy && "toConfig" in options.retryStrategy ? options.retryStrategy.toConfig() : options.retryStrategy ?? {
67
+ type: "exponential",
68
+ config: { initialDelay: 1e3, multiplier: 2 }
69
+ },
70
+ createdAt: now.toISOString()
71
+ };
72
+ const task = {
73
+ name: this.getTaskPath(queueName, jobId),
74
+ httpRequest: {
75
+ httpMethod: "POST",
76
+ url: this.config.handlerUrl,
77
+ headers: {
78
+ "Content-Type": "application/json"
79
+ },
80
+ body: Buffer.from(JSON.stringify(jobData)).toString("base64")
81
+ }
82
+ };
83
+ if (this.config.serviceAccountEmail) {
84
+ task.httpRequest.oidcToken = {
85
+ serviceAccountEmail: this.config.serviceAccountEmail
86
+ };
87
+ }
88
+ if (options.runAt && options.runAt > now) {
89
+ task.scheduleTime = {
90
+ seconds: Math.floor(options.runAt.getTime() / 1e3),
91
+ nanos: options.runAt.getTime() % 1e3 * 1e6
92
+ };
93
+ }
94
+ if (options.timeout) {
95
+ task.dispatchDeadline = {
96
+ seconds: Math.floor(options.timeout / 1e3),
97
+ nanos: options.timeout % 1e3 * 1e6
98
+ };
99
+ }
100
+ const [response] = await this.client.createTask({
101
+ parent: this.getQueuePath(queueName),
102
+ task
103
+ });
104
+ const job = {
105
+ id: jobId,
106
+ queue: queueName,
107
+ payload: options.payload,
108
+ status: "pending",
109
+ priority: priorityToNumber(options.priority),
110
+ attempts: 0,
111
+ maxAttempts: options.maxAttempts ?? 3,
112
+ runAt: options.runAt ?? now,
113
+ startedAt: null,
114
+ completedAt: null,
115
+ timeout: options.timeout ?? 3e5,
116
+ timeoutBehavior: options.timeoutBehavior ?? "fail",
117
+ lastError: null,
118
+ resultPointer: null,
119
+ retryStrategy: jobData.retryStrategy,
120
+ workerId: null,
121
+ workerHeartbeat: null,
122
+ createdAt: now,
123
+ updatedAt: now
124
+ };
125
+ this.jobStates.set(jobId, {
126
+ job,
127
+ taskName: response.name ?? void 0
128
+ });
129
+ await this.emitEvent("job.created", job);
130
+ return job;
131
+ }
132
+ /**
133
+ * Dequeue jobs - NOT SUPPORTED in Cloud Tasks
134
+ * Cloud Tasks is push-based; jobs are delivered to your handler URL
135
+ */
136
+ async dequeue(_queues, _limit, _workerId) {
137
+ throw new Error(
138
+ "Cloud Tasks does not support pull-based dequeue. Jobs are pushed to your handler URL. Use the HTTP handler to receive jobs."
139
+ );
140
+ }
141
+ /**
142
+ * Update a job - LIMITED SUPPORT in Cloud Tasks
143
+ */
144
+ async update(_id, _updates) {
145
+ throw new Error(
146
+ "Cloud Tasks does not support updating tasks after creation. Cancel and recreate the job instead."
147
+ );
148
+ }
149
+ /**
150
+ * Get a job by ID
151
+ */
152
+ async get(id) {
153
+ const state = this.jobStates.get(id);
154
+ if (state) {
155
+ return state.job;
156
+ }
157
+ return null;
158
+ }
159
+ /**
160
+ * List jobs with filtering
161
+ * Note: Only returns jobs from in-memory state
162
+ */
163
+ async list(filter) {
164
+ const jobs = [];
165
+ const limit = filter.limit ?? 100;
166
+ for (const state of this.jobStates.values()) {
167
+ const job = state.job;
168
+ if (filter.queue && job.queue !== filter.queue) continue;
169
+ if (filter.status) {
170
+ const statuses = Array.isArray(filter.status) ? filter.status : [filter.status];
171
+ if (!statuses.includes(job.status)) continue;
172
+ }
173
+ if (filter.objectType && job.payload.objectType !== filter.objectType)
174
+ continue;
175
+ if (filter.method && job.payload.method !== filter.method) continue;
176
+ if (filter.createdAfter && job.createdAt < filter.createdAfter) continue;
177
+ if (filter.createdBefore && job.createdAt > filter.createdBefore)
178
+ continue;
179
+ jobs.push(job);
180
+ if (jobs.length >= limit) break;
181
+ }
182
+ return jobs;
183
+ }
184
+ /**
185
+ * Cancel a job by deleting its Cloud Task
186
+ */
187
+ async cancel(id) {
188
+ if (!this.initialized || !this.client) {
189
+ throw new Error("CloudTasksJobStore not initialized");
190
+ }
191
+ const state = this.jobStates.get(id);
192
+ if (!state || !state.taskName) {
193
+ throw new Error(`Job ${id} not found`);
194
+ }
195
+ try {
196
+ await this.client.deleteTask({ name: state.taskName });
197
+ } catch (error) {
198
+ const err = error;
199
+ if (err.code !== 5) {
200
+ throw error;
201
+ }
202
+ }
203
+ state.job.status = "cancelled";
204
+ state.job.completedAt = /* @__PURE__ */ new Date();
205
+ await this.emitEvent("job.cancelled", state.job);
206
+ }
207
+ /**
208
+ * Mark job as started (called by your handler when it receives the job)
209
+ */
210
+ async markStarted(id, workerId) {
211
+ const state = this.jobStates.get(id);
212
+ if (!state) {
213
+ throw new Error(`Job ${id} not found`);
214
+ }
215
+ state.job.status = "running";
216
+ state.job.workerId = workerId;
217
+ state.job.startedAt = /* @__PURE__ */ new Date();
218
+ state.job.attempts++;
219
+ await this.emitEvent("job.started", state.job);
220
+ }
221
+ /**
222
+ * Mark job as completed (called by your handler after successful execution)
223
+ */
224
+ async markCompleted(id, resultPointer) {
225
+ const state = this.jobStates.get(id);
226
+ if (!state) {
227
+ throw new Error(`Job ${id} not found`);
228
+ }
229
+ state.job.status = "completed";
230
+ state.job.completedAt = /* @__PURE__ */ new Date();
231
+ state.job.resultPointer = resultPointer ?? null;
232
+ await this.emitEvent("job.completed", state.job, { resultPointer });
233
+ }
234
+ /**
235
+ * Mark job as failed (called by your handler after failed execution)
236
+ */
237
+ async markFailed(id, error) {
238
+ const state = this.jobStates.get(id);
239
+ if (!state) {
240
+ throw new Error(`Job ${id} not found`);
241
+ }
242
+ state.job.status = "failed";
243
+ state.job.completedAt = /* @__PURE__ */ new Date();
244
+ state.job.lastError = error;
245
+ await this.emitEvent("job.failed", state.job, { error });
246
+ }
247
+ /**
248
+ * Clean up old jobs from in-memory state
249
+ */
250
+ async cleanup(options) {
251
+ let cleaned = 0;
252
+ for (const [id, state] of this.jobStates) {
253
+ const { job } = state;
254
+ if (options.completedBefore && job.status === "completed" && job.completedAt && job.completedAt < options.completedBefore) {
255
+ this.jobStates.delete(id);
256
+ cleaned++;
257
+ continue;
258
+ }
259
+ if (options.failedBefore && job.status === "failed" && job.completedAt && job.completedAt < options.failedBefore) {
260
+ this.jobStates.delete(id);
261
+ cleaned++;
262
+ continue;
263
+ }
264
+ if (options.cancelledBefore && job.status === "cancelled" && job.completedAt && job.completedAt < options.cancelledBefore) {
265
+ this.jobStates.delete(id);
266
+ cleaned++;
267
+ continue;
268
+ }
269
+ if (options.limit && cleaned >= options.limit) break;
270
+ }
271
+ return cleaned;
272
+ }
273
+ /**
274
+ * Update worker heartbeat - stored in memory only
275
+ */
276
+ async heartbeat(jobId, _workerId) {
277
+ const state = this.jobStates.get(jobId);
278
+ if (state) {
279
+ state.job.workerHeartbeat = /* @__PURE__ */ new Date();
280
+ }
281
+ }
282
+ /**
283
+ * Get queue statistics from in-memory state
284
+ */
285
+ async stats(queue) {
286
+ const totals = {
287
+ pending: 0,
288
+ running: 0,
289
+ completed: 0,
290
+ failed: 0,
291
+ cancelled: 0,
292
+ avgDuration: null
293
+ };
294
+ for (const state of this.jobStates.values()) {
295
+ if (queue && state.job.queue !== queue) continue;
296
+ switch (state.job.status) {
297
+ case "pending":
298
+ totals.pending++;
299
+ break;
300
+ case "running":
301
+ totals.running++;
302
+ break;
303
+ case "completed":
304
+ totals.completed++;
305
+ break;
306
+ case "failed":
307
+ totals.failed++;
308
+ break;
309
+ case "cancelled":
310
+ totals.cancelled++;
311
+ break;
312
+ }
313
+ }
314
+ return totals;
315
+ }
316
+ /**
317
+ * Close the Cloud Tasks client
318
+ */
319
+ async close() {
320
+ if (this.client) {
321
+ await this.client.close();
322
+ this.client = null;
323
+ }
324
+ this.jobStates.clear();
325
+ this.initialized = false;
326
+ }
327
+ }
328
+ function createCloudTasksJobStore(config) {
329
+ return new CloudTasksJobStore(config);
330
+ }
331
+ export {
332
+ CloudTasksJobStore,
333
+ createCloudTasksJobStore,
334
+ CloudTasksJobStore as default
335
+ };
336
+ //# sourceMappingURL=cloud-tasks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cloud-tasks.js","sources":["../../src/adapters/cloud-tasks.ts"],"sourcesContent":["/**\n * Google Cloud Tasks Job Store Adapter\n *\n * Uses Google Cloud Tasks for serverless job execution with HTTP triggers.\n * Cloud Tasks provides automatic retries, scheduling, and rate limiting.\n *\n * @example\n * ```typescript\n * import { CloudTasksJobStore } from '@happyvertical/jobs/adapters/cloud-tasks';\n *\n * const store = new CloudTasksJobStore({\n * projectId: 'my-project',\n * location: 'us-central1',\n * queuePrefix: 'myapp-',\n * handlerUrl: 'https://myapp.run.app/jobs/handle',\n * });\n *\n * await store.initialize();\n * ```\n *\n * Note: This adapter requires `@google-cloud/tasks` as a peer dependency.\n * Install it with: npm install @google-cloud/tasks\n *\n * Important: Cloud Tasks is designed for HTTP-based job execution.\n * Jobs are sent to your handler URL when scheduled to run.\n */\n\nimport type { CloudTasksClient as CloudTasksClientType } from '@google-cloud/tasks';\nimport { createId } from '@happyvertical/utils';\nimport { BaseJobStore, priorityToNumber } from '../base-store.js';\nimport type {\n CleanupOptions,\n Job,\n JobCreateOptions,\n JobFilter,\n QueueStats,\n} from '../types.js';\n\n/**\n * Cloud Tasks adapter configuration\n */\nexport interface CloudTasksJobStoreConfig {\n /** GCP project ID */\n projectId: string;\n /** GCP location (e.g., 'us-central1') */\n location: string;\n /** Queue name prefix */\n queuePrefix?: string;\n /** HTTP handler URL for job execution */\n handlerUrl: string;\n /** Service account email for OIDC authentication */\n serviceAccountEmail?: string;\n /** Default retry configuration */\n defaultRetry?: {\n maxAttempts?: number;\n minBackoff?: string;\n maxBackoff?: string;\n };\n}\n\n/**\n * Job data sent to handler\n */\ninterface CloudTaskJobData {\n id: string;\n queue: string;\n payload: Job['payload'];\n priority: number;\n maxAttempts: number;\n timeout: number;\n timeoutBehavior: Job['timeoutBehavior'];\n retryStrategy: Job['retryStrategy'];\n createdAt: string;\n}\n\n/**\n * In-memory job state tracking\n */\ninterface JobState {\n job: Job;\n taskName?: string;\n}\n\n/**\n * Cloud Tasks-based job store implementation\n *\n * Note: Cloud Tasks operates differently from traditional job queues:\n * - Jobs are HTTP tasks sent to your handler URL\n * - No pull-based dequeue (Cloud Tasks pushes to your handler)\n * - Updates are limited (tasks can be deleted but not modified)\n */\nexport class CloudTasksJobStore extends BaseJobStore {\n private config: CloudTasksJobStoreConfig;\n private client: CloudTasksClientType | null = null;\n // biome-ignore lint/style/useNamingConvention: Google Cloud Tasks module reference\n private cloudTasksModule: typeof import('@google-cloud/tasks') | null = null;\n private jobStates: Map<string, JobState> = new Map();\n\n constructor(config: CloudTasksJobStoreConfig) {\n super();\n this.config = {\n queuePrefix: '',\n defaultRetry: {\n maxAttempts: 3,\n minBackoff: '1s',\n maxBackoff: '300s',\n },\n ...config,\n };\n }\n\n /**\n * Initialize the store - dynamically imports Google Cloud Tasks\n */\n async initialize(): Promise<void> {\n if (this.initialized) return;\n\n try {\n // Dynamic import to avoid requiring Cloud Tasks as a hard dependency\n this.cloudTasksModule = await import('@google-cloud/tasks');\n } catch {\n throw new Error(\n 'Google Cloud Tasks is required for CloudTasksJobStore. ' +\n 'Install it with: npm install @google-cloud/tasks',\n );\n }\n\n this.client = new this.cloudTasksModule.CloudTasksClient();\n this.initialized = true;\n }\n\n /**\n * Get the full queue path\n */\n private getQueuePath(queueName: string): string {\n return `projects/${this.config.projectId}/locations/${this.config.location}/queues/${this.config.queuePrefix}${queueName}`;\n }\n\n /**\n * Get the full task path\n */\n private getTaskPath(queueName: string, taskId: string): string {\n return `${this.getQueuePath(queueName)}/tasks/${taskId}`;\n }\n\n /**\n * Enqueue a new job\n */\n async enqueue(options: JobCreateOptions): Promise<Job> {\n if (!this.initialized || !this.client) {\n throw new Error('CloudTasksJobStore not initialized');\n }\n\n const queueName = options.queue ?? 'default';\n const now = new Date();\n const jobId = createId();\n\n const jobData: CloudTaskJobData = {\n id: jobId,\n queue: queueName,\n payload: options.payload,\n priority: priorityToNumber(options.priority),\n maxAttempts: options.maxAttempts ?? 3,\n timeout: options.timeout ?? 300000,\n timeoutBehavior: options.timeoutBehavior ?? 'fail',\n retryStrategy:\n options.retryStrategy && 'toConfig' in options.retryStrategy\n ? options.retryStrategy.toConfig()\n : ((options.retryStrategy as Job['retryStrategy']) ?? {\n type: 'exponential',\n config: { initialDelay: 1000, multiplier: 2 },\n }),\n createdAt: now.toISOString(),\n };\n\n const task: Parameters<typeof this.client.createTask>[0]['task'] = {\n name: this.getTaskPath(queueName, jobId),\n httpRequest: {\n httpMethod: 'POST',\n url: this.config.handlerUrl,\n headers: {\n 'Content-Type': 'application/json',\n },\n body: Buffer.from(JSON.stringify(jobData)).toString('base64'),\n },\n };\n\n // Add OIDC authentication if service account is configured\n if (this.config.serviceAccountEmail) {\n task.httpRequest!.oidcToken = {\n serviceAccountEmail: this.config.serviceAccountEmail,\n };\n }\n\n // Set schedule time for delayed jobs\n if (options.runAt && options.runAt > now) {\n task.scheduleTime = {\n seconds: Math.floor(options.runAt.getTime() / 1000),\n nanos: (options.runAt.getTime() % 1000) * 1000000,\n };\n }\n\n // Set dispatch deadline (timeout)\n if (options.timeout) {\n task.dispatchDeadline = {\n seconds: Math.floor(options.timeout / 1000),\n nanos: (options.timeout % 1000) * 1000000,\n };\n }\n\n const [response] = await this.client.createTask({\n parent: this.getQueuePath(queueName),\n task,\n });\n\n const job: Job = {\n id: jobId,\n queue: queueName,\n payload: options.payload,\n status: 'pending',\n priority: priorityToNumber(options.priority),\n attempts: 0,\n maxAttempts: options.maxAttempts ?? 3,\n runAt: options.runAt ?? now,\n startedAt: null,\n completedAt: null,\n timeout: options.timeout ?? 300000,\n timeoutBehavior: options.timeoutBehavior ?? 'fail',\n lastError: null,\n resultPointer: null,\n retryStrategy: jobData.retryStrategy,\n workerId: null,\n workerHeartbeat: null,\n createdAt: now,\n updatedAt: now,\n };\n\n // Store job state\n this.jobStates.set(jobId, {\n job,\n taskName: response.name ?? undefined,\n });\n\n await this.emitEvent('job.created', job);\n return job;\n }\n\n /**\n * Dequeue jobs - NOT SUPPORTED in Cloud Tasks\n * Cloud Tasks is push-based; jobs are delivered to your handler URL\n */\n async dequeue(\n _queues: string[],\n _limit: number,\n _workerId: string,\n ): Promise<Job[]> {\n throw new Error(\n 'Cloud Tasks does not support pull-based dequeue. ' +\n 'Jobs are pushed to your handler URL. ' +\n 'Use the HTTP handler to receive jobs.',\n );\n }\n\n /**\n * Update a job - LIMITED SUPPORT in Cloud Tasks\n */\n async update(_id: string, _updates: Partial<Job>): Promise<Job> {\n throw new Error(\n 'Cloud Tasks does not support updating tasks after creation. ' +\n 'Cancel and recreate the job instead.',\n );\n }\n\n /**\n * Get a job by ID\n */\n async get(id: string): Promise<Job | null> {\n // Check in-memory state first\n const state = this.jobStates.get(id);\n if (state) {\n return state.job;\n }\n\n // Cloud Tasks doesn't support getting task details easily after creation\n return null;\n }\n\n /**\n * List jobs with filtering\n * Note: Only returns jobs from in-memory state\n */\n async list(filter: JobFilter): Promise<Job[]> {\n const jobs: Job[] = [];\n const limit = filter.limit ?? 100;\n\n for (const state of this.jobStates.values()) {\n const job = state.job;\n\n // Apply filters\n if (filter.queue && job.queue !== filter.queue) continue;\n if (filter.status) {\n const statuses = Array.isArray(filter.status)\n ? filter.status\n : [filter.status];\n if (!statuses.includes(job.status)) continue;\n }\n if (filter.objectType && job.payload.objectType !== filter.objectType)\n continue;\n if (filter.method && job.payload.method !== filter.method) continue;\n if (filter.createdAfter && job.createdAt < filter.createdAfter) continue;\n if (filter.createdBefore && job.createdAt > filter.createdBefore)\n continue;\n\n jobs.push(job);\n if (jobs.length >= limit) break;\n }\n\n return jobs;\n }\n\n /**\n * Cancel a job by deleting its Cloud Task\n */\n async cancel(id: string): Promise<void> {\n if (!this.initialized || !this.client) {\n throw new Error('CloudTasksJobStore not initialized');\n }\n\n const state = this.jobStates.get(id);\n if (!state || !state.taskName) {\n throw new Error(`Job ${id} not found`);\n }\n\n try {\n await this.client.deleteTask({ name: state.taskName });\n } catch (error: unknown) {\n // Task may already be executed or deleted\n const err = error as { code?: number };\n if (err.code !== 5) {\n // NOT_FOUND\n throw error;\n }\n }\n\n state.job.status = 'cancelled';\n state.job.completedAt = new Date();\n\n await this.emitEvent('job.cancelled', state.job);\n }\n\n /**\n * Mark job as started (called by your handler when it receives the job)\n */\n async markStarted(id: string, workerId: string): Promise<void> {\n const state = this.jobStates.get(id);\n if (!state) {\n throw new Error(`Job ${id} not found`);\n }\n\n state.job.status = 'running';\n state.job.workerId = workerId;\n state.job.startedAt = new Date();\n state.job.attempts++;\n\n await this.emitEvent('job.started', state.job);\n }\n\n /**\n * Mark job as completed (called by your handler after successful execution)\n */\n async markCompleted(id: string, resultPointer?: string): Promise<void> {\n const state = this.jobStates.get(id);\n if (!state) {\n throw new Error(`Job ${id} not found`);\n }\n\n state.job.status = 'completed';\n state.job.completedAt = new Date();\n state.job.resultPointer = resultPointer ?? null;\n\n await this.emitEvent('job.completed', state.job, { resultPointer });\n }\n\n /**\n * Mark job as failed (called by your handler after failed execution)\n */\n async markFailed(id: string, error: string): Promise<void> {\n const state = this.jobStates.get(id);\n if (!state) {\n throw new Error(`Job ${id} not found`);\n }\n\n state.job.status = 'failed';\n state.job.completedAt = new Date();\n state.job.lastError = error;\n\n await this.emitEvent('job.failed', state.job, { error });\n }\n\n /**\n * Clean up old jobs from in-memory state\n */\n async cleanup(options: CleanupOptions): Promise<number> {\n let cleaned = 0;\n\n for (const [id, state] of this.jobStates) {\n const { job } = state;\n\n if (\n options.completedBefore &&\n job.status === 'completed' &&\n job.completedAt &&\n job.completedAt < options.completedBefore\n ) {\n this.jobStates.delete(id);\n cleaned++;\n continue;\n }\n\n if (\n options.failedBefore &&\n job.status === 'failed' &&\n job.completedAt &&\n job.completedAt < options.failedBefore\n ) {\n this.jobStates.delete(id);\n cleaned++;\n continue;\n }\n\n if (\n options.cancelledBefore &&\n job.status === 'cancelled' &&\n job.completedAt &&\n job.completedAt < options.cancelledBefore\n ) {\n this.jobStates.delete(id);\n cleaned++;\n continue;\n }\n\n if (options.limit && cleaned >= options.limit) break;\n }\n\n return cleaned;\n }\n\n /**\n * Update worker heartbeat - stored in memory only\n */\n async heartbeat(jobId: string, _workerId: string): Promise<void> {\n const state = this.jobStates.get(jobId);\n if (state) {\n state.job.workerHeartbeat = new Date();\n }\n }\n\n /**\n * Get queue statistics from in-memory state\n */\n async stats(queue?: string): Promise<QueueStats> {\n const totals: QueueStats = {\n pending: 0,\n running: 0,\n completed: 0,\n failed: 0,\n cancelled: 0,\n avgDuration: null,\n };\n\n for (const state of this.jobStates.values()) {\n if (queue && state.job.queue !== queue) continue;\n\n switch (state.job.status) {\n case 'pending':\n totals.pending++;\n break;\n case 'running':\n totals.running++;\n break;\n case 'completed':\n totals.completed++;\n break;\n case 'failed':\n totals.failed++;\n break;\n case 'cancelled':\n totals.cancelled++;\n break;\n }\n }\n\n return totals;\n }\n\n /**\n * Close the Cloud Tasks client\n */\n async close(): Promise<void> {\n if (this.client) {\n await this.client.close();\n this.client = null;\n }\n this.jobStates.clear();\n this.initialized = false;\n }\n}\n\n/**\n * Create a Cloud Tasks job store instance\n */\nexport function createCloudTasksJobStore(\n config: CloudTasksJobStoreConfig,\n): CloudTasksJobStore {\n return new CloudTasksJobStore(config);\n}\n\nexport default CloudTasksJobStore;\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,55 @@
1
+ import { DatabaseInterface } from '@happyvertical/sql';
2
+ import { BaseJobStore } from '../base-store.js';
3
+ import { CleanupOptions, Job, JobCreateOptions, JobFilter, QueueStats } from '../types.js';
4
+ /**
5
+ * PostgreSQL job store configuration
6
+ */
7
+ export interface PostgresJobStoreConfig {
8
+ /** Database connection URL */
9
+ url?: string;
10
+ /** Existing database instance to use */
11
+ db?: DatabaseInterface;
12
+ /** Table name for jobs (default: '_jobs') */
13
+ tableName?: string;
14
+ /** Enable NOTIFY/LISTEN for push-based job retrieval */
15
+ enableNotify?: boolean;
16
+ /** Channel name for notifications (default: 'job_events') */
17
+ notifyChannel?: string;
18
+ }
19
+ /**
20
+ * PostgreSQL-based job store with NOTIFY/LISTEN support
21
+ *
22
+ * Uses PostgreSQL for job persistence with optional push-based
23
+ * notifications via NOTIFY/LISTEN for efficient job retrieval.
24
+ */
25
+ export declare class PostgresJobStore extends BaseJobStore {
26
+ private db;
27
+ private readonly url;
28
+ private readonly externalDb;
29
+ private readonly tableName;
30
+ private readonly enableNotify;
31
+ private readonly notifyChannel;
32
+ private notifyListeners;
33
+ private listening;
34
+ constructor(config?: PostgresJobStoreConfig);
35
+ initialize(): Promise<void>;
36
+ private setupNotifyTriggers;
37
+ /**
38
+ * Start listening for PostgreSQL notifications
39
+ * This enables push-based job retrieval
40
+ */
41
+ startListening(): Promise<void>;
42
+ stopListening(): Promise<void>;
43
+ enqueue(options: JobCreateOptions): Promise<Job>;
44
+ dequeue(queues: string[], limit: number, workerId: string): Promise<Job[]>;
45
+ update(id: string, updates: Partial<Job>): Promise<Job>;
46
+ get(id: string): Promise<Job | null>;
47
+ list(filter: JobFilter): Promise<Job[]>;
48
+ cancel(id: string): Promise<void>;
49
+ cleanup(options: CleanupOptions): Promise<number>;
50
+ heartbeat(jobId: string, workerId: string): Promise<void>;
51
+ stats(queue?: string): Promise<QueueStats>;
52
+ close(): Promise<void>;
53
+ }
54
+ export default PostgresJobStore;
55
+ //# sourceMappingURL=postgres.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"postgres.d.ts","sourceRoot":"","sources":["../../src/adapters/postgres.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,iBAAiB,EAAe,MAAM,oBAAoB,CAAC;AACzE,OAAO,EAAE,YAAY,EAAqB,MAAM,kBAAkB,CAAC;AACnE,OAAO,KAAK,EACV,cAAc,EACd,GAAG,EACH,gBAAgB,EAEhB,SAAS,EACT,UAAU,EACX,MAAM,aAAa,CAAC;AAErB;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC,8BAA8B;IAC9B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,wCAAwC;IACxC,EAAE,CAAC,EAAE,iBAAiB,CAAC;IACvB,6CAA6C;IAC7C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,wDAAwD;IACxD,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,6DAA6D;IAC7D,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;GAKG;AACH,qBAAa,gBAAiB,SAAQ,YAAY;IAChD,OAAO,CAAC,EAAE,CAAkC;IAC5C,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;IAC7B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA2B;IACtD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAU;IACvC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,eAAe,CAAoC;IAC3D,OAAO,CAAC,SAAS,CAAS;gBAEd,MAAM,GAAE,sBAA2B;IAWzC,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;YA0DnB,mBAAmB;IA4DjC;;;OAGG;IACG,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC;IAa/B,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC;IAW9B,OAAO,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,GAAG,CAAC;IAoChD,OAAO,CACX,MAAM,EAAE,MAAM,EAAE,EAChB,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,GAAG,EAAE,CAAC;IA2CX,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC;IA6EvD,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,GAAG,IAAI,CAAC;IASpC,IAAI,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC;IAqEvC,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAgBjC,OAAO,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC;IAiDjD,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAczD,KAAK,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAiD1C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAM7B;AAED,eAAe,gBAAgB,CAAC"}