@treatwell/moleculer-essentials 1.0.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/LICENSE +21 -0
- package/README.md +1 -0
- package/dist/context-factory-BWO3xPWE.d.cts +520 -0
- package/dist/context-factory-BWO3xPWE.d.mts +520 -0
- package/dist/index-82e1CXJX.cjs +11 -0
- package/dist/index-BV1ZqQrU.mjs +351 -0
- package/dist/index-DNJWwcZu.mjs +8 -0
- package/dist/index-rZl77S1z.cjs +375 -0
- package/dist/index.cjs +1570 -0
- package/dist/index.d.cts +373 -0
- package/dist/index.d.mts +373 -0
- package/dist/index.mjs +1535 -0
- package/dist/mixins/database.mixin.cjs +1673 -0
- package/dist/mixins/database.mixin.d.cts +958 -0
- package/dist/mixins/database.mixin.d.mts +958 -0
- package/dist/mixins/database.mixin.mjs +1645 -0
- package/dist/mixins/encryptor.mixin.cjs +84 -0
- package/dist/mixins/encryptor.mixin.d.cts +31 -0
- package/dist/mixins/encryptor.mixin.d.mts +31 -0
- package/dist/mixins/encryptor.mixin.mjs +81 -0
- package/dist/mixins/global-store.mixin.cjs +56 -0
- package/dist/mixins/global-store.mixin.d.cts +39 -0
- package/dist/mixins/global-store.mixin.d.mts +39 -0
- package/dist/mixins/global-store.mixin.mjs +54 -0
- package/dist/mixins/jwt.mixin.cjs +118 -0
- package/dist/mixins/jwt.mixin.d.cts +43 -0
- package/dist/mixins/jwt.mixin.d.mts +43 -0
- package/dist/mixins/jwt.mixin.mjs +115 -0
- package/dist/mixins/queue.mixin.cjs +420 -0
- package/dist/mixins/queue.mixin.d.cts +150 -0
- package/dist/mixins/queue.mixin.d.mts +150 -0
- package/dist/mixins/queue.mixin.mjs +414 -0
- package/dist/mixins/redis.mixin.cjs +50 -0
- package/dist/mixins/redis.mixin.d.cts +27 -0
- package/dist/mixins/redis.mixin.d.mts +27 -0
- package/dist/mixins/redis.mixin.mjs +48 -0
- package/dist/mixins/redlock.mixin.cjs +76 -0
- package/dist/mixins/redlock.mixin.d.cts +30 -0
- package/dist/mixins/redlock.mixin.d.mts +30 -0
- package/dist/mixins/redlock.mixin.mjs +74 -0
- package/package.json +181 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { a as CustomServiceSchema } from '../context-factory-BWO3xPWE.mjs';
|
|
2
|
+
import { Redis } from 'ioredis';
|
|
3
|
+
import { QueueOptions, Queue, JobsOptions, Job, QueueEventsOptions, QueueEvents, QueueBaseOptions, FlowProducer, RepeatOptions, WorkerOptions, Worker } from 'bullmq';
|
|
4
|
+
import { Service } from 'moleculer';
|
|
5
|
+
import 'bson';
|
|
6
|
+
import 'zod/v4';
|
|
7
|
+
import 'ajv/dist/2019.js';
|
|
8
|
+
|
|
9
|
+
type QueueMixinOptions = {
|
|
10
|
+
brokerURL: string | (<TService extends Service = Service>(svc: TService) => Promise<string>);
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* connection is mandatory for BullMQ but is managed by each mixins.
|
|
14
|
+
*/
|
|
15
|
+
type WithoutConnection<T> = Omit<T, 'connection'>;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* This Mixin add the capability to launch a job.
|
|
19
|
+
*/
|
|
20
|
+
declare function QueueClient<N extends string>(queueName: N, opts: WithoutConnection<QueueOptions> & QueueMixinOptions): Partial<CustomServiceSchema<unknown, {
|
|
21
|
+
_getQueues(): Map<string, Queue>;
|
|
22
|
+
/**
|
|
23
|
+
* Add a job to a specific queue. The queue name needs to be passed as parameters
|
|
24
|
+
* to enable multiple queue clients in the same service.
|
|
25
|
+
*/
|
|
26
|
+
addJob<D = unknown>(qName: N, name: string, data: D, jOpts?: JobsOptions): Promise<Job>;
|
|
27
|
+
/**
|
|
28
|
+
* Add jobs to a specific queue. The queue name needs to be passed as parameters
|
|
29
|
+
* to enable multiple queue clients in the same service.
|
|
30
|
+
*/
|
|
31
|
+
addBulkJob<D = unknown>(qName: N, name: string, data: D[], jOpts?: JobsOptions): Promise<Job[]>;
|
|
32
|
+
/**
|
|
33
|
+
* Get a queue bundle if you need specific features
|
|
34
|
+
*/
|
|
35
|
+
getQueue(qName: N): Queue;
|
|
36
|
+
}, Partial<CustomServiceSchema<unknown, {
|
|
37
|
+
getStore(storeName: string): Map<string, {
|
|
38
|
+
services: Set<unknown>;
|
|
39
|
+
client: Redis;
|
|
40
|
+
onClose: () => Promise<void> | void;
|
|
41
|
+
}>;
|
|
42
|
+
getFromStore(storeName: string, key: string): Redis | null;
|
|
43
|
+
removeServiceFromStore(storeName: string, key: string): Promise<boolean>;
|
|
44
|
+
setClientToStore(storeName: string, key: string, client: Redis, onClose: () => Promise<void> | void): void;
|
|
45
|
+
}, unknown, unknown>>[], unknown>>;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* This Mixin add the capability to wait on a job.
|
|
49
|
+
* WARNING: Needs to be used with QueueClient mixin.
|
|
50
|
+
*/
|
|
51
|
+
declare function QueueEventsClient<N extends string>(queueName: N, opts: WithoutConnection<QueueEventsOptions> & QueueMixinOptions): Partial<CustomServiceSchema<unknown, {
|
|
52
|
+
_getQueueEvents(): Map<string, {
|
|
53
|
+
events: QueueEvents;
|
|
54
|
+
running: boolean;
|
|
55
|
+
}>;
|
|
56
|
+
/**
|
|
57
|
+
* Return the QueueEvents instance for the given queue name.
|
|
58
|
+
* Will run the QueueEvents if it's not already running (lazy connect).
|
|
59
|
+
* Will throw an error if the QueueEvents is not found.
|
|
60
|
+
*/
|
|
61
|
+
getQueueEvents(qName: N): QueueEvents;
|
|
62
|
+
/**
|
|
63
|
+
* Same as addJob but will also wait for the job to finish before returning.
|
|
64
|
+
*/
|
|
65
|
+
addAndWait<T = unknown, D = unknown>(qName: N, name: string, data: D, jOpts?: JobsOptions, ttl?: number): Promise<T>;
|
|
66
|
+
/**
|
|
67
|
+
* Same as addBulkJob but will also wait for the jobs to finish before returning.
|
|
68
|
+
*/
|
|
69
|
+
addBulkAndWait<T = unknown, D = unknown>(qName: N, name: string, data: D[], jOpts?: JobsOptions, ttl?: number): Promise<T[]>;
|
|
70
|
+
}, Partial<CustomServiceSchema<unknown, {
|
|
71
|
+
getStore(storeName: string): Map<string, {
|
|
72
|
+
services: Set<unknown>;
|
|
73
|
+
client: Redis;
|
|
74
|
+
onClose: () => Promise<void> | void;
|
|
75
|
+
}>;
|
|
76
|
+
getFromStore(storeName: string, key: string): Redis | null;
|
|
77
|
+
removeServiceFromStore(storeName: string, key: string): Promise<boolean>;
|
|
78
|
+
setClientToStore(storeName: string, key: string, client: Redis, onClose: () => Promise<void> | void): void;
|
|
79
|
+
}, unknown, unknown>>[], unknown>>;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* This Mixin add the capability to launch a BullMQ flows.
|
|
83
|
+
*/
|
|
84
|
+
declare function QueueFlowProducerMixin(opts: WithoutConnection<QueueBaseOptions> & QueueMixinOptions): Partial<CustomServiceSchema<unknown, {
|
|
85
|
+
getFlowProducer(): FlowProducer;
|
|
86
|
+
}, unknown, unknown>>;
|
|
87
|
+
|
|
88
|
+
type RepeatableJob = {
|
|
89
|
+
name: string;
|
|
90
|
+
data?: unknown;
|
|
91
|
+
} & RepeatOptions;
|
|
92
|
+
type QueueStaticRepeatableJobsOptions = {
|
|
93
|
+
/**
|
|
94
|
+
* Set to `false` to prevent deleting repeatable jobs not registered.
|
|
95
|
+
*/
|
|
96
|
+
autoRemove?: boolean;
|
|
97
|
+
};
|
|
98
|
+
/**
|
|
99
|
+
* This Mixin allow to register repeatable jobs and remove other ones.
|
|
100
|
+
* It should mainly be setup on the same service as the related QueueWorker.
|
|
101
|
+
*/
|
|
102
|
+
declare function QueueStaticRepeatableJobs(queueName: string, jobs: RepeatableJob[], opts: QueueStaticRepeatableJobsOptions & QueueMixinOptions): Partial<CustomServiceSchema<unknown, {
|
|
103
|
+
registerRepeatableJobs(): Promise<void>;
|
|
104
|
+
}, Partial<CustomServiceSchema<unknown, {
|
|
105
|
+
getStore(storeName: string): Map<string, {
|
|
106
|
+
services: Set<unknown>;
|
|
107
|
+
client: Redis;
|
|
108
|
+
onClose: () => Promise<void> | void;
|
|
109
|
+
}>;
|
|
110
|
+
getFromStore(storeName: string, key: string): Redis | null;
|
|
111
|
+
removeServiceFromStore(storeName: string, key: string): Promise<boolean>;
|
|
112
|
+
setClientToStore(storeName: string, key: string, client: Redis, onClose: () => Promise<void> | void): void;
|
|
113
|
+
}, unknown, unknown>>[], unknown>>;
|
|
114
|
+
|
|
115
|
+
type JobMeta = {
|
|
116
|
+
job?: Job;
|
|
117
|
+
jobWorkerToken?: string;
|
|
118
|
+
};
|
|
119
|
+
type JobProcessorOptions = {
|
|
120
|
+
/**
|
|
121
|
+
* Use the job name as the action name.
|
|
122
|
+
* When false (default), the action name is 'processor'.
|
|
123
|
+
*/
|
|
124
|
+
useNamedFunctions?: boolean;
|
|
125
|
+
/**
|
|
126
|
+
* Skip normal info logs at the start and end of the job.
|
|
127
|
+
* Will never disable logs on errors.
|
|
128
|
+
*/
|
|
129
|
+
skipNormalLogs?: boolean;
|
|
130
|
+
/**
|
|
131
|
+
* Allow to log additional fields from the job data.
|
|
132
|
+
* Currently, doesn't support nested fields.
|
|
133
|
+
*/
|
|
134
|
+
logDataFields?: string[];
|
|
135
|
+
/**
|
|
136
|
+
* Optional wrapper that MUST call the passed function.
|
|
137
|
+
*/
|
|
138
|
+
wrapper?: (job: Job, token: string | undefined, fn: (job: Job, token?: string) => Promise<unknown>) => Promise<unknown>;
|
|
139
|
+
};
|
|
140
|
+
/**
|
|
141
|
+
* This Mixin create the worker system.
|
|
142
|
+
*/
|
|
143
|
+
declare function QueueWorker(queueName: string, opts: WithoutConnection<WorkerOptions> & QueueMixinOptions, processorOptions?: JobProcessorOptions): Partial<CustomServiceSchema<unknown, {
|
|
144
|
+
getWorker(): Worker;
|
|
145
|
+
processJob(job: Job, token?: string): Promise<any>;
|
|
146
|
+
_processJob(job: Job, token?: string): Promise<any>;
|
|
147
|
+
}, unknown, unknown>>;
|
|
148
|
+
|
|
149
|
+
export { QueueClient, QueueEventsClient, QueueFlowProducerMixin, QueueStaticRepeatableJobs, QueueWorker };
|
|
150
|
+
export type { JobMeta, JobProcessorOptions, QueueMixinOptions, QueueStaticRepeatableJobsOptions, RepeatableJob };
|
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
import { Errors } from 'moleculer';
|
|
2
|
+
import { Queue, QueueEvents, FlowProducer, Worker } from 'bullmq';
|
|
3
|
+
import { isFunction } from 'lodash';
|
|
4
|
+
import { w as wrapMixin } from '../index-DNJWwcZu.mjs';
|
|
5
|
+
import { GlobalStoreMixin } from './global-store.mixin.mjs';
|
|
6
|
+
import { Redis } from 'ioredis';
|
|
7
|
+
import { parseURL } from 'ioredis/built/utils';
|
|
8
|
+
|
|
9
|
+
function createRedisConnection(url) {
|
|
10
|
+
let tls;
|
|
11
|
+
if (url.startsWith("rediss://")) {
|
|
12
|
+
tls = {};
|
|
13
|
+
}
|
|
14
|
+
const connection = new Redis({
|
|
15
|
+
...parseURL(url),
|
|
16
|
+
tls,
|
|
17
|
+
maxRetriesPerRequest: null,
|
|
18
|
+
connectTimeout: 1e3 * 60
|
|
19
|
+
});
|
|
20
|
+
connection.setMaxListeners(0);
|
|
21
|
+
return connection;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function QueueClient(queueName, opts) {
|
|
25
|
+
const kKey = Symbol("Queue Client Key");
|
|
26
|
+
return wrapMixin({
|
|
27
|
+
mixins: [GlobalStoreMixin()],
|
|
28
|
+
methods: {
|
|
29
|
+
_getQueues() {
|
|
30
|
+
return this.$queues;
|
|
31
|
+
},
|
|
32
|
+
/**
|
|
33
|
+
* Add a job to a specific queue. The queue name needs to be passed as parameters
|
|
34
|
+
* to enable multiple queue clients in the same service.
|
|
35
|
+
*/
|
|
36
|
+
addJob(qName, name, data, jOpts) {
|
|
37
|
+
const q = this._getQueues().get(qName);
|
|
38
|
+
if (!q) {
|
|
39
|
+
throw new Errors.MoleculerServerError(
|
|
40
|
+
`Queue '${qName}' does not exists on '${this.name}'`
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
return q.add(name, data, jOpts);
|
|
44
|
+
},
|
|
45
|
+
/**
|
|
46
|
+
* Add jobs to a specific queue. The queue name needs to be passed as parameters
|
|
47
|
+
* to enable multiple queue clients in the same service.
|
|
48
|
+
*/
|
|
49
|
+
addBulkJob(qName, name, data, jOpts) {
|
|
50
|
+
const q = this._getQueues().get(qName);
|
|
51
|
+
if (!q) {
|
|
52
|
+
throw new Errors.MoleculerServerError(
|
|
53
|
+
`Queue '${qName}' does not exists on '${this.name}'`
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
return q.addBulk(data.map((d) => ({ name, data: d, opts: jOpts })));
|
|
57
|
+
},
|
|
58
|
+
/**
|
|
59
|
+
* Get a queue bundle if you need specific features
|
|
60
|
+
*/
|
|
61
|
+
getQueue(qName) {
|
|
62
|
+
const q = this._getQueues().get(qName);
|
|
63
|
+
if (!q) {
|
|
64
|
+
throw new Errors.MoleculerServerError(
|
|
65
|
+
`Queue '${qName}' does not exists on '${this.name}'`
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
return q;
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
created() {
|
|
72
|
+
if (!this.$queues) {
|
|
73
|
+
this.$queues = /* @__PURE__ */ new Map();
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
async started() {
|
|
77
|
+
if (this._getQueues().has(queueName)) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
`Queue client '${queueName}' already exists on service '${this.name}'`
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
const key = isFunction(opts.brokerURL) ? await opts.brokerURL(this) : opts.brokerURL;
|
|
83
|
+
this[kKey] = key;
|
|
84
|
+
let connection = this.getFromStore("redis", key);
|
|
85
|
+
if (!connection) {
|
|
86
|
+
connection = createRedisConnection(key);
|
|
87
|
+
this.setClientToStore(
|
|
88
|
+
"redis",
|
|
89
|
+
key,
|
|
90
|
+
connection,
|
|
91
|
+
() => connection?.disconnect()
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
const queue = new Queue(queueName, { connection, ...opts });
|
|
95
|
+
queue.setMaxListeners(0);
|
|
96
|
+
queue.on(
|
|
97
|
+
"error",
|
|
98
|
+
(err) => this.logger.error(`Queue ${queueName} got error`, { err })
|
|
99
|
+
);
|
|
100
|
+
this._getQueues().set(queueName, queue);
|
|
101
|
+
},
|
|
102
|
+
async stopped() {
|
|
103
|
+
const q = this._getQueues().get(queueName);
|
|
104
|
+
if (q) {
|
|
105
|
+
await q.close();
|
|
106
|
+
}
|
|
107
|
+
await this.removeServiceFromStore("redis", this[kKey]);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function QueueEventsClient(queueName, opts) {
|
|
113
|
+
const kKey = Symbol("Queue Events Client Key");
|
|
114
|
+
return wrapMixin({
|
|
115
|
+
mixins: [GlobalStoreMixin()],
|
|
116
|
+
methods: {
|
|
117
|
+
_getQueueEvents() {
|
|
118
|
+
return this.$queuesEvents;
|
|
119
|
+
},
|
|
120
|
+
/**
|
|
121
|
+
* Return the QueueEvents instance for the given queue name.
|
|
122
|
+
* Will run the QueueEvents if it's not already running (lazy connect).
|
|
123
|
+
* Will throw an error if the QueueEvents is not found.
|
|
124
|
+
*/
|
|
125
|
+
getQueueEvents(qName) {
|
|
126
|
+
const queueEvents = this._getQueueEvents().get(qName);
|
|
127
|
+
if (!queueEvents) {
|
|
128
|
+
throw new Error(`QueueEvents '${qName}' not found`);
|
|
129
|
+
}
|
|
130
|
+
const qEvents = queueEvents.events;
|
|
131
|
+
if (!queueEvents.running) {
|
|
132
|
+
qEvents.run().catch((error) => qEvents.emit("error", error));
|
|
133
|
+
queueEvents.running = true;
|
|
134
|
+
}
|
|
135
|
+
return qEvents;
|
|
136
|
+
},
|
|
137
|
+
/**
|
|
138
|
+
* Same as addJob but will also wait for the job to finish before returning.
|
|
139
|
+
*/
|
|
140
|
+
async addAndWait(qName, name, data, jOpts, ttl) {
|
|
141
|
+
const job = await this.addJob(qName, name, data, jOpts);
|
|
142
|
+
return job.waitUntilFinished(this.getQueueEvents(qName), ttl);
|
|
143
|
+
},
|
|
144
|
+
/**
|
|
145
|
+
* Same as addBulkJob but will also wait for the jobs to finish before returning.
|
|
146
|
+
*/
|
|
147
|
+
async addBulkAndWait(qName, name, data, jOpts, ttl) {
|
|
148
|
+
const jobs = await this.addBulkJob(qName, name, data, jOpts);
|
|
149
|
+
const q = this.getQueueEvents(qName);
|
|
150
|
+
return Promise.all(jobs.map((j) => j.waitUntilFinished(q, ttl)));
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
created() {
|
|
154
|
+
if (!this.$queuesEvents) {
|
|
155
|
+
this.$queuesEvents = /* @__PURE__ */ new Map();
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
async started() {
|
|
159
|
+
if (this._getQueueEvents().get(queueName)) {
|
|
160
|
+
throw new Error(
|
|
161
|
+
`Queue client '${queueName}' already exists on service '${this.name}'`
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
const key = isFunction(opts.brokerURL) ? await opts.brokerURL(this) : opts.brokerURL;
|
|
165
|
+
this[kKey] = key;
|
|
166
|
+
let connection = this.getFromStore("redis", key);
|
|
167
|
+
if (!connection) {
|
|
168
|
+
connection = createRedisConnection(key);
|
|
169
|
+
this.setClientToStore(
|
|
170
|
+
"redis",
|
|
171
|
+
key,
|
|
172
|
+
connection,
|
|
173
|
+
() => connection?.disconnect()
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
const qEvents = new QueueEvents(queueName, {
|
|
177
|
+
connection,
|
|
178
|
+
...opts,
|
|
179
|
+
autorun: false
|
|
180
|
+
});
|
|
181
|
+
qEvents.setMaxListeners(0);
|
|
182
|
+
qEvents.on(
|
|
183
|
+
"error",
|
|
184
|
+
(err) => this.logger.error(`QueueEvents on ${queueName} got error`, { err })
|
|
185
|
+
);
|
|
186
|
+
this._getQueueEvents().set(queueName, {
|
|
187
|
+
events: qEvents,
|
|
188
|
+
running: false
|
|
189
|
+
});
|
|
190
|
+
},
|
|
191
|
+
async stopped() {
|
|
192
|
+
const q = this._getQueueEvents().get(queueName);
|
|
193
|
+
if (q) {
|
|
194
|
+
await q.events.close();
|
|
195
|
+
}
|
|
196
|
+
await this.removeServiceFromStore("redis", this[kKey]);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const kConnection$1 = Symbol("Queue FlowProducer Connection");
|
|
202
|
+
function QueueFlowProducerMixin(opts) {
|
|
203
|
+
return wrapMixin({
|
|
204
|
+
methods: {
|
|
205
|
+
getFlowProducer() {
|
|
206
|
+
return this.$flowProducer;
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
async started() {
|
|
210
|
+
if (this.$flowProducer) {
|
|
211
|
+
throw new Error(
|
|
212
|
+
`Queue Flow producer already exists on service '${this.name}'`
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
const url = isFunction(opts.brokerURL) ? await opts.brokerURL(this) : opts.brokerURL;
|
|
216
|
+
const connection = createRedisConnection(url);
|
|
217
|
+
this.$flowProducer = new FlowProducer({ connection, ...opts });
|
|
218
|
+
this[kConnection$1] = connection;
|
|
219
|
+
},
|
|
220
|
+
async stopped() {
|
|
221
|
+
await this.$flowProducer?.close();
|
|
222
|
+
this[kConnection$1]?.disconnect();
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function isPropertyEqual(a, b) {
|
|
228
|
+
if (a && b && a === b) {
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
return !a && !b;
|
|
232
|
+
}
|
|
233
|
+
function isSameRepeatableJob(repeatableJob, job) {
|
|
234
|
+
return isPropertyEqual(repeatableJob.name, job.name) && isPropertyEqual(repeatableJob.pattern, job.pattern) && isPropertyEqual(repeatableJob.every, job.every?.toString()) && isPropertyEqual(repeatableJob.tz, job.tz);
|
|
235
|
+
}
|
|
236
|
+
function getFilteredJobs(qName, jobs) {
|
|
237
|
+
const allowList = new Set(
|
|
238
|
+
process.env.SCHEDULER_JOBS_ALLOWLIST?.split(",") || []
|
|
239
|
+
);
|
|
240
|
+
const denyList = new Set(
|
|
241
|
+
process.env.SCHEDULER_JOBS_DENYLIST?.split(",") || []
|
|
242
|
+
);
|
|
243
|
+
return jobs.filter((j) => {
|
|
244
|
+
if (allowList.size > 0) {
|
|
245
|
+
return allowList.has(`${qName}:${j.name}`);
|
|
246
|
+
}
|
|
247
|
+
if (denyList.size > 0) {
|
|
248
|
+
return !denyList.has(`${qName}:${j.name}`);
|
|
249
|
+
}
|
|
250
|
+
return true;
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
const mixinStore = Symbol("QueueStaticRepeatableJobsMixinStore");
|
|
254
|
+
function QueueStaticRepeatableJobs(queueName, jobs, opts) {
|
|
255
|
+
const kKey = Symbol("Queue Static Repeatable Jobs Key");
|
|
256
|
+
return wrapMixin({
|
|
257
|
+
mixins: [GlobalStoreMixin()],
|
|
258
|
+
methods: {
|
|
259
|
+
async registerRepeatableJobs() {
|
|
260
|
+
const key = isFunction(opts.brokerURL) ? await opts.brokerURL(this) : opts.brokerURL;
|
|
261
|
+
this[kKey] = key;
|
|
262
|
+
let connection = this.getFromStore("redis", key);
|
|
263
|
+
if (!connection) {
|
|
264
|
+
connection = createRedisConnection(key);
|
|
265
|
+
this.setClientToStore(
|
|
266
|
+
"redis",
|
|
267
|
+
key,
|
|
268
|
+
connection,
|
|
269
|
+
() => connection?.disconnect()
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
const queue = new Queue(queueName, { connection });
|
|
273
|
+
const filteredJobs = getFilteredJobs(queueName, jobs);
|
|
274
|
+
await Promise.all(
|
|
275
|
+
filteredJobs.map(
|
|
276
|
+
({ name, data, ...repeat }) => queue.add(name, data, { repeat, removeOnComplete: true })
|
|
277
|
+
)
|
|
278
|
+
);
|
|
279
|
+
if (opts.autoRemove !== false) {
|
|
280
|
+
const repeatableJobs = await queue.getRepeatableJobs();
|
|
281
|
+
const rjToRemove = repeatableJobs.filter(
|
|
282
|
+
(rj) => !filteredJobs.some((j) => isSameRepeatableJob(rj, j))
|
|
283
|
+
);
|
|
284
|
+
if (rjToRemove.length) {
|
|
285
|
+
await Promise.all(
|
|
286
|
+
rjToRemove.map((rj) => queue.removeRepeatableByKey(rj.key))
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
await queue.close();
|
|
291
|
+
await this.removeServiceFromStore("redis", key);
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
created() {
|
|
295
|
+
if (!this[mixinStore]) {
|
|
296
|
+
this[mixinStore] = /* @__PURE__ */ new Set();
|
|
297
|
+
}
|
|
298
|
+
if (this[mixinStore].has(this[kKey])) {
|
|
299
|
+
throw new Error(
|
|
300
|
+
`Queue ${queueName} has already registered static repeatable jobs`
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
this[mixinStore].add(queueName);
|
|
304
|
+
},
|
|
305
|
+
events: {
|
|
306
|
+
// TODO Improve this to only be run once in a while globally on the cluster
|
|
307
|
+
"$broker.started": {
|
|
308
|
+
async handler() {
|
|
309
|
+
await this.registerRepeatableJobs();
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const kConnection = Symbol("Queue Worker Connection");
|
|
317
|
+
function QueueWorker(queueName, opts, processorOptions = {}) {
|
|
318
|
+
return wrapMixin({
|
|
319
|
+
metadata: {
|
|
320
|
+
worker: true,
|
|
321
|
+
[`worker-${queueName}`]: true
|
|
322
|
+
},
|
|
323
|
+
methods: {
|
|
324
|
+
getWorker() {
|
|
325
|
+
return this.$worker;
|
|
326
|
+
},
|
|
327
|
+
async processJob(job, token) {
|
|
328
|
+
if (processorOptions.wrapper) {
|
|
329
|
+
return processorOptions.wrapper(
|
|
330
|
+
job,
|
|
331
|
+
token,
|
|
332
|
+
this._processJob.bind(this)
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
return this._processJob(job, token);
|
|
336
|
+
},
|
|
337
|
+
async _processJob(job, token) {
|
|
338
|
+
const methodName = processorOptions.useNamedFunctions ? job.name : "processor";
|
|
339
|
+
if (!this.actions[methodName]) {
|
|
340
|
+
throw new Error(`action '${methodName}' does not exist.`);
|
|
341
|
+
}
|
|
342
|
+
const logBase = {
|
|
343
|
+
msg: `Job ${job.name} on ${queueName} (${job.id})`,
|
|
344
|
+
name: job.name,
|
|
345
|
+
id: job.id
|
|
346
|
+
};
|
|
347
|
+
if (processorOptions.logDataFields) {
|
|
348
|
+
processorOptions.logDataFields.forEach((field) => {
|
|
349
|
+
logBase[field] = job.data?.[field];
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
if (processorOptions.skipNormalLogs !== true) {
|
|
353
|
+
this.logger.info(logBase);
|
|
354
|
+
}
|
|
355
|
+
const start = Date.now();
|
|
356
|
+
try {
|
|
357
|
+
const res = await this.actions[methodName](job.data, {
|
|
358
|
+
meta: { job, jobWorkerToken: token }
|
|
359
|
+
});
|
|
360
|
+
if (processorOptions.skipNormalLogs !== true) {
|
|
361
|
+
this.logger.info({
|
|
362
|
+
...logBase,
|
|
363
|
+
msg: `${logBase.msg} succeeded`,
|
|
364
|
+
duration: Date.now() - start
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
return res;
|
|
368
|
+
} catch (err) {
|
|
369
|
+
this.logger.info({
|
|
370
|
+
...logBase,
|
|
371
|
+
msg: `${logBase.msg} failed`,
|
|
372
|
+
duration: Date.now() - start,
|
|
373
|
+
err
|
|
374
|
+
});
|
|
375
|
+
throw err;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
},
|
|
379
|
+
async started() {
|
|
380
|
+
if (this.$worker) {
|
|
381
|
+
throw new Error(
|
|
382
|
+
`A QueueWorker mixin was already on service '${this.name}'`
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
const url = isFunction(opts.brokerURL) ? await opts.brokerURL(this) : opts.brokerURL;
|
|
386
|
+
const connection = createRedisConnection(url);
|
|
387
|
+
const worker = new Worker(queueName, this.processJob.bind(this), {
|
|
388
|
+
connection,
|
|
389
|
+
...opts,
|
|
390
|
+
autorun: false
|
|
391
|
+
});
|
|
392
|
+
worker.on(
|
|
393
|
+
"error",
|
|
394
|
+
(err) => this.logger.error(`Worker on ${queueName} got error`, { err })
|
|
395
|
+
);
|
|
396
|
+
this.$worker = worker;
|
|
397
|
+
this[kConnection] = connection;
|
|
398
|
+
},
|
|
399
|
+
events: {
|
|
400
|
+
"$broker.started": {
|
|
401
|
+
handler() {
|
|
402
|
+
const worker = this.getWorker();
|
|
403
|
+
worker.run().catch((error) => worker.emit("error", error));
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
},
|
|
407
|
+
async stopped() {
|
|
408
|
+
await this.$worker?.close();
|
|
409
|
+
this[kConnection]?.disconnect();
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
export { QueueClient, QueueEventsClient, QueueFlowProducerMixin, QueueStaticRepeatableJobs, QueueWorker };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var ioredis = require('ioredis');
|
|
4
|
+
var mixins_globalStore_mixin = require('./global-store.mixin.cjs');
|
|
5
|
+
var index = require('../index-82e1CXJX.cjs');
|
|
6
|
+
|
|
7
|
+
function RedisMixin(options, { reuseClient = true, getOptions } = {}) {
|
|
8
|
+
const kKey = Symbol("Redis Key Symbol");
|
|
9
|
+
return index.wrapMixin({
|
|
10
|
+
mixins: [mixins_globalStore_mixin.GlobalStoreMixin()],
|
|
11
|
+
methods: {
|
|
12
|
+
getRedis() {
|
|
13
|
+
return this.redis;
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
async started() {
|
|
17
|
+
const finalOptions = {
|
|
18
|
+
...options,
|
|
19
|
+
...await getOptions?.(this)
|
|
20
|
+
};
|
|
21
|
+
const key = [
|
|
22
|
+
finalOptions.host,
|
|
23
|
+
finalOptions.port,
|
|
24
|
+
finalOptions.db,
|
|
25
|
+
finalOptions.username,
|
|
26
|
+
finalOptions.path,
|
|
27
|
+
reuseClient ? "" : Math.random()
|
|
28
|
+
].filter(Boolean).join("|");
|
|
29
|
+
this[kKey] = key;
|
|
30
|
+
this.redis = this.getFromStore("redis", key);
|
|
31
|
+
if (!this.redis) {
|
|
32
|
+
this.redis = new ioredis.Redis({
|
|
33
|
+
...finalOptions,
|
|
34
|
+
lazyConnect: true
|
|
35
|
+
});
|
|
36
|
+
this.setClientToStore(
|
|
37
|
+
"redis",
|
|
38
|
+
key,
|
|
39
|
+
this.redis,
|
|
40
|
+
() => this.redis?.disconnect()
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
async stopped() {
|
|
45
|
+
await this.removeServiceFromStore("redis", this[kKey]);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
exports.RedisMixin = RedisMixin;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { a as CustomServiceSchema } from '../context-factory-BWO3xPWE.cjs';
|
|
2
|
+
import { RedisOptions, Redis } from 'ioredis';
|
|
3
|
+
import { Service } from 'moleculer';
|
|
4
|
+
import 'bson';
|
|
5
|
+
import 'zod/v4';
|
|
6
|
+
import 'ajv/dist/2019.js';
|
|
7
|
+
|
|
8
|
+
type AllowedOptions = Omit<RedisOptions, 'lazyConnect'>;
|
|
9
|
+
type RedisMixinOptions = {
|
|
10
|
+
reuseClient?: boolean;
|
|
11
|
+
getOptions?: <TService extends Service = Service>(svc: TService) => Promise<AllowedOptions>;
|
|
12
|
+
};
|
|
13
|
+
declare function RedisMixin(options: AllowedOptions, { reuseClient, getOptions }?: RedisMixinOptions): Partial<CustomServiceSchema<unknown, {
|
|
14
|
+
getRedis(): Redis;
|
|
15
|
+
}, Partial<CustomServiceSchema<unknown, {
|
|
16
|
+
getStore(storeName: string): Map<string, {
|
|
17
|
+
services: Set<unknown>;
|
|
18
|
+
client: Redis;
|
|
19
|
+
onClose: () => Promise<void> | void;
|
|
20
|
+
}>;
|
|
21
|
+
getFromStore(storeName: string, key: string): Redis | null;
|
|
22
|
+
removeServiceFromStore(storeName: string, key: string): Promise<boolean>;
|
|
23
|
+
setClientToStore(storeName: string, key: string, client: Redis, onClose: () => Promise<void> | void): void;
|
|
24
|
+
}, unknown, unknown>>[], unknown>>;
|
|
25
|
+
|
|
26
|
+
export { RedisMixin };
|
|
27
|
+
export type { RedisMixinOptions };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { a as CustomServiceSchema } from '../context-factory-BWO3xPWE.mjs';
|
|
2
|
+
import { RedisOptions, Redis } from 'ioredis';
|
|
3
|
+
import { Service } from 'moleculer';
|
|
4
|
+
import 'bson';
|
|
5
|
+
import 'zod/v4';
|
|
6
|
+
import 'ajv/dist/2019.js';
|
|
7
|
+
|
|
8
|
+
type AllowedOptions = Omit<RedisOptions, 'lazyConnect'>;
|
|
9
|
+
type RedisMixinOptions = {
|
|
10
|
+
reuseClient?: boolean;
|
|
11
|
+
getOptions?: <TService extends Service = Service>(svc: TService) => Promise<AllowedOptions>;
|
|
12
|
+
};
|
|
13
|
+
declare function RedisMixin(options: AllowedOptions, { reuseClient, getOptions }?: RedisMixinOptions): Partial<CustomServiceSchema<unknown, {
|
|
14
|
+
getRedis(): Redis;
|
|
15
|
+
}, Partial<CustomServiceSchema<unknown, {
|
|
16
|
+
getStore(storeName: string): Map<string, {
|
|
17
|
+
services: Set<unknown>;
|
|
18
|
+
client: Redis;
|
|
19
|
+
onClose: () => Promise<void> | void;
|
|
20
|
+
}>;
|
|
21
|
+
getFromStore(storeName: string, key: string): Redis | null;
|
|
22
|
+
removeServiceFromStore(storeName: string, key: string): Promise<boolean>;
|
|
23
|
+
setClientToStore(storeName: string, key: string, client: Redis, onClose: () => Promise<void> | void): void;
|
|
24
|
+
}, unknown, unknown>>[], unknown>>;
|
|
25
|
+
|
|
26
|
+
export { RedisMixin };
|
|
27
|
+
export type { RedisMixinOptions };
|