@hotmeshio/hotmesh 0.0.17 → 0.0.18

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/build/modules/utils.d.ts +3 -0
  2. package/build/modules/utils.js +10 -1
  3. package/build/package.json +1 -1
  4. package/build/services/activities/activity.d.ts +4 -12
  5. package/build/services/activities/activity.js +14 -156
  6. package/build/services/activities/hook.d.ts +20 -0
  7. package/build/services/activities/hook.js +124 -0
  8. package/build/services/activities/index.d.ts +2 -0
  9. package/build/services/activities/index.js +2 -0
  10. package/build/services/collator/index.js +0 -1
  11. package/build/services/compiler/deployer.d.ts +2 -0
  12. package/build/services/compiler/deployer.js +29 -2
  13. package/build/services/durable/client.d.ts +8 -1
  14. package/build/services/durable/client.js +46 -0
  15. package/build/services/durable/factory.js +11 -10
  16. package/build/services/durable/search.d.ts +15 -0
  17. package/build/services/durable/search.js +45 -0
  18. package/build/services/durable/workflow.d.ts +1 -0
  19. package/build/services/durable/workflow.js +34 -0
  20. package/build/services/engine/index.d.ts +7 -2
  21. package/build/services/engine/index.js +2 -1
  22. package/build/services/store/clients/ioredis.d.ts +1 -0
  23. package/build/services/store/clients/ioredis.js +12 -0
  24. package/build/services/store/clients/redis.d.ts +1 -0
  25. package/build/services/store/clients/redis.js +3 -0
  26. package/build/services/store/index.d.ts +1 -0
  27. package/build/services/telemetry/index.js +2 -1
  28. package/build/types/activity.d.ts +6 -3
  29. package/build/types/durable.d.ts +10 -1
  30. package/build/types/hook.d.ts +1 -0
  31. package/build/types/index.d.ts +2 -2
  32. package/modules/utils.ts +11 -0
  33. package/package.json +1 -1
  34. package/services/activities/activity.ts +15 -167
  35. package/services/activities/hook.ts +149 -0
  36. package/services/activities/index.ts +2 -0
  37. package/services/collator/index.ts +0 -1
  38. package/services/compiler/deployer.ts +32 -2
  39. package/services/durable/client.ts +50 -2
  40. package/services/durable/factory.ts +11 -10
  41. package/services/durable/search.ts +54 -0
  42. package/services/durable/workflow.ts +32 -1
  43. package/services/engine/index.ts +8 -4
  44. package/services/store/clients/ioredis.ts +13 -0
  45. package/services/store/clients/redis.ts +4 -0
  46. package/services/store/index.ts +1 -0
  47. package/services/telemetry/index.ts +2 -1
  48. package/types/activity.ts +7 -2
  49. package/types/durable.ts +8 -0
  50. package/types/hook.ts +1 -0
  51. package/types/index.ts +2 -0
@@ -0,0 +1,54 @@
1
+ import { HotMeshService as HotMesh } from '../hotmesh'
2
+ import { RedisClient, RedisMulti } from '../../types/redis';
3
+ import { StoreService } from '../store';
4
+ import { KeyService, KeyType } from '../../modules/key';
5
+
6
+ export class Search {
7
+ jobId: string;
8
+ hotMeshClient: HotMesh;
9
+ store: StoreService<RedisClient, RedisMulti> | null;
10
+
11
+ safeKey(key:string): string {
12
+ //note: protect the execution namespace with a prefix,
13
+ //so its design never conflicts with the hotmesh keyspace
14
+ return `_${key}`;
15
+ }
16
+
17
+ constructor(workflowId: string, hotMeshClient: HotMesh) {
18
+ const keyParams = {
19
+ appId: hotMeshClient.appId,
20
+ jobId: ''
21
+ }
22
+ const hotMeshPrefix = KeyService.mintKey(hotMeshClient.namespace, KeyType.JOB_STATE, keyParams);
23
+ this.jobId = `${hotMeshPrefix}${workflowId}`;
24
+ this.hotMeshClient = hotMeshClient;
25
+ this.store = hotMeshClient.engine.store as StoreService<RedisClient, RedisMulti>;
26
+ }
27
+
28
+ async set(key: string, value: string): Promise<void> {
29
+ await this.store.exec('HSET', this.jobId, this.safeKey(key), value.toString());
30
+ }
31
+
32
+ async get(key: string): Promise<string> {
33
+ try {
34
+ return await this.store.exec('HGET',this.jobId, this.safeKey(key)) as string;
35
+ } catch (err) {
36
+ this.hotMeshClient.logger.error('durable-search-get-error', { err });
37
+ return '';
38
+ }
39
+ }
40
+
41
+ async del(key: string): Promise<void> {
42
+ await this.store.exec('HDEL', this.jobId, this.safeKey(key));
43
+ }
44
+
45
+ async incr(key: string, val: number): Promise<number> {
46
+ return Number(await this.store.exec('HINCRBYFLOAT', this.jobId, this.safeKey(key), val.toString()) as string);
47
+ }
48
+
49
+ async mult(key: string, val: number): Promise<number> {
50
+ const log = Math.log(val);
51
+ const logTotal = Number(await this.store.exec('HINCRBYFLOAT', this.jobId, this.safeKey(key), log.toString()) as string);
52
+ return Math.exp(logTotal);
53
+ }
54
+ }
@@ -4,10 +4,11 @@ import { asyncLocalStorage } from './asyncLocalStorage';
4
4
  import { WorkerService } from './worker';
