├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── main.yaml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── codecov.yaml ├── debug.png ├── diagram.png ├── lerna.json ├── package-lock.json ├── package.json └── packages ├── core ├── .gitignore ├── .prettierignore ├── .prettierrc ├── CHANGELOG.md ├── api-extractor.json ├── api │ └── post-me.api.md ├── demo │ ├── build.js │ ├── child.html │ ├── child.js │ ├── index.html │ ├── parent.js │ └── worker.js ├── jest.config.js ├── package-lock.json ├── package.json ├── rollup.config.js ├── src │ ├── common.ts │ ├── connection.ts │ ├── dispatcher.ts │ ├── emitter.ts │ ├── handles.ts │ ├── handshake.ts │ ├── index.ts │ ├── messages.ts │ ├── messenger.ts │ └── proxy.ts ├── tests │ ├── index.spec.ts │ └── jsdom.d.ts └── tsconfig.json └── mpi ├── .gitignore ├── .prettierignore ├── .prettierrc ├── CHANGELOG.md ├── README.md ├── api-extractor.json ├── api └── mpi.api.md ├── benchmark.png ├── diagram.png ├── index.spec.ts ├── jest.config.js ├── package-lock.json ├── package.json ├── rollup.config.js ├── src ├── index.ts ├── mpi.ts ├── parent.ts └── worker.ts ├── tests └── index.spec.ts └── tsconfig.json /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | name: main 2 | on: [push] 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v2 8 | - uses: actions/setup-node@v1 9 | with: 10 | node-version: '14.x' 11 | - run: npm ci 12 | - run: npm run bootstrap 13 | - run: npm run build 14 | - run: npm run test 15 | - run: bash <(curl -s https://codecov.io/bash) 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | docs/ 3 | 4 | .rpt2_cache/ 5 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at ales.genova@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Alessandro Genova 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![workflow status](https://github.com/alesgenova/post-me/workflows/main/badge.svg?branch=main)](https://github.com/alesgenova/post-me/actions?query=workflow%3Amain+branch%3Amain) 2 | [![npm package](https://img.shields.io/npm/v/post-me.svg)](https://www.npmjs.com/package/post-me) 3 | [![codecov](https://codecov.io/gh/alesgenova/post-me/branch/main/graph/badge.svg)](https://codecov.io/gh/alesgenova/post-me) 4 | 5 |

post-me

6 | 7 |

Communicate with web Workers and other Windows using a simple Promise based API

8 | 9 | ![diagram](./diagram.png) 10 | 11 | With __post-me__ it is easy for a parent (for example the main app) and a child (for example a worker or an iframe) to expose methods and custom events to each other. 12 | 13 | ## Features 14 | - 🔁 Parent and child can both __expose__ __methods__ and/or __events__. 15 | - 🔎 __Strong typing__ of method names, arguments, return values, as well as event names and payloads. 16 | - 🤙 Seamlessly pass __callbacks__ to the other context to get progress or partial results. 17 | - 📨 __Transfer__ arguments/return values/payloads when needed instead of cloning. 18 | - 🔗 Establish __multiple__ concurrent __connections__. 19 | - 🌱 __No dependencies__: 2kb gzip bundle. 20 | - 🧪 Excellent __test coverage__. 21 | - 👐 Open source (MIT) 22 | 23 | ## Demo 24 | In this [live demo](https://alesgenova.github.io/post-me-demo) the main window communicates with a web worker and an iframe ([source](https://github.com/alesgenova/post-me-demo)). 25 | 26 | ## Content: 27 | 1. [Install](#install) 28 | 2. [Basic Usage](#usage) 29 | 3. [Typescript Support](#typescript) 30 | 4. [Other Uses](#other) 31 | - [Windows](#windows) 32 | - [MessageChannels](#channels) 33 | 5. [Callbacks as parameters](#callbacks) 34 | 6. [Transfer vs Clone](#transfer) 35 | 7. [Debugging](#debugging) 36 | 8. [Parallel Programming](#parallel) 37 | 9. [API Documentation](#api) 38 | 10. [References](#references) 39 | 40 | 41 | 42 | ## Install 43 | Import __post-me__ as a module: 44 | ```bash 45 | npm install post-me 46 | ``` 47 | ```typescript 48 | import { ParentHandshake } from 'post-me'; 49 | ``` 50 | 51 | Import __post-me__ as a script: 52 | ```html 53 | 54 | 55 | 58 | ``` 59 | 60 | 61 | 62 | ## Usage 63 | In the example below, the parent application calls methods exposed by the worker and listens to events emitted by it. 64 | 65 | For the sake of simiplicity, only the worker is exposing methods and events, however the parent could do it as well. 66 | 67 | Parent code: 68 | ```typescript 69 | import { ParentHandshake, WorkerMessenger } from 'post-me'; 70 | 71 | const worker = new Worker('./worker.js'); 72 | 73 | const messenger = new WorkerMessenger({ worker }); 74 | 75 | ParentHandshake(messenger).then((connection) => { 76 | const remoteHandle = connection.remoteHandle(); 77 | 78 | // Call methods on the worker and get the result as a promise 79 | remoteHandle.call('sum', 3, 4).then((result) => { 80 | console.log(result); // 7 81 | }); 82 | 83 | // Listen for a specific custom event from the worker 84 | remoteHandle.addEventListener('ping', (payload) => { 85 | console.log(payload) // 'Oh, hi!' 86 | }); 87 | }); 88 | ``` 89 | 90 | Worker code: 91 | ```typescript 92 | import { ChildHandshake, WorkerMessenger } from 'post-me'; 93 | 94 | // Methods exposed by the worker: each function can either return a value or a Promise. 95 | const methods = { 96 | sum: (x, y) => x + y, 97 | mul: (x, y) => x * y 98 | } 99 | 100 | const messenger = WorkerMessenger({worker: self}); 101 | ChildHandshake(messenger, methods).then((connection) => { 102 | const localHandle = connection.localHandle(); 103 | 104 | // Emit custom events to the app 105 | localHandle.emit('ping', 'Oh, hi!'); 106 | }); 107 | ``` 108 | 109 | 110 | 111 | ## Typescript 112 | Using typescript you can ensure that the parent and the child are using each other's methods and events correctly. Most coding mistakes will be caught during development by the typescript compiler. 113 | 114 | Thanks to __post-me__ extensive typescript support, the correctness of the following items can be statically checked during development: 115 | - Method names 116 | - Argument number and types 117 | - Return values type 118 | - Event names 119 | - Event payload type 120 | 121 | Below a modified version of the previous example using typescript. 122 | 123 | Types code: 124 | ```typescript 125 | // types.ts 126 | 127 | export type WorkerMethods = { 128 | sum: (x: number, y: number) => number; 129 | mul: (x: number, y: number) => number; 130 | } 131 | 132 | export type WorkerEvents = { 133 | 'ping': string; 134 | } 135 | ``` 136 | 137 | Parent Code: 138 | ```typescript 139 | import { 140 | ParentHandshake, WorkerMessenger, RemoteHandle 141 | } from 'post-me'; 142 | 143 | import { WorkerMethods, WorkerEvents } from './types'; 144 | 145 | const worker = new Worker('./worker.js'); 146 | 147 | const messenger = new WorkerMessenger({ worker }); 148 | 149 | ParentHandshake(messenger).then((connection) => { 150 | const remoteHandle: RemoteHandle 151 | = connection.remoteHandle(); 152 | 153 | // Call methods on the worker and get the result as a Promise 154 | remoteHandle.call('sum', 3, 4).then((result) => { 155 | console.log(result); // 7 156 | }); 157 | 158 | // Listen for a specific custom event from the app 159 | remoteHandle.addEventListener('ping', (payload) => { 160 | console.log(payload) // 'Oh, hi!' 161 | }); 162 | 163 | // The following lines have various mistakes that will be caught by the compiler 164 | remoteHandle.call('mul', 3, 'four'); // Wrong argument type 165 | remoteHandle.call('foo'); // 'foo' doesn't exist on WorkerMethods type 166 | }); 167 | ``` 168 | 169 | Worker code: 170 | ```typescript 171 | import { ChildHandshake, WorkerMessenger, LocalHandle } from 'post-me'; 172 | 173 | import { WorkerMethods, WorkerEvents } from './types'; 174 | 175 | const methods: WorkerMethods = { 176 | sum: (x: number, y: number) => x + y, 177 | mul: (x: number, y: number) => x * y, 178 | } 179 | 180 | const messenger = WorkerMessenger({worker: self}); 181 | ChildHandshake(messenger, methods).then((connection) => { 182 | const localHandle: LocalHandle 183 | = connection.localHandle(); 184 | 185 | // Emit custom events to the worker 186 | localHandle.emit('ping', 'Oh, hi!'); 187 | }); 188 | ``` 189 | 190 | 191 | 192 | ## Other Uses 193 | post-me can establish the same level of bidirectional communications not only with workers but with other windows too (e.g. iframes) and message channels. 194 | 195 | Internally, the low level differences between communicating with a `Worker`, a `Window`, or a `MessageChannel` have been abstracted, and the `Handshake` will accept any object that implements the `Messenger` interface defined by __post-me__. 196 | 197 | This approach makes it easy for post-me to be extended by its users. 198 | 199 | A `Messenger` implementation for communicating between `Windows` and `MessagePorts` is already provided in the library (`WindowMessenger` and `PortMessenger`). 200 | 201 | 202 | 203 | ### Windows 204 | Here is an example of using post-me to communicate with an iframe. 205 | 206 | Parent code: 207 | ```typescript 208 | import { ParentHandshake, WindowMessenger } from 'post-me'; 209 | 210 | // Create the child window any way you like (iframe here, but could be popup or tab too) 211 | const childFrame = document.createElement('iframe'); 212 | const childWindow = childFrame.contentWindow; 213 | 214 | // For safety it is strongly adviced to pass the explicit child origin instead of '*' 215 | const messenger = new WindowMessenger({ 216 | localWindow: window, 217 | remoteWindow: childWindow, 218 | remoteOrigin: '*' 219 | }); 220 | 221 | ParentHandshake(messenger).then((connection) => {/* ... */}); 222 | ``` 223 | 224 | Child code: 225 | ```typescript 226 | import { ChildHandshake, WindowMessenger } from 'post-me'; 227 | 228 | // For safety it is strongly adviced to pass the explicit child origin instead of '*' 229 | const messenger = new WindowMessenger({ 230 | localWindow: window, 231 | remoteWindow: window.parent, 232 | remoteOrigin: '*' 233 | }); 234 | 235 | ChildHandshake(messenger).then((connection) => {/* ... */}); 236 | ``` 237 | 238 | 239 | 240 | ### MessageChannels 241 | Here is an example of using post-me to communicate over a `MessageChannel`. 242 | 243 | ```typescript 244 | import { ParentHandshake, ChildHandshake, PortMessenger } from 'post-me'; 245 | 246 | // Create a MessageChannel 247 | const channel = new MessageChannel(); 248 | const port1 = channel.port1; 249 | const port2 = channel.port2; 250 | 251 | // In the real world port1 and port2 would be transferred to other workers/windows 252 | { 253 | const messenger = new PortMessenger({port: port1}); 254 | ParentHandshake(messenger).then(connection => {/* ... */}); 255 | } 256 | { 257 | const messenger = new PortMessenger({port: port2}); 258 | ChildHandshake(messenger).then(connection => {/* ... */}); 259 | } 260 | ``` 261 | 262 | 263 | 264 | ## Callbacks as call parameters 265 | Even though functions cannot actually be shared across contexts, with a little magic under the hood __post-me__ let's you pass callback functions as arguments when calling a method on the other worker/window. 266 | 267 | Passing callbacks can be useful to obtain progress or partial results from a long running task. 268 | 269 | Parent code: 270 | ```typescript 271 | //... 272 | ParentHandshake(messenger).then(connection => { 273 | const remoteHandle = connection.remoteHandle(); 274 | 275 | const onProgress = (progress) => { 276 | console.log(progress); // 0.25, 0.5, 0.75 277 | } 278 | 279 | remoteHandle.call("slowSum", 2, 3, onProgress).then(result => { 280 | console.log(result); // 5 281 | }); 282 | }); 283 | ``` 284 | 285 | Worker code: 286 | ```typescript 287 | const methods = { 288 | slowSum: (x, y, onProgress) => { 289 | onProgress(0.25); 290 | onProgress(0.5); 291 | onProgress(0.75); 292 | 293 | return x + y; 294 | } 295 | // ... 296 | ChildHandshake(messenger, methods).then(connection => {/* */}) 297 | ``` 298 | 299 | 300 | 301 | ## Transfer vs Clone 302 | By default any call parameter, return value, and event payload is cloned when passed to the other context. 303 | 304 | While in most cases this doesn't have a significant impact on performance, sometimes you might need to transfer an object instead of cloning it. NOTE: only `Transferable` objects can be transfered (`ArrayBuffer`, `MessagePort`, `ImageBitmap`, `OffscreenCanvas`). 305 | 306 | __post-me__ provides a way to optionally transfer objects that are part of a method call, return value, or event payload. 307 | 308 | In the example below, the parent passes a very large array to a worker, the worker modifies the array in place, and returns it to the parent. Transfering the array instead of cloning it twice can save significant amounts of time. 309 | 310 | Parent code: 311 | ```typescript 312 | // ... 313 | 314 | ParentHandshake(messenger).then((connection) => { 315 | const remoteHandle = connection.remoteHandle(); 316 | 317 | // Transfer the buffer of the array parameter of every call that will be made to 'fillArray' 318 | remoteHandle.setCallTransfer('fillArray', (array, value) => [array.buffer]); 319 | { 320 | const array = new Float64Array(100000000); 321 | remoteHandle.call('fillArray', array, 5); 322 | } 323 | 324 | // Transfer the buffer of the array parameter only for this one call made to 'scaleArray' 325 | { 326 | const array = new Float64Array(100000000); 327 | const args = [array, 2]; 328 | const callOptions = { transfer: [array.buffer] }; 329 | remoteHandle.customCall('scaleArray', args, callOptions); 330 | } 331 | }); 332 | ``` 333 | 334 | Worker code: 335 | ```typescript 336 | // ... 337 | 338 | const methods = { 339 | fillArray: (array, value) => { 340 | array.forEach((_, i) => {array[i] = value}); 341 | return array; 342 | }, 343 | scaleArray: (buffer, type value) => { 344 | array.forEach((a, i) => {array[i] = a * value}); 345 | return array; 346 | } 347 | } 348 | 349 | ChildHandshake(messenger, model).then((connection) => { 350 | const localHandle = connection.localHandle(); 351 | 352 | // For each method, declare which parts of the return value should be transferred instead of cloned. 353 | localHandle.setReturnTransfer('fillArray', (result) => [result.buffer]); 354 | localHandle.setReturnTransfer('scaleArray', (result) => [result.buffer]); 355 | }); 356 | ``` 357 | 358 | 359 | 360 | ## Debugging 361 | You can optionally output the internal low-level messages exchanged between the two ends. 362 | 363 | To enable debugging, simply decorate any `Messenger` instance with the provided `DebugMessenger` decorator. 364 | 365 | You can optionally pass to the decorator your own logging function (a glorified `console.log` by default), which can be useful to make the output more readable, or to inspect messages in automated tests. 366 | 367 | ```typescript 368 | import { ParentHandshake, WindowMessenger, DebugMessenger } from 'post-me'; 369 | 370 | import debug from 'debug'; // Use the full feature logger from the debug library 371 | // import { debug } from 'post-me'; // Or the lightweight implementation provided 372 | 373 | let messenger = new WindowMessenger({ 374 | localWindow: window, 375 | remoteWindow: childWindow, 376 | remoteOrigin: '*' 377 | }); 378 | 379 | // To enable debugging of each message exchange, decorate the messenger with DebugMessenger 380 | const log = debug('post-me:parent'); // optional 381 | messenger = DebugMessenger(messenger, log); 382 | 383 | ParentHandshake(messenger).then((connection) => { 384 | // ... 385 | }); 386 | ``` 387 | 388 | Output: 389 | ![debug output](debug.png) 390 | 391 | 392 | 393 | ## Parallel Programming 394 | [__@post-me/mpi__](https://github.com/alesgenova/post-me/tree/main/packages/mpi) is an experimental library to write parallel algorithms that run on a pool of workers using a MPI-like syntax. See the dedicated [README](https://github.com/alesgenova/post-me/tree/main/packages/mpi) for more information. 395 | 396 | 397 | 398 | ## API Documentation 399 | 400 | The full [__API reference__](https://alesgenova.github.io/post-me/post-me.html) can be found [here](https://alesgenova.github.io/post-me/post-me.html). 401 | 402 | 403 | 404 | ## References 405 | The __post-me__ API is loosely inspired by [postmate](https://github.com/dollarshaveclub/postmate), with several major improvements and fixes to outstanding issues: 406 | - Native typescript support 407 | - Method calls can have both arguments and a return value: ([#94](https://github.com/dollarshaveclub/postmate/issues/94)) 408 | - Parent and child can both expose methods and/or events (instead of child only): [#118](https://github.com/dollarshaveclub/postmate/issues/118) 409 | - Exceptions that occur in a method call can be caught by the caller. 410 | - Better control over handshake origin and attempts: ([#150](https://github.com/dollarshaveclub/postmate/issues/150), [#195](https://github.com/dollarshaveclub/postmate/issues/195)) 411 | - Multiple listeners for each event: ([#58](https://github.com/dollarshaveclub/postmate/issues/58)) 412 | -------------------------------------------------------------------------------- /codecov.yaml: -------------------------------------------------------------------------------- 1 | fixes: 2 | - "/home/runner/work/alesgenova/post-me/packages::packages" 3 | flags: 4 | core: 5 | paths: 6 | - packages/core/ 7 | mpi: 8 | paths: 9 | - packages/mpi/ -------------------------------------------------------------------------------- /debug.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alesgenova/post-me/c567a2b8d1194582f145970209b35c64691ee5d5/debug.png -------------------------------------------------------------------------------- /diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alesgenova/post-me/c567a2b8d1194582f145970209b35c64691ee5d5/diagram.png -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "packages/*" 4 | ], 5 | "version": "independent", 6 | "command": { 7 | "publish": { 8 | "conventionalCommits": true, 9 | "message": "chore(release): publish" 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "private": true, 4 | "devDependencies": { 5 | "@microsoft/api-documenter": "^7.12.6", 6 | "lerna": "^3.22.1" 7 | }, 8 | "scripts": { 9 | "bootstrap": "lerna bootstrap", 10 | "build": "lerna run build", 11 | "test": "lerna run test", 12 | "build:docs": "lerna run build:docs && api-documenter markdown -i=docs/input -o=docs/output", 13 | "deploy:docs": "npm run build:docs && gh-pages -d docs/output", 14 | "publish": "lerna publish" 15 | } 16 | } -------------------------------------------------------------------------------- /packages/core/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | www/ 4 | docs/ 5 | temp/ 6 | 7 | coverage/ 8 | .rpt2_cache/ 9 | -------------------------------------------------------------------------------- /packages/core/.prettierignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /dist 3 | /www 4 | /coverage 5 | /scripts 6 | /api 7 | /docs 8 | /temp 9 | *.config.js -------------------------------------------------------------------------------- /packages/core/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /packages/core/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | ## [0.4.5](https://github.com/alesgenova/post-me/compare/post-me@0.4.4...post-me@0.4.5) (2021-02-19) 7 | 8 | **Note:** Version bump only for package post-me 9 | 10 | 11 | 12 | 13 | 14 | ## 0.4.4 (2021-02-10) 15 | 16 | **Note:** Version bump only for package post-me 17 | -------------------------------------------------------------------------------- /packages/core/api-extractor.json: -------------------------------------------------------------------------------- 1 | /** 2 | * Config file for API Extractor. For more info, please visit: https://api-extractor.com 3 | */ 4 | { 5 | "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", 6 | /** 7 | * Optionally specifies another JSON config file that this file extends from. This provides a way for 8 | * standard settings to be shared across multiple projects. 9 | * 10 | * If the path starts with "./" or "../", the path is resolved relative to the folder of the file that contains 11 | * the "extends" field. Otherwise, the first path segment is interpreted as an NPM package name, and will be 12 | * resolved using NodeJS require(). 13 | * 14 | * SUPPORTED TOKENS: none 15 | * DEFAULT VALUE: "" 16 | */ 17 | // "extends": "./shared/api-extractor-base.json" 18 | // "extends": "my-package/include/api-extractor-base.json" 19 | /** 20 | * Determines the "" token that can be used with other config file settings. The project folder 21 | * typically contains the tsconfig.json and package.json config files, but the path is user-defined. 22 | * 23 | * The path is resolved relative to the folder of the config file that contains the setting. 24 | * 25 | * The default value for "projectFolder" is the token "", which means the folder is determined by traversing 26 | * parent folders, starting from the folder containing api-extractor.json, and stopping at the first folder 27 | * that contains a tsconfig.json file. If a tsconfig.json file cannot be found in this way, then an error 28 | * will be reported. 29 | * 30 | * SUPPORTED TOKENS: 31 | * DEFAULT VALUE: "" 32 | */ 33 | // "projectFolder": "..", 34 | /** 35 | * (REQUIRED) Specifies the .d.ts file to be used as the starting point for analysis. API Extractor 36 | * analyzes the symbols exported by this module. 37 | * 38 | * The file extension must be ".d.ts" and not ".ts". 39 | * 40 | * The path is resolved relative to the folder of the config file that contains the setting; to change this, 41 | * prepend a folder token such as "". 42 | * 43 | * SUPPORTED TOKENS: , , 44 | */ 45 | "mainEntryPointFilePath": "/dist/index.d.ts", 46 | /** 47 | * A list of NPM package names whose exports should be treated as part of this package. 48 | * 49 | * For example, suppose that Webpack is used to generate a distributed bundle for the project "library1", 50 | * and another NPM package "library2" is embedded in this bundle. Some types from library2 may become part 51 | * of the exported API for library1, but by default API Extractor would generate a .d.ts rollup that explicitly 52 | * imports library2. To avoid this, we can specify: 53 | * 54 | * "bundledPackages": [ "library2" ], 55 | * 56 | * This would direct API Extractor to embed those types directly in the .d.ts rollup, as if they had been 57 | * local files for library1. 58 | */ 59 | "bundledPackages": [], 60 | /** 61 | * Determines how the TypeScript compiler engine will be invoked by API Extractor. 62 | */ 63 | "compiler": { 64 | /** 65 | * Specifies the path to the tsconfig.json file to be used by API Extractor when analyzing the project. 66 | * 67 | * The path is resolved relative to the folder of the config file that contains the setting; to change this, 68 | * prepend a folder token such as "". 69 | * 70 | * Note: This setting will be ignored if "overrideTsconfig" is used. 71 | * 72 | * SUPPORTED TOKENS: , , 73 | * DEFAULT VALUE: "/tsconfig.json" 74 | */ 75 | // "tsconfigFilePath": "/tsconfig.json", 76 | /** 77 | * Provides a compiler configuration that will be used instead of reading the tsconfig.json file from disk. 78 | * The object must conform to the TypeScript tsconfig schema: 79 | * 80 | * http://json.schemastore.org/tsconfig 81 | * 82 | * If omitted, then the tsconfig.json file will be read from the "projectFolder". 83 | * 84 | * DEFAULT VALUE: no overrideTsconfig section 85 | */ 86 | // "overrideTsconfig": { 87 | // . . . 88 | // } 89 | /** 90 | * This option causes the compiler to be invoked with the --skipLibCheck option. This option is not recommended 91 | * and may cause API Extractor to produce incomplete or incorrect declarations, but it may be required when 92 | * dependencies contain declarations that are incompatible with the TypeScript engine that API Extractor uses 93 | * for its analysis. Where possible, the underlying issue should be fixed rather than relying on skipLibCheck. 94 | * 95 | * DEFAULT VALUE: false 96 | */ 97 | // "skipLibCheck": true, 98 | }, 99 | /** 100 | * Configures how the API report file (*.api.md) will be generated. 101 | */ 102 | "apiReport": { 103 | /** 104 | * (REQUIRED) Whether to generate an API report. 105 | */ 106 | "enabled": true, 107 | /** 108 | * The filename for the API report files. It will be combined with "reportFolder" or "reportTempFolder" to produce 109 | * a full file path. 110 | * 111 | * The file extension should be ".api.md", and the string should not contain a path separator such as "\" or "/". 112 | * 113 | * SUPPORTED TOKENS: , 114 | * DEFAULT VALUE: ".api.md" 115 | */ 116 | "reportFileName": ".api.md", 117 | /** 118 | * Specifies the folder where the API report file is written. The file name portion is determined by 119 | * the "reportFileName" setting. 120 | * 121 | * The API report file is normally tracked by Git. Changes to it can be used to trigger a branch policy, 122 | * e.g. for an API review. 123 | * 124 | * The path is resolved relative to the folder of the config file that contains the setting; to change this, 125 | * prepend a folder token such as "". 126 | * 127 | * SUPPORTED TOKENS: , , 128 | * DEFAULT VALUE: "/etc/" 129 | */ 130 | "reportFolder": "/api/" 131 | /** 132 | * Specifies the folder where the temporary report file is written. The file name portion is determined by 133 | * the "reportFileName" setting. 134 | * 135 | * After the temporary file is written to disk, it is compared with the file in the "reportFolder". 136 | * If they are different, a production build will fail. 137 | * 138 | * The path is resolved relative to the folder of the config file that contains the setting; to change this, 139 | * prepend a folder token such as "". 140 | * 141 | * SUPPORTED TOKENS: , , 142 | * DEFAULT VALUE: "/temp/" 143 | */ 144 | // "reportTempFolder": "/temp/" 145 | }, 146 | /** 147 | * Configures how the doc model file (*.api.json) will be generated. 148 | */ 149 | "docModel": { 150 | /** 151 | * (REQUIRED) Whether to generate a doc model file. 152 | */ 153 | "enabled": true, 154 | /** 155 | * The output path for the doc model file. The file extension should be ".api.json". 156 | * 157 | * The path is resolved relative to the folder of the config file that contains the setting; to change this, 158 | * prepend a folder token such as "". 159 | * 160 | * SUPPORTED TOKENS: , , 161 | * DEFAULT VALUE: "/temp/.api.json" 162 | */ 163 | "apiJsonFilePath": "/../../docs/input/.api.json" 164 | }, 165 | /** 166 | * Configures how the .d.ts rollup file will be generated. 167 | */ 168 | "dtsRollup": { 169 | /** 170 | * (REQUIRED) Whether to generate the .d.ts rollup file. 171 | */ 172 | "enabled": false 173 | /** 174 | * Specifies the output path for a .d.ts rollup file to be generated without any trimming. 175 | * This file will include all declarations that are exported by the main entry point. 176 | * 177 | * If the path is an empty string, then this file will not be written. 178 | * 179 | * The path is resolved relative to the folder of the config file that contains the setting; to change this, 180 | * prepend a folder token such as "". 181 | * 182 | * SUPPORTED TOKENS: , , 183 | * DEFAULT VALUE: "/dist/.d.ts" 184 | */ 185 | // "untrimmedFilePath": "/dist/.d.ts", 186 | /** 187 | * Specifies the output path for a .d.ts rollup file to be generated with trimming for a "beta" release. 188 | * This file will include only declarations that are marked as "@public" or "@beta". 189 | * 190 | * The path is resolved relative to the folder of the config file that contains the setting; to change this, 191 | * prepend a folder token such as "". 192 | * 193 | * SUPPORTED TOKENS: , , 194 | * DEFAULT VALUE: "" 195 | */ 196 | // "betaTrimmedFilePath": "/dist/-beta.d.ts", 197 | /** 198 | * Specifies the output path for a .d.ts rollup file to be generated with trimming for a "public" release. 199 | * This file will include only declarations that are marked as "@public". 200 | * 201 | * If the path is an empty string, then this file will not be written. 202 | * 203 | * The path is resolved relative to the folder of the config file that contains the setting; to change this, 204 | * prepend a folder token such as "". 205 | * 206 | * SUPPORTED TOKENS: , , 207 | * DEFAULT VALUE: "" 208 | */ 209 | // "publicTrimmedFilePath": "/dist/-public.d.ts", 210 | /** 211 | * When a declaration is trimmed, by default it will be replaced by a code comment such as 212 | * "Excluded from this release type: exampleMember". Set "omitTrimmingComments" to true to remove the 213 | * declaration completely. 214 | * 215 | * DEFAULT VALUE: false 216 | */ 217 | // "omitTrimmingComments": true 218 | }, 219 | /** 220 | * Configures how the tsdoc-metadata.json file will be generated. 221 | */ 222 | "tsdocMetadata": { 223 | /** 224 | * Whether to generate the tsdoc-metadata.json file. 225 | * 226 | * DEFAULT VALUE: true 227 | */ 228 | // "enabled": true, 229 | /** 230 | * Specifies where the TSDoc metadata file should be written. 231 | * 232 | * The path is resolved relative to the folder of the config file that contains the setting; to change this, 233 | * prepend a folder token such as "". 234 | * 235 | * The default value is "", which causes the path to be automatically inferred from the "tsdocMetadata", 236 | * "typings" or "main" fields of the project's package.json. If none of these fields are set, the lookup 237 | * falls back to "tsdoc-metadata.json" in the package folder. 238 | * 239 | * SUPPORTED TOKENS: , , 240 | * DEFAULT VALUE: "" 241 | */ 242 | // "tsdocMetadataFilePath": "/dist/tsdoc-metadata.json" 243 | }, 244 | /** 245 | * Specifies what type of newlines API Extractor should use when writing output files. By default, the output files 246 | * will be written with Windows-style newlines. To use POSIX-style newlines, specify "lf" instead. 247 | * To use the OS's default newline kind, specify "os". 248 | * 249 | * DEFAULT VALUE: "crlf" 250 | */ 251 | // "newlineKind": "crlf", 252 | /** 253 | * Configures how API Extractor reports error and warning messages produced during analysis. 254 | * 255 | * There are three sources of messages: compiler messages, API Extractor messages, and TSDoc messages. 256 | */ 257 | "messages": { 258 | /** 259 | * Configures handling of diagnostic messages reported by the TypeScript compiler engine while analyzing 260 | * the input .d.ts files. 261 | * 262 | * TypeScript message identifiers start with "TS" followed by an integer. For example: "TS2551" 263 | * 264 | * DEFAULT VALUE: A single "default" entry with logLevel=warning. 265 | */ 266 | "compilerMessageReporting": { 267 | /** 268 | * Configures the default routing for messages that don't match an explicit rule in this table. 269 | */ 270 | "default": { 271 | /** 272 | * Specifies whether the message should be written to the the tool's output log. Note that 273 | * the "addToApiReportFile" property may supersede this option. 274 | * 275 | * Possible values: "error", "warning", "none" 276 | * 277 | * Errors cause the build to fail and return a nonzero exit code. Warnings cause a production build fail 278 | * and return a nonzero exit code. For a non-production build (e.g. when "api-extractor run" includes 279 | * the "--local" option), the warning is displayed but the build will not fail. 280 | * 281 | * DEFAULT VALUE: "warning" 282 | */ 283 | "logLevel": "warning" 284 | /** 285 | * When addToApiReportFile is true: If API Extractor is configured to write an API report file (.api.md), 286 | * then the message will be written inside that file; otherwise, the message is instead logged according to 287 | * the "logLevel" option. 288 | * 289 | * DEFAULT VALUE: false 290 | */ 291 | // "addToApiReportFile": false 292 | } 293 | // "TS2551": { 294 | // "logLevel": "warning", 295 | // "addToApiReportFile": true 296 | // }, 297 | // 298 | // . . . 299 | }, 300 | /** 301 | * Configures handling of messages reported by API Extractor during its analysis. 302 | * 303 | * API Extractor message identifiers start with "ae-". For example: "ae-extra-release-tag" 304 | * 305 | * DEFAULT VALUE: See api-extractor-defaults.json for the complete table of extractorMessageReporting mappings 306 | */ 307 | "extractorMessageReporting": { 308 | "default": { 309 | "logLevel": "warning" 310 | // "addToApiReportFile": false 311 | } 312 | // "ae-extra-release-tag": { 313 | // "logLevel": "warning", 314 | // "addToApiReportFile": true 315 | // }, 316 | // 317 | // . . . 318 | }, 319 | /** 320 | * Configures handling of messages reported by the TSDoc parser when analyzing code comments. 321 | * 322 | * TSDoc message identifiers start with "tsdoc-". For example: "tsdoc-link-tag-unescaped-text" 323 | * 324 | * DEFAULT VALUE: A single "default" entry with logLevel=warning. 325 | */ 326 | "tsdocMessageReporting": { 327 | "default": { 328 | "logLevel": "warning" 329 | // "addToApiReportFile": false 330 | } 331 | // "tsdoc-link-tag-unescaped-text": { 332 | // "logLevel": "warning", 333 | // "addToApiReportFile": true 334 | // }, 335 | // 336 | // . . . 337 | } 338 | } 339 | } -------------------------------------------------------------------------------- /packages/core/api/post-me.api.md: -------------------------------------------------------------------------------- 1 | ## API Report File for "post-me" 2 | 3 | > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). 4 | 5 | ```ts 6 | 7 | // @public (undocumented) 8 | export class BareMessenger implements Messenger { 9 | // Warning: (ae-forgotten-export) The symbol "Postable" needs to be exported by the entry point index.d.ts 10 | constructor(postable: Postable); 11 | // Warning: (ae-forgotten-export) The symbol "MessageListener" needs to be exported by the entry point index.d.ts 12 | // Warning: (ae-forgotten-export) The symbol "ListenerRemover" needs to be exported by the entry point index.d.ts 13 | // 14 | // (undocumented) 15 | addMessageListener: (listener: MessageListener) => ListenerRemover; 16 | // (undocumented) 17 | postMessage: (message: any, transfer?: Transferable[]) => void; 18 | } 19 | 20 | // Warning: (ae-internal-missing-underscore) The name "Callable" should be prefixed with an underscore because the declaration is marked as @internal 21 | // 22 | // @internal (undocumented) 23 | export type Callable, R> = (...args: A) => R; 24 | 25 | // @public 26 | export type CallOptions = { 27 | transfer?: Transferable[]; 28 | }; 29 | 30 | // Warning: (ae-incompatible-release-tags) The symbol "ChildHandshake" is marked as @public, but its signature references "MethodsType" which is marked as @internal 31 | // 32 | // @public 33 | export function ChildHandshake(messenger: Messenger, localMethods?: M): Promise; 34 | 35 | // Warning: (ae-incompatible-release-tags) The symbol "ConcreteEmitter" is marked as @public, but its signature references "EventsType" which is marked as @internal 36 | // 37 | // @public 38 | export class ConcreteEmitter implements Emitter { 39 | constructor(); 40 | addEventListener(eventName: K, listener: (data: E[K]) => void): void; 41 | // @internal (undocumented) 42 | protected emit(eventName: K, data: E[K]): void; 43 | once(eventName: K): Promise; 44 | // @internal (undocumented) 45 | protected removeAllListeners(): void; 46 | removeEventListener(eventName: K, listener: (data: E[K]) => void): void; 47 | } 48 | 49 | // Warning: (ae-incompatible-release-tags) The symbol "Connection" is marked as @public, but its signature references "MethodsType" which is marked as @internal 50 | // Warning: (ae-incompatible-release-tags) The symbol "Connection" is marked as @public, but its signature references "EventsType" which is marked as @internal 51 | // 52 | // @public 53 | export interface Connection { 54 | close(): void; 55 | localHandle(): LocalHandle; 56 | remoteHandle(): RemoteHandle; 57 | } 58 | 59 | // @public 60 | export function debug(namespace: string, log?: (...data: any[]) => void): (...data: any[]) => void; 61 | 62 | // @public 63 | export function DebugMessenger(messenger: Messenger, log?: (...data: any[]) => void): Messenger; 64 | 65 | // @public 66 | export type EmitOptions = { 67 | transfer?: Transferable[]; 68 | }; 69 | 70 | // Warning: (ae-incompatible-release-tags) The symbol "Emitter" is marked as @public, but its signature references "EventsType" which is marked as @internal 71 | // 72 | // @public 73 | export interface Emitter { 74 | addEventListener(eventName: K, listener: (data: E[K]) => void): void; 75 | once(eventName: K): Promise; 76 | removeEventListener(eventName: K, listener: (data: E[K]) => void): void; 77 | } 78 | 79 | // Warning: (ae-forgotten-export) The symbol "KeyType" needs to be exported by the entry point index.d.ts 80 | // Warning: (ae-internal-missing-underscore) The name "EventsType" should be prefixed with an underscore because the declaration is marked as @internal 81 | // 82 | // @internal (undocumented) 83 | export type EventsType = Record; 84 | 85 | // Warning: (ae-internal-missing-underscore) The name "InnerType" should be prefixed with an underscore because the declaration is marked as @internal 86 | // 87 | // @internal (undocumented) 88 | export type InnerType> = T extends Promise ? U : T; 89 | 90 | // Warning: (ae-incompatible-release-tags) The symbol "LocalHandle" is marked as @public, but its signature references "MethodsType" which is marked as @internal 91 | // Warning: (ae-incompatible-release-tags) The symbol "LocalHandle" is marked as @public, but its signature references "EventsType" which is marked as @internal 92 | // 93 | // @public 94 | export interface LocalHandle { 95 | emit(eventName: K, data: E[K], options?: EmitOptions): void; 96 | setEmitTransfer(eventName: K, transfer: (payload: E[K]) => Transferable[]): void; 97 | setMethod(methodName: K, method: M[K]): void; 98 | setMethods(methods: M): void; 99 | // Warning: (ae-incompatible-release-tags) The symbol "setReturnTransfer" is marked as @public, but its signature references "InnerType" which is marked as @internal 100 | setReturnTransfer(methodName: K, transfer: (result: InnerType>) => Transferable[]): void; 101 | } 102 | 103 | // @public 104 | export interface Messenger { 105 | addMessageListener(listener: MessageListener): ListenerRemover; 106 | postMessage(message: any, transfer?: Transferable[]): void; 107 | } 108 | 109 | // Warning: (ae-internal-missing-underscore) The name "MethodsType" should be prefixed with an underscore because the declaration is marked as @internal 110 | // 111 | // @internal (undocumented) 112 | export type MethodsType = Record>>; 113 | 114 | // Warning: (ae-incompatible-release-tags) The symbol "ParentHandshake" is marked as @public, but its signature references "MethodsType" which is marked as @internal 115 | // 116 | // @public 117 | export function ParentHandshake(messenger: Messenger, localMethods?: M0, maxAttempts?: number, attemptsInterval?: number): Promise; 118 | 119 | // @public 120 | export class PortMessenger extends BareMessenger implements Messenger { 121 | constructor({ port }: { 122 | port: MessagePort; 123 | }); 124 | } 125 | 126 | // Warning: (ae-incompatible-release-tags) The symbol "RemoteHandle" is marked as @public, but its signature references "MethodsType" which is marked as @internal 127 | // Warning: (ae-incompatible-release-tags) The symbol "RemoteHandle" is marked as @public, but its signature references "EventsType" which is marked as @internal 128 | // 129 | // @public 130 | export interface RemoteHandle extends Emitter { 131 | // Warning: (ae-incompatible-release-tags) The symbol "call" is marked as @public, but its signature references "InnerType" which is marked as @internal 132 | call(methodName: K, ...args: Parameters): Promise>>; 133 | // Warning: (ae-incompatible-release-tags) The symbol "customCall" is marked as @public, but its signature references "InnerType" which is marked as @internal 134 | customCall(methodName: K, args: Parameters, options?: CallOptions): Promise>>; 135 | setCallTransfer(methodName: K, transfer: (...args: Parameters) => Transferable[]): void; 136 | } 137 | 138 | // Warning: (ae-internal-missing-underscore) The name "ValueOrPromise" should be prefixed with an underscore because the declaration is marked as @internal 139 | // 140 | // @internal (undocumented) 141 | export type ValueOrPromise = T | Promise; 142 | 143 | // @public 144 | export class WindowMessenger implements Messenger { 145 | constructor({ localWindow, remoteWindow, remoteOrigin, }: { 146 | localWindow?: Window; 147 | remoteWindow: Window; 148 | remoteOrigin: string; 149 | }); 150 | // (undocumented) 151 | addMessageListener: (listener: MessageListener) => ListenerRemover; 152 | // (undocumented) 153 | postMessage: (message: any, transfer?: Transferable[]) => void; 154 | } 155 | 156 | // @public 157 | export class WorkerMessenger extends BareMessenger implements Messenger { 158 | constructor({ worker }: { 159 | worker: Worker; 160 | }); 161 | } 162 | 163 | 164 | ``` 165 | -------------------------------------------------------------------------------- /packages/core/demo/build.js: -------------------------------------------------------------------------------- 1 | const argv = process.argv.slice(2); 2 | 3 | if (argv.length === 0) { 4 | throw new Error('Pleas provide a destination folder.'); 5 | } 6 | 7 | const destination = argv[0]; 8 | 9 | const fs = require('fs-extra'); 10 | 11 | if (fs.existsSync(destination)) { 12 | fs.rmdirSync(destination, { recursive: true }); 13 | } 14 | 15 | fs.mkdirSync(destination); 16 | 17 | fs.copySync('dist/index.mjs', `${destination}/post-me.mjs`); 18 | fs.copySync('dist/index.js', `${destination}/post-me.js`); 19 | fs.copySync('demo', destination); 20 | -------------------------------------------------------------------------------- /packages/core/demo/child.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | PostMe Child 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | 18 | 19 | 42 | 43 | -------------------------------------------------------------------------------- /packages/core/demo/child.js: -------------------------------------------------------------------------------- 1 | import { ChildHandshake, WindowMessenger, DebugMessenger } from './post-me.mjs'; 2 | 3 | const debugNamespace = `post-me:${window.name}`; 4 | debug.enable(debugNamespace); 5 | 6 | let title = ''; 7 | let color = '#ffffff'; 8 | 9 | const container = document.getElementById('container'); 10 | const titleElement = document.createElement('h1'); 11 | titleElement.innerHTML = title; 12 | container.appendChild(titleElement); 13 | container.style.backgroundColor = color; 14 | 15 | const methods = { 16 | getTitle: () => title, 17 | setTitle: (t) => { 18 | title = t; 19 | titleElement.innerHTML = title; 20 | }, 21 | getColor: () => color, 22 | setColor: (c) => { 23 | color = c; 24 | container.style.backgroundColor = color; 25 | }, 26 | }; 27 | 28 | const log = debug(debugNamespace); 29 | let messenger = new WindowMessenger({ 30 | localWindow: window, 31 | remoteWindow: window.parent, 32 | remoteOrigin: '*', 33 | }); 34 | messenger = DebugMessenger(messenger, log); 35 | ChildHandshake(messenger, methods).then((connection) => { 36 | const localHandle = connection.localHandle(); 37 | const remoteHandle = connection.remoteHandle(); 38 | 39 | { 40 | const section = document.createElement('div'); 41 | section.style.marginBottom = '0.5rem'; 42 | const input = document.createElement('input'); 43 | const button = document.createElement('button'); 44 | button.innerHTML = 'Set Title'; 45 | button.onclick = () => { 46 | remoteHandle.call('setTitle', input.value); 47 | }; 48 | section.appendChild(input); 49 | section.appendChild(button); 50 | container.appendChild(section); 51 | 52 | // Call a method on the parent to prepopulate the input 53 | remoteHandle.call('getTitle').then((title) => (input.value = title)); 54 | } 55 | 56 | { 57 | const section = document.createElement('div'); 58 | section.style.marginBottom = '0.5rem'; 59 | const input = document.createElement('input'); 60 | const button = document.createElement('button'); 61 | button.innerHTML = 'Set Color'; 62 | button.onclick = () => { 63 | remoteHandle.call('setColor', input.value); 64 | }; 65 | section.appendChild(input); 66 | section.appendChild(button); 67 | container.appendChild(section); 68 | 69 | // Call a method on the parent to prepopulate the input 70 | remoteHandle.call('getColor').then((color) => (input.value = color)); 71 | } 72 | 73 | { 74 | const section = document.createElement('div'); 75 | section.style.marginBottom = '0.5rem'; 76 | const button = document.createElement('button'); 77 | button.innerHTML = 'Emit ping event'; 78 | button.onclick = () => { 79 | localHandle.emit('ping'); 80 | }; 81 | section.appendChild(button); 82 | container.appendChild(section); 83 | } 84 | 85 | { 86 | const section = document.createElement('div'); 87 | section.style.marginBottom = '0.5rem'; 88 | let nPings = 0; 89 | const pingParagraph = document.createElement('span'); 90 | pingParagraph.innerHTML = 'Ping events received: '; 91 | const pingSpan = document.createElement('span'); 92 | pingSpan.innerHTML = nPings; 93 | pingSpan.style.fontWeight = 'bold'; 94 | pingParagraph.appendChild(pingSpan); 95 | section.appendChild(pingParagraph); 96 | container.appendChild(section); 97 | 98 | remoteHandle.addEventListener('ping', () => { 99 | nPings += 1; 100 | pingSpan.innerHTML = nPings; 101 | }); 102 | } 103 | }); 104 | -------------------------------------------------------------------------------- /packages/core/demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | PostMe Demo 10 | 11 | 12 | 13 |
14 |
15 | 16 |
17 |
18 |
19 |
20 |
21 | 22 |
23 |
24 |
25 |
26 |
27 |
28 | 29 | 30 | 31 | 32 | 33 | 34 | 87 | 88 | -------------------------------------------------------------------------------- /packages/core/demo/parent.js: -------------------------------------------------------------------------------- 1 | import { 2 | ParentHandshake, 3 | WindowMessenger, 4 | WorkerMessenger, 5 | DebugMessenger, 6 | } from './post-me.mjs'; 7 | 8 | debug.enable( 9 | 'post-me:parent0,post-me:parent1,post-me:parent2,post-me:parent3,post-me:parentW' 10 | ); 11 | 12 | let title = 'Parent'; 13 | let color = '#eeeeee'; 14 | 15 | const container = document.getElementById(`parent-container`); 16 | const titleElement = document.createElement('h1'); 17 | titleElement.innerHTML = title; 18 | container.appendChild(titleElement); 19 | container.style.backgroundColor = color; 20 | const childrenControlsContainer = document.createElement('div'); 21 | container.appendChild(childrenControlsContainer); 22 | const workerControlsContainer = document.createElement('div'); 23 | container.appendChild(workerControlsContainer); 24 | 25 | const methods = { 26 | getTitle: () => title, 27 | setTitle: (t) => { 28 | title = t; 29 | titleElement.innerHTML = title; 30 | }, 31 | getColor: () => color, 32 | setColor: (c) => { 33 | color = c; 34 | container.style.backgroundColor = color; 35 | }, 36 | }; 37 | 38 | const children = [0, 1, 2, 3]; 39 | 40 | const defaultTitles = { 41 | 0: 'Child 0', 42 | 1: 'Child 1', 43 | 2: 'Child 2', 44 | 3: 'Child 3', 45 | }; 46 | 47 | const defaultColors = { 48 | 0: '#eeaaaa', 49 | 1: '#aaeeaa', 50 | 2: '#aaaaee', 51 | 3: '#eeeeaa', 52 | }; 53 | 54 | const createChildWindow = (i) => { 55 | return new Promise((resolve) => { 56 | const childContainer = document.getElementById(`child${i}-container`); 57 | const childFrame = document.createElement('iframe'); 58 | childFrame.name = `child${i}`; 59 | childFrame.src = './child.html'; 60 | childFrame.width = '100%'; 61 | childFrame.height = '100%'; 62 | childContainer.appendChild(childFrame); 63 | childFrame.onload = () => { 64 | const childWindow = childFrame.contentWindow; 65 | resolve(childWindow); 66 | }; 67 | }); 68 | }; 69 | 70 | const makeHandshake = (i, childWindow) => { 71 | const log = debug(`post-me:parent${i}`); 72 | let messenger = new WindowMessenger({ 73 | localWindow: window, 74 | remoteWindow: childWindow, 75 | remoteOrigin: '*', 76 | }); 77 | messenger = DebugMessenger(messenger, log); 78 | return ParentHandshake(messenger, methods); 79 | }; 80 | 81 | const createChildControls = (i, controlsContainer, connection) => { 82 | const localHandle = connection.localHandle(); 83 | const remoteHandle = connection.remoteHandle(); 84 | 85 | const defaultTitle = defaultTitles[i]; 86 | const defaultColor = defaultColors[i]; 87 | remoteHandle.call('setTitle', defaultTitle); 88 | remoteHandle.call('setColor', defaultColor); 89 | 90 | const title = document.createElement('h4'); 91 | title.innerHTML = `Child ${i}`; 92 | controlsContainer.appendChild(title); 93 | 94 | { 95 | const section = document.createElement('div'); 96 | section.style.marginBottom = '0.5rem'; 97 | const input = document.createElement('input'); 98 | input.style.width = '10rem'; 99 | input.value = defaultTitle; 100 | const button = document.createElement('button'); 101 | button.innerHTML = 'Set Title'; 102 | button.onclick = () => { 103 | remoteHandle.call('setTitle', input.value); 104 | }; 105 | section.appendChild(input); 106 | section.appendChild(button); 107 | controlsContainer.appendChild(section); 108 | } 109 | 110 | { 111 | const section = document.createElement('div'); 112 | section.style.marginBottom = '0.5rem'; 113 | const input = document.createElement('input'); 114 | input.style.width = '10rem'; 115 | input.value = defaultColor; 116 | const button = document.createElement('button'); 117 | button.innerHTML = 'Set Color'; 118 | button.onclick = () => { 119 | remoteHandle.call('setColor', input.value); 120 | }; 121 | section.appendChild(input); 122 | section.appendChild(button); 123 | controlsContainer.appendChild(section); 124 | } 125 | 126 | { 127 | const section = document.createElement('div'); 128 | section.style.marginBottom = '0.5rem'; 129 | const button = document.createElement('button'); 130 | button.innerHTML = 'Emit ping event'; 131 | button.onclick = () => { 132 | localHandle.emit('ping'); 133 | }; 134 | section.appendChild(button); 135 | controlsContainer.appendChild(section); 136 | } 137 | 138 | { 139 | const section = document.createElement('div'); 140 | section.style.marginBottom = '0.5rem'; 141 | let nPings = 0; 142 | const pingParagraph = document.createElement('span'); 143 | pingParagraph.innerHTML = 'Ping events received: '; 144 | const pingSpan = document.createElement('span'); 145 | pingSpan.innerHTML = nPings; 146 | pingSpan.style.fontWeight = 'bold'; 147 | pingParagraph.appendChild(pingSpan); 148 | section.appendChild(pingParagraph); 149 | controlsContainer.appendChild(section); 150 | 151 | remoteHandle.addEventListener('ping', () => { 152 | nPings += 1; 153 | pingSpan.innerHTML = nPings; 154 | }); 155 | } 156 | }; 157 | 158 | const initChild = async (i) => { 159 | const controlsContainer = document.createElement('div'); 160 | childrenControlsContainer.appendChild(controlsContainer); 161 | const childWindow = await createChildWindow(i); 162 | const connection = await makeHandshake(i, childWindow); 163 | createChildControls(i, controlsContainer, connection); 164 | }; 165 | 166 | children.forEach((i) => initChild(i)); 167 | 168 | // Create the worker 169 | { 170 | const worker = new Worker('./worker.js'); 171 | const log = debug('post-me:parentW'); 172 | let messenger = new WorkerMessenger({ worker }); 173 | messenger = DebugMessenger(messenger, log); 174 | 175 | ParentHandshake(messenger, {}).then((connection) => { 176 | const remoteHandle = connection.remoteHandle(); 177 | 178 | const title = document.createElement('h4'); 179 | title.innerHTML = `Worker`; 180 | workerControlsContainer.appendChild(title); 181 | 182 | { 183 | const section = document.createElement('div'); 184 | section.style.marginBottom = '0.5rem'; 185 | 186 | const inputA = document.createElement('input'); 187 | inputA.style.width = '3rem'; 188 | inputA.type = 'number'; 189 | inputA.value = 1; 190 | 191 | const inputB = document.createElement('input'); 192 | inputB.style.width = '3rem'; 193 | inputB.type = 'number'; 194 | inputB.value = 2; 195 | 196 | const inputR = document.createElement('input'); 197 | inputR.style.width = '3rem'; 198 | inputR.type = 'number'; 199 | inputR.disabled = true; 200 | 201 | const button = document.createElement('button'); 202 | button.innerHTML = 'Calculate'; 203 | button.onclick = () => { 204 | remoteHandle 205 | .call('sum', parseFloat(inputA.value), parseFloat(inputB.value)) 206 | .then((result) => { 207 | inputR.value = result; 208 | }); 209 | }; 210 | 211 | const opSpan = document.createElement('span'); 212 | opSpan.innerHTML = ' + '; 213 | const eqSpan = document.createElement('span'); 214 | eqSpan.innerHTML = ' = '; 215 | 216 | section.appendChild(inputA); 217 | section.appendChild(opSpan); 218 | section.appendChild(inputB); 219 | section.appendChild(eqSpan); 220 | section.appendChild(inputR); 221 | section.appendChild(button); 222 | workerControlsContainer.appendChild(section); 223 | } 224 | 225 | { 226 | const section = document.createElement('div'); 227 | section.style.marginBottom = '0.5rem'; 228 | 229 | const inputA = document.createElement('input'); 230 | inputA.style.width = '3rem'; 231 | inputA.type = 'number'; 232 | inputA.value = 3; 233 | 234 | const inputB = document.createElement('input'); 235 | inputB.style.width = '3rem'; 236 | inputB.type = 'number'; 237 | inputB.value = 4; 238 | 239 | const inputR = document.createElement('input'); 240 | inputR.style.width = '3rem'; 241 | inputR.type = 'number'; 242 | inputR.disabled = true; 243 | 244 | const button = document.createElement('button'); 245 | button.innerHTML = 'Calculate'; 246 | button.onclick = () => { 247 | remoteHandle 248 | .call('mul', parseFloat(inputA.value), parseFloat(inputB.value)) 249 | .then((result) => { 250 | inputR.value = result; 251 | }); 252 | }; 253 | 254 | const opSpan = document.createElement('span'); 255 | opSpan.innerHTML = ' * '; 256 | const eqSpan = document.createElement('span'); 257 | eqSpan.innerHTML = ' = '; 258 | 259 | section.appendChild(inputA); 260 | section.appendChild(opSpan); 261 | section.appendChild(inputB); 262 | section.appendChild(eqSpan); 263 | section.appendChild(inputR); 264 | section.appendChild(button); 265 | workerControlsContainer.appendChild(section); 266 | } 267 | }); 268 | } 269 | -------------------------------------------------------------------------------- /packages/core/demo/worker.js: -------------------------------------------------------------------------------- 1 | importScripts('https://unpkg.com/debug-browser/dist/index.js'); 2 | importScripts('./post-me.js'); 3 | 4 | const { WorkerMessenger, DebugMessenger, ChildHandshake } = PostMe; 5 | 6 | debug.enable('post-me:worker'); 7 | 8 | const methods = { 9 | sum: (x, y) => x + y, 10 | mul: (x, y) => x * y, 11 | }; 12 | 13 | const log = debug('post-me:worker'); 14 | let messenger = new WorkerMessenger({ worker: self }); 15 | messenger = DebugMessenger(messenger, log); 16 | ChildHandshake(messenger, methods).then((_connection) => {}); 17 | -------------------------------------------------------------------------------- /packages/core/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'jsdom', 4 | }; -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "post-me", 3 | "version": "0.4.5", 4 | "description": "Use web Workers and other Windows through a simple Promise API", 5 | "author": { 6 | "name": "Alessandro Genova", 7 | "email": "ales.genova@gmail.com", 8 | "url": "https://github.com/alesgenova" 9 | }, 10 | "license": "MIT", 11 | "homepage": "https://github.com/alesgenova/post-me", 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/alesgenova/post-me.git", 15 | "directory": "packages/core" 16 | }, 17 | "bugs": { 18 | "url": "https://github.com/alesgenova/post-me/issues", 19 | "email": "ales.genova@gmail.com" 20 | }, 21 | "main": "./dist/index.js", 22 | "exports": { 23 | "import": "./dist/index.esnext.mjs", 24 | "require": "./dist/index.js" 25 | }, 26 | "module": "./dist/index.mjs", 27 | "types": "./dist/index.d.ts", 28 | "files": [ 29 | "dist" 30 | ], 31 | "sideEffects": false, 32 | "scripts": { 33 | "build": "npm run build:src && npm run build:types", 34 | "build:src": "rollup -c", 35 | "build:types": "tsc --emitDeclarationOnly", 36 | "build:docs": "npm run build:types && api-extractor run --local --verbose", 37 | "build:demo": "npm run build:src && node demo/build.js www", 38 | "prepublishOnly": "npm run build", 39 | "demo": "npm run build:demo && npx serve www", 40 | "deploy:demo": "npm run build:demo && gh-pages -d www", 41 | "test": "jest --coverage tests", 42 | "prettier:check-staged": "pretty-quick --staged --check --pattern '**/*.{js,jsx,ts,tsx,css,html}'", 43 | "prettier:write-staged": "pretty-quick --staged --write --pattern '**/*.{js,jsx,ts,tsx,css,html}'", 44 | "prettier:check-modified": "pretty-quick --check --pattern '**/*.{js,jsx,ts,tsx,css,html}'", 45 | "prettier:write-modified": "pretty-quick --write --pattern '**/*.{js,jsx,ts,tsx,css,html}'", 46 | "prettier:check-all": "prettier --check '**/*.{js,jsx,ts,tsx,css,html}'", 47 | "prettier:write-all": "prettier --write '**/*.{js,jsx,ts,tsx,css,html}'" 48 | }, 49 | "devDependencies": { 50 | "@babel/core": "^7.11.6", 51 | "@babel/preset-env": "^7.12.11", 52 | "@microsoft/api-extractor": "^7.13.0", 53 | "@rollup/plugin-babel": "^5.2.2", 54 | "@rollup/plugin-typescript": "^8.1.0", 55 | "@types/jest": "^26.0.15", 56 | "@types/jsdom": "^16.2.5", 57 | "gh-pages": "^3.1.0", 58 | "husky": "^4.3.0", 59 | "jest": "^26.6.3", 60 | "jsdom": "^16.4.0", 61 | "prettier": "^2.2.1", 62 | "pretty-quick": "^3.1.0", 63 | "rollup": "^2.35.1", 64 | "ts-jest": "^26.4.4", 65 | "tslib": "^2.0.3", 66 | "typescript": "^4.1.3" 67 | }, 68 | "husky": { 69 | "hooks": { 70 | "pre-commit": "npm run prettier:write-staged" 71 | } 72 | }, 73 | "publishConfig": { 74 | "access": "public" 75 | }, 76 | "keywords": [ 77 | "communication", 78 | "postmessage", 79 | "iframe", 80 | "worker", 81 | "workers", 82 | "web-worker", 83 | "web-workers", 84 | "concurrency", 85 | "parallel-computing", 86 | "front-end", 87 | "back-end", 88 | "promise", 89 | "typescript", 90 | "postmate" 91 | ] 92 | } 93 | -------------------------------------------------------------------------------- /packages/core/rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from '@rollup/plugin-typescript'; 2 | import { getBabelOutputPlugin } from '@rollup/plugin-babel'; 3 | 4 | export default { 5 | input: 'src/index.ts', 6 | output: [ 7 | // ES Module, straight TS to JS compilation 8 | { 9 | file: 'dist/index.esnext.mjs', 10 | format: 'esm' 11 | }, 12 | // ES Module, transpiled to ES5 13 | { 14 | file: 'dist/index.mjs', 15 | format: 'esm', 16 | plugins: [ 17 | getBabelOutputPlugin({ 18 | presets: [['@babel/preset-env', { modules: false }]] 19 | }) 20 | ] 21 | }, 22 | // UMD, transpiled to ES5 23 | { 24 | file: 'dist/index.js', 25 | format: 'esm', 26 | plugins: [ 27 | getBabelOutputPlugin({ 28 | moduleId: 'PostMe', 29 | presets: [['@babel/preset-env', { modules: 'umd' }]], 30 | }) 31 | ] 32 | } 33 | ], 34 | plugins: [typescript({ target: 'esnext', module: 'esnext', declaration: false })] 35 | } 36 | -------------------------------------------------------------------------------- /packages/core/src/common.ts: -------------------------------------------------------------------------------- 1 | export const MARKER = '@post-me'; 2 | 3 | export type IdType = number; 4 | export type KeyType = string; 5 | 6 | /** 7 | * The options that can be passed when calling a method on the other context with RemoteHandle.customCall() 8 | * 9 | * @public 10 | * 11 | */ 12 | export type CallOptions = { 13 | transfer?: Transferable[]; 14 | }; 15 | 16 | /** 17 | * The options that can be passed when emitting an event to the other context with LocalHandle.emit() 18 | * 19 | * @public 20 | * 21 | */ 22 | export type EmitOptions = { 23 | transfer?: Transferable[]; 24 | }; 25 | 26 | /** 27 | * @internal 28 | */ 29 | export type MethodsType = Record>>; 30 | 31 | /** 32 | * @internal 33 | */ 34 | export type EventsType = Record; 35 | 36 | /** 37 | * @internal 38 | */ 39 | export type Callable
, R> = (...args: A) => R; 40 | 41 | /** 42 | * @internal 43 | */ 44 | export type ValueOrPromise = T | Promise; 45 | 46 | /** 47 | * @internal 48 | */ 49 | export type InnerType> = T extends Promise< 50 | infer U 51 | > 52 | ? U 53 | : T; 54 | 55 | export function createUniqueIdFn() { 56 | let __id = 0; 57 | return function () { 58 | const id = __id; 59 | __id += 1; 60 | return id; 61 | }; 62 | } 63 | -------------------------------------------------------------------------------- /packages/core/src/connection.ts: -------------------------------------------------------------------------------- 1 | import { MethodsType, EventsType } from './common'; 2 | import { Dispatcher } from './dispatcher'; 3 | import { 4 | ConcreteLocalHandle, 5 | ConcreteRemoteHandle, 6 | LocalHandle, 7 | RemoteHandle, 8 | } from './handles'; 9 | 10 | /** 11 | * An active connection between two contexts 12 | * 13 | * @typeParam M0 - The methods exposed by this context 14 | * @typeParam E0 - The events exposed by this context 15 | * @typeParam M1 - The methods exposed by the other context 16 | * @typeParam E1 - The events exposed by the other context 17 | * 18 | * @public 19 | * 20 | */ 21 | export interface Connection< 22 | M0 extends MethodsType = any, 23 | E0 extends EventsType = any, 24 | M1 extends MethodsType = any, 25 | E1 extends EventsType = any 26 | > { 27 | /** 28 | * Get a handle to the local end of the connection 29 | * 30 | * @returns A {@link LocalHandle} to the local side of the Connection 31 | */ 32 | localHandle(): LocalHandle; 33 | 34 | /** 35 | * Get a handle to the other end of the connection 36 | * 37 | * @returns A {@link RemoteHandle} to the other side of the Connection 38 | */ 39 | remoteHandle(): RemoteHandle; 40 | 41 | /** 42 | * Stop listening to incoming message from the other side 43 | */ 44 | close(): void; 45 | } 46 | 47 | export class ConcreteConnection implements Connection { 48 | private _dispatcher: Dispatcher; 49 | private _remoteHandle: ConcreteRemoteHandle; 50 | private _localHandle: ConcreteLocalHandle; 51 | 52 | constructor(dispatcher: Dispatcher, localMethods: M0) { 53 | this._dispatcher = dispatcher; 54 | this._localHandle = new ConcreteLocalHandle( 55 | dispatcher, 56 | localMethods 57 | ); 58 | this._remoteHandle = new ConcreteRemoteHandle(dispatcher); 59 | } 60 | 61 | close() { 62 | this._dispatcher.close(); 63 | this.remoteHandle().close(); 64 | } 65 | 66 | localHandle() { 67 | return this._localHandle; 68 | } 69 | 70 | remoteHandle() { 71 | return this._remoteHandle; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /packages/core/src/dispatcher.ts: -------------------------------------------------------------------------------- 1 | import { IdType, KeyType, createUniqueIdFn } from './common'; 2 | import { Messenger } from './messenger'; 3 | import { ConcreteEmitter } from './emitter'; 4 | import { 5 | isMessage, 6 | isCallMessage, 7 | isResponseMessage, 8 | isCallbackMessage, 9 | isEventMessage, 10 | MessageType, 11 | CallMessage, 12 | EventMessage, 13 | ResponseMessage, 14 | CallbackMessage, 15 | createCallMessage, 16 | createEventMessage, 17 | createResponsMessage, 18 | createCallbackMessage, 19 | HandshakeResponseMessage, 20 | isHandshakeResponseMessage, 21 | createHandshakeRequestMessage, 22 | HandshakeRequestMessage, 23 | isHandshakeRequestMessage, 24 | createHandshakeResponseMessage, 25 | } from './messages'; 26 | 27 | function makeCallbackEvent(requestId: IdType): string { 28 | return `callback_${requestId}`; 29 | } 30 | 31 | function makeResponseEvent(requestId: IdType): string { 32 | return `response_${requestId}`; 33 | } 34 | 35 | export type DispatcherEvents = { 36 | [x: string]: 37 | | CallMessage 38 | | EventMessage 39 | | CallbackMessage 40 | | ResponseMessage; 41 | [MessageType.Call]: CallMessage; 42 | [MessageType.Event]: EventMessage; 43 | }; 44 | 45 | export class Dispatcher extends ConcreteEmitter { 46 | private messenger: Messenger; 47 | private sessionId: IdType; 48 | private removeMessengerListener: () => void; 49 | private uniqueId: () => IdType; 50 | 51 | constructor(messenger: Messenger, sessionId: IdType) { 52 | super(); 53 | 54 | this.uniqueId = createUniqueIdFn(); 55 | 56 | this.messenger = messenger; 57 | this.sessionId = sessionId; 58 | 59 | this.removeMessengerListener = this.messenger.addMessageListener( 60 | this.messengerListener.bind(this) 61 | ); 62 | } 63 | 64 | private messengerListener(event: MessageEvent) { 65 | const { data } = event; 66 | 67 | if (!isMessage(data)) { 68 | return; 69 | } 70 | 71 | if (this.sessionId !== data.sessionId) { 72 | return; 73 | } 74 | 75 | if (isCallMessage(data)) { 76 | this.emit(MessageType.Call, data); 77 | } else if (isResponseMessage(data)) { 78 | this.emit(makeResponseEvent(data.requestId), data); 79 | } else if (isEventMessage(data)) { 80 | this.emit(MessageType.Event, data); 81 | } else if (isCallbackMessage(data)) { 82 | this.emit(makeCallbackEvent(data.requestId), data); 83 | } 84 | } 85 | 86 | callOnRemote(methodName: KeyType, args: any[], transfer?: Transferable[]) { 87 | const requestId = this.uniqueId(); 88 | const callbackEvent = makeCallbackEvent(requestId); 89 | const responseEvent = makeResponseEvent(requestId); 90 | const message = createCallMessage( 91 | this.sessionId, 92 | requestId, 93 | methodName, 94 | args 95 | ); 96 | this.messenger.postMessage(message, transfer); 97 | 98 | return { callbackEvent, responseEvent }; 99 | } 100 | 101 | respondToRemote( 102 | requestId: IdType, 103 | value: any, 104 | error: any, 105 | transfer?: Transferable[] 106 | ) { 107 | if (error instanceof Error) { 108 | error = { 109 | name: error.name, 110 | message: error.message, 111 | }; 112 | } 113 | const message = createResponsMessage( 114 | this.sessionId, 115 | requestId, 116 | value, 117 | error 118 | ); 119 | this.messenger.postMessage(message, transfer); 120 | } 121 | 122 | callbackToRemote(requestId: IdType, callbackId: IdType, args: any[]) { 123 | const message = createCallbackMessage( 124 | this.sessionId, 125 | requestId, 126 | callbackId, 127 | args 128 | ); 129 | this.messenger.postMessage(message); 130 | } 131 | 132 | emitToRemote(eventName: KeyType, payload: any, transfer?: Transferable[]) { 133 | const message = createEventMessage(this.sessionId, eventName, payload); 134 | this.messenger.postMessage(message, transfer); 135 | } 136 | 137 | close() { 138 | this.removeMessengerListener(); 139 | this.removeAllListeners(); 140 | } 141 | } 142 | 143 | export type ParentHandshakeDispatcherEvents = { 144 | [x: number]: HandshakeResponseMessage; 145 | }; 146 | 147 | export class ParentHandshakeDispatcher extends ConcreteEmitter { 148 | private messenger: Messenger; 149 | private sessionId: IdType; 150 | private removeMessengerListener: () => void; 151 | 152 | constructor(messenger: Messenger, sessionId: IdType) { 153 | super(); 154 | 155 | this.messenger = messenger; 156 | this.sessionId = sessionId; 157 | 158 | this.removeMessengerListener = this.messenger.addMessageListener( 159 | this.messengerListener.bind(this) 160 | ); 161 | } 162 | 163 | private messengerListener(event: MessageEvent) { 164 | const { data } = event; 165 | 166 | if (!isMessage(data)) { 167 | return; 168 | } 169 | 170 | if (this.sessionId !== data.sessionId) { 171 | return; 172 | } 173 | 174 | if (isHandshakeResponseMessage(data)) { 175 | this.emit(data.sessionId, data); 176 | } 177 | } 178 | 179 | initiateHandshake(): IdType { 180 | const message = createHandshakeRequestMessage(this.sessionId); 181 | this.messenger.postMessage(message); 182 | return this.sessionId; 183 | } 184 | 185 | close() { 186 | this.removeMessengerListener(); 187 | this.removeAllListeners(); 188 | } 189 | } 190 | 191 | export type ChildHandshakeDispatcherEvents = { 192 | [MessageType.HandshakeRequest]: HandshakeRequestMessage; 193 | }; 194 | 195 | export class ChildHandshakeDispatcher extends ConcreteEmitter { 196 | private messenger: Messenger; 197 | private removeMessengerListener: () => void; 198 | 199 | constructor(messenger: Messenger) { 200 | super(); 201 | 202 | this.messenger = messenger; 203 | 204 | this.removeMessengerListener = this.messenger.addMessageListener( 205 | this.messengerListener.bind(this) 206 | ); 207 | } 208 | 209 | private messengerListener(event: MessageEvent) { 210 | const { data } = event; 211 | 212 | if (isHandshakeRequestMessage(data)) { 213 | this.emit(MessageType.HandshakeRequest, data); 214 | } 215 | } 216 | 217 | acceptHandshake(sessionId: IdType) { 218 | const message = createHandshakeResponseMessage(sessionId); 219 | this.messenger.postMessage(message); 220 | } 221 | 222 | close() { 223 | this.removeMessengerListener(); 224 | this.removeAllListeners(); 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /packages/core/src/emitter.ts: -------------------------------------------------------------------------------- 1 | import { EventsType } from './common'; 2 | 3 | /** 4 | * A simple event emitter interface used to implement the observer pattern throughout the codebase. 5 | * 6 | * @public 7 | * 8 | */ 9 | export interface Emitter { 10 | /** 11 | * Add a listener to a specific event. 12 | * 13 | * @param eventName - The name of the event 14 | * @param listener - A listener function that takes as parameter the payload of the event 15 | */ 16 | addEventListener( 17 | eventName: K, 18 | listener: (data: E[K]) => void 19 | ): void; 20 | 21 | /** 22 | * Remove a listener from a specific event. 23 | * 24 | * @param eventName - The name of the event 25 | * @param listener - A listener function that had been added previously 26 | */ 27 | removeEventListener( 28 | eventName: K, 29 | listener: (data: E[K]) => void 30 | ): void; 31 | 32 | /** 33 | * Add a listener to a specific event, that will only be invoked once 34 | * 35 | * @remarks 36 | * 37 | * After the first occurrence of the specified event, the listener will be invoked and 38 | * immediately removed. 39 | * 40 | * @param eventName - The name of the event 41 | * @param listener - A listener function that had been added previously 42 | */ 43 | once(eventName: K): Promise; 44 | } 45 | 46 | /** 47 | * A concrete implementation of the {@link Emitter} interface 48 | * 49 | * @public 50 | */ 51 | export class ConcreteEmitter implements Emitter { 52 | private _listeners: Partial void>>>; 53 | 54 | constructor() { 55 | this._listeners = {}; 56 | } 57 | 58 | /** {@inheritDoc Emitter.addEventListener} */ 59 | addEventListener( 60 | eventName: K, 61 | listener: (data: E[K]) => void 62 | ) { 63 | let listeners = this._listeners[eventName]; 64 | 65 | if (!listeners) { 66 | listeners = new Set(); 67 | this._listeners[eventName] = listeners; 68 | } 69 | 70 | listeners.add(listener); 71 | } 72 | 73 | /** {@inheritDoc Emitter.removeEventListener} */ 74 | removeEventListener( 75 | eventName: K, 76 | listener: (data: E[K]) => void 77 | ) { 78 | let listeners = this._listeners[eventName]; 79 | 80 | if (!listeners) { 81 | return; 82 | } 83 | 84 | listeners.delete(listener); 85 | } 86 | 87 | /** {@inheritDoc Emitter.once} */ 88 | once(eventName: K): Promise { 89 | return new Promise((resolve) => { 90 | const listener = (data: E[K]) => { 91 | this.removeEventListener(eventName, listener); 92 | resolve(data); 93 | }; 94 | 95 | this.addEventListener(eventName, listener); 96 | }); 97 | } 98 | 99 | /** @internal */ 100 | protected emit(eventName: K, data: E[K]) { 101 | let listeners = this._listeners[eventName]; 102 | 103 | if (!listeners) { 104 | return; 105 | } 106 | 107 | listeners.forEach((listener) => { 108 | listener(data); 109 | }); 110 | } 111 | 112 | /** @internal */ 113 | protected removeAllListeners() { 114 | Object.values(this._listeners).forEach((listeners) => { 115 | if (listeners) { 116 | listeners.clear(); 117 | } 118 | }); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /packages/core/src/handles.ts: -------------------------------------------------------------------------------- 1 | import { 2 | InnerType, 3 | MethodsType, 4 | EventsType, 5 | EmitOptions, 6 | CallOptions, 7 | } from './common'; 8 | import { Emitter, ConcreteEmitter } from './emitter'; 9 | import { Dispatcher } from './dispatcher'; 10 | import { 11 | MessageType, 12 | CallMessage, 13 | EventMessage, 14 | ResponseMessage, 15 | CallbackMessage, 16 | } from './messages'; 17 | import { createCallbackProxy, isCallbackProxy } from './proxy'; 18 | 19 | /** 20 | * A handle to the other end of the connection 21 | * 22 | * @remarks 23 | * 24 | * Use this handle to: 25 | * 26 | * - Call methods exposed by the other end 27 | * 28 | * - Add listeners to custom events emitted by the other end 29 | * 30 | * @typeParam M - The methods exposed by the other context 31 | * @typeParam E - The events exposed by the other context 32 | * 33 | * @public 34 | * 35 | */ 36 | export interface RemoteHandle< 37 | M extends MethodsType = any, 38 | E extends EventsType = any 39 | > extends Emitter { 40 | /** 41 | * Call a method exposed by the other end. 42 | * 43 | * @param methodName - The name of the method 44 | * @param args - The list of arguments passed to the method 45 | * @returns A Promise of the value returned by the method 46 | * 47 | */ 48 | call( 49 | methodName: K, 50 | ...args: Parameters 51 | ): Promise>>; 52 | 53 | /** 54 | * Call a method exposed by the other end. 55 | * 56 | * @param methodName - The name of the method 57 | * @param args - The list of arguments passed to the method 58 | * @param options - The {@link CallOptions} to customize this method call 59 | * @returns A Promise of the value returned by the method 60 | * 61 | */ 62 | customCall( 63 | methodName: K, 64 | args: Parameters, 65 | options?: CallOptions 66 | ): Promise>>; 67 | 68 | /** 69 | * Specify which parts of the arguments of a given method call should be transferred 70 | * into the other context instead of cloned. 71 | * 72 | * @remarks 73 | * 74 | * You only need to call setCallTransfer once per method. After the transfer function is set, 75 | * it will automatically be used by all subsequent calls to the specified method. 76 | * 77 | * @param methodName - The name of the method 78 | * @param transfer - A function that takes as parameters the arguments of a method call, and returns a list of transferable objects. 79 | * 80 | */ 81 | setCallTransfer( 82 | methodName: K, 83 | transfer: (...args: Parameters) => Transferable[] 84 | ): void; 85 | } 86 | 87 | /** 88 | * A handle to the local end of the connection 89 | * 90 | * @remarks 91 | * 92 | * Use this handle to: 93 | * 94 | * - Emit custom events to the other end 95 | * 96 | * - Set the methods that are exposed to the other end 97 | * 98 | * @typeParam M - The methods exposed by this context 99 | * @typeParam E - The events exposed by this context 100 | * 101 | * @public 102 | * 103 | */ 104 | export interface LocalHandle< 105 | M extends MethodsType = any, 106 | E extends EventsType = any 107 | > { 108 | /** 109 | * Emit a custom event with a payload. The event can be captured by the other context. 110 | * 111 | * @param eventName - The name of the event 112 | * @param data - The payload associated with the event 113 | * @param options - The {@link EmitOptions} to customize this emit call 114 | * 115 | */ 116 | emit( 117 | eventName: K, 118 | data: E[K], 119 | options?: EmitOptions 120 | ): void; 121 | 122 | /** 123 | * Set the methods that will be exposed to the other end of the connection. 124 | * 125 | * @param methods - An object mapping method names to functions 126 | * 127 | */ 128 | setMethods(methods: M): void; 129 | 130 | /** 131 | * Set a specific method that will be exposed to the other end of the connection. 132 | * 133 | * @param methodName - The name of the method 134 | * @param method - The function that will be exposed 135 | * 136 | */ 137 | setMethod(methodName: K, method: M[K]): void; 138 | 139 | /** 140 | * Specify which parts of the return value of a given method call should be transferred 141 | * into the other context instead of cloned. 142 | * 143 | * @remarks 144 | * 145 | * You only need to call setReturnTransfer once per method. After the transfer function is set, 146 | * it will automatically be used every time a value is returned by the specified method. 147 | * 148 | * @param methodName - The name of the method 149 | * @param transfer - A function that takes as parameter the return value of a method call, and returns a list of transferable objects. 150 | * 151 | */ 152 | setReturnTransfer( 153 | methodName: K, 154 | transfer: (result: InnerType>) => Transferable[] 155 | ): void; 156 | 157 | /** 158 | * Specify which parts of the payload of a given event should be transferred 159 | * into the other context instead of cloned. 160 | * 161 | * @remarks 162 | * 163 | * You only need to call setEmitTransfer once per event type. After the transfer function is set, 164 | * it will automatically be used every time a payload is attached to the specific event. 165 | * 166 | * @param eventName - The name of the method 167 | * @param transfer - A function that takes as parameter the payload of an event, and returns a list of transferable objects. 168 | * 169 | */ 170 | setEmitTransfer( 171 | eventName: K, 172 | transfer: (payload: E[K]) => Transferable[] 173 | ): void; 174 | } 175 | 176 | export class ConcreteRemoteHandle< 177 | M extends MethodsType = any, 178 | E extends EventsType = any 179 | > 180 | extends ConcreteEmitter 181 | implements RemoteHandle { 182 | private _dispatcher: Dispatcher; 183 | private _callTransfer: { [x: string]: (...args: any) => Transferable[] }; 184 | 185 | constructor(dispatcher: Dispatcher) { 186 | super(); 187 | 188 | this._dispatcher = dispatcher; 189 | this._callTransfer = {}; 190 | 191 | this._dispatcher.addEventListener( 192 | MessageType.Event, 193 | this._handleEvent.bind(this) 194 | ); 195 | } 196 | 197 | close() { 198 | this.removeAllListeners(); 199 | } 200 | 201 | setCallTransfer( 202 | methodName: K, 203 | transfer: (...args: Parameters) => Transferable[] 204 | ) { 205 | this._callTransfer[methodName as string] = transfer; 206 | } 207 | 208 | call( 209 | methodName: K, 210 | ...args: Parameters 211 | ): Promise>> { 212 | return this.customCall(methodName, args); 213 | } 214 | 215 | customCall( 216 | methodName: K, 217 | args: Parameters, 218 | options: CallOptions = {} 219 | ): Promise>> { 220 | return new Promise((resolve, reject) => { 221 | const sanitizedArgs: any[] = []; 222 | const callbacks: Function[] = []; 223 | let callbackId = 0; 224 | args.forEach((arg) => { 225 | if (typeof arg === 'function') { 226 | callbacks.push(arg); 227 | sanitizedArgs.push(createCallbackProxy(callbackId)); 228 | callbackId += 1; 229 | } else { 230 | sanitizedArgs.push(arg); 231 | } 232 | }); 233 | 234 | const hasCallbacks = callbacks.length > 0; 235 | 236 | let callbackListener: 237 | | undefined 238 | | ((data: CallbackMessage) => void) = undefined; 239 | 240 | if (hasCallbacks) { 241 | callbackListener = (data) => { 242 | const { callbackId, args } = data; 243 | callbacks[callbackId](...args); 244 | }; 245 | } 246 | 247 | let transfer: Transferable[] | undefined = options.transfer; 248 | if (transfer === undefined && this._callTransfer[methodName as string]) { 249 | transfer = this._callTransfer[methodName as string](...sanitizedArgs); 250 | } 251 | 252 | const { callbackEvent, responseEvent } = this._dispatcher.callOnRemote( 253 | methodName as string, 254 | sanitizedArgs, 255 | transfer 256 | ); 257 | 258 | if (hasCallbacks) { 259 | this._dispatcher.addEventListener( 260 | callbackEvent, 261 | callbackListener as any 262 | ); 263 | } 264 | 265 | this._dispatcher.once(responseEvent).then((response) => { 266 | if (callbackListener) { 267 | this._dispatcher.removeEventListener( 268 | callbackEvent, 269 | callbackListener as any 270 | ); 271 | } 272 | 273 | const { result, error } = response as ResponseMessage; 274 | 275 | if (error !== undefined) { 276 | reject(error); 277 | } else { 278 | resolve(result); 279 | } 280 | }); 281 | }); 282 | } 283 | 284 | private _handleEvent(data: EventMessage) { 285 | const { eventName, payload } = data; 286 | this.emit(eventName, payload); 287 | } 288 | } 289 | 290 | export class ConcreteLocalHandle< 291 | M extends MethodsType = any, 292 | E extends EventsType = any 293 | > implements LocalHandle { 294 | private _dispatcher: Dispatcher; 295 | private _methods: M; 296 | private _returnTransfer: { [x: string]: (result: any) => Transferable[] }; 297 | private _emitTransfer: { [x: string]: (payload: any) => Transferable[] }; 298 | 299 | constructor(dispatcher: Dispatcher, localMethods: M) { 300 | this._dispatcher = dispatcher; 301 | this._methods = localMethods; 302 | this._returnTransfer = {}; 303 | this._emitTransfer = {}; 304 | 305 | this._dispatcher.addEventListener( 306 | MessageType.Call, 307 | this._handleCall.bind(this) 308 | ); 309 | } 310 | 311 | emit( 312 | eventName: K, 313 | payload: E[K], 314 | options: EmitOptions = {} 315 | ) { 316 | let transfer: Transferable[] | undefined = options.transfer; 317 | if (transfer === undefined && this._emitTransfer[eventName as string]) { 318 | transfer = this._emitTransfer[eventName as string](payload); 319 | } 320 | 321 | this._dispatcher.emitToRemote(eventName as string, payload, transfer); 322 | } 323 | 324 | setMethods(methods: M) { 325 | this._methods = methods; 326 | } 327 | 328 | setMethod(methodName: K, method: M[K]) { 329 | this._methods[methodName] = method; 330 | } 331 | 332 | setReturnTransfer( 333 | methodName: K, 334 | transfer: (result: InnerType>) => Transferable[] 335 | ) { 336 | this._returnTransfer[methodName as string] = transfer; 337 | } 338 | 339 | setEmitTransfer( 340 | eventName: K, 341 | transfer: (payload: E[K]) => Transferable[] 342 | ) { 343 | this._emitTransfer[eventName as string] = transfer; 344 | } 345 | 346 | private _handleCall(data: CallMessage) { 347 | const { requestId, methodName, args } = data; 348 | 349 | const callMethod = new Promise((resolve, reject) => { 350 | const method = this._methods[methodName]; 351 | if (typeof method !== 'function') { 352 | reject( 353 | new Error(`The method "${methodName}" has not been implemented.`) 354 | ); 355 | return; 356 | } 357 | 358 | const desanitizedArgs = args.map((arg) => { 359 | if (isCallbackProxy(arg)) { 360 | const { callbackId } = arg; 361 | return (...args: any[]) => { 362 | this._dispatcher.callbackToRemote(requestId, callbackId, args); 363 | }; 364 | } else { 365 | return arg; 366 | } 367 | }); 368 | 369 | Promise.resolve(this._methods[methodName](...desanitizedArgs)) 370 | .then(resolve) 371 | .catch(reject); 372 | }); 373 | 374 | callMethod 375 | .then((result) => { 376 | let transfer: Transferable[] | undefined; 377 | if (this._returnTransfer[methodName]) { 378 | transfer = this._returnTransfer[methodName](result); 379 | } 380 | 381 | this._dispatcher.respondToRemote( 382 | requestId, 383 | result, 384 | undefined, 385 | transfer 386 | ); 387 | }) 388 | .catch((error) => { 389 | this._dispatcher.respondToRemote(requestId, undefined, error); 390 | }); 391 | } 392 | } 393 | -------------------------------------------------------------------------------- /packages/core/src/handshake.ts: -------------------------------------------------------------------------------- 1 | import { createUniqueIdFn } from './common'; 2 | import { MethodsType } from './common'; 3 | import { Messenger } from './messenger'; 4 | import { 5 | ParentHandshakeDispatcher, 6 | ChildHandshakeDispatcher, 7 | Dispatcher, 8 | } from './dispatcher'; 9 | import { Connection, ConcreteConnection } from './connection'; 10 | import { MessageType } from './messages'; 11 | 12 | const uniqueSessionId = createUniqueIdFn(); 13 | 14 | const runUntil = ( 15 | worker: () => void, 16 | condition: () => boolean, 17 | unfulfilled: () => void, 18 | maxAttempts: number, 19 | attemptInterval: number 20 | ): void => { 21 | let attempt = 0; 22 | const fn = () => { 23 | if (!condition() && (attempt < maxAttempts || maxAttempts < 1)) { 24 | worker(); 25 | attempt += 1; 26 | setTimeout(fn, attemptInterval); 27 | } else if (!condition() && attempt >= maxAttempts && maxAttempts >= 1) { 28 | unfulfilled(); 29 | } 30 | }; 31 | fn(); 32 | }; 33 | 34 | /** 35 | * Initiate the handshake from the Parent side 36 | * 37 | * @param messenger - The Messenger used to send and receive messages from the other end 38 | * @param localMethods - The methods that will be exposed to the other end 39 | * @param maxAttempts - The maximum number of handshake attempts 40 | * @param attemptsInterval - The interval between handshake attempts 41 | * @returns A Promise to an active {@link Connection} to the other end 42 | * 43 | * @public 44 | */ 45 | export function ParentHandshake( 46 | messenger: Messenger, 47 | localMethods: M0 = {} as M0, 48 | maxAttempts: number = 5, 49 | attemptsInterval: number = 100 50 | ): Promise { 51 | const thisSessionId = uniqueSessionId(); 52 | let connected = false; 53 | return new Promise((resolve, reject) => { 54 | const handshakeDispatcher = new ParentHandshakeDispatcher( 55 | messenger, 56 | thisSessionId 57 | ); 58 | 59 | handshakeDispatcher.once(thisSessionId).then((response) => { 60 | connected = true; 61 | handshakeDispatcher.close(); 62 | const { sessionId } = response; 63 | const dispatcher = new Dispatcher(messenger, sessionId); 64 | const connection = new ConcreteConnection(dispatcher, localMethods); 65 | resolve(connection); 66 | }); 67 | 68 | runUntil( 69 | () => handshakeDispatcher.initiateHandshake(), 70 | () => connected, 71 | () => 72 | reject( 73 | new Error(`Handshake failed, reached maximum number of attempts`) 74 | ), 75 | maxAttempts, 76 | attemptsInterval 77 | ); 78 | }); 79 | } 80 | 81 | /** 82 | * Initiate the handshake from the Child side 83 | * 84 | * @param messenger - The Messenger used to send and receive messages from the other end 85 | * @param localMethods - The methods that will be exposed to the other end 86 | * @returns A Promise to an active {@link Connection} to the other end 87 | * 88 | * @public 89 | */ 90 | export function ChildHandshake( 91 | messenger: Messenger, 92 | localMethods: M = {} as M 93 | ): Promise { 94 | return new Promise((resolve, reject) => { 95 | const handshakeDispatcher = new ChildHandshakeDispatcher(messenger); 96 | 97 | handshakeDispatcher.once(MessageType.HandshakeRequest).then((response) => { 98 | const { sessionId } = response; 99 | handshakeDispatcher.acceptHandshake(sessionId); 100 | handshakeDispatcher.close(); 101 | const dispatcher = new Dispatcher(messenger, sessionId); 102 | const connection = new ConcreteConnection(dispatcher, localMethods); 103 | resolve(connection); 104 | }); 105 | }); 106 | } 107 | -------------------------------------------------------------------------------- /packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @packageDocumentation Use web Workers and other Windows through a simple Promise API 3 | */ 4 | 5 | import { ParentHandshake, ChildHandshake } from './handshake'; 6 | import { Connection } from './connection'; 7 | import { Emitter, ConcreteEmitter } from './emitter'; 8 | import { 9 | Messenger, 10 | WindowMessenger, 11 | WorkerMessenger, 12 | PortMessenger, 13 | BareMessenger, 14 | DebugMessenger, 15 | debug, 16 | } from './messenger'; 17 | import { RemoteHandle, LocalHandle } from './handles'; 18 | import { 19 | CallOptions, 20 | EmitOptions, 21 | MethodsType, 22 | EventsType, 23 | Callable, 24 | ValueOrPromise, 25 | InnerType, 26 | } from './common'; 27 | 28 | export { 29 | // Interfaces 30 | Connection, 31 | Emitter, 32 | RemoteHandle, 33 | LocalHandle, 34 | Messenger, 35 | // Methods 36 | ParentHandshake, 37 | ChildHandshake, 38 | DebugMessenger, 39 | debug, 40 | // Classes 41 | ConcreteEmitter, 42 | WindowMessenger, 43 | WorkerMessenger, 44 | PortMessenger, 45 | BareMessenger, 46 | // Types 47 | CallOptions, 48 | EmitOptions, 49 | // Type Helpers 50 | MethodsType, 51 | EventsType, 52 | Callable, 53 | ValueOrPromise, 54 | InnerType, 55 | }; 56 | -------------------------------------------------------------------------------- /packages/core/src/messages.ts: -------------------------------------------------------------------------------- 1 | import { IdType, KeyType, MARKER } from './common'; 2 | 3 | export enum MessageType { 4 | HandshakeRequest = 'handshake-request', 5 | HandshakeResponse = 'handshake-response', 6 | Call = 'call', 7 | Response = 'response', 8 | Error = 'error', 9 | Event = 'event', 10 | Callback = 'callback', 11 | } 12 | 13 | export interface Message { 14 | type: typeof MARKER; 15 | action: T; 16 | sessionId: IdType; 17 | } 18 | 19 | export interface HandshakeRequestMessage 20 | extends Message {} 21 | 22 | export interface HandshakeResponseMessage 23 | extends Message {} 24 | 25 | export interface CallMessage> 26 | extends Message { 27 | requestId: IdType; 28 | methodName: KeyType; 29 | args: A; 30 | } 31 | 32 | export interface ResponseMessage extends Message { 33 | requestId: IdType; 34 | result?: R; 35 | error?: string; 36 | } 37 | 38 | export interface CallbackMessage> 39 | extends Message { 40 | requestId: IdType; 41 | callbackId: IdType; 42 | args: A; 43 | } 44 | 45 | export interface EventMessage

extends Message { 46 | eventName: KeyType; 47 | payload: P; 48 | } 49 | 50 | // Message Creators 51 | 52 | export function createHandshakeRequestMessage( 53 | sessionId: IdType 54 | ): HandshakeRequestMessage { 55 | return { 56 | type: MARKER, 57 | action: MessageType.HandshakeRequest, 58 | sessionId, 59 | }; 60 | } 61 | 62 | export function createHandshakeResponseMessage( 63 | sessionId: IdType 64 | ): HandshakeResponseMessage { 65 | return { 66 | type: MARKER, 67 | action: MessageType.HandshakeResponse, 68 | sessionId, 69 | }; 70 | } 71 | 72 | export function createCallMessage>( 73 | sessionId: IdType, 74 | requestId: IdType, 75 | methodName: KeyType, 76 | args: A 77 | ): CallMessage { 78 | return { 79 | type: MARKER, 80 | action: MessageType.Call, 81 | sessionId, 82 | requestId, 83 | methodName, 84 | args, 85 | }; 86 | } 87 | 88 | export function createResponsMessage( 89 | sessionId: IdType, 90 | requestId: IdType, 91 | result: R, 92 | error?: string 93 | ): ResponseMessage { 94 | const message: ResponseMessage = { 95 | type: MARKER, 96 | action: MessageType.Response, 97 | sessionId, 98 | requestId, 99 | }; 100 | 101 | if (result !== undefined) { 102 | message.result = result; 103 | } 104 | 105 | if (error !== undefined) { 106 | message.error = error; 107 | } 108 | 109 | return message; 110 | } 111 | 112 | export function createCallbackMessage>( 113 | sessionId: IdType, 114 | requestId: IdType, 115 | callbackId: IdType, 116 | args: A 117 | ): CallbackMessage { 118 | return { 119 | type: MARKER, 120 | action: MessageType.Callback, 121 | sessionId, 122 | requestId, 123 | callbackId, 124 | args, 125 | }; 126 | } 127 | 128 | export function createEventMessage

( 129 | sessionId: IdType, 130 | eventName: KeyType, 131 | payload: P 132 | ): EventMessage

{ 133 | return { 134 | type: MARKER, 135 | action: MessageType.Event, 136 | sessionId, 137 | eventName, 138 | payload, 139 | }; 140 | } 141 | 142 | // Type Guards 143 | 144 | export function isMessage(m: any): m is Message { 145 | return m && m.type === MARKER; 146 | } 147 | 148 | export function isHandshakeRequestMessage( 149 | m: Message 150 | ): m is HandshakeRequestMessage { 151 | return isMessage(m) && m.action === MessageType.HandshakeRequest; 152 | } 153 | 154 | export function isHandshakeResponseMessage( 155 | m: Message 156 | ): m is HandshakeResponseMessage { 157 | return isMessage(m) && m.action === MessageType.HandshakeResponse; 158 | } 159 | 160 | export function isCallMessage(m: Message): m is CallMessage { 161 | return isMessage(m) && m.action === MessageType.Call; 162 | } 163 | 164 | export function isResponseMessage(m: Message): m is ResponseMessage { 165 | return isMessage(m) && m.action === MessageType.Response; 166 | } 167 | 168 | export function isCallbackMessage(m: Message): m is CallbackMessage { 169 | return isMessage(m) && m.action === MessageType.Callback; 170 | } 171 | 172 | export function isEventMessage(m: Message): m is EventMessage { 173 | return isMessage(m) && m.action === MessageType.Event; 174 | } 175 | -------------------------------------------------------------------------------- /packages/core/src/messenger.ts: -------------------------------------------------------------------------------- 1 | type MessageListener = (event: MessageEvent) => void; 2 | type ListenerRemover = () => void; 3 | 4 | /** 5 | * An interface used internally to exchange low level messages across contexts. 6 | * 7 | * @remarks 8 | * 9 | * Having a single interface lets post-me deal with Workers, Windows, and MessagePorts 10 | * without having to worry about their differences. 11 | * 12 | * A few concrete implementations of the Messenger interface are provided. 13 | * 14 | * @public 15 | * 16 | */ 17 | export interface Messenger { 18 | /** 19 | * Send a message to the other context 20 | * 21 | * @param message - The payload of the message 22 | * @param transfer - A list of Transferable objects that should be transferred to the other context instead of cloned. 23 | */ 24 | postMessage(message: any, transfer?: Transferable[]): void; 25 | 26 | /** 27 | * Add a listener to messages received by the other context 28 | * 29 | * @param listener - A listener that will receive the MessageEvent 30 | * @returns A function that can be invoked to remove the listener 31 | */ 32 | addMessageListener(listener: MessageListener): ListenerRemover; 33 | } 34 | 35 | const acceptableMessageEvent = ( 36 | event: MessageEvent, 37 | remoteWindow: Window, 38 | acceptedOrigin: string 39 | ) => { 40 | const { source, origin } = event; 41 | 42 | if (source !== remoteWindow) { 43 | return false; 44 | } 45 | 46 | if (origin !== acceptedOrigin && acceptedOrigin !== '*') { 47 | return false; 48 | } 49 | 50 | return true; 51 | }; 52 | 53 | /** 54 | * A concrete implementation of {@link Messenger} used to communicate with another Window. 55 | * 56 | * @public 57 | * 58 | */ 59 | export class WindowMessenger implements Messenger { 60 | postMessage: (message: any, transfer?: Transferable[]) => void; 61 | addMessageListener: (listener: MessageListener) => ListenerRemover; 62 | 63 | constructor({ 64 | localWindow, 65 | remoteWindow, 66 | remoteOrigin, 67 | }: { 68 | localWindow?: Window; 69 | remoteWindow: Window; 70 | remoteOrigin: string; 71 | }) { 72 | localWindow = localWindow || window; 73 | 74 | this.postMessage = (message, transfer) => { 75 | remoteWindow.postMessage(message, remoteOrigin, transfer); 76 | }; 77 | 78 | this.addMessageListener = (listener) => { 79 | const outerListener = (event: MessageEvent) => { 80 | if (acceptableMessageEvent(event, remoteWindow, remoteOrigin)) { 81 | listener(event); 82 | } 83 | }; 84 | 85 | localWindow!.addEventListener('message', outerListener); 86 | 87 | const removeListener = () => { 88 | localWindow!.removeEventListener('message', outerListener); 89 | }; 90 | 91 | return removeListener; 92 | }; 93 | } 94 | } 95 | 96 | interface Postable { 97 | postMessage(message: any, transfer?: Transferable[]): void; 98 | addEventListener(eventName: 'message', listener: MessageListener): void; 99 | removeEventListener(eventName: 'message', listener: MessageListener): void; 100 | } 101 | 102 | /** @public */ 103 | export class BareMessenger implements Messenger { 104 | postMessage: (message: any, transfer?: Transferable[]) => void; 105 | addMessageListener: (listener: MessageListener) => ListenerRemover; 106 | 107 | constructor(postable: Postable) { 108 | this.postMessage = (message, transfer = []) => { 109 | postable.postMessage(message, transfer); 110 | }; 111 | 112 | this.addMessageListener = (listener) => { 113 | const outerListener = (event: MessageEvent) => { 114 | listener(event); 115 | }; 116 | 117 | postable.addEventListener('message', outerListener); 118 | 119 | const removeListener = () => { 120 | postable.removeEventListener('message', outerListener); 121 | }; 122 | 123 | return removeListener; 124 | }; 125 | } 126 | } 127 | 128 | /** 129 | * A concrete implementation of {@link Messenger} used to communicate with a Worker. 130 | * 131 | * Takes a {@link Postable} representing the `Worker` (when calling from 132 | * the parent context) or the `self` `DedicatedWorkerGlobalScope` object 133 | * (when calling from the child context). 134 | * 135 | * @public 136 | * 137 | */ 138 | export class WorkerMessenger extends BareMessenger implements Messenger { 139 | constructor({ worker }: { worker: Postable }) { 140 | super(worker); 141 | } 142 | } 143 | 144 | /** 145 | * A concrete implementation of {@link Messenger} used to communicate with a MessagePort. 146 | * 147 | * @public 148 | * 149 | */ 150 | export class PortMessenger extends BareMessenger implements Messenger { 151 | constructor({ port }: { port: MessagePort }) { 152 | port.start(); 153 | super(port); 154 | } 155 | } 156 | 157 | /** 158 | * Create a logger function with a specific namespace 159 | * 160 | * @param namespace - The namespace will be prepended to all the arguments passed to the logger function 161 | * @param log - The underlying logger (`console.log` by default) 162 | * 163 | * @public 164 | * 165 | */ 166 | export function debug(namespace: string, log?: (...data: any[]) => void) { 167 | log = log || console.debug || console.log || (() => {}); 168 | return (...data: any[]) => { 169 | log!(namespace, ...data); 170 | }; 171 | } 172 | 173 | /** 174 | * Decorate a {@link Messenger} so that it will log any message exchanged 175 | * @param messenger - The Messenger that will be decorated 176 | * @param log - The logger function that will receive each message 177 | * @returns A decorated Messenger 178 | * 179 | * @public 180 | * 181 | */ 182 | export function DebugMessenger( 183 | messenger: Messenger, 184 | log?: (...data: any[]) => void 185 | ): Messenger { 186 | log = log || debug('post-me'); 187 | 188 | const debugListener: MessageListener = function (event) { 189 | const { data } = event; 190 | log!('⬅️ received message', data); 191 | }; 192 | 193 | messenger.addMessageListener(debugListener); 194 | 195 | return { 196 | postMessage: function (message, transfer) { 197 | log!('➡️ sending message', message); 198 | messenger.postMessage(message, transfer); 199 | }, 200 | addMessageListener: function (listener) { 201 | return messenger.addMessageListener(listener); 202 | }, 203 | }; 204 | } 205 | -------------------------------------------------------------------------------- /packages/core/src/proxy.ts: -------------------------------------------------------------------------------- 1 | import { MARKER, IdType } from './common'; 2 | 3 | export enum ProxyType { 4 | Callback = 'callback', 5 | } 6 | 7 | export interface BaseProxy

{ 8 | type: typeof MARKER; 9 | proxy: P; 10 | } 11 | 12 | export interface CallbackProxy extends BaseProxy { 13 | callbackId: IdType; 14 | } 15 | 16 | export function createCallbackProxy(callbackId: IdType): CallbackProxy { 17 | return { 18 | type: MARKER, 19 | proxy: ProxyType.Callback, 20 | callbackId, 21 | }; 22 | } 23 | 24 | export function isCallbackProxy(p: any): p is CallbackProxy { 25 | return p && p.type === MARKER && p.proxy === ProxyType.Callback; 26 | } 27 | -------------------------------------------------------------------------------- /packages/core/tests/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { ParentHandshake, ChildHandshake } from '../src/handshake'; 2 | import { Connection } from '../src/connection'; 3 | import { ConcreteEmitter } from '../src/emitter'; 4 | import { 5 | Messenger, 6 | WindowMessenger, 7 | DebugMessenger, 8 | debug, 9 | WorkerMessenger, 10 | } from '../src/messenger'; 11 | 12 | import { JSDOM } from 'jsdom'; 13 | 14 | import MessageEventJSDOM from 'jsdom/lib/jsdom/living/generated/MessageEvent'; 15 | import { fireAnEvent } from 'jsdom/lib/jsdom/living/helpers/events'; 16 | import { LocalHandle, RemoteHandle } from '../src/handles'; 17 | 18 | type ChildMethods = { 19 | foo: (x: number) => number; 20 | bar: () => Promise<{ status: number; message: string }>; 21 | throws: () => Promise; 22 | ping: (s: string) => string; 23 | slowSum: ( 24 | x: number, 25 | y: number, 26 | onStart: (start: number) => void, 27 | onProgress: (progress: number) => void 28 | ) => Promise; 29 | }; 30 | 31 | type ChildEvents = { 32 | clicked: number; 33 | tapped: string; 34 | }; 35 | 36 | type ParentMethods = { 37 | baz: (x: number, s: string) => number; 38 | wut: () => Promise; 39 | throws: () => Promise; 40 | ping: (s: string) => string; 41 | }; 42 | 43 | type ParentEvents = { 44 | opened: { type: string; message: string }; 45 | closed: string; 46 | }; 47 | 48 | const parentMethods: ParentMethods = { 49 | baz: (x, s) => x * s.length, 50 | wut: () => Promise.resolve('Oh hi!'), 51 | throws: () => Promise.reject(new Error('Oh no! - parent')), 52 | ping: (s) => s, 53 | }; 54 | 55 | const childMethods: ChildMethods = { 56 | foo: (x) => x * 2, 57 | bar: () => Promise.resolve({ status: 200, message: 'ok' }), 58 | throws: () => Promise.reject(new Error('Oh no! - child')), 59 | ping: (s) => s, 60 | slowSum: (x, y, onStart, onProgress) => { 61 | onStart(-1); 62 | return new Promise((resolve) => { 63 | const nIterations = 5; 64 | 65 | const step = (iter: number) => { 66 | if (iter >= nIterations) { 67 | resolve(x + y); 68 | return; 69 | } 70 | 71 | onProgress!(iter); 72 | 73 | setTimeout(() => step(iter + 1), 10); 74 | }; 75 | 76 | step(0); 77 | }); 78 | }, 79 | }; 80 | 81 | function bindPostMessageToSource(target: Window, source: Window) { 82 | // A simplified copy of JSDOM postMessage, that adds a fixed origin and source to the event. 83 | return function (message: any, targetOrigin: string) { 84 | if (arguments.length < 2) { 85 | throw new TypeError( 86 | "'postMessage' requires 2 arguments: 'message' and 'targetOrigin'" 87 | ); 88 | } 89 | 90 | if (targetOrigin !== '*' && targetOrigin !== target.origin) { 91 | return; 92 | } 93 | 94 | setTimeout(() => { 95 | fireAnEvent('message', target, MessageEventJSDOM, { 96 | source, 97 | origin: source.origin, 98 | data: message, 99 | }); 100 | }, 0); 101 | }; 102 | } 103 | 104 | function makeWindows(parentOrigin: string, childOrigin: string) { 105 | const parentDOM = new JSDOM(``, { url: parentOrigin }); 106 | const childDOM = new JSDOM(``, { url: childOrigin }); 107 | 108 | const parentWindow: Window = parentDOM.window as any; 109 | const childWindow: Window = childDOM.window as any; 110 | 111 | parentWindow.postMessage = bindPostMessageToSource(parentWindow, childWindow); 112 | childWindow.postMessage = bindPostMessageToSource(childWindow, parentWindow); 113 | 114 | return [parentWindow, childWindow] as const; 115 | } 116 | 117 | function makeHandshake( 118 | _windows?: [Window, Window] 119 | ): Promise< 120 | [ 121 | Connection, 122 | Connection 123 | ] 124 | > { 125 | let windows = 126 | _windows || 127 | makeWindows('https://parent.example.com', 'https://child.example.com'); 128 | const [parentWindow, childWindow] = windows; 129 | 130 | let parentMessenger = new WindowMessenger({ 131 | localWindow: parentWindow, 132 | remoteWindow: childWindow, 133 | remoteOrigin: childWindow.origin, 134 | }); 135 | let childMessenger = new WindowMessenger({ 136 | localWindow: childWindow, 137 | remoteWindow: parentWindow, 138 | remoteOrigin: parentWindow.origin, 139 | }); 140 | 141 | // parentMessenger = DebugMessenger(parentMessenger, debug('post-me:parent')); 142 | // childMessenger = DebugMessenger(childMessenger, debug('post-me:child')); 143 | 144 | const handshakes = [ 145 | ParentHandshake(parentMessenger, parentMethods), 146 | ChildHandshake(childMessenger, childMethods), 147 | ] as const; 148 | 149 | return Promise.all(handshakes); 150 | } 151 | 152 | function sleep(duration: number): Promise { 153 | return new Promise((resolve) => setTimeout(resolve, duration)); 154 | } 155 | 156 | test('handshake', () => { 157 | return new Promise((resolve) => { 158 | makeHandshake().then(([_parentConnection, _childConnection]) => { 159 | resolve(); 160 | }); 161 | }); 162 | }); 163 | 164 | test('call', () => { 165 | return new Promise((resolve, reject) => { 166 | makeHandshake().then(([parentConnection, childConnection]) => { 167 | const tasks: Promise[] = []; 168 | 169 | // Code in the parent app 170 | { 171 | const remoteHandle = parentConnection.remoteHandle(); 172 | 173 | const args0 = [2] as const; 174 | 175 | const task0 = remoteHandle.call('foo', ...args0).then(async (value) => { 176 | expect(value).toEqual(childMethods.foo(...args0)); 177 | return value; 178 | }); 179 | tasks.push(task0); 180 | 181 | const args1 = [] as const; 182 | const task1 = remoteHandle.call('bar', ...args1).then(async (value) => { 183 | expect(value).toEqual(await childMethods.bar(...args1)); 184 | return value; 185 | }); 186 | tasks.push(task1); 187 | } 188 | 189 | // Code in the child app 190 | { 191 | const remoteHandle = childConnection.remoteHandle(); 192 | 193 | const args0 = [2, 'four'] as const; 194 | 195 | const task0 = remoteHandle.call('baz', ...args0).then(async (value) => { 196 | expect(value).toEqual(parentMethods.baz(...args0)); 197 | return value; 198 | }); 199 | tasks.push(task0); 200 | 201 | const args1 = [] as const; 202 | const task1 = remoteHandle.call('wut', ...args1).then(async (value) => { 203 | expect(value).toEqual(await parentMethods.wut(...args1)); 204 | return value; 205 | }); 206 | tasks.push(task1); 207 | } 208 | 209 | Promise.all(tasks).then(() => resolve()); 210 | }); 211 | }); 212 | }); 213 | 214 | type ChildClassInterface = { 215 | foo(x: number): number; 216 | }; 217 | 218 | class ChildClass implements ChildClassInterface { 219 | private magicNumber = 7; 220 | 221 | foo(_x: number): number { 222 | return this.magicNumber; 223 | } 224 | } 225 | 226 | test('call:bound methods', () => { 227 | return new Promise((resolve, reject) => { 228 | makeHandshake().then(([parentConnection, childConnection]) => { 229 | const tasks: Promise[] = []; 230 | 231 | // Code in the parent app 232 | { 233 | const remoteHandle = parentConnection.remoteHandle(); 234 | 235 | const task = remoteHandle.call('foo', 0).then(async (value) => { 236 | expect(value).toEqual(7); 237 | return value; 238 | }); 239 | tasks.push(task); 240 | } 241 | 242 | // Code in the child app 243 | { 244 | const localHandle: LocalHandle = childConnection.localHandle(); 245 | localHandle.setMethods(new ChildClass()); 246 | } 247 | 248 | Promise.all(tasks).then(() => resolve()); 249 | }); 250 | }); 251 | }); 252 | 253 | test('emit', () => { 254 | return new Promise((resolve, reject) => { 255 | makeHandshake().then(([parentConnection, childConnection]) => { 256 | const tasks: Promise[] = []; 257 | 258 | const maxRunTime = 500; 259 | const nEmits = 4; 260 | 261 | const messageFromParent: ParentEvents['opened'] = { 262 | type: 'foo', 263 | message: 'bar', 264 | }; 265 | const messageFromChild: ChildEvents['tapped'] = 266 | 'Emitting an important message!'; 267 | 268 | // Code in the parent app 269 | { 270 | const remoteHandle = parentConnection.remoteHandle(); 271 | 272 | const task0 = new Promise((resolve) => { 273 | let count = 0; 274 | remoteHandle.addEventListener('tapped', (data) => { 275 | expect(data).toEqual(messageFromChild); 276 | count += 1; 277 | }); 278 | 279 | setTimeout(() => { 280 | expect(count).toEqual(nEmits); 281 | resolve(count); 282 | }, maxRunTime); 283 | }); 284 | tasks.push(task0); 285 | 286 | const task1 = new Promise((resolve) => { 287 | let count = 0; 288 | // Remove listener 289 | const listener = (data: typeof messageFromChild) => { 290 | remoteHandle.removeEventListener('tapped', listener); 291 | expect(data).toEqual(messageFromChild); 292 | count += 1; 293 | }; 294 | remoteHandle.addEventListener('tapped', listener); 295 | 296 | setTimeout(() => { 297 | expect(count).toEqual(1); 298 | resolve(count); 299 | }, maxRunTime); 300 | }); 301 | tasks.push(task1); 302 | 303 | const task2 = new Promise((resolve) => { 304 | let count = 0; 305 | remoteHandle.once('tapped').then((data) => { 306 | expect(data).toEqual(messageFromChild); 307 | count += 1; 308 | }); 309 | 310 | setTimeout(() => { 311 | expect(count).toEqual(1); 312 | resolve(count); 313 | }, maxRunTime); 314 | }); 315 | tasks.push(task2); 316 | 317 | const localHanlde = parentConnection.localHandle(); 318 | setTimeout(() => { 319 | for (let i = 0; i < nEmits; ++i) { 320 | localHanlde.emit('opened', messageFromParent); 321 | } 322 | }, 50); 323 | } 324 | 325 | // Code in the child app 326 | { 327 | const remoteHandle = childConnection.remoteHandle(); 328 | 329 | const task0 = new Promise((resolve) => { 330 | let count = 0; 331 | remoteHandle.addEventListener('opened', (data) => { 332 | expect(data).toEqual(messageFromParent); 333 | count += 1; 334 | }); 335 | 336 | setTimeout(() => { 337 | expect(count).toEqual(nEmits); 338 | resolve(count); 339 | }, maxRunTime); 340 | }); 341 | tasks.push(task0); 342 | 343 | const task1 = new Promise((resolve) => { 344 | let count = 0; 345 | // Remove listener 346 | const listener = (data: typeof messageFromParent) => { 347 | remoteHandle.removeEventListener('opened', listener); 348 | expect(data).toEqual(messageFromParent); 349 | count += 1; 350 | }; 351 | remoteHandle.addEventListener('opened', listener); 352 | 353 | setTimeout(() => { 354 | expect(count).toEqual(1); 355 | resolve(count); 356 | }, maxRunTime); 357 | }); 358 | tasks.push(task1); 359 | 360 | const task2 = new Promise((resolve) => { 361 | let count = 0; 362 | remoteHandle.once('opened').then((data) => { 363 | expect(data).toEqual(messageFromParent); 364 | count += 1; 365 | }); 366 | 367 | setTimeout(() => { 368 | expect(count).toEqual(1); 369 | resolve(count); 370 | }, maxRunTime); 371 | }); 372 | tasks.push(task2); 373 | 374 | const localHanlde = childConnection.localHandle(); 375 | setTimeout(() => { 376 | for (let i = 0; i < nEmits; ++i) { 377 | localHanlde.emit('tapped', messageFromChild); 378 | } 379 | }, 50); 380 | } 381 | 382 | Promise.all(tasks).then(() => resolve()); 383 | }); 384 | }); 385 | }); 386 | 387 | test('error', () => { 388 | return new Promise((resolve, reject) => { 389 | makeHandshake().then(([parentConnection, childConnection]) => { 390 | const tasks: Promise[] = []; 391 | 392 | // Code in the parent app 393 | { 394 | const remoteHandle = parentConnection.remoteHandle(); 395 | 396 | const args0 = [] as const; 397 | 398 | // The method actually throws an error 399 | const task0 = remoteHandle 400 | .call('throws', ...args0) 401 | .then((_value) => { 402 | throw new Error("It shouldn't have resolved"); 403 | }) 404 | .catch((e: Error) => { 405 | expect(e.message).toEqual('Oh no! - child'); 406 | return e; 407 | }); 408 | tasks.push(task0); 409 | 410 | // The method doesn't exist (typescript would actually catch the mistake during dev) 411 | const task1 = remoteHandle 412 | .call('whatever' as any, ...[]) 413 | .then((_value) => { 414 | throw new Error("It shouldn't have resolved"); 415 | }) 416 | .catch((e: Error) => { 417 | expect(e.message).toEqual( 418 | `The method "whatever" has not been implemented.` 419 | ); 420 | return e; 421 | }); 422 | tasks.push(task1); 423 | } 424 | 425 | // Code in the child app 426 | { 427 | const remoteHandle = childConnection.remoteHandle(); 428 | 429 | const args0 = [] as const; 430 | 431 | // The method actually throws an error 432 | const task0 = remoteHandle 433 | .call('throws', ...args0) 434 | .then((_value) => { 435 | throw new Error("It shouldn't have resolved"); 436 | }) 437 | .catch((e: Error) => { 438 | expect(e.message).toEqual('Oh no! - parent'); 439 | return e; 440 | }); 441 | tasks.push(task0); 442 | 443 | // The method doesn't exist (typescript would actually catch the mistake during dev) 444 | const task1 = remoteHandle 445 | .call('anything' as any, ...[]) 446 | .then((_value) => { 447 | throw new Error("It shouldn't have resolved"); 448 | }) 449 | .catch((e: Error) => { 450 | expect(e.message).toEqual( 451 | `The method "anything" has not been implemented.` 452 | ); 453 | return e; 454 | }); 455 | tasks.push(task1); 456 | } 457 | 458 | Promise.all(tasks).then(() => resolve()); 459 | }); 460 | }); 461 | }); 462 | 463 | test('close', () => { 464 | return new Promise((resolve, reject) => { 465 | makeHandshake().then(([parentConnection, childConnection]) => { 466 | const tasks: Promise[] = []; 467 | 468 | const maxRunTime = 500; 469 | const nEmits = 4; 470 | 471 | const messageFromChild: ChildEvents['tapped'] = 472 | 'Emitting an important message!'; 473 | 474 | // Code in the parent app 475 | { 476 | const remoteHandle = parentConnection.remoteHandle(); 477 | 478 | const task0 = new Promise((resolve, reject) => { 479 | remoteHandle.addEventListener('tapped', (data) => { 480 | expect(data).toEqual(messageFromChild); 481 | reject(new Error(`Shouldn't receive after connection close`)); 482 | }); 483 | 484 | setTimeout(() => { 485 | parentConnection.close(); 486 | }, 0); 487 | 488 | setTimeout(() => { 489 | resolve(); 490 | }, maxRunTime); 491 | }); 492 | tasks.push(task0); 493 | } 494 | 495 | // Code in the child app 496 | { 497 | const localHanlde = childConnection.localHandle(); 498 | setTimeout(() => { 499 | for (let i = 0; i < nEmits; ++i) { 500 | localHanlde.emit('tapped', messageFromChild); 501 | } 502 | }, 50); 503 | } 504 | 505 | Promise.all(tasks).then(() => resolve()); 506 | }); 507 | }); 508 | }); 509 | 510 | test('handshake fail due to wrong origin', () => { 511 | return new Promise((resolve, reject) => { 512 | const parentOrigin = 'https://parent.example.com'; 513 | const childOrigin = 'https://child.example.com'; 514 | const wrongParentOrigin = 'https://wrong.example.com'; 515 | const [parentWindow, childWindow] = makeWindows(parentOrigin, childOrigin); 516 | 517 | const maxRunTime = 500; 518 | 519 | const parentMessenger = new WindowMessenger({ 520 | localWindow: parentWindow, 521 | remoteWindow: childWindow, 522 | remoteOrigin: childOrigin, 523 | }); 524 | const childMessenger = new WindowMessenger({ 525 | localWindow: childWindow, 526 | remoteWindow: parentWindow, 527 | remoteOrigin: wrongParentOrigin, 528 | }); 529 | 530 | const parentHandshake = ParentHandshake(parentMessenger); 531 | const childHandshake = ChildHandshake(childMessenger); 532 | 533 | parentHandshake 534 | .then((_connection) => { 535 | reject(new Error('The handshake should be failing. - parent')); 536 | }) 537 | .catch(resolve); 538 | 539 | childHandshake.then((_connection) => { 540 | reject(new Error('The handshake should be failing. - child')); 541 | }); 542 | }); 543 | }); 544 | 545 | xtest('multi-connection', () => { 546 | return new Promise((resolve, reject) => { 547 | // One parent connected to two children 548 | 549 | const parentOrigin = 'https://parent.example.com'; 550 | const child0Origin = 'https://child0.example.com'; 551 | const child1Origin = 'https://child0.example.com'; 552 | // Because of JSDOM limitations, the two child windows need to have the same origin 553 | const [parentWindow, child0Window] = makeWindows( 554 | parentOrigin, 555 | child0Origin 556 | ); 557 | 558 | const child1Window: Window = new JSDOM(``, { url: child1Origin }) 559 | .window as any; 560 | child1Window.postMessage = bindPostMessageToSource( 561 | child1Window, 562 | parentWindow 563 | ); 564 | 565 | const maxRunTime = 500; 566 | const nEmits = 4; 567 | 568 | makeHandshake([parentWindow, child0Window]).then( 569 | ([parent0Connection, child0Connection]) => { 570 | parentWindow.postMessage = bindPostMessageToSource( 571 | parentWindow, 572 | child1Window 573 | ); 574 | makeHandshake([parentWindow, child1Window]).then( 575 | ([parent1Connection, child1Connection]) => { 576 | const tasks: Promise[] = []; 577 | { 578 | // Parent Code 579 | { 580 | // Child 0 connection 581 | const localHandle = parent0Connection.localHandle(); 582 | const remoteHandle = parent0Connection.remoteHandle(); 583 | 584 | const task0 = remoteHandle 585 | .call('ping', 'child0') 586 | .then((res) => { 587 | expect(res).toEqual('child0'); 588 | return res; 589 | }); 590 | tasks.push(task0); 591 | 592 | const task1 = new Promise((resolve) => { 593 | let count = 0; 594 | remoteHandle.addEventListener('clicked', (data) => { 595 | expect(data).toEqual(0); 596 | count += 1; 597 | }); 598 | 599 | setTimeout(() => { 600 | expect(count).toEqual(nEmits); 601 | resolve(count); 602 | }, maxRunTime); 603 | }); 604 | tasks.push(task1); 605 | 606 | setTimeout(() => { 607 | for (let i = 0; i < nEmits; ++i) { 608 | localHandle.emit('opened', { 609 | type: 'foo', 610 | message: 'child0', 611 | }); 612 | } 613 | }, 50); 614 | } 615 | { 616 | // Child 1 connection 617 | const localHandle = parent1Connection.localHandle(); 618 | const remoteHandle = parent1Connection.remoteHandle(); 619 | 620 | const task0 = remoteHandle 621 | .call('ping', 'child0') 622 | .then((res) => { 623 | expect(res).toEqual('child0'); 624 | return res; 625 | }); 626 | tasks.push(task0); 627 | 628 | const task1 = new Promise((resolve) => { 629 | let count = 0; 630 | remoteHandle.addEventListener('clicked', (data) => { 631 | expect(data).toEqual(1); 632 | count += 1; 633 | }); 634 | 635 | setTimeout(() => { 636 | expect(count).toEqual(nEmits); 637 | resolve(count); 638 | }, maxRunTime); 639 | }); 640 | tasks.push(task1); 641 | 642 | setTimeout(() => { 643 | for (let i = 0; i < nEmits; ++i) { 644 | localHandle.emit('opened', { 645 | type: 'foo', 646 | message: 'child1', 647 | }); 648 | } 649 | }, 50); 650 | } 651 | } 652 | 653 | { 654 | // Child0 Code 655 | const localHandle = child0Connection.localHandle(); 656 | const remoteHandle = child0Connection.remoteHandle(); 657 | 658 | const task0 = new Promise((resolve) => { 659 | let count = 0; 660 | remoteHandle.addEventListener('opened', (data) => { 661 | expect(data.message).toEqual('child0'); 662 | count += 1; 663 | }); 664 | 665 | setTimeout(() => { 666 | expect(count).toEqual(nEmits); 667 | resolve(count); 668 | }, maxRunTime); 669 | }); 670 | tasks.push(task0); 671 | 672 | setTimeout(() => { 673 | for (let i = 0; i < nEmits; ++i) { 674 | localHandle.emit('clicked', 0); 675 | } 676 | }, 50); 677 | } 678 | 679 | { 680 | // Child1 Code 681 | const localHandle = child1Connection.localHandle(); 682 | const remoteHandle = child1Connection.remoteHandle(); 683 | 684 | const task0 = new Promise((resolve) => { 685 | let count = 0; 686 | remoteHandle.addEventListener('opened', (data) => { 687 | expect(data.message).toEqual('child1'); 688 | count += 1; 689 | }); 690 | 691 | setTimeout(() => { 692 | expect(count).toEqual(nEmits); 693 | resolve(count); 694 | }, maxRunTime); 695 | }); 696 | tasks.push(task0); 697 | 698 | setTimeout(() => { 699 | for (let i = 0; i < nEmits; ++i) { 700 | localHandle.emit('clicked', 1); 701 | } 702 | }, 50); 703 | } 704 | 705 | Promise.all(tasks).then(() => resolve()); 706 | } 707 | ); 708 | } 709 | ); 710 | }); 711 | }); 712 | 713 | test('parent handshake before child', async () => { 714 | const parentOrigin = 'https://parent.example.com'; 715 | const childOrigin = 'https://child.example.com'; 716 | const [parentWindow, childWindow] = makeWindows(parentOrigin, childOrigin); 717 | let parentConnection: Promise, 718 | childConnection: Promise; 719 | 720 | const parentMessenger = new WindowMessenger({ 721 | localWindow: parentWindow, 722 | remoteWindow: childWindow, 723 | remoteOrigin: childWindow.origin, 724 | }); 725 | parentConnection = ParentHandshake(parentMessenger); 726 | 727 | await sleep(100); 728 | 729 | const childMessenger = new WindowMessenger({ 730 | localWindow: childWindow, 731 | remoteWindow: parentWindow, 732 | remoteOrigin: parentWindow.origin, 733 | }); 734 | childConnection = ChildHandshake(childMessenger); 735 | await Promise.all([parentConnection, childConnection]); 736 | }); 737 | 738 | test('child handshake before parent', async () => { 739 | const parentOrigin = 'https://parent.example.com'; 740 | const childOrigin = 'https://child.example.com'; 741 | const [parentWindow, childWindow] = makeWindows(parentOrigin, childOrigin); 742 | let parentConnection: Promise, 743 | childConnection: Promise; 744 | 745 | const childMessenger = new WindowMessenger({ 746 | localWindow: childWindow, 747 | remoteWindow: parentWindow, 748 | remoteOrigin: parentWindow.origin, 749 | }); 750 | childConnection = ChildHandshake(childMessenger); 751 | 752 | await sleep(100); 753 | 754 | const parentMessenger = new WindowMessenger({ 755 | localWindow: parentWindow, 756 | remoteWindow: childWindow, 757 | remoteOrigin: childWindow.origin, 758 | }); 759 | parentConnection = ParentHandshake(parentMessenger); 760 | 761 | await Promise.all([childConnection, parentConnection]); 762 | }); 763 | 764 | test('debug', () => { 765 | const makeBouceBackMessenger = function (): Messenger { 766 | const listeners = new Set<(event: MessageEvent) => void>(); 767 | return { 768 | postMessage: function (message: any) { 769 | listeners.forEach((listener) => { 770 | listener({ data: message } as any); 771 | }); 772 | }, 773 | addMessageListener: function (listener) { 774 | listeners.add(listener); 775 | return function () { 776 | listeners.delete(listener); 777 | }; 778 | }, 779 | }; 780 | }; 781 | 782 | const jestLog = jest.fn(); 783 | const namespace = 'post-me'; 784 | const log = debug(namespace, jestLog); 785 | 786 | let messenger = makeBouceBackMessenger(); 787 | messenger = DebugMessenger(messenger, log); 788 | 789 | messenger.postMessage(1); 790 | messenger.postMessage(2); 791 | 792 | expect(jestLog).toHaveBeenCalledWith(namespace, '➡️ sending message', 1); 793 | expect(jestLog).toHaveBeenCalledWith(namespace, '⬅️ received message', 1); 794 | expect(jestLog).toHaveBeenCalledWith(namespace, '➡️ sending message', 2); 795 | expect(jestLog).toHaveBeenCalledWith(namespace, '⬅️ received message', 2); 796 | expect(jestLog).toHaveBeenCalledTimes(4); 797 | }); 798 | 799 | function MockWorker(script: (self: any) => void) { 800 | const worker: any = new ConcreteEmitter(); 801 | const self: any = new ConcreteEmitter(); 802 | 803 | self.postMessage = (payload: any) => { 804 | worker.emit('message', { data: payload }); 805 | }; 806 | 807 | worker.postMessage = (payload: any) => { 808 | self.emit('message', { data: payload }); 809 | }; 810 | 811 | script(self); 812 | 813 | return worker; 814 | } 815 | 816 | test('worker', () => { 817 | return new Promise((resolve, reject) => { 818 | const workerMethods = { 819 | sum: (x: number, y: number) => x + y, 820 | }; 821 | 822 | const worker = MockWorker((self) => { 823 | const messenger = new WorkerMessenger({ worker: self }); 824 | ChildHandshake(messenger, workerMethods); 825 | }); 826 | 827 | const messenger = new WorkerMessenger({ worker }); 828 | ParentHandshake(messenger).then((connection) => { 829 | const remoteHandle: RemoteHandle< 830 | typeof workerMethods, 831 | {} 832 | > = connection.remoteHandle(); 833 | remoteHandle.call('sum', 15, 17).then((result) => { 834 | expect(result).toEqual(workerMethods.sum(15, 17)); 835 | resolve(); 836 | }); 837 | }); 838 | 839 | setTimeout(reject, 1000); 840 | }); 841 | }); 842 | 843 | test('callback', () => { 844 | return new Promise((resolve) => { 845 | makeHandshake().then(([parentConnection, _childConnection]) => { 846 | // Code in the parent app 847 | { 848 | const remoteHandle = parentConnection.remoteHandle(); 849 | 850 | const onStart = jest.fn(); 851 | const onProgress = jest.fn(); 852 | 853 | remoteHandle 854 | .call('slowSum', 3, 4, onStart, onProgress) 855 | .then(async (value) => { 856 | expect(value).toEqual(7); 857 | expect(onStart).toHaveBeenCalledWith(-1); 858 | expect(onStart).toHaveBeenCalledTimes(1); 859 | expect(onProgress).toHaveBeenCalledWith(0); 860 | expect(onProgress).toHaveBeenCalledWith(1); 861 | expect(onProgress).toHaveBeenCalledWith(2); 862 | expect(onProgress).toHaveBeenCalledWith(3); 863 | expect(onProgress).toHaveBeenCalledWith(4); 864 | expect(onProgress).toHaveBeenCalledTimes(5); 865 | resolve(); 866 | return value; 867 | }); 868 | } 869 | }); 870 | }); 871 | }); 872 | 873 | test('set methods', async () => { 874 | const parentOrigin = 'https://parent.example.com'; 875 | const childOrigin = 'https://child.example.com'; 876 | const [parentWindow, childWindow] = makeWindows(parentOrigin, childOrigin); 877 | 878 | const parentMessenger = new WindowMessenger({ 879 | localWindow: parentWindow, 880 | remoteWindow: childWindow, 881 | remoteOrigin: childWindow.origin, 882 | }); 883 | 884 | const childMessenger = new WindowMessenger({ 885 | localWindow: childWindow, 886 | remoteWindow: parentWindow, 887 | remoteOrigin: parentWindow.origin, 888 | }); 889 | 890 | const initialChildMethods = { 891 | foo() { 892 | return 1; 893 | }, 894 | bar() { 895 | return 1; 896 | }, 897 | }; 898 | 899 | const [parentConnection, childConnection] = await Promise.all([ 900 | ParentHandshake(parentMessenger), 901 | ChildHandshake(childMessenger, initialChildMethods), 902 | ] as const); 903 | 904 | // Child code 905 | { 906 | const localHandle: LocalHandle< 907 | typeof initialChildMethods 908 | > = childConnection.localHandle(); 909 | 910 | const newChildMethods = { 911 | foo() { 912 | return 2; 913 | }, 914 | bar() { 915 | return 2; 916 | }, 917 | }; 918 | 919 | localHandle.setMethods(newChildMethods); 920 | localHandle.setMethod('bar', () => 3); 921 | } 922 | 923 | // Parent code 924 | { 925 | const remoteHandle: RemoteHandle< 926 | typeof initialChildMethods 927 | > = parentConnection.remoteHandle(); 928 | 929 | expect(await remoteHandle.call('foo')).toEqual(2); 930 | expect(await remoteHandle.call('bar')).toEqual(3); 931 | } 932 | }); 933 | -------------------------------------------------------------------------------- /packages/core/tests/jsdom.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'jsdom/lib/jsdom/living/generated/MessageEvent'; 2 | declare module 'jsdom/lib/jsdom/living/helpers/events'; 3 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "esnext" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */, 5 | "module": "esnext" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, 6 | "lib": [ 7 | "esnext", 8 | "dom" 9 | // "webworker" 10 | ] /* Specify library files to be included in the compilation. */, 11 | "strict": true /* Enable all strict type-checking options. */, 12 | "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */, 13 | "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */, 14 | "rootDir": "./src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */, 15 | "outDir": "./dist" /* Redirect output structure to the directory. */, 16 | "declaration": true /* Generates corresponding '.d.ts' file. */, 17 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 18 | // "allowJs": true, /* Allow javascript files to be compiled. */ 19 | // "checkJs": true, /* Report errors in .js files. */ 20 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 21 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 22 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 23 | // "outFile": "./", /* Concatenate and emit output to single file. */ 24 | // "composite": true, /* Enable project compilation */ 25 | // "incremental": true, /* Enable incremental compilation */ 26 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 27 | // "removeComments": true, /* Do not emit comments to output. */ 28 | // "noEmit": true, /* Do not emit outputs. */ 29 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 30 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 31 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 32 | /* Strict Type-Checking Options */ 33 | // "strictNullChecks": true, /* Enable strict null checks. */ 34 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 35 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 36 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 37 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 38 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 39 | /* Additional Checks */ 40 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 41 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 42 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 43 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 44 | /* Module Resolution Options */ 45 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 46 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 47 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 48 | // "typeRoots": [], /* List of folders to include type definitions from. */ 49 | // "types": [], /* Type declaration files to be included in compilation. */ 50 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 51 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 52 | /* Source Map Options */ 53 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 54 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 55 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 56 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 57 | /* Experimental Options */ 58 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 59 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 60 | }, 61 | "include": ["src", "tests"], 62 | "exclude": [ 63 | // "dist", 64 | "node_modules", 65 | "**/*.spec.ts" 66 | ] 67 | } 68 | -------------------------------------------------------------------------------- /packages/mpi/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | www/ 4 | docs/ 5 | temp/ 6 | 7 | coverage/ 8 | .rpt2_cache/ 9 | -------------------------------------------------------------------------------- /packages/mpi/.prettierignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /dist 3 | /www 4 | /coverage 5 | /scripts 6 | /api 7 | /docs 8 | /temp 9 | *.config.js -------------------------------------------------------------------------------- /packages/mpi/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /packages/mpi/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | ## [0.1.2](https://github.com/alesgenova/post-me/compare/@post-me/mpi@0.1.1...@post-me/mpi@0.1.2) (2021-02-19) 7 | 8 | **Note:** Version bump only for package @post-me/mpi 9 | 10 | 11 | 12 | 13 | 14 | ## 0.1.1 (2021-02-10) 15 | 16 | **Note:** Version bump only for package @post-me/mpi 17 | -------------------------------------------------------------------------------- /packages/mpi/README.md: -------------------------------------------------------------------------------- 1 |

@post-me/mpi

2 | 3 |

Write parallel algorithms in JavaScript using a MPI-like API.

4 | 5 | ![diagram](./diagram.png) 6 | 7 | __@post-me/mpi__ is a library to write parallel algorithms that can run on a pool of web workers, using an API similar to [MPI](https://en.wikipedia.org/wiki/Message_Passing_Interface). 8 | 9 | A worker pool is a set of workers that are mutually interconnected. Each worker can communicate directly with any other worker in the pool. 10 | 11 | The low level communication between workers and the parent application is managed by __post-me__. 12 | 13 | ## Usage 14 | 15 | Below is a small example of using __@post-me/mpi__ in practice. In this example we will be sorting an array in parallel. 16 | 17 | Worker code: 18 | ```javascript 19 | import { joinPool } from '@post-me/mpi'; 20 | 21 | const connection = await joinPool(self); 22 | 23 | // The parallel sort method 24 | const sort = (communicator) => async (array) => { 25 | const root = 0; 26 | let subArray = await communicator.scatter(array, root); 27 | subArray.sort((a, b) => a - b); 28 | const sorted = await communicator.reduce(subArray, merge, root); 29 | 30 | return sorted; 31 | } 32 | 33 | // Expose parallel methods to the application 34 | connection.registerMethods({ sort }); 35 | 36 | // Merge two sorted arrays into a single sorted array 37 | function merge(a0, a1) {/* ... */} 38 | ``` 39 | 40 | Parent code: 41 | ```javascript 42 | import { createPool } from '@post-me/mpi'; 43 | 44 | const array = new Float32Array(1024); 45 | const N_WORKERS = 4; 46 | 47 | // Create the workers 48 | const workers: Worker[] = []; 49 | for (let i = 0; i < N_WORKERS; ++i) { 50 | workers.push(new Worker('./worker.js')); 51 | } 52 | 53 | // Create a pool of mutually interconnected workers 54 | const workerPool = await createPool(workers); 55 | 56 | // Pass different parameter to the parallel method based on the rank of the worker 57 | const root = 0; 58 | const args = (rank) => rank === root ? array : null; 59 | const transfer = (rank, [arr]) => rank === root ? [arr.buffer] : []; 60 | 61 | // Call the parallel method 'sort' 62 | const result = await workerPool.call('sort', args, transfer); 63 | 64 | // The sorted array is returned by the root worker 65 | const sortedArray = result[root]; 66 | ``` 67 | 68 | ## MPI Operations 69 | 70 | The following MPI operations are already implemented in __@post-me/mpi__: 71 | - `send` 72 | - `recv` 73 | - `bcast` 74 | - `scatter` 75 | - `gather` 76 | - `reduce` 77 | - `barrier` 78 | - `allGather` 79 | - `allReduce` 80 | 81 | ## Typescript 82 | 83 | The library has extensive typescript support, all arguments, methods, return types, etc., are all type checked so that most coding mistakes can be caught at compile tim. 84 | 85 | ## Benchmark 86 | Below is a quick non-scientific benchmark that shows that indeed running a parallel algorithm is faster than serial. In the plot I'm showing the speedup obtained when sorting arrays of various length as a function of the number of workers. 87 | 88 | ![benchmark](./benchmark.png) 89 | 90 | ## Demo 91 | I created a small [demo page](https://alesgenova.github.io/post-me-demo/mpi/) where you can run a couple of test algorithms yourself ([source](https://github.com/alesgenova/post-me-demo/tree/main/examples/mpi)). 92 | -------------------------------------------------------------------------------- /packages/mpi/api-extractor.json: -------------------------------------------------------------------------------- 1 | /** 2 | * Config file for API Extractor. For more info, please visit: https://api-extractor.com 3 | */ 4 | { 5 | "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", 6 | /** 7 | * Optionally specifies another JSON config file that this file extends from. This provides a way for 8 | * standard settings to be shared across multiple projects. 9 | * 10 | * If the path starts with "./" or "../", the path is resolved relative to the folder of the file that contains 11 | * the "extends" field. Otherwise, the first path segment is interpreted as an NPM package name, and will be 12 | * resolved using NodeJS require(). 13 | * 14 | * SUPPORTED TOKENS: none 15 | * DEFAULT VALUE: "" 16 | */ 17 | // "extends": "./shared/api-extractor-base.json" 18 | // "extends": "my-package/include/api-extractor-base.json" 19 | /** 20 | * Determines the "" token that can be used with other config file settings. The project folder 21 | * typically contains the tsconfig.json and package.json config files, but the path is user-defined. 22 | * 23 | * The path is resolved relative to the folder of the config file that contains the setting. 24 | * 25 | * The default value for "projectFolder" is the token "", which means the folder is determined by traversing 26 | * parent folders, starting from the folder containing api-extractor.json, and stopping at the first folder 27 | * that contains a tsconfig.json file. If a tsconfig.json file cannot be found in this way, then an error 28 | * will be reported. 29 | * 30 | * SUPPORTED TOKENS: 31 | * DEFAULT VALUE: "" 32 | */ 33 | // "projectFolder": "..", 34 | /** 35 | * (REQUIRED) Specifies the .d.ts file to be used as the starting point for analysis. API Extractor 36 | * analyzes the symbols exported by this module. 37 | * 38 | * The file extension must be ".d.ts" and not ".ts". 39 | * 40 | * The path is resolved relative to the folder of the config file that contains the setting; to change this, 41 | * prepend a folder token such as "". 42 | * 43 | * SUPPORTED TOKENS: , , 44 | */ 45 | "mainEntryPointFilePath": "/dist/index.d.ts", 46 | /** 47 | * A list of NPM package names whose exports should be treated as part of this package. 48 | * 49 | * For example, suppose that Webpack is used to generate a distributed bundle for the project "library1", 50 | * and another NPM package "library2" is embedded in this bundle. Some types from library2 may become part 51 | * of the exported API for library1, but by default API Extractor would generate a .d.ts rollup that explicitly 52 | * imports library2. To avoid this, we can specify: 53 | * 54 | * "bundledPackages": [ "library2" ], 55 | * 56 | * This would direct API Extractor to embed those types directly in the .d.ts rollup, as if they had been 57 | * local files for library1. 58 | */ 59 | "bundledPackages": [], 60 | /** 61 | * Determines how the TypeScript compiler engine will be invoked by API Extractor. 62 | */ 63 | "compiler": { 64 | /** 65 | * Specifies the path to the tsconfig.json file to be used by API Extractor when analyzing the project. 66 | * 67 | * The path is resolved relative to the folder of the config file that contains the setting; to change this, 68 | * prepend a folder token such as "". 69 | * 70 | * Note: This setting will be ignored if "overrideTsconfig" is used. 71 | * 72 | * SUPPORTED TOKENS: , , 73 | * DEFAULT VALUE: "/tsconfig.json" 74 | */ 75 | // "tsconfigFilePath": "/tsconfig.json", 76 | /** 77 | * Provides a compiler configuration that will be used instead of reading the tsconfig.json file from disk. 78 | * The object must conform to the TypeScript tsconfig schema: 79 | * 80 | * http://json.schemastore.org/tsconfig 81 | * 82 | * If omitted, then the tsconfig.json file will be read from the "projectFolder". 83 | * 84 | * DEFAULT VALUE: no overrideTsconfig section 85 | */ 86 | // "overrideTsconfig": { 87 | // . . . 88 | // } 89 | /** 90 | * This option causes the compiler to be invoked with the --skipLibCheck option. This option is not recommended 91 | * and may cause API Extractor to produce incomplete or incorrect declarations, but it may be required when 92 | * dependencies contain declarations that are incompatible with the TypeScript engine that API Extractor uses 93 | * for its analysis. Where possible, the underlying issue should be fixed rather than relying on skipLibCheck. 94 | * 95 | * DEFAULT VALUE: false 96 | */ 97 | // "skipLibCheck": true, 98 | }, 99 | /** 100 | * Configures how the API report file (*.api.md) will be generated. 101 | */ 102 | "apiReport": { 103 | /** 104 | * (REQUIRED) Whether to generate an API report. 105 | */ 106 | "enabled": true, 107 | /** 108 | * The filename for the API report files. It will be combined with "reportFolder" or "reportTempFolder" to produce 109 | * a full file path. 110 | * 111 | * The file extension should be ".api.md", and the string should not contain a path separator such as "\" or "/". 112 | * 113 | * SUPPORTED TOKENS: , 114 | * DEFAULT VALUE: ".api.md" 115 | */ 116 | "reportFileName": ".api.md", 117 | /** 118 | * Specifies the folder where the API report file is written. The file name portion is determined by 119 | * the "reportFileName" setting. 120 | * 121 | * The API report file is normally tracked by Git. Changes to it can be used to trigger a branch policy, 122 | * e.g. for an API review. 123 | * 124 | * The path is resolved relative to the folder of the config file that contains the setting; to change this, 125 | * prepend a folder token such as "". 126 | * 127 | * SUPPORTED TOKENS: , , 128 | * DEFAULT VALUE: "/etc/" 129 | */ 130 | "reportFolder": "/api/" 131 | /** 132 | * Specifies the folder where the temporary report file is written. The file name portion is determined by 133 | * the "reportFileName" setting. 134 | * 135 | * After the temporary file is written to disk, it is compared with the file in the "reportFolder". 136 | * If they are different, a production build will fail. 137 | * 138 | * The path is resolved relative to the folder of the config file that contains the setting; to change this, 139 | * prepend a folder token such as "". 140 | * 141 | * SUPPORTED TOKENS: , , 142 | * DEFAULT VALUE: "/temp/" 143 | */ 144 | // "reportTempFolder": "/temp/" 145 | }, 146 | /** 147 | * Configures how the doc model file (*.api.json) will be generated. 148 | */ 149 | "docModel": { 150 | /** 151 | * (REQUIRED) Whether to generate a doc model file. 152 | */ 153 | "enabled": true, 154 | /** 155 | * The output path for the doc model file. The file extension should be ".api.json". 156 | * 157 | * The path is resolved relative to the folder of the config file that contains the setting; to change this, 158 | * prepend a folder token such as "". 159 | * 160 | * SUPPORTED TOKENS: , , 161 | * DEFAULT VALUE: "/temp/.api.json" 162 | */ 163 | "apiJsonFilePath": "/../../docs/input/.api.json" 164 | }, 165 | /** 166 | * Configures how the .d.ts rollup file will be generated. 167 | */ 168 | "dtsRollup": { 169 | /** 170 | * (REQUIRED) Whether to generate the .d.ts rollup file. 171 | */ 172 | "enabled": false 173 | /** 174 | * Specifies the output path for a .d.ts rollup file to be generated without any trimming. 175 | * This file will include all declarations that are exported by the main entry point. 176 | * 177 | * If the path is an empty string, then this file will not be written. 178 | * 179 | * The path is resolved relative to the folder of the config file that contains the setting; to change this, 180 | * prepend a folder token such as "". 181 | * 182 | * SUPPORTED TOKENS: , , 183 | * DEFAULT VALUE: "/dist/.d.ts" 184 | */ 185 | // "untrimmedFilePath": "/dist/.d.ts", 186 | /** 187 | * Specifies the output path for a .d.ts rollup file to be generated with trimming for a "beta" release. 188 | * This file will include only declarations that are marked as "@public" or "@beta". 189 | * 190 | * The path is resolved relative to the folder of the config file that contains the setting; to change this, 191 | * prepend a folder token such as "". 192 | * 193 | * SUPPORTED TOKENS: , , 194 | * DEFAULT VALUE: "" 195 | */ 196 | // "betaTrimmedFilePath": "/dist/-beta.d.ts", 197 | /** 198 | * Specifies the output path for a .d.ts rollup file to be generated with trimming for a "public" release. 199 | * This file will include only declarations that are marked as "@public". 200 | * 201 | * If the path is an empty string, then this file will not be written. 202 | * 203 | * The path is resolved relative to the folder of the config file that contains the setting; to change this, 204 | * prepend a folder token such as "". 205 | * 206 | * SUPPORTED TOKENS: , , 207 | * DEFAULT VALUE: "" 208 | */ 209 | // "publicTrimmedFilePath": "/dist/-public.d.ts", 210 | /** 211 | * When a declaration is trimmed, by default it will be replaced by a code comment such as 212 | * "Excluded from this release type: exampleMember". Set "omitTrimmingComments" to true to remove the 213 | * declaration completely. 214 | * 215 | * DEFAULT VALUE: false 216 | */ 217 | // "omitTrimmingComments": true 218 | }, 219 | /** 220 | * Configures how the tsdoc-metadata.json file will be generated. 221 | */ 222 | "tsdocMetadata": { 223 | /** 224 | * Whether to generate the tsdoc-metadata.json file. 225 | * 226 | * DEFAULT VALUE: true 227 | */ 228 | // "enabled": true, 229 | /** 230 | * Specifies where the TSDoc metadata file should be written. 231 | * 232 | * The path is resolved relative to the folder of the config file that contains the setting; to change this, 233 | * prepend a folder token such as "". 234 | * 235 | * The default value is "", which causes the path to be automatically inferred from the "tsdocMetadata", 236 | * "typings" or "main" fields of the project's package.json. If none of these fields are set, the lookup 237 | * falls back to "tsdoc-metadata.json" in the package folder. 238 | * 239 | * SUPPORTED TOKENS: , , 240 | * DEFAULT VALUE: "" 241 | */ 242 | // "tsdocMetadataFilePath": "/dist/tsdoc-metadata.json" 243 | }, 244 | /** 245 | * Specifies what type of newlines API Extractor should use when writing output files. By default, the output files 246 | * will be written with Windows-style newlines. To use POSIX-style newlines, specify "lf" instead. 247 | * To use the OS's default newline kind, specify "os". 248 | * 249 | * DEFAULT VALUE: "crlf" 250 | */ 251 | // "newlineKind": "crlf", 252 | /** 253 | * Configures how API Extractor reports error and warning messages produced during analysis. 254 | * 255 | * There are three sources of messages: compiler messages, API Extractor messages, and TSDoc messages. 256 | */ 257 | "messages": { 258 | /** 259 | * Configures handling of diagnostic messages reported by the TypeScript compiler engine while analyzing 260 | * the input .d.ts files. 261 | * 262 | * TypeScript message identifiers start with "TS" followed by an integer. For example: "TS2551" 263 | * 264 | * DEFAULT VALUE: A single "default" entry with logLevel=warning. 265 | */ 266 | "compilerMessageReporting": { 267 | /** 268 | * Configures the default routing for messages that don't match an explicit rule in this table. 269 | */ 270 | "default": { 271 | /** 272 | * Specifies whether the message should be written to the the tool's output log. Note that 273 | * the "addToApiReportFile" property may supersede this option. 274 | * 275 | * Possible values: "error", "warning", "none" 276 | * 277 | * Errors cause the build to fail and return a nonzero exit code. Warnings cause a production build fail 278 | * and return a nonzero exit code. For a non-production build (e.g. when "api-extractor run" includes 279 | * the "--local" option), the warning is displayed but the build will not fail. 280 | * 281 | * DEFAULT VALUE: "warning" 282 | */ 283 | "logLevel": "warning" 284 | /** 285 | * When addToApiReportFile is true: If API Extractor is configured to write an API report file (.api.md), 286 | * then the message will be written inside that file; otherwise, the message is instead logged according to 287 | * the "logLevel" option. 288 | * 289 | * DEFAULT VALUE: false 290 | */ 291 | // "addToApiReportFile": false 292 | } 293 | // "TS2551": { 294 | // "logLevel": "warning", 295 | // "addToApiReportFile": true 296 | // }, 297 | // 298 | // . . . 299 | }, 300 | /** 301 | * Configures handling of messages reported by API Extractor during its analysis. 302 | * 303 | * API Extractor message identifiers start with "ae-". For example: "ae-extra-release-tag" 304 | * 305 | * DEFAULT VALUE: See api-extractor-defaults.json for the complete table of extractorMessageReporting mappings 306 | */ 307 | "extractorMessageReporting": { 308 | "default": { 309 | "logLevel": "warning" 310 | // "addToApiReportFile": false 311 | } 312 | // "ae-extra-release-tag": { 313 | // "logLevel": "warning", 314 | // "addToApiReportFile": true 315 | // }, 316 | // 317 | // . . . 318 | }, 319 | /** 320 | * Configures handling of messages reported by the TSDoc parser when analyzing code comments. 321 | * 322 | * TSDoc message identifiers start with "tsdoc-". For example: "tsdoc-link-tag-unescaped-text" 323 | * 324 | * DEFAULT VALUE: A single "default" entry with logLevel=warning. 325 | */ 326 | "tsdocMessageReporting": { 327 | "default": { 328 | "logLevel": "warning" 329 | // "addToApiReportFile": false 330 | } 331 | // "tsdoc-link-tag-unescaped-text": { 332 | // "logLevel": "warning", 333 | // "addToApiReportFile": true 334 | // }, 335 | // 336 | // . . . 337 | } 338 | } 339 | } -------------------------------------------------------------------------------- /packages/mpi/api/mpi.api.md: -------------------------------------------------------------------------------- 1 | ## API Report File for "@post-me/mpi" 2 | 3 | > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). 4 | 5 | ```ts 6 | 7 | import { Callable } from 'post-me'; 8 | import { InnerType } from 'post-me'; 9 | import { MethodsType } from 'post-me'; 10 | import { ValueOrPromise } from 'post-me'; 11 | 12 | // @public (undocumented) 13 | export interface Communicator { 14 | // (undocumented) 15 | allGather(data: T): Promise; 16 | // (undocumented) 17 | allReduce(data: T, reducer: (a: T, b: T) => T): Promise; 18 | // (undocumented) 19 | barrier(): Promise; 20 | // (undocumented) 21 | bcast(data: T | null, root: number): Promise; 22 | // (undocumented) 23 | gather(data: T, root: number): Promise; 24 | // (undocumented) 25 | rank(): number; 26 | // (undocumented) 27 | recv(source: number, tag: number): Promise; 28 | // (undocumented) 29 | reduce(data: T, reducer: (a: T, b: T) => T, root: number): Promise; 30 | // Warning: (ae-forgotten-export) The symbol "ArrayLike" needs to be exported by the entry point index.d.ts 31 | // 32 | // (undocumented) 33 | scatter(data: T | null, root: number): Promise; 34 | // (undocumented) 35 | send(data: T, destination: number, tag: number, transfer?: Transferable[]): Promise; 36 | // (undocumented) 37 | size(): number; 38 | } 39 | 40 | // @public (undocumented) 41 | export function createPool(workers: Worker[], ChannelConstructor?: any): Promise; 42 | 43 | // @public (undocumented) 44 | export function joinPool(workerScope: any): Promise; 45 | 46 | // @public (undocumented) 47 | export type ParallelMethod> = (comm: Communicator) => Callable, ValueOrPromise>>>; 48 | 49 | // @public (undocumented) 50 | export type ParallelMethods = { 51 | [K in keyof M]: ParallelMethod; 52 | }; 53 | 54 | // @public (undocumented) 55 | export interface PoolConnection { 56 | // (undocumented) 57 | registerMethod(methodName: K, method: ParallelMethod): void; 58 | // (undocumented) 59 | registerMethods(methods: ParallelMethods): void; 60 | // (undocumented) 61 | setReturnTransfer(methodName: K, transfer: (result: InnerType>) => Transferable[]): void; 62 | } 63 | 64 | // @public (undocumented) 65 | export interface WorkerPool { 66 | // (undocumented) 67 | call(methodName: K, args: (rank: number) => Parameters, transfer?: (rank: number, args: Parameters) => Transferable[]): Promise>[]>; 68 | } 69 | 70 | 71 | ``` 72 | -------------------------------------------------------------------------------- /packages/mpi/benchmark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alesgenova/post-me/c567a2b8d1194582f145970209b35c64691ee5d5/packages/mpi/benchmark.png -------------------------------------------------------------------------------- /packages/mpi/diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alesgenova/post-me/c567a2b8d1194582f145970209b35c64691ee5d5/packages/mpi/diagram.png -------------------------------------------------------------------------------- /packages/mpi/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { ConcreteEmitter, MethodsType } from 'post-me'; 2 | import { 3 | createPool, 4 | joinPool, 5 | ParallelMethods, 6 | WorkerPool, 7 | ParallelMethod, 8 | } from '../src'; 9 | 10 | function MockWorker(script: (self: any) => void) { 11 | const worker: any = new ConcreteEmitter(); 12 | const self: any = new ConcreteEmitter(); 13 | 14 | self.postMessage = (payload: any) => { 15 | worker.emit('message', { data: payload }); 16 | }; 17 | 18 | worker.postMessage = (payload: any) => { 19 | self.emit('message', { data: payload }); 20 | }; 21 | 22 | script(self); 23 | 24 | return worker; 25 | } 26 | 27 | class MockChannel { 28 | public port1: any; 29 | public port2: any; 30 | constructor() { 31 | const port1: any = new ConcreteEmitter(); 32 | port1.start = () => {}; 33 | const port2: any = new ConcreteEmitter(); 34 | port2.start = () => {}; 35 | 36 | port1.postMessage = (payload: any) => { 37 | port2.emit('message', { data: payload }); 38 | }; 39 | 40 | port2.postMessage = (payload: any) => { 41 | port1.emit('message', { data: payload }); 42 | }; 43 | 44 | this.port1 = port1; 45 | this.port2 = port2; 46 | } 47 | } 48 | 49 | function makeHandshake( 50 | n: number, 51 | methods: ParallelMethods 52 | ): Promise> { 53 | const workerScript = (self: any) => { 54 | joinPool(self).then((connection) => { 55 | connection.registerMethods(methods); 56 | }); 57 | }; 58 | 59 | const workers: Worker[] = []; 60 | for (let i = 0; i < N_WORKERS; ++i) { 61 | const worker = MockWorker(workerScript); 62 | workers.push(worker); 63 | } 64 | 65 | return createPool(workers, MockChannel); 66 | } 67 | 68 | const N_WORKERS = 7; 69 | 70 | test('create pool', () => { 71 | return new Promise((resolve) => { 72 | makeHandshake(N_WORKERS, {}).then((pool) => { 73 | resolve(); 74 | }); 75 | }); 76 | }); 77 | 78 | test('send', () => { 79 | return new Promise((resolve) => { 80 | const send: ParallelMethod<(data: number) => number> = ( 81 | communicator 82 | ) => async (data) => { 83 | const rank = communicator.rank(); 84 | const size = communicator.size(); 85 | const destination = (rank + 1) % size; 86 | const source = (rank + size - 1) % size; 87 | communicator.send(data, destination, 0); 88 | data = await communicator.recv(source, 0); 89 | return data; 90 | }; 91 | makeHandshake(N_WORKERS, { send }).then((pool) => { 92 | pool 93 | .call('send', (rank) => [rank]) 94 | .then((result) => { 95 | for (let i = 0; i < N_WORKERS; ++i) { 96 | expect(result[i]).toEqual((i + N_WORKERS - 1) % N_WORKERS); 97 | } 98 | resolve(); 99 | }); 100 | }); 101 | }); 102 | }); 103 | 104 | test('send', async () => { 105 | const send: ParallelMethod<(data: number) => number> = ( 106 | communicator 107 | ) => async (data) => { 108 | const rank = communicator.rank(); 109 | const size = communicator.size(); 110 | const destination = (rank + 1) % size; 111 | const source = (rank + size - 1) % size; 112 | communicator.send(data, destination, 0); 113 | data = await communicator.recv(source, 0); 114 | return data; 115 | }; 116 | 117 | const pool = await makeHandshake(N_WORKERS, { send }); 118 | const result = await pool.call('send', (rank) => [rank]); 119 | result.forEach((value, i) => { 120 | expect(value).toEqual((i + N_WORKERS - 1) % N_WORKERS); 121 | }); 122 | }); 123 | 124 | test('bcast', async () => { 125 | const bcast: ParallelMethod<(data: number | null, root: number) => number> = ( 126 | communicator 127 | ) => async (data, root) => { 128 | return await communicator.bcast(data, root); 129 | }; 130 | 131 | const ROOT = N_WORKERS - 1; 132 | const pool = await makeHandshake(N_WORKERS, { bcast }); 133 | const result = await pool.call('bcast', (rank) => [ 134 | rank === ROOT ? rank : null, 135 | ROOT, 136 | ]); 137 | result.forEach((value) => { 138 | expect(value).toEqual(ROOT); 139 | }); 140 | }); 141 | 142 | function makeSeq(start: number, size: number): Uint16Array { 143 | const array = new Uint16Array(size); 144 | for (let i = 0; i < size; ++i) { 145 | array[i] = i + start; 146 | } 147 | 148 | return array; 149 | } 150 | 151 | test('scatter', async () => { 152 | const scatter: ParallelMethod< 153 | (data: Uint16Array | null, root: number) => Uint16Array 154 | > = (communicator) => async (data, root) => { 155 | return await communicator.scatter(data, root); 156 | }; 157 | 158 | const ROOT = N_WORKERS - 1; 159 | const input = makeSeq(0, N_WORKERS * 4 + 3); 160 | const pool = await makeHandshake(N_WORKERS, { scatter }); 161 | const result = await pool.call('scatter', (rank) => [ 162 | rank === ROOT ? input : null, 163 | ROOT, 164 | ]); 165 | const N = Math.floor(input.length / N_WORKERS); 166 | let output = new Uint16Array(0); 167 | result.forEach((value) => { 168 | expect(value.length).toBeGreaterThanOrEqual(N); 169 | expect(value.length).toBeLessThanOrEqual(N + 1); 170 | output = Uint16Array.of(...output, ...value); 171 | }); 172 | 173 | expect(output).toEqual(input); 174 | }); 175 | 176 | test('gather', async () => { 177 | const gather: ParallelMethod< 178 | (data: Uint16Array, root: number) => Uint16Array | null 179 | > = (communicator) => async (data, root) => { 180 | return await communicator.gather(data, root); 181 | }; 182 | 183 | const allGather: ParallelMethod<(data: Uint16Array) => Uint16Array> = ( 184 | communicator 185 | ) => async (data) => { 186 | return await communicator.allGather(data); 187 | }; 188 | 189 | const ROOT = N_WORKERS - 1; 190 | const SIZE = 4; 191 | const pool = await makeHandshake(N_WORKERS, { gather, allGather }); 192 | 193 | { 194 | const result = await pool.call('gather', (rank) => [ 195 | makeSeq(rank * SIZE, SIZE), 196 | ROOT, 197 | ]); 198 | result.forEach((value, i) => { 199 | if (i === ROOT) { 200 | expect(value).toEqual(makeSeq(0, SIZE * N_WORKERS)); 201 | } else { 202 | expect(value).toEqual(null); 203 | } 204 | }); 205 | } 206 | 207 | { 208 | const result = await pool.call('allGather', (rank) => [ 209 | makeSeq(rank * SIZE, SIZE), 210 | ]); 211 | result.forEach((value, i) => { 212 | expect(value).toEqual(makeSeq(0, SIZE * N_WORKERS)); 213 | }); 214 | } 215 | }); 216 | 217 | test('reduce', async () => { 218 | const reduce: ParallelMethod< 219 | (data: number, root: number) => number | null 220 | > = (communicator) => async (data, root) => { 221 | return await communicator.reduce(data, (a, b) => a + b, root); 222 | }; 223 | 224 | const allReduce: ParallelMethod<(data: number) => number> = ( 225 | communicator 226 | ) => async (data) => { 227 | return await communicator.allReduce(data, (a, b) => a + b); 228 | }; 229 | 230 | const ROOT = 0; 231 | const pool = await makeHandshake(N_WORKERS, { reduce, allReduce }); 232 | 233 | { 234 | const result = await pool.call('reduce', (rank) => [rank, ROOT]); 235 | result.forEach((value, i) => { 236 | if (i === ROOT) { 237 | expect(value).toEqual(((N_WORKERS - 1) * N_WORKERS) / 2); 238 | } else { 239 | expect(value).toEqual(null); 240 | } 241 | }); 242 | } 243 | 244 | { 245 | const result = await pool.call('allReduce', (rank) => [rank]); 246 | result.forEach((value, i) => { 247 | expect(value).toEqual(((N_WORKERS - 1) * N_WORKERS) / 2); 248 | }); 249 | } 250 | }); 251 | 252 | function sleep(time: number) { 253 | return new Promise((resolve) => { 254 | setTimeout(resolve, time); 255 | }); 256 | } 257 | 258 | test('barrier', async () => { 259 | const barrier: ParallelMethod<(t: number) => number> = ( 260 | communicator 261 | ) => async (t) => { 262 | const t0: any = new Date(); 263 | await sleep(t); 264 | await communicator.barrier(); 265 | const t1: any = new Date(); 266 | return t1 - t0; 267 | }; 268 | 269 | const MAX_SLEEP = 500; 270 | const pool = await makeHandshake(N_WORKERS, { barrier }); 271 | const result = await pool.call('barrier', (rank) => [ 272 | (MAX_SLEEP * rank) / (N_WORKERS - 1), 273 | ]); 274 | const eps = 2; 275 | result.forEach((value) => { 276 | expect(Math.abs(value - MAX_SLEEP)).toBeLessThanOrEqual(eps); 277 | }); 278 | }); 279 | -------------------------------------------------------------------------------- /packages/mpi/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'jsdom', 4 | }; -------------------------------------------------------------------------------- /packages/mpi/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@post-me/mpi", 3 | "version": "0.1.2", 4 | "description": "MPI-like concurrency for a pool of web workers", 5 | "author": { 6 | "name": "Alessandro Genova", 7 | "email": "ales.genova@gmail.com", 8 | "url": "https://github.com/alesgenova" 9 | }, 10 | "license": "MIT", 11 | "homepage": "https://github.com/alesgenova/post-me", 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/alesgenova/post-me.git", 15 | "directory": "packages/mpi" 16 | }, 17 | "bugs": { 18 | "url": "https://github.com/alesgenova/post-me/issues", 19 | "email": "ales.genova@gmail.com" 20 | }, 21 | "main": "./dist/index.js", 22 | "exports": { 23 | "import": "./dist/index.esnext.mjs", 24 | "require": "./dist/index.js" 25 | }, 26 | "module": "./dist/index.mjs", 27 | "types": "./dist/index.d.ts", 28 | "files": [ 29 | "dist" 30 | ], 31 | "sideEffects": false, 32 | "scripts": { 33 | "build": "npm run build:src && npm run build:types", 34 | "build:src": "rollup -c", 35 | "build:types": "tsc --emitDeclarationOnly", 36 | "build:docs": "npm run build:types && api-extractor run --local --verbose", 37 | "build:demo": "npm run build:src && node demo/build.js www", 38 | "prepublishOnly": "npm run build", 39 | "test": "jest --coverage tests", 40 | "prettier:check-staged": "pretty-quick --staged --check --pattern '**/*.{js,jsx,ts,tsx,css,html}'", 41 | "prettier:write-staged": "pretty-quick --staged --write --pattern '**/*.{js,jsx,ts,tsx,css,html}'", 42 | "prettier:check-modified": "pretty-quick --check --pattern '**/*.{js,jsx,ts,tsx,css,html}'", 43 | "prettier:write-modified": "pretty-quick --write --pattern '**/*.{js,jsx,ts,tsx,css,html}'", 44 | "prettier:check-all": "prettier --check '**/*.{js,jsx,ts,tsx,css,html}'", 45 | "prettier:write-all": "prettier --write '**/*.{js,jsx,ts,tsx,css,html}'" 46 | }, 47 | "dependencies": { 48 | "post-me": "^0.4.5" 49 | }, 50 | "devDependencies": { 51 | "@babel/core": "^7.11.6", 52 | "@babel/preset-env": "^7.12.11", 53 | "@microsoft/api-extractor": "^7.13.0", 54 | "@rollup/plugin-babel": "^5.2.2", 55 | "@rollup/plugin-node-resolve": "^11.0.1", 56 | "@rollup/plugin-typescript": "^8.1.0", 57 | "@types/jest": "^26.0.15", 58 | "@types/jsdom": "^16.2.5", 59 | "gh-pages": "^3.1.0", 60 | "husky": "^4.3.0", 61 | "jest": "^26.6.3", 62 | "jsdom": "^16.4.0", 63 | "prettier": "^2.2.1", 64 | "pretty-quick": "^3.1.0", 65 | "rollup": "^2.35.1", 66 | "ts-jest": "^26.4.4", 67 | "tslib": "^2.0.3", 68 | "typescript": "^4.1.3" 69 | }, 70 | "husky": { 71 | "hooks": { 72 | "pre-commit": "npm run prettier:write-staged" 73 | } 74 | }, 75 | "publishConfig": { 76 | "access": "public" 77 | }, 78 | "browserslist": [ 79 | "defaults", 80 | "> 0.5%", 81 | "not IE 11" 82 | ], 83 | "keywords": [ 84 | "concurrency", 85 | "parallelization", 86 | "worker", 87 | "workers", 88 | "web-worker", 89 | "web-workers", 90 | "parallel-computing", 91 | "front-end", 92 | "back-end", 93 | "typescript" 94 | ] 95 | } 96 | -------------------------------------------------------------------------------- /packages/mpi/rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from '@rollup/plugin-typescript'; 2 | import { nodeResolve } from '@rollup/plugin-node-resolve'; 3 | import { getBabelOutputPlugin } from '@rollup/plugin-babel'; 4 | 5 | export default [ 6 | { 7 | input: 'src/index.ts', 8 | output: [ 9 | // ES Module, straight TS to JS compilation 10 | { 11 | file: 'dist/index.esnext.mjs', 12 | format: 'esm' 13 | }, 14 | // ES Module, transpiled to ES5 15 | { 16 | file: 'dist/index.mjs', 17 | format: 'esm', 18 | plugins: [ 19 | getBabelOutputPlugin({ 20 | presets: [['@babel/preset-env', { modules: false }]] 21 | }) 22 | ] 23 | } 24 | ], 25 | plugins: [typescript({ target: 'esnext', module: 'esnext', declaration: false })] 26 | }, 27 | { 28 | input: 'src/index.ts', 29 | output: [ 30 | // UMD, transpiled to ES5 31 | { 32 | file: 'dist/index.js', 33 | format: 'esm', 34 | plugins: [ 35 | getBabelOutputPlugin({ 36 | moduleId: '@post-me/mpi', 37 | presets: [['@babel/preset-env', { modules: 'umd' }]], 38 | }) 39 | ] 40 | } 41 | ], 42 | plugins: [nodeResolve(), typescript({ target: 'esnext', module: 'esnext', declaration: false })] 43 | } 44 | ] 45 | -------------------------------------------------------------------------------- /packages/mpi/src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @packageDocumentation MPI-like concurrency for a pool of web workers 3 | */ 4 | 5 | import { createPool, WorkerPool } from './parent'; 6 | import { 7 | joinPool, 8 | PoolConnection, 9 | ParallelMethod, 10 | ParallelMethods, 11 | } from './worker'; 12 | import { Communicator } from './mpi'; 13 | 14 | export { 15 | // Methods 16 | createPool, 17 | joinPool, 18 | // Interfaces 19 | WorkerPool, 20 | PoolConnection, 21 | Communicator, 22 | // Type Helpers 23 | ParallelMethod, 24 | ParallelMethods, 25 | }; 26 | -------------------------------------------------------------------------------- /packages/mpi/src/mpi.ts: -------------------------------------------------------------------------------- 1 | export interface Communicator { 2 | rank(): number; 3 | size(): number; 4 | send( 5 | data: T, 6 | destination: number, 7 | tag: number, 8 | transfer?: Transferable[] 9 | ): Promise; 10 | recv(source: number, tag: number): Promise; 11 | bcast(data: T | null, root: number): Promise; 12 | barrier(): Promise; 13 | scatter(data: T | null, root: number): Promise; 14 | gather(data: T, root: number): Promise; 15 | allGather(data: T): Promise; 16 | reduce( 17 | data: T, 18 | reducer: (a: T, b: T) => T, 19 | root: number 20 | ): Promise; 21 | allReduce(data: T, reducer: (a: T, b: T) => T): Promise; 22 | } 23 | 24 | export type ArrayLike = 25 | | Array 26 | | Uint8Array 27 | | Uint16Array 28 | | Uint32Array 29 | | BigUint64Array 30 | | Int8Array 31 | | Int16Array 32 | | Int32Array 33 | | BigInt64Array 34 | | Float32Array 35 | | Float64Array 36 | | Uint8ClampedArray; 37 | 38 | export type Send = Communicator['send']; 39 | export type Recv = Communicator['recv']; 40 | export type Bcast = Communicator['bcast']; 41 | export type Barrier = Communicator['barrier']; 42 | export type Scatter = Communicator['scatter']; 43 | export type Gather = Communicator['gather']; 44 | export type AllGather = Communicator['allGather']; 45 | export type Reduce = Communicator['reduce']; 46 | export type AllReduce = Communicator['allReduce']; 47 | 48 | type CollectiveOpBuilder Promise> = ( 49 | rank: number, 50 | size: number, 51 | send: Send, 52 | recv: Recv 53 | ) => Op; 54 | 55 | const BARRIER_TAG = -1; 56 | const BCAST_TAG = -2; 57 | const SCATTER_TAG = -3; 58 | const GATHER_TAG = -4; 59 | const REDUCE_TAG = -5; 60 | 61 | export const buildBarrier: CollectiveOpBuilder = function ( 62 | rank, 63 | size, 64 | send, 65 | recv 66 | ) { 67 | return async function () { 68 | const destination = (rank + 1) % size; 69 | const source = (size + rank - 1) % size; 70 | const tag = BARRIER_TAG; 71 | let rounds = 2; 72 | 73 | while (rounds > 0) { 74 | if (rank === 0) { 75 | await send(true, destination, tag); 76 | } 77 | 78 | await recv(source, tag); 79 | 80 | if (rank !== 0) { 81 | await send(true, destination, tag); 82 | } 83 | 84 | rounds -= 1; 85 | } 86 | }; 87 | }; 88 | 89 | export const buildBcast: CollectiveOpBuilder = function ( 90 | rank, 91 | size, 92 | send, 93 | recv 94 | ) { 95 | return async function (data: any, root: number) { 96 | const tag = BCAST_TAG; 97 | 98 | // O(logN) broadcast implementation 99 | const delta = (rank + size - root) % size; 100 | let stride = 1; 101 | 102 | while (stride < size) { 103 | if (delta < stride && delta + stride < size) { 104 | const destination = (rank + stride) % size; 105 | send(data, destination, tag); 106 | } else if (delta >= stride && delta - stride < stride) { 107 | const source = (rank + size - stride) % size; 108 | data = await recv(source, tag); 109 | } 110 | 111 | stride = stride * 2; 112 | } 113 | 114 | return data; 115 | }; 116 | }; 117 | 118 | export const buildScatter: CollectiveOpBuilder = function ( 119 | rank, 120 | size, 121 | send, 122 | recv 123 | ) { 124 | return async function ( 125 | data: T | null, 126 | root: number 127 | ) { 128 | const tag = SCATTER_TAG; 129 | 130 | // O(N) scatter implementation can probably do better 131 | if (rank === root) { 132 | const fullSize = data!.length; 133 | const subSize = Math.max(Math.floor(fullSize / size), 1); 134 | const remainder = Math.max(fullSize - subSize * size, 0); 135 | for (let destination = 0; destination < size; ++destination) { 136 | const extraStart = destination < remainder ? destination : remainder; 137 | const extraStop = destination < remainder ? 1 : 0; 138 | const start = destination * subSize + extraStart; 139 | const stop = start + subSize + extraStop; 140 | const subData = data!.slice(start, stop); 141 | let transfer: Transferable[] | undefined; 142 | if ((subData as any).buffer) { 143 | transfer = [(subData as any).buffer]; 144 | } 145 | send(subData, destination, tag, transfer); 146 | } 147 | } 148 | 149 | const scatterData = await recv(root, tag); 150 | 151 | return scatterData; 152 | }; 153 | }; 154 | 155 | export const buildGather: CollectiveOpBuilder = function ( 156 | rank, 157 | size, 158 | send, 159 | recv 160 | ) { 161 | return async function ( 162 | data: T, 163 | root: number 164 | ) { 165 | const tag = GATHER_TAG; 166 | 167 | // O(N) scatter implementation can probably do better 168 | 169 | let transfer: Transferable[] | undefined; 170 | if ((data as any).buffer) { 171 | transfer = [(data as any).buffer]; 172 | } 173 | send(data, root, tag, transfer); 174 | 175 | let gatheredData: T | null = null; 176 | 177 | if (rank === root) { 178 | for (let source = 0; source < size; ++source) { 179 | let subData: T = await recv(source, tag); 180 | if (source === 0) { 181 | gatheredData = subData; 182 | } else { 183 | const C: any = gatheredData!.constructor; 184 | gatheredData = C.of(...gatheredData!, ...subData); 185 | } 186 | } 187 | } 188 | 189 | return gatheredData; 190 | }; 191 | }; 192 | 193 | export const buildAllGather: CollectiveOpBuilder = function ( 194 | rank, 195 | size, 196 | send, 197 | recv 198 | ) { 199 | return async function (data: T) { 200 | const gather = buildGather(rank, size, send, recv); 201 | const bcast = buildBcast(rank, size, send, recv); 202 | 203 | const root = 0; 204 | 205 | const gatheredData = await gather(data, root); 206 | return await bcast(gatheredData, root); 207 | }; 208 | }; 209 | 210 | export const buildReduce: CollectiveOpBuilder = function ( 211 | rank, 212 | size, 213 | send, 214 | recv 215 | ) { 216 | return async function reduce( 217 | data: T, 218 | reducer: (a: T, b: T) => T, 219 | root: number 220 | ): Promise { 221 | const tag = REDUCE_TAG; 222 | 223 | let result = data; 224 | 225 | // O(logN) reduce implementation 226 | const delta = (rank + size - root) % size; 227 | let stride = 1; 228 | 229 | while (stride <= Math.floor(size / 2)) { 230 | if (delta % stride !== 0) { 231 | break; 232 | } 233 | 234 | const currSize = Math.floor(size / stride); 235 | 236 | // If there is an unpaired process at this iteration, reduce with root 237 | if (currSize % 2 !== 0) { 238 | const unpaired = (root + (currSize - 1) * stride) % size; 239 | if (rank === unpaired) { 240 | send(result, root, tag); 241 | break; 242 | } else if (rank === root) { 243 | const otherResult = await recv(unpaired, tag); 244 | result = reducer(result, otherResult); 245 | } 246 | } 247 | 248 | if (delta % (stride * 2) === 0) { 249 | const source = (rank + stride) % size; 250 | const otherResult = await recv(source, tag); 251 | result = reducer(result, otherResult); 252 | } else { 253 | const destination = (rank + size - stride) % size; 254 | send(result, destination, tag); 255 | break; 256 | } 257 | 258 | stride = stride * 2; 259 | } 260 | 261 | if (rank === root) { 262 | return result; 263 | } else { 264 | return null; 265 | } 266 | }; 267 | }; 268 | 269 | export const buildAllReduce: CollectiveOpBuilder = function ( 270 | rank, 271 | size, 272 | send, 273 | recv 274 | ) { 275 | return async function ( 276 | data: T, 277 | reducer: (a: T, b: T) => T 278 | ): Promise { 279 | const reduce = buildReduce(rank, size, send, recv); 280 | const bcast = buildBcast(rank, size, send, recv); 281 | 282 | const root = 0; 283 | 284 | const reducedData = await reduce(data, reducer, root); 285 | return await bcast(reducedData, root); 286 | }; 287 | }; 288 | -------------------------------------------------------------------------------- /packages/mpi/src/parent.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ParentHandshake, 3 | RemoteHandle, 4 | WorkerMessenger, 5 | Connection, 6 | MethodsType, 7 | InnerType, 8 | } from 'post-me'; 9 | import { InitMethods } from './worker'; 10 | 11 | export interface WorkerPool { 12 | call( 13 | methodName: K, 14 | args: (rank: number) => Parameters, 15 | transfer?: (rank: number, args: Parameters) => Transferable[] 16 | ): Promise>[]>; 17 | } 18 | 19 | function createUniqueIdFn() { 20 | let __id = 0; 21 | return function () { 22 | const id = __id; 23 | __id += 1; 24 | return id; 25 | }; 26 | } 27 | 28 | export function createPool( 29 | workers: Worker[], 30 | ChannelConstructor?: any 31 | ): Promise { 32 | ChannelConstructor = ChannelConstructor || MessageChannel; 33 | return new Promise((resolve, reject) => { 34 | const size = workers.length; 35 | 36 | // Create channels for direct inter-worker communication 37 | const channels: { [_: string]: MessageChannel } = {}; 38 | for (let i = 0; i < size; ++i) { 39 | for (let j = i; j < size; ++j) { 40 | const key = channelKey(i, j); 41 | channels[key] = new ChannelConstructor(); 42 | } 43 | } 44 | 45 | const connectTasks: Promise[] = workers.map((worker, rank) => { 46 | let messenger = new WorkerMessenger({ worker }); 47 | return ParentHandshake(messenger).then((connection) => { 48 | return connection; 49 | }); 50 | }); 51 | 52 | Promise.all(connectTasks) 53 | .then((connections) => { 54 | const initializeTasks = connections.map((connection, rank) => { 55 | const remoteHandle: RemoteHandle = connection.remoteHandle(); 56 | 57 | // Ensure MessagePorts to create direct Worker-to-Workern communication are transferred. 58 | remoteHandle.setCallTransfer( 59 | 'initComm', 60 | (_rank, ports) => 61 | ports.filter((port) => port !== undefined) as Transferable[] 62 | ); 63 | 64 | const ports: (MessagePort | undefined)[] = []; 65 | 66 | for (let otherRank = 0; otherRank < size; ++otherRank) { 67 | if (otherRank === rank) { 68 | ports.push(undefined); 69 | } else { 70 | const key = channelKey(rank, otherRank); 71 | const channel = channels[key]; 72 | let port = rank < otherRank ? channel.port1 : channel.port2; 73 | ports.push(port); 74 | } 75 | } 76 | 77 | return remoteHandle.call('initComm', rank, ports); 78 | }); 79 | 80 | Promise.all(initializeTasks) 81 | .then(() => { 82 | const workerPool = new ConcreteWorkerPool(connections); 83 | resolve(workerPool); 84 | }) 85 | .catch(reject); 86 | }) 87 | .catch(reject); 88 | }); 89 | } 90 | 91 | class ConcreteWorkerPool implements WorkerPool { 92 | private _connections: Connection[]; 93 | private _taskId: () => number; 94 | 95 | constructor(connections: Connection[]) { 96 | this._connections = connections; 97 | this._taskId = createUniqueIdFn(); 98 | } 99 | 100 | call( 101 | methodName: K, 102 | argsFn: (rank: number) => Parameters, 103 | transferFn?: (rank: number, args: Parameters) => Transferable[] 104 | ): Promise>[]> { 105 | const taskId = this._taskId(); 106 | return Promise.all( 107 | this._connections.map((connection, rank) => { 108 | const remoteHandle = connection.remoteHandle(); 109 | const args = argsFn(rank); 110 | const options = transferFn 111 | ? { transfer: transferFn(rank, args) } 112 | : undefined; 113 | return remoteHandle.customCall(methodName, [taskId, ...args], options); 114 | }) 115 | ); 116 | } 117 | } 118 | 119 | function channelKey(_i: number, _j: number): string { 120 | let i = _i; 121 | let j = _j; 122 | if (i > j) { 123 | i = _j; 124 | j = _i; 125 | } 126 | 127 | return `${i},${j}`; 128 | } 129 | -------------------------------------------------------------------------------- /packages/mpi/src/worker.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ParentHandshake, 3 | ChildHandshake, 4 | WorkerMessenger, 5 | PortMessenger, 6 | Connection, 7 | RemoteHandle, 8 | LocalHandle, 9 | MethodsType, 10 | Callable, 11 | ValueOrPromise, 12 | InnerType, 13 | } from 'post-me'; 14 | import { 15 | Communicator, 16 | Send, 17 | Barrier, 18 | Bcast, 19 | Scatter, 20 | Gather, 21 | AllGather, 22 | Reduce, 23 | AllReduce, 24 | buildBarrier, 25 | buildBcast, 26 | buildScatter, 27 | buildGather, 28 | buildAllGather, 29 | buildReduce, 30 | buildAllReduce, 31 | } from './mpi'; 32 | 33 | export type InitMethods = { 34 | initComm(rank: number, ports: (MessagePort | undefined)[]): Promise; 35 | }; 36 | 37 | export type ParallelMethod> = ( 38 | comm: Communicator 39 | ) => Callable, ValueOrPromise>>>; 40 | 41 | export type ParallelMethods = { 42 | [K in keyof M]: ParallelMethod; 43 | }; 44 | 45 | export interface PoolConnection { 46 | registerMethods(methods: ParallelMethods): void; 47 | registerMethod( 48 | methodName: K, 49 | method: ParallelMethod 50 | ): void; 51 | setReturnTransfer( 52 | methodName: K, 53 | transfer: (result: InnerType>) => Transferable[] 54 | ): void; 55 | } 56 | 57 | export function joinPool(workerScope: any): Promise { 58 | return new Promise((resolve, reject) => { 59 | let messenger = new WorkerMessenger({ worker: workerScope }); 60 | ChildHandshake(messenger) 61 | .then((connection) => { 62 | const parentConnection = connection; 63 | 64 | const initMethods: InitMethods = { 65 | initComm(rank, ports) { 66 | return new Promise((thisResolve, thisReject) => { 67 | const handshakes = ports.map((port, otherRank) => { 68 | if (port === undefined) { 69 | return Promise.resolve(undefined); 70 | } 71 | let messenger = new PortMessenger({ port }); 72 | const Handshake = 73 | otherRank < rank ? ChildHandshake : ParentHandshake; 74 | return Handshake(messenger); 75 | }); 76 | 77 | Promise.all(handshakes) 78 | .then((connections) => { 79 | const poolConnection = new ConcretePoolConnection( 80 | parentConnection, 81 | rank, 82 | connections 83 | ); 84 | resolve(poolConnection); 85 | thisResolve(); 86 | }) 87 | .catch((err) => { 88 | reject(err); 89 | thisReject(err); 90 | }); 91 | }); 92 | }, 93 | }; 94 | 95 | parentConnection.localHandle().setMethods(initMethods); 96 | }) 97 | .catch(reject); 98 | }); 99 | } 100 | 101 | type IntraPoolMethods = { 102 | send(taskId: number, source: number, tag: number, data: any): Promise; 103 | }; 104 | 105 | class ConcretePoolConnection 106 | implements PoolConnection { 107 | private _size: number; 108 | private _rank: number; 109 | private _connections: (Connection | undefined)[]; 110 | private _communicators: { [taskId: number]: ConcreteCommunicator }; 111 | private _parentConnection: Connection; 112 | 113 | constructor( 114 | parentConnection: Connection, 115 | rank: number, 116 | connections: (Connection | undefined)[] 117 | ) { 118 | this._rank = rank; 119 | this._size = connections.length; 120 | this._connections = connections; 121 | this._parentConnection = parentConnection; 122 | this._communicators = {}; 123 | 124 | this._connections.forEach((connection) => { 125 | if (connection !== undefined) { 126 | const localHandle: LocalHandle = connection.localHandle(); 127 | localHandle.setMethod('send', this._handleSend.bind(this)); 128 | } 129 | }); 130 | } 131 | 132 | registerMethods(methods: ParallelMethods) { 133 | const exposedMethods = Object.entries(methods).reduce( 134 | (tot, [methodName, method]: [string, ParallelMethod]) => { 135 | tot[methodName] = this._exposeParallelMethod(method); 136 | return tot; 137 | }, 138 | {} as any 139 | ); 140 | 141 | this._parentConnection.localHandle().setMethods(exposedMethods); 142 | } 143 | 144 | registerMethod( 145 | methodName: K, 146 | method: ParallelMethod 147 | ) { 148 | this._parentConnection 149 | .localHandle() 150 | .setMethod(methodName, this._exposeParallelMethod(method)); 151 | } 152 | 153 | setReturnTransfer( 154 | methodName: K, 155 | transfer: (result: InnerType>) => Transferable[] 156 | ) { 157 | this._parentConnection 158 | .localHandle() 159 | .setReturnTransfer(methodName, transfer); 160 | } 161 | 162 | private _exposeParallelMethod>( 163 | method: ParallelMethod 164 | ) { 165 | return (taskId: number, ...args: Parameters) => { 166 | const send: Send = (data, destination, tag, transfer) => { 167 | return this._sendToChannel( 168 | taskId, 169 | this._rank, 170 | destination, 171 | tag, 172 | data, 173 | transfer 174 | ); 175 | }; 176 | 177 | const communicator = new ConcreteCommunicator( 178 | this._rank, 179 | this._size, 180 | send 181 | ); 182 | 183 | this._communicators[taskId] = communicator; 184 | 185 | return new Promise((resolve, reject) => { 186 | setTimeout(() => { 187 | Promise.resolve(method(communicator)(...args)) 188 | .then(resolve) 189 | .catch(reject); 190 | }, 0); 191 | }); 192 | }; 193 | } 194 | 195 | private _sendToChannel( 196 | taskId: number, 197 | source: number, 198 | destination: number, 199 | tag: number, 200 | data: any, 201 | transfer?: Transferable[] 202 | ) { 203 | const connection = this._connections[destination]; 204 | 205 | if (connection === undefined) { 206 | return Promise.reject(new Error('The destination is out of range')); 207 | } 208 | 209 | const remoteHandle: RemoteHandle = connection.remoteHandle(); 210 | return remoteHandle.customCall('send', [taskId, source, tag, data], { 211 | transfer, 212 | }); 213 | } 214 | 215 | private _handleSend( 216 | taskId: number, 217 | source: number, 218 | tag: number, 219 | data: any 220 | ): Promise { 221 | const communicator = this._communicators[taskId]; 222 | if (communicator === undefined) { 223 | return Promise.reject('A communicator does not exist for this task.'); 224 | } 225 | 226 | return communicator._handleSend(source, tag, data); 227 | } 228 | } 229 | 230 | function messageKey(source: number, tag: number): string { 231 | return `${source},${tag}`; 232 | } 233 | 234 | interface QueueItem { 235 | resolve(data?: any): void; 236 | reject(error?: any): void; 237 | } 238 | 239 | interface MessageQueueItem extends QueueItem { 240 | data: any; 241 | } 242 | 243 | class ConcreteCommunicator implements Communicator { 244 | private _size: number; 245 | private _rank: number; 246 | private _messageQueue: { [key: string]: MessageQueueItem[] }; 247 | private _receiveQueue: { [key: string]: QueueItem[] }; 248 | private _sendFn: Send; 249 | bcast: Bcast; 250 | barrier: Barrier; 251 | scatter: Scatter; 252 | gather: Gather; 253 | allGather: AllGather; 254 | reduce: Reduce; 255 | allReduce: AllReduce; 256 | 257 | constructor(rank: number, size: number, sendFn: Send) { 258 | this._rank = rank; 259 | this._size = size; 260 | this._messageQueue = {}; 261 | this._receiveQueue = {}; 262 | this._sendFn = sendFn; 263 | 264 | const send = this.send.bind(this); 265 | const recv = this.recv.bind(this); 266 | this.bcast = buildBcast(rank, size, send, recv); 267 | this.barrier = buildBarrier(rank, size, send, recv); 268 | this.scatter = buildScatter(rank, size, send, recv); 269 | this.gather = buildGather(rank, size, send, recv); 270 | this.allGather = buildAllGather(rank, size, send, recv); 271 | this.reduce = buildReduce(rank, size, send, recv); 272 | this.allReduce = buildAllReduce(rank, size, send, recv); 273 | } 274 | 275 | rank() { 276 | return this._rank; 277 | } 278 | 279 | size() { 280 | return this._size; 281 | } 282 | 283 | send( 284 | data: any, 285 | destination: number, 286 | tag: number, 287 | transfer?: Transferable[] 288 | ): Promise { 289 | if (destination === this.rank()) { 290 | this._handleSend(this.rank(), tag, data); 291 | // Resolve to avoid deadlock 292 | return Promise.resolve(); 293 | } 294 | 295 | return this._sendFn(data, destination, tag, transfer); 296 | } 297 | 298 | recv(source: number, tag: number): Promise { 299 | return new Promise((resolve, reject) => { 300 | const key = messageKey(source, tag); 301 | if (this._messageQueue[key] && this._messageQueue[key].length > 0) { 302 | const [sender] = this._messageQueue[key].splice(0, 1); 303 | sender.resolve(); 304 | resolve(sender.data); 305 | } else { 306 | if (this._receiveQueue[key] === undefined) { 307 | this._receiveQueue[key] = []; 308 | } 309 | 310 | this._receiveQueue[key].push({ resolve, reject }); 311 | } 312 | }); 313 | } 314 | 315 | _handleSend(source: number, tag: number, data: any): Promise { 316 | return new Promise((resolve, reject) => { 317 | const key = messageKey(source, tag); 318 | 319 | if (this._receiveQueue[key] && this._receiveQueue[key].length > 0) { 320 | const [receiver] = this._receiveQueue[key].splice(0, 1); 321 | resolve(); 322 | receiver.resolve(data); 323 | } else { 324 | if (this._messageQueue[key] === undefined) { 325 | this._messageQueue[key] = []; 326 | } 327 | 328 | this._messageQueue[key].push({ resolve, reject, data }); 329 | } 330 | }); 331 | } 332 | } 333 | -------------------------------------------------------------------------------- /packages/mpi/tests/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { ConcreteEmitter, MethodsType } from 'post-me'; 2 | import { 3 | createPool, 4 | joinPool, 5 | ParallelMethods, 6 | WorkerPool, 7 | ParallelMethod, 8 | } from '../src'; 9 | 10 | function MockWorker(script: (self: any) => void) { 11 | const worker: any = new ConcreteEmitter(); 12 | const self: any = new ConcreteEmitter(); 13 | 14 | self.postMessage = (payload: any) => { 15 | worker.emit('message', { data: payload }); 16 | }; 17 | 18 | worker.postMessage = (payload: any) => { 19 | self.emit('message', { data: payload }); 20 | }; 21 | 22 | script(self); 23 | 24 | return worker; 25 | } 26 | 27 | class MockChannel { 28 | public port1: any; 29 | public port2: any; 30 | constructor() { 31 | const port1: any = new ConcreteEmitter(); 32 | port1.start = () => {}; 33 | const port2: any = new ConcreteEmitter(); 34 | port2.start = () => {}; 35 | 36 | port1.postMessage = (payload: any) => { 37 | port2.emit('message', { data: payload }); 38 | }; 39 | 40 | port2.postMessage = (payload: any) => { 41 | port1.emit('message', { data: payload }); 42 | }; 43 | 44 | this.port1 = port1; 45 | this.port2 = port2; 46 | } 47 | } 48 | 49 | function makeHandshake( 50 | n: number, 51 | methods: ParallelMethods 52 | ): Promise> { 53 | const workerScript = (self: any) => { 54 | joinPool(self).then((connection) => { 55 | connection.registerMethods(methods); 56 | }); 57 | }; 58 | 59 | const workers: Worker[] = []; 60 | for (let i = 0; i < N_WORKERS; ++i) { 61 | const worker = MockWorker(workerScript); 62 | workers.push(worker); 63 | } 64 | 65 | return createPool(workers, MockChannel); 66 | } 67 | 68 | const N_WORKERS = 7; 69 | 70 | test('create pool', () => { 71 | return new Promise((resolve) => { 72 | makeHandshake(N_WORKERS, {}).then((pool) => { 73 | resolve(); 74 | }); 75 | }); 76 | }); 77 | 78 | test('send', () => { 79 | return new Promise((resolve) => { 80 | const send: ParallelMethod<(data: number) => number> = ( 81 | communicator 82 | ) => async (data) => { 83 | const rank = communicator.rank(); 84 | const size = communicator.size(); 85 | const destination = (rank + 1) % size; 86 | const source = (rank + size - 1) % size; 87 | communicator.send(data, destination, 0); 88 | data = await communicator.recv(source, 0); 89 | return data; 90 | }; 91 | makeHandshake(N_WORKERS, { send }).then((pool) => { 92 | pool 93 | .call('send', (rank) => [rank]) 94 | .then((result) => { 95 | for (let i = 0; i < N_WORKERS; ++i) { 96 | expect(result[i]).toEqual((i + N_WORKERS - 1) % N_WORKERS); 97 | } 98 | resolve(); 99 | }); 100 | }); 101 | }); 102 | }); 103 | 104 | test('send', async () => { 105 | const send: ParallelMethod<(data: number) => number> = ( 106 | communicator 107 | ) => async (data) => { 108 | const rank = communicator.rank(); 109 | const size = communicator.size(); 110 | const destination = (rank + 1) % size; 111 | const source = (rank + size - 1) % size; 112 | communicator.send(data, destination, 0); 113 | data = await communicator.recv(source, 0); 114 | return data; 115 | }; 116 | 117 | const pool = await makeHandshake(N_WORKERS, { send }); 118 | const result = await pool.call('send', (rank) => [rank]); 119 | result.forEach((value, i) => { 120 | expect(value).toEqual((i + N_WORKERS - 1) % N_WORKERS); 121 | }); 122 | }); 123 | 124 | test('bcast', async () => { 125 | const bcast: ParallelMethod<(data: number | null, root: number) => number> = ( 126 | communicator 127 | ) => async (data, root) => { 128 | return await communicator.bcast(data, root); 129 | }; 130 | 131 | const ROOT = N_WORKERS - 1; 132 | const pool = await makeHandshake(N_WORKERS, { bcast }); 133 | const result = await pool.call('bcast', (rank) => [ 134 | rank === ROOT ? rank : null, 135 | ROOT, 136 | ]); 137 | result.forEach((value) => { 138 | expect(value).toEqual(ROOT); 139 | }); 140 | }); 141 | 142 | function makeSeq(start: number, size: number): Uint16Array { 143 | const array = new Uint16Array(size); 144 | for (let i = 0; i < size; ++i) { 145 | array[i] = i + start; 146 | } 147 | 148 | return array; 149 | } 150 | 151 | test('scatter', async () => { 152 | const scatter: ParallelMethod< 153 | (data: Uint16Array | null, root: number) => Uint16Array 154 | > = (communicator) => async (data, root) => { 155 | return await communicator.scatter(data, root); 156 | }; 157 | 158 | const ROOT = N_WORKERS - 1; 159 | const input = makeSeq(0, N_WORKERS * 4 + 3); 160 | const pool = await makeHandshake(N_WORKERS, { scatter }); 161 | const result = await pool.call('scatter', (rank) => [ 162 | rank === ROOT ? input : null, 163 | ROOT, 164 | ]); 165 | const N = Math.floor(input.length / N_WORKERS); 166 | let output = new Uint16Array(0); 167 | result.forEach((value) => { 168 | expect(value.length).toBeGreaterThanOrEqual(N); 169 | expect(value.length).toBeLessThanOrEqual(N + 1); 170 | output = Uint16Array.of(...output, ...value); 171 | }); 172 | 173 | expect(output).toEqual(input); 174 | }); 175 | 176 | test('gather', async () => { 177 | const gather: ParallelMethod< 178 | (data: Uint16Array, root: number) => Uint16Array | null 179 | > = (communicator) => async (data, root) => { 180 | return await communicator.gather(data, root); 181 | }; 182 | 183 | const allGather: ParallelMethod<(data: Uint16Array) => Uint16Array> = ( 184 | communicator 185 | ) => async (data) => { 186 | return await communicator.allGather(data); 187 | }; 188 | 189 | const ROOT = N_WORKERS - 1; 190 | const SIZE = 4; 191 | const pool = await makeHandshake(N_WORKERS, { gather, allGather }); 192 | 193 | { 194 | const result = await pool.call('gather', (rank) => [ 195 | makeSeq(rank * SIZE, SIZE), 196 | ROOT, 197 | ]); 198 | result.forEach((value, i) => { 199 | if (i === ROOT) { 200 | expect(value).toEqual(makeSeq(0, SIZE * N_WORKERS)); 201 | } else { 202 | expect(value).toEqual(null); 203 | } 204 | }); 205 | } 206 | 207 | { 208 | const result = await pool.call('allGather', (rank) => [ 209 | makeSeq(rank * SIZE, SIZE), 210 | ]); 211 | result.forEach((value, i) => { 212 | expect(value).toEqual(makeSeq(0, SIZE * N_WORKERS)); 213 | }); 214 | } 215 | }); 216 | 217 | test('reduce', async () => { 218 | const reduce: ParallelMethod< 219 | (data: number, root: number) => number | null 220 | > = (communicator) => async (data, root) => { 221 | return await communicator.reduce(data, (a, b) => a + b, root); 222 | }; 223 | 224 | const allReduce: ParallelMethod<(data: number) => number> = ( 225 | communicator 226 | ) => async (data) => { 227 | return await communicator.allReduce(data, (a, b) => a + b); 228 | }; 229 | 230 | const ROOT = 0; 231 | const pool = await makeHandshake(N_WORKERS, { reduce, allReduce }); 232 | 233 | { 234 | const result = await pool.call('reduce', (rank) => [rank, ROOT]); 235 | result.forEach((value, i) => { 236 | if (i === ROOT) { 237 | expect(value).toEqual(((N_WORKERS - 1) * N_WORKERS) / 2); 238 | } else { 239 | expect(value).toEqual(null); 240 | } 241 | }); 242 | } 243 | 244 | { 245 | const result = await pool.call('allReduce', (rank) => [rank]); 246 | result.forEach((value, i) => { 247 | expect(value).toEqual(((N_WORKERS - 1) * N_WORKERS) / 2); 248 | }); 249 | } 250 | }); 251 | 252 | function sleep(time: number) { 253 | return new Promise((resolve) => { 254 | setTimeout(resolve, time); 255 | }); 256 | } 257 | 258 | test('barrier', async () => { 259 | const barrier: ParallelMethod<(t: number) => number> = ( 260 | communicator 261 | ) => async (t) => { 262 | const t0: any = new Date(); 263 | await sleep(t); 264 | await communicator.barrier(); 265 | const t1: any = new Date(); 266 | return t1 - t0; 267 | }; 268 | 269 | const MAX_SLEEP = 500; 270 | const pool = await makeHandshake(N_WORKERS, { barrier }); 271 | const result = await pool.call('barrier', (rank) => [ 272 | (MAX_SLEEP * rank) / (N_WORKERS - 1), 273 | ]); 274 | const eps = 2; 275 | result.forEach((value) => { 276 | expect(Math.abs(value - MAX_SLEEP)).toBeLessThanOrEqual(eps); 277 | }); 278 | }); 279 | -------------------------------------------------------------------------------- /packages/mpi/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "esnext" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */, 5 | "module": "esnext" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, 6 | "lib": [ 7 | "esnext", 8 | "dom" 9 | // "webworker" 10 | ] /* Specify library files to be included in the compilation. */, 11 | "strict": true /* Enable all strict type-checking options. */, 12 | "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */, 13 | "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */, 14 | "rootDir": "./src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */, 15 | "outDir": "./dist" /* Redirect output structure to the directory. */, 16 | "declaration": true /* Generates corresponding '.d.ts' file. */, 17 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 18 | // "allowJs": true, /* Allow javascript files to be compiled. */ 19 | // "checkJs": true, /* Report errors in .js files. */ 20 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 21 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 22 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 23 | // "outFile": "./", /* Concatenate and emit output to single file. */ 24 | // "composite": true, /* Enable project compilation */ 25 | // "incremental": true, /* Enable incremental compilation */ 26 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 27 | // "removeComments": true, /* Do not emit comments to output. */ 28 | // "noEmit": true, /* Do not emit outputs. */ 29 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 30 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 31 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 32 | /* Strict Type-Checking Options */ 33 | // "strictNullChecks": true, /* Enable strict null checks. */ 34 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 35 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 36 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 37 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 38 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 39 | /* Additional Checks */ 40 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 41 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 42 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 43 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 44 | /* Module Resolution Options */ 45 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 46 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 47 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 48 | // "typeRoots": [], /* List of folders to include type definitions from. */ 49 | // "types": [], /* Type declaration files to be included in compilation. */ 50 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 51 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 52 | /* Source Map Options */ 53 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 54 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 55 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 56 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 57 | /* Experimental Options */ 58 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 59 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 60 | }, 61 | "include": ["src", "tests"], 62 | "exclude": [ 63 | // "dist", 64 | "node_modules", 65 | "**/*.spec.ts" 66 | ] 67 | } 68 | --------------------------------------------------------------------------------