fix(fetch): preserve WebIDL conversion semantics

This commit is contained in:
Chris Tate
2026-08-01 12:31:13 -05:00
parent 1544ccda88
commit bc00d3d65f
9 changed files with 433 additions and 124 deletions
@@ -250,6 +250,69 @@ function liveDynRefAdapter(
return adapter;
}
/** A union itself is only the tagged box; the identity Web APIs expose is
* the selected mutable arm. Dispatch on the tag and point the typed capsule
* directly at that arm's payload. Non-mutable arms keep ordinary dynFrom
* semantics. */
function liveDynUnionRefAdapter(
E: CEmitter,
t: IrType & { kind: "union" },
): string {
const key = typeKey(t);
const existing = E.liveDynUnionRefAdapters.get(key);
if (existing) return existing;
const union = E.unionsById.get(t.unionId);
if (!union) {
throw new Error(`emitter bug: live dyn ref of unknown union ${t.unionId}`);
}
const mutableArms = union.arms
.map((arm, tag) => ({ arm, tag }))
.filter(({ arm }) => streamTypedRefEligible(arm));
if (mutableArms.length === 0) {
throw new Error(`emitter bug: live dyn ref of immutable union ${key}`);
}
const sym = `sc_ldu_${E.liveDynUnionRefAdapters.size}`;
E.liveDynUnionRefAdapters.set(key, sym);
const sig = `static ScrDyn *${sym}(ScrUnion *sc_u)`;
E.walkerProtos.push(`${sig}; /* materialize live union value ${key} */`);
const defs: string[] = [];
const adapters = new Map<number, StreamTypedRefAdapter>();
for (const { arm, tag } of mutableArms) {
const prefix = `${sym}_${tag}`;
adapters.set(
tag,
streamTypedRefAdapter(
E,
arm,
{ prefix, defs, adapters: new Map() },
`${prefix}_materialize`,
),
);
}
defs.push(`${sig} {`, ` switch (sc_u->tag) {`);
for (const { arm, tag } of mutableArms) {
const adapter = adapters.get(tag)!;
const rc = vAdapters(arm);
const armKey = typeKey(arm);
const keyLit = cStringLiteral(Buffer.from(armKey, "utf8"));
defs.push(
` case ${tag}:`,
` return scr_dyn_new_typed_ref(scr_union_peek(sc_u), &${rc.retain}, &${rc.release}, ${keyLit}, ${Buffer.byteLength(armKey, "utf8")}, &${adapter.snapshot}, ${adapter.commit});`,
);
}
defs.push(
` default:`,
` return ${E.toDynHelper(t)}(sc_u);`,
` }`,
`}`,
``,
);
E.walkerDefs.push(...defs);
return sym;
}
function streamFromArrayAdapter(
E: CEmitter,
t: IrType & { kind: "array" },
@@ -2145,6 +2208,10 @@ export function emitExpr(E: CEmitter, e: IrExpr): Temp {
}
const v = E.emitExpr(e.value);
if (e.liveRef) {
if (v.type.kind === "union") {
const adapter = liveDynUnionRefAdapter(E, v.type);
return E.newTemp(e.type, `${adapter}(${v.name})`);
}
const adapter = liveDynRefAdapter(E, v.type);
const rc = vAdapters(v.type);
const key = typeKey(v.type);
@@ -213,6 +213,10 @@ export class CEmitter {
string,
{ snapshot: string; commit: string }
>();
/** Runtime-arm dispatchers for live references whose static type is a
* union. Mutable arms become typed capsules; scalar/unit arms retain the
* ordinary static-to-dyn conversion. */
readonly liveDynUnionRefAdapters = new Map<string, string>();
readonly dynPromiseAdapters = new Map<string, string>();
/** The checked-dynamic function boundary's per-signature helpers (see
* emit-walkers.ts): call thunks (sc_dfk_*), box builders (sc_dfb_*),
@@ -935,6 +935,8 @@ class LlEmitter {
string,
LlStreamTypedRefAdapter
>();
/** Runtime-arm dispatchers for live union values. */
private readonly liveDynUnionRefAdapters = new Map<string, string>();
private readonly dynPromiseAdapters = new Map<string, string>();
readonly unionsById = new Map<string, IrUnionDef>();
readonly recordsById = new Map<string, IrRecordShape>();
@@ -6113,6 +6115,12 @@ class LlEmitter {
}
const v = this.emitExpr(e.value);
if (e.liveRef) {
if (v.type.kind === "union") {
const adapter = this.liveDynUnionRefAdapter(v.type);
const boxed = B.tmp();
B.line(`${boxed} = call ptr @${adapter}(ptr ${v.name})`);
return this.own({ name: boxed, type: e.type });
}
if (!this.streamTypedRefEligible(v.type)) {
throw new Error(`llvm emitter bug: live dyn ref of ${typeKey(v.type)}`);
}
@@ -10134,6 +10142,88 @@ class LlEmitter {
return `@${commit}`;
}
/** Box the mutable payload of a union directly, preserving the identity
* that AbortSignal reasons and stream chunks expose. Scalar/unit arms use
* the ordinary union-to-dyn converter. */
private liveDynUnionRefAdapter(
t: IrType & { kind: "union" },
): string {
const key = typeKey(t);
const existing = this.liveDynUnionRefAdapters.get(key);
if (existing) return existing;
const union = this.unionsById.get(t.unionId);
if (!union) {
throw new Error(
`llvm emitter bug: live dyn ref of unknown union ${t.unionId}`,
);
}
const mutableArms = union.arms
.map((arm, tag) => ({ arm, tag }))
.filter(({ arm }) => this.streamTypedRefEligible(arm));
if (mutableArms.length === 0) {
throw new Error(`llvm emitter bug: live dyn ref of immutable union ${key}`);
}
const sym = `sc_ldu_${this.liveDynUnionRefAdapters.size}`;
this.liveDynUnionRefAdapters.set(key, sym);
this.declare(
`declare ptr @scr_dyn_new_typed_ref(ptr, ptr, ptr, ptr, i64, ptr, ptr)`,
);
const adapters = new Map<number, LlStreamTypedRefAdapter>();
for (const { arm, tag } of mutableArms) {
const prefix = `${sym}_${tag}`;
adapters.set(
tag,
this.streamTypedRefMaterializeAdapter(
arm,
{ prefix, adapters: new Map() },
`${prefix}_materialize`,
),
);
}
const B = new BlockBuilder();
const tagPtr = B.tmp();
const tagValue = B.tmp();
B.line(
`${tagPtr} = getelementptr inbounds %ScrUnion, ptr %u, i64 0, i32 1`,
);
B.line(`${tagValue} = load i32, ptr ${tagPtr}`);
const fallback = B.newLabel("ldu.dyn");
const armLabels = mutableArms.map(() => B.newLabel("ldu.ref"));
B.terminate(
`switch i32 ${tagValue}, label %${fallback} [ ${mutableArms.map(({ tag }, index) => `i32 ${tag}, label %${armLabels[index]}`).join(" ")} ]`,
);
mutableArms.forEach(({ arm, tag }, index) => {
const adapter = adapters.get(tag)!;
const rc = vAdapters(this, arm);
const armKey = typeKey(arm);
B.startBlock(armLabels[index]!);
const payloadPtr = B.tmp();
const payload = B.tmp();
const boxed = B.tmp();
B.line(
`${payloadPtr} = getelementptr inbounds %ScrUnion, ptr %u, i64 0, i32 5`,
);
B.line(`${payload} = load ptr, ptr ${payloadPtr}`);
B.line(
`${boxed} = call ptr @scr_dyn_new_typed_ref(ptr ${payload}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${this.cstr(armKey)}, i64 ${Buffer.byteLength(armKey, "utf8")}, ptr @${adapter.snapshot}, ptr ${adapter.commit})`,
);
B.terminate(`ret ptr ${boxed}`);
});
B.startBlock(fallback);
const boxed = B.tmp();
B.line(`${boxed} = call ptr @${this.dyn.toDynHelper(t)}(ptr %u)`);
B.terminate(`ret ptr ${boxed}`);
this.resolveThunkDefs.push(
`define internal ptr @${sym}(ptr %u) ${FN_ATTRS} { ; materialize live union value ${key}`,
B.render(),
`}`,
``,
);
return sym;
}
private streamTypedRefEligible(t: IrType): boolean {
return t.kind === "record" || t.kind === "array" || t.kind === "bytes";
}
@@ -317,16 +317,6 @@ function fenceStaticRequestInitLiteral(
if (!L.dynamic) {
const inputNode = call.arguments[0]!;
const input = L.lowerExpr(inputNode);
const url =
input.type.kind === "url"
? {
kind: "libCall" as const,
fn: "url.href" as const,
args: [input],
type: STRING,
loc: locOf(inputNode),
}
: L.coerceInto(inputNode, input, STRING);
const initNode = call.arguments[1];
let init: IrExpr;
if (initNode === undefined) {
@@ -337,25 +327,19 @@ function fenceStaticRequestInitLiteral(
} else {
init = L.lowerExprExpecting(initNode, DYN);
}
const answer: IrExpr = {
kind: "libCall",
fn: "fetch.start",
args: [url, init],
type: { kind: "promise", inner: DYN },
loc,
};
if (call.arguments.length <= 2) return answer;
// JavaScript evaluates surplus call arguments in source order even
// though fetch ignores their values. Preserve the URL/init values
// across those evaluations, then start the request.
const urlLocal = L.declareHiddenLocal("%fetchUrl", url.type);
// Calling a WebIDL operation has two distinct phases: JavaScript
// first evaluates every argument expression, then the callee converts
// those values. In particular, evaluating RequestInit may mutate a
// URL object passed as input; its href must not be snapshotted until
// all argument expressions (including ignored surplus ones) ran.
const inputLocal = L.declareHiddenLocal("%fetchInput", input.type);
const initLocal = L.declareHiddenLocal("%fetchInit", init.type);
const urlRef: IrExpr = {
const inputRef: IrExpr = {
kind: "varRef",
localId: urlLocal.id,
type: url.type,
loc,
localId: inputLocal.id,
type: input.type,
loc: locOf(inputNode),
};
const initRef: IrExpr = {
kind: "varRef",
@@ -364,7 +348,7 @@ function fenceStaticRequestInitLiteral(
loc,
};
const stmts: IrStmt[] = [
{ kind: "varDecl", localId: urlLocal.id, init: url, loc },
{ kind: "varDecl", localId: inputLocal.id, init: input, loc },
{ kind: "varDecl", localId: initLocal.id, init, loc },
];
for (const argument of call.arguments.slice(2)) {
@@ -380,7 +364,23 @@ function fenceStaticRequestInitLiteral(
: L.lowerExpr(argument);
stmts.push({ kind: "exprStmt", expr: discarded, loc: discarded.loc });
}
answer.args = [urlRef, initRef];
const url: IrExpr =
input.type.kind === "url"
? {
kind: "libCall",
fn: "url.href",
args: [inputRef],
type: STRING,
loc: locOf(inputNode),
}
: L.coerceInto(inputNode, inputRef, STRING);
const answer: IrExpr = {
kind: "libCall",
fn: "fetch.start",
args: [url, initRef],
type: { kind: "promise", inner: DYN },
loc,
};
return { kind: "seqExpr", stmts, result: answer, type: answer.type, loc };
}
const fetchFn: IrExpr = { kind: "jsOp", op: "globalGet", name: "fetch", args: [], type: JSVAL, loc };
@@ -483,16 +483,29 @@ function staticResponseMemberName(
return key.isStringLiteralType() ? key.value : null;
}
function hasLiveWebMutableArm(L: Lowerer, type: IrType): boolean {
if (
type.kind === "record" ||
type.kind === "array" ||
type.kind === "bytes"
) {
return true;
}
if (type.kind !== "union") return false;
return L.unions.get(type.unionId)?.arms.some((arm) =>
hasLiveWebMutableArm(L, arm)
) ?? false;
}
/** Box mutable data for Web API slots that expose the exact JavaScript
* value again. Ordinary dynFrom remains the documented copy boundary;
* this capsule is deliberately limited to records/arrays/bytes that the
* live materializer can snapshot and commit. */
* this capsule is deliberately limited to records/arrays/bytes (including
* those selected at runtime from a union) that the live materializer can
* snapshot and commit. */
function lowerLiveWebValue(L: Lowerer, node: ts.Expression): IrExpr {
const value = L.lowerExpr(node);
if (
(value.type.kind === "record" ||
value.type.kind === "array" ||
value.type.kind === "bytes") &&
hasLiveWebMutableArm(L, value.type) &&
canConvertToDyn(
value.type,
(id) => L.shapes.get(id),
+5 -4
View File
@@ -4461,10 +4461,11 @@ export type IrExpr =
* path), and `fnName` — the best-effort static spelling for inspect
* ([Function: name]) and Node-shaped call errors. The operand is
* borrowed; the result is owned (+1). Never throws. `liveRef` is the
* narrow Web-platform exception for record/array values whose API
* contract exposes the same reference again (stream chunks and abort
* reasons): it emits a typed capsule with a live materializer instead
* of the ordinary deep copy. */
* narrow Web-platform exception for record/array/bytes values (including
* mutable arms selected at runtime from a union) whose API contract
* exposes the same reference again (stream chunks and abort reasons): it
* emits a typed capsule with a live materializer instead of the ordinary
* deep copy. */
| { kind: "dynFrom"; value: IrExpr; fnName?: string; liveRef?: true; type: IrType; loc: SrcLoc }
/** Island value → dyn conversion (`type` is always dyn; the operand
* is always jsval): the jsval→dyn crossing — an 'any'-typed engine
+19 -6
View File
@@ -1628,6 +1628,24 @@ function validateFunction(
// the body of the optChain whose id it names.
const activeChains = new Map<string, IrType>();
const liveDynRefEligible = (
type: IrType,
seen = new Set<string>(),
): boolean => {
if (
type.kind === "record" ||
type.kind === "array" ||
type.kind === "bytes"
) {
return true;
}
if (type.kind !== "union" || seen.has(type.unionId)) return false;
seen.add(type.unionId);
return unions.get(type.unionId)?.arms.some((arm) =>
liveDynRefEligible(arm, seen)
) ?? false;
};
function checkExpr(e: IrExpr): void {
switch (e.kind) {
case "numLit":
@@ -3018,12 +3036,7 @@ function validateFunction(
if (!canConvertToDyn(vt, (id) => records.get(id), (id) => unions.get(id))) {
err(`dynFrom of non-dyn-convertible type ${vt.kind}`, e.loc);
}
if (
e.liveRef &&
vt.kind !== "record" &&
vt.kind !== "array" &&
vt.kind !== "bytes"
) {
if (e.liveRef && !liveDynRefEligible(vt)) {
err(`live dynFrom of unsupported type ${vt.kind}`, e.loc);
}
break;
+116 -81
View File
@@ -3808,14 +3808,21 @@ static bool sf_content_length(ScrArr *headers, bool *present_out,
return true;
}
static ScrStr *sf_method(const ScrDyn *init) {
/* RequestInit dictionary conversion coerces every member before Request's
* method validation runs. Keep ToString separate so later members
* (redirect and signal) are still observed when the resulting method is
* syntactically invalid or forbidden. */
static ScrStr *sf_method_value(const ScrDyn *init) {
const ScrDyn *value =
init && init->kind == SCR_DYN_OBJ
? scr_dyn_obj_get(init, "method", 6)
: NULL;
if (!value || value->kind == SCR_DYN_UNDEF) return scr_str_new("GET", 3);
ScrStr *method = scr_dyn_string_coerce_js(value);
if (!method) return NULL;
return scr_dyn_string_coerce_js(value);
}
static bool sf_method_validate(ScrStr **method_out) {
ScrStr *method = *method_out;
static const char *normalized[] = {
"DELETE", "GET", "HEAD", "OPTIONS", "POST", "PUT"};
for (size_t i = 0; i < sizeof normalized / sizeof normalized[0]; i++) {
@@ -3830,7 +3837,8 @@ static ScrStr *sf_method(const ScrDyn *init) {
}
if (same) {
scr_str_release(method);
return scr_str_new(candidate, n);
*method_out = scr_str_new(candidate, n);
return true;
}
}
bool valid = method->len > 0;
@@ -3841,9 +3849,8 @@ static ScrStr *sf_method(const ScrDyn *init) {
strchr("!#$%&'*+-.^_`|~", c) != NULL;
}
if (!valid) {
scr_str_release(method);
sf_type_error("fetch failed");
return NULL;
return false;
}
if (sf_eq_ci(method, "connect") || sf_eq_ci(method, "trace") ||
sf_eq_ci(method, "track")) {
@@ -3852,13 +3859,12 @@ static ScrStr *sf_method(const ScrDyn *init) {
if (!message) sf_oom();
int len = snprintf(message, message_len + 1, "'%.*s' HTTP method is unsupported.",
(int)method->len, method->data);
scr_str_release(method);
if (len < 0 || (size_t)len > message_len) sf_oom();
sf_type_error(message);
free(message);
return NULL;
return false;
}
return method;
return true;
}
static bool sf_redirect_mode(const ScrDyn *init, int *out) {
@@ -4553,10 +4559,86 @@ ScrPromise *scr_fetch_static(ScrStr *url, ScrDyn *init) {
}
}
}
int redirect_mode;
if (!sf_redirect_mode(init, &redirect_mode)) {
/* WebIDL converts dictionary members by name, not object insertion
* order: body, duplex, headers, method, redirect, signal. Every
* observable conversion completes before Request validates the URL,
* method/body combination, or an already-aborted signal. */
const ScrDyn *body =
init && init->kind == SCR_DYN_OBJ
? scr_dyn_obj_get(init, "body", 4)
: NULL;
bool body_present =
body && body->kind != SCR_DYN_UNDEF && body->kind != SCR_DYN_NULL;
bool body_stream =
body_present &&
body->kind == SCR_DYN_HANDLE &&
body->v.handle.tag == SCR_DYNH_WEB_STREAM;
/* BodyInit's USVString fallback applies to every ordinary value that
* is not already bytes or a ReadableStream. This includes scalars,
* arrays, and plain objects (with the ordinary JS object coercion
* protocol), while null and undefined continue to mean no body. */
ScrDyn *coerced_body = NULL;
if (body_present && body->kind != SCR_DYN_STR &&
body->kind != SCR_DYN_BYTES && !body_stream) {
ScrStr *text = scr_dyn_string_coerce_js(body);
if (!text) {
return sf_reject_now(promise, "fetch failed");
}
coerced_body = scr_dyn_new_str(text);
scr_str_release(text);
body = coerced_body;
}
const ScrDyn *duplex =
init && init->kind == SCR_DYN_OBJ
? scr_dyn_obj_get(init, "duplex", 6)
: NULL;
bool duplex_present =
duplex && duplex->kind != SCR_DYN_UNDEF;
bool duplex_half = false;
if (duplex_present) {
ScrStr *duplex_mode = scr_dyn_string_coerce_js(duplex);
if (!duplex_mode) {
scr_dyn_release(coerced_body);
return sf_reject_now(promise, "fetch failed");
}
duplex_half =
duplex_mode->len == 4 &&
memcmp(duplex_mode->data, "half", 4) == 0;
scr_str_release(duplex_mode);
}
if (duplex_present && !duplex_half) {
scr_dyn_release(coerced_body);
return sf_reject_now(
promise, "RequestInit.duplex must be 'half'");
}
ScrArr *headers = scr_arr_new(SCR_ELEM_STR, 16);
const ScrDyn *init_headers =
init && init->kind == SCR_DYN_OBJ
? scr_dyn_obj_get(init, "headers", 7)
: NULL;
if (!sf_add_headers(headers, init_headers)) {
scr_dyn_release(coerced_body);
scr_arr_release(headers);
return sf_reject_now(promise, "fetch failed");
}
ScrStr *method = sf_method_value(init);
if (!method) {
scr_dyn_release(coerced_body);
scr_arr_release(headers);
return sf_reject_now(promise, "fetch failed");
}
int redirect_mode;
if (!sf_redirect_mode(init, &redirect_mode)) {
scr_dyn_release(coerced_body);
scr_arr_release(headers);
scr_str_release(method);
return sf_reject_now(promise, "fetch failed");
}
const ScrDyn *signal_dyn =
init && init->kind == SCR_DYN_OBJ
? scr_dyn_obj_get(init, "signal", 6)
@@ -4566,11 +4648,19 @@ ScrPromise *scr_fetch_static(ScrStr *url, ScrDyn *init) {
signal_dyn->kind != SCR_DYN_NULL) {
signal = sf_signal_of(signal_dyn,
"Request init.signal must be an AbortSignal");
if (!signal) return sf_reject_now(promise, "fetch failed");
if (!signal) {
scr_dyn_release(coerced_body);
scr_arr_release(headers);
scr_str_release(method);
return sf_reject_now(promise, "fetch failed");
}
}
ScrUrl *u = scr_url_new(url);
if (!u) {
scr_dyn_release(coerced_body);
scr_arr_release(headers);
scr_str_release(method);
scr_promise_reject_pending(promise);
return promise;
}
@@ -4585,74 +4675,30 @@ ScrPromise *scr_fetch_static(ScrStr *url, ScrDyn *init) {
message[message_len] = '\0';
sf_type_error(message);
free(message);
scr_dyn_release(coerced_body);
scr_arr_release(headers);
scr_str_release(method);
scr_url_release(u);
return sf_reject_now(promise, "fetch failed");
}
bool https = sf_eq_ci(u->scheme, "https");
if (!https && !sf_eq_ci(u->scheme, "http")) {
scr_url_release(u);
return sf_reject_now(promise, "fetch failed");
}
ScrStr *method = sf_method(init);
if (!method) {
scr_url_release(u);
return sf_reject_now(promise, "fetch failed");
}
const ScrDyn *body =
init && init->kind == SCR_DYN_OBJ
? scr_dyn_obj_get(init, "body", 4)
: NULL;
bool body_present =
body && body->kind != SCR_DYN_UNDEF && body->kind != SCR_DYN_NULL;
bool body_stream =
body_present &&
body->kind == SCR_DYN_HANDLE &&
body->v.handle.tag == SCR_DYNH_WEB_STREAM;
const ScrDyn *duplex =
init && init->kind == SCR_DYN_OBJ
? scr_dyn_obj_get(init, "duplex", 6)
: NULL;
bool duplex_present =
duplex && duplex->kind != SCR_DYN_UNDEF;
bool duplex_half = false;
if (duplex_present) {
ScrStr *duplex_mode = scr_dyn_string_coerce_js(duplex);
if (!duplex_mode) {
scr_str_release(method);
scr_url_release(u);
return sf_reject_now(promise, "fetch failed");
}
duplex_half =
duplex_mode->len == 4 &&
memcmp(duplex_mode->data, "half", 4) == 0;
scr_str_release(duplex_mode);
}
if (duplex_present && !duplex_half) {
scr_dyn_release(coerced_body);
scr_arr_release(headers);
scr_str_release(method);
scr_url_release(u);
return sf_reject_now(
promise, "RequestInit.duplex must be 'half'");
return sf_reject_now(promise, "fetch failed");
}
/* BodyInit's USVString fallback applies to every ordinary value that
* is not already bytes or a ReadableStream. This includes scalars,
* arrays, and plain objects (with the ordinary JS object coercion
* protocol), while null and undefined continue to mean no body. */
ScrDyn *coerced_body = NULL;
if (body_present && body->kind != SCR_DYN_STR &&
body->kind != SCR_DYN_BYTES && !body_stream) {
ScrStr *text = scr_dyn_string_coerce_js(body);
if (!text) {
scr_str_release(method);
scr_url_release(u);
return sf_reject_now(promise, "fetch failed");
}
coerced_body = scr_dyn_new_str(text);
scr_str_release(text);
body = coerced_body;
if (!sf_method_validate(&method)) {
scr_dyn_release(coerced_body);
scr_arr_release(headers);
scr_str_release(method);
scr_url_release(u);
return sf_reject_now(promise, "fetch failed");
}
if (body_stream && !duplex_half) {
scr_dyn_release(coerced_body);
scr_arr_release(headers);
scr_str_release(method);
scr_url_release(u);
return sf_reject_now(
@@ -4663,24 +4709,13 @@ ScrPromise *scr_fetch_static(ScrStr *url, ScrDyn *init) {
((method->len == 3 && memcmp(method->data, "GET", 3) == 0) ||
(method->len == 4 && memcmp(method->data, "HEAD", 4) == 0))) {
scr_dyn_release(coerced_body);
scr_arr_release(headers);
scr_str_release(method);
scr_url_release(u);
return sf_reject_now(
promise, "Request with GET/HEAD method cannot have body.");
}
ScrArr *headers = scr_arr_new(SCR_ELEM_STR, 16);
const ScrDyn *init_headers =
init && init->kind == SCR_DYN_OBJ
? scr_dyn_obj_get(init, "headers", 7)
: NULL;
if (!sf_add_headers(headers, init_headers)) {
scr_dyn_release(coerced_body);
scr_arr_release(headers);
scr_str_release(method);
scr_url_release(u);
return sf_reject_now(promise, "fetch failed");
}
/* Fetch Metadata is controlled by the fetch implementation. Undici
* replaces a caller-provided sec-fetch-mode with "cors". */
sf_strip_header(&headers, "sec-fetch-mode");
+43
View File
@@ -55,4 +55,47 @@ const plainObjectResponse = await fetch(
const plainObjectResult = await plainObjectResponse.json();
console.log("plain object body coercion:", plainObjectResult.body);
const mutableUrl = new URL(`${process.argv[2]}/text?discarded=1`);
const mutableUrlResponse = await fetch(mutableUrl, {
method: (mutableUrl.searchParams.delete("discarded"), "GET"),
});
console.log(
"url conversion after init:",
mutableUrlResponse.status,
await mutableUrlResponse.text(),
);
function orderedBodyToString() {
console.log("request init coercion: body");
return "ordered body";
}
function orderedDuplexToString() {
console.log("request init coercion: duplex");
return "half";
}
function orderedMethodToString() {
console.log("request init coercion: method");
return "POST";
}
function orderedRedirectToString() {
console.log("request init coercion: redirect");
return "follow";
}
const orderedInitResponse = await fetch(`${process.argv[2]}/post-echo`, {
method: { toString: orderedMethodToString },
body: { toString: orderedBodyToString },
duplex: { toString: orderedDuplexToString },
redirect: { toString: orderedRedirectToString },
});
const orderedInitResult = await orderedInitResponse.json();
console.log(
"request init coercion result:",
orderedInitResult.method,
orderedInitResult.body,
);
export {};
+43
View File
@@ -37,6 +37,49 @@ console.log(
enqueuedIdentityBox.value,
);
type LiveUnionBox = { value: number };
type LiveUnionValue = LiveUnionBox | string;
async function checkEnqueuedUnionIdentity(
value: LiveUnionValue,
original: LiveUnionBox,
): Promise<void> {
const stream = new ReadableStream<LiveUnionValue>({
start(controller) {
controller.enqueue(value);
controller.close();
},
});
const part = await stream.getReader().read();
if (!part.done && typeof part.value !== "string") {
part.value.value = 3;
}
console.log(
"controller enqueue union identity:",
part.done ? false : part.value === original,
original.value,
);
}
const enqueuedUnionBox = { value: 1 };
await checkEnqueuedUnionIdentity(enqueuedUnionBox, enqueuedUnionBox);
function checkAbortUnionIdentity(
value: LiveUnionValue,
original: LiveUnionBox,
): void {
const reason = AbortSignal.abort(value).reason as LiveUnionValue;
if (typeof reason !== "string") reason.value = 4;
console.log(
"abort reason union identity:",
reason === original,
original.value,
);
}
const abortUnionBox = { value: 1 };
checkAbortUnionIdentity(abortUnionBox, abortUnionBox);
const enqueuedIdentityBytes = Buffer.from([1]);
const enqueuedBytesStream = new ReadableStream<Uint8Array>({
start(controller) {