UNPKG

@upstash/qstash

Version:

Official Typescript client for QStash

1,574 lines (1,550 loc) 90.1 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // platforms/svelte.ts var svelte_exports = {}; __export(svelte_exports, { serve: () => serve2, verifySignatureSvelte: () => verifySignatureSvelte }); module.exports = __toCommonJS(svelte_exports); // src/receiver.ts var jose = __toESM(require("jose")); var import_crypto_js = __toESM(require("crypto-js")); var SignatureError = class extends Error { constructor(message) { super(message); this.name = "SignatureError"; } }; var Receiver = class { currentSigningKey; nextSigningKey; constructor(config) { this.currentSigningKey = config.currentSigningKey; this.nextSigningKey = config.nextSigningKey; } /** * Verify the signature of a request. * * Tries to verify the signature with the current signing key. * If that fails, maybe because you have rotated the keys recently, it will * try to verify the signature with the next signing key. * * If that fails, the signature is invalid and a `SignatureError` is thrown. */ async verify(request) { let payload; try { payload = await this.verifyWithKey(this.currentSigningKey, request); } catch { payload = await this.verifyWithKey(this.nextSigningKey, request); } this.verifyBodyAndUrl(payload, request); return true; } /** * Verify signature with a specific signing key */ async verifyWithKey(key, request) { const jwt = await jose.jwtVerify(request.signature, new TextEncoder().encode(key), { issuer: "Upstash", clockTolerance: request.clockTolerance }).catch((error) => { throw new SignatureError(error.message); }); return jwt.payload; } verifyBodyAndUrl(payload, request) { const p = payload; if (request.url !== void 0 && p.sub !== request.url) { throw new SignatureError(`invalid subject: ${p.sub}, want: ${request.url}`); } const bodyHash = import_crypto_js.default.SHA256(request.body).toString(import_crypto_js.default.enc.Base64url); const padding = new RegExp(/=+$/); if (p.body.replace(padding, "") !== bodyHash.replace(padding, "")) { throw new SignatureError(`body hash does not match, want: ${p.body}, got: ${bodyHash}`); } } }; // src/client/dlq.ts var DLQ = class { http; constructor(http) { this.http = http; } /** * List messages in the dlq */ async listMessages(options) { const filterPayload = { ...options?.filter, topicName: options?.filter?.urlGroup }; const messagesPayload = await this.http.request({ method: "GET", path: ["v2", "dlq"], query: { cursor: options?.cursor, count: options?.count, ...filterPayload } }); return { messages: messagesPayload.messages.map((message) => { return { ...message, urlGroup: message.topicName, ratePerSecond: "rate" in message ? message.rate : void 0 }; }), cursor: messagesPayload.cursor }; } /** * Remove a message from the dlq using it's `dlqId` */ async delete(dlqMessageId) { return await this.http.request({ method: "DELETE", path: ["v2", "dlq", dlqMessageId], parseResponseAsJson: false // there is no response }); } /** * Remove multiple messages from the dlq using their `dlqId`s */ async deleteMany(request) { return await this.http.request({ method: "DELETE", path: ["v2", "dlq"], headers: { "Content-Type": "application/json" }, body: JSON.stringify({ dlqIds: request.dlqIds }) }); } }; // src/client/error.ts var RATELIMIT_STATUS = 429; var QstashError = class extends Error { status; constructor(message, status) { super(message); this.name = "QstashError"; this.status = status; } }; var QstashRatelimitError = class extends QstashError { limit; remaining; reset; constructor(args) { super(`Exceeded burst rate limit. ${JSON.stringify(args)}`, RATELIMIT_STATUS); this.name = "QstashRatelimitError"; this.limit = args.limit; this.remaining = args.remaining; this.reset = args.reset; } }; var QstashChatRatelimitError = class extends QstashError { limitRequests; limitTokens; remainingRequests; remainingTokens; resetRequests; resetTokens; constructor(args) { super(`Exceeded chat rate limit. ${JSON.stringify(args)}`, RATELIMIT_STATUS); this.name = "QstashChatRatelimitError"; this.limitRequests = args["limit-requests"]; this.limitTokens = args["limit-tokens"]; this.remainingRequests = args["remaining-requests"]; this.remainingTokens = args["remaining-tokens"]; this.resetRequests = args["reset-requests"]; this.resetTokens = args["reset-tokens"]; } }; var QstashDailyRatelimitError = class extends QstashError { limit; remaining; reset; constructor(args) { super(`Exceeded daily rate limit. ${JSON.stringify(args)}`, RATELIMIT_STATUS); this.name = "QstashDailyRatelimitError"; this.limit = args.limit; this.remaining = args.remaining; this.reset = args.reset; } }; var QStashWorkflowError = class extends QstashError { constructor(message) { super(message); this.name = "QStashWorkflowError"; } }; var QStashWorkflowAbort = class extends Error { stepInfo; stepName; constructor(stepName, stepInfo) { super( `This is an Upstash Workflow error thrown after a step executes. It is expected to be raised. Make sure that you await for each step. Also, if you are using try/catch blocks, you should not wrap context.run/sleep/sleepUntil/call methods with try/catch. Aborting workflow after executing step '${stepName}'.` ); this.name = "QStashWorkflowAbort"; this.stepName = stepName; this.stepInfo = stepInfo; } }; var formatWorkflowError = (error) => { return error instanceof Error ? { error: error.name, message: error.message } : { error: "Error", message: "An error occured while executing workflow." }; }; // src/client/http.ts var HttpClient = class { baseUrl; authorization; options; retry; headers; telemetryHeaders; constructor(config) { this.baseUrl = config.baseUrl.replace(/\/$/, ""); this.authorization = config.authorization; this.retry = // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition typeof config.retry === "boolean" && !config.retry ? { attempts: 1, backoff: () => 0 } : { attempts: config.retry?.retries ?? 5, backoff: config.retry?.backoff ?? ((retryCount) => Math.exp(retryCount) * 50) }; this.headers = config.headers; this.telemetryHeaders = config.telemetryHeaders; } async request(request) { const { response } = await this.requestWithBackoff(request); if (request.parseResponseAsJson === false) { return void 0; } return await response.json(); } async *requestStream(request) { const { response } = await this.requestWithBackoff(request); if (!response.body) { throw new Error("No response body"); } const body = response.body; const reader = body.getReader(); const decoder = new TextDecoder(); try { while (true) { const { done, value } = await reader.read(); if (done) { break; } const chunkText = decoder.decode(value, { stream: true }); const chunks = chunkText.split("\n").filter(Boolean); for (const chunk of chunks) { if (chunk.startsWith("data: ")) { const data = chunk.slice(6); if (data === "[DONE]") { break; } yield JSON.parse(data); } } } } finally { await reader.cancel(); } } requestWithBackoff = async (request) => { const [url, requestOptions] = this.processRequest(request); let response = void 0; let error = void 0; for (let index = 0; index <= this.retry.attempts; index++) { try { response = await fetch(url.toString(), requestOptions); break; } catch (error_) { error = error_; if (index < this.retry.attempts) { await new Promise((r) => setTimeout(r, this.retry.backoff(index))); } } } if (!response) { throw error ?? new Error("Exhausted all retries"); } await this.checkResponse(response); return { response, error }; }; processRequest = (request) => { const headers = new Headers(request.headers); if (!headers.has("Authorization")) { headers.set("Authorization", this.authorization); } const requestOptions = { method: request.method, headers, body: request.body, keepalive: request.keepalive }; const url = new URL([request.baseUrl ?? this.baseUrl, ...request.path].join("/")); if (request.query) { for (const [key, value] of Object.entries(request.query)) { if (value !== void 0) { url.searchParams.set(key, value.toString()); } } } return [url.toString(), requestOptions]; }; async checkResponse(response) { if (response.status === 429) { if (response.headers.get("x-ratelimit-limit-requests")) { throw new QstashChatRatelimitError({ "limit-requests": response.headers.get("x-ratelimit-limit-requests"), "limit-tokens": response.headers.get("x-ratelimit-limit-tokens"), "remaining-requests": response.headers.get("x-ratelimit-remaining-requests"), "remaining-tokens": response.headers.get("x-ratelimit-remaining-tokens"), "reset-requests": response.headers.get("x-ratelimit-reset-requests"), "reset-tokens": response.headers.get("x-ratelimit-reset-tokens") }); } else if (response.headers.get("RateLimit-Limit")) { throw new QstashDailyRatelimitError({ limit: response.headers.get("RateLimit-Limit"), remaining: response.headers.get("RateLimit-Remaining"), reset: response.headers.get("RateLimit-Reset") }); } throw new QstashRatelimitError({ limit: response.headers.get("Burst-RateLimit-Limit"), remaining: response.headers.get("Burst-RateLimit-Remaining"), reset: response.headers.get("Burst-RateLimit-Reset") }); } if (response.status < 200 || response.status >= 300) { const body = await response.text(); throw new QstashError( body.length > 0 ? body : `Error: status=${response.status}`, response.status ); } } }; // src/client/llm/providers.ts var setupAnalytics = (analytics, providerApiKey, providerBaseUrl, provider) => { if (!analytics) return {}; switch (analytics.name) { case "helicone": { switch (provider) { case "upstash": { return { baseURL: "https://qstash.helicone.ai/llm/v1/chat/completions", defaultHeaders: { "Helicone-Auth": `Bearer ${analytics.token}`, Authorization: `Bearer ${providerApiKey}` } }; } default: { return { baseURL: "https://gateway.helicone.ai/v1/chat/completions", defaultHeaders: { "Helicone-Auth": `Bearer ${analytics.token}`, "Helicone-Target-Url": providerBaseUrl, Authorization: `Bearer ${providerApiKey}` } }; } } } default: { throw new Error("Unknown analytics provider"); } } }; // src/client/llm/chat.ts var Chat = class _Chat { http; token; constructor(http, token) { this.http = http; this.token = token; } static toChatRequest(request) { const messages = []; messages.push( { role: "system", content: request.system }, { role: "user", content: request.user } ); const chatRequest = { ...request, messages }; return chatRequest; } /** * Calls the Upstash completions api given a ChatRequest. * * Returns a ChatCompletion or a stream of ChatCompletionChunks * if stream is enabled. * * @param request ChatRequest with messages * @returns Chat completion or stream */ create = async (request) => { if (request.provider.owner != "upstash") return this.createThirdParty(request); const body = JSON.stringify(request); let baseUrl = void 0; let headers = { "Content-Type": "application/json", Authorization: `Bearer ${this.token}`, ..."stream" in request && request.stream ? { Connection: "keep-alive", Accept: "text/event-stream", "Cache-Control": "no-cache" } : {} }; if (request.analytics) { const { baseURL, defaultHeaders } = setupAnalytics( { name: "helicone", token: request.analytics.token }, this.getAuthorizationToken(), request.provider.baseUrl, "upstash" ); headers = { ...headers, ...defaultHeaders }; baseUrl = baseURL; } const path = request.analytics ? [] : ["llm", "v1", "chat", "completions"]; return "stream" in request && request.stream ? this.http.requestStream({ path, method: "POST", headers, baseUrl, body }) : this.http.request({ path, method: "POST", headers, baseUrl, body }); }; /** * Calls the Upstash completions api given a ChatRequest. * * Returns a ChatCompletion or a stream of ChatCompletionChunks * if stream is enabled. * * @param request ChatRequest with messages * @returns Chat completion or stream */ createThirdParty = async (request) => { const { baseUrl, token, owner, organization } = request.provider; if (owner === "upstash") throw new Error("Upstash is not 3rd party provider!"); delete request.provider; delete request.system; const analytics = request.analytics; delete request.analytics; const body = JSON.stringify(request); const isAnalyticsEnabled = analytics?.name && analytics.token; const analyticsConfig = analytics?.name && analytics.token ? setupAnalytics({ name: analytics.name, token: analytics.token }, token, baseUrl, owner) : { defaultHeaders: void 0, baseURL: baseUrl }; const isStream = "stream" in request && request.stream; const headers = { "Content-Type": "application/json", Authorization: `Bearer ${token}`, ...organization ? { "OpenAI-Organization": organization } : {}, ...isStream ? { Connection: "keep-alive", Accept: "text/event-stream", "Cache-Control": "no-cache" } : {}, ...analyticsConfig.defaultHeaders }; const response = await this.http[isStream ? "requestStream" : "request"]({ path: isAnalyticsEnabled ? [] : ["v1", "chat", "completions"], method: "POST", headers, body, baseUrl: analyticsConfig.baseURL }); return response; }; // Helper method to get the authorization token getAuthorizationToken() { const authHeader = String(this.http.authorization); const match = /Bearer (.+)/.exec(authHeader); if (!match) { throw new Error("Invalid authorization header format"); } return match[1]; } /** * Calls the Upstash completions api given a PromptRequest. * * Returns a ChatCompletion or a stream of ChatCompletionChunks * if stream is enabled. * * @param request PromptRequest with system and user messages. * Note that system parameter shouldn't be passed in the case of * mistralai/Mistral-7B-Instruct-v0.2 model. * @returns Chat completion or stream */ prompt = async (request) => { const chatRequest = _Chat.toChatRequest(request); return this.create(chatRequest); }; }; // src/client/messages.ts var Messages = class { http; constructor(http) { this.http = http; } /** * Get a message */ async get(messageId) { const messagePayload = await this.http.request({ method: "GET", path: ["v2", "messages", messageId] }); const message = { ...messagePayload, urlGroup: messagePayload.topicName, ratePerSecond: "rate" in messagePayload ? messagePayload.rate : void 0 }; return message; } /** * Cancel a message */ async delete(messageId) { return await this.http.request({ method: "DELETE", path: ["v2", "messages", messageId], parseResponseAsJson: false }); } async deleteMany(messageIds) { const result = await this.http.request({ method: "DELETE", path: ["v2", "messages"], headers: { "Content-Type": "application/json" }, body: JSON.stringify({ messageIds }) }); return result.cancelled; } async deleteAll() { const result = await this.http.request({ method: "DELETE", path: ["v2", "messages"] }); return result.cancelled; } }; // src/client/api/base.ts var BaseProvider = class { baseUrl; token; owner; constructor(baseUrl, token, owner) { this.baseUrl = baseUrl; this.token = token; this.owner = owner; } getUrl() { return `${this.baseUrl}/${this.getRoute().join("/")}`; } }; // src/client/api/llm.ts var LLMProvider = class extends BaseProvider { apiKind = "llm"; organization; method = "POST"; constructor(baseUrl, token, owner, organization) { super(baseUrl, token, owner); this.organization = organization; } getRoute() { return this.owner === "anthropic" ? ["v1", "messages"] : ["v1", "chat", "completions"]; } getHeaders(options) { if (this.owner === "upstash" && !options.analytics) { return { "content-type": "application/json" }; } const header = this.owner === "anthropic" ? "x-api-key" : "authorization"; const headerValue = this.owner === "anthropic" ? this.token : `Bearer ${this.token}`; const headers = { [header]: headerValue, "content-type": "application/json" }; if (this.owner === "openai" && this.organization) { headers["OpenAI-Organization"] = this.organization; } if (this.owner === "anthropic") { headers["anthropic-version"] = "2023-06-01"; } return headers; } /** * Checks if callback exists and adds analytics in place if it's set. * * @param request * @param options */ onFinish(providerInfo, options) { if (options.analytics) { return updateWithAnalytics(providerInfo, options.analytics); } return providerInfo; } }; var upstash = () => { return new LLMProvider("https://qstash.upstash.io/llm", "", "upstash"); }; // src/client/api/utils.ts var getProviderInfo = (api, upstashToken) => { const { name, provider, ...parameters } = api; const finalProvider = provider ?? upstash(); if (finalProvider.owner === "upstash" && !finalProvider.token) { finalProvider.token = upstashToken; } if (!finalProvider.baseUrl) throw new TypeError("baseUrl cannot be empty or undefined!"); if (!finalProvider.token) throw new TypeError("token cannot be empty or undefined!"); if (finalProvider.apiKind !== name) { throw new TypeError( `Unexpected api name. Expected '${finalProvider.apiKind}', received ${name}` ); } const providerInfo = { url: finalProvider.getUrl(), baseUrl: finalProvider.baseUrl, route: finalProvider.getRoute(), appendHeaders: finalProvider.getHeaders(parameters), owner: finalProvider.owner, method: finalProvider.method }; return finalProvider.onFinish(providerInfo, parameters); }; var safeJoinHeaders = (headers, record) => { const joinedHeaders = new Headers(record); for (const [header, value] of headers.entries()) { joinedHeaders.set(header, value); } return joinedHeaders; }; var processApi = (request, headers, upstashToken) => { if (!request.api) { request.headers = headers; return request; } const { url, appendHeaders, owner, method } = getProviderInfo(request.api, upstashToken); if (request.api.name === "llm") { const callback = request.callback; if (!callback) { throw new TypeError("Callback cannot be undefined when using LLM api."); } return { ...request, method: request.method ?? method, headers: safeJoinHeaders(headers, appendHeaders), ...owner === "upstash" && !request.api.analytics ? { api: { name: "llm" }, url: void 0, callback } : { url, api: void 0 } }; } else { return { ...request, method: request.method ?? method, headers: safeJoinHeaders(headers, appendHeaders), url, api: void 0 }; } }; function updateWithAnalytics(providerInfo, analytics) { switch (analytics.name) { case "helicone": { providerInfo.appendHeaders["Helicone-Auth"] = `Bearer ${analytics.token}`; if (providerInfo.owner === "upstash") { updateProviderInfo(providerInfo, "https://qstash.helicone.ai", [ "llm", ...providerInfo.route ]); } else { providerInfo.appendHeaders["Helicone-Target-Url"] = providerInfo.baseUrl; updateProviderInfo(providerInfo, "https://gateway.helicone.ai", providerInfo.route); } return providerInfo; } default: { throw new Error("Unknown analytics provider"); } } } function updateProviderInfo(providerInfo, baseUrl, route) { providerInfo.baseUrl = baseUrl; providerInfo.route = route; providerInfo.url = `${baseUrl}/${route.join("/")}`; } // src/client/utils.ts var isIgnoredHeader = (header) => { const lowerCaseHeader = header.toLowerCase(); return lowerCaseHeader.startsWith("content-type") || lowerCaseHeader.startsWith("upstash-"); }; function prefixHeaders(headers) { const keysToBePrefixed = [...headers.keys()].filter((key) => !isIgnoredHeader(key)); for (const key of keysToBePrefixed) { const value = headers.get(key); if (value !== null) { headers.set(`Upstash-Forward-${key}`, value); } headers.delete(key); } return headers; } function wrapWithGlobalHeaders(headers, globalHeaders, telemetryHeaders) { if (!globalHeaders) { return headers; } const finalHeaders = new Headers(globalHeaders); headers.forEach((value, key) => { finalHeaders.set(key, value); }); telemetryHeaders?.forEach((value, key) => { if (!value) return; finalHeaders.append(key, value); }); return finalHeaders; } function processHeaders(request) { const headers = prefixHeaders(new Headers(request.headers)); headers.set("Upstash-Method", request.method ?? "POST"); if (request.delay !== void 0) { if (typeof request.delay === "string") { headers.set("Upstash-Delay", request.delay); } else { headers.set("Upstash-Delay", `${request.delay.toFixed(0)}s`); } } if (request.notBefore !== void 0) { headers.set("Upstash-Not-Before", request.notBefore.toFixed(0)); } if (request.deduplicationId !== void 0) { headers.set("Upstash-Deduplication-Id", request.deduplicationId); } if (request.contentBasedDeduplication) { headers.set("Upstash-Content-Based-Deduplication", "true"); } if (request.retries !== void 0) { headers.set("Upstash-Retries", request.retries.toFixed(0)); } if (request.retryDelay !== void 0) { headers.set("Upstash-Retry-Delay", request.retryDelay); } if (request.callback !== void 0) { headers.set("Upstash-Callback", request.callback); } if (request.failureCallback !== void 0) { headers.set("Upstash-Failure-Callback", request.failureCallback); } if (request.timeout !== void 0) { if (typeof request.timeout === "string") { headers.set("Upstash-Timeout", request.timeout); } else { headers.set("Upstash-Timeout", `${request.timeout}s`); } } if (request.flowControl?.key) { const parallelism = request.flowControl.parallelism?.toString(); const rate = (request.flowControl.rate ?? request.flowControl.ratePerSecond)?.toString(); const period = typeof request.flowControl.period === "number" ? `${request.flowControl.period}s` : request.flowControl.period; const controlValue = [ parallelism ? `parallelism=${parallelism}` : void 0, rate ? `rate=${rate}` : void 0, period ? `period=${period}` : void 0 ].filter(Boolean); if (controlValue.length === 0) { throw new QstashError("Provide at least one of parallelism or ratePerSecond for flowControl"); } headers.set("Upstash-Flow-Control-Key", request.flowControl.key); headers.set("Upstash-Flow-Control-Value", controlValue.join(", ")); } if (request.label !== void 0) { headers.set("Upstash-Label", request.label); } return headers; } function getRequestPath(request) { const nonApiPath = request.url ?? request.urlGroup ?? request.topic; if (nonApiPath) return nonApiPath; if (request.api?.name === "llm") return `api/llm`; if (request.api?.name === "email") { const providerInfo = getProviderInfo(request.api, "not-needed"); return providerInfo.baseUrl; } throw new QstashError(`Failed to infer request path for ${JSON.stringify(request)}`); } var NANOID_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"; var NANOID_LENGTH = 21; function nanoid() { return [...crypto.getRandomValues(new Uint8Array(NANOID_LENGTH))].map((x) => NANOID_CHARS[x % NANOID_CHARS.length]).join(""); } function decodeBase64(base64) { try { const binString = atob(base64); const intArray = Uint8Array.from(binString, (m) => m.codePointAt(0)); return new TextDecoder().decode(intArray); } catch (error) { try { const result = atob(base64); console.warn( `Upstash QStash: Failed while decoding base64 "${base64}". Decoding with atob and returning it instead. ${error}` ); return result; } catch (error2) { console.warn( `Upstash QStash: Failed to decode base64 "${base64}" with atob. Returning it as it is. ${error2}` ); return base64; } } } function getRuntime() { if (typeof process === "object" && typeof process.versions == "object" && process.versions.bun) return `bun@${process.versions.bun}`; if (typeof EdgeRuntime === "string") return "edge-light"; else if (typeof process === "object" && typeof process.version === "string") return `node@${process.version}`; return ""; } // src/client/queue.ts var Queue = class { http; queueName; constructor(http, queueName) { this.http = http; this.queueName = queueName; } /** * Create or update the queue */ async upsert(request) { if (!this.queueName) { throw new Error("Please provide a queue name to the Queue constructor"); } const body = { queueName: this.queueName, parallelism: request.parallelism ?? 1, paused: request.paused ?? false }; await this.http.request({ method: "POST", path: ["v2", "queues"], headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), parseResponseAsJson: false }); } /** * Get the queue details */ async get() { if (!this.queueName) { throw new Error("Please provide a queue name to the Queue constructor"); } return await this.http.request({ method: "GET", path: ["v2", "queues", this.queueName] }); } /** * List queues */ async list() { return await this.http.request({ method: "GET", path: ["v2", "queues"] }); } /** * Delete the queue */ async delete() { if (!this.queueName) { throw new Error("Please provide a queue name to the Queue constructor"); } await this.http.request({ method: "DELETE", path: ["v2", "queues", this.queueName], parseResponseAsJson: false }); } /** * Enqueue a message to a queue. */ async enqueue(request) { if (!this.queueName) { throw new Error("Please provide a queue name to the Queue constructor"); } const headers = wrapWithGlobalHeaders( processHeaders(request), this.http.headers, this.http.telemetryHeaders ); const destination = getRequestPath(request); const response = await this.http.request({ path: ["v2", "enqueue", this.queueName, destination], body: request.body, headers, method: "POST" }); return response; } /** * Enqueue a message to a queue, serializing the body to JSON. */ async enqueueJSON(request) { const headers = prefixHeaders(new Headers(request.headers)); headers.set("Content-Type", "application/json"); const upstashToken = String(this.http.authorization).split("Bearer ")[1]; const nonApiRequest = processApi(request, headers, upstashToken); const response = await this.enqueue({ ...nonApiRequest, body: JSON.stringify(nonApiRequest.body) }); return response; } /** * Pauses the queue. * * A paused queue will not deliver messages until * it is resumed. */ async pause() { if (!this.queueName) { throw new Error("Please provide a queue name to the Queue constructor"); } await this.http.request({ method: "POST", path: ["v2", "queues", this.queueName, "pause"], parseResponseAsJson: false }); } /** * Resumes the queue. */ async resume() { if (!this.queueName) { throw new Error("Please provide a queue name to the Queue constructor"); } await this.http.request({ method: "POST", path: ["v2", "queues", this.queueName, "resume"], parseResponseAsJson: false }); } }; // src/client/schedules.ts var Schedules = class { http; constructor(http) { this.http = http; } /** * Create a schedule */ async create(request) { const headers = prefixHeaders(new Headers(request.headers)); if (!headers.has("Content-Type")) { headers.set("Content-Type", "application/json"); } headers.set("Upstash-Cron", request.cron); if (request.method !== void 0) { headers.set("Upstash-Method", request.method); } if (request.delay !== void 0) { if (typeof request.delay === "string") { headers.set("Upstash-Delay", request.delay); } else { headers.set("Upstash-Delay", `${request.delay.toFixed(0)}s`); } } if (request.retries !== void 0) { headers.set("Upstash-Retries", request.retries.toFixed(0)); } if (request.retryDelay !== void 0) { headers.set("Upstash-Retry-Delay", request.retryDelay); } if (request.callback !== void 0) { headers.set("Upstash-Callback", request.callback); } if (request.failureCallback !== void 0) { headers.set("Upstash-Failure-Callback", request.failureCallback); } if (request.timeout !== void 0) { if (typeof request.timeout === "string") { headers.set("Upstash-Timeout", request.timeout); } else { headers.set("Upstash-Timeout", `${request.timeout}s`); } } if (request.scheduleId !== void 0) { headers.set("Upstash-Schedule-Id", request.scheduleId); } if (request.queueName !== void 0) { headers.set("Upstash-Queue-Name", request.queueName); } if (request.flowControl?.key) { const parallelism = request.flowControl.parallelism?.toString(); const rate = (request.flowControl.rate ?? request.flowControl.ratePerSecond)?.toString(); const period = typeof request.flowControl.period === "number" ? `${request.flowControl.period}s` : request.flowControl.period; const controlValue = [ parallelism ? `parallelism=${parallelism}` : void 0, rate ? `rate=${rate}` : void 0, period ? `period=${period}` : void 0 ].filter(Boolean); if (controlValue.length === 0) { throw new QstashError( "Provide at least one of parallelism or ratePerSecond for flowControl" ); } headers.set("Upstash-Flow-Control-Key", request.flowControl.key); headers.set("Upstash-Flow-Control-Value", controlValue.join(", ")); } if (request.label !== void 0) { headers.set("Upstash-Label", request.label); } return await this.http.request({ method: "POST", headers: wrapWithGlobalHeaders(headers, this.http.headers, this.http.telemetryHeaders), path: ["v2", "schedules", request.destination], body: request.body }); } /** * Get a schedule */ async get(scheduleId) { const schedule = await this.http.request({ method: "GET", path: ["v2", "schedules", scheduleId] }); if ("rate" in schedule) schedule.ratePerSecond = schedule.rate; return schedule; } /** * List your schedules */ async list() { const schedules = await this.http.request({ method: "GET", path: ["v2", "schedules"] }); for (const schedule of schedules) { if ("rate" in schedule) schedule.ratePerSecond = schedule.rate; } return schedules; } /** * Delete a schedule */ async delete(scheduleId) { return await this.http.request({ method: "DELETE", path: ["v2", "schedules", scheduleId], parseResponseAsJson: false }); } /** * Pauses the schedule. * * A paused schedule will not deliver messages until * it is resumed. */ async pause({ schedule }) { await this.http.request({ method: "PATCH", path: ["v2", "schedules", schedule, "pause"], parseResponseAsJson: false }); } /** * Resumes the schedule. */ async resume({ schedule }) { await this.http.request({ method: "PATCH", path: ["v2", "schedules", schedule, "resume"], parseResponseAsJson: false }); } }; // src/client/url-groups.ts var UrlGroups = class { http; constructor(http) { this.http = http; } /** * Create a new url group with the given name and endpoints */ async addEndpoints(request) { await this.http.request({ method: "POST", path: ["v2", "topics", request.name, "endpoints"], headers: { "Content-Type": "application/json" }, body: JSON.stringify({ endpoints: request.endpoints }), parseResponseAsJson: false }); } /** * Remove endpoints from a url group. */ async removeEndpoints(request) { await this.http.request({ method: "DELETE", path: ["v2", "topics", request.name, "endpoints"], headers: { "Content-Type": "application/json" }, body: JSON.stringify({ endpoints: request.endpoints }), parseResponseAsJson: false }); } /** * Get a list of all url groups. */ async list() { return await this.http.request({ method: "GET", path: ["v2", "topics"] }); } /** * Get a single url group */ async get(name) { return await this.http.request({ method: "GET", path: ["v2", "topics", name] }); } /** * Delete a url group */ async delete(name) { return await this.http.request({ method: "DELETE", path: ["v2", "topics", name], parseResponseAsJson: false }); } }; // src/client/workflow/constants.ts var WORKFLOW_ID_HEADER = "Upstash-Workflow-RunId"; var WORKFLOW_INIT_HEADER = "Upstash-Workflow-Init"; var WORKFLOW_URL_HEADER = "Upstash-Workflow-Url"; var WORKFLOW_FAILURE_HEADER = "Upstash-Workflow-Is-Failure"; var WORKFLOW_PROTOCOL_VERSION = "1"; var WORKFLOW_PROTOCOL_VERSION_HEADER = "Upstash-Workflow-Sdk-Version"; var DEFAULT_CONTENT_TYPE = "application/json"; var NO_CONCURRENCY = 1; var DEFAULT_RETRIES = 3; // src/client/workflow/context.ts var import_neverthrow2 = require("neverthrow"); // src/client/workflow/workflow-requests.ts var import_neverthrow = require("neverthrow"); // src/client/workflow/types.ts var StepTypes = ["Initial", "Run", "SleepFor", "SleepUntil", "Call"]; // src/client/workflow/workflow-requests.ts var triggerFirstInvocation = async (workflowContext, retries, debug) => { const headers = getHeaders( "true", workflowContext.workflowRunId, workflowContext.url, workflowContext.headers, void 0, workflowContext.failureUrl, retries ); await debug?.log("SUBMIT", "SUBMIT_FIRST_INVOCATION", { headers, requestPayload: workflowContext.requestPayload, url: workflowContext.url }); try { await workflowContext.qstashClient.publishJSON({ headers, method: "POST", body: workflowContext.requestPayload, url: workflowContext.url }); return (0, import_neverthrow.ok)("success"); } catch (error) { const error_ = error; return (0, import_neverthrow.err)(error_); } }; var triggerRouteFunction = async ({ onCleanup, onStep }) => { try { await onStep(); await onCleanup(); return (0, import_neverthrow.ok)("workflow-finished"); } catch (error) { const error_ = error; return error_ instanceof QStashWorkflowAbort ? (0, import_neverthrow.ok)("step-finished") : (0, import_neverthrow.err)(error_); } }; var triggerWorkflowDelete = async (workflowContext, debug, cancel = false) => { await debug?.log("SUBMIT", "SUBMIT_CLEANUP", { deletedWorkflowRunId: workflowContext.workflowRunId }); const result = await workflowContext.qstashClient.http.request({ path: ["v2", "workflows", "runs", `${workflowContext.workflowRunId}?cancel=${cancel}`], method: "DELETE", parseResponseAsJson: false }); await debug?.log("SUBMIT", "SUBMIT_CLEANUP", result); }; var recreateUserHeaders = (headers) => { const filteredHeaders = new Headers(); const pairs = headers.entries(); for (const [header, value] of pairs) { const headerLowerCase = header.toLowerCase(); if (!headerLowerCase.startsWith("upstash-workflow-") && !headerLowerCase.startsWith("x-vercel-") && !headerLowerCase.startsWith("x-forwarded-") && headerLowerCase !== "cf-connecting-ip") { filteredHeaders.append(header, value); } } return filteredHeaders; }; var handleThirdPartyCallResult = async (request, requestPayload, client, workflowUrl, failureUrl, retries, debug) => { try { if (request.headers.get("Upstash-Workflow-Callback")) { const callbackMessage = JSON.parse(requestPayload); if (!(callbackMessage.status >= 200 && callbackMessage.status < 300)) { await debug?.log("WARN", "SUBMIT_THIRD_PARTY_RESULT", { status: callbackMessage.status, body: atob(callbackMessage.body) }); console.warn( `Workflow Warning: "context.call" failed with status ${callbackMessage.status} and will retry (if there are retries remaining). Error Message: ${atob(callbackMessage.body)}` ); return (0, import_neverthrow.ok)("call-will-retry"); } const workflowRunId = request.headers.get(WORKFLOW_ID_HEADER); const stepIdString = request.headers.get("Upstash-Workflow-StepId"); const stepName = request.headers.get("Upstash-Workflow-StepName"); const stepType = request.headers.get("Upstash-Workflow-StepType"); const concurrentString = request.headers.get("Upstash-Workflow-Concurrent"); const contentType = request.headers.get("Upstash-Workflow-ContentType"); if (!(workflowRunId && stepIdString && stepName && StepTypes.includes(stepType) && concurrentString && contentType)) { throw new Error( `Missing info in callback message source header: ${JSON.stringify({ workflowRunId, stepIdString, stepName, stepType, concurrentString, contentType })}` ); } const userHeaders = recreateUserHeaders(request.headers); const requestHeaders = getHeaders( "false", workflowRunId, workflowUrl, userHeaders, void 0, failureUrl, retries ); const callResultStep = { stepId: Number(stepIdString), stepName, stepType, out: atob(callbackMessage.body), concurrent: Number(concurrentString) }; await debug?.log("SUBMIT", "SUBMIT_THIRD_PARTY_RESULT", { step: callResultStep, headers: requestHeaders, url: workflowUrl }); const result = await client.publishJSON({ headers: requestHeaders, method: "POST", body: callResultStep, url: workflowUrl }); await debug?.log("SUBMIT", "SUBMIT_THIRD_PARTY_RESULT", { messageId: result.messageId }); return (0, import_neverthrow.ok)("is-call-return"); } else { return (0, import_neverthrow.ok)("continue-workflow"); } } catch (error) { const isCallReturn = request.headers.get("Upstash-Workflow-Callback"); return (0, import_neverthrow.err)( new QStashWorkflowError( `Error when handling call return (isCallReturn=${isCallReturn}): ${error}` ) ); } }; var getHeaders = (initHeaderValue, workflowRunId, workflowUrl, userHeaders, step, failureUrl, retries) => { const baseHeaders = { [WORKFLOW_INIT_HEADER]: initHeaderValue, [WORKFLOW_ID_HEADER]: workflowRunId, [WORKFLOW_URL_HEADER]: workflowUrl, [`Upstash-Forward-${WORKFLOW_PROTOCOL_VERSION_HEADER}`]: WORKFLOW_PROTOCOL_VERSION, ...failureUrl ? { [`Upstash-Failure-Callback-Forward-${WORKFLOW_FAILURE_HEADER}`]: "true", "Upstash-Failure-Callback": failureUrl } : {}, ...retries === void 0 ? {} : { "Upstash-Retries": retries.toString() } }; if (userHeaders) { for (const header of userHeaders.keys()) { if (step?.callHeaders) { baseHeaders[`Upstash-Callback-Forward-${header}`] = userHeaders.get(header); } else { baseHeaders[`Upstash-Forward-${header}`] = userHeaders.get(header); } } } if (step?.callHeaders) { const forwardedHeaders = Object.fromEntries( Object.entries(step.callHeaders).map(([header, value]) => [ `Upstash-Forward-${header}`, value ]) ); const contentType = step.callHeaders["Content-Type"]; return { ...baseHeaders, ...forwardedHeaders, "Upstash-Callback": workflowUrl, "Upstash-Callback-Workflow-RunId": workflowRunId, "Upstash-Callback-Workflow-CallType": "fromCallback", "Upstash-Callback-Workflow-Init": "false", "Upstash-Callback-Workflow-Url": workflowUrl, "Upstash-Callback-Forward-Upstash-Workflow-Callback": "true", "Upstash-Callback-Forward-Upstash-Workflow-StepId": step.stepId.toString(), "Upstash-Callback-Forward-Upstash-Workflow-StepName": step.stepName, "Upstash-Callback-Forward-Upstash-Workflow-StepType": step.stepType, "Upstash-Callback-Forward-Upstash-Workflow-Concurrent": step.concurrent.toString(), "Upstash-Callback-Forward-Upstash-Workflow-ContentType": contentType ?? DEFAULT_CONTENT_TYPE, "Upstash-Workflow-CallType": "toCallback" }; } return baseHeaders; }; var verifyRequest = async (body, signature, verifier) => { if (!verifier) { return; } try { if (!signature) { throw new Error("`Upstash-Signature` header is not passed."); } const isValid = await verifier.verify({ body, signature }); if (!isValid) { throw new Error("Signature in `Upstash-Signature` header is not valid"); } } catch (error) { throw new QStashWorkflowError( `Failed to verify that the Workflow request comes from QStash: ${error} If signature is missing, trigger the workflow endpoint by publishing your request to QStash instead of calling it directly. If you want to disable QStash Verification, you should clear env variables QSTASH_CURRENT_SIGNING_KEY and QSTASH_NEXT_SIGNING_KEY` ); } }; // src/client/workflow/auto-executor.ts var AutoExecutor = class _AutoExecutor { context; promises = /* @__PURE__ */ new WeakMap(); activeLazyStepList; debug; nonPlanStepCount; steps; indexInCurrentList = 0; stepCount = 0; planStepCount = 0; executingStep = false; constructor(context, steps, debug) { this.context = context; this.debug = debug; this.steps = steps; this.nonPlanStepCount = this.steps.filter((step) => !step.targetStep).length; } /** * Adds the step function to the list of step functions to run in * parallel. After adding the function, defers the execution, so * that if there is another step function to be added, it's also * added. * * After all functions are added, list of functions are executed. * If there is a single function, it's executed by itself. If there * are multiple, they are run in parallel. * * If a function is already executing (this.executingStep), this * means that there is a nested step which is not allowed. In this * case, addStep throws QStashWorkflowError. * * @param stepInfo step plan to add * @returns result of the step function */ async addStep(stepInfo) { if (this.executingStep) { throw new QStashWorkflowError( `A step can not be run inside another step. Tried to run '${stepInfo.stepName}' inside '${this.executingStep}'` ); } this.stepCount += 1; const lazyStepList = this.activeLazyStepList ?? []; if (!this.activeLazyStepList) { this.activeLazyStepList = lazyStepList; this.indexInCurrentList = 0; } lazyStepList.push(stepInfo); const index = this.indexInCurrentList++; const requestComplete = this.deferExecution().then(async () => { if (!this.promises.has(lazyStepList)) { const promise2 = this.getExecutionPromise(lazyStepList); this.promises.set(lazyStepList, promise2); this.activeLazyStepList = void 0; this.planStepCount += lazyStepList.length > 1 ? lazyStepList.length : 0; } const promise = this.promises.get(lazyStepList); return promise; }); const result = await requestComplete; return _AutoExecutor.getResult(lazyStepList, result, index); } /** * Wraps a step function to set this.executingStep to step name * before running and set this.executingStep to False after execution * ends. * * this.executingStep allows us to detect nested steps which are not * allowed. * * @param stepName name of the step being wrapped * @param stepFunction step function to wrap * @returns wrapped step function */ wrapStep(stepName, stepFunction) { this.executingStep = stepName; const result = stepFunction(); this.executingStep = false; return result; } /** * Executes a step: * - If the step result is available in the steps, returns the result * - If the result is not avaiable, runs the function * - Sends the result to QStash * * @param lazyStep lazy step to execute * @returns step result */ async runSingle(lazyStep) { if (this.stepCount < this.nonPlanStepCount) { const step = this.steps[this.stepCount + this.planStepCount]; validateStep(lazyStep, step); await this.debug?.log("INFO", "RUN_SINGLE", { fromRequest: true, step, stepCount: this.stepCount }); return step.out; } const resultStep = await lazyStep.getResultStep(NO_CONCURRENCY, this.stepCount); await this.debug?.log("INFO", "RUN_SINGLE", { fromRequest: false, step: resultStep, stepCount: this.stepCount }); await this.submitStepsToQStash([resultStep]); return resultStep.out; } /** * Runs steps in parallel. * * @param stepName parallel step name * @param stepFunctions list of async functions to run in parallel * @returns results of the functions run in parallel */ async runParallel(parallelSteps) { const initialStepCount = this.stepCount - (parallelSteps.length - 1); const parallelCallState = this.getParallelCallState(parallelSteps.length, initialStepCount); const sortedSteps = sortSteps(this.steps); const plannedParallelStepCount = sortedSteps[initialStepCount + this.planStepCount]?.concurrent; if (parallelCallState !== "first" && plannedParallelStepCount !== parallelSteps.length) { throw new QStashWorkflowError( `Incompatible number of parallel steps when call state was '${parallelCallState}'. Expected ${parallelSteps.length}, got ${plannedParallelStepCount} from the request.` ); } await this.debug?.log("INFO", "RUN_PARALLEL", { parallelCallState, initialStepCount, plannedParallelStepCount, stepCount: this.stepCount, planStepCount: this.planStepCount }); switch (parallelCallState) {