@hotmeshio/hotmesh 0.7.0 → 0.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.
- package/.claude/settings.local.json +7 -0
- package/README.md +1 -1
- package/build/index.d.ts +1 -3
- package/build/index.js +1 -5
- package/build/modules/utils.js +3 -31
- package/build/package.json +16 -27
- package/build/services/activities/activity.d.ts +43 -6
- package/build/services/activities/activity.js +262 -54
- package/build/services/activities/await.js +2 -2
- package/build/services/activities/cycle.js +1 -1
- package/build/services/activities/hook.d.ts +5 -0
- package/build/services/activities/hook.js +22 -19
- package/build/services/activities/interrupt.js +17 -25
- package/build/services/activities/signal.d.ts +4 -2
- package/build/services/activities/signal.js +27 -24
- package/build/services/activities/worker.js +2 -2
- package/build/services/collator/index.d.ts +123 -25
- package/build/services/collator/index.js +224 -101
- package/build/services/connector/factory.d.ts +1 -1
- package/build/services/connector/factory.js +1 -11
- package/build/services/engine/index.d.ts +5 -5
- package/build/services/engine/index.js +36 -15
- package/build/services/router/consumption/index.js +1 -1
- package/build/services/search/factory.js +1 -9
- package/build/services/store/factory.js +1 -9
- package/build/services/store/index.d.ts +5 -0
- package/build/services/store/providers/postgres/kvsql.d.ts +4 -0
- package/build/services/store/providers/postgres/kvsql.js +4 -0
- package/build/services/store/providers/postgres/kvtransaction.d.ts +2 -0
- package/build/services/store/providers/postgres/kvtransaction.js +23 -0
- package/build/services/store/providers/postgres/kvtypes/hash/basic.d.ts +51 -0
- package/build/services/store/providers/postgres/kvtypes/hash/basic.js +193 -1
- package/build/services/store/providers/postgres/kvtypes/hash/index.d.ts +4 -0
- package/build/services/store/providers/postgres/kvtypes/hash/index.js +6 -0
- package/build/services/store/providers/postgres/postgres.d.ts +20 -0
- package/build/services/store/providers/postgres/postgres.js +38 -1
- package/build/services/stream/factory.js +1 -17
- package/build/services/stream/providers/postgres/scout.js +2 -2
- package/build/services/sub/factory.js +1 -9
- package/build/services/sub/index.d.ts +1 -1
- package/build/services/sub/providers/postgres/postgres.d.ts +1 -1
- package/build/services/sub/providers/postgres/postgres.js +25 -10
- package/build/services/task/index.d.ts +1 -1
- package/build/services/task/index.js +2 -6
- package/build/types/index.d.ts +0 -1
- package/build/types/index.js +1 -4
- package/build/types/provider.d.ts +1 -1
- package/index.ts +0 -4
- package/package.json +16 -27
- package/build/services/connector/providers/ioredis.d.ts +0 -9
- package/build/services/connector/providers/ioredis.js +0 -26
- package/build/services/connector/providers/redis.d.ts +0 -9
- package/build/services/connector/providers/redis.js +0 -38
- package/build/services/search/providers/redis/ioredis.d.ts +0 -23
- package/build/services/search/providers/redis/ioredis.js +0 -189
- package/build/services/search/providers/redis/redis.d.ts +0 -23
- package/build/services/search/providers/redis/redis.js +0 -202
- package/build/services/store/providers/redis/_base.d.ts +0 -137
- package/build/services/store/providers/redis/_base.js +0 -980
- package/build/services/store/providers/redis/ioredis.d.ts +0 -20
- package/build/services/store/providers/redis/ioredis.js +0 -190
- package/build/services/store/providers/redis/redis.d.ts +0 -18
- package/build/services/store/providers/redis/redis.js +0 -199
- package/build/services/stream/providers/redis/ioredis.d.ts +0 -61
- package/build/services/stream/providers/redis/ioredis.js +0 -272
- package/build/services/stream/providers/redis/redis.d.ts +0 -61
- package/build/services/stream/providers/redis/redis.js +0 -305
- package/build/services/sub/providers/redis/ioredis.d.ts +0 -20
- package/build/services/sub/providers/redis/ioredis.js +0 -161
- package/build/services/sub/providers/redis/redis.d.ts +0 -18
- package/build/services/sub/providers/redis/redis.js +0 -148
- package/build/types/redis.d.ts +0 -258
- package/build/types/redis.js +0 -11
|
@@ -18,6 +18,7 @@ class Activity {
|
|
|
18
18
|
this.status = stream_1.StreamStatus.SUCCESS;
|
|
19
19
|
this.code = 200;
|
|
20
20
|
this.adjacentIndex = 0;
|
|
21
|
+
this.guidLedger = 0;
|
|
21
22
|
this.config = config;
|
|
22
23
|
this.data = data;
|
|
23
24
|
this.metadata = metadata;
|
|
@@ -56,6 +57,7 @@ class Activity {
|
|
|
56
57
|
}
|
|
57
58
|
catch (error) {
|
|
58
59
|
await collator_1.CollatorService.notarizeEntry(this);
|
|
60
|
+
//todo: confirm this check is still needed; the edge event cleanup should handle fully
|
|
59
61
|
if (threshold > 0) {
|
|
60
62
|
if (this.context.metadata.js === threshold) {
|
|
61
63
|
//conclude job EXACTLY ONCE
|
|
@@ -73,14 +75,19 @@ class Activity {
|
|
|
73
75
|
await collator_1.CollatorService.notarizeEntry(this);
|
|
74
76
|
}
|
|
75
77
|
/**
|
|
76
|
-
* Upon entering leg 2 of a duplexed activity
|
|
78
|
+
* Upon entering leg 2 of a duplexed activity.
|
|
79
|
+
* Increments both the activity ledger (+1) and GUID ledger (+1).
|
|
80
|
+
* Stores the GUID ledger value for step-level resume decisions.
|
|
77
81
|
*/
|
|
78
82
|
async verifyReentry() {
|
|
79
|
-
const
|
|
83
|
+
const msgGuid = this.context.metadata.guid;
|
|
80
84
|
this.setLeg(2);
|
|
81
85
|
await this.getState();
|
|
86
|
+
this.context.metadata.guid = msgGuid;
|
|
82
87
|
collator_1.CollatorService.assertJobActive(this.context.metadata.js, this.context.metadata.jid, this.metadata.aid);
|
|
83
|
-
|
|
88
|
+
const [activityLedger, guidLedger] = await collator_1.CollatorService.notarizeLeg2Entry(this, msgGuid);
|
|
89
|
+
this.guidLedger = guidLedger;
|
|
90
|
+
return activityLedger;
|
|
84
91
|
}
|
|
85
92
|
//******** DUPLEX RE-ENTRY POINT ********//
|
|
86
93
|
async processEvent(status = stream_1.StreamStatus.SUCCESS, code = 200, type = 'output') {
|
|
@@ -108,17 +115,43 @@ class Activity {
|
|
|
108
115
|
this.adjacentIndex = collator_1.CollatorService.getDimensionalIndex(collationKey);
|
|
109
116
|
telemetry = new telemetry_1.TelemetryService(this.engine.appId, this.config, this.metadata, this.context);
|
|
110
117
|
telemetry.startActivitySpan(this.leg);
|
|
111
|
-
|
|
112
|
-
if (status === stream_1.StreamStatus.
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
118
|
+
//bind data per status type
|
|
119
|
+
if (status === stream_1.StreamStatus.ERROR) {
|
|
120
|
+
this.bindActivityError(this.data);
|
|
121
|
+
this.adjacencyList = await this.filterAdjacent();
|
|
122
|
+
if (!this.adjacencyList.length) {
|
|
123
|
+
this.bindJobError(this.data);
|
|
124
|
+
}
|
|
117
125
|
}
|
|
118
126
|
else {
|
|
119
|
-
|
|
127
|
+
this.bindActivityData(type);
|
|
128
|
+
this.adjacencyList = await this.filterAdjacent();
|
|
120
129
|
}
|
|
121
|
-
this.
|
|
130
|
+
this.mapJobData();
|
|
131
|
+
//When an unrecoverable error has no matching transitions
|
|
132
|
+
//(e.g., code 500 from raw errors after retries exhausted),
|
|
133
|
+
//mark the job as terminally errored so the step protocol
|
|
134
|
+
//can force completion via the isErrorTerminal path.
|
|
135
|
+
if (status === stream_1.StreamStatus.ERROR && !this.adjacencyList?.length) {
|
|
136
|
+
if (!this.context.data)
|
|
137
|
+
this.context.data = {};
|
|
138
|
+
this.context.data.done = true;
|
|
139
|
+
this.context.data.$error = {
|
|
140
|
+
message: this.data?.message || 'unknown error',
|
|
141
|
+
code: enums_1.HMSH_CODE_MEMFLOW_MAXED,
|
|
142
|
+
stack: this.data?.stack,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
//determine step parameters
|
|
146
|
+
const delta = status === stream_1.StreamStatus.PENDING
|
|
147
|
+
? this.adjacencyList.length
|
|
148
|
+
: this.adjacencyList.length - 1;
|
|
149
|
+
const shouldFinalize = status !== stream_1.StreamStatus.PENDING;
|
|
150
|
+
//execute 3-step protocol
|
|
151
|
+
const thresholdHit = await this.executeStepProtocol(delta, shouldFinalize);
|
|
152
|
+
//telemetry
|
|
153
|
+
telemetry.mapActivityAttributes();
|
|
154
|
+
telemetry.setActivityAttributes({});
|
|
122
155
|
}
|
|
123
156
|
catch (error) {
|
|
124
157
|
if (error instanceof errors_1.CollationError) {
|
|
@@ -151,50 +184,100 @@ class Activity {
|
|
|
151
184
|
this.logger.debug('activity-process-event-end', { jid, aid });
|
|
152
185
|
}
|
|
153
186
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
}
|
|
196
|
-
|
|
187
|
+
/**
|
|
188
|
+
* Executes the 3-step Leg2 protocol using GUID ledger for
|
|
189
|
+
* crash-safe resume. Each step bundles durable writes with
|
|
190
|
+
* its concluding digit update in a single transaction.
|
|
191
|
+
*
|
|
192
|
+
* @returns true if this transition caused the job to complete
|
|
193
|
+
*/
|
|
194
|
+
async executeStepProtocol(delta, shouldFinalize) {
|
|
195
|
+
const msgGuid = this.context.metadata.guid;
|
|
196
|
+
const threshold = this.mapStatusThreshold();
|
|
197
|
+
const { id: appId } = await this.engine.getVID();
|
|
198
|
+
//Step 1: Save work (skip if GUID 10B already set)
|
|
199
|
+
if (!collator_1.CollatorService.isGuidStep1Done(this.guidLedger)) {
|
|
200
|
+
const txn1 = this.store.transact();
|
|
201
|
+
await this.setState(txn1);
|
|
202
|
+
await collator_1.CollatorService.notarizeStep1(this, msgGuid, txn1);
|
|
203
|
+
await txn1.exec();
|
|
204
|
+
}
|
|
205
|
+
//Step 2: Spawn children + semaphore + edge capture (skip if GUID 1B already set)
|
|
206
|
+
let thresholdHit = false;
|
|
207
|
+
if (!collator_1.CollatorService.isGuidStep2Done(this.guidLedger)) {
|
|
208
|
+
const txn2 = this.store.transact();
|
|
209
|
+
//queue step markers first
|
|
210
|
+
await collator_1.CollatorService.notarizeStep2(this, msgGuid, txn2);
|
|
211
|
+
//queue child publications
|
|
212
|
+
for (const child of this.adjacencyList) {
|
|
213
|
+
await this.engine.router?.publishMessage(null, child, txn2);
|
|
214
|
+
}
|
|
215
|
+
//queue semaphore update + edge capture LAST (so result is at end)
|
|
216
|
+
await this.store.setStatusAndCollateGuid(delta, threshold, this.context.metadata.jid, appId, msgGuid, collator_1.CollatorService.WEIGHTS.GUID_SNAPSHOT, txn2);
|
|
217
|
+
const results = (await txn2.exec());
|
|
218
|
+
thresholdHit = this.resolveThresholdHit(results);
|
|
219
|
+
this.logger.debug('step-protocol-step2-complete', {
|
|
220
|
+
jid: this.context.metadata.jid,
|
|
221
|
+
aid: this.metadata.aid,
|
|
222
|
+
delta,
|
|
223
|
+
threshold,
|
|
224
|
+
thresholdHit,
|
|
225
|
+
lastResult: results[results.length - 1],
|
|
226
|
+
resultCount: results.length,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
//Step 2 already done; check GUID snapshot for edge
|
|
231
|
+
thresholdHit = collator_1.CollatorService.isGuidJobClosed(this.guidLedger);
|
|
232
|
+
}
|
|
233
|
+
//Step 3: Job completion tasks (edge hit OR emit/persist, skip if GUID 100M already set)
|
|
234
|
+
//When an activity marks the job done with an unrecoverable error
|
|
235
|
+
//(e.g., stopper after max retries), force completion even when the
|
|
236
|
+
//semaphore threshold isn't hit (the signaler's +1 contribution
|
|
237
|
+
//prevents threshold 0 from matching).
|
|
238
|
+
const isErrorTerminal = !thresholdHit
|
|
239
|
+
&& this.context.data?.done === true
|
|
240
|
+
&& !!this.context.data?.$error;
|
|
241
|
+
const needsCompletion = thresholdHit || this.shouldEmit() || this.shouldPersistJob() || isErrorTerminal;
|
|
242
|
+
if (needsCompletion && !collator_1.CollatorService.isGuidStep3Done(this.guidLedger)) {
|
|
243
|
+
const txn3 = this.store.transact();
|
|
244
|
+
const options = (thresholdHit || isErrorTerminal) ? {} : { emit: !this.shouldPersistJob() };
|
|
245
|
+
await this.engine.runJobCompletionTasks(this.context, options, txn3);
|
|
246
|
+
await collator_1.CollatorService.notarizeStep3(this, msgGuid, txn3);
|
|
247
|
+
const shouldFinalizeNow = (thresholdHit || isErrorTerminal) ? shouldFinalize : this.shouldPersistJob();
|
|
248
|
+
if (shouldFinalizeNow) {
|
|
249
|
+
await collator_1.CollatorService.notarizeFinalize(this, txn3);
|
|
250
|
+
}
|
|
251
|
+
await txn3.exec();
|
|
252
|
+
}
|
|
253
|
+
else if (needsCompletion) {
|
|
254
|
+
this.logger.debug('step-protocol-step3-skipped-already-done', {
|
|
255
|
+
jid: this.context.metadata.jid,
|
|
256
|
+
aid: this.metadata.aid,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
this.logger.debug('step-protocol-no-threshold', {
|
|
261
|
+
jid: this.context.metadata.jid,
|
|
262
|
+
aid: this.metadata.aid,
|
|
263
|
+
thresholdHit,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
return thresholdHit;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Extracts the thresholdHit value from transaction results.
|
|
270
|
+
* The setStatusAndCollateGuid result is the last item.
|
|
271
|
+
*/
|
|
272
|
+
resolveThresholdHit(results) {
|
|
273
|
+
const last = results[results.length - 1];
|
|
274
|
+
const value = Array.isArray(last) ? last[1] : last;
|
|
275
|
+
return Number(value) === 1;
|
|
197
276
|
}
|
|
277
|
+
/**
|
|
278
|
+
* Extracts the job status from the last result of a transaction.
|
|
279
|
+
* Used by subclass Leg1 process methods for telemetry.
|
|
280
|
+
*/
|
|
198
281
|
resolveStatus(multiResponse) {
|
|
199
282
|
const activityStatus = multiResponse[multiResponse.length - 1];
|
|
200
283
|
if (Array.isArray(activityStatus)) {
|
|
@@ -204,6 +287,127 @@ class Activity {
|
|
|
204
287
|
return Number(activityStatus);
|
|
205
288
|
}
|
|
206
289
|
}
|
|
290
|
+
/**
|
|
291
|
+
* Leg1 entry verification for Category B activities (Leg1-only with children).
|
|
292
|
+
* Returns true if this is a resume (Leg1 already completed on a prior attempt).
|
|
293
|
+
* On resume, loads the GUID ledger for step-level resume decisions.
|
|
294
|
+
*/
|
|
295
|
+
async verifyLeg1Entry() {
|
|
296
|
+
const msgGuid = this.context.metadata.guid;
|
|
297
|
+
this.setLeg(1);
|
|
298
|
+
await this.getState();
|
|
299
|
+
this.context.metadata.guid = msgGuid;
|
|
300
|
+
const threshold = this.mapStatusThreshold();
|
|
301
|
+
try {
|
|
302
|
+
collator_1.CollatorService.assertJobActive(this.context.metadata.js, this.context.metadata.jid, this.metadata.aid, threshold);
|
|
303
|
+
}
|
|
304
|
+
catch (error) {
|
|
305
|
+
if (error instanceof errors_1.InactiveJobError && threshold > 0) {
|
|
306
|
+
//Dynamic Activation Control: threshold met, close the job
|
|
307
|
+
await collator_1.CollatorService.notarizeEntry(this);
|
|
308
|
+
if (this.context.metadata.js === threshold) {
|
|
309
|
+
//conclude job EXACTLY ONCE
|
|
310
|
+
const status = await this.setStatus(-threshold);
|
|
311
|
+
if (Number(status) === 0) {
|
|
312
|
+
await this.engine.runJobCompletionTasks(this.context);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
throw error;
|
|
317
|
+
}
|
|
318
|
+
try {
|
|
319
|
+
await collator_1.CollatorService.notarizeEntry(this);
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
catch (error) {
|
|
323
|
+
if (error instanceof errors_1.CollationError && error.fault === 'duplicate') {
|
|
324
|
+
if (this.config.cycle) {
|
|
325
|
+
//Cycle re-entry: Leg1 already complete from prior iteration.
|
|
326
|
+
//Increment Leg2 counter to derive the new dimensional index,
|
|
327
|
+
//so children run in a fresh dimensional plane.
|
|
328
|
+
const [activityLedger, guidLedger] = await collator_1.CollatorService.notarizeLeg2Entry(this, msgGuid);
|
|
329
|
+
this.adjacentIndex =
|
|
330
|
+
collator_1.CollatorService.getDimensionalIndex(activityLedger);
|
|
331
|
+
this.guidLedger = guidLedger;
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
//100B is set — Leg1 work already committed. Load GUID for step resume.
|
|
335
|
+
const guidValue = await this.store.collateSynthetic(this.context.metadata.jid, msgGuid, 0);
|
|
336
|
+
this.guidLedger = guidValue;
|
|
337
|
+
return true;
|
|
338
|
+
}
|
|
339
|
+
throw error;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Executes the 3-step Leg1 protocol for Category B activities
|
|
344
|
+
* (Leg1-only with children, e.g., Hook passthrough, Signal, Interrupt-another).
|
|
345
|
+
* Uses the incoming Leg1 message GUID as the GUID ledger key.
|
|
346
|
+
*
|
|
347
|
+
* Step A: setState + notarizeLeg1Completion + step1 markers (transaction 1)
|
|
348
|
+
* Step B: publish children + step2 markers + setStatusAndCollateGuid (transaction 2)
|
|
349
|
+
* Step C: if edge → runJobCompletionTasks + step3 markers + finalize (transaction 3)
|
|
350
|
+
*
|
|
351
|
+
* @returns true if this transition caused the job to complete
|
|
352
|
+
*/
|
|
353
|
+
async executeLeg1StepProtocol(delta) {
|
|
354
|
+
const msgGuid = this.context.metadata.guid;
|
|
355
|
+
const threshold = this.mapStatusThreshold();
|
|
356
|
+
const { id: appId } = await this.engine.getVID();
|
|
357
|
+
//Step A: Save work + Leg1 completion marker
|
|
358
|
+
if (!collator_1.CollatorService.isGuidStep1Done(this.guidLedger)) {
|
|
359
|
+
const txn1 = this.store.transact();
|
|
360
|
+
await this.setState(txn1);
|
|
361
|
+
if (this.adjacentIndex === 0) {
|
|
362
|
+
//First entry: mark Leg1 complete. On cycle re-entry
|
|
363
|
+
//(adjacentIndex > 0), Leg1 is already complete and the
|
|
364
|
+
//Leg2 counter was already incremented by notarizeLeg2Entry.
|
|
365
|
+
await collator_1.CollatorService.notarizeLeg1Completion(this, txn1);
|
|
366
|
+
}
|
|
367
|
+
await collator_1.CollatorService.notarizeStep1(this, msgGuid, txn1);
|
|
368
|
+
await txn1.exec();
|
|
369
|
+
}
|
|
370
|
+
//Step B: Spawn children + semaphore + edge capture
|
|
371
|
+
let thresholdHit = false;
|
|
372
|
+
if (!collator_1.CollatorService.isGuidStep2Done(this.guidLedger)) {
|
|
373
|
+
const txn2 = this.store.transact();
|
|
374
|
+
await collator_1.CollatorService.notarizeStep2(this, msgGuid, txn2);
|
|
375
|
+
for (const child of this.adjacencyList) {
|
|
376
|
+
await this.engine.router?.publishMessage(null, child, txn2);
|
|
377
|
+
}
|
|
378
|
+
await this.store.setStatusAndCollateGuid(delta, threshold, this.context.metadata.jid, appId, msgGuid, collator_1.CollatorService.WEIGHTS.GUID_SNAPSHOT, txn2);
|
|
379
|
+
const results = (await txn2.exec());
|
|
380
|
+
thresholdHit = this.resolveThresholdHit(results);
|
|
381
|
+
this.logger.debug('leg1-step-protocol-stepB-complete', {
|
|
382
|
+
jid: this.context.metadata.jid,
|
|
383
|
+
aid: this.metadata.aid,
|
|
384
|
+
delta,
|
|
385
|
+
threshold,
|
|
386
|
+
thresholdHit,
|
|
387
|
+
lastResult: results[results.length - 1],
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
else {
|
|
391
|
+
thresholdHit = collator_1.CollatorService.isGuidJobClosed(this.guidLedger);
|
|
392
|
+
}
|
|
393
|
+
//Step C: Job completion tasks (edge hit OR emit/persist)
|
|
394
|
+
//When an activity marks the job done with an unrecoverable error
|
|
395
|
+
//(e.g., stopper after max retries), force completion even when the
|
|
396
|
+
//semaphore threshold isn't hit.
|
|
397
|
+
const isErrorTerminal = !thresholdHit
|
|
398
|
+
&& this.context.data?.done === true
|
|
399
|
+
&& !!this.context.data?.$error;
|
|
400
|
+
const needsCompletion = thresholdHit || this.shouldEmit() || this.shouldPersistJob() || isErrorTerminal;
|
|
401
|
+
if (needsCompletion && !collator_1.CollatorService.isGuidStep3Done(this.guidLedger)) {
|
|
402
|
+
const txn3 = this.store.transact();
|
|
403
|
+
const options = (thresholdHit || isErrorTerminal) ? {} : { emit: !this.shouldPersistJob() };
|
|
404
|
+
await this.engine.runJobCompletionTasks(this.context, options, txn3);
|
|
405
|
+
await collator_1.CollatorService.notarizeStep3(this, msgGuid, txn3);
|
|
406
|
+
await collator_1.CollatorService.notarizeFinalize(this, txn3);
|
|
407
|
+
await txn3.exec();
|
|
408
|
+
}
|
|
409
|
+
return thresholdHit;
|
|
410
|
+
}
|
|
207
411
|
mapJobData() {
|
|
208
412
|
if (this.config.job?.maps) {
|
|
209
413
|
const mapper = new mapper_1.MapperService((0, utils_1.deepCopy)(this.config.job.maps), this.context);
|
|
@@ -517,6 +721,10 @@ class Activity {
|
|
|
517
721
|
}
|
|
518
722
|
return false;
|
|
519
723
|
}
|
|
724
|
+
/**
|
|
725
|
+
* Transition method for Category C (Leg1-only, no children, no semaphore change)
|
|
726
|
+
* and Category D (Trigger) activities. NOT used by the Leg2 step protocol.
|
|
727
|
+
*/
|
|
520
728
|
async transition(adjacencyList, jobStatus) {
|
|
521
729
|
if (this.jobWasInterrupted(jobStatus)) {
|
|
522
730
|
return;
|
|
@@ -25,11 +25,11 @@ class Await extends activity_1.Activity {
|
|
|
25
25
|
telemetry = new telemetry_1.TelemetryService(this.engine.appId, this.config, this.metadata, this.context);
|
|
26
26
|
telemetry.startActivitySpan(this.leg);
|
|
27
27
|
this.mapInputData();
|
|
28
|
-
//save state and
|
|
28
|
+
//save state and mark Leg1 complete
|
|
29
29
|
const transaction = this.store.transact();
|
|
30
30
|
//todo: await this.registerTimeout();
|
|
31
31
|
const messageId = await this.execActivity(transaction);
|
|
32
|
-
await collator_1.CollatorService.
|
|
32
|
+
await collator_1.CollatorService.notarizeLeg1Completion(this, transaction);
|
|
33
33
|
await this.setState(transaction);
|
|
34
34
|
await this.setStatus(0, transaction);
|
|
35
35
|
const multiResponse = (await transaction.exec());
|
|
@@ -38,7 +38,7 @@ class Cycle extends activity_1.Activity {
|
|
|
38
38
|
'app.job.jss': jobStatus,
|
|
39
39
|
});
|
|
40
40
|
//exit early (`Cycle` activities only execute Leg 1)
|
|
41
|
-
await collator_1.CollatorService.
|
|
41
|
+
await collator_1.CollatorService.notarizeLeg1Completion(this, transaction);
|
|
42
42
|
(await transaction.exec());
|
|
43
43
|
return this.context.metadata.aid;
|
|
44
44
|
}
|
|
@@ -13,6 +13,11 @@ declare class Hook extends Activity {
|
|
|
13
13
|
config: HookActivity;
|
|
14
14
|
constructor(config: ActivityType, data: ActivityData, metadata: ActivityMetadata, hook: ActivityData | null, engine: EngineService, context?: JobState);
|
|
15
15
|
process(): Promise<string>;
|
|
16
|
+
/**
|
|
17
|
+
* Static config check: does this activity have a hook or sleep config?
|
|
18
|
+
* Used for routing before context is loaded.
|
|
19
|
+
*/
|
|
20
|
+
isConfiguredAsHook(): boolean;
|
|
16
21
|
/**
|
|
17
22
|
* does this activity use a time-hook or web-hook
|
|
18
23
|
*/
|
|
@@ -24,15 +24,21 @@ class Hook extends activity_1.Activity {
|
|
|
24
24
|
});
|
|
25
25
|
let telemetry;
|
|
26
26
|
try {
|
|
27
|
-
|
|
27
|
+
//Phase 1: Load state and verify entry (all paths use verifyLeg1Entry
|
|
28
|
+
//for GUID-ledger-backed crash recovery)
|
|
29
|
+
const isResume = await this.verifyLeg1Entry();
|
|
28
30
|
telemetry = new telemetry_1.TelemetryService(this.engine.appId, this.config, this.metadata, this.context);
|
|
29
31
|
telemetry.startActivitySpan(this.leg);
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
32
|
+
//Phase 2: Route based on RUNTIME evaluation (not static config)
|
|
33
|
+
if (this.isConfiguredAsHook() && this.doesHook()) {
|
|
34
|
+
//Category A: duplexed hook registration (Leg2 handles completion)
|
|
35
|
+
if (!isResume) {
|
|
36
|
+
await this.doHook(telemetry);
|
|
37
|
+
}
|
|
38
|
+
//If resume, Leg1 already ran — Leg2 will handle completion
|
|
33
39
|
}
|
|
34
40
|
else {
|
|
35
|
-
//
|
|
41
|
+
//Category B: passthrough with crash-safe step protocol + GUID ledger
|
|
36
42
|
await this.doPassThrough(telemetry);
|
|
37
43
|
}
|
|
38
44
|
return this.context.metadata.aid;
|
|
@@ -76,6 +82,13 @@ class Hook extends activity_1.Activity {
|
|
|
76
82
|
});
|
|
77
83
|
}
|
|
78
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* Static config check: does this activity have a hook or sleep config?
|
|
87
|
+
* Used for routing before context is loaded.
|
|
88
|
+
*/
|
|
89
|
+
isConfiguredAsHook() {
|
|
90
|
+
return !!this.config.sleep || !!this.config.hook?.topic;
|
|
91
|
+
}
|
|
79
92
|
/**
|
|
80
93
|
* does this activity use a time-hook or web-hook
|
|
81
94
|
*/
|
|
@@ -92,29 +105,19 @@ class Hook extends activity_1.Activity {
|
|
|
92
105
|
this.mapOutputData();
|
|
93
106
|
this.mapJobData();
|
|
94
107
|
await this.setState(transaction);
|
|
95
|
-
await collator_1.CollatorService.
|
|
108
|
+
await collator_1.CollatorService.notarizeLeg1Completion(this, transaction);
|
|
96
109
|
await this.setStatus(0, transaction);
|
|
97
110
|
await transaction.exec();
|
|
98
111
|
telemetry.mapActivityAttributes();
|
|
99
112
|
}
|
|
100
113
|
async doPassThrough(telemetry) {
|
|
101
|
-
const transaction = this.store.transact();
|
|
102
|
-
let multiResponse;
|
|
103
114
|
this.adjacencyList = await this.filterAdjacent();
|
|
104
115
|
this.mapOutputData();
|
|
105
116
|
this.mapJobData();
|
|
106
|
-
|
|
107
|
-
await
|
|
108
|
-
await this.setStatus(this.adjacencyList.length - 1, transaction);
|
|
109
|
-
multiResponse = (await transaction.exec());
|
|
117
|
+
//Category B: use Leg1 step protocol for crash-safe edge capture
|
|
118
|
+
await this.executeLeg1StepProtocol(this.adjacencyList.length - 1);
|
|
110
119
|
telemetry.mapActivityAttributes();
|
|
111
|
-
|
|
112
|
-
const attrs = { 'app.job.jss': jobStatus };
|
|
113
|
-
const messageIds = await this.transition(this.adjacencyList, jobStatus);
|
|
114
|
-
if (messageIds.length) {
|
|
115
|
-
attrs['app.activity.mids'] = messageIds.join(',');
|
|
116
|
-
}
|
|
117
|
-
telemetry.setActivityAttributes(attrs);
|
|
120
|
+
telemetry.setActivityAttributes({});
|
|
118
121
|
}
|
|
119
122
|
async getHookRule(topic) {
|
|
120
123
|
const rules = await this.store.getHookRules();
|
|
@@ -19,13 +19,18 @@ class Interrupt extends activity_1.Activity {
|
|
|
19
19
|
});
|
|
20
20
|
let telemetry;
|
|
21
21
|
try {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
if (!this.config.target) {
|
|
23
|
+
//Category C: self-interrupt (no children, no semaphore edge risk)
|
|
24
|
+
await this.verifyEntry();
|
|
25
|
+
telemetry = new telemetry_1.TelemetryService(this.engine.appId, this.config, this.metadata, this.context);
|
|
26
|
+
telemetry.startActivitySpan(this.leg);
|
|
26
27
|
await this.interruptSelf(telemetry);
|
|
27
28
|
}
|
|
28
29
|
else {
|
|
30
|
+
//Category B: interrupt another (spawns children, needs step protocol)
|
|
31
|
+
await this.verifyLeg1Entry();
|
|
32
|
+
telemetry = new telemetry_1.TelemetryService(this.engine.appId, this.config, this.metadata, this.context);
|
|
33
|
+
telemetry.startActivitySpan(this.leg);
|
|
29
34
|
await this.interruptAnother(telemetry);
|
|
30
35
|
}
|
|
31
36
|
}
|
|
@@ -76,10 +81,10 @@ class Interrupt extends activity_1.Activity {
|
|
|
76
81
|
}
|
|
77
82
|
// Interrupt THIS job
|
|
78
83
|
const messageId = await this.interrupt();
|
|
79
|
-
// Notarize completion and
|
|
84
|
+
// Notarize Leg1 completion and set status
|
|
80
85
|
telemetry.mapActivityAttributes();
|
|
81
86
|
const transaction = this.store.transact();
|
|
82
|
-
await collator_1.CollatorService.
|
|
87
|
+
await collator_1.CollatorService.notarizeLeg1Completion(this, transaction);
|
|
83
88
|
await this.setStatus(-1, transaction);
|
|
84
89
|
const txResponse = (await transaction.exec());
|
|
85
90
|
const jobStatus = this.resolveStatus(txResponse);
|
|
@@ -90,31 +95,18 @@ class Interrupt extends activity_1.Activity {
|
|
|
90
95
|
return this.context.metadata.aid;
|
|
91
96
|
}
|
|
92
97
|
async interruptAnother(telemetry) {
|
|
93
|
-
// Interrupt ANOTHER job
|
|
94
|
-
|
|
95
|
-
const attrs = { 'app.activity.mid': messageId };
|
|
98
|
+
// Interrupt ANOTHER job (best-effort, fires before step protocol)
|
|
99
|
+
await this.interrupt();
|
|
96
100
|
// Apply updates to THIS job's state
|
|
97
|
-
telemetry.mapActivityAttributes();
|
|
98
101
|
this.adjacencyList = await this.filterAdjacent();
|
|
99
102
|
if (this.config.job?.maps || this.config.output?.maps) {
|
|
100
103
|
this.mapOutputData();
|
|
101
104
|
this.mapJobData();
|
|
102
|
-
const transaction = this.store.transact();
|
|
103
|
-
await this.setState(transaction);
|
|
104
105
|
}
|
|
105
|
-
//
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const txResponse = (await transaction.exec());
|
|
110
|
-
const jobStatus = this.resolveStatus(txResponse);
|
|
111
|
-
attrs['app.job.jss'] = jobStatus;
|
|
112
|
-
// Transition next generation and log
|
|
113
|
-
const messageIds = await this.transition(this.adjacencyList, jobStatus);
|
|
114
|
-
if (messageIds.length) {
|
|
115
|
-
attrs['app.activity.mids'] = messageIds.join(',');
|
|
116
|
-
}
|
|
117
|
-
telemetry.setActivityAttributes(attrs);
|
|
106
|
+
//Category B: use Leg1 step protocol for crash-safe edge capture
|
|
107
|
+
await this.executeLeg1StepProtocol(this.adjacencyList.length - 1);
|
|
108
|
+
telemetry.mapActivityAttributes();
|
|
109
|
+
telemetry.setActivityAttributes({});
|
|
118
110
|
return this.context.metadata.aid;
|
|
119
111
|
}
|
|
120
112
|
isInterruptingSelf() {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { EngineService } from '../engine';
|
|
2
2
|
import { ActivityData, ActivityMetadata, ActivityType, SignalActivity } from '../../types/activity';
|
|
3
3
|
import { JobState } from '../../types/job';
|
|
4
|
+
import { ProviderTransaction } from '../../types/provider';
|
|
4
5
|
import { Activity } from './activity';
|
|
5
6
|
declare class Signal extends Activity {
|
|
6
7
|
config: SignalActivity;
|
|
@@ -9,9 +10,10 @@ declare class Signal extends Activity {
|
|
|
9
10
|
mapSignalData(): Record<string, any>;
|
|
10
11
|
mapResolverData(): Record<string, any>;
|
|
11
12
|
/**
|
|
12
|
-
* The signal activity will hook one
|
|
13
|
+
* The signal activity will hook one. Accepts an optional transaction
|
|
14
|
+
* so the hook publish can be bundled with the Leg1 completion marker.
|
|
13
15
|
*/
|
|
14
|
-
hookOne(): Promise<string>;
|
|
16
|
+
hookOne(transaction?: ProviderTransaction): Promise<string>;
|
|
15
17
|
/**
|
|
16
18
|
* The signal activity will hook all paused jobs that share the same job key.
|
|
17
19
|
*/
|