Skip to content

Commit

Permalink
Merge branch 'main' into lduarte/fix-workflow-step-types
Browse files Browse the repository at this point in the history
  • Loading branch information
LuisDuarte1 committed Oct 14, 2024
2 parents 5be53bf + 5b740e6 commit b46edf2
Show file tree
Hide file tree
Showing 3 changed files with 141 additions and 21 deletions.
5 changes: 1 addition & 4 deletions src/workerd/io/worker.c++
Original file line number Diff line number Diff line change
Expand Up @@ -1655,7 +1655,6 @@ Worker::Worker(kj::Own<const Script> scriptParam,

auto& api = script->isolate->getApi();
auto handlers = api.unwrapExports(lock, ns);
auto features = api.getFeatureFlags();
auto entrypointClasses = api.getEntrypointClasses(lock);

for (auto& handler: handlers.fields) {
Expand All @@ -1682,9 +1681,7 @@ Worker::Worker(kj::Own<const Script> scriptParam,
impl->statelessClasses.insert(kj::mv(handler.name), kj::mv(cls));
return;
} else if (handle == entrypointClasses.workflowEntrypoint) {
if (features.getWorkerdExperimental()) {
impl->statelessClasses.insert(kj::mv(handler.name), kj::mv(cls));
}
impl->statelessClasses.insert(kj::mv(handler.name), kj::mv(cls));
return;
}

Expand Down
29 changes: 19 additions & 10 deletions types/defines/rpc.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ declare namespace Rpc {
export type Stubable = RpcTargetBranded | ((...args: any[]) => any);

// Types that can be passed over RPC
type Serializable =
// The reason for using a generic type here is to build a serializable subset of structured
// cloneable composite types. This allows types defined with the "interface" keyword to pass the
// serializable check as well. Otherwise, only types defined with the "type" keyword would pass.
type Serializable<T> =
// Structured cloneables
| void
| undefined
Expand All @@ -48,10 +51,15 @@ declare namespace Rpc {
| Error
| RegExp
// Structured cloneable composites
| Map<Serializable, Serializable>
| Set<Serializable>
| ReadonlyArray<Serializable>
| { [key: string | number]: Serializable }
| Map<
T extends Map<infer U, unknown> ? Serializable<U> : never,
T extends Map<unknown, infer U> ? Serializable<U> : never
>
| Set<T extends Set<infer U> ? Serializable<U> : never>
| ReadonlyArray<T extends ReadonlyArray<infer U> ? Serializable<U> : never>
| {
[K in keyof T]: K extends number | string ? Serializable<T[K]> : never;
}
// Special types
| ReadableStream<Uint8Array>
| WritableStream<Uint8Array>
Expand All @@ -78,7 +86,8 @@ declare namespace Rpc {
: T extends Set<infer V> ? Set<Stubify<V>>
: T extends Array<infer V> ? Array<Stubify<V>>
: T extends ReadonlyArray<infer V> ? ReadonlyArray<Stubify<V>>
: T extends { [key: string | number]: unknown } ? { [K in keyof T]: Stubify<T[K]> }
// When using "unknown" instead of "any", interfaces are not stubified.
: T extends { [key: string | number]: any } ? { [K in keyof T]: Stubify<T[K]> }
: T;

// Recursively rewrite all `Stub<T>`s with the corresponding `T`s.
Expand Down Expand Up @@ -110,7 +119,7 @@ declare namespace Rpc {
// prettier-ignore
type Result<R> =
R extends Stubable ? Promise<Stub<R>> & Provider<R>
: R extends Serializable ? Promise<Stubify<R> & MaybeDisposable<R>> & MaybeProvider<R>
: R extends Serializable<R> ? Promise<Stubify<R> & MaybeDisposable<R>> & MaybeProvider<R>
: never;

// Type for method or property on an RPC interface.
Expand Down Expand Up @@ -225,15 +234,15 @@ declare module "cloudflare:workers" {
};

export abstract class WorkflowStep {
do<T extends Rpc.Serializable>(name: string, callback: () => Promise<T>): Promise<T>;
do<T extends Rpc.Serializable>(name: string, config: WorkflowStepConfig, callback: () => Promise<T>): Promise<T>;
do<T extends Rpc.Serializable<T>>(name: string, callback: () => Promise<T>): Promise<T>;
do<T extends Rpc.Serializable<T>>(name: string, config: WorkflowStepConfig, callback: () => Promise<T>): Promise<T>;
sleep: (name: string, duration: WorkflowSleepDuration) => Promise<void>;
sleepUntil: (name: string, timestamp: Date | number) => Promise<void>;
}

export abstract class WorkflowEntrypoint<
Env = unknown,
T extends Rpc.Serializable | unknown = unknown,
T extends Rpc.Serializable<T> | unknown = unknown,
> implements Rpc.WorkflowEntrypointBranded
{
[Rpc.__WORKFLOW_ENTRYPOINT_BRAND]: never;
Expand Down
128 changes: 121 additions & 7 deletions types/test/types/rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,40 @@ import {
} from "cloudflare:workers";
import { expectTypeOf } from "expect-type";

class BoringClass {}
type TestType = {
fieldString: string;
fieldCallback: (p: string) => number;
fieldBasicMap: Map<string, number>;
fieldComplexMap: Map<string, {
fieldString: string;
fieldCallback: (p: string) => number;
}>;
fieldSet: Set<string>;
fieldSubLevel: {
fieldString: string;
fieldCallback: (p: string) => number;
};
};

interface ABasicInterface {
fieldString: string;
fieldCallback: (p: string) => number;
};

interface TestInterface extends ABasicInterface {
fieldBasicMap: Map<string, number>;
fieldComplexMap: Map<string, ABasicInterface>;
fieldSet: Set<string>;
fieldSubLevelInline: {
fieldString: string;
fieldCallback: (p: string) => number;
};
fieldSubLevelInterface: ABasicInterface;
};

interface NonSerializableInterface {
field: ReadableStream<string>;
};

class TestCounter extends RpcTarget {
constructor(private val = 0) {
Expand Down Expand Up @@ -181,15 +214,53 @@ class TestEntrypoint extends WorkerEntrypoint<Env> {
Object: { a: { b: new TestCounter() } },
};
}
get nonSerializable1() {
return new BoringClass();

methodReturnsTypeObject(): TestType {
return {
fieldString: "a",
fieldCallback: (p: string) => 1,
fieldBasicMap: new Map([["b", 2]]),
fieldComplexMap: new Map([["c", {
fieldString: "d",
fieldCallback: (p: string) => 3,
}]]),
fieldSet: new Set(["e"]),
fieldSubLevel: {
fieldString: "f",
fieldCallback: (p: string) => 4,
},
};
}
nonSerializable2() {
return { a: new BoringClass() };
methodReturnsInterfaceObject(): TestInterface {
return {
fieldString: "a",
fieldCallback: (p: string) => 1,
fieldBasicMap: new Map([["b", 2]]),
fieldComplexMap: new Map([["c", {
fieldString: "d",
fieldCallback: (p: string) => 3,
}]]),
fieldSet: new Set(["e"]),
fieldSubLevelInline: {
fieldString: "f",
fieldCallback: (p: string) => 4,
},
fieldSubLevelInterface: {
fieldString: "e",
fieldCallback: (p: string) => 5,
},
};
}
async nonSerializable3() {

nonSerializable1() {
return new ReadableStream<string>();
}
nonSerializable2() {
return { field: new ReadableStream<string>() };
}
nonSerializable3(): NonSerializableInterface {
return { field: new ReadableStream<string>() };
}

[Symbol.dispose]() {
console.log("Disposing");
Expand Down Expand Up @@ -440,7 +511,50 @@ export default <ExportedHandler<Env>>{
expectTypeOf(s.objectProperty.z(false)).toEqualTypeOf<Promise<number>>(); // (pipelining)

expectTypeOf(s.everySerializable).not.toBeNever();
expectTypeOf(s.nonSerializable1).toBeNever();

// Verify serializable composite objects defined with "type" keyword
const oType = await s.methodReturnsTypeObject();
expectTypeOf(oType).not.toBeNever();
expectTypeOf(oType.fieldString).toEqualTypeOf<string>();
expectTypeOf(oType.fieldCallback).toEqualTypeOf<
RpcStub<(p: string) => number>
>(); // stubified
expectTypeOf(oType.fieldBasicMap).toEqualTypeOf<Map<string, number>>();
expectTypeOf(oType.fieldComplexMap).toEqualTypeOf<Map<string, {
fieldString: string;
fieldCallback: RpcStub<(p: string) => number>; // stubified
}>>();
expectTypeOf(oType.fieldSet).toEqualTypeOf<Set<string>>();
expectTypeOf(oType.fieldSubLevel.fieldString).toEqualTypeOf<string>();
expectTypeOf(oType.fieldSubLevel.fieldCallback).toEqualTypeOf<
RpcStub<(p: string) => number>
>(); // stubified

// Verify serializable composite objects defined with "inteface" keyword
const oInterface = await s.methodReturnsInterfaceObject();
expectTypeOf(oInterface).not.toBeNever();
expectTypeOf(oInterface.fieldString).toEqualTypeOf<string>();
expectTypeOf(oInterface.fieldCallback).toEqualTypeOf<
RpcStub<(p: string) => number>
>(); // stubified
expectTypeOf(oInterface.fieldBasicMap).toEqualTypeOf<Map<string, number>>();
expectTypeOf(oInterface.fieldComplexMap).toEqualTypeOf<Map<string, {
fieldString: string;
fieldCallback: RpcStub<(p: string) => number>; // stubified
}>>();
expectTypeOf(oInterface.fieldSet).toEqualTypeOf<Set<string>>();
expectTypeOf(oInterface.fieldSubLevelInline.fieldString).toEqualTypeOf<string>();
expectTypeOf(oInterface.fieldSubLevelInline.fieldCallback).toEqualTypeOf<
RpcStub<(p: string) => number>
>(); // stubified
expectTypeOf(oInterface.fieldSubLevelInterface.fieldString).toEqualTypeOf<string>();
expectTypeOf(oInterface.fieldSubLevelInterface.fieldCallback).toEqualTypeOf<
RpcStub<(p: string) => number>
>(); // stubified

expectTypeOf(s.nonSerializable1).returns.toBeNever();
// Note: Since one of the object's members is non-serializable,
// the entire object is resolved as 'never'.
expectTypeOf(s.nonSerializable2).returns.toBeNever();
expectTypeOf(s.nonSerializable3).returns.toBeNever();

Expand Down

0 comments on commit b46edf2

Please sign in to comment.