Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 111 additions & 0 deletions crates/bindings-typescript/src/lib/algebraic_type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import BinaryWriter from './binary_writer';
import { Identity } from './identity';
import * as AlgebraicTypeVariants from './algebraic_type_variants';
import { hasOwn } from './util';
import { canCompileFunctions } from './runtime_codegen';

type TypespaceType = {
types: AlgebraicTypeType[];
Expand Down Expand Up @@ -448,6 +449,10 @@ export const ProductType = {
let serializer = SERIALIZERS.get(ty);
if (serializer != null) return serializer;

if (!canCompileFunctions()) {
return makeProductSerializerWithClosures(ty, typespace);
}

if (isFixedSizeProduct(ty)) {
const size = productSize(ty);
const body = `\
Expand Down Expand Up @@ -527,6 +532,10 @@ writer.offset += ${primitiveSizes[tag]};`
let deserializer = DESERIALIZERS.get(ty);
if (deserializer != null) return deserializer;

if (!canCompileFunctions()) {
return makeProductDeserializerWithClosures(ty, typespace);
}

if (isFixedSizeProduct(ty)) {
const body = `\
"use strict";
Expand Down Expand Up @@ -678,6 +687,10 @@ export const SumType = {
let serializer = SERIALIZERS.get(ty);
if (serializer != null) return serializer;

if (!canCompileFunctions()) {
return makeSumSerializerWithClosures(ty, typespace);
}

const serializers: Record<string, Serializer<any>> = {};

const body = `\
Expand Down Expand Up @@ -782,6 +795,11 @@ ${ty.variants
} else {
let deserializer = DESERIALIZERS.get(ty);
if (deserializer != null) return deserializer;

if (!canCompileFunctions()) {
return makeSumDeserializerWithClosures(ty, typespace);
}

const deserializers: Record<string, Deserializer<any>> = {};
deserializer = Function(
'reader',
Expand Down Expand Up @@ -816,4 +834,97 @@ ${ty.variants
},
};

// Closure-based (de)serializers, used when `Function` is unavailable
// (see `canCompileFunctions`). They must produce the same bytes and values as
// the `Function`-based ones in `ProductType` and `SumType`.
//
// Like those, each one is cached *before* the (de)serializers of its fields or
// variants are built, so that a recursive type has an exit condition.

function makeProductSerializerWithClosures(
ty: ProductTypeType,
typespace?: TypespaceType
): Serializer<any> {
const names = ty.elements.map(({ name }) => name!);
const serializers: Serializer<any>[] = [];
const serializer: Serializer<any> = (writer, value) => {
for (let i = 0; i < names.length; i++) {
serializers[i](writer, value[names[i]]);
}
};
SERIALIZERS.set(ty, serializer);
for (const { algebraicType } of ty.elements) {
serializers.push(AlgebraicType.makeSerializer(algebraicType, typespace));
}
return serializer;
}

function makeProductDeserializerWithClosures(
ty: ProductTypeType,
typespace?: TypespaceType
): Deserializer<any> {
const names = ty.elements.map(({ name }) => name!);
const deserializers: Deserializer<any>[] = [];
const deserializer: Deserializer<any> = reader => {
// Fields are always added in the same order, so V8 gives every result the
// same hidden class, as it does for the object literal in the fast path.
const result: Record<string, any> = {};
for (let i = 0; i < names.length; i++) {
result[names[i]] = deserializers[i](reader);
}
return result;
};
DESERIALIZERS.set(ty, deserializer);
for (const { algebraicType } of ty.elements) {
deserializers.push(
AlgebraicType.makeDeserializer(algebraicType, typespace)
);
}
return deserializer;
}

function makeSumSerializerWithClosures(
ty: SumTypeType,
typespace?: TypespaceType
): Serializer<any> {
const tags = new Map(ty.variants.map(({ name }, i) => [name!, i]));
const serializers: Serializer<any>[] = [];
const serializer: Serializer<any> = (writer, value) => {
const tag = tags.get(value.tag);
if (tag === undefined) {
throw new TypeError(
`Could not serialize sum type; unknown tag ${value.tag}`
);
}
writer.writeByte(tag);
serializers[tag](writer, value.value);
};
SERIALIZERS.set(ty, serializer);
for (const { algebraicType } of ty.variants) {
serializers.push(AlgebraicType.makeSerializer(algebraicType, typespace));
}
return serializer;
}

function makeSumDeserializerWithClosures(
ty: SumTypeType,
typespace?: TypespaceType
): Deserializer<any> {
const names = ty.variants.map(({ name }) => name!);
const deserializers: Deserializer<any>[] = [];
const deserializer: Deserializer<any> = reader => {
const tag = reader.readU8();
// An unknown tag yields `undefined`, like the `switch` in the fast path.
if (tag >= names.length) return undefined;
return { tag: names[tag], value: deserializers[tag](reader) };
};
DESERIALIZERS.set(ty, deserializer);
for (const { algebraicType } of ty.variants) {
deserializers.push(
AlgebraicType.makeDeserializer(algebraicType, typespace)
);
}
return deserializer;
}

export type ComparablePrimitive = number | string | boolean | bigint;
33 changes: 33 additions & 0 deletions crates/bindings-typescript/src/lib/runtime_codegen.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Whether this environment lets us compile code from strings with `Function`.
//
// Generating (de)serializers with `Function` is faster, but some environments
// forbid it: browsers with a Content-Security-Policy that lacks
// `'unsafe-eval'`, Cloudflare Workers, and Node with
// `--disallow-code-generation-from-strings`. In those we fall back to closures.
//
// We probe once and cache the answer, because under a CSP every failed
// attempt also fires a `securitypolicyviolation` report.
let allowed: boolean | undefined;

export function canCompileFunctions(): boolean {
if (allowed === undefined) {
try {
Function('');
allowed = true;
} catch {
allowed = false;
}
}
return allowed;
}

/**
* Overrides the result of {@link canCompileFunctions}. Pass `undefined` to
* probe the environment again. For tests only.
*
* (De)serializers are cached per type, so this only affects types whose
* (de)serializers have not been built yet.
*/
export function setCanCompileFunctionsForTesting(value: boolean | undefined) {
allowed = value;
}
11 changes: 6 additions & 5 deletions crates/bindings-typescript/src/sdk/ws.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@ async function resolveWS(): Promise<typeof WebSocket> {
}

// Node without a global WebSocket: lazily load undici's polyfill.
// Use an unstatable dynamic import so bundlers don't prebundle it.
const dynamicImport = new Function('m', 'return import(m)') as (
m: string
) => Promise<any>;
// Import from a variable, with the bundlers' ignore comments, so bundlers
// don't prebundle it. Not `new Function`, which a strict CSP forbids.
const undici = 'undici';

try {
const { WebSocket: UndiciWS } = await dynamicImport('undici');
const { WebSocket: UndiciWS } = await import(
/* webpackIgnore: true */ /* @vite-ignore */ undici
);
return UndiciWS as unknown as typeof WebSocket;
} catch (err) {
stdbLogger(
Expand Down
Loading
Loading