@hotmeshio/hotmesh 0.7.0 → 0.9.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 (138) hide show
  1. package/.claude/settings.local.json +8 -0
  2. package/README.md +158 -38
  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 +63 -79
  7. package/build/services/activities/activity.d.ts +97 -9
  8. package/build/services/activities/activity.js +323 -86
  9. package/build/services/activities/await.d.ts +101 -0
  10. package/build/services/activities/await.js +103 -2
  11. package/build/services/activities/cycle.d.ts +82 -0
  12. package/build/services/activities/cycle.js +86 -8
  13. package/build/services/activities/hook.d.ts +144 -1
  14. package/build/services/activities/hook.js +162 -21
  15. package/build/services/activities/interrupt.d.ts +112 -0
  16. package/build/services/activities/interrupt.js +134 -29
  17. package/build/services/activities/signal.d.ts +111 -4
  18. package/build/services/activities/signal.js +136 -28
  19. package/build/services/activities/trigger.d.ts +56 -4
  20. package/build/services/activities/trigger.js +119 -35
  21. package/build/services/activities/worker.d.ts +107 -0
  22. package/build/services/activities/worker.js +109 -2
  23. package/build/services/collator/index.d.ts +116 -30
  24. package/build/services/collator/index.js +211 -115
  25. package/build/services/connector/factory.d.ts +1 -1
  26. package/build/services/connector/factory.js +1 -11
  27. package/build/services/engine/index.d.ts +22 -6
  28. package/build/services/engine/index.js +49 -18
  29. package/build/services/exporter/index.d.ts +2 -0
  30. package/build/services/exporter/index.js +1 -0
  31. package/build/services/hotmesh/index.d.ts +471 -236
  32. package/build/services/hotmesh/index.js +473 -238
  33. package/build/services/memflow/client.js +2 -2
  34. package/build/services/memflow/handle.js +1 -1
  35. package/build/services/memflow/index.d.ts +1 -1
  36. package/build/services/memflow/index.js +1 -1
  37. package/build/services/memflow/workflow/all.d.ts +28 -3
  38. package/build/services/memflow/workflow/all.js +28 -3
  39. package/build/services/memflow/workflow/context.d.ts +44 -1
  40. package/build/services/memflow/workflow/context.js +44 -1
  41. package/build/services/memflow/workflow/didRun.d.ts +23 -3
  42. package/build/services/memflow/workflow/didRun.js +23 -3
  43. package/build/services/memflow/workflow/emit.d.ts +43 -4
  44. package/build/services/memflow/workflow/emit.js +43 -4
  45. package/build/services/memflow/workflow/enrich.d.ts +32 -4
  46. package/build/services/memflow/workflow/enrich.js +32 -4
  47. package/build/services/memflow/workflow/entityMethods.d.ts +54 -7
  48. package/build/services/memflow/workflow/entityMethods.js +54 -7
  49. package/build/services/memflow/workflow/execChild.d.ts +96 -8
  50. package/build/services/memflow/workflow/execChild.js +96 -8
  51. package/build/services/memflow/workflow/execHook.d.ts +54 -39
  52. package/build/services/memflow/workflow/execHook.js +52 -38
  53. package/build/services/memflow/workflow/execHookBatch.d.ts +82 -29
  54. package/build/services/memflow/workflow/execHookBatch.js +80 -28
  55. package/build/services/memflow/workflow/hook.d.ts +68 -3
  56. package/build/services/memflow/workflow/hook.js +69 -4
  57. package/build/services/memflow/workflow/index.d.ts +65 -10
  58. package/build/services/memflow/workflow/index.js +65 -10
  59. package/build/services/memflow/workflow/interrupt.d.ts +50 -4
  60. package/build/services/memflow/workflow/interrupt.js +50 -4
  61. package/build/services/memflow/workflow/interruption.d.ts +49 -16
  62. package/build/services/memflow/workflow/interruption.js +49 -16
  63. package/build/services/memflow/workflow/isSideEffectAllowed.d.ts +21 -4
  64. package/build/services/memflow/workflow/isSideEffectAllowed.js +21 -4
  65. package/build/services/memflow/workflow/proxyActivities.d.ts +70 -42
  66. package/build/services/memflow/workflow/proxyActivities.js +70 -42
  67. package/build/services/memflow/workflow/random.d.ts +33 -3
  68. package/build/services/memflow/workflow/random.js +33 -3
  69. package/build/services/memflow/workflow/searchMethods.d.ts +49 -2
  70. package/build/services/memflow/workflow/searchMethods.js +49 -2
  71. package/build/services/memflow/workflow/signal.d.ts +51 -22
  72. package/build/services/memflow/workflow/signal.js +52 -23
  73. package/build/services/memflow/workflow/sleepFor.d.ts +57 -18
  74. package/build/services/memflow/workflow/sleepFor.js +57 -18
  75. package/build/services/memflow/workflow/trace.d.ts +39 -6
  76. package/build/services/memflow/workflow/trace.js +39 -6
  77. package/build/services/memflow/workflow/waitFor.d.ts +55 -18
  78. package/build/services/memflow/workflow/waitFor.js +55 -18
  79. package/build/services/router/consumption/index.js +1 -1
  80. package/build/services/search/factory.js +1 -9
  81. package/build/services/store/factory.js +1 -9
  82. package/build/services/store/index.d.ts +6 -1
  83. package/build/services/store/providers/postgres/kvsql.d.ts +4 -0
  84. package/build/services/store/providers/postgres/kvsql.js +4 -0
  85. package/build/services/store/providers/postgres/kvtransaction.d.ts +2 -0
  86. package/build/services/store/providers/postgres/kvtransaction.js +23 -0
  87. package/build/services/store/providers/postgres/kvtypes/hash/basic.d.ts +51 -0
  88. package/build/services/store/providers/postgres/kvtypes/hash/basic.js +193 -1
  89. package/build/services/store/providers/postgres/kvtypes/hash/index.d.ts +4 -0
  90. package/build/services/store/providers/postgres/kvtypes/hash/index.js +6 -0
  91. package/build/services/store/providers/postgres/postgres.d.ts +21 -1
  92. package/build/services/store/providers/postgres/postgres.js +42 -4
  93. package/build/services/stream/factory.js +1 -17
  94. package/build/services/stream/providers/postgres/scout.js +2 -2
  95. package/build/services/sub/factory.js +1 -9
  96. package/build/services/sub/index.d.ts +1 -1
  97. package/build/services/sub/providers/postgres/postgres.d.ts +1 -1
  98. package/build/services/sub/providers/postgres/postgres.js +25 -10
  99. package/build/services/task/index.d.ts +1 -1
  100. package/build/services/task/index.js +2 -6
  101. package/build/services/telemetry/index.js +6 -0
  102. package/build/types/activity.d.ts +1 -1
  103. package/build/types/hotmesh.d.ts +1 -1
  104. package/build/types/index.d.ts +0 -1
  105. package/build/types/index.js +1 -4
  106. package/build/types/job.d.ts +1 -1
  107. package/build/types/memflow.d.ts +1 -1
  108. package/build/types/provider.d.ts +1 -1
  109. package/build/types/quorum.d.ts +2 -2
  110. package/build/vitest.config.d.ts +2 -0
  111. package/build/vitest.config.js +18 -0
  112. package/index.ts +0 -4
  113. package/package.json +63 -79
  114. package/vitest.config.ts +17 -0
  115. package/build/services/connector/providers/ioredis.d.ts +0 -9
  116. package/build/services/connector/providers/ioredis.js +0 -26
  117. package/build/services/connector/providers/redis.d.ts +0 -9
  118. package/build/services/connector/providers/redis.js +0 -38
  119. package/build/services/search/providers/redis/ioredis.d.ts +0 -23
  120. package/build/services/search/providers/redis/ioredis.js +0 -189
  121. package/build/services/search/providers/redis/redis.d.ts +0 -23
  122. package/build/services/search/providers/redis/redis.js +0 -202
  123. package/build/services/store/providers/redis/_base.d.ts +0 -137
  124. package/build/services/store/providers/redis/_base.js +0 -980
  125. package/build/services/store/providers/redis/ioredis.d.ts +0 -20
  126. package/build/services/store/providers/redis/ioredis.js +0 -190
  127. package/build/services/store/providers/redis/redis.d.ts +0 -18
  128. package/build/services/store/providers/redis/redis.js +0 -199
  129. package/build/services/stream/providers/redis/ioredis.d.ts +0 -61
  130. package/build/services/stream/providers/redis/ioredis.js +0 -272
  131. package/build/services/stream/providers/redis/redis.d.ts +0 -61
  132. package/build/services/stream/providers/redis/redis.js +0 -305
  133. package/build/services/sub/providers/redis/ioredis.d.ts +0 -20
  134. package/build/services/sub/providers/redis/ioredis.js +0 -161
  135. package/build/services/sub/providers/redis/redis.d.ts +0 -18
  136. package/build/services/sub/providers/redis/redis.js +0 -148
  137. package/build/types/redis.d.ts +0 -258
  138. package/build/types/redis.js +0 -11
