├── .babelrc ├── .gitignore ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── README.md ├── base ├── buffer-utils.ts ├── buffer.ts ├── cancelablePromise │ ├── cancelablePromise.ts │ └── timeout.ts ├── disposable │ ├── disposable.ts │ └── disposableStore.ts ├── errors.ts ├── event.ts ├── interface.ts ├── iterator.ts ├── leakageMonitor.ts ├── linkedList.ts └── utils.ts ├── core ├── common │ ├── ipc.electron.ts │ └── ipc.ts ├── electron-main │ └── ipc.electron-main.ts └── electron-render │ └── IPCClient.ts ├── nodemon.json ├── package.json ├── src ├── main.ts ├── render │ └── index.html ├── windowService.ts └── windowServiceIpc.ts ├── tsconfig.json ├── yarn-error.log └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "useBuiltIns": "entry" 7 | } 8 | ] 9 | ], 10 | "plugins": [ 11 | "@babel/plugin-syntax-dynamic-import", 12 | "@babel/plugin-transform-runtime" 13 | ] 14 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.js 3 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "jpoissonnier.vscode-styled-components", 4 | "kumar-harsh.graphql-for-vscode", 5 | "dtsvet.vscode-wasm", 6 | "cpylua.language-postcss", 7 | "EditorConfig.editorconfig", 8 | "dbaeumer.vscode-eslint", 9 | "drKnoxy.eslint-disable-snippets", 10 | "mkaufman.htmlhint", 11 | "streetsidesoftware.code-spell-checker", 12 | "msjsdiag.debugger-for-chrome", 13 | "ms-vscode.node-debug2", 14 | "codezombiech.gitignore", 15 | "aaron-bond.better-comments", 16 | "ziyasal.vscode-open-in-github", 17 | "jasonnutter.search-node-modules", 18 | "jock.svg", 19 | "andrejunges.handlebars", 20 | "bungcip.better-toml", 21 | "mikestead.dotenv", 22 | "gruntfuggly.todo-tree", 23 | "vscode-icons-team.vscode-icons" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | // not ok now!!!! 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "type": "node", 10 | "request": "launch", 11 | "name": "Launch Electron", 12 | "program": "${workspaceFolder}/electron/node_modules/.bin/electron", 13 | "args": [ 14 | "${workspaceFolder}/electron/index.js" 15 | ], 16 | "skipFiles": [ 17 | "/**" 18 | ] 19 | }, 20 | { 21 | "type": "node", 22 | "request": "launch", 23 | "name": "Launch Program", 24 | "program": "${workspaceFolder}/../../node_modules/@jupiter-cli/app-tools/bin/index.js", 25 | "args": [ 26 | "dev" 27 | ], 28 | "skipFiles": [ 29 | "/**" 30 | ] 31 | } 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | ".code-workspace": "jsonc", 4 | ".babelrc": "json", 5 | ".eslintrc": "jsonc", 6 | ".eslintrc*.json": "jsonc", 7 | ".stylelintrc": "jsonc", 8 | "stylelintrc": "jsonc", 9 | ".htmlhintrc": "jsonc", 10 | "htmlhintrc": "jsonc", 11 | "Procfile*": "shellscript", 12 | "README": "markdown", 13 | }, 14 | "search.useIgnoreFiles": true, 15 | "files.exclude": { 16 | "**.js": { 17 | "when": "$(basename).ts" 18 | }, 19 | "**/adapters/index.ts": true 20 | }, 21 | "search.exclude": { 22 | "**/build": true, 23 | "**/output": true, 24 | "**/dist": true, 25 | "**/yarn.lock": true, 26 | "**/package-lock.json": true, 27 | "**/*.log": true, 28 | "**/*.pid": true, 29 | "**/.git": true, 30 | "**/node_modules": true, 31 | "**/bower_components": true 32 | }, 33 | "editor.rulers": [ 34 | 80, 35 | 120 36 | ], 37 | "files.eol": "\n", 38 | "files.trimTrailingWhitespace": true, 39 | "files.insertFinalNewline": true, 40 | "todo-tree.general.tags": [ 41 | "TODO:", 42 | "FIXME:" 43 | ], 44 | "todo-tree.highlights.defaultHighlight": { 45 | "gutterIcon": true 46 | }, 47 | "todo-tree.highlights.customHighlight": { 48 | "TODO:": { 49 | "foreground": "#fff", 50 | "background": "#ffbd2a", 51 | "iconColour": "#ffbd2a" 52 | }, 53 | "FIXME:": { 54 | "foreground": "#fff", 55 | "background": "#f06292", 56 | "icon": "flame", 57 | "iconColour": "#f06292" 58 | } 59 | }, 60 | "cSpell.diagnosticLevel": "Hint", 61 | "eslint.alwaysShowStatus": true, 62 | "eslint.nodePath": "./node_modules", 63 | "eslint.enable": true, 64 | "eslint.run": "onType", 65 | "eslint.options": { 66 | "rules": { 67 | "no-debugger": "off" 68 | } 69 | }, 70 | "eslint.probe": [ 71 | "javascript", 72 | "javascriptreact", 73 | "typescript", 74 | "typescriptreact", 75 | "vue" 76 | ], 77 | "eslint.format.enable": true, 78 | "eslint.lintTask.enable": true, 79 | "javascript.validate.enable": false, 80 | "typescript.validate.enable": true, 81 | "flow.enabled": false, 82 | "stylelint.enable": true, 83 | "css.validate": false, 84 | "scss.validate": false, 85 | "less.validate": false, 86 | "htmlhint.enable": true, 87 | "prettier.disableLanguages": [ 88 | "javascript", 89 | "javascriptreact", 90 | "typescript", 91 | "typescriptreact", 92 | "jsonc", 93 | "json" 94 | ], 95 | "prettier.trailingComma": "all", 96 | "prettier.printWidth": 80, 97 | "prettier.semi": true, 98 | "prettier.arrowParens": "avoid", 99 | "prettier.bracketSpacing": true, 100 | "prettier.jsxBracketSameLine": true, 101 | "editor.codeActionsOnSave": { 102 | "source.fixAll.eslint": true 103 | }, 104 | "editor.codeActionsOnSaveTimeout": 5000, 105 | "javascript.format.enable": false, 106 | "typescript.format.enable": false, 107 | "json.format.enable": true, 108 | "[json]": { 109 | "editor.tabSize": 2, 110 | "editor.formatOnType": true, 111 | "editor.formatOnPaste": true, 112 | "editor.formatOnSave": true 113 | }, 114 | "[jsonc]": { 115 | "editor.tabSize": 2, 116 | "editor.formatOnType": true, 117 | "editor.formatOnPaste": true, 118 | "editor.formatOnSave": true 119 | }, 120 | "emmet.triggerExpansionOnTab": true, 121 | "typescript.tsdk": "node_modules/typescript/lib" 122 | } 123 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 启动 2 | 3 | ```bash 4 | git clone git@github.com:spcBackToLife/jupiter-electron-ipc-demo.git 5 | cd jupiter-electron-ipc-demo 6 | yarn install 7 | yarn dev 8 | ``` 9 | 10 | ## 简介 11 | 此项目是将 vscode 中的 ipc 通信机制完整的实现了一遍,大家可以看如上的启动方式,进行启动和体验。 12 | 13 | ## 服务使用示例 14 | 15 | [创建一个windowSercice] 16 | 17 | ```ts 18 | export class WindowService { 19 | 20 | doSomething(): string { 21 | console.log('do something and return done') 22 | return 'done'; 23 | } 24 | } 25 | 26 | ``` 27 | 28 | [创建一个服务的频道] 29 | 30 | ```ts 31 | import { IServerChannel } from "../core/common/ipc"; 32 | import { WindowService } from "./windowService"; 33 | import { Event } from '../base/event'; 34 | 35 | export class WindowChannel implements IServerChannel { 36 | constructor( 37 | public readonly windowService: WindowService, 38 | ) {} 39 | 40 | listen(_: unknown, event: string): Event { 41 | // 暂时不支持 42 | throw new Error(`Not support listen event currently: ${event}`); 43 | } 44 | 45 | call(_: unknown, command: string, arg?: any): Promise { 46 | switch (command) { 47 | case 'doSomething': 48 | return Promise.resolve(this.windowService.doSomething()); 49 | 50 | default: 51 | return Promise.reject('无可调用服务!'); 52 | } 53 | } 54 | } 55 | 56 | ``` 57 | 58 | [渲染进程-src/render/index.html] 59 | 60 | ```ts 61 | 62 |
jupiter electron
63 | 69 | 70 | 71 | ``` 72 | 73 | [主进程-src/main.ts] 74 | 75 | ```ts 76 | import { app, BrowserWindow } from 'electron'; 77 | import path from 'path'; 78 | import { Server as ElectronIPCServer } from '../core/electron-main/ipc.electron-main'; 79 | import { WindowChannel } from './windowServiceIpc'; 80 | import { WindowService } from './windowService'; 81 | 82 | app.on('ready', () => { 83 | const electronIpcServer = new ElectronIPCServer(); 84 | electronIpcServer.registerChannel('windowManager', new WindowChannel(new WindowService())) 85 | 86 | 87 | const win = new BrowserWindow({ 88 | width: 1000, 89 | height: 800, 90 | webPreferences: { 91 | nodeIntegration: true 92 | } 93 | }); 94 | 95 | console.log('render index html:', path.join(__dirname, 'render', 'index.html')); 96 | win.loadFile(path.join(__dirname, 'render', 'index.html')); 97 | }) 98 | 99 | ``` 100 | 101 | 启动并运行一波: 102 | 103 | ```ts 104 | "scripts": { 105 | ... 106 | "dev": "tsc && electron ./src/main.js", 107 | ... 108 | }, 109 | ``` 110 | 111 | 启动: 112 | 113 | ```ts 114 | yarn dev 115 | ``` 116 | 117 | ![image](https://user-images.githubusercontent.com/20703494/99907036-81ca1600-2d15-11eb-9bf0-e8aa12db3795.png) 118 | 119 | 至此,我们实现了 vscode 的 ipc 机制,大家可以前往这里进行体验: 120 | 121 | [jupiter-electron-ipc-demo](https://github.com/spcBackToLife/jupiter-electron-ipc-demo/) 122 | 123 | 124 | 125 | ## 模式介绍 126 | **你也可以在你的 Electron 中使用 Vscode 的通信机制:从零到一实现 Vscode 通信机制** 127 | 128 | 129 | 130 | `Electron`是多进程的,因此,在写应用的时候少不了的就是进程间的通信,尤其是主进程与渲染进程之间的通信。但是如果不好好设计通信机制,那么,在应用里的通信就会混乱不堪,无法管理,开发 `Electron` 的同学可能深有体会。 131 | 132 | 我们举个例子,来看在 `传统 Electron`中和`Jupiter Electron`中通信的样子: 133 | 134 | 135 | ### 示例: 窗口发消息给主进程,做一件事情,并返回一个结果:“完成”。 136 | 137 | 在 `Electron` 中,我们可能需要这么做: 138 | 139 | 【主进程】 140 | 141 | ```typescript 142 | 143 | // 做事情 144 | const doSomething = (...params) => { 145 | console.log('do some sync things'); 146 | return Promise.resolve('完成'); 147 | } 148 | app.on('ready', () => { 149 | const win = new BrowserWindow({}); 150 | ipcMain.on('dosomething', async (e, message) => { 151 | const result = await doSomething(message.params); 152 | // 返回结果(渲染进程需要传递请求id过来确保频道通信的唯一性) 153 | win.webContents.send(`dosomething_${message.requestId}`, result); 154 | }) 155 | }) 156 | ``` 157 | 158 | 【渲染进程】 159 | ```typescript 160 | const doSomething = () => { 161 | return new Promise((resolve, reject) => { 162 | const requestId = new Date().getTime(); 163 | // 注意只监听一次返回就好了 164 | ipcRenderer.once(`dosomething_${requestId}`, (result) => { 165 | console.log('result:', result); 166 | resolve(result); 167 | }) 168 | // 发送消息-dosomething 169 | ipcRenderer.send('dosomething', { 170 | requestId, 171 | params: {...} 172 | }) 173 | }) 174 | } 175 | ... 176 | doSomething(); 177 | ``` 178 | > 很明显的一个问题是,如果主进程执行失败了,还得把失败信息返回回来,做下处理。 179 | 180 | 上述传统的 Electron 通信写法其实还有不少问题,这里就不再补充了。 181 | 182 | 我们在 `Jupiter Electron` 中是怎么样的呢? 183 | 184 | 【主进程】 185 | ```typescript 186 | const doSomething = (params) => { 187 | console.log('do something'); 188 | return Promise.resolve('完成'); 189 | } 190 | ``` 191 | 192 | 【渲染进程】 193 | 194 | ```typescript 195 | import bridge from '@jupiter-electron/runtime'; 196 | 197 | export const doAthing = () => { 198 | return bridge.call('doSomething', params) 199 | .catch((err) => console.log('main exec error:', err)); 200 | } 201 | 202 | doAthing(); 203 | ``` 204 | 205 | > - 不用担心唯一性,内部机制已处理。 206 | > - 不用担心失败异常怎么处理返回给前端,内部机制会把错误返回给渲染进程。 207 | > - 通信压缩优化?不用担心,已处理。 208 | 209 | 可以发现,其实在 `Jupiter Electron`中,通信,则是一件非常简单的事情,主进程、渲染进程通信;窗口间通信。 210 | 211 | 那这样的机制是怎么实现的呢,背后设计是什么? 212 | 213 | 其实,通信机制是基于 Vsocde 源码的机制抽象出来的,下面,就对背后的设计机制进行解读,带着大家一起来实现一套 IPC 通信机制。 214 | 215 | ### 设计通信机制目标是什么?进程通信用什么? 216 | 首先,进程通信,我们肯定还是用 `Electron` 中的 `webContents.send`、`ipcRender.send`、`ipcMain.on`,我们设计通信机制的目标是: 217 | - 简化我们的通信流程 218 | - 保证通信的稳定性 219 | - 提高通信的质量 220 | - 提高通信效率,降低资源使用 221 | 222 | 设计通信机制,就不得不说,我们在`Electron`中的通信是为了什么,有什么特征。 223 | 224 | ### 通信机制设计 225 | 226 | 由于`Electron`多进程特征,有时候做一件事情,就不得不需要多进程协作,因此,我们需要通信。 227 | 228 | 一般,我们会有这些特征场景: 229 | 230 | - 「渲染进程」期望「主进程」做一件事情,并返回执行结果。 231 | - 「渲染进程」通知「主进程」一个消息,不需要返回结果。 232 | - 「主进程」期望「渲染进程」做一件事情,并返回执行结果。 233 | - 「渲染进程」期望「渲染进程」做一件事情,并返回执行结果。 234 | - 「渲染进程」会监听来自于「主进程」的消息。 235 | 236 | 总体来说,基于上述特征,我们总结成:**服务化**,这也是我提出的在`Electron`开发与`Web`的差异的另一个特点。 237 | 238 | **服务化** 的含义即:提供服务 239 | - 当你的「渲染进程」期望「主进程」做一件事情,并返回执行结果时,「主进程」需要做的事情,既可以抽象成对应的服务,主进程负责提供服务。 240 | - 当你的「主进程」期望「渲染进程」做一件事情,并返回执行结果时,则「渲染进程」需要做的事情,则可以抽象成对应的服务,渲染进程负责提供服务。 241 | 242 | 因此,我们可以设计成如下形式: 243 | 244 | ![image](https://user-images.githubusercontent.com/20703494/99906970-0ff1cc80-2d15-11eb-8101-7ea62690e7dc.png) 245 | 246 | > 服务端可以是「渲染进程」、也可以是「主进程」,取决于谁提供服务给谁。 247 | 248 | 可以看到,服务端提供 n 个服务供客户端访问。但这样有一个问题,这里,服务端提供的服务,是所有客户端都能访问的,就会有问题,就好比:支付宝为所有用户提供了基础服务,比如:电费、税费、社保查询服务,但有些服务可能只有特定人群能访问,比如:优选100% 赚钱的基金服务。 249 | 250 | 因此,我们这样的设计就无法满足这个要求,因此我们需要做一个调整: 251 | 252 | ![image](https://user-images.githubusercontent.com/20703494/99907010-4d565a00-2d15-11eb-8b0e-6bcf17b83c39.png) 253 | 254 | 我们增加了 频道服务的概念,每个客户端基于频道来访问服务,比如: 255 | - 客户端1 访问了 频道服务1,客服端2 访问 频道服务2 256 | - 频道服务1和2都有通用的服务,也有自己的特权服务。 257 | - 客服端访问服务的时候,会为每个客户,生成一个频道,来给他提供他具有的服务 258 | - 在为客服创建对应的频道服务的时候,会将服务端通用服务注册到频道中,也会根据用户的特点,注册其特有的服务。 259 | 260 | 通过以上模式,解决了上述问题,但又带来了一些问题: 261 | 262 | 用户每一次访问服务的时候,都去新建一个频道服务吗? 263 | 264 | 按照上述逻辑,的确会是这样,因此,为了解决这个问题,我们需要如下设计: 265 | 266 | ![image](https://user-images.githubusercontent.com/20703494/99907005-44658880-2d15-11eb-9be9-3d87aa715dff.png) 267 | 268 | 269 | 我们在服务端,新加一个概念,叫连接(Connection),客户端初始化的时候,可以发起通信连接,此时就会去新建一个频道服务,并存储在服务端,客户端下一次发起服务访问请求的时候,直接去获取频道服务,去那相应的服务进行执行。 270 | 271 | 这样的方案,看起来完美了,但还有一个问题,上述方案,我们可以适用如下场景: 272 | - 「渲染进程」期望「主进程」做一件事情,并返回执行结果。 273 | 那如果是这样的设计,又如何去满足: 274 | - 「主进程」期望「渲染进程」做一件事情,并返回执行结果。 275 | 276 | 这好像是「服务端」和「客户端」互换了身份。如何让这两种情况同时存在呢? 277 | 278 | 我们可以做如下设计: 279 | ![image](https://user-images.githubusercontent.com/20703494/99907014-56472b80-2d15-11eb-8425-57de6d12fe39.png) 280 | 281 | 我们在服务端连接上增加「频道客户端」,在客户端增加「频道服务」,从而客户端可访问服务端频道服务中的服务,在服务端,也可以调用客户端里频道服务的服务,从而实现上述问题。 282 | 283 | 到此,我们对 vscode 整个通信机制的设计解析基本完成,接下来就是具体实现,当然,在实现的过程中,我们还需要去考虑: 284 | - 通信消息的 Buffer 处理 285 | - 通信时执行异常处理 286 | - 通信的唯一性保证 287 | 288 | 289 | ### 通信机制实现 290 | 291 | 292 | 上面在表述中,我们有提到服务,也有提到频道, 我们在这里统一概念: 293 | - 一个频道提供一个服务,服务即频道 294 | 后续统一使用「频道」来表示一个「服务」 295 | 296 | 我们来梳理下,在上述流程中,我们出现的一些概念。 297 | 298 | - 服务端 -> IPCServer 299 | - 即图中最外层的服务端,用于管理所有连接的 300 | - 客户端 -> IPCClient 301 | - 即图中最外层的客户端,用于建立连接,统一收发消息、处理消息 302 | - 连接 -> Connection 303 | - 频道服务端 -> ChannelServer 304 | - 提供服务的一端 305 | - 频道客户端 -> ChannelClient 306 | - 「频道客户端」即访问服务端某个频道(服务)的一端 307 | - 服务端频道 -> ServerChannel 308 | - 频道服务端注册的频道(服务)即「服务端频道」 309 | 310 | 当然,在实现 vscode 通信机制之前,其实还有不少必修课,但这些会在后续为大家一一讲解,现在可以理解他们的作用,并拿来使用即可, **不影响对 IPC机制的使用与理解**。 311 | 312 | 我们在开始 vscode 通信设计之前,需要为大家提供一些基础工具类: 313 | 314 | ``` 315 | - 「cancelablePromise/」 可取消的 promise 316 | - 「disposable/」 监听资源释放基类 317 | - 「buffer、buffer-utils」 对消息的 buffer 处理 318 | - 「events」 是 vscode 自己实现的事件类、也是一个对事件做装饰的类、很赞的!! 319 | - 「iterator」 迭代器,用于 linkedlist 数据结构迭代 320 | - 「linkedlist」 js 的双链数据结构实现 321 | - 「utils」 辅助工具类 322 | ``` 323 | 324 | 以下会按照如下顺序一一实现: 325 | - 消息通信协议设计与实现 326 | - 服务端频道 -> ServerChannel 327 | - 频道服务端 -> ChannelServer 328 | - 频道客户端 -> ChannelClient 329 | - 连接 -> Connection 330 | - 客户端 -> IPCClient 331 | - 服务端 -> IPCServer 332 | 333 | 334 | 335 | #### 消息通信协议设计与实现 336 | 337 | 在上述图中,我们有描述到一个「客户端」和「服务端」会建立一个「连接」,并有一个「频道服务端」。在「客户端」中,如何去访问「频道服务端」呢?这里就需要定义一下访问协议: 338 | 339 | 我们规定: 340 | - 「客户端」初始化的时候会发送:ipc:hello 频道消息,和「服务端」建立连接。 341 | ```ts 342 | // 类似于这样的含义, 不是实现代码,只是示意 343 | class IPCClient { 344 | constructor() { 345 | ipcRenderer.send('ipc:hello'); 346 | } 347 | } 348 | ``` 349 | - 「客户端」和服务端的消息传递,统一在 ipc:message 频道进行消息接收和发送,即: 350 | ``` 351 | xxx.webContents.send('ipc:message', message); 352 | ... 353 | ipcRenderer.send('ipc:message', message); 354 | ``` 355 | - 当「客户端」被卸载的时候(比如窗口关闭),发送断开连接消息:ipc:disconnect ,进行销毁所有的消息监听。 356 | 357 | 因此,我们设计如下协议: 358 | 359 | [core/common/ipc.electron.ts] 360 | ```ts 361 | import { Event } from '../../base/event'; // 工具类 362 | import { VSBuffer } from '../../base/buffer'; // 工具类 363 | 364 | export interface IMessagePassingProtocol { 365 | onMessage: Event; 366 | send(buffer: VSBuffer): void; 367 | } 368 | 369 | export interface Sender { 370 | send(channel: string, msg: Buffer | null): void; 371 | } 372 | 373 | export class Protocol implements IMessagePassingProtocol { 374 | constructor( 375 | private readonly sender: Sender, 376 | readonly onMessage: Event, 377 | ) {} 378 | 379 | send(message: VSBuffer): void { 380 | try { 381 | this.sender.send('ipc:message', message.buffer); 382 | } catch (e) { 383 | // systems are going down 384 | } 385 | } 386 | 387 | dispose(): void { 388 | this.sender.send('ipc:disconnect', null); 389 | } 390 | } 391 | 392 | ``` 393 | 394 | 使用示意: 395 | 396 | ```ts 397 | ... 398 | const protocol = new Protocol(webContents, onMessage); 399 | .. 400 | const protocol = new Protocol(ipcRenderer, onMessage); 401 | ``` 402 | 403 | #### 定义服务端频道:IServerChannel 404 | 405 | 「服务端频道」即在「服务端」的「频道服务」中注册的「频道」(服务)。 406 | 407 | [core/common/ipc.ts] 408 | ```ts 409 | export interface IServerChannel { 410 | call( 411 | ctx: TContext, 412 | command: string, 413 | arg?: any, 414 | cancellationToken?: CancellationToken, 415 | ): Promise; // 发起服务请求 416 | listen(ctx: TContext, event: string, arg?: any): Event;// 监听消息 417 | } 418 | ``` 419 | > 具体实现,是在实际使用的时候才会去定义服务,因此会在后续完成 IPC 机制后,进行使用用例的时候再介绍「服务端频道」如何定义与使用。 420 | 421 | #### 定义频道的服务端 422 | 前面有讲解到,「客户端」访问服务前,会和「服务端」建立一个「连接」,在「连接」中,存在一个「频道服务端」管理着服可提供给「客户端」访问的「服务频道」。 423 | 424 | 首先,我们定义一下「服务频道」接口: 425 | 426 | [core/common/ipc.ts] 427 | ```ts 428 | export interface IChannelServer { 429 | registerChannel(channelName: string, channel: IServerChannel): void; 430 | } 431 | ``` 432 | - 主要有一个注册频道的方法 433 | 434 | 接着,我们实现一个「频道服务」 435 | ```ts 436 | export class ChannelServer 437 | implements IChannelServer, IDisposable { 438 | // 保存客户端可以访问的频道信息 439 | private readonly channels = new Map>(); 440 | 441 | // 消息通信协议监听 442 | private protocolListener: IDisposable | null; 443 | 444 | // 保存活跃的请求,在收到取消消息后,进行取消执行,释放资源 445 | private readonly activeRequests = new Map(); 446 | 447 | // 在频道服务器注册之前,可能会到来很多请求,此时他们会停留在这个队列里 448 | // 如果 timeoutDelay 过时后,则会移除 449 | // 如果频道注册完成,则会从此队列里拿出并执行 450 | private readonly pendingRequests = new Map(); 451 | 452 | constructor( 453 | private readonly protocol: IMessagePassingProtocol, // 消息协议 454 | private readonly ctx: TContext, // 服务名 455 | private readonly timeoutDelay: number = 1000, // 通信超时时间 456 | ) { 457 | // 接收 ChannelClient 的消息 458 | this.protocolListener = this.protocol.onMessage(msg => 459 | this.onRawMessage(msg), 460 | ); 461 | // 当我们频道服务端实例化完成时,我们需要给频道客服端返回实例化完成的消息: 462 | this.sendResponse({ type: ResponseType.Initialize }); 463 | } 464 | 465 | public dispose(): void { ... } 466 | public registerChannel( 467 | channelName: string, channel: IServerChannel): void { 468 | ... 469 | } 470 | private onRawMessage(message: VSBuffer): void { ... } 471 | private disposeActiveRequest(request: IRawRequest): void { ... } 472 | private flushPendingRequests(channelName: string): void { ... } 473 | private sendResponse(response: IRawResponse): void { ... } 474 | private send(header: any, body: any = undefined): void { ... } 475 | private sendBuffer(message: VSBuffer): void { ... } 476 | private onPromise(request: IRawPromiseRequest): void { ... } 477 | private collectPendingRequest(request: IRawPromiseRequest): void { 478 | ... 479 | } 480 | 481 | ``` 482 | 我们来解释一个重要的集合:`activeRequests` 483 | ```ts 484 | private readonly activeRequests = new Map(); 485 | ``` 486 | 这个 Map 存储着活跃的「服务请求」,即仍然在执行中的请求,如果客户端销毁(比如窗口关闭),则会进行统一的释放,即:dispose 487 | 488 | ```ts 489 | public dispose(): void { 490 | if (this.protocolListener) { 491 | this.protocolListener.dispose(); 492 | this.protocolListener = null; 493 | } 494 | this.activeRequests.forEach(d => d.dispose()); 495 | this.activeRequests.clear(); 496 | } 497 | ``` 498 | 499 | > 释放的时候,除了 activeRequests,也还包括建立了连接的协议监听释放:protocolListener 500 | 501 | 接着,我们来实现「注册频道」方法 502 | 503 | ```ts 504 | registerChannel( 505 | channelName: string, channel: IServerChannel): void { 506 | 507 | // 保存频道 508 | this.channels.set(channelName, channel); 509 | 510 | // 如果频道还未注册好之前就来了很多请求,则在此时进行请求执行。 511 | // https://github.com/microsoft/vscode/issues/72531 512 | setTimeout(() => this.flushPendingRequests(channelName), 0); 513 | } 514 | 515 | private flushPendingRequests(channelName: string): void { 516 | const requests = this.pendingRequests.get(channelName); 517 | 518 | if (requests) { 519 | for (const request of requests) { 520 | clearTimeout(request.timeoutTimer); 521 | 522 | switch (request.request.type) { 523 | case RequestType.Promise: 524 | this.onPromise(request.request); 525 | break; 526 | default: 527 | break; 528 | } 529 | } 530 | 531 | this.pendingRequests.delete(channelName); 532 | } 533 | } 534 | ``` 535 | 536 | 接下来,我们再来实现 `onRawMessage`: 537 | 538 | `onRawMessage`用于处理`Buffer`消息。 539 | 540 | [core/common/ipc.ts] 541 | ```ts 542 | private onRawMessage(message: VSBuffer): void { 543 | // 解读 Buffer 消息 544 | const reader = new BufferReader(message); 545 | // 解读消息头: 546 | // [ 547 | // type, 消息类型 548 | // id, 消息 id 549 | // channelName, 频道名 550 | // name 服务方法名 551 | // ] 552 | // deserialize 为工具方法,解读 Buffer 553 | const header = deserialize(reader); 554 | // 解读消息体,即执行服务方法的参数 555 | const body = deserialize(reader); 556 | const type = header[0] as RequestType; 557 | 558 | // 返回执行结果 559 | switch (type) { 560 | case RequestType.Promise: 561 | // 562 | return this.onPromise({ 563 | type, 564 | id: header[1], 565 | channelName: header[2], 566 | name: header[3], 567 | arg: body, 568 | }); 569 | case RequestType.PromiseCancel: 570 | return this.disposeActiveRequest({ type, id: header[1] }); 571 | default: 572 | break; 573 | } 574 | } 575 | ``` 576 | 其中,`onPromise` 为开始访问具体的服务,执行服务方法,并返回结果给「客户端」: 577 | 578 | 579 | [core/common/ipc.ts] 580 | ```ts 581 | private onPromise(request: IRawPromiseRequest): void { 582 | const channel = this.channels.get(request.channelName); 583 | // 如果频道不存在,则放入 PendingRequest,等待频道注册后执行或者过期后清理。 584 | if (!channel) { 585 | this.collectPendingRequest(request); 586 | return; 587 | } 588 | 589 | // 取消请求 token -> 机制见 可取消的 Promise 部分内容讲解 590 | const cancellationTokenSource = new CancellationTokenSource(); 591 | let promise: Promise; 592 | try { 593 | // 调用频道 call 执行具体服务方法 594 | promise = channel.call( 595 | this.ctx, 596 | request.name, 597 | request.arg, 598 | cancellationTokenSource.token, 599 | ); 600 | } catch (err) { 601 | promise = Promise.reject(err); 602 | } 603 | 604 | const { id } = request; 605 | 606 | promise.then( 607 | data => { 608 | // 执行完成,返回执行结果 609 | this.sendResponse({ 610 | id, 611 | data, 612 | type: ResponseType.PromiseSuccess, 613 | }); 614 | // 从活跃的请求中清理该请求 615 | this.activeRequests.delete(request.id); 616 | }, 617 | err => { 618 | // 如果有异常,进行消息的异常处理,并返回响应结果。 619 | if (err instanceof Error) { 620 | this.sendResponse({ 621 | id, 622 | data: { 623 | message: err.message, 624 | name: err.name, 625 | stack: err.stack 626 | ? err.stack.split 627 | ? err.stack.split('\n') 628 | : err.stack 629 | : undefined, 630 | }, 631 | type: ResponseType.PromiseError, 632 | }); 633 | } else { 634 | this.sendResponse({ 635 | id, 636 | data: err, 637 | type: ResponseType.PromiseErrorObj, 638 | }); 639 | } 640 | 641 | this.activeRequests.delete(request.id); 642 | }, 643 | ); 644 | // 将请求存储到活跃请求,并提供可以释放的令牌。 645 | const disposable = toDisposable(() => cancellationTokenSource.cancel()); 646 | this.activeRequests.set(request.id, disposable); 647 | } 648 | ``` 649 | 650 | `sendResponse` 会根据执行结果,返回具体类型的消息; 651 | `send` 则是将消息进行 buffer 序列化 652 | `sendBuffer` 发送消息给「客户端」 653 | 654 | [core/common/ipc.ts] 655 | 656 | ```ts 657 | export enum ResponseType { 658 | Initialize = 200, // 初始化消息返回 659 | PromiseSuccess = 201, // promise 成功 660 | PromiseError = 202, // promise 失败 661 | PromiseErrorObj = 203, 662 | EventFire = 204, 663 | } 664 | 665 | type IRawInitializeResponse = { type: ResponseType.Initialize }; 666 | type IRawPromiseSuccessResponse = { 667 | type: ResponseType.PromiseSuccess; // 类型 668 | id: number; // 请求 id 669 | data: any; // 数据 670 | }; 671 | type IRawPromiseErrorResponse = { 672 | type: ResponseType.PromiseError; 673 | id: number; 674 | data: { message: string; name: string; stack: string[] | undefined }; 675 | }; 676 | type IRawPromiseErrorObjResponse = { 677 | type: ResponseType.PromiseErrorObj; 678 | id: number; 679 | data: any; 680 | }; 681 | 682 | type IRawResponse = 683 | | IRawInitializeResponse 684 | | IRawPromiseSuccessResponse 685 | | IRawPromiseErrorResponse 686 | | IRawPromiseErrorObjResponse; 687 | 688 | private sendResponse(response: IRawResponse): void { 689 | switch (response.type) { 690 | case ResponseType.Initialize: 691 | return this.send([response.type]); 692 | 693 | case ResponseType.PromiseSuccess: 694 | case ResponseType.PromiseError: 695 | case ResponseType.EventFire: 696 | case ResponseType.PromiseErrorObj: 697 | return this.send([response.type, response.id], response.data); 698 | default: 699 | break; 700 | } 701 | } 702 | 703 | 704 | private send(header: any, body: any = undefined): void { 705 | const writer = new BufferWriter(); 706 | serialize(writer, header); 707 | serialize(writer, body); 708 | this.sendBuffer(writer.buffer); 709 | } 710 | 711 | private sendBuffer(message: VSBuffer): void { 712 | try { 713 | this.protocol.send(message); 714 | } catch (err) { 715 | // noop 716 | } 717 | } 718 | ``` 719 | 720 | 如果请求被取消了,我们会如下操作: 721 | 722 | [core/common/ipc.ts] 723 | ```ts 724 | private disposeActiveRequest(request: IRawRequest): void { 725 | const disposable = this.activeRequests.get(request.id); 726 | 727 | if (disposable) { 728 | disposable.dispose(); 729 | this.activeRequests.delete(request.id); 730 | } 731 | } 732 | ``` 733 | 734 | #### 定义频道的客户端 735 | 736 | 「频道客户端」用于像服务发送请求,并接收请求的结果: 737 | 738 | 首先,我们可以定义个处理返回结果的接口: 739 | 740 | ```ts 741 | type IHandler = (response: IRawResponse) => void; 742 | ``` 743 | 744 | ```ts 745 | export interface IChannelClient { 746 | getChannel(channelName: string): T; 747 | } 748 | ``` 749 | 750 | ```ts 751 | export class ChannelClient implements IChannelClient, IDisposable { 752 | private protocolListener: IDisposable | null; 753 | 754 | private state: State = State.Uninitialized; // 频道的状态 755 | 756 | private lastRequestId = 0; // 通信请求唯一 ID 管理 757 | 758 | // 活跃中的 request, 用于取消的时候统一关闭;如果频道被关闭了(dispose),则统一会往所有的频道发送取消消息,从而确保通信的可靠性。 759 | private readonly activeRequests = new Set(); 760 | 761 | private readonly handlers = new Map(); // 通信返回结果后的处理 762 | 763 | private readonly _onDidInitialize = new Emitter(); 764 | 765 | // 当频道被初始化时会触发事件 766 | readonly onDidInitialize = this._onDidInitialize.event; 767 | 768 | constructor(private readonly protocol: IMessagePassingProtocol) { 769 | this.protocolListener = 770 | this.protocol.onMessage(msg => this.onBuffer(msg)); 771 | } 772 | } 773 | ``` 774 | 775 | ```ts 776 | enum State { 777 | Uninitialized, // 未初始化 778 | Idle, // 就绪 779 | } 780 | 781 | private state: State = State.Uninitialized; 782 | ``` 783 | 「频道客户端」的状态,有两种,一个是「未初始化」,或者是「就绪状态」。未初始化就是指「频道服务端」还未准备好之前的状态,准备好之后会触发`_onDidInitialize`事件,更新频道状态。 784 | 785 | ```ts 786 | constructor( 787 | private readonly protocol: IMessagePassingProtocol) { 788 | this.protocolListener = 789 | this.protocol.onMessage(msg => this.onBuffer(msg)); 790 | } 791 | ``` 792 | 在「频道客户端」初始化的时候,监听了「频道服务端」的消息,「频道服务端」准备好之后,发送的就绪状态的消息也是通过此消息监听。 793 | 794 | `onBuffer` 即解读`Buffer`消息; 795 | `onResponse` 根据解读的消息,进行消息处理,返回到调用的地方。 796 | 797 | ```ts 798 | private onBuffer(message: VSBuffer): void { 799 | const reader = new BufferReader(message); 800 | const header = deserialize(reader); 801 | const body = deserialize(reader); 802 | const type: ResponseType = header[0]; 803 | 804 | switch (type) { 805 | case ResponseType.Initialize: 806 | return this.onResponse({ type: header[0] }); 807 | 808 | case ResponseType.PromiseSuccess: 809 | case ResponseType.PromiseError: 810 | case ResponseType.EventFire: 811 | case ResponseType.PromiseErrorObj: 812 | return this.onResponse({ type: header[0], id: header[1], data: body }); 813 | } 814 | } 815 | 816 | private onResponse(response: IRawResponse): void { 817 | 818 | // 「频道服务端」就绪消息处理 819 | if (response.type === ResponseType.Initialize) { 820 | this.state = State.Idle; 821 | this._onDidInitialize.fire(); 822 | return; 823 | } 824 | 825 | // 「频道服务端」进行消息处理与返回 826 | const handler = this.handlers.get(response.id); 827 | 828 | if (handler) { 829 | handler(response); 830 | } 831 | } 832 | ``` 833 | 834 | 「频道服务端」发起请求之前,构造一个频道结构,去发送消息。 835 | > 为什么要构造一个频道来发消息,不直接发?留个疑问。 836 | 837 | ```ts 838 | getChannel(channelName: string): T { 839 | const that = this; 840 | return { 841 | call( 842 | command: string, // 服务方法名 843 | arg?: any, // 参数 844 | cancellationToken?: CancellationToken) { // 取消 845 | return that.requestPromise( 846 | channelName, 847 | command, 848 | arg, 849 | cancellationToken, 850 | ); 851 | }, 852 | listen(event: string, arg: any) { 853 | // TODO 854 | // return that.requestEvent(channelName, event, arg); 855 | }, 856 | } as T; 857 | } 858 | ``` 859 | 860 | `requestPromise`即发起服务调用请求: 861 | 862 | ```ts 863 | private requestPromise( 864 | channelName: string, 865 | name: string, 866 | arg?: any, 867 | cancellationToken = CancellationToken.None, 868 | ): Promise { 869 | const id = this.lastRequestId++; 870 | const type = RequestType.Promise; 871 | const request: IRawRequest = { id, type, channelName, name, arg }; 872 | 873 | // 如果请求被取消了,则不再执行。 874 | if (cancellationToken.isCancellationRequested) { 875 | return Promise.reject(canceled()); 876 | } 877 | 878 | let disposable: IDisposable; 879 | 880 | const result = new Promise((c, e) => { 881 | // 如果请求被取消了,则不再执行。 882 | if (cancellationToken.isCancellationRequested) { 883 | return e(canceled()); 884 | } 885 | 886 | // 只有频道确认注册完成后,才开始发送请求,否则一直处于队列中 887 | // 在「频道服务端」准备就绪后,会发送就绪消息回来,此时会触发状态变更为「idle」就绪状态 888 | // 从而会触发 uninitializedPromise.then 889 | // 从而消息可以进行发送 890 | let uninitializedPromise: CancelablePromise< 891 | void 892 | > | null = createCancelablePromise(_ => this.whenInitialized()); 893 | uninitializedPromise.then(() => { 894 | uninitializedPromise = null; 895 | 896 | const handler: IHandler = response => { 897 | console.log( 898 | 'main process response:', 899 | JSON.stringify(response, null, 2), 900 | ); 901 | // 根据返回的结果类型,进行处理, 这里不处理 Initialize 这个会在更上层处理 902 | switch (response.type) { 903 | case ResponseType.PromiseSuccess: 904 | this.handlers.delete(id); 905 | c(response.data); 906 | break; 907 | 908 | case ResponseType.PromiseError: 909 | this.handlers.delete(id); 910 | const error = new Error(response.data.message); 911 | (error).stack = response.data.stack; 912 | error.name = response.data.name; 913 | e(error); 914 | break; 915 | 916 | case ResponseType.PromiseErrorObj: 917 | this.handlers.delete(id); 918 | e(response.data); 919 | break; 920 | default: 921 | break; 922 | } 923 | }; 924 | 925 | // 保存此次请求的处理 926 | this.handlers.set(id, handler); 927 | 928 | // 开始发送请求 929 | this.sendRequest(request); 930 | }); 931 | 932 | const cancel = () => { 933 | // 如果还未初始化,则直接取消 934 | if (uninitializedPromise) { 935 | uninitializedPromise.cancel(); 936 | uninitializedPromise = null; 937 | } else { 938 | // 如果已经初始化,并且在请求中,则发送中断消息 939 | this.sendRequest({ id, type: RequestType.PromiseCancel }); 940 | } 941 | 942 | e(canceled()); 943 | }; 944 | 945 | const cancellationTokenListener = cancellationToken.onCancellationRequested( 946 | cancel, 947 | ); 948 | disposable = combinedDisposable( 949 | toDisposable(cancel), 950 | cancellationTokenListener, 951 | ); 952 | // 将请求保存到活跃请求中 953 | this.activeRequests.add(disposable); 954 | }); 955 | // 执行完毕后从活跃请求中移除此次请求 956 | return result.finally(() => this.activeRequests.delete(disposable)); 957 | } 958 | ``` 959 | 960 | 发送消息的方法,同接收消息一致,此处不在累赘: 961 | ```ts 962 | private sendRequest(request: IRawRequest): void { 963 | switch (request.type) { 964 | case RequestType.Promise: 965 | return this.send( 966 | [request.type, request.id, request.channelName, request.name], 967 | request.arg, 968 | ); 969 | 970 | case RequestType.PromiseCancel: 971 | return this.send([request.type, request.id]); 972 | default: 973 | break; 974 | } 975 | } 976 | 977 | private send(header: any, body: any = undefined): void { 978 | const writer = new BufferWriter(); 979 | serialize(writer, header); 980 | serialize(writer, body); 981 | this.sendBuffer(writer.buffer); 982 | } 983 | 984 | private sendBuffer(message: VSBuffer): void { 985 | try { 986 | this.protocol.send(message); 987 | } catch (err) { 988 | // noop 989 | } 990 | } 991 | ``` 992 | 993 | #### 定义一个连接: Connection 994 | 根据上面的设计图所示,如下: 995 | ```ts 996 | export interface Client { 997 | readonly ctx: TContext; 998 | } 999 | export interface Connection extends Client { 1000 | readonly channelServer: ChannelServer; // 频道服务端 1001 | readonly channelClient: ChannelClient; // 频道客户端 1002 | } 1003 | ``` 1004 | 1005 | #### 定义服务端:IPCServer 1006 | 1007 | ```ts 1008 | class IPCServer 1009 | implements 1010 | IChannelServer, 1011 | IDisposable { 1012 | 1013 | // 服务端侧可访问的频道 1014 | private readonly channels = new Map>(); 1015 | 1016 | // 客户端和服务端的连接 1017 | private readonly _connections = new Set>(); 1018 | 1019 | private readonly _onDidChangeConnections = new Emitter< 1020 | Connection 1021 | >(); 1022 | 1023 | // 连接改变的时候触发得事件监听 1024 | readonly onDidChangeConnections: Event> = this 1025 | ._onDidChangeConnections.event; 1026 | 1027 | // 所有连接 1028 | get connections(): Array> { 1029 | const result: Array> = []; 1030 | this._connections.forEach(ctx => result.push(ctx)); 1031 | return result; 1032 | } 1033 | // 释放所有监听 1034 | dispose(): void { 1035 | this.channels.clear(); 1036 | this._connections.clear(); 1037 | this._onDidChangeConnections.dispose(); 1038 | } 1039 | } 1040 | ``` 1041 | 1042 | 前面我们有提到「消息通信协议」,即: 1043 | - 在 'ipc:message' 频道收发消息 1044 | - 发送 'ipc:hello' 频道消息开始建立链接 1045 | - 断开连接的时,发送一个消息到 'ipc:disconnect' 频道 1046 | 1047 | 可能大家会有疑问,为什么我们的通信消息还需要建立连接,这意味着是长连接吗? 1048 | 1049 | 其实不是长连接,还是一次性通信的,其实流程是这样的: 1050 | - 一开始渲染进程发送 'ipc:hello'消息,打算后续可能会在 'ipc:message' 进行通信, 请主进程做好准备。 1051 | - 主进程接受到 'ipc:hello' 消息,发现是这个渲染进程**第一次**需要 'ipc:message' 频道通信,就开始监听这个频道消息。 1052 | - 当渲染进程卸载的时候,发出了 'ipc:disconnect' 消息。 1053 | - 主进程接收到渲染进程的 'ipc:disconnect' 消息。 就取消了在 'ipc:message' 的监听。 1054 | 1055 | 这里有一个注意点就是,'ipc:hello' 其实是在主进程 IPCServer 实例化的时候就建立的一个监听,用于了解每一个渲染进程需要的通信情况。 1056 | 1057 | 因此,这个通信机制的完整流程其实是这样: 1058 | 1059 | TODO 1060 | 1061 | > 这里有个很细节的地方,就是所有的渲染进程和主进程的通信都是通过 'ipc:message' 进行通信,那原本发送给窗口 A 的消息,窗口 B 也会收到?当然不会!,请听后续讲解。 1062 | > 上面的图只是简单的演示,当然连接会有重试机制。 1063 | 1064 | 接下来,我们开始实现上面的流程。 1065 | 1066 | 首先,定义一个客户端连接事件接口: 1067 | ```ts 1068 | export interface ClientConnectionEvent { 1069 | protocol: IMessagePassingProtocol; // 消息通信协议 1070 | onDidClientDisconnect: Event; // 断开连接事件 1071 | } 1072 | 1073 | ``` 1074 | 1075 | 接下来,我们实现一个监听客户端连接的方法 `getOnDidClientConnect` 1076 | 1077 | ```ts 1078 | export class IPCServer 1079 | implements 1080 | IChannelServer, 1081 | IDisposable { 1082 | private static getOnDidClientConnect(): Event { 1083 | const onHello = Event.fromNodeEventEmitter( 1084 | ipcMain, 1085 | 'ipc:hello', 1086 | ({ sender }) => sender, 1087 | ); 1088 | ... 1089 | } 1090 | 1091 | constructor(onDidClientConnect: Event) { 1092 | onDidClientConnect(({ protocol, onDidClientDisconnect }) => { 1093 | ... 1094 | } 1095 | } 1096 | } 1097 | ``` 1098 | 1099 | 通过流程图,我们知道在 IPCServer 实例化的时候,我们会注册 'ipc:hello' 消息监听, 我们可以把收到此消息作为建立连接的标志。 1100 | 1101 | > 如果我发送多个 onHello 消息,就连接了多次?当然不是,请听下面分析。 1102 | 1103 | 首先简单解释下这句话,更详细的解析会在后续的 vscode 事件机制中进行解读: 1104 | 1105 | ```ts 1106 | const onHello = Event.fromNodeEventEmitter( 1107 | ipcMain, 1108 | 'ipc:hello', 1109 | ({ sender }) => sender, 1110 | ); 1111 | ``` 1112 | 1113 | 这个的含义,其实就是定义了一个 ipc:hello 的监听,比如原来,你注册监听 ipc:hello 可能是这样: 1114 | 1115 | ```ts 1116 | const handler = (e) => { 1117 | console.log('sender:', e.sender.id); 1118 | }; 1119 | ipcMain.on('ipc:hello', handler) 1120 | 1121 | // 移除监听 1122 | ipcMain.removeListener('ipc:hello', handler); 1123 | ``` 1124 | 1125 | 而现在是这样的: 1126 | ```ts 1127 | const onHello = Event.fromNodeEventEmitter( 1128 | ipcMain, 1129 | 'ipc:hello', 1130 | ({ sender }) => sender, 1131 | ); 1132 | const listener = onHello((sender) => { 1133 | console.log('sender'); 1134 | }); 1135 | 1136 | // 移除监听 1137 | listener.dispose(); 1138 | ``` 1139 | 这样的写法有着诸多的好处,以及说 Event.fromNodeEventEmitter 是怎样的实现,会在后续持续更新。 1140 | 1141 | 接下来,我们来继续实现 `getOnDidClientConnect()` 1142 | 1143 | ```ts 1144 | export class IPCServer 1145 | implements 1146 | IChannelServer, 1147 | IDisposable { 1148 | private static getOnDidClientConnect(): Event { 1149 | const onHello = Event.fromNodeEventEmitter( 1150 | ipcMain, 1151 | 'ipc:hello', 1152 | ({ sender }) => sender, 1153 | ); 1154 | return Event.map(onHello, webContents => { 1155 | const { id } = webContents; 1156 | ... 1157 | const onMessage = createScopedOnMessageEvent(id, 'ipc:message') as Event< 1158 | VSBuffer 1159 | >; 1160 | const onDidClientDisconnect = Event.any( 1161 | Event.signal(createScopedOnMessageEvent(id, 'ipc:disconnect')), 1162 | onDidClientReconnect.event, 1163 | ); 1164 | const protocol = new Protocol(webContents, onMessage); 1165 | return { protocol, onDidClientDisconnect }; 1166 | }); 1167 | } 1168 | ... 1169 | } 1170 | ``` 1171 | 我们前面有讲解到,我们定义了 onHello 之后,去监听事件的时候是这样: 1172 | 1173 | ```ts 1174 | const listener = onHello((sender) => { 1175 | console.log('sender'); 1176 | }); 1177 | ``` 1178 | 1179 | 可以看到,针对所有 ipc:hello 的消息的消息参数都被过滤成了 sender,而不是原先的 e。 1180 | 而: 1181 | ```ts 1182 | getOnDidClientConnect(): Event { 1183 | return Event.map(onHello, webContents => { 1184 | ... 1185 | return { protocol, onDidClientDisconnect }; 1186 | }) 1187 | } 1188 | ``` 1189 | 则将 onHello 的参数过滤成了:{ protocol, onDidClientDisconnect }。 相当于在 onHello 事件上再一次使用装饰者模式装饰参数 1190 | 1191 | 可能有点蒙,我再横向对比下,经过**两次装饰**后 1192 | 1193 | ```ts 1194 | // 第一次装饰, 事件参数 e 变成了参数 sender 1195 | const onHello = Event.fromNodeEventEmitter( 1196 | ipcMain, 1197 | 'ipc:hello', 1198 | ({ sender }) => sender, 1199 | ); 1200 | 1201 | // 第二次装饰 事件参数 sender(webContents) 变成了 { protocol, onDidClientDisconnect } 1202 | getOnDidClientConnect(): Event { 1203 | return Event.map(onHello, webContents => { 1204 | ... 1205 | return { protocol, onDidClientDisconnect }; 1206 | }) 1207 | } 1208 | 1209 | ``` 1210 | 1211 | 从原来的: 1212 | ```ts 1213 | const handler = (e) => { 1214 | console.log('sender:', e.sender.id); 1215 | }; 1216 | ipcMain.on('ipc:hello', handler) 1217 | 1218 | // 移除监听 1219 | ipcMain.removeListener('ipc:hello', handler); 1220 | ``` 1221 | 1222 | 就变成了: 1223 | ```ts 1224 | 1225 | // 仍然是 ipc:hello 事件,只不过从原来的消息参数 e 变成了 {protocol, onDidClientDisconnect} 1226 | const onDidClientConnnect = getOnDidClientConnect(); 1227 | const listener = onDidClientConnnect(({protocol, onDidClientDisconnect}) => { 1228 | ... 1229 | }); 1230 | 1231 | // 解除监听 1232 | listener.dispose(); 1233 | 1234 | ``` 1235 | 1236 | 好,接下来我们来分析下 `{ protocol, onDidClientDisconnect }` 这两个参数。 1237 | 1238 | protocol 即我们在上面定义的通信协议,包含了: 1239 | 1240 | - sender 是发送对象的接口,只要满足存在一个方法:send 即可。 1241 | - 协议规定: 1242 | - 发送 'ipc:hello' 频道消息开始建立链接 1243 | - 在 'ipc:message' 频道收发消息 1244 | - 断开连接的时,发送一个消息到 'ipc:disconnect' 频道 1245 | 1246 | 因此: 1247 | 1248 | ```ts 1249 | const onMessage = createScopedOnMessageEvent(id, 'ipc:message') as Event< 1250 | VSBuffer 1251 | >; 1252 | const protocol = new Protocol(webContents, onMessage); 1253 | ``` 1254 | - webContents 即为 sender:发送消息的对象。 1255 | - onMessage 即为在 ipc:message 频道监听消息的方法 1256 | 1257 | 这里的 onMessage 可以简单理解为通过封装后的消息监听,即从原先的: 1258 | ```ts 1259 | ipcMain.on('ipc:message', (e, message) => { 1260 | console.log('message:', message); 1261 | }) 1262 | ``` 1263 | 变成了: 1264 | ```ts 1265 | onMessage((message) => { 1266 | console.log('message:', message); 1267 | }) 1268 | ``` 1269 | 1270 | > - 有个很显著的特征就是事件参数由原来的(e, message)变成了 (message) 1271 | > - message被使用 buffer 压缩,这也是通信优化的一部分。 1272 | > - 当然除此以外,createScopedOnMessageEvent 还有一个能力,是进行了过滤,前面有提到,所有渲染进程,都通过 1273 | `ipc:message` 频道通信,那如何避免发给渲染进程A的消息,渲染进程B也收到了呢,就是在这里过滤的,过滤后,渲染进程A只会收到给A的消息。 1274 | 1275 | `onDidClientDisconnect` 同理,是 'ipc:disconnect' 消息监听,就不做解释了。 1276 | 1277 | 接下来,我们来继续解析 IPCServer 如何在实例化的时候注册的 ipc:hello 监听 1278 | 1279 | ```ts 1280 | class IPCServer 1281 | implements 1282 | IChannelServer, 1283 | IDisposable { 1284 | constructor(onDidClientConnect: Event) { 1285 | onDidClientConnect(({ protocol, onDidClientDisconnect }) => { 1286 | const onFirstMessage = Event.once(protocol.onMessage); 1287 | // 第一次接收消息 1288 | onFirstMessage(msg => { 1289 | const reader = new BufferReader(msg); 1290 | const ctx = deserialize(reader) as TContext; // 后续解释 1291 | 1292 | const channelServer = new ChannelServer(protocol, ctx); 1293 | const channelClient = new ChannelClient(protocol); 1294 | 1295 | this.channels.forEach((channel, name) => 1296 | channelServer.registerChannel(name, channel), 1297 | ); 1298 | 1299 | const connection: Connection = { 1300 | channelServer, 1301 | channelClient, 1302 | ctx, 1303 | }; 1304 | this._connections.add(connection); 1305 | // this._onDidChangeConnections.fire(connection); 1306 | 1307 | onDidClientDisconnect(() => { 1308 | channelServer.dispose(); 1309 | channelClient.dispose(); 1310 | this._connections.delete(connection); 1311 | }); 1312 | }) 1313 | } 1314 | } 1315 | } 1316 | ``` 1317 | 在第一次接收到 ipc:message 的时候,我们会新建一个连接: connection;并在收到:ipc:disconnect 的时候,删除连接、移除监听。 1318 | 1319 | 1320 | 这里的重点方法是注册频道,正如上面设计图中所示,每个新增的频道,都会在已有的连接中添加改频道: 1321 | ```ts 1322 | registerChannel( 1323 | channelName: string, 1324 | channel: IServerChannel, 1325 | ): void { 1326 | this.channels.set(channelName, channel); 1327 | 1328 | // 同时在所有的连接中,需要注册频道 1329 | this._connections.forEach(connection => { 1330 | connection.channelServer.registerChannel(channelName, channel); 1331 | }); 1332 | } 1333 | ``` 1334 | 1335 | 服务端准备好了,我们需要实现客户端的细节: 1336 | ```ts 1337 | export class IPCClient 1338 | implements IChannelClient, IChannelServer, IDisposable { 1339 | private readonly channelClient: ChannelClient; 1340 | 1341 | private readonly channelServer: ChannelServer; 1342 | 1343 | constructor(protocol: IMessagePassingProtocol, ctx: TContext) { 1344 | 1345 | const writer = new BufferWriter(); 1346 | serialize(writer, ctx); 1347 | // 发送服务注册消息 ctx 即服务名。 1348 | protocol.send(writer.buffer); 1349 | 1350 | this.channelClient = new ChannelClient(protocol); 1351 | this.channelServer = new ChannelServer(protocol, ctx); 1352 | } 1353 | 1354 | getChannel(channelName: string): T { 1355 | return this.channelClient.getChannel(channelName); 1356 | } 1357 | 1358 | registerChannel( 1359 | channelName: string, 1360 | channel: IServerChannel, 1361 | ): void { 1362 | // 注册频道 1363 | this.channelServer.registerChannel(channelName, channel); 1364 | } 1365 | 1366 | dispose(): void { 1367 | this.channelClient.dispose(); 1368 | this.channelServer.dispose(); 1369 | } 1370 | } 1371 | ``` 1372 | 1373 | ```ts 1374 | export class Client extends IPCClient implements IDisposable { 1375 | private readonly protocol: Protocol; 1376 | 1377 | private static createProtocol(): Protocol { 1378 | const onMessage = Event.fromNodeEventEmitter( 1379 | ipcRenderer, 1380 | 'ipc:message', 1381 | (_, message: Buffer) => VSBuffer.wrap(message), 1382 | ); 1383 | ipcRenderer.send('ipc:hello'); 1384 | return new Protocol(ipcRenderer, onMessage); 1385 | } 1386 | 1387 | constructor(id: string) { 1388 | const protocol = Client.createProtocol(); 1389 | super(protocol, id); 1390 | this.protocol = protocol; 1391 | } 1392 | 1393 | dispose(): void { 1394 | this.protocol.dispose(); 1395 | } 1396 | } 1397 | 1398 | ``` 1399 | -------------------------------------------------------------------------------- /base/buffer-utils.ts: -------------------------------------------------------------------------------- 1 | import { hasBuffer, VSBuffer } from "./buffer"; 2 | 3 | interface IReader { 4 | read(bytes: number): VSBuffer; 5 | } 6 | 7 | interface IWriter { 8 | write(buffer: VSBuffer): void; 9 | } 10 | 11 | export class BufferReader implements IReader { 12 | private pos = 0; 13 | 14 | constructor(private readonly buffer: VSBuffer) {} 15 | 16 | read(bytes: number): VSBuffer { 17 | const result = this.buffer.slice(this.pos, this.pos + bytes); 18 | this.pos += result.byteLength; 19 | return result; 20 | } 21 | } 22 | 23 | export class BufferWriter implements IWriter { 24 | private readonly buffers: VSBuffer[] = []; 25 | 26 | get buffer(): VSBuffer { 27 | return VSBuffer.concat(this.buffers); 28 | } 29 | 30 | write(buffer: VSBuffer): void { 31 | this.buffers.push(buffer); 32 | } 33 | } 34 | 35 | enum DataType { 36 | Undefined = 0, 37 | String = 1, 38 | Buffer = 2, 39 | VSBuffer = 3, 40 | Array = 4, 41 | Object = 5, 42 | } 43 | 44 | function readSizeBuffer(reader: IReader): number { 45 | return reader.read(4).readUInt32BE(0); 46 | } 47 | 48 | function createOneByteBuffer(value: number): VSBuffer { 49 | const result = VSBuffer.alloc(1); 50 | result.writeUInt8(value, 0); 51 | return result; 52 | } 53 | 54 | const BufferPresets = { 55 | Undefined: createOneByteBuffer(DataType.Undefined), 56 | String: createOneByteBuffer(DataType.String), 57 | Buffer: createOneByteBuffer(DataType.Buffer), 58 | VSBuffer: createOneByteBuffer(DataType.VSBuffer), 59 | Array: createOneByteBuffer(DataType.Array), 60 | Object: createOneByteBuffer(DataType.Object), 61 | }; 62 | 63 | 64 | function createSizeBuffer(size: number): VSBuffer { 65 | const result = VSBuffer.alloc(4); 66 | result.writeUInt32BE(size, 0); 67 | return result; 68 | } 69 | 70 | 71 | export function serialize(writer: IWriter, data: any): void { 72 | if (typeof data === 'undefined') { 73 | writer.write(BufferPresets.Undefined); 74 | } else if (typeof data === 'string') { 75 | const buffer = VSBuffer.fromString(data); 76 | writer.write(BufferPresets.String); 77 | writer.write(createSizeBuffer(buffer.byteLength)); 78 | writer.write(buffer); 79 | } else if (hasBuffer && Buffer.isBuffer(data)) { 80 | const buffer = VSBuffer.wrap(data); 81 | writer.write(BufferPresets.Buffer); 82 | writer.write(createSizeBuffer(buffer.byteLength)); 83 | writer.write(buffer); 84 | } else if (data instanceof VSBuffer) { 85 | writer.write(BufferPresets.VSBuffer); 86 | writer.write(createSizeBuffer(data.byteLength)); 87 | writer.write(data); 88 | } else if (Array.isArray(data)) { 89 | writer.write(BufferPresets.Array); 90 | writer.write(createSizeBuffer(data.length)); 91 | 92 | for (const el of data) { 93 | serialize(writer, el); 94 | } 95 | } else { 96 | const buffer = VSBuffer.fromString(JSON.stringify(data)); 97 | writer.write(BufferPresets.Object); 98 | writer.write(createSizeBuffer(buffer.byteLength)); 99 | writer.write(buffer); 100 | } 101 | } 102 | 103 | 104 | export function deserialize(reader: IReader): any { 105 | const type = reader.read(1).readUInt8(0); 106 | 107 | switch (type) { 108 | case DataType.Undefined: 109 | return undefined; 110 | case DataType.String: 111 | return reader.read(readSizeBuffer(reader)).toString(); 112 | case DataType.Buffer: 113 | return reader.read(readSizeBuffer(reader)).buffer; 114 | case DataType.VSBuffer: 115 | return reader.read(readSizeBuffer(reader)); 116 | case DataType.Array: { 117 | const length = readSizeBuffer(reader); 118 | const result: any[] = []; 119 | 120 | for (let i = 0; i < length; i++) { 121 | result.push(deserialize(reader)); 122 | } 123 | 124 | return result; 125 | } 126 | case DataType.Object: 127 | return JSON.parse(reader.read(readSizeBuffer(reader)).toString()); 128 | default: 129 | break; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /base/buffer.ts: -------------------------------------------------------------------------------- 1 | declare let Buffer: any; 2 | export const hasBuffer = typeof Buffer !== 'undefined'; 3 | 4 | let textEncoder: TextEncoder | null; 5 | let textDecoder: TextDecoder | null; 6 | 7 | export class VSBuffer { 8 | static alloc(byteLength: number): VSBuffer { 9 | if (hasBuffer) { 10 | return new VSBuffer(Buffer.allocUnsafe(byteLength)); 11 | } else { 12 | return new VSBuffer(new Uint8Array(byteLength)); 13 | } 14 | } 15 | 16 | static wrap(actual: Uint8Array): VSBuffer { 17 | if (hasBuffer && !Buffer.isBuffer(actual)) { 18 | // https://nodejs.org/dist/latest-v10.x/docs/api/buffer.html#buffer_class_method_buffer_from_arraybuffer_byteoffset_length 19 | // Create a zero-copy Buffer wrapper around the ArrayBuffer pointed to by the Uint8Array 20 | actual = Buffer.from(actual.buffer, actual.byteOffset, actual.byteLength); 21 | } 22 | return new VSBuffer(actual); 23 | } 24 | 25 | static fromString(source: string): VSBuffer { 26 | if (hasBuffer) { 27 | return new VSBuffer(Buffer.from(source)); 28 | } else { 29 | if (!textEncoder) { 30 | textEncoder = new TextEncoder(); 31 | } 32 | return new VSBuffer(textEncoder.encode(source)); 33 | } 34 | } 35 | 36 | static concat(buffers: VSBuffer[], totalLength?: number): VSBuffer { 37 | if (typeof totalLength === 'undefined') { 38 | totalLength = 0; 39 | for (let i = 0, len = buffers.length; i < len; i++) { 40 | totalLength += buffers[i].byteLength; 41 | } 42 | } 43 | 44 | const ret = VSBuffer.alloc(totalLength); 45 | let offset = 0; 46 | for (let i = 0, len = buffers.length; i < len; i++) { 47 | const element = buffers[i]; 48 | ret.set(element, offset); 49 | offset += element.byteLength; 50 | } 51 | 52 | return ret; 53 | } 54 | 55 | readonly buffer: Uint8Array; 56 | 57 | readonly byteLength: number; 58 | 59 | private constructor(buffer: Uint8Array) { 60 | this.buffer = buffer; 61 | this.byteLength = this.buffer.byteLength; 62 | } 63 | 64 | toString(): string { 65 | if (hasBuffer) { 66 | return this.buffer.toString(); 67 | } else { 68 | if (!textDecoder) { 69 | textDecoder = new TextDecoder(); 70 | } 71 | return textDecoder.decode(this.buffer); 72 | } 73 | } 74 | 75 | slice(start?: number, end?: number): VSBuffer { 76 | // IMPORTANT: use subarray instead of slice because TypedArray#slice 77 | // creates shallow copy and NodeBuffer#slice doesn't. The use of subarray 78 | // ensures the same, performant, behaviour. 79 | return new VSBuffer(this.buffer.subarray(start || 0, end)); 80 | } 81 | 82 | set(array: VSBuffer, offset?: number): void { 83 | this.buffer.set(array.buffer, offset); 84 | } 85 | 86 | readUInt32BE(offset: number): number { 87 | return readUInt32BE(this.buffer, offset); 88 | } 89 | 90 | writeUInt32BE(value: number, offset: number): void { 91 | writeUInt32BE(this.buffer, value, offset); 92 | } 93 | 94 | readUInt8(offset: number): number { 95 | return readUInt8(this.buffer, offset); 96 | } 97 | 98 | writeUInt8(value: number, offset: number): void { 99 | writeUInt8(this.buffer, value, offset); 100 | } 101 | } 102 | 103 | export function readUInt32BE(source: Uint8Array, offset: number): number { 104 | return ( 105 | source[offset] * 2 ** 24 + 106 | source[offset + 1] * 2 ** 16 + 107 | source[offset + 2] * 2 ** 8 + 108 | source[offset + 3] 109 | ); 110 | } 111 | 112 | export function writeUInt32BE( 113 | destination: Uint8Array, 114 | value: number, 115 | offset: number, 116 | ): void { 117 | destination[offset + 3] = value; 118 | value >>>= 8; 119 | destination[offset + 2] = value; 120 | value >>>= 8; 121 | destination[offset + 1] = value; 122 | value >>>= 8; 123 | destination[offset] = value; 124 | } 125 | 126 | function readUInt8(source: Uint8Array, offset: number): number { 127 | return source[offset]; 128 | } 129 | 130 | function writeUInt8( 131 | destination: Uint8Array, 132 | value: number, 133 | offset: number, 134 | ): void { 135 | destination[offset] = value; 136 | } 137 | 138 | export interface VSBufferReadable { 139 | /** 140 | * Read data from the underlying source. Will return 141 | * null to indicate that no more data can be read. 142 | */ 143 | read(): VSBuffer | null; 144 | } 145 | 146 | export interface ReadableStream { 147 | /** 148 | * The 'data' event is emitted whenever the stream is 149 | * relinquishing ownership of a chunk of data to a consumer. 150 | */ 151 | on(event: 'data', callback: (chunk: T) => void): void; 152 | 153 | /** 154 | * Emitted when any error occurs. 155 | */ 156 | on(event: 'error', callback: (err: any) => void): void; 157 | 158 | /** 159 | * The 'end' event is emitted when there is no more data 160 | * to be consumed from the stream. The 'end' event will 161 | * not be emitted unless the data is completely consumed. 162 | */ 163 | on(event: 'end', callback: () => void): void; 164 | 165 | /** 166 | * Stops emitting any events until resume() is called. 167 | */ 168 | pause?(): void; 169 | 170 | /** 171 | * Starts emitting events again after pause() was called. 172 | */ 173 | resume?(): void; 174 | 175 | /** 176 | * Destroys the stream and stops emitting any event. 177 | */ 178 | destroy?(): void; 179 | } 180 | 181 | /** 182 | * A readable stream that sends data via VSBuffer. 183 | */ 184 | export interface VSBufferReadableStream extends ReadableStream { 185 | pause(): void; 186 | resume(): void; 187 | destroy(): void; 188 | } 189 | 190 | export function isVSBufferReadableStream( 191 | obj: any, 192 | ): obj is VSBufferReadableStream { 193 | const candidate: VSBufferReadableStream = obj; 194 | 195 | return ( 196 | candidate && 197 | [candidate.on, candidate.pause, candidate.resume, candidate.destroy].every( 198 | fn => typeof fn === 'function', 199 | ) 200 | ); 201 | } 202 | 203 | /** 204 | * Helper to fully read a VSBuffer readable into a single buffer. 205 | * 206 | * @example 207 | */ 208 | export function readableToBuffer(readable: VSBufferReadable): VSBuffer { 209 | const chunks: VSBuffer[] = []; 210 | 211 | let chunk: VSBuffer | null; 212 | chunk = readable.read(); 213 | while (chunk) { 214 | chunks.push(chunk); 215 | chunk = readable.read(); 216 | } 217 | 218 | return VSBuffer.concat(chunks); 219 | } 220 | 221 | /** 222 | * Helper to convert a buffer into a readable buffer. 223 | * 224 | * @example 225 | */ 226 | export function bufferToReadable(buffer: VSBuffer): VSBufferReadable { 227 | let done = false; 228 | 229 | return { 230 | read: () => { 231 | if (done) { 232 | return null; 233 | } 234 | 235 | done = true; 236 | 237 | return buffer; 238 | }, 239 | }; 240 | } 241 | 242 | /** 243 | * Helper to fully read a VSBuffer stream into a single buffer. 244 | * 245 | * @example 246 | */ 247 | export function streamToBuffer( 248 | stream: VSBufferReadableStream, 249 | ): Promise { 250 | return new Promise((resolve, reject) => { 251 | const chunks: VSBuffer[] = []; 252 | 253 | stream.on('data', chunk => chunks.push(chunk)); 254 | stream.on('error', error => reject(error)); 255 | stream.on('end', () => resolve(VSBuffer.concat(chunks))); 256 | }); 257 | } 258 | 259 | /** 260 | * Helper to create a VSBufferStream from an existing VSBuffer. 261 | * 262 | * @example 263 | */ 264 | export function bufferToStream(buffer: VSBuffer): VSBufferReadableStream { 265 | const stream = writeableBufferStream(); 266 | 267 | stream.end(buffer); 268 | 269 | return stream; 270 | } 271 | 272 | /** 273 | * Helper to create a VSBufferStream from a Uint8Array stream. 274 | * 275 | * @example 276 | */ 277 | export function toVSBufferReadableStream( 278 | stream: ReadableStream, 279 | ): VSBufferReadableStream { 280 | const vsbufferStream = writeableBufferStream(); 281 | 282 | stream.on('data', data => 283 | vsbufferStream.write( 284 | typeof data === 'string' 285 | ? VSBuffer.fromString(data) 286 | : VSBuffer.wrap(data), 287 | ), 288 | ); 289 | stream.on('end', () => vsbufferStream.end()); 290 | stream.on('error', error => vsbufferStream.error(error)); 291 | 292 | return vsbufferStream; 293 | } 294 | 295 | /** 296 | * Helper to create a VSBufferStream that can be pushed 297 | buffers to. Will only start to emit data when a listener 298 | is added. 299 | * 300 | * @example 301 | */ 302 | export function writeableBufferStream(): VSBufferWriteableStream { 303 | return new VSBufferWriteableStreamImpl(); 304 | } 305 | 306 | export interface VSBufferWriteableStream extends VSBufferReadableStream { 307 | write(chunk: VSBuffer): void; 308 | error(error: Error): void; 309 | end(result?: VSBuffer | Error): void; 310 | } 311 | 312 | class VSBufferWriteableStreamImpl implements VSBufferWriteableStream { 313 | private readonly state = { 314 | flowing: false, 315 | ended: false, 316 | destroyed: false, 317 | }; 318 | 319 | private readonly buffer = { 320 | data: [] as VSBuffer[], 321 | error: [] as Error[], 322 | }; 323 | 324 | private readonly listeners = { 325 | data: [] as Array<(chunk: VSBuffer) => void>, 326 | error: [] as Array<(error: Error) => void>, 327 | end: [] as Array<() => void>, 328 | }; 329 | 330 | pause(): void { 331 | if (this.state.destroyed) { 332 | return; 333 | } 334 | 335 | this.state.flowing = false; 336 | } 337 | 338 | resume(): void { 339 | if (this.state.destroyed) { 340 | return; 341 | } 342 | 343 | if (!this.state.flowing) { 344 | this.state.flowing = true; 345 | 346 | // emit buffered events 347 | this.flowData(); 348 | this.flowErrors(); 349 | this.flowEnd(); 350 | } 351 | } 352 | 353 | write(chunk: VSBuffer): void { 354 | if (this.state.destroyed) { 355 | return; 356 | } 357 | 358 | // flowing: directly send the data to listeners 359 | if (this.state.flowing) { 360 | this.listeners.data.forEach(listener => listener(chunk)); 361 | } 362 | 363 | // not yet flowing: buffer data until flowing 364 | else { 365 | this.buffer.data.push(chunk); 366 | } 367 | } 368 | 369 | error(error: Error): void { 370 | if (this.state.destroyed) { 371 | return; 372 | } 373 | 374 | // flowing: directly send the error to listeners 375 | if (this.state.flowing) { 376 | this.listeners.error.forEach(listener => listener(error)); 377 | } 378 | 379 | // not yet flowing: buffer errors until flowing 380 | else { 381 | this.buffer.error.push(error); 382 | } 383 | } 384 | 385 | end(result?: VSBuffer | Error): void { 386 | if (this.state.destroyed) { 387 | return; 388 | } 389 | 390 | // end with data or error if provided 391 | if (result instanceof Error) { 392 | this.error(result); 393 | } else if (result) { 394 | this.write(result); 395 | } 396 | 397 | // flowing: send end event to listeners 398 | if (this.state.flowing) { 399 | this.listeners.end.forEach(listener => listener()); 400 | 401 | this.destroy(); 402 | } 403 | 404 | // not yet flowing: remember state 405 | else { 406 | this.state.ended = true; 407 | } 408 | } 409 | 410 | on(event: 'data', callback: (chunk: VSBuffer) => void): void; 411 | 412 | on(event: 'error', callback: (err: any) => void): void; 413 | 414 | on(event: 'end', callback: () => void): void; 415 | 416 | on(event: 'data' | 'error' | 'end', callback: (arg0?: any) => void): void { 417 | if (this.state.destroyed) { 418 | return; 419 | } 420 | 421 | switch (event) { 422 | case 'data': 423 | this.listeners.data.push(callback); 424 | 425 | // switch into flowing mode as soon as the first 'data' 426 | // listener is added and we are not yet in flowing mode 427 | this.resume(); 428 | 429 | break; 430 | 431 | case 'end': 432 | this.listeners.end.push(callback); 433 | 434 | // emit 'end' event directly if we are flowing 435 | // and the end has already been reached 436 | // 437 | // finish() when it went through 438 | if (this.state.flowing && this.flowEnd()) { 439 | this.destroy(); 440 | } 441 | 442 | break; 443 | 444 | case 'error': 445 | this.listeners.error.push(callback); 446 | 447 | // emit buffered 'error' events unless done already 448 | // now that we know that we have at least one listener 449 | if (this.state.flowing) { 450 | this.flowErrors(); 451 | } 452 | 453 | break; 454 | default: 455 | break; 456 | } 457 | } 458 | 459 | destroy(): void { 460 | if (!this.state.destroyed) { 461 | this.state.destroyed = true; 462 | this.state.ended = true; 463 | 464 | this.buffer.data.length = 0; 465 | this.buffer.error.length = 0; 466 | 467 | this.listeners.data.length = 0; 468 | this.listeners.error.length = 0; 469 | this.listeners.end.length = 0; 470 | } 471 | } 472 | 473 | private flowData(): void { 474 | if (this.buffer.data.length > 0) { 475 | const fullDataBuffer = VSBuffer.concat(this.buffer.data); 476 | 477 | this.listeners.data.forEach(listener => listener(fullDataBuffer)); 478 | 479 | this.buffer.data.length = 0; 480 | } 481 | } 482 | 483 | private flowErrors(): void { 484 | if (this.listeners.error.length > 0) { 485 | for (const error of this.buffer.error) { 486 | this.listeners.error.forEach(listener => listener(error)); 487 | } 488 | 489 | this.buffer.error.length = 0; 490 | } 491 | } 492 | 493 | private flowEnd(): boolean { 494 | if (this.state.ended) { 495 | this.listeners.end.forEach(listener => listener()); 496 | 497 | return this.listeners.end.length > 0; 498 | } 499 | 500 | return false; 501 | } 502 | } 503 | -------------------------------------------------------------------------------- /base/cancelablePromise/cancelablePromise.ts: -------------------------------------------------------------------------------- 1 | import * as errors from '../errors'; 2 | import { Emitter, Event } from '../event'; 3 | import { IDisposable } from '../interface'; 4 | 5 | export function isThenable(obj: any): obj is Promise { 6 | return obj && typeof (obj as Promise).then === 'function'; 7 | } 8 | 9 | export interface CancelablePromise extends Promise { 10 | cancel(): void; 11 | } 12 | 13 | export interface CancellationToken { 14 | readonly isCancellationRequested: boolean; // 被要求取消 15 | /** 16 | * An event emitted when cancellation is requested 17 | * 18 | * @event 19 | */ 20 | readonly onCancellationRequested: Event; 21 | } 22 | 23 | // 快速的事件 24 | const shortcutEvent = Object.freeze(function (callback, context?): IDisposable { 25 | const handle = setTimeout(callback.bind(context), 0); 26 | return { 27 | dispose() { 28 | clearTimeout(handle); 29 | }, 30 | }; 31 | } as Event); 32 | 33 | 34 | class MutableToken implements CancellationToken { 35 | private _isCancelled: boolean = false; 36 | 37 | private _emitter: Emitter | null = null; 38 | 39 | public cancel() { 40 | if (!this._isCancelled) { 41 | this._isCancelled = true; 42 | if (this._emitter) { 43 | this._emitter.fire(undefined); 44 | this.dispose(); 45 | } 46 | } 47 | } 48 | 49 | get isCancellationRequested(): boolean { 50 | return this._isCancelled; 51 | } 52 | 53 | get onCancellationRequested(): Event { 54 | if (this._isCancelled) { 55 | return shortcutEvent; 56 | } 57 | if (!this._emitter) { 58 | this._emitter = new Emitter(); 59 | } 60 | return this._emitter.event; 61 | } 62 | 63 | public dispose(): void { 64 | if (this._emitter) { 65 | this._emitter.dispose(); 66 | this._emitter = null; 67 | } 68 | } 69 | } 70 | 71 | 72 | export namespace CancellationToken { 73 | export function isCancellationToken(thing: any): thing is CancellationToken { 74 | if (thing === None || thing === Cancelled) { 75 | return true; 76 | } 77 | if (thing instanceof MutableToken) { 78 | return true; 79 | } 80 | if (!thing || typeof thing !== 'object') { 81 | return false; 82 | } 83 | return ( 84 | typeof (thing as CancellationToken).isCancellationRequested === 85 | 'boolean' && 86 | typeof (thing as CancellationToken).onCancellationRequested === 'function' 87 | ); 88 | } 89 | 90 | export const None: CancellationToken = Object.freeze({ 91 | isCancellationRequested: false, 92 | onCancellationRequested: Event.None, 93 | }); 94 | 95 | export const Cancelled: CancellationToken = Object.freeze({ 96 | isCancellationRequested: true, 97 | onCancellationRequested: shortcutEvent, 98 | }); 99 | } 100 | 101 | function createCancelablePromise( 102 | callback: (token: CancellationToken) => Promise, 103 | ): CancelablePromise { 104 | // 生成一个取消令牌 105 | const source = new CancellationTokenSource(); 106 | 107 | // 将令牌给到 callback 中, 这样即可控制在 callback 任何生命周期进行终止 108 | // callback 中也可以主动发起终止。 109 | const thenable = callback(source.token); 110 | const promise = new Promise((resolve, reject) => { 111 | 112 | // 被终止了,则会结束此 promise 113 | source.token.onCancellationRequested(() => { 114 | reject(errors.canceled()); 115 | }); 116 | 117 | // 正常执行,则返回执行结果。 118 | Promise.resolve(thenable).then( 119 | value => { 120 | source.dispose(); 121 | resolve(value); 122 | }, 123 | err => { 124 | source.dispose(); 125 | reject(err); 126 | }, 127 | ); 128 | }); 129 | 130 | // 最终返回 Promise 对象 131 | // @ts-ignore 132 | return new (class implements CancelablePromise { 133 | cancel() { 134 | source.cancel(); 135 | } 136 | 137 | then( 138 | resolve?: ((value: T) => TResult1 | Promise) | undefined | null, 139 | reject?: 140 | | ((reason: any) => TResult2 | Promise) 141 | | undefined 142 | | null, 143 | ): Promise { 144 | return promise.then(resolve, reject); 145 | } 146 | 147 | catch( 148 | reject?: ((reason: any) => TResult | Promise) | undefined | null, 149 | ): Promise { 150 | return this.then(undefined, reject); 151 | } 152 | 153 | finally(onfinally?: (() => void) | undefined | null): Promise { 154 | return promise.finally(onfinally); 155 | } 156 | })(); 157 | } 158 | 159 | export class CancellationTokenSource { 160 | private _token?: CancellationToken = undefined; 161 | 162 | private readonly _parentListener?: IDisposable = undefined; 163 | 164 | constructor(parent?: CancellationToken) { 165 | this._parentListener = 166 | parent && parent.onCancellationRequested(this.cancel, this); 167 | } 168 | 169 | get token(): CancellationToken { 170 | if (!this._token) { 171 | // be lazy and create the token only when 172 | // actually needed 173 | this._token = new MutableToken(); 174 | } 175 | return this._token; 176 | } 177 | 178 | cancel(): void { 179 | if (!this._token) { 180 | // save an object by returning the default 181 | // cancelled token when cancellation happens 182 | // before someone asks for the token 183 | this._token = CancellationToken.Cancelled; 184 | } else if (this._token instanceof MutableToken) { 185 | // actually cancel 186 | this._token.cancel(); 187 | } 188 | } 189 | 190 | dispose(): void { 191 | if (this._parentListener) { 192 | this._parentListener.dispose(); 193 | } 194 | if (!this._token) { 195 | // ensure to initialize with an empty token if we had none 196 | this._token = CancellationToken.None; 197 | } else if (this._token instanceof MutableToken) { 198 | // actually dispose 199 | this._token.dispose(); 200 | } 201 | } 202 | } 203 | 204 | 205 | export { 206 | createCancelablePromise 207 | } 208 | -------------------------------------------------------------------------------- /base/cancelablePromise/timeout.ts: -------------------------------------------------------------------------------- 1 | import { CancelablePromise, CancellationToken, createCancelablePromise } from "./cancelablePromise"; 2 | import * as errors from '../errors'; 3 | 4 | export function timeout(millis: number): CancelablePromise; 5 | export function timeout( 6 | millis: number, 7 | token: CancellationToken, 8 | ): Promise; 9 | export function timeout( 10 | millis: number, 11 | token?: CancellationToken, 12 | ): CancelablePromise | Promise { 13 | if (!token) { 14 | return createCancelablePromise(_token => timeout(millis, _token)); 15 | } 16 | 17 | return new Promise((resolve, reject) => { 18 | const handle = setTimeout(resolve, millis); 19 | token.onCancellationRequested(() => { 20 | clearTimeout(handle); 21 | reject(errors.canceled()); 22 | }); 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /base/disposable/disposable.ts: -------------------------------------------------------------------------------- 1 | import { DisposableStore } from "./disposableStore"; 2 | import { IDisposable } from "../interface"; 3 | import { markTracked, NoneDispose, trackDisposable } from "../utils"; 4 | 5 | 6 | 7 | export function isDisposable( 8 | thing: E, 9 | ): thing is E & IDisposable { 10 | return ( 11 | typeof ((thing)).dispose === 'function' && 12 | ((thing)).dispose.length === 0 13 | ); 14 | } 15 | 16 | export function toDisposable(fn: () => void): IDisposable { 17 | const self = trackDisposable({ 18 | dispose: () => { 19 | markTracked(self); 20 | fn(); 21 | }, 22 | }); 23 | return self; 24 | } 25 | 26 | export function combinedDisposable(...disposables: IDisposable[]): IDisposable { 27 | disposables.forEach(markTracked); 28 | return trackDisposable({ dispose: () => dispose(disposables) }); 29 | } 30 | 31 | 32 | // dispose 抽象类 33 | export abstract class Disposable implements IDisposable { 34 | static None = NoneDispose; // 判断是否为空的 dispose 对象 35 | 36 | private readonly _store = new DisposableStore(); // 存储可释放对象 37 | public dispose(): void { 38 | markTracked(this); 39 | this._store.dispose(); 40 | } 41 | 42 | protected _register(t: T): T { 43 | if (((t as any) as Disposable) === this) { 44 | throw new Error('Cannot register a disposable on itself!'); 45 | } 46 | return this._store.add(t); 47 | } 48 | } 49 | 50 | 51 | export function dispose(disposable: T): T; 52 | export function dispose( 53 | disposable: T | undefined, 54 | ): T | undefined; 55 | export function dispose(disposables: T[]): T[]; 56 | export function dispose( 57 | disposables: readonly T[], 58 | ): readonly T[]; 59 | export function dispose( 60 | disposables: T | T[] | undefined, 61 | ): T | T[] | undefined { 62 | if (Array.isArray(disposables)) { 63 | disposables.forEach(d => { 64 | if (d) { 65 | markTracked(d); 66 | d.dispose(); 67 | } 68 | }); 69 | return []; 70 | } else if (disposables) { 71 | markTracked(disposables); 72 | disposables.dispose(); 73 | return disposables; 74 | } else { 75 | return undefined; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /base/disposable/disposableStore.ts: -------------------------------------------------------------------------------- 1 | import { IDisposable } from "../interface"; 2 | import { markTracked } from "../utils"; 3 | 4 | export class DisposableStore implements IDisposable { 5 | // 存储需要 dispose 的对象 6 | private readonly _toDispose: Set = new Set(); 7 | 8 | // 是否已经全部 disaposed (释放) 完成 9 | private _isDisposed: boolean = false; 10 | 11 | // 释放所有 并标记为可追踪 12 | public dispose(): void { 13 | if (this._isDisposed) { 14 | return; 15 | } 16 | 17 | markTracked(this); 18 | this._isDisposed = true; 19 | this.clear(); 20 | } 21 | 22 | // 释放所有 disposes 但并不标记为可追踪 23 | public clear(): void { 24 | this._toDispose.forEach(item => item.dispose()); 25 | this._toDispose.clear(); 26 | } 27 | 28 | 29 | public add(t: T): T { 30 | if (!t) { 31 | return t; 32 | } 33 | if (((t as any) as DisposableStore) === this) { 34 | throw new Error('Cannot register a disposable on itself!'); 35 | } 36 | 37 | markTracked(t); 38 | if (this._isDisposed) { 39 | console.warn( 40 | new Error( 41 | 'Trying to add a disposable to a DisposableStore that has already been disposed of. The added object will be leaked!', 42 | ).stack, 43 | ); 44 | } else { 45 | this._toDispose.add(t); 46 | } 47 | 48 | return t; 49 | } 50 | 51 | 52 | } 53 | -------------------------------------------------------------------------------- /base/errors.ts: -------------------------------------------------------------------------------- 1 | export type ErrorListenerCallback = (error: any) => void; 2 | 3 | export type ErrorListenerUnbind = () => void; 4 | 5 | // Avoid circular dependency on EventEmitter by implementing a subset of the interface. 6 | export class ErrorHandler { 7 | private unexpectedErrorHandler: (e: any) => void; 8 | 9 | private readonly listeners: ErrorListenerCallback[]; 10 | 11 | constructor() { 12 | this.listeners = []; 13 | 14 | this.unexpectedErrorHandler = function (e: any) { 15 | setTimeout(() => { 16 | if (e.stack) { 17 | throw new Error(`${e.message}\n\n${e.stack}`); 18 | } 19 | 20 | throw e; 21 | }, 0); 22 | }; 23 | } 24 | 25 | public addListener(listener: ErrorListenerCallback): ErrorListenerUnbind { 26 | this.listeners.push(listener); 27 | 28 | return () => { 29 | this._removeListener(listener); 30 | }; 31 | } 32 | 33 | public setUnexpectedErrorHandler( 34 | newUnexpectedErrorHandler: (e: any) => void, 35 | ): void { 36 | this.unexpectedErrorHandler = newUnexpectedErrorHandler; 37 | } 38 | 39 | public getUnexpectedErrorHandler(): (e: any) => void { 40 | return this.unexpectedErrorHandler; 41 | } 42 | 43 | public onUnexpectedError(e: any): void { 44 | this.unexpectedErrorHandler(e); 45 | this.emit(e); 46 | } 47 | 48 | // For external errors, we don't want the listeners to be called 49 | public onUnexpectedExternalError(e: any): void { 50 | this.unexpectedErrorHandler(e); 51 | } 52 | 53 | private emit(e: any): void { 54 | this.listeners.forEach(listener => { 55 | listener(e); 56 | }); 57 | } 58 | 59 | private _removeListener(listener: ErrorListenerCallback): void { 60 | this.listeners.splice(this.listeners.indexOf(listener), 1); 61 | } 62 | } 63 | 64 | export const errorHandler = new ErrorHandler(); 65 | 66 | export function setUnexpectedErrorHandler( 67 | newUnexpectedErrorHandler: (e: any) => void, 68 | ): void { 69 | errorHandler.setUnexpectedErrorHandler(newUnexpectedErrorHandler); 70 | } 71 | 72 | export function onUnexpectedError(e: any): undefined { 73 | // ignore errors from cancelled promises 74 | if (!isPromiseCanceledError(e)) { 75 | errorHandler.onUnexpectedError(e); 76 | } 77 | return undefined; 78 | } 79 | 80 | export function onUnexpectedExternalError(e: any): undefined { 81 | // ignore errors from cancelled promises 82 | if (!isPromiseCanceledError(e)) { 83 | errorHandler.onUnexpectedExternalError(e); 84 | } 85 | return undefined; 86 | } 87 | 88 | export interface SerializedError { 89 | readonly $isError: true; 90 | readonly name: string; 91 | readonly message: string; 92 | readonly stack: string; 93 | } 94 | 95 | export function transformErrorForSerialization(error: Error): SerializedError; 96 | export function transformErrorForSerialization(error: any): any; 97 | export function transformErrorForSerialization(error: any): any { 98 | if (error instanceof Error) { 99 | const { name, message } = error; 100 | const stack: string = (error).stacktrace || (error).stack; 101 | return { 102 | $isError: true, 103 | name, 104 | message, 105 | stack, 106 | }; 107 | } 108 | 109 | // return as is 110 | return error; 111 | } 112 | 113 | // see https://github.com/v8/v8/wiki/Stack%20Trace%20API#basic-stack-traces 114 | export interface V8CallSite { 115 | getThis(): any; 116 | getTypeName(): string; 117 | getFunction(): string; 118 | getFunctionName(): string; 119 | getMethodName(): string; 120 | getFileName(): string; 121 | getLineNumber(): number; 122 | getColumnNumber(): number; 123 | getEvalOrigin(): string; 124 | isToplevel(): boolean; 125 | isEval(): boolean; 126 | isNative(): boolean; 127 | isConstructor(): boolean; 128 | toString(): string; 129 | } 130 | 131 | const canceledName = 'Canceled'; 132 | 133 | /** 134 | * Checks if the given error is a promise in canceled state 135 | */ 136 | export function isPromiseCanceledError(error: any): boolean { 137 | return ( 138 | error instanceof Error && 139 | error.name === canceledName && 140 | error.message === canceledName 141 | ); 142 | } 143 | 144 | /** 145 | * Returns an error that signals cancellation. 146 | */ 147 | export function canceled(): Error { 148 | const error = new Error(canceledName); 149 | error.name = error.message; 150 | return error; 151 | } 152 | 153 | export function illegalArgument(name?: string): Error { 154 | if (name) { 155 | return new Error(`Illegal argument: ${name}`); 156 | } else { 157 | return new Error('Illegal argument'); 158 | } 159 | } 160 | 161 | export function illegalState(name?: string): Error { 162 | if (name) { 163 | return new Error(`Illegal state: ${name}`); 164 | } else { 165 | return new Error('Illegal state'); 166 | } 167 | } 168 | 169 | export function readonly(name?: string): Error { 170 | return name 171 | ? new Error(`readonly property '${name} cannot be changed'`) 172 | : new Error('readonly property cannot be changed'); 173 | } 174 | 175 | export function disposed(what: string): Error { 176 | const result = new Error(`${what} has been disposed`); 177 | result.name = 'DISPOSED'; 178 | return result; 179 | } 180 | 181 | export function getErrorMessage(err: any): string { 182 | if (!err) { 183 | return 'Error'; 184 | } 185 | 186 | if (err.message) { 187 | return err.message; 188 | } 189 | 190 | if (err.stack) { 191 | return err.stack.split('\n')[0]; 192 | } 193 | 194 | return String(err); 195 | } 196 | 197 | export class NotImplementedError extends Error { 198 | constructor(message?: string) { 199 | super('NotImplemented'); 200 | if (message) { 201 | this.message = message; 202 | } 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /base/event.ts: -------------------------------------------------------------------------------- 1 | import { combinedDisposable, Disposable } from "./disposable/disposable"; 2 | import { DisposableStore } from "./disposable/disposableStore"; 3 | import { onUnexpectedError } from "./errors"; 4 | import { IDisposable } from "./interface"; 5 | import { LeakageMonitor, _globalLeakWarningThreshold } from "./leakageMonitor"; 6 | import { LinkedList } from "./linkedList"; 7 | 8 | 9 | // 定义事件类型 10 | export type Event = ( 11 | listener: (e: T) => any, 12 | thisArgs?: any, 13 | disposables?: IDisposable[] | DisposableStore, 14 | ) => IDisposable; 15 | 16 | export namespace Event { 17 | export const None: Event = () => Disposable.None; 18 | 19 | /** 20 | * Given an event, returns another event which only fires once. 21 | */ 22 | export function once(event: Event): Event { 23 | return (listener, thisArgs = null, disposables?) => { 24 | // we need this, in case the event fires during the listener call 25 | let didFire = false; 26 | let result: IDisposable; 27 | result = event(e => { 28 | if (didFire) { 29 | return; 30 | } else if (result) { 31 | result.dispose(); 32 | } else { 33 | didFire = true; 34 | } 35 | 36 | return listener.call(thisArgs, e); 37 | }, null, disposables); 38 | 39 | if (didFire) { 40 | result.dispose(); 41 | } 42 | 43 | return result; 44 | }; 45 | } 46 | 47 | /** 48 | * Given an event and a `map` function, returns another event which maps each element 49 | * through the mapping function. 50 | */ 51 | export function map(event: Event, map: (i: I) => O): Event { 52 | return snapshot((listener, thisArgs = null, disposables?) => event(i => listener.call(thisArgs, map(i)), null, disposables)); 53 | } 54 | 55 | /** 56 | * Given an event and an `each` function, returns another identical event and calls 57 | * the `each` function per each element. 58 | */ 59 | export function forEach(event: Event, each: (i: I) => void): Event { 60 | return snapshot((listener, thisArgs = null, disposables?) => event(i => { each(i); listener.call(thisArgs, i); }, null, disposables)); 61 | } 62 | 63 | /** 64 | * Given an event and a `filter` function, returns another event which emits those 65 | * elements for which the `filter` function returns `true`. 66 | */ 67 | export function filter(event: Event, filter: (e: T) => boolean): Event; 68 | export function filter(event: Event, filter: (e: T | R) => e is R): Event; 69 | export function filter(event: Event, filter: (e: T) => boolean): Event { 70 | return snapshot((listener, thisArgs = null, disposables?) => event(e => filter(e) && listener.call(thisArgs, e), null, disposables)); 71 | } 72 | 73 | /** 74 | * Given an event, returns the same event but typed as `Event`. 75 | */ 76 | export function signal(event: Event): Event { 77 | return event as Event as Event; 78 | } 79 | 80 | /** 81 | * Given a collection of events, returns a single event which emits 82 | * whenever any of the provided events emit. 83 | */ 84 | export function any(...events: Event[]): Event; 85 | export function any(...events: Event[]): Event; 86 | export function any(...events: Event[]): Event { 87 | return (listener, thisArgs = null, disposables?) => combinedDisposable(...events.map(event => event(e => listener.call(thisArgs, e), null, disposables))); 88 | } 89 | 90 | /** 91 | * Given an event and a `merge` function, returns another event which maps each element 92 | * and the cumulative result through the `merge` function. Similar to `map`, but with memory. 93 | */ 94 | export function reduce(event: Event, merge: (last: O | undefined, event: I) => O, initial?: O): Event { 95 | let output: O | undefined = initial; 96 | 97 | return map(event, e => { 98 | output = merge(output, e); 99 | return output; 100 | }); 101 | } 102 | 103 | /** 104 | * Given a chain of event processing functions (filter, map, etc), each 105 | * function will be invoked per event & per listener. Snapshotting an event 106 | * chain allows each function to be invoked just once per event. 107 | */ 108 | export function snapshot(event: Event): Event { 109 | let listener: IDisposable; 110 | const emitter = new Emitter({ 111 | onFirstListenerAdd() { 112 | listener = event(emitter.fire, emitter); 113 | }, 114 | onLastListenerRemove() { 115 | listener.dispose(); 116 | } 117 | }); 118 | 119 | return emitter.event; 120 | } 121 | 122 | /** 123 | * Debounces the provided event, given a `merge` function. 124 | * 125 | * @param event The input event. 126 | * @param merge The reducing function. 127 | * @param delay The debouncing delay in millis. 128 | * @param leading Whether the event should fire in the leading phase of the timeout. 129 | * @param leakWarningThreshold The leak warning threshold override. 130 | */ 131 | export function debounce(event: Event, merge: (last: T | undefined, event: T) => T, delay?: number, leading?: boolean, leakWarningThreshold?: number): Event; 132 | export function debounce(event: Event, merge: (last: O | undefined, event: I) => O, delay?: number, leading?: boolean, leakWarningThreshold?: number): Event; 133 | export function debounce(event: Event, merge: (last: O | undefined, event: I) => O, delay: number = 100, leading = false, leakWarningThreshold?: number): Event { 134 | 135 | let subscription: IDisposable; 136 | let output: O | undefined = undefined; 137 | let handle: any = undefined; 138 | let numDebouncedCalls = 0; 139 | 140 | const emitter = new Emitter({ 141 | leakWarningThreshold, 142 | onFirstListenerAdd() { 143 | subscription = event(cur => { 144 | numDebouncedCalls++; 145 | output = merge(output, cur); 146 | 147 | if (leading && !handle) { 148 | emitter.fire(output); 149 | output = undefined; 150 | } 151 | 152 | clearTimeout(handle); 153 | handle = setTimeout(() => { 154 | const _output = output; 155 | output = undefined; 156 | handle = undefined; 157 | if (!leading || numDebouncedCalls > 1) { 158 | emitter.fire(_output!); 159 | } 160 | 161 | numDebouncedCalls = 0; 162 | }, delay); 163 | }); 164 | }, 165 | onLastListenerRemove() { 166 | subscription.dispose(); 167 | } 168 | }); 169 | 170 | return emitter.event; 171 | } 172 | 173 | /** 174 | * Given an event, it returns another event which fires only once and as soon as 175 | * the input event emits. The event data is the number of millis it took for the 176 | * event to fire. 177 | */ 178 | export function stopwatch(event: Event): Event { 179 | const start = new Date().getTime(); 180 | return map(once(event), _ => new Date().getTime() - start); 181 | } 182 | 183 | /** 184 | * Given an event, it returns another event which fires only when the event 185 | * element changes. 186 | */ 187 | export function latch(event: Event): Event { 188 | let firstCall = true; 189 | let cache: T; 190 | 191 | return filter(event, value => { 192 | const shouldEmit = firstCall || value !== cache; 193 | firstCall = false; 194 | cache = value; 195 | return shouldEmit; 196 | }); 197 | } 198 | 199 | /** 200 | * Buffers the provided event until a first listener comes 201 | * along, at which point fire all the events at once and 202 | * pipe the event from then on. 203 | * 204 | * ```typescript 205 | * const emitter = new Emitter(); 206 | * const event = emitter.event; 207 | * const bufferedEvent = buffer(event); 208 | * 209 | * emitter.fire(1); 210 | * emitter.fire(2); 211 | * emitter.fire(3); 212 | * // nothing... 213 | * 214 | * const listener = bufferedEvent(num => console.log(num)); 215 | * // 1, 2, 3 216 | * 217 | * emitter.fire(4); 218 | * // 4 219 | * ``` 220 | */ 221 | export function buffer(event: Event, nextTick = false, _buffer: T[] = []): Event { 222 | let buffer: T[] | null = _buffer.slice(); 223 | 224 | let listener: IDisposable | null = event(e => { 225 | if (buffer) { 226 | buffer.push(e); 227 | } else { 228 | emitter.fire(e); 229 | } 230 | }); 231 | 232 | const flush = () => { 233 | if (buffer) { 234 | buffer.forEach(e => emitter.fire(e)); 235 | } 236 | buffer = null; 237 | }; 238 | 239 | const emitter = new Emitter({ 240 | onFirstListenerAdd() { 241 | if (!listener) { 242 | listener = event(e => emitter.fire(e)); 243 | } 244 | }, 245 | 246 | onFirstListenerDidAdd() { 247 | if (buffer) { 248 | if (nextTick) { 249 | setTimeout(flush); 250 | } else { 251 | flush(); 252 | } 253 | } 254 | }, 255 | 256 | onLastListenerRemove() { 257 | if (listener) { 258 | listener.dispose(); 259 | } 260 | listener = null; 261 | } 262 | }); 263 | 264 | return emitter.event; 265 | } 266 | 267 | export interface IChainableEvent { 268 | event: Event; 269 | map(fn: (i: T) => O): IChainableEvent; 270 | forEach(fn: (i: T) => void): IChainableEvent; 271 | filter(fn: (e: T) => boolean): IChainableEvent; 272 | filter(fn: (e: T | R) => e is R): IChainableEvent; 273 | reduce(merge: (last: R | undefined, event: T) => R, initial?: R): IChainableEvent; 274 | latch(): IChainableEvent; 275 | debounce(merge: (last: T | undefined, event: T) => T, delay?: number, leading?: boolean, leakWarningThreshold?: number): IChainableEvent; 276 | debounce(merge: (last: R | undefined, event: T) => R, delay?: number, leading?: boolean, leakWarningThreshold?: number): IChainableEvent; 277 | on(listener: (e: T) => any, thisArgs?: any, disposables?: IDisposable[] | DisposableStore): IDisposable; 278 | once(listener: (e: T) => any, thisArgs?: any, disposables?: IDisposable[]): IDisposable; 279 | } 280 | 281 | class ChainableEvent implements IChainableEvent { 282 | 283 | constructor(readonly event: Event) { } 284 | 285 | map(fn: (i: T) => O): IChainableEvent { 286 | return new ChainableEvent(map(this.event, fn)); 287 | } 288 | 289 | forEach(fn: (i: T) => void): IChainableEvent { 290 | return new ChainableEvent(forEach(this.event, fn)); 291 | } 292 | 293 | filter(fn: (e: T) => boolean): IChainableEvent; 294 | filter(fn: (e: T | R) => e is R): IChainableEvent; 295 | filter(fn: (e: T) => boolean): IChainableEvent { 296 | return new ChainableEvent(filter(this.event, fn)); 297 | } 298 | 299 | reduce(merge: (last: R | undefined, event: T) => R, initial?: R): IChainableEvent { 300 | return new ChainableEvent(reduce(this.event, merge, initial)); 301 | } 302 | 303 | latch(): IChainableEvent { 304 | return new ChainableEvent(latch(this.event)); 305 | } 306 | 307 | debounce(merge: (last: T | undefined, event: T) => T, delay?: number, leading?: boolean, leakWarningThreshold?: number): IChainableEvent; 308 | debounce(merge: (last: R | undefined, event: T) => R, delay?: number, leading?: boolean, leakWarningThreshold?: number): IChainableEvent; 309 | debounce(merge: (last: R | undefined, event: T) => R, delay: number = 100, leading = false, leakWarningThreshold?: number): IChainableEvent { 310 | return new ChainableEvent(debounce(this.event, merge, delay, leading, leakWarningThreshold)); 311 | } 312 | 313 | on(listener: (e: T) => any, thisArgs: any, disposables: IDisposable[] | DisposableStore) { 314 | return this.event(listener, thisArgs, disposables); 315 | } 316 | 317 | once(listener: (e: T) => any, thisArgs: any, disposables: IDisposable[]) { 318 | return once(this.event)(listener, thisArgs, disposables); 319 | } 320 | } 321 | 322 | export function chain(event: Event): IChainableEvent { 323 | return new ChainableEvent(event); 324 | } 325 | 326 | export interface NodeEventEmitter { 327 | on(event: string | symbol, listener: Function): unknown; 328 | removeListener(event: string | symbol, listener: Function): unknown; 329 | } 330 | 331 | export function fromNodeEventEmitter(emitter: NodeEventEmitter, eventName: string, map: (...args: any[]) => T = id => id): Event { 332 | const fn = (...args: any[]) => result.fire(map(...args)); 333 | const onFirstListenerAdd = () => emitter.on(eventName, fn); 334 | const onLastListenerRemove = () => emitter.removeListener(eventName, fn); 335 | const result = new Emitter({ onFirstListenerAdd, onLastListenerRemove }); 336 | 337 | return result.event; 338 | } 339 | 340 | export interface DOMEventEmitter { 341 | addEventListener(event: string | symbol, listener: Function): void; 342 | removeEventListener(event: string | symbol, listener: Function): void; 343 | } 344 | 345 | export function fromDOMEventEmitter(emitter: DOMEventEmitter, eventName: string, map: (...args: any[]) => T = id => id): Event { 346 | const fn = (...args: any[]) => result.fire(map(...args)); 347 | const onFirstListenerAdd = () => emitter.addEventListener(eventName, fn); 348 | const onLastListenerRemove = () => emitter.removeEventListener(eventName, fn); 349 | const result = new Emitter({ onFirstListenerAdd, onLastListenerRemove }); 350 | 351 | return result.event; 352 | } 353 | 354 | export function fromPromise(promise: Promise): Event { 355 | const emitter = new Emitter(); 356 | let shouldEmit = false; 357 | 358 | promise 359 | .then(undefined, () => null) 360 | .then(() => { 361 | if (!shouldEmit) { 362 | setTimeout(() => emitter.fire(undefined), 0); 363 | } else { 364 | emitter.fire(undefined); 365 | } 366 | }); 367 | 368 | shouldEmit = true; 369 | return emitter.event; 370 | } 371 | 372 | export function toPromise(event: Event): Promise { 373 | return new Promise(c => once(event)(c)); 374 | } 375 | } 376 | 377 | 378 | export interface EmitterOptions { 379 | onFirstListenerAdd?: Function; // 第一次注册监听 380 | onFirstListenerDidAdd?: Function; // 第一次监听注册成功后 381 | onListenerDidAdd?: Function; // 监听注册后 382 | onLastListenerRemove?: Function; // 最后一个监听移除 383 | leakWarningThreshold?: number; 384 | } 385 | 386 | type Listener = [(e: T) => void, any] | ((e: T) => void); 387 | 388 | export class Emitter { 389 | private static readonly _noop = function () {}; // 空操作 390 | 391 | private readonly _options?: EmitterOptions; 392 | 393 | private readonly _leakageMon?: LeakageMonitor; // 泄漏监控器 394 | 395 | private _disposed = false; // 是否已经释放 396 | 397 | private _event?: Event; 398 | 399 | private _deliveryQueue?: LinkedList<[Listener, T]>; // 分发队列 LinkedList 插入或者删除元素来说,操作方便,性能高 400 | 401 | protected _listeners?: LinkedList>; // listener 队列 402 | 403 | constructor(options?: EmitterOptions) { 404 | this._options = options; 405 | 406 | // 如果设置了消息监控,则进行监控提示,否则不提示 407 | this._leakageMon = 408 | _globalLeakWarningThreshold > 0 409 | ? new LeakageMonitor( 410 | this._options && this._options.leakWarningThreshold, 411 | ) 412 | : undefined; 413 | } 414 | 415 | // 初始化事件函数 416 | // 1. 注册各种事件监听生命周期回调:第一个监听添加、最后一个监听移除等。 417 | // 2. 返回事件取消监听函数,本质是从 linkedlist 中 移除对应监听。 418 | get event(): Event { 419 | if (!this._event) { 420 | this._event = ( 421 | listener: (e: T) => any, 422 | thisArgs?: any, // 指定事件执行对象 423 | disposables?: IDisposable[] | DisposableStore,) => { 424 | if (!this._listeners) { 425 | this._listeners = new LinkedList(); 426 | } 427 | 428 | const firstListener = this._listeners.isEmpty(); 429 | 430 | // 第一次监听,提供监听函数回调 431 | if ( 432 | firstListener && 433 | this._options && 434 | this._options.onFirstListenerAdd 435 | ) { 436 | this._options.onFirstListenerAdd(this); 437 | } 438 | 439 | 440 | const remove = this._listeners.push( 441 | !thisArgs ? listener : [listener, thisArgs], 442 | ); 443 | 444 | // 第一个事件添加完成后回调 445 | if ( 446 | firstListener && 447 | this._options && 448 | this._options.onFirstListenerDidAdd 449 | ) { 450 | this._options.onFirstListenerDidAdd(this); 451 | } 452 | 453 | // 事件添加完成回调 454 | if (this._options && this._options.onListenerDidAdd) { 455 | this._options.onListenerDidAdd(this, listener, thisArgs); 456 | } 457 | 458 | let removeMonitor: (() => void) | undefined; 459 | 460 | if (this._leakageMon) { 461 | removeMonitor = this._leakageMon.check(this._listeners.size); 462 | } 463 | 464 | // 事件监听后返回结果 465 | const result: IDisposable = { 466 | dispose: () => { 467 | if (removeMonitor) { 468 | removeMonitor(); 469 | } 470 | result.dispose = Emitter._noop; 471 | if (!this._disposed) { 472 | remove(); // 移除当前监听事件节点 473 | if (this._options && this._options.onLastListenerRemove) { 474 | const hasListeners = 475 | this._listeners && !this._listeners.isEmpty(); 476 | if (!hasListeners) { 477 | this._options.onLastListenerRemove(this); 478 | } 479 | } 480 | } 481 | } 482 | } 483 | 484 | if (disposables instanceof DisposableStore) { 485 | disposables.add(result); 486 | } else if (Array.isArray(disposables)) { 487 | disposables.push(result); 488 | } 489 | 490 | return result; 491 | 492 | } 493 | 494 | } 495 | return this._event as Event; 496 | 497 | } 498 | 499 | // 触发事件 500 | fire(event: T): void { 501 | if (this._listeners) { 502 | 503 | if (!this._deliveryQueue) { 504 | this._deliveryQueue = new LinkedList(); 505 | } 506 | 507 | for ( 508 | let iter = this._listeners.iterator(), e = iter.next(); 509 | !e.done; 510 | e = iter.next() 511 | ) { 512 | // 遍历 _listeners, 将所有的监听和事件对应的参数放一起 513 | this._deliveryQueue.push([e.value, event]); 514 | } 515 | 516 | while (this._deliveryQueue.size > 0) { 517 | const [listener, event] = this._deliveryQueue.shift()!; 518 | try { 519 | if (typeof listener === 'function') { 520 | listener.call(undefined, event); 521 | } else { 522 | listener[0].call(listener[1], event); 523 | } 524 | } catch (e) { 525 | onUnexpectedError(e); 526 | } 527 | } 528 | } 529 | } 530 | 531 | 532 | // 解除所有事件监听 533 | dispose() { 534 | if (this._listeners) { 535 | this._listeners.clear(); 536 | } 537 | if (this._deliveryQueue) { 538 | this._deliveryQueue.clear(); 539 | } 540 | if (this._leakageMon) { 541 | this._leakageMon.dispose(); 542 | } 543 | this._disposed = true; 544 | } 545 | } 546 | 547 | -------------------------------------------------------------------------------- /base/interface.ts: -------------------------------------------------------------------------------- 1 | // only have a dispose way to release all listeners 2 | export interface IDisposable { 3 | dispose(): void; 4 | } 5 | 6 | 7 | export interface IdleDeadline { 8 | readonly didTimeout: boolean; 9 | timeRemaining(): DOMHighResTimeStamp; 10 | } 11 | -------------------------------------------------------------------------------- /base/iterator.ts: -------------------------------------------------------------------------------- 1 | export interface IteratorDefinedResult { 2 | readonly done: false; 3 | readonly value: T; 4 | } 5 | export interface IteratorUndefinedResult { 6 | readonly done: true; 7 | readonly value: undefined; 8 | } 9 | export const FIN: IteratorUndefinedResult = { done: true, value: undefined }; 10 | export type IteratorResult = 11 | | IteratorDefinedResult 12 | | IteratorUndefinedResult; 13 | 14 | export interface Iterator { 15 | next(): IteratorResult; 16 | } 17 | 18 | export module Iterator { 19 | const _empty: Iterator = { 20 | next() { 21 | return FIN; 22 | }, 23 | }; 24 | 25 | export function empty(): Iterator { 26 | return _empty; 27 | } 28 | 29 | export function single(value: T): Iterator { 30 | let done = false; 31 | 32 | return { 33 | next(): IteratorResult { 34 | if (done) { 35 | return FIN; 36 | } 37 | 38 | done = true; 39 | return { done: false, value }; 40 | }, 41 | }; 42 | } 43 | 44 | export function fromArray( 45 | array: T[], 46 | index = 0, 47 | length = array.length, 48 | ): Iterator { 49 | return { 50 | next(): IteratorResult { 51 | if (index >= length) { 52 | return FIN; 53 | } 54 | 55 | return { done: false, value: array[index++] }; 56 | }, 57 | }; 58 | } 59 | 60 | export function from( 61 | elements: Iterator | T[] | undefined, 62 | ): Iterator { 63 | if (!elements) { 64 | return empty(); 65 | } else if (Array.isArray(elements)) { 66 | return fromArray(elements); 67 | } else { 68 | return elements; 69 | } 70 | } 71 | 72 | export function map( 73 | iterator: Iterator, 74 | fn: (t: T) => R, 75 | ): Iterator { 76 | return { 77 | next() { 78 | const element = iterator.next(); 79 | if (element.done) { 80 | return FIN; 81 | } else { 82 | return { done: false, value: fn(element.value) }; 83 | } 84 | }, 85 | }; 86 | } 87 | 88 | export function filter( 89 | iterator: Iterator, 90 | fn: (t: T) => boolean, 91 | ): Iterator { 92 | return { 93 | next() { 94 | while (true) { 95 | const element = iterator.next(); 96 | if (element.done) { 97 | return FIN; 98 | } 99 | if (fn(element.value)) { 100 | return { done: false, value: element.value }; 101 | } 102 | } 103 | }, 104 | }; 105 | } 106 | 107 | export function forEach(iterator: Iterator, fn: (t: T) => void): void { 108 | for (let next = iterator.next(); !next.done; next = iterator.next()) { 109 | fn(next.value); 110 | } 111 | } 112 | 113 | export function collect( 114 | iterator: Iterator, 115 | atMost: number = Number.POSITIVE_INFINITY, 116 | ): T[] { 117 | const result: T[] = []; 118 | 119 | if (atMost === 0) { 120 | return result; 121 | } 122 | 123 | let i = 0; 124 | 125 | for (let next = iterator.next(); !next.done; next = iterator.next()) { 126 | result.push(next.value); 127 | 128 | if (++i >= atMost) { 129 | break; 130 | } 131 | } 132 | 133 | return result; 134 | } 135 | 136 | export function concat(...iterators: Array>): Iterator { 137 | let i = 0; 138 | 139 | return { 140 | next() { 141 | if (i >= iterators.length) { 142 | return FIN; 143 | } 144 | 145 | const iterator = iterators[i]; 146 | const result = iterator.next(); 147 | 148 | if (result.done) { 149 | i++; 150 | return this.next(); 151 | } 152 | 153 | return result; 154 | }, 155 | }; 156 | } 157 | } 158 | 159 | export type ISequence = Iterator | T[]; 160 | 161 | export function getSequenceIterator(arg: Iterator | T[]): Iterator { 162 | if (Array.isArray(arg)) { 163 | return Iterator.fromArray(arg); 164 | } else { 165 | return arg; 166 | } 167 | } 168 | 169 | export interface INextIterator { 170 | next(): T | null; 171 | } 172 | 173 | export class ArrayIterator implements INextIterator { 174 | protected start: number; 175 | 176 | protected end: number; 177 | 178 | protected index: number; 179 | 180 | private readonly items: T[]; 181 | 182 | constructor( 183 | items: T[], 184 | start = 0, 185 | end: number = items.length, 186 | index = start - 1, 187 | ) { 188 | this.items = items; 189 | this.start = start; 190 | this.end = end; 191 | this.index = index; 192 | } 193 | 194 | public first(): T | null { 195 | this.index = this.start; 196 | return this.current(); 197 | } 198 | 199 | public next(): T | null { 200 | this.index = Math.min(this.index + 1, this.end); 201 | return this.current(); 202 | } 203 | 204 | protected current(): T | null { 205 | if (this.index === this.start - 1 || this.index === this.end) { 206 | return null; 207 | } 208 | 209 | return this.items[this.index]; 210 | } 211 | } 212 | 213 | export class ArrayNavigator 214 | extends ArrayIterator 215 | implements INavigator { 216 | constructor( 217 | items: T[], 218 | start = 0, 219 | end: number = items.length, 220 | index = start - 1, 221 | ) { 222 | super(items, start, end, index); 223 | } 224 | 225 | public current(): T | null { 226 | return super.current(); 227 | } 228 | 229 | public previous(): T | null { 230 | this.index = Math.max(this.index - 1, this.start - 1); 231 | return this.current(); 232 | } 233 | 234 | public first(): T | null { 235 | this.index = this.start; 236 | return this.current(); 237 | } 238 | 239 | public last(): T | null { 240 | this.index = this.end - 1; 241 | return this.current(); 242 | } 243 | 244 | public parent(): T | null { 245 | return null; 246 | } 247 | } 248 | 249 | export class MappedIterator implements INextIterator { 250 | constructor( 251 | protected iterator: INextIterator, 252 | protected fn: (item: T | null) => R, 253 | ) { 254 | // noop 255 | } 256 | 257 | next() { 258 | return this.fn(this.iterator.next()); 259 | } 260 | } 261 | 262 | export interface INavigator extends INextIterator { 263 | current(): T | null; 264 | previous(): T | null; 265 | parent(): T | null; 266 | first(): T | null; 267 | last(): T | null; 268 | next(): T | null; 269 | } 270 | 271 | export class MappedNavigator 272 | extends MappedIterator 273 | implements INavigator { 274 | constructor(protected navigator: INavigator, fn: (item: T | null) => R) { 275 | super(navigator, fn); 276 | } 277 | 278 | current() { 279 | return this.fn(this.navigator.current()); 280 | } 281 | 282 | previous() { 283 | return this.fn(this.navigator.previous()); 284 | } 285 | 286 | parent() { 287 | return this.fn(this.navigator.parent()); 288 | } 289 | 290 | first() { 291 | return this.fn(this.navigator.first()); 292 | } 293 | 294 | last() { 295 | return this.fn(this.navigator.last()); 296 | } 297 | 298 | next() { 299 | return this.fn(this.navigator.next()); 300 | } 301 | } 302 | -------------------------------------------------------------------------------- /base/leakageMonitor.ts: -------------------------------------------------------------------------------- 1 | import { IDisposable } from "./interface"; 2 | 3 | 4 | export let _globalLeakWarningThreshold = -1; // 泄漏监控提示门槛 5 | export function setGlobalLeakWarningThreshold(n: number): IDisposable { 6 | const oldValue = _globalLeakWarningThreshold; 7 | _globalLeakWarningThreshold = n; 8 | return { 9 | dispose() { 10 | _globalLeakWarningThreshold = oldValue; 11 | }, 12 | }; 13 | } 14 | 15 | 16 | export class LeakageMonitor { 17 | private _stacks: Map | undefined; 18 | 19 | private _warnCountdown = 0; 20 | 21 | constructor( 22 | readonly customThreshold?: number, // 自定义泄漏门槛提示 23 | readonly name: string = Math.random().toString(18).slice(2, 5), 24 | ) {} 25 | 26 | dispose(): void { 27 | if (this._stacks) { 28 | this._stacks.clear(); 29 | } 30 | } 31 | 32 | check(listenerCount: number): undefined | (() => void) { 33 | let threshold = _globalLeakWarningThreshold; 34 | if (typeof this.customThreshold === 'number') { 35 | threshold = this.customThreshold; 36 | } 37 | 38 | if (threshold <= 0 || listenerCount < threshold) { 39 | return undefined; 40 | } 41 | 42 | if (!this._stacks) { 43 | this._stacks = new Map(); 44 | } 45 | const stack = new Error().stack!.split('\n').slice(3).join('\n'); 46 | const count = this._stacks.get(stack) || 0; 47 | this._stacks.set(stack, count + 1); 48 | this._warnCountdown -= 1; 49 | 50 | if (this._warnCountdown <= 0) { 51 | // only warn on first exceed and then every time the limit 52 | // is exceeded by 50% again 53 | this._warnCountdown = threshold * 0.5; 54 | 55 | // find most frequent listener and print warning 56 | let topStack: string; 57 | let topCount = 0; 58 | this._stacks.forEach((count, stack) => { 59 | if (!topStack || topCount < count) { 60 | topStack = stack; 61 | topCount = count; 62 | } 63 | }); 64 | 65 | console.warn( 66 | `[${this.name}] potential listener LEAK detected, having ${listenerCount} listeners already. MOST frequent listener (${topCount}):`, 67 | ); 68 | console.warn(topStack!); 69 | } 70 | 71 | return () => { 72 | const count = this._stacks!.get(stack) || 0; 73 | this._stacks!.set(stack, count - 1); 74 | }; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /base/linkedList.ts: -------------------------------------------------------------------------------- 1 | import { Iterator, IteratorResult, FIN } from './iterator'; 2 | 3 | class Node { 4 | static readonly Undefined: Node = new Node(undefined); 5 | 6 | element: E; 7 | 8 | next: Node; 9 | 10 | prev: Node; 11 | 12 | constructor(element: E) { 13 | this.element = element; 14 | this.next = Node.Undefined; 15 | this.prev = Node.Undefined; 16 | } 17 | } 18 | 19 | export class LinkedList { 20 | private _first: Node = Node.Undefined; 21 | 22 | private _last: Node = Node.Undefined; 23 | 24 | private _size: number = 0; 25 | 26 | get size(): number { 27 | return this._size; 28 | } 29 | 30 | isEmpty(): boolean { 31 | return this._first === Node.Undefined; 32 | } 33 | 34 | clear(): void { 35 | this._first = Node.Undefined; 36 | this._last = Node.Undefined; 37 | this._size = 0; 38 | } 39 | 40 | unshift(element: E): () => void { 41 | return this._insert(element, false); 42 | } 43 | 44 | push(element: E): () => void { 45 | return this._insert(element, true); 46 | } 47 | 48 | shift(): E | undefined { 49 | if (this._first === Node.Undefined) { 50 | return undefined; 51 | } else { 52 | const res = this._first.element; 53 | this._remove(this._first); 54 | return res; 55 | } 56 | } 57 | 58 | pop(): E | undefined { 59 | if (this._last === Node.Undefined) { 60 | return undefined; 61 | } else { 62 | const res = this._last.element; 63 | this._remove(this._last); 64 | return res; 65 | } 66 | } 67 | 68 | iterator(): Iterator { 69 | let element: { done: false; value: E }; 70 | let node = this._first; 71 | return { 72 | next(): IteratorResult { 73 | if (node === Node.Undefined) { 74 | return FIN; 75 | } 76 | 77 | if (!element) { 78 | element = { done: false, value: node.element }; 79 | } else { 80 | element.value = node.element; 81 | } 82 | node = node.next; 83 | return element; 84 | }, 85 | }; 86 | } 87 | 88 | toArray(): E[] { 89 | const result: E[] = []; 90 | for (let node = this._first; node !== Node.Undefined; node = node.next) { 91 | result.push(node.element); 92 | } 93 | return result; 94 | } 95 | 96 | private _insert(element: E, atTheEnd: boolean): () => void { 97 | const newNode = new Node(element); 98 | if (this._first === Node.Undefined) { 99 | this._first = newNode; 100 | this._last = newNode; 101 | } else if (atTheEnd) { 102 | // push 103 | const oldLast = this._last; 104 | this._last = newNode; 105 | newNode.prev = oldLast; 106 | oldLast.next = newNode; 107 | } else { 108 | // unshift 109 | const oldFirst = this._first; 110 | this._first = newNode; 111 | newNode.next = oldFirst; 112 | oldFirst.prev = newNode; 113 | } 114 | this._size += 1; 115 | 116 | let didRemove = false; 117 | return () => { 118 | if (!didRemove) { 119 | didRemove = true; 120 | this._remove(newNode); 121 | } 122 | }; 123 | } 124 | 125 | private _remove(node: Node): void { 126 | if (node.prev !== Node.Undefined && node.next !== Node.Undefined) { 127 | // middle 128 | const anchor = node.prev; 129 | anchor.next = node.next; 130 | node.next.prev = anchor; 131 | } else if (node.prev === Node.Undefined && node.next === Node.Undefined) { 132 | // only node 133 | this._first = Node.Undefined; 134 | this._last = Node.Undefined; 135 | } else if (node.next === Node.Undefined) { 136 | // last 137 | this._last = this._last.prev; 138 | this._last.next = Node.Undefined; 139 | } else if (node.prev === Node.Undefined) { 140 | // first 141 | this._first = this._first.next; 142 | this._first.prev = Node.Undefined; 143 | } 144 | 145 | // done 146 | this._size -= 1; 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /base/utils.ts: -------------------------------------------------------------------------------- 1 | 2 | import { IDisposable } from "./interface"; 3 | 4 | // 是否追踪 disposables 5 | const TRACK_DISPOSABLES = false; 6 | 7 | const __is_disposable_tracked__ = '__is_disposable_tracked__'; 8 | 9 | export const NoneDispose = Object.freeze({ dispose() {} }); 10 | 11 | // 标记该对象为可追踪 disposable 对象 12 | export function markTracked(x: T): void { 13 | if (!TRACK_DISPOSABLES) { 14 | return; 15 | } 16 | 17 | if (x && x !== NoneDispose) { 18 | try { 19 | (x as any)[__is_disposable_tracked__] = true; 20 | } catch { 21 | // noop 22 | } 23 | } 24 | } 25 | 26 | 27 | export function trackDisposable(x: T): T { 28 | if (!TRACK_DISPOSABLES) { 29 | return x; 30 | } 31 | 32 | const stack = new Error('Potentially leaked disposable').stack!; 33 | setTimeout(() => { 34 | if (!(x as any)[__is_disposable_tracked__]) { 35 | console.log(stack); 36 | } 37 | }, 3000); 38 | return x; 39 | } 40 | -------------------------------------------------------------------------------- /core/common/ipc.electron.ts: -------------------------------------------------------------------------------- 1 | import { IMessagePassingProtocol } from '../common/ipc'; 2 | import { Event } from '../../base/event'; 3 | import { VSBuffer } from '../../base/buffer'; 4 | 5 | export interface Sender { 6 | send(channel: string, msg: Buffer | null): void; 7 | } 8 | 9 | export class Protocol implements IMessagePassingProtocol { 10 | constructor( 11 | private readonly sender: Sender, 12 | readonly onMessage: Event, 13 | ) {} 14 | 15 | send(message: VSBuffer): void { 16 | try { 17 | this.sender.send('ipc:message', message.buffer); 18 | } catch (e) { 19 | // systems are going down 20 | } 21 | } 22 | 23 | dispose(): void { 24 | this.sender.send('ipc:disconnect', null); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /core/common/ipc.ts: -------------------------------------------------------------------------------- 1 | import { CancelablePromise, CancellationToken, CancellationTokenSource, createCancelablePromise } from "../../base/cancelablePromise/cancelablePromise"; 2 | import { Emitter, Event } from '../../base/event'; 3 | import { IDisposable } from "../../base/interface"; 4 | import { VSBuffer } from "../../base/buffer"; 5 | import { BufferReader, BufferWriter, deserialize, serialize } from "../../base/buffer-utils"; 6 | import { combinedDisposable, toDisposable } from "../../base/disposable/disposable"; 7 | import { canceled } from "../../base/errors"; 8 | 9 | export enum ResponseType { 10 | Initialize = 200, // 初始化消息返回 11 | PromiseSuccess = 201, // promise 成功 12 | PromiseError = 202, // promise 失败 13 | PromiseErrorObj = 203, 14 | EventFire = 204, 15 | } 16 | 17 | type IRawInitializeResponse = { type: ResponseType.Initialize }; 18 | type IRawPromiseSuccessResponse = { 19 | type: ResponseType.PromiseSuccess; // 类型 20 | id: number; // 请求 id 21 | data: any; // 数据 22 | }; 23 | type IRawPromiseErrorResponse = { 24 | type: ResponseType.PromiseError; 25 | id: number; 26 | data: { message: string; name: string; stack: string[] | undefined }; 27 | }; 28 | type IRawPromiseErrorObjResponse = { 29 | type: ResponseType.PromiseErrorObj; 30 | id: number; 31 | data: any; 32 | }; 33 | 34 | type IRawResponse = 35 | | IRawInitializeResponse 36 | | IRawPromiseSuccessResponse 37 | | IRawPromiseErrorResponse 38 | | IRawPromiseErrorObjResponse; 39 | 40 | 41 | type IHandler = (response: IRawResponse) => void; 42 | 43 | export enum RequestType { 44 | Promise = 100, 45 | PromiseCancel = 101, 46 | EventListen = 102, 47 | EventDispose = 103, 48 | } 49 | 50 | type IRawPromiseRequest = { 51 | type: RequestType.Promise; 52 | id: number; 53 | channelName: string; 54 | name: string; 55 | arg: any; 56 | }; 57 | type IRawPromiseCancelRequest = { type: RequestType.PromiseCancel; id: number }; 58 | 59 | type IRawRequest = 60 | | IRawPromiseRequest 61 | | IRawPromiseCancelRequest; 62 | 63 | // 消息传输协议定义 64 | export interface IMessagePassingProtocol { 65 | onMessage: Event; 66 | send(buffer: VSBuffer): void; 67 | } 68 | 69 | // 服务端频道接口 70 | export interface IServerChannel { 71 | call( 72 | ctx: TContext, 73 | command: string, 74 | arg?: any, 75 | cancellationToken?: CancellationToken, 76 | ): Promise; // 发起服务请求 77 | listen(ctx: TContext, event: string, arg?: any): Event;// 监听消息 78 | } 79 | 80 | // 频道的服务端接口 81 | export interface IChannelServer { 82 | registerChannel(channelName: string, channel: IServerChannel): void; 83 | } 84 | 85 | // 频道客户端接口 86 | export interface IChannel { 87 | call( 88 | command: string, 89 | arg?: any, 90 | cancellationToken?: CancellationToken, 91 | ): Promise; 92 | listen(event: string, arg?: any): Event; 93 | } 94 | 95 | 96 | export interface IChannelClient { 97 | getChannel(channelName: string): T; 98 | } 99 | 100 | enum State { 101 | Uninitialized, // 未初始化 102 | Idle, // 空闲 103 | } 104 | 105 | interface PendingRequest { 106 | request: IRawPromiseRequest; 107 | timeoutTimer: any; 108 | } 109 | 110 | export class ChannelServer 111 | implements IChannelServer, IDisposable { 112 | // 保存客户端可以访问的频道信息 113 | private readonly channels = new Map>(); 114 | 115 | // 消息通信协议监听 116 | private protocolListener: IDisposable | null; 117 | 118 | // 保存活跃的请求,在收到取消消息后,进行取消执行,释放资源 119 | private readonly activeRequests = new Map(); 120 | 121 | // 在频道服务器注册之前,可能会到来很多请求,此时他们会停留在这个队列里 122 | // 如果 timeoutDelay 过时后,则会移除 123 | // 如果频道注册完成,则会从此队列里拿出并执行 124 | private readonly pendingRequests = new Map(); 125 | 126 | constructor( 127 | private readonly protocol: IMessagePassingProtocol, // 消息协议 128 | private readonly ctx: TContext, // 服务名 129 | private readonly timeoutDelay: number = 1000, // 通信超时时间 130 | ) { 131 | this.protocolListener = this.protocol.onMessage(msg => 132 | this.onRawMessage(msg), 133 | ); 134 | // 当我们频道服务端实例化完成时,我们需要给频道客服端返回实例化完成的消息: 135 | this.sendResponse({ type: ResponseType.Initialize }); 136 | } 137 | 138 | private onRawMessage(message: VSBuffer): void { 139 | // 解读消息 140 | const reader = new BufferReader(message); 141 | const header = deserialize(reader); 142 | const body = deserialize(reader); 143 | const type = header[0] as RequestType; 144 | 145 | // 返回执行结果 146 | switch (type) { 147 | case RequestType.Promise: 148 | return this.onPromise({ 149 | type, 150 | id: header[1], 151 | channelName: header[2], 152 | name: header[3], 153 | arg: body, 154 | }); 155 | case RequestType.PromiseCancel: 156 | return this.disposeActiveRequest({ type, id: header[1] }); 157 | default: 158 | break; 159 | } 160 | } 161 | 162 | private disposeActiveRequest(request: IRawRequest): void { 163 | const disposable = this.activeRequests.get(request.id); 164 | 165 | if (disposable) { 166 | disposable.dispose(); 167 | this.activeRequests.delete(request.id); 168 | } 169 | } 170 | 171 | public dispose(): void { 172 | if (this.protocolListener) { 173 | this.protocolListener.dispose(); 174 | this.protocolListener = null; 175 | } 176 | this.activeRequests.forEach(d => d.dispose()); 177 | this.activeRequests.clear(); 178 | } 179 | registerChannel(channelName: string, channel: IServerChannel): void { 180 | this.channels.set(channelName, channel); 181 | 182 | // 如果频道还未注册好之前就来了很多请求,则在此时进行请求执行。 183 | // https://github.com/microsoft/vscode/issues/72531 184 | setTimeout(() => this.flushPendingRequests(channelName), 0); 185 | } 186 | 187 | private flushPendingRequests(channelName: string): void { 188 | const requests = this.pendingRequests.get(channelName); 189 | 190 | if (requests) { 191 | for (const request of requests) { 192 | clearTimeout(request.timeoutTimer); 193 | 194 | switch (request.request.type) { 195 | case RequestType.Promise: 196 | this.onPromise(request.request); 197 | break; 198 | default: 199 | break; 200 | } 201 | } 202 | 203 | this.pendingRequests.delete(channelName); 204 | } 205 | } 206 | 207 | private sendResponse(response: IRawResponse): void { 208 | switch (response.type) { 209 | case ResponseType.Initialize: 210 | return this.send([response.type]); 211 | 212 | case ResponseType.PromiseSuccess: 213 | case ResponseType.PromiseError: 214 | case ResponseType.PromiseErrorObj: 215 | return this.send([response.type, response.id], response.data); 216 | default: 217 | break; 218 | } 219 | } 220 | 221 | 222 | private send(header: any, body: any = undefined): void { 223 | const writer = new BufferWriter(); 224 | serialize(writer, header); 225 | serialize(writer, body); 226 | this.sendBuffer(writer.buffer); 227 | } 228 | 229 | private sendBuffer(message: VSBuffer): void { 230 | try { 231 | this.protocol.send(message); 232 | } catch (err) { 233 | // noop 234 | } 235 | } 236 | 237 | private onPromise(request: IRawPromiseRequest): void { 238 | const channel = this.channels.get(request.channelName); 239 | // 如果频道不存在,则放入 PendingRequest,等待频道注册或者过期。 240 | if (!channel) { 241 | this.collectPendingRequest(request); 242 | return; 243 | } 244 | 245 | // 取消请求 token -> 机制见 可取消的 Promise 部分内容讲解 246 | const cancellationTokenSource = new CancellationTokenSource(); 247 | let promise: Promise; 248 | try { 249 | promise = channel.call( 250 | this.ctx, 251 | request.name, 252 | request.arg, 253 | cancellationTokenSource.token, 254 | ); 255 | } catch (err) { 256 | promise = Promise.reject(err); 257 | } 258 | 259 | const { id } = request; 260 | 261 | promise.then( 262 | data => { 263 | this.sendResponse({ 264 | id, 265 | data, 266 | type: ResponseType.PromiseSuccess, 267 | }); 268 | this.activeRequests.delete(request.id); 269 | }, 270 | err => { 271 | if (err instanceof Error) { 272 | // 如果有异常,进行消息的异常处理,并返回响应结果。 273 | this.sendResponse({ 274 | id, 275 | data: { 276 | message: err.message, 277 | name: err.name, 278 | stack: err.stack 279 | ? err.stack.split 280 | ? err.stack.split('\n') 281 | : err.stack 282 | : undefined, 283 | }, 284 | type: ResponseType.PromiseError, 285 | }); 286 | } else { 287 | this.sendResponse({ 288 | id, 289 | data: err, 290 | type: ResponseType.PromiseErrorObj, 291 | }); 292 | } 293 | 294 | this.activeRequests.delete(request.id); 295 | }, 296 | ); 297 | 298 | const disposable = toDisposable(() => cancellationTokenSource.cancel()); 299 | this.activeRequests.set(request.id, disposable); 300 | } 301 | 302 | private collectPendingRequest( 303 | request: IRawPromiseRequest, 304 | ): void { 305 | let pendingRequests = this.pendingRequests.get(request.channelName); 306 | 307 | if (!pendingRequests) { 308 | pendingRequests = []; 309 | this.pendingRequests.set(request.channelName, pendingRequests); 310 | } 311 | 312 | const timer = setTimeout(() => { 313 | console.error(`Unknown channel: ${request.channelName}`); 314 | 315 | if (request.type === RequestType.Promise) { 316 | this.sendResponse({ 317 | id: request.id, 318 | data: { 319 | name: 'Unknown channel', 320 | message: `Channel name '${request.channelName}' timed out after ${this.timeoutDelay}ms`, 321 | stack: undefined, 322 | }, 323 | type: ResponseType.PromiseError, 324 | }); 325 | } 326 | }, this.timeoutDelay); 327 | 328 | pendingRequests.push({ request, timeoutTimer: timer }); 329 | } 330 | 331 | } 332 | 333 | export class ChannelClient implements IChannelClient, IDisposable { 334 | private protocolListener: IDisposable | null; 335 | 336 | private state: State = State.Uninitialized; // 频道的状态 337 | 338 | private lastRequestId = 0; // 通信请求唯一 ID 管理 339 | 340 | // 活跃中的 request, 用于取消的时候统一关闭;如果频道被关闭了(dispose),则统一会往所有的频道发送取消消息,从而确保通信的可靠性。 341 | private readonly activeRequests = new Set(); 342 | 343 | private readonly handlers = new Map(); // 通信返回结果后的处理 344 | 345 | private readonly _onDidInitialize = new Emitter(); 346 | 347 | readonly onDidInitialize = this._onDidInitialize.event; // 当频道被初始化时会触发事件 348 | 349 | constructor(private readonly protocol: IMessagePassingProtocol) { 350 | this.protocolListener = this.protocol.onMessage(msg => this.onBuffer(msg)); 351 | } 352 | 353 | private onBuffer(message: VSBuffer): void { 354 | const reader = new BufferReader(message); 355 | const header = deserialize(reader); 356 | const body = deserialize(reader); 357 | const type: ResponseType = header[0]; 358 | 359 | switch (type) { 360 | case ResponseType.Initialize: 361 | return this.onResponse({ type: header[0] }); 362 | 363 | case ResponseType.PromiseSuccess: 364 | case ResponseType.PromiseError: 365 | case ResponseType.EventFire: 366 | case ResponseType.PromiseErrorObj: 367 | return this.onResponse({ type: header[0], id: header[1], data: body }); 368 | } 369 | } 370 | 371 | private onResponse(response: IRawResponse): void { 372 | if (response.type === ResponseType.Initialize) { 373 | this.state = State.Idle; 374 | this._onDidInitialize.fire(); 375 | return; 376 | } 377 | 378 | const handler = this.handlers.get(response.id); 379 | 380 | if (handler) { 381 | handler(response); 382 | } 383 | } 384 | 385 | dispose(): void { 386 | if (this.protocolListener) { 387 | // 移除消息监听 388 | this.protocolListener.dispose(); 389 | this.protocolListener = null; 390 | } 391 | 392 | // 如果有请求仍然在执行中,清理所有请求,释放主进程资源 393 | this.activeRequests.forEach(p => p.dispose()); 394 | this.activeRequests.clear(); 395 | } 396 | 397 | getChannel(channelName: string): T { 398 | const that = this; 399 | return { 400 | call(command: string, arg?: any, cancellationToken?: CancellationToken) { 401 | return that.requestPromise( 402 | channelName, 403 | command, 404 | arg, 405 | cancellationToken, 406 | ); 407 | }, 408 | listen(event: string, arg: any) { 409 | // TODO 410 | // return that.requestEvent(channelName, event, arg); 411 | }, 412 | } as T; 413 | } 414 | 415 | private requestPromise( 416 | channelName: string, 417 | name: string, 418 | arg?: any, 419 | cancellationToken = CancellationToken.None, 420 | ): Promise { 421 | const id = this.lastRequestId++; 422 | const type = RequestType.Promise; 423 | const request: IRawRequest = { id, type, channelName, name, arg }; 424 | 425 | // 如果请求被取消了,则不再执行。 426 | if (cancellationToken.isCancellationRequested) { 427 | return Promise.reject(canceled()); 428 | } 429 | 430 | let disposable: IDisposable; 431 | 432 | const result = new Promise((c, e) => { 433 | // 如果请求被取消了,则不再执行。 434 | if (cancellationToken.isCancellationRequested) { 435 | return e(canceled()); 436 | } 437 | 438 | // 只有频道确认注册完成后,才开始发送请求,否则一直处于队列中 439 | // 在「频道服务端」准备就绪后,会发送就绪消息回来,此时会触发状态变更为「idle」就绪状态 440 | // 从而会触发 uninitializedPromise.then 441 | // 从而消息可以进行发送 442 | let uninitializedPromise: CancelablePromise< 443 | void 444 | > | null = createCancelablePromise(_ => this.whenInitialized()); 445 | uninitializedPromise.then(() => { 446 | uninitializedPromise = null; 447 | 448 | const handler: IHandler = response => { 449 | console.log( 450 | 'main process response:', 451 | JSON.stringify(response, null, 2), 452 | ); 453 | // 根据返回的结果类型,进行处理, 这里不处理 Initialize 这个会在更上层处理 454 | switch (response.type) { 455 | case ResponseType.PromiseSuccess: 456 | this.handlers.delete(id); 457 | c(response.data); 458 | break; 459 | 460 | case ResponseType.PromiseError: 461 | this.handlers.delete(id); 462 | const error = new Error(response.data.message); 463 | (error).stack = response.data.stack; 464 | error.name = response.data.name; 465 | e(error); 466 | break; 467 | 468 | case ResponseType.PromiseErrorObj: 469 | this.handlers.delete(id); 470 | e(response.data); 471 | break; 472 | default: 473 | break; 474 | } 475 | }; 476 | 477 | // 保存此次请求的处理 478 | this.handlers.set(id, handler); 479 | 480 | // 开始发送请求 481 | this.sendRequest(request); 482 | }); 483 | 484 | const cancel = () => { 485 | // 如果还未初始化,则直接取消 486 | if (uninitializedPromise) { 487 | uninitializedPromise.cancel(); 488 | uninitializedPromise = null; 489 | } else { 490 | // 如果已经初始化,并且在请求中,则发送中断消息 491 | this.sendRequest({ id, type: RequestType.PromiseCancel }); 492 | } 493 | 494 | e(canceled()); 495 | }; 496 | 497 | const cancellationTokenListener = cancellationToken.onCancellationRequested( 498 | cancel, 499 | ); 500 | disposable = combinedDisposable( 501 | toDisposable(cancel), 502 | cancellationTokenListener, 503 | ); 504 | this.activeRequests.add(disposable); 505 | }); 506 | 507 | return result.finally(() => this.activeRequests.delete(disposable)); 508 | } 509 | 510 | private sendRequest(request: IRawRequest): void { 511 | switch (request.type) { 512 | case RequestType.Promise: 513 | return this.send( 514 | [request.type, request.id, request.channelName, request.name], 515 | request.arg, 516 | ); 517 | 518 | case RequestType.PromiseCancel: 519 | return this.send([request.type, request.id]); 520 | default: 521 | break; 522 | } 523 | } 524 | 525 | private send(header: any, body: any = undefined): void { 526 | const writer = new BufferWriter(); 527 | serialize(writer, header); 528 | serialize(writer, body); 529 | this.sendBuffer(writer.buffer); 530 | } 531 | 532 | private sendBuffer(message: VSBuffer): void { 533 | try { 534 | this.protocol.send(message); 535 | } catch (err) { 536 | // noop 537 | } 538 | } 539 | 540 | private whenInitialized(): Promise { 541 | if (this.state === State.Idle) { 542 | return Promise.resolve(); 543 | } else { 544 | return Event.toPromise(this.onDidInitialize); 545 | } 546 | } 547 | 548 | } 549 | 550 | export interface Client { 551 | readonly ctx: TContext; 552 | } 553 | 554 | export interface ClientConnectionEvent { 555 | protocol: IMessagePassingProtocol; 556 | onDidClientDisconnect: Event; 557 | } 558 | 559 | export interface Connection extends Client { 560 | readonly channelServer: ChannelServer; 561 | readonly channelClient: ChannelClient; 562 | } 563 | 564 | 565 | -------------------------------------------------------------------------------- /core/electron-main/ipc.electron-main.ts: -------------------------------------------------------------------------------- 1 | import { IDisposable } from "../../base/interface"; 2 | import { VSBuffer } from "../../base/buffer"; 3 | import {Emitter, Event} from '../../base/event'; 4 | import { ipcMain } from "electron"; 5 | import { ChannelClient, ChannelServer, ClientConnectionEvent, Connection, IChannelServer, IServerChannel } from "../common/ipc"; 6 | import { BufferReader, deserialize } from "../../base/buffer-utils"; 7 | import { toDisposable } from "../../base/disposable/disposable"; 8 | import { Protocol } from "../common/ipc.electron"; 9 | 10 | interface IIPCEvent { 11 | event: { sender: Electron.WebContents }; 12 | message: Buffer | null; 13 | } 14 | 15 | function createScopedOnMessageEvent( 16 | senderId: number, 17 | eventName: string, 18 | ): Event { 19 | const onMessage = Event.fromNodeEventEmitter( 20 | ipcMain, 21 | eventName, 22 | (event, message) => ({ event, message }), 23 | ); 24 | const onMessageFromSender = Event.filter( 25 | onMessage, 26 | ({ event }) => event.sender.id === senderId, 27 | ); 28 | // @ts-ignore 29 | return Event.map(onMessageFromSender, ({ message }) => 30 | message ? VSBuffer.wrap(message) : message, 31 | ); 32 | } 33 | 34 | 35 | class IPCServer 36 | implements 37 | IChannelServer, 38 | IDisposable { 39 | 40 | // 服务端侧可访问的频道 41 | private readonly channels = new Map>(); 42 | 43 | // 客户端和服务端的连接 44 | private readonly _connections = new Set>(); 45 | 46 | private readonly _onDidChangeConnections = new Emitter< 47 | Connection 48 | >(); 49 | 50 | // 连接改变的时候触发得事件监听 51 | readonly onDidChangeConnections: Event> = this 52 | ._onDidChangeConnections.event; 53 | 54 | // 所有连接 55 | get connections(): Array> { 56 | const result: Array> = []; 57 | this._connections.forEach(ctx => result.push(ctx)); 58 | return result; 59 | } 60 | 61 | dispose(): void { 62 | this.channels.clear(); 63 | this._connections.clear(); 64 | this._onDidChangeConnections.dispose(); 65 | } 66 | registerChannel( 67 | channelName: string, 68 | channel: IServerChannel, 69 | ): void { 70 | this.channels.set(channelName, channel); 71 | 72 | // 同时在所有的连接中,需要注册频道 73 | this._connections.forEach(connection => { 74 | connection.channelServer.registerChannel(channelName, channel); 75 | }); 76 | } 77 | 78 | constructor(onDidClientConnect: Event) { 79 | onDidClientConnect(({ protocol, onDidClientDisconnect }) => { 80 | const onFirstMessage = Event.once(protocol.onMessage); 81 | onFirstMessage(msg => { 82 | const reader = new BufferReader(msg); 83 | const ctx = deserialize(reader) as TContext; 84 | const channelServer = new ChannelServer(protocol, ctx); 85 | const channelClient = new ChannelClient(protocol); 86 | 87 | this.channels.forEach((channel, name) => 88 | channelServer.registerChannel(name, channel), 89 | ); 90 | 91 | const connection: Connection = { 92 | channelServer, 93 | channelClient, 94 | ctx, 95 | }; 96 | this._connections.add(connection); 97 | // this._onDidChangeConnections.fire(connection); 98 | 99 | onDidClientDisconnect(() => { 100 | channelServer.dispose(); 101 | channelClient.dispose(); 102 | this._connections.delete(connection); 103 | }); 104 | }); 105 | }); 106 | } 107 | 108 | } 109 | 110 | 111 | export class Server extends IPCServer { 112 | private static readonly Clients: Map = new Map< 113 | number, 114 | IDisposable 115 | >(); 116 | 117 | private static getOnDidClientConnect(): Event { 118 | const onHello = Event.fromNodeEventEmitter( 119 | ipcMain, 120 | 'ipc:hello', 121 | ({ sender }) => sender, 122 | ); 123 | 124 | return Event.map(onHello, webContents => { 125 | const { id } = webContents; 126 | const client = Server.Clients.get(id); 127 | 128 | if (client) { 129 | client.dispose(); 130 | } 131 | 132 | const onDidClientReconnect = new Emitter(); 133 | Server.Clients.set( 134 | id, 135 | toDisposable(() => onDidClientReconnect.fire()), 136 | ); 137 | 138 | const onMessage = createScopedOnMessageEvent(id, 'ipc:message') as Event< 139 | VSBuffer 140 | >; 141 | const onDidClientDisconnect = Event.any( 142 | Event.signal(createScopedOnMessageEvent(id, 'ipc:disconnect')), 143 | onDidClientReconnect.event, 144 | ); 145 | const protocol = new Protocol(webContents, onMessage); 146 | return { protocol, onDidClientDisconnect }; 147 | }); 148 | } 149 | 150 | constructor() { 151 | super(Server.getOnDidClientConnect()); 152 | } 153 | } 154 | 155 | -------------------------------------------------------------------------------- /core/electron-render/IPCClient.ts: -------------------------------------------------------------------------------- 1 | import { IDisposable } from "../../base/interface"; 2 | import { VSBuffer } from "../../base/buffer"; 3 | import { BufferWriter, serialize } from "../../base/buffer-utils"; 4 | import { ChannelClient, ChannelServer, IChannel, IChannelClient, IChannelServer, IMessagePassingProtocol, IServerChannel } from "../common/ipc"; 5 | import { Protocol } from "../common/ipc.electron"; 6 | import { Event } from '../../base/event'; 7 | import { ipcRenderer } from "electron"; 8 | 9 | export class IPCClient 10 | implements IChannelClient, IChannelServer, IDisposable { 11 | private readonly channelClient: ChannelClient; 12 | 13 | private readonly channelServer: ChannelServer; 14 | 15 | constructor(protocol: IMessagePassingProtocol, ctx: TContext) { 16 | const writer = new BufferWriter(); 17 | serialize(writer, ctx); 18 | protocol.send(writer.buffer); 19 | 20 | this.channelClient = new ChannelClient(protocol); 21 | this.channelServer = new ChannelServer(protocol, ctx); 22 | } 23 | 24 | getChannel(channelName: string): T { 25 | return this.channelClient.getChannel(channelName); 26 | } 27 | 28 | registerChannel( 29 | channelName: string, 30 | channel: IServerChannel, 31 | ): void { 32 | this.channelServer.registerChannel(channelName, channel); 33 | } 34 | 35 | dispose(): void { 36 | this.channelClient.dispose(); 37 | this.channelServer.dispose(); 38 | } 39 | } 40 | 41 | export class Client extends IPCClient implements IDisposable { 42 | private readonly protocol: Protocol; 43 | 44 | private static createProtocol(): Protocol { 45 | const onMessage = Event.fromNodeEventEmitter( 46 | ipcRenderer, 47 | 'ipc:message', 48 | (_, message: Buffer) => VSBuffer.wrap(message), 49 | ); 50 | ipcRenderer.send('ipc:hello'); 51 | return new Protocol(ipcRenderer, onMessage); 52 | } 53 | 54 | constructor(id: string) { 55 | const protocol = Client.createProtocol(); 56 | super(protocol, id); 57 | this.protocol = protocol; 58 | } 59 | 60 | dispose(): void { 61 | this.protocol.dispose(); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignore": ["**/*.test.ts", "**/*.spec.ts", ".git", "node_modules"], 3 | "watch": ["src"], 4 | "exec": "yarn dev", 5 | "ext": "ts" 6 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webpack4-ts-environment", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "hot-dev": "nodemon", 9 | "dev": "tsc && electron ./src/main.js", 10 | "build-dev": "webpack --config webpack.config.dev.js", 11 | "start-dev": "yarn build-dev && node ./dist/main.js" 12 | }, 13 | "author": "", 14 | "license": "ISC", 15 | "devDependencies": { 16 | "@babel/core": "^7.6.4", 17 | "@babel/plugin-syntax-dynamic-import": "^7.2.0", 18 | "@babel/plugin-transform-runtime": "^7.6.2", 19 | "@babel/preset-env": "^7.6.3", 20 | "@types/node": "^12.11.1", 21 | "awesome-typescript-loader": "^5.2.1", 22 | "babel-loader": "^8.0.6", 23 | "electron": "8.3.2", 24 | "nodemon": "^1.19.4", 25 | "source-map-loader": "^0.2.4", 26 | "ts-node": "^8.4.1", 27 | "typescript": "^3.6.4", 28 | "webpack": "^4.41.2", 29 | "webpack-cli": "^3.3.9" 30 | }, 31 | "dependencies": { 32 | "express": "^4.17.1" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow } from 'electron'; 2 | import path from 'path'; 3 | import { Server as ElectronIPCServer } from '../core/electron-main/ipc.electron-main'; 4 | import { WindowChannel } from './windowServiceIpc'; 5 | import { WindowService } from './windowService'; 6 | 7 | app.on('ready', () => { 8 | const electronIpcServer = new ElectronIPCServer(); 9 | electronIpcServer.registerChannel('windowManager', new WindowChannel(new WindowService())) 10 | 11 | 12 | const win = new BrowserWindow({ 13 | width: 1000, 14 | height: 800, 15 | webPreferences: { 16 | nodeIntegration: true 17 | } 18 | }); 19 | 20 | console.log('render index html:', path.join(__dirname, 'render', 'index.html')); 21 | win.loadFile(path.join(__dirname, 'render', 'index.html')); 22 | }) 23 | -------------------------------------------------------------------------------- /src/render/index.html: -------------------------------------------------------------------------------- 1 | 2 |
jupiter electron
3 | 9 | 10 | -------------------------------------------------------------------------------- /src/windowService.ts: -------------------------------------------------------------------------------- 1 | export class WindowService { 2 | 3 | doSomething(): string { 4 | console.log('do something and return done') 5 | return 'done'; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/windowServiceIpc.ts: -------------------------------------------------------------------------------- 1 | import { IServerChannel } from "../core/common/ipc"; 2 | import { WindowService } from "./windowService"; 3 | import { Event } from '../base/event'; 4 | 5 | export class WindowChannel implements IServerChannel { 6 | constructor( 7 | public readonly windowService: WindowService, 8 | ) {} 9 | 10 | listen(_: unknown, event: string): Event { 11 | // 暂时不支持 12 | throw new Error(`Not support listen event currently: ${event}`); 13 | } 14 | 15 | call(_: unknown, command: string, arg?: any): Promise { 16 | switch (command) { 17 | case 'doSomething': 18 | return Promise.resolve(this.windowService.doSomething()); 19 | 20 | default: 21 | return Promise.reject('无可调用服务!'); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strictNullChecks": true, 4 | "module": "commonjs", 5 | "esModuleInterop": true, 6 | "importHelpers": true, 7 | "allowSyntheticDefaultImports": true, 8 | "target": "es5", 9 | "lib": [ 10 | "dom", 11 | "ES2019.Symbol", 12 | "ES2020.Symbol.WellKnown", 13 | "es2017.object" 14 | ] 15 | }, 16 | "include": [ 17 | "src", // write ts in src, you can define your own dir 18 | "examples", 19 | "." 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /yarn-error.log: -------------------------------------------------------------------------------- 1 | Arguments: 2 | /Users/xiaofan/.nvm/versions/node/v10.15.0/bin/node /usr/local/bin/yarn add webpack webpack-cli babel-loader awesome-typescript-loader source-map-loader - D 3 | 4 | PATH: 5 | /Users/xiaofan/.rvm/gems/ruby-2.4.2/bin:/Users/xiaofan/.rvm/gems/ruby-2.4.2@global/bin:/Users/xiaofan/.rvm/rubies/ruby-2.4.2/bin:/usr/local/opt/mongodb@3.4/bin:/Users/xiaofan/.nvm/versions/node/v10.15.0/bin:/Library/Frameworks/Python.framework/Versions/3.7/bin:/Library/Frameworks/Python.framework/Versions/3.4/bin:/Library/Frameworks/Python.framework/Versions/3.5/bin:/Library/Frameworks/Python.framework/Versions/3.6/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Applications/VMware Fusion.app/Contents/Public:/usr/local/go/bin:/Library/Frameworks/Mono.framework/Versions/Current/Commands:/Applications/Wireshark.app/Contents/MacOS:/Users/xiaofan/.rvm/bin 6 | 7 | Yarn version: 8 | 1.13.0 9 | 10 | Node version: 11 | 10.15.0 12 | 13 | Platform: 14 | darwin x64 15 | 16 | Trace: 17 | Error: https://registry.npm.taobao.org/-: Not found 18 | at Request.params.callback [as _callback] (/usr/local/lib/node_modules/yarn/lib/cli.js:66255:18) 19 | at Request.self.callback (/usr/local/lib/node_modules/yarn/lib/cli.js:129397:22) 20 | at Request.emit (events.js:182:13) 21 | at Request. (/usr/local/lib/node_modules/yarn/lib/cli.js:130369:10) 22 | at Request.emit (events.js:182:13) 23 | at IncomingMessage. (/usr/local/lib/node_modules/yarn/lib/cli.js:130291:12) 24 | at Object.onceWrapper (events.js:273:13) 25 | at IncomingMessage.emit (events.js:187:15) 26 | at endReadableNT (_stream_readable.js:1094:12) 27 | at process._tickCallback (internal/process/next_tick.js:63:19) 28 | 29 | npm manifest: 30 | { 31 | "name": "webpack4-ts-environment", 32 | "version": "1.0.0", 33 | "description": "", 34 | "main": "index.js", 35 | "scripts": { 36 | "test": "echo \"Error: no test specified\" && exit 1" 37 | }, 38 | "author": "", 39 | "license": "ISC", 40 | "devDependencies": { 41 | "@types/node": "^12.11.1", 42 | "typescript": "^3.6.4" 43 | } 44 | } 45 | 46 | yarn manifest: 47 | No manifest 48 | 49 | Lockfile: 50 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 51 | # yarn lockfile v1 52 | 53 | 54 | "@types/node@^12.11.1": 55 | version "12.11.1" 56 | resolved "https://registry.npm.taobao.org/@types/node/download/@types/node-12.11.1.tgz#1fd7b821f798b7fa29f667a1be8f3442bb8922a3" 57 | integrity sha1-H9e4IfeYt/op9mehvo80QruJIqM= 58 | 59 | typescript@^3.6.4: 60 | version "3.6.4" 61 | resolved "https://registry.npm.taobao.org/typescript/download/typescript-3.6.4.tgz?cache=0&sync_timestamp=1571206674846&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Ftypescript%2Fdownload%2Ftypescript-3.6.4.tgz#b18752bb3792bc1a0281335f7f6ebf1bbfc5b91d" 62 | integrity sha1-sYdSuzeSvBoCgTNff26/G7/FuR0= 63 | --------------------------------------------------------------------------------