forked from oakserver/oak
-
Notifications
You must be signed in to change notification settings - Fork 0
/
context.ts
201 lines (184 loc) · 6.82 KB
/
context.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
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
// Copyright 2018-2021 the oak authors. All rights reserved. MIT license.
import type { Application, State } from "./application.ts";
import { Cookies } from "./cookies.ts";
import { acceptable, acceptWebSocket, WebSocket } from "./deps.ts";
import { NativeRequest } from "./http_server_native.ts";
import type { ServerRequest } from "./http_server_std.ts";
import { createHttpError } from "./httpError.ts";
import type { KeyStack } from "./keyStack.ts";
import { Request } from "./request.ts";
import { Response } from "./response.ts";
import { send, SendOptions } from "./send.ts";
import {
ServerSentEventTargetOptions,
SSEStdLibTarget,
SSEStreamTarget,
} from "./server_sent_event.ts";
import type { ServerSentEventTarget } from "./server_sent_event.ts";
import { structuredClone } from "./structured_clone.ts";
import type { ErrorStatus } from "./types.d.ts";
export interface ContextSendOptions extends SendOptions {
/** The filename to send, which will be resolved based on the other options.
* If this property is omitted, the current context's `.request.url.pathname`
* will be used. */
path?: string;
}
/** Provides context about the current request and response to middleware
* functions. */
export class Context<
S extends AS = State,
// deno-lint-ignore no-explicit-any
AS extends State = Record<string, any>,
> {
#socket?: WebSocket;
#sse?: ServerSentEventTarget;
/** A reference to the current application. */
app: Application<AS>;
/** An object which allows access to cookies, mediating both the request and
* response. */
cookies: Cookies;
/** Is `true` if the current connection is upgradeable to a web socket.
* Otherwise the value is `false`. Use `.upgrade()` to upgrade the connection
* and return the web socket. */
get isUpgradable(): boolean {
return acceptable(this.request);
}
/** Determines if the request should be responded to. If `false` when the
* middleware completes processing, the response will not be sent back to the
* requestor. Typically this is used if the middleware will take over low
* level processing of requests and responses, for example if using web
* sockets. This automatically gets set to `false` when the context is
* upgraded to a web socket via the `.upgrade()` method.
*
* The default is `true`. */
respond: boolean;
/** An object which contains information about the current request. */
request: Request;
/** An object which contains information about the response that will be sent
* when the middleware finishes processing. */
response: Response;
/** If the the current context has been upgraded, then this will be set to
* with the web socket, otherwise it is `undefined`. */
get socket(): WebSocket | undefined {
return this.#socket;
}
/** The object to pass state to front-end views. This can be typed by
* supplying the generic state argument when creating a new app. For
* example:
*
* ```ts
* const app = new Application<{ foo: string }>();
* ```
*
* Or can be contextually inferred based on setting an initial state object:
*
* ```ts
* const app = new Application({ state: { foo: "bar" } });
* ```
*
* On each request/response cycle, the context's state is cloned from the
* application state. This means changes to the context's `.state` will be
* dropped when the request drops, but "defaults" can be applied to the
* application's state. Changes to the application's state though won't be
* reflected until the next request in the context's state.
*/
state: S;
constructor(
app: Application<AS>,
serverRequest: ServerRequest | NativeRequest,
secure = false,
) {
this.app = app;
this.state = structuredClone(app.state);
this.request = new Request(serverRequest, app.proxy, secure);
this.respond = true;
this.response = new Response(this.request);
this.cookies = new Cookies(this.request, this.response, {
keys: this.app.keys as KeyStack | undefined,
secure: this.request.secure,
});
}
/** Asserts the condition and if the condition fails, creates an HTTP error
* with the provided status (which defaults to `500`). The error status by
* default will be set on the `.response.status`.
*/
assert(
// deno-lint-ignore no-explicit-any
condition: any,
errorStatus: ErrorStatus = 500,
message?: string,
props?: Record<string, unknown>,
): asserts condition {
if (condition) {
return;
}
const err = createHttpError(errorStatus, message);
if (props) {
Object.assign(err, props);
}
throw err;
}
/** Asynchronously fulfill a response with a file from the local file
* system.
*
* If the `options.path` is not supplied, the file to be sent will default
* to this `.request.url.pathname`.
*
* Requires Deno read permission. */
send(options: ContextSendOptions): Promise<string | undefined> {
const { path = this.request.url.pathname, ...sendOptions } = options;
return send(this, path, sendOptions);
}
/** Convert the connection to stream events, returning an event target for
* sending server sent events. Events dispatched on the returned target will
* be sent to the client and be available in the client's `EventSource` that
* initiated the connection.
*
* This will set `.respond` to `false`. */
sendEvents(options?: ServerSentEventTargetOptions): ServerSentEventTarget {
if (!this.#sse) {
if (this.request.originalRequest instanceof NativeRequest) {
this.#sse = new SSEStreamTarget(this, options);
} else {
this.respond = false;
this.#sse = new SSEStdLibTarget(this, options);
}
}
return this.#sse;
}
/** Create and throw an HTTP Error, which can be used to pass status
* information which can be caught by other middleware to send more
* meaningful error messages back to the client. The passed error status will
* be set on the `.response.status` by default as well.
*/
throw(
errorStatus: ErrorStatus,
message?: string,
props?: Record<string, unknown>,
): never {
const err = createHttpError(errorStatus, message);
if (props) {
Object.assign(err, props);
}
throw err;
}
/** Take the current request and upgrade it to a web socket, resolving with
* the web socket object. This will set `.respond` to `false`. */
async upgrade(): Promise<WebSocket> {
if (this.#socket) {
return this.#socket;
}
if (this.request.originalRequest instanceof NativeRequest) {
throw new TypeError(
"Socket upgrades are not yet supported on native Deno requests.",
);
}
const { conn, r: bufReader, w: bufWriter, headers } =
this.request.originalRequest;
this.#socket = await acceptWebSocket(
{ conn, bufReader, bufWriter, headers },
);
this.respond = false;
return this.#socket;
}
}