From fefe796a9a40177348433258df33e3d506d23b68 Mon Sep 17 00:00:00 2001 From: Dani Guardiola Date: Fri, 22 Dec 2023 04:41:08 +0100 Subject: [PATCH] Demo + misc. fixes and improvements --- .changeset/cold-turtles-invite.md | 5 + .changeset/lucky-gorillas-yell.md | 5 + .changeset/ninety-clocks-live.md | 5 + .changeset/odd-cobras-relax.md | 5 + .changeset/sour-laws-admire.md | 5 + bun.lockb | Bin 165618 -> 184847 bytes demo/.gitignore | 2 + demo/iframe.html | 114 +++++++++++ demo/iframe.ts | 147 +++++++++++++++ demo/index.html | 250 +++++++++++++++++++++++++ demo/parent.ts | 243 ++++++++++++++++++++++++ demo/tailwind.css | 7 + demo/utils.ts | 5 + package.json | 12 +- src/rpc.ts | 61 ++++-- src/transport-utils.ts | 6 +- src/transports/browser-runtime-port.ts | 6 + src/transports/message-port.ts | 22 ++- src/types.ts | 32 ++-- tailwind.config.js | 5 + tsconfig.json | 2 +- 21 files changed, 903 insertions(+), 36 deletions(-) create mode 100644 .changeset/cold-turtles-invite.md create mode 100644 .changeset/lucky-gorillas-yell.md create mode 100644 .changeset/ninety-clocks-live.md create mode 100644 .changeset/odd-cobras-relax.md create mode 100644 .changeset/sour-laws-admire.md create mode 100644 demo/.gitignore create mode 100644 demo/iframe.html create mode 100644 demo/iframe.ts create mode 100644 demo/index.html create mode 100644 demo/parent.ts create mode 100644 demo/tailwind.css create mode 100644 demo/utils.ts create mode 100644 tailwind.config.js diff --git a/.changeset/cold-turtles-invite.md b/.changeset/cold-turtles-invite.md new file mode 100644 index 0000000..1f66aa3 --- /dev/null +++ b/.changeset/cold-turtles-invite.md @@ -0,0 +1,5 @@ +--- +"rpc-anywhere": patch +--- + +Better naming for low-level message types. diff --git a/.changeset/lucky-gorillas-yell.md b/.changeset/lucky-gorillas-yell.md new file mode 100644 index 0000000..da3d330 --- /dev/null +++ b/.changeset/lucky-gorillas-yell.md @@ -0,0 +1,5 @@ +--- +"rpc-anywhere": minor +--- + +Fixed message port transport. diff --git a/.changeset/ninety-clocks-live.md b/.changeset/ninety-clocks-live.md new file mode 100644 index 0000000..2092b14 --- /dev/null +++ b/.changeset/ninety-clocks-live.md @@ -0,0 +1,5 @@ +--- +"rpc-anywhere": minor +--- + +Added debug hooks for logging and debugging. diff --git a/.changeset/odd-cobras-relax.md b/.changeset/odd-cobras-relax.md new file mode 100644 index 0000000..6f4f090 --- /dev/null +++ b/.changeset/odd-cobras-relax.md @@ -0,0 +1,5 @@ +--- +"rpc-anywhere": patch +--- + +Reduced chance of colision for the transport id key. diff --git a/.changeset/sour-laws-admire.md b/.changeset/sour-laws-admire.md new file mode 100644 index 0000000..e2b6fa8 --- /dev/null +++ b/.changeset/sour-laws-admire.md @@ -0,0 +1,5 @@ +--- +"rpc-anywhere": minor +--- + +Added a cool demo! diff --git a/bun.lockb b/bun.lockb index 6f3fd8b677700f74bfbebcd72a9846d87cbf5946..94eff76e9b743a6bf6c50a9d34534662be66cbad 100755 GIT binary patch delta 44107 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 Wu~>hIo7IKVnHm=DA6>&<>->LZpzZ+x delta 33046 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 + + + RPC Anywhere demo - iframe + + + + + + + + +
+ + +

