Skip to content

Commit

Permalink
39 add event handler for initial setup in the iframe webworker before…
Browse files Browse the repository at this point in the history
… the connection is established (#40)

* add onConnectionSetup event handler

* 0.3.0

* await a connection confirmed before resolving the connection in the host

* 0.4.0
  • Loading branch information
au-re authored Oct 27, 2024
1 parent e8ce245 commit 7385075
Show file tree
Hide file tree
Showing 15 changed files with 172 additions and 101 deletions.
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**.

Expand All @@ -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
Expand Up @@ -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 {
Expand Down
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;
}
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "rimless",
"author": "Aurélien Franky",
"version": "0.2.3",
"version": "0.4.0",
"license": "MIT",
"homepage": "https://github.com/au-re/rimless",
"description": "event base communication made easy with a promise-based API wrapping `postMessage`",
Expand Down
41 changes: 17 additions & 24 deletions src/guest.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,13 @@
import { extractMethods, isWorker } from "./helpers";
import { registerLocalMethods, registerRemoteMethods } from "./rpc";
import { actions, events, IConnection, ISchema } from "./types";
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 = {}): Promise<IConnection> {
return new Promise((resolve, reject) => {
function connect(schema: ISchema = {}, eventHandlers?: EventHandlers): Promise<IConnection> {
return new Promise((resolve) => {
const localMethods = extractMethods(schema);

// on handshake response
function handleHandshakeResponse(event: any) {
async function handleHandshakeResponse(event: any) {
if (event.data.action !== actions.HANDSHAKE_REPLY) return;

// register local methods
Expand All @@ -27,15 +21,24 @@ function connect(schema: ISchema = {}): Promise<IConnection> {
event
);

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);
Expand All @@ -50,18 +53,8 @@ function connect(schema: ISchema = {}): Promise<IConnection> {
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, "*");
});
}

Expand Down
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";
}

/**
Expand Down Expand Up @@ -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;
}
14 changes: 10 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 = {};

Expand Down Expand Up @@ -70,16 +69,23 @@ function connect(guest: HTMLIFrameElement | Worker, schema: ISchema = {}): Promi
listeners.removeEventListener(events.MESSAGE, handleHandshake);
unregisterRemote();
unregisterLocal();
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);
});
}

Expand Down
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
Expand All @@ -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;
Expand All @@ -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)));
}

Expand Down Expand Up @@ -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;
Expand Down
6 changes: 5 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export interface IConnection {
}

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

export interface IEvent extends EventListener {
Expand Down Expand Up @@ -59,3 +59,7 @@ export interface IRPCResolvePayload {
callName: string;
connectionID: string;
}

export interface EventHandlers {
onConnectionSetup: (remote: ISchema) => Promise<void>;
}
1 change: 0 additions & 1 deletion src/typings.d.ts

This file was deleted.

Loading

0 comments on commit 7385075

Please sign in to comment.