5
5
  import { ClientService as Client } from './client';
6
6
  import { ConnectionService as Connection } from './connection';
7
- import { ActivityConfig, ProxyType, WorkflowOptions } from "../../types/durable";
7
+ import { ActivityConfig, ProxyType, WorkflowConfig, WorkflowOptions, WorkflowSearchOptions } from "../../types/durable";
8
8
  import { JobOutput, JobState } from '../../types';
9
9
  import { ACTIVITY_PUBLISHES_TOPIC, ACTIVITY_SUBSCRIBES_TOPIC, SLEEP_SUBSCRIBES_TOPIC, WFS_SUBSCRIBES_TOPIC } from './factory';
10
10
  import { DurableIncompleteSignalError, DurableSleepError, DurableWaitForSignalError } from '../../modules/errors';
11
+ import { Search } from './search';
11
12
 
12
13
  /*
13
14
  `proxyActivities` returns a wrapped instance of the
@@ -97,6 +98,36 @@ export class WorkflowService {
97
98
  return proxy;
98
99
  }
99
100
 
101
+ static async data(command: 'del' | 'get' | 'set' | 'incr' | 'mult', ...args: string[]): Promise<number | boolean | string> {
102
+ const store = asyncLocalStorage.getStore();
103
+ if (!store) {
104
+ throw new Error('durable-store-not-found');
105
+ }
106
+ const workflowId = store.get('workflowId');
107
+ const workflowTopic = store.get('workflowTopic');
108
+
109
+ try {
110
+ const hotMeshClient = await WorkerService.getHotMesh(workflowTopic);
111
+ const search = new Search(workflowId, hotMeshClient);
112
+ if (command === 'get') {
113
+ return await search.get(args[0]) as string;
114
+ } else if (command === 'set') {
115
+ await search.set(args[0], args[1]);
116
+ return true;
117
+ } else if (command === 'del') {
118
+ await search.del(args[0]);
119
+ return true;
120
+ } else if (command === 'incr') {
121
+ return await search.incr(args[0], Number(args[1])) as number;
122
+ } else if (command === 'mult') {
123
+ return await search.mult(args[0], Number(args[1])) as number;
124
+ }
125
+ } catch (e) {
126
+ console.error(e);
127
+ return '';
128
+ }
129
+ }
130
+
100
131
  static async sleep(duration: string): Promise<number> {
101
132
  const seconds = ms(duration) / 1000;
102
133
 
@@ -3,10 +3,13 @@ import {
3
3
  formatISODate,
4
4
  getSubscriptionTopic,
5
5
  identifyRedisType,
6
+ polyfill,
6
7
  restoreHierarchy } from '../../modules/utils';
7
8
  import Activities from '../activities';
8
- import { Activity } from '../activities/activity';
9
9
  import { Await } from '../activities/await';
10
+ import { Cycle } from '../activities/cycle';
11
+ import { Hook } from '../activities/hook';
12
+ import { Signal } from '../activities/signal';
10
13
  import { Worker } from '../activities/worker';
11
14
  import { Trigger } from '../activities/trigger';
12
15
  import { CompilerService } from '../compiler';
@@ -235,9 +238,10 @@ class EngineService {
235
238
  }
236
239
 
237
240
  // ************* METADATA/MODEL METHODS *************
238
- async initActivity(topic: string, data: JobData = {}, context?: JobState): Promise<Activity> {
241
+ async initActivity(topic: string, data: JobData = {}, context?: JobState): Promise<Await|Cycle|Hook|Signal|Trigger|Worker> {
239
242
  const [activityId, schema] = await this.getSchema(topic);
240
- const ActivityHandler = Activities[schema.type];
243
+ polyfill
244
+ const ActivityHandler = Activities[polyfill.resolveActivityType(schema.type)];
241
245
  if (ActivityHandler) {
242
246
  const utc = formatISODate(new Date());
243
247
  const metadata: ActivityMetadata = {
@@ -334,7 +338,7 @@ class EngineService {
334
338
  data: streamData.data,
335
339
  };
336
340
  if (streamData.type === StreamDataType.TIMEHOOK || streamData.type === StreamDataType.WEBHOOK || streamData.type === StreamDataType.TRANSITION) {
337
- const activityHandler = await this.initActivity(`.${streamData.metadata.aid}`, context.data, context as JobState) as Activity;
341
+ const activityHandler = await this.initActivity(`.${streamData.metadata.aid}`, context.data, context as JobState) as Hook;
338
342
  if (streamData.type === StreamDataType.TIMEHOOK) {
339
343
  await activityHandler.processTimeHookEvent(streamData.metadata.jid);
340
344
  } else if (streamData.type === StreamDataType.TRANSITION) {
@@ -5,6 +5,7 @@ import { Cache } from '../cache';
5
5
  import { StoreService } from '../index';
6
6
  import { RedisClientType, RedisMultiType } from '../../../types/ioredisclient';
7
7
  import { ReclaimedMessageType } from '../../../types/stream';
8
+ import { type } from 'os';
8
9
 
9
10
  class IORedisStoreService extends StoreService<RedisClientType, RedisMultiType> {
10
11
  redisClient: RedisClientType;
@@ -22,6 +23,18 @@ class IORedisStoreService extends StoreService<RedisClientType, RedisMultiType>
22
23
  return this.redisClient.multi();
23
24
  }
24
25
 
26
+ async exec(...args: any[]): Promise<string|string[]|string[][]> {
27
+ const response = await this.redisClient.call.apply(this.redisClient, args as any);
28
+ if (typeof response === 'string') {
29
+ return response as string;
30
+ } else if (Array.isArray(response)) {
31
+ if (Array.isArray(response[0])) {
32
+ return response as string[][];
33
+ }
34
+ return response as string[];
35
+ }
36
+ }
37
+
25
38
  hGetAllResult(result: any) {
26
39
  //ioredis response signature is [null, {}] or [null, null]
27
40
  return result[1];
@@ -50,6 +50,10 @@ class RedisStoreService extends StoreService<RedisClientType, RedisMultiType> {
50
50
  return multi as unknown as RedisMultiType;
51
51
  }
52
52
 
53
+ async exec(...args: any[]): Promise<string|string[]|string[][]> {
54
+ return await this.redisClient.sendCommand(args);
55
+ }
56
+
53
57
  async publish(keyType: KeyType.QUORUM, message: Record<string, any>, appId: string, engineId?: string): Promise<boolean> {
54
58
  const topic = this.mintKey(keyType, { appId, engineId });
55
59
  const status: number = await this.redisClient.publish(topic, JSON.stringify(message));
@@ -70,6 +70,7 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
70
70
 
71
71
  //todo: standardize signatures and move concrete methods to this class
72
72
  abstract getMulti(): U;
73
+ abstract exec(...args: any[]): Promise<string|string[]|string[][]>;
73
74
  abstract publish(
74
75
  keyType: KeyType.QUORUM,
75
76
  message: Record<string, any>,
@@ -18,6 +18,7 @@ import {
18
18
  Context,
19
19
  context,
20
20
  SpanStatusCode } from '../../types/telemetry';
21
+ import { polyfill } from '../../modules/utils';
21
22
 
22
23
  class TelemetryService {
23
24
  span: Span;
@@ -253,7 +254,7 @@ class TelemetryService {
253
254
  if (config.type === 'trigger') {
254
255
  state[`${metadata.aid}/output/metadata/l1s`] = context['$self'].output.metadata.l1s;
255
256
  state[`${metadata.aid}/output/metadata/l2s`] = context['$self'].output.metadata.l2s;
256
- } else if (config.type === 'activity' && leg === 1) {
257
+ } else if (polyfill.resolveActivityType(config.type) === 'hook' && leg === 1) {
257
258
  //activities run non-duplexed and only have a single leg
258
259
  state[`${metadata.aid}/output/metadata/l1s`] = context['$self'].output.metadata.l1s;
259
260
  state[`${metadata.aid}/output/metadata/l2s`] = context['$self'].output.metadata.l1s;
package/types/activity.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { MetricTypes } from "./stats";
2
2
  import { StreamRetryPolicy } from "./stream";
3
3
 
4
- type ActivityExecutionType = 'trigger' | 'await' | 'worker' | 'activity' | 'emit' | 'iterate' | 'cycle' | 'signal';
4
+ type ActivityExecutionType = 'trigger' | 'await' | 'worker' | 'activity' | 'emit' | 'iterate' | 'cycle' | 'signal' | 'hook';
5
5
 
6
6
  type Consumes = Record<string, string[]>;
7
7
 
@@ -65,6 +65,10 @@ interface CycleActivity extends BaseActivity {
65
65
  ancestor: string; //ancestor activity id
66
66
  }
67
67
 
68
+ interface HookActivity extends BaseActivity {
69
+ type: 'hook';
70
+ }
71
+
68
72
  interface SignalActivity extends BaseActivity {
69
73
  type: 'signal'; //signal activities call hook/hookAll
70
74
  subtype: 'one' | 'all'; //trigger: hook(One) or hookAll
@@ -80,7 +84,7 @@ interface IterateActivity extends BaseActivity {
80
84
  type: 'iterate';
81
85
  }
82
86
 
83
- type ActivityType = BaseActivity | TriggerActivity | AwaitActivity | WorkerActivity | IterateActivity;
87
+ type ActivityType = BaseActivity | TriggerActivity | AwaitActivity | WorkerActivity | IterateActivity | HookActivity;
84
88
 
85
89
  type ActivityData = Record<string, any>;
86
90
  type ActivityMetadata = {
@@ -124,6 +128,7 @@ export {
124
128
  TriggerActivityStats,
125
129
  AwaitActivity,
126
130
  CycleActivity,
131
+ HookActivity,
127
132
  SignalActivity,
128
133
  BaseActivity,
129
134
  IterateActivity,
package/types/durable.ts CHANGED
@@ -7,6 +7,12 @@ type WorkflowConfig = {
7
7
  initialInterval?: string; //default 1s
8
8
  }
9
9
 
10
+ type WorkflowSearchOptions = {
11
+ index: string; //FT index name (myapp:myindex)
12
+ prefix: string[]; //FT prefixes (['myapp:myindex:prefix1', 'myapp:myindex:prefix2'])
13
+ schema: Record<string, {type: 'TEXT' | 'NUMERIC' | 'TAG', sortable: boolean}>;
14
+ }
15
+
10
16
  type WorkflowOptions = {
11
17
  taskQueue: string;
12
18
  args: any[]; //input arguments to pass in
@@ -15,6 +21,7 @@ type WorkflowOptions = {
15
21
  parentWorkflowId?: string; //system reserved; the id of the parent; if present the flow will not self-clean until the parent that spawned it self-cleans
16
22
  workflowTrace?: string;
17
23
  workflowSpan?: string;
24
+ search?: WorkflowSearchOptions
18
25
  config?: WorkflowConfig;
19
26
  }
20
27
 
@@ -101,6 +108,7 @@ export {
101
108
  WorkerConfig,
102
109
  WorkflowConfig,
103
110
  WorkerOptions,
111
+ WorkflowSearchOptions,
104
112
  WorkflowDataType,
105
113
  WorkflowOptions,
106
114
  };
package/types/hook.ts CHANGED
@@ -16,6 +16,7 @@ interface HookConditions {
16
16
 
17
17
  interface HookRule {
18
18
  to: string;
19
+ keep_alive?: boolean; //if true, the hook will not be deleted after use
19
20
  conditions: HookConditions;
20
21
  }
21
22
 
package/types/index.ts CHANGED
@@ -10,6 +10,7 @@ export {
10
10
  AwaitActivity,
11
11
  BaseActivity,
12
12
  CycleActivity,
13
+ HookActivity,
13
14
  WorkerActivity,
14
15
  IterateActivity,
15
16
  SignalActivity,
@@ -39,6 +40,7 @@ export {
39
40
  WorkflowConfig,
40
41
  WorkerConfig,
41
42
  WorkerOptions,
43
+ WorkflowSearchOptions,
42
44
  WorkflowDataType,
43
45
  WorkflowOptions,
44
46
  }from './durable'