Skip to content

Commit 22ace3e

Browse files
committed
chore: update tests & deps
1 parent 9a28eb8 commit 22ace3e

16 files changed

+7058
-1106
lines changed

.babelrc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"plugins": [
3-
"transform-strict-mode",
4-
"transform-object-rest-spread"
3+
"@babel/plugin-transform-strict-mode",
4+
"@babel/plugin-proposal-object-rest-spread"
55
]
66
}

.commitlintrc.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
module.exports = {
2+
rules: {
3+
'body-leading-blank': [1, 'always'],
4+
'footer-leading-blank': [1, 'always'],
5+
'header-max-length': [2, 'always', 72],
6+
'scope-case': [2, 'always', 'lower-case'],
7+
'subject-case': [
8+
2,
9+
'never',
10+
['sentence-case', 'start-case', 'pascal-case', 'upper-case']
11+
],
12+
'subject-empty': [2, 'never'],
13+
'subject-full-stop': [2, 'never', '.'],
14+
'type-case': [2, 'always', 'lower-case'],
15+
'type-empty': [2, 'never'],
16+
'type-enum': [2, 'always', [
17+
'build',
18+
'ci',
19+
'docs',
20+
'feat',
21+
'fix',
22+
'perf',
23+
'refactor',
24+
'revert',
25+
'style',
26+
'test',
27+
'major',
28+
'minor',
29+
'patch',
30+
'chore'
31+
]
32+
]
33+
}
34+
};

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,5 @@ lib
2727
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
2828
node_modules
2929
test/dump
30+
.nyc_output
31+
coverage

.releaserc.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"branch": "master",
3+
"analyzeCommits": {
4+
"preset": "angular",
5+
"releaseRules": [
6+
{ "type": "docs", "release": "patch" },
7+
{ "type": "refactor", "release": "patch" },
8+
{ "type": "style", "release": "patch" },
9+
{ "type": "minor", "release": "minor" },
10+
{ "type": "patch", "release": "patch" },
11+
{ "type": "major", "release": "major" },
12+
{ "type": "breaking", "release": "major" }
13+
]
14+
}
15+
}

package.json

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@
77
"node": ">= 7.6.0"
88
},
99
"scripts": {
10-
"test": "./test/docker.sh",
10+
"test": "mdep test run",
1111
"compile": "make all",
12-
"prepublish": "npm run compile",
13-
"postversion": "git push && npm publish && git push --tags"
12+
"prepublishOnly": "yarn compile",
13+
"semantic-release": "semantic-release"
1414
},
1515
"repository": {
1616
"type": "git",
@@ -33,17 +33,28 @@
3333
"ioredis": "2.x.x || 3.x.x"
3434
},
3535
"devDependencies": {
36-
"babel-cli": "^6.24.1",
37-
"babel-plugin-transform-object-rest-spread": "^6.23.0",
38-
"babel-plugin-transform-strict-mode": "^6.24.1",
39-
"babel-register": "^6.24.1",
40-
"chai": "^3.4.1",
36+
"@babel/cli": "^7.0.0",
37+
"@babel/core": "^7.0.0",
38+
"@babel/plugin-proposal-object-rest-spread": "^7.0.0",
39+
"@babel/plugin-transform-strict-mode": "^7.0.0",
40+
"@babel/register": "^7.0.0",
41+
"@makeomatic/deploy": "^8.4.4",
42+
"babel-plugin-istanbul": "^5.1.4",
43+
"chai": "^4.2.0",
44+
"cross-env": "^5.2.0",
4145
"faker": "^4.1.0",
42-
"ioredis": "^2.4.0",
43-
"lodash": "^4.16.3",
44-
"mocha": "^3.3.0"
46+
"ioredis": "^4.9.5",
47+
"lodash": "^4.17.11",
48+
"mocha": "^6.1.4",
49+
"nyc": "^14.1.1"
4550
},
4651
"dependencies": {
47-
"cluster-key-slot": "^1.0.6"
52+
"cluster-key-slot": "^1.0.12"
53+
},
54+
"husky": {
55+
"hooks": {
56+
"commit-msg": "commitlint -e $HUSKY_GIT_PARAMS",
57+
"prepare-commit-msg": "./node_modules/@makeomatic/deploy/git-hooks/prepare-commit-msg $HUSKY_GIT_PARAMS"
58+
}
4859
}
4960
}

src/constants.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
exports.FSORT_TEMP_KEYSET = 'fsort_temp_keys';

