mongodash 2.6.0 → 2.8.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.
Files changed (66) hide show
  1. package/README.md +45 -0
  2. package/dist/lib/ConcurrentRunner.js +47 -2
  3. package/dist/lib/ConcurrentRunner.js.map +1 -1
  4. package/dist/lib/createContinuousLock.js +23 -6
  5. package/dist/lib/createContinuousLock.js.map +1 -1
  6. package/dist/lib/cronTasks.js +119 -64
  7. package/dist/lib/cronTasks.js.map +1 -1
  8. package/dist/lib/index.js +11 -6
  9. package/dist/lib/index.js.map +1 -1
  10. package/dist/lib/reactiveTasks/LeaderElector.js +21 -3
  11. package/dist/lib/reactiveTasks/LeaderElector.js.map +1 -1
  12. package/dist/lib/reactiveTasks/MetricsCollector.js +118 -39
  13. package/dist/lib/reactiveTasks/MetricsCollector.js.map +1 -1
  14. package/dist/lib/reactiveTasks/ReactiveTaskPlanner.js +66 -31
  15. package/dist/lib/reactiveTasks/ReactiveTaskPlanner.js.map +1 -1
  16. package/dist/lib/reactiveTasks/ReactiveTaskRepository.js +19 -1
  17. package/dist/lib/reactiveTasks/ReactiveTaskRepository.js.map +1 -1
  18. package/dist/lib/reactiveTasks/ReactiveTaskTypes.js +7 -1
  19. package/dist/lib/reactiveTasks/ReactiveTaskTypes.js.map +1 -1
  20. package/dist/lib/reactiveTasks/ReactiveTaskWorker.js +80 -5
  21. package/dist/lib/reactiveTasks/ReactiveTaskWorker.js.map +1 -1
  22. package/dist/lib/reactiveTasks/index.js +20 -13
  23. package/dist/lib/reactiveTasks/index.js.map +1 -1
  24. package/dist/lib/task-management/OperationalTaskController.js +1 -1
  25. package/dist/lib/task-management/OperationalTaskController.js.map +1 -1
  26. package/dist/lib/testing/assertNoReactiveTaskErrors.js +16 -12
  27. package/dist/lib/testing/assertNoReactiveTaskErrors.js.map +1 -1
  28. package/dist/lib/testing/index.js +2 -0
  29. package/dist/lib/testing/index.js.map +1 -1
  30. package/dist/lib/testing/resolveWhitelistFilter.js +48 -0
  31. package/dist/lib/testing/resolveWhitelistFilter.js.map +1 -0
  32. package/dist/lib/testing/waitUntilReactiveTasksIdle.js +17 -46
  33. package/dist/lib/testing/waitUntilReactiveTasksIdle.js.map +1 -1
  34. package/dist/types/ConcurrentRunner.d.ts +16 -0
  35. package/dist/types/createContinuousLock.d.ts +17 -1
  36. package/dist/types/cronTasks.d.ts +17 -2
  37. package/dist/types/index.d.ts +2 -2
  38. package/dist/types/reactiveTasks/LeaderElector.d.ts +15 -1
  39. package/dist/types/reactiveTasks/MetricsCollector.d.ts +19 -8
  40. package/dist/types/reactiveTasks/ReactiveTaskPlanner.d.ts +11 -0
  41. package/dist/types/reactiveTasks/ReactiveTaskRepository.d.ts +10 -1
  42. package/dist/types/reactiveTasks/ReactiveTaskTypes.d.ts +19 -0
  43. package/dist/types/reactiveTasks/index.d.ts +8 -2
  44. package/dist/types/testing/assertNoReactiveTaskErrors.d.ts +4 -4
  45. package/dist/types/testing/index.d.ts +2 -0
  46. package/dist/types/testing/resolveWhitelistFilter.d.ts +35 -0
  47. package/dist/types/testing/waitUntilReactiveTasksIdle.d.ts +7 -13
  48. package/docs/.vitepress/config.mts +9 -1
  49. package/docs/cron-tasks.md +130 -1
  50. package/docs/error-handling.md +156 -0
  51. package/docs/reactive-tasks/guides.md +1 -1
  52. package/docs/reactive-tasks/index.md +2 -2
  53. package/docs/reactive-tasks/monitoring.md +7 -0
  54. package/docs/reactive-tasks/testing.md +187 -0
  55. package/docs/testing.md +60 -94
  56. package/package.json +36 -24
  57. package/docs/.vitepress/cache/deps/_metadata.json +0 -31
  58. package/docs/.vitepress/cache/deps/chunk-LE5NDSFD.js +0 -12824
  59. package/docs/.vitepress/cache/deps/chunk-LE5NDSFD.js.map +0 -7
  60. package/docs/.vitepress/cache/deps/package.json +0 -3
  61. package/docs/.vitepress/cache/deps/vitepress___@vue_devtools-api.js +0 -4505
  62. package/docs/.vitepress/cache/deps/vitepress___@vue_devtools-api.js.map +0 -7
  63. package/docs/.vitepress/cache/deps/vitepress___@vueuse_core.js +0 -9731
  64. package/docs/.vitepress/cache/deps/vitepress___@vueuse_core.js.map +0 -7
  65. package/docs/.vitepress/cache/deps/vue.js +0 -347
  66. package/docs/.vitepress/cache/deps/vue.js.map +0 -7
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.assertNoReactiveTaskErrors = assertNoReactiveTaskErrors;
4
4
  const reactiveTasks_1 = require("../reactiveTasks");
