Skip to content

Commit

Permalink
Merge pull request #4 from web-infra-dev/feat/add-off-listener
Browse files Browse the repository at this point in the history
feat: add offListener method
  • Loading branch information
sanyuan0704 authored Jul 29, 2024
2 parents 38898c6 + 5520fd3 commit 32c707d
Show file tree
Hide file tree
Showing 4 changed files with 104 additions and 49 deletions.
110 changes: 62 additions & 48 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
Port = f(types, channel)
```

Unport is designed to simplify the complexity revolving around various JSContext environments. These environments encompass a wide range of technologies, including [Node.js](https://nodejs.org/), [ChildProcess](https://nodejs.org/api/child_process.html), [Webview](https://en.wikipedia.org/wiki/WebView), [Web Worker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers), [worker_threads](https://nodejs.org/api/worker_threads.html), [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API), [iframe](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe), [MessageChannel](https://developer.mozilla.org/en-US/docs/Web/API/MessageChannel), [ServiceWorker](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API), and much more.
Unport is designed to simplify the complexity revolving around various JSContext environments. These environments encompass a wide range of technologies, including [Node.js](https://nodejs.org/), [ChildProcess](https://nodejs.org/api/child_process.html), [Webview](https://en.wikipedia.org/wiki/WebView), [Web Worker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers), [worker_threads](https://nodejs.org/api/worker_threads.html), [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API), [iframe](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe), [MessageChannel](https://developer.mozilla.org/en-US/docs/Web/API/MessageChannel), [ServiceWorker](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API), and much more.

Each of these JSContexts exhibits distinct methods of communicating with the external world. Still, the lack of defined types can make handling the code for complex projects an arduous task. In the context of intricate and large-scale projects, it's often challenging to track the message's trajectory and comprehend the fields that the recipient necessitates.

Expand All @@ -41,7 +41,6 @@ Each of these JSContexts exhibits distinct methods of communicating with the ext
- [🤝 Credits](#-credits)
- [LICENSE](#license)


## 💡 Features

1. Provides a unified Port paradigm. You only need to define the message types ([MessageDefinition](#messagedefinition)) and Intermediate communication channel ([Channel](#channel)) that different JSContexts need to pass, and you will get a unified type of Port:
Expand All @@ -50,7 +49,6 @@ Each of these JSContexts exhibits distinct methods of communicating with the ext

![IPC](https://github.com/ulivz/unport/blob/main/.media/ipc.png?raw=true)


## 🛠️ Install

```bash
Expand All @@ -64,7 +62,7 @@ Let's take ChildProcess as an example to implement a process of sending messages
1. Define Message Definition:

```ts
import { Unport } from 'unport';
import { Unport } from "unport";

export type Definition = {
parent2child: {
Expand All @@ -74,7 +72,7 @@ export type Definition = {
body: {
name: string;
path: string;
}
};
};
child2parent: {
ack: {
Expand All @@ -83,52 +81,61 @@ export type Definition = {
};
};

export type ChildPort = Unport<Definition, 'child'>;
export type ParentPort = Unport<Definition, 'parent'>;
export type ChildPort = Unport<Definition, "child">;
export type ParentPort = Unport<Definition, "parent">;
```

2. Parent process implementation:

```ts
// parent.ts
import { join } from 'path';
import { fork } from 'child_process';
import { Unport, ChannelMessage } from 'unport';
import { ParentPort } from './port';
import { join } from "path";
import { fork } from "child_process";
import { Unport, ChannelMessage } from "unport";
import { ParentPort } from "./port";

// 1. Initialize a port
const parentPort: ParentPort = new Unport();

// 2. Implement a Channel based on underlying IPC capabilities
const childProcess = fork(join(__dirname, './child.js'));
const childProcess = fork(join(__dirname, "./child.js"));
parentPort.implementChannel({
send(message) {
childProcess.send(message);
},
accept(pipe) {
childProcess.on('message', (message: ChannelMessage) => {
childProcess.on("message", (message: ChannelMessage) => {
pipe(message);
});
},
});

// 3. You get a complete typed Port with a unified interface 🤩
parentPort.postMessage('syn', { pid: 'parent' });
parentPort.onMessage('ack', payload => {
console.log('[parent] [ack]', payload.pid);
parentPort.postMessage('body', {
name: 'index',
path: ' /',
parentPort.postMessage("syn", { pid: "parent" });
parentPort.onMessage("ack", (payload) => {
console.log("[parent] [ack]", payload.pid);
parentPort.postMessage("body", {
name: "index",
path: " /",
});
});

// 4. If you want to remove some listeners
const handleAck = (payload) => {
console.log("[parent] [syn]");
};
parentPort.onMessage("ack", handleAck);
parentPort.removeMessageListener("ack", handleAck);
// Note: if the second param of `removeMessageListener` is omitted, all listeners will be removed.
parentPort.removeMessageList("ack");
```

