@vibesdotdev/integrations-kafka 0.0.0-stage → 1.0.0-rc.1

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 (166) hide show
  1. package/AUDIT-CODE.md +40 -0
  2. package/AUDIT-SEAL.md +12 -0
  3. package/LICENSE +126 -0
  4. package/README.md +76 -2
  5. package/SEALED.md +13 -0
  6. package/dist/admin.d.ts +20 -0
  7. package/dist/admin.d.ts.map +1 -0
  8. package/dist/admin.js +51 -0
  9. package/dist/admin.js.map +1 -0
  10. package/dist/auth/sasl.node.impl.d.ts +18 -0
  11. package/dist/auth/sasl.node.impl.d.ts.map +1 -0
  12. package/dist/auth/sasl.node.impl.js +62 -0
  13. package/dist/auth/sasl.node.impl.js.map +1 -0
  14. package/dist/chat/realtime-transport/host.d.ts +14 -0
  15. package/dist/chat/realtime-transport/host.d.ts.map +1 -0
  16. package/dist/chat/realtime-transport/host.js +7 -0
  17. package/dist/chat/realtime-transport/host.js.map +1 -0
  18. package/dist/chat/realtime-transport/index.d.ts +15 -0
  19. package/dist/chat/realtime-transport/index.d.ts.map +1 -0
  20. package/dist/chat/realtime-transport/index.js +13 -0
  21. package/dist/chat/realtime-transport/index.js.map +1 -0
  22. package/dist/chat/realtime-transport/lifecycle.d.ts +30 -0
  23. package/dist/chat/realtime-transport/lifecycle.d.ts.map +1 -0
  24. package/dist/chat/realtime-transport/lifecycle.js +125 -0
  25. package/dist/chat/realtime-transport/lifecycle.js.map +1 -0
  26. package/dist/chat/realtime-transport/projection.d.ts +127 -0
  27. package/dist/chat/realtime-transport/projection.d.ts.map +1 -0
  28. package/dist/chat/realtime-transport/projection.js +156 -0
  29. package/dist/chat/realtime-transport/projection.js.map +1 -0
  30. package/dist/chat/realtime-transport/sse-kafka-bridge.d.ts +32 -0
  31. package/dist/chat/realtime-transport/sse-kafka-bridge.d.ts.map +1 -0
  32. package/dist/chat/realtime-transport/sse-kafka-bridge.js +58 -0
  33. package/dist/chat/realtime-transport/sse-kafka-bridge.js.map +1 -0
  34. package/dist/chat/realtime-transport/ws-kafka-bridge.d.ts +10 -0
  35. package/dist/chat/realtime-transport/ws-kafka-bridge.d.ts.map +1 -0
  36. package/dist/chat/realtime-transport/ws-kafka-bridge.js +13 -0
  37. package/dist/chat/realtime-transport/ws-kafka-bridge.js.map +1 -0
  38. package/dist/client.d.ts +16 -0
  39. package/dist/client.d.ts.map +1 -0
  40. package/dist/client.js +29 -0
  41. package/dist/client.js.map +1 -0
  42. package/dist/config/kafka.config.descriptor.d.ts +109 -0
  43. package/dist/config/kafka.config.descriptor.d.ts.map +1 -0
  44. package/dist/config/kafka.config.descriptor.js +60 -0
  45. package/dist/config/kafka.config.descriptor.js.map +1 -0
  46. package/dist/connections.d.ts +27 -0
  47. package/dist/connections.d.ts.map +1 -0
  48. package/dist/connections.js +20 -0
  49. package/dist/connections.js.map +1 -0
  50. package/dist/connections.node.impl.d.ts +34 -0
  51. package/dist/connections.node.impl.d.ts.map +1 -0
  52. package/dist/connections.node.impl.js +236 -0
  53. package/dist/connections.node.impl.js.map +1 -0
  54. package/dist/index.d.ts +22 -0
  55. package/dist/index.d.ts.map +1 -0
  56. package/dist/index.js +24 -0
  57. package/dist/index.js.map +1 -0
  58. package/dist/kafka-client.node.impl.d.ts +9 -0
  59. package/dist/kafka-client.node.impl.d.ts.map +1 -0
  60. package/dist/kafka-client.node.impl.js +10 -0
  61. package/dist/kafka-client.node.impl.js.map +1 -0
  62. package/dist/kafka-connector.impl.d.ts +53 -0
  63. package/dist/kafka-connector.impl.d.ts.map +1 -0
  64. package/dist/kafka-connector.impl.js +97 -0
  65. package/dist/kafka-connector.impl.js.map +1 -0
  66. package/dist/kafka-event-adapter.descriptor.d.ts +11 -0
  67. package/dist/kafka-event-adapter.descriptor.d.ts.map +1 -0
  68. package/dist/kafka-event-adapter.descriptor.js +20 -0
  69. package/dist/kafka-event-adapter.descriptor.js.map +1 -0
  70. package/dist/kafka-event-adapter.impl.d.ts +22 -0
  71. package/dist/kafka-event-adapter.impl.d.ts.map +1 -0
  72. package/dist/kafka-event-adapter.impl.js +140 -0
  73. package/dist/kafka-event-adapter.impl.js.map +1 -0
  74. package/dist/kafka-substrate.descriptor.d.ts +4 -0
  75. package/dist/kafka-substrate.descriptor.d.ts.map +1 -0
  76. package/dist/kafka-substrate.descriptor.js +12 -0
  77. package/dist/kafka-substrate.descriptor.js.map +1 -0
  78. package/dist/kafka.d.ts +18 -0
  79. package/dist/kafka.d.ts.map +1 -0
  80. package/dist/kafka.js +23 -0
  81. package/dist/kafka.js.map +1 -0
  82. package/dist/kafka.node.plugin.d.ts +3 -0
  83. package/dist/kafka.node.plugin.d.ts.map +1 -0
  84. package/dist/kafka.node.plugin.js +66 -0
  85. package/dist/kafka.node.plugin.js.map +1 -0
  86. package/dist/kafka.plugin.d.ts +10 -0
  87. package/dist/kafka.plugin.d.ts.map +1 -0
  88. package/dist/kafka.plugin.js +28 -0
  89. package/dist/kafka.plugin.js.map +1 -0
  90. package/dist/logging/kafka.logger.descriptor.d.ts +7 -0
  91. package/dist/logging/kafka.logger.descriptor.d.ts.map +1 -0
  92. package/dist/logging/kafka.logger.descriptor.js +13 -0
  93. package/dist/logging/kafka.logger.descriptor.js.map +1 -0
  94. package/dist/message-schemas.d.ts +94 -0
  95. package/dist/message-schemas.d.ts.map +1 -0
  96. package/dist/message-schemas.js +68 -0
  97. package/dist/message-schemas.js.map +1 -0
  98. package/dist/request-queue-timeout-guard.d.ts +29 -0
  99. package/dist/request-queue-timeout-guard.d.ts.map +1 -0
  100. package/dist/request-queue-timeout-guard.js +245 -0
  101. package/dist/request-queue-timeout-guard.js.map +1 -0
  102. package/dist/schemas/contracts.d.ts +190 -0
  103. package/dist/schemas/contracts.d.ts.map +1 -0
  104. package/dist/schemas/contracts.js +85 -0
  105. package/dist/schemas/contracts.js.map +1 -0
  106. package/dist/schemas/kafka.d.ts +116 -0
  107. package/dist/schemas/kafka.d.ts.map +1 -0
  108. package/dist/schemas/kafka.js +13 -0
  109. package/dist/schemas/kafka.js.map +1 -0
  110. package/dist/schemas/realtime.d.ts +135 -0
  111. package/dist/schemas/realtime.d.ts.map +1 -0
  112. package/dist/schemas/realtime.js +44 -0
  113. package/dist/schemas/realtime.js.map +1 -0
  114. package/dist/schemas/worker-context.d.ts +14 -0
  115. package/dist/schemas/worker-context.d.ts.map +1 -0
  116. package/dist/schemas/worker-context.js +26 -0
  117. package/dist/schemas/worker-context.js.map +1 -0
  118. package/dist/topics.d.ts +85 -0
  119. package/dist/topics.d.ts.map +1 -0
  120. package/dist/topics.js +90 -0
  121. package/dist/topics.js.map +1 -0
  122. package/dist/workers/adapter/db-ops.d.ts +28 -0
  123. package/dist/workers/adapter/db-ops.d.ts.map +1 -0
  124. package/dist/workers/adapter/db-ops.js +14 -0
  125. package/dist/workers/adapter/db-ops.js.map +1 -0
  126. package/dist/workers/adapter/index.d.ts +16 -0
  127. package/dist/workers/adapter/index.d.ts.map +1 -0
  128. package/dist/workers/adapter/index.js +15 -0
  129. package/dist/workers/adapter/index.js.map +1 -0
  130. package/dist/workers/adapter/provider.d.ts +151 -0
  131. package/dist/workers/adapter/provider.d.ts.map +1 -0
  132. package/dist/workers/adapter/provider.js +654 -0
  133. package/dist/workers/adapter/provider.js.map +1 -0
  134. package/dist/workers/adapter/worker-adapter.kafka.descriptor.d.ts +36 -0
  135. package/dist/workers/adapter/worker-adapter.kafka.descriptor.d.ts.map +1 -0
  136. package/dist/workers/adapter/worker-adapter.kafka.descriptor.js +22 -0
  137. package/dist/workers/adapter/worker-adapter.kafka.descriptor.js.map +1 -0
  138. package/dist/workers/adapter/worker-adapter.kafka.impl.d.ts +36 -0
  139. package/dist/workers/adapter/worker-adapter.kafka.impl.d.ts.map +1 -0
  140. package/dist/workers/adapter/worker-adapter.kafka.impl.js +84 -0
  141. package/dist/workers/adapter/worker-adapter.kafka.impl.js.map +1 -0
  142. package/dist/workers/adapter/workers-kafka.db.context.d.ts +6 -0
  143. package/dist/workers/adapter/workers-kafka.db.context.d.ts.map +1 -0
  144. package/dist/workers/adapter/workers-kafka.db.context.descriptor.d.ts +43 -0
  145. package/dist/workers/adapter/workers-kafka.db.context.descriptor.d.ts.map +1 -0
  146. package/dist/workers/adapter/workers-kafka.db.context.descriptor.js +20 -0
  147. package/dist/workers/adapter/workers-kafka.db.context.descriptor.js.map +1 -0
  148. package/dist/workers/adapter/workers-kafka.db.context.js +5 -0
  149. package/dist/workers/adapter/workers-kafka.db.context.js.map +1 -0
  150. package/dist/workers/index.d.ts +8 -0
  151. package/dist/workers/index.d.ts.map +1 -0
  152. package/dist/workers/index.js +9 -0
  153. package/dist/workers/index.js.map +1 -0
  154. package/dist/workers/provider-jobs.sqlite-schema.d.ts +438 -0
  155. package/dist/workers/provider-jobs.sqlite-schema.d.ts.map +1 -0
  156. package/dist/workers/provider-jobs.sqlite-schema.js +37 -0
  157. package/dist/workers/provider-jobs.sqlite-schema.js.map +1 -0
  158. package/dist/workers/workers-kafka.consumer.plugin.d.ts +4 -0
  159. package/dist/workers/workers-kafka.consumer.plugin.d.ts.map +1 -0
  160. package/dist/workers/workers-kafka.consumer.plugin.js +11 -0
  161. package/dist/workers/workers-kafka.consumer.plugin.js.map +1 -0
  162. package/dist/workers/workers-kafka.plugin.d.ts +9 -0
  163. package/dist/workers/workers-kafka.plugin.d.ts.map +1 -0
  164. package/dist/workers/workers-kafka.plugin.js +27 -0
  165. package/dist/workers/workers-kafka.plugin.js.map +1 -0
  166. package/package.json +176 -4
