ravenflow 0.4.0 → 0.4.1

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.mjs CHANGED
@@ -1,8 +1,8 @@
1
1
  import { z } from 'zod';
2
+ import { Redis } from 'ioredis';
2
3
  import pino from 'pino';
3
4
  import { MongoClient, ObjectId, MongoBulkWriteError, BSON } from 'mongodb';
4
- import { Queue, Worker, MetricsTime, isRedisInstance, Job, DelayedError } from 'bullmq';
5
- import { Redis } from 'ioredis';
5
+ import { Queue, Worker, MetricsTime, Job, isRedisInstance, DelayedError } from 'bullmq';
6
6
  import { format, inspect } from 'util';
7
7
 
8
8
  // src/config/validate-config/schemas/config.schema.ts
@@ -260,145 +260,6 @@ function throwIfAny(errors) {
260
260
  throw new Error(`[ravenflow] Config validation failed:
261
261
  - ${errors.join("\n - ")}`);
262
262
  }
263
- function createLogger(options = {}) {
264
- const isDev = process.env.NODE_ENV !== "production";
265
- const usePretty = options.pretty ?? isDev;
266
- return pino({
267
- level: options.level ?? (isDev ? "debug" : "info"),
268
- transport: usePretty ? {
269
- target: "pino-pretty",
270
- options: {
271
- colorize: true,
272
- translateTime: "HH:MM:ss",
273
- ignore: "pid,hostname,job",
274
- messageFormat: "{if job}({job}) {end}{msg}"
275
- }
276
- } : void 0
277
- });
278
- }
279
- var MongoConnection = class {
280
- constructor(uri) {
281
- this.uri = uri;
282
- this.client = null;
283
- this.database = null;
284
- this.connecting = null;
285
- }
286
- async connect() {
287
- if (this.database) return this.database;
288
- this.connecting ?? (this.connecting = this.openTheDatabase().catch(
289
- (error) => this.forgetTheAttempt(error)
290
- ));
291
- return this.connecting;
292
- }
293
- async close() {
294
- const closing = this.client;
295
- this.client = null;
296
- this.database = null;
297
- this.connecting = null;
298
- await closing?.close();
299
- }
300
- async openTheDatabase() {
301
- const opened = new MongoClient(this.uri);
302
- await opened.connect();
303
- this.client = opened;
304
- this.database = opened.db();
305
- return this.database;
306
- }
307
- forgetTheAttempt(error) {
308
- this.connecting = null;
309
- throw error;
310
- }
311
- };
312
-
313
- // src/queues/declared-queues.ts
314
- function declaredQueueNames(config) {
315
- const queueOfEachJob = Object.values(config.jobs).map(
316
- (job) => queueOf(job, config)
317
- );
318
- return [.../* @__PURE__ */ new Set([config.defaultQueue, ...queueOfEachJob])];
319
- }
320
- function jobsDeclaredOn(queue, config) {
321
- return Object.fromEntries(
322
- Object.entries(config.jobs).filter(
323
- ([, job]) => queueOf(job, config) === queue
324
- )
325
- );
326
- }
327
- function queueOf(job, config) {
328
- return job.queue ?? config.defaultQueue;
329
- }
330
-
331
- // src/queues/open-queues.ts
332
- function openQueues(config, connection) {
333
- const settings = { connection, retention: config.queuesRetention };
334
- return Object.fromEntries(
335
- declaredQueueNames(config).map((name) => [
336
- name,
337
- openQueue(name, settings)
338
- ])
339
- );
340
- }
341
- function openQueue(name, settings) {
342
- return new Queue(name, {
343
- connection: settings.connection,
344
- defaultJobOptions: defaultJobOptionsFor(settings.retention)
345
- });
346
- }
347
- function defaultJobOptionsFor(retention = {}) {
348
- return {
349
- removeOnComplete: {
350
- count: retention.completedCount ?? DEFAULT_COMPLETED_COUNT_RETENTION
351
- },
352
- removeOnFail: {
353
- count: retention.failedCount ?? DEFAULT_FAILED_COUNT_RETENTION
354
- }
355
- };
356
- }
357
-
358
- // src/queues/close-queues.ts
359
- async function closeQueues(queues) {
360
- await Promise.all(Object.values(queues).map((queue) => queue.close()));
361
- }
362
-
363
- // src/queues/build-job-options.ts
364
- function buildJobOptions(job) {
365
- const options = {};
366
- if (job.attempts !== void 0) options.attempts = job.attempts;
367
- if (job.backoff !== void 0) options.backoff = backoffOptions(job.backoff);
368
- return options;
369
- }
370
- function backoffOptions(backoff) {
371
- return {
372
- type: backoff.type,
373
- delay: backoff.delayMs,
374
- jitter: backoff.jitter
375
- };
376
- }
377
-
378
- // src/queues/link-id.ts
379
- var LINK_SEPARATOR = ".";
380
- var A_NUMBERED_LINK = /^(.+)\.([1-9]\d*)$/;
381
- function nextLinkIdOf(linkId) {
382
- const { runId, link } = linkOfItsRun(linkId);
383
- return linkIdOf({ runId, link: link + 1 });
384
- }
385
- function linkIdOf({ runId, link }) {
386
- if (link === 0) return runId;
387
- return `${asACustomJobId(runId)}${LINK_SEPARATOR}${link}`;
388
- }
389
- function linkOfItsRun(linkId) {
390
- const numbered = A_NUMBERED_LINK.exec(linkId);
391
- if (numbered === null) return { runId: linkId, link: 0 };
392
- return { runId: numbered[1], link: Number(numbered[2]) };
393
- }
394
- function asACustomJobId(runId) {
395
- if (holdsColonsBullmqRefuses(runId)) return encodeURIComponent(runId);
396
- return runId;
397
- }
398
- function holdsColonsBullmqRefuses(id) {
399
- const parts = id.split(":").length;
400
- return parts !== 1 && parts !== 3;
401
- }
402
263
 
403
264
  // src/control-plane/control-key.ts
404
265
  var JOB_PREFIX = "job:";
@@ -884,83 +745,222 @@ function initControlPlane(runtime) {
884
745
  });
885
746
  return new ControlPlane({ store, channel, logger: runtime.logger });
886
747
  }
