@upstash/qstash 2.7.5 → 2.7.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -4
- package/chunk-B3NB4QLV.mjs +2920 -0
- package/chunk-IJ3475FO.mjs +403 -0
- package/chunk-IJ5AEYLN.js +1 -0
- package/chunk-NMSGEGBP.js +403 -0
- package/chunk-S7JMIMW4.mjs +0 -0
- package/chunk-WQZ4U6LJ.js +2920 -0
- package/{dist/workflow/index.d.mts → client-aUVEwn93.d.mts} +868 -800
- package/{dist/nextjs/index.d.mts → client-aUVEwn93.d.ts} +871 -743
- package/cloudflare.d.mts +33 -0
- package/cloudflare.d.ts +33 -0
- package/cloudflare.js +37 -0
- package/cloudflare.mjs +37 -0
- package/h3.d.mts +17 -0
- package/h3.d.ts +17 -0
- package/h3.js +10 -0
- package/h3.mjs +10 -0
- package/hono.d.mts +25 -0
- package/hono.d.ts +25 -0
- package/hono.js +22 -0
- package/hono.mjs +22 -0
- package/index.d.mts +54 -0
- package/index.d.ts +54 -0
- package/index.js +41 -0
- package/index.mjs +41 -0
- package/nextjs.d.mts +38 -0
- package/nextjs.d.ts +38 -0
- package/nextjs.js +178 -0
- package/nextjs.mjs +178 -0
- package/nuxt.d.mts +12 -0
- package/nuxt.d.ts +12 -0
- package/nuxt.js +11 -0
- package/nuxt.mjs +11 -0
- package/package.json +1 -1
- package/solidjs.d.mts +22 -0
- package/solidjs.d.ts +22 -0
- package/solidjs.js +56 -0
- package/solidjs.mjs +56 -0
- package/svelte.d.mts +24 -0
- package/svelte.d.ts +24 -0
- package/svelte.js +53 -0
- package/svelte.mjs +53 -0
- package/workflow.d.mts +2 -0
- package/workflow.d.ts +2 -0
- package/workflow.js +18 -0
- package/workflow.mjs +18 -0
- package/dist/base/index.d.mts +0 -1279
- package/dist/base/index.mjs +0 -2
- package/dist/cloudflare/index.d.mts +0 -1652
- package/dist/cloudflare/index.mjs +0 -11
- package/dist/h3/index.d.mts +0 -1637
- package/dist/h3/index.mjs +0 -11
- package/dist/hono/index.d.mts +0 -1645
- package/dist/hono/index.mjs +0 -11
- package/dist/nextjs/index.mjs +0 -11
- package/dist/solidjs/index.d.mts +0 -1642
- package/dist/solidjs/index.mjs +0 -11
- package/dist/svelte/index.d.mts +0 -1644
- package/dist/svelte/index.mjs +0 -11
- package/dist/workflow/index.mjs +0 -11
|
@@ -0,0 +1,2920 @@
|
|
|
1
|
+
// src/receiver.ts
|
|
2
|
+
import * as jose from "jose";
|
|
3
|
+
import crypto2 from "crypto-js";
|
|
4
|
+
var SignatureError = class extends Error {
|
|
5
|
+
constructor(message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "SignatureError";
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
var Receiver = class {
|
|
11
|
+
currentSigningKey;
|
|
12
|
+
nextSigningKey;
|
|
13
|
+
constructor(config) {
|
|
14
|
+
this.currentSigningKey = config.currentSigningKey;
|
|
15
|
+
this.nextSigningKey = config.nextSigningKey;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Verify the signature of a request.
|
|
19
|
+
*
|
|
20
|
+
* Tries to verify the signature with the current signing key.
|
|
21
|
+
* If that fails, maybe because you have rotated the keys recently, it will
|
|
22
|
+
* try to verify the signature with the next signing key.
|
|
23
|
+
*
|
|
24
|
+
* If that fails, the signature is invalid and a `SignatureError` is thrown.
|
|
25
|
+
*/
|
|
26
|
+
async verify(request) {
|
|
27
|
+
const isValid = await this.verifyWithKey(this.currentSigningKey, request);
|
|
28
|
+
if (isValid) {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
return this.verifyWithKey(this.nextSigningKey, request);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Verify signature with a specific signing key
|
|
35
|
+
*/
|
|
36
|
+
async verifyWithKey(key, request) {
|
|
37
|
+
const jwt = await jose.jwtVerify(request.signature, new TextEncoder().encode(key), {
|
|
38
|
+
issuer: "Upstash",
|
|
39
|
+
clockTolerance: request.clockTolerance
|
|
40
|
+
}).catch((error) => {
|
|
41
|
+
throw new SignatureError(error.message);
|
|
42
|
+
});
|
|
43
|
+
const p = jwt.payload;
|
|
44
|
+
if (request.url !== void 0 && p.sub !== request.url) {
|
|
45
|
+
throw new SignatureError(`invalid subject: ${p.sub}, want: ${request.url}`);
|
|
46
|
+
}
|
|
47
|
+
const bodyHash = crypto2.SHA256(request.body).toString(crypto2.enc.Base64url);
|
|
48
|
+
const padding = new RegExp(/=+$/);
|
|
49
|
+
if (p.body.replace(padding, "") !== bodyHash.replace(padding, "")) {
|
|
50
|
+
throw new SignatureError(`body hash does not match, want: ${p.body}, got: ${bodyHash}`);
|
|
51
|
+
}
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// src/client/dlq.ts
|
|
57
|
+
var DLQ = class {
|
|
58
|
+
http;
|
|
59
|
+
constructor(http) {
|
|
60
|
+
this.http = http;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* List messages in the dlq
|
|
64
|
+
*/
|
|
65
|
+
async listMessages(options) {
|
|
66
|
+
const filterPayload = {
|
|
67
|
+
...options?.filter,
|
|
68
|
+
topicName: options?.filter?.urlGroup
|
|
69
|
+
};
|
|
70
|
+
const messagesPayload = await this.http.request({
|
|
71
|
+
method: "GET",
|
|
72
|
+
path: ["v2", "dlq"],
|
|
73
|
+
query: {
|
|
74
|
+
cursor: options?.cursor,
|
|
75
|
+
count: options?.count,
|
|
76
|
+
...filterPayload
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
return {
|
|
80
|
+
messages: messagesPayload.messages.map((message) => {
|
|
81
|
+
return {
|
|
82
|
+
...message,
|
|
83
|
+
urlGroup: message.topicName
|
|
84
|
+
};
|
|
85
|
+
}),
|
|
86
|
+
cursor: messagesPayload.cursor
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Remove a message from the dlq using it's `dlqId`
|
|
91
|
+
*/
|
|
92
|
+
async delete(dlqMessageId) {
|
|
93
|
+
return await this.http.request({
|
|
94
|
+
method: "DELETE",
|
|
95
|
+
path: ["v2", "dlq", dlqMessageId],
|
|
96
|
+
parseResponseAsJson: false
|
|
97
|
+
// there is no response
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Remove multiple messages from the dlq using their `dlqId`s
|
|
102
|
+
*/
|
|
103
|
+
async deleteMany(request) {
|
|
104
|
+
return await this.http.request({
|
|
105
|
+
method: "DELETE",
|
|
106
|
+
path: ["v2", "dlq"],
|
|
107
|
+
headers: { "Content-Type": "application/json" },
|
|
108
|
+
body: JSON.stringify({ dlqIds: request.dlqIds })
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// src/client/error.ts
|
|
114
|
+
var QstashError = class extends Error {
|
|
115
|
+
constructor(message) {
|
|
116
|
+
super(message);
|
|
117
|
+
this.name = "QstashError";
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
var QstashRatelimitError = class extends QstashError {
|
|
121
|
+
limit;
|
|
122
|
+
remaining;
|
|
123
|
+
reset;
|
|
124
|
+
constructor(args) {
|
|
125
|
+
super(`Exceeded burst rate limit. ${JSON.stringify(args)} `);
|
|
126
|
+
this.name = "QstashRatelimitError";
|
|
127
|
+
this.limit = args.limit;
|
|
128
|
+
this.remaining = args.remaining;
|
|
129
|
+
this.reset = args.reset;
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
var QstashChatRatelimitError = class extends QstashError {
|
|
133
|
+
limitRequests;
|
|
134
|
+
limitTokens;
|
|
135
|
+
remainingRequests;
|
|
136
|
+
remainingTokens;
|
|
137
|
+
resetRequests;
|
|
138
|
+
resetTokens;
|
|
139
|
+
constructor(args) {
|
|
140
|
+
super(`Exceeded chat rate limit. ${JSON.stringify(args)} `);
|
|
141
|
+
this.limitRequests = args["limit-requests"];
|
|
142
|
+
this.limitTokens = args["limit-tokens"];
|
|
143
|
+
this.remainingRequests = args["remaining-requests"];
|
|
144
|
+
this.remainingTokens = args["remaining-tokens"];
|
|
145
|
+
this.resetRequests = args["reset-requests"];
|
|
146
|
+
this.resetTokens = args["reset-tokens"];
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
var QstashDailyRatelimitError = class extends QstashError {
|
|
150
|
+
limit;
|
|
151
|
+
remaining;
|
|
152
|
+
reset;
|
|
153
|
+
constructor(args) {
|
|
154
|
+
super(`Exceeded daily rate limit. ${JSON.stringify(args)} `);
|
|
155
|
+
this.limit = args.limit;
|
|
156
|
+
this.remaining = args.remaining;
|
|
157
|
+
this.reset = args.reset;
|
|
158
|
+
this.name = "QstashChatRatelimitError";
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
var QStashWorkflowError = class extends QstashError {
|
|
162
|
+
constructor(message) {
|
|
163
|
+
super(message);
|
|
164
|
+
this.name = "QStashWorkflowError";
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
var QStashWorkflowAbort = class extends Error {
|
|
168
|
+
stepInfo;
|
|
169
|
+
stepName;
|
|
170
|
+
constructor(stepName, stepInfo) {
|
|
171
|
+
super(
|
|
172
|
+
`This is an QStash 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}'.`
|
|
173
|
+
);
|
|
174
|
+
this.name = "QStashWorkflowAbort";
|
|
175
|
+
this.stepName = stepName;
|
|
176
|
+
this.stepInfo = stepInfo;
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
var formatWorkflowError = (error) => {
|
|
180
|
+
return error instanceof Error ? {
|
|
181
|
+
error: error.name,
|
|
182
|
+
message: error.message
|
|
183
|
+
} : {
|
|
184
|
+
error: "Error",
|
|
185
|
+
message: "An error occured while executing workflow."
|
|
186
|
+
};
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
// src/client/http.ts
|
|
190
|
+
var HttpClient = class {
|
|
191
|
+
baseUrl;
|
|
192
|
+
authorization;
|
|
193
|
+
options;
|
|
194
|
+
retry;
|
|
195
|
+
constructor(config) {
|
|
196
|
+
this.baseUrl = config.baseUrl.replace(/\/$/, "");
|
|
197
|
+
this.authorization = config.authorization;
|
|
198
|
+
this.retry = // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
199
|
+
typeof config.retry === "boolean" && !config.retry ? {
|
|
200
|
+
attempts: 1,
|
|
201
|
+
backoff: () => 0
|
|
202
|
+
} : {
|
|
203
|
+
attempts: config.retry?.retries ? config.retry.retries + 1 : 5,
|
|
204
|
+
backoff: config.retry?.backoff ?? ((retryCount) => Math.exp(retryCount) * 50)
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
async request(request) {
|
|
208
|
+
const { response } = await this.requestWithBackoff(request);
|
|
209
|
+
if (request.parseResponseAsJson === false) {
|
|
210
|
+
return void 0;
|
|
211
|
+
}
|
|
212
|
+
return await response.json();
|
|
213
|
+
}
|
|
214
|
+
async *requestStream(request) {
|
|
215
|
+
const { response } = await this.requestWithBackoff(request);
|
|
216
|
+
if (!response.body) {
|
|
217
|
+
throw new Error("No response body");
|
|
218
|
+
}
|
|
219
|
+
const body = response.body;
|
|
220
|
+
const reader = body.getReader();
|
|
221
|
+
const decoder = new TextDecoder();
|
|
222
|
+
try {
|
|
223
|
+
while (true) {
|
|
224
|
+
const { done, value } = await reader.read();
|
|
225
|
+
if (done) {
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
const chunkText = decoder.decode(value, { stream: true });
|
|
229
|
+
const chunks = chunkText.split("\n").filter(Boolean);
|
|
230
|
+
for (const chunk of chunks) {
|
|
231
|
+
if (chunk.startsWith("data: ")) {
|
|
232
|
+
const data = chunk.slice(6);
|
|
233
|
+
if (data === "[DONE]") {
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
yield JSON.parse(data);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
} finally {
|
|
241
|
+
await reader.cancel();
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
requestWithBackoff = async (request) => {
|
|
245
|
+
const [url, requestOptions] = this.processRequest(request);
|
|
246
|
+
let response = void 0;
|
|
247
|
+
let error = void 0;
|
|
248
|
+
for (let index = 0; index < this.retry.attempts; index++) {
|
|
249
|
+
try {
|
|
250
|
+
response = await fetch(url.toString(), requestOptions);
|
|
251
|
+
break;
|
|
252
|
+
} catch (error_) {
|
|
253
|
+
error = error_;
|
|
254
|
+
await new Promise((r) => setTimeout(r, this.retry.backoff(index)));
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (!response) {
|
|
258
|
+
throw error ?? new Error("Exhausted all retries");
|
|
259
|
+
}
|
|
260
|
+
await this.checkResponse(response);
|
|
261
|
+
return {
|
|
262
|
+
response,
|
|
263
|
+
error
|
|
264
|
+
};
|
|
265
|
+
};
|
|
266
|
+
processRequest = (request) => {
|
|
267
|
+
const headers = new Headers(request.headers);
|
|
268
|
+
if (!headers.has("Authorization")) {
|
|
269
|
+
headers.set("Authorization", this.authorization);
|
|
270
|
+
}
|
|
271
|
+
const requestOptions = {
|
|
272
|
+
method: request.method,
|
|
273
|
+
headers,
|
|
274
|
+
body: request.body,
|
|
275
|
+
keepalive: request.keepalive
|
|
276
|
+
};
|
|
277
|
+
const url = new URL([request.baseUrl ?? this.baseUrl, ...request.path].join("/"));
|
|
278
|
+
if (request.query) {
|
|
279
|
+
for (const [key, value] of Object.entries(request.query)) {
|
|
280
|
+
if (value !== void 0) {
|
|
281
|
+
url.searchParams.set(key, value.toString());
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return [url.toString(), requestOptions];
|
|
286
|
+
};
|
|
287
|
+
async checkResponse(response) {
|
|
288
|
+
if (response.status === 429) {
|
|
289
|
+
if (response.headers.get("x-ratelimit-limit-requests")) {
|
|
290
|
+
throw new QstashChatRatelimitError({
|
|
291
|
+
"limit-requests": response.headers.get("x-ratelimit-limit-requests"),
|
|
292
|
+
"limit-tokens": response.headers.get("x-ratelimit-limit-tokens"),
|
|
293
|
+
"remaining-requests": response.headers.get("x-ratelimit-remaining-requests"),
|
|
294
|
+
"remaining-tokens": response.headers.get("x-ratelimit-remaining-tokens"),
|
|
295
|
+
"reset-requests": response.headers.get("x-ratelimit-reset-requests"),
|
|
296
|
+
"reset-tokens": response.headers.get("x-ratelimit-reset-tokens")
|
|
297
|
+
});
|
|
298
|
+
} else if (response.headers.get("RateLimit-Limit")) {
|
|
299
|
+
throw new QstashDailyRatelimitError({
|
|
300
|
+
limit: response.headers.get("RateLimit-Limit"),
|
|
301
|
+
remaining: response.headers.get("RateLimit-Remaining"),
|
|
302
|
+
reset: response.headers.get("RateLimit-Reset")
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
throw new QstashRatelimitError({
|
|
306
|
+
limit: response.headers.get("Burst-RateLimit-Limit"),
|
|
307
|
+
remaining: response.headers.get("Burst-RateLimit-Remaining"),
|
|
308
|
+
reset: response.headers.get("Burst-RateLimit-Reset")
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
if (response.status < 200 || response.status >= 300) {
|
|
312
|
+
const body = await response.text();
|
|
313
|
+
throw new QstashError(body.length > 0 ? body : `Error: status=${response.status}`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
// src/client/llm/providers.ts
|
|
319
|
+
var setupAnalytics = (analytics, providerApiKey, providerBaseUrl, provider) => {
|
|
320
|
+
if (!analytics)
|
|
321
|
+
return {};
|
|
322
|
+
switch (analytics.name) {
|
|
323
|
+
case "helicone": {
|
|
324
|
+
switch (provider) {
|
|
325
|
+
case "upstash": {
|
|
326
|
+
return {
|
|
327
|
+
baseURL: "https://qstash.helicone.ai/llm/v1/chat/completions",
|
|
328
|
+
defaultHeaders: {
|
|
329
|
+
"Helicone-Auth": `Bearer ${analytics.token}`,
|
|
330
|
+
Authorization: `Bearer ${providerApiKey}`
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
default: {
|
|
335
|
+
return {
|
|
336
|
+
baseURL: "https://gateway.helicone.ai/v1/chat/completions",
|
|
337
|
+
defaultHeaders: {
|
|
338
|
+
"Helicone-Auth": `Bearer ${analytics.token}`,
|
|
339
|
+
"Helicone-Target-Url": providerBaseUrl,
|
|
340
|
+
Authorization: `Bearer ${providerApiKey}`
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
default: {
|
|
347
|
+
throw new Error("Unknown analytics provider");
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
var upstash = () => {
|
|
352
|
+
return {
|
|
353
|
+
owner: "upstash",
|
|
354
|
+
baseUrl: "https://qstash.upstash.io/llm",
|
|
355
|
+
token: ""
|
|
356
|
+
};
|
|
357
|
+
};
|
|
358
|
+
var openai = ({
|
|
359
|
+
token
|
|
360
|
+
}) => {
|
|
361
|
+
return { token, owner: "openai", baseUrl: "https://api.openai.com" };
|
|
362
|
+
};
|
|
363
|
+
var custom = ({
|
|
364
|
+
baseUrl,
|
|
365
|
+
token
|
|
366
|
+
}) => {
|
|
367
|
+
const trimmedBaseUrl = baseUrl.replace(/\/(v1\/)?chat\/completions$/, "");
|
|
368
|
+
return {
|
|
369
|
+
token,
|
|
370
|
+
owner: "custom",
|
|
371
|
+
baseUrl: trimmedBaseUrl
|
|
372
|
+
};
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
// src/client/llm/chat.ts
|
|
376
|
+
var Chat = class _Chat {
|
|
377
|
+
http;
|
|
378
|
+
token;
|
|
379
|
+
constructor(http, token) {
|
|
380
|
+
this.http = http;
|
|
381
|
+
this.token = token;
|
|
382
|
+
}
|
|
383
|
+
static toChatRequest(request) {
|
|
384
|
+
const messages = [];
|
|
385
|
+
messages.push(
|
|
386
|
+
{ role: "system", content: request.system },
|
|
387
|
+
{ role: "user", content: request.user }
|
|
388
|
+
);
|
|
389
|
+
const chatRequest = { ...request, messages };
|
|
390
|
+
return chatRequest;
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Calls the Upstash completions api given a ChatRequest.
|
|
394
|
+
*
|
|
395
|
+
* Returns a ChatCompletion or a stream of ChatCompletionChunks
|
|
396
|
+
* if stream is enabled.
|
|
397
|
+
*
|
|
398
|
+
* @param request ChatRequest with messages
|
|
399
|
+
* @returns Chat completion or stream
|
|
400
|
+
*/
|
|
401
|
+
create = async (request) => {
|
|
402
|
+
if (request.provider.owner != "upstash")
|
|
403
|
+
return this.createThirdParty(request);
|
|
404
|
+
const body = JSON.stringify(request);
|
|
405
|
+
let baseUrl = void 0;
|
|
406
|
+
let headers = {
|
|
407
|
+
"Content-Type": "application/json",
|
|
408
|
+
Authorization: `Bearer ${this.token}`,
|
|
409
|
+
..."stream" in request && request.stream ? {
|
|
410
|
+
Connection: "keep-alive",
|
|
411
|
+
Accept: "text/event-stream",
|
|
412
|
+
"Cache-Control": "no-cache"
|
|
413
|
+
} : {}
|
|
414
|
+
};
|
|
415
|
+
if (request.analytics) {
|
|
416
|
+
const { baseURL, defaultHeaders } = setupAnalytics(
|
|
417
|
+
{ name: "helicone", token: request.analytics.token },
|
|
418
|
+
this.getAuthorizationToken(),
|
|
419
|
+
request.provider.baseUrl,
|
|
420
|
+
"upstash"
|
|
421
|
+
);
|
|
422
|
+
headers = { ...headers, ...defaultHeaders };
|
|
423
|
+
baseUrl = baseURL;
|
|
424
|
+
}
|
|
425
|
+
const path = request.analytics ? [] : ["llm", "v1", "chat", "completions"];
|
|
426
|
+
return "stream" in request && request.stream ? this.http.requestStream({
|
|
427
|
+
path,
|
|
428
|
+
method: "POST",
|
|
429
|
+
headers,
|
|
430
|
+
baseUrl,
|
|
431
|
+
body
|
|
432
|
+
}) : this.http.request({
|
|
433
|
+
path,
|
|
434
|
+
method: "POST",
|
|
435
|
+
headers,
|
|
436
|
+
baseUrl,
|
|
437
|
+
body
|
|
438
|
+
});
|
|
439
|
+
};
|
|
440
|
+
/**
|
|
441
|
+
* Calls the Upstash completions api given a ChatRequest.
|
|
442
|
+
*
|
|
443
|
+
* Returns a ChatCompletion or a stream of ChatCompletionChunks
|
|
444
|
+
* if stream is enabled.
|
|
445
|
+
*
|
|
446
|
+
* @param request ChatRequest with messages
|
|
447
|
+
* @returns Chat completion or stream
|
|
448
|
+
*/
|
|
449
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
450
|
+
createThirdParty = async (request) => {
|
|
451
|
+
const { baseUrl, token, owner } = request.provider;
|
|
452
|
+
if (owner === "upstash")
|
|
453
|
+
throw new Error("Upstash is not 3rd party provider!");
|
|
454
|
+
delete request.provider;
|
|
455
|
+
delete request.system;
|
|
456
|
+
const analytics = request.analytics;
|
|
457
|
+
delete request.analytics;
|
|
458
|
+
const body = JSON.stringify(request);
|
|
459
|
+
const isAnalyticsEnabled = analytics?.name && analytics.token;
|
|
460
|
+
const analyticsConfig = analytics?.name && analytics.token ? setupAnalytics({ name: analytics.name, token: analytics.token }, token, baseUrl, owner) : { defaultHeaders: void 0, baseURL: baseUrl };
|
|
461
|
+
const isStream = "stream" in request && request.stream;
|
|
462
|
+
const headers = {
|
|
463
|
+
"Content-Type": "application/json",
|
|
464
|
+
Authorization: `Bearer ${token}`,
|
|
465
|
+
...isStream ? {
|
|
466
|
+
Connection: "keep-alive",
|
|
467
|
+
Accept: "text/event-stream",
|
|
468
|
+
"Cache-Control": "no-cache"
|
|
469
|
+
} : {},
|
|
470
|
+
...analyticsConfig.defaultHeaders
|
|
471
|
+
};
|
|
472
|
+
const response = await this.http[isStream ? "requestStream" : "request"]({
|
|
473
|
+
path: isAnalyticsEnabled ? [] : ["v1", "chat", "completions"],
|
|
474
|
+
method: "POST",
|
|
475
|
+
headers,
|
|
476
|
+
body,
|
|
477
|
+
baseUrl: analyticsConfig.baseURL
|
|
478
|
+
});
|
|
479
|
+
return response;
|
|
480
|
+
};
|
|
481
|
+
// Helper method to get the authorization token
|
|
482
|
+
getAuthorizationToken() {
|
|
483
|
+
const authHeader = String(this.http.authorization);
|
|
484
|
+
const match = authHeader.match(/Bearer (.+)/);
|
|
485
|
+
if (!match) {
|
|
486
|
+
throw new Error("Invalid authorization header format");
|
|
487
|
+
}
|
|
488
|
+
return match[1];
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Calls the Upstash completions api given a PromptRequest.
|
|
492
|
+
*
|
|
493
|
+
* Returns a ChatCompletion or a stream of ChatCompletionChunks
|
|
494
|
+
* if stream is enabled.
|
|
495
|
+
*
|
|
496
|
+
* @param request PromptRequest with system and user messages.
|
|
497
|
+
* Note that system parameter shouldn't be passed in the case of
|
|
498
|
+
* mistralai/Mistral-7B-Instruct-v0.2 model.
|
|
499
|
+
* @returns Chat completion or stream
|
|
500
|
+
*/
|
|
501
|
+
prompt = async (request) => {
|
|
502
|
+
const chatRequest = _Chat.toChatRequest(request);
|
|
503
|
+
return this.create(chatRequest);
|
|
504
|
+
};
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
// src/client/llm/utils.ts
|
|
508
|
+
function appendLLMOptionsIfNeeded(request, headers, http) {
|
|
509
|
+
if (!request.api)
|
|
510
|
+
return;
|
|
511
|
+
const provider = request.api.provider;
|
|
512
|
+
const analytics = request.api.analytics;
|
|
513
|
+
if (provider?.owner === "upstash") {
|
|
514
|
+
handleUpstashProvider(request, headers, http, analytics);
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
if (!("provider" in request.api))
|
|
518
|
+
return;
|
|
519
|
+
const { baseUrl, token } = validateProviderConfig(provider);
|
|
520
|
+
const analyticsConfig = analytics ? setupAnalytics({ name: analytics.name, token: analytics.token }, token, baseUrl, "custom") : void 0;
|
|
521
|
+
if (analyticsConfig) {
|
|
522
|
+
setAnalyticsHeaders(headers, analyticsConfig);
|
|
523
|
+
request.url = analyticsConfig.baseURL;
|
|
524
|
+
} else {
|
|
525
|
+
request.url = `${baseUrl}/v1/chat/completions`;
|
|
526
|
+
headers.set("Authorization", `Bearer ${token}`);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
function handleUpstashProvider(request, headers, http, analytics) {
|
|
530
|
+
if (analytics) {
|
|
531
|
+
const analyticsConfig = setupAnalytics(
|
|
532
|
+
{ name: analytics.name, token: analytics.token },
|
|
533
|
+
//@ts-expect-error hacky way to get bearer token
|
|
534
|
+
String(http.authorization).split("Bearer ")[1],
|
|
535
|
+
request.api?.provider?.baseUrl,
|
|
536
|
+
"upstash"
|
|
537
|
+
);
|
|
538
|
+
setAnalyticsHeaders(headers, analyticsConfig);
|
|
539
|
+
request.url = analyticsConfig.baseURL;
|
|
540
|
+
} else {
|
|
541
|
+
request.api = { name: "llm" };
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
function validateProviderConfig(provider) {
|
|
545
|
+
if (!provider?.baseUrl)
|
|
546
|
+
throw new Error("baseUrl cannot be empty or undefined!");
|
|
547
|
+
if (!provider.token)
|
|
548
|
+
throw new Error("token cannot be empty or undefined!");
|
|
549
|
+
return { baseUrl: provider.baseUrl, token: provider.token };
|
|
550
|
+
}
|
|
551
|
+
function setAnalyticsHeaders(headers, analyticsConfig) {
|
|
552
|
+
headers.set("Helicone-Auth", analyticsConfig.defaultHeaders?.["Helicone-Auth"] ?? "");
|
|
553
|
+
headers.set("Authorization", analyticsConfig.defaultHeaders?.Authorization ?? "");
|
|
554
|
+
if (analyticsConfig.defaultHeaders?.["Helicone-Target-Url"]) {
|
|
555
|
+
headers.set("Helicone-Target-Url", analyticsConfig.defaultHeaders["Helicone-Target-Url"]);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
function ensureCallbackPresent(request) {
|
|
559
|
+
if (request.api?.name === "llm" && !request.callback) {
|
|
560
|
+
throw new TypeError("Callback cannot be undefined when using LLM");
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// src/client/messages.ts
|
|
565
|
+
var Messages = class {
|
|
566
|
+
http;
|
|
567
|
+
constructor(http) {
|
|
568
|
+
this.http = http;
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Get a message
|
|
572
|
+
*/
|
|
573
|
+
async get(messageId) {
|
|
574
|
+
const messagePayload = await this.http.request({
|
|
575
|
+
method: "GET",
|
|
576
|
+
path: ["v2", "messages", messageId]
|
|
577
|
+
});
|
|
578
|
+
const message = {
|
|
579
|
+
...messagePayload,
|
|
580
|
+
urlGroup: messagePayload.topicName
|
|
581
|
+
};
|
|
582
|
+
return message;
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Cancel a message
|
|
586
|
+
*/
|
|
587
|
+
async delete(messageId) {
|
|
588
|
+
return await this.http.request({
|
|
589
|
+
method: "DELETE",
|
|
590
|
+
path: ["v2", "messages", messageId],
|
|
591
|
+
parseResponseAsJson: false
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
async deleteMany(messageIds) {
|
|
595
|
+
const result = await this.http.request({
|
|
596
|
+
method: "DELETE",
|
|
597
|
+
path: ["v2", "messages"],
|
|
598
|
+
headers: { "Content-Type": "application/json" },
|
|
599
|
+
body: JSON.stringify({ messageIds })
|
|
600
|
+
});
|
|
601
|
+
return result.cancelled;
|
|
602
|
+
}
|
|
603
|
+
async deleteAll() {
|
|
604
|
+
const result = await this.http.request({
|
|
605
|
+
method: "DELETE",
|
|
606
|
+
path: ["v2", "messages"]
|
|
607
|
+
});
|
|
608
|
+
return result.cancelled;
|
|
609
|
+
}
|
|
610
|
+
};
|
|
611
|
+
|
|
612
|
+
// src/client/utils.ts
|
|
613
|
+
var isIgnoredHeader = (header) => {
|
|
614
|
+
const lowerCaseHeader = header.toLowerCase();
|
|
615
|
+
return lowerCaseHeader.startsWith("content-type") || lowerCaseHeader.startsWith("upstash-");
|
|
616
|
+
};
|
|
617
|
+
function prefixHeaders(headers) {
|
|
618
|
+
const keysToBePrefixed = [...headers.keys()].filter((key) => !isIgnoredHeader(key));
|
|
619
|
+
for (const key of keysToBePrefixed) {
|
|
620
|
+
const value = headers.get(key);
|
|
621
|
+
if (value !== null) {
|
|
622
|
+
headers.set(`Upstash-Forward-${key}`, value);
|
|
623
|
+
}
|
|
624
|
+
headers.delete(key);
|
|
625
|
+
}
|
|
626
|
+
return headers;
|
|
627
|
+
}
|
|
628
|
+
function processHeaders(request) {
|
|
629
|
+
const headers = prefixHeaders(new Headers(request.headers));
|
|
630
|
+
headers.set("Upstash-Method", request.method ?? "POST");
|
|
631
|
+
if (request.delay !== void 0) {
|
|
632
|
+
if (typeof request.delay === "string") {
|
|
633
|
+
headers.set("Upstash-Delay", request.delay);
|
|
634
|
+
} else {
|
|
635
|
+
headers.set("Upstash-Delay", `${request.delay.toFixed(0)}s`);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
if (request.notBefore !== void 0) {
|
|
639
|
+
headers.set("Upstash-Not-Before", request.notBefore.toFixed(0));
|
|
640
|
+
}
|
|
641
|
+
if (request.deduplicationId !== void 0) {
|
|
642
|
+
headers.set("Upstash-Deduplication-Id", request.deduplicationId);
|
|
643
|
+
}
|
|
644
|
+
if (request.contentBasedDeduplication !== void 0) {
|
|
645
|
+
headers.set("Upstash-Content-Based-Deduplication", "true");
|
|
646
|
+
}
|
|
647
|
+
if (request.retries !== void 0) {
|
|
648
|
+
headers.set("Upstash-Retries", request.retries.toFixed(0));
|
|
649
|
+
}
|
|
650
|
+
if (request.callback !== void 0) {
|
|
651
|
+
headers.set("Upstash-Callback", request.callback);
|
|
652
|
+
}
|
|
653
|
+
if (request.failureCallback !== void 0) {
|
|
654
|
+
headers.set("Upstash-Failure-Callback", request.failureCallback);
|
|
655
|
+
}
|
|
656
|
+
if (request.timeout !== void 0) {
|
|
657
|
+
if (typeof request.timeout === "string") {
|
|
658
|
+
headers.set("Upstash-Timeout", request.timeout);
|
|
659
|
+
} else {
|
|
660
|
+
headers.set("Upstash-Timeout", `${request.timeout}s`);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
return headers;
|
|
664
|
+
}
|
|
665
|
+
function getRequestPath(request) {
|
|
666
|
+
return request.url ?? request.urlGroup ?? request.topic ?? `api/${request.api?.name}`;
|
|
667
|
+
}
|
|
668
|
+
var NANOID_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_";
|
|
669
|
+
var NANOID_LENGTH = 21;
|
|
670
|
+
function nanoid() {
|
|
671
|
+
return [...crypto.getRandomValues(new Uint8Array(NANOID_LENGTH))].map((x) => NANOID_CHARS[x % NANOID_CHARS.length]).join("");
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// src/client/queue.ts
|
|
675
|
+
var Queue = class {
|
|
676
|
+
http;
|
|
677
|
+
queueName;
|
|
678
|
+
constructor(http, queueName) {
|
|
679
|
+
this.http = http;
|
|
680
|
+
this.queueName = queueName;
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* Create or update the queue
|
|
684
|
+
*/
|
|
685
|
+
async upsert(request) {
|
|
686
|
+
if (!this.queueName) {
|
|
687
|
+
throw new Error("Please provide a queue name to the Queue constructor");
|
|
688
|
+
}
|
|
689
|
+
const body = {
|
|
690
|
+
queueName: this.queueName,
|
|
691
|
+
parallelism: request.parallelism ?? 1,
|
|
692
|
+
paused: request.paused ?? false
|
|
693
|
+
};
|
|
694
|
+
await this.http.request({
|
|
695
|
+
method: "POST",
|
|
696
|
+
path: ["v2", "queues"],
|
|
697
|
+
headers: {
|
|
698
|
+
"Content-Type": "application/json"
|
|
699
|
+
},
|
|
700
|
+
body: JSON.stringify(body),
|
|
701
|
+
parseResponseAsJson: false
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* Get the queue details
|
|
706
|
+
*/
|
|
707
|
+
async get() {
|
|
708
|
+
if (!this.queueName) {
|
|
709
|
+
throw new Error("Please provide a queue name to the Queue constructor");
|
|
710
|
+
}
|
|
711
|
+
return await this.http.request({
|
|
712
|
+
method: "GET",
|
|
713
|
+
path: ["v2", "queues", this.queueName]
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
717
|
+
* List queues
|
|
718
|
+
*/
|
|
719
|
+
async list() {
|
|
720
|
+
return await this.http.request({
|
|
721
|
+
method: "GET",
|
|
722
|
+
path: ["v2", "queues"]
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* Delete the queue
|
|
727
|
+
*/
|
|
728
|
+
async delete() {
|
|
729
|
+
if (!this.queueName) {
|
|
730
|
+
throw new Error("Please provide a queue name to the Queue constructor");
|
|
731
|
+
}
|
|
732
|
+
await this.http.request({
|
|
733
|
+
method: "DELETE",
|
|
734
|
+
path: ["v2", "queues", this.queueName],
|
|
735
|
+
parseResponseAsJson: false
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* Enqueue a message to a queue.
|
|
740
|
+
*/
|
|
741
|
+
async enqueue(request) {
|
|
742
|
+
if (!this.queueName) {
|
|
743
|
+
throw new Error("Please provide a queue name to the Queue constructor");
|
|
744
|
+
}
|
|
745
|
+
const headers = processHeaders(request);
|
|
746
|
+
const destination = getRequestPath(request);
|
|
747
|
+
const response = await this.http.request({
|
|
748
|
+
path: ["v2", "enqueue", this.queueName, destination],
|
|
749
|
+
body: request.body,
|
|
750
|
+
headers,
|
|
751
|
+
method: "POST"
|
|
752
|
+
});
|
|
753
|
+
return response;
|
|
754
|
+
}
|
|
755
|
+
/**
|
|
756
|
+
* Enqueue a message to a queue, serializing the body to JSON.
|
|
757
|
+
*/
|
|
758
|
+
async enqueueJSON(request) {
|
|
759
|
+
const headers = prefixHeaders(new Headers(request.headers));
|
|
760
|
+
headers.set("Content-Type", "application/json");
|
|
761
|
+
ensureCallbackPresent(request);
|
|
762
|
+
appendLLMOptionsIfNeeded(request, headers, this.http);
|
|
763
|
+
const response = await this.enqueue({
|
|
764
|
+
...request,
|
|
765
|
+
body: JSON.stringify(request.body),
|
|
766
|
+
headers
|
|
767
|
+
});
|
|
768
|
+
return response;
|
|
769
|
+
}
|
|
770
|
+
/**
|
|
771
|
+
* Pauses the queue.
|
|
772
|
+
*
|
|
773
|
+
* A paused queue will not deliver messages until
|
|
774
|
+
* it is resumed.
|
|
775
|
+
*/
|
|
776
|
+
async pause() {
|
|
777
|
+
if (!this.queueName) {
|
|
778
|
+
throw new Error("Please provide a queue name to the Queue constructor");
|
|
779
|
+
}
|
|
780
|
+
await this.http.request({
|
|
781
|
+
method: "POST",
|
|
782
|
+
path: ["v2", "queues", this.queueName, "pause"],
|
|
783
|
+
parseResponseAsJson: false
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
/**
|
|
787
|
+
* Resumes the queue.
|
|
788
|
+
*/
|
|
789
|
+
async resume() {
|
|
790
|
+
if (!this.queueName) {
|
|
791
|
+
throw new Error("Please provide a queue name to the Queue constructor");
|
|
792
|
+
}
|
|
793
|
+
await this.http.request({
|
|
794
|
+
method: "POST",
|
|
795
|
+
path: ["v2", "queues", this.queueName, "resume"],
|
|
796
|
+
parseResponseAsJson: false
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
};
|
|
800
|
+
|
|
801
|
+
// src/client/schedules.ts
|
|
802
|
+
var Schedules = class {
|
|
803
|
+
http;
|
|
804
|
+
constructor(http) {
|
|
805
|
+
this.http = http;
|
|
806
|
+
}
|
|
807
|
+
/**
|
|
808
|
+
* Create a schedule
|
|
809
|
+
*/
|
|
810
|
+
async create(request) {
|
|
811
|
+
const headers = prefixHeaders(new Headers(request.headers));
|
|
812
|
+
if (!headers.has("Content-Type")) {
|
|
813
|
+
headers.set("Content-Type", "application/json");
|
|
814
|
+
}
|
|
815
|
+
headers.set("Upstash-Cron", request.cron);
|
|
816
|
+
if (request.method !== void 0) {
|
|
817
|
+
headers.set("Upstash-Method", request.method);
|
|
818
|
+
}
|
|
819
|
+
if (request.delay !== void 0) {
|
|
820
|
+
if (typeof request.delay === "string") {
|
|
821
|
+
headers.set("Upstash-Delay", request.delay);
|
|
822
|
+
} else {
|
|
823
|
+
headers.set("Upstash-Delay", `${request.delay.toFixed(0)}s`);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
if (request.retries !== void 0) {
|
|
827
|
+
headers.set("Upstash-Retries", request.retries.toFixed(0));
|
|
828
|
+
}
|
|
829
|
+
if (request.callback !== void 0) {
|
|
830
|
+
headers.set("Upstash-Callback", request.callback);
|
|
831
|
+
}
|
|
832
|
+
if (request.failureCallback !== void 0) {
|
|
833
|
+
headers.set("Upstash-Failure-Callback", request.failureCallback);
|
|
834
|
+
}
|
|
835
|
+
if (request.timeout !== void 0) {
|
|
836
|
+
if (typeof request.timeout === "string") {
|
|
837
|
+
headers.set("Upstash-Timeout", request.timeout);
|
|
838
|
+
} else {
|
|
839
|
+
headers.set("Upstash-Timeout", `${request.timeout}s`);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
if (request.scheduleId !== void 0) {
|
|
843
|
+
headers.set("Upstash-Schedule-Id", request.scheduleId);
|
|
844
|
+
}
|
|
845
|
+
return await this.http.request({
|
|
846
|
+
method: "POST",
|
|
847
|
+
headers,
|
|
848
|
+
path: ["v2", "schedules", request.destination],
|
|
849
|
+
body: request.body
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
/**
|
|
853
|
+
* Get a schedule
|
|
854
|
+
*/
|
|
855
|
+
async get(scheduleId) {
|
|
856
|
+
return await this.http.request({
|
|
857
|
+
method: "GET",
|
|
858
|
+
path: ["v2", "schedules", scheduleId]
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* List your schedules
|
|
863
|
+
*/
|
|
864
|
+
async list() {
|
|
865
|
+
return await this.http.request({
|
|
866
|
+
method: "GET",
|
|
867
|
+
path: ["v2", "schedules"]
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
/**
|
|
871
|
+
* Delete a schedule
|
|
872
|
+
*/
|
|
873
|
+
async delete(scheduleId) {
|
|
874
|
+
return await this.http.request({
|
|
875
|
+
method: "DELETE",
|
|
876
|
+
path: ["v2", "schedules", scheduleId],
|
|
877
|
+
parseResponseAsJson: false
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
/**
|
|
881
|
+
* Pauses the schedule.
|
|
882
|
+
*
|
|
883
|
+
* A paused schedule will not deliver messages until
|
|
884
|
+
* it is resumed.
|
|
885
|
+
*/
|
|
886
|
+
async pause({ schedule }) {
|
|
887
|
+
await this.http.request({
|
|
888
|
+
method: "PATCH",
|
|
889
|
+
path: ["v2", "schedules", schedule, "pause"],
|
|
890
|
+
parseResponseAsJson: false
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
/**
|
|
894
|
+
* Resumes the schedule.
|
|
895
|
+
*/
|
|
896
|
+
async resume({ schedule }) {
|
|
897
|
+
await this.http.request({
|
|
898
|
+
method: "PATCH",
|
|
899
|
+
path: ["v2", "schedules", schedule, "resume"],
|
|
900
|
+
parseResponseAsJson: false
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
};
|
|
904
|
+
|
|
905
|
+
// src/client/url-groups.ts
|
|
906
|
+
var UrlGroups = class {
|
|
907
|
+
http;
|
|
908
|
+
constructor(http) {
|
|
909
|
+
this.http = http;
|
|
910
|
+
}
|
|
911
|
+
/**
|
|
912
|
+
* Create a new url group with the given name and endpoints
|
|
913
|
+
*/
|
|
914
|
+
async addEndpoints(request) {
|
|
915
|
+
await this.http.request({
|
|
916
|
+
method: "POST",
|
|
917
|
+
path: ["v2", "topics", request.name, "endpoints"],
|
|
918
|
+
headers: { "Content-Type": "application/json" },
|
|
919
|
+
body: JSON.stringify({ endpoints: request.endpoints }),
|
|
920
|
+
parseResponseAsJson: false
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
/**
|
|
924
|
+
* Remove endpoints from a url group.
|
|
925
|
+
*/
|
|
926
|
+
async removeEndpoints(request) {
|
|
927
|
+
await this.http.request({
|
|
928
|
+
method: "DELETE",
|
|
929
|
+
path: ["v2", "topics", request.name, "endpoints"],
|
|
930
|
+
headers: { "Content-Type": "application/json" },
|
|
931
|
+
body: JSON.stringify({ endpoints: request.endpoints }),
|
|
932
|
+
parseResponseAsJson: false
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
/**
|
|
936
|
+
* Get a list of all url groups.
|
|
937
|
+
*/
|
|
938
|
+
async list() {
|
|
939
|
+
return await this.http.request({
|
|
940
|
+
method: "GET",
|
|
941
|
+
path: ["v2", "topics"]
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
/**
|
|
945
|
+
* Get a single url group
|
|
946
|
+
*/
|
|
947
|
+
async get(name) {
|
|
948
|
+
return await this.http.request({
|
|
949
|
+
method: "GET",
|
|
950
|
+
path: ["v2", "topics", name]
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
/**
|
|
954
|
+
* Delete a url group
|
|
955
|
+
*/
|
|
956
|
+
async delete(name) {
|
|
957
|
+
return await this.http.request({
|
|
958
|
+
method: "DELETE",
|
|
959
|
+
path: ["v2", "topics", name],
|
|
960
|
+
parseResponseAsJson: false
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
};
|
|
964
|
+
|
|
965
|
+
// src/client/client.ts
|
|
966
|
+
var Client = class {
|
|
967
|
+
http;
|
|
968
|
+
token;
|
|
969
|
+
constructor(config) {
|
|
970
|
+
this.http = new HttpClient({
|
|
971
|
+
retry: config.retry,
|
|
972
|
+
baseUrl: config.baseUrl ? config.baseUrl.replace(/\/$/, "") : "https://qstash.upstash.io",
|
|
973
|
+
authorization: `Bearer ${config.token}`
|
|
974
|
+
});
|
|
975
|
+
this.token = config.token;
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
978
|
+
* Access the urlGroup API.
|
|
979
|
+
*
|
|
980
|
+
* Create, read, update or delete urlGroups.
|
|
981
|
+
*/
|
|
982
|
+
get urlGroups() {
|
|
983
|
+
return new UrlGroups(this.http);
|
|
984
|
+
}
|
|
985
|
+
/**
|
|
986
|
+
* Deprecated. Use urlGroups instead.
|
|
987
|
+
*
|
|
988
|
+
* Access the topic API.
|
|
989
|
+
*
|
|
990
|
+
* Create, read, update or delete topics.
|
|
991
|
+
*/
|
|
992
|
+
get topics() {
|
|
993
|
+
return this.urlGroups;
|
|
994
|
+
}
|
|
995
|
+
/**
|
|
996
|
+
* Access the dlq API.
|
|
997
|
+
*
|
|
998
|
+
* List or remove messages from the DLQ.
|
|
999
|
+
*/
|
|
1000
|
+
get dlq() {
|
|
1001
|
+
return new DLQ(this.http);
|
|
1002
|
+
}
|
|
1003
|
+
/**
|
|
1004
|
+
* Access the message API.
|
|
1005
|
+
*
|
|
1006
|
+
* Read or cancel messages.
|
|
1007
|
+
*/
|
|
1008
|
+
get messages() {
|
|
1009
|
+
return new Messages(this.http);
|
|
1010
|
+
}
|
|
1011
|
+
/**
|
|
1012
|
+
* Access the schedule API.
|
|
1013
|
+
*
|
|
1014
|
+
* Create, read or delete schedules.
|
|
1015
|
+
*/
|
|
1016
|
+
get schedules() {
|
|
1017
|
+
return new Schedules(this.http);
|
|
1018
|
+
}
|
|
1019
|
+
/**
|
|
1020
|
+
* Access the workflow API.
|
|
1021
|
+
*
|
|
1022
|
+
* cancel workflows.
|
|
1023
|
+
*/
|
|
1024
|
+
get workflow() {
|
|
1025
|
+
return new Workflow(this.http);
|
|
1026
|
+
}
|
|
1027
|
+
/**
|
|
1028
|
+
* Access the queue API.
|
|
1029
|
+
*
|
|
1030
|
+
* Create, read, update or delete queues.
|
|
1031
|
+
*/
|
|
1032
|
+
queue(request) {
|
|
1033
|
+
return new Queue(this.http, request?.queueName);
|
|
1034
|
+
}
|
|
1035
|
+
/**
|
|
1036
|
+
* Access the Chat API
|
|
1037
|
+
*
|
|
1038
|
+
* Call the create or prompt methods
|
|
1039
|
+
*/
|
|
1040
|
+
chat() {
|
|
1041
|
+
return new Chat(this.http, this.token);
|
|
1042
|
+
}
|
|
1043
|
+
async publish(request) {
|
|
1044
|
+
const headers = processHeaders(request);
|
|
1045
|
+
const response = await this.http.request({
|
|
1046
|
+
path: ["v2", "publish", getRequestPath(request)],
|
|
1047
|
+
body: request.body,
|
|
1048
|
+
headers,
|
|
1049
|
+
method: "POST"
|
|
1050
|
+
});
|
|
1051
|
+
return response;
|
|
1052
|
+
}
|
|
1053
|
+
/**
|
|
1054
|
+
* publishJSON is a utility wrapper around `publish` that automatically serializes the body
|
|
1055
|
+
* and sets the `Content-Type` header to `application/json`.
|
|
1056
|
+
*/
|
|
1057
|
+
async publishJSON(request) {
|
|
1058
|
+
const headers = prefixHeaders(new Headers(request.headers));
|
|
1059
|
+
headers.set("Content-Type", "application/json");
|
|
1060
|
+
ensureCallbackPresent(request);
|
|
1061
|
+
appendLLMOptionsIfNeeded(request, headers, this.http);
|
|
1062
|
+
const response = await this.publish({
|
|
1063
|
+
...request,
|
|
1064
|
+
headers,
|
|
1065
|
+
body: JSON.stringify(request.body)
|
|
1066
|
+
});
|
|
1067
|
+
return response;
|
|
1068
|
+
}
|
|
1069
|
+
/**
|
|
1070
|
+
* Batch publish messages to QStash.
|
|
1071
|
+
*/
|
|
1072
|
+
async batch(request) {
|
|
1073
|
+
const messages = [];
|
|
1074
|
+
for (const message of request) {
|
|
1075
|
+
const headers = processHeaders(message);
|
|
1076
|
+
const headerEntries = Object.fromEntries(headers.entries());
|
|
1077
|
+
messages.push({
|
|
1078
|
+
destination: getRequestPath(message),
|
|
1079
|
+
headers: headerEntries,
|
|
1080
|
+
body: message.body,
|
|
1081
|
+
...message.queueName && { queue: message.queueName }
|
|
1082
|
+
});
|
|
1083
|
+
}
|
|
1084
|
+
const response = await this.http.request({
|
|
1085
|
+
path: ["v2", "batch"],
|
|
1086
|
+
body: JSON.stringify(messages),
|
|
1087
|
+
headers: {
|
|
1088
|
+
"Content-Type": "application/json"
|
|
1089
|
+
},
|
|
1090
|
+
method: "POST"
|
|
1091
|
+
});
|
|
1092
|
+
const arrayResposne = Array.isArray(response) ? response : [response];
|
|
1093
|
+
return arrayResposne;
|
|
1094
|
+
}
|
|
1095
|
+
/**
|
|
1096
|
+
* Batch publish messages to QStash, serializing each body to JSON.
|
|
1097
|
+
*/
|
|
1098
|
+
async batchJSON(request) {
|
|
1099
|
+
for (const message of request) {
|
|
1100
|
+
if ("body" in message) {
|
|
1101
|
+
message.body = JSON.stringify(message.body);
|
|
1102
|
+
}
|
|
1103
|
+
message.headers = new Headers(message.headers);
|
|
1104
|
+
ensureCallbackPresent(message);
|
|
1105
|
+
appendLLMOptionsIfNeeded(message, message.headers, this.http);
|
|
1106
|
+
message.headers.set("Content-Type", "application/json");
|
|
1107
|
+
}
|
|
1108
|
+
const response = await this.batch(request);
|
|
1109
|
+
return response;
|
|
1110
|
+
}
|
|
1111
|
+
/**
|
|
1112
|
+
* Retrieve your logs.
|
|
1113
|
+
*
|
|
1114
|
+
* The logs endpoint is paginated and returns only 100 logs at a time.
|
|
1115
|
+
* If you want to receive more logs, you can use the cursor to paginate.
|
|
1116
|
+
*
|
|
1117
|
+
* The cursor is a unix timestamp with millisecond precision
|
|
1118
|
+
*
|
|
1119
|
+
* @example
|
|
1120
|
+
* ```ts
|
|
1121
|
+
* let cursor = Date.now()
|
|
1122
|
+
* const logs: Log[] = []
|
|
1123
|
+
* while (cursor > 0) {
|
|
1124
|
+
* const res = await qstash.logs({ cursor })
|
|
1125
|
+
* logs.push(...res.logs)
|
|
1126
|
+
* cursor = res.cursor ?? 0
|
|
1127
|
+
* }
|
|
1128
|
+
* ```
|
|
1129
|
+
*/
|
|
1130
|
+
async events(request) {
|
|
1131
|
+
const query = {};
|
|
1132
|
+
if (request?.cursor && request.cursor > 0) {
|
|
1133
|
+
query.cursor = request.cursor.toString();
|
|
1134
|
+
}
|
|
1135
|
+
for (const [key, value] of Object.entries(request?.filter ?? {})) {
|
|
1136
|
+
if (typeof value === "number" && value < 0) {
|
|
1137
|
+
continue;
|
|
1138
|
+
}
|
|
1139
|
+
if (key === "urlGroup") {
|
|
1140
|
+
query.topicName = value.toString();
|
|
1141
|
+
} else if (typeof value !== "undefined") {
|
|
1142
|
+
query[key] = value.toString();
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
const responsePayload = await this.http.request({
|
|
1146
|
+
path: ["v2", "events"],
|
|
1147
|
+
method: "GET",
|
|
1148
|
+
query
|
|
1149
|
+
});
|
|
1150
|
+
return {
|
|
1151
|
+
cursor: responsePayload.cursor,
|
|
1152
|
+
events: responsePayload.events.map((event) => {
|
|
1153
|
+
return {
|
|
1154
|
+
...event,
|
|
1155
|
+
urlGroup: event.topicName
|
|
1156
|
+
};
|
|
1157
|
+
})
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
|
+
};
|
|
1161
|
+
|
|
1162
|
+
// node_modules/neverthrow/dist/index.es.js
|
|
1163
|
+
var defaultErrorConfig = {
|
|
1164
|
+
withStackTrace: false
|
|
1165
|
+
};
|
|
1166
|
+
var createNeverThrowError = (message, result, config = defaultErrorConfig) => {
|
|
1167
|
+
const data = result.isOk() ? { type: "Ok", value: result.value } : { type: "Err", value: result.error };
|
|
1168
|
+
const maybeStack = config.withStackTrace ? new Error().stack : void 0;
|
|
1169
|
+
return {
|
|
1170
|
+
data,
|
|
1171
|
+
message,
|
|
1172
|
+
stack: maybeStack
|
|
1173
|
+
};
|
|
1174
|
+
};
|
|
1175
|
+
function __awaiter(thisArg, _arguments, P, generator) {
|
|
1176
|
+
function adopt(value) {
|
|
1177
|
+
return value instanceof P ? value : new P(function(resolve) {
|
|
1178
|
+
resolve(value);
|
|
1179
|
+
});
|
|
1180
|
+
}
|
|
1181
|
+
return new (P || (P = Promise))(function(resolve, reject) {
|
|
1182
|
+
function fulfilled(value) {
|
|
1183
|
+
try {
|
|
1184
|
+
step(generator.next(value));
|
|
1185
|
+
} catch (e) {
|
|
1186
|
+
reject(e);
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
function rejected(value) {
|
|
1190
|
+
try {
|
|
1191
|
+
step(generator["throw"](value));
|
|
1192
|
+
} catch (e) {
|
|
1193
|
+
reject(e);
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
function step(result) {
|
|
1197
|
+
result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
|
|
1198
|
+
}
|
|
1199
|
+
step((generator = generator.apply(thisArg, [])).next());
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
function __values(o) {
|
|
1203
|
+
var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0;
|
|
1204
|
+
if (m)
|
|
1205
|
+
return m.call(o);
|
|
1206
|
+
if (o && typeof o.length === "number")
|
|
1207
|
+
return {
|
|
1208
|
+
next: function() {
|
|
1209
|
+
if (o && i >= o.length)
|
|
1210
|
+
o = void 0;
|
|
1211
|
+
return { value: o && o[i++], done: !o };
|
|
1212
|
+
}
|
|
1213
|
+
};
|
|
1214
|
+
throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
|
|
1215
|
+
}
|
|
1216
|
+
function __await(v) {
|
|
1217
|
+
return this instanceof __await ? (this.v = v, this) : new __await(v);
|
|
1218
|
+
}
|
|
1219
|
+
function __asyncGenerator(thisArg, _arguments, generator) {
|
|
1220
|
+
if (!Symbol.asyncIterator)
|
|
1221
|
+
throw new TypeError("Symbol.asyncIterator is not defined.");
|
|
1222
|
+
var g = generator.apply(thisArg, _arguments || []), i, q = [];
|
|
1223
|
+
return i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function() {
|
|
1224
|
+
return this;
|
|
1225
|
+
}, i;
|
|
1226
|
+
function verb(n) {
|
|
1227
|
+
if (g[n])
|
|
1228
|
+
i[n] = function(v) {
|
|
1229
|
+
return new Promise(function(a, b) {
|
|
1230
|
+
q.push([n, v, a, b]) > 1 || resume(n, v);
|
|
1231
|
+
});
|
|
1232
|
+
};
|
|
1233
|
+
}
|
|
1234
|
+
function resume(n, v) {
|
|
1235
|
+
try {
|
|
1236
|
+
step(g[n](v));
|
|
1237
|
+
} catch (e) {
|
|
1238
|
+
settle(q[0][3], e);
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
function step(r) {
|
|
1242
|
+
r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r);
|
|
1243
|
+
}
|
|
1244
|
+
function fulfill(value) {
|
|
1245
|
+
resume("next", value);
|
|
1246
|
+
}
|
|
1247
|
+
function reject(value) {
|
|
1248
|
+
resume("throw", value);
|
|
1249
|
+
}
|
|
1250
|
+
function settle(f, v) {
|
|
1251
|
+
if (f(v), q.shift(), q.length)
|
|
1252
|
+
resume(q[0][0], q[0][1]);
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
function __asyncDelegator(o) {
|
|
1256
|
+
var i, p;
|
|
1257
|
+
return i = {}, verb("next"), verb("throw", function(e) {
|
|
1258
|
+
throw e;
|
|
1259
|
+
}), verb("return"), i[Symbol.iterator] = function() {
|
|
1260
|
+
return this;
|
|
1261
|
+
}, i;
|
|
1262
|
+
function verb(n, f) {
|
|
1263
|
+
i[n] = o[n] ? function(v) {
|
|
1264
|
+
return (p = !p) ? { value: __await(o[n](v)), done: n === "return" } : f ? f(v) : v;
|
|
1265
|
+
} : f;
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
function __asyncValues(o) {
|
|
1269
|
+
if (!Symbol.asyncIterator)
|
|
1270
|
+
throw new TypeError("Symbol.asyncIterator is not defined.");
|
|
1271
|
+
var m = o[Symbol.asyncIterator], i;
|
|
1272
|
+
return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function() {
|
|
1273
|
+
return this;
|
|
1274
|
+
}, i);
|
|
1275
|
+
function verb(n) {
|
|
1276
|
+
i[n] = o[n] && function(v) {
|
|
1277
|
+
return new Promise(function(resolve, reject) {
|
|
1278
|
+
v = o[n](v), settle(resolve, reject, v.done, v.value);
|
|
1279
|
+
});
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
1282
|
+
function settle(resolve, reject, d, v) {
|
|
1283
|
+
Promise.resolve(v).then(function(v2) {
|
|
1284
|
+
resolve({ value: v2, done: d });
|
|
1285
|
+
}, reject);
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
var ResultAsync = class _ResultAsync {
|
|
1289
|
+
constructor(res) {
|
|
1290
|
+
this._promise = res;
|
|
1291
|
+
}
|
|
1292
|
+
static fromSafePromise(promise) {
|
|
1293
|
+
const newPromise = promise.then((value) => new Ok(value));
|
|
1294
|
+
return new _ResultAsync(newPromise);
|
|
1295
|
+
}
|
|
1296
|
+
static fromPromise(promise, errorFn) {
|
|
1297
|
+
const newPromise = promise.then((value) => new Ok(value)).catch((e) => new Err(errorFn(e)));
|
|
1298
|
+
return new _ResultAsync(newPromise);
|
|
1299
|
+
}
|
|
1300
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1301
|
+
static fromThrowable(fn, errorFn) {
|
|
1302
|
+
return (...args) => {
|
|
1303
|
+
return new _ResultAsync((() => __awaiter(this, void 0, void 0, function* () {
|
|
1304
|
+
try {
|
|
1305
|
+
return new Ok(yield fn(...args));
|
|
1306
|
+
} catch (error) {
|
|
1307
|
+
return new Err(errorFn ? errorFn(error) : error);
|
|
1308
|
+
}
|
|
1309
|
+
}))());
|
|
1310
|
+
};
|
|
1311
|
+
}
|
|
1312
|
+
static combine(asyncResultList) {
|
|
1313
|
+
return combineResultAsyncList(asyncResultList);
|
|
1314
|
+
}
|
|
1315
|
+
static combineWithAllErrors(asyncResultList) {
|
|
1316
|
+
return combineResultAsyncListWithAllErrors(asyncResultList);
|
|
1317
|
+
}
|
|
1318
|
+
map(f) {
|
|
1319
|
+
return new _ResultAsync(this._promise.then((res) => __awaiter(this, void 0, void 0, function* () {
|
|
1320
|
+
if (res.isErr()) {
|
|
1321
|
+
return new Err(res.error);
|
|
1322
|
+
}
|
|
1323
|
+
return new Ok(yield f(res.value));
|
|
1324
|
+
})));
|
|
1325
|
+
}
|
|
1326
|
+
mapErr(f) {
|
|
1327
|
+
return new _ResultAsync(this._promise.then((res) => __awaiter(this, void 0, void 0, function* () {
|
|
1328
|
+
if (res.isOk()) {
|
|
1329
|
+
return new Ok(res.value);
|
|
1330
|
+
}
|
|
1331
|
+
return new Err(yield f(res.error));
|
|
1332
|
+
})));
|
|
1333
|
+
}
|
|
1334
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types
|
|
1335
|
+
andThen(f) {
|
|
1336
|
+
return new _ResultAsync(this._promise.then((res) => {
|
|
1337
|
+
if (res.isErr()) {
|
|
1338
|
+
return new Err(res.error);
|
|
1339
|
+
}
|
|
1340
|
+
const newValue = f(res.value);
|
|
1341
|
+
return newValue instanceof _ResultAsync ? newValue._promise : newValue;
|
|
1342
|
+
}));
|
|
1343
|
+
}
|
|
1344
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types
|
|
1345
|
+
orElse(f) {
|
|
1346
|
+
return new _ResultAsync(this._promise.then((res) => __awaiter(this, void 0, void 0, function* () {
|
|
1347
|
+
if (res.isErr()) {
|
|
1348
|
+
return f(res.error);
|
|
1349
|
+
}
|
|
1350
|
+
return new Ok(res.value);
|
|
1351
|
+
})));
|
|
1352
|
+
}
|
|
1353
|
+
match(ok2, _err) {
|
|
1354
|
+
return this._promise.then((res) => res.match(ok2, _err));
|
|
1355
|
+
}
|
|
1356
|
+
unwrapOr(t) {
|
|
1357
|
+
return this._promise.then((res) => res.unwrapOr(t));
|
|
1358
|
+
}
|
|
1359
|
+
/**
|
|
1360
|
+
* Emulates Rust's `?` operator in `safeTry`'s body. See also `safeTry`.
|
|
1361
|
+
*/
|
|
1362
|
+
safeUnwrap() {
|
|
1363
|
+
return __asyncGenerator(this, arguments, function* safeUnwrap_1() {
|
|
1364
|
+
return yield __await(yield __await(yield* __asyncDelegator(__asyncValues(yield __await(this._promise.then((res) => res.safeUnwrap()))))));
|
|
1365
|
+
});
|
|
1366
|
+
}
|
|
1367
|
+
// Makes ResultAsync implement PromiseLike<Result>
|
|
1368
|
+
then(successCallback, failureCallback) {
|
|
1369
|
+
return this._promise.then(successCallback, failureCallback);
|
|
1370
|
+
}
|
|
1371
|
+
};
|
|
1372
|
+
var errAsync = (err2) => new ResultAsync(Promise.resolve(new Err(err2)));
|
|
1373
|
+
var fromPromise = ResultAsync.fromPromise;
|
|
1374
|
+
var fromSafePromise = ResultAsync.fromSafePromise;
|
|
1375
|
+
var fromAsyncThrowable = ResultAsync.fromThrowable;
|
|
1376
|
+
var combineResultList = (resultList) => {
|
|
1377
|
+
let acc = ok([]);
|
|
1378
|
+
for (const result of resultList) {
|
|
1379
|
+
if (result.isErr()) {
|
|
1380
|
+
acc = err(result.error);
|
|
1381
|
+
break;
|
|
1382
|
+
} else {
|
|
1383
|
+
acc.map((list) => list.push(result.value));
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
return acc;
|
|
1387
|
+
};
|
|
1388
|
+
var combineResultAsyncList = (asyncResultList) => ResultAsync.fromSafePromise(Promise.all(asyncResultList)).andThen(combineResultList);
|
|
1389
|
+
var combineResultListWithAllErrors = (resultList) => {
|
|
1390
|
+
let acc = ok([]);
|
|
1391
|
+
for (const result of resultList) {
|
|
1392
|
+
if (result.isErr() && acc.isErr()) {
|
|
1393
|
+
acc.error.push(result.error);
|
|
1394
|
+
} else if (result.isErr() && acc.isOk()) {
|
|
1395
|
+
acc = err([result.error]);
|
|
1396
|
+
} else if (result.isOk() && acc.isOk()) {
|
|
1397
|
+
acc.value.push(result.value);
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
return acc;
|
|
1401
|
+
};
|
|
1402
|
+
var combineResultAsyncListWithAllErrors = (asyncResultList) => ResultAsync.fromSafePromise(Promise.all(asyncResultList)).andThen(combineResultListWithAllErrors);
|
|
1403
|
+
var Result;
|
|
1404
|
+
(function(Result2) {
|
|
1405
|
+
function fromThrowable2(fn, errorFn) {
|
|
1406
|
+
return (...args) => {
|
|
1407
|
+
try {
|
|
1408
|
+
const result = fn(...args);
|
|
1409
|
+
return ok(result);
|
|
1410
|
+
} catch (e) {
|
|
1411
|
+
return err(errorFn ? errorFn(e) : e);
|
|
1412
|
+
}
|
|
1413
|
+
};
|
|
1414
|
+
}
|
|
1415
|
+
Result2.fromThrowable = fromThrowable2;
|
|
1416
|
+
function combine(resultList) {
|
|
1417
|
+
return combineResultList(resultList);
|
|
1418
|
+
}
|
|
1419
|
+
Result2.combine = combine;
|
|
1420
|
+
function combineWithAllErrors(resultList) {
|
|
1421
|
+
return combineResultListWithAllErrors(resultList);
|
|
1422
|
+
}
|
|
1423
|
+
Result2.combineWithAllErrors = combineWithAllErrors;
|
|
1424
|
+
})(Result || (Result = {}));
|
|
1425
|
+
var ok = (value) => new Ok(value);
|
|
1426
|
+
var err = (err2) => new Err(err2);
|
|
1427
|
+
var Ok = class {
|
|
1428
|
+
constructor(value) {
|
|
1429
|
+
this.value = value;
|
|
1430
|
+
}
|
|
1431
|
+
isOk() {
|
|
1432
|
+
return true;
|
|
1433
|
+
}
|
|
1434
|
+
isErr() {
|
|
1435
|
+
return !this.isOk();
|
|
1436
|
+
}
|
|
1437
|
+
map(f) {
|
|
1438
|
+
return ok(f(this.value));
|
|
1439
|
+
}
|
|
1440
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
1441
|
+
mapErr(_f) {
|
|
1442
|
+
return ok(this.value);
|
|
1443
|
+
}
|
|
1444
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types
|
|
1445
|
+
andThen(f) {
|
|
1446
|
+
return f(this.value);
|
|
1447
|
+
}
|
|
1448
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types
|
|
1449
|
+
orElse(_f) {
|
|
1450
|
+
return ok(this.value);
|
|
1451
|
+
}
|
|
1452
|
+
asyncAndThen(f) {
|
|
1453
|
+
return f(this.value);
|
|
1454
|
+
}
|
|
1455
|
+
asyncMap(f) {
|
|
1456
|
+
return ResultAsync.fromSafePromise(f(this.value));
|
|
1457
|
+
}
|
|
1458
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
1459
|
+
unwrapOr(_v) {
|
|
1460
|
+
return this.value;
|
|
1461
|
+
}
|
|
1462
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
1463
|
+
match(ok2, _err) {
|
|
1464
|
+
return ok2(this.value);
|
|
1465
|
+
}
|
|
1466
|
+
safeUnwrap() {
|
|
1467
|
+
const value = this.value;
|
|
1468
|
+
return function* () {
|
|
1469
|
+
return value;
|
|
1470
|
+
}();
|
|
1471
|
+
}
|
|
1472
|
+
_unsafeUnwrap(_) {
|
|
1473
|
+
return this.value;
|
|
1474
|
+
}
|
|
1475
|
+
_unsafeUnwrapErr(config) {
|
|
1476
|
+
throw createNeverThrowError("Called `_unsafeUnwrapErr` on an Ok", this, config);
|
|
1477
|
+
}
|
|
1478
|
+
};
|
|
1479
|
+
var Err = class {
|
|
1480
|
+
constructor(error) {
|
|
1481
|
+
this.error = error;
|
|
1482
|
+
}
|
|
1483
|
+
isOk() {
|
|
1484
|
+
return false;
|
|
1485
|
+
}
|
|
1486
|
+
isErr() {
|
|
1487
|
+
return !this.isOk();
|
|
1488
|
+
}
|
|
1489
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
1490
|
+
map(_f) {
|
|
1491
|
+
return err(this.error);
|
|
1492
|
+
}
|
|
1493
|
+
mapErr(f) {
|
|
1494
|
+
return err(f(this.error));
|
|
1495
|
+
}
|
|
1496
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types
|
|
1497
|
+
andThen(_f) {
|
|
1498
|
+
return err(this.error);
|
|
1499
|
+
}
|
|
1500
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types
|
|
1501
|
+
orElse(f) {
|
|
1502
|
+
return f(this.error);
|
|
1503
|
+
}
|
|
1504
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
1505
|
+
asyncAndThen(_f) {
|
|
1506
|
+
return errAsync(this.error);
|
|
1507
|
+
}
|
|
1508
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
1509
|
+
asyncMap(_f) {
|
|
1510
|
+
return errAsync(this.error);
|
|
1511
|
+
}
|
|
1512
|
+
unwrapOr(v) {
|
|
1513
|
+
return v;
|
|
1514
|
+
}
|
|
1515
|
+
match(_ok, err2) {
|
|
1516
|
+
return err2(this.error);
|
|
1517
|
+
}
|
|
1518
|
+
safeUnwrap() {
|
|
1519
|
+
const error = this.error;
|
|
1520
|
+
return function* () {
|
|
1521
|
+
yield err(error);
|
|
1522
|
+
throw new Error("Do not use this generator out of `safeTry`");
|
|
1523
|
+
}();
|
|
1524
|
+
}
|
|
1525
|
+
_unsafeUnwrap(config) {
|
|
1526
|
+
throw createNeverThrowError("Called `_unsafeUnwrap` on an Err", this, config);
|
|
1527
|
+
}
|
|
1528
|
+
_unsafeUnwrapErr(_) {
|
|
1529
|
+
return this.error;
|
|
1530
|
+
}
|
|
1531
|
+
};
|
|
1532
|
+
var fromThrowable = Result.fromThrowable;
|
|
1533
|
+
|
|
1534
|
+
// src/client/workflow/constants.ts
|
|
1535
|
+
var WORKFLOW_ID_HEADER = "Upstash-Workflow-RunId";
|
|
1536
|
+
var WORKFLOW_INIT_HEADER = "Upstash-Workflow-Init";
|
|
1537
|
+
var WORKFLOW_URL_HEADER = "Upstash-Workflow-Url";
|
|
1538
|
+
var WORKFLOW_FAILURE_HEADER = "Upstash-Workflow-Is-Failure";
|
|
1539
|
+
var WORKFLOW_PROTOCOL_VERSION = "1";
|
|
1540
|
+
var WORKFLOW_PROTOCOL_VERSION_HEADER = "Upstash-Workflow-Sdk-Version";
|
|
1541
|
+
var DEFAULT_CONTENT_TYPE = "application/json";
|
|
1542
|
+
var NO_CONCURRENCY = 1;
|
|
1543
|
+
|
|
1544
|
+
// src/client/workflow/types.ts
|
|
1545
|
+
var StepTypes = ["Initial", "Run", "SleepFor", "SleepUntil", "Call"];
|
|
1546
|
+
|
|
1547
|
+
// src/client/workflow/workflow-requests.ts
|
|
1548
|
+
var triggerFirstInvocation = async (workflowContext, debug) => {
|
|
1549
|
+
const headers = getHeaders(
|
|
1550
|
+
"true",
|
|
1551
|
+
workflowContext.workflowRunId,
|
|
1552
|
+
workflowContext.url,
|
|
1553
|
+
workflowContext.headers,
|
|
1554
|
+
void 0,
|
|
1555
|
+
workflowContext.failureUrl
|
|
1556
|
+
);
|
|
1557
|
+
await debug?.log("SUBMIT", "SUBMIT_FIRST_INVOCATION", {
|
|
1558
|
+
headers,
|
|
1559
|
+
requestPayload: workflowContext.requestPayload,
|
|
1560
|
+
url: workflowContext.url
|
|
1561
|
+
});
|
|
1562
|
+
try {
|
|
1563
|
+
await workflowContext.qstashClient.publishJSON({
|
|
1564
|
+
headers,
|
|
1565
|
+
method: "POST",
|
|
1566
|
+
body: workflowContext.requestPayload,
|
|
1567
|
+
url: workflowContext.url
|
|
1568
|
+
});
|
|
1569
|
+
return ok("success");
|
|
1570
|
+
} catch (error) {
|
|
1571
|
+
const error_ = error;
|
|
1572
|
+
return err(error_);
|
|
1573
|
+
}
|
|
1574
|
+
};
|
|
1575
|
+
var triggerRouteFunction = async ({
|
|
1576
|
+
onCleanup,
|
|
1577
|
+
onStep
|
|
1578
|
+
}) => {
|
|
1579
|
+
try {
|
|
1580
|
+
await onStep();
|
|
1581
|
+
await onCleanup();
|
|
1582
|
+
return ok("workflow-finished");
|
|
1583
|
+
} catch (error) {
|
|
1584
|
+
const error_ = error;
|
|
1585
|
+
return error_ instanceof QStashWorkflowAbort ? ok("step-finished") : err(error_);
|
|
1586
|
+
}
|
|
1587
|
+
};
|
|
1588
|
+
var triggerWorkflowDelete = async (workflowContext, debug, cancel = false) => {
|
|
1589
|
+
await debug?.log("SUBMIT", "SUBMIT_CLEANUP", {
|
|
1590
|
+
deletedWorkflowRunId: workflowContext.workflowRunId
|
|
1591
|
+
});
|
|
1592
|
+
const result = await workflowContext.qstashClient.http.request({
|
|
1593
|
+
path: ["v2", "workflows", "runs", `${workflowContext.workflowRunId}?cancel=${cancel}`],
|
|
1594
|
+
method: "DELETE",
|
|
1595
|
+
parseResponseAsJson: false
|
|
1596
|
+
});
|
|
1597
|
+
await debug?.log("SUBMIT", "SUBMIT_CLEANUP", result);
|
|
1598
|
+
};
|
|
1599
|
+
var recreateUserHeaders = (headers) => {
|
|
1600
|
+
const filteredHeaders = new Headers();
|
|
1601
|
+
const pairs = headers.entries();
|
|
1602
|
+
for (const [header, value] of pairs) {
|
|
1603
|
+
const headerLowerCase = header.toLowerCase();
|
|
1604
|
+
if (!headerLowerCase.startsWith("upstash-workflow-") && !headerLowerCase.startsWith("x-vercel-") && !headerLowerCase.startsWith("x-forwarded-") && headerLowerCase !== "cf-connecting-ip") {
|
|
1605
|
+
filteredHeaders.append(header, value);
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
return filteredHeaders;
|
|
1609
|
+
};
|
|
1610
|
+
var handleThirdPartyCallResult = async (request, requestPayload, client, workflowUrl, failureUrl, debug) => {
|
|
1611
|
+
try {
|
|
1612
|
+
if (request.headers.get("Upstash-Workflow-Callback")) {
|
|
1613
|
+
const callbackMessage = JSON.parse(requestPayload);
|
|
1614
|
+
if (!(callbackMessage.status >= 200 && callbackMessage.status < 300)) {
|
|
1615
|
+
await debug?.log("WARN", "SUBMIT_THIRD_PARTY_RESULT", {
|
|
1616
|
+
status: callbackMessage.status,
|
|
1617
|
+
body: atob(callbackMessage.body)
|
|
1618
|
+
});
|
|
1619
|
+
console.warn(
|
|
1620
|
+
`Workflow Warning: "context.call" failed with status ${callbackMessage.status} and will retry (if there are retries remaining). Error Message:
|
|
1621
|
+
${atob(callbackMessage.body)}`
|
|
1622
|
+
);
|
|
1623
|
+
return ok("call-will-retry");
|
|
1624
|
+
}
|
|
1625
|
+
const workflowRunId = request.headers.get(WORKFLOW_ID_HEADER);
|
|
1626
|
+
const stepIdString = request.headers.get("Upstash-Workflow-StepId");
|
|
1627
|
+
const stepName = request.headers.get("Upstash-Workflow-StepName");
|
|
1628
|
+
const stepType = request.headers.get("Upstash-Workflow-StepType");
|
|
1629
|
+
const concurrentString = request.headers.get("Upstash-Workflow-Concurrent");
|
|
1630
|
+
const contentType = request.headers.get("Upstash-Workflow-ContentType");
|
|
1631
|
+
if (!(workflowRunId && stepIdString && stepName && StepTypes.includes(stepType) && concurrentString && contentType)) {
|
|
1632
|
+
throw new Error(
|
|
1633
|
+
`Missing info in callback message source header: ${JSON.stringify({
|
|
1634
|
+
workflowRunId,
|
|
1635
|
+
stepIdString,
|
|
1636
|
+
stepName,
|
|
1637
|
+
stepType,
|
|
1638
|
+
concurrentString,
|
|
1639
|
+
contentType
|
|
1640
|
+
})}`
|
|
1641
|
+
);
|
|
1642
|
+
}
|
|
1643
|
+
const userHeaders = recreateUserHeaders(request.headers);
|
|
1644
|
+
const requestHeaders = getHeaders(
|
|
1645
|
+
"false",
|
|
1646
|
+
workflowRunId,
|
|
1647
|
+
workflowUrl,
|
|
1648
|
+
userHeaders,
|
|
1649
|
+
void 0,
|
|
1650
|
+
failureUrl
|
|
1651
|
+
);
|
|
1652
|
+
const callResultStep = {
|
|
1653
|
+
stepId: Number(stepIdString),
|
|
1654
|
+
stepName,
|
|
1655
|
+
stepType,
|
|
1656
|
+
out: atob(callbackMessage.body),
|
|
1657
|
+
concurrent: Number(concurrentString)
|
|
1658
|
+
};
|
|
1659
|
+
await debug?.log("SUBMIT", "SUBMIT_THIRD_PARTY_RESULT", {
|
|
1660
|
+
step: callResultStep,
|
|
1661
|
+
headers: requestHeaders,
|
|
1662
|
+
url: workflowUrl
|
|
1663
|
+
});
|
|
1664
|
+
const result = await client.publishJSON({
|
|
1665
|
+
headers: requestHeaders,
|
|
1666
|
+
method: "POST",
|
|
1667
|
+
body: callResultStep,
|
|
1668
|
+
url: workflowUrl
|
|
1669
|
+
});
|
|
1670
|
+
await debug?.log("SUBMIT", "SUBMIT_THIRD_PARTY_RESULT", {
|
|
1671
|
+
messageId: result.messageId
|
|
1672
|
+
});
|
|
1673
|
+
return ok("is-call-return");
|
|
1674
|
+
} else {
|
|
1675
|
+
return ok("continue-workflow");
|
|
1676
|
+
}
|
|
1677
|
+
} catch (error) {
|
|
1678
|
+
const isCallReturn = request.headers.get("Upstash-Workflow-Callback");
|
|
1679
|
+
return err(
|
|
1680
|
+
new QStashWorkflowError(
|
|
1681
|
+
`Error when handling call return (isCallReturn=${isCallReturn}): ${error}`
|
|
1682
|
+
)
|
|
1683
|
+
);
|
|
1684
|
+
}
|
|
1685
|
+
};
|
|
1686
|
+
var getHeaders = (initHeaderValue, workflowRunId, workflowUrl, userHeaders, step, failureUrl) => {
|
|
1687
|
+
const baseHeaders = {
|
|
1688
|
+
[WORKFLOW_INIT_HEADER]: initHeaderValue,
|
|
1689
|
+
[WORKFLOW_ID_HEADER]: workflowRunId,
|
|
1690
|
+
[WORKFLOW_URL_HEADER]: workflowUrl,
|
|
1691
|
+
[`Upstash-Forward-${WORKFLOW_PROTOCOL_VERSION_HEADER}`]: WORKFLOW_PROTOCOL_VERSION,
|
|
1692
|
+
...failureUrl ? {
|
|
1693
|
+
[`Upstash-Failure-Callback-Forward-${WORKFLOW_FAILURE_HEADER}`]: "true",
|
|
1694
|
+
"Upstash-Failure-Callback": failureUrl
|
|
1695
|
+
} : {}
|
|
1696
|
+
};
|
|
1697
|
+
if (userHeaders) {
|
|
1698
|
+
for (const header of userHeaders.keys()) {
|
|
1699
|
+
if (step?.callHeaders) {
|
|
1700
|
+
baseHeaders[`Upstash-Callback-Forward-${header}`] = userHeaders.get(header);
|
|
1701
|
+
} else {
|
|
1702
|
+
baseHeaders[`Upstash-Forward-${header}`] = userHeaders.get(header);
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
if (step?.callHeaders) {
|
|
1707
|
+
const forwardedHeaders = Object.fromEntries(
|
|
1708
|
+
Object.entries(step.callHeaders).map(([header, value]) => [
|
|
1709
|
+
`Upstash-Forward-${header}`,
|
|
1710
|
+
value
|
|
1711
|
+
])
|
|
1712
|
+
);
|
|
1713
|
+
const contentType = step.callHeaders["Content-Type"];
|
|
1714
|
+
return {
|
|
1715
|
+
...baseHeaders,
|
|
1716
|
+
...forwardedHeaders,
|
|
1717
|
+
"Upstash-Callback": workflowUrl,
|
|
1718
|
+
"Upstash-Callback-Workflow-RunId": workflowRunId,
|
|
1719
|
+
"Upstash-Callback-Workflow-CallType": "fromCallback",
|
|
1720
|
+
"Upstash-Callback-Workflow-Init": "false",
|
|
1721
|
+
"Upstash-Callback-Workflow-Url": workflowUrl,
|
|
1722
|
+
"Upstash-Callback-Forward-Upstash-Workflow-Callback": "true",
|
|
1723
|
+
"Upstash-Callback-Forward-Upstash-Workflow-StepId": step.stepId.toString(),
|
|
1724
|
+
"Upstash-Callback-Forward-Upstash-Workflow-StepName": step.stepName,
|
|
1725
|
+
"Upstash-Callback-Forward-Upstash-Workflow-StepType": step.stepType,
|
|
1726
|
+
"Upstash-Callback-Forward-Upstash-Workflow-Concurrent": step.concurrent.toString(),
|
|
1727
|
+
"Upstash-Callback-Forward-Upstash-Workflow-ContentType": contentType ?? DEFAULT_CONTENT_TYPE,
|
|
1728
|
+
"Upstash-Workflow-CallType": "toCallback"
|
|
1729
|
+
};
|
|
1730
|
+
}
|
|
1731
|
+
return baseHeaders;
|
|
1732
|
+
};
|
|
1733
|
+
var verifyRequest = async (body, signature, verifier) => {
|
|
1734
|
+
if (!verifier) {
|
|
1735
|
+
return;
|
|
1736
|
+
}
|
|
1737
|
+
try {
|
|
1738
|
+
if (!signature) {
|
|
1739
|
+
throw new Error("`Upstash-Signature` header is not passed.");
|
|
1740
|
+
}
|
|
1741
|
+
const isValid = await verifier.verify({
|
|
1742
|
+
body,
|
|
1743
|
+
signature
|
|
1744
|
+
});
|
|
1745
|
+
if (!isValid) {
|
|
1746
|
+
throw new Error("Signature in `Upstash-Signature` header is not valid");
|
|
1747
|
+
}
|
|
1748
|
+
} catch (error) {
|
|
1749
|
+
throw new QStashWorkflowError(
|
|
1750
|
+
`Failed to verify that the Workflow request comes from QStash: ${error}
|
|
1751
|
+
|
|
1752
|
+
If signature is missing, trigger the workflow endpoint by publishing your request to QStash instead of calling it directly.
|
|
1753
|
+
|
|
1754
|
+
If you want to disable QStash Verification, you should clear env variables QSTASH_CURRENT_SIGNING_KEY and QSTASH_NEXT_SIGNING_KEY`
|
|
1755
|
+
);
|
|
1756
|
+
}
|
|
1757
|
+
};
|
|
1758
|
+
|
|
1759
|
+
// src/client/workflow/auto-executor.ts
|
|
1760
|
+
var AutoExecutor = class _AutoExecutor {
|
|
1761
|
+
context;
|
|
1762
|
+
promises = /* @__PURE__ */ new WeakMap();
|
|
1763
|
+
activeLazyStepList;
|
|
1764
|
+
debug;
|
|
1765
|
+
nonPlanStepCount;
|
|
1766
|
+
steps;
|
|
1767
|
+
indexInCurrentList = 0;
|
|
1768
|
+
stepCount = 0;
|
|
1769
|
+
planStepCount = 0;
|
|
1770
|
+
executingStep = false;
|
|
1771
|
+
constructor(context, steps, debug) {
|
|
1772
|
+
this.context = context;
|
|
1773
|
+
this.debug = debug;
|
|
1774
|
+
this.steps = steps;
|
|
1775
|
+
this.nonPlanStepCount = this.steps.filter((step) => !step.targetStep).length;
|
|
1776
|
+
}
|
|
1777
|
+
/**
|
|
1778
|
+
* Adds the step function to the list of step functions to run in
|
|
1779
|
+
* parallel. After adding the function, defers the execution, so
|
|
1780
|
+
* that if there is another step function to be added, it's also
|
|
1781
|
+
* added.
|
|
1782
|
+
*
|
|
1783
|
+
* After all functions are added, list of functions are executed.
|
|
1784
|
+
* If there is a single function, it's executed by itself. If there
|
|
1785
|
+
* are multiple, they are run in parallel.
|
|
1786
|
+
*
|
|
1787
|
+
* If a function is already executing (this.executingStep), this
|
|
1788
|
+
* means that there is a nested step which is not allowed. In this
|
|
1789
|
+
* case, addStep throws QStashWorkflowError.
|
|
1790
|
+
*
|
|
1791
|
+
* @param stepInfo step plan to add
|
|
1792
|
+
* @returns result of the step function
|
|
1793
|
+
*/
|
|
1794
|
+
async addStep(stepInfo) {
|
|
1795
|
+
if (this.executingStep) {
|
|
1796
|
+
throw new QStashWorkflowError(
|
|
1797
|
+
`A step can not be run inside another step. Tried to run '${stepInfo.stepName}' inside '${this.executingStep}'`
|
|
1798
|
+
);
|
|
1799
|
+
}
|
|
1800
|
+
this.stepCount += 1;
|
|
1801
|
+
const lazyStepList = this.activeLazyStepList ?? [];
|
|
1802
|
+
if (!this.activeLazyStepList) {
|
|
1803
|
+
this.activeLazyStepList = lazyStepList;
|
|
1804
|
+
this.indexInCurrentList = 0;
|
|
1805
|
+
}
|
|
1806
|
+
lazyStepList.push(stepInfo);
|
|
1807
|
+
const index = this.indexInCurrentList++;
|
|
1808
|
+
const requestComplete = this.deferExecution().then(async () => {
|
|
1809
|
+
if (!this.promises.has(lazyStepList)) {
|
|
1810
|
+
const promise2 = this.getExecutionPromise(lazyStepList);
|
|
1811
|
+
this.promises.set(lazyStepList, promise2);
|
|
1812
|
+
this.activeLazyStepList = void 0;
|
|
1813
|
+
this.planStepCount += lazyStepList.length > 1 ? lazyStepList.length : 0;
|
|
1814
|
+
}
|
|
1815
|
+
const promise = this.promises.get(lazyStepList);
|
|
1816
|
+
return promise;
|
|
1817
|
+
});
|
|
1818
|
+
const result = await requestComplete;
|
|
1819
|
+
return _AutoExecutor.getResult(lazyStepList, result, index);
|
|
1820
|
+
}
|
|
1821
|
+
/**
|
|
1822
|
+
* Wraps a step function to set this.executingStep to step name
|
|
1823
|
+
* before running and set this.executingStep to False after execution
|
|
1824
|
+
* ends.
|
|
1825
|
+
*
|
|
1826
|
+
* this.executingStep allows us to detect nested steps which are not
|
|
1827
|
+
* allowed.
|
|
1828
|
+
*
|
|
1829
|
+
* @param stepName name of the step being wrapped
|
|
1830
|
+
* @param stepFunction step function to wrap
|
|
1831
|
+
* @returns wrapped step function
|
|
1832
|
+
*/
|
|
1833
|
+
wrapStep(stepName, stepFunction) {
|
|
1834
|
+
this.executingStep = stepName;
|
|
1835
|
+
const result = stepFunction();
|
|
1836
|
+
this.executingStep = false;
|
|
1837
|
+
return result;
|
|
1838
|
+
}
|
|
1839
|
+
/**
|
|
1840
|
+
* Executes a step:
|
|
1841
|
+
* - If the step result is available in the steps, returns the result
|
|
1842
|
+
* - If the result is not avaiable, runs the function
|
|
1843
|
+
* - Sends the result to QStash
|
|
1844
|
+
*
|
|
1845
|
+
* @param lazyStep lazy step to execute
|
|
1846
|
+
* @returns step result
|
|
1847
|
+
*/
|
|
1848
|
+
async runSingle(lazyStep) {
|
|
1849
|
+
if (this.stepCount < this.nonPlanStepCount) {
|
|
1850
|
+
const step = this.steps[this.stepCount + this.planStepCount];
|
|
1851
|
+
validateStep(lazyStep, step);
|
|
1852
|
+
await this.debug?.log("INFO", "RUN_SINGLE", {
|
|
1853
|
+
fromRequest: true,
|
|
1854
|
+
step,
|
|
1855
|
+
stepCount: this.stepCount
|
|
1856
|
+
});
|
|
1857
|
+
return step.out;
|
|
1858
|
+
}
|
|
1859
|
+
const resultStep = await lazyStep.getResultStep(NO_CONCURRENCY, this.stepCount);
|
|
1860
|
+
await this.debug?.log("INFO", "RUN_SINGLE", {
|
|
1861
|
+
fromRequest: false,
|
|
1862
|
+
step: resultStep,
|
|
1863
|
+
stepCount: this.stepCount
|
|
1864
|
+
});
|
|
1865
|
+
await this.submitStepsToQStash([resultStep]);
|
|
1866
|
+
return resultStep.out;
|
|
1867
|
+
}
|
|
1868
|
+
/**
|
|
1869
|
+
* Runs steps in parallel.
|
|
1870
|
+
*
|
|
1871
|
+
* @param stepName parallel step name
|
|
1872
|
+
* @param stepFunctions list of async functions to run in parallel
|
|
1873
|
+
* @returns results of the functions run in parallel
|
|
1874
|
+
*/
|
|
1875
|
+
async runParallel(parallelSteps) {
|
|
1876
|
+
const initialStepCount = this.stepCount - (parallelSteps.length - 1);
|
|
1877
|
+
const parallelCallState = this.getParallelCallState(parallelSteps.length, initialStepCount);
|
|
1878
|
+
const sortedSteps = sortSteps(this.steps);
|
|
1879
|
+
const plannedParallelStepCount = sortedSteps[initialStepCount + this.planStepCount]?.concurrent;
|
|
1880
|
+
if (parallelCallState !== "first" && plannedParallelStepCount !== parallelSteps.length) {
|
|
1881
|
+
throw new QStashWorkflowError(
|
|
1882
|
+
`Incompatible number of parallel steps when call state was '${parallelCallState}'. Expected ${parallelSteps.length}, got ${plannedParallelStepCount} from the request.`
|
|
1883
|
+
);
|
|
1884
|
+
}
|
|
1885
|
+
await this.debug?.log("INFO", "RUN_PARALLEL", {
|
|
1886
|
+
parallelCallState,
|
|
1887
|
+
initialStepCount,
|
|
1888
|
+
plannedParallelStepCount,
|
|
1889
|
+
stepCount: this.stepCount,
|
|
1890
|
+
planStepCount: this.planStepCount
|
|
1891
|
+
});
|
|
1892
|
+
switch (parallelCallState) {
|
|
1893
|
+
case "first": {
|
|
1894
|
+
const planSteps = parallelSteps.map(
|
|
1895
|
+
(parallelStep, index) => parallelStep.getPlanStep(parallelSteps.length, initialStepCount + index)
|
|
1896
|
+
);
|
|
1897
|
+
await this.submitStepsToQStash(planSteps);
|
|
1898
|
+
break;
|
|
1899
|
+
}
|
|
1900
|
+
case "partial": {
|
|
1901
|
+
const planStep = this.steps.at(-1);
|
|
1902
|
+
if (!planStep || planStep.targetStep === void 0) {
|
|
1903
|
+
throw new QStashWorkflowError(
|
|
1904
|
+
`There must be a last step and it should have targetStep larger than 0.Received: ${JSON.stringify(planStep)}`
|
|
1905
|
+
);
|
|
1906
|
+
}
|
|
1907
|
+
const stepIndex = planStep.targetStep - initialStepCount;
|
|
1908
|
+
validateStep(parallelSteps[stepIndex], planStep);
|
|
1909
|
+
try {
|
|
1910
|
+
const resultStep = await parallelSteps[stepIndex].getResultStep(
|
|
1911
|
+
parallelSteps.length,
|
|
1912
|
+
planStep.targetStep
|
|
1913
|
+
);
|
|
1914
|
+
await this.submitStepsToQStash([resultStep]);
|
|
1915
|
+
} catch (error) {
|
|
1916
|
+
if (error instanceof QStashWorkflowAbort) {
|
|
1917
|
+
throw error;
|
|
1918
|
+
}
|
|
1919
|
+
throw new QStashWorkflowError(
|
|
1920
|
+
`Error submitting steps to QStash in partial parallel step execution: ${error}`
|
|
1921
|
+
);
|
|
1922
|
+
}
|
|
1923
|
+
break;
|
|
1924
|
+
}
|
|
1925
|
+
case "discard": {
|
|
1926
|
+
throw new QStashWorkflowAbort("discarded parallel");
|
|
1927
|
+
}
|
|
1928
|
+
case "last": {
|
|
1929
|
+
const parallelResultSteps = sortedSteps.filter((step) => step.stepId >= initialStepCount).slice(0, parallelSteps.length);
|
|
1930
|
+
validateParallelSteps(parallelSteps, parallelResultSteps);
|
|
1931
|
+
return parallelResultSteps.map((step) => step.out);
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
const fillValue = void 0;
|
|
1935
|
+
return Array.from({ length: parallelSteps.length }).fill(fillValue);
|
|
1936
|
+
}
|
|
1937
|
+
/**
|
|
1938
|
+
* Determines the parallel call state
|
|
1939
|
+
*
|
|
1940
|
+
* First filters the steps to get the steps which are after `initialStepCount` parameter.
|
|
1941
|
+
*
|
|
1942
|
+
* Depending on the remaining steps, decides the parallel state:
|
|
1943
|
+
* - "first": If there are no steps
|
|
1944
|
+
* - "last" If there are equal to or more than `2 * parallelStepCount`. We multiply by two
|
|
1945
|
+
* because each step in a parallel execution will have 2 steps: a plan step and a result
|
|
1946
|
+
* step.
|
|
1947
|
+
* - "partial": If the last step is a plan step
|
|
1948
|
+
* - "discard": If the last step is not a plan step. This means that the parallel execution
|
|
1949
|
+
* is in progress (there are still steps to run) and one step has finished and submitted
|
|
1950
|
+
* its result to QStash
|
|
1951
|
+
*
|
|
1952
|
+
* @param parallelStepCount number of steps to run in parallel
|
|
1953
|
+
* @param initialStepCount steps after the parallel invocation
|
|
1954
|
+
* @returns parallel call state
|
|
1955
|
+
*/
|
|
1956
|
+
getParallelCallState(parallelStepCount, initialStepCount) {
|
|
1957
|
+
const remainingSteps = this.steps.filter(
|
|
1958
|
+
(step) => (step.targetStep ?? step.stepId) >= initialStepCount
|
|
1959
|
+
);
|
|
1960
|
+
if (remainingSteps.length === 0) {
|
|
1961
|
+
return "first";
|
|
1962
|
+
} else if (remainingSteps.length >= 2 * parallelStepCount) {
|
|
1963
|
+
return "last";
|
|
1964
|
+
} else if (remainingSteps.at(-1)?.targetStep) {
|
|
1965
|
+
return "partial";
|
|
1966
|
+
} else {
|
|
1967
|
+
return "discard";
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
/**
|
|
1971
|
+
* sends the steps to QStash as batch
|
|
1972
|
+
*
|
|
1973
|
+
* @param steps steps to send
|
|
1974
|
+
*/
|
|
1975
|
+
async submitStepsToQStash(steps) {
|
|
1976
|
+
if (steps.length === 0) {
|
|
1977
|
+
throw new QStashWorkflowError(
|
|
1978
|
+
`Unable to submit steps to QStash. Provided list is empty. Current step: ${this.stepCount}`
|
|
1979
|
+
);
|
|
1980
|
+
}
|
|
1981
|
+
await this.debug?.log("SUBMIT", "SUBMIT_STEP", { length: steps.length, steps });
|
|
1982
|
+
const result = await this.context.qstashClient.batchJSON(
|
|
1983
|
+
steps.map((singleStep) => {
|
|
1984
|
+
const headers = getHeaders(
|
|
1985
|
+
"false",
|
|
1986
|
+
this.context.workflowRunId,
|
|
1987
|
+
this.context.url,
|
|
1988
|
+
this.context.headers,
|
|
1989
|
+
singleStep,
|
|
1990
|
+
this.context.failureUrl
|
|
1991
|
+
);
|
|
1992
|
+
const willWait = singleStep.concurrent === NO_CONCURRENCY || singleStep.stepId === 0;
|
|
1993
|
+
return singleStep.callUrl ? (
|
|
1994
|
+
// if the step is a third party call, we call the third party
|
|
1995
|
+
// url (singleStep.callUrl) and pass information about the workflow
|
|
1996
|
+
// in the headers (handled in getHeaders). QStash makes the request
|
|
1997
|
+
// to callUrl and returns the result to Workflow endpoint.
|
|
1998
|
+
// handleThirdPartyCallResult method sends the result of the third
|
|
1999
|
+
// party call to QStash.
|
|
2000
|
+
{
|
|
2001
|
+
headers,
|
|
2002
|
+
method: singleStep.callMethod,
|
|
2003
|
+
body: singleStep.callBody,
|
|
2004
|
+
url: singleStep.callUrl
|
|
2005
|
+
}
|
|
2006
|
+
) : (
|
|
2007
|
+
// if the step is not a third party call, we use workflow
|
|
2008
|
+
// endpoint (context.url) as URL when calling QStash. QStash
|
|
2009
|
+
// calls us back with the updated steps list.
|
|
2010
|
+
{
|
|
2011
|
+
headers,
|
|
2012
|
+
method: "POST",
|
|
2013
|
+
body: singleStep,
|
|
2014
|
+
url: this.context.url,
|
|
2015
|
+
notBefore: willWait ? singleStep.sleepUntil : void 0,
|
|
2016
|
+
delay: willWait ? singleStep.sleepFor : void 0
|
|
2017
|
+
}
|
|
2018
|
+
);
|
|
2019
|
+
})
|
|
2020
|
+
);
|
|
2021
|
+
await this.debug?.log("INFO", "SUBMIT_STEP", {
|
|
2022
|
+
messageIds: result.map((message) => {
|
|
2023
|
+
return {
|
|
2024
|
+
message: message.messageId
|
|
2025
|
+
};
|
|
2026
|
+
})
|
|
2027
|
+
});
|
|
2028
|
+
throw new QStashWorkflowAbort(steps[0].stepName, steps[0]);
|
|
2029
|
+
}
|
|
2030
|
+
/**
|
|
2031
|
+
* Get the promise by executing the lazt steps list. If there is a single
|
|
2032
|
+
* step, we call `runSingle`. Otherwise `runParallel` is called.
|
|
2033
|
+
*
|
|
2034
|
+
* @param lazyStepList steps list to execute
|
|
2035
|
+
* @returns promise corresponding to the execution
|
|
2036
|
+
*/
|
|
2037
|
+
getExecutionPromise(lazyStepList) {
|
|
2038
|
+
return lazyStepList.length === 1 ? this.runSingle(lazyStepList[0]) : this.runParallel(lazyStepList);
|
|
2039
|
+
}
|
|
2040
|
+
/**
|
|
2041
|
+
* @param lazyStepList steps we executed
|
|
2042
|
+
* @param result result of the promise from `getExecutionPromise`
|
|
2043
|
+
* @param index index of the current step
|
|
2044
|
+
* @returns result[index] if lazyStepList > 1, otherwise result
|
|
2045
|
+
*/
|
|
2046
|
+
static getResult(lazyStepList, result, index) {
|
|
2047
|
+
if (lazyStepList.length === 1) {
|
|
2048
|
+
return result;
|
|
2049
|
+
} else if (Array.isArray(result) && lazyStepList.length === result.length && index < lazyStepList.length) {
|
|
2050
|
+
return result[index];
|
|
2051
|
+
} else {
|
|
2052
|
+
throw new QStashWorkflowError(
|
|
2053
|
+
`Unexpected parallel call result while executing step ${index}: '${result}'. Expected ${lazyStepList.length} many items`
|
|
2054
|
+
);
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
async deferExecution() {
|
|
2058
|
+
await Promise.resolve();
|
|
2059
|
+
await Promise.resolve();
|
|
2060
|
+
}
|
|
2061
|
+
};
|
|
2062
|
+
var validateStep = (lazyStep, stepFromRequest) => {
|
|
2063
|
+
if (lazyStep.stepName !== stepFromRequest.stepName) {
|
|
2064
|
+
throw new QStashWorkflowError(
|
|
2065
|
+
`Incompatible step name. Expected '${lazyStep.stepName}', got '${stepFromRequest.stepName}' from the request`
|
|
2066
|
+
);
|
|
2067
|
+
}
|
|
2068
|
+
if (lazyStep.stepType !== stepFromRequest.stepType) {
|
|
2069
|
+
throw new QStashWorkflowError(
|
|
2070
|
+
`Incompatible step type. Expected '${lazyStep.stepType}', got '${stepFromRequest.stepType}' from the request`
|
|
2071
|
+
);
|
|
2072
|
+
}
|
|
2073
|
+
};
|
|
2074
|
+
var validateParallelSteps = (lazySteps, stepsFromRequest) => {
|
|
2075
|
+
try {
|
|
2076
|
+
for (const [index, stepFromRequest] of stepsFromRequest.entries()) {
|
|
2077
|
+
validateStep(lazySteps[index], stepFromRequest);
|
|
2078
|
+
}
|
|
2079
|
+
} catch (error) {
|
|
2080
|
+
if (error instanceof QStashWorkflowError) {
|
|
2081
|
+
const lazyStepNames = lazySteps.map((lazyStep) => lazyStep.stepName);
|
|
2082
|
+
const lazyStepTypes = lazySteps.map((lazyStep) => lazyStep.stepType);
|
|
2083
|
+
const requestStepNames = stepsFromRequest.map((step) => step.stepName);
|
|
2084
|
+
const requestStepTypes = stepsFromRequest.map((step) => step.stepType);
|
|
2085
|
+
throw new QStashWorkflowError(
|
|
2086
|
+
`Incompatible steps detected in parallel execution: ${error.message}
|
|
2087
|
+
> Step Names from the request: ${JSON.stringify(requestStepNames)}
|
|
2088
|
+
Step Types from the request: ${JSON.stringify(requestStepTypes)}
|
|
2089
|
+
> Step Names expected: ${JSON.stringify(lazyStepNames)}
|
|
2090
|
+
Step Types expected: ${JSON.stringify(lazyStepTypes)}`
|
|
2091
|
+
);
|
|
2092
|
+
}
|
|
2093
|
+
throw error;
|
|
2094
|
+
}
|
|
2095
|
+
};
|
|
2096
|
+
var sortSteps = (steps) => {
|
|
2097
|
+
const getStepId = (step) => step.targetStep ?? step.stepId;
|
|
2098
|
+
return steps.toSorted((step, stepOther) => getStepId(step) - getStepId(stepOther));
|
|
2099
|
+
};
|
|
2100
|
+
|
|
2101
|
+
// src/client/workflow/steps.ts
|
|
2102
|
+
var BaseLazyStep = class {
|
|
2103
|
+
stepName;
|
|
2104
|
+
// will be set in the subclasses
|
|
2105
|
+
constructor(stepName) {
|
|
2106
|
+
this.stepName = stepName;
|
|
2107
|
+
}
|
|
2108
|
+
};
|
|
2109
|
+
var LazyFunctionStep = class extends BaseLazyStep {
|
|
2110
|
+
stepFunction;
|
|
2111
|
+
stepType = "Run";
|
|
2112
|
+
constructor(stepName, stepFunction) {
|
|
2113
|
+
super(stepName);
|
|
2114
|
+
this.stepFunction = stepFunction;
|
|
2115
|
+
}
|
|
2116
|
+
getPlanStep(concurrent, targetStep) {
|
|
2117
|
+
{
|
|
2118
|
+
return {
|
|
2119
|
+
stepId: 0,
|
|
2120
|
+
stepName: this.stepName,
|
|
2121
|
+
stepType: this.stepType,
|
|
2122
|
+
concurrent,
|
|
2123
|
+
targetStep
|
|
2124
|
+
};
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
async getResultStep(concurrent, stepId) {
|
|
2128
|
+
let result = this.stepFunction();
|
|
2129
|
+
if (result instanceof Promise) {
|
|
2130
|
+
result = await result;
|
|
2131
|
+
}
|
|
2132
|
+
return {
|
|
2133
|
+
stepId,
|
|
2134
|
+
stepName: this.stepName,
|
|
2135
|
+
stepType: this.stepType,
|
|
2136
|
+
out: result,
|
|
2137
|
+
concurrent
|
|
2138
|
+
};
|
|
2139
|
+
}
|
|
2140
|
+
};
|
|
2141
|
+
var LazySleepStep = class extends BaseLazyStep {
|
|
2142
|
+
sleep;
|
|
2143
|
+
stepType = "SleepFor";
|
|
2144
|
+
constructor(stepName, sleep) {
|
|
2145
|
+
super(stepName);
|
|
2146
|
+
this.sleep = sleep;
|
|
2147
|
+
}
|
|
2148
|
+
getPlanStep(concurrent, targetStep) {
|
|
2149
|
+
{
|
|
2150
|
+
return {
|
|
2151
|
+
stepId: 0,
|
|
2152
|
+
stepName: this.stepName,
|
|
2153
|
+
stepType: this.stepType,
|
|
2154
|
+
sleepFor: this.sleep,
|
|
2155
|
+
concurrent,
|
|
2156
|
+
targetStep
|
|
2157
|
+
};
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
async getResultStep(concurrent, stepId) {
|
|
2161
|
+
return await Promise.resolve({
|
|
2162
|
+
stepId,
|
|
2163
|
+
stepName: this.stepName,
|
|
2164
|
+
stepType: this.stepType,
|
|
2165
|
+
sleepFor: this.sleep,
|
|
2166
|
+
concurrent
|
|
2167
|
+
});
|
|
2168
|
+
}
|
|
2169
|
+
};
|
|
2170
|
+
var LazySleepUntilStep = class extends BaseLazyStep {
|
|
2171
|
+
sleepUntil;
|
|
2172
|
+
stepType = "SleepUntil";
|
|
2173
|
+
constructor(stepName, sleepUntil) {
|
|
2174
|
+
super(stepName);
|
|
2175
|
+
this.sleepUntil = sleepUntil;
|
|
2176
|
+
}
|
|
2177
|
+
getPlanStep(concurrent, targetStep) {
|
|
2178
|
+
{
|
|
2179
|
+
return {
|
|
2180
|
+
stepId: 0,
|
|
2181
|
+
stepName: this.stepName,
|
|
2182
|
+
stepType: this.stepType,
|
|
2183
|
+
sleepUntil: this.sleepUntil,
|
|
2184
|
+
concurrent,
|
|
2185
|
+
targetStep
|
|
2186
|
+
};
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
async getResultStep(concurrent, stepId) {
|
|
2190
|
+
return await Promise.resolve({
|
|
2191
|
+
stepId,
|
|
2192
|
+
stepName: this.stepName,
|
|
2193
|
+
stepType: this.stepType,
|
|
2194
|
+
sleepUntil: this.sleepUntil,
|
|
2195
|
+
concurrent
|
|
2196
|
+
});
|
|
2197
|
+
}
|
|
2198
|
+
};
|
|
2199
|
+
var LazyCallStep = class extends BaseLazyStep {
|
|
2200
|
+
url;
|
|
2201
|
+
method;
|
|
2202
|
+
body;
|
|
2203
|
+
headers;
|
|
2204
|
+
stepType = "Call";
|
|
2205
|
+
constructor(stepName, url, method, body, headers) {
|
|
2206
|
+
super(stepName);
|
|
2207
|
+
this.url = url;
|
|
2208
|
+
this.method = method;
|
|
2209
|
+
this.body = body;
|
|
2210
|
+
this.headers = headers;
|
|
2211
|
+
}
|
|
2212
|
+
getPlanStep(concurrent, targetStep) {
|
|
2213
|
+
{
|
|
2214
|
+
return {
|
|
2215
|
+
stepId: 0,
|
|
2216
|
+
stepName: this.stepName,
|
|
2217
|
+
stepType: this.stepType,
|
|
2218
|
+
concurrent,
|
|
2219
|
+
targetStep
|
|
2220
|
+
};
|
|
2221
|
+
}
|
|
2222
|
+
}
|
|
2223
|
+
async getResultStep(concurrent, stepId) {
|
|
2224
|
+
return await Promise.resolve({
|
|
2225
|
+
stepId,
|
|
2226
|
+
stepName: this.stepName,
|
|
2227
|
+
stepType: this.stepType,
|
|
2228
|
+
concurrent,
|
|
2229
|
+
callUrl: this.url,
|
|
2230
|
+
callMethod: this.method,
|
|
2231
|
+
callBody: this.body,
|
|
2232
|
+
callHeaders: this.headers
|
|
2233
|
+
});
|
|
2234
|
+
}
|
|
2235
|
+
};
|
|
2236
|
+
|
|
2237
|
+
// src/client/workflow/context.ts
|
|
2238
|
+
var WorkflowContext = class {
|
|
2239
|
+
executor;
|
|
2240
|
+
steps;
|
|
2241
|
+
/**
|
|
2242
|
+
* QStash client of the workflow
|
|
2243
|
+
*
|
|
2244
|
+
* Can be overwritten by passing `qstashClient` parameter in `serve`:
|
|
2245
|
+
*
|
|
2246
|
+
* ```ts
|
|
2247
|
+
* import { Client } from "@upstash/qstash"
|
|
2248
|
+
*
|
|
2249
|
+
* export const POST = serve(
|
|
2250
|
+
* async (context) => {
|
|
2251
|
+
* ...
|
|
2252
|
+
* },
|
|
2253
|
+
* {
|
|
2254
|
+
* qstashClient: new Client({...})
|
|
2255
|
+
* }
|
|
2256
|
+
* )
|
|
2257
|
+
* ```
|
|
2258
|
+
*/
|
|
2259
|
+
qstashClient;
|
|
2260
|
+
/**
|
|
2261
|
+
* Run id of the workflow
|
|
2262
|
+
*/
|
|
2263
|
+
workflowRunId;
|
|
2264
|
+
/**
|
|
2265
|
+
* URL of the workflow
|
|
2266
|
+
*
|
|
2267
|
+
* Can be overwritten by passing a `url` parameter in `serve`:
|
|
2268
|
+
*
|
|
2269
|
+
* ```ts
|
|
2270
|
+
* export const POST = serve(
|
|
2271
|
+
* async (context) => {
|
|
2272
|
+
* ...
|
|
2273
|
+
* },
|
|
2274
|
+
* {
|
|
2275
|
+
* url: "new-url-value"
|
|
2276
|
+
* }
|
|
2277
|
+
* )
|
|
2278
|
+
* ```
|
|
2279
|
+
*/
|
|
2280
|
+
url;
|
|
2281
|
+
/**
|
|
2282
|
+
* URL to call in case of workflow failure with QStash failure callback
|
|
2283
|
+
*
|
|
2284
|
+
* https://upstash.com/docs/qstash/features/callbacks#what-is-a-failure-callback
|
|
2285
|
+
*
|
|
2286
|
+
* Can be overwritten by passing a `failureUrl` parameter in `serve`:
|
|
2287
|
+
*
|
|
2288
|
+
* ```ts
|
|
2289
|
+
* export const POST = serve(
|
|
2290
|
+
* async (context) => {
|
|
2291
|
+
* ...
|
|
2292
|
+
* },
|
|
2293
|
+
* {
|
|
2294
|
+
* failureUrl: "new-url-value"
|
|
2295
|
+
* }
|
|
2296
|
+
* )
|
|
2297
|
+
* ```
|
|
2298
|
+
*/
|
|
2299
|
+
failureUrl;
|
|
2300
|
+
/**
|
|
2301
|
+
* Payload of the request which started the workflow.
|
|
2302
|
+
*
|
|
2303
|
+
* To specify its type, you can define `serve` as follows:
|
|
2304
|
+
*
|
|
2305
|
+
* ```ts
|
|
2306
|
+
* // set requestPayload type to MyPayload:
|
|
2307
|
+
* export const POST = serve<MyPayload>(
|
|
2308
|
+
* async (context) => {
|
|
2309
|
+
* ...
|
|
2310
|
+
* }
|
|
2311
|
+
* )
|
|
2312
|
+
* ```
|
|
2313
|
+
*
|
|
2314
|
+
* By default, `serve` tries to apply `JSON.parse` to the request payload.
|
|
2315
|
+
* If your payload is encoded in a format other than JSON, you can utilize
|
|
2316
|
+
* the `initialPayloadParser` parameter:
|
|
2317
|
+
*
|
|
2318
|
+
* ```ts
|
|
2319
|
+
* export const POST = serve<MyPayload>(
|
|
2320
|
+
* async (context) => {
|
|
2321
|
+
* ...
|
|
2322
|
+
* },
|
|
2323
|
+
* {
|
|
2324
|
+
* initialPayloadParser: (initialPayload) => {return doSomething(initialPayload)}
|
|
2325
|
+
* }
|
|
2326
|
+
* )
|
|
2327
|
+
* ```
|
|
2328
|
+
*/
|
|
2329
|
+
requestPayload;
|
|
2330
|
+
/**
|
|
2331
|
+
* headers of the initial request
|
|
2332
|
+
*/
|
|
2333
|
+
headers;
|
|
2334
|
+
/**
|
|
2335
|
+
* initial payload as a raw string
|
|
2336
|
+
*/
|
|
2337
|
+
rawInitialPayload;
|
|
2338
|
+
/**
|
|
2339
|
+
* Map of environment variables and their values.
|
|
2340
|
+
*
|
|
2341
|
+
* Can be set using the `env` option of serve:
|
|
2342
|
+
*
|
|
2343
|
+
* ```ts
|
|
2344
|
+
* export const POST = serve<MyPayload>(
|
|
2345
|
+
* async (context) => {
|
|
2346
|
+
* const key = context.env["API_KEY"];
|
|
2347
|
+
* },
|
|
2348
|
+
* {
|
|
2349
|
+
* env: {
|
|
2350
|
+
* "API_KEY": "*****";
|
|
2351
|
+
* }
|
|
2352
|
+
* }
|
|
2353
|
+
* )
|
|
2354
|
+
* ```
|
|
2355
|
+
*
|
|
2356
|
+
* Default value is set to `process.env`.
|
|
2357
|
+
*/
|
|
2358
|
+
env;
|
|
2359
|
+
constructor({
|
|
2360
|
+
qstashClient,
|
|
2361
|
+
workflowRunId,
|
|
2362
|
+
headers,
|
|
2363
|
+
steps,
|
|
2364
|
+
url,
|
|
2365
|
+
failureUrl,
|
|
2366
|
+
debug,
|
|
2367
|
+
initialPayload,
|
|
2368
|
+
rawInitialPayload,
|
|
2369
|
+
env
|
|
2370
|
+
}) {
|
|
2371
|
+
this.qstashClient = qstashClient;
|
|
2372
|
+
this.workflowRunId = workflowRunId;
|
|
2373
|
+
this.steps = steps;
|
|
2374
|
+
this.url = url;
|
|
2375
|
+
this.failureUrl = failureUrl;
|
|
2376
|
+
this.headers = headers;
|
|
2377
|
+
this.requestPayload = initialPayload;
|
|
2378
|
+
this.rawInitialPayload = rawInitialPayload ?? JSON.stringify(this.requestPayload);
|
|
2379
|
+
this.env = env ?? {};
|
|
2380
|
+
this.executor = new AutoExecutor(this, this.steps, debug);
|
|
2381
|
+
}
|
|
2382
|
+
/**
|
|
2383
|
+
* Executes a workflow step
|
|
2384
|
+
*
|
|
2385
|
+
* ```typescript
|
|
2386
|
+
* const result = await context.run("step 1", () => {
|
|
2387
|
+
* return "result"
|
|
2388
|
+
* })
|
|
2389
|
+
* ```
|
|
2390
|
+
*
|
|
2391
|
+
* Can also be called in parallel and the steps will be executed
|
|
2392
|
+
* simulatenously:
|
|
2393
|
+
*
|
|
2394
|
+
* ```typescript
|
|
2395
|
+
* const [result1, result2] = await Promise.all([
|
|
2396
|
+
* context.run("step 1", () => {
|
|
2397
|
+
* return "result1"
|
|
2398
|
+
* })
|
|
2399
|
+
* context.run("step 2", async () => {
|
|
2400
|
+
* return await fetchResults()
|
|
2401
|
+
* })
|
|
2402
|
+
* ])
|
|
2403
|
+
* ```
|
|
2404
|
+
*
|
|
2405
|
+
* @param stepName name of the step
|
|
2406
|
+
* @param stepFunction step function to be executed
|
|
2407
|
+
* @returns result of the step function
|
|
2408
|
+
*/
|
|
2409
|
+
async run(stepName, stepFunction) {
|
|
2410
|
+
const wrappedStepFunction = () => this.executor.wrapStep(stepName, stepFunction);
|
|
2411
|
+
return this.addStep(new LazyFunctionStep(stepName, wrappedStepFunction));
|
|
2412
|
+
}
|
|
2413
|
+
/**
|
|
2414
|
+
* Stops the execution for the duration provided.
|
|
2415
|
+
*
|
|
2416
|
+
* @param stepName
|
|
2417
|
+
* @param duration sleep duration in seconds
|
|
2418
|
+
* @returns undefined
|
|
2419
|
+
*/
|
|
2420
|
+
async sleep(stepName, duration) {
|
|
2421
|
+
await this.addStep(new LazySleepStep(stepName, duration));
|
|
2422
|
+
}
|
|
2423
|
+
/**
|
|
2424
|
+
* Stops the execution until the date time provided.
|
|
2425
|
+
*
|
|
2426
|
+
* @param stepName
|
|
2427
|
+
* @param datetime time to sleep until. Can be provided as a number (in unix seconds),
|
|
2428
|
+
* as a Date object or a string (passed to `new Date(datetimeString)`)
|
|
2429
|
+
* @returns undefined
|
|
2430
|
+
*/
|
|
2431
|
+
async sleepUntil(stepName, datetime) {
|
|
2432
|
+
let time;
|
|
2433
|
+
if (typeof datetime === "number") {
|
|
2434
|
+
time = datetime;
|
|
2435
|
+
} else {
|
|
2436
|
+
datetime = typeof datetime === "string" ? new Date(datetime) : datetime;
|
|
2437
|
+
time = Math.round(datetime.getTime() / 1e3);
|
|
2438
|
+
}
|
|
2439
|
+
await this.addStep(new LazySleepUntilStep(stepName, time));
|
|
2440
|
+
}
|
|
2441
|
+
/**
|
|
2442
|
+
* Makes a third party call through QStash in order to make a
|
|
2443
|
+
* network call without consuming any runtime.
|
|
2444
|
+
*
|
|
2445
|
+
* ```ts
|
|
2446
|
+
* const postResult = await context.call<string>(
|
|
2447
|
+
* "post call step",
|
|
2448
|
+
* `https://www.some-endpoint.com/api`,
|
|
2449
|
+
* "POST",
|
|
2450
|
+
* "my-payload"
|
|
2451
|
+
* );
|
|
2452
|
+
* ```
|
|
2453
|
+
*
|
|
2454
|
+
* tries to parse the result of the request as JSON. If it's
|
|
2455
|
+
* not a JSON which can be parsed, simply returns the response
|
|
2456
|
+
* body as it is.
|
|
2457
|
+
*
|
|
2458
|
+
* @param stepName
|
|
2459
|
+
* @param url url to call
|
|
2460
|
+
* @param method call method
|
|
2461
|
+
* @param body call body
|
|
2462
|
+
* @param headers call headers
|
|
2463
|
+
* @returns call result (parsed as JSON if possible)
|
|
2464
|
+
*/
|
|
2465
|
+
async call(stepName, url, method, body, headers) {
|
|
2466
|
+
const result = await this.addStep(
|
|
2467
|
+
new LazyCallStep(stepName, url, method, body, headers ?? {})
|
|
2468
|
+
);
|
|
2469
|
+
try {
|
|
2470
|
+
return JSON.parse(result);
|
|
2471
|
+
} catch {
|
|
2472
|
+
return result;
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
/**
|
|
2476
|
+
* Adds steps to the executor. Needed so that it can be overwritten in
|
|
2477
|
+
* DisabledWorkflowContext.
|
|
2478
|
+
*/
|
|
2479
|
+
async addStep(step) {
|
|
2480
|
+
return await this.executor.addStep(step);
|
|
2481
|
+
}
|
|
2482
|
+
};
|
|
2483
|
+
var DisabledWorkflowContext = class _DisabledWorkflowContext extends WorkflowContext {
|
|
2484
|
+
static disabledMessage = "disabled-qstash-worklfow-run";
|
|
2485
|
+
/**
|
|
2486
|
+
* overwrite the WorkflowContext.addStep method to always raise QStashWorkflowAbort
|
|
2487
|
+
* error in order to stop the execution whenever we encounter a step.
|
|
2488
|
+
*
|
|
2489
|
+
* @param _step
|
|
2490
|
+
*/
|
|
2491
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
2492
|
+
async addStep(_step) {
|
|
2493
|
+
throw new QStashWorkflowAbort(_DisabledWorkflowContext.disabledMessage);
|
|
2494
|
+
}
|
|
2495
|
+
/**
|
|
2496
|
+
* copies the passed context to create a DisabledWorkflowContext. Then, runs the
|
|
2497
|
+
* route function with the new context.
|
|
2498
|
+
*
|
|
2499
|
+
* - returns "run-ended" if there are no steps found or
|
|
2500
|
+
* if the auth failed and user called `return`
|
|
2501
|
+
* - returns "step-found" if DisabledWorkflowContext.addStep is called.
|
|
2502
|
+
* - if there is another error, returns the error.
|
|
2503
|
+
*
|
|
2504
|
+
* @param routeFunction
|
|
2505
|
+
*/
|
|
2506
|
+
static async tryAuthentication(routeFunction, context) {
|
|
2507
|
+
const disabledContext = new _DisabledWorkflowContext({
|
|
2508
|
+
qstashClient: new Client({ baseUrl: "disabled-client", token: "disabled-client" }),
|
|
2509
|
+
workflowRunId: context.workflowRunId,
|
|
2510
|
+
headers: context.headers,
|
|
2511
|
+
steps: [],
|
|
2512
|
+
url: context.url,
|
|
2513
|
+
failureUrl: context.failureUrl,
|
|
2514
|
+
initialPayload: context.requestPayload,
|
|
2515
|
+
rawInitialPayload: context.rawInitialPayload,
|
|
2516
|
+
env: context.env
|
|
2517
|
+
});
|
|
2518
|
+
try {
|
|
2519
|
+
await routeFunction(disabledContext);
|
|
2520
|
+
} catch (error) {
|
|
2521
|
+
if (error instanceof QStashWorkflowAbort && error.stepName === this.disabledMessage) {
|
|
2522
|
+
return ok("step-found");
|
|
2523
|
+
}
|
|
2524
|
+
return err(error);
|
|
2525
|
+
}
|
|
2526
|
+
return ok("run-ended");
|
|
2527
|
+
}
|
|
2528
|
+
};
|
|
2529
|
+
|
|
2530
|
+
// src/client/workflow/logger.ts
|
|
2531
|
+
var LOG_LEVELS = ["DEBUG", "INFO", "SUBMIT", "WARN", "ERROR"];
|
|
2532
|
+
var WorkflowLogger = class _WorkflowLogger {
|
|
2533
|
+
logs = [];
|
|
2534
|
+
options;
|
|
2535
|
+
workflowRunId = void 0;
|
|
2536
|
+
constructor(options) {
|
|
2537
|
+
this.options = options;
|
|
2538
|
+
}
|
|
2539
|
+
async log(level, eventType, details) {
|
|
2540
|
+
if (this.shouldLog(level)) {
|
|
2541
|
+
const timestamp = Date.now();
|
|
2542
|
+
const logEntry = {
|
|
2543
|
+
timestamp,
|
|
2544
|
+
workflowRunId: this.workflowRunId ?? "",
|
|
2545
|
+
logLevel: level,
|
|
2546
|
+
eventType,
|
|
2547
|
+
details
|
|
2548
|
+
};
|
|
2549
|
+
this.logs.push(logEntry);
|
|
2550
|
+
if (this.options.logOutput === "console") {
|
|
2551
|
+
this.writeToConsole(logEntry);
|
|
2552
|
+
}
|
|
2553
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
setWorkflowRunId(workflowRunId) {
|
|
2557
|
+
this.workflowRunId = workflowRunId;
|
|
2558
|
+
}
|
|
2559
|
+
writeToConsole(logEntry) {
|
|
2560
|
+
const JSON_SPACING = 2;
|
|
2561
|
+
console.log(JSON.stringify(logEntry, void 0, JSON_SPACING));
|
|
2562
|
+
}
|
|
2563
|
+
shouldLog(level) {
|
|
2564
|
+
return LOG_LEVELS.indexOf(level) >= LOG_LEVELS.indexOf(this.options.logLevel);
|
|
2565
|
+
}
|
|
2566
|
+
static getLogger(verbose) {
|
|
2567
|
+
if (typeof verbose === "object") {
|
|
2568
|
+
return verbose;
|
|
2569
|
+
} else {
|
|
2570
|
+
return verbose ? new _WorkflowLogger({
|
|
2571
|
+
logLevel: "INFO",
|
|
2572
|
+
logOutput: "console"
|
|
2573
|
+
}) : void 0;
|
|
2574
|
+
}
|
|
2575
|
+
}
|
|
2576
|
+
};
|
|
2577
|
+
|
|
2578
|
+
// src/client/workflow/workflow-parser.ts
|
|
2579
|
+
var getPayload = async (request) => {
|
|
2580
|
+
try {
|
|
2581
|
+
return await request.text();
|
|
2582
|
+
} catch {
|
|
2583
|
+
return;
|
|
2584
|
+
}
|
|
2585
|
+
};
|
|
2586
|
+
var parsePayload = (rawPayload) => {
|
|
2587
|
+
const [encodedInitialPayload, ...encodedSteps] = JSON.parse(rawPayload);
|
|
2588
|
+
const rawInitialPayload = atob(encodedInitialPayload.body);
|
|
2589
|
+
const initialStep = {
|
|
2590
|
+
stepId: 0,
|
|
2591
|
+
stepName: "init",
|
|
2592
|
+
stepType: "Initial",
|
|
2593
|
+
out: rawInitialPayload,
|
|
2594
|
+
concurrent: NO_CONCURRENCY
|
|
2595
|
+
};
|
|
2596
|
+
const stepsToDecode = encodedSteps.filter((step) => step.callType === "step");
|
|
2597
|
+
const otherSteps = stepsToDecode.map((rawStep) => {
|
|
2598
|
+
return JSON.parse(atob(rawStep.body));
|
|
2599
|
+
});
|
|
2600
|
+
const steps = [initialStep, ...otherSteps];
|
|
2601
|
+
return {
|
|
2602
|
+
rawInitialPayload,
|
|
2603
|
+
steps
|
|
2604
|
+
};
|
|
2605
|
+
};
|
|
2606
|
+
var deduplicateSteps = (steps) => {
|
|
2607
|
+
const targetStepIds = [];
|
|
2608
|
+
const stepIds = [];
|
|
2609
|
+
const deduplicatedSteps = [];
|
|
2610
|
+
for (const step of steps) {
|
|
2611
|
+
if (step.stepId === 0) {
|
|
2612
|
+
if (!targetStepIds.includes(step.targetStep ?? 0)) {
|
|
2613
|
+
deduplicatedSteps.push(step);
|
|
2614
|
+
targetStepIds.push(step.targetStep ?? 0);
|
|
2615
|
+
}
|
|
2616
|
+
} else {
|
|
2617
|
+
if (!stepIds.includes(step.stepId)) {
|
|
2618
|
+
deduplicatedSteps.push(step);
|
|
2619
|
+
stepIds.push(step.stepId);
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
return deduplicatedSteps;
|
|
2624
|
+
};
|
|
2625
|
+
var checkIfLastOneIsDuplicate = async (steps, debug) => {
|
|
2626
|
+
if (steps.length < 2) {
|
|
2627
|
+
return false;
|
|
2628
|
+
}
|
|
2629
|
+
const lastStep = steps.at(-1);
|
|
2630
|
+
const lastStepId = lastStep.stepId;
|
|
2631
|
+
const lastTargetStepId = lastStep.targetStep;
|
|
2632
|
+
for (let index = 0; index < steps.length - 1; index++) {
|
|
2633
|
+
const step = steps[index];
|
|
2634
|
+
if (step.stepId === lastStepId && step.targetStep === lastTargetStepId) {
|
|
2635
|
+
const message = `QStash Workflow: The step '${step.stepName}' with id '${step.stepId}' has run twice during workflow execution. Rest of the workflow will continue running as usual.`;
|
|
2636
|
+
await debug?.log("WARN", "RESPONSE_DEFAULT", message);
|
|
2637
|
+
console.warn(message);
|
|
2638
|
+
return true;
|
|
2639
|
+
}
|
|
2640
|
+
}
|
|
2641
|
+
return false;
|
|
2642
|
+
};
|
|
2643
|
+
var validateRequest = (request) => {
|
|
2644
|
+
const versionHeader = request.headers.get(WORKFLOW_PROTOCOL_VERSION_HEADER);
|
|
2645
|
+
const isFirstInvocation = !versionHeader;
|
|
2646
|
+
if (!isFirstInvocation && versionHeader !== WORKFLOW_PROTOCOL_VERSION) {
|
|
2647
|
+
throw new QStashWorkflowError(
|
|
2648
|
+
`Incompatible workflow sdk protocol version. Expected ${WORKFLOW_PROTOCOL_VERSION}, got ${versionHeader} from the request.`
|
|
2649
|
+
);
|
|
2650
|
+
}
|
|
2651
|
+
const workflowRunId = isFirstInvocation ? `wfr_${nanoid()}` : request.headers.get(WORKFLOW_ID_HEADER) ?? "";
|
|
2652
|
+
if (workflowRunId.length === 0) {
|
|
2653
|
+
throw new QStashWorkflowError("Couldn't get workflow id from header");
|
|
2654
|
+
}
|
|
2655
|
+
return {
|
|
2656
|
+
isFirstInvocation,
|
|
2657
|
+
workflowRunId
|
|
2658
|
+
};
|
|
2659
|
+
};
|
|
2660
|
+
var parseRequest = async (requestPayload, isFirstInvocation, debug) => {
|
|
2661
|
+
if (isFirstInvocation) {
|
|
2662
|
+
return {
|
|
2663
|
+
rawInitialPayload: requestPayload ?? "",
|
|
2664
|
+
steps: [],
|
|
2665
|
+
isLastDuplicate: false
|
|
2666
|
+
};
|
|
2667
|
+
} else {
|
|
2668
|
+
if (!requestPayload) {
|
|
2669
|
+
throw new QStashWorkflowError("Only first call can have an empty body");
|
|
2670
|
+
}
|
|
2671
|
+
const { rawInitialPayload, steps } = parsePayload(requestPayload);
|
|
2672
|
+
const isLastDuplicate = await checkIfLastOneIsDuplicate(steps, debug);
|
|
2673
|
+
const deduplicatedSteps = deduplicateSteps(steps);
|
|
2674
|
+
return {
|
|
2675
|
+
rawInitialPayload,
|
|
2676
|
+
steps: deduplicatedSteps,
|
|
2677
|
+
isLastDuplicate
|
|
2678
|
+
};
|
|
2679
|
+
}
|
|
2680
|
+
};
|
|
2681
|
+
var handleFailure = async (request, requestPayload, qstashClient, initialPayloadParser, failureFunction, debug) => {
|
|
2682
|
+
if (request.headers.get(WORKFLOW_FAILURE_HEADER) !== "true") {
|
|
2683
|
+
return ok("not-failure-callback");
|
|
2684
|
+
}
|
|
2685
|
+
if (!failureFunction) {
|
|
2686
|
+
return err(
|
|
2687
|
+
new QStashWorkflowError(
|
|
2688
|
+
"Workflow endpoint is called to handle a failure, but a failureFunction is not provided in serve options. Either provide a failureUrl or a failureFunction."
|
|
2689
|
+
)
|
|
2690
|
+
);
|
|
2691
|
+
}
|
|
2692
|
+
try {
|
|
2693
|
+
const { status, header, body, url, sourceHeader, sourceBody, workflowRunId } = JSON.parse(
|
|
2694
|
+
requestPayload
|
|
2695
|
+
);
|
|
2696
|
+
const decodedBody = body ? atob(body) : "{}";
|
|
2697
|
+
const errorPayload = JSON.parse(decodedBody);
|
|
2698
|
+
const {
|
|
2699
|
+
rawInitialPayload,
|
|
2700
|
+
steps,
|
|
2701
|
+
isLastDuplicate: _isLastDuplicate
|
|
2702
|
+
} = await parseRequest(atob(sourceBody), false, debug);
|
|
2703
|
+
const workflowContext = new WorkflowContext({
|
|
2704
|
+
qstashClient,
|
|
2705
|
+
workflowRunId,
|
|
2706
|
+
initialPayload: initialPayloadParser(rawInitialPayload),
|
|
2707
|
+
rawInitialPayload,
|
|
2708
|
+
headers: recreateUserHeaders(new Headers(sourceHeader)),
|
|
2709
|
+
steps,
|
|
2710
|
+
url,
|
|
2711
|
+
failureUrl: url,
|
|
2712
|
+
debug
|
|
2713
|
+
});
|
|
2714
|
+
await failureFunction(workflowContext, status, errorPayload.message, header);
|
|
2715
|
+
} catch (error) {
|
|
2716
|
+
return err(error);
|
|
2717
|
+
}
|
|
2718
|
+
return ok("is-failure-callback");
|
|
2719
|
+
};
|
|
2720
|
+
|
|
2721
|
+
// src/client/workflow/serve.ts
|
|
2722
|
+
var processOptions = (options) => {
|
|
2723
|
+
const environment = options?.env ?? (typeof process === "undefined" ? {} : process.env);
|
|
2724
|
+
const receiverEnvironmentVariablesSet = Boolean(
|
|
2725
|
+
environment.QSTASH_CURRENT_SIGNING_KEY && environment.QSTASH_NEXT_SIGNING_KEY
|
|
2726
|
+
);
|
|
2727
|
+
return {
|
|
2728
|
+
qstashClient: new Client({
|
|
2729
|
+
baseUrl: environment.QSTASH_URL,
|
|
2730
|
+
token: environment.QSTASH_TOKEN
|
|
2731
|
+
}),
|
|
2732
|
+
onStepFinish: (workflowRunId, _finishCondition) => new Response(JSON.stringify({ workflowRunId }), {
|
|
2733
|
+
status: 200
|
|
2734
|
+
}),
|
|
2735
|
+
initialPayloadParser: (initialRequest) => {
|
|
2736
|
+
if (!initialRequest) {
|
|
2737
|
+
return void 0;
|
|
2738
|
+
}
|
|
2739
|
+
try {
|
|
2740
|
+
return JSON.parse(initialRequest);
|
|
2741
|
+
} catch (error) {
|
|
2742
|
+
if (error instanceof SyntaxError) {
|
|
2743
|
+
return initialRequest;
|
|
2744
|
+
}
|
|
2745
|
+
throw error;
|
|
2746
|
+
}
|
|
2747
|
+
},
|
|
2748
|
+
receiver: receiverEnvironmentVariablesSet ? new Receiver({
|
|
2749
|
+
currentSigningKey: environment.QSTASH_CURRENT_SIGNING_KEY,
|
|
2750
|
+
nextSigningKey: environment.QSTASH_NEXT_SIGNING_KEY
|
|
2751
|
+
}) : void 0,
|
|
2752
|
+
baseUrl: environment.UPSTASH_WORKFLOW_URL,
|
|
2753
|
+
env: environment,
|
|
2754
|
+
...options
|
|
2755
|
+
};
|
|
2756
|
+
};
|
|
2757
|
+
var serve = (routeFunction, options) => {
|
|
2758
|
+
const {
|
|
2759
|
+
qstashClient,
|
|
2760
|
+
onStepFinish,
|
|
2761
|
+
initialPayloadParser,
|
|
2762
|
+
url,
|
|
2763
|
+
verbose,
|
|
2764
|
+
receiver,
|
|
2765
|
+
failureUrl,
|
|
2766
|
+
failureFunction,
|
|
2767
|
+
baseUrl,
|
|
2768
|
+
env
|
|
2769
|
+
} = processOptions(options);
|
|
2770
|
+
const debug = WorkflowLogger.getLogger(verbose);
|
|
2771
|
+
const handler = async (request) => {
|
|
2772
|
+
const initialWorkflowUrl = url ?? request.url;
|
|
2773
|
+
const workflowUrl = baseUrl ? initialWorkflowUrl.replace(/^(https?:\/\/[^/]+)(\/.*)?$/, (_, matchedBaseUrl, path) => {
|
|
2774
|
+
return baseUrl + (path || "");
|
|
2775
|
+
}) : initialWorkflowUrl;
|
|
2776
|
+
if (workflowUrl !== initialWorkflowUrl) {
|
|
2777
|
+
await debug?.log("WARN", "ENDPOINT_START", {
|
|
2778
|
+
warning: `QStash Workflow: replacing the base of the url with "${baseUrl}" and using it as workflow endpoint.`,
|
|
2779
|
+
originalURL: initialWorkflowUrl,
|
|
2780
|
+
updatedURL: workflowUrl
|
|
2781
|
+
});
|
|
2782
|
+
}
|
|
2783
|
+
const workflowFailureUrl = failureFunction ? workflowUrl : failureUrl;
|
|
2784
|
+
const requestPayload = await getPayload(request) ?? "";
|
|
2785
|
+
await verifyRequest(requestPayload, request.headers.get("upstash-signature"), receiver);
|
|
2786
|
+
await debug?.log("INFO", "ENDPOINT_START");
|
|
2787
|
+
const failureCheck = await handleFailure(
|
|
2788
|
+
request,
|
|
2789
|
+
requestPayload,
|
|
2790
|
+
qstashClient,
|
|
2791
|
+
initialPayloadParser,
|
|
2792
|
+
failureFunction
|
|
2793
|
+
);
|
|
2794
|
+
if (failureCheck.isErr()) {
|
|
2795
|
+
throw failureCheck.error;
|
|
2796
|
+
} else if (failureCheck.value === "is-failure-callback") {
|
|
2797
|
+
await debug?.log("WARN", "RESPONSE_DEFAULT", "failureFunction executed");
|
|
2798
|
+
return onStepFinish("no-workflow-id", "failure-callback");
|
|
2799
|
+
}
|
|
2800
|
+
const { isFirstInvocation, workflowRunId } = validateRequest(request);
|
|
2801
|
+
debug?.setWorkflowRunId(workflowRunId);
|
|
2802
|
+
const { rawInitialPayload, steps, isLastDuplicate } = await parseRequest(
|
|
2803
|
+
requestPayload,
|
|
2804
|
+
isFirstInvocation,
|
|
2805
|
+
debug
|
|
2806
|
+
);
|
|
2807
|
+
if (isLastDuplicate) {
|
|
2808
|
+
return onStepFinish("no-workflow-id", "duplicate-step");
|
|
2809
|
+
}
|
|
2810
|
+
const workflowContext = new WorkflowContext({
|
|
2811
|
+
qstashClient,
|
|
2812
|
+
workflowRunId,
|
|
2813
|
+
initialPayload: initialPayloadParser(rawInitialPayload),
|
|
2814
|
+
rawInitialPayload,
|
|
2815
|
+
headers: recreateUserHeaders(request.headers),
|
|
2816
|
+
steps,
|
|
2817
|
+
url: workflowUrl,
|
|
2818
|
+
failureUrl: workflowFailureUrl,
|
|
2819
|
+
debug,
|
|
2820
|
+
env
|
|
2821
|
+
});
|
|
2822
|
+
const authCheck = await DisabledWorkflowContext.tryAuthentication(
|
|
2823
|
+
routeFunction,
|
|
2824
|
+
workflowContext
|
|
2825
|
+
);
|
|
2826
|
+
if (authCheck.isErr()) {
|
|
2827
|
+
await debug?.log("ERROR", "ERROR", { error: authCheck.error.message });
|
|
2828
|
+
throw authCheck.error;
|
|
2829
|
+
} else if (authCheck.value === "run-ended") {
|
|
2830
|
+
return onStepFinish("no-workflow-id", "auth-fail");
|
|
2831
|
+
}
|
|
2832
|
+
const callReturnCheck = await handleThirdPartyCallResult(
|
|
2833
|
+
request,
|
|
2834
|
+
rawInitialPayload,
|
|
2835
|
+
qstashClient,
|
|
2836
|
+
workflowUrl,
|
|
2837
|
+
workflowFailureUrl,
|
|
2838
|
+
debug
|
|
2839
|
+
);
|
|
2840
|
+
if (callReturnCheck.isErr()) {
|
|
2841
|
+
await debug?.log("ERROR", "SUBMIT_THIRD_PARTY_RESULT", {
|
|
2842
|
+
error: callReturnCheck.error.message
|
|
2843
|
+
});
|
|
2844
|
+
throw callReturnCheck.error;
|
|
2845
|
+
} else if (callReturnCheck.value === "continue-workflow") {
|
|
2846
|
+
const result = isFirstInvocation ? await triggerFirstInvocation(workflowContext, debug) : await triggerRouteFunction({
|
|
2847
|
+
onStep: async () => routeFunction(workflowContext),
|
|
2848
|
+
onCleanup: async () => {
|
|
2849
|
+
await triggerWorkflowDelete(workflowContext, debug);
|
|
2850
|
+
}
|
|
2851
|
+
});
|
|
2852
|
+
if (result.isErr()) {
|
|
2853
|
+
await debug?.log("ERROR", "ERROR", { error: result.error.message });
|
|
2854
|
+
throw result.error;
|
|
2855
|
+
}
|
|
2856
|
+
await debug?.log("INFO", "RESPONSE_WORKFLOW");
|
|
2857
|
+
return onStepFinish(workflowContext.workflowRunId, "success");
|
|
2858
|
+
}
|
|
2859
|
+
await debug?.log("INFO", "RESPONSE_DEFAULT");
|
|
2860
|
+
return onStepFinish("no-workflow-id", "fromCallback");
|
|
2861
|
+
};
|
|
2862
|
+
return async (request) => {
|
|
2863
|
+
try {
|
|
2864
|
+
return await handler(request);
|
|
2865
|
+
} catch (error) {
|
|
2866
|
+
console.error(error);
|
|
2867
|
+
return new Response(JSON.stringify(formatWorkflowError(error)), { status: 500 });
|
|
2868
|
+
}
|
|
2869
|
+
};
|
|
2870
|
+
};
|
|
2871
|
+
|
|
2872
|
+
// src/client/workflow/index.ts
|
|
2873
|
+
var Workflow = class {
|
|
2874
|
+
http;
|
|
2875
|
+
constructor(http) {
|
|
2876
|
+
this.http = http;
|
|
2877
|
+
}
|
|
2878
|
+
/**
|
|
2879
|
+
* Cancel an ongoing workflow
|
|
2880
|
+
*
|
|
2881
|
+
* @param workflowRunId run id of the workflow to delete
|
|
2882
|
+
* @returns true if workflow is succesfully deleted. Otherwise throws QStashError
|
|
2883
|
+
*/
|
|
2884
|
+
async cancel(workflowRunId) {
|
|
2885
|
+
const result = await this.http.request({
|
|
2886
|
+
path: ["v2", "workflows", "runs", `${workflowRunId}?cancel=true`],
|
|
2887
|
+
method: "DELETE",
|
|
2888
|
+
parseResponseAsJson: false
|
|
2889
|
+
});
|
|
2890
|
+
return result ?? true;
|
|
2891
|
+
}
|
|
2892
|
+
};
|
|
2893
|
+
|
|
2894
|
+
export {
|
|
2895
|
+
SignatureError,
|
|
2896
|
+
Receiver,
|
|
2897
|
+
QstashError,
|
|
2898
|
+
QstashRatelimitError,
|
|
2899
|
+
QstashChatRatelimitError,
|
|
2900
|
+
QstashDailyRatelimitError,
|
|
2901
|
+
QStashWorkflowError,
|
|
2902
|
+
QStashWorkflowAbort,
|
|
2903
|
+
formatWorkflowError,
|
|
2904
|
+
setupAnalytics,
|
|
2905
|
+
upstash,
|
|
2906
|
+
openai,
|
|
2907
|
+
custom,
|
|
2908
|
+
Chat,
|
|
2909
|
+
Messages,
|
|
2910
|
+
Schedules,
|
|
2911
|
+
UrlGroups,
|
|
2912
|
+
StepTypes,
|
|
2913
|
+
WorkflowContext,
|
|
2914
|
+
DisabledWorkflowContext,
|
|
2915
|
+
WorkflowLogger,
|
|
2916
|
+
processOptions,
|
|
2917
|
+
serve,
|
|
2918
|
+
Workflow,
|
|
2919
|
+
Client
|
|
2920
|
+
};
|