@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.
Files changed (162) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +202 -0
  3. package/api/src/fetch.ts +54 -0
  4. package/api/src/generated/surface.gen.json +1 -0
  5. package/api/src/generated/ui.gen.json +1 -0
  6. package/api/src/index.ts +19 -0
  7. package/api/src/key-gate.ts +30 -0
  8. package/api/src/manifest.ts +103 -0
  9. package/api/src/screens/developer-api.tsx +106 -0
  10. package/api/src/screens/qstash.tsx +99 -0
  11. package/api/src/screens/session.tsx +125 -0
  12. package/api/src/screens/teams.tsx +114 -0
  13. package/api/src/semantics/backups.ts +90 -0
  14. package/api/src/semantics/index.ts +191 -0
  15. package/api/src/semantics/shared.ts +42 -0
  16. package/api/src/semantics/teams.ts +108 -0
  17. package/api/src/semantics/time.ts +40 -0
  18. package/dist/api/src/fetch.d.ts +15 -0
  19. package/dist/api/src/fetch.js +44 -0
  20. package/dist/api/src/fetch.ts +54 -0
  21. package/dist/api/src/generated/surface.gen.json +1 -0
  22. package/dist/api/src/generated/ui.gen.json +1 -0
  23. package/dist/api/src/index.ts +19 -0
  24. package/dist/api/src/key-gate.d.ts +3 -0
  25. package/dist/api/src/key-gate.js +30 -0
  26. package/dist/api/src/key-gate.ts +30 -0
  27. package/dist/api/src/manifest.d.ts +2 -0
  28. package/dist/api/src/manifest.js +81 -0
  29. package/dist/api/src/manifest.ts +103 -0
  30. package/dist/api/src/screens/developer-api.d.ts +3 -0
  31. package/dist/api/src/screens/developer-api.js +101 -0
  32. package/dist/api/src/screens/developer-api.tsx +106 -0
  33. package/dist/api/src/screens/qstash.d.ts +3 -0
  34. package/dist/api/src/screens/qstash.js +92 -0
  35. package/dist/api/src/screens/qstash.tsx +99 -0
  36. package/dist/api/src/screens/session.d.ts +9 -0
  37. package/dist/api/src/screens/session.js +118 -0
  38. package/dist/api/src/screens/session.tsx +125 -0
  39. package/dist/api/src/screens/teams.d.ts +3 -0
  40. package/dist/api/src/screens/teams.js +99 -0
  41. package/dist/api/src/screens/teams.tsx +114 -0
  42. package/dist/api/src/semantics/backups.d.ts +7 -0
  43. package/dist/api/src/semantics/backups.js +75 -0
  44. package/dist/api/src/semantics/backups.ts +90 -0
  45. package/dist/api/src/semantics/index.d.ts +10 -0
  46. package/dist/api/src/semantics/index.js +191 -0
  47. package/dist/api/src/semantics/index.ts +191 -0
  48. package/dist/api/src/semantics/shared.d.ts +21 -0
  49. package/dist/api/src/semantics/shared.js +34 -0
  50. package/dist/api/src/semantics/shared.ts +42 -0
  51. package/dist/api/src/semantics/teams.d.ts +13 -0
  52. package/dist/api/src/semantics/teams.js +100 -0
  53. package/dist/api/src/semantics/teams.ts +108 -0
  54. package/dist/api/src/semantics/time.d.ts +2 -0
  55. package/dist/api/src/semantics/time.js +34 -0
  56. package/dist/api/src/semantics/time.ts +40 -0
  57. package/dist/qstash/src/doors.d.ts +6 -0
  58. package/dist/qstash/src/doors.js +33 -0
  59. package/dist/qstash/src/doors.ts +51 -0
  60. package/dist/qstash/src/egress.d.ts +7 -0
  61. package/dist/qstash/src/egress.js +66 -0
  62. package/dist/qstash/src/egress.ts +58 -0
  63. package/dist/qstash/src/fetch.d.ts +7 -0
  64. package/dist/qstash/src/fetch.js +48 -0
  65. package/dist/qstash/src/fetch.ts +46 -0
  66. package/dist/qstash/src/generated/surface.gen.json +1 -0
  67. package/dist/qstash/src/generated/ui.gen.json +1 -0
  68. package/dist/qstash/src/index.ts +35 -0
  69. package/dist/qstash/src/manifest.d.ts +10 -0
  70. package/dist/qstash/src/manifest.js +105 -0
  71. package/dist/qstash/src/manifest.ts +134 -0
  72. package/dist/qstash/src/semantics/account.d.ts +29 -0
  73. package/dist/qstash/src/semantics/account.js +91 -0
  74. package/dist/qstash/src/semantics/account.ts +98 -0
  75. package/dist/qstash/src/semantics/delivery.d.ts +17 -0
  76. package/dist/qstash/src/semantics/delivery.js +274 -0
  77. package/dist/qstash/src/semantics/delivery.ts +264 -0
  78. package/dist/qstash/src/semantics/dlq.d.ts +4 -0
  79. package/dist/qstash/src/semantics/dlq.js +51 -0
  80. package/dist/qstash/src/semantics/dlq.ts +61 -0
  81. package/dist/qstash/src/semantics/index.d.ts +2 -0
  82. package/dist/qstash/src/semantics/index.js +10 -0
  83. package/dist/qstash/src/semantics/index.ts +13 -0
  84. package/dist/qstash/src/semantics/keys.d.ts +2 -0
  85. package/dist/qstash/src/semantics/keys.js +9 -0
  86. package/dist/qstash/src/semantics/keys.ts +14 -0
  87. package/dist/qstash/src/semantics/messages.d.ts +74 -0
  88. package/dist/qstash/src/semantics/messages.js +233 -0
  89. package/dist/qstash/src/semantics/messages.ts +249 -0
  90. package/dist/qstash/src/semantics/queues.d.ts +2 -0
  91. package/dist/qstash/src/semantics/queues.js +60 -0
  92. package/dist/qstash/src/semantics/queues.ts +66 -0
  93. package/dist/qstash/src/semantics/schedules.d.ts +19 -0
  94. package/dist/qstash/src/semantics/schedules.js +125 -0
  95. package/dist/qstash/src/semantics/schedules.ts +132 -0
  96. package/dist/qstash/src/semantics/shared.d.ts +45 -0
  97. package/dist/qstash/src/semantics/shared.js +115 -0
  98. package/dist/qstash/src/semantics/shared.ts +121 -0
  99. package/dist/qstash/src/semantics/urlgroups.d.ts +2 -0
  100. package/dist/qstash/src/semantics/urlgroups.js +58 -0
  101. package/dist/qstash/src/semantics/urlgroups.ts +69 -0
  102. package/dist/qstash/src/semantics/workflows.d.ts +44 -0
  103. package/dist/qstash/src/semantics/workflows.js +379 -0
  104. package/dist/qstash/src/semantics/workflows.ts +401 -0
  105. package/dist/qstash/src/signing.d.ts +4 -0
  106. package/dist/qstash/src/signing.js +16 -0
  107. package/dist/qstash/src/signing.ts +19 -0
  108. package/dist/src/cli.d.ts +2 -0
  109. package/dist/src/cli.js +35 -0
  110. package/dist/src/generated/surface.gen.json +1 -0
  111. package/dist/src/index.d.ts +18 -0
  112. package/dist/src/index.js +124 -0
  113. package/dist/src/manifest.d.ts +14 -0
  114. package/dist/src/manifest.js +8 -0
  115. package/dist/src/upstash-budget.d.ts +85 -0
  116. package/dist/src/upstash-budget.js +440 -0
  117. package/dist/src/upstash-capabilities.d.ts +4 -0
  118. package/dist/src/upstash-capabilities.js +1286 -0
  119. package/dist/src/upstash-conformance.d.ts +7 -0
  120. package/dist/src/upstash-conformance.js +119 -0
  121. package/dist/src/upstash-connector.d.ts +115 -0
  122. package/dist/src/upstash-connector.js +309 -0
  123. package/dist/src/upstash-lua.d.ts +140 -0
  124. package/dist/src/upstash-lua.js +1229 -0
  125. package/dist/src/upstash-server.d.ts +29 -0
  126. package/dist/src/upstash-server.js +81 -0
  127. package/dist/src/upstash-store.d.ts +114 -0
  128. package/dist/src/upstash-store.js +1663 -0
  129. package/dist/src/upstash-twin.d.ts +73 -0
  130. package/dist/src/upstash-twin.js +437 -0
  131. package/package.json +59 -0
  132. package/qstash/src/doors.ts +51 -0
  133. package/qstash/src/egress.ts +58 -0
  134. package/qstash/src/fetch.ts +46 -0
  135. package/qstash/src/generated/surface.gen.json +1 -0
  136. package/qstash/src/generated/ui.gen.json +1 -0
  137. package/qstash/src/index.ts +35 -0
  138. package/qstash/src/manifest.ts +134 -0
  139. package/qstash/src/semantics/account.ts +98 -0
  140. package/qstash/src/semantics/delivery.ts +264 -0
  141. package/qstash/src/semantics/dlq.ts +61 -0
  142. package/qstash/src/semantics/index.ts +13 -0
  143. package/qstash/src/semantics/keys.ts +14 -0
  144. package/qstash/src/semantics/messages.ts +249 -0
  145. package/qstash/src/semantics/queues.ts +66 -0
  146. package/qstash/src/semantics/schedules.ts +132 -0
  147. package/qstash/src/semantics/shared.ts +121 -0
  148. package/qstash/src/semantics/urlgroups.ts +69 -0
  149. package/qstash/src/semantics/workflows.ts +401 -0
  150. package/qstash/src/signing.ts +19 -0
  151. package/src/cli.ts +36 -0
  152. package/src/generated/surface.gen.json +1 -0
  153. package/src/index.ts +203 -0
  154. package/src/manifest.ts +26 -0
  155. package/src/upstash-budget.ts +486 -0
  156. package/src/upstash-capabilities.ts +1418 -0
  157. package/src/upstash-conformance.ts +131 -0
  158. package/src/upstash-connector.ts +340 -0
  159. package/src/upstash-lua.ts +1120 -0
  160. package/src/upstash-server.ts +103 -0
  161. package/src/upstash-store.ts +1437 -0
  162. package/src/upstash-twin.ts +465 -0
