@hotmeshio/hotmesh
Version:
Serverless Workflow
177 lines (176 loc) • 6.86 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.WorkflowHandleService = void 0;
const exporter_1 = require("./exporter");
/**
* The WorkflowHandleService provides methods to interact with a running
* workflow. This includes exporting the workflow, sending signals, and
* querying the state of the workflow. It is instanced/accessed via the
* MeshFlow.Client class.
*
* @example
* ```typescript
* import { Client } from '@hotmeshio/hotmesh';
* import { Client as Postgres } from 'pg';
*
* const client = new Client({ connection: {
* class: Postgres,
* options: { connectionString: 'postgres://user:pass@localhost:5432/db' }
* }});
*
* const handle = await client.workflow.start({
* args: ['HotMesh'],
* taskQueue: 'hello-world',
* });
*
* //perform actions like send a signal
* await handle.signal('my-signal', { data: 'Hello' });
* ```
*/
class WorkflowHandleService {
/**
* @private
*/
constructor(hotMesh, workflowTopic, workflowId) {
this.workflowTopic = workflowTopic;
this.workflowId = workflowId;
this.hotMesh = hotMesh;
this.exporter = new exporter_1.ExporterService(this.hotMesh.appId, this.hotMesh.engine.store, this.hotMesh.engine.logger);
}
/**
* Exports the workflow state to a JSON object.
*/
async export(options) {
return this.exporter.export(this.workflowId, options);
}
/**
* Sends a signal to the workflow. This is a way to send
* a message to a workflow that is paused due to having
* executed `MeshFlow.workflow.waitFor`. The workflow
* will awaken if no other signals are pending.
*/
async signal(signalId, data) {
await this.hotMesh.hook(`${this.hotMesh.appId}.wfs.signal`, {
id: signalId,
data,
});
}
/**
* Returns the job state of the workflow. If the workflow has completed
* this is also the job output. If the workflow is still running, this
* is the current state of the job, but it may change depending upon
* the activities that remain.
*/
async state(metadata = false) {
const state = await this.hotMesh.getState(`${this.hotMesh.appId}.execute`, this.workflowId);
if (!state.data && state.metadata.err) {
throw new Error(JSON.parse(state.metadata.err));
}
return metadata ? state : state.data;
}
/**
* Returns the current search state of the workflow. This is
* different than the job state or individual activity state.
* Search state represents name/value pairs that were added
* to the workflow.
*/
async queryState(fields) {
return await this.hotMesh.getQueryState(this.workflowId, fields);
}
/**
* Returns the current status of the workflow. This is a semaphore
* value that represents the current state of the workflow, where
* 0 is complete and a negative value represents that the flow was
* interrupted.
*/
async status() {
return await this.hotMesh.getStatus(this.workflowId);
}
/**
* Interrupts a running workflow. Standard Job Completion tasks will
* run. Subscribers will be notified and the job hash will be expired.
*/
async interrupt(options) {
return await this.hotMesh.interrupt(`${this.hotMesh.appId}.execute`, this.workflowId, options);
}
/**
* Waits for the workflow to complete and returns the result. If
* the workflow response includes an error, this method will rethrow
* the error, including the stack trace if available.
* Wrap calls in a try/catch as necessary to avoid unhandled exceptions.
*/
async result(config) {
const topic = `${this.hotMesh.appId}.executed.${this.workflowId}`;
let isResolved = false;
return new Promise(async (resolve, reject) => {
/**
* rejects/resolves the promise based on the `throwOnError`
* default behavior is to throw if error
*/
const safeReject = (err) => {
if (config?.throwOnError === false) {
return resolve(err);
}
reject(err);
};
/**
* Common completion function that unsubscribes from the topic/returns
*/
const complete = async (response, err) => {
if (isResolved)
return;
isResolved = true;
if (err) {
return safeReject(err);
}
else if (!response) {
const state = await this.hotMesh.getState(`${this.hotMesh.appId}.execute`, this.workflowId);
if (state.data?.done && !state.data?.$error) {
return resolve(state.data.response);
}
else if (state.data?.$error) {
return safeReject(state.data.$error);
}
else if (state.metadata.err) {
return safeReject(JSON.parse(state.metadata.err));
}
response = state.data?.response;
}
resolve(response);
};
//more expensive; fetches the entire job, not just the `status`
if (config?.state) {
const state = await this.hotMesh.getState(`${this.hotMesh.appId}.execute`, this.workflowId);
if (state?.data?.done && !state.data?.$error) {
return complete(state.data.response);
}
else if (state.data?.$error) {
return complete(null, state.data.$error);
}
else if (state.metadata.err) {
return complete(null, JSON.parse(state.metadata.err));
}
}
//subscribe to 'done' topic
this.hotMesh.sub(topic, async (_topic, state) => {
this.hotMesh.unsub(topic);
if (state.data.done && !state.data?.$error) {
await complete(state.data?.response);
}
else if (state.data?.$error) {
return complete(null, state.data.$error);
}
else if (state.metadata.err) {
const error = JSON.parse(state.metadata.err);
return await complete(null, error);
}
});
//check state in case completed during wiring
const status = await this.hotMesh.getStatus(this.workflowId);
if (status <= 0) {
await complete();
}
});
}
}
exports.WorkflowHandleService = WorkflowHandleService;