@hotmeshio/hotmesh 0.0.55 → 0.0.57

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 (182) hide show
  1. package/README.md +1 -1
  2. package/build/modules/enums.js +1 -10
  3. package/build/modules/key.d.ts +0 -38
  4. package/build/modules/key.js +4 -46
  5. package/build/modules/utils.d.ts +0 -8
  6. package/build/modules/utils.js +0 -14
  7. package/build/package.json +11 -4
  8. package/build/services/activities/activity.d.ts +0 -28
  9. package/build/services/activities/activity.js +1 -46
  10. package/build/services/activities/await.js +0 -4
  11. package/build/services/activities/cycle.d.ts +0 -7
  12. package/build/services/activities/cycle.js +1 -16
  13. package/build/services/activities/hook.d.ts +0 -6
  14. package/build/services/activities/hook.js +2 -12
  15. package/build/services/activities/interrupt.js +0 -8
  16. package/build/services/activities/signal.d.ts +0 -6
  17. package/build/services/activities/signal.js +0 -15
  18. package/build/services/activities/trigger.d.ts +0 -4
  19. package/build/services/activities/trigger.js +1 -7
  20. package/build/services/activities/worker.js +0 -4
  21. package/build/services/collator/index.d.ts +0 -70
  22. package/build/services/collator/index.js +1 -91
  23. package/build/services/compiler/deployer.js +6 -38
  24. package/build/services/compiler/index.d.ts +0 -15
  25. package/build/services/compiler/index.js +0 -20
  26. package/build/services/compiler/validator.d.ts +0 -3
  27. package/build/services/compiler/validator.js +0 -25
  28. package/build/services/connector/clients/ioredis.d.ts +2 -2
  29. package/build/services/connector/clients/ioredis.js +0 -2
  30. package/build/services/connector/clients/redis.d.ts +4 -4
  31. package/build/services/connector/clients/redis.js +1 -3
  32. package/build/services/connector/index.d.ts +1 -1
  33. package/build/services/connector/index.js +0 -2
  34. package/build/services/durable/client.d.ts +1 -26
  35. package/build/services/durable/client.js +0 -56
  36. package/build/services/durable/exporter.d.ts +0 -22
  37. package/build/services/durable/exporter.js +1 -30
  38. package/build/services/durable/handle.d.ts +0 -36
  39. package/build/services/durable/handle.js +0 -46
  40. package/build/services/durable/index.d.ts +0 -4
  41. package/build/services/durable/index.js +0 -4
  42. package/build/services/durable/schemas/factory.d.ts +0 -29
  43. package/build/services/durable/schemas/factory.js +0 -29
  44. package/build/services/durable/search.d.ts +1 -36
  45. package/build/services/durable/search.js +56 -56
  46. package/build/services/durable/worker.js +2 -22
  47. package/build/services/durable/workflow.d.ts +0 -114
  48. package/build/services/durable/workflow.js +1 -141
  49. package/build/services/engine/index.d.ts +1 -6
  50. package/build/services/engine/index.js +1 -43
  51. package/build/services/exporter/index.d.ts +0 -27
  52. package/build/services/exporter/index.js +0 -33
  53. package/build/services/hotmesh/index.d.ts +2 -2
  54. package/build/services/hotmesh/index.js +1 -9
  55. package/build/services/logger/index.js +0 -2
  56. package/build/services/mapper/index.d.ts +0 -14
  57. package/build/services/mapper/index.js +0 -14
  58. package/build/services/pipe/functions/date.d.ts +0 -7
  59. package/build/services/pipe/functions/date.js +0 -7
  60. package/build/services/pipe/functions/math.js +0 -2
  61. package/build/services/pipe/index.d.ts +0 -15
  62. package/build/services/pipe/index.js +2 -23
  63. package/build/services/quorum/index.d.ts +0 -7
  64. package/build/services/quorum/index.js +0 -21
  65. package/build/services/reporter/index.d.ts +0 -5
  66. package/build/services/reporter/index.js +0 -9
  67. package/build/services/router/index.d.ts +0 -9
  68. package/build/services/router/index.js +2 -38
  69. package/build/services/serializer/index.js +7 -26
  70. package/build/services/store/cache.d.ts +0 -18
  71. package/build/services/store/cache.js +0 -18
  72. package/build/services/store/clients/ioredis.d.ts +1 -1
  73. package/build/services/store/clients/ioredis.js +0 -1
  74. package/build/services/store/clients/redis.d.ts +1 -1
  75. package/build/services/store/index.d.ts +0 -55
  76. package/build/services/store/index.js +5 -81
  77. package/build/services/stream/clients/ioredis.d.ts +1 -1
  78. package/build/services/stream/clients/ioredis.js +1 -4
  79. package/build/services/stream/clients/redis.d.ts +1 -1
  80. package/build/services/sub/clients/ioredis.d.ts +1 -1
  81. package/build/services/sub/clients/redis.d.ts +1 -1
  82. package/build/services/task/index.d.ts +0 -9
  83. package/build/services/task/index.js +0 -31
  84. package/build/services/telemetry/index.d.ts +0 -7
  85. package/build/services/telemetry/index.js +1 -13
  86. package/build/services/worker/index.d.ts +0 -4
  87. package/build/services/worker/index.js +2 -6
  88. package/build/types/activity.d.ts +0 -81
  89. package/build/types/durable.d.ts +26 -177
  90. package/build/types/exporter.d.ts +0 -13
  91. package/build/types/hotmesh.d.ts +4 -16
  92. package/build/types/hotmesh.js +0 -3
  93. package/build/types/index.d.ts +4 -6
  94. package/build/types/index.js +4 -3
  95. package/build/types/job.d.ts +1 -86
  96. package/build/types/pipe.d.ts +0 -65
  97. package/build/types/quorum.d.ts +15 -10
  98. package/build/types/redis.d.ts +225 -7
  99. package/build/types/redis.js +9 -0
  100. package/build/types/stream.d.ts +0 -58
  101. package/build/types/stream.js +0 -4
  102. package/package.json +11 -4
  103. package/types/durable.ts +131 -4
  104. package/types/hotmesh.ts +3 -6
  105. package/types/index.ts +23 -10
  106. package/types/job.ts +1 -1
  107. package/types/quorum.ts +22 -0
  108. package/types/redis.ts +267 -18
  109. package/build/types/ioredisclient.d.ts +0 -5
  110. package/build/types/ioredisclient.js +0 -5
  111. package/build/types/redisclient.d.ts +0 -26
  112. package/build/types/redisclient.js +0 -2
  113. package/modules/enums.ts +0 -62
  114. package/modules/errors.ts +0 -280
  115. package/modules/key.ts +0 -101
  116. package/modules/storage.ts +0 -3
  117. package/modules/utils.ts +0 -242
  118. package/services/activities/activity.ts +0 -589
  119. package/services/activities/await.ts +0 -113
  120. package/services/activities/cycle.ts +0 -115
  121. package/services/activities/hook.ts +0 -197
  122. package/services/activities/index.ts +0 -19
  123. package/services/activities/interrupt.ts +0 -172
  124. package/services/activities/signal.ts +0 -148
  125. package/services/activities/trigger.ts +0 -295
  126. package/services/activities/worker.ts +0 -107
  127. package/services/collator/README.md +0 -102
  128. package/services/collator/index.ts +0 -291
  129. package/services/compiler/deployer.ts +0 -504
  130. package/services/compiler/index.ts +0 -98
  131. package/services/compiler/validator.ts +0 -158
  132. package/services/connector/clients/ioredis.ts +0 -57
  133. package/services/connector/clients/redis.ts +0 -72
  134. package/services/connector/index.ts +0 -42
  135. package/services/durable/client.ts +0 -266
  136. package/services/durable/connection.ts +0 -10
  137. package/services/durable/exporter.ts +0 -232
  138. package/services/durable/handle.ts +0 -160
  139. package/services/durable/index.ts +0 -27
  140. package/services/durable/schemas/factory.ts +0 -2358
  141. package/services/durable/search.ts +0 -196
  142. package/services/durable/worker.ts +0 -401
  143. package/services/durable/workflow.ts +0 -557
  144. package/services/engine/index.ts +0 -761
  145. package/services/exporter/index.ts +0 -146
  146. package/services/hotmesh/index.ts +0 -237
  147. package/services/logger/index.ts +0 -79
  148. package/services/mapper/index.ts +0 -89
  149. package/services/pipe/functions/array.ts +0 -78
  150. package/services/pipe/functions/bitwise.ts +0 -27
  151. package/services/pipe/functions/conditional.ts +0 -35
  152. package/services/pipe/functions/date.ts +0 -220
  153. package/services/pipe/functions/index.ts +0 -27
  154. package/services/pipe/functions/json.ts +0 -11
  155. package/services/pipe/functions/logical.ts +0 -11
  156. package/services/pipe/functions/math.ts +0 -217
  157. package/services/pipe/functions/number.ts +0 -75
  158. package/services/pipe/functions/object.ts +0 -98
  159. package/services/pipe/functions/string.ts +0 -86
  160. package/services/pipe/functions/symbol.ts +0 -39
  161. package/services/pipe/functions/unary.ts +0 -19
  162. package/services/pipe/index.ts +0 -216
  163. package/services/quorum/index.ts +0 -319
  164. package/services/reporter/index.ts +0 -387
  165. package/services/router/index.ts +0 -426
  166. package/services/serializer/README.md +0 -10
  167. package/services/serializer/index.ts +0 -285
  168. package/services/store/cache.ts +0 -172
  169. package/services/store/clients/ioredis.ts +0 -145
  170. package/services/store/clients/redis.ts +0 -191
  171. package/services/store/index.ts +0 -1091
  172. package/services/stream/clients/ioredis.ts +0 -157
  173. package/services/stream/clients/redis.ts +0 -158
  174. package/services/stream/index.ts +0 -58
  175. package/services/sub/clients/ioredis.ts +0 -83
  176. package/services/sub/clients/redis.ts +0 -74
  177. package/services/sub/index.ts +0 -25
  178. package/services/task/index.ts +0 -250
  179. package/services/telemetry/index.ts +0 -273
  180. package/services/worker/index.ts +0 -248
  181. package/types/ioredisclient.ts +0 -10
  182. package/types/redisclient.ts +0 -30
