🎯 MapView v2.0 - Global Deployment Ready

 MAJOR FEATURES:
• Auto-zoom intelligence với smart bounds fitting
• Enhanced 3D GPS markers với pulsing effects
• Professional route display với 6-layer rendering
• Status-based parking icons với availability indicators
• Production-ready build optimizations

🗺️ AUTO-ZOOM FEATURES:
• Smart bounds fitting cho GPS + selected parking
• Adaptive padding (50px) cho visual balance
• Max zoom control (level 16) để tránh quá gần
• Dynamic centering khi không có selection

🎨 ENHANCED VISUALS:
• 3D GPS marker với multi-layer pulse effects
• Advanced parking icons với status colors
• Selection highlighting với animation
• Dimming system cho non-selected items

🛣️ ROUTE SYSTEM:
• OpenRouteService API integration
• Multi-layer route rendering (glow, shadow, main, animated)
• Real-time distance & duration calculation
• Visual route info trong popup

📱 PRODUCTION READY:
• SSR safe với dynamic imports
• Build errors resolved
• Global deployment via Vercel
• Optimized performance

🌍 DEPLOYMENT:
• Vercel: https://whatever-ctk2auuxr-phong12hexdockworks-projects.vercel.app
• Bundle size: 22.8 kB optimized
• Global CDN distribution
• HTTPS enabled

💾 VERSION CONTROL:
• MapView-v2.0.tsx backup created
• MAPVIEW_VERSIONS.md documentation
• Full version history tracking
This commit is contained in:
2025-07-20 19:52:16 +07:00
parent 3203463a6a
commit c65cc97a33
64624 changed files with 7199453 additions and 6462 deletions

View File

@@ -0,0 +1,3 @@
# These are supported funding model platforms
github: pubkey

View File

