@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
@@ -1,4 +1,17 @@
1
- import { APP_ID, APP_VERSION, DEFAULT_COEFFICIENT, getWorkflowYAML } from './factory';
1
+ import ms from 'ms';
2
+
3
+ import {
4
+ APP_ID,
5
+ APP_VERSION,
6
+ getWorkflowYAML } from './schemas/factory';
7
+ import {
8
+ HMSH_LOGLEVEL,
9
+ HMSH_EXPIRE_JOB_SECONDS,
10
+ HMSH_QUORUM_DELAY_MS,
11
+ HMSH_DURABLE_EXP_BACKOFF,
12
+ HMSH_DURABLE_MAX_ATTEMPTS,
13
+ HMSH_DURABLE_MAX_INTERVAL } from '../../modules/enums';
14
+ import { sleepFor } from '../../modules/utils';
2
15
  import { WorkflowHandleService } from './handle';
3
16
  import { HotMeshService as HotMesh } from '../hotmesh';
4
17
  import {
@@ -11,8 +24,6 @@ import { JobState } from '../../types/job';
11
24
  import { KeyService, KeyType } from '../../modules/key';
12
25
  import { Search } from './search';
13
26
  import { StreamStatus } from '../../types';
14
- import { HMSH_LOGLEVEL, HMSH_EXPIRE_JOB_SECONDS, HMSH_QUORUM_DELAY_MS } from '../../modules/enums';
15
- import { sleepFor } from '../../modules/utils';
16
27
 
17
28
  export class ClientService {
18
29
 
@@ -32,7 +43,7 @@ export class ClientService {
32
43
  await this.verifyWorkflowActive(hotMeshClient, targetNS);
33
44
  if (!ClientService.topics.includes(workflowTopic)) {
34
45
  ClientService.topics.push(workflowTopic);
35
- await this.createStream(hotMeshClient, workflowTopic, namespace);
46
+ await ClientService.createStream(hotMeshClient, workflowTopic, namespace);
36
47
  }
37
48
  return hotMeshClient;
38
49
  }
@@ -49,7 +60,7 @@ export class ClientService {
49
60
  }
50
61
  });
51
62
  ClientService.instances.set(targetNS, hotMeshClient);
52
- await this.createStream(await hotMeshClient, workflowTopic, namespace);
63
+ await ClientService.createStream(await hotMeshClient, workflowTopic, namespace);
53
64
  await this.activateWorkflow(await hotMeshClient, targetNS);
54
65
  return hotMeshClient;
55
66
  }
@@ -60,7 +71,7 @@ export class ClientService {
60
71
  * has not yet been initialized, so this call ensures that the channel
61
72
  * exists and is ready to serve as a container for events.
62
73
  */
63
- createStream = async(hotMeshClient: HotMesh, workflowTopic: string, namespace?: string) => {
74
+ static createStream = async(hotMeshClient: HotMesh, workflowTopic: string, namespace?: string) => {
64
75
  const store = hotMeshClient.engine.store;
65
76
  const params = { appId: namespace ?? APP_ID, topic: workflowTopic };
66
77
  const streamKey = store.mintKey(KeyType.STREAMS, params);
@@ -71,6 +82,23 @@ export class ClientService {
71
82
  }
72
83
  }
73
84
 
85
+ /**
86
+ * It is possible for a client to invoke a workflow without first
87
+ * creating the stream. This method will verify that the stream
88
+ * exists and if not, create it.
89
+ */
90
+ static verifyStream = async(workflowTopic: string, namespace?: string) => {
91
+ const targetNS = namespace ?? APP_ID;
92
+ if (ClientService.instances.has(targetNS)) {
93
+ const hotMeshClient = await ClientService.instances.get(targetNS);
94
+ if (!ClientService.topics.includes(workflowTopic)) {
95
+ ClientService.topics.push(workflowTopic);
96
+ await ClientService.createStream(hotMeshClient, workflowTopic, namespace);
97
+ }
98
+ return hotMeshClient;
99
+ }
100
+ }
101
+
74
102
  /**
75
103
  * For those deployments with a redis stack backend (with the FT module),
76
104
  * this method will configure the search index for the workflow.
@@ -95,8 +123,8 @@ export class ClientService {
95
123
  const hotMeshPrefix = KeyService.mintKey(hotMeshClient.namespace, KeyType.JOB_STATE, keyParams);
96
124
  const prefixes = search.prefix.map((prefix) => `${hotMeshPrefix}${prefix}`);
97
125
  await store.exec('FT.CREATE', `${search.index}`, 'ON', 'HASH', 'PREFIX', prefixes.length, ...prefixes, 'SCHEMA', ...schema);
98
- } catch (err) {
99
- hotMeshClient.engine.logger.info('durable-client-search-err', { err });
126
+ } catch (error) {
127
+ hotMeshClient.engine.logger.info('durable-client-search-err', { ...error });
100
128
  }
101
129
  }
102
130
  }
@@ -127,8 +155,11 @@ export class ClientService {
127
155
  parentWorkflowId: options.parentWorkflowId,
128
156
  workflowId: options.workflowId || HotMesh.guid(),
129
157
  workflowTopic: workflowTopic,
130
- backoffCoefficient: options.config?.backoffCoefficient || DEFAULT_COEFFICIENT,
158
+ backoffCoefficient: options.config?.backoffCoefficient || HMSH_DURABLE_EXP_BACKOFF,
159
+ maximumAttempts: options.config?.maximumAttempts || HMSH_DURABLE_MAX_ATTEMPTS,
160
+ maximumInterval: ms(options.config?.maximumInterval || HMSH_DURABLE_MAX_INTERVAL) / 1000,
131
161
  }
162
+
132
163
  const context = { metadata: { trc, spn }, data: {}};
133
164
  const jobId = await hotMeshClient.pub(
134
165
  `${options.namespace ?? APP_ID}.execute`,
@@ -165,7 +196,9 @@ export class ClientService {
165
196
  arguments: [...options.args],
166
197
  id: options.workflowId,
167
198
  workflowTopic,
168
- backoffCoefficient: options.config?.backoffCoefficient || DEFAULT_COEFFICIENT,
199
+ backoffCoefficient: options.config?.backoffCoefficient || HMSH_DURABLE_EXP_BACKOFF,
200
+ maximumAttempts: options.config?.maximumAttempts || HMSH_DURABLE_MAX_ATTEMPTS,
201
+ maximumInterval: ms(options.config?.maximumInterval || HMSH_DURABLE_MAX_INTERVAL) / 1000,
169
202
  }
170
203
  //seed search data if presentthe hook before entering
171
204
  const hotMeshClient = await this.getHotMeshClient(workflowTopic, options.namespace);
@@ -190,9 +223,9 @@ export class ClientService {
190
223
  const hotMeshClient = await this.getHotMeshClient(workflowTopic, namespace);
191
224
  try {
192
225
  return await this.search(hotMeshClient, index, query);
193
- } catch (err) {
194
- hotMeshClient.engine.logger.error('durable-client-search-err', { err });
195
- throw err;
226
+ } catch (error) {
227
+ hotMeshClient.engine.logger.error('durable-client-search-err', { ...error });
228
+ throw error;
196
229
  }
197
230
  }
198
231
  }
@@ -218,7 +251,7 @@ export class ClientService {
218
251
  await hotMesh.deploy(getWorkflowYAML(appId, version));
219
252
  await hotMesh.activate(version);
220
253
  } catch (error) {
221
- hotMesh.engine.logger.error('durable-client-deploy-activate-err', { error });
254
+ hotMesh.engine.logger.error('durable-client-deploy-activate-err', { ...error });
222
255
  throw error;
223
256
  }
224
257
  } else if(app && !app.active) {
@@ -1,184 +1,43 @@
1
+ import { VALSEP } from '../../modules/key';
2
+ import { restoreHierarchy } from '../../modules/utils';
1
3
  import { ILogger } from '../logger';
4
+ import { SerializerService } from '../serializer';
2
5
  import { StoreService } from '../store';
3
- import { StringStringType, Symbols } from "../../types";
4
- import { RedisClient, RedisMulti } from '../../types/redis';
5
6
  import {
6
- ActivityAction,
7
- DependencyExport,
8
7
  ExportItem,
9
8
  ExportOptions,
10
- JobAction,
11
- JobActionExport,
12
- DurableJobExport,
13
- JobTimeline } from '../../types/exporter';
14
- import { SerializerService } from '../serializer';
15
- import { restoreHierarchy } from '../../modules/utils';
16
- import { VALSEP } from '../../modules/key';
9
+ DurableJobExport,
10
+ IdemType,
11
+ TimelineEntry } from '../../types/exporter';
12
+ import { RedisClient, RedisMulti } from '../../types/redis';
13
+ import { StringStringType, Symbols } from "../../types/serializer";
17
14
 
18
- /**
19
- * Downloads job data from Redis (hscan, hmget, hgetall)
20
- * Splits, Inflates, and Sorts the job data for use in durable contexts
21
- */
22
15
  class ExporterService {
23
16
  appId: string;
24
17
  logger: ILogger;
25
- serializer: SerializerService
26
18
  store: StoreService<RedisClient, RedisMulti>;
27
19
  symbols: Promise<Symbols> | Symbols;
28
20
 
29
- /**
30
- * Friendly names for the activity ids
31
- */
32
- activitySymbols: Symbols = {
33
- t1: 'trigger',
34
- a1: 'pivot',
35
- w1: 'worker',
36
- a592: 'sleeper',
37
- a594: 'awaiter',
38
- a599: 'retryer',
39
- c592: 'sleep_cycler',
40
- c594: 'await_cycler',
41
- c599: 'retry_cycler',
42
- s5: 'scrubber',
43
- sig: 'hook',
44
- siga1: 'hook_pivot',
45
- sigw1: 'hook_worker',
46
- siga592: 'hook_sleeper',
47
- siga594: 'hook_awaiter',
48
- siga599: 'hook_retryer',
49
- sigc592: 'hook_sleep_cycler',
50
- sigc594: 'hook_await_cycler',
51
- sigc599: 'hook_retry_cycler',
52
- }
53
-
54
- //adjacent transitions
55
- transitions = {
56
- trigger: ['pivot', 'hook'],
57
- pivot: ['worker'],
58
- worker: ['sleeper', 'awaiter', 'retryer', 'scrubber'],
59
- sleeper: ['sleep_cycler'],
60
- awaiter: ['await_cycler'],
61
- retryer: ['retry_cycler'],
62
- hook: ['hook_pivot'],
63
- hook_pivot: ['hook_worker'],
64
- hook_worker: ['hook_sleeper', 'hook_awaiter', 'hook_retryer'],
65
- hook_sleeper: ['hook_sleep_cycler'],
66
- hook_awaiter: ['hook_await_cycler'],
67
- hook_retryer: ['hook_retry_cycler'],
68
- };
69
-
70
- //goto transitions
71
- cycles = {
72
- sleep_cycler: ['pivot'],
73
- await_cycler: ['pivot'],
74
- retry_cycler: ['pivot'],
75
- hook_sleep_cycler: ['hook_pivot'],
76
- hook_await_cycler: ['hook_pivot'],
77
- hook_retry_cycler: ['hook_pivot'],
78
- }
79
-
80
21
  constructor(appId: string, store: StoreService<RedisClient, RedisMulti>, logger: ILogger) {
81
22
  this.appId = appId;
82
23
  this.logger = logger;
83
24
  this.store = store;
84
- this.serializer = new SerializerService();
85
25
  }
86
26
 
87
27
  /**
88
- * Convert the job hash and dependency list into a DurableJobExport object.
89
- * This object contains various facets that describe the interaction
90
- * in terms relevant to narrative storytelling.
28
+ * Convert the job hash from its compiles format into a DurableJobExport object with
29
+ * facets that describe the workflow in terms relevant to narrative storytelling.
91
30
  */
92
31
  async export(jobId: string, options: ExportOptions = {}): Promise<DurableJobExport> {
93
32
  if (!this.symbols) {
94
33
  this.symbols = this.store.getAllSymbols();
95
34
  this.symbols = await this.symbols;
96
35
  }
97
- const depData = await this.store.getDependencies(jobId);
98
36
  const jobData = await this.store.getRaw(jobId);
99
- const jobExport = this.inflate(jobData, depData);
37
+ const jobExport = this.inflate(jobData/*, depData*/);
100
38
  return jobExport;
101
39
  }
102
40
 
103
- /**
104
- * Interleave actions into the replay timeline to create
105
- * a time-ordered timeline of the entire interaction, beginning
106
- * with the entry trigger and concluding with the scrubber
107
- * activity. Using the returned timeline, it is possible to
108
- * create an animated narrative of the job, highlighting
109
- * activities in the graph according to the timeline's
110
- * activity-created (/ac) and activity-updated (/au) entries.
111
- */
112
- createTimeline(replay: ExportItem[], actions: JobActionExport): JobTimeline[] {
113
- const timeline: JobTimeline[] = [];
114
- replay.forEach((item) => {
115
- const dimensions = item[0];
116
- const parts = dimensions.split('/');
117
- const activityName = item[1].split('/')[0];
118
- const duplex = item[1].endsWith('/ac') ? 'entry' : 'exit';
119
- const timestamp = item[2];
120
- let event: JobTimeline = {
121
- activity: activityName,
122
- duplex: duplex as 'entry' | 'exit',
123
- dimension: dimensions,
124
- timestamp,
125
- created: timestamp,
126
- updated: timestamp,
127
- };
128
- const prior = timeline[timeline.length - 1];
129
- if (prior && prior.activity === event.activity && prior.duplex !== event.duplex && prior.dimension === event.dimension) {
130
- if (event.duplex === 'exit') {
131
- prior.updated = event.timestamp;
132
- } else {
133
- prior.created = event.timestamp;
134
- }
135
- event = prior;
136
- } else {
137
- timeline.push(event);
138
- }
139
-
140
- if (this.isMainEntry(item[1])) {
141
- event.actions = [] as ActivityAction[];
142
- this.interleaveActions(actions.main, event.actions);
143
- } else if (this.isHookEntry(item[1])) {
144
- const hookDimension = `/${parts[1]}/${parts[2]}`;
145
- const hookActions = actions.hooks[hookDimension];
146
- event.actions = [] as ActivityAction[];
147
- this.interleaveActions(hookActions, event.actions);
148
- }
149
- });
150
- return timeline;
151
- }
152
-
153
- /**
154
- * Interleave actions into the 'worker' and 'hook_worker'
155
- * activities (between their /ac and /au entries)
156
- */
157
- interleaveActions(target: JobAction, actions: ActivityAction[]) {
158
- if (target) {
159
- for (let i = target.cursor + 1; i < target.items.length; i++) {
160
- const [_, actionType, jobOrIndex] = target.items[i];
161
- actions.push({ action: actionType, target: jobOrIndex });
162
- target.cursor = i;
163
- if (this.isPausingAction(actionType)) {
164
- break;
165
- }
166
- }
167
- }
168
- }
169
-
170
- isPausingAction(actionType: string): boolean {
171
- return actionType === 'sleep' || actionType === 'waitForSignal';
172
- }
173
-
174
- isMainEntry(key: string): boolean {
175
- return key.startsWith('worker/') && key.endsWith('/ac');
176
- }
177
-
178
- isHookEntry(key: string): boolean {
179
- return key.startsWith('hook_worker/') && key.endsWith('/ac');
180
- }
181
-
182
41
  /**
183
42
  * Inflates the key from Redis, 3-character symbol
184
43
  * into a human-readable JSON path, reflecting the
@@ -188,9 +47,6 @@ class ExporterService {
188
47
  if (key in this.symbols) {
189
48
  const path = this.symbols[key];
190
49
  const parts = path.split('/');
191
- if (parts[0] in this.activitySymbols) {
192
- parts[0] = this.activitySymbols[parts[0]];
193
- }
194
50
  return parts.join('/');
195
51
  }
196
52
  return key;
@@ -203,89 +59,19 @@ class ExporterService {
203
59
  * @param data - the dependency data from Redis
204
60
  * @returns - the organized dependency data
205
61
  */
206
- inflateDependencyData(data: string[], actions: JobActionExport): DependencyExport[] {
207
- const hookReg = /([0-9,]+)-(\d+)$/;
208
- const flowReg = /-(\d+)$/;
209
- return data.map((dependency, index: number): DependencyExport => {
210
- const [action, topic, gid, _pd, ...jid] = dependency.split(VALSEP);
211
- const jobId = jid.join(VALSEP);
212
- const match = jobId.match(hookReg);
213
- let prefix: string;
214
- let type: 'hook' | 'flow' | 'other';
215
- let dimensionKey: string = '';
216
- if (match) {
217
- //hook-originating dependency
218
- const [_, dimension, counter] = match;
219
- dimensionKey = dimension.split(',').join('/');
220
- prefix = `${dimensionKey}[${counter}]`;
221
- type = 'hook';
222
- } else {
223
- const match = jobId.match(flowReg);
224
- if (match) {
225
- //main workflow-originating dependency
226
- const [_, counter] = match;
227
- prefix = `[${counter}]`;
228
- type = 'flow';
229
- } else {
230
- //'other' types like signal cleanup
231
- prefix = '/';
232
- type = 'other';
233
- }
234
- }
235
- this.seedActions(
236
- type,
237
- action,
238
- topic,
239
- dependency,
240
- prefix,
241
- dimensionKey,
242
- actions,
243
- jobId,
244
- );
62
+ inflateDependencyData(data: string[]): Record<string, any>[] {
63
+ return data.map((dependency, index: number): Record<string, any> => {
64
+ const [action, topic, gid, dimension, ...jid] = dependency.split(VALSEP);
65
+ const job_id = jid.join(VALSEP);
245
66
  return {
246
- type: action,
67
+ index,
68
+ action,
247
69
  topic,
248
70
  gid,
249
- jid: jobId,
250
- } as unknown as DependencyExport;
251
- });
252
- }
253
-
254
- /**
255
- * Adds historical actions (proxyActivity, executeChild)
256
- * using the `dependency list` to determine
257
- * after-the-fact what happened within the 'black-box'
258
- * worker function. This is necessary to interleave the
259
- * actions into the replay timeline, given that it isn't
260
- * really possible to know the inner-workings of the user's
261
- * function
262
- *
263
- */
264
- seedActions(type: 'flow'|'hook'|'other', action: string, topic: string, dep: string, prefix: string, dimensionKey: string, actions: JobActionExport, jobId: string) {
265
- if (type !== 'other' && action === 'expire-child') {
266
- let depType: string;
267
- if (topic == `${this.appId}.activity.execute`) {
268
- depType = 'proxyActivity';
269
- } else if (topic == `${this.appId}.execute`) {
270
- depType = 'executeChild';
271
- } else if (topic == `${this.appId}.wfsc.execute`) {
272
- depType = 'waitForSignal';
273
- }
274
-
275
- if (depType) {
276
- if (type === 'flow') {
277
- actions.main.items.push([prefix, depType, jobId]);
278
- } else if (type === 'hook') {
279
- if (!actions.hooks[dimensionKey]) {
280
- actions.hooks[dimensionKey] = {
281
- cursor: -1,
282
- items: [],
283
- };
284
- }
285
- actions.hooks[dimensionKey].items.push([prefix, depType, jobId]);
286
- }
71
+ dimension,
72
+ job_id,
287
73
  }
288
- }
74
+ });
289
75
  }
290
76
 
291
77
  /**
@@ -294,17 +80,12 @@ class ExporterService {
294
80
  * @param dependencyList - the list of dependencies for the job
295
81
  * @returns - the inflated job data
296
82
  */
297
- inflate(jobHash: StringStringType, dependencyList: string[]): DurableJobExport {
298
- //the list of actions taken in the workflow and hook functions
299
- const actions: JobActionExport = {
300
- hooks: {},
301
- main: { cursor: -1, items: [] },
302
- };
303
- const dependencies = this.inflateDependencyData(dependencyList, actions);
83
+ inflate(jobHash: StringStringType): DurableJobExport {
84
+ const idempotents: IdemType[] = [];
304
85
  const state: StringStringType = {};
305
86
  const data: StringStringType = {};
306
87
  const other: ExportItem[] = [];
307
- const replay: ExportItem[] = [];
88
+ const replay: Record<string, TimelineEntry> = {};
308
89
  const regex = /^([a-zA-Z]{3}),(\d+(?:,\d+)*)/;
309
90
 
310
91
  Object.entries(jobHash).forEach(([key, value]) => {
@@ -314,104 +95,115 @@ class ExporterService {
314
95
  this.inflateProcess(match, value, replay);
315
96
  } else if (key.length === 3) {
316
97
  //job state
317
- state[this.inflateKey(key)] = this.serializer.fromString(value);
98
+ state[this.inflateKey(key)] = SerializerService.fromString(value);
318
99
  } else if (key.startsWith('_')) {
319
100
  //job data
320
101
  data[key.substring(1)] = value;
321
102
  } else if (key.startsWith('-')) {
322
103
  //actions with side effect (replayable)
323
- this.inflateActions(key, value, actions);
104
+ idempotents.push({
105
+ key,
106
+ value: SerializerService.fromString(value),
107
+ parts: extractParts(key),
108
+ });
324
109
  } else {
325
110
  //collator guids, etc
326
111
  other.push([null, key, value]);
327
112
  }
328
113
  });
329
114
 
330
- replay.sort(this.dateSort)
331
- actions.main.items.sort(this.reverseSort);
332
- Object.entries(actions.hooks).forEach(([key, value]) => {
333
- value.items.sort(this.reverseSort);
334
- });
115
+ const sortEntriesByCreated = (obj: { [key: string]: TimelineEntry }): TimelineEntry[] => {
116
+ const entriesArray: TimelineEntry[] = Object.values(obj);
117
+ entriesArray.sort((a, b) => {
118
+ return (a.created || a.updated).localeCompare(b.created || b.updated);
119
+ });
120
+ return entriesArray;
121
+ }
122
+
123
+ /**
124
+ * idem list has a complicated sort order based on indexes and dimensions
125
+ */
126
+ const sortParts = (parts: IdemType[]): IdemType[]=> {
127
+ return parts.sort((a, b) => {
128
+ const { dimension: aDim, index: aIdx, secondary: aSec } = a.parts;
129
+ const { dimension: bDim, index: bIdx, secondary: bSec } = b.parts;
130
+
131
+ if (aDim === undefined && bDim !== undefined) return -1;
132
+ if (aDim !== undefined && bDim === undefined) return 1;
133
+ if (aDim !== undefined && bDim !== undefined) {
134
+ if (aDim < bDim) return -1;
135
+ if (aDim > bDim) return 1;
136
+ }
137
+
138
+ if (aIdx < bIdx) return -1;
139
+ if (aIdx > bIdx) return 1;
140
+
141
+ if (aSec === undefined && bSec !== undefined) return -1;
142
+ if (aSec !== undefined && bSec === undefined) return 1;
143
+ if (aSec !== undefined && bSec !== undefined) {
144
+ if (aSec < bSec) return -1;
145
+ if (aSec > bSec) return 1;
146
+ }
147
+
148
+ return 0;
149
+ });
150
+ };
335
151
 
152
+ function extractParts(key: string): {index: number, dimension?: string, secondary?: number} {
153
+ function extractDimension(label: string): string {
154
+ const parts = label.split(',');
155
+ if (parts.length > 1) {
156
+ parts.shift();
157
+ return parts.join(',');
158
+ }
159
+ }
160
+
161
+ const parts = key.split('-');
162
+ if (parts.length === 4) {
163
+ //-proxy-5- -search-1-1-
164
+ return {
165
+ index: parseInt(parts[2], 10),
166
+ dimension: extractDimension(parts[1]),
167
+ }
168
+ } else {
169
+ //-search,0,0-1-1- -proxy,0,0-1-
170
+ return {
171
+ index: parseInt(parts[2], 10),
172
+ secondary: parseInt(parts[3], 10),
173
+ dimension: extractDimension(parts[1]),
174
+ }
175
+ }
176
+ }
177
+
336
178
  return {
337
179
  data: restoreHierarchy(data),
338
- dependencies,
180
+ idempotents: sortParts(idempotents),
339
181
  state: Object.entries(restoreHierarchy(state))[0][1],
340
182
  status: jobHash[':'],
341
- timeline: this.createTimeline(replay, actions),
342
- transitions: { ...this.transitions },
343
- cycles: { ...this.cycles },
183
+ replay: sortEntriesByCreated(replay),
344
184
  };
345
185
  }
346
186
 
347
- inflateProcess(match: RegExpMatchArray, value: string, replay: ExportItem[]) {
348
- const [_, letters, numbers] = match;
187
+ inflateProcess(match: RegExpMatchArray, value: string, replay: Record<string, Record<string, any>>) {
188
+ const [_, letters, dimensions] = match;
349
189
  const path = this.inflateKey(letters);
350
- if (path.endsWith('/output/metadata/ac') ||
351
- path.endsWith('/output/metadata/au')) {
352
- const dimensions = `/${numbers.replace(/,/g, '/')}`;
353
- const resolved = this.serializer.fromString(value);
354
- replay.push([
355
- dimensions,
356
- path,
357
- resolved,
358
- ]);
359
- }
360
- }
361
-
362
- inflateActions(key: string, value: string, actions: JobActionExport) {
363
- let [_, dimensionalType, counter, subcounter] = key.split('-');
364
- if (subcounter) {
365
- counter = `${counter}.${subcounter}`;
366
- }
367
- const [type, ...dimensions] = dimensionalType.split(',');
368
- let dimensionKey = '';
369
- let isHook = false;
370
- if (dimensions.length > 0) {
371
- dimensionKey = `/${dimensions.join('/')}`;
372
- isHook = true;
373
- }
374
- let targetList: ExportItem[];
375
- if (isHook) {
376
- if (!actions.hooks[dimensionKey]) {
377
- actions.hooks[dimensionKey] = {
378
- cursor: -1,
379
- items: [],
190
+ const parts = path.split('/');
191
+ const activity = parts[0];
192
+ const isCreate = path.endsWith('/output/metadata/ac');
193
+ const isUpdate = path.endsWith('/output/metadata/au');
194
+ if (isCreate || isUpdate) {
195
+ const targetName = `${activity},${dimensions}`;
196
+ let target = replay[targetName];
197
+ if (!target) {
198
+ replay[targetName] = {
199
+ activity,
200
+ dimensions,
201
+ created: isCreate ? value : null,
202
+ updated: isUpdate ? value : null,
380
203
  };
204
+ } else {
205
+ target[isCreate ? 'created' : 'updated'] = value;
381
206
  }
382
- targetList = actions.hooks[dimensionKey].items;
383
- } else {
384
- targetList = actions.main.items;
385
- }
386
- targetList.push([
387
- `${dimensionKey}[${counter}]`,
388
- type,
389
- value,
390
- ]);
391
- }
392
-
393
- reverseSort(aKey: ExportItem, bKey: ExportItem) {
394
- if (aKey[0] > bKey[0]) {
395
- return 1;
396
- } else if (aKey[0] < bKey[0]) {
397
- return -1;
398
- } else {
399
- if (aKey[1] > bKey[1]) {
400
- return 1;
401
- } else if (aKey[1] < bKey[1]) {
402
- return -1;
403
- }
404
- return 0;
405
- }
406
- }
407
-
408
- dateSort(aKey: ExportItem, bKey: ExportItem) {
409
- if (aKey[2] > bKey[2]) {
410
- return 1;
411
- } else if (aKey[2] < bKey[2]) {
412
- return -1;
413
- } else {
414
- return 0;
415
207
  }
416
208
  }
417
209
  }