@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.
Files changed (35) hide show
  1. package/README.md +1 -1
  2. package/build/package.json +1 -1
  3. package/build/services/activities/activity.d.ts +2 -0
  4. package/build/services/activities/activity.js +27 -9
  5. package/build/services/activities/worker.js +1 -0
  6. package/build/services/collator/index.d.ts +7 -5
  7. package/build/services/collator/index.js +29 -6
  8. package/build/services/compiler/deployer.d.ts +4 -3
  9. package/build/services/compiler/deployer.js +14 -0
  10. package/build/services/dimension/index.d.ts +1 -1
  11. package/build/services/durable/handle.d.ts +1 -1
  12. package/build/services/engine/index.d.ts +1 -1
  13. package/build/services/engine/index.js +22 -5
  14. package/build/services/hotmesh/index.d.ts +1 -1
  15. package/build/services/hotmesh/index.js +2 -2
  16. package/build/services/serializer/index.d.ts +6 -1
  17. package/build/services/serializer/index.js +55 -22
  18. package/build/services/signaler/stream.js +1 -0
  19. package/build/services/store/index.d.ts +3 -3
  20. package/build/services/store/index.js +10 -23
  21. package/build/types/activity.d.ts +1 -0
  22. package/package.json +1 -1
  23. package/services/activities/activity.ts +28 -9
  24. package/services/activities/worker.ts +1 -0
  25. package/services/collator/index.ts +38 -13
  26. package/services/compiler/deployer.ts +24 -9
  27. package/services/dimension/index.ts +1 -1
  28. package/services/durable/handle.ts +2 -2
  29. package/services/engine/index.ts +23 -5
  30. package/services/hotmesh/index.ts +2 -2
  31. package/services/mapper/index.ts +0 -1
  32. package/services/serializer/index.ts +57 -22
  33. package/services/signaler/stream.ts +1 -0
  34. package/services/store/index.ts +10 -22
  35. 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 swarm of [distributed engines](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.
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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotmeshio/hotmesh",
3
- "version": "0.0.3",
3
+ "version": "0.0.4",
4
4
  "description": "Durable Workflows",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -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
- await this.engine.task.registerTimeHook(jobId, activityId, 'sleep', durationInSeconds);
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 { aid, dad } = this.metadata;
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
- return await this.store.setState(state, this.getJobStatus(), jobId, appId, symbolNames, multi);
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
- const { dad, jid } = this.context.metadata;
321
+ let { dad, jid } = this.context.metadata;
319
322
  jobId = jobId || jid;
320
323
  //`state` is a flat hash
321
- const [state, status] = await this.store.getState(jobId, consumes);
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
- const adjacentSuffix = dimension_1.DimensionService.getSeed(this.adjacentIndex);
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: `${this.metadata.dad}${adjacentSuffix}`,
386
+ dad: adjacentDad,
369
387
  aid: toActivityId,
370
388
  spn: this.context['$self'].output.metadata?.l2s,
371
389
  trc: this.context.metadata.trc,
@@ -42,6 +42,7 @@ class Worker extends activity_1.Activity {
42
42
  this.logger.error('worker-get-state-error', error);
43
43
  }
44
44
  else {
45
+ console.error(error);
45
46
  this.logger.error('worker-process-error', error);
46
47
  }
47
48
  telemetry.setActivityError(error.message);
@@ -1,10 +1,11 @@
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";
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 "../../types/hotmesh";
3
- import { RedisClient, RedisMulti } from "../../types/redis";
4
- import { Symbols } from "../../types/serializer";
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 };
@@ -1,4 +1,4 @@
1
- import { HotMeshGraph } from "../../types/hotmesh";
1
+ import { HotMeshGraph } from '../../types/hotmesh';
2
2
  declare class DimensionService {
3
3
  static targetLength: number;
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { HotMeshService as HotMesh } from "../hotmesh";
1
+ import { HotMeshService as HotMesh } from '../hotmesh';
2
2
  export declare class WorkflowHandleService {
3
3
  hotMesh: HotMesh;
4
4
  workflowTopic: string;
@@ -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: { aid: `${hookRule.to}`, topic },
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: activityId,
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
- const output = await this.store.getState(jobId, consumes);
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
- resetSymbols(symKeys: SymbolSets, symVals: Symbols): void;
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
- resetSymbols(symKeys, symVals) {
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 result = { ...document };
74
- const compressWithMap = (abbreviationMap) => {
75
- for (let key in result) {
76
- let safeKey = abbreviationMap.get(key) || key;
77
- let value = result[key];
78
- let safeValue = abbreviationMap.get(value) || value;
79
- if (safeKey !== key || safeValue !== value) {
80
- result[safeKey] = safeValue;
81
- if (safeKey !== key) {
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
- let safeKey = reversedAbbreviationMap.get(key) || key;
104
- let value = result[key];
105
- let safeValue = reversedAbbreviationMap.get(value) || value;
106
- if (safeKey !== key || safeValue !== value) {
107
- result[safeKey] = safeValue;
108
- if (safeKey !== key) {
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
  };
@@ -121,6 +121,7 @@ class StreamSignaler {
121
121
  output = await callback(input);
122
122
  }
123
123
  catch (err) {
124
+ console.error(err);
124
125
  this.logger.error(`stream-call-function-error`, { stream, id, err });
125
126
  output = this.structureUnhandledError(input, err);
126
127
  }
@@ -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, appId: string, symbolNames: string[], multi?: U): Promise<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, appId, symbolNames, multi) {
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
- //todo: refactor into semantic submethods
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
- //always fetch the job status (':') when fetching state
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];
@@ -23,6 +23,7 @@ interface BaseActivity {
23
23
  subscribes?: string;
24
24
  trigger?: string;
25
25
  parent?: string;
26
+ ancestors?: string[];
26
27
  }
27
28
  interface Measure {
28
29
  measure: MetricTypes;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotmeshio/hotmesh",
3
- "version": "0.0.3",
3
+ "version": "0.0.4",
4
4
  "description": "Durable Workflows",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -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
- await this.engine.task.registerTimeHook(jobId, activityId, 'sleep', durationInSeconds);
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 { aid, dad } = this.metadata;
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
- return await this.store.setState(state, this.getJobStatus(), jobId, appId, symbolNames, multi);
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
- const { dad, jid } = this.context.metadata;
392
+ let { dad, jid } = this.context.metadata;
390
393
  jobId = jobId || jid;
391
394
  //`state` is a flat hash
392
- const [state, status] = await this.store.getState(jobId, consumes);
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
- const adjacentSuffix = DimensionService.getSeed(this.adjacentIndex);
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: `${this.metadata.dad}${adjacentSuffix}`,
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 "../../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";
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 "../../modules/key";
2
- import { getSymKey } from "../../modules/utils";
3
- import { CollatorService } from "../collator";
4
- import { SerializerService } from "../serializer";
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 "../../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";
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,4 +1,4 @@
1
- import { HotMeshGraph } from "../../types/hotmesh";
1
+ import { HotMeshGraph } from '../../types/hotmesh';
2
2
 
3
3
  class DimensionService {
4
4
 
@@ -1,5 +1,5 @@
1
- import { JobOutput } from "../../types/job";
2
- import { HotMeshService as HotMesh } from "../hotmesh";
1
+ import { JobOutput } from '../../types/job';
2
+ import { HotMeshService as HotMesh } from '../hotmesh';
3
3
 
4
4
  export class WorkflowHandleService {
5
5
  hotMesh: HotMesh;
@@ -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: { aid: `${hookRule.to}`, topic },
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: activityId,
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
- const output = await this.store.getState(jobId, consumes);
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);
@@ -80,5 +80,4 @@ class MapperService {
80
80
  }
81
81
  }
82
82
 
83
-
84
83
  export { MapperService }
@@ -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
- resetSymbols(symKeys: SymbolSets, symVals: Symbols): void {
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 result: StringStringType = { ...document };
129
+ let source: StringStringType = { ...document };
130
+ let result: StringStringType = { };
91
131
 
92
- const compressWithMap = (abbreviationMap: SymbolMap) => {
93
- for (let key in result) {
94
- let safeKey = abbreviationMap.get(key) || key;
95
- let value = result[key];
96
- let safeValue = abbreviationMap.get(value) || value;
97
- if (safeKey !== key || safeValue !== value) {
98
- result[safeKey] = safeValue;
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
- let safeKey = reversedAbbreviationMap.get(key) || key;
124
- let value = result[key];
125
- let safeValue = reversedAbbreviationMap.get(value) || value;
126
- if (safeKey !== key || safeValue !== value) {
127
- result[safeKey] = safeValue;
128
- if (safeKey !== key) {
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
  };
@@ -150,6 +150,7 @@ class StreamSignaler {
150
150
  try {
151
151
  output = await callback(input);
152
152
  } catch (err) {
153
+ console.error(err);
153
154
  this.logger.error(`stream-call-function-error`, { stream, id, err });
154
155
  output = this.structureUnhandledError(input, err);
155
156
  }
@@ -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, appId: string, symbolNames: string[], multi? : U): Promise<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
- //todo: refactor into semantic submethods
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);
package/types/activity.ts CHANGED
@@ -26,6 +26,7 @@ interface BaseActivity {
26
26
  subscribes?: string; //compiler
27
27
  trigger?: string; //compiler
28
28
  parent?: string; //compiler
29
+ ancestors?: string[]; //compiler
29
30
  }
30
31
 
31
32
  interface Measure {