mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-28 06:53:42 +08:00
Store fetch Headers, Request, and Response handles in class fields
A class field typed `Headers`, `Request`, or `Response` (or an array or nullable form of them) maps to checked-dynamic storage, and the class-field rule refused it (SC1090) although `unknown`, Map values, and closures already store the same native handles. Such fields now take the checked-dynamic slot, like the worker handles; reads keep the handle's identity and the static fetch member lowering. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
6c1af69d03
commit
30e23af63f
@@ -2680,7 +2680,7 @@ export function collectClassShapeInner(
|
||||
type.kind === "void" ||
|
||||
(type.kind === "dyn" &&
|
||||
!isJsSourceFile(member.getSourceFile()) &&
|
||||
!lowerer.classImplementedProtocol(inferred))
|
||||
!lowerer.classFieldStoresDyn(inferred))
|
||||
)
|
||||
lowerer.badType(member, inferred);
|
||||
if (isJsSourceFile(member.getSourceFile())) {
|
||||
|
||||
@@ -235,6 +235,7 @@ import {
|
||||
isOsConstantsSymbol,
|
||||
isUnitOnlyTsType,
|
||||
isWorkerHandleType,
|
||||
isFetchHandleType,
|
||||
jsOpenObjectType,
|
||||
mapType,
|
||||
ShapeRegistry,
|
||||
@@ -3004,12 +3005,14 @@ export class Lowerer {
|
||||
/** Declared class fields may store a DYN value only when its static type
|
||||
* is `unknown` or `object` (also with `null` or `undefined`, or an array
|
||||
* of those), names checked protocol identity, or is an owned runtime
|
||||
* handle (a Worker or MessagePort); `any` fields stay refused. */
|
||||
* handle (a Worker or MessagePort, or a fetch Headers/Request/Response);
|
||||
* `any` fields stay refused. */
|
||||
classFieldStoresDyn(type: ts.Type): boolean {
|
||||
return (
|
||||
this.opaqueValueType(type) ||
|
||||
this.classImplementedProtocol(type) ||
|
||||
isWorkerHandleType(type, this.typeCtx)
|
||||
isWorkerHandleType(type, this.typeCtx) ||
|
||||
isFetchHandleType(type, this.typeCtx)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1313,6 +1313,40 @@ export function isWorkerHandleType(
|
||||
return handle;
|
||||
}
|
||||
|
||||
/** The native fetch handles (Headers, Request, Response), optionally in
|
||||
* arrays or with null/undefined: checked-dynamic handles wherever they are
|
||||
* stored, like the worker handles above. */
|
||||
export function isFetchHandleType(
|
||||
type: ts.Type,
|
||||
ctx: Pick<TypeMapperCtx, "checker" | "isStdlibFile">,
|
||||
): boolean {
|
||||
const members = type.isUnionType() ? ts.constituentTypes(type) : [type];
|
||||
let handle = false;
|
||||
for (const member of members) {
|
||||
if (member.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined)) continue;
|
||||
if (ctx.checker.isArrayType(member)) {
|
||||
const element = ctx.checker.getTypeArguments(member as ts.TypeReference)[0];
|
||||
if (element === undefined || !isFetchHandleType(element, ctx)) return false;
|
||||
} else {
|
||||
const symbol = member.getSymbol();
|
||||
if (
|
||||
!symbol ||
|
||||
(symbol.name !== "Headers" && symbol.name !== "Request" && symbol.name !== "Response") ||
|
||||
!ctx.checker
|
||||
.declarationsOf(symbol)
|
||||
.some(
|
||||
(d) =>
|
||||
(ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) &&
|
||||
ctx.isStdlibFile(d.getSourceFile()),
|
||||
)
|
||||
)
|
||||
return false;
|
||||
}
|
||||
handle = true;
|
||||
}
|
||||
return handle;
|
||||
}
|
||||
|
||||
export function mapType(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
if (mapTypeDepth >= MAP_TYPE_MAX_DEPTH) return null;
|
||||
// These intrinsic domains cannot depend on generic bindings, declaration
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
// Class fields typed as native fetch handles (Headers, Request, Response),
|
||||
// directly, optional, nullable, in arrays, and inside record fields, hold
|
||||
// the live handle: mutation through one reference is seen through another.
|
||||
class Holder {
|
||||
headers: Headers;
|
||||
list: Headers[] = [];
|
||||
maybe?: Response;
|
||||
req: Request | null = null;
|
||||
bag: { headers: Headers; status: number };
|
||||
constructor(headers: Headers) {
|
||||
this.headers = headers;
|
||||
this.bag = { headers, status: 1 };
|
||||
}
|
||||
}
|
||||
const holder = new Holder(new Headers({ a: "1" }));
|
||||
console.log(holder.headers.get("a"), holder.list.length, holder.maybe === undefined, holder.req);
|
||||
holder.list.push(new Headers({ b: "2" }));
|
||||
holder.maybe = new Response("x", { status: 201 });
|
||||
holder.req = new Request("http://localhost/p", { method: "POST" });
|
||||
console.log(holder.list[0]!.get("b"), holder.maybe.status, holder.req.method, holder.req.url);
|
||||
holder.headers.set("c", "3");
|
||||
console.log(holder.bag.headers.get("c"), holder.bag.headers === holder.headers);
|
||||
|
||||
// Symbol-keyed fields hold the handles the same way.
|
||||
const NATIVE = Symbol("native");
|
||||
class Wrapper {
|
||||
[NATIVE]: Response;
|
||||
constructor(body: string) {
|
||||
this[NATIVE] = new Response(body, { status: 202 });
|
||||
}
|
||||
get status(): number {
|
||||
return this[NATIVE].status;
|
||||
}
|
||||
}
|
||||
const wrapped = new Wrapper("ok");
|
||||
console.log(wrapped.status, wrapped[NATIVE].headers.get("content-type"));
|
||||
Reference in New Issue
Block a user