@@ -0,0 +1,29 @@
1
+ import { upstashRedisOwners } from './upstash-store.js';
2
+ export type UpstashRedisServerOptions = {
3
+ root?: string;
4
+ port?: number;
5
+ readOnly?: boolean;
6
+ /** The token the twin demands. Omit to accept any non-empty credential (still 401s a missing one). */
7
+ token?: string;
8
+ /** The injected clock. Returns the ISO instant this request "happens at". */
9
+ now?: () => string;
10
+ };
11
+ /** Options every Upstash Redis-twin HTTP surface needs, independent of who owns the socket. */
12
+ export interface UpstashRedisTwinFetchOptions {
13
+ root?: string;
14
+ readOnly?: boolean;
15
+ /** The token the twin demands. Omit to accept any non-empty credential (still 401s a missing one). */
16
+ token?: string;
17
+ /** The injected clock. Returns the ISO instant this request "happens at". */
18
+ now?: () => string;
19
+ /** the QStash lane's fetch (../qstash/src/fetch.ts), when the caller has one already */
20
+ qstash?: (request: Request) => Promise<Response>;
21
+ }
22
+ /** The pack's fetch, with `owners()`: per id of Redis's command table, the command core (`handler`) or the gap. */
23
+ export declare function createUpstashRedisTwinFetch(options?: UpstashRedisTwinFetchOptions): ((request: Request) => Promise<Response>) & {
24
+ owners: typeof upstashRedisOwners;
25
+ };
26
+ export declare function createUpstashRedisTwinServer(options?: UpstashRedisServerOptions): Promise<{
27
+ port: number;
28
+ stop: () => void;
29
+ }>;
@@ -0,0 +1,81 @@
1
+ // upstash twin HTTP server — serve the Upstash Redis REST twin over HTTP so the real
2
+ // `@upstash/redis` SDK (pointed at it with nothing but its own public `url` option) works
3
+ // UNMODIFIED, and so `@upstash/ratelimit` on top of that SDK enforces a real limit.
4
+ //
5
+ // ── THE INJECTED CLOCK ────────────────────────────────────────────────────────────────────────
6
+ // `now` is the twin's whole notion of time: it produces the `occurredAt` the kernel stamps each
7
+ // action with AND the instant every TTL is measured against. It defaults to the wall clock (a
8
+ // server has to), but a test passes its own so that TTL expiry, `EXPIRE`/`TTL` arithmetic and
9
+ // stream ids become data it controls rather than a race it hopes to win. This is the seam the
10
+ // repo's no-wall-clock discipline requires — see the store module's header.
11
+ //
12
+ // FETCH-FIRST (runtime contract R12b): the surface is the plain `createUpstashRedisTwinFetch` and
13
+ // the SERVER is one line of `Bun.serve` around it. This is a CUSTOM fetch, not the kernel adapter
14
+ // (`createTwinFetchFromHandler`): replies carry ONLY the handler's own headers (the REST envelope
15
+ // declares its own content type; a 405 carries none at all), and the clock is an injectable seam.
16
+ import { serveHttp } from '@volter/world-core';
17
+ import { handleUpstashRedisTwinRequest } from "./upstash-twin.js";
18
+ import { upstashRedisOwners } from "./upstash-store.js";
19
+ import { API_HOST, CONSOLE_HOST, createUpstashApiLaneFetch } from "../api/src/fetch.js";
20
+ import { createQstashLaneFetch } from "../qstash/src/fetch.js";
21
+ import { worldNow, statefulTwinManifest } from '@volter/world-core';
22
+ /** QStash's hosts: qstash.upstash.io and qstash-<region>.upstash.io (both documents' `servers`). */
23
+ const QSTASH_HOST = /^qstash(-[a-z0-9-]+)?\.upstash\.io$/;
24
+ /** The QStash lane's requests at the pack's own address: its API (an application's QSTASH_URL is the pack's address, and
25
+ * every QStash path begins `/v2/`, which no Redis command is) and the World's doors for it. */
26
+ const QSTASH_PATH = /^\/(v2\/|_twin\/(deliveries|destinations)(\/|$))/;
27
+ /** The pack's fetch, with `owners()`: per id of Redis's command table, the command core (`handler`) or the gap. */
28
+ export function createUpstashRedisTwinFetch(options = {}) {
29
+ const readOnly = options.readOnly ?? false;
30
+ const now = options.now ?? (() => worldNow());
31
+ // api.upstash.com's Developer API and console.upstash.com's pages are the pack's `api` lane (../api/src/fetch.ts)
32
+ const api = createUpstashApiLaneFetch({ ...(options.root !== undefined ? { root: options.root } : {}), readOnly, clock: now });
33
+ const qstash = options.qstash ?? createQstashLaneFetch({ ...(options.root !== undefined ? { root: options.root } : {}), readOnly, clock: now });
34
+ const fetch = async function upstashRedisTwinFetch(request) {
35
+ const url = new URL(request.url);
36
+ // the vendor host a redirected request names in x-volter-twin-original-host (the injector's fetch path, a hosted
37
+ // World), else its Host, else its URL's (vercel-server.ts's precedent): the lane's two hosts go to the lane
38
+ const host = (request.headers.get('x-volter-twin-original-host') ?? request.headers.get('host') ?? url.host).split(':')[0].toLowerCase();
39
+ const vendorHost = host === API_HOST || host === CONSOLE_HOST ? host : url.hostname === API_HOST || url.hostname === CONSOLE_HOST ? url.hostname : undefined;
40
+ if (vendorHost !== undefined)
41
+ return api(request, vendorHost);
42
+ if (QSTASH_HOST.test(host) || QSTASH_HOST.test(url.hostname) || QSTASH_PATH.test(url.pathname))
43
+ return qstash(request);
44
+ // GET /twin — the discovery manifest (education inside the twin).
45
+ if (request.method === 'GET' && url.pathname.replace(/\/+$/, '') === '/twin')
46
+ return discovery();
47
+ // a daily backup due before this request is made first, so it holds the keys as they were (../api/src/semantics/time.ts)
48
+ await api.catchUp(request);
49
+ const body = request.method !== 'GET' && request.method !== 'HEAD' ? await request.text() : '';
50
+ const headers = {};
51
+ request.headers.forEach((value, key) => { headers[key.toLowerCase()] = value; });
52
+ const { status, body: out, headers: outHeaders } = await handleUpstashRedisTwinRequest({
53
+ method: request.method,
54
+ path: url.pathname + (url.search || ''),
55
+ body,
56
+ headers,
57
+ readOnly,
58
+ occurredAt: now(),
59
+ ...(options.root !== undefined ? { root: options.root } : {}),
60
+ ...(options.token !== undefined ? { token: options.token } : {}),
61
+ });
62
+ // A null body means "no body at all" (405 on a disallowed method, HEAD, CORS preflight) —
63
+ // the real service answers those with zero bytes and, for 405, no content-type either.
64
+ if (out === null)
65
+ return new Response(null, { status, headers: { ...(outHeaders ?? {}) } });
66
+ return new Response(JSON.stringify(out), { status, headers: { ...(outHeaders ?? {}) } });
67
+ };
68
+ return Object.assign(fetch, { owners: upstashRedisOwners });
69
+ }
70
+ /** `GET /twin`, the discovery manifest. */
71
+ function discovery() {
72
+ return Response.json(statefulTwinManifest({ vendor: 'upstash', twinOf: 'Upstash Redis (REST protocol)', stores: "a real Redis keyspace served over Upstash's REST envelope" }));
73
+ }
74
+ export async function createUpstashRedisTwinServer(options = {}) {
75
+ const server = await serveHttp({
76
+ port: options.port ?? 0,
77
+ idleTimeout: 60,
78
+ fetch: createUpstashRedisTwinFetch(options),
79
+ });
80
+ return { port: server.port ?? options.port ?? 0, stop: () => server.stop(true) };
81
+ }
@@ -0,0 +1,114 @@
1
+ export declare const SERVICE = "upstash";
2
+ /**
3
+ * A Redis SIMPLE STATUS reply, kept distinct from a bulk string.
4
+ *
5
+ * This is not pedantry — it is required for `Upstash-Encoding: base64` fidelity. LIVE-PROBED rule:
6
+ * with that header on, Upstash base64-encodes every string in `result` at any array depth EXCEPT
7
+ * the simple-status reply `OK`, which is passed through raw. `PING`'s status `PONG` IS encoded
8
+ * (`UE9ORw==`), and so is a BULK STRING whose value happens to be `OK` (`GET okkey` → `T0s=`). So
9
+ * the exemption is scoped to the status reply `OK` specifically, and a twin that exempted any
10
+ * string equal to `"OK"` would send raw bytes for `GET okkey`. Hence this wrapper.
11
+ */
12
+ export declare class RedisStatus {
13
+ readonly value: string;
14
+ constructor(value: string);
15
+ }
16
+ /** The JSON projection of a RESP reply — what Upstash puts in `{"result": …}`. */
17
+ export type RedisValue = null | number | string | RedisStatus | RedisValue[];
18
+ /** A command-level failure. `message` is the vendor's literal error string. Maps to HTTP 400. */
19
+ export declare class RedisCommandError extends Error {
20
+ constructor(message: string);
21
+ }
22
+ /** Thrown when a write is attempted against a read-only twin. Mapped to HTTP 405 by the handler. */
23
+ export declare class ReadOnlyError extends Error {
24
+ constructor();
25
+ }
26
+ export declare const KEY_TYPES: readonly ["string", "list", "set", "hash", "zset", "stream"];
27
+ export type KeyType = typeof KEY_TYPES[number];
28
+ /** Everything a run needs. `root` scopes the kernel state; `occurredAt` IS the clock. */
29
+ export type RedisContext = {
30
+ root?: string;
31
+ occurredAt: string;
32
+ /** When true, any write command raises `ReadOnlyError` (the handler answers 405). */
33
+ readOnly?: boolean;
34
+ /** The database a Developer API lane made (its `database_id`), whose keys and scripts are its own: subjects
35
+ * `db:<id>:key:<name>` and `db:<id>:script:<sha1>`. Absent, the World's own database: `key:<name>`, `script:<sha1>`. */
36
+ database?: string;
37
+ };
38
+ /** Upstash's own unavailable-command message — NOT stock Redis's "unknown command". LIVE-PROBED. */
39
+ export declare function unavailableCommand(name: string): string;
40
+ /** Commands the REST layer refuses outright — LIVE-PROBED message, name upper-cased in quotes. */
41
+ export declare function restRestricted(name: string): string;
42
+ export declare function wrongArity(name: string): string;
43
+ /**
44
+ * Commands the REST surface rejects even though Redis has them: connection/transaction/pub-sub
45
+ * control that has no meaning over stateless HTTP. LIVE-PROBED (`AUTH`, `MULTI`, `WATCH`,
46
+ * `SUBSCRIBE`, `CLIENT` all answered the `not allowed in REST` message).
47
+ */
48
+ export declare const REST_RESTRICTED_COMMANDS: Set<string>;
49
+ /**
50
+ * The commands this twin serves, each with the most arguments (after the name) its own parsing takes,
51
+ * `-1` for no bound: `[min, max]`, where `min` is the table's arity (checked equal for every entry) and
52
+ * `max` is what the command itself refuses past, with the same "wrong number of arguments". One table for
53
+ * both callers, `execOne` and `commandShapeError` (which lets `/multi-exec` reject a malformed batch at
54
+ * QUEUE time, as Redis's `EXECABORT` does). SCRIPT is a container: its subcommands are the table's ids.
55
+ */
56
+ export declare const SERVED_COMMANDS: Record<string, [number, number]>;
57
+ /**
58
+ * Does this command MUTATE? Takes the whole argv, not just the name, because two commands are
59
+ * only writes for some of their subcommands. A read-only twin and `EVAL_RO` both key off this,
60
+ * and getting it wrong in either direction is a bug: too broad refuses legitimate reads (§9 round
61
+ * 1), too narrow lets a write through a read-only twin.
62
+ */
63
+ export declare function isWriteCommand(name: string, args?: readonly string[]): boolean;
64
+ /**
65
+ * QUEUE-TIME validation: is this command well-formed enough for Redis to accept it into a MULTI?
66
+ * Returns the vendor's error string, or `null` when the command is fine. Only structural faults
67
+ * live here (unknown command, REST-restricted command, wrong arity) — a WRONGTYPE or a bad integer
68
+ * is a RUNTIME error, which (LIVE-PROBED) does NOT abort a transaction.
69
+ */
70
+ export declare function commandShapeError(argv: unknown[]): string | null;
71
+ /** The table id a command line names: a container's subcommand (`SCRIPT LOAD`) when the table has it. */
72
+ export declare function commandId(argv: string[]): string | undefined;
73
+ /** Per id of Redis's table, who answers it: the command core (`handler`) or the gap. */
74
+ export declare function upstashRedisOwners(): Record<string, 'handler' | 'gap'>;
75
+ /** One key as a backup holds it: its name, type, value, deadline and a stream's last id. */
76
+ export type KeyImage = {
77
+ name: string;
78
+ kind: string;
79
+ v: unknown;
80
+ pexpireAt: number | null;
81
+ lastId?: string;
82
+ };
83
+ /** A database's live keys as they stand at `ctx.occurredAt`: what a backup of it holds (the Developer API lane's
84
+ * backups, ../api/src/semantics/backups.ts). */
85
+ export declare function keyspaceImage(ctx: RedisContext): KeyImage[];
86
+ /** Replace a database's keys with a backup's: every live key is deleted, then the image's keys are written ("All
87
+ * existing data in the target database will be deleted before the restore operation begins",
88
+ * https://upstash.com/docs/redis/features/backup). */
89
+ export declare function restoreKeyspace(image: readonly KeyImage[], ctx: RedisContext): Promise<void>;
90
+ /** Redis glob-style key matching (`*`, `?`, `[abc]`, `[a-c]`, `[^a]`, `\` escape). */
91
+ export declare function globMatch(pattern: string, subject: string): boolean;
92
+ export type RunItem = {
93
+ result: RedisValue;
94
+ } | {
95
+ error: string;
96
+ };
97
+ /**
98
+ * Execute a whole request's worth of commands against one root, then flush.
99
+ *
100
+ * Each element comes back as `{result}` or `{error}` — the exact per-command envelope Upstash's
101
+ * `/pipeline` and `/multi-exec` endpoints return and the SDK's `Pipeline.exec` destructures.
102
+ *
103
+ * RUNTIME ERRORS DO NOT ABORT — not in a pipeline and (LIVE-PROBED, contrary to the intuition that
104
+ * a "transaction" rolls back) NOT in `/multi-exec` either: Upstash's own docs say "all commands
105
+ * will be executed. Upstash Redis will not stop the processing of commands. This is to provide same
106
+ * semantics with Redis when there are errors inside a transaction." A probe of the real service
107
+ * confirmed a `SET` after a failing `INCR` in a `/multi-exec` batch is applied. Structural faults —
108
+ * an unavailable command or a bad arity — are QUEUE-time and DO discard the whole batch, but the
109
+ * caller (`upstash-twin.ts`) rejects those before this function is ever reached.
110
+ */
111
+ export declare function execRedisRun(commands: string[][], ctx: RedisContext): Promise<{
112
+ items: RunItem[];
113
+ wrote: boolean;
114
+ }>;