Skip to content

Commit

Permalink
feat: ♻️ change redis client to use ioredis instead of node-redis
Browse files Browse the repository at this point in the history
BREAKING CHANGE: node-redis -> ioredis
  • Loading branch information
iwpnd authored and vpriem committed Aug 21, 2023
1 parent 242f8bb commit 8606515
Show file tree
Hide file tree
Showing 19 changed files with 3,133 additions and 2,666 deletions.
5 changes: 2 additions & 3 deletions .github/workflows/ci.yml
Expand Up @@ -2,9 +2,9 @@ name: Node CI

on:
push:
branches: [main]
branches: [main, beta]
pull_request:
branches: [main]
branches: [main, beta]
workflow_dispatch:

jobs:
Expand Down Expand Up @@ -57,7 +57,6 @@ jobs:
run: docker compose down

release:
if: github.ref == 'refs/heads/main'
needs: build-lint-test
name: release
runs-on: ubuntu-latest
Expand Down
39 changes: 39 additions & 0 deletions CHANGELOG.md
@@ -1,3 +1,42 @@
## [2.0.0-beta.5](https://github.com/TierMobility/tile38-ts/compare/v2.0.0-beta.4...v2.0.0-beta.5) (2023-08-16)


### Bug Fixes

* 🐛 fix client defaults ([039fce9](https://github.com/TierMobility/tile38-ts/commit/039fce9ea19c9bd8762e4ce9f765a488655174d9))

## [2.0.0-beta.4](https://github.com/TierMobility/tile38-ts/compare/v2.0.0-beta.3...v2.0.0-beta.4) (2023-08-16)


### Documentation

* 📚️ document constructor ([9896edd](https://github.com/TierMobility/tile38-ts/commit/9896edd4113776e6a2322a79d0ac7ab3ac47e67c))

## [2.0.0-beta.3](https://github.com/TierMobility/tile38-ts/compare/v2.0.0-beta.2...v2.0.0-beta.3) (2023-08-16)


### Bug Fixes

* 🐛 handle ioredis quit better ([4809ffe](https://github.com/TierMobility/tile38-ts/commit/4809ffe4b84018463f907665e6d1942059a95bdb))

## [2.0.0-beta.2](https://github.com/TierMobility/tile38-ts/compare/v2.0.0-beta.1...v2.0.0-beta.2) (2023-08-15)


### Bug Fixes

* 🐛 align constructor with ioredis ([f62a4e6](https://github.com/TierMobility/tile38-ts/commit/f62a4e610f05dfaf6842c18e0dc4c1deb193904a))

## [2.0.0-beta.1](https://github.com/TierMobility/tile38-ts/compare/v1.3.2...v2.0.0-beta.1) (2023-08-15)


### ⚠ BREAKING CHANGES

* node-redis -> ioredis

### Features

* ♻️ change redis client to use ioredis instead of node-redis ([38904b4](https://github.com/TierMobility/tile38-ts/commit/38904b470623482afb29aeefc7005c0bbcc1705e))

## [1.3.2](https://github.com/TierMobility/tile38-ts/compare/v1.3.1...v1.3.2) (2023-08-11)


Expand Down
34 changes: 31 additions & 3 deletions README.md
Expand Up @@ -55,7 +55,7 @@ This is a Typescript client for Tile38 that allows for a type-safe interaction w

### Built With

- [redis](https://www.npmjs.com/package/redis)
- [ioredis](https://www.npmjs.com/package/ioredis)

## Getting Started

Expand All @@ -76,6 +76,7 @@ yarn add @tiermobility/tile38-ts

```typescript
import { Tile38 } from '@tiermobility/tile38-ts';

const tile38 = new Tile38('leader:9851');
```

Expand All @@ -88,16 +89,43 @@ This client is not meant to setup a replication, because this should happen in y
For now we allow for one follower `URI` to bet set alongside the leader `URI`.

```typescript
import { Tile38 } from '@tiermobility/tile38-ts';
const tile38 = new Tile38('leader:9851', 'follower:9851');
```

Once the client is instantiated with a follower, commands can be explicitly send to the follower, but adding `.follower()` to your command chaining.
Once the client is instantiated with a follower, commands can be explicitly send
to the follower, but adding `.follower()` to your command chaining.

```typescript
await tile38.follower().get('fleet', 'truck1').asObjects();
```

### Options

We expose `ioredis` [RedisOptions](https://redis.github.io/ioredis/index.html#RedisOptions).

```typescript
new Tile38(
'leader:9851',
'follower:9851',
// e.g. to set a retry strategy
{
retryStrategy: (times) => {
return Math.min(times * 50, 2000);
},
}
);
```

### Events

We expose `ioredis` [Events](https://github.com/redis/ioredis#events).

```typescript
new Tile38()
.on('connect', () => console.log('connected'))
.on('error', console.error);
```

### Pagination

Tile38 has hidden limits set for the amount of objects that can be returned in one request. For most queries/searches this limit is set to `100`. This client gives you the option to either paginate the results yourself by add `.cursor()` and `.limit()` to your queries, or it abstracts pagination away from the user by adding `.all()`.
Expand Down
4 changes: 2 additions & 2 deletions docker-compose.yml
Expand Up @@ -2,7 +2,7 @@ version: '3'

services:
tile38:
image: tile38/tile38:1.30.2
image: tile38/tile38:1.31.0
container_name: tile38
command: >
/bin/sh -c 'mkdir -p tmp/data && \
Expand All @@ -13,7 +13,7 @@ services:
- 9851:9851

tile38-follower:
image: tile38/tile38:1.30.2
image: tile38/tile38:1.31.0
container_name: tile38-follower
command: >
/bin/sh -c 'mkdir -p tmp/data && \
Expand Down
2 changes: 0 additions & 2 deletions jest.setup.js
@@ -1,4 +1,2 @@
module.exports = () => {
process.env.TILE38_URI =
process.env.TILE38_URI || 'redis://localhost:9851/';
};
6 changes: 3 additions & 3 deletions package.json
@@ -1,6 +1,6 @@
{
"name": "@tiermobility/tile38-ts",
"version": "1.3.2",
"version": "2.0.0-beta.5",
"description": "A Node.js Tile38 client written in TypeScript",
"main": "dist/index.js",
"files": [
Expand Down Expand Up @@ -41,14 +41,14 @@
"test": "jest --runInBand",
"format": "prettier --write src/{*.ts,**/*.ts}",
"u": "yarn upgrade-interactive --latest",
"up": "docker-compose up tile38",
"up": "docker-compose up",
"down": "docker-compose down",
"coverage": "FILE=./coverage/lcov-report/index.html; test -f $FILE && open $FILE || echo 'no coverage yet, run yarn test'"
},
"dependencies": {
"@types/node": "18.15.11",
"@vpriem/geojson": "1.1.0",
"redis": "4.6.7"
"ioredis": "5.3.2"
},
"devDependencies": {
"@commitlint/cli": "17.6.3",
Expand Down
6 changes: 5 additions & 1 deletion release.config.js
@@ -1,5 +1,9 @@
module.exports = {
branches: ['main'],
branches: [
'main',
{ name: 'beta', prerelease: true },
{ name: 'alpha', prerelease: true },
],
plugins: [
[
'@semantic-release/commit-analyzer',
Expand Down
124 changes: 52 additions & 72 deletions src/Client.ts
@@ -1,5 +1,6 @@
import EventEmitter from 'events';
import { RedisClientOptions, createClient } from 'redis';
import { Redis, RedisOptions } from 'ioredis';
import { forwardEvents } from './events';
import { parseResponse } from './parseResponse';
import { JSONResponse } from './responses';

Expand Down Expand Up @@ -83,73 +84,74 @@ export enum SubCommand {
SECTOR = 'SECTOR',
}

export type ConstructorArgs = (string | number | RedisOptions | undefined)[];

export type CommandArgs = Array<SubCommand | string | number | object>;

enum Format {
RESP = 'resp',
JSON = 'json',
}

type RedisClient = ReturnType<typeof createClient>;

const toString = (s: string | number): string =>
typeof s === 'string' ? s : `${s}`;

export class Client extends EventEmitter {
private redis: RedisClient;
const applyDefaults = (args: ConstructorArgs) => {
const options = args.find((arg) => typeof arg === 'object');
if (!options) return [...args, { port: 9851, lazyConnect: true }];
return args.map((arg) =>
typeof arg === 'object'
? { port: 9851, ...arg, lazyConnect: true }
: arg
);
};

private redisConnecting?: Promise<void>;
const catchConnectionClosed = (error: Error) => {
if (error.message !== 'Connection is closed.') throw error;
};

private subscriber: RedisClient;
export class Client extends EventEmitter {
private redis: Redis;

private subscriberConnecting?: Promise<void>;
private subscriber: Redis;

private format: `${Format}` = Format.RESP;

constructor(url: string, options?: RedisClientOptions) {
constructor(...args: ConstructorArgs) {
super();

this.redis = createClient({ ...options, url })
this.redis = new Redis(...(applyDefaults(args) as [string]))
.on('ready', () => {
this.format = Format.RESP;
})
.on('error', (error) => {
/* istanbul ignore next */
this.emit('error', error);
})
.on('end', () => {
this.format = Format.RESP;
});

this.subscriber = this.redis.duplicate().on('error', (error) => {
/* istanbul ignore next */
this.emit('error', error);
});
}

private connect(): Promise<void> {
if (typeof this.redisConnecting === 'undefined') {
this.redisConnecting = this.redis.connect();
}

return this.redisConnecting;
}

private connectSubscriber(): Promise<void> {
if (typeof this.subscriberConnecting === 'undefined') {
this.subscriberConnecting = this.subscriber.connect();
}
forwardEvents(this.redis, this);

return this.subscriberConnecting;
this.subscriber = this.redis
.duplicate()
.on('error', (error) =>
/* istanbul ignore next */
this.emit('error', error)
)
.on('message', (channel, message) =>
this.emit('message', JSON.parse(message), channel)
)
.on('pmessage', (pattern, channel, message) =>
this.emit('message', JSON.parse(message), channel, pattern)
);
}

private async rawCommand(
command: string,
args?: CommandArgs
): Promise<string> {
await this.connect();

return this.redis.sendCommand([command, ...(args || []).map(toString)]);
return this.redis.call(
command,
...(args || []).map(toString)
) as Promise<string>;
}

async command<R extends JSONResponse = JSONResponse>(
Expand All @@ -176,51 +178,29 @@ export class Client extends EventEmitter {
}

async subscribe(channels: string | string[]): Promise<void> {
await this.connectSubscriber();

return this.subscriber.subscribe(channels, (message, channel) =>
this.emit('message', JSON.parse(message), channel)
);
await this.subscriber.subscribe(...channels);
}

async pSubscribe(patterns: string | string[]): Promise<void> {
await this.connectSubscriber();

return this.subscriber.pSubscribe(patterns, (message, channel) =>
this.emit('message', JSON.parse(message), channel)
);
await this.subscriber.psubscribe(...patterns);
}

unsubscribe(channels: string | string[] = []): Promise<void> {
return this.subscriber.unsubscribe(channels);
async unsubscribe(...channels: string[]): Promise<void> {
await this.subscriber.unsubscribe(...channels);
}

pUnsubscribe(patterns: string | string[] = []): Promise<void> {
return this.subscriber.unsubscribe(patterns);
async pUnsubscribe(...patterns: string[]): Promise<void> {
await this.subscriber.punsubscribe(...patterns);
}

async quit(force = false): Promise<void> {
await Promise.all([this.redisConnecting, this.subscriberConnecting]);

if (force) {
await Promise.all([
this.redis.isOpen && this.redis.disconnect(),
this.subscriber.isOpen && this.subscriber.disconnect(),
]);
} else {
/**
* Issue with node-redis v4
* We have to put back output to resp otherwise quit will take forever
*/
await (this.redis.isOpen && this.output('resp'));

await Promise.all([
this.redis.isOpen && this.redis.quit(),
this.subscriber.isOpen && this.subscriber.quit(),
]);
}

delete this.redisConnecting;
delete this.subscriberConnecting;
await Promise.all(
force
? [this.redis.disconnect(), this.subscriber.disconnect()]
: [
this.redis.quit().catch(catchConnectionClosed),
this.subscriber.quit().catch(catchConnectionClosed),
]
);
}
}

0 comments on commit 8606515

Please sign in to comment.