Comparing version 2.0.0 to 3.0.0-rc.1
@@ -19,5 +19,2 @@ #!/usr/bin/env bun | ||
index: ['index.html'], | ||
gzip: { | ||
cache: false, | ||
}, | ||
}) | ||
@@ -24,0 +21,0 @@ ); |
63
index.ts
@@ -15,9 +15,12 @@ export { | ||
type NextFunction, | ||
type SingleErrorHandler, | ||
type SingleHandler, | ||
} from './src/HttpRouter/HttpRouter'; | ||
export { | ||
buildFileResponse, | ||
factory, | ||
file, | ||
json, | ||
minGzipSize, | ||
redirect, | ||
sse, | ||
type Factory, | ||
@@ -30,2 +33,36 @@ type FileResponseOptions, | ||
export { | ||
compression, | ||
compressionDefaults, | ||
type CompressionOptions, | ||
} from './src/middleware/compression/compression'; | ||
export { | ||
cors, | ||
corsDefaults, | ||
type CorsOptions, | ||
} from './src/middleware/cors/cors'; | ||
export { devLogger } from './src/middleware/devLogger/devLogger'; | ||
export { | ||
defaultEtagsCalculator, | ||
etags, | ||
type EtagHashCalculator, | ||
type EtagOptions, | ||
} from './src/middleware/etags/etags'; | ||
export { | ||
headers, | ||
type HeaderCondition, | ||
type HeaderValue, | ||
type HeaderValues, | ||
} from './src/middleware/headers/headers'; | ||
export { performanceHeader } from './src/middleware/performanceHeader/performanceHeader'; | ||
export { prodLogger } from './src/middleware/prodLogger/prodLogger'; | ||
export { | ||
responseCache, | ||
type ResponseCache, | ||
} from './src/middleware/responseCache/responseCache'; | ||
export { | ||
serveFiles, | ||
type ServeFilesOptions, | ||
} from './src/middleware/serveFiles/serveFiles'; | ||
export { trailingSlashes } from './src/middleware/trailingSlashes/trailingSlashes'; | ||
export { | ||
default as SocketRouter, | ||
@@ -36,3 +73,3 @@ type BunHandlers, | ||
type SocketErrorHandler, | ||
type SocketEventName, | ||
type SocketEventType, | ||
type SocketMessageHandler, | ||
@@ -42,22 +79,2 @@ type SocketPlainHandler, | ||
type WsDataShape, | ||
} from './src/SocketRouter/SocketRouter.ts'; | ||
export { cors, type CorsOptions } from './src/middleware/cors/cors'; | ||
export { devLogger } from './src/middleware/devLogger/devLogger'; | ||
export { performanceHeader } from './src/middleware/performanceHeader/performanceHeader.ts'; | ||
export { prodLogger } from './src/middleware/prodLogger/prodLogger'; | ||
export { securityHeaders } from './src/middleware/securityHeaders/securityHeaders'; | ||
export type { | ||
AllowedApis, | ||
CSPDirectives, | ||
CSPSource, | ||
ReportOptions, | ||
SandboxOptions, | ||
SecurityHeaderOptions, | ||
SecurityHeaderValue, | ||
} from './src/middleware/securityHeaders/securityHeaders.types.ts'; | ||
export { | ||
serveFiles, | ||
type GzipOptions, | ||
type StaticOptions, | ||
} from './src/middleware/serveFiles/serveFiles'; | ||
export { trailingSlashes } from './src/middleware/trailingSlashes/trailingSlashes'; | ||
} from './src/SocketRouter/SocketRouter'; |
{ | ||
"name": "bunshine", | ||
"version": "2.0.0", | ||
"version": "3.0.0-rc.1", | ||
"module": "server/server.ts", | ||
"type": "module", | ||
"main": "index.ts", | ||
"types": "index.d.ts", | ||
"types": "dist/index.d.ts", | ||
"scripts": { | ||
@@ -12,3 +12,4 @@ "test-watch": "bun test --watch", | ||
"lint": "tsc", | ||
"build": "rm index.d.ts; bunx dts-bundle-generator -o index.d.ts index.ts", | ||
"build:esm": "yes | npx esbuild index.ts --bundle --platform=node --format=esm --external:./package.json --outfile=dist/index.mjs", | ||
"build:dts": "yes | bunx dts-bundle-generator -o dist/index.d.ts index.ts", | ||
"example": "bun --watch ./examples/server.ts" | ||
@@ -22,3 +23,3 @@ }, | ||
"bin": { | ||
"serve": "./bin/serve.ts" | ||
"bunshine-serve": "./bin/serve.ts" | ||
}, | ||
@@ -50,8 +51,6 @@ "keywords": [ | ||
"dependencies": { | ||
"lru-cache": "11.0.1" | ||
"lru-cache": "11.0.2" | ||
}, | ||
"devDependencies": { | ||
"@types/eventsource": "1.1.15", | ||
"@types/ms": "0.7.34", | ||
"bun-types": "1.1.29", | ||
"@types/bun": "1.1.13", | ||
"eventsource": "2.0.2", | ||
@@ -61,7 +60,7 @@ "globby": "14.0.2", | ||
"prettier-plugin-organize-imports": "4.1.0", | ||
"redos-detector": "5.1.0", | ||
"redos-detector": "5.1.3", | ||
"tinybench": "2.9.0", | ||
"type-fest": "4.26.1", | ||
"typescript": "5.6.2" | ||
"typescript": "5.6.3" | ||
} | ||
} |
519
README.md
@@ -5,11 +5,11 @@ # Bunshine | ||
<img alt="Bunshine Logo" src="https://github.com/kensnyder/bunshine/raw/main/assets/bunshine-logo.png?v=2.0.0" width="200" height="187" /> | ||
<img alt="Bunshine Logo" src="https://github.com/kensnyder/bunshine/raw/main/assets/bunshine-logo.png?v=3.0.0-rc.1" width="200" height="187" /> | ||
[](https://npmjs.com/package/bunshine) | ||
[](https://github.com/search?q=repo:kensnyder/bunshine++language:TypeScript&type=code) | ||
 | ||
[](https://bundlephobia.com/package/bunshine@2.0.0) | ||
[](https://www.npmjs.com/package/bunshine?activeTab=dependencies) | ||
[](https://www.npmjs.com/package/bunshine) | ||
[](https://opensource.org/licenses/ISC) | ||
[](https://npmjs.com/package/bunshine) | ||
[](https://github.com/search?q=repo:kensnyder/bunshine++language:TypeScript&type=code) | ||
 | ||
[](https://bundlephobia.com/package/bunshine@3.0.0-rc.1) | ||
[](https://www.npmjs.com/package/bunshine?activeTab=dependencies) | ||
[](https://www.npmjs.com/package/bunshine) | ||
[](https://opensource.org/licenses/ISC) | ||
@@ -34,4 +34,4 @@ ## Installation | ||
7. Support async handlers | ||
8. Provide common middleware out of the box | ||
9. Built-in gzip compression | ||
8. Provide common middleware out of the box (cors, prodLogger, headers, compression, etags) | ||
9. Support traditional routing syntax | ||
10. Make specifically for Bun | ||
@@ -54,6 +54,9 @@ 11. Comprehensive unit tests | ||
- [serveFiles](#servefiles) | ||
- [responseCache](#responseCache) | ||
- [compression](#compression) | ||
- [cors](#cors) | ||
- [devLogger & prodLogger](#devlogger--prodlogger) | ||
- [headers](#headers) | ||
- [performanceHeader](#performanceheader) | ||
- [securityHeaders](#securityheaders) | ||
- [etags](#etags) | ||
11. [TypeScript pro-tips](#typescript-pro-tips) | ||
@@ -63,4 +66,14 @@ 12. [Roadmap](#roadmap) | ||
## Usage | ||
## Upgrading from 1.x to 2.x | ||
RegExp symbols are not allowed in route definitions to avoid ReDoS vulnerabilities. | ||
## Upgrading from 2.x to 3.x | ||
- The `securityHeaders` middleware has been dropped. Use a library such as | ||
[@side/fortifyjs](https://www.npmjs.com/package/@side/fortifyjs) instead. | ||
- The `serveFiles` middleware no longer accepts options for `etags` or `gzip`. | ||
Instead, compose the `etags` and `compression` middlewares: | ||
`app.headGet('/files/*', etags(), compression(), serveFiles(...))` | ||
## Basic example | ||
@@ -77,3 +90,3 @@ | ||
app.listen({ port: 3100 }); | ||
app.listen({ port: 3100, reusePort: true }); | ||
``` | ||
@@ -84,10 +97,11 @@ | ||
```ts | ||
import { HttpRouter, redirect } from 'bunshine'; | ||
import { HttpRouter, redirect, compression } from 'bunshine'; | ||
const app = new HttpRouter(); | ||
app.use(compresion()); | ||
app.patch('/users/:id', async c => { | ||
await authorize(c.request.headers.get('Authorization')); | ||
await authorize(c.request.headers.get('Authorization')); // see implementation below | ||
const data = await c.request.json(); | ||
const result = await updateUser(params.id, data); | ||
const result = await updateUser(params.id, data); // made-up function | ||
if (result === 'not found') { | ||
@@ -113,3 +127,3 @@ return c.json({ error: 'User not found' }, { status: 404 }); | ||
app.listen({ port: 3100 }); | ||
app.listen({ port: 3100, reusePort: true }); | ||
@@ -125,2 +139,22 @@ function authorize(authHeader: string) { | ||
You can also make a path-specific error catcher like this: | ||
```ts | ||
import { HttpRouter, redirect, compression } from 'bunshine'; | ||
const app = new HttpRouter(); | ||
app.get('/api/*', async (c, next) => { | ||
try { | ||
return await next(); | ||
} catch (e) { | ||
// do something with error | ||
// maybe return json | ||
} | ||
}); | ||
// attach other routes | ||
app.get('/api/v1/posts', handler); | ||
``` | ||
### What is `c` here? | ||
@@ -137,4 +171,4 @@ | ||
// Properties of the Context object | ||
c.request; // The raw request object | ||
c.url; // The URL object | ||
c.request; // The raw Request object | ||
c.url; // The URL object (get url string with c.url.href, or query with c.url.searchParams) | ||
c.params; // The request params from route placeholders | ||
@@ -145,3 +179,3 @@ c.server; // The Bun server instance (useful for pub-sub) | ||
c.error; // An error object available to handlers registered with app.on500() | ||
c.ip; // The IP address of the client (not necessarily the end user) | ||
c.ip; // The IP address of the client or load balancer (not necessarily the end user) | ||
c.date; // The date of the request | ||
@@ -151,3 +185,2 @@ c.now; // The result of performance.now() at the start of the request | ||
// Convenience methods for creating Response objects with various content types | ||
// Note that responses are automatically gzipped if the client accepts gzip | ||
c.json(data, init); | ||
@@ -179,3 +212,3 @@ c.text(text, init); | ||
app.listen({ port: 3100 }); | ||
app.listen({ port: 3100, reusePort: true }); | ||
``` | ||
@@ -197,2 +230,5 @@ | ||
// handler not affected by middleware defined below | ||
app.get('/healthcheck', c => c.text('200 OK')); | ||
// Run before each request | ||
@@ -234,2 +270,8 @@ app.use(c => { | ||
// Middleware before a given handler (as args) | ||
app.get('/users/:id', paramValidationMiddleware, async c => { | ||
const user = await getUser(c.params.id); | ||
return c.json(user); | ||
}); | ||
// Middleware before a given handler (as array) | ||
@@ -244,12 +286,6 @@ app.get('/users/:id', [ | ||
// Middleware before a given handler (as args) | ||
app.get('/users/:id', paramValidationMiddleware, async c => { | ||
const user = await getUser(c.params.id); | ||
return c.json(user); | ||
}); | ||
// handler affected by applicable middleware | ||
// handler affected by middleware defined above | ||
app.get('/', c => c.text('Hello World!')); | ||
app.listen({ port: 3100 }); | ||
app.listen({ port: 3100, reusePort: true }); | ||
``` | ||
@@ -263,6 +299,31 @@ | ||
app.get('/users/me', handler1); | ||
app.get('/users/:id', handler2); | ||
app.get('/users/:id', handler2); // runs only if id is not "me" or handler1 doesn't respond | ||
app.get('*', http404Handler); | ||
``` | ||
And to illustrate the wrap-like behavior of `await`ing the `next` function: | ||
```ts | ||
app.get('/', async (c, next) => { | ||
console.log(1); | ||
const resp = await next(); | ||
console.log(5); | ||
return resp; | ||
}); | ||
app.get('/', async (c, next) => { | ||
console.log(2); | ||
const resp = await next(); | ||
console.log(4); | ||
return resp; | ||
}); | ||
app.get('/', async (c, next) => { | ||
console.log(3); | ||
return c.text('Hello'); | ||
}); | ||
// logs 1, 2, 3, 4, then 5 | ||
// Same goes for a list of handlers: | ||
app.get('/', runs1stAnd5th, runs2ndAnd4th, runs3rd); | ||
``` | ||
### What does it mean that "every handler is treated like middleware"? | ||
@@ -372,3 +433,3 @@ | ||
// start the server | ||
app.listen({ port: 3100 }); | ||
app.listen({ port: 3100, reusePort: true }); | ||
``` | ||
@@ -428,3 +489,3 @@ | ||
// start the server | ||
app.listen({ port: 3100 }); | ||
app.listen({ port: 3100, reusePort: true }); | ||
@@ -483,10 +544,10 @@ // | ||
const msg = `${sc.data.username} has left the chat`; | ||
ws.publish(`chat-room-${sc.params.room}`, msg); | ||
ws.unsubscribe(`chat-room-${sc.params.room}`); | ||
sc.publish(`chat-room-${sc.params.room}`, msg); | ||
sc.unsubscribe(`chat-room-${sc.params.room}`); | ||
}, | ||
}); | ||
const server = app.listen({ port: 3100 }); | ||
const server = app.listen({ port: 3100, reusePort: true }); | ||
// at a later time, you can also publish a message from another source | ||
// at a later time, you can also publish a message from another part of your code | ||
server.publish(channel, message); | ||
@@ -517,3 +578,3 @@ ``` | ||
// start the server | ||
app.listen({ port: 3100 }); | ||
app.listen({ port: 3100, reusePort: true }); | ||
@@ -559,3 +620,3 @@ // | ||
// start the server | ||
app.listen({ port: 3100 }); | ||
app.listen({ port: 3100, reusePort: true }); | ||
@@ -605,3 +666,3 @@ // | ||
[RegExp Denial of Service vulnerability](https://security.snyk.io/vuln/SNYK-JS-PATHTOREGEXP-7925106), | ||
Bunshine no longer uses | ||
Bunshine v2+ no longer uses | ||
[path-to-regexp docs](https://www.npmjs.com/package/path-to-regexp). | ||
@@ -617,3 +678,2 @@ | ||
- Static paths (e.g. `/posts`) | ||
- Custom Regular Expression (e.g. `/^\/author\/([a-z]+)$/i`) | ||
@@ -643,7 +703,7 @@ Support for other behaviors can lead to a Regular Expression Denial of service | ||
For example, the dot in `/assets/*.js` will not match all characters--only dots.™™ | ||
For example, the dot in `/assets/*.js` will not match all characters--only dots. | ||
### Not supported | ||
Support for regex-like syntax has been dropped in v2 due to a | ||
Support for regex-like syntax has been dropped in v2 due to the aforementioned | ||
[RegExp Denial of Service vulnerability](https://security.snyk.io/vuln/SNYK-JS-PATHTOREGEXP-7925106). | ||
@@ -662,4 +722,16 @@ For cases where you need to limit by character or specify optional segments, | ||
### Caching | ||
If you want to double check all your routes, you can use code like the following: | ||
```ts | ||
import { HttpRouter } from 'bunshine'; | ||
import { isSafe } from 'redos-detector'; | ||
const app = new HttpRouter(); | ||
app.get('/', home); | ||
// ... all my routes | ||
// detectPotentialDos() calls console.warn with() details of each unsafe pattern | ||
app.matcher.detectPotentialDos(isSafe); | ||
``` | ||
### HTTP methods | ||
@@ -690,3 +762,3 @@ | ||
app.listen({ port: 3100 }); | ||
app.listen({ port: 3100, reusePort: true }); | ||
``` | ||
@@ -709,3 +781,3 @@ | ||
app.listen({ port: 3100 }); | ||
app.listen({ port: 3100, reusePort: true }); | ||
``` | ||
@@ -724,3 +796,3 @@ | ||
app.listen({ port: 3100 }); | ||
app.listen({ port: 3100, reusePort: true }); | ||
``` | ||
@@ -743,3 +815,3 @@ | ||
app.listen({ port: 3100 }); | ||
app.listen({ port: 3100, reusePort: true }); | ||
``` | ||
@@ -762,3 +834,3 @@ | ||
app.listen({ port: 3100 }); | ||
app.listen({ port: 3100, reusePort: true }); | ||
``` | ||
@@ -772,3 +844,2 @@ | ||
| dotfiles | `"ignore"` | How to handle dotfiles; allow=>serve normally, deny=>return 403, ignore=>run next handler | | ||
| etag | N/A | Not yet implemented | | ||
| extensions | `[]` | If given, a list of file extensions to allow | | ||
@@ -783,2 +854,52 @@ | fallthrough | `true` | If false, issue a 404 when a file is not found, otherwise proceed to next handler | | ||
### responseCache | ||
Simple caching can be accomplished with the `responseCache()` middleware. It | ||
saves responses to a cache you supply, based on URL. This can be useful for | ||
builds, where your assets aren't changing. In the example below, `lru-cache` is | ||
used to store assets in memory. Any cache that implements `has(url: string)`, | ||
`get(url: string)` and `set(url: string, resp: Response)` methods can be used. | ||
Your cache can also serialize responses to save them to an external system. | ||
Keep in mind that your `set()` function will receive a `Response` object and | ||
your `get()` function should be an object with a `clone()` method that returns | ||
a `Response` object. | ||
```ts | ||
import { LRUCache } from 'lru-cache'; | ||
import { HttpRouter, responseCache, serveFiles } from 'bunshine'; | ||
const app = new HttpRouter(); | ||
app.headGet( | ||
'/public/*', | ||
responseCache(new LRUCache({ max: 100 })), | ||
serveFiles(`${import.meta.dir}/build/public`) | ||
); | ||
``` | ||
### compression | ||
To add Gzip compression: | ||
```ts | ||
import { HttpRouter, compression, serveFiles } from 'bunshine'; | ||
const app = new HttpRouter(); | ||
app.get('/public/*', compression(), serveFiles(`${import.meta.dir}/public`)); | ||
app.listen({ port: 3100, reusePort: true }); | ||
``` | ||
The compression middleware takes an object with options: | ||
```ts | ||
type CompressionOptions = { | ||
prefer: 'br' | 'gzip' | 'none'; // default gzip | ||
br: BrotliOptions; // default from node:zlib | ||
gzip: ZlibCompressionOptions; // default from node:zlib | ||
minSize: number; // files smaller than this will not be compressed | ||
maxSize: number; // files larger than this will not be compressed | ||
}; | ||
``` | ||
### cors | ||
@@ -800,3 +921,3 @@ | ||
app.use(cors({ origin: ['https://example.com', /https:\/\/stuff.[a-z]+/i] })); | ||
app.use(cors({ origin: incomingOrigin => incomingOrigin })); | ||
app.use(cors({ origin: incomingOrigin => incomingOrigin })); // This may be preferred to * | ||
app.use(cors({ origin: incomingOrigin => getAllowedOrigins(incomingOrigin) })); | ||
@@ -822,3 +943,3 @@ | ||
app.listen({ port: 3100 }); | ||
app.listen({ port: 3100, reusePort: true }); | ||
``` | ||
@@ -833,4 +954,30 @@ | ||
_maxAge_: the number of seconds clients should cache the CORS headers | ||
_credentials_: whether to allow credentials (e.g. cookies or auth headers) | ||
_credentials_: whether to allow clients to send credentials (e.g. cookies or auth headers) | ||
### headers | ||
The `headers` middleware adds headers to outgoing responses. | ||
```ts | ||
import { HttpRouter, headers } from '../index'; | ||
const app = new HttpRouter(); | ||
const htmlSecurityHeaders = headers({ | ||
'Content-Security-Policy': `default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;`, | ||
'Referrer-Policy': 'strict-origin', | ||
'Permissions-Policy': | ||
'accelerometer=(), ambient-light-sensor=(), autoplay=(*), battery=(), camera=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), gamepad=(), geolocation=(), gyroscope=(), hid=(), idle-detection=(), local-fonts=(), magnetometer=(), midi=(), payment=(), picture-in-picture=(), publickey-credentials-create=(), publickey-credentials-get=(), screen-wake-lock=(), serial=(), usb=(), web-share=(self)', | ||
}); | ||
app.get('/login', htmlSecurityHeaders, loginHandler); | ||
app.get('/cms/*', htmlSecurityHeaders); | ||
const neverCache = headers({ | ||
'Cache-control': 'no-store, must-revalidate', | ||
Expires: '0', | ||
}); | ||
app.get('/api/*', neverCache); | ||
``` | ||
### devLogger & prodLogger | ||
@@ -860,6 +1007,6 @@ | ||
"pathname": "/", | ||
"runtime": "Bun v1.1.4", | ||
"poweredBy": "Bunshine v2.0.0", | ||
"runtime": "Bun v1.1.33", | ||
"poweredBy": "Bunshine v3.0.0-rc.1", | ||
"machine": "server1", | ||
"userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36", | ||
"userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0-rc.1.0 Safari/537.36", | ||
"pid": 123 | ||
@@ -880,6 +1027,6 @@ } | ||
"pathname": "/", | ||
"runtime": "Bun v1.1.4", | ||
"poweredBy": "Bunshine v2.0.0", | ||
"runtime": "Bun v1.1.3", | ||
"poweredBy": "Bunshine v3.0.0-rc.1", | ||
"machine": "server1", | ||
"userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36", | ||
"userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0-rc.1.0 Safari/537.36", | ||
"pid": 123, | ||
@@ -890,2 +1037,4 @@ "took": 5 | ||
Note that `id` correlates between a request and its response. | ||
To use these loggers, simply attach them as middleware. | ||
@@ -903,3 +1052,3 @@ | ||
app.listen({ port: 3100 }); | ||
app.listen({ port: 3100, reusePort: true }); | ||
``` | ||
@@ -921,94 +1070,18 @@ | ||
app.listen({ port: 3100 }); | ||
app.listen({ port: 3100, reusePort: true }); | ||
``` | ||
### securityHeaders | ||
### etags | ||
You can add security-related headers to responses with the `securityHeaders` | ||
middleware. For more information about security headers, checkout these | ||
resources: | ||
You can add etag headers and respond to `If-None-Match` headers. | ||
- [securityheaders.com](https://securityheaders.com) | ||
- [MDN Security on the Web](https://developer.mozilla.org/en-US/docs/Web/Security) | ||
- [MDN Content-Security-Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy) | ||
```ts | ||
import { HttpRouter, securityHeaders } from 'bunshine'; | ||
import { HttpRouter, etags } from 'bunshine'; | ||
const app = new HttpRouter(); | ||
app.use(securityHeaders()); | ||
// The following are defaults that you can override | ||
app.use( | ||
securityHeaders({ | ||
contentSecurityPolicy: { | ||
frameSrc: ["'self'"], | ||
workerSrc: ["'self'"], | ||
connectSrc: ["'self'"], | ||
defaultSrc: ["'self'"], | ||
fontSrc: ['*'], | ||
imgSrc: ['*'], | ||
manifestSrc: ["'self'"], | ||
mediaSrc: ["'self' data:"], | ||
objectSrc: ["'self' data:"], | ||
prefetchSrc: ["'self'"], | ||
scriptSrc: ["'self'"], | ||
scriptSrcElem: ["'self' 'unsafe-inline'"], | ||
scriptSrcAttr: ["'none'"], | ||
styleSrcAttr: ["'self' 'unsafe-inline'"], | ||
baseUri: ["'self'"], | ||
formAction: ["'self'"], | ||
frameAncestors: ["'self'"], | ||
sandbox: {}, | ||
}, | ||
crossOriginEmbedderPolicy: 'unsafe-none', | ||
crossOriginOpenerPolicy: 'same-origin', | ||
crossOriginResourcePolicy: 'same-origin', | ||
permissionsPolicy: { | ||
// only include special APIs that you use | ||
accelerometer: [], | ||
ambientLightSensor: [], | ||
autoplay: ['self'], | ||
battery: [], | ||
camera: [], | ||
displayCapture: [], | ||
documentDomain: [], | ||
encryptedMedia: [], | ||
executionWhileNotRendered: [], | ||
executionWhileOutOfViewport: [], | ||
fullscreen: [], | ||
gamepad: [], | ||
geolocation: [], | ||
gyroscope: [], | ||
hid: [], | ||
identityCredentialsGet: [], | ||
idleDetection: [], | ||
localFonts: [], | ||
magnetometer: [], | ||
midi: [], | ||
otpCredentials: [], | ||
payment: [], | ||
pictureInPicture: [], | ||
publickeyCredentialsCreate: [], | ||
publickeyCredentialsGet: [], | ||
screenWakeLock: [], | ||
serial: [], | ||
speakerSelection: [], | ||
storageAccess: [], | ||
usb: [], | ||
webShare: ['self'], | ||
windowManagement: [], | ||
xrSpacialTracking: [], | ||
}, | ||
referrerPolicy: 'strict-origin', | ||
server: false, | ||
strictTransportSecurity: 'max-age=86400; includeSubDomains; preload', | ||
xContentTypeOptions: 'nosniff', | ||
xFrameOptions: 'SAMEORIGIN', | ||
xPoweredBy: false, | ||
xXssProtection: '1; mode=block', | ||
}) | ||
); | ||
app.use(etags()); | ||
app.get('/resource1', c => c.text(someBigThing)); | ||
app.listen({ port: 3100 }); | ||
app.listen({ port: 3100, reusePort: true }); | ||
``` | ||
@@ -1038,3 +1111,3 @@ | ||
app.listen({ port: 3100 }); | ||
app.listen({ port: 3100, reusePort: true }); | ||
``` | ||
@@ -1064,3 +1137,3 @@ | ||
const cookies = req.headers.get('cookie'); | ||
const user = getUserFromCookies(cookies); | ||
const user: User = getUserFromCookies(cookies); | ||
// here user is typed as User | ||
@@ -1084,5 +1157,136 @@ return { user }; | ||
// start the server | ||
app.listen({ port: 3100 }); | ||
app.listen({ port: 3100, reusePort: true }); | ||
``` | ||
### Typing middleware | ||
```ts | ||
function myMiddleware(options: Options): Middleware { | ||
return (c, next) => { | ||
// TypeScript infers c and next because of Middleware | ||
}; | ||
} | ||
``` | ||
## Examples of common http server tasks | ||
```ts | ||
import { HttpRouter } from 'bunshine'; | ||
import { Middleware } from './HttpRouter'; | ||
const app = new HttpRouter(); | ||
// how to read query params | ||
app.get('/', c => { | ||
c.url.searchParams; // URLSearchParams object | ||
Object.fromEntries(c.url.searchParams); // as plain object (but repeated keys are dropped) | ||
for (const [key, value] of c.url.searchParams) { | ||
} // iterate params | ||
}); | ||
// create small functions that always return the same thing | ||
const respondWith404 = c => c.text('Not found', { status: 404 }); | ||
// block dotfile access (e.g. .env, .git, .svn, .htaccess) | ||
app.get(/^\./, respondWith404); | ||
// block URLs that end with .env and other dumb endings | ||
app.all(/\.(env|bak|old|tmp|backup|log|ini|conf)$/, respondWith404); | ||
// block WordPress URLs such as /wordpress/wp-includes/wlwmanifest.xml | ||
app.all(/(^wordpress\/|\/wp-includes\/)/, respondWith404); | ||
// block Other language URLs such as /phpinfo.php and /admin.cgi | ||
app.all(/^[^/]+\.(php|cgi)$/, respondWith404); | ||
// block Commonly probed application paths | ||
app.all(/^(phpmyadmin|mysql|cgi-bin|cpanel|plesk)/i, respondWith404); | ||
// middleware to add CSP | ||
app.use(async (c, next) => { | ||
const resp = await next(); | ||
if ( | ||
resp.headers.get('content-type')?.includes('text/html') && | ||
!resp.headers.has('Content-Security-Headers') | ||
) { | ||
resp.headers.set( | ||
'Content-Security-Headers', | ||
"frame-src 'self'; frame-ancestors 'self'; worker-src 'self'; connect-src 'self'; default-src 'self'; font-src *; img-src *; manifest-src 'self'; media-src 'self' data:; object-src 'self' data:; prefetch-src 'self'; script-src 'self'; script-src-elem 'self' 'unsafe-inline'; script-src-attr 'none'; style-src-attr 'self' 'unsafe-inline'; base-uri 'self'; form-action 'self'" | ||
); | ||
} | ||
return resp; | ||
}); | ||
// Later modify CSP at a certain route | ||
app.headGet('/embeds/*', async (c, next) => { | ||
const resp = await next(); | ||
const csp = response.headers.get('Content-Security-Headers'); | ||
if (csp) { | ||
resp.headers.set( | ||
'Content-Security-Headers', | ||
csp.replace(/frame-ancestors .+?;/, 'frame-ancestors *;') | ||
); | ||
} | ||
return resp; | ||
}); | ||
// Persist data in c.locals | ||
app.get('/api/*', async (c, next) => { | ||
const authValue = c.request.headers.get('Authorization'); | ||
c.locals.auth = { | ||
identity: await getUser(authValue), | ||
permission: await getPermissions(authValue), | ||
}; | ||
// return nothing so that subsequent handlers get called | ||
}); | ||
// Middleware to cast incoming json payload to zod schema | ||
function castSchema(zodSchema: ZodObject): Middleware { | ||
return async c => { | ||
const result = zodSchema.safeParse(await c.json()); | ||
if (result.error) { | ||
return c.json(result.error, { status: 400 }); | ||
} | ||
c.locals.safePayload = result.data; | ||
}; | ||
} | ||
app.post('/api/users', castSchema(userCreateSchema), createUser); | ||
// Destructure context object | ||
app.get('/api/*', async ({ url, request, json }) => { | ||
// do stuff with url and request | ||
return text('my json response'); | ||
}); | ||
``` | ||
## Decisions | ||
The following decisions are based on scripts in /benchmarks: | ||
- bound-functions.ts - The Context object created for each request has its | ||
methods automatically bound to the instance. It is convenient for developers | ||
and adds only a tiny overhead. | ||
- inner-functions.ts - The Context is a class, not a set of functions in an | ||
enclosure which saves about 3% of time. | ||
- compression.ts - gzip is the default preferred format for the compression | ||
middleware. Deflate provides no advantage, and Brotli provides 2-8% additional | ||
size savings at the cost of 7-10x as much CPU time as gzip. Brotli takes on | ||
the order of 100ms to compress 100kb of html, compared to sub-milliseconds | ||
for gzip. | ||
- etags - etag calculation is very fast. On the order of tens of microseconds | ||
for 100kb of html. | ||
- lru-matcher.ts - The default LRU cache size used for the router is 4000. | ||
Cache sizes of 4000+ are all about 1.4x faster than no cache. | ||
- response-reencoding.ts - Both the etags middleware and compression middleware | ||
convert the response body to an ArrayBuffer, process it, then create a new | ||
Response object. The decode/reencode process takes only 10s of microseconds. | ||
- TextEncoder-reuse.ts - The Context object's response factories (c.json(), | ||
c.html(), etc.) reuse a single TextEncoder object. That gains about 18% which | ||
turns out to be only on the order of 10s of nanoseconds. | ||
- timer-resolution.ts - performance.now() is faster than Date.now() even though | ||
it provides additional precision. The performanceHeader uses performance.now() | ||
when it sets the X-Took header, which is rounded to 3 decimal places. | ||
Some additional design decisions: | ||
- I decided to use LRUCache and a custom router. I looked into trie routers and | ||
compile RegExp routers, but they didn't easily support the concept of matching | ||
multiple handlers and running each one in order of registration. Bunshine v1 | ||
did use `path-to-regexp`, but that recently stopped supporting `*` in route | ||
registration. | ||
## Roadmap | ||
@@ -1094,21 +1298,17 @@ | ||
- ✅ examples/server.ts | ||
- ✅ middleware > serveFiles | ||
- ✅ middleware > compression | ||
- ✅ middleware > cors | ||
- ✅ middleware > devLogger | ||
- ✅ middleware > etags | ||
- ✅ middleware > headers | ||
- ✅ middleware > performanceHeader | ||
- ✅ middleware > prodLogger | ||
- ✅ middleware > performanceHeader | ||
- ✅ middleware > securityHeaders | ||
- ✅ middleware > responseCache | ||
- ✅ middleware > serveFiles | ||
- ✅ middleware > trailingSlashes | ||
- 🔲 middleware > html rewriter | ||
- 🔲 middleware > hmr | ||
- 🔲 middleware > directoryListing | ||
- 🔲 middleware > rate limiter | ||
- 🔲 document headers middleware | ||
- 🔲 move some middleware to `@bunshine/\*`? | ||
- ✅ gzip compression | ||
- ✅ document the headers middleware | ||
- ✅ options for serveFiles | ||
- 🔲 tests for cors | ||
- ✅ tests for cors | ||
- 🔲 tests for devLogger | ||
- 🔲 tests for prodLogger | ||
- 🔲 tests for gzip | ||
- 🔲 tests for responseFactories | ||
@@ -1121,6 +1321,3 @@ - ✅ tests for serveFiles | ||
- 🔲 GitHub Actions to run tests and coverage | ||
- 🔲 Support server clusters | ||
- ✅ Replace "ms" with a small and simple implementation | ||
- ✅ Export functions to gzip strings and files | ||
- ✅ Gzip performance testing (to get min/max defaults) | ||
@@ -1127,0 +1324,0 @@ ## License |
@@ -61,34 +61,34 @@ import type { BunFile, Server } from 'bun'; | ||
/** A shorthand for `new Response(text, { headers: { 'Content-type': 'text/plain' } })` */ | ||
text(text: string, init: ResponseInit = {}) { | ||
text = (text: string, init: ResponseInit = {}) => { | ||
return textPlain.call(this, text, init); | ||
} | ||
}; | ||
/** A shorthand for `new Response(js, { headers: { 'Content-type': 'text/javascript' } })` */ | ||
js(js: string, init: ResponseInit = {}) { | ||
js = (js: string, init: ResponseInit = {}) => { | ||
return textJs.call(this, js, init); | ||
} | ||
}; | ||
/** A shorthand for `new Response(html, { headers: { 'Content-type': 'text/html' } })` */ | ||
html(html: string, init: ResponseInit = {}) { | ||
html = (html: string, init: ResponseInit = {}) => { | ||
return textHtml.call(this, html, init); | ||
} | ||
}; | ||
/** A shorthand for `new Response(html, { headers: { 'Content-type': 'text/css' } })` */ | ||
css(css: string, init: ResponseInit = {}) { | ||
css = (css: string, init: ResponseInit = {}) => { | ||
return textCss.call(this, css, init); | ||
} | ||
}; | ||
/** A shorthand for `new Response(xml, { headers: { 'Content-type': 'text/xml' } })` */ | ||
xml(xml: string, init: ResponseInit = {}) { | ||
xml = (xml: string, init: ResponseInit = {}) => { | ||
return textXml.call(this, xml, init); | ||
} | ||
}; | ||
/** A shorthand for `new Response(JSON.stringify(data), { headers: { 'Content-type': 'application/json' } })` */ | ||
json(data: any, init: ResponseInit = {}) { | ||
json = (data: any, init: ResponseInit = {}) => { | ||
return json.call(this, data, init); | ||
} | ||
}; | ||
/** A shorthand for `new Response(null, { headers: { Location: url }, status: 301 })` */ | ||
redirect(url: string, status = 302) { | ||
redirect = (url: string, status = 302) => { | ||
return redirect(url, status); | ||
} | ||
}; | ||
/** A shorthand for `new Response(bunFile, fileHeaders)` */ | ||
async file( | ||
file = async ( | ||
filenameOrBunFile: string | BunFile, | ||
fileOptions: FileResponseOptions = {} | ||
) { | ||
) => { | ||
return file(filenameOrBunFile, { | ||
@@ -98,7 +98,7 @@ range: this.request.headers.get('Range') || undefined, | ||
}); | ||
} | ||
}; | ||
/** A shorthand for `new Response({ headers: { 'Content-type': 'text/event-stream' } })` */ | ||
sse(setup: SseSetupFunction, init: ResponseInit = {}) { | ||
sse = (setup: SseSetupFunction, init: ResponseInit = {}) => { | ||
return sse(this.request.signal, setup, init); | ||
} | ||
}; | ||
} |
import type { ServeOptions, Server } from 'bun'; | ||
import os from 'os'; | ||
import bunshine from '../../package.json'; | ||
import os from 'node:os'; | ||
import bunshine from '../../package.json' assert { type: 'json' }; | ||
import Context, { type ContextWithError } from '../Context/Context'; | ||
import MatcherWithCache from '../MatcherWithCache/MatcherWithCache.ts'; | ||
import SocketRouter from '../SocketRouter/SocketRouter.ts'; | ||
import MatcherWithCache from '../MatcherWithCache/MatcherWithCache'; | ||
import SocketRouter from '../SocketRouter/SocketRouter'; | ||
import { fallback404 } from './fallback404'; | ||
@@ -8,0 +8,0 @@ import { fallback500 } from './fallback500'; |
import { BunFile } from 'bun'; | ||
import path from 'node:path'; | ||
import Context from '../Context/Context.ts'; | ||
import getMimeType from '../getMimeType/getMimeType.ts'; | ||
import { gzipString } from '../gzip/gzip.ts'; | ||
import Context from '../Context/Context'; | ||
import getMimeType from '../getMimeType/getMimeType'; | ||
@@ -11,7 +10,2 @@ export type Factory = (body: string, init?: ResponseInit) => Response; | ||
// body must be large enough to be worth compressing | ||
// (54 is minimum size of gzip after metadata; 100 is arbitrary choice) | ||
// see benchmarks/gzip.ts for more information | ||
export let minGzipSize = 100; | ||
export function json(this: Context, data: any, init: ResponseInit = {}) { | ||
@@ -21,12 +15,4 @@ let body: string | Uint8Array = JSON.stringify(data); | ||
if (!init.headers.has('Content-Type')) { | ||
init.headers.set('Content-type', `application/json; charset=utf-8`); | ||
init.headers.set('Content-Type', `application/json; charset=utf-8`); | ||
} | ||
if (!init.headers.has('Content-Encoding')) { | ||
// body must be large enough to be worth compressing | ||
if (body.length >= minGzipSize) { | ||
body = gzipString(body); | ||
init.headers.set('Content-Encoding', 'gzip'); | ||
init.headers.set('Content-Length', String(body.length)); | ||
} | ||
} | ||
return new Response(body, init); | ||
@@ -39,16 +25,4 @@ } | ||
if (!init.headers.has('Content-Type')) { | ||
init.headers.set('Content-type', `${contentType}; charset=utf-8`); | ||
init.headers.set('Content-Type', `${contentType}; charset=utf-8`); | ||
} | ||
if (!init.headers.has('Content-Encoding')) { | ||
if ( | ||
// client must expect gzip | ||
this.request.headers.get('Accept-Encoding')?.includes('gzip') && | ||
// body must be large enough to be worth compressing | ||
body.length >= minGzipSize | ||
) { | ||
// @ts-expect-error | ||
body = gzipString(body); | ||
init.headers.set('Content-Encoding', 'gzip'); | ||
} | ||
} | ||
init.headers.set('Content-Length', String(body.length)); | ||
@@ -71,3 +45,2 @@ return new Response(body, init); | ||
chunkSize?: number; | ||
gzip?: boolean; | ||
disposition?: 'inline' | 'attachment'; | ||
@@ -93,3 +66,2 @@ acceptRanges?: boolean; | ||
method: 'GET', | ||
gzip: fileOptions.gzip, | ||
}); | ||
@@ -214,3 +186,2 @@ if (fileOptions.acceptRanges !== false) { | ||
method, | ||
gzip, | ||
}: { | ||
@@ -222,3 +193,2 @@ file: BunFile; | ||
method: string; | ||
gzip?: boolean; | ||
}) { | ||
@@ -225,0 +195,0 @@ let response: Response; |
import { LRUCache } from 'lru-cache'; | ||
import RouteMatcher from '../RouteMatcher/RouteMatcher.ts'; | ||
import RouteMatcher from '../RouteMatcher/RouteMatcher'; | ||
@@ -8,3 +8,3 @@ export default class MatcherWithCache< | ||
cache: LRUCache<string, any>; | ||
constructor(size: number = 5000) { | ||
constructor(size: number = 4000) { | ||
super(); | ||
@@ -11,0 +11,0 @@ this.cache = new LRUCache<string, any>({ max: size }); |
@@ -21,3 +21,3 @@ import type Context from '../../Context/Context'; | ||
export const CorsDefaults = { | ||
export const corsDefaults = { | ||
origin: '*', | ||
@@ -31,3 +31,3 @@ allowMethods: ['GET', 'HEAD', 'PUT', 'POST', 'PATCH', 'DELETE'], | ||
const opts = { | ||
...CorsDefaults, | ||
...corsDefaults, | ||
...options, | ||
@@ -34,0 +34,0 @@ }; |
@@ -1,2 +0,2 @@ | ||
import type { Middleware } from '../../HttpRouter/HttpRouter.ts'; | ||
import type { Middleware } from '../../HttpRouter/HttpRouter'; | ||
@@ -3,0 +3,0 @@ export function devLogger(): Middleware { |
@@ -1,3 +0,3 @@ | ||
import type Context from '../../Context/Context.ts'; | ||
import type { Middleware, NextFunction } from '../../HttpRouter/HttpRouter.ts'; | ||
import type Context from '../../Context/Context'; | ||
import type { Middleware, NextFunction } from '../../HttpRouter/HttpRouter'; | ||
@@ -7,3 +7,3 @@ export type HeaderValue = | ||
| ((c: Context, resp: Response) => string | null | Promise<string | null>); | ||
export type HeadersInit = Record<string, HeaderValue>; | ||
export type HeaderValues = Record<string, HeaderValue>; | ||
export type HeaderCondition = ( | ||
@@ -15,3 +15,3 @@ c: Context, | ||
export function headers( | ||
headers: HeadersInit, | ||
headers: HeaderValues, | ||
condition?: HeaderCondition | ||
@@ -18,0 +18,0 @@ ): Middleware { |
import os from 'os'; | ||
// @ts-ignore | ||
import bunshine from '../../../package.json'; | ||
import type { Middleware } from '../../HttpRouter/HttpRouter.ts'; | ||
import bunshinePkg from '../../../package.json' assert { type: 'json' }; | ||
import type { Middleware } from '../../HttpRouter/HttpRouter'; | ||
@@ -10,3 +9,3 @@ const machine = os.hostname(); | ||
: `Node v${process.versions.node}`; | ||
const poweredBy = `Bunshine v${bunshine.version}`; | ||
const poweredBy = `Bunshine v${bunshinePkg.version}`; | ||
@@ -13,0 +12,0 @@ export function prodLogger(): Middleware { |
@@ -1,14 +0,11 @@ | ||
import { ZlibCompressionOptions } from 'bun'; | ||
import path from 'path'; | ||
import type { Middleware } from '../../HttpRouter/HttpRouter.ts'; | ||
import { buildFileResponse } from '../../HttpRouter/responseFactories.ts'; | ||
import { FileGzipper } from '../../gzip/FileGzipper.ts'; | ||
import ms from '../../ms/ms.ts'; | ||
import type { Middleware } from '../../HttpRouter/HttpRouter'; | ||
import { buildFileResponse } from '../../HttpRouter/responseFactories'; | ||
import ms from '../../ms/ms'; | ||
// see https://expressjs.com/en/4x/api.html#express.static | ||
// and https://www.npmjs.com/package/send#dotfiles | ||
export type StaticOptions = { | ||
export type ServeFilesOptions = { | ||
acceptRanges?: boolean; | ||
dotfiles?: 'allow' | 'deny' | 'ignore'; | ||
etag?: boolean; | ||
extensions?: string[]; | ||
@@ -20,34 +17,4 @@ fallthrough?: boolean; | ||
maxAge?: number | string; | ||
gzip?: GzipOptions; | ||
}; | ||
export type GzipOptions = { | ||
minFileSize?: number; | ||
maxFileSize?: number; | ||
mimeTypes?: Array<string | RegExp>; | ||
zlibOptions?: ZlibCompressionOptions; | ||
cache: | ||
| false | ||
| { | ||
type: 'file' | 'precompress' | 'memory' | 'never'; | ||
maxBytes?: number; | ||
path?: string; | ||
}; | ||
}; | ||
const defaultGzipOptions: GzipOptions = { | ||
minFileSize: 1024, | ||
maxFileSize: 1024 * 1024 * 25, | ||
mimeTypes: [ | ||
/^text\/.*/, | ||
/^application\/json/, | ||
/^image\/svg/, | ||
/^font\/(otf|ttf|eot)/, | ||
], | ||
zlibOptions: {}, | ||
cache: { | ||
type: 'never', | ||
}, | ||
}; | ||
export function serveFiles( | ||
@@ -58,4 +25,2 @@ directory: string, | ||
dotfiles = 'ignore', | ||
// etag is Not yet implemented | ||
etag = true, | ||
extensions = [], | ||
@@ -67,15 +32,7 @@ fallthrough = true, | ||
maxAge = undefined, | ||
gzip = undefined, | ||
}: StaticOptions = {} | ||
}: ServeFilesOptions = {} | ||
): Middleware { | ||
const cacheControlHeader = | ||
maxAge === undefined ? null : getCacheControl(maxAge, immutable); | ||
const gzipper = gzip | ||
? new FileGzipper(directory, { ...defaultGzipOptions, ...gzip }) | ||
: undefined; | ||
return async c => { | ||
if (gzipper) { | ||
// wait for setup cache if not done already | ||
await gzipper.setupPromise; | ||
} | ||
const filename = c.params[0] || c.url.pathname; | ||
@@ -124,18 +81,9 @@ if (filename.startsWith('.')) { | ||
const rangeHeader = c.request.headers.get('range'); | ||
let response: Response; | ||
if (rangeHeader || !gzipper) { | ||
// get base response | ||
response = await buildFileResponse({ | ||
file, | ||
acceptRanges, | ||
chunkSize: 0, | ||
rangeHeader, | ||
method: c.request.method, | ||
gzip: false, | ||
}); | ||
} else { | ||
response = await gzipper.fetch(file); | ||
} | ||
// add current date | ||
response.headers.set('Date', new Date().toUTCString()); | ||
const response = await buildFileResponse({ | ||
file, | ||
acceptRanges, | ||
chunkSize: 0, | ||
rangeHeader, | ||
method: c.request.method, | ||
}); | ||
// add last modified | ||
@@ -142,0 +90,0 @@ if (lastModified) { |
@@ -63,3 +63,3 @@ type Registration<T> = { | ||
pattern, | ||
regex: /\/(.+)/, | ||
regex: /^\/(.+)$/, | ||
matcher: subject => ({ '0': subject.slice(1) }), | ||
@@ -73,3 +73,3 @@ target, | ||
pattern, | ||
regex: /(.+)/, | ||
regex: /^(.+)$/, | ||
matcher: subject => ({ '0': subject }), | ||
@@ -138,4 +138,4 @@ target, | ||
if (detector(reg.regex, config).safe === false) { | ||
throw new Error( | ||
`Potential ReDoS detected for pattern ${reg.pattern}. Consider using a ` | ||
console.warn( | ||
`Bunshine: Potential ReDoS detected for pattern "${reg.pattern}" => ${reg.regex.source}` | ||
); | ||
@@ -142,0 +142,0 @@ } |
import { Server, ServerWebSocket, ServerWebSocketSendStatus } from 'bun'; | ||
import { WsDataShape } from './SocketRouter.ts'; | ||
import { WsDataShape } from './SocketRouter'; | ||
@@ -4,0 +4,0 @@ const isBufferSource = (function () { |
import type { ServerWebSocket } from 'bun'; | ||
import { RequireAtLeastOne } from 'type-fest'; | ||
import Context from '../Context/Context.ts'; | ||
import Context from '../Context/Context'; | ||
import HttpRouter, { NextFunction } from '../HttpRouter/HttpRouter'; | ||
import RouteMatcher from '../RouteMatcher/RouteMatcher'; | ||
import SocketContext, { SocketMessage } from './SocketContext.ts'; | ||
import SocketContext, { SocketMessage } from './SocketContext'; | ||
// U = UpgradeShape | ||
// P = ParamsShape | ||
// T = Type i.e. EventName | ||
// T = Type i.e. SocketEventType | ||
@@ -23,3 +23,3 @@ export type WsDataShape<U = any, P = Record<string, any>> = { | ||
export type SocketMessageHandler<U, P, T extends SocketEventName> = ( | ||
export type SocketMessageHandler<U, P, T extends SocketEventType> = ( | ||
context: SocketContext<U, P>, | ||
@@ -67,3 +67,3 @@ message: SocketMessage<T> | ||
export type SocketEventName = | ||
export type SocketEventType = | ||
| 'open' | ||
@@ -105,3 +105,2 @@ | 'message' | ||
this.routeMatcher.add('ALL', path, handlers); | ||
// console.log('ws handlers registered!', path); | ||
// create a router path that upgrades to a socket | ||
@@ -141,3 +140,3 @@ this.httpRouter.get<P>(path, async (c, next) => { | ||
}; | ||
private _createHandler = (eventName: SocketEventName) => { | ||
private _createHandler = (eventName: SocketEventType) => { | ||
return async (ws: ServerWebSocket<WsDataShape>, ...args: any) => { | ||
@@ -144,0 +143,0 @@ const sc = ws.data.sc as SocketContext; |
{ | ||
"include": ["index.ts", "package.json", "src/**/*", "examples", "benchmarks", "bin/**/*"], | ||
"exclude": [ | ||
"**/*.spec.ts" | ||
], | ||
"files": ["index.ts"], | ||
"exclude": ["**/*.spec.ts"], | ||
"compilerOptions": { | ||
"lib": ["ESNext", "dom", "dom.iterable"], | ||
"module": "esnext", | ||
"target": "esnext", | ||
"moduleResolution": "bundler", | ||
"moduleDetection": "force", | ||
"lib": ["DOM", "DOM.Iterable", "ESNext"], | ||
"module": "ESNext", | ||
"target": "ESNext", | ||
"outDir": "./dist", | ||
"declaration": true, | ||
"removeComments": false, | ||
"noEmit": true, | ||
"pretty": true, | ||
"isolatedModules": true, | ||
"sourceMap": false, | ||
"skipLibCheck": true, | ||
"moduleResolution": "Bundler", | ||
"allowSyntheticDefaultImports": true, | ||
"esModuleInterop": true, | ||
"allowImportingTsExtensions": true, | ||
"composite": true, | ||
"strict": true, | ||
"noEmit": true, | ||
"downlevelIteration": true, | ||
"skipLibCheck": true, | ||
"noImplicitAny": false, | ||
"allowSyntheticDefaultImports": true, | ||
"forceConsistentCasingInFileNames": true, | ||
"allowJs": true, | ||
"types": [ | ||
"bun-types" | ||
] | ||
"types": [] | ||
} | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
9
1282
131213
33
2234
1
+ Addedlru-cache@11.0.2(transitive)
- Removedlru-cache@11.0.1(transitive)
Updatedlru-cache@11.0.2