@relaymesh/githook 0.0.7
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/README.md +143 -0
- package/dist/api.d.ts +68 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/api.js +260 -0
- package/dist/api.js.map +1 -0
- package/dist/client.d.ts +10 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +10 -0
- package/dist/client.js.map +1 -0
- package/dist/codec.d.ts +9 -0
- package/dist/codec.d.ts.map +1 -0
- package/dist/codec.js +92 -0
- package/dist/codec.js.map +1 -0
- package/dist/config.d.ts +29 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +2 -0
- package/dist/config.js.map +1 -0
- package/dist/context.d.ts +8 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +2 -0
- package/dist/context.js.map +1 -0
- package/dist/driver_config.d.ts +3 -0
- package/dist/driver_config.d.ts.map +1 -0
- package/dist/driver_config.js +36 -0
- package/dist/driver_config.js.map +1 -0
- package/dist/event.d.ts +14 -0
- package/dist/event.d.ts.map +1 -0
- package/dist/event.js +2 -0
- package/dist/event.js.map +1 -0
- package/dist/event_log_status.d.ts +4 -0
- package/dist/event_log_status.d.ts.map +1 -0
- package/dist/event_log_status.js +4 -0
- package/dist/event_log_status.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/listener.d.ts +15 -0
- package/dist/listener.d.ts.map +1 -0
- package/dist/listener.js +2 -0
- package/dist/listener.js.map +1 -0
- package/dist/metadata.d.ts +12 -0
- package/dist/metadata.d.ts.map +1 -0
- package/dist/metadata.js +12 -0
- package/dist/metadata.js.map +1 -0
- package/dist/oauth2.d.ts +20 -0
- package/dist/oauth2.d.ts.map +1 -0
- package/dist/oauth2.js +84 -0
- package/dist/oauth2.js.map +1 -0
- package/dist/retry.d.ts +20 -0
- package/dist/retry.d.ts.map +1 -0
- package/dist/retry.js +12 -0
- package/dist/retry.js.map +1 -0
- package/dist/scm_client_provider.d.ts +35 -0
- package/dist/scm_client_provider.d.ts.map +1 -0
- package/dist/scm_client_provider.js +131 -0
- package/dist/scm_client_provider.js.map +1 -0
- package/dist/scm_clients.d.ts +36 -0
- package/dist/scm_clients.d.ts.map +1 -0
- package/dist/scm_clients.js +129 -0
- package/dist/scm_clients.js.map +1 -0
- package/dist/subscriber.d.ts +8 -0
- package/dist/subscriber.d.ts.map +1 -0
- package/dist/subscriber.js +185 -0
- package/dist/subscriber.js.map +1 -0
- package/dist/types.d.ts +8 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/worker.d.ts +112 -0
- package/dist/worker.d.ts.map +1 -0
- package/dist/worker.js +726 -0
- package/dist/worker.js.map +1 -0
- package/gen/cloud/v1/githooks_pb.d.ts +2661 -0
- package/gen/cloud/v1/githooks_pb.js +921 -0
- package/package.json +29 -0
package/dist/worker.js
ADDED
|
@@ -0,0 +1,726 @@
|
|
|
1
|
+
import { DefaultCodec } from "./codec.js";
|
|
2
|
+
import { DriversClient, EventLogsClient, RulesClient } from "./api.js";
|
|
3
|
+
import { subscriberConfigFromDriver } from "./driver_config.js";
|
|
4
|
+
import { EventLogStatusFailed, EventLogStatusSuccess } from "./event_log_status.js";
|
|
5
|
+
import { MetadataKeyDriver, MetadataKeyLogID, MetadataKeyRequestID } from "./metadata.js";
|
|
6
|
+
import { NoRetry, normalizeRetryDecision } from "./retry.js";
|
|
7
|
+
import { buildSubscriber } from "./subscriber.js";
|
|
8
|
+
import { resolveOAuth2Config } from "./oauth2.js";
|
|
9
|
+
export class Worker {
|
|
10
|
+
constructor(opts = {}) {
|
|
11
|
+
this.topicHandlers = new Map();
|
|
12
|
+
this.topicDrivers = new Map();
|
|
13
|
+
this.typeHandlers = new Map();
|
|
14
|
+
this.ruleHandlers = new Map();
|
|
15
|
+
this.allowedTopics = new Set();
|
|
16
|
+
this.driverSubs = new Map();
|
|
17
|
+
this.topics = [];
|
|
18
|
+
this.subscriber = opts.subscriber;
|
|
19
|
+
this.codec = opts.codec ?? new DefaultCodec();
|
|
20
|
+
this.retry = opts.retry ?? new NoRetry();
|
|
21
|
+
this.logger = opts.logger ?? defaultLogger;
|
|
22
|
+
this.middleware = opts.middleware ?? [];
|
|
23
|
+
this.listeners = opts.listeners ?? [];
|
|
24
|
+
this.clientProvider = opts.clientProvider;
|
|
25
|
+
this.endpoint = resolveEndpoint(opts.endpoint);
|
|
26
|
+
this.apiKey = resolveApiKey(opts.apiKey);
|
|
27
|
+
this.oauth2Config = resolveOAuth2Config(opts.oauth2Config);
|
|
28
|
+
this.tenantId = resolveTenantId(opts.tenantId);
|
|
29
|
+
this.defaultDriverId = (opts.defaultDriverId ?? "").trim();
|
|
30
|
+
this.validate = opts.validateTopics !== false;
|
|
31
|
+
this.semaphore = new Semaphore(normalizeConcurrency(opts.concurrency));
|
|
32
|
+
if (opts.topics) {
|
|
33
|
+
this.addTopics(opts.topics);
|
|
34
|
+
}
|
|
35
|
+
this.bindClientProvider();
|
|
36
|
+
}
|
|
37
|
+
static new(...options) {
|
|
38
|
+
const wk = new Worker();
|
|
39
|
+
for (const opt of options) {
|
|
40
|
+
opt(wk);
|
|
41
|
+
}
|
|
42
|
+
return wk;
|
|
43
|
+
}
|
|
44
|
+
HandleTopic(topic, driverOrHandler, handler) {
|
|
45
|
+
if (typeof driverOrHandler === "function") {
|
|
46
|
+
this.handleTopic(topic, driverOrHandler);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
this.handleTopic(topic, driverOrHandler, handler);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
handleTopic(topic, driverOrHandler, handler) {
|
|
53
|
+
const trimmed = (topic ?? "").trim();
|
|
54
|
+
if (!trimmed) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (this.allowedTopics.size > 0 && !this.allowedTopics.has(trimmed)) {
|
|
58
|
+
logPrintf(this.logger, "handler topic not subscribed: %s", trimmed);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
let driverId = "";
|
|
62
|
+
let resolvedHandler;
|
|
63
|
+
if (typeof driverOrHandler === "function") {
|
|
64
|
+
resolvedHandler = driverOrHandler;
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
driverId = (driverOrHandler ?? "").trim();
|
|
68
|
+
resolvedHandler = handler;
|
|
69
|
+
}
|
|
70
|
+
if (!resolvedHandler) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (!driverId) {
|
|
74
|
+
driverId = this.defaultDriverId;
|
|
75
|
+
}
|
|
76
|
+
if (!driverId && !this.subscriber) {
|
|
77
|
+
logPrintf(this.logger, "driver id required for topic: %s", trimmed);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
this.topicHandlers.set(trimmed, toContextHandler(resolvedHandler));
|
|
81
|
+
if (driverId) {
|
|
82
|
+
this.topicDrivers.set(trimmed, driverId);
|
|
83
|
+
}
|
|
84
|
+
this.topics.push(trimmed);
|
|
85
|
+
}
|
|
86
|
+
handleType(eventType, handler) {
|
|
87
|
+
const trimmed = (eventType ?? "").trim();
|
|
88
|
+
if (!trimmed || !handler) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
this.typeHandlers.set(trimmed, toContextHandler(handler));
|
|
92
|
+
}
|
|
93
|
+
HandleType(eventType, handler) {
|
|
94
|
+
this.handleType(eventType, handler);
|
|
95
|
+
}
|
|
96
|
+
handleRule(ruleId, handler) {
|
|
97
|
+
const trimmed = (ruleId ?? "").trim();
|
|
98
|
+
if (!trimmed || !handler) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
this.ruleHandlers.set(trimmed, toContextHandler(handler));
|
|
102
|
+
}
|
|
103
|
+
HandleRule(ruleId, handler) {
|
|
104
|
+
this.handleRule(ruleId, handler);
|
|
105
|
+
}
|
|
106
|
+
async run(ctx) {
|
|
107
|
+
const baseCtx = this.resolveContext(ctx);
|
|
108
|
+
await this.prepareRuleSubscriptions(baseCtx);
|
|
109
|
+
if (this.topics.length === 0) {
|
|
110
|
+
throw new Error("at least one topic is required");
|
|
111
|
+
}
|
|
112
|
+
const abortPromise = makeAbortPromise(baseCtx.signal, () => this.close());
|
|
113
|
+
if (this.subscriber) {
|
|
114
|
+
if (this.validate) {
|
|
115
|
+
await this.validateTopics(baseCtx);
|
|
116
|
+
}
|
|
117
|
+
await Promise.race([this.runWithSubscriber(baseCtx, this.subscriber, unique(this.topics)), abortPromise]);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const driverTopics = this.topicsByDriver();
|
|
121
|
+
if (this.validate) {
|
|
122
|
+
await this.validateTopics(baseCtx);
|
|
123
|
+
}
|
|
124
|
+
await this.buildDriverSubscribers(baseCtx, driverTopics);
|
|
125
|
+
await Promise.race([this.runDriverSubscribers(baseCtx, driverTopics), abortPromise]);
|
|
126
|
+
}
|
|
127
|
+
Run(ctx) {
|
|
128
|
+
return this.run(ctx);
|
|
129
|
+
}
|
|
130
|
+
async close() {
|
|
131
|
+
if (this.subscriber) {
|
|
132
|
+
await this.subscriber.close();
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
for (const sub of this.driverSubs.values()) {
|
|
136
|
+
await sub.close();
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
Close() {
|
|
140
|
+
return this.close();
|
|
141
|
+
}
|
|
142
|
+
apply(options) {
|
|
143
|
+
if (options.subscriber) {
|
|
144
|
+
this.subscriber = options.subscriber;
|
|
145
|
+
}
|
|
146
|
+
if (options.codec) {
|
|
147
|
+
this.codec = options.codec;
|
|
148
|
+
}
|
|
149
|
+
if (options.retry) {
|
|
150
|
+
this.retry = options.retry;
|
|
151
|
+
}
|
|
152
|
+
if (options.logger) {
|
|
153
|
+
this.logger = options.logger;
|
|
154
|
+
}
|
|
155
|
+
if (options.middleware) {
|
|
156
|
+
this.middleware.push(...options.middleware);
|
|
157
|
+
}
|
|
158
|
+
if (options.listeners) {
|
|
159
|
+
this.listeners.push(...options.listeners);
|
|
160
|
+
}
|
|
161
|
+
if (options.clientProvider) {
|
|
162
|
+
this.clientProvider = options.clientProvider;
|
|
163
|
+
}
|
|
164
|
+
if (options.endpoint) {
|
|
165
|
+
this.endpoint = resolveEndpoint(options.endpoint);
|
|
166
|
+
}
|
|
167
|
+
if (options.apiKey) {
|
|
168
|
+
this.apiKey = resolveApiKey(options.apiKey);
|
|
169
|
+
}
|
|
170
|
+
if (options.oauth2Config) {
|
|
171
|
+
this.oauth2Config = resolveOAuth2Config(options.oauth2Config);
|
|
172
|
+
}
|
|
173
|
+
if (options.tenantId) {
|
|
174
|
+
this.tenantId = resolveTenantId(options.tenantId);
|
|
175
|
+
}
|
|
176
|
+
if (options.defaultDriverId) {
|
|
177
|
+
this.defaultDriverId = options.defaultDriverId.trim();
|
|
178
|
+
}
|
|
179
|
+
if (options.validateTopics !== undefined) {
|
|
180
|
+
this.validate = options.validateTopics;
|
|
181
|
+
}
|
|
182
|
+
if (options.concurrency) {
|
|
183
|
+
this.semaphore = new Semaphore(normalizeConcurrency(options.concurrency));
|
|
184
|
+
}
|
|
185
|
+
if (options.topics) {
|
|
186
|
+
this.addTopics(options.topics);
|
|
187
|
+
}
|
|
188
|
+
if (options.clientProvider || options.oauth2Config || options.endpoint || options.apiKey) {
|
|
189
|
+
this.bindClientProvider();
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
Apply(options) {
|
|
193
|
+
this.apply(options);
|
|
194
|
+
}
|
|
195
|
+
addTopics(topics) {
|
|
196
|
+
for (const topic of topics) {
|
|
197
|
+
const trimmed = (topic ?? "").trim();
|
|
198
|
+
if (!trimmed) {
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
this.topics.push(trimmed);
|
|
202
|
+
this.allowedTopics.add(trimmed);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
async runWithSubscriber(ctx, sub, topics) {
|
|
206
|
+
this.notifyStart(ctx);
|
|
207
|
+
try {
|
|
208
|
+
await Promise.all(topics.map((topic) => this.runTopicSubscriber(ctx, sub, topic)));
|
|
209
|
+
}
|
|
210
|
+
finally {
|
|
211
|
+
this.notifyExit(ctx);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
async runDriverSubscribers(ctx, driverTopics) {
|
|
215
|
+
this.notifyStart(ctx);
|
|
216
|
+
try {
|
|
217
|
+
const tasks = [];
|
|
218
|
+
for (const [driverId, topics] of driverTopics.entries()) {
|
|
219
|
+
const sub = this.driverSubs.get(driverId);
|
|
220
|
+
if (!sub) {
|
|
221
|
+
throw new Error(`subscriber not initialized for driver: ${driverId}`);
|
|
222
|
+
}
|
|
223
|
+
for (const topic of unique(topics)) {
|
|
224
|
+
tasks.push(this.runTopicSubscriber(ctx, sub, topic));
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
await Promise.all(tasks);
|
|
228
|
+
}
|
|
229
|
+
finally {
|
|
230
|
+
this.notifyExit(ctx);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
async runTopicSubscriber(ctx, sub, topic) {
|
|
234
|
+
await sub.start(topic, async (msg) => {
|
|
235
|
+
await this.semaphore.use(async () => {
|
|
236
|
+
const shouldNack = await this.handleMessage(ctx, topic, msg);
|
|
237
|
+
if (shouldNack && shouldRequeue(msg)) {
|
|
238
|
+
throw new Error("message nack requested");
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
topicsByDriver() {
|
|
244
|
+
const topics = unique(this.topics);
|
|
245
|
+
if (this.topicDrivers.size === 0) {
|
|
246
|
+
if (this.defaultDriverId) {
|
|
247
|
+
return new Map([[this.defaultDriverId, topics]]);
|
|
248
|
+
}
|
|
249
|
+
throw new Error("driver id is required for topics");
|
|
250
|
+
}
|
|
251
|
+
const out = new Map();
|
|
252
|
+
for (const topic of topics) {
|
|
253
|
+
let driverId = this.topicDrivers.get(topic);
|
|
254
|
+
if (!driverId) {
|
|
255
|
+
driverId = this.defaultDriverId;
|
|
256
|
+
}
|
|
257
|
+
const trimmed = (driverId ?? "").trim();
|
|
258
|
+
if (!trimmed) {
|
|
259
|
+
throw new Error(`driver id is required for topic: ${topic}`);
|
|
260
|
+
}
|
|
261
|
+
const list = out.get(trimmed) ?? [];
|
|
262
|
+
list.push(topic);
|
|
263
|
+
out.set(trimmed, list);
|
|
264
|
+
}
|
|
265
|
+
return out;
|
|
266
|
+
}
|
|
267
|
+
async buildDriverSubscribers(ctx, driverTopics) {
|
|
268
|
+
for (const driverId of driverTopics.keys()) {
|
|
269
|
+
if (this.driverSubs.has(driverId)) {
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
const record = await this.driversClient().getDriverById(driverId, ctx);
|
|
273
|
+
if (!record) {
|
|
274
|
+
throw new Error(`driver not found: ${driverId}`);
|
|
275
|
+
}
|
|
276
|
+
if (!record.enabled) {
|
|
277
|
+
throw new Error(`driver is disabled: ${driverId}`);
|
|
278
|
+
}
|
|
279
|
+
const cfg = subscriberConfigFromDriver(record.name, record.configJson);
|
|
280
|
+
const sub = buildSubscriber(cfg);
|
|
281
|
+
this.driverSubs.set(driverId, sub);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
async validateTopics(ctx) {
|
|
285
|
+
const rules = await this.rulesClient().listRules(ctx);
|
|
286
|
+
if (rules.length === 0) {
|
|
287
|
+
throw new Error("no rules available from api");
|
|
288
|
+
}
|
|
289
|
+
const allowedTopics = new Set();
|
|
290
|
+
const allowedByDriver = new Map();
|
|
291
|
+
for (const rule of rules) {
|
|
292
|
+
for (const topic of rule.emit) {
|
|
293
|
+
const trimmed = (topic ?? "").trim();
|
|
294
|
+
if (!trimmed) {
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
allowedTopics.add(trimmed);
|
|
298
|
+
const driverId = (rule.driverId ?? "").trim();
|
|
299
|
+
if (!driverId) {
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
if (!allowedByDriver.has(driverId)) {
|
|
303
|
+
allowedByDriver.set(driverId, new Set());
|
|
304
|
+
}
|
|
305
|
+
allowedByDriver.get(driverId)?.add(trimmed);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (allowedTopics.size === 0) {
|
|
309
|
+
throw new Error("no topics available from rules");
|
|
310
|
+
}
|
|
311
|
+
const topics = unique(this.topics);
|
|
312
|
+
if (this.subscriber) {
|
|
313
|
+
for (const topic of topics) {
|
|
314
|
+
if (!allowedTopics.has(topic)) {
|
|
315
|
+
throw new Error(`unknown topic: ${topic}`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
for (const topic of topics) {
|
|
321
|
+
let driverId = this.topicDrivers.get(topic);
|
|
322
|
+
if (!driverId) {
|
|
323
|
+
driverId = this.defaultDriverId;
|
|
324
|
+
}
|
|
325
|
+
if (!driverId) {
|
|
326
|
+
throw new Error(`driver id is required for topic: ${topic}`);
|
|
327
|
+
}
|
|
328
|
+
const allowed = allowedByDriver.get(driverId);
|
|
329
|
+
if (!allowed) {
|
|
330
|
+
throw new Error(`driver not configured on any rule: ${driverId}`);
|
|
331
|
+
}
|
|
332
|
+
if (!allowed.has(topic)) {
|
|
333
|
+
throw new Error(`topic ${topic} not configured for driver ${driverId}`);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
async prepareRuleSubscriptions(ctx) {
|
|
338
|
+
if (this.ruleHandlers.size === 0) {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
const client = this.rulesClient();
|
|
342
|
+
for (const [ruleId, handler] of this.ruleHandlers.entries()) {
|
|
343
|
+
const record = await client.getRule(ruleId, ctx);
|
|
344
|
+
if (record.emit.length === 0) {
|
|
345
|
+
throw new Error(`rule ${ruleId} has no emit topic`);
|
|
346
|
+
}
|
|
347
|
+
const topic = (record.emit[0] ?? "").trim();
|
|
348
|
+
if (!topic) {
|
|
349
|
+
throw new Error(`rule ${ruleId} emit topic empty`);
|
|
350
|
+
}
|
|
351
|
+
const driverId = (record.driverId ?? "").trim();
|
|
352
|
+
if (!driverId) {
|
|
353
|
+
throw new Error(`rule ${ruleId} driver_id is required`);
|
|
354
|
+
}
|
|
355
|
+
if (this.topicHandlers.has(topic)) {
|
|
356
|
+
logPrintf(this.logger, "overwriting handler for topic=%s due to rule=%s", topic, ruleId);
|
|
357
|
+
}
|
|
358
|
+
this.topicHandlers.set(topic, handler);
|
|
359
|
+
this.topicDrivers.set(topic, driverId);
|
|
360
|
+
this.topics.push(topic);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
async handleMessage(ctx, topic, msg) {
|
|
364
|
+
const logId = msg.metadata?.[MetadataKeyLogID] ?? "";
|
|
365
|
+
let event;
|
|
366
|
+
try {
|
|
367
|
+
event = this.codec.decode(topic, msg);
|
|
368
|
+
}
|
|
369
|
+
catch (err) {
|
|
370
|
+
const error = normalizeError(err);
|
|
371
|
+
logPrintf(this.logger, "decode failed: %s", error.message);
|
|
372
|
+
await this.updateEventLogStatus(ctx, logId, EventLogStatusFailed, error);
|
|
373
|
+
this.notifyError(ctx, undefined, error);
|
|
374
|
+
const decision = normalizeRetryDecision(callRetryPolicy(this.retry, ctx, undefined, error));
|
|
375
|
+
return decision.retry || decision.nack;
|
|
376
|
+
}
|
|
377
|
+
const eventCtx = this.buildContext(ctx, topic, msg);
|
|
378
|
+
if (this.clientProvider) {
|
|
379
|
+
try {
|
|
380
|
+
event.client = await resolveClientProvider(this.clientProvider)(eventCtx, event);
|
|
381
|
+
}
|
|
382
|
+
catch (err) {
|
|
383
|
+
const error = normalizeError(err);
|
|
384
|
+
logPrintf(this.logger, "client init failed: %s", error.message);
|
|
385
|
+
await this.updateEventLogStatus(eventCtx, logId, EventLogStatusFailed, error);
|
|
386
|
+
this.notifyError(eventCtx, event, error);
|
|
387
|
+
const decision = normalizeRetryDecision(callRetryPolicy(this.retry, eventCtx, event, error));
|
|
388
|
+
return decision.retry || decision.nack;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
const reqId = event.metadata[MetadataKeyRequestID];
|
|
392
|
+
if (reqId) {
|
|
393
|
+
logPrintf(this.logger, "request_id=%s topic=%s provider=%s type=%s", reqId, event.topic, event.provider, event.type);
|
|
394
|
+
}
|
|
395
|
+
this.notifyMessageStart(eventCtx, event);
|
|
396
|
+
const handler = this.topicHandlers.get(topic) ?? this.typeHandlers.get(event.type);
|
|
397
|
+
if (!handler) {
|
|
398
|
+
logPrintf(this.logger, "no handler for topic=%s type=%s", topic, event.type);
|
|
399
|
+
this.notifyMessageFinish(eventCtx, event, undefined);
|
|
400
|
+
await this.updateEventLogStatus(eventCtx, logId, EventLogStatusSuccess);
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
403
|
+
const wrapped = this.wrap(handler);
|
|
404
|
+
try {
|
|
405
|
+
await wrapped(eventCtx, event);
|
|
406
|
+
this.notifyMessageFinish(eventCtx, event, undefined);
|
|
407
|
+
await this.updateEventLogStatus(eventCtx, logId, EventLogStatusSuccess);
|
|
408
|
+
return false;
|
|
409
|
+
}
|
|
410
|
+
catch (err) {
|
|
411
|
+
const error = normalizeError(err);
|
|
412
|
+
this.notifyMessageFinish(eventCtx, event, error);
|
|
413
|
+
this.notifyError(eventCtx, event, error);
|
|
414
|
+
await this.updateEventLogStatus(eventCtx, logId, EventLogStatusFailed, error);
|
|
415
|
+
const decision = normalizeRetryDecision(callRetryPolicy(this.retry, eventCtx, event, error));
|
|
416
|
+
return decision.retry || decision.nack;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
wrap(handler) {
|
|
420
|
+
let wrapped = handler;
|
|
421
|
+
for (let i = this.middleware.length - 1; i >= 0; i -= 1) {
|
|
422
|
+
wrapped = this.middleware[i](wrapped);
|
|
423
|
+
}
|
|
424
|
+
return wrapped;
|
|
425
|
+
}
|
|
426
|
+
buildContext(base, topic, msg) {
|
|
427
|
+
return {
|
|
428
|
+
tenantId: base.tenantId,
|
|
429
|
+
signal: base.signal,
|
|
430
|
+
topic,
|
|
431
|
+
requestId: msg.metadata?.[MetadataKeyRequestID],
|
|
432
|
+
logId: msg.metadata?.[MetadataKeyLogID],
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
resolveContext(ctx) {
|
|
436
|
+
if (!ctx) {
|
|
437
|
+
return { tenantId: this.tenantId };
|
|
438
|
+
}
|
|
439
|
+
if (isAbortSignal(ctx)) {
|
|
440
|
+
return { tenantId: this.tenantId, signal: ctx };
|
|
441
|
+
}
|
|
442
|
+
return { tenantId: ctx.tenantId ?? this.tenantId, signal: ctx.signal };
|
|
443
|
+
}
|
|
444
|
+
notifyStart(ctx) {
|
|
445
|
+
for (const listener of this.listeners) {
|
|
446
|
+
listener.onStart?.(ctx);
|
|
447
|
+
listener.OnStart?.(ctx);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
notifyExit(ctx) {
|
|
451
|
+
for (const listener of this.listeners) {
|
|
452
|
+
listener.onExit?.(ctx);
|
|
453
|
+
listener.OnExit?.(ctx);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
notifyMessageStart(ctx, evt) {
|
|
457
|
+
for (const listener of this.listeners) {
|
|
458
|
+
listener.onMessageStart?.(ctx, evt);
|
|
459
|
+
listener.OnMessageStart?.(ctx, evt);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
notifyMessageFinish(ctx, evt, err) {
|
|
463
|
+
for (const listener of this.listeners) {
|
|
464
|
+
listener.onMessageFinish?.(ctx, evt, err);
|
|
465
|
+
listener.OnMessageFinish?.(ctx, evt, err);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
notifyError(ctx, evt, err) {
|
|
469
|
+
for (const listener of this.listeners) {
|
|
470
|
+
listener.onError?.(ctx, evt, err);
|
|
471
|
+
listener.OnError?.(ctx, evt, err);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
driversClient() {
|
|
475
|
+
return new DriversClient(this.apiClientOptions());
|
|
476
|
+
}
|
|
477
|
+
rulesClient() {
|
|
478
|
+
return new RulesClient(this.apiClientOptions());
|
|
479
|
+
}
|
|
480
|
+
eventLogsClient() {
|
|
481
|
+
return new EventLogsClient(this.apiClientOptions());
|
|
482
|
+
}
|
|
483
|
+
apiClientOptions() {
|
|
484
|
+
return {
|
|
485
|
+
baseUrl: this.endpoint,
|
|
486
|
+
apiKey: this.apiKey,
|
|
487
|
+
oauth2Config: this.oauth2Config,
|
|
488
|
+
tenantId: this.tenantId,
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
async updateEventLogStatus(ctx, logId, status, err) {
|
|
492
|
+
if (!logId) {
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
try {
|
|
496
|
+
await this.eventLogsClient().updateStatus(logId, status, err?.message, ctx);
|
|
497
|
+
}
|
|
498
|
+
catch (updateErr) {
|
|
499
|
+
const error = normalizeError(updateErr);
|
|
500
|
+
logPrintf(this.logger, "event log update failed: %s", error.message);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
bindClientProvider() {
|
|
504
|
+
if (!this.clientProvider) {
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
const provider = this.clientProvider;
|
|
508
|
+
const opts = this.apiClientOptions();
|
|
509
|
+
if (provider.bindAPIClient) {
|
|
510
|
+
provider.bindAPIClient(opts);
|
|
511
|
+
}
|
|
512
|
+
else if (provider.BindAPIClient) {
|
|
513
|
+
provider.BindAPIClient(opts);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
export function New(...options) {
|
|
518
|
+
return Worker.new(...options);
|
|
519
|
+
}
|
|
520
|
+
export function WithSubscriber(subscriber) {
|
|
521
|
+
return (wk) => wk.apply({ subscriber });
|
|
522
|
+
}
|
|
523
|
+
export function WithTopics(...topics) {
|
|
524
|
+
return (wk) => wk.apply({ topics });
|
|
525
|
+
}
|
|
526
|
+
export function WithConcurrency(concurrency) {
|
|
527
|
+
return (wk) => wk.apply({ concurrency });
|
|
528
|
+
}
|
|
529
|
+
export function WithCodec(codec) {
|
|
530
|
+
return (wk) => wk.apply({ codec });
|
|
531
|
+
}
|
|
532
|
+
export function WithMiddleware(...middleware) {
|
|
533
|
+
return (wk) => wk.apply({ middleware });
|
|
534
|
+
}
|
|
535
|
+
export function WithRetry(retry) {
|
|
536
|
+
return (wk) => wk.apply({ retry });
|
|
537
|
+
}
|
|
538
|
+
export function WithLogger(logger) {
|
|
539
|
+
return (wk) => wk.apply({ logger });
|
|
540
|
+
}
|
|
541
|
+
export function WithClientProvider(clientProvider) {
|
|
542
|
+
return (wk) => wk.apply({ clientProvider });
|
|
543
|
+
}
|
|
544
|
+
export function WithListener(listener) {
|
|
545
|
+
return (wk) => wk.apply({ listeners: [listener] });
|
|
546
|
+
}
|
|
547
|
+
export function WithEndpoint(endpoint) {
|
|
548
|
+
return (wk) => wk.apply({ endpoint });
|
|
549
|
+
}
|
|
550
|
+
export function WithAPIKey(apiKey) {
|
|
551
|
+
return (wk) => wk.apply({ apiKey });
|
|
552
|
+
}
|
|
553
|
+
export function WithOAuth2Config(oauth2Config) {
|
|
554
|
+
return (wk) => wk.apply({ oauth2Config });
|
|
555
|
+
}
|
|
556
|
+
export function WithTenant(tenantId) {
|
|
557
|
+
return (wk) => wk.apply({ tenantId });
|
|
558
|
+
}
|
|
559
|
+
export function WithDefaultDriver(driverId) {
|
|
560
|
+
return (wk) => wk.apply({ defaultDriverId: driverId });
|
|
561
|
+
}
|
|
562
|
+
export function WithValidateTopics(validate) {
|
|
563
|
+
return (wk) => wk.apply({ validateTopics: validate });
|
|
564
|
+
}
|
|
565
|
+
const defaultLogger = {
|
|
566
|
+
printf(format, ...args) {
|
|
567
|
+
if (args.length === 0) {
|
|
568
|
+
console.log(`githook/worker ${format}`);
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
console.log(`githook/worker ${format}`, ...args);
|
|
572
|
+
},
|
|
573
|
+
};
|
|
574
|
+
function resolveEndpoint(explicit) {
|
|
575
|
+
const trimmed = (explicit ?? "").trim();
|
|
576
|
+
if (trimmed) {
|
|
577
|
+
return trimmed.replace(/\/+$/, "");
|
|
578
|
+
}
|
|
579
|
+
const envEndpoint = envValue("GITHOOK_ENDPOINT");
|
|
580
|
+
if (envEndpoint) {
|
|
581
|
+
return envEndpoint;
|
|
582
|
+
}
|
|
583
|
+
const envBase = envValue("GITHOOK_API_BASE_URL");
|
|
584
|
+
if (envBase) {
|
|
585
|
+
return envBase;
|
|
586
|
+
}
|
|
587
|
+
return "http://localhost:8080";
|
|
588
|
+
}
|
|
589
|
+
function resolveApiKey(explicit) {
|
|
590
|
+
const trimmed = (explicit ?? "").trim();
|
|
591
|
+
if (trimmed) {
|
|
592
|
+
return trimmed;
|
|
593
|
+
}
|
|
594
|
+
return envValue("GITHOOK_API_KEY");
|
|
595
|
+
}
|
|
596
|
+
function resolveTenantId(explicit) {
|
|
597
|
+
const trimmed = (explicit ?? "").trim();
|
|
598
|
+
if (trimmed) {
|
|
599
|
+
return trimmed;
|
|
600
|
+
}
|
|
601
|
+
return envValue("GITHOOK_TENANT_ID");
|
|
602
|
+
}
|
|
603
|
+
function envValue(key) {
|
|
604
|
+
return (process.env[key] ?? "").trim();
|
|
605
|
+
}
|
|
606
|
+
function normalizeConcurrency(value) {
|
|
607
|
+
if (!value || value < 1) {
|
|
608
|
+
return 1;
|
|
609
|
+
}
|
|
610
|
+
return Math.floor(value);
|
|
611
|
+
}
|
|
612
|
+
function unique(values) {
|
|
613
|
+
const seen = new Set();
|
|
614
|
+
const out = [];
|
|
615
|
+
for (const value of values) {
|
|
616
|
+
if (seen.has(value)) {
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
619
|
+
seen.add(value);
|
|
620
|
+
out.push(value);
|
|
621
|
+
}
|
|
622
|
+
return out;
|
|
623
|
+
}
|
|
624
|
+
function normalizeError(err) {
|
|
625
|
+
if (err instanceof Error) {
|
|
626
|
+
return err;
|
|
627
|
+
}
|
|
628
|
+
return new Error(String(err));
|
|
629
|
+
}
|
|
630
|
+
function callRetryPolicy(policy, ctx, evt, err) {
|
|
631
|
+
if (policy.OnError) {
|
|
632
|
+
return policy.OnError(ctx, evt, err);
|
|
633
|
+
}
|
|
634
|
+
if (policy.onError) {
|
|
635
|
+
return policy.onError(ctx, evt, err);
|
|
636
|
+
}
|
|
637
|
+
return { retry: false, nack: true };
|
|
638
|
+
}
|
|
639
|
+
function logPrintf(logger, format, ...args) {
|
|
640
|
+
if (logger.Printf) {
|
|
641
|
+
logger.Printf(format, ...args);
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
if (logger.printf) {
|
|
645
|
+
logger.printf(format, ...args);
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
if (args.length === 0) {
|
|
649
|
+
console.log(`githook/worker ${format}`);
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
console.log(`githook/worker ${format}`, ...args);
|
|
653
|
+
}
|
|
654
|
+
function resolveClientProvider(provider) {
|
|
655
|
+
if (provider.client) {
|
|
656
|
+
return async (ctx, evt) => provider.client?.(ctx, evt);
|
|
657
|
+
}
|
|
658
|
+
if (provider.Client) {
|
|
659
|
+
return async (ctx, evt) => provider.Client?.(ctx, evt);
|
|
660
|
+
}
|
|
661
|
+
return async () => undefined;
|
|
662
|
+
}
|
|
663
|
+
function toContextHandler(handler) {
|
|
664
|
+
if (handler.length >= 2) {
|
|
665
|
+
return handler;
|
|
666
|
+
}
|
|
667
|
+
return (_ctx, event) => handler(event);
|
|
668
|
+
}
|
|
669
|
+
function shouldRequeue(msg) {
|
|
670
|
+
const driver = (msg.metadata?.[MetadataKeyDriver] ?? "").toLowerCase();
|
|
671
|
+
return driver === "amqp";
|
|
672
|
+
}
|
|
673
|
+
function isAbortSignal(value) {
|
|
674
|
+
return !!value && typeof value.aborted === "boolean";
|
|
675
|
+
}
|
|
676
|
+
function makeAbortPromise(signal, onAbort) {
|
|
677
|
+
if (!signal) {
|
|
678
|
+
return new Promise(() => { });
|
|
679
|
+
}
|
|
680
|
+
if (signal.aborted) {
|
|
681
|
+
onAbort();
|
|
682
|
+
return Promise.reject(new Error("aborted"));
|
|
683
|
+
}
|
|
684
|
+
return new Promise((_, reject) => {
|
|
685
|
+
signal.addEventListener("abort", () => {
|
|
686
|
+
onAbort();
|
|
687
|
+
reject(new Error("aborted"));
|
|
688
|
+
}, { once: true });
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
class Semaphore {
|
|
692
|
+
constructor(size) {
|
|
693
|
+
this.size = size;
|
|
694
|
+
this.queue = [];
|
|
695
|
+
this.available = size;
|
|
696
|
+
}
|
|
697
|
+
async use(task) {
|
|
698
|
+
const release = await this.acquire();
|
|
699
|
+
try {
|
|
700
|
+
await task();
|
|
701
|
+
}
|
|
702
|
+
finally {
|
|
703
|
+
release();
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
acquire() {
|
|
707
|
+
return new Promise((resolve) => {
|
|
708
|
+
const attempt = () => {
|
|
709
|
+
if (this.available > 0) {
|
|
710
|
+
this.available -= 1;
|
|
711
|
+
resolve(() => {
|
|
712
|
+
this.available += 1;
|
|
713
|
+
const next = this.queue.shift();
|
|
714
|
+
if (next) {
|
|
715
|
+
next();
|
|
716
|
+
}
|
|
717
|
+
});
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
this.queue.push(attempt);
|
|
721
|
+
};
|
|
722
|
+
attempt();
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
//# sourceMappingURL=worker.js.map
|