├── .eslintignore ├── .eslintrc.json ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md ├── README.md └── workflows │ ├── main.yml │ └── prevent-commit-to-generated.yml ├── .gitignore ├── .npmignore ├── .npmrc ├── .nvmrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── babel.config.js ├── config ├── karma.conf.js ├── rollup.config.mjs └── webpack.config.js ├── dist ├── es5node │ ├── broadcast-channel.js │ ├── browserify.index.js │ ├── index.es5.js │ ├── index.js │ ├── leader-election-util.js │ ├── leader-election-web-lock.js │ ├── leader-election.js │ ├── method-chooser.js │ ├── methods │ │ ├── cookies.js │ │ ├── indexed-db.js │ │ ├── localstorage.js │ │ ├── native.js │ │ ├── node.js │ │ └── simulate.js │ ├── options.js │ └── util.js ├── esbrowser │ ├── broadcast-channel.js │ ├── browserify.index.js │ ├── index.es5.js │ ├── index.js │ ├── leader-election-util.js │ ├── leader-election-web-lock.js │ ├── leader-election.js │ ├── method-chooser.js │ ├── methods │ │ ├── cookies.js │ │ ├── indexed-db.js │ │ ├── localstorage.js │ │ ├── native.js │ │ ├── node.js │ │ └── simulate.js │ ├── options.js │ ├── package.json │ └── util.js ├── esnode │ ├── broadcast-channel.js │ ├── browserify.index.js │ ├── index.es5.js │ ├── index.js │ ├── leader-election-util.js │ ├── leader-election-web-lock.js │ ├── leader-election.js │ ├── method-chooser.js │ ├── methods │ │ ├── cookies.js │ │ ├── indexed-db.js │ │ ├── localstorage.js │ │ ├── native.js │ │ ├── node.js │ │ └── simulate.js │ ├── options.js │ ├── package.json │ └── util.js └── lib │ ├── broadcast-channel.js │ ├── browser.js │ ├── browser.min.js │ ├── browserify.index.js │ ├── index.es5.js │ ├── index.js │ ├── leader-election-util.js │ ├── leader-election-web-lock.js │ ├── leader-election.js │ ├── method-chooser.js │ ├── methods │ ├── cookies.js │ ├── indexed-db.js │ ├── localstorage.js │ ├── native.js │ ├── node.js │ └── simulate.js │ ├── options.js │ └── util.js ├── docs ├── e2e.html ├── e2e.js ├── files │ ├── demo.gif │ ├── icon.png │ └── leader-election.gif ├── iframe.html ├── iframe.js ├── index.html ├── index.js ├── kirby.gif ├── leader-iframe.html ├── leader-iframe.js └── worker.js ├── package.json ├── perf.txt ├── renovate.json ├── src ├── broadcast-channel.js ├── browserify.index.js ├── index.es5.js ├── index.js ├── leader-election-util.js ├── leader-election-web-lock.js ├── leader-election.js ├── method-chooser.js ├── methods │ ├── cookies.js │ ├── indexed-db.js │ ├── localstorage.js │ ├── native.js │ ├── node.js │ └── simulate.js ├── options.js └── util.js ├── test-electron ├── .gitignore ├── .npmrc ├── index.html ├── main.js ├── package.json ├── page.js └── test │ ├── render.test.js │ └── spec.js ├── test ├── .eslintrc ├── close.test.js ├── e2e.test.js ├── index.test.js ├── integration.test.js ├── issues.test.js ├── module.cjs.test.js ├── module.esm.test.mjs ├── performance.test.js ├── scripts │ ├── e2e.js │ ├── iframe.js │ ├── index.js │ ├── leader-iframe.js │ ├── util.js │ ├── windows.sh │ └── worker.js ├── simple.test.js ├── test-deno.js ├── typings.test.js ├── unit.test.js └── unit │ ├── custom.method.test.js │ ├── indexed-db.method.test.js │ ├── localstorage.method.test.js │ ├── native.method.test.js │ └── node.method.test.js └── types ├── broadcast-channel.d.ts ├── index.d.ts └── leader-election.d.ts /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | tmp/ 4 | test/scripts/worker.js 5 | test/e2e.test.js -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: pubkey 4 | custom: ["https://rxdb.info/consulting"] 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | 14 | 15 | ## Case 16 | 17 | 18 | ## Issue 19 | 20 | 21 | ## Info 22 | - Environment: (Node.js/browser/electron/etc..) 23 | - Method: (IndexedDB/Localstorage/Node/etc..) 24 | - Stack: (Typescript, Babel, Angular, React, etc..) 25 | 26 | ## Code 27 | 28 | ```js 29 | import { BroadcastChannel } from 'broadcast-channel'; 30 | const channel = new BroadcastChannel('foobar'); 31 | channel.postMessage('I am not alone'); 32 | /* ... */ 33 | ``` 34 | 35 | 42 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | @pubkey 9 | 10 | 11 | 15 | 16 | 20 | 21 | ## This PR contains: 22 | 31 | 32 | ## Describe the problem you have without this PR 33 | 34 | 35 | ## Todos 36 | - [ ] Tests 37 | - [ ] Documentation 38 | - [ ] Typings 39 | - [ ] Changelog 40 | 41 | 47 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: CI 4 | 5 | # Controls when the action will run. 6 | on: 7 | # Triggers the workflow on push or pull request events but only for the master branch 8 | push: 9 | branches: [ master ] 10 | pull_request: 11 | branches: [ master ] 12 | 13 | # Allows you to run this workflow manually from the Actions tab 14 | workflow_dispatch: 15 | 16 | # https://stackoverflow.com/a/72408109/3443137 17 | concurrency: 18 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 19 | cancel-in-progress: true 20 | 21 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 22 | jobs: 23 | # This workflow contains a single job called "build" 24 | base: 25 | # The type of runner that the job will run on 26 | runs-on: ubuntu-22.04 27 | 28 | # Steps represent a sequence of tasks that will be executed as part of the job 29 | steps: 30 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 31 | - uses: actions/checkout@v4 32 | - name: Setup Node.js environment 33 | uses: actions/setup-node@v4.3.0 34 | with: 35 | node-version-file: ".nvmrc" 36 | 37 | # https://docs.github.com/en/free-pro-team@latest/actions/guides/caching-dependencies-to-speed-up-workflows 38 | - name: Reuse npm cache folder 39 | uses: actions/cache@v3 40 | env: 41 | cache-name: cache-node-modules 42 | with: 43 | # reuse the npm-cache and some node_modules folders 44 | path: | 45 | ~/.npm 46 | ./node_modules 47 | ./test-electron/node_modules 48 | # invalidate cache when any package.json changes 49 | key: ${{ runner.os }}-npm-x3-${{ env.cache-name }}-${{ hashFiles('**/package.json') }} 50 | restore-keys: | 51 | ${{ runner.os }}-npm-x3-${{ env.cache-name }}- 52 | ${{ runner.os }}-npm-x3- 53 | ${{ runner.os }}- 54 | 55 | # install 56 | - name: install node modules 57 | run: npm install --legacy-peer-deps 58 | 59 | - name: build 60 | run: npm run build 61 | 62 | - name: check build size webpack 63 | run: npm run size:webpack 64 | 65 | - name: check build size browserify 66 | run: npm run size:browserify 67 | 68 | - name: check build size rollup 69 | run: npm run size:rollup 70 | 71 | - name: code format 72 | run: npm run lint 73 | 74 | - name: test typings 75 | run: npm run test:typings 76 | 77 | - name: test modules 78 | run: npm run test:modules 79 | 80 | - name: test browser 81 | uses: GabrielBB/xvfb-action@v1 82 | with: 83 | working-directory: ./ #optional 84 | run: npm run test:browser 85 | 86 | - name: test performance 87 | run: npm run test:performance 88 | 89 | - name: test e2e 90 | uses: GabrielBB/xvfb-action@v1 91 | with: 92 | working-directory: ./ #optional 93 | run: npm run test:e2e 94 | 95 | 96 | # run the node test in an own task, so we can use a node-version matrix. 97 | test-node: 98 | runs-on: ubuntu-22.04 99 | strategy: 100 | matrix: 101 | node: ['18.18.2', '20.9.0'] 102 | steps: 103 | - uses: actions/checkout@v4 104 | - name: Setup Node.js environment 105 | uses: actions/setup-node@v4.3.0 106 | with: 107 | node-version: ${{ matrix.node }} 108 | 109 | # https://docs.github.com/en/free-pro-team@latest/actions/guides/caching-dependencies-to-speed-up-workflows 110 | - name: Reuse npm cache folder 111 | uses: actions/cache@v3 112 | env: 113 | cache-name: cache-node-modules 114 | with: 115 | path: | 116 | ~/.npm 117 | ./node_modules 118 | ./test-electron/node_modules 119 | # invalidate cache when any package.json changes 120 | key: ${{ runner.os }}-npm-test-node-x3-${{ matrix.node }}-${{ env.cache-name }}-${{ hashFiles('**/package.json') }} 121 | restore-keys: | 122 | ${{ runner.os }}-npm-test-node-x3-${{ matrix.node }}-${{ env.cache-name }}- 123 | ${{ runner.os }}-npm-test-node 124 | ${{ runner.os }}-test-node 125 | 126 | - name: install node modules 127 | run: npm install --legacy-peer-deps 128 | 129 | - name: build 130 | run: npm run build 131 | 132 | - name: test node 133 | run: npm run test:node 134 | 135 | 136 | test-deno: 137 | runs-on: ubuntu-22.04 138 | steps: 139 | - uses: actions/checkout@v4 140 | - name: Setup Node.js environment 141 | uses: actions/setup-node@v4.3.0 142 | with: 143 | node-version-file: ".nvmrc" 144 | 145 | # https://docs.github.com/en/free-pro-team@latest/actions/guides/caching-dependencies-to-speed-up-workflows 146 | - name: Reuse npm cache folder 147 | uses: actions/cache@v3 148 | env: 149 | cache-name: cache-node-deno-modules 150 | with: 151 | path: | 152 | ~/.npm 153 | ./node_modules 154 | ./test-electron/node_modules 155 | # invalidate cache when any package.json changes 156 | key: ${{ runner.os }}-npm-test-deno-x3-${{ env.cache-name }}-${{ hashFiles('**/package.json') }} 157 | restore-keys: | 158 | ${{ runner.os }}-npm-test-deno-x3-${{ env.cache-name }}- 159 | ${{ runner.os }}-npm-test-deno 160 | ${{ runner.os }}-test-deno 161 | 162 | - name: install node modules 163 | run: npm install --legacy-peer-deps 164 | 165 | - name: build 166 | run: npm run build 167 | 168 | - name: Reuse deno cache folder 169 | uses: actions/cache@v3 170 | env: 171 | cache-name: cache-deno-modules 172 | with: 173 | path: | 174 | /home/runner/.cache/deno 175 | # do not cache based on package.json because deno install randomly fails 176 | # and it would then never succeed on the first run on dependency updateds 177 | key: ${{ runner.os }}-deno-x3- 178 | 179 | - uses: denoland/setup-deno@v1 180 | with: 181 | # https://github.com/denoland/deno/releases 182 | deno-version: "1.37.2" 183 | - name: run deno tests 184 | run: | 185 | sudo npm i -g cross-env 186 | deno info 187 | npm run test:deno 188 | 189 | 190 | # TODO this does not work atm. fix this. 191 | # - name: test electron 192 | # uses: GabrielBB/xvfb-action@v1 193 | # with: 194 | # working-directory: ./test-electron 195 | # run: npm install --depth 0 --silent && npm run test 196 | -------------------------------------------------------------------------------- /.github/workflows/prevent-commit-to-generated.yml: -------------------------------------------------------------------------------- 1 | name: Prevent non-master commits to generated files 2 | 3 | on: 4 | # https://stackoverflow.com/a/70569968/3443137 5 | pull_request: 6 | paths: 7 | - 'dist/**' 8 | 9 | jobs: 10 | warn-user: 11 | runs-on: ubuntu-22.04 12 | steps: 13 | - uses: actions/checkout@v4 14 | - name: Warn User to not commit generated files 15 | run: bash -c 'echo "You have commited generated files (from the dist folder), this is not allowed. You can only commit files that you have manually edited" && exit 1' 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | package-lock.json 3 | .idea/ 4 | .vscode/ 5 | .transpile_state.json 6 | .com.google.* 7 | shelljs_* 8 | test_tmp/ 9 | tmp/ 10 | .eslintcache 11 | dist/**/*.bak -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .git/ 2 | config/ 3 | docs/ 4 | test/ 5 | test_tmp/ 6 | test-electron/ 7 | tmp/ 8 | babel.config.js 9 | .editorconfig 10 | .eslintignore 11 | .eslintrc.json 12 | .eslintcache 13 | .gitignore 14 | .travis.yml 15 | renovate.json 16 | ISSUE_TEMPLATE.md 17 | PULL_REQUEST_TEMPLATE.md 18 | 19 | log.txt 20 | perf.txt 21 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | unsafe-perm = true 2 | package-lock=false 3 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 20.18.3 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | 4 | ## X.X.X (comming soon) 5 | 6 | ## 7.0.0 (27 November 2023) 7 | 8 | - CHANGE do not emit messages that have been existed before the channel was created. 9 | 10 | ## 6.0.0 (30 October 2023) 11 | 12 | - ADD support for the Deno runtime 13 | 14 | ## 5.5.1 (23 October 2023) 15 | 16 | - REPLACE `new Date().getTime()` with `Date.now()` which is faster 17 | 18 | ## 5.5.0 (17 October 2023) 19 | 20 | - Add `sideEffects: false` 21 | ## 5.4.0 (10 October 2023) 22 | 23 | - FIX import of `p-queue` throws `is not a constructor` 24 | 25 | ## 5.3.0 (18 August 2023) 26 | 27 | https://github.com/pubkey/broadcast-channel/pull/1243 28 | 29 | ## 5.2.0 (11 August 2023) 30 | https://github.com/pubkey/broadcast-channel/pull/1237 31 | 32 | ## 5.1.0 (25 April 2023) 33 | 34 | - REFACTOR check for native method [#1157](https://github.com/pubkey/broadcast-channel/pull/1157) 35 | 36 | ## 5.0.1 (23 March 2023) 37 | 38 | - FIX hasLeader() is mixing up states 39 | 40 | ## 5.0.0 (23 March 2023) 41 | 42 | - Use [Web Locks API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Locks_API) for leader election if possible. 43 | - `LeaderElector.hasLeader` is now a function that returns a `Promise`. 44 | 45 | ## 4.20.1 (6 January 2023) 46 | 47 | - FIX exports order 48 | 49 | ## 4.20.0 (6 January 2023) 50 | 51 | - FIX typings did not work with `"moduleResolution": "NodeNext"` 52 | 53 | ## 4.19.0 (28 December 2022) 54 | 55 | - Updated dependencies 56 | 57 | ## 4.18.1 (31 October 2022) 58 | 59 | - Updated dependencies 60 | 61 | ## 4.18.0 (6 October 2022) 62 | 63 | 64 | - FIX fix(indexedDB): Can't start a transaction on a closed database [#1042](https://github.com/pubkey/broadcast-channel/pull/1042) [nabigraphics](https://github.com/nabigraphics) 65 | 66 | 67 | ## 4.17.0 (13 September 2022) 68 | 69 | - REMOVE the `isNode` utility function so that we do not access the `process` variable in browsers. 70 | 71 | ## 4.16.0 (13 September 2022) 72 | 73 | - Rerelase because npm got stuck 74 | ## 4.15.0 (13 September 2022) 75 | 76 | - Remove `microtime` dependency [https://github.com/pubkey/broadcast-channel/pull/1036](#1036) [jaredperreault-okta](https://github.com/jaredperreault-okta) 77 | 78 | ## 4.14.0 (18 Juli June 2022) 79 | 80 | - Updated dependencies. 81 | 82 | ## 4.13.0 (1 June 2022) 83 | 84 | - FIX ES module for Node.js [#972](https://github.com/pubkey/broadcast-channel/pull/972) 85 | 86 | ## 4.12.0 (25 May 2022) 87 | 88 | - FIX ES module for Node.js Thanks [denysoblohin-okta](https://github.com/denysoblohin-okta) 89 | 90 | ## 4.11.0 (12 April 2022) 91 | 92 | - Replaced `nano-time` with `microtime`. 93 | - Improve IndexedDB method performance. 94 | 95 | ## 4.10.0 (3 February 2022) 96 | 97 | - Improve error message when calling `postMessage` to a closed channel. 98 | 99 | ## 4.9.0 (23 December 2021) 100 | 101 | Bugfixes: 102 | - When listening to messages directly, responses that where send directly after `addEventListener()` where missing because of inaccurate JavaScript timing. 103 | 104 | ## 4.8.0 (15 December 2021) 105 | 106 | Changes: 107 | - Better determine the correct `responseTime` to use to make it less likely to elect duplicate leaders. 108 | 109 | ## 4.7.1 (13 December 2021) 110 | 111 | Bugfixes: 112 | - Remove useless log at leader election fallback interval. 113 | 114 | ## 4.7.0 (3 December 2021) 115 | 116 | Bugfixes: 117 | - Prevent `EMFILE, too many open files` error when writing many messages at once. 118 | 119 | ## 4.6.0 (2 December 2021) 120 | 121 | Other: 122 | - Added `broadcastChannel.id()` for debugging 123 | 124 | Bugfixes: 125 | - Refactor `applyOnce()` queue to ensure we do not run more often then needed. 126 | 127 | ## 4.5.0 (5 November 2021) 128 | 129 | Bugfixes: 130 | - Running `applyOnce()` in a loop must not fully block the JavaScript process. 131 | 132 | ## 4.4.0 (2 November 2021) 133 | 134 | Other: 135 | - Replaced `js-sha` with node's `crypto` module. 136 | 137 | ## 4.3.1 (30 October 2021) 138 | 139 | Bugfixes: 140 | - Fixed broken promise rejection. 141 | 142 | ## 4.3.0 (30 October 2021) 143 | 144 | Features: 145 | - Added `LeaderElector.hasLeader` 146 | - Added `LeaderElector.broadcastChannel` 147 | 148 | ## 4.2.0 (3 August 2021) 149 | 150 | Bugfixes: 151 | - Fixed Webpack 5 Relative Import Support. Thanks [catrielmuller](https://github.com/catrielmuller) 152 | ## 4.1.0 (2 August 2021) 153 | 154 | Bugfixes: 155 | - Fixed various problems with the module loading. Thanks [benmccann](https://github.com/benmccann) and [chbdetta](https://github.com/chbdetta) 156 | 157 | 158 | ## 4.0.0 (15 July 2021) 159 | 160 | Other: 161 | - Changed entrypoints and method-choosing [#679](https://github.com/pubkey/broadcast-channel/pull/679). Thanks [benmccann](https://github.com/benmccann) 162 | 163 | ## 3.7.0 (13 June 2021) 164 | 165 | Other: 166 | - Moved `ObliviousSet` into [its own npm module](https://www.npmjs.com/package/oblivious-set) 167 | 168 | ## 3.6.0 (19 May 2021) 169 | 170 | Features: 171 | - Added `BroadcastChannel.isClosed` [#544](https://github.com/pubkey/broadcast-channel/issues/544) 172 | 173 | Other: 174 | - Updated dependencies to work with newer node versions 175 | 176 | ## 3.5.3 (11 March 2021) 177 | 178 | Bugfixes: 179 | - Fixed broken typings 180 | 181 | ## 3.5.2 (11 March 2021) 182 | 183 | Bugfixes: 184 | - `BroadcastChannel.close()` waits for all ongoing message sending to be finished before resolving. 185 | 186 | ## 3.5.0 (11 March 2021) 187 | 188 | Features: 189 | - Added `LeaderElector.onduplicate` 190 | 191 | ## 3.4.0 (24 January 2021) 192 | 193 | Bugfixes: 194 | - fix cursor error in Safari [#420](https://github.com/pubkey/broadcast-channel/pull/420) 195 | 196 | ## 3.3.0 (20 October 2020) 197 | 198 | Bugfixes: 199 | - `new BroadcastChannel().close()` should not resolve before all cleanup is done [#348](https://github.com/pubkey/broadcast-channel/pull/348) 200 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Daniel Meyer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 9 | 10 |

11 | 12 | 13 | 14 |

15 | 16 |

BroadcastChannel

17 |

18 | A BroadcastChannel that works in old browsers, new browsers, WebWorkers and NodeJs and Deno 19 |
20 | + LeaderElection over the channels 21 |

22 | 23 |

24 | 25 | follow on Twitter 27 | 28 |

29 | 30 | ![demo.gif](docs/files/demo.gif) 31 | 32 | * * * 33 | 34 | A BroadcastChannel that allows you to send data between different browser-tabs or nodejs-processes. 35 | And a LeaderElection over the channels. 36 | 37 | # [Read the full documentation on github](https://github.com/pubkey/broadcast-channel) 38 | 39 | 40 | # Sponsored by 41 | 42 |

43 | 44 | JavaScript Database 49 |
50 |
51 | The JavaScript Database 52 |
53 |

