@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.
Files changed (73) hide show
  1. package/.claude/settings.local.json +7 -0
  2. package/README.md +1 -1
  3. package/build/index.d.ts +1 -3
  4. package/build/index.js +1 -5
  5. package/build/modules/utils.js +3 -31
  6. package/build/package.json +16 -27
  7. package/build/services/activities/activity.d.ts +43 -6
  8. package/build/services/activities/activity.js +262 -54
  9. package/build/services/activities/await.js +2 -2
  10. package/build/services/activities/cycle.js +1 -1
  11. package/build/services/activities/hook.d.ts +5 -0
  12. package/build/services/activities/hook.js +22 -19
  13. package/build/services/activities/interrupt.js +17 -25
  14. package/build/services/activities/signal.d.ts +4 -2
  15. package/build/services/activities/signal.js +27 -24
  16. package/build/services/activities/worker.js +2 -2
  17. package/build/services/collator/index.d.ts +123 -25
  18. package/build/services/collator/index.js +224 -101
  19. package/build/services/connector/factory.d.ts +1 -1
  20. package/build/services/connector/factory.js +1 -11
  21. package/build/services/engine/index.d.ts +5 -5
  22. package/build/services/engine/index.js +36 -15
  23. package/build/services/router/consumption/index.js +1 -1
  24. package/build/services/search/factory.js +1 -9
  25. package/build/services/store/factory.js +1 -9
  26. package/build/services/store/index.d.ts +5 -0
  27. package/build/services/store/providers/postgres/kvsql.d.ts +4 -0
  28. package/build/services/store/providers/postgres/kvsql.js +4 -0
  29. package/build/services/store/providers/postgres/kvtransaction.d.ts +2 -0
  30. package/build/services/store/providers/postgres/kvtransaction.js +23 -0
  31. package/build/services/store/providers/postgres/kvtypes/hash/basic.d.ts +51 -0
  32. package/build/services/store/providers/postgres/kvtypes/hash/basic.js +193 -1
  33. package/build/services/store/providers/postgres/kvtypes/hash/index.d.ts +4 -0
  34. package/build/services/store/providers/postgres/kvtypes/hash/index.js +6 -0
  35. package/build/services/store/providers/postgres/postgres.d.ts +20 -0
  36. package/build/services/store/providers/postgres/postgres.js +38 -1
  37. package/build/services/stream/factory.js +1 -17
  38. package/build/services/stream/providers/postgres/scout.js +2 -2
  39. package/build/services/sub/factory.js +1 -9
  40. package/build/services/sub/index.d.ts +1 -1
  41. package/build/services/sub/providers/postgres/postgres.d.ts +1 -1
  42. package/build/services/sub/providers/postgres/postgres.js +25 -10
  43. package/build/services/task/index.d.ts +1 -1
  44. package/build/services/task/index.js +2 -6
  45. package/build/types/index.d.ts +0 -1
  46. package/build/types/index.js +1 -4
  47. package/build/types/provider.d.ts +1 -1
  48. package/index.ts +0 -4
  49. package/package.json +16 -27
  50. package/build/services/connector/providers/ioredis.d.ts +0 -9
  51. package/build/services/connector/providers/ioredis.js +0 -26
  52. package/build/services/connector/providers/redis.d.ts +0 -9
  53. package/build/services/connector/providers/redis.js +0 -38
  54. package/build/services/search/providers/redis/ioredis.d.ts +0 -23
  55. package/build/services/search/providers/redis/ioredis.js +0 -189
  56. package/build/services/search/providers/redis/redis.d.ts +0 -23
  57. package/build/services/search/providers/redis/redis.js +0 -202
  58. package/build/services/store/providers/redis/_base.d.ts +0 -137
  59. package/build/services/store/providers/redis/_base.js +0 -980
  60. package/build/services/store/providers/redis/ioredis.d.ts +0 -20
  61. package/build/services/store/providers/redis/ioredis.js +0 -190
  62. package/build/services/store/providers/redis/redis.d.ts +0 -18
  63. package/build/services/store/providers/redis/redis.js +0 -199
  64. package/build/services/stream/providers/redis/ioredis.d.ts +0 -61
  65. package/build/services/stream/providers/redis/ioredis.js +0 -272
  66. package/build/services/stream/providers/redis/redis.d.ts +0 -61
  67. package/build/services/stream/providers/redis/redis.js +0 -305
  68. package/build/services/sub/providers/redis/ioredis.d.ts +0 -20
  69. package/build/services/sub/providers/redis/ioredis.js +0 -161
  70. package/build/services/sub/providers/redis/redis.d.ts +0 -18
  71. package/build/services/sub/providers/redis/redis.js +0 -148
  72. package/build/types/redis.d.ts +0 -258
  73. package/build/types/redis.js +0 -11