src/filtered-list-bust.lua

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
-- must be a set of ids
2+
local idSet = KEYS[1];
3+
-- current time
4+
local curTime = ARGV[1];
5+
-- caching time
6+
local expiration = tonumber(ARGV[2] or 30000);
7+
8+
--
9+
local tempKeysSet = getIndexTempKeys(idSet);
10+
local keys = redis.call("ZRANGEBYSCORE", tempKeysSet, curTime - expiration, '+inf');
11+
12+
if #keys > 0 then
13+
redis.call("DEL", unpack(keys));
14+
redis.call("DEL", tempKeysSet);
15+
end

src/fsort.js

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
const { FSORT_TEMP_KEYSET } = require('./constants');
2+
3+
function getIndexTempKeys(index) {
4+
return `${index}::${exports.FSORT_TEMP_KEYSET}`;
5+
}
6+
7+
async function fsort(...args) {
8+
const {
9+
redis,
10+
dlock,
11+
} = this;
12+
13+
const [
14+
idSet,
15+
metadataKey,
16+
hashKey,
17+
order,
18+
filter,
19+
curTime,
20+
offset = 0,
21+
limit = 10,
22+
expiration = 30000,
23+
returnKeyOnly = false,
24+
] = args;
25+
26+
const tempKeysSet = getIndexTempKeys(idSet);
27+
const jsonFilter = JSON.parse(filter);
28+
29+
const [FFLKey, PSSKey] = prepareKeys({
30+
metadataKey,
31+
hashKey,
32+
jsonFilter,
33+
});
34+
}
35+
36+
function prepareKeys(opts) {
37+
const {
38+
metadataKey,
39+
hashKey,
40+
jsonFilter,
41+
}
42+
43+
const finalFilteredListKeys = [idSet, order];
44+
const preSortedSetKeys = [idSet, order];
45+
const totalFilters = Object.keys(jsonFilter).length;
46+
47+
if (metadataKey) {
48+
if (hashKey) {
49+
finalFilteredListKeys.push(metadataKey, hashKey);
50+
preSortedSetKeys.push(metadataKey, hashKey);
51+
}
52+
53+
if (totalFilters > 0) {
54+
finalFilteredListKeys.push(filter);
55+
}
56+
} else if (totalFilters >= 1 && typeof jsonFilter['#'] === 'string') {
57+
finalFilteredListKeys.push(`{"#":"${jsonFilter['#']}"}`);
58+
}
59+
60+
return [
61+
finalFilteredListKeys.join(':'),
62+
preSortedSetKeys.join(':'),
63+
];
64+
}
65+
66+
function FilteredSort(redis, dlock) {
67+
this.dlock = dlock;
68+
this.redis = redis;
69+
this.fsort = fsort.bind(this);
70+
}
71+
72+
module.exports = dlock => new FilteredSort(dlock);

src/groupped-list.lua

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
-- cached id list key
2+
local idListKey = KEYS[1];
3+
-- meta key
4+
local metadataKey = KEYS[2];
5+
-- stringified [key]: [aggregateMethod] pairs
6+
local aggregates = ARGV[1];
7+
8+
-- local cache
9+
local rcall = redis.call;
10+
local tinsert = table.insert;
11+
12+
local jsonAggregates = cjson.decode(aggregates);
13+
local aggregateKeys = {};
14+
local result = {};
15+
16+
local function try(what)
17+
local status, result = pcall(what[1]);
18+
if not status then
19+
return what[2](result);
20+
end
21+
22+
return result;
23+
end
24+
25+
local function catch(what)
26+
return what[1]
27+
end
28+
29+
local function anynumber(a)
30+
return try {
31+
function()
32+
local num = tonumber(a);
33+
return num ~= nil and num or tonumber(cjson.decode(a));
34+
end,
35+
36+
catch {
37+
function()
38+
return nil;
39+
end
40+
}
41+
}
42+
end
43+
44+
local function aggregateSum(value1, value2)
45+
return value1 + value2;
46+
end
47+
48+
local aggregateType = {
49+
sum = aggregateSum
50+
};
51+
52+
for key, method in pairs(jsonAggregates) do
53+
tinsert(aggregateKeys, key);
54+
result[key] = 0;
55+
56+
if type(aggregateType[method]) ~= "function" then
57+
return error("not supported op: " .. method);
58+
end
59+
end
60+
61+
local valuesToGroup = rcall("LRANGE", idListKey, 0, -1);
62+
63+
-- group
64+
for _, id in ipairs(valuesToGroup) do
65+
-- metadata is stored here
66+
local metaKey = metadataKey:gsub("*", id, 1);
67+
-- pull information about required aggregate keys
68+
-- only 1 operation is supported now - sum
69+
-- but we can calculate multiple values
70+
local values = rcall("HMGET", metaKey, unpack(aggregateKeys));
71+
72+
for i, aggregateKey in ipairs(aggregateKeys) do
73+
local aggregateMethod = aggregateType[jsonAggregates[aggregateKey]];
74+
local value = anynumber(values[i]) or 0;
75+
result[aggregateKey] = aggregateMethod(result[aggregateKey], value);
76+
end
77+
end
78+
79+
return cjson.encode(result);