@@ -0,0 +1,654 @@
1
+ /**
2
+ * Kafka Provider
3
+ *
4
+ * Kafka-based provider for distributed job processing.
5
+ * Uses existing Kafka infrastructure with worker topics.
6
+ */
7
+ import { BaseQueueAdapter, assertJobMutationGuard, assertJobWriteGuard, assertExecutionClaim, assertJobReadmission, executionClaimFromRecord, jobMutationStamp, snapshotJob, stampRetryPolicy, withRetryAvailability, isDueAt } from '@vibesdotdev/workers/core/queue-adapter';
8
+ import { CapabilityError, CaughtFailureSchema, valueFreeFailure } from '@vibesdotdev/kernel';
9
+ import { JobDataEqualsSchema, JobStatusSchema, JobExecutionRecordSchema, JobExecutionClaimSchema, JobWriteGuardSchema, JobRetryOptionsSchema, JobMutationStampSchema } from '@vibesdotdev/workers/core/schemas';
10
+ import { CancellationSchema, parseCancellation } from '@vibesdotdev/workers/schemas';
11
+ import { JsonObjectSchema } from '@vibesdotdev/kernel/schemas/json';
12
+ import * as z from 'zod/v4';
13
+ /**
14
+ * Payload and metadata are separate physical columns. A row whose metadata
15
+ * column is absent is not decoded by guessing: an application-owned migration
16
+ * splits historical combined rows and preserves their payloads, and until then
17
+ * the adapter refuses rather than inventing metadata.
18
+ */
19
+ function storedJobData(row) {
20
+ const parsed = JsonObjectSchema.safeParse(row.metadata);
21
+ if (!parsed.success) {
22
+ throw new CapabilityError('Stored job metadata is invalid.', 'INTERNAL', {
23
+ reason: 'job-metadata-record-invalid', retryable: false, cause: parsed.error
24
+ });
25
+ }
26
+ return { cleanData: row.data, metadata: parsed.data };
27
+ }
28
+ function inputJobMetadata(value) {
29
+ const parsed = JsonObjectSchema.safeParse(value);
30
+ if (!parsed.success) {
31
+ throw new CapabilityError('Job metadata is invalid.', 'INVALID_INPUT', {
32
+ reason: 'job-metadata-invalid', retryable: false, cause: parsed.error
33
+ });
34
+ }
35
+ return parsed.data;
36
+ }
37
+ function storedCancellation(value) {
38
+ if (value == null)
39
+ return undefined;
40
+ const parsed = CancellationSchema.safeParse(value);
41
+ if (!parsed.success) {
42
+ throw new CapabilityError('Stored job cancellation provenance is invalid.', 'INTERNAL', {
43
+ reason: 'job-cancellation-record-invalid', retryable: false, cause: parsed.error
44
+ });
45
+ }
46
+ return parsed.data;
47
+ }
48
+ /** A JSON `null` payload is written as the SQL JSON literal so the NOT NULL column keeps it. */
49
+ function storedPayloadData(value, sql) {
50
+ return value === null ? sql `'null'` : value;
51
+ }
52
+ function mergeJobMetadata(row, metadata, sql) {
53
+ const stored = storedJobData(row);
54
+ return { data: storedPayloadData(stored.cleanData, sql), metadata: inputJobMetadata({ ...stored.metadata, ...metadata }) };
55
+ }
56
+ /**
57
+ * Page size when a dequeue scans pending rows for the first due job. The scan
58
+ * pages until a due row is claimed or the pending set is exhausted, so a head
59
+ * of delayed rows can never starve due jobs behind it.
60
+ */
61
+ const CLAIM_WINDOW = 20;
62
+ const WORKER_JOB_EVENT_TYPES = [
63
+ 'job.queued',
64
+ 'job.created',
65
+ 'job.started',
66
+ 'job.progress',
67
+ 'job.completed',
68
+ 'job.failed',
69
+ 'job.retrying',
70
+ 'job.cancelled',
71
+ 'tool.event'
72
+ ];
73
+ /** The fields that make a bus message a worker job event; anything else on the bus is not one. */
74
+ const WorkerJobEventIdentitySchema = z.object({
75
+ type: z.enum(WORKER_JOB_EVENT_TYPES),
76
+ jobId: z.string(),
77
+ jobType: z.string(),
78
+ workerId: z.string(),
79
+ timestamp: z.string()
80
+ });
81
+ /**
82
+ * A worker job event as the bus delivers it. A present stamp must be valid;
83
+ * malformed optional detail is dropped rather than refusing the event.
84
+ */
85
+ const WorkerJobEventSchema = WorkerJobEventIdentitySchema.extend({
86
+ stamp: JobMutationStampSchema.optional(),
87
+ payload: z.unknown().optional(),
88
+ progress: z.union([z.number(), JsonObjectSchema]).optional().catch(undefined),
89
+ error: z.string().optional().catch(undefined),
90
+ result: z.unknown().optional(),
91
+ envelope: z.unknown().optional(),
92
+ metadata: JsonObjectSchema.optional().catch(undefined)
93
+ });
94
+ /** The bus is untyped transport input: a non-event is skipped, an event with an invalid stamp refuses. */
95
+ function toWorkerJobEvent(message) {
96
+ if (!WorkerJobEventIdentitySchema.safeParse(message).success)
97
+ return null;
98
+ return WorkerJobEventSchema.parse(message);
99
+ }
100
+ /** `broadcastEvent` publishes its named event and data inside the payload. */
101
+ const BroadcastPayloadSchema = z.object({ event: z.string(), data: z.unknown().optional() });
102
+ const FailedPayloadSchema = z.object({ error: z.string() });
103
+ function toJobEvent(message) {
104
+ const broadcast = BroadcastPayloadSchema.safeParse(message.payload);
105
+ if (broadcast.success) {
106
+ return {
107
+ jobId: message.jobId,
108
+ event: broadcast.data.event,
109
+ data: broadcast.data.data,
110
+ timestamp: new Date(message.timestamp),
111
+ stamp: message.stamp ?? null
112
+ };
113
+ }
114
+ if (message.type === 'job.completed') {
115
+ return {
116
+ jobId: message.jobId,
117
+ event: 'done',
118
+ data: message.payload,
119
+ timestamp: new Date(message.timestamp),
120
+ stamp: message.stamp ?? null
121
+ };
122
+ }
123
+ if (message.type === 'job.failed') {
124
+ const failed = FailedPayloadSchema.safeParse(message.payload);
125
+ const error = (failed.success ? failed.data.error : message.error) ?? 'Job failed';
126
+ return {
127
+ jobId: message.jobId,
128
+ event: 'error',
129
+ data: { error },
130
+ timestamp: new Date(message.timestamp),
131
+ stamp: message.stamp ?? null
132
+ };
133
+ }
134
+ if (message.type === 'job.cancelled') {
135
+ return {
136
+ jobId: message.jobId,
137
+ event: 'cancelled',
138
+ data: message.payload,
139
+ timestamp: new Date(message.timestamp),
140
+ stamp: message.stamp ?? null
141
+ };
142
+ }
143
+ if (message.type === 'job.progress') {
144
+ return {
145
+ jobId: message.jobId,
146
+ event: 'progress',
147
+ data: message.payload,
148
+ timestamp: new Date(message.timestamp),
149
+ stamp: message.stamp ?? null
150
+ };
151
+ }
152
+ return {
153
+ jobId: message.jobId,
154
+ event: message.type === 'job.queued' ? 'queued' : 'started',
155
+ data: message.payload,
156
+ timestamp: new Date(message.timestamp),
157
+ stamp: message.stamp ?? null
158
+ };
159
+ }
160
+ export class KafkaAdapter extends BaseQueueAdapter {
161
+ db;
162
+ providerJobs;
163
+ ops;
164
+ eventHandlers = new Map();
165
+ unsubscribeEvents;
166
+ eventBridgeConnected = false;
167
+ eventBridgeSetup;
168
+ eventBridgeCleanup;
169
+ eventBridgeClosing = false;
170
+ eventBridge;
171
+ diagnostics;
172
+ // The host's declared dialect. 'postgres' claims under a transactional
173
+ // `FOR UPDATE SKIP LOCKED`; 'sqlite' (Cloudflare D1 / libsql) claims by an
174
+ // atomic compare-and-set, because those hosts serialize writes and have no
175
+ // row-lock syntax. The adapter never guesses it.
176
+ dialect;
177
+ constructor(db, queueName, options) {
178
+ super(queueName);
179
+ this.db = db;
180
+ this.providerJobs = options.providerJobs;
181
+ this.ops = options.ops;
182
+ this.dialect = options.dialect;
183
+ this.eventBridge = options.eventBridge;
184
+ this.diagnostics = options.diagnostics;
185
+ }
186
+ async connect() {
187
+ await this.ensureEventBridge();
188
+ }
189
+ disconnect() {
190
+ if (this.eventBridgeCleanup)
191
+ return this.eventBridgeCleanup;
192
+ this.eventBridgeClosing = true;
193
+ this.eventHandlers.clear();
194
+ const setup = this.eventBridgeSetup;
195
+ const cleanupOperation = (async () => {
196
+ const setupResults = setup ? await Promise.allSettled([setup]) : [];
197
+ this.eventBridgeConnected = false;
198
+ const unsubscribe = this.unsubscribeEvents;
199
+ this.unsubscribeEvents = undefined;
200
+ const unsubscribeResults = unsubscribe
201
+ ? await Promise.allSettled([Promise.resolve().then(unsubscribe)])
202
+ : [];
203
+ const failures = [];
204
+ if (setupResults[0]?.status === 'rejected')
205
+ failures.push(setupResults[0].reason);
206
+ if (unsubscribeResults[0]?.status === 'rejected')
207
+ failures.push(unsubscribeResults[0].reason);
208
+ if (failures.length === 1)
209
+ throw failures[0];
210
+ if (failures.length > 1) {
211
+ throw new AggregateError(failures, 'Kafka event bridge cleanup failed.', {
212
+ cause: failures[0]
213
+ });
214
+ }
215
+ })();
216
+ let cleanup;
217
+ cleanup = cleanupOperation.finally(() => {
218
+ if (this.eventBridgeCleanup === cleanup)
219
+ this.eventBridgeCleanup = undefined;
220
+ });
221
+ this.eventBridgeCleanup = cleanup;
222
+ return cleanup;
223
+ }
224
+ rowFence(row) {
225
+ const { eq, and, sql } = this.ops;
226
+ const nullable = (column, value) => value == null ? sql `${column} IS NULL` : eq(column, value);
227
+ return and(eq(this.providerJobs.id, row.id), eq(this.providerJobs.queueName, this.queueName), eq(this.providerJobs.provider, 'kafka'), eq(this.providerJobs.status, row.status), nullable(this.providerJobs.generation, row.generation), nullable(this.providerJobs.revision, row.revision), nullable(this.providerJobs.executionState, row.executionState), nullable(this.providerJobs.claimToken, row.claimToken), nullable(this.providerJobs.claimAttempt, row.claimAttempt), nullable(this.providerJobs.claimOwner, row.claimOwner));
228
+ }
229
+ async readRow(jobId) {
230
+ const { eq, and } = this.ops;
231
+ const [row] = await this.db.select().from(this.providerJobs).where(and(eq(this.providerJobs.id, jobId), eq(this.providerJobs.queueName, this.queueName), eq(this.providerJobs.provider, 'kafka'))).limit(1);
232
+ return row ?? null;
233
+ }
234
+ async requireRow(jobId) {
235
+ const row = await this.readRow(jobId);
236
+ if (!row)
237
+ throw new CapabilityError('Job not found in this queue.', 'NOT_FOUND', { reason: 'job-not-found' });
238
+ return row;
239
+ }
240
+ async writeRow(row, patch, db = this.db) {
241
+ const [admitted] = await db.update(this.providerJobs).set(patch).where(this.rowFence(row)).returning();
242
+ return admitted ?? null;
243
+ }
244
+ /** `updateJob` never admits a cancellation; `cancelJob` is the only provenance owner. */
245
+ rejectProvenancelessCancellation(jobId) {
246
+ throw new CapabilityError('Cancel the job through cancelJob, which requires cancellation provenance.', 'INVALID_INPUT', {
247
+ reason: 'cancel-provenance-required', retryable: false, details: { jobId }
248
+ });
249
+ }
250
+ /** A row already cancelled holds its admitted provenance; a historical row keeps none. */
251
+ assertAlreadyCancelled(job) {
252
+ if (job.status === 'cancelled')
253
+ return;
254
+ this.rejectProvenancelessCancellation(job.id);
255
+ }
256
+ revisionConflict() {
257
+ throw new CapabilityError('The intended job mutation could not be admitted.', 'CONFLICT', {
258
+ reason: 'job-revision-conflict', retryable: false
259
+ });
260
+ }
261
+ /** A bounded native revision retry; assert always holds the caller's original intent. */
262
+ async mutate(jobId, assert, patch) {
263
+ for (let attempt = 0; attempt < 2; attempt++) {
264
+ const row = await this.requireRow(jobId);
265
+ const job = this.toJob(row);
266
+ assert(job, row);
267
+ const admitted = await this.writeRow(row, patch(row, job));
268
+ if (admitted)
269
+ return admitted;
270
+ }
271
+ return this.revisionConflict();
272
+ }
273
+ toJob(row) {
274
+ const parsed = JobExecutionRecordSchema.safeParse(row);
275
+ if (!parsed.success) {
276
+ throw new CapabilityError('Stored job ownership is invalid.', 'INTERNAL', {
277
+ reason: 'job-execution-record-invalid', retryable: false, cause: parsed.error
278
+ });
279
+ }
280
+ const facts = parsed.data;
281
+ const { cleanData, metadata } = storedJobData(row);
282
+ const cancellation = storedCancellation(row.cancellation);
283
+ // SAFETY: TJob and TResult are this queue's declared payload and result; the row holds the
284
+ // JSON its own enqueue and updateJob wrote, so the stored value is that type's JSON form.
285
+ const data = cleanData, result = row.result;
286
+ const job = {
287
+ id: row.id, data, status: JobStatusSchema.parse(row.status),
288
+ progress: row.progress ?? undefined,
289
+ result, error: row.error ?? undefined,
290
+ attemptsMade: row.attemptsMade ?? 0, createdAt: row.createdAt,
291
+ startedAt: row.startedAt ?? undefined, completedAt: row.completedAt ?? undefined,
292
+ generation: facts.generation ?? 0, revision: facts.revision ?? 0,
293
+ executionClaim: executionClaimFromRecord(facts),
294
+ retiredAt: facts.retiredAt == null ? undefined : (facts.retiredAt instanceof Date ? facts.retiredAt : new Date(facts.retiredAt)),
295
+ metadata
296
+ };
297
+ if (cancellation)
298
+ job.cancellation = cancellation;
299
+ return snapshotJob(job);
300
+ }
301
+ async publish(type, jobId, payload, stamp) {
302
+ try {
303
+ await this.eventBridge.sendJobEvent({
304
+ type, jobId, jobType: this.queueName, workerId: this.queueName,
305
+ timestamp: new Date().toISOString(), payload, stamp
306
+ });
307
+ }
308
+ catch (error) {
309
+ this.diagnostics.error('Kafka post-admission event delivery failed', {
310
+ scope: 'workers-kafka:queue', plugin: 'worker-adapter.kafka',
311
+ error: CaughtFailureSchema.parse(error)
312
+ });
313
+ }
314
+ }
315
+ async enqueue(data, options) {
316
+ await this.eventBridge.prepare();
317
+ const jobId = options?.jobId || this.generateJobId();
318
+ const metadata = { ...options?.metadata };
319
+ stampRetryPolicy(metadata, options);
320
+ const now = new Date();
321
+ const delay = options?.delay ?? 0;
322
+ const jobMetadata = inputJobMetadata(delay > 0
323
+ ? withRetryAvailability(metadata, Math.ceil(delay), now.getTime()) : metadata);
324
+ const { eq } = this.ops;
325
+ const [existing] = await this.db.select().from(this.providerJobs)
326
+ .where(eq(this.providerJobs.id, jobId)).limit(1);
327
+ if (existing && (existing.queueName !== this.queueName || existing.provider !== 'kafka')) {
328
+ throw new CapabilityError('This job id is held by another queue or provider.', 'CONFLICT', {
329
+ reason: 'job-key-held-by-another-queue', retryable: false
330
+ });
331
+ }
332
+ if (existing && existing.status !== 'failed')
333
+ return jobId;
334
+ const intended = existing && this.toJob(existing);
335
+ if (intended)
336
+ assertJobReadmission(intended, intended.generation);
337
+ const row = {
338
+ id: jobId, queueName: this.queueName, provider: 'kafka', data: storedPayloadData(data, this.ops.sql), metadata: jobMetadata,
339
+ cancellation: null,
340
+ status: 'pending', attemptsMade: 0, maxAttempts: options?.attempts ?? 3,
341
+ priority: options?.priority ?? 0, createdAt: now, startedAt: null, completedAt: null,
342
+ progress: null, result: null, error: null,
343
+ generation: (intended?.generation ?? 0) + 1, revision: (intended?.revision ?? -1) + 1,
344
+ executionState: 'none', claimToken: null, claimAttempt: null, claimOwner: null,
345
+ claimHeartbeatAt: null, retiredAt: null
346
+ };
347
+ const admitted = existing ? await this.writeRow(existing, row) : (await this.db.insert(this.providerJobs).values(row)
348
+ .onConflictDoNothing({ target: this.providerJobs.id }).returning())[0];
349
+ if (admitted) {
350
+ await this.publish('job.queued', jobId, {
351
+ queueName: this.queueName, data, priority: row.priority,
352
+ attempts: row.maxAttempts, delay: options?.delay, metadata
353
+ }, jobMutationStamp(this.toJob(admitted)));
354
+ }
355
+ else if (!await this.readRow(jobId)) {
356
+ throw new CapabilityError('This job id is held by another queue or provider.', 'CONFLICT', {
357
+ reason: 'job-key-held-by-another-queue', retryable: false
358
+ });
359
+ }
360
+ return jobId;
361
+ }
362
+ /** Both backing dialects use the same native admission fence; PostgreSQL also locks candidate rows. */
363
+ async dequeueNative(db, options) {
364
+ const { eq, and, asc, desc } = this.ops;
365
+ const now = Date.now();
366
+ for (let offset = 0;; offset += CLAIM_WINDOW) {
367
+ let query = db.select().from(this.providerJobs).where(and(eq(this.providerJobs.queueName, this.queueName), eq(this.providerJobs.provider, 'kafka'), eq(this.providerJobs.status, 'pending'))).orderBy(desc(this.providerJobs.priority), asc(this.providerJobs.createdAt))
368
+ .limit(CLAIM_WINDOW).offset(offset);
369
+ if (this.dialect === 'postgres')
370
+ query = query.for('update', { skipLocked: true });
371
+ const candidates = await query;
372
+ for (const candidate of candidates) {
373
+ const job = this.toJob(candidate);
374
+ if (job.executionClaim.state !== 'none' || !isDueAt(job.metadata, now))
375
+ continue;
376
+ const jobType = job.metadata?.type;
377
+ if (options?.jobTypes?.length && !options.jobTypes.some(type => type === jobType))
378
+ continue;
379
+ const identity = this.createExecutionClaim(job, options);
380
+ const patch = {
381
+ status: 'processing', startedAt: new Date(), revision: job.revision + 1,
382
+ executionState: 'held', claimToken: identity.token, claimAttempt: identity.attempt,
383
+ claimOwner: identity.owner, claimHeartbeatAt: new Date().toISOString(), retiredAt: null
384
+ };
385
+ if (options?.claimMetadata)
386
+ Object.assign(patch, mergeJobMetadata(candidate, options.claimMetadata, this.ops.sql));
387
+ const admitted = await this.writeRow(candidate, patch, db);
388
+ if (admitted)
389
+ return admitted;
390
+ }
391
+ if (candidates.length < CLAIM_WINDOW)
392
+ return null;
393
+ }
394
+ }
395
+ async dequeue(options) {
396
+ await this.eventBridge.prepare();
397
+ const row = this.dialect === 'postgres'
398
+ ? await this.db.transaction(tx => this.dequeueNative(tx, options))
399
+ : await this.dequeueNative(this.db, options);
400
+ if (!row)
401
+ return null;
402
+ const job = this.toJob(row);
403
+ await this.publish('job.started', row.id, {}, jobMutationStamp(job));
404
+ return job;
405
+ }
406
+ async getJob(jobId) {
407
+ const row = await this.readRow(jobId);
408
+ return row ? this.toJob(row) : null;
409
+ }
410
+ async updateJob(jobId, update, guard) {
411
+ const intended = JobWriteGuardSchema.parse(guard);
412
+ const input = { ...update, metadata: update.metadata && { ...update.metadata } };
413
+ // A newly admitted cancellation has one owner: cancelJob, which requires and
414
+ // persists Workers provenance. updateJob may hold a cancelled row's other
415
+ // fields, but never move a live row into `cancelled` without provenance.
416
+ if (input.status === 'cancelled')
417
+ this.rejectProvenancelessCancellation(jobId);
418
+ await this.eventBridge.prepare();
419
+ const admitted = await this.mutate(jobId, job => {
420
+ assertJobMutationGuard(job, intended);
421
+ if (input.status === 'cancelled')
422
+ this.assertAlreadyCancelled(job);
423
+ }, (row, job) => {
424
+ const patch = { revision: job.revision + 1 };
425
+ if (input.status !== undefined) {
426
+ patch.status = JobStatusSchema.parse(input.status);
427
+ const terminal = input.status === 'completed' || input.status === 'failed' || input.status === 'cancelled';
428
+ patch.completedAt = terminal ? new Date() : null;
429
+ patch.retiredAt = terminal && job.executionClaim.state === 'none' ? new Date() : null;
430
+ }
431
+ if (input.progress !== undefined)
432
+ patch.progress = input.progress;
433
+ if (input.result !== undefined)
434
+ patch.result = input.result;
435
+ if (input.error !== undefined)
436
+ patch.error = input.error;
437
+ if (input.metadata !== undefined)
438
+ Object.assign(patch, mergeJobMetadata(row, input.metadata, this.ops.sql));
439
+ return patch;
440
+ });
441
+ const stamp = jobMutationStamp(this.toJob(admitted));
442
+ if (input.progress !== undefined)
443
+ await this.publish('job.progress', jobId, { progress: input.progress }, stamp);
444
+ if (input.status === 'completed')
445
+ await this.publish('job.completed', jobId, { result: input.result }, stamp);
446
+ if (input.status === 'failed')
447
+ await this.publish('job.failed', jobId, { error: input.error }, stamp);
448
+ return stamp;
449
+ }
450
+ async renewExecution(jobId, claim) {
451
+ const intended = JobExecutionClaimSchema.parse(claim);
452
+ await this.mutate(jobId, job => { assertExecutionClaim(job, intended); }, () => ({
453
+ claimHeartbeatAt: new Date().toISOString()
454
+ }));
455
+ }
456
+ async releaseExecution(jobId, claim) {
457
+ const intended = JobExecutionClaimSchema.parse(claim);
458
+ await this.mutate(jobId, job => { assertExecutionClaim(job, intended); }, (_row, job) => ({
459
+ executionState: 'none', claimToken: null, claimAttempt: null, claimOwner: null, claimHeartbeatAt: null,
460
+ retiredAt: job.status === 'completed' || job.status === 'failed' || job.status === 'cancelled' ? new Date() : null
461
+ }));
462
+ }
463
+ async cancelJob(jobId, cancellation) {
464
+ const provenance = parseCancellation(cancellation);
465
+ await this.eventBridge.prepare();
466
+ const row = await this.readRow(jobId);
467
+ if (!row || (row.status !== 'pending' && row.status !== 'processing'))
468
+ return false;
469
+ // Administrative cancellation retains the original native admission, including UNKNOWN ownership.
470
+ const intended = Object.freeze({ ...row });
471
+ const admitted = await this.mutate(jobId, (_job, current) => {
472
+ if (current.generation !== intended.generation || current.status !== intended.status ||
473
+ current.executionState !== intended.executionState || current.claimToken !== intended.claimToken ||
474
+ current.claimAttempt !== intended.claimAttempt || current.claimOwner !== intended.claimOwner) {
475
+ throw new CapabilityError('The intended cancellation admission or phase is no longer current.', 'CONFLICT', {
476
+ reason: 'job-write-stale', retryable: false
477
+ });
478
+ }
479
+ }, (_row, job) => ({
480
+ status: 'cancelled', cancellation: provenance, completedAt: new Date(), revision: job.revision + 1,
481
+ retiredAt: job.executionClaim.state === 'none' ? new Date() : null
482
+ }));
483
+ const cancelled = this.toJob(admitted);
484
+ const stamp = jobMutationStamp(cancelled);
485
+ // The admitted record and its stamp travel on both paths: local subscribers
486
+ // and every other adapter reading the same bus.
487
+ const payload = { reason: provenance.reason, cancellation: provenance, queueName: this.queueName };
488
+ this.emitEvent({ jobId, event: 'cancelled', timestamp: new Date(), data: payload, stamp });
489
+ await this.publish('job.cancelled', jobId, payload, stamp);
490
+ return true;
491
+ }
492
+ async retryJob(jobId, options) {
493
+ const intended = JobRetryOptionsSchema.parse(options);
494
+ await this.mutate(jobId, job => assertJobReadmission(job, intended.expectedGeneration), (row, job) => {
495
+ const { cleanData, metadata } = storedJobData(row);
496
+ const delay = intended.delay ?? 0;
497
+ return {
498
+ data: storedPayloadData(cleanData, this.ops.sql), metadata: inputJobMetadata(withRetryAvailability(metadata, delay > 0 ? Math.ceil(delay) : 0)),
499
+ status: 'pending', cancellation: null, attemptsMade: job.attemptsMade + 1,
500
+ generation: job.generation + 1, revision: job.revision + 1,
501
+ startedAt: null, completedAt: null, retiredAt: null, progress: null, result: null, error: null
502
+ };
503
+ });
504
+ return jobId;
505
+ }
506
+ async getStatus(jobId) {
507
+ return this.toJob(await this.requireRow(jobId)).status;
508
+ }
509
+ async listJobs(state, options) {
510
+ const { eq, and, desc } = this.ops;
511
+ const conditions = [eq(this.providerJobs.queueName, this.queueName), eq(this.providerJobs.provider, 'kafka')];
512
+ if (state)
513
+ conditions.push(eq(this.providerJobs.status, state));
514
+ if (options?.dataEquals)
515
+ conditions.push(...this.dataEquals(JobDataEqualsSchema.parse(options.dataEquals)));
516
+ const rows = await this.db.select().from(this.providerJobs).where(and(...conditions))
517
+ .orderBy(desc(this.providerJobs.createdAt), desc(this.providerJobs.id))
518
+ .limit(options?.limit ?? 100).offset(options?.offset ?? 0);
519
+ return rows.map(row => this.toJob(row));
520
+ }
521
+ /** One condition per field: the stored data's top-level field is a JSON string equal to the value. */
522
+ dataEquals(fields) {
523
+ const { sql } = this.ops;
524
+ const data = this.providerJobs.data;
525
+ return Object.entries(fields).map(([field, value]) => this.dialect === 'postgres'
526
+ ? sql `(${data})::jsonb -> ${field} = to_jsonb(${value}::text)`
527
+ : sql `json_type(${data}, ${`$.${field}`}) = 'text' AND json_extract(${data}, ${`$.${field}`}) = ${value}`);
528
+ }
529
+ async getMetrics() {
530
+ const { eq, and, sql } = this.ops;
531
+ const counts = await this.db
532
+ .select({
533
+ status: this.providerJobs.status,
534
+ count: sql `count(*)`
535
+ })
536
+ .from(this.providerJobs)
537
+ .where(and(eq(this.providerJobs.queueName, this.queueName), eq(this.providerJobs.provider, 'kafka')))
538
+ .groupBy(this.providerJobs.status);
539
+ const metrics = { waiting: 0, active: 0, completed: 0, failed: 0 };
540
+ for (const { status, count } of counts) {
541
+ if (status === 'pending')
542
+ metrics.waiting = Number(count);
543
+ if (status === 'processing')
544
+ metrics.active = Number(count);
545
+ if (status === 'completed')
546
+ metrics.completed = Number(count);
547
+ if (status === 'failed')
548
+ metrics.failed = Number(count);
549
+ }
550
+ return metrics;
551
+ }
552
+ subscribeToEvents(jobId, handler) {
553
+ const target = jobId === '*' ? '*' : jobId;
554
+ if (!this.eventHandlers.has(target)) {
555
+ this.eventHandlers.set(target, new Set());
556
+ }
557
+ this.eventHandlers.get(target).add(handler);
558
+ return () => {
559
+ const handlers = this.eventHandlers.get(target);
560
+ if (!handlers)
561
+ return;
562
+ handlers.delete(handler);
563
+ if (handlers.size === 0)
564
+ this.eventHandlers.delete(target);
565
+ };
566
+ }
567
+ async broadcastEvent(jobId, event, guard, data) {
568
+ const intended = JobWriteGuardSchema.parse(guard);
569
+ await this.eventBridge.prepare();
570
+ const admitted = await this.mutate(jobId, job => assertJobWriteGuard(job, intended), (_row, job) => ({
571
+ revision: job.revision + 1
572
+ }));
573
+ const stamp = jobMutationStamp(this.toJob(admitted));
574
+ this.emitEvent({ jobId, event, data, timestamp: new Date(), stamp });
575
+ await this.publish('job.started', jobId, { event, data, queueName: this.queueName }, stamp);
576
+ }
577
+ async ensureEventBridge() {
578
+ const cleanup = this.eventBridgeCleanup;
579
+ if (cleanup) {
580
+ await cleanup;
581
+ return this.ensureEventBridge();
582
+ }
583
+ if (this.eventBridgeConnected)
584
+ return;
585
+ if (this.eventBridgeSetup)
586
+ return this.eventBridgeSetup;
587
+ this.eventBridgeClosing = false;
588
+ let remover;
589
+ const setupOperation = Promise.resolve().then(async () => {
590
+ remover = this.eventBridge.onMessage('worker.jobs', (message) => {
591
+ if (this.eventBridgeClosing)
592
+ return;
593
+ const workerEvent = toWorkerJobEvent(message);
594
+ if (!workerEvent)
595
+ return;
596
+ if (workerEvent.workerId !== this.queueName && workerEvent.type !== 'job.queued')
597
+ return;
598
+ this.emitEvent(toJobEvent(workerEvent));
599
+ });
600
+ this.unsubscribeEvents = remover;
601
+ await this.eventBridge.prepare();
602
+ if (!this.eventBridgeClosing)
603
+ this.eventBridgeConnected = true;
604
+ });
605
+ let setup;
606
+ setup = setupOperation.then(() => undefined, async (error) => {
607
+ this.eventBridgeConnected = false;
608
+ if (this.unsubscribeEvents === remover)
609
+ this.unsubscribeEvents = undefined;
610
+ const failure = valueFreeFailure(CaughtFailureSchema.parse(error));
611
+ this.diagnostics.error('Kafka event bridge setup failed', {
612
+ scope: 'workers-kafka:queue', plugin: 'worker-adapter.kafka', error: failure
613
+ });
614
+ if (!remover)
615
+ throw error;
616
+ try {
617
+ await Promise.resolve().then(remover);
618
+ }
619
+ catch (cleanupFailure) {
620
+ const cleanupFailureSummary = valueFreeFailure(CaughtFailureSchema.parse(cleanupFailure));
621
+ this.diagnostics.error('Kafka event bridge setup cleanup failed', {
622
+ scope: 'workers-kafka:queue', plugin: 'worker-adapter.kafka', error: cleanupFailureSummary
623
+ });
624
+ throw new AggregateError([error, cleanupFailure], 'Kafka event bridge setup and cleanup failed.', {
625
+ cause: error
626
+ });
627
+ }
628
+ throw error;
629
+ }).finally(() => {
630
+ if (this.eventBridgeSetup === setup)
631
+ this.eventBridgeSetup = undefined;
632
+ });
633
+ this.eventBridgeSetup = setup;
634
+ return setup;
635
+ }
636
+ emitEvent(event) {
637
+ const handlers = [...(this.eventHandlers.get(event.jobId) ?? []), ...(this.eventHandlers.get('*') ?? [])];
638
+ for (const handler of handlers)
639
+ this.notifyEventHandler(handler, event);
640
+ }
641
+ /** A failing subscriber is reported; the committed admission and its sibling subscribers stand. */
642
+ notifyEventHandler(handler, event) {
643
+ try {
644
+ handler(event);
645
+ }
646
+ catch (error) {
647
+ this.diagnostics.error('Kafka post-admission subscriber delivery failed', {
648
+ scope: 'workers-kafka:queue', plugin: 'worker-adapter.kafka',
649
+ error: CaughtFailureSchema.parse(error)
650
+ });
651
+ }
652
+ }
653
+ }
654
+ //# sourceMappingURL=provider.js.map