@volter/twin-upstash 0.1.0
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/LICENSE +202 -0
- package/README.md +202 -0
- package/api/src/fetch.ts +54 -0
- package/api/src/generated/surface.gen.json +1 -0
- package/api/src/generated/ui.gen.json +1 -0
- package/api/src/index.ts +19 -0
- package/api/src/key-gate.ts +30 -0
- package/api/src/manifest.ts +103 -0
- package/api/src/screens/developer-api.tsx +106 -0
- package/api/src/screens/qstash.tsx +99 -0
- package/api/src/screens/session.tsx +125 -0
- package/api/src/screens/teams.tsx +114 -0
- package/api/src/semantics/backups.ts +90 -0
- package/api/src/semantics/index.ts +191 -0
- package/api/src/semantics/shared.ts +42 -0
- package/api/src/semantics/teams.ts +108 -0
- package/api/src/semantics/time.ts +40 -0
- package/dist/api/src/fetch.d.ts +15 -0
- package/dist/api/src/fetch.js +44 -0
- package/dist/api/src/fetch.ts +54 -0
- package/dist/api/src/generated/surface.gen.json +1 -0
- package/dist/api/src/generated/ui.gen.json +1 -0
- package/dist/api/src/index.ts +19 -0
- package/dist/api/src/key-gate.d.ts +3 -0
- package/dist/api/src/key-gate.js +30 -0
- package/dist/api/src/key-gate.ts +30 -0
- package/dist/api/src/manifest.d.ts +2 -0
- package/dist/api/src/manifest.js +81 -0
- package/dist/api/src/manifest.ts +103 -0
- package/dist/api/src/screens/developer-api.d.ts +3 -0
- package/dist/api/src/screens/developer-api.js +101 -0
- package/dist/api/src/screens/developer-api.tsx +106 -0
- package/dist/api/src/screens/qstash.d.ts +3 -0
- package/dist/api/src/screens/qstash.js +92 -0
- package/dist/api/src/screens/qstash.tsx +99 -0
- package/dist/api/src/screens/session.d.ts +9 -0
- package/dist/api/src/screens/session.js +118 -0
- package/dist/api/src/screens/session.tsx +125 -0
- package/dist/api/src/screens/teams.d.ts +3 -0
- package/dist/api/src/screens/teams.js +99 -0
- package/dist/api/src/screens/teams.tsx +114 -0
- package/dist/api/src/semantics/backups.d.ts +7 -0
- package/dist/api/src/semantics/backups.js +75 -0
- package/dist/api/src/semantics/backups.ts +90 -0
- package/dist/api/src/semantics/index.d.ts +10 -0
- package/dist/api/src/semantics/index.js +191 -0
- package/dist/api/src/semantics/index.ts +191 -0
- package/dist/api/src/semantics/shared.d.ts +21 -0
- package/dist/api/src/semantics/shared.js +34 -0
- package/dist/api/src/semantics/shared.ts +42 -0
- package/dist/api/src/semantics/teams.d.ts +13 -0
- package/dist/api/src/semantics/teams.js +100 -0
- package/dist/api/src/semantics/teams.ts +108 -0
- package/dist/api/src/semantics/time.d.ts +2 -0
- package/dist/api/src/semantics/time.js +34 -0
- package/dist/api/src/semantics/time.ts +40 -0
- package/dist/qstash/src/doors.d.ts +6 -0
- package/dist/qstash/src/doors.js +33 -0
- package/dist/qstash/src/doors.ts +51 -0
- package/dist/qstash/src/egress.d.ts +7 -0
- package/dist/qstash/src/egress.js +66 -0
- package/dist/qstash/src/egress.ts +58 -0
- package/dist/qstash/src/fetch.d.ts +7 -0
- package/dist/qstash/src/fetch.js +48 -0
- package/dist/qstash/src/fetch.ts +46 -0
- package/dist/qstash/src/generated/surface.gen.json +1 -0
- package/dist/qstash/src/generated/ui.gen.json +1 -0
- package/dist/qstash/src/index.ts +35 -0
- package/dist/qstash/src/manifest.d.ts +10 -0
- package/dist/qstash/src/manifest.js +105 -0
- package/dist/qstash/src/manifest.ts +134 -0
- package/dist/qstash/src/semantics/account.d.ts +29 -0
- package/dist/qstash/src/semantics/account.js +91 -0
- package/dist/qstash/src/semantics/account.ts +98 -0
- package/dist/qstash/src/semantics/delivery.d.ts +17 -0
- package/dist/qstash/src/semantics/delivery.js +274 -0
- package/dist/qstash/src/semantics/delivery.ts +264 -0
- package/dist/qstash/src/semantics/dlq.d.ts +4 -0
- package/dist/qstash/src/semantics/dlq.js +51 -0
- package/dist/qstash/src/semantics/dlq.ts +61 -0
- package/dist/qstash/src/semantics/index.d.ts +2 -0
- package/dist/qstash/src/semantics/index.js +10 -0
- package/dist/qstash/src/semantics/index.ts +13 -0
- package/dist/qstash/src/semantics/keys.d.ts +2 -0
- package/dist/qstash/src/semantics/keys.js +9 -0
- package/dist/qstash/src/semantics/keys.ts +14 -0
- package/dist/qstash/src/semantics/messages.d.ts +74 -0
- package/dist/qstash/src/semantics/messages.js +233 -0
- package/dist/qstash/src/semantics/messages.ts +249 -0
- package/dist/qstash/src/semantics/queues.d.ts +2 -0
- package/dist/qstash/src/semantics/queues.js +60 -0
- package/dist/qstash/src/semantics/queues.ts +66 -0
- package/dist/qstash/src/semantics/schedules.d.ts +19 -0
- package/dist/qstash/src/semantics/schedules.js +125 -0
- package/dist/qstash/src/semantics/schedules.ts +132 -0
- package/dist/qstash/src/semantics/shared.d.ts +45 -0
- package/dist/qstash/src/semantics/shared.js +115 -0
- package/dist/qstash/src/semantics/shared.ts +121 -0
- package/dist/qstash/src/semantics/urlgroups.d.ts +2 -0
- package/dist/qstash/src/semantics/urlgroups.js +58 -0
- package/dist/qstash/src/semantics/urlgroups.ts +69 -0
- package/dist/qstash/src/semantics/workflows.d.ts +44 -0
- package/dist/qstash/src/semantics/workflows.js +379 -0
- package/dist/qstash/src/semantics/workflows.ts +401 -0
- package/dist/qstash/src/signing.d.ts +4 -0
- package/dist/qstash/src/signing.js +16 -0
- package/dist/qstash/src/signing.ts +19 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +35 -0
- package/dist/src/generated/surface.gen.json +1 -0
- package/dist/src/index.d.ts +18 -0
- package/dist/src/index.js +124 -0
- package/dist/src/manifest.d.ts +14 -0
- package/dist/src/manifest.js +8 -0
- package/dist/src/upstash-budget.d.ts +85 -0
- package/dist/src/upstash-budget.js +440 -0
- package/dist/src/upstash-capabilities.d.ts +4 -0
- package/dist/src/upstash-capabilities.js +1286 -0
- package/dist/src/upstash-conformance.d.ts +7 -0
- package/dist/src/upstash-conformance.js +119 -0
- package/dist/src/upstash-connector.d.ts +115 -0
- package/dist/src/upstash-connector.js +309 -0
- package/dist/src/upstash-lua.d.ts +140 -0
- package/dist/src/upstash-lua.js +1229 -0
- package/dist/src/upstash-server.d.ts +29 -0
- package/dist/src/upstash-server.js +81 -0
- package/dist/src/upstash-store.d.ts +114 -0
- package/dist/src/upstash-store.js +1663 -0
- package/dist/src/upstash-twin.d.ts +73 -0
- package/dist/src/upstash-twin.js +437 -0
- package/package.json +59 -0
- package/qstash/src/doors.ts +51 -0
- package/qstash/src/egress.ts +58 -0
- package/qstash/src/fetch.ts +46 -0
- package/qstash/src/generated/surface.gen.json +1 -0
- package/qstash/src/generated/ui.gen.json +1 -0
- package/qstash/src/index.ts +35 -0
- package/qstash/src/manifest.ts +134 -0
- package/qstash/src/semantics/account.ts +98 -0
- package/qstash/src/semantics/delivery.ts +264 -0
- package/qstash/src/semantics/dlq.ts +61 -0
- package/qstash/src/semantics/index.ts +13 -0
- package/qstash/src/semantics/keys.ts +14 -0
- package/qstash/src/semantics/messages.ts +249 -0
- package/qstash/src/semantics/queues.ts +66 -0
- package/qstash/src/semantics/schedules.ts +132 -0
- package/qstash/src/semantics/shared.ts +121 -0
- package/qstash/src/semantics/urlgroups.ts +69 -0
- package/qstash/src/semantics/workflows.ts +401 -0
- package/qstash/src/signing.ts +19 -0
- package/src/cli.ts +36 -0
- package/src/generated/surface.gen.json +1 -0
- package/src/index.ts +203 -0
- package/src/manifest.ts +26 -0
- package/src/upstash-budget.ts +486 -0
- package/src/upstash-capabilities.ts +1418 -0
- package/src/upstash-conformance.ts +131 -0
- package/src/upstash-connector.ts +340 -0
- package/src/upstash-lua.ts +1120 -0
- package/src/upstash-server.ts +103 -0
- package/src/upstash-store.ts +1437 -0
- package/src/upstash-twin.ts +465 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { DELIVERY, DESTINATION } from "./semantics/delivery.js";
|
|
2
|
+
/** A statement without an http(s) URL and a numeric status: refused, the World unchanged. */
|
|
3
|
+
function malformedStatement() {
|
|
4
|
+
return Response.json({ error: 'a url (http or https) and a status (a number) are required' }, { status: 400 });
|
|
5
|
+
}
|
|
6
|
+
/** The operation a door's context is named by: no spec operation is. */
|
|
7
|
+
export const DOOR = { id: 'TwinDoor', method: 'POST', path: '/_twin', class: 'action' };
|
|
8
|
+
/** A door's answer, or undefined for a request that is not a door's. `report` is what the catch-up in front did. */
|
|
9
|
+
export async function door(ctx, request, report) {
|
|
10
|
+
const url = new URL(request.url);
|
|
11
|
+
const path = url.pathname.replace(/\/+$/, '');
|
|
12
|
+
if (request.method === 'POST' && path === '/v2/_twin/drain')
|
|
13
|
+
return Response.json(report);
|
|
14
|
+
if (request.method === 'GET' && path === '/_twin/deliveries') {
|
|
15
|
+
const to = url.searchParams.get('to') ?? '';
|
|
16
|
+
if (!to)
|
|
17
|
+
return Response.json({ error: 'to is required' }, { status: 400 });
|
|
18
|
+
const deliveries = ctx.rowsRaw(DELIVERY).filter((d) => d.to === to)
|
|
19
|
+
.sort((a, b) => String(a.at).localeCompare(String(b.at)) || Number(a.n) - Number(b.n))
|
|
20
|
+
.map((d) => ({ messageId: d.message_id, retried: d.retried, at: d.at, method: d.method, headers: d.headers, body: d.body, status: d.status }));
|
|
21
|
+
return Response.json({ to, deliveries });
|
|
22
|
+
}
|
|
23
|
+
if (request.method === 'POST' && path === '/_twin/destinations') {
|
|
24
|
+
const b = (ctx.body && typeof ctx.body === 'object' ? ctx.body : {});
|
|
25
|
+
if (typeof b.url !== 'string' || !/^https?:\/\//.test(b.url) || typeof b.status !== 'number')
|
|
26
|
+
return malformedStatement();
|
|
27
|
+
const when = b.when && typeof b.when === 'object' ? b.when : {};
|
|
28
|
+
const id = `${b.url} ${Object.entries(when).map(([k, v]) => `${k.toLowerCase()}=${v}`).sort().join('&')}`.trim();
|
|
29
|
+
await ctx.record(DESTINATION, { url: b.url, status: b.status, body: typeof b.body === 'string' ? b.body : '', headers: b.headers ?? {}, when }, id);
|
|
30
|
+
return new Response(null, { status: 204 });
|
|
31
|
+
}
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// The World's doors in front of the QStash lane (docs/contributing/architecture.md, "Doors, screens and the gap"). QStash
|
|
2
|
+
// delivers to the customer's own HTTP endpoints, which are outside Upstash: what an endpoint received and how it answers
|
|
3
|
+
// are the World's, read and stated here, as Slack's app deliveries are read (packages/twin/slack/src/slack-interactions.ts,
|
|
4
|
+
// `GET /_twin/deliveries?to=`) and as an outside mail server's answer is stated (packages/twin/resend/src/resend-doors.ts,
|
|
5
|
+
// `POST /_twin/recipients/:domain`).
|
|
6
|
+
//
|
|
7
|
+
// - GET /_twin/deliveries?to=<url> → {to, deliveries}: every request QStash made to that URL, oldest first, each as the
|
|
8
|
+
// endpoint received it (its method, headers and body) and the status it answered.
|
|
9
|
+
// - POST /_twin/destinations {url, status, body?, headers?, when?} → 204: the World's word on how the endpoints under a URL
|
|
10
|
+
// answer, and, with `when` (header names and values), how they answer a delivery carrying those headers (a workflow's
|
|
11
|
+
// route answers its failure call, `Upstash-Workflow-Is-Failure: true`, apart from its steps). The statement that names
|
|
12
|
+
// headers the delivery carries decides before one that names none, then the longest prefix. A fact about the World, like a person's password: QStash's own
|
|
13
|
+
// delivery attempt reads it (semantics/delivery.ts), and retries an endpoint that answers an error. A destination the
|
|
14
|
+
// World states nothing for is asked over HTTP where the World lets the request out.
|
|
15
|
+
// - POST /v2/_twin/drain → the catch-up's report: the World's runner (a served World's drainer) asks the lane to run
|
|
16
|
+
// what the World clock has made due now. Every request the lane answers runs the same catch-up first; this one asks for
|
|
17
|
+
// nothing else. It is not QStash's API and answers only here.
|
|
18
|
+
import type { DerivedOperation, SemanticsContext } from '@volter/world-core';
|
|
19
|
+
import { DELIVERY, DESTINATION, type CatchUpReport } from './semantics/delivery.ts';
|
|
20
|
+
|
|
21
|
+
/** A statement without an http(s) URL and a numeric status: refused, the World unchanged. */
|
|
22
|
+
function malformedStatement(): Response {
|
|
23
|
+
return Response.json({ error: 'a url (http or https) and a status (a number) are required' }, { status: 400 });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The operation a door's context is named by: no spec operation is. */
|
|
27
|
+
export const DOOR: DerivedOperation = { id: 'TwinDoor', method: 'POST', path: '/_twin', class: 'action' };
|
|
28
|
+
|
|
29
|
+
/** A door's answer, or undefined for a request that is not a door's. `report` is what the catch-up in front did. */
|
|
30
|
+
export async function door(ctx: SemanticsContext, request: Request, report: CatchUpReport): Promise<Response | undefined> {
|
|
31
|
+
const url = new URL(request.url);
|
|
32
|
+
const path = url.pathname.replace(/\/+$/, '');
|
|
33
|
+
if (request.method === 'POST' && path === '/v2/_twin/drain') return Response.json(report);
|
|
34
|
+
if (request.method === 'GET' && path === '/_twin/deliveries') {
|
|
35
|
+
const to = url.searchParams.get('to') ?? '';
|
|
36
|
+
if (!to) return Response.json({ error: 'to is required' }, { status: 400 });
|
|
37
|
+
const deliveries = ctx.rowsRaw(DELIVERY).filter((d) => d.to === to)
|
|
38
|
+
.sort((a, b) => String(a.at).localeCompare(String(b.at)) || Number(a.n) - Number(b.n))
|
|
39
|
+
.map((d) => ({ messageId: d.message_id, retried: d.retried, at: d.at, method: d.method, headers: d.headers, body: d.body, status: d.status }));
|
|
40
|
+
return Response.json({ to, deliveries });
|
|
41
|
+
}
|
|
42
|
+
if (request.method === 'POST' && path === '/_twin/destinations') {
|
|
43
|
+
const b = (ctx.body && typeof ctx.body === 'object' ? ctx.body : {}) as Record<string, unknown>;
|
|
44
|
+
if (typeof b.url !== 'string' || !/^https?:\/\//.test(b.url) || typeof b.status !== 'number') return malformedStatement();
|
|
45
|
+
const when = b.when && typeof b.when === 'object' ? (b.when as Record<string, string>) : {};
|
|
46
|
+
const id = `${b.url} ${Object.entries(when).map(([k, v]) => `${k.toLowerCase()}=${v}`).sort().join('&')}`.trim();
|
|
47
|
+
await ctx.record(DESTINATION, { url: b.url, status: b.status, body: typeof b.body === 'string' ? b.body : '', headers: b.headers ?? {}, when }, id);
|
|
48
|
+
return new Response(null, { status: 204 });
|
|
49
|
+
}
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// The QStash lane's one outbound request: a delivery to the publisher's own destination (usually the application inside
|
|
2
|
+
// the World). The catch-up (semantics/delivery.ts) decides what is delivered and when; this performs one request and
|
|
3
|
+
// reports the destination's answer. The colocated twin host runs without the World's injector, so the kernel applies
|
|
4
|
+
// the World's rules before any socket opens: a destination on one of the application's own hostnames (its production
|
|
5
|
+
// URL, which an app in production mode publishes to) reaches the application where it listens (`appDestination`);
|
|
6
|
+
// otherwise `worldEgressRefusal` decides (internal addresses pass; a sealed World refuses the rest), and a refusal is a
|
|
7
|
+
// failed attempt, as an unreachable host is: QStash reports no answer as status -1.
|
|
8
|
+
import { appDestination, deliverToApp } from '@volter/world-core/app-route';
|
|
9
|
+
import { worldEgressRefusal } from '@volter/world-core/network-policy';
|
|
10
|
+
export async function post(url, method, headers, body) {
|
|
11
|
+
if (appDestination(url))
|
|
12
|
+
return toApp(url, method, headers, body);
|
|
13
|
+
const refusal = worldEgressRefusal(url);
|
|
14
|
+
return refusal !== null ? { status: -1, body: '', headers: {}, error: refusal } : send(url, method, headers, body);
|
|
15
|
+
}
|
|
16
|
+
/** The request itself, to a destination the World lets the request out to. */
|
|
17
|
+
async function send(url, method, headers, body) {
|
|
18
|
+
try {
|
|
19
|
+
const res = await fetch(url, { method, headers, ...(method !== 'GET' && method !== 'HEAD' ? { body } : {}) });
|
|
20
|
+
const out = {};
|
|
21
|
+
res.headers.forEach((v, k) => { out[k.toLowerCase()] = v; });
|
|
22
|
+
return { status: res.status, body: await res.text(), headers: out };
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
return { status: -1, body: '', headers: {}, error: error instanceof Error ? error.message : String(error) };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/** A delivery to the application, answered as the lane's other deliveries are (fetch's defaults: a redirect followed,
|
|
29
|
+
* at most 20, a 303, or a 301/302 after a POST, becoming a GET without its body), each hop routed again. */
|
|
30
|
+
async function toApp(url, method, headers, body) {
|
|
31
|
+
let at = url;
|
|
32
|
+
let m = method;
|
|
33
|
+
let b = body;
|
|
34
|
+
const h = { ...headers };
|
|
35
|
+
for (let hops = 0;; hops++) {
|
|
36
|
+
const app = appDestination(at);
|
|
37
|
+
// leaving the application drops what fetch drops crossing origins
|
|
38
|
+
if (!app) {
|
|
39
|
+
for (const k of Object.keys(h))
|
|
40
|
+
if (['authorization', 'cookie', 'proxy-authorization'].includes(k.toLowerCase()))
|
|
41
|
+
delete h[k];
|
|
42
|
+
return post(at, m, h, b ?? '');
|
|
43
|
+
}
|
|
44
|
+
let answer;
|
|
45
|
+
try {
|
|
46
|
+
answer = await deliverToApp(app, at, { method: m, headers: h, body: b });
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
return { status: -1, body: '', headers: {}, error: error instanceof Error ? error.message : String(error) };
|
|
50
|
+
}
|
|
51
|
+
const location = answer.headers.location;
|
|
52
|
+
if (![301, 302, 303, 307, 308].includes(answer.status) || !location)
|
|
53
|
+
return answer;
|
|
54
|
+
if (hops >= 20)
|
|
55
|
+
return { status: -1, body: '', headers: {}, error: 'redirect count exceeded' };
|
|
56
|
+
at = new URL(location, at).toString();
|
|
57
|
+
if (answer.status === 303 || ((answer.status === 301 || answer.status === 302) && m === 'POST')) {
|
|
58
|
+
if (m !== 'HEAD')
|
|
59
|
+
m = 'GET';
|
|
60
|
+
b = undefined;
|
|
61
|
+
for (const k of Object.keys(h))
|
|
62
|
+
if (/^content-/i.test(k))
|
|
63
|
+
delete h[k];
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// The QStash lane's one outbound request: a delivery to the publisher's own destination (usually the application inside
|
|
2
|
+
// the World). The catch-up (semantics/delivery.ts) decides what is delivered and when; this performs one request and
|
|
3
|
+
// reports the destination's answer. The colocated twin host runs without the World's injector, so the kernel applies
|
|
4
|
+
// the World's rules before any socket opens: a destination on one of the application's own hostnames (its production
|
|
5
|
+
// URL, which an app in production mode publishes to) reaches the application where it listens (`appDestination`);
|
|
6
|
+
// otherwise `worldEgressRefusal` decides (internal addresses pass; a sealed World refuses the rest), and a refusal is a
|
|
7
|
+
// failed attempt, as an unreachable host is: QStash reports no answer as status -1.
|
|
8
|
+
import { appDestination, deliverToApp } from '@volter/world-core/app-route';
|
|
9
|
+
import { worldEgressRefusal } from '@volter/world-core/network-policy';
|
|
10
|
+
|
|
11
|
+
export type Outcome = { status: number; body: string; headers: Record<string, string>; error?: string };
|
|
12
|
+
|
|
13
|
+
export async function post(url: string, method: string, headers: Record<string, string>, body: string): Promise<Outcome> {
|
|
14
|
+
if (appDestination(url)) return toApp(url, method, headers, body);
|
|
15
|
+
const refusal = worldEgressRefusal(url);
|
|
16
|
+
return refusal !== null ? { status: -1, body: '', headers: {}, error: refusal } : send(url, method, headers, body);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** The request itself, to a destination the World lets the request out to. */
|
|
20
|
+
async function send(url: string, method: string, headers: Record<string, string>, body: string): Promise<Outcome> {
|
|
21
|
+
try {
|
|
22
|
+
const res = await fetch(url, { method, headers, ...(method !== 'GET' && method !== 'HEAD' ? { body } : {}) });
|
|
23
|
+
const out: Record<string, string> = {};
|
|
24
|
+
res.headers.forEach((v, k) => { out[k.toLowerCase()] = v; });
|
|
25
|
+
return { status: res.status, body: await res.text(), headers: out };
|
|
26
|
+
} catch (error) {
|
|
27
|
+
return { status: -1, body: '', headers: {}, error: error instanceof Error ? error.message : String(error) };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** A delivery to the application, answered as the lane's other deliveries are (fetch's defaults: a redirect followed,
|
|
32
|
+
* at most 20, a 303, or a 301/302 after a POST, becoming a GET without its body), each hop routed again. */
|
|
33
|
+
async function toApp(url: string, method: string, headers: Record<string, string>, body: string): Promise<Outcome> {
|
|
34
|
+
let at = url; let m = method; let b: string | undefined = body; const h = { ...headers };
|
|
35
|
+
for (let hops = 0; ; hops++) {
|
|
36
|
+
const app = appDestination(at);
|
|
37
|
+
// leaving the application drops what fetch drops crossing origins
|
|
38
|
+
if (!app) {
|
|
39
|
+
for (const k of Object.keys(h)) if (['authorization', 'cookie', 'proxy-authorization'].includes(k.toLowerCase())) delete h[k];
|
|
40
|
+
return post(at, m, h, b ?? '');
|
|
41
|
+
}
|
|
42
|
+
let answer;
|
|
43
|
+
try {
|
|
44
|
+
answer = await deliverToApp(app, at, { method: m, headers: h, body: b });
|
|
45
|
+
} catch (error) {
|
|
46
|
+
return { status: -1, body: '', headers: {}, error: error instanceof Error ? error.message : String(error) };
|
|
47
|
+
}
|
|
48
|
+
const location = answer.headers.location;
|
|
49
|
+
if (![301, 302, 303, 307, 308].includes(answer.status) || !location) return answer;
|
|
50
|
+
if (hops >= 20) return { status: -1, body: '', headers: {}, error: 'redirect count exceeded' };
|
|
51
|
+
at = new URL(location, at).toString();
|
|
52
|
+
if (answer.status === 303 || ((answer.status === 301 || answer.status === 302) && m === 'POST')) {
|
|
53
|
+
if (m !== 'HEAD') m = 'GET';
|
|
54
|
+
b = undefined;
|
|
55
|
+
for (const k of Object.keys(h)) if (/^content-/i.test(k)) delete h[k];
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// The QStash lane's fetch: QStash's and Workflow's API through the derived dispatch over the two documents' union, with the
|
|
2
|
+
// World's doors and the lane's gate in front, and the catch-up before all of them. The upstash pack's fetch
|
|
3
|
+
// (../../src/upstash-server.ts) sends it every request for a QStash host, every `/v2/` path (an application reaches QStash
|
|
4
|
+
// at its QSTASH_URL, the pack's own address) and the lane's doors.
|
|
5
|
+
import { bindSemantics, coreFor, createDerivedFetch, crossCutting, semanticsContext } from '@volter/world-core';
|
|
6
|
+
import surface from './generated/surface.gen.json' with { type: 'json' };
|
|
7
|
+
import { manifest, spanning } from "./manifest.js";
|
|
8
|
+
import { door, DOOR } from "./doors.js";
|
|
9
|
+
import { gate } from "./semantics/account.js";
|
|
10
|
+
import { catchUp } from "./semantics/delivery.js";
|
|
11
|
+
import { qstashSemantics } from "./semantics/index.js";
|
|
12
|
+
// A catch-up delivers to the application, which may call QStash back while it answers (a workflow's route sends its next
|
|
13
|
+
// step): that request is answered without a catch-up of its own, and the one already running takes up what it publishes.
|
|
14
|
+
const catchingUp = new Set();
|
|
15
|
+
export function createQstashLaneFetch(options = {}) {
|
|
16
|
+
const scope = { ...(options.root !== undefined ? { root: options.root } : {}), ...(options.clock ? { clock: options.clock } : {}) };
|
|
17
|
+
const derived = createDerivedFetch({
|
|
18
|
+
surface: { ...surface, spanning },
|
|
19
|
+
handlers: bindSemantics(manifest, qstashSemantics, scope),
|
|
20
|
+
core: coreFor(manifest, scope),
|
|
21
|
+
around: crossCutting(manifest, { readOnly: options.readOnly ?? false, ...scope }),
|
|
22
|
+
// an operation the lane does not serve, and a path neither document has. Where the documents stop and the lane
|
|
23
|
+
// decides: QStash's answer to an unknown request is not documented; the lane answers 404 in the Error schema.
|
|
24
|
+
gap: () => Response.json({ error: 'Not Found' }, { status: 404 }),
|
|
25
|
+
});
|
|
26
|
+
return Object.assign(async (request) => {
|
|
27
|
+
// a body labelled JSON that does not parse is QStash's 400 (the derived dispatch answers it, `malformedBody`); the
|
|
28
|
+
// catch-up and the doors in front read no body of it
|
|
29
|
+
const ctx = await semanticsContext(manifest, request.clone(), DOOR, scope)
|
|
30
|
+
.catch(() => semanticsContext(manifest, new Request(request.url, { method: request.method, headers: request.headers }), DOOR, scope));
|
|
31
|
+
const key = options.root ?? '';
|
|
32
|
+
let report = { delivered: [], retried: [], failed: [] };
|
|
33
|
+
if (!options.readOnly && !catchingUp.has(key)) {
|
|
34
|
+
catchingUp.add(key);
|
|
35
|
+
try {
|
|
36
|
+
report = await catchUp(ctx);
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
catchingUp.delete(key);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const own = await door(ctx, request, report);
|
|
43
|
+
if (own)
|
|
44
|
+
return own;
|
|
45
|
+
const admitted = gate(ctx, request);
|
|
46
|
+
return 'refused' in admitted ? admitted.refused : derived(request);
|
|
47
|
+
}, { owners: () => derived.owners() });
|
|
48
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// The QStash lane's fetch: QStash's and Workflow's API through the derived dispatch over the two documents' union, with the
|
|
2
|
+
// World's doors and the lane's gate in front, and the catch-up before all of them. The upstash pack's fetch
|
|
3
|
+
// (../../src/upstash-server.ts) sends it every request for a QStash host, every `/v2/` path (an application reaches QStash
|
|
4
|
+
// at its QSTASH_URL, the pack's own address) and the lane's doors.
|
|
5
|
+
import { bindSemantics, coreFor, createDerivedFetch, crossCutting, semanticsContext, type DerivedFetch, type DerivedSurface } from '@volter/world-core';
|
|
6
|
+
import surface from './generated/surface.gen.json' with { type: 'json' };
|
|
7
|
+
import { manifest, spanning } from './manifest.ts';
|
|
8
|
+
import { door, DOOR } from './doors.ts';
|
|
9
|
+
import { gate } from './semantics/account.ts';
|
|
10
|
+
import { catchUp, type CatchUpReport } from './semantics/delivery.ts';
|
|
11
|
+
import { qstashSemantics } from './semantics/index.ts';
|
|
12
|
+
|
|
13
|
+
export type QstashLaneOptions = { root?: string; readOnly?: boolean; clock?: () => string };
|
|
14
|
+
|
|
15
|
+
// A catch-up delivers to the application, which may call QStash back while it answers (a workflow's route sends its next
|
|
16
|
+
// step): that request is answered without a catch-up of its own, and the one already running takes up what it publishes.
|
|
17
|
+
const catchingUp = new Set<string>();
|
|
18
|
+
|
|
19
|
+
export function createQstashLaneFetch(options: QstashLaneOptions = {}): DerivedFetch {
|
|
20
|
+
const scope = { ...(options.root !== undefined ? { root: options.root } : {}), ...(options.clock ? { clock: options.clock } : {}) };
|
|
21
|
+
const derived = createDerivedFetch({
|
|
22
|
+
surface: { ...(surface as unknown as DerivedSurface), spanning },
|
|
23
|
+
handlers: bindSemantics(manifest, qstashSemantics, scope),
|
|
24
|
+
core: coreFor(manifest, scope),
|
|
25
|
+
around: crossCutting(manifest, { readOnly: options.readOnly ?? false, ...scope }),
|
|
26
|
+
// an operation the lane does not serve, and a path neither document has. Where the documents stop and the lane
|
|
27
|
+
// decides: QStash's answer to an unknown request is not documented; the lane answers 404 in the Error schema.
|
|
28
|
+
gap: () => Response.json({ error: 'Not Found' }, { status: 404 }),
|
|
29
|
+
});
|
|
30
|
+
return Object.assign(async (request: Request): Promise<Response> => {
|
|
31
|
+
// a body labelled JSON that does not parse is QStash's 400 (the derived dispatch answers it, `malformedBody`); the
|
|
32
|
+
// catch-up and the doors in front read no body of it
|
|
33
|
+
const ctx = await semanticsContext(manifest, request.clone(), DOOR, scope)
|
|
34
|
+
.catch(() => semanticsContext(manifest, new Request(request.url, { method: request.method, headers: request.headers }), DOOR, scope));
|
|
35
|
+
const key = options.root ?? '';
|
|
36
|
+
let report: CatchUpReport = { delivered: [], retried: [], failed: [] };
|
|
37
|
+
if (!options.readOnly && !catchingUp.has(key)) {
|
|
38
|
+
catchingUp.add(key);
|
|
39
|
+
try { report = await catchUp(ctx); } finally { catchingUp.delete(key); }
|
|
40
|
+
}
|
|
41
|
+
const own = await door(ctx, request, report);
|
|
42
|
+
if (own) return own;
|
|
43
|
+
const admitted = gate(ctx, request);
|
|
44
|
+
return 'refused' in admitted ? admitted.refused : derived(request);
|
|
45
|
+
}, { owners: () => derived.owners() });
|
|
46
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"generatedBy":"scripts/derive-pack.ts","spec":"openapi","format":"openapi","version":"2.0.0","resources":[{"name":"BulkAction","schema":"BulkAction","fields":[{"name":"actionId","type":"string","nullable":false},{"name":"completedAt","type":"integer","nullable":false},{"name":"createdAt","type":"integer","nullable":false},{"name":"filter","type":"object","nullable":false},{"name":"kind","type":"string","nullable":false,"enum":["cancel"]},{"name":"processed","type":"integer","nullable":false},{"name":"state","type":"string","nullable":false,"enum":["IN_PROGRESS","FINISHED"]},{"name":"total","type":"integer","nullable":false}],"stateCandidates":[{"field":"state","values":["FINISHED","IN_PROGRESS"]}],"expandable":[]},{"name":"Endpoint","schema":"Endpoint","fields":[{"name":"name","type":"string","nullable":false},{"name":"url","type":"string","nullable":false}],"stateCandidates":[],"expandable":[]},{"name":"FlowControlKey","schema":"FlowControlKey","fields":[{"name":"flowControlKey","type":"string","nullable":false},{"name":"isPaused","type":"boolean","nullable":false},{"name":"isPinnedParallelism","type":"boolean","nullable":false},{"name":"isPinnedRate","type":"boolean","nullable":false},{"name":"parallelismCount","type":"integer","nullable":false},{"name":"parallelismMax","type":"integer","nullable":false},{"name":"rateCount","type":"integer","nullable":false},{"name":"rateMax","type":"integer","nullable":false},{"name":"ratePeriod","type":"integer","nullable":false},{"name":"ratePeriodStart","type":"integer","nullable":false},{"name":"waitListSize","type":"integer","nullable":false}],"stateCandidates":[],"expandable":[]},{"name":"LogEntry","schema":"LogEntry","fields":[{"name":"body","type":"string","nullable":false},{"name":"callback","type":"string","nullable":false},{"name":"callbackHeaders","type":"object","nullable":false},{"name":"endpointName","type":"string","nullable":false},{"name":"error","type":"string","nullable":false},{"name":"failureCallback","type":"string","nullable":false},{"name":"failureCallbackHeaders","type":"object","nullable":false},{"name":"header","type":"object","nullable":false},{"name":"label","type":"string","nullable":false},{"name":"labels","type":"array","nullable":false},{"name":"maxRetries","type":"integer","nullable":false},{"name":"messageId","type":"string","nullable":false},{"name":"method","type":"string","nullable":false},{"name":"nextDeliveryTime","type":"integer","nullable":false},{"name":"queueName","type":"string","nullable":false},{"name":"responseBody","type":"string","nullable":false},{"name":"responseHeaders","type":"object","nullable":false},{"name":"responseStatus","type":"integer","nullable":false},{"name":"retryDelayExpression","type":"string","nullable":false},{"name":"scheduleId","type":"string","nullable":false},{"name":"state","type":"string","nullable":false,"enum":["CREATED","ACTIVE","RETRY","ERROR","DELIVERED","FAILED","CANCEL_REQUESTED","CANCELLED"]},{"name":"time","type":"integer","nullable":false},{"name":"timeout","type":"integer","nullable":false},{"name":"topicName","type":"string","nullable":false},{"name":"url","type":"string","nullable":false}],"stateCandidates":[{"field":"state","values":["ACTIVE","CANCELLED","CANCEL_REQUESTED","CREATED","DELIVERED","ERROR","FAILED","RETRY"]}],"expandable":[]},{"name":"Queue","schema":"Queue","fields":[{"name":"createdAt","type":"integer","nullable":false},{"name":"lag","type":"integer","nullable":false},{"name":"name","type":"string","nullable":false},{"name":"parallelism","type":"integer","nullable":false},{"name":"paused","type":"boolean","nullable":false},{"name":"updatedAt","type":"integer","nullable":false}],"stateCandidates":[],"expandable":[]},{"name":"Schedule","schema":"Schedule","fields":[{"name":"body","type":"string","nullable":false},{"name":"callback","type":"string","nullable":false},{"name":"callerIp","type":"string","nullable":false},{"name":"createdAt","type":"integer","nullable":false,"required":true},{"name":"cron","type":"string","nullable":false,"required":true},{"name":"delay","type":"integer","nullable":false},{"name":"destination","type":"string","nullable":false,"required":true},{"name":"failureCallback","type":"string","nullable":false},{"name":"flowControlKey","type":"string","nullable":false},{"name":"header","type":"object","nullable":false},{"name":"isPaused","type":"boolean","nullable":false,"required":true},{"name":"label","type":"string","nullable":false},{"name":"labels","type":"array","nullable":false},{"name":"lastScheduleStates","type":"object","nullable":false},{"name":"lastScheduleTime","type":"integer","nullable":false},{"name":"method","type":"string","nullable":false,"required":true},{"name":"nextScheduleTime","type":"integer","nullable":false},{"name":"parallelism","type":"integer","nullable":false},{"name":"period","type":"integer","nullable":false},{"name":"rate","type":"integer","nullable":false},{"name":"retries","type":"integer","nullable":false},{"name":"retryDelayExpression","type":"string","nullable":false},{"name":"scheduleId","type":"string","nullable":false,"required":true}],"stateCandidates":[],"expandable":[]},{"name":"URLGroup","schema":"URLGroup","fields":[{"name":"createdAt","type":"integer","nullable":false},{"name":"endpoints","type":"array","nullable":false},{"name":"name","type":"string","nullable":false},{"name":"updatedAt","type":"integer","nullable":false}],"stateCandidates":[],"expandable":[]},{"name":"Waiter","schema":"Waiter","fields":[{"name":"deadline","type":"integer","nullable":false},{"name":"headers","type":"object","nullable":false},{"name":"timeoutBody","type":"string","nullable":false},{"name":"timeoutHeaders","type":"object","nullable":false},{"name":"timeoutUrl","type":"string","nullable":false},{"name":"url","type":"string","nullable":false}],"stateCandidates":[],"expandable":[]},{"name":"WorkflowDLQMessage","schema":"WorkflowDLQMessage","fields":[{"name":"body","type":"string","nullable":false},{"name":"bodyBase64","type":"string","nullable":false},{"name":"callerIP","type":"string","nullable":false},{"name":"createdAt","type":"integer","nullable":false},{"name":"dlqId","type":"string","nullable":false},{"name":"failureCallback","type":"string","nullable":false},{"name":"failureFunctionState","type":"string","nullable":false},{"name":"flowControlKey","type":"string","nullable":false},{"name":"header","type":"object","nullable":false},{"name":"label","type":"string","nullable":false},{"name":"labels","type":"array","nullable":false},{"name":"maxRetries","type":"integer","nullable":false},{"name":"method","type":"string","nullable":false},{"name":"responseBody","type":"string","nullable":false},{"name":"responseBodyBase64","type":"string","nullable":false},{"name":"responseHeader","type":"object","nullable":false},{"name":"responseStatus","type":"integer","nullable":false},{"name":"url","type":"string","nullable":false},{"name":"workflowCreatedAt","type":"integer","nullable":false},{"name":"workflowRunId","type":"string","nullable":false},{"name":"workflowUrl","type":"string","nullable":false}],"stateCandidates":[],"expandable":[]},{"name":"WorkflowRun","schema":"WorkflowRun","fields":[{"name":"dlqId","type":"string","nullable":false},{"name":"failureFunction","type":"FailureFunction","nullable":false},{"name":"flowControlKey","type":"string","nullable":false},{"name":"invoker","type":"InvokerContext","nullable":false},{"name":"label","type":"string","nullable":false},{"name":"labels","type":"array","nullable":false},{"name":"steps","type":"array","nullable":false},{"name":"workflowRunCallerIp","type":"string","nullable":false},{"name":"workflowRunCompletedAt","type":"integer","nullable":false},{"name":"workflowRunCreatedAt","type":"integer","nullable":false},{"name":"workflowRunId","type":"string","nullable":false},{"name":"workflowRunResponse","type":"string","nullable":false},{"name":"workflowState","type":"string","nullable":false},{"name":"workflowUrl","type":"string","nullable":false}],"stateCandidates":[],"expandable":[]}],"operations":[{"id":"post_v2_batch","method":"post","path":"/v2/batch","pathParams":[],"query":[],"body":[],"bodyEncoding":"json","successStatus":200,"class":"non-resource"},{"id":"post_v2_batch_trigger","method":"post","path":"/v2/batch/trigger","pathParams":[],"query":[],"body":[],"bodyEncoding":"json","successStatus":201,"class":"non-resource"},{"id":"get_v2_bulkactions","method":"get","path":"/v2/bulkActions","pathParams":[],"query":[{"name":"count","type":"integer","required":false},{"name":"cursor","type":"string","required":false},{"name":"state","type":"string","required":false}],"body":[],"bodyEncoding":"none","successStatus":200,"answers":{"resource":"BulkAction","list":true,"key":"bulkActions"},"class":"list","resource":"BulkAction"},{"id":"get_v2_bulkactions_actionid","method":"get","path":"/v2/bulkActions/{actionId}","pathParams":["actionId"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"answers":{"resource":"BulkAction","list":false},"class":"retrieve","resource":"BulkAction"},{"id":"get_v2_dlq","method":"get","path":"/v2/dlq","pathParams":[],"query":[{"name":"callerIp","type":"array","required":false},{"name":"count","type":"integer","required":false},{"name":"cursor","type":"string","required":false},{"name":"fromDate","type":"integer","required":false},{"name":"label","type":"array","required":false},{"name":"messageId","type":"string","required":false},{"name":"queueName","type":"array","required":false},{"name":"responseStatus","type":"array","required":false},{"name":"scheduleId","type":"array","required":false},{"name":"source","type":"string","required":false},{"name":"toDate","type":"integer","required":false},{"name":"topicName","type":"array","required":false},{"name":"url","type":"array","required":false}],"body":[],"bodyEncoding":"none","successStatus":200,"class":"computed"},{"id":"delete_v2_dlq","method":"delete","path":"/v2/dlq","pathParams":[],"query":[{"name":"callerIp","type":"array","required":false},{"name":"count","type":"integer","required":false},{"name":"cursor","type":"string","required":false},{"name":"dlqIds","type":"array","required":false},{"name":"flowControlKey","type":"array","required":false},{"name":"fromDate","type":"integer","required":false},{"name":"label","type":"array","required":false},{"name":"messageId","type":"string","required":false},{"name":"queueName","type":"array","required":false},{"name":"responseStatus","type":"array","required":false},{"name":"scheduleId","type":"array","required":false},{"name":"toDate","type":"integer","required":false},{"name":"topicName","type":"array","required":false},{"name":"url","type":"array","required":false}],"body":[],"bodyEncoding":"none","successStatus":200,"class":"action"},{"id":"get_v2_dlq_dlqid","method":"get","path":"/v2/dlq/{dlqId}","pathParams":["dlqId"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"class":"computed"},{"id":"delete_v2_dlq_dlqid","method":"delete","path":"/v2/dlq/{dlqId}","pathParams":["dlqId"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"class":"delete"},{"id":"post_v2_dlq_retry","method":"post","path":"/v2/dlq/retry","pathParams":[],"query":[{"name":"callerIp","type":"array","required":false},{"name":"dlqIds","type":"array","required":false},{"name":"flowControlKey","type":"array","required":false},{"name":"fromDate","type":"integer","required":false},{"name":"label","type":"array","required":false},{"name":"messageId","type":"string","required":false},{"name":"queueName","type":"array","required":false},{"name":"responseStatus","type":"array","required":false},{"name":"scheduleId","type":"array","required":false},{"name":"toDate","type":"integer","required":false},{"name":"topicName","type":"array","required":false},{"name":"url","type":"array","required":false}],"body":[],"bodyEncoding":"none","successStatus":201,"class":"non-resource"},{"id":"post_v2_dlq_retry_dlqid","method":"post","path":"/v2/dlq/retry/{dlqId}","pathParams":["dlqId"],"query":[],"body":[],"bodyEncoding":"none","successStatus":201,"class":"non-resource"},{"id":"post_v2_enqueue_queuename_destination","method":"post","path":"/v2/enqueue/{queueName}/{destination}","pathParams":["queueName","destination","queueName"],"query":[],"body":[],"bodyEncoding":"json","successStatus":200,"class":"non-resource"},{"id":"get_v2_flowcontrol","method":"get","path":"/v2/flowControl","pathParams":[],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"answers":{"resource":"FlowControlKey","list":true},"class":"list","resource":"FlowControlKey"},{"id":"get_v2_flowcontrol_flowcontrolkey","method":"get","path":"/v2/flowControl/{flowControlKey}","pathParams":["flowControlKey"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"answers":{"resource":"FlowControlKey","list":false},"class":"retrieve","resource":"FlowControlKey"},{"id":"post_v2_flowcontrol_flowcontrolkey_pause","method":"post","path":"/v2/flowControl/{flowControlKey}/pause","pathParams":["flowControlKey"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"class":"action","resource":"FlowControlKey"},{"id":"post_v2_flowcontrol_flowcontrolkey_pin","method":"post","path":"/v2/flowControl/{flowControlKey}/pin","pathParams":["flowControlKey"],"query":[{"name":"parallelism","type":"integer","required":false},{"name":"period","type":"integer","required":false},{"name":"rate","type":"integer","required":false}],"body":[],"bodyEncoding":"none","successStatus":200,"class":"action","resource":"FlowControlKey"},{"id":"post_v2_flowcontrol_flowcontrolkey_resetrate","method":"post","path":"/v2/flowControl/{flowControlKey}/resetRate","pathParams":["flowControlKey"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"class":"action","resource":"FlowControlKey"},{"id":"post_v2_flowcontrol_flowcontrolkey_resume","method":"post","path":"/v2/flowControl/{flowControlKey}/resume","pathParams":["flowControlKey"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"class":"action","resource":"FlowControlKey"},{"id":"post_v2_flowcontrol_flowcontrolkey_unpin","method":"post","path":"/v2/flowControl/{flowControlKey}/unpin","pathParams":["flowControlKey"],"query":[{"name":"parallelism","type":"boolean","required":false},{"name":"rate","type":"boolean","required":false}],"body":[],"bodyEncoding":"none","successStatus":200,"class":"action","resource":"FlowControlKey"},{"id":"get_v2_globalparallelism","method":"get","path":"/v2/globalParallelism","pathParams":[],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"class":"computed"},{"id":"get_v2_keys","method":"get","path":"/v2/keys","pathParams":[],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"class":"computed"},{"id":"get_v2_keys_rotate","method":"get","path":"/v2/keys/rotate","pathParams":["flowControlKey"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"answers":{"resource":"FlowControlKey","list":false},"class":"retrieve","resource":"FlowControlKey"},{"id":"post_v2_keys_rotate","method":"post","path":"/v2/keys/rotate","pathParams":[],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"class":"non-resource"},{"id":"get_v2_logs","method":"get","path":"/v2/logs","pathParams":[],"query":[{"name":"count","type":"integer","required":false},{"name":"cursor","type":"string","required":false},{"name":"fromDate","type":"integer","required":false},{"name":"label","type":"array","required":false},{"name":"messageId","type":"string","required":false},{"name":"queueName","type":"string","required":false},{"name":"scheduleId","type":"string","required":false},{"name":"state","type":"string","required":false},{"name":"toDate","type":"integer","required":false},{"name":"topicName","type":"string","required":false},{"name":"url","type":"string","required":false}],"body":[],"bodyEncoding":"none","successStatus":200,"answers":{"resource":"LogEntry","list":true,"key":"logs"},"class":"list","resource":"LogEntry"},{"id":"delete_v2_messages","method":"delete","path":"/v2/messages","pathParams":[],"query":[{"name":"async","type":"boolean","required":false},{"name":"callerIp","type":"array","required":false},{"name":"callerIP","type":"array","required":false},{"name":"count","type":"integer","required":false},{"name":"flowControlKey","type":"array","required":false},{"name":"fromDate","type":"integer","required":false},{"name":"host","type":"array","required":false},{"name":"label","type":"array","required":false},{"name":"messageIds","type":"array","required":false},{"name":"path","type":"array","required":false},{"name":"queueName","type":"array","required":false},{"name":"scheduleId","type":"array","required":false},{"name":"toDate","type":"integer","required":false},{"name":"topicName","type":"array","required":false},{"name":"url","type":"array","required":false}],"body":[],"bodyEncoding":"none","successStatus":202,"class":"action"},{"id":"get_v2_messages_messageid","method":"get","path":"/v2/messages/{messageId}","pathParams":["messageId"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"class":"computed"},{"id":"delete_v2_messages_messageid","method":"delete","path":"/v2/messages/{messageId}","pathParams":["messageId"],"query":[],"body":[],"bodyEncoding":"none","successStatus":202,"class":"delete"},{"id":"post_v2_messages_messageid_retry","method":"post","path":"/v2/messages/{messageId}/retry","pathParams":["messageId"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"class":"non-resource"},{"id":"post_v2_notify_eventid","method":"post","path":"/v2/notify/{eventId}","pathParams":["eventId"],"query":[],"body":[],"bodyEncoding":"json","successStatus":201,"class":"non-resource"},{"id":"post_v2_notify_workflowrunid_eventid","method":"post","path":"/v2/notify/{workflowRunId}/{eventId}","pathParams":["workflowRunId","eventId"],"query":[],"body":[],"bodyEncoding":"json","successStatus":201,"answers":{"resource":"Waiter","list":false,"key":"waiter"},"class":"action","resource":"Waiter"},{"id":"post_v2_publish_destination","method":"post","path":"/v2/publish/{destination}","pathParams":["destination"],"query":[],"body":[],"bodyEncoding":"json","successStatus":200,"class":"non-resource"},{"id":"get_v2_queues","method":"get","path":"/v2/queues","pathParams":[],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"answers":{"resource":"Queue","list":true},"class":"list","resource":"Queue"},{"id":"post_v2_queues","method":"post","path":"/v2/queues","pathParams":[],"query":[],"body":[{"name":"parallelism","type":"integer","required":true},{"name":"queueName","type":"string","required":true}],"bodyEncoding":"json","successStatus":200,"class":"non-resource"},{"id":"get_v2_queues_queuename","method":"get","path":"/v2/queues/{queueName}","pathParams":["queueName"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"answers":{"resource":"Queue","list":false},"class":"retrieve","resource":"Queue"},{"id":"delete_v2_queues_queuename","method":"delete","path":"/v2/queues/{queueName}","pathParams":["queueName"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"class":"delete","resource":"Queue"},{"id":"post_v2_queues_queuename_pause","method":"post","path":"/v2/queues/{queueName}/pause","pathParams":["queueName"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"class":"action","resource":"Queue"},{"id":"post_v2_queues_queuename_resume","method":"post","path":"/v2/queues/{queueName}/resume","pathParams":["queueName"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"class":"action","resource":"Queue"},{"id":"get_v2_schedules","method":"get","path":"/v2/schedules","pathParams":[],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"answers":{"resource":"Schedule","list":true},"class":"list","resource":"Schedule"},{"id":"post_v2_schedules_destination","method":"post","path":"/v2/schedules/{destination}","pathParams":["destination"],"query":[],"body":[],"bodyEncoding":"json","successStatus":200,"class":"non-resource"},{"id":"get_v2_schedules_scheduleid","method":"get","path":"/v2/schedules/{scheduleId}","pathParams":["scheduleId"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"answers":{"resource":"Schedule","list":false},"class":"retrieve","resource":"Schedule"},{"id":"delete_v2_schedules_scheduleid","method":"delete","path":"/v2/schedules/{scheduleId}","pathParams":["scheduleId"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"class":"delete","resource":"Schedule"},{"id":"post_v2_schedules_scheduleid_pause","method":"post","path":"/v2/schedules/{scheduleId}/pause","pathParams":["scheduleId"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"class":"action","resource":"Schedule"},{"id":"patch_v2_schedules_scheduleid_pause","method":"patch","path":"/v2/schedules/{scheduleId}/pause","pathParams":["scheduleId"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"class":"action","resource":"Schedule"},{"id":"post_v2_schedules_scheduleid_resume","method":"post","path":"/v2/schedules/{scheduleId}/resume","pathParams":["scheduleId"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"class":"action","resource":"Schedule"},{"id":"patch_v2_schedules_scheduleid_resume","method":"patch","path":"/v2/schedules/{scheduleId}/resume","pathParams":["scheduleId"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"class":"action","resource":"Schedule"},{"id":"get_v2_topics","method":"get","path":"/v2/topics","pathParams":[],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"answers":{"resource":"URLGroup","list":true},"class":"list","resource":"URLGroup"},{"id":"get_v2_topics_urlgroupname","method":"get","path":"/v2/topics/{urlGroupName}","pathParams":["urlGroupName"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"answers":{"resource":"URLGroup","list":false},"class":"retrieve","resource":"URLGroup"},{"id":"delete_v2_topics_urlgroupname","method":"delete","path":"/v2/topics/{urlGroupName}","pathParams":["urlGroupName"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"class":"delete","resource":"URLGroup"},{"id":"post_v2_topics_urlgroupname_endpoints","method":"post","path":"/v2/topics/{urlGroupName}/endpoints","pathParams":["urlGroupName"],"query":[],"body":[{"name":"endpoints","type":"array","required":true}],"bodyEncoding":"json","successStatus":200,"class":"action","resource":"URLGroup"},{"id":"delete_v2_topics_urlgroupname_endpoints","method":"delete","path":"/v2/topics/{urlGroupName}/endpoints","pathParams":["urlGroupName"],"query":[],"body":[{"name":"endpoints","type":"array","required":true}],"bodyEncoding":"json","successStatus":200,"class":"action","resource":"URLGroup"},{"id":"post_v2_trigger_workflowurl","method":"post","path":"/v2/trigger/{workflowUrl}","pathParams":["workflowUrl"],"query":[],"body":[],"bodyEncoding":"json","successStatus":200,"class":"non-resource"},{"id":"post_v2_wait_eventid","method":"post","path":"/v2/wait/{eventId}","pathParams":["eventId"],"query":[],"body":[{"name":"step","type":"object","required":false},{"name":"timeout","type":"string","required":false},{"name":"timeoutBody","type":"string","required":false},{"name":"timeoutHeaders","type":"object","required":false},{"name":"timeoutUrl","type":"string","required":false},{"name":"url","type":"string","required":false}],"bodyEncoding":"json","successStatus":200,"class":"non-resource"},{"id":"get_v2_waiters_eventid","method":"get","path":"/v2/waiters/{eventId}","pathParams":["eventId"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"answers":{"resource":"Waiter","list":true},"class":"list","resource":"Waiter"},{"id":"get_v2_workflows_bulkactions","method":"get","path":"/v2/workflows/bulkActions","pathParams":[],"query":[{"name":"count","type":"integer","required":false},{"name":"cursor","type":"string","required":false},{"name":"state","type":"string","required":false}],"body":[],"bodyEncoding":"none","successStatus":200,"answers":{"resource":"BulkAction","list":true,"key":"bulkActions"},"class":"list","resource":"BulkAction"},{"id":"get_v2_workflows_bulkactions_actionid","method":"get","path":"/v2/workflows/bulkActions/{actionId}","pathParams":["actionId"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"answers":{"resource":"BulkAction","list":false},"class":"retrieve","resource":"BulkAction"},{"id":"get_v2_workflows_dlq","method":"get","path":"/v2/workflows/dlq","pathParams":[],"query":[{"name":"callerIp","type":"array","required":false},{"name":"count","type":"integer","required":false},{"name":"cursor","type":"string","required":false},{"name":"failureFunctionState","type":"array","required":false},{"name":"flowControlKey","type":"array","required":false},{"name":"fromDate","type":"integer","required":false},{"name":"label","type":"array","required":false},{"name":"toDate","type":"integer","required":false},{"name":"workflowCreatedAt","type":"integer","required":false},{"name":"workflowRunId","type":"string","required":false},{"name":"workflowUrl","type":"array","required":false}],"body":[],"bodyEncoding":"none","successStatus":200,"answers":{"resource":"WorkflowDLQMessage","list":true,"key":"messages"},"class":"list","resource":"WorkflowDLQMessage"},{"id":"delete_v2_workflows_dlq","method":"delete","path":"/v2/workflows/dlq","pathParams":[],"query":[{"name":"callerIp","type":"array","required":false},{"name":"count","type":"integer","required":false},{"name":"cursor","type":"string","required":false},{"name":"dlqIds","type":"array","required":false},{"name":"failureFunctionState","type":"array","required":false},{"name":"flowControlKey","type":"array","required":false},{"name":"fromDate","type":"integer","required":false},{"name":"label","type":"array","required":false},{"name":"toDate","type":"integer","required":false},{"name":"workflowCreatedAt","type":"integer","required":false},{"name":"workflowRunId","type":"string","required":false},{"name":"workflowUrl","type":"array","required":false}],"body":[],"bodyEncoding":"none","successStatus":200,"class":"action"},{"id":"get_v2_workflows_dlq_dlqid","method":"get","path":"/v2/workflows/dlq/{dlqId}","pathParams":["dlqId"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"answers":{"resource":"WorkflowDLQMessage","list":false},"class":"retrieve","resource":"WorkflowDLQMessage"},{"id":"delete_v2_workflows_dlq_dlqid","method":"delete","path":"/v2/workflows/dlq/{dlqId}","pathParams":["dlqId"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"class":"delete","resource":"WorkflowDLQMessage"},{"id":"post_v2_workflows_dlq_callback_dlqid","method":"post","path":"/v2/workflows/dlq/callback/{dlqId}","pathParams":["dlqId"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"class":"non-resource"},{"id":"delete_v2_workflows_dlq_callback_dlqid","method":"delete","path":"/v2/workflows/dlq/callback/{dlqId}","pathParams":["dlqId"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"class":"delete"},{"id":"post_v2_workflows_dlq_restart","method":"post","path":"/v2/workflows/dlq/restart","pathParams":[],"query":[{"name":"callerIp","type":"array","required":false},{"name":"count","type":"integer","required":false},{"name":"cursor","type":"string","required":false},{"name":"dlqIds","type":"array","required":false},{"name":"failureFunctionState","type":"array","required":false},{"name":"flowControlKey","type":"array","required":false},{"name":"fromDate","type":"integer","required":false},{"name":"label","type":"array","required":false},{"name":"toDate","type":"integer","required":false},{"name":"workflowCreatedAt","type":"integer","required":false},{"name":"workflowRunId","type":"string","required":false},{"name":"workflowUrl","type":"array","required":false}],"body":[],"bodyEncoding":"none","successStatus":200,"class":"non-resource"},{"id":"post_v2_workflows_dlq_restart_dlqid","method":"post","path":"/v2/workflows/dlq/restart/{dlqId}","pathParams":["dlqId"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"class":"non-resource"},{"id":"post_v2_workflows_dlq_resume","method":"post","path":"/v2/workflows/dlq/resume","pathParams":[],"query":[{"name":"callerIp","type":"array","required":false},{"name":"count","type":"integer","required":false},{"name":"cursor","type":"string","required":false},{"name":"dlqIds","type":"array","required":false},{"name":"failureFunctionState","type":"array","required":false},{"name":"flowControlKey","type":"array","required":false},{"name":"fromDate","type":"integer","required":false},{"name":"label","type":"array","required":false},{"name":"toDate","type":"integer","required":false},{"name":"workflowCreatedAt","type":"integer","required":false},{"name":"workflowRunId","type":"string","required":false},{"name":"workflowUrl","type":"array","required":false}],"body":[],"bodyEncoding":"none","successStatus":200,"class":"non-resource"},{"id":"post_v2_workflows_dlq_resume_dlqid","method":"post","path":"/v2/workflows/dlq/resume/{dlqId}","pathParams":["dlqId"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"class":"non-resource"},{"id":"get_v2_workflows_logs","method":"get","path":"/v2/workflows/logs","pathParams":[],"query":[{"name":"callerIp","type":"string","required":false},{"name":"count","type":"integer","required":false},{"name":"cursor","type":"string","required":false},{"name":"flowControlKey","type":"string","required":false},{"name":"fromDate","type":"integer","required":false},{"name":"label","type":"array","required":false},{"name":"state","type":"string","required":false},{"name":"toDate","type":"integer","required":false},{"name":"trimBody","type":"integer","required":false},{"name":"workflowCreatedAt","type":"integer","required":false},{"name":"workflowRunId","type":"string","required":false},{"name":"workflowRuns","type":"string","required":false},{"name":"workflowUrl","type":"string","required":false}],"body":[],"bodyEncoding":"none","successStatus":200,"answers":{"resource":"WorkflowRun","list":true,"key":"runs"},"class":"list","resource":"WorkflowRun"},{"id":"delete_v2_workflows_runs","method":"delete","path":"/v2/workflows/runs","pathParams":[],"query":[{"name":"async","type":"boolean","required":false},{"name":"callerIp","type":"array","required":false},{"name":"count","type":"integer","required":false},{"name":"flowControlKey","type":"array","required":false},{"name":"fromDate","type":"number","required":false},{"name":"host","type":"array","required":false},{"name":"label","type":"array","required":false},{"name":"path","type":"array","required":false},{"name":"toDate","type":"number","required":false},{"name":"workflowRunIds","type":"array","required":false},{"name":"workflowUrl","type":"array","required":false},{"name":"workflowUrlExactMatch","type":"boolean","required":false}],"body":[],"bodyEncoding":"none","successStatus":200,"class":"action"},{"id":"delete_v2_workflows_runs_workflowrunid","method":"delete","path":"/v2/workflows/runs/{workflowRunId}","pathParams":["workflowRunId"],"query":[{"name":"cancel","type":"boolean","required":false}],"body":[],"bodyEncoding":"none","successStatus":200,"class":"delete"}]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"generatedBy":"scripts/derive-pack.ts","resources":{"Message":{"actions":[],"states":{"state":["ACTIVE","CANCELLED","CANCEL_REQUESTED","CREATED","DELIVERED","FAILED","RETRY"]},"embeds":{}},"Queue":{"retrieve":"/v2/queues/{queueName}","actions":[],"states":{},"embeds":{}},"Schedule":{"actions":[{"operation":"post_v2_schedules_scheduleid_pause","method":"POST","path":"/v2/schedules/{scheduleId}/pause","field":"isPaused","from":["true"]},{"operation":"post_v2_schedules_scheduleid_pause","method":"POST","path":"/v2/schedules/{scheduleId}/pause","field":"isPaused","from":["false"],"to":"true"},{"operation":"patch_v2_schedules_scheduleid_pause","method":"PATCH","path":"/v2/schedules/{scheduleId}/pause","field":"isPaused","from":["true"]},{"operation":"patch_v2_schedules_scheduleid_pause","method":"PATCH","path":"/v2/schedules/{scheduleId}/pause","field":"isPaused","from":["false"],"to":"true"}],"states":{"isPaused":["false","true"]},"embeds":{}},"URLGroup":{"list":"/v2/topics","retrieve":"/v2/topics/{urlGroupName}","actions":[],"states":{},"embeds":{}},"DLQMessage":{"actions":[],"states":{},"embeds":{}},"WorkflowRun":{"actions":[],"states":{"workflowState":["RUN_CANCELED","RUN_FAILED","RUN_STARTED","RUN_SUCCESS"]},"embeds":{}},"WorkflowDLQMessage":{"list":"/v2/workflows/dlq","actions":[],"states":{},"embeds":{}},"Waiter":{"actions":[],"states":{},"embeds":{}}}}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// The QStash lane of the upstash pack (QStash and Upstash Workflow, one API in two documents), as a plain fetch: the
|
|
2
|
+
// whole pack's fetch (the Redis REST wire, the Developer API and console at api.upstash.com and console.upstash.com, and
|
|
3
|
+
// QStash), whose owners are the operations this lane serves. Its life walks through them: a customer signs in to the
|
|
4
|
+
// console for its QStash token and keys, and publishes, schedules, queues and runs workflows through QStash's API
|
|
5
|
+
// (journeys/customer-life.outline.md).
|
|
6
|
+
import type { DerivedFetch } from '@volter/world-core';
|
|
7
|
+
import { createUpstashRedisTwinFetch } from '../../src/upstash-server.ts';
|
|
8
|
+
import { createQstashLaneFetch, type QstashLaneOptions } from './fetch.ts';
|
|
9
|
+
|
|
10
|
+
export { manifest } from './manifest.ts';
|
|
11
|
+
export { createQstashLaneFetch } from './fetch.ts';
|
|
12
|
+
|
|
13
|
+
export function createQstashTwinFetch(options: QstashLaneOptions = {}): DerivedFetch {
|
|
14
|
+
const lane = createQstashLaneFetch(options);
|
|
15
|
+
const pack = createUpstashRedisTwinFetch({ ...(options.root !== undefined ? { root: options.root } : {}), ...(options.readOnly !== undefined ? { readOnly: options.readOnly } : {}), qstash: lane });
|
|
16
|
+
return Object.assign((request: Request) => pack(request), { owners: () => lane.owners() });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** The pack's views of the lane's answers, for a journey step's `view` (scripts/journey-kit.ts). `steps`: the deliveries
|
|
20
|
+
* door's answer with each workflow delivery's body read as `serve()` reads it: its steps, each step's published body
|
|
21
|
+
* decoded from base64 (and parsed, where it is JSON). */
|
|
22
|
+
export const answerViews: Record<string, (body: unknown) => unknown> = {
|
|
23
|
+
steps: (body) => {
|
|
24
|
+
const b = body as { deliveries?: Array<Record<string, unknown>> };
|
|
25
|
+
const decode = (s: string): unknown => { const t = Buffer.from(s, 'base64').toString('utf8'); try { return JSON.parse(t); } catch { return t; } };
|
|
26
|
+
return {
|
|
27
|
+
...b,
|
|
28
|
+
deliveries: (b.deliveries ?? []).map((d) => {
|
|
29
|
+
let raw: unknown;
|
|
30
|
+
try { raw = JSON.parse(String(d.body)); } catch { return d; }
|
|
31
|
+
return Array.isArray(raw) ? { ...d, steps: raw.map((r: Record<string, unknown>) => ({ ...r, body: decode(String(r.body ?? '')) })) } : d;
|
|
32
|
+
}),
|
|
33
|
+
};
|
|
34
|
+
},
|
|
35
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { DerivedManifest } from '@volter/world-core';
|
|
2
|
+
/** The path parameters whose values hold slashes: a destination is a whole URL (`/v2/publish/https://example.com/x`). */
|
|
3
|
+
export declare const spanning: string[];
|
|
4
|
+
/** The hosts the lane serves: "https://qstash-{region}.upstash.io" with the regions us-east-1 and eu-central-1 (both
|
|
5
|
+
* documents' `servers`), and qstash.upstash.io, the host Upstash's pages send their examples to. An application pointed at
|
|
6
|
+
* the World through QSTASH_URL reaches the lane at the pack's own address instead. SHAPE judges the lane's life on these. */
|
|
7
|
+
export declare const hosts: {
|
|
8
|
+
host: string;
|
|
9
|
+
}[];
|
|
10
|
+
export declare const manifest: DerivedManifest;
|