mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-31 07:23:06 +08:00
fix(fetch): preserve WebIDL conversion semantics
This commit is contained in:
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user