@nestjs-redisx/testing 1.5.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 (37) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +76 -0
  3. package/dist/index.d.ts +10 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +1750 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/index.mjs +1739 -0
  8. package/dist/index.mjs.map +1 -0
  9. package/dist/memory/api/redis-testing.module.d.ts +20 -0
  10. package/dist/memory/api/redis-testing.module.d.ts.map +1 -0
  11. package/dist/memory/api/register-memory-driver.d.ts +8 -0
  12. package/dist/memory/api/register-memory-driver.d.ts.map +1 -0
  13. package/dist/memory/application/ports/command-executor.port.d.ts +9 -0
  14. package/dist/memory/application/ports/command-executor.port.d.ts.map +1 -0
  15. package/dist/memory/application/services/command-executor.service.d.ts +49 -0
  16. package/dist/memory/application/services/command-executor.service.d.ts.map +1 -0
  17. package/dist/memory/domain/lua/lua-interpreter.d.ts +46 -0
  18. package/dist/memory/domain/lua/lua-interpreter.d.ts.map +1 -0
  19. package/dist/memory/domain/lua/lua-lexer.d.ts +11 -0
  20. package/dist/memory/domain/lua/lua-lexer.d.ts.map +1 -0
  21. package/dist/memory/domain/lua/lua-parser.d.ts +67 -0
  22. package/dist/memory/domain/lua/lua-parser.d.ts.map +1 -0
  23. package/dist/memory/domain/lua/redis-call.port.d.ts +7 -0
  24. package/dist/memory/domain/lua/redis-call.port.d.ts.map +1 -0
  25. package/dist/memory/domain/store/memory-store.d.ts +59 -0
  26. package/dist/memory/domain/store/memory-store.d.ts.map +1 -0
  27. package/dist/memory/domain/store/stream-value.d.ts +74 -0
  28. package/dist/memory/domain/store/stream-value.d.ts.map +1 -0
  29. package/dist/memory/infrastructure/adapters/memory-redis.adapter.d.ts +22 -0
  30. package/dist/memory/infrastructure/adapters/memory-redis.adapter.d.ts.map +1 -0
  31. package/dist/shared/constants/index.d.ts +6 -0
  32. package/dist/shared/constants/index.d.ts.map +1 -0
  33. package/dist/shared/errors/index.d.ts +22 -0
  34. package/dist/shared/errors/index.d.ts.map +1 -0
  35. package/dist/shared/types/index.d.ts +19 -0
  36. package/dist/shared/types/index.d.ts.map +1 -0
  37. package/package.json +76 -0
