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
5 changes: 5 additions & 0 deletions .changeset/serialize-url.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"capnweb": minor
---

Support serializing `URL` objects over RPC.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ The following types can be passed over RPC (in arguments or return values), and
* `Error` and its well-known subclasses
* `Blob`
* `ReadableStream` and `WritableStream`, with automatic flow control.
* `URL`
* `Headers`, `Request`, and `Response` from the Fetch API.

The following types are not supported as of this writing, but may be added in the future:
Expand Down
4 changes: 3 additions & 1 deletion __tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ let SERIALIZE_TEST_CASES: Record<string, unknown> = {
'["-inf"]': -Infinity,
'["nan"]': NaN,

'["url","https://example.com/path?q=1"]': new URL("https://example.com/path?q=1"),

'["headers",[]]': new Headers(),
'["headers",[["content-type","text/plain"],["x-custom","hello"]]]':
new Headers({"Content-Type": "text/plain", "X-Custom": "hello"}),
Expand Down Expand Up @@ -80,7 +82,7 @@ describe("simple serialization", () => {
it("can deserialize", () => {
for (let key in SERIALIZE_TEST_CASES) {
let value = deserialize(key);
if (value instanceof Uint8Array ||
if (value instanceof Uint8Array || value instanceof URL ||
value instanceof Headers || value instanceof Request || value instanceof Response) {
// toStrictEqual() won't work for these (e.g. in Node.js, Uint8Array may deserialize as
// Buffer), so test by serializing again and making sure they round-trip.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,9 @@ class Api extends RpcTarget {
expect(accepts(v.regexp, /abc/u)).toBe(true);
expect(accepts(v.regexp, "abc")).toBe(false);

expect(accepts(v.url, new URL("https://example.com/"))).toBe(true);
expect(accepts(v.url, "https://example.com/")).toBe(false);

const int16 = v.typedArray("Int16Array");
expect(accepts(int16, new Int16Array(2))).toBe(true);
expect(accepts(int16, new Uint8Array(2))).toBe(false);
Expand Down
2 changes: 2 additions & 0 deletions packages/capnweb-validate/src/internal/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ type BaseType =
| Float64Array
| ReadableStream<Uint8Array>
| WritableStream<unknown>
| URL
| Request
| Response
| Headers;
Expand Down Expand Up @@ -423,6 +424,7 @@ export const v = {
blob: exactBrand("Blob"),
readableStream: exactBrand("ReadableStream"),
writableStream: exactBrand("WritableStream"),
url: exactBrand("URL"),
headers: exactBrand("Headers"),
request: exactBrand("Request"),
response: exactBrand("Response"),
Expand Down
1 change: 1 addition & 0 deletions packages/capnweb-validate/src/transform/emit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ function emitValidator_(shape: TypeShape, ctx: EmitContext): string {
case "blob":
case "readableStream":
case "writableStream":
case "url":
case "headers":
case "request":
case "response":
Expand Down
2 changes: 2 additions & 0 deletions packages/capnweb-validate/src/transform/type-introspector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ export type TypeShape =
| { kind: "blob" } // Blob
| { kind: "readableStream" }
| { kind: "writableStream" }
| { kind: "url" }
| { kind: "headers" }
| { kind: "request" }
| { kind: "response" }
Expand Down Expand Up @@ -519,6 +520,7 @@ const BUILTIN_VALUE_TYPES: Record<string, TypeShape> = {
Blob: { kind: "blob" },
ReadableStream: { kind: "readableStream" },
WritableStream: { kind: "writableStream" },
URL: { kind: "url" },
Headers: { kind: "headers" },
Request: { kind: "request" },
Response: { kind: "response" },
Expand Down
4 changes: 4 additions & 0 deletions protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,10 @@ A JavaScript `Error` value. `type` is the name of the specific well-known `Error

When `props` is present, `stack` is normalised to `null` if absent so that positional indexing for `props` is unambiguous. When there are no extras, the legacy 3- or 4-element form is emitted unchanged.

`["url", href]`

A `URL` object. `href` is the fully-serialized (and normalized) URL string, i.e. the value of the URL's `href` property. The receiver reconstructs the `URL` via `new URL(href)`. For example: `["url", "https://example.com/path?q=1"]`.

`["headers", pairs]`

A `Headers` object from the Fetch API. `pairs` is an array of `[name, value]` pairs, where both `name` and `value` are strings. For example: `["headers", [["content-type", "text/plain"], ["x-custom", "hello"]]]`.
Expand Down
9 changes: 8 additions & 1 deletion src/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export type PropertyPath = (string | number)[];

type TypeForRpc = "unsupported" | "primitive" | "object" | "function" | "array" | "date" |
"bigint" | "bytes" | "blob" | "stub" | "rpc-promise" | "rpc-target" | "rpc-thenable" |
"error" | "undefined" | "writable" | "readable" | "headers" | "request" | "response";
"error" | "undefined" | "writable" | "readable" | "url" | "headers" | "request" | "response";

const AsyncFunction = (async function () {}).constructor;

Expand Down Expand Up @@ -115,6 +115,9 @@ export function typeForRpc(value: unknown): TypeForRpc {
case ReadableStream.prototype:
return "readable";

case URL.prototype:
return "url";

case Headers.prototype:
return "headers";

Expand Down Expand Up @@ -963,6 +966,7 @@ export class RpcPayload {
case "date":
case "bytes":
case "blob":
case "url":
case "error":
case "undefined":
// immutable, no need to copy
Expand Down Expand Up @@ -1344,6 +1348,7 @@ export class RpcPayload {
case "bytes":
case "blob":
case "date":
case "url":
case "error":
case "undefined":
return;
Expand Down Expand Up @@ -1489,6 +1494,7 @@ export class RpcPayload {
case "rpc-target":
case "writable":
case "readable":
case "url":
case "headers":
case "request":
case "response":
Expand Down Expand Up @@ -1642,6 +1648,7 @@ function followPath(value: unknown, parent: object | undefined,
case "blob":
case "date":
case "error":
case "url":
case "headers":
case "request":
case "response":
Expand Down
10 changes: 10 additions & 0 deletions src/serialize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,10 @@ export class Devaluator {
return alternateTypeName === undefined ? ["bytes", b64] : ["bytes", b64, alternateTypeName];
}

case "url":
// Always tuple-encode URLs; structured-clone support for URL isn't universal.
return ["url", (value as URL).href];

case "headers":
// The `Headers` TS type apparently doesn't declare itself as being
// Iterable<[string, string]>, but it is.
Expand Down Expand Up @@ -950,6 +954,12 @@ export class Evaluator {
case "nan":
return NaN;

case "url":
if (value.length === 2 && typeof value[1] === "string") {
return new URL(value[1]);
}
break;

case "headers":
// We only need to validate that the parameter is an array, so as not to invoke an
// unexpected variant of the Headers constructor. So long as it is an array then we can
Expand Down
1 change: 1 addition & 0 deletions src/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ type BaseType =
| Blob
| ReadableStream<any> // Chunk type can be any RPC-compatible type
| WritableStream<any> // Chunk type can be any RPC-compatible type
| URL
| Request
| Response
| Headers;
Expand Down
Loading