@hotmeshio/hotmesh 0.0.38 → 0.0.40

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 (51) hide show
  1. package/README.md +13 -7
  2. package/build/modules/enums.d.ts +29 -23
  3. package/build/modules/enums.js +38 -29
  4. package/build/modules/errors.d.ts +1 -1
  5. package/build/modules/errors.js +9 -7
  6. package/build/modules/key.d.ts +1 -34
  7. package/build/modules/key.js +24 -47
  8. package/build/package.json +3 -3
  9. package/build/services/activities/activity.js +1 -1
  10. package/build/services/activities/hook.js +4 -9
  11. package/build/services/activities/trigger.d.ts +3 -2
  12. package/build/services/activities/trigger.js +10 -6
  13. package/build/services/durable/client.d.ts +9 -1
  14. package/build/services/durable/client.js +30 -14
  15. package/build/services/durable/handle.js +2 -2
  16. package/build/services/durable/worker.js +4 -3
  17. package/build/services/engine/index.d.ts +2 -1
  18. package/build/services/engine/index.js +6 -6
  19. package/build/services/router/index.js +16 -14
  20. package/build/services/store/index.d.ts +14 -9
  21. package/build/services/store/index.js +46 -23
  22. package/build/services/task/index.d.ts +10 -3
  23. package/build/services/task/index.js +35 -17
  24. package/build/types/durable.d.ts +3 -2
  25. package/build/types/hotmesh.d.ts +43 -2
  26. package/build/types/hotmesh.js +28 -0
  27. package/build/types/index.d.ts +3 -2
  28. package/build/types/index.js +3 -1
  29. package/build/types/logger.d.ts +1 -0
  30. package/build/types/logger.js +1 -0
  31. package/build/types/task.d.ts +1 -0
  32. package/build/types/task.js +2 -0
  33. package/modules/enums.ts +49 -35
  34. package/modules/errors.ts +17 -8
  35. package/modules/key.ts +3 -40
  36. package/package.json +3 -3
  37. package/services/activities/activity.ts +2 -2
  38. package/services/activities/hook.ts +18 -9
  39. package/services/activities/trigger.ts +10 -6
  40. package/services/durable/client.ts +31 -15
  41. package/services/durable/handle.ts +3 -3
  42. package/services/durable/worker.ts +4 -3
  43. package/services/engine/index.ts +13 -12
  44. package/services/router/index.ts +26 -24
  45. package/services/store/index.ts +59 -25
  46. package/services/task/index.ts +66 -24
  47. package/types/durable.ts +6 -5
  48. package/types/hotmesh.ts +47 -2
  49. package/types/index.ts +8 -1
  50. package/types/logger.ts +3 -1
  51. package/types/task.ts +1 -0
@@ -10,12 +10,13 @@ export { ILogger } from './logger';
10
10
  export { JobData, JobsData, JobMetadata, JobOutput, JobState, JobStatus, PartialJobState } from './job';
11
11
  export { MappingStatements } from './map';
12
12
  export { Pipe, PipeItem, PipeItems } from './pipe';
13
- export { HotMesh, HotMeshApp, HotMeshApps, HotMeshConfig, HotMeshEngine, RedisConfig, HotMeshGraph, HotMeshManifest, HotMeshSettings, HotMeshWorker } from './hotmesh';
14
- export { ActivateMessage, JobMessage, JobMessageCallback, PingMessage, PongMessage, QuorumMessage, SubscriptionCallback, ThrottleMessage, WorkMessage } from './quorum';
13
+ export { HotMesh, HotMeshApp, HotMeshApps, HotMeshConfig, HotMeshEngine, RedisConfig, HotMeshGraph, HotMeshManifest, HotMeshSettings, HotMeshWorker, KeyStoreParams, KeyType } from './hotmesh';
14
+ export { ActivateMessage, JobMessage, JobMessageCallback, PingMessage, PongMessage, QuorumMessage, QuorumMessageCallback, QuorumProfile, SubscriptionCallback, ThrottleMessage, WorkMessage } from './quorum';
15
15
  export { MultiResponseFlags, RedisClient, RedisMulti } from './redis';
16
16
  export { RedisClientType, RedisMultiType } from './redisclient';
17
17
  export { JSONSchema, StringAnyType, StringScalarType, StringStringType, SymbolMap, SymbolMaps, SymbolRanges, Symbols, SymbolSets } from './serializer';
18
18
  export { AggregatedData, CountByFacet, GetStatsOptions, IdsData, Measure, MeasureIds, MetricTypes, StatType, StatsType, IdsResponse, JobStats, JobStatsInput, JobStatsRange, StatsResponse, Segment, TimeSegment } from './stats';
