@hotmeshio/hotmesh 0.0.52 → 0.0.54

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 (134) hide show
  1. package/README.md +22 -18
  2. package/build/index.d.ts +1 -2
  3. package/build/index.js +1 -3
  4. package/build/modules/enums.d.ts +8 -3
  5. package/build/modules/enums.js +16 -8
  6. package/build/modules/errors.d.ts +58 -20
  7. package/build/modules/errors.js +90 -33
  8. package/build/package.json +7 -2
  9. package/build/services/activities/activity.d.ts +8 -0
  10. package/build/services/activities/activity.js +63 -14
  11. package/build/services/activities/await.js +6 -6
  12. package/build/services/activities/cycle.d.ts +2 -2
  13. package/build/services/activities/cycle.js +5 -5
  14. package/build/services/activities/hook.js +9 -5
  15. package/build/services/activities/interrupt.d.ts +3 -3
  16. package/build/services/activities/interrupt.js +15 -6
  17. package/build/services/activities/signal.d.ts +2 -2
  18. package/build/services/activities/signal.js +4 -4
  19. package/build/services/activities/trigger.d.ts +5 -2
  20. package/build/services/activities/trigger.js +34 -4
  21. package/build/services/activities/worker.js +6 -6
  22. package/build/services/compiler/deployer.js +33 -5
  23. package/build/services/compiler/validator.d.ts +2 -0
  24. package/build/services/compiler/validator.js +5 -1
  25. package/build/services/durable/client.d.ts +7 -1
  26. package/build/services/durable/client.js +57 -38
  27. package/build/services/durable/exporter.d.ts +27 -81
  28. package/build/services/durable/exporter.js +153 -325
  29. package/build/services/durable/handle.d.ts +13 -8
  30. package/build/services/durable/handle.js +61 -48
  31. package/build/services/durable/index.d.ts +0 -2
  32. package/build/services/durable/index.js +0 -2
  33. package/build/services/durable/schemas/factory.d.ts +33 -0
  34. package/build/services/durable/schemas/factory.js +2356 -0
  35. package/build/services/durable/search.js +8 -8
  36. package/build/services/durable/worker.js +117 -25
  37. package/build/services/durable/workflow.d.ts +67 -52
  38. package/build/services/durable/workflow.js +322 -306
  39. package/build/services/engine/index.d.ts +2 -2
  40. package/build/services/engine/index.js +5 -2
  41. package/build/services/exporter/index.d.ts +2 -4
  42. package/build/services/exporter/index.js +4 -5
  43. package/build/services/hotmesh/index.d.ts +2 -2
  44. package/build/services/hotmesh/index.js +2 -2
  45. package/build/services/mapper/index.d.ts +6 -2
  46. package/build/services/mapper/index.js +6 -2
  47. package/build/services/pipe/functions/array.d.ts +2 -10
  48. package/build/services/pipe/functions/array.js +30 -28
  49. package/build/services/pipe/functions/conditional.d.ts +1 -0
  50. package/build/services/pipe/functions/conditional.js +3 -0
  51. package/build/services/pipe/functions/date.d.ts +1 -0
  52. package/build/services/pipe/functions/date.js +4 -0
  53. package/build/services/pipe/functions/index.d.ts +2 -0
  54. package/build/services/pipe/functions/index.js +2 -0
  55. package/build/services/pipe/functions/logical.d.ts +5 -0
  56. package/build/services/pipe/functions/logical.js +12 -0
  57. package/build/services/pipe/functions/object.d.ts +3 -0
  58. package/build/services/pipe/functions/object.js +25 -7
  59. package/build/services/pipe/index.d.ts +20 -3
  60. package/build/services/pipe/index.js +82 -16
  61. package/build/services/router/index.js +14 -3
  62. package/build/services/serializer/index.d.ts +3 -2
  63. package/build/services/serializer/index.js +11 -4
  64. package/build/services/store/clients/ioredis.js +6 -6
  65. package/build/services/store/clients/redis.js +7 -7
  66. package/build/services/store/index.d.ts +2 -0
  67. package/build/services/store/index.js +4 -1
  68. package/build/services/stream/clients/ioredis.js +8 -8
  69. package/build/services/stream/clients/redis.js +1 -1
  70. package/build/types/activity.d.ts +60 -5
  71. package/build/types/durable.d.ts +183 -36
  72. package/build/types/error.d.ts +48 -0
  73. package/build/types/error.js +2 -0
  74. package/build/types/exporter.d.ts +35 -7
  75. package/build/types/index.d.ts +4 -3
  76. package/build/types/job.d.ts +93 -6
  77. package/build/types/pipe.d.ts +81 -3
  78. package/build/types/stream.d.ts +61 -1
  79. package/build/types/stream.js +4 -0
  80. package/index.ts +1 -2
  81. package/modules/enums.ts +16 -8
  82. package/modules/errors.ts +139 -34
  83. package/package.json +7 -2
  84. package/services/activities/activity.ts +63 -14
  85. package/services/activities/await.ts +6 -6
  86. package/services/activities/cycle.ts +7 -6
  87. package/services/activities/hook.ts +12 -5
  88. package/services/activities/interrupt.ts +19 -9
  89. package/services/activities/signal.ts +6 -5
  90. package/services/activities/trigger.ts +43 -6
  91. package/services/activities/worker.ts +7 -7
  92. package/services/compiler/deployer.ts +33 -6
  93. package/services/compiler/validator.ts +7 -3
  94. package/services/durable/client.ts +49 -22
  95. package/services/durable/exporter.ts +162 -349
  96. package/services/durable/handle.ts +66 -53
  97. package/services/durable/index.ts +0 -2
  98. package/services/durable/schemas/factory.ts +2358 -0
  99. package/services/durable/search.ts +8 -8
  100. package/services/durable/worker.ts +128 -29
  101. package/services/durable/workflow.ts +371 -322
  102. package/services/engine/index.ts +8 -3
  103. package/services/exporter/index.ts +10 -12
  104. package/services/hotmesh/index.ts +4 -3
  105. package/services/mapper/index.ts +6 -2
  106. package/services/pipe/functions/array.ts +24 -37
  107. package/services/pipe/functions/conditional.ts +4 -0
  108. package/services/pipe/functions/date.ts +6 -0
  109. package/services/pipe/functions/index.ts +7 -5
  110. package/services/pipe/functions/logical.ts +11 -0
  111. package/services/pipe/functions/object.ts +26 -7
  112. package/services/pipe/index.ts +99 -21
  113. package/services/quorum/index.ts +1 -3
  114. package/services/router/index.ts +14 -3
  115. package/services/serializer/index.ts +12 -5
  116. package/services/store/clients/ioredis.ts +6 -6
  117. package/services/store/clients/redis.ts +7 -7
  118. package/services/store/index.ts +4 -1
  119. package/services/stream/clients/ioredis.ts +8 -8
  120. package/services/stream/clients/redis.ts +1 -1
  121. package/types/activity.ts +87 -15
  122. package/types/durable.ts +263 -75
  123. package/types/error.ts +52 -0
  124. package/types/exporter.ts +43 -9
  125. package/types/index.ts +14 -8
  126. package/types/job.ts +157 -36
  127. package/types/pipe.ts +84 -3
  128. package/types/stream.ts +82 -23
  129. package/build/services/durable/factory.d.ts +0 -17
  130. package/build/services/durable/factory.js +0 -817
  131. package/build/services/durable/meshos.d.ts +0 -127
  132. package/build/services/durable/meshos.js +0 -380
  133. package/services/durable/factory.ts +0 -818
  134. package/services/durable/meshos.ts +0 -441