src/index.js

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
const fs = require('fs');
2+
const path = require('path');
3+
4+
const camelCase = require('lodash/camelCase');
5+
const snakeCase = require('lodash/snakeCase');
6+
const { FSORT_TEMP_KEYSET } = require('./constants');
7+
8+
const lua = fs.readFileSync(path.join(__dirname, 'sorted-filtered-list.lua'));
9+
const fsortBust = fs.readFileSync(path.join(__dirname, 'filtered-list-bust.lua'));
10+
const aggregateScript = fs.readFileSync(path.join(__dirname, 'groupped-list.lua'));
11+
12+
// cached vars
13+
const regexp = /[\^\$\(\)\%\.\[\]\*\+\-\?]/g;
14+
const keys = Object.keys;
15+
const stringify = JSON.stringify;
16+
const BLACK_LIST_PROPS = ['eq', 'ne'];
17+
18+
const luaWrapper = (script) => `
19+
---
20+
21+
local function getIndexTempKeys(index)
22+
return index .. "::${FSORT_TEMP_KEYSET}";
23+
end
24+
25+
---
26+
27+
${script.toString('utf-8')}
28+
`;
29+
30+
/**
31+
* Attached .sortedFilteredList function to ioredis instance
32+
* @param {ioredis} redis
33+
*/
34+
const fsortScript = luaWrapper(lua);
35+
const fsortBustScript = luaWrapper(fsortBust);
36+
37+
exports.attach = function attachToRedis(redis, _name, useSnakeCase = false) {
38+
const name = _name || 'sortedFilteredList';
39+
const bustName = (useSnakeCase ? snakeCase : camelCase)(`${name}Bust`);
40+
const aggregateName = (useSnakeCase ? snakeCase : camelCase)(`${name}Aggregate`);
41+
42+
redis.defineCommand(name, { numberOfKeys: 2, lua: fsortScript });
43+
redis.defineCommand(bustName, { numberOfKeys: 1, lua: fsortBustScript });
44+
redis.defineCommand(aggregateName, { numberOfKeys: 2, lua: aggregateScript });
45+
};
46+
47+
exports.nonAtomic = (dlock) => require('./fsort')(dlock);
48+
49+
/**
50+
* Performs escape on a filter clause
51+
* ^ $ ( ) % . [ ] * + - ? are prefixed with %
52+
*
53+
* @param {String} filter
54+
* @return {String}
55+
*/
56+
function escape(filter) {
57+
return filter.replace(regexp, '%$&');
58+
};
59+
exports.escape = escape;
60+
61+
/**
62+
* Walks over object and escapes string, does not touch #multi.fields
63+
* @param {Object} obj
64+
* @return {Object}
65+
*/
66+
function iterateOverObject(obj) {
67+
const props = keys(obj);
68+
const output = {};
69+
props.forEach(function iterateOverProps(prop) {
70+
const val = obj[prop];
71+
const type = typeof val;
72+
if (type === 'string' && BLACK_LIST_PROPS.indexOf(prop) === -1) {
73+
output[prop] = escape(val);
74+
} else if (type === 'object') {
75+
if (prop !== '#multi') {
76+
output[prop] = iterateOverObject(val);
77+
} else {
78+
output[prop] = val;
79+
val.match = escape(val.match);
80+
}
81+
} else {
82+
output[prop] = val;
83+
}
84+
});
85+
86+
return output;
87+
}
88+
89+
/**
90+
* Goes over filter and escapes each string
91+
* @param {Object} obj
92+
* @return {String}
93+
*/
94+
exports.filter = function filter(obj) {
95+
return stringify(iterateOverObject(obj));
96+
};
97+
98+
/**
99+
* Exports raw script
100+
* @type {Buffer}
101+
*/
102+
exports.script = lua;

0 commit comments

Comments
 (0)