@hotmeshio/hotmesh 0.0.42 → 0.0.44

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 (61) hide show
  1. package/build/modules/enums.d.ts +2 -0
  2. package/build/modules/enums.js +4 -1
  3. package/build/modules/utils.js +1 -1
  4. package/build/package.json +1 -1
  5. package/build/services/activities/trigger.js +7 -1
  6. package/build/services/durable/client.d.ts +2 -1
  7. package/build/services/durable/client.js +17 -3
  8. package/build/services/durable/exporter.d.ts +105 -0
  9. package/build/services/durable/exporter.js +374 -0
  10. package/build/services/durable/factory.js +6 -63
  11. package/build/services/durable/handle.d.ts +4 -0
  12. package/build/services/durable/handle.js +5 -0
  13. package/build/services/durable/meshos.js +3 -0
  14. package/build/services/durable/workflow.js +24 -21
  15. package/build/services/engine/index.d.ts +6 -1
  16. package/build/services/engine/index.js +9 -2
  17. package/build/services/exporter/index.d.ts +46 -0
  18. package/build/services/exporter/index.js +126 -0
  19. package/build/services/hotmesh/index.d.ts +4 -1
  20. package/build/services/hotmesh/index.js +6 -0
  21. package/build/services/quorum/index.d.ts +5 -2
  22. package/build/services/quorum/index.js +33 -15
  23. package/build/services/router/index.d.ts +3 -0
  24. package/build/services/router/index.js +3 -0
  25. package/build/services/store/clients/redis.js +1 -0
  26. package/build/services/store/index.d.ts +7 -3
  27. package/build/services/store/index.js +62 -12
  28. package/build/services/task/index.js +5 -1
  29. package/build/services/worker/index.js +5 -4
  30. package/build/types/activity.d.ts +6 -1
  31. package/build/types/exporter.d.ts +51 -0
  32. package/build/types/exporter.js +8 -0
  33. package/build/types/hotmesh.d.ts +1 -1
  34. package/build/types/index.d.ts +1 -0
  35. package/build/types/quorum.d.ts +1 -0
  36. package/build/types/task.d.ts +1 -1
  37. package/modules/enums.ts +4 -0
  38. package/modules/utils.ts +1 -1
  39. package/package.json +1 -1
  40. package/services/activities/trigger.ts +14 -0
  41. package/services/durable/client.ts +19 -4
  42. package/services/durable/exporter.ts +408 -0
  43. package/services/durable/factory.ts +6 -63
  44. package/services/durable/handle.ts +12 -0
  45. package/services/durable/meshos.ts +3 -0
  46. package/services/durable/workflow.ts +24 -22
  47. package/services/engine/index.ts +20 -5
  48. package/services/exporter/index.ts +147 -0
  49. package/services/hotmesh/index.ts +8 -1
  50. package/services/quorum/index.ts +37 -13
  51. package/services/router/index.ts +3 -0
  52. package/services/store/clients/redis.ts +1 -0
  53. package/services/store/index.ts +66 -14
  54. package/services/task/index.ts +4 -1
  55. package/services/worker/index.ts +6 -5
  56. package/types/activity.ts +6 -1
  57. package/types/exporter.ts +61 -0
  58. package/types/hotmesh.ts +1 -1
  59. package/types/index.ts +13 -1
  60. package/types/quorum.ts +1 -0
  61. package/types/task.ts +1 -1
