├── .editorconfig ├── .gitignore ├── .npmignore ├── LICENSE.md ├── README.md ├── dist └── lib │ ├── fetchObservable.js │ └── fetchObservable.min.js ├── package.json ├── src ├── example.js └── lib │ ├── BetterObservable.js │ ├── Observable.js │ ├── PausableObservable.js │ └── fetchObservable.js ├── webpack.client-watch.js ├── webpack.client.js └── webpack.umd.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig: http://EditorConfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | end_of_line = crlf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | indent_style = tab 11 | tab_width = 4 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | node_modules/ 3 | dist/* 4 | !dist/lib 5 | *.log 6 | *.map 7 | .DS_Store 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | node_modules/ 3 | dist/* 4 | !dist/lib 5 | *.log 6 | *.map 7 | .DS_Store 8 | 9 | src/example/ 10 | src/example.* 11 | tmp/ 12 | webpack.*.js 13 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # BSD 3-Clause License 2 | 3 | Copyright © 2015, Rick Wong 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright 10 | notice, this list of conditions and the following disclaimer. 11 | 2. Redistributions in binary form must reproduce the above copyright 12 | notice, this list of conditions and the following disclaimer in the 13 | documentation and/or other materials provided with the distribution. 14 | 3. Neither the name of the copyright holder nor the 15 | names of its contributors may be used to endorse or promote products 16 | derived from this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY 22 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 25 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fetchObservable() 2 | 3 | Observable-based [Fetch API](https://github.com/whatwg/fetch) that automatically refreshes data and notifies subscribers. 4 | 5 | ## Features 6 | 7 | - Uses the standard Fetch API. 8 | - Uses Observable syntax from [ES Observable proposal](https://github.com/zenparsing/es-observable). 9 | - Runs in Node and browsers. (BYO Fetch API and Promises polyfills though) 10 | 11 | ## Installation 12 | 13 | ```bash 14 | npm install --save fetch-observable 15 | ``` 16 | 17 | ## Usage 18 | 19 | ````js 20 | import fetchObservable from "fetch-observable"; 21 | 22 | // Creates a single observable for one or multiple URLs. 23 | const liveFeed = fetchObservable( 24 | "http://example.org/live-feed.json", // <-- URL or array of URLs. 25 | { 26 | fetch: fetch, // <-- Replacable fetch implementation. 27 | refreshDelay: (iteration) => iteration * 1000, // <-- Callback or just integer ms. 28 | method: "POST" // <-- Basic Fetch API options. 29 | } 30 | ).map((response) => response.json()); // map() resolves Promises. 31 | 32 | // Subscribe-syntax of ES Observables activates the observable. 33 | const subscription1 = liveFeed.subscribe({ 34 | next (response) { 35 | console.dir(response.json()); 36 | }, 37 | error (error) { 38 | console.warn(error.stack || error); 39 | } 40 | }); 41 | 42 | // Multiple subscriptions allowed. They all get the result. 43 | const subscription2 = liveFeed.subscribe({next () {}}); 44 | 45 | // Observable can be paused and resumed manually. 46 | liveFeed.pause(); 47 | liveFeed.resume(); 48 | 49 | // Observable will be paused automatically when no subscriptions left. 50 | subscription1.unsubscribe(); 51 | subscription2.unsubscribe(); 52 | 53 | ```` 54 | 55 | ## Community 56 | 57 | Let's start one together! After you ★Star this project, follow me [@Rygu](https://twitter.com/rygu) 58 | on Twitter. 59 | 60 | ## License 61 | 62 | BSD 3-Clause license. Copyright © 2015, Rick Wong. All rights reserved. 63 | -------------------------------------------------------------------------------- /dist/lib/fetchObservable.js: -------------------------------------------------------------------------------- 1 | (function webpackUniversalModuleDefinition(root, factory) { 2 | if(typeof exports === 'object' && typeof module === 'object') 3 | module.exports = factory(); 4 | else if(typeof define === 'function' && define.amd) 5 | define([], factory); 6 | else if(typeof exports === 'object') 7 | exports["fetchObservable"] = factory(); 8 | else 9 | root["fetchObservable"] = factory(); 10 | })(this, function() { 11 | return /******/ (function(modules) { // webpackBootstrap 12 | /******/ // The module cache 13 | /******/ var installedModules = {}; 14 | 15 | /******/ // The require function 16 | /******/ function __webpack_require__(moduleId) { 17 | 18 | /******/ // Check if module is in cache 19 | /******/ if(installedModules[moduleId]) 20 | /******/ return installedModules[moduleId].exports; 21 | 22 | /******/ // Create a new module (and put it into the cache) 23 | /******/ var module = installedModules[moduleId] = { 24 | /******/ exports: {}, 25 | /******/ id: moduleId, 26 | /******/ loaded: false 27 | /******/ }; 28 | 29 | /******/ // Execute the module function 30 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 31 | 32 | /******/ // Flag the module as loaded 33 | /******/ module.loaded = true; 34 | 35 | /******/ // Return the exports of the module 36 | /******/ return module.exports; 37 | /******/ } 38 | 39 | 40 | /******/ // expose the modules object (__webpack_modules__) 41 | /******/ __webpack_require__.m = modules; 42 | 43 | /******/ // expose the module cache 44 | /******/ __webpack_require__.c = installedModules; 45 | 46 | /******/ // __webpack_public_path__ 47 | /******/ __webpack_require__.p = ""; 48 | 49 | /******/ // Load entry module and return exports 50 | /******/ return __webpack_require__(0); 51 | /******/ }) 52 | /************************************************************************/ 53 | /******/ ([ 54 | /* 0 */ 55 | /***/ function(module, exports, __webpack_require__) { 56 | 57 | "use strict"; 58 | 59 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; /** 60 | * @copyright © 2015, Rick Wong. All rights reserved. 61 | */ 62 | 63 | var _PausableObservable = __webpack_require__(3); 64 | 65 | var _PausableObservable2 = _interopRequireDefault(_PausableObservable); 66 | 67 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 68 | 69 | function isString(thing) { 70 | return typeof thing === "string"; 71 | } 72 | 73 | function isFunction(thing) { 74 | return typeof thing === "function"; 75 | } 76 | 77 | /** 78 | * Calls the Fetch API and returns an Observable. 79 | * 80 | * @param {string|string[]} urls URL or URLs array. 81 | * @param {object} options 82 | * @returns {PausableObservable|Observable} 83 | */ 84 | function fetchObservable(urls) { 85 | var options = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; 86 | var _options$refreshDelay = options.refreshDelay; 87 | var refreshDelay = _options$refreshDelay === undefined ? false : _options$refreshDelay; 88 | 89 | var fetchFunc = options.fetch || fetch; 90 | 91 | var observers = []; 92 | var timeout = null; 93 | var singleResult = false; 94 | var iteration = 0; 95 | 96 | if (singleResult = isString(urls)) { 97 | urls = [urls]; 98 | } 99 | 100 | var performFetch = function performFetch() { 101 | // Don't do anything if there are no observers. 102 | if (observers.length === 0 || observable.paused()) { 103 | return; 104 | } 105 | 106 | var _finally = function _finally() { 107 | // If refreshing is disabled, complete observers and pause observable. 108 | if (!refreshDelay) { 109 | observable.pause(); 110 | observers.map(function (observer) { 111 | return observer.complete(); 112 | }); 113 | observers = []; 114 | } 115 | // If refreshing is enabled, set a timeout. 116 | else { 117 | timeout = setTimeout(performFetch, isFunction(refreshDelay) ? refreshDelay(iteration++) : refreshDelay); 118 | } 119 | }; 120 | 121 | // Map all URLs to Fetch API calls. 122 | var fetches = urls.map(function (url) { 123 | return fetchFunc(url, _extends({}, options, { refreshDelay: undefined, fetch: undefined })); 124 | }); 125 | 126 | // Wait for all the results to come in, then notify observers. 127 | Promise.all(fetches).then(function (results) { 128 | observers.map(function (observer) { 129 | return observer.next(singleResult ? results[0] : results); 130 | }); 131 | _finally(); 132 | }).catch(function (error) { 133 | observers.map(function (observer) { 134 | return observer.error(error); 135 | }); 136 | _finally(); 137 | }); 138 | }; 139 | 140 | var observable = new _PausableObservable2.default(function (observer) { 141 | observers.push(observer); 142 | observable.resume(); 143 | 144 | return function () { 145 | observers.splice(observers.indexOf(observer), 1); 146 | 147 | if (!observers.length) { 148 | observable.pause(); 149 | } 150 | }; 151 | }, { 152 | onPause: function onPause() { 153 | if (timeout) { 154 | clearTimeout(timeout); 155 | timeout = null; 156 | } 157 | }, 158 | onResume: function onResume() { 159 | if (!timeout) { 160 | performFetch(); 161 | } 162 | } 163 | }); 164 | 165 | return observable; 166 | } 167 | 168 | module.exports = fetchObservable; 169 | 170 | /***/ }, 171 | /* 1 */ 172 | /***/ function(module, exports, __webpack_require__) { 173 | 174 | "use strict"; 175 | 176 | var _createClass = (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); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); 177 | 178 | var _Observable2 = __webpack_require__(2); 179 | 180 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 181 | 182 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 183 | 184 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 185 | 186 | function _typeof(obj) { return obj && typeof Symbol !== "undefined" && obj.constructor === Symbol ? "symbol" : typeof obj; } /** 187 | * @copyright © 2015, Rick Wong. All rights reserved. 188 | */ 189 | 190 | function isPromise(thing) { 191 | return (typeof thing === "undefined" ? "undefined" : _typeof(thing)) === "object" && typeof thing["then"] === "function" && typeof thing["catch"] === "function"; 192 | } 193 | 194 | var BetterObservable = (function (_Observable) { 195 | _inherits(BetterObservable, _Observable); 196 | 197 | function BetterObservable() { 198 | _classCallCheck(this, BetterObservable); 199 | 200 | return _possibleConstructorReturn(this, Object.getPrototypeOf(BetterObservable).apply(this, arguments)); 201 | } 202 | 203 | _createClass(BetterObservable, [{ 204 | key: "map", 205 | value: function map(callback) { 206 | var _this2 = this; 207 | 208 | if (typeof callback !== "function") { 209 | throw new TypeError(callback + " is not a function"); 210 | } 211 | 212 | var parentSubscription = null; 213 | var childObservers = []; 214 | 215 | var createParentSubscription = function createParentSubscription() { 216 | if (parentSubscription) { 217 | return; 218 | } 219 | 220 | parentSubscription = _this2.subscribe({ 221 | next: function next(value) { 222 | try { 223 | value = callback(value); 224 | } catch (e) { 225 | return childObservers.map(function (o) { 226 | return o.error(e); 227 | }); 228 | } 229 | 230 | // Support Promises. 231 | if (isPromise(value)) { 232 | return value.then(function (v) { 233 | return childObservers.map(function (o) { 234 | return o.next(v); 235 | }); 236 | }).catch(function (e) { 237 | return childObservers.map(function (o) { 238 | return o.error(e); 239 | }); 240 | }); 241 | } 242 | 243 | childObservers.map(function (o) { 244 | return o.next(value); 245 | }); 246 | }, 247 | 248 | error: function error(e) { 249 | return childObservers.map(function (o) { 250 | return o.error(e); 251 | }); 252 | }, 253 | complete: function complete() { 254 | return childObservers.map(function (o) { 255 | return o.complete(); 256 | }); 257 | } 258 | }); 259 | }; 260 | 261 | var destroyParentSubscription = function destroyParentSubscription() { 262 | parentSubscription && parentSubscription.unsubscribe(); 263 | parentSubscription = null; 264 | }; 265 | 266 | return new this.constructor(function (observer) { 267 | childObservers.push(observer); 268 | createParentSubscription(); 269 | 270 | return function () { 271 | childObservers.splice(childObservers.indexOf(observer), 1); 272 | 273 | if (!childObservers.length) { 274 | destroyParentSubscription(); 275 | } 276 | }; 277 | }); 278 | } 279 | }]); 280 | 281 | return BetterObservable; 282 | })(_Observable2.Observable); 283 | 284 | module.exports = BetterObservable; 285 | 286 | /***/ }, 287 | /* 2 */ 288 | /***/ function(module, exports, __webpack_require__) { 289 | 290 | /* WEBPACK VAR INJECTION */(function(global, process) {"use strict"; 291 | 292 | Object.defineProperty(exports, "__esModule", { 293 | value: true 294 | }); 295 | exports.Observable = Observable; 296 | 297 | function _typeof(obj) { return obj && typeof Symbol !== "undefined" && obj.constructor === Symbol ? "symbol" : typeof obj; } 298 | 299 | /** 300 | * Observable.js from zenparsing/zen-observable 301 | * 302 | * @copyright © zenparsing 303 | * @homepage https://github.com/zenparsing/zen-observable 304 | * @file https://github.com/zenparsing/zen-observable/blob/de80d63fb166421226bb3c918b111cac40bd672a/src/Observable.js 305 | */ 306 | // === Non-Promise Job Queueing === 307 | 308 | var enqueueJob = (function () { 309 | 310 | // Node 311 | if (typeof global !== "undefined" && typeof process !== "undefined" && process.nextTick) { 312 | 313 | return global.setImmediate ? function (fn) { 314 | global.setImmediate(fn); 315 | } : function (fn) { 316 | process.nextTick(fn); 317 | }; 318 | } 319 | 320 | // Newish Browsers 321 | var Observer = self.MutationObserver || self.WebKitMutationObserver; 322 | 323 | if (Observer) { 324 | var _ret = (function () { 325 | 326 | var div = document.createElement("div"), 327 | twiddle = function twiddle(_) { 328 | return div.classList.toggle("x"); 329 | }, 330 | queue = []; 331 | 332 | var observer = new Observer(function (_) { 333 | 334 | if (queue.length > 1) twiddle(); 335 | 336 | while (queue.length > 0) { 337 | queue.shift()(); 338 | } 339 | }); 340 | 341 | observer.observe(div, { attributes: true }); 342 | 343 | return { 344 | v: function v(fn) { 345 | 346 | queue.push(fn); 347 | 348 | if (queue.length === 1) twiddle(); 349 | } 350 | }; 351 | })(); 352 | 353 | if ((typeof _ret === "undefined" ? "undefined" : _typeof(_ret)) === "object") return _ret.v; 354 | } 355 | 356 | // Fallback 357 | return function (fn) { 358 | setTimeout(fn, 0); 359 | }; 360 | })(); 361 | 362 | // === Symbol Polyfills === 363 | 364 | function polyfillSymbol(name) { 365 | 366 | if (symbolsSupported() && !Symbol[name]) Object.defineProperty(Symbol, name, { value: Symbol(name) }); 367 | } 368 | 369 | function symbolsSupported() { 370 | 371 | return typeof Symbol === "function"; 372 | } 373 | 374 | function hasSymbol(name) { 375 | 376 | return symbolsSupported() && Boolean(Symbol[name]); 377 | } 378 | 379 | function getSymbol(name) { 380 | 381 | return hasSymbol(name) ? Symbol[name] : "@@" + name; 382 | } 383 | 384 | polyfillSymbol("observable"); 385 | 386 | // === Abstract Operations === 387 | 388 | function getMethod(obj, key) { 389 | 390 | var value = obj[key]; 391 | 392 | if (value == null) return undefined; 393 | 394 | if (typeof value !== "function") throw new TypeError(value + " is not a function"); 395 | 396 | return value; 397 | } 398 | 399 | function getSpecies(ctor) { 400 | 401 | var symbol = getSymbol("species"); 402 | return symbol ? ctor[symbol] : ctor; 403 | } 404 | 405 | function addMethods(target, methods) { 406 | 407 | Object.keys(methods).forEach(function (k) { 408 | 409 | var desc = Object.getOwnPropertyDescriptor(methods, k); 410 | desc.enumerable = false; 411 | Object.defineProperty(target, k, desc); 412 | }); 413 | } 414 | 415 | function cleanupSubscription(observer) { 416 | 417 | // Assert: observer._observer is undefined 418 | 419 | var cleanup = observer._cleanup; 420 | 421 | if (!cleanup) return; 422 | 423 | // Drop the reference to the cleanup function so that we won't call it 424 | // more than once 425 | observer._cleanup = undefined; 426 | 427 | // Call the cleanup function 428 | cleanup(); 429 | } 430 | 431 | function subscriptionClosed(observer) { 432 | 433 | return observer._observer === undefined; 434 | } 435 | 436 | function closeSubscription(observer) { 437 | 438 | if (subscriptionClosed(observer)) return; 439 | 440 | observer._observer = undefined; 441 | cleanupSubscription(observer); 442 | } 443 | 444 | function cleanupFromSubscription(subscription) { 445 | // TODO: Should we get the method out and apply it here, instead of 446 | // looking up the method at call time? 447 | return function (_) { 448 | subscription.unsubscribe(); 449 | }; 450 | } 451 | 452 | function createSubscription(observer, subscriber) { 453 | 454 | // Assert: subscriber is callable 455 | 456 | // The observer must be an object 457 | if (Object(observer) !== observer) throw new TypeError("Observer must be an object"); 458 | 459 | // TODO: Should we check for a "next" method here? 460 | 461 | var subscriptionObserver = new SubscriptionObserver(observer), 462 | subscription = new Subscription(subscriptionObserver), 463 | start = getMethod(observer, "start"); 464 | 465 | if (start) start.call(observer, subscription); 466 | 467 | if (subscriptionClosed(subscriptionObserver)) return subscription; 468 | 469 | try { 470 | 471 | // Call the subscriber function 472 | var cleanup = subscriber.call(undefined, subscriptionObserver); 473 | 474 | // The return value must be undefined, null, a subscription object, or a function 475 | if (cleanup != null) { 476 | 477 | if (typeof cleanup.unsubscribe === "function") cleanup = cleanupFromSubscription(cleanup);else if (typeof cleanup !== "function") throw new TypeError(cleanup + " is not a function"); 478 | 479 | subscriptionObserver._cleanup = cleanup; 480 | } 481 | } catch (e) { 482 | 483 | // If an error occurs during startup, then attempt to send the error 484 | // to the observer 485 | subscriptionObserver.error(e); 486 | return subscription; 487 | } 488 | 489 | // If the stream is already finished, then perform cleanup 490 | if (subscriptionClosed(subscriptionObserver)) cleanupSubscription(subscriptionObserver); 491 | 492 | return subscription; 493 | } 494 | 495 | function SubscriptionObserver(observer) { 496 | 497 | this._observer = observer; 498 | this._cleanup = undefined; 499 | } 500 | 501 | addMethods(SubscriptionObserver.prototype = {}, { 502 | 503 | get closed() { 504 | return subscriptionClosed(this); 505 | }, 506 | 507 | next: function next(value) { 508 | 509 | // If the stream if closed, then return undefined 510 | if (subscriptionClosed(this)) return undefined; 511 | 512 | var observer = this._observer; 513 | 514 | try { 515 | 516 | var m = getMethod(observer, "next"); 517 | 518 | // If the observer doesn't support "next", then return undefined 519 | if (!m) return undefined; 520 | 521 | // Send the next value to the sink 522 | return m.call(observer, value); 523 | } catch (e) { 524 | 525 | // If the observer throws, then close the stream and rethrow the error 526 | try { 527 | closeSubscription(this); 528 | } finally { 529 | throw e; 530 | } 531 | } 532 | }, 533 | error: function error(value) { 534 | 535 | // If the stream is closed, throw the error to the caller 536 | if (subscriptionClosed(this)) throw value; 537 | 538 | var observer = this._observer; 539 | this._observer = undefined; 540 | 541 | try { 542 | 543 | var m = getMethod(observer, "error"); 544 | 545 | // If the sink does not support "error", then throw the error to the caller 546 | if (!m) throw value; 547 | 548 | value = m.call(observer, value); 549 | } catch (e) { 550 | 551 | try { 552 | cleanupSubscription(this); 553 | } finally { 554 | throw e; 555 | } 556 | } 557 | 558 | cleanupSubscription(this); 559 | 560 | return value; 561 | }, 562 | complete: function complete(value) { 563 | 564 | // If the stream is closed, then return undefined 565 | if (subscriptionClosed(this)) return undefined; 566 | 567 | var observer = this._observer; 568 | this._observer = undefined; 569 | 570 | try { 571 | 572 | var m = getMethod(observer, "complete"); 573 | 574 | // If the sink does not support "complete", then return undefined 575 | value = m ? m.call(observer, value) : undefined; 576 | } catch (e) { 577 | 578 | try { 579 | cleanupSubscription(this); 580 | } finally { 581 | throw e; 582 | } 583 | } 584 | 585 | cleanupSubscription(this); 586 | 587 | return value; 588 | } 589 | }); 590 | 591 | function Subscription(observer) { 592 | this._observer = observer; 593 | } 594 | 595 | addMethods(Subscription.prototype = {}, { 596 | unsubscribe: function unsubscribe() { 597 | closeSubscription(this._observer); 598 | } 599 | }); 600 | 601 | function Observable(subscriber) { 602 | 603 | // The stream subscriber must be a function 604 | if (typeof subscriber !== "function") throw new TypeError("Observable initializer must be a function"); 605 | 606 | this._subscriber = subscriber; 607 | } 608 | 609 | addMethods(Observable.prototype, { 610 | subscribe: function subscribe(observer) { 611 | 612 | return createSubscription(observer, this._subscriber); 613 | }, 614 | forEach: function forEach(fn) { 615 | var _this = this; 616 | 617 | return new Promise(function (resolve, reject) { 618 | 619 | if (typeof fn !== "function") throw new TypeError(fn + " is not a function"); 620 | 621 | _this.subscribe({ 622 | next: function next(value) { 623 | 624 | try { 625 | return fn(value); 626 | } catch (e) { 627 | reject(e); 628 | } 629 | }, 630 | 631 | error: reject, 632 | complete: resolve 633 | }); 634 | }); 635 | }, 636 | map: function map(fn) { 637 | var _this2 = this; 638 | 639 | if (typeof fn !== "function") throw new TypeError(fn + " is not a function"); 640 | 641 | var C = getSpecies(this.constructor); 642 | 643 | return new C(function (observer) { 644 | return _this2.subscribe({ 645 | next: function next(value) { 646 | 647 | try { 648 | value = fn(value); 649 | } catch (e) { 650 | return observer.error(e); 651 | } 652 | 653 | return observer.next(value); 654 | }, 655 | error: function error(value) { 656 | return observer.error(value); 657 | }, 658 | complete: function complete(value) { 659 | return observer.complete(value); 660 | } 661 | }); 662 | }); 663 | }, 664 | filter: function filter(fn) { 665 | var _this3 = this; 666 | 667 | if (typeof fn !== "function") throw new TypeError(fn + " is not a function"); 668 | 669 | var C = getSpecies(this.constructor); 670 | 671 | return new C(function (observer) { 672 | return _this3.subscribe({ 673 | next: function next(value) { 674 | 675 | try { 676 | if (!fn(value)) return undefined; 677 | } catch (e) { 678 | return observer.error(e); 679 | } 680 | 681 | return observer.next(value); 682 | }, 683 | error: function error(value) { 684 | return observer.error(value); 685 | }, 686 | complete: function complete(value) { 687 | return observer.complete(value); 688 | } 689 | }); 690 | }); 691 | } 692 | }); 693 | 694 | Object.defineProperty(Observable.prototype, getSymbol("observable"), { 695 | value: function value() { 696 | return this; 697 | }, 698 | writable: true, 699 | configurable: true 700 | }); 701 | 702 | addMethods(Observable, { 703 | from: function from(x) { 704 | 705 | var C = typeof this === "function" ? this : Observable; 706 | 707 | if (x == null) throw new TypeError(x + " is not an object"); 708 | 709 | var method = getMethod(x, getSymbol("observable")); 710 | 711 | if (method) { 712 | var _ret2 = (function () { 713 | 714 | var observable = method.call(x); 715 | 716 | if (Object(observable) !== observable) throw new TypeError(observable + " is not an object"); 717 | 718 | if (observable.constructor === C) return { 719 | v: observable 720 | }; 721 | 722 | return { 723 | v: new C(function (observer) { 724 | return observable.subscribe(observer); 725 | }) 726 | }; 727 | })(); 728 | 729 | if ((typeof _ret2 === "undefined" ? "undefined" : _typeof(_ret2)) === "object") return _ret2.v; 730 | } 731 | 732 | return new C(function (observer) { 733 | 734 | enqueueJob(function (_) { 735 | 736 | if (observer.closed) return; 737 | 738 | // Assume that the object is iterable. If not, then the observer 739 | // will receive an error. 740 | try { 741 | 742 | if (hasSymbol("iterator")) { 743 | var _iteratorNormalCompletion = true; 744 | var _didIteratorError = false; 745 | var _iteratorError = undefined; 746 | 747 | try { 748 | 749 | for (var _iterator = x[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { 750 | var item = _step.value; 751 | 752 | observer.next(item); 753 | 754 | if (observer.closed) return; 755 | } 756 | } catch (err) { 757 | _didIteratorError = true; 758 | _iteratorError = err; 759 | } finally { 760 | try { 761 | if (!_iteratorNormalCompletion && _iterator.return) { 762 | _iterator.return(); 763 | } 764 | } finally { 765 | if (_didIteratorError) { 766 | throw _iteratorError; 767 | } 768 | } 769 | } 770 | } else { 771 | 772 | if (!Array.isArray(x)) throw new Error(x + " is not an Array"); 773 | 774 | for (var i = 0; i < x.length; ++i) { 775 | 776 | observer.next(x[i]); 777 | 778 | if (observer.closed) return; 779 | } 780 | } 781 | } catch (e) { 782 | 783 | // If observer.next throws an error, then the subscription will 784 | // be closed and the error method will simply rethrow 785 | observer.error(e); 786 | return; 787 | } 788 | 789 | observer.complete(); 790 | }); 791 | }); 792 | }, 793 | of: function of() { 794 | for (var _len = arguments.length, items = Array(_len), _key = 0; _key < _len; _key++) { 795 | items[_key] = arguments[_key]; 796 | } 797 | 798 | var C = typeof this === "function" ? this : Observable; 799 | 800 | return new C(function (observer) { 801 | 802 | enqueueJob(function (_) { 803 | 804 | if (observer.closed) return; 805 | 806 | for (var i = 0; i < items.length; ++i) { 807 | 808 | observer.next(items[i]); 809 | 810 | if (observer.closed) return; 811 | } 812 | 813 | observer.complete(); 814 | }); 815 | }); 816 | } 817 | }); 818 | 819 | Object.defineProperty(Observable, getSymbol("species"), { 820 | get: function get() { 821 | return this; 822 | }, 823 | 824 | configurable: true 825 | }); 826 | /* WEBPACK VAR INJECTION */}.call(exports, (function() { return this; }()), __webpack_require__(4))) 827 | 828 | /***/ }, 829 | /* 3 */ 830 | /***/ function(module, exports, __webpack_require__) { 831 | 832 | "use strict"; 833 | 834 | var _createClass = (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); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); 835 | 836 | var _get = function get(object, property, receiver) { if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; 837 | 838 | var _BetterObservable2 = __webpack_require__(1); 839 | 840 | var _BetterObservable3 = _interopRequireDefault(_BetterObservable2); 841 | 842 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 843 | 844 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 845 | 846 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 847 | 848 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } /** 849 | * @copyright © 2015, Rick Wong. All rights reserved. 850 | */ 851 | 852 | var PausableObservable = (function (_BetterObservable) { 853 | _inherits(PausableObservable, _BetterObservable); 854 | 855 | function PausableObservable(subscriber) { 856 | var _ref = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; 857 | 858 | var onPause = _ref.onPause; 859 | var onResume = _ref.onResume; 860 | 861 | _classCallCheck(this, PausableObservable); 862 | 863 | var _this = _possibleConstructorReturn(this, Object.getPrototypeOf(PausableObservable).call(this, subscriber)); 864 | 865 | _this.state = "paused"; 866 | 867 | _this.onPause = onPause; 868 | _this.onResume = onResume; 869 | return _this; 870 | } 871 | 872 | _createClass(PausableObservable, [{ 873 | key: "pause", 874 | value: function pause() { 875 | this.state = "paused"; 876 | 877 | return this.onPause && this.onPause.apply(this, arguments); 878 | } 879 | }, { 880 | key: "resume", 881 | value: function resume() { 882 | this.state = "resumed"; 883 | 884 | return this.onResume && this.onResume.apply(this, arguments); 885 | } 886 | }, { 887 | key: "paused", 888 | value: function paused() { 889 | return this.state === "paused"; 890 | } 891 | }, { 892 | key: "map", 893 | value: function map(callback) { 894 | var _this2 = this; 895 | 896 | var pausableObservable = _get(Object.getPrototypeOf(PausableObservable.prototype), "map", this).call(this, callback); 897 | 898 | // Child observable must track parent's state, so bind its pause, resume, and paused. 899 | Object.assign(pausableObservable, { 900 | pause: function pause() { 901 | return _this2.pause.apply(_this2, arguments); 902 | }, 903 | resume: function resume() { 904 | return _this2.resume.apply(_this2, arguments); 905 | }, 906 | paused: function paused() { 907 | return _this2.paused(); 908 | } 909 | }); 910 | 911 | return pausableObservable; 912 | } 913 | }]); 914 | 915 | return PausableObservable; 916 | })(_BetterObservable3.default); 917 | 918 | module.exports = PausableObservable; 919 | 920 | /***/ }, 921 | /* 4 */ 922 | /***/ function(module, exports) { 923 | 924 | // shim for using process in browser 925 | 926 | var process = module.exports = {}; 927 | var queue = []; 928 | var draining = false; 929 | var currentQueue; 930 | var queueIndex = -1; 931 | 932 | function cleanUpNextTick() { 933 | draining = false; 934 | if (currentQueue.length) { 935 | queue = currentQueue.concat(queue); 936 | } else { 937 | queueIndex = -1; 938 | } 939 | if (queue.length) { 940 | drainQueue(); 941 | } 942 | } 943 | 944 | function drainQueue() { 945 | if (draining) { 946 | return; 947 | } 948 | var timeout = setTimeout(cleanUpNextTick); 949 | draining = true; 950 | 951 | var len = queue.length; 952 | while(len) { 953 | currentQueue = queue; 954 | queue = []; 955 | while (++queueIndex < len) { 956 | if (currentQueue) { 957 | currentQueue[queueIndex].run(); 958 | } 959 | } 960 | queueIndex = -1; 961 | len = queue.length; 962 | } 963 | currentQueue = null; 964 | draining = false; 965 | clearTimeout(timeout); 966 | } 967 | 968 | process.nextTick = function (fun) { 969 | var args = new Array(arguments.length - 1); 970 | if (arguments.length > 1) { 971 | for (var i = 1; i < arguments.length; i++) { 972 | args[i - 1] = arguments[i]; 973 | } 974 | } 975 | queue.push(new Item(fun, args)); 976 | if (queue.length === 1 && !draining) { 977 | setTimeout(drainQueue, 0); 978 | } 979 | }; 980 | 981 | // v8 likes predictible objects 982 | function Item(fun, array) { 983 | this.fun = fun; 984 | this.array = array; 985 | } 986 | Item.prototype.run = function () { 987 | this.fun.apply(null, this.array); 988 | }; 989 | process.title = 'browser'; 990 | process.browser = true; 991 | process.env = {}; 992 | process.argv = []; 993 | process.version = ''; // empty string to avoid regexp issues 994 | process.versions = {}; 995 | 996 | function noop() {} 997 | 998 | process.on = noop; 999 | process.addListener = noop; 1000 | process.once = noop; 1001 | process.off = noop; 1002 | process.removeListener = noop; 1003 | process.removeAllListeners = noop; 1004 | process.emit = noop; 1005 | 1006 | process.binding = function (name) { 1007 | throw new Error('process.binding is not supported'); 1008 | }; 1009 | 1010 | process.cwd = function () { return '/' }; 1011 | process.chdir = function (dir) { 1012 | throw new Error('process.chdir is not supported'); 1013 | }; 1014 | process.umask = function() { return 0; }; 1015 | 1016 | 1017 | /***/ } 1018 | /******/ ]) 1019 | }); 1020 | ; -------------------------------------------------------------------------------- /dist/lib/fetchObservable.min.js: -------------------------------------------------------------------------------- 1 | !function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.fetchObservable=e():t.fetchObservable=e()}(this,function(){return function(t){function e(r){if(n[r])return n[r].exports;var o=n[r]={exports:{},id:r,loaded:!1};return t[r].call(o.exports,o,o.exports,e),o.loaded=!0,o.exports}var n={};return e.m=t,e.c=n,e.p="",e(0)}([function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function o(t){return"string"==typeof t}function u(t){return"function"==typeof t}function i(t){var e=arguments.length<=1||void 0===arguments[1]?{}:arguments[1],n=e.refreshDelay,r=void 0===n?!1:n,i=e.fetch||fetch,f=[],a=null,l=!1,p=0;(l=o(t))&&(t=[t]);var h=function y(){if(0!==f.length&&!b.paused()){var n=function(){r?a=setTimeout(y,u(r)?r(p++):r):(b.pause(),f.map(function(t){return t.complete()}),f=[])},o=t.map(function(t){return i(t,c({},e,{refreshDelay:void 0,fetch:void 0}))});Promise.all(o).then(function(t){f.map(function(e){return e.next(l?t[0]:t)}),n()})["catch"](function(t){f.map(function(e){return e.error(t)}),n()})}},b=new s["default"](function(t){return f.push(t),b.resume(),function(){f.splice(f.indexOf(t),1),f.length||b.pause()}},{onPause:function(){a&&(clearTimeout(a),a=null)},onResume:function(){a||h()}});return b}var c=Object.assign||function(t){for(var e=1;e1&&n();r.length>0;)r.shift()()});return o.observe(t,{attributes:!0}),{v:function(t){r.push(t),1===r.length&&n()}}}();if("object"===("undefined"==typeof o?"undefined":r(o)))return o.v}return function(t){setTimeout(t,0)}}();o("observable"),a(v.prototype={},{get closed(){return p(this)},next:function(t){if(!p(this)){var e=this._observer;try{var n=f(e,"next");if(!n)return;return n.call(e,t)}catch(r){try{h(this)}finally{throw r}}}},error:function(t){if(p(this))throw t;var e=this._observer;this._observer=void 0;try{var n=f(e,"error");if(!n)throw t;t=n.call(e,t)}catch(r){try{l(this)}finally{throw r}}return l(this),t},complete:function(t){if(!p(this)){var e=this._observer;this._observer=void 0;try{var n=f(e,"complete");t=n?n.call(e,t):void 0}catch(r){try{l(this)}finally{throw r}}return l(this),t}}}),a(m.prototype={},{unsubscribe:function(){h(this._observer)}}),a(d.prototype,{subscribe:function(t){return y(t,this._subscriber)},forEach:function(t){var e=this;return new Promise(function(n,r){if("function"!=typeof t)throw new TypeError(t+" is not a function");e.subscribe({next:function(e){try{return t(e)}catch(n){r(n)}},error:r,complete:n})})},map:function(t){var e=this;if("function"!=typeof t)throw new TypeError(t+" is not a function");var n=s(this.constructor);return new n(function(n){return e.subscribe({next:function(e){try{e=t(e)}catch(r){return n.error(r)}return n.next(e)},error:function(t){return n.error(t)},complete:function(t){return n.complete(t)}})})},filter:function(t){var e=this;if("function"!=typeof t)throw new TypeError(t+" is not a function");var n=s(this.constructor);return new n(function(n){return e.subscribe({next:function(e){try{if(!t(e))return}catch(r){return n.error(r)}return n.next(e)},error:function(t){return n.error(t)},complete:function(t){return n.complete(t)}})})}}),Object.defineProperty(d.prototype,c("observable"),{value:function(){return this},writable:!0,configurable:!0}),a(d,{from:function(t){var e="function"==typeof this?this:d;if(null==t)throw new TypeError(t+" is not an object");var n=f(t,c("observable"));if(n){var o=function(){var r=n.call(t);if(Object(r)!==r)throw new TypeError(r+" is not an object");return r.constructor===e?{v:r}:{v:new e(function(t){return r.subscribe(t)})}}();if("object"===("undefined"==typeof o?"undefined":r(o)))return o.v}return new e(function(e){w(function(n){if(!e.closed){try{if(i("iterator")){var r=!0,o=!1,u=void 0;try{for(var c,f=t[Symbol.iterator]();!(r=(c=f.next()).done);r=!0){var s=c.value;if(e.next(s),e.closed)return}}catch(a){o=!0,u=a}finally{try{!r&&f["return"]&&f["return"]()}finally{if(o)throw u}}}else{if(!Array.isArray(t))throw new Error(t+" is not an Array");for(var l=0;ln;n++)e[n]=arguments[n];var r="function"==typeof this?this:d;return new r(function(t){w(function(n){if(!t.closed){for(var r=0;r1)for(var n=1;n=0.10.32" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/example.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @copyright © 2015, Rick Wong. All rights reserved. 3 | */ 4 | import __fetch from "isomorphic-fetch"; 5 | import fetchObservable from "lib/fetchObservable"; 6 | import React from "react"; 7 | import ReactDOM from "react-dom"; 8 | 9 | try { 10 | let observable = fetchObservable( 11 | "http://api.openweathermap.org/data/2.5/weather?q=London,uk&appid=2de143494c0b295cca9337e1e96b00e0", { 12 | refreshDelay: 1500 13 | } 14 | ).map(a=>a).map(a=>a).map(a=>a).map(a=>a).map(a=>a).map((response) => response.json()); 15 | 16 | let subscriptions = {}; 17 | 18 | global.o = observable; 19 | 20 | function toggleFetching (index) { 21 | if (subscriptions[index]) { 22 | subscriptions[index].unsubscribe(); 23 | delete subscriptions[index]; 24 | } 25 | else { 26 | subscriptions[index] = observable.subscribe({ 27 | next: (...args) => { 28 | console.log(`subscriptions[${index}] next:`, ...args); 29 | }, 30 | error: (...args) => { 31 | console.warn(`subscriptions[${index}] error:`, ...args); 32 | }, 33 | complete: () => { 34 | console.log(`subscriptions[${index}] complete`); 35 | toggleFetching(index); 36 | } 37 | }); 38 | } 39 | 40 | render(); 41 | } 42 | 43 | function render () { 44 | ReactDOM.render( 45 |
46 |
47 |
48 | 51 | 54 |
, 55 | document.getElementById("react-root") 56 | ); 57 | } 58 | 59 | render(); 60 | } 61 | catch (error) 62 | { 63 | throw error; 64 | } 65 | -------------------------------------------------------------------------------- /src/lib/BetterObservable.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @copyright © 2015, Rick Wong. All rights reserved. 3 | */ 4 | import {Observable} from "lib/Observable"; 5 | 6 | function isPromise (thing) { 7 | return ( 8 | typeof thing === "object" && 9 | typeof thing["then"] === "function" && 10 | typeof thing["catch"] === "function" 11 | ); 12 | } 13 | 14 | class BetterObservable extends Observable { 15 | map (callback) { 16 | if (typeof callback !== "function") { 17 | throw new TypeError(callback + " is not a function"); 18 | } 19 | 20 | let parentSubscription = null; 21 | let childObservers = []; 22 | 23 | const createParentSubscription = () => { 24 | if (parentSubscription) { 25 | return; 26 | } 27 | 28 | parentSubscription = this.subscribe({ 29 | next (value) { 30 | try { 31 | value = callback(value); 32 | } 33 | catch (e) { 34 | return childObservers.map((o) => o.error(e)); 35 | } 36 | 37 | // Support Promises. 38 | if (isPromise(value)) { 39 | return value.then( 40 | (v) => childObservers.map((o) => o.next(v)) 41 | ).catch( 42 | (e) => childObservers.map((o) => o.error(e)) 43 | ); 44 | } 45 | 46 | childObservers.map((o) => o.next(value)); 47 | }, 48 | error: (e) => childObservers.map((o) => o.error(e)), 49 | complete: () => childObservers.map((o) => o.complete()) 50 | }); 51 | }; 52 | 53 | const destroyParentSubscription = () => { 54 | parentSubscription && parentSubscription.unsubscribe(); 55 | parentSubscription = null; 56 | }; 57 | 58 | return new this.constructor((observer) => { 59 | childObservers.push(observer); 60 | createParentSubscription(); 61 | 62 | return () => { 63 | childObservers.splice(childObservers.indexOf(observer), 1); 64 | 65 | if (!childObservers.length) { 66 | destroyParentSubscription(); 67 | } 68 | }; 69 | }); 70 | } 71 | } 72 | 73 | module.exports = BetterObservable; 74 | -------------------------------------------------------------------------------- /src/lib/Observable.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Observable.js from zenparsing/zen-observable 3 | * 4 | * @copyright © zenparsing 5 | * @homepage https://github.com/zenparsing/zen-observable 6 | * @file https://github.com/zenparsing/zen-observable/blob/de80d63fb166421226bb3c918b111cac40bd672a/src/Observable.js 7 | */ 8 | // === Non-Promise Job Queueing === 9 | 10 | const enqueueJob = (function() { 11 | 12 | // Node 13 | if (typeof global !== "undefined" && 14 | typeof process !== "undefined" && 15 | process.nextTick) { 16 | 17 | return global.setImmediate ? 18 | fn => { global.setImmediate(fn) } : 19 | fn => { process.nextTick(fn) }; 20 | } 21 | 22 | // Newish Browsers 23 | let Observer = self.MutationObserver || self.WebKitMutationObserver; 24 | 25 | if (Observer) { 26 | 27 | let div = document.createElement("div"), 28 | twiddle = _=> div.classList.toggle("x"), 29 | queue = []; 30 | 31 | let observer = new Observer(_=> { 32 | 33 | if (queue.length > 1) 34 | twiddle(); 35 | 36 | while (queue.length > 0) 37 | queue.shift()(); 38 | }); 39 | 40 | observer.observe(div, { attributes: true }); 41 | 42 | return fn => { 43 | 44 | queue.push(fn); 45 | 46 | if (queue.length === 1) 47 | twiddle(); 48 | }; 49 | } 50 | 51 | // Fallback 52 | return fn => { setTimeout(fn, 0) }; 53 | 54 | })(); 55 | 56 | // === Symbol Polyfills === 57 | 58 | function polyfillSymbol(name) { 59 | 60 | if (symbolsSupported() && !Symbol[name]) 61 | Object.defineProperty(Symbol, name, { value: Symbol(name) }); 62 | } 63 | 64 | function symbolsSupported() { 65 | 66 | return typeof Symbol === "function"; 67 | } 68 | 69 | function hasSymbol(name) { 70 | 71 | return symbolsSupported() && Boolean(Symbol[name]); 72 | } 73 | 74 | function getSymbol(name) { 75 | 76 | return hasSymbol(name) ? Symbol[name] : "@@" + name; 77 | } 78 | 79 | polyfillSymbol("observable"); 80 | 81 | // === Abstract Operations === 82 | 83 | function getMethod(obj, key) { 84 | 85 | let value = obj[key]; 86 | 87 | if (value == null) 88 | return undefined; 89 | 90 | if (typeof value !== "function") 91 | throw new TypeError(value + " is not a function"); 92 | 93 | return value; 94 | } 95 | 96 | function getSpecies(ctor) { 97 | 98 | let symbol = getSymbol("species"); 99 | return symbol ? ctor[symbol] : ctor; 100 | } 101 | 102 | function addMethods(target, methods) { 103 | 104 | Object.keys(methods).forEach(k => { 105 | 106 | let desc = Object.getOwnPropertyDescriptor(methods, k); 107 | desc.enumerable = false; 108 | Object.defineProperty(target, k, desc); 109 | }); 110 | } 111 | 112 | function cleanupSubscription(observer) { 113 | 114 | // Assert: observer._observer is undefined 115 | 116 | let cleanup = observer._cleanup; 117 | 118 | if (!cleanup) 119 | return; 120 | 121 | // Drop the reference to the cleanup function so that we won't call it 122 | // more than once 123 | observer._cleanup = undefined; 124 | 125 | // Call the cleanup function 126 | cleanup(); 127 | } 128 | 129 | function subscriptionClosed(observer) { 130 | 131 | return observer._observer === undefined; 132 | } 133 | 134 | function closeSubscription(observer) { 135 | 136 | if (subscriptionClosed(observer)) 137 | return; 138 | 139 | observer._observer = undefined; 140 | cleanupSubscription(observer); 141 | } 142 | 143 | function cleanupFromSubscription(subscription) { 144 | // TODO: Should we get the method out and apply it here, instead of 145 | // looking up the method at call time? 146 | return _=> { subscription.unsubscribe() }; 147 | } 148 | 149 | function createSubscription(observer, subscriber) { 150 | 151 | // Assert: subscriber is callable 152 | 153 | // The observer must be an object 154 | if (Object(observer) !== observer) 155 | throw new TypeError("Observer must be an object"); 156 | 157 | // TODO: Should we check for a "next" method here? 158 | 159 | let subscriptionObserver = new SubscriptionObserver(observer), 160 | subscription = new Subscription(subscriptionObserver), 161 | start = getMethod(observer, "start"); 162 | 163 | if (start) 164 | start.call(observer, subscription); 165 | 166 | if (subscriptionClosed(subscriptionObserver)) 167 | return subscription; 168 | 169 | try { 170 | 171 | // Call the subscriber function 172 | let cleanup = subscriber.call(undefined, subscriptionObserver); 173 | 174 | // The return value must be undefined, null, a subscription object, or a function 175 | if (cleanup != null) { 176 | 177 | if (typeof cleanup.unsubscribe === "function") 178 | cleanup = cleanupFromSubscription(cleanup); 179 | else if (typeof cleanup !== "function") 180 | throw new TypeError(cleanup + " is not a function"); 181 | 182 | subscriptionObserver._cleanup = cleanup; 183 | } 184 | 185 | } catch (e) { 186 | 187 | // If an error occurs during startup, then attempt to send the error 188 | // to the observer 189 | subscriptionObserver.error(e); 190 | return subscription; 191 | } 192 | 193 | // If the stream is already finished, then perform cleanup 194 | if (subscriptionClosed(subscriptionObserver)) 195 | cleanupSubscription(subscriptionObserver); 196 | 197 | return subscription; 198 | } 199 | 200 | function SubscriptionObserver(observer) { 201 | 202 | this._observer = observer; 203 | this._cleanup = undefined; 204 | } 205 | 206 | addMethods(SubscriptionObserver.prototype = {}, { 207 | 208 | get closed() { return subscriptionClosed(this) }, 209 | 210 | next(value) { 211 | 212 | // If the stream if closed, then return undefined 213 | if (subscriptionClosed(this)) 214 | return undefined; 215 | 216 | let observer = this._observer; 217 | 218 | try { 219 | 220 | let m = getMethod(observer, "next"); 221 | 222 | // If the observer doesn't support "next", then return undefined 223 | if (!m) 224 | return undefined; 225 | 226 | // Send the next value to the sink 227 | return m.call(observer, value); 228 | 229 | } catch (e) { 230 | 231 | // If the observer throws, then close the stream and rethrow the error 232 | try { closeSubscription(this) } 233 | finally { throw e } 234 | } 235 | }, 236 | 237 | error(value) { 238 | 239 | // If the stream is closed, throw the error to the caller 240 | if (subscriptionClosed(this)) 241 | throw value; 242 | 243 | let observer = this._observer; 244 | this._observer = undefined; 245 | 246 | try { 247 | 248 | let m = getMethod(observer, "error"); 249 | 250 | // If the sink does not support "error", then throw the error to the caller 251 | if (!m) 252 | throw value; 253 | 254 | value = m.call(observer, value); 255 | 256 | } catch (e) { 257 | 258 | try { cleanupSubscription(this) } 259 | finally { throw e } 260 | } 261 | 262 | cleanupSubscription(this); 263 | 264 | return value; 265 | }, 266 | 267 | complete(value) { 268 | 269 | // If the stream is closed, then return undefined 270 | if (subscriptionClosed(this)) 271 | return undefined; 272 | 273 | let observer = this._observer; 274 | this._observer = undefined; 275 | 276 | try { 277 | 278 | let m = getMethod(observer, "complete"); 279 | 280 | // If the sink does not support "complete", then return undefined 281 | value = m ? m.call(observer, value) : undefined; 282 | 283 | } catch (e) { 284 | 285 | try { cleanupSubscription(this) } 286 | finally { throw e } 287 | } 288 | 289 | cleanupSubscription(this); 290 | 291 | return value; 292 | }, 293 | 294 | }); 295 | 296 | function Subscription(observer) { 297 | this._observer = observer; 298 | } 299 | 300 | addMethods(Subscription.prototype = {}, { 301 | unsubscribe() { closeSubscription(this._observer) } 302 | }); 303 | 304 | export function Observable(subscriber) { 305 | 306 | // The stream subscriber must be a function 307 | if (typeof subscriber !== "function") 308 | throw new TypeError("Observable initializer must be a function"); 309 | 310 | this._subscriber = subscriber; 311 | } 312 | 313 | addMethods(Observable.prototype, { 314 | 315 | subscribe(observer) { 316 | 317 | return createSubscription(observer, this._subscriber); 318 | }, 319 | 320 | forEach(fn) { 321 | 322 | return new Promise((resolve, reject) => { 323 | 324 | if (typeof fn !== "function") 325 | throw new TypeError(fn + " is not a function"); 326 | 327 | this.subscribe({ 328 | 329 | next(value) { 330 | 331 | try { return fn(value) } 332 | catch (e) { reject(e) } 333 | }, 334 | 335 | error: reject, 336 | complete: resolve, 337 | }); 338 | }); 339 | }, 340 | 341 | map(fn) { 342 | 343 | if (typeof fn !== "function") 344 | throw new TypeError(fn + " is not a function"); 345 | 346 | let C = getSpecies(this.constructor); 347 | 348 | return new C(observer => this.subscribe({ 349 | 350 | next(value) { 351 | 352 | try { value = fn(value) } 353 | catch (e) { return observer.error(e) } 354 | 355 | return observer.next(value); 356 | }, 357 | 358 | error(value) { return observer.error(value) }, 359 | complete(value) { return observer.complete(value) }, 360 | })); 361 | }, 362 | 363 | filter(fn) { 364 | 365 | if (typeof fn !== "function") 366 | throw new TypeError(fn + " is not a function"); 367 | 368 | let C = getSpecies(this.constructor); 369 | 370 | return new C(observer => this.subscribe({ 371 | 372 | next(value) { 373 | 374 | try { if (!fn(value)) return undefined; } 375 | catch (e) { return observer.error(e) } 376 | 377 | return observer.next(value); 378 | }, 379 | 380 | error(value) { return observer.error(value) }, 381 | complete(value) { return observer.complete(value) }, 382 | })); 383 | }, 384 | 385 | }); 386 | 387 | Object.defineProperty(Observable.prototype, getSymbol("observable"), { 388 | value: function() { return this }, 389 | writable: true, 390 | configurable: true, 391 | }); 392 | 393 | addMethods(Observable, { 394 | 395 | from(x) { 396 | 397 | let C = typeof this === "function" ? this : Observable; 398 | 399 | if (x == null) 400 | throw new TypeError(x + " is not an object"); 401 | 402 | let method = getMethod(x, getSymbol("observable")); 403 | 404 | if (method) { 405 | 406 | let observable = method.call(x); 407 | 408 | if (Object(observable) !== observable) 409 | throw new TypeError(observable + " is not an object"); 410 | 411 | if (observable.constructor === C) 412 | return observable; 413 | 414 | return new C(observer => observable.subscribe(observer)); 415 | } 416 | 417 | return new C(observer => { 418 | 419 | enqueueJob(_=> { 420 | 421 | if (observer.closed) 422 | return; 423 | 424 | // Assume that the object is iterable. If not, then the observer 425 | // will receive an error. 426 | try { 427 | 428 | if (hasSymbol("iterator")) { 429 | 430 | for (let item of x) { 431 | 432 | observer.next(item); 433 | 434 | if (observer.closed) 435 | return; 436 | } 437 | 438 | } else { 439 | 440 | if (!Array.isArray(x)) 441 | throw new Error(x + " is not an Array"); 442 | 443 | for (let i = 0; i < x.length; ++i) { 444 | 445 | observer.next(x[i]); 446 | 447 | if (observer.closed) 448 | return; 449 | } 450 | } 451 | 452 | } catch (e) { 453 | 454 | // If observer.next throws an error, then the subscription will 455 | // be closed and the error method will simply rethrow 456 | observer.error(e); 457 | return; 458 | } 459 | 460 | observer.complete(); 461 | }); 462 | }); 463 | }, 464 | 465 | of(...items) { 466 | 467 | let C = typeof this === "function" ? this : Observable; 468 | 469 | return new C(observer => { 470 | 471 | enqueueJob(_=> { 472 | 473 | if (observer.closed) 474 | return; 475 | 476 | for (let i = 0; i < items.length; ++i) { 477 | 478 | observer.next(items[i]); 479 | 480 | if (observer.closed) 481 | return; 482 | } 483 | 484 | observer.complete(); 485 | }); 486 | }); 487 | }, 488 | 489 | }); 490 | 491 | Object.defineProperty(Observable, getSymbol("species"), { 492 | get() { return this }, 493 | configurable: true, 494 | }); 495 | -------------------------------------------------------------------------------- /src/lib/PausableObservable.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @copyright © 2015, Rick Wong. All rights reserved. 3 | */ 4 | import BetterObservable from "lib/BetterObservable"; 5 | 6 | class PausableObservable extends BetterObservable { 7 | constructor (subscriber, {onPause, onResume} = {}) { 8 | super(subscriber); 9 | 10 | this.state = "paused"; 11 | 12 | this.onPause = onPause; 13 | this.onResume = onResume; 14 | } 15 | 16 | pause (...args) { 17 | this.state = "paused"; 18 | 19 | return this.onPause && this.onPause(...args); 20 | } 21 | 22 | resume (...args) { 23 | this.state = "resumed"; 24 | 25 | return this.onResume && this.onResume(...args); 26 | } 27 | 28 | paused () { 29 | return this.state === "paused"; 30 | } 31 | 32 | map (callback) { 33 | const pausableObservable = super.map(callback); 34 | 35 | // Child observable must track parent's state, so bind its pause, resume, and paused. 36 | Object.assign(pausableObservable, { 37 | pause: (...args) => this.pause(...args), 38 | resume: (...args) => this.resume(...args), 39 | paused: () => this.paused() 40 | }); 41 | 42 | return pausableObservable; 43 | } 44 | } 45 | 46 | module.exports = PausableObservable; 47 | -------------------------------------------------------------------------------- /src/lib/fetchObservable.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @copyright © 2015, Rick Wong. All rights reserved. 3 | */ 4 | import PausableObservable from "lib/PausableObservable"; 5 | 6 | function isString (thing) { 7 | return typeof thing === "string"; 8 | } 9 | 10 | function isFunction (thing) { 11 | return typeof thing === "function"; 12 | } 13 | 14 | /** 15 | * Calls the Fetch API and returns an Observable. 16 | * 17 | * @param {string|string[]} urls URL or URLs array. 18 | * @param {object} options 19 | * @returns {PausableObservable|Observable} 20 | */ 21 | function fetchObservable (urls, options = {}) { 22 | const {refreshDelay = false} = options; 23 | const fetchFunc = options.fetch || fetch; 24 | 25 | let observers = []; 26 | let timeout = null; 27 | let singleResult = false; 28 | let iteration = 0; 29 | 30 | if (singleResult = isString(urls)) { 31 | urls = [urls]; 32 | } 33 | 34 | const performFetch = function () { 35 | // Don't do anything if there are no observers. 36 | if (observers.length === 0 || 37 | observable.paused()) { 38 | return; 39 | } 40 | 41 | const _finally = function () { 42 | // If refreshing is disabled, complete observers and pause observable. 43 | if (!refreshDelay) { 44 | observable.pause(); 45 | observers.map((observer) => observer.complete()); 46 | observers = []; 47 | } 48 | // If refreshing is enabled, set a timeout. 49 | else { 50 | timeout = setTimeout( 51 | performFetch, 52 | isFunction(refreshDelay) ? refreshDelay(iteration++) : refreshDelay 53 | ); 54 | } 55 | }; 56 | 57 | // Map all URLs to Fetch API calls. 58 | let fetches = urls.map( 59 | (url) => fetchFunc(url, {...options, refreshDelay: undefined, fetch: undefined}) 60 | ); 61 | 62 | // Wait for all the results to come in, then notify observers. 63 | Promise.all(fetches).then(function (results) { 64 | observers.map((observer) => observer.next(singleResult ? results[0] : results)); 65 | _finally(); 66 | }).catch(function (error) { 67 | observers.map((observer) => observer.error(error)); 68 | _finally(); 69 | }); 70 | }; 71 | 72 | const observable = new PausableObservable(function (observer) { 73 | observers.push(observer); 74 | observable.resume(); 75 | 76 | return function () { 77 | observers.splice(observers.indexOf(observer), 1); 78 | 79 | if (!observers.length) { 80 | observable.pause(); 81 | } 82 | }; 83 | }, { 84 | onPause () { 85 | if (timeout) { 86 | clearTimeout(timeout); 87 | timeout = null; 88 | } 89 | }, 90 | onResume () { 91 | if (!timeout) { 92 | performFetch(); 93 | } 94 | } 95 | }); 96 | 97 | return observable; 98 | } 99 | 100 | module.exports = fetchObservable; 101 | -------------------------------------------------------------------------------- /webpack.client-watch.js: -------------------------------------------------------------------------------- 1 | var webpack = require("webpack"); 2 | var config = require("./webpack.client.js"); 3 | 4 | config.cache = true; 5 | config.debug = true; 6 | config.devtool = "eval-sourcemap"; 7 | 8 | config.entry._wds = "webpack-dev-server/client?http://localhost:8080"; 9 | config.entry._hmr = "webpack/hot/only-dev-server"; 10 | 11 | config.output.publicPath = "http://localhost:8080/"; 12 | config.output.hotUpdateMainFilename = "update/[hash]/update.json"; 13 | config.output.hotUpdateChunkFilename = "update/[hash]/[id].update.js"; 14 | 15 | config.plugins = [ 16 | new webpack.HotModuleReplacementPlugin() 17 | ]; 18 | 19 | config.devServer = { 20 | publicPath: "http://localhost:8080/", 21 | contentBase: "./dist", 22 | hot: true, 23 | inline: true, 24 | lazy: false, 25 | quiet: true, 26 | noInfo: false, 27 | headers: {"Access-Control-Allow-Origin": "*"}, 28 | stats: {colors: true} 29 | }; 30 | 31 | module.exports = config; 32 | -------------------------------------------------------------------------------- /webpack.client.js: -------------------------------------------------------------------------------- 1 | var webpack = require("webpack"); 2 | var path = require("path"); 3 | 4 | module.exports = { 5 | target: "web", 6 | cache: false, 7 | context: __dirname, 8 | devtool: false, 9 | entry: { 10 | "example": "./src/example" 11 | }, 12 | output: { 13 | path: path.join(__dirname, "dist"), 14 | filename: "[name].js", 15 | chunkFilename: "[name].[id].js", 16 | publicPath: "/" 17 | }, 18 | plugins: [ 19 | new webpack.DefinePlugin({"process.env": {NODE_ENV: '"production"'}}), 20 | new webpack.optimize.DedupePlugin(), 21 | new webpack.optimize.OccurenceOrderPlugin(), 22 | new webpack.optimize.UglifyJsPlugin() 23 | ], 24 | module: { 25 | loaders: [ 26 | {test: /\.json$/, loaders: ["json"]}, 27 | {test: /\.js$/, loaders: ["babel?cacheDirectory&presets[]=es2015&presets[]=react&presets[]=stage-0"], exclude: /node_modules/} 28 | ], 29 | noParse: /\.min\.js$/ 30 | }, 31 | resolve: { 32 | modulesDirectories: [ 33 | "src", 34 | "node_modules", 35 | "web_modules" 36 | ], 37 | extensions: ["", ".json", ".js"] 38 | }, 39 | node: { 40 | __dirname: true, 41 | fs: 'empty' 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /webpack.umd.js: -------------------------------------------------------------------------------- 1 | var webpack = require("webpack"); 2 | var path = require("path"); 3 | 4 | module.exports = { 5 | cache: false, 6 | context: __dirname, 7 | output: { 8 | library: "fetchObservable", 9 | libraryTarget: "umd" 10 | }, 11 | plugins: [ 12 | new webpack.DefinePlugin({"process.env": {NODE_ENV: '"production"'}}), 13 | new webpack.optimize.DedupePlugin(), 14 | new webpack.optimize.OccurenceOrderPlugin() 15 | ], 16 | module: { 17 | loaders: [ 18 | {test: /\.json$/, loaders: ["json"]}, 19 | {test: /\.js$/, 20 | loaders: ["babel?cacheDirectory&presets[]=es2015&presets[]=stage-0"], 21 | exclude: /node_modules/ 22 | } 23 | ], 24 | noParse: /\.min\.js$/ 25 | }, 26 | resolve: { 27 | modulesDirectories: [ 28 | "src", 29 | "node_modules", 30 | "web_modules" 31 | ], 32 | extensions: ["", ".json", ".js"] 33 | }, 34 | node: { 35 | __dirname: true, 36 | fs: 'empty' 37 | } 38 | }; 39 | --------------------------------------------------------------------------------