UNPKG

@upstash/qstash

Version:

Official Typescript client for QStash

1,626 lines (1,604 loc) 89 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); // src/client/workflow/index.ts var workflow_exports = {}; __export(workflow_exports, { DisabledWorkflowContext: () => DisabledWorkflowContext, StepTypes: () => StepTypes, Workflow: () => Workflow, WorkflowContext: () => WorkflowContext, WorkflowLogger: () => WorkflowLogger, processOptions: () => processOptions, serve: () => serve }); module.exports = __toCommonJS(workflow_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 }); } }; // version.ts var VERSION = "v2.8.4"; // src/client/client.ts var Client = class { http; token; constructor(config) { const environment = typeof process === "undefined" ? {} : process.env; let baseUrl = (config?.baseUrl ?? environment.QSTASH_URL ?? "https://qstash.upstash.io").replace(/\/$/, ""); if (baseUrl === "https://qstash.upstash.io/v2/publish") { baseUrl = "https://qstash.upstash.io"; } const token = config?.token ?? environment.QSTASH_TOKEN; const enableTelemetry = environment.UPSTASH_DISABLE_TELEMETRY ? false : config?.enableTelemetry ?? true; const isCloudflare = typeof caches !== "undefined" && "default" in caches; const telemetryHeaders = new Headers( enableTelemetry ? { "Upstash-Telemetry-Sdk": `upstash-qstash-js@${VERSION}`, "Upstash-Telemetry-Platform": isCloudflare ? "cloudflare" : environment.VERCEL ? "vercel" : environment.AWS_REGION ? "aws" : "", "Upstash-Telemetry-Runtime": getRuntime() } : {} ); this.http = new HttpClient({ retry: config?.retry, baseUrl, authorization: `Bearer ${token}`, //@ts-expect-error caused by undici and bunjs type overlap headers: prefixHeaders(new Headers(config?.headers ?? {})), //@ts-expect-error caused by undici and bunjs type overlap telemetryHeaders }); if (!token) { console.warn( "[Upstash QStash] client token is not set. Either pass a token or set QSTASH_TOKEN env variable." ); } this.token = token; } /** * Access the urlGroup API. * * Create, read, update or delete urlGroups. */ get urlGroups() { return new UrlGroups(this.http); } /** * Deprecated. Use urlGroups instead. * * Access the topic API. * * Create, read, update or delete topics. */ get topics() { return this.urlGroups; } /** * Access the dlq API. * * List or remove messages from the DLQ. */ get dlq() { return new DLQ(this.http); } /** * Access the message API. * * Read or cancel messages. */ get messages() { return new Messages(this.http); } /** * Access the schedule API. * * Create, read or delete schedules. */ get schedules() { return new Schedules(this.http); } /** * Access the workflow API. * * cancel workflows. * * @deprecated as of version 2.7.17. Will be removed in qstash-js 3.0.0. * Please use @upstash/workflow instead https://github.com/upstash/workflow-js * Migration Guide: https://upstash.com/docs/workflow/migration */ get workflow() { return new Workflow(this.http); } /** * Access the queue API. * * Create, read, update or delete queues. */ queue(request) { return new Queue(this.http, request?.queueName); } /** * Access the Chat API. * * @deprecated This will be removed in qstash-js 3.0.0. Please use an alternative SDK for interacting with LLMs. * * Use the create or prompt methods. */ chat() { return new Chat(this.http, this.token); } async publish(request) { const headers = wrapWithGlobalHeaders( processHeaders(request), this.http.headers, this.http.telemetryHeaders ); const response = await this.http.request({ path: ["v2", "publish", getRequestPath(request)], body: request.body, headers, method: "POST" }); return response; } /** * publishJSON is a utility wrapper around `publish` that automatically serializes the body * and sets the `Content-Type` header to `application/json`. */ async publishJSON(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.publish({ ...nonApiRequest, body: JSON.stringify(nonApiRequest.body) }); return response; } /** * Batch publish messages to QStash. */ async batch(request) { const messages = []; for (const message of request) { const headers = wrapWithGlobalHeaders( processHeaders(message), this.http.headers, this.http.telemetryHeaders ); const headerEntries = Object.fromEntries(headers.entries()); messages.push({ destination: getRequestPath(message), headers: headerEntries, body: message.body, ...message.queueName && { queue: message.queueName } }); } const response = await this.http.request({ path: ["v2", "batch"], body: JSON.stringify(messages), headers: { "Content-Type": "application/json" }, method: "POST" }); const arrayResposne = Array.isArray(response) ? response : [response]; return arrayResposne; } /** * Batch publish messages to QStash, serializing each body to JSON. */ async batchJSON(request) { const batchPayload = request.map((message) => { if ("body" in message) { message.body = JSON.stringify(message.body); } const upstashToken = String(this.http.authorization).split("Bearer ")[1]; const nonApiMessage = processApi(message, new Headers(message.headers), upstashToken); nonApiMessage.headers.set("Content-Type", "application/json"); return nonApiMessage; }); const response = await this.batch(batchPayload); return response; } /** * Retrieve your logs. * * The logs endpoint is paginated and returns only 100 logs at a time. * If you want to receive more logs, you can use the cursor to paginate. * * The cursor is a unix timestamp with millisecond precision * * @example * ```ts * let cursor = Date.now() * const logs: Log[] = [] * while (cursor > 0) { * const res = await qstash.logs({ cursor }) * logs.push(...res.logs) * cursor = res.cursor ?? 0 * } * ``` */ async logs(request) { const query = {}; if (typeof request?.cursor === "number" && request.cursor > 0) { query.cursor = request.cursor.toString(); } else if (typeof request?.cursor === "string" && request.cursor !== "") { query.cursor = request.cursor; } for (const [key, value] of Object.entries(request?.filter ?? {})) { if (typeof value === "number" && value < 0) { continue; } if (key === "urlGroup") { query.topicName = value.toString(); } else if (typeof value !== "undefined") { query[key] = value.toString(); } } const responsePayload = await this.http.request({ path: ["v2", "events"], method: "GET", query }); const logs = responsePayload.events.map((event) => { return { ...event, urlGroup: event.topicName }; }); return { cursor: responsePayload.cursor, logs, events: logs }; } /** * @deprecated Will be removed in the next major release. Use the `logs` method instead. * * Retrieve your logs. * * The logs endpoint is paginated and returns only 100 logs at a time. * If you want to receive more logs, you can use the cursor to paginate. * * The cursor is a unix timestamp with millisecond precision * * @example * ```ts * let cursor = Date.now() * const logs: Log[] = [] * while (cursor > 0) { * const res = await qstash.logs({ cursor }) * logs.push(...res.logs) * cursor = res.cursor ?? 0 * } * ``` */ async events(request) { return await this.logs(request); } }; // 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-Callbac