@upstash/workflow
Version:
Durable, Reliable and Performant Serverless Functions
353 lines (349 loc) • 10.8 kB
JavaScript
import {
SDK_TELEMETRY,
StepTypes,
WORKFLOW_LABEL_HEADER,
WorkflowAbort,
WorkflowContext,
WorkflowError,
WorkflowLogger,
WorkflowNonRetryableError,
WorkflowTool,
getWorkflowRunId,
makeGetWaitersRequest,
makeNotifyRequest,
prepareFlowControl,
serve,
triggerFirstInvocation
} from "./chunk-LZGX3WMF.mjs";
// src/client/index.ts
import { Client as QStashClient } from "@upstash/qstash";
// src/client/dlq.ts
var DLQ = class _DLQ {
constructor(client) {
this.client = client;
}
/**
* list the items in the DLQ
*
* @param cursor - Optional cursor for pagination.
* @param count - Optional number of items to return.
* @param filter - Optional filter options to apply to the DLQ items.
* The available filter options are:
* - `fromDate`: Filter items which entered the DLQ after this date.
* - `toDate`: Filter items which entered the DLQ before this date.
* - `url`: Filter items by the URL they were sent to.
* - `responseStatus`: Filter items by the response status code.
* @returns
*/
async list(parameters) {
const { cursor, count, filter } = parameters || {};
return await this.client.http.request({
path: ["v2", "dlq"],
method: "GET",
query: {
cursor,
count,
...filter,
source: "workflow"
}
});
}
async resume(parameters) {
const { headers, queryParams } = _DLQ.handleDLQOptions(parameters);
const { workflowRuns } = await this.client.http.request({
path: ["v2", "workflows", "dlq", `resume?${queryParams}`],
headers,
method: "POST"
});
if (Array.isArray(parameters.dlqId)) {
return workflowRuns;
}
return workflowRuns[0];
}
async restart(parameters) {
const { headers, queryParams } = _DLQ.handleDLQOptions(parameters);
const { workflowRuns } = await this.client.http.request({
path: ["v2", "workflows", "dlq", `restart?${queryParams}`],
headers,
method: "POST"
});
if (Array.isArray(parameters.dlqId)) {
return workflowRuns;
}
return workflowRuns[0];
}
/**
* Retry the failure callback of a workflow run whose failureUrl/failureFunction
* request has failed.
*
* @param dlqId - The ID of the DLQ message to retry.
* @returns
*/
async retryFailureFunction({ dlqId }) {
const response = await this.client.http.request({
path: ["v2", "workflows", "dlq", "callback", dlqId],
method: "POST"
});
return response;
}
static handleDLQOptions(options) {
const { dlqId, flowControl, retries } = options;
const headers = {};
if (flowControl) {
const { flowControlKey, flowControlValue } = prepareFlowControl(flowControl);
headers["Upstash-Flow-Control-Key"] = flowControlKey;
headers["Upstash-Flow-Control-Value"] = flowControlValue;
}
if (retries !== void 0) {
headers["Upstash-Retries"] = retries.toString();
}
return {
queryParams: _DLQ.getDlqIdQueryParameter(dlqId),
headers
};
}
static getDlqIdQueryParameter(dlqId) {
const dlqIds = Array.isArray(dlqId) ? dlqId : [dlqId];
const paramsArray = dlqIds.map((id) => ["dlqIds", id]);
return new URLSearchParams(paramsArray).toString();
}
};
// src/client/index.ts
var Client = class {
client;
constructor(clientConfig) {
if (!clientConfig?.token) {
console.error(
"QStash token is required for Upstash Workflow!\n\nTo fix this:\n1. Get your token from the Upstash Console (https://console.upstash.com/qstash)\n2. Initialize the workflow client with:\n\n const client = new Client({\n token: '<YOUR_QSTASH_TOKEN>'\n });"
);
}
this.client = new QStashClient(clientConfig);
}
/**
* Cancel an ongoing workflow
*
* Returns true if workflow is canceled succesfully. Otherwise, throws error.
*
* There are multiple ways you can cancel workflows:
* - pass one or more workflow run ids to cancel them
* - pass a workflow url to cancel all runs starting with this url
* - cancel all pending or active workflow runs
*
* ### Cancel a set of workflow runs
*
* ```ts
* // cancel a single workflow
* await client.cancel({ ids: "<WORKFLOW_RUN_ID>" })
*
* // cancel a set of workflow runs
* await client.cancel({ ids: [
* "<WORKFLOW_RUN_ID_1>",
* "<WORKFLOW_RUN_ID_2>",
* ]})
* ```
*
* ### Cancel workflows starting with a url
*
* If you have an endpoint called `https://your-endpoint.com` and you
* want to cancel all workflow runs on it, you can use `urlStartingWith`.
*
* Note that this will cancel workflows in all endpoints under
* `https://your-endpoint.com`.
*
* ```ts
* await client.cancel({ urlStartingWith: "https://your-endpoint.com" })
* ```
*
* ### Cancel *all* workflows
*
* To cancel all pending and currently running workflows, you can
* do it like this:
*
* ```ts
* await client.cancel({ all: true })
* ```
*
* @param ids run id of the workflow to delete
* @param urlStartingWith cancel workflows starting with this url. Will be ignored
* if `ids` parameter is set.
* @param all set to true in order to cancel all workflows. Will be ignored
* if `ids` or `urlStartingWith` parameters are set.
* @returns true if workflow is succesfully deleted. Otherwise throws QStashError
*/
async cancel({
ids,
urlStartingWith,
all
}) {
let body;
if (ids) {
const runIdArray = typeof ids === "string" ? [ids] : ids;
body = JSON.stringify({ workflowRunIds: runIdArray });
} else if (urlStartingWith) {
body = JSON.stringify({ workflowUrl: urlStartingWith });
} else if (all) {
body = "{}";
} else {
throw new TypeError("The `cancel` method cannot be called without any options.");
}
const result = await this.client.http.request({
path: ["v2", "workflows", "runs"],
method: "DELETE",
body,
headers: {
"Content-Type": "application/json"
}
});
return result;
}
/**
* Notify a workflow run waiting for an event
*
* ```ts
* import { Client } from "@upstash/workflow";
*
* const client = new Client({ token: "<QSTASH_TOKEN>" })
* await client.notify({
* eventId: "my-event-id",
* eventData: "my-data" // data passed to the workflow run
* });
* ```
*
* @param eventId event id to notify
* @param eventData data to provide to the workflow
*/
async notify({
eventId,
eventData
}) {
return await makeNotifyRequest(this.client.http, eventId, eventData);
}
/**
* Check waiters of an event
*
* ```ts
* import { Client } from "@upstash/workflow";
*
* const client = new Client({ token: "<QSTASH_TOKEN>" })
* const result = await client.getWaiters({
* eventId: "my-event-id"
* })
* ```
*
* @param eventId event id to check
*/
async getWaiters({ eventId }) {
return await makeGetWaitersRequest(this.client.http, eventId);
}
async trigger(params) {
const isBatchInput = Array.isArray(params);
const options = isBatchInput ? params : [params];
const invocations = options.map((option) => {
const failureUrl = option.useFailureFunction ? option.url : option.failureUrl;
const finalWorkflowRunId = getWorkflowRunId(option.workflowRunId);
const context = new WorkflowContext({
qstashClient: this.client,
// @ts-expect-error header type mismatch because of bun
headers: new Headers({
...option.headers ?? {},
...option.label ? { [WORKFLOW_LABEL_HEADER]: option.label } : {}
}),
initialPayload: option.body,
steps: [],
url: option.url,
workflowRunId: finalWorkflowRunId,
retries: option.retries,
retryDelay: option.retryDelay,
telemetry: { sdk: SDK_TELEMETRY },
flowControl: option.flowControl,
failureUrl,
label: option.label
});
return {
workflowContext: context,
telemetry: { sdk: SDK_TELEMETRY },
delay: option.delay,
notBefore: option.notBefore
};
});
const result = await triggerFirstInvocation(invocations);
const workflowRunIds = invocations.map(
(invocation) => invocation.workflowContext.workflowRunId
);
if (result.isOk()) {
return isBatchInput ? workflowRunIds.map((id) => ({ workflowRunId: id })) : { workflowRunId: workflowRunIds[0] };
} else {
throw result.error;
}
}
/**
* Fetches logs for workflow runs.
*
* @param workflowRunId - The ID of the workflow run to fetch logs for.
* @param cursor - The cursor for pagination.
* @param count - Number of runs to fetch. Default value is 10.
* @param state - The state of the workflow run.
* @param workflowUrl - The URL of the workflow. Should be an exact match.
* @param workflowCreatedAt - The creation time of the workflow. If you have two workflow runs with the same URL, you can use this to filter them.
* @returns A promise that resolves to either a `WorkflowRunLog` or a `WorkflowRunResponse`.
*
* @example
* Fetch logs for a specific workflow run:
* ```typescript
* const { runs } = await client.logs({ workflowRunId: '12345' });
* const steps = runs[0].steps; // access steps
* ```
*
* @example
* Fetch logs with pagination:
* ```typescript
* const { runs, cursor } = await client.logs();
* const steps = runs[0].steps // access steps
*
* const { runs: nextRuns, cursor: nextCursor } = await client.logs({ cursor, count: 2 });
* ```
*/
async logs(params) {
const { workflowRunId, cursor, count, state, workflowUrl, workflowCreatedAt } = params ?? {};
const urlParams = new URLSearchParams({ groupBy: "workflowRunId" });
if (workflowRunId) {
urlParams.append("workflowRunId", workflowRunId);
}
if (cursor) {
urlParams.append("cursor", cursor);
}
if (count) {
urlParams.append("count", count.toString());
}
if (state) {
urlParams.append("state", state);
}
if (workflowUrl) {
urlParams.append("workflowUrl", workflowUrl);
}
if (workflowCreatedAt) {
urlParams.append("workflowCreatedAt", workflowCreatedAt.toString());
}
if (params?.label) {
urlParams.append("label", params.label);
}
const result = await this.client.http.request({
path: ["v2", "workflows", `events?${urlParams.toString()}`]
});
return result;
}
get dlq() {
return new DLQ(this.client);
}
};
export {
Client,
StepTypes,
WorkflowAbort,
WorkflowContext,
WorkflowError,
WorkflowLogger,
WorkflowNonRetryableError,
WorkflowTool,
serve
};