@hotmeshio/hotmesh 0.0.25 → 0.0.26
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 +22 -8
- package/build/modules/utils.d.ts +1 -0
- package/build/modules/utils.js +6 -1
- package/build/package.json +1 -1
- package/build/services/durable/meshos.d.ts +9 -1
- package/build/services/durable/meshos.js +61 -3
- package/build/services/durable/workflow.d.ts +7 -0
- package/build/services/durable/workflow.js +13 -0
- package/build/types/durable.d.ts +17 -1
- package/build/types/index.d.ts +1 -1
- package/modules/utils.ts +5 -0
- package/package.json +1 -1
- package/services/durable/meshos.ts +66 -6
- package/services/durable/workflow.ts +14 -0
- package/types/durable.ts +20 -0
- package/types/index.ts +2 -0
package/README.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# HotMesh
|
|
2
2
|

|
|
3
3
|
|
|
4
|
-
Elevate Redis from an in-memory data cache
|
|
4
|
+
Elevate Redis from an in-memory data cache, and turn your unpredictable functions into unbreakable workflows.
|
|
5
5
|
|
|
6
|
-
HotMesh is a wrapper for Redis that exposes a higher level set of domain constructs like ‘activities’, ‘workflows’, 'jobs', etc. Behind the scenes, it uses *Redis Data* (Hash, ZSet, List); *Redis Streams* (XReadGroup, XAdd, XLen, etc); and *Redis Publish/Subscribe*.
|
|
6
|
+
**HotMesh** is a wrapper for Redis that exposes a higher level set of domain constructs like ‘activities’, ‘workflows’, 'jobs', etc. Behind the scenes, it uses *Redis Data* (Hash, ZSet, List); *Redis Streams* (XReadGroup, XAdd, XLen, etc); and *Redis Publish/Subscribe*.
|
|
7
7
|
|
|
8
|
-
It's still Redis in the background, but the information flow is
|
|
8
|
+
It's still Redis in the background, but the information flow is reversed. Instead of your functions calling Redis (e.g., for caching a document), Redis governs your function execution. If your microservice container goes down or your function simply fails, HotMesh will restore function state at the point of failure and retry until it succeeds.
|
|
9
9
|
|
|
10
10
|
## Install
|
|
11
11
|
[](https://badge.fury.io/js/%40hotmeshio%2Fhotmesh)
|
|
@@ -79,16 +79,30 @@ The HotMesh SDK is designed to keep your code front-and-center. Write code as yo
|
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
```
|
|
82
|
-
3. Invoke your class, providing a unique id (it's now an idempotent workflow and needs a GUID). Nothing changes from the outside, *but Redis now governs the end-to-end execution.* It's guaranteed to succeed
|
|
82
|
+
3. Invoke your class, providing a unique id (it's now an idempotent workflow and needs a GUID). Nothing changes from the outside, *but Redis now governs the end-to-end execution.* It's guaranteed to succeed, even if it takes a while.
|
|
83
83
|
```javascript
|
|
84
84
|
//mycaller.ts
|
|
85
85
|
|
|
86
86
|
const workflow = new MyWorkflow({ id: 'my123', await: true });
|
|
87
|
-
const response = await workflow.run('
|
|
88
|
-
//
|
|
87
|
+
const response = await workflow.run('World');
|
|
88
|
+
//Hi, World! Hello, World!
|
|
89
89
|
```
|
|
90
90
|
|
|
91
|
-
|
|
91
|
+
Redis governance delivers more than just reliability. Externalizing state fundamentally changes the execution profile for your functions, allowing you to design long-running, durable workflows. Use the following methods to solve the most common state management challenges.
|
|
92
|
+
|
|
93
|
+
- `waitForSignal` | Pause and wait for external event(s) before continuing. The *waitForSignal* method will collate and cache the signals and only awaken your function once they've all arrived.
|
|
94
|
+
- `signal` | Send a signal (and optional payload) to any paused function.
|
|
95
|
+
- `hook` | Redis governance supercharges your functions, transforming them into 're-entrant processes'. Optionally use the *hook* method to spawn parallel execution threads within any running function.
|
|
96
|
+
- `sleep` | Pause function execution for a ridiculous amount of time (months, years, etc). There's no risk of information loss, as Redis governs function state. When your function awakens, function state is efficiently (and automatically) restored.
|
|
97
|
+
- `random` | Generate a deterministic random number that can be used in a reentrant process workflow (replaces `Math.random()`).
|
|
98
|
+
- `executeChild` | Call another durable function and await the response. *Design sophisticated, multi-process solutions by leveraging this command.*
|
|
99
|
+
- `startChild` | Call another durable function, but do not await the response.
|
|
100
|
+
- `set` | Set a value (e.g, `set('name', 'value')`)
|
|
101
|
+
- `get` | Get a value (e.g, `get('name')`)
|
|
102
|
+
- `incr` | Increment (or decrement) a number (e.g, `incr('name', -99)`)
|
|
103
|
+
- `mult` | Multiply (or divide) a number (e.g, `mult('name', 12)`)
|
|
104
|
+
|
|
105
|
+
Refer to the [hotmeshio/samples-typescript](https://github.com/hotmeshio/samples-typescript) repo for usage examples.
|
|
92
106
|
|
|
93
107
|
## Advanced Design
|
|
94
108
|
HotMesh's TypeScript SDK is the easiest way to make your functions durable. But if you need full control over your function lifecycles (including high-volume, high-speed use cases), you can use HotMesh's underlying YAML models to optimize your durable workflows. The following model depicts a sequence of activities orchestrated by HotMesh. Any function you associate with a `topic` in your YAML definition is guaranteed to be durable.
|
|
@@ -225,4 +239,4 @@ HotMesh is a distributed orchestration engine. Refer to the [Distributed Orchest
|
|
|
225
239
|
Gain insight into HotMesh's monitoring, exception handling, and alarm configurations via the [System Lifecycle Guide](https://github.com/hotmeshio/sdk-typescript/blob/main/docs/system_lifecycle.md).
|
|
226
240
|
|
|
227
241
|
## Alpha Release
|
|
228
|
-
So what exacty is an [alpha release](https://github.com/hotmeshio/sdk-typescript/blob/main/docs/alpha.md)
|
|
242
|
+
So what exacty is an [alpha release](https://github.com/hotmeshio/sdk-typescript/blob/main/docs/alpha.md)?
|
package/build/modules/utils.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { RedisClient, RedisMulti } from "../types/redis";
|
|
|
5
5
|
import { StringAnyType } from "../types/serializer";
|
|
6
6
|
import { StreamCode, StreamStatus } from "../types/stream";
|
|
7
7
|
export declare function sleepFor(ms: number): Promise<unknown>;
|
|
8
|
+
export declare function deterministicRandom(seed: number): number;
|
|
8
9
|
export declare function identifyRedisType(redisInstance: any): 'redis' | 'ioredis' | null;
|
|
9
10
|
export declare const polyfill: {
|
|
10
11
|
resolveActivityType(activityType: string): string;
|
package/build/modules/utils.js
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.restoreHierarchy = exports.getValueByPath = exports.getIndexedHash = exports.getSymVal = exports.getSymKey = exports.formatISODate = exports.getTimeSeries = exports.getSubscriptionTopic = exports.findSubscriptionForTrigger = exports.findTopKey = exports.XSleepFor = exports.matchesStatus = exports.matchesStatusCode = exports.identifyRedisTypeFromClass = exports.polyfill = exports.identifyRedisType = exports.sleepFor = void 0;
|
|
3
|
+
exports.restoreHierarchy = exports.getValueByPath = exports.getIndexedHash = exports.getSymVal = exports.getSymKey = exports.formatISODate = exports.getTimeSeries = exports.getSubscriptionTopic = exports.findSubscriptionForTrigger = exports.findTopKey = exports.XSleepFor = exports.matchesStatus = exports.matchesStatusCode = exports.identifyRedisTypeFromClass = exports.polyfill = exports.identifyRedisType = exports.deterministicRandom = exports.sleepFor = void 0;
|
|
4
4
|
async function sleepFor(ms) {
|
|
5
5
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
6
6
|
}
|
|
7
7
|
exports.sleepFor = sleepFor;
|
|
8
|
+
function deterministicRandom(seed) {
|
|
9
|
+
let x = Math.sin(seed) * 10000;
|
|
10
|
+
return x - Math.floor(x);
|
|
11
|
+
}
|
|
12
|
+
exports.deterministicRandom = deterministicRandom;
|
|
8
13
|
function identifyRedisType(redisInstance) {
|
|
9
14
|
const prototype = Object.getPrototypeOf(redisInstance);
|
|
10
15
|
if ('defineCommand' in prototype || Object.keys(prototype).includes('multi')) {
|
package/build/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { WorkflowHandleService } from './handle';
|
|
2
2
|
import { WorkflowService } from './workflow';
|
|
3
|
-
import { FindOptions, MeshOSActivityOptions, MeshOSConfig, MeshOSOptions, MeshOSWorkerOptions, WorkflowSearchOptions } from '../../types/durable';
|
|
3
|
+
import { FindOptions, FindWhereOptions, FindWhereQuery, MeshOSActivityOptions, MeshOSConfig, MeshOSOptions, MeshOSWorkerOptions, WorkflowSearchOptions } from '../../types/durable';
|
|
4
4
|
import { RedisOptions, RedisClass } from '../../types/redis';
|
|
5
5
|
import { StringAnyType } from '../../types/serializer';
|
|
6
6
|
/**
|
|
@@ -97,6 +97,14 @@ export declare class MeshOSService {
|
|
|
97
97
|
* @returns {string}
|
|
98
98
|
*/
|
|
99
99
|
static find(options: FindOptions, ...args: string[]): Promise<string[] | [number]>;
|
|
100
|
+
/**
|
|
101
|
+
* Provides a JSON abstraction for the Redis FT.search command
|
|
102
|
+
* (e.g, `count`, `query`, `return`, `limit`)
|
|
103
|
+
* @param {FindWhereOptions} options
|
|
104
|
+
* @returns {Promise<string[] | [number]>}
|
|
105
|
+
*/
|
|
106
|
+
static findWhere(options: FindWhereOptions): Promise<string[] | [number]>;
|
|
107
|
+
static generateSearchQuery(query: FindWhereQuery[]): string;
|
|
100
108
|
/**
|
|
101
109
|
* returns the workflow handle. The handle can then be
|
|
102
110
|
* used to query for status, state, custom state, etc.
|
|
@@ -151,9 +151,8 @@ class MeshOSService {
|
|
|
151
151
|
class: my.redisClass,
|
|
152
152
|
options: my.redisOptions
|
|
153
153
|
} });
|
|
154
|
-
//workflow name is the function name driving the workflow
|
|
155
154
|
let workflowName;
|
|
156
|
-
if (options
|
|
155
|
+
if (options.workflowName) {
|
|
157
156
|
workflowName = options?.workflowName;
|
|
158
157
|
}
|
|
159
158
|
else if (my.workflowFunctions?.length) {
|
|
@@ -165,7 +164,66 @@ class MeshOSService {
|
|
|
165
164
|
workflowName = target.name;
|
|
166
165
|
}
|
|
167
166
|
}
|
|
168
|
-
return await client.workflow.search(options
|
|
167
|
+
return await client.workflow.search(options.taskQueue ?? my.taskQueue, workflowName, options.namespace ?? my.namespace, options.index ?? my.search.index, ...args); //[count, [id, fields[]], [id, fields[]], [id, fields[]], ...]]
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Provides a JSON abstraction for the Redis FT.search command
|
|
171
|
+
* (e.g, `count`, `query`, `return`, `limit`)
|
|
172
|
+
* @param {FindWhereOptions} options
|
|
173
|
+
* @returns {Promise<string[] | [number]>}
|
|
174
|
+
*/
|
|
175
|
+
static async findWhere(options) {
|
|
176
|
+
const args = [this.generateSearchQuery(options.query)];
|
|
177
|
+
if (options.count) {
|
|
178
|
+
args.push('LIMIT', '0', '0');
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
//limit which hash fields to return
|
|
182
|
+
if (options.return?.length) {
|
|
183
|
+
args.push('RETURN');
|
|
184
|
+
args.push(options.return.length.toString());
|
|
185
|
+
options.return.forEach(returnField => {
|
|
186
|
+
args.push(`_${returnField}`);
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
//paginate
|
|
190
|
+
if (options.limit) {
|
|
191
|
+
args.push('LIMIT', options.limit.start.toString(), options.limit.size.toString());
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return await this.find(options.options ?? {}, ...args);
|
|
195
|
+
}
|
|
196
|
+
static generateSearchQuery(query) {
|
|
197
|
+
const my = new this();
|
|
198
|
+
let queryString = query.map(q => {
|
|
199
|
+
const { field, is, value, type } = q;
|
|
200
|
+
const prefixedFieldName = my.search?.schema && field in my.search.schema ? `@_${field}` : `@${field}`;
|
|
201
|
+
const fieldType = my.search?.schema[field]?.type ?? type ?? 'TEXT';
|
|
202
|
+
switch (fieldType) {
|
|
203
|
+
case 'TAG':
|
|
204
|
+
return `${prefixedFieldName}:{${value}}`;
|
|
205
|
+
case 'TEXT':
|
|
206
|
+
return `${prefixedFieldName}:"${value}"`;
|
|
207
|
+
case 'NUMERIC':
|
|
208
|
+
let range = '';
|
|
209
|
+
if (is.startsWith('=')) {
|
|
210
|
+
range = `[${value} ${value}]`;
|
|
211
|
+
}
|
|
212
|
+
else if (is === '<') {
|
|
213
|
+
range = `[-inf ${value}]`;
|
|
214
|
+
}
|
|
215
|
+
else if (is === '>') {
|
|
216
|
+
range = `[${value} +inf]`;
|
|
217
|
+
}
|
|
218
|
+
else if (is === '[]') {
|
|
219
|
+
range = `[${value[0]} ${value[1]}]`;
|
|
220
|
+
}
|
|
221
|
+
return `${prefixedFieldName}:${range}`;
|
|
222
|
+
default:
|
|
223
|
+
return '';
|
|
224
|
+
}
|
|
225
|
+
}).join(' ');
|
|
226
|
+
return queryString;
|
|
169
227
|
}
|
|
170
228
|
/**
|
|
171
229
|
* returns the workflow handle. The handle can then be
|
|
@@ -28,6 +28,13 @@ export declare class WorkflowService {
|
|
|
28
28
|
* process state and job state)
|
|
29
29
|
*/
|
|
30
30
|
static isSideEffectAllowed(hotMeshClient: HotMesh, prefix: string): Promise<boolean>;
|
|
31
|
+
/**
|
|
32
|
+
* returns a random number between 0 and 1. This number is deterministic
|
|
33
|
+
* and will never vary for a given seed. This is useful for randomizing
|
|
34
|
+
* pathways in a workflow that can be safely replayed.
|
|
35
|
+
* @returns {number}
|
|
36
|
+
*/
|
|
37
|
+
static random(): number;
|
|
31
38
|
/**
|
|
32
39
|
* send signal data into any other paused thread (which is paused and
|
|
33
40
|
* awaiting the signal) from within a hook-thread or the main-thread
|
|
@@ -14,6 +14,7 @@ const factory_1 = require("./factory");
|
|
|
14
14
|
const search_1 = require("./search");
|
|
15
15
|
const worker_1 = require("./worker");
|
|
16
16
|
const stream_1 = require("../../types/stream");
|
|
17
|
+
const utils_1 = require("../../modules/utils");
|
|
17
18
|
class WorkflowService {
|
|
18
19
|
/**
|
|
19
20
|
* Spawn a child workflow. await and return the result.
|
|
@@ -151,6 +152,18 @@ class WorkflowService {
|
|
|
151
152
|
const guidValue = Number(await hotMeshClient.engine.store.exec('HINCRBYFLOAT', workflowGuid, sessionId, '1'));
|
|
152
153
|
return guidValue === 1;
|
|
153
154
|
}
|
|
155
|
+
/**
|
|
156
|
+
* returns a random number between 0 and 1. This number is deterministic
|
|
157
|
+
* and will never vary for a given seed. This is useful for randomizing
|
|
158
|
+
* pathways in a workflow that can be safely replayed.
|
|
159
|
+
* @returns {number}
|
|
160
|
+
*/
|
|
161
|
+
static random() {
|
|
162
|
+
const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
|
|
163
|
+
const COUNTER = store.get('counter');
|
|
164
|
+
const seed = COUNTER.counter = COUNTER.counter + 1;
|
|
165
|
+
return (0, utils_1.deterministicRandom)(seed);
|
|
166
|
+
}
|
|
154
167
|
/**
|
|
155
168
|
* send signal data into any other paused thread (which is paused and
|
|
156
169
|
* awaiting the signal) from within a hook-thread or the main-thread
|
package/build/types/durable.d.ts
CHANGED
|
@@ -82,12 +82,28 @@ type WorkerConfig = {
|
|
|
82
82
|
options?: WorkerOptions;
|
|
83
83
|
search?: WorkflowSearchOptions;
|
|
84
84
|
};
|
|
85
|
+
type FindWhereQuery = {
|
|
86
|
+
field: string;
|
|
87
|
+
is: string;
|
|
88
|
+
value: string | boolean | number | [number, number];
|
|
89
|
+
type?: string;
|
|
90
|
+
};
|
|
85
91
|
type FindOptions = {
|
|
86
92
|
workflowName?: string;
|
|
87
93
|
taskQueue?: string;
|
|
88
94
|
namespace?: string;
|
|
89
95
|
index?: string;
|
|
90
96
|
};
|
|
97
|
+
type FindWhereOptions = {
|
|
98
|
+
options?: FindOptions;
|
|
99
|
+
count?: boolean;
|
|
100
|
+
query: FindWhereQuery[];
|
|
101
|
+
return?: string[];
|
|
102
|
+
limit?: {
|
|
103
|
+
start: number;
|
|
104
|
+
size: number;
|
|
105
|
+
};
|
|
106
|
+
};
|
|
91
107
|
type MeshOSOptions = {
|
|
92
108
|
name: string;
|
|
93
109
|
options: WorkerOptions;
|
|
@@ -126,4 +142,4 @@ type ActivityConfig = {
|
|
|
126
142
|
maximumInterval: string;
|
|
127
143
|
};
|
|
128
144
|
};
|
|
129
|
-
export { ActivityConfig, ActivityWorkflowDataType, ClientConfig, ContextType, ConnectionConfig, Connection, ProxyType, Registry, SignalOptions, FindOptions, HookOptions, MeshOSActivityOptions, MeshOSWorkerOptions, MeshOSClassConfig, MeshOSConfig, MeshOSOptions, WorkerConfig, WorkflowConfig, WorkerOptions, WorkflowSearchOptions, WorkflowDataType, WorkflowOptions, };
|
|
145
|
+
export { ActivityConfig, ActivityWorkflowDataType, ClientConfig, ContextType, ConnectionConfig, Connection, ProxyType, Registry, SignalOptions, FindOptions, FindWhereOptions, FindWhereQuery, HookOptions, MeshOSActivityOptions, MeshOSWorkerOptions, MeshOSClassConfig, MeshOSConfig, MeshOSOptions, WorkerConfig, WorkflowConfig, WorkerOptions, WorkflowSearchOptions, WorkflowDataType, WorkflowOptions, };
|
package/build/types/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export { App, AppVID, AppTransitions, AppSubscriptions } from './app';
|
|
|
3
3
|
export { AsyncSignal } from './async';
|
|
4
4
|
export { CacheMode } from './cache';
|
|
5
5
|
export { CollationFaultType, CollationStage } from './collator';
|
|
6
|
-
export { ActivityConfig, ActivityWorkflowDataType, ClientConfig, ContextType, ConnectionConfig, Connection, ProxyType, Registry, SignalOptions, FindOptions, HookOptions, MeshOSActivityOptions, MeshOSWorkerOptions, MeshOSClassConfig, MeshOSConfig, MeshOSOptions, WorkflowConfig, WorkerConfig, WorkerOptions, WorkflowSearchOptions, WorkflowDataType, WorkflowOptions, } from './durable';
|
|
6
|
+
export { ActivityConfig, ActivityWorkflowDataType, ClientConfig, ContextType, ConnectionConfig, Connection, ProxyType, Registry, SignalOptions, FindOptions, FindWhereOptions, FindWhereQuery, HookOptions, MeshOSActivityOptions, MeshOSWorkerOptions, MeshOSClassConfig, MeshOSConfig, MeshOSOptions, WorkflowConfig, WorkerConfig, WorkerOptions, WorkflowSearchOptions, WorkflowDataType, WorkflowOptions, } from './durable';
|
|
7
7
|
export { HookCondition, HookConditions, HookGate, HookInterface, HookRule, HookRules, HookSignal } from './hook';
|
|
8
8
|
export { RedisClientType as IORedisClientType, RedisMultiType as IORedisMultiType } from './ioredisclient';
|
|
9
9
|
export { ILogger } from './logger';
|
package/modules/utils.ts
CHANGED
|
@@ -8,6 +8,11 @@ export async function sleepFor(ms: number) {
|
|
|
8
8
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
export function deterministicRandom(seed: number): number {
|
|
12
|
+
let x = Math.sin(seed) * 10000;
|
|
13
|
+
return x - Math.floor(x);
|
|
14
|
+
}
|
|
15
|
+
|
|
11
16
|
export function identifyRedisType(redisInstance: any): 'redis' | 'ioredis' | null {
|
|
12
17
|
const prototype = Object.getPrototypeOf(redisInstance);
|
|
13
18
|
if ('defineCommand' in prototype || Object.keys(prototype).includes('multi')) {
|
package/package.json
CHANGED
|
@@ -10,6 +10,8 @@ import { WorkflowService } from './workflow';
|
|
|
10
10
|
import { StreamSignaler } from '../signaler/stream';
|
|
11
11
|
import {
|
|
12
12
|
FindOptions,
|
|
13
|
+
FindWhereOptions,
|
|
14
|
+
FindWhereQuery,
|
|
13
15
|
MeshOSActivityOptions,
|
|
14
16
|
MeshOSConfig,
|
|
15
17
|
MeshOSOptions,
|
|
@@ -245,9 +247,9 @@ export class MeshOSService {
|
|
|
245
247
|
class: my.redisClass,
|
|
246
248
|
options: my.redisOptions
|
|
247
249
|
}});
|
|
248
|
-
|
|
250
|
+
|
|
249
251
|
let workflowName: string;
|
|
250
|
-
if (options
|
|
252
|
+
if (options.workflowName) {
|
|
251
253
|
workflowName = options?.workflowName
|
|
252
254
|
} else if(my.workflowFunctions?.length) {
|
|
253
255
|
let target = my.workflowFunctions[0];
|
|
@@ -258,12 +260,70 @@ export class MeshOSService {
|
|
|
258
260
|
}
|
|
259
261
|
}
|
|
260
262
|
return await client.workflow.search(
|
|
261
|
-
options
|
|
263
|
+
options.taskQueue ?? my.taskQueue,
|
|
262
264
|
workflowName,
|
|
263
|
-
my.namespace,
|
|
264
|
-
my.search.index,
|
|
265
|
+
options.namespace ?? my.namespace,
|
|
266
|
+
options.index ?? my.search.index,
|
|
265
267
|
...args,
|
|
266
|
-
); //[count, [id, fields[], id, fields[], id, fields[], ...]]
|
|
268
|
+
); //[count, [id, fields[]], [id, fields[]], [id, fields[]], ...]]
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Provides a JSON abstraction for the Redis FT.search command
|
|
273
|
+
* (e.g, `count`, `query`, `return`, `limit`)
|
|
274
|
+
* @param {FindWhereOptions} options
|
|
275
|
+
* @returns {Promise<string[] | [number]>}
|
|
276
|
+
*/
|
|
277
|
+
static async findWhere(options: FindWhereOptions): Promise<string[] | [number]> {
|
|
278
|
+
const args: string[] = [this.generateSearchQuery(options.query)];
|
|
279
|
+
if (options.count) {
|
|
280
|
+
args.push('LIMIT', '0', '0');
|
|
281
|
+
} else {
|
|
282
|
+
//limit which hash fields to return
|
|
283
|
+
if (options.return?.length) {
|
|
284
|
+
args.push('RETURN');
|
|
285
|
+
args.push(options.return.length.toString());
|
|
286
|
+
options.return.forEach(returnField => {
|
|
287
|
+
args.push(`_${returnField}`);
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
//paginate
|
|
291
|
+
if (options.limit) {
|
|
292
|
+
args.push('LIMIT', options.limit.start.toString(), options.limit.size.toString());
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return await this.find(options.options ?? {}, ...args);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
static generateSearchQuery(query: FindWhereQuery[]) {
|
|
299
|
+
const my = new this();
|
|
300
|
+
let queryString = query.map(q => {
|
|
301
|
+
const { field, is, value, type } = q;
|
|
302
|
+
const prefixedFieldName = my.search?.schema && field in my.search.schema ? `@_${field}` : `@${field}`;
|
|
303
|
+
const fieldType = my.search?.schema[field]?.type ?? type ?? 'TEXT';
|
|
304
|
+
|
|
305
|
+
switch (fieldType) {
|
|
306
|
+
case 'TAG':
|
|
307
|
+
return `${prefixedFieldName}:{${value}}`;
|
|
308
|
+
case 'TEXT':
|
|
309
|
+
return `${prefixedFieldName}:"${value}"`;
|
|
310
|
+
case 'NUMERIC':
|
|
311
|
+
let range = '';
|
|
312
|
+
if (is.startsWith('=')) {
|
|
313
|
+
range = `[${value} ${value}]`;
|
|
314
|
+
} else if (is === '<') {
|
|
315
|
+
range = `[-inf ${value}]`;
|
|
316
|
+
} else if (is === '>') {
|
|
317
|
+
range = `[${value} +inf]`;
|
|
318
|
+
} else if (is === '[]') {
|
|
319
|
+
range = `[${value[0]} ${value[1]}]`
|
|
320
|
+
}
|
|
321
|
+
return `${prefixedFieldName}:${range}`;
|
|
322
|
+
default:
|
|
323
|
+
return '';
|
|
324
|
+
}
|
|
325
|
+
}).join(' ');
|
|
326
|
+
return queryString;
|
|
267
327
|
}
|
|
268
328
|
|
|
269
329
|
/**
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
WorkflowOptions } from "../../types/durable";
|
|
20
20
|
import { JobOutput, JobState } from '../../types/job';
|
|
21
21
|
import { StreamStatus } from '../../types/stream';
|
|
22
|
+
import { deterministicRandom } from '../../modules/utils';
|
|
22
23
|
|
|
23
24
|
export class WorkflowService {
|
|
24
25
|
|
|
@@ -173,6 +174,19 @@ export class WorkflowService {
|
|
|
173
174
|
return guidValue === 1;
|
|
174
175
|
}
|
|
175
176
|
|
|
177
|
+
/**
|
|
178
|
+
* returns a random number between 0 and 1. This number is deterministic
|
|
179
|
+
* and will never vary for a given seed. This is useful for randomizing
|
|
180
|
+
* pathways in a workflow that can be safely replayed.
|
|
181
|
+
* @returns {number}
|
|
182
|
+
*/
|
|
183
|
+
static random(): number {
|
|
184
|
+
const store = asyncLocalStorage.getStore();
|
|
185
|
+
const COUNTER = store.get('counter');
|
|
186
|
+
const seed = COUNTER.counter = COUNTER.counter + 1;
|
|
187
|
+
return deterministicRandom(seed);
|
|
188
|
+
}
|
|
189
|
+
|
|
176
190
|
/**
|
|
177
191
|
* send signal data into any other paused thread (which is paused and
|
|
178
192
|
* awaiting the signal) from within a hook-thread or the main-thread
|
package/types/durable.ts
CHANGED
|
@@ -93,6 +93,13 @@ type WorkerConfig = {
|
|
|
93
93
|
search?: WorkflowSearchOptions;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
type FindWhereQuery = {
|
|
97
|
+
field: string;
|
|
98
|
+
is: string;
|
|
99
|
+
value: string | boolean | number | [number, number];
|
|
100
|
+
type?: string; //default is TEXT
|
|
101
|
+
}
|
|
102
|
+
|
|
96
103
|
type FindOptions = {
|
|
97
104
|
workflowName?: string; //also the function name
|
|
98
105
|
taskQueue?: string;
|
|
@@ -100,6 +107,17 @@ type FindOptions = {
|
|
|
100
107
|
index?: string; //the FT search index name
|
|
101
108
|
}
|
|
102
109
|
|
|
110
|
+
type FindWhereOptions = {
|
|
111
|
+
options?: FindOptions;
|
|
112
|
+
count?: boolean;
|
|
113
|
+
query: FindWhereQuery[];
|
|
114
|
+
return?: string[];
|
|
115
|
+
limit?: {
|
|
116
|
+
start: number,
|
|
117
|
+
size: number
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
103
121
|
type MeshOSOptions = {
|
|
104
122
|
name: string;
|
|
105
123
|
options: WorkerOptions;
|
|
@@ -156,6 +174,8 @@ export {
|
|
|
156
174
|
Registry,
|
|
157
175
|
SignalOptions,
|
|
158
176
|
FindOptions,
|
|
177
|
+
FindWhereOptions,
|
|
178
|
+
FindWhereQuery,
|
|
159
179
|
HookOptions,
|
|
160
180
|
MeshOSActivityOptions,
|
|
161
181
|
MeshOSWorkerOptions,
|