@hotmeshio/hotmesh 0.0.57 → 0.0.58
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.
- package/README.md +10 -10
- package/build/modules/enums.js +10 -1
- package/build/modules/key.d.ts +38 -0
- package/build/modules/key.js +46 -4
- package/build/modules/utils.d.ts +9 -0
- package/build/modules/utils.js +19 -1
- package/build/package.json +1 -1
- package/build/services/activities/activity.d.ts +28 -0
- package/build/services/activities/activity.js +46 -1
- package/build/services/activities/await.js +4 -0
- package/build/services/activities/cycle.d.ts +7 -0
- package/build/services/activities/cycle.js +16 -1
- package/build/services/activities/hook.d.ts +6 -0
- package/build/services/activities/hook.js +12 -2
- package/build/services/activities/interrupt.js +8 -0
- package/build/services/activities/signal.d.ts +6 -0
- package/build/services/activities/signal.js +15 -0
- package/build/services/activities/trigger.d.ts +4 -0
- package/build/services/activities/trigger.js +7 -1
- package/build/services/activities/worker.js +4 -0
- package/build/services/collator/index.d.ts +70 -0
- package/build/services/collator/index.js +91 -1
- package/build/services/compiler/deployer.js +38 -6
- package/build/services/compiler/index.d.ts +15 -0
- package/build/services/compiler/index.js +20 -0
- package/build/services/compiler/validator.d.ts +3 -0
- package/build/services/compiler/validator.js +25 -0
- package/build/services/connector/clients/ioredis.js +2 -0
- package/build/services/connector/clients/redis.js +2 -0
- package/build/services/connector/index.js +2 -0
- package/build/services/durable/client.d.ts +20 -0
- package/build/services/durable/client.js +25 -0
- package/build/services/durable/exporter.d.ts +22 -0
- package/build/services/durable/exporter.js +30 -1
- package/build/services/durable/handle.d.ts +36 -0
- package/build/services/durable/handle.js +46 -0
- package/build/services/durable/index.d.ts +4 -0
- package/build/services/durable/index.js +4 -0
- package/build/services/durable/schemas/factory.d.ts +29 -0
- package/build/services/durable/schemas/factory.js +29 -0
- package/build/services/durable/search.d.ts +97 -0
- package/build/services/durable/search.js +99 -0
- package/build/services/durable/worker.js +35 -6
- package/build/services/durable/workflow.d.ts +118 -0
- package/build/services/durable/workflow.js +153 -6
- package/build/services/engine/index.d.ts +5 -0
- package/build/services/engine/index.js +43 -1
- package/build/services/exporter/index.d.ts +27 -0
- package/build/services/exporter/index.js +33 -0
- package/build/services/hotmesh/index.js +8 -0
- package/build/services/logger/index.js +2 -0
- package/build/services/mapper/index.d.ts +14 -0
- package/build/services/mapper/index.js +14 -0
- package/build/services/pipe/functions/date.d.ts +7 -0
- package/build/services/pipe/functions/date.js +7 -0
- package/build/services/pipe/functions/math.js +2 -0
- package/build/services/pipe/index.d.ts +15 -0
- package/build/services/pipe/index.js +23 -2
- package/build/services/quorum/index.d.ts +7 -0
- package/build/services/quorum/index.js +21 -0
- package/build/services/reporter/index.d.ts +5 -0
- package/build/services/reporter/index.js +9 -0
- package/build/services/router/index.d.ts +9 -0
- package/build/services/router/index.js +30 -2
- package/build/services/serializer/index.js +23 -6
- package/build/services/store/cache.d.ts +19 -0
- package/build/services/store/cache.js +19 -0
- package/build/services/store/clients/ioredis.js +1 -0
- package/build/services/store/index.d.ts +55 -0
- package/build/services/store/index.js +81 -5
- package/build/services/stream/clients/ioredis.js +4 -1
- package/build/services/task/index.d.ts +9 -0
- package/build/services/task/index.js +31 -0
- package/build/services/telemetry/index.d.ts +7 -0
- package/build/services/telemetry/index.js +13 -1
- package/build/services/worker/index.d.ts +4 -0
- package/build/services/worker/index.js +6 -2
- package/build/types/activity.d.ts +81 -0
- package/build/types/durable.d.ts +255 -0
- package/build/types/exporter.d.ts +13 -0
- package/build/types/hotmesh.d.ts +10 -1
- package/build/types/hotmesh.js +3 -0
- package/build/types/index.js +1 -1
- package/build/types/job.d.ts +85 -0
- package/build/types/pipe.d.ts +65 -0
- package/build/types/quorum.d.ts +14 -0
- package/build/types/redis.d.ts +6 -0
- package/build/types/stream.d.ts +58 -0
- package/build/types/stream.js +4 -0
- package/package.json +1 -1
|
@@ -26,9 +26,12 @@ class QuorumService {
|
|
|
26
26
|
instance.guid = guid;
|
|
27
27
|
instance.logger = logger;
|
|
28
28
|
instance.engine = engine;
|
|
29
|
+
//note: `quorum` shares/re-uses the engine's `store`/`sub` Redis clients
|
|
29
30
|
await instance.initStoreChannel(config.engine.store);
|
|
30
31
|
await instance.initSubChannel(config.engine.sub);
|
|
32
|
+
//general quorum subscription
|
|
31
33
|
await instance.subscribe.subscribe(hotmesh_1.KeyType.QUORUM, instance.subscriptionHandler(), appId);
|
|
34
|
+
//app-specific quorum subscription (used for pubsub one-time request/response)
|
|
32
35
|
await instance.subscribe.subscribe(hotmesh_1.KeyType.QUORUM, instance.subscriptionHandler(), appId, instance.guid);
|
|
33
36
|
instance.engine.processWebHooks();
|
|
34
37
|
instance.engine.processTimeHooks();
|
|
@@ -90,6 +93,7 @@ class QuorumService {
|
|
|
90
93
|
else if (message.type === 'rollcall') {
|
|
91
94
|
self.doRollCall(message);
|
|
92
95
|
}
|
|
96
|
+
//if there are any callbacks, call them
|
|
93
97
|
if (self.callbacks.length > 0) {
|
|
94
98
|
self.callbacks.forEach(cb => cb(topic, message));
|
|
95
99
|
}
|
|
@@ -131,6 +135,10 @@ class QuorumService {
|
|
|
131
135
|
await (0, utils_1.sleepFor)(delay);
|
|
132
136
|
return quorum;
|
|
133
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* A quorum-wide command to broadcaset system details.
|
|
140
|
+
*
|
|
141
|
+
*/
|
|
134
142
|
async doRollCall(message) {
|
|
135
143
|
let iteration = 0;
|
|
136
144
|
let max = !isNaN(message.max) ? message.max : enums_1.HMSH_QUORUM_ROLLCALL_CYCLES;
|
|
@@ -157,15 +165,22 @@ class QuorumService {
|
|
|
157
165
|
stop() {
|
|
158
166
|
this.cancelRollCall();
|
|
159
167
|
}
|
|
168
|
+
// ************* PUB/SUB METHODS *************
|
|
169
|
+
//publish a message to the quorum
|
|
160
170
|
async pub(quorumMessage) {
|
|
161
171
|
return await this.store.publish(hotmesh_1.KeyType.QUORUM, quorumMessage, this.appId, quorumMessage.topic || quorumMessage.guid);
|
|
162
172
|
}
|
|
173
|
+
//subscribe user to quorum messages
|
|
163
174
|
async sub(callback) {
|
|
175
|
+
//the quorum is always subscribed to the `quorum` topic; just register the fn
|
|
164
176
|
this.callbacks.push(callback);
|
|
165
177
|
}
|
|
178
|
+
//unsubscribe user from quorum messages
|
|
166
179
|
async unsub(callback) {
|
|
180
|
+
//the quorum is always subscribed to the `quorum` topic; just unregister the fn
|
|
167
181
|
this.callbacks = this.callbacks.filter(cb => cb !== callback);
|
|
168
182
|
}
|
|
183
|
+
// ************* COMPILER METHODS *************
|
|
169
184
|
async rollCall(delay = enums_1.HMSH_QUORUM_DELAY_MS) {
|
|
170
185
|
await this.requestQuorum(delay, true);
|
|
171
186
|
const targetStreams = [];
|
|
@@ -187,10 +202,14 @@ class QuorumService {
|
|
|
187
202
|
});
|
|
188
203
|
return this.profiles;
|
|
189
204
|
}
|
|
205
|
+
/**
|
|
206
|
+
* request a quorum; if successful activate the app version
|
|
207
|
+
*/
|
|
190
208
|
async activate(version, delay = enums_1.HMSH_QUORUM_DELAY_MS, count = 0) {
|
|
191
209
|
version = version.toString();
|
|
192
210
|
const canActivate = await this.store.reserveScoutRole('activate', Math.ceil(delay * 6 / 1000) + 1);
|
|
193
211
|
if (!canActivate) {
|
|
212
|
+
//another engine is already activating the app version
|
|
194
213
|
this.logger.debug('quorum-activation-awaiting', { version });
|
|
195
214
|
await (0, utils_1.sleepFor)(delay * 6);
|
|
196
215
|
const app = await this.store.getApp(this.appId, true);
|
|
@@ -206,6 +225,7 @@ class QuorumService {
|
|
|
206
225
|
this.store.publish(hotmesh_1.KeyType.QUORUM, { type: 'activate', cache_mode: 'nocache', until_version: version }, this.appId);
|
|
207
226
|
await new Promise(resolve => setTimeout(resolve, delay));
|
|
208
227
|
await this.store.releaseScoutRole('activate');
|
|
228
|
+
//confirm we received the activation message
|
|
209
229
|
if (this.engine.untilVersion === version) {
|
|
210
230
|
this.logger.info('quorum-activation-succeeded', { version });
|
|
211
231
|
const { id } = config;
|
|
@@ -221,6 +241,7 @@ class QuorumService {
|
|
|
221
241
|
this.logger.warn('quorum-rollcall-error', { q1, q2, q3, count });
|
|
222
242
|
this.store.releaseScoutRole('activate');
|
|
223
243
|
if (count < enums_1.HMSH_ACTIVATION_MAX_RETRY) {
|
|
244
|
+
//increase the delay (give the quorum time to respond) and try again
|
|
224
245
|
return await this.activate(version, delay * 2, count + 1);
|
|
225
246
|
}
|
|
226
247
|
throw new Error(`Quorum not reached. Version ${version} not activated.`);
|
|
@@ -28,6 +28,11 @@ declare class ReporterService {
|
|
|
28
28
|
getTargetForTime(key: string): string;
|
|
29
29
|
getWorkItems(options: GetStatsOptions, facets: string[]): Promise<string[]>;
|
|
30
30
|
private buildWorkerLists;
|
|
31
|
+
/**
|
|
32
|
+
* called by `trigger` activity to generate the stats that should
|
|
33
|
+
* be saved to the database. doesn't actually save the stats, but
|
|
34
|
+
* just generates the info that should be saved
|
|
35
|
+
*/
|
|
31
36
|
resolveTriggerStatistics({ stats: statsConfig }: TriggerActivity, context: JobState): StatsType;
|
|
32
37
|
isGeneralMetric(metric: string): boolean;
|
|
33
38
|
isMedianMetric(metric: string): boolean;
|
|
@@ -27,9 +27,11 @@ class ReporterService {
|
|
|
27
27
|
}
|
|
28
28
|
generateDateTimeSets(granularity, range, end, start) {
|
|
29
29
|
if (granularity === 'infinity') {
|
|
30
|
+
//if granularity is infinity, it means a date/time sequence/slice is not used to further segment the statistics
|
|
30
31
|
return ['0'];
|
|
31
32
|
}
|
|
32
33
|
if (!range) {
|
|
34
|
+
//pluck just a single value when no range provided
|
|
33
35
|
range = '0m';
|
|
34
36
|
}
|
|
35
37
|
const granularitiesInMinutes = {
|
|
@@ -47,6 +49,7 @@ class ReporterService {
|
|
|
47
49
|
if (rangeMinutes === null) {
|
|
48
50
|
throw new Error('Invalid range value.');
|
|
49
51
|
}
|
|
52
|
+
// If start is provided, use it. Otherwise, calculate it from the end time and range.
|
|
50
53
|
let startTime;
|
|
51
54
|
let endTime;
|
|
52
55
|
if (start) {
|
|
@@ -57,6 +60,7 @@ class ReporterService {
|
|
|
57
60
|
endTime = end === 'NOW' ? new Date() : new Date(end);
|
|
58
61
|
startTime = new Date(endTime.getTime() - rangeMinutes * 60 * 1000);
|
|
59
62
|
}
|
|
63
|
+
// Round the start time to the nearest granularity unit
|
|
60
64
|
startTime.setUTCMinutes(Math.floor(startTime.getUTCMinutes() / granularityMinutes) * granularityMinutes);
|
|
61
65
|
const dateTimeSets = [];
|
|
62
66
|
for (let time = startTime; time <= endTime; time.setUTCMinutes(time.getUTCMinutes() + granularityMinutes)) {
|
|
@@ -266,6 +270,11 @@ class ReporterService {
|
|
|
266
270
|
}
|
|
267
271
|
return workerLists;
|
|
268
272
|
}
|
|
273
|
+
/**
|
|
274
|
+
* called by `trigger` activity to generate the stats that should
|
|
275
|
+
* be saved to the database. doesn't actually save the stats, but
|
|
276
|
+
* just generates the info that should be saved
|
|
277
|
+
*/
|
|
269
278
|
resolveTriggerStatistics({ stats: statsConfig }, context) {
|
|
270
279
|
const stats = {
|
|
271
280
|
general: [],
|
|
@@ -30,6 +30,15 @@ declare class Router {
|
|
|
30
30
|
private resetThrottleState;
|
|
31
31
|
createGroup(stream: string, group: string): Promise<void>;
|
|
32
32
|
publishMessage(topic: string, streamData: StreamData | StreamDataResponse, multi?: RedisMulti): Promise<string | RedisMulti>;
|
|
33
|
+
/**
|
|
34
|
+
* An adjustable throttle that will interrupt a sleeping
|
|
35
|
+
* router if the throttle is reduced and the sleep time
|
|
36
|
+
* has elapsed. If the throttle is increased, or if
|
|
37
|
+
* the sleep time has not elapsed, the router will continue
|
|
38
|
+
* to sleep until the new termination point. This
|
|
39
|
+
* allows for dynamic, elastic throttling with smooth
|
|
40
|
+
* acceleration and deceleration.
|
|
41
|
+
*/
|
|
33
42
|
customSleep(): Promise<void>;
|
|
34
43
|
consumeMessages(stream: string, group: string, consumer: string, callback: (streamData: StreamData) => Promise<StreamDataResponse | void>): Promise<void>;
|
|
35
44
|
isStreamMessage(result: any): boolean;
|
|
@@ -47,13 +47,22 @@ class Router {
|
|
|
47
47
|
const stream = this.store.mintKey(key_1.KeyType.STREAMS, { appId: this.store.appId, topic });
|
|
48
48
|
return await this.store.xadd(stream, '*', 'message', JSON.stringify(streamData), multi);
|
|
49
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* An adjustable throttle that will interrupt a sleeping
|
|
52
|
+
* router if the throttle is reduced and the sleep time
|
|
53
|
+
* has elapsed. If the throttle is increased, or if
|
|
54
|
+
* the sleep time has not elapsed, the router will continue
|
|
55
|
+
* to sleep until the new termination point. This
|
|
56
|
+
* allows for dynamic, elastic throttling with smooth
|
|
57
|
+
* acceleration and deceleration.
|
|
58
|
+
*/
|
|
50
59
|
async customSleep() {
|
|
51
60
|
if (this.throttle === 0)
|
|
52
61
|
return;
|
|
53
62
|
if (this.isSleeping)
|
|
54
63
|
return;
|
|
55
64
|
this.isSleeping = true;
|
|
56
|
-
let startTime = Date.now();
|
|
65
|
+
let startTime = Date.now(); //anchor the origin
|
|
57
66
|
await new Promise(async (outerResolve) => {
|
|
58
67
|
this.sleepPromiseResolve = outerResolve;
|
|
59
68
|
let elapsedTime = Date.now() - startTime;
|
|
@@ -81,6 +90,7 @@ class Router {
|
|
|
81
90
|
return;
|
|
82
91
|
}
|
|
83
92
|
try {
|
|
93
|
+
//randomizer that asymptotes at 150% of `HMSH_BLOCK_TIME_MS`
|
|
84
94
|
const streamDuration = enums_1.HMSH_BLOCK_TIME_MS + Math.round((enums_1.HMSH_BLOCK_TIME_MS * Math.random()));
|
|
85
95
|
const result = await this.stream.xreadgroup('GROUP', group, consumer, 'BLOCK', streamDuration, 'STREAMS', stream, '>');
|
|
86
96
|
if (this.isStreamMessage(result)) {
|
|
@@ -89,6 +99,7 @@ class Router {
|
|
|
89
99
|
await this.consumeOne(stream, group, id, message, callback);
|
|
90
100
|
}
|
|
91
101
|
}
|
|
102
|
+
// Check for pending messages (note: Redis 6.2 simplifies)
|
|
92
103
|
const now = Date.now();
|
|
93
104
|
if (now - lastCheckedPendingMessagesAt > this.reclaimDelay) {
|
|
94
105
|
lastCheckedPendingMessagesAt = now;
|
|
@@ -162,6 +173,7 @@ class Router {
|
|
|
162
173
|
await (0, utils_1.sleepFor)(timeout);
|
|
163
174
|
return await this.publishMessage(input.metadata.topic, {
|
|
164
175
|
data: input.data,
|
|
176
|
+
//note: retain guid (this is a retry attempt)
|
|
165
177
|
metadata: { ...input.metadata, try: (input.metadata.try || 0) + 1 },
|
|
166
178
|
policies: input.policies,
|
|
167
179
|
});
|
|
@@ -181,12 +193,16 @@ class Router {
|
|
|
181
193
|
}
|
|
182
194
|
}
|
|
183
195
|
shouldRetry(input, output) {
|
|
196
|
+
//const isUnhandledEngineError = output.code === 500;
|
|
184
197
|
const policies = input.policies?.retry;
|
|
185
198
|
const errorCode = output.code.toString();
|
|
186
199
|
const policy = policies?.[errorCode];
|
|
187
200
|
const maxRetries = policy?.[0];
|
|
188
201
|
const tryCount = Math.min(input.metadata.try || 0, enums_1.HMSH_MAX_RETRIES);
|
|
202
|
+
//only possible values for maxRetries are 1, 2, 3
|
|
203
|
+
//only possible values for tryCount are 0, 1, 2
|
|
189
204
|
if (maxRetries > tryCount) {
|
|
205
|
+
// 10ms, 100ms, or 1000ms delays between system retries
|
|
190
206
|
return [true, Math.pow(10, tryCount + 1)];
|
|
191
207
|
}
|
|
192
208
|
return [false, 0];
|
|
@@ -222,6 +238,7 @@ class Router {
|
|
|
222
238
|
code,
|
|
223
239
|
data,
|
|
224
240
|
};
|
|
241
|
+
//send unacknowleded errors to the engine (it has no topic)
|
|
225
242
|
delete output.metadata.topic;
|
|
226
243
|
return output;
|
|
227
244
|
}
|
|
@@ -265,6 +282,7 @@ class Router {
|
|
|
265
282
|
}
|
|
266
283
|
const wasDecreased = delayInMillis < this.throttle;
|
|
267
284
|
this.throttle = delayInMillis;
|
|
285
|
+
// If the throttle was decreased, and we're in the middle of a sleep cycle, adjust immediately
|
|
268
286
|
if (wasDecreased) {
|
|
269
287
|
if (this.sleepTimout) {
|
|
270
288
|
clearTimeout(this.sleepTimout);
|
|
@@ -276,7 +294,7 @@ class Router {
|
|
|
276
294
|
}
|
|
277
295
|
async claimUnacknowledged(stream, group, consumer, idleTimeMs = this.reclaimDelay, limit = enums_1.HMSH_XPENDING_COUNT) {
|
|
278
296
|
let pendingMessages = [];
|
|
279
|
-
const pendingMessagesInfo = await this.stream.xpending(stream, group, '-', '+', limit);
|
|
297
|
+
const pendingMessagesInfo = await this.stream.xpending(stream, group, '-', '+', limit); //[[ '1688768134881-0', 'testConsumer1', 1017, 1 ]]
|
|
280
298
|
for (const pendingMessageInfo of pendingMessagesInfo) {
|
|
281
299
|
if (Array.isArray(pendingMessageInfo)) {
|
|
282
300
|
const [id, , elapsedTimeMs, deliveryCount] = pendingMessageInfo;
|
|
@@ -296,8 +314,16 @@ class Router {
|
|
|
296
314
|
return pendingMessages;
|
|
297
315
|
}
|
|
298
316
|
async expireUnacknowledged(reclaimedMessage, stream, group, consumer, id, count) {
|
|
317
|
+
//The stream activity was not processed within established limits. Possibilities Include:
|
|
318
|
+
// 1) user error: the workers were not properly configured and are timing out
|
|
319
|
+
// 2a) system error: JSON is corrupt
|
|
320
|
+
// i) unwitting actor
|
|
321
|
+
// ii) corrupt hardware/network/transport/etc
|
|
322
|
+
// 3b) system error: Redis unable to accept `xadd` request
|
|
323
|
+
// 4c) system error: Redis unable to accept `xdel`/`xack` request
|
|
299
324
|
this.logger.error('stream-message-max-delivery-count-exceeded', { id, stream, group, consumer, code: enums_1.HMSH_CODE_UNACKED, count });
|
|
300
325
|
const streamData = reclaimedMessage[0]?.[1]?.[1];
|
|
326
|
+
//fatal risk point 1 of 3): json is corrupt
|
|
301
327
|
const [err, input] = this.parseStreamData(streamData);
|
|
302
328
|
if (err) {
|
|
303
329
|
return this.logger.error('expire-unacknowledged-parse-fatal-error', { id, err });
|
|
@@ -311,9 +337,11 @@ class Router {
|
|
|
311
337
|
telemetry = new telemetry_1.TelemetryService(this.appId);
|
|
312
338
|
telemetry.startStreamSpan(input, stream_1.StreamRole.SYSTEM);
|
|
313
339
|
telemetry.setStreamError(`Stream Message Max Delivery Count Exceeded`);
|
|
340
|
+
//fatal risk point 2 of 3): unable to publish error message (to notify the parent job)
|
|
314
341
|
const output = this.structureUnacknowledgedError(input);
|
|
315
342
|
messageId = await this.publishResponse(input, output);
|
|
316
343
|
telemetry.setStreamAttributes({ 'app.worker.mid': messageId });
|
|
344
|
+
//fatal risk point 3 of 3): unable to ack and delete stream message
|
|
317
345
|
await this.ackAndDelete(stream, group, id);
|
|
318
346
|
}
|
|
319
347
|
catch (err) {
|
|
@@ -49,6 +49,7 @@ class SerializerService {
|
|
|
49
49
|
return this.dIds[activityId];
|
|
50
50
|
}
|
|
51
51
|
else if ('$ADJACENT' in this.dIds) {
|
|
52
|
+
//else=> pre-authorizing adjacent activity entry
|
|
52
53
|
return this.dIds['$ADJACENT'];
|
|
53
54
|
}
|
|
54
55
|
return ',0';
|
|
@@ -117,6 +118,7 @@ class SerializerService {
|
|
|
117
118
|
result[shortDimensionalKey] = source[key];
|
|
118
119
|
}
|
|
119
120
|
else if (!(key in result) && this.isLiteralKeyType(key)) {
|
|
121
|
+
//mark (-) and search (_)
|
|
120
122
|
result[key] = source[key];
|
|
121
123
|
}
|
|
122
124
|
}
|
|
@@ -140,6 +142,7 @@ class SerializerService {
|
|
|
140
142
|
const inflateWithMap = (abbreviationMap, id) => {
|
|
141
143
|
const reversedAbbreviationMap = this.getReverseKeyMap(abbreviationMap, id);
|
|
142
144
|
for (let key in result) {
|
|
145
|
+
//strip dimensional index from key
|
|
143
146
|
const shortKey = key.split(',')[0];
|
|
144
147
|
let longKey = reversedAbbreviationMap.get(shortKey);
|
|
145
148
|
if (longKey) {
|
|
@@ -156,22 +159,36 @@ class SerializerService {
|
|
|
156
159
|
}
|
|
157
160
|
return result;
|
|
158
161
|
}
|
|
162
|
+
//stringify: convert a multi-dimensional document to a 2-d hash
|
|
159
163
|
stringify(document) {
|
|
160
164
|
let result = {};
|
|
161
165
|
for (let key in document) {
|
|
162
166
|
let value = SerializerService.toString(document[key]);
|
|
163
167
|
if (value) {
|
|
168
|
+
// if (/^:*[a-zA-Z]{2}$/.test(value)) {
|
|
169
|
+
// value = ':' + value;
|
|
170
|
+
// } else if (this.symValReverseMaps.has(value)) {
|
|
171
|
+
// value = this.symValReverseMaps.get(value);
|
|
172
|
+
// }
|
|
164
173
|
result[key] = value;
|
|
165
174
|
}
|
|
166
175
|
}
|
|
167
176
|
return result;
|
|
168
177
|
}
|
|
178
|
+
//parse: convert a 2-d hash to a multi-dimensional document
|
|
169
179
|
parse(document) {
|
|
170
180
|
let result = {};
|
|
171
181
|
for (let [key, value] of Object.entries(document)) {
|
|
172
182
|
if (value === undefined || value === null)
|
|
173
183
|
continue;
|
|
184
|
+
// if (/^:+[a-zA-Z]{2}$/.test(value)) {
|
|
185
|
+
// result[key] = value.slice(1);
|
|
186
|
+
// } else {
|
|
187
|
+
// if (value?.length === 2 && this.symValMaps.has(value)) {
|
|
188
|
+
// value = this.symValMaps.get(value);
|
|
189
|
+
// }
|
|
174
190
|
result[key] = SerializerService.fromString(value);
|
|
191
|
+
// }
|
|
175
192
|
}
|
|
176
193
|
return result;
|
|
177
194
|
}
|
|
@@ -204,20 +221,20 @@ class SerializerService {
|
|
|
204
221
|
const prefix = value.slice(0, 2);
|
|
205
222
|
const rest = value.slice(2);
|
|
206
223
|
switch (prefix) {
|
|
207
|
-
case '/t':
|
|
224
|
+
case '/t': // boolean true
|
|
208
225
|
return true;
|
|
209
|
-
case '/f':
|
|
226
|
+
case '/f': // boolean false
|
|
210
227
|
return false;
|
|
211
|
-
case '/d':
|
|
228
|
+
case '/d': // number
|
|
212
229
|
return Number(rest);
|
|
213
|
-
case '/n':
|
|
230
|
+
case '/n': // null
|
|
214
231
|
return null;
|
|
215
|
-
case '/s':
|
|
232
|
+
case '/s': // object (JSON string)
|
|
216
233
|
if (dateReg.exec(rest)) {
|
|
217
234
|
return new Date(JSON.parse(rest));
|
|
218
235
|
}
|
|
219
236
|
return JSON.parse(rest);
|
|
220
|
-
default:
|
|
237
|
+
default: // string
|
|
221
238
|
return value;
|
|
222
239
|
}
|
|
223
240
|
}
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Cache is a key/value store and used to store commonly accessed Redis metadata
|
|
3
|
+
* (mainly the execution rules for the app) to save time accessing them as they
|
|
4
|
+
* are immutable per verison. Rules are only ejected when a new version
|
|
5
|
+
* (a new distributed executable) is deployed to the quorum
|
|
6
|
+
* and the cache is invalidated/cleared of the prior version.
|
|
7
|
+
*/
|
|
1
8
|
import { ActivityType } from "../../types/activity";
|
|
2
9
|
import { HookRule } from "../../types/hook";
|
|
3
10
|
import { HotMeshApp, HotMeshSettings } from "../../types/hotmesh";
|
|
@@ -14,7 +21,19 @@ declare class Cache {
|
|
|
14
21
|
transitions: Record<string, Record<string, unknown>>;
|
|
15
22
|
hookRules: Record<string, Record<string, HookRule[]>>;
|
|
16
23
|
workItems: Record<string, string>;
|
|
24
|
+
/**
|
|
25
|
+
* The cache is ALWAYS initialized with HotMeshSettings. The other parameters are optional.
|
|
26
|
+
* @param settings
|
|
27
|
+
* @param apps
|
|
28
|
+
* @param schemas
|
|
29
|
+
* @param subscriptions
|
|
30
|
+
* @param transitions
|
|
31
|
+
* @param hookRules
|
|
32
|
+
*/
|
|
17
33
|
constructor(appId: string, settings: HotMeshSettings, apps?: Record<string, HotMeshApp>, schemas?: Record<string, ActivityType>, subscriptions?: Record<string, Record<string, string>>, symbols?: Record<string, Symbols>, symvals?: Record<string, Symbols>, transitions?: Record<string, Record<string, unknown>>, hookRules?: Record<string, Record<string, HookRule[]>>, workItems?: Record<string, string>);
|
|
34
|
+
/**
|
|
35
|
+
* invalidate the cache; settings are not invalidated!
|
|
36
|
+
*/
|
|
18
37
|
invalidate(): void;
|
|
19
38
|
getSettings(): HotMeshSettings;
|
|
20
39
|
setSettings(settings: HotMeshSettings): void;
|
|
@@ -1,7 +1,23 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The Cache is a key/value store and used to store commonly accessed Redis metadata
|
|
4
|
+
* (mainly the execution rules for the app) to save time accessing them as they
|
|
5
|
+
* are immutable per verison. Rules are only ejected when a new version
|
|
6
|
+
* (a new distributed executable) is deployed to the quorum
|
|
7
|
+
* and the cache is invalidated/cleared of the prior version.
|
|
8
|
+
*/
|
|
2
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
10
|
exports.Cache = void 0;
|
|
4
11
|
class Cache {
|
|
12
|
+
/**
|
|
13
|
+
* The cache is ALWAYS initialized with HotMeshSettings. The other parameters are optional.
|
|
14
|
+
* @param settings
|
|
15
|
+
* @param apps
|
|
16
|
+
* @param schemas
|
|
17
|
+
* @param subscriptions
|
|
18
|
+
* @param transitions
|
|
19
|
+
* @param hookRules
|
|
20
|
+
*/
|
|
5
21
|
constructor(appId, settings, apps = {}, schemas = {}, subscriptions = {}, symbols = {}, symvals = {}, transitions = {}, hookRules = {}, workItems = {}) {
|
|
6
22
|
this.appId = appId;
|
|
7
23
|
this.settings = settings;
|
|
@@ -14,6 +30,9 @@ class Cache {
|
|
|
14
30
|
this.hookRules = hookRules;
|
|
15
31
|
this.workItems = workItems;
|
|
16
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* invalidate the cache; settings are not invalidated!
|
|
35
|
+
*/
|
|
17
36
|
invalidate() {
|
|
18
37
|
this.apps = {};
|
|
19
38
|
this.schemas = {};
|
|
@@ -41,6 +41,11 @@ declare abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
41
41
|
zRangeByScore(key: string, score: number | string, value: string | number): Promise<string | null>;
|
|
42
42
|
mintKey(type: KeyType, params: KeyStoreParams): string;
|
|
43
43
|
invalidateCache(): void;
|
|
44
|
+
/**
|
|
45
|
+
* At any given time only a single engine will
|
|
46
|
+
* check for and process work items in the
|
|
47
|
+
* time and signal task queues.
|
|
48
|
+
*/
|
|
44
49
|
reserveScoutRole(scoutType: 'time' | 'signal' | 'activate', delay?: number): Promise<boolean>;
|
|
45
50
|
releaseScoutRole(scoutType: 'time' | 'signal' | 'activate'): Promise<boolean>;
|
|
46
51
|
getSettings(bCreate?: boolean): Promise<HotMeshSettings>;
|
|
@@ -59,7 +64,16 @@ declare abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
59
64
|
setApp(id: string, version: string): Promise<HotMeshApp>;
|
|
60
65
|
activateAppVersion(id: string, version: string): Promise<boolean>;
|
|
61
66
|
registerAppVersion(appId: string, version: string): Promise<any>;
|
|
67
|
+
/**
|
|
68
|
+
* Registers the job, `jobId`, with `originJobId`. In the future,
|
|
69
|
+
* when `originJobId` is interrupted/expired, the items in the
|
|
70
|
+
* list (added via RPUSH) will be interrupted/expired (removed via LPOPed).
|
|
71
|
+
*/
|
|
62
72
|
registerJobDependency(depType: WorkListTaskType, originJobId: string, topic: string, jobId: string, gId: string, pd?: string, multi?: U): Promise<any>;
|
|
73
|
+
/**
|
|
74
|
+
* Ensures a `hook signal` is delisted when its parent activity/job
|
|
75
|
+
* is interrupted/expired.
|
|
76
|
+
*/
|
|
63
77
|
registerSignalDependency(jobId: string, signalKey: string, dad: string, multi?: U): Promise<any>;
|
|
64
78
|
setStats(jobKey: string, jobId: string, dateTime: string, stats: StatsType, appVersion: AppVID, multi?: U): Promise<any>;
|
|
65
79
|
hGetAllResult(result: any): any;
|
|
@@ -68,10 +82,24 @@ declare abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
68
82
|
setStatus(collationKeyStatus: number, jobId: string, appId: string, multi?: U): Promise<any>;
|
|
69
83
|
getStatus(jobId: string, appId: string): Promise<number>;
|
|
70
84
|
setState({ ...state }: StringAnyType, status: number | null, jobId: string, symbolNames: string[], dIds: StringStringType, multi?: U): Promise<string>;
|
|
85
|
+
/**
|
|
86
|
+
* Returns custom search fields and values.
|
|
87
|
+
* NOTE: The `fields` param should NOT prefix items with an underscore.
|
|
88
|
+
*/
|
|
71
89
|
getQueryState(jobId: string, fields: string[]): Promise<StringAnyType>;
|
|
72
90
|
getState(jobId: string, consumes: Consumes, dIds: StringStringType): Promise<[StringAnyType, number] | undefined>;
|
|
73
91
|
getRaw(jobId: string): Promise<StringStringType>;
|
|
92
|
+
/**
|
|
93
|
+
* collate is a generic method for incrementing a value in a hash
|
|
94
|
+
* in order to track their progress during processing.
|
|
95
|
+
*/
|
|
74
96
|
collate(jobId: string, activityId: string, amount: number, dIds: StringStringType, multi?: U): Promise<number>;
|
|
97
|
+
/**
|
|
98
|
+
* synthentic collation affects those activities in the graph
|
|
99
|
+
* that represent the synthetic DAG that was materialized during compilation;
|
|
100
|
+
* Synthetic targeting ensures that re-entry due to failure can be distinguished from
|
|
101
|
+
* purposeful re-entry.
|
|
102
|
+
*/
|
|
75
103
|
collateSynthetic(jobId: string, guid: string, amount: number, multi?: U): Promise<number>;
|
|
76
104
|
setStateNX(jobId: string, appId: string): Promise<boolean>;
|
|
77
105
|
getSchema(activityId: string, appVersion: AppVID): Promise<ActivityType>;
|
|
@@ -92,11 +120,38 @@ declare abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
92
120
|
deleteProcessedTaskQueue(workItemKey: string, key: string, processedKey: string, scrub?: boolean): Promise<void>;
|
|
93
121
|
processTaskQueue(sourceKey: string, destinationKey: string): Promise<any>;
|
|
94
122
|
expireJob(jobId: string, inSeconds: number): Promise<void>;
|
|
123
|
+
/**
|
|
124
|
+
* register the descendants of an expired origin flow to be
|
|
125
|
+
* expired at a future date; options indicate whether this
|
|
126
|
+
* is a standard `expire` or an `interrupt`
|
|
127
|
+
*/
|
|
95
128
|
registerDependenciesForCleanup(jobId: string, deletionTime: number, options: JobCompletionOptions): Promise<void>;
|
|
96
129
|
getDependencies(jobId: string): Promise<string[]>;
|
|
130
|
+
/**
|
|
131
|
+
* registers a hook activity to be awakened (uses ZSET to
|
|
132
|
+
* store the 'sleep group' and LIST to store the events
|
|
133
|
+
* for the given sleep group. Sleep groups are
|
|
134
|
+
* organized into 'n'-second blocks (LISTS))
|
|
135
|
+
*/
|
|
97
136
|
registerTimeHook(jobId: string, gId: string, activityId: string, type: WorkListTaskType, deletionTime: number, dad: string, multi?: U): Promise<void>;
|
|
98
137
|
getNextTask(listKey?: string): Promise<[listKey: string, jobId: string, gId: string, activityId: string, type: WorkListTaskType] | boolean>;
|
|
138
|
+
/**
|
|
139
|
+
* when processing time jobs, the target LIST ID returned
|
|
140
|
+
* from the ZSET query can be prefixed to denote what to
|
|
141
|
+
* do with the work list. (not everything is known in advance,
|
|
142
|
+
* so the ZSET key defines HOW to approach the work in the
|
|
143
|
+
* generic LIST (lists typically contain target job ids)
|
|
144
|
+
* @param {string} listKey - composite key
|
|
145
|
+
*/
|
|
99
146
|
resolveTaskKeyContext(listKey: string): [WorkListTaskType, string];
|
|
147
|
+
/**
|
|
148
|
+
* Interrupts a job and sets sets a job error (410), if 'throw'!=false.
|
|
149
|
+
* This method is called by the engine and not by an activity and is
|
|
150
|
+
* followed by a call to execute job completion/cleanup tasks
|
|
151
|
+
* associated with a job completion event.
|
|
152
|
+
*
|
|
153
|
+
* Todo: move most of this logic to the engine (too much logic for the store)
|
|
154
|
+
*/
|
|
100
155
|
interrupt(topic: string, jobId: string, options?: JobInterruptOptions): Promise<void>;
|
|
101
156
|
scrub(jobId: string): Promise<void>;
|
|
102
157
|
findJobs(queryString?: string, limit?: number, batchSize?: number, cursor?: string): Promise<[string, string[]]>;
|