diff --git a/.gitignore b/.gitignore index c3046b5..aa4ebe0 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ tsOut/ .vscode/ coverage/ .nyc_output/ +.DS_Store diff --git a/.travis.yml b/.travis.yml index e9151d8..b8ccbee 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,14 +1,17 @@ language: node_js node_js: - - "stable" - - "lts/*" - - "8" + - 'stable' + - 'lts/*' + - '8' services: - redis-server -before_script: +before_script: - npm run build script: - npm run test - npm run coverage:failIfLow -after_success: +after_success: - npm run coverage:coveralls +env: + - NOHM_TEST_IOREDIS="false" + - NOHM_TEST_IOREDIS="true" diff --git a/docs/api/NohmClass.html b/docs/api/NohmClass.html index 7896bd4..6bc8abd 100644 --- a/docs/api/NohmClass.html +++ b/docs/api/NohmClass.html @@ -176,7 +176,7 @@

(async) factor
Source:
@@ -428,7 +428,7 @@

getModelsSource:
@@ -981,7 +981,7 @@

modelSource:
@@ -1199,7 +1199,7 @@

(async) purgeD
Source:
@@ -1346,7 +1346,7 @@

registerSource:
diff --git a/docs/api/NohmModel.html b/docs/api/NohmModel.html index 198cf88..53d88bb 100644 --- a/docs/api/NohmModel.html +++ b/docs/api/NohmModel.html @@ -309,7 +309,7 @@

idSource:
@@ -373,7 +373,7 @@

isDirtySource:
@@ -437,7 +437,7 @@

(readonly) is
Source:
@@ -1398,7 +1398,7 @@

getDefi
Source:
@@ -3204,7 +3204,7 @@

(async) subs
Source:
@@ -3382,7 +3382,7 @@

(async)
Source:
@@ -3953,7 +3953,7 @@

unsub
Source:
diff --git a/docs/api/NohmStaticModel.html b/docs/api/NohmStaticModel.html index 5e31d80..b976a1a 100644 --- a/docs/api/NohmStaticModel.html +++ b/docs/api/NohmStaticModel.html @@ -65,7 +65,7 @@

new No
Source:
@@ -161,7 +161,7 @@

(static)
Source:
@@ -235,7 +235,7 @@

(static) Source:
@@ -318,7 +318,7 @@

(async, static)
Source:
@@ -500,7 +500,7 @@

(async, static)
Source:
@@ -651,7 +651,7 @@

(async, static) Source:
@@ -832,7 +832,7 @@

(async, static) Source:
@@ -985,7 +985,7 @@

(async, static) Source:
@@ -1136,7 +1136,7 @@

(async, static) Source:
diff --git a/docs/api/tsOut_index.js.html b/docs/api/tsOut_index.js.html index 8486c93..ad2f574 100644 --- a/docs/api/tsOut_index.js.html +++ b/docs/api/tsOut_index.js.html @@ -165,11 +165,13 @@

tsOut/index.js

