├── .eslintignore ├── .eslintrc.js ├── .flowconfig ├── .github ├── CODEOWNERS └── workflows │ ├── main.yaml │ └── publish.yml ├── .gitignore ├── .husky ├── .gitignore ├── commit-msg └── pre-commit ├── .npmrc ├── .prettierignore ├── .prettierrc.json ├── AUTHORS ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── babel.config.js ├── commitlint.config.js ├── demo ├── bridge.htm ├── child.htm └── parent.htm ├── dist ├── module │ ├── bridge │ │ ├── bridge.js │ │ ├── child.js │ │ ├── common.js │ │ ├── index.js │ │ ├── parent.js │ │ └── setup.js │ ├── clean.js │ ├── conf │ │ ├── config.js │ │ ├── constants.js │ │ └── index.js │ ├── declarations.js │ ├── drivers │ │ ├── index.js │ │ ├── listeners.js │ │ ├── receive │ │ │ ├── index.js │ │ │ └── types.js │ │ ├── send │ │ │ ├── index.js │ │ │ └── strategies.js │ │ └── types.js │ ├── global.js │ ├── index.js │ ├── lib │ │ ├── compat.js │ │ ├── hello.js │ │ ├── index.js │ │ └── windows.js │ ├── public │ │ ├── index.js │ │ ├── on.js │ │ └── send.js │ ├── serialize │ │ ├── function.js │ │ ├── index.js │ │ ├── promise.js │ │ ├── serialize.js │ │ └── window.js │ ├── setup.js │ └── types.js ├── post-robot.ie.js ├── post-robot.ie.min.js ├── post-robot.ie.min.js.map ├── post-robot.js ├── post-robot.min.js └── post-robot.min.js.map ├── globals.js ├── index.js ├── karma.conf.js ├── package.json ├── src ├── bridge │ ├── bridge.js │ ├── child.js │ ├── common.js │ ├── index.js │ ├── parent.js │ └── setup.js ├── clean.js ├── conf │ ├── config.js │ ├── constants.js │ └── index.js ├── declarations.js ├── drivers │ ├── index.js │ ├── listeners.js │ ├── receive │ │ ├── index.js │ │ └── types.js │ ├── send │ │ ├── index.js │ │ └── strategies.js │ └── types.js ├── global.js ├── index.js ├── lib │ ├── compat.js │ ├── hello.js │ ├── index.js │ └── windows.js ├── public │ ├── index.js │ ├── on.js │ └── send.js ├── serialize │ ├── function.js │ ├── index.js │ ├── promise.js │ ├── serialize.js │ └── window.js ├── setup.js └── types.js ├── test ├── bridge.htm ├── child.htm ├── child.js ├── common.js ├── index.js └── tests │ ├── error.js │ ├── happy.js │ ├── index.js │ ├── options.js │ ├── popup.js │ ├── serialization.js │ └── window-proxy.js └── webpack.config.js /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | module.exports = { 4 | extends: "@krakenjs/eslint-config-grumbler/eslintrc-browser", 5 | 6 | globals: { 7 | __POST_ROBOT__: true, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/node_modules/babel-plugin-flow-runtime 3 | .*/node_modules/flow-runtime 4 | .*/node_modules/npm 5 | .*/node_modules/**/test 6 | .*/node_modules/jsonlint 7 | .*/**/dist/module 8 | .*/node_modules/resolve 9 | [include] 10 | [libs] 11 | flow-typed 12 | src/declarations.js 13 | [options] 14 | module.name_mapper='^src\(.*\)$' -> '/src/\1' 15 | experimental.const_params=false 16 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Owner for everything in the repo 2 | * @krakenjs/checkout-sdk 3 | -------------------------------------------------------------------------------- /.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: {} 7 | jobs: 8 | main: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: ⬇️ Checkout repo 12 | uses: actions/checkout@v2 13 | with: 14 | fetch-depth: 0 15 | 16 | - name: ⎔ Setup node 17 | uses: actions/setup-node@v2 18 | with: 19 | node-version: "16" 20 | registry-url: "https://registry.npmjs.org" 21 | 22 | - name: 📥 Download deps 23 | uses: bahmutov/npm-install@v1 24 | with: 25 | useLockFile: false 26 | 27 | - name: 👕 Lint commit messages 28 | uses: wagoid/commitlint-github-action@v4 29 | 30 | - name: ▶️ Run flow-typed script 31 | run: npm run flow-typed 32 | 33 | - name: ▶️ Run build script 34 | run: npm run build 35 | 36 | - name: ⬆️ Upload karma coverage report 37 | uses: codecov/codecov-action@v2 38 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: publish to npm 2 | on: workflow_dispatch 3 | jobs: 4 | main: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - name: ⬇️ Checkout repo 8 | uses: actions/checkout@v2 9 | with: 10 | fetch-depth: 0 11 | 12 | - name: ⎔ Setup node 13 | # sets up the .npmrc file to publish to npm 14 | uses: actions/setup-node@v2 15 | with: 16 | node-version: "16" 17 | registry-url: "https://registry.npmjs.org" 18 | 19 | - name: 📥 Download deps 20 | uses: bahmutov/npm-install@v1 21 | with: 22 | useLockFile: false 23 | 24 | - name: Configure git user 25 | run: | 26 | git config --global user.email ${{ github.actor }}@users.noreply.github.com 27 | git config --global user.name ${{ github.actor }} 28 | - name: Publish to npm 29 | run: npm run release 30 | env: 31 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | coverage 4 | flow-typed 5 | package-lock.json 6 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no -- commitlint --edit "$1" 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org/ 2 | package-lock=false 3 | save=false 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | build 2 | dist 3 | coverage 4 | flow-typed 5 | CHANGELOG.md 6 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Daniel Brain 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to post-robot 2 | 3 | We are always looking for ways to make our modules better. Adding features and fixing bugs allows everyone who depends 4 | on this code to create better, more stable applications. 5 | Feel free to raise a pull request to us. Our team would review your proposed modifications and, if appropriate, merge 6 | your changes into our code. Ideas and other comments are also welcome. 7 | 8 | ## Getting Started 9 | 10 | 1. Create your own [fork](https://help.github.com/articles/fork-a-repo) of this [repository](../../fork). 11 | 12 | ```bash 13 | # Clone it 14 | $ git clone git@github.com:me/post-robot.git 15 | 16 | # Change directory 17 | $ cd post-robot 18 | 19 | # Add the upstream repo 20 | $ git remote add upstream git://github.com/krakenjs/post-robot.git 21 | 22 | # Get the latest upstream changes 23 | $ git pull upstream 24 | 25 | # Install dependencies 26 | $ npm install 27 | 28 | # Run scripts to verify installation 29 | $ npm test 30 | $ npm run-script lint 31 | $ npm run-script cover 32 | ``` 33 | 34 | ## Making Changes 35 | 36 | 1. Make sure that your changes adhere to the current coding conventions used throughout the project, indentation, accurate comments, etc. 37 | 2. Lint your code regularly and ensure it passes prior to submitting a PR: 38 | `$ npm run lint`. 39 | 3. Ensure existing tests pass (`$ npm test`) and include test cases which fail without your change and succeed with it. 40 | 41 | ## Submitting Changes 42 | 43 | 1. Ensure that no errors are generated by ESLint. 44 | 2. Commit your changes in logical chunks, i.e. keep your changes small per single commit. 45 | 3. Locally merge (or rebase) the upstream branch into your topic branch: `$ git pull upstream && git merge`. 46 | 4. Push your topic branch up to your fork: `$ git push origin `. 47 | 5. Open a [Pull Request](https://help.github.com/articles/using-pull-requests) with a clear title and description. 48 | 49 | If you have any questions about contributing, please feel free to contact us by posting your questions on GitHub. 50 | 51 | Copyright 2016, PayPal under [the Apache 2.0 license](LICENSE.txt). 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # post-robot [:]-\\-< 2 | 3 | [![build status][build-badge]][build] 4 | [![code coverage][coverage-badge]][coverage] 5 | [![npm version][version-badge]][package] 6 | 7 | [build-badge]: https://img.shields.io/github/actions/workflow/status/krakenjs/post-robot/main.yaml?branch=main&logo=github&style=flat-square 8 | [build]: https://github.com/krakenjs/post-robot/actions?query=workflow%3Abuild 9 | [coverage-badge]: https://img.shields.io/codecov/c/github/krakenjs/post-robot.svg?style=flat-square 10 | [coverage]: https://codecov.io/github/krakenjs/post-robot/ 11 | [version-badge]: https://img.shields.io/npm/v/post-robot.svg?style=flat-square 12 | [package]: https://www.npmjs.com/package/post-robot 13 | 14 | Cross domain post-messaging on the client side, using a simple listener/client pattern. 15 | 16 | Send a message to another window, and: 17 | 18 | - [Get a response](#simple-listener-and-sender-with-error-handling) from the window you messaged 19 | - [Pass functions](#functions) to another window, across different domains 20 | - [Handle any errors](#simple-listener-and-sender-with-error-handling) that prevented your message from getting through 21 | - Don't worry about serializing your messages; [just send javascript objects](#simple-listener-and-sender-with-error-handling) 22 | - Use [promises](#listener-with-promise-response) or [async/await](#async--await) to wait for responses from windows you message 23 | - Set up a [secure message channel](#secure-message-channel) between two windows on a certain domain 24 | - Send messages between a [parent and a popup window](#parent-to-popup-messaging) in IE 25 | 26 | ## Serialization 27 | 28 | post-robot will serialize and deserialize the following data types in messages: 29 | 30 | - Objects, arrays, strings, numbers, booleans, null 31 | - Note: this includes any JSON-serializable types 32 | - Functions 33 | - Note: the function passed will be a [reference to the original function](#functions), and the deserialized function will always return a `Promise` - specifically a [`ZalgoPromise`](https://github.com/krakenjs/zalgo-promise) 34 | - Promises 35 | - Note: deserialized promises will be instances of [`ZalgoPromise`](https://github.com/krakenjs/zalgo-promise) 36 | - Error objects 37 | - e.g. `new Error("This error will self-destruct in 10, 9, 8...")` 38 | - Regex objects 39 | - e.g. `/[a-zA-Z0-9]*/` 40 | 41 | ## Simple listener and sender 42 | 43 | ```javascript 44 | // Set up a listener 45 | 46 | postRobot.on("getUser", function (event) { 47 | // Have it return some data to the calling window 48 | 49 | return { 50 | id: 1234, 51 | name: "Zippy the Pinhead", 52 | 53 | // Yep, we're even returning a function to the other window! 54 | 55 | logout: function () { 56 | return $currentUser.logout(); 57 | }, 58 | }; 59 | }); 60 | ``` 61 | 62 | ```javascript 63 | // Call the listener, on a different window, on a different domain 64 | 65 | postRobot 66 | .send(someWindow, "getUser", { id: 1337 }) 67 | .then(function (event) { 68 | var user = event.data; 69 | 70 | console.log(event.source, event.origin, "Got user:", user); 71 | 72 | // Call the user.logout function from the other window! 73 | 74 | user.logout(); 75 | }) 76 | .catch(function (err) { 77 | // Handle any errors that stopped our call from going through 78 | 79 | console.error(err); 80 | }); 81 | ``` 82 | 83 | ## Listener with promise response 84 | 85 | ```javascript 86 | postRobot.on("getUser", function (event) { 87 | return getUser(event.data.id).then(function (user) { 88 | return { 89 | name: user.name, 90 | }; 91 | }); 92 | }); 93 | ``` 94 | 95 | ## One-off listener 96 | 97 | ```javascript 98 | postRobot.once("getUser", function (event) { 99 | return { 100 | name: "Noggin the Nog", 101 | }; 102 | }); 103 | ``` 104 | 105 | ## Cancelling a listener 106 | 107 | ```javascript 108 | var listener = postRobot.on("getUser", function (event) { 109 | return { 110 | id: event.data.id, 111 | name: "Zippy the Pinhead", 112 | }; 113 | }); 114 | 115 | listener.cancel(); 116 | ``` 117 | 118 | ## Listen for messages from a specific window 119 | 120 | ```javascript 121 | postRobot.on("getUser", { window: window.parent }, function (event) { 122 | return { 123 | name: "Guybrush Threepwood", 124 | }; 125 | }); 126 | ``` 127 | 128 | ## Listen for messages from a specific domain 129 | 130 | ```javascript 131 | postRobot.on("getUser", { domain: "http://zombo.com" }, function (event) { 132 | return { 133 | name: "Manny Calavera", 134 | }; 135 | }); 136 | ``` 137 | 138 | ## Set a timeout for a response 139 | 140 | ```javascript 141 | postRobot 142 | .send(someWindow, "getUser", { id: 1337 }, { timeout: 5000 }) 143 | .then(function (event) { 144 | console.log(event.source, event.origin, "Got user:", event.data.name); 145 | }) 146 | .catch(function (err) { 147 | console.error(err); 148 | }); 149 | ``` 150 | 151 | ## Send a message to a specific domain 152 | 153 | ```javascript 154 | postRobot 155 | .send(someWindow, "getUser", { id: 1337 }, { domain: "http://zombo.com" }) 156 | .then(function (event) { 157 | console.log(event.source, event.origin, "Got user:", event.data.name); 158 | }); 159 | ``` 160 | 161 | ## Async / Await 162 | 163 | ```javascript 164 | postRobot.on("getUser", async ({ source, origin, data }) => { 165 | let user = await getUser(data.id); 166 | 167 | return { 168 | id: data.id, 169 | name: user.name, 170 | }; 171 | }); 172 | ``` 173 | 174 | ```javascript 175 | try { 176 | let { source, origin, data } = await postRobot.send(someWindow, `getUser`, { 177 | id: 1337, 178 | }); 179 | console.log(source, origin, "Got user:", data.name); 180 | } catch (err) { 181 | console.error(err); 182 | } 183 | ``` 184 | 185 | ## Secure Message Channel 186 | 187 | For security reasons, it is recommended that you always explicitly specify the window and domain you want to listen 188 | to and send messages to. This creates a secure message channel that only works between two windows on the specified domain: 189 | 190 | ```javascript 191 | postRobot.on( 192 | "getUser", 193 | { window: childWindow, domain: "http://zombo.com" }, 194 | function (event) { 195 | return { 196 | id: event.data.id, 197 | name: "Frodo", 198 | }; 199 | } 200 | ); 201 | ``` 202 | 203 | ```javascript 204 | postRobot 205 | .send(someWindow, "getUser", { id: 1337 }, { domain: "http://zombo.com" }) 206 | .then(function (event) { 207 | console.log(event.source, event.origin, "Got user:", event.data.name); 208 | }) 209 | .catch(function (err) { 210 | console.error(err); 211 | }); 212 | ``` 213 | 214 | ## Functions 215 | 216 | Post robot lets you send across functions in your data payload, fairly seamlessly. 217 | 218 | For example: 219 | 220 | ```javascript 221 | postRobot.on("getUser", function (event) { 222 | return { 223 | id: event.data.id, 224 | name: "Nogbad the Bad", 225 | 226 | logout: function () { 227 | currentUser.logout(); 228 | }, 229 | }; 230 | }); 231 | ``` 232 | 233 | ```javascript 234 | postRobot.send(myWindow, "getUser", { id: 1337 }).then(function (event) { 235 | var user = event.data; 236 | 237 | user.logout().then(function () { 238 | console.log("User was logged out"); 239 | }); 240 | }); 241 | ``` 242 | 243 | The function `user.logout()` will be called on the **original** window. Post Robot transparently messages back to the 244 | original window, calls the function that was passed, then messages back with the result of the function. 245 | 246 | Because this uses post-messaging behind the scenes and is therefore always async, `user.logout()` will **always** return a promise, and must be `.then`'d or `await`ed. 247 | 248 | ## Parent to popup messaging 249 | 250 | Unfortunately, IE blocks direct post messaging between a parent window and a popup, on different domains. 251 | 252 | In order to use post-robot in IE9+ with popup windows, you will need to set up an invisible 'bridge' iframe on your parent page: 253 | 254 | ``` 255 | [ Parent page ] 256 | 257 | +---------------------+ [ Popup ] 258 | | xx.com | 259 | | | +--------------+ 260 | | +---------------+ | | yy.com | 261 | | | [iframe] | | | | 262 | | | | | | | 263 | | | yy.com/bridge | | | | 264 | | | | | | | 265 | | | | | | | 266 | | | | | | | 267 | | | | | +--------------+ 268 | | +---------------+ | 269 | | | 270 | +---------------------+ 271 | ``` 272 | 273 | a. Use the special `ie` build of post-robot: `dist/post-robot.ie.js`. 274 | 275 | b. Create a bridge path on the domain of your popup, for example `http://yy.com/bridge.html`, and include post-robot: 276 | 277 | ```html 278 | 279 | ``` 280 | 281 | c. In the parent page on `xx.com` which opens the popup, include the following javascript: 282 | 283 | ```html 284 | 287 | ``` 288 | 289 | Now `xx.com` and `yy.com` can communicate freely using post-robot, in IE. 290 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | We take security very seriously and ask that you follow the following process. 4 | 5 | ## Contact us 6 | 7 | If you think you may have found a security bug we ask that you privately send the details to DL-PP-Kraken-Js@paypal.com. Please make sure to use a descriptive title in the email. 8 | 9 | ## Expectations 10 | 11 | We will generally get back to you within **24 hours**, but a more detailed response may take up to **48 hours**. If you feel we're not responding back in time, please send us a message _without detail_ on Twitter [@kraken_js](https://twitter.com/kraken_js). 12 | 13 | ## History 14 | 15 | No reported issues 16 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | // eslint-disable-next-line import/no-commonjs 4 | module.exports = { 5 | extends: "@krakenjs/babel-config-grumbler/babelrc-node", 6 | }; 7 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | /* eslint import/no-commonjs: off */ 3 | 4 | module.exports = { 5 | extends: ["@commitlint/config-conventional"], 6 | }; 7 | -------------------------------------------------------------------------------- /demo/bridge.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /demo/child.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 23 | -------------------------------------------------------------------------------- /demo/parent.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 29 | -------------------------------------------------------------------------------- /dist/module/bridge/bridge.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | exports.__esModule = true; 4 | exports.setupOpenTunnelToParent = setupOpenTunnelToParent; 5 | 6 | var _src = require("@krakenjs/cross-domain-utils/src"); 7 | 8 | var _src2 = require("@krakenjs/belter/src"); 9 | 10 | var _conf = require("../conf"); 11 | 12 | var _global = require("../global"); 13 | 14 | function cleanTunnelWindows() { 15 | const tunnelWindows = (0, _global.globalStore)('tunnelWindows'); 16 | 17 | for (const key of tunnelWindows.keys()) { 18 | const tunnelWindow = tunnelWindows[key]; 19 | 20 | try { 21 | (0, _src2.noop)(tunnelWindow.source); 22 | } catch (err) { 23 | tunnelWindows.del(key); 24 | continue; 25 | } 26 | 27 | if ((0, _src.isWindowClosed)(tunnelWindow.source)) { 28 | tunnelWindows.del(key); 29 | } 30 | } 31 | } 32 | 33 | function addTunnelWindow({ 34 | name, 35 | source, 36 | canary, 37 | sendMessage 38 | }) { 39 | cleanTunnelWindows(); 40 | const id = (0, _src2.uniqueID)(); 41 | const tunnelWindows = (0, _global.globalStore)('tunnelWindows'); 42 | tunnelWindows.set(id, { 43 | name, 44 | source, 45 | canary, 46 | sendMessage 47 | }); 48 | return id; 49 | } 50 | 51 | function setupOpenTunnelToParent({ 52 | send 53 | }) { 54 | (0, _global.getGlobal)(window).openTunnelToParent = function openTunnelToParent({ 55 | name, 56 | source, 57 | canary, 58 | sendMessage 59 | }) { 60 | const tunnelWindows = (0, _global.globalStore)('tunnelWindows'); 61 | const parentWindow = (0, _src.getParent)(window); 62 | 63 | if (!parentWindow) { 64 | throw new Error(`No parent window found to open tunnel to`); 65 | } 66 | 67 | const id = addTunnelWindow({ 68 | name, 69 | source, 70 | canary, 71 | sendMessage 72 | }); 73 | return send(parentWindow, _conf.MESSAGE_NAME.OPEN_TUNNEL, { 74 | name, 75 | 76 | sendMessage() { 77 | const tunnelWindow = tunnelWindows.get(id); 78 | 79 | try { 80 | // IE gets antsy if you try to even reference a closed window 81 | (0, _src2.noop)(tunnelWindow && tunnelWindow.source); 82 | } catch (err) { 83 | tunnelWindows.del(id); 84 | return; 85 | } 86 | 87 | if (!tunnelWindow || !tunnelWindow.source || (0, _src.isWindowClosed)(tunnelWindow.source)) { 88 | return; 89 | } 90 | 91 | try { 92 | tunnelWindow.canary(); 93 | } catch (err) { 94 | return; 95 | } // $FlowFixMe[object-this-reference] 96 | 97 | 98 | tunnelWindow.sendMessage.apply(this, arguments); 99 | } 100 | 101 | }, { 102 | domain: _conf.WILDCARD 103 | }); 104 | }; 105 | } -------------------------------------------------------------------------------- /dist/module/bridge/child.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | exports.__esModule = true; 4 | exports.openTunnelToOpener = openTunnelToOpener; 5 | 6 | var _src = require("@krakenjs/zalgo-promise/src"); 7 | 8 | var _src2 = require("@krakenjs/cross-domain-utils/src"); 9 | 10 | var _src3 = require("@krakenjs/belter/src"); 11 | 12 | var _global = require("../global"); 13 | 14 | var _common = require("./common"); 15 | 16 | function awaitRemoteBridgeForWindow(win) { 17 | return (0, _global.windowStore)('remoteBridgeAwaiters').getOrSet(win, () => { 18 | return _src.ZalgoPromise.try(() => { 19 | const frame = (0, _src2.getFrameByName)(win, (0, _common.getBridgeName)((0, _src2.getDomain)())); 20 | 21 | if (!frame) { 22 | return; 23 | } 24 | 25 | if ((0, _src2.isSameDomain)(frame) && (0, _global.getGlobal)((0, _src2.assertSameDomain)(frame))) { 26 | return frame; 27 | } 28 | 29 | return new _src.ZalgoPromise(resolve => { 30 | let interval; 31 | let timeout; // eslint-disable-line prefer-const 32 | 33 | interval = setInterval(() => { 34 | // eslint-disable-line prefer-const 35 | if (frame && (0, _src2.isSameDomain)(frame) && (0, _global.getGlobal)((0, _src2.assertSameDomain)(frame))) { 36 | clearInterval(interval); 37 | clearTimeout(timeout); 38 | return resolve(frame); 39 | } 40 | }, 100); 41 | timeout = setTimeout(() => { 42 | clearInterval(interval); 43 | return resolve(); 44 | }, 2000); 45 | }); 46 | }); 47 | }); 48 | } 49 | 50 | function openTunnelToOpener({ 51 | on, 52 | send, 53 | receiveMessage 54 | }) { 55 | return _src.ZalgoPromise.try(() => { 56 | const opener = (0, _src2.getOpener)(window); 57 | 58 | if (!opener || !(0, _common.needsBridge)({ 59 | win: opener 60 | })) { 61 | return; 62 | } 63 | 64 | (0, _common.registerRemoteWindow)(opener); 65 | return awaitRemoteBridgeForWindow(opener).then(bridge => { 66 | if (!bridge) { 67 | return (0, _common.rejectRemoteSendMessage)(opener, new Error(`Can not register with opener: no bridge found in opener`)); 68 | } 69 | 70 | if (!window.name) { 71 | return (0, _common.rejectRemoteSendMessage)(opener, new Error(`Can not register with opener: window does not have a name`)); 72 | } 73 | 74 | return (0, _global.getGlobal)((0, _src2.assertSameDomain)(bridge)).openTunnelToParent({ 75 | name: window.name, 76 | source: window, 77 | 78 | canary() {// pass 79 | }, 80 | 81 | sendMessage(message) { 82 | try { 83 | (0, _src3.noop)(window); 84 | } catch (err) { 85 | return; 86 | } 87 | 88 | if (!window || window.closed) { 89 | return; 90 | } 91 | 92 | try { 93 | receiveMessage({ 94 | data: message, 95 | // $FlowFixMe[object-this-reference] 96 | origin: this.origin, 97 | // $FlowFixMe[object-this-reference] 98 | source: this.source 99 | }, { 100 | on, 101 | send 102 | }); 103 | } catch (err) { 104 | _src.ZalgoPromise.reject(err); 105 | } 106 | } 107 | 108 | }).then(({ 109 | source, 110 | origin, 111 | data 112 | }) => { 113 | if (source !== opener) { 114 | throw new Error(`Source does not match opener`); 115 | } 116 | 117 | (0, _common.registerRemoteSendMessage)(source, origin, data.sendMessage); 118 | }).catch(err => { 119 | (0, _common.rejectRemoteSendMessage)(opener, err); 120 | throw err; 121 | }); 122 | }); 123 | }); 124 | } -------------------------------------------------------------------------------- /dist/module/bridge/common.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | exports.__esModule = true; 4 | exports.documentBodyReady = void 0; 5 | exports.findRemoteWindow = findRemoteWindow; 6 | exports.getBridgeName = getBridgeName; 7 | exports.isBridge = isBridge; 8 | exports.needsBridge = needsBridge; 9 | exports.needsBridgeForBrowser = needsBridgeForBrowser; 10 | exports.needsBridgeForDomain = needsBridgeForDomain; 11 | exports.needsBridgeForWin = needsBridgeForWin; 12 | exports.registerRemoteSendMessage = registerRemoteSendMessage; 13 | exports.registerRemoteWindow = registerRemoteWindow; 14 | exports.rejectRemoteSendMessage = rejectRemoteSendMessage; 15 | exports.sendBridgeMessage = sendBridgeMessage; 16 | 17 | var _src = require("@krakenjs/zalgo-promise/src"); 18 | 19 | var _src2 = require("@krakenjs/cross-domain-utils/src"); 20 | 21 | var _src3 = require("@krakenjs/belter/src"); 22 | 23 | var _conf = require("../conf"); 24 | 25 | var _global = require("../global"); 26 | 27 | function needsBridgeForBrowser() { 28 | if ((0, _src2.getUserAgent)(window).match(/MSIE|trident|edge\/12|edge\/13/i)) { 29 | return true; 30 | } 31 | 32 | return false; 33 | } 34 | 35 | function needsBridgeForWin(win) { 36 | if (!(0, _src2.isSameTopWindow)(window, win)) { 37 | return true; 38 | } 39 | 40 | return false; 41 | } 42 | 43 | function needsBridgeForDomain(domain, win) { 44 | if (domain) { 45 | if ((0, _src2.getDomain)() !== (0, _src2.getDomainFromUrl)(domain)) { 46 | return true; 47 | } 48 | } else if (win) { 49 | if (!(0, _src2.isSameDomain)(win)) { 50 | return true; 51 | } 52 | } 53 | 54 | return false; 55 | } 56 | 57 | function needsBridge({ 58 | win, 59 | domain 60 | }) { 61 | if (!needsBridgeForBrowser()) { 62 | return false; 63 | } 64 | 65 | if (domain && !needsBridgeForDomain(domain, win)) { 66 | return false; 67 | } 68 | 69 | if (win && !needsBridgeForWin(win)) { 70 | return false; 71 | } 72 | 73 | return true; 74 | } 75 | 76 | function getBridgeName(domain) { 77 | domain = domain || (0, _src2.getDomainFromUrl)(domain); 78 | const sanitizedDomain = domain.replace(/[^a-zA-Z0-9]+/g, '_'); 79 | const id = `${_conf.BRIDGE_NAME_PREFIX}_${sanitizedDomain}`; 80 | return id; 81 | } 82 | 83 | function isBridge() { 84 | return Boolean(window.name && window.name === getBridgeName((0, _src2.getDomain)())); 85 | } 86 | 87 | const documentBodyReady = new _src.ZalgoPromise(resolve => { 88 | if (window.document && window.document.body) { 89 | return resolve(window.document.body); 90 | } 91 | 92 | const interval = setInterval(() => { 93 | if (window.document && window.document.body) { 94 | clearInterval(interval); 95 | return resolve(window.document.body); 96 | } 97 | }, 10); 98 | }); 99 | exports.documentBodyReady = documentBodyReady; 100 | 101 | function registerRemoteWindow(win) { 102 | const remoteWindowPromises = (0, _global.windowStore)('remoteWindowPromises'); 103 | remoteWindowPromises.getOrSet(win, () => new _src.ZalgoPromise()); 104 | } 105 | 106 | function findRemoteWindow(win) { 107 | const remoteWindowPromises = (0, _global.windowStore)('remoteWindowPromises'); 108 | const remoteWinPromise = remoteWindowPromises.get(win); 109 | 110 | if (!remoteWinPromise) { 111 | throw new Error(`Remote window promise not found`); 112 | } 113 | 114 | return remoteWinPromise; 115 | } 116 | 117 | function registerRemoteSendMessage(win, domain, sendMessage) { 118 | const sendMessageWrapper = (remoteWin, remoteDomain, message) => { 119 | if (remoteWin !== win) { 120 | throw new Error(`Remote window does not match window`); 121 | } 122 | 123 | if (!(0, _src2.matchDomain)(remoteDomain, domain)) { 124 | throw new Error(`Remote domain ${remoteDomain} does not match domain ${domain}`); 125 | } 126 | 127 | sendMessage.fireAndForget(message); 128 | }; 129 | 130 | findRemoteWindow(win).resolve(sendMessageWrapper); 131 | } 132 | 133 | function rejectRemoteSendMessage(win, err) { 134 | findRemoteWindow(win).reject(err).catch(_src3.noop); 135 | } 136 | 137 | function sendBridgeMessage(win, domain, message) { 138 | const messagingChild = (0, _src2.isOpener)(window, win); 139 | const messagingParent = (0, _src2.isOpener)(win, window); 140 | 141 | if (!messagingChild && !messagingParent) { 142 | throw new Error(`Can only send messages to and from parent and popup windows`); 143 | } 144 | 145 | return findRemoteWindow(win).then(sendMessage => { 146 | return sendMessage(win, domain, message); 147 | }); 148 | } -------------------------------------------------------------------------------- /dist/module/bridge/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | exports.__esModule = true; 4 | 5 | var _bridge = require("./bridge"); 6 | 7 | Object.keys(_bridge).forEach(function (key) { 8 | if (key === "default" || key === "__esModule") return; 9 | if (key in exports && exports[key] === _bridge[key]) return; 10 | exports[key] = _bridge[key]; 11 | }); 12 | 13 | var _child = require("./child"); 14 | 15 | Object.keys(_child).forEach(function (key) { 16 | if (key === "default" || key === "__esModule") return; 17 | if (key in exports && exports[key] === _child[key]) return; 18 | exports[key] = _child[key]; 19 | }); 20 | 21 | var _common = require("./common"); 22 | 23 | Object.keys(_common).forEach(function (key) { 24 | if (key === "default" || key === "__esModule") return; 25 | if (key in exports && exports[key] === _common[key]) return; 26 | exports[key] = _common[key]; 27 | }); 28 | 29 | var _parent = require("./parent"); 30 | 31 | Object.keys(_parent).forEach(function (key) { 32 | if (key === "default" || key === "__esModule") return; 33 | if (key in exports && exports[key] === _parent[key]) return; 34 | exports[key] = _parent[key]; 35 | }); 36 | 37 | var _setup = require("./setup"); 38 | 39 | Object.keys(_setup).forEach(function (key) { 40 | if (key === "default" || key === "__esModule") return; 41 | if (key in exports && exports[key] === _setup[key]) return; 42 | exports[key] = _setup[key]; 43 | }); -------------------------------------------------------------------------------- /dist/module/bridge/parent.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | exports.__esModule = true; 4 | exports.destroyBridges = destroyBridges; 5 | exports.hasBridge = hasBridge; 6 | exports.linkUrl = linkUrl; 7 | exports.linkWindow = linkWindow; 8 | exports.listenForOpenTunnel = listenForOpenTunnel; 9 | exports.listenForWindowOpen = listenForWindowOpen; 10 | exports.openBridge = openBridge; 11 | 12 | var _src = require("@krakenjs/zalgo-promise/src"); 13 | 14 | var _src2 = require("@krakenjs/cross-domain-utils/src"); 15 | 16 | var _conf = require("../conf"); 17 | 18 | var _lib = require("../lib"); 19 | 20 | var _global = require("../global"); 21 | 22 | var _common = require("./common"); 23 | 24 | function listenForOpenTunnel({ 25 | on, 26 | send, 27 | receiveMessage 28 | }) { 29 | const popupWindowsByName = (0, _global.globalStore)('popupWindowsByName'); 30 | on(_conf.MESSAGE_NAME.OPEN_TUNNEL, ({ 31 | source, 32 | origin, 33 | data 34 | }) => { 35 | const bridgePromise = (0, _global.globalStore)('bridges').get(origin); 36 | 37 | if (!bridgePromise) { 38 | throw new Error(`Can not find bridge promise for domain ${origin}`); 39 | } 40 | 41 | return bridgePromise.then(bridge => { 42 | if (source !== bridge) { 43 | throw new Error(`Message source does not matched registered bridge for domain ${origin}`); 44 | } 45 | 46 | if (!data.name) { 47 | throw new Error(`Register window expected to be passed window name`); 48 | } 49 | 50 | if (!data.sendMessage) { 51 | throw new Error(`Register window expected to be passed sendMessage method`); 52 | } 53 | 54 | if (!popupWindowsByName.has(data.name)) { 55 | throw new Error(`Window with name ${data.name} does not exist, or was not opened by this window`); 56 | } 57 | 58 | const getWindowDetails = () => { 59 | const winDetails = popupWindowsByName.get(data.name); // $FlowFixMe 60 | 61 | return winDetails; 62 | }; 63 | 64 | if (!getWindowDetails().domain) { 65 | throw new Error(`We do not have a registered domain for window ${data.name}`); 66 | } 67 | 68 | if (getWindowDetails().domain !== origin) { 69 | throw new Error(`Message origin ${origin} does not matched registered window origin ${getWindowDetails().domain || 'unknown'}`); 70 | } 71 | 72 | (0, _common.registerRemoteSendMessage)(getWindowDetails().win, origin, data.sendMessage); 73 | return { 74 | sendMessage(message) { 75 | if (!window || window.closed) { 76 | return; 77 | } 78 | 79 | if (!getWindowDetails()) { 80 | return; 81 | } 82 | 83 | const domain = getWindowDetails().domain; 84 | 85 | if (!domain) { 86 | return; 87 | } 88 | 89 | try { 90 | receiveMessage({ 91 | data: message, 92 | origin: domain, 93 | source: getWindowDetails().win 94 | }, { 95 | on, 96 | send 97 | }); 98 | } catch (err) { 99 | _src.ZalgoPromise.reject(err); 100 | } 101 | } 102 | 103 | }; 104 | }); 105 | }); 106 | } 107 | 108 | function openBridgeFrame(name, url) { 109 | const iframe = document.createElement(`iframe`); 110 | iframe.setAttribute(`name`, name); 111 | iframe.setAttribute(`id`, name); 112 | iframe.setAttribute(`style`, `display: none; margin: 0; padding: 0; border: 0px none; overflow: hidden;`); 113 | iframe.setAttribute(`frameborder`, `0`); 114 | iframe.setAttribute(`border`, `0`); 115 | iframe.setAttribute(`scrolling`, `no`); 116 | iframe.setAttribute(`allowTransparency`, `true`); 117 | iframe.setAttribute(`tabindex`, `-1`); 118 | iframe.setAttribute(`hidden`, `true`); 119 | iframe.setAttribute(`title`, ``); 120 | iframe.setAttribute(`role`, `presentation`); 121 | iframe.src = url; 122 | return iframe; 123 | } 124 | 125 | function hasBridge(url, domain) { 126 | const bridges = (0, _global.globalStore)('bridges'); 127 | return bridges.has(domain || (0, _src2.getDomainFromUrl)(url)); 128 | } 129 | 130 | function openBridge(url, domain) { 131 | const bridges = (0, _global.globalStore)('bridges'); 132 | const bridgeFrames = (0, _global.globalStore)('bridgeFrames'); 133 | domain = domain || (0, _src2.getDomainFromUrl)(url); 134 | return bridges.getOrSet(domain, () => _src.ZalgoPromise.try(() => { 135 | if ((0, _src2.getDomain)() === domain) { 136 | throw new Error(`Can not open bridge on the same domain as current domain: ${domain}`); 137 | } 138 | 139 | const name = (0, _common.getBridgeName)(domain); 140 | const frame = (0, _src2.getFrameByName)(window, name); 141 | 142 | if (frame) { 143 | throw new Error(`Frame with name ${name} already exists on page`); 144 | } 145 | 146 | const iframe = openBridgeFrame(name, url); 147 | bridgeFrames.set(domain, iframe); 148 | return _common.documentBodyReady.then(body => { 149 | body.appendChild(iframe); 150 | const bridge = iframe.contentWindow; 151 | return new _src.ZalgoPromise((resolve, reject) => { 152 | iframe.addEventListener('load', resolve); 153 | iframe.addEventListener('error', reject); 154 | }).then(() => { 155 | return (0, _lib.awaitWindowHello)(bridge, _conf.BRIDGE_TIMEOUT, `Bridge ${url}`); 156 | }).then(() => { 157 | return bridge; 158 | }); 159 | }); 160 | })); 161 | } 162 | 163 | function linkWindow({ 164 | win, 165 | name, 166 | domain 167 | }) { 168 | const popupWindowsByName = (0, _global.globalStore)('popupWindowsByName'); 169 | const popupWindowsByWin = (0, _global.windowStore)('popupWindowsByWin'); 170 | 171 | for (const winName of popupWindowsByName.keys()) { 172 | const details = popupWindowsByName.get(winName); 173 | 174 | if (!details || (0, _src2.isWindowClosed)(details.win)) { 175 | popupWindowsByName.del(winName); 176 | } 177 | } 178 | 179 | if ((0, _src2.isWindowClosed)(win)) { 180 | return { 181 | win, 182 | name, 183 | domain 184 | }; 185 | } 186 | 187 | const details = popupWindowsByWin.getOrSet(win, () => { 188 | if (!name) { 189 | return { 190 | win 191 | }; 192 | } // $FlowFixMe 193 | 194 | 195 | return popupWindowsByName.getOrSet(name, () => { 196 | return { 197 | win, 198 | name 199 | }; 200 | }); 201 | }); 202 | 203 | if (details.win && details.win !== win) { 204 | throw new Error(`Different window already linked for window: ${name || 'undefined'}`); 205 | } 206 | 207 | if (name) { 208 | details.name = name; 209 | popupWindowsByName.set(name, details); 210 | } 211 | 212 | if (domain) { 213 | details.domain = domain; 214 | (0, _common.registerRemoteWindow)(win); 215 | } 216 | 217 | popupWindowsByWin.set(win, details); 218 | return details; 219 | } 220 | 221 | function linkUrl(win, url) { 222 | linkWindow({ 223 | win, 224 | domain: (0, _src2.getDomainFromUrl)(url) 225 | }); 226 | } 227 | 228 | function listenForWindowOpen() { 229 | const windowOpen = window.open; 230 | 231 | window.open = function windowOpenWrapper(url, name, options, last) { 232 | const win = windowOpen.call(this, (0, _src2.normalizeMockUrl)(url), name, options, last); 233 | 234 | if (!win) { 235 | return win; 236 | } 237 | 238 | linkWindow({ 239 | win, 240 | name, 241 | domain: url ? (0, _src2.getDomainFromUrl)(url) : null 242 | }); 243 | return win; 244 | }; 245 | } 246 | 247 | function destroyBridges() { 248 | const bridges = (0, _global.globalStore)('bridges'); 249 | const bridgeFrames = (0, _global.globalStore)('bridgeFrames'); 250 | 251 | for (const domain of bridgeFrames.keys()) { 252 | const frame = bridgeFrames.get(domain); 253 | 254 | if (frame && frame.parentNode) { 255 | frame.parentNode.removeChild(frame); 256 | } 257 | } 258 | 259 | bridgeFrames.reset(); 260 | bridges.reset(); 261 | } -------------------------------------------------------------------------------- /dist/module/bridge/setup.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | exports.__esModule = true; 4 | exports.setupBridge = setupBridge; 5 | 6 | var _parent = require("./parent"); 7 | 8 | var _bridge = require("./bridge"); 9 | 10 | var _child = require("./child"); 11 | 12 | function setupBridge({ 13 | on, 14 | send, 15 | receiveMessage 16 | }) { 17 | (0, _parent.listenForWindowOpen)(); 18 | (0, _parent.listenForOpenTunnel)({ 19 | on, 20 | send, 21 | receiveMessage 22 | }); 23 | (0, _bridge.setupOpenTunnelToParent)({ 24 | send 25 | }); 26 | (0, _child.openTunnelToOpener)({ 27 | on, 28 | send, 29 | receiveMessage 30 | }); 31 | } -------------------------------------------------------------------------------- /dist/module/clean.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | exports.__esModule = true; 4 | exports.cleanUpWindow = cleanUpWindow; 5 | 6 | var _src = require("@krakenjs/cross-domain-utils/src"); 7 | 8 | var _src2 = require("@krakenjs/belter/src"); 9 | 10 | var _global = require("./global"); 11 | 12 | function cleanUpWindow(win) { 13 | const requestPromises = (0, _global.windowStore)('requestPromises'); 14 | 15 | for (const promise of requestPromises.get(win, [])) { 16 | promise.reject(new Error(`Window ${(0, _src.isWindowClosed)(win) ? 'closed' : 'cleaned up'} before response`)).catch(_src2.noop); 17 | } 18 | } -------------------------------------------------------------------------------- /dist/module/conf/config.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | exports.__esModule = true; 4 | exports.RES_TIMEOUT = exports.RESPONSE_CYCLE_TIME = exports.CHILD_WINDOW_TIMEOUT = exports.BRIDGE_TIMEOUT = exports.ACK_TIMEOUT_KNOWN = exports.ACK_TIMEOUT = void 0; 5 | const BRIDGE_TIMEOUT = 5000; 6 | exports.BRIDGE_TIMEOUT = BRIDGE_TIMEOUT; 7 | const CHILD_WINDOW_TIMEOUT = 5000; 8 | exports.CHILD_WINDOW_TIMEOUT = CHILD_WINDOW_TIMEOUT; 9 | const ACK_TIMEOUT = 2000; 10 | exports.ACK_TIMEOUT = ACK_TIMEOUT; 11 | const ACK_TIMEOUT_KNOWN = 10000; 12 | exports.ACK_TIMEOUT_KNOWN = ACK_TIMEOUT_KNOWN; 13 | const RES_TIMEOUT = __TEST__ ? 2000 : -1; 14 | exports.RES_TIMEOUT = RES_TIMEOUT; 15 | const RESPONSE_CYCLE_TIME = 500; 16 | exports.RESPONSE_CYCLE_TIME = RESPONSE_CYCLE_TIME; -------------------------------------------------------------------------------- /dist/module/conf/constants.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | exports.__esModule = true; 4 | exports.WILDCARD = exports.SERIALIZATION_TYPE = exports.SEND_STRATEGY = exports.POSTROBOT_PROXY = exports.METHOD = exports.MESSAGE_TYPE = exports.MESSAGE_NAME = exports.MESSAGE_ACK = exports.BRIDGE_NAME_PREFIX = void 0; 5 | const MESSAGE_TYPE = { 6 | REQUEST: 'postrobot_message_request', 7 | RESPONSE: 'postrobot_message_response', 8 | ACK: 'postrobot_message_ack' 9 | }; 10 | exports.MESSAGE_TYPE = MESSAGE_TYPE; 11 | const MESSAGE_ACK = { 12 | SUCCESS: 'success', 13 | ERROR: 'error' 14 | }; 15 | exports.MESSAGE_ACK = MESSAGE_ACK; 16 | const MESSAGE_NAME = { 17 | METHOD: 'postrobot_method', 18 | HELLO: 'postrobot_hello', 19 | OPEN_TUNNEL: 'postrobot_open_tunnel' 20 | }; 21 | exports.MESSAGE_NAME = MESSAGE_NAME; 22 | const SEND_STRATEGY = { 23 | POST_MESSAGE: 'postrobot_post_message', 24 | BRIDGE: 'postrobot_bridge', 25 | GLOBAL: 'postrobot_global' 26 | }; 27 | exports.SEND_STRATEGY = SEND_STRATEGY; 28 | const BRIDGE_NAME_PREFIX = '__postrobot_bridge__'; 29 | exports.BRIDGE_NAME_PREFIX = BRIDGE_NAME_PREFIX; 30 | const POSTROBOT_PROXY = '__postrobot_proxy__'; 31 | exports.POSTROBOT_PROXY = POSTROBOT_PROXY; 32 | const WILDCARD = '*'; 33 | exports.WILDCARD = WILDCARD; 34 | const SERIALIZATION_TYPE = { 35 | CROSS_DOMAIN_ZALGO_PROMISE: 'cross_domain_zalgo_promise', 36 | CROSS_DOMAIN_FUNCTION: 'cross_domain_function', 37 | CROSS_DOMAIN_WINDOW: 'cross_domain_window' 38 | }; 39 | exports.SERIALIZATION_TYPE = SERIALIZATION_TYPE; 40 | const METHOD = { 41 | GET: 'get', 42 | POST: 'post' 43 | }; 44 | exports.METHOD = METHOD; -------------------------------------------------------------------------------- /dist/module/conf/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | exports.__esModule = true; 4 | 5 | var _config = require("./config"); 6 | 7 | Object.keys(_config).forEach(function (key) { 8 | if (key === "default" || key === "__esModule") return; 9 | if (key in exports && exports[key] === _config[key]) return; 10 | exports[key] = _config[key]; 11 | }); 12 | 13 | var _constants = require("./constants"); 14 | 15 | Object.keys(_constants).forEach(function (key) { 16 | if (key === "default" || key === "__esModule") return; 17 | if (key in exports && exports[key] === _constants[key]) return; 18 | exports[key] = _constants[key]; 19 | }); -------------------------------------------------------------------------------- /dist/module/declarations.js: -------------------------------------------------------------------------------- 1 | "use strict"; -------------------------------------------------------------------------------- /dist/module/drivers/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | exports.__esModule = true; 4 | 5 | var _receive = require("./receive"); 6 | 7 | Object.keys(_receive).forEach(function (key) { 8 | if (key === "default" || key === "__esModule") return; 9 | if (key in exports && exports[key] === _receive[key]) return; 10 | exports[key] = _receive[key]; 11 | }); 12 | 13 | var _send = require("./send"); 14 | 15 | Object.keys(_send).forEach(function (key) { 16 | if (key === "default" || key === "__esModule") return; 17 | if (key in exports && exports[key] === _send[key]) return; 18 | exports[key] = _send[key]; 19 | }); 20 | 21 | var _listeners = require("./listeners"); 22 | 23 | Object.keys(_listeners).forEach(function (key) { 24 | if (key === "default" || key === "__esModule") return; 25 | if (key in exports && exports[key] === _listeners[key]) return; 26 | exports[key] = _listeners[key]; 27 | }); -------------------------------------------------------------------------------- /dist/module/drivers/listeners.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | exports.__esModule = true; 4 | exports.addRequestListener = addRequestListener; 5 | exports.addResponseListener = addResponseListener; 6 | exports.cancelResponseListeners = cancelResponseListeners; 7 | exports.deleteResponseListener = deleteResponseListener; 8 | exports.getRequestListener = getRequestListener; 9 | exports.getResponseListener = getResponseListener; 10 | exports.isResponseListenerErrored = isResponseListenerErrored; 11 | exports.markResponseListenerErrored = markResponseListenerErrored; 12 | exports.resetListeners = resetListeners; 13 | 14 | var _src = require("@krakenjs/cross-domain-utils/src"); 15 | 16 | var _src2 = require("@krakenjs/belter/src"); 17 | 18 | var _global = require("../global"); 19 | 20 | var _conf = require("../conf"); 21 | 22 | var _window = require("../serialize/window"); 23 | 24 | function resetListeners() { 25 | const responseListeners = (0, _global.globalStore)('responseListeners'); 26 | const erroredResponseListeners = (0, _global.globalStore)('erroredResponseListeners'); 27 | responseListeners.reset(); 28 | erroredResponseListeners.reset(); 29 | } 30 | 31 | const __DOMAIN_REGEX__ = '__domain_regex__'; 32 | 33 | function addResponseListener(hash, listener) { 34 | const responseListeners = (0, _global.globalStore)('responseListeners'); 35 | responseListeners.set(hash, listener); 36 | } 37 | 38 | function getResponseListener(hash) { 39 | const responseListeners = (0, _global.globalStore)('responseListeners'); 40 | return responseListeners.get(hash); 41 | } 42 | 43 | function deleteResponseListener(hash) { 44 | const responseListeners = (0, _global.globalStore)('responseListeners'); 45 | responseListeners.del(hash); 46 | } 47 | 48 | function cancelResponseListeners() { 49 | const responseListeners = (0, _global.globalStore)('responseListeners'); 50 | 51 | for (const hash of responseListeners.keys()) { 52 | const listener = responseListeners.get(hash); 53 | 54 | if (listener) { 55 | listener.cancelled = true; 56 | } 57 | 58 | responseListeners.del(hash); 59 | } 60 | } 61 | 62 | function markResponseListenerErrored(hash) { 63 | const erroredResponseListeners = (0, _global.globalStore)('erroredResponseListeners'); 64 | erroredResponseListeners.set(hash, true); 65 | } 66 | 67 | function isResponseListenerErrored(hash) { 68 | const erroredResponseListeners = (0, _global.globalStore)('erroredResponseListeners'); 69 | return erroredResponseListeners.has(hash); 70 | } 71 | 72 | function getRequestListener({ 73 | name, 74 | win, 75 | domain 76 | }) { 77 | const requestListeners = (0, _global.windowStore)('requestListeners'); 78 | 79 | if (win === _conf.WILDCARD) { 80 | win = null; 81 | } 82 | 83 | if (domain === _conf.WILDCARD) { 84 | domain = null; 85 | } 86 | 87 | if (!name) { 88 | throw new Error(`Name required to get request listener`); 89 | } 90 | 91 | for (const winQualifier of [win, (0, _global.getWildcard)()]) { 92 | if (!winQualifier) { 93 | continue; 94 | } 95 | 96 | const nameListeners = requestListeners.get(winQualifier); 97 | 98 | if (!nameListeners) { 99 | continue; 100 | } 101 | 102 | const domainListeners = nameListeners[name]; 103 | 104 | if (!domainListeners) { 105 | continue; 106 | } 107 | 108 | if (domain && typeof domain === 'string') { 109 | if (domainListeners[domain]) { 110 | return domainListeners[domain]; 111 | } 112 | 113 | if (domainListeners[__DOMAIN_REGEX__]) { 114 | for (const { 115 | regex, 116 | listener 117 | } of domainListeners[__DOMAIN_REGEX__]) { 118 | if ((0, _src.matchDomain)(regex, domain)) { 119 | return listener; 120 | } 121 | } 122 | } 123 | } 124 | 125 | if (domainListeners[_conf.WILDCARD]) { 126 | return domainListeners[_conf.WILDCARD]; 127 | } 128 | } 129 | } // eslint-disable-next-line complexity 130 | 131 | 132 | function addRequestListener({ 133 | name, 134 | win: winCandidate, 135 | domain 136 | }, listener) { 137 | const requestListeners = (0, _global.windowStore)('requestListeners'); 138 | 139 | if (!name || typeof name !== 'string') { 140 | throw new Error(`Name required to add request listener`); 141 | } // $FlowFixMe 142 | 143 | 144 | if (winCandidate && winCandidate !== _conf.WILDCARD && _window.ProxyWindow.isProxyWindow(winCandidate)) { 145 | // $FlowFixMe 146 | const proxyWin = winCandidate; 147 | const requestListenerPromise = proxyWin.awaitWindow().then(actualWin => { 148 | return addRequestListener({ 149 | name, 150 | win: actualWin, 151 | domain 152 | }, listener); 153 | }); 154 | return { 155 | cancel: () => { 156 | requestListenerPromise.then(requestListener => requestListener.cancel(), _src2.noop); 157 | } 158 | }; 159 | } // $FlowFixMe 160 | 161 | 162 | let win = winCandidate; 163 | 164 | if (Array.isArray(win)) { 165 | const listenersCollection = []; 166 | 167 | for (const item of win) { 168 | listenersCollection.push(addRequestListener({ 169 | name, 170 | domain, 171 | win: item 172 | }, listener)); 173 | } 174 | 175 | return { 176 | cancel() { 177 | for (const cancelListener of listenersCollection) { 178 | cancelListener.cancel(); 179 | } 180 | } 181 | 182 | }; 183 | } 184 | 185 | if (Array.isArray(domain)) { 186 | const listenersCollection = []; 187 | 188 | for (const item of domain) { 189 | listenersCollection.push(addRequestListener({ 190 | name, 191 | win, 192 | domain: item 193 | }, listener)); 194 | } 195 | 196 | return { 197 | cancel() { 198 | for (const cancelListener of listenersCollection) { 199 | cancelListener.cancel(); 200 | } 201 | } 202 | 203 | }; 204 | } 205 | 206 | const existingListener = getRequestListener({ 207 | name, 208 | win, 209 | domain 210 | }); 211 | 212 | if (!win || win === _conf.WILDCARD) { 213 | win = (0, _global.getWildcard)(); 214 | } 215 | 216 | domain = domain || _conf.WILDCARD; 217 | const strDomain = domain.toString(); 218 | 219 | if (existingListener) { 220 | if (win && domain) { 221 | throw new Error(`Request listener already exists for ${name} on domain ${domain.toString()} for ${win === (0, _global.getWildcard)() ? 'wildcard' : 'specified'} window`); 222 | } else if (win) { 223 | throw new Error(`Request listener already exists for ${name} for ${win === (0, _global.getWildcard)() ? 'wildcard' : 'specified'} window`); 224 | } else if (domain) { 225 | throw new Error(`Request listener already exists for ${name} on domain ${domain.toString()}`); 226 | } else { 227 | throw new Error(`Request listener already exists for ${name}`); 228 | } 229 | } 230 | 231 | const winNameListeners = requestListeners.getOrSet(win, () => ({})); 232 | const winNameDomainListeners = (0, _src2.getOrSet)(winNameListeners, name, () => ({})); 233 | let winNameDomainRegexListeners; 234 | let winNameDomainRegexListener; 235 | 236 | if ((0, _src2.isRegex)(domain)) { 237 | winNameDomainRegexListeners = (0, _src2.getOrSet)(winNameDomainListeners, __DOMAIN_REGEX__, () => []); 238 | winNameDomainRegexListener = { 239 | regex: domain, 240 | listener 241 | }; 242 | winNameDomainRegexListeners.push(winNameDomainRegexListener); 243 | } else { 244 | winNameDomainListeners[strDomain] = listener; 245 | } 246 | 247 | return { 248 | cancel() { 249 | delete winNameDomainListeners[strDomain]; 250 | 251 | if (winNameDomainRegexListener) { 252 | winNameDomainRegexListeners.splice(winNameDomainRegexListeners.indexOf(winNameDomainRegexListener, 1)); 253 | 254 | if (!winNameDomainRegexListeners.length) { 255 | delete winNameDomainListeners[__DOMAIN_REGEX__]; 256 | } 257 | } 258 | 259 | if (!Object.keys(winNameDomainListeners).length) { 260 | delete winNameListeners[name]; 261 | } 262 | 263 | if (win && !Object.keys(winNameListeners).length) { 264 | requestListeners.del(win); 265 | } 266 | } 267 | 268 | }; 269 | } -------------------------------------------------------------------------------- /dist/module/drivers/receive/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | exports.__esModule = true; 4 | exports.listenForMessages = listenForMessages; 5 | exports.messageListener = messageListener; 6 | exports.receiveMessage = receiveMessage; 7 | exports.setupGlobalReceiveMessage = setupGlobalReceiveMessage; 8 | exports.stopListenForMessages = stopListenForMessages; 9 | 10 | var _src = require("@krakenjs/zalgo-promise/src"); 11 | 12 | var _src2 = require("@krakenjs/cross-domain-utils/src"); 13 | 14 | var _src3 = require("@krakenjs/belter/src"); 15 | 16 | var _conf = require("../../conf"); 17 | 18 | var _lib = require("../../lib"); 19 | 20 | var _serialize = require("../../serialize"); 21 | 22 | var _global = require("../../global"); 23 | 24 | var _types = require("./types"); 25 | 26 | function deserializeMessages(message, source, origin, { 27 | on, 28 | send 29 | }) { 30 | let parsedMessage; 31 | 32 | try { 33 | parsedMessage = (0, _serialize.deserializeMessage)(source, origin, message, { 34 | on, 35 | send 36 | }); 37 | } catch (err) { 38 | return; 39 | } 40 | 41 | if (!parsedMessage) { 42 | return; 43 | } 44 | 45 | if (typeof parsedMessage !== 'object' || parsedMessage === null) { 46 | return; 47 | } 48 | 49 | const parseMessages = parsedMessage[(0, _global.getGlobalKey)()]; 50 | 51 | if (!Array.isArray(parseMessages)) { 52 | return; 53 | } 54 | 55 | return parseMessages; 56 | } 57 | 58 | function receiveMessage(event, { 59 | on, 60 | send 61 | }) { 62 | const receivedMessages = (0, _global.globalStore)('receivedMessages'); 63 | 64 | try { 65 | if (!window || window.closed || !event.source) { 66 | return; 67 | } 68 | } catch (err) { 69 | return; 70 | } 71 | 72 | let { 73 | source, 74 | origin, 75 | data 76 | } = event; 77 | 78 | if (__TEST__) { 79 | if ((0, _src2.isWindowClosed)(source)) { 80 | return; 81 | } // $FlowFixMe 82 | 83 | 84 | origin = (0, _src2.getDomain)(source); 85 | } 86 | 87 | const messages = deserializeMessages(data, source, origin, { 88 | on, 89 | send 90 | }); 91 | 92 | if (!messages) { 93 | return; 94 | } 95 | 96 | (0, _lib.markWindowKnown)(source); 97 | 98 | for (const message of messages) { 99 | if (receivedMessages.has(message.id)) { 100 | return; 101 | } 102 | 103 | receivedMessages.set(message.id, true); 104 | 105 | if ((0, _src2.isWindowClosed)(source) && !message.fireAndForget) { 106 | return; 107 | } 108 | 109 | if (message.origin.indexOf(_src2.PROTOCOL.FILE) === 0) { 110 | origin = `${_src2.PROTOCOL.FILE}//`; 111 | } 112 | 113 | try { 114 | if (message.type === _conf.MESSAGE_TYPE.REQUEST) { 115 | (0, _types.handleRequest)(source, origin, message, { 116 | on, 117 | send 118 | }); 119 | } else if (message.type === _conf.MESSAGE_TYPE.RESPONSE) { 120 | (0, _types.handleResponse)(source, origin, message); 121 | } else if (message.type === _conf.MESSAGE_TYPE.ACK) { 122 | (0, _types.handleAck)(source, origin, message); 123 | } 124 | } catch (err) { 125 | setTimeout(() => { 126 | throw err; 127 | }, 0); 128 | } 129 | } 130 | } 131 | 132 | function setupGlobalReceiveMessage({ 133 | on, 134 | send 135 | }) { 136 | const global = (0, _global.getGlobal)(); 137 | 138 | global.receiveMessage = global.receiveMessage || (message => receiveMessage(message, { 139 | on, 140 | send 141 | })); 142 | } 143 | 144 | function messageListener(event, { 145 | on, 146 | send 147 | }) { 148 | _src.ZalgoPromise.try(() => { 149 | try { 150 | (0, _src3.noop)(event.source); 151 | } catch (err) { 152 | return; 153 | } 154 | 155 | const source = event.source || event.sourceElement; 156 | let origin = event.origin || event.originalEvent && event.originalEvent.origin; 157 | const data = event.data; 158 | 159 | if (origin === 'null') { 160 | origin = `${_src2.PROTOCOL.FILE}//`; 161 | } 162 | 163 | if (!source) { 164 | return; 165 | } 166 | 167 | if (!origin) { 168 | throw new Error(`Post message did not have origin domain`); 169 | } 170 | 171 | if (__TEST__) { 172 | if ((0, _lib.needsGlobalMessagingForBrowser)() && (0, _src2.isSameTopWindow)(source, window) === false) { 173 | return; 174 | } 175 | } 176 | 177 | receiveMessage({ 178 | source, 179 | origin, 180 | data 181 | }, { 182 | on, 183 | send 184 | }); 185 | }); 186 | } 187 | 188 | function listenForMessages({ 189 | on, 190 | send 191 | }) { 192 | return (0, _global.globalStore)().getOrSet('postMessageListener', () => { 193 | return (0, _src3.addEventListener)(window, 'message', event => { 194 | // $FlowFixMe 195 | messageListener(event, { 196 | on, 197 | send 198 | }); 199 | }); 200 | }); 201 | } 202 | 203 | function stopListenForMessages() { 204 | const listener = (0, _global.globalStore)().get('postMessageListener'); 205 | 206 | if (listener) { 207 | listener.cancel(); 208 | } 209 | } -------------------------------------------------------------------------------- /dist/module/drivers/receive/types.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | exports.__esModule = true; 4 | exports.handleAck = handleAck; 5 | exports.handleRequest = handleRequest; 6 | exports.handleResponse = handleResponse; 7 | 8 | var _src = require("@krakenjs/zalgo-promise/src"); 9 | 10 | var _src2 = require("@krakenjs/cross-domain-utils/src"); 11 | 12 | var _src3 = require("@krakenjs/belter/src"); 13 | 14 | var _conf = require("../../conf"); 15 | 16 | var _send = require("../send"); 17 | 18 | var _listeners = require("../listeners"); 19 | 20 | function handleRequest(source, origin, message, { 21 | on, 22 | send 23 | }) { 24 | const options = (0, _listeners.getRequestListener)({ 25 | name: message.name, 26 | win: source, 27 | domain: origin 28 | }); 29 | const logName = message.name === _conf.MESSAGE_NAME.METHOD && message.data && typeof message.data.name === 'string' ? `${message.data.name}()` : message.name; 30 | 31 | if (__DEBUG__) { 32 | // eslint-disable-next-line no-console 33 | console.info('receive::req', logName, origin, '\n\n', message.data); 34 | } 35 | 36 | function sendAck() { 37 | return _src.ZalgoPromise.flush().then(() => { 38 | if (message.fireAndForget || (0, _src2.isWindowClosed)(source)) { 39 | return; 40 | } 41 | 42 | try { 43 | return (0, _send.sendMessage)(source, origin, { 44 | id: (0, _src3.uniqueID)(), 45 | origin: (0, _src2.getDomain)(window), 46 | type: _conf.MESSAGE_TYPE.ACK, 47 | hash: message.hash, 48 | name: message.name 49 | }, { 50 | on, 51 | send 52 | }); 53 | } catch (err) { 54 | throw new Error(`Send ack message failed for ${logName} in ${(0, _src2.getDomain)()}\n\n${(0, _src3.stringifyError)(err)}`); 55 | } 56 | }); 57 | } 58 | 59 | function sendResponse(ack, data, error) { 60 | return _src.ZalgoPromise.flush().then(() => { 61 | if (message.fireAndForget || (0, _src2.isWindowClosed)(source)) { 62 | return; 63 | } 64 | 65 | if (__DEBUG__) { 66 | if (ack === _conf.MESSAGE_ACK.SUCCESS) { 67 | console.info('respond::res', logName, origin, '\n\n', data); // eslint-disable-line no-console 68 | } else if (ack === _conf.MESSAGE_ACK.ERROR) { 69 | console.error('respond::err', logName, origin, '\n\n', error); // eslint-disable-line no-console 70 | } 71 | } 72 | 73 | try { 74 | return (0, _send.sendMessage)(source, origin, { 75 | id: (0, _src3.uniqueID)(), 76 | origin: (0, _src2.getDomain)(window), 77 | type: _conf.MESSAGE_TYPE.RESPONSE, 78 | hash: message.hash, 79 | name: message.name, 80 | ack, 81 | data, 82 | error 83 | }, { 84 | on, 85 | send 86 | }); 87 | } catch (err) { 88 | throw new Error(`Send response message failed for ${logName} in ${(0, _src2.getDomain)()}\n\n${(0, _src3.stringifyError)(err)}`); 89 | } 90 | }); 91 | } 92 | 93 | return _src.ZalgoPromise.all([sendAck(), _src.ZalgoPromise.try(() => { 94 | if (!options) { 95 | throw new Error(`No handler found for post message: ${message.name} from ${origin} in ${window.location.protocol}//${window.location.host}${window.location.pathname}`); 96 | } 97 | 98 | const data = message.data; 99 | return options.handler({ 100 | source, 101 | origin, 102 | data 103 | }); 104 | }).then(data => { 105 | return sendResponse(_conf.MESSAGE_ACK.SUCCESS, data); 106 | }, error => { 107 | return sendResponse(_conf.MESSAGE_ACK.ERROR, null, error); 108 | })]).then(_src3.noop).catch(err => { 109 | if (options && options.handleError) { 110 | return options.handleError(err); 111 | } else { 112 | throw err; 113 | } 114 | }); 115 | } 116 | 117 | function handleAck(source, origin, message) { 118 | if ((0, _listeners.isResponseListenerErrored)(message.hash)) { 119 | return; 120 | } 121 | 122 | const options = (0, _listeners.getResponseListener)(message.hash); 123 | 124 | if (!options) { 125 | throw new Error(`No handler found for post message ack for message: ${message.name} from ${origin} in ${window.location.protocol}//${window.location.host}${window.location.pathname}`); 126 | } 127 | 128 | try { 129 | if (!(0, _src2.matchDomain)(options.domain, origin)) { 130 | throw new Error(`Ack origin ${origin} does not match domain ${options.domain.toString()}`); 131 | } 132 | 133 | if (source !== options.win) { 134 | throw new Error(`Ack source does not match registered window`); 135 | } 136 | } catch (err) { 137 | options.promise.reject(err); 138 | } 139 | 140 | options.ack = true; 141 | } 142 | 143 | function handleResponse(source, origin, message) { 144 | if ((0, _listeners.isResponseListenerErrored)(message.hash)) { 145 | return; 146 | } 147 | 148 | const options = (0, _listeners.getResponseListener)(message.hash); 149 | 150 | if (!options) { 151 | throw new Error(`No handler found for post message response for message: ${message.name} from ${origin} in ${window.location.protocol}//${window.location.host}${window.location.pathname}`); 152 | } 153 | 154 | if (!(0, _src2.matchDomain)(options.domain, origin)) { 155 | throw new Error(`Response origin ${origin} does not match domain ${(0, _src2.stringifyDomainPattern)(options.domain)}`); 156 | } 157 | 158 | if (source !== options.win) { 159 | throw new Error(`Response source does not match registered window`); 160 | } 161 | 162 | (0, _listeners.deleteResponseListener)(message.hash); 163 | const logName = message.name === _conf.MESSAGE_NAME.METHOD && message.data && typeof message.data.name === 'string' ? `${message.data.name}()` : message.name; 164 | 165 | if (message.ack === _conf.MESSAGE_ACK.ERROR) { 166 | if (__DEBUG__) { 167 | console.error('receive::err', logName, origin, '\n\n', message.error); // eslint-disable-line no-console 168 | } 169 | 170 | options.promise.reject(message.error); 171 | } else if (message.ack === _conf.MESSAGE_ACK.SUCCESS) { 172 | if (__DEBUG__) { 173 | console.info('receive::res', logName, origin, '\n\n', message.data); // eslint-disable-line no-console 174 | } 175 | 176 | options.promise.resolve({ 177 | source, 178 | origin, 179 | data: message.data 180 | }); 181 | } 182 | } -------------------------------------------------------------------------------- /dist/module/drivers/send/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | exports.__esModule = true; 4 | exports.sendMessage = sendMessage; 5 | 6 | var _src = require("@krakenjs/zalgo-promise/src"); 7 | 8 | var _src2 = require("@krakenjs/cross-domain-utils/src"); 9 | 10 | var _src3 = require("@krakenjs/belter/src"); 11 | 12 | var _serialize = require("../../serialize"); 13 | 14 | var _global = require("../../global"); 15 | 16 | var _strategies = require("./strategies"); 17 | 18 | function packMessages(messages) { 19 | return { 20 | [(0, _global.getGlobalKey)()]: messages 21 | }; 22 | } 23 | 24 | function sendMessage(win, domain, message, { 25 | on, 26 | send 27 | }) { 28 | return _src.ZalgoPromise.try(() => { 29 | const messageBuffer = (0, _global.windowStore)(); 30 | const domainBuffer = messageBuffer.getOrSet(win, () => ({})); 31 | domainBuffer.buffer = domainBuffer.buffer || []; 32 | domainBuffer.buffer.push(message); 33 | domainBuffer.flush = domainBuffer.flush || _src.ZalgoPromise.flush().then(() => { 34 | if ((0, _src2.isWindowClosed)(win)) { 35 | throw new Error('Window is closed'); 36 | } 37 | 38 | const serializedMessage = (0, _serialize.serializeMessage)(win, domain, packMessages(domainBuffer.buffer || []), { 39 | on, 40 | send 41 | }); 42 | delete domainBuffer.buffer; 43 | const strategies = Object.keys(_strategies.SEND_MESSAGE_STRATEGIES); 44 | const errors = []; 45 | 46 | for (const strategyName of strategies) { 47 | try { 48 | _strategies.SEND_MESSAGE_STRATEGIES[strategyName](win, serializedMessage, domain); 49 | } catch (err) { 50 | errors.push(err); 51 | } 52 | } 53 | 54 | if (errors.length === strategies.length) { 55 | throw new Error(`All post-robot messaging strategies failed:\n\n${errors.map((err, i) => `${i}. ${(0, _src3.stringifyError)(err)}`).join('\n\n')}`); 56 | } 57 | }); 58 | return domainBuffer.flush.then(() => { 59 | delete domainBuffer.flush; 60 | }); 61 | }).then(_src3.noop); 62 | } -------------------------------------------------------------------------------- /dist/module/drivers/send/strategies.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | exports.__esModule = true; 4 | exports.SEND_MESSAGE_STRATEGIES = void 0; 5 | 6 | var _src = require("@krakenjs/cross-domain-utils/src"); 7 | 8 | var _conf = require("../../conf"); 9 | 10 | var _lib = require("../../lib"); 11 | 12 | var _global = require("../../global"); 13 | 14 | var _bridge = require("../../bridge"); 15 | 16 | const SEND_MESSAGE_STRATEGIES = {}; 17 | exports.SEND_MESSAGE_STRATEGIES = SEND_MESSAGE_STRATEGIES; 18 | 19 | SEND_MESSAGE_STRATEGIES[_conf.SEND_STRATEGY.POST_MESSAGE] = (win, serializedMessage, domain) => { 20 | if (domain.indexOf(_src.PROTOCOL.FILE) === 0) { 21 | domain = _conf.WILDCARD; 22 | } 23 | 24 | if (__TEST__) { 25 | if ((0, _lib.needsGlobalMessagingForBrowser)() && (0, _src.isSameTopWindow)(window, win) === false) { 26 | return; 27 | } 28 | 29 | if (domain.indexOf(_src.PROTOCOL.MOCK) === 0) { 30 | if (!(0, _src.isActuallySameDomain)(win)) { 31 | throw new Error(`Attempting to send message to mock domain ${domain}, but window is actually cross-domain`); 32 | } // $FlowFixMe 33 | 34 | 35 | const windowDomain = (0, _src.getDomain)(win); 36 | 37 | if (windowDomain !== domain) { 38 | throw new Error(`Mock domain target ${domain} does not match window domain ${windowDomain}`); 39 | } // $FlowFixMe 40 | 41 | 42 | domain = (0, _src.getActualDomain)(win); 43 | } 44 | } 45 | 46 | win.postMessage(serializedMessage, domain); 47 | }; 48 | 49 | if (__POST_ROBOT__.__IE_POPUP_SUPPORT__) { 50 | SEND_MESSAGE_STRATEGIES[_conf.SEND_STRATEGY.BRIDGE] = (win, serializedMessage, domain) => { 51 | if (!(0, _bridge.needsBridgeForBrowser)() && !(0, _bridge.isBridge)()) { 52 | throw new Error(`Bridge not needed for browser`); 53 | } 54 | 55 | if ((0, _src.isSameDomain)(win)) { 56 | throw new Error(`Post message through bridge disabled between same domain windows`); 57 | } 58 | 59 | if ((0, _src.isSameTopWindow)(window, win) !== false) { 60 | throw new Error(`Can only use bridge to communicate between two different windows, not between frames`); 61 | } 62 | 63 | (0, _bridge.sendBridgeMessage)(win, domain, serializedMessage); 64 | }; 65 | } 66 | 67 | if (__POST_ROBOT__.__IE_POPUP_SUPPORT__ || __POST_ROBOT__.__GLOBAL_MESSAGE_SUPPORT__) { 68 | SEND_MESSAGE_STRATEGIES[_conf.SEND_STRATEGY.GLOBAL] = (win, serializedMessage) => { 69 | if (!(0, _lib.needsGlobalMessagingForBrowser)()) { 70 | throw new Error(`Global messaging not needed for browser`); 71 | } 72 | 73 | if (!(0, _src.isSameDomain)(win)) { 74 | throw new Error(`Post message through global disabled between different domain windows`); 75 | } 76 | 77 | if ((0, _src.isSameTopWindow)(window, win) !== false) { 78 | throw new Error(`Can only use global to communicate between two different windows, not between frames`); 79 | } // $FlowFixMe 80 | 81 | 82 | const foreignGlobal = (0, _global.getGlobal)(win); 83 | 84 | if (!foreignGlobal) { 85 | throw new Error(`Can not find postRobot global on foreign window`); 86 | } 87 | 88 | foreignGlobal.receiveMessage({ 89 | source: window, 90 | origin: (0, _src.getDomain)(), 91 | data: serializedMessage 92 | }); 93 | }; 94 | } -------------------------------------------------------------------------------- /dist/module/drivers/types.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _conf = require("../conf"); -------------------------------------------------------------------------------- /dist/module/global.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | exports.__esModule = true; 4 | exports.WildCard = void 0; 5 | exports.deleteGlobal = deleteGlobal; 6 | exports.getGlobal = getGlobal; 7 | exports.getGlobalKey = getGlobalKey; 8 | exports.getWildcard = getWildcard; 9 | exports.globalStore = globalStore; 10 | exports.windowStore = windowStore; 11 | 12 | var _src = require("@krakenjs/cross-domain-safe-weakmap/src"); 13 | 14 | var _src2 = require("@krakenjs/belter/src"); 15 | 16 | function getGlobalKey() { 17 | if (__POST_ROBOT__.__SCRIPT_NAMESPACE__) { 18 | return `${__POST_ROBOT__.__GLOBAL_KEY__}_${(0, _src2.getCurrentScriptUID)()}`; 19 | } else { 20 | return __POST_ROBOT__.__GLOBAL_KEY__; 21 | } 22 | } 23 | 24 | function getGlobal(win = window) { 25 | const globalKey = getGlobalKey(); 26 | 27 | if (win !== window) { 28 | return win[globalKey]; 29 | } 30 | 31 | const global = win[globalKey] = win[globalKey] || {}; 32 | return global; 33 | } 34 | 35 | function deleteGlobal() { 36 | const globalKey = getGlobalKey(); 37 | delete window[globalKey]; 38 | } 39 | 40 | const getObj = () => ({}); 41 | 42 | function globalStore(key = 'store', defStore = getObj) { 43 | return (0, _src2.getOrSet)(getGlobal(), key, () => { 44 | let store = defStore(); 45 | return { 46 | has: storeKey => { 47 | return store.hasOwnProperty(storeKey); 48 | }, 49 | get: (storeKey, defVal) => { 50 | // $FlowFixMe 51 | return store.hasOwnProperty(storeKey) ? store[storeKey] : defVal; 52 | }, 53 | set: (storeKey, val) => { 54 | store[storeKey] = val; 55 | return val; 56 | }, 57 | del: storeKey => { 58 | delete store[storeKey]; 59 | }, 60 | getOrSet: (storeKey, getter) => { 61 | // $FlowFixMe 62 | return (0, _src2.getOrSet)(store, storeKey, getter); 63 | }, 64 | reset: () => { 65 | store = defStore(); 66 | }, 67 | keys: () => { 68 | return Object.keys(store); 69 | } 70 | }; 71 | }); 72 | } 73 | 74 | class WildCard {} 75 | 76 | exports.WildCard = WildCard; 77 | 78 | function getWildcard() { 79 | const global = getGlobal(); 80 | global.WINDOW_WILDCARD = global.WINDOW_WILDCARD || new WildCard(); 81 | return global.WINDOW_WILDCARD; 82 | } 83 | 84 | function windowStore(key = 'store', defStore = getObj) { 85 | return globalStore('windowStore').getOrSet(key, () => { 86 | const winStore = new _src.WeakMap(); 87 | 88 | const getStore = win => { 89 | return winStore.getOrSet(win, defStore); 90 | }; 91 | 92 | return { 93 | has: win => { 94 | const store = getStore(win); 95 | return store.hasOwnProperty(key); 96 | }, 97 | get: (win, defVal) => { 98 | const store = getStore(win); // $FlowFixMe 99 | 100 | return store.hasOwnProperty(key) ? store[key] : defVal; 101 | }, 102 | set: (win, val) => { 103 | const store = getStore(win); 104 | store[key] = val; 105 | return val; 106 | }, 107 | del: win => { 108 | const store = getStore(win); 109 | delete store[key]; 110 | }, 111 | getOrSet: (win, getter) => { 112 | const store = getStore(win); 113 | return (0, _src2.getOrSet)(store, key, getter); 114 | } 115 | }; 116 | }); 117 | } -------------------------------------------------------------------------------- /dist/module/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | exports.__esModule = true; 4 | var _exportNames = { 5 | Promise: true, 6 | ProxyWindow: true, 7 | destroy: true, 8 | serializeMessage: true, 9 | deserializeMessage: true, 10 | createProxyWindow: true, 11 | toProxyWindow: true, 12 | on: true, 13 | once: true, 14 | send: true, 15 | markWindowKnown: true, 16 | cleanUpWindow: true, 17 | bridge: true, 18 | setup: true 19 | }; 20 | exports.toProxyWindow = exports.serializeMessage = exports.send = exports.once = exports.on = exports.markWindowKnown = exports.destroy = exports.deserializeMessage = exports.createProxyWindow = exports.cleanUpWindow = exports.bridge = exports.ProxyWindow = exports.Promise = void 0; 21 | 22 | var _setup = require("./setup"); 23 | 24 | exports.setup = _setup.setup; 25 | exports.destroy = _setup.destroy; 26 | exports.serializeMessage = _setup.serializeMessage; 27 | exports.deserializeMessage = _setup.deserializeMessage; 28 | exports.createProxyWindow = _setup.createProxyWindow; 29 | exports.toProxyWindow = _setup.toProxyWindow; 30 | 31 | var _bridge = require("./bridge"); 32 | 33 | var _src = require("@krakenjs/zalgo-promise/src"); 34 | 35 | exports.Promise = _src.ZalgoPromise; 36 | 37 | var _types = require("./types"); 38 | 39 | Object.keys(_types).forEach(function (key) { 40 | if (key === "default" || key === "__esModule") return; 41 | if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; 42 | if (key in exports && exports[key] === _types[key]) return; 43 | exports[key] = _types[key]; 44 | }); 45 | 46 | var _serialize = require("./serialize"); 47 | 48 | exports.ProxyWindow = _serialize.ProxyWindow; 49 | 50 | var _public = require("./public"); 51 | 52 | exports.on = _public.on; 53 | exports.once = _public.once; 54 | exports.send = _public.send; 55 | 56 | var _lib = require("./lib"); 57 | 58 | exports.markWindowKnown = _lib.markWindowKnown; 59 | 60 | var _clean = require("./clean"); 61 | 62 | exports.cleanUpWindow = _clean.cleanUpWindow; 63 | // $FlowFixMe 64 | let bridge; 65 | exports.bridge = bridge; 66 | 67 | if (__POST_ROBOT__.__IE_POPUP_SUPPORT__) { 68 | exports.bridge = bridge = { 69 | setupBridge: _bridge.setupBridge, 70 | openBridge: _bridge.openBridge, 71 | linkWindow: _bridge.linkWindow, 72 | linkUrl: _bridge.linkUrl, 73 | isBridge: _bridge.isBridge, 74 | needsBridge: _bridge.needsBridge, 75 | needsBridgeForBrowser: _bridge.needsBridgeForBrowser, 76 | hasBridge: _bridge.hasBridge, 77 | needsBridgeForWin: _bridge.needsBridgeForWin, 78 | needsBridgeForDomain: _bridge.needsBridgeForDomain, 79 | destroyBridges: _bridge.destroyBridges 80 | }; 81 | } 82 | 83 | if (__POST_ROBOT__.__AUTO_SETUP__) { 84 | (0, _setup.setup)(); 85 | } -------------------------------------------------------------------------------- /dist/module/lib/compat.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | exports.__esModule = true; 4 | exports.needsGlobalMessagingForBrowser = needsGlobalMessagingForBrowser; 5 | 6 | var _src = require("@krakenjs/cross-domain-utils/src"); 7 | 8 | function needsGlobalMessagingForBrowser() { 9 | if ((0, _src.getUserAgent)(window).match(/MSIE|rv:11|trident|edge\/12|edge\/13/i)) { 10 | return true; 11 | } 12 | 13 | return false; 14 | } -------------------------------------------------------------------------------- /dist/module/lib/hello.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | exports.__esModule = true; 4 | exports.awaitWindowHello = awaitWindowHello; 5 | exports.getWindowInstanceID = getWindowInstanceID; 6 | exports.initHello = initHello; 7 | exports.sayHello = sayHello; 8 | 9 | var _src = require("@krakenjs/cross-domain-utils/src"); 10 | 11 | var _src2 = require("@krakenjs/zalgo-promise/src"); 12 | 13 | var _src3 = require("@krakenjs/belter/src"); 14 | 15 | var _conf = require("../conf"); 16 | 17 | var _global = require("../global"); 18 | 19 | function getInstanceID() { 20 | return (0, _global.globalStore)('instance').getOrSet('instanceID', _src3.uniqueID); 21 | } 22 | 23 | function getHelloPromise(win) { 24 | const helloPromises = (0, _global.windowStore)('helloPromises'); 25 | return helloPromises.getOrSet(win, () => new _src2.ZalgoPromise()); 26 | } 27 | 28 | function resolveHelloPromise(win, { 29 | domain 30 | }) { 31 | const helloPromises = (0, _global.windowStore)('helloPromises'); 32 | const existingPromise = helloPromises.get(win); 33 | 34 | if (existingPromise) { 35 | existingPromise.resolve({ 36 | domain 37 | }); 38 | } 39 | 40 | const newPromise = _src2.ZalgoPromise.resolve({ 41 | domain 42 | }); 43 | 44 | helloPromises.set(win, newPromise); 45 | return newPromise; 46 | } 47 | 48 | function listenForHello({ 49 | on 50 | }) { 51 | return on(_conf.MESSAGE_NAME.HELLO, { 52 | domain: _conf.WILDCARD 53 | }, ({ 54 | source, 55 | origin 56 | }) => { 57 | resolveHelloPromise(source, { 58 | domain: origin 59 | }); 60 | return { 61 | instanceID: getInstanceID() 62 | }; 63 | }); 64 | } 65 | 66 | function sayHello(win, { 67 | send 68 | }) { 69 | return send(win, _conf.MESSAGE_NAME.HELLO, { 70 | instanceID: getInstanceID() 71 | }, { 72 | domain: _conf.WILDCARD, 73 | timeout: -1 74 | }).then(({ 75 | origin, 76 | data: { 77 | instanceID 78 | } 79 | }) => { 80 | resolveHelloPromise(win, { 81 | domain: origin 82 | }); 83 | return { 84 | win, 85 | domain: origin, 86 | instanceID 87 | }; 88 | }); 89 | } 90 | 91 | function getWindowInstanceID(win, { 92 | send 93 | }) { 94 | return (0, _global.windowStore)('windowInstanceIDPromises').getOrSet(win, () => { 95 | return sayHello(win, { 96 | send 97 | }).then(({ 98 | instanceID 99 | }) => instanceID); 100 | }); 101 | } 102 | 103 | function initHello({ 104 | on, 105 | send 106 | }) { 107 | return (0, _global.globalStore)('builtinListeners').getOrSet('helloListener', () => { 108 | const listener = listenForHello({ 109 | on 110 | }); 111 | const parent = (0, _src.getAncestor)(); 112 | 113 | if (parent) { 114 | sayHello(parent, { 115 | send 116 | }).catch(err => { 117 | // $FlowFixMe 118 | if (__TEST__ && (0, _global.getGlobal)(parent)) { 119 | throw err; 120 | } 121 | }); 122 | } 123 | 124 | return listener; 125 | }); 126 | } 127 | 128 | function awaitWindowHello(win, timeout = 5000, name = 'Window') { 129 | let promise = getHelloPromise(win); 130 | 131 | if (timeout !== -1) { 132 | promise = promise.timeout(timeout, new Error(`${name} did not load after ${timeout}ms`)); 133 | } 134 | 135 | return promise; 136 | } -------------------------------------------------------------------------------- /dist/module/lib/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | exports.__esModule = true; 4 | 5 | var _hello = require("./hello"); 6 | 7 | Object.keys(_hello).forEach(function (key) { 8 | if (key === "default" || key === "__esModule") return; 9 | if (key in exports && exports[key] === _hello[key]) return; 10 | exports[key] = _hello[key]; 11 | }); 12 | 13 | var _compat = require("./compat"); 14 | 15 | Object.keys(_compat).forEach(function (key) { 16 | if (key === "default" || key === "__esModule") return; 17 | if (key in exports && exports[key] === _compat[key]) return; 18 | exports[key] = _compat[key]; 19 | }); 20 | 21 | var _windows = require("./windows"); 22 | 23 | Object.keys(_windows).forEach(function (key) { 24 | if (key === "default" || key === "__esModule") return; 25 | if (key in exports && exports[key] === _windows[key]) return; 26 | exports[key] = _windows[key]; 27 | }); -------------------------------------------------------------------------------- /dist/module/lib/windows.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | exports.__esModule = true; 4 | exports.isWindowKnown = isWindowKnown; 5 | exports.markWindowKnown = markWindowKnown; 6 | 7 | var _global = require("../global"); 8 | 9 | function markWindowKnown(win) { 10 | const knownWindows = (0, _global.windowStore)('knownWindows'); 11 | knownWindows.set(win, true); 12 | } 13 | 14 | function isWindowKnown(win) { 15 | const knownWindows = (0, _global.windowStore)('knownWindows'); 16 | return knownWindows.get(win, false); 17 | } -------------------------------------------------------------------------------- /dist/module/public/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | exports.__esModule = true; 4 | 5 | var _on = require("./on"); 6 | 7 | Object.keys(_on).forEach(function (key) { 8 | if (key === "default" || key === "__esModule") return; 9 | if (key in exports && exports[key] === _on[key]) return; 10 | exports[key] = _on[key]; 11 | }); 12 | 13 | var _send = require("./send"); 14 | 15 | Object.keys(_send).forEach(function (key) { 16 | if (key === "default" || key === "__esModule") return; 17 | if (key in exports && exports[key] === _send[key]) return; 18 | exports[key] = _send[key]; 19 | }); -------------------------------------------------------------------------------- /dist/module/public/on.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | exports.__esModule = true; 4 | exports.on = on; 5 | exports.once = once; 6 | 7 | var _src = require("@krakenjs/zalgo-promise/src"); 8 | 9 | var _drivers = require("../drivers"); 10 | 11 | var _conf = require("../conf"); 12 | 13 | const getDefaultServerOptions = () => { 14 | // $FlowFixMe 15 | return {}; 16 | }; 17 | 18 | function on(name, options, handler) { 19 | if (!name) { 20 | throw new Error('Expected name'); 21 | } 22 | 23 | options = options || getDefaultServerOptions(); 24 | 25 | if (typeof options === 'function') { 26 | handler = options; 27 | options = getDefaultServerOptions(); 28 | } 29 | 30 | if (!handler) { 31 | throw new Error('Expected handler'); 32 | } 33 | 34 | const winOrProxyWin = options.window; 35 | const domain = options.domain || _conf.WILDCARD; 36 | const successHandler = handler || options.handler; 37 | 38 | const errorHandler = options.errorHandler || (err => { 39 | throw err; 40 | }); 41 | 42 | const requestListener = (0, _drivers.addRequestListener)({ 43 | name, 44 | win: winOrProxyWin, 45 | domain 46 | }, { 47 | handler: successHandler, 48 | handleError: errorHandler 49 | }); 50 | return { 51 | cancel() { 52 | requestListener.cancel(); 53 | } 54 | 55 | }; 56 | } 57 | 58 | function once(name, options, handler) { 59 | options = options || getDefaultServerOptions(); 60 | 61 | if (typeof options === 'function') { 62 | handler = options; 63 | options = getDefaultServerOptions(); 64 | } 65 | 66 | const promise = new _src.ZalgoPromise(); 67 | let listener; // eslint-disable-line prefer-const 68 | 69 | options.errorHandler = err => { 70 | listener.cancel(); 71 | promise.reject(err); 72 | }; 73 | 74 | listener = on(name, options, event => { 75 | listener.cancel(); 76 | promise.resolve(event); 77 | 78 | if (handler) { 79 | return handler(event); 80 | } 81 | }); // $FlowFixMe 82 | 83 | promise.cancel = listener.cancel; // $FlowFixMe 84 | 85 | return promise; 86 | } -------------------------------------------------------------------------------- /dist/module/public/send.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | exports.__esModule = true; 4 | exports.send = void 0; 5 | 6 | var _src = require("@krakenjs/zalgo-promise/src"); 7 | 8 | var _src2 = require("@krakenjs/cross-domain-utils/src"); 9 | 10 | var _src3 = require("@krakenjs/belter/src"); 11 | 12 | var _conf = require("../conf"); 13 | 14 | var _drivers = require("../drivers"); 15 | 16 | var _lib = require("../lib"); 17 | 18 | var _global = require("../global"); 19 | 20 | var _window = require("../serialize/window"); 21 | 22 | var _on = require("./on"); 23 | 24 | function validateOptions(name, win, domain) { 25 | if (!name) { 26 | throw new Error('Expected name'); 27 | } 28 | 29 | if (domain) { 30 | if (typeof domain !== 'string' && !Array.isArray(domain) && !(0, _src3.isRegex)(domain)) { 31 | throw new TypeError(`Can not send ${name}. Expected domain ${JSON.stringify(domain)} to be a string, array, or regex`); 32 | } 33 | } 34 | 35 | if ((0, _src2.isWindowClosed)(win)) { 36 | throw new Error(`Can not send ${name}. Target window is closed`); 37 | } 38 | } 39 | 40 | function normalizeDomain(win, targetDomain, actualDomain, { 41 | send 42 | }) { 43 | return _src.ZalgoPromise.try(() => { 44 | if (typeof targetDomain === 'string') { 45 | return targetDomain; 46 | } 47 | 48 | return _src.ZalgoPromise.try(() => { 49 | return actualDomain || (0, _lib.sayHello)(win, { 50 | send 51 | }).then(({ 52 | domain 53 | }) => domain); 54 | }).then(normalizedDomain => { 55 | if (!(0, _src2.matchDomain)(targetDomain, targetDomain)) { 56 | throw new Error(`Domain ${(0, _src3.stringify)(targetDomain)} does not match ${(0, _src3.stringify)(targetDomain)}`); 57 | } 58 | 59 | return normalizedDomain; 60 | }); 61 | }); 62 | } 63 | 64 | const send = (winOrProxyWin, name, data, options) => { 65 | options = options || {}; 66 | const domainMatcher = options.domain || _conf.WILDCARD; 67 | const responseTimeout = options.timeout || _conf.RES_TIMEOUT; 68 | const childTimeout = options.timeout || _conf.CHILD_WINDOW_TIMEOUT; 69 | const fireAndForget = options.fireAndForget || false; 70 | return _window.ProxyWindow.toProxyWindow(winOrProxyWin, { 71 | send 72 | }).awaitWindow().then(win => { 73 | // $FlowFixMe 74 | return _src.ZalgoPromise.try(() => { 75 | validateOptions(name, win, domainMatcher); 76 | 77 | if ((0, _src2.isAncestor)(window, win)) { 78 | return (0, _lib.awaitWindowHello)(win, childTimeout); 79 | } 80 | }).then(({ 81 | domain: actualDomain 82 | } = {}) => { 83 | return normalizeDomain(win, domainMatcher, actualDomain, { 84 | send 85 | }); 86 | }).then(targetDomain => { 87 | const domain = targetDomain; 88 | const logName = name === _conf.MESSAGE_NAME.METHOD && data && typeof data.name === 'string' ? `${data.name}()` : name; 89 | 90 | if (__DEBUG__) { 91 | console.info('send::req', logName, domain, '\n\n', data); // eslint-disable-line no-console 92 | } 93 | 94 | const promise = new _src.ZalgoPromise(); 95 | const hash = `${name}_${(0, _src3.uniqueID)()}`; 96 | 97 | if (!fireAndForget) { 98 | const responseListener = { 99 | name, 100 | win, 101 | domain, 102 | promise 103 | }; 104 | (0, _drivers.addResponseListener)(hash, responseListener); 105 | const reqPromises = (0, _global.windowStore)('requestPromises').getOrSet(win, () => []); 106 | reqPromises.push(promise); 107 | promise.catch(() => { 108 | (0, _drivers.markResponseListenerErrored)(hash); 109 | (0, _drivers.deleteResponseListener)(hash); 110 | }); 111 | const totalAckTimeout = (0, _lib.isWindowKnown)(win) ? _conf.ACK_TIMEOUT_KNOWN : _conf.ACK_TIMEOUT; 112 | const totalResTimeout = responseTimeout; 113 | let ackTimeout = totalAckTimeout; 114 | let resTimeout = totalResTimeout; 115 | const interval = (0, _src3.safeInterval)(() => { 116 | if ((0, _src2.isWindowClosed)(win)) { 117 | return promise.reject(new Error(`Window closed for ${name} before ${responseListener.ack ? 'response' : 'ack'}`)); 118 | } 119 | 120 | if (responseListener.cancelled) { 121 | return promise.reject(new Error(`Response listener was cancelled for ${name}`)); 122 | } 123 | 124 | ackTimeout = Math.max(ackTimeout - _conf.RESPONSE_CYCLE_TIME, 0); 125 | 126 | if (resTimeout !== -1) { 127 | resTimeout = Math.max(resTimeout - _conf.RESPONSE_CYCLE_TIME, 0); 128 | } 129 | 130 | if (!responseListener.ack && ackTimeout === 0) { 131 | return promise.reject(new Error(`No ack for postMessage ${logName} in ${(0, _src2.getDomain)()} in ${totalAckTimeout}ms`)); 132 | } else if (resTimeout === 0) { 133 | return promise.reject(new Error(`No response for postMessage ${logName} in ${(0, _src2.getDomain)()} in ${totalResTimeout}ms`)); 134 | } 135 | }, _conf.RESPONSE_CYCLE_TIME); 136 | promise.finally(() => { 137 | interval.cancel(); 138 | reqPromises.splice(reqPromises.indexOf(promise, 1)); 139 | }).catch(_src3.noop); 140 | } 141 | 142 | return (0, _drivers.sendMessage)(win, domain, { 143 | id: (0, _src3.uniqueID)(), 144 | origin: (0, _src2.getDomain)(window), 145 | type: _conf.MESSAGE_TYPE.REQUEST, 146 | hash, 147 | name, 148 | data, 149 | fireAndForget 150 | }, { 151 | on: _on.on, 152 | send 153 | }).then(() => { 154 | return fireAndForget ? promise.resolve() : promise; 155 | }, err => { 156 | throw new Error(`Send request message failed for ${logName} in ${(0, _src2.getDomain)()}\n\n${(0, _src3.stringifyError)(err)}`); 157 | }); 158 | }); 159 | }); 160 | }; 161 | 162 | exports.send = send; -------------------------------------------------------------------------------- /dist/module/serialize/function.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | exports.__esModule = true; 4 | exports.deserializeFunction = deserializeFunction; 5 | exports.serializeFunction = serializeFunction; 6 | 7 | var _src = require("@krakenjs/cross-domain-utils/src"); 8 | 9 | var _src2 = require("@krakenjs/zalgo-promise/src"); 10 | 11 | var _src3 = require("@krakenjs/belter/src"); 12 | 13 | var _src4 = require("@krakenjs/universal-serialize/src"); 14 | 15 | var _conf = require("../conf"); 16 | 17 | var _global = require("../global"); 18 | 19 | var _window = require("./window"); 20 | 21 | function addMethod(id, val, name, source, domain) { 22 | const methodStore = (0, _global.windowStore)('methodStore'); 23 | const proxyWindowMethods = (0, _global.globalStore)('proxyWindowMethods'); 24 | 25 | if (_window.ProxyWindow.isProxyWindow(source)) { 26 | proxyWindowMethods.set(id, { 27 | val, 28 | name, 29 | domain, 30 | source 31 | }); 32 | } else { 33 | proxyWindowMethods.del(id); // $FlowFixMe 34 | 35 | const methods = methodStore.getOrSet(source, () => ({})); 36 | methods[id] = { 37 | domain, 38 | name, 39 | val, 40 | source 41 | }; 42 | } 43 | } 44 | 45 | function lookupMethod(source, id) { 46 | const methodStore = (0, _global.windowStore)('methodStore'); 47 | const proxyWindowMethods = (0, _global.globalStore)('proxyWindowMethods'); 48 | const methods = methodStore.getOrSet(source, () => ({})); 49 | return methods[id] || proxyWindowMethods.get(id); 50 | } 51 | 52 | function stringifyArguments(args = []) { 53 | return (0, _src3.arrayFrom)(args).map(arg => { 54 | if (typeof arg === 'string') { 55 | return `'${arg}'`; 56 | } 57 | 58 | if (arg === undefined) { 59 | return 'undefined'; 60 | } 61 | 62 | if (arg === null) { 63 | return 'null'; 64 | } 65 | 66 | if (typeof arg === 'boolean') { 67 | return arg.toString(); 68 | } 69 | 70 | if (Array.isArray(arg)) { 71 | return '[ ... ]'; 72 | } 73 | 74 | if (typeof arg === 'object') { 75 | return '{ ... }'; 76 | } 77 | 78 | if (typeof arg === 'function') { 79 | return '() => { ... }'; 80 | } 81 | 82 | return `<${typeof arg}>`; 83 | }).join(', '); 84 | } 85 | 86 | function listenForFunctionCalls({ 87 | on, 88 | send 89 | }) { 90 | return (0, _global.globalStore)('builtinListeners').getOrSet('functionCalls', () => { 91 | return on(_conf.MESSAGE_NAME.METHOD, { 92 | domain: _conf.WILDCARD 93 | }, ({ 94 | source, 95 | origin, 96 | data 97 | }) => { 98 | const { 99 | id, 100 | name 101 | } = data; 102 | const meth = lookupMethod(source, id); 103 | 104 | if (!meth) { 105 | throw new Error(`Could not find method '${name}' with id: ${data.id} in ${(0, _src.getDomain)(window)}`); 106 | } 107 | 108 | const { 109 | source: methodSource, 110 | domain, 111 | val 112 | } = meth; 113 | return _src2.ZalgoPromise.try(() => { 114 | if (!(0, _src.matchDomain)(domain, origin)) { 115 | // $FlowFixMe 116 | throw new Error(`Method '${data.name}' domain ${JSON.stringify((0, _src3.isRegex)(meth.domain) ? meth.domain.source : meth.domain)} does not match origin ${origin} in ${(0, _src.getDomain)(window)}`); 117 | } 118 | 119 | if (_window.ProxyWindow.isProxyWindow(methodSource)) { 120 | // $FlowFixMe 121 | return methodSource.matchWindow(source, { 122 | send 123 | }).then(match => { 124 | if (!match) { 125 | throw new Error(`Method call '${data.name}' failed - proxy window does not match source in ${(0, _src.getDomain)(window)}`); 126 | } 127 | }); 128 | } 129 | }).then(() => { 130 | return val.apply({ 131 | source, 132 | origin 133 | }, data.args); 134 | }, err => { 135 | return _src2.ZalgoPromise.try(() => { 136 | if (val.onError) { 137 | return val.onError(err); 138 | } 139 | }).then(() => { 140 | // $FlowFixMe 141 | if (err.stack) { 142 | // $FlowFixMe 143 | err.stack = `Remote call to ${name}(${stringifyArguments(data.args)}) failed\n\n${err.stack}`; 144 | } 145 | 146 | throw err; 147 | }); 148 | }).then(result => { 149 | return { 150 | result, 151 | id, 152 | name 153 | }; 154 | }); 155 | }); 156 | }); 157 | } 158 | 159 | function serializeFunction(destination, domain, val, key, { 160 | on, 161 | send 162 | }) { 163 | listenForFunctionCalls({ 164 | on, 165 | send 166 | }); 167 | const id = val.__id__ || (0, _src3.uniqueID)(); 168 | destination = _window.ProxyWindow.unwrap(destination); 169 | let name = val.__name__ || val.name || key; 170 | 171 | if (typeof name === 'string' && typeof name.indexOf === 'function' && name.indexOf('anonymous::') === 0) { 172 | name = name.replace('anonymous::', `${key}::`); 173 | } 174 | 175 | if (_window.ProxyWindow.isProxyWindow(destination)) { 176 | addMethod(id, val, name, destination, domain); // $FlowFixMe 177 | 178 | destination.awaitWindow().then(win => { 179 | addMethod(id, val, name, win, domain); 180 | }); 181 | } else { 182 | addMethod(id, val, name, destination, domain); 183 | } 184 | 185 | return (0, _src4.serializeType)(_conf.SERIALIZATION_TYPE.CROSS_DOMAIN_FUNCTION, { 186 | id, 187 | name 188 | }); 189 | } 190 | 191 | function deserializeFunction(source, origin, { 192 | id, 193 | name 194 | }, { 195 | send 196 | }) { 197 | const getDeserializedFunction = (opts = {}) => { 198 | function crossDomainFunctionWrapper() { 199 | let originalStack; 200 | 201 | if (__DEBUG__) { 202 | originalStack = new Error(`Original call to ${name}():`).stack; 203 | } 204 | 205 | return _window.ProxyWindow.toProxyWindow(source, { 206 | send 207 | }).awaitWindow().then(win => { 208 | const meth = lookupMethod(win, id); 209 | 210 | if (meth && meth.val !== crossDomainFunctionWrapper) { 211 | return meth.val.apply({ 212 | source: window, 213 | origin: (0, _src.getDomain)() 214 | }, arguments); 215 | } else { 216 | // $FlowFixMe[method-unbinding] 217 | const args = Array.prototype.slice.call(arguments); 218 | 219 | if (opts.fireAndForget) { 220 | return send(win, _conf.MESSAGE_NAME.METHOD, { 221 | id, 222 | name, 223 | args 224 | }, { 225 | domain: origin, 226 | fireAndForget: true 227 | }); 228 | } else { 229 | return send(win, _conf.MESSAGE_NAME.METHOD, { 230 | id, 231 | name, 232 | args 233 | }, { 234 | domain: origin, 235 | fireAndForget: false 236 | }).then(res => res.data.result); 237 | } 238 | } 239 | }).catch(err => { 240 | // $FlowFixMe 241 | if (__DEBUG__ && originalStack && err.stack) { 242 | // $FlowFixMe 243 | err.stack = `Remote call to ${name}(${stringifyArguments(arguments)}) failed\n\n${err.stack}\n\n${originalStack}`; 244 | } 245 | 246 | throw err; 247 | }); 248 | } 249 | 250 | crossDomainFunctionWrapper.__name__ = name; 251 | crossDomainFunctionWrapper.__origin__ = origin; 252 | crossDomainFunctionWrapper.__source__ = source; 253 | crossDomainFunctionWrapper.__id__ = id; 254 | crossDomainFunctionWrapper.origin = origin; 255 | return crossDomainFunctionWrapper; 256 | }; 257 | 258 | const crossDomainFunctionWrapper = getDeserializedFunction(); 259 | crossDomainFunctionWrapper.fireAndForget = getDeserializedFunction({ 260 | fireAndForget: true 261 | }); 262 | return crossDomainFunctionWrapper; 263 | } -------------------------------------------------------------------------------- /dist/module/serialize/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | exports.__esModule = true; 4 | 5 | var _serialize = require("./serialize"); 6 | 7 | Object.keys(_serialize).forEach(function (key) { 8 | if (key === "default" || key === "__esModule") return; 9 | if (key in exports && exports[key] === _serialize[key]) return; 10 | exports[key] = _serialize[key]; 11 | }); 12 | 13 | var _function = require("./function"); 14 | 15 | Object.keys(_function).forEach(function (key) { 16 | if (key === "default" || key === "__esModule") return; 17 | if (key in exports && exports[key] === _function[key]) return; 18 | exports[key] = _function[key]; 19 | }); 20 | 21 | var _promise = require("./promise"); 22 | 23 | Object.keys(_promise).forEach(function (key) { 24 | if (key === "default" || key === "__esModule") return; 25 | if (key in exports && exports[key] === _promise[key]) return; 26 | exports[key] = _promise[key]; 27 | }); 28 | 29 | var _window = require("./window"); 30 | 31 | Object.keys(_window).forEach(function (key) { 32 | if (key === "default" || key === "__esModule") return; 33 | if (key in exports && exports[key] === _window[key]) return; 34 | exports[key] = _window[key]; 35 | }); -------------------------------------------------------------------------------- /dist/module/serialize/promise.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | exports.__esModule = true; 4 | exports.deserializePromise = deserializePromise; 5 | exports.serializePromise = serializePromise; 6 | 7 | var _src = require("@krakenjs/zalgo-promise/src"); 8 | 9 | var _src2 = require("@krakenjs/universal-serialize/src"); 10 | 11 | var _conf = require("../conf"); 12 | 13 | var _function = require("./function"); 14 | 15 | var _window = require("./window"); 16 | 17 | function serializePromise(destination, domain, val, key, { 18 | on, 19 | send 20 | }) { 21 | return (0, _src2.serializeType)(_conf.SERIALIZATION_TYPE.CROSS_DOMAIN_ZALGO_PROMISE, { 22 | then: (0, _function.serializeFunction)(destination, domain, (resolve, reject) => val.then(resolve, reject), key, { 23 | on, 24 | send 25 | }) 26 | }); 27 | } 28 | 29 | function deserializePromise(source, origin, { 30 | then 31 | }) { 32 | return new _src.ZalgoPromise(then); 33 | } -------------------------------------------------------------------------------- /dist/module/serialize/serialize.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | exports.__esModule = true; 4 | exports.deserializeMessage = deserializeMessage; 5 | exports.serializeMessage = serializeMessage; 6 | 7 | var _src = require("@krakenjs/cross-domain-utils/src"); 8 | 9 | var _src2 = require("@krakenjs/universal-serialize/src"); 10 | 11 | var _conf = require("../conf"); 12 | 13 | var _function = require("./function"); 14 | 15 | var _promise = require("./promise"); 16 | 17 | var _window = require("./window"); 18 | 19 | function serializeMessage(destination, domain, obj, { 20 | on, 21 | send 22 | }) { 23 | return (0, _src2.serialize)(obj, { 24 | [_src2.TYPE.PROMISE]: (val, key) => (0, _promise.serializePromise)(destination, domain, val, key, { 25 | on, 26 | send 27 | }), 28 | [_src2.TYPE.FUNCTION]: (val, key) => (0, _function.serializeFunction)(destination, domain, val, key, { 29 | on, 30 | send 31 | }), 32 | [_src2.TYPE.OBJECT]: val => { 33 | return (0, _src.isWindow)(val) || _window.ProxyWindow.isProxyWindow(val) ? (0, _window.serializeWindow)(destination, domain, val, { 34 | send 35 | }) : val; 36 | } 37 | }); 38 | } 39 | 40 | function deserializeMessage(source, origin, message, { 41 | send 42 | }) { 43 | return (0, _src2.deserialize)(message, { 44 | [_conf.SERIALIZATION_TYPE.CROSS_DOMAIN_ZALGO_PROMISE]: serializedPromise => (0, _promise.deserializePromise)(source, origin, serializedPromise), 45 | [_conf.SERIALIZATION_TYPE.CROSS_DOMAIN_FUNCTION]: serializedFunction => (0, _function.deserializeFunction)(source, origin, serializedFunction, { 46 | send 47 | }), 48 | [_conf.SERIALIZATION_TYPE.CROSS_DOMAIN_WINDOW]: serializedWindow => (0, _window.deserializeWindow)(source, origin, serializedWindow, { 49 | send 50 | }) 51 | }); 52 | } -------------------------------------------------------------------------------- /dist/module/setup.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | exports.__esModule = true; 4 | exports.createProxyWindow = createProxyWindow; 5 | exports.deserializeMessage = deserializeMessage; 6 | exports.destroy = destroy; 7 | exports.serializeMessage = serializeMessage; 8 | exports.setup = setup; 9 | exports.toProxyWindow = toProxyWindow; 10 | 11 | var _lib = require("./lib"); 12 | 13 | var _drivers = require("./drivers"); 14 | 15 | var _global = require("./global"); 16 | 17 | var _public = require("./public"); 18 | 19 | var _bridge = require("./bridge"); 20 | 21 | var _serialize = require("./serialize"); 22 | 23 | function serializeMessage(destination, domain, obj) { 24 | return (0, _serialize.serializeMessage)(destination, domain, obj, { 25 | on: _public.on, 26 | send: _public.send 27 | }); 28 | } 29 | 30 | function deserializeMessage(source, origin, message) { 31 | return (0, _serialize.deserializeMessage)(source, origin, message, { 32 | on: _public.on, 33 | send: _public.send 34 | }); 35 | } 36 | 37 | function createProxyWindow(win) { 38 | return new _serialize.ProxyWindow({ 39 | send: _public.send, 40 | win 41 | }); 42 | } 43 | 44 | function toProxyWindow(win) { 45 | return _serialize.ProxyWindow.toProxyWindow(win, { 46 | send: _public.send 47 | }); 48 | } 49 | 50 | function setup() { 51 | if (!(0, _global.getGlobal)().initialized) { 52 | (0, _global.getGlobal)().initialized = true; 53 | (0, _drivers.setupGlobalReceiveMessage)({ 54 | on: _public.on, 55 | send: _public.send 56 | }); 57 | (0, _drivers.listenForMessages)({ 58 | on: _public.on, 59 | send: _public.send 60 | }); 61 | 62 | if (__POST_ROBOT__.__IE_POPUP_SUPPORT__) { 63 | (0, _bridge.setupBridge)({ 64 | on: _public.on, 65 | send: _public.send, 66 | receiveMessage: _drivers.receiveMessage 67 | }); 68 | } 69 | 70 | (0, _lib.initHello)({ 71 | on: _public.on, 72 | send: _public.send 73 | }); 74 | } 75 | } 76 | 77 | function destroy() { 78 | (0, _drivers.cancelResponseListeners)(); 79 | (0, _drivers.stopListenForMessages)(); 80 | (0, _global.deleteGlobal)(); 81 | } -------------------------------------------------------------------------------- /dist/module/types.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | exports.__esModule = true; 4 | exports.TYPES = void 0; 5 | // export something to force webpack to see this as an ES module 6 | const TYPES = true; // eslint-disable-next-line flowtype/require-exact-type 7 | 8 | exports.TYPES = TYPES; -------------------------------------------------------------------------------- /globals.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | /* eslint import/no-commonjs: 0 */ 3 | 4 | const pkg = require("./package.json"); 5 | 6 | const formatVersion = (version) => { 7 | return version.replace(/[^\d]+/g, "_"); 8 | }; 9 | 10 | module.exports = { 11 | __POST_ROBOT__: { 12 | __GLOBAL_KEY__: `__post_robot_${formatVersion(pkg.version)}__`, 13 | __AUTO_SETUP__: true, 14 | __IE_POPUP_SUPPORT__: true, 15 | __GLOBAL_MESSAGE_SUPPORT__: true, 16 | __SCRIPT_NAMESPACE__: false, 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | // $FlowFixMe 4 | module.exports = require("./dist/post-robot"); // eslint-disable-line import/no-commonjs 5 | 6 | // $FlowFixMe 7 | module.exports.default = module.exports; // eslint-disable-line import/no-commonjs 8 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | /* eslint import/no-default-export: off */ 3 | 4 | import { getKarmaConfig } from "@krakenjs/karma-config-grumbler"; 5 | 6 | import { WEBPACK_CONFIG_TEST } from "./webpack.config"; 7 | 8 | export default function configKarma(karma: Object) { 9 | const karmaConfig = getKarmaConfig(karma, { 10 | basePath: __dirname, 11 | webpack: WEBPACK_CONFIG_TEST, 12 | }); 13 | 14 | karma.set(karmaConfig); 15 | } 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@krakenjs/post-robot", 3 | "version": "11.0.0", 4 | "description": "Simple postMessage based server.", 5 | "main": "index.js", 6 | "scripts": { 7 | "setup": "npm install && npm run flow-typed", 8 | "lint": "eslint src/ test/ *.js", 9 | "flow-typed": "rm -rf flow-typed && flow-typed install", 10 | "flow": "flow", 11 | "format": "prettier --write --ignore-unknown .", 12 | "format:check": "prettier --check .", 13 | "karma": "cross-env NODE_ENV=test babel-node --plugins=transform-es2015-modules-commonjs ./node_modules/.bin/karma start", 14 | "babel": "babel src/ --out-dir dist/module", 15 | "webpack": "babel-node --config-file ./node_modules/@krakenjs/grumbler-scripts/config/.babelrc-node --plugins=transform-es2015-modules-commonjs ./node_modules/.bin/webpack --progress", 16 | "test": "npm run format:check && npm run lint && npm run flow-typed && npm run flow && npm run karma", 17 | "build": "npm run test && npm run babel && npm run webpack -- $@", 18 | "clean": "rimraf dist coverage", 19 | "reinstall": "rimraf flow-typed && rimraf node_modules && npm install && flow-typed install", 20 | "debug": "cross-env NODE_ENV=debug", 21 | "prepare": "husky install", 22 | "prerelease": "npm run clean && npm run build && git add dist && git commit -m 'ci: check in dist folder' || echo 'Nothing to distribute'", 23 | "release": "standard-version", 24 | "postrelease": "git push && git push --follow-tags && npm publish" 25 | }, 26 | "standard-version": { 27 | "types": [ 28 | { 29 | "type": "feat", 30 | "section": "Features" 31 | }, 32 | { 33 | "type": "fix", 34 | "section": "Bug Fixes" 35 | }, 36 | { 37 | "type": "chore", 38 | "hidden": false 39 | }, 40 | { 41 | "type": "docs", 42 | "hidden": false 43 | }, 44 | { 45 | "type": "style", 46 | "hidden": false 47 | }, 48 | { 49 | "type": "refactor", 50 | "hidden": false 51 | }, 52 | { 53 | "type": "perf", 54 | "hidden": false 55 | }, 56 | { 57 | "type": "test", 58 | "hidden": false 59 | }, 60 | { 61 | "type": "ci", 62 | "hidden": true 63 | } 64 | ] 65 | }, 66 | "repository": { 67 | "type": "git", 68 | "url": "git://github.com/krakenjs/post-robot.git" 69 | }, 70 | "keywords": [ 71 | "cross-domain", 72 | "cross domain", 73 | "xdm", 74 | "iframe", 75 | "postmessage", 76 | "krakenjs", 77 | "kraken" 78 | ], 79 | "licenses": [ 80 | { 81 | "type": "Apache 2.0", 82 | "url": "http://www.apache.org/licenses/LICENSE-2.0.html" 83 | } 84 | ], 85 | "files": [ 86 | "dist/", 87 | "src/", 88 | "globals.js" 89 | ], 90 | "readmeFilename": "README.md", 91 | "devDependencies": { 92 | "@commitlint/cli": "^16.2.1", 93 | "@commitlint/config-conventional": "^16.2.1", 94 | "@krakenjs/grumbler-scripts": "^8.0.4", 95 | "cross-env": "^7.0.3", 96 | "flow-bin": "0.155.0", 97 | "flow-typed": "^3.8.0", 98 | "husky": "^7.0.4", 99 | "jest": "^29.3.1", 100 | "lint-staged": "^12.4.0", 101 | "mocha": "^4", 102 | "prettier": "^2.6.2", 103 | "standard-version": "^9.3.2" 104 | }, 105 | "dependencies": { 106 | "@krakenjs/belter": "^2.0.0", 107 | "@krakenjs/cross-domain-safe-weakmap": "^2.0.0", 108 | "@krakenjs/cross-domain-utils": "^3.0.0", 109 | "@krakenjs/universal-serialize": "^2.0.0", 110 | "@krakenjs/zalgo-promise": "^2.0.0" 111 | }, 112 | "lint-staged": { 113 | "*": "prettier --write --ignore-unknown" 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/bridge/bridge.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { type ZalgoPromise } from "@krakenjs/zalgo-promise/src"; 4 | import { 5 | getParent, 6 | isWindowClosed, 7 | type CrossDomainWindowType, 8 | } from "@krakenjs/cross-domain-utils/src"; 9 | import { noop, uniqueID } from "@krakenjs/belter/src"; 10 | 11 | import { MESSAGE_NAME, WILDCARD } from "../conf"; 12 | import { getGlobal, globalStore } from "../global"; 13 | import type { SendType, ResponseMessageEvent } from "../types"; 14 | 15 | function cleanTunnelWindows() { 16 | const tunnelWindows = globalStore("tunnelWindows"); 17 | 18 | for (const key of tunnelWindows.keys()) { 19 | const tunnelWindow = tunnelWindows[key]; 20 | 21 | try { 22 | noop(tunnelWindow.source); 23 | } catch (err) { 24 | tunnelWindows.del(key); 25 | continue; 26 | } 27 | 28 | if (isWindowClosed(tunnelWindow.source)) { 29 | tunnelWindows.del(key); 30 | } 31 | } 32 | } 33 | 34 | type TunnelWindowDataType = {| 35 | name: string, 36 | source: CrossDomainWindowType, 37 | canary: () => void, 38 | sendMessage: (message: string) => void, 39 | |}; 40 | 41 | function addTunnelWindow({ 42 | name, 43 | source, 44 | canary, 45 | sendMessage, 46 | }: TunnelWindowDataType): string { 47 | cleanTunnelWindows(); 48 | const id = uniqueID(); 49 | const tunnelWindows = globalStore("tunnelWindows"); 50 | tunnelWindows.set(id, { name, source, canary, sendMessage }); 51 | return id; 52 | } 53 | 54 | export function setupOpenTunnelToParent({ send }: {| send: SendType |}) { 55 | getGlobal(window).openTunnelToParent = function openTunnelToParent({ 56 | name, 57 | source, 58 | canary, 59 | sendMessage, 60 | }: TunnelWindowDataType): ZalgoPromise { 61 | const tunnelWindows = globalStore("tunnelWindows"); 62 | const parentWindow = getParent(window); 63 | 64 | if (!parentWindow) { 65 | throw new Error(`No parent window found to open tunnel to`); 66 | } 67 | 68 | const id = addTunnelWindow({ name, source, canary, sendMessage }); 69 | 70 | return send( 71 | parentWindow, 72 | MESSAGE_NAME.OPEN_TUNNEL, 73 | { 74 | name, 75 | 76 | sendMessage() { 77 | const tunnelWindow = tunnelWindows.get(id); 78 | 79 | try { 80 | // IE gets antsy if you try to even reference a closed window 81 | noop(tunnelWindow && tunnelWindow.source); 82 | } catch (err) { 83 | tunnelWindows.del(id); 84 | return; 85 | } 86 | 87 | if ( 88 | !tunnelWindow || 89 | !tunnelWindow.source || 90 | isWindowClosed(tunnelWindow.source) 91 | ) { 92 | return; 93 | } 94 | 95 | try { 96 | tunnelWindow.canary(); 97 | } catch (err) { 98 | return; 99 | } 100 | 101 | // $FlowFixMe[object-this-reference] 102 | tunnelWindow.sendMessage.apply(this, arguments); 103 | }, 104 | }, 105 | { domain: WILDCARD } 106 | ); 107 | }; 108 | } 109 | -------------------------------------------------------------------------------- /src/bridge/child.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { ZalgoPromise } from "@krakenjs/zalgo-promise/src"; 4 | import { 5 | isSameDomain, 6 | getOpener, 7 | getDomain, 8 | getFrameByName, 9 | assertSameDomain, 10 | type CrossDomainWindowType, 11 | } from "@krakenjs/cross-domain-utils/src"; 12 | import { noop } from "@krakenjs/belter/src"; 13 | 14 | import { getGlobal, windowStore } from "../global"; 15 | import type { OnType, SendType, ReceiveMessageType } from "../types"; 16 | 17 | import { 18 | needsBridge, 19 | registerRemoteWindow, 20 | rejectRemoteSendMessage, 21 | registerRemoteSendMessage, 22 | getBridgeName, 23 | } from "./common"; 24 | 25 | function awaitRemoteBridgeForWindow( 26 | win: CrossDomainWindowType 27 | ): ZalgoPromise { 28 | return windowStore("remoteBridgeAwaiters").getOrSet(win, () => { 29 | return ZalgoPromise.try(() => { 30 | const frame = getFrameByName(win, getBridgeName(getDomain())); 31 | 32 | if (!frame) { 33 | return; 34 | } 35 | 36 | if (isSameDomain(frame) && getGlobal(assertSameDomain(frame))) { 37 | return frame; 38 | } 39 | 40 | return new ZalgoPromise((resolve) => { 41 | let interval; 42 | // eslint-disable-next-line prefer-const 43 | let timeout; 44 | 45 | // eslint-disable-next-line prefer-const 46 | interval = setInterval(() => { 47 | if ( 48 | frame && 49 | isSameDomain(frame) && 50 | getGlobal(assertSameDomain(frame)) 51 | ) { 52 | clearInterval(interval); 53 | clearTimeout(timeout); 54 | return resolve(frame); 55 | } 56 | }, 100); 57 | 58 | timeout = setTimeout(() => { 59 | clearInterval(interval); 60 | return resolve(); 61 | }, 2000); 62 | }); 63 | }); 64 | }); 65 | } 66 | 67 | export function openTunnelToOpener({ 68 | on, 69 | send, 70 | receiveMessage, 71 | }: {| 72 | on: OnType, 73 | send: SendType, 74 | receiveMessage: ReceiveMessageType, 75 | |}): ZalgoPromise { 76 | return ZalgoPromise.try(() => { 77 | const opener = getOpener(window); 78 | 79 | if (!opener || !needsBridge({ win: opener })) { 80 | return; 81 | } 82 | 83 | registerRemoteWindow(opener); 84 | 85 | return awaitRemoteBridgeForWindow(opener).then((bridge) => { 86 | if (!bridge) { 87 | return rejectRemoteSendMessage( 88 | opener, 89 | new Error(`Can not register with opener: no bridge found in opener`) 90 | ); 91 | } 92 | 93 | if (!window.name) { 94 | return rejectRemoteSendMessage( 95 | opener, 96 | new Error(`Can not register with opener: window does not have a name`) 97 | ); 98 | } 99 | 100 | return getGlobal(assertSameDomain(bridge)) 101 | .openTunnelToParent({ 102 | name: window.name, 103 | 104 | source: window, 105 | 106 | canary() { 107 | // pass 108 | }, 109 | 110 | sendMessage(message) { 111 | try { 112 | noop(window); 113 | } catch (err) { 114 | return; 115 | } 116 | 117 | if (!window || window.closed) { 118 | return; 119 | } 120 | 121 | try { 122 | receiveMessage( 123 | { 124 | data: message, 125 | // $FlowFixMe[object-this-reference] 126 | origin: this.origin, 127 | // $FlowFixMe[object-this-reference] 128 | source: this.source, 129 | }, 130 | { on, send } 131 | ); 132 | } catch (err) { 133 | ZalgoPromise.reject(err); 134 | } 135 | }, 136 | }) 137 | .then(({ source, origin, data }) => { 138 | if (source !== opener) { 139 | throw new Error(`Source does not match opener`); 140 | } 141 | 142 | registerRemoteSendMessage(source, origin, data.sendMessage); 143 | }) 144 | .catch((err) => { 145 | rejectRemoteSendMessage(opener, err); 146 | throw err; 147 | }); 148 | }); 149 | }); 150 | } 151 | -------------------------------------------------------------------------------- /src/bridge/common.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { ZalgoPromise } from "@krakenjs/zalgo-promise/src"; 4 | import { 5 | getDomain, 6 | isSameDomain, 7 | isOpener, 8 | isSameTopWindow, 9 | matchDomain, 10 | getUserAgent, 11 | getDomainFromUrl, 12 | type CrossDomainWindowType, 13 | } from "@krakenjs/cross-domain-utils/src"; 14 | import { noop } from "@krakenjs/belter/src"; 15 | 16 | import { BRIDGE_NAME_PREFIX } from "../conf"; 17 | import { windowStore } from "../global"; 18 | 19 | export function needsBridgeForBrowser(): boolean { 20 | if (getUserAgent(window).match(/MSIE|trident|edge\/12|edge\/13/i)) { 21 | return true; 22 | } 23 | 24 | return false; 25 | } 26 | 27 | export function needsBridgeForWin(win: CrossDomainWindowType): boolean { 28 | if (!isSameTopWindow(window, win)) { 29 | return true; 30 | } 31 | 32 | return false; 33 | } 34 | 35 | export function needsBridgeForDomain( 36 | domain: ?string, 37 | win: ?CrossDomainWindowType 38 | ): boolean { 39 | if (domain) { 40 | if (getDomain() !== getDomainFromUrl(domain)) { 41 | return true; 42 | } 43 | } else if (win) { 44 | if (!isSameDomain(win)) { 45 | return true; 46 | } 47 | } 48 | 49 | return false; 50 | } 51 | 52 | export function needsBridge({ 53 | win, 54 | domain, 55 | }: {| 56 | win?: CrossDomainWindowType, 57 | domain?: string, 58 | |}): boolean { 59 | if (!needsBridgeForBrowser()) { 60 | return false; 61 | } 62 | 63 | if (domain && !needsBridgeForDomain(domain, win)) { 64 | return false; 65 | } 66 | 67 | if (win && !needsBridgeForWin(win)) { 68 | return false; 69 | } 70 | 71 | return true; 72 | } 73 | 74 | export function getBridgeName(domain: string): string { 75 | domain = domain || getDomainFromUrl(domain); 76 | 77 | const sanitizedDomain = domain.replace(/[^a-zA-Z0-9]+/g, "_"); 78 | 79 | const id = `${BRIDGE_NAME_PREFIX}_${sanitizedDomain}`; 80 | 81 | return id; 82 | } 83 | 84 | export function isBridge(): boolean { 85 | return Boolean(window.name && window.name === getBridgeName(getDomain())); 86 | } 87 | 88 | export const documentBodyReady: ZalgoPromise = 89 | new ZalgoPromise((resolve) => { 90 | if (window.document && window.document.body) { 91 | return resolve(window.document.body); 92 | } 93 | 94 | const interval = setInterval(() => { 95 | if (window.document && window.document.body) { 96 | clearInterval(interval); 97 | return resolve(window.document.body); 98 | } 99 | }, 10); 100 | }); 101 | 102 | export function registerRemoteWindow(win: CrossDomainWindowType) { 103 | const remoteWindowPromises = windowStore("remoteWindowPromises"); 104 | remoteWindowPromises.getOrSet(win, () => new ZalgoPromise()); 105 | } 106 | 107 | export function findRemoteWindow( 108 | win: CrossDomainWindowType 109 | ): ZalgoPromise< 110 | ( 111 | remoteWin: CrossDomainWindowType, 112 | message: string, 113 | remoteDomain: string 114 | ) => void 115 | > { 116 | const remoteWindowPromises = windowStore("remoteWindowPromises"); 117 | const remoteWinPromise = remoteWindowPromises.get(win); 118 | 119 | if (!remoteWinPromise) { 120 | throw new Error(`Remote window promise not found`); 121 | } 122 | 123 | return remoteWinPromise; 124 | } 125 | 126 | type SendMessageType = {| 127 | (string): void, 128 | fireAndForget: (string) => void, 129 | |}; 130 | 131 | export function registerRemoteSendMessage( 132 | win: CrossDomainWindowType, 133 | domain: string, 134 | sendMessage: SendMessageType 135 | ) { 136 | const sendMessageWrapper = ( 137 | remoteWin: CrossDomainWindowType, 138 | remoteDomain: string, 139 | message: string 140 | ) => { 141 | if (remoteWin !== win) { 142 | throw new Error(`Remote window does not match window`); 143 | } 144 | 145 | if (!matchDomain(remoteDomain, domain)) { 146 | throw new Error( 147 | `Remote domain ${remoteDomain} does not match domain ${domain}` 148 | ); 149 | } 150 | 151 | sendMessage.fireAndForget(message); 152 | }; 153 | 154 | findRemoteWindow(win).resolve(sendMessageWrapper); 155 | } 156 | 157 | export function rejectRemoteSendMessage( 158 | win: CrossDomainWindowType, 159 | err: Error 160 | ) { 161 | findRemoteWindow(win).reject(err).catch(noop); 162 | } 163 | 164 | export function sendBridgeMessage( 165 | win: CrossDomainWindowType, 166 | domain: string, 167 | message: string 168 | ): ZalgoPromise { 169 | const messagingChild = isOpener(window, win); 170 | const messagingParent = isOpener(win, window); 171 | 172 | if (!messagingChild && !messagingParent) { 173 | throw new Error( 174 | `Can only send messages to and from parent and popup windows` 175 | ); 176 | } 177 | 178 | return findRemoteWindow(win).then((sendMessage) => { 179 | return sendMessage(win, domain, message); 180 | }); 181 | } 182 | -------------------------------------------------------------------------------- /src/bridge/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | export * from "./bridge"; 4 | export * from "./child"; 5 | export * from "./common"; 6 | export * from "./parent"; 7 | export * from "./setup"; 8 | -------------------------------------------------------------------------------- /src/bridge/parent.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { ZalgoPromise } from "@krakenjs/zalgo-promise/src"; 4 | import { 5 | getDomain, 6 | getFrameByName, 7 | isWindowClosed, 8 | getDomainFromUrl, 9 | normalizeMockUrl, 10 | type CrossDomainWindowType, 11 | } from "@krakenjs/cross-domain-utils/src"; 12 | 13 | import { BRIDGE_TIMEOUT, MESSAGE_NAME } from "../conf"; 14 | import { awaitWindowHello } from "../lib"; 15 | import { windowStore, globalStore } from "../global"; 16 | import type { OnType, SendType, ReceiveMessageType } from "../types"; 17 | 18 | import { 19 | getBridgeName, 20 | documentBodyReady, 21 | registerRemoteSendMessage, 22 | registerRemoteWindow, 23 | } from "./common"; 24 | 25 | type WinDetails = {| 26 | win: CrossDomainWindowType, 27 | domain?: ?string, 28 | name?: ?string, 29 | |}; 30 | 31 | export function listenForOpenTunnel({ 32 | on, 33 | send, 34 | receiveMessage, 35 | }: {| 36 | on: OnType, 37 | send: SendType, 38 | receiveMessage: ReceiveMessageType, 39 | |}) { 40 | const popupWindowsByName = globalStore("popupWindowsByName"); 41 | 42 | on(MESSAGE_NAME.OPEN_TUNNEL, ({ source, origin, data }) => { 43 | const bridgePromise = globalStore("bridges").get(origin); 44 | 45 | if (!bridgePromise) { 46 | throw new Error(`Can not find bridge promise for domain ${origin}`); 47 | } 48 | 49 | return bridgePromise.then((bridge) => { 50 | if (source !== bridge) { 51 | throw new Error( 52 | `Message source does not matched registered bridge for domain ${origin}` 53 | ); 54 | } 55 | 56 | if (!data.name) { 57 | throw new Error(`Register window expected to be passed window name`); 58 | } 59 | 60 | if (!data.sendMessage) { 61 | throw new Error( 62 | `Register window expected to be passed sendMessage method` 63 | ); 64 | } 65 | 66 | if (!popupWindowsByName.has(data.name)) { 67 | throw new Error( 68 | `Window with name ${data.name} does not exist, or was not opened by this window` 69 | ); 70 | } 71 | 72 | const getWindowDetails = (): WinDetails => { 73 | const winDetails = popupWindowsByName.get(data.name); 74 | // $FlowFixMe 75 | return winDetails; 76 | }; 77 | 78 | if (!getWindowDetails().domain) { 79 | throw new Error( 80 | `We do not have a registered domain for window ${data.name}` 81 | ); 82 | } 83 | 84 | if (getWindowDetails().domain !== origin) { 85 | throw new Error( 86 | `Message origin ${origin} does not matched registered window origin ${ 87 | getWindowDetails().domain || "unknown" 88 | }` 89 | ); 90 | } 91 | 92 | registerRemoteSendMessage( 93 | getWindowDetails().win, 94 | origin, 95 | data.sendMessage 96 | ); 97 | 98 | return { 99 | sendMessage(message) { 100 | if (!window || window.closed) { 101 | return; 102 | } 103 | 104 | if (!getWindowDetails()) { 105 | return; 106 | } 107 | 108 | const domain = getWindowDetails().domain; 109 | 110 | if (!domain) { 111 | return; 112 | } 113 | 114 | try { 115 | receiveMessage( 116 | { 117 | data: message, 118 | origin: domain, 119 | source: getWindowDetails().win, 120 | }, 121 | { on, send } 122 | ); 123 | } catch (err) { 124 | ZalgoPromise.reject(err); 125 | } 126 | }, 127 | }; 128 | }); 129 | }); 130 | } 131 | 132 | function openBridgeFrame(name: string, url: string): HTMLIFrameElement { 133 | const iframe = document.createElement(`iframe`); 134 | 135 | iframe.setAttribute(`name`, name); 136 | iframe.setAttribute(`id`, name); 137 | 138 | iframe.setAttribute( 139 | `style`, 140 | `display: none; margin: 0; padding: 0; border: 0px none; overflow: hidden;` 141 | ); 142 | iframe.setAttribute(`frameborder`, `0`); 143 | iframe.setAttribute(`border`, `0`); 144 | iframe.setAttribute(`scrolling`, `no`); 145 | iframe.setAttribute(`allowTransparency`, `true`); 146 | 147 | iframe.setAttribute(`tabindex`, `-1`); 148 | iframe.setAttribute(`hidden`, `true`); 149 | iframe.setAttribute(`title`, ``); 150 | iframe.setAttribute(`role`, `presentation`); 151 | 152 | iframe.src = url; 153 | 154 | return iframe; 155 | } 156 | 157 | export function hasBridge(url: string, domain: string): boolean { 158 | const bridges = globalStore("bridges"); 159 | return bridges.has(domain || getDomainFromUrl(url)); 160 | } 161 | 162 | export function openBridge( 163 | url: string, 164 | domain: string 165 | ): ZalgoPromise { 166 | const bridges = globalStore("bridges"); 167 | const bridgeFrames = globalStore("bridgeFrames"); 168 | 169 | domain = domain || getDomainFromUrl(url); 170 | 171 | return bridges.getOrSet(domain, () => 172 | ZalgoPromise.try(() => { 173 | if (getDomain() === domain) { 174 | throw new Error( 175 | `Can not open bridge on the same domain as current domain: ${domain}` 176 | ); 177 | } 178 | 179 | const name = getBridgeName(domain); 180 | const frame = getFrameByName(window, name); 181 | 182 | if (frame) { 183 | throw new Error(`Frame with name ${name} already exists on page`); 184 | } 185 | 186 | const iframe = openBridgeFrame(name, url); 187 | bridgeFrames.set(domain, iframe); 188 | 189 | return documentBodyReady.then((body) => { 190 | body.appendChild(iframe); 191 | const bridge = iframe.contentWindow; 192 | 193 | return new ZalgoPromise((resolve, reject) => { 194 | iframe.addEventListener("load", resolve); 195 | iframe.addEventListener("error", reject); 196 | }) 197 | .then(() => { 198 | return awaitWindowHello(bridge, BRIDGE_TIMEOUT, `Bridge ${url}`); 199 | }) 200 | .then(() => { 201 | return bridge; 202 | }); 203 | }); 204 | }) 205 | ); 206 | } 207 | 208 | export function linkWindow({ win, name, domain }: WinDetails): WinDetails { 209 | const popupWindowsByName = globalStore("popupWindowsByName"); 210 | const popupWindowsByWin = windowStore("popupWindowsByWin"); 211 | 212 | for (const winName of popupWindowsByName.keys()) { 213 | const details = popupWindowsByName.get(winName); 214 | if (!details || isWindowClosed(details.win)) { 215 | popupWindowsByName.del(winName); 216 | } 217 | } 218 | 219 | if (isWindowClosed(win)) { 220 | return { win, name, domain }; 221 | } 222 | 223 | const details = popupWindowsByWin.getOrSet(win, (): WinDetails => { 224 | if (!name) { 225 | return { win }; 226 | } 227 | 228 | // $FlowFixMe 229 | return popupWindowsByName.getOrSet(name, (): WinDetails => { 230 | return { win, name }; 231 | }); 232 | }); 233 | 234 | if (details.win && details.win !== win) { 235 | throw new Error( 236 | `Different window already linked for window: ${name || "undefined"}` 237 | ); 238 | } 239 | 240 | if (name) { 241 | details.name = name; 242 | popupWindowsByName.set(name, details); 243 | } 244 | 245 | if (domain) { 246 | details.domain = domain; 247 | registerRemoteWindow(win); 248 | } 249 | 250 | popupWindowsByWin.set(win, details); 251 | 252 | return details; 253 | } 254 | 255 | export function linkUrl(win: CrossDomainWindowType, url: string) { 256 | linkWindow({ win, domain: getDomainFromUrl(url) }); 257 | } 258 | 259 | export function listenForWindowOpen() { 260 | const windowOpen = window.open; 261 | 262 | window.open = function windowOpenWrapper( 263 | url: string, 264 | name: string, 265 | options: string, 266 | last: mixed 267 | ): mixed { 268 | const win = windowOpen.call( 269 | this, 270 | normalizeMockUrl(url), 271 | name, 272 | options, 273 | last 274 | ); 275 | 276 | if (!win) { 277 | return win; 278 | } 279 | 280 | linkWindow({ win, name, domain: url ? getDomainFromUrl(url) : null }); 281 | 282 | return win; 283 | }; 284 | } 285 | 286 | export function destroyBridges() { 287 | const bridges = globalStore("bridges"); 288 | const bridgeFrames = globalStore("bridgeFrames"); 289 | 290 | for (const domain of bridgeFrames.keys()) { 291 | const frame = bridgeFrames.get(domain); 292 | if (frame && frame.parentNode) { 293 | frame.parentNode.removeChild(frame); 294 | } 295 | } 296 | bridgeFrames.reset(); 297 | bridges.reset(); 298 | } 299 | -------------------------------------------------------------------------------- /src/bridge/setup.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import type { OnType, SendType, ReceiveMessageType } from "../types"; 4 | 5 | import { listenForWindowOpen, listenForOpenTunnel } from "./parent"; 6 | import { setupOpenTunnelToParent } from "./bridge"; 7 | import { openTunnelToOpener } from "./child"; 8 | 9 | export function setupBridge({ 10 | on, 11 | send, 12 | receiveMessage, 13 | }: {| 14 | on: OnType, 15 | send: SendType, 16 | receiveMessage: ReceiveMessageType, 17 | |}) { 18 | listenForWindowOpen(); 19 | listenForOpenTunnel({ on, send, receiveMessage }); 20 | setupOpenTunnelToParent({ send }); 21 | openTunnelToOpener({ on, send, receiveMessage }); 22 | } 23 | -------------------------------------------------------------------------------- /src/clean.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { 4 | type CrossDomainWindowType, 5 | isWindowClosed, 6 | } from "@krakenjs/cross-domain-utils/src"; 7 | import { noop } from "@krakenjs/belter/src"; 8 | 9 | import { windowStore } from "./global"; 10 | 11 | export function cleanUpWindow(win: CrossDomainWindowType) { 12 | const requestPromises = windowStore("requestPromises"); 13 | for (const promise of requestPromises.get(win, [])) { 14 | promise 15 | .reject( 16 | new Error( 17 | `Window ${ 18 | isWindowClosed(win) ? "closed" : "cleaned up" 19 | } before response` 20 | ) 21 | ) 22 | .catch(noop); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/conf/config.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | export const BRIDGE_TIMEOUT = 5000; 4 | export const CHILD_WINDOW_TIMEOUT = 5000; 5 | 6 | export const ACK_TIMEOUT = 2000; 7 | export const ACK_TIMEOUT_KNOWN = 10000; 8 | export const RES_TIMEOUT: number = __TEST__ ? 2000 : -1; 9 | export const RESPONSE_CYCLE_TIME = 500; 10 | -------------------------------------------------------------------------------- /src/conf/constants.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | export const MESSAGE_TYPE = { 4 | REQUEST: ("postrobot_message_request": "postrobot_message_request"), 5 | RESPONSE: ("postrobot_message_response": "postrobot_message_response"), 6 | ACK: ("postrobot_message_ack": "postrobot_message_ack"), 7 | }; 8 | 9 | export const MESSAGE_ACK = { 10 | SUCCESS: ("success": "success"), 11 | ERROR: ("error": "error"), 12 | }; 13 | 14 | export const MESSAGE_NAME = { 15 | METHOD: ("postrobot_method": "postrobot_method"), 16 | HELLO: ("postrobot_hello": "postrobot_hello"), 17 | OPEN_TUNNEL: ("postrobot_open_tunnel": "postrobot_open_tunnel"), 18 | }; 19 | 20 | export const SEND_STRATEGY = { 21 | POST_MESSAGE: ("postrobot_post_message": "postrobot_post_message"), 22 | BRIDGE: ("postrobot_bridge": "postrobot_bridge"), 23 | GLOBAL: ("postrobot_global": "postrobot_global"), 24 | }; 25 | 26 | export const BRIDGE_NAME_PREFIX = "__postrobot_bridge__"; 27 | export const POSTROBOT_PROXY = "__postrobot_proxy__"; 28 | 29 | export const WILDCARD = "*"; 30 | 31 | export const SERIALIZATION_TYPE = { 32 | CROSS_DOMAIN_ZALGO_PROMISE: 33 | ("cross_domain_zalgo_promise": "cross_domain_zalgo_promise"), 34 | CROSS_DOMAIN_FUNCTION: ("cross_domain_function": "cross_domain_function"), 35 | CROSS_DOMAIN_WINDOW: ("cross_domain_window": "cross_domain_window"), 36 | }; 37 | 38 | export const METHOD = { 39 | GET: ("get": "get"), 40 | POST: ("post": "post"), 41 | }; 42 | -------------------------------------------------------------------------------- /src/conf/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | export * from "./config"; 4 | export * from "./constants"; 5 | -------------------------------------------------------------------------------- /src/declarations.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | declare var __POST_ROBOT__: {| 4 | __GLOBAL_KEY__: string, 5 | __AUTO_SETUP__: boolean, 6 | __IE_POPUP_SUPPORT__: boolean, 7 | __GLOBAL_MESSAGE_SUPPORT__: boolean, 8 | __SCRIPT_NAMESPACE__: boolean, 9 | |}; 10 | 11 | declare var __TEST__: boolean; 12 | declare var __DEBUG__: boolean; 13 | -------------------------------------------------------------------------------- /src/drivers/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | export * from "./receive"; 4 | export * from "./send"; 5 | export * from "./listeners"; 6 | -------------------------------------------------------------------------------- /src/drivers/listeners.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { type ZalgoPromise } from "@krakenjs/zalgo-promise/src"; 4 | import { 5 | matchDomain, 6 | type CrossDomainWindowType, 7 | type DomainMatcher, 8 | } from "@krakenjs/cross-domain-utils/src"; 9 | import { isRegex, getOrSet, noop } from "@krakenjs/belter/src"; 10 | 11 | import { 12 | getWildcard, 13 | type WildCard, 14 | globalStore, 15 | windowStore, 16 | } from "../global"; 17 | import { WILDCARD } from "../conf"; 18 | import { ProxyWindow } from "../serialize/window"; 19 | 20 | export function resetListeners() { 21 | const responseListeners = globalStore("responseListeners"); 22 | const erroredResponseListeners = globalStore("erroredResponseListeners"); 23 | responseListeners.reset(); 24 | erroredResponseListeners.reset(); 25 | } 26 | 27 | const __DOMAIN_REGEX__ = "__domain_regex__"; 28 | 29 | export type RequestListenerType = {| 30 | handler: ({| 31 | source: CrossDomainWindowType, 32 | origin: string, 33 | data: mixed, 34 | |}) => mixed | ZalgoPromise, 35 | handleError: (err: mixed) => void, 36 | |}; 37 | 38 | export type ResponseListenerType = {| 39 | name: string, 40 | win: CrossDomainWindowType, 41 | domain: DomainMatcher, 42 | promise: ZalgoPromise<*>, 43 | ack?: ?boolean, 44 | cancelled?: ?boolean, 45 | |}; 46 | 47 | export function addResponseListener( 48 | hash: string, 49 | listener: ResponseListenerType 50 | ) { 51 | const responseListeners = globalStore("responseListeners"); 52 | responseListeners.set(hash, listener); 53 | } 54 | 55 | export function getResponseListener(hash: string): ?ResponseListenerType { 56 | const responseListeners = globalStore("responseListeners"); 57 | return responseListeners.get(hash); 58 | } 59 | 60 | export function deleteResponseListener(hash: string) { 61 | const responseListeners = globalStore("responseListeners"); 62 | responseListeners.del(hash); 63 | } 64 | 65 | export function cancelResponseListeners() { 66 | const responseListeners = globalStore("responseListeners"); 67 | for (const hash of responseListeners.keys()) { 68 | const listener = responseListeners.get(hash); 69 | if (listener) { 70 | listener.cancelled = true; 71 | } 72 | responseListeners.del(hash); 73 | } 74 | } 75 | 76 | export function markResponseListenerErrored(hash: string) { 77 | const erroredResponseListeners = globalStore("erroredResponseListeners"); 78 | erroredResponseListeners.set(hash, true); 79 | } 80 | 81 | export function isResponseListenerErrored(hash: string): boolean { 82 | const erroredResponseListeners = globalStore("erroredResponseListeners"); 83 | return erroredResponseListeners.has(hash); 84 | } 85 | 86 | export function getRequestListener({ 87 | name, 88 | win, 89 | domain, 90 | }: {| 91 | name: string, 92 | win: ?(CrossDomainWindowType | WildCard), 93 | domain: ?(string | RegExp), 94 | |}): ?RequestListenerType { 95 | const requestListeners = windowStore("requestListeners"); 96 | 97 | if (win === WILDCARD) { 98 | win = null; 99 | } 100 | 101 | if (domain === WILDCARD) { 102 | domain = null; 103 | } 104 | 105 | if (!name) { 106 | throw new Error(`Name required to get request listener`); 107 | } 108 | 109 | for (const winQualifier of [win, getWildcard()]) { 110 | if (!winQualifier) { 111 | continue; 112 | } 113 | 114 | const nameListeners = requestListeners.get(winQualifier); 115 | 116 | if (!nameListeners) { 117 | continue; 118 | } 119 | 120 | const domainListeners = nameListeners[name]; 121 | 122 | if (!domainListeners) { 123 | continue; 124 | } 125 | 126 | if (domain && typeof domain === "string") { 127 | if (domainListeners[domain]) { 128 | return domainListeners[domain]; 129 | } 130 | 131 | if (domainListeners[__DOMAIN_REGEX__]) { 132 | for (const { regex, listener } of domainListeners[__DOMAIN_REGEX__]) { 133 | if (matchDomain(regex, domain)) { 134 | return listener; 135 | } 136 | } 137 | } 138 | } 139 | 140 | if (domainListeners[WILDCARD]) { 141 | return domainListeners[WILDCARD]; 142 | } 143 | } 144 | } 145 | 146 | // eslint-disable-next-line complexity 147 | export function addRequestListener( 148 | { 149 | name, 150 | win: winCandidate, 151 | domain, 152 | }: {| 153 | name: string, 154 | win: ?(CrossDomainWindowType | WildCard | ProxyWindow), 155 | domain: ?DomainMatcher, 156 | |}, 157 | listener: RequestListenerType 158 | ): {| cancel: () => void |} { 159 | const requestListeners = windowStore("requestListeners"); 160 | 161 | if (!name || typeof name !== "string") { 162 | throw new Error(`Name required to add request listener`); 163 | } 164 | 165 | if ( 166 | winCandidate && 167 | winCandidate !== WILDCARD && 168 | // $FlowFixMe 169 | ProxyWindow.isProxyWindow(winCandidate) 170 | ) { 171 | // $FlowFixMe 172 | const proxyWin: ProxyWindow = winCandidate; 173 | 174 | const requestListenerPromise = proxyWin.awaitWindow().then((actualWin) => { 175 | return addRequestListener({ name, win: actualWin, domain }, listener); 176 | }); 177 | 178 | return { 179 | cancel: () => { 180 | requestListenerPromise.then( 181 | (requestListener) => requestListener.cancel(), 182 | noop 183 | ); 184 | }, 185 | }; 186 | } 187 | 188 | // $FlowFixMe 189 | let win: ?(CrossDomainWindowType | WildCard) = winCandidate; 190 | 191 | if (Array.isArray(win)) { 192 | const listenersCollection = []; 193 | 194 | for (const item of win) { 195 | listenersCollection.push( 196 | addRequestListener({ name, domain, win: item }, listener) 197 | ); 198 | } 199 | 200 | return { 201 | cancel() { 202 | for (const cancelListener of listenersCollection) { 203 | cancelListener.cancel(); 204 | } 205 | }, 206 | }; 207 | } 208 | 209 | if (Array.isArray(domain)) { 210 | const listenersCollection = []; 211 | 212 | for (const item of domain) { 213 | listenersCollection.push( 214 | addRequestListener({ name, win, domain: item }, listener) 215 | ); 216 | } 217 | 218 | return { 219 | cancel() { 220 | for (const cancelListener of listenersCollection) { 221 | cancelListener.cancel(); 222 | } 223 | }, 224 | }; 225 | } 226 | 227 | const existingListener = getRequestListener({ name, win, domain }); 228 | 229 | if (!win || win === WILDCARD) { 230 | win = getWildcard(); 231 | } 232 | 233 | domain = domain || WILDCARD; 234 | const strDomain = domain.toString(); 235 | 236 | if (existingListener) { 237 | if (win && domain) { 238 | throw new Error( 239 | `Request listener already exists for ${name} on domain ${domain.toString()} for ${ 240 | win === getWildcard() ? "wildcard" : "specified" 241 | } window` 242 | ); 243 | } else if (win) { 244 | throw new Error( 245 | `Request listener already exists for ${name} for ${ 246 | win === getWildcard() ? "wildcard" : "specified" 247 | } window` 248 | ); 249 | } else if (domain) { 250 | throw new Error( 251 | `Request listener already exists for ${name} on domain ${domain.toString()}` 252 | ); 253 | } else { 254 | throw new Error(`Request listener already exists for ${name}`); 255 | } 256 | } 257 | 258 | const winNameListeners = requestListeners.getOrSet(win, () => ({})); 259 | const winNameDomainListeners = getOrSet(winNameListeners, name, () => ({})); 260 | 261 | let winNameDomainRegexListeners; 262 | let winNameDomainRegexListener; 263 | 264 | if (isRegex(domain)) { 265 | winNameDomainRegexListeners = getOrSet( 266 | winNameDomainListeners, 267 | __DOMAIN_REGEX__, 268 | () => [] 269 | ); 270 | winNameDomainRegexListener = { regex: domain, listener }; 271 | winNameDomainRegexListeners.push(winNameDomainRegexListener); 272 | } else { 273 | winNameDomainListeners[strDomain] = listener; 274 | } 275 | 276 | return { 277 | cancel() { 278 | delete winNameDomainListeners[strDomain]; 279 | 280 | if (winNameDomainRegexListener) { 281 | winNameDomainRegexListeners.splice( 282 | winNameDomainRegexListeners.indexOf(winNameDomainRegexListener, 1) 283 | ); 284 | 285 | if (!winNameDomainRegexListeners.length) { 286 | delete winNameDomainListeners[__DOMAIN_REGEX__]; 287 | } 288 | } 289 | 290 | if (!Object.keys(winNameDomainListeners).length) { 291 | delete winNameListeners[name]; 292 | } 293 | 294 | if (win && !Object.keys(winNameListeners).length) { 295 | requestListeners.del(win); 296 | } 297 | }, 298 | }; 299 | } 300 | -------------------------------------------------------------------------------- /src/drivers/receive/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { ZalgoPromise } from "@krakenjs/zalgo-promise/src"; 4 | import { 5 | isWindowClosed, 6 | type CrossDomainWindowType, 7 | getDomain, 8 | isSameTopWindow, 9 | PROTOCOL, 10 | } from "@krakenjs/cross-domain-utils/src"; 11 | import { addEventListener, noop } from "@krakenjs/belter/src"; 12 | 13 | import type { Message } from "../types"; 14 | import { MESSAGE_TYPE } from "../../conf"; 15 | import { markWindowKnown, needsGlobalMessagingForBrowser } from "../../lib"; 16 | import { deserializeMessage } from "../../serialize"; 17 | import { getGlobal, globalStore, getGlobalKey } from "../../global"; 18 | import type { 19 | OnType, 20 | SendType, 21 | MessageEvent, 22 | CancelableType, 23 | } from "../../types"; 24 | 25 | import { handleRequest, handleResponse, handleAck } from "./types"; 26 | 27 | function deserializeMessages( 28 | message: string, 29 | source: CrossDomainWindowType, 30 | origin: string, 31 | { on, send }: {| on: OnType, send: SendType |} 32 | ): ?$ReadOnlyArray { 33 | let parsedMessage; 34 | 35 | try { 36 | parsedMessage = deserializeMessage(source, origin, message, { on, send }); 37 | } catch (err) { 38 | return; 39 | } 40 | 41 | if (!parsedMessage) { 42 | return; 43 | } 44 | 45 | if (typeof parsedMessage !== "object" || parsedMessage === null) { 46 | return; 47 | } 48 | 49 | const parseMessages = parsedMessage[getGlobalKey()]; 50 | 51 | if (!Array.isArray(parseMessages)) { 52 | return; 53 | } 54 | 55 | return parseMessages; 56 | } 57 | 58 | export function receiveMessage( 59 | event: MessageEvent, 60 | { on, send }: {| on: OnType, send: SendType |} 61 | ) { 62 | const receivedMessages = globalStore("receivedMessages"); 63 | 64 | try { 65 | if (!window || window.closed || !event.source) { 66 | return; 67 | } 68 | } catch (err) { 69 | return; 70 | } 71 | 72 | let { source, origin, data } = event; 73 | 74 | if (__TEST__) { 75 | if (isWindowClosed(source)) { 76 | return; 77 | } 78 | 79 | // $FlowFixMe 80 | origin = getDomain(source); 81 | } 82 | 83 | const messages = deserializeMessages(data, source, origin, { on, send }); 84 | 85 | if (!messages) { 86 | return; 87 | } 88 | 89 | markWindowKnown(source); 90 | 91 | for (const message of messages) { 92 | if (receivedMessages.has(message.id)) { 93 | return; 94 | } 95 | 96 | receivedMessages.set(message.id, true); 97 | 98 | if (isWindowClosed(source) && !message.fireAndForget) { 99 | return; 100 | } 101 | 102 | if (message.origin.indexOf(PROTOCOL.FILE) === 0) { 103 | origin = `${PROTOCOL.FILE}//`; 104 | } 105 | 106 | try { 107 | if (message.type === MESSAGE_TYPE.REQUEST) { 108 | handleRequest(source, origin, message, { on, send }); 109 | } else if (message.type === MESSAGE_TYPE.RESPONSE) { 110 | handleResponse(source, origin, message); 111 | } else if (message.type === MESSAGE_TYPE.ACK) { 112 | handleAck(source, origin, message); 113 | } 114 | } catch (err) { 115 | setTimeout(() => { 116 | throw err; 117 | }, 0); 118 | } 119 | } 120 | } 121 | 122 | export function setupGlobalReceiveMessage({ 123 | on, 124 | send, 125 | }: {| 126 | on: OnType, 127 | send: SendType, 128 | |}) { 129 | const global = getGlobal(); 130 | global.receiveMessage = 131 | global.receiveMessage || 132 | ((message) => receiveMessage(message, { on, send })); 133 | } 134 | 135 | type ListenerEvent = {| 136 | source: CrossDomainWindowType, 137 | origin: string, 138 | data: string, 139 | sourceElement: CrossDomainWindowType, 140 | originalEvent?: {| origin: string |}, 141 | |}; 142 | 143 | export function messageListener( 144 | event: ListenerEvent, 145 | { on, send }: {| on: OnType, send: SendType |} 146 | ) { 147 | ZalgoPromise.try(() => { 148 | try { 149 | noop(event.source); 150 | } catch (err) { 151 | return; 152 | } 153 | 154 | const source = event.source || event.sourceElement; 155 | let origin = 156 | event.origin || (event.originalEvent && event.originalEvent.origin); 157 | const data = event.data; 158 | 159 | if (origin === "null") { 160 | origin = `${PROTOCOL.FILE}//`; 161 | } 162 | 163 | if (!source) { 164 | return; 165 | } 166 | 167 | if (!origin) { 168 | throw new Error(`Post message did not have origin domain`); 169 | } 170 | 171 | if (__TEST__) { 172 | if ( 173 | needsGlobalMessagingForBrowser() && 174 | isSameTopWindow(source, window) === false 175 | ) { 176 | return; 177 | } 178 | } 179 | 180 | receiveMessage({ source, origin, data }, { on, send }); 181 | }); 182 | } 183 | 184 | export function listenForMessages({ 185 | on, 186 | send, 187 | }: {| 188 | on: OnType, 189 | send: SendType, 190 | |}): CancelableType { 191 | return globalStore().getOrSet("postMessageListener", () => { 192 | return addEventListener(window, "message", (event) => { 193 | // $FlowFixMe 194 | messageListener(event, { on, send }); 195 | }); 196 | }); 197 | } 198 | 199 | export function stopListenForMessages() { 200 | const listener = globalStore().get("postMessageListener"); 201 | if (listener) { 202 | listener.cancel(); 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /src/drivers/receive/types.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { ZalgoPromise } from "@krakenjs/zalgo-promise/src"; 4 | import { 5 | getDomain, 6 | isWindowClosed, 7 | matchDomain, 8 | stringifyDomainPattern, 9 | type CrossDomainWindowType, 10 | } from "@krakenjs/cross-domain-utils/src"; 11 | import { noop, stringifyError, uniqueID } from "@krakenjs/belter/src"; 12 | 13 | import { MESSAGE_TYPE, MESSAGE_ACK, MESSAGE_NAME } from "../../conf"; 14 | import { sendMessage } from "../send"; 15 | import { 16 | getRequestListener, 17 | getResponseListener, 18 | deleteResponseListener, 19 | isResponseListenerErrored, 20 | } from "../listeners"; 21 | import type { 22 | RequestMessage, 23 | AckResponseMessage, 24 | ResponseMessage, 25 | } from "../types"; 26 | import type { OnType, SendType } from "../../types"; 27 | 28 | export function handleRequest( 29 | source: CrossDomainWindowType, 30 | origin: string, 31 | message: RequestMessage, 32 | { on, send }: {| on: OnType, send: SendType |} 33 | ): ZalgoPromise { 34 | const options = getRequestListener({ 35 | name: message.name, 36 | win: source, 37 | domain: origin, 38 | }); 39 | 40 | const logName = 41 | message.name === MESSAGE_NAME.METHOD && 42 | message.data && 43 | typeof message.data.name === "string" 44 | ? `${message.data.name}()` 45 | : message.name; 46 | 47 | if (__DEBUG__) { 48 | // eslint-disable-next-line no-console 49 | console.info("receive::req", logName, origin, "\n\n", message.data); 50 | } 51 | 52 | function sendAck(): ZalgoPromise { 53 | return ZalgoPromise.flush().then(() => { 54 | if (message.fireAndForget || isWindowClosed(source)) { 55 | return; 56 | } 57 | 58 | try { 59 | return sendMessage( 60 | source, 61 | origin, 62 | { 63 | id: uniqueID(), 64 | origin: getDomain(window), 65 | type: MESSAGE_TYPE.ACK, 66 | hash: message.hash, 67 | name: message.name, 68 | }, 69 | { on, send } 70 | ); 71 | } catch (err) { 72 | throw new Error( 73 | `Send ack message failed for ${logName} in ${getDomain()}\n\n${stringifyError( 74 | err 75 | )}` 76 | ); 77 | } 78 | }); 79 | } 80 | 81 | function sendResponse( 82 | ack: $Values, 83 | data: ?Object, 84 | error: ?mixed 85 | ): ZalgoPromise { 86 | return ZalgoPromise.flush().then(() => { 87 | if (message.fireAndForget || isWindowClosed(source)) { 88 | return; 89 | } 90 | 91 | if (__DEBUG__) { 92 | if (ack === MESSAGE_ACK.SUCCESS) { 93 | console.info("respond::res", logName, origin, "\n\n", data); // eslint-disable-line no-console 94 | } else if (ack === MESSAGE_ACK.ERROR) { 95 | console.error("respond::err", logName, origin, "\n\n", error); // eslint-disable-line no-console 96 | } 97 | } 98 | 99 | try { 100 | return sendMessage( 101 | source, 102 | origin, 103 | { 104 | id: uniqueID(), 105 | origin: getDomain(window), 106 | type: MESSAGE_TYPE.RESPONSE, 107 | hash: message.hash, 108 | name: message.name, 109 | ack, 110 | data, 111 | error, 112 | }, 113 | { on, send } 114 | ); 115 | } catch (err) { 116 | throw new Error( 117 | `Send response message failed for ${logName} in ${getDomain()}\n\n${stringifyError( 118 | err 119 | )}` 120 | ); 121 | } 122 | }); 123 | } 124 | 125 | return ZalgoPromise.all([ 126 | sendAck(), 127 | 128 | ZalgoPromise.try(() => { 129 | if (!options) { 130 | throw new Error( 131 | `No handler found for post message: ${message.name} from ${origin} in ${window.location.protocol}//${window.location.host}${window.location.pathname}` 132 | ); 133 | } 134 | 135 | const data = message.data; 136 | 137 | return options.handler({ source, origin, data }); 138 | }).then( 139 | (data) => { 140 | return sendResponse(MESSAGE_ACK.SUCCESS, data); 141 | }, 142 | (error) => { 143 | return sendResponse(MESSAGE_ACK.ERROR, null, error); 144 | } 145 | ), 146 | ]) 147 | .then(noop) 148 | .catch((err) => { 149 | if (options && options.handleError) { 150 | return options.handleError(err); 151 | } else { 152 | throw err; 153 | } 154 | }); 155 | } 156 | 157 | export function handleAck( 158 | source: CrossDomainWindowType, 159 | origin: string, 160 | message: AckResponseMessage 161 | ) { 162 | if (isResponseListenerErrored(message.hash)) { 163 | return; 164 | } 165 | 166 | const options = getResponseListener(message.hash); 167 | 168 | if (!options) { 169 | throw new Error( 170 | `No handler found for post message ack for message: ${message.name} from ${origin} in ${window.location.protocol}//${window.location.host}${window.location.pathname}` 171 | ); 172 | } 173 | 174 | try { 175 | if (!matchDomain(options.domain, origin)) { 176 | throw new Error( 177 | `Ack origin ${origin} does not match domain ${options.domain.toString()}` 178 | ); 179 | } 180 | 181 | if (source !== options.win) { 182 | throw new Error(`Ack source does not match registered window`); 183 | } 184 | } catch (err) { 185 | options.promise.reject(err); 186 | } 187 | 188 | options.ack = true; 189 | } 190 | 191 | export function handleResponse( 192 | source: CrossDomainWindowType, 193 | origin: string, 194 | message: ResponseMessage 195 | ): void | ZalgoPromise { 196 | if (isResponseListenerErrored(message.hash)) { 197 | return; 198 | } 199 | 200 | const options = getResponseListener(message.hash); 201 | 202 | if (!options) { 203 | throw new Error( 204 | `No handler found for post message response for message: ${message.name} from ${origin} in ${window.location.protocol}//${window.location.host}${window.location.pathname}` 205 | ); 206 | } 207 | 208 | if (!matchDomain(options.domain, origin)) { 209 | throw new Error( 210 | `Response origin ${origin} does not match domain ${stringifyDomainPattern( 211 | options.domain 212 | )}` 213 | ); 214 | } 215 | 216 | if (source !== options.win) { 217 | throw new Error(`Response source does not match registered window`); 218 | } 219 | 220 | deleteResponseListener(message.hash); 221 | 222 | const logName = 223 | message.name === MESSAGE_NAME.METHOD && 224 | message.data && 225 | typeof message.data.name === "string" 226 | ? `${message.data.name}()` 227 | : message.name; 228 | 229 | if (message.ack === MESSAGE_ACK.ERROR) { 230 | if (__DEBUG__) { 231 | console.error("receive::err", logName, origin, "\n\n", message.error); // eslint-disable-line no-console 232 | } 233 | 234 | options.promise.reject(message.error); 235 | } else if (message.ack === MESSAGE_ACK.SUCCESS) { 236 | if (__DEBUG__) { 237 | console.info("receive::res", logName, origin, "\n\n", message.data); // eslint-disable-line no-console 238 | } 239 | 240 | options.promise.resolve({ source, origin, data: message.data }); 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /src/drivers/send/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { ZalgoPromise } from "@krakenjs/zalgo-promise/src"; 4 | import { 5 | isWindowClosed, 6 | type CrossDomainWindowType, 7 | } from "@krakenjs/cross-domain-utils/src"; 8 | import { stringifyError, noop } from "@krakenjs/belter/src"; 9 | 10 | import { serializeMessage } from "../../serialize"; 11 | import { windowStore, getGlobalKey } from "../../global"; 12 | import type { Message, PackedMessages } from "../types"; 13 | import type { OnType, SendType } from "../../types"; 14 | 15 | import { SEND_MESSAGE_STRATEGIES } from "./strategies"; 16 | 17 | function packMessages(messages: $ReadOnlyArray): PackedMessages { 18 | return { 19 | [getGlobalKey()]: messages, 20 | }; 21 | } 22 | 23 | export function sendMessage( 24 | win: CrossDomainWindowType, 25 | domain: string, 26 | message: Message, 27 | { on, send }: {| on: OnType, send: SendType |} 28 | ): ZalgoPromise { 29 | return ZalgoPromise.try(() => { 30 | const messageBuffer = windowStore(); 31 | 32 | const domainBuffer = messageBuffer.getOrSet(win, () => ({})); 33 | 34 | domainBuffer.buffer = domainBuffer.buffer || []; 35 | domainBuffer.buffer.push(message); 36 | 37 | domainBuffer.flush = 38 | domainBuffer.flush || 39 | ZalgoPromise.flush().then(() => { 40 | if (isWindowClosed(win)) { 41 | throw new Error("Window is closed"); 42 | } 43 | 44 | const serializedMessage = serializeMessage( 45 | win, 46 | domain, 47 | packMessages(domainBuffer.buffer || []), 48 | { on, send } 49 | ); 50 | delete domainBuffer.buffer; 51 | 52 | const strategies = Object.keys(SEND_MESSAGE_STRATEGIES); 53 | const errors = []; 54 | 55 | for (const strategyName of strategies) { 56 | try { 57 | SEND_MESSAGE_STRATEGIES[strategyName]( 58 | win, 59 | serializedMessage, 60 | domain 61 | ); 62 | } catch (err) { 63 | errors.push(err); 64 | } 65 | } 66 | 67 | if (errors.length === strategies.length) { 68 | throw new Error( 69 | `All post-robot messaging strategies failed:\n\n${errors 70 | .map((err, i) => `${i}. ${stringifyError(err)}`) 71 | .join("\n\n")}` 72 | ); 73 | } 74 | }); 75 | 76 | return domainBuffer.flush.then(() => { 77 | delete domainBuffer.flush; 78 | }); 79 | }).then(noop); 80 | } 81 | -------------------------------------------------------------------------------- /src/drivers/send/strategies.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { 4 | isSameDomain, 5 | isSameTopWindow, 6 | isActuallySameDomain, 7 | getActualDomain, 8 | getDomain, 9 | type CrossDomainWindowType, 10 | PROTOCOL, 11 | } from "@krakenjs/cross-domain-utils/src"; 12 | 13 | import { SEND_STRATEGY, WILDCARD } from "../../conf"; 14 | import { needsGlobalMessagingForBrowser } from "../../lib"; 15 | import { getGlobal } from "../../global"; 16 | import { 17 | sendBridgeMessage, 18 | needsBridgeForBrowser, 19 | isBridge, 20 | } from "../../bridge"; 21 | 22 | type SendStrategies = {| 23 | [$Values]: ( 24 | CrossDomainWindowType, 25 | string, 26 | string 27 | ) => void, 28 | |}; 29 | 30 | export const SEND_MESSAGE_STRATEGIES: SendStrategies = {}; 31 | 32 | SEND_MESSAGE_STRATEGIES[SEND_STRATEGY.POST_MESSAGE] = ( 33 | win: CrossDomainWindowType, 34 | serializedMessage: string, 35 | domain: string 36 | ) => { 37 | if (domain.indexOf(PROTOCOL.FILE) === 0) { 38 | domain = WILDCARD; 39 | } 40 | 41 | if (__TEST__) { 42 | if ( 43 | needsGlobalMessagingForBrowser() && 44 | isSameTopWindow(window, win) === false 45 | ) { 46 | return; 47 | } 48 | 49 | if (domain.indexOf(PROTOCOL.MOCK) === 0) { 50 | if (!isActuallySameDomain(win)) { 51 | throw new Error( 52 | `Attempting to send message to mock domain ${domain}, but window is actually cross-domain` 53 | ); 54 | } 55 | 56 | // $FlowFixMe 57 | const windowDomain = getDomain(win); 58 | 59 | if (windowDomain !== domain) { 60 | throw new Error( 61 | `Mock domain target ${domain} does not match window domain ${windowDomain}` 62 | ); 63 | } 64 | 65 | // $FlowFixMe 66 | domain = getActualDomain(win); 67 | } 68 | } 69 | 70 | win.postMessage(serializedMessage, domain); 71 | }; 72 | 73 | if (__POST_ROBOT__.__IE_POPUP_SUPPORT__) { 74 | SEND_MESSAGE_STRATEGIES[SEND_STRATEGY.BRIDGE] = ( 75 | win: CrossDomainWindowType, 76 | serializedMessage: string, 77 | domain: string 78 | ) => { 79 | if (!needsBridgeForBrowser() && !isBridge()) { 80 | throw new Error(`Bridge not needed for browser`); 81 | } 82 | 83 | if (isSameDomain(win)) { 84 | throw new Error( 85 | `Post message through bridge disabled between same domain windows` 86 | ); 87 | } 88 | 89 | if (isSameTopWindow(window, win) !== false) { 90 | throw new Error( 91 | `Can only use bridge to communicate between two different windows, not between frames` 92 | ); 93 | } 94 | 95 | sendBridgeMessage(win, domain, serializedMessage); 96 | }; 97 | } 98 | 99 | if ( 100 | __POST_ROBOT__.__IE_POPUP_SUPPORT__ || 101 | __POST_ROBOT__.__GLOBAL_MESSAGE_SUPPORT__ 102 | ) { 103 | SEND_MESSAGE_STRATEGIES[SEND_STRATEGY.GLOBAL] = ( 104 | win: CrossDomainWindowType, 105 | serializedMessage: string 106 | ) => { 107 | if (!needsGlobalMessagingForBrowser()) { 108 | throw new Error(`Global messaging not needed for browser`); 109 | } 110 | 111 | if (!isSameDomain(win)) { 112 | throw new Error( 113 | `Post message through global disabled between different domain windows` 114 | ); 115 | } 116 | 117 | if (isSameTopWindow(window, win) !== false) { 118 | throw new Error( 119 | `Can only use global to communicate between two different windows, not between frames` 120 | ); 121 | } 122 | 123 | // $FlowFixMe 124 | const foreignGlobal = getGlobal(win); 125 | 126 | if (!foreignGlobal) { 127 | throw new Error(`Can not find postRobot global on foreign window`); 128 | } 129 | 130 | foreignGlobal.receiveMessage({ 131 | source: window, 132 | origin: getDomain(), 133 | data: serializedMessage, 134 | }); 135 | }; 136 | } 137 | -------------------------------------------------------------------------------- /src/drivers/types.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { MESSAGE_TYPE, MESSAGE_ACK } from "../conf"; 4 | 5 | export type RequestMessage = {| 6 | id: string, 7 | origin: string, 8 | type: typeof MESSAGE_TYPE.REQUEST, 9 | name: string, 10 | hash: string, 11 | data: mixed, 12 | fireAndForget?: boolean, 13 | |}; 14 | 15 | export type AckResponseMessage = {| 16 | id: string, 17 | origin: string, 18 | type: typeof MESSAGE_TYPE.ACK, 19 | name: string, 20 | hash: string, 21 | |}; 22 | 23 | export type ResponseMessage = {| 24 | id: string, 25 | origin: string, 26 | type: typeof MESSAGE_TYPE.RESPONSE, 27 | ack: $Values, 28 | name: string, 29 | hash: string, 30 | data: ?mixed, 31 | error: ?mixed, 32 | |}; 33 | 34 | export type Message = RequestMessage | AckResponseMessage | ResponseMessage; 35 | 36 | export type PackedMessages = {| 37 | [typeof __POST_ROBOT__.__GLOBAL_KEY__]: $ReadOnlyArray, 38 | |}; 39 | -------------------------------------------------------------------------------- /src/global.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { 4 | type CrossDomainWindowType, 5 | type SameDomainWindowType, 6 | } from "@krakenjs/cross-domain-utils/src"; 7 | import { WeakMap } from "@krakenjs/cross-domain-safe-weakmap/src"; 8 | import { getOrSet, getCurrentScriptUID } from "@krakenjs/belter/src"; 9 | 10 | export function getGlobalKey(): string { 11 | if (__POST_ROBOT__.__SCRIPT_NAMESPACE__) { 12 | return `${__POST_ROBOT__.__GLOBAL_KEY__}_${getCurrentScriptUID()}`; 13 | } else { 14 | return __POST_ROBOT__.__GLOBAL_KEY__; 15 | } 16 | } 17 | 18 | export function getGlobal(win: SameDomainWindowType = window): Object { 19 | const globalKey = getGlobalKey(); 20 | 21 | if (win !== window) { 22 | return win[globalKey]; 23 | } 24 | const global: Object = (win[globalKey] = win[globalKey] || {}); 25 | return global; 26 | } 27 | 28 | export function deleteGlobal() { 29 | const globalKey = getGlobalKey(); 30 | delete window[globalKey]; 31 | } 32 | 33 | type ObjectGetter = () => Object; 34 | const getObj: ObjectGetter = () => ({}); 35 | 36 | type GetOrSet = ((string, () => T) => T) & ((string, () => void) => void); 37 | 38 | type GlobalStore = {| 39 | get: ((string, T) => T) & ((string, void) => T | void), 40 | set: (string, T) => T, 41 | has: (string) => boolean, 42 | del: (string) => void, 43 | getOrSet: GetOrSet, 44 | reset: () => void, 45 | keys: () => $ReadOnlyArray, 46 | |}; 47 | 48 | export function globalStore( 49 | key?: string = "store", 50 | defStore?: ObjectGetter = getObj 51 | ): GlobalStore { 52 | return getOrSet(getGlobal(), key, () => { 53 | let store = defStore(); 54 | 55 | return { 56 | has: (storeKey) => { 57 | return store.hasOwnProperty(storeKey); 58 | }, 59 | get: (storeKey, defVal) => { 60 | // $FlowFixMe 61 | return store.hasOwnProperty(storeKey) ? store[storeKey] : defVal; 62 | }, 63 | set: (storeKey, val) => { 64 | store[storeKey] = val; 65 | return val; 66 | }, 67 | del: (storeKey) => { 68 | delete store[storeKey]; 69 | }, 70 | getOrSet: (storeKey, getter) => { 71 | // $FlowFixMe 72 | return getOrSet(store, storeKey, getter); 73 | }, 74 | reset: () => { 75 | store = defStore(); 76 | }, 77 | keys: () => { 78 | return Object.keys(store); 79 | }, 80 | }; 81 | }); 82 | } 83 | 84 | export class WildCard {} 85 | 86 | export function getWildcard(): WildCard { 87 | const global = getGlobal(); 88 | global.WINDOW_WILDCARD = global.WINDOW_WILDCARD || new WildCard(); 89 | return global.WINDOW_WILDCARD; 90 | } 91 | 92 | type WindowStore = {| 93 | get: ((CrossDomainWindowType | WildCard, T) => T) & 94 | ((CrossDomainWindowType | WildCard, void) => T | void), 95 | set: (CrossDomainWindowType | WildCard, T) => T, 96 | has: (CrossDomainWindowType | WildCard) => boolean, 97 | del: (CrossDomainWindowType | WildCard) => void, 98 | getOrSet: (CrossDomainWindowType | WildCard, () => T) => T, 99 | |}; 100 | 101 | export function windowStore( 102 | key?: string = "store", 103 | defStore?: ObjectGetter = getObj 104 | ): WindowStore { 105 | return globalStore("windowStore").getOrSet(key, () => { 106 | const winStore = new WeakMap(); 107 | 108 | const getStore = (win: CrossDomainWindowType | WildCard): ObjectGetter => { 109 | return winStore.getOrSet(win, defStore); 110 | }; 111 | 112 | return { 113 | has: (win) => { 114 | const store = getStore(win); 115 | return store.hasOwnProperty(key); 116 | }, 117 | get: (win, defVal) => { 118 | const store = getStore(win); 119 | // $FlowFixMe 120 | return store.hasOwnProperty(key) ? store[key] : defVal; 121 | }, 122 | set: (win, val) => { 123 | const store = getStore(win); 124 | store[key] = val; 125 | return val; 126 | }, 127 | del: (win) => { 128 | const store = getStore(win); 129 | delete store[key]; 130 | }, 131 | getOrSet: (win, getter) => { 132 | const store = getStore(win); 133 | return getOrSet(store, key, getter); 134 | }, 135 | }; 136 | }); 137 | } 138 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { setup } from "./setup"; 4 | import { 5 | setupBridge, 6 | openBridge, 7 | linkWindow, 8 | linkUrl, 9 | isBridge, 10 | needsBridge, 11 | needsBridgeForBrowser, 12 | hasBridge, 13 | needsBridgeForWin, 14 | needsBridgeForDomain, 15 | destroyBridges, 16 | } from "./bridge"; 17 | 18 | export { ZalgoPromise as Promise } from "@krakenjs/zalgo-promise/src"; 19 | 20 | export * from "./types"; 21 | export { ProxyWindow } from "./serialize"; 22 | export { 23 | setup, 24 | destroy, 25 | serializeMessage, 26 | deserializeMessage, 27 | createProxyWindow, 28 | toProxyWindow, 29 | } from "./setup"; 30 | export { on, once, send } from "./public"; 31 | export { markWindowKnown } from "./lib"; 32 | export { cleanUpWindow } from "./clean"; 33 | 34 | // $FlowFixMe 35 | export let bridge; 36 | 37 | if (__POST_ROBOT__.__IE_POPUP_SUPPORT__) { 38 | bridge = { 39 | setupBridge, 40 | openBridge, 41 | linkWindow, 42 | linkUrl, 43 | isBridge, 44 | needsBridge, 45 | needsBridgeForBrowser, 46 | hasBridge, 47 | needsBridgeForWin, 48 | needsBridgeForDomain, 49 | destroyBridges, 50 | }; 51 | } 52 | 53 | if (__POST_ROBOT__.__AUTO_SETUP__) { 54 | setup(); 55 | } 56 | -------------------------------------------------------------------------------- /src/lib/compat.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { getUserAgent } from "@krakenjs/cross-domain-utils/src"; 4 | 5 | export function needsGlobalMessagingForBrowser(): boolean { 6 | if (getUserAgent(window).match(/MSIE|rv:11|trident|edge\/12|edge\/13/i)) { 7 | return true; 8 | } 9 | 10 | return false; 11 | } 12 | -------------------------------------------------------------------------------- /src/lib/hello.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { 4 | getAncestor, 5 | type CrossDomainWindowType, 6 | } from "@krakenjs/cross-domain-utils/src"; 7 | import { ZalgoPromise } from "@krakenjs/zalgo-promise/src"; 8 | import { uniqueID } from "@krakenjs/belter/src"; 9 | 10 | import { MESSAGE_NAME, WILDCARD } from "../conf"; 11 | import { windowStore, globalStore, getGlobal } from "../global"; 12 | import type { OnType, SendType, CancelableType } from "../types"; 13 | 14 | function getInstanceID(): string { 15 | return globalStore("instance").getOrSet("instanceID", uniqueID); 16 | } 17 | 18 | function getHelloPromise( 19 | win: CrossDomainWindowType 20 | ): ZalgoPromise<{| domain: string |}> { 21 | const helloPromises = windowStore("helloPromises"); 22 | return helloPromises.getOrSet(win, () => new ZalgoPromise()); 23 | } 24 | 25 | function resolveHelloPromise( 26 | win: CrossDomainWindowType, 27 | { domain } 28 | ): ZalgoPromise<{| domain: string |}> { 29 | const helloPromises = windowStore("helloPromises"); 30 | const existingPromise = helloPromises.get(win); 31 | if (existingPromise) { 32 | existingPromise.resolve({ domain }); 33 | } 34 | const newPromise = ZalgoPromise.resolve({ domain }); 35 | helloPromises.set(win, newPromise); 36 | return newPromise; 37 | } 38 | 39 | function listenForHello({ on }: {| on: OnType |}): CancelableType { 40 | return on(MESSAGE_NAME.HELLO, { domain: WILDCARD }, ({ source, origin }) => { 41 | resolveHelloPromise(source, { domain: origin }); 42 | return { instanceID: getInstanceID() }; 43 | }); 44 | } 45 | 46 | export function sayHello( 47 | win: CrossDomainWindowType, 48 | { send }: {| send: SendType |} 49 | ): ZalgoPromise<{| 50 | win: CrossDomainWindowType, 51 | domain: string, 52 | instanceID: string, 53 | |}> { 54 | return send( 55 | win, 56 | MESSAGE_NAME.HELLO, 57 | { instanceID: getInstanceID() }, 58 | { domain: WILDCARD, timeout: -1 } 59 | ).then(({ origin, data: { instanceID } }) => { 60 | resolveHelloPromise(win, { domain: origin }); 61 | return { win, domain: origin, instanceID }; 62 | }); 63 | } 64 | 65 | export function getWindowInstanceID( 66 | win: CrossDomainWindowType, 67 | { send }: {| send: SendType |} 68 | ): ZalgoPromise { 69 | return windowStore("windowInstanceIDPromises").getOrSet(win, () => { 70 | return sayHello(win, { send }).then(({ instanceID }) => instanceID); 71 | }); 72 | } 73 | 74 | export function initHello({ 75 | on, 76 | send, 77 | }: {| 78 | on: OnType, 79 | send: SendType, 80 | |}): CancelableType { 81 | return globalStore("builtinListeners").getOrSet("helloListener", () => { 82 | const listener = listenForHello({ on }); 83 | 84 | const parent = getAncestor(); 85 | if (parent) { 86 | sayHello(parent, { send }).catch((err) => { 87 | // $FlowFixMe 88 | if (__TEST__ && getGlobal(parent)) { 89 | throw err; 90 | } 91 | }); 92 | } 93 | 94 | return listener; 95 | }); 96 | } 97 | 98 | export function awaitWindowHello( 99 | win: CrossDomainWindowType, 100 | timeout: number = 5000, 101 | name: string = "Window" 102 | ): ZalgoPromise<{| domain: string |}> { 103 | let promise = getHelloPromise(win); 104 | 105 | if (timeout !== -1) { 106 | promise = promise.timeout( 107 | timeout, 108 | new Error(`${name} did not load after ${timeout}ms`) 109 | ); 110 | } 111 | 112 | return promise; 113 | } 114 | -------------------------------------------------------------------------------- /src/lib/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | export * from "./hello"; 4 | export * from "./compat"; 5 | export * from "./windows"; 6 | -------------------------------------------------------------------------------- /src/lib/windows.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { type CrossDomainWindowType } from "@krakenjs/cross-domain-utils/src"; 4 | 5 | import { windowStore } from "../global"; 6 | 7 | export function markWindowKnown(win: CrossDomainWindowType) { 8 | const knownWindows = windowStore("knownWindows"); 9 | knownWindows.set(win, true); 10 | } 11 | 12 | export function isWindowKnown(win: CrossDomainWindowType): boolean { 13 | const knownWindows = windowStore("knownWindows"); 14 | return knownWindows.get(win, false); 15 | } 16 | -------------------------------------------------------------------------------- /src/public/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | export * from "./on"; 4 | export * from "./send"; 5 | -------------------------------------------------------------------------------- /src/public/on.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { ZalgoPromise } from "@krakenjs/zalgo-promise/src"; 4 | 5 | import { addRequestListener } from "../drivers"; 6 | import { WILDCARD } from "../conf"; 7 | import type { ServerOptionsType, HandlerType, CancelableType } from "../types"; 8 | 9 | const getDefaultServerOptions = (): ServerOptionsType => { 10 | // $FlowFixMe 11 | return {}; 12 | }; 13 | 14 | export function on( 15 | name: string, 16 | options: ServerOptionsType | HandlerType, 17 | handler: ?HandlerType 18 | ): CancelableType { 19 | if (!name) { 20 | throw new Error("Expected name"); 21 | } 22 | 23 | options = options || getDefaultServerOptions(); 24 | if (typeof options === "function") { 25 | handler = options; 26 | options = getDefaultServerOptions(); 27 | } 28 | 29 | if (!handler) { 30 | throw new Error("Expected handler"); 31 | } 32 | 33 | const winOrProxyWin = options.window; 34 | const domain = options.domain || WILDCARD; 35 | 36 | const successHandler = handler || options.handler; 37 | const errorHandler = 38 | options.errorHandler || 39 | ((err) => { 40 | throw err; 41 | }); 42 | 43 | const requestListener = addRequestListener( 44 | { name, win: winOrProxyWin, domain }, 45 | { 46 | handler: successHandler, 47 | handleError: errorHandler, 48 | } 49 | ); 50 | 51 | return { 52 | cancel() { 53 | requestListener.cancel(); 54 | }, 55 | }; 56 | } 57 | 58 | type CancelableZalgoPromise = ZalgoPromise & {| 59 | cancel: () => void, 60 | |}; 61 | 62 | export function once( 63 | name: string, 64 | options?: ServerOptionsType | HandlerType, 65 | handler?: HandlerType 66 | ): CancelableZalgoPromise<{| source: mixed, origin: string, data: Object |}> { 67 | options = options || getDefaultServerOptions(); 68 | if (typeof options === "function") { 69 | handler = options; 70 | options = getDefaultServerOptions(); 71 | } 72 | 73 | const promise = new ZalgoPromise(); 74 | let listener; // eslint-disable-line prefer-const 75 | 76 | options.errorHandler = (err) => { 77 | listener.cancel(); 78 | promise.reject(err); 79 | }; 80 | 81 | listener = on(name, options, (event) => { 82 | listener.cancel(); 83 | promise.resolve(event); 84 | if (handler) { 85 | return handler(event); 86 | } 87 | }); 88 | 89 | // $FlowFixMe 90 | promise.cancel = listener.cancel; 91 | 92 | // $FlowFixMe 93 | return promise; 94 | } 95 | -------------------------------------------------------------------------------- /src/public/send.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { ZalgoPromise } from "@krakenjs/zalgo-promise/src"; 4 | import { 5 | isAncestor, 6 | isWindowClosed, 7 | getDomain, 8 | matchDomain, 9 | type CrossDomainWindowType, 10 | type DomainMatcher, 11 | } from "@krakenjs/cross-domain-utils/src"; 12 | import { 13 | uniqueID, 14 | isRegex, 15 | noop, 16 | safeInterval, 17 | stringify, 18 | stringifyError, 19 | } from "@krakenjs/belter/src"; 20 | 21 | import { 22 | CHILD_WINDOW_TIMEOUT, 23 | MESSAGE_TYPE, 24 | WILDCARD, 25 | MESSAGE_NAME, 26 | ACK_TIMEOUT, 27 | RES_TIMEOUT, 28 | ACK_TIMEOUT_KNOWN, 29 | RESPONSE_CYCLE_TIME, 30 | } from "../conf"; 31 | import { 32 | sendMessage, 33 | addResponseListener, 34 | deleteResponseListener, 35 | markResponseListenerErrored, 36 | type ResponseListenerType, 37 | } from "../drivers"; 38 | import { awaitWindowHello, sayHello, isWindowKnown } from "../lib"; 39 | import { windowStore } from "../global"; 40 | import { ProxyWindow } from "../serialize/window"; 41 | import type { SendType } from "../types"; 42 | 43 | import { on } from "./on"; 44 | 45 | function validateOptions( 46 | name: string, 47 | win: CrossDomainWindowType, 48 | domain: ?DomainMatcher 49 | ) { 50 | if (!name) { 51 | throw new Error("Expected name"); 52 | } 53 | 54 | if (domain) { 55 | if ( 56 | typeof domain !== "string" && 57 | !Array.isArray(domain) && 58 | !isRegex(domain) 59 | ) { 60 | throw new TypeError( 61 | `Can not send ${name}. Expected domain ${JSON.stringify( 62 | domain 63 | )} to be a string, array, or regex` 64 | ); 65 | } 66 | } 67 | 68 | if (isWindowClosed(win)) { 69 | throw new Error(`Can not send ${name}. Target window is closed`); 70 | } 71 | } 72 | 73 | function normalizeDomain( 74 | win: CrossDomainWindowType, 75 | targetDomain: DomainMatcher, 76 | actualDomain: ?string, 77 | { send }: {| send: SendType |} 78 | ): ZalgoPromise { 79 | return ZalgoPromise.try(() => { 80 | if (typeof targetDomain === "string") { 81 | return targetDomain; 82 | } 83 | 84 | return ZalgoPromise.try(() => { 85 | return ( 86 | actualDomain || sayHello(win, { send }).then(({ domain }) => domain) 87 | ); 88 | }).then((normalizedDomain) => { 89 | if (!matchDomain(targetDomain, targetDomain)) { 90 | throw new Error( 91 | `Domain ${stringify(targetDomain)} does not match ${stringify( 92 | targetDomain 93 | )}` 94 | ); 95 | } 96 | 97 | return normalizedDomain; 98 | }); 99 | }); 100 | } 101 | 102 | export const send: SendType = (winOrProxyWin, name, data, options) => { 103 | options = options || {}; 104 | const domainMatcher = options.domain || WILDCARD; 105 | const responseTimeout = options.timeout || RES_TIMEOUT; 106 | const childTimeout = options.timeout || CHILD_WINDOW_TIMEOUT; 107 | const fireAndForget = options.fireAndForget || false; 108 | 109 | return ProxyWindow.toProxyWindow(winOrProxyWin, { send }) 110 | .awaitWindow() 111 | .then((win) => { 112 | // $FlowFixMe 113 | return ZalgoPromise.try(() => { 114 | validateOptions(name, win, domainMatcher); 115 | 116 | if (isAncestor(window, win)) { 117 | return awaitWindowHello(win, childTimeout); 118 | } 119 | }) 120 | .then(({ domain: actualDomain } = {}) => { 121 | return normalizeDomain(win, domainMatcher, actualDomain, { send }); 122 | }) 123 | .then((targetDomain) => { 124 | const domain = targetDomain; 125 | 126 | const logName = 127 | name === MESSAGE_NAME.METHOD && 128 | data && 129 | typeof data.name === "string" 130 | ? `${data.name}()` 131 | : name; 132 | 133 | if (__DEBUG__) { 134 | console.info("send::req", logName, domain, "\n\n", data); // eslint-disable-line no-console 135 | } 136 | 137 | const promise = new ZalgoPromise(); 138 | const hash = `${name}_${uniqueID()}`; 139 | 140 | if (!fireAndForget) { 141 | const responseListener: ResponseListenerType = { 142 | name, 143 | win, 144 | domain, 145 | promise, 146 | }; 147 | addResponseListener(hash, responseListener); 148 | 149 | const reqPromises = windowStore("requestPromises").getOrSet( 150 | win, 151 | () => [] 152 | ); 153 | reqPromises.push(promise); 154 | 155 | promise.catch(() => { 156 | markResponseListenerErrored(hash); 157 | deleteResponseListener(hash); 158 | }); 159 | 160 | const totalAckTimeout = isWindowKnown(win) 161 | ? ACK_TIMEOUT_KNOWN 162 | : ACK_TIMEOUT; 163 | const totalResTimeout = responseTimeout; 164 | 165 | let ackTimeout = totalAckTimeout; 166 | let resTimeout = totalResTimeout; 167 | 168 | const interval = safeInterval(() => { 169 | if (isWindowClosed(win)) { 170 | return promise.reject( 171 | new Error( 172 | `Window closed for ${name} before ${ 173 | responseListener.ack ? "response" : "ack" 174 | }` 175 | ) 176 | ); 177 | } 178 | 179 | if (responseListener.cancelled) { 180 | return promise.reject( 181 | new Error(`Response listener was cancelled for ${name}`) 182 | ); 183 | } 184 | 185 | ackTimeout = Math.max(ackTimeout - RESPONSE_CYCLE_TIME, 0); 186 | if (resTimeout !== -1) { 187 | resTimeout = Math.max(resTimeout - RESPONSE_CYCLE_TIME, 0); 188 | } 189 | 190 | if (!responseListener.ack && ackTimeout === 0) { 191 | return promise.reject( 192 | new Error( 193 | `No ack for postMessage ${logName} in ${getDomain()} in ${totalAckTimeout}ms` 194 | ) 195 | ); 196 | } else if (resTimeout === 0) { 197 | return promise.reject( 198 | new Error( 199 | `No response for postMessage ${logName} in ${getDomain()} in ${totalResTimeout}ms` 200 | ) 201 | ); 202 | } 203 | }, RESPONSE_CYCLE_TIME); 204 | 205 | promise 206 | .finally(() => { 207 | interval.cancel(); 208 | reqPromises.splice(reqPromises.indexOf(promise, 1)); 209 | }) 210 | .catch(noop); 211 | } 212 | 213 | return sendMessage( 214 | win, 215 | domain, 216 | { 217 | id: uniqueID(), 218 | origin: getDomain(window), 219 | type: MESSAGE_TYPE.REQUEST, 220 | hash, 221 | name, 222 | data, 223 | fireAndForget, 224 | }, 225 | { on, send } 226 | ).then( 227 | () => { 228 | return fireAndForget ? promise.resolve() : promise; 229 | }, 230 | (err) => { 231 | throw new Error( 232 | `Send request message failed for ${logName} in ${getDomain()}\n\n${stringifyError( 233 | err 234 | )}` 235 | ); 236 | } 237 | ); 238 | }); 239 | }); 240 | }; 241 | -------------------------------------------------------------------------------- /src/serialize/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | export * from "./serialize"; 4 | export * from "./function"; 5 | export * from "./promise"; 6 | export * from "./window"; 7 | -------------------------------------------------------------------------------- /src/serialize/promise.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { 4 | type CrossDomainWindowType, 5 | type DomainMatcher, 6 | } from "@krakenjs/cross-domain-utils/src"; 7 | import { ZalgoPromise } from "@krakenjs/zalgo-promise/src"; 8 | import { 9 | serializeType, 10 | type CustomSerializedType, 11 | type Thenable, 12 | } from "@krakenjs/universal-serialize/src"; 13 | 14 | import { SERIALIZATION_TYPE } from "../conf"; 15 | import type { OnType, SendType } from "../types"; 16 | 17 | import { serializeFunction, type SerializedFunction } from "./function"; 18 | import { ProxyWindow } from "./window"; 19 | 20 | export type SerializedPromise = CustomSerializedType< 21 | typeof SERIALIZATION_TYPE.CROSS_DOMAIN_ZALGO_PROMISE, 22 | {| 23 | then: SerializedFunction, 24 | |} 25 | >; 26 | 27 | export function serializePromise( 28 | destination: CrossDomainWindowType | ProxyWindow, 29 | domain: DomainMatcher, 30 | val: Thenable, 31 | key: string, 32 | { on, send }: {| on: OnType, send: SendType |} 33 | ): SerializedPromise { 34 | return serializeType(SERIALIZATION_TYPE.CROSS_DOMAIN_ZALGO_PROMISE, { 35 | then: serializeFunction( 36 | destination, 37 | domain, 38 | (resolve, reject) => val.then(resolve, reject), 39 | key, 40 | { on, send } 41 | ), 42 | }); 43 | } 44 | 45 | export function deserializePromise( 46 | source: CrossDomainWindowType | ProxyWindow, 47 | origin: string, 48 | { then }: {| then: Function |} 49 | ): ZalgoPromise { 50 | return new ZalgoPromise(then); 51 | } 52 | -------------------------------------------------------------------------------- /src/serialize/serialize.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { 4 | type CrossDomainWindowType, 5 | isWindow, 6 | type DomainMatcher, 7 | } from "@krakenjs/cross-domain-utils/src"; 8 | import { 9 | TYPE, 10 | serialize, 11 | deserialize, 12 | type Thenable, 13 | } from "@krakenjs/universal-serialize/src"; 14 | 15 | import { SERIALIZATION_TYPE } from "../conf"; 16 | import type { OnType, SendType } from "../types"; 17 | 18 | import { 19 | serializeFunction, 20 | deserializeFunction, 21 | type SerializedFunction, 22 | } from "./function"; 23 | import { 24 | serializePromise, 25 | deserializePromise, 26 | type SerializedPromise, 27 | } from "./promise"; 28 | import { 29 | serializeWindow, 30 | deserializeWindow, 31 | type SerializedWindow, 32 | ProxyWindow, 33 | } from "./window"; 34 | 35 | export function serializeMessage( 36 | destination: CrossDomainWindowType | ProxyWindow, 37 | domain: DomainMatcher, 38 | obj: T, 39 | { on, send }: {| on: OnType, send: SendType |} 40 | ): string { 41 | return serialize(obj, { 42 | [TYPE.PROMISE]: (val: Thenable, key: string): SerializedPromise => 43 | serializePromise(destination, domain, val, key, { on, send }), 44 | [TYPE.FUNCTION]: (val: Function, key: string): SerializedFunction => 45 | serializeFunction(destination, domain, val, key, { on, send }), 46 | [TYPE.OBJECT]: (val: CrossDomainWindowType): Object | SerializedWindow => { 47 | return isWindow(val) || ProxyWindow.isProxyWindow(val) 48 | ? serializeWindow(destination, domain, val, { send }) 49 | : val; 50 | }, 51 | }); 52 | } 53 | 54 | export function deserializeMessage( 55 | source: CrossDomainWindowType | ProxyWindow, 56 | origin: string, 57 | message: string, 58 | { send }: {| on: OnType, send: SendType |} 59 | ): T { 60 | return deserialize(message, { 61 | [SERIALIZATION_TYPE.CROSS_DOMAIN_ZALGO_PROMISE]: (serializedPromise) => 62 | deserializePromise(source, origin, serializedPromise), 63 | [SERIALIZATION_TYPE.CROSS_DOMAIN_FUNCTION]: (serializedFunction) => 64 | deserializeFunction(source, origin, serializedFunction, { send }), 65 | [SERIALIZATION_TYPE.CROSS_DOMAIN_WINDOW]: (serializedWindow) => 66 | deserializeWindow(source, origin, serializedWindow, { send }), 67 | }); 68 | } 69 | -------------------------------------------------------------------------------- /src/setup.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import type { 4 | CrossDomainWindowType, 5 | DomainMatcher, 6 | } from "@krakenjs/cross-domain-utils/src"; 7 | 8 | import { initHello } from "./lib"; 9 | import { 10 | listenForMessages, 11 | stopListenForMessages, 12 | receiveMessage, 13 | setupGlobalReceiveMessage, 14 | cancelResponseListeners, 15 | } from "./drivers"; 16 | import { getGlobal, deleteGlobal } from "./global"; 17 | import { on, send } from "./public"; 18 | import { setupBridge } from "./bridge"; 19 | import { 20 | serializeMessage as internalSerializeMessage, 21 | deserializeMessage as internalDeserializeMessage, 22 | ProxyWindow, 23 | } from "./serialize"; 24 | 25 | export function serializeMessage( 26 | destination: CrossDomainWindowType | ProxyWindow, 27 | domain: DomainMatcher, 28 | obj: T 29 | ): string { 30 | return internalSerializeMessage(destination, domain, obj, { on, send }); 31 | } 32 | 33 | export function deserializeMessage( 34 | source: CrossDomainWindowType | ProxyWindow, 35 | origin: string, 36 | message: string 37 | ): T { 38 | return internalDeserializeMessage(source, origin, message, { on, send }); 39 | } 40 | 41 | export function createProxyWindow(win?: CrossDomainWindowType): ProxyWindow { 42 | return new ProxyWindow({ send, win }); 43 | } 44 | 45 | export function toProxyWindow( 46 | win: CrossDomainWindowType | ProxyWindow 47 | ): ProxyWindow { 48 | return ProxyWindow.toProxyWindow(win, { send }); 49 | } 50 | 51 | export function setup() { 52 | if (!getGlobal().initialized) { 53 | getGlobal().initialized = true; 54 | 55 | setupGlobalReceiveMessage({ on, send }); 56 | listenForMessages({ on, send }); 57 | 58 | if (__POST_ROBOT__.__IE_POPUP_SUPPORT__) { 59 | setupBridge({ on, send, receiveMessage }); 60 | } 61 | 62 | initHello({ on, send }); 63 | } 64 | } 65 | 66 | export function destroy() { 67 | cancelResponseListeners(); 68 | stopListenForMessages(); 69 | deleteGlobal(); 70 | } 71 | -------------------------------------------------------------------------------- /src/types.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import type { ZalgoPromise } from "@krakenjs/zalgo-promise/src"; 4 | import type { 5 | CrossDomainWindowType, 6 | DomainMatcher, 7 | } from "@krakenjs/cross-domain-utils/src"; 8 | 9 | import type { ProxyWindow } from "./serialize/window"; 10 | 11 | // export something to force webpack to see this as an ES module 12 | export const TYPES = true; 13 | 14 | // eslint-disable-next-line flowtype/require-exact-type 15 | export type CancelableType = { 16 | cancel: () => void, 17 | }; 18 | 19 | export type ErrorHandlerType = (err: mixed) => void; 20 | 21 | export type HandlerType = ({| 22 | source: CrossDomainWindowType, 23 | origin: string, 24 | data: any, // eslint-disable-line flowtype/no-weak-types 25 | |}) => void | any | ZalgoPromise; // eslint-disable-line flowtype/no-weak-types 26 | 27 | export type ServerOptionsType = {| 28 | handler?: ?HandlerType, 29 | errorHandler?: ?ErrorHandlerType, 30 | window?: CrossDomainWindowType | ProxyWindow, 31 | name?: ?string, 32 | domain?: ?DomainMatcher, 33 | once?: ?boolean, 34 | errorOnClose?: ?boolean, 35 | |}; 36 | 37 | export type OnType = ( 38 | name: string, 39 | options: ServerOptionsType | HandlerType, 40 | handler: ?HandlerType 41 | ) => CancelableType; 42 | 43 | type RegularRequestOptionsType = {| 44 | domain?: ?DomainMatcher, 45 | fireAndForget?: false, 46 | timeout?: ?number, 47 | |}; 48 | 49 | type FireAndForgetRequestOptionsType = {| 50 | domain?: ?DomainMatcher, 51 | fireAndForget: true, 52 | timeout?: ?number, 53 | |}; 54 | 55 | export type RequestOptionsType = 56 | | RegularRequestOptionsType 57 | | FireAndForgetRequestOptionsType; 58 | 59 | export type ResponseMessageEvent = {| 60 | source: CrossDomainWindowType, 61 | origin: string, 62 | data: Object, 63 | |}; 64 | 65 | type RegularSendType = ( 66 | win: CrossDomainWindowType | ProxyWindow, 67 | name: string, 68 | data: ?Object, 69 | options?: RegularRequestOptionsType 70 | ) => ZalgoPromise; 71 | 72 | type FireAndForgetSendType = ( 73 | win: CrossDomainWindowType | ProxyWindow, 74 | name: string, 75 | data: ?Object, 76 | options?: FireAndForgetRequestOptionsType 77 | ) => ZalgoPromise; 78 | 79 | export type SendType = RegularSendType & FireAndForgetSendType; 80 | 81 | export type MessageEvent = {| 82 | source: CrossDomainWindowType, 83 | origin: string, 84 | data: string, 85 | |}; 86 | 87 | // eslint-disable-next-line flowtype/require-exact-type 88 | export type CrossDomainFunctionType = { 89 | (...args: A): ZalgoPromise, 90 | fireAndForget: (...args: A) => ZalgoPromise, 91 | __id__?: string, 92 | __name__?: string, 93 | }; 94 | 95 | export type ReceiveMessageType = ( 96 | MessageEvent, 97 | {| on: OnType, send: SendType |} 98 | ) => void; 99 | -------------------------------------------------------------------------------- /test/bridge.htm: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /test/child.htm: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /test/child.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { send, on, once } from "../src"; 4 | 5 | on("sendMessageToParent", ({ data }) => { 6 | return send(window.opener || window.parent, data.messageName, data.data).then( 7 | (event) => event.data 8 | ); 9 | }); 10 | 11 | on("setupListener", ({ data }) => { 12 | once(data.messageName, () => { 13 | return data.handler ? data.handler() : data.data; 14 | }); 15 | }); 16 | 17 | on("waitForMessage", ({ data }) => { 18 | return once(data.messageName, () => { 19 | return data.handler ? data.handler() : data.data; 20 | }).then((event) => event.data); 21 | }); 22 | 23 | window.addEventListener("error", () => { 24 | // pass 25 | }); 26 | -------------------------------------------------------------------------------- /test/common.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { ZalgoPromise } from "@krakenjs/zalgo-promise/src"; 4 | import { getBody, noop } from "@krakenjs/belter/src"; 5 | import { type CrossDomainWindowType } from "@krakenjs/cross-domain-utils/src"; 6 | 7 | import { awaitWindowHello } from "../src/lib"; 8 | 9 | window.mockDomain = "mock://test-post-robot.com"; 10 | 11 | window.console.karma = (...args) => { 12 | const karma = 13 | window.karma || 14 | (window.top && window.top.karma) || 15 | (window.opener && window.opener.karma); 16 | if (karma) { 17 | karma.log("debug", args); 18 | } 19 | // eslint-disable-next-line no-console 20 | console.log(...args); 21 | }; 22 | 23 | const IE8_USER_AGENT = 24 | "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0)"; 25 | 26 | export function enableIE8Mode(): {| cancel: () => void |} { 27 | window.navigator.mockUserAgent = IE8_USER_AGENT; 28 | 29 | return { 30 | cancel() { 31 | delete window.navigator.mockUserAgent; 32 | }, 33 | }; 34 | } 35 | 36 | export function createIframe( 37 | name: string, 38 | callback?: () => void 39 | ): CrossDomainWindowType { 40 | const frame = document.createElement("iframe"); 41 | frame.src = `/base/test/${name}`; 42 | frame.id = "childframe"; 43 | frame.name = `${Math.random().toString()}_${name.replace( 44 | /[^a-zA-Z0-9]+/g, 45 | "_" 46 | )}`; 47 | if (callback) { 48 | frame.addEventListener("load", callback); 49 | } 50 | getBody().appendChild(frame); 51 | return frame.contentWindow; 52 | } 53 | 54 | export function createPopup(name: string): CrossDomainWindowType { 55 | const popup = window.open( 56 | `mock://test-post-robot-child.com/base/test/${name}`, 57 | `${Math.random().toString()}_${name.replace(/[^a-zA-Z0-9]+/g, "_")}` 58 | ); 59 | window.focus(); 60 | return popup; 61 | } 62 | 63 | let childWindow; 64 | let childFrame; 65 | let otherChildFrame; 66 | 67 | type Windows = {| 68 | childWindow: CrossDomainWindowType, 69 | childFrame: CrossDomainWindowType, 70 | otherChildFrame: CrossDomainWindowType, 71 | |}; 72 | 73 | export function getWindows(): Windows { 74 | if (!childFrame || !childWindow || !otherChildFrame) { 75 | throw new Error(`Not all windows available`); 76 | } 77 | 78 | return { 79 | childWindow, 80 | childFrame, 81 | otherChildFrame, 82 | }; 83 | } 84 | 85 | before((): ZalgoPromise => { 86 | childWindow = createPopup("child.htm"); 87 | childFrame = createIframe("child.htm"); 88 | otherChildFrame = createIframe("child.htm"); 89 | 90 | return ZalgoPromise.all([ 91 | awaitWindowHello(childWindow), 92 | awaitWindowHello(childFrame), 93 | awaitWindowHello(otherChildFrame), 94 | ]).then(noop); 95 | }); 96 | 97 | after(() => { 98 | if (!document.body) { 99 | throw new Error(`Expected document.body to be available`); 100 | } 101 | const body = document.body; 102 | // $FlowFixMe 103 | if (!childFrame.frameElement) { 104 | throw new Error(`Expected childFrame.frameElement to be available`); 105 | } 106 | body.removeChild(childFrame.frameElement); 107 | // $FlowFixMe 108 | if (!otherChildFrame.frameElement) { 109 | throw new Error(`Expected otherChildFrame.frameElement to be available`); 110 | } 111 | body.removeChild(otherChildFrame.frameElement); 112 | childWindow.close(); 113 | }); 114 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import "./tests"; 4 | -------------------------------------------------------------------------------- /test/tests/error.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | /* eslint max-lines: 0 */ 3 | 4 | import { ZalgoPromise } from "@krakenjs/zalgo-promise/src"; 5 | import { wrapPromise } from "@krakenjs/belter/src"; 6 | 7 | import { on, send } from "../../src"; 8 | import { getWindows } from "../common"; 9 | 10 | describe("Error cases", () => { 11 | it("should get an error when messaging with an unknown name", (): ZalgoPromise => { 12 | const { childFrame } = getWindows(); 13 | 14 | return send(childFrame, "doesntexist").then( 15 | () => { 16 | throw new Error("Expected success handler to not be called"); 17 | }, 18 | (err) => { 19 | if (!err) { 20 | throw new Error(`Expected err`); 21 | } 22 | } 23 | ); 24 | }); 25 | 26 | it("should error out if you try to register the same listener name twice", () => { 27 | on("onceonly", () => { 28 | // pass 29 | }); 30 | 31 | try { 32 | on("onceonly", () => { 33 | // pass 34 | }); 35 | } catch (err) { 36 | if (!err) { 37 | throw new Error(`Expected err`); 38 | } 39 | return; 40 | } 41 | 42 | throw new Error("Expected error handler to be called"); 43 | }); 44 | 45 | it("should fail to send a message when the expected domain does not match", () => { 46 | return wrapPromise(({ expect, avoid }) => { 47 | const { childFrame } = getWindows(); 48 | 49 | on( 50 | "foobuzzzzz", 51 | { domain: "http://www.zombo.com" }, 52 | avoid("onFoobuzzzzz") 53 | ); 54 | 55 | send(childFrame, "sendMessageToParent", { 56 | messageName: "foobuzzzzz", 57 | }).then( 58 | avoid("successHandler"), 59 | expect("errorHandler", (err) => { 60 | if (!err) { 61 | throw new Error(`Expected err`); 62 | } 63 | }) 64 | ); 65 | }); 66 | }); 67 | 68 | it("should fail to send a message when the target domain does not match", (): ZalgoPromise => { 69 | const { childFrame } = getWindows(); 70 | 71 | return send(childFrame, "setupListener", { 72 | messageName: "foo", 73 | data: { 74 | foo: "bar", 75 | }, 76 | }).then(() => { 77 | return send( 78 | childFrame, 79 | "foo", 80 | {}, 81 | { domain: "http://www.zombo.com" } 82 | ).then( 83 | () => { 84 | throw new Error("Expected success handler to not be called"); 85 | }, 86 | (err) => { 87 | if (!err) { 88 | throw new Error(`Expected err`); 89 | } 90 | } 91 | ); 92 | }); 93 | }); 94 | }); 95 | -------------------------------------------------------------------------------- /test/tests/happy.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | /* eslint max-lines: 0 */ 3 | 4 | import { ZalgoPromise } from "@krakenjs/zalgo-promise/src"; 5 | import { wrapPromise } from "@krakenjs/belter/src"; 6 | 7 | import { on, send } from "../../src"; 8 | import { getWindows } from "../common"; 9 | 10 | describe("Happy cases", () => { 11 | it("should set up a simple server and listen for a request", () => { 12 | return wrapPromise(({ expect }) => { 13 | const { childFrame } = getWindows(); 14 | 15 | on("foobu", expect("onFoobu")); 16 | 17 | return send(childFrame, "sendMessageToParent", { 18 | messageName: "foobu", 19 | }).then(expect("sendSuccess")); 20 | }); 21 | }); 22 | 23 | it("should set up a simple server and listen for multiple requests", (): ZalgoPromise => { 24 | const { childFrame } = getWindows(); 25 | 26 | let count = 0; 27 | 28 | on("multilistener", () => { 29 | count += 1; 30 | }); 31 | 32 | return send(childFrame, "sendMessageToParent", { 33 | messageName: "multilistener", 34 | }) 35 | .then(() => { 36 | return send(childFrame, "sendMessageToParent", { 37 | messageName: "multilistener", 38 | }); 39 | }) 40 | .then(() => { 41 | if (count !== 2) { 42 | throw new Error(`Expected count to be 2, got ${count}`); 43 | } 44 | }); 45 | }); 46 | 47 | it("should message a child and expect a response", (): ZalgoPromise => { 48 | const { childFrame } = getWindows(); 49 | 50 | return send(childFrame, "setupListener", { 51 | messageName: "foo", 52 | data: { 53 | foo: "bar", 54 | }, 55 | }).then(() => { 56 | return send(childFrame, "foo").then(({ data }) => { 57 | if (data.foo !== "bar") { 58 | throw new Error(`Expected data.foo to be 'bar', got ${data.foo}`); 59 | } 60 | }); 61 | }); 62 | }); 63 | 64 | it("should set up a simple server and listen for a request from a specific domain", () => { 65 | return wrapPromise(({ expect }) => { 66 | const { childFrame } = getWindows(); 67 | 68 | on( 69 | "domainspecificmessage", 70 | { domain: "mock://test-post-robot-child.com" }, 71 | expect("onDomainspecificmessage") 72 | ); 73 | 74 | return send(childFrame, "sendMessageToParent", { 75 | messageName: "domainspecificmessage", 76 | }).then(expect("sendSuccess")); 77 | }); 78 | }); 79 | 80 | it("should message a child with a specific domain and expect a response", (): ZalgoPromise => { 81 | const { childFrame } = getWindows(); 82 | 83 | return send( 84 | childFrame, 85 | "setupListener", 86 | { 87 | messageName: "domainspecificmessage", 88 | data: { 89 | foo: "bar", 90 | }, 91 | }, 92 | { domain: "mock://test-post-robot-child.com" } 93 | ).then(() => { 94 | return send(childFrame, "domainspecificmessage").then(({ data }) => { 95 | if (data.foo !== "bar") { 96 | throw new Error(`Expected data.foo to be 'bar', got ${data.foo}`); 97 | } 98 | }); 99 | }); 100 | }); 101 | 102 | it("should set up a simple server and listen for a request from multiple domains", () => { 103 | return wrapPromise(({ expect }) => { 104 | const { childFrame } = getWindows(); 105 | 106 | on( 107 | "multidomainspecificmessage", 108 | { 109 | domain: [ 110 | "mock://test-post-robot-child.com", 111 | "mock://non-existant-domain.com", 112 | ], 113 | }, 114 | expect("onMultidomainspecificmessage") 115 | ); 116 | 117 | send(childFrame, "sendMessageToParent", { 118 | messageName: "multidomainspecificmessage", 119 | }).then(expect("sendSuccess")); 120 | }); 121 | }); 122 | 123 | it("should message a child with multiple domains and expect a response", (): ZalgoPromise => { 124 | const { childFrame } = getWindows(); 125 | 126 | return send( 127 | childFrame, 128 | "setupListener", 129 | { 130 | messageName: "multidomainspecificmessage", 131 | data: { 132 | foo: "bar", 133 | }, 134 | }, 135 | { 136 | domain: [ 137 | "mock://test-post-robot-child.com", 138 | "mock://non-existant-domain.com", 139 | ], 140 | } 141 | ).then(() => { 142 | return send(childFrame, "multidomainspecificmessage").then(({ data }) => { 143 | if (data.foo !== "bar") { 144 | throw new Error(`Expected data.foo to be 'bar', got ${data.foo}`); 145 | } 146 | }); 147 | }); 148 | }); 149 | }); 150 | -------------------------------------------------------------------------------- /test/tests/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import "./happy"; 4 | import "./options"; 5 | import "./serialization"; 6 | import "./popup"; 7 | import "./error"; 8 | import "./window-proxy"; 9 | -------------------------------------------------------------------------------- /test/tests/options.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | /* eslint max-lines: 0 */ 3 | 4 | import { ZalgoPromise } from "@krakenjs/zalgo-promise/src"; 5 | 6 | import { on, send, once } from "../../src"; 7 | import { getWindows } from "../common"; 8 | 9 | describe("Options cases", () => { 10 | it("should be able to listen for a message only once", (): ZalgoPromise => { 11 | const { childFrame } = getWindows(); 12 | 13 | let count = 0; 14 | 15 | once("foobuz", () => { 16 | count += 1; 17 | }); 18 | 19 | return send(childFrame, "sendMessageToParent", { 20 | messageName: "foobuz", 21 | }).then(() => { 22 | return send(childFrame, "sendMessageToParent", { 23 | messageName: "foobuz", 24 | }).then( 25 | () => { 26 | throw new Error("Expected success handler to not be called"); 27 | }, 28 | () => { 29 | if (count !== 1) { 30 | throw new Error(`Expected count to be 1, got ${count}`); 31 | } 32 | } 33 | ); 34 | }); 35 | }); 36 | 37 | it("should be able to re-register the same once handler after the first is called", (): ZalgoPromise => { 38 | const { childFrame } = getWindows(); 39 | 40 | let count = 0; 41 | 42 | once("foobuzz", ({ data }) => { 43 | count += data.add; 44 | }); 45 | 46 | return send(childFrame, "sendMessageToParent", { 47 | messageName: "foobuzz", 48 | data: { 49 | add: 2, 50 | }, 51 | }) 52 | .then(() => { 53 | once("foobuzz", ({ data }) => { 54 | count += data.add; 55 | }); 56 | 57 | return send(childFrame, "sendMessageToParent", { 58 | messageName: "foobuzz", 59 | data: { 60 | add: 3, 61 | }, 62 | }); 63 | }) 64 | .then(() => { 65 | if (count !== 5) { 66 | throw new Error(`Expected count to be 5, got ${count}`); 67 | } 68 | }); 69 | }); 70 | 71 | it("should allow you to register the same listener twice providing it is to different windows", () => { 72 | const { childFrame, otherChildFrame } = getWindows(); 73 | 74 | on("onceonlywindow", { window: childFrame }, () => { 75 | // pass 76 | }); 77 | 78 | on("onceonlywindow", { window: otherChildFrame }, () => { 79 | // pass 80 | }); 81 | }); 82 | 83 | it("should allow you to register a listener for a specific window", (): ZalgoPromise => { 84 | const { childFrame, otherChildFrame } = getWindows(); 85 | 86 | let count = 0; 87 | 88 | on("specificchildlistener", { window: otherChildFrame }, () => { 89 | count += 1; 90 | }); 91 | 92 | return send(otherChildFrame, "sendMessageToParent", { 93 | messageName: "specificchildlistener", 94 | }).then(() => { 95 | return send(childFrame, "sendMessageToParent", { 96 | messageName: "specificchildlistener", 97 | }).then( 98 | () => { 99 | throw new Error("Expected success handler to not be called"); 100 | }, 101 | (err) => { 102 | if (!err) { 103 | throw new Error(`Expected err`); 104 | } 105 | if (count !== 1) { 106 | throw new Error(`Expected count to be 1, got ${count}`); 107 | } 108 | } 109 | ); 110 | }); 111 | }); 112 | }); 113 | -------------------------------------------------------------------------------- /test/tests/popup.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | /* eslint max-lines: 0 */ 3 | 4 | import { ZalgoPromise } from "@krakenjs/zalgo-promise/src"; 5 | 6 | import { send, bridge } from "../../src"; 7 | import { awaitWindowHello } from "../../src/lib"; 8 | import { enableIE8Mode, createPopup, getWindows } from "../common"; 9 | 10 | describe("Popup cases", () => { 11 | it("should work with a popup window", (): ZalgoPromise => { 12 | const { childWindow } = getWindows(); 13 | 14 | return send(childWindow, "setupListener", { 15 | messageName: "foo", 16 | data: { 17 | foo: "bar", 18 | }, 19 | }).then(() => { 20 | return send(childWindow, "foo").then(({ data }) => { 21 | if (data.foo !== "bar") { 22 | throw new Error(`Expected data.foo to be 'bar', got ${data.foo}`); 23 | } 24 | }); 25 | }); 26 | }); 27 | 28 | it("should succeed messaging popup when emulating IE", (): ZalgoPromise => { 29 | const ie8mode = enableIE8Mode(); 30 | 31 | if (!bridge) { 32 | throw new Error(`Bridge not found`); 33 | } 34 | 35 | return bridge 36 | .openBridge("/base/test/bridge.htm", "mock://test-post-robot-child.com") 37 | .then(() => { 38 | const ie8Window = createPopup("child.htm"); 39 | 40 | return awaitWindowHello(ie8Window) 41 | .then(() => { 42 | return send(ie8Window, "setupListener", { 43 | messageName: "foo", 44 | data: { 45 | foo: "bar", 46 | }, 47 | }); 48 | }) 49 | .then(() => { 50 | return send(ie8Window, "foo"); 51 | }) 52 | .then(() => { 53 | ie8Window.close(); 54 | ie8mode.cancel(); 55 | }); 56 | }); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /test/tests/window-proxy.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | /* eslint max-lines: 0, max-nested-callbacks: off */ 3 | 4 | import { wrapPromise } from "@krakenjs/belter/src"; 5 | 6 | import { on, send } from "../../src"; 7 | import { getWindows } from "../common"; 8 | 9 | describe("Window Proxy cases", () => { 10 | it("Should send the a window in a message", () => { 11 | return wrapPromise(({ expect }) => { 12 | const { childFrame, otherChildFrame } = getWindows(); 13 | 14 | const listener = on( 15 | "passProxyWindow", 16 | expect("passProxyWindow", ({ data }) => { 17 | if (data.otherFrame.getWindow() !== otherChildFrame) { 18 | throw new Error(`Expected window to be correctly passed`); 19 | } 20 | 21 | listener.cancel(); 22 | }) 23 | ); 24 | 25 | return send(childFrame, "sendMessageToParent", { 26 | messageName: "passProxyWindow", 27 | data: { 28 | otherFrame: otherChildFrame, 29 | }, 30 | }).then(expect("sendSuccess")); 31 | }); 32 | }); 33 | 34 | it("Should send the a window in a message, then call isPopup", () => { 35 | return wrapPromise(({ expect }) => { 36 | const { childFrame, otherChildFrame } = getWindows(); 37 | 38 | const listener = on( 39 | "passProxyWindow", 40 | expect("passProxyWindow", ({ data }) => { 41 | return data.otherFrame.isPopup().then((isPopup) => { 42 | listener.cancel(); 43 | if (isPopup !== false) { 44 | throw new Error( 45 | `Expected isPopup to be false but got ${isPopup}` 46 | ); 47 | } 48 | }); 49 | }) 50 | ); 51 | 52 | return send(childFrame, "sendMessageToParent", { 53 | messageName: "passProxyWindow", 54 | data: { 55 | otherFrame: otherChildFrame, 56 | }, 57 | }).then(expect("sendSuccess")); 58 | }); 59 | }); 60 | 61 | it("Should send a message to a proxy window", () => { 62 | return wrapPromise(({ expect }) => { 63 | const { childFrame, otherChildFrame } = getWindows(); 64 | 65 | const passListener = on( 66 | "passProxyWindow", 67 | expect("passProxyWindow", ({ data }) => { 68 | if (data.otherFrame.getWindow() !== otherChildFrame) { 69 | throw new Error(`Expected window to be correctly passed`); 70 | } 71 | 72 | passListener.cancel(); 73 | 74 | return send(data.otherFrame, "sendMessageToParent", { 75 | messageName: "callProxyWindow", 76 | }).then(expect("sendSuccess")); 77 | }) 78 | ); 79 | 80 | const callListener = on( 81 | "callProxyWindow", 82 | expect("onCallProxyWindow", () => { 83 | callListener.cancel(); 84 | }) 85 | ); 86 | 87 | return send(childFrame, "sendMessageToParent", { 88 | messageName: "passProxyWindow", 89 | data: { 90 | otherFrame: otherChildFrame, 91 | }, 92 | }).then(expect("sendSuccess")); 93 | }); 94 | }); 95 | 96 | it("Should receive a message from a proxy window", () => { 97 | return wrapPromise(({ expect }) => { 98 | const { childFrame, otherChildFrame } = getWindows(); 99 | 100 | const passListener = on( 101 | "passProxyWindow", 102 | expect("passProxyWindow", ({ data }) => { 103 | if (data.otherFrame.getWindow() !== otherChildFrame) { 104 | throw new Error(`Expected window to be correctly passed`); 105 | } 106 | 107 | passListener.cancel(); 108 | 109 | const callListener = on( 110 | "callProxyWindow", 111 | { window: data.otherFrame }, 112 | expect("onCallProxyWindow", () => { 113 | callListener.cancel(); 114 | }) 115 | ); 116 | 117 | return send(data.otherFrame, "sendMessageToParent", { 118 | messageName: "callProxyWindow", 119 | }).then(expect("sendSuccess")); 120 | }) 121 | ); 122 | 123 | return send(childFrame, "sendMessageToParent", { 124 | messageName: "passProxyWindow", 125 | data: { 126 | otherFrame: otherChildFrame, 127 | }, 128 | }).then(expect("sendSuccess")); 129 | }); 130 | }); 131 | }); 132 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | /* eslint import/no-nodejs-modules: off, import/no-default-export: off, import/default: off */ 3 | 4 | import type { WebpackConfig } from "@krakenjs/webpack-config-grumbler/index.flow"; 5 | import { 6 | getWebpackConfig, 7 | getNextVersion, 8 | } from "@krakenjs/webpack-config-grumbler"; 9 | import { argv } from "yargs"; 10 | 11 | import pkg from "./package.json"; 12 | import globals from "./globals"; 13 | 14 | export const FILE_NAME = "post-robot"; 15 | export const MODULE_NAME = "postRobot"; 16 | 17 | const postRobotGlobals = { 18 | ...globals.__POST_ROBOT__, 19 | __GLOBAL_KEY__: `__post_robot_${getNextVersion(pkg, argv.level)}__`, 20 | }; 21 | 22 | export const WEBPACK_CONFIG: WebpackConfig = getWebpackConfig({ 23 | filename: `${FILE_NAME}.js`, 24 | modulename: MODULE_NAME, 25 | minify: false, 26 | vars: { 27 | ...globals, 28 | 29 | __POST_ROBOT__: { 30 | ...postRobotGlobals, 31 | __IE_POPUP_SUPPORT__: false, 32 | __GLOBAL_MESSAGE_SUPPORT__: false, 33 | }, 34 | }, 35 | }); 36 | 37 | export const WEBPACK_CONFIG_MIN: WebpackConfig = getWebpackConfig({ 38 | filename: `${FILE_NAME}.min.js`, 39 | modulename: MODULE_NAME, 40 | minify: true, 41 | vars: { 42 | ...globals, 43 | 44 | __POST_ROBOT__: { 45 | ...postRobotGlobals, 46 | __IE_POPUP_SUPPORT__: false, 47 | __GLOBAL_MESSAGE_SUPPORT__: false, 48 | }, 49 | }, 50 | }); 51 | 52 | export const WEBPACK_CONFIG_IE: WebpackConfig = getWebpackConfig({ 53 | filename: `${FILE_NAME}.ie.js`, 54 | modulename: MODULE_NAME, 55 | minify: false, 56 | vars: { 57 | ...globals, 58 | __POST_ROBOT__: postRobotGlobals, 59 | }, 60 | }); 61 | 62 | export const WEBPACK_CONFIG_IE_MIN: WebpackConfig = getWebpackConfig({ 63 | filename: `${FILE_NAME}.ie.min.js`, 64 | modulename: MODULE_NAME, 65 | minify: true, 66 | vars: { 67 | ...globals, 68 | __POST_ROBOT__: postRobotGlobals, 69 | }, 70 | }); 71 | 72 | export const WEBPACK_CONFIG_TEST: WebpackConfig = getWebpackConfig({ 73 | modulename: MODULE_NAME, 74 | minify: false, 75 | test: true, 76 | vars: { 77 | ...globals, 78 | __POST_ROBOT__: postRobotGlobals, 79 | }, 80 | }); 81 | 82 | export default [ 83 | WEBPACK_CONFIG, 84 | WEBPACK_CONFIG_MIN, 85 | WEBPACK_CONFIG_IE, 86 | WEBPACK_CONFIG_IE_MIN, 87 | ]; 88 | --------------------------------------------------------------------------------