Initial Sample.

This commit is contained in:
2024-06-03 20:23:50 +05:30
parent ef2b65f673
commit 5269ec3c66
2575 changed files with 282312 additions and 0 deletions

View File

@@ -0,0 +1,7 @@
root = true
[*]
end_of_line = lf
insert_final_newline = false
indent_style = space
indent_size = 2

View File

@@ -0,0 +1,47 @@
module.exports = {
"extends": ["eslint:recommended"],
"env": {
"es6": true,
"node": true
},
"globals": {
"setTimeout": true
},
"parserOptions": {
"sourceType": "module"
},
"rules": {
"no-console": ["error", { "allow": ["warn", "error"] }],
"no-unsafe-finally": ["off"],
"camelcase": ["error", { "properties": "always" }],
"brace-style": ["off"],
"eqeqeq": ["error", "smart"],
"indent": ["error", 2, { "SwitchCase": 1 }],
"no-throw-literal": ["error"],
"comma-spacing": ["error", { "before": false, "after": true }],
"comma-style": ["error", "last"],
"comma-dangle": ["error", "always-multiline"],
"keyword-spacing": ["error"],
"no-trailing-spaces": ["error"],
"no-multi-spaces": ["error"],
"no-spaced-func": ["error"],
"no-whitespace-before-property": ["error"],
"space-before-blocks": ["error"],
"space-before-function-paren": ["error", "never"],
"space-in-parens": ["error", "never"],
"eol-last": ["error"],
"quotes": ["error", "single", { "avoidEscape": true }],
"no-implicit-globals": ["error"],
"no-useless-concat": ["error"],
"space-infix-ops": ["error", { "int32Hint": true }],
"semi-spacing": ["error", { "before": false, "after": true }],
"semi": ["error", "always", { "omitLastInOneLineBlock": true }],
"object-curly-spacing": ["error", "always"],
"array-bracket-spacing": ["error"],
"max-len": ["error", 100]
}
};

View File

@@ -0,0 +1,18 @@
Copyright (c) 2018 zenparsing (Kevin Smith)
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,176 @@
# zen-observable
An implementation of Observables for JavaScript. Requires Promises or a Promise polyfill.
## Install
```sh
npm install zen-observable
```
## Usage
```js
import Observable from 'zen-observable';
Observable.of(1, 2, 3).subscribe(x => console.log(x));
```
## API
### new Observable(subscribe)
```js
let observable = new Observable(observer => {
// Emit a single value after 1 second
let timer = setTimeout(() => {
observer.next('hello');
observer.complete();
}, 1000);
// On unsubscription, cancel the timer
return () => clearTimeout(timer);
});
```
Creates a new Observable object using the specified subscriber function. The subscriber function is called whenever the `subscribe` method of the observable object is invoked. The subscriber function is passed an *observer* object which has the following methods:
- `next(value)` Sends the next value in the sequence.
- `error(exception)` Terminates the sequence with an exception.
- `complete()` Terminates the sequence successfully.
- `closed` A boolean property whose value is `true` if the observer's subscription is closed.
The subscriber function can optionally return either a cleanup function or a subscription object. If it returns a cleanup function, that function will be called when the subscription has closed. If it returns a subscription object, then the subscription's `unsubscribe` method will be invoked when the subscription has closed.
### Observable.of(...items)
```js
// Logs 1, 2, 3
Observable.of(1, 2, 3).subscribe(x => {
console.log(x);
});
```
Returns an observable which will emit each supplied argument.
### Observable.from(value)
```js
let list = [1, 2, 3];
// Iterate over an object
Observable.from(list).subscribe(x => {
console.log(x);
});
```
```js
// Convert something 'observable' to an Observable instance
Observable.from(otherObservable).subscribe(x => {
console.log(x);
});
```
Converts `value` to an Observable.
- If `value` is an implementation of Observable, then it is converted to an instance of Observable as defined by this library.
- Otherwise, it is converted to an Observable which synchronously iterates over `value`.
### observable.subscribe([observer])
```js
let subscription = observable.subscribe({
next(x) { console.log(x) },
error(err) { console.log(`Finished with error: ${ err }`) },
complete() { console.log('Finished') }
});
```
Subscribes to the observable. Observer objects may have any of the following methods:
- `next(value)` Receives the next value of the sequence.
- `error(exception)` Receives the terminating error of the sequence.
- `complete()` Called when the stream has completed successfully.
Returns a subscription object that can be used to cancel the stream.
### observable.subscribe(nextCallback[, errorCallback, completeCallback])
```js
let subscription = observable.subscribe(
x => console.log(x),
err => console.log(`Finished with error: ${ err }`),
() => console.log('Finished')
);
```
Subscribes to the observable with callback functions. Returns a subscription object that can be used to cancel the stream.
### observable.forEach(callback)
```js
observable.forEach(x => {
console.log(`Received value: ${ x }`);
}).then(() => {
console.log('Finished successfully')
}).catch(err => {
console.log(`Finished with error: ${ err }`);
})
```
Subscribes to the observable and returns a Promise for the completion value of the stream. The `callback` argument is called once for each value in the stream.
### observable.filter(callback)
```js
Observable.of(1, 2, 3).filter(value => {
return value > 2;
}).subscribe(value => {
console.log(value);
});
// 3
```
Returns a new Observable that emits all values which pass the test implemented by the `callback` argument.
### observable.map(callback)
Returns a new Observable that emits the results of calling the `callback` argument for every value in the stream.
```js
Observable.of(1, 2, 3).map(value => {
return value * 2;
}).subscribe(value => {
console.log(value);
});
// 2
// 4
// 6
```
### observable.reduce(callback [,initialValue])
```js
Observable.of(0, 1, 2, 3, 4).reduce((previousValue, currentValue) => {
return previousValue + currentValue;
}).subscribe(result => {
console.log(result);
});
// 10
```
Returns a new Observable that applies a function against an accumulator and each value of the stream to reduce it to a single value.
### observable.concat(...sources)
```js
Observable.of(1, 2, 3).concat(
Observable.of(4, 5, 6),
Observable.of(7, 8, 9)
).subscribe(result => {
console.log(result);
});
// 1, 2, 3, 4, 5, 6, 7, 8, 9
```
Merges the current observable with additional observables.

View File

@@ -0,0 +1,5 @@
import { Observable } from './src/Observable.js';
export default Observable;
export { Observable };
export * from './src/extras.js';

View File

@@ -0,0 +1 @@
module.exports = require('./lib/extras.js');

View File

@@ -0,0 +1 @@
module.exports = require('./lib/Observable.js').Observable;

View File

