@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.
- package/.claude/settings.local.json +8 -0
- package/README.md +158 -38
- package/build/index.d.ts +1 -3
- package/build/index.js +1 -5
- package/build/modules/utils.js +3 -31
- package/build/package.json +63 -79
- package/build/services/activities/activity.d.ts +97 -9
- package/build/services/activities/activity.js +323 -86
- package/build/services/activities/await.d.ts +101 -0
- package/build/services/activities/await.js +103 -2
- package/build/services/activities/cycle.d.ts +82 -0
- package/build/services/activities/cycle.js +86 -8
- package/build/services/activities/hook.d.ts +144 -1
- package/build/services/activities/hook.js +162 -21
- package/build/services/activities/interrupt.d.ts +112 -0
- package/build/services/activities/interrupt.js +134 -29
- package/build/services/activities/signal.d.ts +111 -4
- package/build/services/activities/signal.js +136 -28
- package/build/services/activities/trigger.d.ts +56 -4
- package/build/services/activities/trigger.js +119 -35
- package/build/services/activities/worker.d.ts +107 -0
- package/build/services/activities/worker.js +109 -2
- package/build/services/collator/index.d.ts +116 -30
- package/build/services/collator/index.js +211 -115
- package/build/services/connector/factory.d.ts +1 -1
- package/build/services/connector/factory.js +1 -11
- package/build/services/engine/index.d.ts +22 -6
- package/build/services/engine/index.js +49 -18
- package/build/services/exporter/index.d.ts +2 -0
- package/build/services/exporter/index.js +1 -0
- package/build/services/hotmesh/index.d.ts +471 -236
- package/build/services/hotmesh/index.js +473 -238
- package/build/services/memflow/client.js +2 -2
- package/build/services/memflow/handle.js +1 -1
- package/build/services/memflow/index.d.ts +1 -1
- package/build/services/memflow/index.js +1 -1
- package/build/services/memflow/workflow/all.d.ts +28 -3
- package/build/services/memflow/workflow/all.js +28 -3
- package/build/services/memflow/workflow/context.d.ts +44 -1
- package/build/services/memflow/workflow/context.js +44 -1
- package/build/services/memflow/workflow/didRun.d.ts +23 -3
- package/build/services/memflow/workflow/didRun.js +23 -3
- package/build/services/memflow/workflow/emit.d.ts +43 -4
- package/build/services/memflow/workflow/emit.js +43 -4
- package/build/services/memflow/workflow/enrich.d.ts +32 -4
- package/build/services/memflow/workflow/enrich.js +32 -4
- package/build/services/memflow/workflow/entityMethods.d.ts +54 -7
- package/build/services/memflow/workflow/entityMethods.js +54 -7
- package/build/services/memflow/workflow/execChild.d.ts +96 -8
- package/build/services/memflow/workflow/execChild.js +96 -8
- package/build/services/memflow/workflow/execHook.d.ts +54 -39
- package/build/services/memflow/workflow/execHook.js +52 -38
- package/build/services/memflow/workflow/execHookBatch.d.ts +82 -29
- package/build/services/memflow/workflow/execHookBatch.js +80 -28
- package/build/services/memflow/workflow/hook.d.ts +68 -3
- package/build/services/memflow/workflow/hook.js +69 -4
- package/build/services/memflow/workflow/index.d.ts +65 -10
- package/build/services/memflow/workflow/index.js +65 -10
- package/build/services/memflow/workflow/interrupt.d.ts +50 -4
- package/build/services/memflow/workflow/interrupt.js +50 -4
- package/build/services/memflow/workflow/interruption.d.ts +49 -16
- package/build/services/memflow/workflow/interruption.js +49 -16
- package/build/services/memflow/workflow/isSideEffectAllowed.d.ts +21 -4
- package/build/services/memflow/workflow/isSideEffectAllowed.js +21 -4
- package/build/services/memflow/workflow/proxyActivities.d.ts +70 -42
- package/build/services/memflow/workflow/proxyActivities.js +70 -42
- package/build/services/memflow/workflow/random.d.ts +33 -3
- package/build/services/memflow/workflow/random.js +33 -3
- package/build/services/memflow/workflow/searchMethods.d.ts +49 -2
- package/build/services/memflow/workflow/searchMethods.js +49 -2
- package/build/services/memflow/workflow/signal.d.ts +51 -22
- package/build/services/memflow/workflow/signal.js +52 -23
- package/build/services/memflow/workflow/sleepFor.d.ts +57 -18
- package/build/services/memflow/workflow/sleepFor.js +57 -18
- package/build/services/memflow/workflow/trace.d.ts +39 -6
- package/build/services/memflow/workflow/trace.js +39 -6
- package/build/services/memflow/workflow/waitFor.d.ts +55 -18
- package/build/services/memflow/workflow/waitFor.js +55 -18
- 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 +6 -1
- 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 +21 -1
- package/build/services/store/providers/postgres/postgres.js +42 -4
- 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/services/telemetry/index.js +6 -0
- package/build/types/activity.d.ts +1 -1
- package/build/types/hotmesh.d.ts +1 -1
- package/build/types/index.d.ts +0 -1
- package/build/types/index.js +1 -4
- package/build/types/job.d.ts +1 -1
- package/build/types/memflow.d.ts +1 -1
- package/build/types/provider.d.ts +1 -1
- package/build/types/quorum.d.ts +2 -2
- package/build/vitest.config.d.ts +2 -0
- package/build/vitest.config.js +18 -0
- package/index.ts +0 -4
- package/package.json +63 -79
- package/vitest.config.ts +17 -0
- 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
|
@@ -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
|
-
*
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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(
|
|
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
|
-
*
|
|
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
|
|
61
|
-
//
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
112
|
-
if (status === stream_1.StreamStatus.
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
184
|
+
this.bindActivityData(type);
|
|
185
|
+
this.adjacencyList = await this.filterAdjacent();
|
|
120
186
|
}
|
|
121
|
-
this.
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
286
|
-
//
|
|
287
|
-
|
|
288
|
-
|
|
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.
|