@rpcbase/worker 0.41.0 → 0.43.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 +477 -565
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2,592 +2,504 @@ import domain from "node:domain";
|
|
|
2
2
|
import { Queue, Worker } from "bullmq";
|
|
3
3
|
import { MongoClient } from "mongodb";
|
|
4
4
|
import mongoose from "mongoose";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
return redisUrl;
|
|
5
|
+
//#region src/queue.ts
|
|
6
|
+
var DEFAULT_QUEUE_NAME = "rb-queue-default";
|
|
7
|
+
var DEFAULT_CONCURRENCY = 2;
|
|
8
|
+
var DEFAULT_RENTENTION = 128;
|
|
9
|
+
var tasksByName = Object.create(null);
|
|
10
|
+
var queueInstance = null;
|
|
11
|
+
var workerInstance = null;
|
|
12
|
+
var hasStarted = false;
|
|
13
|
+
var getRedisUrl = () => {
|
|
14
|
+
const redisUrl = process.env.REDIS_URL?.trim();
|
|
15
|
+
if (!redisUrl) throw new Error("Missing REDIS_URL (required for @rpcbase/worker queue)");
|
|
16
|
+
return redisUrl;
|
|
18
17
|
};
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
18
|
+
var getQueueName = () => process.env.RB_QUEUE_NAME?.trim() || DEFAULT_QUEUE_NAME;
|
|
19
|
+
var getConcurrency = () => {
|
|
20
|
+
const raw = process.env.RB_QUEUE_CONCURRENCY?.trim();
|
|
21
|
+
const value = raw ? Number(raw) : DEFAULT_CONCURRENCY;
|
|
22
|
+
if (!Number.isFinite(value) || value <= 0) return DEFAULT_CONCURRENCY;
|
|
23
|
+
return Math.floor(value);
|
|
25
24
|
};
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
if (error instanceof Error) return error;
|
|
31
|
-
const message = typeof error === "string" ? error : String(error ?? "unknown error");
|
|
32
|
-
return new Error(message);
|
|
25
|
+
var getConnection = () => ({ url: getRedisUrl() });
|
|
26
|
+
var toError = (error) => {
|
|
27
|
+
if (error instanceof Error) return error;
|
|
28
|
+
return new Error(typeof error === "string" ? error : String(error ?? "unknown error"));
|
|
33
29
|
};
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
});
|
|
30
|
+
var runTaskHandler = async (job, handler) => {
|
|
31
|
+
return await new Promise((resolve, reject) => {
|
|
32
|
+
const taskDomain = domain.create();
|
|
33
|
+
let settled = false;
|
|
34
|
+
const settle = (next, value) => {
|
|
35
|
+
if (settled) return;
|
|
36
|
+
settled = true;
|
|
37
|
+
taskDomain.removeAllListeners();
|
|
38
|
+
next(value);
|
|
39
|
+
};
|
|
40
|
+
taskDomain.on("error", (error) => {
|
|
41
|
+
settle(reject, toError(error));
|
|
42
|
+
});
|
|
43
|
+
taskDomain.run(() => {
|
|
44
|
+
Promise.resolve(handler(job.data, job)).then((result) => settle(resolve, result), (error) => settle(reject, toError(error)));
|
|
45
|
+
});
|
|
46
|
+
});
|
|
52
47
|
};
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
48
|
+
var ensureQueue = () => {
|
|
49
|
+
if (queueInstance) return queueInstance;
|
|
50
|
+
queueInstance = new Queue(getQueueName(), {
|
|
51
|
+
connection: getConnection(),
|
|
52
|
+
defaultJobOptions: {
|
|
53
|
+
removeOnComplete: DEFAULT_RENTENTION,
|
|
54
|
+
removeOnFail: DEFAULT_RENTENTION
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
return queueInstance;
|
|
63
58
|
};
|
|
64
59
|
function registerTask(name, handler) {
|
|
65
|
-
|
|
60
|
+
tasksByName[name] = handler;
|
|
66
61
|
}
|
|
67
|
-
|
|
62
|
+
var getTasks = () => tasksByName;
|
|
68
63
|
function add(taskName, payload, options) {
|
|
69
|
-
|
|
64
|
+
return ensureQueue().add(taskName, payload, options);
|
|
70
65
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
console.log(`job ${job?.id ?? "unknown"} failed`, err);
|
|
108
|
-
});
|
|
109
|
-
await workerInstance.waitUntilReady();
|
|
110
|
-
return queue;
|
|
66
|
+
var getJob = async (jobId) => await ensureQueue().getJob(jobId) ?? null;
|
|
67
|
+
var getJobs = async (...args) => ensureQueue().getJobs(...args);
|
|
68
|
+
var getInstance = () => ensureQueue();
|
|
69
|
+
var start = async () => {
|
|
70
|
+
if (hasStarted) return ensureQueue();
|
|
71
|
+
hasStarted = true;
|
|
72
|
+
const queue = ensureQueue();
|
|
73
|
+
const concurrency = getConcurrency();
|
|
74
|
+
console.log("start worker queue", {
|
|
75
|
+
queue: queue.name,
|
|
76
|
+
redisUrl: getRedisUrl(),
|
|
77
|
+
concurrency
|
|
78
|
+
});
|
|
79
|
+
queue.on("error", (err) => {
|
|
80
|
+
console.log(`queue error: ${err.message}`);
|
|
81
|
+
});
|
|
82
|
+
if (!workerInstance) workerInstance = new Worker(queue.name, async (job) => {
|
|
83
|
+
const taskName = job.name;
|
|
84
|
+
const handler = tasksByName[taskName];
|
|
85
|
+
if (!handler) throw new Error(`No task registered for '${taskName}'`);
|
|
86
|
+
return await runTaskHandler(job, handler);
|
|
87
|
+
}, {
|
|
88
|
+
concurrency,
|
|
89
|
+
connection: getConnection()
|
|
90
|
+
});
|
|
91
|
+
workerInstance.on("error", (err) => {
|
|
92
|
+
console.log(`worker error: ${err.message}`);
|
|
93
|
+
});
|
|
94
|
+
workerInstance.on("stalled", (jobId) => {
|
|
95
|
+
console.log(`job ${jobId} stalled`);
|
|
96
|
+
});
|
|
97
|
+
workerInstance.on("failed", (job, err) => {
|
|
98
|
+
console.log(`job ${job?.id ?? "unknown"} failed`, err);
|
|
99
|
+
});
|
|
100
|
+
await workerInstance.waitUntilReady();
|
|
101
|
+
return queue;
|
|
111
102
|
};
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
103
|
+
var close = async () => {
|
|
104
|
+
if (!queueInstance && !workerInstance) return;
|
|
105
|
+
try {
|
|
106
|
+
await workerInstance?.close();
|
|
107
|
+
await queueInstance?.close();
|
|
108
|
+
} finally {
|
|
109
|
+
workerInstance = null;
|
|
110
|
+
queueInstance = null;
|
|
111
|
+
hasStarted = false;
|
|
112
|
+
}
|
|
122
113
|
};
|
|
123
|
-
|
|
114
|
+
var getUrl = () => getRedisUrl();
|
|
124
115
|
async function scheduleTask(taskName, payload, options) {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
...rest,
|
|
132
|
-
jobId: jobId ?? `schedule|${taskName}`,
|
|
133
|
-
repeat
|
|
134
|
-
});
|
|
116
|
+
const { jobId, repeat, ...rest } = options;
|
|
117
|
+
return add(taskName, payload, {
|
|
118
|
+
...rest,
|
|
119
|
+
jobId: jobId ?? `schedule|${taskName}`,
|
|
120
|
+
repeat
|
|
121
|
+
});
|
|
135
122
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
123
|
+
var queueApi = {
|
|
124
|
+
start,
|
|
125
|
+
close,
|
|
126
|
+
registerTask,
|
|
127
|
+
getTasks,
|
|
128
|
+
add,
|
|
129
|
+
scheduleTask,
|
|
130
|
+
getJob,
|
|
131
|
+
getJobs,
|
|
132
|
+
getInstance,
|
|
133
|
+
getUrl
|
|
147
134
|
};
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
135
|
+
//#endregion
|
|
136
|
+
//#region src/queueListener.ts
|
|
137
|
+
var RETRY_MAXIMUM_DELAY_MS = 3e3;
|
|
138
|
+
var RETRY_MINIMUM_DELAY_MS = 50;
|
|
139
|
+
var RETRY_DEFAULT_FACTOR = 2;
|
|
140
|
+
var sleep = async (ms, signal) => new Promise((resolve) => {
|
|
141
|
+
if (signal?.aborted) {
|
|
142
|
+
resolve();
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
let timeout = null;
|
|
146
|
+
const cleanup = () => {
|
|
147
|
+
if (timeout) clearTimeout(timeout);
|
|
148
|
+
signal?.removeEventListener("abort", onAbort);
|
|
149
|
+
};
|
|
150
|
+
const onAbort = () => {
|
|
151
|
+
cleanup();
|
|
152
|
+
resolve();
|
|
153
|
+
};
|
|
154
|
+
timeout = setTimeout(() => {
|
|
155
|
+
cleanup();
|
|
156
|
+
resolve();
|
|
157
|
+
}, ms);
|
|
158
|
+
timeout.unref?.();
|
|
159
|
+
signal?.addEventListener("abort", onAbort);
|
|
171
160
|
});
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
return `mongodb://${host}:${port}`;
|
|
179
|
-
};
|
|
180
|
-
const escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
181
|
-
const isRecord = (value) => typeof value === "object" && value !== null;
|
|
182
|
-
const getDocumentId = (doc) => {
|
|
183
|
-
if (!isRecord(doc)) return void 0;
|
|
184
|
-
return doc._id;
|
|
161
|
+
var getMongoUrl = () => {
|
|
162
|
+
const explicit = process.env.MONGODB_URL ?? process.env.MONGO_URL ?? process.env.MONGODB_URI ?? process.env.DB_URL;
|
|
163
|
+
if (explicit && explicit.trim()) return explicit.trim();
|
|
164
|
+
const port = process.env.DB_PORT?.trim();
|
|
165
|
+
if (!port) throw new Error("Missing Mongo connection details (MONGODB_URL/MONGO_URL/MONGODB_URI/DB_URL/DB_PORT)");
|
|
166
|
+
return `mongodb://${process.env.DB_HOST?.trim() || "localhost"}:${port}`;
|
|
185
167
|
};
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
if (collectionName === collName) {
|
|
192
|
-
return modelName;
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
return null;
|
|
168
|
+
var escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
169
|
+
var isRecord = (value) => typeof value === "object" && value !== null;
|
|
170
|
+
var getDocumentId = (doc) => {
|
|
171
|
+
if (!isRecord(doc)) return void 0;
|
|
172
|
+
return doc._id;
|
|
196
173
|
};
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
}
|
|
205
|
-
return updateDescription;
|
|
174
|
+
var resolveModelNameFromCollection = (collName) => {
|
|
175
|
+
const models = mongoose.models;
|
|
176
|
+
for (const modelName of Object.keys(models)) {
|
|
177
|
+
const model = models[modelName];
|
|
178
|
+
if ((model.collection?.collectionName ?? model.collection?.name) === collName) return modelName;
|
|
179
|
+
}
|
|
180
|
+
return null;
|
|
206
181
|
};
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
182
|
+
var normalizeUpdateDescription = (updateDescription) => {
|
|
183
|
+
if (!isRecord(updateDescription)) return updateDescription;
|
|
184
|
+
if (isRecord(updateDescription.updatedFields)) return {
|
|
185
|
+
...updateDescription,
|
|
186
|
+
updatedFieldsKeys: Object.keys(updateDescription.updatedFields)
|
|
187
|
+
};
|
|
188
|
+
return updateDescription;
|
|
213
189
|
};
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
doc,
|
|
220
|
-
updateDescription
|
|
221
|
-
}) => {
|
|
222
|
-
const tasks = queueApi.getTasks();
|
|
223
|
-
const taskOp = normalizeOpForTaskName(op);
|
|
224
|
-
const handlerName = `on-${taskOp}-${modelName}`;
|
|
225
|
-
if (!tasks[handlerName]) return;
|
|
226
|
-
const normalizedUpdateDescription = normalizeUpdateDescription(updateDescription);
|
|
227
|
-
await queueApi.add(handlerName, {
|
|
228
|
-
tenantId,
|
|
229
|
-
doc,
|
|
230
|
-
updateDescription: normalizedUpdateDescription
|
|
231
|
-
}, {
|
|
232
|
-
jobId: `${dbName}|${taskOp}-${getDocumentId(doc) ?? "unknown"}`,
|
|
233
|
-
removeOnComplete: true,
|
|
234
|
-
removeOnFail: true
|
|
235
|
-
});
|
|
190
|
+
var normalizeOpForTaskName = (op) => op === "replace" ? "update" : op;
|
|
191
|
+
var getTenantIdFromDbName = (dbName, appName) => {
|
|
192
|
+
const escapedAppName = escapeRegex(appName);
|
|
193
|
+
const tenantId = dbName.match(new RegExp(`^${escapedAppName}-(.+)-db$`))?.[1]?.trim();
|
|
194
|
+
return tenantId ? tenantId : null;
|
|
236
195
|
};
|
|
237
|
-
|
|
238
|
-
const
|
|
239
|
-
const
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
196
|
+
var dispatchWorkerQueue = async ({ dbName, tenantId, modelName, op, doc, updateDescription }) => {
|
|
197
|
+
const tasks = queueApi.getTasks();
|
|
198
|
+
const taskOp = normalizeOpForTaskName(op);
|
|
199
|
+
const handlerName = `on-${taskOp}-${modelName}`;
|
|
200
|
+
if (!tasks[handlerName]) return;
|
|
201
|
+
const normalizedUpdateDescription = normalizeUpdateDescription(updateDescription);
|
|
202
|
+
await queueApi.add(handlerName, {
|
|
203
|
+
tenantId,
|
|
204
|
+
doc,
|
|
205
|
+
updateDescription: normalizedUpdateDescription
|
|
206
|
+
}, {
|
|
207
|
+
jobId: `${dbName}|${taskOp}-${getDocumentId(doc) ?? "unknown"}`,
|
|
208
|
+
removeOnComplete: true,
|
|
209
|
+
removeOnFail: true
|
|
210
|
+
});
|
|
250
211
|
};
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
212
|
+
var shouldSkipCollection = (collName) => collName.endsWith(".files") || collName.endsWith(".chunks");
|
|
213
|
+
var INTERNAL_IGNORED_MODEL_NAMES = new Set(["RBRtsChange", "RBRtsCounter"]);
|
|
214
|
+
var INTERNAL_IGNORED_COLLECTION_NAMES = new Set(["rtschanges", "rtscounters"]);
|
|
215
|
+
var normalizeRetryDelays = (input) => {
|
|
216
|
+
const minMs = Math.max(0, Math.floor(input?.minMs ?? RETRY_MINIMUM_DELAY_MS));
|
|
217
|
+
const rawMaxMs = Math.max(0, Math.floor(input?.maxMs ?? RETRY_MAXIMUM_DELAY_MS));
|
|
218
|
+
return {
|
|
219
|
+
minMs,
|
|
220
|
+
maxMs: Math.max(minMs, rawMaxMs),
|
|
221
|
+
factor: Math.max(1, Number.isFinite(input?.factor) ? input?.factor ?? RETRY_DEFAULT_FACTOR : RETRY_DEFAULT_FACTOR)
|
|
222
|
+
};
|
|
256
223
|
};
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
if (!appName) {
|
|
263
|
-
throw new Error("Missing APP_NAME (required to configure the worker DB change listener)");
|
|
264
|
-
}
|
|
265
|
-
const mongoUrl = getMongoUrl();
|
|
266
|
-
const mongoClientOptions = {
|
|
267
|
-
family: 4,
|
|
268
|
-
serverSelectionTimeoutMS: 2e3,
|
|
269
|
-
connectTimeoutMS: 2e3,
|
|
270
|
-
...options.mongoClientOptions
|
|
271
|
-
};
|
|
272
|
-
let stopped = false;
|
|
273
|
-
const abortController = new AbortController();
|
|
274
|
-
let client = null;
|
|
275
|
-
let stream = null;
|
|
276
|
-
let resumeAfter = null;
|
|
277
|
-
let processing = Promise.resolve();
|
|
278
|
-
let status = {
|
|
279
|
-
state: "connecting",
|
|
280
|
-
attempt: 1
|
|
281
|
-
};
|
|
282
|
-
const setStatus = (next) => {
|
|
283
|
-
status = next;
|
|
284
|
-
try {
|
|
285
|
-
options.onStateChange?.(next);
|
|
286
|
-
} catch (err) {
|
|
287
|
-
console.warn("queue listener onStateChange failed", err);
|
|
288
|
-
}
|
|
289
|
-
};
|
|
290
|
-
let readySettled = false;
|
|
291
|
-
let readyResolve = null;
|
|
292
|
-
let readyReject = null;
|
|
293
|
-
const ready = new Promise((resolve, reject) => {
|
|
294
|
-
readyResolve = resolve;
|
|
295
|
-
readyReject = reject;
|
|
296
|
-
});
|
|
297
|
-
const resolveReady = () => {
|
|
298
|
-
if (readySettled) return;
|
|
299
|
-
readySettled = true;
|
|
300
|
-
readyResolve?.();
|
|
301
|
-
};
|
|
302
|
-
const rejectReady = (err) => {
|
|
303
|
-
if (readySettled) return;
|
|
304
|
-
readySettled = true;
|
|
305
|
-
readyReject?.(err);
|
|
306
|
-
};
|
|
307
|
-
const isChangeStreamHistoryLost = (err) => {
|
|
308
|
-
const maybeErr = err;
|
|
309
|
-
const code = typeof maybeErr.code === "number" ? maybeErr.code : null;
|
|
310
|
-
const codeName = typeof maybeErr.codeName === "string" ? maybeErr.codeName : "";
|
|
311
|
-
const message = err instanceof Error ? err.message : String(err ?? "");
|
|
312
|
-
return code === 286 || codeName === "ChangeStreamHistoryLost" || message.includes("ChangeStreamHistoryLost") || message.includes("resume token") || message.includes("Resume token") || message.includes("resume of change stream was not possible") || message.includes("cannot resume");
|
|
313
|
-
};
|
|
314
|
-
const close2 = async () => {
|
|
315
|
-
stopped = true;
|
|
316
|
-
abortController.abort();
|
|
317
|
-
try {
|
|
318
|
-
stream?.removeAllListeners();
|
|
319
|
-
await stream?.close();
|
|
320
|
-
} catch {
|
|
321
|
-
}
|
|
322
|
-
stream = null;
|
|
323
|
-
try {
|
|
324
|
-
await client?.close();
|
|
325
|
-
} catch {
|
|
326
|
-
}
|
|
327
|
-
client = null;
|
|
328
|
-
if (!readySettled) {
|
|
329
|
-
rejectReady(new Error("queue listener closed before ready"));
|
|
330
|
-
}
|
|
331
|
-
setStatus({
|
|
332
|
-
state: "closed"
|
|
333
|
-
});
|
|
334
|
-
};
|
|
335
|
-
const closeResources = async () => {
|
|
336
|
-
try {
|
|
337
|
-
stream?.removeAllListeners();
|
|
338
|
-
await stream?.close();
|
|
339
|
-
} catch {
|
|
340
|
-
}
|
|
341
|
-
stream = null;
|
|
342
|
-
try {
|
|
343
|
-
await client?.close();
|
|
344
|
-
} catch {
|
|
345
|
-
}
|
|
346
|
-
client = null;
|
|
347
|
-
};
|
|
348
|
-
const startStream = async () => {
|
|
349
|
-
if (stopped) return {
|
|
350
|
-
stoppedPromise: Promise.resolve({
|
|
351
|
-
reason: "close"
|
|
352
|
-
})
|
|
353
|
-
};
|
|
354
|
-
if (stream) {
|
|
355
|
-
try {
|
|
356
|
-
stream.removeAllListeners();
|
|
357
|
-
await stream.close();
|
|
358
|
-
} catch {
|
|
359
|
-
}
|
|
360
|
-
stream = null;
|
|
361
|
-
}
|
|
362
|
-
if (client) {
|
|
363
|
-
try {
|
|
364
|
-
await client.close();
|
|
365
|
-
} catch {
|
|
366
|
-
}
|
|
367
|
-
client = null;
|
|
368
|
-
}
|
|
369
|
-
client = new MongoClient(mongoUrl, mongoClientOptions);
|
|
370
|
-
await client.connect();
|
|
371
|
-
const dbMatch = {
|
|
372
|
-
"ns.db": {
|
|
373
|
-
$regex: `^${escapeRegex(appName)}-.*-db$`
|
|
374
|
-
}
|
|
375
|
-
};
|
|
376
|
-
const pipeline = [{
|
|
377
|
-
$match: dbMatch
|
|
378
|
-
}, {
|
|
379
|
-
$match: {
|
|
380
|
-
operationType: {
|
|
381
|
-
$in: ["insert", "update", "replace", "delete"]
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
}, {
|
|
385
|
-
$match: {
|
|
386
|
-
"ns.coll": {
|
|
387
|
-
$nin: Array.from(INTERNAL_IGNORED_COLLECTION_NAMES)
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
}];
|
|
391
|
-
stream = client.watch(pipeline, {
|
|
392
|
-
fullDocument: "updateLookup",
|
|
393
|
-
...resumeAfter ? {
|
|
394
|
-
resumeAfter
|
|
395
|
-
} : {}
|
|
396
|
-
});
|
|
397
|
-
const stoppedPromise = new Promise((resolve) => {
|
|
398
|
-
let settled = false;
|
|
399
|
-
const onAbort = () => settle({
|
|
400
|
-
reason: "close"
|
|
401
|
-
});
|
|
402
|
-
const settle = (result) => {
|
|
403
|
-
if (settled) return;
|
|
404
|
-
settled = true;
|
|
405
|
-
abortController.signal.removeEventListener("abort", onAbort);
|
|
406
|
-
resolve(result);
|
|
407
|
-
};
|
|
408
|
-
abortController.signal.addEventListener("abort", onAbort);
|
|
409
|
-
stream?.once("close", () => settle({
|
|
410
|
-
reason: "close"
|
|
411
|
-
}));
|
|
412
|
-
stream?.once("error", (err) => settle({
|
|
413
|
-
reason: "error",
|
|
414
|
-
error: err
|
|
415
|
-
}));
|
|
416
|
-
});
|
|
417
|
-
stream.on("change", (change) => {
|
|
418
|
-
const streamRef = stream;
|
|
419
|
-
processing = processing.then(async () => {
|
|
420
|
-
const ns = "ns" in change ? change.ns : void 0;
|
|
421
|
-
const dbName = String(ns?.db ?? "");
|
|
422
|
-
if (!dbName) return;
|
|
423
|
-
const tenantId = getTenantIdFromDbName(dbName, appName);
|
|
424
|
-
if (!tenantId) return;
|
|
425
|
-
const collName = String(ns && "coll" in ns ? ns.coll : "");
|
|
426
|
-
if (!collName) return;
|
|
427
|
-
if (shouldSkipCollection(collName)) return;
|
|
428
|
-
if (INTERNAL_IGNORED_COLLECTION_NAMES.has(collName)) return;
|
|
429
|
-
const modelName = resolveModelNameFromCollection(collName);
|
|
430
|
-
if (!modelName) return;
|
|
431
|
-
if (INTERNAL_IGNORED_MODEL_NAMES.has(modelName)) return;
|
|
432
|
-
const op = String(change.operationType ?? "");
|
|
433
|
-
if (!op) return;
|
|
434
|
-
const normalizedOp = normalizeOpForTaskName(op);
|
|
435
|
-
let doc = "fullDocument" in change ? change.fullDocument : void 0;
|
|
436
|
-
if (!doc && normalizedOp === "delete") {
|
|
437
|
-
doc = "documentKey" in change ? change.documentKey : void 0;
|
|
438
|
-
}
|
|
439
|
-
if (!doc) return;
|
|
440
|
-
const updateDescription = "updateDescription" in change ? change.updateDescription : void 0;
|
|
441
|
-
try {
|
|
442
|
-
await dispatchWorkerQueue({
|
|
443
|
-
dbName,
|
|
444
|
-
tenantId,
|
|
445
|
-
modelName,
|
|
446
|
-
op: normalizedOp,
|
|
447
|
-
doc,
|
|
448
|
-
updateDescription
|
|
449
|
-
});
|
|
450
|
-
resumeAfter = change?._id ?? resumeAfter;
|
|
451
|
-
} catch (err) {
|
|
452
|
-
console.warn("queue listener failed to dispatch change", err);
|
|
453
|
-
try {
|
|
454
|
-
await streamRef?.close();
|
|
455
|
-
} catch {
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
}).catch((err) => {
|
|
459
|
-
console.warn("queue listener change handler failed", err);
|
|
460
|
-
});
|
|
461
|
-
});
|
|
462
|
-
stream.on("error", (err) => {
|
|
463
|
-
if (stopped) return;
|
|
464
|
-
if (resumeAfter && isChangeStreamHistoryLost(err)) {
|
|
465
|
-
resumeAfter = null;
|
|
466
|
-
}
|
|
467
|
-
try {
|
|
468
|
-
void Promise.resolve(stream?.close()).catch(() => {
|
|
469
|
-
});
|
|
470
|
-
} catch {
|
|
471
|
-
}
|
|
472
|
-
});
|
|
473
|
-
return {
|
|
474
|
-
stoppedPromise
|
|
475
|
-
};
|
|
476
|
-
};
|
|
477
|
-
const run = async () => {
|
|
478
|
-
let retryCounter = 0;
|
|
479
|
-
while (!stopped) {
|
|
480
|
-
try {
|
|
481
|
-
setStatus({
|
|
482
|
-
state: "connecting",
|
|
483
|
-
attempt: retryCounter + 1
|
|
484
|
-
});
|
|
485
|
-
const {
|
|
486
|
-
stoppedPromise
|
|
487
|
-
} = await startStream();
|
|
488
|
-
retryCounter = 0;
|
|
489
|
-
setStatus({
|
|
490
|
-
state: "ready"
|
|
491
|
-
});
|
|
492
|
-
resolveReady();
|
|
493
|
-
const end = await stoppedPromise;
|
|
494
|
-
if (stopped) return;
|
|
495
|
-
retryCounter += 1;
|
|
496
|
-
if (maxRetries !== "infinite" && retryCounter > maxRetries) {
|
|
497
|
-
const err2 = end.reason === "error" ? end.error : new Error("queue listener closed");
|
|
498
|
-
setStatus({
|
|
499
|
-
state: "failed",
|
|
500
|
-
attempt: retryCounter,
|
|
501
|
-
error: err2
|
|
502
|
-
});
|
|
503
|
-
if (fatalOnMaxRetries) {
|
|
504
|
-
try {
|
|
505
|
-
options.onFatal?.(err2);
|
|
506
|
-
} catch (fatalErr) {
|
|
507
|
-
console.warn("queue listener onFatal failed", fatalErr);
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
rejectReady(err2);
|
|
511
|
-
abortController.abort();
|
|
512
|
-
await closeResources();
|
|
513
|
-
return;
|
|
514
|
-
}
|
|
515
|
-
const delayMs = getRetryDelayMs(retryCounter, retryDelays);
|
|
516
|
-
const err = end.reason === "error" ? end.error : new Error("queue listener closed");
|
|
517
|
-
console.warn("queue listener not ready, retrying in", delayMs, err);
|
|
518
|
-
setStatus({
|
|
519
|
-
state: "error",
|
|
520
|
-
attempt: retryCounter,
|
|
521
|
-
error: err,
|
|
522
|
-
nextRetryInMs: delayMs
|
|
523
|
-
});
|
|
524
|
-
await closeResources();
|
|
525
|
-
await sleep(delayMs, abortController.signal);
|
|
526
|
-
} catch (err) {
|
|
527
|
-
if (stopped) return;
|
|
528
|
-
retryCounter += 1;
|
|
529
|
-
if (maxRetries !== "infinite" && retryCounter > maxRetries) {
|
|
530
|
-
setStatus({
|
|
531
|
-
state: "failed",
|
|
532
|
-
attempt: retryCounter,
|
|
533
|
-
error: err
|
|
534
|
-
});
|
|
535
|
-
if (fatalOnMaxRetries) {
|
|
536
|
-
try {
|
|
537
|
-
options.onFatal?.(err);
|
|
538
|
-
} catch (fatalErr) {
|
|
539
|
-
console.warn("queue listener onFatal failed", fatalErr);
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
rejectReady(err);
|
|
543
|
-
abortController.abort();
|
|
544
|
-
await closeResources();
|
|
545
|
-
return;
|
|
546
|
-
}
|
|
547
|
-
const delayMs = getRetryDelayMs(retryCounter, retryDelays);
|
|
548
|
-
console.warn("queue listener not ready, retrying in", delayMs, err);
|
|
549
|
-
setStatus({
|
|
550
|
-
state: "error",
|
|
551
|
-
attempt: retryCounter,
|
|
552
|
-
error: err,
|
|
553
|
-
nextRetryInMs: delayMs
|
|
554
|
-
});
|
|
555
|
-
await closeResources();
|
|
556
|
-
await sleep(delayMs, abortController.signal);
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
};
|
|
560
|
-
const handle = {
|
|
561
|
-
ready,
|
|
562
|
-
close: close2,
|
|
563
|
-
getStatus: () => status
|
|
564
|
-
};
|
|
565
|
-
void run().catch(async (err) => {
|
|
566
|
-
if (stopped) return;
|
|
567
|
-
setStatus({
|
|
568
|
-
state: "failed",
|
|
569
|
-
attempt: 0,
|
|
570
|
-
error: err
|
|
571
|
-
});
|
|
572
|
-
rejectReady(err);
|
|
573
|
-
await closeResources();
|
|
574
|
-
});
|
|
575
|
-
return handle;
|
|
224
|
+
var getRetryDelayMs = (attempt, delays) => Math.min(delays.maxMs, Math.round(delays.minMs * Math.pow(delays.factor, Math.max(0, attempt - 1))));
|
|
225
|
+
var normalizeMaxRetries = (value) => {
|
|
226
|
+
if (value === "infinite") return value;
|
|
227
|
+
const parsed = Math.floor(value);
|
|
228
|
+
return Number.isFinite(parsed) ? Math.max(0, parsed) : 0;
|
|
576
229
|
};
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
230
|
+
var registerQueueListener = async (options = {}) => {
|
|
231
|
+
const maxRetries = normalizeMaxRetries(options.maxRetries ?? "infinite");
|
|
232
|
+
const fatalOnMaxRetries = options.fatalOnMaxRetries ?? false;
|
|
233
|
+
const retryDelays = normalizeRetryDelays(options.retryDelays);
|
|
234
|
+
const appName = process.env.APP_NAME?.trim();
|
|
235
|
+
if (!appName) throw new Error("Missing APP_NAME (required to configure the worker DB change listener)");
|
|
236
|
+
const mongoUrl = getMongoUrl();
|
|
237
|
+
const mongoClientOptions = {
|
|
238
|
+
family: 4,
|
|
239
|
+
serverSelectionTimeoutMS: 2e3,
|
|
240
|
+
connectTimeoutMS: 2e3,
|
|
241
|
+
...options.mongoClientOptions
|
|
242
|
+
};
|
|
243
|
+
let stopped = false;
|
|
244
|
+
const abortController = new AbortController();
|
|
245
|
+
let client = null;
|
|
246
|
+
let stream = null;
|
|
247
|
+
let resumeAfter = null;
|
|
248
|
+
let processing = Promise.resolve();
|
|
249
|
+
let status = {
|
|
250
|
+
state: "connecting",
|
|
251
|
+
attempt: 1
|
|
252
|
+
};
|
|
253
|
+
const setStatus = (next) => {
|
|
254
|
+
status = next;
|
|
255
|
+
try {
|
|
256
|
+
options.onStateChange?.(next);
|
|
257
|
+
} catch (err) {
|
|
258
|
+
console.warn("queue listener onStateChange failed", err);
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
let readySettled = false;
|
|
262
|
+
let readyResolve = null;
|
|
263
|
+
let readyReject = null;
|
|
264
|
+
const ready = new Promise((resolve, reject) => {
|
|
265
|
+
readyResolve = resolve;
|
|
266
|
+
readyReject = reject;
|
|
267
|
+
});
|
|
268
|
+
const resolveReady = () => {
|
|
269
|
+
if (readySettled) return;
|
|
270
|
+
readySettled = true;
|
|
271
|
+
readyResolve?.();
|
|
272
|
+
};
|
|
273
|
+
const rejectReady = (err) => {
|
|
274
|
+
if (readySettled) return;
|
|
275
|
+
readySettled = true;
|
|
276
|
+
readyReject?.(err);
|
|
277
|
+
};
|
|
278
|
+
const isChangeStreamHistoryLost = (err) => {
|
|
279
|
+
const maybeErr = err;
|
|
280
|
+
const code = typeof maybeErr.code === "number" ? maybeErr.code : null;
|
|
281
|
+
const codeName = typeof maybeErr.codeName === "string" ? maybeErr.codeName : "";
|
|
282
|
+
const message = err instanceof Error ? err.message : String(err ?? "");
|
|
283
|
+
return code === 286 || codeName === "ChangeStreamHistoryLost" || message.includes("ChangeStreamHistoryLost") || message.includes("resume token") || message.includes("Resume token") || message.includes("resume of change stream was not possible") || message.includes("cannot resume");
|
|
284
|
+
};
|
|
285
|
+
const close = async () => {
|
|
286
|
+
stopped = true;
|
|
287
|
+
abortController.abort();
|
|
288
|
+
try {
|
|
289
|
+
stream?.removeAllListeners();
|
|
290
|
+
await stream?.close();
|
|
291
|
+
} catch {}
|
|
292
|
+
stream = null;
|
|
293
|
+
try {
|
|
294
|
+
await client?.close();
|
|
295
|
+
} catch {}
|
|
296
|
+
client = null;
|
|
297
|
+
if (!readySettled) rejectReady(/* @__PURE__ */ new Error("queue listener closed before ready"));
|
|
298
|
+
setStatus({ state: "closed" });
|
|
299
|
+
};
|
|
300
|
+
const closeResources = async () => {
|
|
301
|
+
try {
|
|
302
|
+
stream?.removeAllListeners();
|
|
303
|
+
await stream?.close();
|
|
304
|
+
} catch {}
|
|
305
|
+
stream = null;
|
|
306
|
+
try {
|
|
307
|
+
await client?.close();
|
|
308
|
+
} catch {}
|
|
309
|
+
client = null;
|
|
310
|
+
};
|
|
311
|
+
const startStream = async () => {
|
|
312
|
+
if (stopped) return { stoppedPromise: Promise.resolve({ reason: "close" }) };
|
|
313
|
+
if (stream) {
|
|
314
|
+
try {
|
|
315
|
+
stream.removeAllListeners();
|
|
316
|
+
await stream.close();
|
|
317
|
+
} catch {}
|
|
318
|
+
stream = null;
|
|
319
|
+
}
|
|
320
|
+
if (client) {
|
|
321
|
+
try {
|
|
322
|
+
await client.close();
|
|
323
|
+
} catch {}
|
|
324
|
+
client = null;
|
|
325
|
+
}
|
|
326
|
+
client = new MongoClient(mongoUrl, mongoClientOptions);
|
|
327
|
+
await client.connect();
|
|
328
|
+
const pipeline = [
|
|
329
|
+
{ $match: { "ns.db": { $regex: `^${escapeRegex(appName)}-.*-db$` } } },
|
|
330
|
+
{ $match: { operationType: { $in: [
|
|
331
|
+
"insert",
|
|
332
|
+
"update",
|
|
333
|
+
"replace",
|
|
334
|
+
"delete"
|
|
335
|
+
] } } },
|
|
336
|
+
{ $match: { "ns.coll": { $nin: Array.from(INTERNAL_IGNORED_COLLECTION_NAMES) } } }
|
|
337
|
+
];
|
|
338
|
+
stream = client.watch(pipeline, {
|
|
339
|
+
fullDocument: "updateLookup",
|
|
340
|
+
...resumeAfter ? { resumeAfter } : {}
|
|
341
|
+
});
|
|
342
|
+
const stoppedPromise = new Promise((resolve) => {
|
|
343
|
+
let settled = false;
|
|
344
|
+
const onAbort = () => settle({ reason: "close" });
|
|
345
|
+
const settle = (result) => {
|
|
346
|
+
if (settled) return;
|
|
347
|
+
settled = true;
|
|
348
|
+
abortController.signal.removeEventListener("abort", onAbort);
|
|
349
|
+
resolve(result);
|
|
350
|
+
};
|
|
351
|
+
abortController.signal.addEventListener("abort", onAbort);
|
|
352
|
+
stream?.once("close", () => settle({ reason: "close" }));
|
|
353
|
+
stream?.once("error", (err) => settle({
|
|
354
|
+
reason: "error",
|
|
355
|
+
error: err
|
|
356
|
+
}));
|
|
357
|
+
});
|
|
358
|
+
stream.on("change", (change) => {
|
|
359
|
+
const streamRef = stream;
|
|
360
|
+
processing = processing.then(async () => {
|
|
361
|
+
const ns = "ns" in change ? change.ns : void 0;
|
|
362
|
+
const dbName = String(ns?.db ?? "");
|
|
363
|
+
if (!dbName) return;
|
|
364
|
+
const tenantId = getTenantIdFromDbName(dbName, appName);
|
|
365
|
+
if (!tenantId) return;
|
|
366
|
+
const collName = String(ns && "coll" in ns ? ns.coll : "");
|
|
367
|
+
if (!collName) return;
|
|
368
|
+
if (shouldSkipCollection(collName)) return;
|
|
369
|
+
if (INTERNAL_IGNORED_COLLECTION_NAMES.has(collName)) return;
|
|
370
|
+
const modelName = resolveModelNameFromCollection(collName);
|
|
371
|
+
if (!modelName) return;
|
|
372
|
+
if (INTERNAL_IGNORED_MODEL_NAMES.has(modelName)) return;
|
|
373
|
+
const op = String(change.operationType ?? "");
|
|
374
|
+
if (!op) return;
|
|
375
|
+
const normalizedOp = normalizeOpForTaskName(op);
|
|
376
|
+
let doc = "fullDocument" in change ? change.fullDocument : void 0;
|
|
377
|
+
if (!doc && normalizedOp === "delete") doc = "documentKey" in change ? change.documentKey : void 0;
|
|
378
|
+
if (!doc) return;
|
|
379
|
+
const updateDescription = "updateDescription" in change ? change.updateDescription : void 0;
|
|
380
|
+
try {
|
|
381
|
+
await dispatchWorkerQueue({
|
|
382
|
+
dbName,
|
|
383
|
+
tenantId,
|
|
384
|
+
modelName,
|
|
385
|
+
op: normalizedOp,
|
|
386
|
+
doc,
|
|
387
|
+
updateDescription
|
|
388
|
+
});
|
|
389
|
+
resumeAfter = change?._id ?? resumeAfter;
|
|
390
|
+
} catch (err) {
|
|
391
|
+
console.warn("queue listener failed to dispatch change", err);
|
|
392
|
+
try {
|
|
393
|
+
await streamRef?.close();
|
|
394
|
+
} catch {}
|
|
395
|
+
}
|
|
396
|
+
}).catch((err) => {
|
|
397
|
+
console.warn("queue listener change handler failed", err);
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
stream.on("error", (err) => {
|
|
401
|
+
if (stopped) return;
|
|
402
|
+
if (resumeAfter && isChangeStreamHistoryLost(err)) resumeAfter = null;
|
|
403
|
+
try {
|
|
404
|
+
Promise.resolve(stream?.close()).catch(() => {});
|
|
405
|
+
} catch {}
|
|
406
|
+
});
|
|
407
|
+
return { stoppedPromise };
|
|
408
|
+
};
|
|
409
|
+
const run = async () => {
|
|
410
|
+
let retryCounter = 0;
|
|
411
|
+
while (!stopped) try {
|
|
412
|
+
setStatus({
|
|
413
|
+
state: "connecting",
|
|
414
|
+
attempt: retryCounter + 1
|
|
415
|
+
});
|
|
416
|
+
const { stoppedPromise } = await startStream();
|
|
417
|
+
retryCounter = 0;
|
|
418
|
+
setStatus({ state: "ready" });
|
|
419
|
+
resolveReady();
|
|
420
|
+
const end = await stoppedPromise;
|
|
421
|
+
if (stopped) return;
|
|
422
|
+
retryCounter += 1;
|
|
423
|
+
if (maxRetries !== "infinite" && retryCounter > maxRetries) {
|
|
424
|
+
const err = end.reason === "error" ? end.error : /* @__PURE__ */ new Error("queue listener closed");
|
|
425
|
+
setStatus({
|
|
426
|
+
state: "failed",
|
|
427
|
+
attempt: retryCounter,
|
|
428
|
+
error: err
|
|
429
|
+
});
|
|
430
|
+
if (fatalOnMaxRetries) try {
|
|
431
|
+
options.onFatal?.(err);
|
|
432
|
+
} catch (fatalErr) {
|
|
433
|
+
console.warn("queue listener onFatal failed", fatalErr);
|
|
434
|
+
}
|
|
435
|
+
rejectReady(err);
|
|
436
|
+
abortController.abort();
|
|
437
|
+
await closeResources();
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
const delayMs = getRetryDelayMs(retryCounter, retryDelays);
|
|
441
|
+
const err = end.reason === "error" ? end.error : /* @__PURE__ */ new Error("queue listener closed");
|
|
442
|
+
console.warn("queue listener not ready, retrying in", delayMs, err);
|
|
443
|
+
setStatus({
|
|
444
|
+
state: "error",
|
|
445
|
+
attempt: retryCounter,
|
|
446
|
+
error: err,
|
|
447
|
+
nextRetryInMs: delayMs
|
|
448
|
+
});
|
|
449
|
+
await closeResources();
|
|
450
|
+
await sleep(delayMs, abortController.signal);
|
|
451
|
+
} catch (err) {
|
|
452
|
+
if (stopped) return;
|
|
453
|
+
retryCounter += 1;
|
|
454
|
+
if (maxRetries !== "infinite" && retryCounter > maxRetries) {
|
|
455
|
+
setStatus({
|
|
456
|
+
state: "failed",
|
|
457
|
+
attempt: retryCounter,
|
|
458
|
+
error: err
|
|
459
|
+
});
|
|
460
|
+
if (fatalOnMaxRetries) try {
|
|
461
|
+
options.onFatal?.(err);
|
|
462
|
+
} catch (fatalErr) {
|
|
463
|
+
console.warn("queue listener onFatal failed", fatalErr);
|
|
464
|
+
}
|
|
465
|
+
rejectReady(err);
|
|
466
|
+
abortController.abort();
|
|
467
|
+
await closeResources();
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
const delayMs = getRetryDelayMs(retryCounter, retryDelays);
|
|
471
|
+
console.warn("queue listener not ready, retrying in", delayMs, err);
|
|
472
|
+
setStatus({
|
|
473
|
+
state: "error",
|
|
474
|
+
attempt: retryCounter,
|
|
475
|
+
error: err,
|
|
476
|
+
nextRetryInMs: delayMs
|
|
477
|
+
});
|
|
478
|
+
await closeResources();
|
|
479
|
+
await sleep(delayMs, abortController.signal);
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
const handle = {
|
|
483
|
+
ready,
|
|
484
|
+
close,
|
|
485
|
+
getStatus: () => status
|
|
486
|
+
};
|
|
487
|
+
run().catch(async (err) => {
|
|
488
|
+
if (stopped) return;
|
|
489
|
+
setStatus({
|
|
490
|
+
state: "failed",
|
|
491
|
+
attempt: 0,
|
|
492
|
+
error: err
|
|
493
|
+
});
|
|
494
|
+
rejectReady(err);
|
|
495
|
+
await closeResources();
|
|
496
|
+
});
|
|
497
|
+
return handle;
|
|
592
498
|
};
|
|
593
|
-
//#
|
|
499
|
+
//#endregion
|
|
500
|
+
//#region src/taskNames.ts
|
|
501
|
+
var dbEventTaskName = (op, modelName) => `on-${op}-${modelName}`;
|
|
502
|
+
//#endregion
|
|
503
|
+
export { add, close, dbEventTaskName, getInstance, getJob, getJobs, getTasks, getUrl, queueApi as queue, registerQueueListener, registerTask, scheduleTask, start };
|
|
504
|
+
|
|
505
|
+
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../src/queue.ts","../src/queueListener.ts","../src/taskNames.ts"],"sourcesContent":["import domain from \"node:domain\"\n\nimport { Queue, Worker, type Job, type JobsOptions as JobOptions } from \"bullmq\"\n\nimport type { WorkerTasksMap } from \"./tasksMap\"\n\n\nexport type TaskHandler<TPayload = unknown> = (payload: TPayload, job: Job) => unknown | Promise<unknown>\n\nconst DEFAULT_QUEUE_NAME = \"rb-queue-default\"\nconst DEFAULT_CONCURRENCY = 2\nconst DEFAULT_RENTENTION = 128\n\nconst tasksByName: Record<string, TaskHandler<unknown>> = Object.create(null)\n\nlet queueInstance: Queue | null = null\nlet workerInstance: Worker | null = null\nlet hasStarted = false\n\nconst getRedisUrl = (): string => {\n const redisUrl = process.env.REDIS_URL?.trim()\n if (!redisUrl) {\n throw new Error(\"Missing REDIS_URL (required for @rpcbase/worker queue)\")\n }\n return redisUrl\n}\n\nconst getQueueName = (): string => process.env.RB_QUEUE_NAME?.trim() || DEFAULT_QUEUE_NAME\n\nconst getConcurrency = (): number => {\n const raw = process.env.RB_QUEUE_CONCURRENCY?.trim()\n const value = raw ? Number(raw) : DEFAULT_CONCURRENCY\n if (!Number.isFinite(value) || value <= 0) return DEFAULT_CONCURRENCY\n return Math.floor(value)\n}\n\nconst getConnection = () => ({ url: getRedisUrl() })\n\nconst toError = (error: unknown): Error => {\n if (error instanceof Error) return error\n const message = typeof error === \"string\" ? error : String(error ?? \"unknown error\")\n return new Error(message)\n}\n\nconst runTaskHandler = async (job: Job, handler: TaskHandler<unknown>): Promise<unknown> => {\n return await new Promise((resolve, reject) => {\n const taskDomain = domain.create()\n let settled = false\n\n const settle = (next: (value: unknown) => void, value: unknown) => {\n if (settled) return\n settled = true\n taskDomain.removeAllListeners()\n next(value)\n }\n\n taskDomain.on(\"error\", (error: unknown) => {\n const normalizedError = toError(error)\n settle(reject, normalizedError)\n })\n\n taskDomain.run(() => {\n Promise.resolve(handler(job.data, job)).then(\n (result) => settle(resolve, result),\n (error) => settle(reject, toError(error)),\n )\n })\n })\n}\n\nconst ensureQueue = (): Queue => {\n if (queueInstance) return queueInstance\n queueInstance = new Queue(getQueueName(), {\n connection: getConnection(),\n defaultJobOptions: {\n removeOnComplete: DEFAULT_RENTENTION,\n removeOnFail: DEFAULT_RENTENTION\n },\n })\n\n return queueInstance\n}\n\nexport function registerTask<TName extends keyof WorkerTasksMap & string>(\n name: TName,\n handler: TaskHandler<WorkerTasksMap[TName]>,\n): void\nexport function registerTask(name: string, handler: TaskHandler<unknown>): void\nexport function registerTask(name: string, handler: TaskHandler<unknown>): void {\n tasksByName[name] = handler\n}\n\nexport const getTasks = (): Record<string, TaskHandler<unknown>> => tasksByName\n\nexport function add<TName extends keyof WorkerTasksMap & string>(\n taskName: TName,\n payload: WorkerTasksMap[TName],\n options?: JobOptions,\n): Promise<Job>\nexport function add(taskName: string, payload: unknown, options?: JobOptions): Promise<Job>\nexport function add(taskName: string, payload: unknown, options?: JobOptions): Promise<Job> {\n return ensureQueue().add(taskName, payload, options)\n}\n\nexport const getJob = async (jobId: string): Promise<Job | null> => (await ensureQueue().getJob(jobId)) ?? null\n\nexport const getJobs = async (...args: Parameters<Queue[\"getJobs\"]>): Promise<Job[]> =>\n ensureQueue().getJobs(...args) as unknown as Job[]\n\nexport const getInstance = (): Queue => ensureQueue()\n\nexport const start = async (): Promise<Queue> => {\n if (hasStarted) return ensureQueue()\n hasStarted = true\n\n const queue = ensureQueue()\n const concurrency = getConcurrency()\n\n console.log(\"start worker queue\", { queue: queue.name, redisUrl: getRedisUrl(), concurrency })\n\n queue.on(\"error\", (err) => {\n console.log(`queue error: ${err.message}`)\n })\n\n if (!workerInstance) {\n workerInstance = new Worker(\n queue.name,\n async (job) => {\n const taskName = job.name\n\n const handler = tasksByName[taskName]\n if (!handler) {\n throw new Error(`No task registered for '${taskName}'`)\n }\n\n return await runTaskHandler(job, handler)\n },\n {\n concurrency,\n connection: getConnection(),\n },\n )\n }\n\n workerInstance.on(\"error\", (err) => {\n console.log(`worker error: ${err.message}`)\n })\n\n workerInstance.on(\"stalled\", (jobId) => {\n console.log(`job ${jobId} stalled`)\n })\n\n workerInstance.on(\"failed\", (job, err) => {\n console.log(`job ${job?.id ?? \"unknown\"} failed`, err)\n })\n\n await workerInstance.waitUntilReady()\n\n return queue\n}\n\nexport const close = async (): Promise<void> => {\n if (!queueInstance && !workerInstance) return\n try {\n await workerInstance?.close()\n await queueInstance?.close()\n } finally {\n workerInstance = null\n queueInstance = null\n hasStarted = false\n }\n}\n\nexport const getUrl = (): string => getRedisUrl()\n\nexport type ScheduleTaskOptions = Omit<JobOptions, \"repeat\"> & {\n jobId?: string\n repeat: NonNullable<JobOptions[\"repeat\"]>\n}\n\nexport function scheduleTask<TName extends keyof WorkerTasksMap & string>(\n taskName: TName,\n payload: WorkerTasksMap[TName],\n options: ScheduleTaskOptions,\n): Promise<Job>\nexport function scheduleTask(taskName: string, payload: unknown, options: ScheduleTaskOptions): Promise<Job>\nexport async function scheduleTask(taskName: string, payload: unknown, options: ScheduleTaskOptions): Promise<Job> {\n const { jobId, repeat, ...rest } = options\n return add(taskName, payload, {\n ...rest,\n jobId: jobId ?? `schedule|${taskName}`,\n repeat,\n })\n}\n\nconst queueApi = {\n start,\n close,\n registerTask,\n getTasks,\n add,\n scheduleTask,\n getJob,\n getJobs,\n getInstance,\n getUrl,\n}\n\nexport default queueApi\n","import {\n MongoClient,\n type ChangeStream,\n type ChangeStreamDocument,\n type Document,\n type MongoClientOptions,\n} from \"mongodb\"\nimport mongoose from \"mongoose\"\n\nimport queue from \"./queue\"\n\n\nexport type QueueListenerRetryDelays = {\n minMs?: number\n maxMs?: number\n factor?: number\n}\n\nexport type QueueListenerStatus =\n | { state: \"connecting\"; attempt: number }\n | { state: \"ready\" }\n | { state: \"error\"; attempt: number; error: unknown; nextRetryInMs: number }\n | { state: \"failed\"; attempt: number; error: unknown }\n | { state: \"closed\" }\n\nexport type QueueListenerHandle = {\n ready: Promise<void>\n close: () => Promise<void>\n getStatus: () => QueueListenerStatus\n}\n\nexport type QueueListenerOptions = {\n maxRetries?: number | \"infinite\"\n fatalOnMaxRetries?: boolean\n onStateChange?: (status: QueueListenerStatus) => void\n onFatal?: (err: unknown) => void\n retryDelays?: QueueListenerRetryDelays\n mongoClientOptions?: MongoClientOptions\n}\n\nconst RETRY_MAXIMUM_DELAY_MS = 3000\nconst RETRY_MINIMUM_DELAY_MS = 50\nconst RETRY_DEFAULT_FACTOR = 2\n\nconst sleep = async (ms: number, signal?: AbortSignal): Promise<void> => new Promise((resolve) => {\n if (signal?.aborted) {\n resolve()\n return\n }\n\n let timeout: ReturnType<typeof setTimeout> | null = null\n\n const cleanup = () => {\n if (timeout) clearTimeout(timeout)\n signal?.removeEventListener(\"abort\", onAbort)\n }\n\n const onAbort = () => {\n cleanup()\n resolve()\n }\n\n timeout = setTimeout(() => {\n cleanup()\n resolve()\n }, ms)\n timeout.unref?.()\n\n signal?.addEventListener(\"abort\", onAbort)\n})\n\nconst getMongoUrl = (): string => {\n const explicit =\n process.env.MONGODB_URL\n ?? process.env.MONGO_URL\n ?? process.env.MONGODB_URI\n ?? process.env.DB_URL\n\n if (explicit && explicit.trim()) return explicit.trim()\n\n const port = process.env.DB_PORT?.trim()\n if (!port) throw new Error(\"Missing Mongo connection details (MONGODB_URL/MONGO_URL/MONGODB_URI/DB_URL/DB_PORT)\")\n\n const host = process.env.DB_HOST?.trim() || \"localhost\"\n return `mongodb://${host}:${port}`\n}\n\nconst escapeRegex = (value: string): string => value.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")\n\ntype ModelWithCollection = {\n collection?: {\n collectionName?: string\n name?: string\n }\n}\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === \"object\" && value !== null\n\nconst getDocumentId = (doc: unknown): unknown => {\n if (!isRecord(doc)) return undefined\n return doc._id\n}\n\nconst resolveModelNameFromCollection = (collName: string): string | null => {\n const models = mongoose.models\n\n for (const modelName of Object.keys(models)) {\n const model = models[modelName] as ModelWithCollection\n const collectionName =\n model.collection?.collectionName\n ?? model.collection?.name\n\n if (collectionName === collName) {\n return modelName\n }\n }\n\n return null\n}\n\nconst normalizeUpdateDescription = (updateDescription: unknown): unknown => {\n if (!isRecord(updateDescription)) return updateDescription\n if (isRecord(updateDescription.updatedFields)) {\n return {\n ...updateDescription,\n updatedFieldsKeys: Object.keys(updateDescription.updatedFields),\n }\n }\n\n return updateDescription\n}\n\nconst normalizeOpForTaskName = (op: string): string => (op === \"replace\" ? \"update\" : op)\n\nconst getTenantIdFromDbName = (dbName: string, appName: string): string | null => {\n const escapedAppName = escapeRegex(appName)\n const match = dbName.match(new RegExp(`^${escapedAppName}-(.+)-db$`))\n const tenantId = match?.[1]?.trim()\n return tenantId ? tenantId : null\n}\n\nconst dispatchWorkerQueue = async ({\n dbName,\n tenantId,\n modelName,\n op,\n doc,\n updateDescription,\n}: {\n dbName: string\n tenantId: string\n modelName: string\n op: string\n doc: unknown\n updateDescription?: unknown\n}): Promise<void> => {\n const tasks = queue.getTasks()\n const taskOp = normalizeOpForTaskName(op)\n const handlerName = `on-${taskOp}-${modelName}`\n\n if (!tasks[handlerName]) return\n\n const normalizedUpdateDescription = normalizeUpdateDescription(updateDescription)\n\n await queue.add(handlerName, { tenantId, doc, updateDescription: normalizedUpdateDescription }, {\n jobId: `${dbName}|${taskOp}-${getDocumentId(doc) ?? \"unknown\"}`,\n removeOnComplete: true,\n removeOnFail: true,\n })\n}\n\nconst shouldSkipCollection = (collName: string): boolean =>\n collName.endsWith(\".files\") || collName.endsWith(\".chunks\")\n\nconst INTERNAL_IGNORED_MODEL_NAMES = new Set([\"RBRtsChange\", \"RBRtsCounter\"])\n\nconst INTERNAL_IGNORED_COLLECTION_NAMES = new Set([\"rtschanges\", \"rtscounters\"])\n\nconst normalizeRetryDelays = (input: QueueListenerRetryDelays | undefined): Required<QueueListenerRetryDelays> => {\n const minMs = Math.max(0, Math.floor(input?.minMs ?? RETRY_MINIMUM_DELAY_MS))\n const rawMaxMs = Math.max(0, Math.floor(input?.maxMs ?? RETRY_MAXIMUM_DELAY_MS))\n const maxMs = Math.max(minMs, rawMaxMs)\n const factor = Math.max(1, Number.isFinite(input?.factor) ? (input?.factor ?? RETRY_DEFAULT_FACTOR) : RETRY_DEFAULT_FACTOR)\n return { minMs, maxMs, factor }\n}\n\nconst getRetryDelayMs = (attempt: number, delays: Required<QueueListenerRetryDelays>): number =>\n Math.min(delays.maxMs, Math.round(delays.minMs * Math.pow(delays.factor, Math.max(0, attempt - 1))))\n\nconst normalizeMaxRetries = (value: number | \"infinite\"): number | \"infinite\" => {\n if (value === \"infinite\") return value\n const parsed = Math.floor(value)\n return Number.isFinite(parsed) ? Math.max(0, parsed) : 0\n}\n\nexport const registerQueueListener = async (options: QueueListenerOptions = {}): Promise<QueueListenerHandle> => {\n const maxRetries = normalizeMaxRetries(options.maxRetries ?? \"infinite\")\n const fatalOnMaxRetries = options.fatalOnMaxRetries ?? false\n const retryDelays = normalizeRetryDelays(options.retryDelays)\n\n const appName = process.env.APP_NAME?.trim()\n if (!appName) {\n throw new Error(\"Missing APP_NAME (required to configure the worker DB change listener)\")\n }\n\n const mongoUrl = getMongoUrl()\n const mongoClientOptions: MongoClientOptions = {\n family: 4,\n serverSelectionTimeoutMS: 2000,\n connectTimeoutMS: 2000,\n ...options.mongoClientOptions,\n }\n\n let stopped = false\n const abortController = new AbortController()\n let client: MongoClient | null = null\n let stream: ChangeStream<Document> | null = null\n let resumeAfter: Document | null = null\n let processing = Promise.resolve()\n\n let status: QueueListenerStatus = { state: \"connecting\", attempt: 1 }\n const setStatus = (next: QueueListenerStatus): void => {\n status = next\n try {\n options.onStateChange?.(next)\n } catch (err) {\n console.warn(\"queue listener onStateChange failed\", err)\n }\n }\n\n let readySettled = false\n let readyResolve: (() => void) | null = null\n let readyReject: ((err: unknown) => void) | null = null\n const ready = new Promise<void>((resolve, reject) => {\n readyResolve = resolve\n readyReject = reject\n })\n\n const resolveReady = (): void => {\n if (readySettled) return\n readySettled = true\n readyResolve?.()\n }\n\n const rejectReady = (err: unknown): void => {\n if (readySettled) return\n readySettled = true\n readyReject?.(err)\n }\n\n const isChangeStreamHistoryLost = (err: unknown): boolean => {\n const maybeErr = err as { code?: unknown; codeName?: unknown }\n const code = typeof maybeErr.code === \"number\" ? maybeErr.code : null\n const codeName = typeof maybeErr.codeName === \"string\" ? maybeErr.codeName : \"\"\n const message = err instanceof Error ? err.message : String(err ?? \"\")\n\n return (\n code === 286\n || codeName === \"ChangeStreamHistoryLost\"\n || message.includes(\"ChangeStreamHistoryLost\")\n || message.includes(\"resume token\")\n || message.includes(\"Resume token\")\n || message.includes(\"resume of change stream was not possible\")\n || message.includes(\"cannot resume\")\n )\n }\n\n const close = async (): Promise<void> => {\n stopped = true\n abortController.abort()\n try {\n stream?.removeAllListeners()\n await stream?.close()\n } catch {\n // ignore\n }\n stream = null\n\n try {\n await client?.close()\n } catch {\n // ignore\n }\n client = null\n\n if (!readySettled) {\n rejectReady(new Error(\"queue listener closed before ready\"))\n }\n\n setStatus({ state: \"closed\" })\n }\n\n const closeResources = async (): Promise<void> => {\n try {\n stream?.removeAllListeners()\n await stream?.close()\n } catch {\n // ignore\n }\n stream = null\n\n try {\n await client?.close()\n } catch {\n // ignore\n }\n client = null\n }\n\n const startStream = async (): Promise<{ stoppedPromise: Promise<{ reason: \"close\" | \"error\"; error?: unknown }> }> => {\n if (stopped) return { stoppedPromise: Promise.resolve({ reason: \"close\" }) }\n\n if (stream) {\n try {\n stream.removeAllListeners()\n await stream.close()\n } catch {\n // ignore\n }\n stream = null\n }\n\n if (client) {\n try {\n await client.close()\n } catch {\n // ignore\n }\n client = null\n }\n\n client = new MongoClient(mongoUrl, mongoClientOptions)\n\n await client.connect()\n\n const dbMatch = { \"ns.db\": { $regex: `^${escapeRegex(appName)}-.*-db$` } }\n\n const pipeline: Document[] = [\n { $match: dbMatch },\n {\n $match: {\n operationType: { $in: [\"insert\", \"update\", \"replace\", \"delete\"] },\n }\n },\n { $match: { \"ns.coll\": { $nin: Array.from(INTERNAL_IGNORED_COLLECTION_NAMES) } } },\n ]\n\n stream = client.watch(pipeline, {\n fullDocument: \"updateLookup\",\n ...(resumeAfter ? { resumeAfter } : {}),\n })\n\n const stoppedPromise = new Promise<{ reason: \"close\" | \"error\"; error?: unknown }>((resolve) => {\n let settled = false\n const onAbort = () => settle({ reason: \"close\" })\n\n const settle = (result: { reason: \"close\" | \"error\"; error?: unknown }) => {\n if (settled) return\n settled = true\n abortController.signal.removeEventListener(\"abort\", onAbort)\n resolve(result)\n }\n\n abortController.signal.addEventListener(\"abort\", onAbort)\n stream?.once(\"close\", () => settle({ reason: \"close\" }))\n stream?.once(\"error\", (err) => settle({ reason: \"error\", error: err }))\n })\n\n stream.on(\"change\", (change: ChangeStreamDocument<Document>) => {\n const streamRef = stream\n processing = processing.then(async () => {\n const ns = \"ns\" in change ? change.ns : undefined\n const dbName = String(ns?.db ?? \"\")\n if (!dbName) return\n const tenantId = getTenantIdFromDbName(dbName, appName)\n if (!tenantId) return\n\n const collName = String(ns && \"coll\" in ns ? ns.coll : \"\")\n if (!collName) return\n if (shouldSkipCollection(collName)) return\n if (INTERNAL_IGNORED_COLLECTION_NAMES.has(collName)) return\n\n const modelName = resolveModelNameFromCollection(collName)\n if (!modelName) return\n if (INTERNAL_IGNORED_MODEL_NAMES.has(modelName)) return\n\n const op = String(change.operationType ?? \"\")\n if (!op) return\n const normalizedOp = normalizeOpForTaskName(op)\n\n let doc = \"fullDocument\" in change ? change.fullDocument : undefined\n if (!doc && normalizedOp === \"delete\") {\n doc = \"documentKey\" in change ? change.documentKey : undefined\n }\n if (!doc) return\n\n const updateDescription = \"updateDescription\" in change ? change.updateDescription : undefined\n\n try {\n await dispatchWorkerQueue({\n dbName,\n tenantId,\n modelName,\n op: normalizedOp,\n doc,\n updateDescription,\n })\n\n resumeAfter = change?._id ?? resumeAfter\n } catch (err) {\n console.warn(\"queue listener failed to dispatch change\", err)\n try {\n await streamRef?.close()\n } catch {\n // ignore\n }\n }\n }).catch((err) => {\n console.warn(\"queue listener change handler failed\", err)\n })\n })\n\n stream.on(\"error\", (err) => {\n if (stopped) return\n if (resumeAfter && isChangeStreamHistoryLost(err)) {\n resumeAfter = null\n }\n try {\n void Promise.resolve(stream?.close()).catch(() => {})\n } catch {\n // ignore\n }\n })\n\n return { stoppedPromise }\n }\n\n const run = async (): Promise<void> => {\n let retryCounter = 0\n\n while (!stopped) {\n try {\n setStatus({ state: \"connecting\", attempt: retryCounter + 1 })\n\n const { stoppedPromise } = await startStream()\n retryCounter = 0\n setStatus({ state: \"ready\" })\n resolveReady()\n\n const end = await stoppedPromise\n if (stopped) return\n\n retryCounter += 1\n if (maxRetries !== \"infinite\" && retryCounter > maxRetries) {\n const err = end.reason === \"error\" ? end.error : new Error(\"queue listener closed\")\n setStatus({ state: \"failed\", attempt: retryCounter, error: err })\n if (fatalOnMaxRetries) {\n try {\n options.onFatal?.(err)\n } catch (fatalErr) {\n console.warn(\"queue listener onFatal failed\", fatalErr)\n }\n }\n rejectReady(err)\n abortController.abort()\n await closeResources()\n return\n }\n\n const delayMs = getRetryDelayMs(retryCounter, retryDelays)\n const err = end.reason === \"error\" ? end.error : new Error(\"queue listener closed\")\n console.warn(\"queue listener not ready, retrying in\", delayMs, err)\n setStatus({ state: \"error\", attempt: retryCounter, error: err, nextRetryInMs: delayMs })\n await closeResources()\n await sleep(delayMs, abortController.signal)\n } catch (err) {\n if (stopped) return\n\n retryCounter += 1\n if (maxRetries !== \"infinite\" && retryCounter > maxRetries) {\n setStatus({ state: \"failed\", attempt: retryCounter, error: err })\n if (fatalOnMaxRetries) {\n try {\n options.onFatal?.(err)\n } catch (fatalErr) {\n console.warn(\"queue listener onFatal failed\", fatalErr)\n }\n }\n rejectReady(err)\n abortController.abort()\n await closeResources()\n return\n }\n\n const delayMs = getRetryDelayMs(retryCounter, retryDelays)\n console.warn(\"queue listener not ready, retrying in\", delayMs, err)\n setStatus({ state: \"error\", attempt: retryCounter, error: err, nextRetryInMs: delayMs })\n await closeResources()\n await sleep(delayMs, abortController.signal)\n }\n }\n }\n\n const handle: QueueListenerHandle = {\n ready,\n close,\n getStatus: () => status,\n }\n\n void run().catch(async (err) => {\n if (stopped) return\n setStatus({ state: \"failed\", attempt: 0, error: err })\n rejectReady(err)\n await closeResources()\n })\n\n return handle\n}\n","export type DbEventOp = \"insert\" | \"update\" | \"delete\"\n\nexport type DbEventTaskName<TOp extends DbEventOp = DbEventOp, TModelName extends string = string> =\n `on-${TOp}-${TModelName}`\n\nexport type DbEventTaskPayload<TDoc = unknown, TUpdateDescription = unknown> = {\n tenantId: string\n doc: TDoc\n updateDescription?: TUpdateDescription\n}\n\nexport const dbEventTaskName = <TOp extends DbEventOp, TModelName extends string>(\n op: TOp,\n modelName: TModelName,\n): DbEventTaskName<TOp, TModelName> => `on-${op}-${modelName}`\n"],"names":["DEFAULT_QUEUE_NAME","DEFAULT_CONCURRENCY","DEFAULT_RENTENTION","tasksByName","Object","create","queueInstance","workerInstance","hasStarted","getRedisUrl","redisUrl","process","env","REDIS_URL","trim","Error","getQueueName","RB_QUEUE_NAME","getConcurrency","raw","RB_QUEUE_CONCURRENCY","value","Number","isFinite","Math","floor","getConnection","url","toError","error","message","String","runTaskHandler","job","handler","Promise","resolve","reject","taskDomain","domain","settled","settle","next","removeAllListeners","on","normalizedError","run","data","then","result","ensureQueue","Queue","connection","defaultJobOptions","removeOnComplete","removeOnFail","registerTask","name","getTasks","add","taskName","payload","options","getJob","jobId","getJobs","args","getInstance","start","queue","concurrency","console","log","err","Worker","id","waitUntilReady","close","getUrl","scheduleTask","repeat","rest","queueApi","RETRY_MAXIMUM_DELAY_MS","RETRY_MINIMUM_DELAY_MS","RETRY_DEFAULT_FACTOR","sleep","ms","signal","aborted","timeout","cleanup","removeEventListener","onAbort","setTimeout","unref","addEventListener","getMongoUrl","explicit","MONGODB_URL","MONGO_URL","MONGODB_URI","DB_URL","port","DB_PORT","host","DB_HOST","escapeRegex","replace","isRecord","getDocumentId","doc","undefined","_id","resolveModelNameFromCollection","collName","models","mongoose","modelName","keys","model","collectionName","collection","normalizeUpdateDescription","updateDescription","updatedFields","updatedFieldsKeys","normalizeOpForTaskName","op","getTenantIdFromDbName","dbName","appName","escapedAppName","match","RegExp","tenantId","dispatchWorkerQueue","tasks","taskOp","handlerName","normalizedUpdateDescription","shouldSkipCollection","endsWith","INTERNAL_IGNORED_MODEL_NAMES","Set","INTERNAL_IGNORED_COLLECTION_NAMES","normalizeRetryDelays","input","minMs","max","rawMaxMs","maxMs","factor","getRetryDelayMs","attempt","delays","min","round","pow","normalizeMaxRetries","parsed","registerQueueListener","maxRetries","fatalOnMaxRetries","retryDelays","APP_NAME","mongoUrl","mongoClientOptions","family","serverSelectionTimeoutMS","connectTimeoutMS","stopped","abortController","AbortController","client","stream","resumeAfter","processing","status","state","setStatus","onStateChange","warn","readySettled","readyResolve","readyReject","ready","resolveReady","rejectReady","isChangeStreamHistoryLost","maybeErr","code","codeName","includes","abort","closeResources","startStream","stoppedPromise","reason","MongoClient","connect","dbMatch","$regex","pipeline","$match","operationType","$in","$nin","Array","from","watch","fullDocument","once","change","streamRef","ns","db","coll","has","normalizedOp","documentKey","catch","retryCounter","end","onFatal","fatalErr","delayMs","nextRetryInMs","handle","getStatus","dbEventTaskName"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["domain","Queue","Worker","Job","JobsOptions","JobOptions","WorkerTasksMap","TaskHandler","payload","TPayload","job","Promise","DEFAULT_QUEUE_NAME","DEFAULT_CONCURRENCY","DEFAULT_RENTENTION","tasksByName","Record","Object","create","queueInstance","workerInstance","hasStarted","getRedisUrl","redisUrl","process","env","REDIS_URL","trim","Error","getQueueName","RB_QUEUE_NAME","getConcurrency","raw","RB_QUEUE_CONCURRENCY","value","Number","isFinite","Math","floor","getConnection","url","toError","error","message","String","runTaskHandler","handler","resolve","reject","taskDomain","settled","settle","next","removeAllListeners","on","normalizedError","run","data","then","result","ensureQueue","connection","defaultJobOptions","removeOnComplete","removeOnFail","registerTask","name","TName","getTasks","add","taskName","options","getJob","jobId","getJobs","args","Parameters","getInstance","start","queue","concurrency","console","log","err","id","waitUntilReady","close","getUrl","ScheduleTaskOptions","Omit","repeat","NonNullable","scheduleTask","rest","queueApi","MongoClient","ChangeStream","ChangeStreamDocument","Document","MongoClientOptions","mongoose","queue","QueueListenerRetryDelays","minMs","maxMs","factor","QueueListenerStatus","state","attempt","error","nextRetryInMs","QueueListenerHandle","ready","Promise","close","getStatus","QueueListenerOptions","maxRetries","fatalOnMaxRetries","onStateChange","status","onFatal","err","retryDelays","mongoClientOptions","RETRY_MAXIMUM_DELAY_MS","RETRY_MINIMUM_DELAY_MS","RETRY_DEFAULT_FACTOR","sleep","ms","signal","AbortSignal","resolve","aborted","timeout","ReturnType","setTimeout","cleanup","clearTimeout","removeEventListener","onAbort","unref","addEventListener","getMongoUrl","explicit","process","env","MONGODB_URL","MONGO_URL","MONGODB_URI","DB_URL","trim","port","DB_PORT","Error","host","DB_HOST","escapeRegex","value","replace","ModelWithCollection","collection","collectionName","name","isRecord","Record","getDocumentId","doc","undefined","_id","resolveModelNameFromCollection","collName","models","modelName","Object","keys","model","normalizeUpdateDescription","updateDescription","updatedFields","updatedFieldsKeys","normalizeOpForTaskName","op","getTenantIdFromDbName","dbName","appName","escapedAppName","match","RegExp","tenantId","dispatchWorkerQueue","tasks","getTasks","taskOp","handlerName","normalizedUpdateDescription","add","jobId","removeOnComplete","removeOnFail","shouldSkipCollection","endsWith","INTERNAL_IGNORED_MODEL_NAMES","Set","INTERNAL_IGNORED_COLLECTION_NAMES","normalizeRetryDelays","input","Required","Math","max","floor","rawMaxMs","Number","isFinite","getRetryDelayMs","delays","min","round","pow","normalizeMaxRetries","parsed","registerQueueListener","options","APP_NAME","mongoUrl","family","serverSelectionTimeoutMS","connectTimeoutMS","stopped","abortController","AbortController","client","stream","resumeAfter","processing","setStatus","next","console","warn","readySettled","readyResolve","readyReject","reject","resolveReady","rejectReady","isChangeStreamHistoryLost","maybeErr","code","codeName","message","String","includes","abort","removeAllListeners","closeResources","startStream","stoppedPromise","reason","connect","dbMatch","$regex","pipeline","$match","operationType","$in","$nin","Array","from","watch","fullDocument","settled","settle","result","once","on","change","streamRef","then","ns","db","coll","has","normalizedOp","documentKey","catch","run","retryCounter","end","fatalErr","delayMs","handle","DbEventOp","DbEventTaskName","TOp","TModelName","DbEventTaskPayload","tenantId","doc","TDoc","updateDescription","TUpdateDescription","dbEventTaskName","op","modelName"],"sources":["../src/queue.ts","../src/queueListener.ts","../src/taskNames.ts"],"sourcesContent":["import domain from \"node:domain\"\n\nimport { Queue, Worker, type Job, type JobsOptions as JobOptions } from \"bullmq\"\n\nimport type { WorkerTasksMap } from \"./tasksMap\"\n\n\nexport type TaskHandler<TPayload = unknown> = (payload: TPayload, job: Job) => unknown | Promise<unknown>\n\nconst DEFAULT_QUEUE_NAME = \"rb-queue-default\"\nconst DEFAULT_CONCURRENCY = 2\nconst DEFAULT_RENTENTION = 128\n\nconst tasksByName: Record<string, TaskHandler<unknown>> = Object.create(null)\n\nlet queueInstance: Queue | null = null\nlet workerInstance: Worker | null = null\nlet hasStarted = false\n\nconst getRedisUrl = (): string => {\n const redisUrl = process.env.REDIS_URL?.trim()\n if (!redisUrl) {\n throw new Error(\"Missing REDIS_URL (required for @rpcbase/worker queue)\")\n }\n return redisUrl\n}\n\nconst getQueueName = (): string => process.env.RB_QUEUE_NAME?.trim() || DEFAULT_QUEUE_NAME\n\nconst getConcurrency = (): number => {\n const raw = process.env.RB_QUEUE_CONCURRENCY?.trim()\n const value = raw ? Number(raw) : DEFAULT_CONCURRENCY\n if (!Number.isFinite(value) || value <= 0) return DEFAULT_CONCURRENCY\n return Math.floor(value)\n}\n\nconst getConnection = () => ({ url: getRedisUrl() })\n\nconst toError = (error: unknown): Error => {\n if (error instanceof Error) return error\n const message = typeof error === \"string\" ? error : String(error ?? \"unknown error\")\n return new Error(message)\n}\n\nconst runTaskHandler = async (job: Job, handler: TaskHandler<unknown>): Promise<unknown> => {\n return await new Promise((resolve, reject) => {\n const taskDomain = domain.create()\n let settled = false\n\n const settle = (next: (value: unknown) => void, value: unknown) => {\n if (settled) return\n settled = true\n taskDomain.removeAllListeners()\n next(value)\n }\n\n taskDomain.on(\"error\", (error: unknown) => {\n const normalizedError = toError(error)\n settle(reject, normalizedError)\n })\n\n taskDomain.run(() => {\n Promise.resolve(handler(job.data, job)).then(\n (result) => settle(resolve, result),\n (error) => settle(reject, toError(error)),\n )\n })\n })\n}\n\nconst ensureQueue = (): Queue => {\n if (queueInstance) return queueInstance\n queueInstance = new Queue(getQueueName(), {\n connection: getConnection(),\n defaultJobOptions: {\n removeOnComplete: DEFAULT_RENTENTION,\n removeOnFail: DEFAULT_RENTENTION\n },\n })\n\n return queueInstance\n}\n\nexport function registerTask<TName extends keyof WorkerTasksMap & string>(\n name: TName,\n handler: TaskHandler<WorkerTasksMap[TName]>,\n): void\nexport function registerTask(name: string, handler: TaskHandler<unknown>): void\nexport function registerTask(name: string, handler: TaskHandler<unknown>): void {\n tasksByName[name] = handler\n}\n\nexport const getTasks = (): Record<string, TaskHandler<unknown>> => tasksByName\n\nexport function add<TName extends keyof WorkerTasksMap & string>(\n taskName: TName,\n payload: WorkerTasksMap[TName],\n options?: JobOptions,\n): Promise<Job>\nexport function add(taskName: string, payload: unknown, options?: JobOptions): Promise<Job>\nexport function add(taskName: string, payload: unknown, options?: JobOptions): Promise<Job> {\n return ensureQueue().add(taskName, payload, options)\n}\n\nexport const getJob = async (jobId: string): Promise<Job | null> => (await ensureQueue().getJob(jobId)) ?? null\n\nexport const getJobs = async (...args: Parameters<Queue[\"getJobs\"]>): Promise<Job[]> =>\n ensureQueue().getJobs(...args) as unknown as Job[]\n\nexport const getInstance = (): Queue => ensureQueue()\n\nexport const start = async (): Promise<Queue> => {\n if (hasStarted) return ensureQueue()\n hasStarted = true\n\n const queue = ensureQueue()\n const concurrency = getConcurrency()\n\n console.log(\"start worker queue\", { queue: queue.name, redisUrl: getRedisUrl(), concurrency })\n\n queue.on(\"error\", (err) => {\n console.log(`queue error: ${err.message}`)\n })\n\n if (!workerInstance) {\n workerInstance = new Worker(\n queue.name,\n async (job) => {\n const taskName = job.name\n\n const handler = tasksByName[taskName]\n if (!handler) {\n throw new Error(`No task registered for '${taskName}'`)\n }\n\n return await runTaskHandler(job, handler)\n },\n {\n concurrency,\n connection: getConnection(),\n },\n )\n }\n\n workerInstance.on(\"error\", (err) => {\n console.log(`worker error: ${err.message}`)\n })\n\n workerInstance.on(\"stalled\", (jobId) => {\n console.log(`job ${jobId} stalled`)\n })\n\n workerInstance.on(\"failed\", (job, err) => {\n console.log(`job ${job?.id ?? \"unknown\"} failed`, err)\n })\n\n await workerInstance.waitUntilReady()\n\n return queue\n}\n\nexport const close = async (): Promise<void> => {\n if (!queueInstance && !workerInstance) return\n try {\n await workerInstance?.close()\n await queueInstance?.close()\n } finally {\n workerInstance = null\n queueInstance = null\n hasStarted = false\n }\n}\n\nexport const getUrl = (): string => getRedisUrl()\n\nexport type ScheduleTaskOptions = Omit<JobOptions, \"repeat\"> & {\n jobId?: string\n repeat: NonNullable<JobOptions[\"repeat\"]>\n}\n\nexport function scheduleTask<TName extends keyof WorkerTasksMap & string>(\n taskName: TName,\n payload: WorkerTasksMap[TName],\n options: ScheduleTaskOptions,\n): Promise<Job>\nexport function scheduleTask(taskName: string, payload: unknown, options: ScheduleTaskOptions): Promise<Job>\nexport async function scheduleTask(taskName: string, payload: unknown, options: ScheduleTaskOptions): Promise<Job> {\n const { jobId, repeat, ...rest } = options\n return add(taskName, payload, {\n ...rest,\n jobId: jobId ?? `schedule|${taskName}`,\n repeat,\n })\n}\n\nconst queueApi = {\n start,\n close,\n registerTask,\n getTasks,\n add,\n scheduleTask,\n getJob,\n getJobs,\n getInstance,\n getUrl,\n}\n\nexport default queueApi\n","import {\n MongoClient,\n type ChangeStream,\n type ChangeStreamDocument,\n type Document,\n type MongoClientOptions,\n} from \"mongodb\"\nimport mongoose from \"mongoose\"\n\nimport queue from \"./queue\"\n\n\nexport type QueueListenerRetryDelays = {\n minMs?: number\n maxMs?: number\n factor?: number\n}\n\nexport type QueueListenerStatus =\n | { state: \"connecting\"; attempt: number }\n | { state: \"ready\" }\n | { state: \"error\"; attempt: number; error: unknown; nextRetryInMs: number }\n | { state: \"failed\"; attempt: number; error: unknown }\n | { state: \"closed\" }\n\nexport type QueueListenerHandle = {\n ready: Promise<void>\n close: () => Promise<void>\n getStatus: () => QueueListenerStatus\n}\n\nexport type QueueListenerOptions = {\n maxRetries?: number | \"infinite\"\n fatalOnMaxRetries?: boolean\n onStateChange?: (status: QueueListenerStatus) => void\n onFatal?: (err: unknown) => void\n retryDelays?: QueueListenerRetryDelays\n mongoClientOptions?: MongoClientOptions\n}\n\nconst RETRY_MAXIMUM_DELAY_MS = 3000\nconst RETRY_MINIMUM_DELAY_MS = 50\nconst RETRY_DEFAULT_FACTOR = 2\n\nconst sleep = async (ms: number, signal?: AbortSignal): Promise<void> => new Promise((resolve) => {\n if (signal?.aborted) {\n resolve()\n return\n }\n\n let timeout: ReturnType<typeof setTimeout> | null = null\n\n const cleanup = () => {\n if (timeout) clearTimeout(timeout)\n signal?.removeEventListener(\"abort\", onAbort)\n }\n\n const onAbort = () => {\n cleanup()\n resolve()\n }\n\n timeout = setTimeout(() => {\n cleanup()\n resolve()\n }, ms)\n timeout.unref?.()\n\n signal?.addEventListener(\"abort\", onAbort)\n})\n\nconst getMongoUrl = (): string => {\n const explicit =\n process.env.MONGODB_URL\n ?? process.env.MONGO_URL\n ?? process.env.MONGODB_URI\n ?? process.env.DB_URL\n\n if (explicit && explicit.trim()) return explicit.trim()\n\n const port = process.env.DB_PORT?.trim()\n if (!port) throw new Error(\"Missing Mongo connection details (MONGODB_URL/MONGO_URL/MONGODB_URI/DB_URL/DB_PORT)\")\n\n const host = process.env.DB_HOST?.trim() || \"localhost\"\n return `mongodb://${host}:${port}`\n}\n\nconst escapeRegex = (value: string): string => value.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")\n\ntype ModelWithCollection = {\n collection?: {\n collectionName?: string\n name?: string\n }\n}\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === \"object\" && value !== null\n\nconst getDocumentId = (doc: unknown): unknown => {\n if (!isRecord(doc)) return undefined\n return doc._id\n}\n\nconst resolveModelNameFromCollection = (collName: string): string | null => {\n const models = mongoose.models\n\n for (const modelName of Object.keys(models)) {\n const model = models[modelName] as ModelWithCollection\n const collectionName =\n model.collection?.collectionName\n ?? model.collection?.name\n\n if (collectionName === collName) {\n return modelName\n }\n }\n\n return null\n}\n\nconst normalizeUpdateDescription = (updateDescription: unknown): unknown => {\n if (!isRecord(updateDescription)) return updateDescription\n if (isRecord(updateDescription.updatedFields)) {\n return {\n ...updateDescription,\n updatedFieldsKeys: Object.keys(updateDescription.updatedFields),\n }\n }\n\n return updateDescription\n}\n\nconst normalizeOpForTaskName = (op: string): string => (op === \"replace\" ? \"update\" : op)\n\nconst getTenantIdFromDbName = (dbName: string, appName: string): string | null => {\n const escapedAppName = escapeRegex(appName)\n const match = dbName.match(new RegExp(`^${escapedAppName}-(.+)-db$`))\n const tenantId = match?.[1]?.trim()\n return tenantId ? tenantId : null\n}\n\nconst dispatchWorkerQueue = async ({\n dbName,\n tenantId,\n modelName,\n op,\n doc,\n updateDescription,\n}: {\n dbName: string\n tenantId: string\n modelName: string\n op: string\n doc: unknown\n updateDescription?: unknown\n}): Promise<void> => {\n const tasks = queue.getTasks()\n const taskOp = normalizeOpForTaskName(op)\n const handlerName = `on-${taskOp}-${modelName}`\n\n if (!tasks[handlerName]) return\n\n const normalizedUpdateDescription = normalizeUpdateDescription(updateDescription)\n\n await queue.add(handlerName, { tenantId, doc, updateDescription: normalizedUpdateDescription }, {\n jobId: `${dbName}|${taskOp}-${getDocumentId(doc) ?? \"unknown\"}`,\n removeOnComplete: true,\n removeOnFail: true,\n })\n}\n\nconst shouldSkipCollection = (collName: string): boolean =>\n collName.endsWith(\".files\") || collName.endsWith(\".chunks\")\n\nconst INTERNAL_IGNORED_MODEL_NAMES = new Set([\"RBRtsChange\", \"RBRtsCounter\"])\n\nconst INTERNAL_IGNORED_COLLECTION_NAMES = new Set([\"rtschanges\", \"rtscounters\"])\n\nconst normalizeRetryDelays = (input: QueueListenerRetryDelays | undefined): Required<QueueListenerRetryDelays> => {\n const minMs = Math.max(0, Math.floor(input?.minMs ?? RETRY_MINIMUM_DELAY_MS))\n const rawMaxMs = Math.max(0, Math.floor(input?.maxMs ?? RETRY_MAXIMUM_DELAY_MS))\n const maxMs = Math.max(minMs, rawMaxMs)\n const factor = Math.max(1, Number.isFinite(input?.factor) ? (input?.factor ?? RETRY_DEFAULT_FACTOR) : RETRY_DEFAULT_FACTOR)\n return { minMs, maxMs, factor }\n}\n\nconst getRetryDelayMs = (attempt: number, delays: Required<QueueListenerRetryDelays>): number =>\n Math.min(delays.maxMs, Math.round(delays.minMs * Math.pow(delays.factor, Math.max(0, attempt - 1))))\n\nconst normalizeMaxRetries = (value: number | \"infinite\"): number | \"infinite\" => {\n if (value === \"infinite\") return value\n const parsed = Math.floor(value)\n return Number.isFinite(parsed) ? Math.max(0, parsed) : 0\n}\n\nexport const registerQueueListener = async (options: QueueListenerOptions = {}): Promise<QueueListenerHandle> => {\n const maxRetries = normalizeMaxRetries(options.maxRetries ?? \"infinite\")\n const fatalOnMaxRetries = options.fatalOnMaxRetries ?? false\n const retryDelays = normalizeRetryDelays(options.retryDelays)\n\n const appName = process.env.APP_NAME?.trim()\n if (!appName) {\n throw new Error(\"Missing APP_NAME (required to configure the worker DB change listener)\")\n }\n\n const mongoUrl = getMongoUrl()\n const mongoClientOptions: MongoClientOptions = {\n family: 4,\n serverSelectionTimeoutMS: 2000,\n connectTimeoutMS: 2000,\n ...options.mongoClientOptions,\n }\n\n let stopped = false\n const abortController = new AbortController()\n let client: MongoClient | null = null\n let stream: ChangeStream<Document> | null = null\n let resumeAfter: Document | null = null\n let processing = Promise.resolve()\n\n let status: QueueListenerStatus = { state: \"connecting\", attempt: 1 }\n const setStatus = (next: QueueListenerStatus): void => {\n status = next\n try {\n options.onStateChange?.(next)\n } catch (err) {\n console.warn(\"queue listener onStateChange failed\", err)\n }\n }\n\n let readySettled = false\n let readyResolve: (() => void) | null = null\n let readyReject: ((err: unknown) => void) | null = null\n const ready = new Promise<void>((resolve, reject) => {\n readyResolve = resolve\n readyReject = reject\n })\n\n const resolveReady = (): void => {\n if (readySettled) return\n readySettled = true\n readyResolve?.()\n }\n\n const rejectReady = (err: unknown): void => {\n if (readySettled) return\n readySettled = true\n readyReject?.(err)\n }\n\n const isChangeStreamHistoryLost = (err: unknown): boolean => {\n const maybeErr = err as { code?: unknown; codeName?: unknown }\n const code = typeof maybeErr.code === \"number\" ? maybeErr.code : null\n const codeName = typeof maybeErr.codeName === \"string\" ? maybeErr.codeName : \"\"\n const message = err instanceof Error ? err.message : String(err ?? \"\")\n\n return (\n code === 286\n || codeName === \"ChangeStreamHistoryLost\"\n || message.includes(\"ChangeStreamHistoryLost\")\n || message.includes(\"resume token\")\n || message.includes(\"Resume token\")\n || message.includes(\"resume of change stream was not possible\")\n || message.includes(\"cannot resume\")\n )\n }\n\n const close = async (): Promise<void> => {\n stopped = true\n abortController.abort()\n try {\n stream?.removeAllListeners()\n await stream?.close()\n } catch {\n // ignore\n }\n stream = null\n\n try {\n await client?.close()\n } catch {\n // ignore\n }\n client = null\n\n if (!readySettled) {\n rejectReady(new Error(\"queue listener closed before ready\"))\n }\n\n setStatus({ state: \"closed\" })\n }\n\n const closeResources = async (): Promise<void> => {\n try {\n stream?.removeAllListeners()\n await stream?.close()\n } catch {\n // ignore\n }\n stream = null\n\n try {\n await client?.close()\n } catch {\n // ignore\n }\n client = null\n }\n\n const startStream = async (): Promise<{ stoppedPromise: Promise<{ reason: \"close\" | \"error\"; error?: unknown }> }> => {\n if (stopped) return { stoppedPromise: Promise.resolve({ reason: \"close\" }) }\n\n if (stream) {\n try {\n stream.removeAllListeners()\n await stream.close()\n } catch {\n // ignore\n }\n stream = null\n }\n\n if (client) {\n try {\n await client.close()\n } catch {\n // ignore\n }\n client = null\n }\n\n client = new MongoClient(mongoUrl, mongoClientOptions)\n\n await client.connect()\n\n const dbMatch = { \"ns.db\": { $regex: `^${escapeRegex(appName)}-.*-db$` } }\n\n const pipeline: Document[] = [\n { $match: dbMatch },\n {\n $match: {\n operationType: { $in: [\"insert\", \"update\", \"replace\", \"delete\"] },\n }\n },\n { $match: { \"ns.coll\": { $nin: Array.from(INTERNAL_IGNORED_COLLECTION_NAMES) } } },\n ]\n\n stream = client.watch(pipeline, {\n fullDocument: \"updateLookup\",\n ...(resumeAfter ? { resumeAfter } : {}),\n })\n\n const stoppedPromise = new Promise<{ reason: \"close\" | \"error\"; error?: unknown }>((resolve) => {\n let settled = false\n const onAbort = () => settle({ reason: \"close\" })\n\n const settle = (result: { reason: \"close\" | \"error\"; error?: unknown }) => {\n if (settled) return\n settled = true\n abortController.signal.removeEventListener(\"abort\", onAbort)\n resolve(result)\n }\n\n abortController.signal.addEventListener(\"abort\", onAbort)\n stream?.once(\"close\", () => settle({ reason: \"close\" }))\n stream?.once(\"error\", (err) => settle({ reason: \"error\", error: err }))\n })\n\n stream.on(\"change\", (change: ChangeStreamDocument<Document>) => {\n const streamRef = stream\n processing = processing.then(async () => {\n const ns = \"ns\" in change ? change.ns : undefined\n const dbName = String(ns?.db ?? \"\")\n if (!dbName) return\n const tenantId = getTenantIdFromDbName(dbName, appName)\n if (!tenantId) return\n\n const collName = String(ns && \"coll\" in ns ? ns.coll : \"\")\n if (!collName) return\n if (shouldSkipCollection(collName)) return\n if (INTERNAL_IGNORED_COLLECTION_NAMES.has(collName)) return\n\n const modelName = resolveModelNameFromCollection(collName)\n if (!modelName) return\n if (INTERNAL_IGNORED_MODEL_NAMES.has(modelName)) return\n\n const op = String(change.operationType ?? \"\")\n if (!op) return\n const normalizedOp = normalizeOpForTaskName(op)\n\n let doc = \"fullDocument\" in change ? change.fullDocument : undefined\n if (!doc && normalizedOp === \"delete\") {\n doc = \"documentKey\" in change ? change.documentKey : undefined\n }\n if (!doc) return\n\n const updateDescription = \"updateDescription\" in change ? change.updateDescription : undefined\n\n try {\n await dispatchWorkerQueue({\n dbName,\n tenantId,\n modelName,\n op: normalizedOp,\n doc,\n updateDescription,\n })\n\n resumeAfter = change?._id ?? resumeAfter\n } catch (err) {\n console.warn(\"queue listener failed to dispatch change\", err)\n try {\n await streamRef?.close()\n } catch {\n // ignore\n }\n }\n }).catch((err) => {\n console.warn(\"queue listener change handler failed\", err)\n })\n })\n\n stream.on(\"error\", (err) => {\n if (stopped) return\n if (resumeAfter && isChangeStreamHistoryLost(err)) {\n resumeAfter = null\n }\n try {\n void Promise.resolve(stream?.close()).catch(() => {})\n } catch {\n // ignore\n }\n })\n\n return { stoppedPromise }\n }\n\n const run = async (): Promise<void> => {\n let retryCounter = 0\n\n while (!stopped) {\n try {\n setStatus({ state: \"connecting\", attempt: retryCounter + 1 })\n\n const { stoppedPromise } = await startStream()\n retryCounter = 0\n setStatus({ state: \"ready\" })\n resolveReady()\n\n const end = await stoppedPromise\n if (stopped) return\n\n retryCounter += 1\n if (maxRetries !== \"infinite\" && retryCounter > maxRetries) {\n const err = end.reason === \"error\" ? end.error : new Error(\"queue listener closed\")\n setStatus({ state: \"failed\", attempt: retryCounter, error: err })\n if (fatalOnMaxRetries) {\n try {\n options.onFatal?.(err)\n } catch (fatalErr) {\n console.warn(\"queue listener onFatal failed\", fatalErr)\n }\n }\n rejectReady(err)\n abortController.abort()\n await closeResources()\n return\n }\n\n const delayMs = getRetryDelayMs(retryCounter, retryDelays)\n const err = end.reason === \"error\" ? end.error : new Error(\"queue listener closed\")\n console.warn(\"queue listener not ready, retrying in\", delayMs, err)\n setStatus({ state: \"error\", attempt: retryCounter, error: err, nextRetryInMs: delayMs })\n await closeResources()\n await sleep(delayMs, abortController.signal)\n } catch (err) {\n if (stopped) return\n\n retryCounter += 1\n if (maxRetries !== \"infinite\" && retryCounter > maxRetries) {\n setStatus({ state: \"failed\", attempt: retryCounter, error: err })\n if (fatalOnMaxRetries) {\n try {\n options.onFatal?.(err)\n } catch (fatalErr) {\n console.warn(\"queue listener onFatal failed\", fatalErr)\n }\n }\n rejectReady(err)\n abortController.abort()\n await closeResources()\n return\n }\n\n const delayMs = getRetryDelayMs(retryCounter, retryDelays)\n console.warn(\"queue listener not ready, retrying in\", delayMs, err)\n setStatus({ state: \"error\", attempt: retryCounter, error: err, nextRetryInMs: delayMs })\n await closeResources()\n await sleep(delayMs, abortController.signal)\n }\n }\n }\n\n const handle: QueueListenerHandle = {\n ready,\n close,\n getStatus: () => status,\n }\n\n void run().catch(async (err) => {\n if (stopped) return\n setStatus({ state: \"failed\", attempt: 0, error: err })\n rejectReady(err)\n await closeResources()\n })\n\n return handle\n}\n","export type DbEventOp = \"insert\" | \"update\" | \"delete\"\n\nexport type DbEventTaskName<TOp extends DbEventOp = DbEventOp, TModelName extends string = string> =\n `on-${TOp}-${TModelName}`\n\nexport type DbEventTaskPayload<TDoc = unknown, TUpdateDescription = unknown> = {\n tenantId: string\n doc: TDoc\n updateDescription?: TUpdateDescription\n}\n\nexport const dbEventTaskName = <TOp extends DbEventOp, TModelName extends string>(\n op: TOp,\n modelName: TModelName,\n): DbEventTaskName<TOp, TModelName> => `on-${op}-${modelName}`\n"],"mappings":"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"}
|