@onlineapps/conn-orch-orchestrator 2.0.3 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -44,6 +44,78 @@ const stats = orchestrator.getStats();
44
44
  console.log(`Processed: ${stats.processed}, Failed: ${stats.failed}`);
45
45
  ```
46
46
 
47
+ ## Cache Invalidation for Mutating Operations (`operationMutatesLookup`)
48
+
49
+ The orchestrator memoizes successful task-step results in the optional `cache` connector under
50
+ keys of the form `workflow:step:<step.service>:<operation>:<hash>` so that idempotent reads
51
+ inside the same workflow can be reused. Mutating operations (writes, deletes, RPC side effects)
52
+ must NOT be served from this cache, and any cached reads against the same target service must
53
+ be purged the moment a write succeeds.
54
+
55
+ To enable that behavior, pass an `operationMutatesLookup` hook to the constructor. The hook is
56
+ typically wired by `@onlineapps/service-wrapper` from each service's `operations.json` map.
57
+
58
+ ### Signature
59
+
60
+ ```js
61
+ /**
62
+ * @param {string} executingServiceName Name of the service running the cookbook.
63
+ * @param {Object} step The cookbook step about to execute.
64
+ * @returns {{ mutates?: boolean, resource_type?: string|null }|null|undefined}
65
+ */
66
+ operationMutatesLookup(executingServiceName, step)
67
+ ```
68
+
69
+ ### Return shape
70
+
71
+ - `{ mutates: true }` &mdash; force a read-cache bypass for this step AND, after the handler
72
+ returns success (`status` 2xx), invoke
73
+ `cache.deleteByPattern('workflow:step:<step.service>:*')` to purge stale reads.
74
+ - `{ mutates: false }` (or `undefined` / `null`) &mdash; treat as a read; the orchestrator may
75
+ reuse `cache.get` and store fresh results via `cache.set`.
76
+ - The `resource_type` field is informational and is not consumed by the orchestrator (yet).
77
+
78
+ ### Error contract
79
+
80
+ If the hook throws, `_stepMutatesFlag` re-throws the same error. The orchestrator does NOT
81
+ silently swallow lookup failures &mdash; per
82
+ [architecture-principles.mdc](/docs/architecture/architecture-principles.md) §4 we fail
83
+ loudly so misconfigured `operations.json` is caught at workflow runtime, not by data drift.
84
+
85
+ ### Fallback behavior without the hook
86
+
87
+ If `operationMutatesLookup` is omitted or is not a function, the orchestrator only treats a
88
+ step as mutating when the cookbook step itself sets `step.mutates === true`. Otherwise every
89
+ task step is eligible for read-cache reuse.
90
+
91
+ ### Example
92
+
93
+ ```javascript
94
+ const WorkflowOrchestrator = require('@onlineapps/conn-orch-orchestrator');
95
+
96
+ const operations = {
97
+ 'get-invoice': { mutates: false, resource_type: 'invoice' },
98
+ 'update-invoice': { mutates: true, resource_type: 'invoice' }
99
+ };
100
+
101
+ const orchestrator = new WorkflowOrchestrator({
102
+ mqClient,
103
+ registryClient,
104
+ invoker,
105
+ cookbook,
106
+ cache,
107
+ logger,
108
+ defaultTimeout: 30000,
109
+ operationMutatesLookup: (executingServiceName, step) => {
110
+ const spec = operations[step.operation];
111
+ return {
112
+ mutates: !!(spec && spec.mutates === true),
113
+ resource_type: spec ? spec.resource_type : null
114
+ };
115
+ }
116
+ });
117
+ ```
118
+
47
119
  ## Configuration
48
120
 
49
121
  | Variable | Description | Default |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onlineapps/conn-orch-orchestrator",
3
- "version": "2.0.3",
3
+ "version": "2.1.0",
4
4
  "description": "Workflow orchestration connector for OA Drive - handles message routing and workflow execution",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -24,6 +24,11 @@ class WorkflowOrchestrator {
24
24
  * @param {Object} [config.errorHandler] - Error handler connector
25
25
  * @param {Object} [config.logger] - Logger instance
26
26
  * @param {number} [config.defaultTimeout=30000] - Default timeout for operations
27
+ * @param {(executingServiceName: string, step: Object) =>
28
+ * {mutates?: boolean}|null|undefined} [config.operationMutatesLookup]
29
+ * Optional hook (usually service-wrapper bound to operations.json map). Returning
30
+ * `{ mutates: true }` forces task executions to bypass read-through cache reuse and
31
+ * deletes `workflow:step:<step.service>:*` after successful handler completion.
27
32
  */
28
33
  constructor(config) {
29
34
  if (!config.mqClient) throw new Error('mqClient is required');
@@ -49,6 +54,10 @@ class WorkflowOrchestrator {
49
54
  throw new Error('[WorkflowOrchestrator] defaultTimeout is required — Expected number (ms)');
50
55
  }
51
56
  this.defaultTimeout = config.defaultTimeout;
57
+ this._operationMutatesLookup =
58
+ typeof config.operationMutatesLookup === 'function'
59
+ ? config.operationMutatesLookup
60
+ : null;
52
61
 
53
62
  // INTENTIONAL FALLBACK: operational defaults — see docs/standards/FALLBACKS_INVENTORY.md §5.2
54
63
  this.retryConfig = {
@@ -277,8 +286,13 @@ class WorkflowOrchestrator {
277
286
  delivery: cookbookDef.delivery
278
287
  };
279
288
 
289
+ const stepMutates = this._stepMutatesFlag(step, serviceName);
290
+
280
291
  let result;
281
- if (this.cache && step.type === 'task') {
292
+ const useReadCache =
293
+ Boolean(this.cache) && step.type === 'task' && !stepMutates;
294
+
295
+ if (useReadCache) {
282
296
  const cacheKey = this._getCacheKey(step, stepContext);
283
297
  result = await this.cache.get(cacheKey);
284
298
  if (!result) {
@@ -661,6 +675,12 @@ class WorkflowOrchestrator {
661
675
  });
662
676
 
663
677
  if (envelope && envelope.status >= 200 && envelope.status < 300) {
678
+ const mutated = this._stepMutatesFlag(step, serviceName);
679
+ const targetService = step.service || serviceName;
680
+ if (mutated && this.cache && typeof this.cache.deleteByPattern === 'function') {
681
+ await this._invalidateWorkflowStepCaches(targetService);
682
+ }
683
+
664
684
  this.logger.info(`Task step executed`, {
665
685
  step: stepId,
666
686
  service: step.service,
@@ -1303,6 +1323,61 @@ class WorkflowOrchestrator {
1303
1323
  return `workflow:step:${step.service}:${step.operation || stepId}:${hash}`;
1304
1324
  }
1305
1325
 
1326
+ /**
1327
+ * Whether the task writes domain state and MUST bust cached reads for sibling operations.
1328
+ * @private
1329
+ * @param {Object} step
1330
+ * @param {string} executingServiceName consumer service running the cookbook step
1331
+ * @returns {boolean}
1332
+ */
1333
+ _stepMutatesFlag(step, executingServiceName) {
1334
+ if (!step || step.type !== 'task') return false;
1335
+ if (step.mutates === true) return true;
1336
+ if (this._operationMutatesLookup) {
1337
+ try {
1338
+ const meta = this._operationMutatesLookup(executingServiceName, step);
1339
+ if (meta && meta.mutates === true) return true;
1340
+ } catch (e) {
1341
+ this.logger.warn('[WorkflowOrchestrator] operationMutatesLookup failed', {
1342
+ error: e.message,
1343
+ executingServiceName,
1344
+ step_id: step.step_id
1345
+ });
1346
+ throw e;
1347
+ }
1348
+ }
1349
+ return false;
1350
+ }
1351
+
1352
+ /**
1353
+ * Remove cached orchestrator outputs for non-mutating task steps targeting a service queue.
1354
+ * @private
1355
+ * @param {string} serviceName cookbook step.service (routing target name)
1356
+ * @returns {Promise<void>}
1357
+ */
1358
+ async _invalidateWorkflowStepCaches(serviceName) {
1359
+ if (
1360
+ !serviceName ||
1361
+ !this.cache ||
1362
+ typeof this.cache.deleteByPattern !== 'function'
1363
+ ) {
1364
+ return;
1365
+ }
1366
+
1367
+ const pattern = `workflow:step:${serviceName}:*`;
1368
+
1369
+ await this.cache.deleteByPattern(pattern);
1370
+
1371
+ if (typeof this.logger?.info === 'function') {
1372
+ this.logger.info(
1373
+ '[WorkflowOrchestrator] Invalidated Redis workflow-step cache after mutate',
1374
+ {
1375
+ service: serviceName,
1376
+ pattern
1377
+ }
1378
+ );
1379
+ }
1380
+ }
1306
1381
  }
1307
1382
 
1308
1383
  module.exports = WorkflowOrchestrator;