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-regexp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"capnweb": minor
---

Support serializing `RegExp` objects over RPC.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,11 +203,11 @@ 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.
* `RegExp`
* `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:
* `Map` and `Set`
* `RegExp`

The following are intentionally NOT supported:
* Application-defined classes that do not extend `RpcTarget`.
Expand Down
3 changes: 3 additions & 0 deletions __tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ let SERIALIZE_TEST_CASES: Record<string, unknown> = {
'["-inf"]': -Infinity,
'["nan"]': NaN,

'["regexp","foo\\\\d+","gi"]': /foo\d+/gi,
'["regexp","^bar$",""]': /^bar$/,

'["headers",[]]': new Headers(),
'["headers",[["content-type","text/plain"],["x-custom","hello"]]]':
new Headers({"Content-Type": "text/plain", "X-Custom": "hello"}),
Expand Down
4 changes: 4 additions & 0 deletions protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,10 @@ bound parsing cost.

A JavaScript `Date` value. The number represents milliseconds since the Unix epoch.

`["regexp", source, flags]`

A JavaScript `RegExp` value. `source` and `flags` are the strings from the regular expression's `source` and `flags` properties. The receiver reconstructs the value via `new RegExp(source, flags)`. `flags` is always present, and is an empty string when the expression has no flags. For example, `/foo\d+/gi` is encoded as `["regexp", "foo\\d+", "gi"]`.

`["error", type, message, stack?, props?]`

A JavaScript `Error` value. `type` is the name of the specific well-known `Error` subclass, e.g. "TypeError". `message` is a string containing the error message. `stack` may optionally contain the stack trace, though by default stacks will be redacted for security reasons.
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" | "regexp" | "headers" | "request" | "response";

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

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

case RegExp.prototype:
return "regexp";

case Uint8Array.prototype:
case BUFFER_PROTOTYPE:
case ArrayBuffer.prototype:
Expand Down Expand Up @@ -963,6 +966,7 @@ export class RpcPayload {
case "date":
case "bytes":
case "blob":
case "regexp":
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 "regexp":
case "error":
case "undefined":
return;
Expand Down Expand Up @@ -1489,6 +1494,7 @@ export class RpcPayload {
case "rpc-target":
case "writable":
case "readable":
case "regexp":
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 "regexp":
case "headers":
case "request":
case "response":
Expand Down
15 changes: 15 additions & 0 deletions src/serialize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,15 @@ export class Devaluator {
return ["date", Number.isNaN(time) ? null : time];
}

case "regexp": {
// At structuredClonable level, keep RegExp as native value.
if (this.encodingLevel === "structuredClonable") {
return value;
}
let re = <RegExp>value;
return ["regexp", re.source, re.flags];
}

case "bytes": {
let alternateTypeName = BYTE_CONTAINER_TYPE_BY_PROTOTYPE.get(Object.getPrototypeOf(value));
let bytes: Uint8Array;
Expand Down Expand Up @@ -841,6 +850,12 @@ export class Evaluator {
return new Date(value[1]);
}
break;
case "regexp":
if (value.length === 3 && typeof value[1] === "string" &&
typeof value[2] === "string") {
return new RegExp(value[1], value[2]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This tuple decoder is only reached at non-structuredClonable levels. At structuredClonable level the value arrives as a raw RegExp (emitted by the Devaluator at line 340-341) and never becomes an Array, so it bypasses this case and is mangled into {} by the generic object path (line 1248). The corresponding fix is in the passthrough block at line 819:

if (value instanceof Date || value instanceof RegExp || typeof value === "bigint") {
  return value;
}

}
break;
case "bytes": {
let bytes: Uint8Array;
// At jsonCompatibleWithBytes/structuredClonable level, bytes may already be raw.
Expand Down
Loading