├── .nvmrc
├── test-electron
├── .npmrc
├── .gitignore
├── index.html
├── page.js
├── package.json
├── test
│ ├── render.test.js
│ └── spec.js
└── main.js
├── .npmrc
├── dist
├── esnode
│ ├── package.json
│ ├── methods
│ │ ├── cookies.js
│ │ ├── simulate.js
│ │ ├── native.js
│ │ └── localstorage.js
│ ├── index.js
│ ├── browserify.index.js
│ ├── index.es5.js
│ ├── options.js
│ ├── method-chooser.js
│ ├── leader-election-util.js
│ ├── util.js
│ ├── leader-election-web-lock.js
│ └── broadcast-channel.js
├── esbrowser
│ ├── package.json
│ ├── methods
│ │ ├── cookies.js
│ │ ├── simulate.js
│ │ ├── native.js
│ │ └── localstorage.js
│ ├── index.js
│ ├── browserify.index.js
│ ├── index.es5.js
│ ├── options.js
│ ├── method-chooser.js
│ ├── leader-election-util.js
│ ├── util.js
│ └── leader-election-web-lock.js
├── lib
│ ├── methods
│ │ ├── cookies.js
│ │ ├── simulate.js
│ │ ├── native.js
│ │ └── localstorage.js
│ ├── browserify.index.js
│ ├── index.es5.js
│ ├── index.js
│ ├── options.js
│ ├── leader-election-util.js
│ ├── util.js
│ ├── method-chooser.js
│ └── leader-election-web-lock.js
└── es5node
│ ├── methods
│ ├── cookies.js
│ ├── simulate.js
│ ├── native.js
│ └── localstorage.js
│ ├── browserify.index.js
│ ├── index.es5.js
│ ├── index.js
│ ├── options.js
│ ├── leader-election-util.js
│ ├── util.js
│ ├── method-chooser.js
│ └── leader-election-web-lock.js
├── test
├── .eslintrc
├── unit.test.js
├── module.cjs.test.js
├── module.esm.test.mjs
├── unit
│ ├── native.method.test.js
│ ├── custom.method.test.js
│ └── localstorage.method.test.js
├── scripts
│ ├── windows.sh
│ ├── util.js
│ ├── leader-iframe.js
│ ├── iframe.js
│ ├── index.js
│ └── worker.js
├── index.test.js
├── simple.test.js
├── close.test.js
├── test-deno.js
├── issues.test.js
├── performance.test.js
├── e2e.test.js
└── typings.test.js
├── docs
├── kirby.gif
├── files
│ ├── demo.gif
│ ├── icon.png
│ └── leader-election.gif
├── iframe.html
├── leader-iframe.html
├── index.html
└── e2e.html
├── .eslintignore
├── types
├── index.d.ts
├── leader-election.d.ts
└── broadcast-channel.d.ts
├── src
├── methods
│ ├── cookies.js
│ ├── simulate.js
│ ├── native.js
│ └── localstorage.js
├── browserify.index.js
├── index.js
├── index.es5.js
├── options.js
├── method-chooser.js
├── leader-election-util.js
├── util.js
└── leader-election-web-lock.js
├── .github
├── FUNDING.yml
├── workflows
│ ├── prevent-commit-to-generated.yml
│ └── main.yml
├── PULL_REQUEST_TEMPLATE.md
└── ISSUE_TEMPLATE.md
├── .gitignore
├── .npmignore
├── config
├── rollup.config.mjs
├── webpack.config.js
└── karma.conf.js
├── renovate.json
├── LICENSE
├── README.md
├── babel.config.js
├── CHANGELOG.md
└── perf.txt
/.nvmrc:
--------------------------------------------------------------------------------
1 | 22.21.1
2 |
--------------------------------------------------------------------------------
/test-electron/.npmrc:
--------------------------------------------------------------------------------
1 | package-lock=false
2 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | unsafe-perm = true
2 | package-lock=false
3 |
--------------------------------------------------------------------------------
/dist/esnode/package.json:
--------------------------------------------------------------------------------
1 | { "type": "module", "sideEffects": false }
2 |
--------------------------------------------------------------------------------
/dist/esbrowser/package.json:
--------------------------------------------------------------------------------
1 | { "type": "module", "sideEffects": false }
2 |
--------------------------------------------------------------------------------
/test-electron/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | .DS_Store
3 | log.txt
4 | config.json
--------------------------------------------------------------------------------
/test/.eslintrc:
--------------------------------------------------------------------------------
1 | env:
2 | node: true
3 | mocha: true
4 | jest: true
5 |
--------------------------------------------------------------------------------
/docs/kirby.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pubkey/broadcast-channel/HEAD/docs/kirby.gif
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | dist/
2 | node_modules/
3 | tmp/
4 | test/scripts/worker.js
5 | test/e2e.test.js
--------------------------------------------------------------------------------
/types/index.d.ts:
--------------------------------------------------------------------------------
1 | export * from './broadcast-channel';
2 | export * from './leader-election';
--------------------------------------------------------------------------------
/docs/files/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pubkey/broadcast-channel/HEAD/docs/files/demo.gif
--------------------------------------------------------------------------------
/docs/files/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pubkey/broadcast-channel/HEAD/docs/files/icon.png
--------------------------------------------------------------------------------
/src/methods/cookies.js:
--------------------------------------------------------------------------------
1 | /**
2 | * if you really need this method,
3 | * implement it!
4 | */
5 |
--------------------------------------------------------------------------------
/dist/esnode/methods/cookies.js:
--------------------------------------------------------------------------------
1 | /**
2 | * if you really need this method,
3 | * implement it!
4 | */
--------------------------------------------------------------------------------
/dist/esbrowser/methods/cookies.js:
--------------------------------------------------------------------------------
1 | /**
2 | * if you really need this method,
3 | * implement it!
4 | */
--------------------------------------------------------------------------------
/dist/lib/methods/cookies.js:
--------------------------------------------------------------------------------
1 | /**
2 | * if you really need this method,
3 | * implement it!
4 | */
5 | "use strict";
--------------------------------------------------------------------------------
/dist/es5node/methods/cookies.js:
--------------------------------------------------------------------------------
1 | /**
2 | * if you really need this method,
3 | * implement it!
4 | */
5 | "use strict";
--------------------------------------------------------------------------------
/docs/files/leader-election.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pubkey/broadcast-channel/HEAD/docs/files/leader-election.gif
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: pubkey
4 | custom: ["https://rxdb.info/consulting"]
5 |
--------------------------------------------------------------------------------
/docs/iframe.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/docs/leader-iframe.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.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
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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';
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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;
--------------------------------------------------------------------------------
/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;
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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;
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/test-electron/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Electron Test
6 |
7 |
8 |
9 |
10 |
14 |
15 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/.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@v5
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | };
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | };
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 | };
--------------------------------------------------------------------------------
/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/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-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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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");
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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");
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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/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/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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
27 |
28 |
29 |
30 | 
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 |
49 |
50 |
51 | The JavaScript Database
52 |
53 |
54 |
--------------------------------------------------------------------------------
/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/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-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/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/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 | };
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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-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/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/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 | };
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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 | };
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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/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 _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function _interopRequireWildcard(e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, "default": e }; if (null === e || "object" != _typeof(e) && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (var _t2 in e) "default" !== _t2 && {}.hasOwnProperty.call(e, _t2) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, _t2)) && (i.get || i.set) ? o(f, _t2, i) : f[_t2] = e[_t2]); return f; })(e, t); }
13 | // the line below will be removed from es5/browser builds
14 |
15 | // order is important
16 | var METHODS = [_native.NativeMethod,
17 | // fastest
18 | _indexedDb.IndexedDBMethod, _localstorage.LocalstorageMethod];
19 | function chooseMethod(options) {
20 | var chooseMethods = [].concat(options.methods, METHODS).filter(Boolean);
21 |
22 | // the line below will be removed from es5/browser builds
23 |
24 | // directly chosen
25 | if (options.type) {
26 | if (options.type === 'simulate') {
27 | // only use simulate-method if directly chosen
28 | return _simulate.SimulateMethod;
29 | }
30 | var ret = chooseMethods.find(function (m) {
31 | return m.type === options.type;
32 | });
33 | if (!ret) throw new Error('method-type ' + options.type + ' not found');else return ret;
34 | }
35 |
36 | /**
37 | * if no webworker support is needed,
38 | * remove idb from the list so that localstorage will be chosen
39 | */
40 | if (!options.webWorkerSupport) {
41 | chooseMethods = chooseMethods.filter(function (m) {
42 | return m.type !== 'idb';
43 | });
44 | }
45 | var useMethod = chooseMethods.find(function (method) {
46 | return method.canBeUsed();
47 | });
48 | if (!useMethod) {
49 | throw new Error("No usable method found in " + JSON.stringify(METHODS.map(function (m) {
50 | return m.type;
51 | })));
52 | } else {
53 | return useMethod;
54 | }
55 | }
--------------------------------------------------------------------------------
/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 _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function _interopRequireWildcard(e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, "default": e }; if (null === e || "object" != _typeof(e) && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (var _t2 in e) "default" !== _t2 && {}.hasOwnProperty.call(e, _t2) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, _t2)) && (i.get || i.set) ? o(f, _t2, i) : f[_t2] = e[_t2]); return f; })(e, t); }
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 | chooseMethods.push(NodeMethod);
25 |
26 | // directly chosen
27 | if (options.type) {
28 | if (options.type === 'simulate') {
29 | // only use simulate-method if directly chosen
30 | return _simulate.SimulateMethod;
31 | }
32 | var ret = chooseMethods.find(function (m) {
33 | return m.type === options.type;
34 | });
35 | if (!ret) throw new Error('method-type ' + options.type + ' not found');else return ret;
36 | }
37 |
38 | /**
39 | * if no webworker support is needed,
40 | * remove idb from the list so that localstorage will be chosen
41 | */
42 | if (!options.webWorkerSupport) {
43 | chooseMethods = chooseMethods.filter(function (m) {
44 | return m.type !== 'idb';
45 | });
46 | }
47 | var useMethod = chooseMethods.find(function (method) {
48 | return method.canBeUsed();
49 | });
50 | if (!useMethod) {
51 | throw new Error("No usable method found in " + JSON.stringify(METHODS.map(function (m) {
52 | return m.type;
53 | })));
54 | } else {
55 | return useMethod;
56 | }
57 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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, reject) {
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 (err) {
60 | if (_this3._wKMC.rej) {
61 | _this3._wKMC.rej(err);
62 | }
63 | reject(err);
64 | });
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 sendLeaderMessage(this, 'death');
93 | }
94 | };
--------------------------------------------------------------------------------
/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, reject) {
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 (err) {
60 | if (_this3._wKMC.rej) {
61 | _this3._wKMC.rej(err);
62 | }
63 | reject(err);
64 | });
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 sendLeaderMessage(this, 'death');
93 | }
94 | };
--------------------------------------------------------------------------------
/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 |
Submit
79 |
80 |
81 |
82 |
83 |
84 |
95 |
113 |
114 |
115 |
116 |
117 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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, reject) => {
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((err) => {
68 | if (this._wKMC.rej) {
69 | this._wKMC.rej(err);
70 | }
71 | reject(err);
72 | });
73 | });
74 | }
75 | return this._wLMP;
76 | },
77 |
78 | set onduplicate(_fn) {
79 | // Do nothing because there are no duplicates in the WebLock version
80 | },
81 | die() {
82 | this._lstns.forEach(listener => this.broadcastChannel.removeEventListener('internal', listener));
83 | this._lstns = [];
84 | this._unl.forEach(uFn => uFn.remove());
85 | this._unl = [];
86 | if (this.isLeader) {
87 | this.isLeader = false;
88 | }
89 | this.isDead = true;
90 | if (this._wKMC.res) {
91 | this._wKMC.res();
92 | }
93 | if (this._wKMC.c) {
94 | this._wKMC.c.abort('LeaderElectionWebLock.die() called');
95 | }
96 | return sendLeaderMessage(this, 'death');
97 | }
98 | };
99 |
--------------------------------------------------------------------------------
/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, reject) {
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 (err) {
65 | if (_this3._wKMC.rej) {
66 | _this3._wKMC.rej(err);
67 | }
68 | reject(err);
69 | });
70 | });
71 | }
72 | return this._wLMP;
73 | },
74 | set onduplicate(_fn) {
75 | // Do nothing because there are no duplicates in the WebLock version
76 | },
77 | die: function die() {
78 | var _this4 = this;
79 | this._lstns.forEach(function (listener) {
80 | return _this4.broadcastChannel.removeEventListener('internal', listener);
81 | });
82 | this._lstns = [];
83 | this._unl.forEach(function (uFn) {
84 | return uFn.remove();
85 | });
86 | this._unl = [];
87 | if (this.isLeader) {
88 | this.isLeader = false;
89 | }
90 | this.isDead = true;
91 | if (this._wKMC.res) {
92 | this._wKMC.res();
93 | }
94 | if (this._wKMC.c) {
95 | this._wKMC.c.abort('LeaderElectionWebLock.die() called');
96 | }
97 | return (0, _leaderElectionUtil.sendLeaderMessage)(this, 'death');
98 | }
99 | };
--------------------------------------------------------------------------------
/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, reject) {
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 (err) {
65 | if (_this3._wKMC.rej) {
66 | _this3._wKMC.rej(err);
67 | }
68 | reject(err);
69 | });
70 | });
71 | }
72 | return this._wLMP;
73 | },
74 | set onduplicate(_fn) {
75 | // Do nothing because there are no duplicates in the WebLock version
76 | },
77 | die: function die() {
78 | var _this4 = this;
79 | this._lstns.forEach(function (listener) {
80 | return _this4.broadcastChannel.removeEventListener('internal', listener);
81 | });
82 | this._lstns = [];
83 | this._unl.forEach(function (uFn) {
84 | return uFn.remove();
85 | });
86 | this._unl = [];
87 | if (this.isLeader) {
88 | this.isLeader = false;
89 | }
90 | this.isDead = true;
91 | if (this._wKMC.res) {
92 | this._wKMC.res();
93 | }
94 | if (this._wKMC.c) {
95 | this._wKMC.c.abort('LeaderElectionWebLock.die() called');
96 | }
97 | return (0, _leaderElectionUtil.sendLeaderMessage)(this, 'death');
98 | }
99 | };
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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/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 | };
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/docs/e2e.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
66 |
67 |
68 |
69 |
116 |
120 |
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 | };
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # CHANGELOG
2 |
3 |
4 | ## X.X.X (comming soon)
5 |
6 | ## 7.2.0 (27 October 2025)
7 |
8 | - fix(leader-election-web-lock): rethrow WebLocks acquisition errors to avoid stuck leader election [#1360](https://github.com/pubkey/broadcast-channel/pull/1360)
9 |
10 | ## 7.0.0 (27 November 2023)
11 |
12 | - CHANGE do not emit messages that have been existed before the channel was created.
13 |
14 | ## 6.0.0 (30 October 2023)
15 |
16 | - ADD support for the Deno runtime
17 |
18 | ## 5.5.1 (23 October 2023)
19 |
20 | - REPLACE `new Date().getTime()` with `Date.now()` which is faster
21 |
22 | ## 5.5.0 (17 October 2023)
23 |
24 | - Add `sideEffects: false`
25 | ## 5.4.0 (10 October 2023)
26 |
27 | - FIX import of `p-queue` throws `is not a constructor`
28 |
29 | ## 5.3.0 (18 August 2023)
30 |
31 | https://github.com/pubkey/broadcast-channel/pull/1243
32 |
33 | ## 5.2.0 (11 August 2023)
34 | https://github.com/pubkey/broadcast-channel/pull/1237
35 |
36 | ## 5.1.0 (25 April 2023)
37 |
38 | - REFACTOR check for native method [#1157](https://github.com/pubkey/broadcast-channel/pull/1157)
39 |
40 | ## 5.0.1 (23 March 2023)
41 |
42 | - FIX hasLeader() is mixing up states
43 |
44 | ## 5.0.0 (23 March 2023)
45 |
46 | - Use [Web Locks API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Locks_API) for leader election if possible.
47 | - `LeaderElector.hasLeader` is now a function that returns a `Promise`.
48 |
49 | ## 4.20.1 (6 January 2023)
50 |
51 | - FIX exports order
52 |
53 | ## 4.20.0 (6 January 2023)
54 |
55 | - FIX typings did not work with `"moduleResolution": "NodeNext"`
56 |
57 | ## 4.19.0 (28 December 2022)
58 |
59 | - Updated dependencies
60 |
61 | ## 4.18.1 (31 October 2022)
62 |
63 | - Updated dependencies
64 |
65 | ## 4.18.0 (6 October 2022)
66 |
67 |
68 | - 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)
69 |
70 |
71 | ## 4.17.0 (13 September 2022)
72 |
73 | - REMOVE the `isNode` utility function so that we do not access the `process` variable in browsers.
74 |
75 | ## 4.16.0 (13 September 2022)
76 |
77 | - Rerelase because npm got stuck
78 | ## 4.15.0 (13 September 2022)
79 |
80 | - Remove `microtime` dependency [https://github.com/pubkey/broadcast-channel/pull/1036](#1036) [jaredperreault-okta](https://github.com/jaredperreault-okta)
81 |
82 | ## 4.14.0 (18 Juli June 2022)
83 |
84 | - Updated dependencies.
85 |
86 | ## 4.13.0 (1 June 2022)
87 |
88 | - FIX ES module for Node.js [#972](https://github.com/pubkey/broadcast-channel/pull/972)
89 |
90 | ## 4.12.0 (25 May 2022)
91 |
92 | - FIX ES module for Node.js Thanks [denysoblohin-okta](https://github.com/denysoblohin-okta)
93 |
94 | ## 4.11.0 (12 April 2022)
95 |
96 | - Replaced `nano-time` with `microtime`.
97 | - Improve IndexedDB method performance.
98 |
99 | ## 4.10.0 (3 February 2022)
100 |
101 | - Improve error message when calling `postMessage` to a closed channel.
102 |
103 | ## 4.9.0 (23 December 2021)
104 |
105 | Bugfixes:
106 | - When listening to messages directly, responses that where send directly after `addEventListener()` where missing because of inaccurate JavaScript timing.
107 |
108 | ## 4.8.0 (15 December 2021)
109 |
110 | Changes:
111 | - Better determine the correct `responseTime` to use to make it less likely to elect duplicate leaders.
112 |
113 | ## 4.7.1 (13 December 2021)
114 |
115 | Bugfixes:
116 | - Remove useless log at leader election fallback interval.
117 |
118 | ## 4.7.0 (3 December 2021)
119 |
120 | Bugfixes:
121 | - Prevent `EMFILE, too many open files` error when writing many messages at once.
122 |
123 | ## 4.6.0 (2 December 2021)
124 |
125 | Other:
126 | - Added `broadcastChannel.id()` for debugging
127 |
128 | Bugfixes:
129 | - Refactor `applyOnce()` queue to ensure we do not run more often then needed.
130 |
131 | ## 4.5.0 (5 November 2021)
132 |
133 | Bugfixes:
134 | - Running `applyOnce()` in a loop must not fully block the JavaScript process.
135 |
136 | ## 4.4.0 (2 November 2021)
137 |
138 | Other:
139 | - Replaced `js-sha` with node's `crypto` module.
140 |
141 | ## 4.3.1 (30 October 2021)
142 |
143 | Bugfixes:
144 | - Fixed broken promise rejection.
145 |
146 | ## 4.3.0 (30 October 2021)
147 |
148 | Features:
149 | - Added `LeaderElector.hasLeader`
150 | - Added `LeaderElector.broadcastChannel`
151 |
152 | ## 4.2.0 (3 August 2021)
153 |
154 | Bugfixes:
155 | - Fixed Webpack 5 Relative Import Support. Thanks [catrielmuller](https://github.com/catrielmuller)
156 | ## 4.1.0 (2 August 2021)
157 |
158 | Bugfixes:
159 | - Fixed various problems with the module loading. Thanks [benmccann](https://github.com/benmccann) and [chbdetta](https://github.com/chbdetta)
160 |
161 |
162 | ## 4.0.0 (15 July 2021)
163 |
164 | Other:
165 | - Changed entrypoints and method-choosing [#679](https://github.com/pubkey/broadcast-channel/pull/679). Thanks [benmccann](https://github.com/benmccann)
166 |
167 | ## 3.7.0 (13 June 2021)
168 |
169 | Other:
170 | - Moved `ObliviousSet` into [its own npm module](https://www.npmjs.com/package/oblivious-set)
171 |
172 | ## 3.6.0 (19 May 2021)
173 |
174 | Features:
175 | - Added `BroadcastChannel.isClosed` [#544](https://github.com/pubkey/broadcast-channel/issues/544)
176 |
177 | Other:
178 | - Updated dependencies to work with newer node versions
179 |
180 | ## 3.5.3 (11 March 2021)
181 |
182 | Bugfixes:
183 | - Fixed broken typings
184 |
185 | ## 3.5.2 (11 March 2021)
186 |
187 | Bugfixes:
188 | - `BroadcastChannel.close()` waits for all ongoing message sending to be finished before resolving.
189 |
190 | ## 3.5.0 (11 March 2021)
191 |
192 | Features:
193 | - Added `LeaderElector.onduplicate`
194 |
195 | ## 3.4.0 (24 January 2021)
196 |
197 | Bugfixes:
198 | - fix cursor error in Safari [#420](https://github.com/pubkey/broadcast-channel/pull/420)
199 |
200 | ## 3.3.0 (20 October 2020)
201 |
202 | Bugfixes:
203 | - `new BroadcastChannel().close()` should not resolve before all cleanup is done [#348](https://github.com/pubkey/broadcast-channel/pull/348)
204 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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@v5
32 | - name: Setup Node.js environment
33 | uses: actions/setup-node@v4.4.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@v5
104 | - name: Setup Node.js environment
105 | uses: actions/setup-node@v4.4.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@v5
140 | - name: Setup Node.js environment
141 | uses: actions/setup-node@v4.4.0
142 | with:
143 | node-version-file: ".nvmrc"
144 |
145 | # Caching here is disabled because deno corrupts the npm files for unknown reason
146 | # https://docs.github.com/en/free-pro-team@latest/actions/guides/caching-dependencies-to-speed-up-workflows
147 | # - name: Reuse npm cache folder
148 | # uses: actions/cache@v3
149 | # env:
150 | # cache-name: cache-node-deno-modules
151 | # with:
152 | # path: |
153 | # ~/.npm
154 | # ./node_modules
155 | # ./test-electron/node_modules
156 | # # invalidate cache when any package.json changes
157 | # key: ${{ runner.os }}-npm-test-deno-x3-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}
158 | # restore-keys: |
159 | # ${{ runner.os }}-npm-test-deno-x3-${{ env.cache-name }}-
160 | # ${{ runner.os }}-npm-test-deno
161 | # ${{ runner.os }}-test-deno
162 |
163 | - name: install node modules
164 | run: npm install --legacy-peer-deps
165 |
166 | - name: build
167 | run: npm run build
168 |
169 | - name: Reuse deno cache folder
170 | uses: actions/cache@v3
171 | env:
172 | cache-name: cache-deno-modules
173 | with:
174 | path: |
175 | /home/runner/.cache/deno
176 | # do not cache based on package.json because deno install randomly fails
177 | # and it would then never succeed on the first run on dependency updateds
178 | key: ${{ runner.os }}-deno-x3-
179 |
180 | - uses: denoland/setup-deno@v1
181 | with:
182 | # https://github.com/denoland/deno/releases
183 | deno-version: "1.46.3"
184 | - name: run deno tests
185 | run: |
186 | sudo npm i -g cross-env
187 | deno info
188 | npm run test:deno
189 |
190 |
191 | # TODO this does not work atm. fix this.
192 | # - name: test electron
193 | # uses: GabrielBB/xvfb-action@v1
194 | # with:
195 | # working-directory: ./test-electron
196 | # run: npm install --depth 0 --silent && npm run test
197 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------