Skip to content

Commit

Permalink
Merge pull request #307 from microlinkhq/next
Browse files Browse the repository at this point in the history
refactor: ttl layer
  • Loading branch information
Kikobeats authored Jan 15, 2024
2 parents 46b9758 + 0f4de14 commit b4df49d
Show file tree
Hide file tree
Showing 12 changed files with 62 additions and 58 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
"@keyvhq/multi": "~2.1.0",
"@keyvhq/redis": "~2.1.0",
"@kikobeats/time-span": "~1.0.3",
"@lukeed/ms": "~2.0.2",
"@microlink/mql": "~0.12.2",
"@microlink/ping-url": "~1.4.11",
"@microlink/ua": "~1.2.0",
Expand All @@ -111,7 +112,6 @@
"is-email-like": "~1.0.0",
"is-url-http": "~2.3.7",
"lodash": "~4.17.21",
"ms": "~2.1.3",
"on-finished": "~2.4.1",
"p-any": "~3.0.0",
"p-cancelable": "2.1.1",
Expand Down
2 changes: 1 addition & 1 deletion public/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ So, no matter what type of query you use, **unavatar.io** has you covered. You c

### TTL

Type: `string`<br/>
Type: `number`|`string`<br/>
Default: `'24h'`<br/>
Range: from `'1h'` to `'28d'`

Expand Down
32 changes: 27 additions & 5 deletions src/authentication.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
'use strict'

const debug = require('debug-logfmt')('unavatar:authentication')
const { RateLimiterMemory } = require('rate-limiter-flexible')
const debug = require('debug-logfmt')('unavatar:rate')
const FrequencyCounter = require('frequency-counter')
const onFinished = require('on-finished')
const { format } = require('@lukeed/ms')

const START = Date.now()
const reqsMin = new FrequencyCounter(60)
let reqs = 0

const { API_KEY, RATE_LIMIT_WINDOW, RATE_LIMIT } = require('./constant')

Expand Down Expand Up @@ -36,20 +43,35 @@ const rateLimitError = (() => {
})()

module.exports = async (req, res, next) => {
++reqs && reqsMin.inc()
onFinished(res, () => --reqs)

if (req.headers['x-api-key'] === API_KEY) return next()

const { msBeforeNext, remainingPoints: remaining } = await rateLimiter
const { msBeforeNext, remainingPoints: quotaRemaining } = await rateLimiter
.consume(req.ipAddress)
.catch(error => error)

if (!res.writableEnded) {
res.setHeader('X-Rate-Limit-Limit', RATE_LIMIT)
res.setHeader('X-Rate-Limit-Remaining', remaining)
res.setHeader('X-Rate-Limit-Remaining', quotaRemaining)
res.setHeader('X-Rate-Limit-Reset', new Date(Date.now() + msBeforeNext))
debug(req.ipAddress, { total: RATE_LIMIT, remaining })

const uptime = format(Date.now() - START)
const perMinute = reqsMin.freq()
const perSecond = Number(perMinute / 60).toFixed(1)

debug(req.ipAddress, {
uptime,
reqs,
'req/m': perMinute,
'req/s': perSecond,
quotaTotal: RATE_LIMIT,
quotaRemaining
})
}

if (remaining) return next()
if (quotaRemaining) return next()
res.setHeader('Retry-After', msBeforeNext / 1000)
return next(rateLimitError)
}
3 changes: 1 addition & 2 deletions src/avatar/resolve.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ const isUrlHttp = require('is-url-http')
const pTimeout = require('p-timeout')
const pReflect = require('p-reflect')
const { omit } = require('lodash')
const ms = require('ms')

const isIterable = require('../util/is-iterable')

Expand Down Expand Up @@ -40,7 +39,7 @@ const optimizeUrl = async (url, query) => {
il: '',
n: -1,
w: AVATAR_SIZE,
ttl: ms(getTtl(ttl)),
ttl: getTtl(ttl),
...rest
}).toString()}`
}
Expand Down
10 changes: 7 additions & 3 deletions src/constant.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
'use strict'

const { parse } = require('@lukeed/ms')
const { existsSync } = require('fs')
const ms = require('ms')

const TMP_FOLDER = existsSync('/dev/shm') ? '/dev/shm' : '/tmp'

const {
ALLOWED_REQ_HEADERS = ['accept-encoding', 'accept', 'user-agent'],
AVATAR_SIZE = 400,
AVATAR_TIMEOUT = 25000,
CACHE_TTL = ms('1y'),
TTL_DEFAULT = parse('1y'),
TTL_MIN = parse('1h'),
TTL_MAX = parse('28d'),
NODE_ENV = 'development',
PORT = 3000,
RATE_LIMIT_WINDOW = 86400,
Expand All @@ -26,7 +28,9 @@ module.exports = {
ALLOWED_REQ_HEADERS,
AVATAR_SIZE,
AVATAR_TIMEOUT: Number(AVATAR_TIMEOUT),
CACHE_TTL,
TTL_DEFAULT,
TTL_MIN,
TTL_MAX,
NODE_ENV,
PORT,
TMP_FOLDER,
Expand Down
3 changes: 2 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const debug = require('debug-logfmt')('unavatar')
const serveStatic = require('serve-static')
const createRouter = require('router-http')
const onFinished = require('on-finished')
const { format } = require('@lukeed/ms')
const { forEach } = require('lodash')
const send = require('send-http')
const path = require('path')
Expand Down Expand Up @@ -69,7 +70,7 @@ router
debug(
`${req.ipAddress} ${new URL(req.url, API_URL).toString()} ${
res.statusCode
} ${Math.round(req.timestamp())}ms`
} ${format(Math.round(req.timestamp()))}`
)
})
next()
Expand Down
13 changes: 6 additions & 7 deletions src/send/cache.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,22 @@
'use strict'

