@onlineapps/mq-client-core 4.1.0 → 5.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.
@@ -0,0 +1,502 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * The recurrences a service declares, and the request that registers ONE of them.
5
+ *
6
+ * `docs/architecture/scheduler.md` §7: a service declares the recurrences it owns and the
7
+ * wrapper re-asserts them at every boot as `scheduler.request` messages of kind `recurring`.
8
+ * The service answers with them through ONE hook (`scheduler: { recurrences: async () => [...] }`,
9
+ * d.834) - platform ones as a constant list, tenant ones derived from its own data - and this
10
+ * library fixes the SHAPE of a record (`validateRecurrences()`), never where it comes from
11
+ * (confirmation `scheduler` 011). The scheduler treats a
12
+ * repeated registration as an upsert — `idempotency_key` for a recurring request IS the
13
+ * schedule id — so re-asserting the declared state costs nothing and drifts nowhere
14
+ * (`api/infra/api_scheduler/src/consumer/decideOnRequest.js`, `contracts/scheduler.request.schema.json`).
15
+ *
16
+ * ONE RAIL FOR EVERY THING A SERVICE SAYS TO THE SCHEDULER. The queue takes one message with
17
+ * four branches, and three of them are a service's to send: `recurring` (a schedule, built from
18
+ * a record the service declared), `event` (a fact that happened, naming no
19
+ * cookbook - the scheduler starts every recipe whose `triggers` declare it, confirmation
20
+ * `scheduler` 004) and `cancel` (an item by its key). Each has a pure builder anybody can ask
21
+ * without a broker, and ONE `publishSchedulerRequest()` sends them all: the kind is a field of
22
+ * the message, never a second function - the split `./monitoring-initiated.js` uses for
23
+ * `workflow_initiated`, with one send instead of three.
24
+ *
25
+ * THE KEY IS ALWAYS THE CALLER'S. `idempotency_key` sits in the contract's root `required`, and
26
+ * the consumer decides a one-shot item ONCE by it (a second `event` of a known key answers
27
+ * `duplicate`). A key invented here would make every retry of one fact a second run, so no
28
+ * builder makes one up - it is the identity of the thing, and only the caller knows it.
29
+ *
30
+ * WHERE IT DIFFERS FROM THAT SIBLING, and why: sending is **fail-fast**. A lost monitoring
31
+ * copy is a missing record of work that already happened, so that rail returns `false` and
32
+ * reports. A lost registration is work that will never happen, and nothing rings when a run
33
+ * does not start — so every refusal here is thrown and the boot stops on it
34
+ * (`architecture-principles.md` §4).
35
+ *
36
+ * WHAT IT NEVER INVENTS: `tenant_id` and `workspace_id`. Both are mandatory and ≥ 1, and the
37
+ * caller passes them with every request — nothing here reads them from the environment or
38
+ * supplies a default. For a recurrence the service knows them from its own data, so the records
39
+ * its boot hook returns carry them; inside a handler `ctx.scheduler` takes them from the
40
+ * invocation envelope.
41
+ *
42
+ * @see api/docs/governance/confirmations/scheduler.md 002
43
+ * @see api/docs/governance/confirmations/scheduler.md 010
44
+ * @see api/docs/governance/confirmations/scheduler.md 011
45
+ *
46
+ * @see ./config/recurrencesSchema.js — the shape of one declared recurrence
47
+ * @see ./monitoring-initiated.js — the same split for `workflow_initiated`
48
+ * @see api/docs/architecture/scheduler.md §4, §7
49
+ */
50
+
51
+ const Ajv = require('ajv');
52
+
53
+ const queueConfig = require('./config/queueConfig');
54
+ const { recurrencesSchema } = require('./config/recurrencesSchema');
55
+
56
+ /** The queue the scheduler consumes, composed from the declaration rather than typed. */
57
+ const schedulerRequestQueue = queueConfig.queueName('scheduler', 'request');
58
+
59
+ const ajv = new Ajv({ allErrors: true });
60
+ const validateDeclaration = ajv.compile(recurrencesSchema);
61
+
62
+ /** Ajv's findings as one sentence a human can act on. */
63
+ const describeErrors = (errors) => (errors || [])
64
+ .map((error) => {
65
+ const where = error.instancePath === '' ? 'the document' : error.instancePath;
66
+ const what = error.params && error.params.additionalProperty
67
+ ? `${error.message} ("${error.params.additionalProperty}")`
68
+ : error.message;
69
+ return `${where} ${what}`;
70
+ })
71
+ .join('; ');
72
+
73
+ /**
74
+ * Check a parsed declaration document and hand back its records.
75
+ *
76
+ * @param {Object} document - `{ recurrences: [...] }`, as the service declared it.
77
+ * @param {string} source - Where it came from, named in every sentence this function throws.
78
+ * Required: a default here would put one caller's name on another caller's mistake.
79
+ * @returns {Array<Object>} The declared recurrences, in the order they were given.
80
+ * @throws {Error} When the document does not hold, naming every rule it broke.
81
+ */
82
+ function validateRecurrences(document, source) {
83
+ if (typeof source !== 'string' || source.trim() === '') {
84
+ throw invalidRequest(
85
+ '[mq-client-core/scheduler-request] Missing source - Expected: a name for what is being '
86
+ + 'checked, because every refusal below quotes it and a wrong name sends the reader to the '
87
+ + 'wrong place. Fix: pass what the caller calls this document (e.g. "the declaration").'
88
+ );
89
+ }
90
+
91
+ if (!document || typeof document !== 'object' || Array.isArray(document)) {
92
+ throw invalidRequest(
93
+ `[mq-client-core/scheduler-request] ${source} is not a declaration - Expected: an object with a "recurrences" list, `
94
+ + `got ${Array.isArray(document) ? 'an array' : typeof document}. `
95
+ + 'Fix: write { "recurrences": [ … ] }, or remove the file if the service declares none.'
96
+ );
97
+ }
98
+
99
+ if (!validateDeclaration(document)) {
100
+ throw invalidRequest(
101
+ `[mq-client-core/scheduler-request] ${source} does not hold: ${describeErrors(validateDeclaration.errors)} - `
102
+ + 'Expected: every record carries id, cookbook_ref, cron (five fields) and an explicit '
103
+ + 'IANA timezone, and nothing this shape does not declare. '
104
+ + 'Fix: correct the record the message names; the shape is src/config/recurrencesSchema.js. '
105
+ + 'Note: tenant and workspace do NOT belong here - the caller passes them '
106
+ + '(confirmations scheduler 010, 011; the tenancy rule itself is scheduler 002).'
107
+ );
108
+ }
109
+
110
+ const seen = new Set();
111
+ for (const recurrence of document.recurrences) {
112
+ if (seen.has(recurrence.id)) {
113
+ throw invalidRequest(
114
+ `[mq-client-core/scheduler-request] ${source} declares the id "${recurrence.id}" twice - Expected: one record per `
115
+ + 'id, because the id travels as the scheduler\'s idempotency_key and a second record '
116
+ + 'would silently overwrite the first at every boot. '
117
+ + 'Fix: give each schedule its own id, or remove the duplicate.'
118
+ );
119
+ }
120
+ seen.add(recurrence.id);
121
+ }
122
+
123
+ return document.recurrences;
124
+ }
125
+
126
+ /**
127
+ * The code every refusal of a builder carries — and only those (`SCHEDULER_REQUEST_INVALID`).
128
+ *
129
+ * A caller has two kinds of failure to tell apart and must never do it by the wording of a
130
+ * message: a request that cannot be built (a record that breaks the declaration rules, tenancy,
131
+ * a missing key or requester, a key the port does not declare) and a request that could not be
132
+ * SENT (no connection, a publish the broker refused). The wrapper's boot phase 0.7b isolates the
133
+ * first as a failed source and stops the boot on the second (confirmation `scheduler` 020,
134
+ * operational reading 1). So the builders mark their refusals with this code, and
135
+ * `publishSchedulerRequest()` and the port's construction never do.
136
+ */
137
+ const SCHEDULER_REQUEST_INVALID = 'SCHEDULER_REQUEST_INVALID';
138
+
139
+ /** A builder's refusal: the message as written, plus the code that names its kind. */
140
+ function invalidRequest(message) {
141
+ const error = new Error(message);
142
+ error.code = SCHEDULER_REQUEST_INVALID;
143
+ return error;
144
+ }
145
+
146
+ /** A whole number a tenant or workspace id may be: ≥ 1, never 0 (confirmation scheduler 002). */
147
+ const isContextId = (value) => Number.isInteger(value) && value >= 1;
148
+
149
+ /**
150
+ * Turn ONE declared recurrence into the message the scheduler takes.
151
+ *
152
+ * @param {Object} args
153
+ * @param {Object} args.declaration - One record the service declared.
154
+ * @param {number} args.tenantId - The tenant the runs execute under; supplied by the caller with
155
+ * this recurrence (the service's own data), never a default.
156
+ * @param {number} args.workspaceId - The workspace the runs execute under; supplied the same way.
157
+ * @param {string} args.requestedBy - The service registering this schedule.
158
+ * @returns {Object} The `scheduler.request` message, exactly the keys the contract declares.
159
+ * @throws {Error} When anything is missing, zero, or not a whole number.
160
+ */
161
+ function buildRecurringRequest({ declaration, tenantId, workspaceId, requestedBy } = {}) {
162
+ const [recurrence] = validateRecurrences({ recurrences: [declaration] }, 'the declaration');
163
+
164
+ requireTenancy(tenantId, workspaceId);
165
+ const by = requireRequestedBy(requestedBy);
166
+
167
+ const request = {
168
+ kind: 'recurring',
169
+ idempotency_key: recurrence.id,
170
+ cookbook_ref: recurrence.cookbook_ref,
171
+ recurrence: { cron: recurrence.cron, timezone: recurrence.timezone },
172
+ tenant_id: tenantId,
173
+ workspace_id: workspaceId,
174
+ requested_by: by
175
+ };
176
+
177
+ // Written only when the declaration states them: an absent `input` is no input, and an absent
178
+ // `catch_up` is the scheduler's own default (§5) - a value materialised here would be a second
179
+ // owner of it.
180
+ if (Object.prototype.hasOwnProperty.call(recurrence, 'input')) request.input = recurrence.input;
181
+ if (Object.prototype.hasOwnProperty.call(recurrence, 'catch_up')) request.catch_up = recurrence.catch_up;
182
+
183
+ return request;
184
+ }
185
+
186
+ /** The platform spelling of a service or an event name, as the consumer's contract defines it. */
187
+ const NAME_PATTERN = /^[a-z][a-z0-9-]*$/;
188
+
189
+ /** The key that identifies the item, whatever kind it is - required by the contract's root. */
190
+ function requireIdempotencyKey(idempotencyKey) {
191
+ if (typeof idempotencyKey !== 'string' || idempotencyKey.trim() === '') {
192
+ throw invalidRequest(
193
+ '[mq-client-core/scheduler-request] Missing idempotency_key - Expected: the stable identity '
194
+ + 'of this item, chosen by the caller. The scheduler decides a one-shot item ONCE by this '
195
+ + 'key, so a key invented here would turn every retry of one fact into a second run. '
196
+ + 'Fix: pass the key that names the thing (for an event, what it happened to).'
197
+ );
198
+ }
199
+
200
+ return idempotencyKey.trim();
201
+ }
202
+
203
+ /** The service registering, cancelling or reporting - checked by the scheduler against services.json. */
204
+ function requireRequestedBy(requestedBy) {
205
+ if (typeof requestedBy !== 'string' || requestedBy.trim() === '') {
206
+ throw invalidRequest(
207
+ '[mq-client-core/scheduler-request] Missing requested_by - Expected: the platform name of the '
208
+ + 'service sending this request; the scheduler checks it against api/config/services.json '
209
+ + "and against the cookbook's own trigger declaration. "
210
+ + 'Fix: pass the service name the wrapper already holds.'
211
+ );
212
+ }
213
+
214
+ return requestedBy.trim();
215
+ }
216
+
217
+ /** Both context ids, under the one rule confirmation `scheduler` 002 fixed. */
218
+ function requireTenancy(tenantId, workspaceId) {
219
+ if (!isContextId(tenantId)) {
220
+ throw invalidRequest(
221
+ `[mq-client-core/scheduler-request] Invalid tenant_id ${JSON.stringify(tenantId)} - Expected: `
222
+ + 'a whole number of at least 1. The owner rejected a reserved value: 0 is not a platform '
223
+ + 'scope, and an absent tenant is an error rather than a default (confirmation scheduler 002). '
224
+ + 'Fix: pass tenantId with every request - a recurrence carries it in the record the service '
225
+ + 'returns from its own data, and ctx.scheduler fills it from the invocation envelope; it is '
226
+ + 'never read from anywhere else.'
227
+ );
228
+ }
229
+
230
+ if (!isContextId(workspaceId)) {
231
+ throw invalidRequest(
232
+ `[mq-client-core/scheduler-request] Invalid workspace_id ${JSON.stringify(workspaceId)} - `
233
+ + 'Expected: a whole number of at least 1, under the same rule as the tenant (confirmation '
234
+ + 'scheduler 002). Fix: pass workspaceId with every request - a recurrence carries it in the '
235
+ + 'record the service returns from its own data, and ctx.scheduler fills it from the invocation '
236
+ + 'envelope; it is never read from anywhere else.'
237
+ );
238
+ }
239
+ }
240
+
241
+ /**
242
+ * Turn a fact into the message that reports it.
243
+ *
244
+ * It names NO cookbook, and that is the point: the scheduler starts every recipe whose
245
+ * `triggers` declare this event (confirmation `scheduler` 004), so a producer naming a recipe
246
+ * would be mapping work to something it may not own (`ingest-poll-trigger-rail` 001: which
247
+ * cookbook runs is a platform decision, not the producer's).
248
+ *
249
+ * @param {Object} args
250
+ * @param {{service: string, name: string}} args.event - Who it happened to, and what happened.
251
+ * @param {number} args.tenantId
252
+ * @param {number} args.workspaceId
253
+ * @param {string} args.requestedBy
254
+ * @param {string} args.idempotencyKey - The identity of this fact; see the module note.
255
+ * @param {Object} [args.input] - Handed to every cookbook the event starts, unchanged.
256
+ * @returns {Object} The `scheduler.request` message of kind `event`.
257
+ * @throws {Error} When anything is missing or not the shape the contract takes.
258
+ */
259
+ function buildEventRequest({ event, tenantId, workspaceId, requestedBy, idempotencyKey, input } = {}) {
260
+ const key = requireIdempotencyKey(idempotencyKey);
261
+ const by = requireRequestedBy(requestedBy);
262
+ requireTenancy(tenantId, workspaceId);
263
+
264
+ if (!event || typeof event !== 'object' || Array.isArray(event)) {
265
+ throw invalidRequest(
266
+ `[mq-client-core/scheduler-request] Missing event - Expected: { service, name } - who it `
267
+ + 'happened to and what happened, both of them. '
268
+ + 'Fix: pass the fact this service publishes, never a cookbook name.'
269
+ );
270
+ }
271
+
272
+ for (const [field, value] of [['service', event.service], ['name', event.name]]) {
273
+ if (typeof value !== 'string' || !NAME_PATTERN.test(value)) {
274
+ throw invalidRequest(
275
+ `[mq-client-core/scheduler-request] Invalid event.${field} ${JSON.stringify(value)} - `
276
+ + 'Expected: the platform spelling, lower case letters, digits and dashes '
277
+ + '(the pattern the scheduler\'s own contract declares). '
278
+ + `Fix: pass the ${field} as api/config/services.json spells it.`
279
+ );
280
+ }
281
+ }
282
+
283
+ const request = {
284
+ kind: 'event',
285
+ idempotency_key: key,
286
+ event: { service: event.service, name: event.name },
287
+ tenant_id: tenantId,
288
+ workspace_id: workspaceId,
289
+ requested_by: by
290
+ };
291
+
292
+ if (input !== undefined) request.input = input;
293
+
294
+ return request;
295
+ }
296
+
297
+ /**
298
+ * Take an item back by its key.
299
+ *
300
+ * Idempotent by contract: a key that names nothing is answered `cancel-nothing` by the
301
+ * scheduler, not refused - so this builder judges nothing about the key, it only requires one.
302
+ * The item carries the context it was registered with, which is why no tenancy travels here.
303
+ *
304
+ * @param {Object} args
305
+ * @param {string} args.idempotencyKey - The key of the schedule or pending item to remove.
306
+ * @param {string} args.requestedBy - Only the service that registered an item may cancel it.
307
+ * @returns {Object} The `scheduler.request` message of kind `cancel`.
308
+ * @throws {Error} When the key or the requester is missing.
309
+ */
310
+ function buildCancelRequest({ idempotencyKey, requestedBy } = {}) {
311
+ return {
312
+ kind: 'cancel',
313
+ idempotency_key: requireIdempotencyKey(idempotencyKey),
314
+ requested_by: requireRequestedBy(requestedBy)
315
+ };
316
+ }
317
+
318
+ /** Every key the port's `registerRecurring()` takes — anything else is refused, never dropped. */
319
+ const RECURRING_PORT_KEYS = Object.freeze([
320
+ 'id', 'cookbookRef', 'cron', 'timezone', 'tenantId', 'workspaceId', 'input', 'catchUp'
321
+ ]);
322
+
323
+ /** The kinds this library builds, and the only ones it will send. */
324
+ const SENDABLE_KINDS = Object.freeze(['recurring', 'event', 'cancel']);
325
+
326
+ /**
327
+ * Send one request of any kind this library builds. Throws on every refusal — see the module
328
+ * note on fail-fast.
329
+ *
330
+ * @param {Object} mqClient - A connected `BaseClient`.
331
+ * @param {Object} request - What one of the builders above produced.
332
+ * @returns {Promise<void>}
333
+ * @throws {Error} When the client is not connected, the request is not one of ours, or the
334
+ * publish fails.
335
+ */
336
+ async function publishSchedulerRequest(mqClient, request) {
337
+ if (!mqClient || typeof mqClient.publish !== 'function' || typeof mqClient.isConnected !== 'function') {
338
+ throw new Error(
339
+ `[mq-client-core/scheduler-request] Missing client - Expected: a connected @onlineapps/mq-client-core BaseClient, so `
340
+ + `the registration reaches ${schedulerRequestQueue}. `
341
+ + 'Fix: pass the client the composition root connected.'
342
+ );
343
+ }
344
+
345
+ if (!mqClient.isConnected()) {
346
+ throw new Error(
347
+ `[mq-client-core/scheduler-request] The client is not connected - Expected: a registration reaches `
348
+ + `${schedulerRequestQueue} before the service starts taking work; a schedule nobody `
349
+ + 'registered is a run that never happens. Fix: connect the client first.'
350
+ );
351
+ }
352
+
353
+ if (!request || !SENDABLE_KINDS.includes(request.kind) || typeof request.idempotency_key !== 'string') {
354
+ throw new Error(
355
+ '[mq-client-core/scheduler-request] Not a request this library built - Expected: one of '
356
+ + `${SENDABLE_KINDS.join(', ')}, got kind ${JSON.stringify(request && request.kind)}. `
357
+ + 'Fix: build the message with buildRecurringRequest(), buildEventRequest() or '
358
+ + 'buildCancelRequest() before sending it.'
359
+ );
360
+ }
361
+
362
+ try {
363
+ await mqClient.publish(schedulerRequestQueue, request);
364
+ } catch (error) {
365
+ throw new Error(
366
+ `[mq-client-core/scheduler-request] Could not send "${request.idempotency_key}" (${request.kind}) `
367
+ + `on ${schedulerRequestQueue}: ${error.message} - Expected: the request to reach the `
368
+ + 'scheduler; a request nobody received is work that will not happen. '
369
+ + 'Fix: act on the reason above - the boot stops here rather than running a service whose '
370
+ + 'schedules were silently not registered.',
371
+ { cause: error }
372
+ );
373
+ }
374
+ }
375
+
376
+ /**
377
+ * A port one service holds: the three things it says, with WHO is speaking already decided.
378
+ *
379
+ * TWO CALLERS, TWO DOORS. The wrapper builds `ctx.scheduler` over this factory at boot and fills
380
+ * the tenancy of each call from the envelope of the run. The operator CLIs of biz-ingest
381
+ * (`scripts/run-{poll,fetch-source-item,retry-pending-items}.js`) do NOT boot the wrapper - a
382
+ * bootstrap there would start consumers, the registry and a heartbeat - and today they are the
383
+ * only way a document of the pilot comes into being, so they call this factory directly with the
384
+ * service identity and the tenancy of the row they are working on. That is why tenancy is
385
+ * explicit on every call rather than held by the port: one port, two kinds of caller, and no
386
+ * hidden context that the CLI would have to fake (BIZ-ingest measurement, 2026-09-23).
387
+ *
388
+ * EAGER ON PURPOSE. The client and the name of the queue are decided HERE, when the port is
389
+ * made, so a service that announces facts falls over at boot rather than at its first document.
390
+ * (On a pin whose `queueConfig` declares no `scheduler` section this module already refuses at
391
+ * require time, which is earlier still.) And because a defect of the wiring must never answer in
392
+ * place of the operation, every refusal names the slot it is about and how to fix it.
393
+ *
394
+ * @param {Object} args
395
+ * @param {Object} args.mqClient - A `BaseClient` (connected by the time a verb is called).
396
+ * @param {string} args.requestedBy - The service this port speaks for; it becomes `requested_by`
397
+ * on every request and `event.service` on every fact.
398
+ * @returns {{registerRecurring: Function, publishEvent: Function, cancel: Function}}
399
+ * @throws {Error} When a slot is missing or unusable - at construction, never later.
400
+ */
401
+ function createSchedulerPublisher({ mqClient, requestedBy } = {}) {
402
+ if (!mqClient || typeof mqClient.publish !== 'function' || typeof mqClient.isConnected !== 'function') {
403
+ throw new Error(
404
+ '[mq-client-core/scheduler-request] Cannot build the scheduler port: slot "mqClient" is '
405
+ + 'missing or cannot publish - Expected: the @onlineapps/mq-client-core BaseClient this '
406
+ + `service connects, so a request reaches ${schedulerRequestQueue}. This is decided now, at `
407
+ + 'construction, because a service that announces facts must fail at boot rather than at '
408
+ + 'its first document. Fix: pass the client the composition root built.'
409
+ );
410
+ }
411
+
412
+ const by = (() => {
413
+ try {
414
+ return requireRequestedBy(requestedBy);
415
+ } catch (error) {
416
+ throw new Error(
417
+ '[mq-client-core/scheduler-request] Cannot build the scheduler port: slot "requestedBy" is '
418
+ + `missing - ${error.message} `
419
+ + 'Fix: pass the platform name of the service this port speaks for (config.serviceName).'
420
+ );
421
+ }
422
+ })();
423
+
424
+ return {
425
+ /**
426
+ * Re-assert one recurrence. The id is the schedule's identity, so a repeat upserts.
427
+ *
428
+ * @param {Object} args - `{ id, cookbookRef, cron, timezone, tenantId, workspaceId, input?, catchUp? }`
429
+ * @returns {Promise<void>}
430
+ */
431
+ async registerRecurring(record = {}) {
432
+ // As strict as the declaration it becomes (`additionalProperties: false` in
433
+ // `./config/recurrencesSchema.js`): a key nobody reads is a typo or a second definition of
434
+ // something, and dropping it in silence hides both (`architecture-principles.md` §3).
435
+ for (const key of Object.keys(record)) {
436
+ if (!RECURRING_PORT_KEYS.includes(key)) {
437
+ throw invalidRequest(
438
+ `[mq-client-core/scheduler-request] registerRecurring got a key the request does not declare ("${key}") - `
439
+ + 'Expected: exactly { id, cookbookRef, cron, timezone, tenantId, workspaceId, input?, catchUp? }. '
440
+ + 'Fix: remove the key from the record the service returns / the ctx.scheduler call.'
441
+ );
442
+ }
443
+ }
444
+
445
+ const { id, cookbookRef, cron, timezone, tenantId, workspaceId, input, catchUp } = record;
446
+ const declaration = { id, cookbook_ref: cookbookRef, cron, timezone };
447
+ if (input !== undefined) declaration.input = input;
448
+ if (catchUp !== undefined) declaration.catch_up = catchUp;
449
+
450
+ await publishSchedulerRequest(mqClient, buildRecurringRequest({
451
+ declaration,
452
+ tenantId,
453
+ workspaceId,
454
+ requestedBy: by
455
+ }));
456
+ },
457
+
458
+ /**
459
+ * Announce one fact. The service is this port's; the key is the caller's (the identity of the
460
+ * thing that happened - a job's uuid, not a clock reading), because the scheduler decides a
461
+ * one-shot item once by it.
462
+ *
463
+ * @param {Object} args - `{ name, idempotencyKey, tenantId, workspaceId, input? }`
464
+ * @returns {Promise<void>}
465
+ */
466
+ async publishEvent({ name, idempotencyKey, tenantId, workspaceId, input } = {}) {
467
+ await publishSchedulerRequest(mqClient, buildEventRequest({
468
+ event: { service: by, name },
469
+ idempotencyKey,
470
+ tenantId,
471
+ workspaceId,
472
+ requestedBy: by,
473
+ input
474
+ }));
475
+ },
476
+
477
+ /**
478
+ * Take back an item this service registered, by its id.
479
+ *
480
+ * @param {Object} args - `{ id }`
481
+ * @returns {Promise<void>}
482
+ */
483
+ async cancel({ id } = {}) {
484
+ await publishSchedulerRequest(mqClient, buildCancelRequest({
485
+ idempotencyKey: id,
486
+ requestedBy: by
487
+ }));
488
+ }
489
+ };
490
+ }
491
+
492
+ module.exports = {
493
+ schedulerRequestQueue,
494
+ SENDABLE_KINDS,
495
+ SCHEDULER_REQUEST_INVALID,
496
+ validateRecurrences,
497
+ buildRecurringRequest,
498
+ buildEventRequest,
499
+ buildCancelRequest,
500
+ publishSchedulerRequest,
501
+ createSchedulerPublisher
502
+ };