@hotmeshio/hotmesh 0.0.51 → 0.0.53

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