@@ -8,117 +8,211 @@ const ms_1 = __importDefault(require("ms"));
8
8
  const errors_1 = require("../../modules/errors");
9
9
  const key_1 = require("../../modules/key");
10
10
  const storage_1 = require("../../modules/storage");
11
- const client_1 = require("./client");
12
- const connection_1 = require("./connection");
13
- const factory_1 = require("./factory");
11
+ const utils_1 = require("../../modules/utils");
14
12
  const search_1 = require("./search");
15
13
  const worker_1 = require("./worker");
14
+ const serializer_1 = require("../serializer");
16
15
  const stream_1 = require("../../types/stream");
17
- const utils_1 = require("../../modules/utils");
16
+ const enums_1 = require("../../modules/enums");
18
17
  class WorkflowService {
19
18
  /**
20
- * Spawns a child workflow. await and return the result.
21
- * @template T - the result type
22
- * @param {WorkflowOptions} options - the workflow options
23
- * @returns {Promise<T>} - the result of the child workflow
19
+ * Returns the synchronous output from the activity (replay)
20
+ * if available locally, revealing whether or not the activity already
21
+ * ran during a prior execution cycle
22
+ * @param {string} prefix - one of: proxy, child, start, wait etc
23
+ * @returns
24
+ */
25
+ static async didRun(prefix) {
26
+ const { COUNTER, replay, workflowDimension, } = WorkflowService.getContext();
27
+ const execIndex = COUNTER.counter = COUNTER.counter + 1;
28
+ const sessionId = `-${prefix}${workflowDimension}-${execIndex}-`;
29
+ if (sessionId in replay) {
30
+ const restored = serializer_1.SerializerService.fromString(replay[sessionId]);
31
+ return [true, execIndex, restored];
32
+ }
33
+ return [false, execIndex, null];
34
+ }
35
+ /**
36
+ * Those methods that may only be called once must be protected by flagging
37
+ * their execution with a unique key (the key is stored in the HASH alongside
38
+ * process state and job state)
39
+ * @private
24
40
  */
25
- static async executeChild(options) {
41
+ static async isSideEffectAllowed(hotMeshClient, prefix) {
26
42
  const store = storage_1.asyncLocalStorage.getStore();
27
- const namespace = store.get('namespace');
28
43
  const workflowId = store.get('workflowId');
29
- const originJobId = store.get('originJobId');
30
44
  const workflowDimension = store.get('workflowDimension') ?? '';
31
- const workflowTrace = store.get('workflowTrace');
32
- const workflowSpan = store.get('workflowSpan');
33
45
  const COUNTER = store.get('counter');
34
46
  const execIndex = COUNTER.counter = COUNTER.counter + 1;
35
- //NOTE: this is the hash prefix; necessary for the search index to locate the entity
36
- //if the hash is a helper, a dash begins it, so it isn't indexed
37
- const entityOrEmptyString = options.entity ?? '';
38
- //If the workflowId is not provided, it is generated from the entity and the workflow name
39
- const childJobId = options.workflowId ?? `${entityOrEmptyString}-${workflowId}-$${options.entity ?? options.workflowName}${workflowDimension}-${execIndex}`;
40
- const parentWorkflowId = workflowId;
41
- const client = new client_1.ClientService({
42
- connection: await connection_1.ConnectionService.connect(worker_1.WorkerService.connection),
43
- });
44
- let handle = await client.workflow.getHandle(options.entity ?? options.taskQueue, options.entity ?? options.workflowName, childJobId, namespace);
45
- try {
46
- return await handle.result(true);
47
- }
48
- catch (error) {
49
- handle = await client.workflow.start({
50
- ...options,
51
- namespace,
52
- workflowId: childJobId,
53
- originJobId: originJobId ?? workflowId,
54
- parentWorkflowId,
55
- workflowTrace,
56
- workflowSpan,
57
- });
58
- //todo: options.startToCloseTimeout
59
- const result = await handle.result();
60
- return result;
47
+ const sessionId = `-${prefix}${workflowDimension}-${execIndex}-`;
48
+ const replay = store.get('replay');
49
+ if (sessionId in replay) {
50
+ return false;
61
51
  }
52
+ const keyParams = {
53
+ appId: hotMeshClient.appId,
54
+ jobId: workflowId
55
+ };
56
+ const workflowGuid = key_1.KeyService.mintKey(hotMeshClient.namespace, key_1.KeyType.JOB_STATE, keyParams);
57
+ const guidValue = Number(await hotMeshClient.engine.store.exec('HINCRBYFLOAT', workflowGuid, sessionId, '1'));
58
+ return guidValue === 1;
62
59
  }
63
60
  /**
64
- * Spawns a child workflow. return the childJobId.
65
- * This method is used when the result of the child workflow is not needed.
66
- * @param {WorkflowOptions} options - the workflow options
67
- * @returns {Promise<string>} - the childJobId
61
+ * Returns the current workflow context restored
62
+ * from Redis
68
63
  */
69
- static async startChild(options) {
64
+ static getContext() {
70
65
  const store = storage_1.asyncLocalStorage.getStore();
71
- const namespace = store.get('namespace');
72
66
  const workflowId = store.get('workflowId');
67
+ const replay = store.get('replay');
68
+ const cursor = store.get('cursor');
69
+ const interruptionRegistry = store.get('interruptionRegistry');
73
70
  const workflowDimension = store.get('workflowDimension') ?? '';
71
+ const workflowTopic = store.get('workflowTopic');
72
+ const namespace = store.get('namespace');
73
+ const originJobId = store.get('originJobId');
74
74
  const workflowTrace = store.get('workflowTrace');
75
+ const canRetry = store.get('canRetry');
75
76
  const workflowSpan = store.get('workflowSpan');
76
77
  const COUNTER = store.get('counter');
77
- const execIndex = COUNTER.counter = COUNTER.counter + 1;
78
- const sessionId = `-start${workflowDimension}-${execIndex}-`;
79
- const replay = store.get('replay');
80
- if (sessionId in replay) {
81
- return replay[sessionId];
78
+ const raw = store.get('raw');
79
+ return {
80
+ canRetry,
81
+ COUNTER,
82
+ counter: COUNTER.counter,
83
+ cursor,
84
+ interruptionRegistry,
85
+ namespace,
86
+ originJobId,
87
+ raw,
88
+ replay,
89
+ workflowId,
90
+ workflowDimension,
91
+ workflowTopic,
92
+ workflowTrace,
93
+ workflowSpan,
94
+ };
95
+ }
96
+ /**
97
+ * Return a handle to the hotmesh client hosting the workflow execution
98
+ * @returns {Promise<HotMesh>} - a hotmesh client
99
+ */
100
+ static async getHotMesh() {
101
+ const store = storage_1.asyncLocalStorage.getStore();
102
+ const workflowTopic = store.get('workflowTopic');
103
+ const namespace = store.get('namespace');
104
+ return await worker_1.WorkerService.getHotMesh(workflowTopic, { namespace });
105
+ }
106
+ /**
107
+ * Spawns a child workflow and awaits the return.
108
+ * @template T - the result type
109
+ * @param {WorkflowOptions} options - the workflow options
110
+ * @returns {Promise<T>} - the result of the child workflow
111
+ * @example
112
+ * const result = await Durable.workflow.execChild<typeof resultType>({ ...options });
113
+ */
114
+ static async execChild(options) {
115
+ //SYNC
116
+ //check if the activity already ran (check $error/done)
117
+ const isStartChild = options.await === false;
118
+ const prefix = isStartChild ? 'start' : 'child';
119
+ const [didRun, execIndex, result] = await WorkflowService.didRun(prefix);
120
+ const context = this.getContext();
121
+ const { canRetry, interruptionRegistry } = context;
122
+ if (didRun) {
123
+ if (result?.$error && (!result.$error.is_stream_error || (result.$error.is_stream_error && !canRetry))) {
124
+ if (options?.config?.throwOnError !== false) {
125
+ //rethrow remote execution error (simulates local failure)
126
+ const code = result.$error.code;
127
+ const message = result.$error.message;
128
+ const stack = result.$error.stack;
129
+ if (code === enums_1.HMSH_CODE_DURABLE_FATAL) {
130
+ throw new errors_1.DurableFatalError(message, stack);
131
+ }
132
+ else if (code == enums_1.HMSH_CODE_DURABLE_MAXED) {
133
+ throw new errors_1.DurableMaxedError(message, stack);
134
+ }
135
+ else if (code == enums_1.HMSH_CODE_DURABLE_TIMEOUT) {
136
+ throw new errors_1.DurableTimeoutError(message, stack);
137
+ }
138
+ else {
139
+ throw new errors_1.DurableRetryError(message, stack);
140
+ }
141
+ }
142
+ return result.$error;
143
+ }
144
+ else if (result.data) {
145
+ return result.data;
146
+ }
82
147
  }
83
- //NOTE: this is the hash prefix; necessary for the search index to locate the entity
84
- const entityOrEmptyString = options.entity ?? '';
85
- //If the workflowId is not provided, it is generated from the entity and the workflow name
86
- const parentWorkflowId = workflowId;
87
- const workflowTopic = `${options.entity ?? options.taskQueue}-${options.entity ?? options.workflowName}`;
88
- const hotMeshClient = await worker_1.WorkerService.getHotMesh(workflowTopic, { namespace });
89
- const keyParams = { appId: hotMeshClient.appId, jobId: workflowId };
90
- const workflowGuid = key_1.KeyService.mintKey(hotMeshClient.namespace, key_1.KeyType.JOB_STATE, keyParams);
91
- let childJobId = await hotMeshClient.engine.store.exec('HGET', workflowGuid, sessionId);
92
- if (childJobId) {
93
- return childJobId;
148
+ const interruptionMessage = this.getChildInterruptPayload(context, options, execIndex);
149
+ //push the packaged inputs to the registry
150
+ interruptionRegistry.push({
151
+ code: enums_1.HMSH_CODE_DURABLE_CHILD,
152
+ ...interruptionMessage,
153
+ });
154
+ //ASYNC
155
+ //sleep (allow others to be packaged / registered) and throw the error
156
+ await (0, utils_1.sleepFor)(0);
157
+ throw new errors_1.DurableChildError(interruptionMessage);
158
+ }
159
+ /**
160
+ * constructs the payload necessary to spawn a child job
161
+ * @private
162
+ */
163
+ static getChildInterruptPayload(context, options, execIndex) {
164
+ const { workflowId, originJobId, workflowDimension } = context;
165
+ let childJobId;
166
+ if (options.workflowId) {
167
+ childJobId = options.workflowId;
168
+ }
169
+ else if (options.entity) {
170
+ childJobId = `${options.entity}-${workflowId.substring(0, 7)}-${(0, utils_1.guid)()}-${workflowDimension}-${execIndex}`;
94
171
  }
95
172
  else {
96
- childJobId = options.workflowId ?? `${entityOrEmptyString}-${workflowId}-$${options.entity ?? options.workflowName}${workflowDimension}-${execIndex}`;
173
+ childJobId = `-${options.workflowName}-${(0, utils_1.guid)()}-${workflowDimension}-${execIndex}`;
97
174
  }
98
- const client = new client_1.ClientService({
99
- connection: await connection_1.ConnectionService.connect(worker_1.WorkerService.connection),
100
- });
101
- await client.workflow.start({
102
- ...options,
103
- namespace,
104
- workflowId: childJobId,
175
+ const parentWorkflowId = workflowId;
176
+ const taskQueueName = options.entity ?? options.taskQueue;
177
+ const workflowName = options.entity ?? options.workflowName;
178
+ const workflowTopic = `${taskQueueName}-${workflowName}`;
179
+ return {
180
+ arguments: [...(options.args || [])],
181
+ await: options?.await ?? true,
182
+ backoffCoefficient: options?.config?.backoffCoefficient ?? enums_1.HMSH_DURABLE_EXP_BACKOFF,
183
+ index: execIndex,
184
+ maximumAttempts: options?.config?.maximumAttempts ?? enums_1.HMSH_DURABLE_MAX_ATTEMPTS,
185
+ maximumInterval: (0, ms_1.default)(options?.config?.maximumInterval ?? enums_1.HMSH_DURABLE_MAX_INTERVAL) / 1000,
186
+ originJobId: originJobId ?? workflowId,
105
187
  parentWorkflowId,
106
- workflowTrace,
107
- workflowSpan,
108
- });
109
- await hotMeshClient.engine.store.exec('HSET', workflowGuid, sessionId, childJobId);
110
- return childJobId;
188
+ workflowDimension: workflowDimension,
189
+ workflowId: childJobId,
190
+ workflowTopic,
191
+ };
111
192
  }
112
193
  /**
113
- * Wraps activities in a proxy that will durably run them
114
- * @param {ActivityConfig} options - the activity configuration
115
- * that will be used to wrap the activities. You must pass an
116
- * `activities` object to this configuration. The activities object
117
- * should be a key-value pair of activity names and their respective
118
- * functions. This is typically done by importing the activities.
194
+ * Spawns a child workflow and returns the child Job ID.
195
+ * This method guarantees the spawned child has reserved the Job ID,
196
+ * returning a 'DuplicateJobError' error if not. Otherwise,
197
+ * this is a fire-and-forget method.
198
+ *
199
+ * @param {WorkflowOptions} options - the workflow options
200
+ * @returns {Promise<string>} - the childJobId
201
+ * @example
202
+ * const childJobId = await Durable.workflow.startChild({ ...options });
203
+ */
204
+ static async startChild(options) {
205
+ return this.execChild({ ...options, await: false });
206
+ }
207
+ /**
208
+ * Wraps activities in a proxy that durably runs/re-runs them to completion.
209
+ * TODO: verify that activities do not collide if named same on same server but bound to different workflows
119
210
  *
211
+ * @param {ActivityConfig} options - the activity configuration
212
+ * that will be used to wrap the activities.
120
213
  * @returns {ProxyType<ACT>} - a proxy object with the same keys as the
121
214
  * activities object, but with the values replaced by a wrapped function
215
+ *
122
216
  * @example
123
217
  * // import the activities
124
218
  * import * as activities from './activities';
@@ -141,6 +235,73 @@ class WorkflowService {
141
235
  }
142
236
  return proxy;
143
237
  }
238
+ static wrapActivity(activityName, options) {
239
+ return async function () {
240
+ //SYNC
241
+ //check if the activity already ran
242
+ const [didRun, execIndex, result] = await WorkflowService.didRun('proxy');
243
+ if (didRun) {
244
+ if (result?.$error) {
245
+ if (options?.retryPolicy?.throwOnError !== false) {
246
+ //rethrow remote execution error (simulates throw)
247
+ const code = result.$error.code;
248
+ const message = result.$error.message;
249
+ const stack = result.$error.stack;
250
+ if (code === enums_1.HMSH_CODE_DURABLE_FATAL) {
251
+ throw new errors_1.DurableFatalError(message, stack);
252
+ }
253
+ else if (code == enums_1.HMSH_CODE_DURABLE_MAXED) {
254
+ throw new errors_1.DurableMaxedError(message, stack);
255
+ }
256
+ else if (code == enums_1.HMSH_CODE_DURABLE_TIMEOUT) {
257
+ throw new errors_1.DurableTimeoutError(message, stack);
258
+ }
259
+ }
260
+ return result.$error;
261
+ }
262
+ return result.data;
263
+ }
264
+ //package the interruption inputs
265
+ const context = WorkflowService.getContext();
266
+ const { interruptionRegistry } = context;
267
+ const interruptionMessage = WorkflowService.getProxyInterruptPayload(context, activityName, execIndex, Array.from(arguments), options);
268
+ //push the packaged inputs to the registry
269
+ interruptionRegistry.push({
270
+ code: enums_1.HMSH_CODE_DURABLE_PROXY,
271
+ ...interruptionMessage,
272
+ });
273
+ //ASYNC
274
+ //sleep (allow others to be packaged / registered) and throw the error
275
+ await (0, utils_1.sleepFor)(0);
276
+ throw new errors_1.DurableProxyError(interruptionMessage);
277
+ };
278
+ }
279
+ /**
280
+ * constructs the payload necessary to spawn a proxyActivity job
281
+ * @private
282
+ */
283
+ static getProxyInterruptPayload(context, activityName, execIndex, args, options) {
284
+ const { workflowDimension, workflowId, originJobId, workflowTopic } = context;
285
+ const activityTopic = `${workflowTopic}-activity`;
286
+ const activityJobId = `-${workflowId}-$${activityName}${workflowDimension}-${execIndex}`;
287
+ let maximumInterval;
288
+ if (options.retryPolicy?.maximumInterval) {
289
+ maximumInterval = (0, ms_1.default)(options.retryPolicy.maximumInterval) / 1000;
290
+ }
291
+ return {
292
+ arguments: args,
293
+ workflowDimension: workflowDimension,
294
+ index: execIndex,
295
+ originJobId: originJobId || workflowId,
296
+ parentWorkflowId: workflowId,
297
+ workflowId: activityJobId,
298
+ workflowTopic: activityTopic,
299
+ activityName,
300
+ backoffCoefficient: options?.retryPolicy?.backoffCoefficient ?? undefined,
301
+ maximumAttempts: options?.retryPolicy?.maximumAttempts ?? undefined,
302
+ maximumInterval: maximumInterval ?? undefined,
303
+ };
304
+ }
144
305
  /**
145
306
  * Returns a search session for use when reading/writing to the workflow HASH.
146
307
  * The search session provides access to methods like `get`, `mget`, `set`, `del`, and `incr`.
@@ -159,68 +320,6 @@ class WorkflowService {
159
320
  const searchSessionId = `-search${workflowDimension}-${execIndex}`;
160
321
  return new search_1.Search(workflowId, hotMeshClient, searchSessionId);
161
322
  }
162
- /**
163
- * Return a handle to the hotmesh client currently running the workflow
164
- * @returns {Promise<HotMesh>} - a hotmesh client
165
- */
166
- static async getHotMesh() {
167
- const store = storage_1.asyncLocalStorage.getStore();
168
- const workflowTopic = store.get('workflowTopic');
169
- const namespace = store.get('namespace');
170
- return await worker_1.WorkerService.getHotMesh(workflowTopic, { namespace });
171
- }
172
- /**
173
- * Returns the current workflow context
174
- * @returns {WorkflowContext} - the current workflow context
175
- */
176
- static getContext() {
177
- const store = storage_1.asyncLocalStorage.getStore();
178
- const workflowId = store.get('workflowId');
179
- const replay = store.get('replay');
180
- const cursor = store.get('cursor');
181
- const workflowDimension = store.get('workflowDimension') ?? '';
182
- const workflowTopic = store.get('workflowTopic');
183
- const namespace = store.get('namespace');
184
- const workflowTrace = store.get('workflowTrace');
185
- const workflowSpan = store.get('workflowSpan');
186
- const COUNTER = store.get('counter');
187
- return {
188
- counter: COUNTER.counter,
189
- cursor,
190
- namespace,
191
- replay,
192
- workflowId,
193
- workflowDimension,
194
- workflowTopic,
195
- workflowTrace,
196
- workflowSpan,
197
- };
198
- }
199
- /**
200
- * Those methods that may only be called once must be protected by flagging
201
- * their execution with a unique key (the key is stored in the HASH alongside
202
- * process state and job state)
203
- * @private
204
- */
205
- static async isSideEffectAllowed(hotMeshClient, prefix) {
206
- const store = storage_1.asyncLocalStorage.getStore();
207
- const workflowId = store.get('workflowId');
208
- const workflowDimension = store.get('workflowDimension') ?? '';
209
- const COUNTER = store.get('counter');
210
- const execIndex = COUNTER.counter = COUNTER.counter + 1;
211
- const sessionId = `-${prefix}${workflowDimension}-${execIndex}-`;
212
- const replay = store.get('replay');
213
- if (sessionId in replay) {
214
- return false;
215
- }
216
- const keyParams = {
217
- appId: hotMeshClient.appId,
218
- jobId: workflowId
219
- };
220
- const workflowGuid = key_1.KeyService.mintKey(hotMeshClient.namespace, key_1.KeyType.JOB_STATE, keyParams);
221
- const guidValue = Number(await hotMeshClient.engine.store.exec('HINCRBYFLOAT', workflowGuid, sessionId, '1'));
222
- return guidValue === 1;
223
- }
224
323
  /**
225
324
  * Returns a random number between 0 and 1. This number is deterministic
226
325
  * and will never vary for a given seed. This is useful for randomizing
@@ -234,8 +333,8 @@ class WorkflowService {
234
333
  return (0, utils_1.deterministicRandom)(seed);
235
334
  }
236
335
  /**
237
- * Sends signal data into any other paused thread (which is paused and
238
- * awaiting the signal) from within a hook-thread or the main-thread
336
+ * Sends signal data into any other paused thread (which is currently
337
+ * awaiting the signal)
239
338
  * @param {string} signalId - the signal id
240
339
  * @param {Record<any, any>} data - the signal data
241
340
  * @returns {Promise<string>} - the stream id
@@ -245,8 +344,6 @@ class WorkflowService {
245
344
  const workflowTopic = store.get('workflowTopic');
246
345
  const namespace = store.get('namespace');
247
346
  const hotMeshClient = await worker_1.WorkerService.getHotMesh(workflowTopic, { namespace });
248
- //todo: this particular one is better patterned as a get/set,
249
- //since the receipt is a meaningful string (the stream id)
250
347
  if (await WorkflowService.isSideEffectAllowed(hotMeshClient, 'signal')) {
251
348
  return await hotMeshClient.hook(`${namespace}.wfs.signal`, { id: signalId, data });
252
349
  }
@@ -258,50 +355,39 @@ class WorkflowService {
258
355
  * @param {HookOptions} options - the hook options
259
356
  */
260
357
  static async hook(options) {
261
- const store = storage_1.asyncLocalStorage.getStore();
262
- const workflowTopic = store.get('workflowTopic');
263
- const namespace = store.get('namespace');
358
+ const { workflowId, namespace, workflowTopic, } = WorkflowService.getContext();
264
359
  const hotMeshClient = await worker_1.WorkerService.getHotMesh(workflowTopic, { namespace });
265
360
  if (await WorkflowService.isSideEffectAllowed(hotMeshClient, 'hook')) {
266
- const store = storage_1.asyncLocalStorage.getStore();
267
- const workflowId = options.workflowId ?? store.get('workflowId');
268
- let workflowTopic = store.get('workflowTopic');
361
+ const targetWorkflowId = options.workflowId ?? workflowId;
362
+ let targetTopic;
269
363
  if (options.entity || (options.taskQueue && options.workflowName)) {
270
- workflowTopic = `${options.entity ?? options.taskQueue}-${options.entity ?? options.workflowName}`;
271
- } //else this is essentially recursion as the function calls itself
364
+ targetTopic = `${options.entity ?? options.taskQueue}-${options.entity ?? options.workflowName}`;
365
+ }
366
+ else {
367
+ targetTopic = workflowTopic;
368
+ }
272
369
  const payload = {
273
370
  arguments: [...options.args],
274
- id: workflowId,
371
+ id: targetWorkflowId,
275
372
  workflowTopic,
276
- backoffCoefficient: options.config?.backoffCoefficient || factory_1.DEFAULT_COEFFICIENT,
373
+ backoffCoefficient: options.config?.backoffCoefficient || enums_1.HMSH_DURABLE_EXP_BACKOFF,
277
374
  };
278
375
  return await hotMeshClient.hook(`${namespace}.flow.signal`, payload, stream_1.StreamStatus.PENDING, 202);
279
376
  }
280
377
  }
281
- static getLocalState() {
282
- const store = storage_1.asyncLocalStorage.getStore();
283
- return {
284
- workflowId: store.get('workflowId'),
285
- namespace: store.get('namespace'),
286
- workflowTopic: store.get('workflowTopic'),
287
- workflowDimension: store.get('workflowDimension') ?? '',
288
- counter: store.get('counter'),
289
- replay: store.get('replay'),
290
- };
291
- }
292
378
  /**
293
379
  * Executes a function once and caches the result. If the function is called
294
380
  * again, the cached result is returned. This is useful for wrapping
295
381
  * expensive activity calls that should only be run once, but which might
296
- * not require the configuration nuance/expense provided by proxyActivities.
382
+ * not require the cost and safety provided by proxyActivities.
297
383
  * @template T - the result type
298
384
  */
299
385
  static async once(fn, ...args) {
300
- const { workflowId, namespace, workflowTopic, workflowDimension, counter: COUNTER, replay, } = WorkflowService.getLocalState();
386
+ const { COUNTER, namespace, workflowId, workflowTopic, workflowDimension, replay, } = WorkflowService.getContext();
301
387
  const execIndex = COUNTER.counter = COUNTER.counter + 1;
302
388
  const sessionId = `-once${workflowDimension}-${execIndex}-`;
303
389
  if (sessionId in replay) {
304
- return JSON.parse(replay[sessionId]);
390
+ return serializer_1.SerializerService.fromString(replay[sessionId]).data;
305
391
  }
306
392
  const hotMeshClient = await worker_1.WorkerService.getHotMesh(workflowTopic, { namespace });
307
393
  const keyParams = {
@@ -309,25 +395,22 @@ class WorkflowService {
309
395
  jobId: workflowId
310
396
  };
311
397
  const workflowGuid = key_1.KeyService.mintKey(hotMeshClient.namespace, key_1.KeyType.JOB_STATE, keyParams);
312
- const value = await hotMeshClient.engine.store.exec('HGET', workflowGuid, sessionId);
313
- if (value) {
314
- return JSON.parse(value);
315
- }
398
+ const t1 = new Date();
316
399
  const response = await fn(...args);
317
- await hotMeshClient.engine.store.exec('HSET', workflowGuid, sessionId, JSON.stringify(response));
400
+ const t2 = new Date();
401
+ const payload = {
402
+ data: response,
403
+ ac: (0, utils_1.formatISODate)(t1),
404
+ au: (0, utils_1.formatISODate)(t2),
405
+ };
406
+ await hotMeshClient.engine.store.exec('HSET', workflowGuid, sessionId, serializer_1.SerializerService.toString(payload));
318
407
  return response;
319
408
  }
320
409
  /**
321
410
  * Interrupts a running job
322
- *
323
- * @param {string} jobId - the target job id
324
- * @param {JobInterruptOptions} options - the interrupt options
325
- * @returns {Promise<string>} - the stream id
326
411
  */
327
412
  static async interrupt(jobId, options = {}) {
328
- const store = storage_1.asyncLocalStorage.getStore();
329
- const workflowTopic = store.get('workflowTopic');
330
- const namespace = store.get('namespace');
413
+ const { workflowTopic, namespace } = WorkflowService.getContext();
331
414
  const hotMeshClient = await worker_1.WorkerService.getHotMesh(workflowTopic, { namespace });
332
415
  if (await WorkflowService.isSideEffectAllowed(hotMeshClient, 'interrupt')) {
333
416
  return await hotMeshClient.interrupt(`${hotMeshClient.appId}.execute`, jobId, options);
@@ -336,140 +419,73 @@ class WorkflowService {
336
419
  /**
337
420
  * Sleeps the workflow for a duration. As the function is reentrant,
338
421
  * upon reentry, the function will traverse prior execution paths up
339
- * until the sleep command and then resume execution from that point.
340
- * @param {string} duration - for example: '1 minute', '2 hours', '3 days'
341
- * @returns {Promise<number>}
422
+ * until the sleep command and then resume execution thereafter.
423
+ * @param {string} duration - See the `ms` package for syntax examples: '1 minute', '2 hours', '3 days'
424
+ * @returns {Promise<number>} - resolved duration in seconds
342
425
  */
343
426
  static async sleepFor(duration) {
344
- const seconds = (0, ms_1.default)(duration) / 1000;
345
- const store = storage_1.asyncLocalStorage.getStore();
346
- const namespace = store.get('namespace');
347
- const workflowTopic = store.get('workflowTopic');
348
- const hotMeshClient = await worker_1.WorkerService.getHotMesh(workflowTopic, { namespace });
349
- if (await WorkflowService.isSideEffectAllowed(hotMeshClient, 'sleep')) {
350
- const workflowId = store.get('workflowId');
351
- const workflowDimension = store.get('workflowDimension') ?? '';
352
- const COUNTER = store.get('counter');
353
- const execIndex = COUNTER.counter;
354
- // spawn a new sleep job if error code 592 is thrown by the worker
355
- // NOTE: If this message appears in the stack trace, the `.sleepFor()` method in your workflow code was NOT awaited.
356
- throw new errors_1.DurableSleepForError(workflowId, seconds, execIndex, workflowDimension);
427
+ //SYNC
428
+ //return early if this sleep command has already run
429
+ const [didRun, execIndex, result] = await WorkflowService.didRun('sleep');
430
+ if (didRun) {
431
+ return result.duration; //in seconds
357
432
  }
358
- return seconds;
433
+ //package the interruption inputs
434
+ const store = storage_1.asyncLocalStorage.getStore();
435
+ const interruptionRegistry = store.get('interruptionRegistry');
436
+ const workflowId = store.get('workflowId');
437
+ const workflowDimension = store.get('workflowDimension') ?? '';
438
+ const interruptionMessage = {
439
+ workflowId,
440
+ duration: (0, ms_1.default)(duration) / 1000,
441
+ index: execIndex,
442
+ workflowDimension,
443
+ };
444
+ interruptionRegistry.push({
445
+ code: enums_1.HMSH_CODE_DURABLE_SLEEP,
446
+ ...interruptionMessage,
447
+ });
448
+ //ASYNC
449
+ //sleep to allow other interruptions to be packaged and registered
450
+ await (0, utils_1.sleepFor)(0);
451
+ // NOTE: If you are reading this in the stack trace, await `sleepFor`
452
+ throw new errors_1.DurableSleepError(interruptionMessage);
359
453
  }
360
454
  /**
361
- * Waits for a signal to awaken
362
- * @param {string[]} signals - the signals to wait for
363
- * @param {Record<string, string>} options - the options
364
- * @returns {Promise<Record<any, any>[]>}
455
+ * Pauses the workflow until `signalId` is received.
456
+ * @template T - the result type
457
+ * @param {string} signalId - a unique, shareable guid (e.g, 'abc123')
458
+ * @returns {Promise<T>}
459
+ * @example
460
+ * const result = await Durable.workflow.waitFor<typeof resultType>('abc123');
365
461
  */
366
- static async waitForSignal(signals, options) {
462
+ static async waitFor(signalId) {
463
+ //SYNC
464
+ //return early if this waitFor command has already run
465
+ const [didRun, execIndex, result] = await WorkflowService.didRun('wait');
466
+ if (didRun) {
467
+ return result.data.data;
468
+ }
469
+ //package the interruption inputs
367
470
  const store = storage_1.asyncLocalStorage.getStore();
368
- const COUNTER = store.get('counter');
471
+ const interruptionRegistry = store.get('interruptionRegistry');
369
472
  const workflowId = store.get('workflowId');
370
- const workflowTopic = store.get('workflowTopic');
371
473
  const workflowDimension = store.get('workflowDimension') ?? '';
372
- const namespace = store.get('namespace');
373
- const hotMeshClient = await worker_1.WorkerService.getHotMesh(workflowTopic, { namespace });
374
- //iterate the list of signals and check for done
375
- let allAreComplete = true;
376
- let noneAreComplete = false;
377
- const signalResults = [];
378
- for (const signal of signals) {
379
- const execIndex = COUNTER.counter = COUNTER.counter + 1;
380
- const wfsJobId = `-${workflowId}-$wfs${workflowDimension}-${execIndex}`;
381
- try {
382
- if (allAreComplete) {
383
- const state = await hotMeshClient.getState(`${hotMeshClient.appId}.wfs.execute`, wfsJobId);
384
- if (state.data?.signalData) {
385
- //user data is nested to isolate from the signal id; unpackage it
386
- const signalData = state.data.signalData;
387
- signalResults.push(signalData.data);
388
- }
389
- else {
390
- allAreComplete = false;
391
- }
392
- }
393
- else {
394
- signalResults.push({ signal, index: execIndex });
395
- }
396
- }
397
- catch (err) {
398
- //todo: options.startToCloseTimeout
399
- allAreComplete = false;
400
- noneAreComplete = true;
401
- signalResults.push({ signal, index: execIndex });
402
- }
403
- }
404
- ;
405
- if (allAreComplete) {
406
- return signalResults;
407
- }
408
- else if (noneAreComplete) {
409
- //this error is caught by the workflow runner
410
- //it is then returned as the workflow result (594)
411
- throw new errors_1.DurableWaitForSignalError(workflowId, signalResults);
412
- }
413
- else {
414
- //this error happens when a signal is received but others are still open
415
- throw new errors_1.DurableIncompleteSignalError(workflowId);
416
- }
417
- }
418
- static wrapActivity(activityName, options) {
419
- return async function () {
420
- const store = storage_1.asyncLocalStorage.getStore();
421
- const COUNTER = store.get('counter');
422
- //increment by state (not value) to avoid race conditions
423
- const execIndex = COUNTER.counter = COUNTER.counter + 1;
424
- const workflowId = store.get('workflowId');
425
- const originJobId = store.get('originJobId');
426
- const workflowDimension = store.get('workflowDimension') ?? '';
427
- const workflowTopic = store.get('workflowTopic');
428
- const trc = store.get('workflowTrace');
429
- const spn = store.get('workflowSpan');
430
- const namespace = store.get('namespace');
431
- const activityTopic = `${workflowTopic}-activity`;
432
- const activityJobId = `-${workflowId}-$${activityName}${workflowDimension}-${execIndex}`;
433
- let activityState;
434
- try {
435
- const hotMeshClient = await worker_1.WorkerService.getHotMesh(activityTopic, { namespace });
436
- activityState = await hotMeshClient.getState(`${hotMeshClient.appId}.activity.execute`, activityJobId);
437
- if (activityState.metadata.err) {
438
- await hotMeshClient.scrub(activityJobId);
439
- throw new Error(activityState.metadata.err);
440
- }
441
- else if (activityState.metadata.js === 0 || activityState.data?.done) {
442
- return activityState.data?.response;
443
- }
444
- //one time subscription
445
- return await new Promise((resolve, reject) => {
446
- hotMeshClient.sub(`${hotMeshClient.appId}.activity.executed.${activityJobId}`, async (topic, message) => {
447
- const response = message.data?.response;
448
- hotMeshClient.unsub(`${hotMeshClient.appId}.activity.executed.${activityJobId}`);
449
- // Resolve the Promise when the callback is triggered with a message
450
- resolve(response);
451
- });
452
- });
453
- }
454
- catch (e) {
455
- //expected; thrown by `getState` when the job cannot be found
456
- const duration = (0, ms_1.default)(options?.startToCloseTimeout || '1 minute');
457
- const payload = {
458
- arguments: Array.from(arguments),
459
- //when the origin job is removed
460
- originJobId: originJobId ?? workflowId,
461
- parentWorkflowId: workflowId,
462
- workflowId: activityJobId,
463
- workflowTopic: activityTopic,
464
- activityName,
465
- };
466
- //start the job
467
- const hotMeshClient = await worker_1.WorkerService.getHotMesh(activityTopic, { namespace });
468
- const context = { metadata: { trc, spn }, data: {} };
469
- const jobOutput = await hotMeshClient.pubsub(`${hotMeshClient.appId}.activity.execute`, payload, context, duration);
470
- return jobOutput.data.response;
471
- }
474
+ const interruptionMessage = {
475
+ workflowId,
476
+ signalId,
477
+ index: execIndex,
478
+ workflowDimension,
472
479
  };
480
+ interruptionRegistry.push({
481
+ code: enums_1.HMSH_CODE_DURABLE_WAIT,
482
+ ...interruptionMessage,
483
+ });
484
+ //ASYNC
485
+ //sleep to allow other interruptions to be packaged and registered
486
+ await (0, utils_1.sleepFor)(0);
487
+ // NOTE: If you are reading this in the stack trace, await `waitFor`
488
+ throw new errors_1.DurableWaitForError(interruptionMessage);
473
489
  }
474
490
  }
475
491
  exports.WorkflowService = WorkflowService;