@@ -10,6 +10,7 @@ class Router {
10
10
  constructor(config, stream, store, logger) {
11
11
  this.throttle = 0;
12
12
  this.errorCount = 0;
13
+ this.counts = {};
13
14
  this.currentTimerId = null;
14
15
  this.appId = config.appId;
15
16
  this.guid = config.guid;
@@ -146,6 +147,8 @@ class Router {
146
147
  else {
147
148
  output.metadata.guid = (0, utils_1.guid)();
148
149
  }
150
+ const code = output.code || 200;
151
+ this.counts[code] = (this.counts[code] || 0) + 1;
149
152
  output.type = stream_1.StreamDataType.RESPONSE;
150
153
  return await this.publishMessage(null, output);
151
154
  }
@@ -6,6 +6,7 @@ class RedisStoreService extends index_1.StoreService {
6
6
  constructor(redisClient) {
7
7
  super(redisClient);
8
8
  this.commands = {
9
+ set: 'SET',
9
10
  setnx: 'SETNX',
10
11
  del: 'DEL',
11
12
  expire: 'EXPIRE',
@@ -46,10 +46,12 @@ declare abstract class StoreService<T, U extends AbstractRedisClient> {
46
46
  * check for and process work items in the
47
47
  * time and signal task queues.
48
48
  */
49
- reserveScoutRole(scoutType: 'time' | 'signal', delay?: number): Promise<boolean>;
49
+ reserveScoutRole(scoutType: 'time' | 'signal' | 'activate', delay?: number): Promise<boolean>;
50
+ releaseScoutRole(scoutType: 'time' | 'signal' | 'activate'): Promise<boolean>;
50
51
  getSettings(bCreate?: boolean): Promise<HotMeshSettings>;
51
52
  setSettings(manifest: HotMeshSettings): Promise<any>;
52
53
  reserveSymbolRange(target: string, size: number, type: 'JOB' | 'ACTIVITY'): Promise<[number, number, Symbols]>;
54
+ getAllSymbols(): Promise<Symbols>;
53
55
  getSymbols(activityId: string): Promise<Symbols>;
54
56
  addSymbols(activityId: string, symbols: Symbols): Promise<boolean>;
55
57
  seedSymbols(target: string, type: 'JOB' | 'ACTIVITY', startIndex: number): StringStringType;
@@ -67,7 +69,7 @@ declare abstract class StoreService<T, U extends AbstractRedisClient> {
67
69
  * when `originJobId` is interrupted/expired, the items in the
68
70
  * list (added via RPUSH) will be interrupted/expired (removed via LPOPed).
69
71
  */
70
- registerJobDependency(originJobId: string, topic: string, jobId: string, gId: string, multi?: U): Promise<any>;
72
+ registerJobDependency(depType: WorkListTaskType, originJobId: string, topic: string, jobId: string, gId: string, multi?: U): Promise<any>;
71
73
  /**
72
74
  * Ensures a `hook signal` is delisted when its parent activity/job
73
75
  * is interrupted/expired.
@@ -86,6 +88,7 @@ declare abstract class StoreService<T, U extends AbstractRedisClient> {
86
88
  */
87
89
  getQueryState(jobId: string, fields: string[]): Promise<StringAnyType>;
88
90
  getState(jobId: string, consumes: Consumes, dIds: StringStringType): Promise<[StringAnyType, number] | undefined>;
91
+ getRaw(jobId: string): Promise<StringStringType>;
89
92
  /**
90
93
  * collate is a generic method for incrementing a value in a hash
91
94
  * in order to track their progress during processing.
@@ -123,6 +126,7 @@ declare abstract class StoreService<T, U extends AbstractRedisClient> {
123
126
  * is a standard `expire` or an `interrupt`
124
127
  */
125
128
  registerDependenciesForCleanup(jobId: string, deletionTime: number, options: JobCompletionOptions): Promise<void>;
129
+ getDependencies(jobId: string): Promise<string[]>;
126
130
  /**
127
131
  * registers a hook activity to be awakened (uses ZSET to
128
132
  * store the 'sleep group' and LIST to store the events
@@ -139,7 +143,7 @@ declare abstract class StoreService<T, U extends AbstractRedisClient> {
139
143
  * generic LIST (lists typically contain target job ids)
140
144
  * @param {string} listKey - for example `::INTERRUPT::job123` or `job123`
141
145
  */
142
- resolveTaskKeyContext(listKey: string): [('sleep' | 'expire' | 'interrupt' | 'delist'), string];
146
+ resolveTaskKeyContext(listKey: string): [WorkListTaskType, string];
143
147
  /**
144
148
  * Interrupts a job and sets sets a job error (410), if 'throw'!=false.
145
149
  * This method is called by the engine and not by an activity and is
@@ -33,6 +33,7 @@ const errors_1 = require("../../modules/errors");
33
33
  class StoreService {
34
34
  constructor(redisClient) {
35
35
  this.commands = {
36
+ set: 'set',
36
37
  setnx: 'setnx',
37
38
  del: 'del',
38
39
  expire: 'expire',
@@ -106,12 +107,13 @@ class StoreService {
106
107
  */
107
108
  async reserveScoutRole(scoutType, delay = enums_1.HMSH_SCOUT_INTERVAL_SECONDS) {
108
109
  const key = this.mintKey(key_1.KeyType.WORK_ITEMS, { appId: this.appId, scoutType });
109
- const success = await this.redisClient[this.commands.setnx](key, `${scoutType}:${(0, utils_1.formatISODate)(new Date())}`);
110
- if (this.isSuccessful(success)) {
111
- await this.redisClient[this.commands.expire](key, delay - 1);
112
- return true;
113
- }
114
- return false;
110
+ const success = await this.exec('SET', key, `${scoutType}:${(0, utils_1.formatISODate)(new Date())}`, 'NX', 'EX', `${delay - 1}`);
111
+ return this.isSuccessful(success);
112
+ }
113
+ async releaseScoutRole(scoutType) {
114
+ const key = this.mintKey(key_1.KeyType.WORK_ITEMS, { appId: this.appId, scoutType });
115
+ const success = await this.exec('DEL', key);
116
+ return this.isSuccessful(success);
115
117
  }
116
118
  async getSettings(bCreate = false) {
117
119
  let settings = this.cache?.getSettings();
@@ -162,6 +164,35 @@ class StoreService {
162
164
  return [actualLowerLimit, upperLimit, symbols];
163
165
  }
164
166
  }
167
+ async getAllSymbols() {
168
+ //get hash with all reserved symbol ranges
169
+ const rangeKey = this.mintKey(key_1.KeyType.SYMKEYS, { appId: this.appId });
170
+ const ranges = await this.redisClient[this.commands.hgetall](rangeKey);
171
+ const rangeKeys = Object.keys(ranges).sort();
172
+ delete rangeKeys[':cursor'];
173
+ const multi = this.getMulti();
174
+ for (const rangeKey of rangeKeys) {
175
+ const symbolKey = this.mintKey(key_1.KeyType.SYMKEYS, { activityId: rangeKey, appId: this.appId });
176
+ multi[this.commands.hgetall](symbolKey);
177
+ }
178
+ const results = await multi.exec();
179
+ const symbolSets = {};
180
+ results.forEach((result, index) => {
181
+ if (result) {
182
+ let vals;
183
+ if (Array.isArray(result) && result.length === 2) {
184
+ vals = result[1];
185
+ }
186
+ else {
187
+ vals = result;
188
+ }
189
+ for (const [key, value] of Object.entries(vals)) {
190
+ symbolSets[value] = key.startsWith(rangeKeys[index]) ? key : `${rangeKeys[index]}/${key}`;
191
+ }
192
+ }
193
+ });
194
+ return symbolSets;
195
+ }
165
196
  async getSymbols(activityId) {
166
197
  let symbols = this.cache.getSymbols(this.appId, activityId);
167
198
  if (symbols) {
@@ -312,15 +343,15 @@ class StoreService {
312
343
  * when `originJobId` is interrupted/expired, the items in the
313
344
  * list (added via RPUSH) will be interrupted/expired (removed via LPOPed).
314
345
  */
315
- async registerJobDependency(originJobId, topic, jobId, gId, multi) {
346
+ async registerJobDependency(depType, originJobId, topic, jobId, gId, multi) {
316
347
  const privateMulti = multi || this.getMulti();
317
348
  const dependencyParams = {
318
349
  appId: this.appId,
319
350
  jobId: originJobId,
320
351
  };
321
352
  const depKey = this.mintKey(key_1.KeyType.JOB_DEPENDENTS, dependencyParams);
322
- //tasks have '4' segments
323
- const expireTask = `expire::${topic}::${gId}::${jobId}`;
353
+ //items listed as job dependencies have different relationships
354
+ const expireTask = `${depType}::${topic}::${gId}::${jobId}`;
324
355
  privateMulti[this.commands.rpush](depKey, expireTask);
325
356
  if (!multi) {
326
357
  return await privateMulti.exec();
@@ -485,6 +516,14 @@ class StoreService {
485
516
  throw new errors_1.GetStateError(jobId);
486
517
  }
487
518
  }
519
+ async getRaw(jobId) {
520
+ const jobKey = this.mintKey(key_1.KeyType.JOB_STATE, { appId: this.appId, jobId });
521
+ const job = await this.redisClient[this.commands.hgetall](jobKey);
522
+ if (!job) {
523
+ throw new errors_1.GetStateError(jobId);
524
+ }
525
+ return job;
526
+ }
488
527
  /**
489
528
  * collate is a generic method for incrementing a value in a hash
490
529
  * in order to track their progress during processing.
@@ -718,6 +757,11 @@ class StoreService {
718
757
  const zsetKey = this.mintKey(key_1.KeyType.TIME_RANGE, { appId: this.appId });
719
758
  await this.zAdd(zsetKey, deletionTime.toString(), depKeyContext);
720
759
  }
760
+ async getDependencies(jobId) {
761
+ const depParams = { appId: this.appId, jobId };
762
+ const depKey = this.mintKey(key_1.KeyType.JOB_DEPENDENTS, depParams);
763
+ return this.redisClient[this.commands.lrange](depKey, 0, -1);
764
+ }
721
765
  /**
722
766
  * registers a hook activity to be awakened (uses ZSET to
723
767
  * store the 'sleep group' and LIST to store the events
@@ -740,12 +784,18 @@ class StoreService {
740
784
  let [pType, pKey] = this.resolveTaskKeyContext(listKey);
741
785
  const timeEvent = await this.redisClient[this.commands.lpop](pKey);
742
786
  if (timeEvent) {
743
- //there are 4 time-related task
744
- //1) sleep (awaken), 2) expire, 3) interrupt, 4) delist
745
- const [type, activityId, gId, ...jobId] = timeEvent.split('::');
787
+ //there are task types
788
+ //1) sleep (awaken), 2) expire (OR expire-child), 3) interrupt, 4) delist, 5) child (just an index helper; no work to do)
789
+ let [type, activityId, gId, ...jobId] = timeEvent.split('::');
746
790
  if (type === 'delist') {
747
791
  pType = 'delist';
748
792
  }
793
+ else if (type === 'child') {
794
+ pType = 'child';
795
+ }
796
+ else if (type === 'expire-child') {
797
+ type = 'expire'; //use the same logic as 'expire'
798
+ }
749
799
  return [listKey, jobId.join('::'), gId, activityId, pType];
750
800
  }
751
801
  await this.redisClient[this.commands.zrem](zsetKey, listKey);
@@ -75,7 +75,11 @@ class TaskService {
75
75
  const workListTask = await this.store.getNextTask(listKey);
76
76
  if (Array.isArray(workListTask)) {
77
77
  const [listKey, target, gId, activityId, type] = workListTask;
78
- if (type === 'delist') {
78
+ if (type === 'child') {
79
+ //continue; this child is listed here for convenience, but
80
+ // will be expired by an origin ancestor and is listed there
81
+ }
82
+ else if (type === 'delist') {
79
83
  //delist the signalKey (target)
80
84
  const key = this.store.mintKey(hotmesh_1.KeyType.SIGNALS, { appId: this.store.appId });
81
85
  await this.store.redisClient[this.store.commands.hdel](key, target);
@@ -2,16 +2,16 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.WorkerService = void 0;
4
4
  const key_1 = require("../../modules/key");
5
+ const utils_1 = require("../../modules/utils");
6
+ const connector_1 = require("../connector");
5
7
  const router_1 = require("../router");
6
- const redis_1 = require("../store/clients/redis");
7
8
  const ioredis_1 = require("../store/clients/ioredis");
8
- const redis_2 = require("../stream/clients/redis");
9
+ const redis_1 = require("../store/clients/redis");
9
10
  const ioredis_2 = require("../stream/clients/ioredis");
11
+ const redis_2 = require("../stream/clients/redis");
10
12
  const ioredis_3 = require("../sub/clients/ioredis");
11
13
  const redis_3 = require("../sub/clients/redis");
12
14
  const stream_1 = require("../../types/stream");
13
- const utils_1 = require("../../modules/utils");
14
- const connector_1 = require("../connector");
15
15
  class WorkerService {
16
16
  constructor() {
17
17
  this.reporting = false;
@@ -114,6 +114,7 @@ class WorkerService {
114
114
  app_id: this.appId,
115
115
  worker_topic: this.topic,
116
116
  stream: this.stream.mintKey(key_1.KeyType.STREAMS, params),
117
+ counts: this.router.counts,
117
118
  };
118
119
  }
119
120
  this.store.publish(key_1.KeyType.QUORUM, {
@@ -33,13 +33,18 @@ interface Measure {
33
33
  }
34
34
  interface TriggerActivityStats {
35
35
  /**
36
- * parent job; including this allows the parent's
36
+ * dependent parent job id; including this allows the parent's
37
37
  * expiration/interruption events to cascade; set
38
38
  * `expire` in the YAML for the dependent graph
39
39
  * to 0 and provide the parent for dependent,
40
40
  * cascading interruption and cleanup
41
41
  */
42
42
  parent?: string;
43
+ /**
44
+ * adjacent parent job id; this is the actual adjacent
45
+ * parent in the graph, but it is not used for cascading expiration
46
+ */
47
+ adjacent?: string;
43
48
  id?: {
44
49
  [key: string]: unknown;
45
50
  } | string;
@@ -0,0 +1,51 @@
1
+ import { StringAnyType } from "./serializer";
2
+ export type ExportItem = [(string | null), string, any];
3
+ export interface ExportOptions {
4
+ }
5
+ export type JobAction = {
6
+ cursor: number;
7
+ items: ExportItem[];
8
+ };
9
+ export interface JobActionExport {
10
+ hooks: {
11
+ [key: string]: JobAction;
12
+ };
13
+ main: JobAction;
14
+ }
15
+ export interface ActivityAction {
16
+ action: string;
17
+ target: string;
18
+ }
19
+ export interface JobTimeline {
20
+ activity: string;
21
+ dimension: string;
22
+ duplex: 'entry' | 'exit';
23
+ timestamp: string;
24
+ actions?: ActivityAction[];
25
+ }
26
+ export interface DependencyExport {
27
+ type: string;
28
+ topic: string;
29
+ gid: string;
30
+ jid: string;
31
+ }
32
+ export interface ExportTransitions {
33
+ [key: string]: string[];
34
+ }
35
+ export interface ExportCycles {
36
+ [key: string]: string[];
37
+ }
38
+ export interface DurableJobExport {
39
+ data: StringAnyType;
40
+ dependencies: DependencyExport[];
41
+ state: StringAnyType;
42
+ status: string;
43
+ timeline: JobTimeline[];
44
+ transitions: ExportTransitions;
45
+ cycles: ExportCycles;
46
+ }
47
+ export interface JobExport {
48
+ dependencies: DependencyExport[];
49
+ process: StringAnyType;
50
+ status: string;
51
+ }
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ ;
4
+ ;
5
+ ;
6
+ ;
7
+ ;
8
+ ;
@@ -42,7 +42,7 @@ type KeyStoreParams = {
42
42
  facet?: string;
43
43
  topic?: string;
44
44
  timeValue?: number;
45
- scoutType?: 'signal' | 'time';
45
+ scoutType?: 'signal' | 'time' | 'activate';
46
46
  };
47
47
  type HotMesh = typeof HotMeshService;
48
48
  type RedisConfig = {
@@ -4,6 +4,7 @@ export { AsyncSignal } from './async';
4
4
  export { CacheMode } from './cache';
5
5
  export { CollationFaultType, CollationStage } from './collator';
6
6
  export { ActivityConfig, ActivityWorkflowDataType, ClientConfig, ContextType, ConnectionConfig, Connection, ProxyType, Registry, SignalOptions, FindOptions, FindWhereOptions, FindWhereQuery, HookOptions, MeshOSActivityOptions, MeshOSWorkerOptions, MeshOSClassConfig, MeshOSConfig, MeshOSOptions, WorkflowConfig, WorkerConfig, WorkerOptions, WorkflowContext, WorkflowSearchOptions, WorkflowDataType, WorkflowOptions, } from './durable';
7
+ export { ActivityAction, DependencyExport, DurableJobExport, ExportCycles, ExportItem, ExportOptions, ExportTransitions, JobAction, JobExport, JobActionExport, JobTimeline } from './exporter';
7
8
  export { HookCondition, HookConditions, HookGate, HookInterface, HookRule, HookRules, HookSignal } from './hook';
8
9
  export { RedisClientType as IORedisClientType, RedisMultiType as IORedisMultiType } from './ioredisclient';
9
10
  export { ILogger } from './logger';
@@ -6,6 +6,7 @@ export interface QuorumProfile {
6
6
  worker_topic?: string;
7
7
  stream?: string;
8
8
  stream_depth?: number;
9
+ counts?: Record<string, number>;
9
10
  }
10
11
  export interface PingMessage {
11
12
  type: 'ping';
@@ -1 +1 @@
1
- export type WorkListTaskType = 'sleep' | 'expire' | 'interrupt' | 'delist';
1
+ export type WorkListTaskType = 'sleep' | 'expire' | 'expire-child' | 'interrupt' | 'delist' | 'child';
package/modules/enums.ts CHANGED
@@ -22,6 +22,10 @@ export const HMSH_CODE_DURABLE_RETRYABLE = 599;
22
22
 
23
23
  export const HMSH_STATUS_UNKNOWN = 'unknown';
24
24
 
25
+ // QUORUM
26
+ export const HMSH_QUORUM_DELAY_MS = 250;
27
+ export const HMSH_ACTIVATION_MAX_RETRY = 3;
28
+
25
29
  // ENGINE
26
30
  export const HMSH_OTT_WAIT_TIME = parseInt(process.env.HMSH_OTT_WAIT_TIME, 10) || 1000;
27
31
  export const HMSH_EXPIRE_JOB_SECONDS = parseInt(process.env.HMSH_EXPIRE_JOB_SECONDS, 10) || 1;
package/modules/utils.ts CHANGED
@@ -10,7 +10,7 @@ export async function sleepFor(ms: number) {
10
10
  }
11
11
 
12
12
  export function guid(): string {
13
- return nanoid();
13
+ return nanoid().replace(/[_-]/g, '0');
14
14
  }
15
15
 
16
16
  export function deterministicRandom(seed: number): number {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotmeshio/hotmesh",
3
- "version": "0.0.42",
3
+ "version": "0.0.44",
4
4
  "description": "Unbreakable Workflows",
5
5
  "main": "./build/index.js",
6
6
  "types": "./build/index.d.ts",
@@ -186,11 +186,15 @@ class Trigger extends Activity {
186
186
  async registerJobDependency(multi?: RedisMulti): Promise<void> {
187
187
  const depKey = this.config.stats?.parent ?? this.context.metadata.pj;
188
188
  let resolvedDepKey = depKey ? Pipe.resolve(depKey, this.context) : '';
189
+ const adjKey = this.config.stats?.adjacent;
190
+ let resolvedAdjKey = depKey ? Pipe.resolve(adjKey, this.context) : '';
189
191
  if (!resolvedDepKey) {
190
192
  resolvedDepKey = this.context.metadata.pj;
191
193
  }
192
194
  if (resolvedDepKey) {
195
+ const isParentOrigin = (resolvedDepKey === this.context.metadata.pj) || (resolvedDepKey === resolvedAdjKey);
193
196
  await this.store.registerJobDependency(
197
+ isParentOrigin ? 'expire-child' : 'expire',
194
198
  resolvedDepKey,
195
199
  this.context.metadata.tpc,
196
200
  this.context.metadata.jid,
@@ -198,6 +202,16 @@ class Trigger extends Activity {
198
202
  multi,
199
203
  );
200
204
  }
205
+ if (resolvedAdjKey && resolvedAdjKey !== resolvedDepKey) {
206
+ await this.store.registerJobDependency(
207
+ 'child',
208
+ resolvedAdjKey,
209
+ this.context.metadata.tpc,
210
+ this.context.metadata.jid,
211
+ this.context.metadata.gid,
212
+ multi,
213
+ );
214
+ }
201
215
  }
202
216
 
203
217
  async setStats(multi?: RedisMulti): Promise<void> {
@@ -11,13 +11,14 @@ 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 { HMSH_LOGLEVEL, HMSH_EXPIRE_JOB_SECONDS } from '../../modules/enums';
14
+ import { HMSH_LOGLEVEL, HMSH_EXPIRE_JOB_SECONDS, HMSH_QUORUM_DELAY_MS } from '../../modules/enums';
15
+ import { sleepFor } from '../../modules/utils';
15
16
 
16
17
  export class ClientService {
17
18
 
18
19
  connection: Connection;
19
- topics: string[] = [];
20
20
  options: WorkflowOptions;
21
+ static topics: string[] = [];
21
22
  static instances = new Map<string, HotMesh | Promise<HotMesh>>();
22
23
 
23
24
  constructor(config: ClientConfig) {
@@ -29,8 +30,9 @@ export class ClientService {
29
30
  const instanceId = 'SINGLETON';
30
31
  if (ClientService.instances.has(instanceId)) {
31
32
  const hotMeshClient = await ClientService.instances.get(instanceId);
32
- if (!this.topics.includes(workflowTopic)) {
33
- this.topics.push(workflowTopic);
33
+ await this.verifyWorkflowActive(hotMeshClient, namespace ?? APP_ID);
34
+ if (!ClientService.topics.includes(workflowTopic)) {
35
+ ClientService.topics.push(workflowTopic);
34
36
  await this.createStream(hotMeshClient, workflowTopic, namespace);
35
37
  }
36
38
  return hotMeshClient;
@@ -196,6 +198,19 @@ export class ClientService {
196
198
  }
197
199
  }
198
200
 
201
+ async verifyWorkflowActive(hotMesh: HotMesh, appId = APP_ID, count = 0): Promise<boolean> {
202
+ const app = await hotMesh.engine.store.getApp(appId);
203
+ const appVersion = app?.version as unknown as number;
204
+ if(isNaN(appVersion)) {
205
+ if (count > 10) {
206
+ throw new Error('Workflow failed to activate');
207
+ }
208
+ await sleepFor(HMSH_QUORUM_DELAY_MS * 2);
209
+ return await this.verifyWorkflowActive(hotMesh, appId, count + 1);
210
+ }
211
+ return true;
212
+ }
213
+
199
214
  async activateWorkflow(hotMesh: HotMesh, appId = APP_ID, version = APP_VERSION): Promise<void> {
200
215
  const app = await hotMesh.engine.store.getApp(appId);
201
216
  const appVersion = app?.version as unknown as number;