@volter/twin-upstash 0.1.0 → 0.1.2
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 +2 -1
- package/dist/qstash/src/doors.js +1 -1
- package/dist/qstash/src/doors.ts +2 -2
- package/dist/qstash/src/fetch.js +2 -2
- package/dist/qstash/src/fetch.ts +2 -2
- package/dist/qstash/src/semantics/delivery.d.ts +1 -1
- package/dist/qstash/src/semantics/delivery.js +13 -8
- package/dist/qstash/src/semantics/delivery.ts +13 -8
- package/dist/src/upstash-lua.d.ts +8 -140
- package/dist/src/upstash-lua.js +13 -1227
- package/dist/src/upstash-server.js +8 -4
- package/dist/src/upstash-store.d.ts +15 -87
- package/dist/src/upstash-store.js +49 -1579
- package/dist/src/upstash-twin.js +2 -2
- package/package.json +4 -4
- package/qstash/src/doors.ts +2 -2
- package/qstash/src/fetch.ts +2 -2
- package/qstash/src/semantics/delivery.ts +13 -8
- package/src/manifest.ts +3 -2
- package/src/upstash-lua.ts +31 -1119
- package/src/upstash-server.ts +8 -4
- package/src/upstash-store.ts +73 -1346
- package/src/upstash-twin.ts +2 -2
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// the SERVER is one line of `Bun.serve` around it. This is a CUSTOM fetch, not the kernel adapter
|
|
14
14
|
// (`createTwinFetchFromHandler`): replies carry ONLY the handler's own headers (the REST envelope
|
|
15
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';
|
|
16
|
+
import { isReadOnlyRequest, serveHttp, withRequestScopes } from '@volter/world-core';
|
|
17
17
|
import { handleUpstashRedisTwinRequest } from "./upstash-twin.js";
|
|
18
18
|
import { upstashRedisOwners } from "./upstash-store.js";
|
|
19
19
|
import { API_HOST, CONSOLE_HOST, createUpstashApiLaneFetch } from "../api/src/fetch.js";
|
|
@@ -26,12 +26,16 @@ const QSTASH_HOST = /^qstash(-[a-z0-9-]+)?\.upstash\.io$/;
|
|
|
26
26
|
const QSTASH_PATH = /^\/(v2\/|_twin\/(deliveries|destinations)(\/|$))/;
|
|
27
27
|
/** The pack's fetch, with `owners()`: per id of Redis's command table, the command core (`handler`) or the gap. */
|
|
28
28
|
export function createUpstashRedisTwinFetch(options = {}) {
|
|
29
|
-
const readOnly = options.readOnly ?? false;
|
|
30
29
|
const now = options.now ?? (() => worldNow());
|
|
31
30
|
// api.upstash.com's Developer API and console.upstash.com's pages are the pack's `api` lane (../api/src/fetch.ts)
|
|
31
|
+
// the lanes' own read-only twin; a read-only REQUEST is refused by the kernel's scope around them (withRequestScopes)
|
|
32
|
+
const readOnly = options.readOnly ?? false;
|
|
32
33
|
const api = createUpstashApiLaneFetch({ ...(options.root !== undefined ? { root: options.root } : {}), readOnly, clock: now });
|
|
33
34
|
const qstash = options.qstash ?? createQstashLaneFetch({ ...(options.root !== undefined ? { root: options.root } : {}), readOnly, clock: now });
|
|
34
|
-
|
|
35
|
+
// a read-only request (x-volter-read-only) is a request to a read-only twin: a command that writes
|
|
36
|
+
// (by GET too: Upstash runs any command from the path) is refused, and the kernel refuses any that slip
|
|
37
|
+
const fetch = withRequestScopes(async function upstashRedisTwinFetch(request) {
|
|
38
|
+
const readOnly = (options.readOnly ?? false) || isReadOnlyRequest(request);
|
|
35
39
|
const url = new URL(request.url);
|
|
36
40
|
// the vendor host a redirected request names in x-volter-twin-original-host (the injector's fetch path, a hosted
|
|
37
41
|
// World), else its Host, else its URL's (vercel-server.ts's precedent): the lane's two hosts go to the lane
|
|
@@ -64,7 +68,7 @@ export function createUpstashRedisTwinFetch(options = {}) {
|
|
|
64
68
|
if (out === null)
|
|
65
69
|
return new Response(null, { status, headers: { ...(outHeaders ?? {}) } });
|
|
66
70
|
return new Response(JSON.stringify(out), { status, headers: { ...(outHeaders ?? {}) } });
|
|
67
|
-
};
|
|
71
|
+
}, { refuse: () => Response.json({ error: 'NOPERM this request is read-only' }, { status: 403 }) });
|
|
68
72
|
return Object.assign(fetch, { owners: upstashRedisOwners });
|
|
69
73
|
}
|
|
70
74
|
/** `GET /twin`, the discovery manifest. */
|
|
@@ -1,66 +1,21 @@
|
|
|
1
|
+
import { type KeyImage, type RedisContext as CoreContext, type RedisDialect, type RunItem } from '@volter/world-core/redis';
|
|
2
|
+
export { globMatch, KEY_TYPES, ReadOnlyError, RedisCommandError, RedisStatus, wrongArity, } from '@volter/world-core/redis';
|
|
3
|
+
export type { KeyImage, KeyType, RedisValue, RunItem } from '@volter/world-core/redis';
|
|
1
4
|
export declare const SERVICE = "upstash";
|
|
2
|
-
/**
|
|
3
|
-
|
|
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
|
-
};
|
|
5
|
+
/** A run's context as this pack's callers give it: the service and the dialect are Upstash's. */
|
|
6
|
+
export type RedisContext = Omit<CoreContext, 'service' | 'dialect'>;
|
|
38
7
|
/** Upstash's own unavailable-command message — NOT stock Redis's "unknown command". LIVE-PROBED. */
|
|
39
8
|
export declare function unavailableCommand(name: string): string;
|
|
40
9
|
/** Commands the REST layer refuses outright — LIVE-PROBED message, name upper-cased in quotes. */
|
|
41
10
|
export declare function restRestricted(name: string): string;
|
|
42
|
-
export declare function wrongArity(name: string): string;
|
|
43
11
|
/**
|
|
44
12
|
* Commands the REST surface rejects even though Redis has them: connection/transaction/pub-sub
|
|
45
13
|
* control that has no meaning over stateless HTTP. LIVE-PROBED (`AUTH`, `MULTI`, `WATCH`,
|
|
46
14
|
* `SUBSCRIBE`, `CLIENT` all answered the `not allowed in REST` message).
|
|
47
15
|
*/
|
|
48
16
|
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
|
-
*/
|
|
17
|
+
/** The commands this twin serves: the core's, and SELECT, which Upstash answers for its one database. */
|
|
56
18
|
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
19
|
/**
|
|
65
20
|
* QUEUE-TIME validation: is this command well-formed enough for Redis to accept it into a MULTI?
|
|
66
21
|
* Returns the vendor's error string, or `null` when the command is fine. Only structural faults
|
|
@@ -72,43 +27,16 @@ export declare function commandShapeError(argv: unknown[]): string | null;
|
|
|
72
27
|
export declare function commandId(argv: string[]): string | undefined;
|
|
73
28
|
/** Per id of Redis's table, who answers it: the command core (`handler`) or the gap. */
|
|
74
29
|
export declare function upstashRedisOwners(): Record<string, 'handler' | 'gap'>;
|
|
75
|
-
/**
|
|
76
|
-
export
|
|
77
|
-
|
|
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
|
-
*/
|
|
30
|
+
/** Upstash's REST API as a dialect of the core: its refusals, its one database, its scripts rolled back on error. */
|
|
31
|
+
export declare const UPSTASH_DIALECT: RedisDialect;
|
|
32
|
+
/** Run a request's commands on the core, as Upstash answers them (the kernel's `execRedisRun`). */
|
|
111
33
|
export declare function execRedisRun(commands: string[][], ctx: RedisContext): Promise<{
|
|
112
34
|
items: RunItem[];
|
|
113
35
|
wrote: boolean;
|
|
114
36
|
}>;
|
|
37
|
+
/** Does this command MUTATE, as the REST API's read-only token decides it. */
|
|
38
|
+
export declare function isWriteCommand(name: string, args?: readonly string[]): boolean;
|
|
39
|
+
/** A database's live keys as they stand at `ctx.occurredAt`: what a backup of it holds. */
|
|
40
|
+
export declare function keyspaceImage(ctx: RedisContext): KeyImage[];
|
|
41
|
+
/** Replace a database's keys with a backup's. */
|
|
42
|
+
export declare function restoreKeyspace(image: readonly KeyImage[], ctx: RedisContext): Promise<void>;
|