Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

39 add event handler for initial setup in the iframe webworker before the connection is established #40

Merged
Changes from 1 commit
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
Prev Previous commit
Next Next commit
await a connection confirmed before resolving the connection in the host
au-re committed Oct 27, 2024
commit 7566bf59ce1f09238ef6b3376eaef85b03448e35
7 changes: 6 additions & 1 deletion docs/GettingStarted.mdx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { Meta } from "@storybook/blocks";
import SingleIframeExample from "./examples/SingleIframeExample";
import WorkerExample from "./examples/WorkerExample";

<Meta title="Getting Started" />

# Rimless

Rimless makes event based communication easy with a promise-based API wrapping [postMessage](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage).
Rimless makes event based communication easy with a promise-based API wrapping [postMessage](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage).

Rimless works with both **iframes** and **webworkers**.

@@ -18,3 +19,7 @@ In the example below you can invoke remote procedures from an iframe to the host
versa.

<SingleIframeExample />

## RPCs on Web webworkers

<WorkerExample />
34 changes: 34 additions & 0 deletions docs/examples/WorkerExample.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import React from "react";

import { host } from "../../src/index";
import Worker from "./worker?worker";

function WorkerExample() {
const [color, setColor] = React.useState("#fff");
const [message, setMessage] = React.useState("");

const onClick = async () => {
const options = { initialValue: "initial value from host" };
const connection = await host.connect(new Worker(), options);

const messageRes = await connection?.remote.getMessage();
setMessage(messageRes);

const colorRes = await connection?.remote.createColor();
setColor(colorRes);
};

return (
<div style={{ background: color }}>
<div style={{ flex: 1 }}>
<h1>HOST</h1>
<button type="button" onClick={onClick}>
call web worker function
</button>
<p>{message}</p>
</div>
</div>
);
}