const ms = require('ms')
const { parse } = require('@lukeed/ms')

const memoize = require('../util/memoize')
const send = require('.')

const { CACHE_TTL } = require('../constant')
const { TTL_DEFAULT, TTL_MIN, TTL_MAX } = require('../constant')

const getTtl = memoize(ttl => {
if (ttl === undefined || ttl === null) return CACHE_TTL
const value = ms(ttl)
if (Number.isNaN(Number(value))) return CACHE_TTL
if (value < ms('1h') || value > ms('28d')) return CACHE_TTL
if (ttl === undefined || ttl === null) return TTL_DEFAULT
const value = typeof ttl === 'number' ? ttl : parse(ttl)
if (value === undefined || value < TTL_MIN || value > TTL_MAX) { return TTL_DEFAULT }
return value
})

module.exports = require('cacheable-response')({
logger: require('debug-logfmt')('cacheable-response'),
ttl: CACHE_TTL,
ttl: TTL_DEFAULT,
staleTtl: false,
get: async ({ req, res, fn }) => ({
ttl: getTtl(req.query.ttl),
Expand Down
3 changes: 1 addition & 2 deletions src/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ const { API_URL, NODE_ENV, PORT } = require('./constant')

const server = createServer(require('.'))

require('./util/req-frequency')(server)

server.listen(PORT, () => {
debug({
status: 'listening',
Expand All @@ -24,6 +22,7 @@ server.listen(PORT, () => {

process.on('uncaughtException', error => {
debug.error('uncaughtException', {
message: error.message || error,
requestUrl: error.response?.requestUrl
})
})
4 changes: 2 additions & 2 deletions src/util/cacheable-lookup.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ const Tangerine = require('tangerine')

const { createMultiCache, createRedisCache } = require('./keyv')

const { CACHE_TTL } = require('../constant')
const { TTL_DEFAULT } = require('../constant')

module.exports = new CacheableLookup({
maxTtl: CACHE_TTL,
maxTtl: TTL_DEFAULT,
cache: createMultiCache(createRedisCache({ namespace: 'dns' })),
resolver: new Tangerine(
{
Expand Down
4 changes: 2 additions & 2 deletions src/util/keyv.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ const assert = require('assert')

const redis = require('./redis')

const { CACHE_TTL } = require('../constant')
const { TTL_DEFAULT } = require('../constant')

const createMultiCache = remote =>
new Keyv({ store: new KeyvMulti({ remote }) })

const createKeyv = opts => new Keyv({ ttl: CACHE_TTL, ...opts })
const createKeyv = opts => new Keyv({ ttl: TTL_DEFAULT, ...opts })

const createKeyvNamespace = opts => {
assert(opts.namespace, '`opts.namespace` is required.')
Expand Down
21 changes: 0 additions & 21 deletions src/util/req-frequency.js

This file was deleted.

23 changes: 12 additions & 11 deletions test/query-parameters.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
'use strict'

const { parse } = require('@lukeed/ms')
const test = require('ava')
const got = require('got')
const ms = require('ms')

const { CACHE_TTL } = require('../src/constant')
const { TTL_DEFAULT } = require('../src/constant')

const { runServer } = require('./helpers')
const { getTtl } = require('../src/send/cache')
Expand Down Expand Up @@ -52,13 +52,14 @@ test('fallback # use default value if fallback provided is not reachable', async
})

test('ttl', t => {
t.is(getTtl(), CACHE_TTL)
t.is(getTtl(null), CACHE_TTL)
t.is(getTtl(undefined), CACHE_TTL)
t.is(getTtl(0), CACHE_TTL)
t.is(getTtl('foo'), CACHE_TTL)
t.is(getTtl('29d'), CACHE_TTL)
t.is(getTtl('29d'), CACHE_TTL)
t.is(getTtl(ms('2h')), CACHE_TTL)
t.is(getTtl('2h'), ms('2h'))
t.is(getTtl(), TTL_DEFAULT)
t.is(getTtl(''), TTL_DEFAULT)
t.is(getTtl(null), TTL_DEFAULT)
t.is(getTtl(undefined), TTL_DEFAULT)
t.is(getTtl(0), TTL_DEFAULT)
t.is(getTtl('foo'), TTL_DEFAULT)
t.is(getTtl('29d'), TTL_DEFAULT)
t.is(getTtl('29d'), TTL_DEFAULT)
t.is(getTtl(parse('2h')), parse('2h'))
t.is(getTtl('2h'), parse('2h'))
})

0 comments on commit b4df49d

Please sign in to comment.