3. Child process implementation:

```ts
// child.ts
import { Unport, ChannelMessage } from 'unport';
import { ChildPort } from './port';
import { Unport, ChannelMessage } from "unport";
import { ChildPort } from "./port";

// 1. Initialize a port
const childPort: ChildPort = new Unport();
Expand All @@ -139,21 +146,30 @@ childPort.implementChannel({
process.send && process.send(message);
},
accept(pipe) {
process.on('message', (message: ChannelMessage) => {
process.on("message", (message: ChannelMessage) => {
pipe(message);
});
},
});

// 3. You get a complete typed Port with a unified interface 🤩
childPort.onMessage('syn', payload => {
console.log('[child] [syn]', payload.pid);
childPort.postMessage('ack', { pid: 'child' });
childPort.onMessage("syn", (payload) => {
console.log("[child] [syn]", payload.pid);
childPort.postMessage("ack", { pid: "child" });
});

childPort.onMessage('body', payload => {
console.log('[child] [body]', JSON.stringify(payload));
childPort.onMessage("body", (payload) => {
console.log("[child] [body]", JSON.stringify(payload));
});

// 4. If you want to remove some listeners by `removeMessageList`
const handleSyn = (payload) => {
console.log("[child] [syn]");
};
childPort.onMessage("syn", handleSyn);
childPort.removeMessageListener("syn", handleSyn);
// Note: if the second param of `removeMessageListener` is omitted, all listeners will be removed.
childPort.removeMessageList("syn");
```

