├── .npmignore ├── .gitignore ├── .travis.yml ├── .babelrc ├── src ├── index.js ├── server.js └── client.js ├── test ├── fixtures │ └── fruits.json ├── helpers │ └── fake-windows.js ├── server.callback.test.js ├── server.test.js └── client.test.js ├── LICENSE ├── package.json └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .nyc_output 3 | coverage 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | .nyc_output 4 | coverage 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 6 4 | - 8 5 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { "browsers": ["last 2 versions"] }] 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export { default as caller, call } from './client' 2 | export { default as expose } from './server' 3 | -------------------------------------------------------------------------------- /test/fixtures/fruits.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Apple", 4 | "colour": "green" 5 | }, 6 | { 7 | "name": "Pear", 8 | "colour": "green" 9 | }, 10 | { 11 | "name": "Orange", 12 | "colour": "orange" 13 | }, 14 | { 15 | "name": "Banana", 16 | "colour": "yellow" 17 | }, 18 | { 19 | "name": "Pineapple", 20 | "colour": "yellow" 21 | }, 22 | { 23 | "name": "Kiwi", 24 | "colour": "green" 25 | }, 26 | { 27 | "name": "Grape", 28 | "colour": "green" 29 | }, 30 | { 31 | "name": "Blueberry", 32 | "colour": "blue" 33 | } 34 | ] 35 | -------------------------------------------------------------------------------- /test/helpers/fake-windows.js: -------------------------------------------------------------------------------- 1 | export default function fakeWindows () { 2 | const win0 = { 3 | listeners: [], 4 | addEventListener: (_, listener) => win0.listeners.push(listener), 5 | removeEventListener (_, listener) { 6 | win0.listeners = win0.listeners.filter(l => l !== listener) 7 | }, 8 | postMessage (data) { 9 | process.nextTick(() => win1.listeners.forEach(l => l({ data }))) 10 | } 11 | } 12 | 13 | const win1 = { 14 | listeners: [], 15 | addEventListener: (_, listener) => win1.listeners.push(listener), 16 | removeEventListener (_, listener) { 17 | win1.listeners = win1.listeners.filter(l => l !== listener) 18 | }, 19 | postMessage (data) { 20 | process.nextTick(() => win0.listeners.forEach(l => l({ data }))) 21 | } 22 | } 23 | 24 | return [win0, win1] 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Alan Shaw 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "postmsg-rpc", 3 | "version": "2.4.0", 4 | "description": "Tiny RPC over window.postMessage library", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "build": "run-p build:*", 8 | "build:js": "babel src --out-dir lib --source-maps", 9 | "test": "run-s lint test:*", 10 | "test:ava": "nyc --reporter=lcov --reporter=text ava", 11 | "lint": "standard", 12 | "clean": "rm -rf lib/**", 13 | "prepublishOnly": "run-s build" 14 | }, 15 | "keywords": [ 16 | "postMessage", 17 | "window", 18 | "window.postMessage", 19 | "rpc", 20 | "api" 21 | ], 22 | "author": "Alan Shaw", 23 | "license": "MIT", 24 | "dependencies": { 25 | "shortid": "^2.2.8" 26 | }, 27 | "devDependencies": { 28 | "ava": "^0.25.0", 29 | "babel-cli": "^6.26.0", 30 | "babel-preset-env": "^1.6.1", 31 | "npm-run-all": "^4.1.2", 32 | "nyc": "^11.3.0", 33 | "standard": "^11.0.1" 34 | }, 35 | "ava": { 36 | "require": [ 37 | "babel-register", 38 | "babel-polyfill" 39 | ], 40 | "babel": "inherit" 41 | }, 42 | "repository": { 43 | "type": "git", 44 | "url": "git+https://github.com/tableflip/postmsg-rpc.git" 45 | }, 46 | "bugs": { 47 | "url": "https://github.com/tableflip/postmsg-rpc/issues" 48 | }, 49 | "homepage": "https://github.com/tableflip/postmsg-rpc#readme" 50 | } 51 | -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | export default function expose (funcName, func, opts) { 2 | opts = opts || {} 3 | 4 | const addListener = opts.addListener || window.addEventListener 5 | const removeListener = opts.removeListener || window.removeEventListener 6 | const postMessage = opts.postMessage || window.postMessage 7 | const targetOrigin = opts.targetOrigin || '*' 8 | const getMessageData = opts.getMessageData || ((event) => event.data) 9 | const isCallback = opts.isCallback || false 10 | 11 | const handler = function () { 12 | const data = getMessageData.apply(null, arguments) 13 | if (!data) return 14 | if (data.sender !== 'postmsg-rpc/client' || data.func !== funcName) return 15 | 16 | const msg = { sender: 'postmsg-rpc/server', id: data.id } 17 | 18 | const onSuccess = (res) => { 19 | msg.res = res 20 | postMessage(msg, targetOrigin) 21 | } 22 | 23 | const onError = (err) => { 24 | msg.err = Object.assign({ message: err.message }, err.output && err.output.payload) 25 | 26 | if (process.env.NODE_ENV !== 'production') { 27 | msg.err.stack = msg.err.stack || err.stack 28 | } 29 | 30 | postMessage(msg, targetOrigin) 31 | } 32 | 33 | if (isCallback) { 34 | func.apply(null, data.args.concat((err, res) => { 35 | if (err) return onError(err) 36 | onSuccess(res) 37 | })) 38 | } else { 39 | const res = func.apply(null, data.args) 40 | Promise.resolve(res).then(onSuccess).catch(onError) 41 | } 42 | } 43 | 44 | addListener('message', handler) 45 | 46 | return { close: () => removeListener('message', handler) } 47 | } 48 | -------------------------------------------------------------------------------- /src/client.js: -------------------------------------------------------------------------------- 1 | import shortid from 'shortid' 2 | 3 | export default function caller (funcName, opts) { 4 | opts = opts || {} 5 | 6 | const addListener = opts.addListener || window.addEventListener 7 | const removeListener = opts.removeListener || window.removeEventListener 8 | const postMessage = opts.postMessage || window.postMessage 9 | const targetOrigin = opts.targetOrigin || '*' 10 | const getMessageData = opts.getMessageData || ((event) => event.data) 11 | 12 | return function () { 13 | const msg = { 14 | sender: 'postmsg-rpc/client', 15 | id: shortid(), 16 | func: funcName, 17 | args: Array.from(arguments) 18 | } 19 | 20 | let cancel 21 | 22 | const response = new Promise((resolve, reject) => { 23 | const handler = function () { 24 | const data = getMessageData.apply(null, arguments) 25 | if (!data) return 26 | if (data.sender !== 'postmsg-rpc/server' || data.id !== msg.id) return 27 | removeListener('message', handler) 28 | 29 | if (data.err) { 30 | const err = new Error(`Unexpected error calling ${funcName}`) 31 | Object.assign(err, data.err) 32 | return reject(err) 33 | } 34 | 35 | resolve(data.res) 36 | } 37 | 38 | cancel = () => { 39 | removeListener('message', handler) 40 | const err = new Error(`Canceled call to ${funcName}`) 41 | err.isCanceled = true 42 | reject(err) 43 | } 44 | 45 | addListener('message', handler) 46 | postMessage(msg, targetOrigin) 47 | }) 48 | 49 | response.cancel = () => cancel() 50 | 51 | return response 52 | } 53 | } 54 | 55 | export function call (funcName) { 56 | return caller(funcName).apply(null, [].slice.call(arguments, 1)) 57 | } 58 | -------------------------------------------------------------------------------- /test/server.callback.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import { caller, expose } from '../src/index' 3 | import fakeWindows from './helpers/fake-windows' 4 | import Fruits from './fixtures/fruits.json' 5 | 6 | test('should pass error back to client', async (t) => { 7 | const [ server, client ] = fakeWindows() 8 | 9 | const serverErr = new Error('Boom') 10 | serverErr.output = { payload: { additional: 'data' } } 11 | 12 | const fruitService = { 13 | getFruits (cb) { process.nextTick(() => cb(serverErr)) } 14 | } 15 | 16 | expose('getFruits', fruitService.getFruits, { 17 | addListener: server.addEventListener, 18 | removeListener: server.removeEventListener, 19 | postMessage: server.postMessage, 20 | isCallback: true 21 | }) 22 | 23 | const getFruits = caller('getFruits', { 24 | addListener: client.addEventListener, 25 | removeListener: client.removeEventListener, 26 | postMessage: client.postMessage 27 | }) 28 | 29 | try { 30 | await getFruits() 31 | t.fail() // expected to throw 32 | } catch (err) { 33 | t.is(err.message, serverErr.message) 34 | 35 | Object.keys(serverErr.output.payload).forEach((key) => { 36 | t.is(err[key], serverErr.output.payload[key]) 37 | }) 38 | } 39 | }) 40 | 41 | test('should close', async (t) => { 42 | const [ server, client ] = fakeWindows() 43 | 44 | const fruitService = { 45 | getFruits: (cb) => process.nextTick(() => cb(null, Fruits)) 46 | } 47 | 48 | const serverHandle = expose('getFruits', fruitService.getFruits, { 49 | addListener: server.addEventListener, 50 | removeListener: server.removeEventListener, 51 | postMessage: server.postMessage, 52 | isCallback: true 53 | }) 54 | 55 | const getFruits = caller('getFruits', { 56 | addListener: client.addEventListener, 57 | removeListener: client.removeEventListener, 58 | postMessage: client.postMessage 59 | }) 60 | 61 | const fruits = await getFruits() 62 | 63 | t.deepEqual(fruits, Fruits) 64 | 65 | // Stop listening for requests for fruity treats 66 | serverHandle.close() 67 | 68 | return new Promise(async (resolve) => { 69 | // Try again now it's closed 70 | const fruitPromise = getFruits() 71 | 72 | setTimeout(() => { 73 | // Ok so it's probably not going to respond, this is pass! 74 | t.pass() 75 | resolve() 76 | }, 500) 77 | 78 | // Try to wait for the response 79 | await fruitPromise 80 | throw new Error('should not fulfil') 81 | }) 82 | }) 83 | 84 | test('should ignore bad/irrelevant messages', async (t) => { 85 | const [ server, client ] = fakeWindows() 86 | 87 | const fruitService = { 88 | getFruits: (cb) => setTimeout(() => cb(null, Fruits), 500) 89 | } 90 | 91 | expose('getFruits', fruitService.getFruits, { 92 | addListener: server.addEventListener, 93 | removeListener: server.removeEventListener, 94 | postMessage: server.postMessage, 95 | isCallback: true 96 | }) 97 | 98 | const getFruits = caller('getFruits', { 99 | addListener: client.addEventListener, 100 | removeListener: client.removeEventListener, 101 | postMessage: client.postMessage 102 | }) 103 | 104 | const fruitPromise = getFruits() 105 | 106 | // Inbetween, lets send irrelevant messages that should be ignored 107 | server.listeners.forEach(l => { 108 | l({}) 109 | l({ data: { sender: 'bogus' } }) 110 | l({ data: { id: 'wrong' } }) 111 | }) 112 | 113 | const fruits = await fruitPromise 114 | 115 | t.deepEqual(fruits, Fruits) 116 | }) 117 | -------------------------------------------------------------------------------- /test/server.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import { caller, expose } from '../src/index' 3 | import fakeWindows from './helpers/fake-windows' 4 | import Fruits from './fixtures/fruits.json' 5 | 6 | test('should pass error back to client', async (t) => { 7 | const [ server, client ] = fakeWindows() 8 | 9 | const serverErr = new Error('Boom') 10 | serverErr.output = { payload: { additional: 'data' } } 11 | 12 | const fruitService = { 13 | getFruits: () => Promise.reject(serverErr) 14 | } 15 | 16 | expose('getFruits', fruitService.getFruits, { 17 | addListener: server.addEventListener, 18 | removeListener: server.removeEventListener, 19 | postMessage: server.postMessage 20 | }) 21 | 22 | const getFruits = caller('getFruits', { 23 | addListener: client.addEventListener, 24 | removeListener: client.removeEventListener, 25 | postMessage: client.postMessage 26 | }) 27 | 28 | try { 29 | await getFruits() 30 | t.fail() // expected to throw 31 | } catch (err) { 32 | t.is(err.message, serverErr.message) 33 | 34 | Object.keys(serverErr.output.payload).forEach((key) => { 35 | t.is(err[key], serverErr.output.payload[key]) 36 | }) 37 | } 38 | }) 39 | 40 | test('should close', async (t) => { 41 | const [ server, client ] = fakeWindows() 42 | 43 | const fruitService = { 44 | getFruits: () => Promise.resolve(Fruits) 45 | } 46 | 47 | const serverHandle = expose('getFruits', fruitService.getFruits, { 48 | addListener: server.addEventListener, 49 | removeListener: server.removeEventListener, 50 | postMessage: server.postMessage 51 | }) 52 | 53 | const getFruits = caller('getFruits', { 54 | addListener: client.addEventListener, 55 | removeListener: client.removeEventListener, 56 | postMessage: client.postMessage 57 | }) 58 | 59 | const fruits = await getFruits() 60 | 61 | t.deepEqual(fruits, Fruits) 62 | 63 | // Stop listening for requests for fruity treats 64 | serverHandle.close() 65 | 66 | return new Promise(async (resolve) => { 67 | // Try again now it's closed 68 | const fruitPromise = getFruits() 69 | 70 | setTimeout(() => { 71 | // Ok so it's probably not going to respond, this is pass! 72 | t.pass() 73 | resolve() 74 | }, 500) 75 | 76 | // Try to wait for the response 77 | await fruitPromise 78 | throw new Error('should not fulfil') 79 | }) 80 | }) 81 | 82 | test('should ignore bad/irrelevant messages', async (t) => { 83 | const [ server, client ] = fakeWindows() 84 | 85 | const fruitService = { 86 | getFruits: () => new Promise((resolve) => setTimeout(() => resolve(Fruits), 500)) 87 | } 88 | 89 | expose('getFruits', fruitService.getFruits, { 90 | addListener: server.addEventListener, 91 | removeListener: server.removeEventListener, 92 | postMessage: server.postMessage 93 | }) 94 | 95 | const getFruits = caller('getFruits', { 96 | addListener: client.addEventListener, 97 | removeListener: client.removeEventListener, 98 | postMessage: client.postMessage 99 | }) 100 | 101 | const fruitPromise = getFruits() 102 | 103 | // Inbetween, lets send irrelevant messages that should be ignored 104 | server.listeners.forEach(l => { 105 | l({}) 106 | l({ data: { sender: 'bogus' } }) 107 | l({ data: { id: 'wrong' } }) 108 | }) 109 | 110 | const fruits = await fruitPromise 111 | 112 | t.deepEqual(fruits, Fruits) 113 | }) 114 | 115 | test('should allow non promise return value', async (t) => { 116 | const [ server, client ] = fakeWindows() 117 | 118 | const fruitService = { 119 | getFruits: () => Fruits // sync API on the server 120 | } 121 | 122 | expose('getFruits', fruitService.getFruits, { 123 | addListener: server.addEventListener, 124 | removeListener: server.removeEventListener, 125 | postMessage: server.postMessage 126 | }) 127 | 128 | const getFruits = caller('getFruits', { 129 | addListener: client.addEventListener, 130 | removeListener: client.removeEventListener, 131 | postMessage: client.postMessage 132 | }) 133 | 134 | t.deepEqual(await getFruits(), Fruits) 135 | }) 136 | -------------------------------------------------------------------------------- /test/client.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import { call, caller, expose } from '../src/index' 3 | import fakeWindows from './helpers/fake-windows' 4 | import Fruits from './fixtures/fruits.json' 5 | 6 | test('should fetch data from remote', async (t) => { 7 | const [ server, client ] = fakeWindows() 8 | 9 | const fruitService = { 10 | getFruits: () => Promise.resolve(Fruits) 11 | } 12 | 13 | expose('getFruits', fruitService.getFruits, { 14 | addListener: server.addEventListener, 15 | removeListener: server.removeEventListener, 16 | postMessage: server.postMessage 17 | }) 18 | 19 | const getFruits = caller('getFruits', { 20 | addListener: client.addEventListener, 21 | removeListener: client.removeEventListener, 22 | postMessage: client.postMessage 23 | }) 24 | 25 | const fruits = await getFruits() 26 | 27 | t.deepEqual(fruits, Fruits) 28 | }) 29 | 30 | test('should fetch data from remote with call', async (t) => { 31 | const [ server, client ] = fakeWindows() 32 | 33 | const fruitService = { 34 | getFruits: () => Promise.resolve(Fruits) 35 | } 36 | 37 | expose('getFruits', fruitService.getFruits, { 38 | addListener: server.addEventListener, 39 | removeListener: server.removeEventListener, 40 | postMessage: server.postMessage 41 | }) 42 | 43 | global.window = client 44 | 45 | const fruits = await call('getFruits') 46 | 47 | t.deepEqual(fruits, Fruits) 48 | 49 | delete global.window 50 | }) 51 | 52 | test('should be cancelable', async (t) => { 53 | const [ server, client ] = fakeWindows() 54 | 55 | const fruitService = { 56 | // Take 250ms to get fruits 57 | getFruits: () => new Promise((resolve, reject) => { 58 | setTimeout(() => resolve(Fruits), 250) 59 | }) 60 | } 61 | 62 | expose('getFruits', fruitService.getFruits, { 63 | addListener: server.addEventListener, 64 | removeListener: server.removeEventListener, 65 | postMessage: server.postMessage 66 | }) 67 | 68 | const getFruits = caller('getFruits', { 69 | addListener: client.addEventListener, 70 | removeListener: client.removeEventListener, 71 | postMessage: client.postMessage 72 | }) 73 | 74 | const fruitPromise = getFruits() 75 | 76 | fruitPromise.cancel() 77 | 78 | try { 79 | await fruitPromise 80 | t.fail() 81 | } catch (err) { 82 | t.true(err.isCanceled) 83 | } 84 | }) 85 | 86 | test('should ignore bad/irrelevant messages', async (t) => { 87 | const [ server, client ] = fakeWindows() 88 | 89 | const fruitService = { 90 | getFruits: () => new Promise((resolve) => setTimeout(() => resolve(Fruits), 500)) 91 | } 92 | 93 | expose('getFruits', fruitService.getFruits, { 94 | addListener: server.addEventListener, 95 | removeListener: server.removeEventListener, 96 | postMessage: server.postMessage 97 | }) 98 | 99 | const getFruits = caller('getFruits', { 100 | addListener: client.addEventListener, 101 | removeListener: client.removeEventListener, 102 | postMessage: client.postMessage 103 | }) 104 | 105 | const fruitPromise = getFruits() 106 | 107 | // Inbetween, lets send irrelevant messages that should be ignored 108 | client.listeners.forEach(l => { 109 | l({}) 110 | l({ data: { sender: 'bogus' } }) 111 | l({ data: { id: 'wrong' } }) 112 | }) 113 | 114 | const fruits = await fruitPromise 115 | 116 | t.deepEqual(fruits, Fruits) 117 | }) 118 | 119 | test('should pass arguments', async (t) => { 120 | const [ server, client ] = fakeWindows() 121 | 122 | const fruitService = { 123 | getFruits: (arg0, arg1) => arg0 && arg1 124 | ? Promise.resolve(Fruits) 125 | : Promise.reject(new Error('args not passed')) 126 | } 127 | 128 | expose('getFruits', fruitService.getFruits, { 129 | addListener: server.addEventListener, 130 | removeListener: server.removeEventListener, 131 | postMessage: server.postMessage 132 | }) 133 | 134 | const getFruits = caller('getFruits', { 135 | addListener: client.addEventListener, 136 | removeListener: client.removeEventListener, 137 | postMessage: client.postMessage 138 | }) 139 | 140 | const fruits = await getFruits(true, true) 141 | 142 | t.deepEqual(fruits, Fruits) 143 | }) 144 | 145 | test('should pass arguments with call', async (t) => { 146 | const [ server, client ] = fakeWindows() 147 | 148 | const fruitService = { 149 | getFruits: (arg0, arg1) => arg0 && arg1 150 | ? Promise.resolve(Fruits) 151 | : Promise.reject(new Error('args not passed')) 152 | } 153 | 154 | expose('getFruits', fruitService.getFruits, { 155 | addListener: server.addEventListener, 156 | removeListener: server.removeEventListener, 157 | postMessage: server.postMessage 158 | }) 159 | 160 | global.window = client 161 | 162 | const fruits = await call('getFruits', true, true) 163 | 164 | t.deepEqual(fruits, Fruits) 165 | 166 | delete global.window 167 | }) 168 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # postmsg-rpc 2 | 3 | [![Build Status](https://travis-ci.org/tableflip/postmsg-rpc.svg?branch=master)](https://travis-ci.org/tableflip/postmsg-rpc) [![dependencies Status](https://david-dm.org/tableflip/postmsg-rpc/status.svg)](https://david-dm.org/tableflip/postmsg-rpc) [![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com) 4 | 5 | > Tiny RPC over [window.postMessage](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage) library 6 | 7 | ## Install 8 | 9 | ```sh 10 | npm install postmsg-rpc 11 | ``` 12 | 13 | ## Usage 14 | 15 | In the window you want to call to (**the "server"**): 16 | 17 | ```js 18 | import { expose } from 'postmsg-rpc' 19 | 20 | const fruitService = { 21 | getFruits: (/* arg0, arg1, ... */) => new Promise(/* ... */) 22 | } 23 | 24 | // Expose this function for RPC from other windows 25 | expose('getFruits', fruitService.getFruits) 26 | ``` 27 | 28 | In the other window (**the "client"**): 29 | 30 | ```js 31 | import { call } from 'postmsg-rpc' 32 | 33 | // Call the exposed function 34 | const fruits = await call('getFruits'/*, arg0, arg1, ... */) 35 | ``` 36 | 37 | ### Advanced usage 38 | 39 | Use `caller` to create a function that uses postMessage to call an exposed function in a different window. It also allows you to pass options (see docs below). 40 | 41 | ```js 42 | import { caller } from 'postmsg-rpc' 43 | 44 | const getFruits = caller('getFruits'/*, options */) 45 | 46 | // Wait for the fruits to ripen 47 | const fruits = await getFruits(/*, arg0, arg1, ... */) 48 | ``` 49 | 50 | ## API 51 | 52 | #### `expose(funcName, func, options)` 53 | 54 | Expose `func` as `funcName` for RPC from other windows. Assumes that `func` returns a promise. 55 | 56 | * `funcName` - the name of the function called on the client 57 | * `func` - the function that should be called. Should be synchronous _or_ return a promise. For callbacks, pass `options.isCallback` 58 | * `options.targetOrigin` - passed to postMessage (see [postMessage docs](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage) for more info) 59 | * default `'*'` 60 | * `options.isCallback` - set to true if `func` takes a node style callback instead of returning a promise 61 | * default `false` 62 | * `options.postMessage` - function that posts a message. It is passed two parameters, `data` and `options.targetOrigin`. e.g. `document.querySelector('iframe').contentWindow.postMessage` for exposing functions to an iframe or `window.parent.postMessage` for exposing functions from an iframe to a parent window 63 | * default `window.postMessage` 64 | 65 | The following options are for use with other similar messaging systems, for example when using [message passing in browser extensions](https://developer.chrome.com/extensions/messaging) or for testing: 66 | 67 | * `options.addListener` - function that adds a listener. It is passed two parameters, the event name (always "message") and a `handler` function 68 | * default `window.addEventListener` 69 | * `options.removeListener` - function that removes a listener. It is passed two parameters, the event name (always "message") and a `handler` function 70 | * default `window.removeEventListener` 71 | * `options.getMessageData` - a function that extracts data from the arguments passed to a `message` event handler 72 | * default `(e) => e.data` 73 | 74 | Returns an object with a `close` method to stop the server from listening to messages. 75 | 76 | #### `call(funcName, , , ...)` 77 | 78 | Call an exposed function in a different window. 79 | 80 | * `funcName` - the name of the function to call 81 | 82 | Returns a `Promise`, so can be `await`ed or used in the usual way (`then`/`catch`). The `Promise` returned has an additional property `cancel` which can be called to cancel an in flight request e.g. 83 | 84 | ```js 85 | const fruitPromise = call('getFruits') 86 | 87 | fruitPromise.cancel() 88 | 89 | try { 90 | await fruitPromise 91 | } catch (err) { 92 | if (err.isCanceled) { 93 | console.log('function call canceled') 94 | } 95 | } 96 | ``` 97 | 98 | #### `caller(funcName, options)` 99 | 100 | Create a function that uses postMessage to call an exposed function in a different window. 101 | 102 | * `funcName` - the name of the function to call 103 | * `options.targetOrigin` - passed to postMessage (see [postMessage docs](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage) for more info) 104 | * default `'*'` 105 | * `options.postMessage` - function that posts a message. It is passed two parameters, `data` and `options.targetOrigin`. e.g. `document.querySelector('iframe').contentWindow.postMessage` for calling functions in an iframe or `window.parent.postMessage` for calling functions in a parent window from an iframe 106 | * default `window.postMessage` 107 | 108 | The following options are for use with other similar messaging systems, for example when using [message passing in browser extensions](https://developer.chrome.com/extensions/messaging) or for testing: 109 | 110 | * `options.addListener` - function that adds a listener. It is passed two parameters, the event name (always "message") and a `handler` function 111 | * default `window.addEventListener` 112 | * `options.removeListener` - function that removes a listener. It is passed two parameters, the event name (always "message") and a `handler` function 113 | * default `window.removeEventListener` 114 | * `options.getMessageData` - a function that extracts data from the arguments passed to a `message` event handler 115 | * default `(e) => e.data` 116 | 117 | ## Contribute 118 | 119 | Feel free to dive in! [Open an issue](https://github.com/tableflip/postmsg-rpc/issues/new) or submit PRs. 120 | 121 | ## License 122 | 123 | [MIT](LICENSE) © Alan Shaw 124 | 125 | --- 126 | A [(╯°□°)╯︵TABLEFLIP](https://tableflip.io) side project. 127 | --------------------------------------------------------------------------------