* Note: this will not affect models that have a client set on their own. */ setClient(client) { - debug('Setting new redis client. Connected: %s; Address: %s.', client && client.connected, client && client.address); - if (client && !client.connected) { + debug('Setting new redis client. Connected: %s; Address: %s.', client && (client.connected || client.status === 'ready'), client && client.address); + // ioredis uses .status string instead of .connected boolean + if (client && !(client.connected || client.status === 'ready')) { this .logError(`WARNING: setClient() received a redis client that is not connected yet. - Consider waiting for an established connection before setting it.`); + Consider waiting for an established connection before setting it. Status (if ioredis): ${client.status} + , connected (if node_redis): ${client.connected}`); } else if (!client) { client = redis.createClient(); @@ -650,7 +652,7 @@

tsOut/index.js

async purgeDb(client = this.client) { function delKeys(prefix) { return new Promise((resolve, reject) => { - client.KEYS(prefix + '*', (err, keys) => { + client.keys(prefix + '*', (err, keys) => { if (err) { reject(err); } @@ -658,7 +660,7 @@

tsOut/index.js

resolve(); } else { - client.DEL(keys, (innerErr) => { + client.del(keys, (innerErr) => { if (innerErr) { reject(innerErr); } @@ -734,7 +736,7 @@

tsOut/index.js

this.publishEventEmitter = new events_1.EventEmitter(); this.publishEventEmitter.setMaxListeners(0); // TODO: check if this is sensible this.isPublishSubscribed = true; - await typed_redis_helper_1.PSUBSCRIBE(this.publishClient, this.prefix.channel + PUBSUB_ALL_PATTERN); + await typed_redis_helper_1.psubscribe(this.publishClient, this.prefix.channel + PUBSUB_ALL_PATTERN); debugPubSub(`Redis PSUBSCRIBE for '%s'.`, this.prefix.channel + PUBSUB_ALL_PATTERN); this.publishClient.on('pmessage', (_pattern, channel, message) => { const suffix = channel.slice(this.prefix.channel.length); @@ -782,7 +784,7 @@

tsOut/index.js

if (this.isPublishSubscribed === true) { debugPubSub(`Redis PUNSUBSCRIBE for '%s'.`, this.prefix.channel + PUBSUB_ALL_PATTERN); this.isPublishSubscribed = false; - await typed_redis_helper_1.PUNSUBSCRIBE(this.publishClient, this.prefix.channel + PUBSUB_ALL_PATTERN); + await typed_redis_helper_1.punsubscribe(this.publishClient, this.prefix.channel + PUBSUB_ALL_PATTERN); } return this.publishClient; } diff --git a/docs/api/tsOut_model.js.html b/docs/api/tsOut_model.js.html index a1955cf..2bfed39 100644 --- a/docs/api/tsOut_model.js.html +++ b/docs/api/tsOut_model.js.html @@ -200,14 +200,14 @@

tsOut/model.js

} }); try { - const dbVersion = await typed_redis_helper_1.GET(this.client, versionKey); + const dbVersion = await typed_redis_helper_1.get(this.client, versionKey); if (this.meta.version !== dbVersion) { const generator = this.options.idGenerator || 'default'; debug(`Setting meta for model '%s' with version '%s' and idGenerator '%s' to %j.`, this.modelName, this.meta.version, generator, properties); await Promise.all([ - typed_redis_helper_1.SET(this.client, idGeneratorKey, generator.toString()), - typed_redis_helper_1.SET(this.client, propertiesKey, JSON.stringify(properties)), - typed_redis_helper_1.SET(this.client, versionKey, this.meta.version), + typed_redis_helper_1.set(this.client, idGeneratorKey, generator.toString()), + typed_redis_helper_1.set(this.client, propertiesKey, JSON.stringify(properties)), + typed_redis_helper_1.set(this.client, versionKey, this.meta.version), ]); } this.meta.inDb = true; @@ -480,7 +480,7 @@

tsOut/model.js

} let numIdExisting = 0; if (action !== 'create') { - numIdExisting = await typed_redis_helper_1.SISMEMBER(this.client, this.prefix('idsets'), this.id); + numIdExisting = await typed_redis_helper_1.sismember(this.client, this.prefix('idsets'), this.id); } if (action === 'create' && numIdExisting === 0) { debug(`Creating new instance '%s.%s' because action was '%s' and numIdExisting was %d.`, this.modelName, this.id, action, numIdExisting); @@ -498,7 +498,7 @@

tsOut/model.js

} async create() { const id = await this.generateId(); - await typed_redis_helper_1.SADD(this.client, this.prefix('idsets'), id); + await typed_redis_helper_1.sadd(this.client, this.prefix('idsets'), id); await this.setUniqueIds(id); this.id = id; } @@ -542,7 +542,7 @@

tsOut/model.js

} if (mSetArguments.length !== 0) { debug(`Setting all unique indices of model '%s.%s' to new id '%s'.`, this.modelName, this.id, id); - return typed_redis_helper_1.MSET(this.client, mSetArguments); + return typed_redis_helper_1.mset(this.client, mSetArguments); } } async update(options) { @@ -550,7 +550,7 @@

tsOut/model.js

throw new Error('Update was called without having an id set.'); } const hmSetArguments = []; - const client = this.client.MULTI(); + const client = this.client.multi(); const isCreate = !this.inDb; hmSetArguments.push(`${this.prefix('hash')}:${this.id}`); for (const [key, prop] of this.properties) { @@ -560,10 +560,10 @@

tsOut/model.js

} if (hmSetArguments.length > 1) { hmSetArguments.push('__meta_version', this.meta.version); - client.HMSET.apply(client, hmSetArguments); + client.hmset.apply(client, hmSetArguments); } await this.setIndices(client); - await typed_redis_helper_1.EXEC(client); + await typed_redis_helper_1.exec(client); const linkResults = await this.storeLinks(options); this.relationChanges = []; const linkFailures = linkResults.filter((linkResult) => !linkResult.success); @@ -654,7 +654,7 @@

tsOut/model.js

const foreignName = `${change.options.name}Foreign`; const command = change.action === 'link' ? 'sadd' : 'srem'; const relationKeyPrefix = this.rawPrefix().relationKeys; - const multi = this.client.MULTI(); + const multi = this.client.multi(); // relation to other const toKey = this.getRelationKey(change.object.modelName, change.options.name); // first store the information to which other model names the instance has a relation to @@ -667,7 +667,7 @@

tsOut/model.js

multi[command](fromKey, this.stringId()); try { debug(`Linking in redis.`, this.modelName, change.options.name, command); - await typed_redis_helper_1.EXEC(multi); + await typed_redis_helper_1.exec(multi); if (!change.options.silent) { this.fireEvent(change.action, change.object, change.options.name); } @@ -704,7 +704,7 @@

tsOut/model.js

oldUniqueValue = oldUniqueValue.toLowerCase(); } debug(`Removing old unique '%s' from '%s.%s' because propUpdated: %o && this.inDb %o.`, key, this.modelName, this.id, prop.__updated, this.inDb); - multi.DEL(`${this.prefix('unique')}:${key}:${oldUniqueValue}`, this.nohmClass.logError); + multi.del(`${this.prefix('unique')}:${key}:${oldUniqueValue}`, this.nohmClass.logError); } // set new normal index if (isIndex && isDirty) { @@ -712,14 +712,14 @@

tsOut/model.js

debug(`Adding numeric index '%s' to '%s.%s'.`, key, this.modelName, this.id, prop.__updated); // we use scored sets for things like "get all users older than 5" const scoredPrefix = this.prefix('scoredindex'); - multi.ZADD(`${scoredPrefix}:${key}`, prop.value, this.stringId(), this.nohmClass.logError); + multi.zadd(`${scoredPrefix}:${key}`, prop.value, this.stringId(), this.nohmClass.logError); } debug(`Adding index '%s' to '%s.%s'; isInDb: '%s'; newValue: '%s'; oldValue: '%s'.`, key, this.modelName, this.stringId(), isInDb, prop.value, oldValue); const prefix = this.prefix('index'); if (isInDb) { - multi.SREM(`${prefix}:${key}:${oldValue}`, this.stringId(), this.nohmClass.logError); + multi.srem(`${prefix}:${key}:${oldValue}`, this.stringId(), this.nohmClass.logError); } - multi.SADD(`${prefix}:${key}:${prop.value}`, this.stringId(), this.nohmClass.logError); + multi.sadd(`${prefix}:${key}:${prop.value}`, this.stringId(), this.nohmClass.logError); } } } @@ -859,10 +859,10 @@

tsOut/model.js

* We lock the unique value here if it's not locked yet, then later remove the old uniquelock * when really saving it. (or we free the unique slot if we're not saving) */ - dbValue = await typed_redis_helper_1.SETNX(this.client, key, this.stringId()); + dbValue = await typed_redis_helper_1.setnx(this.client, key, this.stringId()); } else { - dbValue = await typed_redis_helper_1.EXISTS(this.client, key); + dbValue = await typed_redis_helper_1.exists(this.client, key); } let isFreeUnique = false; if (setDirectly && dbValue === 1) { @@ -878,7 +878,7 @@

tsOut/model.js

// setDirectly === true means using setnx which returns 1 if the value did *not* exist // if it did exist, we check if the unique is the same as the one on this model. // see https://github.com/maritz/nohm/issues/82 for use-case - const dbId = await typed_redis_helper_1.GET(this.client, key); + const dbId = await typed_redis_helper_1.get(this.client, key); if (dbId === this.stringId()) { isFreeUnique = true; } @@ -926,7 +926,7 @@

tsOut/model.js

if (this.tmpUniqueKeys.length > 0) { debug(`Clearing temp uniques '%o' for '%s.%s'.`, this.tmpUniqueKeys, this.modelName, this.id); const deletes = this.tmpUniqueKeys.map((key) => { - return typed_redis_helper_1.DEL(this.client, key); + return typed_redis_helper_1.del(this.client, key); }); await Promise.all(deletes); } @@ -957,7 +957,7 @@

tsOut/model.js

} async deleteDbCall() { // TODO: write test for removal of relationKeys - purgeDb kinda tests it already, but not enough - const multi = this.client.MULTI(); + const multi = this.client.multi(); multi.del(`${this.prefix('hash')}:${this.stringId()}`); multi.srem(this.prefix('idsets'), this.stringId()); this.properties.forEach((prop, key) => { @@ -976,7 +976,7 @@

tsOut/model.js

} }); await this.unlinkAll(multi); - await typed_redis_helper_1.EXEC(multi); + await typed_redis_helper_1.exec(multi); } /** * Returns a Promsie that resolves to true if the given id exists for this model. @@ -986,12 +986,12 @@

tsOut/model.js

*/ async exists(id) { helpers_1.callbackError(...arguments); - return !!(await typed_redis_helper_1.SISMEMBER(this.client, this.prefix('idsets'), id)); + return !!(await typed_redis_helper_1.sismember(this.client, this.prefix('idsets'), id)); } async getHashAll(id) { const props = {}; - const values = await typed_redis_helper_1.HGETALL(this.client, `${this.prefix('hash')}:${id}`); - if (values === null) { + const values = await typed_redis_helper_1.hgetall(this.client, `${this.prefix('hash')}:${id}`); + if (values === null || Object.keys(values).length === 0) { throw new Error('not found'); } Object.keys(values).forEach((key) => { @@ -1156,15 +1156,15 @@

tsOut/model.js

multi = givenClient; } else if (givenClient) { - multi = givenClient.MULTI(); + multi = givenClient.multi(); } else { - multi = this.client.MULTI(); + multi = this.client.multi(); } // remove outstanding relation changes this.relationChanges = []; const relationKeysKey = `${this.rawPrefix().relationKeys}${this.modelName}:${this.id}`; - const keys = await typed_redis_helper_1.SMEMBERS(this.client, relationKeysKey); + const keys = await typed_redis_helper_1.smembers(this.client, relationKeysKey); debug(`Remvoing links for '%s.%s': %o.`, this.modelName, this.id, keys); const others = keys.map((key) => { const matches = key.match(/:([\w]*):([\w]*):[^:]+$/i); @@ -1186,11 +1186,11 @@

tsOut/model.js

const otherRelationIdsPromises = others.map((item) => this.removeIdFromOtherRelations(multi, item)); await Promise.all(otherRelationIdsPromises); // add multi'ed delete commands for other keys - others.forEach((item) => multi.DEL(item.ownIdsKey)); + others.forEach((item) => multi.del(item.ownIdsKey)); multi.del(relationKeysKey); // if we didn't get a multi client from the callee we have to exec() ourselves if (!this.isMultiClient(givenClient)) { - await typed_redis_helper_1.EXEC(multi); + await typed_redis_helper_1.exec(multi); } } /* @@ -1200,9 +1200,9 @@

tsOut/model.js

Secondly it adds an SREM to the multi redis client. */ async removeIdFromOtherRelations(multi, item) { - const ids = await typed_redis_helper_1.SMEMBERS(this.client, item.ownIdsKey); + const ids = await typed_redis_helper_1.smembers(this.client, item.ownIdsKey); ids.forEach((id) => { - multi.SREM(`${item.otherIdsKey}${id}`, this.stringId()); + multi.srem(`${item.otherIdsKey}${id}`, this.stringId()); }); } /** @@ -1217,7 +1217,7 @@

tsOut/model.js

if (!this.id || !obj.id) { throw new Error('Calling belongsTo() even though either the object itself or the relation does not have an id.'); } - return !!(await typed_redis_helper_1.SISMEMBER(this.client, this.getRelationKey(obj.modelName, relationName), obj.id)); + return !!(await typed_redis_helper_1.sismember(this.client, this.getRelationKey(obj.modelName, relationName), obj.id)); } /** * Returns an array of the ids of all objects that are linked with the given relation. @@ -1231,7 +1231,7 @@

tsOut/model.js

throw new Error(`Calling getAll() even though this ${this.modelName} has no id. Please load or save it first.`); } const relationKey = this.getRelationKey(otherModelName, relationName); - const ids = await typed_redis_helper_1.SMEMBERS(this.client, relationKey); + const ids = await typed_redis_helper_1.smembers(this.client, relationKey); if (!Array.isArray(ids)) { return []; } @@ -1253,7 +1253,7 @@

tsOut/model.js

throw new Error(`Calling numLinks() even though this ${this.modelName} has no id. Please load or save it first.`); } const relationKey = this.getRelationKey(otherModelName, relationName); - return typed_redis_helper_1.SCARD(this.client, relationKey); + return typed_redis_helper_1.scard(this.client, relationKey); } /** * Finds ids of objects by search arguments @@ -1273,7 +1273,7 @@

tsOut/model.js

const onlyZSets = structuredSearches.filter((search) => search.type === 'zset'); if (onlySets.length === 0 && onlyZSets.length === 0) { // no valid searches - return all ids - return typed_redis_helper_1.SMEMBERS(this.client, `${this.prefix('idsets')}`); + return typed_redis_helper_1.smembers(this.client, `${this.prefix('idsets')}`); } debug(`Finding '%s's with these searches (sets, zsets):\n%o,\n%o.`, this.modelName, this.id, onlySets, onlyZSets); const setPromises = this.setSearch(onlySets); @@ -1338,7 +1338,7 @@

tsOut/model.js

} async uniqueSearch(options) { const key = `${this.prefix('unique')}:${options.key}:${options.value}`; - const id = await typed_redis_helper_1.GET(this.client, key); + const id = await typed_redis_helper_1.get(this.client, key); if (id) { return [id]; } @@ -1354,7 +1354,7 @@

tsOut/model.js

// shortcut return []; } - return typed_redis_helper_1.SINTER(this.client, keys); + return typed_redis_helper_1.sinter(this.client, keys); } async zSetSearch(searches) { const singleSearches = await Promise.all(searches.map((search) => this.singleZSetSearch(search))); @@ -1363,12 +1363,12 @@

tsOut/model.js

singleZSetSearch(search) { return new Promise((resolve, reject) => { const key = `${this.prefix('scoredindex')}:${search.key}`; - let command = 'ZRANGEBYSCORE'; + let command = 'zrangebyscore'; const options = Object.assign({ endpoints: '[]', limit: -1, max: '+inf', min: '-inf', offset: 0 }, search.options); if ((options.min === '+inf' && options.max !== '+inf') || (options.max === '-inf' && options.min !== '-inf') || parseFloat('' + options.min) > parseFloat('' + options.max)) { - command = 'ZREVRANGEBYSCORE'; + command = 'zrevrangebyscore'; } if (options.endpoints === ')') { options.endpoints = '[)'; @@ -1439,9 +1439,9 @@

tsOut/model.js

} let idsetKey = this.prefix('idsets'); let zsetKey = `${this.prefix('scoredindex')}:${options.field}`; - const client = this.client.MULTI(); + const client = this.client.multi(); let tmpKey = ''; - debug(`Sorting '%s's with these options (alpha, direction, scored, start, stop, ids):`, this.modelName, this.id, alpha, direction, scored, start, stop, ids); + debug(`Sorting '%s's with these options (this.id, alpha, direction, scored, start, stop, ids):`, this.modelName, this.id, alpha, direction, scored, start, stop, ids); if (ids) { // to get the intersection of the given ids and all ids on the server we first // temporarily store the given ids either in a set or sorted set and then return the intersection @@ -1466,22 +1466,37 @@

tsOut/model.js

+new Date() + Math.ceil(Math.random() * 1000); ids.unshift(tmpKey); - client.SADD.apply(client, ids); // typecast because rediss doesn't care about numbers/string - client.SINTERSTORE([tmpKey, tmpKey, idsetKey]); + client.sadd.apply(client, ids); // typecast because rediss doesn't care about numbers/string + client.sinterstore([tmpKey, tmpKey, idsetKey]); idsetKey = tmpKey; } } if (scored) { - const method = direction && direction === 'DESC' ? 'ZREVRANGE' : 'ZRANGE'; + const method = direction && direction === 'DESC' ? 'zrevrange' : 'zrange'; client[method](zsetKey, start, stop); } else { - client.sort(idsetKey, 'BY', `${this.prefix('hash')}:*->${options.field}`, 'LIMIT', String(start), String(stop), direction, alpha); + const args = [ + idsetKey, + 'BY', + `${this.prefix('hash')}:*->${options.field}`, + 'LIMIT', + String(start), + String(stop), + direction, + ]; + if (alpha) { + // due to the way ioredis handles input parameters, we have to do this weird apply and append thing. + // when just passing in an undefined value it throws syntax errors because it attempts to add that as an actual + // parameter + args.push(alpha); + } + client.sort.apply(client, args); } if (ids) { client.del(tmpKey); } - const replies = await typed_redis_helper_1.EXEC(client); + const replies = await typed_redis_helper_1.exec(client); let reply; if (ids) { // 2 redis commands to create the temp keys, then the query diff --git a/docs/index.md b/docs/index.md index 23e37e1..818cee6 100644 --- a/docs/index.md +++ b/docs/index.md @@ -109,14 +109,39 @@ nohm.setPrefix('yourAppPrefixForRedis'); You need to set a redis client for nohm. You should connect and select the appropriate database before you set it. +##### node_redis + +The standard method would be to use node_redis: + ```javascript -const redisClient = require('redis').createClient(); +const redis = require('redis'); +const redisClient = redis.createClient(); // wait for redis to connect, to make sure everything will work as expected redis.on('connect', () => { nohm.setClient(redisClient); }); ``` +##### ioredis + +_(available in v2.2.0 onwards)_ +You can also use ioredis instead of node_redis. + +```javascript +const Redis = require('ioredis'); +const redisClient = new Redis(); +// wait for ioredis to be ready, to make sure everything will work as expected +redis.on('ready', () => { + nohm.setClient(redisClient); +}); +``` + +In the [simple stress tests](https://github.com/maritz/nohm/blob/master/extra/stress.js) using ioredis shows [more than a 20% decrease in performance](https://github.com/maritz/nohm/pull/138#issuecomment-417621930). + +If you require ioredis for other parts of your application and performance is important to you, the best option is probably to use ioredis for that and node\*redis for nohm. There _should_ be no conflicts when using both in the same application. Tread with caution though and test and benchmark it yourself for your use-cases. + +_Note: Clustering and sharding is not yet tested or officially supported by nohm._ + #### Logging By default nohm just logs errors it encounters to the console. However you can overwrite the logError method with anything you want: diff --git a/extra/stress.js b/extra/stress.js index bb8a4ba..15886e1 100644 --- a/extra/stress.js +++ b/extra/stress.js @@ -1,14 +1,71 @@ -var nohm = require(__dirname + '/../').Nohm, - iterations = 10000; +const nohm = require(__dirname + '/../').Nohm; +let iterations = 10000; -const redisClient = require('redis').createClient(); +let redisOptions = {}; -redisClient.once('connect', async () => { +process.argv.forEach(function(val, index) { + if (val === '--nohm-prefix') { + redisOptions.prefix = process.argv[index + 1]; + } + if (val === '--redis-host') { + redisOptions.redis_host = process.argv[index + 1]; + } + if (val === '--redis-port') { + redisOptions.redis_port = process.argv[index + 1]; + } + if (val === '--redis-auth') { + redisOptions.redis_auth = process.argv[index + 1]; + } + if (val === '--iterations') { + iterations = parseInt(process.argv[index + 1], 10); + if (isNaN(iterations)) { + throw new Error( + `Invalid iterations argument: ${ + process.argv[index + 1] + }. Must be a number.`, + ); + } + } +}); + +let redisClient; + +const main = () => { + console.info('Connected to redis.'); + stress().catch((error) => { + console.error('An error occured during benchmarking:', error); + process.exit(1); + }); +}; + +if (process.env.NOHM_TEST_IOREDIS == 'true') { + console.info('Using ioredis for stress test'); + const Redis = require('ioredis'); + + redisClient = new Redis({ + port: redisOptions.redis_port, + host: redisOptions.redis_host, + password: redisOptions.redis_auth, + }); + redisClient.once('ready', main); +} else { + console.info('Using node_redis for stress test'); + redisClient = require('redis').createClient( + redisOptions.redis_port, + redisOptions.redis_host, + { + auth_pass: redisOptions.redis_auth, + }, + ); + redisClient.once('connect', main); +} + +const stress = async () => { console.log( `Starting stress test - saving and then updating ${iterations} models in parallel.`, ); - nohm.setPrefix('nohm-stress-test'); + nohm.setPrefix(redisOptions.prefix || 'nohm-stress-test'); nohm.setClient(redisClient); var models = require(__dirname + '/models'); @@ -30,7 +87,7 @@ redisClient.once('connect', async () => { counter++; if (counter >= iterations) { const updateTime = Date.now() - startUpdates; - const timePerUpdate = iterations / updateTime * 1000; + const timePerUpdate = (iterations / updateTime) * 1000; console.log( `${updateTime}ms for ${counter} parallel User updates, ${timePerUpdate.toFixed( 2, @@ -38,7 +95,7 @@ redisClient.once('connect', async () => { ); console.log(`Total time: ${Date.now() - start}ms`); console.log('Memory usage after', process.memoryUsage()); - redisClient.SCARD( + redisClient.scard( `${nohm.prefix.idsets}${new UserModel().modelName}`, async (err, numUsers) => { if (err) { @@ -96,7 +153,7 @@ redisClient.once('connect', async () => { if (counter >= iterations) { saveCallback = null; const saveTime = Date.now() - start; - const timePerSave = iterations / saveTime * 1000; + const timePerSave = (iterations / saveTime) * 1000; console.log( `${saveTime}ms for ${counter} parallel User saves, ${timePerSave.toFixed( 2, @@ -115,4 +172,4 @@ redisClient.once('connect', async () => { .catch(errorCallback); users.push(user); } -}); +}; diff --git a/package-lock.json b/package-lock.json index adc4de5..7776431 100644 --- a/package-lock.json +++ b/package-lock.json @@ -209,6 +209,15 @@ "@types/range-parser": "*" } }, + "@types/ioredis": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/ioredis/-/ioredis-4.0.1.tgz", + "integrity": "sha512-pvGlg7THjfjC6hzhOBVNtD0p+B0ph2FYx0wJjDBW7rpDLe9zazQgAt2WRWm3COW6Z0ZXqmCzPkq7WrOJWJklSQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/lodash": { "version": "4.14.112", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.112.tgz", @@ -948,6 +957,12 @@ } } }, + "cluster-key-slot": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.0.12.tgz", + "integrity": "sha512-21O0kGmvED5OJ7ZTdqQ5lQQ+sjuez33R+d35jZKLwqUb5mqcPHUsxOSzj61+LHVtxGZd1kShbQM3MjB/gBJkVg==", + "dev": true + }, "co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -1605,6 +1620,12 @@ "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", "dev": true }, + "denque": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-1.3.0.tgz", + "integrity": "sha512-4SRaSj+PqmrS1soW5/Avd7eJIM2JJIqLLmwhRqIGleZM/8KwZq80njbSS2Iqas+6oARkSkLDHEk4mm78q3JlIg==", + "dev": true + }, "diff": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/diff/-/diff-1.4.0.tgz", @@ -2216,6 +2237,12 @@ "write": "^0.2.1" } }, + "flexbuffer": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/flexbuffer/-/flexbuffer-0.0.6.tgz", + "integrity": "sha1-A5/fI/iCPkQMOPMnfm/vEXQhWzA=", + "dev": true + }, "for-in": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", @@ -3321,6 +3348,46 @@ "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=", "dev": true }, + "ioredis": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-4.0.0.tgz", + "integrity": "sha512-KDio3eKM4zZWRPWlcM26E4Dcbj1bH6pPLNuCHJwKucklsEVMXT0axh5ctPaETbkPIBLRk910qKOEQoXSFkn+dw==", + "dev": true, + "requires": { + "cluster-key-slot": "^1.0.6", + "debug": "^3.1.0", + "denque": "^1.1.0", + "flexbuffer": "0.0.6", + "lodash.bind": "^4.2.1", + "lodash.clone": "^4.5.0", + "lodash.clonedeep": "^4.5.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.foreach": "^4.5.0", + "lodash.isempty": "^4.4.0", + "lodash.partial": "^4.2.1", + "lodash.pick": "^4.4.0", + "lodash.sample": "^4.2.1", + "lodash.shuffle": "^4.2.0", + "lodash.values": "^4.3.0", + "redis-commands": "^1.2.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^1.0.0" + }, + "dependencies": { + "redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ=", + "dev": true, + "requires": { + "redis-errors": "^1.0.0" + } + } + } + }, "is-accessor-descriptor": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", @@ -3880,12 +3947,84 @@ "integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=", "dev": true }, + "lodash.bind": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/lodash.bind/-/lodash.bind-4.2.1.tgz", + "integrity": "sha1-euMBfpOWIqwxt9fX3LGzTbFpDTU=", + "dev": true + }, + "lodash.clone": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clone/-/lodash.clone-4.5.0.tgz", + "integrity": "sha1-GVhwRQ9aExkkeN9Lw9I9LeoZB7Y=", + "dev": true + }, + "lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=", + "dev": true + }, "lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=", "dev": true }, + "lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=", + "dev": true + }, + "lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha1-nMtOUF1Ia5FlE0V3KIWi3yf9AXw=", + "dev": true + }, + "lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=", + "dev": true + }, + "lodash.foreach": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-4.5.0.tgz", + "integrity": "sha1-Gmo16s5AEoDH8G3d7DUWWrJ+PlM=", + "dev": true + }, + "lodash.isempty": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.isempty/-/lodash.isempty-4.4.0.tgz", + "integrity": "sha1-b4bL7di+TsmHvpqvM8loTbGzHn4=", + "dev": true + }, + "lodash.partial": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/lodash.partial/-/lodash.partial-4.2.1.tgz", + "integrity": "sha1-SfPYz9qjv/izqR0SfpIyRUGJYdQ=", + "dev": true + }, + "lodash.pick": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz", + "integrity": "sha1-UvBWEP/53tQiYRRB7R/BI6AwAbM=", + "dev": true + }, + "lodash.sample": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/lodash.sample/-/lodash.sample-4.2.1.tgz", + "integrity": "sha1-XkKRsMdT+hq+sKq4+ynfG2bwf20=", + "dev": true + }, + "lodash.shuffle": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.shuffle/-/lodash.shuffle-4.2.0.tgz", + "integrity": "sha1-FFtQU8+HX29cKjP0i26ZSMbse0s=", + "dev": true + }, "lodash.template": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.4.0.tgz", @@ -3905,6 +4044,12 @@ "lodash._reinterpolate": "~3.0.0" } }, + "lodash.values": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.values/-/lodash.values-4.3.0.tgz", + "integrity": "sha1-o6bCsOvsxcLLocF+bmIP6BtT00c=", + "dev": true + }, "log-driver": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/log-driver/-/log-driver-1.2.7.tgz", @@ -6996,6 +7141,12 @@ "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.3.5.tgz", "integrity": "sha512-foGF8u6MXGFF++1TZVC6icGXuMYPftKXt1FBT2vrfU9ZATNtZJ8duRC5d1lEfE8hyVe3jhelHGB91oB7I6qLsA==" }, + "redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha1-62LSrbFeTq9GEMBK/hUpOEJQq60=", + "dev": true + }, "redis-parser": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-2.6.0.tgz", @@ -7573,6 +7724,12 @@ "integrity": "sha1-1PM6tU6OOHeLDKXP07OvsS22hiA=", "dev": true }, + "standard-as-callback": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-1.0.1.tgz", + "integrity": "sha512-izxEITSyc7S+5oOiF/URiYaNkemPUxIndCNv66jJ548Y1TVxhBvioNMSPrZIQdaZDlhnguOdUzHA/7hJ3xFhuQ==", + "dev": true + }, "standard-version": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/standard-version/-/standard-version-4.4.0.tgz", diff --git a/package.json b/package.json index 8ca3523..7ac1949 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ }, "dependencies": { "debug": "^3.1.0", + "ioredis": "^4.0.0", "lodash": "^4.17.10", "redis": "^2.8.0", "traverse": "^0.6.6", @@ -55,6 +56,7 @@ "@types/async": "^2.0.47", "@types/debug": "0.0.30", "@types/express": "^4.16.0", + "@types/ioredis": "^4.0.1", "@types/lodash": "^4.14.112", "@types/node": "^10.5.2", "@types/redis": "^2.8.4", diff --git a/test/featureTests.js b/test/featureTests.js index 0ae8fd9..a8c0593 100644 --- a/test/featureTests.js +++ b/test/featureTests.js @@ -937,14 +937,15 @@ exports.deleteNonExistant = async (t) => { try { await user.remove(); + t.fail('Removing a user that should not exist did not throw an error.'); } catch (err) { t.same( err, new Error('not found'), 'Trying to delete an instance that doesn\'t exist did not return "not found".', ); - t.done(); } + t.done(); }; const MethodOverwrite = nohm.model('methodOverwrite', { diff --git a/test/findTests.js b/test/findTests.js index 1aaa8d0..a8cd59d 100644 --- a/test/findTests.js +++ b/test/findTests.js @@ -116,6 +116,7 @@ exports.find = { email: 'numericindextest@hurgel.de', gender: 'male', number: 3, + numberNonIndexed: 4, }, { // id: 2 @@ -124,6 +125,7 @@ exports.find = { gender: 'male', number: 4, number2: 33, + numberNonIndexed: 4, }, { // id: 3 @@ -132,33 +134,39 @@ exports.find = { gender: 'female', number: 4, number2: 1, + numberNonIndexed: 1, }, { // id: 4 name: 'uniquefind', email: 'uniquefind@hurgel.de', + numberNonIndexed: 5, }, { // id: 5 name: 'indextest', email: 'indextest@hurgel.de', + numberNonIndexed: 8, }, { // id: 6 name: 'indextest', email: 'indextest2@hurgel.de', + numberNonIndexed: 200, }, { // id: 7 name: 'a_sort_first', email: 'a_sort_first@hurgel.de', number: 1, + numberNonIndexed: 0, }, { // id: 8 name: 'z_sort_last', email: 'z_sort_last@hurgel.de', number: 100000, + numberNonIndexed: 24.5, }, ], function(users, ids) { diff --git a/test/pubsubTests.js b/test/pubsubTests.js index d8de599..6665c9e 100644 --- a/test/pubsubTests.js +++ b/test/pubsubTests.js @@ -35,15 +35,21 @@ module.exports = { 'set/get pubSub client': async (t) => { t.expect(2); await nohm.setPubSubClient(secondaryClient); - t.same( - nohm.getPubSubClient(), - secondaryClient, - "Second redis client wasn't set properly", - ); - t.ok( - nohm.getPubSubClient().subscription_set, - "Second redis client isn't subscribed to anything", - ); + const client = nohm.getPubSubClient(); + t.same(client, secondaryClient, "Second redis client wasn't set properly"); + const isIoRedis = + client.connected === undefined && client.status === 'ready'; + if (isIoRedis) { + t.ok( + client.condition.subscriber !== false, + "Second redis client isn't subscribed to anything", + ); + } else { + t.ok( + client.subscription_set, + "Second redis client isn't subscribed to anything", + ); + } t.done(); }, diff --git a/test/redisHelperTests.js b/test/redisHelperTests.js index 915e3d3..471123e 100644 --- a/test/redisHelperTests.js +++ b/test/redisHelperTests.js @@ -57,25 +57,25 @@ const testCommand = (name, firstArg, secondArg, resolveExpect) => { }; }; -exports['GET'] = testCommand('GET', 'foo', undefined, 'bar'); -exports['EXISTS'] = testCommand('EXISTS', 'foo', undefined, true); -exports['DEL'] = testCommand('DEL', 'foo'); -exports['SET'] = testCommand('SET', 'foo', 'bar'); -exports['MSET'] = testCommand('MSET', 'foo', ['foo', 'bar']); -exports['SETNX'] = testCommand('SETNX', 'foo', 'bar', true); -exports['SMEMBERS'] = testCommand('SMEMBERS', 'foo', undefined, ['bar']); -exports['SISMEMBER'] = testCommand('SISMEMBER', 'foo', 'bar', 1); -exports['SADD'] = testCommand('SADD', 'foo', 'bar', 1); -exports['SINTER'] = testCommand('SINTER', 'foo', 'bar', ['bar', 'baz']); -exports['HGETALL'] = testCommand('HGETALL', 'foo', undefined, ['bar', 'baz']); -exports['PSUBSCRIBE'] = testCommand('PSUBSCRIBE', 'foo', ['bar', 'baz']); -exports['PUNSUBSCRIBE'] = testCommand('PUNSUBSCRIBE', 'foo', ['bar', 'baz']); +exports['get'] = testCommand('get', 'foo', undefined, 'bar'); +exports['exists'] = testCommand('exists', 'foo', undefined, true); +exports['del'] = testCommand('del', 'foo'); +exports['set'] = testCommand('set', 'foo', 'bar'); +exports['mset'] = testCommand('mset', 'foo', ['foo', 'bar']); +exports['setnx'] = testCommand('setnx', 'foo', 'bar', true); +exports['smembers'] = testCommand('smembers', 'foo', undefined, ['bar']); +exports['sismember'] = testCommand('sismember', 'foo', 'bar', 1); +exports['sadd'] = testCommand('sadd', 'foo', 'bar', 1); +exports['sinter'] = testCommand('sinter', 'foo', 'bar', ['bar', 'baz']); +exports['hgetall'] = testCommand('hgetall', 'foo', undefined, ['bar', 'baz']); +exports['psubscribe'] = testCommand('psubscribe', 'foo', ['bar', 'baz']); +exports['punsubscribe'] = testCommand('punsubscribe', 'foo', ['bar', 'baz']); -exports['EXEC'] = async (t) => { +exports['exec'] = async (t) => { t.expect(4); - // EXEC has no firstArg. it's easier to duplicate the test here instead of changing testCommand - const name = 'EXEC'; + // exec has no firstArg. it's easier to duplicate the test here instead of changing testCommand + const name = 'exec'; const resolveExpect = ['foo', 'baz']; const testMethod = redisHelper[name]; diff --git a/test/regressions.js b/test/regressions.js index a8dac41..702614c 100644 --- a/test/regressions.js +++ b/test/regressions.js @@ -51,7 +51,7 @@ exports['#114 update does not reset index'] = async (t) => { }); await instance2Activated.save(); - const membersTrue = await redisPromise.SMEMBERS( + const membersTrue = await redisPromise.smembers( redis, `${instance2Activated.prefix('index')}:isActive:true`, ); @@ -62,14 +62,14 @@ exports['#114 update does not reset index'] = async (t) => { 'Not all instances were properly indexed as isActive:true', ); - const membersFalse = await redisPromise.SMEMBERS( + const membersFalse = await redisPromise.smembers( redis, `${instance2Activated.prefix('index')}:isActive:false`, ); t.same(membersFalse, [], 'An index for isActive:false remained.'); - const uniqueExists = await redisPromise.EXISTS( + const uniqueExists = await redisPromise.exists( redis, `${instance2Activated.prefix('unique')}:uniqueDeletion:two`, ); diff --git a/test/testArgs.js b/test/testArgs.js index d5e9f15..2120280 100644 --- a/test/testArgs.js +++ b/test/testArgs.js @@ -23,36 +23,36 @@ process.argv.forEach(function(val, index) { } }); -exports.redis = require('redis').createClient( - exports.redis_port, - exports.redis_host, - { - auth_pass: exports.redis_auth, - }, -); +if (process.env.NOHM_TEST_IOREDIS == 'true') { + console.info('Using ioredis for tests'); + const Redis = require('ioredis'); -exports.secondaryClient = require('redis').createClient( - exports.redis_port, - exports.redis_host, - { - auth_pass: exports.redis_auth, - }, -); + exports.redis = new Redis({ + port: exports.redis_port, + host: exports.redis_host, + password: exports.redis_auth, + }); -/** - * -const Redis = require('ioredis'); -const client = exports.redis = new Redis({ - port: exports.port, - host: exports.host, - password: exports.redis_auth, -}); - -client.connected = false; + exports.secondaryClient = new Redis({ + port: exports.redis_port, + host: exports.redis_host, + password: exports.redis_auth, + }); +} else { + console.info('Using node_redis for tests'); + exports.redis = require('redis').createClient( + exports.redis_port, + exports.redis_host, + { + auth_pass: exports.redis_auth, + }, + ); -setInterval(() => { - if (!client.connected && client.status === 'ready') { - client.connected = true; - } -}) - */ + exports.secondaryClient = require('redis').createClient( + exports.redis_port, + exports.redis_host, + { + auth_pass: exports.redis_auth, + }, + ); +} diff --git a/ts/index.ts b/ts/index.ts index f3c2d25..6ed3163 100644 --- a/ts/index.ts +++ b/ts/index.ts @@ -33,7 +33,7 @@ import { timestampProperty, TTypedDefinitions, } from './model.header'; -import { PSUBSCRIBE, PUNSUBSCRIBE } from './typed-redis-helper'; +import { psubscribe, punsubscribe } from './typed-redis-helper'; export { boolProperty, @@ -217,13 +217,17 @@ export class NohmClass { public setClient(client?: redis.RedisClient) { debug( 'Setting new redis client. Connected: %s; Address: %s.', - client && client.connected, + client && (client.connected || (client as any).status === 'ready'), client && (client as any).address, ); - if (client && !client.connected) { + // ioredis uses .status string instead of .connected boolean + if (client && !(client.connected || (client as any).status === 'ready')) { this .logError(`WARNING: setClient() received a redis client that is not connected yet. - Consider waiting for an established connection before setting it.`); + Consider waiting for an established connection before setting it. Status (if ioredis): ${ + (client as any).status + } + , connected (if node_redis): ${client.connected}`); } else if (!client) { client = redis.createClient(); } @@ -816,13 +820,13 @@ export class NohmClass { public async purgeDb(client: redis.RedisClient = this.client): Promise { function delKeys(prefix: string) { return new Promise((resolve, reject) => { - client.KEYS(prefix + '*', (err, keys) => { + client.keys(prefix + '*', (err, keys) => { if (err) { reject(err); } else if (keys.length === 0) { resolve(); } else { - client.DEL(keys, (innerErr) => { + client.del(keys, (innerErr) => { if (innerErr) { reject(innerErr); } else { @@ -919,7 +923,7 @@ export class NohmClass { this.publishEventEmitter.setMaxListeners(0); // TODO: check if this is sensible this.isPublishSubscribed = true; - await PSUBSCRIBE( + await psubscribe( this.publishClient, this.prefix.channel + PUBSUB_ALL_PATTERN, ); @@ -1002,7 +1006,7 @@ export class NohmClass { this.prefix.channel + PUBSUB_ALL_PATTERN, ); this.isPublishSubscribed = false; - await PUNSUBSCRIBE( + await punsubscribe( this.publishClient, this.prefix.channel + PUBSUB_ALL_PATTERN, ); diff --git a/ts/model.ts b/ts/model.ts index 2ba77c2..9a9f6c0 100644 --- a/ts/model.ts +++ b/ts/model.ts @@ -35,19 +35,19 @@ import { TValidationDefinition, } from './model.header'; import { - DEL, - EXEC, - EXISTS, - GET, - HGETALL, - MSET, - SADD, - SCARD, - SET, - SETNX, - SINTER, - SISMEMBER, - SMEMBERS, + del, + exec, + exists, + get, + hgetall, + mset, + sadd, + scard, + set, + setnx, + sinter, + sismember, + smembers, } from './typed-redis-helper'; import { validators } from './validators'; @@ -274,7 +274,7 @@ abstract class NohmModel { }); try { - const dbVersion = await GET(this.client, versionKey); + const dbVersion = await get(this.client, versionKey); if (this.meta.version !== dbVersion) { const generator = this.options.idGenerator || 'default'; debug( @@ -285,9 +285,9 @@ abstract class NohmModel { properties, ); await Promise.all([ - SET(this.client, idGeneratorKey, generator.toString()), - SET(this.client, propertiesKey, JSON.stringify(properties)), - SET(this.client, versionKey, this.meta.version), + set(this.client, idGeneratorKey, generator.toString()), + set(this.client, propertiesKey, JSON.stringify(properties)), + set(this.client, versionKey, this.meta.version), ]); } this.meta.inDb = true; @@ -625,7 +625,7 @@ abstract class NohmModel { } let numIdExisting: number = 0; if (action !== 'create') { - numIdExisting = await SISMEMBER( + numIdExisting = await sismember( this.client, this.prefix('idsets'), this.id, @@ -653,7 +653,7 @@ abstract class NohmModel { private async create() { const id = await this.generateId(); - await SADD(this.client, this.prefix('idsets'), id); + await sadd(this.client, this.prefix('idsets'), id); await this.setUniqueIds(id); this.id = id; } @@ -709,7 +709,7 @@ abstract class NohmModel { this.id, id, ); - return MSET(this.client, mSetArguments); + return mset(this.client, mSetArguments); } } @@ -721,7 +721,7 @@ abstract class NohmModel { } const hmSetArguments = []; - const client = this.client.MULTI(); + const client = this.client.multi(); const isCreate = !this.inDb; hmSetArguments.push(`${this.prefix('hash')}:${this.id}`); @@ -734,12 +734,12 @@ abstract class NohmModel { if (hmSetArguments.length > 1) { hmSetArguments.push('__meta_version', this.meta.version); - client.HMSET.apply(client, hmSetArguments); + client.hmset.apply(client, hmSetArguments); } await this.setIndices(client); - await EXEC(client); + await exec(client); const linkResults = await this.storeLinks(options); this.relationChanges = []; @@ -855,7 +855,7 @@ abstract class NohmModel { const command = change.action === 'link' ? 'sadd' : 'srem'; const relationKeyPrefix = this.rawPrefix().relationKeys; - const multi = this.client.MULTI(); + const multi = this.client.multi(); // relation to other const toKey = this.getRelationKey( change.object.modelName, @@ -876,7 +876,7 @@ abstract class NohmModel { try { debug(`Linking in redis.`, this.modelName, change.options.name, command); - await EXEC(multi); + await exec(multi); if (!change.options.silent) { this.fireEvent(change.action, change.object, change.options.name); } @@ -922,7 +922,7 @@ abstract class NohmModel { prop.__updated, this.inDb, ); - multi.DEL( + multi.del( `${this.prefix('unique')}:${key}:${oldUniqueValue}`, this.nohmClass.logError, ); @@ -940,7 +940,7 @@ abstract class NohmModel { ); // we use scored sets for things like "get all users older than 5" const scoredPrefix = this.prefix('scoredindex'); - multi.ZADD( + multi.zadd( `${scoredPrefix}:${key}`, prop.value, this.stringId(), @@ -958,13 +958,13 @@ abstract class NohmModel { ); const prefix = this.prefix('index'); if (isInDb) { - multi.SREM( + multi.srem( `${prefix}:${key}:${oldValue}`, this.stringId(), this.nohmClass.logError, ); } - multi.SADD( + multi.sadd( `${prefix}:${key}:${prop.value}`, this.stringId(), this.nohmClass.logError, @@ -1155,9 +1155,9 @@ abstract class NohmModel { * We lock the unique value here if it's not locked yet, then later remove the old uniquelock * when really saving it. (or we free the unique slot if we're not saving) */ - dbValue = await SETNX(this.client, key, this.stringId()); + dbValue = await setnx(this.client, key, this.stringId()); } else { - dbValue = await EXISTS(this.client, key); + dbValue = await exists(this.client, key); } let isFreeUnique = false; if (setDirectly && dbValue === 1) { @@ -1171,7 +1171,7 @@ abstract class NohmModel { // setDirectly === true means using setnx which returns 1 if the value did *not* exist // if it did exist, we check if the unique is the same as the one on this model. // see https://github.com/maritz/nohm/issues/82 for use-case - const dbId = await GET(this.client, key); + const dbId = await get(this.client, key); if (dbId === this.stringId()) { isFreeUnique = true; } @@ -1246,7 +1246,7 @@ abstract class NohmModel { this.id, ); const deletes: Array> = this.tmpUniqueKeys.map((key) => { - return DEL(this.client, key); + return del(this.client, key); }); await Promise.all(deletes); } @@ -1281,7 +1281,7 @@ abstract class NohmModel { private async deleteDbCall(): Promise { // TODO: write test for removal of relationKeys - purgeDb kinda tests it already, but not enough - const multi = this.client.MULTI(); + const multi = this.client.multi(); multi.del(`${this.prefix('hash')}:${this.stringId()}`); multi.srem(this.prefix('idsets'), this.stringId()); @@ -1307,7 +1307,7 @@ abstract class NohmModel { await this.unlinkAll(multi); - await EXEC(multi); + await exec(multi); } /** @@ -1318,13 +1318,13 @@ abstract class NohmModel { */ public async exists(id: any): Promise { callbackError(...arguments); - return !!(await SISMEMBER(this.client, this.prefix('idsets'), id)); + return !!(await sismember(this.client, this.prefix('idsets'), id)); } private async getHashAll(id: any): Promise> { const props: Partial = {}; - const values = await HGETALL(this.client, `${this.prefix('hash')}:${id}`); - if (values === null) { + const values = await hgetall(this.client, `${this.prefix('hash')}:${id}`); + if (values === null || Object.keys(values).length === 0) { throw new Error('not found'); } Object.keys(values).forEach((key) => { @@ -1536,9 +1536,9 @@ abstract class NohmModel { if (this.isMultiClient(givenClient)) { multi = givenClient; } else if (givenClient) { - multi = givenClient.MULTI(); + multi = givenClient.multi(); } else { - multi = this.client.MULTI(); + multi = this.client.multi(); } // remove outstanding relation changes @@ -1547,7 +1547,7 @@ abstract class NohmModel { this.modelName }:${this.id}`; - const keys = await SMEMBERS(this.client, relationKeysKey); + const keys = await smembers(this.client, relationKeysKey); debug(`Remvoing links for '%s.%s': %o.`, this.modelName, this.id, keys); @@ -1579,12 +1579,12 @@ abstract class NohmModel { await Promise.all(otherRelationIdsPromises); // add multi'ed delete commands for other keys - others.forEach((item) => multi.DEL(item.ownIdsKey)); + others.forEach((item) => multi.del(item.ownIdsKey)); multi.del(relationKeysKey); // if we didn't get a multi client from the callee we have to exec() ourselves if (!this.isMultiClient(givenClient)) { - await EXEC(multi); + await exec(multi); } } @@ -1598,9 +1598,9 @@ abstract class NohmModel { multi: redis.Multi, item: IUnlinkKeyMapItem, ): Promise { - const ids = await SMEMBERS(this.client, item.ownIdsKey); + const ids = await smembers(this.client, item.ownIdsKey); ids.forEach((id) => { - multi.SREM(`${item.otherIdsKey}${id}`, this.stringId()); + multi.srem(`${item.otherIdsKey}${id}`, this.stringId()); }); } @@ -1621,7 +1621,7 @@ abstract class NohmModel { 'Calling belongsTo() even though either the object itself or the relation does not have an id.', ); } - return !!(await SISMEMBER( + return !!(await sismember( this.client, this.getRelationKey(obj.modelName, relationName), obj.id, @@ -1647,7 +1647,7 @@ abstract class NohmModel { ); } const relationKey = this.getRelationKey(otherModelName, relationName); - const ids = await SMEMBERS(this.client, relationKey); + const ids = await smembers(this.client, relationKey); if (!Array.isArray(ids)) { return []; } else { @@ -1676,7 +1676,7 @@ abstract class NohmModel { ); } const relationKey = this.getRelationKey(otherModelName, relationName); - return SCARD(this.client, relationKey); + return scard(this.client, relationKey); } /** @@ -1721,7 +1721,7 @@ abstract class NohmModel { if (onlySets.length === 0 && onlyZSets.length === 0) { // no valid searches - return all ids - return SMEMBERS(this.client, `${this.prefix('idsets')}`); + return smembers(this.client, `${this.prefix('idsets')}`); } debug( `Finding '%s's with these searches (sets, zsets):\n%o,\n%o.`, @@ -1808,7 +1808,7 @@ abstract class NohmModel { options: IStructuredSearch, ): Promise> { const key = `${this.prefix('unique')}:${options.key}:${options.value}`; - const id = await GET(this.client, key); + const id = await get(this.client, key); if (id) { return [id]; } else { @@ -1826,7 +1826,7 @@ abstract class NohmModel { // shortcut return []; } - return SINTER(this.client, keys); + return sinter(this.client, keys); } private async zSetSearch( @@ -1843,7 +1843,7 @@ abstract class NohmModel { ): Promise> { return new Promise((resolve, reject) => { const key = `${this.prefix('scoredindex')}:${search.key}`; - let command: 'ZRANGEBYSCORE' | 'ZREVRANGEBYSCORE' = 'ZRANGEBYSCORE'; + let command: 'zrangebyscore' | 'zrevrangebyscore' = 'zrangebyscore'; const options: ISearchOption = { endpoints: '[]', limit: -1, @@ -1857,7 +1857,7 @@ abstract class NohmModel { (options.max === '-inf' && options.min !== '-inf') || parseFloat('' + options.min) > parseFloat('' + options.max) ) { - command = 'ZREVRANGEBYSCORE'; + command = 'zrevrangebyscore'; } if (options.endpoints === ')') { @@ -1953,11 +1953,11 @@ abstract class NohmModel { } let idsetKey = this.prefix('idsets'); let zsetKey = `${this.prefix('scoredindex')}:${options.field}`; - const client: redis.Multi = this.client.MULTI(); + const client: redis.Multi = this.client.multi(); let tmpKey: string = ''; debug( - `Sorting '%s's with these options (alpha, direction, scored, start, stop, ids):`, + `Sorting '%s's with these options (this.id, alpha, direction, scored, start, stop, ids):`, this.modelName, this.id, alpha, @@ -1991,16 +1991,16 @@ abstract class NohmModel { +new Date() + Math.ceil(Math.random() * 1000); ids.unshift(tmpKey); - client.SADD.apply(client, ids as Array); // typecast because rediss doesn't care about numbers/string - client.SINTERSTORE([tmpKey, tmpKey, idsetKey]); + client.sadd.apply(client, ids as Array); // typecast because rediss doesn't care about numbers/string + client.sinterstore([tmpKey, tmpKey, idsetKey]); idsetKey = tmpKey; } } if (scored) { - const method = direction && direction === 'DESC' ? 'ZREVRANGE' : 'ZRANGE'; + const method = direction && direction === 'DESC' ? 'zrevrange' : 'zrange'; client[method](zsetKey, start, stop); } else { - client.sort( + const args = [ idsetKey, 'BY', `${this.prefix('hash')}:*->${options.field}`, @@ -2008,13 +2008,19 @@ abstract class NohmModel { String(start), String(stop), direction, - alpha as any, // any casting because passing in an empty string actually results in errors in some cases - ); + ]; + if (alpha) { + // due to the way ioredis handles input parameters, we have to do this weird apply and append thing. + // when just passing in an undefined value it throws syntax errors because it attempts to add that as an actual + // parameter + args.push(alpha); + } + client.sort.apply(client, args); } if (ids) { client.del(tmpKey); } - const replies = await EXEC(client); + const replies = await exec(client); let reply: Array; if (ids) { // 2 redis commands to create the temp keys, then the query diff --git a/ts/typed-redis-helper.ts b/ts/typed-redis-helper.ts index 2d6c3fd..f5eacb3 100644 --- a/ts/typed-redis-helper.ts +++ b/ts/typed-redis-helper.ts @@ -1,14 +1,15 @@ import { Multi, RedisClient } from 'redis'; +import * as IORedis from 'ioredis'; export const errorMessage = 'Supplied redis client does not have the correct methods.'; -export function GET(client: RedisClient | Multi, key: string): Promise { +export function get(client: RedisClient | Multi, key: string): Promise { return new Promise((resolve, reject) => { - if (!client.GET) { + if (!client.get) { return reject(new Error(errorMessage)); } - client.GET(key, (err, value) => { + client.get(key, (err, value) => { if (err) { reject(err); } else { @@ -18,15 +19,15 @@ export function GET(client: RedisClient | Multi, key: string): Promise { }); } -export function EXISTS( +export function exists( client: RedisClient | Multi, key: string, ): Promise { return new Promise((resolve, reject) => { - if (!client.EXISTS) { + if (!client.exists) { return reject(new Error(errorMessage)); } - client.EXISTS(key, (err, reply) => { + client.exists(key, (err, reply) => { if (err) { reject(err); } else { @@ -36,12 +37,12 @@ export function EXISTS( }); } -export function DEL(client: RedisClient | Multi, key: string): Promise { +export function del(client: RedisClient | Multi, key: string): Promise { return new Promise((resolve, reject) => { - if (!client.DEL) { + if (!client.del) { return reject(new Error(errorMessage)); } - client.DEL(key, (err) => { + client.del(key, (err) => { if (err) { reject(err); } else { @@ -51,16 +52,16 @@ export function DEL(client: RedisClient | Multi, key: string): Promise { }); } -export function SET( +export function set( client: RedisClient | Multi, key: string, value: string, ): Promise { return new Promise((resolve, reject) => { - if (!client.SET) { + if (!client.set) { return reject(new Error(errorMessage)); } - client.SET(key, value, (err) => { + client.set(key, value, (err) => { if (err) { reject(err); } else { @@ -70,16 +71,16 @@ export function SET( }); } -export function MSET( +export function mset( client: RedisClient | Multi, keyValueArrayOrString: string | Array, ...keyValuePairs: Array ): Promise { return new Promise((resolve, reject) => { - if (!client.MSET) { + if (!client.mset) { return reject(new Error(errorMessage)); } - client.MSET.apply(client, [ + client.mset.apply(client, [ keyValueArrayOrString, ...keyValuePairs, (err: Error | null) => { @@ -93,16 +94,16 @@ export function MSET( }); } -export function SETNX( +export function setnx( client: RedisClient | Multi, key: string, value: string, ): Promise { return new Promise((resolve, reject) => { - if (!client.SETNX) { + if (!client.setnx) { return reject(new Error(errorMessage)); } - client.SETNX(key, value, (err, reply) => { + client.setnx(key, value, (err, reply) => { if (err) { reject(err); } else { @@ -112,15 +113,15 @@ export function SETNX( }); } -export function SMEMBERS( +export function smembers( client: RedisClient | Multi, key: string, ): Promise> { return new Promise>((resolve, reject) => { - if (!client.SMEMBERS) { + if (!client.smembers) { return reject(new Error(errorMessage)); } - client.SMEMBERS(key, (err, values) => { + client.smembers(key, (err, values) => { if (err) { reject(err); } else { @@ -130,15 +131,15 @@ export function SMEMBERS( }); } -export function SCARD( +export function scard( client: RedisClient | Multi, key: string, ): Promise { return new Promise((resolve, reject) => { - if (!client.SCARD) { + if (!client.scard) { return reject(new Error(errorMessage)); } - client.SCARD(key, (err, value) => { + client.scard(key, (err, value) => { if (err) { reject(err); } else { @@ -148,16 +149,16 @@ export function SCARD( }); } -export function SISMEMBER( +export function sismember( client: RedisClient | Multi, key: string, value: string, ): Promise { return new Promise((resolve, reject) => { - if (!client.SISMEMBER) { + if (!client.sismember) { return reject(new Error(errorMessage)); } - client.SISMEMBER(key, value, (err, numFound) => { + client.sismember(key, value, (err, numFound) => { if (err) { reject(err); } else { @@ -167,16 +168,16 @@ export function SISMEMBER( }); } -export function SADD( +export function sadd( client: RedisClient | Multi, key: string, value: string, ): Promise { return new Promise((resolve, reject) => { - if (!client.SADD) { + if (!client.sadd) { return reject(new Error(errorMessage)); } - client.SADD(key, value, (err, numInserted) => { + client.sadd(key, value, (err, numInserted) => { if (err) { reject(err); } else { @@ -186,16 +187,16 @@ export function SADD( }); } -export function SINTER( +export function sinter( client: RedisClient | Multi, keyArrayOrString: string | Array, ...keys: Array ): Promise> { return new Promise>((resolve, reject) => { - if (!client.SINTER) { + if (!client.sinter) { return reject(new Error(errorMessage)); } - client.SINTER.apply(client, [ + client.sinter.apply(client, [ keyArrayOrString, ...keys, (err: Error | null, values: Array) => { @@ -209,15 +210,15 @@ export function SINTER( }); } -export function HGETALL( +export function hgetall( client: RedisClient | Multi, key: string, ): Promise<{ [key: string]: string }> { return new Promise<{ [key: string]: string }>((resolve, reject) => { - if (!client.HGETALL) { + if (!client.hgetall) { return reject(new Error(errorMessage)); } - client.HGETALL(key, (err, values) => { + client.hgetall(key, (err, values) => { if (err) { reject(err); } else { @@ -227,31 +228,48 @@ export function HGETALL( }); } -export function EXEC(client: Multi): Promise> { +export function exec(client: Multi): Promise> { return new Promise>((resolve, reject) => { - if (!client.EXEC) { + if (!client.exec) { return reject(new Error(errorMessage)); } - client.EXEC((err, results) => { + client.exec((err, results) => { if (err) { return reject(err); } else { + // detect if it's ioredis, which has a different return structure. + // better methods for doing this would be very welcome! + if ( + Array.isArray(results[0]) && + (results[0][0] === null || + // once ioredis has proper typings, this any casting can be changed + results[0][0] instanceof (IORedis as any).ReplyError) + ) { + // transform ioredis format to node_redis format + results = results.map((result: Array) => { + const error = result[0]; + if (error instanceof (IORedis as any).ReplyError) { + return error.message; + } + return result[1]; + }); + } resolve(results); } }); }); } -export function PSUBSCRIBE( +export function psubscribe( client: RedisClient, patternOrPatternArray: string | Array, ...patterns: Array ): Promise { return new Promise((resolve, reject) => { - if (!client.PSUBSCRIBE) { + if (!client.psubscribe) { return reject(new Error(errorMessage)); } - client.PSUBSCRIBE.apply(client, [ + client.psubscribe.apply(client, [ patternOrPatternArray, ...patterns, (err: Error | null) => { @@ -265,16 +283,16 @@ export function PSUBSCRIBE( }); } -export function PUNSUBSCRIBE( +export function punsubscribe( client: RedisClient, patternOrPatternArray: string | Array, ...patterns: Array ): Promise { return new Promise((resolve, reject) => { - if (!client.PUNSUBSCRIBE) { + if (!client.punsubscribe) { return reject(new Error(errorMessage)); } - client.PUNSUBSCRIBE.apply(client, [ + client.punsubscribe.apply(client, [ patternOrPatternArray, ...patterns, (err: Error | null) => {