UNPKG

@upstash/workflow

Version:

Durable, Reliable and Performant Serverless Functions

353 lines (349 loc) 10.8 kB
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 };