@upstash/qstash
Version:
Official Typescript client for QStash
1,626 lines (1,604 loc) • 89 kB
JavaScript
"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