Story time! Fill in the blanks:

+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + + +
+ + + diff --git a/demo/iframe.ts b/demo/iframe.ts new file mode 100644 index 0000000..a06ecec --- /dev/null +++ b/demo/iframe.ts @@ -0,0 +1,147 @@ +import { + type _RPCPacket, + createRPC, + createRPCRequestHandler, + createTransportFromMessagePort, + type RPCSchema, +} from "../src/index.js"; // "rpc-anywhere" +// import the parent (remote) schema +import { type ParentSchema } from "./parent.js"; + +// grab some elements and prepare some stuff +const syncedInputEl = el("synced-input"); +const coloredButtonEl = el("colored-button"); +const storyButtonEl = el("story-button"); +const storyResultEl = el("story-result"); +const storyTitleEl = el("story-title"); +const storyVillageNameEl = el("story-village-name-input"); +const storyAnimalEl = el("story-animal-input"); +const storyNameEl = el("story-name-input"); +const storyActivityEl = el("story-activity-input"); +const storyLandmarkEl = el("story-landmark-input"); +const storyObjectEl = el("story-object-input"); +const storySuperpowerEl = el("story-superpower-input"); +const storyNewTitleEl = el("story-new-title-input"); +const colors = ["red", "green", "blue", "purple"] as const; +type Color = (typeof colors)[number]; + +// request handler +const requestHandler = createRPCRequestHandler({ + /** + * Get the current color of the button. + */ + getColor: () => coloredButtonEl.dataset.color as Color, +}); + +// declare the iframe (local) schema +export type IframeSchema = RPCSchema< + { + messages: { + /** + * Sent when the iframe's RPC is ready. + */ + ready: void; + /** + * Sent when the iframe's input is updated. + */ + iframeInputUpdated: string; + }; + }, + // infer request types from the request handler + typeof requestHandler +>; + +function waitForFrameParentLoad() { + if (window.parent.document.readyState === "complete") + return Promise.resolve(); + return new Promise((resolve) => + window.parent.addEventListener("load", resolve), + ); +} + +// wait for the parent window to load +waitForFrameParentLoad().then(() => { + console.log("[iframe] The parent has loaded!"); + + // create the iframe's RPC + const rpc = createRPC({ + // provide the transport + transport: createTransportFromMessagePort(window, window.parent, { + // provide a unique ID that matches the parent + transportId: "rpc-anywhere-demo", + }), + // provide the request handler + requestHandler, + // this is for demo purposes - you can ignore it + _debugHooks: { onSend: _debugOnSend, onReceive: _debugOnReceive }, + }); + + // use the proxy as an alias ✨ + const parent = rpc.proxy; + + // send the ready message + parent.send.ready(); + + // synced input + syncedInputEl.addEventListener("input", () => + parent.send.iframeInputUpdated(syncedInputEl.value), + ); + rpc.addMessageListener( + "parentInputUpdated", + (value) => (syncedInputEl.value = value), + ); + + // story time + storyButtonEl.addEventListener("click", async () => { + const { title } = await parent.request.createStory({ + villageName: storyVillageNameEl.value, + animal: storyAnimalEl.value, + name: storyNameEl.value, + activity: storyActivityEl.value, + landmark: storyLandmarkEl.value, + object: storyObjectEl.value, + superpower: storySuperpowerEl.value, + newTitle: storyNewTitleEl.value, + }); + storyResultEl.style.removeProperty("display"); + storyTitleEl.textContent = title; + }); +}); + +// non-demo stuff - you can ignore this :) +// --------------------------------------- + +const colorToClass = { + red: "bg-red-500", + green: "bg-green-500", + blue: "bg-blue-500", + purple: "bg-purple-500", +}; +function updateButtonColor(color?: Color) { + const currentColor = coloredButtonEl.dataset.color; + const currentClass = colorToClass[currentColor as Color]; + const nextColor = + color ?? + colors[(colors.indexOf(currentColor as Color) + 1) % colors.length]; + const nextClass = colorToClass[nextColor]; + coloredButtonEl.dataset.color = nextColor; + coloredButtonEl.classList.remove(currentClass); + coloredButtonEl.classList.add(nextClass); +} +coloredButtonEl.addEventListener("click", () => updateButtonColor()); + +function el(id: string) { + const element = document.getElementById(id); + if (!element) throw new Error(`Element with id ${id} not found`); + return element as Element; +} + +const iframeLogsEl = window.parent.document.querySelector("#iframe-logs")!; +function _debugOnSend(packet: _RPCPacket) { + console.log("[iframe] sent", packet); + (window.parent as any)._debugAppendMessage("send", iframeLogsEl, packet); +} +function _debugOnReceive(packet: _RPCPacket) { + console.log("[iframe] received", packet); + (window.parent as any)._debugAppendMessage("receive", iframeLogsEl, packet); +} diff --git a/demo/index.html b/demo/index.html new file mode 100644 index 0000000..c9d6a50 --- /dev/null +++ b/demo/index.html @@ -0,0 +1,250 @@ + + + + RPC Anywhere demo - parent + + + + + + + + + +
+
+