5
+ const resolveWhitelistFilter_1 = require("./resolveWhitelistFilter");
5
6
  /**
6
7
  * Asserts that no reactive tasks have failed during the test run.
7
8
  * Checks the 'executionHistory' and 'lastError' of tasks in all registered collections.
@@ -13,20 +14,23 @@ async function assertNoReactiveTaskErrors(options) {
13
14
  registry = schedulerToUse.getRegistry();
14
15
  const entries = registry.getAllEntries();
15
16
  const errorsFound = [];
17
+ const hasWhitelist = options.whitelist && options.whitelist.length > 0;
16
18
  for (const entry of entries) {
19
+ // If whitelist is active, check if this collection is relevant
20
+ let whitelistFilter = null;
21
+ if (hasWhitelist) {
22
+ const resolution = await (0, resolveWhitelistFilter_1.resolveWhitelistFilter)(options.whitelist, entry.sourceCollection);
23
+ if (resolution === 'skip')
24
+ continue;
25
+ if (resolution !== 'matchAll') {
26
+ whitelistFilter = resolution;
27
+ }
28
+ }
17
29
  // Build independent query for each collection
18
- const query = {
19
- // Optimization: Filter at database level for faster lookup
20
- $or: [
21
- { 'executionHistory.status': 'failed', 'executionHistory.at': { $gte: options.since } },
22
- // Also check lastError if it happened recently (though executionHistory covers history)
23
- // We rely on executionHistory for the time-based check.
24
- ],
30
+ const baseQuery = {
31
+ $or: [{ 'executionHistory.status': 'failed', 'executionHistory.at': { $gte: options.since } }],
25
32
  };
26
- if (options.sourceDocIds && options.sourceDocIds.length > 0) {
27
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
28
- query.sourceDocId = { $in: options.sourceDocIds };
29
- }
33
+ const query = whitelistFilter ? { $and: [baseQuery, whitelistFilter] } : baseQuery;
30
34
  const tasksWithHistory = await entry.tasksCollection.find(query).toArray();
31
35
  for (const taskRecord of tasksWithHistory) {
32
36
  if (!taskRecord.executionHistory)
@@ -39,7 +43,7 @@ async function assertNoReactiveTaskErrors(options) {
39
43
  if (item.status !== 'failed')
40
44
  continue;
41
45
  const errorMessage = item.error || 'Unknown error';
42
- // 3. Check Whitelist
46
+ // 3. Check Whitelist (excludeErrors)
43
47
  let isExcluded = false;
44
48
  if (options.excludeErrors) {
45
49
  for (const pattern of options.excludeErrors) {
@@ -1 +1 @@
1
- {"version":3,"file":"assertNoReactiveTaskErrors.js","sourceRoot":"","sources":["../../../src/testing/assertNoReactiveTaskErrors.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"assertNoReactiveTaskErrors.js","sourceRoot":"","sources":["../../../src/testing/assertNoReactiveTaskErrors.ts"],"names":[],"mappings":"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"}
@@ -16,5 +16,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./assertNoReactiveTaskErrors"), exports);
18
18
  __exportStar(require("./configureForTesting"), exports);
19
+ __exportStar(require("./resolveWhitelistFilter"), exports);
20
+ __exportStar(require("./waitUntil"), exports);
19
21
  __exportStar(require("./waitUntilReactiveTasksIdle"), exports);
20
22
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/testing/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,+DAA6C;AAC7C,wDAAsC;AACtC,+DAA6C"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/testing/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,+DAA6C;AAC7C,wDAAsC;AACtC,2DAAyC;AACzC,8CAA4B;AAC5B,+DAA6C"}
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveWhitelistFilter = resolveWhitelistFilter;
4
+ /**
5
+ * Build the `Filter<ReactiveTaskRecord>` for a single registry entry based on
6
+ * the provided whitelist rules. Extracted from `waitUntilReactiveTasksIdle` /
7
+ * `assertNoReactiveTaskErrors` so the two utilities cannot drift.
8
+ */
9
+ async function resolveWhitelistFilter(whitelist, sourceCollection) {
10
+ const rules = whitelist.filter((rule) => rule.collection === sourceCollection.collectionName);
11
+ if (rules.length === 0) {
12
+ return 'skip';
13
+ }
14
+ const criteria = [];
15
+ for (const rule of rules) {
16
+ let ruleIds = null;
17
+ if (rule.filter) {
18
+ const matchingDocs = (await sourceCollection.find(rule.filter, { projection: { _id: 1 } }).toArray());
19
+ ruleIds = matchingDocs.map((d) => d._id);
20
+ }
21
+ if (ruleIds === null && !rule.task) {
22
+ // Rule covers every document in this collection and every task.
23
+ return 'matchAll';
24
+ }
25
+ // A filter-scoped rule that matched zero source documents can never
26
+ // contribute tasks of its own - drop it even when a task is also
27
+ // specified (the AND of "task=X" and "sourceDocId IN []" is still
28
+ // empty, so emitting that filter just produces useless `$in: []` queries).
29
+ if (ruleIds !== null && ruleIds.length === 0) {
30
+ continue;
31
+ }
32
+ const ruleCriteria = {};
33
+ if (rule.task) {
34
+ ruleCriteria.task = rule.task;
35
+ }
36
+ if (ruleIds !== null) {
37
+ ruleCriteria.sourceDocId = { $in: ruleIds };
38
+ }
39
+ criteria.push(ruleCriteria);
40
+ }
41
+ if (criteria.length === 0) {
42
+ // Rules matched this collection but every rule resolved to an empty
43
+ // document set - nothing to wait for / check.
44
+ return 'skip';
45
+ }
46
+ return { $or: criteria };
47
+ }
48
+ //# sourceMappingURL=resolveWhitelistFilter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolveWhitelistFilter.js","sourceRoot":"","sources":["../../../src/testing/resolveWhitelistFilter.ts"],"names":[],"mappings":";;AAqCA,wDAiDC;AAtDD;;;;GAIG;AACI,KAAK,UAAU,sBAAsB,CACxC,SAA0B,EAC1B,gBAAuE;IAEvE,MAAM,KAAK,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,UAAU,KAAK,gBAAgB,CAAC,cAAc,CAAC,CAAC;IAC9F,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACrB,OAAO,MAAM,CAAC;IAClB,CAAC;IAED,MAAM,QAAQ,GAAsC,EAAE,CAAC;IAEvD,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACvB,IAAI,OAAO,GAAqB,IAAI,CAAC;QAErC,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YACd,MAAM,YAAY,GAAG,CAAC,MAAM,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,UAAU,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,OAAO,EAAE,CAAe,CAAC;YACpH,OAAO,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;QAC7C,CAAC;QAED,IAAI,OAAO,KAAK,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;YACjC,gEAAgE;YAChE,OAAO,UAAU,CAAC;QACtB,CAAC;QAED,oEAAoE;QACpE,iEAAiE;QACjE,kEAAkE;QAClE,2EAA2E;QAC3E,IAAI,OAAO,KAAK,IAAI,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC3C,SAAS;QACb,CAAC;QAED,MAAM,YAAY,GAA+B,EAAE,CAAC;QACpD,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;YACZ,YAAY,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;QAClC,CAAC;QACD,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;YACnB,YAAY,CAAC,WAAW,GAAG,EAAE,GAAG,EAAE,OAA8C,EAAE,CAAC;QACvF,CAAC;QACD,QAAQ,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;IAChC,CAAC;IAED,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxB,oEAAoE;QACpE,8CAA8C;QAC9C,OAAO,MAAM,CAAC;IAClB,CAAC;IAED,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,CAAC;AAC7B,CAAC"}
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.waitUntilReactiveTasksIdle = waitUntilReactiveTasksIdle;
4
4
  const _debug = require("debug");
