sleepy-serv 0.6.2 → 0.7.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/errors.d.ts +173 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/messages.d.ts +63 -0
- package/dist/messages.d.ts.map +1 -0
- package/dist/middleware.d.ts +19 -0
- package/dist/middleware.d.ts.map +1 -0
- package/dist/socket.d.ts +50 -0
- package/dist/socket.d.ts.map +1 -0
- package/dist/utils.d.ts +125 -0
- package/dist/utils.d.ts.map +1 -0
- package/package.json +17 -3
- package/src/{errors.js → errors.ts} +171 -80
- package/src/{index.js → index.ts} +180 -42
- package/src/messages.ts +237 -0
- package/src/middleware.ts +209 -0
- package/src/{socket.js → socket.ts} +247 -86
- package/src/utils.ts +204 -0
- package/src/messages.js +0 -164
- package/src/meta.js +0 -65
- package/src/middleware.js +0 -138
- package/src/utils.js +0 -49
|
@@ -2,10 +2,17 @@ import Ajv from 'ajv'
|
|
|
2
2
|
import addFormats from 'ajv-formats'
|
|
3
3
|
import crypto from 'node:crypto'
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
import { TYPES, createMessage, validateMessage } from './messages'
|
|
5
|
+
import { MessageType, createMessage, validateMessage } from './messages'
|
|
7
6
|
|
|
8
7
|
import {
|
|
8
|
+
StatusCode,
|
|
9
|
+
toSegments,
|
|
10
|
+
formatError,
|
|
11
|
+
executeMiddlewareChain,
|
|
12
|
+
} from './utils'
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
RequestError,
|
|
9
16
|
NotFoundError,
|
|
10
17
|
UnauthorizedError,
|
|
11
18
|
MethodNotAllowedError,
|
|
@@ -14,6 +21,106 @@ import {
|
|
|
14
21
|
ServiceUnavailableError,
|
|
15
22
|
} from './errors'
|
|
16
23
|
|
|
24
|
+
import type { WebSocketHandler } from 'bun'
|
|
25
|
+
import type { ValidateFunction } from 'ajv'
|
|
26
|
+
|
|
27
|
+
import type {
|
|
28
|
+
HttpMethod,
|
|
29
|
+
Request,
|
|
30
|
+
WebSocketRequest,
|
|
31
|
+
Middleware,
|
|
32
|
+
SocketData,
|
|
33
|
+
AppOptions,
|
|
34
|
+
} from './utils'
|
|
35
|
+
|
|
36
|
+
import type {
|
|
37
|
+
BaseMessage,
|
|
38
|
+
RawMessage,
|
|
39
|
+
RequestMessage,
|
|
40
|
+
ResponseMessage,
|
|
41
|
+
} from './messages'
|
|
42
|
+
|
|
43
|
+
export type Ticket = {
|
|
44
|
+
clientId: string
|
|
45
|
+
expiresAt: number
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type SocketConnection = {
|
|
49
|
+
data: SocketData
|
|
50
|
+
send: (data: string) => unknown
|
|
51
|
+
close: () => void
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type ActiveSession = {
|
|
55
|
+
token: string
|
|
56
|
+
ws: SocketConnection
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export type InactiveSession = {
|
|
60
|
+
token: string
|
|
61
|
+
expiresAt: number
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export type Session = ActiveSession | InactiveSession
|
|
65
|
+
|
|
66
|
+
export type SocketState = {
|
|
67
|
+
disconnectThreshold: number
|
|
68
|
+
heartbeatInterval: number
|
|
69
|
+
maxTickets: number
|
|
70
|
+
reclaimTtl: number
|
|
71
|
+
ticketTtl: number
|
|
72
|
+
tickets: Map<string, Ticket>
|
|
73
|
+
activeSessions: Map<string, ActiveSession>
|
|
74
|
+
inactiveSessions: Map<string, InactiveSession>
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export type SocketRoute = {
|
|
78
|
+
method: HttpMethod
|
|
79
|
+
path: string
|
|
80
|
+
segments: string[]
|
|
81
|
+
chain: Middleware[]
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export type SocketEndpoint = {
|
|
85
|
+
method: HttpMethod
|
|
86
|
+
path: string
|
|
87
|
+
handler: (req: Request, res: unknown) => Response
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export type SocketCommands = {
|
|
91
|
+
send: (clientId: string, event: string, body: unknown) => void
|
|
92
|
+
broadcast: (event: string, body: unknown) => void
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
type UpgradeData = {
|
|
96
|
+
clientId?: string
|
|
97
|
+
[key: string]: unknown
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
type UpgradeContext = {
|
|
101
|
+
data?: UpgradeData
|
|
102
|
+
[key: string]: unknown
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
type CreateSocketRequest = {
|
|
106
|
+
query: {
|
|
107
|
+
ticket: string
|
|
108
|
+
}
|
|
109
|
+
server: {
|
|
110
|
+
upgrade: (raw: unknown, ctx: UpgradeContext) => boolean
|
|
111
|
+
}
|
|
112
|
+
raw: unknown
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
type CreateTicketRequest = Record<string, unknown>
|
|
116
|
+
|
|
117
|
+
type UpdateTicketRequest = {
|
|
118
|
+
headers: Headers
|
|
119
|
+
params: {
|
|
120
|
+
clientId: string
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
17
124
|
const ajv = new Ajv({
|
|
18
125
|
allErrors: true,
|
|
19
126
|
})
|
|
@@ -32,7 +139,7 @@ const validateNotMessage = ajv.compile({
|
|
|
32
139
|
},
|
|
33
140
|
})
|
|
34
141
|
|
|
35
|
-
const createSocketValidator = ajv.compile({
|
|
142
|
+
const createSocketValidator = ajv.compile<CreateSocketRequest>({
|
|
36
143
|
type: 'object',
|
|
37
144
|
properties: {
|
|
38
145
|
clientId: {
|
|
@@ -68,7 +175,7 @@ const createSocketValidator = ajv.compile({
|
|
|
68
175
|
},
|
|
69
176
|
})
|
|
70
177
|
|
|
71
|
-
const createTicketValidator = ajv.compile({
|
|
178
|
+
const createTicketValidator = ajv.compile<CreateTicketRequest>({
|
|
72
179
|
type: 'object',
|
|
73
180
|
properties: {
|
|
74
181
|
clientId: {
|
|
@@ -80,7 +187,7 @@ const createTicketValidator = ajv.compile({
|
|
|
80
187
|
},
|
|
81
188
|
})
|
|
82
189
|
|
|
83
|
-
const updateTicketValidator = ajv.compile({
|
|
190
|
+
const updateTicketValidator = ajv.compile<UpdateTicketRequest>({
|
|
84
191
|
type: 'object',
|
|
85
192
|
properties: {
|
|
86
193
|
clientId: {
|
|
@@ -115,17 +222,21 @@ const updateTicketValidator = ajv.compile({
|
|
|
115
222
|
},
|
|
116
223
|
})
|
|
117
224
|
|
|
118
|
-
function isSessionActive (session) {
|
|
225
|
+
function isSessionActive (session: InactiveSession): boolean {
|
|
119
226
|
return !session.expiresAt || session.expiresAt > Date.now()
|
|
120
227
|
}
|
|
121
228
|
|
|
122
|
-
function
|
|
123
|
-
return crypto.randomBytes(
|
|
229
|
+
function randomTicket (): string {
|
|
230
|
+
return crypto.randomBytes(24).toString('base64url')
|
|
124
231
|
}
|
|
125
232
|
|
|
126
|
-
function
|
|
233
|
+
function randomToken (): string {
|
|
234
|
+
return crypto.randomBytes(32).toString('base64url')
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function parseMessage (raw: string | Buffer): RawMessage | undefined {
|
|
127
238
|
try {
|
|
128
|
-
return JSON.parse(raw)
|
|
239
|
+
return JSON.parse(String(raw))
|
|
129
240
|
} catch (err) {
|
|
130
241
|
console.error(err)
|
|
131
242
|
|
|
@@ -133,7 +244,7 @@ function parseMessage (raw) {
|
|
|
133
244
|
}
|
|
134
245
|
}
|
|
135
246
|
|
|
136
|
-
function sweepInactiveSessions (state) {
|
|
247
|
+
function sweepInactiveSessions (state: SocketState): void {
|
|
137
248
|
for (const [key, session] of state.inactiveSessions) {
|
|
138
249
|
if (!isSessionActive(session)) {
|
|
139
250
|
state.inactiveSessions.delete(key)
|
|
@@ -141,8 +252,11 @@ function sweepInactiveSessions (state) {
|
|
|
141
252
|
}
|
|
142
253
|
}
|
|
143
254
|
|
|
144
|
-
function validateSchema
|
|
145
|
-
|
|
255
|
+
function validateSchema<T extends Record<string, unknown>> (
|
|
256
|
+
obj: Record<string, unknown>,
|
|
257
|
+
validator: ValidateFunction<T>,
|
|
258
|
+
): T {
|
|
259
|
+
const headers = obj.headers instanceof Headers
|
|
146
260
|
? Object.fromEntries(obj.headers)
|
|
147
261
|
: undefined
|
|
148
262
|
|
|
@@ -152,19 +266,24 @@ function validateSchema (obj, validator) {
|
|
|
152
266
|
}
|
|
153
267
|
|
|
154
268
|
if (!validateNotMessage(payload)) {
|
|
155
|
-
const errors = validateNotMessage.errors
|
|
269
|
+
const errors = validateNotMessage.errors!.map(item => formatError('', item))
|
|
156
270
|
|
|
157
271
|
throw new UnprocessableContentError(errors)
|
|
158
272
|
}
|
|
159
273
|
|
|
160
274
|
if (!validator(payload)) {
|
|
161
|
-
const errors = validator.errors
|
|
275
|
+
const errors = validator.errors!.map(item => formatError('', item))
|
|
162
276
|
|
|
163
277
|
throw new UnprocessableContentError(errors)
|
|
164
278
|
}
|
|
279
|
+
|
|
280
|
+
return obj as T
|
|
165
281
|
}
|
|
166
282
|
|
|
167
|
-
function matchesSegments (
|
|
283
|
+
function matchesSegments (
|
|
284
|
+
patternSegments: string[],
|
|
285
|
+
requestSegments: string[],
|
|
286
|
+
): boolean {
|
|
168
287
|
if (patternSegments.length !== requestSegments.length) {
|
|
169
288
|
return false
|
|
170
289
|
}
|
|
@@ -175,7 +294,10 @@ function matchesSegments (patternSegments, requestSegments) {
|
|
|
175
294
|
)
|
|
176
295
|
}
|
|
177
296
|
|
|
178
|
-
function matchRoute (
|
|
297
|
+
function matchRoute (
|
|
298
|
+
routes: SocketRoute[],
|
|
299
|
+
message: RequestMessage,
|
|
300
|
+
): SocketRoute {
|
|
179
301
|
const requestSegments = toSegments(message.route)
|
|
180
302
|
|
|
181
303
|
const matchingPaths = routes.filter(route =>
|
|
@@ -195,17 +317,24 @@ function matchRoute (routes, message) {
|
|
|
195
317
|
return route
|
|
196
318
|
}
|
|
197
319
|
|
|
198
|
-
function buildParams (
|
|
320
|
+
function buildParams (
|
|
321
|
+
route: SocketRoute,
|
|
322
|
+
message: RequestMessage,
|
|
323
|
+
): Record<string, string> {
|
|
199
324
|
const requestSegments = toSegments(message.route)
|
|
200
325
|
|
|
201
|
-
return route.segments.reduce
|
|
202
|
-
segment
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
326
|
+
return route.segments.reduce<Record<string, string>>(
|
|
327
|
+
(accum, segment, index) =>
|
|
328
|
+
segment.startsWith(':') ? {
|
|
329
|
+
...accum,
|
|
330
|
+
[segment.slice(1)]: requestSegments[index],
|
|
331
|
+
} : accum, {})
|
|
206
332
|
}
|
|
207
333
|
|
|
208
|
-
function buildRequest (
|
|
334
|
+
function buildRequest (
|
|
335
|
+
params: Record<string, string>,
|
|
336
|
+
message: RequestMessage,
|
|
337
|
+
): WebSocketRequest {
|
|
209
338
|
const { id, clientId, method, route } = message
|
|
210
339
|
const headers = new Headers(message.headers ?? {})
|
|
211
340
|
const query = message.query ?? {}
|
|
@@ -223,13 +352,17 @@ function buildRequest (params, message) {
|
|
|
223
352
|
}
|
|
224
353
|
}
|
|
225
354
|
|
|
226
|
-
async function buildOutgoingMessage (
|
|
355
|
+
async function buildOutgoingMessage (
|
|
356
|
+
id: string,
|
|
357
|
+
clientId: string,
|
|
358
|
+
response: Response,
|
|
359
|
+
): Promise<ResponseMessage> {
|
|
227
360
|
const text = await response.text()
|
|
228
361
|
const contentType = response.headers.get('content-type') ?? ''
|
|
229
362
|
const usingJson = contentType.includes('application/json')
|
|
230
363
|
const body = usingJson ? JSON.parse(text) : text
|
|
231
364
|
|
|
232
|
-
return createMessage(clientId,
|
|
365
|
+
return createMessage(clientId, MessageType.Response, {
|
|
233
366
|
id,
|
|
234
367
|
status: response.status,
|
|
235
368
|
headers: response.headers,
|
|
@@ -237,7 +370,34 @@ async function buildOutgoingMessage (id, clientId, response) {
|
|
|
237
370
|
})
|
|
238
371
|
}
|
|
239
372
|
|
|
240
|
-
|
|
373
|
+
function buildErrorMessage (
|
|
374
|
+
message: RawMessage,
|
|
375
|
+
err: unknown,
|
|
376
|
+
): ResponseMessage {
|
|
377
|
+
const { id, clientId } = message as Pick<BaseMessage, 'id' | 'clientId'>
|
|
378
|
+
|
|
379
|
+
if (err instanceof RequestError) {
|
|
380
|
+
const ctor = err.constructor as typeof RequestError
|
|
381
|
+
|
|
382
|
+
return createMessage(clientId, MessageType.Response, {
|
|
383
|
+
id,
|
|
384
|
+
status: ctor.status,
|
|
385
|
+
headers: new Headers({
|
|
386
|
+
'content-type': 'application/json;charset=utf-8',
|
|
387
|
+
}),
|
|
388
|
+
body: err.output,
|
|
389
|
+
})
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return createMessage(clientId, MessageType.Response, {
|
|
393
|
+
id,
|
|
394
|
+
status: InternalServerError.status,
|
|
395
|
+
headers: new Headers(),
|
|
396
|
+
body: err instanceof Error ? err.message : undefined,
|
|
397
|
+
})
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
export function buildSocketState (opts: AppOptions = {}): SocketState {
|
|
241
401
|
return {
|
|
242
402
|
disconnectThreshold: opts.ws?.disconnectThreshold ?? 120_000,
|
|
243
403
|
heartbeatInterval: opts.ws?.heartbeatInterval ?? 30_000,
|
|
@@ -250,7 +410,10 @@ export function buildSocketState (opts = {}) {
|
|
|
250
410
|
}
|
|
251
411
|
}
|
|
252
412
|
|
|
253
|
-
export function buildSocketServer (
|
|
413
|
+
export function buildSocketServer (
|
|
414
|
+
routes: SocketRoute[],
|
|
415
|
+
state: SocketState,
|
|
416
|
+
): WebSocketHandler<SocketData> {
|
|
254
417
|
const {
|
|
255
418
|
disconnectThreshold,
|
|
256
419
|
heartbeatInterval,
|
|
@@ -259,8 +422,10 @@ export function buildSocketServer (routes, state) {
|
|
|
259
422
|
inactiveSessions,
|
|
260
423
|
} = state
|
|
261
424
|
|
|
262
|
-
function armReaper (ws) {
|
|
263
|
-
|
|
425
|
+
function armReaper (ws: SocketConnection): void {
|
|
426
|
+
if (ws.data.reaperHandle) {
|
|
427
|
+
clearTimeout(ws.data.reaperHandle)
|
|
428
|
+
}
|
|
264
429
|
|
|
265
430
|
ws.data.reaperHandle = setTimeout(() => {
|
|
266
431
|
ws.data.reaped = true
|
|
@@ -270,10 +435,10 @@ export function buildSocketServer (routes, state) {
|
|
|
270
435
|
}
|
|
271
436
|
|
|
272
437
|
return {
|
|
273
|
-
open (ws) {
|
|
438
|
+
open (ws: SocketConnection): void {
|
|
274
439
|
sweepInactiveSessions(state)
|
|
275
440
|
|
|
276
|
-
const token = randomToken(
|
|
441
|
+
const token = randomToken()
|
|
277
442
|
const existingSession = activeSessions.get(ws.data.clientId)
|
|
278
443
|
|
|
279
444
|
if (existingSession) {
|
|
@@ -293,9 +458,9 @@ export function buildSocketServer (routes, state) {
|
|
|
293
458
|
|
|
294
459
|
const welcomeMessage = createMessage(
|
|
295
460
|
ws.data.clientId,
|
|
296
|
-
|
|
461
|
+
MessageType.Welcome,
|
|
297
462
|
{
|
|
298
|
-
headers:
|
|
463
|
+
headers: new Headers(),
|
|
299
464
|
body: {
|
|
300
465
|
heartbeatInterval,
|
|
301
466
|
token,
|
|
@@ -305,8 +470,10 @@ export function buildSocketServer (routes, state) {
|
|
|
305
470
|
|
|
306
471
|
ws.send(JSON.stringify(welcomeMessage))
|
|
307
472
|
},
|
|
308
|
-
close (ws, code) {
|
|
309
|
-
|
|
473
|
+
close (ws: SocketConnection, code: number): void {
|
|
474
|
+
if (ws.data.reaperHandle) {
|
|
475
|
+
clearTimeout(ws.data.reaperHandle)
|
|
476
|
+
}
|
|
310
477
|
|
|
311
478
|
if (ws.data.superseded) {
|
|
312
479
|
return
|
|
@@ -327,7 +494,7 @@ export function buildSocketServer (routes, state) {
|
|
|
327
494
|
})
|
|
328
495
|
}
|
|
329
496
|
},
|
|
330
|
-
async message (ws, raw) {
|
|
497
|
+
async message (ws: SocketConnection, raw: string | Buffer): Promise<void> {
|
|
331
498
|
const incomingMsg = parseMessage(raw)
|
|
332
499
|
|
|
333
500
|
if (incomingMsg === undefined) {
|
|
@@ -337,21 +504,21 @@ export function buildSocketServer (routes, state) {
|
|
|
337
504
|
armReaper(ws)
|
|
338
505
|
|
|
339
506
|
try {
|
|
340
|
-
validateMessage(incomingMsg)
|
|
507
|
+
const message = validateMessage(incomingMsg)
|
|
341
508
|
|
|
342
|
-
if (
|
|
343
|
-
const { id, clientId } =
|
|
344
|
-
const ack = createMessage(clientId,
|
|
509
|
+
if (message.type === MessageType.Heartbeat) {
|
|
510
|
+
const { id, clientId } = message
|
|
511
|
+
const ack = createMessage(clientId, MessageType.Heartbeat, { id })
|
|
345
512
|
|
|
346
513
|
ws.send(JSON.stringify(ack))
|
|
347
514
|
|
|
348
515
|
return
|
|
349
516
|
}
|
|
350
517
|
|
|
351
|
-
const { id, clientId } =
|
|
352
|
-
const route = matchRoute(routes,
|
|
353
|
-
const params = buildParams(route,
|
|
354
|
-
const req = buildRequest(params,
|
|
518
|
+
const { id, clientId } = message
|
|
519
|
+
const route = matchRoute(routes, message)
|
|
520
|
+
const params = buildParams(route, message)
|
|
521
|
+
const req = buildRequest(params, message)
|
|
355
522
|
const res = await executeMiddlewareChain(req, route.chain)
|
|
356
523
|
const outgoingMsg = await buildOutgoingMessage(id, clientId, res)
|
|
357
524
|
|
|
@@ -359,20 +526,7 @@ export function buildSocketServer (routes, state) {
|
|
|
359
526
|
} catch (err) {
|
|
360
527
|
console.error(err)
|
|
361
528
|
|
|
362
|
-
const
|
|
363
|
-
const status = err.constructor.status ?? InternalServerError.status
|
|
364
|
-
const body = err.output !== undefined ? err.output : err.message
|
|
365
|
-
|
|
366
|
-
const headers = err.output !== undefined
|
|
367
|
-
? { 'content-type': 'application/json;charset=utf-8' }
|
|
368
|
-
: {}
|
|
369
|
-
|
|
370
|
-
const res = createMessage(clientId, TYPES.RESPONSE, {
|
|
371
|
-
id,
|
|
372
|
-
status,
|
|
373
|
-
headers,
|
|
374
|
-
body,
|
|
375
|
-
})
|
|
529
|
+
const res = buildErrorMessage(incomingMsg, err)
|
|
376
530
|
|
|
377
531
|
ws.send(JSON.stringify(res))
|
|
378
532
|
}
|
|
@@ -380,7 +534,7 @@ export function buildSocketServer (routes, state) {
|
|
|
380
534
|
}
|
|
381
535
|
}
|
|
382
536
|
|
|
383
|
-
export function buildSocketHandlers (state) {
|
|
537
|
+
export function buildSocketHandlers (state: SocketState): SocketEndpoint[] {
|
|
384
538
|
const {
|
|
385
539
|
maxTickets,
|
|
386
540
|
ticketTtl,
|
|
@@ -389,7 +543,7 @@ export function buildSocketHandlers (state) {
|
|
|
389
543
|
inactiveSessions,
|
|
390
544
|
} = state
|
|
391
545
|
|
|
392
|
-
function bindTicket (clientId) {
|
|
546
|
+
function bindTicket (clientId: string): string {
|
|
393
547
|
for (const [key, entry] of tickets) {
|
|
394
548
|
if (entry.expiresAt > Date.now()) {
|
|
395
549
|
break
|
|
@@ -399,10 +553,10 @@ export function buildSocketHandlers (state) {
|
|
|
399
553
|
}
|
|
400
554
|
|
|
401
555
|
if (tickets.size >= maxTickets) {
|
|
402
|
-
throw new ServiceUnavailableError()
|
|
556
|
+
throw new ServiceUnavailableError('Unable to issue ticket')
|
|
403
557
|
}
|
|
404
558
|
|
|
405
|
-
const ticket =
|
|
559
|
+
const ticket = randomTicket()
|
|
406
560
|
const expiresAt = Date.now() + ticketTtl
|
|
407
561
|
|
|
408
562
|
tickets.set(ticket, {
|
|
@@ -413,7 +567,7 @@ export function buildSocketHandlers (state) {
|
|
|
413
567
|
return ticket
|
|
414
568
|
}
|
|
415
569
|
|
|
416
|
-
function redeemTicket (ticket) {
|
|
570
|
+
function redeemTicket (ticket: string): string | undefined {
|
|
417
571
|
const entry = ticket ? tickets.get(ticket) : undefined
|
|
418
572
|
|
|
419
573
|
if (!entry) {
|
|
@@ -433,23 +587,26 @@ export function buildSocketHandlers (state) {
|
|
|
433
587
|
{
|
|
434
588
|
method: 'GET',
|
|
435
589
|
path: '/ws',
|
|
436
|
-
handler (req, res) {
|
|
437
|
-
validateSchema(req, createSocketValidator)
|
|
590
|
+
handler (req: Record<string, unknown>, res: unknown): Response {
|
|
591
|
+
const validReq = validateSchema(req, createSocketValidator)
|
|
438
592
|
|
|
439
593
|
if (typeof res !== 'object') {
|
|
440
594
|
throw new TypeError('Endpoint "res" must be an object')
|
|
441
595
|
}
|
|
442
596
|
|
|
443
|
-
const ctx = res ? { ...res } : {}
|
|
597
|
+
const ctx: UpgradeContext = res ? { ...res } : {}
|
|
444
598
|
|
|
445
599
|
ctx.data = ctx.data ?? {}
|
|
446
|
-
ctx.data.clientId = redeemTicket(
|
|
600
|
+
ctx.data.clientId = redeemTicket(validReq.query.ticket)
|
|
601
|
+
ctx.data.superseded = false
|
|
602
|
+
ctx.data.reaped = false
|
|
603
|
+
ctx.data.reaperHandle = null
|
|
447
604
|
|
|
448
605
|
if (!ctx.data.clientId) {
|
|
449
606
|
throw new NotFoundError()
|
|
450
607
|
}
|
|
451
608
|
|
|
452
|
-
const useSocket =
|
|
609
|
+
const useSocket = validReq.server.upgrade(validReq.raw, ctx)
|
|
453
610
|
|
|
454
611
|
if (!useSocket) {
|
|
455
612
|
throw new NotFoundError()
|
|
@@ -461,7 +618,7 @@ export function buildSocketHandlers (state) {
|
|
|
461
618
|
{
|
|
462
619
|
method: 'POST',
|
|
463
620
|
path: '/ws',
|
|
464
|
-
handler (req, res) {
|
|
621
|
+
handler (req: Record<string, unknown>, res: unknown): Response {
|
|
465
622
|
validateSchema(req, createTicketValidator)
|
|
466
623
|
|
|
467
624
|
const clientId = crypto.randomUUID()
|
|
@@ -470,25 +627,25 @@ export function buildSocketHandlers (state) {
|
|
|
470
627
|
clientId,
|
|
471
628
|
ticket: bindTicket(clientId),
|
|
472
629
|
data: res,
|
|
473
|
-
}, { status:
|
|
630
|
+
}, { status: StatusCode.Created })
|
|
474
631
|
},
|
|
475
632
|
},
|
|
476
633
|
{
|
|
477
634
|
method: 'PUT',
|
|
478
635
|
path: '/ws/:clientId',
|
|
479
|
-
handler (req, res) {
|
|
480
|
-
validateSchema(req, updateTicketValidator)
|
|
481
|
-
|
|
482
|
-
const
|
|
483
|
-
const token = authHeader.slice('Bearer '.length)
|
|
636
|
+
handler (req: Record<string, unknown>, res: unknown): Response {
|
|
637
|
+
const validReq = validateSchema(req, updateTicketValidator)
|
|
638
|
+
const authHeader = validReq.headers.get('authorization')
|
|
639
|
+
const token = authHeader!.slice('Bearer '.length)
|
|
484
640
|
|
|
485
|
-
let session =
|
|
641
|
+
let session: Session | undefined =
|
|
642
|
+
activeSessions.get(validReq.params.clientId)
|
|
486
643
|
|
|
487
644
|
if (!session) {
|
|
488
|
-
const inactive = inactiveSessions.get(
|
|
645
|
+
const inactive = inactiveSessions.get(validReq.params.clientId)
|
|
489
646
|
|
|
490
647
|
if (inactive && !isSessionActive(inactive)) {
|
|
491
|
-
inactiveSessions.delete(
|
|
648
|
+
inactiveSessions.delete(validReq.params.clientId)
|
|
492
649
|
} else {
|
|
493
650
|
session = inactive
|
|
494
651
|
}
|
|
@@ -499,12 +656,12 @@ export function buildSocketHandlers (state) {
|
|
|
499
656
|
}
|
|
500
657
|
|
|
501
658
|
if (session.token !== token) {
|
|
502
|
-
throw new UnauthorizedError()
|
|
659
|
+
throw new UnauthorizedError('Invalid token')
|
|
503
660
|
}
|
|
504
661
|
|
|
505
662
|
return Response.json({
|
|
506
|
-
clientId:
|
|
507
|
-
ticket: bindTicket(
|
|
663
|
+
clientId: validReq.params.clientId,
|
|
664
|
+
ticket: bindTicket(validReq.params.clientId),
|
|
508
665
|
data: res,
|
|
509
666
|
})
|
|
510
667
|
},
|
|
@@ -512,17 +669,21 @@ export function buildSocketHandlers (state) {
|
|
|
512
669
|
]
|
|
513
670
|
}
|
|
514
671
|
|
|
515
|
-
export function buildSocketCommands (state) {
|
|
516
|
-
function sendToClient (
|
|
672
|
+
export function buildSocketCommands (state: SocketState): SocketCommands {
|
|
673
|
+
function sendToClient (
|
|
674
|
+
clientId: string,
|
|
675
|
+
event: string,
|
|
676
|
+
body: unknown,
|
|
677
|
+
): void {
|
|
517
678
|
const session = state.activeSessions.get(clientId)
|
|
518
679
|
|
|
519
680
|
if (!session) {
|
|
520
681
|
throw new ReferenceError(`No live socket for client: ${clientId}`)
|
|
521
682
|
}
|
|
522
683
|
|
|
523
|
-
const message = createMessage(clientId,
|
|
684
|
+
const message = createMessage(clientId, MessageType.Notification, {
|
|
524
685
|
event,
|
|
525
|
-
headers:
|
|
686
|
+
headers: new Headers(),
|
|
526
687
|
body,
|
|
527
688
|
})
|
|
528
689
|
|