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 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.3.14 or higher
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 = {
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAwBA,OAAO,KAAK,EAIV,UAAU,EACV,MAAM,EACP,MAAM,SAAS,CAAA;AAEhB,OAAO,KAAK,EAGV,cAAc,EACf,MAAM,UAAU,CAAA;AAEjB,cAAc,UAAU,CAAA;AAExB,OAAO,EACL,aAAa,EACb,oBAAoB,EACpB,eAAe,GAChB,MAAM,cAAc,CAAA;AAErB,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AAEhD,YAAY,EACV,UAAU,EACV,eAAe,EACf,cAAc,EACd,UAAU,EACV,MAAM,EACN,OAAO,EACP,MAAM,EACN,aAAa,EACb,gBAAgB,GACjB,MAAM,SAAS,CAAA;AAEhB,YAAY,EACV,cAAc,EACd,eAAe,EACf,iBAAiB,GAClB,MAAM,cAAc,CAAA;AAErB,YAAY,EAAE,cAAc,EAAE,MAAM,UAAU,CAAA;AAE9C,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;AAoZD,wBAAsB,SAAS,CAC7B,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,IAAI,GAAE,UAAe,GACpB,OAAO,CAAC,GAAG,CAAC,CAad"}
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"}
@@ -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;AAC3D,OAAO,KAAK,EAAkB,UAAU,EAAmB,MAAM,SAAS,CAAA;AAE1E,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;AA2F/C,wBAAgB,aAAa,IAAK,UAAU,CAoB3C;AAED,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC9B,IAAI,CAUN;AAID,wBAAgB,2BAA2B,IAAK,IAAI,CAGnD;AAcD,wBAAgB,eAAe,CAC7B,OAAO,EAAE,iBAAiB,GACzB,UAAU,CAgCZ"}
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, Middleware, SocketData, AppOptions } from './utils';
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
- disconnectThreshold: number;
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: Middleware[];
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
@@ -1 +1 @@
1
- {"version":3,"file":"socket.d.ts","sourceRoot":"","sources":["../src/socket.ts"],"names":[],"mappings":"AA4BA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,KAAK,CAAA;AAE3C,OAAO,KAAK,EACV,UAAU,EACV,OAAO,EAEP,UAAU,EACV,UAAU,EACV,UAAU,EACX,MAAM,SAAS,CAAA;AAShB,MAAM,MAAM,MAAM,GAAG;IACnB,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;CAClB,CAAA;AAED,MAAM,MAAM,gBAAgB,GAAG;IAC7B,IAAI,EAAE,UAAU,CAAA;IAChB,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAA;IAC/B,KAAK,EAAE,MAAM,IAAI,CAAA;CAClB,CAAA;AAED,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;CAClB,CAAA;AAED,MAAM,MAAM,OAAO,GAAG,aAAa,GAAG,eAAe,CAAA;AAErD,MAAM,MAAM,WAAW,GAAG;IACxB,mBAAmB,EAAE,MAAM,CAAA;IAC3B,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;CAC/C,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,UAAU,EAAE,CAAA;CACpB,CAAA;AAED,MAAM,MAAM,cAAc,GAAG;IAC3B,MAAM,EAAE,UAAU,CAAA;IAClB,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,KAAK,QAAQ,CAAA;CAClD,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,SAAS,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;CAClD,CAAA;AA2SD,wBAAgB,gBAAgB,CAAE,IAAI,GAAE,UAAe,GAAG,WAAW,CAWpE;AAED,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,WAAW,EAAE,EACrB,KAAK,EAAE,WAAW,GACjB,gBAAgB,CAAC,UAAU,CAAC,CAuH9B;AAED,wBAAgB,mBAAmB,CAAE,KAAK,EAAE,WAAW,GAAG,cAAc,EAAE,CAqIzE;AAED,wBAAgB,mBAAmB,CAAE,KAAK,EAAE,WAAW,GAAG,cAAc,CA+BvE"}
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 NextFn = (data?: unknown) => Response | Promise<Response>;
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 type Middleware = (req: Request, res: unknown, next: NextFn | null) => unknown;
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
- disconnectThreshold?: number;
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: Middleware[]): Promise<Response>;
163
+ export declare function executeMiddlewareChain(req: Request, chain: MiddlewareChain): AsyncHandlerResult;
125
164
  //# sourceMappingURL=utils.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "A more opinionated web server for building REST-ful applications",