package/dist/index.mjs ADDED
@@ -0,0 +1,1739 @@
1
+ import { registerDriver, BaseRedisDriver, RedisXError, ErrorCode, RedisModule } from '@nestjs-redisx/core';
2
+ import { createHash } from 'crypto';
3
+
4
+ // src/memory/api/register-memory-driver.ts
5
+
6
+ // src/shared/constants/index.ts
7
+ var MEMORY_DRIVER_TYPE = "memory";
8
+ var MemoryDriverError = class extends RedisXError {
9
+ constructor(message, cause) {
10
+ super(message, ErrorCode.OP_FAILED, cause);
11
+ this.name = "MemoryDriverError";
12
+ }
13
+ };
14
+ var LuaExecutionError = class extends MemoryDriverError {
15
+ constructor(message, cause) {
16
+ super(`Lua execution error: ${message}`, cause);
17
+ this.name = "LuaExecutionError";
18
+ }
19
+ };
20
+ var WrongTypeError = class extends MemoryDriverError {
21
+ constructor() {
22
+ super("WRONGTYPE Operation against a key holding the wrong kind of value");
23
+ this.name = "WrongTypeError";
24
+ }
25
+ };
26
+
27
+ // src/memory/domain/store/stream-value.ts
28
+ function parseId(id) {
29
+ const [msPart, seqPart = "0"] = id.split("-");
30
+ return { ms: Number(msPart), seq: Number(seqPart) };
31
+ }
32
+ function compareIds(a, b) {
33
+ const pa = parseId(a);
34
+ const pb = parseId(b);
35
+ return pa.ms !== pb.ms ? pa.ms - pb.ms : pa.seq - pb.seq;
36
+ }
37
+ function resolveRangeBound(token, isStart) {
38
+ let exclusive = false;
39
+ let t = token;
40
+ if (t.startsWith("(")) {
41
+ exclusive = true;
42
+ t = t.slice(1);
43
+ }
44
+ if (t === "-") return { id: "0-0", exclusive };
45
+ if (t === "+") return { id: `${Number.MAX_SAFE_INTEGER}-${Number.MAX_SAFE_INTEGER}`, exclusive };
46
+ if (!t.includes("-")) t = isStart ? `${t}-0` : `${t}-${Number.MAX_SAFE_INTEGER}`;
47
+ return { id: t, exclusive };
48
+ }
49
+ var StreamValue = class {
50
+ entries = [];
51
+ lastId = "0-0";
52
+ groups = /* @__PURE__ */ new Map();
53
+ /** Computes the next auto id (`*`) given the current time, keeping ids monotonic. */
54
+ nextAutoId(now) {
55
+ const last = parseId(this.lastId);
56
+ if (now > last.ms) return `${now}-0`;
57
+ return `${last.ms}-${last.seq + 1}`;
58
+ }
59
+ /** Resolves an explicit or `*` id for XADD, validating monotonicity. */
60
+ resolveAddId(id, now) {
61
+ if (id === "*") return this.nextAutoId(now);
62
+ let resolved = id;
63
+ if (id.endsWith("-*")) {
64
+ const ms = Number(id.slice(0, -2));
65
+ const last = parseId(this.lastId);
66
+ resolved = last.ms === ms ? `${ms}-${last.seq + 1}` : `${ms}-0`;
67
+ }
68
+ if (compareIds(resolved, this.lastId) <= 0 && this.lastId !== "0-0") {
69
+ throw new MemoryDriverError("ERR The ID specified in XADD is equal or smaller than the target stream top item");
70
+ }
71
+ return resolved;
72
+ }
73
+ /** Appends an entry. Returns its id. */
74
+ add(id, fields, now) {
75
+ const resolved = this.resolveAddId(id, now);
76
+ this.entries.push({ id: resolved, fields });
77
+ this.lastId = resolved;
78
+ return resolved;
79
+ }
80
+ len() {
81
+ return this.entries.length;
82
+ }
83
+ /** Entries within [start, end] (inclusive bounds, `-`/`+`/`(` supported). */
84
+ range(start, end, count, reverse = false) {
85
+ const lo = resolveRangeBound(start, true);
86
+ const hi = resolveRangeBound(end, false);
87
+ let out = this.entries.filter((e) => {
88
+ const geLo = lo.exclusive ? compareIds(e.id, lo.id) > 0 : compareIds(e.id, lo.id) >= 0;
89
+ const leHi = hi.exclusive ? compareIds(e.id, hi.id) < 0 : compareIds(e.id, hi.id) <= 0;
90
+ return geLo && leHi;
91
+ });
92
+ if (reverse) out = out.reverse();
93
+ if (count !== void 0 && count >= 0) out = out.slice(0, count);
94
+ return out;
95
+ }
96
+ /** Deletes entries by id. Returns the number removed. */
97
+ del(ids) {
98
+ let removed = 0;
99
+ for (const id of ids) {
100
+ const idx = this.entries.findIndex((e) => e.id === id);
101
+ if (idx !== -1) {
102
+ this.entries.splice(idx, 1);
103
+ removed += 1;
104
+ }
105
+ }
106
+ return removed;
107
+ }
108
+ /** Trims to at most `maxLen` newest entries. Returns the number removed. */
109
+ trimMaxLen(maxLen) {
110
+ if (maxLen < 0 || this.entries.length <= maxLen) return 0;
111
+ const removed = this.entries.length - maxLen;
112
+ this.entries.splice(0, removed);
113
+ return removed;
114
+ }
115
+ // --- consumer groups ------------------------------------------------------
116
+ createGroup(name, startId) {
117
+ if (this.groups.has(name)) {
118
+ throw new MemoryDriverError(`BUSYGROUP Consumer Group name already exists`);
119
+ }
120
+ const lastDeliveredId = startId === "$" ? this.lastId : startId === "0" ? "0-0" : startId;
121
+ this.groups.set(name, { lastDeliveredId, pending: /* @__PURE__ */ new Map(), consumers: /* @__PURE__ */ new Set() });
122
+ }
123
+ destroyGroup(name) {
124
+ return this.groups.delete(name) ? 1 : 0;
125
+ }
126
+ setGroupId(name, id) {
127
+ const group = this.requireGroup(name);
128
+ group.lastDeliveredId = id === "$" ? this.lastId : id === "0" ? "0-0" : id;
129
+ }
130
+ delConsumer(name, consumer) {
131
+ const group = this.requireGroup(name);
132
+ let removed = 0;
133
+ for (const [id, p] of group.pending) {
134
+ if (p.consumer === consumer) {
135
+ group.pending.delete(id);
136
+ removed += 1;
137
+ }
138
+ }
139
+ group.consumers.delete(consumer);
140
+ return removed;
141
+ }
142
+ requireGroup(name) {
143
+ const group = this.groups.get(name);
144
+ if (!group) throw new MemoryDriverError(`NOGROUP No such consumer group '${name}'`);
145
+ return group;
146
+ }
147
+ /**
148
+ * XREADGROUP semantics. `id === '>'` delivers never-delivered entries and adds
149
+ * them to the consumer's PEL; an explicit id re-reads that consumer's pending
150
+ * history with id greater than the given one.
151
+ */
152
+ readGroup(name, consumer, id, now, count, noAck) {
153
+ const group = this.requireGroup(name);
154
+ group.consumers.add(consumer);
155
+ if (id === ">") {
156
+ let fresh = this.entries.filter((e) => compareIds(e.id, group.lastDeliveredId) > 0);
157
+ if (count !== void 0) fresh = fresh.slice(0, count);
158
+ for (const entry of fresh) {
159
+ group.lastDeliveredId = entry.id;
160
+ if (!noAck) group.pending.set(entry.id, { consumer, deliveryTime: now, deliveryCount: 1 });
161
+ }
162
+ return fresh;
163
+ }
164
+ const from = resolveRangeBound(id, true);
165
+ const pendingIds = [...group.pending.entries()].filter(([eid, p]) => p.consumer === consumer && compareIds(eid, from.id) >= 0).map(([eid]) => eid).sort(compareIds);
166
+ const limited = count !== void 0 ? pendingIds.slice(0, count) : pendingIds;
167
+ return limited.map((eid) => this.entries.find((e) => e.id === eid)).filter((e) => e !== void 0);
168
+ }
169
+ ack(name, ids) {
170
+ const group = this.requireGroup(name);
171
+ let acked = 0;
172
+ for (const id of ids) {
173
+ if (group.pending.delete(id)) acked += 1;
174
+ }
175
+ return acked;
176
+ }
177
+ /** XPENDING summary: total, min/max pending id, and per-consumer counts. */
178
+ pendingSummary(name) {
179
+ const group = this.requireGroup(name);
180
+ const ids = [...group.pending.keys()].sort(compareIds);
181
+ const perConsumer = /* @__PURE__ */ new Map();
182
+ for (const p of group.pending.values()) perConsumer.set(p.consumer, (perConsumer.get(p.consumer) ?? 0) + 1);
183
+ return {
184
+ count: ids.length,
185
+ minId: ids[0] ?? null,
186
+ maxId: ids[ids.length - 1] ?? null,
187
+ consumers: [...perConsumer.entries()]
188
+ };
189
+ }
190
+ /** XPENDING range: pending entries in [start,end], optionally filtered by consumer. */
191
+ pendingRange(name, start, end, count, now, consumer) {
192
+ const group = this.requireGroup(name);
193
+ const lo = resolveRangeBound(start, true);
194
+ const hi = resolveRangeBound(end, false);
195
+ return [...group.pending.entries()].filter(([id]) => compareIds(id, lo.id) >= 0 && compareIds(id, hi.id) <= 0).filter(([, p]) => consumer ? p.consumer === consumer : true).sort((a, b) => compareIds(a[0], b[0])).slice(0, count).map(([id, p]) => ({ id, consumer: p.consumer, idleTime: Math.max(0, now - p.deliveryTime), deliveryCount: p.deliveryCount }));
196
+ }
197
+ /** XCLAIM: reassign pending entries idle >= minIdle to `consumer`. Returns claimed entries. */
198
+ claim(name, consumer, minIdle, ids, now) {
199
+ const group = this.requireGroup(name);
200
+ group.consumers.add(consumer);
201
+ const claimed = [];
202
+ for (const id of ids) {
203
+ const p = group.pending.get(id);
204
+ if (!p) continue;
205
+ if (now - p.deliveryTime < minIdle) continue;
206
+ const entry = this.entries.find((e) => e.id === id);
207
+ if (!entry) {
208
+ group.pending.delete(id);
209
+ continue;
210
+ }
211
+ group.pending.set(id, { consumer, deliveryTime: now, deliveryCount: p.deliveryCount + 1 });
212
+ claimed.push(entry);
213
+ }
214
+ return claimed;
215
+ }
216
+ /** Entries strictly greater than `id` (XREAD); `$` resolves to the current last id. */
217
+ readAfter(id, count) {
218
+ const after = id === "$" ? this.lastId : resolveRangeBound(id, true).id;
219
+ let out = this.entries.filter((e) => compareIds(e.id, after) > 0);
220
+ if (count !== void 0) out = out.slice(0, count);
221
+ return out;
222
+ }
223
+ };
224
+
225
+ // src/memory/domain/store/memory-store.ts
226
+ var MemoryStore = class {
227
+ keyspace = /* @__PURE__ */ new Map();
228
+ /** Current logical time (epoch ms). Overridable for deterministic TTL tests. */
229
+ now() {
230
+ return Date.now();
231
+ }
232
+ /** Returns the live entry for a key, lazily evicting it if expired. */
233
+ live(key) {
234
+ const entry = this.keyspace.get(key);
235
+ if (!entry) return void 0;
236
+ if (entry.expireAt !== null && entry.expireAt <= this.now()) {
237
+ this.keyspace.delete(key);
238
+ return void 0;
239
+ }
240
+ return entry;
241
+ }
242
+ has(key) {
243
+ return this.live(key) !== void 0;
244
+ }
245
+ /** Deletes a key. Returns true if a live key was removed. */
246
+ delete(key) {
247
+ const existed = this.live(key) !== void 0;
248
+ this.keyspace.delete(key);
249
+ return existed;
250
+ }
251
+ /** Returns the Redis type name, or 'none' if missing. */
252
+ type(key) {
253
+ return this.live(key)?.data.kind ?? "none";
254
+ }
255
+ flush() {
256
+ this.keyspace.clear();
257
+ }
258
+ keys() {
259
+ const out = [];
260
+ for (const key of [...this.keyspace.keys()]) {
261
+ if (this.has(key)) out.push(key);
262
+ }
263
+ return out;
264
+ }
265
+ // --- expiry ---------------------------------------------------------------
266
+ /** Sets absolute expiry (epoch ms) or null to persist. Returns true if key exists. */
267
+ setExpireAt(key, atMs) {
268
+ const entry = this.live(key);
269
+ if (!entry) return false;
270
+ entry.expireAt = atMs;
271
+ return true;
272
+ }
273
+ /** Remaining TTL in ms: -2 missing, -1 no expiry, else >= 0. */
274
+ pttl(key) {
275
+ const entry = this.live(key);
276
+ if (!entry) return -2;
277
+ if (entry.expireAt === null) return -1;
278
+ return Math.max(0, entry.expireAt - this.now());
279
+ }
280
+ // --- typed access ---------------------------------------------------------
281
+ /** Reads a container of the expected kind; undefined if missing; throws on type mismatch. */
282
+ read(key, kind) {
283
+ const entry = this.live(key);
284
+ if (!entry) return void 0;
285
+ if (entry.data.kind !== kind) throw new WrongTypeError();
286
+ return entry.data.value;
287
+ }
288
+ /** Gets the existing container of `kind`, creating an empty one (no expiry) if absent. Throws on type mismatch. */
289
+ readOrCreate(key, kind) {
290
+ const existing = this.read(key, kind);
291
+ if (existing !== void 0) return existing;
292
+ const value = this.emptyContainer(kind);
293
+ this.keyspace.set(key, { data: { kind, value }, expireAt: null });
294
+ return value;
295
+ }
296
+ /** Overwrites a key with a string value, clearing any previous type and expiry. */
297
+ writeString(key, value) {
298
+ this.keyspace.set(key, { data: { kind: "string", value }, expireAt: null });
299
+ }
300
+ emptyContainer(kind) {
301
+ switch (kind) {
302
+ case "string":
303
+ return "";
304
+ case "hash":
305
+ return /* @__PURE__ */ new Map();
306
+ case "set":
307
+ return /* @__PURE__ */ new Set();
308
+ case "zset":
309
+ return /* @__PURE__ */ new Map();
310
+ case "list":
311
+ return [];
312
+ case "stream":
313
+ return new StreamValue();
314
+ }
315
+ }
316
+ };
317
+
318
+ // src/memory/domain/lua/lua-lexer.ts
319
+ var KEYWORDS = /* @__PURE__ */ new Set(["local", "if", "then", "elseif", "else", "end", "for", "do", "return", "and", "or", "not", "nil", "true", "false"]);
320
+ var MULTI_OPS = ["==", "~=", "<=", ">=", ".."];
321
+ var SINGLE_OPS = /* @__PURE__ */ new Set(["+", "-", "*", "/", "%", "<", ">", "=", "#", "(", ")", "[", "]", "{", "}", ",", "."]);
322
+ function tokenize(src) {
323
+ const tokens = [];
324
+ let i = 0;
325
+ const n = src.length;
326
+ const isDigit = (c) => c >= "0" && c <= "9";
327
+ const isNameStart = (c) => c === "_" || c >= "a" && c <= "z" || c >= "A" && c <= "Z";
328
+ const isNamePart = (c) => isNameStart(c) || isDigit(c);
329
+ while (i < n) {
330
+ const c = src[i];
331
+ if (c === " " || c === " " || c === "\r" || c === "\n") {
332
+ i++;
333
+ continue;
334
+ }
335
+ if (c === "-" && src[i + 1] === "-") {
336
+ i += 2;
337
+ while (i < n && src[i] !== "\n") i++;
338
+ continue;
339
+ }
340
+ if (c === '"' || c === "'") {
341
+ const quote = c;
342
+ i++;
343
+ let str = "";
344
+ while (i < n && src[i] !== quote) {
345
+ if (src[i] === "\\") {
346
+ const next = src[i + 1];
347
+ str += next === "n" ? "\n" : next === "t" ? " " : next ?? "";
348
+ i += 2;
349
+ } else {
350
+ str += src[i];
351
+ i++;
352
+ }
353
+ }
354
+ if (i >= n) throw new LuaExecutionError("unterminated string literal");
355
+ i++;
356
+ tokens.push({ type: "string", value: str });
357
+ continue;
358
+ }
359
+ if (isDigit(c) || c === "." && isDigit(src[i + 1] ?? "")) {
360
+ let num = "";
361
+ while (i < n && (isDigit(src[i]) || src[i] === ".")) {
362
+ num += src[i];
363
+ i++;
364
+ }
365
+ tokens.push({ type: "number", value: num });
366
+ continue;
367
+ }
368
+ if (isNameStart(c)) {
369
+ let name = "";
370
+ while (i < n && isNamePart(src[i])) {
371
+ name += src[i];
372
+ i++;
373
+ }
374
+ tokens.push({ type: KEYWORDS.has(name) ? "keyword" : "name", value: name });
375
+ continue;
376
+ }
377
+ const two = src.slice(i, i + 2);
378
+ if (MULTI_OPS.includes(two)) {
379
+ tokens.push({ type: "op", value: two });
380
+ i += 2;
381
+ continue;
382
+ }
383
+ if (SINGLE_OPS.has(c)) {
384
+ tokens.push({ type: "op", value: c });
385
+ i++;
386
+ continue;
387
+ }
388
+ throw new LuaExecutionError(`unexpected character '${c}'`);
389
+ }
390
+ tokens.push({ type: "eof", value: "" });
391
+ return tokens;
392
+ }
393
+
394
+ // src/memory/domain/lua/lua-parser.ts
395
+ var BINARY_PRECEDENCE = {
396
+ or: 1,
397
+ and: 2,
398
+ "==": 3,
399
+ "~=": 3,
400
+ "<": 3,
401
+ "<=": 3,
402
+ ">": 3,
403
+ ">=": 3,
404
+ "..": 4,
405
+ "+": 5,
406
+ "-": 5,
407
+ "*": 6,
408
+ "/": 6,
409
+ "%": 6
410
+ };
411
+ var BLOCK_TERMINATORS = /* @__PURE__ */ new Set(["end", "else", "elseif"]);
412
+ function parse(src) {
413
+ const tokens = tokenize(src);
414
+ let pos = 0;
415
+ const peek = () => tokens[pos];
416
+ const next = () => tokens[pos++];
417
+ const isKeyword = (kw) => peek().type === "keyword" && peek().value === kw;
418
+ const isOp = (op) => peek().type === "op" && peek().value === op;
419
+ const expectOp = (op) => {
420
+ if (!isOp(op)) throw new LuaExecutionError(`expected '${op}' but got '${peek().value}'`);
421
+ pos++;
422
+ };
423
+ const expectKeyword = (kw) => {
424
+ if (!isKeyword(kw)) throw new LuaExecutionError(`expected '${kw}' but got '${peek().value}'`);
425
+ pos++;
426
+ };
427
+ function parseBlock() {
428
+ const stmts = [];
429
+ while (peek().type !== "eof" && !(peek().type === "keyword" && BLOCK_TERMINATORS.has(peek().value))) {
430
+ stmts.push(parseStatement());
431
+ }
432
+ return stmts;
433
+ }
434
+ function parseStatement() {
435
+ if (isKeyword("local")) {
436
+ next();
437
+ const name = next();
438
+ if (name.type !== "name") throw new LuaExecutionError(`expected name after 'local'`);
439
+ let expr;
440
+ if (isOp("=")) {
441
+ next();
442
+ expr = parseExpr();
443
+ }
444
+ return { kind: "local", name: name.value, expr };
445
+ }
446
+ if (isKeyword("if")) return parseIf();
447
+ if (isKeyword("for")) return parseFor();
448
+ if (isKeyword("return")) {
449
+ next();
450
+ if (peek().type === "eof" || peek().type === "keyword" && BLOCK_TERMINATORS.has(peek().value)) {
451
+ return { kind: "return" };
452
+ }
453
+ return { kind: "return", expr: parseExpr() };
454
+ }
455
+ const prefix = parseExpr();
456
+ if (isOp("=")) {
457
+ next();
458
+ const value = parseExpr();
459
+ if (prefix.kind !== "name" && prefix.kind !== "index") {
460
+ throw new LuaExecutionError("invalid assignment target");
461
+ }
462
+ return { kind: "assign", target: prefix, expr: value };
463
+ }
464
+ return { kind: "exprStmt", expr: prefix };
465
+ }
466
+ function parseIf() {
467
+ expectKeyword("if");
468
+ const clauses = [];
469
+ const cond = parseExpr();
470
+ expectKeyword("then");
471
+ clauses.push({ cond, body: parseBlock() });
472
+ while (isKeyword("elseif")) {
473
+ next();
474
+ const c = parseExpr();
475
+ expectKeyword("then");
476
+ clauses.push({ cond: c, body: parseBlock() });
477
+ }
478
+ let elseBody;
479
+ if (isKeyword("else")) {
480
+ next();
481
+ elseBody = parseBlock();
482
+ }
483
+ expectKeyword("end");
484
+ return { kind: "if", clauses, elseBody };
485
+ }
486
+ function parseFor() {
487
+ expectKeyword("for");
488
+ const varTok = next();
489
+ if (varTok.type !== "name") throw new LuaExecutionError(`expected loop variable name`);
490
+ expectOp("=");
491
+ const from = parseExpr();
492
+ expectOp(",");
493
+ const to = parseExpr();
494
+ let step;
495
+ if (isOp(",")) {
496
+ next();
497
+ step = parseExpr();
498
+ }
499
+ expectKeyword("do");
500
+ const body = parseBlock();
501
+ expectKeyword("end");
502
+ return { kind: "for", varName: varTok.value, from, to, step, body };
503
+ }
504
+ function parseExpr(minPrec = 0) {
505
+ let left = parseUnary();
506
+ for (; ; ) {
507
+ const t = peek();
508
+ const opName = t.type === "keyword" && (t.value === "and" || t.value === "or") ? t.value : t.type === "op" ? t.value : void 0;
509
+ if (opName === void 0) break;
510
+ const prec = BINARY_PRECEDENCE[opName];
511
+ if (prec === void 0 || prec < minPrec) break;
512
+ next();
513
+ const nextMin = opName === ".." ? prec : prec + 1;
514
+ const right = parseExpr(nextMin);
515
+ left = { kind: "binop", op: opName, left, right };
516
+ }
517
+ return left;
518
+ }
519
+ function parseUnary() {
520
+ if (isKeyword("not")) {
521
+ next();
522
+ return { kind: "unop", op: "not", expr: parseUnary() };
523
+ }
524
+ if (isOp("-")) {
525
+ next();
526
+ return { kind: "unop", op: "-", expr: parseUnary() };
527
+ }
528
+ if (isOp("#")) {
529
+ next();
530
+ return { kind: "unop", op: "#", expr: parseUnary() };
531
+ }
532
+ return parsePostfix();
533
+ }
534
+ function parsePostfix() {
535
+ let expr = parsePrimary();
536
+ for (; ; ) {
537
+ if (isOp(".")) {
538
+ next();
539
+ const name = next();
540
+ if (name.type !== "name" && name.type !== "keyword") throw new LuaExecutionError("expected field name after .");
541
+ expr = { kind: "index", obj: expr, key: { kind: "str", value: name.value } };
542
+ } else if (isOp("[")) {
543
+ next();
544
+ const key = parseExpr();
545
+ expectOp("]");
546
+ expr = { kind: "index", obj: expr, key };
547
+ } else if (isOp("(")) {
548
+ next();
549
+ const args = [];
550
+ if (!isOp(")")) {
551
+ args.push(parseExpr());
552
+ while (isOp(",")) {
553
+ next();
554
+ args.push(parseExpr());
555
+ }
556
+ }
557
+ expectOp(")");
558
+ expr = { kind: "call", callee: expr, args };
559
+ } else {
560
+ break;
561
+ }
562
+ }
563
+ return expr;
564
+ }
565
+ function parsePrimary() {
566
+ const t = peek();
567
+ if (t.type === "number") {
568
+ next();
569
+ return { kind: "num", value: Number(t.value) };
570
+ }
571
+ if (t.type === "string") {
572
+ next();
573
+ return { kind: "str", value: t.value };
574
+ }
575
+ if (t.type === "keyword") {
576
+ if (t.value === "nil") {
577
+ next();
578
+ return { kind: "nil" };
579
+ }
580
+ if (t.value === "true" || t.value === "false") {
581
+ next();
582
+ return { kind: "bool", value: t.value === "true" };
583
+ }
584
+ }
585
+ if (t.type === "name") {
586
+ next();
587
+ return { kind: "name", name: t.value };
588
+ }
589
+ if (isOp("(")) {
590
+ next();
591
+ const e = parseExpr();
592
+ expectOp(")");
593
+ return e;
594
+ }
595
+ if (isOp("{")) {
596
+ next();
597
+ const items = [];
598
+ if (!isOp("}")) {
599
+ items.push(parseExpr());
600
+ while (isOp(",")) {
601
+ next();
602
+ if (isOp("}")) break;
603
+ items.push(parseExpr());
604
+ }
605
+ }
606
+ expectOp("}");
607
+ return { kind: "table", items };
608
+ }
609
+ throw new LuaExecutionError(`unexpected token '${t.value}'`);
610
+ }
611
+ const block = parseBlock();
612
+ if (peek().type !== "eof") throw new LuaExecutionError(`unexpected trailing token '${peek().value}'`);
613
+ return block;
614
+ }
615
+
616
+ // src/memory/domain/lua/lua-interpreter.ts
617
+ var LuaTable = class {
618
+ map = /* @__PURE__ */ new Map();
619
+ get(key) {
620
+ return this.map.has(key) ? this.map.get(key) : null;
621
+ }
622
+ set(key, value) {
623
+ if (value === null) this.map.delete(key);
624
+ else this.map.set(key, value);
625
+ }
626
+ /** Lua `#t`: length of the array part (largest n with 1..n present). */
627
+ length() {
628
+ let n = 0;
629
+ while (this.map.has(n + 1)) n++;
630
+ return n;
631
+ }
632
+ };
633
+ function truthy(v) {
634
+ return v !== null && v !== false;
635
+ }
636
+ function luaToString(v) {
637
+ if (v === null) return "nil";
638
+ if (typeof v === "boolean") return v ? "true" : "false";
639
+ if (typeof v === "number") return Number.isInteger(v) ? String(v) : String(v);
640
+ if (typeof v === "string") return v;
641
+ throw new LuaExecutionError("cannot convert table to string");
642
+ }
643
+ function toNumber(v) {
644
+ if (typeof v === "number") return v;
645
+ if (typeof v === "string") {
646
+ const n = Number(v);
647
+ if (Number.isNaN(n)) throw new LuaExecutionError(`cannot convert '${v}' to number`);
648
+ return n;
649
+ }
650
+ throw new LuaExecutionError("arithmetic on non-number");
651
+ }
652
+ var LuaInterpreter = class {
653
+ /**
654
+ * Runs a Redis Lua script.
655
+ *
656
+ * @param source - Lua source
657
+ * @param keys - KEYS array (strings)
658
+ * @param argv - ARGV array (strings)
659
+ * @param redisCall - bridge to execute redis.call(cmd, ...args)
660
+ * @returns the script's return value converted to a Redis reply
661
+ */
662
+ run(source, keys, argv, redisCall) {
663
+ const program = parse(source);
664
+ const env = /* @__PURE__ */ new Map();
665
+ env.set("KEYS", this.arrayTable(keys));
666
+ env.set("ARGV", this.arrayTable(argv));
667
+ const result = this.execBlock(program, env, redisCall);
668
+ return result ? this.luaToRedis(result.value) : null;
669
+ }
670
+ arrayTable(items) {
671
+ const t = new LuaTable();
672
+ items.forEach((item, idx) => t.set(idx + 1, item));
673
+ return t;
674
+ }
675
+ execBlock(stmts, env, redisCall) {
676
+ for (const stmt of stmts) {
677
+ const sig = this.execStmt(stmt, env, redisCall);
678
+ if (sig) return sig;
679
+ }
680
+ return void 0;
681
+ }
682
+ execStmt(stmt, env, redisCall) {
683
+ switch (stmt.kind) {
684
+ case "local":
685
+ env.set(stmt.name, stmt.expr ? this.evalExpr(stmt.expr, env, redisCall) : null);
686
+ return void 0;
687
+ case "assign": {
688
+ const value = this.evalExpr(stmt.expr, env, redisCall);
689
+ if (stmt.target.kind === "name") {
690
+ env.set(stmt.target.name, value);
691
+ } else if (stmt.target.kind === "index") {
692
+ const obj = this.evalExpr(stmt.target.obj, env, redisCall);
693
+ if (!(obj instanceof LuaTable)) throw new LuaExecutionError("index assignment on non-table");
694
+ const key = this.normalizeKey(this.evalExpr(stmt.target.key, env, redisCall));
695
+ obj.set(key, value);
696
+ } else {
697
+ throw new LuaExecutionError("invalid assignment target");
698
+ }
699
+ return void 0;
700
+ }
701
+ case "if": {
702
+ for (const clause of stmt.clauses) {
703
+ if (truthy(this.evalExpr(clause.cond, env, redisCall))) {
704
+ return this.execBlock(clause.body, env, redisCall);
705
+ }
706
+ }
707
+ if (stmt.elseBody) return this.execBlock(stmt.elseBody, env, redisCall);
708
+ return void 0;
709
+ }
710
+ case "for": {
711
+ const from = toNumber(this.evalExpr(stmt.from, env, redisCall));
712
+ const to = toNumber(this.evalExpr(stmt.to, env, redisCall));
713
+ const step = stmt.step ? toNumber(this.evalExpr(stmt.step, env, redisCall)) : 1;
714
+ if (step === 0) throw new LuaExecutionError("'for' step is zero");
715
+ const had = env.has(stmt.varName);
716
+ const prev = env.get(stmt.varName);
717
+ for (let i = from; step > 0 ? i <= to : i >= to; i += step) {
718
+ env.set(stmt.varName, i);
719
+ const sig = this.execBlock(stmt.body, env, redisCall);
720
+ if (sig) {
721
+ this.restore(env, stmt.varName, had, prev);
722
+ return sig;
723
+ }
724
+ }
725
+ this.restore(env, stmt.varName, had, prev);
726
+ return void 0;
727
+ }
728
+ case "return":
729
+ return { returned: true, value: stmt.expr ? this.evalExpr(stmt.expr, env, redisCall) : null };
730
+ case "exprStmt":
731
+ this.evalExpr(stmt.expr, env, redisCall);
732
+ return void 0;
733
+ }
734
+ }
735
+ restore(env, name, had, prev) {
736
+ if (had) env.set(name, prev);
737
+ else env.delete(name);
738
+ }
739
+ evalExpr(expr, env, redisCall) {
740
+ switch (expr.kind) {
741
+ case "num":
742
+ return expr.value;
743
+ case "str":
744
+ return expr.value;
745
+ case "bool":
746
+ return expr.value;
747
+ case "nil":
748
+ return null;
749
+ case "name":
750
+ return env.has(expr.name) ? env.get(expr.name) : null;
751
+ case "index": {
752
+ const obj = this.evalExpr(expr.obj, env, redisCall);
753
+ if (!(obj instanceof LuaTable)) {
754
+ throw new LuaExecutionError("attempt to index a non-table value");
755
+ }
756
+ return obj.get(this.normalizeKey(this.evalExpr(expr.key, env, redisCall)));
757
+ }
758
+ case "table": {
759
+ const t = new LuaTable();
760
+ expr.items.forEach((item, idx) => t.set(idx + 1, this.evalExpr(item, env, redisCall)));
761
+ return t;
762
+ }
763
+ case "unop":
764
+ return this.evalUnop(expr.op, this.evalExpr(expr.expr, env, redisCall));
765
+ case "binop":
766
+ return this.evalBinop(expr, env, redisCall);
767
+ case "call":
768
+ return this.evalCall(expr, env, redisCall);
769
+ }
770
+ }
771
+ normalizeKey(key) {
772
+ if (typeof key === "number") return key;
773
+ if (typeof key === "string") return key;
774
+ throw new LuaExecutionError("invalid table key");
775
+ }
776
+ evalUnop(op, v) {
777
+ switch (op) {
778
+ case "not":
779
+ return !truthy(v);
780
+ case "-":
781
+ return -toNumber(v);
782
+ case "#":
783
+ if (typeof v === "string") return v.length;
784
+ if (v instanceof LuaTable) return v.length();
785
+ throw new LuaExecutionError("attempt to get length of a non-string/table");
786
+ default:
787
+ throw new LuaExecutionError(`unsupported unary operator '${op}'`);
788
+ }
789
+ }
790
+ evalBinop(expr, env, redisCall) {
791
+ const { op } = expr;
792
+ if (op === "and") {
793
+ const left = this.evalExpr(expr.left, env, redisCall);
794
+ return truthy(left) ? this.evalExpr(expr.right, env, redisCall) : left;
795
+ }
796
+ if (op === "or") {
797
+ const left = this.evalExpr(expr.left, env, redisCall);
798
+ return truthy(left) ? left : this.evalExpr(expr.right, env, redisCall);
799
+ }
800
+ const l = this.evalExpr(expr.left, env, redisCall);
801
+ const r = this.evalExpr(expr.right, env, redisCall);
802
+ switch (op) {
803
+ case "..":
804
+ return luaToString(l) + luaToString(r);
805
+ case "==":
806
+ return this.luaEquals(l, r);
807
+ case "~=":
808
+ return !this.luaEquals(l, r);
809
+ case "<":
810
+ return this.compare(l, r) < 0;
811
+ case "<=":
812
+ return this.compare(l, r) <= 0;
813
+ case ">":
814
+ return this.compare(l, r) > 0;
815
+ case ">=":
816
+ return this.compare(l, r) >= 0;
817
+ case "+":
818
+ return toNumber(l) + toNumber(r);
819
+ case "-":
820
+ return toNumber(l) - toNumber(r);
821
+ case "*":
822
+ return toNumber(l) * toNumber(r);
823
+ case "/":
824
+ return toNumber(l) / toNumber(r);
825
+ case "%": {
826
+ const a = toNumber(l);
827
+ const b = toNumber(r);
828
+ return a - Math.floor(a / b) * b;
829
+ }
830
+ default:
831
+ throw new LuaExecutionError(`unsupported operator '${op}'`);
832
+ }
833
+ }
834
+ luaEquals(a, b) {
835
+ if (typeof a !== typeof b) return false;
836
+ return a === b;
837
+ }
838
+ compare(a, b) {
839
+ if (typeof a === "number" && typeof b === "number") return a - b;
840
+ if (typeof a === "string" && typeof b === "string") return a < b ? -1 : a > b ? 1 : 0;
841
+ throw new LuaExecutionError("attempt to compare incompatible values");
842
+ }
843
+ evalCall(expr, env, redisCall) {
844
+ const callee = expr.callee;
845
+ const args = expr.args.map((a) => this.evalExpr(a, env, redisCall));
846
+ if (callee.kind === "index" && callee.obj.kind === "name" && callee.obj.name === "redis" && callee.key.kind === "str") {
847
+ if (callee.key.value === "call" || callee.key.value === "pcall") {
848
+ return this.doRedisCall(args, redisCall);
849
+ }
850
+ throw new LuaExecutionError(`unsupported redis.${callee.key.value}()`);
851
+ }
852
+ if (callee.kind === "index" && callee.obj.kind === "name" && callee.obj.name === "math" && callee.key.kind === "str") {
853
+ return this.doMath(callee.key.value, args);
854
+ }
855
+ if (callee.kind === "name" && callee.name === "tonumber") {
856
+ const v = args[0] ?? null;
857
+ if (typeof v === "number") return v;
858
+ if (typeof v === "string") {
859
+ const n = Number(v);
860
+ return Number.isNaN(n) ? null : n;
861
+ }
862
+ return null;
863
+ }
864
+ if (callee.kind === "name" && callee.name === "tostring") {
865
+ return luaToString(args[0] ?? null);
866
+ }
867
+ throw new LuaExecutionError("unsupported function call");
868
+ }
869
+ doMath(fn, args) {
870
+ const nums = args.map(toNumber);
871
+ switch (fn) {
872
+ case "floor":
873
+ return Math.floor(nums[0]);
874
+ case "ceil":
875
+ return Math.ceil(nums[0]);
876
+ case "abs":
877
+ return Math.abs(nums[0]);
878
+ case "min":
879
+ return Math.min(...nums);
880
+ case "max":
881
+ return Math.max(...nums);
882
+ default:
883
+ throw new LuaExecutionError(`unsupported math.${fn}()`);
884
+ }
885
+ }
886
+ doRedisCall(args, redisCall) {
887
+ if (args.length === 0) throw new LuaExecutionError("redis.call requires a command");
888
+ const command = luaToString(args[0]);
889
+ const callArgs = args.slice(1).map((a) => {
890
+ if (typeof a === "number") return a;
891
+ if (typeof a === "string") return a;
892
+ throw new LuaExecutionError("redis.call argument must be string or number");
893
+ });
894
+ return this.redisToLua(redisCall(command, callArgs));
895
+ }
896
+ /** Converts a Redis reply (from the executor) into a Lua value. */
897
+ redisToLua(reply) {
898
+ if (reply === null || reply === void 0) return null;
899
+ if (typeof reply === "number") return reply;
900
+ if (typeof reply === "string") return reply;
901
+ if (typeof reply === "boolean") return reply ? 1 : null;
902
+ if (Array.isArray(reply)) {
903
+ const t = new LuaTable();
904
+ reply.forEach((el, idx) => t.set(idx + 1, this.redisToLua(el)));
905
+ return t;
906
+ }
907
+ if (typeof reply === "object") {
908
+ const t = new LuaTable();
909
+ let idx = 1;
910
+ for (const [field, value] of Object.entries(reply)) {
911
+ t.set(idx++, field);
912
+ t.set(idx++, this.redisToLua(value));
913
+ }
914
+ return t;
915
+ }
916
+ throw new LuaExecutionError("unsupported redis reply type from in-memory executor");
917
+ }
918
+ /** Converts a Lua return value into a Redis reply. */
919
+ luaToRedis(v) {
920
+ if (v === null) return null;
921
+ if (typeof v === "boolean") return v ? 1 : null;
922
+ if (typeof v === "number") return Math.floor(v);
923
+ if (typeof v === "string") return v;
924
+ const out = [];
925
+ const len = v.length();
926
+ for (let i = 1; i <= len; i++) {
927
+ out.push(this.luaToRedis(v.get(i)));
928
+ }
929
+ return out;
930
+ }
931
+ };
932
+
933
+ // src/memory/application/services/command-executor.service.ts
934
+ var OK = "OK";
935
+ function globToRegExp(glob) {
936
+ let re = "^";
937
+ for (const ch of glob) {
938
+ if (ch === "*") re += ".*";
939
+ else if (ch === "?") re += ".";
940
+ else if (".+^${}()|\\/[".includes(ch)) re += `\\${ch}`;
941
+ else re += ch;
942
+ }
943
+ return new RegExp(`${re}$`);
944
+ }
945
+ function parseScoreBound(raw) {
946
+ let str = String(raw);
947
+ let exclusive = false;
948
+ if (str.startsWith("(")) {
949
+ exclusive = true;
950
+ str = str.slice(1);
951
+ }
952
+ if (str === "-inf") return { value: -Infinity, exclusive };
953
+ if (str === "+inf" || str === "inf") return { value: Infinity, exclusive };
954
+ return { value: Number(str), exclusive };
955
+ }
956
+ var CommandExecutor = class {
957
+ constructor(store, interpreter = new LuaInterpreter()) {
958
+ this.store = store;
959
+ this.interpreter = interpreter;
960
+ }
961
+ scripts = /* @__PURE__ */ new Map();
962
+ redisCall = (command, args) => this.execute(command, args);
963
+ execute(command, args) {
964
+ const cmd = command.toUpperCase();
965
+ switch (cmd) {
966
+ // --- connection / server ---
967
+ case "PING":
968
+ return args.length ? this.str(args[0]) : "PONG";
969
+ case "DBSIZE":
970
+ return this.store.keys().length;
971
+ case "FLUSHDB":
972
+ case "FLUSHALL":
973
+ this.store.flush();
974
+ return OK;
975
+ // --- strings ---
976
+ case "GET":
977
+ return this.store.read(this.str(args[0]), "string") ?? null;
978
+ case "SET":
979
+ return this.cmdSet(args);
980
+ case "SETEX":
981
+ return this.cmdSet([args[0], args[2], "EX", args[1]]);
982
+ case "SETNX":
983
+ return this.cmdSet([args[0], args[1], "NX"]) === OK ? 1 : 0;
984
+ case "DEL":
985
+ case "UNLINK":
986
+ return args.reduce((acc, k) => acc + (this.store.delete(this.str(k)) ? 1 : 0), 0);
987
+ case "EXISTS":
988
+ return args.reduce((acc, k) => acc + (this.store.has(this.str(k)) ? 1 : 0), 0);
989
+ case "INCR":
990
+ return this.incrBy(this.str(args[0]), 1);
991
+ case "INCRBY":
992
+ return this.incrBy(this.str(args[0]), this.num(args[1]));
993
+ case "DECR":
994
+ return this.incrBy(this.str(args[0]), -1);
995
+ case "DECRBY":
996
+ return this.incrBy(this.str(args[0]), -this.num(args[1]));
997
+ case "APPEND": {
998
+ const key = this.str(args[0]);
999
+ const cur = this.store.read(key, "string") ?? "";
1000
+ const next = cur + this.str(args[1]);
1001
+ this.preserveTtl(key, () => this.store.writeString(key, next));
1002
+ return next.length;
1003
+ }
1004
+ case "STRLEN":
1005
+ return (this.store.read(this.str(args[0]), "string") ?? "").length;
1006
+ case "MGET":
1007
+ return args.map((k) => this.store.read(this.str(k), "string") ?? null);
1008
+ case "MSET": {
1009
+ for (let i = 0; i < args.length; i += 2) this.store.writeString(this.str(args[i]), this.str(args[i + 1]));
1010
+ return OK;
1011
+ }
1012
+ // --- keys / TTL ---
1013
+ case "EXPIRE":
1014
+ return this.store.setExpireAt(this.str(args[0]), this.store.now() + this.num(args[1]) * 1e3) ? 1 : 0;
1015
+ case "PEXPIRE":
1016
+ return this.store.setExpireAt(this.str(args[0]), this.store.now() + this.num(args[1])) ? 1 : 0;
1017
+ case "EXPIREAT":
1018
+ return this.store.setExpireAt(this.str(args[0]), this.num(args[1]) * 1e3) ? 1 : 0;
1019
+ case "PEXPIREAT":
1020
+ return this.store.setExpireAt(this.str(args[0]), this.num(args[1])) ? 1 : 0;
1021
+ case "PERSIST":
1022
+ return this.store.setExpireAt(this.str(args[0]), null) ? 1 : 0;
1023
+ case "TTL": {
1024
+ const ms = this.store.pttl(this.str(args[0]));
1025
+ return ms < 0 ? ms : Math.ceil(ms / 1e3);
1026
+ }
1027
+ case "PTTL":
1028
+ return this.store.pttl(this.str(args[0]));
1029
+ case "TYPE":
1030
+ return this.store.type(this.str(args[0]));
1031
+ case "KEYS": {
1032
+ const re = globToRegExp(this.str(args[0]));
1033
+ return this.store.keys().filter((k) => re.test(k));
1034
+ }
1035
+ case "SCAN":
1036
+ return this.cmdScan(args);
1037
+ // --- hashes ---
1038
+ case "HSET":
1039
+ case "HMSET":
1040
+ return this.cmdHset(cmd, args);
1041
+ case "HGET": {
1042
+ const hash = this.store.read(this.str(args[0]), "hash");
1043
+ return hash?.get(this.str(args[1])) ?? null;
1044
+ }
1045
+ case "HMGET": {
1046
+ const hash = this.store.read(this.str(args[0]), "hash");
1047
+ return args.slice(1).map((f) => hash?.get(this.str(f)) ?? null);
1048
+ }
1049
+ case "HGETALL": {
1050
+ const hash = this.store.read(this.str(args[0]), "hash");
1051
+ const obj = {};
1052
+ if (hash) for (const [f, v] of hash) obj[f] = v;
1053
+ return obj;
1054
+ }
1055
+ case "HDEL": {
1056
+ const key = this.str(args[0]);
1057
+ const hash = this.store.read(key, "hash");
1058
+ if (!hash) return 0;
1059
+ let removed = 0;
1060
+ for (const f of args.slice(1)) if (hash.delete(this.str(f))) removed++;
1061
+ if (hash.size === 0) this.store.delete(key);
1062
+ return removed;
1063
+ }
1064
+ case "HEXISTS":
1065
+ return this.store.read(this.str(args[0]), "hash")?.has(this.str(args[1])) ? 1 : 0;
1066
+ case "HLEN":
1067
+ return this.store.read(this.str(args[0]), "hash")?.size ?? 0;
1068
+ case "HKEYS":
1069
+ return [...this.store.read(this.str(args[0]), "hash")?.keys() ?? []];
1070
+ case "HVALS":
1071
+ return [...this.store.read(this.str(args[0]), "hash")?.values() ?? []];
1072
+ case "HINCRBY": {
1073
+ const key = this.str(args[0]);
1074
+ const field = this.str(args[1]);
1075
+ const hash = this.store.readOrCreate(key, "hash");
1076
+ const next = Number(hash.get(field) ?? "0") + this.num(args[2]);
1077
+ hash.set(field, String(next));
1078
+ return next;
1079
+ }
1080
+ // --- lists ---
1081
+ case "LPUSH": {
1082
+ const list = this.store.readOrCreate(this.str(args[0]), "list");
1083
+ for (const v of args.slice(1)) list.unshift(this.str(v));
1084
+ return list.length;
1085
+ }
1086
+ case "RPUSH": {
1087
+ const list = this.store.readOrCreate(this.str(args[0]), "list");
1088
+ for (const v of args.slice(1)) list.push(this.str(v));
1089
+ return list.length;
1090
+ }
1091
+ case "LPOP":
1092
+ case "RPOP":
1093
+ return this.cmdPop(cmd, args);
1094
+ case "LLEN":
1095
+ return this.store.read(this.str(args[0]), "list")?.length ?? 0;
1096
+ case "LRANGE":
1097
+ return this.cmdLrange(args);
1098
+ case "LINDEX": {
1099
+ const list = this.store.read(this.str(args[0]), "list");
1100
+ if (!list) return null;
1101
+ let idx = this.num(args[1]);
1102
+ if (idx < 0) idx = list.length + idx;
1103
+ return list[idx] ?? null;
1104
+ }
1105
+ case "LREM": {
1106
+ const key = this.str(args[0]);
1107
+ const list = this.store.read(key, "list");
1108
+ if (!list) return 0;
1109
+ const target = this.str(args[2]);
1110
+ const before = list.length;
1111
+ const kept = list.filter((v) => v !== target);
1112
+ list.length = 0;
1113
+ list.push(...kept);
1114
+ if (list.length === 0) this.store.delete(key);
1115
+ return before - list.length;
1116
+ }
1117
+ // --- sets ---
1118
+ case "SADD": {
1119
+ const set = this.store.readOrCreate(this.str(args[0]), "set");
1120
+ let added = 0;
1121
+ for (const m of args.slice(1)) {
1122
+ const member = this.str(m);
1123
+ if (!set.has(member)) {
1124
+ set.add(member);
1125
+ added++;
1126
+ }
1127
+ }
1128
+ return added;
1129
+ }
1130
+ case "SREM": {
1131
+ const key = this.str(args[0]);
1132
+ const set = this.store.read(key, "set");
1133
+ if (!set) return 0;
1134
+ let removed = 0;
1135
+ for (const m of args.slice(1)) if (set.delete(this.str(m))) removed++;
1136
+ if (set.size === 0) this.store.delete(key);
1137
+ return removed;
1138
+ }
1139
+ case "SMEMBERS":
1140
+ return [...this.store.read(this.str(args[0]), "set") ?? []];
1141
+ case "SISMEMBER":
1142
+ return this.store.read(this.str(args[0]), "set")?.has(this.str(args[1])) ? 1 : 0;
1143
+ case "SCARD":
1144
+ return this.store.read(this.str(args[0]), "set")?.size ?? 0;
1145
+ // --- sorted sets ---
1146
+ case "ZADD":
1147
+ return this.cmdZadd(args);
1148
+ case "ZCARD":
1149
+ return this.store.read(this.str(args[0]), "zset")?.size ?? 0;
1150
+ case "ZSCORE": {
1151
+ const score = this.store.read(this.str(args[0]), "zset")?.get(this.str(args[1]));
1152
+ return score === void 0 ? null : String(score);
1153
+ }
1154
+ case "ZREM": {
1155
+ const key = this.str(args[0]);
1156
+ const zset = this.store.read(key, "zset");
1157
+ if (!zset) return 0;
1158
+ let removed = 0;
1159
+ for (const m of args.slice(1)) if (zset.delete(this.str(m))) removed++;
1160
+ if (zset.size === 0) this.store.delete(key);
1161
+ return removed;
1162
+ }
1163
+ case "ZRANGE":
1164
+ return this.cmdZrange(args);
1165
+ case "ZRANGEBYSCORE":
1166
+ return this.cmdZrangeByScore(args);
1167
+ case "ZREMRANGEBYSCORE":
1168
+ return this.cmdZremRangeByScore(args);
1169
+ case "ZCOUNT": {
1170
+ const zset = this.store.read(this.str(args[0]), "zset");
1171
+ if (!zset) return 0;
1172
+ const min = parseScoreBound(args[1]);
1173
+ const max = parseScoreBound(args[2]);
1174
+ return [...zset.values()].filter((s) => this.inRange(s, min, max)).length;
1175
+ }
1176
+ // --- streams ---
1177
+ case "XADD":
1178
+ return this.cmdXadd(args);
1179
+ case "XLEN":
1180
+ return this.store.read(this.str(args[0]), "stream")?.len() ?? 0;
1181
+ case "XRANGE":
1182
+ return this.cmdXrange(args, false);
1183
+ case "XREVRANGE":
1184
+ return this.cmdXrange(args, true);
1185
+ case "XDEL":
1186
+ return this.store.read(this.str(args[0]), "stream")?.del(args.slice(1).map((i) => this.str(i))) ?? 0;
1187
+ case "XTRIM":
1188
+ return this.cmdXtrim(args);
1189
+ case "XINFO":
1190
+ return this.cmdXinfo(args);
1191
+ case "XGROUP":
1192
+ return this.cmdXgroup(args);
1193
+ case "XREADGROUP":
1194
+ return this.cmdXreadgroup(args);
1195
+ case "XREAD":
1196
+ return this.cmdXread(args);
1197
+ case "XACK":
1198
+ return this.cmdXack(args);
1199
+ case "XPENDING":
1200
+ return this.cmdXpending(args);
1201
+ case "XCLAIM":
1202
+ return this.cmdXclaim(args);
1203
+ // --- scripting ---
1204
+ case "SCRIPT":
1205
+ if (this.str(args[0]).toUpperCase() === "LOAD") {
1206
+ const source = this.str(args[1]);
1207
+ const sha = createHash("sha1").update(source).digest("hex");
1208
+ this.scripts.set(sha, source);
1209
+ return sha;
1210
+ }
1211
+ return OK;
1212
+ case "EVAL":
1213
+ return this.runScript(this.str(args[0]), args);
1214
+ case "EVALSHA": {
1215
+ const sha = this.str(args[0]);
1216
+ const source = this.scripts.get(sha);
1217
+ if (!source) throw new MemoryDriverError("NOSCRIPT No matching script. Please use EVAL.");
1218
+ return this.runScript(source, args);
1219
+ }
1220
+ default:
1221
+ throw new MemoryDriverError(`In-memory driver does not implement command: ${cmd}`);
1222
+ }
1223
+ }
1224
+ // --- helpers --------------------------------------------------------------
1225
+ str(v) {
1226
+ return String(v);
1227
+ }
1228
+ num(v) {
1229
+ const n = Number(v);
1230
+ if (Number.isNaN(n)) throw new MemoryDriverError(`value is not an integer or out of range: ${String(v)}`);
1231
+ return n;
1232
+ }
1233
+ preserveTtl(key, mutate) {
1234
+ const ttl = this.store.pttl(key);
1235
+ mutate();
1236
+ if (ttl >= 0) this.store.setExpireAt(key, this.store.now() + ttl);
1237
+ }
1238
+ incrBy(key, by) {
1239
+ const cur = this.store.read(key, "string");
1240
+ const next = (cur === void 0 ? 0 : this.num(cur)) + by;
1241
+ this.preserveTtl(key, () => this.store.writeString(key, String(next)));
1242
+ return next;
1243
+ }
1244
+ cmdSet(args) {
1245
+ const key = this.str(args[0]);
1246
+ const value = this.str(args[1]);
1247
+ let nx = false;
1248
+ let xx = false;
1249
+ let ex;
1250
+ let px;
1251
+ let getOld = false;
1252
+ for (let i = 2; i < args.length; i++) {
1253
+ const opt = this.str(args[i]).toUpperCase();
1254
+ if (opt === "NX") nx = true;
1255
+ else if (opt === "XX") xx = true;
1256
+ else if (opt === "GET") getOld = true;
1257
+ else if (opt === "EX") ex = this.num(args[++i]);
1258
+ else if (opt === "PX") px = this.num(args[++i]);
1259
+ }
1260
+ const exists = this.store.has(key);
1261
+ const old = exists ? this.store.read(key, "string") ?? null : null;
1262
+ if (nx && exists || xx && !exists) return getOld ? old : null;
1263
+ this.store.writeString(key, value);
1264
+ if (ex !== void 0) this.store.setExpireAt(key, this.store.now() + ex * 1e3);
1265
+ else if (px !== void 0) this.store.setExpireAt(key, this.store.now() + px);
1266
+ return getOld ? old : OK;
1267
+ }
1268
+ cmdPop(cmd, args) {
1269
+ const key = this.str(args[0]);
1270
+ const list = this.store.read(key, "list");
1271
+ if (!list || list.length === 0) return args.length > 1 ? [] : null;
1272
+ const take = (fn) => fn();
1273
+ const pop = () => cmd === "LPOP" ? list.shift() : list.pop();
1274
+ if (args.length > 1) {
1275
+ const count = this.num(args[1]);
1276
+ const out = [];
1277
+ for (let i = 0; i < count && list.length > 0; i++) out.push(take(pop));
1278
+ if (list.length === 0) this.store.delete(key);
1279
+ return out;
1280
+ }
1281
+ const value = pop() ?? null;
1282
+ if (list.length === 0) this.store.delete(key);
1283
+ return value;
1284
+ }
1285
+ cmdLrange(args) {
1286
+ const list = this.store.read(this.str(args[0]), "list");
1287
+ if (!list) return [];
1288
+ const len = list.length;
1289
+ let start = this.num(args[1]);
1290
+ let stop = this.num(args[2]);
1291
+ if (start < 0) start = Math.max(0, len + start);
1292
+ if (stop < 0) stop = len + stop;
1293
+ return list.slice(start, stop + 1);
1294
+ }
1295
+ cmdScan(args) {
1296
+ let match;
1297
+ for (let i = 1; i < args.length; i++) {
1298
+ const opt = this.str(args[i]).toUpperCase();
1299
+ if (opt === "MATCH") match = this.str(args[++i]);
1300
+ else if (opt === "COUNT") i++;
1301
+ }
1302
+ const re = match ? globToRegExp(match) : void 0;
1303
+ const keys = re ? this.store.keys().filter((k) => re.test(k)) : this.store.keys();
1304
+ return ["0", keys];
1305
+ }
1306
+ cmdHset(cmd, args) {
1307
+ const hash = this.store.readOrCreate(this.str(args[0]), "hash");
1308
+ let added = 0;
1309
+ for (let i = 1; i < args.length; i += 2) {
1310
+ const field = this.str(args[i]);
1311
+ if (!hash.has(field)) added++;
1312
+ hash.set(field, this.str(args[i + 1]));
1313
+ }
1314
+ return cmd === "HMSET" ? OK : added;
1315
+ }
1316
+ cmdZadd(args) {
1317
+ const zset = this.store.readOrCreate(this.str(args[0]), "zset");
1318
+ let added = 0;
1319
+ for (let i = 1; i < args.length; i += 2) {
1320
+ const score = this.num(args[i]);
1321
+ const member = this.str(args[i + 1]);
1322
+ if (!zset.has(member)) added++;
1323
+ zset.set(member, score);
1324
+ }
1325
+ return added;
1326
+ }
1327
+ sortedMembers(key) {
1328
+ const zset = this.store.read(key, "zset");
1329
+ if (!zset) return [];
1330
+ return [...zset.entries()].sort((a, b) => a[1] !== b[1] ? a[1] - b[1] : a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0);
1331
+ }
1332
+ cmdZrange(args) {
1333
+ const sorted = this.sortedMembers(this.str(args[0]));
1334
+ let start = this.num(args[1]);
1335
+ let stop = this.num(args[2]);
1336
+ const withScores = args.slice(3).some((a) => this.str(a).toUpperCase() === "WITHSCORES");
1337
+ const len = sorted.length;
1338
+ if (start < 0) start = Math.max(0, len + start);
1339
+ if (stop < 0) stop = len + stop;
1340
+ const slice = sorted.slice(start, stop + 1);
1341
+ return this.flatten(slice, withScores);
1342
+ }
1343
+ cmdZrangeByScore(args) {
1344
+ const sorted = this.sortedMembers(this.str(args[0]));
1345
+ const min = parseScoreBound(args[1]);
1346
+ const max = parseScoreBound(args[2]);
1347
+ const withScores = args.slice(3).some((a) => this.str(a).toUpperCase() === "WITHSCORES");
1348
+ return this.flatten(
1349
+ sorted.filter(([, s]) => this.inRange(s, min, max)),
1350
+ withScores
1351
+ );
1352
+ }
1353
+ cmdZremRangeByScore(args) {
1354
+ const key = this.str(args[0]);
1355
+ const zset = this.store.read(key, "zset");
1356
+ if (!zset) return 0;
1357
+ const min = parseScoreBound(args[1]);
1358
+ const max = parseScoreBound(args[2]);
1359
+ let removed = 0;
1360
+ for (const [member, score] of [...zset.entries()]) {
1361
+ if (this.inRange(score, min, max)) {
1362
+ zset.delete(member);
1363
+ removed++;
1364
+ }
1365
+ }
1366
+ if (zset.size === 0) this.store.delete(key);
1367
+ return removed;
1368
+ }
1369
+ inRange(score, min, max) {
1370
+ const okMin = min.exclusive ? score > min.value : score >= min.value;
1371
+ const okMax = max.exclusive ? score < max.value : score <= max.value;
1372
+ return okMin && okMax;
1373
+ }
1374
+ flatten(entries, withScores) {
1375
+ const out = [];
1376
+ for (const [member, score] of entries) {
1377
+ out.push(member);
1378
+ if (withScores) out.push(String(score));
1379
+ }
1380
+ return out;
1381
+ }
1382
+ // --- stream helpers -------------------------------------------------------
1383
+ /** Formats a stream entry as the Redis reply shape `[id, [field, value, ...]]`. */
1384
+ entryReply(entry) {
1385
+ return [entry.id, entry.fields];
1386
+ }
1387
+ cmdXadd(args) {
1388
+ const key = this.str(args[0]);
1389
+ let i = 1;
1390
+ let noMkStream = false;
1391
+ let maxLen;
1392
+ let minId;
1393
+ for (; i < args.length; ) {
1394
+ const opt = this.str(args[i]).toUpperCase();
1395
+ if (opt === "NOMKSTREAM") {
1396
+ noMkStream = true;
1397
+ i += 1;
1398
+ } else if (opt === "MAXLEN") {
1399
+ i += 1;
1400
+ if (this.str(args[i]) === "~" || this.str(args[i]) === "=") i += 1;
1401
+ maxLen = this.num(args[i]);
1402
+ i += 1;
1403
+ } else if (opt === "MINID") {
1404
+ i += 1;
1405
+ if (this.str(args[i]) === "~" || this.str(args[i]) === "=") i += 1;
1406
+ minId = this.str(args[i]);
1407
+ i += 1;
1408
+ } else {
1409
+ break;
1410
+ }
1411
+ }
1412
+ const id = this.str(args[i]);
1413
+ const fields = args.slice(i + 1).map((f) => this.str(f));
1414
+ if (noMkStream && !this.store.has(key)) return null;
1415
+ const stream = this.store.readOrCreate(key, "stream");
1416
+ const newId = stream.add(id, fields, this.store.now());
1417
+ if (maxLen !== void 0) stream.trimMaxLen(maxLen);
1418
+ if (minId !== void 0) {
1419
+ const toDelete = stream.range("-", "+").filter((e) => compareIds(e.id, minId) < 0).map((e) => e.id);
1420
+ stream.del(toDelete);
1421
+ }
1422
+ return newId;
1423
+ }
1424
+ cmdXrange(args, reverse) {
1425
+ const stream = this.store.read(this.str(args[0]), "stream");
1426
+ if (!stream) return [];
1427
+ const start = reverse ? this.str(args[2]) : this.str(args[1]);
1428
+ const end = reverse ? this.str(args[1]) : this.str(args[2]);
1429
+ let count;
1430
+ for (let i = 3; i < args.length; i++) {
1431
+ if (this.str(args[i]).toUpperCase() === "COUNT") count = this.num(args[++i]);
1432
+ }
1433
+ return stream.range(start, end, count, reverse).map((e) => this.entryReply(e));
1434
+ }
1435
+ cmdXtrim(args) {
1436
+ const stream = this.store.read(this.str(args[0]), "stream");
1437
+ if (!stream) return 0;
1438
+ let i = 1;
1439
+ const strategy = this.str(args[i]).toUpperCase();
1440
+ i += 1;
1441
+ if (this.str(args[i]) === "~" || this.str(args[i]) === "=") i += 1;
1442
+ if (strategy !== "MAXLEN") throw new MemoryDriverError(`XTRIM strategy not supported: ${strategy}`);
1443
+ return stream.trimMaxLen(this.num(args[i]));
1444
+ }
1445
+ cmdXinfo(args) {
1446
+ const sub = this.str(args[0]).toUpperCase();
1447
+ if (sub !== "STREAM") throw new MemoryDriverError(`XINFO subcommand not supported: ${sub}`);
1448
+ const stream = this.store.read(this.str(args[1]), "stream");
1449
+ if (!stream) throw new MemoryDriverError("ERR no such key");
1450
+ const all = stream.range("-", "+");
1451
+ const first = all[0];
1452
+ const last = all[all.length - 1];
1453
+ return ["length", stream.len(), "radix-tree-keys", 1, "radix-tree-nodes", 2, "last-generated-id", stream.lastId, "groups", stream.groups.size, "first-entry", first ? this.entryReply(first) : null, "last-entry", last ? this.entryReply(last) : null];
1454
+ }
1455
+ cmdXgroup(args) {
1456
+ const sub = this.str(args[0]).toUpperCase();
1457
+ const key = this.str(args[1]);
1458
+ const group = this.str(args[2]);
1459
+ if (sub === "CREATE") {
1460
+ const id = this.str(args[3]);
1461
+ const mkstream = args[4] !== void 0 && this.str(args[4]).toUpperCase() === "MKSTREAM";
1462
+ if (!this.store.has(key)) {
1463
+ if (!mkstream) throw new MemoryDriverError("ERR The XGROUP subcommand requires the key to exist. Note that for CREATE you may want to use the MKSTREAM option to create an empty stream automatically.");
1464
+ this.store.readOrCreate(key, "stream");
1465
+ }
1466
+ this.store.read(key, "stream").createGroup(group, id);
1467
+ return OK;
1468
+ }
1469
+ if (sub === "DESTROY") return this.store.read(key, "stream")?.destroyGroup(group) ?? 0;
1470
+ if (sub === "DELCONSUMER") return this.store.read(key, "stream")?.delConsumer(group, this.str(args[3])) ?? 0;
1471
+ if (sub === "SETID") {
1472
+ this.store.read(key, "stream")?.setGroupId(group, this.str(args[3]));
1473
+ return OK;
1474
+ }
1475
+ throw new MemoryDriverError(`XGROUP subcommand not supported: ${sub}`);
1476
+ }
1477
+ /** Parses the trailing `STREAMS key... id...` section into key/id pairs. */
1478
+ parseStreamsSection(args, fromIndex) {
1479
+ let count;
1480
+ let i = fromIndex;
1481
+ for (; i < args.length; i++) {
1482
+ const tok = this.str(args[i]).toUpperCase();
1483
+ if (tok === "STREAMS") {
1484
+ i += 1;
1485
+ break;
1486
+ }
1487
+ if (tok === "COUNT") count = this.num(args[++i]);
1488
+ else if (tok === "BLOCK")
1489
+ i += 1;
1490
+ else if (tok === "NOACK") continue;
1491
+ }
1492
+ const rest = args.slice(i).map((a) => this.str(a));
1493
+ const half = rest.length / 2;
1494
+ const keys = rest.slice(0, half);
1495
+ const ids = rest.slice(half);
1496
+ return { count, pairs: keys.map((k, idx) => [k, ids[idx]]) };
1497
+ }
1498
+ cmdXreadgroup(args) {
1499
+ const group = this.str(args[1]);
1500
+ const consumer = this.str(args[2]);
1501
+ const noAck = args.slice(3).some((a) => this.str(a).toUpperCase() === "NOACK");
1502
+ const { count, pairs } = this.parseStreamsSection(args, 3);
1503
+ const now = this.store.now();
1504
+ const out = [];
1505
+ for (const [key, id] of pairs) {
1506
+ const stream = this.store.read(key, "stream");
1507
+ if (!stream) continue;
1508
+ const entries = stream.readGroup(group, consumer, id, now, count, noAck);
1509
+ if (entries.length > 0) out.push([key, entries.map((e) => this.entryReply(e))]);
1510
+ }
1511
+ return out.length > 0 ? out : null;
1512
+ }
1513
+ cmdXread(args) {
1514
+ const { count, pairs } = this.parseStreamsSection(args, 0);
1515
+ const out = [];
1516
+ for (const [key, id] of pairs) {
1517
+ const stream = this.store.read(key, "stream");
1518
+ if (!stream) continue;
1519
+ const entries = stream.readAfter(id, count);
1520
+ if (entries.length > 0) out.push([key, entries.map((e) => this.entryReply(e))]);
1521
+ }
1522
+ return out.length > 0 ? out : null;
1523
+ }
1524
+ cmdXack(args) {
1525
+ const stream = this.store.read(this.str(args[0]), "stream");
1526
+ const group = this.str(args[1]);
1527
+ if (!stream?.groups.has(group)) return 0;
1528
+ return stream.ack(
1529
+ group,
1530
+ args.slice(2).map((i) => this.str(i))
1531
+ );
1532
+ }
1533
+ cmdXpending(args) {
1534
+ const stream = this.store.read(this.str(args[0]), "stream");
1535
+ const group = this.str(args[1]);
1536
+ const now = this.store.now();
1537
+ if (args.length <= 2) {
1538
+ if (!stream?.groups.has(group)) return [0, null, null, null];
1539
+ const s = stream.pendingSummary(group);
1540
+ return [s.count, s.minId, s.maxId, s.consumers.length > 0 ? s.consumers.map(([name, c]) => [name, String(c)]) : null];
1541
+ }
1542
+ if (!stream?.groups.has(group)) return [];
1543
+ const start = this.str(args[2]);
1544
+ const end = this.str(args[3]);
1545
+ const count = this.num(args[4]);
1546
+ const consumer = args[5] !== void 0 ? this.str(args[5]) : void 0;
1547
+ return stream.pendingRange(group, start, end, count, now, consumer).map((p) => [p.id, p.consumer, p.idleTime, p.deliveryCount]);
1548
+ }
1549
+ cmdXclaim(args) {
1550
+ const stream = this.store.read(this.str(args[0]), "stream");
1551
+ const group = this.str(args[1]);
1552
+ if (!stream?.groups.has(group)) return [];
1553
+ const consumer = this.str(args[2]);
1554
+ const minIdle = this.num(args[3]);
1555
+ const ids = [];
1556
+ for (let i = 4; i < args.length; i++) {
1557
+ const tok = this.str(args[i]);
1558
+ if (["IDLE", "TIME", "RETRYCOUNT", "FORCE", "JUSTID", "LASTID"].includes(tok.toUpperCase())) break;
1559
+ ids.push(tok);
1560
+ }
1561
+ return stream.claim(group, consumer, minIdle, ids, this.store.now()).map((e) => this.entryReply(e));
1562
+ }
1563
+ runScript(source, args) {
1564
+ const numKeys = this.num(args[1]);
1565
+ const keys = args.slice(2, 2 + numKeys).map((k) => this.str(k));
1566
+ const argv = args.slice(2 + numKeys).map((a) => this.str(a));
1567
+ return this.interpreter.run(source, keys, argv, this.redisCall);
1568
+ }
1569
+ };
1570
+
1571
+ // src/memory/infrastructure/adapters/memory-redis.adapter.ts
1572
+ var MemoryPipeline = class {
1573
+ constructor(executor) {
1574
+ this.executor = executor;
1575
+ }
1576
+ queue = [];
1577
+ enqueue(cmd, args) {
1578
+ this.queue.push({ cmd, args });
1579
+ return this;
1580
+ }
1581
+ get(key) {
1582
+ return this.enqueue("GET", [key]);
1583
+ }
1584
+ set(key, value, options) {
1585
+ const args = [key, value];
1586
+ if (options?.ex !== void 0) args.push("EX", options.ex);
1587
+ if (options?.px !== void 0) args.push("PX", options.px);
1588
+ if (options?.nx) args.push("NX");
1589
+ if (options?.xx) args.push("XX");
1590
+ return this.enqueue("SET", args);
1591
+ }
1592
+ del(...keys) {
1593
+ return this.enqueue("DEL", keys);
1594
+ }
1595
+ mget(...keys) {
1596
+ return this.enqueue("MGET", keys);
1597
+ }
1598
+ mset(data) {
1599
+ return this.enqueue("MSET", Object.entries(data).flat());
1600
+ }
1601
+ expire(key, seconds) {
1602
+ return this.enqueue("EXPIRE", [key, seconds]);
1603
+ }
1604
+ ttl(key) {
1605
+ return this.enqueue("TTL", [key]);
1606
+ }
1607
+ incr(key) {
1608
+ return this.enqueue("INCR", [key]);
1609
+ }
1610
+ incrby(key, increment) {
1611
+ return this.enqueue("INCRBY", [key, increment]);
1612
+ }
1613
+ hget(key, field) {
1614
+ return this.enqueue("HGET", [key, field]);
1615
+ }
1616
+ hset(key, field, value) {
1617
+ return this.enqueue("HSET", [key, field, value]);
1618
+ }
1619
+ hmset(key, data) {
1620
+ return this.enqueue("HMSET", [key, ...Object.entries(data).flat()]);
1621
+ }
1622
+ hgetall(key) {
1623
+ return this.enqueue("HGETALL", [key]);
1624
+ }
1625
+ lpush(key, ...values) {
1626
+ return this.enqueue("LPUSH", [key, ...values]);
1627
+ }
1628
+ rpush(key, ...values) {
1629
+ return this.enqueue("RPUSH", [key, ...values]);
1630
+ }
1631
+ sadd(key, ...members) {
1632
+ return this.enqueue("SADD", [key, ...members]);
1633
+ }
1634
+ srem(key, ...members) {
1635
+ return this.enqueue("SREM", [key, ...members]);
1636
+ }
1637
+ zadd(key, ...args) {
1638
+ return this.enqueue("ZADD", [key, ...args]);
1639
+ }
1640
+ zrem(key, ...members) {
1641
+ return this.enqueue("ZREM", [key, ...members]);
1642
+ }
1643
+ exec() {
1644
+ const results = [];
1645
+ for (const { cmd, args } of this.queue) {
1646
+ try {
1647
+ results.push([null, this.executor.execute(cmd, args)]);
1648
+ } catch (error) {
1649
+ results.push([error, null]);
1650
+ }
1651
+ }
1652
+ this.queue = [];
1653
+ return Promise.resolve(results);
1654
+ }
1655
+ };
1656
+ var MemoryMulti = class extends MemoryPipeline {
1657
+ discard() {
1658
+ this.queue = [];
1659
+ }
1660
+ };
1661
+ var MemoryRedisAdapter = class extends BaseRedisDriver {
1662
+ store;
1663
+ executor;
1664
+ constructor(config, options) {
1665
+ super(config, options);
1666
+ this.store = new MemoryStore();
1667
+ this.executor = new CommandExecutor(this.store);
1668
+ }
1669
+ /** Exposes the backing store for assertions / manual reset in tests. */
1670
+ getStore() {
1671
+ return this.store;
1672
+ }
1673
+ doConnect() {
1674
+ return Promise.resolve();
1675
+ }
1676
+ doDisconnect() {
1677
+ return Promise.resolve();
1678
+ }
1679
+ async executeCommand(command, ...args) {
1680
+ const result = this.executor.execute(command, args);
1681
+ const cmd = command.toUpperCase();
1682
+ if (result === null && (cmd === "XREADGROUP" || cmd === "XREAD")) {
1683
+ const blockIdx = args.findIndex((a) => String(a).toUpperCase() === "BLOCK");
1684
+ if (blockIdx !== -1) {
1685
+ const blockMs = Number(args[blockIdx + 1]);
1686
+ const delay = Number.isFinite(blockMs) && blockMs > 0 ? Math.min(blockMs, 20) : 20;
1687
+ await new Promise((resolve) => setTimeout(resolve, delay));
1688
+ }
1689
+ }
1690
+ return result;
1691
+ }
1692
+ createPipeline() {
1693
+ return new MemoryPipeline(this.executor);
1694
+ }
1695
+ createMulti() {
1696
+ return new MemoryMulti(this.executor);
1697
+ }
1698
+ };
1699
+
1700
+ // src/memory/api/register-memory-driver.ts
1701
+ var registered = false;
1702
+ function registerMemoryDriver() {
1703
+ if (registered) return;
1704
+ registerDriver(MEMORY_DRIVER_TYPE, (config, options) => new MemoryRedisAdapter(config, options));
1705
+ registered = true;
1706
+ }
1707
+ var DEFAULT_CLIENTS = { type: "single", host: "localhost", port: 6379 };
1708
+ var RedisTestingModule = class {
1709
+ static forRoot(options = {}) {
1710
+ registerMemoryDriver();
1711
+ return RedisModule.forRoot({
1712
+ ...options,
1713
+ clients: options.clients ?? DEFAULT_CLIENTS,
1714
+ global: { ...options.global, driver: MEMORY_DRIVER_TYPE }
1715
+ });
1716
+ }
1717
+ static forRootAsync(options) {
1718
+ registerMemoryDriver();
1719
+ const userFactory = options.useFactory;
1720
+ return RedisModule.forRootAsync({
1721
+ ...options,
1722
+ useFactory: async (...args) => {
1723
+ const resolved = userFactory ? await userFactory(...args) : {};
1724
+ return {
1725
+ ...resolved,
1726
+ clients: resolved.clients ?? DEFAULT_CLIENTS,
1727
+ global: { ...resolved.global, driver: MEMORY_DRIVER_TYPE }
1728
+ };
1729
+ }
1730
+ });
1731
+ }
1732
+ };
1733
+
1734
+ // src/index.ts
1735
+ registerMemoryDriver();
1736
+
1737
+ export { CommandExecutor, LuaExecutionError, LuaInterpreter, MEMORY_DRIVER_TYPE, MemoryDriverError, MemoryRedisAdapter, MemoryStore, RedisTestingModule, WrongTypeError, registerMemoryDriver };
1738
+ //# sourceMappingURL=index.mjs.map
1739
+ //# sourceMappingURL=index.mjs.map