From e7523e68197dafaa97d01efd36a6ae0dc6fb96a8 Mon Sep 17 00:00:00 2001 From: Alex Atallah Date: Wed, 8 Mar 2023 18:38:43 -0500 Subject: [PATCH 1/7] feat: add multiSet, with tests BREAKING CHANGE: save() now checks the result of txn.exec and throws if it finds an error. otherwise it now returns void --- README.md | 99 +++++++++++++++++++++++++++++++++++++------ src/redbase.ts | 30 +++++++++++-- test/database.spec.ts | 44 ++++++++++++++++++- 3 files changed, 155 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 0e678c8..00c0901 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,8 @@ Exploration API: - [Goals](#goals) - [Install](#install) - [Usage](#usage) + - [One-to-Many Relationships](#one-to-many-relationships) + - [Many-to-Many Relationships](#many-to-many-relationships) - [Core concepts](#core-concepts) - [Entries](#entries) - [Tags](#tags) @@ -77,12 +79,12 @@ const value = { a: 'result' } await db.get(key) // undefined -await db.set(key, value) +await db.save(key, value) await db.get(key) // value // Type safety! -await db.set(key, { c: 'result2' }) // Type error on value +await db.save(key, { c: 'result2' }) // Type error on value // Browsing results let data = await db.filter() @@ -92,9 +94,9 @@ assertEqual(await db.count(), 1) // Hierarchical indexes, using a customizable tag separator (default: '/') await Promise.all([ // Redis auto-pipelines these calls into one fast request! - db.set(uuid(), { a: 'hi' }, { tags: ['user1/project1'] }), - db.set(uuid(), { a: 'there' }, { tags: ['user1/project2'] }), - db.set(uuid(), { a: 'bye' }, { tags: ['user2/project1'] }) + db.save(uuid(), { a: 'hi' }, { tags: ['user1/project1'] }), + db.save(uuid(), { a: 'there' }, { tags: ['user1/project2'] }), + db.save(uuid(), { a: 'bye' }, { tags: ['user2/project1'] }) ]) data = await db.filter() @@ -108,8 +110,8 @@ assertEqual(data.length, 2) data = await db.filter({ where: {OR: ['user1', 'user2']}}) assertEqual(data.length, 3) -const count = await db.count({ where: {OR: ['user1', 'user2']}}) -assertEqual(count, 3) +const count = await db.count({ where: {AND: ['user1', 'user2']}}) +assertEqual(count, 0) // See all your indexes: const tags = await db.tags("user1") @@ -120,6 +122,74 @@ const numberDeleted = await db.clear({ where: 'user2' }) assertEqual(numberDeleted, 1) ``` +### One-to-Many Relationships + +Let's say you have User and Post interfaces that look like this: +```ts +interface User { + id: number + name: string +} +interface Post { + content: string + userId: number +} +``` + +Now we want to store this data in Redbase. Rather than store all posts in an array inside of each `User` (which you might do in a NoSQL database), you can simply create separate Redbase clients for the two interfaces: + +```ts +// Slightly more efficient to share the same redis instance: +const redisInstance = initRedis() +const users = new Redbase('myProject-user', { redisInstance }) +const posts = new Redbase('myProject-post', { redisInstance }) +``` + +When inserting a new Post for a given `userId`, make sure you tag it: + +```ts +await posts.save(uuid(), post, { tags: [`user-${userId}`] }) +``` + +Now we can query all posts for a given user: + +```ts +const userPosts = await posts.filter({ where: `user-${userId}`, limit: 100 }) +``` + +Note that tags are simple and don't have contraints; if the user's id changes, you have to re-save each of its posts with the new tag. + +### Many-to-Many Relationships + +Let's say you have Posts and Categories that look like this: + +```ts +interface Post { + id: number +} +interface Category { + name: string +} +``` + +For many-to-many relationships, e.g. `Post` <-> `Category`, you can make the save atomic inside a Redis transaction by calling `multiSet` instead of `save`: + +```ts +const redisInstance = initRedis() +const categories = new Redbase('myProject-category', { redisInstance }) +const posts = new Redbase('myProject-post', { redisInstance }) + +const post = { id, ... } +const categoryNames = ['tech', 'draft', ...] + +let multi = redisInstance.multi() +multi = posts.multiSet(multi, post.postId, post, { tags: categoryNames }) +for (const name of categoryNames) { + multi = categories.multiSet(name, { name }, { tags: [`post-${post.id}`] }) +} +await multi.exec() +``` + For all functionality, see `test/database.spec.ts`. ## Core concepts @@ -135,18 +205,19 @@ An entry is composed of an `id` and a `value`: ### Tags -Tags are a lightweight primitive for indexing your values. You attach them at insert-time, and they are schemaless. This makes them simple and flexible for many use cases. It also allows you to index values by external attributes (that aren't a part of the value itself: [example](#example-prompt-cache)). +Tags are a lightweight primitive for indexing your values. You attach them at insert-time, and they are schemaless. This makes them simple and flexible for many use cases: -Calling `db.filter({ where: { AND: [...]}})` etc. allows you to compose them together as a list. +- Tags allow you to index values by external attributes (that aren't a part of the value itself: [example](#example-prompt-cache)). +- Tags allow you to link values between Redbase instances in [one-to-many relationships](#one-to-many-relationships), or even have [many-to-many relationships](#many-to-many-relationships) between data types. You can shard data between multiple Redis instances this way. +- Tags can be nested with a chosen separator, e.g. `parentindex/childindex`. This effectively allows you to group your indexes, and makes [browsing](#example-browsing-your-data) your data easier and more fun. -Tags are sort of self-cleaning: indexes delete themselves during bulk-delete operations, and they shrink when entries are deleted individually. However, when the last entry for an index is deleted, the index is not. This shouldn't cause a significant zombie index issue unless you're creating and wiping out an unbounded number of tags. +Calling `db.filter({ where: { AND: [...]}})` etc. allows you to intersect tags together, and doing the same with `OR` allows you to union them. -Tags can get unruly, you you can keep them organized by nesting them: -`parentindex/childindex`. This effectively allows you to group your indexes, and makes [browsing](#example-browsing-your-data) your data easier and more fun. +Call `db.tags(parentIndex)` to get all the children tags of `parentIndex`. -Call `db.tags("parentindex")` to get all the children tags. +Tags are sort of self-cleaning: indexes delete themselves during bulk-delete operations, and they shrink when entries are deleted individually. However, when the last entry for an index is deleted, the index is not. This shouldn't cause a significant zombie index issue unless you're creating and wiping out an unbounded number of tags. -As you might expect, when indexing an entry under `parentindex/childindex` the entry is automatically indexed under `parentindex` as well. This makes it easy to build a url-based [cache exploration server](#example-browsing-your-data). Call `db.filter({ where: 'parentindex' })` to get all entries for all children tags. +Tags can get unruly, so you can keep them organized by nesting them. As you might expect, when indexing an entry under `parentindex/childindex`, the entry is automatically indexed under `parentindex` as well. This makes it easy to build a url-based [cache exploration server](#example-browsing-your-data). Call `db.filter({ where: parentIndex })` to get all entries for all children tags under `parentIndex`. ### Example: Prompt Cache diff --git a/src/redbase.ts b/src/redbase.ts index 09cf1b8..aefb0bf 100644 --- a/src/redbase.ts +++ b/src/redbase.ts @@ -127,11 +127,35 @@ export class Redbase { return parsed } + /** + * Save an entry to the database, setting tags and expiration appropriately + * @param id caller-provided id of the entry + * @param value object value conforming to the database's type + * @param tags tags for this entry + */ async save( + id: string, + value: ValueT, + opts: SaveParams = {} + ): Promise { + let txn = this.redis.multi() + txn = this.multiSet(txn, id, value, opts) + const res = await txn.exec() + if (!res || res.map(r => r[0]).filter(e => !!e).length) { + // Errors occurred during the save, so throw + throw res + } + } + + /** + * Similar to `save` but for setting an entry onto a Redis multi transaction + */ + multiSet( + multi: ChainableCommander, id: string, value: ValueT, { tags, sortBy, ttl }: SaveParams = {} - ): Promise { + ): ChainableCommander { if (!Array.isArray(tags)) { tags = [tags || ''] } @@ -139,7 +163,7 @@ export class Redbase { const score = sortBy ? sortBy(value) : new Date().getTime() const tagInstances = tags.map(p => Tag.fromPath(p)) - let txn = this.redis.multi().set(this._entryKey(id), JSON.stringify(value)) + let txn = multi.set(this._entryKey(id), JSON.stringify(value)) for (const tag of tagInstances) { txn = this._indexEntry(txn, tag, id, score) @@ -151,7 +175,7 @@ export class Redbase { if (ttl) { txn = txn.expire(this._entryKey(id), ttl) } - return txn.exec() + return txn } async clear({ where = '' }: ClearParams = {}): Promise { diff --git a/test/database.spec.ts b/test/database.spec.ts index 9f27ccb..17c43f7 100644 --- a/test/database.spec.ts +++ b/test/database.spec.ts @@ -2,7 +2,7 @@ import { Redbase } from '../src' import { v4 as uuidv4 } from 'uuid' describe('Redbase', () => { - type ValueT = { answer: string; optional?: number[] } + type ValueT = { answer: string; optional?: number[]; id?: string } let db: Redbase let dbComplex: Redbase @@ -30,6 +30,13 @@ describe('Redbase', () => { // @ts-ignore expect(() => (db.name = 'dest')).toThrowError() }) + + it('should allow other clients to use the same Redis instance', () => { + const otherDb = new Redbase('Test-backup', { + redisInstance: db.redis, + }) + expect(db.redis).toStrictEqual(otherDb.redis) + }) }) describe('simple get/save/delete/clear', () => { @@ -83,6 +90,41 @@ describe('Redbase', () => { }) }) + describe('Foreign relations between databases', () => { + afterAll(async () => { + await db.clear() + await dbComplex.clear() + }) + + it('should query one-to-many relationships', async () => { + const userTag = 'user-1' + const obj = { answer: 'hello' } + await Promise.all([ + db.save(userTag, 'new user'), + dbComplex.save(uuidv4(), obj, { tags: [userTag] }), + ]) + const objs = await dbComplex.filter({ where: userTag }) + expect(objs[0].value).toEqual(obj) + }) + + it('should atomically save many-to-many relationships', async () => { + const categories = ['category1', 'category2'] + const obj = { answer: 'hello', id: uuidv4() } + let multi = db.redis.multi() + multi = dbComplex.multiSet(multi, obj.id, obj, { tags: categories }) + for (const name of categories) { + multi = db.multiSet(multi, name, name, { tags: [`obj-${obj.id}`] }) + } + await multi.exec() + + let results = await dbComplex.filter({ where: 'category1' }) + expect(results[0].value).toEqual(obj) + + results = await dbComplex.filter({ where: 'category2' }) + expect(results[0].value).toEqual(obj) + }) + }) + describe('expire entries', () => { afterAll(async () => { await db.clear() From b5ee1db3a11c7f910ff5253121d780e5f9233a94 Mon Sep 17 00:00:00 2001 From: Alex Atallah Date: Wed, 8 Mar 2023 19:44:10 -0500 Subject: [PATCH 2/7] WIP: Adapter architecture and IORedis start --- README.md | 24 +++++++++++-- src/adapters/base.ts | 19 ++++++++++ src/adapters/ioredis.ts | 80 +++++++++++++++++++++++++++++++++++++++++ src/backend.ts | 20 ----------- src/index.ts | 2 +- src/redbase.ts | 31 ++++++++-------- 6 files changed, 135 insertions(+), 41 deletions(-) create mode 100644 src/adapters/base.ts create mode 100644 src/adapters/ioredis.ts delete mode 100644 src/backend.ts diff --git a/README.md b/README.md index 0e678c8..b3821c8 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,8 @@ Built to answer the question, "how can I have a queryable, browsable db that als ## Goals -- **Simple**: less than 500 lines. Only one dependency, `ioredis`. You can copy-paste the code instead if you want. -- **Serverless-friendly**: no Redis modules, only core Redis. +- **Simple**: less than 500 lines, and no dependencies. You can copy-paste the code instead if you want. +- **Serverless-friendly**: no Redis modules, only core Redis. Multiple Redis clients supported, including REST-based ones. - **Fast**: Compared to optimized Postgres, 150% faster at paginating unindexed data. See [all benchmarks](#benchmarks) below. - **Indexable**: Supports hierarchical [tags](#tags), a lightweight primitive for indexing your data. - **Browsable**: [browser-friendly API](#example-browsing-your-data) included for paginating and browsing by tag. @@ -37,6 +37,7 @@ Exploration API: - [Redbase](#redbase) - [Goals](#goals) - [Install](#install) + - [Redis Client Compatibility](#redis-client-compatibility) - [Usage](#usage) - [Core concepts](#core-concepts) - [Entries](#entries) @@ -47,6 +48,8 @@ Exploration API: - [For the cache use-case](#for-the-cache-use-case) - [For the database use-case](#for-the-database-use-case) - [Results](#results) + - [Contributing](#contributing) + - [Writing Adapters](#writing-adapters) - [License](#license) ## Install @@ -55,6 +58,13 @@ Exploration API: npm install redbase ``` +### Redis Client Compatibility + +Redbase can support arbitrary Redis clients through the use of [custom adapters](#writing-adapters). The current clients supported are: + +- [ioredis](https://github.com/luin/ioredis) +- [redis](https://github.com/redis/node-redis) + ## Usage ```ts @@ -68,7 +78,7 @@ type MyValue = { } } -// Options can also use your own ioredis instance if already defined, +// Options can also use your own Redis instance if already defined, // as `redisInstance` const db = new Redbase('myProject', { redisUrl: 'redis://...' }) @@ -229,6 +239,14 @@ Results on Apple M1 Max, 2021: ![Scroll along an index](files/index_scrolling.png) ![Delete data](files/deleting.png) +## Contributing + +See open pull requests and issues on [Github](https://github.com/alexanderatallah/redbase). + +### Writing Adapters + +You can add support for new [Redis clients](#redis-client-compatibility) by writing an adapter. Adapters are located in `src/adapters/`. It's easy to duplicate one and adjust. + ## License MIT diff --git a/src/adapters/base.ts b/src/adapters/base.ts new file mode 100644 index 0000000..965fe4b --- /dev/null +++ b/src/adapters/base.ts @@ -0,0 +1,19 @@ +export const defaultLogger = (err: unknown) => { + console.error('Redbase backend error', err) +} + +export abstract class RedisMultiAdapter { + abstract set(key: string, value: string): RedisMultiAdapter + abstract expire(key: string, ttl: number): RedisMultiAdapter + abstract sadd(key: string, value: string): RedisMultiAdapter + abstract zadd(key: string, score: number, value: string): RedisMultiAdapter + abstract exec(): Promise + abstract del(key: string): RedisMultiAdapter +} + +export abstract class RedisAdapter { + abstract multi(): RedisMultiAdapter + abstract quit(): Promise + abstract ttl(key: string): Promise + abstract smembers(key: string): Promise +} diff --git a/src/adapters/ioredis.ts b/src/adapters/ioredis.ts new file mode 100644 index 0000000..e598060 --- /dev/null +++ b/src/adapters/ioredis.ts @@ -0,0 +1,80 @@ +import Redis, { ChainableCommander } from 'ioredis' +import { RedisAdapter, defaultLogger, RedisMultiAdapter } from './base' +const DEFAULT_URL = process.env['REDIS_URL'] || 'redis://localhost:6379' + +export class IORedisMulti extends RedisMultiAdapter { + multi: ChainableCommander + + constructor(redis: Redis) { + super() + this.multi = redis.multi() + } + + set(key: string, value: string) { + this.multi = this.multi.set(key, value) + return this + } + + expire(key: string, ttl: number) { + this.multi = this.multi.expire(key, ttl) + return this + } + + sadd(key: string, value: string) { + this.multi = this.multi.sadd(key, value) + return this + } + + zadd(key: string, score: number, value: string) { + this.multi = this.multi.zadd(key, score, value) + return this + } + + async exec() { + const res = await this.multi.exec() + if (!res || res.map(r => r[0]).filter(e => !!e).length) { + // Errors occurred during the exec, so throw + throw res + } + } + + del(key: string) { + this.multi = this.multi.del(key) + return this + } +} + +export class IORedis extends RedisAdapter { + redis: Redis + + constructor(url = DEFAULT_URL, errorLogger = defaultLogger) { + super() + this.redis = new Redis(url, { + enableAutoPipelining: true, + }) + + if (errorLogger) { + this.redis.on('error', errorLogger) + } + } + + multi() { + return new IORedisMulti(this.redis) + } + + async quit() { + await this.redis.quit() + } + + async ttl(key: string) { + return this.redis.ttl(key) + } + + async del(key: string) { + return this.redis.del(key) + } + + async smembers(key: string): Promise { + return this.redis.smembers(key) + } +} diff --git a/src/backend.ts b/src/backend.ts deleted file mode 100644 index d3c9051..0000000 --- a/src/backend.ts +++ /dev/null @@ -1,20 +0,0 @@ -import Redis from 'ioredis' -const DEFAULT_URL = process.env['REDIS_URL'] || 'redis://localhost:6379' - -const defaultLogger = (err: unknown) => { - console.error('Redbase backend error', err) -} - -export function initRedis(url = DEFAULT_URL, errorLogger = defaultLogger) { - const redis = new Redis(url, { - enableAutoPipelining: true, - }) - - if (errorLogger) { - redis.on('error', errorLogger) - } - - return redis -} - -export type ExecT = [error: Error | null, result: unknown][] | null diff --git a/src/index.ts b/src/index.ts index 549955a..0b9ea2d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1,2 @@ export { Redbase } from './redbase' -export { initRedis } from './backend' +export { IORedis } from './adapters/ioredis' diff --git a/src/redbase.ts b/src/redbase.ts index 09cf1b8..57a4003 100644 --- a/src/redbase.ts +++ b/src/redbase.ts @@ -1,13 +1,13 @@ -import { initRedis, ExecT } from './backend' -import { ChainableCommander, Redis } from 'ioredis' +import { IORedis } from './adapters/ioredis' +import { RedisAdapter, RedisMultiAdapter } from './adapters/base' import { Tag } from './tag' const GLOBAL_PREFIX = process.env['REDIS_PREFIX'] || '' const DEBUG = process.env['DEBUG'] === 'true' const AGG_TAG_TTL_BUFFER = 0.1 // seconds export interface Options { - redisInstance?: Redis // Redis instance to use. Defaults to undefined. - redisUrl?: string // Redis URL to use. Defaults to undefined. + redisAdapter?: RedisAdapter // Redis adapter to use. Defaults to IORedis. + redisUrl?: string // Redis URL to use. Defaults to REDIS_URL in the environment. defaultTTL?: number // Default expiration (in seconds) to use for each entry. Defaults to undefined. aggregateTagTTL?: number // TTL for computed query tags. Defaults to 10 seconds deletionPageSize?: number // Number of entries to delete at a time when calling "clear". Defaults to 2000. @@ -81,14 +81,14 @@ interface SaveParams { export class Redbase { public deletionPageSize: number - public redis: Redis + public redis: RedisAdapter private _name: string private _defaultTTL: number | undefined private _aggregateTagTTL: number constructor(name: string, opts: Options = {}) { - this.redis = opts.redisInstance || initRedis(opts.redisUrl) + this.redis = opts.redisAdapter || new IORedis(opts.redisUrl) this._defaultTTL = this._validateTTL(opts.defaultTTL) this._aggregateTagTTL = this._validateTTL(opts.aggregateTagTTL) || 10 // seconds this.deletionPageSize = opts.deletionPageSize || 2000 @@ -131,7 +131,7 @@ export class Redbase { id: string, value: ValueT, { tags, sortBy, ttl }: SaveParams = {} - ): Promise { + ): Promise { if (!Array.isArray(tags)) { tags = [tags || ''] } @@ -151,7 +151,7 @@ export class Redbase { if (ttl) { txn = txn.expire(this._entryKey(id), ttl) } - return txn.exec() + await txn.exec() } async clear({ where = '' }: ClearParams = {}): Promise { @@ -245,7 +245,7 @@ export class Redbase { return this.redis.zcount(this._tagKey(computedTag), scoreMin, scoreMax) } - async delete(id: string): Promise { + async delete(id: string): Promise { const tagKey = this._entryTagsKey(id) if (DEBUG) { console.log( @@ -270,7 +270,7 @@ export class Redbase { } txn = txn.del(tagKey) - return txn.exec() + await txn.exec() } async ttl(id: string): Promise { @@ -278,7 +278,7 @@ export class Redbase { return ttl < 0 ? undefined : ttl } - async close(): Promise { + async close(): Promise { return this.redis.quit() } @@ -311,7 +311,7 @@ export class Redbase { } _indexEntry( - txn: ChainableCommander, + txn: RedisMultiAdapter, tag: Tag, entryId: string, score: number @@ -437,7 +437,7 @@ export class Redbase { targetTagKey: string, tagKeys: string[], type: 'union' | 'intersection' - ): Promise { + ): Promise { let txn = this.redis.multi() if ((await this.redis.ttl(targetTagKey)) > AGG_TAG_TTL_BUFFER) { return txn @@ -453,10 +453,7 @@ export class Redbase { return txn } - _recursiveTagDeletion( - multi: ChainableCommander, - tag: Tag - ): ChainableCommander { + _recursiveTagDeletion(multi: RedisMultiAdapter, tag: Tag): RedisMultiAdapter { let ret = multi.del(this._tagKey(tag)) const childtags = this.redis.zrange(this._tagChildrenKey(tag), 0, -1) for (const child in childtags) { From 29e502f3cc18a1a42489f3423de9895a84df3935 Mon Sep 17 00:00:00 2001 From: Alex Atallah Date: Wed, 8 Mar 2023 19:50:20 -0500 Subject: [PATCH 3/7] update for error handler --- src/adapters/ioredis.ts | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/adapters/ioredis.ts b/src/adapters/ioredis.ts index e598060..734756a 100644 --- a/src/adapters/ioredis.ts +++ b/src/adapters/ioredis.ts @@ -4,10 +4,12 @@ const DEFAULT_URL = process.env['REDIS_URL'] || 'redis://localhost:6379' export class IORedisMulti extends RedisMultiAdapter { multi: ChainableCommander + redis: IORedis - constructor(redis: Redis) { + constructor(ioRedis: IORedis) { super() - this.multi = redis.multi() + this.redis = ioRedis + this.multi = ioRedis.redis.multi() } set(key: string, value: string) { @@ -33,8 +35,8 @@ export class IORedisMulti extends RedisMultiAdapter { async exec() { const res = await this.multi.exec() if (!res || res.map(r => r[0]).filter(e => !!e).length) { - // Errors occurred during the exec, so throw - throw res + // Errors occurred during the exec, so record backend error + this.redis.errorHandler(res) } } @@ -46,20 +48,22 @@ export class IORedisMulti extends RedisMultiAdapter { export class IORedis extends RedisAdapter { redis: Redis + errorHandler: (err: unknown) => void - constructor(url = DEFAULT_URL, errorLogger = defaultLogger) { + constructor(url = DEFAULT_URL, errorHandler = defaultLogger) { super() this.redis = new Redis(url, { enableAutoPipelining: true, }) - if (errorLogger) { - this.redis.on('error', errorLogger) + if (errorHandler) { + this.errorHandler = errorHandler + this.redis.on('error', errorHandler) } } multi() { - return new IORedisMulti(this.redis) + return new IORedisMulti(this) } async quit() { From 1992fce2a82225838e7a23c5f37bb0e04a1e7234 Mon Sep 17 00:00:00 2001 From: Alex Atallah Date: Wed, 8 Mar 2023 22:37:58 -0500 Subject: [PATCH 4/7] finish adapter impl for ioredis --- src/adapters/base.ts | 36 ++++++++++++++-- src/adapters/ioredis.ts | 93 ++++++++++++++++++++++++++++++++++------- src/redbase.ts | 21 +++------- 3 files changed, 117 insertions(+), 33 deletions(-) diff --git a/src/adapters/base.ts b/src/adapters/base.ts index 965fe4b..1b87fa7 100644 --- a/src/adapters/base.ts +++ b/src/adapters/base.ts @@ -2,13 +2,28 @@ export const defaultLogger = (err: unknown) => { console.error('Redbase backend error', err) } +export type RawValueT = string | number | Buffer + +export type AggregationMode = 'SUM' | 'MIN' | 'MAX' +export type OrderingMode = 'ASC' | 'DESC' export abstract class RedisMultiAdapter { - abstract set(key: string, value: string): RedisMultiAdapter + abstract set(key: string, value: RawValueT): RedisMultiAdapter abstract expire(key: string, ttl: number): RedisMultiAdapter - abstract sadd(key: string, value: string): RedisMultiAdapter - abstract zadd(key: string, score: number, value: string): RedisMultiAdapter + abstract sadd(key: string, ...values: RawValueT[]): RedisMultiAdapter + abstract zadd(key: string, ...scoreMembers: RawValueT[]): RedisMultiAdapter abstract exec(): Promise - abstract del(key: string): RedisMultiAdapter + abstract del(...keys: string[]): RedisMultiAdapter + abstract zrem(key: string, ...values: RawValueT[]): RedisMultiAdapter + abstract zunionstore( + destination: string, + aggregate: AggregationMode | undefined, + ...keys: string[] + ): RedisMultiAdapter + abstract zinterstore( + destination: string, + aggregate: AggregationMode | undefined, + ...keys: string[] + ): RedisMultiAdapter } export abstract class RedisAdapter { @@ -16,4 +31,17 @@ export abstract class RedisAdapter { abstract quit(): Promise abstract ttl(key: string): Promise abstract smembers(key: string): Promise + abstract zcount( + key: string, + min: string | number, + max: string | number + ): Promise + abstract zrange( + key: string, + min: RawValueT, + max: RawValueT, + order: OrderingMode | undefined + ): Promise + abstract get(key: string): Promise + abstract del(...keys: string[]): Promise } diff --git a/src/adapters/ioredis.ts b/src/adapters/ioredis.ts index 734756a..0cfca46 100644 --- a/src/adapters/ioredis.ts +++ b/src/adapters/ioredis.ts @@ -1,5 +1,12 @@ import Redis, { ChainableCommander } from 'ioredis' -import { RedisAdapter, defaultLogger, RedisMultiAdapter } from './base' +import { + RedisAdapter, + defaultLogger, + RedisMultiAdapter, + RawValueT, + AggregationMode, + OrderingMode, +} from './base' const DEFAULT_URL = process.env['REDIS_URL'] || 'redis://localhost:6379' export class IORedisMulti extends RedisMultiAdapter { @@ -12,7 +19,7 @@ export class IORedisMulti extends RedisMultiAdapter { this.multi = ioRedis.redis.multi() } - set(key: string, value: string) { + set(key: string, value: RawValueT) { this.multi = this.multi.set(key, value) return this } @@ -22,13 +29,13 @@ export class IORedisMulti extends RedisMultiAdapter { return this } - sadd(key: string, value: string) { - this.multi = this.multi.sadd(key, value) + sadd(key: string, ...values: RawValueT[]) { + this.multi = this.multi.sadd(key, ...values) return this } - zadd(key: string, score: number, value: string) { - this.multi = this.multi.zadd(key, score, value) + zadd(key: string, ...scoreMembers: RawValueT[]) { + this.multi = this.multi.zadd(key, ...scoreMembers) return this } @@ -40,8 +47,43 @@ export class IORedisMulti extends RedisMultiAdapter { } } - del(key: string) { - this.multi = this.multi.del(key) + del(...keys: string[]) { + this.multi = this.multi.del(...keys) + return this + } + + zrem(key: string, ...values: RawValueT[]) { + this.multi = this.multi.zrem(key, ...values) + return this + } + + zunionstore( + destination: string, + aggregate: AggregationMode | undefined, + ...keys: string[] + ): RedisMultiAdapter { + const aggSettings = aggregate ? ['AGGREGATE', aggregate] : [] + this.multi = this.multi.zunionstore( + destination, + keys.length, + ...keys, + ...aggSettings + ) + return this + } + + zinterstore( + destination: string, + aggregate: AggregationMode | undefined, + ...keys: string[] + ): RedisMultiAdapter { + const aggSettings = aggregate ? ['AGGREGATE', aggregate] : [] + this.multi = this.multi.zinterstore( + destination, + keys.length, + ...keys, + ...aggSettings + ) return this } } @@ -56,10 +98,8 @@ export class IORedis extends RedisAdapter { enableAutoPipelining: true, }) - if (errorHandler) { - this.errorHandler = errorHandler - this.redis.on('error', errorHandler) - } + this.errorHandler = errorHandler + this.redis.on('error', errorHandler) } multi() { @@ -74,11 +114,36 @@ export class IORedis extends RedisAdapter { return this.redis.ttl(key) } - async del(key: string) { - return this.redis.del(key) + async get(key: string) { + return this.redis.get(key) + } + + async del(...keys: string[]) { + return this.redis.del(...keys) } async smembers(key: string): Promise { return this.redis.smembers(key) } + + async zcount( + key: string, + min: string | number, + max: string | number + ): Promise { + return this.redis.zcount(key, min, max) + } + + async zrange( + key: string, + min: RawValueT, + max: RawValueT, + order: OrderingMode | undefined + ): Promise { + if (order === 'DESC') { + return this.redis.zrange(key, min, max, 'REV') + } else { + return this.redis.zrange(key, min, max) + } + } } diff --git a/src/redbase.ts b/src/redbase.ts index 57a4003..6ff8d30 100644 --- a/src/redbase.ts +++ b/src/redbase.ts @@ -227,10 +227,7 @@ export class Redbase { offset, offset + limit - 1, // ZRANGE limits are inclusive ] - if (ordering === 'desc') { - args.push('REV') - } - return this.redis.zrange(...args) + return this.redis.zrange(...args, ordering === 'desc' ? 'DESC' : 'ASC') } async count({ @@ -304,10 +301,7 @@ export class Redbase { offset, offset + limit - 1, // ZRANGE limits are inclusive ] - if (ordering === 'desc') { - args.push('REV') - } - return this.redis.zrange(...args) + return this.redis.zrange(...args, ordering === 'desc' ? 'DESC' : 'ASC') } _indexEntry( @@ -443,19 +437,16 @@ export class Redbase { return txn } const methodName = type === 'union' ? 'zunionstore' : 'zinterstore' - txn = txn[methodName]( + txn = txn[methodName](targetTagKey, 'MIN', ...tagKeys).expire( targetTagKey, - tagKeys.length, - ...tagKeys, - 'AGGREGATE', - 'MIN' - ).expire(targetTagKey, this.aggregateTagTTL) + this.aggregateTagTTL + ) return txn } _recursiveTagDeletion(multi: RedisMultiAdapter, tag: Tag): RedisMultiAdapter { let ret = multi.del(this._tagKey(tag)) - const childtags = this.redis.zrange(this._tagChildrenKey(tag), 0, -1) + const childtags = this.redis.zrange(this._tagChildrenKey(tag), 0, -1, 'ASC') for (const child in childtags) { ret = this._recursiveTagDeletion(ret, Tag.fromPath(child)) } From 2d723ba1dcc138c862f8a55f22e21d6bd50f5b5a Mon Sep 17 00:00:00 2001 From: Alex Atallah Date: Wed, 8 Mar 2023 22:47:21 -0500 Subject: [PATCH 5/7] remove splats from adapters --- src/adapters/base.ts | 24 ++++++++++++++---------- src/adapters/ioredis.ts | 23 ++++++++++++----------- src/redbase.ts | 22 +++++++++++----------- 3 files changed, 37 insertions(+), 32 deletions(-) diff --git a/src/adapters/base.ts b/src/adapters/base.ts index 1b87fa7..ac75e9e 100644 --- a/src/adapters/base.ts +++ b/src/adapters/base.ts @@ -9,20 +9,24 @@ export type OrderingMode = 'ASC' | 'DESC' export abstract class RedisMultiAdapter { abstract set(key: string, value: RawValueT): RedisMultiAdapter abstract expire(key: string, ttl: number): RedisMultiAdapter - abstract sadd(key: string, ...values: RawValueT[]): RedisMultiAdapter - abstract zadd(key: string, ...scoreMembers: RawValueT[]): RedisMultiAdapter + abstract sadd(key: string, values: RawValueT[]): RedisMultiAdapter + abstract zadd( + key: string, + scores: RawValueT[], + members: RawValueT[] + ): RedisMultiAdapter abstract exec(): Promise - abstract del(...keys: string[]): RedisMultiAdapter - abstract zrem(key: string, ...values: RawValueT[]): RedisMultiAdapter + abstract del(keys: string[]): RedisMultiAdapter + abstract zrem(key: string, values: RawValueT[]): RedisMultiAdapter abstract zunionstore( destination: string, - aggregate: AggregationMode | undefined, - ...keys: string[] + keys: string[], + aggregate?: AggregationMode ): RedisMultiAdapter abstract zinterstore( destination: string, - aggregate: AggregationMode | undefined, - ...keys: string[] + keys: string[], + aggregate?: AggregationMode ): RedisMultiAdapter } @@ -40,8 +44,8 @@ export abstract class RedisAdapter { key: string, min: RawValueT, max: RawValueT, - order: OrderingMode | undefined + order?: OrderingMode ): Promise abstract get(key: string): Promise - abstract del(...keys: string[]): Promise + abstract del(keys: string[]): Promise } diff --git a/src/adapters/ioredis.ts b/src/adapters/ioredis.ts index 0cfca46..36894d4 100644 --- a/src/adapters/ioredis.ts +++ b/src/adapters/ioredis.ts @@ -29,13 +29,14 @@ export class IORedisMulti extends RedisMultiAdapter { return this } - sadd(key: string, ...values: RawValueT[]) { + sadd(key: string, values: RawValueT[]) { this.multi = this.multi.sadd(key, ...values) return this } - zadd(key: string, ...scoreMembers: RawValueT[]) { - this.multi = this.multi.zadd(key, ...scoreMembers) + zadd(key: string, scores: RawValueT[], members: RawValueT[]) { + const zipped = scores.flatMap((s, i) => [s, members[i]]) + this.multi = this.multi.zadd(key, ...zipped) return this } @@ -47,20 +48,20 @@ export class IORedisMulti extends RedisMultiAdapter { } } - del(...keys: string[]) { + del(keys: string[]) { this.multi = this.multi.del(...keys) return this } - zrem(key: string, ...values: RawValueT[]) { + zrem(key: string, values: RawValueT[]) { this.multi = this.multi.zrem(key, ...values) return this } zunionstore( destination: string, - aggregate: AggregationMode | undefined, - ...keys: string[] + keys: string[], + aggregate?: AggregationMode ): RedisMultiAdapter { const aggSettings = aggregate ? ['AGGREGATE', aggregate] : [] this.multi = this.multi.zunionstore( @@ -74,8 +75,8 @@ export class IORedisMulti extends RedisMultiAdapter { zinterstore( destination: string, - aggregate: AggregationMode | undefined, - ...keys: string[] + keys: string[], + aggregate?: AggregationMode ): RedisMultiAdapter { const aggSettings = aggregate ? ['AGGREGATE', aggregate] : [] this.multi = this.multi.zinterstore( @@ -118,7 +119,7 @@ export class IORedis extends RedisAdapter { return this.redis.get(key) } - async del(...keys: string[]) { + async del(keys: string[]) { return this.redis.del(...keys) } @@ -138,7 +139,7 @@ export class IORedis extends RedisAdapter { key: string, min: RawValueT, max: RawValueT, - order: OrderingMode | undefined + order?: OrderingMode ): Promise { if (order === 'DESC') { return this.redis.zrange(key, min, max, 'REV') diff --git a/src/redbase.ts b/src/redbase.ts index 6ff8d30..384b96f 100644 --- a/src/redbase.ts +++ b/src/redbase.ts @@ -254,19 +254,19 @@ export class Redbase { // TODO Using unlink instead of del here doesn't seem to improve perf much let txn = this.redis.multi() - txn = txn.del(this._entryKey(id)) + txn = txn.del([this._entryKey(id)]) for (let tag of tags) { // Traverse child hierarchy while (tag.parent) { - txn = txn.zrem(this._tagKey(tag), id) + txn = txn.zrem(this._tagKey(tag), [id]) tag = tag.parent } // Root. Note that there might be duplicate zrem calls for shared parents, esp root - txn = txn.zrem(this._tagKey(tag), id) + txn = txn.zrem(this._tagKey(tag), [id]) } - txn = txn.del(tagKey) + txn = txn.del([tagKey]) await txn.exec() } @@ -311,19 +311,19 @@ export class Redbase { score: number ) { // Tag this tag under the entry - txn = txn.sadd(this._entryTagsKey(entryId), tag.name) + txn = txn.sadd(this._entryTagsKey(entryId), [tag.name]) // Traverse child hierarchy while (tag.parent) { // Tag the entry under this tag - txn = txn.zadd(this._tagKey(tag), score, entryId) + txn = txn.zadd(this._tagKey(tag), [score], [entryId]) // Register this tag under its parent - txn = txn.zadd(this._tagChildrenKey(tag.parent), 0, tag.name) + txn = txn.zadd(this._tagChildrenKey(tag.parent), [0], [tag.name]) // Move up the hierarchy tag = tag.parent } // We're at the root tag now - add the entry to it as well - txn = txn.zadd(this._tagKey(tag), score, entryId) + txn = txn.zadd(this._tagKey(tag), [score], [entryId]) return txn } @@ -437,7 +437,7 @@ export class Redbase { return txn } const methodName = type === 'union' ? 'zunionstore' : 'zinterstore' - txn = txn[methodName](targetTagKey, 'MIN', ...tagKeys).expire( + txn = txn[methodName](targetTagKey, tagKeys, 'MIN').expire( targetTagKey, this.aggregateTagTTL ) @@ -445,11 +445,11 @@ export class Redbase { } _recursiveTagDeletion(multi: RedisMultiAdapter, tag: Tag): RedisMultiAdapter { - let ret = multi.del(this._tagKey(tag)) + let ret = multi.del([this._tagKey(tag)]) const childtags = this.redis.zrange(this._tagChildrenKey(tag), 0, -1, 'ASC') for (const child in childtags) { ret = this._recursiveTagDeletion(ret, Tag.fromPath(child)) } - return ret.del(this._tagChildrenKey(tag)) + return ret.del([this._tagChildrenKey(tag)]) } } From b8162f1da46dbcd708b999c59ff4792926630b9c Mon Sep 17 00:00:00 2001 From: Alex Atallah Date: Fri, 10 Mar 2023 11:32:57 -0800 Subject: [PATCH 6/7] type fixes for redis count and range --- README.md | 6 +++--- src/adapters/base.ts | 25 +++++++++++-------------- src/adapters/ioredis.ts | 23 ++++++++++++----------- 3 files changed, 26 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index b3821c8..5eaf059 100644 --- a/README.md +++ b/README.md @@ -203,7 +203,7 @@ To browse, paginate, filter, and delete your data directly from a browser, just **Note:** I'm very new to benchmarking open-sourced code, and would appreciate pull requests here! One issue, for example, is that increasing the number of runs can cause the data to scale up (depending on which benchmarks you're running), which seems to e.g. make Redis win on pagination by a larger margin. -This project uses [hyperfine](https://github.com/sharkdp/hyperfine) to compare Redis in a persistent mode with Postgres in an optimized mode. **Yes, this is comparing apples to oranges.** I decided to do it anyway because: +These benchmarks use [hyperfine](https://github.com/sharkdp/hyperfine) and compare Redis in a persistent mode with Postgres in an optimized mode. Yes, this is still comparing apples to oranges in a sense, but I decided to do it anyway because: 1. A big question this project answers is "how can I have a queryable, browsable db that also works well as a cache?" Redis and Postgres are two backend choices that pop up frequently. @@ -229,10 +229,10 @@ Comment-out the call to `ALTER DATABASE ... SET synchronous_commit=OFF;` in `/be ### Results - **Inserting data**: Tie -- **Paginating unindexed data**: Redis is ~150% faster +- **Paginating unindexed data**: Redbase is ~150% faster - **Single-index pagination**: Postgres is ~50% faster - **Joint-index pagination**: Postgres is ~60% faster -- **Inserting and deleting data**: Redis is ~25% faster +- **Inserting and deleting data**: Redbase is ~25% faster Results on Apple M1 Max, 2021: ![Insert and scroll](files/insert_and_scroll.png) diff --git a/src/adapters/base.ts b/src/adapters/base.ts index ac75e9e..b74c33a 100644 --- a/src/adapters/base.ts +++ b/src/adapters/base.ts @@ -2,22 +2,23 @@ export const defaultLogger = (err: unknown) => { console.error('Redbase backend error', err) } -export type RawValueT = string | number | Buffer - +export type RawValue = string | number | Buffer export type AggregationMode = 'SUM' | 'MIN' | 'MAX' export type OrderingMode = 'ASC' | 'DESC' +export type Score = number | '-inf' | '+inf' + export abstract class RedisMultiAdapter { - abstract set(key: string, value: RawValueT): RedisMultiAdapter + abstract set(key: string, value: RawValue): RedisMultiAdapter abstract expire(key: string, ttl: number): RedisMultiAdapter - abstract sadd(key: string, values: RawValueT[]): RedisMultiAdapter + abstract sadd(key: string, values: RawValue[]): RedisMultiAdapter abstract zadd( key: string, - scores: RawValueT[], - members: RawValueT[] + scores: Score[], + members: RawValue[] ): RedisMultiAdapter abstract exec(): Promise abstract del(keys: string[]): RedisMultiAdapter - abstract zrem(key: string, values: RawValueT[]): RedisMultiAdapter + abstract zrem(key: string, values: RawValue[]): RedisMultiAdapter abstract zunionstore( destination: string, keys: string[], @@ -35,15 +36,11 @@ export abstract class RedisAdapter { abstract quit(): Promise abstract ttl(key: string): Promise abstract smembers(key: string): Promise - abstract zcount( - key: string, - min: string | number, - max: string | number - ): Promise + abstract zcount(key: string, min?: Score, max?: Score): Promise abstract zrange( key: string, - min: RawValueT, - max: RawValueT, + start: number, + stop: number, order?: OrderingMode ): Promise abstract get(key: string): Promise diff --git a/src/adapters/ioredis.ts b/src/adapters/ioredis.ts index 36894d4..fb7233f 100644 --- a/src/adapters/ioredis.ts +++ b/src/adapters/ioredis.ts @@ -3,9 +3,10 @@ import { RedisAdapter, defaultLogger, RedisMultiAdapter, - RawValueT, + RawValue, AggregationMode, OrderingMode, + Score, } from './base' const DEFAULT_URL = process.env['REDIS_URL'] || 'redis://localhost:6379' @@ -19,7 +20,7 @@ export class IORedisMulti extends RedisMultiAdapter { this.multi = ioRedis.redis.multi() } - set(key: string, value: RawValueT) { + set(key: string, value: RawValue) { this.multi = this.multi.set(key, value) return this } @@ -29,12 +30,12 @@ export class IORedisMulti extends RedisMultiAdapter { return this } - sadd(key: string, values: RawValueT[]) { + sadd(key: string, values: RawValue[]) { this.multi = this.multi.sadd(key, ...values) return this } - zadd(key: string, scores: RawValueT[], members: RawValueT[]) { + zadd(key: string, scores: Score[], members: RawValue[]) { const zipped = scores.flatMap((s, i) => [s, members[i]]) this.multi = this.multi.zadd(key, ...zipped) return this @@ -53,7 +54,7 @@ export class IORedisMulti extends RedisMultiAdapter { return this } - zrem(key: string, values: RawValueT[]) { + zrem(key: string, values: RawValue[]) { this.multi = this.multi.zrem(key, ...values) return this } @@ -129,22 +130,22 @@ export class IORedis extends RedisAdapter { async zcount( key: string, - min: string | number, - max: string | number + min: Score = '-inf', + max: Score = '+inf' ): Promise { return this.redis.zcount(key, min, max) } async zrange( key: string, - min: RawValueT, - max: RawValueT, + start: number, + stop: number, order?: OrderingMode ): Promise { if (order === 'DESC') { - return this.redis.zrange(key, min, max, 'REV') + return this.redis.zrange(key, start, stop, 'REV') } else { - return this.redis.zrange(key, min, max) + return this.redis.zrange(key, start, stop) } } } From 160dc86b5a0edea8b9b0b699d7ff51a64ef82f1e Mon Sep 17 00:00:00 2001 From: Alex Atallah Date: Mon, 13 Mar 2023 00:59:00 -0700 Subject: [PATCH 7/7] fix many to many PR merge, clean up adapter vars --- README.md | 17 +++++++++-------- src/adapters/base.ts | 5 +++-- src/adapters/ioredis.ts | 38 +++++++++++++++++++------------------- src/redbase.ts | 10 +++------- test/database.spec.ts | 4 +--- 5 files changed, 35 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index 75a3f14..8234a5c 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ type MyValue = { } // Options can also use your own Redis instance if already defined, -// as `redisInstance` +// as `redis` const db = new Redbase('myProject', { redisUrl: 'redis://...' }) const key = uuid() @@ -150,9 +150,10 @@ Now we want to store this data in Redbase. Rather than store all posts in an arr ```ts // Slightly more efficient to share the same redis instance: -const redisInstance = initRedis() -const users = new Redbase('myProject-user', { redisInstance }) -const posts = new Redbase('myProject-post', { redisInstance }) +import { IORedis } from 'redbase' +const redis = new IORedis() +const users = new Redbase('myProject-user', { redis }) +const posts = new Redbase('myProject-post', { redis }) ``` When inserting a new Post for a given `userId`, make sure you tag it: @@ -185,14 +186,14 @@ interface Category { For many-to-many relationships, e.g. `Post` <-> `Category`, you can make the save atomic inside a Redis transaction by calling `multiSet` instead of `save`: ```ts -const redisInstance = initRedis() -const categories = new Redbase('myProject-category', { redisInstance }) -const posts = new Redbase('myProject-post', { redisInstance }) +const redis = new IORedis() +const categories = new Redbase('myProject-category', { redis }) +const posts = new Redbase('myProject-post', { redis }) const post = { id, ... } const categoryNames = ['tech', 'draft', ...] -let multi = redisInstance.multi() +let multi = redis.multi() multi = posts.multiSet(multi, post.postId, post, { tags: categoryNames }) for (const name of categoryNames) { multi = categories.multiSet(name, { name }, { tags: [`post-${post.id}`] }) diff --git a/src/adapters/base.ts b/src/adapters/base.ts index b74c33a..e5c326f 100644 --- a/src/adapters/base.ts +++ b/src/adapters/base.ts @@ -1,5 +1,6 @@ -export const defaultLogger = (err: unknown) => { - console.error('Redbase backend error', err) +export const defaultErrorHandler = (err: unknown): void | never => { + // Screetching halt failure by default + throw err } export type RawValue = string | number | Buffer diff --git a/src/adapters/ioredis.ts b/src/adapters/ioredis.ts index fb7233f..75ed3cc 100644 --- a/src/adapters/ioredis.ts +++ b/src/adapters/ioredis.ts @@ -1,7 +1,7 @@ import Redis, { ChainableCommander } from 'ioredis' import { RedisAdapter, - defaultLogger, + defaultErrorHandler, RedisMultiAdapter, RawValue, AggregationMode, @@ -12,12 +12,12 @@ const DEFAULT_URL = process.env['REDIS_URL'] || 'redis://localhost:6379' export class IORedisMulti extends RedisMultiAdapter { multi: ChainableCommander - redis: IORedis + errorHandler: (err: unknown) => void - constructor(ioRedis: IORedis) { + constructor(origRedis: Redis, errorHandler = defaultErrorHandler) { super() - this.redis = ioRedis - this.multi = ioRedis.redis.multi() + this.multi = origRedis.multi() + this.errorHandler = errorHandler } set(key: string, value: RawValue) { @@ -45,7 +45,7 @@ export class IORedisMulti extends RedisMultiAdapter { const res = await this.multi.exec() if (!res || res.map(r => r[0]).filter(e => !!e).length) { // Errors occurred during the exec, so record backend error - this.redis.errorHandler(res) + this.errorHandler(res) } } @@ -91,41 +91,41 @@ export class IORedisMulti extends RedisMultiAdapter { } export class IORedis extends RedisAdapter { - redis: Redis + origRedis: Redis errorHandler: (err: unknown) => void - constructor(url = DEFAULT_URL, errorHandler = defaultLogger) { + constructor(url = DEFAULT_URL, errorHandler = defaultErrorHandler) { super() - this.redis = new Redis(url, { + this.origRedis = new Redis(url, { enableAutoPipelining: true, }) this.errorHandler = errorHandler - this.redis.on('error', errorHandler) + this.origRedis.on('error', errorHandler) } multi() { - return new IORedisMulti(this) + return new IORedisMulti(this.origRedis, this.errorHandler) } async quit() { - await this.redis.quit() + await this.origRedis.quit() } async ttl(key: string) { - return this.redis.ttl(key) + return this.origRedis.ttl(key) } async get(key: string) { - return this.redis.get(key) + return this.origRedis.get(key) } async del(keys: string[]) { - return this.redis.del(...keys) + return this.origRedis.del(...keys) } async smembers(key: string): Promise { - return this.redis.smembers(key) + return this.origRedis.smembers(key) } async zcount( @@ -133,7 +133,7 @@ export class IORedis extends RedisAdapter { min: Score = '-inf', max: Score = '+inf' ): Promise { - return this.redis.zcount(key, min, max) + return this.origRedis.zcount(key, min, max) } async zrange( @@ -143,9 +143,9 @@ export class IORedis extends RedisAdapter { order?: OrderingMode ): Promise { if (order === 'DESC') { - return this.redis.zrange(key, start, stop, 'REV') + return this.origRedis.zrange(key, start, stop, 'REV') } else { - return this.redis.zrange(key, start, stop) + return this.origRedis.zrange(key, start, stop) } } } diff --git a/src/redbase.ts b/src/redbase.ts index 15a1327..210d24c 100644 --- a/src/redbase.ts +++ b/src/redbase.ts @@ -6,7 +6,7 @@ const GLOBAL_PREFIX = process.env['REDIS_PREFIX'] || '' const DEBUG = process.env['DEBUG'] === 'true' const AGG_TAG_TTL_BUFFER = 0.1 // seconds export interface Options { - redisAdapter?: RedisAdapter // Redis adapter to use. Defaults to IORedis. + redis?: RedisAdapter // Redis adapter to use. Defaults to IORedis. redisUrl?: string // Redis URL to use. Defaults to REDIS_URL in the environment. defaultTTL?: number // Default expiration (in seconds) to use for each entry. Defaults to undefined. aggregateTagTTL?: number // TTL for computed query tags. Defaults to 10 seconds @@ -88,7 +88,7 @@ export class Redbase { private _aggregateTagTTL: number constructor(name: string, opts: Options = {}) { - this.redis = opts.redisAdapter || new IORedis(opts.redisUrl) + this.redis = opts.redis || new IORedis(opts.redisUrl) this._defaultTTL = this._validateTTL(opts.defaultTTL) this._aggregateTagTTL = this._validateTTL(opts.aggregateTagTTL) || 10 // seconds this.deletionPageSize = opts.deletionPageSize || 2000 @@ -140,11 +140,7 @@ export class Redbase { ): Promise { let txn = this.redis.multi() txn = this.multiSet(txn, id, value, opts) - const res = await txn.exec() - if (!res || res.map(r => r[0]).filter(e => !!e).length) { - // Errors occurred during the save, so throw - throw res - } + await txn.exec() } /** diff --git a/test/database.spec.ts b/test/database.spec.ts index 17c43f7..cb69f2a 100644 --- a/test/database.spec.ts +++ b/test/database.spec.ts @@ -32,9 +32,7 @@ describe('Redbase', () => { }) it('should allow other clients to use the same Redis instance', () => { - const otherDb = new Redbase('Test-backup', { - redisInstance: db.redis, - }) + const otherDb = new Redbase('Test-backup', { redis: db.redis }) expect(db.redis).toStrictEqual(otherDb.redis) }) })