sleepy-serv 0.11.0 → 0.13.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 +63 -5
- package/dist/index.d.ts +3 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/socket.d.ts +2 -9
- package/dist/socket.d.ts.map +1 -1
- package/dist/utils.d.ts +12 -3
- package/dist/utils.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +20 -21
- package/src/socket.ts +37 -26
- package/src/utils.ts +19 -3
package/README.md
CHANGED
|
@@ -12,10 +12,7 @@ A directory-driven web server designed for REST-ful applications
|
|
|
12
12
|
Here's a minimalist example on how to create a sleepy-serv app:
|
|
13
13
|
|
|
14
14
|
```js
|
|
15
|
-
import {
|
|
16
|
-
middleware,
|
|
17
|
-
createApp,
|
|
18
|
-
} from 'sleepy-serv'
|
|
15
|
+
import { createApp } from 'sleepy-serv'
|
|
19
16
|
|
|
20
17
|
const PORT = 3000
|
|
21
18
|
|
|
@@ -29,7 +26,7 @@ The parameter for `import.meta.dirname` can be any directory you prefer, but it'
|
|
|
29
26
|
`sleepy-serv` was originally built for NodeJS, but it was ported to `bun` recently (before the initial release). The `createApp()` function merely calls `Bun.serve()` under-the-hood, and returns the `app` object that contains these properties:
|
|
30
27
|
- `routes`: Contains a list of all of the routes defined by the file structure. This is useful for debugging.
|
|
31
28
|
- `server`: this is the object that's returned from `Bun.serve()`. The `server` object has an `async` `.stop()` method on it, but prefer `app.close()` (see [Shutting Down](#shutting-down)), which stops the server and releases everything else the app holds.
|
|
32
|
-
- `
|
|
29
|
+
- `ws`: WebSocket commands for interacting with connected clients: `query(fn)`, `send(fn, event, body)`, `broadcast(event, body)`, and `drop(fn, code?, reason?)`.
|
|
33
30
|
- `close`: an `async` function that shuts the app down. See [Shutting Down](#shutting-down).
|
|
34
31
|
|
|
35
32
|
### Shutting Down
|
|
@@ -488,3 +485,64 @@ const app = await createApp(PORT, import.meta.dirname, {
|
|
|
488
485
|
onClose: () => console.info('closing down...'),
|
|
489
486
|
})
|
|
490
487
|
```
|
|
488
|
+
|
|
489
|
+
### `ws`
|
|
490
|
+
|
|
491
|
+
WebSocket tuning and lifecycle hooks:
|
|
492
|
+
|
|
493
|
+
```js
|
|
494
|
+
const app = await createApp(PORT, import.meta.dirname, {
|
|
495
|
+
ws: {
|
|
496
|
+
heartbeatInterval: 30_000,
|
|
497
|
+
dropThreshold: 120_000,
|
|
498
|
+
reclaimTtl: 300_000,
|
|
499
|
+
ticketTtl: 10_000,
|
|
500
|
+
onOpen: clientId => console.log('connected:', clientId),
|
|
501
|
+
onClose: (clientId, reason) => console.log('closed:', clientId, reason),
|
|
502
|
+
},
|
|
503
|
+
})
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
- `heartbeatInterval`: how often the client should send heartbeats, in milliseconds. Sent to the client in the welcome message. Defaults to `30_000`.
|
|
507
|
+
- `dropThreshold`: how long the server waits without an inbound message before reaping the connection, in milliseconds. Defaults to `120_000`.
|
|
508
|
+
- `reclaimTtl`: how long an inactive (reaped/dropped) session stays reclaimable, in milliseconds. Defaults to `300_000`.
|
|
509
|
+
- `ticketTtl`: how long a minted upgrade ticket stays valid, in milliseconds. Defaults to `10_000`.
|
|
510
|
+
- `onOpen(clientId)`: fires after a client's welcome message is sent. Wrapped in try/catch so a throwing hook does not break the connection.
|
|
511
|
+
- `onClose(clientId, reason)`: fires when a connection closes. `reason` is a `CloseReason` value: `'ok'`, `'dropped'`, `'reaped'`, or `'superseded'`. Also wrapped in try/catch.
|
|
512
|
+
|
|
513
|
+
## WebSocket Commands
|
|
514
|
+
|
|
515
|
+
The `app.ws` object exposes four methods for interacting with connected clients:
|
|
516
|
+
|
|
517
|
+
- `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)`.
|
|
518
|
+
- `send(fn, event, body)`: 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(id => id === targetId, event, body)`.
|
|
519
|
+
- `broadcast(event, body)`: push a notification to all connected clients.
|
|
520
|
+
- `drop(fn, code?, reason?)`: close connections matching a filter. The filter function receives `(clientId, data, index)` and returns a boolean. The default code is `CloseCode.Ok` (1000), which tells the client not to reconnect. Passing a custom code (e.g. 4000) allows the client to reconnect. To drop one client: `app.ws.drop(id => id === targetId)`.
|
|
521
|
+
|
|
522
|
+
The same commands are available inside endpoint handlers via `req.ws`:
|
|
523
|
+
|
|
524
|
+
```js
|
|
525
|
+
export default function (req) {
|
|
526
|
+
const targetId = req.query.targetId
|
|
527
|
+
|
|
528
|
+
req.ws.send(id => id === targetId, 'ping', { from: 'handler' })
|
|
529
|
+
|
|
530
|
+
return Response.json({ ok: true })
|
|
531
|
+
}
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
This works from both HTTP and WebSocket transports.
|
|
535
|
+
|
|
536
|
+
## Exports
|
|
537
|
+
|
|
538
|
+
`sleepy-serv` exports several runtime constants and types:
|
|
539
|
+
|
|
540
|
+
- `CloseCode`: WebSocket close codes: `Ok` (1000), `Abnormal` (1006), `Reaped` (4999)
|
|
541
|
+
- `CloseReason`: close reason values: `Ok`, `Dropped`, `Reaped`, `Superseded`
|
|
542
|
+
- `StatusCode`: the full range of HTTP status codes (1xx through 5xx)
|
|
543
|
+
- `HttpMethod`: HTTP verbs: `Head`, `Get`, `Post`, `Put`, `Patch`, `Delete`
|
|
544
|
+
- `SessionEntry`: the shape returned by `query()`: `{ clientId: string, app: unknown }`
|
|
545
|
+
- `FilterFn`: the filter callback type: `(clientId: string, data: unknown, index: number) => boolean`
|
|
546
|
+
- `SocketCommands`: the type for `app.ws` and `req.ws`
|
|
547
|
+
- Error classes for every 4xx and 5xx status (e.g. `NotFoundError`, `UnauthorizedError`, `InternalServerError`)
|
|
548
|
+
- Middleware helpers: `parseJsonBody`, `validateSchemas`, `setValidationFormats`
|
package/dist/index.d.ts
CHANGED
|
@@ -1,18 +1,16 @@
|
|
|
1
|
-
import type { AppOptions, Server } from './utils';
|
|
2
|
-
import type { SocketCommands } from './socket';
|
|
1
|
+
import type { SocketCommands, AppOptions, Server } from './utils';
|
|
3
2
|
export * from './errors';
|
|
4
3
|
export { StatusCode, CloseCode, CloseReason, HttpMethod } from './utils';
|
|
5
4
|
export { parseJsonBody, setValidationFormats, validateSchemas, } from './middleware';
|
|
6
|
-
export type { SocketCommands } from './
|
|
7
|
-
export type { ActiveSessions } from './utils';
|
|
5
|
+
export type { FilterFn, SessionEntry, SocketCommands } from './utils';
|
|
8
6
|
export type { AppOptions, AsyncHandlerResult, BaseRequest, EndpointRequest, FormattedError, Handler, Middleware, MiddlewareChain, NextFn, HandlerResult, Request, Server, SocketConnection, SocketOptions, WebSocketRequest, } from './utils';
|
|
9
7
|
export type { FormatterField, FormatterSchema, ValidationSchemas, } from './middleware';
|
|
10
8
|
type OutputRoutes = Record<string, string[]>;
|
|
11
9
|
type CloseFn = (force?: boolean) => Promise<void>;
|
|
12
10
|
export type App = {
|
|
13
11
|
server: Server;
|
|
14
|
-
commands: SocketCommands;
|
|
15
12
|
routes: OutputRoutes;
|
|
13
|
+
ws: SocketCommands;
|
|
16
14
|
close: CloseFn;
|
|
17
15
|
};
|
|
18
16
|
export declare function createApp(port: number, rootPath: string, opts?: AppOptions): Promise<App>;
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AA4BA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AA4BA,OAAO,KAAK,EAMV,cAAc,EACd,UAAU,EACV,MAAM,EACP,MAAM,SAAS,CAAA;AAOhB,cAAc,UAAU,CAAA;AACxB,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AAExE,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,eAAe,EACf,cAAc,EACd,OAAO,EACP,UAAU,EACV,eAAe,EACf,MAAM,EACN,aAAa,EACb,OAAO,EACP,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;AA4C5C,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;AA0aD,wBAAsB,SAAS,CAC7B,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,IAAI,GAAE,UAAe,GACpB,OAAO,CAAC,GAAG,CAAC,CAad"}
|
package/dist/socket.d.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { CloseReason } from './utils';
|
|
2
2
|
import type { WebSocketHandler } from 'bun';
|
|
3
|
-
import type { AsyncHandlerResult, HttpMethod, Request, MiddlewareChain, SocketData, ActiveSession, InactiveSession, AppOptions } from './utils';
|
|
3
|
+
import type { AsyncHandlerResult, HttpMethod, Request, MiddlewareChain, SocketCommands, SocketData, ActiveSession, InactiveSession, AppOptions } from './utils';
|
|
4
4
|
type SocketHandler = (req: Request, res: unknown) => AsyncHandlerResult;
|
|
5
|
-
type FilterFn = (clientId: string, data: unknown) => boolean;
|
|
6
5
|
type SocketEndpoint = {
|
|
7
6
|
method: HttpMethod;
|
|
8
7
|
path: string;
|
|
@@ -31,14 +30,8 @@ export type SocketRoute = {
|
|
|
31
30
|
segments: string[];
|
|
32
31
|
chain: MiddlewareChain;
|
|
33
32
|
};
|
|
34
|
-
export type SocketCommands = {
|
|
35
|
-
send: (clientId: string, event: string, body: unknown) => void;
|
|
36
|
-
sendToGroup: (fn: FilterFn, event: string, body: unknown) => void;
|
|
37
|
-
broadcast: (event: string, body: unknown) => void;
|
|
38
|
-
drop: (clientId: string, code?: number, reason?: string) => void;
|
|
39
|
-
};
|
|
40
33
|
export declare function buildSocketState(opts?: AppOptions): SocketState;
|
|
41
|
-
export declare function buildSocketServer(routes: SocketRoute[], state: SocketState): WebSocketHandler<SocketData>;
|
|
34
|
+
export declare function buildSocketServer(routes: SocketRoute[], state: SocketState, commands: SocketCommands): WebSocketHandler<SocketData>;
|
|
42
35
|
export declare function buildSocketHandlers(state: SocketState): SocketEndpoint[];
|
|
43
36
|
export declare function buildSocketCommands(state: SocketState): SocketCommands;
|
|
44
37
|
export {};
|
package/dist/socket.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"socket.d.ts","sourceRoot":"","sources":["../src/socket.ts"],"names":[],"mappings":"AAUA,OAAO,EAGL,WAAW,EAIZ,MAAM,SAAS,CAAA;AAchB,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,KAAK,CAAA;AAE3C,OAAO,KAAK,EACV,kBAAkB,EAClB,UAAU,EACV,OAAO,EACP,eAAe,
|
|
1
|
+
{"version":3,"file":"socket.d.ts","sourceRoot":"","sources":["../src/socket.ts"],"names":[],"mappings":"AAUA,OAAO,EAGL,WAAW,EAIZ,MAAM,SAAS,CAAA;AAchB,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,KAAK,CAAA;AAE3C,OAAO,KAAK,EACV,kBAAkB,EAClB,UAAU,EACV,OAAO,EACP,eAAe,EACf,cAAc,EAGd,UAAU,EAEV,aAAa,EACb,eAAe,EAEf,UAAU,EACX,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;AAiTD,wBAAgB,gBAAgB,CAAE,IAAI,GAAE,UAAe,GAAG,WAAW,CAapE;AAED,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,WAAW,EAAE,EACrB,KAAK,EAAE,WAAW,EAClB,QAAQ,EAAE,cAAc,GACvB,gBAAgB,CAAC,UAAU,CAAC,CAoJ9B;AAED,wBAAgB,mBAAmB,CAAE,KAAK,EAAE,WAAW,GAAG,cAAc,EAAE,CAyIzE;AAED,wBAAgB,mBAAmB,CAAE,KAAK,EAAE,WAAW,GAAG,cAAc,CAgEvE"}
|
package/dist/utils.d.ts
CHANGED
|
@@ -96,6 +96,17 @@ export type InactiveSession = {
|
|
|
96
96
|
};
|
|
97
97
|
export type ActiveSessions = ReadonlyMap<string, ActiveSession>;
|
|
98
98
|
export type Session = ActiveSession | InactiveSession;
|
|
99
|
+
export type SessionEntry = {
|
|
100
|
+
clientId: string;
|
|
101
|
+
app: unknown;
|
|
102
|
+
};
|
|
103
|
+
export type FilterFn = (clientId: string, data: unknown, index: number) => boolean;
|
|
104
|
+
export type SocketCommands = {
|
|
105
|
+
query: (fn: FilterFn) => SessionEntry[];
|
|
106
|
+
send: (fn: FilterFn, event: string, body: unknown) => void;
|
|
107
|
+
broadcast: (event: string, body: unknown) => void;
|
|
108
|
+
drop: (fn: FilterFn, code?: number, reason?: string) => void;
|
|
109
|
+
};
|
|
99
110
|
export type BaseRequest = {
|
|
100
111
|
method: HttpMethod;
|
|
101
112
|
route: string;
|
|
@@ -103,9 +114,7 @@ export type BaseRequest = {
|
|
|
103
114
|
params: Record<string, string>;
|
|
104
115
|
query: Record<string, unknown>;
|
|
105
116
|
json: () => Promise<unknown>;
|
|
106
|
-
ws:
|
|
107
|
-
active: ActiveSessions;
|
|
108
|
-
};
|
|
117
|
+
ws: SocketCommands;
|
|
109
118
|
};
|
|
110
119
|
export type EndpointRequest = BaseRequest & {
|
|
111
120
|
raw: BunRequest;
|
package/dist/utils.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/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,WAAW,GAAG;IACxB,MAAM,EAAE,UAAU,CAAA;IAClB,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,OAAO,CAAA;IAChB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC9B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC9B,IAAI,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAA;IAC5B,EAAE,EAAE
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -32,7 +32,7 @@ import type {
|
|
|
32
32
|
Middleware,
|
|
33
33
|
MiddlewareChain,
|
|
34
34
|
EndpointRequest,
|
|
35
|
-
|
|
35
|
+
SocketCommands,
|
|
36
36
|
AppOptions,
|
|
37
37
|
Server,
|
|
38
38
|
} from './utils'
|
|
@@ -40,7 +40,6 @@ import type {
|
|
|
40
40
|
import type {
|
|
41
41
|
SocketRoute,
|
|
42
42
|
SocketState,
|
|
43
|
-
SocketCommands,
|
|
44
43
|
} from './socket'
|
|
45
44
|
|
|
46
45
|
export * from './errors'
|
|
@@ -52,8 +51,7 @@ export {
|
|
|
52
51
|
validateSchemas,
|
|
53
52
|
} from './middleware'
|
|
54
53
|
|
|
55
|
-
export type { SocketCommands } from './
|
|
56
|
-
export type { ActiveSessions } from './utils'
|
|
54
|
+
export type { FilterFn, SessionEntry, SocketCommands } from './utils'
|
|
57
55
|
|
|
58
56
|
export type {
|
|
59
57
|
AppOptions,
|
|
@@ -127,8 +125,8 @@ type CloseFn = (force?: boolean) => Promise<void>
|
|
|
127
125
|
|
|
128
126
|
export type App = {
|
|
129
127
|
server: Server
|
|
130
|
-
commands: SocketCommands
|
|
131
128
|
routes: OutputRoutes
|
|
129
|
+
ws: SocketCommands
|
|
132
130
|
close: CloseFn
|
|
133
131
|
}
|
|
134
132
|
|
|
@@ -167,7 +165,7 @@ function defaultMethodMap (): Record<string, EndpointHandler> {
|
|
|
167
165
|
function buildEndpointRequest (
|
|
168
166
|
bunReq: BunRequest,
|
|
169
167
|
server: Server,
|
|
170
|
-
|
|
168
|
+
ws: SocketCommands,
|
|
171
169
|
): EndpointRequest {
|
|
172
170
|
const url = new URL(bunReq.url)
|
|
173
171
|
const qs = url.search.replace('?', '')
|
|
@@ -191,9 +189,7 @@ function buildEndpointRequest (
|
|
|
191
189
|
raw: bunReq,
|
|
192
190
|
server,
|
|
193
191
|
json,
|
|
194
|
-
ws
|
|
195
|
-
active: activeSessions,
|
|
196
|
-
},
|
|
192
|
+
ws,
|
|
197
193
|
}
|
|
198
194
|
}
|
|
199
195
|
|
|
@@ -405,15 +401,11 @@ function buildSocketRoutes (mergedRoutes: ChainRoute[]): SocketRoute[] {
|
|
|
405
401
|
|
|
406
402
|
function buildModuleRoutes (
|
|
407
403
|
socketRoutes: SocketRoute[],
|
|
408
|
-
|
|
404
|
+
ws: SocketCommands,
|
|
409
405
|
): ModuleRoute[] {
|
|
410
406
|
return socketRoutes.map(route => {
|
|
411
407
|
const handler: EndpointHandler = async (bunReq, server) => {
|
|
412
|
-
const req = buildEndpointRequest(
|
|
413
|
-
bunReq,
|
|
414
|
-
server,
|
|
415
|
-
state.activeSessions,
|
|
416
|
-
)
|
|
408
|
+
const req = buildEndpointRequest(bunReq, server, ws)
|
|
417
409
|
|
|
418
410
|
return executeMiddlewareChain(req, route.chain)
|
|
419
411
|
}
|
|
@@ -451,6 +443,7 @@ function buildOutputRoutes (moduleRoutes: ModuleRoute[]): OutputRoutes {
|
|
|
451
443
|
async function buildRoutes (
|
|
452
444
|
rootPath: string,
|
|
453
445
|
state: SocketState,
|
|
446
|
+
ws: SocketCommands,
|
|
454
447
|
opts: AppOptions,
|
|
455
448
|
): Promise<AppRoutes> {
|
|
456
449
|
const basePath = `${rootPath}/api`
|
|
@@ -474,7 +467,7 @@ async function buildRoutes (
|
|
|
474
467
|
)
|
|
475
468
|
|
|
476
469
|
const socketRoutes = buildSocketRoutes(mergedRoutes)
|
|
477
|
-
const moduleRoutes = buildModuleRoutes(socketRoutes,
|
|
470
|
+
const moduleRoutes = buildModuleRoutes(socketRoutes, ws)
|
|
478
471
|
const serverRoutes = buildServerRoutes(moduleRoutes)
|
|
479
472
|
const outputRoutes = buildOutputRoutes(moduleRoutes)
|
|
480
473
|
|
|
@@ -489,10 +482,16 @@ function buildServer (
|
|
|
489
482
|
port: number,
|
|
490
483
|
routes: AppRoutes,
|
|
491
484
|
state: SocketState,
|
|
485
|
+
ws: SocketCommands,
|
|
492
486
|
opts: AppOptions,
|
|
493
487
|
): Server {
|
|
494
488
|
const hostname = opts.hostname || '0.0.0.0'
|
|
495
|
-
|
|
489
|
+
|
|
490
|
+
const websocketServer = buildSocketServer(
|
|
491
|
+
routes.socket,
|
|
492
|
+
state,
|
|
493
|
+
ws,
|
|
494
|
+
)
|
|
496
495
|
|
|
497
496
|
return Bun.serve({
|
|
498
497
|
port,
|
|
@@ -561,15 +560,15 @@ export async function createApp (
|
|
|
561
560
|
opts: AppOptions = {},
|
|
562
561
|
): Promise<App> {
|
|
563
562
|
const state = buildSocketState(opts)
|
|
564
|
-
const
|
|
565
|
-
const
|
|
566
|
-
const
|
|
563
|
+
const ws = buildSocketCommands(state)
|
|
564
|
+
const routes = await buildRoutes(rootPath, state, ws, opts)
|
|
565
|
+
const server = buildServer(port, routes, state, ws, opts)
|
|
567
566
|
const close = processIO(port, server, opts)
|
|
568
567
|
|
|
569
568
|
return {
|
|
570
569
|
routes: routes.output,
|
|
571
570
|
server,
|
|
572
|
-
|
|
571
|
+
ws,
|
|
573
572
|
close,
|
|
574
573
|
}
|
|
575
574
|
}
|
package/src/socket.ts
CHANGED
|
@@ -36,11 +36,12 @@ import type {
|
|
|
36
36
|
HttpMethod,
|
|
37
37
|
Request,
|
|
38
38
|
MiddlewareChain,
|
|
39
|
+
SocketCommands,
|
|
40
|
+
SessionEntry,
|
|
39
41
|
WebSocketRequest,
|
|
40
42
|
SocketData,
|
|
41
43
|
SocketConnection,
|
|
42
44
|
ActiveSession,
|
|
43
|
-
ActiveSessions,
|
|
44
45
|
InactiveSession,
|
|
45
46
|
Session,
|
|
46
47
|
AppOptions,
|
|
@@ -54,7 +55,6 @@ import type {
|
|
|
54
55
|
} from './messages'
|
|
55
56
|
|
|
56
57
|
type SocketHandler = (req: Request, res: unknown) => AsyncHandlerResult
|
|
57
|
-
type FilterFn = (clientId: string, data: unknown) => boolean
|
|
58
58
|
|
|
59
59
|
type UpgradeData = {
|
|
60
60
|
clientId?: string
|
|
@@ -117,13 +117,6 @@ export type SocketRoute = {
|
|
|
117
117
|
chain: MiddlewareChain
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
export type SocketCommands = {
|
|
121
|
-
send: (clientId: string, event: string, body: unknown) => void
|
|
122
|
-
sendToGroup: (fn: FilterFn, event: string, body: unknown) => void
|
|
123
|
-
broadcast: (event: string, body: unknown) => void
|
|
124
|
-
drop: (clientId: string, code?: number, reason?: string) => void
|
|
125
|
-
}
|
|
126
|
-
|
|
127
120
|
const ajv = new Ajv({
|
|
128
121
|
allErrors: true,
|
|
129
122
|
})
|
|
@@ -361,7 +354,7 @@ function buildParams (
|
|
|
361
354
|
function buildRequest (
|
|
362
355
|
params: Record<string, string>,
|
|
363
356
|
message: RequestMessage,
|
|
364
|
-
|
|
357
|
+
ws: SocketCommands,
|
|
365
358
|
): WebSocketRequest {
|
|
366
359
|
const { id, clientId, method, route } = message
|
|
367
360
|
const headers = new Headers(message.headers ?? {})
|
|
@@ -376,10 +369,8 @@ function buildRequest (
|
|
|
376
369
|
headers,
|
|
377
370
|
params,
|
|
378
371
|
query,
|
|
372
|
+
ws,
|
|
379
373
|
json,
|
|
380
|
-
ws: {
|
|
381
|
-
active: activeSessions,
|
|
382
|
-
},
|
|
383
374
|
}
|
|
384
375
|
}
|
|
385
376
|
|
|
@@ -447,6 +438,7 @@ export function buildSocketState (opts: AppOptions = {}): SocketState {
|
|
|
447
438
|
export function buildSocketServer (
|
|
448
439
|
routes: SocketRoute[],
|
|
449
440
|
state: SocketState,
|
|
441
|
+
commands: SocketCommands,
|
|
450
442
|
): WebSocketHandler<SocketData> {
|
|
451
443
|
const {
|
|
452
444
|
dropThreshold,
|
|
@@ -581,7 +573,7 @@ export function buildSocketServer (
|
|
|
581
573
|
const { id, clientId } = message
|
|
582
574
|
const route = matchRoute(routes, message)
|
|
583
575
|
const params = buildParams(route, message)
|
|
584
|
-
const req = buildRequest(params, message,
|
|
576
|
+
const req = buildRequest(params, message, commands)
|
|
585
577
|
const res = await executeMiddlewareChain(req, route.chain)
|
|
586
578
|
const outgoingMsg = await buildOutgoingMessage(id, clientId, res)
|
|
587
579
|
|
|
@@ -754,16 +746,33 @@ export function buildSocketCommands (state: SocketState): SocketCommands {
|
|
|
754
746
|
}
|
|
755
747
|
|
|
756
748
|
return {
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
749
|
+
query (fn) {
|
|
750
|
+
const results: SessionEntry[] = []
|
|
751
|
+
let index = 0
|
|
752
|
+
|
|
761
753
|
for (const [clientId, session] of state.activeSessions) {
|
|
762
|
-
|
|
754
|
+
if (fn(clientId, session.ws.data, index)) {
|
|
755
|
+
results.push({
|
|
756
|
+
clientId,
|
|
757
|
+
app: session.ws.data.app,
|
|
758
|
+
})
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
index += 1
|
|
762
|
+
}
|
|
763
763
|
|
|
764
|
-
|
|
764
|
+
return results
|
|
765
|
+
},
|
|
766
|
+
send (fn, event, body) {
|
|
767
|
+
let index = 0
|
|
768
|
+
|
|
769
|
+
/* TODO: look into concurrency at some point */
|
|
770
|
+
for (const [clientId, session] of state.activeSessions) {
|
|
771
|
+
if (fn(clientId, session.ws.data, index)) {
|
|
765
772
|
sendToClient(clientId, event, body)
|
|
766
773
|
}
|
|
774
|
+
|
|
775
|
+
index += 1
|
|
767
776
|
}
|
|
768
777
|
},
|
|
769
778
|
broadcast (event, body) {
|
|
@@ -771,14 +780,16 @@ export function buildSocketCommands (state: SocketState): SocketCommands {
|
|
|
771
780
|
sendToClient(clientId, event, body)
|
|
772
781
|
}
|
|
773
782
|
},
|
|
774
|
-
drop (
|
|
775
|
-
|
|
783
|
+
drop (fn, code, reason) {
|
|
784
|
+
let index = 0
|
|
776
785
|
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
786
|
+
for (const [clientId, session] of state.activeSessions) {
|
|
787
|
+
if (fn(clientId, session.ws.data, index)) {
|
|
788
|
+
session.ws.close(code, reason)
|
|
789
|
+
}
|
|
780
790
|
|
|
781
|
-
|
|
791
|
+
index += 1
|
|
792
|
+
}
|
|
782
793
|
},
|
|
783
794
|
}
|
|
784
795
|
}
|
package/src/utils.ts
CHANGED
|
@@ -121,6 +121,24 @@ export type InactiveSession = {
|
|
|
121
121
|
export type ActiveSessions = ReadonlyMap<string, ActiveSession>
|
|
122
122
|
export type Session = ActiveSession | InactiveSession
|
|
123
123
|
|
|
124
|
+
export type SessionEntry = {
|
|
125
|
+
clientId: string
|
|
126
|
+
app: unknown
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export type FilterFn = (
|
|
130
|
+
clientId: string,
|
|
131
|
+
data: unknown,
|
|
132
|
+
index: number,
|
|
133
|
+
) => boolean
|
|
134
|
+
|
|
135
|
+
export type SocketCommands = {
|
|
136
|
+
query: (fn: FilterFn) => SessionEntry[]
|
|
137
|
+
send: (fn: FilterFn, event: string, body: unknown) => void
|
|
138
|
+
broadcast: (event: string, body: unknown) => void
|
|
139
|
+
drop: (fn: FilterFn, code?: number, reason?: string) => void
|
|
140
|
+
}
|
|
141
|
+
|
|
124
142
|
export type BaseRequest = {
|
|
125
143
|
method: HttpMethod
|
|
126
144
|
route: string
|
|
@@ -128,9 +146,7 @@ export type BaseRequest = {
|
|
|
128
146
|
params: Record<string, string>
|
|
129
147
|
query: Record<string, unknown>
|
|
130
148
|
json: () => Promise<unknown>
|
|
131
|
-
ws:
|
|
132
|
-
active: ActiveSessions
|
|
133
|
-
}
|
|
149
|
+
ws: SocketCommands
|
|
134
150
|
}
|
|
135
151
|
|
|
136
152
|
export type EndpointRequest = BaseRequest & {
|