js-valkey-server 0.3.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.
- package/LICENSE +21 -0
- package/README.md +558 -0
- package/dist/cli.js +17420 -0
- package/dist/cli.js.map +1 -0
- package/dist/core.d.mts +651 -0
- package/dist/core.d.ts +651 -0
- package/dist/core.js +17929 -0
- package/dist/core.js.map +1 -0
- package/dist/core.mjs +17691 -0
- package/dist/core.mjs.map +1 -0
- package/dist/index.d.mts +502 -0
- package/dist/index.d.ts +502 -0
- package/dist/index.js +18845 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +18749 -0
- package/dist/index.mjs.map +1 -0
- package/dist/redis-error-C96iHLJ-.d.mts +1392 -0
- package/dist/redis-error-C96iHLJ-.d.ts +1392 -0
- package/package.json +119 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024-present Artur K
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
# js-redis-server
|
|
2
|
+
|
|
3
|
+
[](https://github.com/fatal10110/js-redis-server/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/js-redis-server)
|
|
5
|
+
[](https://www.npmjs.com/package/js-redis-server)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
[](https://nodejs.org)
|
|
8
|
+
|
|
9
|
+
**An in-memory Redis-compatible server implemented in JavaScript/TypeScript
|
|
10
|
+
for Node.js tests.** Use real ioredis or node-redis clients over TCP and RESP,
|
|
11
|
+
without installing a Redis binary or running Docker. Lua scripting uses
|
|
12
|
+
WebAssembly.
|
|
13
|
+
|
|
14
|
+
▶ **[Try the interactive browser demo](https://fatal10110.github.io/js-redis-server/)** —
|
|
15
|
+
the server executes locally in your browser (no Redis backend): type Redis
|
|
16
|
+
commands in an xterm REPL, run Lua `EVAL`, toggle single/cluster mode and watch
|
|
17
|
+
`MOVED` routing, and open multiple tabs that share one keyspace so `MONITOR` /
|
|
18
|
+
`SUBSCRIBE` / `BLPOP` observe each other.
|
|
19
|
+
|
|
20
|
+
### vs similar tools
|
|
21
|
+
|
|
22
|
+
| | **js-redis-server** | [ioredis-mock](https://www.npmjs.com/package/ioredis-mock) | [redis-memory-server](https://www.npmjs.com/package/redis-memory-server) |
|
|
23
|
+
| --- | --- | --- | --- |
|
|
24
|
+
| What it is | Redis-compatible **protocol server** in JavaScript/TypeScript | **Client API mock** that mimics ioredis | Starts a **real Redis binary** |
|
|
25
|
+
| Your tests talk to | Real TCP + RESP (ioredis / node-redis) | Mocked client methods | Real Redis over TCP |
|
|
26
|
+
| Redis binary required | No | No | Yes |
|
|
27
|
+
| Best for | Real-client tests without a Redis binary | ioredis API-level tests | Tests that need genuine Redis |
|
|
28
|
+
|
|
29
|
+
The server has its own command implementation and compatibility limits; it is
|
|
30
|
+
not the native Redis or Valkey engine. It depends on `cluster-key-slot` and
|
|
31
|
+
`lua-redis-wasm`, with Lua running via WebAssembly. Keep real-server integration
|
|
32
|
+
tests for production compatibility and failure behavior.
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npm install --save-dev js-redis-server ioredis
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
import { createRedisMock } from 'js-redis-server'
|
|
40
|
+
import { Redis } from 'ioredis'
|
|
41
|
+
|
|
42
|
+
const mock = await createRedisMock()
|
|
43
|
+
const redis = new Redis(mock.addresses()[0])
|
|
44
|
+
|
|
45
|
+
await redis.set('foo', 'bar')
|
|
46
|
+
await redis.get('foo') // 'bar'
|
|
47
|
+
|
|
48
|
+
redis.disconnect()
|
|
49
|
+
await mock.close()
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
That's the recommended path: a real server + your real client over a real
|
|
53
|
+
socket, so your client's own encoding and parsing are exercised exactly as in
|
|
54
|
+
production. Jump to [Use as a Redis mock in tests](#use-as-a-redis-mock-in-tests).
|
|
55
|
+
|
|
56
|
+
## Table of Contents
|
|
57
|
+
|
|
58
|
+
- [Why](#why)
|
|
59
|
+
- [Features](#features)
|
|
60
|
+
- [Installation](#installation)
|
|
61
|
+
- [Use as a Redis mock in tests](#use-as-a-redis-mock-in-tests)
|
|
62
|
+
- [Connecting your client](#connecting-your-client)
|
|
63
|
+
- [node:test](#nodetest)
|
|
64
|
+
- [vitest / jest](#vitest--jest)
|
|
65
|
+
- [Cluster mocks](#cluster-mocks)
|
|
66
|
+
- [Compatibility profiles](#compatibility-profiles)
|
|
67
|
+
- [Seeding](#seeding)
|
|
68
|
+
- [`createRedisMock` options](#createredismock-options)
|
|
69
|
+
- [Experimental: socketless client mocks](#experimental-socketless-client-mocks)
|
|
70
|
+
- [`createIoredisMock` — ioredis-mock replacement](#createioredismock--ioredis-mock-replacement)
|
|
71
|
+
- [`createNodeRedisMock` — node-redis in-memory mock](#createnoderedismock--node-redis-in-memory-mock)
|
|
72
|
+
- [`createInMemoryClient` — our own socketless client](#createinmemoryclient--our-own-socketless-client)
|
|
73
|
+
- [Running a server (not a test mock)](#running-a-server-not-a-test-mock)
|
|
74
|
+
- [Supported Commands](docs/COMMANDS.md)
|
|
75
|
+
- [Requirements](#requirements)
|
|
76
|
+
- [Development](#development)
|
|
77
|
+
- [Contributing](#contributing)
|
|
78
|
+
- [License](#license)
|
|
79
|
+
|
|
80
|
+
## Why
|
|
81
|
+
|
|
82
|
+
- **No Redis binary to install, start, or clean up** — the JavaScript server runs in-process and keeps its data in memory.
|
|
83
|
+
- **Isolated and reproducible** — a fresh keyspace per test, reset between tests.
|
|
84
|
+
- **High fidelity** — your real client talks RESP over a real socket, so client-side encoding/parsing is part of the test.
|
|
85
|
+
- **Standalone and cluster** — same API, just pass a `cluster` option.
|
|
86
|
+
- **Protocol server, not a client stub** — unlike ioredis-mock; **no Redis download** — unlike redis-memory-server.
|
|
87
|
+
|
|
88
|
+
## Features
|
|
89
|
+
|
|
90
|
+
- **RESP2 and RESP3 protocols** - Per-session version negotiation via `HELLO`
|
|
91
|
+
- **Standalone and Cluster modes** - Run a single server or a full cluster
|
|
92
|
+
- **Redis / Valkey compatibility profiles** - Pin implemented command behavior to
|
|
93
|
+
older Redis or Valkey versions
|
|
94
|
+
- **Lua scripting support** - Execute Redis Lua scripts via WebAssembly
|
|
95
|
+
- **No Redis installation required** - JavaScript server with Lua via WebAssembly; no Redis binary or Docker needed
|
|
96
|
+
- **TypeScript support** - Ships with full type definitions
|
|
97
|
+
|
|
98
|
+
## Installation
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
npm install js-redis-server
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Starting with the next release, **0.3.0**, the same implementation is also
|
|
105
|
+
published as `js-valkey-server`. Choose one package; you do not need both:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
npm install js-valkey-server
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Both names share the same version, API, compatibility defaults, and GitHub
|
|
112
|
+
repository. `js-redis-server` remains supported and is not deprecated. The
|
|
113
|
+
repository and browser demo URLs are unchanged.
|
|
114
|
+
|
|
115
|
+
Both packages export `createRedisMock` and `createValkeyMock`; the latter is an
|
|
116
|
+
alias, not a different engine or default compatibility profile:
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
import { createValkeyMock } from 'js-valkey-server'
|
|
120
|
+
|
|
121
|
+
const mock = await createValkeyMock({ compatibility: 'valkey-9.0' })
|
|
122
|
+
// Connect your normal client to mock.url.
|
|
123
|
+
await mock.close()
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
The `/core` subpath is available under either package name. Each package also
|
|
127
|
+
provides its matching CLI: `npx js-redis-server` or `npx js-valkey-server`.
|
|
128
|
+
Neither package downloads or wraps the official Redis/Valkey binary; the
|
|
129
|
+
server is implemented in JavaScript/TypeScript and Lua uses WebAssembly.
|
|
130
|
+
|
|
131
|
+
## Use as a Redis mock in tests
|
|
132
|
+
|
|
133
|
+
`createRedisMock()` owns the whole lifecycle: it spins up a standalone server
|
|
134
|
+
(16 databases, random free port) or a whole cluster, seeds data, and resets
|
|
135
|
+
between tests — you just connect your real client library to it.
|
|
136
|
+
|
|
137
|
+
`RedisMock` surface:
|
|
138
|
+
|
|
139
|
+
| Member | Description |
|
|
140
|
+
| :---------------------- | :---------------------------------------------------------------------------------- |
|
|
141
|
+
| `host` / `port` / `url` | Connection coordinates of the (first) node. |
|
|
142
|
+
| `addresses()` | `{ host, port }[]` — one entry standalone, every node for cluster. Client-agnostic. |
|
|
143
|
+
| `seed(entries)` | Preload data (see [Seeding](#seeding)). |
|
|
144
|
+
| `flush()` / `reset()` | Clear all keyspace data between tests. |
|
|
145
|
+
| `close()` | Shut down the server / cluster. |
|
|
146
|
+
| `state` / `nodes` | Escape hatches to the underlying `RedisServerState` / node handles. |
|
|
147
|
+
|
|
148
|
+
### Connecting your client
|
|
149
|
+
|
|
150
|
+
A mock is a real server on a random free port, so connect any standard client
|
|
151
|
+
to `mock.addresses()` / `mock.url`:
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
// ioredis
|
|
155
|
+
import { Redis } from 'ioredis'
|
|
156
|
+
const redis = new Redis(mock.addresses()[0])
|
|
157
|
+
|
|
158
|
+
// node-redis
|
|
159
|
+
import { createClient } from 'redis'
|
|
160
|
+
const client = createClient({ url: mock.url })
|
|
161
|
+
await client.connect()
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Connections start on RESP2 and upgrade to RESP3 when the client asks for it
|
|
165
|
+
(ioredis sends `HELLO 3`; node-redis takes a `RESP: 3` option) — negotiated
|
|
166
|
+
per-connection, no special setup.
|
|
167
|
+
|
|
168
|
+
### node:test
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
import { test, beforeEach, afterEach } from 'node:test'
|
|
172
|
+
import assert from 'node:assert'
|
|
173
|
+
import { Redis } from 'ioredis'
|
|
174
|
+
import { createRedisMock, type RedisMock } from 'js-redis-server'
|
|
175
|
+
|
|
176
|
+
let mock: RedisMock
|
|
177
|
+
let client: Redis
|
|
178
|
+
|
|
179
|
+
beforeEach(async () => {
|
|
180
|
+
mock = await createRedisMock()
|
|
181
|
+
client = new Redis(mock.addresses()[0])
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
afterEach(async () => {
|
|
185
|
+
client.disconnect()
|
|
186
|
+
await mock.close()
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
test('basic set/get operations', async () => {
|
|
190
|
+
await client.set('foo', 'bar')
|
|
191
|
+
assert.strictEqual(await client.get('foo'), 'bar')
|
|
192
|
+
})
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### vitest / jest
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
import { beforeEach, afterEach, test, expect } from 'vitest' // or '@jest/globals'
|
|
199
|
+
import { Redis } from 'ioredis'
|
|
200
|
+
import { createRedisMock, type RedisMock } from 'js-redis-server'
|
|
201
|
+
|
|
202
|
+
let mock: RedisMock
|
|
203
|
+
let client: Redis
|
|
204
|
+
|
|
205
|
+
beforeEach(async () => {
|
|
206
|
+
mock = await createRedisMock()
|
|
207
|
+
await mock.seed([{ key: 'counter', type: 'string', value: 1 }])
|
|
208
|
+
client = new Redis(mock.addresses()[0])
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
afterEach(async () => {
|
|
212
|
+
client.disconnect()
|
|
213
|
+
await mock.close()
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
test('increments a seeded counter', async () => {
|
|
217
|
+
expect(await client.incr('counter')).toBe(2)
|
|
218
|
+
})
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Prefer a fresh `createRedisMock()` per test for full isolation; to reuse one
|
|
222
|
+
instance across a file, call `await mock.flush()` in `afterEach` instead.
|
|
223
|
+
|
|
224
|
+
### Cluster mocks
|
|
225
|
+
|
|
226
|
+
Same facade — pass `cluster`, then point a cluster client at every node via
|
|
227
|
+
`mock.addresses()`:
|
|
228
|
+
|
|
229
|
+
```typescript
|
|
230
|
+
const mock = await createRedisMock({ cluster: { masters: 3, replicas: 1 } })
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
```typescript
|
|
234
|
+
// ioredis
|
|
235
|
+
const cluster = new Redis.Cluster(mock.addresses())
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
// node-redis
|
|
240
|
+
import { createCluster } from 'redis'
|
|
241
|
+
const cluster = createCluster({
|
|
242
|
+
rootNodes: mock
|
|
243
|
+
.addresses()
|
|
244
|
+
.map(n => ({ url: `redis://${n.host}:${n.port}` })),
|
|
245
|
+
})
|
|
246
|
+
await cluster.connect()
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### Compatibility profiles
|
|
250
|
+
|
|
251
|
+
By default the mock exposes the newest implemented Redis behavior. Pass
|
|
252
|
+
`compatibility` when a test needs to match an older Redis or Valkey target:
|
|
253
|
+
|
|
254
|
+
```typescript
|
|
255
|
+
const redis62 = await createRedisMock({ compatibility: 'redis-6.2' })
|
|
256
|
+
|
|
257
|
+
const valkeyCluster = await createRedisMock({
|
|
258
|
+
cluster: { masters: 3 },
|
|
259
|
+
compatibility: 'valkey-9.0',
|
|
260
|
+
})
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Profiles gate implemented commands, subcommands, options, and known behavioral
|
|
264
|
+
differences. For example, `EXPIRETIME key` is unavailable under `redis-6.2` but
|
|
265
|
+
available under newer Redis profiles. Unsupported commands remain unsupported
|
|
266
|
+
regardless of profile. See the current gate matrix in
|
|
267
|
+
[Compatibility Profiles](docs/API.md#compatibility-profiles).
|
|
268
|
+
|
|
269
|
+
Supported presets: `redis-6.2`, `redis-7.0`, `redis-7.2`, `redis-7.4`,
|
|
270
|
+
`redis-8.0`, `valkey-8.0`, and `valkey-9.0`.
|
|
271
|
+
|
|
272
|
+
Valkey profiles model the Redis 7.0-era gates as enabled:
|
|
273
|
+
|
|
274
|
+
| Profile | Redis 7.0 command/subcommand/option gates | Valkey-only modeled gate |
|
|
275
|
+
| --- | --- | --- |
|
|
276
|
+
| `valkey-8.0` | enabled | cluster multi-DB disabled |
|
|
277
|
+
| `valkey-9.0` | enabled | cluster multi-DB enabled |
|
|
278
|
+
|
|
279
|
+
### Seeding
|
|
280
|
+
|
|
281
|
+
`seed()` takes an explicit entries array — you supply keys, types, values, and
|
|
282
|
+
optional `ttlMs` / `db`; the mock owns placement (including cluster slot
|
|
283
|
+
routing) and the internal value conversion.
|
|
284
|
+
|
|
285
|
+
```typescript
|
|
286
|
+
const mock = await createRedisMock()
|
|
287
|
+
|
|
288
|
+
await mock.seed([
|
|
289
|
+
{ key: 'user:1', type: 'string', value: 'alice' },
|
|
290
|
+
{ key: 'counter', type: 'string', value: 42 },
|
|
291
|
+
{ key: 'h:1', type: 'hash', value: { name: 'bob', age: 30 } },
|
|
292
|
+
{ key: 'l:1', type: 'list', value: ['a', 'b', 1] },
|
|
293
|
+
{ key: 's:1', type: 'set', value: ['x', 'y'] },
|
|
294
|
+
{ key: 'z:1', type: 'zset', value: { a: 1, b: 2 } },
|
|
295
|
+
{ key: 'ttl:1', type: 'string', value: 'temp', ttlMs: 50_000 },
|
|
296
|
+
{ key: 'in-db-3', type: 'string', value: 'scoped', db: 3 },
|
|
297
|
+
])
|
|
298
|
+
|
|
299
|
+
// any client connected to the mock now sees the seeded keys
|
|
300
|
+
// (e.g. new Redis(mock.addresses()[0]) — GET user:1 → 'alice')
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
Each entry's shape is checked against its `type`:
|
|
304
|
+
|
|
305
|
+
```typescript
|
|
306
|
+
type SeedEntry =
|
|
307
|
+
| {
|
|
308
|
+
key: string
|
|
309
|
+
type: 'string'
|
|
310
|
+
value: string | number
|
|
311
|
+
ttlMs?: number
|
|
312
|
+
db?: number
|
|
313
|
+
}
|
|
314
|
+
| {
|
|
315
|
+
key: string
|
|
316
|
+
type: 'hash'
|
|
317
|
+
value: Record<string, string | number>
|
|
318
|
+
ttlMs?: number
|
|
319
|
+
db?: number
|
|
320
|
+
}
|
|
321
|
+
| {
|
|
322
|
+
key: string
|
|
323
|
+
type: 'list'
|
|
324
|
+
value: (string | number)[]
|
|
325
|
+
ttlMs?: number
|
|
326
|
+
db?: number
|
|
327
|
+
}
|
|
328
|
+
| {
|
|
329
|
+
key: string
|
|
330
|
+
type: 'set'
|
|
331
|
+
value: (string | number)[]
|
|
332
|
+
ttlMs?: number
|
|
333
|
+
db?: number
|
|
334
|
+
}
|
|
335
|
+
| {
|
|
336
|
+
key: string
|
|
337
|
+
type: 'zset'
|
|
338
|
+
value: Record<string, number>
|
|
339
|
+
ttlMs?: number
|
|
340
|
+
db?: number
|
|
341
|
+
}
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
`db` selects the logical database (standalone mocks). Streams are not seedable
|
|
345
|
+
yet. For anything beyond these shapes, drive your client directly or reach for
|
|
346
|
+
the `mock.state` escape hatch.
|
|
347
|
+
|
|
348
|
+
### `createRedisMock` options
|
|
349
|
+
|
|
350
|
+
```typescript
|
|
351
|
+
createRedisMock(options?: CreateRedisMockOptions): Promise<RedisMock>
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
| Parameter | Type | Default | Description |
|
|
355
|
+
| :-------------- | :--------------------------------------- | :------------ | :----------------------------------------------------------- |
|
|
356
|
+
| `cluster` | `{ masters: number; replicas?: number }` | `undefined` | When set, builds a cluster mock instead of a standalone one. |
|
|
357
|
+
| `databaseCount` | `number` | `16` | Standalone-only: logical database count. |
|
|
358
|
+
| `compatibility` | `CompatibilitySpec` | `'redis-8.0'` | Redis / Valkey compatibility profile. |
|
|
359
|
+
| `port` | `number` | `0` | Standalone bind port (`0` = OS-assigned). |
|
|
360
|
+
| `basePort` | `number` | `0` | Cluster base port (`0` = each node OS-assigned). |
|
|
361
|
+
| `logger` | `Pick<Logger, 'error'>` | `undefined` | Optional logger. |
|
|
362
|
+
|
|
363
|
+
## Experimental: socketless client mocks
|
|
364
|
+
|
|
365
|
+
> ⚠️ **Not recommended.** These return a client object directly — no socket, no
|
|
366
|
+
> port — so they skip the real network round-trip and (in some cases) real RESP
|
|
367
|
+
> encoding. They're faster and need no `addresses()` wiring, but they're
|
|
368
|
+
> **lower fidelity** than the recommended path and the surfaces are still
|
|
369
|
+
> evolving. Prefer [`createRedisMock`](#use-as-a-redis-mock-in-tests) + a real
|
|
370
|
+
> client unless you have a specific reason not to.
|
|
371
|
+
|
|
372
|
+
Three flavours, depending on which client you want to look like:
|
|
373
|
+
|
|
374
|
+
| Helper | Looks like | How |
|
|
375
|
+
| :--------------------- | :-------------- | :--------------------------------------------------------- |
|
|
376
|
+
| `createIoredisMock` | `ioredis` | the **real** ioredis client over a fake `net.Socket` |
|
|
377
|
+
| `createNodeRedisMock` | `node-redis` | a hand-written facade mirroring node-redis' public surface |
|
|
378
|
+
| `createInMemoryClient` | our own bespoke | a thin client that returns native JS replies, no RESP |
|
|
379
|
+
|
|
380
|
+
### `createIoredisMock` — ioredis-mock replacement
|
|
381
|
+
|
|
382
|
+
A drop-in alternative to the [`ioredis-mock`](https://www.npmjs.com/package/ioredis-mock)
|
|
383
|
+
library. `createIoredisMock()` returns a **real** `ioredis` client wired to the
|
|
384
|
+
in-memory pipeline over a fake `net.Socket` — no TCP port, no loopback. Because
|
|
385
|
+
it's the genuine client speaking real RESP, typed methods, pipelines, `multi`,
|
|
386
|
+
pub/sub, and `scanStream` all work unchanged. `ioredis` is an optional peer
|
|
387
|
+
dependency, imported lazily — install `ioredis` yourself to use this helper.
|
|
388
|
+
|
|
389
|
+
```typescript
|
|
390
|
+
import { createIoredisMock } from 'js-redis-server'
|
|
391
|
+
import type { Redis } from 'ioredis'
|
|
392
|
+
|
|
393
|
+
const redis = (await createIoredisMock()) as Redis // 16 logical DBs by default
|
|
394
|
+
|
|
395
|
+
await redis.set('k', 'v')
|
|
396
|
+
await redis.get('k') // 'v'
|
|
397
|
+
await redis.hset('h', 'f1', 'a', 'f2', 'b')
|
|
398
|
+
await redis.hgetall('h') // { f1: 'a', f2: 'b' }
|
|
399
|
+
|
|
400
|
+
await redis.quit() // tears down the in-memory state
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
Pass `cluster` for a real `Cluster` client; keyed commands follow `MOVED`
|
|
404
|
+
in-process across the synthetic nodes:
|
|
405
|
+
|
|
406
|
+
```typescript
|
|
407
|
+
import type { Cluster } from 'ioredis'
|
|
408
|
+
|
|
409
|
+
const cluster = (await createIoredisMock({
|
|
410
|
+
cluster: { masters: 3, replicasPerMaster: 1 }, // replicasPerMaster optional
|
|
411
|
+
})) as Cluster
|
|
412
|
+
|
|
413
|
+
await cluster.set('alpha', '1') // routed to its owning master
|
|
414
|
+
await cluster.get('alpha') // '1'
|
|
415
|
+
|
|
416
|
+
await cluster.quit()
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
Preload data with a `seed` array (same [`SeedEntry`](#seeding) shapes as
|
|
420
|
+
`createRedisMock().seed()`). The keyspace is populated before the client
|
|
421
|
+
connects, so it's ready on the first command. In cluster mode each key is
|
|
422
|
+
routed to its slot-owning master:
|
|
423
|
+
|
|
424
|
+
```typescript
|
|
425
|
+
const redis = (await createIoredisMock({
|
|
426
|
+
seed: [
|
|
427
|
+
{ key: 'user:1', type: 'string', value: 'alice' },
|
|
428
|
+
{ key: 'h:1', type: 'hash', value: { name: 'bob', age: 30 } },
|
|
429
|
+
{ key: 'temp', type: 'string', value: 'x', ttlMs: 50_000 },
|
|
430
|
+
],
|
|
431
|
+
})) as Redis
|
|
432
|
+
|
|
433
|
+
await redis.get('user:1') // 'alice'
|
|
434
|
+
|
|
435
|
+
// cluster: createIoredisMock({ cluster: { masters: 3 }, seed: [...] })
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
### `createNodeRedisMock` — node-redis in-memory mock
|
|
439
|
+
|
|
440
|
+
node-redis exposes no socket hook, so this can't drive the real client over a
|
|
441
|
+
virtual socket the way `createIoredisMock` does. Instead `createNodeRedisMock()`
|
|
442
|
+
returns a **hand-written facade** that mirrors node-redis' public surface — a
|
|
443
|
+
curated set of camelCase methods with node-redis-correct return types — and
|
|
444
|
+
routes every command through the same in-memory pipeline. Anything not curated
|
|
445
|
+
falls through to the generic `sendCommand()` escape hatch, which decodes replies
|
|
446
|
+
to native JS.
|
|
447
|
+
|
|
448
|
+
```typescript
|
|
449
|
+
import { createNodeRedisMock } from 'js-redis-server'
|
|
450
|
+
|
|
451
|
+
const client = await createNodeRedisMock() // 16 logical DBs by default
|
|
452
|
+
|
|
453
|
+
await client.set('k', 'v')
|
|
454
|
+
await client.get('k') // 'v'
|
|
455
|
+
await client.sendCommand(['HSET', 'h', 'f1', 'a']) // escape hatch
|
|
456
|
+
|
|
457
|
+
await client.quit() // tears down the in-memory state
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
Pass `cluster` for a cluster facade; keyed commands route by slot in-process:
|
|
461
|
+
|
|
462
|
+
```typescript
|
|
463
|
+
const cluster = await createNodeRedisMock({
|
|
464
|
+
cluster: { masters: 3, replicas: 1 },
|
|
465
|
+
})
|
|
466
|
+
|
|
467
|
+
await cluster.set('alpha', '1')
|
|
468
|
+
await cluster.get('alpha') // '1'
|
|
469
|
+
|
|
470
|
+
await cluster.quit()
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
### `createInMemoryClient` — our own socketless client
|
|
474
|
+
|
|
475
|
+
If you don't need to look like any particular client library,
|
|
476
|
+
`createInMemoryClient()` returns an in-process client with its **own** keyspace
|
|
477
|
+
that drives the command pipeline directly — no TCP loopback, no RESP encoding —
|
|
478
|
+
and resolves to native JS replies (throwing `RedisCommandError` on `-ERR`).
|
|
479
|
+
Standalone only.
|
|
480
|
+
|
|
481
|
+
```typescript
|
|
482
|
+
import { createInMemoryClient } from 'js-redis-server'
|
|
483
|
+
|
|
484
|
+
const client = await createInMemoryClient({
|
|
485
|
+
// databaseCount?, database?, returnBuffers?, seed?
|
|
486
|
+
})
|
|
487
|
+
|
|
488
|
+
await client.command('SET', 'k', 'v')
|
|
489
|
+
await client.command('GET', 'k') // 'v'
|
|
490
|
+
await client.command('INCR', 'n') // 1 (number)
|
|
491
|
+
await client.command('HGETALL', 'h') // { field: 'value', ... }
|
|
492
|
+
|
|
493
|
+
client.close() // tears down its keyspace
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
It takes the same `seed` array as `createRedisMock().seed()` to pre-populate its
|
|
497
|
+
keyspace before the first command. Need to drive an existing `createRedisMock()`'s
|
|
498
|
+
keyspace instead of an independent one? Construct `InMemoryRedisClient` directly
|
|
499
|
+
with that mock's `state` and an executor from `js-redis-server/core`.
|
|
500
|
+
|
|
501
|
+
## Running a server (not a test mock)
|
|
502
|
+
|
|
503
|
+
Need a real, **listening** server a separate process connects to (a CLI, a dev
|
|
504
|
+
tool), or want to assemble the pipeline by hand with custom commands/policies?
|
|
505
|
+
That lives in the **[Server & Low-Level API](docs/API.md)** doc:
|
|
506
|
+
`createRedisServer`, `createRedisCluster`, the CLI, `Resp2Server`,
|
|
507
|
+
`RedisServerState`, and package entry points.
|
|
508
|
+
|
|
509
|
+
## Requirements
|
|
510
|
+
|
|
511
|
+
- Node.js >= 24
|
|
512
|
+
|
|
513
|
+
## Development
|
|
514
|
+
|
|
515
|
+
```bash
|
|
516
|
+
# Install dependencies
|
|
517
|
+
npm install
|
|
518
|
+
|
|
519
|
+
# Build
|
|
520
|
+
npm run build
|
|
521
|
+
|
|
522
|
+
# Run tests
|
|
523
|
+
npm test
|
|
524
|
+
|
|
525
|
+
# Lint and format
|
|
526
|
+
npm run lint
|
|
527
|
+
npm run format
|
|
528
|
+
|
|
529
|
+
# Run integration tests (mock backend)
|
|
530
|
+
npm run test:integration:mock
|
|
531
|
+
|
|
532
|
+
# Run integration tests (real Redis)
|
|
533
|
+
# Requires a Redis cluster — start one with docker-compose.test.yml first:
|
|
534
|
+
# docker compose -f docker-compose.test.yml up -d --wait
|
|
535
|
+
npm run test:integration:real
|
|
536
|
+
|
|
537
|
+
# Run all tests
|
|
538
|
+
npm run test:all
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
> **CI** runs four jobs on every push and pull request: lint + format check,
|
|
542
|
+
> unit tests, mock-backend integration tests, and real-backend integration
|
|
543
|
+
> tests against a Redis cluster spun up via `docker-compose.test.yml`.
|
|
544
|
+
|
|
545
|
+
## Further Documentation
|
|
546
|
+
|
|
547
|
+
- [Server & Low-Level API](docs/API.md) — running a listening server, the CLI, cluster builders, and the `core` building blocks
|
|
548
|
+
- [Architecture](docs/ARCHITECTURE.md) — layers, command pipeline, execution policies, cluster routing, RESP2/RESP3, and diagrams
|
|
549
|
+
- [Detailed Command Implementation Status](docs/COMMANDS.md)
|
|
550
|
+
- [Integration Testing Infrastructure](docs/TEST-INTEGRATION.md)
|
|
551
|
+
|
|
552
|
+
## Contributing
|
|
553
|
+
|
|
554
|
+
Contributions are welcome! Please read the [contributing guidelines](CONTRIBUTING.md) before submitting a pull request.
|
|
555
|
+
|
|
556
|
+
## License
|
|
557
|
+
|
|
558
|
+
MIT - see [LICENSE](LICENSE) for details.
|