Skip to content

Commit

Permalink
Merge pull request #19 from stevent-team/feat/caching
Browse files Browse the repository at this point in the history
In-memory caching
  • Loading branch information
GRA0007 authored Jul 2, 2022
2 parents 0297f79 + f28be82 commit cac75ca
Show file tree
Hide file tree
Showing 6 changed files with 107 additions and 19 deletions.
25 changes: 19 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,13 @@ target Path to static directory
routeFile Path to cjs router script (can use ES6 with --build option)
Options:
--help Show help [boolean]
--version Show version number [boolean]
-p, --port port to use for http server [string] [default: 8080]
-h, --host host to use for http server [string] [default: "0.0.0.0"]
-i, --index path to index html inside of target [string] [default: "index.html"]
-b, --build build routes file in memory [boolean] [default: false]
--help Show help [boolean]
--version Show version number [boolean]
-p, --port port to use for http server [string] [default: 8080]
-h, --host host to use for http server [string] [default: "0.0.0.0"]
-i, --index path to index html inside of target [string] [default: "index.html"]
-b, --build build routes file in memory [boolean] [default: false]
--cache use --no-cache to disable all route caching [boolean] [default: true]
```

#### `epoxy build`
Expand Down Expand Up @@ -136,8 +137,20 @@ Your route handler will be built by Epoxy using Parcel when Epoxy is started. It
export default {
'express/js/route/:withParams': yourRouteHandlerFunction
}

// or

export default {
'express/js/route/:withParams': {
handler: yourRouteHandlerFunction,
key: request => ['A key to cache with', request.params.withParams], // optional, any type, will cache result based on key
ttl: 3600000, // time to live, optional, in milliseconds
}
}
```

If you include a `key` that is not undefined, then the result of the handler function will be cached based on that key. You can also include a time to live `ttl` parameter which will expire the cache after a certain amount of milliseconds. There is also a command line argument `--no-cache` that will disable all caching irrespective of any keys provided.

Each route must have a function to handle it, which will receive a `request` object from ExpressJS, from which you can learn about the request. See the express docs for the [request object](https://expressjs.com/en/api.html#req) for more information.

## Contributing
Expand Down
8 changes: 7 additions & 1 deletion index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Parcel } from '@parcel/core'
import loadRoutes from './src/loadRoutes'
import startServer, { Routes } from './src/server'

