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/src/utils.ts ADDED
@@ -0,0 +1,204 @@
1
+ import type { ErrorObject } from 'ajv'
2
+ import type { BunRequest, Server as BunServer } from 'bun'
3
+
4
+ export const HttpMethod = {
5
+ Head: 'HEAD',
6
+ Get: 'GET',
7
+ Post: 'POST',
8
+ Put: 'PUT',
9
+ Patch: 'PATCH',
10
+ Delete: 'DELETE',
11
+ } as const
12
+
13
+ export type HttpMethod = typeof HttpMethod[keyof typeof HttpMethod]
14
+
15
+ export const StatusCode = {
16
+ Continue: 100,
17
+ SwitchingProtocols: 101,
18
+ Processing: 102,
19
+ EarlyHints: 103,
20
+
21
+ Ok: 200,
22
+ Created: 201,
23
+ Accepted: 202,
24
+ NonAuthoritativeInformation: 203,
25
+ NoContent: 204,
26
+ ResetContent: 205,
27
+ PartialContent: 206,
28
+ MultiStatus: 207,
29
+ AlreadyReported: 208,
30
+ ImUsed: 226,
31
+
32
+ MultipleChoices: 300,
33
+ MovedPermanently: 301,
34
+ Found: 302,
35
+ SeeOther: 303,
36
+ NotModified: 304,
37
+ UseProxy: 305,
38
+ TemporaryRedirect: 307,
39
+ PermanentRedirect: 308,
40
+
41
+ BadRequest: 400,
42
+ Unauthorized: 401,
43
+ PaymentRequired: 402,
44
+ Forbidden: 403,
45
+ NotFound: 404,
46
+ MethodNotAllowed: 405,
47
+ NotAcceptable: 406,
48
+ ProxyAuthenticationRequired: 407,
49
+ RequestTimeout: 408,
50
+ Conflict: 409,
51
+ Gone: 410,
52
+ LengthRequired: 411,
53
+ PreconditionFailed: 412,
54
+ PayloadTooLarge: 413,
55
+ UriTooLong: 414,
56
+ UnsupportedMediaType: 415,
57
+ RangeNotSatisfiable: 416,
58
+ ExpectationFailed: 417,
59
+ ImATeapot: 418,
60
+ MisdirectedRequest: 421,
61
+ UnprocessableContent: 422,
62
+ Locked: 423,
63
+ FailedDependency: 424,
64
+ TooEarly: 425,
65
+ UpgradeRequired: 426,
66
+ PreconditionRequired: 428,
67
+ TooManyRequests: 429,
68
+ RequestHeaderFieldsTooLarge: 431,
69
+ UnavailableForLegalReasons: 451,
70
+
71
+ InternalServerError: 500,
72
+ NotImplemented: 501,
73
+ BadGateway: 502,
74
+ ServiceUnavailable: 503,
75
+ GatewayTimeout: 504,
76
+ HTTPVersionNotSupported: 505,
77
+ VariantAlsoNegotiates: 506,
78
+ InsufficientStorage: 507,
79
+ LoopDetected: 508,
80
+ NotExtended: 510,
81
+ NetworkAuthenticationRequired: 511,
82
+ } as const
83
+
84
+ export type StatusCode = typeof StatusCode[keyof typeof StatusCode]
85
+
86
+ export type ValidationError = Partial<ErrorObject>
87
+
88
+ export type FormattedError = {
89
+ path: string
90
+ message: string
91
+ }
92
+
93
+ export type NextFn = (data?: unknown) => Response | Promise<Response>
94
+
95
+ export type BaseRequest = {
96
+ method: HttpMethod
97
+ route: string
98
+ headers: Headers
99
+ params: Record<string, string>
100
+ query: Record<string, unknown>
101
+ json: () => Promise<unknown>
102
+ }
103
+
104
+ export type EndpointRequest = BaseRequest & {
105
+ raw: BunRequest
106
+ server: Server
107
+ }
108
+
109
+ export type WebSocketRequest = BaseRequest & {
110
+ id: string
111
+ clientId: string
112
+ }
113
+
114
+ export type Request = EndpointRequest | WebSocketRequest
115
+
116
+ export type Middleware = (
117
+ req: Request,
118
+ res: unknown,
119
+ next: NextFn | null,
120
+ ) => unknown
121
+
122
+ export type SocketOptions = {
123
+ disconnectThreshold?: number
124
+ heartbeatInterval?: number
125
+ maxTickets?: number
126
+ reclaimTtl?: number
127
+ ticketTtl?: number
128
+ }
129
+
130
+ export type SocketData = {
131
+ clientId: string
132
+ superseded: boolean
133
+ reaped: boolean
134
+ reaperHandle: ReturnType<typeof setTimeout> | null
135
+ }
136
+
137
+ export type Server = BunServer<SocketData>
138
+
139
+ export type AppOptions = {
140
+ hostname?: string
141
+ mountPath?: string
142
+ middleware?: Middleware[]
143
+ ws?: SocketOptions
144
+ onClose?: () => Promise<void> | void
145
+ }
146
+
147
+ export function toSegments (pathString: string): string[] {
148
+ const [pathname] = String(pathString).split('?')
149
+ const segments = pathname.split('/')
150
+
151
+ if (pathname.startsWith('/')) {
152
+ segments.shift()
153
+ }
154
+
155
+ if (pathname.endsWith('/')) {
156
+ segments.pop()
157
+ }
158
+
159
+ return segments
160
+ }
161
+
162
+ export function formatError (
163
+ prefix: string,
164
+ input: ValidationError,
165
+ ): FormattedError {
166
+ const fixedPath = input.instancePath || '/'
167
+ const suffixPath = fixedPath.replace(/\//g, '.').replace('.', '')
168
+
169
+ return {
170
+ path: [prefix, suffixPath].filter(item => item).join('.'),
171
+ message: input.message ?? '',
172
+ }
173
+ }
174
+
175
+ export async function executeMiddlewareChain (
176
+ req: Request,
177
+ chain: Middleware[],
178
+ ): Promise<Response> {
179
+ if (!chain.length) {
180
+ throw new RangeError('Middleware chain is empty')
181
+ }
182
+
183
+ const executeMiddleware = async (
184
+ index: number,
185
+ 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)
195
+
196
+ if (result instanceof Response) {
197
+ return result
198
+ } else {
199
+ throw new TypeError('Handler does not return a Response object')
200
+ }
201
+ }
202
+
203
+ return executeMiddleware(0, null)
204
+ }
package/src/messages.js DELETED
@@ -1,164 +0,0 @@
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
- }
package/src/meta.js DELETED
@@ -1,65 +0,0 @@
1
- export function range (count, startIndex = 0) {
2
- return new Array(count).fill(0).map((_, index) => index + startIndex)
3
- }
4
-
5
- export function traverse (obj, onKey, includeRoot = false) {
6
- const path = ['']
7
-
8
- const fn = target => {
9
- Object.entries(target).forEach(([k, v]) => {
10
- path[path.length - 1] = k
11
-
12
- const dateType = v instanceof Date
13
- const clip = onKey([...path], v) === false
14
- const updatedVal = getValueByPath(obj, path)
15
- const nonNullObj = updatedVal !== null && typeof updatedVal === 'object'
16
-
17
- if (!clip && !dateType && nonNullObj) {
18
- path.push('')
19
- fn(updatedVal)
20
- path.pop()
21
- }
22
- })
23
- }
24
-
25
- if (includeRoot) {
26
- onKey([], obj)
27
- }
28
-
29
- fn(obj)
30
- }
31
-
32
- export function map (obj, onKey) {
33
- const result = Array.isArray(obj) ? [] : {}
34
-
35
- traverse(obj, (keyPath, value) => {
36
- const dateType = value instanceof Date
37
-
38
- if (!dateType && value !== null && typeof value === 'object') {
39
- setValueByPath(result, keyPath, Array.isArray(value) ? [] : {})
40
- } else {
41
- setValueByPath(result, keyPath, onKey(keyPath, value))
42
- }
43
- })
44
-
45
- return result
46
- }
47
-
48
- export function deepCopy (obj) {
49
- return map(obj, (_, value) => value)
50
- }
51
-
52
- export function setValueByPath (obj, keyPath, value) {
53
- keyPath.reduce((subObj, key, index) => {
54
- if (index === keyPath.length - 1) {
55
- subObj[key] = value
56
- } else {
57
- return subObj[key]
58
- }
59
- }, obj)
60
- }
61
-
62
- export function getValueByPath (obj, keyPath) {
63
- return keyPath.reduce((obj, key) =>
64
- (typeof obj !== 'undefined' ? obj[key] : undefined), obj)
65
- }
package/src/middleware.js DELETED
@@ -1,138 +0,0 @@
1
- import Ajv from 'ajv'
2
- import addFormats from 'ajv-formats'
3
-
4
- import { formatError } from './utils'
5
-
6
- import {
7
- BadRequestError,
8
- UnsupportedMediaTypeError,
9
- UnprocessableContentError,
10
- } from './errors'
11
-
12
- let _schemasCompiled = false
13
- let _customFormats = null
14
-
15
- async function parseBody (req) {
16
- try {
17
- const result = await req.json()
18
-
19
- return result
20
- } catch {
21
- throw new BadRequestError('Invalid JSON')
22
- }
23
- }
24
-
25
- function buildFormatterSchema (schema) {
26
- const properties = Object
27
- .entries(schema)
28
- .map(([key, config]) => [
29
- key,
30
- {
31
- type: 'string',
32
- [config.type]: config.value,
33
- },
34
- ])
35
- .reduce((accum, [key, value]) => ({
36
- ...accum,
37
- [key]: value,
38
- }), {})
39
-
40
- return {
41
- type: 'object',
42
- properties,
43
- }
44
- }
45
-
46
- function compileSchemas (schemas) {
47
- return Object
48
- .entries(schemas)
49
- .reduce((accum, [key, schema]) => {
50
- const formattedSchema = key !== 'body'
51
- ? buildFormatterSchema(schema)
52
- : schema
53
-
54
- return [
55
- ...accum,
56
- [key, formattedSchema],
57
- ]
58
- }, [])
59
- .map(([key, schema]) => {
60
- const ajv = new Ajv({
61
- allErrors: true,
62
- removeAdditional: 'all',
63
- })
64
-
65
- addFormats(ajv)
66
-
67
- Object
68
- .entries(_customFormats ?? [])
69
- .forEach(([k, v]) => ajv.addFormat(k, v))
70
-
71
- const validator = ajv.compile(schema)
72
-
73
- return [key, validator]
74
- })
75
- }
76
-
77
- export function parseJsonBody () {
78
- return async (req, res, next) => {
79
- const contentType = req.headers.get('content-type')
80
-
81
- if (!contentType) {
82
- return next(res)
83
- }
84
-
85
- if (!contentType.startsWith('application/json')) {
86
- throw new UnsupportedMediaTypeError('content-type')
87
- }
88
-
89
- const body = await parseBody(req)
90
-
91
- return next(body)
92
- }
93
- }
94
-
95
- export function setValidationFormats (formats) {
96
- if (_customFormats) {
97
- console.warn('setValidationFormats() - already initialized')
98
- }
99
-
100
- if (_schemasCompiled) {
101
- console.warn('setValidationFormats() - called after compilation')
102
- }
103
-
104
- _customFormats = formats
105
- }
106
-
107
- /* only for testing purposes */
108
-
109
- export function resetValidationFormatsState () {
110
- _customFormats = null
111
- _schemasCompiled = false
112
- }
113
-
114
- export function validateSchemas (schemas) {
115
- const entries = compileSchemas(schemas)
116
-
117
- _schemasCompiled = true
118
-
119
- return (req, res, next) => {
120
- const errors = entries.reduce((accum, [key, validator]) => {
121
- const data = key === 'body' ? res : req[key]
122
- const valid = validator(data)
123
-
124
- return !valid
125
- ? [
126
- ...accum,
127
- ...validator.errors.map(item => formatError(key, item)),
128
- ]
129
- : accum
130
- }, [])
131
-
132
- if (errors.length > 0) {
133
- throw new UnprocessableContentError(errors)
134
- }
135
-
136
- return next(res)
137
- }
138
- }
package/src/utils.js DELETED
@@ -1,49 +0,0 @@
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
- }