## 📖 Basic Concepts
Expand All @@ -175,7 +191,7 @@ export type Definition = {
body: {
name: string;
path: string;
}
};
};
child2parent: {
ack: {
Expand Down Expand Up @@ -207,7 +223,7 @@ parentPort.implementChannel({
childProcess.send(message);
},
accept(pipe) {
childProcess.on('message', (message: ChannelMessage) => {
childProcess.on("message", (message: ChannelMessage) => {
pipe(message);
});
},
Expand All @@ -225,7 +241,7 @@ By abstracting the details of the underlying communication mechanism, Unport all
The `Unport` class is used to create a new port.

```ts
import { Unport } from 'unport';
import { Unport } from "unport";
```

#### .implementChannel()
Expand All @@ -238,7 +254,7 @@ parentPort.implementChannel({
childProcess.send(message);
},
accept(pipe) {
childProcess.on('message', (message: ChannelMessage) => {
childProcess.on("message", (message: ChannelMessage) => {
pipe(message);
});
},
Expand All @@ -250,19 +266,19 @@ parentPort.implementChannel({
This method is used to post a message.

```ts
parentPort.postMessage('syn', { pid: 'parent' });
parentPort.postMessage("syn", { pid: "parent" });
```

#### .onMessage()

This method is used to listen for a message.

```ts
parentPort.onMessage('ack', payload => {
console.log('[parent] [ack]', payload.pid);
parentPort.postMessage('body', {
name: 'index',
path: ' /',
parentPort.onMessage("ack", (payload) => {
console.log("[parent] [ack]", payload.pid);
parentPort.postMessage("body", {
name: "index",
path: " /",
});
});
```
Expand Down Expand Up @@ -297,7 +313,7 @@ See our [Web Socket](./examples/web-socket/) example to check more details.
The `ChannelMessage` type is used for the message in the `onMessage` method.

```ts
import { ChannelMessage } from 'unport';
import { ChannelMessage } from "unport";
```

### Unrpc (Experimental)
Expand All @@ -324,7 +340,7 @@ export type IpcDefinition = {
In the case where an RPC call needs to be encapsulated, the API might look like this:

```ts
function rpcCall(request: { input: string; }): Promise<{ result: string; }>;
function rpcCall(request: { input: string }): Promise<{ result: string }>;
```

Consequently, to associate a callback function, it becomes a requirement to include a `CallbackId` at the **application layer** for every RPC method:
Expand Down Expand Up @@ -353,8 +369,8 @@ Consequently, to associate a callback function, it becomes a requirement to incl
const parent = new Unrpc(parentPort);

// Implementing an RPC method.
parent.implement('getParentInfo', request => ({
id: 'parent',
parent.implement("getParentInfo", (request) => ({
id: "parent",
from: request.user,
}));
```
Expand All @@ -364,13 +380,13 @@ The implementation on the `child` side is as follows:
```ts
// "parentPort" is a Port also defined based on Unport.
const child = new Unrpc(childPort);
const response = await child.call('getParentInfo', { user: "child" }); // => { id: "parent", from: "child" }
const response = await child.call("getParentInfo", { user: "child" }); // => { id: "parent", from: "child" }
```

The types are defined as such:

```ts
import { Unport } from 'unport';
import { Unport } from "unport";

export type Definition = {
parent2child: {
Expand All @@ -385,15 +401,14 @@ export type Definition = {
};
};

export type ChildPort = Unport<Definition, 'child'>;
export type ParentPort = Unport<Definition, 'parent'>;
export type ChildPort = Unport<Definition, "child">;
export type ParentPort = Unport<Definition, "parent">;
```

In comparison to Unport, the only new concept to grasp is that the RPC response message key must end with `__callback`. Other than that, no additional changes are necessary! `Unrpc` also offers comprehensive type inference based on this convention; for instance, you won't be able to implement an RPC method that is meant to serve as a response.

> [!NOTE]
> You can find the full code example here: [child-process-rpc](https://github.com/web-infra-dev/unport/tree/main/examples/child-process-rpc).
>
## 🤝 Contributing

Expand All @@ -411,7 +426,6 @@ Here are some ways you can contribute:

The birth of this project is inseparable from the complex IPC problems we encountered when working in large companies. The previous name of this project was `Multidirectional Typed Port`, and we would like to thank [ahaoboy](https://github.com/ahaoboy) for his previous ideas on this matter.


## LICENSE

MIT License © [ULIVZ](https://github.com/ulivz)
Expand All @@ -421,4 +435,4 @@ MIT License © [ULIVZ](https://github.com/ulivz)
[ci-badge]: https://github.com/ulivz/unport/actions/workflows/ci.yml/badge.svg?event=push&branch=main
[ci-url]: https://github.com/ulivz/unport/actions/workflows/ci.yml?query=event%3Apush+branch%3Amain
[code-coverage-badge]: https://codecov.io/github/ulivz/unport/branch/main/graph/badge.svg
[code-coverage-url]: https://codecov.io/gh/ulivz/unport
[code-coverage-url]: https://codecov.io/gh/ulivz/unport
23 changes: 23 additions & 0 deletions __tests__/rpc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,4 +149,27 @@ describe('Unrpc', () => {
expect(response1).toMatchObject({ user: 'child' });
expect(response2).toMatchObject({ clientKey: 'parent' });
});

it('removeMessageListener - remove specific callback', () => {
const callback = vi.fn();
parent.port.onMessage('getInfo', callback);
parent.port.removeMessageListener('getInfo', callback);
child.port.postMessage('getInfo', {
id: 'child',
});
expect(callback).not.toHaveBeenCalled();
});

it('removeMessageListener - remove all callbacks for an event', () => {
const callback1 = vi.fn();
const callback2 = vi.fn();
parent.port.onMessage('getInfo', callback1);
parent.port.onMessage('getInfo', callback2);
parent.port.removeMessageListener('getInfo');
child.port.postMessage('getInfo', {
id: 'child',
});
expect(callback1).not.toHaveBeenCalled();
expect(callback2).not.toHaveBeenCalled();
});
});
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "unport",
"description": "Unport - a Universal Port with strict type inference capability for cross-JSContext communication.",
"version": "0.6.0",
"version": "0.7.0",
"main": "lib/index.js",
"module": "esm/index.js",
"typings": "esm/index.d.ts",
Expand Down
18 changes: 18 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,10 @@ interface Port<T extends MessageDefinition, D extends Direction<T>> {
t: U,
handler: Callback<[Payload<T, ReverseDirection<T, D>, U>]>,
): void;
removeMessageListener<U extends keyof T[ReverseDirection<T, D>]>(
t: U,
handler?: Callback<[Payload<T, ReverseDirection<T, D>, U>]>,
): void;
}

export type EnsureString<T> = T extends string ? T : never;
Expand Down Expand Up @@ -258,6 +262,20 @@ export class Unport<
this.handlers[t].push(handler);
};

public removeMessageListener: Port<T, InferDirectionByPort<T, U>>['removeMessageListener'] = (t, handler) => {
if (!this.handlers[t]) {
return;
}
if (handler) {
this.handlers[t] = this.handlers[t].filter(h => h !== handler);
if (this.handlers[t].length === 0) {
delete this.handlers[t];
}
} else {
delete this.handlers[t];
}
};

public destroy() {
this.handlers = {};
this.channel?.destroy && this.channel.destroy();
Expand Down

0 comments on commit 32c707d

Please sign in to comment.