@@ -0,0 +1,262 @@
<p align="center">
<a href="https://github.com/pubkey/broadcast-channel">
<img src="../docs/files/icon.png" width="150px" />
</a>
</p>
<h1 align="center">BroadcastChannel</h1>
<p align="center">
<strong>A BroadcastChannel to send data between different browser-tabs or nodejs-processes</strong>
<br/>
<span>+ LeaderElection over the channels</span><br />
</p>
<p align="center">
<a href="https://twitter.com/pubkeypubkey">
<img src="https://img.shields.io/twitter/follow/pubkeypubkey.svg?style=social&logo=twitter"
alt="follow on Twitter"></a>
</p>
![demo.gif](../docs/files/demo.gif)
* * *
A BroadcastChannel that allows you to send data between different browser-tabs or nodejs-processes.
- It works completely **client-side** and **offline**.
- Tested on **old browsers**, **new browsers**, **WebWorkers**, **Iframes** and **NodeJs**
This behaves similar to the [BroadcastChannel-API](https://developer.mozilla.org/en-US/docs/Web/API/Broadcast_Channel_API) which is currently only featured in [some browsers](https://caniuse.com/#feat=broadcastchannel).
## Using the BroadcastChannel
```bash
npm install --save broadcast-channel
```
#### Create a channel in one tab/process and send a message.
```ts
import { BroadcastChannel } from 'broadcast-channel';
const channel = new BroadcastChannel('foobar');
channel.postMessage('I am not alone');
```
#### Create a channel with the same name in another tab/process and recieve messages.
```ts
import { BroadcastChannel } from 'broadcast-channel';
const channel = new BroadcastChannel('foobar');
channel.onmessage = msg => console.dir(msg);
// > 'I am not alone'
```
#### Add and remove multiple eventlisteners
```ts
import { BroadcastChannel } from 'broadcast-channel';
const channel = new BroadcastChannel('foobar');
const handler = msg => console.log(msg);
channel.addEventListener('message', handler);
// remove it
channel.removeEventListener('message', handler);
```
#### Close the channel if you do not need it anymore.
Returns a `Promise` that resolved when everything is processed.
```js
await channel.close();
```
#### Set options when creating a channel (optional):
```js
const options = {
type: 'localstorage', // (optional) enforce a type, oneOf['native', 'idb', 'localstorage', 'node']
webWorkerSupport: true; // (optional) set this to false if you know that your channel will never be used in a WebWorker (increases performance)
};
const channel = new BroadcastChannel('foobar', options);
```
#### Create a typed channel in typescript:
```typescript
import { BroadcastChannel } from 'broadcast-channel';
declare type Message = {
foo: string;
};
const channel: BroadcastChannel<Message> = new BroadcastChannel('foobar');
channel.postMessage({
foo: 'bar'
});
```
#### Enforce a options globally
When you use this module in a test-suite, it is recommended to enforce the fast `simulate` method on all channels so your tests run faster. You can do this with `enforceOptions()`. If you set this, all channels have the enforced options, no mather what options are given in the constructor.
```typescript
import { enforceOptions } from 'broadcast-channel';
// enforce this config for all channels
enforceOptions({
type: 'simulate'
});
// reset the enforcement
enforceOptions(null);
```
#### Clear tmp-folder:
When used in NodeJs, the BroadcastChannel will communicate with other processes over filesystem based sockets.
When you create a huge amount of channels, like you would do when running unit tests, you might get problems because there are too many folders in the tmp-directory. Calling `BroadcastChannel.clearNodeFolder()` will clear the tmp-folder and it is recommended to run this at the beginning of your test-suite.
```typescript
import { clearNodeFolder } from 'broadcast-channel';
// jest
beforeAll(async () => {
const hasRun = await clearNodeFolder();
console.log(hasRun); // > true on NodeJs, false on Browsers
})
```
```typescript
import { clearNodeFolder } from 'broadcast-channel';
// mocha
before(async () => {
const hasRun = await clearNodeFolder();
console.log(hasRun); // > true on NodeJs, false on Browsers
})
```
#### Handling IndexedDB onclose events
IndexedDB databases can close unexpectedly for various reasons. This could happen, for example, if the underlying storage is removed or if the user clears the database in the browser's history preferences. Most often we have seen this happen in Mobile Safari. By default, we let the connection close and stop polling for changes. If you would like to continue listening you should close BroadcastChannel and create a new one.
Example of how you might do this:
```typescript
import { BroadcastChannel } from 'broadcast-channel';
let channel;
const createChannel = () => {
channel = new BroadcastChannel(CHANNEL_NAME, {
idb: {
onclose: () => {
// the onclose event is just the IndexedDB closing.
// you should also close the channel before creating
// a new one.
channel.close();
createChannel();
},
},
});
channel.onmessage = message => {
// handle message
};
};
```
## Methods:
Depending in which environment this is used, a proper method is automatically selected to ensure it always works.
| Method | Used in | Description |
| ---------------- | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Native** | [Modern Browsers](https://caniuse.com/broadcastchannel) | If the browser supports the BroadcastChannel-API, this method will be used because it is the fastest |
| **IndexedDB** | [Browsers with WebWorkers](https://caniuse.com/#feat=indexeddb) | If there is no native BroadcastChannel support, the IndexedDB method is used because it supports messaging between browser-tabs, iframes and WebWorkers |
| **LocalStorage** | [Older Browsers](https://caniuse.com/#feat=namevalue-storage) | In older browsers that do not support IndexedDb, a localstorage-method is used |
| **Sockets** | NodeJs | In NodeJs the communication is handled by sockets that send each other messages |
| **Simulate** | none per default | This method simulates the behavior of the other methods but only runs in the current process without sharing data between processes. Use this method in your test-suite because it is much faster. |
## Using the LeaderElection
This module also comes with a leader-election which can be used so elect a leader between different BroadcastChannels.
For example if you have a stable connection from the frontend to your server, you can use the LeaderElection to save server-side performance by only connecting once, even if the user has opened your website in multiple tabs.
In this example the leader is marked with the crown ♛:
![leader-election.gif](../docs/files/leader-election.gif)
Create a channel and an elector.
```ts
import {
BroadcastChannel,
createLeaderElection
} from 'broadcast-channel';
const channel = new BroadcastChannel('foobar');
const elector = createLeaderElection(channel);
```
Wait until the elector becomes leader.
```js
import { createLeaderElection } from 'broadcast-channel';
const elector = createLeaderElection(channel);
elector.awaitLeadership().then(()=> {
console.log('this tab is now leader');
})
```
If more than one tab is becoming leader adjust `LeaderElectionOptions` configuration.
```js
import { createLeaderElection } from 'broadcast-channel';
const elector = createLeaderElection(channel, {
fallbackInterval: 2000, // optional configuration for how often will renegotiation for leader occur
responseTime: 1000, // optional configuration for how long will instances have to respond
});
elector.awaitLeadership().then(()=> {
console.log('this tab is now leader');
})
```
Let the leader die. (automatically happens if the tab is closed or the process exits).
```js
const elector = createLeaderElection(channel);
await elector.die();
```
Handle duplicate leaders. This can happen on rare occurences like when the [CPU is on 100%](https://github.com/pubkey/broadcast-channel/issues/385) for longer time, or the browser [has throttled the javascript timers](https://github.com/pubkey/broadcast-channel/issues/414).
```js
const elector = createLeaderElection(channel);
elector.onduplicate = () => {
alert('have duplicate leaders!');
}
```
## What this is
This module is optimised for:
- **low latency**: When you postMessage on one channel, it should take as low as possible time until other channels recieve the message.
- **lossless**: When you send a message, it should be impossible that the message is lost before other channels recieved it
- **low idle workload**: During the time when no messages are send, there should be a low processor footprint.
## What this is not
- This is not a polyfill. Do not set this module to `window.BroadcastChannel`. This implementation behaves similiar to the [BroadcastChannel-Standard](https://developer.mozilla.org/en-US/docs/Web/API/Broadcast_Channel_API) with these limitations:
- You can only send data that can be `JSON.stringify`-ed.
- While the offical API emits [onmessage-events](https://developer.mozilla.org/en-US/docs/Web/API/BroadcastChannel/onmessage), this module directly emitts the data which was posted
- This is not a replacement for a message queue. If you use this in NodeJs and want send more than 50 messages per second, you should use proper [IPC-Tooling](https://en.wikipedia.org/wiki/Message_queue)
## Browser Support
I have tested this in all browsers that I could find. For ie8 and ie9 you must transpile the code before you can use this. If you want to know if this works with your browser, [open the demo page](https://pubkey.github.io/broadcast-channel/e2e.html).
## Thanks
Thanks to [Hemanth.HM](https://github.com/hemanth) for the module name.

View File

@@ -0,0 +1,91 @@
# This is a basic workflow to help you get started with Actions
name: CI
# Controls when the action will run.
on:
# Triggers the workflow on push or pull request events but only for the master branch
push:
branches: [ master ]
pull_request:
branches: [ master ]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# This workflow contains a single job called "build"
all:
# The type of runner that the job will run on
runs-on: ubuntu-18.04
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v2
# https://docs.github.com/en/free-pro-team@latest/actions/guides/caching-dependencies-to-speed-up-workflows
- name: Reuse npm cache folder
uses: actions/cache@v2
env:
cache-name: cache-node-modules
with:
# reuse the npm-cache and some node_modules folders
path: |
~/.npm
./node_modules
./test-electron/node_modules
# invalidate cache when any package.json changes
key: ${{ runner.os }}-npm-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}
restore-keys: |
${{ runner.os }}-npm-${{ env.cache-name }}-
${{ runner.os }}-npm-
${{ runner.os }}-
# install
- name: install node modules
run: npm install
- name: build
run: npm run build
- name: check build size webpack
run: npm run size:webpack
- name: check build size browserify
run: npm run size:browserify
- name: check build size rollup
run: npm run size:rollup
- name: code format
run: npm run lint
- name: test typings
run: npm run test:typings
- name: test node
run: npm run test:node
- name: test browser
uses: GabrielBB/xvfb-action@v1
with:
working-directory: ./ #optional
run: npm run test:browser
- name: test performance
run: npm run test:performance
- name: test e2e
uses: GabrielBB/xvfb-action@v1
with:
working-directory: ./ #optional
run: npm run test:e2e
# TODO this does not work atm. fix this.
# - name: test electron
# uses: GabrielBB/xvfb-action@v1
# with:
# working-directory: ./test-electron
# run: npm install --depth 0 --silent && npm run test

41
frontend/node_modules/broadcast-channel/CHANGELOG.md generated vendored Normal file
View File

@@ -0,0 +1,41 @@
# CHANGELOG
## X.X.X (comming soon)
## 3.7.0 (13 June 2021)
Other:
- Moved `ObliviousSet` into [its own npm module](https://www.npmjs.com/package/oblivious-set)
## 3.6.0 (19 May 2021)
Features:
- Added `BroadcastChannel.isClosed` [#544](https://github.com/pubkey/broadcast-channel/issues/544)
Other:
- Updated dependencies to work with newer node versions
## 3.5.3 (11 March 2021)
Bugfixes:
- Fixed broken typings
## 3.5.2 (11 March 2021)
Bugfixes:
- `BroadcastChannel.close()` waits for all ongoing message sending to be finished before resolving.
## 3.5.0 (11 March 2021)
Features:
- Added `LeaderElector.onduplicate`
## 3.4.0 (24 January 2021)
Bugfixes:
- fix cursor error in Safari [#420](https://github.com/pubkey/broadcast-channel/pull/420)
## 3.3.0 (20 October 2020)
Bugfixes:
- `new BroadcastChannel().close()` should not resolve before all cleanup is done [#348](https://github.com/pubkey/broadcast-channel/pull/348)

21
frontend/node_modules/broadcast-channel/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 Daniel Meyer
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

36
frontend/node_modules/broadcast-channel/README.md generated vendored Normal file
View File

@@ -0,0 +1,36 @@
<!--
This is a different REDME file which will be published to npm
The one for GitHub is in .github directory.
@link https://stackoverflow.com/a/65676410/3443137
The problem was that google indexed the npm-site instead of the github site
-->
<p align="center">
<a href="https://github.com/pubkey/broadcast-channel">
<img src="./docs/files/icon.png" width="150px" />
</a>
</p>
<h1 align="center">BroadcastChannel</h1>
<p align="center">
<strong>A BroadcastChannel that works in old browsers, new browsers, WebWorkers and NodeJs</strong>
<br/>
<span>+ LeaderElection over the channels</span>
</p>
<p align="center">
<a href="https://twitter.com/pubkeypubkey">
<img src="https://img.shields.io/twitter/follow/pubkeypubkey.svg?style=social&logo=twitter"
alt="follow on Twitter"></a>
</p>
![demo.gif](docs/files/demo.gif)
* * *
A BroadcastChannel that allows you to send data between different browser-tabs or nodejs-processes.
And a LeaderElection over the channels.
# [Read the full documentation on github](https://github.com/pubkey/broadcast-channel)

View File

@@ -0,0 +1,262 @@
import { isPromise } from './util.js';
import { chooseMethod } from './method-chooser.js';
import { fillOptionsWithDefaults } from './options.js';
export var BroadcastChannel = function BroadcastChannel(name, options) {
this.name = name;
if (ENFORCED_OPTIONS) {
options = ENFORCED_OPTIONS;
}
this.options = fillOptionsWithDefaults(options);
this.method = chooseMethod(this.options); // isListening
this._iL = false;
/**
* _onMessageListener
* setting onmessage twice,
* will overwrite the first listener
*/
this._onML = null;
/**
* _addEventListeners
*/
this._addEL = {
message: [],
internal: []
};
/**
* Unsend message promises
* where the sending is still in progress
* @type {Set<Promise>}
*/
this._uMP = new Set();
/**
* _beforeClose
* array of promises that will be awaited
* before the channel is closed
*/
this._befC = [];
/**
* _preparePromise
*/
this._prepP = null;
_prepareChannel(this);
}; // STATICS
/**
* used to identify if someone overwrites
* window.BroadcastChannel with this
* See methods/native.js
*/
BroadcastChannel._pubkey = true;
/**
* clears the tmp-folder if is node
* @return {Promise<boolean>} true if has run, false if not node
*/
export function clearNodeFolder(options) {
options = fillOptionsWithDefaults(options);
var method = chooseMethod(options);
if (method.type === 'node') {
return method.clearNodeFolder().then(function () {
return true;
});
} else {
return Promise.resolve(false);
}
}
/**
* if set, this method is enforced,
* no mather what the options are
*/
var ENFORCED_OPTIONS;
export function enforceOptions(options) {
ENFORCED_OPTIONS = options;
} // PROTOTYPE
BroadcastChannel.prototype = {
postMessage: function postMessage(msg) {
if (this.closed) {
throw new Error('BroadcastChannel.postMessage(): ' + 'Cannot post message after channel has closed');
}
return _post(this, 'message', msg);
},
postInternal: function postInternal(msg) {
return _post(this, 'internal', msg);
},
set onmessage(fn) {
var time = this.method.microSeconds();
var listenObj = {
time: time,
fn: fn
};
_removeListenerObject(this, 'message', this._onML);
if (fn && typeof fn === 'function') {
this._onML = listenObj;
_addListenerObject(this, 'message', listenObj);
} else {
this._onML = null;
}
},
addEventListener: function addEventListener(type, fn) {
var time = this.method.microSeconds();
var listenObj = {
time: time,
fn: fn
};
_addListenerObject(this, type, listenObj);
},
removeEventListener: function removeEventListener(type, fn) {
var obj = this._addEL[type].find(function (obj) {
return obj.fn === fn;
});
_removeListenerObject(this, type, obj);
},
close: function close() {
var _this = this;
if (this.closed) {
return;
}
this.closed = true;
var awaitPrepare = this._prepP ? this._prepP : Promise.resolve();
this._onML = null;
this._addEL.message = [];
return awaitPrepare // wait until all current sending are processed
.then(function () {
return Promise.all(Array.from(_this._uMP));
}) // run before-close hooks
.then(function () {
return Promise.all(_this._befC.map(function (fn) {
return fn();
}));
}) // close the channel
.then(function () {
return _this.method.close(_this._state);
});
},
get type() {
return this.method.type;
},
get isClosed() {
return this.closed;
}
};
/**
* Post a message over the channel
* @returns {Promise} that resolved when the message sending is done
*/
function _post(broadcastChannel, type, msg) {
var time = broadcastChannel.method.microSeconds();
var msgObj = {
time: time,
type: type,
data: msg
};
var awaitPrepare = broadcastChannel._prepP ? broadcastChannel._prepP : Promise.resolve();
return awaitPrepare.then(function () {
var sendPromise = broadcastChannel.method.postMessage(broadcastChannel._state, msgObj); // add/remove to unsend messages list
broadcastChannel._uMP.add(sendPromise);
sendPromise["catch"]().then(function () {
return broadcastChannel._uMP["delete"](sendPromise);
});
return sendPromise;
});
}
function _prepareChannel(channel) {
var maybePromise = channel.method.create(channel.name, channel.options);
if (isPromise(maybePromise)) {
channel._prepP = maybePromise;
maybePromise.then(function (s) {
// used in tests to simulate slow runtime
/*if (channel.options.prepareDelay) {
await new Promise(res => setTimeout(res, this.options.prepareDelay));
}*/
channel._state = s;
});
} else {
channel._state = maybePromise;
}
}
function _hasMessageListeners(channel) {
if (channel._addEL.message.length > 0) return true;
if (channel._addEL.internal.length > 0) return true;
return false;
}
function _addListenerObject(channel, type, obj) {
channel._addEL[type].push(obj);
_startListening(channel);
}
function _removeListenerObject(channel, type, obj) {
channel._addEL[type] = channel._addEL[type].filter(function (o) {
return o !== obj;
});
_stopListening(channel);
}
function _startListening(channel) {
if (!channel._iL && _hasMessageListeners(channel)) {
// someone is listening, start subscribing
var listenerFn = function listenerFn(msgObj) {
channel._addEL[msgObj.type].forEach(function (obj) {
if (msgObj.time >= obj.time) {
obj.fn(msgObj.data);
}
});
};
var time = channel.method.microSeconds();
if (channel._prepP) {
channel._prepP.then(function () {
channel._iL = true;
channel.method.onMessage(channel._state, listenerFn, time);
});
} else {
channel._iL = true;
channel.method.onMessage(channel._state, listenerFn, time);
}
}
}
function _stopListening(channel) {
if (channel._iL && !_hasMessageListeners(channel)) {
// noone is listening, stop subscribing
channel._iL = false;
var time = channel.method.microSeconds();
channel.method.onMessage(channel._state, null, time);
}
}

View File

@@ -0,0 +1,6 @@
var module = require('./index.es5.js');
var BroadcastChannel = module.BroadcastChannel;
var createLeaderElection = module.createLeaderElection;
window['BroadcastChannel2'] = BroadcastChannel;
window['createLeaderElection'] = createLeaderElection;

View File

@@ -0,0 +1,16 @@
/**
* because babel can only export on default-attribute,
* we use this for the non-module-build
* this ensures that users do not have to use
* var BroadcastChannel = require('broadcast-channel').default;
* but
* var BroadcastChannel = require('broadcast-channel');
*/
import { BroadcastChannel, createLeaderElection, clearNodeFolder, enforceOptions, beLeader } from './index.js';
module.exports = {
BroadcastChannel: BroadcastChannel,
createLeaderElection: createLeaderElection,
clearNodeFolder: clearNodeFolder,
enforceOptions: enforceOptions,
beLeader: beLeader
};

View File

@@ -0,0 +1,2 @@
export { BroadcastChannel, clearNodeFolder, enforceOptions } from './broadcast-channel';
export { createLeaderElection, beLeader } from './leader-election';

View File

@@ -0,0 +1,262 @@
import { sleep, randomToken } from './util.js';
import unload from 'unload';
var LeaderElection = function LeaderElection(channel, options) {
this._channel = channel;
this._options = options;
this.isLeader = false;
this.isDead = false;
this.token = randomToken();
this._isApl = false; // _isApplying
this._reApply = false; // things to clean up
this._unl = []; // _unloads
this._lstns = []; // _listeners
this._invs = []; // _intervals
this._dpL = function () {}; // onduplicate listener
this._dpLC = false; // true when onduplicate called
};
LeaderElection.prototype = {
applyOnce: function applyOnce() {
var _this = this;
if (this.isLeader) return Promise.resolve(false);
if (this.isDead) return Promise.resolve(false); // do nothing if already running
if (this._isApl) {
this._reApply = true;
return Promise.resolve(false);
}
this._isApl = true;
var stopCriteria = false;
var recieved = [];
var handleMessage = function handleMessage(msg) {
if (msg.context === 'leader' && msg.token != _this.token) {
recieved.push(msg);
if (msg.action === 'apply') {
// other is applying
if (msg.token > _this.token) {
// other has higher token, stop applying
stopCriteria = true;
}
}
if (msg.action === 'tell') {
// other is already leader
stopCriteria = true;
}
}
};
this._channel.addEventListener('internal', handleMessage);
var ret = _sendMessage(this, 'apply') // send out that this one is applying
.then(function () {
return sleep(_this._options.responseTime);
}) // let others time to respond
.then(function () {
if (stopCriteria) return Promise.reject(new Error());else return _sendMessage(_this, 'apply');
}).then(function () {
return sleep(_this._options.responseTime);
}) // let others time to respond
.then(function () {
if (stopCriteria) return Promise.reject(new Error());else return _sendMessage(_this);
}).then(function () {
return beLeader(_this);
}) // no one disagreed -> this one is now leader
.then(function () {
return true;
})["catch"](function () {
return false;
}) // apply not successfull
.then(function (success) {
_this._channel.removeEventListener('internal', handleMessage);
_this._isApl = false;
if (!success && _this._reApply) {
_this._reApply = false;
return _this.applyOnce();
} else return success;
});
return ret;
},
awaitLeadership: function awaitLeadership() {
if (
/* _awaitLeadershipPromise */
!this._aLP) {
this._aLP = _awaitLeadershipOnce(this);
}
return this._aLP;
},
set onduplicate(fn) {
this._dpL = fn;
},
die: function die() {
var _this2 = this;
if (this.isDead) return;
this.isDead = true;
this._lstns.forEach(function (listener) {
return _this2._channel.removeEventListener('internal', listener);
});
this._invs.forEach(function (interval) {
return clearInterval(interval);
});
this._unl.forEach(function (uFn) {
uFn.remove();
});
return _sendMessage(this, 'death');
}
};
function _awaitLeadershipOnce(leaderElector) {
if (leaderElector.isLeader) return Promise.resolve();
return new Promise(function (res) {
var resolved = false;
function finish() {
if (resolved) {
return;
}
resolved = true;
clearInterval(interval);
leaderElector._channel.removeEventListener('internal', whenDeathListener);
res(true);
} // try once now
leaderElector.applyOnce().then(function () {
if (leaderElector.isLeader) {
finish();
}
}); // try on fallbackInterval
var interval = setInterval(function () {
leaderElector.applyOnce().then(function () {
if (leaderElector.isLeader) {
finish();
}
});
}, leaderElector._options.fallbackInterval);
leaderElector._invs.push(interval); // try when other leader dies
var whenDeathListener = function whenDeathListener(msg) {
if (msg.context === 'leader' && msg.action === 'death') {
leaderElector.applyOnce().then(function () {
if (leaderElector.isLeader) finish();
});
}
};
leaderElector._channel.addEventListener('internal', whenDeathListener);
leaderElector._lstns.push(whenDeathListener);
});
}
/**
* sends and internal message over the broadcast-channel
*/
function _sendMessage(leaderElector, action) {
var msgJson = {
context: 'leader',
action: action,
token: leaderElector.token
};
return leaderElector._channel.postInternal(msgJson);
}
export function beLeader(leaderElector) {
leaderElector.isLeader = true;
var unloadFn = unload.add(function () {
return leaderElector.die();
});
leaderElector._unl.push(unloadFn);
var isLeaderListener = function isLeaderListener(msg) {
if (msg.context === 'leader' && msg.action === 'apply') {
_sendMessage(leaderElector, 'tell');
}
if (msg.context === 'leader' && msg.action === 'tell' && !leaderElector._dpLC) {
/**
* another instance is also leader!
* This can happen on rare events
* like when the CPU is at 100% for long time
* or the tabs are open very long and the browser throttles them.
* @link https://github.com/pubkey/broadcast-channel/issues/414
* @link https://github.com/pubkey/broadcast-channel/issues/385
*/
leaderElector._dpLC = true;
leaderElector._dpL(); // message the lib user so the app can handle the problem
_sendMessage(leaderElector, 'tell'); // ensure other leader also knows the problem
}
};
leaderElector._channel.addEventListener('internal', isLeaderListener);
leaderElector._lstns.push(isLeaderListener);
return _sendMessage(leaderElector, 'tell');
}
function fillOptionsWithDefaults(options, channel) {
if (!options) options = {};
options = JSON.parse(JSON.stringify(options));
if (!options.fallbackInterval) {
options.fallbackInterval = 3000;
}
if (!options.responseTime) {
options.responseTime = channel.method.averageResponseTime(channel.options);
}
return options;
}
export function createLeaderElection(channel, options) {
if (channel._leaderElector) {
throw new Error('BroadcastChannel already has a leader-elector');
}
options = fillOptionsWithDefaults(options, channel);
var elector = new LeaderElection(channel, options);
channel._befC.push(function () {
return elector.die();
});
channel._leaderElector = elector;
return elector;
}

View File

@@ -0,0 +1,66 @@
import NativeMethod from './methods/native.js';
import IndexeDbMethod from './methods/indexed-db.js';
import LocalstorageMethod from './methods/localstorage.js';
import SimulateMethod from './methods/simulate.js';
import { isNode } from './util'; // order is important
var METHODS = [NativeMethod, // fastest
IndexeDbMethod, LocalstorageMethod];
/**
* The NodeMethod is loaded lazy
* so it will not get bundled in browser-builds
*/
if (isNode) {
/**
* we use the non-transpiled code for nodejs
* because it runs faster
*/
var NodeMethod = require('../../src/methods/' + // use this hack so that browserify and others
// do not import the node-method by default
// when bundling.
'node.js');
/**
* this will be false for webpackbuilds
* which will shim the node-method with an empty object {}
*/
if (typeof NodeMethod.canBeUsed === 'function') {
METHODS.push(NodeMethod);
}
}
export function chooseMethod(options) {
var chooseMethods = [].concat(options.methods, METHODS).filter(Boolean); // directly chosen
if (options.type) {
if (options.type === 'simulate') {
// only use simulate-method if directly chosen
return SimulateMethod;
}
var ret = chooseMethods.find(function (m) {
return m.type === options.type;
});
if (!ret) throw new Error('method-type ' + options.type + ' not found');else return ret;
}
/**
* if no webworker support is needed,
* remove idb from the list so that localstorage is been chosen
*/
if (!options.webWorkerSupport && !isNode) {
chooseMethods = chooseMethods.filter(function (m) {
return m.type !== 'idb';
});
}
var useMethod = chooseMethods.find(function (method) {
return method.canBeUsed();
});
if (!useMethod) throw new Error('No useable methode found:' + JSON.stringify(METHODS.map(function (m) {
return m.type;
})));else return useMethod;
}

View File

@@ -0,0 +1,4 @@
/**
* if you really need this method,
* implement it
*/

View File

@@ -0,0 +1,309 @@
/**
* this method uses indexeddb to store the messages
* There is currently no observerAPI for idb
* @link https://github.com/w3c/IndexedDB/issues/51
*/
import { sleep, randomInt, randomToken, microSeconds as micro, isNode } from '../util.js';
export var microSeconds = micro;
import { ObliviousSet } from 'oblivious-set';
import { fillOptionsWithDefaults } from '../options';
var DB_PREFIX = 'pubkey.broadcast-channel-0-';
var OBJECT_STORE_ID = 'messages';
export var type = 'idb';
export function getIdb() {
if (typeof indexedDB !== 'undefined') return indexedDB;
if (typeof window !== 'undefined') {
if (typeof window.mozIndexedDB !== 'undefined') return window.mozIndexedDB;
if (typeof window.webkitIndexedDB !== 'undefined') return window.webkitIndexedDB;
if (typeof window.msIndexedDB !== 'undefined') return window.msIndexedDB;
}
return false;
}
export function createDatabase(channelName) {
var IndexedDB = getIdb(); // create table
var dbName = DB_PREFIX + channelName;
var openRequest = IndexedDB.open(dbName, 1);
openRequest.onupgradeneeded = function (ev) {
var db = ev.target.result;
db.createObjectStore(OBJECT_STORE_ID, {
keyPath: 'id',
autoIncrement: true
});
};
var dbPromise = new Promise(function (res, rej) {
openRequest.onerror = function (ev) {
return rej(ev);
};
openRequest.onsuccess = function () {
res(openRequest.result);
};
});
return dbPromise;
}
/**
* writes the new message to the database
* so other readers can find it
*/
export function writeMessage(db, readerUuid, messageJson) {
var time = new Date().getTime();
var writeObject = {
uuid: readerUuid,
time: time,
data: messageJson
};
var transaction = db.transaction([OBJECT_STORE_ID], 'readwrite');
return new Promise(function (res, rej) {
transaction.oncomplete = function () {
return res();
};
transaction.onerror = function (ev) {
return rej(ev);
};
var objectStore = transaction.objectStore(OBJECT_STORE_ID);
objectStore.add(writeObject);
});
}
export function getAllMessages(db) {
var objectStore = db.transaction(OBJECT_STORE_ID).objectStore(OBJECT_STORE_ID);
var ret = [];
return new Promise(function (res) {
objectStore.openCursor().onsuccess = function (ev) {
var cursor = ev.target.result;
if (cursor) {
ret.push(cursor.value); //alert("Name for SSN " + cursor.key + " is " + cursor.value.name);
cursor["continue"]();
} else {
res(ret);
}
};
});
}
export function getMessagesHigherThan(db, lastCursorId) {
var objectStore = db.transaction(OBJECT_STORE_ID).objectStore(OBJECT_STORE_ID);
var ret = [];
function openCursor() {
// Occasionally Safari will fail on IDBKeyRange.bound, this
// catches that error, having it open the cursor to the first
// item. When it gets data it will advance to the desired key.
try {
var keyRangeValue = IDBKeyRange.bound(lastCursorId + 1, Infinity);
return objectStore.openCursor(keyRangeValue);
} catch (e) {
return objectStore.openCursor();
}
}
return new Promise(function (res) {
openCursor().onsuccess = function (ev) {
var cursor = ev.target.result;
if (cursor) {
if (cursor.value.id < lastCursorId + 1) {
cursor["continue"](lastCursorId + 1);
} else {
ret.push(cursor.value);
cursor["continue"]();
}
} else {
res(ret);
}
};
});
}
export function removeMessageById(db, id) {
var request = db.transaction([OBJECT_STORE_ID], 'readwrite').objectStore(OBJECT_STORE_ID)["delete"](id);
return new Promise(function (res) {
request.onsuccess = function () {
return res();
};
});
}
export function getOldMessages(db, ttl) {
var olderThen = new Date().getTime() - ttl;
var objectStore = db.transaction(OBJECT_STORE_ID).objectStore(OBJECT_STORE_ID);
var ret = [];
return new Promise(function (res) {
objectStore.openCursor().onsuccess = function (ev) {
var cursor = ev.target.result;
if (cursor) {
var msgObk = cursor.value;
if (msgObk.time < olderThen) {
ret.push(msgObk); //alert("Name for SSN " + cursor.key + " is " + cursor.value.name);
cursor["continue"]();
} else {
// no more old messages,
res(ret);
return;
}
} else {
res(ret);
}
};
});
}
export function cleanOldMessages(db, ttl) {
return getOldMessages(db, ttl).then(function (tooOld) {
return Promise.all(tooOld.map(function (msgObj) {
return removeMessageById(db, msgObj.id);
}));
});
}
export function create(channelName, options) {
options = fillOptionsWithDefaults(options);
return createDatabase(channelName).then(function (db) {
var state = {
closed: false,
lastCursorId: 0,
channelName: channelName,
options: options,
uuid: randomToken(),
/**
* emittedMessagesIds
* contains all messages that have been emitted before
* @type {ObliviousSet}
*/
eMIs: new ObliviousSet(options.idb.ttl * 2),
// ensures we do not read messages in parrallel
writeBlockPromise: Promise.resolve(),
messagesCallback: null,
readQueuePromises: [],
db: db
};
/**
* Handle abrupt closes that do not originate from db.close().
* This could happen, for example, if the underlying storage is
* removed or if the user clears the database in the browser's
* history preferences.
*/
db.onclose = function () {
state.closed = true;
if (options.idb.onclose) options.idb.onclose();
};
/**
* if service-workers are used,
* we have no 'storage'-event if they post a message,
* therefore we also have to set an interval
*/
_readLoop(state);
return state;
});
}
function _readLoop(state) {
if (state.closed) return;
readNewMessages(state).then(function () {
return sleep(state.options.idb.fallbackInterval);
}).then(function () {
return _readLoop(state);
});
}
function _filterMessage(msgObj, state) {
if (msgObj.uuid === state.uuid) return false; // send by own
if (state.eMIs.has(msgObj.id)) return false; // already emitted
if (msgObj.data.time < state.messagesCallbackTime) return false; // older then onMessageCallback
return true;
}
/**
* reads all new messages from the database and emits them
*/
function readNewMessages(state) {
// channel already closed
if (state.closed) return Promise.resolve(); // if no one is listening, we do not need to scan for new messages
if (!state.messagesCallback) return Promise.resolve();
return getMessagesHigherThan(state.db, state.lastCursorId).then(function (newerMessages) {
var useMessages = newerMessages
/**
* there is a bug in iOS where the msgObj can be undefined some times
* so we filter them out
* @link https://github.com/pubkey/broadcast-channel/issues/19
*/
.filter(function (msgObj) {
return !!msgObj;
}).map(function (msgObj) {
if (msgObj.id > state.lastCursorId) {
state.lastCursorId = msgObj.id;
}
return msgObj;
}).filter(function (msgObj) {
return _filterMessage(msgObj, state);
}).sort(function (msgObjA, msgObjB) {
return msgObjA.time - msgObjB.time;
}); // sort by time
useMessages.forEach(function (msgObj) {
if (state.messagesCallback) {
state.eMIs.add(msgObj.id);
state.messagesCallback(msgObj.data);
}
});
return Promise.resolve();
});
}
export function close(channelState) {
channelState.closed = true;
channelState.db.close();
}
export function postMessage(channelState, messageJson) {
channelState.writeBlockPromise = channelState.writeBlockPromise.then(function () {
return writeMessage(channelState.db, channelState.uuid, messageJson);
}).then(function () {
if (randomInt(0, 10) === 0) {
/* await (do not await) */
cleanOldMessages(channelState.db, channelState.options.idb.ttl);
}
});
return channelState.writeBlockPromise;
}
export function onMessage(channelState, fn, time) {
channelState.messagesCallbackTime = time;
channelState.messagesCallback = fn;
readNewMessages(channelState);
}
export function canBeUsed() {
if (isNode) return false;
var idb = getIdb();
if (!idb) return false;
return true;
}
export function averageResponseTime(options) {
return options.idb.fallbackInterval * 2;
}
export default {
create: create,
close: close,
onMessage: onMessage,
postMessage: postMessage,
canBeUsed: canBeUsed,
type: type,
averageResponseTime: averageResponseTime,
microSeconds: microSeconds
};

View File

@@ -0,0 +1,163 @@
/**
* A localStorage-only method which uses localstorage and its 'storage'-event
* This does not work inside of webworkers because they have no access to locastorage
* This is basically implemented to support IE9 or your grandmothers toaster.
* @link https://caniuse.com/#feat=namevalue-storage
* @link https://caniuse.com/#feat=indexeddb
*/
import { ObliviousSet } from 'oblivious-set';
import { fillOptionsWithDefaults } from '../options';
import { sleep, randomToken, microSeconds as micro, isNode } from '../util';
export var microSeconds = micro;
var KEY_PREFIX = 'pubkey.broadcastChannel-';
export var type = 'localstorage';
/**
* copied from crosstab
* @link https://github.com/tejacques/crosstab/blob/master/src/crosstab.js#L32
*/
export function getLocalStorage() {
var localStorage;
if (typeof window === 'undefined') return null;
try {
localStorage = window.localStorage;
localStorage = window['ie8-eventlistener/storage'] || window.localStorage;
} catch (e) {// New versions of Firefox throw a Security exception
// if cookies are disabled. See
// https://bugzilla.mozilla.org/show_bug.cgi?id=1028153
}
return localStorage;
}
export function storageKey(channelName) {
return KEY_PREFIX + channelName;
}
/**
* writes the new message to the storage
* and fires the storage-event so other readers can find it
*/
export function postMessage(channelState, messageJson) {
return new Promise(function (res) {
sleep().then(function () {
var key = storageKey(channelState.channelName);
var writeObj = {
token: randomToken(),
time: new Date().getTime(),
data: messageJson,
uuid: channelState.uuid
};
var value = JSON.stringify(writeObj);
getLocalStorage().setItem(key, value);
/**
* StorageEvent does not fire the 'storage' event
* in the window that changes the state of the local storage.
* So we fire it manually
*/
var ev = document.createEvent('Event');
ev.initEvent('storage', true, true);
ev.key = key;
ev.newValue = value;
window.dispatchEvent(ev);
res();
});
});
}
export function addStorageEventListener(channelName, fn) {
var key = storageKey(channelName);
var listener = function listener(ev) {
if (ev.key === key) {
fn(JSON.parse(ev.newValue));
}
};
window.addEventListener('storage', listener);
return listener;
}
export function removeStorageEventListener(listener) {
window.removeEventListener('storage', listener);
}
export function create(channelName, options) {
options = fillOptionsWithDefaults(options);
if (!canBeUsed()) {
throw new Error('BroadcastChannel: localstorage cannot be used');
}
var uuid = randomToken();
/**
* eMIs
* contains all messages that have been emitted before
* @type {ObliviousSet}
*/
var eMIs = new ObliviousSet(options.localstorage.removeTimeout);
var state = {
channelName: channelName,
uuid: uuid,
eMIs: eMIs // emittedMessagesIds
};
state.listener = addStorageEventListener(channelName, function (msgObj) {
if (!state.messagesCallback) return; // no listener
if (msgObj.uuid === uuid) return; // own message
if (!msgObj.token || eMIs.has(msgObj.token)) return; // already emitted
if (msgObj.data.time && msgObj.data.time < state.messagesCallbackTime) return; // too old
eMIs.add(msgObj.token);
state.messagesCallback(msgObj.data);
});
return state;
}
export function close(channelState) {
removeStorageEventListener(channelState.listener);
}
export function onMessage(channelState, fn, time) {
channelState.messagesCallbackTime = time;
channelState.messagesCallback = fn;
}
export function canBeUsed() {
if (isNode) return false;
var ls = getLocalStorage();
if (!ls) return false;
try {
var key = '__broadcastchannel_check';
ls.setItem(key, 'works');
ls.removeItem(key);
} catch (e) {
// Safari 10 in private mode will not allow write access to local
// storage and fail with a QuotaExceededError. See
// https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API#Private_Browsing_Incognito_modes
return false;
}
return true;
}
export function averageResponseTime() {
var defaultTime = 120;
var userAgent = navigator.userAgent.toLowerCase();
if (userAgent.includes('safari') && !userAgent.includes('chrome')) {
// safari is much slower so this time is higher
return defaultTime * 2;
}
return defaultTime;
}
export default {
create: create,
close: close,
onMessage: onMessage,
postMessage: postMessage,
canBeUsed: canBeUsed,
type: type,
averageResponseTime: averageResponseTime,
microSeconds: microSeconds
};

View File

@@ -0,0 +1,62 @@
import { microSeconds as micro, isNode } from '../util';
export var microSeconds = micro;
export var type = 'native';
export function create(channelName) {
var state = {
messagesCallback: null,
bc: new BroadcastChannel(channelName),
subFns: [] // subscriberFunctions
};
state.bc.onmessage = function (msg) {
if (state.messagesCallback) {
state.messagesCallback(msg.data);
}
};
return state;
}
export function close(channelState) {
channelState.bc.close();
channelState.subFns = [];
}
export function postMessage(channelState, messageJson) {
try {
channelState.bc.postMessage(messageJson, false);
return Promise.resolve();
} catch (err) {
return Promise.reject(err);
}
}
export function onMessage(channelState, fn) {
channelState.messagesCallback = fn;
}
export function canBeUsed() {
/**
* in the electron-renderer, isNode will be true even if we are in browser-context
* so we also check if window is undefined
*/
if (isNode && typeof window === 'undefined') return false;
if (typeof BroadcastChannel === 'function') {
if (BroadcastChannel._pubkey) {
throw new Error('BroadcastChannel: Do not overwrite window.BroadcastChannel with this module, this is not a polyfill');
}
return true;
} else return false;
}
export function averageResponseTime() {
return 150;
}
export default {
create: create,
close: close,
onMessage: onMessage,
postMessage: postMessage,
canBeUsed: canBeUsed,
type: type,
averageResponseTime: averageResponseTime,
microSeconds: microSeconds
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,51 @@
import { microSeconds as micro } from '../util';
export var microSeconds = micro;
export var type = 'simulate';
var SIMULATE_CHANNELS = new Set();
export function create(channelName) {
var state = {
name: channelName,
messagesCallback: null
};
SIMULATE_CHANNELS.add(state);
return state;
}
export function close(channelState) {
SIMULATE_CHANNELS["delete"](channelState);
}
export function postMessage(channelState, messageJson) {
return new Promise(function (res) {
return setTimeout(function () {
var channelArray = Array.from(SIMULATE_CHANNELS);
channelArray.filter(function (channel) {
return channel.name === channelState.name;
}).filter(function (channel) {
return channel !== channelState;
}).filter(function (channel) {
return !!channel.messagesCallback;
}).forEach(function (channel) {
return channel.messagesCallback(messageJson);
});
res();
}, 5);
});
}
export function onMessage(channelState, fn) {
channelState.messagesCallback = fn;
}
export function canBeUsed() {
return true;
}
export function averageResponseTime() {
return 5;
}
export default {
create: create,
close: close,
onMessage: onMessage,
postMessage: postMessage,
canBeUsed: canBeUsed,
type: type,
averageResponseTime: averageResponseTime,
microSeconds: microSeconds
};

View File

@@ -0,0 +1,24 @@
export function fillOptionsWithDefaults() {
var originalOptions = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
var options = JSON.parse(JSON.stringify(originalOptions)); // main
if (typeof options.webWorkerSupport === 'undefined') options.webWorkerSupport = true; // indexed-db
if (!options.idb) options.idb = {}; // after this time the messages get deleted
if (!options.idb.ttl) options.idb.ttl = 1000 * 45;
if (!options.idb.fallbackInterval) options.idb.fallbackInterval = 150; // handles abrupt db onclose events.
if (originalOptions.idb && typeof originalOptions.idb.onclose === 'function') options.idb.onclose = originalOptions.idb.onclose; // localstorage
if (!options.localstorage) options.localstorage = {};
if (!options.localstorage.removeTimeout) options.localstorage.removeTimeout = 1000 * 60; // custom methods
if (originalOptions.methods) options.methods = originalOptions.methods; // node
if (!options.node) options.node = {};
if (!options.node.ttl) options.node.ttl = 1000 * 60 * 2; // 2 minutes;
if (typeof options.node.useFastPath === 'undefined') options.node.useFastPath = true;
return options;
}

View File

@@ -0,0 +1,55 @@
/**
* returns true if the given object is a promise
*/
export function isPromise(obj) {
if (obj && typeof obj.then === 'function') {
return true;
} else {
return false;
}
}
export function sleep(time) {
if (!time) time = 0;
return new Promise(function (res) {
return setTimeout(res, time);
});
}
export function randomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1) + min);
}
/**
* https://stackoverflow.com/a/8084248
*/
export function randomToken() {
return Math.random().toString(36).substring(2);
}
var lastMs = 0;
var additional = 0;
/**
* returns the current time in micro-seconds,
* WARNING: This is a pseudo-function
* Performance.now is not reliable in webworkers, so we just make sure to never return the same time.
* This is enough in browsers, and this function will not be used in nodejs.
* The main reason for this hack is to ensure that BroadcastChannel behaves equal to production when it is used in fast-running unit tests.
*/
export function microSeconds() {
var ms = new Date().getTime();
if (ms === lastMs) {
additional++;
return ms * 1000 + additional;
} else {
lastMs = ms;
additional = 0;
return ms * 1000;
}
}
/**
* copied from the 'detect-node' npm module
* We cannot use the module directly because it causes problems with rollup
* @link https://github.com/iliakan/detect-node/blob/master/index.js
*/
export var isNode = Object.prototype.toString.call(typeof process !== 'undefined' ? process : 0) === '[object process]';

View File

@@ -0,0 +1,279 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.clearNodeFolder = clearNodeFolder;
exports.enforceOptions = enforceOptions;
exports.BroadcastChannel = void 0;
var _util = require("./util.js");
var _methodChooser = require("./method-chooser.js");
var _options = require("./options.js");
var BroadcastChannel = function BroadcastChannel(name, options) {
this.name = name;
if (ENFORCED_OPTIONS) {
options = ENFORCED_OPTIONS;
}
this.options = (0, _options.fillOptionsWithDefaults)(options);
this.method = (0, _methodChooser.chooseMethod)(this.options); // isListening
this._iL = false;
/**
* _onMessageListener
* setting onmessage twice,
* will overwrite the first listener
*/
this._onML = null;
/**
* _addEventListeners
*/
this._addEL = {
message: [],
internal: []
};
/**
* Unsend message promises
* where the sending is still in progress
* @type {Set<Promise>}
*/
this._uMP = new Set();
/**
* _beforeClose
* array of promises that will be awaited
* before the channel is closed
*/
this._befC = [];
/**
* _preparePromise
*/
this._prepP = null;
_prepareChannel(this);
}; // STATICS
/**
* used to identify if someone overwrites
* window.BroadcastChannel with this
* See methods/native.js
*/
exports.BroadcastChannel = BroadcastChannel;
BroadcastChannel._pubkey = true;
/**
* clears the tmp-folder if is node
* @return {Promise<boolean>} true if has run, false if not node
*/
function clearNodeFolder(options) {
options = (0, _options.fillOptionsWithDefaults)(options);
var method = (0, _methodChooser.chooseMethod)(options);
if (method.type === 'node') {
return method.clearNodeFolder().then(function () {
return true;
});
} else {
return Promise.resolve(false);
}
}
/**
* if set, this method is enforced,
* no mather what the options are
*/
var ENFORCED_OPTIONS;
function enforceOptions(options) {
ENFORCED_OPTIONS = options;
} // PROTOTYPE
BroadcastChannel.prototype = {
postMessage: function postMessage(msg) {
if (this.closed) {
throw new Error('BroadcastChannel.postMessage(): ' + 'Cannot post message after channel has closed');
}
return _post(this, 'message', msg);
},
postInternal: function postInternal(msg) {
return _post(this, 'internal', msg);
},
set onmessage(fn) {
var time = this.method.microSeconds();
var listenObj = {
time: time,
fn: fn
};
_removeListenerObject(this, 'message', this._onML);
if (fn && typeof fn === 'function') {
this._onML = listenObj;
_addListenerObject(this, 'message', listenObj);
} else {
this._onML = null;
}
},
addEventListener: function addEventListener(type, fn) {
var time = this.method.microSeconds();
var listenObj = {
time: time,
fn: fn
};
_addListenerObject(this, type, listenObj);
},
removeEventListener: function removeEventListener(type, fn) {
var obj = this._addEL[type].find(function (obj) {
return obj.fn === fn;
});
_removeListenerObject(this, type, obj);
},
close: function close() {
var _this = this;
if (this.closed) {
return;
}
this.closed = true;
var awaitPrepare = this._prepP ? this._prepP : Promise.resolve();
this._onML = null;
this._addEL.message = [];
return awaitPrepare // wait until all current sending are processed
.then(function () {
return Promise.all(Array.from(_this._uMP));
}) // run before-close hooks
.then(function () {
return Promise.all(_this._befC.map(function (fn) {
return fn();
}));
}) // close the channel
.then(function () {
return _this.method.close(_this._state);
});
},
get type() {
return this.method.type;
},
get isClosed() {
return this.closed;
}
};
/**
* Post a message over the channel
* @returns {Promise} that resolved when the message sending is done
*/
function _post(broadcastChannel, type, msg) {
var time = broadcastChannel.method.microSeconds();
var msgObj = {
time: time,
type: type,
data: msg
};
var awaitPrepare = broadcastChannel._prepP ? broadcastChannel._prepP : Promise.resolve();
return awaitPrepare.then(function () {
var sendPromise = broadcastChannel.method.postMessage(broadcastChannel._state, msgObj); // add/remove to unsend messages list
broadcastChannel._uMP.add(sendPromise);
sendPromise["catch"]().then(function () {
return broadcastChannel._uMP["delete"](sendPromise);
});
return sendPromise;
});
}
function _prepareChannel(channel) {
var maybePromise = channel.method.create(channel.name, channel.options);
if ((0, _util.isPromise)(maybePromise)) {
channel._prepP = maybePromise;
maybePromise.then(function (s) {
// used in tests to simulate slow runtime
/*if (channel.options.prepareDelay) {
await new Promise(res => setTimeout(res, this.options.prepareDelay));
}*/
channel._state = s;
});
} else {
channel._state = maybePromise;
}
}
function _hasMessageListeners(channel) {
if (channel._addEL.message.length > 0) return true;
if (channel._addEL.internal.length > 0) return true;
return false;
}
function _addListenerObject(channel, type, obj) {
channel._addEL[type].push(obj);
_startListening(channel);
}
function _removeListenerObject(channel, type, obj) {
channel._addEL[type] = channel._addEL[type].filter(function (o) {
return o !== obj;
});
_stopListening(channel);
}
function _startListening(channel) {
if (!channel._iL && _hasMessageListeners(channel)) {
// someone is listening, start subscribing
var listenerFn = function listenerFn(msgObj) {
channel._addEL[msgObj.type].forEach(function (obj) {
if (msgObj.time >= obj.time) {
obj.fn(msgObj.data);
}
});
};
var time = channel.method.microSeconds();
if (channel._prepP) {
channel._prepP.then(function () {
channel._iL = true;
channel.method.onMessage(channel._state, listenerFn, time);
});
} else {
channel._iL = true;
channel.method.onMessage(channel._state, listenerFn, time);
}
}
}
function _stopListening(channel) {
if (channel._iL && !_hasMessageListeners(channel)) {
// noone is listening, stop subscribing
channel._iL = false;
var time = channel.method.microSeconds();
channel.method.onMessage(channel._state, null, time);
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,8 @@
"use strict";
var _module = require('./index.es5.js');
var BroadcastChannel = _module.BroadcastChannel;
var createLeaderElection = _module.createLeaderElection;
window['BroadcastChannel2'] = BroadcastChannel;
window['createLeaderElection'] = createLeaderElection;

View File

@@ -0,0 +1,19 @@
"use strict";
var _index = require("./index.js");
/**
* because babel can only export on default-attribute,
* we use this for the non-module-build
* this ensures that users do not have to use
* var BroadcastChannel = require('broadcast-channel').default;
* but
* var BroadcastChannel = require('broadcast-channel');
*/
module.exports = {
BroadcastChannel: _index.BroadcastChannel,
createLeaderElection: _index.createLeaderElection,
clearNodeFolder: _index.clearNodeFolder,
enforceOptions: _index.enforceOptions,
beLeader: _index.beLeader
};

View File

@@ -0,0 +1,39 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
Object.defineProperty(exports, "BroadcastChannel", {
enumerable: true,
get: function get() {
return _broadcastChannel.BroadcastChannel;
}
});
Object.defineProperty(exports, "clearNodeFolder", {
enumerable: true,
get: function get() {
return _broadcastChannel.clearNodeFolder;
}
});
Object.defineProperty(exports, "enforceOptions", {
enumerable: true,
get: function get() {
return _broadcastChannel.enforceOptions;
}
});
Object.defineProperty(exports, "createLeaderElection", {
enumerable: true,
get: function get() {
return _leaderElection.createLeaderElection;
}
});
Object.defineProperty(exports, "beLeader", {
enumerable: true,
get: function get() {
return _leaderElection.beLeader;
}
});
var _broadcastChannel = require("./broadcast-channel");
var _leaderElection = require("./leader-election");

View File

@@ -0,0 +1,274 @@
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.beLeader = beLeader;
exports.createLeaderElection = createLeaderElection;
var _util = require("./util.js");
var _unload = _interopRequireDefault(require("unload"));
var LeaderElection = function LeaderElection(channel, options) {
this._channel = channel;
this._options = options;
this.isLeader = false;
this.isDead = false;
this.token = (0, _util.randomToken)();
this._isApl = false; // _isApplying
this._reApply = false; // things to clean up
this._unl = []; // _unloads
this._lstns = []; // _listeners
this._invs = []; // _intervals
this._dpL = function () {}; // onduplicate listener
this._dpLC = false; // true when onduplicate called
};
LeaderElection.prototype = {
applyOnce: function applyOnce() {
var _this = this;
if (this.isLeader) return Promise.resolve(false);
if (this.isDead) return Promise.resolve(false); // do nothing if already running
if (this._isApl) {
this._reApply = true;
return Promise.resolve(false);
}
this._isApl = true;
var stopCriteria = false;
var recieved = [];
var handleMessage = function handleMessage(msg) {
if (msg.context === 'leader' && msg.token != _this.token) {
recieved.push(msg);
if (msg.action === 'apply') {
// other is applying
if (msg.token > _this.token) {
// other has higher token, stop applying
stopCriteria = true;
}
}
if (msg.action === 'tell') {
// other is already leader
stopCriteria = true;
}
}
};
this._channel.addEventListener('internal', handleMessage);
var ret = _sendMessage(this, 'apply') // send out that this one is applying
.then(function () {
return (0, _util.sleep)(_this._options.responseTime);
}) // let others time to respond
.then(function () {
if (stopCriteria) return Promise.reject(new Error());else return _sendMessage(_this, 'apply');
}).then(function () {
return (0, _util.sleep)(_this._options.responseTime);
}) // let others time to respond
.then(function () {
if (stopCriteria) return Promise.reject(new Error());else return _sendMessage(_this);
}).then(function () {
return beLeader(_this);
}) // no one disagreed -> this one is now leader
.then(function () {
return true;
})["catch"](function () {
return false;
}) // apply not successfull
.then(function (success) {
_this._channel.removeEventListener('internal', handleMessage);
_this._isApl = false;
if (!success && _this._reApply) {
_this._reApply = false;
return _this.applyOnce();
} else return success;
});
return ret;
},
awaitLeadership: function awaitLeadership() {
if (
/* _awaitLeadershipPromise */
!this._aLP) {
this._aLP = _awaitLeadershipOnce(this);
}
return this._aLP;
},
set onduplicate(fn) {
this._dpL = fn;
},
die: function die() {
var _this2 = this;
if (this.isDead) return;
this.isDead = true;
this._lstns.forEach(function (listener) {
return _this2._channel.removeEventListener('internal', listener);
});
this._invs.forEach(function (interval) {
return clearInterval(interval);
});
this._unl.forEach(function (uFn) {
uFn.remove();
});
return _sendMessage(this, 'death');
}
};
function _awaitLeadershipOnce(leaderElector) {
if (leaderElector.isLeader) return Promise.resolve();
return new Promise(function (res) {
var resolved = false;
function finish() {
if (resolved) {
return;
}
resolved = true;
clearInterval(interval);
leaderElector._channel.removeEventListener('internal', whenDeathListener);
res(true);
} // try once now
leaderElector.applyOnce().then(function () {
if (leaderElector.isLeader) {
finish();
}
}); // try on fallbackInterval
var interval = setInterval(function () {
leaderElector.applyOnce().then(function () {
if (leaderElector.isLeader) {
finish();
}
});
}, leaderElector._options.fallbackInterval);
leaderElector._invs.push(interval); // try when other leader dies
var whenDeathListener = function whenDeathListener(msg) {
if (msg.context === 'leader' && msg.action === 'death') {
leaderElector.applyOnce().then(function () {
if (leaderElector.isLeader) finish();
});
}
};
leaderElector._channel.addEventListener('internal', whenDeathListener);
leaderElector._lstns.push(whenDeathListener);
});
}
/**
* sends and internal message over the broadcast-channel
*/
function _sendMessage(leaderElector, action) {
var msgJson = {
context: 'leader',
action: action,
token: leaderElector.token
};
return leaderElector._channel.postInternal(msgJson);
}
function beLeader(leaderElector) {
leaderElector.isLeader = true;
var unloadFn = _unload["default"].add(function () {
return leaderElector.die();
});
leaderElector._unl.push(unloadFn);
var isLeaderListener = function isLeaderListener(msg) {
if (msg.context === 'leader' && msg.action === 'apply') {
_sendMessage(leaderElector, 'tell');
}
if (msg.context === 'leader' && msg.action === 'tell' && !leaderElector._dpLC) {
/**
* another instance is also leader!
* This can happen on rare events
* like when the CPU is at 100% for long time
* or the tabs are open very long and the browser throttles them.
* @link https://github.com/pubkey/broadcast-channel/issues/414
* @link https://github.com/pubkey/broadcast-channel/issues/385
*/
leaderElector._dpLC = true;
leaderElector._dpL(); // message the lib user so the app can handle the problem
_sendMessage(leaderElector, 'tell'); // ensure other leader also knows the problem
}
};
leaderElector._channel.addEventListener('internal', isLeaderListener);
leaderElector._lstns.push(isLeaderListener);
return _sendMessage(leaderElector, 'tell');
}
function fillOptionsWithDefaults(options, channel) {
if (!options) options = {};
options = JSON.parse(JSON.stringify(options));
if (!options.fallbackInterval) {
options.fallbackInterval = 3000;
}
if (!options.responseTime) {
options.responseTime = channel.method.averageResponseTime(channel.options);
}
return options;
}
function createLeaderElection(channel, options) {
if (channel._leaderElector) {
throw new Error('BroadcastChannel already has a leader-elector');
}
options = fillOptionsWithDefaults(options, channel);
var elector = new LeaderElection(channel, options);
channel._befC.push(function () {
return elector.die();
});
channel._leaderElector = elector;
return elector;
}

View File

@@ -0,0 +1,80 @@
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.chooseMethod = chooseMethod;
var _native = _interopRequireDefault(require("./methods/native.js"));
var _indexedDb = _interopRequireDefault(require("./methods/indexed-db.js"));
var _localstorage = _interopRequireDefault(require("./methods/localstorage.js"));
var _simulate = _interopRequireDefault(require("./methods/simulate.js"));
var _util = require("./util");
// order is important
var METHODS = [_native["default"], // fastest
_indexedDb["default"], _localstorage["default"]];
/**
* The NodeMethod is loaded lazy
* so it will not get bundled in browser-builds
*/
if (_util.isNode) {
/**
* we use the non-transpiled code for nodejs
* because it runs faster
*/
var NodeMethod = require('../../src/methods/' + // use this hack so that browserify and others
// do not import the node-method by default
// when bundling.
'node.js');
/**
* this will be false for webpackbuilds
* which will shim the node-method with an empty object {}
*/
if (typeof NodeMethod.canBeUsed === 'function') {
METHODS.push(NodeMethod);
}
}
function chooseMethod(options) {
var chooseMethods = [].concat(options.methods, METHODS).filter(Boolean); // directly chosen
if (options.type) {
if (options.type === 'simulate') {
// only use simulate-method if directly chosen
return _simulate["default"];
}
var ret = chooseMethods.find(function (m) {
return m.type === options.type;
});
if (!ret) throw new Error('method-type ' + options.type + ' not found');else return ret;
}
/**
* if no webworker support is needed,
* remove idb from the list so that localstorage is been chosen
*/
if (!options.webWorkerSupport && !_util.isNode) {
chooseMethods = chooseMethods.filter(function (m) {
return m.type !== 'idb';
});
}
var useMethod = chooseMethods.find(function (method) {
return method.canBeUsed();
});
if (!useMethod) throw new Error('No useable methode found:' + JSON.stringify(METHODS.map(function (m) {
return m.type;
})));else return useMethod;
}

View File

@@ -0,0 +1,5 @@
/**
* if you really need this method,
* implement it
*/
"use strict";

View File

@@ -0,0 +1,350 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.getIdb = getIdb;
exports.createDatabase = createDatabase;
exports.writeMessage = writeMessage;
exports.getAllMessages = getAllMessages;
exports.getMessagesHigherThan = getMessagesHigherThan;
exports.removeMessageById = removeMessageById;
exports.getOldMessages = getOldMessages;
exports.cleanOldMessages = cleanOldMessages;
exports.create = create;
exports.close = close;
exports.postMessage = postMessage;
exports.onMessage = onMessage;
exports.canBeUsed = canBeUsed;
exports.averageResponseTime = averageResponseTime;
exports["default"] = exports.type = exports.microSeconds = void 0;
var _util = require("../util.js");
var _obliviousSet = require("oblivious-set");
var _options = require("../options");
/**
* this method uses indexeddb to store the messages
* There is currently no observerAPI for idb
* @link https://github.com/w3c/IndexedDB/issues/51
*/
var microSeconds = _util.microSeconds;
exports.microSeconds = microSeconds;
var DB_PREFIX = 'pubkey.broadcast-channel-0-';
var OBJECT_STORE_ID = 'messages';
var type = 'idb';
exports.type = type;
function getIdb() {
if (typeof indexedDB !== 'undefined') return indexedDB;
if (typeof window !== 'undefined') {
if (typeof window.mozIndexedDB !== 'undefined') return window.mozIndexedDB;
if (typeof window.webkitIndexedDB !== 'undefined') return window.webkitIndexedDB;
if (typeof window.msIndexedDB !== 'undefined') return window.msIndexedDB;
}
return false;
}
function createDatabase(channelName) {
var IndexedDB = getIdb(); // create table
var dbName = DB_PREFIX + channelName;
var openRequest = IndexedDB.open(dbName, 1);
openRequest.onupgradeneeded = function (ev) {
var db = ev.target.result;
db.createObjectStore(OBJECT_STORE_ID, {
keyPath: 'id',
autoIncrement: true
});
};
var dbPromise = new Promise(function (res, rej) {
openRequest.onerror = function (ev) {
return rej(ev);
};
openRequest.onsuccess = function () {
res(openRequest.result);
};
});
return dbPromise;
}
/**
* writes the new message to the database
* so other readers can find it
*/
function writeMessage(db, readerUuid, messageJson) {
var time = new Date().getTime();
var writeObject = {
uuid: readerUuid,
time: time,
data: messageJson
};
var transaction = db.transaction([OBJECT_STORE_ID], 'readwrite');
return new Promise(function (res, rej) {
transaction.oncomplete = function () {
return res();
};
transaction.onerror = function (ev) {
return rej(ev);
};
var objectStore = transaction.objectStore(OBJECT_STORE_ID);
objectStore.add(writeObject);
});
}
function getAllMessages(db) {
var objectStore = db.transaction(OBJECT_STORE_ID).objectStore(OBJECT_STORE_ID);
var ret = [];
return new Promise(function (res) {
objectStore.openCursor().onsuccess = function (ev) {
var cursor = ev.target.result;
if (cursor) {
ret.push(cursor.value); //alert("Name for SSN " + cursor.key + " is " + cursor.value.name);
cursor["continue"]();
} else {
res(ret);
}
};
});
}
function getMessagesHigherThan(db, lastCursorId) {
var objectStore = db.transaction(OBJECT_STORE_ID).objectStore(OBJECT_STORE_ID);
var ret = [];
function openCursor() {
// Occasionally Safari will fail on IDBKeyRange.bound, this
// catches that error, having it open the cursor to the first
// item. When it gets data it will advance to the desired key.
try {
var keyRangeValue = IDBKeyRange.bound(lastCursorId + 1, Infinity);
return objectStore.openCursor(keyRangeValue);
} catch (e) {
return objectStore.openCursor();
}
}
return new Promise(function (res) {
openCursor().onsuccess = function (ev) {
var cursor = ev.target.result;
if (cursor) {
if (cursor.value.id < lastCursorId + 1) {
cursor["continue"](lastCursorId + 1);
} else {
ret.push(cursor.value);
cursor["continue"]();
}
} else {
res(ret);
}
};
});
}
function removeMessageById(db, id) {
var request = db.transaction([OBJECT_STORE_ID], 'readwrite').objectStore(OBJECT_STORE_ID)["delete"](id);
return new Promise(function (res) {
request.onsuccess = function () {
return res();
};
});
}
function getOldMessages(db, ttl) {
var olderThen = new Date().getTime() - ttl;
var objectStore = db.transaction(OBJECT_STORE_ID).objectStore(OBJECT_STORE_ID);
var ret = [];
return new Promise(function (res) {
objectStore.openCursor().onsuccess = function (ev) {
var cursor = ev.target.result;
if (cursor) {
var msgObk = cursor.value;
if (msgObk.time < olderThen) {
ret.push(msgObk); //alert("Name for SSN " + cursor.key + " is " + cursor.value.name);
cursor["continue"]();
} else {
// no more old messages,
res(ret);
return;
}
} else {
res(ret);
}
};
});
}
function cleanOldMessages(db, ttl) {
return getOldMessages(db, ttl).then(function (tooOld) {
return Promise.all(tooOld.map(function (msgObj) {
return removeMessageById(db, msgObj.id);
}));
});
}
function create(channelName, options) {
options = (0, _options.fillOptionsWithDefaults)(options);
return createDatabase(channelName).then(function (db) {
var state = {
closed: false,
lastCursorId: 0,
channelName: channelName,
options: options,
uuid: (0, _util.randomToken)(),
/**
* emittedMessagesIds
* contains all messages that have been emitted before
* @type {ObliviousSet}
*/
eMIs: new _obliviousSet.ObliviousSet(options.idb.ttl * 2),
// ensures we do not read messages in parrallel
writeBlockPromise: Promise.resolve(),
messagesCallback: null,
readQueuePromises: [],
db: db
};
/**
* Handle abrupt closes that do not originate from db.close().
* This could happen, for example, if the underlying storage is
* removed or if the user clears the database in the browser's
* history preferences.
*/
db.onclose = function () {
state.closed = true;
if (options.idb.onclose) options.idb.onclose();
};
/**
* if service-workers are used,
* we have no 'storage'-event if they post a message,
* therefore we also have to set an interval
*/
_readLoop(state);
return state;
});
}
function _readLoop(state) {
if (state.closed) return;
readNewMessages(state).then(function () {
return (0, _util.sleep)(state.options.idb.fallbackInterval);
}).then(function () {
return _readLoop(state);
});
}
function _filterMessage(msgObj, state) {
if (msgObj.uuid === state.uuid) return false; // send by own
if (state.eMIs.has(msgObj.id)) return false; // already emitted
if (msgObj.data.time < state.messagesCallbackTime) return false; // older then onMessageCallback
return true;
}
/**
* reads all new messages from the database and emits them
*/
function readNewMessages(state) {
// channel already closed
if (state.closed) return Promise.resolve(); // if no one is listening, we do not need to scan for new messages
if (!state.messagesCallback) return Promise.resolve();
return getMessagesHigherThan(state.db, state.lastCursorId).then(function (newerMessages) {
var useMessages = newerMessages
/**
* there is a bug in iOS where the msgObj can be undefined some times
* so we filter them out
* @link https://github.com/pubkey/broadcast-channel/issues/19
*/
.filter(function (msgObj) {
return !!msgObj;
}).map(function (msgObj) {
if (msgObj.id > state.lastCursorId) {
state.lastCursorId = msgObj.id;
}
return msgObj;
}).filter(function (msgObj) {
return _filterMessage(msgObj, state);
}).sort(function (msgObjA, msgObjB) {
return msgObjA.time - msgObjB.time;
}); // sort by time
useMessages.forEach(function (msgObj) {
if (state.messagesCallback) {
state.eMIs.add(msgObj.id);
state.messagesCallback(msgObj.data);
}
});
return Promise.resolve();
});
}
function close(channelState) {
channelState.closed = true;
channelState.db.close();
}
function postMessage(channelState, messageJson) {
channelState.writeBlockPromise = channelState.writeBlockPromise.then(function () {
return writeMessage(channelState.db, channelState.uuid, messageJson);
}).then(function () {
if ((0, _util.randomInt)(0, 10) === 0) {
/* await (do not await) */
cleanOldMessages(channelState.db, channelState.options.idb.ttl);
}
});
return channelState.writeBlockPromise;
}
function onMessage(channelState, fn, time) {
channelState.messagesCallbackTime = time;
channelState.messagesCallback = fn;
readNewMessages(channelState);
}
function canBeUsed() {
if (_util.isNode) return false;
var idb = getIdb();
if (!idb) return false;
return true;
}
function averageResponseTime(options) {
return options.idb.fallbackInterval * 2;
}
var _default = {
create: create,
close: close,
onMessage: onMessage,
postMessage: postMessage,
canBeUsed: canBeUsed,
type: type,
averageResponseTime: averageResponseTime,
microSeconds: microSeconds
};
exports["default"] = _default;

View File

@@ -0,0 +1,197 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.getLocalStorage = getLocalStorage;
exports.storageKey = storageKey;
exports.postMessage = postMessage;
exports.addStorageEventListener = addStorageEventListener;
exports.removeStorageEventListener = removeStorageEventListener;
exports.create = create;
exports.close = close;
exports.onMessage = onMessage;
exports.canBeUsed = canBeUsed;
exports.averageResponseTime = averageResponseTime;
exports["default"] = exports.type = exports.microSeconds = void 0;
var _obliviousSet = require("oblivious-set");
var _options = require("../options");
var _util = require("../util");
/**
* A localStorage-only method which uses localstorage and its 'storage'-event
* This does not work inside of webworkers because they have no access to locastorage
* This is basically implemented to support IE9 or your grandmothers toaster.
* @link https://caniuse.com/#feat=namevalue-storage
* @link https://caniuse.com/#feat=indexeddb
*/
var microSeconds = _util.microSeconds;
exports.microSeconds = microSeconds;
var KEY_PREFIX = 'pubkey.broadcastChannel-';
var type = 'localstorage';
/**
* copied from crosstab
* @link https://github.com/tejacques/crosstab/blob/master/src/crosstab.js#L32
*/
exports.type = type;
function getLocalStorage() {
var localStorage;
if (typeof window === 'undefined') return null;
try {
localStorage = window.localStorage;
localStorage = window['ie8-eventlistener/storage'] || window.localStorage;
} catch (e) {// New versions of Firefox throw a Security exception
// if cookies are disabled. See
// https://bugzilla.mozilla.org/show_bug.cgi?id=1028153
}
return localStorage;
}
function storageKey(channelName) {
return KEY_PREFIX + channelName;
}
/**
* writes the new message to the storage
* and fires the storage-event so other readers can find it
*/
function postMessage(channelState, messageJson) {
return new Promise(function (res) {
(0, _util.sleep)().then(function () {
var key = storageKey(channelState.channelName);
var writeObj = {
token: (0, _util.randomToken)(),
time: new Date().getTime(),
data: messageJson,
uuid: channelState.uuid
};
var value = JSON.stringify(writeObj);
getLocalStorage().setItem(key, value);
/**
* StorageEvent does not fire the 'storage' event
* in the window that changes the state of the local storage.
* So we fire it manually
*/
var ev = document.createEvent('Event');
ev.initEvent('storage', true, true);
ev.key = key;
ev.newValue = value;
window.dispatchEvent(ev);
res();
});
});
}
function addStorageEventListener(channelName, fn) {
var key = storageKey(channelName);
var listener = function listener(ev) {
if (ev.key === key) {
fn(JSON.parse(ev.newValue));
}
};
window.addEventListener('storage', listener);
return listener;
}
function removeStorageEventListener(listener) {
window.removeEventListener('storage', listener);
}
function create(channelName, options) {
options = (0, _options.fillOptionsWithDefaults)(options);
if (!canBeUsed()) {
throw new Error('BroadcastChannel: localstorage cannot be used');
}
var uuid = (0, _util.randomToken)();
/**
* eMIs
* contains all messages that have been emitted before
* @type {ObliviousSet}
*/
var eMIs = new _obliviousSet.ObliviousSet(options.localstorage.removeTimeout);
var state = {
channelName: channelName,
uuid: uuid,
eMIs: eMIs // emittedMessagesIds
};
state.listener = addStorageEventListener(channelName, function (msgObj) {
if (!state.messagesCallback) return; // no listener
if (msgObj.uuid === uuid) return; // own message
if (!msgObj.token || eMIs.has(msgObj.token)) return; // already emitted
if (msgObj.data.time && msgObj.data.time < state.messagesCallbackTime) return; // too old
eMIs.add(msgObj.token);
state.messagesCallback(msgObj.data);
});
return state;
}
function close(channelState) {
removeStorageEventListener(channelState.listener);
}
function onMessage(channelState, fn, time) {
channelState.messagesCallbackTime = time;
channelState.messagesCallback = fn;
}
function canBeUsed() {
if (_util.isNode) return false;
var ls = getLocalStorage();
if (!ls) return false;
try {
var key = '__broadcastchannel_check';
ls.setItem(key, 'works');
ls.removeItem(key);
} catch (e) {
// Safari 10 in private mode will not allow write access to local
// storage and fail with a QuotaExceededError. See
// https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API#Private_Browsing_Incognito_modes
return false;
}
return true;
}
function averageResponseTime() {
var defaultTime = 120;
var userAgent = navigator.userAgent.toLowerCase();
if (userAgent.includes('safari') && !userAgent.includes('chrome')) {
// safari is much slower so this time is higher
return defaultTime * 2;
}
return defaultTime;
}
var _default = {
create: create,
close: close,
onMessage: onMessage,
postMessage: postMessage,
canBeUsed: canBeUsed,
type: type,
averageResponseTime: averageResponseTime,
microSeconds: microSeconds
};
exports["default"] = _default;

View File

@@ -0,0 +1,86 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.create = create;
exports.close = close;
exports.postMessage = postMessage;
exports.onMessage = onMessage;
exports.canBeUsed = canBeUsed;
exports.averageResponseTime = averageResponseTime;
exports["default"] = exports.type = exports.microSeconds = void 0;
var _util = require("../util");
var microSeconds = _util.microSeconds;
exports.microSeconds = microSeconds;
var type = 'native';
exports.type = type;
function create(channelName) {
var state = {
messagesCallback: null,
bc: new BroadcastChannel(channelName),
subFns: [] // subscriberFunctions
};
state.bc.onmessage = function (msg) {
if (state.messagesCallback) {
state.messagesCallback(msg.data);
}
};
return state;
}
function close(channelState) {
channelState.bc.close();
channelState.subFns = [];
}
function postMessage(channelState, messageJson) {
try {
channelState.bc.postMessage(messageJson, false);
return Promise.resolve();
} catch (err) {
return Promise.reject(err);
}
}
function onMessage(channelState, fn) {
channelState.messagesCallback = fn;
}
function canBeUsed() {
/**
* in the electron-renderer, isNode will be true even if we are in browser-context
* so we also check if window is undefined
*/
if (_util.isNode && typeof window === 'undefined') return false;
if (typeof BroadcastChannel === 'function') {
if (BroadcastChannel._pubkey) {
throw new Error('BroadcastChannel: Do not overwrite window.BroadcastChannel with this module, this is not a polyfill');
}
return true;
} else return false;
}
function averageResponseTime() {
return 150;
}
var _default = {
create: create,
close: close,
onMessage: onMessage,
postMessage: postMessage,
canBeUsed: canBeUsed,
type: type,
averageResponseTime: averageResponseTime,
microSeconds: microSeconds
};
exports["default"] = _default;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,75 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.create = create;
exports.close = close;
exports.postMessage = postMessage;
exports.onMessage = onMessage;
exports.canBeUsed = canBeUsed;
exports.averageResponseTime = averageResponseTime;
exports["default"] = exports.type = exports.microSeconds = void 0;
var _util = require("../util");
var microSeconds = _util.microSeconds;
exports.microSeconds = microSeconds;
var type = 'simulate';
exports.type = type;
var SIMULATE_CHANNELS = new Set();
function create(channelName) {
var state = {
name: channelName,
messagesCallback: null
};
SIMULATE_CHANNELS.add(state);
return state;
}
function close(channelState) {
SIMULATE_CHANNELS["delete"](channelState);
}
function postMessage(channelState, messageJson) {
return new Promise(function (res) {
return setTimeout(function () {
var channelArray = Array.from(SIMULATE_CHANNELS);
channelArray.filter(function (channel) {
return channel.name === channelState.name;
}).filter(function (channel) {
return channel !== channelState;
}).filter(function (channel) {
return !!channel.messagesCallback;
}).forEach(function (channel) {
return channel.messagesCallback(messageJson);
});
res();
}, 5);
});
}
function onMessage(channelState, fn) {
channelState.messagesCallback = fn;
}
function canBeUsed() {
return true;
}
function averageResponseTime() {
return 5;
}
var _default = {
create: create,
close: close,
onMessage: onMessage,
postMessage: postMessage,
canBeUsed: canBeUsed,
type: type,
averageResponseTime: averageResponseTime,
microSeconds: microSeconds
};
exports["default"] = _default;

View File

@@ -0,0 +1,31 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.fillOptionsWithDefaults = fillOptionsWithDefaults;
function fillOptionsWithDefaults() {
var originalOptions = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
var options = JSON.parse(JSON.stringify(originalOptions)); // main
if (typeof options.webWorkerSupport === 'undefined') options.webWorkerSupport = true; // indexed-db
if (!options.idb) options.idb = {}; // after this time the messages get deleted
if (!options.idb.ttl) options.idb.ttl = 1000 * 45;
if (!options.idb.fallbackInterval) options.idb.fallbackInterval = 150; // handles abrupt db onclose events.
if (originalOptions.idb && typeof originalOptions.idb.onclose === 'function') options.idb.onclose = originalOptions.idb.onclose; // localstorage
if (!options.localstorage) options.localstorage = {};
if (!options.localstorage.removeTimeout) options.localstorage.removeTimeout = 1000 * 60; // custom methods
if (originalOptions.methods) options.methods = originalOptions.methods; // node
if (!options.node) options.node = {};
if (!options.node.ttl) options.node.ttl = 1000 * 60 * 2; // 2 minutes;
if (typeof options.node.useFastPath === 'undefined') options.node.useFastPath = true;
return options;
}

View File

@@ -0,0 +1,73 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.isPromise = isPromise;
exports.sleep = sleep;
exports.randomInt = randomInt;
exports.randomToken = randomToken;
exports.microSeconds = microSeconds;
exports.isNode = void 0;
/**
* returns true if the given object is a promise
*/
function isPromise(obj) {
if (obj && typeof obj.then === 'function') {
return true;
} else {
return false;
}
}
function sleep(time) {
if (!time) time = 0;
return new Promise(function (res) {
return setTimeout(res, time);
});
}
function randomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1) + min);
}
/**
* https://stackoverflow.com/a/8084248
*/
function randomToken() {
return Math.random().toString(36).substring(2);
}
var lastMs = 0;
var additional = 0;
/**
* returns the current time in micro-seconds,
* WARNING: This is a pseudo-function
* Performance.now is not reliable in webworkers, so we just make sure to never return the same time.
* This is enough in browsers, and this function will not be used in nodejs.
* The main reason for this hack is to ensure that BroadcastChannel behaves equal to production when it is used in fast-running unit tests.
*/
function microSeconds() {
var ms = new Date().getTime();
if (ms === lastMs) {
additional++;
return ms * 1000 + additional;
} else {
lastMs = ms;
additional = 0;
return ms * 1000;
}
}
/**
* copied from the 'detect-node' npm module
* We cannot use the module directly because it causes problems with rollup
* @link https://github.com/iliakan/detect-node/blob/master/index.js
*/
var isNode = Object.prototype.toString.call(typeof process !== 'undefined' ? process : 0) === '[object process]';
exports.isNode = isNode;

131
frontend/node_modules/broadcast-channel/package.json generated vendored Normal file
View File

@@ -0,0 +1,131 @@
{
"name": "broadcast-channel",
"version": "3.7.0",
"description": "A BroadcastChannel that works in New Browsers, Old Browsers, WebWorkers and NodeJs",
"homepage": "https://github.com/pubkey/broadcast-channel#readme",
"keywords": [
"broadcast-channel",
"broadcastchannel",
"broadcast",
"polyfill",
"localstorage",
"indexeddb",
"postMessage",
"crosstab",
"ipc",
"leader-election"
],
"repository": {
"type": "git",
"url": "git+https://github.com/pubkey/broadcast-channel.git"
},
"author": "pubkey",
"license": "MIT",
"bugs": {
"url": "https://github.com/pubkey/broadcast-channel/issues"
},
"main": "./dist/lib/index.es5.js",
"jsnext:main": "./dist/es/index.js",
"module": "./dist/es/index.js",
"sideEffects": false,
"types": "./types/index.d.ts",
"scripts": {
"test": "echo \"RUN ALL:\" && npm run test:node && npm run test:browser && npm run test:e2e",
"test:node": "npm run build && mocha ./test/index.test.js -b --timeout 6000 --exit",
"test:node:loop": "npm run test:node && npm run test:node:loop",
"test:browser": "npm run build && karma start ./config/karma.conf.js --single-run",
"test:e2e": "concurrently \"npm run docs:serve\" \"sleep 20 && testcafe -b && testcafe chrome -e test/e2e.test.js --hostname localhost\" --kill-others --success first",
"test:typings": "npm run build && mocha ./test/typings.test.js -b --timeout 12000 --exit",
"test:performance": "npm run build && mocha ./test/performance.test.js -b --timeout 24000 --exit",
"test:simple": "npm run build && node ./test_tmp/simple.test.js",
"test:electron": "(cd ./test-electron && npm run test)",
"size:prewebpack": "npm run build && cross-env NODE_ENV=build webpack --config ./config/webpack.config.js",
"size:webpack": "npm run size:prewebpack && echo \"Build-Size Webpack (minified+gzip):\" && gzip-size --raw ./test_tmp/webpack.bundle.js",
"size:browserify": "npm run build && rimraf test_tmp/browserify.js && browserify --no-builtins dist/lib/browserify.index.js > test_tmp/browserify.js && uglifyjs --compress --mangle --output test_tmp/browserify.min.js -- test_tmp/browserify.js && echo \"Build-Size browserify (minified+gzip):\" && gzip-size --raw test_tmp/browserify.min.js",
"size:rollup": "npm run build && rollup --config ./config/rollup.config.js && echo \"Build-Size Rollup (minified+gzip):\" && gzip-size --raw ./test_tmp/rollup.bundle.js",
"lint": "eslint src test config",
"clear": "rimraf -rf ./dist && rimraf -rf ./gen",
"build:es6": "rimraf -rf dist/es && cross-env NODE_ENV=es6 babel src --out-dir dist/es",
"build:es5": "cross-env NODE_ENV=es5 babel src --out-dir dist/lib",
"build:test": "cross-env NODE_ENV=es5 babel test --out-dir test_tmp",
"build:index": "browserify test_tmp/scripts/index.js > docs/index.js",
"build:browser": "browserify test_tmp/scripts/e2e.js > docs/e2e.js",
"build:worker": "browserify test_tmp/scripts/worker.js > docs/worker.js",
"build:iframe": "browserify test_tmp/scripts/iframe.js > docs/iframe.js",
"build:leader-iframe": "browserify test_tmp/scripts/leader-iframe.js > docs/leader-iframe.js",
"build:lib-browser": "browserify dist/lib/browserify.index.js > dist/lib/browser.js",
"build:lib-browser:min": "uglifyjs --compress --mangle --output dist/lib/browser.min.js -- dist/lib/browser.js",
"build": "npm run clear && concurrently \"npm run build:es6\" \"npm run build:es5\" \"npm run build:test\" && concurrently \"npm run build:index\" \"npm run build:browser\" \"npm run build:worker\" \"npm run build:iframe\" \"npm run build:leader-iframe\" && npm run build:lib-browser && npm run build:lib-browser:min",
"build:min": "uglifyjs --compress --mangle --output dist/lib/browserify.min.js -- dist/lib/browserify.index.js",
"docs:only": "http-server ./docs --silent",
"docs:serve": "npm run build && echo \"Open http://localhost:8080/\" && npm run docs:only"
},
"pre-commit": [
"lint"
],
"dependencies": {
"@babel/runtime": "^7.7.2",
"detect-node": "^2.1.0",
"js-sha3": "0.8.0",
"microseconds": "0.2.0",
"nano-time": "1.0.0",
"oblivious-set": "1.0.0",
"rimraf": "3.0.2",
"unload": "2.2.0"
},
"devDependencies": {
"@babel/cli": "7.14.3",
"@babel/core": "7.14.3",
"@babel/plugin-proposal-object-rest-spread": "7.14.4",
"@babel/plugin-transform-member-expression-literals": "7.12.13",
"@babel/plugin-transform-property-literals": "7.12.13",
"@babel/plugin-transform-runtime": "7.14.3",
"@babel/polyfill": "7.12.1",
"@babel/preset-env": "7.14.4",
"@babel/types": "7.14.4",
"@types/core-js": "2.5.4",
"assert": "2.0.0",
"async-test-util": "1.7.3",
"browserify": "17.0.0",
"child-process-promise": "2.2.1",
"clone": "2.1.2",
"concurrently": "6.2.0",
"convert-hrtime": "5.0.0",
"copyfiles": "2.4.1",
"cross-env": "7.0.3",
"eslint": "7.27.0",
"gzip-size-cli": "5.0.0",
"http-server": "0.12.3",
"jest": "27.0.3",
"karma": "6.3.3",
"karma-babel-preprocessor": "8.0.1",
"karma-browserify": "8.0.0",
"karma-chrome-launcher": "3.1.0",
"karma-coverage": "2.0.3",
"karma-detect-browsers": "2.3.3",
"karma-edge-launcher": "0.4.2",
"karma-firefox-launcher": "2.1.1",
"karma-ie-launcher": "1.0.0",
"karma-mocha": "2.0.1",
"karma-opera-launcher": "1.0.0",
"karma-safari-launcher": "1.0.0",
"mocha": "8.4.0",
"pre-commit": "1.2.2",
"random-int": "3.0.0",
"random-token": "0.0.8",
"rollup": "2.50.5",
"rollup-plugin-node-resolve": "5.2.0",
"rollup-plugin-uglify": "6.0.4",
"testcafe": "1.14.2",
"ts-node": "10.0.0",
"typescript": "4.3.2",
"watchify": "4.0.0",
"webpack": "5.38.1",
"webpack-cli": "4.7.0"
},
"browser": {
"./src/methods/node.js": false,
"./dist/es/methods/node.js": false,
"./dist/lib/methods/node.js": false
}
}

View File

@@ -0,0 +1,272 @@
import {
isPromise
} from './util.js';
import {
chooseMethod
} from './method-chooser.js';
import {
fillOptionsWithDefaults
} from './options.js';
export const BroadcastChannel = function (name, options) {
this.name = name;
if (ENFORCED_OPTIONS) {
options = ENFORCED_OPTIONS;
}
this.options = fillOptionsWithDefaults(options);
this.method = chooseMethod(this.options);
// isListening
this._iL = false;
/**
* _onMessageListener
* setting onmessage twice,
* will overwrite the first listener
*/
this._onML = null;
/**
* _addEventListeners
*/
this._addEL = {
message: [],
internal: []
};
/**
* Unsend message promises
* where the sending is still in progress
* @type {Set<Promise>}
*/
this._uMP = new Set();
/**
* _beforeClose
* array of promises that will be awaited
* before the channel is closed
*/
this._befC = [];
/**
* _preparePromise
*/
this._prepP = null;
_prepareChannel(this);
};
// STATICS
/**
* used to identify if someone overwrites
* window.BroadcastChannel with this
* See methods/native.js
*/
BroadcastChannel._pubkey = true;
/**
* clears the tmp-folder if is node
* @return {Promise<boolean>} true if has run, false if not node
*/
export function clearNodeFolder(options) {
options = fillOptionsWithDefaults(options);
const method = chooseMethod(options);
if (method.type === 'node') {
return method.clearNodeFolder().then(() => true);
} else {
return Promise.resolve(false);
}
}
/**
* if set, this method is enforced,
* no mather what the options are
*/
let ENFORCED_OPTIONS;
export function enforceOptions(options) {
ENFORCED_OPTIONS = options;
}
// PROTOTYPE
BroadcastChannel.prototype = {
postMessage(msg) {
if (this.closed) {
throw new Error(
'BroadcastChannel.postMessage(): ' +
'Cannot post message after channel has closed'
);
}
return _post(this, 'message', msg);
},
postInternal(msg) {
return _post(this, 'internal', msg);
},
set onmessage(fn) {
const time = this.method.microSeconds();
const listenObj = {
time,
fn
};
_removeListenerObject(this, 'message', this._onML);
if (fn && typeof fn === 'function') {
this._onML = listenObj;
_addListenerObject(this, 'message', listenObj);
} else {
this._onML = null;
}
},
addEventListener(type, fn) {
const time = this.method.microSeconds();
const listenObj = {
time,
fn
};
_addListenerObject(this, type, listenObj);
},
removeEventListener(type, fn) {
const obj = this._addEL[type].find(obj => obj.fn === fn);
_removeListenerObject(this, type, obj);
},
close() {
if (this.closed) {
return;
}
this.closed = true;
const awaitPrepare = this._prepP ? this._prepP : Promise.resolve();
this._onML = null;
this._addEL.message = [];
return awaitPrepare
// wait until all current sending are processed
.then(() => Promise.all(Array.from(this._uMP)))
// run before-close hooks
.then(() => Promise.all(this._befC.map(fn => fn())))
// close the channel
.then(() => this.method.close(this._state));
},
get type() {
return this.method.type;
},
get isClosed() {
return this.closed;
}
};
/**
* Post a message over the channel
* @returns {Promise} that resolved when the message sending is done
*/
function _post(broadcastChannel, type, msg) {
const time = broadcastChannel.method.microSeconds();
const msgObj = {
time,
type,
data: msg
};
const awaitPrepare = broadcastChannel._prepP ? broadcastChannel._prepP : Promise.resolve();
return awaitPrepare.then(() => {
const sendPromise = broadcastChannel.method.postMessage(
broadcastChannel._state,
msgObj
);
// add/remove to unsend messages list
broadcastChannel._uMP.add(sendPromise);
sendPromise
.catch()
.then(() => broadcastChannel._uMP.delete(sendPromise));
return sendPromise;
});
}
function _prepareChannel(channel) {
const maybePromise = channel.method.create(channel.name, channel.options);
if (isPromise(maybePromise)) {
channel._prepP = maybePromise;
maybePromise.then(s => {
// used in tests to simulate slow runtime
/*if (channel.options.prepareDelay) {
await new Promise(res => setTimeout(res, this.options.prepareDelay));
}*/
channel._state = s;
});
} else {
channel._state = maybePromise;
}
}
function _hasMessageListeners(channel) {
if (channel._addEL.message.length > 0) return true;
if (channel._addEL.internal.length > 0) return true;
return false;
}
function _addListenerObject(channel, type, obj) {
channel._addEL[type].push(obj);
_startListening(channel);
}
function _removeListenerObject(channel, type, obj) {
channel._addEL[type] = channel._addEL[type].filter(o => o !== obj);
_stopListening(channel);
}
function _startListening(channel) {
if (!channel._iL && _hasMessageListeners(channel)) {
// someone is listening, start subscribing
const listenerFn = msgObj => {
channel._addEL[msgObj.type].forEach(obj => {
if (msgObj.time >= obj.time) {
obj.fn(msgObj.data);
}
});
};
const time = channel.method.microSeconds();
if (channel._prepP) {
channel._prepP.then(() => {
channel._iL = true;
channel.method.onMessage(
channel._state,
listenerFn,
time
);
});
} else {
channel._iL = true;
channel.method.onMessage(
channel._state,
listenerFn,
time
);
}
}
}
function _stopListening(channel) {
if (channel._iL && !_hasMessageListeners(channel)) {
// noone is listening, stop subscribing
channel._iL = false;
const time = channel.method.microSeconds();
channel.method.onMessage(
channel._state,
null,
time
);
}
}

View File

@@ -0,0 +1,6 @@
const module = require('./index.es5.js');
const BroadcastChannel = module.BroadcastChannel;
const createLeaderElection = module.createLeaderElection;
window['BroadcastChannel2'] = BroadcastChannel;
window['createLeaderElection'] = createLeaderElection;

View File

@@ -0,0 +1,24 @@
/**
* because babel can only export on default-attribute,
* we use this for the non-module-build
* this ensures that users do not have to use
* var BroadcastChannel = require('broadcast-channel').default;
* but
* var BroadcastChannel = require('broadcast-channel');
*/
import {
BroadcastChannel,
createLeaderElection,
clearNodeFolder,
enforceOptions,
beLeader
} from './index.js';
module.exports = {
BroadcastChannel,
createLeaderElection,
clearNodeFolder,
enforceOptions,
beLeader
};

9
frontend/node_modules/broadcast-channel/src/index.js generated vendored Normal file
View File

@@ -0,0 +1,9 @@
export {
BroadcastChannel,
clearNodeFolder,
enforceOptions
} from './broadcast-channel';
export {
createLeaderElection,
beLeader
} from './leader-election';

View File

@@ -0,0 +1,230 @@
import {
sleep,
randomToken
} from './util.js';
import unload from 'unload';
const LeaderElection = function (channel, options) {
this._channel = channel;
this._options = options;
this.isLeader = false;
this.isDead = false;
this.token = randomToken();
this._isApl = false; // _isApplying
this._reApply = false;
// things to clean up
this._unl = []; // _unloads
this._lstns = []; // _listeners
this._invs = []; // _intervals
this._dpL = () => { }; // onduplicate listener
this._dpLC = false; // true when onduplicate called
};
LeaderElection.prototype = {
applyOnce() {
if (this.isLeader) return Promise.resolve(false);
if (this.isDead) return Promise.resolve(false);
// do nothing if already running
if (this._isApl) {
this._reApply = true;
return Promise.resolve(false);
}
this._isApl = true;
let stopCriteria = false;
const recieved = [];
const handleMessage = (msg) => {
if (msg.context === 'leader' && msg.token != this.token) {
recieved.push(msg);
if (msg.action === 'apply') {
// other is applying
if (msg.token > this.token) {
// other has higher token, stop applying
stopCriteria = true;
}
}
if (msg.action === 'tell') {
// other is already leader
stopCriteria = true;
}
}
};
this._channel.addEventListener('internal', handleMessage);
const ret = _sendMessage(this, 'apply') // send out that this one is applying
.then(() => sleep(this._options.responseTime)) // let others time to respond
.then(() => {
if (stopCriteria) return Promise.reject(new Error());
else return _sendMessage(this, 'apply');
})
.then(() => sleep(this._options.responseTime)) // let others time to respond
.then(() => {
if (stopCriteria) return Promise.reject(new Error());
else return _sendMessage(this);
})
.then(() => beLeader(this)) // no one disagreed -> this one is now leader
.then(() => true)
.catch(() => false) // apply not successfull
.then(success => {
this._channel.removeEventListener('internal', handleMessage);
this._isApl = false;
if (!success && this._reApply) {
this._reApply = false;
return this.applyOnce();
} else return success;
});
return ret;
},
awaitLeadership() {
if (
/* _awaitLeadershipPromise */
!this._aLP
) {
this._aLP = _awaitLeadershipOnce(this);
}
return this._aLP;
},
set onduplicate(fn) {
this._dpL = fn;
},
die() {
if (this.isDead) return;
this.isDead = true;
this._lstns.forEach(listener => this._channel.removeEventListener('internal', listener));
this._invs.forEach(interval => clearInterval(interval));
this._unl.forEach(uFn => {
uFn.remove();
});
return _sendMessage(this, 'death');
}
};
function _awaitLeadershipOnce(leaderElector) {
if (leaderElector.isLeader) return Promise.resolve();
return new Promise((res) => {
let resolved = false;
function finish() {
if (resolved) {
return;
}
resolved = true;
clearInterval(interval);
leaderElector._channel.removeEventListener('internal', whenDeathListener);
res(true);
}
// try once now
leaderElector.applyOnce().then(() => {
if (leaderElector.isLeader) {
finish();
}
});
// try on fallbackInterval
const interval = setInterval(() => {
leaderElector.applyOnce().then(() => {
if (leaderElector.isLeader) {
finish();
}
});
}, leaderElector._options.fallbackInterval);
leaderElector._invs.push(interval);
// try when other leader dies
const whenDeathListener = msg => {
if (msg.context === 'leader' && msg.action === 'death') {
leaderElector.applyOnce().then(() => {
if (leaderElector.isLeader) finish();
});
}
};
leaderElector._channel.addEventListener('internal', whenDeathListener);
leaderElector._lstns.push(whenDeathListener);
});
}
/**
* sends and internal message over the broadcast-channel
*/
function _sendMessage(leaderElector, action) {
const msgJson = {
context: 'leader',
action,
token: leaderElector.token
};
return leaderElector._channel.postInternal(msgJson);
}
export function beLeader(leaderElector) {
leaderElector.isLeader = true;
const unloadFn = unload.add(() => leaderElector.die());
leaderElector._unl.push(unloadFn);
const isLeaderListener = msg => {
if (msg.context === 'leader' && msg.action === 'apply') {
_sendMessage(leaderElector, 'tell');
}
if (msg.context === 'leader' && msg.action === 'tell' && !leaderElector._dpLC) {
/**
* another instance is also leader!
* This can happen on rare events
* like when the CPU is at 100% for long time
* or the tabs are open very long and the browser throttles them.
* @link https://github.com/pubkey/broadcast-channel/issues/414
* @link https://github.com/pubkey/broadcast-channel/issues/385
*/
leaderElector._dpLC = true;
leaderElector._dpL(); // message the lib user so the app can handle the problem
_sendMessage(leaderElector, 'tell'); // ensure other leader also knows the problem
}
};
leaderElector._channel.addEventListener('internal', isLeaderListener);
leaderElector._lstns.push(isLeaderListener);
return _sendMessage(leaderElector, 'tell');
}
function fillOptionsWithDefaults(options, channel) {
if (!options) options = {};
options = JSON.parse(JSON.stringify(options));
if (!options.fallbackInterval) {
options.fallbackInterval = 3000;
}
if (!options.responseTime) {
options.responseTime = channel.method.averageResponseTime(channel.options);
}
return options;
}
export function createLeaderElection(channel, options) {
if (channel._leaderElector) {
throw new Error('BroadcastChannel already has a leader-elector');
}
options = fillOptionsWithDefaults(options, channel);
const elector = new LeaderElection(channel, options);
channel._befC.push(() => elector.die());
channel._leaderElector = elector;
return elector;
}

View File

@@ -0,0 +1,72 @@
import NativeMethod from './methods/native.js';
import IndexeDbMethod from './methods/indexed-db.js';
import LocalstorageMethod from './methods/localstorage.js';
import SimulateMethod from './methods/simulate.js';
import {
isNode
} from './util';
// order is important
const METHODS = [
NativeMethod, // fastest
IndexeDbMethod,
LocalstorageMethod
];
/**
* The NodeMethod is loaded lazy
* so it will not get bundled in browser-builds
*/
if (isNode) {
/**
* we use the non-transpiled code for nodejs
* because it runs faster
*/
const NodeMethod = require(
'../../src/methods/' +
// use this hack so that browserify and others
// do not import the node-method by default
// when bundling.
'node.js'
);
/**
* this will be false for webpackbuilds
* which will shim the node-method with an empty object {}
*/
if (typeof NodeMethod.canBeUsed === 'function') {
METHODS.push(NodeMethod);
}
}
export function chooseMethod(options) {
let chooseMethods = [].concat(options.methods, METHODS).filter(Boolean);
// directly chosen
if (options.type) {
if (options.type === 'simulate') {
// only use simulate-method if directly chosen
return SimulateMethod;
}
const ret = chooseMethods.find(m => m.type === options.type);
if (!ret) throw new Error('method-type ' + options.type + ' not found');
else return ret;
}
/**
* if no webworker support is needed,
* remove idb from the list so that localstorage is been chosen
*/
if (!options.webWorkerSupport && !isNode) {
chooseMethods = chooseMethods.filter(m => m.type !== 'idb');
}
const useMethod = chooseMethods.find(method => method.canBeUsed());
if (!useMethod)
throw new Error('No useable methode found:' + JSON.stringify(METHODS.map(m => m.type)));
else
return useMethod;
}

View File

@@ -0,0 +1,4 @@
/**
* if you really need this method,
* implement it
*/

View File

@@ -0,0 +1,332 @@
/**
* this method uses indexeddb to store the messages
* There is currently no observerAPI for idb
* @link https://github.com/w3c/IndexedDB/issues/51
*/
import {
sleep,
randomInt,
randomToken,
microSeconds as micro,
isNode
} from '../util.js';
export const microSeconds = micro;
import { ObliviousSet } from 'oblivious-set';
import {
fillOptionsWithDefaults
} from '../options';
const DB_PREFIX = 'pubkey.broadcast-channel-0-';
const OBJECT_STORE_ID = 'messages';
export const type = 'idb';
export function getIdb() {
if (typeof indexedDB !== 'undefined') return indexedDB;
if (typeof window !== 'undefined') {
if (typeof window.mozIndexedDB !== 'undefined') return window.mozIndexedDB;
if (typeof window.webkitIndexedDB !== 'undefined') return window.webkitIndexedDB;
if (typeof window.msIndexedDB !== 'undefined') return window.msIndexedDB;
}
return false;
}
export function createDatabase(channelName) {
const IndexedDB = getIdb();
// create table
const dbName = DB_PREFIX + channelName;
const openRequest = IndexedDB.open(dbName, 1);
openRequest.onupgradeneeded = ev => {
const db = ev.target.result;
db.createObjectStore(OBJECT_STORE_ID, {
keyPath: 'id',
autoIncrement: true
});
};
const dbPromise = new Promise((res, rej) => {
openRequest.onerror = ev => rej(ev);
openRequest.onsuccess = () => {
res(openRequest.result);
};
});
return dbPromise;
}
/**
* writes the new message to the database
* so other readers can find it
*/
export function writeMessage(db, readerUuid, messageJson) {
const time = new Date().getTime();
const writeObject = {
uuid: readerUuid,
time,
data: messageJson
};
const transaction = db.transaction([OBJECT_STORE_ID], 'readwrite');
return new Promise((res, rej) => {
transaction.oncomplete = () => res();
transaction.onerror = ev => rej(ev);
const objectStore = transaction.objectStore(OBJECT_STORE_ID);
objectStore.add(writeObject);
});
}
export function getAllMessages(db) {
const objectStore = db.transaction(OBJECT_STORE_ID).objectStore(OBJECT_STORE_ID);
const ret = [];
return new Promise(res => {
objectStore.openCursor().onsuccess = ev => {
const cursor = ev.target.result;
if (cursor) {
ret.push(cursor.value);
//alert("Name for SSN " + cursor.key + " is " + cursor.value.name);
cursor.continue();
} else {
res(ret);
}
};
});
}
export function getMessagesHigherThan(db, lastCursorId) {
const objectStore = db.transaction(OBJECT_STORE_ID).objectStore(OBJECT_STORE_ID);
const ret = [];
function openCursor() {
// Occasionally Safari will fail on IDBKeyRange.bound, this
// catches that error, having it open the cursor to the first
// item. When it gets data it will advance to the desired key.
try {
const keyRangeValue = IDBKeyRange.bound(lastCursorId + 1, Infinity);
return objectStore.openCursor(keyRangeValue);
} catch (e) {
return objectStore.openCursor();
}
}
return new Promise(res => {
openCursor().onsuccess = ev => {
const cursor = ev.target.result;
if (cursor) {
if (cursor.value.id < lastCursorId + 1) {
cursor.continue(lastCursorId + 1);
} else {
ret.push(cursor.value);
cursor.continue();
}
} else {
res(ret);
}
};
});
}
export function removeMessageById(db, id) {
const request = db.transaction([OBJECT_STORE_ID], 'readwrite')
.objectStore(OBJECT_STORE_ID)
.delete(id);
return new Promise(res => {
request.onsuccess = () => res();
});
}
export function getOldMessages(db, ttl) {
const olderThen = new Date().getTime() - ttl;
const objectStore = db.transaction(OBJECT_STORE_ID).objectStore(OBJECT_STORE_ID);
const ret = [];
return new Promise(res => {
objectStore.openCursor().onsuccess = ev => {
const cursor = ev.target.result;
if (cursor) {
const msgObk = cursor.value;
if (msgObk.time < olderThen) {
ret.push(msgObk);
//alert("Name for SSN " + cursor.key + " is " + cursor.value.name);
cursor.continue();
} else {
// no more old messages,
res(ret);
return;
}
} else {
res(ret);
}
};
});
}
export function cleanOldMessages(db, ttl) {
return getOldMessages(db, ttl)
.then(tooOld => {
return Promise.all(
tooOld.map(msgObj => removeMessageById(db, msgObj.id))
);
});
}
export function create(channelName, options) {
options = fillOptionsWithDefaults(options);
return createDatabase(channelName).then(db => {
const state = {
closed: false,
lastCursorId: 0,
channelName,
options,
uuid: randomToken(),
/**
* emittedMessagesIds
* contains all messages that have been emitted before
* @type {ObliviousSet}
*/
eMIs: new ObliviousSet(options.idb.ttl * 2),
// ensures we do not read messages in parrallel
writeBlockPromise: Promise.resolve(),
messagesCallback: null,
readQueuePromises: [],
db
};
/**
* Handle abrupt closes that do not originate from db.close().
* This could happen, for example, if the underlying storage is
* removed or if the user clears the database in the browser's
* history preferences.
*/
db.onclose = function () {
state.closed = true;
if (options.idb.onclose) options.idb.onclose();
};
/**
* if service-workers are used,
* we have no 'storage'-event if they post a message,
* therefore we also have to set an interval
*/
_readLoop(state);
return state;
});
}
function _readLoop(state) {
if (state.closed) return;
readNewMessages(state)
.then(() => sleep(state.options.idb.fallbackInterval))
.then(() => _readLoop(state));
}
function _filterMessage(msgObj, state) {
if (msgObj.uuid === state.uuid) return false; // send by own
if (state.eMIs.has(msgObj.id)) return false; // already emitted
if (msgObj.data.time < state.messagesCallbackTime) return false; // older then onMessageCallback
return true;
}
/**
* reads all new messages from the database and emits them
*/
function readNewMessages(state) {
// channel already closed
if (state.closed) return Promise.resolve();
// if no one is listening, we do not need to scan for new messages
if (!state.messagesCallback) return Promise.resolve();
return getMessagesHigherThan(state.db, state.lastCursorId)
.then(newerMessages => {
const useMessages = newerMessages
/**
* there is a bug in iOS where the msgObj can be undefined some times
* so we filter them out
* @link https://github.com/pubkey/broadcast-channel/issues/19
*/
.filter(msgObj => !!msgObj)
.map(msgObj => {
if (msgObj.id > state.lastCursorId) {
state.lastCursorId = msgObj.id;
}
return msgObj;
})
.filter(msgObj => _filterMessage(msgObj, state))
.sort((msgObjA, msgObjB) => msgObjA.time - msgObjB.time); // sort by time
useMessages.forEach(msgObj => {
if (state.messagesCallback) {
state.eMIs.add(msgObj.id);
state.messagesCallback(msgObj.data);
}
});
return Promise.resolve();
});
}
export function close(channelState) {
channelState.closed = true;
channelState.db.close();
}
export function postMessage(channelState, messageJson) {
channelState.writeBlockPromise = channelState.writeBlockPromise
.then(() => writeMessage(
channelState.db,
channelState.uuid,
messageJson
))
.then(() => {
if (randomInt(0, 10) === 0) {
/* await (do not await) */
cleanOldMessages(
channelState.db,
channelState.options.idb.ttl
);
}
});
return channelState.writeBlockPromise;
}
export function onMessage(channelState, fn, time) {
channelState.messagesCallbackTime = time;
channelState.messagesCallback = fn;
readNewMessages(channelState);
}
export function canBeUsed() {
if (isNode) return false;
const idb = getIdb();
if (!idb) return false;
return true;
}
export function averageResponseTime(options) {
return options.idb.fallbackInterval * 2;
}
export default {
create,
close,
onMessage,
postMessage,
canBeUsed,
type,
averageResponseTime,
microSeconds
};

View File

@@ -0,0 +1,185 @@
/**
* A localStorage-only method which uses localstorage and its 'storage'-event
* This does not work inside of webworkers because they have no access to locastorage
* This is basically implemented to support IE9 or your grandmothers toaster.
* @link https://caniuse.com/#feat=namevalue-storage
* @link https://caniuse.com/#feat=indexeddb
*/
import { ObliviousSet } from 'oblivious-set';
import {
fillOptionsWithDefaults
} from '../options';
import {
sleep,
randomToken,
microSeconds as micro,
isNode
} from '../util';
export const microSeconds = micro;
const KEY_PREFIX = 'pubkey.broadcastChannel-';
export const type = 'localstorage';
/**
* copied from crosstab
* @link https://github.com/tejacques/crosstab/blob/master/src/crosstab.js#L32
*/
export function getLocalStorage() {
let localStorage;
if (typeof window === 'undefined') return null;
try {
localStorage = window.localStorage;
localStorage = window['ie8-eventlistener/storage'] || window.localStorage;
} catch (e) {
// New versions of Firefox throw a Security exception
// if cookies are disabled. See
// https://bugzilla.mozilla.org/show_bug.cgi?id=1028153
}
return localStorage;
}
export function storageKey(channelName) {
return KEY_PREFIX + channelName;
}
/**
* writes the new message to the storage
* and fires the storage-event so other readers can find it
*/
export function postMessage(channelState, messageJson) {
return new Promise(res => {
sleep().then(() => {
const key = storageKey(channelState.channelName);
const writeObj = {
token: randomToken(),
time: new Date().getTime(),
data: messageJson,
uuid: channelState.uuid
};
const value = JSON.stringify(writeObj);
getLocalStorage().setItem(key, value);
/**
* StorageEvent does not fire the 'storage' event
* in the window that changes the state of the local storage.
* So we fire it manually
*/
const ev = document.createEvent('Event');
ev.initEvent('storage', true, true);
ev.key = key;
ev.newValue = value;
window.dispatchEvent(ev);
res();
});
});
}
export function addStorageEventListener(channelName, fn) {
const key = storageKey(channelName);
const listener = ev => {
if (ev.key === key) {
fn(JSON.parse(ev.newValue));
}
};
window.addEventListener('storage', listener);
return listener;
}
export function removeStorageEventListener(listener) {
window.removeEventListener('storage', listener);
}
export function create(channelName, options) {
options = fillOptionsWithDefaults(options);
if (!canBeUsed()) {
throw new Error('BroadcastChannel: localstorage cannot be used');
}
const uuid = randomToken();
/**
* eMIs
* contains all messages that have been emitted before
* @type {ObliviousSet}
*/
const eMIs = new ObliviousSet(options.localstorage.removeTimeout);
const state = {
channelName,
uuid,
eMIs // emittedMessagesIds
};
state.listener = addStorageEventListener(
channelName,
(msgObj) => {
if (!state.messagesCallback) return; // no listener
if (msgObj.uuid === uuid) return; // own message
if (!msgObj.token || eMIs.has(msgObj.token)) return; // already emitted
if (msgObj.data.time && msgObj.data.time < state.messagesCallbackTime) return; // too old
eMIs.add(msgObj.token);
state.messagesCallback(msgObj.data);
}
);
return state;
}
export function close(channelState) {
removeStorageEventListener(channelState.listener);
}
export function onMessage(channelState, fn, time) {
channelState.messagesCallbackTime = time;
channelState.messagesCallback = fn;
}
export function canBeUsed() {
if (isNode) return false;
const ls = getLocalStorage();
if (!ls) return false;
try {
const key = '__broadcastchannel_check';
ls.setItem(key, 'works');
ls.removeItem(key);
} catch (e) {
// Safari 10 in private mode will not allow write access to local
// storage and fail with a QuotaExceededError. See
// https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API#Private_Browsing_Incognito_modes
return false;
}
return true;
}
export function averageResponseTime() {
const defaultTime = 120;
const userAgent = navigator.userAgent.toLowerCase();
if (userAgent.includes('safari') && !userAgent.includes('chrome')) {
// safari is much slower so this time is higher
return defaultTime * 2;
}
return defaultTime;
}
export default {
create,
close,
onMessage,
postMessage,
canBeUsed,
type,
averageResponseTime,
microSeconds
};

View File

@@ -0,0 +1,76 @@
import {
microSeconds as micro,
isNode
} from '../util';
export const microSeconds = micro;
export const type = 'native';
export function create(channelName) {
const state = {
messagesCallback: null,
bc: new BroadcastChannel(channelName),
subFns: [] // subscriberFunctions
};
state.bc.onmessage = msg => {
if (state.messagesCallback) {
state.messagesCallback(msg.data);
}
};
return state;
}
export function close(channelState) {
channelState.bc.close();
channelState.subFns = [];
}
export function postMessage(channelState, messageJson) {
try {
channelState.bc.postMessage(messageJson, false);
return Promise.resolve();
} catch (err) {
return Promise.reject(err);
}
}
export function onMessage(channelState, fn) {
channelState.messagesCallback = fn;
}
export function canBeUsed() {
/**
* in the electron-renderer, isNode will be true even if we are in browser-context
* so we also check if window is undefined
*/
if (isNode && typeof window === 'undefined') return false;
if (typeof BroadcastChannel === 'function') {
if (BroadcastChannel._pubkey) {
throw new Error(
'BroadcastChannel: Do not overwrite window.BroadcastChannel with this module, this is not a polyfill'
);
}
return true;
} else return false;
}
export function averageResponseTime() {
return 150;
}
export default {
create,
close,
onMessage,
postMessage,
canBeUsed,
type,
averageResponseTime,
microSeconds
};

View File

@@ -0,0 +1,694 @@
/**
* this method is used in nodejs-environments.
* The ipc is handled via sockets and file-writes to the tmp-folder
*/
const util = require('util');
const fs = require('fs');
const os = require('os');
const events = require('events');
const net = require('net');
const path = require('path');
const micro = require('nano-time');
const rimraf = require('rimraf');
const sha3_224 = require('js-sha3').sha3_224;
const isNode = require('detect-node');
const unload = require('unload');
const fillOptionsWithDefaults = require('../../dist/lib/options.js').fillOptionsWithDefaults;
const ownUtil = require('../../dist/lib/util.js');
const randomInt = ownUtil.randomInt;
const randomToken = ownUtil.randomToken;
const { ObliviousSet } = require('oblivious-set');
/**
* windows sucks, so we have handle windows-type of socket-paths
* @link https://gist.github.com/domenic/2790533#gistcomment-331356
*/
function cleanPipeName(str) {
if (
process.platform === 'win32' &&
!str.startsWith('\\\\.\\pipe\\')
) {
str = str.replace(/^\//, '');
str = str.replace(/\//g, '-');
return '\\\\.\\pipe\\' + str;
} else {
return str;
}
}
const mkdir = util.promisify(fs.mkdir);
const writeFile = util.promisify(fs.writeFile);
const readFile = util.promisify(fs.readFile);
const unlink = util.promisify(fs.unlink);
const readdir = util.promisify(fs.readdir);
const chmod = util.promisify(fs.chmod);
const removeDir = util.promisify(rimraf);
const OTHER_INSTANCES = {};
const TMP_FOLDER_NAME = 'pubkey.bc';
const TMP_FOLDER_BASE = path.join(
os.tmpdir(),
TMP_FOLDER_NAME
);
const getPathsCache = new Map();
function getPaths(channelName) {
if (!getPathsCache.has(channelName)) {
const channelHash = sha3_224(channelName); // use hash incase of strange characters
/**
* because the lenght of socket-paths is limited, we use only the first 20 chars
* and also start with A to ensure we do not start with a number
* @link https://serverfault.com/questions/641347/check-if-a-path-exceeds-maximum-for-unix-domain-socket
*/
const channelFolder = 'A' + channelHash.substring(0, 20);
const channelPathBase = path.join(
TMP_FOLDER_BASE,
channelFolder
);
const folderPathReaders = path.join(
channelPathBase,
'rdrs'
);
const folderPathMessages = path.join(
channelPathBase,
'messages'
);
const ret = {
channelBase: channelPathBase,
readers: folderPathReaders,
messages: folderPathMessages
};
getPathsCache.set(channelName, ret);
return ret;
}
return getPathsCache.get(channelName);
}
let ENSURE_BASE_FOLDER_EXISTS_PROMISE = null;
async function ensureBaseFolderExists() {
if (!ENSURE_BASE_FOLDER_EXISTS_PROMISE) {
ENSURE_BASE_FOLDER_EXISTS_PROMISE = mkdir(TMP_FOLDER_BASE).catch(() => null);
}
return ENSURE_BASE_FOLDER_EXISTS_PROMISE;
}
async function ensureFoldersExist(channelName, paths) {
paths = paths || getPaths(channelName);
await ensureBaseFolderExists();
await mkdir(paths.channelBase).catch(() => null);
await Promise.all([
mkdir(paths.readers).catch(() => null),
mkdir(paths.messages).catch(() => null)
]);
// set permissions so other users can use the same channel
const chmodValue = '777';
await Promise.all([
chmod(paths.channelBase, chmodValue),
chmod(paths.readers, chmodValue),
chmod(paths.messages, chmodValue)
]).catch(() => null);
}
/**
* removes the tmp-folder
* @return {Promise<true>}
*/
async function clearNodeFolder() {
if (!TMP_FOLDER_BASE || TMP_FOLDER_BASE === '' || TMP_FOLDER_BASE === '/') {
throw new Error('BroadcastChannel.clearNodeFolder(): path is wrong');
}
ENSURE_BASE_FOLDER_EXISTS_PROMISE = null;
await removeDir(TMP_FOLDER_BASE);
ENSURE_BASE_FOLDER_EXISTS_PROMISE = null;
return true;
}
function socketPath(channelName, readerUuid, paths) {
paths = paths || getPaths(channelName);
const socketPath = path.join(
paths.readers,
readerUuid + '.s'
);
return cleanPipeName(socketPath);
}
function socketInfoPath(channelName, readerUuid, paths) {
paths = paths || getPaths(channelName);
const socketPath = path.join(
paths.readers,
readerUuid + '.json'
);
return socketPath;
}
/**
* Because it is not possible to get all socket-files in a folder,
* when used under fucking windows,
* we have to set a normal file so other readers know our socket exists
*/
function createSocketInfoFile(channelName, readerUuid, paths) {
const pathToFile = socketInfoPath(channelName, readerUuid, paths);
return writeFile(
pathToFile,
JSON.stringify({
time: microSeconds()
})
).then(() => pathToFile);
}
/**
* returns the amount of channel-folders in the tmp-directory
* @return {Promise<number>}
*/
async function countChannelFolders() {
await ensureBaseFolderExists();
const folders = await readdir(TMP_FOLDER_BASE);
return folders.length;
}
async function connectionError(originalError) {
const count = await countChannelFolders();
// we only show the augmented message if there are more then 30 channels
// because we then assume that BroadcastChannel is used in unit-tests
if (count < 30) return originalError;
const addObj = {};
Object.entries(originalError).forEach(([k, v]) => addObj[k] = v);
const text = 'BroadcastChannel.create(): error: ' +
'This might happen if you have created to many channels, ' +
'like when you use BroadcastChannel in unit-tests.' +
'Try using BroadcastChannel.clearNodeFolder() to clear the tmp-folder before each test.' +
'See https://github.com/pubkey/broadcast-channel#clear-tmp-folder';
const newError = new Error(text + ': ' + JSON.stringify(addObj, null, 2));
return newError;
}
/**
* creates the socket-file and subscribes to it
* @return {{emitter: EventEmitter, server: any}}
*/
async function createSocketEventEmitter(channelName, readerUuid, paths) {
const pathToSocket = socketPath(channelName, readerUuid, paths);
const emitter = new events.EventEmitter();
const server = net
.createServer(stream => {
stream.on('end', function () { });
stream.on('data', function (msg) {
emitter.emit('data', msg.toString());
});
});
await new Promise((resolve, reject) => {
server.on('error', async (err) => {
const useErr = await connectionError(err);
reject(useErr);
});
server.listen(pathToSocket, async (err, res) => {
if (err) {
const useErr = await connectionError(err);
reject(useErr);
} else resolve(res);
});
});
return {
path: pathToSocket,
emitter,
server
};
}
async function openClientConnection(channelName, readerUuid) {
const pathToSocket = socketPath(channelName, readerUuid);
const client = new net.Socket();
return new Promise((res, rej) => {
client.connect(
pathToSocket,
() => res(client)
);
client.on('error', err => rej(err));
});
}
/**
* writes the new message to the file-system
* so other readers can find it
* @return {Promise}
*/
function writeMessage(channelName, readerUuid, messageJson, paths) {
paths = paths || getPaths(channelName);
const time = microSeconds();
const writeObject = {
uuid: readerUuid,
time,
data: messageJson
};
const token = randomToken();
const fileName = time + '_' + readerUuid + '_' + token + '.json';
const msgPath = path.join(
paths.messages,
fileName
);
return writeFile(
msgPath,
JSON.stringify(writeObject)
).then(() => {
return {
time,
uuid: readerUuid,
token,
path: msgPath
};
});
}
/**
* returns the uuids of all readers
* @return {string[]}
*/
async function getReadersUuids(channelName, paths) {
paths = paths || getPaths(channelName);
const readersPath = paths.readers;
const files = await readdir(readersPath);
return files
.map(file => file.split('.'))
.filter(split => split[1] === 'json') // do not scan .socket-files
.map(split => split[0]);
}
async function messagePath(channelName, time, token, writerUuid) {
const fileName = time + '_' + writerUuid + '_' + token + '.json';
const msgPath = path.join(
getPaths(channelName).messages,
fileName
);
return msgPath;
}
async function getAllMessages(channelName, paths) {
paths = paths || getPaths(channelName);
const messagesPath = paths.messages;
const files = await readdir(messagesPath);
return files.map(file => {
const fileName = file.split('.')[0];
const split = fileName.split('_');
return {
path: path.join(
messagesPath,
file
),
time: parseInt(split[0]),
senderUuid: split[1],
token: split[2]
};
});
}
function getSingleMessage(channelName, msgObj, paths) {
paths = paths || getPaths(channelName);
return {
path: path.join(
paths.messages,
msgObj.t + '_' + msgObj.u + '_' + msgObj.to + '.json'
),
time: msgObj.t,
senderUuid: msgObj.u,
token: msgObj.to
};
}
function readMessage(messageObj) {
return readFile(messageObj.path, 'utf8')
.then(content => JSON.parse(content));
}
async function cleanOldMessages(messageObjects, ttl) {
const olderThen = Date.now() - ttl;
await Promise.all(
messageObjects
.filter(obj => (obj.time / 1000) < olderThen)
.map(obj => unlink(obj.path).catch(() => null))
);
}
const type = 'node';
/**
* creates a new channelState
* @return {Promise<any>}
*/
async function create(channelName, options = {}) {
options = fillOptionsWithDefaults(options);
const time = microSeconds();
const paths = getPaths(channelName);
const ensureFolderExistsPromise = ensureFoldersExist(channelName, paths);
const uuid = randomToken();
const state = {
time,
channelName,
options,
uuid,
paths,
// contains all messages that have been emitted before
emittedMessagesIds: new ObliviousSet(options.node.ttl * 2),
messagesCallbackTime: null,
messagesCallback: null,
// ensures we do not read messages in parrallel
writeBlockPromise: Promise.resolve(),
otherReaderClients: {},
// ensure if process crashes, everything is cleaned up
removeUnload: unload.add(() => close(state)),
closed: false
};
if (!OTHER_INSTANCES[channelName]) OTHER_INSTANCES[channelName] = [];
OTHER_INSTANCES[channelName].push(state);
await ensureFolderExistsPromise;
const [
socketEE,
infoFilePath
] = await Promise.all([
createSocketEventEmitter(channelName, uuid, paths),
createSocketInfoFile(channelName, uuid, paths),
refreshReaderClients(state)
]);
state.socketEE = socketEE;
state.infoFilePath = infoFilePath;
// when new message comes in, we read it and emit it
socketEE.emitter.on('data', data => {
// if the socket is used fast, it may appear that multiple messages are flushed at once
// so we have to split them before
const singleOnes = data.split('|');
singleOnes
.filter(single => single !== '')
.forEach(single => {
try {
const obj = JSON.parse(single);
handleMessagePing(state, obj);
} catch (err) {
throw new Error('could not parse data: ' + single);
}
});
});
return state;
}
function _filterMessage(msgObj, state) {
if (msgObj.senderUuid === state.uuid) return false; // not send by own
if (state.emittedMessagesIds.has(msgObj.token)) return false; // not already emitted
if (!state.messagesCallback) return false; // no listener
if (msgObj.time < state.messagesCallbackTime) return false; // not older then onMessageCallback
if (msgObj.time < state.time) return false; // msgObj is older then channel
state.emittedMessagesIds.add(msgObj.token);
return true;
}
/**
* when the socket pings, so that we now new messages came,
* run this
*/
async function handleMessagePing(state, msgObj) {
/**
* when there are no listener, we do nothing
*/
if (!state.messagesCallback) return;
let messages;
if (!msgObj) {
// get all
messages = await getAllMessages(state.channelName, state.paths);
} else {
// get single message
messages = [
getSingleMessage(state.channelName, msgObj, state.paths)
];
}
const useMessages = messages
.filter(msgObj => _filterMessage(msgObj, state))
.sort((msgObjA, msgObjB) => msgObjA.time - msgObjB.time); // sort by time
// if no listener or message, so not do anything
if (!useMessages.length || !state.messagesCallback) return;
// read contents
await Promise.all(
useMessages
.map(
msgObj => readMessage(msgObj).then(content => msgObj.content = content)
)
);
useMessages.forEach(msgObj => {
state.emittedMessagesIds.add(msgObj.token);
if (state.messagesCallback) {
// emit to subscribers
state.messagesCallback(msgObj.content.data);
}
});
}
/**
* ensures that the channelState is connected with all other readers
* @return {Promise<void>}
*/
function refreshReaderClients(channelState) {
return getReadersUuids(channelState.channelName, channelState.paths)
.then(otherReaders => {
// remove subscriptions to closed readers
Object.keys(channelState.otherReaderClients)
.filter(readerUuid => !otherReaders.includes(readerUuid))
.forEach(async (readerUuid) => {
try {
await channelState.otherReaderClients[readerUuid].destroy();
} catch (err) { }
delete channelState.otherReaderClients[readerUuid];
});
// add new readers
return Promise.all(
otherReaders
.filter(readerUuid => readerUuid !== channelState.uuid) // not own
.filter(readerUuid => !channelState.otherReaderClients[readerUuid]) // not already has client
.map(async (readerUuid) => {
try {
if (channelState.closed) return;
try {
const client = await openClientConnection(channelState.channelName, readerUuid);
channelState.otherReaderClients[readerUuid] = client;
} catch (err) {
// this can throw when the cleanup of another channel was interrupted
// or the socket-file does not exits yet
}
} catch (err) {
// this might throw if the other channel is closed at the same time when this one is running refresh
// so we do not throw an error
}
})
);
});
}
/**
* post a message to the other readers
* @return {Promise<void>}
*/
function postMessage(channelState, messageJson) {
const writePromise = writeMessage(
channelState.channelName,
channelState.uuid,
messageJson,
channelState.paths
);
channelState.writeBlockPromise = channelState.writeBlockPromise.then(async () => {
// w8 one tick to let the buffer flush
await new Promise(res => setTimeout(res, 0));
const [msgObj] = await Promise.all([
writePromise,
refreshReaderClients(channelState)
]);
emitOverFastPath(channelState, msgObj, messageJson);
const pingStr = '{"t":' + msgObj.time + ',"u":"' + msgObj.uuid + '","to":"' + msgObj.token + '"}|';
const writeToReadersPromise = Promise.all(
Object.values(channelState.otherReaderClients)
.filter(client => client.writable) // client might have closed in between
.map(client => {
return new Promise(res => {
client.write(pingStr, res);
});
})
);
/**
* clean up old messages
* to not waste resources on cleaning up,
* only if random-int matches, we clean up old messages
*/
if (randomInt(0, 20) === 0) {
/* await */
getAllMessages(channelState.channelName, channelState.paths)
.then(allMessages => cleanOldMessages(allMessages, channelState.options.node.ttl));
}
return writeToReadersPromise;
});
return channelState.writeBlockPromise;
}
/**
* When multiple BroadcastChannels with the same name
* are created in a single node-process, we can access them directly and emit messages.
* This might not happen often in production
* but will speed up things when this module is used in unit-tests.
*/
function emitOverFastPath(state, msgObj, messageJson) {
if (!state.options.node.useFastPath) return; // disabled
const others = OTHER_INSTANCES[state.channelName].filter(s => s !== state);
const checkObj = {
time: msgObj.time,
senderUuid: msgObj.uuid,
token: msgObj.token
};
others
.filter(otherState => _filterMessage(checkObj, otherState))
.forEach(otherState => {
otherState.messagesCallback(messageJson);
});
}
function onMessage(channelState, fn, time = microSeconds()) {
channelState.messagesCallbackTime = time;
channelState.messagesCallback = fn;
handleMessagePing(channelState);
}
/**
* closes the channel
* @return {Promise}
*/
function close(channelState) {
if (channelState.closed) return;
channelState.closed = true;
channelState.emittedMessagesIds.clear();
OTHER_INSTANCES[channelState.channelName] = OTHER_INSTANCES[channelState.channelName].filter(o => o !== channelState);
if (channelState.removeUnload) {
channelState.removeUnload.remove();
}
return new Promise((res) => {
if (channelState.socketEE)
channelState.socketEE.emitter.removeAllListeners();
Object.values(channelState.otherReaderClients)
.forEach(client => client.destroy());
if (channelState.infoFilePath) {
try {
fs.unlinkSync(channelState.infoFilePath);
} catch (err) { }
}
/**
* the server get closed lazy because others might still write on it
* and have not found out that the infoFile was deleted
*/
setTimeout(() => {
channelState.socketEE.server.close();
res();
}, 200);
});
}
function canBeUsed() {
return isNode;
}
/**
* on node we use a relatively height averageResponseTime,
* because the file-io might be in use.
* Also it is more important that the leader-election is reliable,
* then to have a fast election.
*/
function averageResponseTime() {
return 200;
}
function microSeconds() {
return parseInt(micro.microseconds());
}
module.exports = {
TMP_FOLDER_BASE,
cleanPipeName,
getPaths,
ensureFoldersExist,
clearNodeFolder,
socketPath,
socketInfoPath,
createSocketInfoFile,
countChannelFolders,
createSocketEventEmitter,
openClientConnection,
writeMessage,
getReadersUuids,
messagePath,
getAllMessages,
getSingleMessage,
readMessage,
cleanOldMessages,
type,
create,
_filterMessage,
handleMessagePing,
refreshReaderClients,
postMessage,
emitOverFastPath,
onMessage,
close,
canBeUsed,
averageResponseTime,
microSeconds
};

View File

@@ -0,0 +1,59 @@
import {
microSeconds as micro,
} from '../util';
export const microSeconds = micro;
export const type = 'simulate';
const SIMULATE_CHANNELS = new Set();
export function create(channelName) {
const state = {
name: channelName,
messagesCallback: null
};
SIMULATE_CHANNELS.add(state);
return state;
}
export function close(channelState) {
SIMULATE_CHANNELS.delete(channelState);
}
export function postMessage(channelState, messageJson) {
return new Promise(res => setTimeout(() => {
const channelArray = Array.from(SIMULATE_CHANNELS);
channelArray
.filter(channel => channel.name === channelState.name)
.filter(channel => channel !== channelState)
.filter(channel => !!channel.messagesCallback)
.forEach(channel => channel.messagesCallback(messageJson));
res();
}, 5));
}
export function onMessage(channelState, fn) {
channelState.messagesCallback = fn;
}
export function canBeUsed() {
return true;
}
export function averageResponseTime() {
return 5;
}
export default {
create,
close,
onMessage,
postMessage,
canBeUsed,
type,
averageResponseTime,
microSeconds
};

30
frontend/node_modules/broadcast-channel/src/options.js generated vendored Normal file
View File

@@ -0,0 +1,30 @@
export function fillOptionsWithDefaults(originalOptions = {}) {
const options = JSON.parse(JSON.stringify(originalOptions));
// main
if (typeof options.webWorkerSupport === 'undefined') options.webWorkerSupport = true;
// indexed-db
if (!options.idb) options.idb = {};
// after this time the messages get deleted
if (!options.idb.ttl) options.idb.ttl = 1000 * 45;
if (!options.idb.fallbackInterval) options.idb.fallbackInterval = 150;
// handles abrupt db onclose events.
if (originalOptions.idb && typeof originalOptions.idb.onclose === 'function')
options.idb.onclose = originalOptions.idb.onclose;
// localstorage
if (!options.localstorage) options.localstorage = {};
if (!options.localstorage.removeTimeout) options.localstorage.removeTimeout = 1000 * 60;
// custom methods
if (originalOptions.methods) options.methods = originalOptions.methods;
// node
if (!options.node) options.node = {};
if (!options.node.ttl) options.node.ttl = 1000 * 60 * 2; // 2 minutes;
if (typeof options.node.useFastPath === 'undefined') options.node.useFastPath = true;
return options;
}

57
frontend/node_modules/broadcast-channel/src/util.js generated vendored Normal file
View File

@@ -0,0 +1,57 @@
/**
* returns true if the given object is a promise
*/
export function isPromise(obj) {
if (obj &&
typeof obj.then === 'function') {
return true;
} else {
return false;
}
}
export function sleep(time) {
if (!time) time = 0;
return new Promise(res => setTimeout(res, time));
}
export function randomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1) + min);
}
/**
* https://stackoverflow.com/a/8084248
*/
export function randomToken() {
return Math.random().toString(36).substring(2);
}
let lastMs = 0;
let additional = 0;
/**
* returns the current time in micro-seconds,
* WARNING: This is a pseudo-function
* Performance.now is not reliable in webworkers, so we just make sure to never return the same time.
* This is enough in browsers, and this function will not be used in nodejs.
* The main reason for this hack is to ensure that BroadcastChannel behaves equal to production when it is used in fast-running unit tests.
*/
export function microSeconds() {
const ms = new Date().getTime();
if (ms === lastMs) {
additional++;
return ms * 1000 + additional;
} else {
lastMs = ms;
additional = 0;
return ms * 1000;
}
}
/**
* copied from the 'detect-node' npm module
* We cannot use the module directly because it causes problems with rollup
* @link https://github.com/iliakan/detect-node/blob/master/index.js
*/
export const isNode = Object.prototype.toString.call(typeof process !== 'undefined' ? process : 0) === '[object process]';

View File

@@ -0,0 +1,65 @@
declare type MethodType = 'node' | 'idb' | 'native' | 'localstorage' | 'simulate';
interface BroadcastChannelEventMap {
"message": MessageEvent;
"messageerror": MessageEvent;
}
export interface BroadcastMethod<State = object> {
type: string;
microSeconds(): number;
create(channelName: string, options: BroadcastChannelOptions): Promise<State> | State;
close(channelState: State): void;
onMessage(channelState: State, callback: (args: any) => void): void;
postMessage(channelState: State, message: any): Promise<any>;
canBeUsed(): boolean;
averageResponseTime(): number;
}
export type BroadcastChannelOptions = {
type?: MethodType,
methods?: BroadcastMethod[] | BroadcastMethod,
webWorkerSupport?: boolean;
prepareDelay?: number;
node?: {
ttl?: number;
useFastPath?: boolean;
};
idb?: {
ttl?: number;
fallbackInterval?: number;
onclose?: () => void;
};
};
declare type EventContext = 'message' | 'internal' | 'leader';
declare type OnMessageHandler<T> = ((this: BroadcastChannel, ev: T) => any) | null;
/**
* api as defined in
* @link https://html.spec.whatwg.org/multipage/web-messaging.html#broadcasting-to-other-browsing-contexts
* @link https://github.com/Microsoft/TypeScript/blob/master/src/lib/webworker.generated.d.ts#L325
*/
export class BroadcastChannel<T = any> {
constructor(name: string, opts?: BroadcastChannelOptions);
readonly name: string;
readonly options: BroadcastChannelOptions;
readonly type: MethodType;
readonly isClosed: boolean;
postMessage(msg: T): Promise<void>;
close(): Promise<void>;
onmessage: OnMessageHandler<T>;
// not defined in the offical standard
addEventListener(type: EventContext, handler: OnMessageHandler<T>): void;
removeEventListener(type: EventContext, handler: OnMessageHandler<T>): void;
}
// statics
export function clearNodeFolder(opts?: BroadcastChannelOptions): Promise<boolean>;
export function enforceOptions(opts?: BroadcastChannelOptions | false | null): void;

View File

@@ -0,0 +1,2 @@
export * from './broadcast-channel';
export * from './leader-election';

View File

@@ -0,0 +1,44 @@
import {
BroadcastChannel,
OnMessageHandler
} from './broadcast-channel';
export type LeaderElectionOptions = {
/**
* This value decides how often instances will renegotiate who is leader.
* Probably should be at least 2x bigger than responseTime.
*/
fallbackInterval?: number;
/**
* This timer value is used when resolving which instance should be leader.
* In case when your application elects more than one leader increase this value.
*/
responseTime?: number;
};
export declare class LeaderElector {
/**
* IMPORTANT: The leader election is lazy,
* it will not start before you call awaitLeadership()
* so isLeader will never become true then
*/
readonly isLeader: boolean;
readonly isDead: boolean;
readonly token: string;
applyOnce(): Promise<boolean>;
awaitLeadership(): Promise<void>;
die(): Promise<void>;
/**
* Add an event handler that is run
* when it is detected that there are duplicate leaders
*/
onduplicate: OnMessageHandler<any>;
}
type CreateFunction = (channel: BroadcastChannel, options?: LeaderElectionOptions) => LeaderElector;
export const createLeaderElection: CreateFunction;