Skip to content

Commit

Permalink
feat: Share context between Redis instances (#1121)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: See readme for upgrade instructions
  • Loading branch information
stipsan authored Jan 25, 2022
1 parent 3c578b7 commit a9adbd0
Show file tree
Hide file tree
Showing 10 changed files with 83 additions and 60 deletions.
99 changes: 61 additions & 38 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ Check the [compatibility table](compat.md) for supported redis commands.
## Usage ([try it in your browser](https://runkit.com/npm/ioredis-mock))

```js
var Redis = require('ioredis-mock');
var redis = new Redis({
const Redis = require('ioredis-mock');
const redis = new Redis({
// `options.data` does not exist in `ioredis`, only `ioredis-mock`
data: {
user_next: '3',
Expand All @@ -40,6 +40,49 @@ var redis = new Redis({
// Basically use it just like ioredis
```

### Breaking API changes from v5

Before v6, each instance of `ioredis-mock` lived in isolation:

```js
const Redis = require('ioredis-mock');
const redis1 = new Redis();
const redis2 = new Redis();

await redis1.set('foo', 'bar');
console.log(await redis1.get('foo'), await redis2.get('foo')); // 'bar', null
```

In v6 the [internals were rewritten](/~https://github.com/stipsan/ioredis-mock/pull/1110) to behave more like real life redis, if the host and port is the same, the context is now shared:

```js
const Redis = require('ioredis-mock');
const redis1 = new Redis();
const redis2 = new Redis();
const redis3 = new Redis({ port: 6380 }); // 6379 is the default port

await redis1.set('foo', 'bar');
console.log(
await redis1.get('foo'), // 'bar'
await redis2.get('foo'), // 'bar'
await redis3.get('foo') // null
);
```

And since `ioredis-mock` now persist data between instances, you'll [likely](/~https://github.com/luin/ioredis/blob/8278ec0a435756c54ba4f98587aec1a913e8b7d3/test/helpers/global.ts#L8) need to run `flushall` between testing suites:

```js
const Redis = require('ioredis-mock');

afterEach((done) => {
new Redis().flushall().then(() => done());
});
```

#### `createConnectedClient` is deprecated

Replace it with `.duplicate()` or use another `new Redis` instance.

### Configuring Jest

Use the jest specific bundle when setting up mocks:
Expand All @@ -52,24 +95,19 @@ The `ioredis-mock/jest` bundle inlines imports from `ioredis` that `ioredis-mock

### Pub/Sub channels

We also support redis publish/subscribe channels (just like ioredis).
Like ioredis, you need two clients:

- the pubSub client for subcriptions and events, [which can only be used for subscriptions](https://redis.io/topics/pubsub)
- the usual client for issuing 'synchronous' commands like get, publish, etc
We also support redis [publish/subscribe](https://redis.io/topics/pubsub) channels.
Like [ioredis](/~https://github.com/luin/ioredis#pubsub), you need two clients:

```js
var Redis = require('ioredis-mock');
var redisPubSub = new Redis();
// create a second Redis Mock (connected to redisPubSub)
var redisSync = redisPubSub.createConnectedClient();
redisPubSub.on('message', (channel, message) => {
expect(channel).toBe('emails');
expect(message).toBe('clark@daily.planet');
done();
const Redis = require('ioredis-mock');
const redisPub = new Redis();
const redisSub = new Redis();

redisSub.on('message', (channel, message) => {
console.log(`Received ${message} from ${channel}`);
});
redisPubSub.subscribe('emails');
redisSync.publish('emails', 'clark@daily.planet');
redisSub.subscribe('emails');
redisPub.publish('emails', 'clark@daily.planet');
```

### Promises
Expand All @@ -93,7 +131,7 @@ You could define a custom command `MULTIPLY` which accepts one
key and one argument. A redis key, where you can get the multiplicand, and an argument which will be the multiplicator:

```js
var Redis = require('ioredis-mock');
const Redis = require('ioredis-mock');
const redis = new Redis({ data: { 'k1': 5 } });
const commandDefinition: { numberOfKeys: 1, lua: 'return KEYS[1] * ARGV[1]' };
redis.defineCommand('MULTIPLY', commandDefinition) // defineCommand(name, definition)
Expand All @@ -107,7 +145,7 @@ redis.defineCommand('MULTIPLY', commandDefinition) // defineCommand(name, defini
You can also achieve the same effect by using the `eval` command:

```js
var Redis = require('ioredis-mock');
const Redis = require('ioredis-mock');
const redis = new Redis({ data: { k1: 5 } });
const result = redis.eval(`return redis.call("GET", "k1") * 10`);
expect(result).toBe(5 * 10);
Expand All @@ -127,31 +165,16 @@ As a difference from ioredis we currently don't support:
Work on Cluster support has started, the current implementation is minimal and PRs welcome #359

```js
var Redis = require('ioredis-mock');
const Redis = require('ioredis-mock');

const cluster = new Redis.Cluster(['redis://localhost:7001']);
const nodes = cluster.nodes;
expect(nodes.length).toEqual(1);
```

## Roadmap

This project started off as just an utility in
[another project](/~https://github.com/stipsan/epic) and got open sourced to
benefit the rest of the ioredis community. This means there's work to do before
it's feature complete:

- [x] Setup testing suite for the library itself.
- [x] Refactor to bluebird promises like ioredis, support node style callback
too.
- [x] Implement remaining basic features that read/write data.
- [x] Implement ioredis
[argument and reply transformers](/~https://github.com/luin/ioredis#transforming-arguments--replies).
- [ ] Connection Events
- [ ] Offline Queue
- [x] Pub/Sub
- [ ] Error Handling
- [ ] Implement [remaining](compat.md) commands
## [Roadmap](/~https://github.com/users/stipsan/projects/1/views/4)

You can check the [roadmap project page](/~https://github.com/users/stipsan/projects/1/views/4), and [the compat table](compat.md), to see how close we are to feature parity with `ioredis`.

## I need a feature not listed here

Expand Down
2 changes: 1 addition & 1 deletion test/commands/expire.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ describe('expire', () => {

it('should emit keyspace notification if configured', (done) => {
const redis = new Redis({ notifyKeyspaceEvents: 'gK' }); // gK: generic Keyspace
const redisPubSub = redis.createConnectedClient();
const redisPubSub = redis.duplicate();
redisPubSub.on('message', (channel, message) => {
expect(channel).toBe('__keyspace@0__:foo');
expect(message).toBe('expire');
Expand Down
4 changes: 2 additions & 2 deletions test/commands/psubscribe.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ describe('psubscribe', () => {

it('should allow multiple instances to subscribe to the same channel', () => {
const redisOne = new Redis();
const redisTwo = redisOne.createConnectedClient();
const redisTwo = new Redis();

return Promise.all([
redisOne.psubscribe('first.*', 'second.*'),
Expand All @@ -66,7 +66,7 @@ describe('psubscribe', () => {
redisOne.on('pmessage', promiseOneFulfill);
redisTwo.on('pmessage', PromiseTwoFulfill);

redisOne.createConnectedClient().publish('first.test', 'blah');
redisOne.duplicate().publish('first.test', 'blah');

return Promise.all([promiseOne, promiseTwo]);
});
Expand Down
12 changes: 6 additions & 6 deletions test/commands/publish.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ describe('publish', () => {

it('should return 1 when publishing with a single subscriber', () => {
const redisPubSub = new Redis();
const redis2 = redisPubSub.createConnectedClient();
const redis2 = new Redis();
redisPubSub.subscribe('emails');
return redis2
.publish('emails', 'clark@daily.planet')
Expand All @@ -19,7 +19,7 @@ describe('publish', () => {

it('should publish a message, which can be received by a previous subscribe', (done) => {
const redisPubSub = new Redis();
const redis2 = redisPubSub.createConnectedClient();
const redis2 = new Redis();
redisPubSub.on('message', (channel, message) => {
expect(channel).toBe('emails');
expect(message).toBe('clark@daily.planet');
Expand All @@ -31,7 +31,7 @@ describe('publish', () => {

it('should emit messageBuffer event when a Buffer message is published on a subscribed channel', (done) => {
const redisPubSub = new Redis();
const redis2 = redisPubSub.createConnectedClient();
const redis2 = new Redis();
const buffer = Buffer.alloc(8);
redisPubSub.on('messageBuffer', (channel, message) => {
expect(channel).toBe('emails');
Expand All @@ -44,7 +44,7 @@ describe('publish', () => {

it('should return 1 when publishing with a single pattern subscriber', () => {
const redisPubSub = new Redis();
const redis2 = redisPubSub.createConnectedClient();
const redis2 = new Redis();
redisPubSub.psubscribe('emails.*');
return redis2
.publish('emails.urgent', 'clark@daily.planet')
Expand All @@ -53,7 +53,7 @@ describe('publish', () => {

it('should publish a message, which can be received by a previous psubscribe', (done) => {
const redisPubSub = new Redis();
const redis2 = redisPubSub.createConnectedClient();
const redis2 = new Redis();
redisPubSub.on('pmessage', (pattern, channel, message) => {
expect(pattern).toBe('emails.*');
expect(channel).toBe('emails.urgent');
Expand All @@ -66,7 +66,7 @@ describe('publish', () => {

it('should emit a pmessageBuffer event when a Buffer message is published matching a psubscribed pattern', (done) => {
const redisPubSub = new Redis();
const redis2 = redisPubSub.createConnectedClient();
const redis2 = new Redis();
const buffer = Buffer.alloc(0);
redisPubSub.on('pmessageBuffer', (pattern, channel, message) => {
expect(pattern).toBe('emails.*');
Expand Down
4 changes: 2 additions & 2 deletions test/commands/punsubscribe.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ describe('punsubscribe', () => {

it('should unsubscribe only one instance when more than one is subscribed to a channel', () => {
const redisOne = new Redis();
const redisTwo = redisOne.createConnectedClient();
const redisTwo = new Redis();

return Promise.all([
redisOne.psubscribe('first.*'),
Expand All @@ -56,7 +56,7 @@ describe('punsubscribe', () => {

redisOne.on('pmessage', promiseFulfill);

redisOne.createConnectedClient().publish('first.test', 'TEST');
redisOne.duplicate().publish('first.test', 'TEST');

return promise;
});
Expand Down
2 changes: 1 addition & 1 deletion test/commands/rename.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ describe('rename', () => {

it('should emit keyspace notifications if configured', (done) => {
const redis = new Redis({ notifyKeyspaceEvents: 'gK' }); // gK: generic Keyspace
const redisPubSub = redis.createConnectedClient();
const redisPubSub = redis.duplicate();
let messagesReceived = 0;
redisPubSub.on('message', (channel, message) => {
messagesReceived++;
Expand Down
4 changes: 2 additions & 2 deletions test/commands/subscribe.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ describe('subscribe', () => {

it('should allow multiple instances to subscribe to the same channel', () => {
const redisOne = new Redis();
const redisTwo = redisOne.createConnectedClient();
const redisTwo = new Redis();

return Promise.all([
redisOne.subscribe('first', 'second'),
Expand All @@ -71,7 +71,7 @@ describe('subscribe', () => {
redisOne.on('message', promiseOneFulfill);
redisTwo.on('message', PromiseTwoFulfill);

redisOne.createConnectedClient().publish('first', 'blah');
redisOne.duplicate().publish('first', 'blah');

return Promise.all([promiseOne, promiseTwo]);
});
Expand Down
6 changes: 3 additions & 3 deletions test/commands/unsubscribe.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ describe('unsubscribe', () => {

it('should unsubscribe only one instance when more than one is subscribed to a channel', () => {
const redisOne = new Redis();
const redisTwo = redisOne.createConnectedClient();
const redisTwo = new Redis();

return Promise.all([
redisOne.subscribe('first'),
Expand All @@ -55,15 +55,15 @@ describe('unsubscribe', () => {

redisOne.on('message', promiseFulfill);

redisOne.createConnectedClient().publish('first', 'TEST');
redisOne.duplicate().publish('first', 'TEST');

return promise;
});
});

it('should not alter parent instance when connected client unsubscribes', () => {
const redisOne = new Redis();
const redisTwo = redisOne.createConnectedClient();
const redisTwo = new Redis();
return redisOne
.subscribe('first')
.then(() => redisTwo.unsubscribe('first'))
Expand Down
4 changes: 2 additions & 2 deletions test/keyspace-notifications.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ describe('parseKeyspaceEvents', () => {
describe('keyspaceNotifications', () => {
it('should appear when configured and the triggering event occurs', (done) => {
const redis = new Redis({ notifyKeyspaceEvents: 'gK' }); // gK: generic keyspace
const redisPubSub = redis.createConnectedClient();
const redisPubSub = redis.duplicate();
redisPubSub.on('message', (channel, message) => {
expect(channel).toBe('__keyspace@0__:key');
expect(message).toBe('del');
Expand Down Expand Up @@ -105,7 +105,7 @@ describe('keyspaceNotifications', () => {
describe('keyeventNotifications', () => {
it('should appear when configured and the triggering event occurs', (done) => {
const redis = new Redis({ notifyKeyspaceEvents: 'gE' }); // gK: generic keyevent
const redisPubSub = redis.createConnectedClient();
const redisPubSub = redis.duplicate();
redisPubSub.on('message', (channel, message) => {
expect(channel).toBe('__keyevent@0__:del');
expect(message).toBe('key');
Expand Down
6 changes: 3 additions & 3 deletions test/multiple-mocks.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ import Redis from 'ioredis';
describe('multipleMocks', () => {
it('should be possible to create a second IORedis client, which is working on shared data with the first client', () => {
const client1 = new Redis();
const client2 = client1.createConnectedClient();
const client2 = new Redis();
client1.hset('testing', 'test', '2').then(() => {
client2.hget('testing', 'test').then((val) => expect(val).toBe('2'));
});
});

it('should be possible to create a second IORedis client, which is working on shared channels with the first client', (done) => {
const client1 = new Redis();
const client2 = client1.createConnectedClient();
const client2 = new Redis();
client1.on('message', (channel, message) => {
expect(channel).toBe('channel');
expect(message).toBe('hello');
Expand All @@ -31,7 +31,7 @@ describe('multipleMocks', () => {

const connectedClients = [];
for (let i = 0; i < 10; i++) {
const connectedClient = client.createConnectedClient();
const connectedClient = client.duplicate();
connectedClients.push(connectedClient);
connectedClient.subscribe(testChannel);
}
Expand Down

0 comments on commit a9adbd0

Please sign in to comment.