sleepy-serv 0.4.0 → 0.6.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 +38 -47
- package/package.json +8 -9
- package/src/errors.js +53 -41
- package/src/index.js +176 -101
- package/src/messages.js +164 -0
- package/src/middleware.js +83 -61
- package/src/socket.js +542 -0
- package/src/utils.js +49 -0
package/src/socket.js
ADDED
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
import Ajv from 'ajv'
|
|
2
|
+
import addFormats from 'ajv-formats'
|
|
3
|
+
import crypto from 'node:crypto'
|
|
4
|
+
|
|
5
|
+
import { toSegments, formatError, executeMiddlewareChain } from './utils'
|
|
6
|
+
import { TYPES, createMessage, validateMessage } from './messages'
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
NotFoundError,
|
|
10
|
+
UnauthorizedError,
|
|
11
|
+
MethodNotAllowedError,
|
|
12
|
+
UnprocessableContentError,
|
|
13
|
+
InternalServerError,
|
|
14
|
+
ServiceUnavailableError,
|
|
15
|
+
} from './errors'
|
|
16
|
+
|
|
17
|
+
const ajv = new Ajv({
|
|
18
|
+
allErrors: true,
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
addFormats(ajv)
|
|
22
|
+
|
|
23
|
+
const validateNotMessage = ajv.compile({
|
|
24
|
+
type: 'object',
|
|
25
|
+
properties: {
|
|
26
|
+
clientId: {
|
|
27
|
+
type: 'string',
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
not: {
|
|
31
|
+
required: ['clientId'],
|
|
32
|
+
},
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const createSocketValidator = ajv.compile({
|
|
36
|
+
type: 'object',
|
|
37
|
+
properties: {
|
|
38
|
+
clientId: {
|
|
39
|
+
type: 'string',
|
|
40
|
+
},
|
|
41
|
+
query: {
|
|
42
|
+
type: 'object',
|
|
43
|
+
properties: {
|
|
44
|
+
ticket: {
|
|
45
|
+
type: 'string',
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
required: ['ticket'],
|
|
49
|
+
},
|
|
50
|
+
server: {
|
|
51
|
+
type: 'object',
|
|
52
|
+
properties: {
|
|
53
|
+
upgrade: {},
|
|
54
|
+
},
|
|
55
|
+
required: ['upgrade'],
|
|
56
|
+
},
|
|
57
|
+
raw: {
|
|
58
|
+
type: 'object',
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
required: [
|
|
62
|
+
'query',
|
|
63
|
+
'server',
|
|
64
|
+
'raw',
|
|
65
|
+
],
|
|
66
|
+
not: {
|
|
67
|
+
required: ['clientId'],
|
|
68
|
+
},
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
const createTicketValidator = ajv.compile({
|
|
72
|
+
type: 'object',
|
|
73
|
+
properties: {
|
|
74
|
+
clientId: {
|
|
75
|
+
type: 'string',
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
not: {
|
|
79
|
+
required: ['clientId'],
|
|
80
|
+
},
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
const updateTicketValidator = ajv.compile({
|
|
84
|
+
type: 'object',
|
|
85
|
+
properties: {
|
|
86
|
+
clientId: {
|
|
87
|
+
type: 'string',
|
|
88
|
+
},
|
|
89
|
+
params: {
|
|
90
|
+
type: 'object',
|
|
91
|
+
properties: {
|
|
92
|
+
clientId: {
|
|
93
|
+
type: 'string',
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
required: ['clientId'],
|
|
97
|
+
},
|
|
98
|
+
headers: {
|
|
99
|
+
type: 'object',
|
|
100
|
+
properties: {
|
|
101
|
+
authorization: {
|
|
102
|
+
type: 'string',
|
|
103
|
+
pattern: '^Bearer .+$',
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
required: ['authorization'],
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
required: [
|
|
110
|
+
'params',
|
|
111
|
+
'headers',
|
|
112
|
+
],
|
|
113
|
+
not: {
|
|
114
|
+
required: ['clientId'],
|
|
115
|
+
},
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
function isSessionActive (session) {
|
|
119
|
+
return !session.expiresAt || session.expiresAt > Date.now()
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function randomToken (count) {
|
|
123
|
+
return crypto.randomBytes(count).toString('base64url')
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function parseMessage (raw) {
|
|
127
|
+
try {
|
|
128
|
+
return JSON.parse(raw)
|
|
129
|
+
} catch (err) {
|
|
130
|
+
console.error(err)
|
|
131
|
+
|
|
132
|
+
return undefined
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function sweepInactiveSessions (state) {
|
|
137
|
+
for (const [key, session] of state.inactiveSessions) {
|
|
138
|
+
if (!isSessionActive(session)) {
|
|
139
|
+
state.inactiveSessions.delete(key)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function validateSchema (obj, validator) {
|
|
145
|
+
const headers = obj.headers
|
|
146
|
+
? Object.fromEntries(obj.headers)
|
|
147
|
+
: undefined
|
|
148
|
+
|
|
149
|
+
const payload = {
|
|
150
|
+
...obj,
|
|
151
|
+
headers,
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (!validateNotMessage(payload)) {
|
|
155
|
+
const errors = validateNotMessage.errors.map(item => formatError('', item))
|
|
156
|
+
|
|
157
|
+
throw new UnprocessableContentError(errors)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (!validator(payload)) {
|
|
161
|
+
const errors = validator.errors.map(item => formatError('', item))
|
|
162
|
+
|
|
163
|
+
throw new UnprocessableContentError(errors)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function matchesSegments (patternSegments, requestSegments) {
|
|
168
|
+
if (patternSegments.length !== requestSegments.length) {
|
|
169
|
+
return false
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return patternSegments.every((segment, index) =>
|
|
173
|
+
segment.startsWith(':') ||
|
|
174
|
+
segment === requestSegments[index],
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function matchRoute (routes, message) {
|
|
179
|
+
const requestSegments = toSegments(message.route)
|
|
180
|
+
|
|
181
|
+
const matchingPaths = routes.filter(route =>
|
|
182
|
+
matchesSegments(route.segments, requestSegments),
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
if (!matchingPaths.length) {
|
|
186
|
+
throw new NotFoundError()
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const route = matchingPaths.find(route => route.method === message.method)
|
|
190
|
+
|
|
191
|
+
if (!route) {
|
|
192
|
+
throw new MethodNotAllowedError()
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return route
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function buildParams (route, message) {
|
|
199
|
+
const requestSegments = toSegments(message.route)
|
|
200
|
+
|
|
201
|
+
return route.segments.reduce((accum, segment, index) =>
|
|
202
|
+
segment.startsWith(':') ? {
|
|
203
|
+
...accum,
|
|
204
|
+
[segment.slice(1)]: requestSegments[index],
|
|
205
|
+
} : accum, {})
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function buildRequest (params, message) {
|
|
209
|
+
const { id, clientId, method, route } = message
|
|
210
|
+
const headers = new Headers(message.headers ?? {})
|
|
211
|
+
const query = message.query ?? {}
|
|
212
|
+
const json = async () => message.body
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
id,
|
|
216
|
+
clientId,
|
|
217
|
+
method,
|
|
218
|
+
route,
|
|
219
|
+
headers,
|
|
220
|
+
params,
|
|
221
|
+
query,
|
|
222
|
+
json,
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function buildOutgoingMessage (id, clientId, response) {
|
|
227
|
+
const text = await response.text()
|
|
228
|
+
const contentType = response.headers.get('content-type') ?? ''
|
|
229
|
+
const usingJson = contentType.includes('application/json')
|
|
230
|
+
const body = usingJson ? JSON.parse(text) : text
|
|
231
|
+
|
|
232
|
+
return createMessage(clientId, TYPES.RESPONSE, {
|
|
233
|
+
id,
|
|
234
|
+
status: response.status,
|
|
235
|
+
headers: response.headers,
|
|
236
|
+
body,
|
|
237
|
+
})
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function buildSocketState (opts = {}) {
|
|
241
|
+
return {
|
|
242
|
+
disconnectThreshold: opts.ws?.disconnectThreshold ?? 120_000,
|
|
243
|
+
heartbeatInterval: opts.ws?.heartbeatInterval ?? 30_000,
|
|
244
|
+
maxTickets: opts.ws?.maxTickets ?? 100_000,
|
|
245
|
+
reclaimTtl: opts.ws?.reclaimTtl ?? 300_000,
|
|
246
|
+
ticketTtl: opts.ws?.ticketTtl ?? 10_000,
|
|
247
|
+
tickets: new Map(),
|
|
248
|
+
activeSessions: new Map(),
|
|
249
|
+
inactiveSessions: new Map(),
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function buildSocketServer (routes, state) {
|
|
254
|
+
const {
|
|
255
|
+
disconnectThreshold,
|
|
256
|
+
heartbeatInterval,
|
|
257
|
+
reclaimTtl,
|
|
258
|
+
activeSessions,
|
|
259
|
+
inactiveSessions,
|
|
260
|
+
} = state
|
|
261
|
+
|
|
262
|
+
function armReaper (ws) {
|
|
263
|
+
clearTimeout(ws.data.reaperHandle)
|
|
264
|
+
|
|
265
|
+
ws.data.reaperHandle = setTimeout(() => {
|
|
266
|
+
ws.data.reaped = true
|
|
267
|
+
|
|
268
|
+
ws.close()
|
|
269
|
+
}, disconnectThreshold)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
open (ws) {
|
|
274
|
+
sweepInactiveSessions(state)
|
|
275
|
+
|
|
276
|
+
const token = randomToken(32)
|
|
277
|
+
const existingSession = activeSessions.get(ws.data.clientId)
|
|
278
|
+
|
|
279
|
+
if (existingSession) {
|
|
280
|
+
existingSession.ws.data.superseded = true
|
|
281
|
+
|
|
282
|
+
existingSession.ws.close()
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
inactiveSessions.delete(ws.data.clientId)
|
|
286
|
+
|
|
287
|
+
activeSessions.set(ws.data.clientId, {
|
|
288
|
+
token,
|
|
289
|
+
ws,
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
armReaper(ws)
|
|
293
|
+
|
|
294
|
+
const welcomeMessage = createMessage(
|
|
295
|
+
ws.data.clientId,
|
|
296
|
+
TYPES.WELCOME,
|
|
297
|
+
{
|
|
298
|
+
headers: {},
|
|
299
|
+
body: {
|
|
300
|
+
heartbeatInterval,
|
|
301
|
+
token,
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
ws.send(JSON.stringify(welcomeMessage))
|
|
307
|
+
},
|
|
308
|
+
close (ws, code) {
|
|
309
|
+
clearTimeout(ws.data.reaperHandle)
|
|
310
|
+
|
|
311
|
+
if (ws.data.superseded) {
|
|
312
|
+
return
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const existingSession = activeSessions.get(ws.data.clientId)
|
|
316
|
+
|
|
317
|
+
if (!existingSession || existingSession.ws !== ws) {
|
|
318
|
+
return
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
activeSessions.delete(ws.data.clientId)
|
|
322
|
+
|
|
323
|
+
if (code !== 1000 || ws.data.reaped) {
|
|
324
|
+
inactiveSessions.set(ws.data.clientId, {
|
|
325
|
+
token: existingSession.token,
|
|
326
|
+
expiresAt: Date.now() + reclaimTtl,
|
|
327
|
+
})
|
|
328
|
+
}
|
|
329
|
+
},
|
|
330
|
+
async message (ws, raw) {
|
|
331
|
+
const incomingMsg = parseMessage(raw)
|
|
332
|
+
|
|
333
|
+
if (incomingMsg === undefined) {
|
|
334
|
+
return
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
armReaper(ws)
|
|
338
|
+
|
|
339
|
+
try {
|
|
340
|
+
validateMessage(incomingMsg)
|
|
341
|
+
|
|
342
|
+
if (incomingMsg.type === TYPES.HEARTBEAT) {
|
|
343
|
+
const { id, clientId } = incomingMsg
|
|
344
|
+
const ack = createMessage(clientId, TYPES.HEARTBEAT, { id })
|
|
345
|
+
|
|
346
|
+
ws.send(JSON.stringify(ack))
|
|
347
|
+
|
|
348
|
+
return
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const { id, clientId } = incomingMsg
|
|
352
|
+
const route = matchRoute(routes, incomingMsg)
|
|
353
|
+
const params = buildParams(route, incomingMsg)
|
|
354
|
+
const req = buildRequest(params, incomingMsg)
|
|
355
|
+
const res = await executeMiddlewareChain(req, route.chain)
|
|
356
|
+
const outgoingMsg = await buildOutgoingMessage(id, clientId, res)
|
|
357
|
+
|
|
358
|
+
ws.send(JSON.stringify(outgoingMsg))
|
|
359
|
+
} catch (err) {
|
|
360
|
+
console.error(err)
|
|
361
|
+
|
|
362
|
+
const { id, clientId } = incomingMsg
|
|
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
|
+
})
|
|
376
|
+
|
|
377
|
+
ws.send(JSON.stringify(res))
|
|
378
|
+
}
|
|
379
|
+
},
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
export function buildSocketHandlers (state) {
|
|
384
|
+
const {
|
|
385
|
+
maxTickets,
|
|
386
|
+
ticketTtl,
|
|
387
|
+
tickets,
|
|
388
|
+
activeSessions,
|
|
389
|
+
inactiveSessions,
|
|
390
|
+
} = state
|
|
391
|
+
|
|
392
|
+
function bindTicket (clientId) {
|
|
393
|
+
for (const [key, entry] of tickets) {
|
|
394
|
+
if (entry.expiresAt > Date.now()) {
|
|
395
|
+
break
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
tickets.delete(key)
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (tickets.size >= maxTickets) {
|
|
402
|
+
throw new ServiceUnavailableError()
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const ticket = randomToken(24)
|
|
406
|
+
const expiresAt = Date.now() + ticketTtl
|
|
407
|
+
|
|
408
|
+
tickets.set(ticket, {
|
|
409
|
+
clientId,
|
|
410
|
+
expiresAt,
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
return ticket
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function redeemTicket (ticket) {
|
|
417
|
+
const entry = ticket ? tickets.get(ticket) : undefined
|
|
418
|
+
|
|
419
|
+
if (!entry) {
|
|
420
|
+
return undefined
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
tickets.delete(ticket)
|
|
424
|
+
|
|
425
|
+
if (entry.expiresAt <= Date.now()) {
|
|
426
|
+
return undefined
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
return entry.clientId
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return [
|
|
433
|
+
{
|
|
434
|
+
method: 'GET',
|
|
435
|
+
path: '/ws',
|
|
436
|
+
handler (req, res) {
|
|
437
|
+
validateSchema(req, createSocketValidator)
|
|
438
|
+
|
|
439
|
+
if (typeof res !== 'object') {
|
|
440
|
+
throw new TypeError('Endpoint "res" must be an object')
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const ctx = res ? { ...res } : {}
|
|
444
|
+
|
|
445
|
+
ctx.data = ctx.data ?? {}
|
|
446
|
+
ctx.data.clientId = redeemTicket(req.query.ticket)
|
|
447
|
+
|
|
448
|
+
if (!ctx.data.clientId) {
|
|
449
|
+
throw new NotFoundError()
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const useSocket = req.server.upgrade(req.raw, ctx)
|
|
453
|
+
|
|
454
|
+
if (!useSocket) {
|
|
455
|
+
throw new NotFoundError()
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return new Response()
|
|
459
|
+
},
|
|
460
|
+
},
|
|
461
|
+
{
|
|
462
|
+
method: 'POST',
|
|
463
|
+
path: '/ws',
|
|
464
|
+
handler (req, res) {
|
|
465
|
+
validateSchema(req, createTicketValidator)
|
|
466
|
+
|
|
467
|
+
const clientId = crypto.randomUUID()
|
|
468
|
+
|
|
469
|
+
return Response.json({
|
|
470
|
+
clientId,
|
|
471
|
+
ticket: bindTicket(clientId),
|
|
472
|
+
data: res,
|
|
473
|
+
}, { status: 201 })
|
|
474
|
+
},
|
|
475
|
+
},
|
|
476
|
+
{
|
|
477
|
+
method: 'PUT',
|
|
478
|
+
path: '/ws/:clientId',
|
|
479
|
+
handler (req, res) {
|
|
480
|
+
validateSchema(req, updateTicketValidator)
|
|
481
|
+
|
|
482
|
+
const authHeader = req.headers.get('authorization')
|
|
483
|
+
const token = authHeader.slice('Bearer '.length)
|
|
484
|
+
|
|
485
|
+
let session = activeSessions.get(req.params.clientId)
|
|
486
|
+
|
|
487
|
+
if (!session) {
|
|
488
|
+
const inactive = inactiveSessions.get(req.params.clientId)
|
|
489
|
+
|
|
490
|
+
if (inactive && !isSessionActive(inactive)) {
|
|
491
|
+
inactiveSessions.delete(req.params.clientId)
|
|
492
|
+
} else {
|
|
493
|
+
session = inactive
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
if (!session) {
|
|
498
|
+
throw new NotFoundError()
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
if (session.token !== token) {
|
|
502
|
+
throw new UnauthorizedError()
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
return Response.json({
|
|
506
|
+
clientId: req.params.clientId,
|
|
507
|
+
ticket: bindTicket(req.params.clientId),
|
|
508
|
+
data: res,
|
|
509
|
+
})
|
|
510
|
+
},
|
|
511
|
+
},
|
|
512
|
+
]
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
export function buildSocketCommands (state) {
|
|
516
|
+
function sendToClient (clientId, event, body) {
|
|
517
|
+
const session = state.activeSessions.get(clientId)
|
|
518
|
+
|
|
519
|
+
if (!session) {
|
|
520
|
+
throw new ReferenceError(`No live socket for client: ${clientId}`)
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const message = createMessage(clientId, TYPES.NOTIFICATION, {
|
|
524
|
+
event,
|
|
525
|
+
headers: {},
|
|
526
|
+
body,
|
|
527
|
+
})
|
|
528
|
+
|
|
529
|
+
session.ws.send(JSON.stringify(message))
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
return {
|
|
533
|
+
send (clientId, event, body) {
|
|
534
|
+
sendToClient(clientId, event, body)
|
|
535
|
+
},
|
|
536
|
+
broadcast (event, body) {
|
|
537
|
+
for (const clientId of state.activeSessions.keys()) {
|
|
538
|
+
sendToClient(clientId, event, body)
|
|
539
|
+
}
|
|
540
|
+
},
|
|
541
|
+
}
|
|
542
|
+
}
|
package/src/utils.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export function toSegments (pathString) {
|
|
2
|
+
const [pathname] = String(pathString).split('?')
|
|
3
|
+
const segments = pathname.split('/')
|
|
4
|
+
|
|
5
|
+
if (pathname.startsWith('/')) {
|
|
6
|
+
segments.shift()
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
if (pathname.endsWith('/')) {
|
|
10
|
+
segments.pop()
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return segments
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function formatError (prefix, input) {
|
|
17
|
+
const fixedPath = input.instancePath || '/'
|
|
18
|
+
const suffixPath = fixedPath.replace(/\//g, '.').replace('.', '')
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
path: [prefix, suffixPath].filter(item => item).join('.'),
|
|
22
|
+
message: input.message,
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function executeMiddlewareChain (req, chain) {
|
|
27
|
+
if (!chain.length) {
|
|
28
|
+
throw new RangeError('Middleware chain is empty')
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const executeMiddleware = async (index, res) => {
|
|
32
|
+
const currentMiddleware = chain[index]
|
|
33
|
+
const isLastMiddleware = index === chain.length - 1
|
|
34
|
+
|
|
35
|
+
const next = !isLastMiddleware ?
|
|
36
|
+
(data) => executeMiddleware(index + 1, data)
|
|
37
|
+
: null
|
|
38
|
+
|
|
39
|
+
const result = await currentMiddleware(req, res, next)
|
|
40
|
+
|
|
41
|
+
if (result instanceof Response) {
|
|
42
|
+
return result
|
|
43
|
+
} else {
|
|
44
|
+
throw new TypeError('Handler does not return a Response object')
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return executeMiddleware(0, null)
|
|
49
|
+
}
|