const serve = async ({ target, routeFile, host, port, index, build }) => {
const serve = async ({ target, routeFile, host, port, index, build, cache }) => {
// Load routes
let routes: Routes = {}
if (routeFile) {
Expand All @@ -30,6 +30,7 @@ const serve = async ({ target, routeFile, host, port, index, build }) => {
target,
routes,
index: path.join(target, index),
cache,
})
}

Expand Down Expand Up @@ -91,6 +92,11 @@ yargs
type: 'boolean',
description: 'build routes file in memory',
default: false,
})
.option('cache', {
type: 'boolean',
description: 'use --no-cache to disable all route handler result caching',
default: true,
}),
serve
)
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@stevent-team/epoxy",
"version": "2.0.1",
"version": "2.1.0",
"description": "Lightweight server-side per-route html injection",
"keywords": [
"epoxy",
Expand Down Expand Up @@ -38,6 +38,7 @@
"@parcel/fs": "^2.5.0",
"cors": "^2.8.5",
"express": "^4.18.1",
"keyv": "^4.3.2",
"parcel": "^2.5.0",
"require-from-string": "^2.0.2",
"yargs": "^17.5.1"
Expand Down
5 changes: 5 additions & 0 deletions src/cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import Keyv from 'keyv'

const cache = new Keyv({ namespace: 'epoxy' })

export default cache
59 changes: 48 additions & 11 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,53 @@ import cors from 'cors'
import { promises as fs } from 'fs'

import injectHTML from './injectHTML'
import cache from './cache'

export type Injection = { head: string, body: string }
export type RouteHandler = (req: Request) => Promise<Injection>
export type Routes = {
[route: string]: RouteHandler
export type RouteHandlerFn = (req: Request) => Promise<Injection>
export type RouteHandlerObject = { handler: RouteHandlerFn, key?: any, ttl?: number }
export type RouteHandler = RouteHandlerFn | RouteHandlerObject
export type Routes = { [route: string]: RouteHandler }

// Take the route object and return a handler and optional key
const routeHandlerAsObject: (value: RouteHandler) => Promise<RouteHandlerObject> = async value => {
if (typeof value === 'function') return { handler: value }
return value
}

// Take a key that could be a string or function, and resolve to a string
const resolveKey = async (key, req) => {
if (typeof key === 'function') return key(req)
return key
}

// Given a route, run the handler or serve the cached handler
const applyHandler = async (
route: string,
req: Request,
value: RouteHandler,
indexText: string,
cacheEnabled: boolean,
) => {
const { handler, key, ttl } = await routeHandlerAsObject(value)

const resolvedKey = await resolveKey(key, req)
const keyValue = resolvedKey && `${route}-${JSON.stringify(resolvedKey)}`

const cachedResult = cacheEnabled && keyValue && await cache.get(keyValue)
if (cachedResult) {
return injectHTML(indexText, cachedResult)
} else {
const handlerResult = await handler(req)
.catch(e => console.warn(`Handler for route ${route} threw an error`, e))

// Save result in cache if key set
if (cacheEnabled && keyValue && handlerResult) {
await cache.set(keyValue, handlerResult, ttl)
}

return handlerResult ? injectHTML(indexText, handlerResult) : indexText
}
}

const startServer = async ({
Expand All @@ -17,6 +59,7 @@ const startServer = async ({
index = './dist/index.html',
port,
routes,
cache = true,
}) => {
// Resolve paths
const resolvedTarget = path.resolve(target)
Expand All @@ -34,14 +77,8 @@ const startServer = async ({
// Register dynamic routes
Object.entries(routes as Routes).forEach(([route, handler]) => {
app.get(route, async (req, res) => {
const handlerResult = await handler(req)
.catch(e => console.warn(`Handler for route ${route} threw an error`, e))
const injectedIndex = handlerResult
? injectHTML(indexText, handlerResult)
: indexText
return res
.header('Content-Type', 'text/html')
.send(injectedIndex)
const injectedIndex = await applyHandler(route, req, handler, indexText, cache)
return res.header('Content-Type', 'text/html').send(injectedIndex)
})
})

Expand Down
26 changes: 26 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -702,6 +702,11 @@
"@types/qs" "*"
"@types/serve-static" "*"

"@types/json-buffer@~3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@types/json-buffer/-/json-buffer-3.0.0.tgz#85c1ff0f0948fc159810d4b5be35bf8c20875f64"
integrity sha512-3YP80IxxFJB4b5tYC2SUPwkg0XQLiu0nWvhRgEatgjf+29IcWO9X1k8xRv5DGssJ/lCrjYTjQPcobJr2yWIVuQ==

"@types/mime@^1":
version "1.3.2"
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a"
Expand Down Expand Up @@ -921,6 +926,14 @@ commander@^7.0.0, commander@^7.2.0:
resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7"
integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==

compress-brotli@^1.3.8:
version "1.3.8"
resolved "https://registry.yarnpkg.com/compress-brotli/-/compress-brotli-1.3.8.tgz#0c0a60c97a989145314ec381e84e26682e7b38db"
integrity sha512-lVcQsjhxhIXsuupfy9fmZUFtAIdBmXA7EGY6GBdgZ++qkM9zG4YFT8iU7FoBxzryNDMOpD1HIFHUSX4D87oqhQ==
dependencies:
"@types/json-buffer" "~3.0.0"
json-buffer "~3.0.1"

content-disposition@0.5.4:
version "0.5.4"
resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe"
Expand Down Expand Up @@ -1300,6 +1313,11 @@ js-tokens@^4.0.0:
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==

json-buffer@3.0.1, json-buffer@~3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13"
integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==

json-parse-even-better-errors@^2.3.0:
version "2.3.1"
resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d"
Expand All @@ -1310,6 +1328,14 @@ json5@^2.2.0, json5@^2.2.1:
resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.1.tgz#655d50ed1e6f95ad1a3caababd2b0efda10b395c"
integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==

keyv@^4.3.2:
version "4.3.2"
resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.3.2.tgz#e839df676a0c7ee594c8835e7c1c83742558e5c2"
integrity sha512-kn8WmodVBe12lmHpA6W8OY7SNh6wVR+Z+wZESF4iF5FCazaVXGWOtnbnvX0tMQ1bO+/TmOD9LziuYMvrIIs0xw==
dependencies:
compress-brotli "^1.3.8"
json-buffer "3.0.1"

lines-and-columns@^1.1.6:
version "1.2.4"
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632"
Expand Down

0 comments on commit cac75ca

Please sign in to comment.