@@ -42,32 +42,108 @@ class CollatorService {
42
42
  dimensions.push('0');
43
43
  return dimensions.join(',');
44
44
  }
45
+ // ──────────────────────────────────────────────────
46
+ // Leg1 notarization
47
+ // ──────────────────────────────────────────────────
48
+ /**
49
+ * Leg1 entry: increment attempt counter (+1T).
50
+ * NOT bundled with Leg1 work — exists only to mark entry.
51
+ */
45
52
  static async notarizeEntry(activity, transaction) {
46
- //decrement by -100_000_000_000_000
47
- const amount = await activity.store.collate(activity.context.metadata.jid, activity.metadata.aid, -100000000000000, this.getDimensionalAddress(activity), transaction);
53
+ const amount = await activity.store.collate(activity.context.metadata.jid, activity.metadata.aid, this.WEIGHTS.LEG1_ENTRY, this.getDimensionalAddress(activity), transaction);
48
54
  this.verifyInteger(amount, 1, 'enter');
49
55
  return amount;
50
56
  }
51
- static async authorizeReentry(activity, transaction) {
52
- //set second digit to 8, allowing for re-entry
53
- //decrement by -10_000_000_000_000
54
- const amount = await activity.store.collate(activity.context.metadata.jid, activity.metadata.aid, -10000000000000, this.getDimensionalAddress(activity), transaction);
55
- return amount;
57
+ /**
58
+ * Leg1 completion: increment +100B to mark Leg1 complete.
59
+ * For cycle=true activities, also pre-seeds the Leg2 entry counter (+1)
60
+ * so the first real Leg2 gets adjacentIndex=1 (new dimension).
61
+ * MUST be bundled in the same transaction as Leg1 durable work.
62
+ */
63
+ static async notarizeLeg1Completion(activity, transaction) {
64
+ const amount = activity.config.cycle
65
+ ? this.WEIGHTS.LEG1_COMPLETE + this.WEIGHTS.LEG2_ENTRY
66
+ : this.WEIGHTS.LEG1_COMPLETE;
67
+ return await activity.store.collate(activity.context.metadata.jid, activity.metadata.aid, amount, this.getDimensionalAddress(activity), transaction);
56
68
  }
69
+ /**
70
+ * Leg1 early exit: marks Leg1 complete for activities that
71
+ * only run Leg1 and fully close (e.g., Cycle).
72
+ * Increment +100B (Leg1 completion marker).
73
+ */
57
74
  static async notarizeEarlyExit(activity, transaction) {
58
- //decrement the 2nd and 3rd digits to fully deactivate (`cycle` activities use this command to fully exit after leg 1) (should result in `888000000000000`)
59
- return await activity.store.collate(activity.context.metadata.jid, activity.metadata.aid, -11000000000000, this.getDimensionalAddress(activity), transaction);
75
+ return await activity.store.collate(activity.context.metadata.jid, activity.metadata.aid, this.WEIGHTS.LEG1_COMPLETE, this.getDimensionalAddress(activity), transaction);
60
76
  }
77
+ /**
78
+ * Leg1 early completion: marks Leg1 complete for Leg1-only
79
+ * activities that spawn children (e.g., Signal, Hook passthrough,
80
+ * Interrupt-another). Increment +100B.
81
+ */
61
82
  static async notarizeEarlyCompletion(activity, transaction) {
62
- //initialize both `possible` (1m) and `actualized` (1) zero dimension, while decrementing the 2nd
63
- //3rd digit is optionally kept open if the activity might be used in a cycle
64
- const decrement = activity.config.cycle
65
- ? 10000000000000
66
- : 11000000000000;
67
- return await activity.store.collate(activity.context.metadata.jid, activity.metadata.aid, 1000001 - decrement, this.getDimensionalAddress(activity), transaction);
83
+ return await activity.store.collate(activity.context.metadata.jid, activity.metadata.aid, this.WEIGHTS.LEG1_COMPLETE, this.getDimensionalAddress(activity), transaction);
84
+ }
85
+ // ──────────────────────────────────────────────────
86
+ // Leg2 notarization (entry + 3-step protocol)
87
+ // ──────────────────────────────────────────────────
88
+ /**
89
+ * Leg2 entry: atomically increments the activity ledger (+1) and
90
+ * seeds the GUID ledger with the ordinal IF NOT EXISTS.
91
+ * Returns [activityLedger, guidLedger] after the compound operation.
92
+ */
93
+ static async notarizeLeg2Entry(activity, guid, transaction) {
94
+ const jid = activity.context.metadata.jid;
95
+ const localMulti = transaction || activity.store.transact();
96
+ //compound: increment activity Leg2 counter and seed GUID ledger
97
+ await activity.store.collateLeg2Entry(jid, activity.metadata.aid, guid, this.getDimensionalAddress(activity, true), localMulti);
98
+ const results = await localMulti.exec();
99
+ const result = results[0];
100
+ const [amountConcrete, amountSynthetic] = Array.isArray(result)
101
+ ? result
102
+ : [result, result];
103
+ this.verifyInteger(amountConcrete, 2, 'enter');
104
+ this.verifySyntheticInteger(amountSynthetic);
105
+ return [amountConcrete, amountSynthetic];
106
+ }
107
+ /**
108
+ * Step 1: Mark Leg2 work done (+10B on GUID ledger only).
109
+ * MUST be bundled with Leg2 durable work writes.
110
+ */
111
+ static async notarizeStep1(activity, guid, transaction) {
112
+ const jid = activity.context.metadata.jid;
113
+ await activity.store.collateSynthetic(jid, guid, this.WEIGHTS.STEP1_WORK, transaction);
114
+ }
115
+ /**
116
+ * Step 2: Mark children spawned (+1B on GUID ledger only).
117
+ * The job semaphore update and GUID job-closed snapshot are handled
118
+ * by the compound `setStatusAndCollateGuid` primitive, which MUST
119
+ * be called in the same transaction.
120
+ */
121
+ static async notarizeStep2(activity, guid, transaction) {
122
+ const jid = activity.context.metadata.jid;
123
+ await activity.store.collateSynthetic(jid, guid, this.WEIGHTS.STEP2_SPAWN, transaction);
124
+ }
125
+ /**
126
+ * Step 3: Mark job completion tasks done (+100M on GUID ledger only).
127
+ * MUST be bundled with job completion durable writes.
128
+ */
129
+ static async notarizeStep3(activity, guid, transaction) {
130
+ const jid = activity.context.metadata.jid;
131
+ await activity.store.collateSynthetic(jid, guid, this.WEIGHTS.STEP3_CLEANUP, transaction);
132
+ }
133
+ /**
134
+ * Finalize: close the activity to new Leg2 GUIDs (+10T).
135
+ * Only for non-cycle activities after final SUCCESS/ERROR.
136
+ */
137
+ static async notarizeFinalize(activity, transaction) {
138
+ if (!activity.config.cycle) {
139
+ await activity.store.collate(activity.context.metadata.jid, activity.metadata.aid, this.WEIGHTS.FINALIZE, this.getDimensionalAddress(activity), transaction);
140
+ }
68
141
  }
142
+ // ──────────────────────────────────────────────────
143
+ // Inception (trigger duplicate detection)
144
+ // ──────────────────────────────────────────────────
69
145
  /**
70
- * sets the synthetic inception key (in case of an overage occurs).
146
+ * sets the synthetic inception key (in case an overage occurs).
71
147
  */
72
148
  static async notarizeInception(activity, guid, transaction) {
73
149
  if (guid) {
@@ -84,43 +160,60 @@ class CollatorService {
84
160
  }
85
161
  return false;
86
162
  }
163
+ // ──────────────────────────────────────────────────
164
+ // GUID ledger extraction (step-level resume)
165
+ // ──────────────────────────────────────────────────
87
166
  /**
88
- * verifies both the concrete and synthetic keys for the activity; concrete keys
89
- * exist in the original model and are effectively the 'real' keys. In reality,
90
- * hook activities are atomized during compilation to create a synthetic DAG that
91
- * is used to track the status of the graph in a distributed environment. The
92
- * synthetic key represents different dimensional realities and is used to
93
- * track re-entry overages (it distinguishes between the original and re-entry).
94
- * The essential challenge is: is this a re-entry that is purposeful in
95
- * order to induce cycles, or is the re-entry due to a failure in the system?
167
+ * Check if Step 1 (work done) is complete on the GUID ledger.
168
+ * Position 5 (10B digit) > 0.
96
169
  */
97
- static async notarizeReentry(activity, guid, transaction) {
98
- const jid = activity.context.metadata.jid;
99
- const localMulti = transaction || activity.store.transact();
100
- //increment by 1_000_000 (indicates re-entry and is used to drive the 'dimensional address' for adjacent activities (minus 1))
101
- await activity.store.collate(jid, activity.metadata.aid, 1000000, this.getDimensionalAddress(activity, true), localMulti);
102
- await activity.store.collateSynthetic(jid, guid, 1000000, localMulti);
103
- const [_amountConcrete, _amountSynthetic] = await localMulti.exec();
104
- const amountConcrete = Array.isArray(_amountConcrete)
105
- ? _amountConcrete[1]
106
- : _amountConcrete;
107
- const amountSynthetic = Array.isArray(_amountSynthetic)
108
- ? _amountSynthetic[1]
109
- : _amountSynthetic;
110
- this.verifyInteger(amountConcrete, 2, 'enter');
111
- this.verifySyntheticInteger(amountSynthetic);
112
- return amountConcrete;
170
+ static isGuidStep1Done(guidLedger) {
171
+ return this.getDigitAtPosition(guidLedger, 5) > 0;
113
172
  }
114
- static async notarizeContinuation(activity, transaction) {
115
- //keep open; actualize the leg2 dimension (+1)
116
- return await activity.store.collate(activity.context.metadata.jid, activity.metadata.aid, 1, this.getDimensionalAddress(activity), transaction);
173
+ /**
174
+ * Check if Step 2 (children spawned) is complete on the GUID ledger.
175
+ * Position 6 (1B digit) > 0.
176
+ */
177
+ static isGuidStep2Done(guidLedger) {
178
+ return this.getDigitAtPosition(guidLedger, 6) > 0;
179
+ }
180
+ /**
181
+ * Check if Step 3 (job completion tasks) is complete on the GUID ledger.
182
+ * Position 7 (100M digit) > 0.
183
+ */
184
+ static isGuidStep3Done(guidLedger) {
185
+ return this.getDigitAtPosition(guidLedger, 7) > 0;
186
+ }
187
+ /**
188
+ * Check if this GUID was responsible for closing the job.
189
+ * Position 4 (100B digit) > 0 (job closed snapshot).
190
+ */
191
+ static isGuidJobClosed(guidLedger) {
192
+ return this.getDigitAtPosition(guidLedger, 4) > 0;
193
+ }
194
+ /**
195
+ * Get the attempt count from the GUID ledger (last 8 digits).
196
+ */
197
+ static getGuidAttemptCount(guidLedger) {
198
+ return guidLedger % 100000000;
117
199
  }
118
- static async notarizeCompletion(activity, transaction) {
119
- //1) ALWAYS actualize leg2 dimension (+1)
120
- //2) IF the activity is used in a cycle, don't close leg 2!
121
- const decrement = activity.config.cycle ? 0 : 1000000000000;
122
- return await activity.store.collate(activity.context.metadata.jid, activity.metadata.aid, 1 - decrement, this.getDimensionalAddress(activity), transaction);
200
+ // ──────────────────────────────────────────────────
201
+ // Digit extraction
202
+ // ──────────────────────────────────────────────────
203
+ /**
204
+ * Gets the digit at a 1-indexed position from a 15-digit ledger value.
205
+ * The value is left-padded to 15 digits before extraction.
206
+ */
207
+ static getDigitAtPosition(num, position) {
208
+ const numStr = num.toString().padStart(this.targetLength, '0');
209
+ if (position < 1 || position > this.targetLength) {
210
+ return 0;
211
+ }
212
+ return parseInt(numStr[position - 1], 10);
123
213
  }
214
+ /**
215
+ * @deprecated Use getDigitAtPosition (1-indexed) instead
216
+ */
124
217
  static getDigitAtIndex(num, targetDigitIndex) {
125
218
  const numStr = num.toString();
126
219
  if (targetDigitIndex < 0 || targetDigitIndex >= numStr.length) {
@@ -129,77 +222,79 @@ class CollatorService {
129
222
  const digit = parseInt(numStr[targetDigitIndex], 10);
130
223
  return digit;
131
224
  }
225
+ /**
226
+ * Extracts the dimensional index from the Leg2 entry counter.
227
+ * Non-cycle activities: first Leg2 → leg2Count=1 → 1-1=0 (same dimension as Leg1).
228
+ * Cycle activities: first Leg2 → leg2Count=2 (pre-seeded +1) → 2-1=1 (new dimension).
229
+ */
132
230
  static getDimensionalIndex(num) {
133
- const numStr = num.toString();
134
- if (numStr.length < 9) {
231
+ const leg2EntryCount = num % 100000000;
232
+ if (leg2EntryCount <= 0) {
135
233
  return null;
136
234
  }
137
- const extractedStr = numStr.substring(3, 9);
138
- const extractedInt = parseInt(extractedStr, 10);
139
- return extractedInt - 1;
140
- }
141
- static isDuplicate(num, targetDigitIndex) {
142
- return this.getDigitAtIndex(num, targetDigitIndex) < 8;
143
- }
144
- static isInactive(num) {
145
- return this.getDigitAtIndex(num, 2) < 9;
146
- }
147
- static isPrimed(amount, leg) {
148
- //activity entry is not allowed if paths not properly pre-set
149
- if (leg == 1) {
150
- return amount != -100000000000000;
151
- }
152
- else {
153
- return (this.getDigitAtIndex(amount, 0) < 9 &&
154
- this.getDigitAtIndex(amount, 1) < 9);
155
- }
235
+ return leg2EntryCount - 1;
156
236
  }
237
+ // ──────────────────────────────────────────────────
238
+ // Verification
239
+ // ──────────────────────────────────────────────────
157
240
  /**
158
- * During compilation, the graphs are compiled into structures necessary
159
- * for distributed processing; these are referred to as 'synthetic DAGs',
160
- * because they are not part of the original graph, but are used to track
161
- * the status of the graph in a distributed environment. This check ensures
162
- * that the 'synthetic key' is not a duplicate. (which is different than
163
- * saying the 'key' is not a duplicate)
241
+ * Verifies the GUID ledger value for step-level resume decisions.
242
+ * The GUID ledger is seeded with an ordinal position (last 8 digits)
243
+ * on first entry; step markers drive all resume/reject logic.
244
+ *
245
+ * Fully processed: Step 3 done, or Steps 1+2 done without job closure.
246
+ * Crash recovery: Any incomplete step combination is allowed for resume.
164
247
  */
165
248
  static verifySyntheticInteger(amount) {
166
- const samount = amount.toString();
167
- const isCompletedValue = parseInt(samount[samount.length - 1], 10);
168
- if (isCompletedValue > 0) {
169
- //already done error (ack/delete clearly failed; this is a duplicate)
249
+ const step2Done = this.isGuidStep2Done(amount);
250
+ const step3Done = this.isGuidStep3Done(amount);
251
+ const jobClosed = this.isGuidJobClosed(amount);
252
+ if (step3Done) {
253
+ //all steps complete; nothing more to do
170
254
  throw new errors_1.CollationError(amount, 2, 'enter', collator_1.CollationFaultType.INACTIVE);
171
255
  }
172
- else if (amount >= 2000000) {
173
- //duplicate synthetic key (this is a duplicate job ID)
174
- throw new errors_1.CollationError(amount, 2, 'enter', collator_1.CollationFaultType.DUPLICATE);
256
+ if (step2Done && !jobClosed) {
257
+ //steps 1+2 done but this GUID didn't close the job; no Step 3 needed
258
+ throw new errors_1.CollationError(amount, 2, 'enter', collator_1.CollationFaultType.INACTIVE);
175
259
  }
260
+ //all other cases: allow entry
261
+ // - no steps done (fresh entry or pre-step-1 crash recovery)
262
+ // - step 1 done, step 2 not (crash after step 1)
263
+ // - steps 1+2 done, job closed, step 3 not (crash recovery for step 3)
176
264
  }
265
+ /**
266
+ * Verifies the activity ledger value at entry boundaries.
267
+ *
268
+ * Leg1 enter: pos 3 (1T digit) must be > 0 after +1T (proves seed exists).
269
+ * pos 4 (100B) must be 0 (Leg1 not yet complete).
270
+ * If pos 3 > 1 and pos 4 == 1, it's a stale/replayed message.
271
+ *
272
+ * Leg2 enter: pos 4 (100B) must be > 0 (Leg1 complete, reentry authorized).
273
+ * pos 2 (10T) must be 0 (not finalized) — cycle activities exempt.
274
+ */
177
275
  static verifyInteger(amount, leg, stage) {
178
276
  let faultType;
179
277
  if (leg === 1 && stage === 'enter') {
180
- if (!this.isPrimed(amount, 1)) {
181
- faultType = collator_1.CollationFaultType.MISSING;
182
- }
183
- else if (this.isDuplicate(amount, 0)) {
184
- faultType = collator_1.CollationFaultType.DUPLICATE;
185
- }
186
- else if (amount != 899000000000000) {
187
- faultType = collator_1.CollationFaultType.INVALID;
188
- }
189
- }
190
- else if (leg === 1 && stage === 'exit') {
191
- if (amount === -10000000000000) {
278
+ const leg1Attempts = this.getDigitAtPosition(amount, 3);
279
+ const leg1Complete = this.getDigitAtPosition(amount, 4);
280
+ if (leg1Attempts === 0) {
281
+ //seed was not set (no authorization)
192
282
  faultType = collator_1.CollationFaultType.MISSING;
193
283
  }
194
- else if (this.isDuplicate(amount, 1)) {
284
+ else if (leg1Complete > 0) {
285
+ //Leg1 already completed — stale/replayed message
195
286
  faultType = collator_1.CollationFaultType.DUPLICATE;
196
287
  }
197
288
  }
198
289
  else if (leg === 2 && stage === 'enter') {
199
- if (!this.isPrimed(amount, 2)) {
290
+ const leg1Complete = this.getDigitAtPosition(amount, 4);
291
+ const finalized = this.getDigitAtPosition(amount, 2);
292
+ if (leg1Complete === 0) {
293
+ //Leg1 not complete — reentry not authorized
200
294
  faultType = collator_1.CollationFaultType.FORBIDDEN;
201
295
  }
202
- else if (this.isInactive(amount)) {
296
+ else if (finalized > 0) {
297
+ //activity finalized — no new Leg2 GUIDs accepted
203
298
  faultType = collator_1.CollationFaultType.INACTIVE;
204
299
  }
205
300
  }
@@ -207,6 +302,9 @@ class CollatorService {
207
302
  throw new errors_1.CollationError(amount, leg, stage, faultType);
208
303
  }
209
304
  }
305
+ // ──────────────────────────────────────────────────
306
+ // Dimensional address resolution
307
+ // ──────────────────────────────────────────────────
210
308
  static getDimensionsById(ancestors, dad) {
211
309
  //ancestors is an ordered list of all ancestors, starting with the trigger (['t1', 'a1', 'a2'])
212
310
  //dad is the dimensional address of the ancestors list (',0,5,3')
@@ -221,18 +319,26 @@ class CollatorService {
221
319
  });
222
320
  return map;
223
321
  }
322
+ // ──────────────────────────────────────────────────
323
+ // Seeds
324
+ // ──────────────────────────────────────────────────
224
325
  /**
225
- * All non-trigger activities are assigned a status seed by their parent
326
+ * All non-trigger activities are assigned a status seed by their parent.
327
+ * Seed: 100000000000000 (pos 1 = 1, authorized for entry)
226
328
  */
227
329
  static getSeed() {
228
- return '999000000000000';
330
+ return '100000000000000';
229
331
  }
230
332
  /**
231
- * All trigger activities are assigned a status seed in a completed state
333
+ * All trigger activities are assigned a status seed in a completed state.
334
+ * Seed: 101100000000001 (authorized, 1 Leg1 entry, Leg1 complete, 1 Leg2 entry)
232
335
  */
233
336
  static getTriggerSeed() {
234
- return '888000001000001';
337
+ return '101100000000001';
235
338
  }
339
+ // ──────────────────────────────────────────────────
340
+ // Compiler
341
+ // ──────────────────────────────────────────────────
236
342
  /**
237
343
  * entry point for compiler-type activities. This is called by the compiler
238
344
  * to bind the sorted activity IDs to the trigger activity. These are then used
@@ -291,3 +397,20 @@ class CollatorService {
291
397
  exports.CollatorService = CollatorService;
292
398
  //max int digit count that supports `hincrby`
293
399
  CollatorService.targetLength = 15;
400
+ /**
401
+ * Positional weights for the 15-digit activity/GUID ledger.
402
+ *
403
+ * Position: 1 2 3 4 5 6 7 8-15
404
+ * Weight: 100T 10T 1T 100B 10B 1B 100M 10M..1
405
+ */
406
+ CollatorService.WEIGHTS = {
407
+ AUTH: 100000000000000,
408
+ FINALIZE: 10000000000000,
409
+ LEG1_ENTRY: 1000000000000,
410
+ LEG1_COMPLETE: 100000000000,
411
+ STEP1_WORK: 10000000000,
412
+ STEP2_SPAWN: 1000000000,
413
+ STEP3_CLEANUP: 100000000,
414
+ LEG2_ENTRY: 1,
415
+ GUID_SNAPSHOT: 100000000000, // 100B on GUID ledger (job closed snapshot)
416
+ };
@@ -3,7 +3,7 @@ import { ProviderConfig, ProviderNativeClient } from '../../types/provider';
3
3
  export declare class ConnectorService {
4
4
  static disconnectAll(): Promise<void>;
5
5
  /**
6
- * Connect to a provider (redis, nats, postgres) and return the native
6
+ * Connect to a provider (nats, postgres) and return the native
7
7
  * client. Connections are handled by the engine and worker routers at
8
8
  * initialization, but the factory method provided here is useful
9
9
  * for testing provider configurations.
@@ -2,19 +2,15 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ConnectorService = void 0;
4
4
  const utils_1 = require("../../modules/utils");
5
- const ioredis_1 = require("./providers/ioredis");
6
5
  const nats_1 = require("./providers/nats");
7
6
  const postgres_1 = require("./providers/postgres");
8
- const redis_1 = require("./providers/redis");
9
7
  class ConnectorService {
10
8
  static async disconnectAll() {
11
- await redis_1.RedisConnection.disconnectAll();
12
- await ioredis_1.RedisConnection.disconnectAll();
13
9
  await postgres_1.PostgresConnection.disconnectAll();
14
10
  await nats_1.NatsConnection.disconnectAll();
15
11
  }
16
12
  /**
17
- * Connect to a provider (redis, nats, postgres) and return the native
13
+ * Connect to a provider (nats, postgres) and return the native
18
14
  * client. Connections are handled by the engine and worker routers at
19
15
  * initialization, but the factory method provided here is useful
20
16
  * for testing provider configurations.
@@ -81,12 +77,6 @@ class ConnectorService {
81
77
  let clientInstance;
82
78
  const id = (0, utils_1.guid)();
83
79
  switch (providerType) {
84
- case 'redis':
85
- clientInstance = await redis_1.RedisConnection.connect(id, providerClass, options, { provider: providerName });
86
- break;
87
- case 'ioredis':
88
- clientInstance = await ioredis_1.RedisConnection.connect(id, providerClass, options, { provider: providerName });
89
- break;
90
80
  case 'nats':
91
81
  clientInstance = await nats_1.NatsConnection.connect(id, providerClass, options, { provider: providerName });
92
82
  break;
@@ -143,7 +143,7 @@ declare class EngineService {
143
143
  /**
144
144
  * @private
145
145
  */
146
- execAdjacentParent(context: JobState, jobOutput: JobOutput, emit?: boolean): Promise<string>;
146
+ execAdjacentParent(context: JobState, jobOutput: JobOutput, emit?: boolean, transaction?: ProviderTransaction): Promise<string>;
147
147
  /**
148
148
  * @private
149
149
  */
@@ -163,7 +163,7 @@ declare class EngineService {
163
163
  /**
164
164
  * @private
165
165
  */
166
- hook(topic: string, data: JobData, status?: StreamStatus, code?: StreamCode): Promise<string>;
166
+ hook(topic: string, data: JobData, status?: StreamStatus, code?: StreamCode, transaction?: ProviderTransaction): Promise<string>;
167
167
  /**
168
168
  * @private
169
169
  */
@@ -199,7 +199,7 @@ declare class EngineService {
199
199
  /**
200
200
  * @private
201
201
  */
202
- pubOneTimeSubs(context: JobState, jobOutput: JobOutput, emit?: boolean): Promise<void>;
202
+ pubOneTimeSubs(context: JobState, jobOutput: JobOutput, emit?: boolean, transaction?: ProviderTransaction): Promise<void>;
203
203
  /**
204
204
  * @private
205
205
  */
@@ -207,7 +207,7 @@ declare class EngineService {
207
207
  /**
208
208
  * @private
209
209
  */
210
- pubPermSubs(context: JobState, jobOutput: JobOutput, emit?: boolean): Promise<void>;
210
+ pubPermSubs(context: JobState, jobOutput: JobOutput, emit?: boolean, transaction?: ProviderTransaction): Promise<void>;
211
211
  /**
212
212
  * @private
213
213
  */
@@ -227,7 +227,7 @@ declare class EngineService {
227
227
  /**
228
228
  * @private
229
229
  */
230
- runJobCompletionTasks(context: JobState, options?: JobCompletionOptions): Promise<string | void>;
230
+ runJobCompletionTasks(context: JobState, options?: JobCompletionOptions, transaction?: ProviderTransaction): Promise<string | void>;
231
231
  /**
232
232
  * Job hash expiration is typically reliant on the metadata field
233
233
  * if the activity concludes normally. However, if the job is `interrupted`,
@@ -381,7 +381,7 @@ class EngineService {
381
381
  /**
382
382
  * @private
383
383
  */
384
- async execAdjacentParent(context, jobOutput, emit = false) {
384
+ async execAdjacentParent(context, jobOutput, emit = false, transaction) {
385
385
  if (this.hasParentJob(context)) {
386
386
  //errors are stringified `StreamError` objects
387
387
  const error = this.resolveError(jobOutput.metadata);
@@ -414,7 +414,7 @@ class EngineService {
414
414
  streamData.status = stream_1.StreamStatus.SUCCESS;
415
415
  streamData.code = enums_1.HMSH_CODE_SUCCESS;
416
416
  }
417
- return (await this.router?.publishMessage(null, streamData));
417
+ return (await this.router?.publishMessage(null, streamData, transaction));
418
418
  }
419
419
  }
420
420
  /**
@@ -461,7 +461,7 @@ class EngineService {
461
461
  /**
462
462
  * @private
463
463
  */
464
- async hook(topic, data, status = stream_1.StreamStatus.SUCCESS, code = 200) {
464
+ async hook(topic, data, status = stream_1.StreamStatus.SUCCESS, code = 200, transaction) {
465
465
  const hookRule = await this.taskService.getHookRule(topic);
466
466
  const [aid] = await this.getSchema(`.${hookRule.to}`);
467
467
  const streamData = {
@@ -475,7 +475,7 @@ class EngineService {
475
475
  },
476
476
  data,
477
477
  };
478
- return (await this.router?.publishMessage(null, streamData));
478
+ return (await this.router?.publishMessage(null, streamData, transaction));
479
479
  }
480
480
  /**
481
481
  * @private
@@ -621,7 +621,7 @@ class EngineService {
621
621
  /**
622
622
  * @private
623
623
  */
624
- async pubOneTimeSubs(context, jobOutput, emit = false) {
624
+ async pubOneTimeSubs(context, jobOutput, emit = false, transaction) {
625
625
  //todo: subscriber should query for the job...only publish minimum context needed
626
626
  if (this.hasOneTimeSubscription(context)) {
627
627
  const message = {
@@ -629,7 +629,7 @@ class EngineService {
629
629
  topic: context.metadata.jid,
630
630
  job: (0, utils_1.restoreHierarchy)(jobOutput),
631
631
  };
632
- this.subscribe.publish(key_1.KeyType.QUORUM, message, this.appId, context.metadata.ngn);
632
+ await this.subscribe.publish(key_1.KeyType.QUORUM, message, this.appId, context.metadata.ngn, transaction);
633
633
  }
634
634
  }
635
635
  /**
@@ -644,7 +644,7 @@ class EngineService {
644
644
  /**
645
645
  * @private
646
646
  */
647
- async pubPermSubs(context, jobOutput, emit = false) {
647
+ async pubPermSubs(context, jobOutput, emit = false, transaction) {
648
648
  const topic = await this.getPublishesTopic(context);
649
649
  if (topic) {
650
650
  const message = {
@@ -652,7 +652,7 @@ class EngineService {
652
652
  topic,
653
653
  job: (0, utils_1.restoreHierarchy)(jobOutput),
654
654
  };
655
- this.subscribe.publish(key_1.KeyType.QUORUM, message, this.appId, `${topic}.${context.metadata.jid}`);
655
+ await this.subscribe.publish(key_1.KeyType.QUORUM, message, this.appId, `${topic}.${context.metadata.jid}`, transaction);
656
656
  }
657
657
  }
658
658
  /**
@@ -683,20 +683,41 @@ class EngineService {
683
683
  /**
684
684
  * @private
685
685
  */
686
- async runJobCompletionTasks(context, options = {}) {
686
+ async runJobCompletionTasks(context, options = {}, transaction) {
687
687
  //'emit' indicates the job is still active
688
688
  const isAwait = this.hasParentJob(context, true);
689
689
  const isOneTimeSub = this.hasOneTimeSubscription(context);
690
690
  const topic = await this.getPublishesTopic(context);
691
691
  let msgId;
692
+ let jobOutput;
692
693
  if (isAwait || isOneTimeSub || topic) {
693
- const jobOutput = await this.getState(context.metadata.tpc, context.metadata.jid);
694
- msgId = await this.execAdjacentParent(context, jobOutput, options.emit);
695
- this.pubOneTimeSubs(context, jobOutput, options.emit);
696
- this.pubPermSubs(context, jobOutput, options.emit);
694
+ jobOutput = await this.getState(context.metadata.tpc, context.metadata.jid);
695
+ //only send RESULT to parent for non-severed children (execChild).
696
+ //startChild (px=true) children already sent RESULT at spawn time
697
+ //via Trigger.execAdjacentParent; sending again here would cause
698
+ //a duplicate semaphore decrement in collation workflows.
699
+ if (isAwait) {
700
+ msgId = await this.execAdjacentParent(context, jobOutput, options.emit, transaction);
701
+ }
702
+ if (transaction) {
703
+ //transactional: await to queue NOTIFY in the transaction
704
+ await this.pubOneTimeSubs(context, jobOutput, options.emit, transaction);
705
+ await this.pubPermSubs(context, jobOutput, options.emit, transaction);
706
+ }
707
+ else {
708
+ //non-transactional: fire-and-forget to avoid race with inline
709
+ //trigger processing (callback registered after pub() returns)
710
+ this.pubOneTimeSubs(context, jobOutput, options.emit);
711
+ this.pubPermSubs(context, jobOutput, options.emit);
712
+ }
697
713
  }
698
714
  if (!options.emit) {
699
- this.taskService.registerJobForCleanup(context.metadata.jid, this.resolveExpires(context, options), options);
715
+ if (transaction) {
716
+ await this.taskService.registerJobForCleanup(context.metadata.jid, this.resolveExpires(context, options), options, transaction);
717
+ }
718
+ else {
719
+ this.taskService.registerJobForCleanup(context.metadata.jid, this.resolveExpires(context, options), options);
720
+ }
700
721
  }
701
722
  return msgId;
702
723
  }
@@ -745,7 +766,7 @@ class EngineService {
745
766
  }
746
767
  const [state, status] = output;
747
768
  const stateTree = (0, utils_1.restoreHierarchy)(state);
748
- if (status && stateTree.metadata) {
769
+ if (status != null && stateTree.metadata) {
749
770
  stateTree.metadata.js = status;
750
771
  }
751
772
  return stateTree;
@@ -75,7 +75,7 @@ class ConsumptionManager {
75
75
  const features = this.stream.getProviderSpecificFeatures();
76
76
  const supportsNotifications = features.supportsNotifications;
77
77
  if (supportsNotifications) {
78
- this.logger.info(`router-stream-using-notifications`, {
78
+ this.logger.debug(`router-stream-using-notifications`, {
79
79
  group,
80
80
  consumer,
81
81
  stream,