19
19
  export { ReclaimedMessageType, StreamCode, StreamConfig, StreamData, StreamDataType, StreamError, StreamDataResponse, StreamRetryPolicy, StreamRole, StreamStatus } from './stream';
20
20
  export { context, Context, Counter, Meter, metrics, propagation, SpanContext, Span, SpanStatus, SpanStatusCode, SpanKind, trace, Tracer, ValueType } from './telemetry';
21
+ export { WorkListTaskType } from './task';
21
22
  export { TransitionMatch, TransitionRule, Transitions } from './transition';
@@ -1,12 +1,14 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ValueType = exports.trace = exports.SpanKind = exports.SpanStatusCode = exports.propagation = exports.metrics = exports.context = exports.StreamStatus = exports.StreamRole = exports.StreamDataType = exports.IORedisClientType = exports.HookGate = exports.CollationFaultType = void 0;
3
+ exports.ValueType = exports.trace = exports.SpanKind = exports.SpanStatusCode = exports.propagation = exports.metrics = exports.context = exports.StreamStatus = exports.StreamRole = exports.StreamDataType = exports.KeyType = exports.IORedisClientType = exports.HookGate = exports.CollationFaultType = void 0;
4
4
  var collator_1 = require("./collator");
5
5
  Object.defineProperty(exports, "CollationFaultType", { enumerable: true, get: function () { return collator_1.CollationFaultType; } });
6
6
  var hook_1 = require("./hook");
7
7
  Object.defineProperty(exports, "HookGate", { enumerable: true, get: function () { return hook_1.HookGate; } });
8
8
  var ioredisclient_1 = require("./ioredisclient");
9
9
  Object.defineProperty(exports, "IORedisClientType", { enumerable: true, get: function () { return ioredisclient_1.RedisClientType; } });
10
+ var hotmesh_1 = require("./hotmesh");
11
+ Object.defineProperty(exports, "KeyType", { enumerable: true, get: function () { return hotmesh_1.KeyType; } });
10
12
  var stream_1 = require("./stream");
11
13
  Object.defineProperty(exports, "StreamDataType", { enumerable: true, get: function () { return stream_1.StreamDataType; } });
12
14
  Object.defineProperty(exports, "StreamRole", { enumerable: true, get: function () { return stream_1.StreamRole; } });
@@ -4,3 +4,4 @@ export interface ILogger {
4
4
  warn(message: string, ...meta: any[]): void;
5
5
  debug(message: string, ...meta: any[]): void;
6
6
  }
