From edadd4f07cab8d8f8feadfbaa2a393668626883c Mon Sep 17 00:00:00 2001 From: Ben Grant Date: Thu, 30 Jun 2022 13:20:32 +1000 Subject: [PATCH 1/6] Rudimentary caching --- src/server.ts | 58 +++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 47 insertions(+), 11 deletions(-) diff --git a/src/server.ts b/src/server.ts index 6b8edb3..a509430 100644 --- a/src/server.ts +++ b/src/server.ts @@ -7,8 +7,50 @@ import injectHTML from './injectHTML' export type Injection = { head: string, body: string } export type RouteHandler = (req: Request) => Promise -export type Routes = { - [route: string]: RouteHandler +export type RouteObject = { handler: RouteHandler, key?: any } +export type RouteValue = RouteHandler | RouteObject +export type Routes = { [route: string]: RouteValue } + +// TODO: use a more robust cache storage +const cache = {} + +// Take the route object and return a handler and optional key +const getRouteObject: (value: RouteValue) => Promise = 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, resolve the handler or serve the cached handler +const resolveHandler = async ( + route: string, + req: Request, + value: RouteValue, + indexText: string, +) => { + const { handler, key } = await getRouteObject(value) + + const resolvedKey = await resolveKey(key, req) + const keyValue = resolvedKey && `${route}-${JSON.stringify(resolvedKey)}` + + if (keyValue && cache[keyValue]) { + return cache[keyValue] ? injectHTML(indexText, cache[keyValue]) : indexText + } 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 (keyValue) { + cache[keyValue] = handlerResult + } + + return handlerResult ? injectHTML(indexText, handlerResult) : indexText + } } const startServer = async ({ @@ -32,16 +74,10 @@ const startServer = async ({ app.use(cors()) // Register dynamic routes - Object.entries(routes as Routes).forEach(([route, handler]) => { + Object.entries(routes as Routes).forEach(([route, value]) => { 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 resolveHandler(route, req, value, indexText) + return res.header('Content-Type', 'text/html').send(injectedIndex) }) }) From bf9c6b46d56df518f6eb482544c5b82b9c61ecbe Mon Sep 17 00:00:00 2001 From: Ben Grant Date: Thu, 30 Jun 2022 13:41:56 +1000 Subject: [PATCH 2/6] Use keyv for caching and add ttl option --- package.json | 1 + src/cache.ts | 5 +++++ src/server.ts | 17 ++++++++--------- yarn.lock | 26 ++++++++++++++++++++++++++ 4 files changed, 40 insertions(+), 9 deletions(-) create mode 100644 src/cache.ts diff --git a/package.json b/package.json index 4b91511..93a6642 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/src/cache.ts b/src/cache.ts new file mode 100644 index 0000000..bd82f04 --- /dev/null +++ b/src/cache.ts @@ -0,0 +1,5 @@ +import Keyv from 'keyv' + +const cache = new Keyv({ namespace: 'epoxy' }) + +export default cache diff --git a/src/server.ts b/src/server.ts index a509430..0dd004e 100644 --- a/src/server.ts +++ b/src/server.ts @@ -4,16 +4,14 @@ 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 -export type RouteObject = { handler: RouteHandler, key?: any } +export type RouteObject = { handler: RouteHandler, key?: any, ttl?: number } export type RouteValue = RouteHandler | RouteObject export type Routes = { [route: string]: RouteValue } -// TODO: use a more robust cache storage -const cache = {} - // Take the route object and return a handler and optional key const getRouteObject: (value: RouteValue) => Promise = async value => { if (typeof value === 'function') return { handler: value } @@ -33,20 +31,21 @@ const resolveHandler = async ( value: RouteValue, indexText: string, ) => { - const { handler, key } = await getRouteObject(value) + const { handler, key, ttl } = await getRouteObject(value) const resolvedKey = await resolveKey(key, req) const keyValue = resolvedKey && `${route}-${JSON.stringify(resolvedKey)}` - if (keyValue && cache[keyValue]) { - return cache[keyValue] ? injectHTML(indexText, cache[keyValue]) : indexText + const cachedResult = 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 (keyValue) { - cache[keyValue] = handlerResult + if (keyValue && handlerResult) { + await cache.set(keyValue, handlerResult, ttl) } return handlerResult ? injectHTML(indexText, handlerResult) : indexText diff --git a/yarn.lock b/yarn.lock index dde26c0..4a4963f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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" @@ -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" @@ -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" @@ -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" From 29ff4870eadb4016511879999d63c30751120c29 Mon Sep 17 00:00:00 2001 From: Ben Grant Date: Thu, 30 Jun 2022 13:54:32 +1000 Subject: [PATCH 3/6] Update docs --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index 0d6655f..c7128ce 100644 --- a/README.md +++ b/README.md @@ -136,8 +136,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. + 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 From 905aa9bc2fc6276af40cc98310d1698c09ab456e Mon Sep 17 00:00:00 2001 From: Ben Grant Date: Thu, 30 Jun 2022 13:58:42 +1000 Subject: [PATCH 4/6] Bump version to 2.1.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 93a6642..3692b44 100644 --- a/package.json +++ b/package.json @@ -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", From 60bbcb89c8cdc353df1e6cdc526d699dc0e03e4c Mon Sep 17 00:00:00 2001 From: Ben Grant Date: Sat, 2 Jul 2022 11:18:31 +1000 Subject: [PATCH 5/6] Use logical type/fn names --- src/server.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/server.ts b/src/server.ts index 0dd004e..9934894 100644 --- a/src/server.ts +++ b/src/server.ts @@ -7,13 +7,13 @@ import injectHTML from './injectHTML' import cache from './cache' export type Injection = { head: string, body: string } -export type RouteHandler = (req: Request) => Promise -export type RouteObject = { handler: RouteHandler, key?: any, ttl?: number } -export type RouteValue = RouteHandler | RouteObject -export type Routes = { [route: string]: RouteValue } +export type RouteHandlerFn = (req: Request) => Promise +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 getRouteObject: (value: RouteValue) => Promise = async value => { +const routeHandlerAsObject: (value: RouteHandler) => Promise = async value => { if (typeof value === 'function') return { handler: value } return value } @@ -24,14 +24,14 @@ const resolveKey = async (key, req) => { return key } -// Given a route, resolve the handler or serve the cached handler -const resolveHandler = async ( +// Given a route, run the handler or serve the cached handler +const applyHandler = async ( route: string, req: Request, - value: RouteValue, + value: RouteHandler, indexText: string, ) => { - const { handler, key, ttl } = await getRouteObject(value) + const { handler, key, ttl } = await routeHandlerAsObject(value) const resolvedKey = await resolveKey(key, req) const keyValue = resolvedKey && `${route}-${JSON.stringify(resolvedKey)}` @@ -73,9 +73,9 @@ const startServer = async ({ app.use(cors()) // Register dynamic routes - Object.entries(routes as Routes).forEach(([route, value]) => { + Object.entries(routes as Routes).forEach(([route, handler]) => { app.get(route, async (req, res) => { - const injectedIndex = await resolveHandler(route, req, value, indexText) + const injectedIndex = await applyHandler(route, req, handler, indexText) return res.header('Content-Type', 'text/html').send(injectedIndex) }) }) From f28be82c7f6099a5776424825ee9a10d54f9eaaa Mon Sep 17 00:00:00 2001 From: Ben Grant Date: Sat, 2 Jul 2022 11:35:52 +1000 Subject: [PATCH 6/6] Add command line option to disable caching --- README.md | 15 ++++++++------- index.ts | 8 +++++++- src/server.ts | 8 +++++--- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index c7128ce..a3fbe60 100644 --- a/README.md +++ b/README.md @@ -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` @@ -148,7 +149,7 @@ export default { } ``` -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. +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. diff --git a/index.ts b/index.ts index c233136..69c0078 100644 --- a/index.ts +++ b/index.ts @@ -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) { @@ -30,6 +30,7 @@ const serve = async ({ target, routeFile, host, port, index, build }) => { target, routes, index: path.join(target, index), + cache, }) } @@ -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 ) diff --git a/src/server.ts b/src/server.ts index 9934894..a148d85 100644 --- a/src/server.ts +++ b/src/server.ts @@ -30,13 +30,14 @@ const applyHandler = async ( 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 = keyValue && await cache.get(keyValue) + const cachedResult = cacheEnabled && keyValue && await cache.get(keyValue) if (cachedResult) { return injectHTML(indexText, cachedResult) } else { @@ -44,7 +45,7 @@ const applyHandler = async ( .catch(e => console.warn(`Handler for route ${route} threw an error`, e)) // Save result in cache if key set - if (keyValue && handlerResult) { + if (cacheEnabled && keyValue && handlerResult) { await cache.set(keyValue, handlerResult, ttl) } @@ -58,6 +59,7 @@ const startServer = async ({ index = './dist/index.html', port, routes, + cache = true, }) => { // Resolve paths const resolvedTarget = path.resolve(target) @@ -75,7 +77,7 @@ const startServer = async ({ // Register dynamic routes Object.entries(routes as Routes).forEach(([route, handler]) => { app.get(route, async (req, res) => { - const injectedIndex = await applyHandler(route, req, handler, indexText) + const injectedIndex = await applyHandler(route, req, handler, indexText, cache) return res.header('Content-Type', 'text/html').send(injectedIndex) }) })