887
- var BACKLOG_COLLECTION_PREFIX = "rav:spool-backlog:";
888
- var NEVER_CLAIMED = { claimedAt: { $exists: false } };
889
- var CLAIMED = { claimedAt: { $type: "date" } };
890
- var CLAIM_INDEX = { claimedAt: 1, _id: 1 };
891
- var OLDEST_FIRST = { _id: 1 };
892
- var MS_PER_SECOND = 1e3;
893
- var Backlog = class {
894
- constructor(spoolName, connection) {
895
- this.spoolName = spoolName;
896
- this.connection = connection;
897
- }
898
- async push(item) {
899
- const backlog = await this.collection();
900
- const { insertedId } = await backlog.insertOne({ item });
901
- return insertedId.toHexString();
902
- }
903
- async pushMany(items) {
904
- if (items.length === 0) return [];
905
- const backlog = await this.collection();
906
- const { insertedIds } = await backlog.insertMany(
907
- items.map((item) => ({ item }))
908
- );
909
- return items.map((_, position) => insertedIds[position].toHexString());
910
- }
911
- async purge() {
912
- const backlog = await this.collection();
913
- const { deletedCount } = await backlog.deleteMany(NEVER_CLAIMED);
914
- return {
915
- dropped: deletedCount,
916
- inFlight: await backlog.countDocuments(CLAIMED)
917
- };
918
- }
919
- async indexForTheClaim() {
920
- const backlog = await this.collection();
921
- await backlog.createIndex(CLAIM_INDEX);
922
- }
923
- async countInFlight() {
924
- const backlog = await this.collection();
925
- return backlog.countDocuments(CLAIMED);
748
+ function createLogger(options = {}) {
749
+ const isDev = process.env.NODE_ENV !== "production";
750
+ const usePretty = options.pretty ?? isDev;
751
+ return pino({
752
+ level: options.level ?? (isDev ? "debug" : "info"),
753
+ transport: usePretty ? {
754
+ target: "pino-pretty",
755
+ options: {
756
+ colorize: true,
757
+ translateTime: "HH:MM:ss",
758
+ ignore: "pid,hostname,job",
759
+ messageFormat: "{if job}({job}) {end}{msg}"
760
+ }
761
+ } : void 0
762
+ });
763
+ }
764
+ var MongoConnection = class {
765
+ constructor(uri) {
766
+ this.uri = uri;
767
+ this.client = null;
768
+ this.database = null;
769
+ this.connecting = null;
926
770
  }
927
- async expireUnclaimedPushedBefore(pushedBefore) {
928
- const backlog = await this.collection();
929
- const { deletedCount } = await backlog.deleteMany({
930
- ...NEVER_CLAIMED,
931
- _id: { $lt: ObjectId.createFromTime(secondsOf(pushedBefore)) }
932
- });
933
- return deletedCount;
771
+ async connect() {
772
+ if (this.database) return this.database;
773
+ this.connecting ?? (this.connecting = this.openTheDatabase().catch(
774
+ (error) => this.forgetTheAttempt(error)
775
+ ));
776
+ return this.connecting;
934
777
  }
935
- async reclaimStaleClaims(claimedBefore, now) {
936
- const staleClaim = {
937
- claimedAt: { $lte: claimedBefore }
938
- };
939
- const backlog = await this.collection();
940
- const stale = await backlog.find(staleClaim).toArray();
941
- return this.claimAll(stale, staleClaim, now);
778
+ async close() {
779
+ const closing = this.client;
780
+ this.client = null;
781
+ this.database = null;
782
+ this.connecting = null;
783
+ await closing?.close();
942
784
  }
943
- async claimOldestNeverClaimed(count, now) {
944
- if (count <= 0) return [];
945
- const backlog = await this.collection();
946
- const oldest = await backlog.find(NEVER_CLAIMED, { sort: OLDEST_FIRST, limit: count }).toArray();
947
- return this.claimAll(oldest, NEVER_CLAIMED, now);
785
+ async openTheDatabase() {
786
+ const opened = new MongoClient(this.uri);
787
+ await opened.connect();
788
+ this.client = opened;
789
+ this.database = opened.db();
790
+ return this.database;
948
791
  }
949
- async removeAll(ids) {
950
- if (ids.length === 0) return;
951
- const backlog = await this.collection();
952
- await backlog.deleteMany({ _id: { $in: ids } });
792
+ forgetTheAttempt(error) {
793
+ this.connecting = null;
794
+ throw error;
953
795
  }
954
- async claimAll(entries, stillClaimable, claimedAt) {
955
- if (entries.length === 0) return [];
956
- const ids = entries.map(({ _id }) => _id);
957
- const backlog = await this.collection();
958
- const { modifiedCount } = await backlog.updateMany(
959
- { _id: { $in: ids }, ...stillClaimable },
960
- { $set: { claimedAt } }
961
- );
962
- if (modifiedCount < entries.length) {
963
- return this.whatThisClaimTook(ids, claimedAt);
796
+ };
797
+
798
+ // src/queues/declared-queues.ts
799
+ function declaredQueueNames(config) {
800
+ const queueOfEachJob = Object.values(config.jobs).map(
801
+ (job) => queueOf(job, config)
802
+ );
803
+ return [.../* @__PURE__ */ new Set([config.defaultQueue, ...queueOfEachJob])];
804
+ }
805
+ function jobsDeclaredOn(queue, config) {
806
+ return Object.fromEntries(
807
+ Object.entries(config.jobs).filter(
808
+ ([, job]) => queueOf(job, config) === queue
809
+ )
810
+ );
811
+ }
812
+ function queueOf(job, config) {
813
+ return job.queue ?? config.defaultQueue;
814
+ }
815
+
816
+ // src/queues/open-queues.ts
817
+ function openQueues(config, connection) {
818
+ const settings = { connection, retention: config.queuesRetention };
819
+ return Object.fromEntries(
820
+ declaredQueueNames(config).map((name) => [
821
+ name,
822
+ openQueue(name, settings)
823
+ ])
824
+ );
825
+ }
826
+ function openQueue(name, settings) {
827
+ return new Queue(name, {
828
+ connection: settings.connection,
829
+ defaultJobOptions: defaultJobOptionsFor(settings.retention)
830
+ });
831
+ }
832
+ function defaultJobOptionsFor(retention = {}) {
833
+ return {
834
+ removeOnComplete: {
835
+ count: retention.completedCount ?? DEFAULT_COMPLETED_COUNT_RETENTION
836
+ },
837
+ removeOnFail: {
838
+ count: retention.failedCount ?? DEFAULT_FAILED_COUNT_RETENTION
839
+ }
840
+ };
841
+ }
842
+
843
+ // src/queues/close-queues.ts
844
+ async function closeQueues(queues) {
845
+ await Promise.all(Object.values(queues).map((queue) => queue.close()));
846
+ }
847
+
848
+ // src/queues/build-job-options.ts
849
+ function buildJobOptions(job) {
850
+ const options = {};
851
+ if (job.attempts !== void 0) options.attempts = job.attempts;
852
+ if (job.backoff !== void 0) options.backoff = backoffOptions(job.backoff);
853
+ return options;
854
+ }
855
+ function backoffOptions(backoff) {
856
+ return {
857
+ type: backoff.type,
858
+ delay: backoff.delayMs,
859
+ jitter: backoff.jitter
860
+ };
861
+ }
862
+
863
+ // src/queues/link-id.ts
864
+ var LINK_SEPARATOR = ".";
865
+ var A_NUMBERED_LINK = /^(.+)\.([1-9]\d*)$/;
866
+ function nextLinkIdOf(linkId) {
867
+ const { runId, link } = linkOfItsRun(linkId);
868
+ return linkIdOf({ runId, link: link + 1 });
869
+ }
870
+ function linkIdOf({ runId, link }) {
871
+ if (link === 0) return runId;
872
+ return `${asACustomJobId(runId)}${LINK_SEPARATOR}${link}`;
873
+ }
874
+ function linkOfItsRun(linkId) {
875
+ const numbered = A_NUMBERED_LINK.exec(linkId);
876
+ if (numbered === null) return { runId: linkId, link: 0 };
877
+ return { runId: numbered[1], link: Number(numbered[2]) };
878
+ }
879
+ function asACustomJobId(runId) {
880
+ if (holdsColonsBullmqRefuses(runId)) return encodeURIComponent(runId);
881
+ return runId;
882
+ }
883
+ function holdsColonsBullmqRefuses(id) {
884
+ const parts = id.split(":").length;
885
+ return parts !== 1 && parts !== 3;
886
+ }
887
+ var BACKLOG_COLLECTION_PREFIX = "rav:spool-backlog:";
888
+ var NEVER_CLAIMED = { claimedAt: { $exists: false } };
889
+ var CLAIMED = { claimedAt: { $type: "date" } };
890
+ var CLAIM_INDEX = { claimedAt: 1, _id: 1 };
891
+ var OLDEST_FIRST = { _id: 1 };
892
+ var MS_PER_SECOND = 1e3;
893
+ var Backlog = class {
894
+ constructor(spoolName, connection) {
895
+ this.spoolName = spoolName;
896
+ this.connection = connection;
897
+ }
898
+ async push(item) {
899
+ const backlog = await this.collection();
900
+ const { insertedId } = await backlog.insertOne({ item });
901
+ return insertedId.toHexString();
902
+ }
903
+ async pushMany(items) {
904
+ if (items.length === 0) return [];
905
+ const backlog = await this.collection();
906
+ const { insertedIds } = await backlog.insertMany(
907
+ items.map((item) => ({ item }))
908
+ );
909
+ return items.map((_, position) => insertedIds[position].toHexString());
910
+ }
911
+ async purge() {
912
+ const backlog = await this.collection();
913
+ const { deletedCount } = await backlog.deleteMany(NEVER_CLAIMED);
914
+ return {
915
+ dropped: deletedCount,
916
+ inFlight: await backlog.countDocuments(CLAIMED)
917
+ };
918
+ }
919
+ async indexForTheClaim() {
920
+ const backlog = await this.collection();
921
+ await backlog.createIndex(CLAIM_INDEX);
922
+ }
923
+ async countInFlight() {
924
+ const backlog = await this.collection();
925
+ return backlog.countDocuments(CLAIMED);
926
+ }
927
+ async expireUnclaimedPushedBefore(pushedBefore) {
928
+ const backlog = await this.collection();
929
+ const { deletedCount } = await backlog.deleteMany({
930
+ ...NEVER_CLAIMED,
931
+ _id: { $lt: ObjectId.createFromTime(secondsOf(pushedBefore)) }
932
+ });
933
+ return deletedCount;
934
+ }
935
+ async reclaimStaleClaims(claimedBefore, now) {
936
+ const staleClaim = {
937
+ claimedAt: { $lte: claimedBefore }
938
+ };
939
+ const backlog = await this.collection();
940
+ const stale = await backlog.find(staleClaim).toArray();
941
+ return this.claimAll(stale, staleClaim, now);
942
+ }
943
+ async claimOldestNeverClaimed(count, now) {
944
+ if (count <= 0) return [];
945
+ const backlog = await this.collection();
946
+ const oldest = await backlog.find(NEVER_CLAIMED, { sort: OLDEST_FIRST, limit: count }).toArray();
947
+ return this.claimAll(oldest, NEVER_CLAIMED, now);
948
+ }
949
+ async removeAll(ids) {
950
+ if (ids.length === 0) return;
951
+ const backlog = await this.collection();
952
+ await backlog.deleteMany({ _id: { $in: ids } });
953
+ }
954
+ async claimAll(entries, stillClaimable, claimedAt) {
955
+ if (entries.length === 0) return [];
956
+ const ids = entries.map(({ _id }) => _id);
957
+ const backlog = await this.collection();
958
+ const { modifiedCount } = await backlog.updateMany(
959
+ { _id: { $in: ids }, ...stillClaimable },
960
+ { $set: { claimedAt } }
961
+ );
962
+ if (modifiedCount < entries.length) {
963
+ return this.whatThisClaimTook(ids, claimedAt);
964
964
  }
965
965
  return entries;
966
966
  }
@@ -1172,172 +1172,68 @@ function buildSendOrIngest(job, controls) {
1172
1172
  return { send: buildSend(job) };
1173
1173
  }
1174
1174
 
1175
- // src/client/ravenflow-client.ts
1176
- var RavenflowClient = class {
1177
- constructor(config, options) {
1178
- validateConfig(config);
1179
- this.config = config;
1180
- this.mongoUri = options.mongo;
1181
- this.redisConnectionOptions = options.redis;
1182
- this.logger = createLogger(config.logger);
1183
- this.mongo = new MongoConnection(options.mongo);
1184
- this.queues = openQueues(config, this.redisConnectionOptions);
1175
+ // src/client/create-ravenflow-client.ts
1176
+ function createRavenflowClient(config, options) {
1177
+ validateConfig(config);
1178
+ const logger = createLogger(config.logger);
1179
+ const mongo = new MongoConnection(options.mongo);
1180
+ const queues = openQueues(config, options.redis);
1181
+ const controlPlane = initControlPlane({ redis: options.redis, mongo, logger });
1182
+ return {
1183
+ jobs: initJobProxies(config, { queues, mongo, controls: controlPlane }),
1184
+ close: async () => {
1185
+ await controlPlane.close();
1186
+ await closeQueues(queues);
1187
+ await mongo.close();
1188
+ }
1189
+ };
1190
+ }
1191
+
1192
+ // src/worker/control-plane-reader.ts
1193
+ var ControlPlaneReader = class {
1194
+ constructor(settings) {
1195
+ this.mirrorConnection = new MongoConnection(settings.mongo);
1185
1196
  this.controlPlane = initControlPlane({
1186
- redis: this.redisConnectionOptions,
1187
- mongo: this.mongo,
1188
- logger: this.logger
1189
- });
1190
- this.jobs = initJobProxies(config, {
1191
- queues: this.queues,
1192
- mongo: this.mongo,
1193
- controls: this.controlPlane
1197
+ redis: settings.redis,
1198
+ mongo: this.mirrorConnection,
1199
+ logger: settings.logger
1194
1200
  });
1195
1201
  }
1202
+ async open() {
1203
+ await this.controlPlane.rehydrate();
1204
+ await this.mirrorConnection.close();
1205
+ }
1206
+ read(jobName) {
1207
+ return this.controlPlane.read(jobName);
1208
+ }
1196
1209
  async close() {
1197
1210
  await this.controlPlane.close();
1198
- await closeQueues(this.queues);
1199
- await this.mongo.close();
1211
+ await this.mirrorConnection.close();
1200
1212
  }
1201
1213
  };
1202
1214
 
1203
- // src/client/create-ravenflow-client.ts
1204
- function createRavenflowClient(config, options) {
1205
- return new RavenflowClient(config, options);
1215
+ // src/worker/log-bullmq-errors.ts
1216
+ function logBullmqErrors(source, logger) {
1217
+ source.on("error", (error) => {
1218
+ logger.error(
1219
+ { err: error, queue: source.name },
1220
+ `[ravenflow] BullMQ reported an error on "${source.name}".`
1221
+ );
1222
+ });
1206
1223
  }
1207
1224
 
1208
- // src/describe-error.ts
1209
- function describeError(error) {
1210
- return error instanceof Error ? error.message : String(error);
1211
- }
1212
- var RAVENFLOW = `
1213
- \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557
1214
- \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551
1215
- \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2557 \u2588\u2588\u2551
1216
- \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u255A\u2588\u2588\u2557 \u2588\u2588\u2554\u255D\u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2588\u2557\u2588\u2588\u2551
1217
- \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2554\u255D \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u255A\u2588\u2588\u2588\u2554\u2588\u2588\u2588\u2554\u255D
1218
- \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u255D\u255A\u2550\u2550\u255D
1219
- `;
1220
- var RULE = "\u2550".repeat(85);
1221
- var FEEDS_IN_ORDER = ["nothing", "clock", "spool"];
1222
- function printBanner(facts) {
1223
- console.log(
1224
- [
1225
- RAVENFLOW,
1226
- RULE,
1227
- "[Config]",
1228
- ...configLines(facts),
1229
- "",
1230
- "[Registered Jobs]",
1231
- ...registeredJobLines(facts.jobs),
1232
- "",
1233
- "[Scheduled Jobs]",
1234
- ...schedulerLines(facts.schedulers),
1235
- RULE,
1236
- "Ready to process jobs..."
1237
- ].join("\n")
1238
- );
1225
+ // src/worker/mongodb-deadline-on-close.ts
1226
+ var MONGODB_DEADLINE_ON_CLOSE_MS = 5e3;
1227
+ async function waitOrCutShort(waitingOnMongoDB, cutItShort) {
1228
+ if (await settlesBeforeTheDeadline(waitingOnMongoDB)) return;
1229
+ await cutItShort();
1230
+ await waitingOnMongoDB;
1239
1231
  }
1240
- function configLines(facts) {
1241
- return [
1242
- ` -> queue: ${facts.queue}`,
1243
- ` -> concurrency: ${facts.concurrency}`,
1244
- ` -> redis: ${addressOf(facts.redis)}`,
1245
- ` -> mongo: ${mongoLineOf(facts)}`
1246
- ];
1247
- }
1248
- function addressOf(redis) {
1249
- if (isRedisInstance(redis)) return "an ioredis client";
1250
- const {
1251
- url,
1252
- host = "localhost",
1253
- port = 6379,
1254
- db = 0
1255
- } = redis;
1256
- return url ? withoutPassword(url) : `${host}:${port}/${db}`;
1257
- }
1258
- function mongoLineOf({ mongo, recorderOpening }) {
1259
- const address = withoutPassword(mongo);
1260
- if (!recorderOpening.opened) {
1261
- return `${address} (job results are not recorded: ${recorderOpening.because})`;
1262
- }
1263
- return address;
1264
- }
1265
- function withoutPassword(uri) {
1266
- return uri.replace(/\/\/([^:/@]*):[^@]*@/, "//$1:***@");
1267
- }
1268
- function registeredJobLines(jobs) {
1269
- const lines = Object.entries(jobs).map(([name, job]) => ({ name, feed: feedOf(job) })).sort((a, b) => feedRank(a.feed) - feedRank(b.feed)).map(({ name, feed }) => ` . ${name}${feed.label}`);
1270
- return lines.length > 0 ? lines : [" (none)"];
1271
- }
1272
- function feedOf(job) {
1273
- const cron = job.trigger?.cron;
1274
- if (cron) return { kind: "clock", label: ` (\u21BB ${cron})` };
1275
- if (isSpoolFed(job)) return { kind: "spool", label: spoolLabelOf(job) };
1276
- return { kind: "nothing", label: "" };
1277
- }
1278
- function spoolLabelOf({ trigger, pace }) {
1279
- const ceiling = inFlightCeilingOf(trigger.spool, pace);
1280
- return ` (\u2190 ${trigger.spool.name}, ${ceiling} in flight at most)`;
1281
- }
1282
- function feedRank(feed) {
1283
- return FEEDS_IN_ORDER.indexOf(feed.kind);
1284
- }
1285
- function schedulerLines(changes) {
1286
- const lines = [
1287
- ...changes.added.map(({ name, pattern }) => ` + ${name} \u2192 ${pattern}`),
1288
- ...changes.rescheduled.map(
1289
- ({ name, pattern, previousPattern }) => ` \u21BB ${name} \u2192 ${pattern} (was ${previousPattern})`
1290
- ),
1291
- ...changes.removed.map((name) => ` - ${name}`)
1292
- ];
1293
- return lines.length > 0 ? lines : [" (no change)"];
1294
- }
1295
-
1296
- // src/worker/control-plane-reader.ts
1297
- var ControlPlaneReader = class {
1298
- constructor(settings) {
1299
- this.mirrorConnection = new MongoConnection(settings.mongo);
1300
- this.controlPlane = initControlPlane({
1301
- redis: settings.redis,
1302
- mongo: this.mirrorConnection,
1303
- logger: settings.logger
1304
- });
1305
- }
1306
- async open() {
1307
- await this.controlPlane.rehydrate();
1308
- await this.mirrorConnection.close();
1309
- }
1310
- read(jobName) {
1311
- return this.controlPlane.read(jobName);
1312
- }
1313
- async close() {
1314
- await this.controlPlane.close();
1315
- await this.mirrorConnection.close();
1316
- }
1317
- };
1318
-
1319
- // src/worker/log-bullmq-errors.ts
1320
- function logBullmqErrors(source, logger) {
1321
- source.on("error", (error) => {
1322
- logger.error(
1323
- { err: error, queue: source.name },
1324
- `[ravenflow] BullMQ reported an error on "${source.name}".`
1325
- );
1326
- });
1327
- }
1328
-
1329
- // src/worker/mongodb-deadline-on-close.ts
1330
- var MONGODB_DEADLINE_ON_CLOSE_MS = 5e3;
1331
- async function waitOrCutShort(waitingOnMongoDB, cutItShort) {
1332
- if (await settlesBeforeTheDeadline(waitingOnMongoDB)) return;
1333
- await cutItShort();
1334
- await waitingOnMongoDB;
1335
- }
1336
- async function settlesBeforeTheDeadline(work) {
1337
- const deadline = new Promise((resolve) => {
1338
- setTimeout(() => resolve(false), MONGODB_DEADLINE_ON_CLOSE_MS).unref();
1339
- });
1340
- return Promise.race([work.then(() => true), deadline]);
1232
+ async function settlesBeforeTheDeadline(work) {
1233
+ const deadline = new Promise((resolve) => {
1234
+ setTimeout(() => resolve(false), MONGODB_DEADLINE_ON_CLOSE_MS).unref();
1235
+ });
1236
+ return Promise.race([work.then(() => true), deadline]);
1341
1237
  }
1342
1238
  var FLUSH_INTERVAL_MS = 3e4;
1343
1239
  var JOB_RESULTS_PER_BATCH = 1e3;
@@ -2700,100 +2596,6 @@ function abortWhenClosing(worker, shutdown) {
2700
2596
  shutdown.abort(new Error("[ravenflow] the worker is shutting down"));
2701
2597
  });
2702
2598
  }
2703
-
2704
- // src/worker/schedulers.ts
2705
- async function reconcileSchedulers(queue, jobs) {
2706
- const declared = cronSchedulesOf(jobs);
2707
- const existing = await patternsByScheduler(queue);
2708
- const changes = changesBetween(declared, existing);
2709
- await Promise.all(
2710
- changes.removed.map((key) => queue.removeJobScheduler(key))
2711
- );
2712
- await Promise.all(declared.map((schedule) => upsert(queue, schedule)));
2713
- return changes;
2714
- }
2715
- function cronSchedulesOf(jobs) {
2716
- return Object.entries(jobs).flatMap(([name, job]) => {
2717
- const pattern = job.trigger?.cron;
2718
- if (pattern === void 0) return [];
2719
- return [{ name, pattern, options: buildJobOptions(job) }];
2720
- });
2721
- }
2722
- async function patternsByScheduler(queue) {
2723
- const schedulers = await queue.getJobSchedulers();
2724
- return new Map(
2725
- schedulers.map(({ key, pattern }) => [key, pattern ?? "no pattern"])
2726
- );
2727
- }
2728
- function changesBetween(declared, existing) {
2729
- const declaredNames = new Set(declared.map(({ name }) => name));
2730
- return {
2731
- added: declared.filter(({ name }) => !existing.has(name)),
2732
- rescheduled: declared.flatMap(
2733
- (schedule) => reschedulingOf(schedule, existing)
2734
- ),
2735
- removed: [...existing.keys()].filter((key) => !declaredNames.has(key))
2736
- };
2737
- }
2738
- function reschedulingOf(schedule, existing) {
2739
- const previousPattern = existing.get(schedule.name);
2740
- if (previousPattern === void 0) return [];
2741
- if (previousPattern === schedule.pattern) return [];
2742
- return [{ ...schedule, previousPattern }];
2743
- }
2744
- async function upsert(queue, { name, pattern, options }) {
2745
- try {
2746
- await queue.upsertJobScheduler(name, { pattern }, { name, opts: options });
2747
- } catch (error) {
2748
- throw new Error(
2749
- `[ravenflow] "${name}" could not be scheduled on "${pattern}": ${describeError(error)}`
2750
- );
2751
- }
2752
- }
2753
-
2754
- // src/worker/served-queue.ts
2755
- function refuseAQueueItsConfigDoesNotDeclare(queue, config) {
2756
- const declared = declaredQueueNames(config);
2757
- if (declared.includes(queue)) return;
2758
- const quoted = declared.map((name) => `"${name}"`).join(", ");
2759
- throw new Error(
2760
- `[ravenflow] no job of this config runs on "${queue}": its queues are ${quoted}.`
2761
- );
2762
- }
2763
-
2764
- // src/worker/serving.ts
2765
- async function closeInOrder(serving) {
2766
- await stopWatchingMongoDB(serving);
2767
- await stopTheTicks(serving);
2768
- await stopTheWorker(serving);
2769
- await writeWhatItsJobsLeft(serving);
2770
- await closeTheConnections(serving);
2771
- }
2772
- async function stopWatchingMongoDB(serving) {
2773
- await serving.mongoWatch.close();
2774
- }
2775
- async function stopTheTicks(serving) {
2776
- await Promise.all([
2777
- serving.jobResultsAggregator.stopTicking(),
2778
- serving.spoolFeeder.stopTicking()
2779
- ]);
2780
- }
2781
- async function stopTheWorker(serving) {
2782
- serving.jobResultsRecorder.stopHoldingJobs();
2783
- await serving.worker.close();
2784
- }
2785
- async function writeWhatItsJobsLeft(serving) {
2786
- await Promise.all([
2787
- serving.jobResultsRecorder.close(),
2788
- serving.spoolFeeder.finishTheRemovals()
2789
- ]);
2790
- }
2791
- async function closeTheConnections(serving) {
2792
- await serving.spoolFeeder.close();
2793
- await serving.jobResultsAggregator.close();
2794
- await serving.controlPlaneReader.close();
2795
- await serving.queue.close();
2796
- }
2797
2599
  var THE_DRIVERS_RECOVERY_LAG_MS = 1e4;
2798
2600
  var MongoWatch = class {
2799
2601
  constructor(settings) {
@@ -2915,189 +2717,374 @@ async function callOnWithdraw({ logger, onWithdraw }, reason) {
2915
2717
  }
2916
2718
  }
2917
2719
 
2918
- // src/worker/ravenflow-worker.ts
2919
- var RavenflowWorker = class {
2920
- constructor(config, options) {
2921
- this.serving = null;
2922
- this.starting = null;
2923
- this.closing = null;
2924
- validateConfig(config);
2925
- refuseAQueueItsConfigDoesNotDeclare(options.queue, config);
2926
- this.config = config;
2927
- this.options = options;
2928
- this.jobs = jobsDeclaredOn(options.queue, config);
2929
- this.logger = createLogger(config.logger);
2930
- }
2931
- async start() {
2932
- this.refuseASecondStart();
2933
- const serving = this.openServing();
2934
- this.serving = serving;
2935
- this.starting = this.serveOrStop(serving);
2720
+ // src/worker/open-serving.ts
2721
+ function openServing(settings, onDurableLoss) {
2722
+ const { config, options, jobs, logger } = settings;
2723
+ const queue = openQueue(options.queue, {
2724
+ connection: options.redis,
2725
+ retention: config.queuesRetention
2726
+ });
2727
+ logBullmqErrors(queue, logger);
2728
+ const controlPlaneReader = buildControlPlaneReader(settings);
2729
+ const jobResultsRecorder = buildJobResultsRecorder(settings, queue);
2730
+ const worker = openWorker(options, {
2731
+ jobs,
2732
+ logger,
2733
+ queue,
2734
+ controls: controlPlaneReader,
2735
+ jobResultsRecorder
2736
+ });
2737
+ return {
2738
+ queue,
2739
+ controlPlaneReader,
2740
+ jobResultsRecorder,
2741
+ jobResultsAggregator: buildJobResultsAggregator(settings),
2742
+ spoolFeeder: buildSpoolFeeder(settings, { queue, controlPlaneReader }),
2743
+ mongoWatch: buildMongoWatch(settings, onDurableLoss),
2744
+ worker
2745
+ };
2746
+ }
2747
+ function buildControlPlaneReader({
2748
+ options,
2749
+ logger
2750
+ }) {
2751
+ return new ControlPlaneReader({
2752
+ redis: options.redis,
2753
+ mongo: options.mongo,
2754
+ logger
2755
+ });
2756
+ }
2757
+ function buildJobResultsRecorder({ config, options, jobs, logger }, queue) {
2758
+ return new JobResultsRecorder({
2759
+ mongo: options.mongo,
2760
+ retentionHours: config.jobResultsRetentionHours ?? DEFAULT_JOB_RESULTS_RETENTION_HOURS,
2761
+ queue,
2762
+ jobs,
2763
+ logger
2764
+ });
2765
+ }
2766
+ function buildJobResultsAggregator({
2767
+ config,
2768
+ options,
2769
+ logger
2770
+ }) {
2771
+ return new JobResultsAggregator({
2772
+ redis: options.redis,
2773
+ mongo: options.mongo,
2774
+ retentionHours: config.aggregatedResultsRetentionHours ?? DEFAULT_AGGREGATED_RESULTS_RETENTION_HOURS,
2775
+ logger
2776
+ });
2777
+ }
2778
+ function buildSpoolFeeder({ options, jobs, logger }, { queue, controlPlaneReader }) {
2779
+ return new SpoolFeeder({
2780
+ redis: options.redis,
2781
+ mongo: options.mongo,
2782
+ queue,
2783
+ controls: controlPlaneReader,
2784
+ jobs,
2785
+ logger
2786
+ });
2787
+ }
2788
+ function buildMongoWatch({ options, logger }, onDurableLoss) {
2789
+ return new MongoWatch({
2790
+ mongo: options.mongo,
2791
+ gracePeriodMs: options.withdrawal?.afterMs ?? DEFAULT_GRACE_PERIOD_MS,
2792
+ logger,
2793
+ onDurableLoss
2794
+ });
2795
+ }
2796
+
2797
+ // src/worker/served-queue.ts
2798
+ function refuseAQueueItsConfigDoesNotDeclare(queue, config) {
2799
+ const declared = declaredQueueNames(config);
2800
+ if (declared.includes(queue)) return;
2801
+ const quoted = declared.map((name) => `"${name}"`).join(", ");
2802
+ throw new Error(
2803
+ `[ravenflow] no job of this config runs on "${queue}": its queues are ${quoted}.`
2804
+ );
2805
+ }
2806
+
2807
+ // src/describe-error.ts
2808
+ function describeError(error) {
2809
+ return error instanceof Error ? error.message : String(error);
2810
+ }
2811
+ var RAVENFLOW = `
2812
+ \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557
2813
+ \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551
2814
+ \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2557 \u2588\u2588\u2551
2815
+ \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u255A\u2588\u2588\u2557 \u2588\u2588\u2554\u255D\u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2588\u2557\u2588\u2588\u2551
2816
+ \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2554\u255D \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u255A\u2588\u2588\u2588\u2554\u2588\u2588\u2588\u2554\u255D
2817
+ \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u255D\u255A\u2550\u2550\u255D
2818
+ `;
2819
+ var RULE = "\u2550".repeat(85);
2820
+ var FEEDS_IN_ORDER = ["nothing", "clock", "spool"];
2821
+ function printBanner(facts) {
2822
+ console.log(
2823
+ [
2824
+ RAVENFLOW,
2825
+ RULE,
2826
+ "[Config]",
2827
+ ...configLines(facts),
2828
+ "",
2829
+ "[Registered Jobs]",
2830
+ ...registeredJobLines(facts.jobs),
2831
+ "",
2832
+ "[Scheduled Jobs]",
2833
+ ...schedulerLines(facts.schedulers),
2834
+ RULE,
2835
+ "Ready to process jobs..."
2836
+ ].join("\n")
2837
+ );
2838
+ }
2839
+ function configLines(facts) {
2840
+ return [
2841
+ ` -> queue: ${facts.queue}`,
2842
+ ` -> concurrency: ${facts.concurrency}`,
2843
+ ` -> redis: ${addressOf(facts.redis)}`,
2844
+ ` -> mongo: ${mongoLineOf(facts)}`
2845
+ ];
2846
+ }
2847
+ function addressOf(redis) {
2848
+ if (isRedisInstance(redis)) return "an ioredis client";
2849
+ const {
2850
+ url,
2851
+ host = "localhost",
2852
+ port = 6379,
2853
+ db = 0
2854
+ } = redis;
2855
+ return url ? withoutPassword(url) : `${host}:${port}/${db}`;
2856
+ }
2857
+ function mongoLineOf({ mongo, recorderOpening }) {
2858
+ const address = withoutPassword(mongo);
2859
+ if (!recorderOpening.opened) {
2860
+ return `${address} (job results are not recorded: ${recorderOpening.because})`;
2861
+ }
2862
+ return address;
2863
+ }
2864
+ function withoutPassword(uri) {
2865
+ return uri.replace(/\/\/([^:/@]*):[^@]*@/, "//$1:***@");
2866
+ }
2867
+ function registeredJobLines(jobs) {
2868
+ const lines = Object.entries(jobs).map(([name, job]) => ({ name, feed: feedOf(job) })).sort((a, b) => feedRank(a.feed) - feedRank(b.feed)).map(({ name, feed }) => ` . ${name}${feed.label}`);
2869
+ return lines.length > 0 ? lines : [" (none)"];
2870
+ }
2871
+ function feedOf(job) {
2872
+ const cron = job.trigger?.cron;
2873
+ if (cron) return { kind: "clock", label: ` (\u21BB ${cron})` };
2874
+ if (isSpoolFed(job)) return { kind: "spool", label: spoolLabelOf(job) };
2875
+ return { kind: "nothing", label: "" };
2876
+ }
2877
+ function spoolLabelOf({ trigger, pace }) {
2878
+ const ceiling = inFlightCeilingOf(trigger.spool, pace);
2879
+ return ` (\u2190 ${trigger.spool.name}, ${ceiling} in flight at most)`;
2880
+ }
2881
+ function feedRank(feed) {
2882
+ return FEEDS_IN_ORDER.indexOf(feed.kind);
2883
+ }
2884
+ function schedulerLines(changes) {
2885
+ const lines = [
2886
+ ...changes.added.map(({ name, pattern }) => ` + ${name} \u2192 ${pattern}`),
2887
+ ...changes.rescheduled.map(
2888
+ ({ name, pattern, previousPattern }) => ` \u21BB ${name} \u2192 ${pattern} (was ${previousPattern})`
2889
+ ),
2890
+ ...changes.removed.map((name) => ` - ${name}`)
2891
+ ];
2892
+ return lines.length > 0 ? lines : [" (no change)"];
2893
+ }
2894
+
2895
+ // src/worker/schedulers.ts
2896
+ async function reconcileSchedulers(queue, jobs) {
2897
+ const declared = cronSchedulesOf(jobs);
2898
+ const existing = await patternsByScheduler(queue);
2899
+ const changes = changesBetween(declared, existing);
2900
+ await Promise.all(
2901
+ changes.removed.map((key) => queue.removeJobScheduler(key))
2902
+ );
2903
+ await Promise.all(declared.map((schedule) => upsert(queue, schedule)));
2904
+ return changes;
2905
+ }
2906
+ function cronSchedulesOf(jobs) {
2907
+ return Object.entries(jobs).flatMap(([name, job]) => {
2908
+ const pattern = job.trigger?.cron;
2909
+ if (pattern === void 0) return [];
2910
+ return [{ name, pattern, options: buildJobOptions(job) }];
2911
+ });
2912
+ }
2913
+ async function patternsByScheduler(queue) {
2914
+ const schedulers = await queue.getJobSchedulers();
2915
+ return new Map(
2916
+ schedulers.map(({ key, pattern }) => [key, pattern ?? "no pattern"])
2917
+ );
2918
+ }
2919
+ function changesBetween(declared, existing) {
2920
+ const declaredNames = new Set(declared.map(({ name }) => name));
2921
+ return {
2922
+ added: declared.filter(({ name }) => !existing.has(name)),
2923
+ rescheduled: declared.flatMap(
2924
+ (schedule) => reschedulingOf(schedule, existing)
2925
+ ),
2926
+ removed: [...existing.keys()].filter((key) => !declaredNames.has(key))
2927
+ };
2928
+ }
2929
+ function reschedulingOf(schedule, existing) {
2930
+ const previousPattern = existing.get(schedule.name);
2931
+ if (previousPattern === void 0) return [];
2932
+ if (previousPattern === schedule.pattern) return [];
2933
+ return [{ ...schedule, previousPattern }];
2934
+ }
2935
+ async function upsert(queue, { name, pattern, options }) {
2936
+ try {
2937
+ await queue.upsertJobScheduler(name, { pattern }, { name, opts: options });
2938
+ } catch (error) {
2939
+ throw new Error(
2940
+ `[ravenflow] "${name}" could not be scheduled on "${pattern}": ${describeError(error)}`
2941
+ );
2942
+ }
2943
+ }
2944
+
2945
+ // src/worker/serving.ts
2946
+ async function serve(serving, settings) {
2947
+ const { options, jobs } = settings;
2948
+ const { queue, worker } = serving;
2949
+ serving.mongoWatch.open();
2950
+ await serving.controlPlaneReader.open();
2951
+ const recorderOpening = await openRecorder(serving, settings);
2952
+ await serving.jobResultsAggregator.open();
2953
+ await serving.spoolFeeder.open(worker);
2954
+ const schedulers = await reconcileSchedulers(queue, jobs);
2955
+ takeJobs(worker, settings);
2956
+ await worker.waitUntilReady();
2957
+ printBanner({
2958
+ queue: options.queue,
2959
+ concurrency: worker.concurrency,
2960
+ redis: options.redis,
2961
+ mongo: options.mongo,
2962
+ recorderOpening,
2963
+ jobs,
2964
+ schedulers
2965
+ });
2966
+ }
2967
+ async function openRecorder({ jobResultsRecorder, worker }, settings) {
2968
+ try {
2969
+ await jobResultsRecorder.open(worker);
2970
+ } catch (error) {
2971
+ sayJobResultsAreNotRecorded(settings, error);
2972
+ return { opened: false, because: describeError(error) };
2973
+ }
2974
+ return { opened: true };
2975
+ }
2976
+ function sayJobResultsAreNotRecorded({ options, logger }, error) {
2977
+ logger.error(
2978
+ { err: error },
2979
+ `[ravenflow] the job results recorder could not start: the worker serves "${options.queue}", and its job results are not recorded.`
2980
+ );
2981
+ }
2982
+ function takeJobs(worker, { options, logger }) {
2983
+ worker.run().catch((error) => {
2984
+ logger.error(
2985
+ { err: error },
2986
+ `[ravenflow] the worker serving "${options.queue}" stopped taking jobs.`
2987
+ );
2988
+ });
2989
+ }
2990
+ async function closeInOrder(serving) {
2991
+ await stopWatchingMongoDB(serving);
2992
+ await stopTheTicks(serving);
2993
+ await stopTheWorker(serving);
2994
+ await writeWhatItsJobsLeft(serving);
2995
+ await closeTheConnections(serving);
2996
+ }
2997
+ async function stopWatchingMongoDB(serving) {
2998
+ await serving.mongoWatch.close();
2999
+ }
3000
+ async function stopTheTicks(serving) {
3001
+ await Promise.all([
3002
+ serving.jobResultsAggregator.stopTicking(),
3003
+ serving.spoolFeeder.stopTicking()
3004
+ ]);
3005
+ }
3006
+ async function stopTheWorker(serving) {
3007
+ serving.jobResultsRecorder.stopHoldingJobs();
3008
+ await serving.worker.close();
3009
+ }
3010
+ async function writeWhatItsJobsLeft(serving) {
3011
+ await Promise.all([
3012
+ serving.jobResultsRecorder.close(),
3013
+ serving.spoolFeeder.finishTheRemovals()
3014
+ ]);
3015
+ }
3016
+ async function closeTheConnections(serving) {
3017
+ await serving.spoolFeeder.close();
3018
+ await serving.jobResultsAggregator.close();
3019
+ await serving.controlPlaneReader.close();
3020
+ await serving.queue.close();
3021
+ }
3022
+
3023
+ // src/worker/create-ravenflow-worker.ts
3024
+ function createRavenflowWorker(config, options) {
3025
+ validateConfig(config);
3026
+ refuseAQueueItsConfigDoesNotDeclare(options.queue, config);
3027
+ return workerOf({
3028
+ config,
3029
+ options,
3030
+ jobs: jobsDeclaredOn(options.queue, config),
3031
+ logger: createLogger(config.logger)
3032
+ });
3033
+ }
3034
+ function workerOf(settings) {
3035
+ let serving = null;
3036
+ let starting = null;
3037
+ let closing = null;
3038
+ return { start, close };
3039
+ async function start() {
3040
+ if (serving) throw aSecondStart(settings);
3041
+ const opened = openServing(settings, beginAWithdrawal);
3042
+ serving = opened;
3043
+ starting = serveOrStop(opened);
2936
3044
  try {
2937
- await this.starting;
3045
+ await starting;
2938
3046
  } finally {
2939
- this.starting = null;
3047
+ starting = null;
2940
3048
  }
2941
3049
  }
2942
- async close() {
2943
- await Promise.allSettled([this.starting]);
2944
- if (this.serving === null) return;
2945
- this.closing ?? (this.closing = this.stopServing(this.serving));
2946
- await this.closing;
3050
+ async function close() {
3051
+ await Promise.allSettled([starting]);
3052
+ if (serving === null) return;
3053
+ closing ?? (closing = stopServing(serving));
3054
+ await closing;
2947
3055
  }
2948
- async serveOrStop(serving) {
3056
+ async function serveOrStop(opened) {
2949
3057
  try {
2950
- await this.serve(serving);
3058
+ await serve(opened, settings);
2951
3059
  } catch (error) {
2952
- await this.stopServing(serving);
3060
+ await stopServing(opened);
2953
3061
  throw error;
2954
3062
  }
2955
3063
  }
2956
- async stopServing(serving) {
3064
+ async function stopServing(opened) {
2957
3065
  try {
2958
- await closeInOrder(serving);
3066
+ await closeInOrder(opened);
2959
3067
  } finally {
2960
- this.serving = null;
2961
- this.closing = null;
2962
- }
2963
- }
2964
- refuseASecondStart() {
2965
- if (!this.serving) return;
2966
- throw new Error(
2967
- `[ravenflow] this worker has already started on "${this.options.queue}".`
2968
- );
2969
- }
2970
- openServing() {
2971
- const queue = openQueue(this.options.queue, {
2972
- connection: this.options.redis,
2973
- retention: this.config.queuesRetention
2974
- });
2975
- logBullmqErrors(queue, this.logger);
2976
- const controlPlaneReader = this.buildControlPlaneReader();
2977
- const jobResultsRecorder = this.buildJobResultsRecorder(queue);
2978
- const worker = openWorker(this.options, {
2979
- jobs: this.jobs,
2980
- logger: this.logger,
2981
- queue,
2982
- controls: controlPlaneReader,
2983
- jobResultsRecorder
2984
- });
2985
- return {
2986
- queue,
2987
- controlPlaneReader,
2988
- jobResultsRecorder,
2989
- jobResultsAggregator: this.buildJobResultsAggregator(),
2990
- spoolFeeder: this.buildSpoolFeeder(queue, controlPlaneReader),
2991
- mongoWatch: this.buildMongoWatch(),
2992
- worker
2993
- };
2994
- }
2995
- buildControlPlaneReader() {
2996
- return new ControlPlaneReader({
2997
- redis: this.options.redis,
2998
- mongo: this.options.mongo,
2999
- logger: this.logger
3000
- });
3001
- }
3002
- buildJobResultsRecorder(queue) {
3003
- return new JobResultsRecorder({
3004
- mongo: this.options.mongo,
3005
- retentionHours: this.config.jobResultsRetentionHours ?? DEFAULT_JOB_RESULTS_RETENTION_HOURS,
3006
- queue,
3007
- jobs: this.jobs,
3008
- logger: this.logger
3009
- });
3010
- }
3011
- buildJobResultsAggregator() {
3012
- return new JobResultsAggregator({
3013
- redis: this.options.redis,
3014
- mongo: this.options.mongo,
3015
- retentionHours: this.config.aggregatedResultsRetentionHours ?? DEFAULT_AGGREGATED_RESULTS_RETENTION_HOURS,
3016
- logger: this.logger
3017
- });
3018
- }
3019
- buildSpoolFeeder(queue, controls) {
3020
- return new SpoolFeeder({
3021
- redis: this.options.redis,
3022
- mongo: this.options.mongo,
3023
- queue,
3024
- controls,
3025
- jobs: this.jobs,
3026
- logger: this.logger
3027
- });
3028
- }
3029
- buildMongoWatch() {
3030
- return new MongoWatch({
3031
- mongo: this.options.mongo,
3032
- gracePeriodMs: this.options.withdrawal?.afterMs ?? DEFAULT_GRACE_PERIOD_MS,
3033
- logger: this.logger,
3034
- onDurableLoss: (unreachableForMs) => this.beginAWithdrawal(unreachableForMs)
3035
- });
3036
- }
3037
- beginAWithdrawal(unreachableForMs) {
3038
- const withdrawing = {
3039
- queue: this.options.queue,
3040
- logger: this.logger,
3041
- onWithdraw: this.options.withdrawal?.onWithdraw,
3042
- close: () => this.close()
3043
- };
3044
- void withdraw(withdrawing, unreachableForMs);
3045
- }
3046
- async serve({
3047
- queue,
3048
- controlPlaneReader,
3049
- jobResultsRecorder,
3050
- jobResultsAggregator,
3051
- spoolFeeder,
3052
- mongoWatch,
3053
- worker
3054
- }) {
3055
- mongoWatch.open();
3056
- await controlPlaneReader.open();
3057
- const recorderOpening = await this.openRecorder(jobResultsRecorder, worker);
3058
- await jobResultsAggregator.open();
3059
- await spoolFeeder.open(worker);
3060
- const schedulers = await reconcileSchedulers(queue, this.jobs);
3061
- this.takeJobs(worker);
3062
- await worker.waitUntilReady();
3063
- printBanner({
3064
- queue: this.options.queue,
3065
- concurrency: worker.concurrency,
3066
- redis: this.options.redis,
3067
- mongo: this.options.mongo,
3068
- recorderOpening,
3069
- jobs: this.jobs,
3070
- schedulers
3071
- });
3072
- }
3073
- async openRecorder(jobResultsRecorder, worker) {
3074
- try {
3075
- await jobResultsRecorder.open(worker);
3076
- } catch (error) {
3077
- this.sayJobResultsAreNotRecorded(error);
3078
- return { opened: false, because: describeError(error) };
3068
+ serving = null;
3069
+ closing = null;
3079
3070
  }
3080
- return { opened: true };
3081
3071
  }
3082
- sayJobResultsAreNotRecorded(error) {
3083
- this.logger.error(
3084
- { err: error },
3085
- `[ravenflow] the job results recorder could not start: the worker serves "${this.options.queue}", and its job results are not recorded.`
3086
- );
3072
+ function beginAWithdrawal(unreachableForMs) {
3073
+ void withdraw(withdrawingOf(settings, close), unreachableForMs);
3087
3074
  }
3088
- takeJobs(worker) {
3089
- worker.run().catch((error) => {
3090
- this.logger.error(
3091
- { err: error },
3092
- `[ravenflow] the worker serving "${this.options.queue}" stopped taking jobs.`
3093
- );
3094
- });
3095
- }
3096
- };
3097
-
3098
- // src/worker/create-ravenflow-worker.ts
3099
- function createRavenflowWorker(config, options) {
3100
- return new RavenflowWorker(config, options);
3075
+ }
3076
+ function aSecondStart({ options }) {
3077
+ return new Error(
3078
+ `[ravenflow] this worker has already started on "${options.queue}".`
3079
+ );
3080
+ }
3081
+ function withdrawingOf({ options, logger }, close) {
3082
+ return {
3083
+ queue: options.queue,
3084
+ logger,
3085
+ onWithdraw: options.withdrawal?.onWithdraw,
3086
+ close
3087
+ };
3101
3088
  }
3102
3089
 
3103
3090
  // src/config/ravenflow-config.ts