4
4
  "author": "Travis J True",
5
5
  "license": "MIT",
6
- "version": "0.9.0",
6
+ "version": "0.10.0",
7
7
  "type": "module",
8
8
  "exports": {
9
9
  ".": {
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
- import { toSegments, executeMiddlewareChain } from './utils'
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 { HttpMethod, StatusCode } from './utils'
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
- ) => Promise<Response>
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: Middleware[]
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
- const json = () => bunReq.json()
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: segments[lastIndex].toUpperCase() as HttpMethod,
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 (socketRoutes: SocketRoute[]): ModuleRoute[] {
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(bunReq, server)
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
- import type { FormattedError, Middleware, NextFn, Request } from './utils'
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 | null,
125
- ): Promise<unknown> => {
124
+ next: NextFn,
125
+ ): AsyncHandlerResult => {
126
126
  const contentType = req.headers.get('content-type')
127
127
 
128
128
  if (!contentType) {
129
- return requireNext(next)(res)
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 requireNext(next)(body)
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 | null,
186
- ): unknown => {
181
+ next: NextFn,
182
+ ): HandlerResult => {
187
183
  const source = buildValidationSource(req, res)
188
184
 
189
- const errors = entries.reduce((accum: FormattedError[], [
190
- key,
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 requireNext(next)(res)
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
- export type Ticket = {
48
- clientId: string
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 randomTicket (): string {
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
- disconnectThreshold: opts.ws?.disconnectThreshold ?? 120_000,
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
- disconnectThreshold,
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): void {
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
- }, disconnectThreshold)
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 existingSession = activeSessions.get(ws.data.clientId)
541
+ const reason = getCloseReason(ws, code)
542
+ const exists = activeSessions.get(ws.data.clientId)
479
543
 
480
- if (!existingSession || existingSession.ws !== ws) {
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 !== 1000 || ws.data.reaped) {
550
+ if (code !== CloseCode.Ok || ws.data.reaped) {
487
551
  inactiveSessions.set(ws.data.clientId, {
488
- token: existingSession.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 bindTicket (clientId: string): string {
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 ticket = randomTicket()
622
+ const hash = randomTicketHash()
556
623
  const expiresAt = Date.now() + ticketTtl
557
624
 
558
- tickets.set(ticket, {
625
+ tickets.set(hash, {
559
626
  clientId,
560
627
  expiresAt,
628
+ data,
561
629
  })
562
630
 
563
- return ticket
631
+ return hash
564
632
  }
565
633
 
566
- function redeemTicket (ticket: string): string | undefined {
567
- const entry = ticket ? tickets.get(ticket) : undefined
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(ticket)
641
+ tickets.delete(hash)
574
642
 
575
- if (entry.expiresAt <= Date.now()) {
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: Record<string, unknown>, res: unknown): Response {
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 = redeemTicket(validReq.query.ticket)
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: Record<string, unknown>, res: unknown): Response {
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: bindTicket(clientId),
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: Record<string, unknown>, res: unknown): Response {
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!.slice('Bearer '.length)
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: bindTicket(validReq.params.clientId),
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 live socket for client: ${clientId}`)
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 NextFn = (data?: unknown) => Response | Promise<Response>
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 type Middleware = (
117
- req: Request,
118
- res: unknown,
119
- next: NextFn | null,
120
- ) => unknown
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
- disconnectThreshold?: number
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: Middleware[],
178
- ): Promise<Response> {
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
- ): Promise<Response> => {
187
- const currentMiddleware = chain[index]
188
- const isLastMiddleware = index === chain.length - 1
189
-
190
- const next = !isLastMiddleware ?
191
- (data?: unknown) => executeMiddleware(index + 1, data)
192
- : null
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