54 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | let presets = [ 2 | [ 3 | '@babel/preset-env', 4 | { 5 | targets: { 6 | node: 'current' 7 | }, 8 | modules: false 9 | } 10 | ] 11 | ]; 12 | 13 | const plugins = [ 14 | ['@babel/transform-template-literals', { 15 | 'loose': true 16 | }], 17 | '@babel/transform-literals', 18 | '@babel/transform-function-name', 19 | '@babel/transform-arrow-functions', 20 | '@babel/transform-block-scoped-functions', 21 | ['@babel/transform-classes', { 22 | 'loose': true 23 | }], 24 | '@babel/transform-object-super', 25 | '@babel/transform-shorthand-properties', 26 | ['@babel/transform-computed-properties', { 27 | 'loose': true 28 | }], 29 | ['@babel/transform-for-of', { 30 | 'loose': true 31 | }], 32 | '@babel/transform-sticky-regex', 33 | '@babel/transform-unicode-regex', 34 | '@babel/transform-parameters', 35 | ['@babel/transform-destructuring', { 36 | 'loose': true 37 | }], 38 | '@babel/transform-block-scoping', 39 | '@babel/plugin-proposal-object-rest-spread', 40 | '@babel/plugin-transform-member-expression-literals', 41 | '@babel/transform-property-literals', 42 | '@babel/transform-async-to-generator', 43 | '@babel/transform-regenerator', 44 | ['@babel/transform-runtime', { 45 | 'regenerator': true 46 | }] 47 | ]; 48 | 49 | if (process.env['NODE_ENV'] === 'es5') { 50 | presets = [ 51 | ['@babel/env', { 52 | targets: { 53 | edge: '17', 54 | firefox: '60', 55 | chrome: '67', 56 | safari: '11.1', 57 | ie: '11' 58 | }, 59 | useBuiltIns: false 60 | }] 61 | ]; 62 | } 63 | 64 | module.exports = { 65 | presets, 66 | plugins 67 | }; 68 | -------------------------------------------------------------------------------- /config/karma.conf.js: -------------------------------------------------------------------------------- 1 | const configuration = { 2 | basePath: '', 3 | frameworks: [ 4 | 'mocha', 5 | 'browserify', 6 | 'detectBrowsers' 7 | ], 8 | files: [ 9 | '../test/index.test.js' 10 | ], 11 | // reporters: ['progress'], 12 | port: 9876, 13 | colors: true, 14 | autoWatch: false, 15 | 16 | /** 17 | * see 18 | * @link https://github.com/litixsoft/karma-detect-browsers 19 | */ 20 | detectBrowsers: { 21 | enabled: true, 22 | usePhantomJS: false, 23 | postDetection: function (availableBrowser) { 24 | // return ['Chrome']; // comment in to test specific browser 25 | // return ['Firefox']; // comment in to test specific browser 26 | console.log('availableBrowser:'); 27 | console.dir(availableBrowser); 28 | const browsers = availableBrowser 29 | .filter(b => !['PhantomJS', 'FirefoxAurora', 'FirefoxNightly'].includes(b)); 30 | return browsers; 31 | } 32 | }, 33 | 34 | // Karma plugins loaded 35 | plugins: [ 36 | 'karma-mocha', 37 | 'karma-browserify', 38 | 'karma-chrome-launcher', 39 | 'karma-edge-launcher', 40 | 'karma-firefox-launcher', 41 | 'karma-ie-launcher', 42 | 'karma-opera-launcher', 43 | 'karma-safari-launcher', 44 | 'karma-detect-browsers', 45 | 'karma-env-preprocessor' 46 | ], 47 | 48 | // Source files that you wanna generate coverage for. 49 | // Do not include tests or libraries (these files will be instrumented by Istanbul) 50 | preprocessors: { 51 | '../test/*.test.js': ['browserify', 'env'] 52 | }, 53 | 54 | envPreprocessor: [ 55 | 'GITHUB_ACTIONS', 56 | ], 57 | client: { 58 | mocha: { 59 | bail: true, 60 | timeout: 12000 61 | }, 62 | captureConsole: true 63 | }, 64 | // browsers: ['ChromeNoSandbox'], 65 | browserDisconnectTimeout: 24000, 66 | processKillTimeout: 24000, 67 | customLaunchers: { 68 | Chrome_travis_ci: { 69 | base: 'ChromeHeadless', 70 | flags: ['--no-sandbox'] 71 | } 72 | }, 73 | singleRun: true, 74 | concurrency: 1 75 | }; 76 | 77 | module.exports = function (config) { 78 | config.set(configuration); 79 | }; 80 | -------------------------------------------------------------------------------- /config/rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import terser from '@rollup/plugin-terser'; 2 | 3 | export default { 4 | input: './dist/esbrowser/index.js', 5 | output: { 6 | sourcemap: true, 7 | format: 'iife', 8 | name: 'app', 9 | file: './test_tmp/rollup.bundle.js' 10 | }, 11 | plugins: [ 12 | terser() 13 | ] 14 | }; 15 | -------------------------------------------------------------------------------- /config/webpack.config.js: -------------------------------------------------------------------------------- 1 | 2 | const path = require('path'); 3 | const TerserPlugin = require('terser-webpack-plugin'); 4 | 5 | module.exports = { 6 | mode: 'production', 7 | entry: './dist/lib/browserify.index.js', 8 | optimization: { 9 | minimize: true, 10 | minimizer: [new TerserPlugin()] 11 | }, 12 | plugins: [], 13 | output: { 14 | path: path.resolve(__dirname, '../test_tmp'), 15 | filename: 'webpack.bundle.js' 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /dist/es5node/browserify.index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _module = require('./index.es5.js'); 4 | var BroadcastChannel = _module.BroadcastChannel; 5 | var createLeaderElection = _module.createLeaderElection; 6 | window['BroadcastChannel2'] = BroadcastChannel; 7 | window['createLeaderElection'] = createLeaderElection; -------------------------------------------------------------------------------- /dist/es5node/index.es5.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _index = require("./index.js"); 4 | /** 5 | * because babel can only export on default-attribute, 6 | * we use this for the non-module-build 7 | * this ensures that users do not have to use 8 | * var BroadcastChannel = require('broadcast-channel').default; 9 | * but 10 | * var BroadcastChannel = require('broadcast-channel'); 11 | */ 12 | 13 | module.exports = { 14 | BroadcastChannel: _index.BroadcastChannel, 15 | createLeaderElection: _index.createLeaderElection, 16 | clearNodeFolder: _index.clearNodeFolder, 17 | enforceOptions: _index.enforceOptions, 18 | beLeader: _index.beLeader 19 | }; -------------------------------------------------------------------------------- /dist/es5node/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | Object.defineProperty(exports, "BroadcastChannel", { 7 | enumerable: true, 8 | get: function get() { 9 | return _broadcastChannel.BroadcastChannel; 10 | } 11 | }); 12 | Object.defineProperty(exports, "OPEN_BROADCAST_CHANNELS", { 13 | enumerable: true, 14 | get: function get() { 15 | return _broadcastChannel.OPEN_BROADCAST_CHANNELS; 16 | } 17 | }); 18 | Object.defineProperty(exports, "beLeader", { 19 | enumerable: true, 20 | get: function get() { 21 | return _leaderElectionUtil.beLeader; 22 | } 23 | }); 24 | Object.defineProperty(exports, "clearNodeFolder", { 25 | enumerable: true, 26 | get: function get() { 27 | return _broadcastChannel.clearNodeFolder; 28 | } 29 | }); 30 | Object.defineProperty(exports, "createLeaderElection", { 31 | enumerable: true, 32 | get: function get() { 33 | return _leaderElection.createLeaderElection; 34 | } 35 | }); 36 | Object.defineProperty(exports, "enforceOptions", { 37 | enumerable: true, 38 | get: function get() { 39 | return _broadcastChannel.enforceOptions; 40 | } 41 | }); 42 | var _broadcastChannel = require("./broadcast-channel.js"); 43 | var _leaderElection = require("./leader-election.js"); 44 | var _leaderElectionUtil = require("./leader-election-util.js"); -------------------------------------------------------------------------------- /dist/es5node/leader-election-util.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.beLeader = beLeader; 7 | exports.sendLeaderMessage = sendLeaderMessage; 8 | var _unload = require("unload"); 9 | /** 10 | * sends and internal message over the broadcast-channel 11 | */ 12 | function sendLeaderMessage(leaderElector, action) { 13 | var msgJson = { 14 | context: 'leader', 15 | action: action, 16 | token: leaderElector.token 17 | }; 18 | return leaderElector.broadcastChannel.postInternal(msgJson); 19 | } 20 | function beLeader(leaderElector) { 21 | leaderElector.isLeader = true; 22 | leaderElector._hasLeader = true; 23 | var unloadFn = (0, _unload.add)(function () { 24 | return leaderElector.die(); 25 | }); 26 | leaderElector._unl.push(unloadFn); 27 | var isLeaderListener = function isLeaderListener(msg) { 28 | if (msg.context === 'leader' && msg.action === 'apply') { 29 | sendLeaderMessage(leaderElector, 'tell'); 30 | } 31 | if (msg.context === 'leader' && msg.action === 'tell' && !leaderElector._dpLC) { 32 | /** 33 | * another instance is also leader! 34 | * This can happen on rare events 35 | * like when the CPU is at 100% for long time 36 | * or the tabs are open very long and the browser throttles them. 37 | * @link https://github.com/pubkey/broadcast-channel/issues/414 38 | * @link https://github.com/pubkey/broadcast-channel/issues/385 39 | */ 40 | leaderElector._dpLC = true; 41 | leaderElector._dpL(); // message the lib user so the app can handle the problem 42 | sendLeaderMessage(leaderElector, 'tell'); // ensure other leader also knows the problem 43 | } 44 | }; 45 | leaderElector.broadcastChannel.addEventListener('internal', isLeaderListener); 46 | leaderElector._lstns.push(isLeaderListener); 47 | return sendLeaderMessage(leaderElector, 'tell'); 48 | } -------------------------------------------------------------------------------- /dist/es5node/leader-election-web-lock.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.LeaderElectionWebLock = void 0; 7 | var _util = require("./util.js"); 8 | var _leaderElectionUtil = require("./leader-election-util.js"); 9 | /** 10 | * A faster version of the leader elector that uses the WebLock API 11 | * @link https://developer.mozilla.org/en-US/docs/Web/API/Web_Locks_API 12 | */ 13 | var LeaderElectionWebLock = exports.LeaderElectionWebLock = function LeaderElectionWebLock(broadcastChannel, options) { 14 | var _this = this; 15 | this.broadcastChannel = broadcastChannel; 16 | broadcastChannel._befC.push(function () { 17 | return _this.die(); 18 | }); 19 | this._options = options; 20 | this.isLeader = false; 21 | this.isDead = false; 22 | this.token = (0, _util.randomToken)(); 23 | this._lstns = []; 24 | this._unl = []; 25 | this._dpL = function () {}; // onduplicate listener 26 | this._dpLC = false; // true when onduplicate called 27 | 28 | this._wKMC = {}; // stuff for cleanup 29 | 30 | // lock name 31 | this.lN = 'pubkey-bc||' + broadcastChannel.method.type + '||' + broadcastChannel.name; 32 | }; 33 | LeaderElectionWebLock.prototype = { 34 | hasLeader: function hasLeader() { 35 | var _this2 = this; 36 | return navigator.locks.query().then(function (locks) { 37 | var relevantLocks = locks.held ? locks.held.filter(function (lock) { 38 | return lock.name === _this2.lN; 39 | }) : []; 40 | if (relevantLocks && relevantLocks.length > 0) { 41 | return true; 42 | } else { 43 | return false; 44 | } 45 | }); 46 | }, 47 | awaitLeadership: function awaitLeadership() { 48 | var _this3 = this; 49 | if (!this._wLMP) { 50 | this._wKMC.c = new AbortController(); 51 | var returnPromise = new Promise(function (res, rej) { 52 | _this3._wKMC.res = res; 53 | _this3._wKMC.rej = rej; 54 | }); 55 | this._wLMP = new Promise(function (res) { 56 | navigator.locks.request(_this3.lN, { 57 | signal: _this3._wKMC.c.signal 58 | }, function () { 59 | // if the lock resolved, we can drop the abort controller 60 | _this3._wKMC.c = undefined; 61 | (0, _leaderElectionUtil.beLeader)(_this3); 62 | res(); 63 | return returnPromise; 64 | })["catch"](function () {}); 65 | }); 66 | } 67 | return this._wLMP; 68 | }, 69 | set onduplicate(_fn) { 70 | // Do nothing because there are no duplicates in the WebLock version 71 | }, 72 | die: function die() { 73 | var _this4 = this; 74 | this._lstns.forEach(function (listener) { 75 | return _this4.broadcastChannel.removeEventListener('internal', listener); 76 | }); 77 | this._lstns = []; 78 | this._unl.forEach(function (uFn) { 79 | return uFn.remove(); 80 | }); 81 | this._unl = []; 82 | if (this.isLeader) { 83 | this.isLeader = false; 84 | } 85 | this.isDead = true; 86 | if (this._wKMC.res) { 87 | this._wKMC.res(); 88 | } 89 | if (this._wKMC.c) { 90 | this._wKMC.c.abort('LeaderElectionWebLock.die() called'); 91 | } 92 | return (0, _leaderElectionUtil.sendLeaderMessage)(this, 'death'); 93 | } 94 | }; -------------------------------------------------------------------------------- /dist/es5node/method-chooser.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _typeof = require("@babel/runtime/helpers/typeof"); 4 | Object.defineProperty(exports, "__esModule", { 5 | value: true 6 | }); 7 | exports.chooseMethod = chooseMethod; 8 | var _native = require("./methods/native.js"); 9 | var _indexedDb = require("./methods/indexed-db.js"); 10 | var _localstorage = require("./methods/localstorage.js"); 11 | var _simulate = require("./methods/simulate.js"); 12 | var NodeMethod = _interopRequireWildcard(require("./methods/node.js")); 13 | function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(e) { return e ? t : r; })(e); } 14 | function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != _typeof(e) && "function" != typeof e) return { "default": e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n["default"] = e, t && t.set(e, n), n; } 15 | // the line below will be removed from es5/browser builds 16 | 17 | // order is important 18 | var METHODS = [_native.NativeMethod, 19 | // fastest 20 | _indexedDb.IndexedDBMethod, _localstorage.LocalstorageMethod]; 21 | function chooseMethod(options) { 22 | var chooseMethods = [].concat(options.methods, METHODS).filter(Boolean); 23 | 24 | // the line below will be removed from es5/browser builds 25 | chooseMethods.push(NodeMethod); 26 | 27 | // directly chosen 28 | if (options.type) { 29 | if (options.type === 'simulate') { 30 | // only use simulate-method if directly chosen 31 | return _simulate.SimulateMethod; 32 | } 33 | var ret = chooseMethods.find(function (m) { 34 | return m.type === options.type; 35 | }); 36 | if (!ret) throw new Error('method-type ' + options.type + ' not found');else return ret; 37 | } 38 | 39 | /** 40 | * if no webworker support is needed, 41 | * remove idb from the list so that localstorage will be chosen 42 | */ 43 | if (!options.webWorkerSupport) { 44 | chooseMethods = chooseMethods.filter(function (m) { 45 | return m.type !== 'idb'; 46 | }); 47 | } 48 | var useMethod = chooseMethods.find(function (method) { 49 | return method.canBeUsed(); 50 | }); 51 | if (!useMethod) { 52 | throw new Error("No usable method found in " + JSON.stringify(METHODS.map(function (m) { 53 | return m.type; 54 | }))); 55 | } else { 56 | return useMethod; 57 | } 58 | } -------------------------------------------------------------------------------- /dist/es5node/methods/cookies.js: -------------------------------------------------------------------------------- 1 | /** 2 | * if you really need this method, 3 | * implement it! 4 | */ 5 | "use strict"; -------------------------------------------------------------------------------- /dist/es5node/methods/localstorage.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.LocalstorageMethod = void 0; 7 | exports.addStorageEventListener = addStorageEventListener; 8 | exports.averageResponseTime = averageResponseTime; 9 | exports.canBeUsed = canBeUsed; 10 | exports.close = close; 11 | exports.create = create; 12 | exports.getLocalStorage = getLocalStorage; 13 | exports.microSeconds = void 0; 14 | exports.onMessage = onMessage; 15 | exports.postMessage = postMessage; 16 | exports.removeStorageEventListener = removeStorageEventListener; 17 | exports.storageKey = storageKey; 18 | exports.type = void 0; 19 | var _obliviousSet = require("oblivious-set"); 20 | var _options = require("../options.js"); 21 | var _util = require("../util.js"); 22 | /** 23 | * A localStorage-only method which uses localstorage and its 'storage'-event 24 | * This does not work inside webworkers because they have no access to localstorage 25 | * This is basically implemented to support IE9 or your grandmother's toaster. 26 | * @link https://caniuse.com/#feat=namevalue-storage 27 | * @link https://caniuse.com/#feat=indexeddb 28 | */ 29 | 30 | var microSeconds = exports.microSeconds = _util.microSeconds; 31 | var KEY_PREFIX = 'pubkey.broadcastChannel-'; 32 | var type = exports.type = 'localstorage'; 33 | 34 | /** 35 | * copied from crosstab 36 | * @link https://github.com/tejacques/crosstab/blob/master/src/crosstab.js#L32 37 | */ 38 | function getLocalStorage() { 39 | var localStorage; 40 | if (typeof window === 'undefined') return null; 41 | try { 42 | localStorage = window.localStorage; 43 | localStorage = window['ie8-eventlistener/storage'] || window.localStorage; 44 | } catch (e) { 45 | // New versions of Firefox throw a Security exception 46 | // if cookies are disabled. See 47 | // https://bugzilla.mozilla.org/show_bug.cgi?id=1028153 48 | } 49 | return localStorage; 50 | } 51 | function storageKey(channelName) { 52 | return KEY_PREFIX + channelName; 53 | } 54 | 55 | /** 56 | * writes the new message to the storage 57 | * and fires the storage-event so other readers can find it 58 | */ 59 | function postMessage(channelState, messageJson) { 60 | return new Promise(function (res) { 61 | (0, _util.sleep)().then(function () { 62 | var key = storageKey(channelState.channelName); 63 | var writeObj = { 64 | token: (0, _util.randomToken)(), 65 | time: Date.now(), 66 | data: messageJson, 67 | uuid: channelState.uuid 68 | }; 69 | var value = JSON.stringify(writeObj); 70 | getLocalStorage().setItem(key, value); 71 | 72 | /** 73 | * StorageEvent does not fire the 'storage' event 74 | * in the window that changes the state of the local storage. 75 | * So we fire it manually 76 | */ 77 | var ev = document.createEvent('Event'); 78 | ev.initEvent('storage', true, true); 79 | ev.key = key; 80 | ev.newValue = value; 81 | window.dispatchEvent(ev); 82 | res(); 83 | }); 84 | }); 85 | } 86 | function addStorageEventListener(channelName, fn) { 87 | var key = storageKey(channelName); 88 | var listener = function listener(ev) { 89 | if (ev.key === key) { 90 | fn(JSON.parse(ev.newValue)); 91 | } 92 | }; 93 | window.addEventListener('storage', listener); 94 | return listener; 95 | } 96 | function removeStorageEventListener(listener) { 97 | window.removeEventListener('storage', listener); 98 | } 99 | function create(channelName, options) { 100 | options = (0, _options.fillOptionsWithDefaults)(options); 101 | if (!canBeUsed()) { 102 | throw new Error('BroadcastChannel: localstorage cannot be used'); 103 | } 104 | var uuid = (0, _util.randomToken)(); 105 | 106 | /** 107 | * eMIs 108 | * contains all messages that have been emitted before 109 | * @type {ObliviousSet} 110 | */ 111 | var eMIs = new _obliviousSet.ObliviousSet(options.localstorage.removeTimeout); 112 | var state = { 113 | channelName: channelName, 114 | uuid: uuid, 115 | eMIs: eMIs // emittedMessagesIds 116 | }; 117 | state.listener = addStorageEventListener(channelName, function (msgObj) { 118 | if (!state.messagesCallback) return; // no listener 119 | if (msgObj.uuid === uuid) return; // own message 120 | if (!msgObj.token || eMIs.has(msgObj.token)) return; // already emitted 121 | if (msgObj.data.time && msgObj.data.time < state.messagesCallbackTime) return; // too old 122 | 123 | eMIs.add(msgObj.token); 124 | state.messagesCallback(msgObj.data); 125 | }); 126 | return state; 127 | } 128 | function close(channelState) { 129 | removeStorageEventListener(channelState.listener); 130 | } 131 | function onMessage(channelState, fn, time) { 132 | channelState.messagesCallbackTime = time; 133 | channelState.messagesCallback = fn; 134 | } 135 | function canBeUsed() { 136 | var ls = getLocalStorage(); 137 | if (!ls) return false; 138 | try { 139 | var key = '__broadcastchannel_check'; 140 | ls.setItem(key, 'works'); 141 | ls.removeItem(key); 142 | } catch (e) { 143 | // Safari 10 in private mode will not allow write access to local 144 | // storage and fail with a QuotaExceededError. See 145 | // https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API#Private_Browsing_Incognito_modes 146 | return false; 147 | } 148 | return true; 149 | } 150 | function averageResponseTime() { 151 | var defaultTime = 120; 152 | var userAgent = navigator.userAgent.toLowerCase(); 153 | if (userAgent.includes('safari') && !userAgent.includes('chrome')) { 154 | // safari is much slower so this time is higher 155 | return defaultTime * 2; 156 | } 157 | return defaultTime; 158 | } 159 | var LocalstorageMethod = exports.LocalstorageMethod = { 160 | create: create, 161 | close: close, 162 | onMessage: onMessage, 163 | postMessage: postMessage, 164 | canBeUsed: canBeUsed, 165 | type: type, 166 | averageResponseTime: averageResponseTime, 167 | microSeconds: microSeconds 168 | }; -------------------------------------------------------------------------------- /dist/es5node/methods/native.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.NativeMethod = void 0; 7 | exports.averageResponseTime = averageResponseTime; 8 | exports.canBeUsed = canBeUsed; 9 | exports.close = close; 10 | exports.create = create; 11 | exports.microSeconds = void 0; 12 | exports.onMessage = onMessage; 13 | exports.postMessage = postMessage; 14 | exports.type = void 0; 15 | var _util = require("../util.js"); 16 | var microSeconds = exports.microSeconds = _util.microSeconds; 17 | var type = exports.type = 'native'; 18 | function create(channelName) { 19 | var state = { 20 | time: (0, _util.microSeconds)(), 21 | messagesCallback: null, 22 | bc: new BroadcastChannel(channelName), 23 | subFns: [] // subscriberFunctions 24 | }; 25 | state.bc.onmessage = function (msgEvent) { 26 | if (state.messagesCallback) { 27 | state.messagesCallback(msgEvent.data); 28 | } 29 | }; 30 | return state; 31 | } 32 | function close(channelState) { 33 | channelState.bc.close(); 34 | channelState.subFns = []; 35 | } 36 | function postMessage(channelState, messageJson) { 37 | try { 38 | channelState.bc.postMessage(messageJson, false); 39 | return _util.PROMISE_RESOLVED_VOID; 40 | } catch (err) { 41 | return Promise.reject(err); 42 | } 43 | } 44 | function onMessage(channelState, fn) { 45 | channelState.messagesCallback = fn; 46 | } 47 | function canBeUsed() { 48 | // Deno runtime 49 | // eslint-disable-next-line 50 | if (typeof globalThis !== 'undefined' && globalThis.Deno && globalThis.Deno.args) { 51 | return true; 52 | } 53 | 54 | // Browser runtime 55 | if ((typeof window !== 'undefined' || typeof self !== 'undefined') && typeof BroadcastChannel === 'function') { 56 | if (BroadcastChannel._pubkey) { 57 | throw new Error('BroadcastChannel: Do not overwrite window.BroadcastChannel with this module, this is not a polyfill'); 58 | } 59 | return true; 60 | } else { 61 | return false; 62 | } 63 | } 64 | function averageResponseTime() { 65 | return 150; 66 | } 67 | var NativeMethod = exports.NativeMethod = { 68 | create: create, 69 | close: close, 70 | onMessage: onMessage, 71 | postMessage: postMessage, 72 | canBeUsed: canBeUsed, 73 | type: type, 74 | averageResponseTime: averageResponseTime, 75 | microSeconds: microSeconds 76 | }; -------------------------------------------------------------------------------- /dist/es5node/methods/simulate.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.SimulateMethod = exports.SIMULATE_DELAY_TIME = void 0; 7 | exports.averageResponseTime = averageResponseTime; 8 | exports.canBeUsed = canBeUsed; 9 | exports.close = close; 10 | exports.create = create; 11 | exports.microSeconds = void 0; 12 | exports.onMessage = onMessage; 13 | exports.postMessage = postMessage; 14 | exports.type = void 0; 15 | var _util = require("../util.js"); 16 | var microSeconds = exports.microSeconds = _util.microSeconds; 17 | var type = exports.type = 'simulate'; 18 | var SIMULATE_CHANNELS = new Set(); 19 | function create(channelName) { 20 | var state = { 21 | time: microSeconds(), 22 | name: channelName, 23 | messagesCallback: null 24 | }; 25 | SIMULATE_CHANNELS.add(state); 26 | return state; 27 | } 28 | function close(channelState) { 29 | SIMULATE_CHANNELS["delete"](channelState); 30 | } 31 | var SIMULATE_DELAY_TIME = exports.SIMULATE_DELAY_TIME = 5; 32 | function postMessage(channelState, messageJson) { 33 | return new Promise(function (res) { 34 | return setTimeout(function () { 35 | var channelArray = Array.from(SIMULATE_CHANNELS); 36 | channelArray.forEach(function (channel) { 37 | if (channel.name === channelState.name && 38 | // has same name 39 | channel !== channelState && 40 | // not own channel 41 | !!channel.messagesCallback && 42 | // has subscribers 43 | channel.time < messageJson.time // channel not created after postMessage() call 44 | ) { 45 | channel.messagesCallback(messageJson); 46 | } 47 | }); 48 | res(); 49 | }, SIMULATE_DELAY_TIME); 50 | }); 51 | } 52 | function onMessage(channelState, fn) { 53 | channelState.messagesCallback = fn; 54 | } 55 | function canBeUsed() { 56 | return true; 57 | } 58 | function averageResponseTime() { 59 | return SIMULATE_DELAY_TIME; 60 | } 61 | var SimulateMethod = exports.SimulateMethod = { 62 | create: create, 63 | close: close, 64 | onMessage: onMessage, 65 | postMessage: postMessage, 66 | canBeUsed: canBeUsed, 67 | type: type, 68 | averageResponseTime: averageResponseTime, 69 | microSeconds: microSeconds 70 | }; -------------------------------------------------------------------------------- /dist/es5node/options.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.fillOptionsWithDefaults = fillOptionsWithDefaults; 7 | function fillOptionsWithDefaults() { 8 | var originalOptions = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; 9 | var options = JSON.parse(JSON.stringify(originalOptions)); 10 | 11 | // main 12 | if (typeof options.webWorkerSupport === 'undefined') options.webWorkerSupport = true; 13 | 14 | // indexed-db 15 | if (!options.idb) options.idb = {}; 16 | // after this time the messages get deleted 17 | if (!options.idb.ttl) options.idb.ttl = 1000 * 45; 18 | if (!options.idb.fallbackInterval) options.idb.fallbackInterval = 150; 19 | // handles abrupt db onclose events. 20 | if (originalOptions.idb && typeof originalOptions.idb.onclose === 'function') options.idb.onclose = originalOptions.idb.onclose; 21 | 22 | // localstorage 23 | if (!options.localstorage) options.localstorage = {}; 24 | if (!options.localstorage.removeTimeout) options.localstorage.removeTimeout = 1000 * 60; 25 | 26 | // custom methods 27 | if (originalOptions.methods) options.methods = originalOptions.methods; 28 | 29 | // node 30 | if (!options.node) options.node = {}; 31 | if (!options.node.ttl) options.node.ttl = 1000 * 60 * 2; // 2 minutes; 32 | /** 33 | * On linux use 'ulimit -Hn' to get the limit of open files. 34 | * On ubuntu this was 4096 for me, so we use half of that as maxParallelWrites default. 35 | */ 36 | if (!options.node.maxParallelWrites) options.node.maxParallelWrites = 2048; 37 | if (typeof options.node.useFastPath === 'undefined') options.node.useFastPath = true; 38 | return options; 39 | } -------------------------------------------------------------------------------- /dist/es5node/util.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.PROMISE_RESOLVED_VOID = exports.PROMISE_RESOLVED_TRUE = exports.PROMISE_RESOLVED_FALSE = void 0; 7 | exports.isPromise = isPromise; 8 | exports.microSeconds = microSeconds; 9 | exports.randomInt = randomInt; 10 | exports.randomToken = randomToken; 11 | exports.sleep = sleep; 12 | exports.supportsWebLockAPI = supportsWebLockAPI; 13 | /** 14 | * returns true if the given object is a promise 15 | */ 16 | function isPromise(obj) { 17 | return obj && typeof obj.then === 'function'; 18 | } 19 | var PROMISE_RESOLVED_FALSE = exports.PROMISE_RESOLVED_FALSE = Promise.resolve(false); 20 | var PROMISE_RESOLVED_TRUE = exports.PROMISE_RESOLVED_TRUE = Promise.resolve(true); 21 | var PROMISE_RESOLVED_VOID = exports.PROMISE_RESOLVED_VOID = Promise.resolve(); 22 | function sleep(time, resolveWith) { 23 | if (!time) time = 0; 24 | return new Promise(function (res) { 25 | return setTimeout(function () { 26 | return res(resolveWith); 27 | }, time); 28 | }); 29 | } 30 | function randomInt(min, max) { 31 | return Math.floor(Math.random() * (max - min + 1) + min); 32 | } 33 | 34 | /** 35 | * https://stackoverflow.com/a/8084248 36 | */ 37 | function randomToken() { 38 | return Math.random().toString(36).substring(2); 39 | } 40 | var lastMs = 0; 41 | 42 | /** 43 | * Returns the current unix time in micro-seconds, 44 | * WARNING: This is a pseudo-function 45 | * Performance.now is not reliable in webworkers, so we just make sure to never return the same time. 46 | * This is enough in browsers, and this function will not be used in nodejs. 47 | * The main reason for this hack is to ensure that BroadcastChannel behaves equal to production when it is used in fast-running unit tests. 48 | */ 49 | function microSeconds() { 50 | var ret = Date.now() * 1000; // milliseconds to microseconds 51 | if (ret <= lastMs) { 52 | ret = lastMs + 1; 53 | } 54 | lastMs = ret; 55 | return ret; 56 | } 57 | 58 | /** 59 | * Check if WebLock API is supported. 60 | * @link https://developer.mozilla.org/en-US/docs/Web/API/Web_Locks_API 61 | */ 62 | function supportsWebLockAPI() { 63 | if (typeof navigator !== 'undefined' && typeof navigator.locks !== 'undefined' && typeof navigator.locks.request === 'function') { 64 | return true; 65 | } else { 66 | return false; 67 | } 68 | } -------------------------------------------------------------------------------- /dist/esbrowser/browserify.index.js: -------------------------------------------------------------------------------- 1 | var module = require('./index.es5.js'); 2 | var BroadcastChannel = module.BroadcastChannel; 3 | var createLeaderElection = module.createLeaderElection; 4 | window['BroadcastChannel2'] = BroadcastChannel; 5 | window['createLeaderElection'] = createLeaderElection; -------------------------------------------------------------------------------- /dist/esbrowser/index.es5.js: -------------------------------------------------------------------------------- 1 | /** 2 | * because babel can only export on default-attribute, 3 | * we use this for the non-module-build 4 | * this ensures that users do not have to use 5 | * var BroadcastChannel = require('broadcast-channel').default; 6 | * but 7 | * var BroadcastChannel = require('broadcast-channel'); 8 | */ 9 | 10 | import { BroadcastChannel, createLeaderElection, clearNodeFolder, enforceOptions, beLeader } from './index.js'; 11 | module.exports = { 12 | BroadcastChannel: BroadcastChannel, 13 | createLeaderElection: createLeaderElection, 14 | clearNodeFolder: clearNodeFolder, 15 | enforceOptions: enforceOptions, 16 | beLeader: beLeader 17 | }; -------------------------------------------------------------------------------- /dist/esbrowser/index.js: -------------------------------------------------------------------------------- 1 | export { BroadcastChannel, clearNodeFolder, enforceOptions, OPEN_BROADCAST_CHANNELS } from './broadcast-channel.js'; 2 | export { createLeaderElection } from './leader-election.js'; 3 | export { beLeader } from './leader-election-util.js'; -------------------------------------------------------------------------------- /dist/esbrowser/leader-election-util.js: -------------------------------------------------------------------------------- 1 | import { add as unloadAdd } from 'unload'; 2 | 3 | /** 4 | * sends and internal message over the broadcast-channel 5 | */ 6 | export function sendLeaderMessage(leaderElector, action) { 7 | var msgJson = { 8 | context: 'leader', 9 | action: action, 10 | token: leaderElector.token 11 | }; 12 | return leaderElector.broadcastChannel.postInternal(msgJson); 13 | } 14 | export function beLeader(leaderElector) { 15 | leaderElector.isLeader = true; 16 | leaderElector._hasLeader = true; 17 | var unloadFn = unloadAdd(function () { 18 | return leaderElector.die(); 19 | }); 20 | leaderElector._unl.push(unloadFn); 21 | var isLeaderListener = function isLeaderListener(msg) { 22 | if (msg.context === 'leader' && msg.action === 'apply') { 23 | sendLeaderMessage(leaderElector, 'tell'); 24 | } 25 | if (msg.context === 'leader' && msg.action === 'tell' && !leaderElector._dpLC) { 26 | /** 27 | * another instance is also leader! 28 | * This can happen on rare events 29 | * like when the CPU is at 100% for long time 30 | * or the tabs are open very long and the browser throttles them. 31 | * @link https://github.com/pubkey/broadcast-channel/issues/414 32 | * @link https://github.com/pubkey/broadcast-channel/issues/385 33 | */ 34 | leaderElector._dpLC = true; 35 | leaderElector._dpL(); // message the lib user so the app can handle the problem 36 | sendLeaderMessage(leaderElector, 'tell'); // ensure other leader also knows the problem 37 | } 38 | }; 39 | leaderElector.broadcastChannel.addEventListener('internal', isLeaderListener); 40 | leaderElector._lstns.push(isLeaderListener); 41 | return sendLeaderMessage(leaderElector, 'tell'); 42 | } -------------------------------------------------------------------------------- /dist/esbrowser/leader-election-web-lock.js: -------------------------------------------------------------------------------- 1 | import { randomToken } from './util.js'; 2 | import { sendLeaderMessage, beLeader } from './leader-election-util.js'; 3 | 4 | /** 5 | * A faster version of the leader elector that uses the WebLock API 6 | * @link https://developer.mozilla.org/en-US/docs/Web/API/Web_Locks_API 7 | */ 8 | export var LeaderElectionWebLock = function LeaderElectionWebLock(broadcastChannel, options) { 9 | var _this = this; 10 | this.broadcastChannel = broadcastChannel; 11 | broadcastChannel._befC.push(function () { 12 | return _this.die(); 13 | }); 14 | this._options = options; 15 | this.isLeader = false; 16 | this.isDead = false; 17 | this.token = randomToken(); 18 | this._lstns = []; 19 | this._unl = []; 20 | this._dpL = function () {}; // onduplicate listener 21 | this._dpLC = false; // true when onduplicate called 22 | 23 | this._wKMC = {}; // stuff for cleanup 24 | 25 | // lock name 26 | this.lN = 'pubkey-bc||' + broadcastChannel.method.type + '||' + broadcastChannel.name; 27 | }; 28 | LeaderElectionWebLock.prototype = { 29 | hasLeader: function hasLeader() { 30 | var _this2 = this; 31 | return navigator.locks.query().then(function (locks) { 32 | var relevantLocks = locks.held ? locks.held.filter(function (lock) { 33 | return lock.name === _this2.lN; 34 | }) : []; 35 | if (relevantLocks && relevantLocks.length > 0) { 36 | return true; 37 | } else { 38 | return false; 39 | } 40 | }); 41 | }, 42 | awaitLeadership: function awaitLeadership() { 43 | var _this3 = this; 44 | if (!this._wLMP) { 45 | this._wKMC.c = new AbortController(); 46 | var returnPromise = new Promise(function (res, rej) { 47 | _this3._wKMC.res = res; 48 | _this3._wKMC.rej = rej; 49 | }); 50 | this._wLMP = new Promise(function (res) { 51 | navigator.locks.request(_this3.lN, { 52 | signal: _this3._wKMC.c.signal 53 | }, function () { 54 | // if the lock resolved, we can drop the abort controller 55 | _this3._wKMC.c = undefined; 56 | beLeader(_this3); 57 | res(); 58 | return returnPromise; 59 | })["catch"](function () {}); 60 | }); 61 | } 62 | return this._wLMP; 63 | }, 64 | set onduplicate(_fn) { 65 | // Do nothing because there are no duplicates in the WebLock version 66 | }, 67 | die: function die() { 68 | var _this4 = this; 69 | this._lstns.forEach(function (listener) { 70 | return _this4.broadcastChannel.removeEventListener('internal', listener); 71 | }); 72 | this._lstns = []; 73 | this._unl.forEach(function (uFn) { 74 | return uFn.remove(); 75 | }); 76 | this._unl = []; 77 | if (this.isLeader) { 78 | this.isLeader = false; 79 | } 80 | this.isDead = true; 81 | if (this._wKMC.res) { 82 | this._wKMC.res(); 83 | } 84 | if (this._wKMC.c) { 85 | this._wKMC.c.abort('LeaderElectionWebLock.die() called'); 86 | } 87 | return sendLeaderMessage(this, 'death'); 88 | } 89 | }; -------------------------------------------------------------------------------- /dist/esbrowser/method-chooser.js: -------------------------------------------------------------------------------- 1 | import { NativeMethod } from './methods/native.js'; 2 | import { IndexedDBMethod } from './methods/indexed-db.js'; 3 | import { LocalstorageMethod } from './methods/localstorage.js'; 4 | import { SimulateMethod } from './methods/simulate.js'; 5 | // the line below will be removed from es5/browser builds 6 | 7 | // order is important 8 | var METHODS = [NativeMethod, 9 | // fastest 10 | IndexedDBMethod, LocalstorageMethod]; 11 | export function chooseMethod(options) { 12 | var chooseMethods = [].concat(options.methods, METHODS).filter(Boolean); 13 | 14 | // the line below will be removed from es5/browser builds 15 | 16 | // directly chosen 17 | if (options.type) { 18 | if (options.type === 'simulate') { 19 | // only use simulate-method if directly chosen 20 | return SimulateMethod; 21 | } 22 | var ret = chooseMethods.find(function (m) { 23 | return m.type === options.type; 24 | }); 25 | if (!ret) throw new Error('method-type ' + options.type + ' not found');else return ret; 26 | } 27 | 28 | /** 29 | * if no webworker support is needed, 30 | * remove idb from the list so that localstorage will be chosen 31 | */ 32 | if (!options.webWorkerSupport) { 33 | chooseMethods = chooseMethods.filter(function (m) { 34 | return m.type !== 'idb'; 35 | }); 36 | } 37 | var useMethod = chooseMethods.find(function (method) { 38 | return method.canBeUsed(); 39 | }); 40 | if (!useMethod) { 41 | throw new Error("No usable method found in " + JSON.stringify(METHODS.map(function (m) { 42 | return m.type; 43 | }))); 44 | } else { 45 | return useMethod; 46 | } 47 | } -------------------------------------------------------------------------------- /dist/esbrowser/methods/cookies.js: -------------------------------------------------------------------------------- 1 | /** 2 | * if you really need this method, 3 | * implement it! 4 | */ -------------------------------------------------------------------------------- /dist/esbrowser/methods/localstorage.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A localStorage-only method which uses localstorage and its 'storage'-event 3 | * This does not work inside webworkers because they have no access to localstorage 4 | * This is basically implemented to support IE9 or your grandmother's toaster. 5 | * @link https://caniuse.com/#feat=namevalue-storage 6 | * @link https://caniuse.com/#feat=indexeddb 7 | */ 8 | 9 | import { ObliviousSet } from 'oblivious-set'; 10 | import { fillOptionsWithDefaults } from '../options.js'; 11 | import { sleep, randomToken, microSeconds as micro } from '../util.js'; 12 | export var microSeconds = micro; 13 | var KEY_PREFIX = 'pubkey.broadcastChannel-'; 14 | export var type = 'localstorage'; 15 | 16 | /** 17 | * copied from crosstab 18 | * @link https://github.com/tejacques/crosstab/blob/master/src/crosstab.js#L32 19 | */ 20 | export function getLocalStorage() { 21 | var localStorage; 22 | if (typeof window === 'undefined') return null; 23 | try { 24 | localStorage = window.localStorage; 25 | localStorage = window['ie8-eventlistener/storage'] || window.localStorage; 26 | } catch (e) { 27 | // New versions of Firefox throw a Security exception 28 | // if cookies are disabled. See 29 | // https://bugzilla.mozilla.org/show_bug.cgi?id=1028153 30 | } 31 | return localStorage; 32 | } 33 | export function storageKey(channelName) { 34 | return KEY_PREFIX + channelName; 35 | } 36 | 37 | /** 38 | * writes the new message to the storage 39 | * and fires the storage-event so other readers can find it 40 | */ 41 | export function postMessage(channelState, messageJson) { 42 | return new Promise(function (res) { 43 | sleep().then(function () { 44 | var key = storageKey(channelState.channelName); 45 | var writeObj = { 46 | token: randomToken(), 47 | time: Date.now(), 48 | data: messageJson, 49 | uuid: channelState.uuid 50 | }; 51 | var value = JSON.stringify(writeObj); 52 | getLocalStorage().setItem(key, value); 53 | 54 | /** 55 | * StorageEvent does not fire the 'storage' event 56 | * in the window that changes the state of the local storage. 57 | * So we fire it manually 58 | */ 59 | var ev = document.createEvent('Event'); 60 | ev.initEvent('storage', true, true); 61 | ev.key = key; 62 | ev.newValue = value; 63 | window.dispatchEvent(ev); 64 | res(); 65 | }); 66 | }); 67 | } 68 | export function addStorageEventListener(channelName, fn) { 69 | var key = storageKey(channelName); 70 | var listener = function listener(ev) { 71 | if (ev.key === key) { 72 | fn(JSON.parse(ev.newValue)); 73 | } 74 | }; 75 | window.addEventListener('storage', listener); 76 | return listener; 77 | } 78 | export function removeStorageEventListener(listener) { 79 | window.removeEventListener('storage', listener); 80 | } 81 | export function create(channelName, options) { 82 | options = fillOptionsWithDefaults(options); 83 | if (!canBeUsed()) { 84 | throw new Error('BroadcastChannel: localstorage cannot be used'); 85 | } 86 | var uuid = randomToken(); 87 | 88 | /** 89 | * eMIs 90 | * contains all messages that have been emitted before 91 | * @type {ObliviousSet} 92 | */ 93 | var eMIs = new ObliviousSet(options.localstorage.removeTimeout); 94 | var state = { 95 | channelName: channelName, 96 | uuid: uuid, 97 | eMIs: eMIs // emittedMessagesIds 98 | }; 99 | state.listener = addStorageEventListener(channelName, function (msgObj) { 100 | if (!state.messagesCallback) return; // no listener 101 | if (msgObj.uuid === uuid) return; // own message 102 | if (!msgObj.token || eMIs.has(msgObj.token)) return; // already emitted 103 | if (msgObj.data.time && msgObj.data.time < state.messagesCallbackTime) return; // too old 104 | 105 | eMIs.add(msgObj.token); 106 | state.messagesCallback(msgObj.data); 107 | }); 108 | return state; 109 | } 110 | export function close(channelState) { 111 | removeStorageEventListener(channelState.listener); 112 | } 113 | export function onMessage(channelState, fn, time) { 114 | channelState.messagesCallbackTime = time; 115 | channelState.messagesCallback = fn; 116 | } 117 | export function canBeUsed() { 118 | var ls = getLocalStorage(); 119 | if (!ls) return false; 120 | try { 121 | var key = '__broadcastchannel_check'; 122 | ls.setItem(key, 'works'); 123 | ls.removeItem(key); 124 | } catch (e) { 125 | // Safari 10 in private mode will not allow write access to local 126 | // storage and fail with a QuotaExceededError. See 127 | // https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API#Private_Browsing_Incognito_modes 128 | return false; 129 | } 130 | return true; 131 | } 132 | export function averageResponseTime() { 133 | var defaultTime = 120; 134 | var userAgent = navigator.userAgent.toLowerCase(); 135 | if (userAgent.includes('safari') && !userAgent.includes('chrome')) { 136 | // safari is much slower so this time is higher 137 | return defaultTime * 2; 138 | } 139 | return defaultTime; 140 | } 141 | export var LocalstorageMethod = { 142 | create: create, 143 | close: close, 144 | onMessage: onMessage, 145 | postMessage: postMessage, 146 | canBeUsed: canBeUsed, 147 | type: type, 148 | averageResponseTime: averageResponseTime, 149 | microSeconds: microSeconds 150 | }; -------------------------------------------------------------------------------- /dist/esbrowser/methods/native.js: -------------------------------------------------------------------------------- 1 | import { microSeconds as micro, PROMISE_RESOLVED_VOID } from '../util.js'; 2 | export var microSeconds = micro; 3 | export var type = 'native'; 4 | export function create(channelName) { 5 | var state = { 6 | time: micro(), 7 | messagesCallback: null, 8 | bc: new BroadcastChannel(channelName), 9 | subFns: [] // subscriberFunctions 10 | }; 11 | state.bc.onmessage = function (msgEvent) { 12 | if (state.messagesCallback) { 13 | state.messagesCallback(msgEvent.data); 14 | } 15 | }; 16 | return state; 17 | } 18 | export function close(channelState) { 19 | channelState.bc.close(); 20 | channelState.subFns = []; 21 | } 22 | export function postMessage(channelState, messageJson) { 23 | try { 24 | channelState.bc.postMessage(messageJson, false); 25 | return PROMISE_RESOLVED_VOID; 26 | } catch (err) { 27 | return Promise.reject(err); 28 | } 29 | } 30 | export function onMessage(channelState, fn) { 31 | channelState.messagesCallback = fn; 32 | } 33 | export function canBeUsed() { 34 | // Deno runtime 35 | // eslint-disable-next-line 36 | if (typeof globalThis !== 'undefined' && globalThis.Deno && globalThis.Deno.args) { 37 | return true; 38 | } 39 | 40 | // Browser runtime 41 | if ((typeof window !== 'undefined' || typeof self !== 'undefined') && typeof BroadcastChannel === 'function') { 42 | if (BroadcastChannel._pubkey) { 43 | throw new Error('BroadcastChannel: Do not overwrite window.BroadcastChannel with this module, this is not a polyfill'); 44 | } 45 | return true; 46 | } else { 47 | return false; 48 | } 49 | } 50 | export function averageResponseTime() { 51 | return 150; 52 | } 53 | export var NativeMethod = { 54 | create: create, 55 | close: close, 56 | onMessage: onMessage, 57 | postMessage: postMessage, 58 | canBeUsed: canBeUsed, 59 | type: type, 60 | averageResponseTime: averageResponseTime, 61 | microSeconds: microSeconds 62 | }; -------------------------------------------------------------------------------- /dist/esbrowser/methods/simulate.js: -------------------------------------------------------------------------------- 1 | import { microSeconds as micro } from '../util.js'; 2 | export var microSeconds = micro; 3 | export var type = 'simulate'; 4 | var SIMULATE_CHANNELS = new Set(); 5 | export function create(channelName) { 6 | var state = { 7 | time: microSeconds(), 8 | name: channelName, 9 | messagesCallback: null 10 | }; 11 | SIMULATE_CHANNELS.add(state); 12 | return state; 13 | } 14 | export function close(channelState) { 15 | SIMULATE_CHANNELS["delete"](channelState); 16 | } 17 | export var SIMULATE_DELAY_TIME = 5; 18 | export function postMessage(channelState, messageJson) { 19 | return new Promise(function (res) { 20 | return setTimeout(function () { 21 | var channelArray = Array.from(SIMULATE_CHANNELS); 22 | channelArray.forEach(function (channel) { 23 | if (channel.name === channelState.name && 24 | // has same name 25 | channel !== channelState && 26 | // not own channel 27 | !!channel.messagesCallback && 28 | // has subscribers 29 | channel.time < messageJson.time // channel not created after postMessage() call 30 | ) { 31 | channel.messagesCallback(messageJson); 32 | } 33 | }); 34 | res(); 35 | }, SIMULATE_DELAY_TIME); 36 | }); 37 | } 38 | export function onMessage(channelState, fn) { 39 | channelState.messagesCallback = fn; 40 | } 41 | export function canBeUsed() { 42 | return true; 43 | } 44 | export function averageResponseTime() { 45 | return SIMULATE_DELAY_TIME; 46 | } 47 | export var SimulateMethod = { 48 | create: create, 49 | close: close, 50 | onMessage: onMessage, 51 | postMessage: postMessage, 52 | canBeUsed: canBeUsed, 53 | type: type, 54 | averageResponseTime: averageResponseTime, 55 | microSeconds: microSeconds 56 | }; -------------------------------------------------------------------------------- /dist/esbrowser/options.js: -------------------------------------------------------------------------------- 1 | export function fillOptionsWithDefaults() { 2 | var originalOptions = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; 3 | var options = JSON.parse(JSON.stringify(originalOptions)); 4 | 5 | // main 6 | if (typeof options.webWorkerSupport === 'undefined') options.webWorkerSupport = true; 7 | 8 | // indexed-db 9 | if (!options.idb) options.idb = {}; 10 | // after this time the messages get deleted 11 | if (!options.idb.ttl) options.idb.ttl = 1000 * 45; 12 | if (!options.idb.fallbackInterval) options.idb.fallbackInterval = 150; 13 | // handles abrupt db onclose events. 14 | if (originalOptions.idb && typeof originalOptions.idb.onclose === 'function') options.idb.onclose = originalOptions.idb.onclose; 15 | 16 | // localstorage 17 | if (!options.localstorage) options.localstorage = {}; 18 | if (!options.localstorage.removeTimeout) options.localstorage.removeTimeout = 1000 * 60; 19 | 20 | // custom methods 21 | if (originalOptions.methods) options.methods = originalOptions.methods; 22 | 23 | // node 24 | if (!options.node) options.node = {}; 25 | if (!options.node.ttl) options.node.ttl = 1000 * 60 * 2; // 2 minutes; 26 | /** 27 | * On linux use 'ulimit -Hn' to get the limit of open files. 28 | * On ubuntu this was 4096 for me, so we use half of that as maxParallelWrites default. 29 | */ 30 | if (!options.node.maxParallelWrites) options.node.maxParallelWrites = 2048; 31 | if (typeof options.node.useFastPath === 'undefined') options.node.useFastPath = true; 32 | return options; 33 | } -------------------------------------------------------------------------------- /dist/esbrowser/package.json: -------------------------------------------------------------------------------- 1 | { "type": "module", "sideEffects": false } 2 | -------------------------------------------------------------------------------- /dist/esbrowser/util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * returns true if the given object is a promise 3 | */ 4 | export function isPromise(obj) { 5 | return obj && typeof obj.then === 'function'; 6 | } 7 | export var PROMISE_RESOLVED_FALSE = Promise.resolve(false); 8 | export var PROMISE_RESOLVED_TRUE = Promise.resolve(true); 9 | export var PROMISE_RESOLVED_VOID = Promise.resolve(); 10 | export function sleep(time, resolveWith) { 11 | if (!time) time = 0; 12 | return new Promise(function (res) { 13 | return setTimeout(function () { 14 | return res(resolveWith); 15 | }, time); 16 | }); 17 | } 18 | export function randomInt(min, max) { 19 | return Math.floor(Math.random() * (max - min + 1) + min); 20 | } 21 | 22 | /** 23 | * https://stackoverflow.com/a/8084248 24 | */ 25 | export function randomToken() { 26 | return Math.random().toString(36).substring(2); 27 | } 28 | var lastMs = 0; 29 | 30 | /** 31 | * Returns the current unix time in micro-seconds, 32 | * WARNING: This is a pseudo-function 33 | * Performance.now is not reliable in webworkers, so we just make sure to never return the same time. 34 | * This is enough in browsers, and this function will not be used in nodejs. 35 | * The main reason for this hack is to ensure that BroadcastChannel behaves equal to production when it is used in fast-running unit tests. 36 | */ 37 | export function microSeconds() { 38 | var ret = Date.now() * 1000; // milliseconds to microseconds 39 | if (ret <= lastMs) { 40 | ret = lastMs + 1; 41 | } 42 | lastMs = ret; 43 | return ret; 44 | } 45 | 46 | /** 47 | * Check if WebLock API is supported. 48 | * @link https://developer.mozilla.org/en-US/docs/Web/API/Web_Locks_API 49 | */ 50 | export function supportsWebLockAPI() { 51 | if (typeof navigator !== 'undefined' && typeof navigator.locks !== 'undefined' && typeof navigator.locks.request === 'function') { 52 | return true; 53 | } else { 54 | return false; 55 | } 56 | } -------------------------------------------------------------------------------- /dist/esnode/broadcast-channel.js: -------------------------------------------------------------------------------- 1 | import { isPromise, PROMISE_RESOLVED_FALSE, PROMISE_RESOLVED_VOID } from './util.js'; 2 | import { chooseMethod } from './method-chooser.js'; 3 | import { fillOptionsWithDefaults } from './options.js'; 4 | 5 | /** 6 | * Contains all open channels, 7 | * used in tests to ensure everything is closed. 8 | */ 9 | export var OPEN_BROADCAST_CHANNELS = new Set(); 10 | var lastId = 0; 11 | export var BroadcastChannel = function BroadcastChannel(name, options) { 12 | // identifier of the channel to debug stuff 13 | this.id = lastId++; 14 | OPEN_BROADCAST_CHANNELS.add(this); 15 | this.name = name; 16 | if (ENFORCED_OPTIONS) { 17 | options = ENFORCED_OPTIONS; 18 | } 19 | this.options = fillOptionsWithDefaults(options); 20 | this.method = chooseMethod(this.options); 21 | 22 | // isListening 23 | this._iL = false; 24 | 25 | /** 26 | * _onMessageListener 27 | * setting onmessage twice, 28 | * will overwrite the first listener 29 | */ 30 | this._onML = null; 31 | 32 | /** 33 | * _addEventListeners 34 | */ 35 | this._addEL = { 36 | message: [], 37 | internal: [] 38 | }; 39 | 40 | /** 41 | * Unsent message promises 42 | * where the sending is still in progress 43 | * @type {Set} 44 | */ 45 | this._uMP = new Set(); 46 | 47 | /** 48 | * _beforeClose 49 | * array of promises that will be awaited 50 | * before the channel is closed 51 | */ 52 | this._befC = []; 53 | 54 | /** 55 | * _preparePromise 56 | */ 57 | this._prepP = null; 58 | _prepareChannel(this); 59 | }; 60 | 61 | // STATICS 62 | 63 | /** 64 | * used to identify if someone overwrites 65 | * window.BroadcastChannel with this 66 | * See methods/native.js 67 | */ 68 | BroadcastChannel._pubkey = true; 69 | 70 | /** 71 | * clears the tmp-folder if is node 72 | * @return {Promise} true if has run, false if not node 73 | */ 74 | export function clearNodeFolder(options) { 75 | options = fillOptionsWithDefaults(options); 76 | var method = chooseMethod(options); 77 | if (method.type === 'node') { 78 | return method.clearNodeFolder().then(function () { 79 | return true; 80 | }); 81 | } else { 82 | return PROMISE_RESOLVED_FALSE; 83 | } 84 | } 85 | 86 | /** 87 | * if set, this method is enforced, 88 | * no mather what the options are 89 | */ 90 | var ENFORCED_OPTIONS; 91 | export function enforceOptions(options) { 92 | ENFORCED_OPTIONS = options; 93 | } 94 | 95 | // PROTOTYPE 96 | BroadcastChannel.prototype = { 97 | postMessage: function postMessage(msg) { 98 | if (this.closed) { 99 | throw new Error('BroadcastChannel.postMessage(): ' + 'Cannot post message after channel has closed ' + 100 | /** 101 | * In the past when this error appeared, it was really hard to debug. 102 | * So now we log the msg together with the error so it at least 103 | * gives some clue about where in your application this happens. 104 | */ 105 | JSON.stringify(msg)); 106 | } 107 | return _post(this, 'message', msg); 108 | }, 109 | postInternal: function postInternal(msg) { 110 | return _post(this, 'internal', msg); 111 | }, 112 | set onmessage(fn) { 113 | var time = this.method.microSeconds(); 114 | var listenObj = { 115 | time: time, 116 | fn: fn 117 | }; 118 | _removeListenerObject(this, 'message', this._onML); 119 | if (fn && typeof fn === 'function') { 120 | this._onML = listenObj; 121 | _addListenerObject(this, 'message', listenObj); 122 | } else { 123 | this._onML = null; 124 | } 125 | }, 126 | addEventListener: function addEventListener(type, fn) { 127 | var time = this.method.microSeconds(); 128 | var listenObj = { 129 | time: time, 130 | fn: fn 131 | }; 132 | _addListenerObject(this, type, listenObj); 133 | }, 134 | removeEventListener: function removeEventListener(type, fn) { 135 | var obj = this._addEL[type].find(function (obj) { 136 | return obj.fn === fn; 137 | }); 138 | _removeListenerObject(this, type, obj); 139 | }, 140 | close: function close() { 141 | var _this = this; 142 | if (this.closed) { 143 | return; 144 | } 145 | OPEN_BROADCAST_CHANNELS["delete"](this); 146 | this.closed = true; 147 | var awaitPrepare = this._prepP ? this._prepP : PROMISE_RESOLVED_VOID; 148 | this._onML = null; 149 | this._addEL.message = []; 150 | return awaitPrepare 151 | // wait until all current sending are processed 152 | .then(function () { 153 | return Promise.all(Array.from(_this._uMP)); 154 | }) 155 | // run before-close hooks 156 | .then(function () { 157 | return Promise.all(_this._befC.map(function (fn) { 158 | return fn(); 159 | })); 160 | }) 161 | // close the channel 162 | .then(function () { 163 | return _this.method.close(_this._state); 164 | }); 165 | }, 166 | get type() { 167 | return this.method.type; 168 | }, 169 | get isClosed() { 170 | return this.closed; 171 | } 172 | }; 173 | 174 | /** 175 | * Post a message over the channel 176 | * @returns {Promise} that resolved when the message sending is done 177 | */ 178 | function _post(broadcastChannel, type, msg) { 179 | var time = broadcastChannel.method.microSeconds(); 180 | var msgObj = { 181 | time: time, 182 | type: type, 183 | data: msg 184 | }; 185 | var awaitPrepare = broadcastChannel._prepP ? broadcastChannel._prepP : PROMISE_RESOLVED_VOID; 186 | return awaitPrepare.then(function () { 187 | var sendPromise = broadcastChannel.method.postMessage(broadcastChannel._state, msgObj); 188 | 189 | // add/remove to unsent messages list 190 | broadcastChannel._uMP.add(sendPromise); 191 | sendPromise["catch"]().then(function () { 192 | return broadcastChannel._uMP["delete"](sendPromise); 193 | }); 194 | return sendPromise; 195 | }); 196 | } 197 | function _prepareChannel(channel) { 198 | var maybePromise = channel.method.create(channel.name, channel.options); 199 | if (isPromise(maybePromise)) { 200 | channel._prepP = maybePromise; 201 | maybePromise.then(function (s) { 202 | // used in tests to simulate slow runtime 203 | /*if (channel.options.prepareDelay) { 204 | await new Promise(res => setTimeout(res, this.options.prepareDelay)); 205 | }*/ 206 | channel._state = s; 207 | }); 208 | } else { 209 | channel._state = maybePromise; 210 | } 211 | } 212 | function _hasMessageListeners(channel) { 213 | if (channel._addEL.message.length > 0) return true; 214 | if (channel._addEL.internal.length > 0) return true; 215 | return false; 216 | } 217 | function _addListenerObject(channel, type, obj) { 218 | channel._addEL[type].push(obj); 219 | _startListening(channel); 220 | } 221 | function _removeListenerObject(channel, type, obj) { 222 | channel._addEL[type] = channel._addEL[type].filter(function (o) { 223 | return o !== obj; 224 | }); 225 | _stopListening(channel); 226 | } 227 | function _startListening(channel) { 228 | if (!channel._iL && _hasMessageListeners(channel)) { 229 | // someone is listening, start subscribing 230 | 231 | var listenerFn = function listenerFn(msgObj) { 232 | channel._addEL[msgObj.type].forEach(function (listenerObject) { 233 | if (msgObj.time >= listenerObject.time) { 234 | listenerObject.fn(msgObj.data); 235 | } 236 | }); 237 | }; 238 | var time = channel.method.microSeconds(); 239 | if (channel._prepP) { 240 | channel._prepP.then(function () { 241 | channel._iL = true; 242 | channel.method.onMessage(channel._state, listenerFn, time); 243 | }); 244 | } else { 245 | channel._iL = true; 246 | channel.method.onMessage(channel._state, listenerFn, time); 247 | } 248 | } 249 | } 250 | function _stopListening(channel) { 251 | if (channel._iL && !_hasMessageListeners(channel)) { 252 | // no one is listening, stop subscribing 253 | channel._iL = false; 254 | var time = channel.method.microSeconds(); 255 | channel.method.onMessage(channel._state, null, time); 256 | } 257 | } -------------------------------------------------------------------------------- /dist/esnode/browserify.index.js: -------------------------------------------------------------------------------- 1 | var module = require('./index.es5.js'); 2 | var BroadcastChannel = module.BroadcastChannel; 3 | var createLeaderElection = module.createLeaderElection; 4 | window['BroadcastChannel2'] = BroadcastChannel; 5 | window['createLeaderElection'] = createLeaderElection; -------------------------------------------------------------------------------- /dist/esnode/index.es5.js: -------------------------------------------------------------------------------- 1 | /** 2 | * because babel can only export on default-attribute, 3 | * we use this for the non-module-build 4 | * this ensures that users do not have to use 5 | * var BroadcastChannel = require('broadcast-channel').default; 6 | * but 7 | * var BroadcastChannel = require('broadcast-channel'); 8 | */ 9 | 10 | import { BroadcastChannel, createLeaderElection, clearNodeFolder, enforceOptions, beLeader } from './index.js'; 11 | module.exports = { 12 | BroadcastChannel: BroadcastChannel, 13 | createLeaderElection: createLeaderElection, 14 | clearNodeFolder: clearNodeFolder, 15 | enforceOptions: enforceOptions, 16 | beLeader: beLeader 17 | }; -------------------------------------------------------------------------------- /dist/esnode/index.js: -------------------------------------------------------------------------------- 1 | export { BroadcastChannel, clearNodeFolder, enforceOptions, OPEN_BROADCAST_CHANNELS } from './broadcast-channel.js'; 2 | export { createLeaderElection } from './leader-election.js'; 3 | export { beLeader } from './leader-election-util.js'; -------------------------------------------------------------------------------- /dist/esnode/leader-election-util.js: -------------------------------------------------------------------------------- 1 | import { add as unloadAdd } from 'unload'; 2 | 3 | /** 4 | * sends and internal message over the broadcast-channel 5 | */ 6 | export function sendLeaderMessage(leaderElector, action) { 7 | var msgJson = { 8 | context: 'leader', 9 | action: action, 10 | token: leaderElector.token 11 | }; 12 | return leaderElector.broadcastChannel.postInternal(msgJson); 13 | } 14 | export function beLeader(leaderElector) { 15 | leaderElector.isLeader = true; 16 | leaderElector._hasLeader = true; 17 | var unloadFn = unloadAdd(function () { 18 | return leaderElector.die(); 19 | }); 20 | leaderElector._unl.push(unloadFn); 21 | var isLeaderListener = function isLeaderListener(msg) { 22 | if (msg.context === 'leader' && msg.action === 'apply') { 23 | sendLeaderMessage(leaderElector, 'tell'); 24 | } 25 | if (msg.context === 'leader' && msg.action === 'tell' && !leaderElector._dpLC) { 26 | /** 27 | * another instance is also leader! 28 | * This can happen on rare events 29 | * like when the CPU is at 100% for long time 30 | * or the tabs are open very long and the browser throttles them. 31 | * @link https://github.com/pubkey/broadcast-channel/issues/414 32 | * @link https://github.com/pubkey/broadcast-channel/issues/385 33 | */ 34 | leaderElector._dpLC = true; 35 | leaderElector._dpL(); // message the lib user so the app can handle the problem 36 | sendLeaderMessage(leaderElector, 'tell'); // ensure other leader also knows the problem 37 | } 38 | }; 39 | leaderElector.broadcastChannel.addEventListener('internal', isLeaderListener); 40 | leaderElector._lstns.push(isLeaderListener); 41 | return sendLeaderMessage(leaderElector, 'tell'); 42 | } -------------------------------------------------------------------------------- /dist/esnode/leader-election-web-lock.js: -------------------------------------------------------------------------------- 1 | import { randomToken } from './util.js'; 2 | import { sendLeaderMessage, beLeader } from './leader-election-util.js'; 3 | 4 | /** 5 | * A faster version of the leader elector that uses the WebLock API 6 | * @link https://developer.mozilla.org/en-US/docs/Web/API/Web_Locks_API 7 | */ 8 | export var LeaderElectionWebLock = function LeaderElectionWebLock(broadcastChannel, options) { 9 | var _this = this; 10 | this.broadcastChannel = broadcastChannel; 11 | broadcastChannel._befC.push(function () { 12 | return _this.die(); 13 | }); 14 | this._options = options; 15 | this.isLeader = false; 16 | this.isDead = false; 17 | this.token = randomToken(); 18 | this._lstns = []; 19 | this._unl = []; 20 | this._dpL = function () {}; // onduplicate listener 21 | this._dpLC = false; // true when onduplicate called 22 | 23 | this._wKMC = {}; // stuff for cleanup 24 | 25 | // lock name 26 | this.lN = 'pubkey-bc||' + broadcastChannel.method.type + '||' + broadcastChannel.name; 27 | }; 28 | LeaderElectionWebLock.prototype = { 29 | hasLeader: function hasLeader() { 30 | var _this2 = this; 31 | return navigator.locks.query().then(function (locks) { 32 | var relevantLocks = locks.held ? locks.held.filter(function (lock) { 33 | return lock.name === _this2.lN; 34 | }) : []; 35 | if (relevantLocks && relevantLocks.length > 0) { 36 | return true; 37 | } else { 38 | return false; 39 | } 40 | }); 41 | }, 42 | awaitLeadership: function awaitLeadership() { 43 | var _this3 = this; 44 | if (!this._wLMP) { 45 | this._wKMC.c = new AbortController(); 46 | var returnPromise = new Promise(function (res, rej) { 47 | _this3._wKMC.res = res; 48 | _this3._wKMC.rej = rej; 49 | }); 50 | this._wLMP = new Promise(function (res) { 51 | navigator.locks.request(_this3.lN, { 52 | signal: _this3._wKMC.c.signal 53 | }, function () { 54 | // if the lock resolved, we can drop the abort controller 55 | _this3._wKMC.c = undefined; 56 | beLeader(_this3); 57 | res(); 58 | return returnPromise; 59 | })["catch"](function () {}); 60 | }); 61 | } 62 | return this._wLMP; 63 | }, 64 | set onduplicate(_fn) { 65 | // Do nothing because there are no duplicates in the WebLock version 66 | }, 67 | die: function die() { 68 | var _this4 = this; 69 | this._lstns.forEach(function (listener) { 70 | return _this4.broadcastChannel.removeEventListener('internal', listener); 71 | }); 72 | this._lstns = []; 73 | this._unl.forEach(function (uFn) { 74 | return uFn.remove(); 75 | }); 76 | this._unl = []; 77 | if (this.isLeader) { 78 | this.isLeader = false; 79 | } 80 | this.isDead = true; 81 | if (this._wKMC.res) { 82 | this._wKMC.res(); 83 | } 84 | if (this._wKMC.c) { 85 | this._wKMC.c.abort('LeaderElectionWebLock.die() called'); 86 | } 87 | return sendLeaderMessage(this, 'death'); 88 | } 89 | }; -------------------------------------------------------------------------------- /dist/esnode/method-chooser.js: -------------------------------------------------------------------------------- 1 | import { NativeMethod } from './methods/native.js'; 2 | import { IndexedDBMethod } from './methods/indexed-db.js'; 3 | import { LocalstorageMethod } from './methods/localstorage.js'; 4 | import { SimulateMethod } from './methods/simulate.js'; 5 | // the line below will be removed from es5/browser builds 6 | import * as NodeMethod from './methods/node.js'; 7 | 8 | // order is important 9 | var METHODS = [NativeMethod, 10 | // fastest 11 | IndexedDBMethod, LocalstorageMethod]; 12 | export function chooseMethod(options) { 13 | var chooseMethods = [].concat(options.methods, METHODS).filter(Boolean); 14 | 15 | // the line below will be removed from es5/browser builds 16 | chooseMethods.push(NodeMethod); 17 | 18 | // directly chosen 19 | if (options.type) { 20 | if (options.type === 'simulate') { 21 | // only use simulate-method if directly chosen 22 | return SimulateMethod; 23 | } 24 | var ret = chooseMethods.find(function (m) { 25 | return m.type === options.type; 26 | }); 27 | if (!ret) throw new Error('method-type ' + options.type + ' not found');else return ret; 28 | } 29 | 30 | /** 31 | * if no webworker support is needed, 32 | * remove idb from the list so that localstorage will be chosen 33 | */ 34 | if (!options.webWorkerSupport) { 35 | chooseMethods = chooseMethods.filter(function (m) { 36 | return m.type !== 'idb'; 37 | }); 38 | } 39 | var useMethod = chooseMethods.find(function (method) { 40 | return method.canBeUsed(); 41 | }); 42 | if (!useMethod) { 43 | throw new Error("No usable method found in " + JSON.stringify(METHODS.map(function (m) { 44 | return m.type; 45 | }))); 46 | } else { 47 | return useMethod; 48 | } 49 | } -------------------------------------------------------------------------------- /dist/esnode/methods/cookies.js: -------------------------------------------------------------------------------- 1 | /** 2 | * if you really need this method, 3 | * implement it! 4 | */ -------------------------------------------------------------------------------- /dist/esnode/methods/localstorage.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A localStorage-only method which uses localstorage and its 'storage'-event 3 | * This does not work inside webworkers because they have no access to localstorage 4 | * This is basically implemented to support IE9 or your grandmother's toaster. 5 | * @link https://caniuse.com/#feat=namevalue-storage 6 | * @link https://caniuse.com/#feat=indexeddb 7 | */ 8 | 9 | import { ObliviousSet } from 'oblivious-set'; 10 | import { fillOptionsWithDefaults } from '../options.js'; 11 | import { sleep, randomToken, microSeconds as micro } from '../util.js'; 12 | export var microSeconds = micro; 13 | var KEY_PREFIX = 'pubkey.broadcastChannel-'; 14 | export var type = 'localstorage'; 15 | 16 | /** 17 | * copied from crosstab 18 | * @link https://github.com/tejacques/crosstab/blob/master/src/crosstab.js#L32 19 | */ 20 | export function getLocalStorage() { 21 | var localStorage; 22 | if (typeof window === 'undefined') return null; 23 | try { 24 | localStorage = window.localStorage; 25 | localStorage = window['ie8-eventlistener/storage'] || window.localStorage; 26 | } catch (e) { 27 | // New versions of Firefox throw a Security exception 28 | // if cookies are disabled. See 29 | // https://bugzilla.mozilla.org/show_bug.cgi?id=1028153 30 | } 31 | return localStorage; 32 | } 33 | export function storageKey(channelName) { 34 | return KEY_PREFIX + channelName; 35 | } 36 | 37 | /** 38 | * writes the new message to the storage 39 | * and fires the storage-event so other readers can find it 40 | */ 41 | export function postMessage(channelState, messageJson) { 42 | return new Promise(function (res) { 43 | sleep().then(function () { 44 | var key = storageKey(channelState.channelName); 45 | var writeObj = { 46 | token: randomToken(), 47 | time: Date.now(), 48 | data: messageJson, 49 | uuid: channelState.uuid 50 | }; 51 | var value = JSON.stringify(writeObj); 52 | getLocalStorage().setItem(key, value); 53 | 54 | /** 55 | * StorageEvent does not fire the 'storage' event 56 | * in the window that changes the state of the local storage. 57 | * So we fire it manually 58 | */ 59 | var ev = document.createEvent('Event'); 60 | ev.initEvent('storage', true, true); 61 | ev.key = key; 62 | ev.newValue = value; 63 | window.dispatchEvent(ev); 64 | res(); 65 | }); 66 | }); 67 | } 68 | export function addStorageEventListener(channelName, fn) { 69 | var key = storageKey(channelName); 70 | var listener = function listener(ev) { 71 | if (ev.key === key) { 72 | fn(JSON.parse(ev.newValue)); 73 | } 74 | }; 75 | window.addEventListener('storage', listener); 76 | return listener; 77 | } 78 | export function removeStorageEventListener(listener) { 79 | window.removeEventListener('storage', listener); 80 | } 81 | export function create(channelName, options) { 82 | options = fillOptionsWithDefaults(options); 83 | if (!canBeUsed()) { 84 | throw new Error('BroadcastChannel: localstorage cannot be used'); 85 | } 86 | var uuid = randomToken(); 87 | 88 | /** 89 | * eMIs 90 | * contains all messages that have been emitted before 91 | * @type {ObliviousSet} 92 | */ 93 | var eMIs = new ObliviousSet(options.localstorage.removeTimeout); 94 | var state = { 95 | channelName: channelName, 96 | uuid: uuid, 97 | eMIs: eMIs // emittedMessagesIds 98 | }; 99 | state.listener = addStorageEventListener(channelName, function (msgObj) { 100 | if (!state.messagesCallback) return; // no listener 101 | if (msgObj.uuid === uuid) return; // own message 102 | if (!msgObj.token || eMIs.has(msgObj.token)) return; // already emitted 103 | if (msgObj.data.time && msgObj.data.time < state.messagesCallbackTime) return; // too old 104 | 105 | eMIs.add(msgObj.token); 106 | state.messagesCallback(msgObj.data); 107 | }); 108 | return state; 109 | } 110 | export function close(channelState) { 111 | removeStorageEventListener(channelState.listener); 112 | } 113 | export function onMessage(channelState, fn, time) { 114 | channelState.messagesCallbackTime = time; 115 | channelState.messagesCallback = fn; 116 | } 117 | export function canBeUsed() { 118 | var ls = getLocalStorage(); 119 | if (!ls) return false; 120 | try { 121 | var key = '__broadcastchannel_check'; 122 | ls.setItem(key, 'works'); 123 | ls.removeItem(key); 124 | } catch (e) { 125 | // Safari 10 in private mode will not allow write access to local 126 | // storage and fail with a QuotaExceededError. See 127 | // https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API#Private_Browsing_Incognito_modes 128 | return false; 129 | } 130 | return true; 131 | } 132 | export function averageResponseTime() { 133 | var defaultTime = 120; 134 | var userAgent = navigator.userAgent.toLowerCase(); 135 | if (userAgent.includes('safari') && !userAgent.includes('chrome')) { 136 | // safari is much slower so this time is higher 137 | return defaultTime * 2; 138 | } 139 | return defaultTime; 140 | } 141 | export var LocalstorageMethod = { 142 | create: create, 143 | close: close, 144 | onMessage: onMessage, 145 | postMessage: postMessage, 146 | canBeUsed: canBeUsed, 147 | type: type, 148 | averageResponseTime: averageResponseTime, 149 | microSeconds: microSeconds 150 | }; -------------------------------------------------------------------------------- /dist/esnode/methods/native.js: -------------------------------------------------------------------------------- 1 | import { microSeconds as micro, PROMISE_RESOLVED_VOID } from '../util.js'; 2 | export var microSeconds = micro; 3 | export var type = 'native'; 4 | export function create(channelName) { 5 | var state = { 6 | time: micro(), 7 | messagesCallback: null, 8 | bc: new BroadcastChannel(channelName), 9 | subFns: [] // subscriberFunctions 10 | }; 11 | state.bc.onmessage = function (msgEvent) { 12 | if (state.messagesCallback) { 13 | state.messagesCallback(msgEvent.data); 14 | } 15 | }; 16 | return state; 17 | } 18 | export function close(channelState) { 19 | channelState.bc.close(); 20 | channelState.subFns = []; 21 | } 22 | export function postMessage(channelState, messageJson) { 23 | try { 24 | channelState.bc.postMessage(messageJson, false); 25 | return PROMISE_RESOLVED_VOID; 26 | } catch (err) { 27 | return Promise.reject(err); 28 | } 29 | } 30 | export function onMessage(channelState, fn) { 31 | channelState.messagesCallback = fn; 32 | } 33 | export function canBeUsed() { 34 | // Deno runtime 35 | // eslint-disable-next-line 36 | if (typeof globalThis !== 'undefined' && globalThis.Deno && globalThis.Deno.args) { 37 | return true; 38 | } 39 | 40 | // Browser runtime 41 | if ((typeof window !== 'undefined' || typeof self !== 'undefined') && typeof BroadcastChannel === 'function') { 42 | if (BroadcastChannel._pubkey) { 43 | throw new Error('BroadcastChannel: Do not overwrite window.BroadcastChannel with this module, this is not a polyfill'); 44 | } 45 | return true; 46 | } else { 47 | return false; 48 | } 49 | } 50 | export function averageResponseTime() { 51 | return 150; 52 | } 53 | export var NativeMethod = { 54 | create: create, 55 | close: close, 56 | onMessage: onMessage, 57 | postMessage: postMessage, 58 | canBeUsed: canBeUsed, 59 | type: type, 60 | averageResponseTime: averageResponseTime, 61 | microSeconds: microSeconds 62 | }; -------------------------------------------------------------------------------- /dist/esnode/methods/simulate.js: -------------------------------------------------------------------------------- 1 | import { microSeconds as micro } from '../util.js'; 2 | export var microSeconds = micro; 3 | export var type = 'simulate'; 4 | var SIMULATE_CHANNELS = new Set(); 5 | export function create(channelName) { 6 | var state = { 7 | time: microSeconds(), 8 | name: channelName, 9 | messagesCallback: null 10 | }; 11 | SIMULATE_CHANNELS.add(state); 12 | return state; 13 | } 14 | export function close(channelState) { 15 | SIMULATE_CHANNELS["delete"](channelState); 16 | } 17 | export var SIMULATE_DELAY_TIME = 5; 18 | export function postMessage(channelState, messageJson) { 19 | return new Promise(function (res) { 20 | return setTimeout(function () { 21 | var channelArray = Array.from(SIMULATE_CHANNELS); 22 | channelArray.forEach(function (channel) { 23 | if (channel.name === channelState.name && 24 | // has same name 25 | channel !== channelState && 26 | // not own channel 27 | !!channel.messagesCallback && 28 | // has subscribers 29 | channel.time < messageJson.time // channel not created after postMessage() call 30 | ) { 31 | channel.messagesCallback(messageJson); 32 | } 33 | }); 34 | res(); 35 | }, SIMULATE_DELAY_TIME); 36 | }); 37 | } 38 | export function onMessage(channelState, fn) { 39 | channelState.messagesCallback = fn; 40 | } 41 | export function canBeUsed() { 42 | return true; 43 | } 44 | export function averageResponseTime() { 45 | return SIMULATE_DELAY_TIME; 46 | } 47 | export var SimulateMethod = { 48 | create: create, 49 | close: close, 50 | onMessage: onMessage, 51 | postMessage: postMessage, 52 | canBeUsed: canBeUsed, 53 | type: type, 54 | averageResponseTime: averageResponseTime, 55 | microSeconds: microSeconds 56 | }; -------------------------------------------------------------------------------- /dist/esnode/options.js: -------------------------------------------------------------------------------- 1 | export function fillOptionsWithDefaults() { 2 | var originalOptions = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; 3 | var options = JSON.parse(JSON.stringify(originalOptions)); 4 | 5 | // main 6 | if (typeof options.webWorkerSupport === 'undefined') options.webWorkerSupport = true; 7 | 8 | // indexed-db 9 | if (!options.idb) options.idb = {}; 10 | // after this time the messages get deleted 11 | if (!options.idb.ttl) options.idb.ttl = 1000 * 45; 12 | if (!options.idb.fallbackInterval) options.idb.fallbackInterval = 150; 13 | // handles abrupt db onclose events. 14 | if (originalOptions.idb && typeof originalOptions.idb.onclose === 'function') options.idb.onclose = originalOptions.idb.onclose; 15 | 16 | // localstorage 17 | if (!options.localstorage) options.localstorage = {}; 18 | if (!options.localstorage.removeTimeout) options.localstorage.removeTimeout = 1000 * 60; 19 | 20 | // custom methods 21 | if (originalOptions.methods) options.methods = originalOptions.methods; 22 | 23 | // node 24 | if (!options.node) options.node = {}; 25 | if (!options.node.ttl) options.node.ttl = 1000 * 60 * 2; // 2 minutes; 26 | /** 27 | * On linux use 'ulimit -Hn' to get the limit of open files. 28 | * On ubuntu this was 4096 for me, so we use half of that as maxParallelWrites default. 29 | */ 30 | if (!options.node.maxParallelWrites) options.node.maxParallelWrites = 2048; 31 | if (typeof options.node.useFastPath === 'undefined') options.node.useFastPath = true; 32 | return options; 33 | } -------------------------------------------------------------------------------- /dist/esnode/package.json: -------------------------------------------------------------------------------- 1 | { "type": "module", "sideEffects": false } 2 | -------------------------------------------------------------------------------- /dist/esnode/util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * returns true if the given object is a promise 3 | */ 4 | export function isPromise(obj) { 5 | return obj && typeof obj.then === 'function'; 6 | } 7 | export var PROMISE_RESOLVED_FALSE = Promise.resolve(false); 8 | export var PROMISE_RESOLVED_TRUE = Promise.resolve(true); 9 | export var PROMISE_RESOLVED_VOID = Promise.resolve(); 10 | export function sleep(time, resolveWith) { 11 | if (!time) time = 0; 12 | return new Promise(function (res) { 13 | return setTimeout(function () { 14 | return res(resolveWith); 15 | }, time); 16 | }); 17 | } 18 | export function randomInt(min, max) { 19 | return Math.floor(Math.random() * (max - min + 1) + min); 20 | } 21 | 22 | /** 23 | * https://stackoverflow.com/a/8084248 24 | */ 25 | export function randomToken() { 26 | return Math.random().toString(36).substring(2); 27 | } 28 | var lastMs = 0; 29 | 30 | /** 31 | * Returns the current unix time in micro-seconds, 32 | * WARNING: This is a pseudo-function 33 | * Performance.now is not reliable in webworkers, so we just make sure to never return the same time. 34 | * This is enough in browsers, and this function will not be used in nodejs. 35 | * The main reason for this hack is to ensure that BroadcastChannel behaves equal to production when it is used in fast-running unit tests. 36 | */ 37 | export function microSeconds() { 38 | var ret = Date.now() * 1000; // milliseconds to microseconds 39 | if (ret <= lastMs) { 40 | ret = lastMs + 1; 41 | } 42 | lastMs = ret; 43 | return ret; 44 | } 45 | 46 | /** 47 | * Check if WebLock API is supported. 48 | * @link https://developer.mozilla.org/en-US/docs/Web/API/Web_Locks_API 49 | */ 50 | export function supportsWebLockAPI() { 51 | if (typeof navigator !== 'undefined' && typeof navigator.locks !== 'undefined' && typeof navigator.locks.request === 'function') { 52 | return true; 53 | } else { 54 | return false; 55 | } 56 | } -------------------------------------------------------------------------------- /dist/lib/browserify.index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _module = require('./index.es5.js'); 4 | var BroadcastChannel = _module.BroadcastChannel; 5 | var createLeaderElection = _module.createLeaderElection; 6 | window['BroadcastChannel2'] = BroadcastChannel; 7 | window['createLeaderElection'] = createLeaderElection; -------------------------------------------------------------------------------- /dist/lib/index.es5.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _index = require("./index.js"); 4 | /** 5 | * because babel can only export on default-attribute, 6 | * we use this for the non-module-build 7 | * this ensures that users do not have to use 8 | * var BroadcastChannel = require('broadcast-channel').default; 9 | * but 10 | * var BroadcastChannel = require('broadcast-channel'); 11 | */ 12 | 13 | module.exports = { 14 | BroadcastChannel: _index.BroadcastChannel, 15 | createLeaderElection: _index.createLeaderElection, 16 | clearNodeFolder: _index.clearNodeFolder, 17 | enforceOptions: _index.enforceOptions, 18 | beLeader: _index.beLeader 19 | }; -------------------------------------------------------------------------------- /dist/lib/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | Object.defineProperty(exports, "BroadcastChannel", { 7 | enumerable: true, 8 | get: function get() { 9 | return _broadcastChannel.BroadcastChannel; 10 | } 11 | }); 12 | Object.defineProperty(exports, "OPEN_BROADCAST_CHANNELS", { 13 | enumerable: true, 14 | get: function get() { 15 | return _broadcastChannel.OPEN_BROADCAST_CHANNELS; 16 | } 17 | }); 18 | Object.defineProperty(exports, "beLeader", { 19 | enumerable: true, 20 | get: function get() { 21 | return _leaderElectionUtil.beLeader; 22 | } 23 | }); 24 | Object.defineProperty(exports, "clearNodeFolder", { 25 | enumerable: true, 26 | get: function get() { 27 | return _broadcastChannel.clearNodeFolder; 28 | } 29 | }); 30 | Object.defineProperty(exports, "createLeaderElection", { 31 | enumerable: true, 32 | get: function get() { 33 | return _leaderElection.createLeaderElection; 34 | } 35 | }); 36 | Object.defineProperty(exports, "enforceOptions", { 37 | enumerable: true, 38 | get: function get() { 39 | return _broadcastChannel.enforceOptions; 40 | } 41 | }); 42 | var _broadcastChannel = require("./broadcast-channel.js"); 43 | var _leaderElection = require("./leader-election.js"); 44 | var _leaderElectionUtil = require("./leader-election-util.js"); -------------------------------------------------------------------------------- /dist/lib/leader-election-util.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.beLeader = beLeader; 7 | exports.sendLeaderMessage = sendLeaderMessage; 8 | var _unload = require("unload"); 9 | /** 10 | * sends and internal message over the broadcast-channel 11 | */ 12 | function sendLeaderMessage(leaderElector, action) { 13 | var msgJson = { 14 | context: 'leader', 15 | action: action, 16 | token: leaderElector.token 17 | }; 18 | return leaderElector.broadcastChannel.postInternal(msgJson); 19 | } 20 | function beLeader(leaderElector) { 21 | leaderElector.isLeader = true; 22 | leaderElector._hasLeader = true; 23 | var unloadFn = (0, _unload.add)(function () { 24 | return leaderElector.die(); 25 | }); 26 | leaderElector._unl.push(unloadFn); 27 | var isLeaderListener = function isLeaderListener(msg) { 28 | if (msg.context === 'leader' && msg.action === 'apply') { 29 | sendLeaderMessage(leaderElector, 'tell'); 30 | } 31 | if (msg.context === 'leader' && msg.action === 'tell' && !leaderElector._dpLC) { 32 | /** 33 | * another instance is also leader! 34 | * This can happen on rare events 35 | * like when the CPU is at 100% for long time 36 | * or the tabs are open very long and the browser throttles them. 37 | * @link https://github.com/pubkey/broadcast-channel/issues/414 38 | * @link https://github.com/pubkey/broadcast-channel/issues/385 39 | */ 40 | leaderElector._dpLC = true; 41 | leaderElector._dpL(); // message the lib user so the app can handle the problem 42 | sendLeaderMessage(leaderElector, 'tell'); // ensure other leader also knows the problem 43 | } 44 | }; 45 | leaderElector.broadcastChannel.addEventListener('internal', isLeaderListener); 46 | leaderElector._lstns.push(isLeaderListener); 47 | return sendLeaderMessage(leaderElector, 'tell'); 48 | } -------------------------------------------------------------------------------- /dist/lib/leader-election-web-lock.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.LeaderElectionWebLock = void 0; 7 | var _util = require("./util.js"); 8 | var _leaderElectionUtil = require("./leader-election-util.js"); 9 | /** 10 | * A faster version of the leader elector that uses the WebLock API 11 | * @link https://developer.mozilla.org/en-US/docs/Web/API/Web_Locks_API 12 | */ 13 | var LeaderElectionWebLock = exports.LeaderElectionWebLock = function LeaderElectionWebLock(broadcastChannel, options) { 14 | var _this = this; 15 | this.broadcastChannel = broadcastChannel; 16 | broadcastChannel._befC.push(function () { 17 | return _this.die(); 18 | }); 19 | this._options = options; 20 | this.isLeader = false; 21 | this.isDead = false; 22 | this.token = (0, _util.randomToken)(); 23 | this._lstns = []; 24 | this._unl = []; 25 | this._dpL = function () {}; // onduplicate listener 26 | this._dpLC = false; // true when onduplicate called 27 | 28 | this._wKMC = {}; // stuff for cleanup 29 | 30 | // lock name 31 | this.lN = 'pubkey-bc||' + broadcastChannel.method.type + '||' + broadcastChannel.name; 32 | }; 33 | LeaderElectionWebLock.prototype = { 34 | hasLeader: function hasLeader() { 35 | var _this2 = this; 36 | return navigator.locks.query().then(function (locks) { 37 | var relevantLocks = locks.held ? locks.held.filter(function (lock) { 38 | return lock.name === _this2.lN; 39 | }) : []; 40 | if (relevantLocks && relevantLocks.length > 0) { 41 | return true; 42 | } else { 43 | return false; 44 | } 45 | }); 46 | }, 47 | awaitLeadership: function awaitLeadership() { 48 | var _this3 = this; 49 | if (!this._wLMP) { 50 | this._wKMC.c = new AbortController(); 51 | var returnPromise = new Promise(function (res, rej) { 52 | _this3._wKMC.res = res; 53 | _this3._wKMC.rej = rej; 54 | }); 55 | this._wLMP = new Promise(function (res) { 56 | navigator.locks.request(_this3.lN, { 57 | signal: _this3._wKMC.c.signal 58 | }, function () { 59 | // if the lock resolved, we can drop the abort controller 60 | _this3._wKMC.c = undefined; 61 | (0, _leaderElectionUtil.beLeader)(_this3); 62 | res(); 63 | return returnPromise; 64 | })["catch"](function () {}); 65 | }); 66 | } 67 | return this._wLMP; 68 | }, 69 | set onduplicate(_fn) { 70 | // Do nothing because there are no duplicates in the WebLock version 71 | }, 72 | die: function die() { 73 | var _this4 = this; 74 | this._lstns.forEach(function (listener) { 75 | return _this4.broadcastChannel.removeEventListener('internal', listener); 76 | }); 77 | this._lstns = []; 78 | this._unl.forEach(function (uFn) { 79 | return uFn.remove(); 80 | }); 81 | this._unl = []; 82 | if (this.isLeader) { 83 | this.isLeader = false; 84 | } 85 | this.isDead = true; 86 | if (this._wKMC.res) { 87 | this._wKMC.res(); 88 | } 89 | if (this._wKMC.c) { 90 | this._wKMC.c.abort('LeaderElectionWebLock.die() called'); 91 | } 92 | return (0, _leaderElectionUtil.sendLeaderMessage)(this, 'death'); 93 | } 94 | }; -------------------------------------------------------------------------------- /dist/lib/method-chooser.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _typeof = require("@babel/runtime/helpers/typeof"); 4 | Object.defineProperty(exports, "__esModule", { 5 | value: true 6 | }); 7 | exports.chooseMethod = chooseMethod; 8 | var _native = require("./methods/native.js"); 9 | var _indexedDb = require("./methods/indexed-db.js"); 10 | var _localstorage = require("./methods/localstorage.js"); 11 | var _simulate = require("./methods/simulate.js"); 12 | function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(e) { return e ? t : r; })(e); } 13 | function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != _typeof(e) && "function" != typeof e) return { "default": e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n["default"] = e, t && t.set(e, n), n; } 14 | // the line below will be removed from es5/browser builds 15 | 16 | // order is important 17 | var METHODS = [_native.NativeMethod, 18 | // fastest 19 | _indexedDb.IndexedDBMethod, _localstorage.LocalstorageMethod]; 20 | function chooseMethod(options) { 21 | var chooseMethods = [].concat(options.methods, METHODS).filter(Boolean); 22 | 23 | // the line below will be removed from es5/browser builds 24 | 25 | // directly chosen 26 | if (options.type) { 27 | if (options.type === 'simulate') { 28 | // only use simulate-method if directly chosen 29 | return _simulate.SimulateMethod; 30 | } 31 | var ret = chooseMethods.find(function (m) { 32 | return m.type === options.type; 33 | }); 34 | if (!ret) throw new Error('method-type ' + options.type + ' not found');else return ret; 35 | } 36 | 37 | /** 38 | * if no webworker support is needed, 39 | * remove idb from the list so that localstorage will be chosen 40 | */ 41 | if (!options.webWorkerSupport) { 42 | chooseMethods = chooseMethods.filter(function (m) { 43 | return m.type !== 'idb'; 44 | }); 45 | } 46 | var useMethod = chooseMethods.find(function (method) { 47 | return method.canBeUsed(); 48 | }); 49 | if (!useMethod) { 50 | throw new Error("No usable method found in " + JSON.stringify(METHODS.map(function (m) { 51 | return m.type; 52 | }))); 53 | } else { 54 | return useMethod; 55 | } 56 | } -------------------------------------------------------------------------------- /dist/lib/methods/cookies.js: -------------------------------------------------------------------------------- 1 | /** 2 | * if you really need this method, 3 | * implement it! 4 | */ 5 | "use strict"; -------------------------------------------------------------------------------- /dist/lib/methods/localstorage.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.LocalstorageMethod = void 0; 7 | exports.addStorageEventListener = addStorageEventListener; 8 | exports.averageResponseTime = averageResponseTime; 9 | exports.canBeUsed = canBeUsed; 10 | exports.close = close; 11 | exports.create = create; 12 | exports.getLocalStorage = getLocalStorage; 13 | exports.microSeconds = void 0; 14 | exports.onMessage = onMessage; 15 | exports.postMessage = postMessage; 16 | exports.removeStorageEventListener = removeStorageEventListener; 17 | exports.storageKey = storageKey; 18 | exports.type = void 0; 19 | var _obliviousSet = require("oblivious-set"); 20 | var _options = require("../options.js"); 21 | var _util = require("../util.js"); 22 | /** 23 | * A localStorage-only method which uses localstorage and its 'storage'-event 24 | * This does not work inside webworkers because they have no access to localstorage 25 | * This is basically implemented to support IE9 or your grandmother's toaster. 26 | * @link https://caniuse.com/#feat=namevalue-storage 27 | * @link https://caniuse.com/#feat=indexeddb 28 | */ 29 | 30 | var microSeconds = exports.microSeconds = _util.microSeconds; 31 | var KEY_PREFIX = 'pubkey.broadcastChannel-'; 32 | var type = exports.type = 'localstorage'; 33 | 34 | /** 35 | * copied from crosstab 36 | * @link https://github.com/tejacques/crosstab/blob/master/src/crosstab.js#L32 37 | */ 38 | function getLocalStorage() { 39 | var localStorage; 40 | if (typeof window === 'undefined') return null; 41 | try { 42 | localStorage = window.localStorage; 43 | localStorage = window['ie8-eventlistener/storage'] || window.localStorage; 44 | } catch (e) { 45 | // New versions of Firefox throw a Security exception 46 | // if cookies are disabled. See 47 | // https://bugzilla.mozilla.org/show_bug.cgi?id=1028153 48 | } 49 | return localStorage; 50 | } 51 | function storageKey(channelName) { 52 | return KEY_PREFIX + channelName; 53 | } 54 | 55 | /** 56 | * writes the new message to the storage 57 | * and fires the storage-event so other readers can find it 58 | */ 59 | function postMessage(channelState, messageJson) { 60 | return new Promise(function (res) { 61 | (0, _util.sleep)().then(function () { 62 | var key = storageKey(channelState.channelName); 63 | var writeObj = { 64 | token: (0, _util.randomToken)(), 65 | time: Date.now(), 66 | data: messageJson, 67 | uuid: channelState.uuid 68 | }; 69 | var value = JSON.stringify(writeObj); 70 | getLocalStorage().setItem(key, value); 71 | 72 | /** 73 | * StorageEvent does not fire the 'storage' event 74 | * in the window that changes the state of the local storage. 75 | * So we fire it manually 76 | */ 77 | var ev = document.createEvent('Event'); 78 | ev.initEvent('storage', true, true); 79 | ev.key = key; 80 | ev.newValue = value; 81 | window.dispatchEvent(ev); 82 | res(); 83 | }); 84 | }); 85 | } 86 | function addStorageEventListener(channelName, fn) { 87 | var key = storageKey(channelName); 88 | var listener = function listener(ev) { 89 | if (ev.key === key) { 90 | fn(JSON.parse(ev.newValue)); 91 | } 92 | }; 93 | window.addEventListener('storage', listener); 94 | return listener; 95 | } 96 | function removeStorageEventListener(listener) { 97 | window.removeEventListener('storage', listener); 98 | } 99 | function create(channelName, options) { 100 | options = (0, _options.fillOptionsWithDefaults)(options); 101 | if (!canBeUsed()) { 102 | throw new Error('BroadcastChannel: localstorage cannot be used'); 103 | } 104 | var uuid = (0, _util.randomToken)(); 105 | 106 | /** 107 | * eMIs 108 | * contains all messages that have been emitted before 109 | * @type {ObliviousSet} 110 | */ 111 | var eMIs = new _obliviousSet.ObliviousSet(options.localstorage.removeTimeout); 112 | var state = { 113 | channelName: channelName, 114 | uuid: uuid, 115 | eMIs: eMIs // emittedMessagesIds 116 | }; 117 | state.listener = addStorageEventListener(channelName, function (msgObj) { 118 | if (!state.messagesCallback) return; // no listener 119 | if (msgObj.uuid === uuid) return; // own message 120 | if (!msgObj.token || eMIs.has(msgObj.token)) return; // already emitted 121 | if (msgObj.data.time && msgObj.data.time < state.messagesCallbackTime) return; // too old 122 | 123 | eMIs.add(msgObj.token); 124 | state.messagesCallback(msgObj.data); 125 | }); 126 | return state; 127 | } 128 | function close(channelState) { 129 | removeStorageEventListener(channelState.listener); 130 | } 131 | function onMessage(channelState, fn, time) { 132 | channelState.messagesCallbackTime = time; 133 | channelState.messagesCallback = fn; 134 | } 135 | function canBeUsed() { 136 | var ls = getLocalStorage(); 137 | if (!ls) return false; 138 | try { 139 | var key = '__broadcastchannel_check'; 140 | ls.setItem(key, 'works'); 141 | ls.removeItem(key); 142 | } catch (e) { 143 | // Safari 10 in private mode will not allow write access to local 144 | // storage and fail with a QuotaExceededError. See 145 | // https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API#Private_Browsing_Incognito_modes 146 | return false; 147 | } 148 | return true; 149 | } 150 | function averageResponseTime() { 151 | var defaultTime = 120; 152 | var userAgent = navigator.userAgent.toLowerCase(); 153 | if (userAgent.includes('safari') && !userAgent.includes('chrome')) { 154 | // safari is much slower so this time is higher 155 | return defaultTime * 2; 156 | } 157 | return defaultTime; 158 | } 159 | var LocalstorageMethod = exports.LocalstorageMethod = { 160 | create: create, 161 | close: close, 162 | onMessage: onMessage, 163 | postMessage: postMessage, 164 | canBeUsed: canBeUsed, 165 | type: type, 166 | averageResponseTime: averageResponseTime, 167 | microSeconds: microSeconds 168 | }; -------------------------------------------------------------------------------- /dist/lib/methods/native.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.NativeMethod = void 0; 7 | exports.averageResponseTime = averageResponseTime; 8 | exports.canBeUsed = canBeUsed; 9 | exports.close = close; 10 | exports.create = create; 11 | exports.microSeconds = void 0; 12 | exports.onMessage = onMessage; 13 | exports.postMessage = postMessage; 14 | exports.type = void 0; 15 | var _util = require("../util.js"); 16 | var microSeconds = exports.microSeconds = _util.microSeconds; 17 | var type = exports.type = 'native'; 18 | function create(channelName) { 19 | var state = { 20 | time: (0, _util.microSeconds)(), 21 | messagesCallback: null, 22 | bc: new BroadcastChannel(channelName), 23 | subFns: [] // subscriberFunctions 24 | }; 25 | state.bc.onmessage = function (msgEvent) { 26 | if (state.messagesCallback) { 27 | state.messagesCallback(msgEvent.data); 28 | } 29 | }; 30 | return state; 31 | } 32 | function close(channelState) { 33 | channelState.bc.close(); 34 | channelState.subFns = []; 35 | } 36 | function postMessage(channelState, messageJson) { 37 | try { 38 | channelState.bc.postMessage(messageJson, false); 39 | return _util.PROMISE_RESOLVED_VOID; 40 | } catch (err) { 41 | return Promise.reject(err); 42 | } 43 | } 44 | function onMessage(channelState, fn) { 45 | channelState.messagesCallback = fn; 46 | } 47 | function canBeUsed() { 48 | // Deno runtime 49 | // eslint-disable-next-line 50 | if (typeof globalThis !== 'undefined' && globalThis.Deno && globalThis.Deno.args) { 51 | return true; 52 | } 53 | 54 | // Browser runtime 55 | if ((typeof window !== 'undefined' || typeof self !== 'undefined') && typeof BroadcastChannel === 'function') { 56 | if (BroadcastChannel._pubkey) { 57 | throw new Error('BroadcastChannel: Do not overwrite window.BroadcastChannel with this module, this is not a polyfill'); 58 | } 59 | return true; 60 | } else { 61 | return false; 62 | } 63 | } 64 | function averageResponseTime() { 65 | return 150; 66 | } 67 | var NativeMethod = exports.NativeMethod = { 68 | create: create, 69 | close: close, 70 | onMessage: onMessage, 71 | postMessage: postMessage, 72 | canBeUsed: canBeUsed, 73 | type: type, 74 | averageResponseTime: averageResponseTime, 75 | microSeconds: microSeconds 76 | }; -------------------------------------------------------------------------------- /dist/lib/methods/simulate.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.SimulateMethod = exports.SIMULATE_DELAY_TIME = void 0; 7 | exports.averageResponseTime = averageResponseTime; 8 | exports.canBeUsed = canBeUsed; 9 | exports.close = close; 10 | exports.create = create; 11 | exports.microSeconds = void 0; 12 | exports.onMessage = onMessage; 13 | exports.postMessage = postMessage; 14 | exports.type = void 0; 15 | var _util = require("../util.js"); 16 | var microSeconds = exports.microSeconds = _util.microSeconds; 17 | var type = exports.type = 'simulate'; 18 | var SIMULATE_CHANNELS = new Set(); 19 | function create(channelName) { 20 | var state = { 21 | time: microSeconds(), 22 | name: channelName, 23 | messagesCallback: null 24 | }; 25 | SIMULATE_CHANNELS.add(state); 26 | return state; 27 | } 28 | function close(channelState) { 29 | SIMULATE_CHANNELS["delete"](channelState); 30 | } 31 | var SIMULATE_DELAY_TIME = exports.SIMULATE_DELAY_TIME = 5; 32 | function postMessage(channelState, messageJson) { 33 | return new Promise(function (res) { 34 | return setTimeout(function () { 35 | var channelArray = Array.from(SIMULATE_CHANNELS); 36 | channelArray.forEach(function (channel) { 37 | if (channel.name === channelState.name && 38 | // has same name 39 | channel !== channelState && 40 | // not own channel 41 | !!channel.messagesCallback && 42 | // has subscribers 43 | channel.time < messageJson.time // channel not created after postMessage() call 44 | ) { 45 | channel.messagesCallback(messageJson); 46 | } 47 | }); 48 | res(); 49 | }, SIMULATE_DELAY_TIME); 50 | }); 51 | } 52 | function onMessage(channelState, fn) { 53 | channelState.messagesCallback = fn; 54 | } 55 | function canBeUsed() { 56 | return true; 57 | } 58 | function averageResponseTime() { 59 | return SIMULATE_DELAY_TIME; 60 | } 61 | var SimulateMethod = exports.SimulateMethod = { 62 | create: create, 63 | close: close, 64 | onMessage: onMessage, 65 | postMessage: postMessage, 66 | canBeUsed: canBeUsed, 67 | type: type, 68 | averageResponseTime: averageResponseTime, 69 | microSeconds: microSeconds 70 | }; -------------------------------------------------------------------------------- /dist/lib/options.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.fillOptionsWithDefaults = fillOptionsWithDefaults; 7 | function fillOptionsWithDefaults() { 8 | var originalOptions = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; 9 | var options = JSON.parse(JSON.stringify(originalOptions)); 10 | 11 | // main 12 | if (typeof options.webWorkerSupport === 'undefined') options.webWorkerSupport = true; 13 | 14 | // indexed-db 15 | if (!options.idb) options.idb = {}; 16 | // after this time the messages get deleted 17 | if (!options.idb.ttl) options.idb.ttl = 1000 * 45; 18 | if (!options.idb.fallbackInterval) options.idb.fallbackInterval = 150; 19 | // handles abrupt db onclose events. 20 | if (originalOptions.idb && typeof originalOptions.idb.onclose === 'function') options.idb.onclose = originalOptions.idb.onclose; 21 | 22 | // localstorage 23 | if (!options.localstorage) options.localstorage = {}; 24 | if (!options.localstorage.removeTimeout) options.localstorage.removeTimeout = 1000 * 60; 25 | 26 | // custom methods 27 | if (originalOptions.methods) options.methods = originalOptions.methods; 28 | 29 | // node 30 | if (!options.node) options.node = {}; 31 | if (!options.node.ttl) options.node.ttl = 1000 * 60 * 2; // 2 minutes; 32 | /** 33 | * On linux use 'ulimit -Hn' to get the limit of open files. 34 | * On ubuntu this was 4096 for me, so we use half of that as maxParallelWrites default. 35 | */ 36 | if (!options.node.maxParallelWrites) options.node.maxParallelWrites = 2048; 37 | if (typeof options.node.useFastPath === 'undefined') options.node.useFastPath = true; 38 | return options; 39 | } -------------------------------------------------------------------------------- /dist/lib/util.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.PROMISE_RESOLVED_VOID = exports.PROMISE_RESOLVED_TRUE = exports.PROMISE_RESOLVED_FALSE = void 0; 7 | exports.isPromise = isPromise; 8 | exports.microSeconds = microSeconds; 9 | exports.randomInt = randomInt; 10 | exports.randomToken = randomToken; 11 | exports.sleep = sleep; 12 | exports.supportsWebLockAPI = supportsWebLockAPI; 13 | /** 14 | * returns true if the given object is a promise 15 | */ 16 | function isPromise(obj) { 17 | return obj && typeof obj.then === 'function'; 18 | } 19 | var PROMISE_RESOLVED_FALSE = exports.PROMISE_RESOLVED_FALSE = Promise.resolve(false); 20 | var PROMISE_RESOLVED_TRUE = exports.PROMISE_RESOLVED_TRUE = Promise.resolve(true); 21 | var PROMISE_RESOLVED_VOID = exports.PROMISE_RESOLVED_VOID = Promise.resolve(); 22 | function sleep(time, resolveWith) { 23 | if (!time) time = 0; 24 | return new Promise(function (res) { 25 | return setTimeout(function () { 26 | return res(resolveWith); 27 | }, time); 28 | }); 29 | } 30 | function randomInt(min, max) { 31 | return Math.floor(Math.random() * (max - min + 1) + min); 32 | } 33 | 34 | /** 35 | * https://stackoverflow.com/a/8084248 36 | */ 37 | function randomToken() { 38 | return Math.random().toString(36).substring(2); 39 | } 40 | var lastMs = 0; 41 | 42 | /** 43 | * Returns the current unix time in micro-seconds, 44 | * WARNING: This is a pseudo-function 45 | * Performance.now is not reliable in webworkers, so we just make sure to never return the same time. 46 | * This is enough in browsers, and this function will not be used in nodejs. 47 | * The main reason for this hack is to ensure that BroadcastChannel behaves equal to production when it is used in fast-running unit tests. 48 | */ 49 | function microSeconds() { 50 | var ret = Date.now() * 1000; // milliseconds to microseconds 51 | if (ret <= lastMs) { 52 | ret = lastMs + 1; 53 | } 54 | lastMs = ret; 55 | return ret; 56 | } 57 | 58 | /** 59 | * Check if WebLock API is supported. 60 | * @link https://developer.mozilla.org/en-US/docs/Web/API/Web_Locks_API 61 | */ 62 | function supportsWebLockAPI() { 63 | if (typeof navigator !== 'undefined' && typeof navigator.locks !== 'undefined' && typeof navigator.locks.request === 'function') { 64 | return true; 65 | } else { 66 | return false; 67 | } 68 | } -------------------------------------------------------------------------------- /docs/e2e.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 66 | 67 | 68 | 69 | 116 | 120 | Fork me on GitHub 125 | 126 | 127 |
131 |
132 |

BroadcastChannel Test

133 |

This will send a message from the main-context, await until the iframe and the web-worker answered, then 134 | repeat 135 | until all messages have been send.

136 |
137 | MessageCount: 138 | 0 139 |
140 | 144 |
148 |
149 | 150 | 151 |
156 |
157 |

LeaderElection Test

158 |

This will spawn several iframes which all want to be leader. No matter what happens, exactly one iframe 159 | should be 160 | leader. 161 |

162 |
163 |
164 | 165 |
166 |
167 | 168 |
172 |
173 |

WebWorker Test

174 |

175 | This will send a message from the main-context to the worker and wait for a response message. 176 | This runs many times with random timings to ensure there are no edge cases where messages are missing or 177 | in the wrong order. 178 |

179 |
180 | MessageCount: 181 | 0 182 |
183 | 187 |
191 |
192 | 193 | 194 | 195 | 196 | 197 | 198 | -------------------------------------------------------------------------------- /docs/files/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pubkey/broadcast-channel/4ee7fba766aea1a98de263ea5006c42d872c47f6/docs/files/demo.gif -------------------------------------------------------------------------------- /docs/files/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pubkey/broadcast-channel/4ee7fba766aea1a98de263ea5006c42d872c47f6/docs/files/icon.png -------------------------------------------------------------------------------- /docs/files/leader-election.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pubkey/broadcast-channel/4ee7fba766aea1a98de263ea5006c42d872c47f6/docs/files/leader-election.gif -------------------------------------------------------------------------------- /docs/iframe.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BroadcastChannel Demo 6 | 12 | 60 | 61 | 62 | 63 |
64 |

BroadcastChannel Demo

65 |

66 | This is the demo page for the BroadcastChannel module. 67 | Open it in multiple tabs and send messages across them. 68 |

69 | 70 | 75 | 79 | 80 |
81 |
82 |
83 |
84 |
85 |

Links

86 | Advanced Test-Page
90 | Github Repo
94 |
95 | 113 | 114 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /docs/kirby.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pubkey/broadcast-channel/4ee7fba766aea1a98de263ea5006c42d872c47f6/docs/kirby.gif -------------------------------------------------------------------------------- /docs/leader-iframe.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /perf.txt: -------------------------------------------------------------------------------- 1 | BEFORE: 2 | 3 | (clean application state), exmpty idb 4 | - intervall-timeout set to zero 5 | 2220ms 6 | 2494ms 7 | 2235ms 8 | 9 | // series run without cleaning the state 10 | 2385 11 | 4813 12 | 6079 13 | 7553 14 | 9573 15 | 16 | 17 | AFTER: 18 | (clear state) 19 | 1370 20 | 1281 21 | 1212 22 | 23 | (non clear) 24 | 1312 25 | 1314 26 | 1362 27 | 1448 28 | 1450 29 | 1397 30 | 31 | 32 | 33 | 34 | 35 | =================================== 36 | 20.06.2018 37 | =================================== 38 | IndexedDB 39 | 40 | BEFORE: 41 | 3610 42 | 3380 43 | 3489 44 | 45 | 46 | AFTER (with localstorage ping): 47 | 4183 48 | 3962 49 | 3821 50 | 51 | => sending localstorage-pings is slower 52 | 53 | 54 | 55 | 56 | ====== build-size 57 | BEFORE: 37251 58 | AFTER: 4077 59 | 60 | 61 | ====== build-size 2 62 | BEFORE: 4077 63 | AFTER: 3795 64 | 65 | 66 | ====== build-size 3 67 | BEFORE: 3795 68 | AFTER: 3110 69 | 70 | 71 | 72 | ----------------------------------------- 73 | 14.July.2018: test:performance 74 | 75 | before: { 76 | "openClose": 1589.1032320000231, 77 | "sendRecieve": { 78 | "parallel": 8576.14631400071, 79 | "series": 8902.407701000571 80 | } 81 | } 82 | 83 | 84 | after: { 85 | "openClose": 1606.3578069992363, 86 | "sendRecieve": { 87 | "parallel": 6627.974293999374, 88 | "series": 5202.203781999648 89 | } 90 | } 91 | ----------------------------------------- 92 | 93 | 94 | 95 | ----------------------------------------- 96 | test:performance 97 | 98 | BEFORE: { 99 | "openClose": 1499.9152579996735, 100 | "sendRecieve": { 101 | "parallel": 6752.695256000385, 102 | "series": 5142.3914529997855 103 | } 104 | } 105 | 106 | AFTER: { // getPathsCache 107 | "openClose": 1154.4196130000055, 108 | "sendRecieve": { 109 | "parallel": 6559.061360999942, 110 | "series": 4965.728401999921 111 | } 112 | } 113 | 114 | AFTER2: { // cleanup things 115 | "openClose": 1086.149023000151, 116 | "sendRecieve": { 117 | "parallel": 6496.672225000337, 118 | "series": 4932.777033999562 119 | } 120 | } 121 | 122 | AFTER3: { // run things in parallel 123 | "openClose": 737.8487470000982, 124 | "sendRecieve": { 125 | "parallel": 6637.516607999802, 126 | "series": 4835.849313000217 127 | } 128 | } 129 | 130 | AFTER4: { // read content in parallel 131 | "openClose": 746.0398439988494, 132 | "sendRecieve": { 133 | "parallel": 6332.704676998779, 134 | "series": 4761.053835000843 135 | } 136 | } 137 | 138 | AFTER5: { // better postMessage 139 | "openClose": 666.0222460012883, 140 | "sendRecieve": { 141 | "parallel": 5854.225347001106, 142 | "series": 4425.243154998869 143 | } 144 | } 145 | 146 | 147 | 148 | ----------------------------------------- 149 | test:performance 150 | 151 | BEFORE: { 152 | "openClose": 714.9132689982653, 153 | "sendRecieve": { 154 | "parallel": 6018.035248000175, 155 | "series": 4019.5094799995422 156 | } 157 | } 158 | 159 | AFTER: { // write message up front 160 | "openClose": 703.9341719998047, 161 | "sendRecieve": { 162 | "parallel": 233.59367400035262, 163 | "series": 4531.717969999649 164 | } 165 | } 166 | 167 | ----------------------------------------- 168 | 169 | ----------------------------------------- 170 | test:performance - forgetting set 171 | 172 | BEFORE: { 173 | "openClose": 703.9341719998047, 174 | "sendRecieve": { 175 | "parallel": 233.59367400035262, 176 | "series": 4531.717969999649 177 | } 178 | } 179 | 180 | AFTER: { // add fast path 181 | "openClose": 698.5278329998255, 182 | "sendRecieve": { 183 | "parallel": 254.588275000453, 184 | "series": 3679.5491359978914 185 | } 186 | } 187 | 188 | ----------------------------------------- 189 | no idle-queue 190 | 191 | BEFORE: { 192 | "openClose": 720.8237979999976, 193 | "sendRecieve": { 194 | "parallel": 250.95046299998648, 195 | "series": 3671.9275919999927 196 | } 197 | } 198 | 199 | AFTER: { 200 | "openClose": 684.5638470000122, 201 | "sendRecieve": { 202 | "parallel": 246.08427699981257, 203 | "series": 2251.4478739998303 204 | } 205 | } 206 | 207 | ----------------------------------------- 208 | 209 | ## no default import 210 | npm run build:size 211 | BEFORE: 112358 212 | AFTER : 112401 213 | 214 | 215 | ## only use async/await when needed 216 | BEFORE: 112401 217 | AFTER : 111582 218 | 219 | 220 | ----------------------------------------- 221 | new unload module 222 | 223 | BEFORE: { 224 | "openClose": 765.1154530011117, 225 | "sendRecieve": { 226 | "parallel": 259.89112799987197, 227 | "series": 4052.1648419983685 228 | } 229 | } 230 | 231 | AFTER: { 232 | "openClose": 672.2327830009162, 233 | "sendRecieve": { 234 | "parallel": 250.701522000134, 235 | "series": 4001.1675169989467 236 | } 237 | } 238 | 239 | ----------------------------------------- 240 | use native node-code without transpilation 241 | 242 | BEFORE: { 243 | "openClose": 733.8300310000777, 244 | "sendRecieve": { 245 | "parallel": 245.27187200076878, 246 | "series": 3865.821045000106 247 | } 248 | } 249 | 250 | AFTER: { 251 | "openClose": 577.4335329998285, 252 | "sendRecieve": { 253 | "parallel": 226.03592699952424, 254 | "series": 4163.729206999764 255 | } 256 | } 257 | 258 | 259 | ----------------------------------------- 260 | CACHE tmp-folder: 261 | BEFORE: { 262 | "openClose": 611.5843779994175, 263 | "sendRecieve": { 264 | "parallel": 229.9967109998688, 265 | "series": 4040.1850410001352 266 | } 267 | } 268 | { 269 | "openClose": 586.3366490006447, 270 | "sendRecieve": { 271 | "parallel": 237.4793659998104, 272 | "series": 3972.603798000142 273 | } 274 | } 275 | AFTER: { 276 | "openClose": 563.7609900003299, 277 | "sendRecieve": { 278 | "parallel": 233.5304539995268, 279 | "series": 3869.6750210002065 280 | } 281 | } 282 | 283 | ----------------------------------------- 284 | CACHE ensure-folder exists: 285 | BEFORE:{ 286 | "openClose": 583.2204679995775, 287 | "sendRecieve": { 288 | "parallel": 206.64538500085473, 289 | "series": 3861.489134998992 290 | } 291 | } 292 | 293 | AFTER: { 294 | "openClose": 544.0778630003333, 295 | "sendRecieve": { 296 | "parallel": 220.51885700039566, 297 | "series": 2255.5608139988035 298 | } 299 | } 300 | 301 | 302 | 303 | 4431 304 | 4425 305 | 4417 306 | 4405 307 | 4382 308 | 4376 309 | 4350 310 | 4319 311 | 312 | 313 | =================================== 314 | 13.11.2019 315 | =================================== 316 | 317 | { 318 | "openClose": 565.4872879981995, 319 | "sendRecieve": { 320 | "parallel": 181.46631100028753, 321 | "series": 2321.6348760016263 322 | } 323 | } 324 | 325 | 326 | 327 | =================================== 328 | 3.12.2021 329 | =================================== 330 | 331 | { 332 | "openClose": 1110.2557100057602, 333 | "sendRecieve": { 334 | "parallel": 279.0764960050583, 335 | "series": 2797.712993979454 336 | }, 337 | "leaderElection": 2122.0940190553665 338 | } 339 | 340 | { 341 | "openClose": 885.0331689119339, 342 | "sendRecieve": { 343 | "parallel": 279.1763379573822, 344 | "series": 2232.475461959839 345 | }, 346 | "leaderElection": 2150.9966419935226 347 | } 348 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ], 5 | "statusCheckVerify": true, 6 | "ignoreDeps": [], 7 | "automerge": true, 8 | "major": { 9 | "automerge": false 10 | }, 11 | "rebaseStalePrs": true, 12 | "prHourlyLimit": 2, 13 | "ignorePaths": [ 14 | "test-electron/package.json" 15 | ], 16 | "dependencyDashboard": false 17 | } 18 | -------------------------------------------------------------------------------- /src/browserify.index.js: -------------------------------------------------------------------------------- 1 | const module = require('./index.es5.js'); 2 | const BroadcastChannel = module.BroadcastChannel; 3 | const createLeaderElection = module.createLeaderElection; 4 | 5 | window['BroadcastChannel2'] = BroadcastChannel; 6 | window['createLeaderElection'] = createLeaderElection; -------------------------------------------------------------------------------- /src/index.es5.js: -------------------------------------------------------------------------------- 1 | /** 2 | * because babel can only export on default-attribute, 3 | * we use this for the non-module-build 4 | * this ensures that users do not have to use 5 | * var BroadcastChannel = require('broadcast-channel').default; 6 | * but 7 | * var BroadcastChannel = require('broadcast-channel'); 8 | */ 9 | 10 | import { 11 | BroadcastChannel, 12 | createLeaderElection, 13 | clearNodeFolder, 14 | enforceOptions, 15 | beLeader 16 | } from './index.js'; 17 | 18 | module.exports = { 19 | BroadcastChannel, 20 | createLeaderElection, 21 | clearNodeFolder, 22 | enforceOptions, 23 | beLeader 24 | }; 25 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export { 2 | BroadcastChannel, 3 | clearNodeFolder, 4 | enforceOptions, 5 | OPEN_BROADCAST_CHANNELS 6 | } from './broadcast-channel.js'; 7 | export { 8 | createLeaderElection 9 | } from './leader-election.js'; 10 | export { 11 | beLeader 12 | } from './leader-election-util.js'; 13 | -------------------------------------------------------------------------------- /src/leader-election-util.js: -------------------------------------------------------------------------------- 1 | import { 2 | add as unloadAdd 3 | } from 'unload'; 4 | 5 | /** 6 | * sends and internal message over the broadcast-channel 7 | */ 8 | export function sendLeaderMessage(leaderElector, action) { 9 | const msgJson = { 10 | context: 'leader', 11 | action, 12 | token: leaderElector.token 13 | }; 14 | return leaderElector.broadcastChannel.postInternal(msgJson); 15 | } 16 | 17 | export function beLeader(leaderElector) { 18 | leaderElector.isLeader = true; 19 | leaderElector._hasLeader = true; 20 | const unloadFn = unloadAdd(() => leaderElector.die()); 21 | leaderElector._unl.push(unloadFn); 22 | 23 | const isLeaderListener = msg => { 24 | if (msg.context === 'leader' && msg.action === 'apply') { 25 | sendLeaderMessage(leaderElector, 'tell'); 26 | } 27 | 28 | if (msg.context === 'leader' && msg.action === 'tell' && !leaderElector._dpLC) { 29 | /** 30 | * another instance is also leader! 31 | * This can happen on rare events 32 | * like when the CPU is at 100% for long time 33 | * or the tabs are open very long and the browser throttles them. 34 | * @link https://github.com/pubkey/broadcast-channel/issues/414 35 | * @link https://github.com/pubkey/broadcast-channel/issues/385 36 | */ 37 | leaderElector._dpLC = true; 38 | leaderElector._dpL(); // message the lib user so the app can handle the problem 39 | sendLeaderMessage(leaderElector, 'tell'); // ensure other leader also knows the problem 40 | } 41 | }; 42 | leaderElector.broadcastChannel.addEventListener('internal', isLeaderListener); 43 | leaderElector._lstns.push(isLeaderListener); 44 | return sendLeaderMessage(leaderElector, 'tell'); 45 | } 46 | -------------------------------------------------------------------------------- /src/leader-election-web-lock.js: -------------------------------------------------------------------------------- 1 | import { 2 | randomToken 3 | } from './util.js'; 4 | import { 5 | sendLeaderMessage, 6 | beLeader 7 | } from './leader-election-util.js'; 8 | 9 | /** 10 | * A faster version of the leader elector that uses the WebLock API 11 | * @link https://developer.mozilla.org/en-US/docs/Web/API/Web_Locks_API 12 | */ 13 | export const LeaderElectionWebLock = function (broadcastChannel, options) { 14 | this.broadcastChannel = broadcastChannel; 15 | broadcastChannel._befC.push(() => this.die()); 16 | this._options = options; 17 | 18 | this.isLeader = false; 19 | this.isDead = false; 20 | this.token = randomToken(); 21 | this._lstns = []; 22 | this._unl = []; 23 | this._dpL = () => { }; // onduplicate listener 24 | this._dpLC = false; // true when onduplicate called 25 | 26 | this._wKMC = {}; // stuff for cleanup 27 | 28 | // lock name 29 | this.lN = 'pubkey-bc||' + broadcastChannel.method.type + '||' + broadcastChannel.name; 30 | 31 | }; 32 | 33 | 34 | 35 | LeaderElectionWebLock.prototype = { 36 | hasLeader() { 37 | return navigator.locks.query().then(locks => { 38 | const relevantLocks = locks.held ? locks.held.filter(lock => lock.name === this.lN) : []; 39 | if (relevantLocks && relevantLocks.length > 0) { 40 | return true; 41 | } else { 42 | return false; 43 | } 44 | }); 45 | }, 46 | awaitLeadership() { 47 | if (!this._wLMP) { 48 | this._wKMC.c = new AbortController(); 49 | const returnPromise = new Promise((res, rej) => { 50 | this._wKMC.res = res; 51 | this._wKMC.rej = rej; 52 | }); 53 | this._wLMP = new Promise((res) => { 54 | navigator.locks.request( 55 | this.lN, 56 | { 57 | signal: this._wKMC.c.signal 58 | }, 59 | () => { 60 | // if the lock resolved, we can drop the abort controller 61 | this._wKMC.c = undefined; 62 | 63 | beLeader(this); 64 | res(); 65 | return returnPromise; 66 | } 67 | ).catch(() => { }); 68 | }); 69 | } 70 | return this._wLMP; 71 | }, 72 | 73 | set onduplicate(_fn) { 74 | // Do nothing because there are no duplicates in the WebLock version 75 | }, 76 | die() { 77 | this._lstns.forEach(listener => this.broadcastChannel.removeEventListener('internal', listener)); 78 | this._lstns = []; 79 | this._unl.forEach(uFn => uFn.remove()); 80 | this._unl = []; 81 | if (this.isLeader) { 82 | this.isLeader = false; 83 | } 84 | this.isDead = true; 85 | if (this._wKMC.res) { 86 | this._wKMC.res(); 87 | } 88 | if (this._wKMC.c) { 89 | this._wKMC.c.abort('LeaderElectionWebLock.die() called'); 90 | } 91 | return sendLeaderMessage(this, 'death'); 92 | } 93 | }; 94 | -------------------------------------------------------------------------------- /src/method-chooser.js: -------------------------------------------------------------------------------- 1 | import { NativeMethod } from './methods/native.js'; 2 | import { IndexedDBMethod } from './methods/indexed-db.js'; 3 | import { LocalstorageMethod } from './methods/localstorage.js'; 4 | import { SimulateMethod } from './methods/simulate.js'; 5 | // the line below will be removed from es5/browser builds 6 | import * as NodeMethod from './methods/node.js'; 7 | 8 | // order is important 9 | const METHODS = [ 10 | NativeMethod, // fastest 11 | IndexedDBMethod, 12 | LocalstorageMethod 13 | ]; 14 | 15 | export function chooseMethod(options) { 16 | let chooseMethods = [].concat(options.methods, METHODS).filter(Boolean); 17 | 18 | // the line below will be removed from es5/browser builds 19 | chooseMethods.push(NodeMethod); 20 | 21 | // directly chosen 22 | if (options.type) { 23 | if (options.type === 'simulate') { 24 | // only use simulate-method if directly chosen 25 | return SimulateMethod; 26 | } 27 | const ret = chooseMethods.find(m => m.type === options.type); 28 | if (!ret) throw new Error('method-type ' + options.type + ' not found'); 29 | else return ret; 30 | } 31 | 32 | /** 33 | * if no webworker support is needed, 34 | * remove idb from the list so that localstorage will be chosen 35 | */ 36 | if (!options.webWorkerSupport) { 37 | chooseMethods = chooseMethods.filter(m => m.type !== 'idb'); 38 | } 39 | 40 | const useMethod = chooseMethods.find(method => method.canBeUsed()); 41 | if (!useMethod) { 42 | throw new Error(`No usable method found in ${JSON.stringify(METHODS.map(m => m.type))}`); 43 | } else { 44 | return useMethod; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/methods/cookies.js: -------------------------------------------------------------------------------- 1 | /** 2 | * if you really need this method, 3 | * implement it! 4 | */ 5 | -------------------------------------------------------------------------------- /src/methods/localstorage.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A localStorage-only method which uses localstorage and its 'storage'-event 3 | * This does not work inside webworkers because they have no access to localstorage 4 | * This is basically implemented to support IE9 or your grandmother's toaster. 5 | * @link https://caniuse.com/#feat=namevalue-storage 6 | * @link https://caniuse.com/#feat=indexeddb 7 | */ 8 | 9 | import { ObliviousSet } from 'oblivious-set'; 10 | 11 | import { 12 | fillOptionsWithDefaults 13 | } from '../options.js'; 14 | 15 | import { 16 | sleep, 17 | randomToken, 18 | microSeconds as micro 19 | } from '../util.js'; 20 | 21 | export const microSeconds = micro; 22 | 23 | const KEY_PREFIX = 'pubkey.broadcastChannel-'; 24 | export const type = 'localstorage'; 25 | 26 | /** 27 | * copied from crosstab 28 | * @link https://github.com/tejacques/crosstab/blob/master/src/crosstab.js#L32 29 | */ 30 | export function getLocalStorage() { 31 | let localStorage; 32 | if (typeof window === 'undefined') return null; 33 | try { 34 | localStorage = window.localStorage; 35 | localStorage = window['ie8-eventlistener/storage'] || window.localStorage; 36 | } catch (e) { 37 | // New versions of Firefox throw a Security exception 38 | // if cookies are disabled. See 39 | // https://bugzilla.mozilla.org/show_bug.cgi?id=1028153 40 | } 41 | return localStorage; 42 | } 43 | 44 | export function storageKey(channelName) { 45 | return KEY_PREFIX + channelName; 46 | } 47 | 48 | 49 | /** 50 | * writes the new message to the storage 51 | * and fires the storage-event so other readers can find it 52 | */ 53 | export function postMessage(channelState, messageJson) { 54 | return new Promise(res => { 55 | sleep().then(() => { 56 | const key = storageKey(channelState.channelName); 57 | const writeObj = { 58 | token: randomToken(), 59 | time: Date.now(), 60 | data: messageJson, 61 | uuid: channelState.uuid 62 | }; 63 | const value = JSON.stringify(writeObj); 64 | getLocalStorage().setItem(key, value); 65 | 66 | /** 67 | * StorageEvent does not fire the 'storage' event 68 | * in the window that changes the state of the local storage. 69 | * So we fire it manually 70 | */ 71 | const ev = document.createEvent('Event'); 72 | ev.initEvent('storage', true, true); 73 | ev.key = key; 74 | ev.newValue = value; 75 | window.dispatchEvent(ev); 76 | 77 | res(); 78 | }); 79 | }); 80 | } 81 | 82 | export function addStorageEventListener(channelName, fn) { 83 | const key = storageKey(channelName); 84 | const listener = ev => { 85 | if (ev.key === key) { 86 | fn(JSON.parse(ev.newValue)); 87 | } 88 | }; 89 | window.addEventListener('storage', listener); 90 | return listener; 91 | } 92 | export function removeStorageEventListener(listener) { 93 | window.removeEventListener('storage', listener); 94 | } 95 | 96 | export function create(channelName, options) { 97 | options = fillOptionsWithDefaults(options); 98 | if (!canBeUsed()) { 99 | throw new Error('BroadcastChannel: localstorage cannot be used'); 100 | } 101 | 102 | const uuid = randomToken(); 103 | 104 | /** 105 | * eMIs 106 | * contains all messages that have been emitted before 107 | * @type {ObliviousSet} 108 | */ 109 | const eMIs = new ObliviousSet(options.localstorage.removeTimeout); 110 | 111 | const state = { 112 | channelName, 113 | uuid, 114 | eMIs // emittedMessagesIds 115 | }; 116 | 117 | 118 | state.listener = addStorageEventListener( 119 | channelName, 120 | (msgObj) => { 121 | if (!state.messagesCallback) return; // no listener 122 | if (msgObj.uuid === uuid) return; // own message 123 | if (!msgObj.token || eMIs.has(msgObj.token)) return; // already emitted 124 | if (msgObj.data.time && msgObj.data.time < state.messagesCallbackTime) return; // too old 125 | 126 | eMIs.add(msgObj.token); 127 | state.messagesCallback(msgObj.data); 128 | } 129 | ); 130 | 131 | 132 | return state; 133 | } 134 | 135 | export function close(channelState) { 136 | removeStorageEventListener(channelState.listener); 137 | } 138 | 139 | export function onMessage(channelState, fn, time) { 140 | channelState.messagesCallbackTime = time; 141 | channelState.messagesCallback = fn; 142 | } 143 | 144 | export function canBeUsed() { 145 | const ls = getLocalStorage(); 146 | if (!ls) return false; 147 | 148 | try { 149 | const key = '__broadcastchannel_check'; 150 | ls.setItem(key, 'works'); 151 | ls.removeItem(key); 152 | } catch (e) { 153 | // Safari 10 in private mode will not allow write access to local 154 | // storage and fail with a QuotaExceededError. See 155 | // https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API#Private_Browsing_Incognito_modes 156 | return false; 157 | } 158 | 159 | return true; 160 | } 161 | 162 | 163 | export function averageResponseTime() { 164 | const defaultTime = 120; 165 | const userAgent = navigator.userAgent.toLowerCase(); 166 | if (userAgent.includes('safari') && !userAgent.includes('chrome')) { 167 | // safari is much slower so this time is higher 168 | return defaultTime * 2; 169 | } 170 | return defaultTime; 171 | } 172 | 173 | export const LocalstorageMethod = { 174 | create, 175 | close, 176 | onMessage, 177 | postMessage, 178 | canBeUsed, 179 | type, 180 | averageResponseTime, 181 | microSeconds 182 | }; 183 | -------------------------------------------------------------------------------- /src/methods/native.js: -------------------------------------------------------------------------------- 1 | 2 | import { 3 | microSeconds as micro, 4 | PROMISE_RESOLVED_VOID 5 | } from '../util.js'; 6 | 7 | export const microSeconds = micro; 8 | 9 | export const type = 'native'; 10 | 11 | export function create(channelName) { 12 | const state = { 13 | time: micro(), 14 | messagesCallback: null, 15 | bc: new BroadcastChannel(channelName), 16 | subFns: [] // subscriberFunctions 17 | }; 18 | 19 | state.bc.onmessage = msgEvent => { 20 | if (state.messagesCallback) { 21 | state.messagesCallback(msgEvent.data); 22 | } 23 | }; 24 | 25 | return state; 26 | } 27 | 28 | export function close(channelState) { 29 | channelState.bc.close(); 30 | channelState.subFns = []; 31 | } 32 | 33 | export function postMessage(channelState, messageJson) { 34 | try { 35 | channelState.bc.postMessage(messageJson, false); 36 | return PROMISE_RESOLVED_VOID; 37 | } catch (err) { 38 | return Promise.reject(err); 39 | } 40 | } 41 | 42 | export function onMessage(channelState, fn) { 43 | channelState.messagesCallback = fn; 44 | } 45 | 46 | export function canBeUsed() { 47 | 48 | // Deno runtime 49 | // eslint-disable-next-line 50 | if (typeof globalThis !== 'undefined' && globalThis.Deno && globalThis.Deno.args) { 51 | return true; 52 | } 53 | 54 | // Browser runtime 55 | if ( 56 | (typeof window !== 'undefined' || typeof self !== 'undefined') && 57 | typeof BroadcastChannel === 'function' 58 | ) { 59 | if (BroadcastChannel._pubkey) { 60 | throw new Error( 61 | 'BroadcastChannel: Do not overwrite window.BroadcastChannel with this module, this is not a polyfill' 62 | ); 63 | } 64 | return true; 65 | } else { 66 | return false; 67 | } 68 | } 69 | 70 | 71 | export function averageResponseTime() { 72 | return 150; 73 | } 74 | 75 | export const NativeMethod = { 76 | create, 77 | close, 78 | onMessage, 79 | postMessage, 80 | canBeUsed, 81 | type, 82 | averageResponseTime, 83 | microSeconds 84 | }; 85 | -------------------------------------------------------------------------------- /src/methods/simulate.js: -------------------------------------------------------------------------------- 1 | import { 2 | microSeconds as micro, 3 | } from '../util.js'; 4 | 5 | export const microSeconds = micro; 6 | 7 | export const type = 'simulate'; 8 | 9 | const SIMULATE_CHANNELS = new Set(); 10 | 11 | export function create(channelName) { 12 | const state = { 13 | time: microSeconds(), 14 | name: channelName, 15 | messagesCallback: null 16 | }; 17 | SIMULATE_CHANNELS.add(state); 18 | return state; 19 | } 20 | 21 | export function close(channelState) { 22 | SIMULATE_CHANNELS.delete(channelState); 23 | } 24 | 25 | export const SIMULATE_DELAY_TIME = 5; 26 | 27 | export function postMessage(channelState, messageJson) { 28 | return new Promise(res => setTimeout(() => { 29 | const channelArray = Array.from(SIMULATE_CHANNELS); 30 | channelArray.forEach(channel => { 31 | if ( 32 | channel.name === channelState.name && // has same name 33 | channel !== channelState && // not own channel 34 | !!channel.messagesCallback && // has subscribers 35 | channel.time < messageJson.time // channel not created after postMessage() call 36 | ) { 37 | channel.messagesCallback(messageJson); 38 | } 39 | }); 40 | res(); 41 | }, SIMULATE_DELAY_TIME)); 42 | } 43 | 44 | export function onMessage(channelState, fn) { 45 | channelState.messagesCallback = fn; 46 | } 47 | 48 | export function canBeUsed() { 49 | return true; 50 | } 51 | 52 | 53 | export function averageResponseTime() { 54 | return SIMULATE_DELAY_TIME; 55 | } 56 | 57 | export const SimulateMethod = { 58 | create, 59 | close, 60 | onMessage, 61 | postMessage, 62 | canBeUsed, 63 | type, 64 | averageResponseTime, 65 | microSeconds 66 | }; 67 | -------------------------------------------------------------------------------- /src/options.js: -------------------------------------------------------------------------------- 1 | export function fillOptionsWithDefaults(originalOptions = {}) { 2 | const options = JSON.parse(JSON.stringify(originalOptions)); 3 | 4 | // main 5 | if (typeof options.webWorkerSupport === 'undefined') options.webWorkerSupport = true; 6 | 7 | 8 | // indexed-db 9 | if (!options.idb) options.idb = {}; 10 | // after this time the messages get deleted 11 | if (!options.idb.ttl) options.idb.ttl = 1000 * 45; 12 | if (!options.idb.fallbackInterval) options.idb.fallbackInterval = 150; 13 | // handles abrupt db onclose events. 14 | if (originalOptions.idb && typeof originalOptions.idb.onclose === 'function') 15 | options.idb.onclose = originalOptions.idb.onclose; 16 | 17 | // localstorage 18 | if (!options.localstorage) options.localstorage = {}; 19 | if (!options.localstorage.removeTimeout) options.localstorage.removeTimeout = 1000 * 60; 20 | 21 | // custom methods 22 | if (originalOptions.methods) options.methods = originalOptions.methods; 23 | 24 | // node 25 | if (!options.node) options.node = {}; 26 | if (!options.node.ttl) options.node.ttl = 1000 * 60 * 2; // 2 minutes; 27 | /** 28 | * On linux use 'ulimit -Hn' to get the limit of open files. 29 | * On ubuntu this was 4096 for me, so we use half of that as maxParallelWrites default. 30 | */ 31 | if (!options.node.maxParallelWrites) options.node.maxParallelWrites = 2048; 32 | if (typeof options.node.useFastPath === 'undefined') options.node.useFastPath = true; 33 | 34 | return options; 35 | } 36 | -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * returns true if the given object is a promise 3 | */ 4 | export function isPromise(obj) { 5 | return obj && 6 | typeof obj.then === 'function'; 7 | } 8 | 9 | export const PROMISE_RESOLVED_FALSE = Promise.resolve(false); 10 | export const PROMISE_RESOLVED_TRUE = Promise.resolve(true); 11 | export const PROMISE_RESOLVED_VOID = Promise.resolve(); 12 | 13 | export function sleep(time, resolveWith) { 14 | if (!time) time = 0; 15 | return new Promise(res => setTimeout(() => res(resolveWith), time)); 16 | } 17 | 18 | export function randomInt(min, max) { 19 | return Math.floor(Math.random() * (max - min + 1) + min); 20 | } 21 | 22 | /** 23 | * https://stackoverflow.com/a/8084248 24 | */ 25 | export function randomToken() { 26 | return Math.random().toString(36).substring(2); 27 | } 28 | 29 | 30 | let lastMs = 0; 31 | 32 | /** 33 | * Returns the current unix time in micro-seconds, 34 | * WARNING: This is a pseudo-function 35 | * Performance.now is not reliable in webworkers, so we just make sure to never return the same time. 36 | * This is enough in browsers, and this function will not be used in nodejs. 37 | * The main reason for this hack is to ensure that BroadcastChannel behaves equal to production when it is used in fast-running unit tests. 38 | */ 39 | export function microSeconds() { 40 | let ret = Date.now() * 1000; // milliseconds to microseconds 41 | if (ret <= lastMs) { 42 | ret = lastMs + 1; 43 | } 44 | lastMs = ret; 45 | return ret; 46 | } 47 | 48 | /** 49 | * Check if WebLock API is supported. 50 | * @link https://developer.mozilla.org/en-US/docs/Web/API/Web_Locks_API 51 | */ 52 | export function supportsWebLockAPI() { 53 | if ( 54 | typeof navigator !== 'undefined' && 55 | typeof navigator.locks !== 'undefined' && 56 | typeof navigator.locks.request === 'function' 57 | ) { 58 | return true; 59 | } else { 60 | return false; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /test-electron/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .DS_Store 3 | log.txt 4 | config.json -------------------------------------------------------------------------------- /test-electron/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /test-electron/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Electron Test 6 | 7 | 8 | 9 | 10 | 14 | 15 | -------------------------------------------------------------------------------- /test-electron/main.js: -------------------------------------------------------------------------------- 1 | const electron = require('electron'); 2 | require('electron-window-manager'); 3 | const path = require('path'); 4 | const url = require('url'); 5 | 6 | const { BroadcastChannel } = require('broadcast-channel'); 7 | 8 | const app = electron.app; 9 | const BrowserWindow = electron.BrowserWindow; 10 | 11 | const windows = []; 12 | 13 | function createWindow() { 14 | const width = 300; 15 | const height = 600; 16 | const w = new BrowserWindow({ 17 | width, 18 | height, 19 | webPreferences: { 20 | nodeIntegration: true 21 | } 22 | }); 23 | 24 | w.loadURL(url.format({ 25 | pathname: path.join(__dirname, 'index.html'), 26 | protocol: 'file:', 27 | slashes: true 28 | })); 29 | 30 | const x = windows.length * width; 31 | const y = 0; 32 | w.setPosition(x, y); 33 | w.custom = { 34 | }; 35 | windows.push(w); 36 | } 37 | 38 | 39 | app.on('ready', async function () { 40 | 41 | const channel = new BroadcastChannel('foobar'); 42 | channel.postMessage('hi'); 43 | 44 | 45 | 46 | 47 | 48 | createWindow(); 49 | createWindow(); 50 | }); 51 | 52 | app.on('window-all-closed', function () { 53 | if (process.platform !== 'darwin') 54 | app.quit(); 55 | }); 56 | -------------------------------------------------------------------------------- /test-electron/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "broadcast-channel-electron-test", 3 | "private": true, 4 | "main": "main.js", 5 | "scripts": { 6 | "start": "npm run electron", 7 | "electron": "electron .", 8 | "test": "mocha" 9 | }, 10 | "dependencies": { 11 | "babel-polyfill": "6.26.0", 12 | "babel-runtime": "6.26.0", 13 | "broadcast-channel": "../", 14 | "electron": "8.5.5", 15 | "electron-tabs": "0.15.0", 16 | "electron-window-manager": "1.0.6", 17 | "melanke-watchjs": "1.5.2" 18 | }, 19 | "devDependencies": { 20 | "mocha": "8.2.1", 21 | "node": "13.14.0", 22 | "spectron": "10.0.1" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /test-electron/page.js: -------------------------------------------------------------------------------- 1 | const electron = require('electron'); 2 | const renderTest = require('./test/render.test.js'); 3 | // const BroadcastChannel = require('broadcast-channel'); 4 | 5 | require('babel-polyfill'); 6 | 7 | 8 | const RxDB = require('rxdb'); 9 | RxDB.plugin(require('pouchdb-adapter-idb')); 10 | 11 | async function run() { 12 | /** 13 | * to check if rxdb works correctly, we run some integration-tests here 14 | * if you want to use this electron-example as boilerplate, remove this line 15 | */ 16 | await renderTest(); 17 | } 18 | run(); 19 | -------------------------------------------------------------------------------- /test-electron/test/render.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const { BroadcastChannel } = require('broadcast-channel'); 3 | 4 | /** 5 | * this tests run inside of the browser-windows so we can ensure 6 | * everything there works correctly 7 | */ 8 | module.exports = (function () { 9 | const runTests = async function () { 10 | 11 | // normal channel 12 | const channel = new BroadcastChannel('foobar'); 13 | assert.ok(channel); 14 | channel.postMessage('lulz'); 15 | 16 | // no webworker 17 | const channelNoWebWorker = new BroadcastChannel('foobar', { 18 | webWorkerSupport: false 19 | }); 20 | assert.ok(channelNoWebWorker); 21 | channelNoWebWorker.postMessage('lulz'); 22 | 23 | }; 24 | return runTests; 25 | }()); 26 | -------------------------------------------------------------------------------- /test-electron/test/spec.js: -------------------------------------------------------------------------------- 1 | const Application = require('spectron').Application; 2 | const assert = require('assert'); 3 | const electronPath = require('electron'); // Require Electron from the binaries included in node_modules. 4 | const path = require('path'); 5 | const AsyncTestUtil = require('async-test-util'); 6 | 7 | describe('Application launch', function() { 8 | this.timeout(20000); 9 | let app; 10 | before(function() { 11 | this.app = new Application({ 12 | // Your electron path can be any binary 13 | // i.e for OSX an example path could be '/Applications/MyApp.app/Contents/MacOS/MyApp' 14 | // But for the sake of the example we fetch it from our node_modules. 15 | path: electronPath, 16 | 17 | // Assuming you have the following directory structure 18 | 19 | // |__ my project 20 | // |__ ... 21 | // |__ main.js 22 | // |__ package.json 23 | // |__ index.html 24 | // |__ ... 25 | // |__ test 26 | // |__ spec.js <- You are here! ~ Well you should be. 27 | 28 | // The following line tells spectron to look and use the main.js file 29 | // and the package.json located 1 level above. 30 | args: [path.join(__dirname, '..')] 31 | }); 32 | app = this.app; 33 | return this.app.start(); 34 | }); 35 | 36 | after(function() { 37 | if (this.app && this.app.isRunning()) 38 | return this.app.stop(); 39 | }); 40 | 41 | it('shows an initial window', async () => { 42 | await app.client.waitUntilWindowLoaded(); 43 | const count = await app.client.getWindowCount(); 44 | assert.equal(count, 2); 45 | // Please note that getWindowCount() will return 2 if `dev tools` are opened. 46 | // assert.equal(count, 2) 47 | await AsyncTestUtil.wait(500); 48 | }); 49 | 50 | /* 51 | it('insert one hero', async () => { 52 | console.log('test: insert one hero'); 53 | console.dir(await app.client.getRenderProcessLogs()); 54 | await app.client.waitUntilWindowLoaded(); 55 | await app.client.element('#input-name').setValue('Bob Kelso'); 56 | await app.client.element('#input-color').setValue('blue'); 57 | await app.client.element('#input-submit').click(); 58 | 59 | await AsyncTestUtil.waitUntil(async () => { 60 | const foundElement = await app.client.element('.name[name="Bob Kelso"]'); 61 | return foundElement.value; 62 | }); 63 | await AsyncTestUtil.wait(100); 64 | }); 65 | it('check if replicated to both windows', async () => { 66 | const window1 = app.client.windowByIndex(0); 67 | await AsyncTestUtil.waitUntil(async () => { 68 | const foundElement = await window1.element('.name[name="Bob Kelso"]'); 69 | return foundElement.value; 70 | }); 71 | 72 | const window2 = app.client.windowByIndex(1); 73 | await AsyncTestUtil.waitUntil(async () => { 74 | const foundElement = await window2.element('.name[name="Bob Kelso"]'); 75 | return foundElement.value; 76 | }); 77 | await AsyncTestUtil.wait(100); 78 | });*/ 79 | }); 80 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | env: 2 | node: true 3 | mocha: true 4 | jest: true 5 | -------------------------------------------------------------------------------- /test/close.test.js: -------------------------------------------------------------------------------- 1 | import BroadcastChannel from 'broadcast-channel'; 2 | 3 | class Foo { 4 | constructor () { 5 | this.bc = new BroadcastChannel.BroadcastChannel('test'); 6 | this.bc.addEventListener('message', this.cb); 7 | } 8 | 9 | cb () {} 10 | } 11 | 12 | 13 | describe('Broadcast Channel', () => { 14 | beforeEach(async () => { 15 | await BroadcastChannel.clearNodeFolder(); 16 | }); 17 | 18 | test('local', async () => { 19 | const foo = new Foo(); 20 | 21 | const result = await new Promise((a) => { 22 | setTimeout(() => { 23 | a(true); 24 | }, 1000); 25 | }); 26 | 27 | expect(result).toBe(true); 28 | 29 | // Cleanup 30 | await foo.bc.close(); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /test/e2e.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | Selector 3 | } from 'testcafe'; 4 | import AsyncTestUtil from 'async-test-util'; 5 | 6 | const BASE_PAGE = 'http://localhost:8080/e2e.html'; 7 | 8 | fixture`Example page` 9 | .page`http://localhost:8080/`; 10 | 11 | /** 12 | * Checks if there where errors on the browser console. 13 | * If yes, this will kill the process 14 | */ 15 | async function assertNoErrors(t) { 16 | const logs = await t.getBrowserConsoleMessages(); 17 | console.log('logs:'); 18 | console.dir(logs); 19 | if (logs.error.length > 0) { 20 | console.log('assertNoErrors got ' + logs.error.length + ' errors:'); 21 | console.dir(logs.error); 22 | process.kill(process.pid); 23 | } 24 | } 25 | 26 | async function nativeBroadcastChannelExists(t) { 27 | const prop = await t.eval(() => window.BroadcastChannel); 28 | const ret = !!prop; 29 | console.log('nativeBroadcastChannelExists: ' + ret); 30 | return ret; 31 | } 32 | 33 | // BroadcastChannel 34 | [ 35 | 'native', 36 | 'idb', 37 | 'localstorage', 38 | 'default' 39 | ].forEach(methodType => { 40 | test.page(BASE_PAGE + '?methodType=' + methodType + '&autoStart=startBroadcastChannel') 41 | ( 42 | 'test(BroadcastChannel) with method: ' + methodType, 43 | async (t) => { 44 | console.log('##### START BroadcastChannel TEST WITH ' + methodType); 45 | 46 | if (methodType === 'native' && !(await nativeBroadcastChannelExists(t))) { 47 | console.log('skipping native method since it is not supported by the browser'); 48 | return; 49 | } 50 | 51 | await assertNoErrors(t); 52 | await AsyncTestUtil.waitUntil(async () => { 53 | await assertNoErrors(t); 54 | const stateContainer = Selector('#state'); 55 | const exists = await stateContainer.exists; 56 | if (!exists) { 57 | console.log('stateContainer not exists'); 58 | /* 59 | const out = await t.getBrowserConsoleMessages(); 60 | console.log('out:'); 61 | console.log(JSON.stringify(out)); 62 | */ 63 | return false; 64 | } else { 65 | console.log('stateContainer exists'); 66 | } 67 | const value = await stateContainer.innerText; 68 | // console.log(value); 69 | 70 | 71 | // make a console.log so travis does not terminate because of no output 72 | console.log('BroadcastChannel(' + methodType + ') still no success'); 73 | 74 | return value === 'SUCCESS'; 75 | }, 0, 500); 76 | }); 77 | }); 78 | 79 | 80 | // LeaderElection 81 | [ 82 | 'native', 83 | 'idb', 84 | 'localstorage', 85 | 'default' 86 | ].forEach(methodType => { 87 | test.page(BASE_PAGE + '?methodType=' + methodType + '&autoStart=startLeaderElection')('test(LeaderElection) with method: ' + methodType, async (t) => { 88 | console.log('##### START LeaderElection TEST WITH ' + methodType); 89 | 90 | if (methodType === 'native' && !(await nativeBroadcastChannelExists(t))) { 91 | console.log('skipping native method since it is not supported by the browser'); 92 | return; 93 | } 94 | 95 | await assertNoErrors(t); 96 | 97 | await AsyncTestUtil.waitUntil(async () => { 98 | await assertNoErrors(t); 99 | const stateContainer = Selector('#state'); 100 | const value = await stateContainer.innerText; 101 | 102 | // make a console.log so travis does not terminate because of no output 103 | const iframeAmount = await Selector('#leader-iframes iframe').count; 104 | console.log('LeaderElection(' + methodType + ') still no success (' + iframeAmount + ' iframes left)'); 105 | 106 | return value === 'SUCCESS'; 107 | }, 0, 1000); 108 | console.log('LeaderElection(' + methodType + ') DONE'); 109 | }); 110 | }); 111 | 112 | 113 | // Worker 114 | [ 115 | 'native', 116 | 'idb', 117 | // 'localstorage', // WebWorker does not work with localstorage method 118 | 'default' 119 | ].forEach(methodType => { 120 | test.page(BASE_PAGE + '?methodType=' + methodType + '&autoStart=startWorkerTest')('test(startWorkerTest) with method: ' + methodType, async (t) => { 121 | console.log('##### START LeaderElection TEST WITH ' + methodType); 122 | 123 | if (methodType === 'native' && !(await nativeBroadcastChannelExists(t))) { 124 | console.log('skipping native method since it is not supported by the browser'); 125 | return; 126 | } 127 | 128 | await assertNoErrors(t); 129 | await AsyncTestUtil.waitUntil(async () => { 130 | await assertNoErrors(t); 131 | const stateContainer = Selector('#state'); 132 | const exists = await stateContainer.exists; 133 | if (!exists) { 134 | console.log('stateContainer not exists'); 135 | /* 136 | const out = await t.getBrowserConsoleMessages(); 137 | console.log('out:'); 138 | console.log(JSON.stringify(out)); 139 | */ 140 | return false; 141 | } else { 142 | console.log('stateContainer exists'); 143 | } 144 | const value = await stateContainer.innerText; 145 | // console.log(value); 146 | 147 | 148 | // make a console.log so travis does not terminate because of no output 149 | console.log('BroadcastChannel(' + methodType + ') still no success'); 150 | 151 | return value === 'SUCCESS'; 152 | }, 0, 500); 153 | }); 154 | }); 155 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | const isNode = require('detect-node'); 2 | 3 | if (!isNode) { 4 | // if browsers 5 | console.dir = obj => console.log(JSON.stringify(obj, null, 2)); 6 | 7 | const errorBefore = console.error.bind(console); 8 | console.error = (args) => { 9 | console.log('console.error(): ' + args); 10 | errorBefore(args); 11 | }; 12 | 13 | 14 | } 15 | 16 | require('./unit.test'); 17 | require('./integration.test'); 18 | require('./issues.test'); 19 | -------------------------------------------------------------------------------- /test/issues.test.js: -------------------------------------------------------------------------------- 1 | const isNode = require('detect-node'); 2 | const { 3 | BroadcastChannel 4 | } = require('../'); 5 | const AsyncTestUtil = require('async-test-util'); 6 | 7 | describe('issues.test.js', () => { 8 | it('#4 should throw when window.BroadcastChannel is overwritten', async () => { 9 | if (isNode) return; // only on browsers 10 | const bcBefore = window.BroadcastChannel; 11 | window.BroadcastChannel = BroadcastChannel; 12 | 13 | let bc; 14 | await AsyncTestUtil.assertThrows( 15 | () => { 16 | bc = new BroadcastChannel(); 17 | }, 18 | Error, 19 | 'polyfill' 20 | ); 21 | if (bc) bc.close(); 22 | 23 | // reset 24 | window.BroadcastChannel = bcBefore; 25 | }); 26 | it('https://github.com/pubkey/rxdb/issues/852 if cleanup did not remove the info-file, it should not crash even if socket-file not exists', async () => { 27 | if (!isNode) return; // only on node 28 | const fs = require('fs'); 29 | const channelName = AsyncTestUtil.randomString(12); 30 | 31 | const channel1 = new BroadcastChannel(channelName); 32 | await channel1._prepP; 33 | 34 | // remove socket-file 35 | fs.unlinkSync(channel1._state.socketEE.path); 36 | 37 | // send message over other channel 38 | const channel2 = new BroadcastChannel(channelName); 39 | await channel2.postMessage({ 40 | foo: 'bar' 41 | }); 42 | 43 | await channel1.close(); 44 | await channel2.close(); 45 | }); 46 | it('write many messages and then close', async function() { 47 | this.timeout(40 * 1000); 48 | const channelName = AsyncTestUtil.randomString(12); 49 | const channel = new BroadcastChannel(channelName); 50 | new Array(5000) 51 | .fill(0) 52 | .map((_i, idx) => ({ 53 | foo: 'bar', 54 | idx, 55 | longString: AsyncTestUtil.randomString(40) 56 | })) 57 | .map(msg => channel.postMessage(msg)); 58 | 59 | 60 | await channel.close(); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /test/module.cjs.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const { BroadcastChannel } = require('../'); 3 | 4 | describe('CJS module', () => { 5 | it('should require without error', () => { 6 | assert.ok(BroadcastChannel.prototype.postMessage); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /test/module.esm.test.mjs: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import { BroadcastChannel } from '../dist/esnode/index.js'; 3 | 4 | describe('ESM module', () => { 5 | it('should import without error', () => { 6 | assert.ok(BroadcastChannel.prototype.postMessage); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /test/performance.test.js: -------------------------------------------------------------------------------- 1 | const AsyncTestUtil = require('async-test-util'); 2 | const { 3 | BroadcastChannel, 4 | clearNodeFolder, 5 | createLeaderElection 6 | } = require('../'); 7 | 8 | const benchmark = { 9 | openClose: {}, 10 | sendRecieve: {} 11 | }; 12 | 13 | const options = { 14 | node: { 15 | useFastPath: false 16 | } 17 | }; 18 | 19 | const elapsedTime = before => { 20 | return AsyncTestUtil.performanceNow() - before; 21 | }; 22 | 23 | describe('performance.test.js', () => { 24 | it('clear tmp-folder', async () => { 25 | await clearNodeFolder(); 26 | }); 27 | it('wait a bit for jit etc..', async () => { 28 | await AsyncTestUtil.wait(2000); 29 | }); 30 | it('open/close channels', async () => { 31 | const channelName = AsyncTestUtil.randomString(10); 32 | 33 | const amount = 110; 34 | const channels = []; 35 | 36 | const startTime = AsyncTestUtil.performanceNow(); 37 | for (let i = 0; i < amount; i++) { 38 | const channel = new BroadcastChannel(channelName, options); 39 | channels.push(channel); 40 | } 41 | await Promise.all( 42 | channels.map(c => c.close()) 43 | ); 44 | 45 | const elapsed = elapsedTime(startTime); 46 | benchmark.openClose = elapsed; 47 | }); 48 | it('sendRecieve.parallel', async () => { 49 | const channelName = AsyncTestUtil.randomString(10); 50 | const channelSender = new BroadcastChannel(channelName, options); 51 | const channelReciever = new BroadcastChannel(channelName, options); 52 | const msgAmount = 2000; 53 | let emittedCount = 0; 54 | const waitPromise = new Promise(res => { 55 | channelReciever.onmessage = () => { 56 | emittedCount++; 57 | if (emittedCount === msgAmount) { 58 | res(); 59 | } 60 | }; 61 | }); 62 | 63 | const startTime = AsyncTestUtil.performanceNow(); 64 | for (let i = 0; i < msgAmount; i++) { 65 | channelSender.postMessage('foobar'); 66 | } 67 | await waitPromise; 68 | 69 | channelSender.close(); 70 | channelReciever.close(); 71 | 72 | const elapsed = elapsedTime(startTime); 73 | benchmark.sendRecieve.parallel = elapsed; 74 | }); 75 | it('sendRecieve.series', async () => { 76 | const channelName = AsyncTestUtil.randomString(10); 77 | const channelSender = new BroadcastChannel(channelName, options); 78 | const channelReciever = new BroadcastChannel(channelName, options); 79 | const msgAmount = 600; 80 | let emittedCount = 0; 81 | 82 | 83 | channelReciever.onmessage = () => { 84 | channelReciever.postMessage('pong'); 85 | }; 86 | 87 | const waitPromise = new Promise(res => { 88 | channelSender.onmessage = () => { 89 | emittedCount++; 90 | if (emittedCount === msgAmount) { 91 | res(); 92 | } else { 93 | channelSender.postMessage('ping'); 94 | } 95 | }; 96 | }); 97 | 98 | const startTime = AsyncTestUtil.performanceNow(); 99 | channelSender.postMessage('ping'); 100 | await waitPromise; 101 | 102 | channelSender.close(); 103 | channelReciever.close(); 104 | 105 | const elapsed = elapsedTime(startTime); 106 | benchmark.sendRecieve.series = elapsed; 107 | }); 108 | it('leaderElection', async () => { 109 | const startTime = AsyncTestUtil.performanceNow(); 110 | 111 | let t = 10; 112 | const channelsToClose = []; 113 | while (t > 0) { 114 | t--; 115 | const channelName = AsyncTestUtil.randomString(10); 116 | const channelA = new BroadcastChannel(channelName, options); 117 | channelsToClose.push(channelA); 118 | const channelB = new BroadcastChannel(channelName, options); 119 | channelsToClose.push(channelB); 120 | const leaderElectorA = createLeaderElection(channelA); 121 | const leaderElectorB = createLeaderElection(channelB); 122 | 123 | leaderElectorA.applyOnce(); 124 | leaderElectorB.applyOnce(); 125 | 126 | while ( 127 | !leaderElectorA.isLeader && 128 | !leaderElectorB.isLeader 129 | ) { 130 | await Promise.all([ 131 | leaderElectorA.applyOnce(), 132 | leaderElectorB.applyOnce(), 133 | /** 134 | * We apply twice to better simulate 135 | * real world usage. 136 | */ 137 | leaderElectorA.applyOnce(), 138 | leaderElectorB.applyOnce() 139 | ]); 140 | } 141 | } 142 | 143 | const elapsed = elapsedTime(startTime); 144 | benchmark.leaderElection = elapsed; 145 | 146 | await channelsToClose.forEach(channel => channel.close()); 147 | }); 148 | it('show result', () => { 149 | console.log('benchmark result:'); 150 | console.log(JSON.stringify(benchmark, null, 2)); 151 | }); 152 | }); 153 | -------------------------------------------------------------------------------- /test/scripts/iframe.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * used in docs/iframe.html 4 | */ 5 | require('@babel/polyfill'); 6 | import { 7 | getParameterByName 8 | } from './util.js'; 9 | 10 | var msgContainer = document.getElementById('messages'); 11 | 12 | var { 13 | BroadcastChannel 14 | } = require('../../'); 15 | 16 | const channelName = getParameterByName('channelName'); 17 | const methodType = getParameterByName('methodType'); 18 | 19 | // overwrite console.log 20 | const logBefore = console.log; 21 | console.log = function (str) { logBefore('iframe: ' + str); } 22 | function logToDom(str){ 23 | var textnode = document.createTextNode(str); 24 | var lineBreak = document.createElement('br'); 25 | msgContainer.appendChild(textnode); 26 | msgContainer.appendChild(lineBreak); 27 | } 28 | 29 | var channel = new BroadcastChannel(channelName, { 30 | type: methodType 31 | }); 32 | 33 | logToDom('created channel with type ' + methodType); 34 | 35 | channel.onmessage = function (msg) { 36 | logToDom('message:'); 37 | logToDom('recieved message(' + msg.step + ') from ' + msg.from + ': '); 38 | logToDom(JSON.stringify(msg)); 39 | 40 | if (!msg.answer) { 41 | logToDom('answer back(' + msg.step + ')'); 42 | channel.postMessage({ 43 | answer: true, 44 | from: 'iframe', 45 | original: msg 46 | }); 47 | } 48 | }; 49 | -------------------------------------------------------------------------------- /test/scripts/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * used in docs/index.html 4 | */ 5 | require('@babel/polyfill'); 6 | var { 7 | BroadcastChannel, 8 | createLeaderElection 9 | } = require('../../'); 10 | 11 | var channelName = 'demo'; 12 | 13 | var channel = new BroadcastChannel(channelName); 14 | 15 | // leader election 16 | var leaderElector = createLeaderElection(channel); 17 | leaderElector.awaitLeadership().then(function () { 18 | console.log('is leader'); 19 | document.title = '♛ Is Leader!'; 20 | }); 21 | 22 | var messageInput = document.getElementById('message-input'); 23 | var submitButton = document.getElementById('submit-button'); 24 | var messagesBox = document.getElementById('messages'); 25 | 26 | messageInput.onkeyup = function () { 27 | if (messageInput.value !== '') submitButton.disabled = false; 28 | else submitButton.disabled = true; 29 | }; 30 | 31 | submitButton.onclick = function () { 32 | if (submitButton.disabled) return; 33 | else { 34 | console.log('postMessage ' + messageInput.value); 35 | channel.postMessage(messageInput.value); 36 | addTextToMessageBox('send: ' + messageInput.value); 37 | messageInput.value = ''; 38 | } 39 | } 40 | 41 | function addTextToMessageBox(text) { 42 | var textnode = document.createTextNode(text); 43 | var lineBreak = document.createElement('br'); 44 | messagesBox.appendChild(textnode); 45 | messagesBox.appendChild(lineBreak); 46 | } 47 | 48 | channel.onmessage = function (message) { 49 | console.dir('recieved message: ' + message); 50 | addTextToMessageBox('recieved: ' + message); 51 | } -------------------------------------------------------------------------------- /test/scripts/leader-iframe.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * this isframe is used to test the leader-election 4 | * in the e2e tests and the demo-page 5 | * used in docs/leader-iframe.html 6 | */ 7 | 8 | require('@babel/polyfill'); 9 | import { 10 | getParameterByName 11 | } from './util.js'; 12 | 13 | var { 14 | BroadcastChannel, 15 | createLeaderElection 16 | } = require('../../'); 17 | 18 | const channelName = getParameterByName('channelName'); 19 | const methodType = getParameterByName('methodType'); 20 | const boxEl = document.getElementById('box'); 21 | 22 | // overwrite console.log 23 | const logBefore = console.log; 24 | console.log = function (str) { logBefore('iframe: ' + str); } 25 | 26 | var channel = new BroadcastChannel(channelName, { 27 | type: methodType 28 | }); 29 | 30 | var elector = createLeaderElection(channel); 31 | 32 | boxEl.innerHTML = 'start election'; 33 | console.log('leader-iframe ('+elector.token+'): start leader-election'); 34 | elector.awaitLeadership().then(()=> { 35 | console.log('leader-iframe ('+elector.token+'): I am now the leader!'); 36 | boxEl.innerHTML = 'Leader'; 37 | document.title = '♛ Leader'; 38 | }); 39 | -------------------------------------------------------------------------------- /test/scripts/util.js: -------------------------------------------------------------------------------- 1 | /* eslint no-useless-escape: "off" */ 2 | 3 | // https://stackoverflow.com/a/901144/3443137 4 | export function getParameterByName(name, url) { 5 | if (!url) url = window.location.href; 6 | name = name.replace(/[\[\]]/g, '\\$&'); 7 | const regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)'); 8 | const results = regex.exec(url); 9 | if (!results) return null; 10 | if (!results[2]) return ''; 11 | return decodeURIComponent(results[2].replace(/\+/g, ' ')); 12 | } 13 | -------------------------------------------------------------------------------- /test/scripts/windows.sh: -------------------------------------------------------------------------------- 1 | # because windows sucks, we have to install some things localy in the test-vms 2 | npm i -g rimraf 3 | npm i -g cross-env 4 | npm i -g concurrently 5 | npm i -g babel-cli 6 | npm i -g browserify 7 | npm i -g testcafe 8 | npm i -g karma 9 | npm i -g http-server 10 | npm i -g copyfiles 11 | npm i -g mocha 12 | -------------------------------------------------------------------------------- /test/scripts/worker.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * used in the test-docs as web-worker 4 | */ 5 | require('@babel/polyfill'); 6 | var { 7 | BroadcastChannel 8 | } = require('../../'); 9 | 10 | var { 11 | randomNumber, 12 | randomBoolean, 13 | wait 14 | } = require('async-test-util'); 15 | var resolved = Promise.resolve(); 16 | 17 | // overwrite console.log 18 | try { 19 | var logBefore = console.log; 20 | // console.log = function (str) { logBefore('worker: ' + str); } 21 | } catch (err) { 22 | // does not work in IE11 23 | } 24 | 25 | 26 | /** 27 | * because shitware microsoft-edge sucks, the worker 28 | * when initialisation is done, 29 | * we have to set a interval here. 30 | */ 31 | setInterval(function () { }, 10 * 1000); 32 | 33 | var channel; 34 | self.addEventListener('message', function (e) { 35 | var data = e.data; 36 | switch (data.cmd) { 37 | case 'start': 38 | console.log('Worker started'); 39 | console.log(JSON.stringify(data.msg)); 40 | 41 | channel = new BroadcastChannel(data.msg.channelName, { 42 | type: data.msg.methodType 43 | }); 44 | // console.log('Worker channel-uuid: ' + channel._state.uuid); 45 | channel.onmessage = function (msg) { 46 | console.log('recieved message(' + msg.step + ') from ' + msg.from + ': ' + JSON.stringify(msg)); 47 | 48 | if (!msg.answer) { 49 | /** 50 | * Wait a random amount of time to simulate 'normal' usage 51 | * where the worker would do some work before returning anything. 52 | * Sometimes do not wait at all to simulate a direct response. 53 | */ 54 | const waitBefore = randomBoolean() ? resolved : wait(randomNumber(10, 150)); 55 | waitBefore 56 | .then(function () { 57 | console.log('(' + msg.step + ') answer back'); 58 | channel.postMessage({ 59 | answer: true, 60 | from: 'worker', 61 | original: msg 62 | }); 63 | }); 64 | } 65 | }; 66 | 67 | self.postMessage('WORKER STARTED: '); 68 | break; 69 | case 'stop': 70 | self.postMessage('WORKER STOPPED: ' + data.msg + '. (buttons will no longer work)'); 71 | channel.close(); 72 | self.close(); // Terminates the worker. 73 | break; 74 | default: 75 | self.postMessage('Unknown command: ' + data.msg); 76 | }; 77 | }, false); 78 | -------------------------------------------------------------------------------- /test/simple.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * a simple test which just checks if the basics work 3 | */ 4 | const { 5 | BroadcastChannel 6 | } = require('../'); 7 | 8 | async function run() { 9 | const channelName = 'simpleTestChannel'; 10 | const channel = new BroadcastChannel(channelName); 11 | const channel2 = new BroadcastChannel(channelName); 12 | await channel.postMessage({ 13 | foo: 'bar' 14 | }); 15 | const messages = []; 16 | channel.onmessage = msg => messages.push(msg); 17 | 18 | await channel2.postMessage({ 19 | foo: 'bar' 20 | }); 21 | 22 | 23 | await channel.close(); 24 | await channel2.close(); 25 | } 26 | run(); 27 | -------------------------------------------------------------------------------- /test/test-deno.js: -------------------------------------------------------------------------------- 1 | import { BroadcastChannel } from '../dist/esbrowser/index.js'; 2 | import { randomString } from 'async-test-util'; 3 | import assert from 'assert'; 4 | export async function run() { 5 | 6 | console.log('--- 1'); 7 | 8 | console.dir({ 9 | // eslint-disable-next-line 10 | 'globalThis.Deno': !!globalThis.Deno, 11 | // eslint-disable-next-line 12 | 'globalThis.Deno.args': !!globalThis.Deno.args 13 | }); 14 | console.log('--- 2'); 15 | // eslint-disable-next-line 16 | console.log(Object.keys(Deno).sort().join(', ')); 17 | 18 | console.log('--- 3'); 19 | 20 | 21 | const bc = new BroadcastChannel(randomString()); 22 | console.log('bc.type: ' + bc.type); 23 | 24 | 25 | /** 26 | * Deno should use its global native BroadcastChannel 27 | * @link https://docs.deno.com/deploy/api/runtime-broadcast-channel 28 | */ 29 | assert.strictEqual(bc.type, 'native'); 30 | 31 | await bc.postMessage({ foo: 'bar' }); 32 | await bc.close(); 33 | } 34 | run(); 35 | -------------------------------------------------------------------------------- /test/typings.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * checks if the typings are correct 3 | * run via 'npm run test:typings' 4 | */ 5 | const assert = require('assert'); 6 | const path = require('path'); 7 | const AsyncTestUtil = require('async-test-util'); 8 | 9 | describe('typings.test.ts', () => { 10 | const mainPath = path.join(__dirname, '../'); 11 | const codeBase = ` 12 | import { 13 | BroadcastChannel, 14 | createLeaderElection, 15 | clearNodeFolder 16 | } from '${mainPath}'; 17 | declare type Message = { 18 | foo: string; 19 | }; 20 | `; 21 | const transpileCode = async (code) => { 22 | const spawn = require('child-process-promise').spawn; 23 | const stdout = []; 24 | const stderr = []; 25 | 26 | const tsConfig = { 27 | target: 'es6', 28 | strict: true, 29 | isolatedModules: false 30 | }; 31 | const promise = spawn('ts-node', [ 32 | '--compiler-options', JSON.stringify(tsConfig), 33 | '-e', codeBase + '\n' + code 34 | ]); 35 | const childProcess = promise.childProcess; 36 | childProcess.stdout.on('data', data => { 37 | // console.dir(data.toString()); 38 | stdout.push(data.toString()); 39 | }); 40 | childProcess.stderr.on('data', data => { 41 | // console.log('err:'); 42 | // console.dir(data.toString()); 43 | stderr.push(data.toString()); 44 | }); 45 | try { 46 | await promise; 47 | } catch (err) { 48 | throw new Error(`could not run 49 | # Error: ${err} 50 | # Output: ${stdout} 51 | # ErrOut: ${stderr} 52 | `); 53 | } 54 | }; 55 | describe('basic', () => { 56 | it('should sucess on basic test', async () => { 57 | await transpileCode('console.log("Hello, world!")'); 58 | }); 59 | it('should fail on broken code', async () => { 60 | const brokenCode = ` 61 | let x: string = 'foo'; 62 | x = 1337; 63 | `; 64 | let thrown = false; 65 | try { 66 | await transpileCode(brokenCode); 67 | } catch (err) { 68 | thrown = true; 69 | } 70 | assert.ok(thrown); 71 | }); 72 | }); 73 | describe('statics', () => { 74 | it('.clearNodeFolder()', async () => { 75 | const code = ` 76 | (async() => { 77 | let b: boolean = false; 78 | b = await clearNodeFolder(); 79 | })(); 80 | `; 81 | await transpileCode(code); 82 | }); 83 | 84 | }); 85 | describe('non-typed channel', () => { 86 | it('should be ok to create post and recieve', async () => { 87 | const code = ` 88 | (async() => { 89 | const channel = new BroadcastChannel('foobar'); 90 | const emitted: any[] = []; 91 | channel.onmessage = msg => emitted.push(msg); 92 | await channel.postMessage({foo: 'bar'}); 93 | channel.close(); 94 | })(); 95 | `; 96 | await transpileCode(code); 97 | }); 98 | it('should not allow to set wrong onmessage', async () => { 99 | const code = ` 100 | (async() => { 101 | const channel = new BroadcastChannel('foobar'); 102 | 103 | const emitted: any[] = []; 104 | channel.onmessage = {}; 105 | await channel.postMessage({foo: 'bar'}); 106 | channel.close(); 107 | })(); 108 | `; 109 | await AsyncTestUtil.assertThrows( 110 | () => transpileCode(code) 111 | ); 112 | }); 113 | }); 114 | describe('typed channel', () => { 115 | it('should be ok to create and post', async () => { 116 | const code = ` 117 | (async() => { 118 | const channel = new BroadcastChannel('foobar'); 119 | await channel.postMessage({foo: 'bar'}); 120 | channel.close(); 121 | })(); 122 | `; 123 | await transpileCode(code); 124 | }); 125 | it('should be ok to recieve', async () => { 126 | const code = ` 127 | (async() => { 128 | const channel: BroadcastChannel = new BroadcastChannel('foobar'); 129 | const emitted: Message[] = []; 130 | channel.onmessage = msg => { 131 | const f: string = msg.foo; 132 | emitted.push(msg); 133 | }; 134 | channel.close(); 135 | })(); 136 | `; 137 | await transpileCode(code); 138 | }); 139 | it('should not allow to post wrong message', async () => { 140 | const code = ` 141 | (async() => { 142 | const channel = new BroadcastChannel('foobar'); 143 | await channel.postMessage({x: 42}); 144 | channel.close(); 145 | })(); 146 | `; 147 | await AsyncTestUtil.assertThrows( 148 | () => transpileCode(code) 149 | ); 150 | }); 151 | }); 152 | describe('LeaderElection', () => { 153 | it('call all methods', async () => { 154 | const code = ` 155 | (async() => { 156 | const channel = new BroadcastChannel('foobar'); 157 | const elector = createLeaderElection(channel, {}); 158 | await elector.awaitLeadership(); 159 | await elector.die(); 160 | channel.close(); 161 | })(); 162 | `; 163 | await transpileCode(code); 164 | }); 165 | }); 166 | }); 167 | -------------------------------------------------------------------------------- /test/unit.test.js: -------------------------------------------------------------------------------- 1 | require('./unit/custom.method.test'); 2 | require('./unit/node.method.test.js'); 3 | require('./unit/native.method.test.js'); 4 | require('./unit/indexed-db.method.test.js'); 5 | require('./unit/localstorage.method.test.js'); 6 | -------------------------------------------------------------------------------- /test/unit/custom.method.test.js: -------------------------------------------------------------------------------- 1 | const AsyncTestUtil = require('async-test-util'); 2 | const assert = require('assert'); 3 | const { 4 | BroadcastChannel 5 | } = require('../../'); 6 | 7 | describe('unit/custom.method.test.js', () => { 8 | describe('custom methods', () => { 9 | it('should select provided method', () => { 10 | const channelName = AsyncTestUtil.randomString(12); 11 | const method = { 12 | type: 'custom', 13 | canBeUsed: () => true, 14 | create: () => ({}), 15 | close: () => { } 16 | }; 17 | const channel = new BroadcastChannel(channelName, { methods: method }); 18 | assert.equal(channel.method, method); 19 | channel.close(); 20 | }); 21 | it('should select one of the provided methods', () => { 22 | const channelName = AsyncTestUtil.randomString(12); 23 | const method = { 24 | type: 'custom', 25 | canBeUsed: () => true, 26 | create: () => ({}), 27 | close: () => { } 28 | }; 29 | const channel = new BroadcastChannel(channelName, { methods: [method] }); 30 | assert.equal(channel.method, method); 31 | channel.close(); 32 | }); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /test/unit/localstorage.method.test.js: -------------------------------------------------------------------------------- 1 | const AsyncTestUtil = require('async-test-util'); 2 | const assert = require('assert'); 3 | const isNode = require('detect-node'); 4 | const LocalstorageMethod = require('../../dist/lib/methods/localstorage.js'); 5 | 6 | describe('unit/localstorage.method.test.js', () => { 7 | if (isNode) return; 8 | describe('.getLocalStorage()', () => { 9 | it('should always get a object', () => { 10 | const ls = LocalstorageMethod.getLocalStorage(); 11 | assert.ok(ls); 12 | assert.equal(typeof ls.setItem, 'function'); 13 | }); 14 | }); 15 | describe('.postMessage()', () => { 16 | it('should set the message', async () => { 17 | const channelState = { 18 | channelName: AsyncTestUtil.randomString(10), 19 | uuid: AsyncTestUtil.randomString(10) 20 | }; 21 | const json = { foo: 'bar' }; 22 | await LocalstorageMethod.postMessage( 23 | channelState, 24 | json 25 | ); 26 | const ls = LocalstorageMethod.getLocalStorage(); 27 | const key = LocalstorageMethod.storageKey(channelState.channelName); 28 | const value = JSON.parse(ls.getItem(key)); 29 | assert.equal(value.data.foo, 'bar'); 30 | }); 31 | it('should fire an event', async () => { 32 | const channelState = { 33 | channelName: AsyncTestUtil.randomString(10), 34 | uuid: AsyncTestUtil.randomString(10) 35 | }; 36 | const json = { foo: 'bar' }; 37 | 38 | const emitted = []; 39 | const listener = LocalstorageMethod.addStorageEventListener( 40 | channelState.channelName, 41 | ev => { 42 | emitted.push(ev); 43 | } 44 | ); 45 | 46 | LocalstorageMethod.postMessage( 47 | channelState, 48 | json 49 | ); 50 | 51 | await AsyncTestUtil.waitUntil(() => emitted.length === 1); 52 | assert.equal(emitted[0].data.foo, 'bar'); 53 | 54 | LocalstorageMethod.removeStorageEventListener(listener); 55 | }); 56 | }); 57 | describe('.create()', () => { 58 | it('create an instance', async () => { 59 | const channelName = AsyncTestUtil.randomString(10); 60 | const state = LocalstorageMethod.create(channelName); 61 | assert.ok(state.uuid); 62 | LocalstorageMethod.close(state); 63 | }); 64 | }); 65 | describe('.onMessage()', () => { 66 | it('should emit to the other channel', async () => { 67 | const channelName = AsyncTestUtil.randomString(12); 68 | const channelState1 = await LocalstorageMethod.create(channelName); 69 | const channelState2 = await LocalstorageMethod.create(channelName); 70 | 71 | const emitted = []; 72 | LocalstorageMethod.onMessage( 73 | channelState2, 74 | msg => { 75 | emitted.push(msg); 76 | console.log('was emitted'); 77 | }, 78 | new Date().getTime() 79 | ); 80 | const json = { 81 | foo: 'bar' 82 | }; 83 | LocalstorageMethod.postMessage(channelState1, json); 84 | 85 | await AsyncTestUtil.waitUntil(() => emitted.length === 1); 86 | 87 | assert.deepEqual(emitted[0], json); 88 | 89 | LocalstorageMethod.close(channelState1); 90 | LocalstorageMethod.close(channelState2); 91 | }); 92 | }); 93 | }); 94 | -------------------------------------------------------------------------------- /test/unit/native.method.test.js: -------------------------------------------------------------------------------- 1 | // const AsyncTestUtil = require('async-test-util'); 2 | // const assert = require('assert'); 3 | const isNode = require('detect-node'); 4 | 5 | describe('unit/native.method.test.js', () => { 6 | /** 7 | * do not run in node-tests 8 | */ 9 | if (isNode) return; 10 | }); 11 | -------------------------------------------------------------------------------- /types/broadcast-channel.d.ts: -------------------------------------------------------------------------------- 1 | declare type MethodType = 'node' | 'idb' | 'native' | 'localstorage' | 'simulate'; 2 | 3 | 4 | 5 | interface BroadcastChannelEventMap { 6 | "message": MessageEvent; 7 | "messageerror": MessageEvent; 8 | } 9 | 10 | export interface BroadcastMethod { 11 | type: string; 12 | microSeconds(): number; 13 | create(channelName: string, options: BroadcastChannelOptions): Promise | State; 14 | close(channelState: State): void; 15 | onMessage(channelState: State, callback: (args: any) => void): void; 16 | postMessage(channelState: State, message: any): Promise; 17 | canBeUsed(): boolean; 18 | averageResponseTime(): number; 19 | } 20 | 21 | export type BroadcastChannelOptions = { 22 | type?: MethodType, 23 | methods?: BroadcastMethod[] | BroadcastMethod, 24 | webWorkerSupport?: boolean; 25 | prepareDelay?: number; 26 | node?: { 27 | ttl?: number; 28 | useFastPath?: boolean; 29 | /** 30 | * Opening too many write files will throw an error. 31 | * So we ensure we throttle to have a max limit on writes. 32 | * @link https://stackoverflow.com/questions/8965606/node-and-error-emfile-too-many-open-files 33 | */ 34 | maxParallelWrites?: number; 35 | }; 36 | idb?: { 37 | ttl?: number; 38 | fallbackInterval?: number; 39 | onclose?: () => void; 40 | }; 41 | }; 42 | 43 | declare type EventContext = 'message' | 'internal' | 'leader'; 44 | 45 | declare type OnMessageHandler = ((this: BroadcastChannel, ev: T) => any) | null; 46 | 47 | /** 48 | * api as defined in 49 | * @link https://html.spec.whatwg.org/multipage/web-messaging.html#broadcasting-to-other-browsing-contexts 50 | * @link https://github.com/Microsoft/TypeScript/blob/master/src/lib/webworker.generated.d.ts#L325 51 | */ 52 | export class BroadcastChannel { 53 | constructor(name: string, opts?: BroadcastChannelOptions); 54 | readonly id: number; 55 | readonly name: string; 56 | readonly options: BroadcastChannelOptions; 57 | readonly type: MethodType; 58 | readonly isClosed: boolean; 59 | 60 | postMessage(msg: T): Promise; 61 | close(): Promise; 62 | 63 | onmessage: OnMessageHandler; 64 | 65 | // not defined in the official standard 66 | addEventListener(type: EventContext, handler: OnMessageHandler): void; 67 | removeEventListener(type: EventContext, handler: OnMessageHandler): void; 68 | 69 | } 70 | // statics 71 | export function clearNodeFolder(opts?: BroadcastChannelOptions): Promise; 72 | export function enforceOptions(opts?: BroadcastChannelOptions | false | null): void; 73 | 74 | export const OPEN_BROADCAST_CHANNELS: Set; 75 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './broadcast-channel'; 2 | export * from './leader-election'; -------------------------------------------------------------------------------- /types/leader-election.d.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BroadcastChannel, 3 | OnMessageHandler 4 | } from './broadcast-channel'; 5 | 6 | export type LeaderElectionOptions = { 7 | /** 8 | * Normally, when the leading JavaScript process dies, it will send an I-am-dead 9 | * message to the other LeaderElectors, so that they can elect a new leader. 10 | * On rare cases, when the JavaScript process exits ungracefully, it can happen 11 | * that the other electors do not get a dead-message. 12 | * So we have to also run the election cycle in an interval to ensure 13 | * we never stuck on a state where noone is leader and noone is trying to get elected. 14 | */ 15 | fallbackInterval?: number; 16 | /** 17 | * This timer value is used when resolving which instance should be leader. 18 | * In case when your application elects more than one leader increase this value. 19 | */ 20 | responseTime?: number; 21 | }; 22 | 23 | export declare class LeaderElector { 24 | 25 | /** 26 | * The broadcastChannel with which the 27 | * leader elector was created. 28 | */ 29 | readonly broadcastChannel: BroadcastChannel; 30 | 31 | /** 32 | * IMPORTANT: The leader election is lazy, 33 | * it will not start before you call awaitLeadership() 34 | * so isLeader will never become true then. 35 | */ 36 | readonly isLeader: boolean; 37 | 38 | /** 39 | * Returns true if this or another instance is leader. 40 | * False if there is no leader at the moment 41 | * and we must wait for the election. 42 | */ 43 | hasLeader(): Promise; 44 | 45 | readonly isDead: boolean; 46 | readonly token: string; 47 | 48 | applyOnce(isFromFallbackInterval?: boolean): Promise; 49 | awaitLeadership(): Promise; 50 | die(): Promise; 51 | 52 | /** 53 | * Add an event handler that is run 54 | * when it is detected that there are duplicate leaders 55 | */ 56 | onduplicate: OnMessageHandler; 57 | } 58 | 59 | type CreateFunction = (broadcastChannel: BroadcastChannel, options?: LeaderElectionOptions) => LeaderElector; 60 | 61 | export const createLeaderElection: CreateFunction; 62 | --------------------------------------------------------------------------------