Skip to content

Commit

Permalink
Cancel outstanding calls when removing event handler.
Browse files Browse the repository at this point in the history
Change typing to accept web worker as child.
  • Loading branch information
jasny committed Mar 8, 2023
1 parent 5ae9f3f commit 62a3414
Show file tree
Hide file tree
Showing 6 changed files with 63 additions and 15 deletions.
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,19 @@ delete rpc.handler;
```

In Typescript, use `delete (rpc as any).handler`.

## Web worker

The library can also be used with [Web workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers)

```js
const myWorker = new Worker('worker.js');
const rpc = connect(window, myWorker, "*");
```

in `worker.js`

```js
// ...
listener.listen(self, "*");
```
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export {connect} from "./sender";
export {connect, Cancelled} from "./sender";
export {default as Listener} from "./listener";
8 changes: 4 additions & 4 deletions src/listener.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import {CALL_TYPE, RESPONSE_TYPE} from "./constants";
import {RPCRequest, WindowLike} from "./types";
import {MessageTarget, RPCRequest, WindowLike} from "./types";

export default class Listener {
public fallbackSource: WindowLike; // Only for debugging
public fallbackSource: MessageTarget; // Only for testing

constructor(private readonly methods: {[fn: string]: (...args: any[]) => any}) {}

private sendResult(event: MessageEvent<RPCRequest>, result: any): void {
const {channel, id} = event.data;
const source: WindowLike = (event.source as Window) || this.fallbackSource;
const source: MessageTarget = (event.source as MessageTarget) || this.fallbackSource;
const targetOrigin = event.origin && event.origin !== "null" ? event.origin : "*";

Promise.resolve(result).then(r => {
Expand All @@ -18,7 +18,7 @@ export default class Listener {

private sendError(event: MessageEvent<RPCRequest>, error: any): void {
const {channel, id} = event.data;
const source: WindowLike = (event.source as Window) || this.fallbackSource;
const source: MessageTarget = (event.source as MessageTarget) || this.fallbackSource;
const targetOrigin = event.origin && event.origin !== "null" ? event.origin : "*";

source.postMessage({'@rpc': RESPONSE_TYPE, channel, id, error}, targetOrigin);
Expand Down
29 changes: 24 additions & 5 deletions src/sender.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,29 @@
import {CALL_TYPE, RESPONSE_TYPE} from "./constants";
import {RPCResponse, WindowLike} from "./types";
import {MessageTarget, RPCResponse, WindowLike} from "./types";

interface Options {
timeout?: number
}

let currentChannelId = 1;

export class Cancelled extends Error {}

export function connect<T extends {[name: string]: (...args: any) => Promise<any>}>(
parent: WindowLike,
child: WindowLike,
child: MessageTarget,
targetOrigin: string,
options: Options = {},
): T {
let currentId = 1;
let currentId = 0;
const promises = new Map<number, {resolve: (v: any) => void; reject: (reason?: any) => void, timeoutId?: number}>();
const channel = currentChannelId++;

function newId() {
if (currentId < 0) throw new Error("RPC no longer usable. Handler is removed");
return ++currentId;
}

function call(id: number, fn: string, args: Array<any>): void {
child.postMessage({'@rpc': CALL_TYPE, channel, id, fn, args}, targetOrigin);
}
Expand All @@ -34,6 +41,18 @@ export function connect<T extends {[name: string]: (...args: any) => Promise<any
});
}

function destroy() {
parent.removeEventListener("message", handler);

for (const {reject, timeoutId} of promises.values()) {
reject(new Cancelled("Event handler removed"));
parent.clearTimeout(timeoutId);
}
promises.clear();

currentId = -1;
}

const handler = (event: MessageEvent<RPCResponse>) => {
if (
(targetOrigin !== "*" && event.origin !== targetOrigin) ||
Expand All @@ -60,7 +79,7 @@ export function connect<T extends {[name: string]: (...args: any) => Promise<any
return new Proxy({} as T, {
get: function get(_, name: string) {
return function wrapper() {
const id = currentId++;
const id = newId();
const args = Array.prototype.slice.call(arguments);

const promise = waitFor(id, name);
Expand All @@ -71,7 +90,7 @@ export function connect<T extends {[name: string]: (...args: any) => Promise<any
},
deleteProperty(_: T, prop: string | symbol): boolean {
if (prop === 'handler') {
parent.removeEventListener("message", handler);
destroy();
return true;
}
return false;
Expand Down
2 changes: 2 additions & 0 deletions src/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,5 @@ export type WindowLike = Pick<
Window,
"addEventListener" | "removeEventListener" | "postMessage" | "setTimeout" | "clearTimeout"
>

export type MessageTarget = Pick<Window, "postMessage">
21 changes: 16 additions & 5 deletions test/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@ describe("simple-iframe-rpc", () => {

it("gives a timeout when there's no response", async () => {
const child = new JSDOM('').window; // child without listener
const parent = new JSDOM('').window;
const rpc = connect<MathRPC>(parent, child, "*", {timeout: 100});

try {
Expand All @@ -77,19 +76,31 @@ describe("simple-iframe-rpc", () => {
}
});

it("will return the handler to remove the event listener A", async () => {
const rpc = connect<MathRPC>(parent, child, "*", {timeout: 100});
it("will remove the event handler", async () => {
const rpc = connect<MathRPC>(parent, child, "*");

assert.equal(await rpc.add(2, 3), 5);

delete (rpc as any).handler;

// Should time out because there's no handler.
try {
await rpc.add(1, 2);
assert.fail("No error was thrown");
} catch (e) {
assert.equal(e.message, 'No response for RCP call \'add\'');
assert.equal(e.message, 'RPC no longer usable. Handler is removed');
}
});

it("will cancel when removing the event handler", async () => {
const child = new JSDOM('').window; // child without listener
const rpc = connect<MathRPC>(parent, child, "*", {timeout: 10000});

const promise = rpc.add(2, 3)
.then(() => assert.fail("No error was thrown"))
.catch(reason => assert.equal(reason.message, "Event handler removed"))

delete (rpc as any).handler;

await promise;
});
});

0 comments on commit 62a3414

Please sign in to comment.