Skip to content
Snippets Groups Projects
Commit 51ad5f9a authored by richard.petersen's avatar richard.petersen :sailboat:
Browse files

Add: ui-files are compressed before stored in cache or delivered to the client

parent f8077a21
No related branches found
No related tags found
No related merge requests found
...@@ -4,6 +4,7 @@ import { generateSimpleViteManifest, mockApp, mockConfig, mockFetch } from '../s ...@@ -4,6 +4,7 @@ import { generateSimpleViteManifest, mockApp, mockConfig, mockFetch } from '../s
import { client } from '../src/redis.js' import { client } from '../src/redis.js'
import * as td from 'testdouble' import * as td from 'testdouble'
import { getRedisKey } from '../src/util.js' import { getRedisKey } from '../src/util.js'
import { gunzipSync } from 'node:zlib'
describe('File caching service', function () { describe('File caching service', function () {
let app let app
...@@ -40,8 +41,10 @@ describe('File caching service', function () { ...@@ -40,8 +41,10 @@ describe('File caching service', function () {
expect(response.statusCode).to.equal(200) expect(response.statusCode).to.equal(200)
const version = response.headers.version const version = response.headers.version
expect(await client.get(getRedisKey({ version, name: '/index.html:meta' }))).to.equal('{"sha256Sum":"iFSuC3aK6EN/ASUamuZ+j3xZXI9eBdIlxtVDFjn7y1I=","headers":{"content-type":"text/html","dependencies":false}}') const body = await client.getBuffer(getRedisKey({ version, name: '/index.html:body' }))
expect(await client.get(getRedisKey({ version, name: '/index.html:body' }))).to.equal('<html><head></head><body>it\'s me</body></html>') expect(gunzipSync(body).toString()).to.equal('<html><head></head><body>it\'s me</body></html>')
const meta = await client.get(getRedisKey({ version, name: '/index.html:meta' }))
expect(meta).to.equal('{"headers":{"content-type":"text/html","dependencies":false,"content-encoding":"gzip"}}')
}) })
it('serves files from redis and stores them in local cache', async function () { it('serves files from redis and stores them in local cache', async function () {
......
...@@ -5,6 +5,7 @@ import { expect } from 'chai' ...@@ -5,6 +5,7 @@ import { expect } from 'chai'
import * as td from 'testdouble' import * as td from 'testdouble'
import RedisMock from 'ioredis-mock' import RedisMock from 'ioredis-mock'
import sinon from 'sinon' import sinon from 'sinon'
import zlib from 'node:zlib'
const image = fs.readFileSync('./spec/media/image.png') const image = fs.readFileSync('./spec/media/image.png')
const imageStat = fs.statSync('./spec/media/image.png') const imageStat = fs.statSync('./spec/media/image.png')
...@@ -347,4 +348,13 @@ describe('File caching service', function () { ...@@ -347,4 +348,13 @@ describe('File caching service', function () {
expect(spy.callCount).to.equal(1) expect(spy.callCount).to.equal(1)
}) })
}) })
it('serves files as gzip', async function () {
const response = await request(app).get('/example.js')
expect(response.statusCode).to.equal(200)
expect(response.headers['content-encoding']).to.equal('gzip')
// check for files in redis
expect(zlib.gunzipSync(await redis.client.getBuffer('ui-middleware:554855300:/example.js:body')).toString()).to.equal(response.text)
})
}) })
import crypto from 'crypto'
import { config } from './config.js' import { config } from './config.js'
import { getRedisKey, isJSFile } from './util.js' import { getRedisKey, isJSFile } from './util.js'
import { getCSSDependenciesFor, getViteManifests } from './manifests.js' import { getCSSDependenciesFor, getViteManifests } from './manifests.js'
...@@ -6,12 +5,28 @@ import * as cache from './cache.js' ...@@ -6,12 +5,28 @@ import * as cache from './cache.js'
import { logger } from './logger.js' import { logger } from './logger.js'
import { NotFoundError } from './errors.js' import { NotFoundError } from './errors.js'
import * as redis from './redis.js' import * as redis from './redis.js'
import zlib from 'node:zlib'
import { promisify } from 'node:util'
const gzip = promisify(zlib.gzip)
export function createWritable (body) { export function createWritable (body) {
if (typeof body !== 'string' && !(body instanceof Buffer)) return JSON.stringify(body) if (typeof body !== 'string' && !(body instanceof Buffer)) return JSON.stringify(body)
return body return body
} }
async function createFileBuffer (response, dependencies) {
const cssString = dependencies && dependencies.map(file => `"${file}"`).join(',')
const appendix = cssString && `\n/*injected by ui-middleware*/document.dispatchEvent(new CustomEvent("load-css",{detail:{css:[${cssString}]}}))`
const resBuffer = await response.arrayBuffer()
const appendixLength = appendix?.length || 0
const buffer = Buffer.alloc(resBuffer.byteLength + appendixLength)
buffer.fill(Buffer.from(resBuffer), 0, resBuffer.byteLength)
if (appendix) buffer.write(appendix, resBuffer.byteLength)
return await gzip(buffer)
}
export async function fetchFileWithHeadersFromBaseUrl (path, baseUrl, version) { export async function fetchFileWithHeadersFromBaseUrl (path, baseUrl, version) {
const [response, dependencies] = await Promise.all([ const [response, dependencies] = await Promise.all([
fetch(new URL(path, baseUrl)), fetch(new URL(path, baseUrl)),
...@@ -20,23 +35,14 @@ export async function fetchFileWithHeadersFromBaseUrl (path, baseUrl, version) { ...@@ -20,23 +35,14 @@ export async function fetchFileWithHeadersFromBaseUrl (path, baseUrl, version) {
if (!response.ok) throw new NotFoundError(`Error fetching file: ${path}`) if (!response.ok) throw new NotFoundError(`Error fetching file: ${path}`)
const cssString = dependencies && dependencies.map(file => `"${file}"`).join(',') const body = await createFileBuffer(response, dependencies)
const appendix = cssString && `\n/*injected by ui-middleware*/document.dispatchEvent(new CustomEvent("load-css",{detail:{css:[${cssString}]}}))`
const resBuffer = await response.arrayBuffer()
const appendixLength = appendix?.length || 0
const body = Buffer.alloc(resBuffer.byteLength + appendixLength)
body.fill(Buffer.from(resBuffer), 0, resBuffer.byteLength)
if (appendix) body.write(appendix, resBuffer.byteLength)
const sha256Sum = crypto.createHash('sha256').update(body).digest('base64')
return { return {
body, body,
sha256Sum,
headers: { headers: {
'content-type': response.headers.get('content-type'), 'content-type': response.headers.get('content-type'),
dependencies dependencies,
'content-encoding': 'gzip'
} }
} }
} }
......
...@@ -7,10 +7,9 @@ export default async function (req, res, next) { ...@@ -7,10 +7,9 @@ export default async function (req, res, next) {
if (req.method !== 'GET') return next() if (req.method !== 'GET') return next()
const version = res.version const version = res.version
const path = req.path === '/' ? '/index.html' : req.path const path = req.path === '/' ? '/index.html' : req.path
const { body, headers, sha256Sum } = await getFile({ version, path }) const { body, headers } = await getFile({ version, path })
res.set(headers) res.set(headers)
res.locals.sha256Sum = sha256Sum
res.send(body) res.send(body)
} catch (err) { } catch (err) {
const errors = err.errors || [err] const errors = err.errors || [err]
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment