Skip to content

Commit bb1ded0

Browse files
authored
Merge pull request #76 from yossydev/feat/fetch-response
feat(ext/fetch): Response
2 parents 97209d0 + 198bb01 commit bb1ded0

File tree

5 files changed

+302
-0
lines changed

5 files changed

+302
-0
lines changed

examples/response.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
const url = "https://example.com";
2+
const response1 = new Response(`${url}`, {});
3+
4+
console.log("body", response1.body);
5+
console.log("status", response1.status);
6+
console.log("url", response1.url);
7+
console.log("ok", response1.ok);
8+
console.log("redirected", response1.redirected);
9+
console.log("type", response1.type);
10+
console.log("statusText", response1.statusText);

runtime/src/ext/fetch/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
mod headers;
66
mod request;
7+
mod response;
78

89
pub use headers::*;
910
pub use request::*;
11+
pub use response::*;
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4+
5+
use andromeda_core::Extension;
6+
7+
#[derive(Default)]
8+
pub struct ResponseExt;
9+
10+
impl ResponseExt {
11+
pub fn new_extension() -> Extension {
12+
Extension {
13+
name: "response",
14+
ops: vec![],
15+
storage: None,
16+
files: vec![include_str!("./mod.ts")],
17+
}
18+
}
19+
}
Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
interface ResponseInit {
2+
headers?: HeadersInit;
3+
status?: number;
4+
statusText?: string;
5+
}
6+
7+
class Response {
8+
#response;
9+
#headers: any;
10+
/**
11+
* The new Response(body, init) constructor steps are:
12+
* @see https://fetch.spec.whatwg.org/#dom-response
13+
*/
14+
constructor(body: any, init: ResponseInit) {
15+
// 1. Set this’s response to a new response.
16+
this.#response = makeResponse(init);
17+
18+
// TODO: implement module
19+
// 2. Set this’s headers to a new Headers object with this’s relevant realm, whose header list is this’s response’s header list and guard is "response".
20+
21+
// 3. Let bodyWithType be null.
22+
let bodyWithType = null;
23+
24+
// 4. If body is non-null, then set bodyWithType to the result of extracting body.
25+
if (body != null) {
26+
const [extractedBody, type] = extractBody(body);
27+
bodyWithType = { body: extractedBody, type };
28+
}
29+
// 5. Perform initialize a response given this, init, and bodyWithType.
30+
initializeAResponse(this, init, bodyWithType);
31+
}
32+
33+
static getResponse(response: Response) {
34+
return response.#response;
35+
}
36+
37+
/** The type getter steps are to return this’s response’s type. */
38+
get type() {
39+
return this.#response.type;
40+
}
41+
42+
/**
43+
* The url getter steps are to return the empty string if this’s response’s URL is null;
44+
* otherwise this’s response’s URL, serialized with exclude fragment set to true.
45+
*/
46+
get url() {
47+
return this.#response.url;
48+
}
49+
50+
/** The redirected getter steps are to return true if this’s response’s URL list’s size is greater than 1; otherwise false. */
51+
get redirected() {
52+
return this.#response.url.length > 1;
53+
}
54+
55+
/** The status getter steps are to return this’s response’s status. */
56+
get status() {
57+
return this.#response.status;
58+
}
59+
60+
/** The ok getter steps are to return true if this’s response’s status is an ok status; otherwise false. */
61+
get ok() {
62+
const status = this.#response.status;
63+
return status >= 200 && status <= 299;
64+
}
65+
66+
/** The statusText getter steps are to return this’s response’s status message. */
67+
get statusText() {
68+
return this.#response.statusText;
69+
}
70+
71+
/** The headers getter steps are to return this’s headers. */
72+
get headers() {
73+
return this.#headers;
74+
}
75+
76+
// TODO
77+
get body() {
78+
return this.#response.body;
79+
}
80+
}
81+
82+
const { getResponse } = Response;
83+
84+
// TODO: headers
85+
function makeResponse(init: ResponseInit) {
86+
return {
87+
aborted: false,
88+
rangeRequested: false,
89+
timingAllowPassed: false,
90+
requestIncludesCredentials: false,
91+
type: "default",
92+
status: 200,
93+
timingInfo: null,
94+
cacheState: "",
95+
statusText: "",
96+
url: "",
97+
body: null,
98+
...init,
99+
};
100+
}
101+
102+
function initializeAResponse(
103+
response: Response,
104+
init: ResponseInit,
105+
body: {
106+
body: any;
107+
type: any;
108+
} | null,
109+
) {
110+
// 1. If init["status"] is not in the range 200 to 599, inclusive, then throw a RangeError.
111+
if (
112+
init.status != null && (init.status < 200 || init.status > 599)
113+
) {
114+
throw new RangeError(
115+
`The status provided (${init.status}) is not equal to 101 and outside the range [200, 599]`,
116+
);
117+
}
118+
119+
// 2. If init["statusText"] is not the empty string and does not match the reason-phrase token production, then throw a TypeError.
120+
// TODO: implement RegExp.
121+
if (
122+
init.statusText && isValidReasonPhrase(init.statusText)
123+
) {
124+
throw new TypeError(
125+
`Invalid status text: "${init.statusText}"`,
126+
);
127+
}
128+
129+
// 3. Set response’s response’s status to init["status"].
130+
if (init.status != null) {
131+
getResponse(response).status = init.status;
132+
}
133+
134+
// 4. Set response’s response’s status message to init["statusText"].
135+
if (init.statusText != null) {
136+
getResponse(response).statusText = init.statusText;
137+
}
138+
139+
// 5. If init["headers"] exists, then fill response’s headers with init["headers"].
140+
if (init.headers != null) {
141+
// TODO: get headerlist
142+
getResponse(response).headers = init.headers;
143+
}
144+
145+
// 6. If body is non-null, then:
146+
if (body != null) {
147+
// 1. If response’s status is a null body status, then throw a TypeError.
148+
// NOTE: 101 and 103 are included in null body status due to their use elsewhere. They do not affect this step.
149+
if (nullBodyStatus(response.status)) {
150+
throw new TypeError(
151+
"Response with null body status cannot have body",
152+
);
153+
}
154+
// 2. Set response’s body to body’s body.
155+
getResponse(response).body = body.body;
156+
// 3. If body’s type is non-null and response’s header list does not contain `Content-Type`, then append (`Content-Type`, body’s type) to response’s header list.
157+
}
158+
}
159+
160+
/**
161+
* TODO: when implemented module, move to ext/fetch/body
162+
* To extract a body with type from a byte sequence or BodyInit object object,
163+
* with an optional boolean keepalive (default false)
164+
* @see https://fetch.spec.whatwg.org/#concept-bodyinit-extract
165+
*/
166+
function extractBody(object: any, _keepalive = false) {
167+
// 1. Let stream be null.
168+
let stream = null;
169+
// 2. If object is a ReadableStream object, then set stream to object.
170+
// TODO: implement ReadableStream
171+
// 3. Otherwise, if object is a Blob object, set stream to the result of running object’s get stream.
172+
// 4. Otherwise, set stream to a new ReadableStream object, and set up stream with byte reading support.
173+
// 5. Assert: stream is a ReadableStream object.
174+
175+
// 6, Let action be null.
176+
let _action = null;
177+
178+
// 7. Let source be null.
179+
let source = null;
180+
181+
// 8. Let length be null.
182+
let length = null;
183+
184+
// Let type be null.
185+
let type = null;
186+
187+
// Switch on object:
188+
if (typeof object == "string") {
189+
// scalar value string:
190+
// Set source to the UTF-8 encoding of object.
191+
// Set type to `text/plain;charset=UTF-8`.
192+
source = object;
193+
type = "text/plain;charset=UTF-8";
194+
} else {
195+
console.error("TODO: these doesn't yet supported");
196+
// Blob
197+
// Set source to object.
198+
// Set length to object’s size.
199+
// If object’s type attribute is not the empty byte sequence, set type to its value.
200+
201+
// byte sequence:
202+
// Set source to object.
203+
204+
// BufferSource:
205+
// Set source to a copy of the bytes held by object.
206+
207+
// FormData:
208+
// Set action to this step: run the multipart/form-data encoding algorithm, with object’s entry list and UTF-8.
209+
210+
// Set source to object.
211+
212+
// Set length to unclear, see html/6424 for improving this.
213+
214+
// Set type to `multipart/form-data; boundary=`, followed by the multipart/form-data boundary string generated by the multipart/form-data encoding algorithm.
215+
216+
// URLSearchParams:
217+
// Set source to the result of running the application/x-www-form-urlencoded serializer with object’s list.
218+
219+
// Set type to `application/x-www-form-urlencoded;charset=UTF-8`.
220+
221+
// ReadableStream:
222+
// If keepalive is true, then throw a TypeError.
223+
// If object is disturbed or locked, then throw a TypeError.
224+
}
225+
226+
// 11. If source is a byte sequence, then set action to a step that returns source and length to source’s length.
227+
228+
// 12. If action is non-null, then run these steps in parallel:
229+
// 1. Run action.
230+
// Whenever one or more bytes are available and stream is not errored, enqueue the result of creating a Uint8Array from the available bytes into stream.
231+
// When running action is done, close stream.
232+
233+
// 13. Let body be a body whose stream is stream, source is source, and length is length.
234+
const body = { stream, source, length };
235+
236+
// 14. Return (body, type).
237+
return [body, type];
238+
}
239+
240+
// Check whether |statusText| is a ByteString and
241+
// matches the Reason-Phrase token production.
242+
// RFC 2616: https://tools.ietf.org/html/rfc2616
243+
// RFC 7230: https://tools.ietf.org/html/rfc7230
244+
// "reason-phrase = *( HTAB / SP / VCHAR / obs-text )"
245+
// https://github.com/chromium/chromium/blob/94.0.4604.1/third_party/blink/renderer/core/fetch/response.cc#L116
246+
function isValidReasonPhrase(statusText: string) {
247+
for (let i = 0; i < statusText.length; ++i) {
248+
const c = statusText.charCodeAt(i);
249+
if (
250+
!(
251+
c === 0x09 || // HTAB
252+
(c >= 0x20 && c <= 0x7e) || // SP / VCHAR
253+
(c >= 0x80 && c <= 0xff) // obs-text
254+
)
255+
) {
256+
return false;
257+
}
258+
}
259+
return true;
260+
}
261+
262+
/**
263+
* A null body status is a status that is 101, 103, 204, 205, or 304.
264+
* @see https://fetch.spec.whatwg.org/#null-body-status
265+
*/
266+
function nullBodyStatus(status: number): boolean {
267+
return status === 101 || status === 103 || status === 204 || status === 205 ||
268+
status === 304;
269+
}

runtime/src/recommended.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use crate::{
1212
HeadersExt,
1313
ProcessExt,
1414
RequestExt,
15+
ResponseExt,
1516
RuntimeMacroTask,
1617
TimeExt,
1718
URLExt,
@@ -29,6 +30,7 @@ pub fn recommended_extensions() -> Vec<Extension> {
2930
HeadersExt::new_extension(),
3031
// BroadcastChannelExt::new_extension(),
3132
RequestExt::new_extension(),
33+
ResponseExt::new_extension(),
3234
#[cfg(feature = "canvas")]
3335
crate::CanvasExt::new_extension(),
3436
#[cfg(feature = "crypto")]

0 commit comments

Comments
 (0)