+ RPC + + Anywhere +

+
+

+ RPC Anywhere + lets you create type-safe RPCs. +

+

+ $ bun add rpc-anywhere +

+

+ This is a demo of an RPC between this page ("parent") and a child + iframe. Read the code: + parent.ts + and + iframe.ts. +

+

+ RPC Anywhere is transport agnostic, and is not limited to iframes. + It works for Electron IPC, Web Workers, browser extension content + scripts... Anything! + Learn more. +

+
+

parent

+
+ +

+ Waiting for the iframe to load... +

+ +
+

The button is red.

+ +
+ +
+
+

iframe

+
+ +
+
+
+
+
+

about this demo

+

+ The blue container aboveto the left is an iframe (a + different webpage loaded inside this one). +

+

+ The iframe's page is separate from this one, and has a different + JavaScript context. An RPC helps bridge the gap, allowing both pages + to send messages and make requests to each other. +

+
+ +

+ parent logs · + +

+
+
+

+ iframe logs · + +

+
+
+
+ + + + diff --git a/demo/parent.ts b/demo/parent.ts new file mode 100644 index 0000000..8872bb6 --- /dev/null +++ b/demo/parent.ts @@ -0,0 +1,243 @@ +import { + type _RPCPacket, + createRPC, + createRPCRequestHandler, + createTransportFromMessagePort, + type RPCSchema, +} from "../src/index.js"; // "rpc-anywhere" +// import the iframe (remote) schema +import { type IframeSchema } from "./iframe.js"; + +// grab some elements +const iframeEl = el("iframe"); +const syncedInputEl = el("synced-input"); +const updateColoredButtonEl = el("update-colored-button"); + +// request handler +const requestHandler = createRPCRequestHandler({ + /** + * Create a story with the given details. + */ + createStory: (storyDetails: StoryDetails) => { + renderStory(storyDetails); + const { name, newTitle, villageName } = storyDetails; + return { title: `The Tale of ${name}, the ${newTitle} of ${villageName}` }; + }, +}); + +// declare the parent (local) schema +export type ParentSchema = RPCSchema< + { + messages: { + /** + * Sent when the parent's input is updated. + */ + parentInputUpdated: string; + }; + }, + typeof requestHandler +>; + +console.log({ thing: iframeEl.contentWindow }); + +function waitForFrameLoad(frame: HTMLIFrameElement) { + if (frame.contentWindow?.document.readyState === "complete") + return Promise.resolve(); + return new Promise((resolve) => + window.parent.addEventListener("load", resolve), + ); +} + +// wait for the iframe to load +await waitForFrameLoad(iframeEl); +console.log("[parent] Parent loaded!"); + +// create the parent's RPC +const rpc = createRPC({ + // provide the transport + transport: createTransportFromMessagePort(window, iframeEl.contentWindow!, { + // provide a unique ID that matches the iframe + transportId: "rpc-anywhere-demo", + }), + // provide the request handler + requestHandler, + // this is for demo purposes - you can ignore it + _debugHooks: { onSend: _debugOnSend, onReceive: _debugOnReceive }, +}); + +// use the proxy as an alias ✨ +const iframe = rpc.proxy; + +// ready +rpc.addMessageListener("ready", onIframeReady); + +// synced input +syncedInputEl.addEventListener("input", () => + iframe.send.parentInputUpdated(syncedInputEl.value), +); +rpc.addMessageListener( + "iframeInputUpdated", + (value) => (syncedInputEl.value = value), +); + +// colored button +updateColoredButtonEl.addEventListener("click", async () => { + const currentColor = await iframe.request.getColor(); + el("button-color").textContent = currentColor; +}); + +type StoryDetails = { + /** The name of the village where the story is set. */ + villageName: string; + + /** The type of animal the main character is. */ + animal: string; + + /** The name of the main character. */ + name: string; + + /** The main activity the character enjoys. */ + activity: string; + + /** A significant landmark in the village. */ + landmark: string; + + /** An object found by the main character that has special powers. */ + object: string; + + /** A special ability or power provided by the magical object. */ + superpower: string; + + /** The new title or status achieved by the main character at the end. */ + newTitle: string; +}; + +// non-demo stuff - you can ignore this :) +// --------------------------------------- + +function el(id: string) { + const element = document.getElementById(id); + if (!element) throw new Error(`Element with id ${id} not found`); + return element as Element; +} + +function els(selector: string) { + const elements = document.querySelectorAll(selector); + if (elements.length === 0) + throw new Error(`No elements found matching selector ${selector}`); + return Array.from(elements) as Element[]; +} + +function onIframeReady() { + el("ready").style.removeProperty("display"); // remove display: none + el("loading").style.display = "none"; + el("controls").classList.remove("opacity-60", "pointer-events-none"); +} + +function updateIframeSize() { + const size = + iframeEl.contentWindow!.document.querySelector("#sizer")!.scrollHeight; + iframeEl.style.height = `${size + 4}px`; +} + +iframeEl.addEventListener("load", () => { + updateIframeSize(); + window.addEventListener("resize", updateIframeSize); +}); + +function renderStory({ + villageName, + animal, + name, + activity, + landmark, + object, + superpower, + newTitle, +}: StoryDetails) { + els(".story-village-name").forEach((el) => (el.textContent = villageName)); + els(".story-animal").forEach((el) => (el.textContent = animal)); + els(".story-name").forEach((el) => (el.textContent = name)); + els(".story-activity").forEach((el) => (el.textContent = activity)); + els(".story-landmark").forEach((el) => (el.textContent = landmark)); + els(".story-object").forEach((el) => (el.textContent = object)); + els(".story-superpower").forEach((el) => (el.textContent = superpower)); + els(".story-new-title").forEach((el) => (el.textContent = newTitle)); + el("story").style.removeProperty("display"); + const storyEl = el("story"); + storyEl.style.removeProperty("display"); +} + +declare const jsonFormatHighlight: (data: any) => any; +const messageTemplate = el("message-template"); +function _debugAppendMessage( + type: "send" | "receive", + logElement: HTMLElement, + packet: _RPCPacket, +) { + const msgEl = messageTemplate.content.firstElementChild!.cloneNode( + true, + ) as HTMLParagraphElement; + const typeArrow = type === "send" ? "→" : "←"; + if (type === "receive") msgEl.classList.remove("bg-white/10"); + // time in HH:MM:SS + const time = new Date().toLocaleTimeString("en-US").split(" ")[0]; + if (packet.type === "message") { + msgEl.querySelector(".packet-meta")!.textContent = + `${time} ${typeArrow} message: ${packet.id}`; + msgEl.querySelector(".packet-payload")!.innerHTML = jsonFormatHighlight( + packet.payload === undefined ? "<no payload>" : packet.payload, + ); + } + if (packet.type === "request") { + msgEl.querySelector(".packet-meta")!.textContent = + `${time} ${typeArrow} request (${packet.id}): ${packet.method}`; + msgEl.querySelector(".packet-payload")!.innerHTML = jsonFormatHighlight( + packet.params === undefined ? "<no params>" : packet.params, + ); + } + if (packet.type === "response") { + msgEl.querySelector(".packet-meta")!.textContent = + `${time} ${typeArrow} response (id: ${packet.id}): ${ + packet.success ? "✅" : "❌" + }`; + if (packet.success) + msgEl.querySelector(".packet-payload")!.innerHTML = jsonFormatHighlight( + packet.payload === undefined ? "<no response>" : packet.payload, + ); + else + msgEl.querySelector(".packet-payload")!.innerHTML = jsonFormatHighlight( + packet.error === undefined ? "<no error>" : packet.error, + ); + } + const wasScrolledToBottom = + Math.abs( + logElement.scrollHeight - logElement.scrollTop - logElement.clientHeight, + ) < 1; + + logElement.appendChild(msgEl); + if (wasScrolledToBottom) logElement.scrollTop = logElement.scrollHeight; +} +(window as any)._debugAppendMessage = _debugAppendMessage; + +const parentLogsEl = el("parent-logs"); +const iframeLogsEl = el("iframe-logs"); +const parentLogsClearEl = el("parent-logs-clear"); +const iframeLogsClearEl = el("iframe-logs-clear"); +function _debugOnSend(packet: _RPCPacket) { + console.log("[parent] sent", packet); + setTimeout(updateIframeSize, 100); // hack ¯\_(ツ)_/¯ + (window as any)._debugAppendMessage("send", parentLogsEl, packet); +} +function _debugOnReceive(packet: _RPCPacket) { + console.log("[parent] received", packet); + (window as any)._debugAppendMessage("receive", parentLogsEl, packet); +} +parentLogsClearEl.addEventListener( + "click", + () => (parentLogsEl.innerHTML = ""), +); +iframeLogsClearEl.addEventListener( + "click", + () => (iframeLogsEl.innerHTML = ""), +); diff --git a/demo/tailwind.css b/demo/tailwind.css new file mode 100644 index 0000000..4106d30 --- /dev/null +++ b/demo/tailwind.css @@ -0,0 +1,7 @@ +:root { + font-family: "Inter", sans-serif; +} + +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/demo/utils.ts b/demo/utils.ts new file mode 100644 index 0000000..b420fad --- /dev/null +++ b/demo/utils.ts @@ -0,0 +1,5 @@ +export function el(id: string) { + const element = document.getElementById(id); + if (!element) throw new Error(`Element with id ${id} not found`); + return element as Element; +} diff --git a/package.json b/package.json index cd3ea8f..f8a9fb3 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,14 @@ "scripts": { "prepare": "tshy", "publish": "changeset publish", - "test": "bun test src/tests" + "test": "bun test src/tests", + "build-demo:js": "bun build demo/*.ts --outdir ./demo", + "build-demo:css": "tailwindcss -i demo/tailwind.css -o demo/style.css", + "build-demo": "bun --bun conc bun:build-demo:*", + "demo:watch:js": "bun build-demo:js --watch", + "demo:watch:css": "bun build-demo:css --watch --watch", + "demo:serve": "bun --bun servor demo --reload", + "demo": "bun --bun conc -k bun:demo:*" }, "dependencies": { "browser-namespace": "^1.3.0" @@ -25,10 +32,13 @@ "@typescript-eslint/eslint-plugin": "^6.14.0", "@typescript-eslint/parser": "^6.14.0", "bun-types": "latest", + "concurrently": "^8.2.2", "eslint": "^8.56.0", "eslint-plugin-import": "^2.29.1", "eslint-plugin-simple-import-sort": "^10.0.0", "prettier": "^3.1.1", + "servor": "^4.0.2", + "tailwindcss": "^3.4.0", "tshy": "^1.8.2", "typescript": "^5.3.3" }, diff --git a/src/rpc.ts b/src/rpc.ts index be4d5a9..51f6012 100644 --- a/src/rpc.ts +++ b/src/rpc.ts @@ -1,17 +1,18 @@ import { - type RPCMessage, - type RPCMessageFromSchema, + type _RPCMessagePacket, + type _RPCMessagePacketFromSchema, + type _RPCPacket, + type _RPCRequestPacket, + type _RPCRequestPacketFromSchema, + type _RPCResponsePacket, + type _RPCResponsePacketFromSchema, type RPCMessageHandlerFn, type RPCMessagePayload, type RPCMessagesProxy, - type RPCRequest, - type RPCRequestFromSchema, type RPCRequestHandler, type RPCRequestHandlerFn, type RPCRequestResponse, type RPCRequestsProxy, - type RPCResponse, - type RPCResponseFromSchema, type RPCSchema, type RPCTransport, type WildcardRPCMessageHandlerFn, @@ -27,6 +28,20 @@ function missingTransportMethodError(methods: string[], action: string) { ); } +type DebugHooks = { + /** + * A function that will be called when the RPC sends a low-level + * message. + */ + onSend?: (packet: _RPCPacket) => void; + + /** + * A function that will be called when the RPC receives a low-level + * message. + */ + onReceive?: (packet: _RPCPacket) => void; +}; + export type _RPCOptions = { /** * A transport object that will be used to send and receive @@ -46,6 +61,13 @@ export type _RPCOptions = { * @default 1000 */ maxRequestTime?: number; + + /** + * A collection of optional functions that will be called when + * the RPC sends or receives a low-level message. Useful for + * debugging and logging. + */ + _debugHooks?: DebugHooks; }; export function _createRPC< @@ -60,6 +82,15 @@ export function _createRPC< // setters // ------- + let debugHooks: DebugHooks = {}; + + /** + * Sets the debug hooks that will be used to debug the RPC instance. + */ + function _setDebugHooks(newDebugHooks: DebugHooks) { + debugHooks = newDebugHooks; + } + let transport: RPCTransport = {}; /** @@ -107,6 +138,7 @@ export function _createRPC< const { maxRequestTime = DEFAULT_MAX_REQUEST_TIME } = options; if (options.transport) setTransport(options.transport); if (options.requestHandler) setRequestHandler(options.requestHandler); + if (options._debugHooks) _setDebugHooks(options._debugHooks); // requests // -------- @@ -139,7 +171,7 @@ export function _createRPC< if (!transport.send) throw missingTransportMethodError(["send"], "make requests"); const requestId = getRequestId(); - const request: RPCRequest = { + const request: _RPCRequestPacket = { type: "request", id: requestId, method, @@ -154,6 +186,7 @@ export function _createRPC< reject(new Error("RPC request timed out.")); }, maxRequestTime), ); + debugHooks.onSend?.(request); transport.send(request); }) as Promise; } @@ -200,11 +233,12 @@ export function _createRPC< const payload = args[0]; if (!transport.send) throw missingTransportMethodError(["send"], "send messages"); - const rpcMessage: RPCMessage = { + const rpcMessage: _RPCMessagePacket = { type: "message", id: message as string, payload, }; + debugHooks.onSend?.(rpcMessage); transport.send(rpcMessage); } @@ -363,10 +397,11 @@ export function _createRPC< async function handler( message: - | RPCRequestFromSchema - | RPCResponseFromSchema - | RPCMessageFromSchema, + | _RPCRequestPacketFromSchema + | _RPCResponsePacketFromSchema + | _RPCMessagePacketFromSchema, ) { + debugHooks.onReceive?.(message); if (!("type" in message)) throw new Error("Message does not contain a type."); if (message.type === "request") { @@ -376,7 +411,7 @@ export function _createRPC< "handle requests", ); const { id, method, params } = message; - let response: RPCResponse; + let response: _RPCResponsePacket; try { response = { type: "response", @@ -393,6 +428,7 @@ export function _createRPC< error: error.message, }; } + debugHooks.onSend?.(response); transport.send(response); return; } @@ -433,6 +469,7 @@ export function _createRPC< addMessageListener, removeMessageListener, proxy, + _setDebugHooks, }; } diff --git a/src/transport-utils.ts b/src/transport-utils.ts index bdc2545..c8b315e 100644 --- a/src/transport-utils.ts +++ b/src/transport-utils.ts @@ -1,3 +1,5 @@ +const transportIdKey = "[transport-id]"; + /** * Common options for a transport. */ @@ -25,7 +27,7 @@ export function rpcTransportMessageOut( options: Pick, ) { const { transportId } = options; - if (transportId != null) return { transportId, data }; + if (transportId != null) return { [transportIdKey]: transportId, data }; return data; } @@ -46,7 +48,7 @@ export function rpcTransportMessageIn( let data = message; if (transportId) { - if (message.transportId !== transportId) return [true]; + if (message[transportIdKey] !== transportId) return [true]; data = message.data; } if (filterResult === false) return [true]; diff --git a/src/transports/browser-runtime-port.ts b/src/transports/browser-runtime-port.ts index 8b17b9d..3459fb6 100644 --- a/src/transports/browser-runtime-port.ts +++ b/src/transports/browser-runtime-port.ts @@ -34,7 +34,13 @@ export type RPCBrowserRuntimePortTransportOptions = Pick< * between content scripts and service workers in browser extensions. */ export function createTransportFromBrowserRuntimePort( + /** + * The browser runtime port. + */ port: Port, + /** + * Options for the browser runtime port transport. + */ options: RPCBrowserRuntimePortTransportOptions = {}, ): RPCTransport { const { transportId, filter } = options; diff --git a/src/transports/message-port.ts b/src/transports/message-port.ts index 5da7c8e..cdfc725 100644 --- a/src/transports/message-port.ts +++ b/src/transports/message-port.ts @@ -34,23 +34,29 @@ export type RPCMessagePortTransportOptions = Pick< */ export function createTransportFromMessagePort( /** - * The object that supports `postMessage(message)` and - * `addEventListener("message", listener)`. This includes `Window`, - * `Worker`, `MessagePort`, and `BroadcastChannel`. + * The local port that will receive and handled "message" events + * through `addEventListener("message", listener)`. */ - messageEventTarget: Window | Worker | MessagePort | BroadcastChannel, + localPort: Window | Worker | MessagePort | BroadcastChannel, + /** + * The remote port to send messages to through `postMessage(message)`. + */ + remotePort: Window | Worker | MessagePort | BroadcastChannel, /** * Options for the message port transport. */ options: RPCMessagePortTransportOptions = {}, ): RPCTransport { const { transportId, filter } = options; - const target = messageEventTarget as Window; // little white TypeScript lie + + // little white TypeScript lies + const local = localPort as Window; + const remote = remotePort as Window; let transportHandler: ((event: MessageEvent) => any) | undefined; return { send(data) { - target.postMessage(rpcTransportMessageOut(data, { transportId })); + remote.postMessage(rpcTransportMessageOut(data, { transportId })); }, registerHandler(handler) { transportHandler = (event: MessageEvent) => { @@ -62,11 +68,11 @@ export function createTransportFromMessagePort( if (ignore) return; handler(data); }; - target.addEventListener("message", transportHandler); + local.addEventListener("message", transportHandler); }, unregisterHandler() { if (transportHandler) - target.removeEventListener("message", transportHandler); + local.removeEventListener("message", transportHandler); }, }; } diff --git a/src/types.ts b/src/types.ts index 08ad7d7..d1da910 100644 --- a/src/types.ts +++ b/src/types.ts @@ -6,7 +6,7 @@ import { type _RPCOptions, type RPCInstance } from "./rpc.js"; /** * A low-level RPC message representing a request. */ -export type RPCRequest = { +export type _RPCRequestPacket = { /** * The type of the message. */ @@ -28,7 +28,7 @@ export type RPCRequest = { /** * A low-level RPC message representing a response. */ -export type RPCResponse = +export type _RPCResponsePacket = | { /** * The type of the message. @@ -69,7 +69,7 @@ export type RPCResponse = /** * A low-level RPC message representing a message. */ -export type RPCMessage = { +export type _RPCMessagePacket = { /** * The type of the message. */ @@ -84,6 +84,14 @@ export type RPCMessage = { payload: Payload; }; +/** + * A low-level RPC message. + */ +export type _RPCPacket = + | _RPCRequestPacket + | _RPCResponsePacket + | _RPCMessagePacket; + // requests // -------- @@ -128,20 +136,20 @@ export type RPCRequestResponse< * for that method. Otherwise, it will return a union of messages * for all methods. */ -export type RPCRequestFromSchema< +export type _RPCRequestPacketFromSchema< RequestsSchema extends RPCRequestsSchema, Method extends keyof RequestsSchema = keyof RequestsSchema, -> = RPCRequest>; +> = _RPCRequestPacket>; /** * A utility type for getting the response low-level message from * a schema. If a method is provided, it will return the message * for that method. Otherwise, it will return a union of messages * for all methods. */ -export type RPCResponseFromSchema< +export type _RPCResponsePacketFromSchema< RequestsSchema extends RPCRequestsSchema, Method extends keyof RequestsSchema = keyof RequestsSchema, -> = RPCResponse>; +> = _RPCResponsePacket>; /** * A request handler in "function" form. @@ -296,10 +304,10 @@ export type RPCMessagePayload< * message for that message. Otherwise, it will return a union * of messages for all messages. */ -export type RPCMessageFromSchema< +export type _RPCMessagePacketFromSchema< MessagesSchema extends RPCMessagesSchema, MessageName extends keyof MessagesSchema = keyof MessagesSchema, -> = RPCMessage>; +> = _RPCMessagePacket>; /** * A message handler for a specific message. @@ -407,7 +415,7 @@ export type RPCTransport = { // options // ------- -type RPCBaseOption = "transport"; +type RPCBaseOption = "transport" | "_debugHooks"; type RPCRequestsInOption = "requestHandler"; type RPCRequestsOutOption = "maxRequestTime"; @@ -441,7 +449,7 @@ export type RPCOptions< // rpc // --- -type RPCMethods = "setTransport"; +type RPCMethod = "setTransport"; type RPCRequestsInMethod = "setRequestHandler"; type RPCRequestsOutMethod = "request" | "requestProxy"; type RPCMessagesInMethod = "addMessageListener" | "removeMessageListener"; @@ -486,7 +494,7 @@ export type RPC< RemoteSchema extends RPCSchema, > = Pick< RPCInstance, - | RPCMethods + | RPCMethod | MethodsByLocalSchema | MethodsByRemoteSchema | MethodsByRemoteSchemaAndLocalSchema diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..4ba8fa9 --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,5 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ["./demo/*.html"], + safelist: ["bg-red-500", "bg-blue-500", "bg-green-500", "bg-purple-500"], +}; diff --git a/tsconfig.json b/tsconfig.json index b6113a7..61b6610 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,5 +17,5 @@ "bun-types" // add Bun global ] }, - "include": ["src/**/*"] + "include": ["src/**/*", "demo/**/*"] }