@@ -0,0 +1,617 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.Observable = void 0;
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
// === Symbol Support ===
var hasSymbols = function () {
return typeof Symbol === 'function';
};
var hasSymbol = function (name) {
return hasSymbols() && Boolean(Symbol[name]);
};
var getSymbol = function (name) {
return hasSymbol(name) ? Symbol[name] : '@@' + name;
};
if (hasSymbols() && !hasSymbol('observable')) {
Symbol.observable = Symbol('observable');
}
var SymbolIterator = getSymbol('iterator');
var SymbolObservable = getSymbol('observable');
var SymbolSpecies = getSymbol('species'); // === Abstract Operations ===
function getMethod(obj, key) {
var value = obj[key];
if (value == null) return undefined;
if (typeof value !== 'function') throw new TypeError(value + ' is not a function');
return value;
}
function getSpecies(obj) {
var ctor = obj.constructor;
if (ctor !== undefined) {
ctor = ctor[SymbolSpecies];
if (ctor === null) {
ctor = undefined;
}
}
return ctor !== undefined ? ctor : Observable;
}
function isObservable(x) {
return x instanceof Observable; // SPEC: Brand check
}
function hostReportError(e) {
if (hostReportError.log) {
hostReportError.log(e);
} else {
setTimeout(function () {
throw e;
});
}
}
function enqueue(fn) {
Promise.resolve().then(function () {
try {
fn();
} catch (e) {
hostReportError(e);
}
});
}
function cleanupSubscription(subscription) {
var cleanup = subscription._cleanup;
if (cleanup === undefined) return;
subscription._cleanup = undefined;
if (!cleanup) {
return;
}
try {
if (typeof cleanup === 'function') {
cleanup();
} else {
var unsubscribe = getMethod(cleanup, 'unsubscribe');
if (unsubscribe) {
unsubscribe.call(cleanup);
}
}
} catch (e) {
hostReportError(e);
}
}
function closeSubscription(subscription) {
subscription._observer = undefined;
subscription._queue = undefined;
subscription._state = 'closed';
}
function flushSubscription(subscription) {
var queue = subscription._queue;
if (!queue) {
return;
}
subscription._queue = undefined;
subscription._state = 'ready';
for (var i = 0; i < queue.length; ++i) {
notifySubscription(subscription, queue[i].type, queue[i].value);
if (subscription._state === 'closed') break;
}
}
function notifySubscription(subscription, type, value) {
subscription._state = 'running';
var observer = subscription._observer;
try {
var m = getMethod(observer, type);
switch (type) {
case 'next':
if (m) m.call(observer, value);
break;
case 'error':
closeSubscription(subscription);
if (m) m.call(observer, value);else throw value;
break;
case 'complete':
closeSubscription(subscription);
if (m) m.call(observer);
break;
}
} catch (e) {
hostReportError(e);
}
if (subscription._state === 'closed') cleanupSubscription(subscription);else if (subscription._state === 'running') subscription._state = 'ready';
}
function onNotify(subscription, type, value) {
if (subscription._state === 'closed') return;
if (subscription._state === 'buffering') {
subscription._queue.push({
type: type,
value: value
});
return;
}
if (subscription._state !== 'ready') {
subscription._state = 'buffering';
subscription._queue = [{
type: type,
value: value
}];
enqueue(function () {
return flushSubscription(subscription);
});
return;
}
notifySubscription(subscription, type, value);
}
var Subscription =
/*#__PURE__*/
function () {
function Subscription(observer, subscriber) {
_classCallCheck(this, Subscription);
// ASSERT: observer is an object
// ASSERT: subscriber is callable
this._cleanup = undefined;
this._observer = observer;
this._queue = undefined;
this._state = 'initializing';
var subscriptionObserver = new SubscriptionObserver(this);
try {
this._cleanup = subscriber.call(undefined, subscriptionObserver);
} catch (e) {
subscriptionObserver.error(e);
}
if (this._state === 'initializing') this._state = 'ready';
}
_createClass(Subscription, [{
key: "unsubscribe",
value: function unsubscribe() {
if (this._state !== 'closed') {
closeSubscription(this);
cleanupSubscription(this);
}
}
}, {
key: "closed",
get: function () {
return this._state === 'closed';
}
}]);
return Subscription;
}();
var SubscriptionObserver =
/*#__PURE__*/
function () {
function SubscriptionObserver(subscription) {
_classCallCheck(this, SubscriptionObserver);
this._subscription = subscription;
}
_createClass(SubscriptionObserver, [{
key: "next",
value: function next(value) {
onNotify(this._subscription, 'next', value);
}
}, {
key: "error",
value: function error(value) {
onNotify(this._subscription, 'error', value);
}
}, {
key: "complete",
value: function complete() {
onNotify(this._subscription, 'complete');
}
}, {
key: "closed",
get: function () {
return this._subscription._state === 'closed';
}
}]);
return SubscriptionObserver;
}();
var Observable =
/*#__PURE__*/
function () {
function Observable(subscriber) {
_classCallCheck(this, Observable);
if (!(this instanceof Observable)) throw new TypeError('Observable cannot be called as a function');
if (typeof subscriber !== 'function') throw new TypeError('Observable initializer must be a function');
this._subscriber = subscriber;
}
_createClass(Observable, [{
key: "subscribe",
value: function subscribe(observer) {
if (typeof observer !== 'object' || observer === null) {
observer = {
next: observer,
error: arguments[1],
complete: arguments[2]
};
}
return new Subscription(observer, this._subscriber);
}
}, {
key: "forEach",
value: function forEach(fn) {
var _this = this;
return new Promise(function (resolve, reject) {
if (typeof fn !== 'function') {
reject(new TypeError(fn + ' is not a function'));
return;
}
function done() {
subscription.unsubscribe();
resolve();
}
var subscription = _this.subscribe({
next: function (value) {
try {
fn(value, done);
} catch (e) {
reject(e);
subscription.unsubscribe();
}
},
error: reject,
complete: resolve
});
});
}
}, {
key: "map",
value: function map(fn) {
var _this2 = this;
if (typeof fn !== 'function') throw new TypeError(fn + ' is not a function');
var C = getSpecies(this);
return new C(function (observer) {
return _this2.subscribe({
next: function (value) {
try {
value = fn(value);
} catch (e) {
return observer.error(e);
}
observer.next(value);
},
error: function (e) {
observer.error(e);
},
complete: function () {
observer.complete();
}
});
});
}
}, {
key: "filter",
value: function filter(fn) {
var _this3 = this;
if (typeof fn !== 'function') throw new TypeError(fn + ' is not a function');
var C = getSpecies(this);
return new C(function (observer) {
return _this3.subscribe({
next: function (value) {
try {
if (!fn(value)) return;
} catch (e) {
return observer.error(e);
}
observer.next(value);
},
error: function (e) {
observer.error(e);
},
complete: function () {
observer.complete();
}
});
});
}
}, {
key: "reduce",
value: function reduce(fn) {
var _this4 = this;
if (typeof fn !== 'function') throw new TypeError(fn + ' is not a function');
var C = getSpecies(this);
var hasSeed = arguments.length > 1;
var hasValue = false;
var seed = arguments[1];
var acc = seed;
return new C(function (observer) {
return _this4.subscribe({
next: function (value) {
var first = !hasValue;
hasValue = true;
if (!first || hasSeed) {
try {
acc = fn(acc, value);
} catch (e) {
return observer.error(e);
}
} else {
acc = value;
}
},
error: function (e) {
observer.error(e);
},
complete: function () {
if (!hasValue && !hasSeed) return observer.error(new TypeError('Cannot reduce an empty sequence'));
observer.next(acc);
observer.complete();
}
});
});
}
}, {
key: "concat",
value: function concat() {
var _this5 = this;
for (var _len = arguments.length, sources = new Array(_len), _key = 0; _key < _len; _key++) {
sources[_key] = arguments[_key];
}
var C = getSpecies(this);
return new C(function (observer) {
var subscription;
var index = 0;
function startNext(next) {
subscription = next.subscribe({
next: function (v) {
observer.next(v);
},
error: function (e) {
observer.error(e);
},
complete: function () {
if (index === sources.length) {
subscription = undefined;
observer.complete();
} else {
startNext(C.from(sources[index++]));
}
}
});
}
startNext(_this5);
return function () {
if (subscription) {
subscription.unsubscribe();
subscription = undefined;
}
};
});
}
}, {
key: "flatMap",
value: function flatMap(fn) {
var _this6 = this;
if (typeof fn !== 'function') throw new TypeError(fn + ' is not a function');
var C = getSpecies(this);
return new C(function (observer) {
var subscriptions = [];
var outer = _this6.subscribe({
next: function (value) {
if (fn) {
try {
value = fn(value);
} catch (e) {
return observer.error(e);
}
}
var inner = C.from(value).subscribe({
next: function (value) {
observer.next(value);
},
error: function (e) {
observer.error(e);
},
complete: function () {
var i = subscriptions.indexOf(inner);
if (i >= 0) subscriptions.splice(i, 1);
completeIfDone();
}
});
subscriptions.push(inner);
},
error: function (e) {
observer.error(e);
},
complete: function () {
completeIfDone();
}
});
function completeIfDone() {
if (outer.closed && subscriptions.length === 0) observer.complete();
}
return function () {
subscriptions.forEach(function (s) {
return s.unsubscribe();
});
outer.unsubscribe();
};
});
}
}, {
key: SymbolObservable,
value: function () {
return this;
}
}], [{
key: "from",
value: function from(x) {
var C = typeof this === 'function' ? this : Observable;
if (x == null) throw new TypeError(x + ' is not an object');
var method = getMethod(x, SymbolObservable);
if (method) {
var observable = method.call(x);
if (Object(observable) !== observable) throw new TypeError(observable + ' is not an object');
if (isObservable(observable) && observable.constructor === C) return observable;
return new C(function (observer) {
return observable.subscribe(observer);
});
}
if (hasSymbol('iterator')) {
method = getMethod(x, SymbolIterator);
if (method) {
return new C(function (observer) {
enqueue(function () {
if (observer.closed) return;
var _iteratorNormalCompletion = true;
var _didIteratorError = false;
var _iteratorError = undefined;
try {
for (var _iterator = method.call(x)[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
var _item = _step.value;
observer.next(_item);
if (observer.closed) return;
}
} catch (err) {
_didIteratorError = true;
_iteratorError = err;
} finally {
try {
if (!_iteratorNormalCompletion && _iterator.return != null) {
_iterator.return();
}
} finally {
if (_didIteratorError) {
throw _iteratorError;
}
}
}
observer.complete();
});
});
}
}
if (Array.isArray(x)) {
return new C(function (observer) {
enqueue(function () {
if (observer.closed) return;
for (var i = 0; i < x.length; ++i) {
observer.next(x[i]);
if (observer.closed) return;
}
observer.complete();
});
});
}
throw new TypeError(x + ' is not observable');
}
}, {
key: "of",
value: function of() {
for (var _len2 = arguments.length, items = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
items[_key2] = arguments[_key2];
}
var C = typeof this === 'function' ? this : Observable;
return new C(function (observer) {
enqueue(function () {
if (observer.closed) return;
for (var i = 0; i < items.length; ++i) {
observer.next(items[i]);
if (observer.closed) return;
}
observer.complete();
});
});
}
}, {
key: SymbolSpecies,
get: function () {
return this;
}
}]);
return Observable;
}();
exports.Observable = Observable;
if (hasSymbols()) {
Object.defineProperty(Observable, Symbol('extensions'), {
value: {
symbol: SymbolObservable,
hostReportError: hostReportError
},
configurable: true
});
}

View File

@@ -0,0 +1,132 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.merge = merge;
exports.combineLatest = combineLatest;
exports.zip = zip;
var _Observable = require("./Observable.js");
// Emits all values from all inputs in parallel
function merge() {
for (var _len = arguments.length, sources = new Array(_len), _key = 0; _key < _len; _key++) {
sources[_key] = arguments[_key];
}
return new _Observable.Observable(function (observer) {
if (sources.length === 0) return _Observable.Observable.from([]);
var count = sources.length;
var subscriptions = sources.map(function (source) {
return _Observable.Observable.from(source).subscribe({
next: function (v) {
observer.next(v);
},
error: function (e) {
observer.error(e);
},
complete: function () {
if (--count === 0) observer.complete();
}
});
});
return function () {
return subscriptions.forEach(function (s) {
return s.unsubscribe();
});
};
});
} // Emits arrays containing the most current values from each input
function combineLatest() {
for (var _len2 = arguments.length, sources = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
sources[_key2] = arguments[_key2];
}
return new _Observable.Observable(function (observer) {
if (sources.length === 0) return _Observable.Observable.from([]);
var count = sources.length;
var seen = new Set();
var seenAll = false;
var values = sources.map(function () {
return undefined;
});
var subscriptions = sources.map(function (source, index) {
return _Observable.Observable.from(source).subscribe({
next: function (v) {
values[index] = v;
if (!seenAll) {
seen.add(index);
if (seen.size !== sources.length) return;
seen = null;
seenAll = true;
}
observer.next(Array.from(values));
},
error: function (e) {
observer.error(e);
},
complete: function () {
if (--count === 0) observer.complete();
}
});
});
return function () {
return subscriptions.forEach(function (s) {
return s.unsubscribe();
});
};
});
} // Emits arrays containing the matching index values from each input
function zip() {
for (var _len3 = arguments.length, sources = new Array(_len3), _key3 = 0; _key3 < _len3; _key3++) {
sources[_key3] = arguments[_key3];
}
return new _Observable.Observable(function (observer) {
if (sources.length === 0) return _Observable.Observable.from([]);
var queues = sources.map(function () {
return [];
});
function done() {
return queues.some(function (q, i) {
return q.length === 0 && subscriptions[i].closed;
});
}
var subscriptions = sources.map(function (source, index) {
return _Observable.Observable.from(source).subscribe({
next: function (v) {
queues[index].push(v);
if (queues.every(function (q) {
return q.length > 0;
})) {
observer.next(queues.map(function (q) {
return q.shift();
}));
if (done()) observer.complete();
}
},
error: function (e) {
observer.error(e);
},
complete: function () {
if (done()) observer.complete();
}
});
});
return function () {
return subscriptions.forEach(function (s) {
return s.unsubscribe();
});
};
});
}

View File

@@ -0,0 +1,23 @@
{
"name": "zen-observable",
"version": "0.8.15",
"repository": "zenparsing/zen-observable",
"description": "An Implementation of ES Observables",
"homepage": "https://github.com/zenparsing/zen-observable",
"license": "MIT",
"devDependencies": {
"@babel/cli": "^7.6.0",
"@babel/core": "^7.6.0",
"@babel/preset-env": "^7.6.0",
"@babel/register": "^7.6.0",
"eslint": "^6.5.0",
"mocha": "^6.2.0"
},
"dependencies": {},
"scripts": {
"test": "mocha --recursive --require ./scripts/mocha-require",
"lint": "eslint src/*",
"build": "git clean -dfX ./lib && node ./scripts/build",
"prepublishOnly": "npm run lint && npm test && npm run build"
}
}

View File

@@ -0,0 +1,16 @@
module.exports = [
'@babel/plugin-transform-arrow-functions',
'@babel/plugin-transform-block-scoped-functions',
'@babel/plugin-transform-block-scoping',
'@babel/plugin-transform-classes',
'@babel/plugin-transform-computed-properties',
'@babel/plugin-transform-destructuring',
'@babel/plugin-transform-duplicate-keys',
'@babel/plugin-transform-for-of',
'@babel/plugin-transform-literals',
'@babel/plugin-transform-modules-commonjs',
'@babel/plugin-transform-parameters',
'@babel/plugin-transform-shorthand-properties',
'@babel/plugin-transform-spread',
'@babel/plugin-transform-template-literals',
];

View File

@@ -0,0 +1,7 @@
const { execSync } = require('child_process');
const plugins = require('./babel-plugins');
execSync('babel src --out-dir lib --plugins=' + plugins.join(','), {
env: process.env,
stdio: 'inherit',
});

View File

@@ -0,0 +1,3 @@
require('@babel/register')({
plugins: require('./babel-plugins'),
});

View File

@@ -0,0 +1,476 @@
// === Symbol Support ===
const hasSymbols = () => typeof Symbol === 'function';
const hasSymbol = name => hasSymbols() && Boolean(Symbol[name]);
const getSymbol = name => hasSymbol(name) ? Symbol[name] : '@@' + name;
if (hasSymbols() && !hasSymbol('observable')) {
Symbol.observable = Symbol('observable');
}
const SymbolIterator = getSymbol('iterator');
const SymbolObservable = getSymbol('observable');
const SymbolSpecies = getSymbol('species');
// === Abstract Operations ===
function getMethod(obj, key) {
let value = obj[key];
if (value == null)
return undefined;
if (typeof value !== 'function')
throw new TypeError(value + ' is not a function');
return value;
}
function getSpecies(obj) {
let ctor = obj.constructor;
if (ctor !== undefined) {
ctor = ctor[SymbolSpecies];
if (ctor === null) {
ctor = undefined;
}
}
return ctor !== undefined ? ctor : Observable;
}
function isObservable(x) {
return x instanceof Observable; // SPEC: Brand check
}
function hostReportError(e) {
if (hostReportError.log) {
hostReportError.log(e);
} else {
setTimeout(() => { throw e });
}
}
function enqueue(fn) {
Promise.resolve().then(() => {
try { fn() }
catch (e) { hostReportError(e) }
});
}
function cleanupSubscription(subscription) {
let cleanup = subscription._cleanup;
if (cleanup === undefined)
return;
subscription._cleanup = undefined;
if (!cleanup) {
return;
}
try {
if (typeof cleanup === 'function') {
cleanup();
} else {
let unsubscribe = getMethod(cleanup, 'unsubscribe');
if (unsubscribe) {
unsubscribe.call(cleanup);
}
}
} catch (e) {
hostReportError(e);
}
}
function closeSubscription(subscription) {
subscription._observer = undefined;
subscription._queue = undefined;
subscription._state = 'closed';
}
function flushSubscription(subscription) {
let queue = subscription._queue;
if (!queue) {
return;
}
subscription._queue = undefined;
subscription._state = 'ready';
for (let i = 0; i < queue.length; ++i) {
notifySubscription(subscription, queue[i].type, queue[i].value);
if (subscription._state === 'closed')
break;
}
}
function notifySubscription(subscription, type, value) {
subscription._state = 'running';
let observer = subscription._observer;
try {
let m = getMethod(observer, type);
switch (type) {
case 'next':
if (m) m.call(observer, value);
break;
case 'error':
closeSubscription(subscription);
if (m) m.call(observer, value);
else throw value;
break;
case 'complete':
closeSubscription(subscription);
if (m) m.call(observer);
break;
}
} catch (e) {
hostReportError(e);
}
if (subscription._state === 'closed')
cleanupSubscription(subscription);
else if (subscription._state === 'running')
subscription._state = 'ready';
}
function onNotify(subscription, type, value) {
if (subscription._state === 'closed')
return;
if (subscription._state === 'buffering') {
subscription._queue.push({ type, value });
return;
}
if (subscription._state !== 'ready') {
subscription._state = 'buffering';
subscription._queue = [{ type, value }];
enqueue(() => flushSubscription(subscription));
return;
}
notifySubscription(subscription, type, value);
}
class Subscription {
constructor(observer, subscriber) {
// ASSERT: observer is an object
// ASSERT: subscriber is callable
this._cleanup = undefined;
this._observer = observer;
this._queue = undefined;
this._state = 'initializing';
let subscriptionObserver = new SubscriptionObserver(this);
try {
this._cleanup = subscriber.call(undefined, subscriptionObserver);
} catch (e) {
subscriptionObserver.error(e);
}
if (this._state === 'initializing')
this._state = 'ready';
}
get closed() {
return this._state === 'closed';
}
unsubscribe() {
if (this._state !== 'closed') {
closeSubscription(this);
cleanupSubscription(this);
}
}
}
class SubscriptionObserver {
constructor(subscription) { this._subscription = subscription }
get closed() { return this._subscription._state === 'closed' }
next(value) { onNotify(this._subscription, 'next', value) }
error(value) { onNotify(this._subscription, 'error', value) }
complete() { onNotify(this._subscription, 'complete') }
}
export class Observable {
constructor(subscriber) {
if (!(this instanceof Observable))
throw new TypeError('Observable cannot be called as a function');
if (typeof subscriber !== 'function')
throw new TypeError('Observable initializer must be a function');
this._subscriber = subscriber;
}
subscribe(observer) {
if (typeof observer !== 'object' || observer === null) {
observer = {
next: observer,
error: arguments[1],
complete: arguments[2],
};
}
return new Subscription(observer, this._subscriber);
}
forEach(fn) {
return new Promise((resolve, reject) => {
if (typeof fn !== 'function') {
reject(new TypeError(fn + ' is not a function'));
return;
}
function done() {
subscription.unsubscribe();
resolve();
}
let subscription = this.subscribe({
next(value) {
try {
fn(value, done);
} catch (e) {
reject(e);
subscription.unsubscribe();
}
},
error: reject,
complete: resolve,
});
});
}
map(fn) {
if (typeof fn !== 'function')
throw new TypeError(fn + ' is not a function');
let C = getSpecies(this);
return new C(observer => this.subscribe({
next(value) {
try { value = fn(value) }
catch (e) { return observer.error(e) }
observer.next(value);
},
error(e) { observer.error(e) },
complete() { observer.complete() },
}));
}
filter(fn) {
if (typeof fn !== 'function')
throw new TypeError(fn + ' is not a function');
let C = getSpecies(this);
return new C(observer => this.subscribe({
next(value) {
try { if (!fn(value)) return; }
catch (e) { return observer.error(e) }
observer.next(value);
},
error(e) { observer.error(e) },
complete() { observer.complete() },
}));
}
reduce(fn) {
if (typeof fn !== 'function')
throw new TypeError(fn + ' is not a function');
let C = getSpecies(this);
let hasSeed = arguments.length > 1;
let hasValue = false;
let seed = arguments[1];
let acc = seed;
return new C(observer => this.subscribe({
next(value) {
let first = !hasValue;
hasValue = true;
if (!first || hasSeed) {
try { acc = fn(acc, value) }
catch (e) { return observer.error(e) }
} else {
acc = value;
}
},
error(e) { observer.error(e) },
complete() {
if (!hasValue && !hasSeed)
return observer.error(new TypeError('Cannot reduce an empty sequence'));
observer.next(acc);
observer.complete();
},
}));
}
concat(...sources) {
let C = getSpecies(this);
return new C(observer => {
let subscription;
let index = 0;
function startNext(next) {
subscription = next.subscribe({
next(v) { observer.next(v) },
error(e) { observer.error(e) },
complete() {
if (index === sources.length) {
subscription = undefined;
observer.complete();
} else {
startNext(C.from(sources[index++]));
}
},
});
}
startNext(this);
return () => {
if (subscription) {
subscription.unsubscribe();
subscription = undefined;
}
};
});
}
flatMap(fn) {
if (typeof fn !== 'function')
throw new TypeError(fn + ' is not a function');
let C = getSpecies(this);
return new C(observer => {
let subscriptions = [];
let outer = this.subscribe({
next(value) {
if (fn) {
try { value = fn(value) }
catch (e) { return observer.error(e) }
}
let inner = C.from(value).subscribe({
next(value) { observer.next(value) },
error(e) { observer.error(e) },
complete() {
let i = subscriptions.indexOf(inner);
if (i >= 0) subscriptions.splice(i, 1);
completeIfDone();
},
});
subscriptions.push(inner);
},
error(e) { observer.error(e) },
complete() { completeIfDone() },
});
function completeIfDone() {
if (outer.closed && subscriptions.length === 0)
observer.complete();
}
return () => {
subscriptions.forEach(s => s.unsubscribe());
outer.unsubscribe();
};
});
}
[SymbolObservable]() { return this }
static from(x) {
let C = typeof this === 'function' ? this : Observable;
if (x == null)
throw new TypeError(x + ' is not an object');
let method = getMethod(x, SymbolObservable);
if (method) {
let observable = method.call(x);
if (Object(observable) !== observable)
throw new TypeError(observable + ' is not an object');
if (isObservable(observable) && observable.constructor === C)
return observable;
return new C(observer => observable.subscribe(observer));
}
if (hasSymbol('iterator')) {
method = getMethod(x, SymbolIterator);
if (method) {
return new C(observer => {
enqueue(() => {
if (observer.closed) return;
for (let item of method.call(x)) {
observer.next(item);
if (observer.closed) return;
}
observer.complete();
});
});
}
}
if (Array.isArray(x)) {
return new C(observer => {
enqueue(() => {
if (observer.closed) return;
for (let i = 0; i < x.length; ++i) {
observer.next(x[i]);
if (observer.closed) return;
}
observer.complete();
});
});
}
throw new TypeError(x + ' is not observable');
}
static of(...items) {
let C = typeof this === 'function' ? this : Observable;
return new C(observer => {
enqueue(() => {
if (observer.closed) return;
for (let i = 0; i < items.length; ++i) {
observer.next(items[i]);
if (observer.closed) return;
}
observer.complete();
});
});
}
static get [SymbolSpecies]() { return this }
}
if (hasSymbols()) {
Object.defineProperty(Observable, Symbol('extensions'), {
value: {
symbol: SymbolObservable,
hostReportError,
},
configurable: true,
});
}

View File

@@ -0,0 +1,99 @@
import { Observable } from './Observable.js';
// Emits all values from all inputs in parallel
export function merge(...sources) {
return new Observable(observer => {
if (sources.length === 0)
return Observable.from([]);
let count = sources.length;
let subscriptions = sources.map(source => Observable.from(source).subscribe({
next(v) {
observer.next(v);
},
error(e) {
observer.error(e);
},
complete() {
if (--count === 0)
observer.complete();
},
}));
return () => subscriptions.forEach(s => s.unsubscribe());
});
}
// Emits arrays containing the most current values from each input
export function combineLatest(...sources) {
return new Observable(observer => {
if (sources.length === 0)
return Observable.from([]);
let count = sources.length;
let seen = new Set();
let seenAll = false;
let values = sources.map(() => undefined);
let subscriptions = sources.map((source, index) => Observable.from(source).subscribe({
next(v) {
values[index] = v;
if (!seenAll) {
seen.add(index);
if (seen.size !== sources.length)
return;
seen = null;
seenAll = true;
}
observer.next(Array.from(values));
},
error(e) {
observer.error(e);
},
complete() {
if (--count === 0)
observer.complete();
},
}));
return () => subscriptions.forEach(s => s.unsubscribe());
});
}
// Emits arrays containing the matching index values from each input
export function zip(...sources) {
return new Observable(observer => {
if (sources.length === 0)
return Observable.from([]);
let queues = sources.map(() => []);
function done() {
return queues.some((q, i) => q.length === 0 && subscriptions[i].closed);
}
let subscriptions = sources.map((source, index) => Observable.from(source).subscribe({
next(v) {
queues[index].push(v);
if (queues.every(q => q.length > 0)) {
observer.next(queues.map(q => q.shift()));
if (done())
observer.complete();
}
},
error(e) {
observer.error(e);
},
complete() {
if (done())
observer.complete();
},
}));
return () => subscriptions.forEach(s => s.unsubscribe());
});
}

View File

@@ -0,0 +1,30 @@
import assert from 'assert';
describe('concat', () => {
it('concatenates the supplied Observable arguments', async () => {
let list = [];
await Observable
.from([1, 2, 3, 4])
.concat(Observable.of(5, 6, 7))
.forEach(x => list.push(x));
assert.deepEqual(list, [1, 2, 3, 4, 5, 6, 7]);
});
it('can be used multiple times to produce the same results', async () => {
const list1 = [];
const list2 = [];
const concatenated = Observable.from([1, 2, 3, 4])
.concat(Observable.of(5, 6, 7));
await concatenated
.forEach(x => list1.push(x));
await concatenated
.forEach(x => list2.push(x));
assert.deepEqual(list1, [1, 2, 3, 4, 5, 6, 7]);
assert.deepEqual(list2, [1, 2, 3, 4, 5, 6, 7]);
});
});

View File

@@ -0,0 +1,36 @@
import assert from 'assert';
import { testMethodProperty } from './properties.js';
describe('constructor', () => {
it('throws if called as a function', () => {
assert.throws(() => Observable(() => {}));
assert.throws(() => Observable.call({}, () => {}));
});
it('throws if the argument is not callable', () => {
assert.throws(() => new Observable({}));
assert.throws(() => new Observable());
assert.throws(() => new Observable(1));
assert.throws(() => new Observable('string'));
});
it('accepts a function argument', () => {
let result = new Observable(() => {});
assert.ok(result instanceof Observable);
});
it('is the value of Observable.prototype.constructor', () => {
testMethodProperty(Observable.prototype, 'constructor', {
configurable: true,
writable: true,
length: 1,
});
});
it('does not call the subscriber function', () => {
let called = 0;
new Observable(() => { called++ });
assert.equal(called, 0);
});
});

View File

@@ -0,0 +1,43 @@
import assert from 'assert';
import { parse } from './parse.js';
import { combineLatest } from '../../src/extras.js';
describe('extras/combineLatest', () => {
it('should emit arrays containing the most recent values', async () => {
let output = [];
await combineLatest(
parse('a-b-c-d'),
parse('-A-B-C-D')
).forEach(
value => output.push(value.join(''))
);
assert.deepEqual(output, [
'aA',
'bA',
'bB',
'cB',
'cC',
'dC',
'dD',
]);
});
it('should emit values in the correct order', async () => {
let output = [];
await combineLatest(
parse('-a-b-c-d'),
parse('A-B-C-D')
).forEach(
value => output.push(value.join(''))
);
assert.deepEqual(output, [
'aA',
'aB',
'bB',
'bC',
'cC',
'cD',
'dD',
]);
});
});

View File

@@ -0,0 +1,16 @@
import assert from 'assert';
import { parse } from './parse.js';
import { merge } from '../../src/extras.js';
describe('extras/merge', () => {
it('should emit all data from each input in parallel', async () => {
let output = '';
await merge(
parse('a-b-c-d'),
parse('-A-B-C-D')
).forEach(
value => output += value
);
assert.equal(output, 'aAbBcCdD');
});
});

View File

@@ -0,0 +1,11 @@
export function parse(string) {
return new Observable(async observer => {
await null;
for (let char of string) {
if (observer.closed) return;
else if (char !== '-') observer.next(char);
await null;
}
observer.complete();
});
}

View File

@@ -0,0 +1,21 @@
import assert from 'assert';
import { parse } from './parse.js';
import { zip } from '../../src/extras.js';
describe('extras/zip', () => {
it('should emit pairs of corresponding index values', async () => {
let output = [];
await zip(
parse('a-b-c-d'),
parse('-A-B-C-D')
).forEach(
value => output.push(value.join(''))
);
assert.deepEqual(output, [
'aA',
'bB',
'cC',
'dD',
]);
});
});

View File

@@ -0,0 +1,14 @@
import assert from 'assert';
describe('filter', () => {
it('filters the results using the supplied callback', async () => {
let list = [];
await Observable
.from([1, 2, 3, 4])
.filter(x => x > 2)
.forEach(x => list.push(x));
assert.deepEqual(list, [3, 4]);
});
});

View File

@@ -0,0 +1,23 @@
import assert from 'assert';
describe('flatMap', () => {
it('maps and flattens the results using the supplied callback', async () => {
let list = [];
await Observable.of('a', 'b', 'c').flatMap(x =>
Observable.of(1, 2, 3).map(y => [x, y])
).forEach(x => list.push(x));
assert.deepEqual(list, [
['a', 1],
['a', 2],
['a', 3],
['b', 1],
['b', 2],
['b', 3],
['c', 1],
['c', 2],
['c', 3],
]);
});
});

View File

@@ -0,0 +1,70 @@
import assert from 'assert';
describe('forEach', () => {
it('rejects if the argument is not a function', async () => {
let promise = Observable.of(1, 2, 3).forEach();
try {
await promise;
assert.ok(false);
} catch (err) {
assert.equal(err.name, 'TypeError');
}
});
it('rejects if the callback throws', async () => {
let error = {};
try {
await Observable.of(1, 2, 3).forEach(x => { throw error });
assert.ok(false);
} catch (err) {
assert.equal(err, error);
}
});
it('does not execute callback after callback throws', async () => {
let calls = [];
try {
await Observable.of(1, 2, 3).forEach(x => {
calls.push(x);
throw {};
});
assert.ok(false);
} catch (err) {
assert.deepEqual(calls, [1]);
}
});
it('rejects if the producer calls error', async () => {
let error = {};
try {
let observer;
let promise = new Observable(x => { observer = x }).forEach(() => {});
observer.error(error);
await promise;
assert.ok(false);
} catch (err) {
assert.equal(err, error);
}
});
it('resolves with undefined if the producer calls complete', async () => {
let observer;
let promise = new Observable(x => { observer = x }).forEach(() => {});
observer.complete();
assert.equal(await promise, undefined);
});
it('provides a cancellation function as the second argument', async () => {
let observer;
let results = [];
await Observable.of(1, 2, 3).forEach((value, cancel) => {
results.push(value);
if (value > 1) {
return cancel();
}
});
assert.deepEqual(results, [1, 2]);
});
});

View File

@@ -0,0 +1,95 @@
import assert from 'assert';
import { testMethodProperty } from './properties.js';
describe('from', () => {
const iterable = {
*[Symbol.iterator]() {
yield 1;
yield 2;
yield 3;
},
};
it('is a method on Observable', () => {
testMethodProperty(Observable, 'from', {
configurable: true,
writable: true,
length: 1,
});
});
it('throws if the argument is null', () => {
assert.throws(() => Observable.from(null));
});
it('throws if the argument is undefined', () => {
assert.throws(() => Observable.from(undefined));
});
it('throws if the argument is not observable or iterable', () => {
assert.throws(() => Observable.from({}));
});
describe('observables', () => {
it('returns the input if the constructor matches "this"', () => {
let ctor = function() {};
let observable = new Observable(() => {});
observable.constructor = ctor;
assert.equal(Observable.from.call(ctor, observable), observable);
});
it('wraps the input if it is not an instance of Observable', () => {
let obj = {
'constructor': Observable,
[Symbol.observable]() { return this },
};
assert.ok(Observable.from(obj) !== obj);
});
it('throws if @@observable property is not a method', () => {
assert.throws(() => Observable.from({
[Symbol.observable]: 1
}));
});
it('returns an observable wrapping @@observable result', () => {
let inner = {
subscribe(x) {
observer = x;
return () => { cleanupCalled = true };
},
};
let observer;
let cleanupCalled = true;
let observable = Observable.from({
[Symbol.observable]() { return inner },
});
observable.subscribe();
assert.equal(typeof observer.next, 'function');
observer.complete();
assert.equal(cleanupCalled, true);
});
});
describe('iterables', () => {
it('throws if @@iterator is not a method', () => {
assert.throws(() => Observable.from({ [Symbol.iterator]: 1 }));
});
it('returns an observable wrapping iterables', async () => {
let calls = [];
let subscription = Observable.from(iterable).subscribe({
next(v) { calls.push(['next', v]) },
complete() { calls.push(['complete']) },
});
assert.deepEqual(calls, []);
await null;
assert.deepEqual(calls, [
['next', 1],
['next', 2],
['next', 3],
['complete'],
]);
});
});
});

View File

@@ -0,0 +1,13 @@
import assert from 'assert';
describe('map', () => {
it('maps the results using the supplied callback', async () => {
let list = [];
await Observable.from([1, 2, 3])
.map(x => x * 2)
.forEach(x => list.push(x));
assert.deepEqual(list, [2, 4, 6]);
});
});

View File

@@ -0,0 +1,35 @@
import assert from 'assert';
import { testMethodProperty } from './properties.js';
describe('observer.closed', () => {
it('is a getter on SubscriptionObserver.prototype', () => {
let observer;
new Observable(x => { observer = x }).subscribe();
testMethodProperty(Object.getPrototypeOf(observer), 'closed', {
get: true,
configurable: true,
writable: true,
length: 1
});
});
it('returns false when the subscription is open', () => {
new Observable(observer => {
assert.equal(observer.closed, false);
}).subscribe();
});
it('returns true when the subscription is completed', () => {
let observer;
new Observable(x => { observer = x; }).subscribe();
observer.complete();
assert.equal(observer.closed, true);
});
it('returns true when the subscription is errored', () => {
let observer;
new Observable(x => { observer = x; }).subscribe(null, () => {});
observer.error();
assert.equal(observer.closed, true);
});
});

View File

@@ -0,0 +1,143 @@
import assert from 'assert';
import { testMethodProperty } from './properties.js';
describe('observer.complete', () => {
function getObserver(inner) {
let observer;
new Observable(x => { observer = x }).subscribe(inner);
return observer;
}
it('is a method of SubscriptionObserver', () => {
let observer = getObserver();
testMethodProperty(Object.getPrototypeOf(observer), 'complete', {
configurable: true,
writable: true,
length: 0,
});
});
it('does not forward arguments', () => {
let args;
let observer = getObserver({ complete(...a) { args = a } });
observer.complete(1);
assert.deepEqual(args, []);
});
it('does not return a value', () => {
let observer = getObserver({ complete() { return 1 } });
assert.equal(observer.complete(), undefined);
});
it('does not forward when the subscription is complete', () => {
let count = 0;
let observer = getObserver({ complete() { count++ } });
observer.complete();
observer.complete();
assert.equal(count, 1);
});
it('does not forward when the subscription is cancelled', () => {
let count = 0;
let observer;
let subscription = new Observable(x => { observer = x }).subscribe({
complete() { count++ },
});
subscription.unsubscribe();
observer.complete();
assert.equal(count, 0);
});
it('queues if the subscription is not initialized', async () => {
let completed = false;
new Observable(x => { x.complete() }).subscribe({
complete() { completed = true },
});
assert.equal(completed, false);
await null;
assert.equal(completed, true);
});
it('queues if the observer is running', async () => {
let observer;
let completed = false
new Observable(x => { observer = x }).subscribe({
next() { observer.complete() },
complete() { completed = true },
});
observer.next();
assert.equal(completed, false);
await null;
assert.equal(completed, true);
});
it('closes the subscription before invoking inner observer', () => {
let closed;
let observer = getObserver({
complete() { closed = observer.closed },
});
observer.complete();
assert.equal(closed, true);
});
it('reports error if "complete" is not a method', () => {
let observer = getObserver({ complete: 1 });
observer.complete();
assert.ok(hostError instanceof Error);
});
it('does not report error if "complete" is undefined', () => {
let observer = getObserver({ complete: undefined });
observer.complete();
assert.ok(!hostError);
});
it('does not report error if "complete" is null', () => {
let observer = getObserver({ complete: null });
observer.complete();
assert.ok(!hostError);
});
it('reports error if "complete" throws', () => {
let error = {};
let observer = getObserver({ complete() { throw error } });
observer.complete();
assert.equal(hostError, error);
});
it('calls the cleanup method after "complete"', () => {
let calls = [];
let observer;
new Observable(x => {
observer = x;
return () => { calls.push('cleanup') };
}).subscribe({
complete() { calls.push('complete') },
});
observer.complete();
assert.deepEqual(calls, ['complete', 'cleanup']);
});
it('calls the cleanup method if there is no "complete"', () => {
let calls = [];
let observer;
new Observable(x => {
observer = x;
return () => { calls.push('cleanup') };
}).subscribe({});
observer.complete();
assert.deepEqual(calls, ['cleanup']);
});
it('reports error if the cleanup function throws', () => {
let error = {};
let observer;
new Observable(x => {
observer = x;
return () => { throw error };
}).subscribe();
observer.complete();
assert.equal(hostError, error);
});
});

View File

@@ -0,0 +1,145 @@
import assert from 'assert';
import { testMethodProperty } from './properties.js';
describe('observer.error', () => {
function getObserver(inner) {
let observer;
new Observable(x => { observer = x }).subscribe(inner);
return observer;
}
it('is a method of SubscriptionObserver', () => {
let observer = getObserver();
testMethodProperty(Object.getPrototypeOf(observer), 'error', {
configurable: true,
writable: true,
length: 1,
});
});
it('forwards the argument', () => {
let args;
let observer = getObserver({ error(...a) { args = a } });
observer.error(1);
assert.deepEqual(args, [1]);
});
it('does not return a value', () => {
let observer = getObserver({ error() { return 1 } });
assert.equal(observer.error(), undefined);
});
it('does not throw when the subscription is complete', () => {
let observer = getObserver({ error() {} });
observer.complete();
observer.error('error');
});
it('does not throw when the subscription is cancelled', () => {
let observer;
let subscription = new Observable(x => { observer = x }).subscribe({
error() {},
});
subscription.unsubscribe();
observer.error(1);
assert.ok(!hostError);
});
it('queues if the subscription is not initialized', async () => {
let error;
new Observable(x => { x.error({}) }).subscribe({
error(err) { error = err },
});
assert.equal(error, undefined);
await null;
assert.ok(error);
});
it('queues if the observer is running', async () => {
let observer;
let error;
new Observable(x => { observer = x }).subscribe({
next() { observer.error({}) },
error(e) { error = e },
});
observer.next();
assert.ok(!error);
await null;
assert.ok(error);
});
it('closes the subscription before invoking inner observer', () => {
let closed;
let observer = getObserver({
error() { closed = observer.closed },
});
observer.error(1);
assert.equal(closed, true);
});
it('reports an error if "error" is not a method', () => {
let observer = getObserver({ error: 1 });
observer.error(1);
assert.ok(hostError);
});
it('reports an error if "error" is undefined', () => {
let error = {};
let observer = getObserver({ error: undefined });
observer.error(error);
assert.equal(hostError, error);
});
it('reports an error if "error" is null', () => {
let error = {};
let observer = getObserver({ error: null });
observer.error(error);
assert.equal(hostError, error);
});
it('reports error if "error" throws', () => {
let error = {};
let observer = getObserver({ error() { throw error } });
observer.error(1);
assert.equal(hostError, error);
});
it('calls the cleanup method after "error"', () => {
let calls = [];
let observer;
new Observable(x => {
observer = x;
return () => { calls.push('cleanup') };
}).subscribe({
error() { calls.push('error') },
});
observer.error();
assert.deepEqual(calls, ['error', 'cleanup']);
});
it('calls the cleanup method if there is no "error"', () => {
let calls = [];
let observer;
new Observable(x => {
observer = x;
return () => { calls.push('cleanup') };
}).subscribe({});
try {
observer.error();
} catch (err) {}
assert.deepEqual(calls, ['cleanup']);
});
it('reports error if the cleanup function throws', () => {
let error = {};
let observer;
new Observable(x => {
observer = x;
return () => { throw error };
}).subscribe();
observer.error(1);
assert.equal(hostError, error);
});
});

View File

@@ -0,0 +1,137 @@
import assert from 'assert';
import { testMethodProperty } from './properties.js';
describe('observer.next', () => {
function getObserver(inner) {
let observer;
new Observable(x => { observer = x }).subscribe(inner);
return observer;
}
it('is a method of SubscriptionObserver', () => {
let observer = getObserver();
testMethodProperty(Object.getPrototypeOf(observer), 'next', {
configurable: true,
writable: true,
length: 1,
});
});
it('forwards the first argument', () => {
let args;
let observer = getObserver({ next(...a) { args = a } });
observer.next(1, 2);
assert.deepEqual(args, [1]);
});
it('does not return a value', () => {
let observer = getObserver({ next() { return 1 } });
assert.equal(observer.next(), undefined);
});
it('does not forward when the subscription is complete', () => {
let count = 0;
let observer = getObserver({ next() { count++ } });
observer.complete();
observer.next();
assert.equal(count, 0);
});
it('does not forward when the subscription is cancelled', () => {
let count = 0;
let observer;
let subscription = new Observable(x => { observer = x }).subscribe({
next() { count++ },
});
subscription.unsubscribe();
observer.next();
assert.equal(count, 0);
});
it('remains closed if the subscription is cancelled from "next"', () => {
let observer;
let subscription = new Observable(x => { observer = x }).subscribe({
next() { subscription.unsubscribe() },
});
observer.next();
assert.equal(observer.closed, true);
});
it('queues if the subscription is not initialized', async () => {
let values = [];
let observer;
new Observable(x => { observer = x, x.next(1) }).subscribe({
next(val) {
values.push(val);
if (val === 1) {
observer.next(3);
}
},
});
observer.next(2);
assert.deepEqual(values, []);
await null;
assert.deepEqual(values, [1, 2]);
await null;
assert.deepEqual(values, [1, 2, 3]);
});
it('drops queue if subscription is closed', async () => {
let values = [];
let subscription = new Observable(x => { x.next(1) }).subscribe({
next(val) { values.push(val) },
});
assert.deepEqual(values, []);
subscription.unsubscribe();
await null;
assert.deepEqual(values, []);
});
it('queues if the observer is running', async () => {
let observer;
let values = [];
new Observable(x => { observer = x }).subscribe({
next(val) {
values.push(val);
if (val === 1) observer.next(2);
},
});
observer.next(1);
assert.deepEqual(values, [1]);
await null;
assert.deepEqual(values, [1, 2]);
});
it('reports error if "next" is not a method', () => {
let observer = getObserver({ next: 1 });
observer.next();
assert.ok(hostError);
});
it('does not report error if "next" is undefined', () => {
let observer = getObserver({ next: undefined });
observer.next();
assert.ok(!hostError);
});
it('does not report error if "next" is null', () => {
let observer = getObserver({ next: null });
observer.next();
assert.ok(!hostError);
});
it('reports error if "next" throws', () => {
let error = {};
let observer = getObserver({ next() { throw error } });
observer.next();
assert.equal(hostError, error);
});
it('does not close the subscription on error', () => {
let observer = getObserver({ next() { throw {} } });
observer.next();
assert.equal(observer.closed, false);
});
});

View File

@@ -0,0 +1,32 @@
import assert from 'assert';
import { testMethodProperty } from './properties.js';
describe('of', () => {
it('is a method on Observable', () => {
testMethodProperty(Observable, 'of', {
configurable: true,
writable: true,
length: 0,
});
});
it('uses the this value if it is a function', () => {
let usesThis = false;
Observable.of.call(function() { usesThis = true; });
assert.ok(usesThis);
});
it('uses Observable if the this value is not a function', () => {
let result = Observable.of.call({}, 1, 2, 3, 4);
assert.ok(result instanceof Observable);
});
it('delivers arguments to next in a job', async () => {
let values = [];
let turns = 0;
Observable.of(1, 2, 3, 4).subscribe(v => values.push(v));
assert.equal(values.length, 0);
await null;
assert.deepEqual(values, [1, 2, 3, 4]);
});
});

View File

@@ -0,0 +1,31 @@
import assert from 'assert';
export function testMethodProperty(object, key, options) {
let desc = Object.getOwnPropertyDescriptor(object, key);
let { enumerable = false, configurable = false, writable = false, length } = options;
assert.ok(desc, `Property ${ key.toString() } exists`);
if (options.get || options.set) {
if (options.get) {
assert.equal(typeof desc.get, 'function', 'Getter is a function');
assert.equal(desc.get.length, 0, 'Getter length is 0');
} else {
assert.equal(desc.get, undefined, 'Getter is undefined');
}
if (options.set) {
assert.equal(typeof desc.set, 'function', 'Setter is a function');
assert.equal(desc.set.length, 1, 'Setter length is 1');
} else {
assert.equal(desc.set, undefined, 'Setter is undefined');
}
} else {
assert.equal(typeof desc.value, 'function', 'Value is a function');
assert.equal(desc.value.length, length, `Function length is ${ length }`);
assert.equal(desc.writable, writable, `Writable property is correct ${ writable }`);
}
assert.equal(desc.enumerable, enumerable, `Enumerable property is ${ enumerable }`);
assert.equal(desc.configurable, configurable, `Configurable property is ${ configurable }`);
}

View File

@@ -0,0 +1,38 @@
import assert from 'assert';
describe('reduce', () => {
it('reduces without a seed', async () => {
await Observable.from([1, 2, 3, 4, 5, 6]).reduce((a, b) => {
return a + b;
}).forEach(x => {
assert.equal(x, 21);
});
});
it('errors if empty and no seed', async () => {
try {
await Observable.from([]).reduce((a, b) => {
return a + b;
}).forEach(() => null);
assert.ok(false);
} catch (err) {
assert.ok(true);
}
});
it('reduces with a seed', async () => {
Observable.from([1, 2, 3, 4, 5, 6]).reduce((a, b) => {
return a + b;
}, 100).forEach(x => {
assert.equal(x, 121);
});
});
it('reduces an empty list with a seed', async () => {
await Observable.from([]).reduce((a, b) => {
return a + b;
}, 100).forEach(x => {
assert.equal(x, 100);
});
});
});

View File

@@ -0,0 +1,9 @@
import { Observable } from '../src/Observable.js';
beforeEach(() => {
global.Observable = Observable;
global.hostError = null;
let $extensions = Object.getOwnPropertySymbols(Observable)[1];
let { hostReportError } = Observable[$extensions];
hostReportError.log = (e => global.hostError = e);
});

View File

@@ -0,0 +1,28 @@
import assert from 'assert';
describe('species', () => {
it('uses Observable when constructor is undefined', () => {
let instance = new Observable(() => {});
instance.constructor = undefined;
assert.ok(instance.map(x => x) instanceof Observable);
});
it('uses Observable if species is null', () => {
let instance = new Observable(() => {});
instance.constructor = { [Symbol.species]: null };
assert.ok(instance.map(x => x) instanceof Observable);
});
it('uses Observable if species is undefined', () => {
let instance = new Observable(() => {});
instance.constructor = { [Symbol.species]: undefined };
assert.ok(instance.map(x => x) instanceof Observable);
});
it('uses value of Symbol.species', () => {
function ctor() {}
let instance = new Observable(() => {});
instance.constructor = { [Symbol.species]: ctor };
assert.ok(instance.map(x => x) instanceof ctor);
});
});

View File

@@ -0,0 +1,137 @@
import assert from 'assert';
import { testMethodProperty } from './properties.js';
describe('subscribe', () => {
it('is a method of Observable.prototype', () => {
testMethodProperty(Observable.prototype, 'subscribe', {
configurable: true,
writable: true,
length: 1,
});
});
it('accepts an observer argument', () => {
let observer;
let nextValue;
new Observable(x => observer = x).subscribe({
next(v) { nextValue = v },
});
observer.next(1);
assert.equal(nextValue, 1);
});
it('accepts a next function argument', () => {
let observer;
let nextValue;
new Observable(x => observer = x).subscribe(
v => nextValue = v
);
observer.next(1);
assert.equal(nextValue, 1);
});
it('accepts an error function argument', () => {
let observer;
let errorValue;
let error = {};
new Observable(x => observer = x).subscribe(
null,
e => errorValue = e
);
observer.error(error);
assert.equal(errorValue, error);
});
it('accepts a complete function argument', () => {
let observer;
let completed = false;
new Observable(x => observer = x).subscribe(
null,
null,
() => completed = true
);
observer.complete();
assert.equal(completed, true);
});
it('uses function overload if first argument is null', () => {
let observer;
let completed = false;
new Observable(x => observer = x).subscribe(
null,
null,
() => completed = true
);
observer.complete();
assert.equal(completed, true);
});
it('uses function overload if first argument is undefined', () => {
let observer;
let completed = false;
new Observable(x => observer = x).subscribe(
undefined,
null,
() => completed = true
);
observer.complete();
assert.equal(completed, true);
});
it('uses function overload if first argument is a primative', () => {
let observer;
let completed = false;
new Observable(x => observer = x).subscribe(
'abc',
null,
() => completed = true
);
observer.complete();
assert.equal(completed, true);
});
it('enqueues a job to send error if subscriber throws', async () => {
let error = {};
let errorValue = undefined;
new Observable(() => { throw error }).subscribe({
error(e) { errorValue = e },
});
assert.equal(errorValue, undefined);
await null;
assert.equal(errorValue, error);
});
it('does not send error if unsubscribed', async () => {
let error = {};
let errorValue = undefined;
let subscription = new Observable(() => { throw error }).subscribe({
error(e) { errorValue = e },
});
subscription.unsubscribe();
assert.equal(errorValue, undefined);
await null;
assert.equal(errorValue, undefined);
});
it('accepts a cleanup function from the subscriber function', () => {
let cleanupCalled = false;
let subscription = new Observable(() => {
return () => cleanupCalled = true;
}).subscribe();
subscription.unsubscribe();
assert.equal(cleanupCalled, true);
});
it('accepts a subscription object from the subscriber function', () => {
let cleanupCalled = false;
let subscription = new Observable(() => {
return {
unsubscribe() { cleanupCalled = true },
};
}).subscribe();
subscription.unsubscribe();
assert.equal(cleanupCalled, true);
});
});

View File

@@ -0,0 +1,41 @@
import assert from 'assert';
import { testMethodProperty } from './properties.js';
describe('subscription', () => {
function getSubscription(subscriber = () => {}) {
return new Observable(subscriber).subscribe();
}
describe('unsubscribe', () => {
it('is a method on Subscription.prototype', () => {
let subscription = getSubscription();
testMethodProperty(Object.getPrototypeOf(subscription), 'unsubscribe', {
configurable: true,
writable: true,
length: 0,
});
});
it('reports an error if the cleanup function throws', () => {
let error = {};
let subscription = getSubscription(() => {
return () => { throw error };
});
subscription.unsubscribe();
assert.equal(hostError, error);
});
});
describe('closed', () => {
it('is a getter on Subscription.prototype', () => {
let subscription = getSubscription();
testMethodProperty(Object.getPrototypeOf(subscription), 'closed', {
configurable: true,
writable: true,
get: true,
});
});
});
});