7
+ export type LogLevel = 'silly' | 'debug' | 'info' | 'warn' | 'error' | 'silent';
@@ -1,2 +1,3 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ ;
@@ -0,0 +1 @@
1
+ export type WorkListTaskType = 'sleep' | 'expire' | 'interrupt' | 'delist';
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/modules/enums.ts CHANGED
@@ -1,35 +1,49 @@
1
- // Engine Constants
2
- export const STATUS_CODE_SUCCESS = 200;
3
- export const STATUS_CODE_PENDING = 202;
4
- export const STATUS_CODE_TIMEOUT = 504;
5
- export const STATUS_CODE_INTERRUPT = 410;
6
- export const OTT_WAIT_TIME = 1000;
7
-
8
- // Stream Constants
9
- export const MAX_RETRIES = 3; //local retry; 10, 100, 1000ms
10
- export const MAX_TIMEOUT_MS = 60000;
11
- export const GRADUATED_INTERVAL_MS = 5000;
12
-
13
- export const BLOCK_DURATION = 15000; //Set to `15` so SIGINT/SIGTERM can interrupt; set to `0` to BLOCK indefinitely
14
- export const TEST_BLOCK_DURATION = 1000; //Set to `1000` so tests can interrupt quickly
15
- export const BLOCK_TIME_MS = process.env.NODE_ENV === 'test' ? TEST_BLOCK_DURATION : BLOCK_DURATION;
16
-
17
- export const XCLAIM_DELAY_MS = 1000 * 60; //max time a message can be unacked before it is claimed by another
18
- export const XCLAIM_COUNT = 3; //max number of times a message can be claimed by another before it is dead-lettered
19
- export const XPENDING_COUNT = 10;
20
-
21
- export const STATUS_CODE_UNACKED = 999;
22
- export const STATUS_CODE_UNKNOWN = 500;
23
- export const STATUS_MESSAGE_UNKNOWN = 'unknown';
24
-
25
- // HotMesh Constants
26
- export const EXPIRE_DURATION = 15; // default expire in seconds; once job state semaphore reaches '0', this is applied to set Redis to expire the job HASH
27
- export const BASE_FIDELITY_SECONDS = 15; // granularity resolution window size
28
- export const TEST_FIDELITY_SECONDS = 5;
29
- export const FIDELITY_SECONDS = process.env.NODE_ENV === 'test' ? TEST_FIDELITY_SECONDS : BASE_FIDELITY_SECONDS
30
-
31
- // DURABLE CONSTANTS
32
- export const DURABLE_EXPIRE_SECONDS = 1;
33
-
34
- // TASK CONSTANTS
35
- export const SCOUT_INTERVAL_SECONDS = 60;
1
+ import { LogLevel } from "../types/logger";
2
+
3
+ // HOTMESH SYSTEM
4
+ export const HMSH_LOGLEVEL = process.env.HMSH_LOGLEVEL as LogLevel || 'info';
5
+
6
+ // STATUS CODES AND MESSAGES
7
+ export const HMSH_CODE_SUCCESS = 200;
8
+ export const HMSH_CODE_PENDING = 202;
9
+ export const HMSH_CODE_NOTFOUND = 404;
10
+ export const HMSH_CODE_INTERRUPT = 410;
11
+ export const HMSH_CODE_UNKNOWN = 500;
12
+ export const HMSH_CODE_TIMEOUT = 504;
13
+ export const HMSH_CODE_UNACKED = 999;
14
+
15
+ export const HMSH_CODE_DURABLE_SLEEPFOR = 592;
16
+ export const HMSH_CODE_DURABLE_INCOMPLETE = 593;
17
+ export const HMSH_CODE_DURABLE_WAITFOR = 594;
18
+ export const HMSH_CODE_DURABLE_TIMEOUT = 596;
19
+ export const HMSH_CODE_DURABLE_MAXED = 597;
20
+ export const HMSH_CODE_DURABLE_FATAL = 598;
21
+ export const HMSH_CODE_DURABLE_RETRYABLE = 599;
22
+
23
+ export const HMSH_STATUS_UNKNOWN = 'unknown';
24
+
25
+ // ENGINE
26
+ export const HMSH_OTT_WAIT_TIME = parseInt(process.env.HMSH_OTT_WAIT_TIME, 10) || 1000;
27
+ export const HMSH_EXPIRE_JOB_SECONDS = parseInt(process.env.HMSH_EXPIRE_JOB_SECONDS, 10) || 1;
28
+
29
+ // STREAM ROUTER
30
+ export const HMSH_MAX_RETRIES = parseInt(process.env.HMSH_MAX_RETRIES, 10) || 3;
31
+ export const HMSH_MAX_TIMEOUT_MS = parseInt(process.env.HMSH_MAX_TIMEOUT_MS, 10) || 60000;
32
+ export const HMSH_GRADUATED_INTERVAL_MS = parseInt(process.env.HMSH_GRADUATED_INTERVAL_MS, 10) || 5000;
33
+
34
+ const BASE_BLOCK_DURATION = 10000; // Modified for clarity
35
+ const TEST_BLOCK_DURATION = 1000; // Modified for clarity
36
+ export const HMSH_BLOCK_TIME_MS = process.env.HMSH_BLOCK_TIME_MS ? parseInt(process.env.HMSH_BLOCK_TIME_MS, 10) : (process.env.NODE_ENV === 'test' ? TEST_BLOCK_DURATION : BASE_BLOCK_DURATION);
37
+
38
+ export const HMSH_XCLAIM_DELAY_MS = parseInt(process.env.HMSH_XCLAIM_DELAY_MS, 10) || 1000 * 60;
39
+ export const HMSH_XCLAIM_COUNT = parseInt(process.env.HMSH_XCLAIM_COUNT, 10) || 3;
40
+ export const HMSH_XPENDING_COUNT = parseInt(process.env.HMSH_XPENDING_COUNT, 10) || 10;
41
+
42
+ // TASK WORKER
43
+ export const HMSH_EXPIRE_DURATION = parseInt(process.env.HMSH_EXPIRE_DURATION, 10) || 1;
44
+
45
+ const BASE_FIDELITY_SECONDS = 5;
46
+ const TEST_FIDELITY_SECONDS = 5;
47
+ export const HMSH_FIDELITY_SECONDS = process.env.HMSH_FIDELITY_SECONDS ? parseInt(process.env.HMSH_FIDELITY_SECONDS, 10) : (process.env.NODE_ENV === 'test' ? TEST_FIDELITY_SECONDS : BASE_FIDELITY_SECONDS);
48
+
49
+ export const HMSH_SCOUT_INTERVAL_SECONDS = parseInt(process.env.HMSH_SCOUT_INTERVAL_SECONDS, 10) || 60;
package/modules/errors.ts CHANGED
@@ -1,9 +1,18 @@
1
1
  import { ActivityDuplex } from "../types/activity";