export default WorkerExample;
2 changes: 1 addition & 1 deletion docs/examples/iframe.html
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@
<head>
<meta charset="utf-8">
<title>Rimless Guest</title>
<script src="https://unpkg.com/rimless/lib/rimless.min.js"></script>
<script src="../../lib/rimless.min.js"></script>
<style>
html,
body {
35 changes: 35 additions & 0 deletions docs/examples/worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import guest from "../../src/guest";

function createColor() {
const letters = "0123456789ABCDEF";
let color = "#";
for (let i = 0; i < 6; i++) {
color += letters[Math.floor(Math.random() * 16)];
}
return color;
}

function getMessage() {
return "Hello from the worker! Initialized with:" + (self as any).config?.initialValue;
}

const run = async () => {
try {
await guest.connect(
{
createColor,
getMessage,
},
{
onConnectionSetup: async (config) => {
console.log("Connection setup with config:", config);
(self as any).config = config;
},
}
);
} catch (e) {
console.error(e);
}
};

run();
11 changes: 11 additions & 0 deletions docs/typings.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
declare module "*.html?raw" {
const content: any;
export default content;
}

declare module "*?worker" {
const WorkerFactory: {
new (): Worker;
};
export default WorkerFactory;
}
33 changes: 12 additions & 21 deletions src/guest.ts
Original file line number Diff line number Diff line change
@@ -2,14 +2,8 @@ import { extractMethods, isWorker } from "./helpers";
import { registerLocalMethods, registerRemoteMethods } from "./rpc";
import { actions, EventHandlers, events, IConnection, ISchema } from "./types";

const REQUEST_INTERVAL = 10;
const TIMEOUT_INTERVAL = 3000;

let interval: any = null;
let connected = false;

function connect(schema: ISchema = {}, eventHandlers?: EventHandlers): Promise<IConnection> {
return new Promise((resolve, reject) => {
return new Promise((resolve) => {
const localMethods = extractMethods(schema);

// on handshake response
@@ -29,15 +23,22 @@ function connect(schema: ISchema = {}, eventHandlers?: EventHandlers): Promise<I

await eventHandlers?.onConnectionSetup?.(remote);

// send a HANDSHAKE REPLY to the host
const payload = {
action: actions.HANDSHAKE_REPLY,
connectionID: event.data.connectionID,
};

if (isWorker()) self.postMessage(payload);
else window.parent.postMessage(payload, "*");

// close the connection and all listeners when called
const close = () => {
self.removeEventListener(events.MESSAGE, handleHandshakeResponse);
unregisterRemote();
unregisterLocal();
};

connected = true;

// resolve connection object
const connection = { remote, close };
return resolve(connection);
@@ -52,18 +53,8 @@ function connect(schema: ISchema = {}, eventHandlers?: EventHandlers): Promise<I
schema: JSON.parse(JSON.stringify(schema)),
};

interval = setInterval(() => {
if (connected) return clearInterval(interval);

// publish the HANDSHAKE REQUEST
if (isWorker()) (self as any).postMessage(payload);
else window.parent.postMessage(payload, "*");
}, REQUEST_INTERVAL);

// timeout the connection after a time
setTimeout(() => {
if (!connected) reject("connection timeout");
}, TIMEOUT_INTERVAL);
if (isWorker()) (self as any).postMessage(payload);
else window.parent.postMessage(payload, "*");
});
}

60 changes: 48 additions & 12 deletions src/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,12 @@
export const CONNECTION_TIMEOUT = 1000;

/**
* check if the remote is trusted
*
* @param event
*/
export function isTrustedRemote(_event: any) {
// TODO: implement
return true;
}

/**
* check if run in a webworker
*
* @param event
*/
export function isWorker() {
return typeof WorkerGlobalScope !== "undefined" && self instanceof WorkerGlobalScope;
export function isWorker(): boolean {
return typeof window === "undefined" && typeof self !== "undefined";
}

/**
@@ -81,3 +71,49 @@ export function getOriginFromURL(url: string | null) {
const portSuffix = port && port !== ports[protocol] ? `:${port}` : "";
return `${protocol}//${hostname}${portSuffix}`;
}

export function get(obj: any, path: string | Array<string | number>, defaultValue?: any): any {
const keys = Array.isArray(path) ? path : path.split(".").filter(Boolean);
let result = obj;

for (const key of keys) {
result = result?.[key];
if (result === undefined) {
return defaultValue;
}
}

return result;
}

export function set(obj: any, path: string | (string | number)[], value: any): any {
if (!obj || typeof obj !== "object") return obj;

const pathArray = Array.isArray(path) ? path : path.split(".").map((key) => (key.match(/^\d+$/) ? Number(key) : key));

let current = obj;

for (let i = 0; i < pathArray.length; i++) {
const key = pathArray[i];

if (i === pathArray.length - 1) {
current[key] = value;
} else {
if (!current[key] || typeof current[key] !== "object") {
current[key] = typeof pathArray[i + 1] === "number" ? [] : {};
}
current = current[key];
}
}

return obj;
}

export function generateId(length: number = 10): string {
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let result = "";
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
}
13 changes: 9 additions & 4 deletions src/host.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { extractMethods, getOriginFromURL } from "./helpers";
import { extractMethods, generateId, getOriginFromURL } from "./helpers";
import { registerLocalMethods, registerRemoteMethods } from "./rpc";
import { actions, events, IConnection, IConnections, ISchema } from "./types";
import { generateId } from "./utils";

const connections: IConnections = {};

@@ -73,14 +72,20 @@ function connect(guest: HTMLIFrameElement | Worker, schema: ISchema = {}): Promi
if (guestIsWorker) (guest as Worker).terminate();
};

// resolve connection object
const connection: IConnection = { remote, close };
connections[connectionID] = connection;
return resolve(connection);
}

// subscribe to HANDSHAKE MESSAGES
listeners.addEventListener(events.MESSAGE, handleHandshake);

// on handshake reply
function handleHandshakeReply(event: any) {
if (event.data.action !== actions.HANDSHAKE_REPLY) return;
return resolve(connections[event.data.connectionID]);
}

listeners.addEventListener(events.MESSAGE, handleHandshakeReply);
});
}

6 changes: 2 additions & 4 deletions src/rpc.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { isTrustedRemote, isWorker } from "./helpers";
import { generateId, get, isWorker, set } from "./helpers";
import { actions, events, IRPCRequestPayload, IRPCResolvePayload, ISchema } from "./types";
import { generateId, get, set } from "./utils";

/**
* for each function in the schema
@@ -24,7 +23,6 @@ export function registerLocalMethods(
const { action, callID, connectionID, callName, args = [] } = event.data as IRPCRequestPayload;

if (action !== actions.RPC_REQUEST) return;
if (!isTrustedRemote(event)) return;
if (!callID || !callName) return;
if (callName !== methodName) return;
if (connectionID !== _connectionID) return;
@@ -43,6 +41,7 @@ export function registerLocalMethods(
const result = await get(schema, methodName)(...args);
payload.result = JSON.parse(JSON.stringify(result));
} catch (error) {
payload.action = actions.RPC_REJECT;
payload.error = JSON.parse(JSON.stringify(error, Object.getOwnPropertyNames(error)));
}

@@ -88,7 +87,6 @@ export function createRPC(
function handleResponse(event: any) {
const { callID, connectionID, callName, result, error, action } = event.data as IRPCResolvePayload;

if (!isTrustedRemote(event)) return;
if (!callID || !callName) return;
if (callName !== _callName) return;
if (connectionID !== _connectionID) return;
2 changes: 1 addition & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
@@ -20,7 +20,7 @@ export interface IConnection {
}

export interface IConnections {
[connectionID: string]: ISchema;
[connectionID: string]: IConnection;
}

export interface IEvent extends EventListener {
1 change: 0 additions & 1 deletion src/typings.d.ts

This file was deleted.

45 changes: 0 additions & 45 deletions src/utils.ts

This file was deleted.

5 changes: 0 additions & 5 deletions src/vite-env.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1 @@
/// <reference types="vite/client" />

declare module "*.html" {
const content: any;
export default content;
}