@hotmeshio/hotmesh 0.0.3 → 0.0.4
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/README.md +1 -1
- package/build/package.json +1 -1
- package/build/services/activities/activity.d.ts +2 -0
- package/build/services/activities/activity.js +27 -9
- package/build/services/activities/worker.js +1 -0
- package/build/services/collator/index.d.ts +7 -5
- package/build/services/collator/index.js +29 -6
- package/build/services/compiler/deployer.d.ts +4 -3
- package/build/services/compiler/deployer.js +14 -0
- package/build/services/dimension/index.d.ts +1 -1
- package/build/services/durable/handle.d.ts +1 -1
- package/build/services/engine/index.d.ts +1 -1
- package/build/services/engine/index.js +22 -5
- package/build/services/hotmesh/index.d.ts +1 -1
- package/build/services/hotmesh/index.js +2 -2
- package/build/services/serializer/index.d.ts +6 -1
- package/build/services/serializer/index.js +55 -22
- package/build/services/signaler/stream.js +1 -0
- package/build/services/store/index.d.ts +3 -3
- package/build/services/store/index.js +10 -23
- package/build/types/activity.d.ts +1 -0
- package/package.json +1 -1
- package/services/activities/activity.ts +28 -9
- package/services/activities/worker.ts +1 -0
- package/services/collator/index.ts +38 -13
- package/services/compiler/deployer.ts +24 -9
- package/services/dimension/index.ts +1 -1
- package/services/durable/handle.ts +2 -2
- package/services/engine/index.ts +23 -5
- package/services/hotmesh/index.ts +2 -2
- package/services/mapper/index.ts +0 -1
- package/services/serializer/index.ts +57 -22
- package/services/signaler/stream.ts +1 -0
- package/services/store/index.ts +10 -22
- package/types/activity.ts +1 -0
package/README.md
CHANGED
|
@@ -98,7 +98,7 @@ run().catch((err) => {
|
|
|
98
98
|
});
|
|
99
99
|
```
|
|
100
100
|
|
|
101
|
-
>HotMesh delivers durable function execution using a
|
|
101
|
+
>HotMesh delivers durable function execution using a [distributed service mesh](https://github.com/hotmeshio/sdk-typescript/blob/main/docs/distributed_orchestration.md). The design consumes leftover CPU on your microservices to execute workflows without the cost and complexity of a central server/control plane.
|
|
102
102
|
|
|
103
103
|
## Advanced Design
|
|
104
104
|
HotMesh's TypeScript SDK is the easiest way to make your functions durable. But if you need full control over your function lifecycles (including high-volume, high-speed use cases), you can use HotMesh's underlying YAML models to optimize your durable workflows. The following model depicts a sequence of activities orchestrated by HotMesh. Any function you associate with a `topic` in your YAML definition is guaranteed to be durable.
|
package/build/package.json
CHANGED
|
@@ -53,6 +53,8 @@ declare class Activity {
|
|
|
53
53
|
initSelf(context: StringAnyType): JobState;
|
|
54
54
|
initPolicies(context: JobState): void;
|
|
55
55
|
bindActivityData(type: 'output' | 'hook'): void;
|
|
56
|
+
resolveDad(): string;
|
|
57
|
+
resolveAdjacentDad(): string;
|
|
56
58
|
filterAdjacent(): Promise<StreamData[]>;
|
|
57
59
|
transition(adjacencyList: StreamData[], jobStatus: JobStatus): Promise<string[]>;
|
|
58
60
|
}
|
|
@@ -100,7 +100,8 @@ class Activity {
|
|
|
100
100
|
const durationInSeconds = pipe_1.Pipe.resolve(this.config.sleep, this.context);
|
|
101
101
|
const jobId = this.context.metadata.jid;
|
|
102
102
|
const activityId = this.metadata.aid;
|
|
103
|
-
|
|
103
|
+
const dId = this.metadata.dad;
|
|
104
|
+
await this.engine.task.registerTimeHook(jobId, `${activityId}${dId || ''}`, 'sleep', durationInSeconds);
|
|
104
105
|
return jobId;
|
|
105
106
|
}
|
|
106
107
|
}
|
|
@@ -158,6 +159,7 @@ class Activity {
|
|
|
158
159
|
return jobStatus;
|
|
159
160
|
}
|
|
160
161
|
catch (error) {
|
|
162
|
+
console.error('this error?', error);
|
|
161
163
|
this.logger.error('engine-process-hook-event-error', error);
|
|
162
164
|
telemetry.setActivityError(error.message);
|
|
163
165
|
throw error;
|
|
@@ -214,11 +216,10 @@ class Activity {
|
|
|
214
216
|
}) ?? [];
|
|
215
217
|
}
|
|
216
218
|
bindDimensionalAddress(state) {
|
|
217
|
-
const
|
|
218
|
-
state[`${aid}/output/metadata/dad`] = dad;
|
|
219
|
+
const dad = this.resolveDad();
|
|
220
|
+
state[`${this.metadata.aid}/output/metadata/dad`] = dad;
|
|
219
221
|
}
|
|
220
222
|
async setState(multi) {
|
|
221
|
-
const { id: appId } = await this.engine.getVID();
|
|
222
223
|
const jobId = this.context.metadata.jid;
|
|
223
224
|
this.bindJobMetadata();
|
|
224
225
|
this.bindActivityMetadata();
|
|
@@ -233,7 +234,8 @@ class Activity {
|
|
|
233
234
|
this.metadata.aid,
|
|
234
235
|
...presets
|
|
235
236
|
];
|
|
236
|
-
|
|
237
|
+
const dIds = collator_1.CollatorService.getDimensionsById([...this.config.ancestors, this.metadata.aid], this.resolveDad());
|
|
238
|
+
return await this.store.setState(state, this.getJobStatus(), jobId, symbolNames, dIds, multi);
|
|
237
239
|
}
|
|
238
240
|
bindJobMetadata() {
|
|
239
241
|
//both legs of the most recently run activity (1 and 2) modify ju (job_updated)
|
|
@@ -247,6 +249,7 @@ class Activity {
|
|
|
247
249
|
if (this.status === stream_1.StreamStatus.ERROR) {
|
|
248
250
|
self.output.metadata.err = JSON.stringify(this.data);
|
|
249
251
|
}
|
|
252
|
+
//todo: verify leg2 never overwrites leg1 `ac`
|
|
250
253
|
self.output.metadata.ac =
|
|
251
254
|
self.output.metadata.au = (0, utils_1.formatISODate)(new Date());
|
|
252
255
|
self.output.metadata.atp = this.config.type;
|
|
@@ -315,10 +318,11 @@ class Activity {
|
|
|
315
318
|
}
|
|
316
319
|
}
|
|
317
320
|
telemetry_1.TelemetryService.addTargetTelemetryPaths(consumes, this.config, this.metadata, this.leg);
|
|
318
|
-
|
|
321
|
+
let { dad, jid } = this.context.metadata;
|
|
319
322
|
jobId = jobId || jid;
|
|
320
323
|
//`state` is a flat hash
|
|
321
|
-
const
|
|
324
|
+
const dIds = collator_1.CollatorService.getDimensionsById([...this.config.ancestors, this.metadata.aid], dad);
|
|
325
|
+
const [state, status] = await this.store.getState(jobId, consumes, dIds);
|
|
322
326
|
//`context` is a tree
|
|
323
327
|
this.context = (0, utils_1.restoreHierarchy)(state);
|
|
324
328
|
this.initDimensionalAddress(dad);
|
|
@@ -353,11 +357,25 @@ class Activity {
|
|
|
353
357
|
bindActivityData(type) {
|
|
354
358
|
this.context[this.metadata.aid][type].data = this.data;
|
|
355
359
|
}
|
|
360
|
+
resolveDad() {
|
|
361
|
+
let dad = this.metadata.dad;
|
|
362
|
+
if (this.adjacentIndex > 0) {
|
|
363
|
+
//if adjacent index > 0 the activity is cycling; replace last index with cycle index
|
|
364
|
+
dad = `${dad.substring(0, dad.lastIndexOf(','))},${this.adjacentIndex}`;
|
|
365
|
+
}
|
|
366
|
+
return dad;
|
|
367
|
+
}
|
|
368
|
+
resolveAdjacentDad() {
|
|
369
|
+
//concat self and child dimension (all children (leg 1) begin life at 0)
|
|
370
|
+
return `${this.resolveDad()}${dimension_1.DimensionService.getSeed(0)}`;
|
|
371
|
+
}
|
|
372
|
+
;
|
|
356
373
|
async filterAdjacent() {
|
|
357
374
|
const adjacencyList = [];
|
|
358
375
|
const transitions = await this.store.getTransitions(await this.engine.getVID());
|
|
359
376
|
const transition = transitions[`.${this.metadata.aid}`];
|
|
360
|
-
|
|
377
|
+
//resolve the dimensional address for adjacent children
|
|
378
|
+
const adjacentDad = this.resolveAdjacentDad();
|
|
361
379
|
if (transition) {
|
|
362
380
|
for (const toActivityId in transition) {
|
|
363
381
|
const transitionRule = transition[toActivityId];
|
|
@@ -365,7 +383,7 @@ class Activity {
|
|
|
365
383
|
adjacencyList.push({
|
|
366
384
|
metadata: {
|
|
367
385
|
jid: this.context.metadata.jid,
|
|
368
|
-
dad:
|
|
386
|
+
dad: adjacentDad,
|
|
369
387
|
aid: toActivityId,
|
|
370
388
|
spn: this.context['$self'].output.metadata?.l2s,
|
|
371
389
|
trc: this.context.metadata.trc,
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { RedisMulti } from
|
|
2
|
-
import { CollationStage } from
|
|
3
|
-
import { ActivityDuplex } from
|
|
4
|
-
import { HotMeshGraph } from
|
|
5
|
-
import { Activity } from
|
|
1
|
+
import { RedisMulti } from '../../types/redis';
|
|
2
|
+
import { CollationStage } from '../../types/collator';
|
|
3
|
+
import { ActivityDuplex } from '../../types/activity';
|
|
4
|
+
import { HotMeshGraph } from '../../types/hotmesh';
|
|
5
|
+
import { Activity } from '../activities/activity';
|
|
6
6
|
declare class CollatorService {
|
|
7
7
|
static targetLength: number;
|
|
8
|
+
static getDimensionalAddress(activity: Activity): Record<string, string>;
|
|
8
9
|
static notarizeEntry(activity: Activity, multi?: RedisMulti): Promise<number>;
|
|
9
10
|
static authorizeReentry(activity: Activity, multi?: RedisMulti): Promise<number>;
|
|
10
11
|
static notarizeEarlyCompletion(activity: Activity, multi?: RedisMulti): Promise<number>;
|
|
@@ -17,6 +18,7 @@ declare class CollatorService {
|
|
|
17
18
|
static isInactive(num: number): boolean;
|
|
18
19
|
static isPrimed(amount: number, leg: ActivityDuplex): boolean;
|
|
19
20
|
static verifyInteger(amount: number, leg: ActivityDuplex, stage: CollationStage): void;
|
|
21
|
+
static getDimensionsById(ancestors: string[], dad: string): Record<string, string>;
|
|
20
22
|
/**
|
|
21
23
|
* All non-trigger activities are assigned a status seed by their parent
|
|
22
24
|
*/
|
|
@@ -4,9 +4,18 @@ exports.CollatorService = void 0;
|
|
|
4
4
|
const errors_1 = require("../../modules/errors");
|
|
5
5
|
const collator_1 = require("../../types/collator");
|
|
6
6
|
class CollatorService {
|
|
7
|
+
static getDimensionalAddress(activity) {
|
|
8
|
+
let dad = activity.context.metadata.dad || activity.metadata.dad;
|
|
9
|
+
//todo: unsure about this reset
|
|
10
|
+
// if (dad && activity.leg === 2) {
|
|
11
|
+
// console.log('setting dad index back to 0=>', dad);
|
|
12
|
+
// dad = `${dad.substring(0, dad.lastIndexOf(','))},0`;
|
|
13
|
+
// }
|
|
14
|
+
return CollatorService.getDimensionsById([...activity.config.ancestors, activity.metadata.aid], dad);
|
|
15
|
+
}
|
|
7
16
|
static async notarizeEntry(activity, multi) {
|
|
8
17
|
//decrement by -100_000_000_000_000
|
|
9
|
-
const amount = await activity.store.collate(activity.context.metadata.jid, activity.metadata.aid, -100000000000000, multi);
|
|
18
|
+
const amount = await activity.store.collate(activity.context.metadata.jid, activity.metadata.aid, -100000000000000, this.getDimensionalAddress(activity), multi);
|
|
10
19
|
this.verifyInteger(amount, 1, 'enter');
|
|
11
20
|
return amount;
|
|
12
21
|
}
|
|
@@ -14,30 +23,30 @@ class CollatorService {
|
|
|
14
23
|
static async authorizeReentry(activity, multi) {
|
|
15
24
|
//set second digit to 8, allowing for re-entry
|
|
16
25
|
//decrement by -10_000_000_000_000
|
|
17
|
-
const amount = await activity.store.collate(activity.context.metadata.jid, activity.metadata.aid, -10000000000000, multi);
|
|
26
|
+
const amount = await activity.store.collate(activity.context.metadata.jid, activity.metadata.aid, -10000000000000, this.getDimensionalAddress(activity), multi);
|
|
18
27
|
//this.verifyInteger(amount, 1, 'exit');
|
|
19
28
|
return amount;
|
|
20
29
|
}
|
|
21
30
|
static async notarizeEarlyCompletion(activity, multi) {
|
|
22
31
|
//initialize both `possible` (1m) and `actualized` (1) zero dimension, while decrementing the 2nd and 3rd digits to deactivate the activity
|
|
23
|
-
return await activity.store.collate(activity.context.metadata.jid, activity.metadata.aid, 1000001 - 11000000000000, multi);
|
|
32
|
+
return await activity.store.collate(activity.context.metadata.jid, activity.metadata.aid, 1000001 - 11000000000000, this.getDimensionalAddress(activity), multi);
|
|
24
33
|
}
|
|
25
34
|
;
|
|
26
35
|
static async notarizeReentry(activity, multi) {
|
|
27
36
|
//increment by 1_000_000 (indicates re-entry and is used to drive the 'dimensional address' for adjacent activities (minus 1))
|
|
28
|
-
const amount = await activity.store.collate(activity.context.metadata.jid, activity.metadata.aid, 1000000, multi);
|
|
37
|
+
const amount = await activity.store.collate(activity.context.metadata.jid, activity.metadata.aid, 1000000, this.getDimensionalAddress(activity), multi);
|
|
29
38
|
this.verifyInteger(amount, 2, 'enter');
|
|
30
39
|
return amount;
|
|
31
40
|
}
|
|
32
41
|
;
|
|
33
42
|
static async notarizeContinuation(activity, multi) {
|
|
34
43
|
//keep open; actualize the leg2 dimension (+1)
|
|
35
|
-
return await activity.store.collate(activity.context.metadata.jid, activity.metadata.aid, 1, multi);
|
|
44
|
+
return await activity.store.collate(activity.context.metadata.jid, activity.metadata.aid, 1, this.getDimensionalAddress(activity), multi);
|
|
36
45
|
}
|
|
37
46
|
;
|
|
38
47
|
static async notarizeCompletion(activity, multi) {
|
|
39
48
|
//close out; actualize leg2 dimension (+1) and decrement the 3rd digit (-1_000_000_000_000)
|
|
40
|
-
return await activity.store.collate(activity.context.metadata.jid, activity.metadata.aid, 1 - 1000000000000, multi);
|
|
49
|
+
return await activity.store.collate(activity.context.metadata.jid, activity.metadata.aid, 1 - 1000000000000, this.getDimensionalAddress(activity), multi);
|
|
41
50
|
}
|
|
42
51
|
;
|
|
43
52
|
static getDigitAtIndex(num, targetDigitIndex) {
|
|
@@ -106,6 +115,20 @@ class CollatorService {
|
|
|
106
115
|
throw new errors_1.CollationError(amount, leg, stage, faultType);
|
|
107
116
|
}
|
|
108
117
|
}
|
|
118
|
+
static getDimensionsById(ancestors, dad) {
|
|
119
|
+
//ancestors is an ordered list of all ancestors, starting with the trigger (['t1', 'a1', 'a2'])
|
|
120
|
+
//dad is the dimensional address of the ancestors list (',0,5,3')
|
|
121
|
+
//loop through the ancestors list and create a map of the ancestor to the dimensional address.
|
|
122
|
+
//return { 't1': ',0', 'a1': ',0,5', 'a1': ',0,5,3', $ADJACENT: ',0,5,3,0' };
|
|
123
|
+
// `adjacent` is a special key that is used to track the dimensional address of adjacent activities
|
|
124
|
+
const map = { '$ADJACENT': `${dad},0` };
|
|
125
|
+
let dadStr = dad;
|
|
126
|
+
ancestors.reverse().forEach((ancestor) => {
|
|
127
|
+
map[ancestor] = dadStr;
|
|
128
|
+
dadStr = dadStr.substring(0, dadStr.lastIndexOf(','));
|
|
129
|
+
});
|
|
130
|
+
return map;
|
|
131
|
+
}
|
|
109
132
|
/**
|
|
110
133
|
* All non-trigger activities are assigned a status seed by their parent
|
|
111
134
|
*/
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { StoreService } from '../store';
|
|
2
|
-
import { HotMeshGraph, HotMeshManifest } from
|
|
3
|
-
import { RedisClient, RedisMulti } from
|
|
4
|
-
import { Symbols } from
|
|
2
|
+
import { HotMeshGraph, HotMeshManifest } from '../../types/hotmesh';
|
|
3
|
+
import { RedisClient, RedisMulti } from '../../types/redis';
|
|
4
|
+
import { Symbols } from '../../types/serializer';
|
|
5
5
|
declare class Deployer {
|
|
6
6
|
manifest: HotMeshManifest | null;
|
|
7
7
|
store: StoreService<RedisClient, RedisMulti> | null;
|
|
@@ -12,6 +12,7 @@ declare class Deployer {
|
|
|
12
12
|
version: string;
|
|
13
13
|
};
|
|
14
14
|
generateSymKeys(): Promise<void>;
|
|
15
|
+
bindSelf(consumes: Record<string, string[]>, produces: string[], activityId: string): void;
|
|
15
16
|
bindSymbols(startIndex: number, maxIndex: number, existingSymbols: Symbols, prefix: string, produces: string[]): Symbols;
|
|
16
17
|
copyJobSchemas(): void;
|
|
17
18
|
bindBackRefs(): void;
|
|
@@ -53,6 +53,7 @@ class Deployer {
|
|
|
53
53
|
for (const [activityId, activity] of Object.entries(graph.activities)) {
|
|
54
54
|
const [lower, upper, symbols] = await this.store.reserveSymbolRange(activityId, DEFAULT_RANGE_SIZE, 'ACTIVITY');
|
|
55
55
|
const prefix = `${activityId}/`; //activity meta/data is namespaced
|
|
56
|
+
this.bindSelf(activity.consumes, activity.produces, activityId);
|
|
56
57
|
const newSymbols = this.bindSymbols(lower, upper, symbols, prefix, activity.produces);
|
|
57
58
|
if (Object.keys(newSymbols).length) {
|
|
58
59
|
await this.store.addSymbols(activityId, newSymbols);
|
|
@@ -60,6 +61,19 @@ class Deployer {
|
|
|
60
61
|
}
|
|
61
62
|
}
|
|
62
63
|
}
|
|
64
|
+
bindSelf(consumes, produces, activityId) {
|
|
65
|
+
//bind self-referential mappings
|
|
66
|
+
for (const selfId of [activityId, '$self']) {
|
|
67
|
+
const selfConsumes = consumes[selfId];
|
|
68
|
+
if (selfConsumes) {
|
|
69
|
+
for (const path of selfConsumes) {
|
|
70
|
+
if (!produces.includes(path)) {
|
|
71
|
+
produces.push(path);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
63
77
|
bindSymbols(startIndex, maxIndex, existingSymbols, prefix, produces) {
|
|
64
78
|
let newSymbols = {};
|
|
65
79
|
let currentSymbols = { ...existingSymbols };
|
|
@@ -58,7 +58,7 @@ declare class EngineService {
|
|
|
58
58
|
hasParentJob(context: JobState): boolean;
|
|
59
59
|
resolveError(metadata: JobMetadata): StreamError | undefined;
|
|
60
60
|
scrub(jobId: string): Promise<void>;
|
|
61
|
-
hook(topic: string, data: JobData): Promise<JobStatus | void>;
|
|
61
|
+
hook(topic: string, data: JobData, dad?: string): Promise<JobStatus | void>;
|
|
62
62
|
hookTime(jobId: string, activityId: string): Promise<JobStatus | void>;
|
|
63
63
|
hookAll(hookTopic: string, data: JobData, query: JobStatsInput, queryFacets?: string[]): Promise<string[]>;
|
|
64
64
|
pub(topic: string, data: JobData, context?: JobState): Promise<string>;
|
|
@@ -317,21 +317,37 @@ class EngineService {
|
|
|
317
317
|
await this.store.scrub(jobId);
|
|
318
318
|
}
|
|
319
319
|
// ****************** `HOOK` ACTIVITY RE-ENTRY POINT *****************
|
|
320
|
-
async hook(topic, data) {
|
|
320
|
+
async hook(topic, data, dad) {
|
|
321
321
|
const hookRule = await this.storeSignaler.getHookRule(topic);
|
|
322
|
+
const [aid, schema] = await this.getSchema(`.${hookRule.to}`);
|
|
323
|
+
if (!dad) {
|
|
324
|
+
//assume dimensional address is singular (0)
|
|
325
|
+
// for ancestors and self if not provided
|
|
326
|
+
// todo: register
|
|
327
|
+
dad = ',0'.repeat(schema.ancestors.length + 1);
|
|
328
|
+
}
|
|
322
329
|
const streamData = {
|
|
323
330
|
type: stream_2.StreamDataType.WEBHOOK,
|
|
324
|
-
metadata: {
|
|
331
|
+
metadata: {
|
|
332
|
+
//jid is unknown at this point; will be resolved using the data
|
|
333
|
+
aid,
|
|
334
|
+
dad,
|
|
335
|
+
topic
|
|
336
|
+
},
|
|
325
337
|
data,
|
|
326
338
|
};
|
|
327
339
|
await this.streamSignaler.publishMessage(null, streamData);
|
|
328
340
|
}
|
|
329
341
|
async hookTime(jobId, activityId) {
|
|
342
|
+
//the activityid is concatenated with its dimensional address (dad); split to resolve
|
|
343
|
+
const [aid, ...dimensions] = activityId.split(',');
|
|
344
|
+
const dad = `,${dimensions.join(',')}`;
|
|
330
345
|
const streamData = {
|
|
331
346
|
type: stream_2.StreamDataType.TIMEHOOK,
|
|
332
347
|
metadata: {
|
|
333
348
|
jid: jobId,
|
|
334
|
-
aid
|
|
349
|
+
aid,
|
|
350
|
+
dad,
|
|
335
351
|
},
|
|
336
352
|
data: { timestamp: Date.now() },
|
|
337
353
|
};
|
|
@@ -484,12 +500,13 @@ class EngineService {
|
|
|
484
500
|
// (e.g, if {dimensions:true}, use hscan to deliver
|
|
485
501
|
// the full set of dimensional job data)
|
|
486
502
|
async getState(topic, jobId) {
|
|
487
|
-
const { id: appId } = await this.getVID();
|
|
488
503
|
const jobSymbols = await this.store.getSymbols(`$${topic}`);
|
|
489
504
|
const consumes = {
|
|
490
505
|
[`$${topic}`]: Object.keys(jobSymbols)
|
|
491
506
|
};
|
|
492
|
-
|
|
507
|
+
//job data exists at the 'zero' dimension; pass an empty object
|
|
508
|
+
const dIds = {};
|
|
509
|
+
const output = await this.store.getState(jobId, consumes, dIds);
|
|
493
510
|
if (!output) {
|
|
494
511
|
throw new Error(`not found ${jobId}`);
|
|
495
512
|
}
|
|
@@ -37,7 +37,7 @@ declare class HotMeshService {
|
|
|
37
37
|
getIds(topic: string, query: JobStatsInput, queryFacets?: any[]): Promise<IdsResponse>;
|
|
38
38
|
resolveQuery(topic: string, query: JobStatsInput): Promise<GetStatsOptions>;
|
|
39
39
|
scrub(jobId: string): Promise<void>;
|
|
40
|
-
hook(topic: string, data: JobData): Promise<JobStatus | void>;
|
|
40
|
+
hook(topic: string, data: JobData, dad?: string): Promise<JobStatus | void>;
|
|
41
41
|
hookAll(hookTopic: string, data: JobData, query: JobStatsInput, queryFacets?: string[]): Promise<string[]>;
|
|
42
42
|
stop(): Promise<void>;
|
|
43
43
|
compress(terms: string[]): Promise<boolean>;
|
|
@@ -116,9 +116,9 @@ class HotMeshService {
|
|
|
116
116
|
await this.engine?.scrub(jobId);
|
|
117
117
|
}
|
|
118
118
|
// ****** `HOOK` ACTIVITY RE-ENTRY POINT ******
|
|
119
|
-
async hook(topic, data) {
|
|
119
|
+
async hook(topic, data, dad) {
|
|
120
120
|
//return collation int
|
|
121
|
-
return await this.engine?.hook(topic, data);
|
|
121
|
+
return await this.engine?.hook(topic, data, dad);
|
|
122
122
|
}
|
|
123
123
|
async hookAll(hookTopic, data, query, queryFacets = []) {
|
|
124
124
|
return await this.engine?.hookAll(hookTopic, data, query, queryFacets);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Consumes } from '../../types/activity';
|
|
1
2
|
import { StringStringType, StringAnyType, SymbolMap, SymbolMaps, SymbolSets, Symbols } from '../../types/serializer';
|
|
2
3
|
export declare const MDATA_SYMBOLS: {
|
|
3
4
|
SLOTS: number;
|
|
@@ -15,12 +16,16 @@ export declare const MDATA_SYMBOLS: {
|
|
|
15
16
|
};
|
|
16
17
|
};
|
|
17
18
|
export declare class SerializerService {
|
|
19
|
+
dIds: StringStringType;
|
|
18
20
|
symKeys: SymbolMaps;
|
|
19
21
|
symReverseKeys: SymbolMaps;
|
|
20
22
|
symValMaps: SymbolMap;
|
|
21
23
|
symValReverseMaps: SymbolMap;
|
|
22
24
|
constructor();
|
|
23
|
-
|
|
25
|
+
abbreviate(consumes: Consumes, symbolNames: string[], fields?: string[]): string[];
|
|
26
|
+
resolveDimensionalIndex(path: string): string;
|
|
27
|
+
isJobPath(path: string): boolean;
|
|
28
|
+
resetSymbols(symKeys: SymbolSets, symVals: Symbols, dIds: StringStringType): void;
|
|
24
29
|
getReverseKeyMap(keyMap: SymbolMap, id?: string): SymbolMap;
|
|
25
30
|
getReverseValueMap(valueMap: SymbolMap): SymbolMap;
|
|
26
31
|
static filterSymVals(startIndex: number, maxIndex: number, existingSymbolValues: Symbols, proposedValues: Set<string>): Symbols;
|
|
@@ -20,9 +20,45 @@ exports.MDATA_SYMBOLS = {
|
|
|
20
20
|
};
|
|
21
21
|
class SerializerService {
|
|
22
22
|
constructor() {
|
|
23
|
-
this.resetSymbols({}, {});
|
|
23
|
+
this.resetSymbols({}, {}, {});
|
|
24
24
|
}
|
|
25
|
-
|
|
25
|
+
abbreviate(consumes, symbolNames, fields = []) {
|
|
26
|
+
for (const symbolName of symbolNames) {
|
|
27
|
+
const symbolSet = this.symKeys.get(symbolName);
|
|
28
|
+
const symbolPaths = consumes[symbolName];
|
|
29
|
+
for (const symbolPath of symbolPaths) {
|
|
30
|
+
const abbreviation = symbolSet.get(symbolPath);
|
|
31
|
+
if (abbreviation) {
|
|
32
|
+
const dimensionalIndex = this.resolveDimensionalIndex(symbolPath);
|
|
33
|
+
fields.push(`${abbreviation}${dimensionalIndex}`);
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
fields.push(symbolPath);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return fields;
|
|
41
|
+
}
|
|
42
|
+
resolveDimensionalIndex(path) {
|
|
43
|
+
if (this.isJobPath(path)) {
|
|
44
|
+
return '';
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
const [activityId] = path.split('/');
|
|
48
|
+
if (activityId in this.dIds) {
|
|
49
|
+
return this.dIds[activityId];
|
|
50
|
+
}
|
|
51
|
+
else if ('$ADJACENT' in this.dIds) {
|
|
52
|
+
//else=> pre-authorizing adjacent activity entry
|
|
53
|
+
return this.dIds['$ADJACENT'];
|
|
54
|
+
}
|
|
55
|
+
return ',0';
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
isJobPath(path) {
|
|
59
|
+
return path.startsWith('data/') || path.startsWith('metadata/');
|
|
60
|
+
}
|
|
61
|
+
resetSymbols(symKeys, symVals, dIds) {
|
|
26
62
|
this.symKeys = new Map();
|
|
27
63
|
this.symReverseKeys = new Map();
|
|
28
64
|
for (const id in symKeys) {
|
|
@@ -30,6 +66,7 @@ class SerializerService {
|
|
|
30
66
|
}
|
|
31
67
|
this.symValMaps = new Map(Object.entries(symVals));
|
|
32
68
|
this.symValReverseMaps = this.getReverseValueMap(this.symValMaps);
|
|
69
|
+
this.dIds = dIds;
|
|
33
70
|
}
|
|
34
71
|
getReverseKeyMap(keyMap, id) {
|
|
35
72
|
let map = this.symReverseKeys.get(id);
|
|
@@ -70,24 +107,22 @@ class SerializerService {
|
|
|
70
107
|
if (this.symKeys.size === 0) {
|
|
71
108
|
return document;
|
|
72
109
|
}
|
|
73
|
-
let
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
delete result[key];
|
|
83
|
-
}
|
|
110
|
+
let source = { ...document };
|
|
111
|
+
let result = {};
|
|
112
|
+
const compressWithMap = (abbreviationMap, id) => {
|
|
113
|
+
for (let key in source) {
|
|
114
|
+
if (key.startsWith(`${id}/`) || (id.startsWith('$') && ['data', 'metadata'].includes(key.split('/')[0]))) {
|
|
115
|
+
const dimensionalIndex = this.resolveDimensionalIndex(key);
|
|
116
|
+
let shortKey = abbreviationMap.get(key) || key;
|
|
117
|
+
const shortDimensionalKey = `${shortKey}${dimensionalIndex}`;
|
|
118
|
+
result[shortDimensionalKey] = source[key];
|
|
84
119
|
}
|
|
85
120
|
}
|
|
86
121
|
};
|
|
87
122
|
for (let id of ids) {
|
|
88
123
|
const abbreviationMap = this.symKeys.get(id);
|
|
89
124
|
if (abbreviationMap) {
|
|
90
|
-
compressWithMap(abbreviationMap);
|
|
125
|
+
compressWithMap(abbreviationMap, id);
|
|
91
126
|
}
|
|
92
127
|
}
|
|
93
128
|
return result;
|
|
@@ -100,14 +135,12 @@ class SerializerService {
|
|
|
100
135
|
const inflateWithMap = (abbreviationMap, id) => {
|
|
101
136
|
const reversedAbbreviationMap = this.getReverseKeyMap(abbreviationMap, id);
|
|
102
137
|
for (let key in result) {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
let
|
|
106
|
-
if (
|
|
107
|
-
result[
|
|
108
|
-
|
|
109
|
-
delete result[key];
|
|
110
|
-
}
|
|
138
|
+
//strip dimensional index from key
|
|
139
|
+
const shortKey = key.split(',')[0];
|
|
140
|
+
let longKey = reversedAbbreviationMap.get(shortKey);
|
|
141
|
+
if (longKey) {
|
|
142
|
+
result[longKey] = result[key];
|
|
143
|
+
delete result[key];
|
|
111
144
|
}
|
|
112
145
|
}
|
|
113
146
|
};
|
|
@@ -59,9 +59,9 @@ declare abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
59
59
|
getJobIds(indexKeys: string[], idRange: [number, number]): Promise<IdsData>;
|
|
60
60
|
setStatus(collationKeyStatus: number, jobId: string, appId: string, multi?: U): Promise<any>;
|
|
61
61
|
getStatus(jobId: string, appId: string): Promise<number>;
|
|
62
|
-
setState({ ...state }: StringAnyType, status: number | null, jobId: string,
|
|
63
|
-
getState(jobId: string, consumes: Consumes): Promise<[StringAnyType, number] | undefined>;
|
|
64
|
-
collate(jobId: string, activityId: string, amount: number, multi?: U): Promise<number>;
|
|
62
|
+
setState({ ...state }: StringAnyType, status: number | null, jobId: string, symbolNames: string[], dIds: StringStringType, multi?: U): Promise<string>;
|
|
63
|
+
getState(jobId: string, consumes: Consumes, dIds: StringStringType): Promise<[StringAnyType, number] | undefined>;
|
|
64
|
+
collate(jobId: string, activityId: string, amount: number, dIds: StringStringType, multi?: U): Promise<number>;
|
|
65
65
|
setStateNX(jobId: string, appId: string): Promise<boolean>;
|
|
66
66
|
getSchema(activityId: string, appVersion: AppVID): Promise<ActivityType>;
|
|
67
67
|
getSchemas(appVersion: AppVID): Promise<Record<string, ActivityType>>;
|
|
@@ -374,12 +374,12 @@ class StoreService {
|
|
|
374
374
|
const status = await this.redisClient[this.commands.hget](jobKey, ':');
|
|
375
375
|
return Number(status);
|
|
376
376
|
}
|
|
377
|
-
async setState({ ...state }, status, jobId,
|
|
377
|
+
async setState({ ...state }, status, jobId, symbolNames, dIds, multi) {
|
|
378
378
|
delete state['metadata/js'];
|
|
379
|
-
const hashKey = this.mintKey(key_1.KeyType.JOB_STATE, { appId, jobId });
|
|
379
|
+
const hashKey = this.mintKey(key_1.KeyType.JOB_STATE, { appId: this.appId, jobId });
|
|
380
380
|
const symKeys = await this.getSymbolKeys(symbolNames);
|
|
381
381
|
const symVals = await this.getSymbolValues();
|
|
382
|
-
this.serializer.resetSymbols(symKeys, symVals);
|
|
382
|
+
this.serializer.resetSymbols(symKeys, symVals, dIds);
|
|
383
383
|
const hashData = this.serializer.package(state, symbolNames);
|
|
384
384
|
if (status !== null) {
|
|
385
385
|
hashData[':'] = status.toString();
|
|
@@ -390,26 +390,13 @@ class StoreService {
|
|
|
390
390
|
await (multi || this.redisClient)[this.commands.hset](hashKey, hashData);
|
|
391
391
|
return jobId;
|
|
392
392
|
}
|
|
393
|
-
async getState(jobId, consumes) {
|
|
394
|
-
//
|
|
393
|
+
async getState(jobId, consumes, dIds) {
|
|
394
|
+
//get abbreviated field list (the symbols for the paths)
|
|
395
395
|
const key = this.mintKey(key_1.KeyType.JOB_STATE, { appId: this.appId, jobId });
|
|
396
396
|
const symbolNames = Object.keys(consumes);
|
|
397
397
|
const symKeys = await this.getSymbolKeys(symbolNames);
|
|
398
|
-
|
|
399
|
-
const fields = [':'];
|
|
400
|
-
for (const symbolName of symbolNames) {
|
|
401
|
-
const symbolSet = symKeys[symbolName];
|
|
402
|
-
const symbolPaths = consumes[symbolName];
|
|
403
|
-
for (const symbolPath of symbolPaths) {
|
|
404
|
-
const abbreviation = symbolSet[symbolPath];
|
|
405
|
-
if (abbreviation) {
|
|
406
|
-
fields.push(abbreviation);
|
|
407
|
-
}
|
|
408
|
-
else {
|
|
409
|
-
fields.push(symbolPath);
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
}
|
|
398
|
+
this.serializer.resetSymbols(symKeys, {}, dIds);
|
|
399
|
+
const fields = this.serializer.abbreviate(consumes, symbolNames, [':']);
|
|
413
400
|
const jobDataArray = await this.redisClient[this.commands.hmget](key, fields);
|
|
414
401
|
const jobData = {};
|
|
415
402
|
let atLeast1 = false; //if status field (':') isn't present assume 404
|
|
@@ -421,7 +408,7 @@ class StoreService {
|
|
|
421
408
|
});
|
|
422
409
|
if (atLeast1) {
|
|
423
410
|
const symVals = await this.getSymbolValues();
|
|
424
|
-
this.serializer.resetSymbols(symKeys, symVals);
|
|
411
|
+
this.serializer.resetSymbols(symKeys, symVals, dIds);
|
|
425
412
|
const state = this.serializer.unpackage(jobData, symbolNames);
|
|
426
413
|
let status = 0;
|
|
427
414
|
if (state[':']) {
|
|
@@ -432,13 +419,13 @@ class StoreService {
|
|
|
432
419
|
return [state, status];
|
|
433
420
|
}
|
|
434
421
|
}
|
|
435
|
-
async collate(jobId, activityId, amount, multi) {
|
|
422
|
+
async collate(jobId, activityId, amount, dIds, multi) {
|
|
436
423
|
const jobKey = this.mintKey(key_1.KeyType.JOB_STATE, { appId: this.appId, jobId });
|
|
437
424
|
const collationKey = `${activityId}/output/metadata/as`; //activity state
|
|
438
425
|
const symbolNames = [activityId];
|
|
439
426
|
const symKeys = await this.getSymbolKeys(symbolNames);
|
|
440
427
|
const symVals = await this.getSymbolValues();
|
|
441
|
-
this.serializer.resetSymbols(symKeys, symVals);
|
|
428
|
+
this.serializer.resetSymbols(symKeys, symVals, dIds);
|
|
442
429
|
const payload = { [collationKey]: amount.toString() };
|
|
443
430
|
const hashData = this.serializer.package(payload, symbolNames);
|
|
444
431
|
const targetId = Object.keys(hashData)[0];
|
package/package.json
CHANGED
|
@@ -142,7 +142,8 @@ class Activity {
|
|
|
142
142
|
const durationInSeconds = Pipe.resolve(this.config.sleep, this.context);
|
|
143
143
|
const jobId = this.context.metadata.jid;
|
|
144
144
|
const activityId = this.metadata.aid;
|
|
145
|
-
|
|
145
|
+
const dId = this.metadata.dad;
|
|
146
|
+
await this.engine.task.registerTimeHook(jobId, `${activityId}${dId||''}`, 'sleep', durationInSeconds);
|
|
146
147
|
return jobId;
|
|
147
148
|
}
|
|
148
149
|
}
|
|
@@ -206,6 +207,7 @@ class Activity {
|
|
|
206
207
|
telemetry.setActivityAttributes(attrs);
|
|
207
208
|
return jobStatus as number;
|
|
208
209
|
} catch (error) {
|
|
210
|
+
console.error('this error?', error);
|
|
209
211
|
this.logger.error('engine-process-hook-event-error', error);
|
|
210
212
|
telemetry.setActivityError(error.message);
|
|
211
213
|
throw error;
|
|
@@ -278,12 +280,11 @@ class Activity {
|
|
|
278
280
|
}
|
|
279
281
|
|
|
280
282
|
bindDimensionalAddress(state: StringAnyType) {
|
|
281
|
-
const
|
|
282
|
-
state[`${aid}/output/metadata/dad`] = dad;
|
|
283
|
+
const dad = this.resolveDad();
|
|
284
|
+
state[`${this.metadata.aid}/output/metadata/dad`] = dad;
|
|
283
285
|
}
|
|
284
286
|
|
|
285
287
|
async setState(multi?: RedisMulti): Promise<string> {
|
|
286
|
-
const { id: appId } = await this.engine.getVID();
|
|
287
288
|
const jobId = this.context.metadata.jid;
|
|
288
289
|
this.bindJobMetadata();
|
|
289
290
|
this.bindActivityMetadata();
|
|
@@ -298,7 +299,8 @@ class Activity {
|
|
|
298
299
|
this.metadata.aid,
|
|
299
300
|
...presets
|
|
300
301
|
];
|
|
301
|
-
|
|
302
|
+
const dIds = CollatorService.getDimensionsById([...this.config.ancestors, this.metadata.aid], this.resolveDad());
|
|
303
|
+
return await this.store.setState(state, this.getJobStatus(), jobId, symbolNames, dIds, multi);
|
|
302
304
|
}
|
|
303
305
|
|
|
304
306
|
bindJobMetadata(): void {
|
|
@@ -314,6 +316,7 @@ class Activity {
|
|
|
314
316
|
if (this.status === StreamStatus.ERROR) {
|
|
315
317
|
self.output.metadata.err = JSON.stringify(this.data);
|
|
316
318
|
}
|
|
319
|
+
//todo: verify leg2 never overwrites leg1 `ac`
|
|
317
320
|
self.output.metadata.ac =
|
|
318
321
|
self.output.metadata.au = formatISODate(new Date());
|
|
319
322
|
self.output.metadata.atp = this.config.type;
|
|
@@ -386,10 +389,11 @@ class Activity {
|
|
|
386
389
|
}
|
|
387
390
|
}
|
|
388
391
|
TelemetryService.addTargetTelemetryPaths(consumes, this.config, this.metadata, this.leg);
|
|
389
|
-
|
|
392
|
+
let { dad, jid } = this.context.metadata;
|
|
390
393
|
jobId = jobId || jid;
|
|
391
394
|
//`state` is a flat hash
|
|
392
|
-
const
|
|
395
|
+
const dIds = CollatorService.getDimensionsById([...this.config.ancestors, this.metadata.aid], dad);
|
|
396
|
+
const [state, status] = await this.store.getState(jobId, consumes, dIds);
|
|
393
397
|
//`context` is a tree
|
|
394
398
|
this.context = restoreHierarchy(state) as JobState;
|
|
395
399
|
this.initDimensionalAddress(dad);
|
|
@@ -429,11 +433,26 @@ class Activity {
|
|
|
429
433
|
this.context[this.metadata.aid][type].data = this.data;
|
|
430
434
|
}
|
|
431
435
|
|
|
436
|
+
resolveDad(): string {
|
|
437
|
+
let dad = this.metadata.dad;
|
|
438
|
+
if (this.adjacentIndex > 0) {
|
|
439
|
+
//if adjacent index > 0 the activity is cycling; replace last index with cycle index
|
|
440
|
+
dad = `${dad.substring(0, dad.lastIndexOf(','))},${this.adjacentIndex}`
|
|
441
|
+
}
|
|
442
|
+
return dad;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
resolveAdjacentDad(): string {
|
|
446
|
+
//concat self and child dimension (all children (leg 1) begin life at 0)
|
|
447
|
+
return `${this.resolveDad()}${DimensionService.getSeed(0)}`;
|
|
448
|
+
};
|
|
449
|
+
|
|
432
450
|
async filterAdjacent(): Promise<StreamData[]> {
|
|
433
451
|
const adjacencyList: StreamData[] = [];
|
|
434
452
|
const transitions = await this.store.getTransitions(await this.engine.getVID());
|
|
435
453
|
const transition = transitions[`.${this.metadata.aid}`];
|
|
436
|
-
|
|
454
|
+
//resolve the dimensional address for adjacent children
|
|
455
|
+
const adjacentDad = this.resolveAdjacentDad();
|
|
437
456
|
if (transition) {
|
|
438
457
|
for (const toActivityId in transition) {
|
|
439
458
|
const transitionRule: boolean | TransitionRule = transition[toActivityId];
|
|
@@ -441,7 +460,7 @@ class Activity {
|
|
|
441
460
|
adjacencyList.push({
|
|
442
461
|
metadata: {
|
|
443
462
|
jid: this.context.metadata.jid,
|
|
444
|
-
dad:
|
|
463
|
+
dad: adjacentDad,
|
|
445
464
|
aid: toActivityId,
|
|
446
465
|
spn: this.context['$self'].output.metadata?.l2s,
|
|
447
466
|
trc: this.context.metadata.trc,
|
|
@@ -62,6 +62,7 @@ class Worker extends Activity {
|
|
|
62
62
|
if (error instanceof GetStateError) {
|
|
63
63
|
this.logger.error('worker-get-state-error', error);
|
|
64
64
|
} else {
|
|
65
|
+
console.error(error);
|
|
65
66
|
this.logger.error('worker-process-error', error);
|
|
66
67
|
}
|
|
67
68
|
telemetry.setActivityError(error.message);
|
|
@@ -1,18 +1,28 @@
|
|
|
1
|
-
import { CollationError } from
|
|
2
|
-
import { RedisMulti } from
|
|
3
|
-
import { CollationFaultType, CollationStage } from
|
|
4
|
-
import { ActivityDuplex } from
|
|
5
|
-
import { HotMeshGraph } from
|
|
6
|
-
import { Activity } from
|
|
1
|
+
import { CollationError } from '../../modules/errors';
|
|
2
|
+
import { RedisMulti } from '../../types/redis';
|
|
3
|
+
import { CollationFaultType, CollationStage } from '../../types/collator';
|
|
4
|
+
import { ActivityDuplex } from '../../types/activity';
|
|
5
|
+
import { HotMeshGraph } from '../../types/hotmesh';
|
|
6
|
+
import { Activity } from '../activities/activity';
|
|
7
7
|
|
|
8
8
|
class CollatorService {
|
|
9
9
|
|
|
10
10
|
//max int digit count that supports `hincrby`
|
|
11
|
-
static targetLength = 15;
|
|
11
|
+
static targetLength = 15;
|
|
12
|
+
|
|
13
|
+
static getDimensionalAddress(activity: Activity): Record<string, string> {
|
|
14
|
+
let dad = activity.context.metadata.dad || activity.metadata.dad;
|
|
15
|
+
//todo: unsure about this reset
|
|
16
|
+
// if (dad && activity.leg === 2) {
|
|
17
|
+
// console.log('setting dad index back to 0=>', dad);
|
|
18
|
+
// dad = `${dad.substring(0, dad.lastIndexOf(','))},0`;
|
|
19
|
+
// }
|
|
20
|
+
return CollatorService.getDimensionsById([...activity.config.ancestors, activity.metadata.aid], dad);
|
|
21
|
+
}
|
|
12
22
|
|
|
13
23
|
static async notarizeEntry(activity: Activity, multi?: RedisMulti): Promise<number> {
|
|
14
24
|
//decrement by -100_000_000_000_000
|
|
15
|
-
const amount = await activity.store.collate(activity.context.metadata.jid, activity.metadata.aid, -100_000_000_000_000, multi);
|
|
25
|
+
const amount = await activity.store.collate(activity.context.metadata.jid, activity.metadata.aid, -100_000_000_000_000, this.getDimensionalAddress(activity), multi);
|
|
16
26
|
this.verifyInteger(amount, 1, 'enter');
|
|
17
27
|
return amount;
|
|
18
28
|
};
|
|
@@ -20,31 +30,31 @@ class CollatorService {
|
|
|
20
30
|
static async authorizeReentry(activity: Activity, multi?: RedisMulti): Promise<number> {
|
|
21
31
|
//set second digit to 8, allowing for re-entry
|
|
22
32
|
//decrement by -10_000_000_000_000
|
|
23
|
-
const amount = await activity.store.collate(activity.context.metadata.jid, activity.metadata.aid, -10_000_000_000_000, multi);
|
|
33
|
+
const amount = await activity.store.collate(activity.context.metadata.jid, activity.metadata.aid, -10_000_000_000_000, this.getDimensionalAddress(activity), multi);
|
|
24
34
|
//this.verifyInteger(amount, 1, 'exit');
|
|
25
35
|
return amount;
|
|
26
36
|
}
|
|
27
37
|
|
|
28
38
|
static async notarizeEarlyCompletion(activity: Activity, multi?: RedisMulti): Promise<number> {
|
|
29
39
|
//initialize both `possible` (1m) and `actualized` (1) zero dimension, while decrementing the 2nd and 3rd digits to deactivate the activity
|
|
30
|
-
return await activity.store.collate(activity.context.metadata.jid, activity.metadata.aid, 1_000_001 - 11_000_000_000_000, multi);
|
|
40
|
+
return await activity.store.collate(activity.context.metadata.jid, activity.metadata.aid, 1_000_001 - 11_000_000_000_000, this.getDimensionalAddress(activity), multi);
|
|
31
41
|
};
|
|
32
42
|
|
|
33
43
|
static async notarizeReentry(activity: Activity, multi?: RedisMulti): Promise<number> {
|
|
34
44
|
//increment by 1_000_000 (indicates re-entry and is used to drive the 'dimensional address' for adjacent activities (minus 1))
|
|
35
|
-
const amount = await activity.store.collate(activity.context.metadata.jid, activity.metadata.aid, 1_000_000, multi);
|
|
45
|
+
const amount = await activity.store.collate(activity.context.metadata.jid, activity.metadata.aid, 1_000_000, this.getDimensionalAddress(activity), multi);
|
|
36
46
|
this.verifyInteger(amount, 2, 'enter');
|
|
37
47
|
return amount;
|
|
38
48
|
};
|
|
39
49
|
|
|
40
50
|
static async notarizeContinuation(activity: Activity, multi?: RedisMulti): Promise<number> {
|
|
41
51
|
//keep open; actualize the leg2 dimension (+1)
|
|
42
|
-
return await activity.store.collate(activity.context.metadata.jid, activity.metadata.aid, 1, multi);
|
|
52
|
+
return await activity.store.collate(activity.context.metadata.jid, activity.metadata.aid, 1, this.getDimensionalAddress(activity), multi);
|
|
43
53
|
};
|
|
44
54
|
|
|
45
55
|
static async notarizeCompletion(activity: Activity, multi?: RedisMulti): Promise<number> {
|
|
46
56
|
//close out; actualize leg2 dimension (+1) and decrement the 3rd digit (-1_000_000_000_000)
|
|
47
|
-
return await activity.store.collate(activity.context.metadata.jid, activity.metadata.aid, 1 - 1_000_000_000_000, multi);
|
|
57
|
+
return await activity.store.collate(activity.context.metadata.jid, activity.metadata.aid, 1 - 1_000_000_000_000, this.getDimensionalAddress(activity), multi);
|
|
48
58
|
};
|
|
49
59
|
|
|
50
60
|
static getDigitAtIndex(num: number, targetDigitIndex: number): number | null {
|
|
@@ -112,6 +122,21 @@ class CollatorService {
|
|
|
112
122
|
}
|
|
113
123
|
}
|
|
114
124
|
|
|
125
|
+
static getDimensionsById(ancestors: string[], dad: string): Record<string, string> {
|
|
126
|
+
//ancestors is an ordered list of all ancestors, starting with the trigger (['t1', 'a1', 'a2'])
|
|
127
|
+
//dad is the dimensional address of the ancestors list (',0,5,3')
|
|
128
|
+
//loop through the ancestors list and create a map of the ancestor to the dimensional address.
|
|
129
|
+
//return { 't1': ',0', 'a1': ',0,5', 'a1': ',0,5,3', $ADJACENT: ',0,5,3,0' };
|
|
130
|
+
// `adjacent` is a special key that is used to track the dimensional address of adjacent activities
|
|
131
|
+
const map: Record<string, string> = { '$ADJACENT': `${dad},0` };
|
|
132
|
+
let dadStr = dad;
|
|
133
|
+
ancestors.reverse().forEach((ancestor) => {
|
|
134
|
+
map[ancestor] = dadStr;
|
|
135
|
+
dadStr = dadStr.substring(0, dadStr.lastIndexOf(','));
|
|
136
|
+
});
|
|
137
|
+
return map;
|
|
138
|
+
}
|
|
139
|
+
|
|
115
140
|
/**
|
|
116
141
|
* All non-trigger activities are assigned a status seed by their parent
|
|
117
142
|
*/
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { KeyStoreParams, KeyType } from
|
|
2
|
-
import { getSymKey } from
|
|
3
|
-
import { CollatorService } from
|
|
4
|
-
import { SerializerService } from
|
|
1
|
+
import { KeyStoreParams, KeyType } from '../../modules/key';
|
|
2
|
+
import { getSymKey } from '../../modules/utils';
|
|
3
|
+
import { CollatorService } from '../collator';
|
|
4
|
+
import { SerializerService } from '../serializer';
|
|
5
5
|
import { StoreService } from '../store';
|
|
6
|
-
import { ActivityType } from
|
|
7
|
-
import { HookRule } from
|
|
8
|
-
import { HotMeshGraph, HotMeshManifest } from
|
|
9
|
-
import { RedisClient, RedisMulti } from
|
|
10
|
-
import { StringAnyType, Symbols } from
|
|
6
|
+
import { ActivityType } from '../../types/activity';
|
|
7
|
+
import { HookRule } from '../../types/hook';
|
|
8
|
+
import { HotMeshGraph, HotMeshManifest } from '../../types/hotmesh';
|
|
9
|
+
import { RedisClient, RedisMulti } from '../../types/redis';
|
|
10
|
+
import { StringAnyType, Symbols } from '../../types/serializer';
|
|
11
11
|
|
|
12
12
|
const DEFAULT_METADATA_RANGE_SIZE = 26; //metadata is 26 slots ([a-z] * 1)
|
|
13
13
|
const DEFAULT_DATA_RANGE_SIZE = 260; //data is 260 slots ([a-zA-Z] * 5)
|
|
@@ -63,6 +63,7 @@ class Deployer {
|
|
|
63
63
|
for (const [activityId, activity] of Object.entries(graph.activities)) {
|
|
64
64
|
const [lower, upper, symbols] = await this.store.reserveSymbolRange(activityId, DEFAULT_RANGE_SIZE, 'ACTIVITY');
|
|
65
65
|
const prefix = `${activityId}/`; //activity meta/data is namespaced
|
|
66
|
+
this.bindSelf(activity.consumes, activity.produces, activityId);
|
|
66
67
|
const newSymbols = this.bindSymbols(lower, upper, symbols, prefix, activity.produces);
|
|
67
68
|
if (Object.keys(newSymbols).length) {
|
|
68
69
|
await this.store.addSymbols(activityId, newSymbols);
|
|
@@ -71,6 +72,20 @@ class Deployer {
|
|
|
71
72
|
}
|
|
72
73
|
}
|
|
73
74
|
|
|
75
|
+
bindSelf(consumes: Record<string, string[]>, produces: string[], activityId: string) {
|
|
76
|
+
//bind self-referential mappings
|
|
77
|
+
for (const selfId of [activityId, '$self']) {
|
|
78
|
+
const selfConsumes = consumes[selfId];
|
|
79
|
+
if (selfConsumes) {
|
|
80
|
+
for (const path of selfConsumes) {
|
|
81
|
+
if (!produces.includes(path)) {
|
|
82
|
+
produces.push(path);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
74
89
|
bindSymbols(startIndex: number, maxIndex: number, existingSymbols: Symbols, prefix: string, produces: string[]): Symbols {
|
|
75
90
|
let newSymbols: Symbols = {};
|
|
76
91
|
let currentSymbols: Symbols = {...existingSymbols};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { JobOutput } from
|
|
2
|
-
import { HotMeshService as HotMesh } from
|
|
1
|
+
import { JobOutput } from '../../types/job';
|
|
2
|
+
import { HotMeshService as HotMesh } from '../hotmesh';
|
|
3
3
|
|
|
4
4
|
export class WorkflowHandleService {
|
|
5
5
|
hotMesh: HotMesh;
|
package/services/engine/index.ts
CHANGED
|
@@ -63,6 +63,7 @@ import {
|
|
|
63
63
|
StreamError,
|
|
64
64
|
StreamRole,
|
|
65
65
|
StreamStatus } from '../../types/stream';
|
|
66
|
+
import { StringStringType } from '../../types';
|
|
66
67
|
|
|
67
68
|
//wait time to see if a job is complete
|
|
68
69
|
const OTT_WAIT_TIME = 1000;
|
|
@@ -408,21 +409,37 @@ class EngineService {
|
|
|
408
409
|
}
|
|
409
410
|
|
|
410
411
|
// ****************** `HOOK` ACTIVITY RE-ENTRY POINT *****************
|
|
411
|
-
async hook(topic: string, data: JobData): Promise<JobStatus | void> {
|
|
412
|
+
async hook(topic: string, data: JobData, dad?: string): Promise<JobStatus | void> {
|
|
412
413
|
const hookRule = await this.storeSignaler.getHookRule(topic);
|
|
414
|
+
const [aid, schema] = await this.getSchema(`.${hookRule.to}`);
|
|
415
|
+
if (!dad) {
|
|
416
|
+
//assume dimensional address is singular (0)
|
|
417
|
+
// for ancestors and self if not provided
|
|
418
|
+
// todo: register
|
|
419
|
+
dad = ',0'.repeat(schema.ancestors.length + 1);
|
|
420
|
+
}
|
|
413
421
|
const streamData: StreamData = {
|
|
414
422
|
type: StreamDataType.WEBHOOK,
|
|
415
|
-
metadata: {
|
|
423
|
+
metadata: {
|
|
424
|
+
//jid is unknown at this point; will be resolved using the data
|
|
425
|
+
aid,
|
|
426
|
+
dad,
|
|
427
|
+
topic
|
|
428
|
+
},
|
|
416
429
|
data,
|
|
417
430
|
};
|
|
418
431
|
await this.streamSignaler.publishMessage(null, streamData);
|
|
419
432
|
}
|
|
420
433
|
async hookTime(jobId: string, activityId: string): Promise<JobStatus | void> {
|
|
434
|
+
//the activityid is concatenated with its dimensional address (dad); split to resolve
|
|
435
|
+
const [aid, ...dimensions] = activityId.split(',');
|
|
436
|
+
const dad = `,${dimensions.join(',')}`;
|
|
421
437
|
const streamData: StreamData = {
|
|
422
438
|
type: StreamDataType.TIMEHOOK,
|
|
423
439
|
metadata: {
|
|
424
440
|
jid: jobId,
|
|
425
|
-
aid
|
|
441
|
+
aid,
|
|
442
|
+
dad,
|
|
426
443
|
},
|
|
427
444
|
data: { timestamp: Date.now() },
|
|
428
445
|
};
|
|
@@ -586,12 +603,13 @@ class EngineService {
|
|
|
586
603
|
// (e.g, if {dimensions:true}, use hscan to deliver
|
|
587
604
|
// the full set of dimensional job data)
|
|
588
605
|
async getState(topic: string, jobId: string): Promise<JobOutput> {
|
|
589
|
-
const { id: appId } = await this.getVID();
|
|
590
606
|
const jobSymbols = await this.store.getSymbols(`$${topic}`);
|
|
591
607
|
const consumes: Consumes = {
|
|
592
608
|
[`$${topic}`]: Object.keys(jobSymbols)
|
|
593
609
|
}
|
|
594
|
-
|
|
610
|
+
//job data exists at the 'zero' dimension; pass an empty object
|
|
611
|
+
const dIds = {} as StringStringType;
|
|
612
|
+
const output = await this.store.getState(jobId, consumes, dIds);
|
|
595
613
|
if (!output) {
|
|
596
614
|
throw new Error(`not found ${jobId}`);
|
|
597
615
|
}
|
|
@@ -161,9 +161,9 @@ class HotMeshService {
|
|
|
161
161
|
}
|
|
162
162
|
|
|
163
163
|
// ****** `HOOK` ACTIVITY RE-ENTRY POINT ******
|
|
164
|
-
async hook(topic: string, data: JobData): Promise<JobStatus | void> {
|
|
164
|
+
async hook(topic: string, data: JobData, dad?: string): Promise<JobStatus | void> {
|
|
165
165
|
//return collation int
|
|
166
|
-
return await this.engine?.hook(topic, data);
|
|
166
|
+
return await this.engine?.hook(topic, data, dad);
|
|
167
167
|
}
|
|
168
168
|
async hookAll(hookTopic: string, data: JobData, query: JobStatsInput, queryFacets: string[] = []): Promise<string[]> {
|
|
169
169
|
return await this.engine?.hookAll(hookTopic, data, query, queryFacets);
|
package/services/mapper/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getSymVal } from '../../modules/utils';
|
|
2
|
+
import { Consumes } from '../../types/activity';
|
|
2
3
|
import {
|
|
3
4
|
StringStringType,
|
|
4
5
|
StringAnyType,
|
|
@@ -26,16 +27,53 @@ export const MDATA_SYMBOLS = {
|
|
|
26
27
|
};
|
|
27
28
|
|
|
28
29
|
export class SerializerService {
|
|
30
|
+
dIds: StringStringType;
|
|
29
31
|
symKeys: SymbolMaps;
|
|
30
32
|
symReverseKeys: SymbolMaps;
|
|
31
33
|
symValMaps: SymbolMap;
|
|
32
34
|
symValReverseMaps: SymbolMap;
|
|
33
35
|
|
|
34
36
|
constructor() {
|
|
35
|
-
this.resetSymbols({}, {});
|
|
37
|
+
this.resetSymbols({}, {}, {});
|
|
36
38
|
}
|
|
37
39
|
|
|
38
|
-
|
|
40
|
+
abbreviate(consumes: Consumes, symbolNames: string[], fields: string[] = []): string[] {
|
|
41
|
+
for (const symbolName of symbolNames) {
|
|
42
|
+
const symbolSet = this.symKeys.get(symbolName);
|
|
43
|
+
const symbolPaths = consumes[symbolName];
|
|
44
|
+
for (const symbolPath of symbolPaths) {
|
|
45
|
+
const abbreviation = symbolSet.get(symbolPath);
|
|
46
|
+
if (abbreviation) {
|
|
47
|
+
const dimensionalIndex = this.resolveDimensionalIndex(symbolPath);
|
|
48
|
+
fields.push(`${abbreviation}${dimensionalIndex}`);
|
|
49
|
+
} else {
|
|
50
|
+
fields.push(symbolPath);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return fields;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
resolveDimensionalIndex(path: string): string {
|
|
58
|
+
if (this.isJobPath(path)) {
|
|
59
|
+
return '';
|
|
60
|
+
} else {
|
|
61
|
+
const [activityId] = path.split('/');
|
|
62
|
+
if (activityId in this.dIds) {
|
|
63
|
+
return this.dIds[activityId];
|
|
64
|
+
} else if ('$ADJACENT' in this.dIds) {
|
|
65
|
+
//else=> pre-authorizing adjacent activity entry
|
|
66
|
+
return this.dIds['$ADJACENT'];
|
|
67
|
+
}
|
|
68
|
+
return ',0';
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
isJobPath(path: string): boolean {
|
|
73
|
+
return path.startsWith('data/') || path.startsWith('metadata/');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
resetSymbols(symKeys: SymbolSets, symVals: Symbols, dIds: StringStringType): void {
|
|
39
77
|
this.symKeys = new Map();
|
|
40
78
|
this.symReverseKeys = new Map();
|
|
41
79
|
for (const id in symKeys) {
|
|
@@ -43,6 +81,7 @@ export class SerializerService {
|
|
|
43
81
|
}
|
|
44
82
|
this.symValMaps = new Map(Object.entries(symVals));
|
|
45
83
|
this.symValReverseMaps = this.getReverseValueMap(this.symValMaps);
|
|
84
|
+
this.dIds = dIds;
|
|
46
85
|
}
|
|
47
86
|
|
|
48
87
|
getReverseKeyMap(keyMap: SymbolMap, id?: string): SymbolMap {
|
|
@@ -87,25 +126,23 @@ export class SerializerService {
|
|
|
87
126
|
if (this.symKeys.size === 0) {
|
|
88
127
|
return document;
|
|
89
128
|
}
|
|
90
|
-
let
|
|
129
|
+
let source: StringStringType = { ...document };
|
|
130
|
+
let result: StringStringType = { };
|
|
91
131
|
|
|
92
|
-
const compressWithMap = (abbreviationMap: SymbolMap) => {
|
|
93
|
-
for (let key in
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
result[
|
|
99
|
-
if (safeKey !== key) {
|
|
100
|
-
delete result[key];
|
|
132
|
+
const compressWithMap = (abbreviationMap: SymbolMap, id: string) => {
|
|
133
|
+
for (let key in source) {
|
|
134
|
+
if (key.startsWith(`${id}/`) || (id.startsWith('$') && ['data', 'metadata'].includes(key.split('/')[0]))) {
|
|
135
|
+
const dimensionalIndex = this.resolveDimensionalIndex(key);
|
|
136
|
+
let shortKey = abbreviationMap.get(key) || key;
|
|
137
|
+
const shortDimensionalKey = `${shortKey}${dimensionalIndex}`;
|
|
138
|
+
result[shortDimensionalKey] = source[key];
|
|
101
139
|
}
|
|
102
|
-
}
|
|
103
140
|
}
|
|
104
141
|
};
|
|
105
142
|
for (let id of ids) {
|
|
106
143
|
const abbreviationMap = this.symKeys.get(id);
|
|
107
144
|
if (abbreviationMap) {
|
|
108
|
-
compressWithMap(abbreviationMap);
|
|
145
|
+
compressWithMap(abbreviationMap, id);
|
|
109
146
|
}
|
|
110
147
|
}
|
|
111
148
|
return result;
|
|
@@ -120,14 +157,12 @@ export class SerializerService {
|
|
|
120
157
|
const inflateWithMap = (abbreviationMap: SymbolMap, id: string) => {
|
|
121
158
|
const reversedAbbreviationMap = this.getReverseKeyMap(abbreviationMap, id);
|
|
122
159
|
for (let key in result) {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
let
|
|
126
|
-
if (
|
|
127
|
-
result[
|
|
128
|
-
|
|
129
|
-
delete result[key];
|
|
130
|
-
}
|
|
160
|
+
//strip dimensional index from key
|
|
161
|
+
const shortKey = key.split(',')[0];
|
|
162
|
+
let longKey = reversedAbbreviationMap.get(shortKey);
|
|
163
|
+
if (longKey) {
|
|
164
|
+
result[longKey] = result[key];
|
|
165
|
+
delete result[key];
|
|
131
166
|
}
|
|
132
167
|
}
|
|
133
168
|
};
|
package/services/store/index.ts
CHANGED
|
@@ -452,12 +452,12 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
452
452
|
return Number(status);
|
|
453
453
|
}
|
|
454
454
|
|
|
455
|
-
async setState({ ...state }: StringAnyType, status: number | null, jobId: string,
|
|
455
|
+
async setState({ ...state }: StringAnyType, status: number | null, jobId: string, symbolNames: string[], dIds: StringStringType, multi? : U): Promise<string> {
|
|
456
456
|
delete state['metadata/js'];
|
|
457
|
-
const hashKey = this.mintKey(KeyType.JOB_STATE, { appId, jobId });
|
|
457
|
+
const hashKey = this.mintKey(KeyType.JOB_STATE, { appId: this.appId, jobId });
|
|
458
458
|
const symKeys = await this.getSymbolKeys(symbolNames);
|
|
459
459
|
const symVals = await this.getSymbolValues();
|
|
460
|
-
this.serializer.resetSymbols(symKeys, symVals);
|
|
460
|
+
this.serializer.resetSymbols(symKeys, symVals, dIds);
|
|
461
461
|
|
|
462
462
|
const hashData = this.serializer.package(state, symbolNames);
|
|
463
463
|
if (status !== null) {
|
|
@@ -469,26 +469,14 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
469
469
|
return jobId;
|
|
470
470
|
}
|
|
471
471
|
|
|
472
|
-
async getState(jobId: string, consumes: Consumes): Promise<[StringAnyType, number] | undefined> {
|
|
473
|
-
//
|
|
472
|
+
async getState(jobId: string, consumes: Consumes, dIds: StringStringType): Promise<[StringAnyType, number] | undefined> {
|
|
473
|
+
//get abbreviated field list (the symbols for the paths)
|
|
474
474
|
const key = this.mintKey(KeyType.JOB_STATE, { appId: this.appId, jobId });
|
|
475
475
|
const symbolNames = Object.keys(consumes);
|
|
476
476
|
const symKeys = await this.getSymbolKeys(symbolNames);
|
|
477
|
+
this.serializer.resetSymbols(symKeys, {}, dIds);
|
|
478
|
+
const fields = this.serializer.abbreviate(consumes, symbolNames, [':']);
|
|
477
479
|
|
|
478
|
-
//always fetch the job status (':') when fetching state
|
|
479
|
-
const fields = [':'];
|
|
480
|
-
for (const symbolName of symbolNames) {
|
|
481
|
-
const symbolSet = symKeys[symbolName];
|
|
482
|
-
const symbolPaths = consumes[symbolName];
|
|
483
|
-
for (const symbolPath of symbolPaths) {
|
|
484
|
-
const abbreviation = symbolSet[symbolPath];
|
|
485
|
-
if (abbreviation) {
|
|
486
|
-
fields.push(abbreviation);
|
|
487
|
-
} else {
|
|
488
|
-
fields.push(symbolPath);
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
480
|
const jobDataArray = await this.redisClient[this.commands.hmget](key, fields);
|
|
493
481
|
const jobData: StringAnyType = {};
|
|
494
482
|
let atLeast1 = false; //if status field (':') isn't present assume 404
|
|
@@ -500,7 +488,7 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
500
488
|
});
|
|
501
489
|
if (atLeast1) {
|
|
502
490
|
const symVals = await this.getSymbolValues();
|
|
503
|
-
this.serializer.resetSymbols(symKeys, symVals);
|
|
491
|
+
this.serializer.resetSymbols(symKeys, symVals, dIds);
|
|
504
492
|
const state = this.serializer.unpackage(jobData, symbolNames);
|
|
505
493
|
let status = 0;
|
|
506
494
|
if (state[':']) {
|
|
@@ -512,13 +500,13 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
512
500
|
}
|
|
513
501
|
}
|
|
514
502
|
|
|
515
|
-
async collate(jobId: string, activityId: string, amount: number, multi? : U): Promise<number> {
|
|
503
|
+
async collate(jobId: string, activityId: string, amount: number, dIds: StringStringType, multi? : U): Promise<number> {
|
|
516
504
|
const jobKey = this.mintKey(KeyType.JOB_STATE, { appId: this.appId, jobId });
|
|
517
505
|
const collationKey = `${activityId}/output/metadata/as`; //activity state
|
|
518
506
|
const symbolNames = [activityId];
|
|
519
507
|
const symKeys = await this.getSymbolKeys(symbolNames);
|
|
520
508
|
const symVals = await this.getSymbolValues();
|
|
521
|
-
this.serializer.resetSymbols(symKeys, symVals);
|
|
509
|
+
this.serializer.resetSymbols(symKeys, symVals, dIds);
|
|
522
510
|
|
|
523
511
|
const payload = { [collationKey]: amount.toString() }
|
|
524
512
|
const hashData = this.serializer.package(payload, symbolNames);
|