sleepy-serv 0.9.0 → 0.10.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 +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/middleware.d.ts.map +1 -1
- package/dist/socket.d.ts +17 -22
- package/dist/socket.d.ts.map +1 -1
- package/dist/utils.d.ts +43 -4
- package/dist/utils.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +45 -11
- package/src/middleware.ts +22 -26
- package/src/socket.ts +180 -95
- package/src/utils.ts +69 -18
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ A directory-driven web server designed for REST-ful applications
|
|
|
5
5
|
## Important Notes
|
|
6
6
|
|
|
7
7
|
- This package requires [`bun.sh`](https://bun.sh) instead of NodeJS to run.
|
|
8
|
-
- This project requires bun v1.
|
|
8
|
+
- This project requires bun v1.4 or higher
|
|
9
9
|
|
|
10
10
|
## Getting Started
|
|
11
11
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import type { AppOptions, Server } from './utils';
|
|
2
2
|
import type { SocketCommands } from './socket';
|
|
3
3
|
export * from './errors';
|
|
4
|
+
export { StatusCode, CloseCode, CloseReason } from './utils';
|
|
4
5
|
export { parseJsonBody, setValidationFormats, validateSchemas, } from './middleware';
|
|
5
|
-
export { HttpMethod, StatusCode } from './utils';
|
|
6
|
-
export type { AppOptions, EndpointRequest, FormattedError, Middleware, NextFn, Request, Server, SocketOptions, WebSocketRequest, } from './utils';
|
|
7
|
-
export type { FormatterField, FormatterSchema, ValidationSchemas, } from './middleware';
|
|
8
6
|
export type { SocketCommands } from './socket';
|
|
7
|
+
export type { HttpMethod, ActiveSessions } from './utils';
|
|
8
|
+
export type { AppOptions, AsyncHandlerResult, BaseRequest, EndpointRequest, FormattedError, Handler, Middleware, MiddlewareChain, NextFn, HandlerResult, Request, Server, SocketConnection, SocketOptions, WebSocketRequest, } from './utils';
|
|
9
|
+
export type { FormatterField, FormatterSchema, ValidationSchemas, } from './middleware';
|
|
9
10
|
type OutputRoutes = Record<string, string[]>;
|
|
10
11
|
type CloseFn = (force?: boolean) => Promise<void>;
|
|
11
12
|
export type App = {
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AA4BA,OAAO,KAAK,EAOV,UAAU,EACV,MAAM,EACP,MAAM,SAAS,CAAA;AAEhB,OAAO,KAAK,EAGV,cAAc,EACf,MAAM,UAAU,CAAA;AAEjB,cAAc,UAAU,CAAA;AACxB,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AAE5D,OAAO,EACL,aAAa,EACb,oBAAoB,EACpB,eAAe,GAChB,MAAM,cAAc,CAAA;AAErB,YAAY,EAAE,cAAc,EAAE,MAAM,UAAU,CAAA;AAC9C,YAAY,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAEzD,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,QAAQ,EAAE,cAAc,CAAA;IACxB,MAAM,EAAE,YAAY,CAAA;IACpB,KAAK,EAAE,OAAO,CAAA;CACf,CAAA;AAyaD,wBAAsB,SAAS,CAC7B,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,IAAI,GAAE,UAAe,GACpB,OAAO,CAAC,GAAG,CAAC,CAad"}
|
package/dist/middleware.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../src/middleware.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,EAAoB,MAAM,KAAK,CAAA;
|
|
1
|
+
{"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../src/middleware.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,EAAoB,MAAM,KAAK,CAAA;AAE3D,OAAO,KAAK,EAIV,UAAU,EAGX,MAAM,SAAS,CAAA;AAEhB,MAAM,MAAM,cAAc,GAAG;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,OAAO,CAAA;CACf,CAAA;AAED,MAAM,MAAM,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAA;AAE5D,MAAM,MAAM,iBAAiB,GAAG;IAC9B,OAAO,CAAC,EAAE,eAAe,CAAA;IACzB,MAAM,CAAC,EAAE,eAAe,CAAA;IACxB,KAAK,CAAC,EAAE,eAAe,CAAA;IACvB,IAAI,CAAC,EAAE,MAAM,CAAA;CACd,CAAA;AAED,MAAM,MAAM,SAAS,GAAG,MAAM,iBAAiB,CAAA;AAmF/C,wBAAgB,aAAa,IAAK,UAAU,CAoB3C;AAED,wBAAgB,oBAAoB,CAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAU3E;AAID,wBAAgB,2BAA2B,IAAK,IAAI,CAGnD;AAcD,wBAAgB,eAAe,CAAE,OAAO,EAAE,iBAAiB,GAAG,UAAU,CAgCvE"}
|
package/dist/socket.d.ts
CHANGED
|
@@ -1,25 +1,20 @@
|
|
|
1
|
+
import { CloseReason } from './utils';
|
|
1
2
|
import type { WebSocketHandler } from 'bun';
|
|
2
|
-
import type { HttpMethod, Request,
|
|
3
|
+
import type { AsyncHandlerResult, HttpMethod, Request, MiddlewareChain, SocketData, ActiveSession, InactiveSession, AppOptions } from './utils';
|
|
4
|
+
type SocketHandler = (req: Request, res: unknown) => AsyncHandlerResult;
|
|
5
|
+
type FilterFn = (clientId: string, data: unknown) => boolean;
|
|
6
|
+
type SocketEndpoint = {
|
|
7
|
+
method: HttpMethod;
|
|
8
|
+
path: string;
|
|
9
|
+
handler: SocketHandler;
|
|
10
|
+
};
|
|
3
11
|
export type Ticket = {
|
|
4
12
|
clientId: string;
|
|
5
13
|
expiresAt: number;
|
|
14
|
+
data: unknown;
|
|
6
15
|
};
|
|
7
|
-
export type SocketConnection = {
|
|
8
|
-
data: SocketData;
|
|
9
|
-
send: (data: string) => unknown;
|
|
10
|
-
close: () => void;
|
|
11
|
-
};
|
|
12
|
-
export type ActiveSession = {
|
|
13
|
-
token: string;
|
|
14
|
-
ws: SocketConnection;
|
|
15
|
-
};
|
|
16
|
-
export type InactiveSession = {
|
|
17
|
-
token: string;
|
|
18
|
-
expiresAt: number;
|
|
19
|
-
};
|
|
20
|
-
export type Session = ActiveSession | InactiveSession;
|
|
21
16
|
export type SocketState = {
|
|
22
|
-
|
|
17
|
+
dropThreshold: number;
|
|
23
18
|
heartbeatInterval: number;
|
|
24
19
|
maxTickets: number;
|
|
25
20
|
reclaimTtl: number;
|
|
@@ -27,24 +22,24 @@ export type SocketState = {
|
|
|
27
22
|
tickets: Map<string, Ticket>;
|
|
28
23
|
activeSessions: Map<string, ActiveSession>;
|
|
29
24
|
inactiveSessions: Map<string, InactiveSession>;
|
|
25
|
+
onOpen: ((clientId: string) => void) | null;
|
|
26
|
+
onClose: ((clientId: string, reason: CloseReason) => void) | null;
|
|
30
27
|
};
|
|
31
28
|
export type SocketRoute = {
|
|
32
29
|
method: HttpMethod;
|
|
33
30
|
path: string;
|
|
34
31
|
segments: string[];
|
|
35
|
-
chain:
|
|
36
|
-
};
|
|
37
|
-
export type SocketEndpoint = {
|
|
38
|
-
method: HttpMethod;
|
|
39
|
-
path: string;
|
|
40
|
-
handler: (req: Request, res: unknown) => Response;
|
|
32
|
+
chain: MiddlewareChain;
|
|
41
33
|
};
|
|
42
34
|
export type SocketCommands = {
|
|
43
35
|
send: (clientId: string, event: string, body: unknown) => void;
|
|
36
|
+
sendToGroup: (fn: FilterFn, event: string, body: unknown) => void;
|
|
44
37
|
broadcast: (event: string, body: unknown) => void;
|
|
38
|
+
drop: (clientId: string, code?: number, reason?: string) => void;
|
|
45
39
|
};
|
|
46
40
|
export declare function buildSocketState(opts?: AppOptions): SocketState;
|
|
47
41
|
export declare function buildSocketServer(routes: SocketRoute[], state: SocketState): WebSocketHandler<SocketData>;
|
|
48
42
|
export declare function buildSocketHandlers(state: SocketState): SocketEndpoint[];
|
|
49
43
|
export declare function buildSocketCommands(state: SocketState): SocketCommands;
|
|
44
|
+
export {};
|
|
50
45
|
//# sourceMappingURL=socket.d.ts.map
|
package/dist/socket.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"socket.d.ts","sourceRoot":"","sources":["../src/socket.ts"],"names":[],"mappings":"
|
|
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,EAEf,UAAU,EAEV,aAAa,EAEb,eAAe,EAEf,UAAU,EACX,MAAM,SAAS,CAAA;AAShB,KAAK,aAAa,GAAG,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,KAAK,kBAAkB,CAAA;AACvE,KAAK,QAAQ,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,KAAK,OAAO,CAAA;AAY5D,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;AAED,MAAM,MAAM,cAAc,GAAG;IAC3B,IAAI,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IAC9D,WAAW,EAAE,CAAC,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACjE,SAAS,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACjD,IAAI,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;CACjE,CAAA;AAmTD,wBAAgB,gBAAgB,CAAE,IAAI,GAAE,UAAe,GAAG,WAAW,CAapE;AAED,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,WAAW,EAAE,EACrB,KAAK,EAAE,WAAW,GACjB,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,CA6CvE"}
|
package/dist/utils.d.ts
CHANGED
|
@@ -79,7 +79,23 @@ export type FormattedError = {
|
|
|
79
79
|
path: string;
|
|
80
80
|
message: string;
|
|
81
81
|
};
|
|
82
|
-
export type
|
|
82
|
+
export type AsyncHandlerResult = Promise<Response>;
|
|
83
|
+
export type HandlerResult = Response | AsyncHandlerResult;
|
|
84
|
+
export type NextFn = (data?: unknown) => HandlerResult;
|
|
85
|
+
export type Middleware = (req: Request, res: unknown, next: NextFn) => HandlerResult;
|
|
86
|
+
export type Handler = (req: Request, res: unknown) => HandlerResult;
|
|
87
|
+
export type MiddlewareChain = (Middleware | Handler)[];
|
|
88
|
+
export type ActiveSession = {
|
|
89
|
+
token: string;
|
|
90
|
+
ws: SocketConnection;
|
|
91
|
+
};
|
|
92
|
+
export type InactiveSession = {
|
|
93
|
+
token: string;
|
|
94
|
+
expiresAt: number;
|
|
95
|
+
app: unknown;
|
|
96
|
+
};
|
|
97
|
+
export type ActiveSessions = ReadonlyMap<string, ActiveSession>;
|
|
98
|
+
export type Session = ActiveSession | InactiveSession;
|
|
83
99
|
export type BaseRequest = {
|
|
84
100
|
method: HttpMethod;
|
|
85
101
|
route: string;
|
|
@@ -87,6 +103,9 @@ export type BaseRequest = {
|
|
|
87
103
|
params: Record<string, string>;
|
|
88
104
|
query: Record<string, unknown>;
|
|
89
105
|
json: () => Promise<unknown>;
|
|
106
|
+
ws: {
|
|
107
|
+
active: ActiveSessions;
|
|
108
|
+
};
|
|
90
109
|
};
|
|
91
110
|
export type EndpointRequest = BaseRequest & {
|
|
92
111
|
raw: BunRequest;
|
|
@@ -97,19 +116,39 @@ export type WebSocketRequest = BaseRequest & {
|
|
|
97
116
|
clientId: string;
|
|
98
117
|
};
|
|
99
118
|
export type Request = EndpointRequest | WebSocketRequest;
|
|
100
|
-
export
|
|
119
|
+
export declare const CloseCode: {
|
|
120
|
+
readonly Ok: 1000;
|
|
121
|
+
readonly Abnormal: 1006;
|
|
122
|
+
readonly Reaped: 4999;
|
|
123
|
+
};
|
|
124
|
+
export type CloseCode = typeof CloseCode[keyof typeof CloseCode];
|
|
125
|
+
export declare const CloseReason: {
|
|
126
|
+
readonly Ok: "ok";
|
|
127
|
+
readonly Dropped: "dropped";
|
|
128
|
+
readonly Reaped: "reaped";
|
|
129
|
+
readonly Superseded: "superseded";
|
|
130
|
+
};
|
|
131
|
+
export type CloseReason = typeof CloseReason[keyof typeof CloseReason];
|
|
101
132
|
export type SocketOptions = {
|
|
102
|
-
|
|
133
|
+
dropThreshold?: number;
|
|
103
134
|
heartbeatInterval?: number;
|
|
104
135
|
maxTickets?: number;
|
|
105
136
|
reclaimTtl?: number;
|
|
106
137
|
ticketTtl?: number;
|
|
138
|
+
onOpen?: (clientId: string) => void;
|
|
139
|
+
onClose?: (clientId: string, reason: CloseReason) => void;
|
|
107
140
|
};
|
|
108
141
|
export type SocketData = {
|
|
109
142
|
clientId: string;
|
|
110
143
|
superseded: boolean;
|
|
111
144
|
reaped: boolean;
|
|
112
145
|
reaperHandle: ReturnType<typeof setTimeout> | null;
|
|
146
|
+
app: unknown;
|
|
147
|
+
};
|
|
148
|
+
export type SocketConnection = {
|
|
149
|
+
data: SocketData;
|
|
150
|
+
send: (data: string) => unknown;
|
|
151
|
+
close: (code?: number, reason?: string) => void;
|
|
113
152
|
};
|
|
114
153
|
export type Server = BunServer<SocketData>;
|
|
115
154
|
export type AppOptions = {
|
|
@@ -121,5 +160,5 @@ export type AppOptions = {
|
|
|
121
160
|
};
|
|
122
161
|
export declare function toSegments(pathString: string): string[];
|
|
123
162
|
export declare function formatError(prefix: string, input: ValidationError): FormattedError;
|
|
124
|
-
export declare function executeMiddlewareChain(req: Request, chain:
|
|
163
|
+
export declare function executeMiddlewareChain(req: Request, chain: MiddlewareChain): AsyncHandlerResult;
|
|
125
164
|
//# sourceMappingURL=utils.d.ts.map
|
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,MAAM,GAAG,CAAC,IAAI,CAAC,EAAE,OAAO,KAAK,
|
|
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
|
@@ -4,7 +4,11 @@ import querystring from 'querystring'
|
|
|
4
4
|
import readline from 'node:readline'
|
|
5
5
|
|
|
6
6
|
import { stdin, stdout } from 'node:process'
|
|
7
|
-
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
toSegments,
|
|
10
|
+
executeMiddlewareChain,
|
|
11
|
+
} from './utils'
|
|
8
12
|
|
|
9
13
|
import {
|
|
10
14
|
buildSocketState,
|
|
@@ -23,9 +27,12 @@ import {
|
|
|
23
27
|
import type { BunRequest } from 'bun'
|
|
24
28
|
|
|
25
29
|
import type {
|
|
30
|
+
AsyncHandlerResult,
|
|
26
31
|
HttpMethod,
|
|
27
32
|
Middleware,
|
|
33
|
+
MiddlewareChain,
|
|
28
34
|
EndpointRequest,
|
|
35
|
+
ActiveSessions,
|
|
29
36
|
AppOptions,
|
|
30
37
|
Server,
|
|
31
38
|
} from './utils'
|
|
@@ -37,6 +44,7 @@ import type {
|
|
|
37
44
|
} from './socket'
|
|
38
45
|
|
|
39
46
|
export * from './errors'
|
|
47
|
+
export { StatusCode, CloseCode, CloseReason } from './utils'
|
|
40
48
|
|
|
41
49
|
export {
|
|
42
50
|
parseJsonBody,
|
|
@@ -44,16 +52,23 @@ export {
|
|
|
44
52
|
validateSchemas,
|
|
45
53
|
} from './middleware'
|
|
46
54
|
|
|
47
|
-
export {
|
|
55
|
+
export type { SocketCommands } from './socket'
|
|
56
|
+
export type { HttpMethod, ActiveSessions } from './utils'
|
|
48
57
|
|
|
49
58
|
export type {
|
|
50
59
|
AppOptions,
|
|
60
|
+
AsyncHandlerResult,
|
|
61
|
+
BaseRequest,
|
|
51
62
|
EndpointRequest,
|
|
52
63
|
FormattedError,
|
|
64
|
+
Handler,
|
|
53
65
|
Middleware,
|
|
66
|
+
MiddlewareChain,
|
|
54
67
|
NextFn,
|
|
68
|
+
HandlerResult,
|
|
55
69
|
Request,
|
|
56
70
|
Server,
|
|
71
|
+
SocketConnection,
|
|
57
72
|
SocketOptions,
|
|
58
73
|
WebSocketRequest,
|
|
59
74
|
} from './utils'
|
|
@@ -64,15 +79,13 @@ export type {
|
|
|
64
79
|
ValidationSchemas,
|
|
65
80
|
} from './middleware'
|
|
66
81
|
|
|
67
|
-
export type { SocketCommands } from './socket'
|
|
68
|
-
|
|
69
82
|
type OutputRoutes = Record<string, string[]>
|
|
70
83
|
type ServerRoutes = Record<string, Record<string, EndpointHandler>>
|
|
71
84
|
|
|
72
85
|
type EndpointHandler = (
|
|
73
86
|
bunReq: BunRequest,
|
|
74
87
|
server: Server,
|
|
75
|
-
) =>
|
|
88
|
+
) => AsyncHandlerResult
|
|
76
89
|
|
|
77
90
|
type DirEntry = {
|
|
78
91
|
path: string
|
|
@@ -89,7 +102,7 @@ type RoutePath = {
|
|
|
89
102
|
type ChainRoute = {
|
|
90
103
|
method: HttpMethod
|
|
91
104
|
path: string
|
|
92
|
-
chain:
|
|
105
|
+
chain: MiddlewareChain
|
|
93
106
|
}
|
|
94
107
|
|
|
95
108
|
type ModuleRoute = {
|
|
@@ -154,10 +167,20 @@ function defaultMethodMap (): Record<string, EndpointHandler> {
|
|
|
154
167
|
function buildEndpointRequest (
|
|
155
168
|
bunReq: BunRequest,
|
|
156
169
|
server: Server,
|
|
170
|
+
activeSessions: ActiveSessions,
|
|
157
171
|
): EndpointRequest {
|
|
158
172
|
const url = new URL(bunReq.url)
|
|
159
173
|
const qs = url.search.replace('?', '')
|
|
160
|
-
|
|
174
|
+
|
|
175
|
+
let bodyPromise: Promise<unknown> | null = null
|
|
176
|
+
|
|
177
|
+
const json = (): Promise<unknown> => {
|
|
178
|
+
if (!bodyPromise) {
|
|
179
|
+
bodyPromise = bunReq.json()
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return bodyPromise
|
|
183
|
+
}
|
|
161
184
|
|
|
162
185
|
return {
|
|
163
186
|
method: bunReq.method as HttpMethod,
|
|
@@ -168,6 +191,9 @@ function buildEndpointRequest (
|
|
|
168
191
|
raw: bunReq,
|
|
169
192
|
server,
|
|
170
193
|
json,
|
|
194
|
+
ws: {
|
|
195
|
+
active: activeSessions,
|
|
196
|
+
},
|
|
171
197
|
}
|
|
172
198
|
}
|
|
173
199
|
|
|
@@ -279,9 +305,10 @@ function buildRoutePaths (
|
|
|
279
305
|
.join('') || '/'
|
|
280
306
|
|
|
281
307
|
const metaMiddlewarePath = selectMetaPaths(metadata, modulePath)
|
|
308
|
+
const rawMethod = segments[lastIndex].toUpperCase()
|
|
282
309
|
|
|
283
310
|
return {
|
|
284
|
-
method:
|
|
311
|
+
method: rawMethod as HttpMethod,
|
|
285
312
|
path: joinedPath,
|
|
286
313
|
metaMiddlewarePath,
|
|
287
314
|
modulePath,
|
|
@@ -376,10 +403,17 @@ function buildSocketRoutes (mergedRoutes: ChainRoute[]): SocketRoute[] {
|
|
|
376
403
|
}))
|
|
377
404
|
}
|
|
378
405
|
|
|
379
|
-
function buildModuleRoutes (
|
|
406
|
+
function buildModuleRoutes (
|
|
407
|
+
socketRoutes: SocketRoute[],
|
|
408
|
+
state: SocketState,
|
|
409
|
+
): ModuleRoute[] {
|
|
380
410
|
return socketRoutes.map(route => {
|
|
381
411
|
const handler: EndpointHandler = async (bunReq, server) => {
|
|
382
|
-
const req = buildEndpointRequest(
|
|
412
|
+
const req = buildEndpointRequest(
|
|
413
|
+
bunReq,
|
|
414
|
+
server,
|
|
415
|
+
state.activeSessions,
|
|
416
|
+
)
|
|
383
417
|
|
|
384
418
|
return executeMiddlewareChain(req, route.chain)
|
|
385
419
|
}
|
|
@@ -440,7 +474,7 @@ async function buildRoutes (
|
|
|
440
474
|
)
|
|
441
475
|
|
|
442
476
|
const socketRoutes = buildSocketRoutes(mergedRoutes)
|
|
443
|
-
const moduleRoutes = buildModuleRoutes(socketRoutes)
|
|
477
|
+
const moduleRoutes = buildModuleRoutes(socketRoutes, state)
|
|
444
478
|
const serverRoutes = buildServerRoutes(moduleRoutes)
|
|
445
479
|
const outputRoutes = buildOutputRoutes(moduleRoutes)
|
|
446
480
|
|
package/src/middleware.ts
CHANGED
|
@@ -10,7 +10,15 @@ import {
|
|
|
10
10
|
} from './errors'
|
|
11
11
|
|
|
12
12
|
import type { Format, Schema, ValidateFunction } from 'ajv'
|
|
13
|
-
|
|
13
|
+
|
|
14
|
+
import type {
|
|
15
|
+
AsyncHandlerResult,
|
|
16
|
+
FormattedError,
|
|
17
|
+
HandlerResult,
|
|
18
|
+
Middleware,
|
|
19
|
+
NextFn,
|
|
20
|
+
Request,
|
|
21
|
+
} from './utils'
|
|
14
22
|
|
|
15
23
|
export type FormatterField = {
|
|
16
24
|
type: string
|
|
@@ -28,14 +36,6 @@ export type ValidationSchemas = {
|
|
|
28
36
|
|
|
29
37
|
export type SchemaKey = keyof ValidationSchemas
|
|
30
38
|
|
|
31
|
-
function requireNext (next: NextFn | null): NextFn {
|
|
32
|
-
if (!next) {
|
|
33
|
-
throw new TypeError('Middleware cannot be the last entry in a chain')
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
return next
|
|
37
|
-
}
|
|
38
|
-
|
|
39
39
|
let _schemasCompiled = false
|
|
40
40
|
let _customFormats: Record<string, Format> | null = null
|
|
41
41
|
|
|
@@ -121,12 +121,12 @@ export function parseJsonBody (): Middleware {
|
|
|
121
121
|
return async (
|
|
122
122
|
req: Request,
|
|
123
123
|
res: unknown,
|
|
124
|
-
next: NextFn
|
|
125
|
-
):
|
|
124
|
+
next: NextFn,
|
|
125
|
+
): AsyncHandlerResult => {
|
|
126
126
|
const contentType = req.headers.get('content-type')
|
|
127
127
|
|
|
128
128
|
if (!contentType) {
|
|
129
|
-
return
|
|
129
|
+
return next(res)
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
if (!contentType.startsWith('application/json')) {
|
|
@@ -135,13 +135,11 @@ export function parseJsonBody (): Middleware {
|
|
|
135
135
|
|
|
136
136
|
const body = await parseBody(req)
|
|
137
137
|
|
|
138
|
-
return
|
|
138
|
+
return next(body)
|
|
139
139
|
}
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
-
export function setValidationFormats (
|
|
143
|
-
formats: Record<string, Format>,
|
|
144
|
-
): void {
|
|
142
|
+
export function setValidationFormats (formats: Record<string, Format>): void {
|
|
145
143
|
if (_customFormats) {
|
|
146
144
|
console.warn('setValidationFormats() - already initialized')
|
|
147
145
|
}
|
|
@@ -172,9 +170,7 @@ function buildValidationSource (
|
|
|
172
170
|
}
|
|
173
171
|
}
|
|
174
172
|
|
|
175
|
-
export function validateSchemas (
|
|
176
|
-
schemas: ValidationSchemas,
|
|
177
|
-
): Middleware {
|
|
173
|
+
export function validateSchemas (schemas: ValidationSchemas): Middleware {
|
|
178
174
|
const entries = compileSchemas(schemas)
|
|
179
175
|
|
|
180
176
|
_schemasCompiled = true
|
|
@@ -182,14 +178,14 @@ export function validateSchemas (
|
|
|
182
178
|
return (
|
|
183
179
|
req: Request,
|
|
184
180
|
res: unknown,
|
|
185
|
-
next: NextFn
|
|
186
|
-
):
|
|
181
|
+
next: NextFn,
|
|
182
|
+
): HandlerResult => {
|
|
187
183
|
const source = buildValidationSource(req, res)
|
|
188
184
|
|
|
189
|
-
const errors = entries.reduce((
|
|
190
|
-
|
|
191
|
-
validator,
|
|
192
|
-
|
|
185
|
+
const errors = entries.reduce((
|
|
186
|
+
accum: FormattedError[],
|
|
187
|
+
[key, validator]: [SchemaKey, ValidateFunction],
|
|
188
|
+
) => {
|
|
193
189
|
const valid = validator(source[key])
|
|
194
190
|
|
|
195
191
|
return !valid
|
|
@@ -204,6 +200,6 @@ export function validateSchemas (
|
|
|
204
200
|
throw new UnprocessableContentError(errors)
|
|
205
201
|
}
|
|
206
202
|
|
|
207
|
-
return
|
|
203
|
+
return next(res)
|
|
208
204
|
}
|
|
209
205
|
}
|
package/src/socket.ts
CHANGED
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
|
|
11
11
|
import {
|
|
12
12
|
StatusCode,
|
|
13
|
+
CloseCode,
|
|
14
|
+
CloseReason,
|
|
13
15
|
toSegments,
|
|
14
16
|
formatError,
|
|
15
17
|
executeMiddlewareChain,
|
|
@@ -17,6 +19,7 @@ import {
|
|
|
17
19
|
|
|
18
20
|
import {
|
|
19
21
|
RequestError,
|
|
22
|
+
BadRequestError,
|
|
20
23
|
NotFoundError,
|
|
21
24
|
UnauthorizedError,
|
|
22
25
|
MethodNotAllowedError,
|
|
@@ -29,11 +32,17 @@ import type { ValidateFunction } from 'ajv'
|
|
|
29
32
|
import type { WebSocketHandler } from 'bun'
|
|
30
33
|
|
|
31
34
|
import type {
|
|
35
|
+
AsyncHandlerResult,
|
|
32
36
|
HttpMethod,
|
|
33
37
|
Request,
|
|
38
|
+
MiddlewareChain,
|
|
34
39
|
WebSocketRequest,
|
|
35
|
-
Middleware,
|
|
36
40
|
SocketData,
|
|
41
|
+
SocketConnection,
|
|
42
|
+
ActiveSession,
|
|
43
|
+
ActiveSessions,
|
|
44
|
+
InactiveSession,
|
|
45
|
+
Session,
|
|
37
46
|
AppOptions,
|
|
38
47
|
} from './utils'
|
|
39
48
|
|
|
@@ -44,57 +53,8 @@ import type {
|
|
|
44
53
|
ResponseMessage,
|
|
45
54
|
} from './messages'
|
|
46
55
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
expiresAt: number
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export type SocketConnection = {
|
|
53
|
-
data: SocketData
|
|
54
|
-
send: (data: string) => unknown
|
|
55
|
-
close: () => void
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export type ActiveSession = {
|
|
59
|
-
token: string
|
|
60
|
-
ws: SocketConnection
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export type InactiveSession = {
|
|
64
|
-
token: string
|
|
65
|
-
expiresAt: number
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export type Session = ActiveSession | InactiveSession
|
|
69
|
-
|
|
70
|
-
export type SocketState = {
|
|
71
|
-
disconnectThreshold: number
|
|
72
|
-
heartbeatInterval: number
|
|
73
|
-
maxTickets: number
|
|
74
|
-
reclaimTtl: number
|
|
75
|
-
ticketTtl: number
|
|
76
|
-
tickets: Map<string, Ticket>
|
|
77
|
-
activeSessions: Map<string, ActiveSession>
|
|
78
|
-
inactiveSessions: Map<string, InactiveSession>
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export type SocketRoute = {
|
|
82
|
-
method: HttpMethod
|
|
83
|
-
path: string
|
|
84
|
-
segments: string[]
|
|
85
|
-
chain: Middleware[]
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export type SocketEndpoint = {
|
|
89
|
-
method: HttpMethod
|
|
90
|
-
path: string
|
|
91
|
-
handler: (req: Request, res: unknown) => Response
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export type SocketCommands = {
|
|
95
|
-
send: (clientId: string, event: string, body: unknown) => void
|
|
96
|
-
broadcast: (event: string, body: unknown) => void
|
|
97
|
-
}
|
|
56
|
+
type SocketHandler = (req: Request, res: unknown) => AsyncHandlerResult
|
|
57
|
+
type FilterFn = (clientId: string, data: unknown) => boolean
|
|
98
58
|
|
|
99
59
|
type UpgradeData = {
|
|
100
60
|
clientId?: string
|
|
@@ -106,6 +66,12 @@ type UpgradeContext = {
|
|
|
106
66
|
[key: string]: unknown
|
|
107
67
|
}
|
|
108
68
|
|
|
69
|
+
type SocketEndpoint = {
|
|
70
|
+
method: HttpMethod
|
|
71
|
+
path: string
|
|
72
|
+
handler: SocketHandler
|
|
73
|
+
}
|
|
74
|
+
|
|
109
75
|
type CreateSocketRequest = {
|
|
110
76
|
query: {
|
|
111
77
|
ticket: string
|
|
@@ -125,6 +91,39 @@ type UpdateTicketRequest = {
|
|
|
125
91
|
}
|
|
126
92
|
}
|
|
127
93
|
|
|
94
|
+
export type Ticket = {
|
|
95
|
+
clientId: string
|
|
96
|
+
expiresAt: number
|
|
97
|
+
data: unknown
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export type SocketState = {
|
|
101
|
+
dropThreshold: number
|
|
102
|
+
heartbeatInterval: number
|
|
103
|
+
maxTickets: number
|
|
104
|
+
reclaimTtl: number
|
|
105
|
+
ticketTtl: number
|
|
106
|
+
tickets: Map<string, Ticket>
|
|
107
|
+
activeSessions: Map<string, ActiveSession>
|
|
108
|
+
inactiveSessions: Map<string, InactiveSession>
|
|
109
|
+
onOpen: ((clientId: string) => void) | null
|
|
110
|
+
onClose: ((clientId: string, reason: CloseReason) => void) | null
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export type SocketRoute = {
|
|
114
|
+
method: HttpMethod
|
|
115
|
+
path: string
|
|
116
|
+
segments: string[]
|
|
117
|
+
chain: MiddlewareChain
|
|
118
|
+
}
|
|
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
|
+
|
|
128
127
|
const ajv = new Ajv({
|
|
129
128
|
allErrors: true,
|
|
130
129
|
})
|
|
@@ -230,7 +229,7 @@ function isSessionActive (session: InactiveSession): boolean {
|
|
|
230
229
|
return !session.expiresAt || session.expiresAt > Date.now()
|
|
231
230
|
}
|
|
232
231
|
|
|
233
|
-
function
|
|
232
|
+
function randomTicketHash (): string {
|
|
234
233
|
return crypto.randomBytes(24).toString('base64url')
|
|
235
234
|
}
|
|
236
235
|
|
|
@@ -248,6 +247,30 @@ function parseMessage (raw: string | Buffer): RawMessage | undefined {
|
|
|
248
247
|
}
|
|
249
248
|
}
|
|
250
249
|
|
|
250
|
+
async function parseJsonBody (req: Request): Promise<unknown> {
|
|
251
|
+
try {
|
|
252
|
+
const body = await req.json()
|
|
253
|
+
|
|
254
|
+
return body
|
|
255
|
+
} catch {
|
|
256
|
+
throw new BadRequestError('Invalid JSON')
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function parseJsonBodyAppData (req: Request): Promise<unknown> {
|
|
261
|
+
const contentType = req.headers.get('content-type')
|
|
262
|
+
const usingJsonBody = contentType?.startsWith('application/json')
|
|
263
|
+
|
|
264
|
+
if (!usingJsonBody) {
|
|
265
|
+
return null
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const rawBody = await parseJsonBody(req)
|
|
269
|
+
const appData = (rawBody as Record<string, unknown> | null)?.data ?? null
|
|
270
|
+
|
|
271
|
+
return appData
|
|
272
|
+
}
|
|
273
|
+
|
|
251
274
|
function sweepInactiveSessions (state: SocketState): void {
|
|
252
275
|
for (const [key, session] of state.inactiveSessions) {
|
|
253
276
|
if (!isSessionActive(session)) {
|
|
@@ -338,6 +361,7 @@ function buildParams (
|
|
|
338
361
|
function buildRequest (
|
|
339
362
|
params: Record<string, string>,
|
|
340
363
|
message: RequestMessage,
|
|
364
|
+
activeSessions: ActiveSessions,
|
|
341
365
|
): WebSocketRequest {
|
|
342
366
|
const { id, clientId, method, route } = message
|
|
343
367
|
const headers = new Headers(message.headers ?? {})
|
|
@@ -353,6 +377,9 @@ function buildRequest (
|
|
|
353
377
|
params,
|
|
354
378
|
query,
|
|
355
379
|
json,
|
|
380
|
+
ws: {
|
|
381
|
+
active: activeSessions,
|
|
382
|
+
},
|
|
356
383
|
}
|
|
357
384
|
}
|
|
358
385
|
|
|
@@ -364,13 +391,12 @@ async function buildOutgoingMessage (
|
|
|
364
391
|
const text = await response.text()
|
|
365
392
|
const contentType = response.headers.get('content-type') ?? ''
|
|
366
393
|
const usingJson = contentType.includes('application/json')
|
|
367
|
-
const body = usingJson ? JSON.parse(text) : text
|
|
368
394
|
|
|
369
395
|
return createMessage(clientId, MessageType.Response, {
|
|
370
396
|
id,
|
|
371
397
|
status: response.status,
|
|
372
398
|
headers: response.headers,
|
|
373
|
-
body,
|
|
399
|
+
body: usingJson ? JSON.parse(text) : text,
|
|
374
400
|
})
|
|
375
401
|
}
|
|
376
402
|
|
|
@@ -393,9 +419,19 @@ function buildErrorMessage (
|
|
|
393
419
|
})
|
|
394
420
|
}
|
|
395
421
|
|
|
422
|
+
function getCloseReason (ws: SocketConnection, code: number): CloseReason {
|
|
423
|
+
if (ws.data.reaped) {
|
|
424
|
+
return CloseReason.Reaped
|
|
425
|
+
} else if (code === CloseCode.Ok) {
|
|
426
|
+
return CloseReason.Ok
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
return CloseReason.Dropped
|
|
430
|
+
}
|
|
431
|
+
|
|
396
432
|
export function buildSocketState (opts: AppOptions = {}): SocketState {
|
|
397
433
|
return {
|
|
398
|
-
|
|
434
|
+
dropThreshold: opts.ws?.dropThreshold ?? 120_000,
|
|
399
435
|
heartbeatInterval: opts.ws?.heartbeatInterval ?? 30_000,
|
|
400
436
|
maxTickets: opts.ws?.maxTickets ?? 100_000,
|
|
401
437
|
reclaimTtl: opts.ws?.reclaimTtl ?? 300_000,
|
|
@@ -403,6 +439,8 @@ export function buildSocketState (opts: AppOptions = {}): SocketState {
|
|
|
403
439
|
tickets: new Map(),
|
|
404
440
|
activeSessions: new Map(),
|
|
405
441
|
inactiveSessions: new Map(),
|
|
442
|
+
onOpen: opts.ws?.onOpen ?? null,
|
|
443
|
+
onClose: opts.ws?.onClose ?? null,
|
|
406
444
|
}
|
|
407
445
|
}
|
|
408
446
|
|
|
@@ -411,14 +449,16 @@ export function buildSocketServer (
|
|
|
411
449
|
state: SocketState,
|
|
412
450
|
): WebSocketHandler<SocketData> {
|
|
413
451
|
const {
|
|
414
|
-
|
|
452
|
+
dropThreshold,
|
|
415
453
|
heartbeatInterval,
|
|
416
454
|
reclaimTtl,
|
|
417
455
|
activeSessions,
|
|
418
456
|
inactiveSessions,
|
|
457
|
+
onOpen,
|
|
458
|
+
onClose,
|
|
419
459
|
} = state
|
|
420
460
|
|
|
421
|
-
function armReaper (ws: SocketConnection)
|
|
461
|
+
function armReaper (ws: SocketConnection) {
|
|
422
462
|
if (ws.data.reaperHandle) {
|
|
423
463
|
clearTimeout(ws.data.reaperHandle)
|
|
424
464
|
}
|
|
@@ -426,8 +466,28 @@ export function buildSocketServer (
|
|
|
426
466
|
ws.data.reaperHandle = setTimeout(() => {
|
|
427
467
|
ws.data.reaped = true
|
|
428
468
|
|
|
429
|
-
ws.close()
|
|
430
|
-
},
|
|
469
|
+
ws.close(CloseCode.Reaped)
|
|
470
|
+
}, dropThreshold)
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function invokeOpen (ws: SocketConnection) {
|
|
474
|
+
if (onOpen) {
|
|
475
|
+
try {
|
|
476
|
+
onOpen(ws.data.clientId)
|
|
477
|
+
} catch (err) {
|
|
478
|
+
console.error(err)
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function invokeClose (ws: SocketConnection, reason: CloseReason) {
|
|
484
|
+
if (onClose) {
|
|
485
|
+
try {
|
|
486
|
+
onClose(ws.data.clientId, reason)
|
|
487
|
+
} catch (err) {
|
|
488
|
+
console.error(err)
|
|
489
|
+
}
|
|
490
|
+
}
|
|
431
491
|
}
|
|
432
492
|
|
|
433
493
|
return {
|
|
@@ -465,6 +525,7 @@ export function buildSocketServer (
|
|
|
465
525
|
)
|
|
466
526
|
|
|
467
527
|
ws.send(JSON.stringify(welcomeMessage))
|
|
528
|
+
invokeOpen(ws)
|
|
468
529
|
},
|
|
469
530
|
close (ws: SocketConnection, code: number): void {
|
|
470
531
|
if (ws.data.reaperHandle) {
|
|
@@ -472,23 +533,29 @@ export function buildSocketServer (
|
|
|
472
533
|
}
|
|
473
534
|
|
|
474
535
|
if (ws.data.superseded) {
|
|
536
|
+
invokeClose(ws, CloseReason.Superseded)
|
|
537
|
+
|
|
475
538
|
return
|
|
476
539
|
}
|
|
477
540
|
|
|
478
|
-
const
|
|
541
|
+
const reason = getCloseReason(ws, code)
|
|
542
|
+
const exists = activeSessions.get(ws.data.clientId)
|
|
479
543
|
|
|
480
|
-
if (!
|
|
544
|
+
if (!exists || exists.ws !== ws) {
|
|
481
545
|
return
|
|
482
546
|
}
|
|
483
547
|
|
|
484
548
|
activeSessions.delete(ws.data.clientId)
|
|
485
549
|
|
|
486
|
-
if (code !==
|
|
550
|
+
if (code !== CloseCode.Ok || ws.data.reaped) {
|
|
487
551
|
inactiveSessions.set(ws.data.clientId, {
|
|
488
|
-
token:
|
|
552
|
+
token: exists.token,
|
|
489
553
|
expiresAt: Date.now() + reclaimTtl,
|
|
554
|
+
app: ws.data.app,
|
|
490
555
|
})
|
|
491
556
|
}
|
|
557
|
+
|
|
558
|
+
invokeClose(ws, reason)
|
|
492
559
|
},
|
|
493
560
|
async message (ws: SocketConnection, raw: string | Buffer): Promise<void> {
|
|
494
561
|
const incomingMsg = parseMessage(raw)
|
|
@@ -514,7 +581,7 @@ export function buildSocketServer (
|
|
|
514
581
|
const { id, clientId } = message
|
|
515
582
|
const route = matchRoute(routes, message)
|
|
516
583
|
const params = buildParams(route, message)
|
|
517
|
-
const req = buildRequest(params, message)
|
|
584
|
+
const req = buildRequest(params, message, activeSessions)
|
|
518
585
|
const res = await executeMiddlewareChain(req, route.chain)
|
|
519
586
|
const outgoingMsg = await buildOutgoingMessage(id, clientId, res)
|
|
520
587
|
|
|
@@ -539,7 +606,7 @@ export function buildSocketHandlers (state: SocketState): SocketEndpoint[] {
|
|
|
539
606
|
inactiveSessions,
|
|
540
607
|
} = state
|
|
541
608
|
|
|
542
|
-
function
|
|
609
|
+
function issueTicket (clientId: string, data: unknown): string {
|
|
543
610
|
for (const [key, entry] of tickets) {
|
|
544
611
|
if (entry.expiresAt > Date.now()) {
|
|
545
612
|
break
|
|
@@ -552,38 +619,36 @@ export function buildSocketHandlers (state: SocketState): SocketEndpoint[] {
|
|
|
552
619
|
throw new ServiceUnavailableError('Unable to issue ticket')
|
|
553
620
|
}
|
|
554
621
|
|
|
555
|
-
const
|
|
622
|
+
const hash = randomTicketHash()
|
|
556
623
|
const expiresAt = Date.now() + ticketTtl
|
|
557
624
|
|
|
558
|
-
tickets.set(
|
|
625
|
+
tickets.set(hash, {
|
|
559
626
|
clientId,
|
|
560
627
|
expiresAt,
|
|
628
|
+
data,
|
|
561
629
|
})
|
|
562
630
|
|
|
563
|
-
return
|
|
631
|
+
return hash
|
|
564
632
|
}
|
|
565
633
|
|
|
566
|
-
function redeemTicket (
|
|
567
|
-
const entry =
|
|
634
|
+
function redeemTicket (hash: string): Ticket | undefined {
|
|
635
|
+
const entry = hash ? tickets.get(hash) : undefined
|
|
568
636
|
|
|
569
637
|
if (!entry) {
|
|
570
638
|
return undefined
|
|
571
639
|
}
|
|
572
640
|
|
|
573
|
-
tickets.delete(
|
|
641
|
+
tickets.delete(hash)
|
|
574
642
|
|
|
575
|
-
|
|
576
|
-
return undefined
|
|
577
|
-
}
|
|
643
|
+
return entry.expiresAt > Date.now() ? entry : undefined
|
|
578
644
|
|
|
579
|
-
return entry.clientId
|
|
580
645
|
}
|
|
581
646
|
|
|
582
647
|
return [
|
|
583
648
|
{
|
|
584
649
|
method: 'GET',
|
|
585
650
|
path: '/ws',
|
|
586
|
-
handler (req:
|
|
651
|
+
handler (req: Request, res: unknown): AsyncHandlerResult {
|
|
587
652
|
const validReq = validateSchema(req, createSocketValidator)
|
|
588
653
|
|
|
589
654
|
if (typeof res !== 'object') {
|
|
@@ -591,16 +656,18 @@ export function buildSocketHandlers (state: SocketState): SocketEndpoint[] {
|
|
|
591
656
|
}
|
|
592
657
|
|
|
593
658
|
const ctx: UpgradeContext = res ? { ...res } : {}
|
|
659
|
+
const ticket = redeemTicket(validReq.query.ticket)
|
|
660
|
+
|
|
661
|
+
if (!ticket) {
|
|
662
|
+
throw new NotFoundError()
|
|
663
|
+
}
|
|
594
664
|
|
|
595
665
|
ctx.data = ctx.data ?? {}
|
|
596
|
-
ctx.data.clientId =
|
|
666
|
+
ctx.data.clientId = ticket.clientId
|
|
597
667
|
ctx.data.superseded = false
|
|
598
668
|
ctx.data.reaped = false
|
|
599
669
|
ctx.data.reaperHandle = null
|
|
600
|
-
|
|
601
|
-
if (!ctx.data.clientId) {
|
|
602
|
-
throw new NotFoundError()
|
|
603
|
-
}
|
|
670
|
+
ctx.data.app = ticket.data
|
|
604
671
|
|
|
605
672
|
const useSocket = validReq.server.upgrade(validReq.raw, ctx)
|
|
606
673
|
|
|
@@ -608,20 +675,22 @@ export function buildSocketHandlers (state: SocketState): SocketEndpoint[] {
|
|
|
608
675
|
throw new NotFoundError()
|
|
609
676
|
}
|
|
610
677
|
|
|
611
|
-
return new Response()
|
|
678
|
+
return Promise.resolve(new Response())
|
|
612
679
|
},
|
|
613
680
|
},
|
|
614
681
|
{
|
|
615
682
|
method: 'POST',
|
|
616
683
|
path: '/ws',
|
|
617
|
-
handler (req:
|
|
684
|
+
async handler (req: Request, res: unknown): AsyncHandlerResult {
|
|
618
685
|
validateSchema(req, createTicketValidator)
|
|
619
686
|
|
|
620
687
|
const clientId = crypto.randomUUID()
|
|
688
|
+
const appData = await parseJsonBodyAppData(req)
|
|
689
|
+
const ticket = issueTicket(clientId, appData)
|
|
621
690
|
|
|
622
691
|
return Response.json({
|
|
623
692
|
clientId,
|
|
624
|
-
ticket
|
|
693
|
+
ticket,
|
|
625
694
|
data: res,
|
|
626
695
|
}, { status: StatusCode.Created })
|
|
627
696
|
},
|
|
@@ -629,10 +698,10 @@ export function buildSocketHandlers (state: SocketState): SocketEndpoint[] {
|
|
|
629
698
|
{
|
|
630
699
|
method: 'PUT',
|
|
631
700
|
path: '/ws/:clientId',
|
|
632
|
-
handler (req:
|
|
701
|
+
async handler (req: Request, res: unknown): AsyncHandlerResult {
|
|
633
702
|
const validReq = validateSchema(req, updateTicketValidator)
|
|
634
|
-
const authHeader = validReq.headers.get('authorization')
|
|
635
|
-
const token = authHeader
|
|
703
|
+
const authHeader = validReq.headers.get('authorization')!
|
|
704
|
+
const token = authHeader.slice('Bearer '.length)
|
|
636
705
|
|
|
637
706
|
let session: Session | undefined =
|
|
638
707
|
activeSessions.get(validReq.params.clientId)
|
|
@@ -655,9 +724,11 @@ export function buildSocketHandlers (state: SocketState): SocketEndpoint[] {
|
|
|
655
724
|
throw new UnauthorizedError('Invalid token')
|
|
656
725
|
}
|
|
657
726
|
|
|
727
|
+
const appData = 'ws' in session ? session.ws.data.app : session.app
|
|
728
|
+
|
|
658
729
|
return Response.json({
|
|
659
730
|
clientId: validReq.params.clientId,
|
|
660
|
-
ticket:
|
|
731
|
+
ticket: issueTicket(validReq.params.clientId, appData),
|
|
661
732
|
data: res,
|
|
662
733
|
})
|
|
663
734
|
},
|
|
@@ -666,15 +737,11 @@ export function buildSocketHandlers (state: SocketState): SocketEndpoint[] {
|
|
|
666
737
|
}
|
|
667
738
|
|
|
668
739
|
export function buildSocketCommands (state: SocketState): SocketCommands {
|
|
669
|
-
function sendToClient (
|
|
670
|
-
clientId: string,
|
|
671
|
-
event: string,
|
|
672
|
-
body: unknown,
|
|
673
|
-
): void {
|
|
740
|
+
function sendToClient (clientId: string, event: string, body: unknown) {
|
|
674
741
|
const session = state.activeSessions.get(clientId)
|
|
675
742
|
|
|
676
743
|
if (!session) {
|
|
677
|
-
throw new ReferenceError(`No
|
|
744
|
+
throw new ReferenceError(`No active socket for client: ${clientId}`)
|
|
678
745
|
}
|
|
679
746
|
|
|
680
747
|
const message = createMessage(clientId, MessageType.Notification, {
|
|
@@ -690,10 +757,28 @@ export function buildSocketCommands (state: SocketState): SocketCommands {
|
|
|
690
757
|
send (clientId, event, body) {
|
|
691
758
|
sendToClient(clientId, event, body)
|
|
692
759
|
},
|
|
760
|
+
sendToGroup (fn, event, body) {
|
|
761
|
+
for (const [clientId, session] of state.activeSessions) {
|
|
762
|
+
const allow = fn(clientId, session.ws.data)
|
|
763
|
+
|
|
764
|
+
if (allow) {
|
|
765
|
+
sendToClient(clientId, event, body)
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
},
|
|
693
769
|
broadcast (event, body) {
|
|
694
770
|
for (const clientId of state.activeSessions.keys()) {
|
|
695
771
|
sendToClient(clientId, event, body)
|
|
696
772
|
}
|
|
697
773
|
},
|
|
774
|
+
drop (clientId, code, reason) {
|
|
775
|
+
const session = state.activeSessions.get(clientId)
|
|
776
|
+
|
|
777
|
+
if (!session) {
|
|
778
|
+
throw new ReferenceError(`No active socket for client: ${clientId}`)
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
session.ws.close(code, reason)
|
|
782
|
+
},
|
|
698
783
|
}
|
|
699
784
|
}
|
package/src/utils.ts
CHANGED
|
@@ -90,7 +90,36 @@ export type FormattedError = {
|
|
|
90
90
|
message: string
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
export type
|
|
93
|
+
export type AsyncHandlerResult = Promise<Response>
|
|
94
|
+
export type HandlerResult = Response | AsyncHandlerResult
|
|
95
|
+
export type NextFn = (data?: unknown) => HandlerResult
|
|
96
|
+
|
|
97
|
+
export type Middleware = (
|
|
98
|
+
req: Request,
|
|
99
|
+
res: unknown,
|
|
100
|
+
next: NextFn,
|
|
101
|
+
) => HandlerResult
|
|
102
|
+
|
|
103
|
+
export type Handler = (
|
|
104
|
+
req: Request,
|
|
105
|
+
res: unknown,
|
|
106
|
+
) => HandlerResult
|
|
107
|
+
|
|
108
|
+
export type MiddlewareChain = (Middleware | Handler)[]
|
|
109
|
+
|
|
110
|
+
export type ActiveSession = {
|
|
111
|
+
token: string
|
|
112
|
+
ws: SocketConnection
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export type InactiveSession = {
|
|
116
|
+
token: string
|
|
117
|
+
expiresAt: number
|
|
118
|
+
app: unknown
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export type ActiveSessions = ReadonlyMap<string, ActiveSession>
|
|
122
|
+
export type Session = ActiveSession | InactiveSession
|
|
94
123
|
|
|
95
124
|
export type BaseRequest = {
|
|
96
125
|
method: HttpMethod
|
|
@@ -99,6 +128,9 @@ export type BaseRequest = {
|
|
|
99
128
|
params: Record<string, string>
|
|
100
129
|
query: Record<string, unknown>
|
|
101
130
|
json: () => Promise<unknown>
|
|
131
|
+
ws: {
|
|
132
|
+
active: ActiveSessions
|
|
133
|
+
}
|
|
102
134
|
}
|
|
103
135
|
|
|
104
136
|
export type EndpointRequest = BaseRequest & {
|
|
@@ -113,18 +145,31 @@ export type WebSocketRequest = BaseRequest & {
|
|
|
113
145
|
|
|
114
146
|
export type Request = EndpointRequest | WebSocketRequest
|
|
115
147
|
|
|
116
|
-
export
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
148
|
+
export const CloseCode = {
|
|
149
|
+
Ok: 1000,
|
|
150
|
+
Abnormal: 1006,
|
|
151
|
+
Reaped: 4999,
|
|
152
|
+
} as const
|
|
153
|
+
|
|
154
|
+
export type CloseCode = typeof CloseCode[keyof typeof CloseCode]
|
|
155
|
+
|
|
156
|
+
export const CloseReason = {
|
|
157
|
+
Ok: 'ok',
|
|
158
|
+
Dropped: 'dropped',
|
|
159
|
+
Reaped: 'reaped',
|
|
160
|
+
Superseded: 'superseded',
|
|
161
|
+
} as const
|
|
162
|
+
|
|
163
|
+
export type CloseReason = typeof CloseReason[keyof typeof CloseReason]
|
|
121
164
|
|
|
122
165
|
export type SocketOptions = {
|
|
123
|
-
|
|
166
|
+
dropThreshold?: number
|
|
124
167
|
heartbeatInterval?: number
|
|
125
168
|
maxTickets?: number
|
|
126
169
|
reclaimTtl?: number
|
|
127
170
|
ticketTtl?: number
|
|
171
|
+
onOpen?: (clientId: string) => void
|
|
172
|
+
onClose?: (clientId: string, reason: CloseReason) => void
|
|
128
173
|
}
|
|
129
174
|
|
|
130
175
|
export type SocketData = {
|
|
@@ -132,6 +177,13 @@ export type SocketData = {
|
|
|
132
177
|
superseded: boolean
|
|
133
178
|
reaped: boolean
|
|
134
179
|
reaperHandle: ReturnType<typeof setTimeout> | null
|
|
180
|
+
app: unknown
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export type SocketConnection = {
|
|
184
|
+
data: SocketData
|
|
185
|
+
send: (data: string) => unknown
|
|
186
|
+
close: (code?: number, reason?: string) => void
|
|
135
187
|
}
|
|
136
188
|
|
|
137
189
|
export type Server = BunServer<SocketData>
|
|
@@ -174,8 +226,8 @@ export function formatError (
|
|
|
174
226
|
|
|
175
227
|
export async function executeMiddlewareChain (
|
|
176
228
|
req: Request,
|
|
177
|
-
chain:
|
|
178
|
-
):
|
|
229
|
+
chain: MiddlewareChain,
|
|
230
|
+
): AsyncHandlerResult {
|
|
179
231
|
if (!chain.length) {
|
|
180
232
|
throw new RangeError('Middleware chain is empty')
|
|
181
233
|
}
|
|
@@ -183,15 +235,14 @@ export async function executeMiddlewareChain (
|
|
|
183
235
|
const executeMiddleware = async (
|
|
184
236
|
index: number,
|
|
185
237
|
res: unknown,
|
|
186
|
-
):
|
|
187
|
-
const
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
const result = await currentMiddleware(req, res, next)
|
|
238
|
+
): AsyncHandlerResult => {
|
|
239
|
+
const isLast = index === chain.length - 1
|
|
240
|
+
const fn = chain[index]
|
|
241
|
+
const next = (data?: unknown) => executeMiddleware(index + 1, data)
|
|
242
|
+
|
|
243
|
+
const result = isLast
|
|
244
|
+
? await (fn as Handler)(req, res)
|
|
245
|
+
: await (fn as Middleware)(req, res, next)
|
|
195
246
|
|
|
196
247
|
if (result instanceof Response) {
|
|
197
248
|
return result
|