@hotmeshio/hotmesh 0.0.25 → 0.0.27

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 CHANGED
@@ -1,11 +1,13 @@
1
1
  # HotMesh
2
2
  ![alpha release](https://img.shields.io/badge/release-alpha-yellow)
3
3
 
4
- Elevate Redis from an in-memory data cache to a game-changing [service mesh](https://github.com/hotmeshio/sdk-typescript/blob/main/docs/faq.md#what-is-hotmesh). Turn your unpredictable functions into unbreakable workflows.
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 concepts like ‘activities’, ‘workflows’, and 'jobs'. 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 fundamentally different. Your functions no longer call Redis (e.g., to cache a document) and instead are called by Redis.
8
+ It's still Redis in the background, but your functions are run as *reentrant processes* and are executed in a distributed environment, with all the benefits of a distributed system, including fault tolerance, scalability, and high availability.
9
+
10
+ Write functions in your own preferred style, and let Redis govern their execution at its unmatched scale and performance.
9
11
 
10
12
  ## Install
11
13
  [![npm version](https://badge.fury.io/js/%40hotmeshio%2Fhotmesh.svg)](https://badge.fury.io/js/%40hotmeshio%2Fhotmesh)
@@ -79,16 +81,30 @@ The HotMesh SDK is designed to keep your code front-and-center. Write code as yo
79
81
  }
80
82
  }
81
83
  ```
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 and return regardless of network or microservice failure. Redis will simply inflate like a balloon and deflate as your services come back online.
84
+ 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
85
  ```javascript
84
86
  //mycaller.ts
85
87
 
86
88
  const workflow = new MyWorkflow({ id: 'my123', await: true });
87
- const response = await workflow.run('John');
88
- //Hello, John! Hi, John!
89
+ const response = await workflow.run('World');
90
+ //Hi, World! Hello, World!
89
91
  ```
90
92
 
91
- >NOTE: MeshOS provides a full suite of durable workflow methods for extending your functions, including: `waitForSignal`, `signal`, `hook`, `sleep` (months, years, etc), `executeChild`, `startChild`, `get`, `set`, `incr` (increment), and `mult` (multiply).
93
+ 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.
94
+
95
+ - `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.
96
+ - `signal` | Send a signal (and optional payload) to any paused function.
97
+ - `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.
98
+ - `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.
99
+ - `random` | Generate a deterministic random number that can be used in a reentrant process workflow (replaces `Math.random()`).
100
+ - `executeChild` | Call another durable function and await the response. *Design sophisticated, multi-process solutions by leveraging this command.*
101
+ - `startChild` | Call another durable function, but do not await the response.
102
+ - `set` | Set a value (e.g, `set('name', 'value')`)
103
+ - `get` | Get a value (e.g, `get('name')`)
104
+ - `incr` | Increment (or decrement) a number (e.g, `incr('name', -99)`)
105
+ - `mult` | Multiply (or divide) a number (e.g, `mult('name', 12)`)
106
+
107
+ Refer to the [hotmeshio/samples-typescript](https://github.com/hotmeshio/samples-typescript) repo for usage examples.
92
108
 
93
109
  ## Advanced Design
94
110
  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 +241,4 @@ HotMesh is a distributed orchestration engine. Refer to the [Distributed Orchest
225
241
  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
242
 
227
243
  ## Alpha Release
228
- So what exacty is an [alpha release](https://github.com/hotmeshio/sdk-typescript/blob/main/docs/alpha.md)?!
244
+ So what exacty is an [alpha release](https://github.com/hotmeshio/sdk-typescript/blob/main/docs/alpha.md)?
@@ -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;
@@ -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')) {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotmeshio/hotmesh",
3
- "version": "0.0.25",
3
+ "version": "0.0.27",
4
4
  "description": "Unbreakable Workflows",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -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?.workflowName) {
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?.taskQueue ?? my.taskQueue, workflowName, my.namespace, my.search.index, ...args); //[count, [id, fields[], id, fields[], id, fields[], ...]]
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('=')) { //equal
210
+ range = `[${value} ${value}]`;
211
+ }
212
+ else if (is.startsWith('<')) { //less than or equal
213
+ range = `[-inf ${value}]`;
214
+ }
215
+ else if (is.startsWith('>')) { //greater than or equal
216
+ range = `[${value} +inf]`;
217
+ }
218
+ else if (is === '[]') { //between
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
@@ -23,9 +23,10 @@ export declare class Search {
23
23
  * calling any method that produces side effects (changes the value)
24
24
  */
25
25
  getSearchSessionGuid(): string;
26
- set(key: string, value: string): Promise<void>;
26
+ set(...args: string[]): Promise<void>;
27
27
  get(key: string): Promise<string>;
28
- del(key: string): Promise<void>;
28
+ mget(...args: string[]): Promise<string[]>;
29
+ del(...args: string[]): Promise<number | void>;
29
30
  incr(key: string, val: number): Promise<number>;
30
31
  mult(key: string, val: number): Promise<number>;
31
32
  }
@@ -59,12 +59,17 @@ class Search {
59
59
  //return the search session as it would exist in the search session index
60
60
  return `${this.searchSessionId}-${this.searchSessionIndex++}-`;
61
61
  }
62
- async set(key, value) {
62
+ async set(...args) {
63
63
  const ssGuid = this.getSearchSessionGuid();
64
64
  const ssGuidValue = Number(await this.store.exec('HINCRBYFLOAT', this.jobId, ssGuid, '1'));
65
65
  if (ssGuidValue === 1) {
66
- //only allowed to set a value the first time
67
- await this.store.exec('HSET', this.jobId, this.safeKey(key), value.toString());
66
+ const safeArgs = [];
67
+ for (let i = 0; i < args.length; i += 2) {
68
+ const key = this.safeKey(args[i]);
69
+ const value = args[i + 1].toString();
70
+ safeArgs.push(key, value);
71
+ }
72
+ await this.store.exec('HSET', this.jobId, ...safeArgs);
68
73
  }
69
74
  }
70
75
  async get(key) {
@@ -76,11 +81,29 @@ class Search {
76
81
  return '';
77
82
  }
78
83
  }
79
- async del(key) {
84
+ async mget(...args) {
85
+ const safeArgs = [];
86
+ for (let i = 0; i < args.length; i++) {
87
+ safeArgs.push(this.safeKey(args[i]));
88
+ }
89
+ try {
90
+ return await this.store.exec('HMGET', this.jobId, ...safeArgs);
91
+ }
92
+ catch (err) {
93
+ this.hotMeshClient.logger.error('durable-search-mget-error', { err });
94
+ return [];
95
+ }
96
+ }
97
+ async del(...args) {
80
98
  const ssGuid = this.getSearchSessionGuid();
81
99
  const ssGuidValue = Number(await this.store.exec('HINCRBYFLOAT', this.jobId, ssGuid, '1'));
82
100
  if (ssGuidValue === 1) {
83
- await this.store.exec('HDEL', this.jobId, this.safeKey(key));
101
+ const safeArgs = [];
102
+ for (let i = 0; i < args.length; i++) {
103
+ safeArgs.push(this.safeKey(args[i]));
104
+ }
105
+ const response = await this.store.exec('HDEL', this.jobId, ...safeArgs);
106
+ return isNaN(response) ? undefined : Number(response);
84
107
  }
85
108
  }
86
109
  async incr(key, val) {
@@ -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
@@ -82,12 +82,28 @@ type WorkerConfig = {
82
82
  options?: WorkerOptions;
83
83
  search?: WorkflowSearchOptions;
84
84
  };
85
+ type FindWhereQuery = {
86
+ field: string;
87
+ is: '=' | '==' | '>=' | '<=' | '[]';
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, };
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotmeshio/hotmesh",
3
- "version": "0.0.25",
3
+ "version": "0.0.27",
4
4
  "description": "Unbreakable Workflows",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -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
- //workflow name is the function name driving the workflow
250
+
249
251
  let workflowName: string;
250
- if (options?.workflowName) {
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?.taskQueue ?? my.taskQueue,
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('=')) { //equal
313
+ range = `[${value} ${value}]`;
314
+ } else if (is.startsWith('<')) { //less than or equal
315
+ range = `[-inf ${value}]`;
316
+ } else if (is.startsWith('>')) { //greater than or equal
317
+ range = `[${value} +inf]`;
318
+ } else if (is === '[]') { //between
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
  /**
@@ -69,12 +69,17 @@ export class Search {
69
69
  return `${this.searchSessionId}-${this.searchSessionIndex++}-`;
70
70
  }
71
71
 
72
- async set(key: string, value: string): Promise<void> {
72
+ async set(...args: string[]): Promise<void> {
73
73
  const ssGuid = this.getSearchSessionGuid();
74
74
  const ssGuidValue = Number(await this.store.exec('HINCRBYFLOAT', this.jobId, ssGuid, '1') as string);
75
75
  if (ssGuidValue === 1) {
76
- //only allowed to set a value the first time
77
- await this.store.exec('HSET', this.jobId, this.safeKey(key), value.toString());
76
+ const safeArgs: string[] = [];
77
+ for (let i = 0; i < args.length; i += 2) {
78
+ const key = this.safeKey(args[i]);
79
+ const value = args[i+1].toString();
80
+ safeArgs.push(key, value);
81
+ }
82
+ await this.store.exec('HSET', this.jobId, ...safeArgs);
78
83
  }
79
84
  }
80
85
 
@@ -87,11 +92,29 @@ export class Search {
87
92
  }
88
93
  }
89
94
 
90
- async del(key: string): Promise<void> {
95
+ async mget(...args: string[]): Promise<string[]> {
96
+ const safeArgs: string[] = [];
97
+ for (let i = 0; i < args.length; i++) {
98
+ safeArgs.push(this.safeKey(args[i]));
99
+ }
100
+ try {
101
+ return await this.store.exec('HMGET', this.jobId, ...safeArgs) as string[];
102
+ } catch (err) {
103
+ this.hotMeshClient.logger.error('durable-search-mget-error', { err });
104
+ return [];
105
+ }
106
+ }
107
+
108
+ async del(...args: string[]): Promise<number | void> {
91
109
  const ssGuid = this.getSearchSessionGuid();
92
110
  const ssGuidValue = Number(await this.store.exec('HINCRBYFLOAT', this.jobId, ssGuid, '1') as string);
93
111
  if (ssGuidValue === 1) {
94
- await this.store.exec('HDEL', this.jobId, this.safeKey(key));
112
+ const safeArgs: string[] = [];
113
+ for (let i = 0; i < args.length; i++) {
114
+ safeArgs.push(this.safeKey(args[i]));
115
+ }
116
+ const response = await this.store.exec('HDEL', this.jobId, ...safeArgs);
117
+ return isNaN(response as unknown as number) ? undefined : Number(response);
95
118
  }
96
119
  }
97
120
 
@@ -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: '=' | '==' | '>=' | '<=' | '[]';
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,
package/types/index.ts CHANGED
@@ -38,6 +38,8 @@ export {
38
38
  Registry,
39
39
  SignalOptions,
40
40
  FindOptions,
41
+ FindWhereOptions,
42
+ FindWhereQuery,
41
43
  HookOptions,
42
44
  MeshOSActivityOptions,
43
45
  MeshOSWorkerOptions,