/
api.ts
138 lines (126 loc) · 4.5 KB
/
api.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
import { compress as brotli } from 'https://deno.land/x/brotli@v0.1.4/mod.ts'
import { gzipEncode } from 'https://deno.land/x/wasm_gzip@v1.0.0/mod.ts'
import log from './log.ts'
import { ServerRequest } from './std.ts'
import type { APIRequest } from './types.ts'
export class Request extends ServerRequest implements APIRequest {
#pathname: string
#params: Record<string, string>
#query: URLSearchParams
#cookies: ReadonlyMap<string, string>
#resp = {
status: 200,
headers: new Headers({
'Status': '200',
'Server': 'Aleph.js',
}),
done: false
}
constructor(req: ServerRequest, pathname: string, params: Record<string, string>, query: URLSearchParams) {
super()
this.conn = req.conn
this.r = req.r
this.w = req.w
this.method = req.method
this.url = req.url
this.proto = req.proto
this.protoMinor = req.protoMinor
this.protoMajor = req.protoMajor
this.headers = req.headers
this.done = req.done
this.#pathname = pathname
this.#params = params
this.#query = query
const cookies = new Map()
this.headers.get('cookie')?.split(';').forEach(cookie => {
const p = cookie.trim().split('=')
if (p.length >= 2) {
cookies.set(p.shift()!.trim(), decodeURI(p.join('=')))
}
})
this.#cookies = cookies
}
get pathname(): string {
return this.#pathname
}
get params(): Record<string, string> {
return this.#params
}
get query(): URLSearchParams {
return this.#query
}
get cookies(): ReadonlyMap<string, string> {
return this.#cookies
}
status(code: number): this {
this.#resp.headers.set('status', code.toString())
this.#resp.status = code
return this
}
addHeader(key: string, value: string): this {
this.#resp.headers.append(key, value)
return this
}
setHeader(key: string, value: string): this {
this.#resp.headers.set(key, value)
return this
}
removeHeader(key: string): this {
this.#resp.headers.delete(key)
return this
}
async json(data: any, replacer?: (this: any, key: string, value: any) => any, space?: string | number): Promise<void> {
await this.send(JSON.stringify(data, replacer, space), 'application/json; charset=utf-8')
}
async send(data: string | Uint8Array | ArrayBuffer, contentType?: string): Promise<void> {
if (this.#resp.done) {
log.warn('ServerRequest: repeat respond calls')
return
}
let body: Uint8Array
if (typeof data === 'string') {
body = new TextEncoder().encode(data)
} else if (data instanceof ArrayBuffer) {
body = new Uint8Array(data)
} else if (data instanceof Uint8Array) {
body = data
} else {
return
}
if (contentType) {
this.#resp.headers.set('Content-Type', contentType)
} else if (this.#resp.headers.has('Content-Type')) {
contentType = this.#resp.headers.get('Content-Type')!
} else if (typeof data === 'string' && data.length > 0) {
contentType = 'text/plain; charset=utf-8'
}
let isText = false
if (contentType) {
if (contentType.startsWith('text/')) {
isText = true
} else if (/^application\/(javascript|typecript|json|xml)/i.test(contentType)) {
isText = true
} else if (/^image\/svg+xml/i.test(contentType)) {
isText = true
}
}
if (isText && body.length > 1024) {
if (this.headers.get('accept-encoding')?.includes('br')) {
this.#resp.headers.set('Vary', 'Origin')
this.#resp.headers.set('Content-Encoding', 'br')
body = brotli(body)
} else if (this.headers.get('accept-encoding')?.includes('gzip')) {
this.#resp.headers.set('Vary', 'Origin')
this.#resp.headers.set('Content-Encoding', 'gzip')
body = gzipEncode(body)
}
}
this.#resp.headers.set('Date', (new Date).toUTCString())
this.#resp.done = true
await this.respond({
status: this.#resp.status,
headers: this.#resp.headers,
body
}).catch(err => log.warn('ServerRequest.respond:', err.message))
}
}