├── .editorconfig ├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── rollup.config.js ├── src ├── Loader.ts ├── Resource.ts ├── async │ ├── AsyncQueue.ts │ └── eachSeries.ts ├── bundle.ts ├── index.ts ├── load_strategies │ ├── AbstractLoadStrategy.ts │ ├── AudioLoadStrategy.ts │ ├── ImageLoadStrategy.ts │ ├── MediaElementLoadStrategy.ts │ ├── VideoLoadStrategy.ts │ └── XhrLoadStrategy.ts ├── resource_type.ts └── utilities.ts ├── test ├── data │ ├── hud.json │ ├── hud.png │ ├── hud2.json │ └── hud2.png ├── fixtures │ ├── data.js │ └── spritesheetMiddleware.js ├── karma.conf.js └── spec │ ├── Loader.test.js │ ├── Resource.test.js │ └── async.test.js ├── tsconfig.json └── types └── parse-uri.d.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs. 2 | # More information at http://EditorConfig.org 3 | root = true 4 | 5 | [*] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | indent_style = space 10 | indent_size = 4 11 | 12 | [{package.json,bower.json,*.yml}] 13 | indent_size = 2 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # sublime text files 2 | *.sublime* 3 | *.*~*.TMP 4 | 5 | # temp files 6 | .DS_Store 7 | Thumbs.db 8 | Desktop.ini 9 | npm-debug.log 10 | 11 | # project files 12 | .project 13 | .idea 14 | 15 | # vim swap files 16 | *.sw* 17 | 18 | # emacs temp files 19 | *~ 20 | \#*# 21 | 22 | # project ignores 23 | !.gitkeep 24 | *__temp 25 | *.sqlite 26 | .snyk 27 | .commit 28 | entry-*.js 29 | node_modules/ 30 | dist/ 31 | lib/ 32 | docs/ 33 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | addons: 4 | firefox: "latest" 5 | 6 | language: node_js 7 | 8 | node_js: 9 | - '10' 10 | - '12' 11 | 12 | branches: 13 | only: 14 | - master 15 | 16 | cache: 17 | directories: 18 | - node_modules 19 | 20 | install: 21 | - npm install 22 | 23 | before_script: 24 | - export DISPLAY=':99.0' 25 | - Xvfb :99 -screen 0 1024x768x24 -extension RANDR & 26 | 27 | script: 28 | - npm test 29 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | Please read this short guide to contributing before performing pull requests or reporting issues. The purpose 4 | of this guide is to ensure the best experience for all involved and make development as smooth as possible. 5 | 6 | 7 | ## Reporting issues 8 | 9 | To report a bug, request a feature, or even ask a question, make use of the [GitHub Issues][10] in this repo. 10 | When submitting an issue please take the following steps: 11 | 12 | **1. Search for existing issues.** Your bug may have already been fixed or addressed in an unreleased version, so 13 | be sure to search the issues first before putting in a duplicate issue. 14 | 15 | **2. Create an isolated and reproducible test case.** If you are reporting a bug, make sure you also have a minimal, 16 | runnable, code example that reproduces the problem you have. 17 | 18 | **3. Include a live example.** After narrowing your code down to only the problem areas, make use of [jsFiddle][11], 19 | [jsBin][12], or a link to your live site so that we can view a live example of the problem. 20 | 21 | **4. Share as much information as possible.** Include browser/node version affected, your OS, version of the library, 22 | steps to reproduce, etc. "X isn't working!!!1!" will probably just be closed. 23 | 24 | [10]: https://github.com/englercj/asset-loader/issues 25 | [11]: http://jsfiddle.net 26 | [12]: http://jsbin.com/ 27 | 28 | 29 | ## Making Changes 30 | 31 | To build the library you will need to download node.js from [nodejs.org][20]. After it has been installed open a 32 | console and run `npm install -g gulp` to install the global `gulp` executable. 33 | 34 | After that you can clone the repository and run `npm install` inside the cloned folder. This will install 35 | dependencies necessary for building the project. You can rebuild the project by running `gulp` in the cloned 36 | folder. 37 | 38 | Once that is ready, you can make your changes and submit a Pull Request: 39 | 40 | - **Send Pull Requests to the `master` branch.** All Pull Requests must be sent to the `master` branch, which is where 41 | all "bleeding-edge" development takes place. 42 | 43 | - **Ensure changes are jshint validated.** Our JSHint configuration file is provided in the repository and you 44 | should check against it before submitting. This should happen automatically when running `gulp` in the repo directory. 45 | 46 | - **Never commit new builds.** When making a code change you should always run `gulp` which will rebuild the project 47 | so you can test, *however* please do not commit the new builds placed in `dist/` or your PR will be closed. By default 48 | the `dist/` folder is ignored so this shouldn't happen by accident. 49 | 50 | - **Only commit relevant changes.** Don't include changes that are not directly relevant to the fix you are making. 51 | The more focused a PR is, the faster it will get attention and be merged. Extra files changing only whitespace or 52 | trash files will likely get your PR closed. 53 | 54 | [20]: http://nodejs.org 55 | 56 | 57 | ## Quickie Code Style Guide 58 | 59 | Use EditorConfig and JSHint! Both tools will ensure your code is in the required styles! Either way, here are some tips: 60 | 61 | - Use 4 spaces for tabs, never tab characters. 62 | 63 | - No trailing whitespace, blank lines should have no whitespace. 64 | 65 | - Always favor strict equals `===` unless you *need* to use type coercion. 66 | 67 | - Follow conventions already in the code, and listen to jshint. Our config is set-up for a reason. 68 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2015-2019 Chad Engler 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Resource Loader [![Build Status](https://travis-ci.org/englercj/resource-loader.svg?branch=master)](https://travis-ci.org/englercj/resource-loader) 2 | 3 | A generic resource loader, made with web games in mind. 4 | 5 | ## Philosophy 6 | 7 | This library was built to make it easier to load and prepare data asynchronously. The 8 | goal was mainly to unify the many different APIs browsers expose for loading data and 9 | smooth the differences between versions and vendors. 10 | 11 | It is not a goal of this library to be a resource caching and management system, 12 | just a loader. This library is for the actual mechanism of loading data. All 13 | caching, resource management, knowing what is loaded and what isn't, deciding 14 | what to load, etc, should all exist as logic outside of this library. 15 | 16 | As a more concrete statement, your project should have a Resource Manager that 17 | stores resources and manages data lifetime. When it decides something needs to be 18 | loaded from a remote source, only then does it create a loader and load them. 19 | 20 | ## Usage 21 | 22 | ```js 23 | // ctor 24 | import { Loader } from 'resource-loader'; 25 | 26 | const loader = new Loader(); 27 | 28 | loader 29 | // Chainable `add` to enqueue a resource 30 | .add(url) 31 | 32 | // Chainable `use` to add a middleware that runs for each resource, *after* loading that resource. 33 | // This is useful to implement custom parsing modules (like spritesheet parsers). 34 | .use((resource, next) => 35 | { 36 | // Be sure to call next() when you have completed your middleware work. 37 | next(); 38 | }) 39 | 40 | // The `load` method loads the queue of resources, and calls the passed in callback called once all 41 | // resources have loaded. 42 | .load((loader, resources) => { 43 | // resources is an object where the key is the name of the resource loaded and the value is the resource object. 44 | // They have a couple default properties: 45 | // - `url`: The URL that the resource was loaded from 46 | // - `error`: The error that happened when trying to load (if any) 47 | // - `data`: The raw data that was loaded 48 | // also may contain other properties based on the middleware that runs. 49 | }); 50 | 51 | // Throughout the process multiple signals can be dispatched. 52 | loader.onStart.add(() => {}); // Called when a resource starts loading. 53 | loader.onError.add(() => {}); // Called when a resource fails to load. 54 | loader.onLoad.add(() => {}); // Called when a resource successfully loads. 55 | loader.onProgress.add(() => {}); // Called when a resource finishes loading (success or fail). 56 | loader.onComplete.add(() => {}); // Called when all resources have finished loading. 57 | ``` 58 | 59 | ## Building 60 | 61 | You will need to have [node][node] setup on your machine. 62 | 63 | Then you can install dependencies and build: 64 | 65 | ```js 66 | npm i && npm run build 67 | ``` 68 | 69 | That will output the built distributables to `./dist`. 70 | 71 | [node]: http://nodejs.org/ 72 | 73 | ## Supported Browsers 74 | 75 | - IE 9+ 76 | - FF 13+ 77 | - Chrome 20+ 78 | - Safari 6+ 79 | - Opera 12.1+ 80 | 81 | ## Upgrading to v4 82 | 83 | - Before middleware has been removed, so no more `pre` function. 84 | * If you used `pre` middleware for url parsing, use the new `urlResolver` property instead. 85 | - `crossOrigin` must now be a string if specified. 86 | - `Resource.LOAD_TYPE` enum replaced with Load Strategies. 87 | * For example, `loadType: Resource.LOAD_TYPE.IMAGE` is now `strategy: Loader.ImageLoadStrategy`. 88 | - `Resource.XHR_RESPONSE_TYPE` enum replaced with `XhrLoadStrategy.ResponseType`. 89 | * For example, `xhrType: Resource.XHR_RESPONSE_TYPE.DOCUMENT` is now `xhrType: Loader.XhrLoadStrategy.ResponseType.Document`. 90 | - Overloads for the `add` function have been simplified. 91 | * The removed overloads were not widely used. See the docs for what is now valid. 92 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "resource-loader", 3 | "version": "4.0.0-rc4", 4 | "main": "./dist/resource-loader.cjs.js", 5 | "module": "./dist/resource-loader.esm.js", 6 | "bundle": "./dist/resource-loader.js", 7 | "types": "./dist/index.d.ts", 8 | "description": "A generic asset loader, made with web games in mind.", 9 | "author": "Chad Engler ", 10 | "license": "MIT", 11 | "homepage": "https://github.com/englercj/resource-loader", 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/englercj/resource-loader.git" 15 | }, 16 | "bugs": { 17 | "url": "https://github.com/englercj/resource-loader/issues" 18 | }, 19 | "keywords": [], 20 | "files": [ 21 | "dist", 22 | "typings", 23 | "package.json", 24 | "CONTRIBUTING.md", 25 | "LICENSE", 26 | "README.md" 27 | ], 28 | "scripts": { 29 | "clean": "rimraf ./dist", 30 | "prebuild": "npm run clean", 31 | "build": "rollup -c", 32 | "watch": "rollup -cw", 33 | "start": "npm run build", 34 | "test": "npm run test-dev -- --single-run", 35 | "pretest-dev": "npm run build", 36 | "test-dev": "karma start test/karma.conf.js", 37 | "docs": "typedoc", 38 | "prepublishOnly": "npm run build", 39 | "predeploy": "rimraf ./docs && npm run docs", 40 | "deploy": "gh-pages -d docs", 41 | "postpublish": "npm run deploy" 42 | }, 43 | "dependencies": { 44 | "parse-uri": "^1.0.0", 45 | "type-signals": "^1.0.3" 46 | }, 47 | "devDependencies": { 48 | "@rollup/plugin-commonjs": "^11.0.2", 49 | "@rollup/plugin-node-resolve": "^7.1.1", 50 | "chai": "^4.2.0", 51 | "gh-pages": "^2.2.0", 52 | "karma": "^4.4.1", 53 | "karma-chrome-launcher": "^3.1.0", 54 | "karma-firefox-launcher": "^1.3.0", 55 | "karma-mocha": "^1.3.0", 56 | "karma-mocha-reporter": "^2.2.5", 57 | "karma-sinon-chai": "^2.0.2", 58 | "mkdirp": "^1.0.3", 59 | "mocha": "^7.1.0", 60 | "npm-run-all": "^4.1.5", 61 | "rimraf": "^3.0.2", 62 | "rollup": "^1.32.1", 63 | "rollup-plugin-terser": "^5.2.0", 64 | "rollup-plugin-typescript2": "^0.26.0", 65 | "sinon": "^9.0.0", 66 | "sinon-chai": "^3.5.0", 67 | "tslib": "^1.11.1", 68 | "typedoc": "^0.16.11", 69 | "typescript": "^3.8.3" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from 'rollup-plugin-typescript2'; 2 | import resolve from '@rollup/plugin-node-resolve'; 3 | import commonjs from '@rollup/plugin-commonjs'; 4 | import { terser } from 'rollup-plugin-terser'; 5 | import pkg from './package.json'; 6 | 7 | const plugins = [ 8 | typescript(), 9 | resolve(), 10 | commonjs(), 11 | ]; 12 | const sourcemap = true; 13 | const freeze = false; 14 | const input = 'src/index.ts'; 15 | const bundleInput = 'src/bundle.ts'; 16 | const external = Object.keys(pkg.dependencies); 17 | const compiled = (new Date()).toUTCString().replace(/GMT/g, "UTC"); 18 | 19 | const banner = `/*! 20 | * ${pkg.name} - v${pkg.version} 21 | * ${pkg.homepage} 22 | * Compiled ${compiled} 23 | * 24 | * ${pkg.name} is licensed under the MIT license. 25 | * http://www.opensource.org/licenses/mit-license 26 | */`; 27 | 28 | export default [ 29 | { 30 | input, 31 | plugins, 32 | external, 33 | output: [ 34 | { 35 | banner, 36 | file: 'dist/resource-loader.cjs.js', 37 | format: 'cjs', 38 | freeze, 39 | sourcemap, 40 | }, 41 | { 42 | banner, 43 | file: 'dist/resource-loader.esm.js', 44 | format: 'esm', 45 | freeze, 46 | sourcemap, 47 | } 48 | ] 49 | }, 50 | { 51 | input: bundleInput, 52 | plugins, 53 | output: { 54 | banner, 55 | name: 'Loader', 56 | file: 'dist/resource-loader.js', 57 | format: 'iife', 58 | freeze, 59 | sourcemap, 60 | } 61 | }, 62 | { 63 | input: bundleInput, 64 | plugins: [].concat(plugins, terser({ 65 | output: { 66 | comments(node, comment) { 67 | return comment.line === 1; 68 | }, 69 | }, 70 | compress: { 71 | drop_console: true, 72 | }, 73 | })), 74 | output: { 75 | banner, 76 | name: 'Loader', 77 | file: 'dist/resource-loader.min.js', 78 | format: 'iife', 79 | freeze, 80 | sourcemap, 81 | } 82 | } 83 | ]; 84 | -------------------------------------------------------------------------------- /src/Loader.ts: -------------------------------------------------------------------------------- 1 | import parseUri from 'parse-uri'; 2 | import { Signal } from 'type-signals'; 3 | import { AsyncQueue } from './async/AsyncQueue'; 4 | import { Resource, IResourceOptions } from './Resource'; 5 | import { eachSeries } from './async/eachSeries'; 6 | 7 | // some constants 8 | const MAX_PROGRESS = 100; 9 | const rgxExtractUrlHash = /(#[\w-]+)?$/; 10 | 11 | /** 12 | * @category Type Aliases 13 | */ 14 | export namespace Loader 15 | { 16 | export type ResourceMap = Partial>; 17 | 18 | export type OnProgressSignal = (loader: Loader, resource: Resource) => void; 19 | export type OnErrorSignal = (errMessage: string, loader: Loader, resource: Resource) => void; 20 | export type OnLoadSignal = (loader: Loader, resource: Resource) => void; 21 | export type OnStartSignal = (loader: Loader) => void; 22 | export type OnCompleteSignal = (loader: Loader, resources: ResourceMap) => void; 23 | 24 | export type MiddlewareFn = (resource: Resource, next: () => void) => void; 25 | export type UrlResolverFn = (url: string, parsed: ReturnType) => string; 26 | } 27 | 28 | interface Middleware 29 | { 30 | fn: Loader.MiddlewareFn; 31 | priority: number; 32 | } 33 | 34 | /** 35 | * Options for a call to `.add()`. 36 | */ 37 | export interface IAddOptions extends IResourceOptions 38 | { 39 | // Extra values to be used by specific load strategies. 40 | [key: string]: any; 41 | 42 | // The url to load the resource from. 43 | url: string; 44 | 45 | // A base url to use for just this resource load. 46 | baseUrl?: string; 47 | 48 | // The name of the resource to load, if not passed the url is used. 49 | name?: string; 50 | 51 | // Callback to add an an onComplete signal istener. 52 | onComplete?: Resource.OnCompleteSignal; 53 | 54 | // Parent resource this newly added resource is a child of. 55 | parentResource?: Resource; 56 | } 57 | 58 | /** 59 | * Manages the state and loading of multiple resources to load. 60 | * @preferred 61 | */ 62 | export class Loader 63 | { 64 | /** 65 | * The default middleware priority (50). 66 | */ 67 | static readonly DefaultMiddlewarePriority = 50; 68 | 69 | /** 70 | * The progress percent of the loader going through the queue. 71 | */ 72 | progress = 0; 73 | 74 | /** 75 | * Loading state of the loader, true if it is currently loading resources. 76 | */ 77 | loading = false; 78 | 79 | /** 80 | * A querystring to append to every URL added to the loader. 81 | * 82 | * This should be a valid query string *without* the question-mark (`?`). The loader will 83 | * also *not* escape values for you. Make sure to escape your parameters with 84 | * [`encodeURIComponent`](https://mdn.io/encodeURIComponent) before assigning this property. 85 | * 86 | * @example 87 | * const loader = new Loader(); 88 | * 89 | * loader.defaultQueryString = 'user=me&password=secret'; 90 | * 91 | * // This will request 'image.png?user=me&password=secret' 92 | * loader.add('image.png').load(); 93 | * 94 | * loader.reset(); 95 | * 96 | * // This will request 'image.png?v=1&user=me&password=secret' 97 | * loader.add('iamge.png?v=1').load(); 98 | */ 99 | defaultQueryString = ''; 100 | 101 | /** 102 | * All the resources for this loader keyed by name, or URL if no name was given. 103 | */ 104 | resources: Loader.ResourceMap = {}; 105 | 106 | /** 107 | * Dispatched once per errored resource. 108 | */ 109 | readonly onError: Signal = new Signal(); 110 | 111 | /** 112 | * Dispatched once per loaded resource. 113 | */ 114 | readonly onLoad: Signal = new Signal(); 115 | 116 | /** 117 | * Dispatched when the loader begins to process the queue. 118 | */ 119 | readonly onStart: Signal = new Signal(); 120 | 121 | /** 122 | * Dispatched when the queued resources all load. 123 | */ 124 | readonly onComplete: Signal = new Signal(); 125 | 126 | /** 127 | * Dispatched once per loaded or errored resource. 128 | */ 129 | readonly onProgress: Signal = new Signal(); 130 | 131 | /** 132 | * The base url for all resources loaded by this loader. 133 | */ 134 | private _baseUrl = ''; 135 | 136 | /** 137 | * The internal list of URL resolver functions called within `_prepareUrl`. 138 | */ 139 | private _urlResolvers: Loader.UrlResolverFn[] = []; 140 | 141 | /** 142 | * The middleware to run after loading each resource. 143 | */ 144 | private _middleware: Middleware[] = []; 145 | 146 | /** 147 | * The tracks the resources we are currently completing parsing for. 148 | */ 149 | private _resourcesParsing: Resource[] = []; 150 | 151 | /** 152 | * The `_loadResource` function bound with this object context. 153 | */ 154 | private _boundLoadResource = this._loadResource.bind(this); 155 | 156 | /** 157 | * The resources waiting to be loaded. 158 | */ 159 | private _queue: AsyncQueue; 160 | 161 | /** 162 | * @param baseUrl The base url for all resources loaded by this loader. 163 | * @param concurrency The number of resources to load concurrently. 164 | */ 165 | constructor(baseUrl = '', concurrency = 10) 166 | { 167 | this.baseUrl = baseUrl; 168 | 169 | this._queue = new AsyncQueue(this._boundLoadResource, concurrency); 170 | this._queue.pause(); 171 | 172 | // Add default middleware. This is already sorted so no need to do that again. 173 | this._middleware = Loader._defaultMiddleware.slice(); 174 | } 175 | 176 | /** 177 | * The base url for all resources loaded by this loader. 178 | * Any trailing slashes are trimmed off. 179 | */ 180 | get baseUrl(): string { return this._baseUrl; } 181 | 182 | set baseUrl(url: string) 183 | { 184 | while (url.length && url.charAt(url.length - 1) === '/') 185 | { 186 | url = url.slice(0, -1); 187 | } 188 | 189 | this._baseUrl = url; 190 | } 191 | 192 | /** 193 | * Adds a resource (or multiple resources) to the loader queue. 194 | * 195 | * This function can take a wide variety of different parameters. The only thing that is always 196 | * required the url to load. All the following will work: 197 | * 198 | * ```js 199 | * loader 200 | * // name & url param syntax 201 | * .add('http://...') 202 | * .add('key', 'http://...') 203 | * 204 | * // object syntax 205 | * .add({ 206 | * name: 'key3', 207 | * url: 'http://...', 208 | * onComplete: function () {}, 209 | * }) 210 | * 211 | * // you can also pass an array of objects or urls or both 212 | * .add([ 213 | * { name: 'key4', url: 'http://...', onComplete: function () {} }, 214 | * { url: 'http://...', onComplete: function () {} }, 215 | * 'http://...' 216 | * ]) 217 | * ``` 218 | */ 219 | add(url: string): this; 220 | add(name: string, url: string): this; 221 | add(options: IAddOptions): this; 222 | add(resources: (IAddOptions|string)[]): this; 223 | add(options: string | IAddOptions | (string | IAddOptions)[], url_?: string): this 224 | { 225 | // An array is a resource list. 226 | if (Array.isArray(options)) 227 | { 228 | for (let i = 0; i < options.length; ++i) 229 | { 230 | // can be string or IAddOptions, but either one is fine to pass 231 | // as a param alone. The type assertion is just to appease TS. 232 | this.add(options[i] as string); 233 | } 234 | 235 | return this; 236 | } 237 | 238 | let url = ''; 239 | let name = ''; 240 | let baseUrl = this._baseUrl; 241 | let resOptions: IAddOptions = { url: '' }; 242 | 243 | if (typeof options === 'object') 244 | { 245 | url = options.url; 246 | name = options.name || options.url; 247 | baseUrl = options.baseUrl || baseUrl; 248 | resOptions = options; 249 | } 250 | else 251 | { 252 | name = options; 253 | 254 | if (typeof url_ === 'string') 255 | url = url_; 256 | else 257 | url = name; 258 | } 259 | 260 | if (!url) 261 | throw new Error('You must specify the `url` property.'); 262 | 263 | // if loading already you can only add resources that have a parent. 264 | if (this.loading && !resOptions.parentResource) 265 | { 266 | throw new Error('Cannot add root resources while the loader is running.'); 267 | } 268 | 269 | // check if resource already exists. 270 | if (this.resources[name]) 271 | { 272 | throw new Error(`Resource named "${name}" already exists.`); 273 | } 274 | 275 | // add base url if this isn't an absolute url 276 | url = this._prepareUrl(url, baseUrl); 277 | resOptions.url = url; 278 | 279 | const resource = new Resource(name, resOptions); 280 | 281 | this.resources[name] = resource; 282 | 283 | if (typeof resOptions.onComplete === 'function') 284 | { 285 | resource.onAfterMiddleware.once(resOptions.onComplete); 286 | } 287 | 288 | // if actively loading, make sure to adjust progress chunks for that parent and its children 289 | if (this.loading) 290 | { 291 | const parent = resOptions.parentResource!; 292 | const incompleteChildren: Resource[] = []; 293 | 294 | for (let i = 0; i < parent.children.length; ++i) 295 | { 296 | if (!parent.children[i].isComplete) 297 | { 298 | incompleteChildren.push(parent.children[i]); 299 | } 300 | } 301 | 302 | const fullChunk = parent.progressChunk * (incompleteChildren.length + 1); // +1 for parent 303 | const eachChunk = fullChunk / (incompleteChildren.length + 2); // +2 for parent & new child 304 | 305 | parent.children.push(resource); 306 | parent.progressChunk = eachChunk; 307 | 308 | for (let i = 0; i < incompleteChildren.length; ++i) 309 | { 310 | incompleteChildren[i].progressChunk = eachChunk; 311 | } 312 | 313 | resource.progressChunk = eachChunk; 314 | } 315 | 316 | // add the resource to the queue 317 | this._queue.push(resource); 318 | 319 | return this; 320 | } 321 | 322 | /** 323 | * Sets up a middleware function that will run *after* the 324 | * resource is loaded. 325 | * 326 | * You can optionally specify a priority for this middleware 327 | * which will determine the order middleware functions are run. 328 | * A lower priority value will make the function run earlier. 329 | * That is, priority 30 is run before priority 50. 330 | */ 331 | use(fn: Loader.MiddlewareFn, priority: number = Loader.DefaultMiddlewarePriority): this 332 | { 333 | this._middleware.push({ fn, priority }); 334 | this._middleware.sort((a, b) => a.priority - b.priority); 335 | return this; 336 | } 337 | 338 | /** 339 | * Resets the queue of the loader to prepare for a new load. 340 | */ 341 | reset(): this 342 | { 343 | this.progress = 0; 344 | this.loading = false; 345 | 346 | this._queue.reset(); 347 | this._queue.pause(); 348 | 349 | // abort all resource loads 350 | for (const k in this.resources) 351 | { 352 | const res = this.resources[k]; 353 | 354 | if (!res) 355 | continue; 356 | 357 | if (res._onCompleteBinding) 358 | res._onCompleteBinding.detach(); 359 | 360 | if (res.isLoading) 361 | res.abort(); 362 | } 363 | 364 | this.resources = {}; 365 | 366 | return this; 367 | } 368 | 369 | /** 370 | * Starts loading the queued resources. 371 | */ 372 | load(cb?: Loader.OnCompleteSignal): this 373 | { 374 | if (typeof cb === 'function') 375 | this.onComplete.once(cb); 376 | 377 | // if the queue has already started we are done here 378 | if (this.loading) 379 | return this; 380 | 381 | if (this._queue.idle()) 382 | { 383 | this._onStart(); 384 | this._onComplete(); 385 | } 386 | else 387 | { 388 | // distribute progress chunks 389 | const numTasks = this._queue.length(); 390 | const chunk = MAX_PROGRESS / numTasks; 391 | 392 | for (let i = 0; i < this._queue.length(); ++i) 393 | { 394 | this._queue.getTask(i).data.progressChunk = chunk; 395 | } 396 | 397 | // notify we are starting 398 | this._onStart(); 399 | 400 | // start loading 401 | this._queue.resume(); 402 | } 403 | 404 | return this; 405 | } 406 | 407 | /** 408 | * The number of resources to load concurrently. 409 | */ 410 | get concurrency(): number 411 | { 412 | return this._queue.concurrency; 413 | } 414 | 415 | set concurrency(concurrency) 416 | { 417 | this._queue.concurrency = concurrency; 418 | } 419 | 420 | /** 421 | * Add a function that can be used to modify the url just prior 422 | * to `baseUrl` and `defaultQueryString` being applied. 423 | */ 424 | addUrlResolver(func: Loader.UrlResolverFn): this 425 | { 426 | this._urlResolvers.push(func); 427 | return this; 428 | } 429 | 430 | /** 431 | * Prepares a url for usage based on the configuration of this object 432 | */ 433 | private _prepareUrl(url: string, baseUrl: string): string 434 | { 435 | let parsed = parseUri(url, { strictMode: true }); 436 | 437 | this._urlResolvers.forEach(resolver => { 438 | url = resolver(url, parsed); 439 | parsed = parseUri(url, { strictMode: true }); 440 | }); 441 | 442 | // Only add `baseUrl` for urls that are not absolute. 443 | if (!parsed.protocol && url.indexOf('//') !== 0) 444 | { 445 | // if the url doesn't start with a slash, then add one inbetween. 446 | if (baseUrl.length && url.charAt(0) !== '/') 447 | url = `${baseUrl}/${url}`; 448 | else 449 | url = baseUrl + url; 450 | } 451 | 452 | // if we need to add a default querystring, there is a bit more work 453 | if (this.defaultQueryString) 454 | { 455 | const match = rgxExtractUrlHash.exec(url); 456 | 457 | if (match) 458 | { 459 | const hash = match[0]; 460 | 461 | url = url.substr(0, url.length - hash.length); 462 | 463 | if (url.indexOf('?') !== -1) 464 | url += `&${this.defaultQueryString}`; 465 | else 466 | url += `?${this.defaultQueryString}`; 467 | 468 | url += hash; 469 | } 470 | } 471 | 472 | return url; 473 | } 474 | 475 | /** 476 | * Loads a single resource. 477 | */ 478 | private _loadResource(resource: Resource, dequeue: Function): void 479 | { 480 | resource._dequeue = dequeue; 481 | resource._onCompleteBinding = resource.onComplete.once(this._onLoad, this); 482 | resource.load(); 483 | } 484 | 485 | /** 486 | * Called once loading has started. 487 | */ 488 | private _onStart(): void 489 | { 490 | this.progress = 0; 491 | this.loading = true; 492 | this.onStart.dispatch(this); 493 | } 494 | 495 | /** 496 | * Called once each resource has loaded. 497 | */ 498 | private _onComplete(): void 499 | { 500 | this.progress = MAX_PROGRESS; 501 | this.loading = false; 502 | this.onComplete.dispatch(this, this.resources); 503 | } 504 | 505 | /** 506 | * Called each time a resources is loaded. 507 | */ 508 | private _onLoad(resource: Resource): void 509 | { 510 | resource._onCompleteBinding = null; 511 | 512 | // remove this resource from the async queue, and add it to our list 513 | // of resources that are being parsed 514 | this._resourcesParsing.push(resource); 515 | resource._dequeue(); 516 | 517 | // run all the after middleware for this resource 518 | eachSeries( 519 | this._middleware, 520 | (middleware, next) => 521 | { 522 | middleware.fn.call(this, resource, next); 523 | }, 524 | () => 525 | { 526 | resource.onAfterMiddleware.dispatch(resource); 527 | 528 | this.progress = Math.min(MAX_PROGRESS, this.progress + resource.progressChunk); 529 | this.onProgress.dispatch(this, resource); 530 | 531 | if (resource.error) 532 | this.onError.dispatch(resource.error, this, resource); 533 | else 534 | this.onLoad.dispatch(this, resource); 535 | 536 | this._resourcesParsing.splice(this._resourcesParsing.indexOf(resource), 1); 537 | 538 | // do completion check 539 | if (this._queue.idle() && this._resourcesParsing.length === 0) 540 | this._onComplete(); 541 | }, 542 | true); 543 | } 544 | 545 | /** 546 | * A default array of middleware to run after loading each resource. 547 | * Each of these middlewares are added to any new Loader instances when they are created. 548 | */ 549 | private static _defaultMiddleware: Middleware[] = []; 550 | 551 | /** 552 | * Sets up a middleware function that will run *after* the 553 | * resource is loaded. 554 | * 555 | * You can optionally specify a priority for this middleware 556 | * which will determine the order middleware functions are run. 557 | * A lower priority value will make the function run earlier. 558 | * That is, priority 30 is run before priority 50. 559 | */ 560 | static use(fn: Loader.MiddlewareFn, priority = Loader.DefaultMiddlewarePriority): typeof Loader 561 | { 562 | Loader._defaultMiddleware.push({ fn, priority }); 563 | Loader._defaultMiddleware.sort((a, b) => a.priority - b.priority); 564 | return Loader; 565 | } 566 | } 567 | -------------------------------------------------------------------------------- /src/Resource.ts: -------------------------------------------------------------------------------- 1 | import parseUri from 'parse-uri'; 2 | import { Signal, SignalBinding } from 'type-signals'; 3 | import { AbstractLoadStrategy, ILoadConfig, AbstractLoadStrategyCtor } from './load_strategies/AbstractLoadStrategy'; 4 | import { ImageLoadStrategy } from './load_strategies/ImageLoadStrategy'; 5 | import { AudioLoadStrategy } from './load_strategies/AudioLoadStrategy'; 6 | import { VideoLoadStrategy } from './load_strategies/VideoLoadStrategy'; 7 | import { XhrLoadStrategy } from './load_strategies/XhrLoadStrategy'; 8 | import { ResourceType, ResourceState } from './resource_type'; 9 | import { getExtension } from './utilities'; 10 | 11 | export interface IResourceOptions extends ILoadConfig 12 | { 13 | // OVerride the load strategy to use for this one resource. 14 | strategy?: AbstractLoadStrategy | AbstractLoadStrategyCtor; 15 | 16 | // Extra info added by the user, usually for middleware. 17 | metadata?: any; 18 | } 19 | 20 | /** 21 | * @category Type Aliases 22 | */ 23 | export namespace Resource 24 | { 25 | export type OnStartSignal = (resource: Resource) => void; 26 | export type OnErrorSignal = (resource: Resource) => void; 27 | export type OnCompleteSignal = (resource: Resource) => void; 28 | export type OnProgressSignal = (resource: Resource, percent: number) => void; 29 | } 30 | 31 | /** 32 | * Manages the state and loading of a resource and all child resources. 33 | * @preferred 34 | */ 35 | export class Resource 36 | { 37 | private static _tempAnchor: HTMLAnchorElement | null = null; 38 | 39 | private static _defaultLoadStrategy: AbstractLoadStrategyCtor = XhrLoadStrategy; 40 | private static _loadStrategyMap: Partial> = { 41 | // images 42 | gif: ImageLoadStrategy, 43 | png: ImageLoadStrategy, 44 | bmp: ImageLoadStrategy, 45 | jpg: ImageLoadStrategy, 46 | jpeg: ImageLoadStrategy, 47 | tif: ImageLoadStrategy, 48 | tiff: ImageLoadStrategy, 49 | webp: ImageLoadStrategy, 50 | tga: ImageLoadStrategy, 51 | svg: ImageLoadStrategy, 52 | 'svg+xml': ImageLoadStrategy, // for SVG data urls 53 | 54 | // audio 55 | mp3: AudioLoadStrategy, 56 | ogg: AudioLoadStrategy, 57 | wav: AudioLoadStrategy, 58 | 59 | // videos 60 | mp4: VideoLoadStrategy, 61 | webm: VideoLoadStrategy, 62 | mov: VideoLoadStrategy, 63 | }; 64 | 65 | /** 66 | * Sets the default load stragety to use when there is no extension-specific strategy. 67 | */ 68 | static setDefaultLoadStrategy(strategy: AbstractLoadStrategyCtor): void 69 | { 70 | Resource._defaultLoadStrategy = strategy; 71 | } 72 | 73 | /** 74 | * Sets the load strategy to be used for a specific extension. 75 | * 76 | * @param extname The extension to set the type for, e.g. "png" or "fnt" 77 | * @param strategy The load strategy to use for loading resources with that extension. 78 | */ 79 | static setLoadStrategy(extname: string, strategy: AbstractLoadStrategyCtor): void 80 | { 81 | if (extname && extname.indexOf('.') === 0) 82 | extname = extname.substring(1); 83 | 84 | if (!extname) 85 | return; 86 | 87 | Resource._loadStrategyMap[extname] = strategy; 88 | } 89 | 90 | /** 91 | * The name of this resource. 92 | */ 93 | readonly name: string; 94 | 95 | /** 96 | * The child resources of this resource. 97 | */ 98 | readonly children: Resource[] = []; 99 | 100 | /** 101 | * Dispatched when the resource beings to load. 102 | */ 103 | readonly onStart: Signal = new Signal(); 104 | 105 | /** 106 | * Dispatched each time progress of this resource load updates. 107 | * Not all resources types and loader systems can support this event 108 | * so sometimes it may not be available. If the resource 109 | * is being loaded on a modern browser, using XHR, and the remote server 110 | * properly sets Content-Length headers, then this will be available. 111 | */ 112 | readonly onProgress: Signal = new Signal(); 113 | 114 | /** 115 | * Dispatched once this resource has loaded, if there was an error it will 116 | * be in the `error` property. 117 | */ 118 | readonly onComplete: Signal = new Signal(); 119 | 120 | /** 121 | * Dispatched after this resource has had all the *after* middleware run on it. 122 | */ 123 | readonly onAfterMiddleware: Signal = new Signal(); 124 | 125 | /** 126 | * The data that was loaded by the resource. The type of this member is 127 | * described by the `type` member. 128 | */ 129 | data: any = null; 130 | 131 | /** 132 | * Extra info added by the user, usually for middleware. 133 | */ 134 | metadata: any; 135 | 136 | /** 137 | * Describes the type of the `data` member for this resource. 138 | * 139 | * @see ResourceType 140 | */ 141 | type = ResourceType.Unknown; 142 | 143 | /** 144 | * The error that occurred while loading (if any). 145 | */ 146 | error = ''; 147 | 148 | /** 149 | * The progress chunk owned by this resource. 150 | */ 151 | progressChunk = 0; 152 | 153 | /** 154 | * Storage for use privately by the Loader. 155 | * Do not touch this member. 156 | * 157 | * @ignore 158 | */ 159 | _dequeue: Function = function () {}; 160 | 161 | /** 162 | * Storage for use privately by the Loader. 163 | * Do not touch this member. 164 | * 165 | * @ignore 166 | */ 167 | _onCompleteBinding: SignalBinding | null = null; 168 | 169 | private _strategy: AbstractLoadStrategy; 170 | private _state = ResourceState.NotStarted; 171 | 172 | /** 173 | * @param name The name of the resource to load. 174 | * @param options The options for the load strategy that will be used. 175 | */ 176 | constructor(name: string, options: IResourceOptions) 177 | { 178 | this.name = name; 179 | this.metadata = options.metadata; 180 | 181 | if (typeof options.crossOrigin !== 'string') 182 | options.crossOrigin = this._determineCrossOrigin(options.url); 183 | 184 | if (options.strategy && typeof options.strategy !== 'function') 185 | { 186 | this._strategy = options.strategy; 187 | 188 | // Only `Resource` is allowed to set the config object, 189 | // it is otherwise readonly. 190 | (this._strategy as any).config = options; 191 | } 192 | else 193 | { 194 | let StrategyCtor = options.strategy; 195 | 196 | if (!StrategyCtor) 197 | StrategyCtor = Resource._loadStrategyMap[getExtension(options.url)]; 198 | 199 | if (!StrategyCtor) 200 | StrategyCtor = Resource._defaultLoadStrategy; 201 | 202 | this._strategy = new StrategyCtor(options); 203 | } 204 | 205 | this._strategy.onError.add(this._error, this); 206 | this._strategy.onComplete.add(this._complete, this); 207 | this._strategy.onProgress.add(this._progress, this); 208 | } 209 | 210 | get strategy(): AbstractLoadStrategy { return this._strategy; } 211 | get url(): string { return this._strategy.config.url; } 212 | get isLoading(): boolean { return this._state === ResourceState.Loading; } 213 | get isComplete(): boolean { return this._state === ResourceState.Complete; } 214 | 215 | /** 216 | * Aborts the loading of the resource. 217 | */ 218 | abort(): void 219 | { 220 | this._strategy.abort(); 221 | } 222 | 223 | /** 224 | * Kicks off loading of this resource. 225 | */ 226 | load(): void 227 | { 228 | this._state = ResourceState.Loading; 229 | this.onStart.dispatch(this); 230 | this._strategy.load(); 231 | } 232 | 233 | private _error(errMessage: string): void 234 | { 235 | this._state = ResourceState.Complete; 236 | this.error = errMessage; 237 | this.onComplete.dispatch(this); 238 | } 239 | 240 | private _complete(type: ResourceType, data: any): void 241 | { 242 | this._state = ResourceState.Complete; 243 | this.type = type; 244 | this.data = data; 245 | this.onComplete.dispatch(this); 246 | } 247 | 248 | private _progress(percent: number): void 249 | { 250 | this.onProgress.dispatch(this, percent); 251 | } 252 | 253 | /** 254 | * Determines if a URL is crossOrigin, and if so returns the crossOrigin string. 255 | */ 256 | private _determineCrossOrigin(url: string, loc = window.location): string 257 | { 258 | // data: and javascript: urls are considered same-origin 259 | if (url.indexOf('data:') === 0 || url.indexOf('javascript:') === 0) 260 | return ''; 261 | 262 | // A sandboxed iframe without the 'allow-same-origin' attribute will have a special 263 | // origin designed not to match window.location.origin, and will always require 264 | // crossOrigin requests regardless of whether the location matches. 265 | if (window.origin !== window.location.origin) 266 | return 'anonymous'; 267 | 268 | if (!Resource._tempAnchor) 269 | Resource._tempAnchor = document.createElement('a'); 270 | 271 | // Let the browser determine the full href for the url and then parse with the 272 | // url lib. We can't use the properties of the anchor element because they 273 | // don't work in IE9 :( 274 | Resource._tempAnchor.href = url; 275 | 276 | const parsed = parseUri(Resource._tempAnchor.href, { strictMode: true }); 277 | 278 | const samePort = (!parsed.port && loc.port === '') || (parsed.port === loc.port); 279 | const protocol = parsed.protocol ? `${parsed.protocol}:` : ''; 280 | 281 | // if cross origin 282 | if (parsed.host !== loc.hostname || !samePort || protocol !== loc.protocol) 283 | return 'anonymous'; 284 | 285 | return ''; 286 | } 287 | } 288 | -------------------------------------------------------------------------------- /src/async/AsyncQueue.ts: -------------------------------------------------------------------------------- 1 | import { Signal } from 'type-signals'; 2 | 3 | /** 4 | * Ensures a function is only called once. 5 | * 6 | * @ignore 7 | * @typeparam R Return type of the function to wrap. 8 | * @param func The function to wrap. 9 | * @return The wrapping function. 10 | */ 11 | function onlyOnce(func: (...args: any[]) => R): (...args: any[]) => R 12 | { 13 | let fn: typeof func | null = func; 14 | 15 | return function onceWrapper(this: any, ...args: any[]) 16 | { 17 | if (fn === null) 18 | throw new Error('Callback was already called.'); 19 | 20 | const callFn = fn; 21 | fn = null; 22 | return callFn.apply(this, args); 23 | }; 24 | } 25 | 26 | export type INext = (err?: Error) => void; 27 | export type IWorker = (item: T, next: INext) => void; 28 | export type IItemCallback = (...args: any[]) => void; 29 | 30 | export type OnDoneSignal = () => void; 31 | export type OnSaturatedSignal = () => void; 32 | export type OnUnsaturatedSignal = () => void; 33 | export type OnEmptySignal = () => void; 34 | export type OnDrainSignal = () => void; 35 | export type OnErrorSignal = (err: Error, data: T) => void; 36 | 37 | interface ITask 38 | { 39 | data: T; 40 | callback?: IItemCallback; 41 | } 42 | 43 | /** 44 | * Async queue. 45 | * 46 | * @typeparam T Element type of the queue. 47 | * @param worker The worker function to call for each task. 48 | * @param concurrency How many workers to run in parrallel. Must be greater than 0. 49 | * @return The async queue object. 50 | */ 51 | export class AsyncQueue 52 | { 53 | private workers = 0; 54 | private buffer = 0; 55 | private paused = false; 56 | 57 | private _started = false; 58 | private _tasks: ITask[] = []; 59 | 60 | readonly onSaturated: Signal = new Signal(); 61 | readonly onUnsaturated: Signal = new Signal(); 62 | readonly onEmpty: Signal = new Signal(); 63 | readonly onDrain: Signal = new Signal(); 64 | readonly onError: Signal> = new Signal>(); 65 | 66 | constructor(readonly worker: IWorker, public concurrency = 1) 67 | { 68 | if (concurrency === 0) 69 | throw new Error('Concurrency must not be zero'); 70 | 71 | this.buffer = concurrency / 4; 72 | } 73 | 74 | get started() { return this._started; } 75 | 76 | reset() 77 | { 78 | this.onDrain.detachAll(); 79 | this.workers = 0; 80 | this._started = false; 81 | this._tasks = []; 82 | } 83 | 84 | push(data: T, callback?: IItemCallback) 85 | { 86 | this._insert(data, false, callback); 87 | } 88 | 89 | unshift(data: T, callback?: IItemCallback) 90 | { 91 | this._insert(data, true, callback); 92 | } 93 | 94 | process() 95 | { 96 | while (!this.paused && this.workers < this.concurrency && this._tasks.length) 97 | { 98 | const task = this._tasks.shift()!; 99 | 100 | if (this._tasks.length === 0) 101 | this.onEmpty.dispatch(); 102 | 103 | this.workers += 1; 104 | 105 | if (this.workers === this.concurrency) 106 | this.onSaturated.dispatch(); 107 | 108 | this.worker(task.data, onlyOnce(this._next(task))); 109 | } 110 | } 111 | 112 | length() 113 | { 114 | return this._tasks.length; 115 | } 116 | 117 | running() 118 | { 119 | return this.workers; 120 | } 121 | 122 | idle() 123 | { 124 | return this._tasks.length + this.workers === 0; 125 | } 126 | 127 | pause() 128 | { 129 | if (this.paused === true) 130 | return; 131 | 132 | this.paused = true; 133 | } 134 | 135 | resume() 136 | { 137 | if (this.paused === false) 138 | return; 139 | 140 | this.paused = false; 141 | 142 | // Need to call this.process once per concurrent 143 | // worker to preserve full concurrency after pause 144 | for (let w = 1; w <= this.concurrency; w++) 145 | { 146 | this.process(); 147 | } 148 | } 149 | 150 | getTask(index: number): ITask 151 | { 152 | return this._tasks[index]; 153 | } 154 | 155 | private _insert(data: T, insertAtFront: boolean, callback?: IItemCallback) 156 | { 157 | if (callback != null && typeof callback !== 'function') 158 | { 159 | throw new Error('task callback must be a function'); 160 | } 161 | 162 | this._started = true; 163 | 164 | if (data == null && this.idle()) 165 | { 166 | // call drain immediately if there are no tasks 167 | setTimeout(() => this.onDrain.dispatch(), 1); 168 | return; 169 | } 170 | 171 | const task: ITask = { data, callback }; 172 | 173 | if (insertAtFront) 174 | this._tasks.unshift(task); 175 | else 176 | this._tasks.push(task); 177 | 178 | setTimeout(() => this.process(), 1); 179 | } 180 | 181 | private _next(task: ITask) 182 | { 183 | return (err?: Error, ...args: any[]) => 184 | { 185 | this.workers -= 1; 186 | 187 | if (task.callback) 188 | task.callback(err, ...args); 189 | 190 | if (err) 191 | this.onError.dispatch(err, task.data); 192 | 193 | if (this.workers <= (this.concurrency - this.buffer)) 194 | this.onUnsaturated.dispatch(); 195 | 196 | if (this.idle()) 197 | this.onDrain.dispatch(); 198 | 199 | this.process(); 200 | }; 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /src/async/eachSeries.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Iterates an array in series. 3 | * 4 | * @typeparam T Element type of the array. 5 | * @param array Array to iterate. 6 | * @param iterator Function to call for each element. 7 | * @param callback Function to call when done, or on error. 8 | * @param deferNext Break synchronous each loop by calling next with a setTimeout of 1. 9 | */ 10 | export function eachSeries( 11 | array: T[], 12 | iterator: (item: T, next: (err?: Error) => void) => void, 13 | callback?: (err?: Error) => void, 14 | deferNext = false) : void 15 | { 16 | let i = 0; 17 | const len = array.length; 18 | 19 | (function next(err?: Error) 20 | { 21 | if (err || i === len) 22 | { 23 | if (callback) 24 | callback(err); 25 | return; 26 | } 27 | 28 | if (deferNext) 29 | setTimeout(() => iterator(array[i++], next), 1); 30 | else 31 | iterator(array[i++], next); 32 | })(); 33 | } 34 | -------------------------------------------------------------------------------- /src/bundle.ts: -------------------------------------------------------------------------------- 1 | import { AbstractLoadStrategy } from './load_strategies/AbstractLoadStrategy'; 2 | import { AudioLoadStrategy } from './load_strategies/AudioLoadStrategy'; 3 | import { ImageLoadStrategy } from './load_strategies/ImageLoadStrategy'; 4 | import { MediaElementLoadStrategy } from './load_strategies/MediaElementLoadStrategy'; 5 | import { VideoLoadStrategy } from './load_strategies/VideoLoadStrategy'; 6 | import { XhrLoadStrategy } from './load_strategies/XhrLoadStrategy'; 7 | 8 | import { Loader } from './Loader'; 9 | import { Resource } from './Resource'; 10 | import { ResourceType, ResourceState } from './resource_type'; 11 | 12 | // TODO: Hide this stuff and only expose for tests 13 | import { AsyncQueue } from './async/AsyncQueue'; 14 | import { eachSeries } from './async/eachSeries'; 15 | import { getExtension } from './utilities'; 16 | 17 | Object.defineProperties(Loader, { 18 | AbstractLoadStrategy: { get() { return AbstractLoadStrategy; } }, 19 | AudioLoadStrategy: { get() { return AudioLoadStrategy; } }, 20 | ImageLoadStrategy: { get() { return ImageLoadStrategy; } }, 21 | MediaElementLoadStrategy: { get() { return MediaElementLoadStrategy; } }, 22 | VideoLoadStrategy: { get() { return VideoLoadStrategy; } }, 23 | XhrLoadStrategy: { get() { return XhrLoadStrategy; } }, 24 | 25 | Resource: { get() { return Resource; } }, 26 | ResourceType: { get() { return ResourceType; } }, 27 | ResourceState: { get() { return ResourceState; } }, 28 | 29 | // TODO: Hide this stuff and only expose for tests 30 | async: { get() { return { AsyncQueue, eachSeries }; } }, 31 | getExtension: { get() { return getExtension; } }, 32 | }); 33 | 34 | export default Loader; 35 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { AbstractLoadStrategy, ILoadConfig } from './load_strategies/AbstractLoadStrategy'; 2 | export { AudioLoadStrategy } from './load_strategies/AudioLoadStrategy'; 3 | export { ImageLoadStrategy, IImageLoadConfig } from './load_strategies/ImageLoadStrategy'; 4 | export { MediaElementLoadStrategy, IMediaElementLoadConfig } from './load_strategies/MediaElementLoadStrategy'; 5 | export { VideoLoadStrategy } from './load_strategies/VideoLoadStrategy'; 6 | export { XhrLoadStrategy , XhrResponseType, IXhrLoadConfig } from './load_strategies/XhrLoadStrategy'; 7 | 8 | export { Loader, IAddOptions } from './Loader'; 9 | export { Resource, IResourceOptions } from './Resource'; 10 | export { ResourceType, ResourceState } from './resource_type'; 11 | -------------------------------------------------------------------------------- /src/load_strategies/AbstractLoadStrategy.ts: -------------------------------------------------------------------------------- 1 | import { Signal } from 'type-signals'; 2 | import { ResourceType } from '../resource_type'; 3 | 4 | export interface ILoadConfig 5 | { 6 | // The url for this resource, relative to the baseUrl of this loader. 7 | url: string; 8 | 9 | // A base url to use for just this resource load. This can be passed in 10 | // as the base url for a subresource if desired. 11 | baseUrl?: string; 12 | 13 | // String to use for crossOrigin properties on load elements. 14 | crossOrigin?: string; 15 | 16 | // The time to wait in milliseconds before considering the load a failure. 17 | timeout?: number; 18 | } 19 | 20 | /** 21 | * @category Type Aliases 22 | */ 23 | export namespace AbstractLoadStrategy 24 | { 25 | export type OnErrorSignal = (errMessage: string) => void; 26 | export type OnCompleteSignal = (type: ResourceType, data: any) => void; 27 | export type OnProgressSignal = (percent: number) => void; 28 | } 29 | 30 | /** 31 | * Base load strategy interface that all custom load strategies 32 | * are expected to inherit from and implement. 33 | * @preferred 34 | */ 35 | export abstract class AbstractLoadStrategy 36 | { 37 | /** 38 | * Dispatched when the resource fails to load. 39 | */ 40 | readonly onError: Signal = new Signal(); 41 | 42 | /** 43 | * Dispatched once this resource has loaded, if there was an error it will 44 | * be in the `error` property. 45 | */ 46 | readonly onComplete: Signal = new Signal(); 47 | 48 | /** 49 | * Dispatched each time progress of this resource load updates. 50 | * Not all resources types and loader systems can support this event 51 | * so sometimes it may not be available. If the resource 52 | * is being loaded on a modern browser, using XHR, and the remote server 53 | * properly sets Content-Length headers, then this will be available. 54 | */ 55 | readonly onProgress: Signal = new Signal(); 56 | 57 | constructor(readonly config: C) 58 | { } 59 | 60 | /** 61 | * Load the resource described by `config`. 62 | */ 63 | abstract load(): void; 64 | 65 | /** 66 | * Abort the loading of the resource. 67 | */ 68 | abstract abort(): void; 69 | } 70 | 71 | export type AbstractLoadStrategyCtor = 72 | new (config: C) => AbstractLoadStrategy; 73 | -------------------------------------------------------------------------------- /src/load_strategies/AudioLoadStrategy.ts: -------------------------------------------------------------------------------- 1 | import { MediaElementLoadStrategy, IMediaElementLoadConfig } from './MediaElementLoadStrategy'; 2 | 3 | export class AudioLoadStrategy extends MediaElementLoadStrategy 4 | { 5 | constructor(config: IMediaElementLoadConfig) 6 | { 7 | super(config, 'audio'); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/load_strategies/ImageLoadStrategy.ts: -------------------------------------------------------------------------------- 1 | import { AbstractLoadStrategy, ILoadConfig } from './AbstractLoadStrategy'; 2 | import { ResourceType } from '../resource_type'; 3 | 4 | // We can't set the `src` attribute to empty string, so on abort we set it to this 1px transparent gif 5 | const EMPTY_GIF = 'data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=='; 6 | 7 | export interface IImageLoadConfig extends ILoadConfig 8 | { 9 | loadElement?: HTMLImageElement; 10 | } 11 | 12 | export class ImageLoadStrategy extends AbstractLoadStrategy 13 | { 14 | private _boundOnLoad = this._onLoad.bind(this); 15 | private _boundOnError = this._onError.bind(this); 16 | private _boundOnTimeout = this._onTimeout.bind(this); 17 | 18 | private _element = this._createElement(); 19 | private _elementTimer = 0; 20 | 21 | load(): void 22 | { 23 | const config = this.config; 24 | 25 | if (config.crossOrigin) 26 | this._element.crossOrigin = config.crossOrigin; 27 | 28 | this._element.src = config.url; 29 | 30 | this._element.addEventListener('load', this._boundOnLoad, false); 31 | this._element.addEventListener('error', this._boundOnError, false); 32 | 33 | if (config.timeout) 34 | this._elementTimer = window.setTimeout(this._boundOnTimeout, config.timeout); 35 | } 36 | 37 | abort(): void 38 | { 39 | this._clearEvents(); 40 | this._element.src = EMPTY_GIF; 41 | this._error('Image load aborted by the user.'); 42 | } 43 | 44 | private _createElement(): HTMLImageElement 45 | { 46 | if (this.config.loadElement) 47 | return this.config.loadElement; 48 | else 49 | return document.createElement('img') 50 | } 51 | 52 | private _clearEvents(): void 53 | { 54 | clearTimeout(this._elementTimer); 55 | 56 | this._element.removeEventListener('load', this._boundOnLoad, false); 57 | this._element.removeEventListener('error', this._boundOnError, false); 58 | } 59 | 60 | private _error(errMessage: string): void 61 | { 62 | this._clearEvents(); 63 | this.onError.dispatch(errMessage); 64 | } 65 | 66 | private _complete(): void 67 | { 68 | this._clearEvents(); 69 | this.onComplete.dispatch(ResourceType.Image, this._element); 70 | } 71 | 72 | private _onLoad(): void 73 | { 74 | this._complete(); 75 | } 76 | 77 | private _onError(): void 78 | { 79 | this._error('Image failed to load.'); 80 | } 81 | 82 | private _onTimeout(): void 83 | { 84 | this._error('Image load timed out.'); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/load_strategies/MediaElementLoadStrategy.ts: -------------------------------------------------------------------------------- 1 | import { AbstractLoadStrategy, ILoadConfig } from './AbstractLoadStrategy'; 2 | import { getExtension, assertNever } from '../utilities'; 3 | import { ResourceType } from '../resource_type'; 4 | 5 | export interface IMediaElementLoadConfig extends ILoadConfig 6 | { 7 | sourceSet?: string[]; 8 | mimeTypes?: string[]; 9 | loadElement?: HTMLMediaElement; 10 | } 11 | 12 | export abstract class MediaElementLoadStrategy extends AbstractLoadStrategy 13 | { 14 | private _boundOnLoad = this._onLoad.bind(this); 15 | private _boundOnError = this._onError.bind(this); 16 | private _boundOnTimeout = this._onTimeout.bind(this); 17 | 18 | private _element = this._createElement(); 19 | private _elementTimer = 0; 20 | 21 | constructor(config: IMediaElementLoadConfig, readonly elementType: ('audio' | 'video')) 22 | { 23 | super(config); 24 | } 25 | 26 | load(): void 27 | { 28 | const config = this.config; 29 | 30 | if (config.crossOrigin) 31 | this._element.crossOrigin = config.crossOrigin; 32 | 33 | const urls = config.sourceSet || [config.url]; 34 | 35 | // support for CocoonJS Canvas+ runtime, lacks document.createElement('source') 36 | if ((navigator as any).isCocoonJS) 37 | { 38 | this._element.src = urls[0]; 39 | } 40 | else 41 | { 42 | for (let i = 0; i < urls.length; ++i) 43 | { 44 | const url = urls[i]; 45 | let mimeType = config.mimeTypes ? config.mimeTypes[i] : undefined; 46 | 47 | if (!mimeType) 48 | mimeType = `${this.elementType}/${getExtension(url)}`; 49 | 50 | const source = document.createElement('source'); 51 | 52 | source.src = url; 53 | source.type = mimeType; 54 | 55 | this._element.appendChild(source); 56 | } 57 | } 58 | 59 | this._element.addEventListener('load', this._boundOnLoad, false); 60 | this._element.addEventListener('canplaythrough', this._boundOnLoad, false); 61 | this._element.addEventListener('error', this._boundOnError, false); 62 | 63 | this._element.load(); 64 | 65 | if (config.timeout) 66 | this._elementTimer = window.setTimeout(this._boundOnTimeout, config.timeout); 67 | } 68 | 69 | abort(): void 70 | { 71 | this._clearEvents(); 72 | while (this._element.firstChild) 73 | { 74 | this._element.removeChild(this._element.firstChild); 75 | } 76 | this._error(`${this.elementType} load aborted by the user.`); 77 | } 78 | 79 | private _createElement(): HTMLMediaElement 80 | { 81 | if (this.config.loadElement) 82 | return this.config.loadElement; 83 | else 84 | return document.createElement(this.elementType); 85 | } 86 | 87 | private _clearEvents(): void 88 | { 89 | clearTimeout(this._elementTimer); 90 | 91 | this._element.removeEventListener('load', this._boundOnLoad, false); 92 | this._element.removeEventListener('canplaythrough', this._boundOnLoad, false); 93 | this._element.removeEventListener('error', this._boundOnError, false); 94 | } 95 | 96 | private _error(errMessage: string): void 97 | { 98 | this._clearEvents(); 99 | this.onError.dispatch(errMessage); 100 | } 101 | 102 | private _complete(): void 103 | { 104 | this._clearEvents(); 105 | 106 | let resourceType = ResourceType.Unknown; 107 | 108 | switch (this.elementType) 109 | { 110 | case 'audio': resourceType = ResourceType.Audio; break; 111 | case 'video': resourceType = ResourceType.Video; break; 112 | default: assertNever(this.elementType); 113 | } 114 | 115 | this.onComplete.dispatch(resourceType, this._element); 116 | } 117 | 118 | private _onLoad(): void 119 | { 120 | this._complete(); 121 | } 122 | 123 | private _onError(): void 124 | { 125 | this._error(`${this.elementType} failed to load.`); 126 | } 127 | 128 | private _onTimeout(): void 129 | { 130 | this._error(`${this.elementType} load timed out.`); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/load_strategies/VideoLoadStrategy.ts: -------------------------------------------------------------------------------- 1 | import { MediaElementLoadStrategy, IMediaElementLoadConfig } from './MediaElementLoadStrategy'; 2 | 3 | export class VideoLoadStrategy extends MediaElementLoadStrategy 4 | { 5 | constructor(config: IMediaElementLoadConfig) 6 | { 7 | super(config, 'video'); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/load_strategies/XhrLoadStrategy.ts: -------------------------------------------------------------------------------- 1 | import { AbstractLoadStrategy, ILoadConfig } from './AbstractLoadStrategy'; 2 | import { getExtension, assertNever } from '../utilities'; 3 | import { ResourceType } from '../resource_type'; 4 | 5 | // tests if CORS is supported in XHR, if not we need to use XDR 6 | // Mainly this is for IE9 support. 7 | const useXdr = !!((window as any).XDomainRequest && !('withCredentials' in (new XMLHttpRequest()))); 8 | 9 | const enum HttpStatus 10 | { 11 | None = 0, 12 | Ok = 200, 13 | Empty = 204, 14 | IeEmptyBug = 1223, 15 | } 16 | 17 | /** 18 | * The XHR response types. 19 | */ 20 | export enum XhrResponseType 21 | { 22 | /** string */ 23 | Default = 'text', 24 | /** ArrayBuffer */ 25 | Buffer = 'arraybuffer', 26 | /** Blob */ 27 | Blob = 'blob', 28 | /** Document */ 29 | Document = 'document', 30 | /** Object */ 31 | Json = 'json', 32 | /** String */ 33 | Text = 'text', 34 | }; 35 | 36 | export interface IXhrLoadConfig extends ILoadConfig 37 | { 38 | xhrType?: XhrResponseType; 39 | } 40 | 41 | /** 42 | * Quick helper to get string xhr type. 43 | */ 44 | function reqType(xhr: XMLHttpRequest): string 45 | { 46 | return xhr.toString().replace('object ', ''); 47 | } 48 | 49 | export class XhrLoadStrategy extends AbstractLoadStrategy 50 | { 51 | static readonly ResponseType = XhrResponseType; 52 | 53 | private _boundOnLoad = this._onLoad.bind(this); 54 | private _boundOnAbort = this._onAbort.bind(this); 55 | private _boundOnError = this._onError.bind(this); 56 | private _boundOnTimeout = this._onTimeout.bind(this); 57 | private _boundOnProgress = this._onProgress.bind(this); 58 | 59 | private _xhr = this._createRequest(); 60 | private _xhrType = XhrResponseType.Default; 61 | 62 | load(): void 63 | { 64 | const config = this.config; 65 | const ext = getExtension(config.url); 66 | 67 | if (typeof config.xhrType !== 'string') 68 | { 69 | config.xhrType = this._determineXhrType(ext); 70 | } 71 | 72 | const xhr = this._xhr; 73 | 74 | this._xhrType = config.xhrType || XhrResponseType.Default; 75 | 76 | // XDomainRequest has a few quirks. Occasionally it will abort requests 77 | // A way to avoid this is to make sure ALL callbacks are set even if not used 78 | // More info here: http://stackoverflow.com/questions/15786966/xdomainrequest-aborts-post-on-ie-9 79 | 80 | if (useXdr) 81 | { 82 | // XDR needs a timeout value or it breaks in IE9 83 | xhr.timeout = config.timeout || 5000; 84 | 85 | xhr.onload = this._boundOnLoad; 86 | xhr.onerror = this._boundOnError; 87 | xhr.ontimeout = this._boundOnTimeout; 88 | xhr.onprogress = this._boundOnProgress; 89 | 90 | xhr.open('GET', config.url, true); 91 | 92 | // Note: The xdr.send() call is wrapped in a timeout to prevent an issue with 93 | // the interface where some requests are lost if multiple XDomainRequests are 94 | // being sent at the same time. 95 | setTimeout(function () { xhr.send(); }, 0); 96 | } 97 | else 98 | { 99 | xhr.open('GET', config.url, true); 100 | 101 | if (config.timeout) 102 | xhr.timeout = config.timeout; 103 | 104 | // load json as text and parse it ourselves. We do this because some browsers 105 | // *cough* safari *cough* can't deal with it. 106 | if (config.xhrType === XhrResponseType.Json || config.xhrType === XhrResponseType.Document) 107 | xhr.responseType = XhrResponseType.Text; 108 | else 109 | xhr.responseType = config.xhrType; 110 | 111 | xhr.addEventListener('load', this._boundOnLoad, false); 112 | xhr.addEventListener('abort', this._boundOnAbort, false); 113 | xhr.addEventListener('error', this._boundOnError, false); 114 | xhr.addEventListener('timeout', this._boundOnTimeout, false); 115 | xhr.addEventListener('progress', this._boundOnProgress, false); 116 | 117 | xhr.send(); 118 | } 119 | } 120 | 121 | abort(): void 122 | { 123 | if (useXdr) 124 | { 125 | this._clearEvents(); 126 | this._xhr.abort(); 127 | this._onAbort(); 128 | } 129 | else 130 | { 131 | // will call the abort event 132 | this._xhr.abort(); 133 | } 134 | } 135 | 136 | private _createRequest(): XMLHttpRequest 137 | { 138 | if (useXdr) 139 | return new (window as any).XDomainRequest(); 140 | else 141 | return new XMLHttpRequest(); 142 | } 143 | 144 | private _determineXhrType(ext: string): XhrResponseType 145 | { 146 | return XhrLoadStrategy._xhrTypeMap[ext] || XhrResponseType.Default; 147 | } 148 | 149 | private _clearEvents(): void 150 | { 151 | if (useXdr) 152 | { 153 | this._xhr.onload = null; 154 | this._xhr.onerror = null; 155 | this._xhr.ontimeout = null; 156 | this._xhr.onprogress = null; 157 | } 158 | else 159 | { 160 | this._xhr.removeEventListener('load', this._boundOnLoad, false); 161 | this._xhr.removeEventListener('abort', this._boundOnAbort, false); 162 | this._xhr.removeEventListener('error', this._boundOnError, false); 163 | this._xhr.removeEventListener('timeout', this._boundOnTimeout, false); 164 | this._xhr.removeEventListener('progress', this._boundOnProgress, false); 165 | } 166 | } 167 | 168 | private _error(errMessage: string): void 169 | { 170 | this._clearEvents(); 171 | this.onError.dispatch(errMessage); 172 | } 173 | 174 | private _complete(type: ResourceType, data: any): void 175 | { 176 | this._clearEvents(); 177 | this.onComplete.dispatch(type, data); 178 | } 179 | 180 | private _onLoad(): void 181 | { 182 | const xhr = this._xhr; 183 | let text = ''; 184 | 185 | // XDR has no `.status`, assume 200. 186 | let status = typeof xhr.status === 'undefined' ? HttpStatus.Ok : xhr.status; 187 | 188 | // responseText is accessible only if responseType is '' or 'text' and on older browsers 189 | if (typeof xhr.responseType === 'undefined' || xhr.responseType === '' || xhr.responseType === 'text') 190 | { 191 | text = xhr.responseText; 192 | } 193 | 194 | // status can be 0 when using the `file://` protocol so we also check if a response is set. 195 | // If it has a response, we assume 200; otherwise a 0 status code with no contents is an aborted request. 196 | if (status === HttpStatus.None && (text.length > 0 || xhr.responseType === XhrResponseType.Buffer)) 197 | { 198 | status = HttpStatus.Ok; 199 | } 200 | // handle IE9 bug: http://stackoverflow.com/questions/10046972/msie-returns-status-code-of-1223-for-ajax-request 201 | else if (status === HttpStatus.IeEmptyBug) 202 | { 203 | status = HttpStatus.Empty; 204 | } 205 | 206 | const flattenedStatus = Math.floor(status / 100) * 100; 207 | 208 | if (flattenedStatus !== HttpStatus.Ok) 209 | { 210 | this._error(`[${xhr.status}] ${xhr.statusText}: ${xhr.responseURL}`); 211 | return; 212 | } 213 | 214 | switch (this._xhrType) 215 | { 216 | case XhrResponseType.Buffer: 217 | this._complete(ResourceType.Buffer, xhr.response); 218 | break; 219 | 220 | case XhrResponseType.Blob: 221 | this._complete(ResourceType.Blob, xhr.response); 222 | break; 223 | 224 | case XhrResponseType.Document: 225 | this._parseDocument(text); 226 | break; 227 | 228 | case XhrResponseType.Json: 229 | this._parseJson(text); 230 | break; 231 | 232 | case XhrResponseType.Default: 233 | case XhrResponseType.Text: 234 | this._complete(ResourceType.Text, text); 235 | break; 236 | 237 | default: 238 | assertNever(this._xhrType); 239 | } 240 | } 241 | 242 | private _parseDocument(text: string): void 243 | { 244 | try 245 | { 246 | if (window.DOMParser) 247 | { 248 | const parser = new DOMParser(); 249 | const data = parser.parseFromString(text, 'text/xml'); 250 | this._complete(ResourceType.Xml, data); 251 | } 252 | else 253 | { 254 | const div = document.createElement('div'); 255 | div.innerHTML = text; 256 | this._complete(ResourceType.Xml, div); 257 | } 258 | } 259 | catch (e) 260 | { 261 | this._error(`Error trying to parse loaded xml: ${e}`); 262 | } 263 | } 264 | 265 | private _parseJson(text: string): void 266 | { 267 | try 268 | { 269 | const data = JSON.parse(text); 270 | this._complete(ResourceType.Json, data); 271 | } 272 | catch (e) 273 | { 274 | this._error(`Error trying to parse loaded json: ${e}`); 275 | } 276 | } 277 | 278 | private _onAbort(): void 279 | { 280 | const xhr = this._xhr; 281 | this._error(`${reqType(xhr)} Request was aborted by the user.`); 282 | } 283 | 284 | private _onError(): void 285 | { 286 | const xhr = this._xhr; 287 | this._error(`${reqType(xhr)} Request failed. Status: ${xhr.status}, text: "${xhr.statusText}"`); 288 | } 289 | 290 | private _onTimeout(): void 291 | { 292 | const xhr = this._xhr; 293 | this._error(`${reqType(xhr)} Request timed out.`); 294 | } 295 | 296 | private _onProgress(event: ProgressEvent): void 297 | { 298 | if (event && event.lengthComputable) 299 | { 300 | this.onProgress.dispatch(event.loaded / event.total); 301 | } 302 | } 303 | 304 | /** 305 | * Sets the load type to be used for a specific extension. 306 | * 307 | * @param extname The extension to set the type for, e.g. "png" or "fnt" 308 | * @param xhrType The xhr type to set it to. 309 | */ 310 | static setExtensionXhrType(extname: string, xhrType: XhrResponseType) 311 | { 312 | if (extname && extname.indexOf('.') === 0) 313 | extname = extname.substring(1); 314 | 315 | if (!extname) 316 | return; 317 | 318 | XhrLoadStrategy._xhrTypeMap[extname] = xhrType; 319 | } 320 | 321 | private static _xhrTypeMap: Partial> = { 322 | // xml 323 | xhtml: XhrResponseType.Document, 324 | html: XhrResponseType.Document, 325 | htm: XhrResponseType.Document, 326 | xml: XhrResponseType.Document, 327 | tmx: XhrResponseType.Document, 328 | svg: XhrResponseType.Document, 329 | 330 | // This was added to handle Tiled Tileset XML, but .tsx is also a TypeScript React Component. 331 | // Since it is way less likely for people to be loading TypeScript files instead of Tiled files, 332 | // this should probably be fine. 333 | tsx: XhrResponseType.Document, 334 | 335 | // images 336 | gif: XhrResponseType.Blob, 337 | png: XhrResponseType.Blob, 338 | bmp: XhrResponseType.Blob, 339 | jpg: XhrResponseType.Blob, 340 | jpeg: XhrResponseType.Blob, 341 | tif: XhrResponseType.Blob, 342 | tiff: XhrResponseType.Blob, 343 | webp: XhrResponseType.Blob, 344 | tga: XhrResponseType.Blob, 345 | 346 | // json 347 | json: XhrResponseType.Json, 348 | 349 | // text 350 | text: XhrResponseType.Text, 351 | txt: XhrResponseType.Text, 352 | 353 | // fonts 354 | ttf: XhrResponseType.Buffer, 355 | otf: XhrResponseType.Buffer, 356 | }; 357 | } 358 | -------------------------------------------------------------------------------- /src/resource_type.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Describes the type of data the Resource holds. 3 | */ 4 | export enum ResourceType 5 | { 6 | /** The resource data type is unknown. */ 7 | Unknown, 8 | /** The resource data is an ArrayBuffer. */ 9 | Buffer, 10 | /** The resource data is a Blob. */ 11 | Blob, 12 | /** The resource data is a parsed JSON Object. */ 13 | Json, 14 | /** The resource data is a Document or
element representing parsed XML. */ 15 | Xml, 16 | /** The resource data is an element. */ 17 | Image, 18 | /** The resource data is an