@@ -1,20 +1,75 @@
1
1
  import { EngineService } from '../engine';
2
2
  import { ILogger } from '../logger';
3
3
  import { StoreService } from '../store';
4
- import { TelemetryService } from '../telemetry';
5
4
  import { ActivityData, ActivityLeg, ActivityMetadata, ActivityType } from '../../types/activity';
6
5
  import { ProviderClient, ProviderTransaction, TransactionResultList } from '../../types/provider';
7
6
  import { JobState, JobStatus } from '../../types/job';
8
7
  import { StringAnyType } from '../../types/serializer';
9
8
  import { StreamCode, StreamData, StreamStatus } from '../../types/stream';
10
9
  /**
11
- * The base class for all activities
10
+ * Base class for all HotMesh activity types. Activities are the execution
11
+ * units within a YAML-defined workflow graph. Each activity represents a
12
+ * node in a Directed Acyclic Graph (DAG) that the engine orchestrates.
13
+ *
14
+ * ## Activity Categories
15
+ *
16
+ * Activities fall into three execution categories:
17
+ *
18
+ * - **Category A (Duplex)**: Two-phase activities with Leg 1 (dispatch) and
19
+ * Leg 2 (response). Used by `Worker` and `Await`. Leg 1
20
+ * publishes a message and waits; Leg 2 handles the response via
21
+ * `processEvent` and transitions to adjacent activities.
22
+ *
23
+ * - **Category B (Leg1-only with children)**: Single-phase activities that
24
+ * execute work and transition to children using the crash-safe
25
+ * `executeLeg1StepProtocol`. Used by `Hook` (passthrough mode),
26
+ * `Signal`, and `Interrupt` (target mode).
27
+ *
28
+ * - **Category C (Leg1-only, no children)**: Terminal activities that
29
+ * execute without spawning children. Used by `Interrupt` (self mode).
30
+ *
31
+ * ## Shared YAML Configuration
32
+ *
33
+ * All activity types support these base properties in the YAML descriptor:
34
+ *
35
+ * | Property | Type | Description |
36
+ * |----------------------|---------|-------------|
37
+ * | `type` | string | Activity type: `trigger`, `worker`, `await`, `hook`, `signal`, `interrupt`, `cycle` |
38
+ * | `title` | string | Human-readable label for the activity |
39
+ * | `input.schema` | object | JSON Schema for input validation |
40
+ * | `input.maps` | object | Maps data from other activities into this activity's input |
41
+ * | `output.schema` | object | JSON Schema for output validation |
42
+ * | `output.maps` | object | Maps/transforms the activity's own output data |
43
+ * | `job.maps` | object | Maps activity data to the shared job state |
44
+ * | `emit` | boolean | If `true`, emits a message to the graph's `publishes` topic |
45
+ * | `persist` | boolean | If `true`, emits the job-completed event while keeping the job active |
46
+ * | `expire` | number | Seconds until the job expires after completion (`-1` = forever) |
47
+ * | `statusThreshold` | number | Custom semaphore threshold for Dynamic Activation Control |
48
+ * | `cycle` | boolean | If `true`, leaves Leg 2 open so the activity can be re-entered |
49
+ *
50
+ * ## Data Mapping Syntax
51
+ *
52
+ * Mapping expressions use curly-brace references to bind data between
53
+ * activities and the shared job state:
54
+ *
55
+ * ```yaml
56
+ * input:
57
+ * maps:
58
+ * x: '{t1.output.data.fieldName}' # reference another activity's output
59
+ * y: '{$self.output.data.fieldName}' # reference own output
60
+ * z: '{$job.data.fieldName}' # reference shared job state
61
+ * s: '{$app.settings.configKey}' # reference app-level settings
62
+ * ```
63
+ *
64
+ * @see {@link https://hotmeshio.github.io/sdk-typescript/docs/quickstart | Quick Start Guide}
65
+ * @see [Model Driven Development](https://hotmeshio.github.io/sdk-typescript/docs/model_driven_development)
12
66
  */
