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,3 @@
# These are supported funding model platforms
github: pubkey

262
server/node_modules/broadcast-channel/.github/README.md generated vendored Normal file
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
server/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
server/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
server/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;
}

55
server/node_modules/broadcast-channel/dist/es/util.js generated vendored Normal file
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);
}
}

1913
server/node_modules/broadcast-channel/dist/lib/browser.js generated vendored Normal file

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;
}

73
server/node_modules/broadcast-channel/dist/lib/util.js generated vendored Normal file
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;

View File

@@ -0,0 +1 @@
../rimraf/bin.js

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2013 Julian Gruber <julian@juliangruber.com>
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.

View File

@@ -0,0 +1,129 @@
# brace-expansion
[Brace expansion](https://www.gnu.org/software/bash/manual/html_node/Brace-Expansion.html),
as known from sh/bash, in JavaScript.
[![build status](https://secure.travis-ci.org/juliangruber/brace-expansion.svg)](http://travis-ci.org/juliangruber/brace-expansion)
[![downloads](https://img.shields.io/npm/dm/brace-expansion.svg)](https://www.npmjs.org/package/brace-expansion)
[![Greenkeeper badge](https://badges.greenkeeper.io/juliangruber/brace-expansion.svg)](https://greenkeeper.io/)
[![testling badge](https://ci.testling.com/juliangruber/brace-expansion.png)](https://ci.testling.com/juliangruber/brace-expansion)
## Example
```js
var expand = require('brace-expansion');
expand('file-{a,b,c}.jpg')
// => ['file-a.jpg', 'file-b.jpg', 'file-c.jpg']
expand('-v{,,}')
// => ['-v', '-v', '-v']
expand('file{0..2}.jpg')
// => ['file0.jpg', 'file1.jpg', 'file2.jpg']
expand('file-{a..c}.jpg')
// => ['file-a.jpg', 'file-b.jpg', 'file-c.jpg']
expand('file{2..0}.jpg')
// => ['file2.jpg', 'file1.jpg', 'file0.jpg']
expand('file{0..4..2}.jpg')
// => ['file0.jpg', 'file2.jpg', 'file4.jpg']
expand('file-{a..e..2}.jpg')
// => ['file-a.jpg', 'file-c.jpg', 'file-e.jpg']
expand('file{00..10..5}.jpg')
// => ['file00.jpg', 'file05.jpg', 'file10.jpg']
expand('{{A..C},{a..c}}')
// => ['A', 'B', 'C', 'a', 'b', 'c']
expand('ppp{,config,oe{,conf}}')
// => ['ppp', 'pppconfig', 'pppoe', 'pppoeconf']
```
## API
```js
var expand = require('brace-expansion');
```
### var expanded = expand(str)
Return an array of all possible and valid expansions of `str`. If none are
found, `[str]` is returned.
Valid expansions are:
```js
/^(.*,)+(.+)?$/
// {a,b,...}
```
A comma separated list of options, like `{a,b}` or `{a,{b,c}}` or `{,a,}`.
```js
/^-?\d+\.\.-?\d+(\.\.-?\d+)?$/
// {x..y[..incr]}
```
A numeric sequence from `x` to `y` inclusive, with optional increment.
If `x` or `y` start with a leading `0`, all the numbers will be padded
to have equal length. Negative numbers and backwards iteration work too.
```js
/^-?\d+\.\.-?\d+(\.\.-?\d+)?$/
// {x..y[..incr]}
```
An alphabetic sequence from `x` to `y` inclusive, with optional increment.
`x` and `y` must be exactly one character, and if given, `incr` must be a
number.
For compatibility reasons, the string `${` is not eligible for brace expansion.
## Installation
With [npm](https://npmjs.org) do:
```bash
npm install brace-expansion
```
## Contributors
- [Julian Gruber](https://github.com/juliangruber)
- [Isaac Z. Schlueter](https://github.com/isaacs)
## Sponsors
This module is proudly supported by my [Sponsors](https://github.com/juliangruber/sponsors)!
Do you want to support modules like this to improve their quality, stability and weigh in on new features? Then please consider donating to my [Patreon](https://www.patreon.com/juliangruber). Not sure how much of my modules you're using? Try [feross/thanks](https://github.com/feross/thanks)!
## License
(MIT)
Copyright (c) 2013 Julian Gruber &lt;julian@juliangruber.com&gt;
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.

View File

@@ -0,0 +1,201 @@
var concatMap = require('concat-map');
var balanced = require('balanced-match');
module.exports = expandTop;
var escSlash = '\0SLASH'+Math.random()+'\0';
var escOpen = '\0OPEN'+Math.random()+'\0';
var escClose = '\0CLOSE'+Math.random()+'\0';
var escComma = '\0COMMA'+Math.random()+'\0';
var escPeriod = '\0PERIOD'+Math.random()+'\0';
function numeric(str) {
return parseInt(str, 10) == str
? parseInt(str, 10)
: str.charCodeAt(0);
}
function escapeBraces(str) {
return str.split('\\\\').join(escSlash)
.split('\\{').join(escOpen)
.split('\\}').join(escClose)
.split('\\,').join(escComma)
.split('\\.').join(escPeriod);
}
function unescapeBraces(str) {
return str.split(escSlash).join('\\')
.split(escOpen).join('{')
.split(escClose).join('}')
.split(escComma).join(',')
.split(escPeriod).join('.');
}
// Basically just str.split(","), but handling cases
// where we have nested braced sections, which should be
// treated as individual members, like {a,{b,c},d}
function parseCommaParts(str) {
if (!str)
return [''];
var parts = [];
var m = balanced('{', '}', str);
if (!m)
return str.split(',');
var pre = m.pre;
var body = m.body;
var post = m.post;
var p = pre.split(',');
p[p.length-1] += '{' + body + '}';
var postParts = parseCommaParts(post);
if (post.length) {
p[p.length-1] += postParts.shift();
p.push.apply(p, postParts);
}
parts.push.apply(parts, p);
return parts;
}
function expandTop(str) {
if (!str)
return [];
// I don't know why Bash 4.3 does this, but it does.
// Anything starting with {} will have the first two bytes preserved
// but *only* at the top level, so {},a}b will not expand to anything,
// but a{},b}c will be expanded to [a}c,abc].
// One could argue that this is a bug in Bash, but since the goal of
// this module is to match Bash's rules, we escape a leading {}
if (str.substr(0, 2) === '{}') {
str = '\\{\\}' + str.substr(2);
}
return expand(escapeBraces(str), true).map(unescapeBraces);
}
function identity(e) {
return e;
}
function embrace(str) {
return '{' + str + '}';
}
function isPadded(el) {
return /^-?0\d/.test(el);
}
function lte(i, y) {
return i <= y;
}
function gte(i, y) {
return i >= y;
}
function expand(str, isTop) {
var expansions = [];
var m = balanced('{', '}', str);
if (!m || /\$$/.test(m.pre)) return [str];
var isNumericSequence = /^-?\d+\.\.-?\d+(?:\.\.-?\d+)?$/.test(m.body);
var isAlphaSequence = /^[a-zA-Z]\.\.[a-zA-Z](?:\.\.-?\d+)?$/.test(m.body);
var isSequence = isNumericSequence || isAlphaSequence;
var isOptions = m.body.indexOf(',') >= 0;
if (!isSequence && !isOptions) {
// {a},b}
if (m.post.match(/,.*\}/)) {
str = m.pre + '{' + m.body + escClose + m.post;
return expand(str);
}
return [str];
}
var n;
if (isSequence) {
n = m.body.split(/\.\./);
} else {
n = parseCommaParts(m.body);
if (n.length === 1) {
// x{{a,b}}y ==> x{a}y x{b}y
n = expand(n[0], false).map(embrace);
if (n.length === 1) {
var post = m.post.length
? expand(m.post, false)
: [''];
return post.map(function(p) {
return m.pre + n[0] + p;
});
}
}
}
// at this point, n is the parts, and we know it's not a comma set
// with a single entry.
// no need to expand pre, since it is guaranteed to be free of brace-sets
var pre = m.pre;
var post = m.post.length
? expand(m.post, false)
: [''];
var N;
if (isSequence) {
var x = numeric(n[0]);
var y = numeric(n[1]);
var width = Math.max(n[0].length, n[1].length)
var incr = n.length == 3
? Math.abs(numeric(n[2]))
: 1;
var test = lte;
var reverse = y < x;
if (reverse) {
incr *= -1;
test = gte;
}
var pad = n.some(isPadded);
N = [];
for (var i = x; test(i, y); i += incr) {
var c;
if (isAlphaSequence) {
c = String.fromCharCode(i);
if (c === '\\')
c = '';
} else {
c = String(i);
if (pad) {
var need = width - c.length;
if (need > 0) {
var z = new Array(need + 1).join('0');
if (i < 0)
c = '-' + z + c.slice(1);
else
c = z + c;
}
}
}
N.push(c);
}
} else {
N = concatMap(n, function(el) { return expand(el, false) });
}
for (var j = 0; j < N.length; j++) {
for (var k = 0; k < post.length; k++) {
var expansion = pre + N[j] + post[k];
if (!isTop || isSequence || expansion)
expansions.push(expansion);
}
}
return expansions;
}

View File

@@ -0,0 +1,47 @@
{
"name": "brace-expansion",
"description": "Brace expansion as known from sh/bash",
"version": "1.1.11",
"repository": {
"type": "git",
"url": "git://github.com/juliangruber/brace-expansion.git"
},
"homepage": "https://github.com/juliangruber/brace-expansion",
"main": "index.js",
"scripts": {
"test": "tape test/*.js",
"gentest": "bash test/generate.sh",
"bench": "matcha test/perf/bench.js"
},
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
},
"devDependencies": {
"matcha": "^0.7.0",
"tape": "^4.6.0"
},
"keywords": [],
"author": {
"name": "Julian Gruber",
"email": "mail@juliangruber.com",
"url": "http://juliangruber.com"
},
"license": "MIT",
"testling": {
"files": "test/*.js",
"browsers": [
"ie/8..latest",
"firefox/20..latest",
"firefox/nightly",
"chrome/25..latest",
"chrome/canary",
"opera/12..latest",
"opera/next",
"safari/5.1..latest",
"ipad/6.0..latest",
"iphone/6.0..latest",
"android-browser/4.2..latest"
]
}
}

View File

@@ -0,0 +1,21 @@
The ISC License
Copyright (c) Isaac Z. Schlueter and Contributors
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
## Glob Logo
Glob's logo created by Tanya Brassie <http://tanyabrassie.com/>, licensed
under a Creative Commons Attribution-ShareAlike 4.0 International License
https://creativecommons.org/licenses/by-sa/4.0/

View File

@@ -0,0 +1,378 @@
# Glob
Match files using the patterns the shell uses, like stars and stuff.
[![Build Status](https://travis-ci.org/isaacs/node-glob.svg?branch=master)](https://travis-ci.org/isaacs/node-glob/) [![Build Status](https://ci.appveyor.com/api/projects/status/kd7f3yftf7unxlsx?svg=true)](https://ci.appveyor.com/project/isaacs/node-glob) [![Coverage Status](https://coveralls.io/repos/isaacs/node-glob/badge.svg?branch=master&service=github)](https://coveralls.io/github/isaacs/node-glob?branch=master)
This is a glob implementation in JavaScript. It uses the `minimatch`
library to do its matching.
![a fun cartoon logo made of glob characters](logo/glob.png)
## Usage
Install with npm
```
npm i glob
```
```javascript
var glob = require("glob")
// options is optional
glob("**/*.js", options, function (er, files) {
// files is an array of filenames.
// If the `nonull` option is set, and nothing
// was found, then files is ["**/*.js"]
// er is an error object or null.
})
```
## Glob Primer
"Globs" are the patterns you type when you do stuff like `ls *.js` on
the command line, or put `build/*` in a `.gitignore` file.
Before parsing the path part patterns, braced sections are expanded
into a set. Braced sections start with `{` and end with `}`, with any
number of comma-delimited sections within. Braced sections may contain
slash characters, so `a{/b/c,bcd}` would expand into `a/b/c` and `abcd`.
The following characters have special magic meaning when used in a
path portion:
* `*` Matches 0 or more characters in a single path portion
* `?` Matches 1 character
* `[...]` Matches a range of characters, similar to a RegExp range.
If the first character of the range is `!` or `^` then it matches
any character not in the range.
* `!(pattern|pattern|pattern)` Matches anything that does not match
any of the patterns provided.
* `?(pattern|pattern|pattern)` Matches zero or one occurrence of the
patterns provided.
* `+(pattern|pattern|pattern)` Matches one or more occurrences of the
patterns provided.
* `*(a|b|c)` Matches zero or more occurrences of the patterns provided
* `@(pattern|pat*|pat?erN)` Matches exactly one of the patterns
provided
* `**` If a "globstar" is alone in a path portion, then it matches
zero or more directories and subdirectories searching for matches.
It does not crawl symlinked directories.
### Dots
If a file or directory path portion has a `.` as the first character,
then it will not match any glob pattern unless that pattern's
corresponding path part also has a `.` as its first character.
For example, the pattern `a/.*/c` would match the file at `a/.b/c`.
However the pattern `a/*/c` would not, because `*` does not start with
a dot character.
You can make glob treat dots as normal characters by setting
`dot:true` in the options.
### Basename Matching
If you set `matchBase:true` in the options, and the pattern has no
slashes in it, then it will seek for any file anywhere in the tree
with a matching basename. For example, `*.js` would match
`test/simple/basic.js`.
### Empty Sets
If no matching files are found, then an empty array is returned. This
differs from the shell, where the pattern itself is returned. For
example:
$ echo a*s*d*f
a*s*d*f
To get the bash-style behavior, set the `nonull:true` in the options.
### See Also:
* `man sh`
* `man bash` (Search for "Pattern Matching")
* `man 3 fnmatch`
* `man 5 gitignore`
* [minimatch documentation](https://github.com/isaacs/minimatch)
## glob.hasMagic(pattern, [options])
Returns `true` if there are any special characters in the pattern, and
`false` otherwise.
Note that the options affect the results. If `noext:true` is set in
the options object, then `+(a|b)` will not be considered a magic
pattern. If the pattern has a brace expansion, like `a/{b/c,x/y}`
then that is considered magical, unless `nobrace:true` is set in the
options.
## glob(pattern, [options], cb)
* `pattern` `{String}` Pattern to be matched
* `options` `{Object}`
* `cb` `{Function}`
* `err` `{Error | null}`
* `matches` `{Array<String>}` filenames found matching the pattern
Perform an asynchronous glob search.
## glob.sync(pattern, [options])
* `pattern` `{String}` Pattern to be matched
* `options` `{Object}`
* return: `{Array<String>}` filenames found matching the pattern
Perform a synchronous glob search.
## Class: glob.Glob
Create a Glob object by instantiating the `glob.Glob` class.
```javascript
var Glob = require("glob").Glob
var mg = new Glob(pattern, options, cb)
```
It's an EventEmitter, and starts walking the filesystem to find matches
immediately.
### new glob.Glob(pattern, [options], [cb])
* `pattern` `{String}` pattern to search for
* `options` `{Object}`
* `cb` `{Function}` Called when an error occurs, or matches are found
* `err` `{Error | null}`
* `matches` `{Array<String>}` filenames found matching the pattern
Note that if the `sync` flag is set in the options, then matches will
be immediately available on the `g.found` member.
### Properties
* `minimatch` The minimatch object that the glob uses.
* `options` The options object passed in.
* `aborted` Boolean which is set to true when calling `abort()`. There
is no way at this time to continue a glob search after aborting, but
you can re-use the statCache to avoid having to duplicate syscalls.
* `cache` Convenience object. Each field has the following possible
values:
* `false` - Path does not exist
* `true` - Path exists
* `'FILE'` - Path exists, and is not a directory
* `'DIR'` - Path exists, and is a directory
* `[file, entries, ...]` - Path exists, is a directory, and the
array value is the results of `fs.readdir`
* `statCache` Cache of `fs.stat` results, to prevent statting the same
path multiple times.
* `symlinks` A record of which paths are symbolic links, which is
relevant in resolving `**` patterns.
* `realpathCache` An optional object which is passed to `fs.realpath`
to minimize unnecessary syscalls. It is stored on the instantiated
Glob object, and may be re-used.
### Events
* `end` When the matching is finished, this is emitted with all the
matches found. If the `nonull` option is set, and no match was found,
then the `matches` list contains the original pattern. The matches
are sorted, unless the `nosort` flag is set.
* `match` Every time a match is found, this is emitted with the specific
thing that matched. It is not deduplicated or resolved to a realpath.
* `error` Emitted when an unexpected error is encountered, or whenever
any fs error occurs if `options.strict` is set.
* `abort` When `abort()` is called, this event is raised.
### Methods
* `pause` Temporarily stop the search
* `resume` Resume the search
* `abort` Stop the search forever
### Options
All the options that can be passed to Minimatch can also be passed to
Glob to change pattern matching behavior. Also, some have been added,
or have glob-specific ramifications.
All options are false by default, unless otherwise noted.
All options are added to the Glob object, as well.
If you are running many `glob` operations, you can pass a Glob object
as the `options` argument to a subsequent operation to shortcut some
`stat` and `readdir` calls. At the very least, you may pass in shared
`symlinks`, `statCache`, `realpathCache`, and `cache` options, so that
parallel glob operations will be sped up by sharing information about
the filesystem.
* `cwd` The current working directory in which to search. Defaults
to `process.cwd()`.
* `root` The place where patterns starting with `/` will be mounted
onto. Defaults to `path.resolve(options.cwd, "/")` (`/` on Unix
systems, and `C:\` or some such on Windows.)
* `dot` Include `.dot` files in normal matches and `globstar` matches.
Note that an explicit dot in a portion of the pattern will always
match dot files.
* `nomount` By default, a pattern starting with a forward-slash will be
"mounted" onto the root setting, so that a valid filesystem path is
returned. Set this flag to disable that behavior.
* `mark` Add a `/` character to directory matches. Note that this
requires additional stat calls.
* `nosort` Don't sort the results.
* `stat` Set to true to stat *all* results. This reduces performance
somewhat, and is completely unnecessary, unless `readdir` is presumed
to be an untrustworthy indicator of file existence.
* `silent` When an unusual error is encountered when attempting to
read a directory, a warning will be printed to stderr. Set the
`silent` option to true to suppress these warnings.
* `strict` When an unusual error is encountered when attempting to
read a directory, the process will just continue on in search of
other matches. Set the `strict` option to raise an error in these
cases.
* `cache` See `cache` property above. Pass in a previously generated
cache object to save some fs calls.
* `statCache` A cache of results of filesystem information, to prevent
unnecessary stat calls. While it should not normally be necessary
to set this, you may pass the statCache from one glob() call to the
options object of another, if you know that the filesystem will not
change between calls. (See "Race Conditions" below.)
* `symlinks` A cache of known symbolic links. You may pass in a
previously generated `symlinks` object to save `lstat` calls when
resolving `**` matches.
* `sync` DEPRECATED: use `glob.sync(pattern, opts)` instead.
* `nounique` In some cases, brace-expanded patterns can result in the
same file showing up multiple times in the result set. By default,
this implementation prevents duplicates in the result set. Set this
flag to disable that behavior.
* `nonull` Set to never return an empty set, instead returning a set
containing the pattern itself. This is the default in glob(3).
* `debug` Set to enable debug logging in minimatch and glob.
* `nobrace` Do not expand `{a,b}` and `{1..3}` brace sets.
* `noglobstar` Do not match `**` against multiple filenames. (Ie,
treat it as a normal `*` instead.)
* `noext` Do not match `+(a|b)` "extglob" patterns.
* `nocase` Perform a case-insensitive match. Note: on
case-insensitive filesystems, non-magic patterns will match by
default, since `stat` and `readdir` will not raise errors.
* `matchBase` Perform a basename-only match if the pattern does not
contain any slash characters. That is, `*.js` would be treated as
equivalent to `**/*.js`, matching all js files in all directories.
* `nodir` Do not match directories, only files. (Note: to match
*only* directories, simply put a `/` at the end of the pattern.)
* `ignore` Add a pattern or an array of glob patterns to exclude matches.
Note: `ignore` patterns are *always* in `dot:true` mode, regardless
of any other settings.
* `follow` Follow symlinked directories when expanding `**` patterns.
Note that this can result in a lot of duplicate references in the
presence of cyclic links.
* `realpath` Set to true to call `fs.realpath` on all of the results.
In the case of a symlink that cannot be resolved, the full absolute
path to the matched entry is returned (though it will usually be a
broken symlink)
* `absolute` Set to true to always receive absolute paths for matched
files. Unlike `realpath`, this also affects the values returned in
the `match` event.
* `fs` File-system object with Node's `fs` API. By default, the built-in
`fs` module will be used. Set to a volume provided by a library like
`memfs` to avoid using the "real" file-system.
## Comparisons to other fnmatch/glob implementations
While strict compliance with the existing standards is a worthwhile
goal, some discrepancies exist between node-glob and other
implementations, and are intentional.
The double-star character `**` is supported by default, unless the
`noglobstar` flag is set. This is supported in the manner of bsdglob
and bash 4.3, where `**` only has special significance if it is the only
thing in a path part. That is, `a/**/b` will match `a/x/y/b`, but
`a/**b` will not.
Note that symlinked directories are not crawled as part of a `**`,
though their contents may match against subsequent portions of the
pattern. This prevents infinite loops and duplicates and the like.
If an escaped pattern has no matches, and the `nonull` flag is set,
then glob returns the pattern as-provided, rather than
interpreting the character escapes. For example,
`glob.match([], "\\*a\\?")` will return `"\\*a\\?"` rather than
`"*a?"`. This is akin to setting the `nullglob` option in bash, except
that it does not resolve escaped pattern characters.
If brace expansion is not disabled, then it is performed before any
other interpretation of the glob pattern. Thus, a pattern like
`+(a|{b),c)}`, which would not be valid in bash or zsh, is expanded
**first** into the set of `+(a|b)` and `+(a|c)`, and those patterns are
checked for validity. Since those two are valid, matching proceeds.
### Comments and Negation
Previously, this module let you mark a pattern as a "comment" if it
started with a `#` character, or a "negated" pattern if it started
with a `!` character.
These options were deprecated in version 5, and removed in version 6.
To specify things that should not match, use the `ignore` option.
## Windows
**Please only use forward-slashes in glob expressions.**
Though windows uses either `/` or `\` as its path separator, only `/`
characters are used by this glob implementation. You must use
forward-slashes **only** in glob expressions. Back-slashes will always
be interpreted as escape characters, not path separators.
Results from absolute patterns such as `/foo/*` are mounted onto the
root setting using `path.join`. On windows, this will by default result
in `/foo/*` matching `C:\foo\bar.txt`.
## Race Conditions
Glob searching, by its very nature, is susceptible to race conditions,
since it relies on directory walking and such.
As a result, it is possible that a file that exists when glob looks for
it may have been deleted or modified by the time it returns the result.
As part of its internal implementation, this program caches all stat
and readdir calls that it makes, in order to cut down on system
overhead. However, this also makes it even more susceptible to races,
especially if the cache or statCache objects are reused between glob
calls.
Users are thus advised not to use a glob result as a guarantee of
filesystem state in the face of rapid changes. For the vast majority
of operations, this is never a problem.
## Glob Logo
Glob's logo was created by [Tanya Brassie](http://tanyabrassie.com/). Logo files can be found [here](https://github.com/isaacs/node-glob/tree/master/logo).
The logo is licensed under a [Creative Commons Attribution-ShareAlike 4.0 International License](https://creativecommons.org/licenses/by-sa/4.0/).
## Contributing
Any change to behavior (including bugfixes) must come with a test.
Patches that fail tests or reduce performance will be rejected.
```
# to run tests
npm test
# to re-generate test fixtures
npm run test-regen
# to benchmark against bash/zsh
npm run bench
# to profile javascript
npm run prof
```
![](oh-my-glob.gif)

View File

@@ -0,0 +1,238 @@
exports.setopts = setopts
exports.ownProp = ownProp
exports.makeAbs = makeAbs
exports.finish = finish
exports.mark = mark
exports.isIgnored = isIgnored
exports.childrenIgnored = childrenIgnored
function ownProp (obj, field) {
return Object.prototype.hasOwnProperty.call(obj, field)
}
var fs = require("fs")
var path = require("path")
var minimatch = require("minimatch")
var isAbsolute = require("path-is-absolute")
var Minimatch = minimatch.Minimatch
function alphasort (a, b) {
return a.localeCompare(b, 'en')
}
function setupIgnores (self, options) {
self.ignore = options.ignore || []
if (!Array.isArray(self.ignore))
self.ignore = [self.ignore]
if (self.ignore.length) {
self.ignore = self.ignore.map(ignoreMap)
}
}
// ignore patterns are always in dot:true mode.
function ignoreMap (pattern) {
var gmatcher = null
if (pattern.slice(-3) === '/**') {
var gpattern = pattern.replace(/(\/\*\*)+$/, '')
gmatcher = new Minimatch(gpattern, { dot: true })
}
return {
matcher: new Minimatch(pattern, { dot: true }),
gmatcher: gmatcher
}
}
function setopts (self, pattern, options) {
if (!options)
options = {}
// base-matching: just use globstar for that.
if (options.matchBase && -1 === pattern.indexOf("/")) {
if (options.noglobstar) {
throw new Error("base matching requires globstar")
}
pattern = "**/" + pattern
}
self.silent = !!options.silent
self.pattern = pattern
self.strict = options.strict !== false
self.realpath = !!options.realpath
self.realpathCache = options.realpathCache || Object.create(null)
self.follow = !!options.follow
self.dot = !!options.dot
self.mark = !!options.mark
self.nodir = !!options.nodir
if (self.nodir)
self.mark = true
self.sync = !!options.sync
self.nounique = !!options.nounique
self.nonull = !!options.nonull
self.nosort = !!options.nosort
self.nocase = !!options.nocase
self.stat = !!options.stat
self.noprocess = !!options.noprocess
self.absolute = !!options.absolute
self.fs = options.fs || fs
self.maxLength = options.maxLength || Infinity
self.cache = options.cache || Object.create(null)
self.statCache = options.statCache || Object.create(null)
self.symlinks = options.symlinks || Object.create(null)
setupIgnores(self, options)
self.changedCwd = false
var cwd = process.cwd()
if (!ownProp(options, "cwd"))
self.cwd = cwd
else {
self.cwd = path.resolve(options.cwd)
self.changedCwd = self.cwd !== cwd
}
self.root = options.root || path.resolve(self.cwd, "/")
self.root = path.resolve(self.root)
if (process.platform === "win32")
self.root = self.root.replace(/\\/g, "/")
// TODO: is an absolute `cwd` supposed to be resolved against `root`?
// e.g. { cwd: '/test', root: __dirname } === path.join(__dirname, '/test')
self.cwdAbs = isAbsolute(self.cwd) ? self.cwd : makeAbs(self, self.cwd)
if (process.platform === "win32")
self.cwdAbs = self.cwdAbs.replace(/\\/g, "/")
self.nomount = !!options.nomount
// disable comments and negation in Minimatch.
// Note that they are not supported in Glob itself anyway.
options.nonegate = true
options.nocomment = true
// always treat \ in patterns as escapes, not path separators
options.allowWindowsEscape = false
self.minimatch = new Minimatch(pattern, options)
self.options = self.minimatch.options
}
function finish (self) {
var nou = self.nounique
var all = nou ? [] : Object.create(null)
for (var i = 0, l = self.matches.length; i < l; i ++) {
var matches = self.matches[i]
if (!matches || Object.keys(matches).length === 0) {
if (self.nonull) {
// do like the shell, and spit out the literal glob
var literal = self.minimatch.globSet[i]
if (nou)
all.push(literal)
else
all[literal] = true
}
} else {
// had matches
var m = Object.keys(matches)
if (nou)
all.push.apply(all, m)
else
m.forEach(function (m) {
all[m] = true
})
}
}
if (!nou)
all = Object.keys(all)
if (!self.nosort)
all = all.sort(alphasort)
// at *some* point we statted all of these
if (self.mark) {
for (var i = 0; i < all.length; i++) {
all[i] = self._mark(all[i])
}
if (self.nodir) {
all = all.filter(function (e) {
var notDir = !(/\/$/.test(e))
var c = self.cache[e] || self.cache[makeAbs(self, e)]
if (notDir && c)
notDir = c !== 'DIR' && !Array.isArray(c)
return notDir
})
}
}
if (self.ignore.length)
all = all.filter(function(m) {
return !isIgnored(self, m)
})
self.found = all
}
function mark (self, p) {
var abs = makeAbs(self, p)
var c = self.cache[abs]
var m = p
if (c) {
var isDir = c === 'DIR' || Array.isArray(c)
var slash = p.slice(-1) === '/'
if (isDir && !slash)
m += '/'
else if (!isDir && slash)
m = m.slice(0, -1)
if (m !== p) {
var mabs = makeAbs(self, m)
self.statCache[mabs] = self.statCache[abs]
self.cache[mabs] = self.cache[abs]
}
}
return m
}
// lotta situps...
function makeAbs (self, f) {
var abs = f
if (f.charAt(0) === '/') {
abs = path.join(self.root, f)
} else if (isAbsolute(f) || f === '') {
abs = f
} else if (self.changedCwd) {
abs = path.resolve(self.cwd, f)
} else {
abs = path.resolve(f)
}
if (process.platform === 'win32')
abs = abs.replace(/\\/g, '/')
return abs
}
// Return true, if pattern ends with globstar '**', for the accompanying parent directory.
// Ex:- If node_modules/** is the pattern, add 'node_modules' to ignore list along with it's contents
function isIgnored (self, path) {
if (!self.ignore.length)
return false
return self.ignore.some(function(item) {
return item.matcher.match(path) || !!(item.gmatcher && item.gmatcher.match(path))
})
}
function childrenIgnored (self, path) {
if (!self.ignore.length)
return false
return self.ignore.some(function(item) {
return !!(item.gmatcher && item.gmatcher.match(path))
})
}

View File

@@ -0,0 +1,790 @@
// Approach:
//
// 1. Get the minimatch set
// 2. For each pattern in the set, PROCESS(pattern, false)
// 3. Store matches per-set, then uniq them
//
// PROCESS(pattern, inGlobStar)
// Get the first [n] items from pattern that are all strings
// Join these together. This is PREFIX.
// If there is no more remaining, then stat(PREFIX) and
// add to matches if it succeeds. END.
//
// If inGlobStar and PREFIX is symlink and points to dir
// set ENTRIES = []
// else readdir(PREFIX) as ENTRIES
// If fail, END
//
// with ENTRIES
// If pattern[n] is GLOBSTAR
// // handle the case where the globstar match is empty
// // by pruning it out, and testing the resulting pattern
// PROCESS(pattern[0..n] + pattern[n+1 .. $], false)
// // handle other cases.
// for ENTRY in ENTRIES (not dotfiles)
// // attach globstar + tail onto the entry
// // Mark that this entry is a globstar match
// PROCESS(pattern[0..n] + ENTRY + pattern[n .. $], true)
//
// else // not globstar
// for ENTRY in ENTRIES (not dotfiles, unless pattern[n] is dot)
// Test ENTRY against pattern[n]
// If fails, continue
// If passes, PROCESS(pattern[0..n] + item + pattern[n+1 .. $])
//
// Caveat:
// Cache all stats and readdirs results to minimize syscall. Since all
// we ever care about is existence and directory-ness, we can just keep
// `true` for files, and [children,...] for directories, or `false` for
// things that don't exist.
module.exports = glob
var rp = require('fs.realpath')
var minimatch = require('minimatch')
var Minimatch = minimatch.Minimatch
var inherits = require('inherits')
var EE = require('events').EventEmitter
var path = require('path')
var assert = require('assert')
var isAbsolute = require('path-is-absolute')
var globSync = require('./sync.js')
var common = require('./common.js')
var setopts = common.setopts
var ownProp = common.ownProp
var inflight = require('inflight')
var util = require('util')
var childrenIgnored = common.childrenIgnored
var isIgnored = common.isIgnored
var once = require('once')
function glob (pattern, options, cb) {
if (typeof options === 'function') cb = options, options = {}
if (!options) options = {}
if (options.sync) {
if (cb)
throw new TypeError('callback provided to sync glob')
return globSync(pattern, options)
}
return new Glob(pattern, options, cb)
}
glob.sync = globSync
var GlobSync = glob.GlobSync = globSync.GlobSync
// old api surface
glob.glob = glob
function extend (origin, add) {
if (add === null || typeof add !== 'object') {
return origin
}
var keys = Object.keys(add)
var i = keys.length
while (i--) {
origin[keys[i]] = add[keys[i]]
}
return origin
}
glob.hasMagic = function (pattern, options_) {
var options = extend({}, options_)
options.noprocess = true
var g = new Glob(pattern, options)
var set = g.minimatch.set
if (!pattern)
return false
if (set.length > 1)
return true
for (var j = 0; j < set[0].length; j++) {
if (typeof set[0][j] !== 'string')
return true
}
return false
}
glob.Glob = Glob
inherits(Glob, EE)
function Glob (pattern, options, cb) {
if (typeof options === 'function') {
cb = options
options = null
}
if (options && options.sync) {
if (cb)
throw new TypeError('callback provided to sync glob')
return new GlobSync(pattern, options)
}
if (!(this instanceof Glob))
return new Glob(pattern, options, cb)
setopts(this, pattern, options)
this._didRealPath = false
// process each pattern in the minimatch set
var n = this.minimatch.set.length
// The matches are stored as {<filename>: true,...} so that
// duplicates are automagically pruned.
// Later, we do an Object.keys() on these.
// Keep them as a list so we can fill in when nonull is set.
this.matches = new Array(n)
if (typeof cb === 'function') {
cb = once(cb)
this.on('error', cb)
this.on('end', function (matches) {
cb(null, matches)
})
}
var self = this
this._processing = 0
this._emitQueue = []
this._processQueue = []
this.paused = false
if (this.noprocess)
return this
if (n === 0)
return done()
var sync = true
for (var i = 0; i < n; i ++) {
this._process(this.minimatch.set[i], i, false, done)
}
sync = false
function done () {
--self._processing
if (self._processing <= 0) {
if (sync) {
process.nextTick(function () {
self._finish()
})
} else {
self._finish()
}
}
}
}
Glob.prototype._finish = function () {
assert(this instanceof Glob)
if (this.aborted)
return
if (this.realpath && !this._didRealpath)
return this._realpath()
common.finish(this)
this.emit('end', this.found)
}
Glob.prototype._realpath = function () {
if (this._didRealpath)
return
this._didRealpath = true
var n = this.matches.length
if (n === 0)
return this._finish()
var self = this
for (var i = 0; i < this.matches.length; i++)
this._realpathSet(i, next)
function next () {
if (--n === 0)
self._finish()
}
}
Glob.prototype._realpathSet = function (index, cb) {
var matchset = this.matches[index]
if (!matchset)
return cb()
var found = Object.keys(matchset)
var self = this
var n = found.length
if (n === 0)
return cb()
var set = this.matches[index] = Object.create(null)
found.forEach(function (p, i) {
// If there's a problem with the stat, then it means that
// one or more of the links in the realpath couldn't be
// resolved. just return the abs value in that case.
p = self._makeAbs(p)
rp.realpath(p, self.realpathCache, function (er, real) {
if (!er)
set[real] = true
else if (er.syscall === 'stat')
set[p] = true
else
self.emit('error', er) // srsly wtf right here
if (--n === 0) {
self.matches[index] = set
cb()
}
})
})
}
Glob.prototype._mark = function (p) {
return common.mark(this, p)
}
Glob.prototype._makeAbs = function (f) {
return common.makeAbs(this, f)
}
Glob.prototype.abort = function () {
this.aborted = true
this.emit('abort')
}
Glob.prototype.pause = function () {
if (!this.paused) {
this.paused = true
this.emit('pause')
}
}
Glob.prototype.resume = function () {
if (this.paused) {
this.emit('resume')
this.paused = false
if (this._emitQueue.length) {
var eq = this._emitQueue.slice(0)
this._emitQueue.length = 0
for (var i = 0; i < eq.length; i ++) {
var e = eq[i]
this._emitMatch(e[0], e[1])
}
}
if (this._processQueue.length) {
var pq = this._processQueue.slice(0)
this._processQueue.length = 0
for (var i = 0; i < pq.length; i ++) {
var p = pq[i]
this._processing--
this._process(p[0], p[1], p[2], p[3])
}
}
}
}
Glob.prototype._process = function (pattern, index, inGlobStar, cb) {
assert(this instanceof Glob)
assert(typeof cb === 'function')
if (this.aborted)
return
this._processing++
if (this.paused) {
this._processQueue.push([pattern, index, inGlobStar, cb])
return
}
//console.error('PROCESS %d', this._processing, pattern)
// Get the first [n] parts of pattern that are all strings.
var n = 0
while (typeof pattern[n] === 'string') {
n ++
}
// now n is the index of the first one that is *not* a string.
// see if there's anything else
var prefix
switch (n) {
// if not, then this is rather simple
case pattern.length:
this._processSimple(pattern.join('/'), index, cb)
return
case 0:
// pattern *starts* with some non-trivial item.
// going to readdir(cwd), but not include the prefix in matches.
prefix = null
break
default:
// pattern has some string bits in the front.
// whatever it starts with, whether that's 'absolute' like /foo/bar,
// or 'relative' like '../baz'
prefix = pattern.slice(0, n).join('/')
break
}
var remain = pattern.slice(n)
// get the list of entries.
var read
if (prefix === null)
read = '.'
else if (isAbsolute(prefix) ||
isAbsolute(pattern.map(function (p) {
return typeof p === 'string' ? p : '[*]'
}).join('/'))) {
if (!prefix || !isAbsolute(prefix))
prefix = '/' + prefix
read = prefix
} else
read = prefix
var abs = this._makeAbs(read)
//if ignored, skip _processing
if (childrenIgnored(this, read))
return cb()
var isGlobStar = remain[0] === minimatch.GLOBSTAR
if (isGlobStar)
this._processGlobStar(prefix, read, abs, remain, index, inGlobStar, cb)
else
this._processReaddir(prefix, read, abs, remain, index, inGlobStar, cb)
}
Glob.prototype._processReaddir = function (prefix, read, abs, remain, index, inGlobStar, cb) {
var self = this
this._readdir(abs, inGlobStar, function (er, entries) {
return self._processReaddir2(prefix, read, abs, remain, index, inGlobStar, entries, cb)
})
}
Glob.prototype._processReaddir2 = function (prefix, read, abs, remain, index, inGlobStar, entries, cb) {
// if the abs isn't a dir, then nothing can match!
if (!entries)
return cb()
// It will only match dot entries if it starts with a dot, or if
// dot is set. Stuff like @(.foo|.bar) isn't allowed.
var pn = remain[0]
var negate = !!this.minimatch.negate
var rawGlob = pn._glob
var dotOk = this.dot || rawGlob.charAt(0) === '.'
var matchedEntries = []
for (var i = 0; i < entries.length; i++) {
var e = entries[i]
if (e.charAt(0) !== '.' || dotOk) {
var m
if (negate && !prefix) {
m = !e.match(pn)
} else {
m = e.match(pn)
}
if (m)
matchedEntries.push(e)
}
}
//console.error('prd2', prefix, entries, remain[0]._glob, matchedEntries)
var len = matchedEntries.length
// If there are no matched entries, then nothing matches.
if (len === 0)
return cb()
// if this is the last remaining pattern bit, then no need for
// an additional stat *unless* the user has specified mark or
// stat explicitly. We know they exist, since readdir returned
// them.
if (remain.length === 1 && !this.mark && !this.stat) {
if (!this.matches[index])
this.matches[index] = Object.create(null)
for (var i = 0; i < len; i ++) {
var e = matchedEntries[i]
if (prefix) {
if (prefix !== '/')
e = prefix + '/' + e
else
e = prefix + e
}
if (e.charAt(0) === '/' && !this.nomount) {
e = path.join(this.root, e)
}
this._emitMatch(index, e)
}
// This was the last one, and no stats were needed
return cb()
}
// now test all matched entries as stand-ins for that part
// of the pattern.
remain.shift()
for (var i = 0; i < len; i ++) {
var e = matchedEntries[i]
var newPattern
if (prefix) {
if (prefix !== '/')
e = prefix + '/' + e
else
e = prefix + e
}
this._process([e].concat(remain), index, inGlobStar, cb)
}
cb()
}
Glob.prototype._emitMatch = function (index, e) {
if (this.aborted)
return
if (isIgnored(this, e))
return
if (this.paused) {
this._emitQueue.push([index, e])
return
}
var abs = isAbsolute(e) ? e : this._makeAbs(e)
if (this.mark)
e = this._mark(e)
if (this.absolute)
e = abs
if (this.matches[index][e])
return
if (this.nodir) {
var c = this.cache[abs]
if (c === 'DIR' || Array.isArray(c))
return
}
this.matches[index][e] = true
var st = this.statCache[abs]
if (st)
this.emit('stat', e, st)
this.emit('match', e)
}
Glob.prototype._readdirInGlobStar = function (abs, cb) {
if (this.aborted)
return
// follow all symlinked directories forever
// just proceed as if this is a non-globstar situation
if (this.follow)
return this._readdir(abs, false, cb)
var lstatkey = 'lstat\0' + abs
var self = this
var lstatcb = inflight(lstatkey, lstatcb_)
if (lstatcb)
self.fs.lstat(abs, lstatcb)
function lstatcb_ (er, lstat) {
if (er && er.code === 'ENOENT')
return cb()
var isSym = lstat && lstat.isSymbolicLink()
self.symlinks[abs] = isSym
// If it's not a symlink or a dir, then it's definitely a regular file.
// don't bother doing a readdir in that case.
if (!isSym && lstat && !lstat.isDirectory()) {
self.cache[abs] = 'FILE'
cb()
} else
self._readdir(abs, false, cb)
}
}
Glob.prototype._readdir = function (abs, inGlobStar, cb) {
if (this.aborted)
return
cb = inflight('readdir\0'+abs+'\0'+inGlobStar, cb)
if (!cb)
return
//console.error('RD %j %j', +inGlobStar, abs)
if (inGlobStar && !ownProp(this.symlinks, abs))
return this._readdirInGlobStar(abs, cb)
if (ownProp(this.cache, abs)) {
var c = this.cache[abs]
if (!c || c === 'FILE')
return cb()
if (Array.isArray(c))
return cb(null, c)
}
var self = this
self.fs.readdir(abs, readdirCb(this, abs, cb))
}
function readdirCb (self, abs, cb) {
return function (er, entries) {
if (er)
self._readdirError(abs, er, cb)
else
self._readdirEntries(abs, entries, cb)
}
}
Glob.prototype._readdirEntries = function (abs, entries, cb) {
if (this.aborted)
return
// if we haven't asked to stat everything, then just
// assume that everything in there exists, so we can avoid
// having to stat it a second time.
if (!this.mark && !this.stat) {
for (var i = 0; i < entries.length; i ++) {
var e = entries[i]
if (abs === '/')
e = abs + e
else
e = abs + '/' + e
this.cache[e] = true
}
}
this.cache[abs] = entries
return cb(null, entries)
}
Glob.prototype._readdirError = function (f, er, cb) {
if (this.aborted)
return
// handle errors, and cache the information
switch (er.code) {
case 'ENOTSUP': // https://github.com/isaacs/node-glob/issues/205
case 'ENOTDIR': // totally normal. means it *does* exist.
var abs = this._makeAbs(f)
this.cache[abs] = 'FILE'
if (abs === this.cwdAbs) {
var error = new Error(er.code + ' invalid cwd ' + this.cwd)
error.path = this.cwd
error.code = er.code
this.emit('error', error)
this.abort()
}
break
case 'ENOENT': // not terribly unusual
case 'ELOOP':
case 'ENAMETOOLONG':
case 'UNKNOWN':
this.cache[this._makeAbs(f)] = false
break
default: // some unusual error. Treat as failure.
this.cache[this._makeAbs(f)] = false
if (this.strict) {
this.emit('error', er)
// If the error is handled, then we abort
// if not, we threw out of here
this.abort()
}
if (!this.silent)
console.error('glob error', er)
break
}
return cb()
}
Glob.prototype._processGlobStar = function (prefix, read, abs, remain, index, inGlobStar, cb) {
var self = this
this._readdir(abs, inGlobStar, function (er, entries) {
self._processGlobStar2(prefix, read, abs, remain, index, inGlobStar, entries, cb)
})
}
Glob.prototype._processGlobStar2 = function (prefix, read, abs, remain, index, inGlobStar, entries, cb) {
//console.error('pgs2', prefix, remain[0], entries)
// no entries means not a dir, so it can never have matches
// foo.txt/** doesn't match foo.txt
if (!entries)
return cb()
// test without the globstar, and with every child both below
// and replacing the globstar.
var remainWithoutGlobStar = remain.slice(1)
var gspref = prefix ? [ prefix ] : []
var noGlobStar = gspref.concat(remainWithoutGlobStar)
// the noGlobStar pattern exits the inGlobStar state
this._process(noGlobStar, index, false, cb)
var isSym = this.symlinks[abs]
var len = entries.length
// If it's a symlink, and we're in a globstar, then stop
if (isSym && inGlobStar)
return cb()
for (var i = 0; i < len; i++) {
var e = entries[i]
if (e.charAt(0) === '.' && !this.dot)
continue
// these two cases enter the inGlobStar state
var instead = gspref.concat(entries[i], remainWithoutGlobStar)
this._process(instead, index, true, cb)
var below = gspref.concat(entries[i], remain)
this._process(below, index, true, cb)
}
cb()
}
Glob.prototype._processSimple = function (prefix, index, cb) {
// XXX review this. Shouldn't it be doing the mounting etc
// before doing stat? kinda weird?
var self = this
this._stat(prefix, function (er, exists) {
self._processSimple2(prefix, index, er, exists, cb)
})
}
Glob.prototype._processSimple2 = function (prefix, index, er, exists, cb) {
//console.error('ps2', prefix, exists)
if (!this.matches[index])
this.matches[index] = Object.create(null)
// If it doesn't exist, then just mark the lack of results
if (!exists)
return cb()
if (prefix && isAbsolute(prefix) && !this.nomount) {
var trail = /[\/\\]$/.test(prefix)
if (prefix.charAt(0) === '/') {
prefix = path.join(this.root, prefix)
} else {
prefix = path.resolve(this.root, prefix)
if (trail)
prefix += '/'
}
}
if (process.platform === 'win32')
prefix = prefix.replace(/\\/g, '/')
// Mark this as a match
this._emitMatch(index, prefix)
cb()
}
// Returns either 'DIR', 'FILE', or false
Glob.prototype._stat = function (f, cb) {
var abs = this._makeAbs(f)
var needDir = f.slice(-1) === '/'
if (f.length > this.maxLength)
return cb()
if (!this.stat && ownProp(this.cache, abs)) {
var c = this.cache[abs]
if (Array.isArray(c))
c = 'DIR'
// It exists, but maybe not how we need it
if (!needDir || c === 'DIR')
return cb(null, c)
if (needDir && c === 'FILE')
return cb()
// otherwise we have to stat, because maybe c=true
// if we know it exists, but not what it is.
}
var exists
var stat = this.statCache[abs]
if (stat !== undefined) {
if (stat === false)
return cb(null, stat)
else {
var type = stat.isDirectory() ? 'DIR' : 'FILE'
if (needDir && type === 'FILE')
return cb()
else
return cb(null, type, stat)
}
}
var self = this
var statcb = inflight('stat\0' + abs, lstatcb_)
if (statcb)
self.fs.lstat(abs, statcb)
function lstatcb_ (er, lstat) {
if (lstat && lstat.isSymbolicLink()) {
// If it's a symlink, then treat it as the target, unless
// the target does not exist, then treat it as a file.
return self.fs.stat(abs, function (er, stat) {
if (er)
self._stat2(f, abs, null, lstat, cb)
else
self._stat2(f, abs, er, stat, cb)
})
} else {
self._stat2(f, abs, er, lstat, cb)
}
}
}
Glob.prototype._stat2 = function (f, abs, er, stat, cb) {
if (er && (er.code === 'ENOENT' || er.code === 'ENOTDIR')) {
this.statCache[abs] = false
return cb()
}
var needDir = f.slice(-1) === '/'
this.statCache[abs] = stat
if (abs.slice(-1) === '/' && stat && !stat.isDirectory())
return cb(null, false, stat)
var c = true
if (stat)
c = stat.isDirectory() ? 'DIR' : 'FILE'
this.cache[abs] = this.cache[abs] || c
if (needDir && c === 'FILE')
return cb()
return cb(null, c, stat)
}

View File

@@ -0,0 +1,55 @@
{
"author": "Isaac Z. Schlueter <i@izs.me> (http://blog.izs.me/)",
"name": "glob",
"description": "a little globber",
"version": "7.2.3",
"publishConfig": {
"tag": "v7-legacy"
},
"repository": {
"type": "git",
"url": "git://github.com/isaacs/node-glob.git"
},
"main": "glob.js",
"files": [
"glob.js",
"sync.js",
"common.js"
],
"engines": {
"node": "*"
},
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"devDependencies": {
"memfs": "^3.2.0",
"mkdirp": "0",
"rimraf": "^2.2.8",
"tap": "^15.0.6",
"tick": "0.0.6"
},
"tap": {
"before": "test/00-setup.js",
"after": "test/zz-cleanup.js",
"jobs": 1
},
"scripts": {
"prepublish": "npm run benchclean",
"profclean": "rm -f v8.log profile.txt",
"test": "tap",
"test-regen": "npm run profclean && TEST_REGEN=1 node test/00-setup.js",
"bench": "bash benchmark.sh",
"prof": "bash prof.sh && cat profile.txt",
"benchclean": "node benchclean.js"
},
"license": "ISC",
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
}

View File

@@ -0,0 +1,486 @@
module.exports = globSync
globSync.GlobSync = GlobSync
var rp = require('fs.realpath')
var minimatch = require('minimatch')
var Minimatch = minimatch.Minimatch
var Glob = require('./glob.js').Glob
var util = require('util')
var path = require('path')
var assert = require('assert')
var isAbsolute = require('path-is-absolute')
var common = require('./common.js')
var setopts = common.setopts
var ownProp = common.ownProp
var childrenIgnored = common.childrenIgnored
var isIgnored = common.isIgnored
function globSync (pattern, options) {
if (typeof options === 'function' || arguments.length === 3)
throw new TypeError('callback provided to sync glob\n'+
'See: https://github.com/isaacs/node-glob/issues/167')
return new GlobSync(pattern, options).found
}
function GlobSync (pattern, options) {
if (!pattern)
throw new Error('must provide pattern')
if (typeof options === 'function' || arguments.length === 3)
throw new TypeError('callback provided to sync glob\n'+
'See: https://github.com/isaacs/node-glob/issues/167')
if (!(this instanceof GlobSync))
return new GlobSync(pattern, options)
setopts(this, pattern, options)
if (this.noprocess)
return this
var n = this.minimatch.set.length
this.matches = new Array(n)
for (var i = 0; i < n; i ++) {
this._process(this.minimatch.set[i], i, false)
}
this._finish()
}
GlobSync.prototype._finish = function () {
assert.ok(this instanceof GlobSync)
if (this.realpath) {
var self = this
this.matches.forEach(function (matchset, index) {
var set = self.matches[index] = Object.create(null)
for (var p in matchset) {
try {
p = self._makeAbs(p)
var real = rp.realpathSync(p, self.realpathCache)
set[real] = true
} catch (er) {
if (er.syscall === 'stat')
set[self._makeAbs(p)] = true
else
throw er
}
}
})
}
common.finish(this)
}
GlobSync.prototype._process = function (pattern, index, inGlobStar) {
assert.ok(this instanceof GlobSync)
// Get the first [n] parts of pattern that are all strings.
var n = 0
while (typeof pattern[n] === 'string') {
n ++
}
// now n is the index of the first one that is *not* a string.
// See if there's anything else
var prefix
switch (n) {
// if not, then this is rather simple
case pattern.length:
this._processSimple(pattern.join('/'), index)
return
case 0:
// pattern *starts* with some non-trivial item.
// going to readdir(cwd), but not include the prefix in matches.
prefix = null
break
default:
// pattern has some string bits in the front.
// whatever it starts with, whether that's 'absolute' like /foo/bar,
// or 'relative' like '../baz'
prefix = pattern.slice(0, n).join('/')
break
}
var remain = pattern.slice(n)
// get the list of entries.
var read
if (prefix === null)
read = '.'
else if (isAbsolute(prefix) ||
isAbsolute(pattern.map(function (p) {
return typeof p === 'string' ? p : '[*]'
}).join('/'))) {
if (!prefix || !isAbsolute(prefix))
prefix = '/' + prefix
read = prefix
} else
read = prefix
var abs = this._makeAbs(read)
//if ignored, skip processing
if (childrenIgnored(this, read))
return
var isGlobStar = remain[0] === minimatch.GLOBSTAR
if (isGlobStar)
this._processGlobStar(prefix, read, abs, remain, index, inGlobStar)
else
this._processReaddir(prefix, read, abs, remain, index, inGlobStar)
}
GlobSync.prototype._processReaddir = function (prefix, read, abs, remain, index, inGlobStar) {
var entries = this._readdir(abs, inGlobStar)
// if the abs isn't a dir, then nothing can match!
if (!entries)
return
// It will only match dot entries if it starts with a dot, or if
// dot is set. Stuff like @(.foo|.bar) isn't allowed.
var pn = remain[0]
var negate = !!this.minimatch.negate
var rawGlob = pn._glob
var dotOk = this.dot || rawGlob.charAt(0) === '.'
var matchedEntries = []
for (var i = 0; i < entries.length; i++) {
var e = entries[i]
if (e.charAt(0) !== '.' || dotOk) {
var m
if (negate && !prefix) {
m = !e.match(pn)
} else {
m = e.match(pn)
}
if (m)
matchedEntries.push(e)
}
}
var len = matchedEntries.length
// If there are no matched entries, then nothing matches.
if (len === 0)
return
// if this is the last remaining pattern bit, then no need for
// an additional stat *unless* the user has specified mark or
// stat explicitly. We know they exist, since readdir returned
// them.
if (remain.length === 1 && !this.mark && !this.stat) {
if (!this.matches[index])
this.matches[index] = Object.create(null)
for (var i = 0; i < len; i ++) {
var e = matchedEntries[i]
if (prefix) {
if (prefix.slice(-1) !== '/')
e = prefix + '/' + e
else
e = prefix + e
}
if (e.charAt(0) === '/' && !this.nomount) {
e = path.join(this.root, e)
}
this._emitMatch(index, e)
}
// This was the last one, and no stats were needed
return
}
// now test all matched entries as stand-ins for that part
// of the pattern.
remain.shift()
for (var i = 0; i < len; i ++) {
var e = matchedEntries[i]
var newPattern
if (prefix)
newPattern = [prefix, e]
else
newPattern = [e]
this._process(newPattern.concat(remain), index, inGlobStar)
}
}
GlobSync.prototype._emitMatch = function (index, e) {
if (isIgnored(this, e))
return
var abs = this._makeAbs(e)
if (this.mark)
e = this._mark(e)
if (this.absolute) {
e = abs
}
if (this.matches[index][e])
return
if (this.nodir) {
var c = this.cache[abs]
if (c === 'DIR' || Array.isArray(c))
return
}
this.matches[index][e] = true
if (this.stat)
this._stat(e)
}
GlobSync.prototype._readdirInGlobStar = function (abs) {
// follow all symlinked directories forever
// just proceed as if this is a non-globstar situation
if (this.follow)
return this._readdir(abs, false)
var entries
var lstat
var stat
try {
lstat = this.fs.lstatSync(abs)
} catch (er) {
if (er.code === 'ENOENT') {
// lstat failed, doesn't exist
return null
}
}
var isSym = lstat && lstat.isSymbolicLink()
this.symlinks[abs] = isSym
// If it's not a symlink or a dir, then it's definitely a regular file.
// don't bother doing a readdir in that case.
if (!isSym && lstat && !lstat.isDirectory())
this.cache[abs] = 'FILE'
else
entries = this._readdir(abs, false)
return entries
}
GlobSync.prototype._readdir = function (abs, inGlobStar) {
var entries
if (inGlobStar && !ownProp(this.symlinks, abs))
return this._readdirInGlobStar(abs)
if (ownProp(this.cache, abs)) {
var c = this.cache[abs]
if (!c || c === 'FILE')
return null
if (Array.isArray(c))
return c
}
try {
return this._readdirEntries(abs, this.fs.readdirSync(abs))
} catch (er) {
this._readdirError(abs, er)
return null
}
}
GlobSync.prototype._readdirEntries = function (abs, entries) {
// if we haven't asked to stat everything, then just
// assume that everything in there exists, so we can avoid
// having to stat it a second time.
if (!this.mark && !this.stat) {
for (var i = 0; i < entries.length; i ++) {
var e = entries[i]
if (abs === '/')
e = abs + e
else
e = abs + '/' + e
this.cache[e] = true
}
}
this.cache[abs] = entries
// mark and cache dir-ness
return entries
}
GlobSync.prototype._readdirError = function (f, er) {
// handle errors, and cache the information
switch (er.code) {
case 'ENOTSUP': // https://github.com/isaacs/node-glob/issues/205
case 'ENOTDIR': // totally normal. means it *does* exist.
var abs = this._makeAbs(f)
this.cache[abs] = 'FILE'
if (abs === this.cwdAbs) {
var error = new Error(er.code + ' invalid cwd ' + this.cwd)
error.path = this.cwd
error.code = er.code
throw error
}
break
case 'ENOENT': // not terribly unusual
case 'ELOOP':
case 'ENAMETOOLONG':
case 'UNKNOWN':
this.cache[this._makeAbs(f)] = false
break
default: // some unusual error. Treat as failure.
this.cache[this._makeAbs(f)] = false
if (this.strict)
throw er
if (!this.silent)
console.error('glob error', er)
break
}
}
GlobSync.prototype._processGlobStar = function (prefix, read, abs, remain, index, inGlobStar) {
var entries = this._readdir(abs, inGlobStar)
// no entries means not a dir, so it can never have matches
// foo.txt/** doesn't match foo.txt
if (!entries)
return
// test without the globstar, and with every child both below
// and replacing the globstar.
var remainWithoutGlobStar = remain.slice(1)
var gspref = prefix ? [ prefix ] : []
var noGlobStar = gspref.concat(remainWithoutGlobStar)
// the noGlobStar pattern exits the inGlobStar state
this._process(noGlobStar, index, false)
var len = entries.length
var isSym = this.symlinks[abs]
// If it's a symlink, and we're in a globstar, then stop
if (isSym && inGlobStar)
return
for (var i = 0; i < len; i++) {
var e = entries[i]
if (e.charAt(0) === '.' && !this.dot)
continue
// these two cases enter the inGlobStar state
var instead = gspref.concat(entries[i], remainWithoutGlobStar)
this._process(instead, index, true)
var below = gspref.concat(entries[i], remain)
this._process(below, index, true)
}
}
GlobSync.prototype._processSimple = function (prefix, index) {
// XXX review this. Shouldn't it be doing the mounting etc
// before doing stat? kinda weird?
var exists = this._stat(prefix)
if (!this.matches[index])
this.matches[index] = Object.create(null)
// If it doesn't exist, then just mark the lack of results
if (!exists)
return
if (prefix && isAbsolute(prefix) && !this.nomount) {
var trail = /[\/\\]$/.test(prefix)
if (prefix.charAt(0) === '/') {
prefix = path.join(this.root, prefix)
} else {
prefix = path.resolve(this.root, prefix)
if (trail)
prefix += '/'
}
}
if (process.platform === 'win32')
prefix = prefix.replace(/\\/g, '/')
// Mark this as a match
this._emitMatch(index, prefix)
}
// Returns either 'DIR', 'FILE', or false
GlobSync.prototype._stat = function (f) {
var abs = this._makeAbs(f)
var needDir = f.slice(-1) === '/'
if (f.length > this.maxLength)
return false
if (!this.stat && ownProp(this.cache, abs)) {
var c = this.cache[abs]
if (Array.isArray(c))
c = 'DIR'
// It exists, but maybe not how we need it
if (!needDir || c === 'DIR')
return c
if (needDir && c === 'FILE')
return false
// otherwise we have to stat, because maybe c=true
// if we know it exists, but not what it is.
}
var exists
var stat = this.statCache[abs]
if (!stat) {
var lstat
try {
lstat = this.fs.lstatSync(abs)
} catch (er) {
if (er && (er.code === 'ENOENT' || er.code === 'ENOTDIR')) {
this.statCache[abs] = false
return false
}
}
if (lstat && lstat.isSymbolicLink()) {
try {
stat = this.fs.statSync(abs)
} catch (er) {
stat = lstat
}
} else {
stat = lstat
}
}
this.statCache[abs] = stat
var c = true
if (stat)
c = stat.isDirectory() ? 'DIR' : 'FILE'
this.cache[abs] = this.cache[abs] || c
if (needDir && c === 'FILE')
return false
return c
}
GlobSync.prototype._mark = function (p) {
return common.mark(this, p)
}
GlobSync.prototype._makeAbs = function (f) {
return common.makeAbs(this, f)
}

View File

@@ -0,0 +1,15 @@
The ISC License
Copyright (c) Isaac Z. Schlueter and Contributors
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

View File

@@ -0,0 +1,230 @@
# minimatch
A minimal matching utility.
[![Build Status](https://travis-ci.org/isaacs/minimatch.svg?branch=master)](http://travis-ci.org/isaacs/minimatch)
This is the matching library used internally by npm.
It works by converting glob expressions into JavaScript `RegExp`
objects.
## Usage
```javascript
var minimatch = require("minimatch")
minimatch("bar.foo", "*.foo") // true!
minimatch("bar.foo", "*.bar") // false!
minimatch("bar.foo", "*.+(bar|foo)", { debug: true }) // true, and noisy!
```
## Features
Supports these glob features:
* Brace Expansion
* Extended glob matching
* "Globstar" `**` matching
See:
* `man sh`
* `man bash`
* `man 3 fnmatch`
* `man 5 gitignore`
## Minimatch Class
Create a minimatch object by instantiating the `minimatch.Minimatch` class.
```javascript
var Minimatch = require("minimatch").Minimatch
var mm = new Minimatch(pattern, options)
```
### Properties
* `pattern` The original pattern the minimatch object represents.
* `options` The options supplied to the constructor.
* `set` A 2-dimensional array of regexp or string expressions.
Each row in the
array corresponds to a brace-expanded pattern. Each item in the row
corresponds to a single path-part. For example, the pattern
`{a,b/c}/d` would expand to a set of patterns like:
[ [ a, d ]
, [ b, c, d ] ]
If a portion of the pattern doesn't have any "magic" in it
(that is, it's something like `"foo"` rather than `fo*o?`), then it
will be left as a string rather than converted to a regular
expression.
* `regexp` Created by the `makeRe` method. A single regular expression
expressing the entire pattern. This is useful in cases where you wish
to use the pattern somewhat like `fnmatch(3)` with `FNM_PATH` enabled.
* `negate` True if the pattern is negated.
* `comment` True if the pattern is a comment.
* `empty` True if the pattern is `""`.
### Methods
* `makeRe` Generate the `regexp` member if necessary, and return it.
Will return `false` if the pattern is invalid.
* `match(fname)` Return true if the filename matches the pattern, or
false otherwise.
* `matchOne(fileArray, patternArray, partial)` Take a `/`-split
filename, and match it against a single row in the `regExpSet`. This
method is mainly for internal use, but is exposed so that it can be
used by a glob-walker that needs to avoid excessive filesystem calls.
All other methods are internal, and will be called as necessary.
### minimatch(path, pattern, options)
Main export. Tests a path against the pattern using the options.
```javascript
var isJS = minimatch(file, "*.js", { matchBase: true })
```
### minimatch.filter(pattern, options)
Returns a function that tests its
supplied argument, suitable for use with `Array.filter`. Example:
```javascript
var javascripts = fileList.filter(minimatch.filter("*.js", {matchBase: true}))
```
### minimatch.match(list, pattern, options)
Match against the list of
files, in the style of fnmatch or glob. If nothing is matched, and
options.nonull is set, then return a list containing the pattern itself.
```javascript
var javascripts = minimatch.match(fileList, "*.js", {matchBase: true}))
```
### minimatch.makeRe(pattern, options)
Make a regular expression object from the pattern.
## Options
All options are `false` by default.
### debug
Dump a ton of stuff to stderr.
### nobrace
Do not expand `{a,b}` and `{1..3}` brace sets.
### noglobstar
Disable `**` matching against multiple folder names.
### dot
Allow patterns to match filenames starting with a period, even if
the pattern does not explicitly have a period in that spot.
Note that by default, `a/**/b` will **not** match `a/.d/b`, unless `dot`
is set.
### noext
Disable "extglob" style patterns like `+(a|b)`.
### nocase
Perform a case-insensitive match.
### nonull
When a match is not found by `minimatch.match`, return a list containing
the pattern itself if this option is set. When not set, an empty list
is returned if there are no matches.
### matchBase
If set, then patterns without slashes will be matched
against the basename of the path if it contains slashes. For example,
`a?b` would match the path `/xyz/123/acb`, but not `/xyz/acb/123`.
### nocomment
Suppress the behavior of treating `#` at the start of a pattern as a
comment.
### nonegate
Suppress the behavior of treating a leading `!` character as negation.
### flipNegate
Returns from negate expressions the same as if they were not negated.
(Ie, true on a hit, false on a miss.)
### partial
Compare a partial path to a pattern. As long as the parts of the path that
are present are not contradicted by the pattern, it will be treated as a
match. This is useful in applications where you're walking through a
folder structure, and don't yet have the full path, but want to ensure that
you do not walk down paths that can never be a match.
For example,
```js
minimatch('/a/b', '/a/*/c/d', { partial: true }) // true, might be /a/b/c/d
minimatch('/a/b', '/**/d', { partial: true }) // true, might be /a/b/.../d
minimatch('/x/y/z', '/a/**/z', { partial: true }) // false, because x !== a
```
### allowWindowsEscape
Windows path separator `\` is by default converted to `/`, which
prohibits the usage of `\` as a escape character. This flag skips that
behavior and allows using the escape character.
## Comparisons to other fnmatch/glob implementations
While strict compliance with the existing standards is a worthwhile
goal, some discrepancies exist between minimatch and other
implementations, and are intentional.
If the pattern starts with a `!` character, then it is negated. Set the
`nonegate` flag to suppress this behavior, and treat leading `!`
characters normally. This is perhaps relevant if you wish to start the
pattern with a negative extglob pattern like `!(a|B)`. Multiple `!`
characters at the start of a pattern will negate the pattern multiple
times.
If a pattern starts with `#`, then it is treated as a comment, and
will not match anything. Use `\#` to match a literal `#` at the
start of a line, or set the `nocomment` flag to suppress this behavior.
The double-star character `**` is supported by default, unless the
`noglobstar` flag is set. This is supported in the manner of bsdglob
and bash 4.1, where `**` only has special significance if it is the only
thing in a path part. That is, `a/**/b` will match `a/x/y/b`, but
`a/**b` will not.
If an escaped pattern has no matches, and the `nonull` flag is set,
then minimatch.match returns the pattern as-provided, rather than
interpreting the character escapes. For example,
`minimatch.match([], "\\*a\\?")` will return `"\\*a\\?"` rather than
`"*a?"`. This is akin to setting the `nullglob` option in bash, except
that it does not resolve escaped pattern characters.
If brace expansion is not disabled, then it is performed before any
other interpretation of the glob pattern. Thus, a pattern like
`+(a|{b),c)}`, which would not be valid in bash or zsh, is expanded
**first** into the set of `+(a|b)` and `+(a|c)`, and those patterns are
checked for validity. Since those two are valid, matching proceeds.

View File

@@ -0,0 +1,947 @@
module.exports = minimatch
minimatch.Minimatch = Minimatch
var path = (function () { try { return require('path') } catch (e) {}}()) || {
sep: '/'
}
minimatch.sep = path.sep
var GLOBSTAR = minimatch.GLOBSTAR = Minimatch.GLOBSTAR = {}
var expand = require('brace-expansion')
var plTypes = {
'!': { open: '(?:(?!(?:', close: '))[^/]*?)'},
'?': { open: '(?:', close: ')?' },
'+': { open: '(?:', close: ')+' },
'*': { open: '(?:', close: ')*' },
'@': { open: '(?:', close: ')' }
}
// any single thing other than /
// don't need to escape / when using new RegExp()
var qmark = '[^/]'
// * => any number of characters
var star = qmark + '*?'
// ** when dots are allowed. Anything goes, except .. and .
// not (^ or / followed by one or two dots followed by $ or /),
// followed by anything, any number of times.
var twoStarDot = '(?:(?!(?:\\\/|^)(?:\\.{1,2})($|\\\/)).)*?'
// not a ^ or / followed by a dot,
// followed by anything, any number of times.
var twoStarNoDot = '(?:(?!(?:\\\/|^)\\.).)*?'
// characters that need to be escaped in RegExp.
var reSpecials = charSet('().*{}+?[]^$\\!')
// "abc" -> { a:true, b:true, c:true }
function charSet (s) {
return s.split('').reduce(function (set, c) {
set[c] = true
return set
}, {})
}
// normalizes slashes.
var slashSplit = /\/+/
minimatch.filter = filter
function filter (pattern, options) {
options = options || {}
return function (p, i, list) {
return minimatch(p, pattern, options)
}
}
function ext (a, b) {
b = b || {}
var t = {}
Object.keys(a).forEach(function (k) {
t[k] = a[k]
})
Object.keys(b).forEach(function (k) {
t[k] = b[k]
})
return t
}
minimatch.defaults = function (def) {
if (!def || typeof def !== 'object' || !Object.keys(def).length) {
return minimatch
}
var orig = minimatch
var m = function minimatch (p, pattern, options) {
return orig(p, pattern, ext(def, options))
}
m.Minimatch = function Minimatch (pattern, options) {
return new orig.Minimatch(pattern, ext(def, options))
}
m.Minimatch.defaults = function defaults (options) {
return orig.defaults(ext(def, options)).Minimatch
}
m.filter = function filter (pattern, options) {
return orig.filter(pattern, ext(def, options))
}
m.defaults = function defaults (options) {
return orig.defaults(ext(def, options))
}
m.makeRe = function makeRe (pattern, options) {
return orig.makeRe(pattern, ext(def, options))
}
m.braceExpand = function braceExpand (pattern, options) {
return orig.braceExpand(pattern, ext(def, options))
}
m.match = function (list, pattern, options) {
return orig.match(list, pattern, ext(def, options))
}
return m
}
Minimatch.defaults = function (def) {
return minimatch.defaults(def).Minimatch
}
function minimatch (p, pattern, options) {
assertValidPattern(pattern)
if (!options) options = {}
// shortcut: comments match nothing.
if (!options.nocomment && pattern.charAt(0) === '#') {
return false
}
return new Minimatch(pattern, options).match(p)
}
function Minimatch (pattern, options) {
if (!(this instanceof Minimatch)) {
return new Minimatch(pattern, options)
}
assertValidPattern(pattern)
if (!options) options = {}
pattern = pattern.trim()
// windows support: need to use /, not \
if (!options.allowWindowsEscape && path.sep !== '/') {
pattern = pattern.split(path.sep).join('/')
}
this.options = options
this.set = []
this.pattern = pattern
this.regexp = null
this.negate = false
this.comment = false
this.empty = false
this.partial = !!options.partial
// make the set of regexps etc.
this.make()
}
Minimatch.prototype.debug = function () {}
Minimatch.prototype.make = make
function make () {
var pattern = this.pattern
var options = this.options
// empty patterns and comments match nothing.
if (!options.nocomment && pattern.charAt(0) === '#') {
this.comment = true
return
}
if (!pattern) {
this.empty = true
return
}
// step 1: figure out negation, etc.
this.parseNegate()
// step 2: expand braces
var set = this.globSet = this.braceExpand()
if (options.debug) this.debug = function debug() { console.error.apply(console, arguments) }
this.debug(this.pattern, set)
// step 3: now we have a set, so turn each one into a series of path-portion
// matching patterns.
// These will be regexps, except in the case of "**", which is
// set to the GLOBSTAR object for globstar behavior,
// and will not contain any / characters
set = this.globParts = set.map(function (s) {
return s.split(slashSplit)
})
this.debug(this.pattern, set)
// glob --> regexps
set = set.map(function (s, si, set) {
return s.map(this.parse, this)
}, this)
this.debug(this.pattern, set)
// filter out everything that didn't compile properly.
set = set.filter(function (s) {
return s.indexOf(false) === -1
})
this.debug(this.pattern, set)
this.set = set
}
Minimatch.prototype.parseNegate = parseNegate
function parseNegate () {
var pattern = this.pattern
var negate = false
var options = this.options
var negateOffset = 0
if (options.nonegate) return
for (var i = 0, l = pattern.length
; i < l && pattern.charAt(i) === '!'
; i++) {
negate = !negate
negateOffset++
}
if (negateOffset) this.pattern = pattern.substr(negateOffset)
this.negate = negate
}
// Brace expansion:
// a{b,c}d -> abd acd
// a{b,}c -> abc ac
// a{0..3}d -> a0d a1d a2d a3d
// a{b,c{d,e}f}g -> abg acdfg acefg
// a{b,c}d{e,f}g -> abdeg acdeg abdeg abdfg
//
// Invalid sets are not expanded.
// a{2..}b -> a{2..}b
// a{b}c -> a{b}c
minimatch.braceExpand = function (pattern, options) {
return braceExpand(pattern, options)
}
Minimatch.prototype.braceExpand = braceExpand
function braceExpand (pattern, options) {
if (!options) {
if (this instanceof Minimatch) {
options = this.options
} else {
options = {}
}
}
pattern = typeof pattern === 'undefined'
? this.pattern : pattern
assertValidPattern(pattern)
// Thanks to Yeting Li <https://github.com/yetingli> for
// improving this regexp to avoid a ReDOS vulnerability.
if (options.nobrace || !/\{(?:(?!\{).)*\}/.test(pattern)) {
// shortcut. no need to expand.
return [pattern]
}
return expand(pattern)
}
var MAX_PATTERN_LENGTH = 1024 * 64
var assertValidPattern = function (pattern) {
if (typeof pattern !== 'string') {
throw new TypeError('invalid pattern')
}
if (pattern.length > MAX_PATTERN_LENGTH) {
throw new TypeError('pattern is too long')
}
}
// parse a component of the expanded set.
// At this point, no pattern may contain "/" in it
// so we're going to return a 2d array, where each entry is the full
// pattern, split on '/', and then turned into a regular expression.
// A regexp is made at the end which joins each array with an
// escaped /, and another full one which joins each regexp with |.
//
// Following the lead of Bash 4.1, note that "**" only has special meaning
// when it is the *only* thing in a path portion. Otherwise, any series
// of * is equivalent to a single *. Globstar behavior is enabled by
// default, and can be disabled by setting options.noglobstar.
Minimatch.prototype.parse = parse
var SUBPARSE = {}
function parse (pattern, isSub) {
assertValidPattern(pattern)
var options = this.options
// shortcuts
if (pattern === '**') {
if (!options.noglobstar)
return GLOBSTAR
else
pattern = '*'
}
if (pattern === '') return ''
var re = ''
var hasMagic = !!options.nocase
var escaping = false
// ? => one single character
var patternListStack = []
var negativeLists = []
var stateChar
var inClass = false
var reClassStart = -1
var classStart = -1
// . and .. never match anything that doesn't start with .,
// even when options.dot is set.
var patternStart = pattern.charAt(0) === '.' ? '' // anything
// not (start or / followed by . or .. followed by / or end)
: options.dot ? '(?!(?:^|\\\/)\\.{1,2}(?:$|\\\/))'
: '(?!\\.)'
var self = this
function clearStateChar () {
if (stateChar) {
// we had some state-tracking character
// that wasn't consumed by this pass.
switch (stateChar) {
case '*':
re += star
hasMagic = true
break
case '?':
re += qmark
hasMagic = true
break
default:
re += '\\' + stateChar
break
}
self.debug('clearStateChar %j %j', stateChar, re)
stateChar = false
}
}
for (var i = 0, len = pattern.length, c
; (i < len) && (c = pattern.charAt(i))
; i++) {
this.debug('%s\t%s %s %j', pattern, i, re, c)
// skip over any that are escaped.
if (escaping && reSpecials[c]) {
re += '\\' + c
escaping = false
continue
}
switch (c) {
/* istanbul ignore next */
case '/': {
// completely not allowed, even escaped.
// Should already be path-split by now.
return false
}
case '\\':
clearStateChar()
escaping = true
continue
// the various stateChar values
// for the "extglob" stuff.
case '?':
case '*':
case '+':
case '@':
case '!':
this.debug('%s\t%s %s %j <-- stateChar', pattern, i, re, c)
// all of those are literals inside a class, except that
// the glob [!a] means [^a] in regexp
if (inClass) {
this.debug(' in class')
if (c === '!' && i === classStart + 1) c = '^'
re += c
continue
}
// if we already have a stateChar, then it means
// that there was something like ** or +? in there.
// Handle the stateChar, then proceed with this one.
self.debug('call clearStateChar %j', stateChar)
clearStateChar()
stateChar = c
// if extglob is disabled, then +(asdf|foo) isn't a thing.
// just clear the statechar *now*, rather than even diving into
// the patternList stuff.
if (options.noext) clearStateChar()
continue
case '(':
if (inClass) {
re += '('
continue
}
if (!stateChar) {
re += '\\('
continue
}
patternListStack.push({
type: stateChar,
start: i - 1,
reStart: re.length,
open: plTypes[stateChar].open,
close: plTypes[stateChar].close
})
// negation is (?:(?!js)[^/]*)
re += stateChar === '!' ? '(?:(?!(?:' : '(?:'
this.debug('plType %j %j', stateChar, re)
stateChar = false
continue
case ')':
if (inClass || !patternListStack.length) {
re += '\\)'
continue
}
clearStateChar()
hasMagic = true
var pl = patternListStack.pop()
// negation is (?:(?!js)[^/]*)
// The others are (?:<pattern>)<type>
re += pl.close
if (pl.type === '!') {
negativeLists.push(pl)
}
pl.reEnd = re.length
continue
case '|':
if (inClass || !patternListStack.length || escaping) {
re += '\\|'
escaping = false
continue
}
clearStateChar()
re += '|'
continue
// these are mostly the same in regexp and glob
case '[':
// swallow any state-tracking char before the [
clearStateChar()
if (inClass) {
re += '\\' + c
continue
}
inClass = true
classStart = i
reClassStart = re.length
re += c
continue
case ']':
// a right bracket shall lose its special
// meaning and represent itself in
// a bracket expression if it occurs
// first in the list. -- POSIX.2 2.8.3.2
if (i === classStart + 1 || !inClass) {
re += '\\' + c
escaping = false
continue
}
// handle the case where we left a class open.
// "[z-a]" is valid, equivalent to "\[z-a\]"
// split where the last [ was, make sure we don't have
// an invalid re. if so, re-walk the contents of the
// would-be class to re-translate any characters that
// were passed through as-is
// TODO: It would probably be faster to determine this
// without a try/catch and a new RegExp, but it's tricky
// to do safely. For now, this is safe and works.
var cs = pattern.substring(classStart + 1, i)
try {
RegExp('[' + cs + ']')
} catch (er) {
// not a valid class!
var sp = this.parse(cs, SUBPARSE)
re = re.substr(0, reClassStart) + '\\[' + sp[0] + '\\]'
hasMagic = hasMagic || sp[1]
inClass = false
continue
}
// finish up the class.
hasMagic = true
inClass = false
re += c
continue
default:
// swallow any state char that wasn't consumed
clearStateChar()
if (escaping) {
// no need
escaping = false
} else if (reSpecials[c]
&& !(c === '^' && inClass)) {
re += '\\'
}
re += c
} // switch
} // for
// handle the case where we left a class open.
// "[abc" is valid, equivalent to "\[abc"
if (inClass) {
// split where the last [ was, and escape it
// this is a huge pita. We now have to re-walk
// the contents of the would-be class to re-translate
// any characters that were passed through as-is
cs = pattern.substr(classStart + 1)
sp = this.parse(cs, SUBPARSE)
re = re.substr(0, reClassStart) + '\\[' + sp[0]
hasMagic = hasMagic || sp[1]
}
// handle the case where we had a +( thing at the *end*
// of the pattern.
// each pattern list stack adds 3 chars, and we need to go through
// and escape any | chars that were passed through as-is for the regexp.
// Go through and escape them, taking care not to double-escape any
// | chars that were already escaped.
for (pl = patternListStack.pop(); pl; pl = patternListStack.pop()) {
var tail = re.slice(pl.reStart + pl.open.length)
this.debug('setting tail', re, pl)
// maybe some even number of \, then maybe 1 \, followed by a |
tail = tail.replace(/((?:\\{2}){0,64})(\\?)\|/g, function (_, $1, $2) {
if (!$2) {
// the | isn't already escaped, so escape it.
$2 = '\\'
}
// need to escape all those slashes *again*, without escaping the
// one that we need for escaping the | character. As it works out,
// escaping an even number of slashes can be done by simply repeating
// it exactly after itself. That's why this trick works.
//
// I am sorry that you have to see this.
return $1 + $1 + $2 + '|'
})
this.debug('tail=%j\n %s', tail, tail, pl, re)
var t = pl.type === '*' ? star
: pl.type === '?' ? qmark
: '\\' + pl.type
hasMagic = true
re = re.slice(0, pl.reStart) + t + '\\(' + tail
}
// handle trailing things that only matter at the very end.
clearStateChar()
if (escaping) {
// trailing \\
re += '\\\\'
}
// only need to apply the nodot start if the re starts with
// something that could conceivably capture a dot
var addPatternStart = false
switch (re.charAt(0)) {
case '[': case '.': case '(': addPatternStart = true
}
// Hack to work around lack of negative lookbehind in JS
// A pattern like: *.!(x).!(y|z) needs to ensure that a name
// like 'a.xyz.yz' doesn't match. So, the first negative
// lookahead, has to look ALL the way ahead, to the end of
// the pattern.
for (var n = negativeLists.length - 1; n > -1; n--) {
var nl = negativeLists[n]
var nlBefore = re.slice(0, nl.reStart)
var nlFirst = re.slice(nl.reStart, nl.reEnd - 8)
var nlLast = re.slice(nl.reEnd - 8, nl.reEnd)
var nlAfter = re.slice(nl.reEnd)
nlLast += nlAfter
// Handle nested stuff like *(*.js|!(*.json)), where open parens
// mean that we should *not* include the ) in the bit that is considered
// "after" the negated section.
var openParensBefore = nlBefore.split('(').length - 1
var cleanAfter = nlAfter
for (i = 0; i < openParensBefore; i++) {
cleanAfter = cleanAfter.replace(/\)[+*?]?/, '')
}
nlAfter = cleanAfter
var dollar = ''
if (nlAfter === '' && isSub !== SUBPARSE) {
dollar = '$'
}
var newRe = nlBefore + nlFirst + nlAfter + dollar + nlLast
re = newRe
}
// if the re is not "" at this point, then we need to make sure
// it doesn't match against an empty path part.
// Otherwise a/* will match a/, which it should not.
if (re !== '' && hasMagic) {
re = '(?=.)' + re
}
if (addPatternStart) {
re = patternStart + re
}
// parsing just a piece of a larger pattern.
if (isSub === SUBPARSE) {
return [re, hasMagic]
}
// skip the regexp for non-magical patterns
// unescape anything in it, though, so that it'll be
// an exact match against a file etc.
if (!hasMagic) {
return globUnescape(pattern)
}
var flags = options.nocase ? 'i' : ''
try {
var regExp = new RegExp('^' + re + '$', flags)
} catch (er) /* istanbul ignore next - should be impossible */ {
// If it was an invalid regular expression, then it can't match
// anything. This trick looks for a character after the end of
// the string, which is of course impossible, except in multi-line
// mode, but it's not a /m regex.
return new RegExp('$.')
}
regExp._glob = pattern
regExp._src = re
return regExp
}
minimatch.makeRe = function (pattern, options) {
return new Minimatch(pattern, options || {}).makeRe()
}
Minimatch.prototype.makeRe = makeRe
function makeRe () {
if (this.regexp || this.regexp === false) return this.regexp
// at this point, this.set is a 2d array of partial
// pattern strings, or "**".
//
// It's better to use .match(). This function shouldn't
// be used, really, but it's pretty convenient sometimes,
// when you just want to work with a regex.
var set = this.set
if (!set.length) {
this.regexp = false
return this.regexp
}
var options = this.options
var twoStar = options.noglobstar ? star
: options.dot ? twoStarDot
: twoStarNoDot
var flags = options.nocase ? 'i' : ''
var re = set.map(function (pattern) {
return pattern.map(function (p) {
return (p === GLOBSTAR) ? twoStar
: (typeof p === 'string') ? regExpEscape(p)
: p._src
}).join('\\\/')
}).join('|')
// must match entire pattern
// ending in a * or ** will make it less strict.
re = '^(?:' + re + ')$'
// can match anything, as long as it's not this.
if (this.negate) re = '^(?!' + re + ').*$'
try {
this.regexp = new RegExp(re, flags)
} catch (ex) /* istanbul ignore next - should be impossible */ {
this.regexp = false
}
return this.regexp
}
minimatch.match = function (list, pattern, options) {
options = options || {}
var mm = new Minimatch(pattern, options)
list = list.filter(function (f) {
return mm.match(f)
})
if (mm.options.nonull && !list.length) {
list.push(pattern)
}
return list
}
Minimatch.prototype.match = function match (f, partial) {
if (typeof partial === 'undefined') partial = this.partial
this.debug('match', f, this.pattern)
// short-circuit in the case of busted things.
// comments, etc.
if (this.comment) return false
if (this.empty) return f === ''
if (f === '/' && partial) return true
var options = this.options
// windows: need to use /, not \
if (path.sep !== '/') {
f = f.split(path.sep).join('/')
}
// treat the test path as a set of pathparts.
f = f.split(slashSplit)
this.debug(this.pattern, 'split', f)
// just ONE of the pattern sets in this.set needs to match
// in order for it to be valid. If negating, then just one
// match means that we have failed.
// Either way, return on the first hit.
var set = this.set
this.debug(this.pattern, 'set', set)
// Find the basename of the path by looking for the last non-empty segment
var filename
var i
for (i = f.length - 1; i >= 0; i--) {
filename = f[i]
if (filename) break
}
for (i = 0; i < set.length; i++) {
var pattern = set[i]
var file = f
if (options.matchBase && pattern.length === 1) {
file = [filename]
}
var hit = this.matchOne(file, pattern, partial)
if (hit) {
if (options.flipNegate) return true
return !this.negate
}
}
// didn't get any hits. this is success if it's a negative
// pattern, failure otherwise.
if (options.flipNegate) return false
return this.negate
}
// set partial to true to test if, for example,
// "/a/b" matches the start of "/*/b/*/d"
// Partial means, if you run out of file before you run
// out of pattern, then that's fine, as long as all
// the parts match.
Minimatch.prototype.matchOne = function (file, pattern, partial) {
var options = this.options
this.debug('matchOne',
{ 'this': this, file: file, pattern: pattern })
this.debug('matchOne', file.length, pattern.length)
for (var fi = 0,
pi = 0,
fl = file.length,
pl = pattern.length
; (fi < fl) && (pi < pl)
; fi++, pi++) {
this.debug('matchOne loop')
var p = pattern[pi]
var f = file[fi]
this.debug(pattern, p, f)
// should be impossible.
// some invalid regexp stuff in the set.
/* istanbul ignore if */
if (p === false) return false
if (p === GLOBSTAR) {
this.debug('GLOBSTAR', [pattern, p, f])
// "**"
// a/**/b/**/c would match the following:
// a/b/x/y/z/c
// a/x/y/z/b/c
// a/b/x/b/x/c
// a/b/c
// To do this, take the rest of the pattern after
// the **, and see if it would match the file remainder.
// If so, return success.
// If not, the ** "swallows" a segment, and try again.
// This is recursively awful.
//
// a/**/b/**/c matching a/b/x/y/z/c
// - a matches a
// - doublestar
// - matchOne(b/x/y/z/c, b/**/c)
// - b matches b
// - doublestar
// - matchOne(x/y/z/c, c) -> no
// - matchOne(y/z/c, c) -> no
// - matchOne(z/c, c) -> no
// - matchOne(c, c) yes, hit
var fr = fi
var pr = pi + 1
if (pr === pl) {
this.debug('** at the end')
// a ** at the end will just swallow the rest.
// We have found a match.
// however, it will not swallow /.x, unless
// options.dot is set.
// . and .. are *never* matched by **, for explosively
// exponential reasons.
for (; fi < fl; fi++) {
if (file[fi] === '.' || file[fi] === '..' ||
(!options.dot && file[fi].charAt(0) === '.')) return false
}
return true
}
// ok, let's see if we can swallow whatever we can.
while (fr < fl) {
var swallowee = file[fr]
this.debug('\nglobstar while', file, fr, pattern, pr, swallowee)
// XXX remove this slice. Just pass the start index.
if (this.matchOne(file.slice(fr), pattern.slice(pr), partial)) {
this.debug('globstar found match!', fr, fl, swallowee)
// found a match.
return true
} else {
// can't swallow "." or ".." ever.
// can only swallow ".foo" when explicitly asked.
if (swallowee === '.' || swallowee === '..' ||
(!options.dot && swallowee.charAt(0) === '.')) {
this.debug('dot detected!', file, fr, pattern, pr)
break
}
// ** swallows a segment, and continue.
this.debug('globstar swallow a segment, and continue')
fr++
}
}
// no match was found.
// However, in partial mode, we can't say this is necessarily over.
// If there's more *pattern* left, then
/* istanbul ignore if */
if (partial) {
// ran out of file
this.debug('\n>>> no match, partial?', file, fr, pattern, pr)
if (fr === fl) return true
}
return false
}
// something other than **
// non-magic patterns just have to match exactly
// patterns with magic have been turned into regexps.
var hit
if (typeof p === 'string') {
hit = f === p
this.debug('string match', p, f, hit)
} else {
hit = f.match(p)
this.debug('pattern match', p, f, hit)
}
if (!hit) return false
}
// Note: ending in / means that we'll get a final ""
// at the end of the pattern. This can only match a
// corresponding "" at the end of the file.
// If the file ends in /, then it can only match a
// a pattern that ends in /, unless the pattern just
// doesn't have any more for it. But, a/b/ should *not*
// match "a/b/*", even though "" matches against the
// [^/]*? pattern, except in partial mode, where it might
// simply not be reached yet.
// However, a/b/ should still satisfy a/*
// now either we fell off the end of the pattern, or we're done.
if (fi === fl && pi === pl) {
// ran out of pattern and filename at the same time.
// an exact hit!
return true
} else if (fi === fl) {
// ran out of file, but still had pattern left.
// this is ok if we're doing the match as part of
// a glob fs traversal.
return partial
} else /* istanbul ignore else */ if (pi === pl) {
// ran out of pattern, still have file left.
// this is only acceptable if we're on the very last
// empty segment of a file with a trailing slash.
// a/* should match a/b/
return (fi === fl - 1) && (file[fi] === '')
}
// should be unreachable.
/* istanbul ignore next */
throw new Error('wtf?')
}
// replace stuff like \* with *
function globUnescape (s) {
return s.replace(/\\(.)/g, '$1')
}
function regExpEscape (s) {
return s.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&')
}

View File

@@ -0,0 +1,33 @@
{
"author": "Isaac Z. Schlueter <i@izs.me> (http://blog.izs.me)",
"name": "minimatch",
"description": "a glob matcher in javascript",
"version": "3.1.2",
"publishConfig": {
"tag": "v3-legacy"
},
"repository": {
"type": "git",
"url": "git://github.com/isaacs/minimatch.git"
},
"main": "minimatch.js",
"scripts": {
"test": "tap",
"preversion": "npm test",
"postversion": "npm publish",
"postpublish": "git push origin --all; git push origin --tags"
},
"engines": {
"node": "*"
},
"dependencies": {
"brace-expansion": "^1.1.7"
},
"devDependencies": {
"tap": "^15.1.6"
},
"license": "ISC",
"files": [
"minimatch.js"
]
}

View File

@@ -0,0 +1,65 @@
# v3.0
- Add `--preserve-root` option to executable (default true)
- Drop support for Node.js below version 6
# v2.7
- Make `glob` an optional dependency
# 2.6
- Retry on EBUSY on non-windows platforms as well
- Make `rimraf.sync` 10000% more reliable on Windows
# 2.5
- Handle Windows EPERM when lstat-ing read-only dirs
- Add glob option to pass options to glob
# 2.4
- Add EPERM to delay/retry loop
- Add `disableGlob` option
# 2.3
- Make maxBusyTries and emfileWait configurable
- Handle weird SunOS unlink-dir issue
- Glob the CLI arg for better Windows support
# 2.2
- Handle ENOENT properly on Windows
- Allow overriding fs methods
- Treat EPERM as indicative of non-empty dir
- Remove optional graceful-fs dep
- Consistently return null error instead of undefined on success
- win32: Treat ENOTEMPTY the same as EBUSY
- Add `rimraf` binary
# 2.1
- Fix SunOS error code for a non-empty directory
- Try rmdir before readdir
- Treat EISDIR like EPERM
- Remove chmod
- Remove lstat polyfill, node 0.7 is not supported
# 2.0
- Fix myGid call to check process.getgid
- Simplify the EBUSY backoff logic.
- Use fs.lstat in node >= 0.7.9
- Remove gently option
- remove fiber implementation
- Delete files that are marked read-only
# 1.0
- Allow ENOENT in sync method
- Throw when no callback is provided
- Make opts.gently an absolute path
- use 'stat' if 'lstat' is not available
- Consistent error naming, and rethrow non-ENOENT stat errors
- add fiber implementation

View File

@@ -0,0 +1,15 @@
The ISC License
Copyright (c) Isaac Z. Schlueter and Contributors
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

View File

@@ -0,0 +1,101 @@
[![Build Status](https://travis-ci.org/isaacs/rimraf.svg?branch=master)](https://travis-ci.org/isaacs/rimraf) [![Dependency Status](https://david-dm.org/isaacs/rimraf.svg)](https://david-dm.org/isaacs/rimraf) [![devDependency Status](https://david-dm.org/isaacs/rimraf/dev-status.svg)](https://david-dm.org/isaacs/rimraf#info=devDependencies)
The [UNIX command](http://en.wikipedia.org/wiki/Rm_(Unix)) `rm -rf` for node.
Install with `npm install rimraf`, or just drop rimraf.js somewhere.
## API
`rimraf(f, [opts], callback)`
The first parameter will be interpreted as a globbing pattern for files. If you
want to disable globbing you can do so with `opts.disableGlob` (defaults to
`false`). This might be handy, for instance, if you have filenames that contain
globbing wildcard characters.
The callback will be called with an error if there is one. Certain
errors are handled for you:
* Windows: `EBUSY` and `ENOTEMPTY` - rimraf will back off a maximum of
`opts.maxBusyTries` times before giving up, adding 100ms of wait
between each attempt. The default `maxBusyTries` is 3.
* `ENOENT` - If the file doesn't exist, rimraf will return
successfully, since your desired outcome is already the case.
* `EMFILE` - Since `readdir` requires opening a file descriptor, it's
possible to hit `EMFILE` if too many file descriptors are in use.
In the sync case, there's nothing to be done for this. But in the
async case, rimraf will gradually back off with timeouts up to
`opts.emfileWait` ms, which defaults to 1000.
## options
* unlink, chmod, stat, lstat, rmdir, readdir,
unlinkSync, chmodSync, statSync, lstatSync, rmdirSync, readdirSync
In order to use a custom file system library, you can override
specific fs functions on the options object.
If any of these functions are present on the options object, then
the supplied function will be used instead of the default fs
method.
Sync methods are only relevant for `rimraf.sync()`, of course.
For example:
```javascript
var myCustomFS = require('some-custom-fs')
rimraf('some-thing', myCustomFS, callback)
```
* maxBusyTries
If an `EBUSY`, `ENOTEMPTY`, or `EPERM` error code is encountered
on Windows systems, then rimraf will retry with a linear backoff
wait of 100ms longer on each try. The default maxBusyTries is 3.
Only relevant for async usage.
* emfileWait
If an `EMFILE` error is encountered, then rimraf will retry
repeatedly with a linear backoff of 1ms longer on each try, until
the timeout counter hits this max. The default limit is 1000.
If you repeatedly encounter `EMFILE` errors, then consider using
[graceful-fs](http://npm.im/graceful-fs) in your program.
Only relevant for async usage.
* glob
Set to `false` to disable [glob](http://npm.im/glob) pattern
matching.
Set to an object to pass options to the glob module. The default
glob options are `{ nosort: true, silent: true }`.
Glob version 6 is used in this module.
Relevant for both sync and async usage.
* disableGlob
Set to any non-falsey value to disable globbing entirely.
(Equivalent to setting `glob: false`.)
## rimraf.sync
It can remove stuff synchronously, too. But that's not so good. Use
the async API. It's better.
## CLI
If installed with `npm install rimraf -g` it can be used as a global
command `rimraf <path> [<path> ...]` which is useful for cross platform support.
## mkdirp
If you need to create a directory recursively, check out
[mkdirp](https://github.com/substack/node-mkdirp).

View File

@@ -0,0 +1,68 @@
#!/usr/bin/env node
const rimraf = require('./')
const path = require('path')
const isRoot = arg => /^(\/|[a-zA-Z]:\\)$/.test(path.resolve(arg))
const filterOutRoot = arg => {
const ok = preserveRoot === false || !isRoot(arg)
if (!ok) {
console.error(`refusing to remove ${arg}`)
console.error('Set --no-preserve-root to allow this')
}
return ok
}
let help = false
let dashdash = false
let noglob = false
let preserveRoot = true
const args = process.argv.slice(2).filter(arg => {
if (dashdash)
return !!arg
else if (arg === '--')
dashdash = true
else if (arg === '--no-glob' || arg === '-G')
noglob = true
else if (arg === '--glob' || arg === '-g')
noglob = false
else if (arg.match(/^(-+|\/)(h(elp)?|\?)$/))
help = true
else if (arg === '--preserve-root')
preserveRoot = true
else if (arg === '--no-preserve-root')
preserveRoot = false
else
return !!arg
}).filter(arg => !preserveRoot || filterOutRoot(arg))
const go = n => {
if (n >= args.length)
return
const options = noglob ? { glob: false } : {}
rimraf(args[n], options, er => {
if (er)
throw er
go(n+1)
})
}
if (help || args.length === 0) {
// If they didn't ask for help, then this is not a "success"
const log = help ? console.log : console.error
log('Usage: rimraf <path> [<path> ...]')
log('')
log(' Deletes all files and folders at "path" recursively.')
log('')
log('Options:')
log('')
log(' -h, --help Display this usage info')
log(' -G, --no-glob Do not expand glob patterns in arguments')
log(' -g, --glob Expand glob patterns in arguments (default)')
log(' --preserve-root Do not remove \'/\' (default)')
log(' --no-preserve-root Do not treat \'/\' specially')
log(' -- Stop parsing flags')
process.exit(help ? 0 : 1)
} else
go(0)

View File

@@ -0,0 +1,32 @@
{
"name": "rimraf",
"version": "3.0.2",
"main": "rimraf.js",
"description": "A deep deletion module for node (like `rm -rf`)",
"author": "Isaac Z. Schlueter <i@izs.me> (http://blog.izs.me/)",
"license": "ISC",
"repository": "git://github.com/isaacs/rimraf.git",
"scripts": {
"preversion": "npm test",
"postversion": "npm publish",
"postpublish": "git push origin --follow-tags",
"test": "tap test/*.js"
},
"bin": "./bin.js",
"dependencies": {
"glob": "^7.1.3"
},
"files": [
"LICENSE",
"README.md",
"bin.js",
"rimraf.js"
],
"devDependencies": {
"mkdirp": "^0.5.1",
"tap": "^12.1.1"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
}

View File

@@ -0,0 +1,360 @@
const assert = require("assert")
const path = require("path")
const fs = require("fs")
let glob = undefined
try {
glob = require("glob")
} catch (_err) {
// treat glob as optional.
}
const defaultGlobOpts = {
nosort: true,
silent: true
}
// for EMFILE handling
let timeout = 0
const isWindows = (process.platform === "win32")
const defaults = options => {
const methods = [
'unlink',
'chmod',
'stat',
'lstat',
'rmdir',
'readdir'
]
methods.forEach(m => {
options[m] = options[m] || fs[m]
m = m + 'Sync'
options[m] = options[m] || fs[m]
})
options.maxBusyTries = options.maxBusyTries || 3
options.emfileWait = options.emfileWait || 1000
if (options.glob === false) {
options.disableGlob = true
}
if (options.disableGlob !== true && glob === undefined) {
throw Error('glob dependency not found, set `options.disableGlob = true` if intentional')
}
options.disableGlob = options.disableGlob || false
options.glob = options.glob || defaultGlobOpts
}
const rimraf = (p, options, cb) => {
if (typeof options === 'function') {
cb = options
options = {}
}
assert(p, 'rimraf: missing path')
assert.equal(typeof p, 'string', 'rimraf: path should be a string')
assert.equal(typeof cb, 'function', 'rimraf: callback function required')
assert(options, 'rimraf: invalid options argument provided')
assert.equal(typeof options, 'object', 'rimraf: options should be object')
defaults(options)
let busyTries = 0
let errState = null
let n = 0
const next = (er) => {
errState = errState || er
if (--n === 0)
cb(errState)
}
const afterGlob = (er, results) => {
if (er)
return cb(er)
n = results.length
if (n === 0)
return cb()
results.forEach(p => {
const CB = (er) => {
if (er) {
if ((er.code === "EBUSY" || er.code === "ENOTEMPTY" || er.code === "EPERM") &&
busyTries < options.maxBusyTries) {
busyTries ++
// try again, with the same exact callback as this one.
return setTimeout(() => rimraf_(p, options, CB), busyTries * 100)
}
// this one won't happen if graceful-fs is used.
if (er.code === "EMFILE" && timeout < options.emfileWait) {
return setTimeout(() => rimraf_(p, options, CB), timeout ++)
}
// already gone
if (er.code === "ENOENT") er = null
}
timeout = 0
next(er)
}
rimraf_(p, options, CB)
})
}
if (options.disableGlob || !glob.hasMagic(p))
return afterGlob(null, [p])
options.lstat(p, (er, stat) => {
if (!er)
return afterGlob(null, [p])
glob(p, options.glob, afterGlob)
})
}
// Two possible strategies.
// 1. Assume it's a file. unlink it, then do the dir stuff on EPERM or EISDIR
// 2. Assume it's a directory. readdir, then do the file stuff on ENOTDIR
//
// Both result in an extra syscall when you guess wrong. However, there
// are likely far more normal files in the world than directories. This
// is based on the assumption that a the average number of files per
// directory is >= 1.
//
// If anyone ever complains about this, then I guess the strategy could
// be made configurable somehow. But until then, YAGNI.
const rimraf_ = (p, options, cb) => {
assert(p)
assert(options)
assert(typeof cb === 'function')
// sunos lets the root user unlink directories, which is... weird.
// so we have to lstat here and make sure it's not a dir.
options.lstat(p, (er, st) => {
if (er && er.code === "ENOENT")
return cb(null)
// Windows can EPERM on stat. Life is suffering.
if (er && er.code === "EPERM" && isWindows)
fixWinEPERM(p, options, er, cb)
if (st && st.isDirectory())
return rmdir(p, options, er, cb)
options.unlink(p, er => {
if (er) {
if (er.code === "ENOENT")
return cb(null)
if (er.code === "EPERM")
return (isWindows)
? fixWinEPERM(p, options, er, cb)
: rmdir(p, options, er, cb)
if (er.code === "EISDIR")
return rmdir(p, options, er, cb)
}
return cb(er)
})
})
}
const fixWinEPERM = (p, options, er, cb) => {
assert(p)
assert(options)
assert(typeof cb === 'function')
options.chmod(p, 0o666, er2 => {
if (er2)
cb(er2.code === "ENOENT" ? null : er)
else
options.stat(p, (er3, stats) => {
if (er3)
cb(er3.code === "ENOENT" ? null : er)
else if (stats.isDirectory())
rmdir(p, options, er, cb)
else
options.unlink(p, cb)
})
})
}
const fixWinEPERMSync = (p, options, er) => {
assert(p)
assert(options)
try {
options.chmodSync(p, 0o666)
} catch (er2) {
if (er2.code === "ENOENT")
return
else
throw er
}
let stats
try {
stats = options.statSync(p)
} catch (er3) {
if (er3.code === "ENOENT")
return
else
throw er
}
if (stats.isDirectory())
rmdirSync(p, options, er)
else
options.unlinkSync(p)
}
const rmdir = (p, options, originalEr, cb) => {
assert(p)
assert(options)
assert(typeof cb === 'function')
// try to rmdir first, and only readdir on ENOTEMPTY or EEXIST (SunOS)
// if we guessed wrong, and it's not a directory, then
// raise the original error.
options.rmdir(p, er => {
if (er && (er.code === "ENOTEMPTY" || er.code === "EEXIST" || er.code === "EPERM"))
rmkids(p, options, cb)
else if (er && er.code === "ENOTDIR")
cb(originalEr)
else
cb(er)
})
}
const rmkids = (p, options, cb) => {
assert(p)
assert(options)
assert(typeof cb === 'function')
options.readdir(p, (er, files) => {
if (er)
return cb(er)
let n = files.length
if (n === 0)
return options.rmdir(p, cb)
let errState
files.forEach(f => {
rimraf(path.join(p, f), options, er => {
if (errState)
return
if (er)
return cb(errState = er)
if (--n === 0)
options.rmdir(p, cb)
})
})
})
}
// this looks simpler, and is strictly *faster*, but will
// tie up the JavaScript thread and fail on excessively
// deep directory trees.
const rimrafSync = (p, options) => {
options = options || {}
defaults(options)
assert(p, 'rimraf: missing path')
assert.equal(typeof p, 'string', 'rimraf: path should be a string')
assert(options, 'rimraf: missing options')
assert.equal(typeof options, 'object', 'rimraf: options should be object')
let results
if (options.disableGlob || !glob.hasMagic(p)) {
results = [p]
} else {
try {
options.lstatSync(p)
results = [p]
} catch (er) {
results = glob.sync(p, options.glob)
}
}
if (!results.length)
return
for (let i = 0; i < results.length; i++) {
const p = results[i]
let st
try {
st = options.lstatSync(p)
} catch (er) {
if (er.code === "ENOENT")
return
// Windows can EPERM on stat. Life is suffering.
if (er.code === "EPERM" && isWindows)
fixWinEPERMSync(p, options, er)
}
try {
// sunos lets the root user unlink directories, which is... weird.
if (st && st.isDirectory())
rmdirSync(p, options, null)
else
options.unlinkSync(p)
} catch (er) {
if (er.code === "ENOENT")
return
if (er.code === "EPERM")
return isWindows ? fixWinEPERMSync(p, options, er) : rmdirSync(p, options, er)
if (er.code !== "EISDIR")
throw er
rmdirSync(p, options, er)
}
}
}
const rmdirSync = (p, options, originalEr) => {
assert(p)
assert(options)
try {
options.rmdirSync(p)
} catch (er) {
if (er.code === "ENOENT")
return
if (er.code === "ENOTDIR")
throw originalEr
if (er.code === "ENOTEMPTY" || er.code === "EEXIST" || er.code === "EPERM")
rmkidsSync(p, options)
}
}
const rmkidsSync = (p, options) => {
assert(p)
assert(options)
options.readdirSync(p).forEach(f => rimrafSync(path.join(p, f), options))
// We only end up here once we got ENOTEMPTY at least once, and
// at this point, we are guaranteed to have removed all the kids.
// So, we know that it won't be ENOENT or ENOTDIR or anything else.
// try really hard to delete stuff on windows, because it has a
// PROFOUNDLY annoying habit of not closing handles promptly when
// files are deleted, resulting in spurious ENOTEMPTY errors.
const retries = isWindows ? 100 : 1
let i = 0
do {
let threw = true
try {
const ret = options.rmdirSync(p, options)
threw = false
return ret
} finally {
if (++i < retries && threw)
continue
}
} while (true)
}
module.exports = rimraf
rimraf.sync = rimrafSync

131
server/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;

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]';

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;