2
2
  import { CollationFaultType, CollationStage } from "../types/collator";
3
+ import {
4
+ HMSH_CODE_DURABLE_MAXED,
5
+ HMSH_CODE_DURABLE_TIMEOUT,
6
+ HMSH_CODE_DURABLE_FATAL,
7
+ HMSH_CODE_DURABLE_INCOMPLETE,
8
+ HMSH_CODE_NOTFOUND,
9
+ HMSH_CODE_DURABLE_RETRYABLE,
10
+ HMSH_CODE_DURABLE_SLEEPFOR,
11
+ HMSH_CODE_DURABLE_WAITFOR } from "./enums";
3
12
 
4
13
  class GetStateError extends Error {
5
14
  jobId: string;
6
- code: 404;
15
+ code = HMSH_CODE_NOTFOUND;
7
16
  constructor(jobId: string) {
8
17
  super(`${jobId} Not Found`);
9
18
  this.jobId = jobId;
@@ -21,7 +30,7 @@ class DurableIncompleteSignalError extends Error {
21
30
  code: number;
22
31
  constructor(message: string) {
23
32
  super(message);
24
- this.code = 593;
33
+ this.code = HMSH_CODE_DURABLE_INCOMPLETE;
25
34
  }
26
35
  }
27
36
 
@@ -32,7 +41,7 @@ class DurableWaitForSignalError extends Error {
32
41
  constructor(message: string, signals: {signal: string, index: number}[]) {
33
42
  super(message);
34
43
  this.signals = signals;
35
- this.code = 594;
44
+ this.code = HMSH_CODE_DURABLE_WAITFOR;
36
45
  }
37
46
  }
38
47
 
@@ -60,35 +69,35 @@ class DurableSleepForError extends Error {
60
69
  this.duration = duration;
61
70
  this.index = index;
62
71
  this.dimension = dimension;
63
- this.code = 592;
72
+ this.code = HMSH_CODE_DURABLE_SLEEPFOR;
64
73
  }
65
74
  }
66
75
  class DurableTimeoutError extends Error {
67
76
  code: number;
68
77
  constructor(message: string) {
69
78
  super(message);
70
- this.code = 596;
79
+ this.code = HMSH_CODE_DURABLE_TIMEOUT;
71
80
  }
72
81
  }
73
82
  class DurableMaxedError extends Error {
74
83
  code: number;
75
84
  constructor(message: string) {
76
85
  super(message);
77
- this.code = 597;
86
+ this.code = HMSH_CODE_DURABLE_MAXED;
78
87
  }
79
88
  }
80
89
  class DurableFatalError extends Error {
81
90
  code: number;
82
91
  constructor(message: string) {
83
92
  super(message);
84
- this.code = 598;
93
+ this.code = HMSH_CODE_DURABLE_FATAL;
85
94
  }
86
95
  }
87
96
  class DurableRetryError extends Error {
88
97
  code: number;
89
98
  constructor(message: string) {
90
99
  super(message);
91
- this.code = 599;
100
+ this.code = HMSH_CODE_DURABLE_RETRYABLE;
92
101
  }
93
102
  }
94
103
 
package/modules/key.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { KeyStoreParams, KeyType } from '../types/hotmesh';
2
+
1
3
  /**
2
4
  * Keys
3
5
  *
@@ -26,46 +28,7 @@
26
28
  * hmsh:<appid>:sym:vals: -> {hash} list of symbols for job values across all app versions
27
29
  */
28
30
 
29
- //default namespace for hotmesh
30
- const HMNS = "hmsh";
31
-
32
- //these are the entity types that are stored in the key/value store
33
- enum KeyType {
34
- APP = 'APP',
35
- ENGINE_ID = 'ENGINE',
36
- HOOKS = 'HOOKS',
37
- JOB_DEPENDENTS = 'JOB_DEPENDENTS',
38
- JOB_STATE = 'JOB_STATE',
39
- JOB_STATS_GENERAL = 'JOB_STATS_GENERAL',
40
- JOB_STATS_MEDIAN = 'JOB_STATS_MEDIAN',
41
- JOB_STATS_INDEX = 'JOB_STATS_INDEX',
42
- HOTMESH = 'HOTMESH',
43
- QUORUM = 'QUORUM',
44
- SCHEMAS = 'SCHEMAS',
45
- SIGNALS = 'SIGNALS',
46
- STREAMS = 'STREAMS',
47
- SUBSCRIPTIONS = 'SUBSCRIPTIONS',
48
- SUBSCRIPTION_PATTERNS = 'SUBSCRIPTION_PATTERNS',
49
- SYMKEYS = 'SYMKEYS',
50
- SYMVALS = 'SYMVALS',
51
- TIME_RANGE = 'TIME_RANGE',
52
- WORK_ITEMS = 'WORK_ITEMS',
53
- }
54
-
55
- //when minting a key, the following parameters are used to create a unique key per entity
56
- type KeyStoreParams = {
57
- appId?: string; //app id is a uuid for a hotmesh app
58
- engineId?: string; //unique auto-generated guid for an ephemeral engine instance
59
- appVersion?: string; //(e.g. "1.0.0", "1", "1.0")
60
- jobId?: string; //a customer-defined id for job; must be unique for the entire app
61
- activityId?: string; //activity id is a uuid for a given hotmesh app
62
- jobKey?: string; //a customer-defined label for a job that serves to categorize events
63
- dateTime?: string; //UTC date time: YYYY-MM-DDTHH:MM (20203-04-12T00:00); serves as a time-series bucket for the job_key
64
- facet?: string; //data path starting at root with values separated by colons (e.g. "object/type:bar")
65
- topic?: string; //topic name (e.g., "foo" or "" for top-level)
66
- timeValue?: number; //time value (rounded to minute) (for delete range)
67
- scoutType?: 'signal' | 'time'; //a single member of the quorum serves as the 'scout' for the group, triaging tasks for the collective
68
- };
31
+ const HMNS = "hmsh"; //default
69
32
 
70
33
  class KeyService {
71
34
 
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@hotmeshio/hotmesh",
3
- "version": "0.0.38",
3
+ "version": "0.0.40",
4
4
  "description": "Unbreakable Workflows",
5
- "main": "build/index.js",
6
- "types": "build/index.d.ts",
5
+ "main": "./build/index.js",
6
+ "types": "./build/types/index.d.ts",
7
7
  "repository": {
8
8
  "type": "git",
9
9
  "url": "https://github.com/hotmeshio/sdk-typescript.git"
@@ -1,4 +1,4 @@
1
- import { EXPIRE_DURATION } from '../../modules/enums';
1
+ import { HMSH_EXPIRE_DURATION } from '../../modules/enums';
2
2
  import {
3
3
  CollationError,
4
4
  GenerationalError,
@@ -435,7 +435,7 @@ class Activity {
435
435
 
436
436
  initPolicies(context: JobState) {
437
437
  const expire = Pipe.resolve(
438
- this.config.expire ?? EXPIRE_DURATION,
438
+ this.config.expire ?? HMSH_EXPIRE_DURATION,
439
439
  context
440
440
  );
441
441
  context.metadata.expire = expire;
@@ -131,16 +131,25 @@ class Hook extends Activity {
131
131
 
132
132
  async registerHook(multi?: RedisMulti): Promise<string | void> {
133
133
  if (this.config.hook?.topic) {
134
- const taskService = new TaskService(this.store, this.logger);
135
- return await taskService.registerWebHook(this.config.hook.topic, this.context, this.resolveDad(), multi);
134
+ return await this.engine.taskService.registerWebHook(
135
+ this.config.hook.topic,
136
+ this.context,
137
+ this.resolveDad(),
138
+ multi
139
+ );
136
140
  } else if (this.config.sleep) {
137
- const durationInSeconds = Pipe.resolve(this.config.sleep, this.context);
138
- const jobId = this.context.metadata.jid;
139
- const gId = this.context.metadata.gid;
140
- const activityId = this.metadata.aid;
141
- const dId = this.metadata.dad;
142
- await this.engine.taskService.registerTimeHook(jobId, gId, `${activityId}${dId||''}`, 'sleep', durationInSeconds);
143
- return jobId;
141
+ const duration = Pipe.resolve(
142
+ this.config.sleep,
143
+ this.context,
144
+ );
145
+ await this.engine.taskService.registerTimeHook(
146
+ this.context.metadata.jid,
147
+ this.context.metadata.gid,
148
+ `${this.metadata.aid}${this.metadata.dad || ''}`,
149
+ 'sleep',
150
+ duration,
151
+ );
152
+ return this.context.metadata.jid;
144
153
  }
145
154
  }
146
155
 
@@ -47,7 +47,7 @@ class Trigger extends Activity {
47
47
  const multi = this.store.getMulti();
48
48
  await this.setState(multi);
49
49
  await this.setStats(multi);
50
- await this.setDependency(multi);
50
+ await this.registerJobDependency(multi);
51
51
  await multi.exec();
52
52
 
53
53
  telemetry.mapActivityAttributes();
@@ -180,13 +180,17 @@ class Trigger extends Activity {
180
180
  }
181
181
 
182
182
  /**
183
- * Registers this job as a dependent of the parent job
183
+ * Registers this job as a dependent of the parent job; when the
184
+ * parent job is interrupted, this job will be interrupted
184
185
  */
185
- async setDependency(multi?: RedisMulti): Promise<void> {
186
- const depKey = this.config.stats?.parent;
187
- const resolvedDepKey = depKey ? Pipe.resolve(depKey, this.context) : '';
186
+ async registerJobDependency(multi?: RedisMulti): Promise<void> {
187
+ const depKey = this.config.stats?.parent ?? this.context.metadata.pj;
188
+ let resolvedDepKey = depKey ? Pipe.resolve(depKey, this.context) : '';
189
+ if (!resolvedDepKey) {
190
+ resolvedDepKey = this.context.metadata.pj;
191
+ }
188
192
  if (resolvedDepKey) {
189
- await this.store.setDependency(
193
+ await this.store.registerJobDependency(
190
194
  resolvedDepKey,
191
195
  this.context.metadata.tpc,
192
196
  this.context.metadata.jid,
@@ -11,11 +11,12 @@ import { JobState } from '../../types/job';
11
11
  import { KeyService, KeyType } from '../../modules/key';
12
12
  import { Search } from './search';
13
13
  import { StreamStatus } from '../../types';
14
- import { DURABLE_EXPIRE_SECONDS } from '../../modules/enums';
14
+ import { HMSH_LOGLEVEL, HMSH_EXPIRE_JOB_SECONDS } from '../../modules/enums';
15
15
 
16
16
  export class ClientService {
17
17
 
18
18
  connection: Connection;
19
+ topics: string[] = [];
19
20
  options: WorkflowOptions;
20
21
  static instances = new Map<string, HotMesh | Promise<HotMesh>>();
21
22
 
@@ -23,14 +24,22 @@ export class ClientService {
23
24
  this.connection = config.connection;
24
25
  }
25
26
 
26
- getHotMeshClient = async (worflowTopic: string, namespace?: string) => {
27
- //NOTE: every unique topic inits a new engine
28
- if (ClientService.instances.has(worflowTopic)) {
29
- return await ClientService.instances.get(worflowTopic);
27
+ getHotMeshClient = async (workflowTopic: string, namespace?: string) => {
28
+ //use the cached instance
29
+ const instanceId = 'SINGLETON';
30
+ if (ClientService.instances.has(instanceId)) {
31
+ const hotMeshClient = await ClientService.instances.get(instanceId);
32
+ if (!this.topics.includes(workflowTopic)) {
33
+ this.topics.push(workflowTopic);
34
+ await this.createStream(hotMeshClient, workflowTopic, namespace);
35
+ }
36
+ return hotMeshClient;
30
37
  }
31
38
 
39
+ //create and cache an instance
32
40
  const hotMeshClient = HotMesh.init({
33
41
  appId: namespace ?? APP_ID,
42
+ logLevel: HMSH_LOGLEVEL,
34
43
  engine: {
35
44
  redis: {
36
45
  class: this.connection.class,
@@ -38,19 +47,27 @@ export class ClientService {
38
47
  }
39
48
  }
40
49
  });
41
- ClientService.instances.set(worflowTopic, hotMeshClient);
50
+ ClientService.instances.set(instanceId, hotMeshClient);
51
+ await this.createStream(await hotMeshClient, workflowTopic, namespace);
52
+ await this.activateWorkflow(await hotMeshClient, namespace ?? APP_ID);
53
+ return hotMeshClient;
54
+ }
42
55
 
43
- //since the YAML topic is dynamic, it MUST be manually created before use
44
- const store = (await hotMeshClient).engine.store;
45
- const params = { appId: namespace ?? APP_ID, topic: worflowTopic };
56
+ /**
57
+ * Creates a stream (Redis `XGROUP.CREATE`) where events can be published (XADD).
58
+ * It is possible that the worker that will read from this stream channel
59
+ * has not yet been initialized, so this call ensures that the channel
60
+ * exists and is ready to serve as a container for events.
61
+ */
62
+ createStream = async(hotMeshClient: HotMesh, workflowTopic: string, namespace?: string) => {
63
+ const store = hotMeshClient.engine.store;
64
+ const params = { appId: namespace ?? APP_ID, topic: workflowTopic };
46
65
  const streamKey = store.mintKey(KeyType.STREAMS, params);
47
66
  try {
48
67
  await store.xgroup('CREATE', streamKey, 'WORKER', '$', 'MKSTREAM');
49
68
  } catch (err) {
50
69
  //ignore if already exists
51
70
  }
52
- await this.activateWorkflow(await hotMeshClient, namespace ?? APP_ID);
53
- return hotMeshClient;
54
71
  }
55
72
 
56
73
  /**
@@ -105,7 +122,7 @@ export class ClientService {
105
122
  const payload = {
106
123
  arguments: [...options.args],
107
124
  originJobId: options.originJobId,
108
- expire: options.expire ?? DURABLE_EXPIRE_SECONDS,
125
+ expire: options.expire ?? HMSH_EXPIRE_JOB_SECONDS,
109
126
  parentWorkflowId: options.parentWorkflowId,
110
127
  workflowId: options.workflowId || HotMesh.guid(),
111
128
  workflowTopic: workflowTopic,
@@ -155,9 +172,8 @@ export class ClientService {
155
172
  if (options.search?.data) {
156
173
  const searchSessionId = `-search-${HotMesh.guid()}-0`;
157
174
  const search = new Search(options.workflowId, hotMeshClient, searchSessionId);
158
- for (const [key, value] of Object.entries(options.search.data)) {
159
- search.set(key, value);
160
- }
175
+ const entries = Object.entries(options.search.data).flat();
176
+ await search.set(...entries);
161
177
  }
162
178
  return msgId;
163
179
  },
@@ -1,4 +1,4 @@
1
- import { STATUS_CODE_INTERRUPT } from '../../modules/enums';
1
+ import { HMSH_CODE_INTERRUPT } from '../../modules/enums';
2
2
  import { HotMeshService as HotMesh } from '../hotmesh';
3
3
  import { JobInterruptOptions, JobOutput } from '../../types/job';
4
4
  import { StreamError } from '../../types/stream';
@@ -101,7 +101,7 @@ export class WorkflowHandleService {
101
101
  const state = await this.hotMesh.getState(`${this.hotMesh.appId}.execute`, this.workflowId);
102
102
  if (state.metadata.err) {
103
103
  const error = JSON.parse(state.metadata.err) as StreamError;
104
- if (error.code === STATUS_CODE_INTERRUPT || !state.data) {
104
+ if (error.code === HMSH_CODE_INTERRUPT || !state.data) {
105
105
  return reject({ ...error, job_id: this.workflowId });
106
106
  }
107
107
  }
@@ -117,7 +117,7 @@ export class WorkflowHandleService {
117
117
  this.hotMesh.sub(topic, async (topic: string, state: JobOutput) => {
118
118
  if (state.metadata.err) {
119
119
  const error = JSON.parse(state.metadata.err) as StreamError;
120
- if (error.code === STATUS_CODE_INTERRUPT || !state.data) {
120
+ if (error.code === HMSH_CODE_INTERRUPT || !state.data) {
121
121
  return await complete(null, state.metadata.err);
122
122
  }
123
123
  }
@@ -23,6 +23,7 @@ import {
23
23
  StreamData,
24
24
  StreamDataResponse,
25
25
  StreamStatus } from '../../types/stream';
26
+ import { HMSH_LOGLEVEL } from '../../modules/enums';
26
27
 
27
28
  export class WorkerService {
28
29
  static activityRegistry: Registry = {}; //user's activities
@@ -36,7 +37,7 @@ export class WorkerService {
36
37
  return await WorkerService.instances.get(workflowTopic);
37
38
  }
38
39
  const hotMeshClient = HotMesh.init({
39
- logLevel: options?.logLevel as 'debug' ?? 'info',
40
+ logLevel: options?.logLevel ?? HMSH_LOGLEVEL,
40
41
  appId: config.namespace ?? APP_ID,
41
42
  engine: { redis: { ...WorkerService.connection } }
42
43
  });
@@ -121,7 +122,7 @@ export class WorkerService {
121
122
  options: config.connection.options as RedisOptions
122
123
  };
123
124
  const hotMeshWorker = await HotMesh.init({
124
- logLevel: config.options?.logLevel as 'debug' ?? 'info',
125
+ logLevel: config.options?.logLevel ?? HMSH_LOGLEVEL,
125
126
  appId: config.namespace ?? APP_ID,
126
127
  engine: { redis: redisConfig },
127
128
  workers: [
@@ -172,7 +173,7 @@ export class WorkerService {
172
173
  options: config.connection.options as RedisOptions
173
174
  };
174
175
  const hotMeshWorker = await HotMesh.init({
175
- logLevel: config.options?.logLevel as 'debug' ?? 'info',
176
+ logLevel: config.options?.logLevel ?? HMSH_LOGLEVEL,
176
177
  appId: config.namespace ?? APP_ID,
177
178
  engine: { redis: redisConfig },
178
179
  workers: [{
@@ -1,10 +1,10 @@
1
1
  import { KeyType } from '../../modules/key';
2
2
  import {
3
- OTT_WAIT_TIME,
4
- STATUS_CODE_SUCCESS,
5
- STATUS_CODE_PENDING,
6
- STATUS_CODE_TIMEOUT,
7
- DURABLE_EXPIRE_SECONDS } from '../../modules/enums';
3
+ HMSH_OTT_WAIT_TIME,
4
+ HMSH_CODE_SUCCESS,
5
+ HMSH_CODE_PENDING,
6
+ HMSH_CODE_TIMEOUT,
7
+ HMSH_EXPIRE_JOB_SECONDS } from '../../modules/enums';
8
8
  import {
9
9
  formatISODate,
10
10
  getSubscriptionTopic,
@@ -77,6 +77,7 @@ import {
77
77
  StreamError,
78
78
  StreamRole,
79
79
  StreamStatus } from '../../types/stream';
80
+ import { WorkListTaskType } from '../../types/task';
80
81
 
81
82
  class EngineService {
82
83
  namespace: string;
@@ -438,10 +439,10 @@ class EngineService {
438
439
  streamData.code = error.code;
439
440
  } else if (emit) {
440
441
  streamData.status = StreamStatus.PENDING;
441
- streamData.code = STATUS_CODE_PENDING;
442
+ streamData.code = HMSH_CODE_PENDING;
442
443
  } else {
443
444
  streamData.status = StreamStatus.SUCCESS;
444
- streamData.code = STATUS_CODE_SUCCESS;
445
+ streamData.code = HMSH_CODE_SUCCESS;
445
446
  }
446
447
  return (await this.router?.publishMessage(null, streamData)) as string;
447
448
  }
@@ -489,7 +490,7 @@ class EngineService {
489
490
  };
490
491
  return await this.router.publishMessage(null, streamData) as string;
491
492
  }
492
- async hookTime(jobId: string, gId: string, activityId: string, type?: 'sleep'|'expire'|'interrupt'): Promise<string | void> {
493
+ async hookTime(jobId: string, gId: string, activityId: string, type?: WorkListTaskType): Promise<string | void> {
493
494
  if (type === 'interrupt') {
494
495
  return await this.interrupt(
495
496
  activityId, //note: 'activityId' is the actually job topic
@@ -499,7 +500,6 @@ class EngineService {
499
500
  } else if (type === 'expire') {
500
501
  return await this.store.expireJob(jobId, 1);
501
502
  }
502
- //'sleep': parse the activityId into parts
503
503
  const [aid, ...dimensions] = activityId.split(',');
504
504
  const dad = `,${dimensions.join(',')}`;
505
505
  const streamData: StreamData = {
@@ -575,7 +575,7 @@ class EngineService {
575
575
  return await this.subscribe.punsubscribe(KeyType.QUORUM, this.appId, wild);
576
576
  }
577
577
  //publish and await (returns the job and data (if ready)); throws error with jobid if not
578
- async pubsub(topic: string, data: JobData, context?: JobState | null, timeout = OTT_WAIT_TIME): Promise<JobOutput> {
578
+ async pubsub(topic: string, data: JobData, context?: JobState | null, timeout = HMSH_OTT_WAIT_TIME): Promise<JobOutput> {
579
579
  context = {
580
580
  metadata: {
581
581
  ngn: this.guid,
@@ -597,9 +597,10 @@ class EngineService {
597
597
  }
598
598
  });
599
599
  setTimeout(() => {
600
+ //note: job is still active (the subscriber timed out)
600
601
  this.delistJobCallback(jobId);
601
602
  reject({
602
- code: STATUS_CODE_TIMEOUT,
603
+ code: HMSH_CODE_TIMEOUT,
603
604
  message: 'timeout',
604
605
  job_id: jobId
605
606
  } as StreamError);
@@ -685,7 +686,7 @@ class EngineService {
685
686
  * it will be expired immediately.
686
687
  */
687
688
  resolveExpires(context: JobState, options: JobCompletionOptions): number {
688
- return options.expire ?? context.metadata.expire ?? DURABLE_EXPIRE_SECONDS;
689
+ return options.expire ?? context.metadata.expire ?? HMSH_EXPIRE_JOB_SECONDS;
689
690
  }
690
691
 
691
692