@hotmeshio/hotmesh
Version:
Serverless Workflow
646 lines (543 loc) • 16.7 kB
text/typescript
import { WorkflowHandleService } from '../services/meshflow/handle';
import { LogLevel } from './logger';
import { ProviderConfig, ProvidersConfig } from './provider';
import { StringAnyType, StringStringType } from './serializer';
import { StreamData, StreamError } from './stream';
/**
* Type definition for workflow configuration.
*/
type WorkflowConfig = {
/**
* Backoff coefficient for retry mechanism.
* @default 10 (HMSH_MESHFLOW_EXP_BACKOFF)
*/
backoffCoefficient?: number;
/**
* Maximum number of attempts for retries.
* @default 5 (HMSH_MESHFLOW_MAX_ATTEMPTS)
*/
maximumAttempts?: number;
/**
* Maximum interval between retries.
* @default 120s (HMSH_MESHFLOW_MAX_INTERVAL)
*/
maximumInterval?: string;
/**
* Whether to throw an error on final failure after retries are exhausted
* or return the error object as a standard response containing error-related
* fields like `stack`, `code`, `message`.
* @default true
*/
throwOnError?: boolean;
};
type WorkflowContext = {
/**
* can the workflow be retried if an error occurs
*/
canRetry: boolean;
COUNTER: {
/**
* the reentrant semaphore parent counter object for object reference during increment
*/
counter: number;
};
/**
* the reentrant semaphore, incremented in real-time as idempotent statements are re-traversed upon reentry. Indicates the current semaphore count.
*/
counter: number;
/**
* number as string for the replay cursor
*/
cursor: string;
/**
* the replay hash of name/value pairs representing prior executions
*/
replay: StringStringType;
/**
* the HotMesh App namespace
* @default meshflow
*/
namespace: string;
/**
* holds list of interruption payloads; if list is longer than 1 when the error is thrown, a `collator` subflow will be used
*/
interruptionRegistry: any[];
/**
* entry point ancestor flow; might be the parent; will never be self
*/
originJobId: string;
/**
* the workflow/job ID
*/
workflowId: string;
/**
* the dimensional isolation for the reentrant hook, expressed in the format `0,0`, `0,1`, etc
*/
workflowDimension: string;
/**
* a concatenation of the task queue and workflow name (e.g., `${taskQueueName}-${workflowName}`)
*/
workflowTopic: string;
/**
* the open telemetry trace context for the workflow, used for logging and tracing. If a sink is enabled, this will be sent to the sink.
*/
workflowTrace: string;
/**
* the open telemetry span context for the workflow, used for logging and tracing. If a sink is enabled, this will be sent to the sink.
*/
workflowSpan: string;
/**
* the native HotMesh message that encapsulates the arguments, metadata, and raw data for the workflow
*/
raw: StreamData;
/**
* the HotMesh connection configuration (io/redis NPM package reference and login credentials)
*/
connection: Connection;
/**
* if present, the workflow will delay expiration for the specified number of seconds
*/
expire?: number;
};
/**
* The schema for the full-text-search (RediSearch) index.
*/
export type WorkflowSearchSchema = Record<
string,
{
/**
* The FT.SEARCH field type. One of: TEXT, NUMERIC, TAG. TEXT is
* most expensive, but also most expressive.
*/
type: 'TEXT' | 'NUMERIC' | 'TAG';
/**
* FT.SEARCH SORTABLE field. If true, results may be sorted according to this field
* @default false
*/
sortable?: boolean;
/**
* FT.SEARCH NOSTEM field. applies to TEXT fields types.
* If true, the text field index will not stem words
* @default false
*/
nostem?: boolean;
/**
* FT.SEARCH NOINDEX field. If true and if the field is sortable, the field will aid
* in sorting results but not be directly indexed as a standalone
* @default false
*/
noindex?: boolean;
/**
* if true, the field is indexed and searchable within the FT.SEARCH index
* This is different from `noindex` which is FT.SEARCH specific and relates
* to sorting and indexing. This is a general flag for the field that will
* enable or disable indexing and searching entirely. Use for fields with
* absolutely no meaning to query or sorting but which are important
* nonetheless as part of the data record that is saved and returned.
* @default true
*/
indexed?: boolean;
/**
* An array of possible values for the field
*/
examples?: string[];
/**
* The 'nilable' setting may NOT be set to `true` for
* NUMBER types as it causes an indexing error;
* consider a custom (e.g., negative number) value to represent
* `null` if desired for a NUMERIC field.
* Set to true only if the field is a TEXT or TAG type and
* you wish to save the string `null` as a value to search
* on (the tag, {null}, or the string, (null)
* @default false
*/
nilable?: boolean;
/**
* possible scalar/primitive types for the field. Use when
* serializing and restoring data to ensure the field is
* properly typed. If not provided, the field will be
* treated as a string.
*/
primitive?: 'string' | 'number' | 'boolean' | 'array' | 'object';
/**
* if true, the field is required to be present in the data record
* @default false
*/
required?: boolean;
/**
* an enumerated list of allowed values; if field is nilable, it is implied
* and therefore not necessary to include `null` in the list
* @default []
*/
enum?: string[];
/**
* a regular expression pattern for the field
* @default '.*'
* @example '^[a-zA-Z0-9_]*$'
*/
pattern?: string;
/**
* literal value to use for the indexed field name (without including the standard underscore (_) prefix isolate)
*/
fieldName?: string;
}
>;
type WorkflowSearchOptions = {
/** FT index name (myapp:myindex) */
index?: string;
/** FT prefixes (['myapp:myindex:prefix1', 'myapp:myindex:prefix2']) */
prefix?: string[];
/**
* Schema mapping each field. Each field is a key-value pair where the key is the field name
* and the value is a record of field options. If the fieldName is provided,
* it will be used as the indexed field name. If not provided
* key will be used as the indexed field name with an underscore prefix.
*
*/
schema?: WorkflowSearchSchema;
/** Additional data as a key-value record */
data?: StringStringType;
};
type SearchResults = {
/**
* the total number of results
*/
count: number;
/**
* the raw FT.SEARCH query string
*/
query: string;
/**
* the raw FT.SEARCH results as an array of objects
*/
data: StringStringType[];
};
type WorkflowOptions = {
/**
* the namespace for the workflow
* @default meshflow
*/
namespace?: string;
/**
* the task queue for the workflow; optional if entity is provided
*/
taskQueue?: string;
/**
* input arguments to pass in
*/
args: any[];
/**
* the job id
*/
workflowId?: string;
/**
* if invoking a workflow, passing 'entity' will apply the value as the workflowName, taskQueue, and prefix, ensuring the FT.SEARCH index is properly scoped. This is a convenience method but limits options.
*/
entity?: string;
/**
* the name of the user's workflow function; optional if 'entity' is provided
*/
workflowName?: string;
/**
* the parent workflow id; adjacent ancestor ID
*/
parentWorkflowId?: string;
/**
* the entry point workflow id
*/
originJobId?: string;
/**
* OpenTelemetry trace context for the workflow
*/
workflowTrace?: string;
/**
* OpenTelemetry span context for the workflow
*/
workflowSpan?: string;
/**
* the full-text-search (RediSearch) options for the workflow
*/
search?: WorkflowSearchOptions;
/**
* marker data (begins with a -)
*/
marker?: StringStringType;
/**
* the workflow configuration object
*/
config?: WorkflowConfig;
/**
* sets the number of seconds a workflow may exist after completion. As the process engine is an in-memory cache, the default policy is to expire and scrub the job hash as soon as it completes.
*/
expire?: number;
/**
* system flag to indicate that the flow should remain open beyond main method completion while still emitting the 'job done' event
*/
persistent?: boolean;
/**
* default is true; set to false to optimize workflows that do not require a `signal in`
*/
signalIn?: boolean;
/**
* default is true; if false, will not await the execution
*/
await?: boolean;
/**
* If provided, the job will initialize in a pending state, reserving
* only the job ID (HSETNX) and persisting search and marker (if provided).
* If a `resume` signal is sent before the specified number of seconds,
* the job will resume as normal, transitioning to the adjacent children
* of the trigger. If the job is not resumed within the number
* of seconds specified, the job will be scrubbed. No dependencies
* are added for a job in a pending state; however, dependencies
* will be added after the job is resumed if relevant.
*/
pending?: number;
/**
* Provide to set the engine name. This MUST be unique, so do not
* provide unless it is guaranteed to be a unique engine/worker guid
* when identifying the point of presence within the mesh.
*/
guid?: string;
};
/**
* Options for setting up a hook.
* 'meshflow' is the default namespace if not provided;
* similar to setting `appid` in the YAML
*/
type HookOptions = {
/** Optional namespace under which the hook function will be grouped */
namespace?: string;
/** Optional task queue, needed unless 'entity' is provided */
taskQueue?: string;
/** Input arguments to pass into the hook */
args: any[];
/**
* Optional entity name. If provided, applies as the workflowName,
* taskQueue, and prefix. This scopes the FT.SEARCH index appropriately.
* This is a convenience method but limits options.
*/
entity?: string;
/** Execution ID, also known as the job ID to hook into */
workflowId?: string;
/** The name of the user's hook function */
workflowName?: string;
/** Bind additional search terms immediately before hook reentry */
search?: WorkflowSearchOptions;
/** Hook function constraints (backoffCoefficient, maximumAttempts, maximumInterval) */
config?: WorkflowConfig;
};
/**
* Options for sending signals in a workflow.
*/
type SignalOptions = {
/**
* Task queue associated with the workflow
*/
taskQueue: string;
/**
* Input data for the signal (any serializable object)
*/
data: StringAnyType;
/**
* Execution ID, also known as the job ID
*/
workflowId: string;
/**
* Optional name of the user's workflow function
*/
workflowName?: string;
};
type ActivityWorkflowDataType = {
activityName: string;
arguments: any[];
workflowId: string;
workflowTopic: string;
};
type WorkflowDataType = {
arguments: any[];
workflowId: string;
workflowTopic: string;
workflowDimension?: string; //is present if hook (not main workflow)
originJobId?: string; //is present if there is an originating ancestor job
canRetry?: boolean;
expire?: number;
};
type Connection = ProviderConfig | ProvidersConfig;
type ClientConfig = {
connection: Connection;
};
type Registry = {
[key: string]: Function;
};
type WorkerConfig = {
/** Connection configuration for the worker */
connection: Connection;
/**
* Namespace used in the app configuration, denoted as `appid` in the YAML
* @default meshflow
*/
namespace?: string;
/** Task queue name, denoted as `subscribes` in the YAML (e.g., 'hello-world') */
taskQueue: string;
/** Target function or a record type with a name (string) and reference function */
workflow: Function | Record<string | symbol, Function>;
/** Additional options for configuring the worker */
options?: WorkerOptions;
/** Search options for workflow execution details */
search?: WorkflowSearchOptions;
/**
* Provide to set the engine name. This MUST be unique, so do not
* provide unless it is guaranteed to be a unique engine/worker guid
* when identifying the point of presence within the mesh.
*/
guid?: string;
};
type FindWhereQuery = {
field: string;
is: '=' | '==' | '>=' | '<=' | '[]';
value: string | boolean | number | [number, number];
type?: string; //default is TEXT
};
type FindOptions = {
workflowName?: string; //also the function name
taskQueue?: string;
namespace?: string;
index?: string;
search?: WorkflowSearchOptions;
};
type FindWhereOptions = {
options?: FindOptions;
count?: boolean;
query: FindWhereQuery[];
return?: string[];
limit?: {
start: number;
size: number;
};
};
type FindJobsOptions = {
/** The workflow name; include an asterisk for wilcard search; refer to Redis SCAN for the allowed format */
match?: string;
/**
* application namespace
* @default meshflow
*/
namespace?: string;
/** The suggested response limit. Reduce batch size to reduce the likelihood of large overages. */
limit?: number;
/** How many records to scan at a time */
batch?: number;
/** The start cursor; defaults to 0 */
cursor?: string;
};
type WorkerOptions = {
/** Log level: debug, info, warn, error */
logLevel?: LogLevel;
/** Maximum number of attempts, default 5 (HMSH_MESHFLOW_MAX_ATTEMPTS) */
maximumAttempts?: number;
/** Backoff coefficient for retry logic, default 10 (HMSH_MESHFLOW_EXP_BACKOFF) */
backoffCoefficient?: number;
/** Maximum interval between retries, default 120s (HMSH_MESHFLOW_MAX_INTERVAL) */
maximumInterval?: string;
};
type ContextType = {
workflowId: string;
workflowTopic: string;
};
type FunctionSignature<T> = T extends (...args: infer A) => infer R
? (...args: A) => R
: never;
type ProxyType<ACT> = {
[K in keyof ACT]: FunctionSignature<ACT[K]>;
};
/**
* Configuration settings for activities within a workflow.
*/
type ActivityConfig = {
/** place holder setting; unused at this time (re: activity workflow expire configuration) */
expire?: number;
/** Start to close timeout for the activity; not yet implemented */
startToCloseTimeout?: string;
/** Configuration for specific activities, type not yet specified */
activities?: any;
/** Retry policy configuration for activities */
retryPolicy?: {
/** Maximum number of retry attempts, default is 5 (HMSH_MESHFLOW_MAX_ATTEMPTS) */
maximumAttempts?: number;
/** Factor by which the retry timeout increases, default is 10 (HMSH_MESHFLOW_MAX_INTERVAL) */
backoffCoefficient?: number;
/** Maximum interval between retries, default is '120s' (HMSH_MESHFLOW_EXP_BACKOFF) */
maximumInterval?: string;
/** Whether to throw an error on failure, default is true */
throwOnError?: boolean;
};
};
/**
* The proxy response object returned from the activity proxy flow
*/
type ProxyResponseType<T> = {
data?: T; //expected data
$error?: StreamError;
done?: boolean; //non-existent if error was thrown in transition (not during execution)
jc: string;
ju: string;
};
/**
* The child flow response object returned from the main flow during recursion
*/
type ChildResponseType<T> = {
data?: T; //expected data
$error?: StreamError;
done?: boolean; //non-existent if error was thrown in transition (not during execution)
jc: string;
ju: string;
};
interface ClientWorkflow {
start(options: WorkflowOptions): Promise<WorkflowHandleService>;
signal(
signalId: string,
data: StringAnyType,
namespace?: string,
): Promise<string>;
hook(options: HookOptions): Promise<string>;
getHandle(
taskQueue: string,
workflowName: string,
workflowId: string,
namespace?: string,
): Promise<WorkflowHandleService>;
search(
taskQueue: string,
workflowName: string,
namespace: string | null,
index: string,
...query: string[]
): Promise<string[]>;
}
export {
ActivityConfig,
ActivityWorkflowDataType,
ChildResponseType,
ClientConfig,
ClientWorkflow,
ContextType,
Connection,
ProxyResponseType,
ProxyType,
Registry,
SignalOptions,
FindJobsOptions,
FindOptions,
FindWhereOptions,
FindWhereQuery,
HookOptions,
SearchResults,
WorkerConfig,
WorkflowConfig,
WorkerOptions,
WorkflowSearchOptions,
WorkflowDataType,
WorkflowOptions,
WorkflowContext,
};