@happyvertical/jobs 0.80.0 → 0.80.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/dist/adapters/bull.js +343 -346
- package/dist/adapters/bull.js.map +1 -1
- package/dist/adapters/bullmq.js +351 -388
- package/dist/adapters/bullmq.js.map +1 -1
- package/dist/adapters/cloud-tasks.js +307 -333
- package/dist/adapters/cloud-tasks.js.map +1 -1
- package/dist/adapters/postgres.js +288 -328
- package/dist/adapters/postgres.js.map +1 -1
- package/dist/adapters/sqlite.js +236 -258
- package/dist/adapters/sqlite.js.map +1 -1
- package/dist/adapters/sqs.js +363 -408
- package/dist/adapters/sqs.js.map +1 -1
- package/dist/chunks/base-store-DIasEzL0.js +386 -0
- package/dist/chunks/base-store-DIasEzL0.js.map +1 -0
- package/dist/cli/claude-context.js +17 -17
- package/dist/cli/claude-context.js.map +1 -1
- package/dist/index.js +219 -242
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/dist/chunks/base-store-DlNksWvQ.js +0 -324
- package/dist/chunks/base-store-DlNksWvQ.js.map +0 -1
package/dist/adapters/sqs.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sqs.js","sources":["../../src/adapters/sqs.ts"],"sourcesContent":["/**\n * AWS SQS Job Store Adapter\n *\n * Uses AWS SQS for cloud-based job storage with automatic scaling.\n * SQS provides managed message queuing with features like\n * visibility timeout, dead letter queues, and FIFO ordering.\n *\n * @example\n * ```typescript\n * import { SQSJobStore } from '@happyvertical/jobs/adapters/sqs';\n *\n * const store = new SQSJobStore({\n * region: 'us-east-1',\n * queueUrlPrefix: 'https://sqs.us-east-1.amazonaws.com/123456789/myapp-',\n * // Or use credentials explicitly\n * credentials: {\n * accessKeyId: 'AKIA...',\n * secretAccessKey: '...',\n * },\n * });\n *\n * await store.initialize();\n * ```\n *\n * Note: This adapter requires the `@aws-sdk/client-sqs` package as a peer dependency.\n * Install it with: npm install @aws-sdk/client-sqs\n *\n * Important: SQS has some limitations for job storage:\n * - Jobs cannot be updated after enqueue (SQS messages are immutable)\n * - Job listing is limited (SQS doesn't support efficient listing)\n * - Cleanup is automatic via message retention policy\n * - Use DynamoDB alongside SQS for full job tracking if needed\n */\n\nimport type { Message, SQSClient as SQSClientType } from '@aws-sdk/client-sqs';\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 * AWS credentials configuration\n */\nexport interface AWSCredentials {\n accessKeyId: string;\n secretAccessKey: string;\n sessionToken?: string;\n}\n\n/**\n * SQS adapter configuration\n */\nexport interface SQSJobStoreConfig {\n /** AWS region */\n region?: string;\n /** AWS credentials (optional - uses default chain if not provided) */\n credentials?: AWSCredentials;\n /** Queue URL prefix (queues will be named {prefix}{queueName}) */\n queueUrlPrefix: string;\n /** Default visibility timeout in seconds */\n visibilityTimeout?: number;\n /** Message retention in days (1-14, default: 4) */\n messageRetentionDays?: number;\n /** Use FIFO queues for ordering guarantees */\n useFifo?: boolean;\n}\n\n/**\n * Job data stored in SQS message body\n */\ninterface SQSJobData {\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 runAt: string;\n createdAt: string;\n}\n\n/**\n * In-memory job state tracking (SQS messages are immutable)\n */\ninterface JobState {\n job: Job;\n receiptHandle?: string;\n}\n\n/**\n * SQS-based job store implementation\n *\n * Note: SQS has limitations that make some operations different:\n * - `update()` throws error (messages are immutable)\n * - `list()` only returns pending jobs from SQS\n * - `cancel()` requires the job to have been dequeued first\n * - For full job tracking, consider using DynamoDB alongside SQS\n */\nexport class SQSJobStore extends BaseJobStore {\n private config: SQSJobStoreConfig;\n private client: SQSClientType | null = null;\n // biome-ignore lint/style/useNamingConvention: AWS SDK module reference\n private awsSdkModule: typeof import('@aws-sdk/client-sqs') | null = null;\n private queueUrls: Map<string, string> = new Map();\n // In-memory state tracking for jobs that have been dequeued\n private jobStates: Map<string, JobState> = new Map();\n\n constructor(config: SQSJobStoreConfig) {\n super();\n this.config = {\n visibilityTimeout: 300, // 5 minutes default\n messageRetentionDays: 4,\n useFifo: false,\n ...config,\n };\n }\n\n /**\n * Initialize the store - dynamically imports AWS SDK\n */\n async initialize(): Promise<void> {\n if (this.initialized) return;\n\n try {\n // Dynamic import to avoid requiring AWS SDK as a hard dependency\n this.awsSdkModule = await import('@aws-sdk/client-sqs');\n } catch {\n throw new Error(\n 'AWS SDK is required for SQSJobStore. Install it with: npm install @aws-sdk/client-sqs',\n );\n }\n\n this.client = new this.awsSdkModule.SQSClient({\n region: this.config.region,\n credentials: this.config.credentials,\n });\n\n this.initialized = true;\n }\n\n /**\n * Get or create queue URL for a queue name\n */\n private getQueueUrl(queueName: string): string {\n if (!this.queueUrls.has(queueName)) {\n const suffix = this.config.useFifo ? '.fifo' : '';\n this.queueUrls.set(\n queueName,\n `${this.config.queueUrlPrefix}${queueName}${suffix}`,\n );\n }\n return this.queueUrls.get(queueName)!;\n }\n\n /**\n * Convert SQS message to Job format\n */\n private sqsMessageToJob(message: Message, queueName: string): Job | null {\n if (!message.Body) return null;\n\n try {\n const data: SQSJobData = JSON.parse(message.Body);\n const now = new Date();\n\n // Check if we have state for this job\n const state = this.jobStates.get(data.id);\n if (state) {\n // Update receipt handle\n state.receiptHandle = message.ReceiptHandle;\n return state.job;\n }\n\n const job: Job = {\n id: data.id,\n queue: queueName,\n payload: data.payload,\n status: 'pending',\n priority: data.priority,\n attempts: Number(message.Attributes?.ApproximateReceiveCount ?? 0),\n maxAttempts: data.maxAttempts,\n runAt: new Date(data.runAt),\n startedAt: null,\n completedAt: null,\n timeout: data.timeout,\n timeoutBehavior: data.timeoutBehavior,\n lastError: null,\n resultPointer: null,\n retryStrategy: data.retryStrategy,\n workerId: null,\n workerHeartbeat: null,\n createdAt: new Date(data.createdAt),\n updatedAt: now,\n };\n\n // Store state with receipt handle\n this.jobStates.set(data.id, {\n job,\n receiptHandle: message.ReceiptHandle,\n });\n\n return job;\n } catch {\n return null;\n }\n }\n\n /**\n * Enqueue a new job\n */\n async enqueue(options: JobCreateOptions): Promise<Job> {\n if (!this.initialized || !this.client || !this.awsSdkModule) {\n throw new Error('SQSJobStore not initialized');\n }\n\n const queueName = options.queue ?? 'default';\n const queueUrl = this.getQueueUrl(queueName);\n const now = new Date();\n const jobId = createId();\n\n const jobData: SQSJobData = {\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 runAt: (options.runAt ?? now).toISOString(),\n createdAt: now.toISOString(),\n };\n\n const messageParams: import('@aws-sdk/client-sqs').SendMessageCommandInput =\n {\n QueueUrl: queueUrl,\n MessageBody: JSON.stringify(jobData),\n // Use delay for scheduled jobs (max 15 minutes in SQS)\n DelaySeconds: options.runAt\n ? Math.min(\n 900,\n Math.max(\n 0,\n Math.floor((options.runAt.getTime() - now.getTime()) / 1000),\n ),\n )\n : undefined,\n };\n\n // Add FIFO-specific parameters\n if (this.config.useFifo) {\n messageParams.MessageGroupId = queueName;\n messageParams.MessageDeduplicationId = jobId;\n }\n\n await this.client.send(\n new this.awsSdkModule.SendMessageCommand(messageParams),\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, { job });\n\n await this.emitEvent('job.created', job);\n return job;\n }\n\n /**\n * Dequeue jobs ready for processing\n */\n async dequeue(\n queues: string[],\n limit: number,\n workerId: string,\n ): Promise<Job[]> {\n if (!this.initialized || !this.client || !this.awsSdkModule) {\n throw new Error('SQSJobStore not initialized');\n }\n\n const jobs: Job[] = [];\n\n for (const queueName of queues) {\n if (jobs.length >= limit) break;\n\n const queueUrl = this.getQueueUrl(queueName);\n\n const result = await this.client.send(\n new this.awsSdkModule.ReceiveMessageCommand({\n QueueUrl: queueUrl,\n MaxNumberOfMessages: Math.min(10, limit - jobs.length), // SQS max is 10\n VisibilityTimeout: this.config.visibilityTimeout,\n MessageSystemAttributeNames: ['ApproximateReceiveCount'],\n WaitTimeSeconds: 0, // Short poll for compatibility\n }),\n );\n\n if (result.Messages) {\n for (const message of result.Messages) {\n const job = this.sqsMessageToJob(message, queueName);\n if (job) {\n job.status = 'running';\n job.workerId = workerId;\n job.startedAt = new Date();\n jobs.push(job);\n\n await this.emitEvent('job.started', job);\n }\n }\n }\n }\n\n return jobs;\n }\n\n /**\n * Update a job - NOT SUPPORTED in SQS (messages are immutable)\n */\n async update(_id: string, _updates: Partial<Job>): Promise<Job> {\n throw new Error(\n 'SQS does not support updating jobs. Messages are immutable. ' +\n 'Consider using DynamoDB alongside SQS for full job state tracking.',\n );\n }\n\n /**\n * Get a job by ID (from in-memory state only)\n */\n async get(id: string): Promise<Job | null> {\n const state = this.jobStates.get(id);\n return state?.job ?? null;\n }\n\n /**\n * List jobs with filtering\n * Note: SQS doesn't support efficient listing - this only returns 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 message from SQS\n */\n async cancel(id: string): Promise<void> {\n if (!this.initialized || !this.client || !this.awsSdkModule) {\n throw new Error('SQSJobStore not initialized');\n }\n\n const state = this.jobStates.get(id);\n if (!state) {\n throw new Error(`Job ${id} not found or not yet received from queue`);\n }\n\n if (!state.receiptHandle) {\n throw new Error(\n `Job ${id} has no receipt handle - cannot delete from SQS`,\n );\n }\n\n const queueUrl = this.getQueueUrl(state.job.queue);\n\n await this.client.send(\n new this.awsSdkModule.DeleteMessageCommand({\n QueueUrl: queueUrl,\n ReceiptHandle: state.receiptHandle,\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 completed\n */\n async markCompleted(id: string, resultPointer?: string): Promise<void> {\n if (!this.initialized || !this.client || !this.awsSdkModule) {\n throw new Error('SQSJobStore not initialized');\n }\n\n const state = this.jobStates.get(id);\n if (!state || !state.receiptHandle) {\n throw new Error(`Job ${id} not found or has no receipt handle`);\n }\n\n const queueUrl = this.getQueueUrl(state.job.queue);\n\n // Delete message from queue (marks as processed)\n await this.client.send(\n new this.awsSdkModule.DeleteMessageCommand({\n QueueUrl: queueUrl,\n ReceiptHandle: state.receiptHandle,\n }),\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\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 // Message will become visible again after visibility timeout\n // or go to DLQ if configured\n\n await this.emitEvent('job.failed', state.job, { error });\n }\n\n /**\n * Clean up old jobs from in-memory state\n * Note: SQS handles message retention automatically\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 visibility timeout for a job (extends processing time)\n */\n async heartbeat(jobId: string, _workerId: string): Promise<void> {\n if (!this.initialized || !this.client || !this.awsSdkModule) {\n throw new Error('SQSJobStore not initialized');\n }\n\n const state = this.jobStates.get(jobId);\n if (!state || !state.receiptHandle) {\n return; // Job not found or no receipt handle\n }\n\n const queueUrl = this.getQueueUrl(state.job.queue);\n\n await this.client.send(\n new this.awsSdkModule.ChangeMessageVisibilityCommand({\n QueueUrl: queueUrl,\n ReceiptHandle: state.receiptHandle,\n VisibilityTimeout: this.config.visibilityTimeout,\n }),\n );\n\n state.job.workerHeartbeat = new Date();\n }\n\n /**\n * Get queue statistics\n */\n async stats(queue?: string): Promise<QueueStats> {\n if (!this.initialized || !this.client || !this.awsSdkModule) {\n throw new Error('SQSJobStore not initialized');\n }\n\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 // Count from in-memory state\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 SQS client\n */\n async close(): Promise<void> {\n if (this.client) {\n this.client.destroy();\n this.client = null;\n }\n this.jobStates.clear();\n this.queueUrls.clear();\n this.initialized = false;\n }\n}\n\n/**\n * Create an SQS job store instance\n */\nexport function createSQSJobStore(config: SQSJobStoreConfig): SQSJobStore {\n return new SQSJobStore(config);\n}\n\nexport default SQSJobStore;\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"sqs.js","names":[],"sources":["../../src/adapters/sqs.ts"],"sourcesContent":["/**\n * AWS SQS Job Store Adapter\n *\n * Uses AWS SQS for cloud-based job storage with automatic scaling.\n * SQS provides managed message queuing with features like\n * visibility timeout, dead letter queues, and FIFO ordering.\n *\n * @example\n * ```typescript\n * import { SQSJobStore } from '@happyvertical/jobs/adapters/sqs';\n *\n * const store = new SQSJobStore({\n * region: 'us-east-1',\n * queueUrlPrefix: 'https://sqs.us-east-1.amazonaws.com/123456789/myapp-',\n * // Or use credentials explicitly\n * credentials: {\n * accessKeyId: 'AKIA...',\n * secretAccessKey: '...',\n * },\n * });\n *\n * await store.initialize();\n * ```\n *\n * Note: This adapter requires the `@aws-sdk/client-sqs` package as a peer dependency.\n * Install it with: npm install @aws-sdk/client-sqs\n *\n * Important: SQS has some limitations for job storage:\n * - Jobs cannot be updated after enqueue (SQS messages are immutable)\n * - Job listing is limited (SQS doesn't support efficient listing)\n * - Cleanup is automatic via message retention policy\n * - Use DynamoDB alongside SQS for full job tracking if needed\n */\n\nimport type { Message, SQSClient as SQSClientType } from '@aws-sdk/client-sqs';\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 * AWS credentials configuration\n */\nexport interface AWSCredentials {\n accessKeyId: string;\n secretAccessKey: string;\n sessionToken?: string;\n}\n\n/**\n * SQS adapter configuration\n */\nexport interface SQSJobStoreConfig {\n /** AWS region */\n region?: string;\n /** AWS credentials (optional - uses default chain if not provided) */\n credentials?: AWSCredentials;\n /** Queue URL prefix (queues will be named {prefix}{queueName}) */\n queueUrlPrefix: string;\n /** Default visibility timeout in seconds */\n visibilityTimeout?: number;\n /** Message retention in days (1-14, default: 4) */\n messageRetentionDays?: number;\n /** Use FIFO queues for ordering guarantees */\n useFifo?: boolean;\n}\n\n/**\n * Job data stored in SQS message body\n */\ninterface SQSJobData {\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 runAt: string;\n createdAt: string;\n}\n\n/**\n * In-memory job state tracking (SQS messages are immutable)\n */\ninterface JobState {\n job: Job;\n receiptHandle?: string;\n}\n\n/**\n * SQS-based job store implementation\n *\n * Note: SQS has limitations that make some operations different:\n * - `update()` throws error (messages are immutable)\n * - `list()` only returns pending jobs from SQS\n * - `cancel()` requires the job to have been dequeued first\n * - For full job tracking, consider using DynamoDB alongside SQS\n */\nexport class SQSJobStore extends BaseJobStore {\n private config: SQSJobStoreConfig;\n private client: SQSClientType | null = null;\n // biome-ignore lint/style/useNamingConvention: AWS SDK module reference\n private awsSdkModule: typeof import('@aws-sdk/client-sqs') | null = null;\n private queueUrls: Map<string, string> = new Map();\n // In-memory state tracking for jobs that have been dequeued\n private jobStates: Map<string, JobState> = new Map();\n\n constructor(config: SQSJobStoreConfig) {\n super();\n this.config = {\n visibilityTimeout: 300, // 5 minutes default\n messageRetentionDays: 4,\n useFifo: false,\n ...config,\n };\n }\n\n /**\n * Initialize the store - dynamically imports AWS SDK\n */\n async initialize(): Promise<void> {\n if (this.initialized) return;\n\n try {\n // Dynamic import to avoid requiring AWS SDK as a hard dependency\n this.awsSdkModule = await import('@aws-sdk/client-sqs');\n } catch {\n throw new Error(\n 'AWS SDK is required for SQSJobStore. Install it with: npm install @aws-sdk/client-sqs',\n );\n }\n\n this.client = new this.awsSdkModule.SQSClient({\n region: this.config.region,\n credentials: this.config.credentials,\n });\n\n this.initialized = true;\n }\n\n /**\n * Get or create queue URL for a queue name\n */\n private getQueueUrl(queueName: string): string {\n if (!this.queueUrls.has(queueName)) {\n const suffix = this.config.useFifo ? '.fifo' : '';\n this.queueUrls.set(\n queueName,\n `${this.config.queueUrlPrefix}${queueName}${suffix}`,\n );\n }\n return this.queueUrls.get(queueName)!;\n }\n\n /**\n * Convert SQS message to Job format\n */\n private sqsMessageToJob(message: Message, queueName: string): Job | null {\n if (!message.Body) return null;\n\n try {\n const data: SQSJobData = JSON.parse(message.Body);\n const now = new Date();\n\n // Check if we have state for this job\n const state = this.jobStates.get(data.id);\n if (state) {\n // Update receipt handle\n state.receiptHandle = message.ReceiptHandle;\n return state.job;\n }\n\n const job: Job = {\n id: data.id,\n queue: queueName,\n payload: data.payload,\n status: 'pending',\n priority: data.priority,\n attempts: Number(message.Attributes?.ApproximateReceiveCount ?? 0),\n maxAttempts: data.maxAttempts,\n runAt: new Date(data.runAt),\n startedAt: null,\n completedAt: null,\n timeout: data.timeout,\n timeoutBehavior: data.timeoutBehavior,\n lastError: null,\n resultPointer: null,\n retryStrategy: data.retryStrategy,\n workerId: null,\n workerHeartbeat: null,\n createdAt: new Date(data.createdAt),\n updatedAt: now,\n };\n\n // Store state with receipt handle\n this.jobStates.set(data.id, {\n job,\n receiptHandle: message.ReceiptHandle,\n });\n\n return job;\n } catch {\n return null;\n }\n }\n\n /**\n * Enqueue a new job\n */\n async enqueue(options: JobCreateOptions): Promise<Job> {\n if (!this.initialized || !this.client || !this.awsSdkModule) {\n throw new Error('SQSJobStore not initialized');\n }\n\n const queueName = options.queue ?? 'default';\n const queueUrl = this.getQueueUrl(queueName);\n const now = new Date();\n const jobId = createId();\n\n const jobData: SQSJobData = {\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 runAt: (options.runAt ?? now).toISOString(),\n createdAt: now.toISOString(),\n };\n\n const messageParams: import('@aws-sdk/client-sqs').SendMessageCommandInput =\n {\n QueueUrl: queueUrl,\n MessageBody: JSON.stringify(jobData),\n // Use delay for scheduled jobs (max 15 minutes in SQS)\n DelaySeconds: options.runAt\n ? Math.min(\n 900,\n Math.max(\n 0,\n Math.floor((options.runAt.getTime() - now.getTime()) / 1000),\n ),\n )\n : undefined,\n };\n\n // Add FIFO-specific parameters\n if (this.config.useFifo) {\n messageParams.MessageGroupId = queueName;\n messageParams.MessageDeduplicationId = jobId;\n }\n\n await this.client.send(\n new this.awsSdkModule.SendMessageCommand(messageParams),\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, { job });\n\n await this.emitEvent('job.created', job);\n return job;\n }\n\n /**\n * Dequeue jobs ready for processing\n */\n async dequeue(\n queues: string[],\n limit: number,\n workerId: string,\n ): Promise<Job[]> {\n if (!this.initialized || !this.client || !this.awsSdkModule) {\n throw new Error('SQSJobStore not initialized');\n }\n\n const jobs: Job[] = [];\n\n for (const queueName of queues) {\n if (jobs.length >= limit) break;\n\n const queueUrl = this.getQueueUrl(queueName);\n\n const result = await this.client.send(\n new this.awsSdkModule.ReceiveMessageCommand({\n QueueUrl: queueUrl,\n MaxNumberOfMessages: Math.min(10, limit - jobs.length), // SQS max is 10\n VisibilityTimeout: this.config.visibilityTimeout,\n MessageSystemAttributeNames: ['ApproximateReceiveCount'],\n WaitTimeSeconds: 0, // Short poll for compatibility\n }),\n );\n\n if (result.Messages) {\n for (const message of result.Messages) {\n const job = this.sqsMessageToJob(message, queueName);\n if (job) {\n job.status = 'running';\n job.workerId = workerId;\n job.startedAt = new Date();\n jobs.push(job);\n\n await this.emitEvent('job.started', job);\n }\n }\n }\n }\n\n return jobs;\n }\n\n /**\n * Update a job - NOT SUPPORTED in SQS (messages are immutable)\n */\n async update(_id: string, _updates: Partial<Job>): Promise<Job> {\n throw new Error(\n 'SQS does not support updating jobs. Messages are immutable. ' +\n 'Consider using DynamoDB alongside SQS for full job state tracking.',\n );\n }\n\n /**\n * Get a job by ID (from in-memory state only)\n */\n async get(id: string): Promise<Job | null> {\n const state = this.jobStates.get(id);\n return state?.job ?? null;\n }\n\n /**\n * List jobs with filtering\n * Note: SQS doesn't support efficient listing - this only returns 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 message from SQS\n */\n async cancel(id: string): Promise<void> {\n if (!this.initialized || !this.client || !this.awsSdkModule) {\n throw new Error('SQSJobStore not initialized');\n }\n\n const state = this.jobStates.get(id);\n if (!state) {\n throw new Error(`Job ${id} not found or not yet received from queue`);\n }\n\n if (!state.receiptHandle) {\n throw new Error(\n `Job ${id} has no receipt handle - cannot delete from SQS`,\n );\n }\n\n const queueUrl = this.getQueueUrl(state.job.queue);\n\n await this.client.send(\n new this.awsSdkModule.DeleteMessageCommand({\n QueueUrl: queueUrl,\n ReceiptHandle: state.receiptHandle,\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 completed\n */\n async markCompleted(id: string, resultPointer?: string): Promise<void> {\n if (!this.initialized || !this.client || !this.awsSdkModule) {\n throw new Error('SQSJobStore not initialized');\n }\n\n const state = this.jobStates.get(id);\n if (!state || !state.receiptHandle) {\n throw new Error(`Job ${id} not found or has no receipt handle`);\n }\n\n const queueUrl = this.getQueueUrl(state.job.queue);\n\n // Delete message from queue (marks as processed)\n await this.client.send(\n new this.awsSdkModule.DeleteMessageCommand({\n QueueUrl: queueUrl,\n ReceiptHandle: state.receiptHandle,\n }),\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\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 // Message will become visible again after visibility timeout\n // or go to DLQ if configured\n\n await this.emitEvent('job.failed', state.job, { error });\n }\n\n /**\n * Clean up old jobs from in-memory state\n * Note: SQS handles message retention automatically\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 visibility timeout for a job (extends processing time)\n */\n async heartbeat(jobId: string, _workerId: string): Promise<void> {\n if (!this.initialized || !this.client || !this.awsSdkModule) {\n throw new Error('SQSJobStore not initialized');\n }\n\n const state = this.jobStates.get(jobId);\n if (!state || !state.receiptHandle) {\n return; // Job not found or no receipt handle\n }\n\n const queueUrl = this.getQueueUrl(state.job.queue);\n\n await this.client.send(\n new this.awsSdkModule.ChangeMessageVisibilityCommand({\n QueueUrl: queueUrl,\n ReceiptHandle: state.receiptHandle,\n VisibilityTimeout: this.config.visibilityTimeout,\n }),\n );\n\n state.job.workerHeartbeat = new Date();\n }\n\n /**\n * Get queue statistics\n */\n async stats(queue?: string): Promise<QueueStats> {\n if (!this.initialized || !this.client || !this.awsSdkModule) {\n throw new Error('SQSJobStore not initialized');\n }\n\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 // Count from in-memory state\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 SQS client\n */\n async close(): Promise<void> {\n if (this.client) {\n this.client.destroy();\n this.client = null;\n }\n this.jobStates.clear();\n this.queueUrls.clear();\n this.initialized = false;\n }\n}\n\n/**\n * Create an SQS job store instance\n */\nexport function createSQSJobStore(config: SQSJobStoreConfig): SQSJobStore {\n return new SQSJobStore(config);\n}\n\nexport default SQSJobStore;\n"],"mappings":"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"}
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
import { createId } from "@happyvertical/utils";
|
|
2
|
+
//#region src/retry.ts
|
|
3
|
+
/**
|
|
4
|
+
* Exponential backoff retry strategy
|
|
5
|
+
*
|
|
6
|
+
* Delay increases exponentially: initialDelay * multiplier^(attempt-1)
|
|
7
|
+
* With optional jitter to prevent thundering herd problem.
|
|
8
|
+
*/
|
|
9
|
+
var ExponentialBackoffStrategy = class {
|
|
10
|
+
initialDelay;
|
|
11
|
+
maxDelay;
|
|
12
|
+
multiplier;
|
|
13
|
+
jitter;
|
|
14
|
+
maxAttempts;
|
|
15
|
+
constructor(options = {}) {
|
|
16
|
+
this.initialDelay = options.initialDelay ?? 1e3;
|
|
17
|
+
this.maxDelay = options.maxDelay ?? 3e5;
|
|
18
|
+
this.multiplier = options.multiplier ?? 2;
|
|
19
|
+
this.jitter = options.jitter ?? true;
|
|
20
|
+
this.maxAttempts = options.maxAttempts ?? null;
|
|
21
|
+
}
|
|
22
|
+
shouldRetry(attempt, _error) {
|
|
23
|
+
if (this.maxAttempts !== null && attempt >= this.maxAttempts) return {
|
|
24
|
+
shouldRetry: false,
|
|
25
|
+
delay: 0
|
|
26
|
+
};
|
|
27
|
+
let delay = this.initialDelay * this.multiplier ** (attempt - 1);
|
|
28
|
+
delay = Math.min(delay, this.maxDelay);
|
|
29
|
+
if (this.jitter) {
|
|
30
|
+
const jitterRange = delay * .25;
|
|
31
|
+
delay = delay - jitterRange + Math.random() * jitterRange * 2;
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
shouldRetry: true,
|
|
35
|
+
delay: Math.round(delay)
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
toConfig() {
|
|
39
|
+
return {
|
|
40
|
+
type: "exponential",
|
|
41
|
+
config: {
|
|
42
|
+
initialDelay: this.initialDelay,
|
|
43
|
+
maxDelay: this.maxDelay,
|
|
44
|
+
multiplier: this.multiplier,
|
|
45
|
+
jitter: this.jitter,
|
|
46
|
+
maxAttempts: this.maxAttempts
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Linear retry strategy
|
|
53
|
+
*
|
|
54
|
+
* Uses a fixed delay between all retry attempts.
|
|
55
|
+
*/
|
|
56
|
+
var LinearBackoffStrategy = class {
|
|
57
|
+
delay;
|
|
58
|
+
maxAttempts;
|
|
59
|
+
constructor(options = {}) {
|
|
60
|
+
this.delay = options.delay ?? 5e3;
|
|
61
|
+
this.maxAttempts = options.maxAttempts ?? null;
|
|
62
|
+
}
|
|
63
|
+
shouldRetry(attempt, _error) {
|
|
64
|
+
if (this.maxAttempts !== null && attempt >= this.maxAttempts) return {
|
|
65
|
+
shouldRetry: false,
|
|
66
|
+
delay: 0
|
|
67
|
+
};
|
|
68
|
+
return {
|
|
69
|
+
shouldRetry: true,
|
|
70
|
+
delay: this.delay
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
toConfig() {
|
|
74
|
+
return {
|
|
75
|
+
type: "linear",
|
|
76
|
+
config: {
|
|
77
|
+
delay: this.delay,
|
|
78
|
+
maxAttempts: this.maxAttempts
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* Custom retry strategy
|
|
85
|
+
*
|
|
86
|
+
* Allows full control over retry logic via a custom function.
|
|
87
|
+
*/
|
|
88
|
+
var CustomRetryStrategy = class {
|
|
89
|
+
fn;
|
|
90
|
+
fnString;
|
|
91
|
+
constructor(fn) {
|
|
92
|
+
this.fn = fn;
|
|
93
|
+
this.fnString = fn.toString();
|
|
94
|
+
}
|
|
95
|
+
shouldRetry(attempt, error) {
|
|
96
|
+
return this.fn(attempt, error);
|
|
97
|
+
}
|
|
98
|
+
toConfig() {
|
|
99
|
+
return {
|
|
100
|
+
type: "custom",
|
|
101
|
+
config: { fn: this.fnString }
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
/**
|
|
106
|
+
* No retry strategy - never retry
|
|
107
|
+
*/
|
|
108
|
+
var NoRetryStrategy = class {
|
|
109
|
+
shouldRetry(_attempt, _error) {
|
|
110
|
+
return {
|
|
111
|
+
shouldRetry: false,
|
|
112
|
+
delay: 0
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
toConfig() {
|
|
116
|
+
return {
|
|
117
|
+
type: "linear",
|
|
118
|
+
config: { maxAttempts: 1 }
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
/**
|
|
123
|
+
* Create an exponential backoff retry strategy
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* ```typescript
|
|
127
|
+
* const strategy = exponential({
|
|
128
|
+
* initialDelay: 1000,
|
|
129
|
+
* maxDelay: 300000,
|
|
130
|
+
* multiplier: 2,
|
|
131
|
+
* jitter: true,
|
|
132
|
+
* });
|
|
133
|
+
* ```
|
|
134
|
+
*/
|
|
135
|
+
function exponential(options) {
|
|
136
|
+
return new ExponentialBackoffStrategy(options);
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Create a linear retry strategy with fixed delay
|
|
140
|
+
*
|
|
141
|
+
* @example
|
|
142
|
+
* ```typescript
|
|
143
|
+
* const strategy = linear({ delay: 5000 });
|
|
144
|
+
* ```
|
|
145
|
+
*/
|
|
146
|
+
function linear(options) {
|
|
147
|
+
return new LinearBackoffStrategy(options);
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Create a custom retry strategy
|
|
151
|
+
*
|
|
152
|
+
* @example
|
|
153
|
+
* ```typescript
|
|
154
|
+
* const strategy = custom((attempt, error) => {
|
|
155
|
+
* if (error.message.includes('RATE_LIMITED')) {
|
|
156
|
+
* return { shouldRetry: true, delay: 60000 };
|
|
157
|
+
* }
|
|
158
|
+
* return { shouldRetry: attempt < 3, delay: attempt * 1000 };
|
|
159
|
+
* });
|
|
160
|
+
* ```
|
|
161
|
+
*/
|
|
162
|
+
function custom(fn) {
|
|
163
|
+
return new CustomRetryStrategy(fn);
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Create a no-retry strategy
|
|
167
|
+
*
|
|
168
|
+
* @example
|
|
169
|
+
* ```typescript
|
|
170
|
+
* const strategy = noRetry();
|
|
171
|
+
* ```
|
|
172
|
+
*/
|
|
173
|
+
function noRetry() {
|
|
174
|
+
return new NoRetryStrategy();
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Reconstruct a retry strategy from its config
|
|
178
|
+
*/
|
|
179
|
+
function fromConfig(config) {
|
|
180
|
+
switch (config.type) {
|
|
181
|
+
case "exponential": return new ExponentialBackoffStrategy(config.config);
|
|
182
|
+
case "linear": return new LinearBackoffStrategy(config.config);
|
|
183
|
+
case "custom":
|
|
184
|
+
console.warn("Custom retry strategy cannot be reconstructed from config, using exponential");
|
|
185
|
+
return new ExponentialBackoffStrategy();
|
|
186
|
+
default: return new ExponentialBackoffStrategy();
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Default retry strategy
|
|
191
|
+
*/
|
|
192
|
+
var DEFAULT_RETRY_STRATEGY = exponential({
|
|
193
|
+
initialDelay: 1e3,
|
|
194
|
+
maxDelay: 3e5,
|
|
195
|
+
multiplier: 2,
|
|
196
|
+
jitter: true
|
|
197
|
+
});
|
|
198
|
+
//#endregion
|
|
199
|
+
//#region src/base-store.ts
|
|
200
|
+
/**
|
|
201
|
+
* Valid table name pattern: alphanumeric and underscores, must start with letter or underscore
|
|
202
|
+
*/
|
|
203
|
+
var TABLE_NAME_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
204
|
+
/**
|
|
205
|
+
* Validate table name to prevent SQL injection
|
|
206
|
+
* @throws Error if table name contains invalid characters
|
|
207
|
+
*/
|
|
208
|
+
function validateTableName(tableName) {
|
|
209
|
+
if (!tableName || tableName.length === 0) throw new Error("Table name cannot be empty");
|
|
210
|
+
if (tableName.length > 128) throw new Error("Table name cannot exceed 128 characters");
|
|
211
|
+
if (!TABLE_NAME_PATTERN.test(tableName)) throw new Error(`Invalid table name "${tableName}": must contain only alphanumeric characters and underscores, and must start with a letter or underscore`);
|
|
212
|
+
return tableName;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Convert priority string to number
|
|
216
|
+
*/
|
|
217
|
+
function priorityToNumber(priority) {
|
|
218
|
+
if (typeof priority === "number") return priority;
|
|
219
|
+
switch (priority) {
|
|
220
|
+
case "critical": return 100;
|
|
221
|
+
case "high": return 75;
|
|
222
|
+
case "normal": return 50;
|
|
223
|
+
case "low": return 25;
|
|
224
|
+
default: return 50;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Base job store with common functionality
|
|
229
|
+
*/
|
|
230
|
+
var BaseJobStore = class {
|
|
231
|
+
listeners = /* @__PURE__ */ new Set();
|
|
232
|
+
initialized = false;
|
|
233
|
+
/**
|
|
234
|
+
* Create a new job record
|
|
235
|
+
*/
|
|
236
|
+
createJobRecord(options) {
|
|
237
|
+
const now = /* @__PURE__ */ new Date();
|
|
238
|
+
const retryConfig = options.retryStrategy && "toConfig" in options.retryStrategy ? options.retryStrategy.toConfig() : options.retryStrategy ?? DEFAULT_RETRY_STRATEGY.toConfig();
|
|
239
|
+
return {
|
|
240
|
+
id: createId(),
|
|
241
|
+
queue: options.queue ?? "default",
|
|
242
|
+
payload: options.payload,
|
|
243
|
+
status: "pending",
|
|
244
|
+
priority: priorityToNumber(options.priority),
|
|
245
|
+
attempts: 0,
|
|
246
|
+
maxAttempts: options.maxAttempts ?? 3,
|
|
247
|
+
runAt: options.runAt ?? now,
|
|
248
|
+
startedAt: null,
|
|
249
|
+
completedAt: null,
|
|
250
|
+
timeout: options.timeout ?? 3e5,
|
|
251
|
+
timeoutBehavior: options.timeoutBehavior ?? "fail",
|
|
252
|
+
lastError: null,
|
|
253
|
+
resultPointer: null,
|
|
254
|
+
retryStrategy: retryConfig,
|
|
255
|
+
workerId: null,
|
|
256
|
+
workerHeartbeat: null,
|
|
257
|
+
createdAt: now,
|
|
258
|
+
updatedAt: now
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Emit a job event to all listeners
|
|
263
|
+
*/
|
|
264
|
+
async emitEvent(type, job, extra) {
|
|
265
|
+
const event = {
|
|
266
|
+
type,
|
|
267
|
+
job,
|
|
268
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
269
|
+
...extra
|
|
270
|
+
};
|
|
271
|
+
const promises = Array.from(this.listeners).map((listener) => Promise.resolve(listener(event)).catch((err) => {
|
|
272
|
+
console.error("Job event listener error:", err);
|
|
273
|
+
}));
|
|
274
|
+
await Promise.allSettled(promises);
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Subscribe to job events
|
|
278
|
+
*/
|
|
279
|
+
subscribe(listener) {
|
|
280
|
+
this.listeners.add(listener);
|
|
281
|
+
return () => {
|
|
282
|
+
this.listeners.delete(listener);
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Build WHERE clause for filters
|
|
287
|
+
*/
|
|
288
|
+
buildFilterWhere(filter) {
|
|
289
|
+
const conditions = [];
|
|
290
|
+
const params = [];
|
|
291
|
+
if (filter.queue) {
|
|
292
|
+
conditions.push("queue = ?");
|
|
293
|
+
params.push(filter.queue);
|
|
294
|
+
}
|
|
295
|
+
if (filter.status) if (Array.isArray(filter.status)) {
|
|
296
|
+
const placeholders = filter.status.map(() => "?").join(", ");
|
|
297
|
+
conditions.push(`status IN (${placeholders})`);
|
|
298
|
+
params.push(...filter.status);
|
|
299
|
+
} else {
|
|
300
|
+
conditions.push("status = ?");
|
|
301
|
+
params.push(filter.status);
|
|
302
|
+
}
|
|
303
|
+
if (filter.objectType) {
|
|
304
|
+
conditions.push("json_extract(payload, '$.objectType') = ?");
|
|
305
|
+
params.push(filter.objectType);
|
|
306
|
+
}
|
|
307
|
+
if (filter.method) {
|
|
308
|
+
conditions.push("json_extract(payload, '$.method') = ?");
|
|
309
|
+
params.push(filter.method);
|
|
310
|
+
}
|
|
311
|
+
if (filter.createdAfter) {
|
|
312
|
+
conditions.push("created_at > ?");
|
|
313
|
+
params.push(filter.createdAfter.toISOString());
|
|
314
|
+
}
|
|
315
|
+
if (filter.createdBefore) {
|
|
316
|
+
conditions.push("created_at < ?");
|
|
317
|
+
params.push(filter.createdBefore.toISOString());
|
|
318
|
+
}
|
|
319
|
+
return {
|
|
320
|
+
where: conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "",
|
|
321
|
+
params
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Build ORDER BY clause for filters
|
|
326
|
+
*/
|
|
327
|
+
buildOrderBy(filter) {
|
|
328
|
+
const field = filter.orderBy ?? "createdAt";
|
|
329
|
+
const dir = filter.orderDir ?? "desc";
|
|
330
|
+
return `ORDER BY ${{
|
|
331
|
+
createdAt: "created_at",
|
|
332
|
+
runAt: "run_at",
|
|
333
|
+
priority: "priority",
|
|
334
|
+
attempts: "attempts"
|
|
335
|
+
}[field] ?? "created_at"} ${dir.toUpperCase()}`;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Build LIMIT/OFFSET clause
|
|
339
|
+
*/
|
|
340
|
+
buildLimitOffset(filter) {
|
|
341
|
+
const params = [];
|
|
342
|
+
let clause = "";
|
|
343
|
+
if (filter.limit) {
|
|
344
|
+
clause = "LIMIT ?";
|
|
345
|
+
params.push(filter.limit);
|
|
346
|
+
if (filter.offset) {
|
|
347
|
+
clause += " OFFSET ?";
|
|
348
|
+
params.push(filter.offset);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return {
|
|
352
|
+
clause,
|
|
353
|
+
params
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Parse a job row from the database
|
|
358
|
+
*/
|
|
359
|
+
parseJobRow(row) {
|
|
360
|
+
return {
|
|
361
|
+
id: row.id,
|
|
362
|
+
queue: row.queue,
|
|
363
|
+
payload: typeof row.payload === "string" ? JSON.parse(row.payload) : row.payload,
|
|
364
|
+
status: row.status,
|
|
365
|
+
priority: row.priority,
|
|
366
|
+
attempts: row.attempts,
|
|
367
|
+
maxAttempts: row.max_attempts,
|
|
368
|
+
runAt: new Date(row.run_at),
|
|
369
|
+
startedAt: row.started_at ? new Date(row.started_at) : null,
|
|
370
|
+
completedAt: row.completed_at ? new Date(row.completed_at) : null,
|
|
371
|
+
timeout: row.timeout,
|
|
372
|
+
timeoutBehavior: row.timeout_behavior,
|
|
373
|
+
lastError: row.last_error,
|
|
374
|
+
resultPointer: row.result_pointer,
|
|
375
|
+
retryStrategy: typeof row.retry_strategy === "string" ? JSON.parse(row.retry_strategy) : row.retry_strategy,
|
|
376
|
+
workerId: row.worker_id,
|
|
377
|
+
workerHeartbeat: row.worker_heartbeat ? new Date(row.worker_heartbeat) : null,
|
|
378
|
+
createdAt: new Date(row.created_at),
|
|
379
|
+
updatedAt: new Date(row.updated_at)
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
//#endregion
|
|
384
|
+
export { custom as a, linear as c, DEFAULT_RETRY_STRATEGY as i, noRetry as l, priorityToNumber as n, exponential as o, validateTableName as r, fromConfig as s, BaseJobStore as t };
|
|
385
|
+
|
|
386
|
+
//# sourceMappingURL=base-store-DIasEzL0.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"base-store-DIasEzL0.js","names":[],"sources":["../../src/retry.ts","../../src/base-store.ts"],"sourcesContent":["import type {\n RetryDecision,\n RetryStrategy,\n RetryStrategyConfig,\n} from './types.js';\n\n/**\n * Options for exponential backoff retry strategy\n */\nexport interface ExponentialBackoffOptions {\n /** Initial delay in milliseconds (default: 1000) */\n initialDelay?: number;\n /** Maximum delay in milliseconds (default: 300000 = 5 minutes) */\n maxDelay?: number;\n /** Multiplier for each attempt (default: 2) */\n multiplier?: number;\n /** Add random jitter to prevent thundering herd (default: true) */\n jitter?: boolean;\n /** Maximum attempts (optional, can also be set on job) */\n maxAttempts?: number;\n}\n\n/**\n * Exponential backoff retry strategy\n *\n * Delay increases exponentially: initialDelay * multiplier^(attempt-1)\n * With optional jitter to prevent thundering herd problem.\n */\nclass ExponentialBackoffStrategy implements RetryStrategy {\n private readonly initialDelay: number;\n private readonly maxDelay: number;\n private readonly multiplier: number;\n private readonly jitter: boolean;\n private readonly maxAttempts: number | null;\n\n constructor(options: ExponentialBackoffOptions = {}) {\n this.initialDelay = options.initialDelay ?? 1000;\n this.maxDelay = options.maxDelay ?? 300000;\n this.multiplier = options.multiplier ?? 2;\n this.jitter = options.jitter ?? true;\n this.maxAttempts = options.maxAttempts ?? null;\n }\n\n shouldRetry(attempt: number, _error: Error): RetryDecision {\n // Check max attempts if configured\n if (this.maxAttempts !== null && attempt >= this.maxAttempts) {\n return { shouldRetry: false, delay: 0 };\n }\n\n // Calculate base delay: initialDelay * multiplier^(attempt-1)\n let delay = this.initialDelay * this.multiplier ** (attempt - 1);\n\n // Cap at maxDelay\n delay = Math.min(delay, this.maxDelay);\n\n // Add jitter (±25% randomization)\n if (this.jitter) {\n const jitterRange = delay * 0.25;\n delay = delay - jitterRange + Math.random() * jitterRange * 2;\n }\n\n return { shouldRetry: true, delay: Math.round(delay) };\n }\n\n toConfig(): RetryStrategyConfig {\n return {\n type: 'exponential',\n config: {\n initialDelay: this.initialDelay,\n maxDelay: this.maxDelay,\n multiplier: this.multiplier,\n jitter: this.jitter,\n maxAttempts: this.maxAttempts,\n },\n };\n }\n}\n\n/**\n * Options for linear retry strategy\n */\nexport interface LinearBackoffOptions {\n /** Fixed delay between retries in milliseconds (default: 5000) */\n delay?: number;\n /** Maximum attempts (optional) */\n maxAttempts?: number;\n}\n\n/**\n * Linear retry strategy\n *\n * Uses a fixed delay between all retry attempts.\n */\nclass LinearBackoffStrategy implements RetryStrategy {\n private readonly delay: number;\n private readonly maxAttempts: number | null;\n\n constructor(options: LinearBackoffOptions = {}) {\n this.delay = options.delay ?? 5000;\n this.maxAttempts = options.maxAttempts ?? null;\n }\n\n shouldRetry(attempt: number, _error: Error): RetryDecision {\n if (this.maxAttempts !== null && attempt >= this.maxAttempts) {\n return { shouldRetry: false, delay: 0 };\n }\n\n return { shouldRetry: true, delay: this.delay };\n }\n\n toConfig(): RetryStrategyConfig {\n return {\n type: 'linear',\n config: {\n delay: this.delay,\n maxAttempts: this.maxAttempts,\n },\n };\n }\n}\n\n/**\n * Custom retry decision function\n */\nexport type CustomRetryFn = (attempt: number, error: Error) => RetryDecision;\n\n/**\n * Custom retry strategy\n *\n * Allows full control over retry logic via a custom function.\n */\nclass CustomRetryStrategy implements RetryStrategy {\n private readonly fn: CustomRetryFn;\n private readonly fnString: string;\n\n constructor(fn: CustomRetryFn) {\n this.fn = fn;\n // Store string representation for serialization\n this.fnString = fn.toString();\n }\n\n shouldRetry(attempt: number, error: Error): RetryDecision {\n return this.fn(attempt, error);\n }\n\n toConfig(): RetryStrategyConfig {\n return {\n type: 'custom',\n config: {\n fn: this.fnString,\n },\n };\n }\n}\n\n/**\n * No retry strategy - never retry\n */\nclass NoRetryStrategy implements RetryStrategy {\n shouldRetry(_attempt: number, _error: Error): RetryDecision {\n return { shouldRetry: false, delay: 0 };\n }\n\n toConfig(): RetryStrategyConfig {\n return {\n type: 'linear',\n config: { maxAttempts: 1 },\n };\n }\n}\n\n// Factory functions\n\n/**\n * Create an exponential backoff retry strategy\n *\n * @example\n * ```typescript\n * const strategy = exponential({\n * initialDelay: 1000,\n * maxDelay: 300000,\n * multiplier: 2,\n * jitter: true,\n * });\n * ```\n */\nexport function exponential(\n options?: ExponentialBackoffOptions,\n): RetryStrategy {\n return new ExponentialBackoffStrategy(options);\n}\n\n/**\n * Create a linear retry strategy with fixed delay\n *\n * @example\n * ```typescript\n * const strategy = linear({ delay: 5000 });\n * ```\n */\nexport function linear(options?: LinearBackoffOptions): RetryStrategy {\n return new LinearBackoffStrategy(options);\n}\n\n/**\n * Create a custom retry strategy\n *\n * @example\n * ```typescript\n * const strategy = custom((attempt, error) => {\n * if (error.message.includes('RATE_LIMITED')) {\n * return { shouldRetry: true, delay: 60000 };\n * }\n * return { shouldRetry: attempt < 3, delay: attempt * 1000 };\n * });\n * ```\n */\nexport function custom(fn: CustomRetryFn): RetryStrategy {\n return new CustomRetryStrategy(fn);\n}\n\n/**\n * Create a no-retry strategy\n *\n * @example\n * ```typescript\n * const strategy = noRetry();\n * ```\n */\nexport function noRetry(): RetryStrategy {\n return new NoRetryStrategy();\n}\n\n/**\n * Reconstruct a retry strategy from its config\n */\nexport function fromConfig(config: RetryStrategyConfig): RetryStrategy {\n switch (config.type) {\n case 'exponential':\n return new ExponentialBackoffStrategy(\n config.config as ExponentialBackoffOptions,\n );\n case 'linear':\n return new LinearBackoffStrategy(config.config as LinearBackoffOptions);\n case 'custom':\n // For custom strategies loaded from config, we can't reconstruct the function\n // So we fall back to exponential\n console.warn(\n 'Custom retry strategy cannot be reconstructed from config, using exponential',\n );\n return new ExponentialBackoffStrategy();\n default:\n return new ExponentialBackoffStrategy();\n }\n}\n\n/**\n * Default retry strategy\n */\nexport const DEFAULT_RETRY_STRATEGY = exponential({\n initialDelay: 1000,\n maxDelay: 300000,\n multiplier: 2,\n jitter: true,\n});\n","import { createId } from '@happyvertical/utils';\nimport { DEFAULT_RETRY_STRATEGY } from './retry.js';\nimport type {\n CleanupOptions,\n Job,\n JobCreateOptions,\n JobEvent,\n JobEventListener,\n JobEventType,\n JobFilter,\n JobPriority,\n JobStore,\n QueueStats,\n RetryStrategyConfig,\n} from './types.js';\n\n/**\n * Valid table name pattern: alphanumeric and underscores, must start with letter or underscore\n */\nconst TABLE_NAME_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/;\n\n/**\n * Validate table name to prevent SQL injection\n * @throws Error if table name contains invalid characters\n */\nexport function validateTableName(tableName: string): string {\n if (!tableName || tableName.length === 0) {\n throw new Error('Table name cannot be empty');\n }\n if (tableName.length > 128) {\n throw new Error('Table name cannot exceed 128 characters');\n }\n if (!TABLE_NAME_PATTERN.test(tableName)) {\n throw new Error(\n `Invalid table name \"${tableName}\": must contain only alphanumeric characters and underscores, and must start with a letter or underscore`,\n );\n }\n return tableName;\n}\n\n/**\n * Convert priority string to number\n */\nexport function priorityToNumber(\n priority: JobPriority | number | undefined,\n): number {\n if (typeof priority === 'number') return priority;\n switch (priority) {\n case 'critical':\n return 100;\n case 'high':\n return 75;\n case 'normal':\n return 50;\n case 'low':\n return 25;\n default:\n return 50;\n }\n}\n\n/**\n * Base job store with common functionality\n */\nexport abstract class BaseJobStore implements JobStore {\n protected listeners: Set<JobEventListener> = new Set();\n protected initialized = false;\n\n /**\n * Initialize the store - must be implemented by subclasses\n */\n abstract initialize(): Promise<void>;\n\n /**\n * Create a new job record\n */\n protected createJobRecord(options: JobCreateOptions): Job {\n const now = new Date();\n const retryConfig: RetryStrategyConfig =\n options.retryStrategy && 'toConfig' in options.retryStrategy\n ? options.retryStrategy.toConfig()\n : ((options.retryStrategy as RetryStrategyConfig) ??\n DEFAULT_RETRY_STRATEGY.toConfig());\n\n return {\n id: createId(),\n queue: options.queue ?? 'default',\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: retryConfig,\n workerId: null,\n workerHeartbeat: null,\n createdAt: now,\n updatedAt: now,\n };\n }\n\n /**\n * Emit a job event to all listeners\n */\n protected async emitEvent(\n type: JobEventType,\n job: Job,\n extra?: { error?: string; resultPointer?: string },\n ): Promise<void> {\n const event: JobEvent = {\n type,\n job,\n timestamp: new Date(),\n ...extra,\n };\n\n const promises = Array.from(this.listeners).map((listener) =>\n Promise.resolve(listener(event)).catch((err) => {\n console.error('Job event listener error:', err);\n }),\n );\n\n await Promise.allSettled(promises);\n }\n\n /**\n * Subscribe to job events\n */\n subscribe(listener: JobEventListener): () => void {\n this.listeners.add(listener);\n return () => {\n this.listeners.delete(listener);\n };\n }\n\n /**\n * Build WHERE clause for filters\n */\n protected buildFilterWhere(filter: JobFilter): {\n where: string;\n params: unknown[];\n } {\n const conditions: string[] = [];\n const params: unknown[] = [];\n\n if (filter.queue) {\n conditions.push('queue = ?');\n params.push(filter.queue);\n }\n\n if (filter.status) {\n if (Array.isArray(filter.status)) {\n const placeholders = filter.status.map(() => '?').join(', ');\n conditions.push(`status IN (${placeholders})`);\n params.push(...filter.status);\n } else {\n conditions.push('status = ?');\n params.push(filter.status);\n }\n }\n\n if (filter.objectType) {\n conditions.push(\"json_extract(payload, '$.objectType') = ?\");\n params.push(filter.objectType);\n }\n\n if (filter.method) {\n conditions.push(\"json_extract(payload, '$.method') = ?\");\n params.push(filter.method);\n }\n\n if (filter.createdAfter) {\n conditions.push('created_at > ?');\n params.push(filter.createdAfter.toISOString());\n }\n\n if (filter.createdBefore) {\n conditions.push('created_at < ?');\n params.push(filter.createdBefore.toISOString());\n }\n\n return {\n where: conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '',\n params,\n };\n }\n\n /**\n * Build ORDER BY clause for filters\n */\n protected buildOrderBy(filter: JobFilter): string {\n const field = filter.orderBy ?? 'createdAt';\n const dir = filter.orderDir ?? 'desc';\n\n const fieldMap: Record<string, string> = {\n createdAt: 'created_at',\n runAt: 'run_at',\n priority: 'priority',\n attempts: 'attempts',\n };\n\n return `ORDER BY ${fieldMap[field] ?? 'created_at'} ${dir.toUpperCase()}`;\n }\n\n /**\n * Build LIMIT/OFFSET clause\n */\n protected buildLimitOffset(filter: JobFilter): {\n clause: string;\n params: unknown[];\n } {\n const params: unknown[] = [];\n let clause = '';\n\n if (filter.limit) {\n clause = 'LIMIT ?';\n params.push(filter.limit);\n\n if (filter.offset) {\n clause += ' OFFSET ?';\n params.push(filter.offset);\n }\n }\n\n return { clause, params };\n }\n\n /**\n * Parse a job row from the database\n */\n protected parseJobRow(row: Record<string, unknown>): Job {\n return {\n id: row.id as string,\n queue: row.queue as string,\n payload:\n typeof row.payload === 'string' ? JSON.parse(row.payload) : row.payload,\n status: row.status as Job['status'],\n priority: row.priority as number,\n attempts: row.attempts as number,\n maxAttempts: row.max_attempts as number,\n runAt: new Date(row.run_at as string),\n startedAt: row.started_at ? new Date(row.started_at as string) : null,\n completedAt: row.completed_at\n ? new Date(row.completed_at as string)\n : null,\n timeout: row.timeout as number,\n timeoutBehavior: row.timeout_behavior as Job['timeoutBehavior'],\n lastError: row.last_error as string | null,\n resultPointer: row.result_pointer as string | null,\n retryStrategy:\n typeof row.retry_strategy === 'string'\n ? JSON.parse(row.retry_strategy)\n : row.retry_strategy,\n workerId: row.worker_id as string | null,\n workerHeartbeat: row.worker_heartbeat\n ? new Date(row.worker_heartbeat as string)\n : null,\n createdAt: new Date(row.created_at as string),\n updatedAt: new Date(row.updated_at as string),\n };\n }\n\n // Abstract methods that must be implemented by subclasses\n abstract enqueue(options: JobCreateOptions): Promise<Job>;\n abstract dequeue(\n queues: string[],\n limit: number,\n workerId: string,\n ): Promise<Job[]>;\n abstract update(id: string, updates: Partial<Job>): Promise<Job>;\n abstract get(id: string): Promise<Job | null>;\n abstract list(filter: JobFilter): Promise<Job[]>;\n abstract cancel(id: string): Promise<void>;\n abstract cleanup(options: CleanupOptions): Promise<number>;\n abstract heartbeat(jobId: string, workerId: string): Promise<void>;\n abstract stats(queue?: string): Promise<QueueStats>;\n abstract close(): Promise<void>;\n}\n"],"mappings":"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"}
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { existsSync, mkdirSync
|
|
2
|
+
import { copyFileSync, existsSync, mkdirSync } from "node:fs";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
if (existsSync(metaSrc)) {
|
|
18
|
-
copyFileSync(metaSrc, join(targetDir, `have-${pkgName}.meta.json`));
|
|
19
|
-
}
|
|
5
|
+
//#region src/cli/claude-context.ts
|
|
6
|
+
/**
|
|
7
|
+
* CLI script to install agent context for @happyvertical/jobs
|
|
8
|
+
* Run the published context installer binary for this package.
|
|
9
|
+
*/
|
|
10
|
+
var pkgRoot = join(dirname(fileURLToPath(import.meta.url)), "../..");
|
|
11
|
+
var targetDir = join(process.cwd(), ".claude");
|
|
12
|
+
if (!existsSync(targetDir)) mkdirSync(targetDir, { recursive: true });
|
|
13
|
+
var pkgName = "jobs";
|
|
14
|
+
var agentMdSrc = existsSync(join(pkgRoot, "AGENT.md")) ? join(pkgRoot, "AGENT.md") : join(pkgRoot, "CLAUDE.md");
|
|
15
|
+
var metaSrc = existsSync(join(pkgRoot, "metadata.json")) ? join(pkgRoot, "metadata.json") : join(pkgRoot, ".claude-meta.json");
|
|
16
|
+
if (existsSync(agentMdSrc)) copyFileSync(agentMdSrc, join(targetDir, `have-${pkgName}.md`));
|
|
17
|
+
if (existsSync(metaSrc)) copyFileSync(metaSrc, join(targetDir, `have-${pkgName}.meta.json`));
|
|
20
18
|
console.log(`✓ Installed @happyvertical/${pkgName} context to .claude/`);
|
|
21
|
-
//#
|
|
19
|
+
//#endregion
|
|
20
|
+
|
|
21
|
+
//# sourceMappingURL=claude-context.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"claude-context.js","sources":["../../src/cli/claude-context.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * CLI script to install agent context for @happyvertical/jobs\n * Run the published context installer binary for this package.\n */\nimport { copyFileSync, existsSync, mkdirSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nconst Dirname = dirname(fileURLToPath(import.meta.url));\nconst pkgRoot = join(Dirname, '../..');\nconst targetDir = join(process.cwd(), '.claude');\n\nif (!existsSync(targetDir)) {\n mkdirSync(targetDir, { recursive: true });\n}\n\nconst pkgName = 'jobs';\nconst agentMdSrc = existsSync(join(pkgRoot, 'AGENT.md'))\n ? join(pkgRoot, 'AGENT.md')\n : join(pkgRoot, 'CLAUDE.md');\nconst metaSrc = existsSync(join(pkgRoot, 'metadata.json'))\n ? join(pkgRoot, 'metadata.json')\n : join(pkgRoot, '.claude-meta.json');\n\nif (existsSync(agentMdSrc)) {\n copyFileSync(agentMdSrc, join(targetDir, `have-${pkgName}.md`));\n}\n\nif (existsSync(metaSrc)) {\n copyFileSync(metaSrc, join(targetDir, `have-${pkgName}.meta.json`));\n}\n\nconsole.log(`✓ Installed @happyvertical/${pkgName} context to .claude/`);\n"],"
|
|
1
|
+
{"version":3,"file":"claude-context.js","names":[],"sources":["../../src/cli/claude-context.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * CLI script to install agent context for @happyvertical/jobs\n * Run the published context installer binary for this package.\n */\nimport { copyFileSync, existsSync, mkdirSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nconst Dirname = dirname(fileURLToPath(import.meta.url));\nconst pkgRoot = join(Dirname, '../..');\nconst targetDir = join(process.cwd(), '.claude');\n\nif (!existsSync(targetDir)) {\n mkdirSync(targetDir, { recursive: true });\n}\n\nconst pkgName = 'jobs';\nconst agentMdSrc = existsSync(join(pkgRoot, 'AGENT.md'))\n ? join(pkgRoot, 'AGENT.md')\n : join(pkgRoot, 'CLAUDE.md');\nconst metaSrc = existsSync(join(pkgRoot, 'metadata.json'))\n ? join(pkgRoot, 'metadata.json')\n : join(pkgRoot, '.claude-meta.json');\n\nif (existsSync(agentMdSrc)) {\n copyFileSync(agentMdSrc, join(targetDir, `have-${pkgName}.md`));\n}\n\nif (existsSync(metaSrc)) {\n copyFileSync(metaSrc, join(targetDir, `have-${pkgName}.meta.json`));\n}\n\nconsole.log(`✓ Installed @happyvertical/${pkgName} context to .claude/`);\n"],"mappings":";;;;;;;;;AAUA,IAAM,UAAU,KADA,QAAQ,cAAc,OAAO,KAAK,GAAG,CAChC,GAAS,OAAO;AACrC,IAAM,YAAY,KAAK,QAAQ,IAAI,GAAG,SAAS;AAE/C,IAAI,CAAC,WAAW,SAAS,GACvB,UAAU,WAAW,EAAE,WAAW,KAAK,CAAC;AAG1C,IAAM,UAAU;AAChB,IAAM,aAAa,WAAW,KAAK,SAAS,UAAU,CAAC,IACnD,KAAK,SAAS,UAAU,IACxB,KAAK,SAAS,WAAW;AAC7B,IAAM,UAAU,WAAW,KAAK,SAAS,eAAe,CAAC,IACrD,KAAK,SAAS,eAAe,IAC7B,KAAK,SAAS,mBAAmB;AAErC,IAAI,WAAW,UAAU,GACvB,aAAa,YAAY,KAAK,WAAW,QAAQ,QAAQ,IAAI,CAAC;AAGhE,IAAI,WAAW,OAAO,GACpB,aAAa,SAAS,KAAK,WAAW,QAAQ,QAAQ,WAAW,CAAC;AAGpE,QAAQ,IAAI,8BAA8B,QAAQ,qBAAqB"}
|