sleepy-serv 0.21.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -8
- package/dist/core/index.d.ts +2 -2
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/socket.d.ts +4 -4
- package/dist/core/socket.d.ts.map +1 -1
- package/dist/core/utils.d.ts +21 -16
- package/dist/core/utils.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/core/index.ts +44 -22
- package/src/core/socket.ts +56 -62
- package/src/core/utils.ts +20 -17
package/README.md
CHANGED
|
@@ -65,7 +65,7 @@ object with these properties:
|
|
|
65
65
|
directly.
|
|
66
66
|
- `ws`: WebSocket commands for interacting with connected clients:
|
|
67
67
|
`query(fn)`, `send(fn, event, body)`, `broadcast(event, body)`, and
|
|
68
|
-
`drop(
|
|
68
|
+
`drop(signal, fn)`.
|
|
69
69
|
- `close`: an `async` function that shuts the app down. See
|
|
70
70
|
[Shutting Down](#shutting-down).
|
|
71
71
|
|
|
@@ -476,7 +476,13 @@ const app = createApp(3000, {
|
|
|
476
476
|
|
|
477
477
|
App-level middleware is executed before any directory-level or route-level middleware.
|
|
478
478
|
|
|
479
|
-
|
|
479
|
+
When WebSocket support is enabled (`ws: true` or `ws: { ... }`), the
|
|
480
|
+
reserved `/ws` handshake routes are folded into these same chains, so
|
|
481
|
+
app-level middleware runs against them too. A catch-all validator (for
|
|
482
|
+
example a `validateSchemas` that requires a JSON body or a specific
|
|
483
|
+
header on every request) will therefore also run against the body-less
|
|
484
|
+
`/ws` handshake requests and reject them. Scope such validators below
|
|
485
|
+
the reserved paths rather than applying them app-wide.
|
|
480
486
|
|
|
481
487
|
### `hostname`
|
|
482
488
|
|
|
@@ -528,7 +534,14 @@ const app = createApp(3000, {
|
|
|
528
534
|
|
|
529
535
|
### `ws`
|
|
530
536
|
|
|
531
|
-
WebSocket
|
|
537
|
+
WebSocket support is opt-in. Pass `ws: true` to enable with defaults,
|
|
538
|
+
or pass an options object to customize behavior:
|
|
539
|
+
|
|
540
|
+
```js
|
|
541
|
+
createApp(3000, { ws: true })
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
With custom tuning and lifecycle hooks:
|
|
532
545
|
|
|
533
546
|
```js
|
|
534
547
|
const app = createApp(3000, {
|
|
@@ -538,17 +551,21 @@ const app = createApp(3000, {
|
|
|
538
551
|
reclaimTtl: 300_000,
|
|
539
552
|
ticketTtl: 10_000,
|
|
540
553
|
onOpen: clientId => console.log('connected:', clientId),
|
|
541
|
-
onClose: (clientId,
|
|
554
|
+
onClose: (clientId, signal) => console.log('closed:', clientId, signal),
|
|
542
555
|
},
|
|
543
556
|
})
|
|
544
557
|
```
|
|
545
558
|
|
|
559
|
+
When `ws` is omitted (or `false`), no `/ws` endpoints are registered
|
|
560
|
+
and the server does not accept WebSocket upgrades. Calling `app.ws` or
|
|
561
|
+
`req.ws` methods when WebSocket support is disabled throws an error.
|
|
562
|
+
|
|
546
563
|
- `heartbeatInterval`: how often the client should send heartbeats, in milliseconds. Sent to the client in the welcome message. Defaults to `30_000`.
|
|
547
564
|
- `dropThreshold`: how long the server waits without an inbound message before reaping the connection, in milliseconds. Defaults to `120_000`.
|
|
548
565
|
- `reclaimTtl`: how long an inactive (reaped/dropped) session stays reclaimable, in milliseconds. Defaults to `300_000`.
|
|
549
566
|
- `ticketTtl`: how long a minted upgrade ticket stays valid, in milliseconds. Defaults to `10_000`.
|
|
550
567
|
- `onOpen(clientId)`: fires after a client's welcome message is sent. Wrapped in try/catch so a throwing hook does not break the connection.
|
|
551
|
-
- `onClose(clientId,
|
|
568
|
+
- `onClose(clientId, signal)`: fires when a connection closes. `signal` is a `CloseSignal` value (`{ code, reason }`). Also wrapped in try/catch.
|
|
552
569
|
|
|
553
570
|
## WebSocket Commands
|
|
554
571
|
|
|
@@ -557,7 +574,7 @@ The `app.ws` object exposes four methods for interacting with connected clients:
|
|
|
557
574
|
- `query(fn)`: return a filtered list of active sessions. The filter function receives `(clientId, data, index)` and returns a boolean. Each entry in the returned array is a `SessionEntry` with `clientId` and `app` (the application context). To list all sessions: `app.ws.query(() => true)`.
|
|
558
575
|
- `send(event, body, fn)`: push a notification to clients matching a filter. The filter function receives `(clientId, data, index)` and returns a boolean. To target one client: `app.ws.send('ping', body, id => id === targetId)`.
|
|
559
576
|
- `broadcast(event, body)`: push a notification to all connected clients.
|
|
560
|
-
- `drop(
|
|
577
|
+
- `drop(signal, fn)`: close connections matching a filter. `signal` is a `CloseSignal` (`{ code, reason }`) with `code` validated in the range [4000-4099]. The filter function receives `(clientId, data, index)` and returns a boolean. To drop one client: `app.ws.drop({ code: 4000, reason: 'kicked' }, id => id === targetId)`.
|
|
561
578
|
|
|
562
579
|
The same commands are available inside endpoint handlers via `req.ws`:
|
|
563
580
|
|
|
@@ -577,8 +594,8 @@ This works from both HTTP and WebSocket transports.
|
|
|
577
594
|
|
|
578
595
|
`sleepy-serv` exports several runtime constants and types:
|
|
579
596
|
|
|
580
|
-
- `
|
|
581
|
-
- `
|
|
597
|
+
- `InternalCloseSignal`: internal close signals: `Ok` (`{ code: 1000, reason: 'ok' }`), `Reaped` (`{ code: 4998, reason: 'reaped' }`), `Superseded` (`{ code: 4999, reason: 'superseded' }`)
|
|
598
|
+
- `CloseSignal`: the type for close signals: `{ code: number, reason: string }`
|
|
582
599
|
- `StatusCode`: the full range of HTTP status codes (1xx through 5xx)
|
|
583
600
|
- `HttpMethod`: HTTP verbs: `Head`, `Get`, `Post`, `Put`, `Patch`, `Delete`
|
|
584
601
|
- `SessionEntry`: the shape returned by `query()`: `{ clientId: string, app: unknown }`
|
package/dist/core/index.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { RouteConfig, SocketCommands, AppOptions, Server } from './utils';
|
|
2
2
|
export * from './errors';
|
|
3
|
-
export { StatusCode,
|
|
3
|
+
export { StatusCode, HttpMethod, InternalCloseSignal, } from './utils';
|
|
4
4
|
export { parseJsonBody, setValidationFormats, validateSchemas, } from './middleware';
|
|
5
5
|
export type { FilterFn, SessionEntry, SocketCommands } from './utils';
|
|
6
|
-
export type { AppOptions, AsyncHandlerResult, BaseRequest, EndpointRequest, FormattedError, Handler, MetaEntry, Middleware, MiddlewareChain, NextFn, HandlerResult, Request, RouteConfig, RouteDefinition, Server, SocketConnection, SocketOptions, WebSocketRequest, } from './utils';
|
|
6
|
+
export type { AppOptions, AsyncHandlerResult, BaseRequest, CloseSignal, EndpointRequest, FormattedError, Handler, MetaEntry, Middleware, MiddlewareChain, NextFn, HandlerResult, Request, RouteConfig, RouteDefinition, Server, SocketConnection, SocketOptions, WebSocketRequest, } from './utils';
|
|
7
7
|
export type { FormatterField, FormatterSchema, ValidationSchemas, } from './middleware';
|
|
8
8
|
type OutputRoutes = Record<string, string[]>;
|
|
9
9
|
type CloseFn = (force?: boolean) => Promise<void>;
|
package/dist/core/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/core/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/core/index.ts"],"names":[],"mappings":"AA2BA,OAAO,KAAK,EAOV,WAAW,EAEX,cAAc,EAEd,UAAU,EACV,MAAM,EACP,MAAM,SAAS,CAAA;AAOhB,cAAc,UAAU,CAAA;AAExB,OAAO,EACL,UAAU,EACV,UAAU,EACV,mBAAmB,GACpB,MAAM,SAAS,CAAA;AAEhB,OAAO,EACL,aAAa,EACb,oBAAoB,EACpB,eAAe,GAChB,MAAM,cAAc,CAAA;AAErB,YAAY,EAAE,QAAQ,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAErE,YAAY,EACV,UAAU,EACV,kBAAkB,EAClB,WAAW,EACX,WAAW,EACX,eAAe,EACf,cAAc,EACd,OAAO,EACP,SAAS,EACT,UAAU,EACV,eAAe,EACf,MAAM,EACN,aAAa,EACb,OAAO,EACP,WAAW,EACX,eAAe,EACf,MAAM,EACN,gBAAgB,EAChB,aAAa,EACb,gBAAgB,GACjB,MAAM,SAAS,CAAA;AAEhB,YAAY,EACV,cAAc,EACd,eAAe,EACf,iBAAiB,GAClB,MAAM,cAAc,CAAA;AAErB,KAAK,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAA;AA0B5C,KAAK,OAAO,GAAG,CAAC,KAAK,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;AAEjD,MAAM,MAAM,GAAG,GAAG;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,YAAY,CAAA;IACpB,EAAE,EAAE,cAAc,CAAA;IAClB,KAAK,EAAE,OAAO,CAAA;CACf,CAAA;AAsRD,wBAAgB,SAAS,CACvB,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,WAAW,EACnB,IAAI,GAAE,UAAe,GACpB,GAAG,CAqBL"}
|
package/dist/core/socket.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { CloseReason } from './utils';
|
|
2
1
|
import type { WebSocketHandler } from 'bun';
|
|
3
|
-
import type { AsyncHandlerResult, HttpMethod, Request, MiddlewareChain, SocketCommands, SocketData, ActiveSession, InactiveSession,
|
|
2
|
+
import type { AsyncHandlerResult, CloseSignal, HttpMethod, Request, MiddlewareChain, SocketCommands, SocketData, ActiveSession, InactiveSession, SocketOptions } from './utils';
|
|
4
3
|
type SocketHandler = (req: Request, res: unknown) => AsyncHandlerResult;
|
|
5
4
|
type SocketEndpoint = {
|
|
6
5
|
method: HttpMethod;
|
|
@@ -22,7 +21,7 @@ export type SocketState = {
|
|
|
22
21
|
activeSessions: Map<string, ActiveSession>;
|
|
23
22
|
inactiveSessions: Map<string, InactiveSession>;
|
|
24
23
|
onOpen: ((clientId: string) => void) | null;
|
|
25
|
-
onClose: ((clientId: string,
|
|
24
|
+
onClose: ((clientId: string, signal: CloseSignal) => void) | null;
|
|
26
25
|
};
|
|
27
26
|
export type SocketRoute = {
|
|
28
27
|
method: HttpMethod;
|
|
@@ -30,9 +29,10 @@ export type SocketRoute = {
|
|
|
30
29
|
segments: string[];
|
|
31
30
|
chain: MiddlewareChain;
|
|
32
31
|
};
|
|
33
|
-
export declare function buildSocketState(opts?:
|
|
32
|
+
export declare function buildSocketState(opts?: SocketOptions): SocketState;
|
|
34
33
|
export declare function buildSocketServer(routes: SocketRoute[], state: SocketState, commands: SocketCommands): WebSocketHandler<SocketData>;
|
|
35
34
|
export declare function buildSocketHandlers(state: SocketState): SocketEndpoint[];
|
|
35
|
+
export declare function buildDisabledSocketCommands(): SocketCommands;
|
|
36
36
|
export declare function buildSocketCommands(state: SocketState): SocketCommands;
|
|
37
37
|
export {};
|
|
38
38
|
//# sourceMappingURL=socket.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"socket.d.ts","sourceRoot":"","sources":["../../src/core/socket.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"socket.d.ts","sourceRoot":"","sources":["../../src/core/socket.ts"],"names":[],"mappings":"AA6BA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,KAAK,CAAA;AAE3C,OAAO,KAAK,EACV,kBAAkB,EAClB,WAAW,EACX,UAAU,EACV,OAAO,EAEP,eAAe,EACf,cAAc,EAGd,UAAU,EAEV,aAAa,EACb,eAAe,EAEf,aAAa,EACd,MAAM,SAAS,CAAA;AAShB,KAAK,aAAa,GAAG,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,KAAK,kBAAkB,CAAA;AAYvE,KAAK,cAAc,GAAG;IACpB,MAAM,EAAE,UAAU,CAAA;IAClB,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,aAAa,CAAA;CACvB,CAAA;AAqBD,MAAM,MAAM,MAAM,GAAG;IACnB,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;IACjB,IAAI,EAAE,OAAO,CAAA;CACd,CAAA;AAED,MAAM,MAAM,WAAW,GAAG;IACxB,aAAa,EAAE,MAAM,CAAA;IACrB,iBAAiB,EAAE,MAAM,CAAA;IACzB,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC5B,cAAc,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,CAAA;IAC1C,gBAAgB,EAAE,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,CAAA;IAC9C,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC,GAAG,IAAI,CAAA;IAC3C,OAAO,EAAE,CAAC,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,KAAK,IAAI,CAAC,GAAG,IAAI,CAAA;CAClE,CAAA;AAED,MAAM,MAAM,WAAW,GAAG;IACxB,MAAM,EAAE,UAAU,CAAA;IAClB,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,MAAM,EAAE,CAAA;IAClB,KAAK,EAAE,eAAe,CAAA;CACvB,CAAA;AA+QD,wBAAgB,gBAAgB,CAAE,IAAI,GAAE,aAAkB,GAAG,WAAW,CAavE;AAED,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,WAAW,EAAE,EACrB,KAAK,EAAE,WAAW,EAClB,QAAQ,EAAE,cAAc,GACvB,gBAAgB,CAAC,UAAU,CAAC,CA8J9B;AAED,wBAAgB,mBAAmB,CAAE,KAAK,EAAE,WAAW,GAAG,cAAc,EAAE,CAwIzE;AAED,wBAAgB,2BAA2B,IAAK,cAAc,CAS7D;AAED,wBAAgB,mBAAmB,CAAE,KAAK,EAAE,WAAW,GAAG,cAAc,CAwEvE"}
|
package/dist/core/utils.d.ts
CHANGED
|
@@ -104,7 +104,7 @@ export type FilterFn = (clientId: string, data: unknown, index: number) => boole
|
|
|
104
104
|
export type SocketCommands = {
|
|
105
105
|
broadcast: (event: string, body: unknown) => void;
|
|
106
106
|
send: (event: string, body: unknown, fn: FilterFn) => void;
|
|
107
|
-
drop: (
|
|
107
|
+
drop: (signal: CloseSignal, fn: FilterFn) => void;
|
|
108
108
|
query: (fn: FilterFn) => SessionEntry[];
|
|
109
109
|
};
|
|
110
110
|
export type BaseRequest = {
|
|
@@ -125,19 +125,24 @@ export type WebSocketRequest = BaseRequest & {
|
|
|
125
125
|
clientId: string;
|
|
126
126
|
};
|
|
127
127
|
export type Request = EndpointRequest | WebSocketRequest;
|
|
128
|
-
export
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
readonly Reaped:
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
128
|
+
export type CloseSignal = {
|
|
129
|
+
code: number;
|
|
130
|
+
reason: string;
|
|
131
|
+
};
|
|
132
|
+
export declare const InternalCloseSignal: {
|
|
133
|
+
readonly Ok: {
|
|
134
|
+
readonly code: 1000;
|
|
135
|
+
readonly reason: "ok";
|
|
136
|
+
};
|
|
137
|
+
readonly Reaped: {
|
|
138
|
+
readonly code: 4998;
|
|
139
|
+
readonly reason: "reaped";
|
|
140
|
+
};
|
|
141
|
+
readonly Superseded: {
|
|
142
|
+
readonly code: 4999;
|
|
143
|
+
readonly reason: "superseded";
|
|
144
|
+
};
|
|
145
|
+
};
|
|
141
146
|
export type SocketOptions = {
|
|
142
147
|
dropThreshold?: number;
|
|
143
148
|
heartbeatInterval?: number;
|
|
@@ -145,7 +150,7 @@ export type SocketOptions = {
|
|
|
145
150
|
reclaimTtl?: number;
|
|
146
151
|
ticketTtl?: number;
|
|
147
152
|
onOpen?: (clientId: string) => void;
|
|
148
|
-
onClose?: (clientId: string,
|
|
153
|
+
onClose?: (clientId: string, signal: CloseSignal) => void;
|
|
149
154
|
};
|
|
150
155
|
export type SocketData = {
|
|
151
156
|
clientId: string;
|
|
@@ -177,7 +182,7 @@ export type AppOptions = {
|
|
|
177
182
|
hostname?: string;
|
|
178
183
|
mountPath?: string;
|
|
179
184
|
middleware?: Middleware[];
|
|
180
|
-
ws?: SocketOptions;
|
|
185
|
+
ws?: boolean | SocketOptions;
|
|
181
186
|
onClose?: () => Promise<void> | void;
|
|
182
187
|
};
|
|
183
188
|
export declare function toSegments(pathString: string): string[];
|
package/dist/core/utils.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/core/utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,KAAK,CAAA;AACtC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,IAAI,SAAS,EAAE,MAAM,KAAK,CAAA;AAE1D,eAAO,MAAM,UAAU;;;;;;;CAOb,CAAA;AAEV,MAAM,MAAM,UAAU,GAAG,OAAO,UAAU,CAAC,MAAM,OAAO,UAAU,CAAC,CAAA;AAEnE,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmEb,CAAA;AAEV,MAAM,MAAM,UAAU,GAAG,OAAO,UAAU,CAAC,MAAM,OAAO,UAAU,CAAC,CAAA;AAEnE,MAAM,MAAM,eAAe,GAAG,OAAO,CAAC,WAAW,CAAC,CAAA;AAElD,MAAM,MAAM,cAAc,GAAG;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;CAChB,CAAA;AAED,MAAM,MAAM,kBAAkB,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;AAClD,MAAM,MAAM,aAAa,GAAG,QAAQ,GAAG,kBAAkB,CAAA;AACzD,MAAM,MAAM,MAAM,GAAG,CAAC,IAAI,CAAC,EAAE,OAAO,KAAK,aAAa,CAAA;AAEtD,MAAM,MAAM,UAAU,GAAG,CACvB,GAAG,EAAE,OAAO,EACZ,GAAG,EAAE,OAAO,EACZ,IAAI,EAAE,MAAM,KACT,aAAa,CAAA;AAElB,MAAM,MAAM,OAAO,GAAG,CACpB,GAAG,EAAE,OAAO,EACZ,GAAG,EAAE,OAAO,KACT,aAAa,CAAA;AAElB,MAAM,MAAM,eAAe,GAAG,CAAC,UAAU,GAAG,OAAO,CAAC,EAAE,CAAA;AAEtD,MAAM,MAAM,aAAa,GAAG;IAC1B,KAAK,EAAE,MAAM,CAAA;IACb,EAAE,EAAE,gBAAgB,CAAA;CACrB,CAAA;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,MAAM,CAAA;IACjB,GAAG,EAAE,OAAO,CAAA;CACb,CAAA;AAED,MAAM,MAAM,cAAc,GAAG,WAAW,CAAC,MAAM,EAAE,aAAa,CAAC,CAAA;AAC/D,MAAM,MAAM,OAAO,GAAG,aAAa,GAAG,eAAe,CAAA;AAErD,MAAM,MAAM,YAAY,GAAG;IACzB,QAAQ,EAAE,MAAM,CAAA;IAChB,GAAG,EAAE,OAAO,CAAA;CACb,CAAA;AAED,MAAM,MAAM,QAAQ,GAAG,CACrB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,OAAO,EACb,KAAK,EAAE,MAAM,KACV,OAAO,CAAA;AAEZ,MAAM,MAAM,cAAc,GAAG;IAC3B,SAAS,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACjD,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,QAAQ,KAAK,IAAI,CAAA;IAC1D,IAAI,EAAE,CAAC,
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/core/utils.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/core/index.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
buildSocketServer,
|
|
14
14
|
buildSocketHandlers,
|
|
15
15
|
buildSocketCommands,
|
|
16
|
+
buildDisabledSocketCommands,
|
|
16
17
|
} from './socket'
|
|
17
18
|
|
|
18
19
|
import {
|
|
@@ -34,6 +35,7 @@ import type {
|
|
|
34
35
|
RouteConfig,
|
|
35
36
|
RouteDefinition,
|
|
36
37
|
SocketCommands,
|
|
38
|
+
SocketOptions,
|
|
37
39
|
AppOptions,
|
|
38
40
|
Server,
|
|
39
41
|
} from './utils'
|
|
@@ -44,7 +46,12 @@ import type {
|
|
|
44
46
|
} from './socket'
|
|
45
47
|
|
|
46
48
|
export * from './errors'
|
|
47
|
-
|
|
49
|
+
|
|
50
|
+
export {
|
|
51
|
+
StatusCode,
|
|
52
|
+
HttpMethod,
|
|
53
|
+
InternalCloseSignal,
|
|
54
|
+
} from './utils'
|
|
48
55
|
|
|
49
56
|
export {
|
|
50
57
|
parseJsonBody,
|
|
@@ -58,6 +65,7 @@ export type {
|
|
|
58
65
|
AppOptions,
|
|
59
66
|
AsyncHandlerResult,
|
|
60
67
|
BaseRequest,
|
|
68
|
+
CloseSignal,
|
|
61
69
|
EndpointRequest,
|
|
62
70
|
FormattedError,
|
|
63
71
|
Handler,
|
|
@@ -131,6 +139,20 @@ function defaultMethodMap (): Record<string, EndpointHandler> {
|
|
|
131
139
|
}
|
|
132
140
|
}
|
|
133
141
|
|
|
142
|
+
function resolveSocketOptions (
|
|
143
|
+
ws: boolean | SocketOptions | undefined,
|
|
144
|
+
): SocketOptions | null {
|
|
145
|
+
if (!ws) {
|
|
146
|
+
return null
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (ws === true) {
|
|
150
|
+
return {}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return ws
|
|
154
|
+
}
|
|
155
|
+
|
|
134
156
|
function buildEndpointRequest (
|
|
135
157
|
bunReq: BunRequest,
|
|
136
158
|
server: Server,
|
|
@@ -162,9 +184,7 @@ function buildEndpointRequest (
|
|
|
162
184
|
}
|
|
163
185
|
}
|
|
164
186
|
|
|
165
|
-
function normalizeChain (
|
|
166
|
-
route: RouteDefinition,
|
|
167
|
-
): ChainRoute {
|
|
187
|
+
function normalizeChain (route: RouteDefinition): ChainRoute {
|
|
168
188
|
const chain = Array.isArray(route.chain)
|
|
169
189
|
? route.chain
|
|
170
190
|
: [route.chain]
|
|
@@ -268,7 +288,7 @@ function buildOutputRoutes (moduleRoutes: ModuleRoute[]): OutputRoutes {
|
|
|
268
288
|
|
|
269
289
|
function buildRoutes (
|
|
270
290
|
config: RouteConfig,
|
|
271
|
-
state: SocketState,
|
|
291
|
+
state: SocketState | null,
|
|
272
292
|
ws: SocketCommands,
|
|
273
293
|
opts: AppOptions,
|
|
274
294
|
): AppRoutes {
|
|
@@ -294,13 +314,9 @@ function buildRoutes (
|
|
|
294
314
|
}
|
|
295
315
|
})
|
|
296
316
|
|
|
297
|
-
const mergedRoutes =
|
|
298
|
-
normalRoutes,
|
|
299
|
-
|
|
300
|
-
meta,
|
|
301
|
-
state,
|
|
302
|
-
mountPath,
|
|
303
|
-
)
|
|
317
|
+
const mergedRoutes = state
|
|
318
|
+
? buildMergedRoutes(normalRoutes, middleware, meta, state, mountPath)
|
|
319
|
+
: normalRoutes
|
|
304
320
|
|
|
305
321
|
const socketRoutes = buildSocketRoutes(mergedRoutes)
|
|
306
322
|
const moduleRoutes = buildModuleRoutes(socketRoutes, ws)
|
|
@@ -317,23 +333,21 @@ function buildRoutes (
|
|
|
317
333
|
function buildServer (
|
|
318
334
|
port: number,
|
|
319
335
|
routes: AppRoutes,
|
|
320
|
-
state: SocketState,
|
|
336
|
+
state: SocketState | null,
|
|
321
337
|
ws: SocketCommands,
|
|
322
338
|
opts: AppOptions,
|
|
323
339
|
): Server {
|
|
324
340
|
const hostname = opts.hostname || '0.0.0.0'
|
|
325
341
|
|
|
326
|
-
const
|
|
327
|
-
routes.socket,
|
|
328
|
-
|
|
329
|
-
ws,
|
|
330
|
-
)
|
|
342
|
+
const websocket = state
|
|
343
|
+
? buildSocketServer(routes.socket, state, ws)
|
|
344
|
+
: undefined
|
|
331
345
|
|
|
332
346
|
return Bun.serve({
|
|
333
347
|
port,
|
|
334
348
|
hostname,
|
|
335
349
|
routes: routes.server,
|
|
336
|
-
websocket:
|
|
350
|
+
...(websocket ? { websocket } : {}),
|
|
337
351
|
async fetch (_req, _server) {
|
|
338
352
|
throw new NotFoundError()
|
|
339
353
|
},
|
|
@@ -346,7 +360,7 @@ function buildServer (
|
|
|
346
360
|
|
|
347
361
|
return Response.json(httpError.output, { status })
|
|
348
362
|
},
|
|
349
|
-
})
|
|
363
|
+
}) as Server
|
|
350
364
|
}
|
|
351
365
|
|
|
352
366
|
function processIO (server: Server, opts: AppOptions): CloseFn {
|
|
@@ -391,8 +405,16 @@ export function createApp (
|
|
|
391
405
|
config: RouteConfig,
|
|
392
406
|
opts: AppOptions = {},
|
|
393
407
|
): App {
|
|
394
|
-
const
|
|
395
|
-
|
|
408
|
+
const socketOpts = resolveSocketOptions(opts.ws)
|
|
409
|
+
|
|
410
|
+
const state = socketOpts
|
|
411
|
+
? buildSocketState(socketOpts)
|
|
412
|
+
: null
|
|
413
|
+
|
|
414
|
+
const ws = state
|
|
415
|
+
? buildSocketCommands(state)
|
|
416
|
+
: buildDisabledSocketCommands()
|
|
417
|
+
|
|
396
418
|
const routes = buildRoutes(config, state, ws, opts)
|
|
397
419
|
const server = buildServer(port, routes, state, ws, opts)
|
|
398
420
|
const close = processIO(server, opts)
|
package/src/core/socket.ts
CHANGED
|
@@ -10,8 +10,7 @@ import {
|
|
|
10
10
|
|
|
11
11
|
import {
|
|
12
12
|
StatusCode,
|
|
13
|
-
|
|
14
|
-
CloseReason,
|
|
13
|
+
InternalCloseSignal,
|
|
15
14
|
toSegments,
|
|
16
15
|
formatError,
|
|
17
16
|
executeMiddlewareChain,
|
|
@@ -19,7 +18,6 @@ import {
|
|
|
19
18
|
|
|
20
19
|
import {
|
|
21
20
|
RequestError,
|
|
22
|
-
BadRequestError,
|
|
23
21
|
NotFoundError,
|
|
24
22
|
UnauthorizedError,
|
|
25
23
|
MethodNotAllowedError,
|
|
@@ -33,8 +31,10 @@ import type { WebSocketHandler } from 'bun'
|
|
|
33
31
|
|
|
34
32
|
import type {
|
|
35
33
|
AsyncHandlerResult,
|
|
34
|
+
CloseSignal,
|
|
36
35
|
HttpMethod,
|
|
37
36
|
Request,
|
|
37
|
+
FilterFn,
|
|
38
38
|
MiddlewareChain,
|
|
39
39
|
SocketCommands,
|
|
40
40
|
SessionEntry,
|
|
@@ -44,7 +44,7 @@ import type {
|
|
|
44
44
|
ActiveSession,
|
|
45
45
|
InactiveSession,
|
|
46
46
|
Session,
|
|
47
|
-
|
|
47
|
+
SocketOptions,
|
|
48
48
|
} from './utils'
|
|
49
49
|
|
|
50
50
|
import type {
|
|
@@ -107,7 +107,7 @@ export type SocketState = {
|
|
|
107
107
|
activeSessions: Map<string, ActiveSession>
|
|
108
108
|
inactiveSessions: Map<string, InactiveSession>
|
|
109
109
|
onOpen: ((clientId: string) => void) | null
|
|
110
|
-
onClose: ((clientId: string,
|
|
110
|
+
onClose: ((clientId: string, signal: CloseSignal) => void) | null
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
export type SocketRoute = {
|
|
@@ -240,30 +240,6 @@ function parseMessage (raw: string | Buffer): RawMessage | undefined {
|
|
|
240
240
|
}
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
-
async function parseJsonBody (req: Request): Promise<unknown> {
|
|
244
|
-
try {
|
|
245
|
-
const body = await req.json()
|
|
246
|
-
|
|
247
|
-
return body
|
|
248
|
-
} catch {
|
|
249
|
-
throw new BadRequestError('Invalid JSON')
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
async function parseJsonBodyAppData (req: Request): Promise<unknown> {
|
|
254
|
-
const contentType = req.headers.get('content-type')
|
|
255
|
-
const usingJsonBody = contentType?.startsWith('application/json')
|
|
256
|
-
|
|
257
|
-
if (!usingJsonBody) {
|
|
258
|
-
return null
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
const rawBody = await parseJsonBody(req)
|
|
262
|
-
const appData = (rawBody as Record<string, unknown> | null)?.data ?? null
|
|
263
|
-
|
|
264
|
-
return appData
|
|
265
|
-
}
|
|
266
|
-
|
|
267
243
|
function sweepInactiveSessions (state: SocketState): void {
|
|
268
244
|
for (const [key, session] of state.inactiveSessions) {
|
|
269
245
|
if (!isSessionActive(session)) {
|
|
@@ -410,28 +386,18 @@ function buildErrorMessage (
|
|
|
410
386
|
})
|
|
411
387
|
}
|
|
412
388
|
|
|
413
|
-
function
|
|
414
|
-
if (ws.data.reaped) {
|
|
415
|
-
return CloseReason.Reaped
|
|
416
|
-
} else if (code === CloseCode.Ok) {
|
|
417
|
-
return CloseReason.Ok
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
return CloseReason.Dropped
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
export function buildSocketState (opts: AppOptions = {}): SocketState {
|
|
389
|
+
export function buildSocketState (opts: SocketOptions = {}): SocketState {
|
|
424
390
|
return {
|
|
425
|
-
dropThreshold: opts.
|
|
426
|
-
heartbeatInterval: opts.
|
|
427
|
-
maxTickets: opts.
|
|
428
|
-
reclaimTtl: opts.
|
|
429
|
-
ticketTtl: opts.
|
|
391
|
+
dropThreshold: opts.dropThreshold ?? 120_000,
|
|
392
|
+
heartbeatInterval: opts.heartbeatInterval ?? 30_000,
|
|
393
|
+
maxTickets: opts.maxTickets ?? 100_000,
|
|
394
|
+
reclaimTtl: opts.reclaimTtl ?? 300_000,
|
|
395
|
+
ticketTtl: opts.ticketTtl ?? 10_000,
|
|
430
396
|
tickets: new Map(),
|
|
431
397
|
activeSessions: new Map(),
|
|
432
398
|
inactiveSessions: new Map(),
|
|
433
|
-
onOpen: opts.
|
|
434
|
-
onClose: opts.
|
|
399
|
+
onOpen: opts.onOpen ?? null,
|
|
400
|
+
onClose: opts.onClose ?? null,
|
|
435
401
|
}
|
|
436
402
|
}
|
|
437
403
|
|
|
@@ -458,7 +424,10 @@ export function buildSocketServer (
|
|
|
458
424
|
ws.data.reaperHandle = setTimeout(() => {
|
|
459
425
|
ws.data.reaped = true
|
|
460
426
|
|
|
461
|
-
ws.close(
|
|
427
|
+
ws.close(
|
|
428
|
+
InternalCloseSignal.Reaped.code,
|
|
429
|
+
InternalCloseSignal.Reaped.reason,
|
|
430
|
+
)
|
|
462
431
|
}, dropThreshold)
|
|
463
432
|
}
|
|
464
433
|
|
|
@@ -472,10 +441,10 @@ export function buildSocketServer (
|
|
|
472
441
|
}
|
|
473
442
|
}
|
|
474
443
|
|
|
475
|
-
function invokeClose (ws: SocketConnection,
|
|
444
|
+
function invokeClose (ws: SocketConnection, signal: CloseSignal) {
|
|
476
445
|
if (onClose) {
|
|
477
446
|
try {
|
|
478
|
-
onClose(ws.data.clientId,
|
|
447
|
+
onClose(ws.data.clientId, signal)
|
|
479
448
|
} catch (err) {
|
|
480
449
|
console.error(err)
|
|
481
450
|
}
|
|
@@ -492,7 +461,10 @@ export function buildSocketServer (
|
|
|
492
461
|
if (existingSession) {
|
|
493
462
|
existingSession.ws.data.superseded = true
|
|
494
463
|
|
|
495
|
-
existingSession.ws.close(
|
|
464
|
+
existingSession.ws.close(
|
|
465
|
+
InternalCloseSignal.Superseded.code,
|
|
466
|
+
InternalCloseSignal.Superseded.reason,
|
|
467
|
+
)
|
|
496
468
|
}
|
|
497
469
|
|
|
498
470
|
inactiveSessions.delete(ws.data.clientId)
|
|
@@ -519,18 +491,22 @@ export function buildSocketServer (
|
|
|
519
491
|
ws.send(JSON.stringify(welcomeMessage))
|
|
520
492
|
invokeOpen(ws)
|
|
521
493
|
},
|
|
522
|
-
close (ws: SocketConnection, code: number): void {
|
|
494
|
+
close (ws: SocketConnection, code: number, reason: string): void {
|
|
495
|
+
const signal: CloseSignal = {
|
|
496
|
+
code,
|
|
497
|
+
reason,
|
|
498
|
+
}
|
|
499
|
+
|
|
523
500
|
if (ws.data.reaperHandle) {
|
|
524
501
|
clearTimeout(ws.data.reaperHandle)
|
|
525
502
|
}
|
|
526
503
|
|
|
527
504
|
if (ws.data.superseded) {
|
|
528
|
-
invokeClose(ws,
|
|
505
|
+
invokeClose(ws, signal)
|
|
529
506
|
|
|
530
507
|
return
|
|
531
508
|
}
|
|
532
509
|
|
|
533
|
-
const reason = getCloseReason(ws, code)
|
|
534
510
|
const exists = activeSessions.get(ws.data.clientId)
|
|
535
511
|
|
|
536
512
|
if (!exists || exists.ws !== ws) {
|
|
@@ -539,7 +515,7 @@ export function buildSocketServer (
|
|
|
539
515
|
|
|
540
516
|
activeSessions.delete(ws.data.clientId)
|
|
541
517
|
|
|
542
|
-
if (code !==
|
|
518
|
+
if (code !== InternalCloseSignal.Ok.code || ws.data.reaped) {
|
|
543
519
|
inactiveSessions.set(ws.data.clientId, {
|
|
544
520
|
token: exists.token,
|
|
545
521
|
expiresAt: Date.now() + reclaimTtl,
|
|
@@ -547,7 +523,7 @@ export function buildSocketServer (
|
|
|
547
523
|
})
|
|
548
524
|
}
|
|
549
525
|
|
|
550
|
-
invokeClose(ws,
|
|
526
|
+
invokeClose(ws, signal)
|
|
551
527
|
},
|
|
552
528
|
async message (ws: SocketConnection, raw: string | Buffer): Promise<void> {
|
|
553
529
|
const incomingMsg = parseMessage(raw)
|
|
@@ -677,8 +653,7 @@ export function buildSocketHandlers (state: SocketState): SocketEndpoint[] {
|
|
|
677
653
|
validateSchema(req, createTicketValidator)
|
|
678
654
|
|
|
679
655
|
const clientId = crypto.randomUUID()
|
|
680
|
-
const
|
|
681
|
-
const ticket = issueTicket(clientId, appData)
|
|
656
|
+
const ticket = issueTicket(clientId, res)
|
|
682
657
|
|
|
683
658
|
return Response.json({
|
|
684
659
|
clientId,
|
|
@@ -728,6 +703,17 @@ export function buildSocketHandlers (state: SocketState): SocketEndpoint[] {
|
|
|
728
703
|
]
|
|
729
704
|
}
|
|
730
705
|
|
|
706
|
+
export function buildDisabledSocketCommands (): SocketCommands {
|
|
707
|
+
const msg = 'WebSocket support is not enabled'
|
|
708
|
+
|
|
709
|
+
return {
|
|
710
|
+
broadcast () { throw new Error(msg) },
|
|
711
|
+
send () { throw new Error(msg) },
|
|
712
|
+
drop () { throw new Error(msg) },
|
|
713
|
+
query () { throw new Error(msg) },
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
731
717
|
export function buildSocketCommands (state: SocketState): SocketCommands {
|
|
732
718
|
function sendToClient (clientId: string, event: string, body: unknown) {
|
|
733
719
|
const session = state.activeSessions.get(clientId)
|
|
@@ -746,12 +732,12 @@ export function buildSocketCommands (state: SocketState): SocketCommands {
|
|
|
746
732
|
}
|
|
747
733
|
|
|
748
734
|
return {
|
|
749
|
-
broadcast (event, body) {
|
|
735
|
+
broadcast (event: string, body: unknown) {
|
|
750
736
|
for (const clientId of state.activeSessions.keys()) {
|
|
751
737
|
sendToClient(clientId, event, body)
|
|
752
738
|
}
|
|
753
739
|
},
|
|
754
|
-
send (event, body, fn) {
|
|
740
|
+
send (event: string, body: unknown, fn: FilterFn) {
|
|
755
741
|
let index = 0
|
|
756
742
|
|
|
757
743
|
/* TODO: look into concurrency at some point */
|
|
@@ -763,9 +749,17 @@ export function buildSocketCommands (state: SocketState): SocketCommands {
|
|
|
763
749
|
index += 1
|
|
764
750
|
}
|
|
765
751
|
},
|
|
766
|
-
drop (
|
|
752
|
+
drop (signal: CloseSignal, fn: FilterFn) {
|
|
753
|
+
const { code, reason } = signal
|
|
754
|
+
|
|
767
755
|
let index = 0
|
|
768
756
|
|
|
757
|
+
if (!Number.isInteger(code) || code < 4000 || code > 4099) {
|
|
758
|
+
throw new RangeError(
|
|
759
|
+
`Signal code must be an integer in [4000, 4099], got ${code}`,
|
|
760
|
+
)
|
|
761
|
+
}
|
|
762
|
+
|
|
769
763
|
for (const [clientId, session] of state.activeSessions) {
|
|
770
764
|
if (fn(clientId, session.ws.data, index)) {
|
|
771
765
|
session.ws.close(code, reason)
|
|
@@ -774,7 +768,7 @@ export function buildSocketCommands (state: SocketState): SocketCommands {
|
|
|
774
768
|
index += 1
|
|
775
769
|
}
|
|
776
770
|
},
|
|
777
|
-
query (fn) {
|
|
771
|
+
query (fn: FilterFn) {
|
|
778
772
|
const results: SessionEntry[] = []
|
|
779
773
|
let index = 0
|
|
780
774
|
|
package/src/core/utils.ts
CHANGED
|
@@ -135,7 +135,7 @@ export type FilterFn = (
|
|
|
135
135
|
export type SocketCommands = {
|
|
136
136
|
broadcast: (event: string, body: unknown) => void
|
|
137
137
|
send: (event: string, body: unknown, fn: FilterFn) => void
|
|
138
|
-
drop: (
|
|
138
|
+
drop: (signal: CloseSignal, fn: FilterFn) => void
|
|
139
139
|
query: (fn: FilterFn) => SessionEntry[]
|
|
140
140
|
}
|
|
141
141
|
|
|
@@ -161,23 +161,26 @@ export type WebSocketRequest = BaseRequest & {
|
|
|
161
161
|
|
|
162
162
|
export type Request = EndpointRequest | WebSocketRequest
|
|
163
163
|
|
|
164
|
-
export
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
} as const
|
|
169
|
-
|
|
170
|
-
export type CloseCode = typeof CloseCode[keyof typeof CloseCode]
|
|
164
|
+
export type CloseSignal = {
|
|
165
|
+
code: number
|
|
166
|
+
reason: string
|
|
167
|
+
}
|
|
171
168
|
|
|
172
|
-
export const
|
|
173
|
-
Ok:
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
169
|
+
export const InternalCloseSignal = {
|
|
170
|
+
Ok: {
|
|
171
|
+
code: 1000,
|
|
172
|
+
reason: 'ok',
|
|
173
|
+
},
|
|
174
|
+
Reaped: {
|
|
175
|
+
code: 4998,
|
|
176
|
+
reason: 'reaped',
|
|
177
|
+
},
|
|
178
|
+
Superseded: {
|
|
179
|
+
code: 4999,
|
|
180
|
+
reason: 'superseded',
|
|
181
|
+
},
|
|
177
182
|
} as const
|
|
178
183
|
|
|
179
|
-
export type CloseReason = typeof CloseReason[keyof typeof CloseReason]
|
|
180
|
-
|
|
181
184
|
export type SocketOptions = {
|
|
182
185
|
dropThreshold?: number
|
|
183
186
|
heartbeatInterval?: number
|
|
@@ -185,7 +188,7 @@ export type SocketOptions = {
|
|
|
185
188
|
reclaimTtl?: number
|
|
186
189
|
ticketTtl?: number
|
|
187
190
|
onOpen?: (clientId: string) => void
|
|
188
|
-
onClose?: (clientId: string,
|
|
191
|
+
onClose?: (clientId: string, signal: CloseSignal) => void
|
|
189
192
|
}
|
|
190
193
|
|
|
191
194
|
export type SocketData = {
|
|
@@ -224,7 +227,7 @@ export type AppOptions = {
|
|
|
224
227
|
hostname?: string
|
|
225
228
|
mountPath?: string
|
|
226
229
|
middleware?: Middleware[]
|
|
227
|
-
ws?: SocketOptions
|
|
230
|
+
ws?: boolean | SocketOptions
|
|
228
231
|
onClose?: () => Promise<void> | void
|
|
229
232
|
}
|
|
230
233
|
|