sleepy-serv 0.5.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 +474 -0
- package/package.json +8 -9
- package/src/errors.js +53 -41
- package/src/index.js +170 -104
- 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/index.js
CHANGED
|
@@ -2,9 +2,16 @@ import fs from 'fs'
|
|
|
2
2
|
import path from 'path'
|
|
3
3
|
import querystring from 'querystring'
|
|
4
4
|
import readline from 'node:readline'
|
|
5
|
+
|
|
5
6
|
import { stdin, stdout } from 'node:process'
|
|
7
|
+
import { toSegments, executeMiddlewareChain } from './utils'
|
|
6
8
|
|
|
7
|
-
import
|
|
9
|
+
import {
|
|
10
|
+
buildSocketState,
|
|
11
|
+
buildSocketServer,
|
|
12
|
+
buildSocketHandlers,
|
|
13
|
+
buildSocketCommands,
|
|
14
|
+
} from './socket'
|
|
8
15
|
|
|
9
16
|
import {
|
|
10
17
|
NotFoundError,
|
|
@@ -13,10 +20,13 @@ import {
|
|
|
13
20
|
|
|
14
21
|
export * from './errors'
|
|
15
22
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
23
|
+
export {
|
|
24
|
+
parseJsonBody,
|
|
25
|
+
setValidationFormats,
|
|
26
|
+
validateSchemas,
|
|
27
|
+
} from './middleware'
|
|
28
|
+
|
|
29
|
+
const ALLOWED_FILES_META = ['meta.js', 'meta.ts']
|
|
20
30
|
|
|
21
31
|
const ALLOWED_FILES_METHODS = [
|
|
22
32
|
'head.js',
|
|
@@ -33,18 +43,11 @@ const ALLOWED_FILES_METHODS = [
|
|
|
33
43
|
'delete.ts',
|
|
34
44
|
]
|
|
35
45
|
|
|
36
|
-
const ALLOWED_FILES_ALL = [
|
|
37
|
-
...ALLOWED_FILES_META,
|
|
38
|
-
...ALLOWED_FILES_METHODS,
|
|
39
|
-
]
|
|
40
|
-
|
|
41
46
|
/* istanbul ignore if */
|
|
42
47
|
if (process.stdin.isTTY) {
|
|
43
48
|
process.stdin.setRawMode(true)
|
|
44
49
|
}
|
|
45
50
|
|
|
46
|
-
export const middleware = _middleware
|
|
47
|
-
|
|
48
51
|
const rl = readline.createInterface({
|
|
49
52
|
input: stdin,
|
|
50
53
|
output: stdout,
|
|
@@ -54,18 +57,32 @@ function methodNotAllowedHandler (_req) {
|
|
|
54
57
|
throw new MethodNotAllowedError()
|
|
55
58
|
}
|
|
56
59
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
60
|
+
function defaultMethodMap () {
|
|
61
|
+
return {
|
|
62
|
+
HEAD: methodNotAllowedHandler,
|
|
63
|
+
GET: methodNotAllowedHandler,
|
|
64
|
+
PUT: methodNotAllowedHandler,
|
|
65
|
+
POST: methodNotAllowedHandler,
|
|
66
|
+
PATCH: methodNotAllowedHandler,
|
|
67
|
+
DELETE: methodNotAllowedHandler,
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function buildBunRequest (bunReq, server) {
|
|
72
|
+
const url = new URL(bunReq.url)
|
|
73
|
+
const qs = url.search.replace('?', '')
|
|
74
|
+
const json = () => bunReq.json()
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
method: bunReq.method,
|
|
78
|
+
route: url.pathname,
|
|
79
|
+
headers: bunReq.headers,
|
|
80
|
+
params: bunReq.params ?? {},
|
|
81
|
+
query: querystring.parse(qs),
|
|
82
|
+
raw: bunReq,
|
|
83
|
+
server,
|
|
84
|
+
json,
|
|
85
|
+
}
|
|
69
86
|
}
|
|
70
87
|
|
|
71
88
|
function validateLeafDirectory (targetPath, filenames, entries) {
|
|
@@ -73,7 +90,7 @@ function validateLeafDirectory (targetPath, filenames, entries) {
|
|
|
73
90
|
|
|
74
91
|
if (!hasDirectories) {
|
|
75
92
|
const hasMethodEntry = filenames.some(filename =>
|
|
76
|
-
ALLOWED_FILES_METHODS.includes(filename)
|
|
93
|
+
ALLOWED_FILES_METHODS.includes(filename),
|
|
77
94
|
)
|
|
78
95
|
|
|
79
96
|
if (!hasMethodEntry) {
|
|
@@ -90,7 +107,6 @@ function validateDirectory (targetPath, entries) {
|
|
|
90
107
|
.filter(entry => entry.stat.isFile())
|
|
91
108
|
.map(entry => path.basename(entry.path))
|
|
92
109
|
|
|
93
|
-
validateDirectoryIllegalFiles(targetPath, filenames)
|
|
94
110
|
validateLeafDirectory(targetPath, filenames, entries)
|
|
95
111
|
}
|
|
96
112
|
|
|
@@ -109,19 +125,19 @@ function getAllFilePathsRec (targetPath, paths) {
|
|
|
109
125
|
validateDirectory(targetPath, children)
|
|
110
126
|
|
|
111
127
|
return children.reduce((accum, curr) => {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
128
|
+
const result = curr.stat.isDirectory()
|
|
129
|
+
? getAllFilePathsRec(curr.path, paths)
|
|
130
|
+
: [curr.path]
|
|
115
131
|
|
|
116
|
-
|
|
117
|
-
|
|
132
|
+
return [...accum, ...result]
|
|
133
|
+
}, [])
|
|
118
134
|
}
|
|
119
135
|
|
|
120
136
|
function getFilteredFilePaths (targetPath, allowedFiles) {
|
|
121
137
|
const allPaths = getAllFilePathsRec(targetPath, [])
|
|
122
138
|
|
|
123
139
|
return allPaths.filter(item =>
|
|
124
|
-
allowedFiles.includes(path.basename(item))
|
|
140
|
+
allowedFiles.includes(path.basename(item)),
|
|
125
141
|
)
|
|
126
142
|
}
|
|
127
143
|
|
|
@@ -133,8 +149,24 @@ function getMetaFilePaths (targetPath) {
|
|
|
133
149
|
return getFilteredFilePaths(targetPath, ALLOWED_FILES_META)
|
|
134
150
|
}
|
|
135
151
|
|
|
136
|
-
function
|
|
137
|
-
|
|
152
|
+
function selectMetaPaths (metadata, modulePath) {
|
|
153
|
+
return metadata
|
|
154
|
+
.filter(metaPath => modulePath.startsWith(path.dirname(metaPath)))
|
|
155
|
+
.sort((a, b) => a.length - b.length)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function resolveMetaMiddleware (metaPaths) {
|
|
159
|
+
const modules = await Promise.all(metaPaths.map(item => import(item)))
|
|
160
|
+
|
|
161
|
+
return modules
|
|
162
|
+
.map(item => item.middleware)
|
|
163
|
+
.reduce((accum, curr) => [
|
|
164
|
+
...accum,
|
|
165
|
+
...(curr || []),
|
|
166
|
+
], [])
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function buildRoutePaths (rootPath, mountPath, metadata) {
|
|
138
170
|
const paths = getMethodFilePaths(rootPath)
|
|
139
171
|
|
|
140
172
|
return paths.map(modulePath => {
|
|
@@ -147,13 +179,7 @@ function buildRoutesPaths (rootPath, mountPath) {
|
|
|
147
179
|
.filter(item => item)
|
|
148
180
|
.join('') || '/'
|
|
149
181
|
|
|
150
|
-
const metaMiddlewarePath = metadata
|
|
151
|
-
.filter(metaPath => {
|
|
152
|
-
const metaBasePath = path.dirname(metaPath)
|
|
153
|
-
|
|
154
|
-
return modulePath.startsWith(metaBasePath)
|
|
155
|
-
})
|
|
156
|
-
.sort((a, b) => a.length - b.length)
|
|
182
|
+
const metaMiddlewarePath = selectMetaPaths(metadata, modulePath)
|
|
157
183
|
|
|
158
184
|
return {
|
|
159
185
|
method: segments[lastIndex].toUpperCase(),
|
|
@@ -164,19 +190,9 @@ function buildRoutesPaths (rootPath, mountPath) {
|
|
|
164
190
|
})
|
|
165
191
|
}
|
|
166
192
|
|
|
167
|
-
async function
|
|
193
|
+
async function buildChain (route, rootMiddleware) {
|
|
168
194
|
const module = await import(route.modulePath)
|
|
169
|
-
|
|
170
|
-
const middlewareModules = await Promise.all(
|
|
171
|
-
route.metaMiddlewarePath.map(item => import(item))
|
|
172
|
-
)
|
|
173
|
-
|
|
174
|
-
const metaMiddleware = middlewareModules
|
|
175
|
-
.map(item => item.middleware)
|
|
176
|
-
.reduce((accum, curr) => [
|
|
177
|
-
...accum,
|
|
178
|
-
...(curr || []),
|
|
179
|
-
], [])
|
|
195
|
+
const metaMiddleware = await resolveMetaMiddleware(route.metaMiddlewarePath)
|
|
180
196
|
|
|
181
197
|
if (!module.default) {
|
|
182
198
|
throw new ReferenceError(`
|
|
@@ -189,62 +205,87 @@ ${route.modulePath}
|
|
|
189
205
|
? module.default
|
|
190
206
|
: [module.default]
|
|
191
207
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
208
|
+
return {
|
|
209
|
+
method: route.method,
|
|
210
|
+
path: route.path,
|
|
211
|
+
chain: [
|
|
212
|
+
...rootMiddleware,
|
|
213
|
+
...metaMiddleware,
|
|
214
|
+
...baseChain,
|
|
215
|
+
],
|
|
216
|
+
}
|
|
217
|
+
}
|
|
197
218
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
219
|
+
function buildNormalRoutes (routePaths, rootMiddleware) {
|
|
220
|
+
return Promise.all(
|
|
221
|
+
routePaths.map(route => buildChain(route, rootMiddleware)),
|
|
222
|
+
)
|
|
223
|
+
}
|
|
201
224
|
|
|
202
|
-
|
|
225
|
+
async function buildMergedRoutes (routePaths, middleware, state, opts) {
|
|
226
|
+
const { basePath, mountPath, metadata } = opts
|
|
227
|
+
const socketRoutes = buildSocketHandlers(state)
|
|
203
228
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
const isLastMiddleware = index === middlewareChain.length - 1
|
|
229
|
+
for (const socketRoute of socketRoutes) {
|
|
230
|
+
const mountedPath = `${mountPath}${socketRoute.path}`
|
|
207
231
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
232
|
+
const targetItem = routePaths.find(item => (
|
|
233
|
+
item.method === socketRoute.method &&
|
|
234
|
+
item.path === mountedPath
|
|
235
|
+
))
|
|
211
236
|
|
|
212
|
-
|
|
237
|
+
if (targetItem) {
|
|
238
|
+
targetItem.chain.push(socketRoute.handler)
|
|
213
239
|
|
|
214
|
-
|
|
215
|
-
return result
|
|
216
|
-
} else {
|
|
217
|
-
throw new TypeError('Handler does not return a Response object')
|
|
218
|
-
}
|
|
240
|
+
continue
|
|
219
241
|
}
|
|
220
242
|
|
|
221
|
-
|
|
243
|
+
const method = socketRoute.method.toLowerCase()
|
|
244
|
+
const modulePath = path.join(basePath, socketRoute.path, `${method}.js`)
|
|
245
|
+
const metaPaths = selectMetaPaths(metadata, modulePath)
|
|
246
|
+
const metaMiddleware = await resolveMetaMiddleware(metaPaths)
|
|
247
|
+
|
|
248
|
+
routePaths.push({
|
|
249
|
+
method: socketRoute.method,
|
|
250
|
+
path: mountedPath,
|
|
251
|
+
chain: [
|
|
252
|
+
...middleware,
|
|
253
|
+
...metaMiddleware,
|
|
254
|
+
socketRoute.handler,
|
|
255
|
+
],
|
|
256
|
+
})
|
|
222
257
|
}
|
|
223
258
|
|
|
224
|
-
return
|
|
225
|
-
method: route.method,
|
|
226
|
-
path: route.path,
|
|
227
|
-
handler,
|
|
228
|
-
}
|
|
259
|
+
return routePaths
|
|
229
260
|
}
|
|
230
261
|
|
|
231
|
-
function
|
|
232
|
-
return
|
|
233
|
-
|
|
234
|
-
|
|
262
|
+
function buildSocketRoutes (mergedRoutes) {
|
|
263
|
+
return mergedRoutes.map(route => ({
|
|
264
|
+
...route,
|
|
265
|
+
segments: toSegments(route.path),
|
|
266
|
+
}))
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function buildModuleRoutes (routePaths) {
|
|
270
|
+
return routePaths.map(route => {
|
|
271
|
+
const handler = async (bunReq, server) => {
|
|
272
|
+
const req = buildBunRequest(bunReq, server)
|
|
273
|
+
|
|
274
|
+
return executeMiddlewareChain(req, route.chain)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return {
|
|
278
|
+
method: route.method,
|
|
279
|
+
path: route.path,
|
|
280
|
+
handler,
|
|
281
|
+
}
|
|
282
|
+
})
|
|
235
283
|
}
|
|
236
284
|
|
|
237
285
|
function buildServerRoutes (moduleRoutes) {
|
|
238
286
|
return moduleRoutes.reduce((accum, curr) => {
|
|
239
287
|
if (!accum[curr.path]) {
|
|
240
|
-
accum[curr.path] =
|
|
241
|
-
HEAD: methodNotAllowedHandler,
|
|
242
|
-
GET: methodNotAllowedHandler,
|
|
243
|
-
PUT: methodNotAllowedHandler,
|
|
244
|
-
POST: methodNotAllowedHandler,
|
|
245
|
-
PATCH: methodNotAllowedHandler,
|
|
246
|
-
DELETE: methodNotAllowedHandler,
|
|
247
|
-
}
|
|
288
|
+
accum[curr.path] = defaultMethodMap()
|
|
248
289
|
}
|
|
249
290
|
|
|
250
291
|
accum[curr.path][curr.method] = curr.handler
|
|
@@ -262,43 +303,65 @@ function buildOutputRoutes (moduleRoutes) {
|
|
|
262
303
|
}, {})
|
|
263
304
|
}
|
|
264
305
|
|
|
265
|
-
async function buildRoutes (rootPath, opts) {
|
|
306
|
+
async function buildRoutes (rootPath, state, opts) {
|
|
266
307
|
const basePath = `${rootPath}/api`
|
|
267
308
|
const mountPath = opts.mountPath || ''
|
|
268
|
-
const
|
|
269
|
-
const
|
|
270
|
-
const
|
|
309
|
+
const middleware = opts.middleware || []
|
|
310
|
+
const metadata = getMetaFilePaths(basePath)
|
|
311
|
+
const routePaths = buildRoutePaths(basePath, mountPath, metadata)
|
|
312
|
+
const normalRoutes = await buildNormalRoutes(routePaths, middleware)
|
|
313
|
+
|
|
314
|
+
const routingOpts = {
|
|
315
|
+
basePath,
|
|
316
|
+
mountPath,
|
|
317
|
+
metadata,
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const mergedRoutes = await buildMergedRoutes(
|
|
321
|
+
normalRoutes,
|
|
322
|
+
middleware,
|
|
323
|
+
state,
|
|
324
|
+
routingOpts,
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
const socketRoutes = buildSocketRoutes(mergedRoutes)
|
|
328
|
+
const moduleRoutes = buildModuleRoutes(socketRoutes)
|
|
271
329
|
const serverRoutes = buildServerRoutes(moduleRoutes)
|
|
272
330
|
const outputRoutes = buildOutputRoutes(moduleRoutes)
|
|
273
331
|
|
|
274
332
|
return {
|
|
275
333
|
server: serverRoutes,
|
|
334
|
+
socket: socketRoutes,
|
|
276
335
|
output: outputRoutes,
|
|
277
336
|
}
|
|
278
337
|
}
|
|
279
338
|
|
|
280
|
-
function buildServer (port, routes, opts) {
|
|
339
|
+
function buildServer (port, routes, state, opts) {
|
|
281
340
|
const hostname = opts.hostname || '0.0.0.0'
|
|
341
|
+
const websocketServer = buildSocketServer(routes.socket, state)
|
|
282
342
|
|
|
283
343
|
return Bun.serve({
|
|
284
344
|
port,
|
|
285
345
|
hostname,
|
|
286
346
|
routes: routes.server,
|
|
287
|
-
|
|
347
|
+
websocket: websocketServer,
|
|
348
|
+
async fetch (_req, _server) {
|
|
288
349
|
throw new NotFoundError()
|
|
289
350
|
},
|
|
290
351
|
error (err) {
|
|
291
352
|
console.error(err)
|
|
292
353
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
354
|
+
const status = err.constructor.status ?? 500
|
|
355
|
+
|
|
356
|
+
return err.output !== undefined
|
|
357
|
+
? Response.json(err.output, { status })
|
|
358
|
+
: new Response(err.message, { status })
|
|
296
359
|
},
|
|
297
360
|
})
|
|
298
361
|
}
|
|
299
362
|
|
|
300
363
|
function processIO (port, server, opts) {
|
|
301
|
-
const onClose = opts.onClose || (() => {})
|
|
364
|
+
const onClose = opts.onClose || (() => { })
|
|
302
365
|
|
|
303
366
|
console.info(`Running on port: ${port}`)
|
|
304
367
|
console.info('')
|
|
@@ -314,13 +377,16 @@ function processIO (port, server, opts) {
|
|
|
314
377
|
}
|
|
315
378
|
|
|
316
379
|
export async function createApp (port, rootPath, opts = {}) {
|
|
317
|
-
const
|
|
318
|
-
const
|
|
380
|
+
const state = buildSocketState(opts)
|
|
381
|
+
const routes = await buildRoutes(rootPath, state, opts)
|
|
382
|
+
const server = buildServer(port, routes, state, opts)
|
|
383
|
+
const commands = buildSocketCommands(state)
|
|
319
384
|
|
|
320
385
|
processIO(port, server, opts)
|
|
321
386
|
|
|
322
387
|
return {
|
|
323
388
|
routes: routes.output,
|
|
324
389
|
server,
|
|
390
|
+
commands,
|
|
325
391
|
}
|
|
326
392
|
}
|
package/src/messages.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import Ajv from 'ajv'
|
|
2
|
+
import addFormats from 'ajv-formats'
|
|
3
|
+
import crypto from 'node:crypto'
|
|
4
|
+
import { formatError } from './utils'
|
|
5
|
+
import { UnprocessableContentError } from './errors'
|
|
6
|
+
|
|
7
|
+
export const TYPES = {
|
|
8
|
+
REQUEST: 'request',
|
|
9
|
+
RESPONSE: 'response',
|
|
10
|
+
WELCOME: 'welcome',
|
|
11
|
+
HEARTBEAT: 'heartbeat',
|
|
12
|
+
NOTIFICATION: 'notification',
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const TYPES_RECEIVED = [
|
|
16
|
+
TYPES.HEARTBEAT,
|
|
17
|
+
TYPES.REQUEST,
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
const ajv = new Ajv({
|
|
21
|
+
allErrors: true,
|
|
22
|
+
removeAdditional: 'all',
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
addFormats(ajv)
|
|
26
|
+
|
|
27
|
+
const SCHEMA_BASE = {
|
|
28
|
+
type: 'object',
|
|
29
|
+
properties: {
|
|
30
|
+
id: {
|
|
31
|
+
type: 'string',
|
|
32
|
+
format: 'uuid',
|
|
33
|
+
},
|
|
34
|
+
clientId: {
|
|
35
|
+
type: 'string',
|
|
36
|
+
format: 'uuid',
|
|
37
|
+
},
|
|
38
|
+
type: {
|
|
39
|
+
type: 'string',
|
|
40
|
+
enum: TYPES_RECEIVED,
|
|
41
|
+
},
|
|
42
|
+
timestamp: {
|
|
43
|
+
type: 'string',
|
|
44
|
+
format: 'date-time',
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
required: [
|
|
48
|
+
'id',
|
|
49
|
+
'clientId',
|
|
50
|
+
'type',
|
|
51
|
+
'timestamp',
|
|
52
|
+
],
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const validateHeartbeat = ajv.compile({
|
|
56
|
+
type: 'object',
|
|
57
|
+
properties: {
|
|
58
|
+
...SCHEMA_BASE.properties,
|
|
59
|
+
type: {
|
|
60
|
+
type: 'string',
|
|
61
|
+
const: TYPES.HEARTBEAT,
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
required: SCHEMA_BASE.required,
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
const validateRequest = ajv.compile({
|
|
68
|
+
type: 'object',
|
|
69
|
+
properties: {
|
|
70
|
+
...SCHEMA_BASE.properties,
|
|
71
|
+
type: {
|
|
72
|
+
type: 'string',
|
|
73
|
+
const: TYPES.REQUEST,
|
|
74
|
+
},
|
|
75
|
+
method: {
|
|
76
|
+
type: 'string',
|
|
77
|
+
enum: [
|
|
78
|
+
'HEAD',
|
|
79
|
+
'GET',
|
|
80
|
+
'PUT',
|
|
81
|
+
'POST',
|
|
82
|
+
'PATCH',
|
|
83
|
+
'DELETE',
|
|
84
|
+
],
|
|
85
|
+
},
|
|
86
|
+
route: {
|
|
87
|
+
type: 'string',
|
|
88
|
+
format: 'uri-reference',
|
|
89
|
+
},
|
|
90
|
+
headers: {
|
|
91
|
+
type: 'object',
|
|
92
|
+
},
|
|
93
|
+
query: {
|
|
94
|
+
type: 'object',
|
|
95
|
+
},
|
|
96
|
+
body: {
|
|
97
|
+
type: [
|
|
98
|
+
'boolean',
|
|
99
|
+
'number',
|
|
100
|
+
'string',
|
|
101
|
+
'object',
|
|
102
|
+
'array',
|
|
103
|
+
'null',
|
|
104
|
+
],
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
required: [
|
|
108
|
+
...SCHEMA_BASE.required,
|
|
109
|
+
'method',
|
|
110
|
+
'route',
|
|
111
|
+
'headers',
|
|
112
|
+
'query',
|
|
113
|
+
'body',
|
|
114
|
+
],
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
const TYPE_VALIDATORS = {
|
|
118
|
+
[TYPES.HEARTBEAT]: validateHeartbeat,
|
|
119
|
+
[TYPES.REQUEST]: validateRequest,
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function createMessage (clientId, type, opts = {}) {
|
|
123
|
+
const timestamp = new Date().toISOString()
|
|
124
|
+
|
|
125
|
+
const base = {
|
|
126
|
+
id: opts.id ?? crypto.randomUUID(),
|
|
127
|
+
clientId,
|
|
128
|
+
type,
|
|
129
|
+
timestamp,
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
...opts,
|
|
134
|
+
...base,
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function validateMessage (message) {
|
|
139
|
+
const validate = TYPE_VALIDATORS[message.type]
|
|
140
|
+
|
|
141
|
+
if (message.type === undefined) {
|
|
142
|
+
throw new UnprocessableContentError([
|
|
143
|
+
{
|
|
144
|
+
path: '',
|
|
145
|
+
message: `must have required property 'type'`,
|
|
146
|
+
},
|
|
147
|
+
])
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (!TYPES_RECEIVED.includes(message.type)) {
|
|
151
|
+
throw new UnprocessableContentError([
|
|
152
|
+
{
|
|
153
|
+
path: 'type',
|
|
154
|
+
message: `must be one of: ${TYPES_RECEIVED}`,
|
|
155
|
+
},
|
|
156
|
+
])
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (!validate(message)) {
|
|
160
|
+
const errors = validate.errors.map(item => formatError('', item))
|
|
161
|
+
|
|
162
|
+
throw new UnprocessableContentError(errors)
|
|
163
|
+
}
|
|
164
|
+
}
|