@rpcbase/worker 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +176 -26
- package/dist/queueListener.d.ts +34 -5
- package/dist/queueListener.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -109,7 +109,28 @@ const queueApi = {
|
|
|
109
109
|
};
|
|
110
110
|
const RETRY_MAXIMUM_DELAY_MS = 3e3;
|
|
111
111
|
const RETRY_MINIMUM_DELAY_MS = 50;
|
|
112
|
-
const
|
|
112
|
+
const RETRY_DEFAULT_FACTOR = 2;
|
|
113
|
+
const sleep = async (ms, signal) => new Promise((resolve) => {
|
|
114
|
+
if (signal?.aborted) {
|
|
115
|
+
resolve();
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
let timeout = null;
|
|
119
|
+
const cleanup = () => {
|
|
120
|
+
if (timeout) clearTimeout(timeout);
|
|
121
|
+
signal?.removeEventListener("abort", onAbort);
|
|
122
|
+
};
|
|
123
|
+
const onAbort = () => {
|
|
124
|
+
cleanup();
|
|
125
|
+
resolve();
|
|
126
|
+
};
|
|
127
|
+
timeout = setTimeout(() => {
|
|
128
|
+
cleanup();
|
|
129
|
+
resolve();
|
|
130
|
+
}, ms);
|
|
131
|
+
timeout.unref?.();
|
|
132
|
+
signal?.addEventListener("abort", onAbort);
|
|
133
|
+
});
|
|
113
134
|
const getMongoUrl = () => {
|
|
114
135
|
const explicit = process.env.MONGODB_URL ?? process.env.MONGO_URL ?? process.env.MONGODB_URI ?? process.env.DB_URL;
|
|
115
136
|
if (explicit && explicit.trim()) return explicit.trim();
|
|
@@ -162,19 +183,66 @@ const dispatchWorkerQueue = async ({
|
|
|
162
183
|
const shouldSkipCollection = (collName) => collName.endsWith(".files") || collName.endsWith(".chunks");
|
|
163
184
|
const INTERNAL_IGNORED_MODEL_NAMES = /* @__PURE__ */ new Set(["RBRtsChange", "RBRtsCounter"]);
|
|
164
185
|
const INTERNAL_IGNORED_COLLECTION_NAMES = /* @__PURE__ */ new Set(["rtschanges", "rtscounters"]);
|
|
186
|
+
const normalizeRetryDelays = (input) => {
|
|
187
|
+
const minMs = Math.max(0, Math.floor(input?.minMs ?? RETRY_MINIMUM_DELAY_MS));
|
|
188
|
+
const rawMaxMs = Math.max(0, Math.floor(input?.maxMs ?? RETRY_MAXIMUM_DELAY_MS));
|
|
189
|
+
const maxMs = Math.max(minMs, rawMaxMs);
|
|
190
|
+
const factor = Math.max(1, Number.isFinite(input?.factor) ? input?.factor ?? RETRY_DEFAULT_FACTOR : RETRY_DEFAULT_FACTOR);
|
|
191
|
+
return { minMs, maxMs, factor };
|
|
192
|
+
};
|
|
193
|
+
const getRetryDelayMs = (attempt, delays) => Math.min(delays.maxMs, Math.round(delays.minMs * Math.pow(delays.factor, Math.max(0, attempt - 1))));
|
|
194
|
+
const normalizeMaxRetries = (value) => {
|
|
195
|
+
if (value === "infinite") return value;
|
|
196
|
+
const parsed = Math.floor(value);
|
|
197
|
+
return Number.isFinite(parsed) ? Math.max(0, parsed) : 0;
|
|
198
|
+
};
|
|
165
199
|
const registerQueueListener = async (options = {}) => {
|
|
166
|
-
const maxRetries =
|
|
200
|
+
const maxRetries = normalizeMaxRetries(options.maxRetries ?? "infinite");
|
|
201
|
+
const fatalOnMaxRetries = options.fatalOnMaxRetries ?? false;
|
|
202
|
+
const retryDelays = normalizeRetryDelays(options.retryDelays);
|
|
167
203
|
const appName = process.env.APP_NAME?.trim();
|
|
168
204
|
if (!appName) {
|
|
169
205
|
throw new Error("Missing APP_NAME (required to configure the worker DB change listener)");
|
|
170
206
|
}
|
|
171
207
|
const mongoUrl = getMongoUrl();
|
|
208
|
+
const mongoClientOptions = {
|
|
209
|
+
family: 4,
|
|
210
|
+
serverSelectionTimeoutMS: 2e3,
|
|
211
|
+
connectTimeoutMS: 2e3,
|
|
212
|
+
...options.mongoClientOptions
|
|
213
|
+
};
|
|
172
214
|
let stopped = false;
|
|
173
|
-
|
|
215
|
+
const abortController = new AbortController();
|
|
174
216
|
let client = null;
|
|
175
217
|
let stream = null;
|
|
176
218
|
let resumeAfter = null;
|
|
177
219
|
let processing = Promise.resolve();
|
|
220
|
+
let status = { state: "connecting", attempt: 1 };
|
|
221
|
+
const setStatus = (next) => {
|
|
222
|
+
status = next;
|
|
223
|
+
try {
|
|
224
|
+
options.onStateChange?.(next);
|
|
225
|
+
} catch (err) {
|
|
226
|
+
console.warn("queue listener onStateChange failed", err);
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
let readySettled = false;
|
|
230
|
+
let readyResolve = null;
|
|
231
|
+
let readyReject = null;
|
|
232
|
+
const ready = new Promise((resolve, reject) => {
|
|
233
|
+
readyResolve = resolve;
|
|
234
|
+
readyReject = reject;
|
|
235
|
+
});
|
|
236
|
+
const resolveReady = () => {
|
|
237
|
+
if (readySettled) return;
|
|
238
|
+
readySettled = true;
|
|
239
|
+
readyResolve?.();
|
|
240
|
+
};
|
|
241
|
+
const rejectReady = (err) => {
|
|
242
|
+
if (readySettled) return;
|
|
243
|
+
readySettled = true;
|
|
244
|
+
readyReject?.(err);
|
|
245
|
+
};
|
|
178
246
|
const isChangeStreamHistoryLost = (err) => {
|
|
179
247
|
const anyErr = err;
|
|
180
248
|
const code = typeof anyErr?.code === "number" ? anyErr.code : null;
|
|
@@ -184,6 +252,24 @@ const registerQueueListener = async (options = {}) => {
|
|
|
184
252
|
};
|
|
185
253
|
const close2 = async () => {
|
|
186
254
|
stopped = true;
|
|
255
|
+
abortController.abort();
|
|
256
|
+
try {
|
|
257
|
+
stream?.removeAllListeners();
|
|
258
|
+
await stream?.close();
|
|
259
|
+
} catch {
|
|
260
|
+
}
|
|
261
|
+
stream = null;
|
|
262
|
+
try {
|
|
263
|
+
await client?.close();
|
|
264
|
+
} catch {
|
|
265
|
+
}
|
|
266
|
+
client = null;
|
|
267
|
+
if (!readySettled) {
|
|
268
|
+
rejectReady(new Error("queue listener closed before ready"));
|
|
269
|
+
}
|
|
270
|
+
setStatus({ state: "closed" });
|
|
271
|
+
};
|
|
272
|
+
const closeResources = async () => {
|
|
187
273
|
try {
|
|
188
274
|
stream?.removeAllListeners();
|
|
189
275
|
await stream?.close();
|
|
@@ -197,7 +283,7 @@ const registerQueueListener = async (options = {}) => {
|
|
|
197
283
|
client = null;
|
|
198
284
|
};
|
|
199
285
|
const startStream = async () => {
|
|
200
|
-
if (stopped) return;
|
|
286
|
+
if (stopped) return { stoppedPromise: Promise.resolve({ reason: "close" }) };
|
|
201
287
|
if (stream) {
|
|
202
288
|
try {
|
|
203
289
|
stream.removeAllListeners();
|
|
@@ -213,11 +299,7 @@ const registerQueueListener = async (options = {}) => {
|
|
|
213
299
|
}
|
|
214
300
|
client = null;
|
|
215
301
|
}
|
|
216
|
-
client = new MongoClient(mongoUrl,
|
|
217
|
-
family: 4,
|
|
218
|
-
serverSelectionTimeoutMS: 2e3,
|
|
219
|
-
connectTimeoutMS: 2e3
|
|
220
|
-
});
|
|
302
|
+
client = new MongoClient(mongoUrl, mongoClientOptions);
|
|
221
303
|
await client.connect();
|
|
222
304
|
const dbMatch = { "ns.db": { $regex: `^${escapeRegex(appName)}-.*-db$` } };
|
|
223
305
|
const pipeline = [
|
|
@@ -233,6 +315,19 @@ const registerQueueListener = async (options = {}) => {
|
|
|
233
315
|
fullDocument: "updateLookup",
|
|
234
316
|
...resumeAfter ? { resumeAfter } : {}
|
|
235
317
|
});
|
|
318
|
+
const stoppedPromise = new Promise((resolve) => {
|
|
319
|
+
let settled = false;
|
|
320
|
+
const onAbort = () => settle({ reason: "close" });
|
|
321
|
+
const settle = (result) => {
|
|
322
|
+
if (settled) return;
|
|
323
|
+
settled = true;
|
|
324
|
+
abortController.signal.removeEventListener("abort", onAbort);
|
|
325
|
+
resolve(result);
|
|
326
|
+
};
|
|
327
|
+
abortController.signal.addEventListener("abort", onAbort);
|
|
328
|
+
stream?.once("close", () => settle({ reason: "close" }));
|
|
329
|
+
stream?.once("error", (err) => settle({ reason: "error", error: err }));
|
|
330
|
+
});
|
|
236
331
|
stream.on("change", (change) => {
|
|
237
332
|
const streamRef = stream;
|
|
238
333
|
processing = processing.then(async () => {
|
|
@@ -274,33 +369,88 @@ const registerQueueListener = async (options = {}) => {
|
|
|
274
369
|
});
|
|
275
370
|
});
|
|
276
371
|
stream.on("error", (err) => {
|
|
277
|
-
console.warn("queue listener change stream error", err);
|
|
278
372
|
if (stopped) return;
|
|
279
373
|
if (resumeAfter && isChangeStreamHistoryLost(err)) {
|
|
280
374
|
resumeAfter = null;
|
|
281
375
|
}
|
|
282
376
|
try {
|
|
283
|
-
void stream?.close()
|
|
377
|
+
void Promise.resolve(stream?.close()).catch(() => {
|
|
378
|
+
});
|
|
284
379
|
} catch {
|
|
285
380
|
}
|
|
286
381
|
});
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
382
|
+
return { stoppedPromise };
|
|
383
|
+
};
|
|
384
|
+
const run = async () => {
|
|
385
|
+
let retryCounter = 0;
|
|
386
|
+
while (!stopped) {
|
|
387
|
+
try {
|
|
388
|
+
setStatus({ state: "connecting", attempt: retryCounter + 1 });
|
|
389
|
+
const { stoppedPromise } = await startStream();
|
|
390
|
+
retryCounter = 0;
|
|
391
|
+
setStatus({ state: "ready" });
|
|
392
|
+
resolveReady();
|
|
393
|
+
const end = await stoppedPromise;
|
|
394
|
+
if (stopped) return;
|
|
395
|
+
retryCounter += 1;
|
|
396
|
+
if (maxRetries !== "infinite" && retryCounter > maxRetries) {
|
|
397
|
+
const err2 = end.reason === "error" ? end.error : new Error("queue listener closed");
|
|
398
|
+
setStatus({ state: "failed", attempt: retryCounter, error: err2 });
|
|
399
|
+
if (fatalOnMaxRetries) {
|
|
400
|
+
try {
|
|
401
|
+
options.onFatal?.(err2);
|
|
402
|
+
} catch (fatalErr) {
|
|
403
|
+
console.warn("queue listener onFatal failed", fatalErr);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
rejectReady(err2);
|
|
407
|
+
abortController.abort();
|
|
408
|
+
await closeResources();
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
const delayMs = getRetryDelayMs(retryCounter, retryDelays);
|
|
412
|
+
const err = end.reason === "error" ? end.error : new Error("queue listener closed");
|
|
413
|
+
console.warn("queue listener not ready, retrying in", delayMs, err);
|
|
414
|
+
setStatus({ state: "error", attempt: retryCounter, error: err, nextRetryInMs: delayMs });
|
|
415
|
+
await closeResources();
|
|
416
|
+
await sleep(delayMs, abortController.signal);
|
|
417
|
+
} catch (err) {
|
|
418
|
+
if (stopped) return;
|
|
419
|
+
retryCounter += 1;
|
|
420
|
+
if (maxRetries !== "infinite" && retryCounter > maxRetries) {
|
|
421
|
+
setStatus({ state: "failed", attempt: retryCounter, error: err });
|
|
422
|
+
if (fatalOnMaxRetries) {
|
|
423
|
+
try {
|
|
424
|
+
options.onFatal?.(err);
|
|
425
|
+
} catch (fatalErr) {
|
|
426
|
+
console.warn("queue listener onFatal failed", fatalErr);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
rejectReady(err);
|
|
430
|
+
abortController.abort();
|
|
431
|
+
await closeResources();
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
const delayMs = getRetryDelayMs(retryCounter, retryDelays);
|
|
435
|
+
console.warn("queue listener not ready, retrying in", delayMs, err);
|
|
436
|
+
setStatus({ state: "error", attempt: retryCounter, error: err, nextRetryInMs: delayMs });
|
|
437
|
+
await closeResources();
|
|
438
|
+
await sleep(delayMs, abortController.signal);
|
|
293
439
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
});
|
|
440
|
+
}
|
|
441
|
+
};
|
|
442
|
+
const handle = {
|
|
443
|
+
ready,
|
|
444
|
+
close: close2,
|
|
445
|
+
getStatus: () => status
|
|
301
446
|
};
|
|
302
|
-
|
|
303
|
-
|
|
447
|
+
void run().catch(async (err) => {
|
|
448
|
+
if (stopped) return;
|
|
449
|
+
setStatus({ state: "failed", attempt: 0, error: err });
|
|
450
|
+
rejectReady(err);
|
|
451
|
+
await closeResources();
|
|
452
|
+
});
|
|
453
|
+
return handle;
|
|
304
454
|
};
|
|
305
455
|
const dbEventTaskName = (op, modelName) => `on-${op}-${modelName}`;
|
|
306
456
|
export {
|
package/dist/queueListener.d.ts
CHANGED
|
@@ -1,9 +1,38 @@
|
|
|
1
|
-
|
|
1
|
+
import { MongoClientOptions } from 'mongodb';
|
|
2
|
+
export type QueueListenerRetryDelays = {
|
|
3
|
+
minMs?: number;
|
|
4
|
+
maxMs?: number;
|
|
5
|
+
factor?: number;
|
|
6
|
+
};
|
|
7
|
+
export type QueueListenerStatus = {
|
|
8
|
+
state: "connecting";
|
|
9
|
+
attempt: number;
|
|
10
|
+
} | {
|
|
11
|
+
state: "ready";
|
|
12
|
+
} | {
|
|
13
|
+
state: "error";
|
|
14
|
+
attempt: number;
|
|
15
|
+
error: unknown;
|
|
16
|
+
nextRetryInMs: number;
|
|
17
|
+
} | {
|
|
18
|
+
state: "failed";
|
|
19
|
+
attempt: number;
|
|
20
|
+
error: unknown;
|
|
21
|
+
} | {
|
|
22
|
+
state: "closed";
|
|
23
|
+
};
|
|
24
|
+
export type QueueListenerHandle = {
|
|
25
|
+
ready: Promise<void>;
|
|
2
26
|
close: () => Promise<void>;
|
|
27
|
+
getStatus: () => QueueListenerStatus;
|
|
3
28
|
};
|
|
4
|
-
type QueueListenerOptions = {
|
|
5
|
-
maxRetries?: number;
|
|
29
|
+
export type QueueListenerOptions = {
|
|
30
|
+
maxRetries?: number | "infinite";
|
|
31
|
+
fatalOnMaxRetries?: boolean;
|
|
32
|
+
onStateChange?: (status: QueueListenerStatus) => void;
|
|
33
|
+
onFatal?: (err: unknown) => void;
|
|
34
|
+
retryDelays?: QueueListenerRetryDelays;
|
|
35
|
+
mongoClientOptions?: MongoClientOptions;
|
|
6
36
|
};
|
|
7
|
-
export declare const registerQueueListener: (options?: QueueListenerOptions) => Promise<
|
|
8
|
-
export {};
|
|
37
|
+
export declare const registerQueueListener: (options?: QueueListenerOptions) => Promise<QueueListenerHandle>;
|
|
9
38
|
//# sourceMappingURL=queueListener.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"queueListener.d.ts","sourceRoot":"","sources":["../src/queueListener.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"queueListener.d.ts","sourceRoot":"","sources":["../src/queueListener.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiD,KAAK,kBAAkB,EAAE,MAAM,SAAS,CAAA;AAMhG,MAAM,MAAM,wBAAwB,GAAG;IACrC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB,CAAA;AAED,MAAM,MAAM,mBAAmB,GAC3B;IAAE,KAAK,EAAE,YAAY,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACxC;IAAE,KAAK,EAAE,OAAO,CAAA;CAAE,GAClB;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAC;IAAC,aAAa,EAAE,MAAM,CAAA;CAAE,GAC1E;IAAE,KAAK,EAAE,QAAQ,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,GACpD;IAAE,KAAK,EAAE,QAAQ,CAAA;CAAE,CAAA;AAEvB,MAAM,MAAM,mBAAmB,GAAG;IAChC,KAAK,EAAE,OAAO,CAAC,IAAI,CAAC,CAAA;IACpB,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAC1B,SAAS,EAAE,MAAM,mBAAmB,CAAA;CACrC,CAAA;AAED,MAAM,MAAM,oBAAoB,GAAG;IACjC,UAAU,CAAC,EAAE,MAAM,GAAG,UAAU,CAAA;IAChC,iBAAiB,CAAC,EAAE,OAAO,CAAA;IAC3B,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE,mBAAmB,KAAK,IAAI,CAAA;IACrD,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAA;IAChC,WAAW,CAAC,EAAE,wBAAwB,CAAA;IACtC,kBAAkB,CAAC,EAAE,kBAAkB,CAAA;CACxC,CAAA;AAuID,eAAO,MAAM,qBAAqB,GAAU,UAAS,oBAAyB,KAAG,OAAO,CAAC,mBAAmB,CA4T3G,CAAA"}
|