13
67
  declare class Activity {
14
68
  config: ActivityType;
15
69
  data: ActivityData;
16
70
  hook: ActivityData;
17
71
  metadata: ActivityMetadata;
72
+ /** @hidden */
18
73
  store: StoreService<ProviderClient, ProviderTransaction>;
19
74
  context: JobState;
20
75
  engine: EngineService;
@@ -24,6 +79,7 @@ declare class Activity {
24
79
  leg: ActivityLeg;
25
80
  adjacencyList: StreamData[];
26
81
  adjacentIndex: number;
82
+ guidLedger: number;
27
83
  constructor(config: ActivityType, data: ActivityData, metadata: ActivityMetadata, hook: ActivityData | null, engine: EngineService, context?: JobState);
28
84
  setLeg(leg: ActivityLeg): void;
29
85
  /**
@@ -37,15 +93,48 @@ declare class Activity {
37
93
  */
38
94
  verifyEntry(): Promise<void>;
39
95
  /**
40
- * Upon entering leg 2 of a duplexed activity
96
+ * Upon entering leg 2 of a duplexed activity.
97
+ * Increments both the activity ledger (+1) and GUID ledger (+1).
98
+ * Stores the GUID ledger value for step-level resume decisions.
41
99
  */
42
100
  verifyReentry(): Promise<number>;
43
101
  processEvent(status?: StreamStatus, code?: StreamCode, type?: 'hook' | 'output'): Promise<void>;
44
- processPending(type: 'hook' | 'output'): Promise<TransactionResultList>;
45
- processSuccess(type: 'hook' | 'output'): Promise<TransactionResultList>;
46
- processError(): Promise<TransactionResultList>;
47
- transitionAdjacent(multiResponse: TransactionResultList, telemetry: TelemetryService): Promise<void>;
102
+ /**
103
+ * Executes the 3-step Leg2 protocol using GUID ledger for
104
+ * crash-safe resume. Each step bundles durable writes with
105
+ * its concluding digit update in a single transaction.
106
+ *
107
+ * @returns true if this transition caused the job to complete
108
+ */
109
+ executeStepProtocol(delta: number, shouldFinalize: boolean): Promise<boolean>;
110
+ /**
111
+ * Extracts the thresholdHit value from transaction results.
112
+ * The setStatusAndCollateGuid result is the last item.
113
+ */
114
+ resolveThresholdHit(results: TransactionResultList): boolean;
115
+ /**
116
+ * Extracts the job status from the last result of a transaction.
117
+ * Used by subclass Leg1 process methods for telemetry.
118
+ */
48
119
  resolveStatus(multiResponse: TransactionResultList): number;
120
+ /**
121
+ * Leg1 entry verification for Category B activities (Leg1-only with children).
122
+ * Returns true if this is a resume (Leg1 already completed on a prior attempt).
123
+ * On resume, loads the GUID ledger for step-level resume decisions.
124
+ */
125
+ verifyLeg1Entry(): Promise<boolean>;
126
+ /**
127
+ * Executes the 3-step Leg1 protocol for Category B activities
128
+ * (Leg1-only with children, e.g., Hook passthrough, Signal, Interrupt-another).
129
+ * Uses the incoming Leg1 message GUID as the GUID ledger key.
130
+ *
131
+ * Step A: setState + notarizeLeg1Completion + step1 markers (transaction 1)
132
+ * Step B: publish children + step2 markers + setStatusAndCollateGuid (transaction 2)
133
+ * Step C: if edge → runJobCompletionTasks + step3 markers + finalize (transaction 3)
134
+ *
135
+ * @returns true if this transition caused the job to complete
136
+ */
137
+ executeLeg1StepProtocol(delta: number): Promise<boolean>;
49
138
  mapJobData(): void;
50
139
  mapInputData(): void;
51
140
  mapOutputData(): void;
@@ -62,7 +151,7 @@ declare class Activity {
62
151
  getTriggerConfig(): Promise<ActivityType>;
63
152
  getJobStatus(): null | number;
64
153
  setStatus(amount: number, transaction?: ProviderTransaction): Promise<void | any>;
65
- authorizeEntry(state: StringAnyType): string[];
154
+ authorizeEntry(_state: StringAnyType): string[];
66
155
  bindDimensionalAddress(state: StringAnyType): void;
67
156
  setState(transaction?: ProviderTransaction): Promise<string>;
68
157
  bindJobMetadata(): void;
@@ -94,7 +183,6 @@ declare class Activity {
94
183
  * @private
95
184
  */
96
185
  shouldPersistJob(): boolean;
97
- transition(adjacencyList: StreamData[], jobStatus: JobStatus): Promise<string[]>;
98
186
  /**
99
187
  * A job with a vale < -100_000_000 is considered interrupted,
100
188
  * as the interruption event decrements the job status by 1billion.
@@ -11,13 +11,69 @@ const serializer_1 = require("../serializer");
11
11
  const telemetry_1 = require("../telemetry");
12
12
  const stream_1 = require("../../types/stream");
13
13
  /**
14
- * The base class for all activities
14
+ * Base class for all HotMesh activity types. Activities are the execution
15
+ * units within a YAML-defined workflow graph. Each activity represents a
16
+ * node in a Directed Acyclic Graph (DAG) that the engine orchestrates.
17
+ *
18
+ * ## Activity Categories
19
+ *
20
+ * Activities fall into three execution categories:
21
+ *
22
+ * - **Category A (Duplex)**: Two-phase activities with Leg 1 (dispatch) and
23
+ * Leg 2 (response). Used by `Worker` and `Await`. Leg 1
24
+ * publishes a message and waits; Leg 2 handles the response via
25
+ * `processEvent` and transitions to adjacent activities.
26
+ *
27
+ * - **Category B (Leg1-only with children)**: Single-phase activities that
28
+ * execute work and transition to children using the crash-safe
29
+ * `executeLeg1StepProtocol`. Used by `Hook` (passthrough mode),
30
+ * `Signal`, and `Interrupt` (target mode).
31
+ *
32
+ * - **Category C (Leg1-only, no children)**: Terminal activities that
33
+ * execute without spawning children. Used by `Interrupt` (self mode).
34
+ *
35
+ * ## Shared YAML Configuration
36
+ *
37
+ * All activity types support these base properties in the YAML descriptor:
38
+ *
39
+ * | Property | Type | Description |
40
+ * |----------------------|---------|-------------|
41
+ * | `type` | string | Activity type: `trigger`, `worker`, `await`, `hook`, `signal`, `interrupt`, `cycle` |
42
+ * | `title` | string | Human-readable label for the activity |
43
+ * | `input.schema` | object | JSON Schema for input validation |
44
+ * | `input.maps` | object | Maps data from other activities into this activity's input |
45
+ * | `output.schema` | object | JSON Schema for output validation |
46
+ * | `output.maps` | object | Maps/transforms the activity's own output data |
47
+ * | `job.maps` | object | Maps activity data to the shared job state |
48
+ * | `emit` | boolean | If `true`, emits a message to the graph's `publishes` topic |
49
+ * | `persist` | boolean | If `true`, emits the job-completed event while keeping the job active |
50
+ * | `expire` | number | Seconds until the job expires after completion (`-1` = forever) |
51
+ * | `statusThreshold` | number | Custom semaphore threshold for Dynamic Activation Control |
52
+ * | `cycle` | boolean | If `true`, leaves Leg 2 open so the activity can be re-entered |
53
+ *
54
+ * ## Data Mapping Syntax
55
+ *
56
+ * Mapping expressions use curly-brace references to bind data between
57
+ * activities and the shared job state:
58
+ *
59
+ * ```yaml
60
+ * input:
61
+ * maps:
62
+ * x: '{t1.output.data.fieldName}' # reference another activity's output
63
+ * y: '{$self.output.data.fieldName}' # reference own output
64
+ * z: '{$job.data.fieldName}' # reference shared job state
65
+ * s: '{$app.settings.configKey}' # reference app-level settings
66
+ * ```
67
+ *
68
+ * @see {@link https://hotmeshio.github.io/sdk-typescript/docs/quickstart | Quick Start Guide}
69
+ * @see [Model Driven Development](https://hotmeshio.github.io/sdk-typescript/docs/model_driven_development)
15
70
  */
16
71
  class Activity {
17
72
  constructor(config, data, metadata, hook, engine, context) {
18
73
  this.status = stream_1.StreamStatus.SUCCESS;
19
74
  this.code = 200;
20
75
  this.adjacentIndex = 0;
76
+ this.guidLedger = 0;
21
77
  this.config = config;
22
78
  this.data = data;
23
79
  this.metadata = metadata;
@@ -57,11 +113,14 @@ class Activity {
57
113
  catch (error) {
58
114
  await collator_1.CollatorService.notarizeEntry(this);
59
115
  if (threshold > 0) {
60
- if (this.context.metadata.js === threshold) {
61
- //conclude job EXACTLY ONCE
116
+ if (this.context.metadata.js <= threshold) {
117
+ //Dynamic Activation Control: convergent claim — only the
118
+ //activity whose HINCRBY reaches exactly 0 runs completion.
62
119
  const status = await this.setStatus(-threshold);
63
120
  if (Number(status) === 0) {
64
- await this.engine.runJobCompletionTasks(this.context);
121
+ const txn = this.store.transact();
122
+ await this.engine.runJobCompletionTasks(this.context, {}, txn);
123
+ await txn.exec();
65
124
  }
66
125
  }
67
126
  }
@@ -73,14 +132,19 @@ class Activity {
73
132
  await collator_1.CollatorService.notarizeEntry(this);
74
133
  }
75
134
  /**
76
- * Upon entering leg 2 of a duplexed activity
135
+ * Upon entering leg 2 of a duplexed activity.
136
+ * Increments both the activity ledger (+1) and GUID ledger (+1).
137
+ * Stores the GUID ledger value for step-level resume decisions.
77
138
  */
78
139
  async verifyReentry() {
79
- const guid = this.context.metadata.guid;
140
+ const msgGuid = this.context.metadata.guid;
80
141
  this.setLeg(2);
81
142
  await this.getState();
143
+ this.context.metadata.guid = msgGuid;
82
144
  collator_1.CollatorService.assertJobActive(this.context.metadata.js, this.context.metadata.jid, this.metadata.aid);
83
- return await collator_1.CollatorService.notarizeReentry(this, guid);
145
+ const [activityLedger, guidLedger] = await collator_1.CollatorService.notarizeLeg2Entry(this, msgGuid);
146
+ this.guidLedger = guidLedger;
147
+ return activityLedger;
84
148
  }
85
149
  //******** DUPLEX RE-ENTRY POINT ********//
86
150
  async processEvent(status = stream_1.StreamStatus.SUCCESS, code = 200, type = 'output') {
@@ -108,17 +172,43 @@ class Activity {
108
172
  this.adjacentIndex = collator_1.CollatorService.getDimensionalIndex(collationKey);
109
173
  telemetry = new telemetry_1.TelemetryService(this.engine.appId, this.config, this.metadata, this.context);
110
174
  telemetry.startActivitySpan(this.leg);
111
- let multiResponse;
112
- if (status === stream_1.StreamStatus.PENDING) {
113
- multiResponse = await this.processPending(type);
114
- }
115
- else if (status === stream_1.StreamStatus.SUCCESS) {
116
- multiResponse = await this.processSuccess(type);
175
+ //bind data per status type
176
+ if (status === stream_1.StreamStatus.ERROR) {
177
+ this.bindActivityError(this.data);
178
+ this.adjacencyList = await this.filterAdjacent();
179
+ if (!this.adjacencyList.length) {
180
+ this.bindJobError(this.data);
181
+ }
117
182
  }
118
183
  else {
119
- multiResponse = await this.processError();
184
+ this.bindActivityData(type);
185
+ this.adjacencyList = await this.filterAdjacent();
120
186
  }
121
- this.transitionAdjacent(multiResponse, telemetry);
187
+ this.mapJobData();
188
+ //When an unrecoverable error has no matching transitions
189
+ //(e.g., code 500 from raw errors after retries exhausted),
190
+ //mark the job as terminally errored so the step protocol
191
+ //can force completion via the isErrorTerminal path.
192
+ if (status === stream_1.StreamStatus.ERROR && !this.adjacencyList?.length) {
193
+ if (!this.context.data)
194
+ this.context.data = {};
195
+ this.context.data.done = true;
196
+ this.context.data.$error = {
197
+ message: this.data?.message || 'unknown error',
198
+ code: enums_1.HMSH_CODE_MEMFLOW_MAXED,
199
+ stack: this.data?.stack,
200
+ };
201
+ }
202
+ //determine step parameters
203
+ const delta = status === stream_1.StreamStatus.PENDING
204
+ ? this.adjacencyList.length
205
+ : this.adjacencyList.length - 1;
206
+ const shouldFinalize = status !== stream_1.StreamStatus.PENDING;
207
+ //execute 3-step protocol
208
+ const thresholdHit = await this.executeStepProtocol(delta, shouldFinalize);
209
+ //telemetry
210
+ telemetry.mapActivityAttributes();
211
+ telemetry.setActivityAttributes({});
122
212
  }
123
213
  catch (error) {
124
214
  if (error instanceof errors_1.CollationError) {
@@ -151,50 +241,100 @@ class Activity {
151
241
  this.logger.debug('activity-process-event-end', { jid, aid });
152
242
  }
153
243
  }
154
- async processPending(type) {
155
- this.bindActivityData(type);
156
- this.adjacencyList = await this.filterAdjacent();
157
- this.mapJobData();
158
- const transaction = this.store.transact();
159
- await this.setState(transaction);
160
- await collator_1.CollatorService.notarizeContinuation(this, transaction);
161
- await this.setStatus(this.adjacencyList.length, transaction);
162
- return (await transaction.exec());
163
- }
164
- async processSuccess(type) {
165
- this.bindActivityData(type);
166
- this.adjacencyList = await this.filterAdjacent();
167
- this.mapJobData();
168
- const transaction = this.store.transact();
169
- await this.setState(transaction);
170
- await collator_1.CollatorService.notarizeCompletion(this, transaction);
171
- await this.setStatus(this.adjacencyList.length - 1, transaction);
172
- return (await transaction.exec());
173
- }
174
- async processError() {
175
- this.bindActivityError(this.data);
176
- this.adjacencyList = await this.filterAdjacent();
177
- if (!this.adjacencyList.length) {
178
- this.bindJobError(this.data);
179
- }
180
- this.mapJobData();
181
- const transaction = this.store.transact();
182
- await this.setState(transaction);
183
- await collator_1.CollatorService.notarizeCompletion(this, transaction);
184
- await this.setStatus(this.adjacencyList.length - 1, transaction);
185
- return (await transaction.exec());
186
- }
187
- async transitionAdjacent(multiResponse, telemetry) {
188
- telemetry.mapActivityAttributes();
189
- const jobStatus = this.resolveStatus(multiResponse);
190
- const attrs = { 'app.job.jss': jobStatus };
191
- //adjacencyList membership has already been set at this point (according to activity status)
192
- const messageIds = await this.transition(this.adjacencyList, jobStatus);
193
- if (messageIds?.length) {
194
- attrs['app.activity.mids'] = messageIds.join(',');
195
- }
196
- telemetry.setActivityAttributes(attrs);
244
+ /**
245
+ * Executes the 3-step Leg2 protocol using GUID ledger for
246
+ * crash-safe resume. Each step bundles durable writes with
247
+ * its concluding digit update in a single transaction.
248
+ *
249
+ * @returns true if this transition caused the job to complete
250
+ */
251
+ async executeStepProtocol(delta, shouldFinalize) {
252
+ const msgGuid = this.context.metadata.guid;
253
+ const threshold = this.mapStatusThreshold();
254
+ const { id: appId } = await this.engine.getVID();
255
+ //Step 1: Save work (skip if GUID 10B already set)
256
+ if (!collator_1.CollatorService.isGuidStep1Done(this.guidLedger)) {
257
+ const txn1 = this.store.transact();
258
+ await this.setState(txn1);
259
+ await collator_1.CollatorService.notarizeStep1(this, msgGuid, txn1);
260
+ await txn1.exec();
261
+ }
262
+ //Step 2: Spawn children + semaphore + edge capture (skip if GUID 1B already set)
263
+ let thresholdHit = false;
264
+ if (!collator_1.CollatorService.isGuidStep2Done(this.guidLedger)) {
265
+ const txn2 = this.store.transact();
266
+ //queue step markers first
267
+ await collator_1.CollatorService.notarizeStep2(this, msgGuid, txn2);
268
+ //queue child publications
269
+ for (const child of this.adjacencyList) {
270
+ await this.engine.router?.publishMessage(null, child, txn2);
271
+ }
272
+ //queue semaphore update + edge capture LAST (so result is at end)
273
+ await this.store.setStatusAndCollateGuid(delta, threshold, this.context.metadata.jid, appId, msgGuid, collator_1.CollatorService.WEIGHTS.GUID_SNAPSHOT, txn2);
274
+ const results = (await txn2.exec());
275
+ thresholdHit = this.resolveThresholdHit(results);
276
+ this.logger.debug('step-protocol-step2-complete', {
277
+ jid: this.context.metadata.jid,
278
+ aid: this.metadata.aid,
279
+ delta,
280
+ threshold,
281
+ thresholdHit,
282
+ lastResult: results[results.length - 1],
283
+ resultCount: results.length,
284
+ });
285
+ }
286
+ else {
287
+ //Step 2 already done; check GUID snapshot for edge
288
+ thresholdHit = collator_1.CollatorService.isGuidJobClosed(this.guidLedger);
289
+ }
290
+ //Step 3: Job completion tasks (edge hit OR emit/persist, skip if GUID 100M already set)
291
+ //When an activity marks the job done with an unrecoverable error
292
+ //(e.g., stopper after max retries), force completion even when the
293
+ //semaphore threshold isn't hit (the signaler's +1 contribution
294
+ //prevents threshold 0 from matching).
295
+ const isErrorTerminal = !thresholdHit
296
+ && this.context.data?.done === true
297
+ && !!this.context.data?.$error;
298
+ const needsCompletion = thresholdHit || this.shouldEmit() || this.shouldPersistJob() || isErrorTerminal;
299
+ if (needsCompletion && !collator_1.CollatorService.isGuidStep3Done(this.guidLedger)) {
300
+ const txn3 = this.store.transact();
301
+ const options = (thresholdHit || isErrorTerminal) ? {} : { emit: !this.shouldPersistJob() };
302
+ await this.engine.runJobCompletionTasks(this.context, options, txn3);
303
+ await collator_1.CollatorService.notarizeStep3(this, msgGuid, txn3);
304
+ const shouldFinalizeNow = (thresholdHit || isErrorTerminal) ? shouldFinalize : this.shouldPersistJob();
305
+ if (shouldFinalizeNow) {
306
+ await collator_1.CollatorService.notarizeFinalize(this, txn3);
307
+ }
308
+ await txn3.exec();
309
+ }
310
+ else if (needsCompletion) {
311
+ this.logger.debug('step-protocol-step3-skipped-already-done', {
312
+ jid: this.context.metadata.jid,
313
+ aid: this.metadata.aid,
314
+ });
315
+ }
316
+ else {
317
+ this.logger.debug('step-protocol-no-threshold', {
318
+ jid: this.context.metadata.jid,
319
+ aid: this.metadata.aid,
320
+ thresholdHit,
321
+ });
322
+ }
323
+ return thresholdHit;
324
+ }
325
+ /**
326
+ * Extracts the thresholdHit value from transaction results.
327
+ * The setStatusAndCollateGuid result is the last item.
328
+ */
329
+ resolveThresholdHit(results) {
330
+ const last = results[results.length - 1];
331
+ const value = Array.isArray(last) ? last[1] : last;
332
+ return Number(value) === 1;
197
333
  }
334
+ /**
335
+ * Extracts the job status from the last result of a transaction.
336
+ * Used by subclass Leg1 process methods for telemetry.
337
+ */
198
338
  resolveStatus(multiResponse) {
199
339
  const activityStatus = multiResponse[multiResponse.length - 1];
200
340
  if (Array.isArray(activityStatus)) {
@@ -204,6 +344,127 @@ class Activity {
204
344
  return Number(activityStatus);
205
345
  }
206
346
  }
347
+ /**
348
+ * Leg1 entry verification for Category B activities (Leg1-only with children).
349
+ * Returns true if this is a resume (Leg1 already completed on a prior attempt).
350
+ * On resume, loads the GUID ledger for step-level resume decisions.
351
+ */
352
+ async verifyLeg1Entry() {
353
+ const msgGuid = this.context.metadata.guid;
354
+ this.setLeg(1);
355
+ await this.getState();
356
+ this.context.metadata.guid = msgGuid;
357
+ const threshold = this.mapStatusThreshold();
358
+ try {
359
+ collator_1.CollatorService.assertJobActive(this.context.metadata.js, this.context.metadata.jid, this.metadata.aid, threshold);
360
+ }
361
+ catch (error) {
362
+ if (error instanceof errors_1.InactiveJobError && threshold > 0) {
363
+ //Dynamic Activation Control: threshold met, close the job
364
+ await collator_1.CollatorService.notarizeEntry(this);
365
+ if (this.context.metadata.js === threshold) {
366
+ //conclude job EXACTLY ONCE
367
+ const status = await this.setStatus(-threshold);
368
+ if (Number(status) === 0) {
369
+ await this.engine.runJobCompletionTasks(this.context);
370
+ }
371
+ }
372
+ }
373
+ throw error;
374
+ }
375
+ try {
376
+ await collator_1.CollatorService.notarizeEntry(this);
377
+ return false;
378
+ }
379
+ catch (error) {
380
+ if (error instanceof errors_1.CollationError && error.fault === 'duplicate') {
381
+ if (this.config.cycle) {
382
+ //Cycle re-entry: Leg1 already complete from prior iteration.
383
+ //Increment Leg2 counter to derive the new dimensional index,
384
+ //so children run in a fresh dimensional plane.
385
+ const [activityLedger, guidLedger] = await collator_1.CollatorService.notarizeLeg2Entry(this, msgGuid);
386
+ this.adjacentIndex =
387
+ collator_1.CollatorService.getDimensionalIndex(activityLedger);
388
+ this.guidLedger = guidLedger;
389
+ return false;
390
+ }
391
+ //100B is set — Leg1 work already committed. Load GUID for step resume.
392
+ const guidValue = await this.store.collateSynthetic(this.context.metadata.jid, msgGuid, 0);
393
+ this.guidLedger = guidValue;
394
+ return true;
395
+ }
396
+ throw error;
397
+ }
398
+ }
399
+ /**
400
+ * Executes the 3-step Leg1 protocol for Category B activities
401
+ * (Leg1-only with children, e.g., Hook passthrough, Signal, Interrupt-another).
402
+ * Uses the incoming Leg1 message GUID as the GUID ledger key.
403
+ *
404
+ * Step A: setState + notarizeLeg1Completion + step1 markers (transaction 1)
405
+ * Step B: publish children + step2 markers + setStatusAndCollateGuid (transaction 2)
406
+ * Step C: if edge → runJobCompletionTasks + step3 markers + finalize (transaction 3)
407
+ *
408
+ * @returns true if this transition caused the job to complete
409
+ */
410
+ async executeLeg1StepProtocol(delta) {
411
+ const msgGuid = this.context.metadata.guid;
412
+ const threshold = this.mapStatusThreshold();
413
+ const { id: appId } = await this.engine.getVID();
414
+ //Step A: Save work + Leg1 completion marker
415
+ if (!collator_1.CollatorService.isGuidStep1Done(this.guidLedger)) {
416
+ const txn1 = this.store.transact();
417
+ await this.setState(txn1);
418
+ if (this.adjacentIndex === 0) {
419
+ //First entry: mark Leg1 complete. On cycle re-entry
420
+ //(adjacentIndex > 0), Leg1 is already complete and the
421
+ //Leg2 counter was already incremented by notarizeLeg2Entry.
422
+ await collator_1.CollatorService.notarizeLeg1Completion(this, txn1);
423
+ }
424
+ await collator_1.CollatorService.notarizeStep1(this, msgGuid, txn1);
425
+ await txn1.exec();
426
+ }
427
+ //Step B: Spawn children + semaphore + edge capture
428
+ let thresholdHit = false;
429
+ if (!collator_1.CollatorService.isGuidStep2Done(this.guidLedger)) {
430
+ const txn2 = this.store.transact();
431
+ await collator_1.CollatorService.notarizeStep2(this, msgGuid, txn2);
432
+ for (const child of this.adjacencyList) {
433
+ await this.engine.router?.publishMessage(null, child, txn2);
434
+ }
435
+ await this.store.setStatusAndCollateGuid(delta, threshold, this.context.metadata.jid, appId, msgGuid, collator_1.CollatorService.WEIGHTS.GUID_SNAPSHOT, txn2);
436
+ const results = (await txn2.exec());
437
+ thresholdHit = this.resolveThresholdHit(results);
438
+ this.logger.debug('leg1-step-protocol-stepB-complete', {
439
+ jid: this.context.metadata.jid,
440
+ aid: this.metadata.aid,
441
+ delta,
442
+ threshold,
443
+ thresholdHit,
444
+ lastResult: results[results.length - 1],
445
+ });
446
+ }
447
+ else {
448
+ thresholdHit = collator_1.CollatorService.isGuidJobClosed(this.guidLedger);
449
+ }
450
+ //Step C: Job completion tasks (edge hit OR emit/persist)
451
+ //When an activity marks the job done with an unrecoverable error
452
+ //(e.g., stopper after max retries), force completion even when the
453
+ //semaphore threshold isn't hit.
454
+ const isErrorTerminal = !thresholdHit
455
+ && this.context.data?.done === true
456
+ && !!this.context.data?.$error;
457
+ const needsCompletion = thresholdHit || this.shouldEmit() || this.shouldPersistJob() || isErrorTerminal;
458
+ if (needsCompletion && !collator_1.CollatorService.isGuidStep3Done(this.guidLedger)) {
459
+ const txn3 = this.store.transact();
460
+ const options = (thresholdHit || isErrorTerminal) ? {} : { emit: !this.shouldPersistJob() };
461
+ await this.engine.runJobCompletionTasks(this.context, options, txn3);
462
+ await collator_1.CollatorService.notarizeStep3(this, msgGuid, txn3);
463
+ await collator_1.CollatorService.notarizeFinalize(this, txn3);
464
+ await txn3.exec();
465
+ }
466
+ return thresholdHit;
467
+ }
207
468
  mapJobData() {
208
469
  if (this.config.job?.maps) {
209
470
  const mapper = new mapper_1.MapperService((0, utils_1.deepCopy)(this.config.job.maps), this.context);
@@ -282,13 +543,10 @@ class Activity {
282
543
  const { id: appId } = await this.engine.getVID();
283
544
  return await this.store.setStatus(amount, this.context.metadata.jid, appId, transaction);
284
545
  }
285
- authorizeEntry(state) {
286
- //pre-authorize activity state to allow entry for adjacent activities
287
- return (this.adjacencyList?.map((streamData) => {
288
- const { metadata: { aid }, } = streamData;
289
- state[`${aid}/output/metadata/as`] = collator_1.CollatorService.getSeed();
290
- return aid;
291
- }) ?? []);
546
+ authorizeEntry(_state) {
547
+ //seed writes removed: child activities increment from 0 (null field).
548
+ //FINALIZE (200T) sets pos 1 directly to 2 without needing a 100T base.
549
+ return [];
292
550
  }
293
551
  bindDimensionalAddress(state) {
294
552
  const dad = this.resolveDad();
@@ -517,27 +775,6 @@ class Activity {
517
775
  }
518
776
  return false;
519
777
  }
520
- async transition(adjacencyList, jobStatus) {
521
- if (this.jobWasInterrupted(jobStatus)) {
522
- return;
523
- }
524
- let mIds = [];
525
- if (this.shouldEmit() ||
526
- this.isJobComplete(jobStatus) ||
527
- this.shouldPersistJob()) {
528
- await this.engine.runJobCompletionTasks(this.context, {
529
- emit: !this.isJobComplete(jobStatus) && !this.shouldPersistJob(),
530
- });
531
- }
532
- if (adjacencyList.length && !this.isJobComplete(jobStatus)) {
533
- const transaction = this.store.transact();
534
- for (const execSignal of adjacencyList) {
535
- await this.engine.router?.publishMessage(null, execSignal, transaction);
536
- }
537
- mIds = (await transaction.exec());
538
- }
539
- return mIds;
540
- }
541
778
  /**
542
779
  * A job with a vale < -100_000_000 is considered interrupted,
543
780
  * as the interruption event decrements the job status by 1billion.