@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.
- package/AUDIT-CODE.md +40 -0
- package/AUDIT-SEAL.md +12 -0
- package/LICENSE +126 -0
- package/README.md +76 -2
- package/SEALED.md +13 -0
- package/dist/admin.d.ts +20 -0
- package/dist/admin.d.ts.map +1 -0
- package/dist/admin.js +51 -0
- package/dist/admin.js.map +1 -0
- package/dist/auth/sasl.node.impl.d.ts +18 -0
- package/dist/auth/sasl.node.impl.d.ts.map +1 -0
- package/dist/auth/sasl.node.impl.js +62 -0
- package/dist/auth/sasl.node.impl.js.map +1 -0
- package/dist/chat/realtime-transport/host.d.ts +14 -0
- package/dist/chat/realtime-transport/host.d.ts.map +1 -0
- package/dist/chat/realtime-transport/host.js +7 -0
- package/dist/chat/realtime-transport/host.js.map +1 -0
- package/dist/chat/realtime-transport/index.d.ts +15 -0
- package/dist/chat/realtime-transport/index.d.ts.map +1 -0
- package/dist/chat/realtime-transport/index.js +13 -0
- package/dist/chat/realtime-transport/index.js.map +1 -0
- package/dist/chat/realtime-transport/lifecycle.d.ts +30 -0
- package/dist/chat/realtime-transport/lifecycle.d.ts.map +1 -0
- package/dist/chat/realtime-transport/lifecycle.js +125 -0
- package/dist/chat/realtime-transport/lifecycle.js.map +1 -0
- package/dist/chat/realtime-transport/projection.d.ts +127 -0
- package/dist/chat/realtime-transport/projection.d.ts.map +1 -0
- package/dist/chat/realtime-transport/projection.js +156 -0
- package/dist/chat/realtime-transport/projection.js.map +1 -0
- package/dist/chat/realtime-transport/sse-kafka-bridge.d.ts +32 -0
- package/dist/chat/realtime-transport/sse-kafka-bridge.d.ts.map +1 -0
- package/dist/chat/realtime-transport/sse-kafka-bridge.js +58 -0
- package/dist/chat/realtime-transport/sse-kafka-bridge.js.map +1 -0
- package/dist/chat/realtime-transport/ws-kafka-bridge.d.ts +10 -0
- package/dist/chat/realtime-transport/ws-kafka-bridge.d.ts.map +1 -0
- package/dist/chat/realtime-transport/ws-kafka-bridge.js +13 -0
- package/dist/chat/realtime-transport/ws-kafka-bridge.js.map +1 -0
- package/dist/client.d.ts +16 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +29 -0
- package/dist/client.js.map +1 -0
- package/dist/config/kafka.config.descriptor.d.ts +109 -0
- package/dist/config/kafka.config.descriptor.d.ts.map +1 -0
- package/dist/config/kafka.config.descriptor.js +60 -0
- package/dist/config/kafka.config.descriptor.js.map +1 -0
- package/dist/connections.d.ts +27 -0
- package/dist/connections.d.ts.map +1 -0
- package/dist/connections.js +20 -0
- package/dist/connections.js.map +1 -0
- package/dist/connections.node.impl.d.ts +34 -0
- package/dist/connections.node.impl.d.ts.map +1 -0
- package/dist/connections.node.impl.js +236 -0
- package/dist/connections.node.impl.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/kafka-client.node.impl.d.ts +9 -0
- package/dist/kafka-client.node.impl.d.ts.map +1 -0
- package/dist/kafka-client.node.impl.js +10 -0
- package/dist/kafka-client.node.impl.js.map +1 -0
- package/dist/kafka-connector.impl.d.ts +53 -0
- package/dist/kafka-connector.impl.d.ts.map +1 -0
- package/dist/kafka-connector.impl.js +97 -0
- package/dist/kafka-connector.impl.js.map +1 -0
- package/dist/kafka-event-adapter.descriptor.d.ts +11 -0
- package/dist/kafka-event-adapter.descriptor.d.ts.map +1 -0
- package/dist/kafka-event-adapter.descriptor.js +20 -0
- package/dist/kafka-event-adapter.descriptor.js.map +1 -0
- package/dist/kafka-event-adapter.impl.d.ts +22 -0
- package/dist/kafka-event-adapter.impl.d.ts.map +1 -0
- package/dist/kafka-event-adapter.impl.js +140 -0
- package/dist/kafka-event-adapter.impl.js.map +1 -0
- package/dist/kafka-substrate.descriptor.d.ts +4 -0
- package/dist/kafka-substrate.descriptor.d.ts.map +1 -0
- package/dist/kafka-substrate.descriptor.js +12 -0
- package/dist/kafka-substrate.descriptor.js.map +1 -0
- package/dist/kafka.d.ts +18 -0
- package/dist/kafka.d.ts.map +1 -0
- package/dist/kafka.js +23 -0
- package/dist/kafka.js.map +1 -0
- package/dist/kafka.node.plugin.d.ts +3 -0
- package/dist/kafka.node.plugin.d.ts.map +1 -0
- package/dist/kafka.node.plugin.js +66 -0
- package/dist/kafka.node.plugin.js.map +1 -0
- package/dist/kafka.plugin.d.ts +10 -0
- package/dist/kafka.plugin.d.ts.map +1 -0
- package/dist/kafka.plugin.js +28 -0
- package/dist/kafka.plugin.js.map +1 -0
- package/dist/logging/kafka.logger.descriptor.d.ts +7 -0
- package/dist/logging/kafka.logger.descriptor.d.ts.map +1 -0
- package/dist/logging/kafka.logger.descriptor.js +13 -0
- package/dist/logging/kafka.logger.descriptor.js.map +1 -0
- package/dist/message-schemas.d.ts +94 -0
- package/dist/message-schemas.d.ts.map +1 -0
- package/dist/message-schemas.js +68 -0
- package/dist/message-schemas.js.map +1 -0
- package/dist/request-queue-timeout-guard.d.ts +29 -0
- package/dist/request-queue-timeout-guard.d.ts.map +1 -0
- package/dist/request-queue-timeout-guard.js +245 -0
- package/dist/request-queue-timeout-guard.js.map +1 -0
- package/dist/schemas/contracts.d.ts +190 -0
- package/dist/schemas/contracts.d.ts.map +1 -0
- package/dist/schemas/contracts.js +85 -0
- package/dist/schemas/contracts.js.map +1 -0
- package/dist/schemas/kafka.d.ts +116 -0
- package/dist/schemas/kafka.d.ts.map +1 -0
- package/dist/schemas/kafka.js +13 -0
- package/dist/schemas/kafka.js.map +1 -0
- package/dist/schemas/realtime.d.ts +135 -0
- package/dist/schemas/realtime.d.ts.map +1 -0
- package/dist/schemas/realtime.js +44 -0
- package/dist/schemas/realtime.js.map +1 -0
- package/dist/schemas/worker-context.d.ts +14 -0
- package/dist/schemas/worker-context.d.ts.map +1 -0
- package/dist/schemas/worker-context.js +26 -0
- package/dist/schemas/worker-context.js.map +1 -0
- package/dist/topics.d.ts +85 -0
- package/dist/topics.d.ts.map +1 -0
- package/dist/topics.js +90 -0
- package/dist/topics.js.map +1 -0
- package/dist/workers/adapter/db-ops.d.ts +28 -0
- package/dist/workers/adapter/db-ops.d.ts.map +1 -0
- package/dist/workers/adapter/db-ops.js +14 -0
- package/dist/workers/adapter/db-ops.js.map +1 -0
- package/dist/workers/adapter/index.d.ts +16 -0
- package/dist/workers/adapter/index.d.ts.map +1 -0
- package/dist/workers/adapter/index.js +15 -0
- package/dist/workers/adapter/index.js.map +1 -0
- package/dist/workers/adapter/provider.d.ts +151 -0
- package/dist/workers/adapter/provider.d.ts.map +1 -0
- package/dist/workers/adapter/provider.js +654 -0
- package/dist/workers/adapter/provider.js.map +1 -0
- package/dist/workers/adapter/worker-adapter.kafka.descriptor.d.ts +36 -0
- package/dist/workers/adapter/worker-adapter.kafka.descriptor.d.ts.map +1 -0
- package/dist/workers/adapter/worker-adapter.kafka.descriptor.js +22 -0
- package/dist/workers/adapter/worker-adapter.kafka.descriptor.js.map +1 -0
- package/dist/workers/adapter/worker-adapter.kafka.impl.d.ts +36 -0
- package/dist/workers/adapter/worker-adapter.kafka.impl.d.ts.map +1 -0
- package/dist/workers/adapter/worker-adapter.kafka.impl.js +84 -0
- package/dist/workers/adapter/worker-adapter.kafka.impl.js.map +1 -0
- package/dist/workers/adapter/workers-kafka.db.context.d.ts +6 -0
- package/dist/workers/adapter/workers-kafka.db.context.d.ts.map +1 -0
- package/dist/workers/adapter/workers-kafka.db.context.descriptor.d.ts +43 -0
- package/dist/workers/adapter/workers-kafka.db.context.descriptor.d.ts.map +1 -0
- package/dist/workers/adapter/workers-kafka.db.context.descriptor.js +20 -0
- package/dist/workers/adapter/workers-kafka.db.context.descriptor.js.map +1 -0
- package/dist/workers/adapter/workers-kafka.db.context.js +5 -0
- package/dist/workers/adapter/workers-kafka.db.context.js.map +1 -0
- package/dist/workers/index.d.ts +8 -0
- package/dist/workers/index.d.ts.map +1 -0
- package/dist/workers/index.js +9 -0
- package/dist/workers/index.js.map +1 -0
- package/dist/workers/provider-jobs.sqlite-schema.d.ts +438 -0
- package/dist/workers/provider-jobs.sqlite-schema.d.ts.map +1 -0
- package/dist/workers/provider-jobs.sqlite-schema.js +37 -0
- package/dist/workers/provider-jobs.sqlite-schema.js.map +1 -0
- package/dist/workers/workers-kafka.consumer.plugin.d.ts +4 -0
- package/dist/workers/workers-kafka.consumer.plugin.d.ts.map +1 -0
- package/dist/workers/workers-kafka.consumer.plugin.js +11 -0
- package/dist/workers/workers-kafka.consumer.plugin.js.map +1 -0
- package/dist/workers/workers-kafka.plugin.d.ts +9 -0
- package/dist/workers/workers-kafka.plugin.d.ts.map +1 -0
- package/dist/workers/workers-kafka.plugin.js +27 -0
- package/dist/workers/workers-kafka.plugin.js.map +1 -0
- 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
|