Comparing version 3.2.2 to 3.2.3
# Changelog | ||
## v3.2.3 - Jan 13, 2024 | ||
- `c.sse()` to support multi-line messages | ||
- Accommodate Bun 1.1.43 bugfix to HEAD content-length header | ||
- Avoid etags and compression when body is a stream | ||
- Add `maxSize` option to compression middleware (default 2GB) | ||
- Add LRU cache for file-based mime detection | ||
## v3.2.2 - Dec 7, 2024 | ||
@@ -4,0 +12,0 @@ |
@@ -58,6 +58,6 @@ // Generated by dts-bundle-generator v9.5.1 | ||
constructor(type: T, rawMessage: string | Buffer); | ||
raw(): string | Buffer; | ||
raw(): string | Buffer<ArrayBufferLike>; | ||
text(encoding?: BufferEncoding): string; | ||
toString(encoding?: BufferEncoding): string; | ||
buffer(): Buffer; | ||
buffer(): Buffer<ArrayBufferLike>; | ||
arrayBuffer(): ArrayBufferLike; | ||
@@ -251,4 +251,5 @@ readableStream(chunkSize?: number): ReadableStream<Uint8Array<ArrayBufferLike>>; | ||
calculator?: EtagHashCalculator; | ||
maxSize?: number; | ||
}; | ||
export declare function etags({ calculator, }?: EtagOptions): Middleware; | ||
export declare function etags({ calculator, maxSize, }?: EtagOptions): Middleware; | ||
export declare function defaultEtagsCalculator(_: Context, resp: Response): Promise<{ | ||
@@ -255,0 +256,0 @@ buffer: ArrayBuffer; |
{ | ||
"name": "bunshine", | ||
"version": "3.2.2", | ||
"version": "3.2.3", | ||
"module": "index.ts", | ||
@@ -8,2 +8,5 @@ "type": "module", | ||
"types": "dist/index.d.ts", | ||
"engines": { | ||
"bun": ">=1.1.33" | ||
}, | ||
"scripts": { | ||
@@ -53,11 +56,12 @@ "test-watch": "bun test --watch", | ||
"devDependencies": { | ||
"@types/bun": "^1.1.14", | ||
"@types/bun": "^1.1.16", | ||
"@types/node": "^22.10.6", | ||
"eventsource": "^2.0.2", | ||
"prettier": "^3.4.1", | ||
"prettier": "^3.4.2", | ||
"prettier-plugin-organize-imports": "^4.1.0", | ||
"redos-detector": "^5.1.3", | ||
"tinybench": "^3.0.6", | ||
"type-fest": "^4.29.0", | ||
"typescript": "^5.7.2" | ||
"tinybench": "^3.1.0", | ||
"type-fest": "^4.32.0", | ||
"typescript": "^5.7.3" | ||
} | ||
} |
@@ -1316,4 +1316,6 @@ # Bunshine | ||
// use etag headers | ||
app.use(etags()); | ||
// add total execution time in milliseconds | ||
app.use(performanceHeader); | ||
app.use(performanceHeader()); | ||
// log all requests | ||
@@ -1323,4 +1325,2 @@ app.use(process.env.NODE_ENV === 'development' ? devLogger() : prodLogger()); | ||
app.use(trailingSlashes('remove')); | ||
// use etag headers | ||
app.use(etags()); | ||
// compress all payloads | ||
@@ -1529,3 +1529,3 @@ app.use(compression()); | ||
closure, which saves about 3% of time. | ||
- `compression.ts` - gzip is the default preferred format for the compression | ||
- `compression-speed.ts` - gzip is the default preferred format for the compression | ||
middleware. Deflate provides no advantage, and Brotli provides 2-8% additional | ||
@@ -1535,3 +1535,3 @@ size savings at the cost of 100x as much CPU time as gzip. Brotli takes on | ||
for gzip. | ||
- `etags.ts` - etag calculation is very fast. On the order of tens of | ||
- `etags-speed.ts` - etag calculation is very fast. On the order of tens of | ||
microseconds for 100kb of html. | ||
@@ -1592,3 +1592,3 @@ - `lru-matcher.ts` - The default LRU cache size used for the router is 4000. | ||
- 🔲 tests for prodLogger | ||
- 🔲 tests for responseFactories | ||
- ✅ tests for responseFactories | ||
- ✅ tests for serveFiles | ||
@@ -1595,0 +1595,0 @@ - 🔲 100% test coverage |
@@ -39,2 +39,4 @@ import type { ZlibCompressionOptions } from 'bun'; | ||
if ( | ||
// no compression for streams such as text/stream | ||
/stream/i.test(resp.headers.get('Content-Type') || '') || | ||
context.request.method === 'HEAD' || | ||
@@ -41,0 +43,0 @@ !isCompressibleMime(resp.headers.get('Content-Type')) |
@@ -12,2 +12,3 @@ import { TypedArray } from 'type-fest'; | ||
calculator?: EtagHashCalculator; | ||
maxSize?: number; | ||
}; | ||
@@ -17,18 +18,20 @@ | ||
calculator = defaultEtagsCalculator, | ||
maxSize = 2 * 1024 * 1024 * 1024, // 2GB | ||
}: EtagOptions = {}): Middleware { | ||
return async (context: Context, next: NextFunction) => { | ||
const resp = await next(); | ||
if (context.request.method !== 'GET' || resp.status !== 200) { | ||
// Only use Etags for successful GET requests | ||
if (!_shouldGenerateEtag(resp)) { | ||
return resp; | ||
} | ||
const ifNoneMatch = context.request.headers.get('if-none-match'); | ||
const { buffer, hash } = await calculator(context, resp); | ||
const etag = `"${hash}"`; | ||
resp.headers.set('Etag', etag); | ||
if (ifNoneMatch && _includesEtag(ifNoneMatch, etag)) { | ||
if (_matches(context.request.headers, etag)) { | ||
resp.headers.set('Vary', 'Content-Encoding'); | ||
const status = ['GET', 'HEAD'].includes(context.request.method) | ||
? 204 // No content | ||
: 412; // Precondition failed | ||
return new Response('', { | ||
headers: resp.headers, | ||
status: 304, | ||
statusText: '', | ||
status, | ||
}); | ||
@@ -39,3 +42,2 @@ } | ||
status: 200, | ||
statusText: '', | ||
}); | ||
@@ -45,7 +47,62 @@ }; | ||
function _includesEtag(ifNoneMatch: string, etag: string) { | ||
const matches = ifNoneMatch.split(',').map(s => s.trim()); | ||
function _matches(headers: Headers, etag: string) { | ||
const ifNoneMatch = headers.get('if-none-match'); | ||
if (ifNoneMatch) { | ||
return ( | ||
!_includesEtag(ifNoneMatch, etag) || !_includesEtag(ifNoneMatch, '*') | ||
); | ||
} | ||
const ifMatch = headers.get('if-match'); | ||
if (ifMatch) { | ||
return _includesEtag(ifMatch, etag) || _includesEtag(ifMatch, '*'); | ||
} | ||
return false; | ||
} | ||
function _includesEtag(header: string, etag: string) { | ||
const matches = header.split(',').map(s => s.trim()); | ||
return matches.includes(etag); | ||
} | ||
function _shouldGenerateEtag(response: Response) { | ||
// Ensure the response object is valid | ||
if (!(response instanceof Response)) { | ||
return false; | ||
} | ||
// List of status codes where ETags generally make sense | ||
// Technically 404 could be included, but the application should use custom logic | ||
const validStatusCodes = [200, 201, 203, 204, 206, /*404,*/ 410]; | ||
// Check if the response status code is in the valid list | ||
if (!validStatusCodes.includes(response.status)) { | ||
return false; | ||
} | ||
// Check if the response is cacheable | ||
const cacheControl = response.headers.get('Cache-Control'); | ||
if (cacheControl && /no-store/i.test(cacheControl)) { | ||
return false; // Do not generate ETag for non-cacheable responses | ||
} | ||
// Check if the response method supports ETag generation | ||
const method = response.headers.get('X-Request-Method') || 'GET'; // Custom header to track the request method | ||
const methodsThatSupportEtag = ['GET', 'HEAD', 'PUT', 'POST', 'PATCH']; | ||
if (!methodsThatSupportEtag.includes(method.toUpperCase())) { | ||
return false; | ||
} | ||
// Check if the response has a body or meaningful representation | ||
const contentLength = response.headers.get('Content-Length'); | ||
if ( | ||
response.status === 204 || | ||
(contentLength && parseInt(contentLength, 10) === 0) | ||
) { | ||
return false; // No body, no ETag | ||
} | ||
// If all conditions are met, it makes sense to generate an ETag | ||
return true; | ||
} | ||
export async function defaultEtagsCalculator(_: Context, resp: Response) { | ||
@@ -52,0 +109,0 @@ const buffer = await resp.arrayBuffer(); |
import { BunFile } from 'bun'; | ||
import { fileTypeFromBuffer } from 'file-type'; | ||
import fs from 'fs/promises'; | ||
import { LRUCache } from 'lru-cache'; | ||
import path from 'node:path'; | ||
@@ -10,2 +11,3 @@ | ||
const detectMimeChunkSize = 4100; // from file-type reasonableDetectionSizeInBytes | ||
const mimeCache = new LRUCache<string, string>({ max: 10_000 }); | ||
@@ -39,3 +41,3 @@ export function isFileLike(file: any): file is FileLike { | ||
file: FileLike, | ||
maybeBuffer?: ArrayBuffer | Uint8Array | ||
maybeEntireFile?: ArrayBuffer | Uint8Array | ||
) { | ||
@@ -53,4 +55,4 @@ const filename = getFileBaseName(file); | ||
return ( | ||
(maybeBuffer | ||
? await getBufferMime(maybeBuffer) | ||
(maybeEntireFile | ||
? await getBufferMime(maybeEntireFile) | ||
: await getChunkMime(file)) || defaultMimeType | ||
@@ -67,4 +69,10 @@ ); | ||
export async function getChunkMime(path: string) { | ||
const chunk = await readChunk(path, 0, detectMimeChunkSize); | ||
return chunk ? getBufferMime(chunk) : null; | ||
const lastModified = Bun.file(path).lastModified; | ||
const key = `${path}-${lastModified}`; | ||
if (!mimeCache.has(key)) { | ||
const chunk = await readChunk(path, 0, detectMimeChunkSize); | ||
const mime = chunk ? await getBufferMime(chunk) : ''; | ||
mimeCache.set(key, mime); | ||
} | ||
return mimeCache.get(key); | ||
} | ||
@@ -71,0 +79,0 @@ |
@@ -35,8 +35,12 @@ import type Context from '../../Context/Context'; | ||
if (arguments.length === 1) { | ||
encoded = textEncoder.encode(`data: ${eventName}\n\n`); | ||
encoded = textEncoder.encode( | ||
`data: ${handleNewlines(eventName)}\n\n` | ||
); | ||
} else { | ||
if (data && typeof data !== 'string') { | ||
data = JSON.stringify(data); | ||
} else { | ||
data = handleNewlines(String(data)); | ||
} | ||
let message = `event: ${eventName}\ndata:${String(data)}`; | ||
let message = `event: ${eventName}\ndata:${data}`; | ||
if (id) { | ||
@@ -104,1 +108,5 @@ message += `\nid: ${id}`; | ||
} | ||
function handleNewlines(data: string) { | ||
return data.replace(/\n/g, '\ndata: '); | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
2210560
2640
9