From 9acf8e76f4ee3b5159eea2cf76034ad607e41889 Mon Sep 17 00:00:00 2001 From: Denis Pushkarev Date: Sat, 9 Dec 2017 00:17:03 +0700 Subject: [PATCH] update `Observable` by actual version of the spec, fixes #257, #276 https://github.com/tc39/proposal-observable --- .eslintrc.js | 1 - .travis.yml | 2 +- CHANGELOG.md | 1 + README.md | 15 +-- modules/_host-report-errors.js | 7 ++ modules/es.promise.js | 7 +- modules/esnext.observable.js | 171 +++++++++++++-------------- package.json | 6 +- tests/library/esnext.observable.js | 16 +-- tests/observables/adapter-library.js | 7 +- tests/observables/adapter.js | 4 +- tests/tests/esnext.observable.js | 16 +-- 12 files changed, 121 insertions(+), 132 deletions(-) create mode 100644 modules/_host-report-errors.js diff --git a/.eslintrc.js b/.eslintrc.js index e91b636af3f1..ef9c3d22a4ea 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -375,7 +375,6 @@ module.exports = { 'library/**', 'modules/**', 'stage/**', - 'tests/observables/**', 'tests/promises-aplus/**', 'web/**', ], diff --git a/.travis.yml b/.travis.yml index 68242c02deb8..285ea3570daa 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,5 @@ language: node_js node_js: - - "5.7" + - "9.2" sudo: false diff --git a/CHANGELOG.md b/CHANGELOG.md index c7b9bffc64a6..28e430441bb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ - `Reflect.enumerate` (removed from the spec) - Unnecessary iteration methods from `CSSRuleList`, `MediaList`, `StyleSheetList` - Updated proposals: + - [`Observable`](https://github.com/tc39/proposal-observable) ([#257](https://github.com/zloirock/core-js/issues/257), [#276](https://github.com/zloirock/core-js/issues/276), etc.) - `Array#flatten` and `Array#flatMap` updated and moved to the stage 3 - `Symbol.asyncIterator` moved to the stage 3 - ES2016 and ES2017 features marked as stable: diff --git a/README.md b/README.md index 41d94d6425f7..ce99eef98a44 100644 --- a/README.md +++ b/README.md @@ -1520,12 +1520,11 @@ for (let [_, d, D] of '1111a2b3cccc'.matchAll(/(\d)(\D)/)) { * `Observable` [proposal](https://github.com/zenparsing/es-observable) - modules [`esnext.observable`](https://github.com/zloirock/core-js/blob/v3/modules/esnext.observable.js) and [`esnext.symbol.observable`](https://github.com/zloirock/core-js/blob/v3/modules/esnext.symbol.observable.js) ```js class Observable { - constructor(fn: Function): Observable - subscribe(observer: Observer): Subscription; - forEach(fn: Function): Promise; + constructor(subscriber: Function): Observable; + subscribe(observer: Function | { next?: Function, error?: Function, complete?: Function }): Subscription; @@observable(): this; - static of(...items: Aray): Observable - static from(x: Observable | Iterable): Observable + static of(...items: Aray): Observable; + static from(x: Observable | Iterable): Observable; static get @@species: this; } @@ -1544,8 +1543,10 @@ new Observable(observer => { observer.next('hello'); observer.next('world'); observer.complete(); -}).forEach(it => console.log(it)) - .then(() => console.log('!')); +}).subscribe({ + next(it) { console.log(it); }, + complete() { console.log('!'); } +}); ``` * `Math.{clamp, DEG_PER_RAD, degrees, fscale, rad-per-deg, radians, scale}` [proposal](https://github.com/rwaldron/proposal-math-extensions) - modules diff --git a/modules/_host-report-errors.js b/modules/_host-report-errors.js new file mode 100644 index 000000000000..5b7e9eb6563a --- /dev/null +++ b/modules/_host-report-errors.js @@ -0,0 +1,7 @@ +var global = require('./_global'); +module.exports = function (a, b) { + var console = global.console; + if (console && console.error) { + arguments.length === 1 ? console.error(a) : console.error(a, b); + } +}; diff --git a/modules/es.promise.js b/modules/es.promise.js index 88bf65d4c881..bcc2513196f7 100644 --- a/modules/es.promise.js +++ b/modules/es.promise.js @@ -14,6 +14,7 @@ var microtask = require('./_microtask')(); var newPromiseCapabilityModule = require('./_new-promise-capability'); var perform = require('./_perform'); var promiseResolve = require('./_promise-resolve'); +var hostReportErrors = require('./_host-report-errors'); var PROMISE = 'Promise'; var TypeError = global.TypeError; var process = global.process; @@ -86,16 +87,14 @@ var onUnhandled = function (promise) { task.call(global, function () { var value = promise._v; var unhandled = isUnhandled(promise); - var result, handler, console; + var result, handler; if (unhandled) { result = perform(function () { if (isNode) { process.emit('unhandledRejection', value, promise); } else if (handler = global.onunhandledrejection) { handler({ promise: promise, reason: value }); - } else if ((console = global.console) && console.error) { - console.error('Unhandled promise rejection', value); - } + } else hostReportErrors('Unhandled promise rejection', value); }); // Browsers should not trigger `rejectionHandled` event if it was handled here, NodeJS - should promise._h = isNode || isUnhandled(promise) ? 2 : 1; diff --git a/modules/esnext.observable.js b/modules/esnext.observable.js index 6dcb2c8f2afa..e10dcef34ab5 100644 --- a/modules/esnext.observable.js +++ b/modules/esnext.observable.js @@ -1,16 +1,18 @@ 'use strict'; // https://github.com/zenparsing/es-observable var $export = require('./_export'); -var global = require('./_global'); -var core = require('./_core'); -var microtask = require('./_microtask')(); -var OBSERVABLE = require('./_wks')('observable'); var aFunction = require('./_a-function'); var anObject = require('./_an-object'); +var isObject = require('./_is-object'); var anInstance = require('./_an-instance'); var redefineAll = require('./_redefine-all'); var hide = require('./_hide'); +var getIterator = require('./core.get-iterator'); var forOf = require('./_for-of'); +var hostReportErrors = require('./_host-report-errors'); +var dP = require('./_object-dp').f; +var DESCRIPTORS = require('./_descriptors'); +var OBSERVABLE = require('./_wks')('observable'); var RETURN = forOf.RETURN; var getMethod = function (fn) { @@ -21,7 +23,11 @@ var cleanupSubscription = function (subscription) { var cleanup = subscription._c; if (cleanup) { subscription._c = undefined; - cleanup(); + try { + cleanup(); + } catch (e) { + hostReportErrors(e); + } } }; @@ -31,26 +37,39 @@ var subscriptionClosed = function (subscription) { var closeSubscription = function (subscription) { if (!subscriptionClosed(subscription)) { - subscription._o = undefined; + close(subscription); cleanupSubscription(subscription); } }; +var close = function (subscription) { + if (!DESCRIPTORS) { + subscription.closed = true; + var subscriptionObserver = subscription._s; + if (subscriptionObserver) subscriptionObserver.closed = true; + } subscription._o = undefined; +}; + var Subscription = function (observer, subscriber) { - anObject(observer); + var start; + if (!DESCRIPTORS) this.closed = false; this._c = undefined; - this._o = observer; - observer = new SubscriptionObserver(this); + this._o = anObject(observer); try { - var cleanup = subscriber(observer); + if (start = getMethod(observer.start)) start.call(observer, this); + } catch (e) { + hostReportErrors(e); + } + if (subscriptionClosed(this)) return; + var subscriptionObserver = this._s = new SubscriptionObserver(this); + try { + var cleanup = subscriber(subscriptionObserver); var subscription = cleanup; - if (cleanup != null) { - if (typeof cleanup.unsubscribe === 'function') cleanup = function () { subscription.unsubscribe(); }; - else aFunction(cleanup); - this._c = cleanup; - } + if (cleanup != null) this._c = typeof cleanup.unsubscribe === 'function' + ? function () { subscription.unsubscribe(); } + : aFunction(cleanup); } catch (e) { - observer.error(e); + subscriptionObserver.error(e); return; } if (subscriptionClosed(this)) cleanupSubscription(this); }; @@ -59,8 +78,16 @@ Subscription.prototype = redefineAll({}, { unsubscribe: function unsubscribe() { closeSubscription(this); } }); +if (DESCRIPTORS) dP(Subscription.prototype, 'closed', { + configurable: true, + get: function () { + return subscriptionClosed(this); + } +}); + var SubscriptionObserver = function (subscription) { this._s = subscription; + if (!DESCRIPTORS) this.closed = false; }; SubscriptionObserver.prototype = redefineAll({}, { @@ -70,79 +97,59 @@ SubscriptionObserver.prototype = redefineAll({}, { var observer = subscription._o; try { var m = getMethod(observer.next); - if (m) return m.call(observer, value); + if (m) m.call(observer, value); } catch (e) { - try { - closeSubscription(subscription); - } finally { - throw e; - } + hostReportErrors(e); } } }, error: function error(value) { var subscription = this._s; - if (subscriptionClosed(subscription)) throw value; - var observer = subscription._o; - subscription._o = undefined; - try { - var m = getMethod(observer.error); - if (!m) throw value; - value = m.call(observer, value); - } catch (e) { + if (!subscriptionClosed(subscription)) { + var observer = subscription._o; + close(subscription); try { - cleanupSubscription(subscription); - } finally { - throw e; - } - } cleanupSubscription(subscription); - return value; + var m = getMethod(observer.error); + if (m) m.call(observer, value); + else hostReportErrors(value); + } catch (e) { + hostReportErrors(e); + } cleanupSubscription(subscription); + } }, - complete: function complete(value) { + complete: function complete() { var subscription = this._s; if (!subscriptionClosed(subscription)) { var observer = subscription._o; - subscription._o = undefined; + close(subscription); try { var m = getMethod(observer.complete); - value = m ? m.call(observer, value) : undefined; + if (m) m.call(observer); } catch (e) { - try { - cleanupSubscription(subscription); - } finally { - throw e; - } + hostReportErrors(e); } cleanupSubscription(subscription); - return value; } } }); +if (DESCRIPTORS) dP(SubscriptionObserver.prototype, 'closed', { + configurable: true, + get: function () { + return subscriptionClosed(this._s); + } +}); + var $Observable = function Observable(subscriber) { anInstance(this, $Observable, 'Observable', '_f')._f = aFunction(subscriber); }; redefineAll($Observable.prototype, { subscribe: function subscribe(observer) { - return new Subscription(observer, this._f); - }, - forEach: function forEach(fn) { - var that = this; - return new (core.Promise || global.Promise)(function (resolve, reject) { - aFunction(fn); - var subscription = that.subscribe({ - next: function (value) { - try { - return fn(value); - } catch (e) { - reject(e); - subscription.unsubscribe(); - } - }, - error: reject, - complete: resolve - }); - }); + return new Subscription(typeof observer === 'function' ? { + next: observer, + error: arguments.length > 1 ? arguments[1] : undefined, + complete: arguments.length > 2 ? arguments[2] : undefined + } : isObject(observer) ? observer : {}, this._f); } }); @@ -156,38 +163,22 @@ redefineAll($Observable, { return observable.subscribe(observer); }); } + var iterator = getIterator(x); return new C(function (observer) { - var done = false; - microtask(function () { - if (!done) { - try { - if (forOf(x, false, function (it) { - observer.next(it); - if (done) return RETURN; - }) === RETURN) return; - } catch (e) { - if (done) throw e; - observer.error(e); - return; - } observer.complete(); - } - }); - return function () { done = true; }; + forOf(iterator, false, function (it) { + observer.next(it); + if (observer.closed) return RETURN; + }, undefined, true); + observer.complete(); }); }, of: function of() { for (var i = 0, l = arguments.length, items = new Array(l); i < l;) items[i] = arguments[i++]; return new (typeof this === 'function' ? this : $Observable)(function (observer) { - var done = false; - microtask(function () { - if (!done) { - for (var j = 0; j < items.length; ++j) { - observer.next(items[j]); - if (done) return; - } observer.complete(); - } - }); - return function () { done = true; }; + for (var j = 0; j < items.length; ++j) { + observer.next(items[j]); + if (observer.closed) return; + } observer.complete(); }); } }); diff --git a/package.json b/package.json index c8905bab21ca..180894459079 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ }, "main": "index.js", "devDependencies": { + "babel-cli": "^6.26.0", "babel-core": "^6.26.0", "babel-loader": "^7.1.2", "babel-plugin-check-es2015-constants": "^6.22.0", @@ -27,7 +28,7 @@ "babel-plugin-transform-es3-property-literals": "^6.22.0", "babel-plugin-transform-exponentiation-operator": "^6.24.1", "babel-plugin-transform-for-of-as-array": "^1.0.4", - "es-observable-tests": "0.2.x", + "es-observable": "git+https://github.com/tc39/proposal-observable.git#bf4d87144b6189e793593868e3c022eb51a7d292", "eslint": "4.13.x", "eslint-plugin-import": "2.8.x", "grunt": "^1.0.1", @@ -42,6 +43,7 @@ "karma-qunit": "1.2.x", "karma-phantomjs-launcher": "1.0.x", "mkdirp": "^0.5.1", + "moon-unit": "^0.2.2", "phantomjs-prebuilt": "2.1.x", "promises-aplus-tests": "^2.1.2", "qunitjs": "2.4.x", @@ -53,7 +55,7 @@ "lint": "eslint ./", "promises-tests": "promises-aplus-tests tests/promises-aplus/adapter", "bundle-promises-tests": "npm run grunt webpack:promises-aplus-tests", - "observables-tests": "node tests/observables/adapter && node tests/observables/adapter-library", + "observables-tests": "babel node_modules/es-observable/test/ -d tests/bundles/observables-tests/ --plugins=transform-es2015-modules-commonjs && node tests/observables/adapter && node tests/observables/adapter-library", "test": "npm run grunt clean copy && npm run lint && npm run grunt webpack:helpers webpack:tests client karma:default && npm run grunt webpack:library library karma:library && npm run promises-tests && npm run observables-tests && node tests/commonjs" }, "license": "MIT", diff --git a/tests/library/esnext.observable.js b/tests/library/esnext.observable.js index 53afa32a0398..32f80c6bd9db 100644 --- a/tests/library/esnext.observable.js +++ b/tests/library/esnext.observable.js @@ -1,6 +1,6 @@ import { STRICT } from '../helpers/constants'; -const { Observable, Promise, Symbol } = core; +const { Observable, Symbol } = core; QUnit.test('Observable', assert => { assert.isFunction(Observable); @@ -8,7 +8,7 @@ QUnit.test('Observable', assert => { assert.throws(() => { Observable(() => { /* empty */ }); }, 'throws w/o `new`'); - const obsevable = new Observable(function (subscriptionObserver) { + const observable = new Observable(function (subscriptionObserver) { assert.same(typeof subscriptionObserver, 'object', 'Subscription observer is object'); assert.same(subscriptionObserver.constructor, Object); const { next, error, complete } = subscriptionObserver; @@ -17,13 +17,13 @@ QUnit.test('Observable', assert => { assert.isFunction(complete); assert.arity(next, 1); assert.arity(error, 1); - assert.arity(complete, 1); + assert.arity(complete, 0); if (STRICT) { assert.same(this, undefined, 'correct executor context'); } }); - obsevable.subscribe({}); - assert.ok(obsevable instanceof Observable); + observable.subscribe({}); + assert.ok(observable instanceof Observable); }); QUnit.test('Observable#subscribe', assert => { @@ -36,12 +36,6 @@ QUnit.test('Observable#subscribe', assert => { assert.arity(subscription.unsubscribe, 0); }); -QUnit.test('Observable#forEach', assert => { - assert.isFunction(Observable.prototype.forEach); - assert.arity(Observable.prototype.forEach, 1); - assert.ok(new Observable(() => { /* empty */ }).forEach(() => { /* empty */ }) instanceof Promise, 'returns Promise'); -}); - QUnit.test('Observable#constructor', assert => { assert.same(Observable.prototype.constructor, Observable); }); diff --git a/tests/observables/adapter-library.js b/tests/observables/adapter-library.js index e8e53b9413fc..c67d91cb0f9c 100644 --- a/tests/observables/adapter-library.js +++ b/tests/observables/adapter-library.js @@ -1,5 +1,6 @@ -delete global.Observable; -var core = require('../../library'); +'use strict'; +const core = require('../../library'); global.Promise = core.Promise; global.Symbol = core.Symbol; -require('es-observable-tests').runTests(core.Observable); +// eslint-disable-next-line import/no-unresolved +require('../bundles/observables-tests/default').runTests(core.Observable); diff --git a/tests/observables/adapter.js b/tests/observables/adapter.js index f1ff03ac9f18..f88fe9684bfb 100644 --- a/tests/observables/adapter.js +++ b/tests/observables/adapter.js @@ -1,3 +1,5 @@ +'use strict'; delete global.Observable; require('../../'); -require('es-observable-tests').runTests(global.Observable); +// eslint-disable-next-line import/no-unresolved +require('../bundles/observables-tests/default').runTests(global.Observable); diff --git a/tests/tests/esnext.observable.js b/tests/tests/esnext.observable.js index f308609281ac..507ced30fadd 100644 --- a/tests/tests/esnext.observable.js +++ b/tests/tests/esnext.observable.js @@ -8,7 +8,7 @@ QUnit.test('Observable', assert => { assert.throws(() => { Observable(() => { /* empty */ }); }, 'throws w/o `new`'); - const obsevable = new Observable(function (subscriptionObserver) { + const observable = new Observable(function (subscriptionObserver) { assert.same(typeof subscriptionObserver, 'object', 'Subscription observer is object'); assert.same(subscriptionObserver.constructor, Object); const { next, error, complete } = subscriptionObserver; @@ -17,13 +17,13 @@ QUnit.test('Observable', assert => { assert.isFunction(complete); assert.arity(next, 1); assert.arity(error, 1); - assert.arity(complete, 1); + assert.arity(complete, 0); if (STRICT) { assert.same(this, undefined, 'correct executor context'); } }); - obsevable.subscribe({}); - assert.ok(obsevable instanceof Observable); + observable.subscribe({}); + assert.ok(observable instanceof Observable); }); QUnit.test('Observable#subscribe', assert => { @@ -38,14 +38,6 @@ QUnit.test('Observable#subscribe', assert => { assert.arity(subscription.unsubscribe, 0); }); -QUnit.test('Observable#forEach', assert => { - assert.isFunction(Observable.prototype.forEach); - assert.arity(Observable.prototype.forEach, 1); - assert.name(Observable.prototype.forEach, 'forEach'); - assert.looksNative(Observable.prototype.forEach); - assert.ok(new Observable(() => { /* empty */ }).forEach(() => { /* empty */ }) instanceof Promise, 'returns Promise'); -}); - QUnit.test('Observable#constructor', assert => { assert.same(Observable.prototype.constructor, Observable); });