@@ -1,1091 +0,0 @@
1
- import {
2
- KeyService,
3
- KeyStoreParams,
4
- KeyType,
5
- HMNS,
6
- VALSEP,
7
- TYPSEP} from '../../modules/key';
8
- import { ILogger } from '../logger';
9
- import { MDATA_SYMBOLS, SerializerService as Serializer } from '../serializer';
10
- import { Cache } from './cache';
11
- import { ActivityType, Consumes} from '../../types/activity';
12
- import { AppVID } from '../../types/app';
13
- import {
14
- HookRule,
15
- HookSignal } from '../../types/hook';
16
- import {
17
- HotMeshApp,
18
- HotMeshApps,
19
- HotMeshSettings } from '../../types/hotmesh';
20
- import {
21
- SymbolSets,
22
- StringStringType,
23
- StringAnyType,
24
- Symbols } from '../../types/serializer';
25
- import {
26
- IdsData,
27
- JobStats,
28
- JobStatsRange,
29
- StatsType } from '../../types/stats';
30
- import { Transitions } from '../../types/transition';
31
- import { formatISODate, getSymKey } from '../../modules/utils';
32
- import { ReclaimedMessageType } from '../../types/stream';
33
- import { JobCompletionOptions, JobInterruptOptions } from '../../types/job';
34
- import { HMSH_SCOUT_INTERVAL_SECONDS, HMSH_CODE_INTERRUPT } from '../../modules/enums';
35
- import { GetStateError } from '../../modules/errors';
36
- import { WorkListTaskType } from '../../types/task';
37
-
38
- interface AbstractRedisClient {
39
- exec(): any;
40
- }
41
-
42
- abstract class StoreService<T, U extends AbstractRedisClient> {
43
- redisClient: T;
44
- cache: Cache;
45
- serializer: Serializer;
46
- namespace: string;
47
- appId: string
48
- logger: ILogger;
49
- commands: Record<string, string> = {
50
- set: 'set',
51
- setnx: 'setnx',
52
- del: 'del',
53
- expire: 'expire',
54
- hset: 'hset',
55
- hscan: 'hscan',
56
- hsetnx: 'hsetnx',
57
- hincrby: 'hincrby',
58
- hdel: 'hdel',
59
- hget: 'hget',
60
- hmget: 'hmget',
61
- hgetall: 'hgetall',
62
- hincrbyfloat: 'hincrbyfloat',
63
- zrange: 'zrange',
64
- zrangebyscore_withscores: 'zrangebyscore',
65
- zrangebyscore: 'zrangebyscore',
66
- zrem: 'zrem',
67
- zadd: 'zadd',
68
- lmove: 'lmove',
69
- llen: 'llen',
70
- lpop: 'lpop',
71
- lrange: 'lrange',
72
- rename: 'rename',
73
- rpush: 'rpush',
74
- scan: 'scan',
75
- xack: 'xack',
76
- xdel: 'xdel',
77
- };
78
-
79
-
80
- //todo: standardize signatures and move concrete methods to this class
81
- abstract getMulti(): U;
82
- abstract exec(...args: any[]): Promise<string|string[]|string[][]>;
83
- abstract publish(
84
- keyType: KeyType.QUORUM,
85
- message: Record<string, any>,
86
- appId: string,
87
- engineId?: string
88
- ): Promise<boolean>;
89
- abstract xgroup(
90
- command: 'CREATE',
91
- key: string,
92
- groupName: string,
93
- id: string,
94
- mkStream?: 'MKSTREAM'
95
- ): Promise<boolean>;
96
- abstract xadd(
97
- key: string,
98
- id: string,
99
- messageId: string,
100
- messageValue: string,
101
- multi?: U): Promise<string | U>;
102
- abstract xpending(
103
- key: string,
104
- group: string,
105
- start?: string,
106
- end?: string,
107
- count?: number,
108
- consumer?: string): Promise<[string, string, number, [string, number][]][] | [string, string, number, number] | unknown[]>;
109
- abstract xclaim(
110
- key: string,
111
- group: string,
112
- consumer: string,
113
- minIdleTime: number,
114
- id: string,
115
- ...args: string[]): Promise<ReclaimedMessageType>;
116
- abstract xack(
117
- key: string,
118
- group: string,
119
- id: string,
120
- multi?: U
121
- ): Promise<number|U>;
122
- abstract xdel(
123
- key: string,
124
- id: string,
125
- multi?: U
126
- ): Promise<number|U>;
127
- abstract xlen(
128
- key: string,
129
- multi?: U
130
- ): Promise<number|U>;
131
-
132
- constructor(redisClient: T) {
133
- this.redisClient = redisClient;
134
- }
135
-
136
- async init(namespace = HMNS, appId: string, logger: ILogger): Promise<HotMeshApps> {
137
- this.namespace = namespace;
138
- this.appId = appId;
139
- this.logger = logger;
140
- const settings = await this.getSettings(true);
141
- this.cache = new Cache(appId, settings);
142
- this.serializer = new Serializer();
143
- await this.getApp(appId);
144
- return this.cache.getApps();
145
- }
146
-
147
- isSuccessful(result: any): boolean {
148
- return result > 0 || result === 'OK' || result === true;
149
- }
150
-
151
- async zAdd(key: string, score: number | string, value: string | number, redisMulti?: U): Promise<any> {
152
- //default call signature uses 'ioredis' NPM Package format
153
- return await (redisMulti || this.redisClient)[this.commands.zadd](key, score, value);
154
- }
155
-
156
- async zRangeByScoreWithScores(key: string, score: number | string, value: string | number): Promise<string | null> {
157
- const result = await this.redisClient[this.commands.zrangebyscore_withscores](key, score, value, 'WITHSCORES');
158
- if (result?.length > 0) {
159
- return result[0];
160
- }
161
- return null;
162
- }
163
-
164
- async zRangeByScore(key: string, score: number | string, value: string | number): Promise<string | null> {
165
- const result = await this.redisClient[this.commands.zrangebyscore](key, score, value);
166
- if (result?.length > 0) {
167
- return result[0];
168
- }
169
- return null;
170
- }
171
-
172
- mintKey(type: KeyType, params: KeyStoreParams): string {
173
- if (!this.namespace) throw new Error('namespace not set');
174
- return KeyService.mintKey(this.namespace, type, params);
175
- }
176
-
177
- invalidateCache() {
178
- this.cache.invalidate();
179
- }
180
-
181
- /**
182
- * At any given time only a single engine will
183
- * check for and process work items in the
184
- * time and signal task queues.
185
- */
186
- async reserveScoutRole(scoutType: 'time' | 'signal' | 'activate', delay = HMSH_SCOUT_INTERVAL_SECONDS): Promise<boolean> {
187
- const key = this.mintKey(KeyType.WORK_ITEMS, { appId: this.appId, scoutType });
188
- const success = await this.exec('SET', key, `${scoutType}:${formatISODate(new Date())}`, 'NX', 'EX', `${delay - 1}`);
189
- return this.isSuccessful(success);
190
- }
191
-
192
- async releaseScoutRole(scoutType: 'time' | 'signal' | 'activate'): Promise<boolean> {
193
- const key = this.mintKey(KeyType.WORK_ITEMS, { appId: this.appId, scoutType });
194
- const success = await this.exec('DEL', key);
195
- return this.isSuccessful(success);
196
- }
197
-
198
- async getSettings(bCreate = false): Promise<HotMeshSettings> {
199
- let settings = this.cache?.getSettings();
200
- if (settings) {
201
- return settings;
202
- } else {
203
- if (bCreate) {
204
- const packageJson = await import('../../package.json');
205
- const version: string = packageJson['version'] || '0.0.0';
206
- settings = { namespace: HMNS, version } as HotMeshSettings;
207
- await this.setSettings(settings);
208
- return settings;
209
- }
210
- }
211
- throw new Error('settings not found');
212
- }
213
-
214
- async setSettings(manifest: HotMeshSettings): Promise<any> {
215
- //HotMesh heartbeat. If a connection is made, the version will be set
216
- const params: KeyStoreParams = {};
217
- const key = this.mintKey(KeyType.HOTMESH, params);
218
- return await this.redisClient[this.commands.hset](key, manifest);
219
- }
220
-
221
- async reserveSymbolRange(target: string, size: number, type: 'JOB' | 'ACTIVITY'): Promise<[number, number, Symbols]> {
222
- const rangeKey = this.mintKey(KeyType.SYMKEYS, { appId: this.appId });
223
- const symbolKey = this.mintKey(KeyType.SYMKEYS, { activityId: target, appId: this.appId });
224
- //reserve the slot in a `pending` state (range will be established in the next step)
225
- const response = await this.redisClient[this.commands.hsetnx](rangeKey, target, '?:?');
226
- if (response) {
227
- //if the key didn't exist, set the inclusive range and seed metadata fields
228
- const upperLimit = await this.redisClient[this.commands.hincrby](rangeKey, ':cursor', size);
229
- const lowerLimit = upperLimit - size;
230
- const inclusiveRange = `${lowerLimit}:${upperLimit - 1}`;
231
- await this.redisClient[this.commands.hset](rangeKey, target, inclusiveRange);
232
- const metadataSeeds = this.seedSymbols(target, type, lowerLimit);
233
- await this.redisClient[this.commands.hset](symbolKey, metadataSeeds);
234
- return [lowerLimit + MDATA_SYMBOLS.SLOTS, upperLimit - 1, {} as Symbols];
235
- } else {
236
- //if the key already existed, get the lower limit and add the number of symbols
237
- const range = await this.redisClient[this.commands.hget](rangeKey, target);
238
- const [lowerLimitString] = range.split(':');
239
- const lowerLimit = parseInt(lowerLimitString, 10);
240
- const symbols = await this.redisClient[this.commands.hgetall](symbolKey);
241
- const symbolCount = Object.keys(symbols).length;
242
- const actualLowerLimit = lowerLimit + MDATA_SYMBOLS.SLOTS + symbolCount;
243
- const upperLimit = Number(lowerLimit + size - 1);
244
- return [actualLowerLimit, upperLimit, symbols as Symbols];
245
- }
246
- }
247
-
248
- async getAllSymbols(): Promise<Symbols> {
249
- //get hash with all reserved symbol ranges
250
- const rangeKey = this.mintKey(KeyType.SYMKEYS, { appId: this.appId });
251
- const ranges = await this.redisClient[this.commands.hgetall](rangeKey);
252
- const rangeKeys = Object.keys(ranges).sort();
253
- delete rangeKeys[':cursor'];
254
- const multi = this.getMulti();
255
- for (const rangeKey of rangeKeys) {
256
- const symbolKey = this.mintKey(KeyType.SYMKEYS, { activityId: rangeKey, appId: this.appId });
257
- multi[this.commands.hgetall](symbolKey);
258
- }
259
- const results = await multi.exec() as Array<[null, Symbols]> | Array<Symbols>;
260
-
261
- const symbolSets: Symbols = {};
262
- results.forEach((result: [null, Symbols] | Symbols, index: number) => {
263
- if (result) {
264
- let vals: Symbols;
265
- if (Array.isArray(result) && result.length === 2) {
266
- vals = result[1];
267
- } else {
268
- vals = result as Symbols;
269
- }
270
- for (const [key, value] of Object.entries(vals)) {
271
- symbolSets[value as string] = key.startsWith(rangeKeys[index]) ? key : `${rangeKeys[index]}/${key}`;
272
- }
273
- }
274
- });
275
- return symbolSets;
276
- }
277
-
278
- async getSymbols(activityId: string): Promise<Symbols> {
279
- let symbols: Symbols = this.cache.getSymbols(this.appId, activityId);
280
- if (symbols) {
281
- return symbols;
282
- } else {
283
- const params: KeyStoreParams = { activityId, appId: this.appId };
284
- const key = this.mintKey(KeyType.SYMKEYS, params);
285
- symbols = (await this.redisClient[this.commands.hgetall](key)) as Symbols;
286
- this.cache.setSymbols(this.appId, activityId, symbols);
287
- return symbols;
288
- }
289
- }
290
-
291
- async addSymbols(activityId: string, symbols: Symbols): Promise<boolean> {
292
- if (!symbols || !Object.keys(symbols).length) return false;
293
- const params: KeyStoreParams = { activityId, appId: this.appId };
294
- const key = this.mintKey(KeyType.SYMKEYS, params);
295
- const success = await this.redisClient[this.commands.hset](key, symbols);
296
- this.cache.deleteSymbols(this.appId, activityId);
297
- return success > 0;
298
- }
299
-
300
- seedSymbols(target: string, type: 'JOB'|'ACTIVITY', startIndex: number): StringStringType {
301
- if (type === 'JOB') {
302
- return this.seedJobSymbols(startIndex);
303
- }
304
- return this.seedActivitySymbols(startIndex, target);
305
- }
306
-
307
- seedJobSymbols(startIndex: number): StringStringType {
308
- const hash: StringStringType = {};
309
- MDATA_SYMBOLS.JOB.KEYS.forEach((key) => {
310
- hash[`metadata/${key}`] = getSymKey(startIndex);
311
- startIndex++;
312
- });
313
- return hash;
314
- }
315
-
316
- seedActivitySymbols(startIndex: number, activityId: string): StringStringType {
317
- const hash: StringStringType = {};
318
- MDATA_SYMBOLS.ACTIVITY.KEYS.forEach((key) => {
319
- hash[`${activityId}/output/metadata/${key}`] = getSymKey(startIndex);
320
- startIndex++;
321
- });
322
- return hash;
323
- }
324
-
325
- async getSymbolValues(): Promise<Symbols> {
326
- let symvals: Symbols = this.cache.getSymbolValues(this.appId);
327
- if (symvals) {
328
- return symvals;
329
- } else {
330
- const key = this.mintKey(KeyType.SYMVALS, { appId: this.appId });
331
- symvals = await this.redisClient[this.commands.hgetall](key);
332
- this.cache.setSymbolValues(this.appId, symvals as Symbols);
333
- return symvals;
334
- }
335
- }
336
-
337
- async addSymbolValues(symvals: Symbols): Promise<boolean> {
338
- if (!symvals || !Object.keys(symvals).length) return false;
339
- const key = this.mintKey(KeyType.SYMVALS, { appId: this.appId });
340
- const success = await this.redisClient[this.commands.hset](key, symvals);
341
- this.cache.deleteSymbolValues(this.appId);
342
- return this.isSuccessful(success);
343
- }
344
-
345
- async getSymbolKeys(symbolNames: string[]): Promise<SymbolSets> {
346
- const symbolLookups = [];
347
- for (const symbolName of symbolNames) {
348
- symbolLookups.push(this.getSymbols(symbolName));
349
- }
350
- const symbolSets = await Promise.all(symbolLookups);
351
- const symKeys: SymbolSets = {};
352
- for (const symbolName of symbolNames) {
353
- symKeys[symbolName] = symbolSets.shift();
354
- }
355
- return symKeys;
356
- }
357
-
358
- async getApp(id: string, refresh = false): Promise<HotMeshApp> {
359
- let app: Partial<HotMeshApp> = this.cache.getApp(id);
360
- if (refresh || !(app && Object.keys(app).length > 0)) {
361
- const params: KeyStoreParams = { appId: id };
362
- const key = this.mintKey(KeyType.APP, params);
363
- const sApp = await this.redisClient[this.commands.hgetall](key);
364
- if (!sApp) return null;
365
- app = {};
366
- for (const field in sApp) {
367
- try {
368
- if (field === 'active') {
369
- app[field] = sApp[field] === 'true';
370
- } else {
371
- app[field] = sApp[field];
372
- }
373
- } catch (e) {
374
- app[field] = sApp[field];
375
- }
376
- }
377
- this.cache.setApp(id, app as HotMeshApp);
378
- }
379
- return app as HotMeshApp;
380
- }
381
-
382
- async setApp(id: string, version: string): Promise<HotMeshApp> {
383
- const params: KeyStoreParams = { appId: id };
384
- const key = this.mintKey(KeyType.APP, params);
385
- const versionId = `versions/${version}`;
386
- const payload: HotMeshApp = {
387
- id,
388
- version,
389
- [versionId]: `deployed:${formatISODate(new Date())}`,
390
- };
391
- await this.redisClient[this.commands.hset](key, payload as any);
392
- this.cache.setApp(id, payload);
393
- return payload;
394
- }
395
-
396
- async activateAppVersion(id: string, version: string): Promise<boolean> {
397
- const params: KeyStoreParams = { appId: id };
398
- const key = this.mintKey(KeyType.APP, params);
399
- const versionId = `versions/${version}`;
400
- const app = await this.getApp(id, true);
401
- if (app && app[versionId]) {
402
- const payload: HotMeshApp = {
403
- id,
404
- version: version.toString(),
405
- [versionId]: `activated:${formatISODate(new Date())}`,
406
- active: true
407
- };
408
- Object.entries(payload).forEach(([key, value]) => {
409
- payload[key] = value.toString();
410
- });
411
- await this.redisClient[this.commands.hset](key, payload as any);
412
- return true;
413
- }
414
- throw new Error(`Version ${version} does not exist for app ${id}`);
415
- }
416
-
417
- async registerAppVersion(appId: string, version: string): Promise<any> {
418
- const params: KeyStoreParams = { appId };
419
- const key = this.mintKey(KeyType.APP, params);
420
- const payload: HotMeshApp = {
421
- id: appId,
422
- version: version.toString(),
423
- [`versions/${version}`]: formatISODate(new Date()),
424
- };
425
- return await this.redisClient[this.commands.hset](key, payload as any);
426
- }
427
-
428
- /**
429
- * Registers the job, `jobId`, with `originJobId`. In the future,
430
- * when `originJobId` is interrupted/expired, the items in the
431
- * list (added via RPUSH) will be interrupted/expired (removed via LPOPed).
432
- */
433
- async registerJobDependency(depType: WorkListTaskType, originJobId: string, topic: string, jobId: string, gId: string, pd = '', multi? : U): Promise<any> {
434
- const privateMulti = multi || this.getMulti();
435
- const dependencyParams = {
436
- appId: this.appId,
437
- jobId: originJobId,
438
- };
439
- const depKey = this.mintKey(
440
- KeyType.JOB_DEPENDENTS,
441
- dependencyParams,
442
- );
443
- const expireTask = [
444
- depType,
445
- topic,
446
- gId,
447
- pd,
448
- jobId,
449
- ].join(VALSEP);
450
- privateMulti[this.commands.rpush](depKey, expireTask);
451
- if (!multi) {
452
- return await privateMulti.exec();
453
- }
454
- }
455
-
456
- /**
457
- * Ensures a `hook signal` is delisted when its parent activity/job
458
- * is interrupted/expired.
459
- */
460
- async registerSignalDependency(jobId: string, signalKey: string, dad: string, multi? : U): Promise<any> {
461
- const privateMulti = multi || this.getMulti();
462
- const dependencyParams = { appId: this.appId, jobId };
463
- const dependencyKey = this.mintKey(
464
- KeyType.JOB_DEPENDENTS,
465
- dependencyParams,
466
- );
467
- //persiste dependency tasks as multi-segment composite keys
468
- const delistTask = [
469
- 'delist',
470
- 'signal',
471
- jobId,
472
- dad,
473
- signalKey].join(VALSEP);
474
- privateMulti[this.commands.rpush](
475
- dependencyKey,
476
- delistTask,
477
- );
478
- if (!multi) {
479
- return await privateMulti.exec();
480
- }
481
- }
482
-
483
- async setStats(jobKey: string, jobId: string, dateTime: string, stats: StatsType, appVersion: AppVID, multi? : U): Promise<any> {
484
- const params: KeyStoreParams = { appId: appVersion.id, jobId, jobKey, dateTime };
485
- const privateMulti = multi || this.getMulti();
486
- if (stats.general.length) {
487
- const generalStatsKey = this.mintKey(KeyType.JOB_STATS_GENERAL, params);
488
- for (const { target, value } of stats.general) {
489
- privateMulti[this.commands.hincrbyfloat](generalStatsKey, target, value as number);
490
- }
491
- }
492
- for (const { target, value } of stats.index) {
493
- const indexParams = { ...params, facet: target };
494
- const indexStatsKey = this.mintKey(KeyType.JOB_STATS_INDEX, indexParams);
495
- privateMulti[this.commands.rpush](indexStatsKey, value.toString());
496
- }
497
- for (const { target, value } of stats.median) {
498
- const medianParams = { ...params, facet: target };
499
- const medianStatsKey = this.mintKey(KeyType.JOB_STATS_MEDIAN, medianParams);
500
- this.zAdd(medianStatsKey, value, target, privateMulti);
501
- }
502
- if (!multi) {
503
- return await privateMulti.exec();
504
- }
505
- }
506
-
507
- hGetAllResult(result: any) {
508
- //default response signature uses 'redis' NPM Package format
509
- return result;
510
- }
511
-
512
- async getJobStats(jobKeys: string[]): Promise<JobStatsRange> {
513
- const multi = this.getMulti();
514
- for (const jobKey of jobKeys) {
515
- multi[this.commands.hgetall](jobKey);
516
- }
517
- const results = await multi.exec();
518
- const output: { [key: string]: JobStats } = {};
519
- for (const [index, result] of results.entries()) {
520
- const key = jobKeys[index];
521
- const statsHash: unknown = this.hGetAllResult(result);
522
- if (statsHash && Object.keys(statsHash).length > 0) {
523
- const resolvedStatsHash: JobStats = { ...statsHash as object };
524
- for (const [key, val] of Object.entries(resolvedStatsHash)) {
525
- resolvedStatsHash[key] = Number(val);
526
- }
527
- output[key] = resolvedStatsHash;
528
- } else {
529
- output[key] = {} as JobStats;
530
- }
531
- }
532
- return output;
533
- }
534
-
535
- async getJobIds(indexKeys: string[], idRange: [number, number]): Promise<IdsData> {
536
- const multi = this.getMulti();
537
- for (const idsKey of indexKeys) {
538
- multi[this.commands.lrange](idsKey, idRange[0], idRange[1]); //0,-1 returns all ids
539
- }
540
- const results = await multi.exec();
541
- const output: IdsData = {};
542
- for (const [index, result] of results.entries()) {
543
- const key = indexKeys[index];
544
-
545
- //todo: resolve this discrepancy between redis/ioredis
546
- const idsList: string[] = result[1] || result;
547
-
548
- if (idsList && idsList.length > 0) {
549
- output[key] = idsList;
550
- } else {
551
- output[key] = [];
552
- }
553
- }
554
- return output;
555
- }
556
-
557
- async setStatus(collationKeyStatus: number, jobId: string, appId: string, multi? : U): Promise<any> {
558
- const jobKey = this.mintKey(KeyType.JOB_STATE, { appId, jobId });
559
- return await (multi || this.redisClient)[this.commands.hincrbyfloat](jobKey, ':', collationKeyStatus);
560
- }
561
-
562
- async getStatus(jobId: string, appId: string): Promise<number> {
563
- const jobKey = this.mintKey(KeyType.JOB_STATE, { appId, jobId });
564
- const status = await this.redisClient[this.commands.hget](jobKey, ':');
565
- if (status === null) {
566
- throw new Error(`Job ${jobId} not found`);
567
- }
568
- return Number(status);
569
- }
570
-
571
- async setState({ ...state }: StringAnyType, status: number | null, jobId: string, symbolNames: string[], dIds: StringStringType, multi? : U): Promise<string> {
572
- delete state['metadata/js'];
573
- const hashKey = this.mintKey(KeyType.JOB_STATE, { appId: this.appId, jobId });
574
- const symKeys = await this.getSymbolKeys(symbolNames);
575
- const symVals = await this.getSymbolValues();
576
- this.serializer.resetSymbols(symKeys, symVals, dIds);
577
-
578
- const hashData = this.serializer.package(state, symbolNames);
579
- if (status !== null) {
580
- hashData[':'] = status.toString();
581
- } else {
582
- delete hashData[':'];
583
- }
584
- await (multi || this.redisClient)[this.commands.hset](hashKey, hashData);
585
- return jobId;
586
- }
587
-
588
- /**
589
- * Returns custom search fields and values.
590
- * NOTE: The `fields` param should NOT prefix items with an underscore.
591
- */
592
- async getQueryState(jobId: string, fields: string[]): Promise<StringAnyType> {
593
- const key = this.mintKey(KeyType.JOB_STATE, { appId: this.appId, jobId });
594
- const _fields = fields.map(field => `_${field}`);
595
- const jobDataArray = await this.redisClient[this.commands.hmget](key, _fields);
596
- const jobData: StringAnyType = {};
597
- fields.forEach((field, index) => {
598
- jobData[field] = jobDataArray[index];
599
- });
600
- return jobData;
601
- }
602
-
603
- async getState(jobId: string, consumes: Consumes, dIds: StringStringType): Promise<[StringAnyType, number] | undefined> {
604
- //get abbreviated field list (the symbols for the paths)
605
- const key = this.mintKey(KeyType.JOB_STATE, { appId: this.appId, jobId });
606
- const symbolNames = Object.keys(consumes);
607
- const symKeys = await this.getSymbolKeys(symbolNames);
608
- this.serializer.resetSymbols(symKeys, {}, dIds);
609
- const fields = this.serializer.abbreviate(consumes, symbolNames, [':']);
610
-
611
- const jobDataArray = await this.redisClient[this.commands.hmget](key, fields);
612
- const jobData: StringAnyType = {};
613
- let atLeast1 = false; //if status field (':') isn't present assume 404
614
- fields.forEach((field, index) => {
615
- if (jobDataArray[index]) {
616
- atLeast1 = true;
617
- }
618
- jobData[field] = jobDataArray[index];
619
- });
620
- if (atLeast1) {
621
- const symVals = await this.getSymbolValues();
622
- this.serializer.resetSymbols(symKeys, symVals, dIds);
623
- const state = this.serializer.unpackage(jobData, symbolNames);
624
- let status = 0;
625
- if (state[':']) {
626
- status = Number(state[':']);
627
- state[`metadata/js`] = status;
628
- delete state[':'];
629
- }
630
- return [state, status];
631
- } else {
632
- throw new GetStateError(jobId);
633
- }
634
- }
635
-
636
- async getRaw(jobId: string): Promise<StringStringType> {
637
- const jobKey = this.mintKey(KeyType.JOB_STATE, { appId: this.appId, jobId });
638
- const job = await this.redisClient[this.commands.hgetall](jobKey);
639
- if (!job) {
640
- throw new GetStateError(jobId);
641
- }
642
- return job;
643
- }
644
-
645
- /**
646
- * collate is a generic method for incrementing a value in a hash
647
- * in order to track their progress during processing.
648
- */
649
- async collate(jobId: string, activityId: string, amount: number, dIds: StringStringType, multi? : U): Promise<number> {
650
- const jobKey = this.mintKey(KeyType.JOB_STATE, { appId: this.appId, jobId });
651
- const collationKey = `${activityId}/output/metadata/as`; //activity state
652
- const symbolNames = [activityId];
653
- const symKeys = await this.getSymbolKeys(symbolNames);
654
- const symVals = await this.getSymbolValues();
655
- this.serializer.resetSymbols(symKeys, symVals, dIds);
656
-
657
- const payload = { [collationKey]: amount.toString() }
658
- const hashData = this.serializer.package(payload, symbolNames);
659
- const targetId = Object.keys(hashData)[0];
660
- return await (multi || this.redisClient)[this.commands.hincrbyfloat](jobKey, targetId, amount);
661
- }
662
-
663
- /**
664
- * synthentic collation affects those activities in the graph
665
- * that represent the synthetic DAG that was materialized during compilation;
666
- * Synthetic targeting ensures that re-entry due to failure can be distinguished from
667
- * purposeful re-entry.
668
- */
669
- async collateSynthetic(jobId: string, guid: string, amount: number, multi? : U): Promise<number> {
670
- const jobKey = this.mintKey(KeyType.JOB_STATE, { appId: this.appId, jobId });
671
- return await (multi || this.redisClient)[this.commands.hincrbyfloat](jobKey, guid, amount);
672
- }
673
-
674
- async setStateNX(jobId: string, appId: string): Promise<boolean> {
675
- const hashKey = this.mintKey(KeyType.JOB_STATE, { appId, jobId });
676
- const result = await this.redisClient[this.commands.hsetnx](hashKey, ':', '1');
677
- return this.isSuccessful(result);
678
- }
679
-
680
- async getSchema(activityId: string, appVersion: AppVID): Promise<ActivityType> {
681
- const schema = this.cache.getSchema(appVersion.id, appVersion.version, activityId);
682
- if (schema) {
683
- return schema
684
- } else {
685
- const schemas = await this.getSchemas(appVersion);
686
- return schemas[activityId];
687
- }
688
- }
689
-
690
- async getSchemas(appVersion: AppVID): Promise<Record<string, ActivityType>> {
691
- let schemas = this.cache.getSchemas(appVersion.id, appVersion.version);
692
- if (schemas && Object.keys(schemas).length > 0) {
693
- return schemas;
694
- } else {
695
- const params: KeyStoreParams = { appId: appVersion.id, appVersion: appVersion.version };
696
- const key = this.mintKey(KeyType.SCHEMAS, params);
697
- schemas = {};
698
- const hash = await this.redisClient[this.commands.hgetall](key);
699
- Object.entries(hash).forEach(([key, value]) => {
700
- schemas[key] = JSON.parse(value as string);
701
- });
702
- this.cache.setSchemas(appVersion.id, appVersion.version, schemas);
703
- return schemas;
704
- }
705
- }
706
-
707
- async setSchemas(schemas: Record<string, ActivityType>, appVersion: AppVID): Promise<any> {
708
- const params: KeyStoreParams = { appId: appVersion.id, appVersion: appVersion.version };
709
- const key = this.mintKey(KeyType.SCHEMAS, params);
710
- const _schemas = {...schemas} as Record<string, string>;
711
- Object.entries(_schemas).forEach(([key, value]) => {
712
- _schemas[key] = JSON.stringify(value);
713
- });
714
- const response = await this.redisClient[this.commands.hset](key, _schemas);
715
- this.cache.setSchemas(appVersion.id, appVersion.version, schemas);
716
- return response;
717
- }
718
-
719
- async setSubscriptions(subscriptions: Record<string, any>, appVersion: AppVID): Promise<boolean> {
720
- const params: KeyStoreParams = { appId: appVersion.id, appVersion: appVersion.version };
721
- const key = this.mintKey(KeyType.SUBSCRIPTIONS, params);
722
- const _subscriptions = {...subscriptions};
723
- Object.entries(_subscriptions).forEach(([key, value]) => {
724
- _subscriptions[key] = JSON.stringify(value);
725
- });
726
- const status = await this.redisClient[this.commands.hset](key, _subscriptions);
727
- this.cache.setSubscriptions(appVersion.id, appVersion.version, subscriptions);
728
- return this.isSuccessful(status);
729
- }
730
-
731
- async getSubscriptions(appVersion: AppVID): Promise<Record<string, string>> {
732
- let subscriptions = this.cache.getSubscriptions(appVersion.id, appVersion.version);
733
- if (subscriptions && Object.keys(subscriptions).length > 0) {
734
- return subscriptions;
735
- } else {
736
- const params: KeyStoreParams = { appId: appVersion.id, appVersion: appVersion.version };
737
- const key = this.mintKey(KeyType.SUBSCRIPTIONS, params);
738
- subscriptions = await this.redisClient[this.commands.hgetall](key) || {};
739
- Object.entries(subscriptions).forEach(([key, value]) => {
740
- subscriptions[key] = JSON.parse(value as string);
741
- });
742
- this.cache.setSubscriptions(appVersion.id, appVersion.version, subscriptions);
743
- return subscriptions;
744
- }
745
- }
746
-
747
- async getSubscription(topic: string, appVersion: AppVID): Promise<string | undefined> {
748
- const subscriptions = await this.getSubscriptions(appVersion);
749
- return subscriptions[topic];
750
- }
751
-
752
- async setTransitions(transitions: Record<string, any>, appVersion: AppVID): Promise<any> {
753
- const params: KeyStoreParams = { appId: appVersion.id, appVersion: appVersion.version };
754
- const key = this.mintKey(KeyType.SUBSCRIPTION_PATTERNS, params);
755
- const _subscriptions = {...transitions};
756
- Object.entries(_subscriptions).forEach(([key, value]) => {
757
- _subscriptions[key] = JSON.stringify(value);
758
- });
759
- if (Object.keys(_subscriptions).length !== 0) {
760
- const response = await this.redisClient[this.commands.hset](key, _subscriptions);
761
- this.cache.setTransitions(appVersion.id, appVersion.version, transitions);
762
- return response;
763
- }
764
- }
765
-
766
- async getTransitions(appVersion: AppVID): Promise<Transitions> {
767
- let transitions = this.cache.getTransitions(appVersion.id, appVersion.version);
768
- if (transitions && Object.keys(transitions).length > 0) {
769
- return transitions;
770
- } else {
771
- const params: KeyStoreParams = { appId: appVersion.id, appVersion: appVersion.version };
772
- const key = this.mintKey(KeyType.SUBSCRIPTION_PATTERNS, params);
773
- transitions = {};
774
- const hash = await this.redisClient[this.commands.hgetall](key);
775
- Object.entries(hash).forEach(([key, value]) => {
776
- transitions[key] = JSON.parse(value as string);
777
- });
778
- this.cache.setTransitions(appVersion.id, appVersion.version, transitions);
779
- return transitions;
780
- }
781
- }
782
-
783
- async setHookRules(hookRules: Record<string, HookRule[]>): Promise<any> {
784
- const key = this.mintKey(KeyType.HOOKS, { appId: this.appId });
785
- const _hooks = { };
786
- Object.entries(hookRules).forEach(([key, value]) => {
787
- _hooks[key.toString()] = JSON.stringify(value);
788
- });
789
- if (Object.keys(_hooks).length !== 0) {
790
- const response = await this.redisClient[this.commands.hset](key, _hooks);
791
- this.cache.setHookRules(this.appId, hookRules);
792
- return response;
793
- }
794
- }
795
-
796
- async getHookRules(): Promise<Record<string, HookRule[]>> {
797
- let patterns = this.cache.getHookRules(this.appId);
798
- if (patterns && Object.keys(patterns).length > 0) {
799
- return patterns;
800
- } else {
801
- const key = this.mintKey(KeyType.HOOKS, { appId: this.appId });
802
- const _hooks = await this.redisClient[this.commands.hgetall](key);
803
- patterns = {};
804
- Object.entries(_hooks).forEach(([key, value]) => {
805
- patterns[key] = JSON.parse(value as string);
806
- });
807
- this.cache.setHookRules(this.appId, patterns);
808
- return patterns;
809
- }
810
- }
811
-
812
- async setHookSignal(hook: HookSignal, multi?: U): Promise<any> {
813
- const key = this.mintKey(KeyType.SIGNALS, { appId: this.appId });
814
- //destructure the hook key
815
- const { topic, resolved, jobId} = hook;
816
- const signalKey = `${topic}:${resolved}`;
817
- const payload = { [signalKey]: jobId };
818
- await (multi || this.redisClient)[this.commands.hset](key, payload);
819
- //jobId needs even more destructuring
820
- const [_aid, dad, _gid, jid] = jobId.split(VALSEP);
821
- return await this.registerSignalDependency(jid, signalKey, dad, multi);
822
- }
823
-
824
- async getHookSignal(topic: string, resolved: string): Promise<string | undefined> {
825
- const key = this.mintKey(KeyType.SIGNALS, { appId: this.appId });
826
- const response = await this.redisClient[this.commands.hget](key, `${topic}:${resolved}`);
827
- return response ? response.toString() : undefined;
828
- }
829
-
830
- async deleteHookSignal(topic: string, resolved: string): Promise<number | undefined> {
831
- const key = this.mintKey(KeyType.SIGNALS, { appId: this.appId });
832
- const response = await this.redisClient[this.commands.hdel](key, `${topic}:${resolved}`);
833
- return response ? Number(response) : undefined;
834
- }
835
-
836
- async addTaskQueues(keys: string[]): Promise<void> {
837
- const multi = this.getMulti();
838
- const zsetKey = this.mintKey(KeyType.WORK_ITEMS, { appId: this.appId });
839
- for (const key of keys) {
840
- multi[this.commands.zadd](zsetKey, { score: Date.now().toString(), value: key } as any, { NX: true });
841
- }
842
- await multi.exec();
843
- }
844
-
845
- async getActiveTaskQueue(): Promise<string | null> {
846
- let workItemKey = this.cache.getActiveTaskQueue(this.appId) || null;
847
- if (!workItemKey) {
848
- const zsetKey = this.mintKey(KeyType.WORK_ITEMS, { appId: this.appId });
849
- const result = await this.redisClient[this.commands.zrange](zsetKey, 0, 0);
850
- workItemKey = result.length > 0 ? result[0] : null;
851
- if (workItemKey) {
852
- this.cache.setWorkItem(this.appId, workItemKey);
853
- }
854
- }
855
- return workItemKey;
856
- }
857
-
858
- async deleteProcessedTaskQueue(workItemKey: string, key: string, processedKey: string, scrub = false): Promise<void> {
859
- const zsetKey = this.mintKey(KeyType.WORK_ITEMS, { appId: this.appId });
860
- const didRemove = await this.redisClient[this.commands.zrem](zsetKey, workItemKey);
861
- if (didRemove) {
862
- if (scrub) {
863
- //indexes can be designed to be self-cleaning; `engine.hookAll` exposes this option
864
- this.redisClient[this.commands.expire](processedKey, 0);
865
- this.redisClient[this.commands.expire](key.split(":").slice(0, 5).join(":"), 0);
866
- } else {
867
- await this.redisClient[this.commands.rename](processedKey, key);
868
- }
869
- }
870
- this.cache.removeWorkItem(this.appId);
871
- }
872
-
873
- async processTaskQueue(sourceKey: string, destinationKey: string): Promise<any> {
874
- return await this.redisClient[this.commands.lmove](sourceKey, destinationKey, 'LEFT', 'RIGHT');
875
- }
876
-
877
- async expireJob(jobId: string, inSeconds: number): Promise<void> {
878
- if (!isNaN(inSeconds) && inSeconds > 0) {
879
- const jobKey = this.mintKey(KeyType.JOB_STATE, { appId: this.appId, jobId });
880
- await this.redisClient[this.commands.expire](jobKey, inSeconds);
881
- }
882
- }
883
-
884
- /**
885
- * register the descendants of an expired origin flow to be
886
- * expired at a future date; options indicate whether this
887
- * is a standard `expire` or an `interrupt`
888
- */
889
- async registerDependenciesForCleanup(jobId: string, deletionTime: number, options: JobCompletionOptions): Promise<void> {
890
- const depParams = { appId: this.appId, jobId };
891
- const depKey = this.mintKey(KeyType.JOB_DEPENDENTS, depParams);
892
- const context = options.interrupt ? 'INTERRUPT' : 'EXPIRE';
893
- const depKeyContext = `${TYPSEP}${context}${TYPSEP}${depKey}`;
894
- const zsetKey = this.mintKey(KeyType.TIME_RANGE, { appId: this.appId });
895
- await this.zAdd(zsetKey, deletionTime.toString(), depKeyContext);
896
- }
897
-
898
- async getDependencies(jobId: string): Promise<string[]> {
899
- const depParams = { appId: this.appId, jobId };
900
- const depKey = this.mintKey(KeyType.JOB_DEPENDENTS, depParams);
901
- return this.redisClient[this.commands.lrange](depKey, 0, -1);
902
- }
903
-
904
- /**
905
- * registers a hook activity to be awakened (uses ZSET to
906
- * store the 'sleep group' and LIST to store the events
907
- * for the given sleep group. Sleep groups are
908
- * organized into 'n'-second blocks (LISTS))
909
- */
910
- async registerTimeHook(jobId: string, gId: string, activityId: string, type: WorkListTaskType, deletionTime: number, dad: string, multi?: U): Promise<void> {
911
- const listKey = this.mintKey(KeyType.TIME_RANGE, { appId: this.appId, timeValue: deletionTime });
912
- //construct the composite key (the key has enough info to signal the hook)
913
- const timeEvent = [
914
- type,
915
- activityId,
916
- gId,
917
- dad,
918
- jobId].join(VALSEP);
919
- const len = await (multi || this.redisClient)[this.commands.rpush](listKey, timeEvent);
920
- if (multi || len === 1) {
921
- const zsetKey = this.mintKey(KeyType.TIME_RANGE, { appId: this.appId });
922
- await this.zAdd(zsetKey, deletionTime.toString(), listKey, multi);
923
- }
924
- }
925
-
926
- async getNextTask(listKey?: string): Promise<[listKey: string, jobId: string, gId: string, activityId: string, type: WorkListTaskType] | boolean> {
927
- const zsetKey = this.mintKey(KeyType.TIME_RANGE, { appId: this.appId });
928
- listKey = listKey || await this.zRangeByScore(zsetKey, 0, Date.now());
929
- if (listKey) {
930
- let [pType, pKey] = this.resolveTaskKeyContext(listKey);
931
- const timeEvent = await this.redisClient[this.commands.lpop](pKey);
932
- if (timeEvent) {
933
- //deconstruct composite key
934
- let [
935
- type,
936
- activityId,
937
- gId,
938
- _pd,
939
- ...jobId] = timeEvent.split(VALSEP);
940
- const jid = jobId.join(VALSEP);
941
-
942
- if (type === 'delist') {
943
- pType = 'delist';
944
- } else if (type === 'child') {
945
- pType = 'child';
946
- } else if (type === 'expire-child') {
947
- type = 'expire';
948
- }
949
- return [listKey, jid, gId, activityId, pType];
950
- }
951
- await this.redisClient[this.commands.zrem](zsetKey, listKey);
952
- return true;
953
- }
954
- return false;
955
- }
956
-
957
- /**
958
- * when processing time jobs, the target LIST ID returned
959
- * from the ZSET query can be prefixed to denote what to
960
- * do with the work list. (not everything is known in advance,
961
- * so the ZSET key defines HOW to approach the work in the
962
- * generic LIST (lists typically contain target job ids)
963
- * @param {string} listKey - composite key
964
- */
965
- resolveTaskKeyContext(listKey: string): [WorkListTaskType, string] {
966
- if (listKey.startsWith(`${TYPSEP}INTERRUPT`)) {
967
- return ['interrupt', listKey.split(TYPSEP)[2]];
968
- } else if (listKey.startsWith(`${TYPSEP}EXPIRE`)) {
969
- return ['expire', listKey.split(TYPSEP)[2]];
970
- } else {
971
- return ['sleep', listKey];
972
- }
973
- }
974
-
975
- /**
976
- * Interrupts a job and sets sets a job error (410), if 'throw'!=false.
977
- * This method is called by the engine and not by an activity and is
978
- * followed by a call to execute job completion/cleanup tasks
979
- * associated with a job completion event.
980
- *
981
- * Todo: move most of this logic to the engine (too much logic for the store)
982
- */
983
- async interrupt(topic: string, jobId: string, options: JobInterruptOptions = {}): Promise<void> {
984
- try {
985
- //verify job exists
986
- const status = await this.getStatus(jobId, this.appId);
987
- if (status <= 0) {
988
- //verify still active; job already completed
989
- throw new Error(`Job ${jobId} already completed`);
990
- }
991
- //decrement job status (:) by 1bil
992
- const amount = -1_000_000_000;
993
- const jobKey = this.mintKey(KeyType.JOB_STATE, { appId: this.appId, jobId });
994
- const result = await this.redisClient[this.commands.hincrbyfloat](jobKey, ':', amount);
995
- if (result <= amount) {
996
- //verify active state; job already interrupted
997
- throw new Error(`Job ${jobId} already completed`);
998
- }
999
- //persist the error unless specifically told not to
1000
- if (options.throw !== false) {
1001
- const errKey = `metadata/err`; //job errors are stored at the path `metadata/err`
1002
- const symbolNames = [`$${topic}`]; //the symbol for `metadata/err` is in redis and stored using the job topic
1003
- const symKeys = await this.getSymbolKeys(symbolNames);
1004
- const symVals = await this.getSymbolValues();
1005
- this.serializer.resetSymbols(symKeys, symVals, {});
1006
-
1007
- //persists the standard 410 error (job is `gone`)
1008
- const err = JSON.stringify({
1009
- code: options.code ?? HMSH_CODE_INTERRUPT,
1010
- message: options.reason ?? `job [${jobId}] interrupted`,
1011
- stack: options.stack ?? '',
1012
- job_id: jobId
1013
- });
1014
-
1015
- const payload = { [errKey]: amount.toString() }
1016
- const hashData = this.serializer.package(payload, symbolNames);
1017
- const errSymbol = Object.keys(hashData)[0];
1018
- await this.redisClient[this.commands.hset](jobKey, errSymbol, err);
1019
- }
1020
- } catch (e) {
1021
- if (!options.suppress) {
1022
- throw e;
1023
- } else {
1024
- this.logger.debug('suppressed-interrupt', { message: e.message })
1025
- }
1026
- }
1027
- }
1028
-
1029
- async scrub(jobId: string) {
1030
- const jobKey = this.mintKey(KeyType.JOB_STATE, { appId: this.appId, jobId });
1031
- await this.redisClient[this.commands.del](jobKey);
1032
- }
1033
-
1034
- async findJobs(queryString: string = '*', limit: number = 1000, batchSize: number = 1000, cursor = '0'): Promise<[string, string[]]> {
1035
- const matchKey = this.mintKey(KeyType.JOB_STATE, { appId: this.appId, jobId: queryString });
1036
- let keys: string[];
1037
- const matchingKeys: string[] = [];
1038
- do {
1039
- const output = await this.exec(
1040
- 'SCAN',
1041
- cursor,
1042
- 'MATCH',
1043
- matchKey,
1044
- 'COUNT',
1045
- batchSize.toString(),
1046
- ) as unknown as [string, string[]];
1047
- if (Array.isArray(output)) {
1048
- [cursor, keys] = output;
1049
- for (let key of [...keys]) {
1050
- matchingKeys.push(key);
1051
- }
1052
- if (matchingKeys.length >= limit) {
1053
- break;
1054
- }
1055
- } else {
1056
- break;
1057
- }
1058
- } while (cursor !== '0');
1059
- return [cursor, matchingKeys];
1060
- }
1061
-
1062
- async findJobFields(jobId: string, fieldMatchPattern: string = '*', limit: number = 1000, batchSize: number = 1000, cursor = '0'): Promise<[string, StringStringType]> {
1063
- let fields: string[] = [];
1064
- const matchingFields: StringStringType = {};
1065
- const jobKey = this.mintKey(KeyType.JOB_STATE, { appId: this.appId, jobId });
1066
- let len = 0;
1067
- do {
1068
- const output = await this.exec(
1069
- 'HSCAN',
1070
- jobKey,
1071
- cursor,
1072
- 'MATCH',
1073
- fieldMatchPattern,
1074
- 'COUNT',
1075
- batchSize.toString(),
1076
- ) as unknown as [string, string[]];
1077
- if (Array.isArray(output)) {
1078
- [cursor, fields] = output;
1079
- for (let i = 0; i < fields.length; i += 2) {
1080
- len++;
1081
- matchingFields[fields[i]] = fields[i + 1];
1082
- }
1083
- } else {
1084
- break;
1085
- }
1086
- } while (cursor !== '0' && len < limit);
1087
- return [cursor, matchingFields];
1088
- }
1089
- }
1090
-
1091
- export { StoreService };