@blamejs/core 0.9.23 → 0.9.28
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/CHANGELOG.md +5 -0
- package/index.js +18 -1
- package/lib/agent-audit.js +45 -0
- package/lib/agent-event-bus.js +336 -0
- package/lib/agent-idempotency.js +2 -8
- package/lib/agent-orchestrator.js +2 -8
- package/lib/agent-posture-chain.js +208 -0
- package/lib/agent-saga.js +191 -0
- package/lib/agent-stream.js +237 -0
- package/lib/agent-tenant.js +308 -0
- package/lib/guard-event-bus-payload.js +217 -0
- package/lib/guard-event-bus-topic.js +150 -0
- package/lib/guard-posture-chain.js +201 -0
- package/lib/guard-saga-config.js +157 -0
- package/lib/guard-stream-args.js +166 -0
- package/lib/guard-tenant-id.js +138 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module b.agent.postureChain
|
|
4
|
+
* @nav Agent
|
|
5
|
+
* @title Agent Posture Chain
|
|
6
|
+
* @order 80
|
|
7
|
+
*
|
|
8
|
+
* @intro
|
|
9
|
+
* Set-based compliance posture propagated across every agent
|
|
10
|
+
* boundary (sub-agent delegation, queue envelopes, event-bus
|
|
11
|
+
* payloads, saga steps). The hard rule: **target's posture set MUST
|
|
12
|
+
* be a SUPERSET of source's posture set.** A "downgrade" (target
|
|
13
|
+
* missing a regime the source requires) is refused at the boundary.
|
|
14
|
+
*
|
|
15
|
+
* Compliance regimes (HIPAA / PCI-DSS / GDPR / SOC2) protect
|
|
16
|
+
* DIFFERENT regulated-data classes — they're orthogonal, not a
|
|
17
|
+
* linear lattice. A clinic that processes payment cards operates
|
|
18
|
+
* under BOTH HIPAA + PCI; an EU clinic adds GDPR; an aggregator
|
|
19
|
+
* may add SOC2. Set semantics match how real-world regulations
|
|
20
|
+
* actually overlap.
|
|
21
|
+
*
|
|
22
|
+
* ```js
|
|
23
|
+
* var chain = b.agent.postureChain.create({});
|
|
24
|
+
*
|
|
25
|
+
* var sourceSet = ["hipaa", "pci-dss"];
|
|
26
|
+
* var targetSet = ["pci-dss"]; // missing hipaa
|
|
27
|
+
*
|
|
28
|
+
* chain.isSubset(targetSet, sourceSet); // false — target lacks hipaa
|
|
29
|
+
* chain.canDelegate(sourceSet, targetSet, "mail.fetch");
|
|
30
|
+
* // → false; agent.posture-chain.canDelegate-denied audit emit
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* ## Per-module declaration
|
|
34
|
+
*
|
|
35
|
+
* Each module declares its applicable regimes via a static
|
|
36
|
+
* `POSTURES` export OR an `@compliance` JSDoc tag. The agent
|
|
37
|
+
* primitive's posture SET = union of all composed modules' declared
|
|
38
|
+
* regimes (operator can narrow at composition time).
|
|
39
|
+
*
|
|
40
|
+
* ## Hop trail
|
|
41
|
+
*
|
|
42
|
+
* Every cross-boundary envelope carries `{ postureSet, chainTrail,
|
|
43
|
+
* enteredAt, hopCount }`. Hop count caps at default 16 — defends
|
|
44
|
+
* infinite recursion across agent delegation. `appendHop` extends
|
|
45
|
+
* the trail when an envelope crosses a new boundary.
|
|
46
|
+
*
|
|
47
|
+
* @card
|
|
48
|
+
* Set-based compliance posture propagated across every boundary.
|
|
49
|
+
* target.set ⊇ source.set required; downgrade refused. Hop-trail
|
|
50
|
+
* tracking for audit + debugging.
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
var lazyRequire = require("./lazy-require");
|
|
54
|
+
var { defineClass } = require("./framework-error");
|
|
55
|
+
var guardPostureChain = require("./guard-posture-chain");
|
|
56
|
+
var agentAudit = require("./agent-audit");
|
|
57
|
+
|
|
58
|
+
var audit = lazyRequire(function () { return require("./audit"); });
|
|
59
|
+
|
|
60
|
+
var AgentPostureChainError = defineClass("AgentPostureChainError", { alwaysPermanent: true });
|
|
61
|
+
|
|
62
|
+
var BUILTIN_REGIMES = Object.freeze(["hipaa", "pci-dss", "gdpr", "soc2"]);
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @primitive b.agent.postureChain.create
|
|
66
|
+
* @signature b.agent.postureChain.create(opts)
|
|
67
|
+
* @since 0.9.28
|
|
68
|
+
* @status stable
|
|
69
|
+
* @related b.agent.tenant.create, b.agent.eventBus.create
|
|
70
|
+
*
|
|
71
|
+
* Create the posture-chain facade. Returns an instance with
|
|
72
|
+
* `isSubset` / `union` / `canDelegate` / `declareRegime` / `validate`
|
|
73
|
+
* / `appendHop`.
|
|
74
|
+
*
|
|
75
|
+
* @opts
|
|
76
|
+
* audit: b.audit namespace, // optional
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* var chain = b.agent.postureChain.create({});
|
|
80
|
+
* chain.isSubset(["pci-dss"], ["hipaa", "pci-dss"]); // → false
|
|
81
|
+
*/
|
|
82
|
+
function create(opts) {
|
|
83
|
+
opts = opts || {};
|
|
84
|
+
var auditImpl = opts.audit || audit();
|
|
85
|
+
var declaredRegimes = Object.create(null);
|
|
86
|
+
for (var i = 0; i < BUILTIN_REGIMES.length; i += 1) declaredRegimes[BUILTIN_REGIMES[i]] = true;
|
|
87
|
+
return {
|
|
88
|
+
declareRegime: function (name) { return _declareRegime(declaredRegimes, name); },
|
|
89
|
+
isSubset: function (targetSet, sourceSet) { return _isSubset(targetSet, sourceSet); },
|
|
90
|
+
union: function () { return _union.apply(null, arguments); },
|
|
91
|
+
canDelegate: function (sourceSet, targetSet, method) { return _canDelegate(sourceSet, targetSet, method, auditImpl); },
|
|
92
|
+
appendHop: function (envelope, hopName) { return _appendHop(envelope, hopName); },
|
|
93
|
+
validate: function (envelope, agentPostureSet) { return _validate(envelope, agentPostureSet, auditImpl); },
|
|
94
|
+
REGIMES: Object.freeze(Object.keys(declaredRegimes)),
|
|
95
|
+
AgentPostureChainError: AgentPostureChainError,
|
|
96
|
+
_declaredRegimes: declaredRegimes,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function _declareRegime(declaredRegimes, name) {
|
|
101
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
102
|
+
throw new AgentPostureChainError("agent-posture-chain/bad-regime",
|
|
103
|
+
"declareRegime: name must be a non-empty string");
|
|
104
|
+
}
|
|
105
|
+
if (declaredRegimes[name]) {
|
|
106
|
+
throw new AgentPostureChainError("agent-posture-chain/duplicate-regime",
|
|
107
|
+
"declareRegime: '" + name + "' already declared");
|
|
108
|
+
}
|
|
109
|
+
declaredRegimes[name] = true;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function _isSubset(targetSet, sourceSet) {
|
|
113
|
+
if (!Array.isArray(targetSet) || !Array.isArray(sourceSet)) return false;
|
|
114
|
+
if (sourceSet.length === 0) return true; // empty source ⊆ any target
|
|
115
|
+
var targetIdx = Object.create(null);
|
|
116
|
+
for (var i = 0; i < targetSet.length; i += 1) targetIdx[targetSet[i]] = true;
|
|
117
|
+
for (var j = 0; j < sourceSet.length; j += 1) {
|
|
118
|
+
if (!targetIdx[sourceSet[j]]) return false;
|
|
119
|
+
}
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function _union() {
|
|
124
|
+
var seen = Object.create(null);
|
|
125
|
+
var out = [];
|
|
126
|
+
for (var a = 0; a < arguments.length; a += 1) {
|
|
127
|
+
var set = arguments[a];
|
|
128
|
+
if (!Array.isArray(set)) continue;
|
|
129
|
+
for (var i = 0; i < set.length; i += 1) {
|
|
130
|
+
if (!seen[set[i]]) {
|
|
131
|
+
seen[set[i]] = true;
|
|
132
|
+
out.push(set[i]);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return out;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function _canDelegate(sourceSet, targetSet, method, auditImpl) {
|
|
140
|
+
if (_isSubset(targetSet, sourceSet)) return true;
|
|
141
|
+
agentAudit.safeAudit(auditImpl, "agent.posture_chain.delegate_denied", null, {
|
|
142
|
+
method: method, sourceSet: sourceSet, targetSet: targetSet,
|
|
143
|
+
missing: _missing(targetSet, sourceSet),
|
|
144
|
+
});
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function _missing(targetSet, sourceSet) {
|
|
149
|
+
var idx = Object.create(null);
|
|
150
|
+
if (Array.isArray(targetSet)) for (var i = 0; i < targetSet.length; i += 1) idx[targetSet[i]] = true;
|
|
151
|
+
var out = [];
|
|
152
|
+
if (Array.isArray(sourceSet)) for (var j = 0; j < sourceSet.length; j += 1) {
|
|
153
|
+
if (!idx[sourceSet[j]]) out.push(sourceSet[j]);
|
|
154
|
+
}
|
|
155
|
+
return out;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function _appendHop(envelope, hopName) {
|
|
159
|
+
if (!envelope || typeof envelope !== "object") {
|
|
160
|
+
throw new AgentPostureChainError("agent-posture-chain/bad-envelope",
|
|
161
|
+
"appendHop: envelope required");
|
|
162
|
+
}
|
|
163
|
+
if (typeof hopName !== "string" || hopName.length === 0) {
|
|
164
|
+
throw new AgentPostureChainError("agent-posture-chain/bad-hop-name",
|
|
165
|
+
"appendHop: hopName must be a non-empty string");
|
|
166
|
+
}
|
|
167
|
+
var trail = Array.isArray(envelope.chainTrail) ? envelope.chainTrail.slice() : [];
|
|
168
|
+
trail.push(hopName);
|
|
169
|
+
var enteredAt = Array.isArray(envelope.enteredAt) ? envelope.enteredAt.slice() : [];
|
|
170
|
+
enteredAt.push(Date.now());
|
|
171
|
+
var newEnvelope = Object.assign({}, envelope, {
|
|
172
|
+
chainTrail: trail,
|
|
173
|
+
enteredAt: enteredAt,
|
|
174
|
+
hopCount: trail.length,
|
|
175
|
+
});
|
|
176
|
+
guardPostureChain.validate(newEnvelope);
|
|
177
|
+
return newEnvelope;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function _validate(envelope, agentPostureSet, auditImpl) {
|
|
181
|
+
guardPostureChain.validate(envelope);
|
|
182
|
+
// The source (envelope) carries a postureSet; the target (the agent
|
|
183
|
+
// we're entering) declares its own posture set. Target must be a
|
|
184
|
+
// superset of source — i.e., the agent covers every regime the
|
|
185
|
+
// calling context requires.
|
|
186
|
+
if (Array.isArray(agentPostureSet)) {
|
|
187
|
+
if (!_isSubset(agentPostureSet, envelope.postureSet)) {
|
|
188
|
+
var missing = _missing(agentPostureSet, envelope.postureSet);
|
|
189
|
+
agentAudit.safeAudit(auditImpl, "agent.posture_chain.downgrade_refused", null, {
|
|
190
|
+
sourceSet: envelope.postureSet, targetSet: agentPostureSet, missing: missing,
|
|
191
|
+
chainTrail: envelope.chainTrail,
|
|
192
|
+
});
|
|
193
|
+
throw new AgentPostureChainError("agent-posture-chain/downgrade-refused",
|
|
194
|
+
"validate: agent posture-set " + JSON.stringify(agentPostureSet) +
|
|
195
|
+
" missing regimes required by envelope: " + JSON.stringify(missing));
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return envelope;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
module.exports = {
|
|
202
|
+
create: create,
|
|
203
|
+
BUILTIN_REGIMES: BUILTIN_REGIMES,
|
|
204
|
+
AgentPostureChainError: AgentPostureChainError,
|
|
205
|
+
guards: {
|
|
206
|
+
chain: guardPostureChain,
|
|
207
|
+
},
|
|
208
|
+
};
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module b.agent.saga
|
|
4
|
+
* @nav Agent
|
|
5
|
+
* @title Agent Saga
|
|
6
|
+
* @order 75
|
|
7
|
+
*
|
|
8
|
+
* @intro
|
|
9
|
+
* Multi-step coordination with compensation cascade. When a saga's
|
|
10
|
+
* step fails mid-way, the framework fires every previously-completed
|
|
11
|
+
* step's `compensate` in reverse order so the operator-side state
|
|
12
|
+
* doesn't end up half-written.
|
|
13
|
+
*
|
|
14
|
+
* Substrate for v0.9.34 submission (DKIM-sign → ARC-sign → outbox-
|
|
15
|
+
* enqueue → SMTP-deliver → store-move-to-Sent), regulated export,
|
|
16
|
+
* journal compaction, every future multi-step write.
|
|
17
|
+
*
|
|
18
|
+
* ```js
|
|
19
|
+
* var sendSaga = b.agent.saga.create({
|
|
20
|
+
* name: "mail.send",
|
|
21
|
+
* audit: b.audit,
|
|
22
|
+
* steps: [
|
|
23
|
+
* {
|
|
24
|
+
* name: "dkim-sign",
|
|
25
|
+
* run: async function (ctx, state) { state.signed = sign(state.message); },
|
|
26
|
+
* compensate: async function (ctx, state) { /* sign is pure, nothing to undo *\/ },
|
|
27
|
+
* },
|
|
28
|
+
* {
|
|
29
|
+
* name: "store-draft",
|
|
30
|
+
* run: async function (ctx, state) { state.draftId = store.append("Drafts", state.signed); },
|
|
31
|
+
* compensate: async function (ctx, state) { if (state.draftId) store.delete(state.draftId); },
|
|
32
|
+
* },
|
|
33
|
+
* {
|
|
34
|
+
* name: "smtp-deliver",
|
|
35
|
+
* run: async function (ctx, state) { await smtp.deliver(state.signed); },
|
|
36
|
+
* compensate: async function (ctx, state) { /* idempotent: SMTP delivery doesn't have a recall *\/ },
|
|
37
|
+
* },
|
|
38
|
+
* ],
|
|
39
|
+
* });
|
|
40
|
+
*
|
|
41
|
+
* var result = await sendSaga.run({ store, smtp }, { message: bytes });
|
|
42
|
+
* ```
|
|
43
|
+
*
|
|
44
|
+
* ## Compensation order
|
|
45
|
+
*
|
|
46
|
+
* If step `i` throws, the framework calls `step[i-1].compensate`,
|
|
47
|
+
* `step[i-2].compensate`, ..., `step[0].compensate` in reverse
|
|
48
|
+
* order. Each compensate receives the SAME `state` object that
|
|
49
|
+
* the corresponding `run` mutated — operator inspects what got
|
|
50
|
+
* written and undoes it.
|
|
51
|
+
*
|
|
52
|
+
* Compensations that throw emit `agent.saga.compensation_failed`
|
|
53
|
+
* audit at CRITICAL severity and halt further compensations
|
|
54
|
+
* (operator alert; manual intervention needed). The saga returns
|
|
55
|
+
* `{ status: "failed", failedStep, lastCompensationError }`.
|
|
56
|
+
*
|
|
57
|
+
* ## No saga-level retry
|
|
58
|
+
*
|
|
59
|
+
* Per the substrate playbook decision (operator-confirmed
|
|
60
|
+
* 2026-05-14): saga's value-add is compensation, not retry. If a
|
|
61
|
+
* step needs retry-with-backoff, the operator wraps `step.run`
|
|
62
|
+
* with `b.retry` inside the step body. With v0.9.22 idempotency
|
|
63
|
+
* available, internal retry inside step.run is side-effect-safe.
|
|
64
|
+
*
|
|
65
|
+
* @card
|
|
66
|
+
* Multi-step coordination with compensation cascade. Reverse-order
|
|
67
|
+
* compensations on step failure. No saga-level retry — step.run
|
|
68
|
+
* owns its own retry semantics via b.retry + v0.9.22 idempotency.
|
|
69
|
+
*/
|
|
70
|
+
|
|
71
|
+
var lazyRequire = require("./lazy-require");
|
|
72
|
+
var { defineClass } = require("./framework-error");
|
|
73
|
+
var guardSagaConfig = require("./guard-saga-config");
|
|
74
|
+
var bCrypto = require("./crypto");
|
|
75
|
+
var agentAudit = require("./agent-audit");
|
|
76
|
+
|
|
77
|
+
var audit = lazyRequire(function () { return require("./audit"); });
|
|
78
|
+
|
|
79
|
+
var AgentSagaError = defineClass("AgentSagaError", { alwaysPermanent: true });
|
|
80
|
+
|
|
81
|
+
var SAGA_ID_RAND_BYTES = 8; // allow:raw-byte-literal — saga-id random-suffix byte length
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* @primitive b.agent.saga.create
|
|
85
|
+
* @signature b.agent.saga.create(config)
|
|
86
|
+
* @since 0.9.27
|
|
87
|
+
* @status stable
|
|
88
|
+
* @related b.agent.idempotency.create, b.outbox.enqueue
|
|
89
|
+
*
|
|
90
|
+
* Create a saga definition. Returns an instance with `run(ctx,
|
|
91
|
+
* initialState, opts) → Promise<finalState>`.
|
|
92
|
+
*
|
|
93
|
+
* @opts
|
|
94
|
+
* name: string, // required (audit label)
|
|
95
|
+
* steps: Array<{ name, run, compensate? }>, // required, non-empty
|
|
96
|
+
* audit: b.audit namespace, // optional
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* var saga = b.agent.saga.create({
|
|
100
|
+
* name: "my.workflow",
|
|
101
|
+
* steps: [{ name: "step1", run: async (ctx, s) => { s.x = 1; } }],
|
|
102
|
+
* });
|
|
103
|
+
* var final = await saga.run({}, {});
|
|
104
|
+
*/
|
|
105
|
+
function create(config) {
|
|
106
|
+
guardSagaConfig.validate(config);
|
|
107
|
+
var auditImpl = config.audit || audit();
|
|
108
|
+
return {
|
|
109
|
+
run: function (ctx, initialState, opts) { return _run(config, auditImpl, ctx, initialState, opts || {}); },
|
|
110
|
+
name: config.name,
|
|
111
|
+
stepCount: config.steps.length,
|
|
112
|
+
AgentSagaError: AgentSagaError,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function _run(config, auditImpl, ctx, initialState, opts) {
|
|
117
|
+
var sagaId = opts.sagaId || "saga-" + bCrypto.generateToken(SAGA_ID_RAND_BYTES);
|
|
118
|
+
var state = Object.assign({}, initialState || {});
|
|
119
|
+
var completedSteps = [];
|
|
120
|
+
|
|
121
|
+
agentAudit.safeAudit(auditImpl, "agent.saga.started", opts.actor, {
|
|
122
|
+
sagaId: sagaId, name: config.name, stepCount: config.steps.length,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
for (var i = 0; i < config.steps.length; i += 1) {
|
|
126
|
+
var step = config.steps[i];
|
|
127
|
+
try {
|
|
128
|
+
agentAudit.safeAudit(auditImpl, "agent.saga.step_started", opts.actor, {
|
|
129
|
+
sagaId: sagaId, name: config.name, stepName: step.name, stepIndex: i,
|
|
130
|
+
});
|
|
131
|
+
await step.run(ctx, state);
|
|
132
|
+
completedSteps.push(step);
|
|
133
|
+
agentAudit.safeAudit(auditImpl, "agent.saga.step_completed", opts.actor, {
|
|
134
|
+
sagaId: sagaId, name: config.name, stepName: step.name, stepIndex: i,
|
|
135
|
+
});
|
|
136
|
+
} catch (stepErr) {
|
|
137
|
+
// Step failed — compensate in reverse over already-completed steps.
|
|
138
|
+
agentAudit.safeAudit(auditImpl, "agent.saga.step_failed", opts.actor, {
|
|
139
|
+
sagaId: sagaId, name: config.name, stepName: step.name, stepIndex: i,
|
|
140
|
+
message: (stepErr && stepErr.message) || String(stepErr),
|
|
141
|
+
});
|
|
142
|
+
var compensationError = null;
|
|
143
|
+
for (var c = completedSteps.length - 1; c >= 0; c -= 1) {
|
|
144
|
+
var compStep = completedSteps[c];
|
|
145
|
+
if (typeof compStep.compensate !== "function") continue;
|
|
146
|
+
try {
|
|
147
|
+
agentAudit.safeAudit(auditImpl, "agent.saga.compensation_started", opts.actor, {
|
|
148
|
+
sagaId: sagaId, name: config.name, stepName: compStep.name,
|
|
149
|
+
});
|
|
150
|
+
await compStep.compensate(ctx, state);
|
|
151
|
+
agentAudit.safeAudit(auditImpl, "agent.saga.compensation_completed", opts.actor, {
|
|
152
|
+
sagaId: sagaId, name: config.name, stepName: compStep.name,
|
|
153
|
+
});
|
|
154
|
+
} catch (compErr) {
|
|
155
|
+
// CRITICAL: compensation failed — operator intervention needed.
|
|
156
|
+
// Halt further compensations; record what failed so audit
|
|
157
|
+
// pipeline can alert.
|
|
158
|
+
compensationError = compErr;
|
|
159
|
+
agentAudit.safeAudit(auditImpl, "agent.saga.compensation_failed", opts.actor, {
|
|
160
|
+
sagaId: sagaId, name: config.name, stepName: compStep.name,
|
|
161
|
+
message: (compErr && compErr.message) || String(compErr),
|
|
162
|
+
});
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
agentAudit.safeAudit(auditImpl, "agent.saga.failed", opts.actor, {
|
|
167
|
+
sagaId: sagaId, name: config.name, failedStep: step.name,
|
|
168
|
+
compensationFailed: compensationError !== null,
|
|
169
|
+
});
|
|
170
|
+
throw new AgentSagaError("agent-saga/failed",
|
|
171
|
+
"saga '" + config.name + "' failed at step '" + step.name + "': " +
|
|
172
|
+
((stepErr && stepErr.message) || String(stepErr)) +
|
|
173
|
+
(compensationError ? " — and compensation '" + completedSteps[completedSteps.length - 1].name +
|
|
174
|
+
"' subsequently failed: " +
|
|
175
|
+
(compensationError.message || String(compensationError))
|
|
176
|
+
: ""));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
agentAudit.safeAudit(auditImpl, "agent.saga.completed", opts.actor, {
|
|
180
|
+
sagaId: sagaId, name: config.name, stepCount: config.steps.length,
|
|
181
|
+
});
|
|
182
|
+
return { status: "completed", sagaId: sagaId, state: state };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
module.exports = {
|
|
186
|
+
create: create,
|
|
187
|
+
AgentSagaError: AgentSagaError,
|
|
188
|
+
guards: {
|
|
189
|
+
config: guardSagaConfig,
|
|
190
|
+
},
|
|
191
|
+
};
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module b.agent.stream
|
|
4
|
+
* @nav Agent
|
|
5
|
+
* @title Agent Stream
|
|
6
|
+
* @order 60
|
|
7
|
+
*
|
|
8
|
+
* @intro
|
|
9
|
+
* Async-iterable variants for agent methods that yield N rows.
|
|
10
|
+
* Operator wraps a cursor-shaped fetcher with `b.agent.stream.create`;
|
|
11
|
+
* the resulting object is `AsyncIterable<row>` — built-in
|
|
12
|
+
* backpressure (each `yield` blocks until the consumer pulls),
|
|
13
|
+
* automatic cursor close via `try`/`finally` on any exit path
|
|
14
|
+
* (consumer break, throw, network drop), and drain-marker emit on
|
|
15
|
+
* orchestrator drain so clients can resume from `lastSeenCursor`
|
|
16
|
+
* against the new agent post-drain.
|
|
17
|
+
*
|
|
18
|
+
* ```js
|
|
19
|
+
* var stream = b.agent.stream.create({
|
|
20
|
+
* orchestrator: orch, // optional — for drain reg
|
|
21
|
+
* actor: { id: "u1" },
|
|
22
|
+
* kind: "search",
|
|
23
|
+
* batchSize: 256,
|
|
24
|
+
* openCursor: function (cursorOpts) {
|
|
25
|
+
* return store.openSearchCursor(cursorOpts); // operator
|
|
26
|
+
* },
|
|
27
|
+
* cursorOpts: { folder: "INBOX", sinceModseq: 0 },
|
|
28
|
+
* });
|
|
29
|
+
*
|
|
30
|
+
* for await (var row of stream) {
|
|
31
|
+
* // row delivered as soon as the cursor yields it.
|
|
32
|
+
* // Pulling slowly applies backpressure to the store.
|
|
33
|
+
* if (someCondition) break; // cursor.close() fires automatically
|
|
34
|
+
* }
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* ## Drain-marker semantic
|
|
38
|
+
*
|
|
39
|
+
* When orchestrator drain fires, in-flight streams emit ONE final
|
|
40
|
+
* `{ _drainMarker: true, lastSeenCursor: <opaque>, reason: "drain" }`
|
|
41
|
+
* row and exit cleanly. Clients reconnecting via JMAP-WebSocket /
|
|
42
|
+
* IMAP NOTIFY pass `lastSeenCursor` back to resume.
|
|
43
|
+
*
|
|
44
|
+
* ## Cursor contract
|
|
45
|
+
*
|
|
46
|
+
* Operator-supplied cursor:
|
|
47
|
+
* `cursor.fetchBatch(batchSize) → { rows, nextCursor, done }`
|
|
48
|
+
* `cursor.close() → void | Promise<void>`
|
|
49
|
+
*
|
|
50
|
+
* The framework's `b.mailStore` will gain `openSearchCursor` /
|
|
51
|
+
* `openFolderCursor` / `openExportCursor` etc. at later mail-stack
|
|
52
|
+
* slices that compose this primitive.
|
|
53
|
+
*
|
|
54
|
+
* @card
|
|
55
|
+
* Async-iterable variants for agent methods that yield N rows.
|
|
56
|
+
* Cursor-backed backpressure; auto-close on exit; drain-marker
|
|
57
|
+
* emit so clients resume cleanly post-deploy.
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
var lazyRequire = require("./lazy-require");
|
|
61
|
+
var { defineClass } = require("./framework-error");
|
|
62
|
+
var guardStreamArgs = require("./guard-stream-args");
|
|
63
|
+
var agentAudit = require("./agent-audit");
|
|
64
|
+
|
|
65
|
+
var audit = lazyRequire(function () { return require("./audit"); });
|
|
66
|
+
|
|
67
|
+
var AgentStreamError = defineClass("AgentStreamError", { alwaysPermanent: true });
|
|
68
|
+
|
|
69
|
+
var DEFAULT_BATCH_SIZE = 256; // allow:raw-byte-literal — cursor batch row count, not bytes
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @primitive b.agent.stream.create
|
|
73
|
+
* @signature b.agent.stream.create(opts)
|
|
74
|
+
* @since 0.9.24
|
|
75
|
+
* @status stable
|
|
76
|
+
* @related b.agent.orchestrator.create
|
|
77
|
+
*
|
|
78
|
+
* Create an async-iterable backed by an operator-supplied cursor.
|
|
79
|
+
* Returns an object that implements `[Symbol.asyncIterator]` — usable
|
|
80
|
+
* with `for await (var row of stream)`. Cursor close + audit emit +
|
|
81
|
+
* orchestrator stream-registry hook are owned by the framework;
|
|
82
|
+
* operator only supplies the `openCursor` factory + `cursorOpts`.
|
|
83
|
+
*
|
|
84
|
+
* @opts
|
|
85
|
+
* openCursor: function(cursorOpts) → cursor, // required
|
|
86
|
+
* cursorOpts: object, // operator-passed
|
|
87
|
+
* batchSize: integer, // default 256
|
|
88
|
+
* orchestrator: b.agent.orchestrator, // optional — for drain reg
|
|
89
|
+
* actor: { id, ... }, // optional — audit attribution
|
|
90
|
+
* kind: string, // "search" / "export" / ...
|
|
91
|
+
* audit: b.audit, // optional
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* var stream = b.agent.stream.create({
|
|
95
|
+
* openCursor: function (o) { return store.openSearchCursor(o); },
|
|
96
|
+
* cursorOpts: { folder: "INBOX" },
|
|
97
|
+
* });
|
|
98
|
+
* for await (var row of stream) { process(row); }
|
|
99
|
+
*/
|
|
100
|
+
function create(opts) {
|
|
101
|
+
if (!opts || typeof opts !== "object") {
|
|
102
|
+
throw new AgentStreamError("agent-stream/bad-opts", "create: opts required");
|
|
103
|
+
}
|
|
104
|
+
if (typeof opts.openCursor !== "function") {
|
|
105
|
+
throw new AgentStreamError("agent-stream/bad-open-cursor",
|
|
106
|
+
"create: opts.openCursor must be a function");
|
|
107
|
+
}
|
|
108
|
+
guardStreamArgs.validate({
|
|
109
|
+
batchSize: opts.batchSize,
|
|
110
|
+
kind: opts.kind,
|
|
111
|
+
cursorOpts: opts.cursorOpts,
|
|
112
|
+
});
|
|
113
|
+
var batchSize = typeof opts.batchSize === "number" ? opts.batchSize : DEFAULT_BATCH_SIZE;
|
|
114
|
+
var orch = opts.orchestrator || null;
|
|
115
|
+
var auditImpl = opts.audit || audit();
|
|
116
|
+
var actor = opts.actor || null;
|
|
117
|
+
var kind = opts.kind || "stream";
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
[Symbol.asyncIterator]: function () {
|
|
121
|
+
return _makeIterator({
|
|
122
|
+
openCursor: opts.openCursor,
|
|
123
|
+
cursorOpts: opts.cursorOpts,
|
|
124
|
+
batchSize: batchSize,
|
|
125
|
+
orchestrator: orch,
|
|
126
|
+
audit: auditImpl,
|
|
127
|
+
actor: actor,
|
|
128
|
+
kind: kind,
|
|
129
|
+
});
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function _makeIterator(ctx) {
|
|
135
|
+
var streamId = ctx.orchestrator ? ctx.orchestrator.registerStream({ kind: ctx.kind, actor: ctx.actor }) : null;
|
|
136
|
+
var cursor = null;
|
|
137
|
+
var buffer = [];
|
|
138
|
+
var done = false;
|
|
139
|
+
var closed = false;
|
|
140
|
+
var drained = false;
|
|
141
|
+
_safeAudit(ctx.audit, "agent.stream.opened", ctx.actor, { kind: ctx.kind, streamId: streamId });
|
|
142
|
+
|
|
143
|
+
async function _closeOnce(reason) {
|
|
144
|
+
if (closed) return;
|
|
145
|
+
closed = true;
|
|
146
|
+
if (cursor && typeof cursor.close === "function") {
|
|
147
|
+
try { await cursor.close(); } catch (_e) { /* best-effort */ }
|
|
148
|
+
}
|
|
149
|
+
if (streamId && ctx.orchestrator) {
|
|
150
|
+
try { ctx.orchestrator.unregisterStream(streamId); } catch (_e) { /* best-effort */ }
|
|
151
|
+
}
|
|
152
|
+
_safeAudit(ctx.audit, "agent.stream.closed", ctx.actor, {
|
|
153
|
+
kind: ctx.kind, streamId: streamId, reason: reason || "exhausted",
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
next: async function () {
|
|
159
|
+
try {
|
|
160
|
+
if (buffer.length > 0) {
|
|
161
|
+
var row = buffer.shift();
|
|
162
|
+
return { value: row, done: false };
|
|
163
|
+
}
|
|
164
|
+
if (done) {
|
|
165
|
+
if (!closed) await _closeOnce("exhausted");
|
|
166
|
+
return { value: undefined, done: true };
|
|
167
|
+
}
|
|
168
|
+
// Check orchestrator drain BEFORE fetching the next batch.
|
|
169
|
+
if (ctx.orchestrator && ctx.orchestrator.isDraining && ctx.orchestrator.isDraining()) {
|
|
170
|
+
if (!drained) {
|
|
171
|
+
drained = true;
|
|
172
|
+
var marker = {
|
|
173
|
+
_drainMarker: true,
|
|
174
|
+
lastSeenCursor: cursor && typeof cursor.lastSeenCursor === "function"
|
|
175
|
+
? cursor.lastSeenCursor() : null,
|
|
176
|
+
reason: "drain",
|
|
177
|
+
};
|
|
178
|
+
_safeAudit(ctx.audit, "agent.stream.drain_marker_emitted", ctx.actor, {
|
|
179
|
+
kind: ctx.kind, streamId: streamId,
|
|
180
|
+
});
|
|
181
|
+
done = true;
|
|
182
|
+
return { value: marker, done: false };
|
|
183
|
+
}
|
|
184
|
+
await _closeOnce("drain");
|
|
185
|
+
return { value: undefined, done: true };
|
|
186
|
+
}
|
|
187
|
+
if (!cursor) {
|
|
188
|
+
cursor = await ctx.openCursor(ctx.cursorOpts);
|
|
189
|
+
if (!cursor || typeof cursor.fetchBatch !== "function") {
|
|
190
|
+
throw new AgentStreamError("agent-stream/bad-cursor",
|
|
191
|
+
"openCursor returned non-cursor (missing fetchBatch)");
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
var batch = await cursor.fetchBatch(ctx.batchSize);
|
|
195
|
+
if (!batch || typeof batch !== "object") {
|
|
196
|
+
throw new AgentStreamError("agent-stream/bad-batch",
|
|
197
|
+
"cursor.fetchBatch returned non-object");
|
|
198
|
+
}
|
|
199
|
+
var rows = batch.rows || [];
|
|
200
|
+
if (batch.done) done = true;
|
|
201
|
+
if (rows.length === 0) {
|
|
202
|
+
if (!closed) await _closeOnce("exhausted");
|
|
203
|
+
return { value: undefined, done: true };
|
|
204
|
+
}
|
|
205
|
+
// Push all but the first into the buffer; return the first.
|
|
206
|
+
for (var i = 1; i < rows.length; i += 1) buffer.push(rows[i]);
|
|
207
|
+
return { value: rows[0], done: false };
|
|
208
|
+
} catch (e) {
|
|
209
|
+
// Any error closes the cursor + emits an audit. Re-throw to
|
|
210
|
+
// surface upward.
|
|
211
|
+
await _closeOnce("error");
|
|
212
|
+
throw e;
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
return: async function () {
|
|
216
|
+
// Consumer's `break` calls this — close the cursor cleanly.
|
|
217
|
+
await _closeOnce("consumer-break");
|
|
218
|
+
return { value: undefined, done: true };
|
|
219
|
+
},
|
|
220
|
+
throw: async function (err) {
|
|
221
|
+
await _closeOnce("consumer-throw");
|
|
222
|
+
throw err;
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function _safeAudit(auditImpl, action, actor, metadata) {
|
|
228
|
+
agentAudit.safeAudit(auditImpl, action, actor, metadata);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
module.exports = {
|
|
232
|
+
create: create,
|
|
233
|
+
AgentStreamError: AgentStreamError,
|
|
234
|
+
guards: {
|
|
235
|
+
args: guardStreamArgs,
|
|
236
|
+
},
|
|
237
|
+
};
|