@alchemy.run/redis 0.0.0 → 2.0.0-beta.85
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 +201 -0
- package/README.md +55 -4
- package/lib/Client.d.ts +114 -0
- package/lib/Client.d.ts.map +1 -0
- package/lib/Client.js +166 -0
- package/lib/Client.js.map +1 -0
- package/lib/Errors.d.ts +37 -0
- package/lib/Errors.d.ts.map +1 -0
- package/lib/Errors.js +17 -0
- package/lib/Errors.js.map +1 -0
- package/lib/Protocol.d.ts +47 -0
- package/lib/Protocol.d.ts.map +1 -0
- package/lib/Protocol.js +273 -0
- package/lib/Protocol.js.map +1 -0
- package/lib/Resp.d.ts +65 -0
- package/lib/Resp.d.ts.map +1 -0
- package/lib/Resp.js +566 -0
- package/lib/Resp.js.map +1 -0
- package/lib/RuntimeContext.d.ts +12 -0
- package/lib/RuntimeContext.d.ts.map +1 -0
- package/lib/RuntimeContext.js +1 -0
- package/lib/RuntimeContext.js.map +1 -0
- package/lib/index.d.ts +28 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +28 -0
- package/lib/index.js.map +1 -0
- package/package.json +41 -3
- package/src/Client.ts +303 -0
- package/src/Errors.ts +27 -0
- package/src/Protocol.ts +414 -0
- package/src/Resp.ts +612 -0
- package/src/RuntimeContext.ts +11 -0
- package/src/index.ts +27 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloud-agnostic Redis runtime client. Fly and Railway resources bind
|
|
3
|
+
* `REDIS_URL`; this module speaks RESP over that URL. Import from
|
|
4
|
+
* `@alchemy.run/redis`.
|
|
5
|
+
*
|
|
6
|
+
* The codec is a complete RESP2 parser (plus RESP3 types) reimplemented
|
|
7
|
+
* in TypeScript after the official spec and the algorithm in
|
|
8
|
+
* `node-redis-parser`. There is no ioredis / node-redis dependency.
|
|
9
|
+
*
|
|
10
|
+
* Bindings stay on the cloud (`Fly.ReadWriteRedis`,
|
|
11
|
+
* `Railway.ReadWriteRedis`). Those layers call {@link makeReadWrite}
|
|
12
|
+
* so the RESP client is not duplicated.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* import * as Redis from "@alchemy.run/redis";
|
|
17
|
+
*
|
|
18
|
+
* const cache = Redis.makeReadWrite(url);
|
|
19
|
+
* yield* cache.set("marker", "hello");
|
|
20
|
+
* const value = yield* cache.get("marker");
|
|
21
|
+
* const custom = yield* cache.send("ECHO", ["hi"]);
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export * from "./Client.js";
|
|
25
|
+
export * from "./Errors.js";
|
|
26
|
+
export * from "./Protocol.js";
|
|
27
|
+
export * from "./Resp.js";
|
|
28
|
+
//# sourceMappingURL=index.js.map
|
package/lib/index.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,WAAW,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alchemy.run/redis",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "2.0.0-beta.85",
|
|
4
4
|
"description": "An Effect RESP client for Redis and Valkey, shared by Alchemy's Redis bindings.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"alchemy",
|
|
@@ -9,12 +9,50 @@
|
|
|
9
9
|
"resp",
|
|
10
10
|
"valkey"
|
|
11
11
|
],
|
|
12
|
+
"homepage": "https://alchemy.run",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/alchemy-run/alchemy/issues"
|
|
15
|
+
},
|
|
12
16
|
"license": "Apache-2.0",
|
|
13
17
|
"author": "Sam Goodwin <sam@alchemy.run>",
|
|
14
|
-
"homepage": "https://alchemy.run",
|
|
15
18
|
"repository": {
|
|
16
19
|
"type": "git",
|
|
17
20
|
"url": "git+https://github.com/alchemy-run/alchemy.git",
|
|
18
21
|
"directory": "packages/redis"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"lib",
|
|
25
|
+
"src"
|
|
26
|
+
],
|
|
27
|
+
"type": "module",
|
|
28
|
+
"sideEffects": false,
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"types": "./lib/index.d.ts",
|
|
32
|
+
"bun": "./src/index.ts",
|
|
33
|
+
"default": "./lib/index.js"
|
|
34
|
+
},
|
|
35
|
+
"./*": {
|
|
36
|
+
"types": "./lib/*.d.ts",
|
|
37
|
+
"bun": "./src/*.ts",
|
|
38
|
+
"default": "./lib/*.js"
|
|
39
|
+
},
|
|
40
|
+
"./package.json": "./package.json"
|
|
41
|
+
},
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@types/bun": "1.4.3",
|
|
47
|
+
"@types/node": "26.6.5",
|
|
48
|
+
"effect": "^4.1.0",
|
|
49
|
+
"typescript": "^7.0.2"
|
|
50
|
+
},
|
|
51
|
+
"peerDependencies": {
|
|
52
|
+
"effect": "^4.1.0"
|
|
53
|
+
},
|
|
54
|
+
"scripts": {
|
|
55
|
+
"build": "pnpm -r --filter \"{.}...\" run build:package",
|
|
56
|
+
"build:package": "node scripts/build.ts"
|
|
19
57
|
}
|
|
20
|
-
}
|
|
58
|
+
}
|
package/src/Client.ts
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import type { CommandError, UrlMissing } from "./Errors.ts";
|
|
3
|
+
import { command, commandPipeline } from "./Protocol.ts";
|
|
4
|
+
import type { Arg, Reply } from "./Resp.ts";
|
|
5
|
+
import type { RuntimeContext } from "./RuntimeContext.ts";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A Redis URL that resolves from the Function/Service environment at
|
|
9
|
+
* runtime (`REDIS_URL`).
|
|
10
|
+
*/
|
|
11
|
+
export type Url = Effect.Effect<string, UrlMissing, RuntimeContext>;
|
|
12
|
+
|
|
13
|
+
export type RuntimeError = CommandError | UrlMissing;
|
|
14
|
+
|
|
15
|
+
export interface SetOptions {
|
|
16
|
+
readonly ex?: number;
|
|
17
|
+
readonly px?: number;
|
|
18
|
+
readonly exAt?: number;
|
|
19
|
+
readonly pxAt?: number;
|
|
20
|
+
readonly nx?: boolean;
|
|
21
|
+
readonly xx?: boolean;
|
|
22
|
+
readonly keepTtl?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Read-only Redis client. Any Redis command is available via {@link Client.send}
|
|
27
|
+
* on the read/write client.
|
|
28
|
+
*/
|
|
29
|
+
export interface ReadClient {
|
|
30
|
+
get(key: string): Effect.Effect<string | null, RuntimeError, RuntimeContext>;
|
|
31
|
+
mget(...keys: string[]): Effect.Effect<Array<string | null>, RuntimeError, RuntimeContext>;
|
|
32
|
+
ping(message?: string): Effect.Effect<string, RuntimeError, RuntimeContext>;
|
|
33
|
+
echo(message: string): Effect.Effect<string, RuntimeError, RuntimeContext>;
|
|
34
|
+
exists(...keys: string[]): Effect.Effect<number, RuntimeError, RuntimeContext>;
|
|
35
|
+
ttl(key: string): Effect.Effect<number, RuntimeError, RuntimeContext>;
|
|
36
|
+
pttl(key: string): Effect.Effect<number, RuntimeError, RuntimeContext>;
|
|
37
|
+
type(key: string): Effect.Effect<string, RuntimeError, RuntimeContext>;
|
|
38
|
+
hget(key: string, field: string): Effect.Effect<string | null, RuntimeError, RuntimeContext>;
|
|
39
|
+
hmget(
|
|
40
|
+
key: string,
|
|
41
|
+
...fields: string[]
|
|
42
|
+
): Effect.Effect<Array<string | null>, RuntimeError, RuntimeContext>;
|
|
43
|
+
hgetall(key: string): Effect.Effect<Record<string, string>, RuntimeError, RuntimeContext>;
|
|
44
|
+
hexists(key: string, field: string): Effect.Effect<boolean, RuntimeError, RuntimeContext>;
|
|
45
|
+
hkeys(key: string): Effect.Effect<string[], RuntimeError, RuntimeContext>;
|
|
46
|
+
hvals(key: string): Effect.Effect<string[], RuntimeError, RuntimeContext>;
|
|
47
|
+
hlen(key: string): Effect.Effect<number, RuntimeError, RuntimeContext>;
|
|
48
|
+
llen(key: string): Effect.Effect<number, RuntimeError, RuntimeContext>;
|
|
49
|
+
lrange(
|
|
50
|
+
key: string,
|
|
51
|
+
start: number,
|
|
52
|
+
stop: number,
|
|
53
|
+
): Effect.Effect<string[], RuntimeError, RuntimeContext>;
|
|
54
|
+
lindex(key: string, index: number): Effect.Effect<string | null, RuntimeError, RuntimeContext>;
|
|
55
|
+
scard(key: string): Effect.Effect<number, RuntimeError, RuntimeContext>;
|
|
56
|
+
sismember(key: string, member: string): Effect.Effect<boolean, RuntimeError, RuntimeContext>;
|
|
57
|
+
smembers(key: string): Effect.Effect<string[], RuntimeError, RuntimeContext>;
|
|
58
|
+
zscore(key: string, member: string): Effect.Effect<string | null, RuntimeError, RuntimeContext>;
|
|
59
|
+
zcard(key: string): Effect.Effect<number, RuntimeError, RuntimeContext>;
|
|
60
|
+
zrange(
|
|
61
|
+
key: string,
|
|
62
|
+
start: number,
|
|
63
|
+
stop: number,
|
|
64
|
+
): Effect.Effect<string[], RuntimeError, RuntimeContext>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Write Redis client.
|
|
69
|
+
*/
|
|
70
|
+
export interface WriteClient {
|
|
71
|
+
set(
|
|
72
|
+
key: string,
|
|
73
|
+
value: string | Uint8Array,
|
|
74
|
+
options?: SetOptions,
|
|
75
|
+
): Effect.Effect<void, RuntimeError, RuntimeContext>;
|
|
76
|
+
mset(
|
|
77
|
+
values: Record<string, string | Uint8Array>,
|
|
78
|
+
): Effect.Effect<void, RuntimeError, RuntimeContext>;
|
|
79
|
+
del(...keys: string[]): Effect.Effect<number, RuntimeError, RuntimeContext>;
|
|
80
|
+
unlink(...keys: string[]): Effect.Effect<number, RuntimeError, RuntimeContext>;
|
|
81
|
+
expire(key: string, seconds: number): Effect.Effect<boolean, RuntimeError, RuntimeContext>;
|
|
82
|
+
persist(key: string): Effect.Effect<boolean, RuntimeError, RuntimeContext>;
|
|
83
|
+
incr(key: string): Effect.Effect<number, RuntimeError, RuntimeContext>;
|
|
84
|
+
incrBy(key: string, amount: number): Effect.Effect<number, RuntimeError, RuntimeContext>;
|
|
85
|
+
decr(key: string): Effect.Effect<number, RuntimeError, RuntimeContext>;
|
|
86
|
+
hset(
|
|
87
|
+
key: string,
|
|
88
|
+
field: string | Record<string, string | Uint8Array>,
|
|
89
|
+
value?: string | Uint8Array,
|
|
90
|
+
): Effect.Effect<number, RuntimeError, RuntimeContext>;
|
|
91
|
+
hdel(key: string, ...fields: string[]): Effect.Effect<number, RuntimeError, RuntimeContext>;
|
|
92
|
+
hincrBy(
|
|
93
|
+
key: string,
|
|
94
|
+
field: string,
|
|
95
|
+
amount: number,
|
|
96
|
+
): Effect.Effect<number, RuntimeError, RuntimeContext>;
|
|
97
|
+
lpush(
|
|
98
|
+
key: string,
|
|
99
|
+
...values: Array<string | Uint8Array>
|
|
100
|
+
): Effect.Effect<number, RuntimeError, RuntimeContext>;
|
|
101
|
+
rpush(
|
|
102
|
+
key: string,
|
|
103
|
+
...values: Array<string | Uint8Array>
|
|
104
|
+
): Effect.Effect<number, RuntimeError, RuntimeContext>;
|
|
105
|
+
lpop(key: string): Effect.Effect<string | null, RuntimeError, RuntimeContext>;
|
|
106
|
+
rpop(key: string): Effect.Effect<string | null, RuntimeError, RuntimeContext>;
|
|
107
|
+
sadd(
|
|
108
|
+
key: string,
|
|
109
|
+
...members: Array<string | Uint8Array>
|
|
110
|
+
): Effect.Effect<number, RuntimeError, RuntimeContext>;
|
|
111
|
+
srem(
|
|
112
|
+
key: string,
|
|
113
|
+
...members: Array<string | Uint8Array>
|
|
114
|
+
): Effect.Effect<number, RuntimeError, RuntimeContext>;
|
|
115
|
+
zadd(
|
|
116
|
+
key: string,
|
|
117
|
+
score: number,
|
|
118
|
+
member: string | Uint8Array,
|
|
119
|
+
): Effect.Effect<number, RuntimeError, RuntimeContext>;
|
|
120
|
+
zrem(
|
|
121
|
+
key: string,
|
|
122
|
+
...members: Array<string | Uint8Array>
|
|
123
|
+
): Effect.Effect<number, RuntimeError, RuntimeContext>;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Read + write Redis client. {@link send} is the total command surface —
|
|
128
|
+
* every Redis command is an array of bulk strings on the wire.
|
|
129
|
+
*/
|
|
130
|
+
export interface Client extends ReadClient, WriteClient {
|
|
131
|
+
send(name: string, args?: readonly Arg[]): Effect.Effect<Reply, RuntimeError, RuntimeContext>;
|
|
132
|
+
pipeline(
|
|
133
|
+
commands: ReadonlyArray<readonly [string, ...Arg[]]>,
|
|
134
|
+
): Effect.Effect<readonly Reply[], RuntimeError, RuntimeContext>;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export interface ReadWriteClient extends Client {}
|
|
138
|
+
|
|
139
|
+
const asString = (value: unknown): string => {
|
|
140
|
+
if (typeof value === "string") return value;
|
|
141
|
+
if (value == null) return "";
|
|
142
|
+
// RESP scalars (integers, bigints, booleans) coerce to their text form.
|
|
143
|
+
return String(value as number | bigint | boolean);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const asNullableString = (value: unknown): string | null => {
|
|
147
|
+
if (value == null) return null;
|
|
148
|
+
return asString(value);
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const asNumber = (value: unknown): number => {
|
|
152
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
153
|
+
if (typeof value === "bigint") return Number(value);
|
|
154
|
+
const parsed = Number(asString(value));
|
|
155
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const asBoolean = (value: unknown): boolean => asNumber(value) !== 0;
|
|
159
|
+
|
|
160
|
+
const asStringArray = (value: unknown): string[] => {
|
|
161
|
+
if (!Array.isArray(value)) return [];
|
|
162
|
+
return value.map(asString);
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const asNullableStringArray = (value: unknown): Array<string | null> => {
|
|
166
|
+
if (!Array.isArray(value)) return [];
|
|
167
|
+
return value.map(asNullableString);
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const asHash = (value: unknown): Record<string, string> => {
|
|
171
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
172
|
+
const out: Record<string, string> = {};
|
|
173
|
+
for (const [key, item] of Object.entries(value)) {
|
|
174
|
+
out[key] = asString(item);
|
|
175
|
+
}
|
|
176
|
+
return out;
|
|
177
|
+
}
|
|
178
|
+
if (!Array.isArray(value)) return {};
|
|
179
|
+
const out: Record<string, string> = {};
|
|
180
|
+
for (let i = 0; i + 1 < value.length; i += 2) {
|
|
181
|
+
out[asString(value[i])] = asString(value[i + 1]);
|
|
182
|
+
}
|
|
183
|
+
return out;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const setArgs = (key: string, value: string | Uint8Array, options?: SetOptions): Arg[] => {
|
|
187
|
+
const args: Arg[] = [key, value];
|
|
188
|
+
if (options?.nx) args.push("NX");
|
|
189
|
+
if (options?.xx) args.push("XX");
|
|
190
|
+
if (options?.ex !== undefined) args.push("EX", options.ex);
|
|
191
|
+
if (options?.px !== undefined) args.push("PX", options.px);
|
|
192
|
+
if (options?.exAt !== undefined) args.push("EXAT", options.exAt);
|
|
193
|
+
if (options?.pxAt !== undefined) args.push("PXAT", options.pxAt);
|
|
194
|
+
if (options?.keepTtl) args.push("KEEPTTL");
|
|
195
|
+
return args;
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Read-only client over a runtime Redis URL.
|
|
200
|
+
*/
|
|
201
|
+
export const makeRead = (url: Url): ReadClient => ({
|
|
202
|
+
get: (key) => command(url, "GET", [key]).pipe(Effect.map(asNullableString)),
|
|
203
|
+
mget: (...keys) => command(url, "MGET", keys).pipe(Effect.map(asNullableStringArray)),
|
|
204
|
+
ping: (message) =>
|
|
205
|
+
command(url, "PING", message === undefined ? [] : [message]).pipe(Effect.map(asString)),
|
|
206
|
+
echo: (message) => command(url, "ECHO", [message]).pipe(Effect.map(asString)),
|
|
207
|
+
exists: (...keys) => command(url, "EXISTS", keys).pipe(Effect.map(asNumber)),
|
|
208
|
+
ttl: (key) => command(url, "TTL", [key]).pipe(Effect.map(asNumber)),
|
|
209
|
+
pttl: (key) => command(url, "PTTL", [key]).pipe(Effect.map(asNumber)),
|
|
210
|
+
type: (key) => command(url, "TYPE", [key]).pipe(Effect.map(asString)),
|
|
211
|
+
hget: (key, field) => command(url, "HGET", [key, field]).pipe(Effect.map(asNullableString)),
|
|
212
|
+
hmget: (key, ...fields) =>
|
|
213
|
+
command(url, "HMGET", [key, ...fields]).pipe(Effect.map(asNullableStringArray)),
|
|
214
|
+
hgetall: (key) => command(url, "HGETALL", [key]).pipe(Effect.map(asHash)),
|
|
215
|
+
hexists: (key, field) => command(url, "HEXISTS", [key, field]).pipe(Effect.map(asBoolean)),
|
|
216
|
+
hkeys: (key) => command(url, "HKEYS", [key]).pipe(Effect.map(asStringArray)),
|
|
217
|
+
hvals: (key) => command(url, "HVALS", [key]).pipe(Effect.map(asStringArray)),
|
|
218
|
+
hlen: (key) => command(url, "HLEN", [key]).pipe(Effect.map(asNumber)),
|
|
219
|
+
llen: (key) => command(url, "LLEN", [key]).pipe(Effect.map(asNumber)),
|
|
220
|
+
lrange: (key, start, stop) =>
|
|
221
|
+
command(url, "LRANGE", [key, start, stop]).pipe(Effect.map(asStringArray)),
|
|
222
|
+
lindex: (key, index) => command(url, "LINDEX", [key, index]).pipe(Effect.map(asNullableString)),
|
|
223
|
+
scard: (key) => command(url, "SCARD", [key]).pipe(Effect.map(asNumber)),
|
|
224
|
+
sismember: (key, member) => command(url, "SISMEMBER", [key, member]).pipe(Effect.map(asBoolean)),
|
|
225
|
+
smembers: (key) => command(url, "SMEMBERS", [key]).pipe(Effect.map(asStringArray)),
|
|
226
|
+
zscore: (key, member) => command(url, "ZSCORE", [key, member]).pipe(Effect.map(asNullableString)),
|
|
227
|
+
zcard: (key) => command(url, "ZCARD", [key]).pipe(Effect.map(asNumber)),
|
|
228
|
+
zrange: (key, start, stop) =>
|
|
229
|
+
command(url, "ZRANGE", [key, start, stop]).pipe(Effect.map(asStringArray)),
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Write client over a runtime Redis URL.
|
|
234
|
+
*/
|
|
235
|
+
export const makeWrite = (url: Url): WriteClient => ({
|
|
236
|
+
set: (key, value, options) =>
|
|
237
|
+
command(url, "SET", setArgs(key, value, options)).pipe(Effect.asVoid),
|
|
238
|
+
mset: (values) => {
|
|
239
|
+
const args: Arg[] = [];
|
|
240
|
+
for (const [key, value] of Object.entries(values)) {
|
|
241
|
+
args.push(key, value);
|
|
242
|
+
}
|
|
243
|
+
return command(url, "MSET", args).pipe(Effect.asVoid);
|
|
244
|
+
},
|
|
245
|
+
del: (...keys) => command(url, "DEL", keys).pipe(Effect.map(asNumber)),
|
|
246
|
+
unlink: (...keys) => command(url, "UNLINK", keys).pipe(Effect.map(asNumber)),
|
|
247
|
+
expire: (key, seconds) => command(url, "EXPIRE", [key, seconds]).pipe(Effect.map(asBoolean)),
|
|
248
|
+
persist: (key) => command(url, "PERSIST", [key]).pipe(Effect.map(asBoolean)),
|
|
249
|
+
incr: (key) => command(url, "INCR", [key]).pipe(Effect.map(asNumber)),
|
|
250
|
+
incrBy: (key, amount) => command(url, "INCRBY", [key, amount]).pipe(Effect.map(asNumber)),
|
|
251
|
+
decr: (key) => command(url, "DECR", [key]).pipe(Effect.map(asNumber)),
|
|
252
|
+
hset: (key, field, value) => {
|
|
253
|
+
const args: Arg[] = [key];
|
|
254
|
+
if (typeof field === "string") {
|
|
255
|
+
args.push(field, value ?? "");
|
|
256
|
+
} else {
|
|
257
|
+
for (const [name, item] of Object.entries(field)) {
|
|
258
|
+
args.push(name, item);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return command(url, "HSET", args).pipe(Effect.map(asNumber));
|
|
262
|
+
},
|
|
263
|
+
hdel: (key, ...fields) => command(url, "HDEL", [key, ...fields]).pipe(Effect.map(asNumber)),
|
|
264
|
+
hincrBy: (key, field, amount) =>
|
|
265
|
+
command(url, "HINCRBY", [key, field, amount]).pipe(Effect.map(asNumber)),
|
|
266
|
+
lpush: (key, ...values) => command(url, "LPUSH", [key, ...values]).pipe(Effect.map(asNumber)),
|
|
267
|
+
rpush: (key, ...values) => command(url, "RPUSH", [key, ...values]).pipe(Effect.map(asNumber)),
|
|
268
|
+
lpop: (key) => command(url, "LPOP", [key]).pipe(Effect.map(asNullableString)),
|
|
269
|
+
rpop: (key) => command(url, "RPOP", [key]).pipe(Effect.map(asNullableString)),
|
|
270
|
+
sadd: (key, ...members) => command(url, "SADD", [key, ...members]).pipe(Effect.map(asNumber)),
|
|
271
|
+
srem: (key, ...members) => command(url, "SREM", [key, ...members]).pipe(Effect.map(asNumber)),
|
|
272
|
+
zadd: (key, score, member) =>
|
|
273
|
+
command(url, "ZADD", [key, score, member]).pipe(Effect.map(asNumber)),
|
|
274
|
+
zrem: (key, ...members) => command(url, "ZREM", [key, ...members]).pipe(Effect.map(asNumber)),
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Full Redis client over a runtime Redis URL.
|
|
279
|
+
*
|
|
280
|
+
* Fly and Railway `*RedisHttp` layers pass this (or {@link makeRead} /
|
|
281
|
+
* {@link makeWrite}) as `makeClient`. Tests can also drive RESP
|
|
282
|
+
* directly via `command` / `run` / `connect`.
|
|
283
|
+
*
|
|
284
|
+
* ```typescript
|
|
285
|
+
* import * as Redis from "@alchemy.run/redis";
|
|
286
|
+
*
|
|
287
|
+
* const cache = Redis.make(url);
|
|
288
|
+
* yield* cache.set("marker", "hello");
|
|
289
|
+
* const value = yield* cache.get("marker");
|
|
290
|
+
* const echoed = yield* cache.send("ECHO", ["hi"]);
|
|
291
|
+
* ```
|
|
292
|
+
*/
|
|
293
|
+
export const make = (url: Url): Client => ({
|
|
294
|
+
...makeRead(url),
|
|
295
|
+
...makeWrite(url),
|
|
296
|
+
send: (name, args = []) => command(url, name, args),
|
|
297
|
+
pipeline: (commands) => commandPipeline(url, commands),
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Read + write client over a runtime Redis URL. Alias of {@link make}.
|
|
302
|
+
*/
|
|
303
|
+
export const makeReadWrite = (url: Url): ReadWriteClient => make(url);
|
package/src/Errors.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import * as Data from "effect/Data";
|
|
2
|
+
|
|
3
|
+
/** No `REDIS_URL` in the Function/Service environment. */
|
|
4
|
+
export class UrlMissing extends Data.TaggedError("Redis.UrlMissing")<{
|
|
5
|
+
resourceName: string;
|
|
6
|
+
}> {
|
|
7
|
+
override get message() {
|
|
8
|
+
return `REDIS_URL missing for ${this.resourceName}`;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** A Redis command failed (RESP error, socket, or protocol). */
|
|
13
|
+
export class CommandError extends Data.TaggedError("Redis.CommandError")<{
|
|
14
|
+
command: string;
|
|
15
|
+
cause: unknown;
|
|
16
|
+
}> {}
|
|
17
|
+
|
|
18
|
+
/** Redis replied with a RESP error (`-ERR …`, `-WRONGTYPE …`). */
|
|
19
|
+
export class ReplyError extends Data.TaggedError("Redis.ReplyError")<{
|
|
20
|
+
readonly code: string;
|
|
21
|
+
readonly message: string;
|
|
22
|
+
}> {}
|
|
23
|
+
|
|
24
|
+
/** Bytes on the wire were not valid RESP. */
|
|
25
|
+
export class ProtocolError extends Data.TaggedError("Redis.ProtocolError")<{
|
|
26
|
+
readonly message: string;
|
|
27
|
+
}> {}
|