bundis 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.
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Handshake / connection commands (§2.2).
3
+ *
4
+ * These must be answered before (or alongside) data commands or the stock client
5
+ * stalls during connect: HELLO, AUTH, SELECT, PING, INFO, QUIT, CLIENT, ECHO.
6
+ */
7
+
8
+ import { R, type Reply } from "../resp/types";
9
+ import { Errors } from "../engine/errors";
10
+ import type { CommandContext } from "../engine/context";
11
+
12
+ /** HELLO [protover [AUTH user pass] [SETNAME name]] → RESP3 server-info map. */
13
+ export function hello(ctx: CommandContext): Reply {
14
+ let i = 0;
15
+ if (ctx.argOpt(i) !== undefined && /^\d+$/.test(ctx.str(i))) {
16
+ const proto = parseInt(ctx.str(i), 10);
17
+ if (proto !== 2 && proto !== 3) {
18
+ return R.error("NOPROTO", "unsupported protocol version");
19
+ }
20
+ ctx.conn.proto = proto;
21
+ i++;
22
+ }
23
+ // Optional AUTH / SETNAME sub-options.
24
+ while (ctx.argOpt(i) !== undefined) {
25
+ const opt = ctx.upper(i);
26
+ if (opt === "AUTH") {
27
+ const pass = ctx.str(i + 2); // user is ctx.str(i+1), ignored (single user)
28
+ if (!authenticate(ctx, pass)) return R.error(...wrongPass());
29
+ ctx.conn.authed = true;
30
+ i += 3;
31
+ } else if (opt === "SETNAME") {
32
+ ctx.conn.name = ctx.str(i + 1);
33
+ i += 2;
34
+ } else {
35
+ throw Errors.syntax();
36
+ }
37
+ }
38
+ if (requiresAuth(ctx) && !ctx.conn.authed) return R.error(...noAuth());
39
+ ctx.conn.state = "READY";
40
+ return helloMap(ctx);
41
+ }
42
+
43
+ /** AUTH [user] pass */
44
+ export function auth(ctx: CommandContext): Reply {
45
+ ctx.requireArgc(1);
46
+ const pass = ctx.argc >= 2 ? ctx.str(1) : ctx.str(0);
47
+ if (ctx.server.config.password === null) {
48
+ // No password configured: Redis errors, but we stay lenient and accept.
49
+ ctx.conn.authed = true;
50
+ return R.ok();
51
+ }
52
+ if (!authenticate(ctx, pass)) throw Errors.wrongPass();
53
+ ctx.conn.authed = true;
54
+ return R.ok();
55
+ }
56
+
57
+ export function ping(ctx: CommandContext): Reply {
58
+ if (ctx.argc >= 1) return R.bulk(ctx.arg(0));
59
+ return R.simple("PONG");
60
+ }
61
+
62
+ export function select(_ctx: CommandContext): Reply {
63
+ // Single logical DB; accept any index for client compatibility.
64
+ return R.ok();
65
+ }
66
+
67
+ export function echo(ctx: CommandContext): Reply {
68
+ ctx.requireExactArgc(1);
69
+ return R.bulk(ctx.arg(0));
70
+ }
71
+
72
+ export function quit(ctx: CommandContext): null {
73
+ ctx.conn.send(R.ok());
74
+ ctx.conn.socket.end();
75
+ return null; // reply already written; socket is closing
76
+ }
77
+
78
+ export function info(ctx: CommandContext): Reply {
79
+ const lines = [
80
+ "# Server",
81
+ "redis_version:7.4.0",
82
+ "redis_mode:standalone",
83
+ "run_id:bundis",
84
+ "tcp_port:" + ctx.server.config.port,
85
+ "",
86
+ "# Clients",
87
+ "connected_clients:1",
88
+ "",
89
+ "# Keyspace",
90
+ `db0:keys=${ctx.storage.dbsize(ctx.nowMs)},expires=0,avg_ttl=0`,
91
+ "",
92
+ ];
93
+ return R.verbatim("txt", lines.join("\r\n"));
94
+ }
95
+
96
+ /** CLIENT SETINFO/SETNAME/GETNAME/ID/... — lenient, returns sensible replies. */
97
+ export function client(ctx: CommandContext): Reply {
98
+ const sub = ctx.upper(0);
99
+ switch (sub) {
100
+ case "SETNAME":
101
+ ctx.conn.name = ctx.argc >= 2 ? ctx.str(1) : "";
102
+ return R.ok();
103
+ case "GETNAME":
104
+ return R.bulk(ctx.conn.name || "");
105
+ case "ID":
106
+ return R.int(ctx.conn.id);
107
+ case "SETINFO":
108
+ return R.ok();
109
+ case "INFO":
110
+ return R.bulk(`id=${ctx.conn.id} name=${ctx.conn.name}`);
111
+ default:
112
+ return R.ok();
113
+ }
114
+ }
115
+
116
+ // ── helpers ──────────────────────────────────────────────────────────────---
117
+
118
+ function helloMap(ctx: CommandContext): Reply {
119
+ return R.map([
120
+ [R.bulk("server"), R.bulk("bundis")],
121
+ [R.bulk("version"), R.bulk("0.1.0")],
122
+ [R.bulk("proto"), R.int(ctx.conn.proto)],
123
+ [R.bulk("id"), R.int(ctx.conn.id)],
124
+ [R.bulk("mode"), R.bulk("standalone")],
125
+ [R.bulk("role"), R.bulk("master")],
126
+ [R.bulk("modules"), R.array([])],
127
+ ]);
128
+ }
129
+
130
+ function authenticate(ctx: CommandContext, pass: string): boolean {
131
+ const expected = ctx.server.config.password;
132
+ return expected === null || expected === pass;
133
+ }
134
+
135
+ function requiresAuth(ctx: CommandContext): boolean {
136
+ return ctx.server.config.password !== null;
137
+ }
138
+
139
+ // Small adapters so we can `R.error(...wrongPass())` cleanly.
140
+ function wrongPass(): [string, string] {
141
+ const e = Errors.wrongPass();
142
+ return [e.code, e.message];
143
+ }
144
+ function noAuth(): [string, string] {
145
+ const e = Errors.noAuth();
146
+ return [e.code, e.message];
147
+ }
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Hash commands (Phase 1).
3
+ *
4
+ * Type-conversion notes (§2.4): HGETALL → RESP3 Map (`%`) so the client yields an
5
+ * object; HEXISTS → Integer (client coerces to boolean); HINCRBYFLOAT → bulk.
6
+ */
7
+
8
+ import { R, type Reply } from "../resp/types";
9
+ import { Errors } from "../engine/errors";
10
+ import type { CommandContext } from "../engine/context";
11
+
12
+ export function hset(ctx: CommandContext): Reply {
13
+ ctx.requireArgc(3);
14
+ if (ctx.argc % 2 !== 1) throw Errors.wrongArgs(ctx.cmd); // key + field/value pairs
15
+ const key = ctx.arg(0);
16
+ const pairs: Array<[Uint8Array, Uint8Array]> = [];
17
+ for (let i = 1; i < ctx.argc; i += 2) pairs.push([ctx.arg(i), ctx.arg(i + 1)]);
18
+ return R.int(ctx.storage.hSet(key, pairs, ctx.nowMs));
19
+ }
20
+
21
+ /** HMSET is like HSET but returns OK. */
22
+ export function hmset(ctx: CommandContext): Reply {
23
+ ctx.requireArgc(3);
24
+ if (ctx.argc % 2 !== 1) throw Errors.wrongArgs(ctx.cmd);
25
+ const key = ctx.arg(0);
26
+ const pairs: Array<[Uint8Array, Uint8Array]> = [];
27
+ for (let i = 1; i < ctx.argc; i += 2) pairs.push([ctx.arg(i), ctx.arg(i + 1)]);
28
+ ctx.storage.hSet(key, pairs, ctx.nowMs);
29
+ return R.ok();
30
+ }
31
+
32
+ export function hsetnx(ctx: CommandContext): Reply {
33
+ ctx.requireExactArgc(3);
34
+ const key = ctx.arg(0);
35
+ const field = ctx.arg(1);
36
+ return ctx.storage.withTransaction(() => {
37
+ if (ctx.storage.hExists(key, field, ctx.nowMs)) return R.int(0);
38
+ ctx.storage.hSet(key, [[field, ctx.arg(2)]], ctx.nowMs);
39
+ return R.int(1);
40
+ });
41
+ }
42
+
43
+ export function hget(ctx: CommandContext): Reply {
44
+ ctx.requireExactArgc(2);
45
+ return R.bulk(ctx.storage.hGet(ctx.arg(0), ctx.arg(1), ctx.nowMs));
46
+ }
47
+
48
+ export function hmget(ctx: CommandContext): Reply {
49
+ ctx.requireArgc(2);
50
+ const key = ctx.arg(0);
51
+ const out: Reply[] = [];
52
+ for (let i = 1; i < ctx.argc; i++) {
53
+ out.push(R.bulk(ctx.storage.hGet(key, ctx.arg(i), ctx.nowMs)));
54
+ }
55
+ return R.array(out);
56
+ }
57
+
58
+ export function hgetall(ctx: CommandContext): Reply {
59
+ ctx.requireExactArgc(1);
60
+ const entries = ctx.storage.hGetAll(ctx.arg(0), ctx.nowMs);
61
+ return R.map(entries.map(([f, v]) => [R.bulk(f), R.bulk(v)] as const));
62
+ }
63
+
64
+ export function hdel(ctx: CommandContext): Reply {
65
+ ctx.requireArgc(2);
66
+ return R.int(ctx.storage.hDel(ctx.arg(0), ctx.args.slice(2), ctx.nowMs));
67
+ }
68
+
69
+ export function hexists(ctx: CommandContext): Reply {
70
+ ctx.requireExactArgc(2);
71
+ return R.int(ctx.storage.hExists(ctx.arg(0), ctx.arg(1), ctx.nowMs) ? 1 : 0);
72
+ }
73
+
74
+ export function hkeys(ctx: CommandContext): Reply {
75
+ ctx.requireExactArgc(1);
76
+ return R.array(ctx.storage.hKeys(ctx.arg(0), ctx.nowMs).map((k) => R.bulk(k)));
77
+ }
78
+
79
+ export function hvals(ctx: CommandContext): Reply {
80
+ ctx.requireExactArgc(1);
81
+ return R.array(ctx.storage.hVals(ctx.arg(0), ctx.nowMs).map((v) => R.bulk(v)));
82
+ }
83
+
84
+ export function hlen(ctx: CommandContext): Reply {
85
+ ctx.requireExactArgc(1);
86
+ return R.int(ctx.storage.hLen(ctx.arg(0), ctx.nowMs));
87
+ }
88
+
89
+ export function hincrby(ctx: CommandContext): Reply {
90
+ ctx.requireExactArgc(3);
91
+ return R.int(ctx.storage.hIncrBy(ctx.arg(0), ctx.arg(1), ctx.int(2), ctx.nowMs));
92
+ }
93
+
94
+ export function hincrbyfloat(ctx: CommandContext): Reply {
95
+ ctx.requireExactArgc(3);
96
+ const next = ctx.storage.hIncrByFloat(ctx.arg(0), ctx.arg(1), ctx.float(2), ctx.nowMs);
97
+ return R.bulk(formatFloat(next));
98
+ }
99
+
100
+ function formatFloat(n: number): string {
101
+ if (n === Infinity) return "inf";
102
+ if (n === -Infinity) return "-inf";
103
+ let s = n.toPrecision(17);
104
+ if (s.includes(".") && !/[eE]/.test(s)) s = s.replace(/0+$/, "").replace(/\.$/, "");
105
+ return Number(s).toString();
106
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Multi-key string commands (Phase 1): MGET, MSET, MSETNX, SETEX, PSETEX, SETNX.
3
+ * MSET/MSETNX run inside one transaction for atomicity.
4
+ */
5
+
6
+ import { R, type Reply } from "../resp/types";
7
+ import { Errors } from "../engine/errors";
8
+ import type { CommandContext } from "../engine/context";
9
+
10
+ export function mget(ctx: CommandContext): Reply {
11
+ ctx.requireArgc(1);
12
+ const out: Reply[] = [];
13
+ for (let i = 0; i < ctx.argc; i++) {
14
+ out.push(R.bulk(ctx.storage.kvGet(ctx.arg(i), ctx.nowMs)));
15
+ }
16
+ return R.array(out);
17
+ }
18
+
19
+ export function mset(ctx: CommandContext): Reply {
20
+ ctx.requireArgc(2);
21
+ if (ctx.argc % 2 !== 0) throw Errors.wrongArgs(ctx.cmd);
22
+ ctx.storage.withTransaction(() => {
23
+ for (let i = 0; i < ctx.argc; i += 2) {
24
+ ctx.storage.kvSet(ctx.arg(i), ctx.arg(i + 1), ctx.nowMs);
25
+ }
26
+ });
27
+ return R.ok();
28
+ }
29
+
30
+ export function msetnx(ctx: CommandContext): Reply {
31
+ ctx.requireArgc(2);
32
+ if (ctx.argc % 2 !== 0) throw Errors.wrongArgs(ctx.cmd);
33
+ const ok = ctx.storage.withTransaction(() => {
34
+ for (let i = 0; i < ctx.argc; i += 2) {
35
+ if (ctx.storage.exists(ctx.arg(i), ctx.nowMs)) return false;
36
+ }
37
+ for (let i = 0; i < ctx.argc; i += 2) {
38
+ ctx.storage.kvSet(ctx.arg(i), ctx.arg(i + 1), ctx.nowMs);
39
+ }
40
+ return true;
41
+ });
42
+ return R.int(ok ? 1 : 0);
43
+ }
44
+
45
+ export function setex(ctx: CommandContext): Reply {
46
+ ctx.requireExactArgc(3);
47
+ const atMs = ctx.nowMs + Number(ctx.int(1)) * 1000;
48
+ ctx.storage.kvSet(ctx.arg(0), ctx.arg(2), ctx.nowMs, { expireAtMs: atMs });
49
+ return R.ok();
50
+ }
51
+
52
+ export function psetex(ctx: CommandContext): Reply {
53
+ ctx.requireExactArgc(3);
54
+ const atMs = ctx.nowMs + Number(ctx.int(1));
55
+ ctx.storage.kvSet(ctx.arg(0), ctx.arg(2), ctx.nowMs, { expireAtMs: atMs });
56
+ return R.ok();
57
+ }
58
+
59
+ export function setnx(ctx: CommandContext): Reply {
60
+ ctx.requireExactArgc(2);
61
+ const res = ctx.storage.kvSet(ctx.arg(0), ctx.arg(1), ctx.nowMs, { mode: "NX" });
62
+ return R.int(res === "set" ? 1 : 0);
63
+ }
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Pub/Sub commands (Phase 2): SUBSCRIBE, UNSUBSCRIBE, PSUBSCRIBE, PUNSUBSCRIBE,
3
+ * PUBLISH, PUBSUB.
4
+ *
5
+ * (Un)subscribe confirmations are sent as RESP3 push frames (`>`), one per
6
+ * channel, matching what the stock client's subscribe()/unsubscribe() expect.
7
+ * These handlers write their own frames and return null (the dispatcher then
8
+ * writes nothing further).
9
+ */
10
+
11
+ import { R, type Reply } from "../resp/types";
12
+ import type { CommandContext } from "../engine/context";
13
+
14
+ export function subscribe(ctx: CommandContext): null {
15
+ ctx.requireArgc(1);
16
+ for (let i = 0; i < ctx.argc; i++) {
17
+ const channel = ctx.str(i);
18
+ const count = ctx.server.hub.subscribe(ctx.conn, channel);
19
+ ctx.conn.state = "SUBSCRIBED";
20
+ ctx.conn.send(R.push([R.bulk("subscribe"), R.bulk(channel), R.int(count)]));
21
+ }
22
+ return null;
23
+ }
24
+
25
+ export function psubscribe(ctx: CommandContext): null {
26
+ ctx.requireArgc(1);
27
+ for (let i = 0; i < ctx.argc; i++) {
28
+ const pattern = ctx.str(i);
29
+ const count = ctx.server.hub.psubscribe(ctx.conn, pattern);
30
+ ctx.conn.state = "SUBSCRIBED";
31
+ ctx.conn.send(R.push([R.bulk("psubscribe"), R.bulk(pattern), R.int(count)]));
32
+ }
33
+ return null;
34
+ }
35
+
36
+ export function unsubscribe(ctx: CommandContext): null {
37
+ const channels =
38
+ ctx.argc >= 1
39
+ ? rangeArgs(ctx, 0)
40
+ : [...ctx.conn.channels];
41
+ if (channels.length === 0) {
42
+ // No channels: still send a single confirmation with null channel.
43
+ ctx.conn.send(
44
+ R.push([R.bulk("unsubscribe"), R.nullReply(), R.int(ctx.conn.subscriptionCount())]),
45
+ );
46
+ maybeExitSubscribe(ctx);
47
+ return null;
48
+ }
49
+ for (const channel of channels) {
50
+ const count = ctx.server.hub.unsubscribe(ctx.conn, channel);
51
+ ctx.conn.send(R.push([R.bulk("unsubscribe"), R.bulk(channel), R.int(count)]));
52
+ }
53
+ maybeExitSubscribe(ctx);
54
+ return null;
55
+ }
56
+
57
+ export function punsubscribe(ctx: CommandContext): null {
58
+ const patterns = ctx.argc >= 1 ? rangeArgs(ctx, 0) : [...ctx.conn.patterns];
59
+ if (patterns.length === 0) {
60
+ ctx.conn.send(
61
+ R.push([R.bulk("punsubscribe"), R.nullReply(), R.int(ctx.conn.subscriptionCount())]),
62
+ );
63
+ maybeExitSubscribe(ctx);
64
+ return null;
65
+ }
66
+ for (const pattern of patterns) {
67
+ const count = ctx.server.hub.punsubscribe(ctx.conn, pattern);
68
+ ctx.conn.send(R.push([R.bulk("punsubscribe"), R.bulk(pattern), R.int(count)]));
69
+ }
70
+ maybeExitSubscribe(ctx);
71
+ return null;
72
+ }
73
+
74
+ export function publish(ctx: CommandContext): Reply {
75
+ ctx.requireExactArgc(2);
76
+ return R.int(ctx.server.hub.publish(ctx.str(0), ctx.arg(1)));
77
+ }
78
+
79
+ export function pubsub(ctx: CommandContext): Reply {
80
+ ctx.requireArgc(1);
81
+ const sub = ctx.upper(0);
82
+ switch (sub) {
83
+ case "CHANNELS":
84
+ return R.array(ctx.server.hub.channelNames().map((c) => R.bulk(c)));
85
+ case "NUMSUB": {
86
+ const out: Reply[] = [];
87
+ for (let i = 1; i < ctx.argc; i++) {
88
+ const ch = ctx.str(i);
89
+ out.push(R.bulk(ch), R.int(ctx.server.hub.numSub(ch)));
90
+ }
91
+ return R.array(out);
92
+ }
93
+ default:
94
+ return R.array([]);
95
+ }
96
+ }
97
+
98
+ function rangeArgs(ctx: CommandContext, from: number): string[] {
99
+ const out: string[] = [];
100
+ for (let i = from; i < ctx.argc; i++) out.push(ctx.str(i));
101
+ return out;
102
+ }
103
+
104
+ function maybeExitSubscribe(ctx: CommandContext): void {
105
+ if (!ctx.conn.inSubscribeMode()) ctx.conn.state = "READY";
106
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Set commands (Phase 1).
3
+ *
4
+ * Type-conversion notes (§2.4): SMEMBERS / SPOP(count) / SRANDMEMBER(count) →
5
+ * RESP3 Set (`~`) so the client yields an array; SISMEMBER → Integer (client
6
+ * coerces to boolean).
7
+ */
8
+
9
+ import { R, type Reply } from "../resp/types";
10
+ import type { CommandContext } from "../engine/context";
11
+
12
+ export function sadd(ctx: CommandContext): Reply {
13
+ ctx.requireArgc(2);
14
+ return R.int(ctx.storage.sAdd(ctx.arg(0), ctx.args.slice(2), ctx.nowMs));
15
+ }
16
+
17
+ export function srem(ctx: CommandContext): Reply {
18
+ ctx.requireArgc(2);
19
+ return R.int(ctx.storage.sRem(ctx.arg(0), ctx.args.slice(2), ctx.nowMs));
20
+ }
21
+
22
+ export function sismember(ctx: CommandContext): Reply {
23
+ ctx.requireExactArgc(2);
24
+ return R.int(ctx.storage.sIsMember(ctx.arg(0), ctx.arg(1), ctx.nowMs) ? 1 : 0);
25
+ }
26
+
27
+ export function smembers(ctx: CommandContext): Reply {
28
+ ctx.requireExactArgc(1);
29
+ return R.set(ctx.storage.sMembers(ctx.arg(0), ctx.nowMs).map((m) => R.bulk(m)));
30
+ }
31
+
32
+ export function scard(ctx: CommandContext): Reply {
33
+ ctx.requireExactArgc(1);
34
+ return R.int(ctx.storage.sCard(ctx.arg(0), ctx.nowMs));
35
+ }
36
+
37
+ export function srandmember(ctx: CommandContext): Reply {
38
+ ctx.requireArgc(1);
39
+ const hasCount = ctx.argOpt(1) !== undefined;
40
+ const count = hasCount ? Number(ctx.int(1)) : null;
41
+ const res = ctx.storage.sRandMember(ctx.arg(0), count, ctx.nowMs);
42
+ if (count === null) {
43
+ return res === null ? R.nullReply() : R.bulk(res as Uint8Array);
44
+ }
45
+ // With a count argument, reply is an array (possibly empty), not a set.
46
+ return R.array((res as Uint8Array[]).map((m) => R.bulk(m)));
47
+ }
48
+
49
+ export function spop(ctx: CommandContext): Reply {
50
+ ctx.requireArgc(1);
51
+ const hasCount = ctx.argOpt(1) !== undefined;
52
+ const count = hasCount ? Number(ctx.int(1)) : null;
53
+ const res = ctx.storage.sPop(ctx.arg(0), count, ctx.nowMs);
54
+ if (count === null) {
55
+ return res === null ? R.nullReply() : R.bulk(res as Uint8Array);
56
+ }
57
+ return R.set((res as Uint8Array[]).map((m) => R.bulk(m)));
58
+ }
@@ -0,0 +1,156 @@
1
+ /**
2
+ * String / KV + numeric commands (Phase 0).
3
+ *
4
+ * SET (with EX/PX/EXAT/PXAT/NX/XX/KEEPTTL/GET), GET, GETSET, GETDEL, APPEND,
5
+ * STRLEN, DEL, EXISTS, INCR/DECR/INCRBY/DECRBY/INCRBYFLOAT.
6
+ *
7
+ * Type-conversion notes (§2.4): GET miss → RESP3 null; EXISTS → Integer (the
8
+ * stock client coerces 1/0 to boolean); INCRBYFLOAT → bulk string.
9
+ */
10
+
11
+ import { R, type Reply } from "../resp/types";
12
+ import { Errors } from "../engine/errors";
13
+ import type { CommandContext } from "../engine/context";
14
+ import type { SetOptions } from "../storage/types";
15
+
16
+ export function set(ctx: CommandContext): Reply {
17
+ ctx.requireArgc(2);
18
+ const key = ctx.arg(0);
19
+ const value = ctx.arg(1);
20
+ const opts: { -readonly [K in keyof SetOptions]: SetOptions[K] } = {};
21
+ let wantGet = false;
22
+
23
+ let i = 2;
24
+ while (ctx.argOpt(i) !== undefined) {
25
+ const o = ctx.upper(i);
26
+ switch (o) {
27
+ case "EX":
28
+ opts.expireAtMs = ctx.nowMs + Number(ctx.int(i + 1)) * 1000;
29
+ i += 2;
30
+ break;
31
+ case "PX":
32
+ opts.expireAtMs = ctx.nowMs + Number(ctx.int(i + 1));
33
+ i += 2;
34
+ break;
35
+ case "EXAT":
36
+ opts.expireAtMs = Number(ctx.int(i + 1)) * 1000;
37
+ i += 2;
38
+ break;
39
+ case "PXAT":
40
+ opts.expireAtMs = Number(ctx.int(i + 1));
41
+ i += 2;
42
+ break;
43
+ case "NX":
44
+ opts.mode = "NX";
45
+ i += 1;
46
+ break;
47
+ case "XX":
48
+ opts.mode = "XX";
49
+ i += 1;
50
+ break;
51
+ case "KEEPTTL":
52
+ opts.keepTtl = true;
53
+ i += 1;
54
+ break;
55
+ case "GET":
56
+ wantGet = true;
57
+ i += 1;
58
+ break;
59
+ default:
60
+ throw Errors.syntax();
61
+ }
62
+ }
63
+
64
+ let old: Uint8Array | null = null;
65
+ const result = ctx.storage.withTransaction(() => {
66
+ if (wantGet) old = ctx.storage.kvGet(key, ctx.nowMs);
67
+ return ctx.storage.kvSet(key, value, ctx.nowMs, opts);
68
+ });
69
+
70
+ if (wantGet) return R.bulk(old);
71
+ return result === "set" ? R.ok() : R.nullReply();
72
+ }
73
+
74
+ export function get(ctx: CommandContext): Reply {
75
+ ctx.requireExactArgc(1);
76
+ return R.bulk(ctx.storage.kvGet(ctx.arg(0), ctx.nowMs));
77
+ }
78
+
79
+ export function getset(ctx: CommandContext): Reply {
80
+ ctx.requireExactArgc(2);
81
+ const key = ctx.arg(0);
82
+ return ctx.storage.withTransaction(() => {
83
+ const old = ctx.storage.kvGet(key, ctx.nowMs);
84
+ ctx.storage.kvSet(key, ctx.arg(1), ctx.nowMs);
85
+ return R.bulk(old);
86
+ });
87
+ }
88
+
89
+ export function getdel(ctx: CommandContext): Reply {
90
+ ctx.requireExactArgc(1);
91
+ const key = ctx.arg(0);
92
+ return ctx.storage.withTransaction(() => {
93
+ const old = ctx.storage.kvGet(key, ctx.nowMs);
94
+ if (old !== null) ctx.storage.del([key], ctx.nowMs);
95
+ return R.bulk(old);
96
+ });
97
+ }
98
+
99
+ export function append(ctx: CommandContext): Reply {
100
+ ctx.requireExactArgc(2);
101
+ return R.int(ctx.storage.append(ctx.arg(0), ctx.arg(1), ctx.nowMs));
102
+ }
103
+
104
+ export function strlen(ctx: CommandContext): Reply {
105
+ ctx.requireExactArgc(1);
106
+ const v = ctx.storage.kvGet(ctx.arg(0), ctx.nowMs);
107
+ return R.int(v === null ? 0 : v.length);
108
+ }
109
+
110
+ export function del(ctx: CommandContext): Reply {
111
+ ctx.requireArgc(1);
112
+ return R.int(ctx.storage.del(ctx.args.slice(1), ctx.nowMs));
113
+ }
114
+
115
+ export function exists(ctx: CommandContext): Reply {
116
+ ctx.requireArgc(1);
117
+ let n = 0;
118
+ for (let i = 0; i < ctx.argc; i++) {
119
+ if (ctx.storage.exists(ctx.arg(i), ctx.nowMs)) n++;
120
+ }
121
+ return R.int(n);
122
+ }
123
+
124
+ export function incr(ctx: CommandContext): Reply {
125
+ ctx.requireExactArgc(1);
126
+ return R.int(ctx.storage.incrBy(ctx.arg(0), 1n, ctx.nowMs));
127
+ }
128
+
129
+ export function decr(ctx: CommandContext): Reply {
130
+ ctx.requireExactArgc(1);
131
+ return R.int(ctx.storage.incrBy(ctx.arg(0), -1n, ctx.nowMs));
132
+ }
133
+
134
+ export function incrby(ctx: CommandContext): Reply {
135
+ ctx.requireExactArgc(2);
136
+ return R.int(ctx.storage.incrBy(ctx.arg(0), ctx.int(1), ctx.nowMs));
137
+ }
138
+
139
+ export function decrby(ctx: CommandContext): Reply {
140
+ ctx.requireExactArgc(2);
141
+ return R.int(ctx.storage.incrBy(ctx.arg(0), -ctx.int(1), ctx.nowMs));
142
+ }
143
+
144
+ export function incrbyfloat(ctx: CommandContext): Reply {
145
+ ctx.requireExactArgc(2);
146
+ const next = ctx.storage.incrByFloat(ctx.arg(0), ctx.float(1), ctx.nowMs);
147
+ return R.bulk(formatFloat(next));
148
+ }
149
+
150
+ function formatFloat(n: number): string {
151
+ if (n === Infinity) return "inf";
152
+ if (n === -Infinity) return "-inf";
153
+ let s = n.toPrecision(17);
154
+ if (s.includes(".") && !/[eE]/.test(s)) s = s.replace(/0+$/, "").replace(/\.$/, "");
155
+ return Number(s).toString();
156
+ }