5
5
  const reactiveTasks_1 = require("../reactiveTasks");
6
+ const resolveWhitelistFilter_1 = require("./resolveWhitelistFilter");
6
7
  const waitUntil_1 = require("./waitUntil");
7
8
  const debug = _debug('mongodash:testing');
8
9
  async function waitUntilReactiveTasksIdle(customOptions = {}) {
@@ -13,16 +14,22 @@ async function waitUntilReactiveTasksIdle(customOptions = {}) {
13
14
  const planner = reactiveTasks_1._scheduler.taskPlannerInstance;
14
15
  const runner = reactiveTasks_1._scheduler.concurrentRunnerInstance;
15
16
  const registry = reactiveTasks_1._scheduler.getRegistry();
16
- // --- 1. Global Checks (Always run) ---
17
- // 1. Check Internal Buffers (Planner)
17
+ // --- 1. Global Checks ---
18
+ // The planner buffer check is kept in whitelist mode too: change-stream
19
+ // events for *our* collections land there before task records exist,
20
+ // and skipping the check altogether would let us return idle before
21
+ // the events have been turned into rows the DB check below can see.
22
+ // The worker check is only applied globally - in whitelist mode
23
+ // other tests' workers must not block us.
18
24
  if (planner && !planner.isEmpty) {
19
25
  debug('Planner not empty');
20
26
  return false;
21
27
  }
22
- // 2. Check Active Workers (Runner)
23
- if (runner && runner.activeWorkers > 0) {
24
- debug(`Active workers: ${runner.activeWorkers}`);
25
- return false;
28
+ if (!hasWhitelist) {
29
+ if (runner && runner.activeWorkers > 0) {
30
+ debug(`Active workers: ${runner.activeWorkers}`);
31
+ return false;
32
+ }
26
33
  }
27
34
  // --- 2. Check Database ---
28
35
  const entries = registry.getAllEntries();
@@ -31,47 +38,11 @@ async function waitUntilReactiveTasksIdle(customOptions = {}) {
31
38
  // If whitelisting is active, we only check tasks that match the whitelist
32
39
  let whitelistFilter = null;
33
40
  if (hasWhitelist) {
34
- const rules = customOptions.whitelist.filter((rule) => rule.collection === entry.sourceCollection.collectionName);
35
- if (rules.length === 0) {
41
+ const resolution = await (0, resolveWhitelistFilter_1.resolveWhitelistFilter)(customOptions.whitelist, entry.sourceCollection);
42
+ if (resolution === 'skip')
36
43
  continue;
37
- }
38
- const criteria = [];
39
- let matchAll = false;
40
- for (const rule of rules) {
41
- let ruleIds = null;
42
- if (rule.filter) {
43
- // If we have a filter, we need to find which docs match it.
44
- // We can't filter tasks directly by source properties efficiently without joining,
45
- // so we find the matching source docs first.
46
- const matchingDocs = (await entry.sourceCollection.find(rule.filter, { projection: { _id: 1 } }).toArray());
47
- ruleIds = matchingDocs.map((d) => d._id);
48
- }
49
- if (ruleIds === null && !rule.task) {
50
- // One rule validates 'all', so we wait for everything in this collection
51
- matchAll = true;
52
- break;
53
- }
54
- if (rule.task) {
55
- criteria.push({ task: rule.task });
56
- }
57
- if (ruleIds !== null) {
58
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
59
- criteria.push({ sourceDocId: { $in: ruleIds } });
60
- }
61
- }
62
- if (!matchAll) {
63
- if (criteria.length > 0) {
64
- whitelistFilter = { $or: criteria };
65
- }
66
- else {
67
- // Whitelist is active, but we have rules that result in effectively "nothing"
68
- // (e.g. filter returned no docs).
69
- // If we have NO criteria and NO matchAll, it implies we wait for nothing on this collection?
70
- // Or should we treat it as blocking?
71
- // If filter didn't match any doc, then we effectively wait for nothing for that rule.
72
- // If ALL rules resulted in nothing, we continue to next entry.
73
- continue;
74
- }
44
+ if (resolution !== 'matchAll') {
45
+ whitelistFilter = resolution;
75
46
  }
76
47
  }
77
48
  const stableThresholdMs = (options.timeoutMs || 0) + (options.stabilityDurationMs || 0) + 100;
@@ -1 +1 @@
1
- {"version":3,"file":"waitUntilReactiveTasksIdle.js","sourceRoot":"","sources":["../../../src/testing/waitUntilReactiveTasksIdle.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"waitUntilReactiveTasksIdle.js","sourceRoot":"","sources":["../../../src/testing/waitUntilReactiveTasksIdle.ts"],"names":[],"mappings":";;AA+BA,gEA0EC;AAzGD,gCAAgC;AAEhC,oDAAkE;AAClE,qEAAiF;AACjF,2CAA0D;AAE1D,MAAM,KAAK,GAAG,MAAM,CAAC,mBAAmB,CAAC,CAAC;AAyBnC,KAAK,UAAU,0BAA0B,CAAC,gBAAmD,EAAE;IAClG,MAAM,OAAO,mBACT,SAAS,EAAE,KAAK,EAChB,cAAc,EAAE,EAAE,EAClB,mBAAmB,EAAE,GAAG,IACrB,aAAa,CACnB,CAAC;IAEF,MAAM,YAAY,GAAG,aAAa,CAAC,SAAS,IAAI,aAAa,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,CAAC;IAEnF,MAAM,IAAA,qBAAS,EAAC,KAAK,IAAI,EAAE;QACvB,6BAA6B;QAC7B,MAAM,OAAO,GAAG,0BAAU,CAAC,mBAAmB,CAAC;QAC/C,MAAM,MAAM,GAAG,0BAAU,CAAC,wBAAwB,CAAC;QACnD,MAAM,QAAQ,GAAG,0BAAU,CAAC,WAAW,EAAE,CAAC;QAE1C,2BAA2B;QAC3B,wEAAwE;QACxE,qEAAqE;QACrE,oEAAoE;QACpE,oEAAoE;QACpE,gEAAgE;QAChE,0CAA0C;QAC1C,IAAI,OAAO,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;YAC9B,KAAK,CAAC,mBAAmB,CAAC,CAAC;YAC3B,OAAO,KAAK,CAAC;QACjB,CAAC;QACD,IAAI,CAAC,YAAY,EAAE,CAAC;YAChB,IAAI,MAAM,IAAI,MAAM,CAAC,aAAa,GAAG,CAAC,EAAE,CAAC;gBACrC,KAAK,CAAC,mBAAmB,MAAM,CAAC,aAAa,EAAE,CAAC,CAAC;gBACjD,OAAO,KAAK,CAAC;YACjB,CAAC;QACL,CAAC;QAED,4BAA4B;QAC5B,MAAM,OAAO,GAAG,QAAQ,CAAC,aAAa,EAAE,CAAC;QAEzC,wEAAwE;QACxE,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;YAC1B,0EAA0E;YAC1E,IAAI,eAAe,GAAsC,IAAI,CAAC;YAE9D,IAAI,YAAY,EAAE,CAAC;gBACf,MAAM,UAAU,GAAG,MAAM,IAAA,+CAAsB,EAAC,aAAa,CAAC,SAAU,EAAE,KAAK,CAAC,gBAAgB,CAAC,CAAC;gBAClG,IAAI,UAAU,KAAK,MAAM;oBAAE,SAAS;gBACpC,IAAI,UAAU,KAAK,UAAU,EAAE,CAAC;oBAC5B,eAAe,GAAG,UAAU,CAAC;gBACjC,CAAC;YACL,CAAC;YAED,MAAM,iBAAiB,GAAG,CAAC,OAAO,CAAC,SAAS,IAAI,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,mBAAmB,IAAI,CAAC,CAAC,GAAG,GAAG,CAAC;YAE9F,MAAM,SAAS,GAA+B;gBAC1C,GAAG,EAAE;oBACD,EAAE,MAAM,EAAE,EAAE,GAAG,EAAE,CAAC,YAAY,EAAE,kBAAkB,CAAC,EAAE,EAAE;oBACvD;wBACI,MAAM,EAAE,SAAS;wBACjB,GAAG,EAAE,CAAC,EAAE,SAAS,EAAE,EAAE,IAAI,EAAE,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,iBAAiB,CAAC,EAAE,EAAE,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;qBAChG;iBACJ;aACJ,CAAC;YAEF,MAAM,KAAK,GAA+B,eAAe,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,SAAS,EAAE,eAAe,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;YAE/G,MAAM,KAAK,GAAG,MAAM,KAAK,CAAC,eAAe,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC;YAEhE,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;gBACZ,KAAK,CAAC,cAAc,KAAK,CAAC,eAAe,CAAC,cAAc,QAAQ,KAAK,eAAe,CAAC,CAAC;gBACtF,OAAO,KAAK,CAAC;YACjB,CAAC;QACL,CAAC;QAED,OAAO,IAAI,CAAC;IAChB,CAAC,EAAE,OAAO,CAAC,CAAC;AAChB,CAAC"}
@@ -23,6 +23,22 @@ export declare class ConcurrentRunner {
23
23
  start(tryRunATask: TryRunATaskCallback): void;
24
24
  stop(): Promise<void>;
25
25
  speedUp(sourceName: string): void;
26
+ /**
27
+ * Override the back-off schedule so the next poll for `sourceName`
28
+ * happens at approximately `runAt` (a millisecond epoch timestamp).
29
+ * Intended for callers that already know when their next unit of work
30
+ * is due - e.g. cron scheduling an hour out - to skip wasted polls.
31
+ *
32
+ * - `runAt` must be a finite number; non-finite values are ignored.
33
+ * - If `state.nextRunAt` is already at or before `now`, the write is
34
+ * skipped: something more urgent (usually {@link speedUp}) has
35
+ * already signalled an immediate poll and we must not push it
36
+ * back out. The worker picks up the signal on its next iteration.
37
+ * - Otherwise `nextRunAt` is overwritten with `runAt` (even if `runAt`
38
+ * is in the past - that behaves like {@link speedUp}).
39
+ * - Back-off is reset so a subsequent wake-up fires at `minPollMs`.
40
+ */
41
+ setNextRunAt(sourceName: string, runAt: number): void;
26
42
  updateAllSources(options: Partial<SourceOptions>): void;
27
43
  private runWorker;
28
44
  private prolongNextRun;
@@ -1,6 +1,22 @@
1
1
  import { Collection, ObjectId } from 'mongodb';
2
2
  type StopContinuousLock = () => Promise<void>;
3
+ export interface CreateContinuousLockOptions {
4
+ /**
5
+ * Initial value of the lock property written by whoever acquired the lock.
6
+ * When provided, every renewal becomes a compare-and-swap: the update only
7
+ * succeeds if the lock still carries the previously-written value. If another
8
+ * actor has taken over the lock in the meantime, `onLockLost` is invoked and
9
+ * no further renewals are attempted. This prevents a slow renewal from
10
+ * accidentally extending a lock that has already been stolen.
11
+ */
12
+ expectedInitialValue?: unknown;
13
+ /**
14
+ * Invoked once when CAS detects we no longer own the lock. Only fires when
15
+ * `expectedInitialValue` is set. Renewals stop after this callback.
16
+ */
17
+ onLockLost?: () => void;
18
+ }
3
19
  export declare function createContinuousLock<DocumentType extends {
4
20
  _id: string | ObjectId;
5
- }>(collection: Collection<DocumentType>, documentId: DocumentType['_id'], lockProperty: keyof DocumentType, lockTime: number): StopContinuousLock;
21
+ }>(collection: Collection<DocumentType>, documentId: DocumentType['_id'], lockProperty: keyof DocumentType, lockTime: number, options?: CreateContinuousLockOptions): StopContinuousLock;
6
22
  export {};
@@ -1,6 +1,19 @@
1
1
  import { CronExpressionOptions } from 'cron-parser';
2
2
  export interface InitOptions {
3
3
  runCronTasks: boolean;
4
+ /**
5
+ * Maximum number of cron tasks this instance will execute in parallel.
6
+ *
7
+ * The default of `1` preserves the historical behaviour: one task is
8
+ * processed at a time per instance. Raise it when you have many
9
+ * independent cron tasks and want to avoid head-of-line blocking (a
10
+ * long-running task delaying unrelated ones).
11
+ *
12
+ * Tasks with the same id are always serialised via the per-task lock
13
+ * (`lockedTill`), so raising this does not cause a single task to run
14
+ * twice in parallel.
15
+ */
16
+ cronTaskConcurrency: number;
4
17
  cronExpressionParserOptions: CronExpressionOptions;
5
18
  cronTaskCaller: CronTaskCaller;
6
19
  cronTaskFilter: CronTaskFilter;
@@ -64,8 +77,10 @@ export declare function cronTask(taskId: TaskId, interval: Interval, task: TaskF
64
77
  */
65
78
  export declare function getCronTasksList(query?: CronTaskQuery): Promise<CronPagedResult<CronTaskRecord>>;
66
79
  /**
67
- * Triggers a cron task immediately.
68
- * Alias for scheduleCronTaskImmediately but returns the new state or confirmation.
80
+ * @deprecated Alias for {@link scheduleCronTaskImmediately}. Prefer that name for
81
+ * clarity - it describes exactly what happens (the task is scheduled to run on
82
+ * the next polling tick, not necessarily this very millisecond). This alias will
83
+ * be removed in a future major version.
69
84
  */
70
85
  export declare function triggerCronTask(taskId: TaskId): Promise<void>;
71
86
  /**
@@ -5,12 +5,12 @@ import { InitOptions as GetMongoClientInitOptions } from './getMongoClient';
5
5
  import { OnError } from './OnError';
6
6
  import { OnInfo } from './OnInfo';
7
7
  import { InitOptions as ReactiveTasksInitOptions } from './reactiveTasks';
8
- export { CODE_CRON_TASK_FAILED, CODE_CRON_TASK_FINISHED, CODE_CRON_TASK_SCHEDULED, CODE_CRON_TASK_STARTED, CronPagedResult, cronTask, CronTaskQuery, CronTaskRecord, CronTaskStatus, getCronTasksList, Interval, runCronTask, scheduleCronTaskImmediately, startCronTasks, stopCronTasks, TaskFunction, TaskId, triggerCronTask, } from './cronTasks';
8
+ export { CODE_CRON_TASK_FAILED, CODE_CRON_TASK_FINISHED, CODE_CRON_TASK_SCHEDULED, CODE_CRON_TASK_STARTED, CronPagedResult, cronTask, CronTaskQuery, CronTaskRecord, CronTaskStatus, getCronTasksList, getRegisteredCronTaskIds, Interval, runCronTask, scheduleCronTaskImmediately, startCronTasks, stopCronTasks, TaskFunction, TaskId, triggerCronTask, } from './cronTasks';
9
9
  export { getCollection } from './getCollection';
10
10
  export { getMongoClient } from './getMongoClient';
11
11
  export { OnError } from './OnError';
12
12
  export { processInBatches, ProcessInBatchesOptions, ProcessInBatchesResult } from './processInBatches';
13
- export { CODE_REACTIVE_TASK_FAILED, CODE_REACTIVE_TASK_FINISHED, CODE_REACTIVE_TASK_LEADER_LOCK_LOST, CODE_REACTIVE_TASK_PLANNER_RECONCILIATION_FINISHED, CODE_REACTIVE_TASK_PLANNER_RECONCILIATION_STARTED, CODE_REACTIVE_TASK_PLANNER_STARTED, CODE_REACTIVE_TASK_PLANNER_STOPPED, CODE_REACTIVE_TASK_PLANNER_STREAM_ERROR, CODE_REACTIVE_TASK_STARTED, countReactiveTasks, getPrometheusMetrics, getReactiveTasks, reactiveTask, ReactiveTask, ReactiveTaskHandler, retryReactiveTasks, startReactiveTasks, stopReactiveTasks, TaskConditionFailedError, _scheduler, } from './reactiveTasks';
13
+ export { CODE_REACTIVE_TASK_CLEANUP, CODE_REACTIVE_TASK_FAILED, CODE_REACTIVE_TASK_FINISHED, CODE_REACTIVE_TASK_INITIALIZED, CODE_REACTIVE_TASK_LEADER_LOCK_LOST, CODE_REACTIVE_TASK_LOCK_LOST, CODE_REACTIVE_TASK_PLANNER_RECONCILIATION_FINISHED, CODE_REACTIVE_TASK_PLANNER_RECONCILIATION_STARTED, CODE_REACTIVE_TASK_PLANNER_STARTED, CODE_REACTIVE_TASK_PLANNER_STOPPED, CODE_REACTIVE_TASK_PLANNER_STREAM_ERROR, CODE_REACTIVE_TASK_STARTED, countReactiveTasks, getPrometheusMetrics, getReactiveTasks, PagedResult, PaginationOptions, reactiveTask, ReactiveTask, ReactiveTaskHandler, ReactiveTaskQuery, ReactiveTaskRecord, ReactiveTaskStatus, retryReactiveTasks, startReactiveTasks, stopReactiveTasks, TaskConditionFailedError, _scheduler, } from './reactiveTasks';
14
14
  export { OperationalTaskController, serveDashboard } from './task-management';
15
15
  export * from './testing';
16
16
  export { isLockAlreadyAcquiredError, LockAlreadyAcquiredError, withLock, WithLockOptions } from './withLock';
@@ -1,6 +1,6 @@
1
1
  import { GlobalsCollection } from '../globalsCollection';
2
- import { OnInfo } from '../OnInfo';
3
2
  import { OnError } from '../OnError';
3
+ import { OnInfo } from '../OnInfo';
4
4
  export interface LeaderElectorCallbacks {
5
5
  onBecomeLeader: () => Promise<void>;
6
6
  onLoseLeader: () => Promise<void>;
@@ -35,6 +35,20 @@ export declare class LeaderElector {
35
35
  get isLeader(): boolean;
36
36
  start(): Promise<void>;
37
37
  stop(): Promise<void>;
38
+ /**
39
+ * Give up leadership locally. The DB lock is NOT released - the next
40
+ * heartbeat will likely re-acquire it (unless another instance raced
41
+ * in). onLoseLeader is fired asynchronously so callers (e.g. the
42
+ * scheduler wiring this to a flush-failure path) get a clean
43
+ * planner.stop() before the next heartbeat restarts it, rather than
44
+ * starting a new planner on top of a live one.
45
+ *
46
+ * Note: the follow-up onBecomeLeader that fires after a forced loss
47
+ * looks identical to a real leader election and will increment
48
+ * reactive_tasks_leader_elections_total; see the event codes
49
+ * CODE_REACTIVE_TASK_PLANNER_STREAM_ERROR and the flush-failure
50
+ * counter to disambiguate "real" flapping from restart-driven ones.
51
+ */
38
52
  forceLoseLeader(): void;
39
53
  private runLeaderElectionLoop;
40
54
  private tryAcquireLock;
@@ -1,9 +1,7 @@
1
1
  import type { Registry } from 'prom-client';
2
2
  import { GlobalsCollection } from '../globalsCollection';
3
3
  import { OnError } from '../OnError';
4
- import { OnInfo } from '../OnInfo';
5
4
  import { LeaderElector } from './LeaderElector';
6
- import { ReactiveTaskPlanner } from './ReactiveTaskPlanner';
7
5
  import { ReactiveTaskRegistry } from './ReactiveTaskRegistry';
8
6
  import { ReactiveTaskSchedulerOptions } from './ReactiveTaskTypes';
9
7
  /**
@@ -15,7 +13,12 @@ import { ReactiveTaskSchedulerOptions } from './ReactiveTaskTypes';
15
13
  * - **local**: Returns metrics from THIS instance only.
16
14
  * Use when Prometheus scrapes each pod individually (K8s Pod Monitors).
17
15
  *
18
- * Global stats (queue depth, lag) are computed on-the-fly by the **Leader only**.
16
+ * Global stats (queue depth, lag, change-stream lag, reconciliation timestamp)
17
+ * are computed by the Leader. In `cluster` mode the Leader also pushes these
18
+ * values into the registry document on each push interval so a scrape that
19
+ * lands on a Follower still returns a complete metrics view - bounded-stale
20
+ * at `GLOBAL_STATS_STALE_MULTIPLIER * pushIntervalMs` (2x by default,
21
+ * i.e. a single missed push is tolerated).
19
22
  */
20
23
  export declare class MetricsCollector {
21
24
  private enabled;
@@ -24,17 +27,19 @@ export declare class MetricsCollector {
24
27
  private readonly registry;
25
28
  private readonly globalsCollection;
26
29
  private readonly leaderElector;
27
- private readonly onInfo;
28
30
  private readonly onError;
29
31
  private promClientModule;
30
32
  private localPromRegistry?;
31
33
  private globalStatsRegistry?;
32
34
  private metricDuration?;
33
35
  private metricRetries?;
36
+ private metricLeaderElections?;
37
+ private metricLockLost?;
38
+ private metricStreamErrors?;
39
+ private metricFlushFailures?;
34
40
  private pushInterval?;
35
41
  private queueMetricsPromise;
36
- planner?: ReactiveTaskPlanner;
37
- constructor(instanceId: string, registry: ReactiveTaskRegistry, globalsCollection: GlobalsCollection, leaderElector: LeaderElector, options: ReactiveTaskSchedulerOptions['monitoring'], onInfo?: OnInfo, onError?: OnError);
42
+ constructor(instanceId: string, registry: ReactiveTaskRegistry, globalsCollection: GlobalsCollection, leaderElector: LeaderElector, options: ReactiveTaskSchedulerOptions['monitoring'], onError?: OnError);
38
43
  private initPrometheus;
39
44
  private initLocalMetrics;
40
45
  private initGlobalStatsMetrics;
@@ -42,11 +47,17 @@ export declare class MetricsCollector {
42
47
  stop(): void;
43
48
  recordTaskExecution(task: string, status: 'success' | 'failed', durationMs: number): void;
44
49
  recordRetry(task: string): void;
50
+ recordLeaderElection(): void;
51
+ recordLockLost(task: string): void;
52
+ recordStreamError(): void;
53
+ recordFlushFailure(): void;
45
54
  getPrometheusMetrics(): Promise<Registry | null>;
46
55
  /**
47
56
  * Returns aggregated metrics from ALL instances.
48
57
  * Fetches other instances' metrics from DB and merges with fresh local metrics.
49
- * Leader also includes global stats.
58
+ * Global stats: leader computes fresh; followers read the leader's last
59
+ * push from the registry doc (so a scrape that hits a follower still
60
+ * returns queue depth / lag / reconciliation / stream-lag gauges).
50
61
  */
51
62
  private getClusterMetrics;
52
63
  /**
@@ -54,7 +65,7 @@ export declare class MetricsCollector {
54
65
  * Leader also includes global stats.
55
66
  */
56
67
  private getLocalMetrics;
57
- private fetchOtherInstancesMetrics;
68
+ private fetchClusterStateFromDb;
58
69
  private getLocalMetricsAsJson;
59
70
  private getGlobalStatsAsJson;
60
71
  private triggerGlobalStatsCollection;
@@ -6,6 +6,15 @@ import { ReactiveTaskRegistry } from './ReactiveTaskRegistry';
6
6
  export interface PlannerCallbacks {
7
7
  onStreamError: () => void;
8
8
  onTaskPlanned: (tasksCollectionName: string, debounceMs: number) => void;
9
+ /** Fired when a batch flush fails. Records the metric and should trigger a planner restart. */
10
+ onFlushFailure?: () => void;
11
+ /**
12
+ * Fired when the planner needs to restart due to a flush failure (distinct from a
13
+ * real change-stream error). Callers should trigger a leader-election cycle here
14
+ * instead of reacting to `onStreamError`, so flush failures don't pollute the
15
+ * stream-error metric.
16
+ */
17
+ onRequestRestart?: () => void;
9
18
  }
10
19
  /**
11
20
  * Responsible for listening to MongoDB Change Stream events and planning tasks.
@@ -31,6 +40,7 @@ export declare class ReactiveTaskPlanner {
31
40
  private batchFlushTimer;
32
41
  private batchFirstEventTime;
33
42
  private isFlushing;
43
+ private lastFlushFailed;
34
44
  private metaDocId;
35
45
  private lastClusterTime;
36
46
  private ops;
@@ -59,6 +69,7 @@ export declare class ReactiveTaskPlanner {
59
69
  private groupEventsByCollection;
60
70
  private processDeletions;
61
71
  private executeUpsertOperations;
72
+ private throwOnAnyRejection;
62
73
  private handleStreamError;
63
74
  private checkEvolutionStrategies;
64
75
  private checkTriggerEvolution;
@@ -24,11 +24,20 @@ export declare class ReactiveTaskRepository<T extends Document> {
24
24
  findAndLockNextTask(taskDefs: ReactiveTaskInternal<T>[], options: {
25
25
  visibilityTimeoutMs: number;
26
26
  }): Promise<ReactiveTaskRecord<T> | null>;
27
+ /**
28
+ * Finalize a task record (success or failure). Returns `true` when the
29
+ * update matched the record, `false` when it did not - which in
30
+ * practice means another worker has since re-claimed the task (its
31
+ * startedAt no longer matches) and this call was a no-op.
32
+ *
33
+ * Callers that care about the distinction (e.g. to suppress success /
34
+ * failure metrics for a stolen task) should inspect the return value.
35
+ */
27
36
  finalizeTask(taskRecord: ReactiveTaskRecord<T>, strategy: ReactiveTaskRetryStrategy, error?: Error, debounceMs?: number, executionStats?: {
28
37
  durationMs: number;
29
38
  }, executionHistoryLimit?: number, options?: {
30
39
  session?: import('mongodb').ClientSession;
31
- }): Promise<void>;
40
+ }): Promise<boolean>;
32
41
  deferTask(taskRecord: ReactiveTaskRecord<T>, delay: number | Date): Promise<void>;
33
42
  executeBulkWrite(operations: Parameters<Collection<ReactiveTaskRecord<T>>['bulkWrite']>[0], options?: CompatibleBulkWriteOptions): Promise<void>;
34
43
  findTasks(filter: Filter<ReactiveTaskRecord<T>>, options?: {
@@ -103,6 +103,19 @@ export interface RegistryDocument {
103
103
  lastSeen: Date | string;
104
104
  metrics: unknown;
105
105
  }>;
106
+ /**
107
+ * Cluster-wide stats (queue depth, global lag, change-stream lag,
108
+ * last reconciliation). Written only by the current Leader on each
109
+ * push; non-leaders read it so that a follower-served scrape in
110
+ * `cluster` mode can return a complete metrics view. A single field
111
+ * (not per-instance) prevents double-counting across leader
112
+ * transitions - the next leader overwrites on its first push.
113
+ */
114
+ globalStats?: {
115
+ updatedAt: Date | string;
116
+ leaderId: string;
117
+ metrics: unknown;
118
+ };
106
119
  }
107
120
  /**
108
121
  * Error thrown when `getDocument` fails because the document no longer matches the filter
@@ -336,6 +349,7 @@ export interface ReactiveTaskCaller {
336
349
  export declare const CODE_REACTIVE_TASK_STARTED = "reactiveTaskStarted";
337
350
  export declare const CODE_REACTIVE_TASK_FINISHED = "reactiveTaskFinished";
338
351
  export declare const CODE_REACTIVE_TASK_FAILED = "reactiveTaskFailed";
352
+ export declare const CODE_REACTIVE_TASK_LOCK_LOST = "reactiveTaskLockLost";
339
353
  export declare const CODE_REACTIVE_TASK_PLANNER_STARTED = "reactiveTaskPlannerStarted";
340
354
  export declare const CODE_REACTIVE_TASK_PLANNER_STOPPED = "reactiveTaskPlannerStopped";
341
355
  export declare const CODE_REACTIVE_TASK_PLANNER_RECONCILIATION_STARTED = "reactiveTaskPlannerReconciliationStarted";
@@ -345,6 +359,11 @@ export declare const CODE_REACTIVE_TASK_LEADER_LOCK_LOST = "reactiveTaskLeaderLo
345
359
  export declare const CODE_REACTIVE_TASK_INITIALIZED = "reactiveTaskInitialized";
346
360
  export declare const CODE_REACTIVE_TASK_CLEANUP = "reactiveTaskCleanup";
347
361
  export declare const CODE_MANUAL_TRIGGER = "manualTrigger";
362
+ /**
363
+ * @internal
364
+ * Document id used by the planner for its meta document. Exposed for the
365
+ * dashboard and advanced tooling - not part of the public API contract.
366
+ */
348
367
  export declare const REACTIVE_TASK_META_DOC_ID = "_mongodash_planner_meta";
349
368
  /**
350
369
  * Filter for querying tasks.
@@ -6,7 +6,13 @@ import { ReactiveTaskManager } from './ReactiveTaskManager';
6
6
  import { ReactiveTaskPlanner } from './ReactiveTaskPlanner';
7
7
  import { ReactiveTaskRegistry } from './ReactiveTaskRegistry';
8
8
  import { PagedResult, PaginationOptions, ReactiveTask, ReactiveTaskQuery, ReactiveTaskRecord, ReactiveTaskSchedulerOptions } from './ReactiveTaskTypes';
9
- export { CODE_REACTIVE_TASK_CLEANUP, CODE_REACTIVE_TASK_FAILED, CODE_REACTIVE_TASK_FINISHED, CODE_REACTIVE_TASK_INITIALIZED, CODE_REACTIVE_TASK_LEADER_LOCK_LOST, CODE_REACTIVE_TASK_PLANNER_RECONCILIATION_FINISHED, CODE_REACTIVE_TASK_PLANNER_RECONCILIATION_STARTED, CODE_REACTIVE_TASK_PLANNER_STARTED, CODE_REACTIVE_TASK_PLANNER_STOPPED, CODE_REACTIVE_TASK_PLANNER_STREAM_ERROR, CODE_REACTIVE_TASK_STARTED, PagedResult, PaginationOptions, ReactiveTask, ReactiveTaskCaller, ReactiveTaskFilter, ReactiveTaskHandler, ReactiveTaskQuery, ReactiveTaskRecord, ReactiveTaskSchedulerOptions, ReactiveTaskStatus, REACTIVE_TASK_META_DOC_ID, TaskConditionFailedError, } from './ReactiveTaskTypes';
9
+ export { CODE_REACTIVE_TASK_CLEANUP, CODE_REACTIVE_TASK_FAILED, CODE_REACTIVE_TASK_FINISHED, CODE_REACTIVE_TASK_INITIALIZED, CODE_REACTIVE_TASK_LEADER_LOCK_LOST, CODE_REACTIVE_TASK_LOCK_LOST, CODE_REACTIVE_TASK_PLANNER_RECONCILIATION_FINISHED, CODE_REACTIVE_TASK_PLANNER_RECONCILIATION_STARTED, CODE_REACTIVE_TASK_PLANNER_STARTED, CODE_REACTIVE_TASK_PLANNER_STOPPED, CODE_REACTIVE_TASK_PLANNER_STREAM_ERROR, CODE_REACTIVE_TASK_STARTED, PagedResult, PaginationOptions, ReactiveTask, ReactiveTaskCaller, ReactiveTaskFilter, ReactiveTaskHandler, ReactiveTaskQuery, ReactiveTaskRecord, ReactiveTaskSchedulerOptions, ReactiveTaskStatus, REACTIVE_TASK_META_DOC_ID, TaskConditionFailedError, } from './ReactiveTaskTypes';
10
+ /**
11
+ * @internal
12
+ * Exported only for the built-in OperationalTaskController / dashboard bridge
13
+ * and for advanced testing. Not part of the public API contract: fields and
14
+ * methods on the scheduler instance can change between minor versions.
15
+ */
10
16
  export { scheduler as _scheduler };
11
17
  export type InitOptions = {
12
18
  globalsCollection: GlobalsCollection;
@@ -54,7 +60,7 @@ export declare class ReactiveTaskScheduler {
54
60
  debounce?: number;
55
61
  }): void;
56
62
  get forceDebounce(): number | string | undefined;
57
- addTask(taskDef: ReactiveTask<Document>): Promise<void>;
63
+ addTask<T extends Document>(taskDef: ReactiveTask<T>): Promise<void>;
58
64
  /**
59
65
  * Starts the entire system - leader election and workers.
60
66
  */
@@ -1,4 +1,5 @@
1
1
  import { ReactiveTaskScheduler } from '../reactiveTasks';
2
+ import { WhitelistRule } from './resolveWhitelistFilter';
2
3
  export interface AssertNoReactiveTaskErrorsOptions {
3
4
  /**
4
5
  * Check for errors occurring after this time.
@@ -6,11 +7,10 @@ export interface AssertNoReactiveTaskErrorsOptions {
6
7
  */
7
8
  since: Date;
8
9
  /**
9
- * Optional: Check only tasks related to these specific source documents.
10
- * Useful when other tests might be generating noise in the background.
11
- * Supports generic ID types (ObjectId, string, number).
10
+ * Optional: Check only tasks related to specific entities.
11
+ * If provided, errors in collections/tasks not matching the whitelist are ignored.
12
12
  */
13
- sourceDocIds?: unknown[];
13
+ whitelist?: WhitelistRule[];
14
14
  /**
15
15
  * Optional: Whitelist specific errors.
16
16
  * If a string is provided, exact match is required.
@@ -1,3 +1,5 @@
1
1
  export * from './assertNoReactiveTaskErrors';
2
2
  export * from './configureForTesting';
3
+ export * from './resolveWhitelistFilter';
4
+ export * from './waitUntil';
3
5
  export * from './waitUntilReactiveTasksIdle';
@@ -0,0 +1,35 @@
1
+ import { Collection, Document, Filter } from 'mongodb';
2
+ import { ReactiveTaskRecord } from '../reactiveTasks';
3
+ /**
4
+ * A single rule used by the testing utilities to scope checks to a set of
5
+ * source documents.
6
+ */
7
+ export interface WhitelistRule {
8
+ collection: string;
9
+ /**
10
+ * Filter to find relevant source documents. When omitted every document
11
+ * in the collection is considered.
12
+ */
13
+ filter?: Filter<Document>;
14
+ /**
15
+ * Optional: restrict to a specific reactive task name.
16
+ */
17
+ task?: string;
18
+ }
19
+ /**
20
+ * Resolution outcome for a whitelist against one registry entry.
21
+ *
22
+ * - `'skip'`: the whitelist has rules, but none apply to this collection or
23
+ * the source filters matched zero documents. Callers should skip this
24
+ * entry entirely.
25
+ * - `'matchAll'`: at least one rule for this collection wants the full
26
+ * collection. Callers should apply no extra filter.
27
+ * - An object: the caller should AND this filter with its base query.
28
+ */
29
+ export type WhitelistResolution = 'skip' | 'matchAll' | Filter<ReactiveTaskRecord>;
30
+ /**
31
+ * Build the `Filter<ReactiveTaskRecord>` for a single registry entry based on
32
+ * the provided whitelist rules. Extracted from `waitUntilReactiveTasksIdle` /
33
+ * `assertNoReactiveTaskErrors` so the two utilities cannot drift.
34
+ */
35
+ export declare function resolveWhitelistFilter(whitelist: WhitelistRule[], sourceCollection: Pick<Collection<Document>, 'collectionName' | 'find'>): Promise<WhitelistResolution>;