node_modules ignore

This commit is contained in:
2025-05-08 23:43:47 +02:00
parent e19d52f172
commit 4574544c9f
65041 changed files with 10593536 additions and 0 deletions

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;

24
server/node_modules/broadcast-channel/src/index.es5.js generated vendored Normal file
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
server/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
server/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
server/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]';