sleepy-serv 0.22.0 → 0.24.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 +13 -11
- package/dist/core/index.d.ts +27 -4
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/socket.d.ts +29 -3
- package/dist/core/socket.d.ts.map +1 -1
- package/dist/core/utils.d.ts +26 -67
- package/dist/core/utils.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/core/index.ts +51 -15
- package/src/core/socket.ts +142 -44
- package/src/core/utils.ts +30 -79
- package/src/index.ts +1 -1
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
|
|
|
@@ -551,7 +551,7 @@ const app = createApp(3000, {
|
|
|
551
551
|
reclaimTtl: 300_000,
|
|
552
552
|
ticketTtl: 10_000,
|
|
553
553
|
onOpen: clientId => console.log('connected:', clientId),
|
|
554
|
-
onClose: (clientId,
|
|
554
|
+
onClose: (clientId, signal) => console.log('closed:', clientId, signal),
|
|
555
555
|
},
|
|
556
556
|
})
|
|
557
557
|
```
|
|
@@ -565,16 +565,16 @@ and the server does not accept WebSocket upgrades. Calling `app.ws` or
|
|
|
565
565
|
- `reclaimTtl`: how long an inactive (reaped/dropped) session stays reclaimable, in milliseconds. Defaults to `300_000`.
|
|
566
566
|
- `ticketTtl`: how long a minted upgrade ticket stays valid, in milliseconds. Defaults to `10_000`.
|
|
567
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.
|
|
568
|
-
- `onClose(clientId,
|
|
568
|
+
- `onClose(clientId, signal)`: fires when a connection closes. `signal` is a `CloseSignal` value (`{ code, reason }`). Also wrapped in try/catch.
|
|
569
569
|
|
|
570
570
|
## WebSocket Commands
|
|
571
571
|
|
|
572
572
|
The `app.ws` object exposes four methods for interacting with connected clients:
|
|
573
573
|
|
|
574
|
-
- `query(fn)`: return a filtered list of
|
|
575
|
-
- `send(event, body, fn)`: push a notification to clients matching a filter. The filter function receives `(
|
|
574
|
+
- `query(fn, filter?)`: return a filtered list of sessions. The filter function receives `(session, index)` where `session` is a `SessionEntry` (`{ clientId, type, data }`), and returns a boolean. The optional `filter` parameter controls which sessions are iterated: `'active'` (default), `'inactive'`, or `'all'`. To list all active sessions: `app.ws.query(() => true)`. To include inactive sessions: `app.ws.query(() => true, 'all')`.
|
|
575
|
+
- `send(event, body, fn)`: push a notification to clients matching a filter. The filter function receives `(session, index)` and returns a boolean. To target one client: `app.ws.send('ping', body, s => s.clientId === targetId)`.
|
|
576
576
|
- `broadcast(event, body)`: push a notification to all connected clients.
|
|
577
|
-
- `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 `(session, index)` and returns a boolean. To drop one client: `app.ws.drop({ code: 4000, reason: 'kicked' }, s => s.clientId === targetId)`.
|
|
578
578
|
|
|
579
579
|
The same commands are available inside endpoint handlers via `req.ws`:
|
|
580
580
|
|
|
@@ -582,7 +582,7 @@ The same commands are available inside endpoint handlers via `req.ws`:
|
|
|
582
582
|
export default function (req) {
|
|
583
583
|
const targetId = req.query.targetId
|
|
584
584
|
|
|
585
|
-
req.ws.send('ping', { from: 'handler' },
|
|
585
|
+
req.ws.send('ping', { from: 'handler' }, s => s.clientId === targetId)
|
|
586
586
|
|
|
587
587
|
return Response.json({ ok: true })
|
|
588
588
|
}
|
|
@@ -594,12 +594,14 @@ This works from both HTTP and WebSocket transports.
|
|
|
594
594
|
|
|
595
595
|
`sleepy-serv` exports several runtime constants and types:
|
|
596
596
|
|
|
597
|
-
- `
|
|
598
|
-
- `
|
|
597
|
+
- `ServerCloseSignals`: server 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 }`
|
|
599
599
|
- `StatusCode`: the full range of HTTP status codes (1xx through 5xx)
|
|
600
600
|
- `HttpMethod`: HTTP verbs: `Head`, `Get`, `Post`, `Put`, `Patch`, `Delete`
|
|
601
|
-
- `
|
|
602
|
-
- `
|
|
601
|
+
- `SessionType`: session state: `Active` (`'active'`), `Inactive` (`'inactive'`)
|
|
602
|
+
- `SessionFilter`: session filter: `Active` (`'active'`), `Inactive` (`'inactive'`), `All` (`'all'`)
|
|
603
|
+
- `SessionEntry`: the shape returned by `query()` and received by filter callbacks: `{ clientId: string, type: SessionType, data: unknown }`
|
|
604
|
+
- `FilterFn`: the filter callback type: `(session: SessionEntry, index: number) => boolean`
|
|
603
605
|
- `SocketCommands`: the type for `app.ws` and `req.ws`
|
|
604
606
|
- Error classes for every 4xx and 5xx status (e.g. `NotFoundError`, `UnauthorizedError`, `InternalServerError`)
|
|
605
607
|
- Middleware helpers: `parseJsonBody`, `validateSchemas`, `setValidationFormats`
|
package/dist/core/index.d.ts
CHANGED
|
@@ -1,10 +1,33 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { HttpMethod, Middleware, MiddlewareChain, Handler, Server } from './utils';
|
|
2
|
+
import type { SocketCommands } from './utils';
|
|
3
|
+
import type { SocketOptions } from './socket';
|
|
2
4
|
export * from './errors';
|
|
3
|
-
export {
|
|
5
|
+
export { ServerCloseSignals } from './socket';
|
|
6
|
+
export { StatusCode, HttpMethod, SessionType, SessionFilter, } from './utils';
|
|
4
7
|
export { parseJsonBody, setValidationFormats, validateSchemas, } from './middleware';
|
|
5
|
-
export type {
|
|
6
|
-
export type {
|
|
8
|
+
export type { ActiveSession, ActiveSessions, InactiveSession, SocketConnection, SocketOptions, } from './socket';
|
|
9
|
+
export type { AsyncHandlerResult, BaseRequest, CloseSignal, EndpointRequest, FilterFn, FormattedError, Handler, HandlerResult, Middleware, MiddlewareChain, NextFn, Request, Server, SessionEntry, SocketCommands, SocketData, WebSocketRequest, } from './utils';
|
|
7
10
|
export type { FormatterField, FormatterSchema, ValidationSchemas, } from './middleware';
|
|
11
|
+
export type RouteDefinition = {
|
|
12
|
+
method: HttpMethod;
|
|
13
|
+
path: string;
|
|
14
|
+
chain: Handler | MiddlewareChain;
|
|
15
|
+
};
|
|
16
|
+
export type MetaEntry = {
|
|
17
|
+
path: string;
|
|
18
|
+
middleware: Middleware[];
|
|
19
|
+
};
|
|
20
|
+
export type RouteConfig = {
|
|
21
|
+
routes: RouteDefinition[];
|
|
22
|
+
meta?: MetaEntry[];
|
|
23
|
+
};
|
|
24
|
+
export type AppOptions = {
|
|
25
|
+
hostname?: string;
|
|
26
|
+
mountPath?: string;
|
|
27
|
+
middleware?: Middleware[];
|
|
28
|
+
ws?: boolean | SocketOptions;
|
|
29
|
+
onClose?: () => Promise<void> | void;
|
|
30
|
+
};
|
|
8
31
|
type OutputRoutes = Record<string, string[]>;
|
|
9
32
|
type CloseFn = (force?: boolean) => Promise<void>;
|
|
10
33
|
export type App = {
|
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":"AA2BA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/core/index.ts"],"names":[],"mappings":"AA2BA,OAAO,KAAK,EAEV,UAAU,EACV,UAAU,EACV,eAAe,EAEf,OAAO,EACP,MAAM,EACP,MAAM,SAAS,CAAA;AAEhB,OAAO,KAAK,EACV,cAAc,EACf,MAAM,SAAS,CAAA;AAEhB,OAAO,KAAK,EACV,aAAa,EAGd,MAAM,UAAU,CAAA;AAEjB,cAAc,UAAU,CAAA;AACxB,OAAO,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAA;AAE7C,OAAO,EACL,UAAU,EACV,UAAU,EACV,WAAW,EACX,aAAa,GACd,MAAM,SAAS,CAAA;AAEhB,OAAO,EACL,aAAa,EACb,oBAAoB,EACpB,eAAe,GAChB,MAAM,cAAc,CAAA;AAErB,YAAY,EACV,aAAa,EACb,cAAc,EACd,eAAe,EACf,gBAAgB,EAChB,aAAa,GACd,MAAM,UAAU,CAAA;AAEjB,YAAY,EACV,kBAAkB,EAClB,WAAW,EACX,WAAW,EACX,eAAe,EACf,QAAQ,EACR,cAAc,EACd,OAAO,EACP,aAAa,EACb,UAAU,EACV,eAAe,EACf,MAAM,EACN,OAAO,EACP,MAAM,EACN,YAAY,EACZ,cAAc,EACd,UAAU,EACV,gBAAgB,GACjB,MAAM,SAAS,CAAA;AAEhB,YAAY,EACV,cAAc,EACd,eAAe,EACf,iBAAiB,GAClB,MAAM,cAAc,CAAA;AAErB,MAAM,MAAM,eAAe,GAAG;IAC5B,MAAM,EAAE,UAAU,CAAA;IAClB,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,OAAO,GAAG,eAAe,CAAA;CACjC,CAAA;AAED,MAAM,MAAM,SAAS,GAAG;IACtB,IAAI,EAAE,MAAM,CAAA;IACZ,UAAU,EAAE,UAAU,EAAE,CAAA;CACzB,CAAA;AAED,MAAM,MAAM,WAAW,GAAG;IACxB,MAAM,EAAE,eAAe,EAAE,CAAA;IACzB,IAAI,CAAC,EAAE,SAAS,EAAE,CAAA;CACnB,CAAA;AAED,MAAM,MAAM,UAAU,GAAG;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,UAAU,CAAC,EAAE,UAAU,EAAE,CAAA;IACzB,EAAE,CAAC,EAAE,OAAO,GAAG,aAAa,CAAA;IAC5B,OAAO,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;CACrC,CAAA;AAED,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,12 +1,38 @@
|
|
|
1
|
-
import { CloseReason } from './utils';
|
|
2
1
|
import type { WebSocketHandler } from 'bun';
|
|
3
|
-
import type { AsyncHandlerResult, HttpMethod, Request, MiddlewareChain, SocketCommands, SocketData
|
|
2
|
+
import type { AsyncHandlerResult, CloseSignal, HttpMethod, Request, MiddlewareChain, SocketCommands, SocketData } from './utils';
|
|
4
3
|
type SocketHandler = (req: Request, res: unknown) => AsyncHandlerResult;
|
|
5
4
|
type SocketEndpoint = {
|
|
6
5
|
method: HttpMethod;
|
|
7
6
|
path: string;
|
|
8
7
|
handler: SocketHandler;
|
|
9
8
|
};
|
|
9
|
+
export type SocketConnection = {
|
|
10
|
+
data: SocketData;
|
|
11
|
+
send: (data: string) => unknown;
|
|
12
|
+
close: (code?: number, reason?: string) => void;
|
|
13
|
+
};
|
|
14
|
+
export type ActiveSession = {
|
|
15
|
+
token: string;
|
|
16
|
+
ws: SocketConnection;
|
|
17
|
+
};
|
|
18
|
+
export type InactiveSession = {
|
|
19
|
+
token: string;
|
|
20
|
+
expiresAt: number;
|
|
21
|
+
data: unknown;
|
|
22
|
+
};
|
|
23
|
+
export type ActiveSessions = ReadonlyMap<string, ActiveSession>;
|
|
24
|
+
export declare const ServerCloseSignals: Record<string, CloseSignal>;
|
|
25
|
+
export type ServerCloseSignals = typeof ServerCloseSignals[keyof typeof ServerCloseSignals];
|
|
26
|
+
export type Session = ActiveSession | InactiveSession;
|
|
27
|
+
export type SocketOptions = {
|
|
28
|
+
dropThreshold?: number;
|
|
29
|
+
heartbeatInterval?: number;
|
|
30
|
+
maxTickets?: number;
|
|
31
|
+
reclaimTtl?: number;
|
|
32
|
+
ticketTtl?: number;
|
|
33
|
+
onOpen?: (clientId: string) => void;
|
|
34
|
+
onClose?: (clientId: string, signal: CloseSignal) => void;
|
|
35
|
+
};
|
|
10
36
|
export type Ticket = {
|
|
11
37
|
clientId: string;
|
|
12
38
|
expiresAt: number;
|
|
@@ -22,7 +48,7 @@ export type SocketState = {
|
|
|
22
48
|
activeSessions: Map<string, ActiveSession>;
|
|
23
49
|
inactiveSessions: Map<string, InactiveSession>;
|
|
24
50
|
onOpen: ((clientId: string) => void) | null;
|
|
25
|
-
onClose: ((clientId: string,
|
|
51
|
+
onClose: ((clientId: string, signal: CloseSignal) => void) | null;
|
|
26
52
|
};
|
|
27
53
|
export type SocketRoute = {
|
|
28
54
|
method: HttpMethod;
|
|
@@ -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":"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"}
|
package/dist/core/utils.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ErrorObject } from 'ajv';
|
|
2
2
|
import type { BunRequest, Server as BunServer } from 'bun';
|
|
3
|
+
export type TimeoutHandle = ReturnType<typeof setTimeout>;
|
|
3
4
|
export declare const HttpMethod: {
|
|
4
5
|
readonly Head: "HEAD";
|
|
5
6
|
readonly Get: "GET";
|
|
@@ -85,27 +86,39 @@ export type NextFn = (data?: unknown) => HandlerResult;
|
|
|
85
86
|
export type Middleware = (req: Request, res: unknown, next: NextFn) => HandlerResult;
|
|
86
87
|
export type Handler = (req: Request, res: unknown) => HandlerResult;
|
|
87
88
|
export type MiddlewareChain = (Middleware | Handler)[];
|
|
88
|
-
export type
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
export type CloseSignal = {
|
|
90
|
+
code: number;
|
|
91
|
+
reason: string;
|
|
91
92
|
};
|
|
92
|
-
export type
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
93
|
+
export type SocketData = {
|
|
94
|
+
clientId: string;
|
|
95
|
+
superseded: boolean;
|
|
96
|
+
reaped: boolean;
|
|
97
|
+
reaperHandle: TimeoutHandle | null;
|
|
98
|
+
data: unknown;
|
|
99
|
+
};
|
|
100
|
+
export declare const SessionType: {
|
|
101
|
+
readonly Active: "active";
|
|
102
|
+
readonly Inactive: "inactive";
|
|
103
|
+
};
|
|
104
|
+
export type SessionType = typeof SessionType[keyof typeof SessionType];
|
|
105
|
+
export declare const SessionFilter: {
|
|
106
|
+
readonly Active: "active";
|
|
107
|
+
readonly Inactive: "inactive";
|
|
108
|
+
readonly All: "all";
|
|
96
109
|
};
|
|
97
|
-
export type
|
|
98
|
-
export type Session = ActiveSession | InactiveSession;
|
|
110
|
+
export type SessionFilter = typeof SessionFilter[keyof typeof SessionFilter];
|
|
99
111
|
export type SessionEntry = {
|
|
100
112
|
clientId: string;
|
|
101
|
-
|
|
113
|
+
type: SessionType;
|
|
114
|
+
data: unknown;
|
|
102
115
|
};
|
|
103
|
-
export type FilterFn = (
|
|
116
|
+
export type FilterFn = (session: SessionEntry, index: number) => boolean;
|
|
104
117
|
export type SocketCommands = {
|
|
105
118
|
broadcast: (event: string, body: unknown) => void;
|
|
106
119
|
send: (event: string, body: unknown, fn: FilterFn) => void;
|
|
107
|
-
drop: (
|
|
108
|
-
query: (fn: FilterFn) => SessionEntry[];
|
|
120
|
+
drop: (signal: CloseSignal, fn: FilterFn) => void;
|
|
121
|
+
query: (fn: FilterFn, filter?: SessionFilter) => SessionEntry[];
|
|
109
122
|
};
|
|
110
123
|
export type BaseRequest = {
|
|
111
124
|
method: HttpMethod;
|
|
@@ -125,61 +138,7 @@ export type WebSocketRequest = BaseRequest & {
|
|
|
125
138
|
clientId: string;
|
|
126
139
|
};
|
|
127
140
|
export type Request = EndpointRequest | WebSocketRequest;
|
|
128
|
-
export declare const CloseCode: {
|
|
129
|
-
readonly Ok: 1000;
|
|
130
|
-
readonly Abnormal: 1006;
|
|
131
|
-
readonly Reaped: 4999;
|
|
132
|
-
};
|
|
133
|
-
export type CloseCode = typeof CloseCode[keyof typeof CloseCode];
|
|
134
|
-
export declare const CloseReason: {
|
|
135
|
-
readonly Ok: "ok";
|
|
136
|
-
readonly Dropped: "dropped";
|
|
137
|
-
readonly Reaped: "reaped";
|
|
138
|
-
readonly Superseded: "superseded";
|
|
139
|
-
};
|
|
140
|
-
export type CloseReason = typeof CloseReason[keyof typeof CloseReason];
|
|
141
|
-
export type SocketOptions = {
|
|
142
|
-
dropThreshold?: number;
|
|
143
|
-
heartbeatInterval?: number;
|
|
144
|
-
maxTickets?: number;
|
|
145
|
-
reclaimTtl?: number;
|
|
146
|
-
ticketTtl?: number;
|
|
147
|
-
onOpen?: (clientId: string) => void;
|
|
148
|
-
onClose?: (clientId: string, reason: CloseReason) => void;
|
|
149
|
-
};
|
|
150
|
-
export type SocketData = {
|
|
151
|
-
clientId: string;
|
|
152
|
-
superseded: boolean;
|
|
153
|
-
reaped: boolean;
|
|
154
|
-
reaperHandle: ReturnType<typeof setTimeout> | null;
|
|
155
|
-
app: unknown;
|
|
156
|
-
};
|
|
157
|
-
export type SocketConnection = {
|
|
158
|
-
data: SocketData;
|
|
159
|
-
send: (data: string) => unknown;
|
|
160
|
-
close: (code?: number, reason?: string) => void;
|
|
161
|
-
};
|
|
162
141
|
export type Server = BunServer<SocketData>;
|
|
163
|
-
export type RouteDefinition = {
|
|
164
|
-
method: HttpMethod;
|
|
165
|
-
path: string;
|
|
166
|
-
chain: Handler | MiddlewareChain;
|
|
167
|
-
};
|
|
168
|
-
export type MetaEntry = {
|
|
169
|
-
path: string;
|
|
170
|
-
middleware: Middleware[];
|
|
171
|
-
};
|
|
172
|
-
export type RouteConfig = {
|
|
173
|
-
routes: RouteDefinition[];
|
|
174
|
-
meta?: MetaEntry[];
|
|
175
|
-
};
|
|
176
|
-
export type AppOptions = {
|
|
177
|
-
hostname?: string;
|
|
178
|
-
mountPath?: string;
|
|
179
|
-
middleware?: Middleware[];
|
|
180
|
-
ws?: boolean | SocketOptions;
|
|
181
|
-
onClose?: () => Promise<void> | void;
|
|
182
|
-
};
|
|
183
142
|
export declare function toSegments(pathString: string): string[];
|
|
184
143
|
export declare function formatError(prefix: string, input: ValidationError): FormattedError;
|
|
185
144
|
export declare function executeMiddlewareChain(req: Request, chain: MiddlewareChain): AsyncHandlerResult;
|
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,
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/core/utils.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,QAAQ,CAAA;AAEtB,OAAO,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,QAAQ,CAAA;AAEtB,OAAO,KAAK,EAAE,UAAU,EAAE,GAAG,EAAE,MAAM,QAAQ,CAAA;AAE7C,MAAM,CAAC,OAAO,UAAU,SAAS,CAC/B,IAAI,EAAE,MAAM,EACZ,IAAI,CAAC,EAAE,UAAU,GAChB,GAAG,CAAA"}
|
package/package.json
CHANGED
package/src/core/index.ts
CHANGED
|
@@ -31,22 +31,29 @@ import type {
|
|
|
31
31
|
Middleware,
|
|
32
32
|
MiddlewareChain,
|
|
33
33
|
EndpointRequest,
|
|
34
|
-
|
|
35
|
-
RouteConfig,
|
|
36
|
-
RouteDefinition,
|
|
37
|
-
SocketCommands,
|
|
38
|
-
SocketOptions,
|
|
39
|
-
AppOptions,
|
|
34
|
+
Handler,
|
|
40
35
|
Server,
|
|
41
36
|
} from './utils'
|
|
42
37
|
|
|
43
38
|
import type {
|
|
39
|
+
SocketCommands,
|
|
40
|
+
} from './utils'
|
|
41
|
+
|
|
42
|
+
import type {
|
|
43
|
+
SocketOptions,
|
|
44
44
|
SocketRoute,
|
|
45
45
|
SocketState,
|
|
46
46
|
} from './socket'
|
|
47
47
|
|
|
48
48
|
export * from './errors'
|
|
49
|
-
export {
|
|
49
|
+
export { ServerCloseSignals } from './socket'
|
|
50
|
+
|
|
51
|
+
export {
|
|
52
|
+
StatusCode,
|
|
53
|
+
HttpMethod,
|
|
54
|
+
SessionType,
|
|
55
|
+
SessionFilter,
|
|
56
|
+
} from './utils'
|
|
50
57
|
|
|
51
58
|
export {
|
|
52
59
|
parseJsonBody,
|
|
@@ -54,26 +61,31 @@ export {
|
|
|
54
61
|
validateSchemas,
|
|
55
62
|
} from './middleware'
|
|
56
63
|
|
|
57
|
-
export type {
|
|
64
|
+
export type {
|
|
65
|
+
ActiveSession,
|
|
66
|
+
ActiveSessions,
|
|
67
|
+
InactiveSession,
|
|
68
|
+
SocketConnection,
|
|
69
|
+
SocketOptions,
|
|
70
|
+
} from './socket'
|
|
58
71
|
|
|
59
72
|
export type {
|
|
60
|
-
AppOptions,
|
|
61
73
|
AsyncHandlerResult,
|
|
62
74
|
BaseRequest,
|
|
75
|
+
CloseSignal,
|
|
63
76
|
EndpointRequest,
|
|
77
|
+
FilterFn,
|
|
64
78
|
FormattedError,
|
|
65
79
|
Handler,
|
|
66
|
-
|
|
80
|
+
HandlerResult,
|
|
67
81
|
Middleware,
|
|
68
82
|
MiddlewareChain,
|
|
69
83
|
NextFn,
|
|
70
|
-
HandlerResult,
|
|
71
84
|
Request,
|
|
72
|
-
RouteConfig,
|
|
73
|
-
RouteDefinition,
|
|
74
85
|
Server,
|
|
75
|
-
|
|
76
|
-
|
|
86
|
+
SessionEntry,
|
|
87
|
+
SocketCommands,
|
|
88
|
+
SocketData,
|
|
77
89
|
WebSocketRequest,
|
|
78
90
|
} from './utils'
|
|
79
91
|
|
|
@@ -83,6 +95,30 @@ export type {
|
|
|
83
95
|
ValidationSchemas,
|
|
84
96
|
} from './middleware'
|
|
85
97
|
|
|
98
|
+
export type RouteDefinition = {
|
|
99
|
+
method: HttpMethod
|
|
100
|
+
path: string
|
|
101
|
+
chain: Handler | MiddlewareChain
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export type MetaEntry = {
|
|
105
|
+
path: string
|
|
106
|
+
middleware: Middleware[]
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export type RouteConfig = {
|
|
110
|
+
routes: RouteDefinition[]
|
|
111
|
+
meta?: MetaEntry[]
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export type AppOptions = {
|
|
115
|
+
hostname?: string
|
|
116
|
+
mountPath?: string
|
|
117
|
+
middleware?: Middleware[]
|
|
118
|
+
ws?: boolean | SocketOptions
|
|
119
|
+
onClose?: () => Promise<void> | void
|
|
120
|
+
}
|
|
121
|
+
|
|
86
122
|
type OutputRoutes = Record<string, string[]>
|
|
87
123
|
type ServerRoutes = Record<string, Record<string, EndpointHandler>>
|
|
88
124
|
|
package/src/core/socket.ts
CHANGED
|
@@ -10,8 +10,8 @@ import {
|
|
|
10
10
|
|
|
11
11
|
import {
|
|
12
12
|
StatusCode,
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
SessionType,
|
|
14
|
+
SessionFilter,
|
|
15
15
|
toSegments,
|
|
16
16
|
formatError,
|
|
17
17
|
executeMiddlewareChain,
|
|
@@ -32,18 +32,15 @@ import type { WebSocketHandler } from 'bun'
|
|
|
32
32
|
|
|
33
33
|
import type {
|
|
34
34
|
AsyncHandlerResult,
|
|
35
|
+
CloseSignal,
|
|
35
36
|
HttpMethod,
|
|
36
37
|
Request,
|
|
38
|
+
FilterFn,
|
|
37
39
|
MiddlewareChain,
|
|
38
40
|
SocketCommands,
|
|
41
|
+
SocketData,
|
|
39
42
|
SessionEntry,
|
|
40
43
|
WebSocketRequest,
|
|
41
|
-
SocketData,
|
|
42
|
-
SocketConnection,
|
|
43
|
-
ActiveSession,
|
|
44
|
-
InactiveSession,
|
|
45
|
-
Session,
|
|
46
|
-
SocketOptions,
|
|
47
44
|
} from './utils'
|
|
48
45
|
|
|
49
46
|
import type {
|
|
@@ -90,6 +87,55 @@ type UpdateTicketRequest = {
|
|
|
90
87
|
}
|
|
91
88
|
}
|
|
92
89
|
|
|
90
|
+
export type SocketConnection = {
|
|
91
|
+
data: SocketData
|
|
92
|
+
send: (data: string) => unknown
|
|
93
|
+
close: (code?: number, reason?: string) => void
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export type ActiveSession = {
|
|
97
|
+
token: string
|
|
98
|
+
ws: SocketConnection
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export type InactiveSession = {
|
|
102
|
+
token: string
|
|
103
|
+
expiresAt: number
|
|
104
|
+
data: unknown
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export type ActiveSessions = ReadonlyMap<string, ActiveSession>
|
|
108
|
+
|
|
109
|
+
export const ServerCloseSignals: Record<string, CloseSignal> = {
|
|
110
|
+
Ok: {
|
|
111
|
+
code: 1000,
|
|
112
|
+
reason: 'ok',
|
|
113
|
+
},
|
|
114
|
+
Reaped: {
|
|
115
|
+
code: 4998,
|
|
116
|
+
reason: 'reaped',
|
|
117
|
+
},
|
|
118
|
+
Superseded: {
|
|
119
|
+
code: 4999,
|
|
120
|
+
reason: 'superseded',
|
|
121
|
+
},
|
|
122
|
+
} as const
|
|
123
|
+
|
|
124
|
+
export type ServerCloseSignals =
|
|
125
|
+
typeof ServerCloseSignals[keyof typeof ServerCloseSignals]
|
|
126
|
+
|
|
127
|
+
export type Session = ActiveSession | InactiveSession
|
|
128
|
+
|
|
129
|
+
export type SocketOptions = {
|
|
130
|
+
dropThreshold?: number
|
|
131
|
+
heartbeatInterval?: number
|
|
132
|
+
maxTickets?: number
|
|
133
|
+
reclaimTtl?: number
|
|
134
|
+
ticketTtl?: number
|
|
135
|
+
onOpen?: (clientId: string) => void
|
|
136
|
+
onClose?: (clientId: string, signal: CloseSignal) => void
|
|
137
|
+
}
|
|
138
|
+
|
|
93
139
|
export type Ticket = {
|
|
94
140
|
clientId: string
|
|
95
141
|
expiresAt: number
|
|
@@ -106,7 +152,7 @@ export type SocketState = {
|
|
|
106
152
|
activeSessions: Map<string, ActiveSession>
|
|
107
153
|
inactiveSessions: Map<string, InactiveSession>
|
|
108
154
|
onOpen: ((clientId: string) => void) | null
|
|
109
|
-
onClose: ((clientId: string,
|
|
155
|
+
onClose: ((clientId: string, signal: CloseSignal) => void) | null
|
|
110
156
|
}
|
|
111
157
|
|
|
112
158
|
export type SocketRoute = {
|
|
@@ -385,16 +431,6 @@ function buildErrorMessage (
|
|
|
385
431
|
})
|
|
386
432
|
}
|
|
387
433
|
|
|
388
|
-
function getCloseReason (ws: SocketConnection, code: number): CloseReason {
|
|
389
|
-
if (ws.data.reaped) {
|
|
390
|
-
return CloseReason.Reaped
|
|
391
|
-
} else if (code === CloseCode.Ok) {
|
|
392
|
-
return CloseReason.Ok
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
return CloseReason.Dropped
|
|
396
|
-
}
|
|
397
|
-
|
|
398
434
|
export function buildSocketState (opts: SocketOptions = {}): SocketState {
|
|
399
435
|
return {
|
|
400
436
|
dropThreshold: opts.dropThreshold ?? 120_000,
|
|
@@ -433,7 +469,10 @@ export function buildSocketServer (
|
|
|
433
469
|
ws.data.reaperHandle = setTimeout(() => {
|
|
434
470
|
ws.data.reaped = true
|
|
435
471
|
|
|
436
|
-
ws.close(
|
|
472
|
+
ws.close(
|
|
473
|
+
ServerCloseSignals.Reaped.code,
|
|
474
|
+
ServerCloseSignals.Reaped.reason,
|
|
475
|
+
)
|
|
437
476
|
}, dropThreshold)
|
|
438
477
|
}
|
|
439
478
|
|
|
@@ -447,10 +486,10 @@ export function buildSocketServer (
|
|
|
447
486
|
}
|
|
448
487
|
}
|
|
449
488
|
|
|
450
|
-
function invokeClose (ws: SocketConnection,
|
|
489
|
+
function invokeClose (ws: SocketConnection, signal: CloseSignal) {
|
|
451
490
|
if (onClose) {
|
|
452
491
|
try {
|
|
453
|
-
onClose(ws.data.clientId,
|
|
492
|
+
onClose(ws.data.clientId, signal)
|
|
454
493
|
} catch (err) {
|
|
455
494
|
console.error(err)
|
|
456
495
|
}
|
|
@@ -467,7 +506,10 @@ export function buildSocketServer (
|
|
|
467
506
|
if (existingSession) {
|
|
468
507
|
existingSession.ws.data.superseded = true
|
|
469
508
|
|
|
470
|
-
existingSession.ws.close(
|
|
509
|
+
existingSession.ws.close(
|
|
510
|
+
ServerCloseSignals.Superseded.code,
|
|
511
|
+
ServerCloseSignals.Superseded.reason,
|
|
512
|
+
)
|
|
471
513
|
}
|
|
472
514
|
|
|
473
515
|
inactiveSessions.delete(ws.data.clientId)
|
|
@@ -494,18 +536,22 @@ export function buildSocketServer (
|
|
|
494
536
|
ws.send(JSON.stringify(welcomeMessage))
|
|
495
537
|
invokeOpen(ws)
|
|
496
538
|
},
|
|
497
|
-
close (ws: SocketConnection, code: number): void {
|
|
539
|
+
close (ws: SocketConnection, code: number, reason: string): void {
|
|
540
|
+
const signal: CloseSignal = {
|
|
541
|
+
code,
|
|
542
|
+
reason,
|
|
543
|
+
}
|
|
544
|
+
|
|
498
545
|
if (ws.data.reaperHandle) {
|
|
499
546
|
clearTimeout(ws.data.reaperHandle)
|
|
500
547
|
}
|
|
501
548
|
|
|
502
549
|
if (ws.data.superseded) {
|
|
503
|
-
invokeClose(ws,
|
|
550
|
+
invokeClose(ws, signal)
|
|
504
551
|
|
|
505
552
|
return
|
|
506
553
|
}
|
|
507
554
|
|
|
508
|
-
const reason = getCloseReason(ws, code)
|
|
509
555
|
const exists = activeSessions.get(ws.data.clientId)
|
|
510
556
|
|
|
511
557
|
if (!exists || exists.ws !== ws) {
|
|
@@ -514,15 +560,15 @@ export function buildSocketServer (
|
|
|
514
560
|
|
|
515
561
|
activeSessions.delete(ws.data.clientId)
|
|
516
562
|
|
|
517
|
-
if (code !==
|
|
563
|
+
if (code !== ServerCloseSignals.Ok.code || ws.data.reaped) {
|
|
518
564
|
inactiveSessions.set(ws.data.clientId, {
|
|
519
565
|
token: exists.token,
|
|
520
566
|
expiresAt: Date.now() + reclaimTtl,
|
|
521
|
-
|
|
567
|
+
data: ws.data.data,
|
|
522
568
|
})
|
|
523
569
|
}
|
|
524
570
|
|
|
525
|
-
invokeClose(ws,
|
|
571
|
+
invokeClose(ws, signal)
|
|
526
572
|
},
|
|
527
573
|
async message (ws: SocketConnection, raw: string | Buffer): Promise<void> {
|
|
528
574
|
const incomingMsg = parseMessage(raw)
|
|
@@ -634,7 +680,7 @@ export function buildSocketHandlers (state: SocketState): SocketEndpoint[] {
|
|
|
634
680
|
ctx.data.superseded = false
|
|
635
681
|
ctx.data.reaped = false
|
|
636
682
|
ctx.data.reaperHandle = null
|
|
637
|
-
ctx.data.
|
|
683
|
+
ctx.data.data = ticket.data
|
|
638
684
|
|
|
639
685
|
const useSocket = validReq.server.upgrade(validReq.raw, ctx)
|
|
640
686
|
|
|
@@ -690,7 +736,7 @@ export function buildSocketHandlers (state: SocketState): SocketEndpoint[] {
|
|
|
690
736
|
throw new UnauthorizedError('Invalid token')
|
|
691
737
|
}
|
|
692
738
|
|
|
693
|
-
const appData = 'ws' in session ? session.ws.data.
|
|
739
|
+
const appData = 'ws' in session ? session.ws.data.data : session.data
|
|
694
740
|
|
|
695
741
|
return Response.json({
|
|
696
742
|
clientId: validReq.params.clientId,
|
|
@@ -731,49 +777,101 @@ export function buildSocketCommands (state: SocketState): SocketCommands {
|
|
|
731
777
|
}
|
|
732
778
|
|
|
733
779
|
return {
|
|
734
|
-
broadcast (event, body) {
|
|
780
|
+
broadcast (event: string, body: unknown) {
|
|
735
781
|
for (const clientId of state.activeSessions.keys()) {
|
|
736
782
|
sendToClient(clientId, event, body)
|
|
737
783
|
}
|
|
738
784
|
},
|
|
739
|
-
send (event, body, fn) {
|
|
785
|
+
send (event: string, body: unknown, fn: FilterFn) {
|
|
740
786
|
let index = 0
|
|
741
787
|
|
|
742
788
|
/* TODO: look into concurrency at some point */
|
|
743
789
|
for (const [clientId, session] of state.activeSessions) {
|
|
744
|
-
|
|
790
|
+
const entry: SessionEntry = {
|
|
791
|
+
clientId,
|
|
792
|
+
type: SessionType.Active,
|
|
793
|
+
data: session.ws.data.data,
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
if (fn(entry, index)) {
|
|
745
797
|
sendToClient(clientId, event, body)
|
|
746
798
|
}
|
|
747
799
|
|
|
748
800
|
index += 1
|
|
749
801
|
}
|
|
750
802
|
},
|
|
751
|
-
drop (
|
|
803
|
+
drop (signal: CloseSignal, fn: FilterFn) {
|
|
804
|
+
const { code, reason } = signal
|
|
805
|
+
|
|
752
806
|
let index = 0
|
|
753
807
|
|
|
808
|
+
if (!Number.isInteger(code) || code < 4000 || code > 4099) {
|
|
809
|
+
throw new RangeError(
|
|
810
|
+
`Signal code must be an integer in [4000, 4099], got ${code}`,
|
|
811
|
+
)
|
|
812
|
+
}
|
|
813
|
+
|
|
754
814
|
for (const [clientId, session] of state.activeSessions) {
|
|
755
|
-
|
|
815
|
+
const entry: SessionEntry = {
|
|
816
|
+
clientId,
|
|
817
|
+
type: SessionType.Active,
|
|
818
|
+
data: session.ws.data.data,
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
if (fn(entry, index)) {
|
|
756
822
|
session.ws.close(code, reason)
|
|
757
823
|
}
|
|
758
824
|
|
|
759
825
|
index += 1
|
|
760
826
|
}
|
|
761
827
|
},
|
|
762
|
-
query (
|
|
828
|
+
query (
|
|
829
|
+
fn: FilterFn,
|
|
830
|
+
filter: SessionFilter = SessionFilter.Active,
|
|
831
|
+
) {
|
|
763
832
|
const results: SessionEntry[] = []
|
|
764
|
-
let index = 0
|
|
765
833
|
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
834
|
+
function processSessions (
|
|
835
|
+
type: SessionType,
|
|
836
|
+
filter: SessionFilter,
|
|
837
|
+
startIndex: number,
|
|
838
|
+
): number {
|
|
839
|
+
let index = startIndex
|
|
840
|
+
|
|
841
|
+
const sessions = type === SessionType.Active
|
|
842
|
+
? state.activeSessions
|
|
843
|
+
: state.inactiveSessions
|
|
844
|
+
|
|
845
|
+
if (type !== filter && filter !== SessionFilter.All) {
|
|
846
|
+
return index
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
for (const [clientId, session] of sessions) {
|
|
850
|
+
const data = 'ws' in session ? session.ws.data.data : session.data
|
|
851
|
+
|
|
852
|
+
const entry: SessionEntry = {
|
|
769
853
|
clientId,
|
|
770
|
-
|
|
771
|
-
|
|
854
|
+
type,
|
|
855
|
+
data,
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
if (fn(entry, index)) {
|
|
859
|
+
results.push(entry)
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
index += 1
|
|
772
863
|
}
|
|
773
864
|
|
|
774
|
-
index
|
|
865
|
+
return index
|
|
775
866
|
}
|
|
776
867
|
|
|
868
|
+
let index = 0
|
|
869
|
+
|
|
870
|
+
sweepInactiveSessions(state)
|
|
871
|
+
|
|
872
|
+
index = processSessions(SessionType.Active, filter, index)
|
|
873
|
+
index = processSessions(SessionType.Inactive, filter, index)
|
|
874
|
+
|
|
777
875
|
return results
|
|
778
876
|
},
|
|
779
877
|
}
|
package/src/core/utils.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { ErrorObject } from 'ajv'
|
|
2
2
|
import type { BunRequest, Server as BunServer } from 'bun'
|
|
3
3
|
|
|
4
|
+
export type TimeoutHandle = ReturnType<typeof setTimeout>
|
|
5
|
+
|
|
4
6
|
export const HttpMethod = {
|
|
5
7
|
Head: 'HEAD',
|
|
6
8
|
Get: 'GET',
|
|
@@ -107,36 +109,50 @@ export type Handler = (
|
|
|
107
109
|
|
|
108
110
|
export type MiddlewareChain = (Middleware | Handler)[]
|
|
109
111
|
|
|
110
|
-
export type
|
|
111
|
-
|
|
112
|
-
|
|
112
|
+
export type CloseSignal = {
|
|
113
|
+
code: number
|
|
114
|
+
reason: string
|
|
113
115
|
}
|
|
114
116
|
|
|
115
|
-
export type
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
117
|
+
export type SocketData = {
|
|
118
|
+
clientId: string
|
|
119
|
+
superseded: boolean
|
|
120
|
+
reaped: boolean
|
|
121
|
+
reaperHandle: TimeoutHandle | null
|
|
122
|
+
data: unknown
|
|
119
123
|
}
|
|
120
124
|
|
|
121
|
-
export
|
|
122
|
-
|
|
125
|
+
export const SessionType = {
|
|
126
|
+
Active: 'active',
|
|
127
|
+
Inactive: 'inactive',
|
|
128
|
+
} as const
|
|
129
|
+
|
|
130
|
+
export type SessionType = typeof SessionType[keyof typeof SessionType]
|
|
131
|
+
|
|
132
|
+
export const SessionFilter = {
|
|
133
|
+
Active: 'active',
|
|
134
|
+
Inactive: 'inactive',
|
|
135
|
+
All: 'all',
|
|
136
|
+
} as const
|
|
137
|
+
|
|
138
|
+
export type SessionFilter = typeof SessionFilter[keyof typeof SessionFilter]
|
|
123
139
|
|
|
124
140
|
export type SessionEntry = {
|
|
125
141
|
clientId: string
|
|
126
|
-
|
|
142
|
+
type: SessionType
|
|
143
|
+
data: unknown
|
|
127
144
|
}
|
|
128
145
|
|
|
129
146
|
export type FilterFn = (
|
|
130
|
-
|
|
131
|
-
data: unknown,
|
|
147
|
+
session: SessionEntry,
|
|
132
148
|
index: number,
|
|
133
149
|
) => boolean
|
|
134
150
|
|
|
135
151
|
export type SocketCommands = {
|
|
136
152
|
broadcast: (event: string, body: unknown) => void
|
|
137
153
|
send: (event: string, body: unknown, fn: FilterFn) => void
|
|
138
|
-
drop: (
|
|
139
|
-
query: (fn: FilterFn) => SessionEntry[]
|
|
154
|
+
drop: (signal: CloseSignal, fn: FilterFn) => void
|
|
155
|
+
query: (fn: FilterFn, filter?: SessionFilter) => SessionEntry[]
|
|
140
156
|
}
|
|
141
157
|
|
|
142
158
|
export type BaseRequest = {
|
|
@@ -161,73 +177,8 @@ export type WebSocketRequest = BaseRequest & {
|
|
|
161
177
|
|
|
162
178
|
export type Request = EndpointRequest | WebSocketRequest
|
|
163
179
|
|
|
164
|
-
export const CloseCode = {
|
|
165
|
-
Ok: 1000,
|
|
166
|
-
Abnormal: 1006,
|
|
167
|
-
Reaped: 4999,
|
|
168
|
-
} as const
|
|
169
|
-
|
|
170
|
-
export type CloseCode = typeof CloseCode[keyof typeof CloseCode]
|
|
171
|
-
|
|
172
|
-
export const CloseReason = {
|
|
173
|
-
Ok: 'ok',
|
|
174
|
-
Dropped: 'dropped',
|
|
175
|
-
Reaped: 'reaped',
|
|
176
|
-
Superseded: 'superseded',
|
|
177
|
-
} as const
|
|
178
|
-
|
|
179
|
-
export type CloseReason = typeof CloseReason[keyof typeof CloseReason]
|
|
180
|
-
|
|
181
|
-
export type SocketOptions = {
|
|
182
|
-
dropThreshold?: number
|
|
183
|
-
heartbeatInterval?: number
|
|
184
|
-
maxTickets?: number
|
|
185
|
-
reclaimTtl?: number
|
|
186
|
-
ticketTtl?: number
|
|
187
|
-
onOpen?: (clientId: string) => void
|
|
188
|
-
onClose?: (clientId: string, reason: CloseReason) => void
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
export type SocketData = {
|
|
192
|
-
clientId: string
|
|
193
|
-
superseded: boolean
|
|
194
|
-
reaped: boolean
|
|
195
|
-
reaperHandle: ReturnType<typeof setTimeout> | null
|
|
196
|
-
app: unknown
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
export type SocketConnection = {
|
|
200
|
-
data: SocketData
|
|
201
|
-
send: (data: string) => unknown
|
|
202
|
-
close: (code?: number, reason?: string) => void
|
|
203
|
-
}
|
|
204
|
-
|
|
205
180
|
export type Server = BunServer<SocketData>
|
|
206
181
|
|
|
207
|
-
export type RouteDefinition = {
|
|
208
|
-
method: HttpMethod
|
|
209
|
-
path: string
|
|
210
|
-
chain: Handler | MiddlewareChain
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
export type MetaEntry = {
|
|
214
|
-
path: string
|
|
215
|
-
middleware: Middleware[]
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
export type RouteConfig = {
|
|
219
|
-
routes: RouteDefinition[]
|
|
220
|
-
meta?: MetaEntry[]
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
export type AppOptions = {
|
|
224
|
-
hostname?: string
|
|
225
|
-
mountPath?: string
|
|
226
|
-
middleware?: Middleware[]
|
|
227
|
-
ws?: boolean | SocketOptions
|
|
228
|
-
onClose?: () => Promise<void> | void
|
|
229
|
-
}
|
|
230
|
-
|
|
231
182
|
export function toSegments (pathString: string): string[] {
|
|
232
183
|
const [pathname] = String(pathString).split('?')
|
|
233
184
|
const segments = pathname.split('/')
|