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 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
+ [![CI](https://github.com/fatal10110/js-redis-server/actions/workflows/ci.yml/badge.svg)](https://github.com/fatal10110/js-redis-server/actions/workflows/ci.yml)
4
+ [![npm version](https://img.shields.io/npm/v/js-redis-server.svg)](https://www.npmjs.com/package/js-redis-server)
5
+ [![npm downloads](https://img.shields.io/npm/dm/js-redis-server.svg)](https://www.npmjs.com/package/js-redis-server)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+ [![Node.js Version](https://img.shields.io/node/v/js-redis-server.svg)](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.