diff --git a/TODO.md b/TODO.md index 5d7a459337..fc63534adc 100644 --- a/TODO.md +++ b/TODO.md @@ -3,11 +3,11 @@ * Scan stream * PubSub * [`return_buffers`](https://github.com/NodeRedis/node-redis#options-object-properties) (? supported in v3, but have performance drawbacks) -* Support options in a command function (`.get`, `.set`, ...) +* ~~Support options in a command function (`.get`, `.set`, ...)~~ * Key prefixing (?) (partially supported in v3) ## Client -* Blocking Commands +* ~~Blocking Commands~~ * Events * ~~ready~~ * ~~connect~~ @@ -15,17 +15,17 @@ * ~~error~~ * ~~end~~ * warning (?) -* Select command +* ~~SELECT command~~ +* WATCH command ## Cluster * Retry strategy -* Random client iterator (to split the work between commands that are not bounded to a slot) +* ~~Random client iterator (to split the work between commands that are not bounded to a slot)~~ * Multi command * NAT mapping (AWS) -* Read/Write splitting configurations - * master(RW) - * master(RW) & slaves(R) - * choose automatically (?) +* ~~Read/Write splitting configurations~~ + * ~~master(RW)~~ + * ~~master(RW) & slaves(R)~~ * optionally filtered master(RW) & optionally filtered slaves(R) (?) ## Lua Scripts diff --git a/lib/client.spec.ts b/lib/client.spec.ts index efca816706..1a30536083 100644 --- a/lib/client.spec.ts +++ b/lib/client.spec.ts @@ -1,7 +1,8 @@ import { strict as assert } from 'assert'; import { once } from 'events'; -import { TestRedisServers, TEST_REDIS_SERVERS, itWithClient } from './test-utils'; +import { itWithClient, TEST_REDIS_SERVERS, TestRedisServers } from './test-utils'; import RedisClient from './client'; +import { AbortError } from './errors'; describe('Client', () => { describe('authentication', () => { @@ -31,6 +32,46 @@ describe('Client', () => { }); }); + describe('callbackify', () => { + const client = RedisClient.create({ + socket: TEST_REDIS_SERVERS[TestRedisServers.OPEN], + callbackify: true + }); + + before(() => client.connect()); + after(async () => { + await (client as any).flushAllAsync(); + await client.disconnect(); + }); + + it('client.{command} should call the callback', done => { + (client as any).ping((err: Error, reply: string) => { + if (err) { + return done(err); + } + + try { + assert.equal(reply, 'PONG'); + done(); + } catch (err) { + done(err); + } + }); + }); + + it('client.{command} should work without callback', async () => { + (client as any).ping(); + await (client as any).pingAsync(); // make sure the first command was replied + }); + + it('client.{command}Async should return a promise', async () => { + assert.equal( + await (client as any).pingAsync(), + 'PONG' + ); + }); + }); + describe('events', () => { it('connect, ready, end', async () => { const client = RedisClient.create({ @@ -38,26 +79,42 @@ describe('Client', () => { }); await Promise.all([ - assert.doesNotReject(client.connect()), - assert.doesNotReject(once(client, 'connect')), - assert.doesNotReject(once(client, 'ready')) + client.connect(), + once(client, 'connect'), + once(client, 'ready') ]); await Promise.all([ - assert.doesNotReject(client.disconnect()), - assert.doesNotReject(once(client, 'end')) + client.disconnect(), + once(client, 'end') ]); }); }); - it('sendCommand', async () => { - const client = RedisClient.create({ - socket: TEST_REDIS_SERVERS[TestRedisServers.OPEN] + describe('sendCommand', () => { + itWithClient(TestRedisServers.OPEN, 'PING', async client => { + assert.equal(await client.sendCommand(['PING']), 'PONG'); }); - await client.connect(); - assert.equal(await client.sendCommand(['PING']), 'PONG'); - await client.disconnect(); + describe('AbortController', () => { + itWithClient(TestRedisServers.OPEN, 'success', async client => { + await client.sendCommand(['PING'], { + signal: new AbortController().signal + }); + }); + + itWithClient(TestRedisServers.OPEN, 'AbortError', async client => { + const controller = new AbortController(); + controller.abort(); + + await assert.rejects( + client.sendCommand(['PING'], { + signal: controller.signal + }), + AbortError + ); + }); + }); }); describe('multi', () => { @@ -71,5 +128,40 @@ describe('Client', () => { ['PONG', 'OK', 'value'] ); }); + + itWithClient(TestRedisServers.OPEN, 'should reject the whole chain on error', async client => { + client.on('error', () => { + // ignore errors + }); + + await assert.rejects( + client.multi() + .ping() + .addCommand(['DEBUG', 'RESTART']) + .ping() + .exec() + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'should reconnect after DEBUG RESTART', async client => { + client.on('error', () => { + // ignore errors + }); + + await client.sendCommand(['CLIENT', 'SETNAME', 'client']); + await assert.rejects(client.sendCommand(['DEBUG', 'RESTART'])); + assert.ok(await client.sendCommand(['CLIENT', 'GETNAME']) === null); + }); + + itWithClient(TestRedisServers.OPEN, 'should SELECT db after reconnection', async client => { + client.on('error', () => { + // ignore errors + }); + + await client.select(1); + await client.set('key', 'value'); + await assert.rejects(client.sendCommand(['DEBUG', 'RESTART'])); + // assert.equal(await client.get('key'), 'value'); }); }); \ No newline at end of file diff --git a/lib/client.ts b/lib/client.ts index 92e82578d3..83b98e5a32 100644 --- a/lib/client.ts +++ b/lib/client.ts @@ -1,17 +1,21 @@ import RedisSocket, { RedisSocketOptions } from './socket'; -import RedisCommandsQueue, { AddCommandOptions } from './commands-queue'; +import RedisCommandsQueue, { QueueCommandOptions } from './commands-queue'; import COMMANDS from './commands/client'; import { RedisCommand, RedisModules, RedisModule, RedisReply } from './commands'; import RedisMultiCommand, { MultiQueuedCommand, RedisMultiCommandType } from './multi-command'; import EventEmitter from 'events'; +import { commandOptions, CommandOptions, isCommandOptions } from './command-options'; export interface RedisClientOptions { socket?: RedisSocketOptions; modules?: M; commandsQueueMaxLength?: number; + readOnly?: boolean; + callbackify?: boolean; } -export type RedisCommandSignature = (...args: Parameters) => Promise>; +export type RedisCommandSignature = + (...args: Parameters | [options: CommandOptions, ...rest: Parameters]) => Promise>; type WithCommands = { [P in keyof typeof COMMANDS]: RedisCommandSignature<(typeof COMMANDS)[P]>; @@ -23,23 +27,54 @@ type WithModules> = { export type RedisClientType = WithCommands & WithModules & RedisClient; +export interface ClientCommandOptions extends QueueCommandOptions { + duplicateConnection?: boolean; +} + export default class RedisClient extends EventEmitter { static defineCommand(on: any, name: string, command: RedisCommand): void { on[name] = async function (...args: Array): Promise { + const options = isCommandOptions(args[0]) && args.shift(); return command.transformReply( - await this.sendCommand(command.transformArguments(...args)) + await this.sendCommand( + command.transformArguments(...args), + options + ) ); }; } + static callbackifyCommand(on: any, name: string): void { + const originalFunction = on[name + 'Async'] = on[name]; + on[name] = function (...args: Array) { + const hasCallback = typeof args[args.length - 1] === 'function', + callback = (hasCallback && args.pop()) as Function; + + const promise = originalFunction.apply(this, args); + if (hasCallback) { + promise + .then((reply: RedisReply) => callback(null, reply)) + .catch((err: Error) => callback(err)); + } else { + promise + .catch((err: Error) => this.emit('error', err)); + } + }; + } + static create(options?: RedisClientOptions): RedisClientType { return new RedisClient(options); } + static commandOptions(options: ClientCommandOptions): CommandOptions { + return commandOptions(options); + }; + + readonly #options?: RedisClientOptions; readonly #socket: RedisSocket; readonly #queue: RedisCommandsQueue; readonly #Multi: typeof RedisMultiCommand & { new(): RedisMultiCommandType }; - readonly #modules?: M; + #selectedDB = 0; get isOpen(): boolean { return this.#socket.isOpen; @@ -47,31 +82,51 @@ export default class RedisClient extends constructor(options?: RedisClientOptions) { super(); - this.#socket = this.#initiateSocket(options?.socket); - this.#queue = this.#initiateQueue(options?.commandsQueueMaxLength); + this.#options = options; + this.#socket = this.#initiateSocket(); + this.#queue = this.#initiateQueue(); this.#Multi = this.#initiateMulti(); - this.#modules = this.#initiateModules(options?.modules); + this.#initiateModules(); + this.#callbackify(); } - #initiateSocket(socketOptions?: RedisSocketOptions): RedisSocket { + #initiateSocket(): RedisSocket { const socketInitiator = async (): Promise => { - if (socketOptions?.password) { - await (this as any).auth(socketOptions); + const promises = []; + + if (this.#options?.socket?.password) { + promises.push((this as any).auth(this.#options?.socket)); } + + if (this.#options?.readOnly) { + promises.push((this as any).readOnly()); + } + + if (this.#selectedDB !== 0) { + promises.push((this as any).select(this.#selectedDB)); + } + + await Promise.all(promises); }; - return new RedisSocket(socketInitiator, socketOptions) + return new RedisSocket(socketInitiator, this.#options?.socket) .on('data', data => this.#queue.parseResponse(data)) - .on('error', err => this.emit('error', err)) + .on('error', err => { + this.emit('error', err); + this.#queue.flushWaitingForReply(err); + }) .on('connect', () => this.emit('connect')) - .on('ready', () => this.emit('ready')) + .on('ready', () => { + this.emit('ready'); + this.#tick(); + }) .on('reconnecting', () => this.emit('reconnecting')) .on('end', () => this.emit('end')); } - #initiateQueue(maxLength: number | null | undefined): RedisCommandsQueue { + #initiateQueue(): RedisCommandsQueue { return new RedisCommandsQueue( - maxLength, + this.#options?.commandsQueueMaxLength, (encodedCommands: string) => this.#socket.write(encodedCommands) ); } @@ -90,7 +145,7 @@ export default class RedisClient extends return (replies[replies.length - 1] as Array); }; - const modules = this.#modules; + const modules = this.#options?.modules; return class extends RedisMultiCommand { constructor() { @@ -99,31 +154,68 @@ export default class RedisClient extends }; } - #initiateModules(modules?: M): M | undefined { - if (!modules) return; + #initiateModules(): void { + if (!this.#options?.modules) return; - for (const m of modules) { + for (const m of this.#options.modules) { for (const [name, command] of Object.entries(m)) { RedisClient.defineCommand(this, name, command); this.#Multi.defineCommand(this.#Multi, name, command); } } + } - return modules; + #callbackify(): void { + if (!this.#options?.callbackify) return; + + for (const name of Object.keys(COMMANDS)) { + RedisClient.callbackifyCommand(this, name); + RedisClient.callbackifyCommand(this.#Multi.prototype, name); + } + + if (!this.#options?.modules) return; + + for (const m of this.#options.modules) { + for (const name of Object.keys(m)) { + RedisClient.callbackifyCommand(this, name); + RedisClient.callbackifyCommand(this.#Multi.prototype, name); + } + } + } + + duplicate(): RedisClientType { + return RedisClient.create(this.#options); } async connect(): Promise { await this.#socket.connect(); - - this.#tick(); } - sendCommand(args: Array, options?: AddCommandOptions): Promise { + async SELECT(db: number): Promise { + await this.sendCommand(['SELECT', db.toString()]); + this.#selectedDB = db; + } + + select = this.SELECT; + + async sendCommand(args: Array, options?: ClientCommandOptions): Promise { + if (options?.duplicateConnection) { + const duplicate = this.duplicate(); + await duplicate.connect(); + + try { + return await duplicate.sendCommand(args, { + ...options, + duplicateConnection: false + }); + } finally { + await duplicate.disconnect(); + } + } + const promise = this.#queue.addCommand(args, options); - this.#tick(); - - return promise; + return await promise; } multi(): RedisMultiCommandType { @@ -131,6 +223,7 @@ export default class RedisClient extends } disconnect(): Promise { + this.#queue.flushAll(new Error('Disconnecting')); return this.#socket.disconnect(); } diff --git a/lib/cluster-slots.ts b/lib/cluster-slots.ts index 243b8609fb..328522a7f4 100644 --- a/lib/cluster-slots.ts +++ b/lib/cluster-slots.ts @@ -1,35 +1,45 @@ import calculateSlot from 'cluster-key-slot'; import RedisClient from './client'; import { RedisSocketOptions } from './socket'; -import { RedisClusterNode } from './commands/CLUSTER_NODES'; +import { RedisClusterMasterNode, RedisClusterReplicaNode } from './commands/CLUSTER_NODES'; import { RedisClusterOptions } from './cluster'; +interface SlotClients { + master: RedisClient; + replicas: Array; + iterator: IterableIterator | undefined; +} + export default class RedisClusterSlots { readonly #options: RedisClusterOptions; readonly #clientByKey = new Map(); - readonly #slots: Array = []; + readonly #slots: Array = []; constructor(options: RedisClusterOptions) { this.#options = options; } async connect(): Promise { - // TODO: if connected use a random client? for (const rootNode of this.#options.rootNodes) { try { await this.#discoverNodes(rootNode); + return; } catch (err) { // this.emit('error', err); } } - throw new Error('None of the root nodes was available'); + throw new Error('None of the root nodes is available'); + } + + async discover(): Promise { + // TODO + throw new Error('None of the cluster node is available'); } async #discoverNodes(socketOptions: RedisSocketOptions) { const client = RedisClient.create({ - socket: socketOptions, - modules: this.#options?.modules + socket: socketOptions }); await client.connect(); @@ -41,23 +51,23 @@ export default class RedisClusterSlots { } } - async #reset(nodes: Array): Promise { + async #reset(masters: Array): Promise { // Override this.#slots and add not existing clients to this.#clientByKey - const promises = [], - clientsInUse = new Set(); - for (const {url, slots} of nodes) { - clientsInUse.add(url); + const promises: Array> = [], + clientsInUse = new Set(); + for (const master of masters) { + const masterClient = this.#initiateClientForNode(master, false, clientsInUse, promises), + replicasClients = this.#options.useReplicas ? + master.replicas.map(replica => this.#initiateClientForNode(replica, true, clientsInUse, promises)) : + []; - let client = this.#clientByKey.get(url); - if (!client) { - // TODO: client configuration - client = RedisClient.create(); - promises.push(client.connect()); - } - - for (const slot of slots) { + for (const slot of master.slots) { for (let i = slot.from; i < slot.to; i++) { - this.#slots[i] = client; + this.#slots[i] = { + master: masterClient, + replicas: replicasClients, + iterator: undefined // will be initiated in use + }; } } } @@ -72,8 +82,51 @@ export default class RedisClusterSlots { } } - #getSlotClient(slot: number): RedisClient { - return this.#slots[slot]; + #initiateClientForNode(node: RedisClusterMasterNode | RedisClusterReplicaNode, readOnly: boolean, clientsInUse: Set, promises: Array>): RedisClient { + clientsInUse.add(node.url); + + let client = this.#clientByKey.get(node.url); + if (!client) { + client = RedisClient.create({ + socket: { + host: node.host, + port: node.port + }, + readOnly + }); + promises.push(client.connect()); + this.#clientByKey.set(node.url, client); + } + + return client; + } + + #getSlotMaster(slot: number): RedisClient { + return this.#slots[slot].master; + } + + *#slotIterator(slotNumber: number): IterableIterator { + const slot = this.#slots[slotNumber]; + yield slot.master; + + for (const replica of slot.replicas) { + yield replica; + } + } + + #getSlotClient(slotNumber: number): RedisClient { + const slot = this.#slots[slotNumber]; + if (!slot.iterator) { + slot.iterator = this.#slotIterator(slotNumber); + } + + const {done, value} = slot.iterator.next(); + if (done) { + slot.iterator = undefined; + return this.#getSlotClient(slotNumber); + } + + return value; } #randomClientIterator?: IterableIterator; @@ -96,17 +149,25 @@ export default class RedisClusterSlots { return value; } - getClient(firstKey?: string): RedisClient { + getClient(firstKey?: string, isReadOnly?: boolean): RedisClient { if (!firstKey) { return this.#getRandomClient(); } - return this.#getSlotClient(calculateSlot(firstKey)); + const slot = calculateSlot(firstKey); + if (!isReadOnly || !this.#options.useReplicas) { + return this.#getSlotMaster(slot); + } + + return this.#getSlotClient(slot); } async disconnect(): Promise { await Promise.all( [...this.#clientByKey.values()].map(client => client.disconnect()) ); + + this.#clientByKey.clear(); + this.#slots.splice(0); } } diff --git a/lib/cluster.spec.ts b/lib/cluster.spec.ts index f68215c066..8490940c24 100644 --- a/lib/cluster.spec.ts +++ b/lib/cluster.spec.ts @@ -5,7 +5,8 @@ describe.skip('Cluster', () => { const cluster = RedisCluster.create({ rootNodes: [{ port: 30001 - }] + }], + useReplicas: true }); await cluster.connect(); @@ -14,6 +15,10 @@ describe.skip('Cluster', () => { await cluster.set('a', 'b'); await cluster.set('a{a}', 'bb'); await cluster.set('aa', 'bb'); + await cluster.get('aa'); + await cluster.get('aa'); + await cluster.get('aa'); + await cluster.get('aa'); await cluster.disconnect(); }); diff --git a/lib/cluster.ts b/lib/cluster.ts index ab2375e1bf..f4f973780a 100644 --- a/lib/cluster.ts +++ b/lib/cluster.ts @@ -7,6 +7,8 @@ import RedisClusterSlots from './cluster-slots'; export interface RedisClusterOptions { rootNodes: Array; modules?: M; + useReplicas?: boolean; + maxCommandRedirections?: number; } type WithCommands = { @@ -23,11 +25,11 @@ export default class RedisCluster { static defineCommand(on: any, name: string, command: RedisCommand): void { on[name] = async function (...args: Array): Promise { const transformedArguments = command.transformArguments(...args); - return command.transformReply( await this.sendCommand( transformedArguments, - command.FIRST_KEY_INDEX + command.FIRST_KEY_INDEX, + command.IS_READ_ONLY ) ); }; @@ -37,9 +39,11 @@ export default class RedisCluster { return new RedisCluster(options); } + readonly #options: RedisClusterOptions; readonly #slots: RedisClusterSlots; constructor(options: RedisClusterOptions) { + this.#options = options; this.#slots = new RedisClusterSlots(options); } @@ -47,10 +51,25 @@ export default class RedisCluster { return this.#slots.connect(); } - sendCommand(args: Array, firstKeyIndex?: number): Promise { - const firstKey = firstKeyIndex ? args[firstKeyIndex] : undefined; - return this.#slots.getClient(firstKey) - .sendCommand(args); + async sendCommand(args: Array, firstKeyIndex?: number, isReadOnly?: boolean, redirections: number = 0): Promise { + const firstKey = firstKeyIndex ? args[firstKeyIndex] : undefined, + client = this.#slots.getClient(firstKey, isReadOnly); + + try { + return await client.sendCommand(args); + } catch (err) { + if (err.message.startsWith('ASK')) { + // TODO + } else if (err.message.startsWith('MOVED')) { + await this.#slots.discover(); + + if (redirections < (this.#options.maxCommandRedirections ?? 16)) { + return this.sendCommand(args, firstKeyIndex, isReadOnly, redirections + 1); + } + } + + throw err; + } } disconnect(): Promise { diff --git a/lib/command-options.ts b/lib/command-options.ts new file mode 100644 index 0000000000..f0a0cb4691 --- /dev/null +++ b/lib/command-options.ts @@ -0,0 +1,14 @@ +export type CommandOptions = T & { + options: never; +}; + +const set = new WeakSet(); + +export function commandOptions(options: T): CommandOptions { + set.add(options); + return options as CommandOptions; +} + +export function isCommandOptions(options: any): options is CommandOptions { + return set.delete(options); +} diff --git a/lib/commands-queue.ts b/lib/commands-queue.ts index 6659cbd9aa..1f5b6292ca 100644 --- a/lib/commands-queue.ts +++ b/lib/commands-queue.ts @@ -1,7 +1,8 @@ -import LinkedList from 'yallist'; +import LinkedList, { Node } from 'yallist'; import RedisParser from 'redis-parser'; +import { AbortError } from './errors'; -export interface AddCommandOptions { +export interface QueueCommandOptions { asap?: boolean; signal?: AbortSignal; chainId?: Symbol; @@ -73,17 +74,19 @@ export default class RedisCommandsQueue { undefined; } - addCommand(args: Array, options?: AddCommandOptions): Promise { + addCommand(args: Array, options?: QueueCommandOptions): Promise { return this.#isQueueFull() || this.addEncodedCommand( RedisCommandsQueue.encodeCommand(args), options ); } - addEncodedCommand(encodedCommand: string, options?: AddCommandOptions): Promise { + addEncodedCommand(encodedCommand: string, options?: QueueCommandOptions): Promise { const fullQueuePromise = this.#isQueueFull(); if (fullQueuePromise) { return fullQueuePromise; + } else if (options?.signal?.aborted) { + return Promise.reject(new AbortError()); } return new Promise((resolve, reject) => { @@ -95,14 +98,20 @@ export default class RedisCommandsQueue { }); if (options?.signal) { + const listener = () => { + this.#waitingToBeSent.removeNode(node); + node.value.reject(new AbortError()); + }; + + if (options.signal.aborted) { + return listener(); + } + node.value.abort = { signal: options.signal, - listener: () => { - this.#waitingToBeSent.removeNode(node); - node.value.reject(new Error('The command was aborted')); - } + listener }; - options.signal.addEventListener('abort', node.value.abort.listener, { + options.signal.addEventListener('abort', listener, { once: true }); } @@ -119,8 +128,8 @@ export default class RedisCommandsQueue { if (!this.#waitingToBeSent.length) return; const encoded: Array = []; - let size = 0; - let lastCommandChainId: Symbol | undefined; + let size = 0, + lastCommandChainId: Symbol | undefined; for (const {encodedCommand, chainId} of this.#waitingToBeSent) { encoded.push(encodedCommand); size += encodedCommand.length; @@ -130,6 +139,12 @@ export default class RedisCommandsQueue { } } + if (!lastCommandChainId && encoded.length === this.#waitingToBeSent.length) { + lastCommandChainId = (this.#waitingToBeSent.tail as Node).value.chainId; + } + + lastCommandChainId ??= this.#waitingToBeSent.tail?.value.chainId; + this.#executor(encoded.join('')); for (let i = 0; i < encoded.length; i++) { @@ -173,7 +188,8 @@ export default class RedisCommandsQueue { this.#chainInExecution = undefined; } - flushWaitingToBeSent(err: Error): void { + flushAll(err: Error): void { + RedisCommandsQueue.#flushQueue(this.#waitingForReply, err); RedisCommandsQueue.#flushQueue(this.#waitingToBeSent, err); } }; diff --git a/lib/commands/BLPOP.spec.ts b/lib/commands/BLPOP.spec.ts new file mode 100644 index 0000000000..1939455935 --- /dev/null +++ b/lib/commands/BLPOP.spec.ts @@ -0,0 +1,40 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './BLPOP'; +import RedisClient from '../client'; + +describe('BLPOP', () => { + describe('transformArguments', () => { + it('single', () => { + assert.deepEqual( + transformArguments('key', 0), + ['BLPOP', 'key', '0'] + ); + }); + + it('multiple', () => { + assert.deepEqual( + transformArguments(['key1', 'key2'], 0), + ['BLPOP', 'key1', 'key2', '0'] + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.blPop', async client => { + const [popReply, pushReply] = await Promise.all([ + client.blPop(RedisClient.commandOptions({ + duplicateConnection: true + }), 'key', 0), + client.lPush('key', ['1', '2']) + ]); + + assert.deepEqual( + popReply, + ['key', '2'] + ); + + client.blPop() + + assert.equal(pushReply, 2); + }); +}); diff --git a/lib/commands/BLPOP.ts b/lib/commands/BLPOP.ts new file mode 100644 index 0000000000..eaaac886c3 --- /dev/null +++ b/lib/commands/BLPOP.ts @@ -0,0 +1,21 @@ +export const FIRST_KEY_INDEX = 0; + +export function transformArguments(keys: string | Array, timeout: number): Array { + const args = ['BLPOP']; + + if (typeof keys === 'string') { + args.push(keys); + } else { + args.push(...keys); + } + + args.push(timeout.toString()); + + return args; +} + +type BLPOPReply = [list: string, value: string]; + +export function transformReply(reply: BLPOPReply): BLPOPReply { + return reply; +} diff --git a/lib/commands/CLUSTER_NODES.spec.ts b/lib/commands/CLUSTER_NODES.spec.ts index 4fb2767918..2b3881d8cd 100644 --- a/lib/commands/CLUSTER_NODES.spec.ts +++ b/lib/commands/CLUSTER_NODES.spec.ts @@ -20,8 +20,10 @@ describe('CLUSTER NODES', () => { [{ id: 'master', url: '127.0.0.1:30001@31001', + host: '127.0.0.1', + port: 30001, + cport: 31001, flags: ['myself', 'master'], - master: null, pingSent: 0, pongRecv: 0, configEpoch: 1, @@ -29,17 +31,19 @@ describe('CLUSTER NODES', () => { slots: [{ from: 0, to: 16384 + }], + replicas: [{ + id: 'slave', + url: '127.0.0.1:30002@31002', + host: '127.0.0.1', + port: 30002, + cport: 31002, + flags: ['slave'], + pingSent: 0, + pongRecv: 0, + configEpoch: 1, + linkState: RedisClusterNodeLinkStates.CONNECTED }] - }, { - id: 'slave', - url: '127.0.0.1:30002@31002', - flags: ['slave'], - master: 'master', - pingSent: 0, - pongRecv: 0, - configEpoch: 1, - linkState: RedisClusterNodeLinkStates.CONNECTED, - slots: [] }] ); }); @@ -47,18 +51,21 @@ describe('CLUSTER NODES', () => { it.skip('with importing slots', () => { assert.deepEqual( transformReply( - 'id 127.0.0.1:30001@31001 master - 0 0 0 connected 0-<-16384' + 'id 127.0.0.1:30001@31001 master - 0 0 0 connected 0-<-16384\n' ), [{ id: 'id', url: '127.0.0.1:30001@31001', + host: '127.0.0.1', + port: 30001, + cport: 31001, flags: ['master'], - master: null, pingSent: 0, pongRecv: 0, configEpoch: 0, linkState: RedisClusterNodeLinkStates.CONNECTED, - slots: [] // TODO + slots: [], // TODO + replicas: [] }] ); }); @@ -66,18 +73,21 @@ describe('CLUSTER NODES', () => { it.skip('with migrating slots', () => { assert.deepEqual( transformReply( - 'id 127.0.0.1:30001@31001 master - 0 0 0 connected 0->-16384' + 'id 127.0.0.1:30001@31001 master - 0 0 0 connected 0->-16384\n' ), [{ id: 'id', url: '127.0.0.1:30001@31001', + host: '127.0.0.1', + port: 30001, + cport: 31001, flags: ['master'], - master: null, pingSent: 0, pongRecv: 0, configEpoch: 0, linkState: RedisClusterNodeLinkStates.CONNECTED, - slots: [] // TODO + slots: [], // TODO + replicas: [] }] ); }); diff --git a/lib/commands/CLUSTER_NODES.ts b/lib/commands/CLUSTER_NODES.ts index 4902523de3..dd2bab36df 100644 --- a/lib/commands/CLUSTER_NODES.ts +++ b/lib/commands/CLUSTER_NODES.ts @@ -7,44 +7,90 @@ export enum RedisClusterNodeLinkStates { DISCONNECTED = 'disconnected' } -export interface RedisClusterNode { +interface RedisClusterNodeTransformedUrl { + host: string; + port: number; + cport: number; +} + +export interface RedisClusterReplicaNode extends RedisClusterNodeTransformedUrl { id: string; url: string; flags: Array, - master: string | null; pingSent: number; pongRecv: number; configEpoch: number; linkState: RedisClusterNodeLinkStates; +} + +export interface RedisClusterMasterNode extends RedisClusterReplicaNode { slots: Array<{ from: number, to: number - }> + }>; + replicas: Array; } -export function transformReply(reply: string): Array { +export function transformReply(reply: string): Array { const lines = reply.split('\n'); lines.pop(); // last line is empty - return lines.map(line => { - const [id, url, flags, master, pingSent, pongRecv, configEpoch, linkState, ...slots] = line.split(' '); - return { - id, - url, - flags: flags.split(','), - master: master === '-' ? null : master, - pingSent: Number(pingSent), - pongRecv: Number(pongRecv), - configEpoch: Number(configEpoch), - linkState: (linkState as RedisClusterNodeLinkStates), - slots: slots.map(slot => { - // TODO: importing & exporting (https://redis.io/commands/cluster-nodes#special-slot-entries) - const [fromString, toString] = slot.split('-', 2), - from = Number(fromString); - return { - from, - to: toString ? Number(toString) : from - }; - }) - }; - }); + + const mastersMap = new Map(), + replicasMap = new Map>(); + + for (const line of lines) { + const [id, url, flags, masterId, pingSent, pongRecv, configEpoch, linkState, ...slots] = line.split(' '), + node = { + id, + url, + ...transformNodeUrl(url), + flags: flags.split(','), + pingSent: Number(pingSent), + pongRecv: Number(pongRecv), + configEpoch: Number(configEpoch), + linkState: (linkState as RedisClusterNodeLinkStates) + }; + + if (masterId === '-') { + let replicas = replicasMap.get(id); + if (!replicas) { + replicas = []; + replicasMap.set(id, replicas); + } + + mastersMap.set(id, { + ...node, + slots: slots.map(slot => { + // TODO: importing & exporting (https://redis.io/commands/cluster-nodes#special-slot-entries) + const [fromString, toString] = slot.split('-', 2), + from = Number(fromString); + return { + from, + to: toString ? Number(toString) : from + }; + }), + replicas + }); + } else { + const replicas = replicasMap.get(masterId); + if (!replicas) { + replicasMap.set(masterId, [node]); + } else { + replicas.push(node); + } + } + } + + return [...mastersMap.values()]; +} + +function transformNodeUrl(url: string): RedisClusterNodeTransformedUrl { + const indexOfColon = url.indexOf(':'), + indexOfAt = url.indexOf('@', indexOfColon); + + return { + host: url.substring(0, indexOfColon), + port: Number(url.substring(indexOfColon + 1, indexOfAt)), + cport: Number(url.substring(indexOfAt + 1)) + }; } diff --git a/lib/commands/DEL.spec.ts b/lib/commands/DEL.spec.ts index 6124c97897..30a2c286b0 100644 --- a/lib/commands/DEL.spec.ts +++ b/lib/commands/DEL.spec.ts @@ -1,12 +1,20 @@ import { strict as assert } from 'assert'; +import RedisClient from '../client'; import { TestRedisServers, itWithClient } from '../test-utils'; import { transformArguments } from './DEL'; describe('DEL', () => { describe('transformArguments', () => { - it('multiple keys', () => { + it('string', () => { assert.deepEqual( - transformArguments('key1', 'key2'), + transformArguments('key'), + ['DEL', 'key'] + ); + }); + + it('array', () => { + assert.deepEqual( + transformArguments(['key1', 'key2']), ['DEL', 'key1', 'key2'] ); }); @@ -14,7 +22,7 @@ describe('DEL', () => { itWithClient(TestRedisServers.OPEN, 'client.del', async client => { assert.equal( - await client.del('key1', 'key2'), + await client.del('key'), 0 ); }); diff --git a/lib/commands/DEL.ts b/lib/commands/DEL.ts index c4dd6e73ad..c1c223a4cd 100644 --- a/lib/commands/DEL.ts +++ b/lib/commands/DEL.ts @@ -1,7 +1,15 @@ import { transformReplyNumber } from './generic-transformers'; -export function transformArguments(...keys: Array): Array { - return ['DEL', ...keys]; +export function transformArguments(keys: string | Array): Array { + const args = ['DEL']; + + if (typeof keys === 'string') { + args.push(keys); + } else { + args.push(...keys); + } + + return args; } export const transformReply = transformReplyNumber; diff --git a/lib/commands/GET.ts b/lib/commands/GET.ts index 2ed1c0dc36..714ad953d8 100644 --- a/lib/commands/GET.ts +++ b/lib/commands/GET.ts @@ -2,6 +2,8 @@ import { transformReplyString } from './generic-transformers'; export const FIRST_KEY_INDEX = 1; +export const IS_READ_ONLY = true; + export function transformArguments(key: string): Array { return ['GET', key]; } diff --git a/lib/commands/LPUSH.ts b/lib/commands/LPUSH.ts new file mode 100644 index 0000000000..8b2699793b --- /dev/null +++ b/lib/commands/LPUSH.ts @@ -0,0 +1,20 @@ +import { transformReplyNumber } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 0; + +export function transformArguments(key: string, elements: string | Array): Array { + const args = [ + 'LPUSH', + key + ]; + + if (typeof elements === 'string') { + args.push(elements); + } else { + args.push(...elements); + } + + return args; +} + +export const transformReply = transformReplyNumber; diff --git a/lib/commands/READONLY.ts b/lib/commands/READONLY.ts new file mode 100644 index 0000000000..00fbe4e435 --- /dev/null +++ b/lib/commands/READONLY.ts @@ -0,0 +1,7 @@ +import { transformReplyString } from './generic-transformers'; + +export function transformArguments(): Array { + return ['READONLY']; +} + +export const transformReply = transformReplyString; diff --git a/lib/commands/index.ts b/lib/commands/index.ts index 8a74a3e096..0712ecac30 100644 --- a/lib/commands/index.ts +++ b/lib/commands/index.ts @@ -1,5 +1,6 @@ import * as APPEND from './APPEND'; import * as AUTH from './AUTH'; +import * as BLPOP from './BLPOP'; import * as CLUSTER_NODES from './CLUSTER_NODES'; import * as COPY from './COPY'; import * as DECR from './DECR'; @@ -25,7 +26,9 @@ import * as INCR from './INCR'; import * as INCRBY from './INCRBY'; import * as INCRBYFLOAT from './INCRBYFLOAT'; import * as KEYS from './KEYS'; +import * as LPUSH from './LPUSH'; import * as PING from './PING'; +import * as READONLY from './READONLY'; import * as SET from './SET'; export default { @@ -33,6 +36,8 @@ export default { append: APPEND, AUTH, auth: AUTH, + BLPOP, + blPop: BLPOP, CLUSTER_NODES, clusterNodes: CLUSTER_NODES, COPY, @@ -83,8 +88,12 @@ export default { incrByFloat: INCRBYFLOAT, KEYS, keys: KEYS, + LPUSH, + lPush: LPUSH, PING, ping: PING, + READONLY, + readOnly: READONLY, SET, set: SET }; @@ -93,6 +102,7 @@ export type RedisReply = string | number | Array | null | undefined; export interface RedisCommand { FIRST_KEY_INDEX?: number; + IS_READ_ONLY?: boolean; transformArguments(...args: Array): Array; transformReply(reply: RedisReply): any; } diff --git a/lib/errors.ts b/lib/errors.ts new file mode 100644 index 0000000000..9040d7b589 --- /dev/null +++ b/lib/errors.ts @@ -0,0 +1,5 @@ +export class AbortError extends Error { + constructor() { + super('The command was aborted'); + } +} diff --git a/lib/socket.ts b/lib/socket.ts index b400f0e3a6..586d02242a 100644 --- a/lib/socket.ts +++ b/lib/socket.ts @@ -101,24 +101,34 @@ export default class RedisSocket extends EventEmitter { } this.#isOpen = true; - this.#socket = await this.#retryConnection(0); - this.emit('connect'); - - if (!this.#initiator) return; try { - await this.#initiator(); - this.emit('ready'); + await this.#connect(); } catch (err) { this.#isOpen = false; - this.#socket.end(); - this.#socket = undefined; throw err; } } - async #retryConnection(retries: number): Promise { - if (retries > 0 || this.#socket) { + async #connect(hadError?: boolean): Promise { + this.#socket = await this.#retryConnection(0, hadError); + this.emit('connect'); + + if (this.#initiator) { + try { + await this.#initiator(); + } catch (err) { + this.#socket.end(); + this.#socket = undefined; + throw err; + } + } + + this.emit('ready'); + } + + async #retryConnection(retries: number, hadError?: boolean): Promise { + if (retries > 0 || hadError) { this.emit('reconnecting'); } @@ -148,16 +158,14 @@ export default class RedisSocket extends EventEmitter { this.#createNetSocket(); socket - .once('error', reject) + .once('error', (err) => reject(err)) .once(connectEvent, () => { socket .off('error', reject) .once('error', (err: Error) => this.#onSocketError(err)) - .once('end', () => { - this.emit('end'); - - if (this.#isOpen) { - this.#onSocketError(new Error('Socket ended')); + .once('close', hadError => { + if (!hadError && this.#isOpen) { + this.#onSocketError(new Error('Socket closed unexpectedly')); } }) .on('drain', () => this.emit('drain')) @@ -183,12 +191,11 @@ export default class RedisSocket extends EventEmitter { } #onSocketError(err: Error): void { + this.#socket = undefined; this.emit('error', err); - this.#retryConnection(0).catch(err => { - this.emit('error', err); - this.#socket = undefined; - }); + this.#connect(true) + .catch(err => this.emit('error', err)); } write(encodedCommands: string): boolean { @@ -208,5 +215,6 @@ export default class RedisSocket extends EventEmitter { this.#socket.end(); await EventEmitter.once(this.#socket, 'end'); this.#socket = undefined; + this.emit('end'); } } diff --git a/lib/test-utils.ts b/lib/test-utils.ts index 56fdaa8019..c0cb395995 100644 --- a/lib/test-utils.ts +++ b/lib/test-utils.ts @@ -1,3 +1,4 @@ +import assert from 'assert/strict'; import RedisClient, { RedisClientType } from './client'; import { RedisModules } from './commands'; import { spawn } from 'child_process'; @@ -53,7 +54,7 @@ let port = 6379; async function spawnRedisServer(args?: Array): Promise { const currentPort = port++, - process = spawn('redis-server', [ + process = spawn('/usr/local/bin/redis-server', [ '--save', '', '--port', @@ -66,7 +67,7 @@ async function spawnRedisServer(args?: Array): Promise { await tcpPortUsed.waitForStatus(currentPort, '127.0.0.1', true, 10, 1000); after(() => { - process.kill(); + assert.ok(process.kill()); return once(process, 'close'); }); diff --git a/package-lock.json b/package-lock.json index 3534449ce1..c7d4c5aabd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,14 +16,15 @@ "devDependencies": { "@istanbuljs/nyc-config-typescript": "^1.0.1", "@types/mocha": "^8.2.2", - "@types/node": "^15.0.2", + "@types/node": "^15.3.1", "@types/tcp-port-used": "^1.0.0", "@types/yallist": "^4.0.0", "mocha": "^8.4.0", "nyc": "^15.1.0", + "source-map-support": "^0.5.19", "tcp-port-used": "^1.0.2", "ts-node": "^9.1.1", - "typescript": "^4.3.0-beta" + "typescript": "^4.3.1-rc" }, "engines": { "node": ">=10" @@ -49,17 +50,17 @@ "dev": true }, "node_modules/@babel/core": { - "version": "7.14.2", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.14.2.tgz", - "integrity": "sha512-OgC1mON+l4U4B4wiohJlQNUU3H73mpTyYY3j/c8U9dr9UagGGSm+WFpzjy/YLdoyjiG++c1kIDgxCo/mLwQJeQ==", + "version": "7.14.3", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.14.3.tgz", + "integrity": "sha512-jB5AmTKOCSJIZ72sd78ECEhuPiDMKlQdDI/4QRI6lzYATx5SSogS1oQA2AoPecRCknm30gHi2l+QVvNUu3wZAg==", "dev": true, "dependencies": { "@babel/code-frame": "^7.12.13", - "@babel/generator": "^7.14.2", + "@babel/generator": "^7.14.3", "@babel/helper-compilation-targets": "^7.13.16", "@babel/helper-module-transforms": "^7.14.2", "@babel/helpers": "^7.14.0", - "@babel/parser": "^7.14.2", + "@babel/parser": "^7.14.3", "@babel/template": "^7.12.13", "@babel/traverse": "^7.14.2", "@babel/types": "^7.14.2", @@ -79,9 +80,9 @@ } }, "node_modules/@babel/generator": { - "version": "7.14.2", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.2.tgz", - "integrity": "sha512-OnADYbKrffDVai5qcpkMxQ7caomHOoEwjkouqnN2QhydAjowFAZcsdecFIRUBdb+ZcruwYE4ythYmF1UBZU5xQ==", + "version": "7.14.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.3.tgz", + "integrity": "sha512-bn0S6flG/j0xtQdz3hsjJ624h3W0r3llttBMfyHX3YrZ/KtLYr15bjA0FXkgW7FpvrDuTuElXeVjiKlYRpnOFA==", "dev": true, "dependencies": { "@babel/types": "^7.14.2", @@ -168,15 +169,15 @@ } }, "node_modules/@babel/helper-replace-supers": { - "version": "7.13.12", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.13.12.tgz", - "integrity": "sha512-Gz1eiX+4yDO8mT+heB94aLVNCL+rbuT2xy4YfyNqu8F+OI6vMvJK891qGBTqL9Uc8wxEvRW92Id6G7sDen3fFw==", + "version": "7.14.3", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.14.3.tgz", + "integrity": "sha512-Rlh8qEWZSTfdz+tgNV/N4gz1a0TMNwCUcENhMjHTHKp3LseYH5Jha0NSlyTQWMnjbYcwFt+bqAMqSLHVXkQ6UA==", "dev": true, "dependencies": { "@babel/helper-member-expression-to-functions": "^7.13.12", "@babel/helper-optimise-call-expression": "^7.12.13", - "@babel/traverse": "^7.13.0", - "@babel/types": "^7.13.12" + "@babel/traverse": "^7.14.2", + "@babel/types": "^7.14.2" } }, "node_modules/@babel/helper-simple-access": { @@ -303,9 +304,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.14.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.2.tgz", - "integrity": "sha512-IoVDIHpsgE/fu7eXBeRWt8zLbDrSvD7H1gpomOkPpBoEN8KCruCqSDdqo8dddwQQrui30KSvQBaMUOJiuFu6QQ==", + "version": "7.14.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.3.tgz", + "integrity": "sha512-7MpZDIfI7sUC5zWo2+foJ50CSI5lcqDehZ0lVgIhSi4bFEk94fLAKlF3Q0nzSQQ+ca0lm+O6G9ztKVBeu8PMRQ==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -474,9 +475,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "15.0.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-15.0.3.tgz", - "integrity": "sha512-/WbxFeBU+0F79z9RdEOXH4CsDga+ibi5M8uEYr91u3CkT/pdWcV8MCook+4wDPnZBexRdwWS+PiVZ2xJviAzcQ==", + "version": "15.6.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-15.6.1.tgz", + "integrity": "sha512-7EIraBEyRHEe7CH+Fm1XvgqU6uwZN8Q7jppJGcqjROMT29qhAuuOxYB1uEY5UMYQKEmA5D+5tBnhdaPXSsLONA==", "dev": true }, "node_modules/@types/tcp-port-used": { @@ -938,9 +939,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.3.727", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.727.tgz", - "integrity": "sha512-Mfz4FIB4FSvEwBpDfdipRIrwd6uo8gUDoRDF4QEYb4h4tSuI3ov594OrjU6on042UlFHouIJpClDODGkPcBSbg==", + "version": "1.3.738", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.738.tgz", + "integrity": "sha512-vCMf4gDOpEylPSLPLSwAEsz+R3ShP02Y3cAKMZvTqule3XcPp7tgc/0ESI7IS6ZeyBlGClE50N53fIOkcIVnpw==", "dev": true }, "node_modules/emoji-regex": { @@ -1680,9 +1681,9 @@ } }, "node_modules/node-releases": { - "version": "1.1.71", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.71.tgz", - "integrity": "sha512-zR6HoT6LrLCRBwukmrVbHv0EpEQjksO6GmFcZQQuCAy139BEsoVKPYnf3jongYW83fAa1torLGYwxxky/p28sg==", + "version": "1.1.72", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.72.tgz", + "integrity": "sha512-LLUo+PpH3dU6XizX3iVoubUNheF/owjXCZZ5yACDxNnPtgFuludV1ZL3ayK1kVep42Rmm0+R9/Y60NQbZ2bifw==", "dev": true }, "node_modules/normalize-path": { @@ -2000,9 +2001,9 @@ } }, "node_modules/picomatch": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.3.tgz", - "integrity": "sha512-KpELjfwcCDUb9PeigTs2mBJzXUPzAuP2oPcA989He8Rte0+YUAjw1JVedDhuTKPkHjSYzMN3npC9luThGYEKdg==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", + "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", "dev": true, "engines": { "node": ">=8.6" @@ -2446,9 +2447,9 @@ } }, "node_modules/typescript": { - "version": "4.3.0-dev.20210510", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.0-dev.20210510.tgz", - "integrity": "sha512-HR21ZkELulLWOHfHIA57/6rD1rkadgjyMp1TVeXQ5v82/rn0V3ROkAnd1elt+Mrjc2pTXcIwom7E/N5RCUUxMg==", + "version": "4.3.1-rc", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.1-rc.tgz", + "integrity": "sha512-L3uJ0gcntaRaKni9aV2amYB+pCDVodKe/B5+IREyvtKGsDOF7cYjchHb/B894skqkgD52ykRuWatIZMqEsHIqA==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -2745,17 +2746,17 @@ "dev": true }, "@babel/core": { - "version": "7.14.2", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.14.2.tgz", - "integrity": "sha512-OgC1mON+l4U4B4wiohJlQNUU3H73mpTyYY3j/c8U9dr9UagGGSm+WFpzjy/YLdoyjiG++c1kIDgxCo/mLwQJeQ==", + "version": "7.14.3", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.14.3.tgz", + "integrity": "sha512-jB5AmTKOCSJIZ72sd78ECEhuPiDMKlQdDI/4QRI6lzYATx5SSogS1oQA2AoPecRCknm30gHi2l+QVvNUu3wZAg==", "dev": true, "requires": { "@babel/code-frame": "^7.12.13", - "@babel/generator": "^7.14.2", + "@babel/generator": "^7.14.3", "@babel/helper-compilation-targets": "^7.13.16", "@babel/helper-module-transforms": "^7.14.2", "@babel/helpers": "^7.14.0", - "@babel/parser": "^7.14.2", + "@babel/parser": "^7.14.3", "@babel/template": "^7.12.13", "@babel/traverse": "^7.14.2", "@babel/types": "^7.14.2", @@ -2768,9 +2769,9 @@ } }, "@babel/generator": { - "version": "7.14.2", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.2.tgz", - "integrity": "sha512-OnADYbKrffDVai5qcpkMxQ7caomHOoEwjkouqnN2QhydAjowFAZcsdecFIRUBdb+ZcruwYE4ythYmF1UBZU5xQ==", + "version": "7.14.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.3.tgz", + "integrity": "sha512-bn0S6flG/j0xtQdz3hsjJ624h3W0r3llttBMfyHX3YrZ/KtLYr15bjA0FXkgW7FpvrDuTuElXeVjiKlYRpnOFA==", "dev": true, "requires": { "@babel/types": "^7.14.2", @@ -2854,15 +2855,15 @@ } }, "@babel/helper-replace-supers": { - "version": "7.13.12", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.13.12.tgz", - "integrity": "sha512-Gz1eiX+4yDO8mT+heB94aLVNCL+rbuT2xy4YfyNqu8F+OI6vMvJK891qGBTqL9Uc8wxEvRW92Id6G7sDen3fFw==", + "version": "7.14.3", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.14.3.tgz", + "integrity": "sha512-Rlh8qEWZSTfdz+tgNV/N4gz1a0TMNwCUcENhMjHTHKp3LseYH5Jha0NSlyTQWMnjbYcwFt+bqAMqSLHVXkQ6UA==", "dev": true, "requires": { "@babel/helper-member-expression-to-functions": "^7.13.12", "@babel/helper-optimise-call-expression": "^7.12.13", - "@babel/traverse": "^7.13.0", - "@babel/types": "^7.13.12" + "@babel/traverse": "^7.14.2", + "@babel/types": "^7.14.2" } }, "@babel/helper-simple-access": { @@ -2976,9 +2977,9 @@ } }, "@babel/parser": { - "version": "7.14.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.2.tgz", - "integrity": "sha512-IoVDIHpsgE/fu7eXBeRWt8zLbDrSvD7H1gpomOkPpBoEN8KCruCqSDdqo8dddwQQrui30KSvQBaMUOJiuFu6QQ==", + "version": "7.14.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.3.tgz", + "integrity": "sha512-7MpZDIfI7sUC5zWo2+foJ50CSI5lcqDehZ0lVgIhSi4bFEk94fLAKlF3Q0nzSQQ+ca0lm+O6G9ztKVBeu8PMRQ==", "dev": true }, "@babel/template": { @@ -3111,9 +3112,9 @@ "dev": true }, "@types/node": { - "version": "15.0.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-15.0.3.tgz", - "integrity": "sha512-/WbxFeBU+0F79z9RdEOXH4CsDga+ibi5M8uEYr91u3CkT/pdWcV8MCook+4wDPnZBexRdwWS+PiVZ2xJviAzcQ==", + "version": "15.6.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-15.6.1.tgz", + "integrity": "sha512-7EIraBEyRHEe7CH+Fm1XvgqU6uwZN8Q7jppJGcqjROMT29qhAuuOxYB1uEY5UMYQKEmA5D+5tBnhdaPXSsLONA==", "dev": true }, "@types/tcp-port-used": { @@ -3479,9 +3480,9 @@ "dev": true }, "electron-to-chromium": { - "version": "1.3.727", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.727.tgz", - "integrity": "sha512-Mfz4FIB4FSvEwBpDfdipRIrwd6uo8gUDoRDF4QEYb4h4tSuI3ov594OrjU6on042UlFHouIJpClDODGkPcBSbg==", + "version": "1.3.738", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.738.tgz", + "integrity": "sha512-vCMf4gDOpEylPSLPLSwAEsz+R3ShP02Y3cAKMZvTqule3XcPp7tgc/0ESI7IS6ZeyBlGClE50N53fIOkcIVnpw==", "dev": true }, "emoji-regex": { @@ -4015,9 +4016,9 @@ } }, "node-releases": { - "version": "1.1.71", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.71.tgz", - "integrity": "sha512-zR6HoT6LrLCRBwukmrVbHv0EpEQjksO6GmFcZQQuCAy139BEsoVKPYnf3jongYW83fAa1torLGYwxxky/p28sg==", + "version": "1.1.72", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.72.tgz", + "integrity": "sha512-LLUo+PpH3dU6XizX3iVoubUNheF/owjXCZZ5yACDxNnPtgFuludV1ZL3ayK1kVep42Rmm0+R9/Y60NQbZ2bifw==", "dev": true }, "normalize-path": { @@ -4262,9 +4263,9 @@ "dev": true }, "picomatch": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.3.tgz", - "integrity": "sha512-KpELjfwcCDUb9PeigTs2mBJzXUPzAuP2oPcA989He8Rte0+YUAjw1JVedDhuTKPkHjSYzMN3npC9luThGYEKdg==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", + "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", "dev": true }, "pkg-dir": { @@ -4597,9 +4598,9 @@ } }, "typescript": { - "version": "4.3.0-dev.20210510", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.0-dev.20210510.tgz", - "integrity": "sha512-HR21ZkELulLWOHfHIA57/6rD1rkadgjyMp1TVeXQ5v82/rn0V3ROkAnd1elt+Mrjc2pTXcIwom7E/N5RCUUxMg==", + "version": "4.3.1-rc", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.1-rc.tgz", + "integrity": "sha512-L3uJ0gcntaRaKni9aV2amYB+pCDVodKe/B5+IREyvtKGsDOF7cYjchHb/B894skqkgD52ykRuWatIZMqEsHIqA==", "dev": true }, "uuid": { diff --git a/package.json b/package.json index 717365316f..c8fbeec98a 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "main": "./dist/index.js", "types": "./dist/index.t.ts", "scripts": { - "test": "nyc -r text-summary -r html mocha -r ts-node/register './lib/**/*.spec.ts'", + "test": "nyc -r text-summary -r html mocha -r source-map-support/register -r ts-node/register './lib/**/*.spec.ts'", "build": "tsc", "benchmark": "cd ./benchmark && npm run start" }, @@ -34,14 +34,15 @@ "devDependencies": { "@istanbuljs/nyc-config-typescript": "^1.0.1", "@types/mocha": "^8.2.2", - "@types/node": "^15.0.2", + "@types/node": "^15.3.1", "@types/tcp-port-used": "^1.0.0", "@types/yallist": "^4.0.0", "mocha": "^8.4.0", "nyc": "^15.1.0", + "source-map-support": "^0.5.19", "tcp-port-used": "^1.0.2", "ts-node": "^9.1.1", - "typescript": "^4.3.0-beta" + "typescript": "^4.3.1-rc" }, "engines": { "node": ">=10"