diff --git a/packages/compiler/src/backend/emission/emit-exprs.ts b/packages/compiler/src/backend/emission/emit-exprs.ts index aeb7e57cd..527615862 100644 --- a/packages/compiler/src/backend/emission/emit-exprs.ts +++ b/packages/compiler/src/backend/emission/emit-exprs.ts @@ -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(); + 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); diff --git a/packages/compiler/src/backend/emission/emitter.ts b/packages/compiler/src/backend/emission/emitter.ts index 076f20dba..e11c4b495 100644 --- a/packages/compiler/src/backend/emission/emitter.ts +++ b/packages/compiler/src/backend/emission/emitter.ts @@ -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(); readonly dynPromiseAdapters = new Map(); /** The checked-dynamic function boundary's per-signature helpers (see * emit-walkers.ts): call thunks (sc_dfk_*), box builders (sc_dfb_*), diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index d3b44b28b..6d2fce615 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -935,6 +935,8 @@ class LlEmitter { string, LlStreamTypedRefAdapter >(); + /** Runtime-arm dispatchers for live union values. */ + private readonly liveDynUnionRefAdapters = new Map(); private readonly dynPromiseAdapters = new Map(); readonly unionsById = new Map(); readonly recordsById = new Map(); @@ -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(); + 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"; } diff --git a/packages/compiler/src/frontend/lowering/lower-island.ts b/packages/compiler/src/frontend/lowering/lower-island.ts index 6caea6230..3dda9f359 100644 --- a/packages/compiler/src/frontend/lowering/lower-island.ts +++ b/packages/compiler/src/frontend/lowering/lower-island.ts @@ -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), diff --git a/packages/compiler/src/ir/nodes.ts b/packages/compiler/src/ir/nodes.ts index 38da35a13..87d746058 100644 --- a/packages/compiler/src/ir/nodes.ts +++ b/packages/compiler/src/ir/nodes.ts @@ -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 diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index 7580a35d8..afc87ac80 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -1628,6 +1628,24 @@ function validateFunction( // the body of the optChain whose id it names. const activeChains = new Map(); + const liveDynRefEligible = ( + type: IrType, + seen = new Set(), + ): 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; diff --git a/packages/runtime/src/scr_fetch.c b/packages/runtime/src/scr_fetch.c index 2b163f80a..386714702 100644 --- a/packages/runtime/src/scr_fetch.c +++ b/packages/runtime/src/scr_fetch.c @@ -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"); diff --git a/tests/fixtures/fetch/static-coercion/main.js b/tests/fixtures/fetch/static-coercion/main.js index 51220e075..9cb1f318f 100644 --- a/tests/fixtures/fetch/static-coercion/main.js +++ b/tests/fixtures/fetch/static-coercion/main.js @@ -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 {}; diff --git a/tests/fixtures/fetch/static-stream/main.mts b/tests/fixtures/fetch/static-stream/main.mts index 371975f00..15b016be4 100644 --- a/tests/fixtures/fetch/static-stream/main.mts +++ b/tests/fixtures/fetch/static-stream/main.mts @@ -37,6 +37,49 @@ console.log( enqueuedIdentityBox.value, ); +type LiveUnionBox = { value: number }; +type LiveUnionValue = LiveUnionBox | string; + +async function checkEnqueuedUnionIdentity( + value: LiveUnionValue, + original: LiveUnionBox, +): Promise { + const stream = new ReadableStream({ + 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({ start(controller) {