Source: dependencies/resource-loader/lib/Loader.js

dependencies/resource-loader/lib/Loader.js

  1. 'use strict';
  2. exports.__esModule = true;
  3. exports.Loader = undefined;
  4. var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };
  5. 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; }; }();
  6. var _miniSignals = require('mini-signals');
  7. var _miniSignals2 = _interopRequireDefault(_miniSignals);
  8. var _parseUri = require('parse-uri');
  9. var _parseUri2 = _interopRequireDefault(_parseUri);
  10. var _async = require('./async');
  11. var async = _interopRequireWildcard(_async);
  12. var _Resource = require('./Resource');
  13. function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }
  14. function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
  15. function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
  16. // some constants
  17. var MAX_PROGRESS = 100;
  18. var rgxExtractUrlHash = /(#[\w-]+)?$/;
  19. /**
  20. * Manages the state and loading of multiple resources to load.
  21. *
  22. * @class
  23. */
  24. var Loader = exports.Loader = function () {
  25. /**
  26. * @param {string} [baseUrl=''] - The base url for all resources loaded by this loader.
  27. * @param {number} [concurrency=10] - The number of resources to load concurrently.
  28. */
  29. function Loader() {
  30. var _this = this;
  31. var baseUrl = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
  32. var concurrency = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 10;
  33. _classCallCheck(this, Loader);
  34. /**
  35. * The base url for all resources loaded by this loader.
  36. *
  37. * @member {string}
  38. */
  39. this.baseUrl = baseUrl;
  40. /**
  41. * The progress percent of the loader going through the queue.
  42. *
  43. * @member {number}
  44. */
  45. this.progress = 0;
  46. /**
  47. * Loading state of the loader, true if it is currently loading resources.
  48. *
  49. * @member {boolean}
  50. */
  51. this.loading = false;
  52. /**
  53. * A querystring to append to every URL added to the loader.
  54. *
  55. * This should be a valid query string *without* the question-mark (`?`). The loader will
  56. * also *not* escape values for you. Make sure to escape your parameters with
  57. * [`encodeURIComponent`](https://mdn.io/encodeURIComponent) before assigning this property.
  58. *
  59. * @example
  60. * const loader = new Loader();
  61. *
  62. * loader.defaultQueryString = 'user=me&password=secret';
  63. *
  64. * // This will request 'image.png?user=me&password=secret'
  65. * loader.add('image.png').load();
  66. *
  67. * loader.reset();
  68. *
  69. * // This will request 'image.png?v=1&user=me&password=secret'
  70. * loader.add('iamge.png?v=1').load();
  71. *
  72. * @member {string}
  73. */
  74. this.defaultQueryString = '';
  75. /**
  76. * The middleware to run before loading each resource.
  77. *
  78. * @private
  79. * @member {function[]}
  80. */
  81. this._beforeMiddleware = [];
  82. /**
  83. * The middleware to run after loading each resource.
  84. *
  85. * @private
  86. * @member {function[]}
  87. */
  88. this._afterMiddleware = [];
  89. /**
  90. * The tracks the resources we are currently completing parsing for.
  91. *
  92. * @private
  93. * @member {Resource[]}
  94. */
  95. this._resourcesParsing = [];
  96. /**
  97. * The `_loadResource` function bound with this object context.
  98. *
  99. * @private
  100. * @member {function}
  101. * @param {Resource} r - The resource to load
  102. * @param {Function} d - The dequeue function
  103. * @return {undefined}
  104. */
  105. this._boundLoadResource = function (r, d) {
  106. return _this._loadResource(r, d);
  107. };
  108. /**
  109. * The resources waiting to be loaded.
  110. *
  111. * @private
  112. * @member {Resource[]}
  113. */
  114. this._queue = async.queue(this._boundLoadResource, concurrency);
  115. this._queue.pause();
  116. /**
  117. * All the resources for this loader keyed by name.
  118. *
  119. * @member {object<string, Resource>}
  120. */
  121. this.resources = {};
  122. /**
  123. * Dispatched once per loaded or errored resource.
  124. *
  125. * The callback looks like {@link Loader.OnProgressSignal}.
  126. *
  127. * @member {Signal<Loader.OnProgressSignal>}
  128. */
  129. this.onProgress = new _miniSignals2.default();
  130. /**
  131. * Dispatched once per errored resource.
  132. *
  133. * The callback looks like {@link Loader.OnErrorSignal}.
  134. *
  135. * @member {Signal<Loader.OnErrorSignal>}
  136. */
  137. this.onError = new _miniSignals2.default();
  138. /**
  139. * Dispatched once per loaded resource.
  140. *
  141. * The callback looks like {@link Loader.OnLoadSignal}.
  142. *
  143. * @member {Signal<Loader.OnLoadSignal>}
  144. */
  145. this.onLoad = new _miniSignals2.default();
  146. /**
  147. * Dispatched when the loader begins to process the queue.
  148. *
  149. * The callback looks like {@link Loader.OnStartSignal}.
  150. *
  151. * @member {Signal<Loader.OnStartSignal>}
  152. */
  153. this.onStart = new _miniSignals2.default();
  154. /**
  155. * Dispatched when the queued resources all load.
  156. *
  157. * The callback looks like {@link Loader.OnCompleteSignal}.
  158. *
  159. * @member {Signal<Loader.OnCompleteSignal>}
  160. */
  161. this.onComplete = new _miniSignals2.default();
  162. // Add default before middleware
  163. for (var i = 0; i < Loader._defaultBeforeMiddleware.length; ++i) {
  164. this.pre(Loader._defaultBeforeMiddleware[i]);
  165. }
  166. // Add default after middleware
  167. for (var _i = 0; _i < Loader._defaultAfterMiddleware.length; ++_i) {
  168. this.use(Loader._defaultAfterMiddleware[_i]);
  169. }
  170. }
  171. /**
  172. * When the progress changes the loader and resource are disaptched.
  173. *
  174. * @memberof Loader
  175. * @callback OnProgressSignal
  176. * @param {Loader} loader - The loader the progress is advancing on.
  177. * @param {Resource} resource - The resource that has completed or failed to cause the progress to advance.
  178. */
  179. /**
  180. * When an error occurrs the loader and resource are disaptched.
  181. *
  182. * @memberof Loader
  183. * @callback OnErrorSignal
  184. * @param {Loader} loader - The loader the error happened in.
  185. * @param {Resource} resource - The resource that caused the error.
  186. */
  187. /**
  188. * When a load completes the loader and resource are disaptched.
  189. *
  190. * @memberof Loader
  191. * @callback OnLoadSignal
  192. * @param {Loader} loader - The loader that laoded the resource.
  193. * @param {Resource} resource - The resource that has completed loading.
  194. */
  195. /**
  196. * When the loader starts loading resources it dispatches this callback.
  197. *
  198. * @memberof Loader
  199. * @callback OnStartSignal
  200. * @param {Loader} loader - The loader that has started loading resources.
  201. */
  202. /**
  203. * When the loader completes loading resources it dispatches this callback.
  204. *
  205. * @memberof Loader
  206. * @callback OnCompleteSignal
  207. * @param {Loader} loader - The loader that has finished loading resources.
  208. */
  209. /**
  210. * Options for a call to `.add()`.
  211. *
  212. * @see Loader#add
  213. *
  214. * @typedef {object} IAddOptions
  215. * @property {string} [name] - The name of the resource to load, if not passed the url is used.
  216. * @property {string} [key] - Alias for `name`.
  217. * @property {string} [url] - The url for this resource, relative to the baseUrl of this loader.
  218. * @property {string|boolean} [crossOrigin] - Is this request cross-origin? Default is to
  219. * determine automatically.
  220. * @property {number} [timeout=0] - A timeout in milliseconds for the load. If the load takes
  221. * longer than this time it is cancelled and the load is considered a failure. If this value is
  222. * set to `0` then there is no explicit timeout.
  223. * @property {Resource.LOAD_TYPE} [loadType=Resource.LOAD_TYPE.XHR] - How should this resource
  224. * be loaded?
  225. * @property {Resource.XHR_RESPONSE_TYPE} [xhrType=Resource.XHR_RESPONSE_TYPE.DEFAULT] - How
  226. * should the data being loaded be interpreted when using XHR?
  227. * @property {Resource.OnCompleteSignal} [onComplete] - Callback to add an an onComplete signal istener.
  228. * @property {Resource.OnCompleteSignal} [callback] - Alias for `onComplete`.
  229. * @property {Resource.IMetadata} [metadata] - Extra configuration for middleware and the Resource object.
  230. */
  231. /* eslint-disable require-jsdoc,valid-jsdoc */
  232. /**
  233. * Adds a resource (or multiple resources) to the loader queue.
  234. *
  235. * This function can take a wide variety of different parameters. The only thing that is always
  236. * required the url to load. All the following will work:
  237. *
  238. * ```js
  239. * loader
  240. * // normal param syntax
  241. * .add('key', 'http://...', function () {})
  242. * .add('http://...', function () {})
  243. * .add('http://...')
  244. *
  245. * // object syntax
  246. * .add({
  247. * name: 'key2',
  248. * url: 'http://...'
  249. * }, function () {})
  250. * .add({
  251. * url: 'http://...'
  252. * }, function () {})
  253. * .add({
  254. * name: 'key3',
  255. * url: 'http://...'
  256. * onComplete: function () {}
  257. * })
  258. * .add({
  259. * url: 'https://...',
  260. * onComplete: function () {},
  261. * crossOrigin: true
  262. * })
  263. *
  264. * // you can also pass an array of objects or urls or both
  265. * .add([
  266. * { name: 'key4', url: 'http://...', onComplete: function () {} },
  267. * { url: 'http://...', onComplete: function () {} },
  268. * 'http://...'
  269. * ])
  270. *
  271. * // and you can use both params and options
  272. * .add('key', 'http://...', { crossOrigin: true }, function () {})
  273. * .add('http://...', { crossOrigin: true }, function () {});
  274. * ```
  275. *
  276. * @function
  277. * @variation 1
  278. * @param {string} name - The name of the resource to load.
  279. * @param {string} url - The url for this resource, relative to the baseUrl of this loader.
  280. * @param {Resource.OnCompleteSignal} [callback] - Function to call when this specific resource completes loading.
  281. * @return {this} Returns itself.
  282. */ /**
  283. * @function
  284. * @variation 2
  285. * @param {string} name - The name of the resource to load.
  286. * @param {string} url - The url for this resource, relative to the baseUrl of this loader.
  287. * @param {IAddOptions} [options] - The options for the load.
  288. * @param {Resource.OnCompleteSignal} [callback] - Function to call when this specific resource completes loading.
  289. * @return {this} Returns itself.
  290. */ /**
  291. * @function
  292. * @variation 3
  293. * @param {string} url - The url for this resource, relative to the baseUrl of this loader.
  294. * @param {Resource.OnCompleteSignal} [callback] - Function to call when this specific resource completes loading.
  295. * @return {this} Returns itself.
  296. */ /**
  297. * @function
  298. * @variation 4
  299. * @param {string} url - The url for this resource, relative to the baseUrl of this loader.
  300. * @param {IAddOptions} [options] - The options for the load.
  301. * @param {Resource.OnCompleteSignal} [callback] - Function to call when this specific resource completes loading.
  302. * @return {this} Returns itself.
  303. */ /**
  304. * @function
  305. * @variation 5
  306. * @param {IAddOptions} options - The options for the load. This object must contain a `url` property.
  307. * @param {Resource.OnCompleteSignal} [callback] - Function to call when this specific resource completes loading.
  308. * @return {this} Returns itself.
  309. */ /**
  310. * @function
  311. * @variation 6
  312. * @param {Array<IAddOptions|string>} resources - An array of resources to load, where each is
  313. * either an object with the options or a string url. If you pass an object, it must contain a `url` property.
  314. * @param {Resource.OnCompleteSignal} [callback] - Function to call when this specific resource completes loading.
  315. * @return {this} Returns itself.
  316. */
  317. Loader.prototype.add = function add(name, url, options, cb) {
  318. // special case of an array of objects or urls
  319. if (Array.isArray(name)) {
  320. for (var i = 0; i < name.length; ++i) {
  321. this.add(name[i]);
  322. }
  323. return this;
  324. }
  325. // if an object is passed instead of params
  326. if ((typeof name === 'undefined' ? 'undefined' : _typeof(name)) === 'object') {
  327. cb = url || name.callback || name.onComplete;
  328. options = name;
  329. url = name.url;
  330. name = name.name || name.key || name.url;
  331. }
  332. // case where no name is passed shift all args over by one.
  333. if (typeof url !== 'string') {
  334. cb = options;
  335. options = url;
  336. url = name;
  337. }
  338. // now that we shifted make sure we have a proper url.
  339. if (typeof url !== 'string') {
  340. throw new Error('No url passed to add resource to loader.');
  341. }
  342. // options are optional so people might pass a function and no options
  343. if (typeof options === 'function') {
  344. cb = options;
  345. options = null;
  346. }
  347. // if loading already you can only add resources that have a parent.
  348. if (this.loading && (!options || !options.parentResource)) {
  349. throw new Error('Cannot add resources while the loader is running.');
  350. }
  351. // check if resource already exists.
  352. if (this.resources[name]) {
  353. throw new Error('Resource named "' + name + '" already exists.');
  354. }
  355. // add base url if this isn't an absolute url
  356. url = this._prepareUrl(url);
  357. // create the store the resource
  358. this.resources[name] = new _Resource.Resource(name, url, options);
  359. if (typeof cb === 'function') {
  360. this.resources[name].onAfterMiddleware.once(cb);
  361. }
  362. // if actively loading, make sure to adjust progress chunks for that parent and its children
  363. if (this.loading) {
  364. var parent = options.parentResource;
  365. var incompleteChildren = [];
  366. for (var _i2 = 0; _i2 < parent.children.length; ++_i2) {
  367. if (!parent.children[_i2].isComplete) {
  368. incompleteChildren.push(parent.children[_i2]);
  369. }
  370. }
  371. var fullChunk = parent.progressChunk * (incompleteChildren.length + 1); // +1 for parent
  372. var eachChunk = fullChunk / (incompleteChildren.length + 2); // +2 for parent & new child
  373. parent.children.push(this.resources[name]);
  374. parent.progressChunk = eachChunk;
  375. for (var _i3 = 0; _i3 < incompleteChildren.length; ++_i3) {
  376. incompleteChildren[_i3].progressChunk = eachChunk;
  377. }
  378. this.resources[name].progressChunk = eachChunk;
  379. }
  380. // add the resource to the queue
  381. this._queue.push(this.resources[name]);
  382. return this;
  383. };
  384. /* eslint-enable require-jsdoc,valid-jsdoc */
  385. /**
  386. * Sets up a middleware function that will run *before* the
  387. * resource is loaded.
  388. *
  389. * @param {function} fn - The middleware function to register.
  390. * @return {this} Returns itself.
  391. */
  392. Loader.prototype.pre = function pre(fn) {
  393. this._beforeMiddleware.push(fn);
  394. return this;
  395. };
  396. /**
  397. * Sets up a middleware function that will run *after* the
  398. * resource is loaded.
  399. *
  400. * @param {function} fn - The middleware function to register.
  401. * @return {this} Returns itself.
  402. */
  403. Loader.prototype.use = function use(fn) {
  404. this._afterMiddleware.push(fn);
  405. return this;
  406. };
  407. /**
  408. * Resets the queue of the loader to prepare for a new load.
  409. *
  410. * @return {this} Returns itself.
  411. */
  412. Loader.prototype.reset = function reset() {
  413. this.progress = 0;
  414. this.loading = false;
  415. this._queue.kill();
  416. this._queue.pause();
  417. // abort all resource loads
  418. for (var k in this.resources) {
  419. var res = this.resources[k];
  420. if (res._onLoadBinding) {
  421. res._onLoadBinding.detach();
  422. }
  423. if (res.isLoading) {
  424. res.abort();
  425. }
  426. }
  427. this.resources = {};
  428. return this;
  429. };
  430. /**
  431. * Starts loading the queued resources.
  432. *
  433. * @param {function} [cb] - Optional callback that will be bound to the `complete` event.
  434. * @return {this} Returns itself.
  435. */
  436. Loader.prototype.load = function load(cb) {
  437. // register complete callback if they pass one
  438. if (typeof cb === 'function') {
  439. this.onComplete.once(cb);
  440. }
  441. // if the queue has already started we are done here
  442. if (this.loading) {
  443. return this;
  444. }
  445. if (this._queue.idle()) {
  446. this._onStart();
  447. this._onComplete();
  448. } else {
  449. // distribute progress chunks
  450. var numTasks = this._queue._tasks.length;
  451. var chunk = MAX_PROGRESS / numTasks;
  452. for (var i = 0; i < this._queue._tasks.length; ++i) {
  453. this._queue._tasks[i].data.progressChunk = chunk;
  454. }
  455. // notify we are starting
  456. this._onStart();
  457. // start loading
  458. this._queue.resume();
  459. }
  460. return this;
  461. };
  462. /**
  463. * The number of resources to load concurrently.
  464. *
  465. * @member {number}
  466. * @default 10
  467. */
  468. /**
  469. * Prepares a url for usage based on the configuration of this object
  470. *
  471. * @private
  472. * @param {string} url - The url to prepare.
  473. * @return {string} The prepared url.
  474. */
  475. Loader.prototype._prepareUrl = function _prepareUrl(url) {
  476. var parsedUrl = (0, _parseUri2.default)(url, { strictMode: true });
  477. var result = void 0;
  478. // absolute url, just use it as is.
  479. if (parsedUrl.protocol || !parsedUrl.path || url.indexOf('//') === 0) {
  480. result = url;
  481. }
  482. // if baseUrl doesn't end in slash and url doesn't start with slash, then add a slash inbetween
  483. else if (this.baseUrl.length && this.baseUrl.lastIndexOf('/') !== this.baseUrl.length - 1 && url.charAt(0) !== '/') {
  484. result = this.baseUrl + '/' + url;
  485. } else {
  486. result = this.baseUrl + url;
  487. }
  488. // if we need to add a default querystring, there is a bit more work
  489. if (this.defaultQueryString) {
  490. var hash = rgxExtractUrlHash.exec(result)[0];
  491. result = result.substr(0, result.length - hash.length);
  492. if (result.indexOf('?') !== -1) {
  493. result += '&' + this.defaultQueryString;
  494. } else {
  495. result += '?' + this.defaultQueryString;
  496. }
  497. result += hash;
  498. }
  499. return result;
  500. };
  501. /**
  502. * Loads a single resource.
  503. *
  504. * @private
  505. * @param {Resource} resource - The resource to load.
  506. * @param {function} dequeue - The function to call when we need to dequeue this item.
  507. */
  508. Loader.prototype._loadResource = function _loadResource(resource, dequeue) {
  509. var _this2 = this;
  510. resource._dequeue = dequeue;
  511. // run before middleware
  512. async.eachSeries(this._beforeMiddleware, function (fn, next) {
  513. fn.call(_this2, resource, function () {
  514. // if the before middleware marks the resource as complete,
  515. // break and don't process any more before middleware
  516. next(resource.isComplete ? {} : null);
  517. });
  518. }, function () {
  519. if (resource.isComplete) {
  520. _this2._onLoad(resource);
  521. } else {
  522. resource._onLoadBinding = resource.onComplete.once(_this2._onLoad, _this2);
  523. resource.load();
  524. }
  525. }, true);
  526. };
  527. /**
  528. * Called once loading has started.
  529. *
  530. * @private
  531. */
  532. Loader.prototype._onStart = function _onStart() {
  533. this.progress = 0;
  534. this.loading = true;
  535. this.onStart.dispatch(this);
  536. };
  537. /**
  538. * Called once each resource has loaded.
  539. *
  540. * @private
  541. */
  542. Loader.prototype._onComplete = function _onComplete() {
  543. this.progress = MAX_PROGRESS;
  544. this.loading = false;
  545. this.onComplete.dispatch(this, this.resources);
  546. };
  547. /**
  548. * Called each time a resources is loaded.
  549. *
  550. * @private
  551. * @param {Resource} resource - The resource that was loaded
  552. */
  553. Loader.prototype._onLoad = function _onLoad(resource) {
  554. var _this3 = this;
  555. resource._onLoadBinding = null;
  556. // remove this resource from the async queue, and add it to our list of resources that are being parsed
  557. this._resourcesParsing.push(resource);
  558. resource._dequeue();
  559. // run all the after middleware for this resource
  560. async.eachSeries(this._afterMiddleware, function (fn, next) {
  561. fn.call(_this3, resource, next);
  562. }, function () {
  563. resource.onAfterMiddleware.dispatch(resource);
  564. _this3.progress = Math.min(MAX_PROGRESS, _this3.progress + resource.progressChunk);
  565. _this3.onProgress.dispatch(_this3, resource);
  566. if (resource.error) {
  567. _this3.onError.dispatch(resource.error, _this3, resource);
  568. } else {
  569. _this3.onLoad.dispatch(_this3, resource);
  570. }
  571. _this3._resourcesParsing.splice(_this3._resourcesParsing.indexOf(resource), 1);
  572. // do completion check
  573. if (_this3._queue.idle() && _this3._resourcesParsing.length === 0) {
  574. _this3._onComplete();
  575. }
  576. }, true);
  577. };
  578. _createClass(Loader, [{
  579. key: 'concurrency',
  580. get: function get() {
  581. return this._queue.concurrency;
  582. }
  583. // eslint-disable-next-line require-jsdoc
  584. ,
  585. set: function set(concurrency) {
  586. this._queue.concurrency = concurrency;
  587. }
  588. }]);
  589. return Loader;
  590. }();
  591. /**
  592. * A default array of middleware to run before loading each resource.
  593. * Each of these middlewares are added to any new Loader instances when they are created.
  594. *
  595. * @private
  596. * @member {function[]}
  597. */
  598. Loader._defaultBeforeMiddleware = [];
  599. /**
  600. * A default array of middleware to run after loading each resource.
  601. * Each of these middlewares are added to any new Loader instances when they are created.
  602. *
  603. * @private
  604. * @member {function[]}
  605. */
  606. Loader._defaultAfterMiddleware = [];
  607. /**
  608. * Sets up a middleware function that will run *before* the
  609. * resource is loaded.
  610. *
  611. * @static
  612. * @param {function} fn - The middleware function to register.
  613. * @return {Loader} Returns itself.
  614. */
  615. Loader.pre = function LoaderPreStatic(fn) {
  616. Loader._defaultBeforeMiddleware.push(fn);
  617. return Loader;
  618. };
  619. /**
  620. * Sets up a middleware function that will run *after* the
  621. * resource is loaded.
  622. *
  623. * @static
  624. * @param {function} fn - The middleware function to register.
  625. * @return {Loader} Returns itself.
  626. */
  627. Loader.use = function LoaderUseStatic(fn) {
  628. Loader._defaultAfterMiddleware.push(fn);
  629. return Loader;
  630. };
  631. //# sourceMappingURL=Loader.js.map