Skip to content

Commit 32c707d

Browse files
authored
Merge pull request #4 from web-infra-dev/feat/add-off-listener
feat: add offListener method
2 parents 38898c6 + 5520fd3 commit 32c707d

File tree

4 files changed

+104
-49
lines changed

4 files changed

+104
-49
lines changed

README.md

+62-48
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
Port = f(types, channel)
1818
```
1919

20-
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.
20+
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.
2121

2222
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.
2323

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

44-
4544
## 💡 Features
4645

4746
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:
@@ -50,7 +49,6 @@ Each of these JSContexts exhibits distinct methods of communicating with the ext
5049

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

53-
5452
## 🛠️ Install
5553

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

6664
```ts
67-
import { Unport } from 'unport';
65+
import { Unport } from "unport";
6866

6967
export type Definition = {
7068
parent2child: {
@@ -74,7 +72,7 @@ export type Definition = {
7472
body: {
7573
name: string;
7674
path: string;
77-
}
75+
};
7876
};
7977
child2parent: {
8078
ack: {
@@ -83,52 +81,61 @@ export type Definition = {
8381
};
8482
};
8583

86-
export type ChildPort = Unport<Definition, 'child'>;
87-
export type ParentPort = Unport<Definition, 'parent'>;
84+
export type ChildPort = Unport<Definition, "child">;
85+
export type ParentPort = Unport<Definition, "parent">;
8886
```
8987

9088
2. Parent process implementation:
9189

9290
```ts
9391
// parent.ts
94-
import { join } from 'path';
95-
import { fork } from 'child_process';
96-
import { Unport, ChannelMessage } from 'unport';
97-
import { ParentPort } from './port';
92+
import { join } from "path";
93+
import { fork } from "child_process";
94+
import { Unport, ChannelMessage } from "unport";
95+
import { ParentPort } from "./port";
9896

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

102100
// 2. Implement a Channel based on underlying IPC capabilities
103-
const childProcess = fork(join(__dirname, './child.js'));
101+
const childProcess = fork(join(__dirname, "./child.js"));
104102
parentPort.implementChannel({
105103
send(message) {
106104
childProcess.send(message);
107105
},
108106
accept(pipe) {
109-
childProcess.on('message', (message: ChannelMessage) => {
107+
childProcess.on("message", (message: ChannelMessage) => {
110108
pipe(message);
111109
});
112110
},
113111
});
114112

115113
// 3. You get a complete typed Port with a unified interface 🤩
116-
parentPort.postMessage('syn', { pid: 'parent' });
117-
parentPort.onMessage('ack', payload => {
118-
console.log('[parent] [ack]', payload.pid);
119-
parentPort.postMessage('body', {
120-
name: 'index',
121-
path: ' /',
114+
parentPort.postMessage("syn", { pid: "parent" });
115+
parentPort.onMessage("ack", (payload) => {
116+
console.log("[parent] [ack]", payload.pid);
117+
parentPort.postMessage("body", {
118+
name: "index",
119+
path: " /",
122120
});
123121
});
122+
123+
// 4. If you want to remove some listeners
124+
const handleAck = (payload) => {
125+
console.log("[parent] [syn]");
126+
};
127+
parentPort.onMessage("ack", handleAck);
128+
parentPort.removeMessageListener("ack", handleAck);
129+
// Note: if the second param of `removeMessageListener` is omitted, all listeners will be removed.
130+
parentPort.removeMessageList("ack");
124131
```
125132

126133
3. Child process implementation:
127134

128135
```ts
129136
// child.ts
130-
import { Unport, ChannelMessage } from 'unport';
131-
import { ChildPort } from './port';
137+
import { Unport, ChannelMessage } from "unport";
138+
import { ChildPort } from "./port";
132139

133140
// 1. Initialize a port
134141
const childPort: ChildPort = new Unport();
@@ -139,21 +146,30 @@ childPort.implementChannel({
139146
process.send && process.send(message);
140147
},
141148
accept(pipe) {
142-
process.on('message', (message: ChannelMessage) => {
149+
process.on("message", (message: ChannelMessage) => {
143150
pipe(message);
144151
});
145152
},
146153
});
147154

148155
// 3. You get a complete typed Port with a unified interface 🤩
149-
childPort.onMessage('syn', payload => {
150-
console.log('[child] [syn]', payload.pid);
151-
childPort.postMessage('ack', { pid: 'child' });
156+
childPort.onMessage("syn", (payload) => {
157+
console.log("[child] [syn]", payload.pid);
158+
childPort.postMessage("ack", { pid: "child" });
152159
});
153160

154-
childPort.onMessage('body', payload => {
155-
console.log('[child] [body]', JSON.stringify(payload));
161+
childPort.onMessage("body", (payload) => {
162+
console.log("[child] [body]", JSON.stringify(payload));
156163
});
164+
165+
// 4. If you want to remove some listeners by `removeMessageList`
166+
const handleSyn = (payload) => {
167+
console.log("[child] [syn]");
168+
};
169+
childPort.onMessage("syn", handleSyn);
170+
childPort.removeMessageListener("syn", handleSyn);
171+
// Note: if the second param of `removeMessageListener` is omitted, all listeners will be removed.
172+
childPort.removeMessageList("syn");
157173
```
158174

159175
## 📖 Basic Concepts
@@ -175,7 +191,7 @@ export type Definition = {
175191
body: {
176192
name: string;
177193
path: string;
178-
}
194+
};
179195
};
180196
child2parent: {
181197
ack: {
@@ -207,7 +223,7 @@ parentPort.implementChannel({
207223
childProcess.send(message);
208224
},
209225
accept(pipe) {
210-
childProcess.on('message', (message: ChannelMessage) => {
226+
childProcess.on("message", (message: ChannelMessage) => {
211227
pipe(message);
212228
});
213229
},
@@ -225,7 +241,7 @@ By abstracting the details of the underlying communication mechanism, Unport all
225241
The `Unport` class is used to create a new port.
226242

227243
```ts
228-
import { Unport } from 'unport';
244+
import { Unport } from "unport";
229245
```
230246

231247
#### .implementChannel()
@@ -238,7 +254,7 @@ parentPort.implementChannel({
238254
childProcess.send(message);
239255
},
240256
accept(pipe) {
241-
childProcess.on('message', (message: ChannelMessage) => {
257+
childProcess.on("message", (message: ChannelMessage) => {
242258
pipe(message);
243259
});
244260
},
@@ -250,19 +266,19 @@ parentPort.implementChannel({
250266
This method is used to post a message.
251267

252268
```ts
253-
parentPort.postMessage('syn', { pid: 'parent' });
269+
parentPort.postMessage("syn", { pid: "parent" });
254270
```
255271

256272
#### .onMessage()
257273

258274
This method is used to listen for a message.
259275

260276
```ts
261-
parentPort.onMessage('ack', payload => {
262-
console.log('[parent] [ack]', payload.pid);
263-
parentPort.postMessage('body', {
264-
name: 'index',
265-
path: ' /',
277+
parentPort.onMessage("ack", (payload) => {
278+
console.log("[parent] [ack]", payload.pid);
279+
parentPort.postMessage("body", {
280+
name: "index",
281+
path: " /",
266282
});
267283
});
268284
```
@@ -297,7 +313,7 @@ See our [Web Socket](./examples/web-socket/) example to check more details.
297313
The `ChannelMessage` type is used for the message in the `onMessage` method.
298314

299315
```ts
300-
import { ChannelMessage } from 'unport';
316+
import { ChannelMessage } from "unport";
301317
```
302318

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

326342
```ts
327-
function rpcCall(request: { input: string; }): Promise<{ result: string; }>;
343+
function rpcCall(request: { input: string }): Promise<{ result: string }>;
328344
```
329345

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

355371
// Implementing an RPC method.
356-
parent.implement('getParentInfo', request => ({
357-
id: 'parent',
372+
parent.implement("getParentInfo", (request) => ({
373+
id: "parent",
358374
from: request.user,
359375
}));
360376
```
@@ -364,13 +380,13 @@ The implementation on the `child` side is as follows:
364380
```ts
365381
// "parentPort" is a Port also defined based on Unport.
366382
const child = new Unrpc(childPort);
367-
const response = await child.call('getParentInfo', { user: "child" }); // => { id: "parent", from: "child" }
383+
const response = await child.call("getParentInfo", { user: "child" }); // => { id: "parent", from: "child" }
368384
```
369385

370386
The types are defined as such:
371387

372388
```ts
373-
import { Unport } from 'unport';
389+
import { Unport } from "unport";
374390

375391
export type Definition = {
376392
parent2child: {
@@ -385,15 +401,14 @@ export type Definition = {
385401
};
386402
};
387403

388-
export type ChildPort = Unport<Definition, 'child'>;
389-
export type ParentPort = Unport<Definition, 'parent'>;
404+
export type ChildPort = Unport<Definition, "child">;
405+
export type ParentPort = Unport<Definition, "parent">;
390406
```
391407

392408
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.
393409

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

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

412427
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.
413428

414-
415429
## LICENSE
416430

417431
MIT License © [ULIVZ](https://github.com/ulivz)
@@ -421,4 +435,4 @@ MIT License © [ULIVZ](https://github.com/ulivz)
421435
[ci-badge]: https://github.com/ulivz/unport/actions/workflows/ci.yml/badge.svg?event=push&branch=main
422436
[ci-url]: https://github.com/ulivz/unport/actions/workflows/ci.yml?query=event%3Apush+branch%3Amain
423437
[code-coverage-badge]: https://codecov.io/github/ulivz/unport/branch/main/graph/badge.svg
424-
[code-coverage-url]: https://codecov.io/gh/ulivz/unport
438+
[code-coverage-url]: https://codecov.io/gh/ulivz/unport

__tests__/rpc.test.ts

+23
Original file line numberDiff line numberDiff line change
@@ -149,4 +149,27 @@ describe('Unrpc', () => {
149149
expect(response1).toMatchObject({ user: 'child' });
150150
expect(response2).toMatchObject({ clientKey: 'parent' });
151151
});
152+
153+
it('removeMessageListener - remove specific callback', () => {
154+
const callback = vi.fn();
155+
parent.port.onMessage('getInfo', callback);
156+
parent.port.removeMessageListener('getInfo', callback);
157+
child.port.postMessage('getInfo', {
158+
id: 'child',
159+
});
160+
expect(callback).not.toHaveBeenCalled();
161+
});
162+
163+
it('removeMessageListener - remove all callbacks for an event', () => {
164+
const callback1 = vi.fn();
165+
const callback2 = vi.fn();
166+
parent.port.onMessage('getInfo', callback1);
167+
parent.port.onMessage('getInfo', callback2);
168+
parent.port.removeMessageListener('getInfo');
169+
child.port.postMessage('getInfo', {
170+
id: 'child',
171+
});
172+
expect(callback1).not.toHaveBeenCalled();
173+
expect(callback2).not.toHaveBeenCalled();
174+
});
152175
});

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "unport",
33
"description": "Unport - a Universal Port with strict type inference capability for cross-JSContext communication.",
4-
"version": "0.6.0",
4+
"version": "0.7.0",
55
"main": "lib/index.js",
66
"module": "esm/index.js",
77
"typings": "esm/index.d.ts",

src/index.ts

+18
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,10 @@ interface Port<T extends MessageDefinition, D extends Direction<T>> {
116116
t: U,
117117
handler: Callback<[Payload<T, ReverseDirection<T, D>, U>]>,
118118
): void;
119+
removeMessageListener<U extends keyof T[ReverseDirection<T, D>]>(
120+
t: U,
121+
handler?: Callback<[Payload<T, ReverseDirection<T, D>, U>]>,
122+
): void;
119123
}
120124

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

265+
public removeMessageListener: Port<T, InferDirectionByPort<T, U>>['removeMessageListener'] = (t, handler) => {
266+
if (!this.handlers[t]) {
267+
return;
268+
}
269+
if (handler) {
270+
this.handlers[t] = this.handlers[t].filter(h => h !== handler);
271+
if (this.handlers[t].length === 0) {
272+
delete this.handlers[t];
273+
}
274+
} else {
275+
delete this.handlers[t];
276+
}
277+
};
278+
261279
public destroy() {
262280
this.handlers = {};
263281
this.channel?.destroy && this.channel.destroy();

0 commit comments

Comments
 (0)