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
import { client } from '../src/redis.js'
import * as td from 'testdouble'
import { getRedisKey } from '../src/util.js'
import { gunzipSync } from 'node:zlib'
describe('File caching service', function () {
let app
......@@ -40,8 +41,10 @@ describe('File caching service', function () {
expect(response.statusCode).to.equal(200)
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}}')
expect(await client.get(getRedisKey({ version, name: '/index.html:body' }))).to.equal('<html><head></head><body>it\'s me</body></html>')
const body = await client.getBuffer(getRedisKey({ version, name: '/index.html:body' }))
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 () {
......
......@@ -5,6 +5,7 @@ import { expect } from 'chai'
import * as td from 'testdouble'
import RedisMock from 'ioredis-mock'
import sinon from 'sinon'
import zlib from 'node:zlib'
const image = fs.readFileSync('./spec/media/image.png')
const imageStat = fs.statSync('./spec/media/image.png')
......@@ -347,4 +348,13 @@ describe('File caching service', function () {
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 { getRedisKey, isJSFile } from './util.js'
import { getCSSDependenciesFor, getViteManifests } from './manifests.js'
......@@ -6,12 +5,28 @@ import * as cache from './cache.js'
import { logger } from './logger.js'
import { NotFoundError } from './errors.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) {
if (typeof body !== 'string' && !(body instanceof Buffer)) return JSON.stringify(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) {
const [response, dependencies] = await Promise.all([
fetch(new URL(path, baseUrl)),
......@@ -20,23 +35,14 @@ export async function fetchFileWithHeadersFromBaseUrl (path, baseUrl, version) {
if (!response.ok) throw new NotFoundError(`Error fetching file: ${path}`)
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 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')
const body = await createFileBuffer(response, dependencies)
return {
body,
sha256Sum,
headers: {
'content-type': response.headers.get('content-type'),
dependencies
dependencies,
'content-encoding': 'gzip'
}
}
}
......
......@@ -7,10 +7,9 @@ export default async function (req, res, next) {
if (req.method !== 'GET') return next()
const version = res.version
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.locals.sha256Sum = sha256Sum
res.send(body)
} catch (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