resend 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +6 -21
- package/.eslintrc +0 -6
- package/.npmignore +0 -7
- package/Dockerfile +0 -5
- package/README.md +0 -74
- package/bin/redeploy.sh +0 -13
- package/development/commit.sh +0 -9
- package/development/run.sh +0 -24
- package/docs/readme/main.png +0 -0
- package/docs/readme/main2.png +0 -0
- package/lib/index.js +0 -2
- package/lib/main.js +0 -132
- package/lib/spec.js +0 -50
- package/test/curl.sh +0 -9
package/package.json
CHANGED
|
@@ -1,26 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "resend",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "
|
|
5
|
-
"main": "
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "",
|
|
5
|
+
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"
|
|
8
|
-
"start": "node --harmony lib/index.js"
|
|
7
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
9
8
|
},
|
|
10
|
-
"author": "
|
|
11
|
-
"license": "ISC"
|
|
12
|
-
"dependencies": {
|
|
13
|
-
"redis-koa-app-rpf": "^0.6.5"
|
|
14
|
-
},
|
|
15
|
-
"devDependencies": {
|
|
16
|
-
"eslint": "^3.12.2"
|
|
17
|
-
},
|
|
18
|
-
"repository": {
|
|
19
|
-
"type": "git",
|
|
20
|
-
"url": "git+https://github.com/evanx/resend.git"
|
|
21
|
-
},
|
|
22
|
-
"bugs": {
|
|
23
|
-
"url": "https://github.com/evanx/resend/issues"
|
|
24
|
-
},
|
|
25
|
-
"homepage": "https://github.com/evanx/resend#readme"
|
|
9
|
+
"author": "",
|
|
10
|
+
"license": "ISC"
|
|
26
11
|
}
|
package/.eslintrc
DELETED
package/.npmignore
DELETED
package/Dockerfile
DELETED
package/README.md
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
# resend
|
|
2
|
-
|
|
3
|
-
Containerized utility to scan and print llen of Redis keys.
|
|
4
|
-
|
|
5
|
-
<img src="https://raw.githubusercontent.com/evanx/resend/master/docs/readme/main.png"/>
|
|
6
|
-
|
|
7
|
-
## Use case
|
|
8
|
-
|
|
9
|
-
We wish to expire a set of keys in Redis matching some pattern.
|
|
10
|
-
|
|
11
|
-
```
|
|
12
|
-
```
|
|
13
|
-
|
|
14
|
-
## Config
|
|
15
|
-
|
|
16
|
-
See `lib/config.js`
|
|
17
|
-
```javascript
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
## Docker
|
|
21
|
-
|
|
22
|
-
You can build as follows:
|
|
23
|
-
```shell
|
|
24
|
-
docker build -t resend https://github.com/evanx/resend.git
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
See `bin/test.sh` https://github.com/evanx/resend/blob/master/bin/test.sh
|
|
28
|
-
|
|
29
|
-
Builds:
|
|
30
|
-
- isolated network `resend-network`
|
|
31
|
-
- isolated Redis instance named `resend-redis`
|
|
32
|
-
- this utility `evanx/resend`
|
|
33
|
-
|
|
34
|
-
We populate our test keys:
|
|
35
|
-
```
|
|
36
|
-
populate() {
|
|
37
|
-
}
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
We build a container image for this service:
|
|
41
|
-
```
|
|
42
|
-
docker build -t resend https://github.com/evanx/resend.git
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
We interactively run the service on our test Redis container:
|
|
46
|
-
```
|
|
47
|
-
docker run --name resend-instance --rm -i \
|
|
48
|
-
--network=resend-network \
|
|
49
|
-
-e host=$redisHost \
|
|
50
|
-
-e pattern='*' \
|
|
51
|
-
resend
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
## Implementation
|
|
55
|
-
|
|
56
|
-
See `lib/main.js`
|
|
57
|
-
|
|
58
|
-
```javascript
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
### Appication archetype
|
|
62
|
-
|
|
63
|
-
Incidently `lib/index.js` uses the `redis-koa-app-rpf` application archetype.
|
|
64
|
-
```
|
|
65
|
-
require('redis-koa-app-rpf')(require('./spec'), require('./main'));
|
|
66
|
-
```
|
|
67
|
-
where we extract the `config` from `process.env` according to the `spec` and invoke our `main` function.
|
|
68
|
-
|
|
69
|
-
See https://github.com/evanx/redis-koa-app-rpf.
|
|
70
|
-
|
|
71
|
-
This provides lifecycle boilerplate to reuse across similar applications.
|
|
72
|
-
|
|
73
|
-
<hr>
|
|
74
|
-
https://twitter.com/@evanxsummers
|
package/bin/redeploy.sh
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
echo "NODE_ENV $NODE_ENV"
|
|
3
|
-
docker build -t geo-cache https://github.com/evanx/geo-cache.git
|
|
4
|
-
docker rm -f `docker ps -q -f name=geo-cache`
|
|
5
|
-
container=`docker run --name geo-cache -d \
|
|
6
|
-
--network=host --restart unless-stopped \
|
|
7
|
-
-e httpPort=8851 \
|
|
8
|
-
-e NODE_ENV=$NODE_ENV \
|
|
9
|
-
geo-cache`
|
|
10
|
-
sleep 1
|
|
11
|
-
echo "docker logs $container"
|
|
12
|
-
docker logs $container
|
|
13
|
-
|
package/development/commit.sh
DELETED
package/development/run.sh
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
set -u -x
|
|
3
|
-
|
|
4
|
-
mkdir -p tmp
|
|
5
|
-
|
|
6
|
-
redis-cli keys cache-geo-cache:* | xargs -n 1 redis-cli del
|
|
7
|
-
|
|
8
|
-
echo '
|
|
9
|
-
redis-cli keys cache-geo-cache:* | head
|
|
10
|
-
redis-cli keys cache-geo-cache:* | wc -l
|
|
11
|
-
' | sed '/^$/d' | tee tmp/keys.sh | dash -x
|
|
12
|
-
cat tmp/keys.sh
|
|
13
|
-
ls -l tmp/keys.sh
|
|
14
|
-
|
|
15
|
-
(
|
|
16
|
-
echo 'client sleep'
|
|
17
|
-
sleep 1
|
|
18
|
-
echo 'client curl'
|
|
19
|
-
curl 'http://localhost:8851/maps/api/geocode/json' -G --data-urlencode 'address=20 Falstaff Close, Eynsham OX29 4QA'
|
|
20
|
-
redis-cli keys cache-geo-cache:*
|
|
21
|
-
) &
|
|
22
|
-
|
|
23
|
-
echo 'start server'
|
|
24
|
-
apiKey=$MAPS_API_KEY node --harmony lib/index.js
|
package/docs/readme/main.png
DELETED
|
Binary file
|
package/docs/readme/main2.png
DELETED
|
Binary file
|
package/lib/index.js
DELETED
package/lib/main.js
DELETED
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
const assert = require('assert');
|
|
2
|
-
const crypto = require('crypto');
|
|
3
|
-
const fetch = require('node-fetch');
|
|
4
|
-
const h = require('render-html-rpf');
|
|
5
|
-
const lodash = require('lodash');
|
|
6
|
-
const multiExecAsync = require('multi-exec-async');
|
|
7
|
-
|
|
8
|
-
const reduceAllProperties = (object, fn) => Object.keys(object).reduce((result, key) => {
|
|
9
|
-
result[key] = fn(object[key]);
|
|
10
|
-
return result;
|
|
11
|
-
}, {});
|
|
12
|
-
|
|
13
|
-
module.exports = async appx => {
|
|
14
|
-
const {config, logger, client, api} = appx;
|
|
15
|
-
logger.info({config}, process.env.NODE_ENV);
|
|
16
|
-
api.get('/stats', async ctx => {
|
|
17
|
-
ctx.redirect('/metrics');
|
|
18
|
-
});
|
|
19
|
-
api.get('/metrics', async ctx => {
|
|
20
|
-
const [getCountRes, setCountRes, migrateCountRes] = await multiExecAsync(client, multi => {
|
|
21
|
-
multi.hgetall([config.redisNamespace, 'metric:get:path:count:h'].join(':'));
|
|
22
|
-
multi.hgetall([config.redisNamespace, 'metric:set:path:count:h'].join(':'));
|
|
23
|
-
multi.hgetall([config.redisNamespace, 'metric:migrate:path:count:h'].join(':'));
|
|
24
|
-
});
|
|
25
|
-
const getCount = reduceAllProperties(getCountRes || {}, value => parseInt(value));
|
|
26
|
-
const setCount = reduceAllProperties(setCountRes || {}, value => parseInt(value));
|
|
27
|
-
const migrateCount = reduceAllProperties(migrateCountRes || {}, value => parseInt(value));
|
|
28
|
-
const metrics = {getCount, setCount, migrateCount};
|
|
29
|
-
if (/(Mobile)/.test(ctx.get('user-agent'))) {
|
|
30
|
-
ctx.body = h.page({
|
|
31
|
-
title: 'gcache',
|
|
32
|
-
heading: 'Metrics',
|
|
33
|
-
content: [{
|
|
34
|
-
name: 'pre',
|
|
35
|
-
content: JSON.stringify(metrics, null, 2)}
|
|
36
|
-
],
|
|
37
|
-
footerLink: 'https://github.com/evanx/geo-cache'
|
|
38
|
-
});
|
|
39
|
-
} else {
|
|
40
|
-
ctx.body = metrics;
|
|
41
|
-
}
|
|
42
|
-
});
|
|
43
|
-
api.get('/maps/api/*', async ctx => {
|
|
44
|
-
const path = ctx.params[0];
|
|
45
|
-
const url = 'https://maps.googleapis.com/maps/api/' + path;
|
|
46
|
-
const query = Object.keys(ctx.query)
|
|
47
|
-
.filter(key => key !== 'key')
|
|
48
|
-
.reduce((query, key) => {
|
|
49
|
-
query[key] = ctx.query[key];
|
|
50
|
-
return query;
|
|
51
|
-
}, {});
|
|
52
|
-
assert(!query.key);
|
|
53
|
-
const queryString = Object.keys(query).slice(0).sort().map(
|
|
54
|
-
key => [key, encodeURIComponent(query[key])].join('=')
|
|
55
|
-
).join('&');
|
|
56
|
-
const urlString = [url, queryString].join('?');
|
|
57
|
-
const authQuery = Object.assign({}, {key: config.apiKey}, ctx.query);
|
|
58
|
-
if (!authQuery.key) {
|
|
59
|
-
ctx.statusCode = 401;
|
|
60
|
-
const statusText = 'Unauthorized';
|
|
61
|
-
ctx.body = statusText + '\n';
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
logger.debug({url, query, urlString});
|
|
65
|
-
const sha = crypto.createHash('sha1').update(urlString).digest('hex');
|
|
66
|
-
const cacheKey = [config.redisNamespace, sha, 'j'].join(':');
|
|
67
|
-
if (true) {
|
|
68
|
-
const migrateSha = crypto.createHash('sha1').update(
|
|
69
|
-
[url, JSON.stringify(query)].join('#')
|
|
70
|
-
).digest('hex');
|
|
71
|
-
const migrateKey = ['cache-geo-cache', migrateSha, 'json'].join(':');
|
|
72
|
-
const [migrateContent] = await multiExecAsync(client, multi => {
|
|
73
|
-
multi.get(migrateKey);
|
|
74
|
-
});
|
|
75
|
-
if (migrateContent) {
|
|
76
|
-
await multiExecAsync(client, multi => {
|
|
77
|
-
multi.set(cacheKey, JSON.stringify(JSON.parse(migrateContent)));
|
|
78
|
-
multi.del(migrateKey);
|
|
79
|
-
multi.hincrby([config.redisNamespace, 'metric:migrate:path:count:h'].join(':'), path, 1);
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
let [cachedContent] = await multiExecAsync(client, multi => {
|
|
84
|
-
multi.get(cacheKey);
|
|
85
|
-
multi.expire(cacheKey, config.expireSeconds);
|
|
86
|
-
multi.hincrby([config.redisNamespace, 'metric:get:path:count:h'].join(':'), path, 1);
|
|
87
|
-
});
|
|
88
|
-
if (cachedContent) {
|
|
89
|
-
logger.debug('hit', {url, sha, cacheKey});
|
|
90
|
-
const parsedContent = JSON.parse(cachedContent);
|
|
91
|
-
const formattedContent = JSON.stringify(parsedContent);
|
|
92
|
-
if (cachedContent !== formattedContent) {
|
|
93
|
-
logger.debug('reformat', cacheKey, cachedContent);
|
|
94
|
-
await multiExecAsync(client, multi => {
|
|
95
|
-
multi.set(cacheKey, formattedContent);
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
if (lodash.includes(['OK', 'ZERO_RESULTS'], parsedContent.status)) {
|
|
99
|
-
logger.warn('hit', {url, sha, cacheKey, parsedContent});
|
|
100
|
-
ctx.set('Content-Type', 'application/json');
|
|
101
|
-
ctx.body = JSON.stringify(parsedContent, null, 2) + '\n';
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
const urlQuery = url + '?' + Object.keys(authQuery)
|
|
106
|
-
.map(key => [key, encodeURIComponent(authQuery[key])].join('='))
|
|
107
|
-
.join('&');
|
|
108
|
-
const res = await fetch(urlQuery);
|
|
109
|
-
if (res.status !== 200) {
|
|
110
|
-
logger.debug('statusCode', url, res.status, res.statusText, query);
|
|
111
|
-
ctx.statusCode = res.status;
|
|
112
|
-
ctx.body = res.statusText + '\n';
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
const fetchedContent = await res.json();
|
|
116
|
-
const formattedContent = JSON.stringify(fetchedContent, null, 2) + '\n';
|
|
117
|
-
ctx.set('Content-Type', 'application/json');
|
|
118
|
-
ctx.body = formattedContent;
|
|
119
|
-
if (!lodash.includes(['OK', 'ZERO_RESULTS'], fetchedContent.status)) {
|
|
120
|
-
logger.debug('status', fetchedContent.status, url);
|
|
121
|
-
} else {
|
|
122
|
-
const expireSeconds = lodash.includes(['ZERO_RESULTS'], fetchedContent.status)?
|
|
123
|
-
config.shortExpireSeconds:
|
|
124
|
-
config.expireSeconds;
|
|
125
|
-
logger.debug('expireSeconds', expireSeconds, fetchedContent.status, url);
|
|
126
|
-
await multiExecAsync(client, multi => {
|
|
127
|
-
multi.setex(cacheKey, expireSeconds, JSON.stringify(fetchedContent));
|
|
128
|
-
multi.hincrby([config.redisNamespace, 'metric:set:path:count:h'].join(':'), path, 1);
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
});
|
|
132
|
-
}
|
package/lib/spec.js
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
module.exports = {
|
|
2
|
-
description: 'Redis-based caching proxy for Google Maps API queries.',
|
|
3
|
-
required: {
|
|
4
|
-
redisHost: {
|
|
5
|
-
description: 'the Redis host',
|
|
6
|
-
default: 'localhost'
|
|
7
|
-
},
|
|
8
|
-
redisPort: {
|
|
9
|
-
description: 'the Redis port',
|
|
10
|
-
default: 6379
|
|
11
|
-
},
|
|
12
|
-
redisPassword: {
|
|
13
|
-
description: 'the Redis password',
|
|
14
|
-
required: false
|
|
15
|
-
},
|
|
16
|
-
redisNamespace: {
|
|
17
|
-
description: 'the Redis namespace',
|
|
18
|
-
default: 'cache-geo'
|
|
19
|
-
},
|
|
20
|
-
expireSeconds: {
|
|
21
|
-
description: 'the TTL for the cached content',
|
|
22
|
-
default: 21*24*3600
|
|
23
|
-
},
|
|
24
|
-
shortExpireSeconds: {
|
|
25
|
-
description: 'the TTL for the cached content',
|
|
26
|
-
default: 3*24*3600
|
|
27
|
-
},
|
|
28
|
-
httpPort: {
|
|
29
|
-
description: 'the HTTP port',
|
|
30
|
-
default: 8851
|
|
31
|
-
},
|
|
32
|
-
apiKey: {
|
|
33
|
-
description: 'our Google Maps API key',
|
|
34
|
-
required: false
|
|
35
|
-
},
|
|
36
|
-
loggerLevel: {
|
|
37
|
-
description: 'the logging level',
|
|
38
|
-
default: 'info',
|
|
39
|
-
example: 'debug'
|
|
40
|
-
}
|
|
41
|
-
},
|
|
42
|
-
test: {
|
|
43
|
-
loggerLevel: 'debug',
|
|
44
|
-
debug: true
|
|
45
|
-
},
|
|
46
|
-
development: {
|
|
47
|
-
loggerLevel: 'debug',
|
|
48
|
-
debug: true
|
|
49
|
-
}
|
|
50
|
-
}
|
package/test/curl.sh
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
set -u -x
|
|
3
|
-
|
|
4
|
-
curl 'http://localhost:8851/maps/api/geocode/json' -G --data-urlencode 'address=20 Falstaff Close, Eynsham OX29 4QA'
|
|
5
|
-
curl 'http://localhost:8851/maps/api/geocode/json' -G --data-urlencode 'address=14 Grays Inn Road, Chancery Lane, London'
|
|
6
|
-
sleep 1
|
|
7
|
-
redis-cli keys cache-geo-cache:*
|
|
8
|
-
redis-cli hkeys cache-geo-cache:url:h
|
|
9
|
-
redis-cli hgetall cache-geo-cache:url:h
|