├── .gitignore ├── Gruntfile.js ├── LICENSE-MIT ├── Makefile ├── bower.json ├── component.json ├── dist ├── operative.js └── operative.min.js ├── package.json ├── readme.md ├── src ├── OperativeContext.js ├── contexts │ ├── BrowserWorker.js │ └── Iframe.js └── operative.js ├── test ├── benchmarks │ ├── benchmark.js │ ├── index.html │ └── suite.js ├── demos │ ├── img.jpg │ ├── noise.html │ └── promises.html ├── operative.iframe.spec.js ├── operative.spec.js └── resources │ ├── dependency1.js │ ├── dependency2.js │ ├── dev.html │ ├── run.html │ ├── run_dist.html │ ├── specHelpers.js │ └── swarm_run.html └── vendor ├── Promise.js └── require.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .grunt 3 | .idea 4 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | var MIN_BANNER = '/** Operative v<%= pkg.version %> (c) 2013 James padolsey, MIT-licensed, http://github.com/padolsey/operative **/\n'; 2 | 3 | var DEBUG_BANNER = [ 4 | '/*!', 5 | ' * Operative', 6 | ' * ---', 7 | ' * Operative is a small JS utility for seamlessly creating Web Worker scripts.', 8 | ' * ---', 9 | ' * @author James Padolsey http://james.padolsey.com', 10 | ' * @repo http://github.com/padolsey/operative', 11 | ' * @version <%= pkg.version %>', 12 | ' * @license MIT', 13 | ' */' 14 | ].join('\n') + '\n'; 15 | 16 | module.exports = function(grunt) { 17 | 18 | 'use strict'; 19 | 20 | grunt.initConfig({ 21 | pkg: grunt.file.readJSON('package.json'), 22 | uglify: { 23 | options: { 24 | banner: MIN_BANNER, 25 | compress: true, 26 | mangle: true 27 | }, 28 | dist: { 29 | src: 'dist/operative.js', 30 | dest: 'dist/operative.min.js' 31 | } 32 | }, 33 | concat: { 34 | options: { 35 | banner: DEBUG_BANNER, 36 | stripBanners: true 37 | }, 38 | dist_browser: { 39 | src: [ 40 | 'src/operative.js', 41 | 'src/OperativeContext.js', 42 | 'src/contexts/BrowserWorker.js', 43 | 'src/contexts/Iframe.js' 44 | ], 45 | dest: 'dist/operative.js' 46 | } 47 | }, 48 | mocha_phantomjs: { 49 | all: { 50 | options: { 51 | urls: ['http://localhost:8000/test/resources/run.html'] 52 | } 53 | } 54 | }, 55 | connect: { 56 | server: { 57 | options: { 58 | port: 8000, 59 | base: '.' 60 | } 61 | } 62 | }, 63 | bumpup: ['package.json', 'bower.json', 'component.json'] 64 | }); 65 | 66 | grunt.loadNpmTasks('grunt-contrib-uglify'); 67 | grunt.loadNpmTasks('grunt-contrib-concat'); 68 | grunt.loadNpmTasks('grunt-bumpup'); 69 | grunt.loadNpmTasks('grunt-contrib-connect'); 70 | grunt.loadNpmTasks('grunt-mocha-phantomjs'); 71 | 72 | grunt.registerTask('bump', function (type) { 73 | type = type ? type : 'patch'; 74 | grunt.task.run('bumpup:' + type); 75 | }); 76 | 77 | grunt.registerTask('default', ['test', 'build']); 78 | grunt.registerTask('build', ['concat:dist_browser', 'uglify:dist']); 79 | grunt.registerTask('test', ['connect', 'mocha_phantomjs']); 80 | 81 | }; 82 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 James Padolsey 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VERSION=$(grep 'version' package.json | sed 's/.*\"\(.*\)\".*/\1/') 2 | 3 | install: 4 | npm install # Install node modules 5 | bower install # Install bower components 6 | grunt build # Build & test src 7 | 8 | release: 9 | grunt bump 10 | make install 11 | git add dist bower.json component.json package.json 12 | git commit -m "Bumped version to $(value VERSION)" 13 | git tag -a $(value VERSION) -m "v$(value VERSION)" 14 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "operative", 3 | "version": "0.4.6", 4 | "main": "dist/operative.js", 5 | "ignore": [ 6 | "**/.*", 7 | "node_modules", 8 | "bower_components", 9 | "test", 10 | "tests", 11 | "src", 12 | "Gruntfile.js" 13 | ] 14 | } -------------------------------------------------------------------------------- /component.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "operative", 3 | "version": "0.4.6", 4 | "main": "dist/operative.js", 5 | "scripts": [ 6 | "dist/operative.js" 7 | ] 8 | } -------------------------------------------------------------------------------- /dist/operative.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Operative 3 | * --- 4 | * Operative is a small JS utility for seamlessly creating Web Worker scripts. 5 | * --- 6 | * @author James Padolsey http://james.padolsey.com 7 | * @repo http://github.com/padolsey/operative 8 | * @version 0.4.6 9 | * @license MIT 10 | */ 11 | (function () { 12 | 13 | if (typeof window == 'undefined' && self.importScripts) { 14 | // Exit if operative.js is being loaded as worker (no blob support flow); 15 | return; 16 | } 17 | 18 | var hasOwn = {}.hasOwnProperty; 19 | 20 | // Note: This will work only in the built dist: 21 | // (Otherwise you must explicitly set selfURL to BrowserWorker.js) 22 | var scripts = document.getElementsByTagName('script'); 23 | var opScript = scripts[scripts.length - 1]; 24 | var opScriptURL = /operative/.test(opScript.src) && opScript.src; 25 | 26 | operative.pool = function(size, module, dependencies) { 27 | size = 0 | Math.abs(size) || 1; 28 | var operatives = []; 29 | var current = 0; 30 | 31 | for (var i = 0; i < size; ++i) { 32 | operatives.push(operative(module, dependencies)); 33 | } 34 | 35 | return { 36 | terminate: function() { 37 | for (var i = 0; i < size; ++i) { 38 | operatives[i].destroy(); 39 | } 40 | }, 41 | next: function() { 42 | current = current + 1 === size ? 0 : current + 1; 43 | return operatives[current]; 44 | } 45 | }; 46 | }; 47 | 48 | /** 49 | * Exposed operative factory 50 | */ 51 | function operative(module, dependencies) { 52 | 53 | var getBase = operative.getBaseURL; 54 | var getSelf = operative.getSelfURL; 55 | 56 | var OperativeContext = operative.hasWorkerSupport ? operative.Operative.BrowserWorker : operative.Operative.Iframe; 57 | 58 | if (typeof module == 'function') { 59 | // Allow a single function to be passed. 60 | var o = new OperativeContext({ main: module }, dependencies, getBase, getSelf); 61 | var singularOperative = function() { 62 | return o.api.main.apply(o, arguments); 63 | }; 64 | singularOperative.transfer = function() { 65 | return o.api.main.transfer.apply(o, arguments); 66 | }; 67 | // Copy across exposable API to the returned function: 68 | for (var i in o.api) { 69 | if (hasOwn.call(o.api, i)) { 70 | singularOperative[i] = o.api[i]; 71 | } 72 | } 73 | return singularOperative; 74 | } 75 | 76 | return new OperativeContext(module, dependencies, getBase, getSelf).api; 77 | 78 | } 79 | 80 | // Indicates whether operatives will run within workers: 81 | operative.hasWorkerSupport = !!window.Worker; 82 | operative.hasWorkerViaBlobSupport = false; 83 | operative.hasTransferSupport = false; 84 | 85 | // Default base URL (to be prepended to relative dependency URLs) 86 | // is current page's parent dir: 87 | var baseURL = ( 88 | location.protocol + '//' + 89 | location.hostname + 90 | (location.port?':'+location.port:'') + 91 | location.pathname 92 | ).replace(/[^\/]+$/, ''); 93 | 94 | /** 95 | * Provide Object.create shim 96 | */ 97 | operative.objCreate = Object.create || function(o) { 98 | function F() {} 99 | F.prototype = o; 100 | return new F(); 101 | }; 102 | 103 | /** 104 | * Set and get Self URL, i.e. the url of the 105 | * operative script itself. 106 | */ 107 | 108 | operative.setSelfURL = function(url) { 109 | opScriptURL = url; 110 | }; 111 | 112 | operative.getSelfURL = function(url) { 113 | return opScriptURL; 114 | }; 115 | 116 | /** 117 | * Set and get Base URL, i.e. the path used 118 | * as a base for getting dependencies 119 | */ 120 | 121 | operative.setBaseURL = function(base) { 122 | baseURL = base; 123 | }; 124 | 125 | operative.getBaseURL = function() { 126 | return baseURL; 127 | }; 128 | 129 | // Expose: 130 | window.operative = operative; 131 | })(); 132 | 133 | (function() { 134 | 135 | if (typeof window == 'undefined' && self.importScripts) { 136 | // Exit if operative.js is being loaded as worker (no blob support flow); 137 | return; 138 | } 139 | 140 | var hasOwn = {}.hasOwnProperty; 141 | var slice = [].slice; 142 | var toString = {}.toString; 143 | 144 | operative.Operative = OperativeContext; 145 | 146 | var Promise = OperativeContext.Promise = window.Promise; 147 | 148 | function OperativeTransfers(transfers) { 149 | this.value = transfers; 150 | } 151 | 152 | /** 153 | * OperativeContext 154 | * A type of context: could be a worker, an iframe, etc. 155 | * @param {Object} module Object containing methods/properties 156 | */ 157 | function OperativeContext(module, dependencies, getBaseURL, getSelfURL) { 158 | 159 | var _self = this; 160 | 161 | module.get = module.get || function(prop) { 162 | return this[prop]; 163 | }; 164 | 165 | module.set = module.set || function(prop, value) { 166 | return this[prop] = value; 167 | }; 168 | 169 | this._curToken = 0; 170 | this._queue = []; 171 | 172 | this._getBaseURL = getBaseURL; 173 | this._getSelfURL = getSelfURL; 174 | 175 | this.isDestroyed = false; 176 | this.isContextReady = false; 177 | 178 | this.module = module; 179 | this.dependencies = dependencies || []; 180 | 181 | this.dataProperties = {}; 182 | this.api = {}; 183 | this.callbacks = {}; 184 | this.deferreds = {}; 185 | 186 | this._fixDependencyURLs(); 187 | this._setup(); 188 | 189 | for (var methodName in module) { 190 | if (hasOwn.call(module, methodName)) { 191 | this._createExposedMethod(methodName); 192 | } 193 | } 194 | 195 | this.api.__operative__ = this; 196 | 197 | // Provide the instance's destroy method on the exposed API: 198 | this.api.destroy = this.api.terminate = function() { 199 | return _self.destroy(); 200 | }; 201 | 202 | } 203 | 204 | OperativeContext.prototype = { 205 | 206 | _marshal: function(v) { 207 | return v; 208 | }, 209 | 210 | _demarshal: function(v) { 211 | return v; 212 | }, 213 | 214 | _enqueue: function(fn) { 215 | this._queue.push(fn); 216 | }, 217 | 218 | _fixDependencyURLs: function() { 219 | var deps = this.dependencies; 220 | for (var i = 0, l = deps.length; i < l; ++i) { 221 | var dep = deps[i]; 222 | if (!/\/\//.test(dep)) { 223 | deps[i] = dep.replace(/^\/?/, this._getBaseURL().replace(/([^\/])$/, '$1/')); 224 | } 225 | } 226 | }, 227 | 228 | _dequeueAll: function() { 229 | for (var i = 0, l = this._queue.length; i < l; ++i) { 230 | this._queue[i].call(this); 231 | } 232 | this._queue = []; 233 | }, 234 | 235 | _buildContextScript: function(boilerScript) { 236 | 237 | var script = []; 238 | var module = this.module; 239 | var dataProperties = this.dataProperties; 240 | var property; 241 | 242 | for (var i in module) { 243 | property = module[i]; 244 | if (typeof property == 'function') { 245 | script.push(' self["' + i.replace(/"/g, '\\"') + '"] = ' + property.toString() + ';'); 246 | } else { 247 | dataProperties[i] = property; 248 | } 249 | } 250 | 251 | return script.join('\n') + ( 252 | boilerScript ? '\n(' + boilerScript.toString() + '());' : '' 253 | ); 254 | 255 | }, 256 | 257 | _createExposedMethod: function(methodName) { 258 | 259 | var self = this; 260 | 261 | var method = this.api[methodName] = function() { 262 | 263 | if (self.isDestroyed) { 264 | throw new Error('Operative: Cannot run method. Operative has already been destroyed'); 265 | } 266 | 267 | var token = ++self._curToken; 268 | var args = slice.call(arguments); 269 | var cb = typeof args[args.length - 1] == 'function' && args.pop(); 270 | var transferables = args[args.length - 1] instanceof OperativeTransfers && args.pop(); 271 | 272 | if (!cb && !Promise) { 273 | throw new Error( 274 | 'Operative: No callback has been passed. Assumed that you want a promise. ' + 275 | 'But `operative.Promise` is null. Please provide Promise polyfill/lib.' 276 | ); 277 | } 278 | 279 | if (cb) { 280 | 281 | self.callbacks[token] = cb; 282 | 283 | // Ensure either context runs the method async: 284 | setTimeout(function() { 285 | runMethod(); 286 | }, 1); 287 | 288 | } else if (Promise) { 289 | 290 | // No Callback -- Promise used: 291 | 292 | return new Promise(function(resolve, reject) { 293 | var deferred; 294 | 295 | if (resolve.fulfil || resolve.fulfill) { 296 | // Backwards compatibility 297 | deferred = resolve; 298 | deferred.fulfil = deferred.fulfill = resolve.fulfil || resolve.fulfill; 299 | } else { 300 | deferred = { 301 | // Deprecate: 302 | fulfil: resolve, 303 | fulfill: resolve, 304 | 305 | resolve: resolve, 306 | reject: reject, 307 | 308 | // For the iframe: 309 | transferResolve: resolve, 310 | transferReject: reject 311 | }; 312 | } 313 | 314 | self.deferreds[token] = deferred; 315 | runMethod(); 316 | }); 317 | 318 | } 319 | 320 | function runMethod() { 321 | if (self.isContextReady) { 322 | self._runMethod(methodName, token, args, transferables); 323 | } else { 324 | self._enqueue(runMethod); 325 | } 326 | } 327 | 328 | }; 329 | 330 | method.transfer = function() { 331 | 332 | var args = [].slice.call(arguments); 333 | var transfersIndex = typeof args[args.length - 1] == 'function' ? 334 | args.length - 2: 335 | args.length - 1; 336 | var transfers = args[transfersIndex]; 337 | var transfersType = toString.call(transfers); 338 | 339 | if (transfersType !== '[object Array]') { 340 | throw new Error( 341 | 'Operative:transfer() must be passed an Array of transfers as its last arguments ' + 342 | '(Expected: [object Array], Received: ' + transfersType + ')' 343 | ); 344 | } 345 | 346 | args[transfersIndex] = new OperativeTransfers(transfers); 347 | return method.apply(null, args); 348 | 349 | }; 350 | 351 | }, 352 | 353 | destroy: function() { 354 | this.isDestroyed = true; 355 | } 356 | }; 357 | 358 | })(); 359 | 360 | (function() { 361 | 362 | if (typeof window == 'undefined' && self.importScripts) { 363 | // I'm a worker! Run the boiler-script: 364 | // (Operative itself is called in IE10 as a worker, 365 | // to avoid SecurityErrors) 366 | workerBoilerScript(); 367 | return; 368 | } 369 | 370 | var Operative = operative.Operative; 371 | 372 | var URL = window.URL || window.webkitURL; 373 | var BlobBuilder = window.BlobBuilder || window.WebKitBlobBuilder || window.MozBlobBuilder; 374 | 375 | var workerViaBlobSupport = (function() { 376 | try { 377 | new Worker(makeBlobURI(';')); 378 | } catch(e) { 379 | return false; 380 | } 381 | return true; 382 | }()); 383 | 384 | var transferrableObjSupport = (function() { 385 | try { 386 | var ab = new ArrayBuffer(1); 387 | new Worker( makeBlobURI(';') ).postMessage(ab, [ab]); 388 | return !ab.byteLength; 389 | } catch(e) { 390 | return false; 391 | } 392 | }()); 393 | 394 | operative.hasWorkerViaBlobSupport = workerViaBlobSupport; 395 | operative.hasTransferSupport = transferrableObjSupport; 396 | 397 | function makeBlobURI(script) { 398 | var blob; 399 | 400 | try { 401 | blob = new Blob([script], { type: 'text/javascript' }); 402 | } catch (e) { 403 | blob = new BlobBuilder(); 404 | blob.append(script); 405 | blob = blob.getBlob(); 406 | } 407 | 408 | return URL.createObjectURL(blob); 409 | } 410 | 411 | /** 412 | * Operative BrowserWorker 413 | */ 414 | Operative.BrowserWorker = function BrowserWorker() { 415 | Operative.apply(this, arguments); 416 | }; 417 | 418 | var WorkerProto = Operative.BrowserWorker.prototype = operative.objCreate(Operative.prototype); 419 | 420 | WorkerProto._onWorkerMessage = function(e) { 421 | var data = e.data; 422 | 423 | if (typeof data === 'string' && data.indexOf('pingback') === 0) { 424 | if (data === 'pingback:structuredCloningSupport=NO') { 425 | // No structuredCloningSupport support (marshal JSON from now on): 426 | this._marshal = function(o) { return JSON.stringify(o); }; 427 | this._demarshal = function(o) { return JSON.parse(o); }; 428 | } 429 | 430 | this.isContextReady = true; 431 | this._postMessage({ 432 | definitions: this.dataProperties 433 | }); 434 | this._dequeueAll(); 435 | return; 436 | 437 | } 438 | 439 | data = this._demarshal(data); 440 | 441 | switch (data.cmd) { 442 | case 'console': 443 | window.console && window.console[data.method].apply(window.console, data.args); 444 | break; 445 | case 'deferred_reject_error': 446 | this.deferreds[data.token].reject(data.error); 447 | break; 448 | case 'result': 449 | 450 | var callback = this.callbacks[data.token]; 451 | var deferred = this.deferreds[data.token]; 452 | 453 | var deferredAction = data.result && data.result.isDeferred && data.result.action; 454 | 455 | if (deferred && deferredAction) { 456 | deferred[deferredAction](data.result.args[0]); 457 | } else if (callback) { 458 | callback.apply(this, data.result.args); 459 | } else if (deferred) { 460 | // Resolve promise even if result was given 461 | // via callback within the worker: 462 | deferred.fulfil(data.result.args[0]); 463 | } 464 | 465 | break; 466 | } 467 | }; 468 | 469 | WorkerProto._isWorkerViaBlobSupported = function() { 470 | return workerViaBlobSupport; 471 | }; 472 | 473 | WorkerProto._setup = function() { 474 | var self = this; 475 | 476 | var worker; 477 | var selfURL = this._getSelfURL(); 478 | var blobSupport = this._isWorkerViaBlobSupported(); 479 | var script = this._buildContextScript( 480 | // The script is not included if we're Eval'ing this file directly: 481 | blobSupport ? workerBoilerScript : '' 482 | ); 483 | 484 | if (this.dependencies.length) { 485 | script = 'importScripts("' + this.dependencies.join('", "') + '");\n' + script; 486 | } 487 | 488 | if (blobSupport) { 489 | worker = this.worker = new Worker( makeBlobURI(script) ); 490 | } else { 491 | 492 | if (!selfURL) { 493 | throw new Error('Operaritve: No operative.js URL available. Please set via operative.setSelfURL(...)'); 494 | } 495 | worker = this.worker = new Worker( selfURL ); 496 | // Marshal-agnostic initial message is boiler-code: 497 | // (We don't yet know if structured-cloning is supported so we send a string) 498 | worker.postMessage('EVAL|' + script); 499 | } 500 | 501 | worker.postMessage('EVAL|self.hasTransferSupport=' + transferrableObjSupport); 502 | worker.postMessage(['PING']); // Initial PING 503 | 504 | worker.addEventListener('message', function(e) { 505 | self._onWorkerMessage(e); 506 | }); 507 | }; 508 | 509 | WorkerProto._postMessage = function(msg) { 510 | var transfers = transferrableObjSupport && msg.transfers; 511 | return transfers ? 512 | this.worker.postMessage(msg, transfers.value) : 513 | this.worker.postMessage( 514 | this._marshal(msg) 515 | ); 516 | }; 517 | 518 | WorkerProto._runMethod = function(methodName, token, args, transfers) { 519 | this._postMessage({ 520 | method: methodName, 521 | args: args, 522 | token: token, 523 | transfers: transfers 524 | }); 525 | }; 526 | 527 | WorkerProto.destroy = function() { 528 | this.worker.terminate(); 529 | Operative.prototype.destroy.call(this); 530 | }; 531 | 532 | /** 533 | * The boilerplate for the Worker Blob 534 | * NOTE: 535 | * this'll be executed within an worker, not here. 536 | * Indented @ Zero to make nicer debug code within worker 537 | */ 538 | function workerBoilerScript() { 539 | 540 | var postMessage = self.postMessage; 541 | var structuredCloningSupport = null; 542 | var toString = {}.toString; 543 | 544 | self.console = {}; 545 | self.isWorker = true; 546 | 547 | // Provide basic console interface: 548 | ['log', 'debug', 'error', 'info', 'warn', 'time', 'timeEnd'].forEach(function(meth) { 549 | self.console[meth] = function() { 550 | postMessage({ 551 | cmd: 'console', 552 | method: meth, 553 | args: [].slice.call(arguments) 554 | }); 555 | }; 556 | }); 557 | 558 | self.addEventListener('message', function(e) { 559 | 560 | var data = e.data; 561 | 562 | if (typeof data == 'string' && data.indexOf('EVAL|') === 0) { 563 | eval(data.substring(5)); 564 | return; 565 | } 566 | 567 | if (structuredCloningSupport == null) { 568 | 569 | // e.data of ['PING'] (An array) indicates structuredCloning support 570 | // e.data of '"PING"' (A string) indicates no support (Array has been serialized) 571 | structuredCloningSupport = e.data[0] === 'PING'; 572 | 573 | // Pingback to parent page: 574 | self.postMessage( 575 | structuredCloningSupport ? 576 | 'pingback:structuredCloningSupport=YES' : 577 | 'pingback:structuredCloningSupport=NO' 578 | ); 579 | 580 | if (!structuredCloningSupport) { 581 | postMessage = function(msg) { 582 | // Marshal before sending 583 | return self.postMessage(JSON.stringify(msg)); 584 | }; 585 | } 586 | 587 | return; 588 | } 589 | 590 | if (!structuredCloningSupport) { 591 | // Demarshal: 592 | data = JSON.parse(data); 593 | } 594 | 595 | var defs = data.definitions; 596 | var isDeferred = false; 597 | var args = data.args; 598 | 599 | if (defs) { 600 | // Initial definitions: 601 | for (var i in defs) { 602 | self[i] = defs[i]; 603 | } 604 | return; 605 | } 606 | 607 | function callback() { 608 | // Callback function to be passed to operative method 609 | returnResult({ 610 | args: [].slice.call(arguments) 611 | }); 612 | } 613 | 614 | callback.transfer = function() { 615 | var args = [].slice.call(arguments); 616 | var transfers = extractTransfers(args); 617 | // Callback function to be passed to operative method 618 | returnResult({ 619 | args: args 620 | }, transfers); 621 | }; 622 | 623 | args.push(callback); 624 | 625 | self.deferred = function() { 626 | isDeferred = true; 627 | var def = {}; 628 | function resolve(r, transfers) { 629 | returnResult({ 630 | isDeferred: true, 631 | action: 'resolve', 632 | args: [r] 633 | }, transfers); 634 | return def; 635 | } 636 | function reject(r, transfers) { 637 | if (r instanceof Error) { 638 | // Create an error object that can be cloned: (See #44/#45): 639 | var cloneableError = { 640 | message: r.message, 641 | stack: r.stack, 642 | name: r.name, 643 | code: r.code 644 | }; 645 | for (var i in r) { 646 | if (r.hasOwnProperty(i)) { 647 | cloneableError[i] = r[i]; 648 | } 649 | } 650 | postMessage({ 651 | cmd: 'deferred_reject_error', 652 | token: data.token, 653 | error: cloneableError 654 | }); 655 | return; 656 | } 657 | returnResult({ 658 | isDeferred: true, 659 | action: 'reject', 660 | args: [r] 661 | }, transfers); 662 | } 663 | // Deprecated: 664 | def.fulfil = def.fulfill = def.resolve = function(value) { 665 | return resolve(value); 666 | }; 667 | def.reject = function(value) { 668 | return reject(value); 669 | }; 670 | def.transferResolve = function(value) { 671 | var transfers = extractTransfers(arguments); 672 | return resolve(value, transfers); 673 | }; 674 | def.transferReject = function(value) { 675 | var transfers = extractTransfers(arguments); 676 | return reject(value, transfers); 677 | }; 678 | return def; 679 | }; 680 | 681 | // Call actual operative method: 682 | var result = self[data.method].apply(self, args); 683 | 684 | if (!isDeferred && result !== void 0) { 685 | // Deprecated direct-returning as of 0.2.0 686 | returnResult({ 687 | args: [result] 688 | }); 689 | } 690 | 691 | self.deferred = function() { 692 | throw new Error('Operative: deferred() called at odd time'); 693 | }; 694 | 695 | function returnResult(res, transfers) { 696 | postMessage({ 697 | cmd: 'result', 698 | token: data.token, 699 | result: res 700 | }, self.hasTransferSupport && transfers || []); 701 | } 702 | 703 | function extractTransfers(args) { 704 | var transfers = args[args.length - 1]; 705 | 706 | if (toString.call(transfers) !== '[object Array]') { 707 | throw new Error('Operative: callback.transfer() must be passed an Array of transfers as its last arguments'); 708 | } 709 | 710 | return transfers; 711 | } 712 | }); 713 | } 714 | 715 | })(); 716 | 717 | (function() { 718 | 719 | if (typeof window == 'undefined' && self.importScripts) { 720 | // Exit if operative.js is being loaded as worker (no blob support flow); 721 | return; 722 | } 723 | 724 | var Operative = operative.Operative; 725 | 726 | /** 727 | * Operative IFrame 728 | */ 729 | Operative.Iframe = function Iframe(module) { 730 | Operative.apply(this, arguments); 731 | }; 732 | 733 | var IframeProto = Operative.Iframe.prototype = operative.objCreate(Operative.prototype); 734 | 735 | var _loadedMethodNameI = 0; 736 | 737 | IframeProto._setup = function() { 738 | 739 | var self = this; 740 | var loadedMethodName = '__operativeIFrameLoaded' + (++_loadedMethodNameI); 741 | 742 | this.module.isWorker = false; 743 | 744 | var iframe = this.iframe = document.body.appendChild( 745 | document.createElement('iframe') 746 | ); 747 | 748 | iframe.style.display = 'none'; 749 | 750 | var iWin = this.iframeWindow = iframe.contentWindow; 751 | var iDoc = iWin.document; 752 | 753 | // Cross browser (tested in IE8,9) way to call method from within 754 | // IFRAME after all < script >s have loaded: 755 | window[loadedMethodName] = function() { 756 | 757 | window[loadedMethodName] = null; 758 | 759 | var script = iDoc.createElement('script'); 760 | var js = self._buildContextScript(iframeBoilerScript); 761 | 762 | if (script.text !== void 0) { 763 | script.text = js; 764 | } else { 765 | script.innerHTML = js; 766 | } 767 | 768 | iDoc.documentElement.appendChild(script); 769 | 770 | for (var i in self.dataProperties) { 771 | iWin[i] = self.dataProperties[i]; 772 | } 773 | 774 | self.isContextReady = true; 775 | self._dequeueAll(); 776 | 777 | }; 778 | 779 | iDoc.open(); 780 | 781 | var documentContent = ''; 782 | 783 | if (this.dependencies.length) { 784 | documentContent += '\n'),g.write(h+"\n"),g.close()},c._runMethod=function(a,b,c){var d=this,e=this.callbacks[b],f=this.deferreds[b];this.iframeWindow.__run__(a,c,function(a){var b=e,c=f;b?b.apply(d,arguments):c&&c.fulfil(a)},f)},c.destroy=function(){this.iframe.parentNode.removeChild(this.iframe),b.prototype.destroy.call(this)}}}(); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "operative", 3 | "title": "operative", 4 | "description": "Operative: Inline Web-Worker Helper", 5 | "version": "0.4.6", 6 | "author": "James Padolsey (http://git.io/padolsey)", 7 | "main": "dist/operative.min.js", 8 | "devDependencies": { 9 | "chai": "^3.5.0", 10 | "grunt": "^1.0.1", 11 | "grunt-bumpup": "^0.6.3", 12 | "grunt-contrib-concat": "^1.0.1", 13 | "grunt-contrib-connect": "^1.0.2", 14 | "grunt-contrib-jshint": "^1.0.0", 15 | "grunt-contrib-uglify": "^2.0.0", 16 | "grunt-mocha-phantomjs": "^4.0.0", 17 | "mocha": "^3.1.2" 18 | }, 19 | "keywords": [], 20 | "scripts": {}, 21 | "repository": { 22 | "type": "git", 23 | "url": "git://github.com/padolsey/operative.git" 24 | }, 25 | "license": "MIT" 26 | } -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Operative 2 | 3 | **Before reading this please ensure you fully understand [the concept of Web Workers](https://developer.mozilla.org/en-US/docs/Web/Guide/Performance/Using_web_workers)**. 4 | 5 | Operative is a small JS utility (~1.8k *gzipped*) for seamlessly creating Web Worker scripts. Its features include: 6 | 7 | * Seamless API Authoring 8 | * Producing debuggable Worker Blobs 9 | * Providing `console` interface for simple logging 10 | * Degrading where Worker/Blob support is lacking 11 | 12 | ### Why Operative? 13 | 14 | Utilising unabstracted Workers can be cumbersome and awkward. Having to design and implement message-passing contracts and having to setup event listeners yourself is time-consuming and error-prone. Operative takes care of this stuff so you can focus on your code. 15 | 16 | ### Before you get excited: 17 | 18 | Even with Operative you are still subject to the constraints of Web Workers, i.e. 19 | 20 | * No DOM/BOM Access. 21 | * No synchronous communication with parent page. 22 | * An entirely separate execution context (no shared scope/variables). 23 | * Limitations on data transfer depending on browser. 24 | 25 | And it won't make things uniformly faster or less burdensome on the UI. Operative will fall-back to using iframes in older browsers, which gives you no non-blocking advantage. 26 | 27 | Non-blob worker support (i.e. for IE10) requires that you have a same-origin copy of Operative (this means you can't solely rely on CDNs or elsewhere-hosted scripts if you want to support IE10). 28 | 29 | ### Browser Support 30 | 31 | Operative `0.4.4` has been explicitly tested in: 32 | 33 | * Chrome 14, 23, 29, 37, 42 34 | * Firefox 3, 10, 18, 23, 32 35 | * IE 8, 9, 10 (Windows 7) 36 | * IE 11 (Windows 10) 37 | * Opera 25 (Mac) 38 | * Opera 10.6 (Windows 7) 39 | * Safari 5.1 (Windows 7) 40 | * Safari 6, 8 (Mac) 41 | * Safari (iPad Air, iOS 8) 42 | * Safari (iPhone 4S, iOS 5.1) 43 | 44 | Support for Workers, with varying degrees of support for Transferables and Blobs: 45 | 46 | * FF 17+ 47 | * Chrome 7+ 48 | * Safari 4+ 49 | * Opera 11+ 50 | * IE10+ 51 | 52 | *Note: Operative has not been tested in non-browser envs* 53 | 54 | ### Quick install 55 | 56 | ```sh 57 | # bower 58 | bower install operative 59 | # or npm 60 | npm install operative 61 | ``` 62 | 63 | Or just grab the built JS file from `dist/`, also available here (0.4.4): 64 | 65 | * https://raw.github.com/padolsey/operative/0.4.4/dist/operative.js 66 | * https://raw.github.com/padolsey/operative/0.4.4/dist/operative.min.js 67 | 68 | ### Creating an Operative Module 69 | 70 | An Operative module is defined as an object containing properties/methods: 71 | 72 | ```js 73 | var calculator = operative({ 74 | add: function(a, b, callback) { 75 | callback( a + b ); 76 | } 77 | }); 78 | ``` 79 | 80 | This would expose an asynchronous API: 81 | 82 | ```js 83 | calculator.add(1, 2, function(result) { 84 | result; // => 3 85 | }); 86 | ``` 87 | 88 | The `add()` function will run within a worker. The value it returns is handled by operative and forwarded, asynchronously to your callback function in the parent page. 89 | 90 | Notice that the exposed `add` method requires its last argument to be a callback. The last argument passed to an operative method must always be a callback. All preceding arguments are passed into the worker itself. 91 | 92 | **NOTE:** It's important to note that the Operative code is not executed in-place. *It's executed within a Worker. You won't be able to access variables surrounding the definition of your operative: 93 | 94 | ```js 95 | // THIS WILL NOT WORK! 96 | 97 | var something = 123; 98 | 99 | var myWorker = operative({ 100 | doStuff: function() { 101 | something += 456; 102 | } 103 | }); 104 | ``` 105 | 106 | *(the something variable won't exist within the Worker)* 107 | 108 | Instead you can do: 109 | 110 | ```js 111 | var myWorker = operative({ 112 | something: 123, 113 | doStuff: function() { 114 | this.something += 456; 115 | } 116 | }); 117 | ``` 118 | 119 | ### Need to iterate 10,000,000,000 times? No problem! 120 | 121 | ```js 122 | var craziness = operative({ 123 | 124 | doCrazy: function(cb) { 125 | 126 | console.time('Craziness'); 127 | for (var i = 0; i < 10000000000; ++i); 128 | console.timeEnd('Craziness'); 129 | 130 | cb('I am done!'); 131 | } 132 | 133 | }); 134 | 135 | craziness.doCrazy(function(result) { 136 | // Console outputs: Craziness: 14806.419ms 137 | result; // => "I am done!" 138 | }); 139 | ``` 140 | 141 | ### Degraded Operative 142 | 143 | Operative degrades in this order: 144 | 145 | *(higher is better/cooler)* 146 | 147 | * Full Worker via Blob & [Structured-Cloning](https://developer.mozilla.org/en-US/docs/Web/Guide/DOM/The_structured_clone_algorithm?redirectlocale=en-US&redirectslug=DOM%2FThe_structured_clone_algorithm) (Ch13+, FF8+, IE11+, Op11.5+, Sf5.1+) 148 | * Full Worker via Eval & [Structured-Cloning](https://developer.mozilla.org/en-US/docs/Web/Guide/DOM/The_structured_clone_algorithm?redirectlocale=en-US&redirectslug=DOM%2FThe_structured_clone_algorithm) (IE10) 149 | * Full Worker via Blob & JSON marshalling *(???)* 150 | * Full Worker via Eval & JSON marshalling (Sf4) 151 | * No Worker: Regular JS called via iframe (*older browsers*) 152 | 153 | Operative will degrade in environments with no Worker or Blob support. In such a case the code would execute as regular in-place JavaScript. The calls will still be asynchronous though, not immediate. 154 | 155 | If you are looking to support this fully degraded state (honestly, only do it if you have to) then you'll also need to avoid utilising Worker-specific APIs like `importScripts`. 156 | 157 | ### No Worker-Via-Blob Support 158 | 159 | Operative supports browsers with no worker-via-blob support (e.g. IE10, Safari 4.0) via eval, and it requires `operative.js` or `operative.min.js` to be its own file and included in the page via a ``. 171 | * *{Function}* *operative.setBaseURL*: Allows you to set the URL that should be prepended to relative dependency URLs. 172 | 173 | #### Creating an Operative: 174 | 175 | To create an operative module pass an object of methods/properties: 176 | 177 | ```js 178 | var myOperative = operative({ 179 | doX: function(a, b, c, callback) { 180 | // ... 181 | }, 182 | doY: function(a, b, c, callback) { 183 | // ... 184 | } 185 | }); 186 | ``` 187 | 188 | Or a single function to create a singular operative: 189 | 190 | ```js 191 | var myOperative = operative(function(a, b, c, callback) { 192 | // Send the result to the parent page: 193 | callback(...); 194 | }); 195 | 196 | // Example call: 197 | myOperative(1, 2, 3, function() { /*callback*/ }); 198 | ``` 199 | 200 | #### Returning results 201 | 202 | The most simple way to use operative is to pass in a callback function when calling an operative function and within the operative method call the callback with your result: 203 | 204 | ```js 205 | var combine = operative(function(foo, bar, callback) { 206 | callback(foo + bar); 207 | }); 208 | 209 | combine('foo', 'bar', function() { 210 | // This callback function will be called with 211 | // the result from your operative function. 212 | result; // => 'foobar' 213 | }); 214 | ``` 215 | 216 | #### Return via Promises 217 | 218 | If you don't pass a callback when calling an operative method, operative will assume you want a Promise. Note that operative will reference `operative.Promise` and will expect it to be a [native Promise implementation](http://dom.spec.whatwg.org/#promises) or [compliant polyfill](https://github.com/slightlyoff/Promises). *Operative does not come bundled with a Promise implementation.* 219 | 220 | ```js 221 | var combine = operative(function(foo, bar) { 222 | // Internally, use a Deferred: 223 | var deferred = this.deferred(); 224 | 225 | // A deferred has two methods: fulfill & reject: 226 | 227 | if (foo !== 'foo') { 228 | // Error (Rejection) 229 | deferred.reject('foo should be "foo"!'); 230 | } else { 231 | // Success (Filfillment) 232 | deferred.fulfill(foo + bar); 233 | } 234 | }); 235 | 236 | // Usage externally: 237 | var promise = combine('foo', 'bar'); 238 | 239 | promise.then(function(value) { 240 | // Fulfilled 241 | }, function(err) { 242 | // Rejected 243 | }); 244 | ``` 245 | 246 | **NOTE:** Operative will only give you a promise if you don't pass a callback *and* if `operative.Promise` is defined. By default `operative.Promise` will reference `window.Promise` (*native implementation if it exists*). 247 | 248 | #### Declaring dependencies 249 | 250 | Operative accepts a second argument, an array of JS files to load within the worker ( *or in its degraded state, an Iframe* ): 251 | 252 | ```js 253 | // Create interface to call lodash methods inside a worker: 254 | var lodashWorker = operative(function(method, args, cb) { 255 | cb( 256 | _[method].apply(_, args) 257 | ); 258 | }, [ 259 | 'http://cdnjs.cloudflare.com/ajax/libs/lodash.js/1.3.1/lodash.min.js' 260 | ]); 261 | 262 | lodashWorker('uniq', [[1, 2, 3, 3, 2, 1, 4, 3, 2]], function(output) { 263 | output; // => [1, 2, 3, 4] 264 | }); 265 | ``` 266 | 267 | Declared dependencies will be loaded before any of your operative's methods are called. Even if you call one from the outside, that call will be queued until the context (Worker/iFrame) completes loading the dependencies. 268 | 269 | Note: Each dependency, if not an absolute URL, will be loaded relative to the calculated base URL, which operative determines like so: 270 | 271 | ```js 272 | var baseURL = ( 273 | location.protocol + '//' + 274 | location.hostname + 275 | (location.port?':'+location.port:'') + 276 | location.pathname 277 | ).replace(/[^\/]+$/, ''); 278 | ``` 279 | 280 | To override at runtime use: 281 | 282 | ```js 283 | operative.setBaseURL('http://some.com/new/absolute/base/'); 284 | // Ensure it ends in a '/' 285 | 286 | // To retrieve the current Base URL: 287 | operative.getBaseURL(); 288 | ``` 289 | 290 | #### Destroying an operative 291 | 292 | To terminate the operative (and thus its worker/iframe): 293 | 294 | ```js 295 | o.terminate(); 296 | ``` 297 | 298 | (`terminate` is aliased to the now-deprecated `destroy`) 299 | 300 | ### Testing & Building 301 | 302 | **Special thanks to [BrowserStack](http://www.browserstack.com) for providing free testing!** 303 | 304 | ``` 305 | $ # grab dependencies 306 | $ npm install 307 | 308 | $ # install grunt globally if you don't have it... 309 | $ npm install -g grunt-cli 310 | 311 | $ # test 312 | $ grunt test 313 | 314 | $ # do everything + build dist: 315 | $ grunt 316 | ``` 317 | 318 | ### Changelog 319 | * 0.4.6 (19 Jan 2017) 320 | * Fix uncloneable native `Error` obj issue (see [#44](https://github.com/padolsey/operative/issues/44) & [#45](https://github.com/padolsey/operative/issues/45)) 321 | * 0.4.5 322 | * Fix error `Uncaught ReferenceError: hasTransferSupport is not defined` (see [#43](https://github.com/padolsey/operative/issues/43)) 323 | * 0.4.4 (27 Apr 2015) 324 | * Reverted to a global variable to fix undefined errors in bundles 325 | * 0.4.3 (26 Apr 2015) 326 | * Fixed self-url setting (see [#36](https://github.com/padolsey/operative/issues/36)) 327 | * Improved readme 328 | * 0.4.2 (25 Apr 2015) 329 | * Added support for CommonJS 330 | * 0.4.0 (10 Apr 2015) 331 | * Removed deprecated `async()` method in favor of callbacks or promises 332 | * Refactor / Restructure code to make maintenance a bit easier 333 | * Use `mocha_phantomjs` to setup mocha testing via grunt 334 | * 0.4.0-rc1 335 | * Refactor test suites (use mocha instead of jasmine and fix various flakey specs). 336 | * Deprecate `deferred.fulfil()` (worker context promise API) in favour of `deferred.resolve()` (alias for `fulfil` still exists). 337 | * Introduce [Transfers API](https://github.com/padolsey/operative/wiki/Transfers-API) ([#23](https://github.com/padolsey/operative/issues/23)). 338 | * Fix [#18](https://github.com/padolsey/operative/issues/18). 339 | * Retain callbacks (allowing them to be called again and again -- a la Events). See [#15](https://github.com/padolsey/operative/issues/15). 340 | * Introduce small benchmarks suite 341 | * 0.3.2 (7 Jul 2014) AMD Support + Align correctly with ES6 Promise API (PRs [21](https://github.com/padolsey/operative/pull/21) and [22](https://github.com/padolsey/operative/pull/22) -- thanks [Rich](https://github.com/Rich-Harris)!) 342 | * 0.3.1 (27 Apr 2014) Improved release flow via [PR #20](https://github.com/padolsey/operative/pull/20). 343 | * 0.3.0 (21 Sep 2013) API: `terminate` aliased to `destroy` (deprecating the latter). See [Issue #14](https://github.com/padolsey/operative/issues/14). 344 | * 0.2.1 (30 Jul 2013) Fix worker-via-eval support (Safari 4/5, IE8/9) 345 | * 0.2.0 (29 Jul 2013) See #10 346 | * Dependency Loading (initially suggested in #8) 347 | * Deprecate direct returning in favour of a callback passed to each operative invocation. 348 | * Fallback to IFrame (to provide safer sandbox for degraded state) 349 | * 0.1.0 (25 Jul 2013) Support Promises (from [Issue #3](https://github.com/padolsey/operative/issues/3)) if they're provided by a [native Promise implementation](http://dom.spec.whatwg.org/#promises) or [compliant polyfill](https://github.com/slightlyoff/Promises). Also added support for `operative(Function)` which returns a single function. 350 | * 0.0.3 (18 Jul 2013) Support for asynchronous returning from within operative methods (via `this.async()`). 351 | * 0.0.2 (12 Jul 2013) Improved browser support: IE10 support via eval, degraded JSON-marshalling etc. 352 | * 0.0.1 (11 Jul 2013) Initial 353 | 354 | ### Contributors 355 | 356 | * [James Padolsey](https://github.com/padolsey) 357 | * [Evan Worley](https://github.com/evanworley) 358 | * [Raynos](https://github.com/Raynos) 359 | * [Calvin Metcalf](https://github.com/calvinmetcalf) 360 | * [PG Herveou](https://github.com/pgherveou) 361 | * [Luke Bunselmeyer](https://github.com/wmluke) 362 | * [Rich Harris](https://github.com/Rich-Harris) 363 | * [Riley Shaw](https://github.com/rileyjshaw) 364 | -------------------------------------------------------------------------------- /src/OperativeContext.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | if (typeof window == 'undefined' && self.importScripts) { 4 | // Exit if operative.js is being loaded as worker (no blob support flow); 5 | return; 6 | } 7 | 8 | var hasOwn = {}.hasOwnProperty; 9 | var slice = [].slice; 10 | var toString = {}.toString; 11 | 12 | operative.Operative = OperativeContext; 13 | 14 | var Promise = OperativeContext.Promise = window.Promise; 15 | 16 | function OperativeTransfers(transfers) { 17 | this.value = transfers; 18 | } 19 | 20 | /** 21 | * OperativeContext 22 | * A type of context: could be a worker, an iframe, etc. 23 | * @param {Object} module Object containing methods/properties 24 | */ 25 | function OperativeContext(module, dependencies, getBaseURL, getSelfURL) { 26 | 27 | var _self = this; 28 | 29 | module.get = module.get || function(prop) { 30 | return this[prop]; 31 | }; 32 | 33 | module.set = module.set || function(prop, value) { 34 | return this[prop] = value; 35 | }; 36 | 37 | this._curToken = 0; 38 | this._queue = []; 39 | 40 | this._getBaseURL = getBaseURL; 41 | this._getSelfURL = getSelfURL; 42 | 43 | this.isDestroyed = false; 44 | this.isContextReady = false; 45 | 46 | this.module = module; 47 | this.dependencies = dependencies || []; 48 | 49 | this.dataProperties = {}; 50 | this.api = {}; 51 | this.callbacks = {}; 52 | this.deferreds = {}; 53 | 54 | this._fixDependencyURLs(); 55 | this._setup(); 56 | 57 | for (var methodName in module) { 58 | if (hasOwn.call(module, methodName)) { 59 | this._createExposedMethod(methodName); 60 | } 61 | } 62 | 63 | this.api.__operative__ = this; 64 | 65 | // Provide the instance's destroy method on the exposed API: 66 | this.api.destroy = this.api.terminate = function() { 67 | return _self.destroy(); 68 | }; 69 | 70 | } 71 | 72 | OperativeContext.prototype = { 73 | 74 | _marshal: function(v) { 75 | return v; 76 | }, 77 | 78 | _demarshal: function(v) { 79 | return v; 80 | }, 81 | 82 | _enqueue: function(fn) { 83 | this._queue.push(fn); 84 | }, 85 | 86 | _fixDependencyURLs: function() { 87 | var deps = this.dependencies; 88 | for (var i = 0, l = deps.length; i < l; ++i) { 89 | var dep = deps[i]; 90 | if (!/\/\//.test(dep)) { 91 | deps[i] = dep.replace(/^\/?/, this._getBaseURL().replace(/([^\/])$/, '$1/')); 92 | } 93 | } 94 | }, 95 | 96 | _dequeueAll: function() { 97 | for (var i = 0, l = this._queue.length; i < l; ++i) { 98 | this._queue[i].call(this); 99 | } 100 | this._queue = []; 101 | }, 102 | 103 | _buildContextScript: function(boilerScript) { 104 | 105 | var script = []; 106 | var module = this.module; 107 | var dataProperties = this.dataProperties; 108 | var property; 109 | 110 | for (var i in module) { 111 | property = module[i]; 112 | if (typeof property == 'function') { 113 | script.push(' self["' + i.replace(/"/g, '\\"') + '"] = ' + property.toString() + ';'); 114 | } else { 115 | dataProperties[i] = property; 116 | } 117 | } 118 | 119 | return script.join('\n') + ( 120 | boilerScript ? '\n(' + boilerScript.toString() + '());' : '' 121 | ); 122 | 123 | }, 124 | 125 | _createExposedMethod: function(methodName) { 126 | 127 | var self = this; 128 | 129 | var method = this.api[methodName] = function() { 130 | 131 | if (self.isDestroyed) { 132 | throw new Error('Operative: Cannot run method. Operative has already been destroyed'); 133 | } 134 | 135 | var token = ++self._curToken; 136 | var args = slice.call(arguments); 137 | var cb = typeof args[args.length - 1] == 'function' && args.pop(); 138 | var transferables = args[args.length - 1] instanceof OperativeTransfers && args.pop(); 139 | 140 | if (!cb && !Promise) { 141 | throw new Error( 142 | 'Operative: No callback has been passed. Assumed that you want a promise. ' + 143 | 'But `operative.Promise` is null. Please provide Promise polyfill/lib.' 144 | ); 145 | } 146 | 147 | if (cb) { 148 | 149 | self.callbacks[token] = cb; 150 | 151 | // Ensure either context runs the method async: 152 | setTimeout(function() { 153 | runMethod(); 154 | }, 1); 155 | 156 | } else if (Promise) { 157 | 158 | // No Callback -- Promise used: 159 | 160 | return new Promise(function(resolve, reject) { 161 | var deferred; 162 | 163 | if (resolve.fulfil || resolve.fulfill) { 164 | // Backwards compatibility 165 | deferred = resolve; 166 | deferred.fulfil = deferred.fulfill = resolve.fulfil || resolve.fulfill; 167 | } else { 168 | deferred = { 169 | // Deprecate: 170 | fulfil: resolve, 171 | fulfill: resolve, 172 | 173 | resolve: resolve, 174 | reject: reject, 175 | 176 | // For the iframe: 177 | transferResolve: resolve, 178 | transferReject: reject 179 | }; 180 | } 181 | 182 | self.deferreds[token] = deferred; 183 | runMethod(); 184 | }); 185 | 186 | } 187 | 188 | function runMethod() { 189 | if (self.isContextReady) { 190 | self._runMethod(methodName, token, args, transferables); 191 | } else { 192 | self._enqueue(runMethod); 193 | } 194 | } 195 | 196 | }; 197 | 198 | method.transfer = function() { 199 | 200 | var args = [].slice.call(arguments); 201 | var transfersIndex = typeof args[args.length - 1] == 'function' ? 202 | args.length - 2: 203 | args.length - 1; 204 | var transfers = args[transfersIndex]; 205 | var transfersType = toString.call(transfers); 206 | 207 | if (transfersType !== '[object Array]') { 208 | throw new Error( 209 | 'Operative:transfer() must be passed an Array of transfers as its last arguments ' + 210 | '(Expected: [object Array], Received: ' + transfersType + ')' 211 | ); 212 | } 213 | 214 | args[transfersIndex] = new OperativeTransfers(transfers); 215 | return method.apply(null, args); 216 | 217 | }; 218 | 219 | }, 220 | 221 | destroy: function() { 222 | this.isDestroyed = true; 223 | } 224 | }; 225 | 226 | })(); 227 | -------------------------------------------------------------------------------- /src/contexts/BrowserWorker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Web Worker context 3 | */ 4 | (function() { 5 | 6 | if (typeof window == 'undefined' && self.importScripts) { 7 | // I'm a worker! Run the boiler-script: 8 | // (Operative itself is called in IE10 as a worker, 9 | // to avoid SecurityErrors) 10 | workerBoilerScript(); 11 | return; 12 | } 13 | 14 | var Operative = operative.Operative; 15 | 16 | var URL = window.URL || window.webkitURL; 17 | var BlobBuilder = window.BlobBuilder || window.WebKitBlobBuilder || window.MozBlobBuilder; 18 | 19 | var workerViaBlobSupport = (function() { 20 | try { 21 | new Worker(makeBlobURI(';')); 22 | } catch(e) { 23 | return false; 24 | } 25 | return true; 26 | }()); 27 | 28 | var transferrableObjSupport = (function() { 29 | try { 30 | var ab = new ArrayBuffer(1); 31 | new Worker( makeBlobURI(';') ).postMessage(ab, [ab]); 32 | return !ab.byteLength; 33 | } catch(e) { 34 | return false; 35 | } 36 | }()); 37 | 38 | operative.hasWorkerViaBlobSupport = workerViaBlobSupport; 39 | operative.hasTransferSupport = transferrableObjSupport; 40 | 41 | function makeBlobURI(script) { 42 | var blob; 43 | 44 | try { 45 | blob = new Blob([script], { type: 'text/javascript' }); 46 | } catch (e) { 47 | blob = new BlobBuilder(); 48 | blob.append(script); 49 | blob = blob.getBlob(); 50 | } 51 | 52 | return URL.createObjectURL(blob); 53 | } 54 | 55 | /** 56 | * Operative BrowserWorker 57 | */ 58 | Operative.BrowserWorker = function BrowserWorker() { 59 | Operative.apply(this, arguments); 60 | }; 61 | 62 | var WorkerProto = Operative.BrowserWorker.prototype = operative.objCreate(Operative.prototype); 63 | 64 | WorkerProto._onWorkerMessage = function(e) { 65 | var data = e.data; 66 | 67 | if (typeof data === 'string' && data.indexOf('pingback') === 0) { 68 | if (data === 'pingback:structuredCloningSupport=NO') { 69 | // No structuredCloningSupport support (marshal JSON from now on): 70 | this._marshal = function(o) { return JSON.stringify(o); }; 71 | this._demarshal = function(o) { return JSON.parse(o); }; 72 | } 73 | 74 | this.isContextReady = true; 75 | this._postMessage({ 76 | definitions: this.dataProperties 77 | }); 78 | this._dequeueAll(); 79 | return; 80 | 81 | } 82 | 83 | data = this._demarshal(data); 84 | 85 | switch (data.cmd) { 86 | case 'console': 87 | window.console && window.console[data.method].apply(window.console, data.args); 88 | break; 89 | case 'deferred_reject_error': 90 | this.deferreds[data.token].reject(data.error); 91 | break; 92 | case 'result': 93 | 94 | var callback = this.callbacks[data.token]; 95 | var deferred = this.deferreds[data.token]; 96 | 97 | var deferredAction = data.result && data.result.isDeferred && data.result.action; 98 | 99 | if (deferred && deferredAction) { 100 | deferred[deferredAction](data.result.args[0]); 101 | } else if (callback) { 102 | callback.apply(this, data.result.args); 103 | } else if (deferred) { 104 | // Resolve promise even if result was given 105 | // via callback within the worker: 106 | deferred.fulfil(data.result.args[0]); 107 | } 108 | 109 | break; 110 | } 111 | }; 112 | 113 | WorkerProto._isWorkerViaBlobSupported = function() { 114 | return workerViaBlobSupport; 115 | }; 116 | 117 | WorkerProto._setup = function() { 118 | var self = this; 119 | 120 | var worker; 121 | var selfURL = this._getSelfURL(); 122 | var blobSupport = this._isWorkerViaBlobSupported(); 123 | var script = this._buildContextScript( 124 | // The script is not included if we're Eval'ing this file directly: 125 | blobSupport ? workerBoilerScript : '' 126 | ); 127 | 128 | if (this.dependencies.length) { 129 | script = 'importScripts("' + this.dependencies.join('", "') + '");\n' + script; 130 | } 131 | 132 | if (blobSupport) { 133 | worker = this.worker = new Worker( makeBlobURI(script) ); 134 | } else { 135 | 136 | if (!selfURL) { 137 | throw new Error('Operaritve: No operative.js URL available. Please set via operative.setSelfURL(...)'); 138 | } 139 | worker = this.worker = new Worker( selfURL ); 140 | // Marshal-agnostic initial message is boiler-code: 141 | // (We don't yet know if structured-cloning is supported so we send a string) 142 | worker.postMessage('EVAL|' + script); 143 | } 144 | 145 | worker.postMessage('EVAL|self.hasTransferSupport=' + transferrableObjSupport); 146 | worker.postMessage(['PING']); // Initial PING 147 | 148 | worker.addEventListener('message', function(e) { 149 | self._onWorkerMessage(e); 150 | }); 151 | }; 152 | 153 | WorkerProto._postMessage = function(msg) { 154 | var transfers = transferrableObjSupport && msg.transfers; 155 | return transfers ? 156 | this.worker.postMessage(msg, transfers.value) : 157 | this.worker.postMessage( 158 | this._marshal(msg) 159 | ); 160 | }; 161 | 162 | WorkerProto._runMethod = function(methodName, token, args, transfers) { 163 | this._postMessage({ 164 | method: methodName, 165 | args: args, 166 | token: token, 167 | transfers: transfers 168 | }); 169 | }; 170 | 171 | WorkerProto.destroy = function() { 172 | this.worker.terminate(); 173 | Operative.prototype.destroy.call(this); 174 | }; 175 | 176 | /** 177 | * The boilerplate for the Worker Blob 178 | * NOTE: 179 | * this'll be executed within an worker, not here. 180 | * Indented @ Zero to make nicer debug code within worker 181 | */ 182 | function workerBoilerScript() { 183 | 184 | var postMessage = self.postMessage; 185 | var structuredCloningSupport = null; 186 | var toString = {}.toString; 187 | 188 | self.console = {}; 189 | self.isWorker = true; 190 | 191 | // Provide basic console interface: 192 | ['log', 'debug', 'error', 'info', 'warn', 'time', 'timeEnd'].forEach(function(meth) { 193 | self.console[meth] = function() { 194 | postMessage({ 195 | cmd: 'console', 196 | method: meth, 197 | args: [].slice.call(arguments) 198 | }); 199 | }; 200 | }); 201 | 202 | self.addEventListener('message', function(e) { 203 | 204 | var data = e.data; 205 | 206 | if (typeof data == 'string' && data.indexOf('EVAL|') === 0) { 207 | eval(data.substring(5)); 208 | return; 209 | } 210 | 211 | if (structuredCloningSupport == null) { 212 | 213 | // e.data of ['PING'] (An array) indicates structuredCloning support 214 | // e.data of '"PING"' (A string) indicates no support (Array has been serialized) 215 | structuredCloningSupport = e.data[0] === 'PING'; 216 | 217 | // Pingback to parent page: 218 | self.postMessage( 219 | structuredCloningSupport ? 220 | 'pingback:structuredCloningSupport=YES' : 221 | 'pingback:structuredCloningSupport=NO' 222 | ); 223 | 224 | if (!structuredCloningSupport) { 225 | postMessage = function(msg) { 226 | // Marshal before sending 227 | return self.postMessage(JSON.stringify(msg)); 228 | }; 229 | } 230 | 231 | return; 232 | } 233 | 234 | if (!structuredCloningSupport) { 235 | // Demarshal: 236 | data = JSON.parse(data); 237 | } 238 | 239 | var defs = data.definitions; 240 | var isDeferred = false; 241 | var args = data.args; 242 | 243 | if (defs) { 244 | // Initial definitions: 245 | for (var i in defs) { 246 | self[i] = defs[i]; 247 | } 248 | return; 249 | } 250 | 251 | function callback() { 252 | // Callback function to be passed to operative method 253 | returnResult({ 254 | args: [].slice.call(arguments) 255 | }); 256 | } 257 | 258 | callback.transfer = function() { 259 | var args = [].slice.call(arguments); 260 | var transfers = extractTransfers(args); 261 | // Callback function to be passed to operative method 262 | returnResult({ 263 | args: args 264 | }, transfers); 265 | }; 266 | 267 | args.push(callback); 268 | 269 | self.deferred = function() { 270 | isDeferred = true; 271 | var def = {}; 272 | function resolve(r, transfers) { 273 | returnResult({ 274 | isDeferred: true, 275 | action: 'resolve', 276 | args: [r] 277 | }, transfers); 278 | return def; 279 | } 280 | function reject(r, transfers) { 281 | if (r instanceof Error) { 282 | // Create an error object that can be cloned: (See #44/#45): 283 | var cloneableError = { 284 | message: r.message, 285 | stack: r.stack, 286 | name: r.name, 287 | code: r.code 288 | }; 289 | for (var i in r) { 290 | if (r.hasOwnProperty(i)) { 291 | cloneableError[i] = r[i]; 292 | } 293 | } 294 | postMessage({ 295 | cmd: 'deferred_reject_error', 296 | token: data.token, 297 | error: cloneableError 298 | }); 299 | return; 300 | } 301 | returnResult({ 302 | isDeferred: true, 303 | action: 'reject', 304 | args: [r] 305 | }, transfers); 306 | } 307 | // Deprecated: 308 | def.fulfil = def.fulfill = def.resolve = function(value) { 309 | return resolve(value); 310 | }; 311 | def.reject = function(value) { 312 | return reject(value); 313 | }; 314 | def.transferResolve = function(value) { 315 | var transfers = extractTransfers(arguments); 316 | return resolve(value, transfers); 317 | }; 318 | def.transferReject = function(value) { 319 | var transfers = extractTransfers(arguments); 320 | return reject(value, transfers); 321 | }; 322 | return def; 323 | }; 324 | 325 | // Call actual operative method: 326 | var result = self[data.method].apply(self, args); 327 | 328 | if (!isDeferred && result !== void 0) { 329 | // Deprecated direct-returning as of 0.2.0 330 | returnResult({ 331 | args: [result] 332 | }); 333 | } 334 | 335 | self.deferred = function() { 336 | throw new Error('Operative: deferred() called at odd time'); 337 | }; 338 | 339 | function returnResult(res, transfers) { 340 | postMessage({ 341 | cmd: 'result', 342 | token: data.token, 343 | result: res 344 | }, self.hasTransferSupport && transfers || []); 345 | } 346 | 347 | function extractTransfers(args) { 348 | var transfers = args[args.length - 1]; 349 | 350 | if (toString.call(transfers) !== '[object Array]') { 351 | throw new Error('Operative: callback.transfer() must be passed an Array of transfers as its last arguments'); 352 | } 353 | 354 | return transfers; 355 | } 356 | }); 357 | } 358 | 359 | })(); 360 | -------------------------------------------------------------------------------- /src/contexts/Iframe.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Iframe (degraded) context 3 | * 4 | * This executes the code in an iframe, via a zero-timeout on each method call. 5 | */ 6 | (function() { 7 | 8 | if (typeof window == 'undefined' && self.importScripts) { 9 | // Exit if operative.js is being loaded as worker (no blob support flow); 10 | return; 11 | } 12 | 13 | var Operative = operative.Operative; 14 | 15 | /** 16 | * Operative IFrame 17 | */ 18 | Operative.Iframe = function Iframe(module) { 19 | Operative.apply(this, arguments); 20 | }; 21 | 22 | var IframeProto = Operative.Iframe.prototype = operative.objCreate(Operative.prototype); 23 | 24 | var _loadedMethodNameI = 0; 25 | 26 | IframeProto._setup = function() { 27 | 28 | var self = this; 29 | var loadedMethodName = '__operativeIFrameLoaded' + (++_loadedMethodNameI); 30 | 31 | this.module.isWorker = false; 32 | 33 | var iframe = this.iframe = document.body.appendChild( 34 | document.createElement('iframe') 35 | ); 36 | 37 | iframe.style.display = 'none'; 38 | 39 | var iWin = this.iframeWindow = iframe.contentWindow; 40 | var iDoc = iWin.document; 41 | 42 | // Cross browser (tested in IE8,9) way to call method from within 43 | // IFRAME after all < script >s have loaded: 44 | window[loadedMethodName] = function() { 45 | 46 | window[loadedMethodName] = null; 47 | 48 | var script = iDoc.createElement('script'); 49 | var js = self._buildContextScript(iframeBoilerScript); 50 | 51 | if (script.text !== void 0) { 52 | script.text = js; 53 | } else { 54 | script.innerHTML = js; 55 | } 56 | 57 | iDoc.documentElement.appendChild(script); 58 | 59 | for (var i in self.dataProperties) { 60 | iWin[i] = self.dataProperties[i]; 61 | } 62 | 63 | self.isContextReady = true; 64 | self._dequeueAll(); 65 | 66 | }; 67 | 68 | iDoc.open(); 69 | 70 | var documentContent = ''; 71 | 72 | if (this.dependencies.length) { 73 | documentContent += '\n 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /test/benchmarks/suite.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function computation(done) { 4 | // Do something silly and slow: 5 | var r = Math.random(); 6 | done( 7 | Array(4000).join( 8 | [+new Date, +new Date].join(';') 9 | ).split('').map(function(a) { 10 | return a.charCodeAt(0) * r; 11 | }) 12 | ); 13 | } 14 | 15 | var test = (function() { 16 | 17 | var queue = []; 18 | 19 | var hrtime = typeof performance != 'undefined' ? function(prev) { 20 | prev = prev || [0, 0]; 21 | // From [seconds, nanoseconds] to MS 22 | prev = prev[1] / 1e6 + (prev[0] * 1000); 23 | var now = performance.now() - prev; 24 | var s = 0 | now / 1000; 25 | // Back to [seconds, ~nanoseconds] 26 | return [s, 0 | (now - s * 1e3) * 1e6]; 27 | } : process.hrtime; 28 | 29 | function runTest(name, fn) { 30 | 31 | console.log('Running: ', name) 32 | 33 | var SAMPLE_SIZE = 1; 34 | var total = 0; 35 | var i = 0; 36 | 37 | setTimeout(tick, 100); 38 | 39 | function tick() { 40 | 41 | if (i >= SAMPLE_SIZE) { 42 | console.log(name + ': Avg. ' + total/SAMPLE_SIZE + 'ms'); 43 | dequeue(); 44 | return; 45 | } 46 | 47 | var now = hrtime(); 48 | 49 | fn(function(cb) { 50 | var diff = hrtime(now); 51 | total += diff[1] / 1e6 + (diff[0] * 1000); 52 | cb && cb(); // for the test to terminate its things 53 | ++i; 54 | setTimeout(tick, 100); 55 | }); 56 | } 57 | 58 | } 59 | 60 | function dequeue() { 61 | queue.shift(); 62 | if (queue.length) { 63 | queue[0](); 64 | } 65 | } 66 | 67 | return function(name, fn) { 68 | var i = queue.push(function() { 69 | runTest(name, fn); 70 | }); 71 | if (i === 1) { // was first element in queue 72 | queue[0](); 73 | } 74 | }; 75 | 76 | }()); 77 | 78 | 79 | test('Sync In Page', function(done) { 80 | for (var i = 0; i < 36; i++) { 81 | computation(function(){}); 82 | } 83 | done(); 84 | }); 85 | 86 | test('Operative #1', function(done) { 87 | var c = operative({ 88 | exec: function(cb) { 89 | for (var i = 0; i < 36; i++) { 90 | this.computation(function(){}); 91 | } 92 | cb(); 93 | }, 94 | computation: computation 95 | }); 96 | c.exec(function(x) { 97 | console.log('GotResult') 98 | done(function() { 99 | console.log('Terminating'); 100 | c.terminate(); 101 | }); 102 | }); 103 | }); 104 | 105 | test('Operative pool:2', function(done) { 106 | var c = operative.pool(2, { 107 | exec: function(cb) { 108 | for (var i = 0; i < 18; i++) { 109 | this.computation(function(){}); 110 | } 111 | cb(); 112 | }, 113 | computation: computation 114 | }); 115 | var completed = 0; 116 | function check() { 117 | completed++; 118 | if (completed === 2) { 119 | done(function() { 120 | console.log('Terminating'); 121 | c.terminate(); 122 | }); 123 | } 124 | } 125 | c.next().exec(check); 126 | c.next().exec(check); 127 | }); 128 | 129 | test('Operative pool:4', function(done) { 130 | var c = operative.pool(4, { 131 | exec: function(cb) { 132 | for (var i = 0; i < 9; i++) { 133 | this.computation(function(){}); 134 | } 135 | cb(); 136 | }, 137 | computation: computation 138 | }); 139 | var completed = 0; 140 | function check() { 141 | completed++; 142 | if (completed === 4) { 143 | done(function() { 144 | console.log('Terminating'); 145 | c.terminate(); 146 | }); 147 | } 148 | } 149 | c.next().exec(check); 150 | c.next().exec(check); 151 | c.next().exec(check); 152 | c.next().exec(check); 153 | }); 154 | 155 | test('4 M/B w/o transfer', function(done) { 156 | var o = operative(function(d, cb) { 157 | cb(); 158 | }); 159 | 160 | o(new Uint8Array(1024*1024*4), function() { 161 | done() 162 | }); 163 | }); 164 | 165 | test('4 M/B w/ transfer', function(done) { 166 | var o = operative(function(d, cb) { 167 | cb(); 168 | }); 169 | 170 | var arr = new Uint8Array(1024*1024*4); 171 | 172 | o.transfer(arr.buffer, [arr.buffer], function() { 173 | done(); 174 | }); 175 | }); -------------------------------------------------------------------------------- /test/demos/img.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/padolsey/operative/b1353cb6961d8bc4f410149401e950d111e6e9b3/test/demos/img.jpg -------------------------------------------------------------------------------- /test/demos/noise.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Noisy Pixels Demo 4 | 5 | 6 |

Noisy Pixels

7 | 8 | 9 | 10 | 48 | 49 | -------------------------------------------------------------------------------- /test/demos/promises.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Promise demo [incl. Polyfill] 4 | 5 | 6 |

Promise demo [incl. Polyfill]

7 | 8 | 9 | 10 | 11 | 14 | 15 | -------------------------------------------------------------------------------- /test/operative.iframe.spec.js: -------------------------------------------------------------------------------- 1 | describe('Operative (forced iframe context)', function() { 2 | 3 | beforeEach(function() { 4 | operative.hasWorkerSupport = false; 5 | }); 6 | 7 | afterEach(function() { 8 | operative.hasWorkerSupport = true; 9 | }); 10 | 11 | describe('Callback', function() { 12 | it('Is called and removed correctly', function(done) { 13 | var o = operative({ 14 | longAction: function(cb) { 15 | for (var i = 0; i < 100000; ++i); 16 | cb(); 17 | } 18 | }); 19 | 20 | function callback() { 21 | done(); 22 | } 23 | 24 | o.longAction(callback); 25 | expect(o.__operative__.callbacks[1]).to.equal(callback); 26 | 27 | }); 28 | }); 29 | 30 | describe('Multiple Operatives', function() { 31 | it('Each complete asynchronously', function(done) { 32 | var s = []; 33 | var a = operative({ 34 | run: function(cb) { 35 | for (var i = 0; i < 1000000; ++i); 36 | cb('A'); 37 | } 38 | }); 39 | var b = operative({ 40 | run: function(cb) { 41 | for (var i = 0; i < 1000; ++i); 42 | cb('B'); 43 | } 44 | }); 45 | var c = operative({ 46 | run: function(cb) { 47 | for (var i = 0; i < 1; ++i); 48 | cb('C'); 49 | } 50 | }); 51 | 52 | function add(v) { 53 | s.push(v); 54 | if (s.length === 3) { 55 | expect(s.sort().join('')).to.equal('ABC'); 56 | done(); 57 | } 58 | } 59 | 60 | a.run(add); 61 | b.run(add); 62 | c.run(add); 63 | }); 64 | }); 65 | 66 | describe('Promise API', function() { 67 | describe('Operative', function() { 68 | var op; 69 | beforeEach(function() { 70 | op = operative(function(beSuccessful) { 71 | var deferred = this.deferred(); 72 | if (beSuccessful) { 73 | deferred.fulfil(873); 74 | } else { 75 | deferred.reject(999); 76 | } 77 | }); 78 | }) 79 | it('Should return a promise', function() { 80 | expect(op() instanceof operative.Operative.Promise).to.equal(true); 81 | }); 82 | describe('fulfil()', function() { 83 | it('Should fulfil the exposed promise', function(done) { 84 | op(true).then(function(a) { 85 | expect(a).to.equal(873); 86 | done(); 87 | }, function() {}); 88 | }); 89 | }); 90 | describe('reject()', function() { 91 | it('Should reject the exposed promise', function(done) { 92 | op(false).then(function() { 93 | expect(true).to.equal(false); // this should not run 94 | }, function(err) { 95 | expect(err).to.equal(999); 96 | done(); 97 | }); 98 | }); 99 | }); 100 | describe('Rejecting with an error', function() { 101 | it('Should reject correctly', function(done) { 102 | var op = operative(function() { 103 | var deferred = this.deferred(); 104 | deferred.reject(new Error('foo 789')); 105 | }); 106 | op().then(function() { 107 | expect(true).to.be.false; // fail 108 | done(); 109 | }).catch(function(err) { 110 | console.log(err); 111 | expect(err.message).to.equal('foo 789'); // pass 112 | done(); 113 | }); 114 | }); 115 | }); 116 | }); 117 | }); 118 | 119 | describe('An example stream of operations', function() { 120 | 121 | it('Works with basic calculator', function(done) { 122 | var o = operative({ 123 | something: 3333, 124 | setup: function(cb) { 125 | this.somethingElse = 4444; 126 | cb(); 127 | }, 128 | add: function(a, b) { 129 | return a + b; 130 | }, 131 | subtract: function(a, b) { 132 | return a - b; 133 | }, 134 | getSomething: function() { 135 | return this.something; 136 | }, 137 | getSomethingElse: function() { 138 | return this.somethingElse; 139 | }, 140 | isItAWorker: function() { 141 | return this.isWorker; 142 | }, 143 | isItAWorker_Promisable: function() { 144 | this.deferred().fulfil(this.isWorker); 145 | } 146 | }); 147 | 148 | o.setup() 149 | .then(function() { 150 | return o.add(1, 2); 151 | }) 152 | .then(function(n) { 153 | expect(n).to.equal(3); 154 | }) 155 | .then(o.isItAWorker_Promisable) 156 | .then(function(isWorker) { 157 | expect(isWorker).to.equal(false); 158 | }) 159 | .then(o.isItAWorker) 160 | .then(function(isWorker) { 161 | expect(isWorker).to.equal(false); 162 | }) 163 | .then(o.getSomething) 164 | .then(function(n) { 165 | expect(n).to.equal(3333); 166 | }) 167 | .then(o.getSomethingElse) 168 | .then(function(n) { 169 | expect(n).to.equal(4444); 170 | }) 171 | .then(function() { 172 | return o.subtract(100, 2); 173 | }) 174 | .then(function(n) { 175 | expect(n).to.equal(98); 176 | }) 177 | .then(done, done); 178 | 179 | }); 180 | 181 | }); 182 | 183 | describe('Perpetual callbacks', function() { 184 | 185 | it('Is possible to keep calling the same callback (with progress etc.)', function(done) { 186 | 187 | var o = operative({ 188 | process: function(cb) { 189 | 190 | var progress = 0; 191 | var me = setInterval(function() { 192 | progress++; 193 | if (progress === 5) { 194 | clearInterval(me); 195 | cb({ progress: 5, done: true }); 196 | } else { 197 | cb({ progress: progress, done: false }); 198 | } 199 | }, 30); 200 | 201 | } 202 | }); 203 | 204 | var called = 0; 205 | 206 | o.process(function(status) { 207 | called++; 208 | expect(status.progress).to.be.above(0); 209 | if (status.progress === 5) { 210 | expect(status.done).to.equal(true); 211 | expect(called).to.equal(5); // called five times 212 | done(); 213 | } 214 | }); 215 | 216 | }); 217 | 218 | }); 219 | 220 | 221 | describe('Transfers', function() { 222 | describe('Using callback API', function() { 223 | it('Works in fallback state (no ownership transfer)', function(done) { 224 | 225 | if (typeof Uint8Array == 'undefined') { 226 | done(); 227 | return; 228 | } 229 | 230 | var o = operative({ 231 | receive: function(t, cb) { 232 | self.arr = new Uint8Array([1,2,3]); 233 | cb.transfer(self.arr.buffer, [self.arr.buffer]); 234 | }, 235 | isYourByteLengthIsNonEmpty: function(cb) { 236 | cb( 237 | 3 == (self.arr.buffer ? self.arr.buffer.byteLength : self.arr.byteLength) 238 | ); 239 | } 240 | }); 241 | 242 | var a = new Uint8Array([33,22,11]); 243 | 244 | o.receive.transfer(a.buffer, [a.buffer], function(r) { 245 | expect(a.buffer ? a.buffer.byteLength : a.byteLength).to.equal(3); 246 | o.isYourByteLengthIsNonEmpty(function(result) { 247 | expect(result).to.be.true; 248 | done(); 249 | }) 250 | }); 251 | 252 | }); 253 | }); 254 | describe('Using promise API', function() { 255 | it('Works in fallback state (no ownership transfer)', function(done) { 256 | 257 | if (typeof Uint8Array == 'undefined') { 258 | done(); 259 | return; 260 | } 261 | 262 | var o = operative({ 263 | receive: function(t, cb) { 264 | self.arr = new Uint8Array([1,2,3]); 265 | var def = this.deferred(); 266 | def.transferResolve(self.arr.buffer, [self.arr.buffer]); 267 | }, 268 | isYourByteLengthIsNonEmpty: function(cb) { 269 | this.deferred().resolve( 270 | 3 == (self.arr.buffer ? self.arr.buffer.byteLength : self.arr.byteLength) 271 | ); 272 | } 273 | }); 274 | 275 | var a = new Uint8Array([33,22,11]); 276 | 277 | o.receive.transfer(a.buffer, [a.buffer]) 278 | .then(function(r) { 279 | expect(a.buffer ? a.buffer.byteLength : a.byteLength).to.equal(3); 280 | return o.isYourByteLengthIsNonEmpty().then(function(result) { 281 | expect(result).to.be.true; 282 | }) 283 | }).then(done, done); 284 | 285 | }); 286 | }); 287 | }); 288 | 289 | }); -------------------------------------------------------------------------------- /test/operative.spec.js: -------------------------------------------------------------------------------- 1 | describe('Operative (worker Context)', function() { 2 | 3 | describe('Callback', function() { 4 | it('Is called correctly', function(done) { 5 | var o = operative({ 6 | longAction: function(cb) { 7 | for (var i = 0; i < 10000000; ++i); 8 | cb(); 9 | } 10 | }); 11 | 12 | function callback() { 13 | done(); 14 | } 15 | 16 | o.longAction(callback); 17 | expect(o.__operative__.callbacks[1]).to.equal(callback); 18 | 19 | }); 20 | }); 21 | 22 | describe('Destroying', function() { 23 | it('Should have destroy + terminate alias', function() { 24 | expect(function() { 25 | operative(function(){}).destroy(); 26 | operative(function(){}).terminate(); 27 | }).to.not.throw(); 28 | }); 29 | }); 30 | 31 | describe('Multiple Operatives', function() { 32 | it('Each complete asynchronously', function(done) { 33 | var s = []; 34 | var a = operative({ 35 | run: function(cb) { 36 | for (var i = 0; i < 1000000; ++i); 37 | cb('A'); 38 | } 39 | }); 40 | var b = operative({ 41 | run: function(cb) { 42 | for (var i = 0; i < 1000; ++i); 43 | cb('B'); 44 | } 45 | }); 46 | var c = operative({ 47 | run: function(cb) { 48 | for (var i = 0; i < 1; ++i); 49 | cb('C'); 50 | } 51 | }); 52 | function add(v) { 53 | s.push(v); 54 | if (s.length === 3) { 55 | expect(s.sort().join('')).to.equal('ABC'); 56 | done(); 57 | } 58 | } 59 | 60 | a.run(add); 61 | b.run(add); 62 | c.run(add); 63 | 64 | }); 65 | }); 66 | 67 | describe('Promise API', function() { 68 | describe('Operative', function() { 69 | var op; 70 | beforeEach(function() { 71 | op = operative(function(beSuccessful, cb) { 72 | var deferred = this.deferred(); 73 | if (beSuccessful) { 74 | deferred.fulfil(873); 75 | } else { 76 | deferred.reject(999); 77 | } 78 | }); 79 | }) 80 | it('Should return a promise', function() { 81 | expect(op() instanceof operative.Operative.Promise).to.be.true; 82 | }); 83 | describe('fulfil()', function() { 84 | it('Should fulfil the exposed promise', function(done) { 85 | var fulfilled = false; 86 | 87 | op(true).then(function(a) { 88 | expect(a).to.equal(873); 89 | done(); 90 | }, function() { 91 | expect(true).to.be.false; // this should not run 92 | done(); 93 | }); 94 | 95 | }); 96 | }); 97 | describe('reject()', function() { 98 | it('Should reject the exposed promise', function(done) { 99 | var rejected = false; 100 | var fulfilled = false; 101 | op(false).then(function() { 102 | expect(true).to.be.false; // this should not run 103 | done(); 104 | }, function(err) { 105 | expect(err).to.equal(999); 106 | done(); 107 | }); 108 | }); 109 | }); 110 | describe('Rejecting with an error', function() { 111 | it('Should reject correctly', function(done) { 112 | var op = operative(function() { 113 | var deferred = this.deferred(); 114 | deferred.reject(new Error('foo 789')); 115 | }); 116 | op().then(function() { 117 | expect(true).to.be.false; // fail 118 | done(); 119 | }).catch(function(err) { 120 | expect(err.message).to.equal('foo 789'); // pass 121 | done(); 122 | }); 123 | }); 124 | describe('With additional props', function() { 125 | it('Should reject correctly and be received with props', function(done) { 126 | var op = operative(function() { 127 | var deferred = this.deferred(); 128 | var error = new Error('foo'); 129 | error.custom = 123; 130 | deferred.reject(error); 131 | }); 132 | op().then(function() { 133 | expect(true).to.be.false; // fail 134 | done(); 135 | }).catch(function(err) { 136 | expect(err.message).to.equal('foo'); 137 | expect(err.custom).to.equal(123); 138 | done(); 139 | }); 140 | }); 141 | }); 142 | }); 143 | }); 144 | }); 145 | 146 | describe('An example stream of operations', function() { 147 | 148 | it('Works with basic calculator', function(done) { 149 | var o = operative({ 150 | something: 3333, 151 | setup: function(cb) { 152 | this.somethingElse = 4444; 153 | cb(); 154 | }, 155 | add: function(a, b) { 156 | return a + b; 157 | }, 158 | subtract: function(a, b) { 159 | return a - b; 160 | }, 161 | getSomething: function() { 162 | return this.something; 163 | }, 164 | getSomethingElse: function() { 165 | return this.somethingElse; 166 | }, 167 | isItAWorker: function() { 168 | return this.isWorker; 169 | }, 170 | isItAWorker_Promisable: function() { 171 | this.deferred().fulfil(this.isWorker); 172 | } 173 | }); 174 | 175 | o.setup() 176 | .then(function() { 177 | return o.add(1, 2); 178 | }) 179 | .then(function(n) { 180 | expect(n).to.equal(3); 181 | }) 182 | .then(o.isItAWorker_Promisable) 183 | .then(function(isWorker) { 184 | expect(isWorker).to.equal( operative.hasWorkerSupport ); 185 | }) 186 | .then(o.isItAWorker) 187 | .then(function(isWorker) { 188 | expect(isWorker).to.equal( operative.hasWorkerSupport ); 189 | }) 190 | .then(o.getSomething) 191 | .then(function(n) { 192 | expect(n).to.equal(3333); 193 | }) 194 | .then(o.getSomethingElse) 195 | .then(function(n) { 196 | expect(n).to.equal(4444); 197 | }) 198 | .then(function() { 199 | return o.subtract(100, 2); 200 | }) 201 | .then(function(n) { 202 | expect(n).to.equal(98); 203 | }) 204 | .then(done, done); 205 | 206 | }); 207 | 208 | }); 209 | 210 | describe('Dependency loading', function() { 211 | 212 | it('Can load dependencies', function(done) { 213 | var o = operative({ 214 | typeofDependencies: function(cb) { 215 | cb([ typeof dependency1, typeof dependency2 ]); 216 | } 217 | }, ['dependency1.js', 'dependency2.js']); 218 | 219 | o.typeofDependencies(function(t) { 220 | expect(t).to.deep.equal(['function', 'function']); 221 | done(); 222 | }); 223 | }); 224 | 225 | it('Can load external dependencies', function(done) { 226 | var o = operative({ 227 | version_: function(cb) { 228 | cb( _.VERSION ); 229 | } 230 | }, ['http://cdnjs.cloudflare.com/ajax/libs/lodash.js/1.3.1/lodash.min.js']); 231 | 232 | o.version_(function(t) { 233 | expect(t).to.equal('1.3.1'); 234 | done(); 235 | }); 236 | 237 | }); 238 | }); 239 | 240 | describe('No blob-support case', function() { 241 | beforeEach(function() { 242 | operative.Operative.BrowserWorker.prototype._isWorkerViaBlobSupported = function() { 243 | return false; 244 | }; 245 | }); 246 | it('Works correctly', function(done) { 247 | var o = operative(function() { 248 | return 888; 249 | }); 250 | o(function(v) { 251 | expect(v).to.equal(888); 252 | done(); 253 | }); 254 | }); 255 | describe('Explicitly setting the self URL', function() { 256 | beforeEach(function() { 257 | operative.setSelfURL( 258 | '../../src/contexts/BrowserWorker.js?test-explicit' 259 | ); 260 | }); 261 | it('Works; a new Worker is created with that URL', function(done) { 262 | var o = operative(function() { 263 | return self.location.href; 264 | }); 265 | o(function(url) { 266 | if (operative.hasWorkerSupport) { 267 | // Worker context 268 | expect(url).to.contain('?test-explicit'); 269 | } else { 270 | // Iframe context (self-url changing does nothing) 271 | expect(url).to.contain('.html'); 272 | } 273 | done(); 274 | }); 275 | }); 276 | }); 277 | }); 278 | 279 | describe('Perpetual callbacks', function() { 280 | 281 | it('Is possible to keep calling the same callback (with progress etc.)', function(done) { 282 | 283 | var o = operative({ 284 | process: function(cb) { 285 | 286 | var progress = 0; 287 | var me = setInterval(function() { 288 | progress++; 289 | if (progress === 5) { 290 | clearInterval(me); 291 | cb({ progress: 5, done: true }); 292 | } else { 293 | cb({ progress: progress, done: false }); 294 | } 295 | }, 30); 296 | 297 | } 298 | }); 299 | 300 | var called = 0; 301 | 302 | o.process(function(status) { 303 | called++; 304 | expect(status.progress).to.be.above(0); 305 | if (status.progress === 5) { 306 | expect(status.done).to.equal(true); 307 | expect(called).to.equal(5); // called five times 308 | done(); 309 | } 310 | }); 311 | 312 | }); 313 | 314 | }); 315 | 316 | describe('Transfers', function() { 317 | describe('Using callback API', function() { 318 | it('Transfers ownership of the buffer', function(done) { 319 | 320 | if (!operative.hasTransferSupport && typeof Uint8Array == 'undefined') { 321 | done(); 322 | return; 323 | } 324 | 325 | var o = operative({ 326 | receive: function(t, cb) { 327 | self.arr = new Uint8Array([1,2,3]); 328 | cb.transfer(self.arr.buffer, [self.arr.buffer]); 329 | }, 330 | isYourByteLengthEmpty: function(cb) { 331 | cb( 332 | 0 == (self.arr.buffer ? self.arr.buffer.byteLength : self.arr.byteLength) 333 | ); 334 | } 335 | }); 336 | 337 | var a = new Uint8Array([33,22,11]); 338 | 339 | o.receive.transfer(a.buffer, [a.buffer], function(r) { 340 | if (operative.hasTransferSupport) { 341 | expect(a.buffer ? a.buffer.byteLength : a.byteLength).to.equal(0); 342 | } 343 | o.isYourByteLengthEmpty(function(result) { 344 | if (operative.hasTransferSupport) { 345 | expect(result).to.be.true; 346 | } 347 | done(); 348 | }) 349 | }); 350 | 351 | }); 352 | }); 353 | describe('Using promise API', function() { 354 | it('Transfers ownership of the buffer', function(done) { 355 | 356 | if (!operative.hasTransferSupport && typeof Uint8Array == 'undefined') { 357 | done(); 358 | return; 359 | } 360 | 361 | var o = operative({ 362 | receive: function(t, cb) { 363 | self.arr = new Uint8Array([1,2,3]); 364 | var def = this.deferred(); 365 | def.transferResolve(self.arr.buffer, [self.arr.buffer]); 366 | }, 367 | isYourByteLengthEmpty: function(cb) { 368 | this.deferred().resolve( 369 | 0 == (self.arr.buffer ? self.arr.buffer.byteLength : self.arr.byteLength) 370 | ); 371 | } 372 | }); 373 | 374 | var a = new Uint8Array([33,22,11]); 375 | 376 | o.receive.transfer(a.buffer, [a.buffer]) 377 | .then(function(r) { 378 | if (operative.hasTransferSupport) { 379 | expect(a.buffer ? a.buffer.byteLength : a.byteLength).to.equal(0); 380 | } 381 | return o.isYourByteLengthEmpty().then(function(result) { 382 | if (operative.hasTransferSupport) { 383 | expect(result).to.be.true; 384 | } 385 | }) 386 | }).then(done, done); 387 | 388 | }); 389 | }); 390 | }); 391 | 392 | }); -------------------------------------------------------------------------------- /test/resources/dependency1.js: -------------------------------------------------------------------------------- 1 | function dependency1() {} 2 | -------------------------------------------------------------------------------- /test/resources/dependency2.js: -------------------------------------------------------------------------------- 1 | function dependency2() {} 2 | -------------------------------------------------------------------------------- /test/resources/dev.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 32 | 33 | -------------------------------------------------------------------------------- /test/resources/run.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Mocha 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /test/resources/run_dist.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Mocha 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | 22 | 23 | 24 | 25 | 26 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /test/resources/specHelpers.js: -------------------------------------------------------------------------------- 1 | // Helpers can go here... 2 | 3 | var async = function async(fn) { 4 | // Little wrapper for async tests 5 | jasmine.getEnv().currentSpec.queue.add({ 6 | execute: function(next) { 7 | fn(next); 8 | } 9 | }); 10 | }; 11 | 12 | if (/_SpecRunner/.test(location.href)) { 13 | // Ensure correct base-url for grunt-run jasmine: 14 | operative.setBaseURL( operative.getBaseURL() + 'test/resources/' ); 15 | } 16 | -------------------------------------------------------------------------------- /test/resources/swarm_run.html: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | Mocha 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 24 | 25 | 26 | 27 | 28 | 32 | 33 | 34 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /vendor/Promise.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var define, requireModule, require, requirejs; 3 | 4 | (function() { 5 | var registry = {}, seen = {}; 6 | 7 | define = function(name, deps, callback) { 8 | registry[name] = { deps: deps, callback: callback }; 9 | }; 10 | 11 | requirejs = require = requireModule = function(name) { 12 | requirejs._eak_seen = registry; 13 | 14 | if (seen[name]) { return seen[name]; } 15 | seen[name] = {}; 16 | 17 | if (!registry[name]) { 18 | throw new Error("Could not find module " + name); 19 | } 20 | 21 | var mod = registry[name], 22 | deps = mod.deps, 23 | callback = mod.callback, 24 | reified = [], 25 | exports; 26 | 27 | for (var i=0, l=deps.length; i -1; i -= 1) { 72 | if (ary[i] && func(ary[i], i, ary)) { 73 | break; 74 | } 75 | } 76 | } 77 | } 78 | 79 | function hasProp(obj, prop) { 80 | return hasOwn.call(obj, prop); 81 | } 82 | 83 | function getOwn(obj, prop) { 84 | return hasProp(obj, prop) && obj[prop]; 85 | } 86 | 87 | /** 88 | * Cycles over properties in an object and calls a function for each 89 | * property value. If the function returns a truthy value, then the 90 | * iteration is stopped. 91 | */ 92 | function eachProp(obj, func) { 93 | var prop; 94 | for (prop in obj) { 95 | if (hasProp(obj, prop)) { 96 | if (func(obj[prop], prop)) { 97 | break; 98 | } 99 | } 100 | } 101 | } 102 | 103 | /** 104 | * Simple function to mix in properties from source into target, 105 | * but only if target does not already have a property of the same name. 106 | */ 107 | function mixin(target, source, force, deepStringMixin) { 108 | if (source) { 109 | eachProp(source, function (value, prop) { 110 | if (force || !hasProp(target, prop)) { 111 | if (deepStringMixin && typeof value !== 'string') { 112 | if (!target[prop]) { 113 | target[prop] = {}; 114 | } 115 | mixin(target[prop], value, force, deepStringMixin); 116 | } else { 117 | target[prop] = value; 118 | } 119 | } 120 | }); 121 | } 122 | return target; 123 | } 124 | 125 | //Similar to Function.prototype.bind, but the 'this' object is specified 126 | //first, since it is easier to read/figure out what 'this' will be. 127 | function bind(obj, fn) { 128 | return function () { 129 | return fn.apply(obj, arguments); 130 | }; 131 | } 132 | 133 | function scripts() { 134 | return document.getElementsByTagName('script'); 135 | } 136 | 137 | function defaultOnError(err) { 138 | throw err; 139 | } 140 | 141 | //Allow getting a global that expressed in 142 | //dot notation, like 'a.b.c'. 143 | function getGlobal(value) { 144 | if (!value) { 145 | return value; 146 | } 147 | var g = global; 148 | each(value.split('.'), function (part) { 149 | g = g[part]; 150 | }); 151 | return g; 152 | } 153 | 154 | /** 155 | * Constructs an error with a pointer to an URL with more information. 156 | * @param {String} id the error ID that maps to an ID on a web page. 157 | * @param {String} message human readable error. 158 | * @param {Error} [err] the original error, if there is one. 159 | * 160 | * @returns {Error} 161 | */ 162 | function makeError(id, msg, err, requireModules) { 163 | var e = new Error(msg + '\nhttp://requirejs.org/docs/errors.html#' + id); 164 | e.requireType = id; 165 | e.requireModules = requireModules; 166 | if (err) { 167 | e.originalError = err; 168 | } 169 | return e; 170 | } 171 | 172 | if (typeof define !== 'undefined') { 173 | //If a define is already in play via another AMD loader, 174 | //do not overwrite. 175 | return; 176 | } 177 | 178 | if (typeof requirejs !== 'undefined') { 179 | if (isFunction(requirejs)) { 180 | //Do not overwrite and existing requirejs instance. 181 | return; 182 | } 183 | cfg = requirejs; 184 | requirejs = undefined; 185 | } 186 | 187 | //Allow for a require config object 188 | if (typeof require !== 'undefined' && !isFunction(require)) { 189 | //assume it is a config object. 190 | cfg = require; 191 | require = undefined; 192 | } 193 | 194 | function newContext(contextName) { 195 | var inCheckLoaded, Module, context, handlers, 196 | checkLoadedTimeoutId, 197 | config = { 198 | //Defaults. Do not set a default for map 199 | //config to speed up normalize(), which 200 | //will run faster if there is no default. 201 | waitSeconds: 7, 202 | baseUrl: './', 203 | paths: {}, 204 | pkgs: {}, 205 | shim: {}, 206 | config: {} 207 | }, 208 | registry = {}, 209 | //registry of just enabled modules, to speed 210 | //cycle breaking code when lots of modules 211 | //are registered, but not activated. 212 | enabledRegistry = {}, 213 | undefEvents = {}, 214 | defQueue = [], 215 | defined = {}, 216 | urlFetched = {}, 217 | requireCounter = 1, 218 | unnormalizedCounter = 1; 219 | 220 | /** 221 | * Trims the . and .. from an array of path segments. 222 | * It will keep a leading path segment if a .. will become 223 | * the first path segment, to help with module name lookups, 224 | * which act like paths, but can be remapped. But the end result, 225 | * all paths that use this function should look normalized. 226 | * NOTE: this method MODIFIES the input array. 227 | * @param {Array} ary the array of path segments. 228 | */ 229 | function trimDots(ary) { 230 | var i, part; 231 | for (i = 0; ary[i]; i += 1) { 232 | part = ary[i]; 233 | if (part === '.') { 234 | ary.splice(i, 1); 235 | i -= 1; 236 | } else if (part === '..') { 237 | if (i === 1 && (ary[2] === '..' || ary[0] === '..')) { 238 | //End of the line. Keep at least one non-dot 239 | //path segment at the front so it can be mapped 240 | //correctly to disk. Otherwise, there is likely 241 | //no path mapping for a path starting with '..'. 242 | //This can still fail, but catches the most reasonable 243 | //uses of .. 244 | break; 245 | } else if (i > 0) { 246 | ary.splice(i - 1, 2); 247 | i -= 2; 248 | } 249 | } 250 | } 251 | } 252 | 253 | /** 254 | * Given a relative module name, like ./something, normalize it to 255 | * a real name that can be mapped to a path. 256 | * @param {String} name the relative name 257 | * @param {String} baseName a real name that the name arg is relative 258 | * to. 259 | * @param {Boolean} applyMap apply the map config to the value. Should 260 | * only be done if this normalization is for a dependency ID. 261 | * @returns {String} normalized name 262 | */ 263 | function normalize(name, baseName, applyMap) { 264 | var pkgName, pkgConfig, mapValue, nameParts, i, j, nameSegment, 265 | foundMap, foundI, foundStarMap, starI, 266 | baseParts = baseName && baseName.split('/'), 267 | normalizedBaseParts = baseParts, 268 | map = config.map, 269 | starMap = map && map['*']; 270 | 271 | //Adjust any relative paths. 272 | if (name && name.charAt(0) === '.') { 273 | //If have a base name, try to normalize against it, 274 | //otherwise, assume it is a top-level require that will 275 | //be relative to baseUrl in the end. 276 | if (baseName) { 277 | if (getOwn(config.pkgs, baseName)) { 278 | //If the baseName is a package name, then just treat it as one 279 | //name to concat the name with. 280 | normalizedBaseParts = baseParts = [baseName]; 281 | } else { 282 | //Convert baseName to array, and lop off the last part, 283 | //so that . matches that 'directory' and not name of the baseName's 284 | //module. For instance, baseName of 'one/two/three', maps to 285 | //'one/two/three.js', but we want the directory, 'one/two' for 286 | //this normalization. 287 | normalizedBaseParts = baseParts.slice(0, baseParts.length - 1); 288 | } 289 | 290 | name = normalizedBaseParts.concat(name.split('/')); 291 | trimDots(name); 292 | 293 | //Some use of packages may use a . path to reference the 294 | //'main' module name, so normalize for that. 295 | pkgConfig = getOwn(config.pkgs, (pkgName = name[0])); 296 | name = name.join('/'); 297 | if (pkgConfig && name === pkgName + '/' + pkgConfig.main) { 298 | name = pkgName; 299 | } 300 | } else if (name.indexOf('./') === 0) { 301 | // No baseName, so this is ID is resolved relative 302 | // to baseUrl, pull off the leading dot. 303 | name = name.substring(2); 304 | } 305 | } 306 | 307 | //Apply map config if available. 308 | if (applyMap && map && (baseParts || starMap)) { 309 | nameParts = name.split('/'); 310 | 311 | for (i = nameParts.length; i > 0; i -= 1) { 312 | nameSegment = nameParts.slice(0, i).join('/'); 313 | 314 | if (baseParts) { 315 | //Find the longest baseName segment match in the config. 316 | //So, do joins on the biggest to smallest lengths of baseParts. 317 | for (j = baseParts.length; j > 0; j -= 1) { 318 | mapValue = getOwn(map, baseParts.slice(0, j).join('/')); 319 | 320 | //baseName segment has config, find if it has one for 321 | //this name. 322 | if (mapValue) { 323 | mapValue = getOwn(mapValue, nameSegment); 324 | if (mapValue) { 325 | //Match, update name to the new value. 326 | foundMap = mapValue; 327 | foundI = i; 328 | break; 329 | } 330 | } 331 | } 332 | } 333 | 334 | if (foundMap) { 335 | break; 336 | } 337 | 338 | //Check for a star map match, but just hold on to it, 339 | //if there is a shorter segment match later in a matching 340 | //config, then favor over this star map. 341 | if (!foundStarMap && starMap && getOwn(starMap, nameSegment)) { 342 | foundStarMap = getOwn(starMap, nameSegment); 343 | starI = i; 344 | } 345 | } 346 | 347 | if (!foundMap && foundStarMap) { 348 | foundMap = foundStarMap; 349 | foundI = starI; 350 | } 351 | 352 | if (foundMap) { 353 | nameParts.splice(0, foundI, foundMap); 354 | name = nameParts.join('/'); 355 | } 356 | } 357 | 358 | return name; 359 | } 360 | 361 | function removeScript(name) { 362 | if (isBrowser) { 363 | each(scripts(), function (scriptNode) { 364 | if (scriptNode.getAttribute('data-requiremodule') === name && 365 | scriptNode.getAttribute('data-requirecontext') === context.contextName) { 366 | scriptNode.parentNode.removeChild(scriptNode); 367 | return true; 368 | } 369 | }); 370 | } 371 | } 372 | 373 | function hasPathFallback(id) { 374 | var pathConfig = getOwn(config.paths, id); 375 | if (pathConfig && isArray(pathConfig) && pathConfig.length > 1) { 376 | removeScript(id); 377 | //Pop off the first array value, since it failed, and 378 | //retry 379 | pathConfig.shift(); 380 | context.require.undef(id); 381 | context.require([id]); 382 | return true; 383 | } 384 | } 385 | 386 | //Turns a plugin!resource to [plugin, resource] 387 | //with the plugin being undefined if the name 388 | //did not have a plugin prefix. 389 | function splitPrefix(name) { 390 | var prefix, 391 | index = name ? name.indexOf('!') : -1; 392 | if (index > -1) { 393 | prefix = name.substring(0, index); 394 | name = name.substring(index + 1, name.length); 395 | } 396 | return [prefix, name]; 397 | } 398 | 399 | /** 400 | * Creates a module mapping that includes plugin prefix, module 401 | * name, and path. If parentModuleMap is provided it will 402 | * also normalize the name via require.normalize() 403 | * 404 | * @param {String} name the module name 405 | * @param {String} [parentModuleMap] parent module map 406 | * for the module name, used to resolve relative names. 407 | * @param {Boolean} isNormalized: is the ID already normalized. 408 | * This is true if this call is done for a define() module ID. 409 | * @param {Boolean} applyMap: apply the map config to the ID. 410 | * Should only be true if this map is for a dependency. 411 | * 412 | * @returns {Object} 413 | */ 414 | function makeModuleMap(name, parentModuleMap, isNormalized, applyMap) { 415 | var url, pluginModule, suffix, nameParts, 416 | prefix = null, 417 | parentName = parentModuleMap ? parentModuleMap.name : null, 418 | originalName = name, 419 | isDefine = true, 420 | normalizedName = ''; 421 | 422 | //If no name, then it means it is a require call, generate an 423 | //internal name. 424 | if (!name) { 425 | isDefine = false; 426 | name = '_@r' + (requireCounter += 1); 427 | } 428 | 429 | nameParts = splitPrefix(name); 430 | prefix = nameParts[0]; 431 | name = nameParts[1]; 432 | 433 | if (prefix) { 434 | prefix = normalize(prefix, parentName, applyMap); 435 | pluginModule = getOwn(defined, prefix); 436 | } 437 | 438 | //Account for relative paths if there is a base name. 439 | if (name) { 440 | if (prefix) { 441 | if (pluginModule && pluginModule.normalize) { 442 | //Plugin is loaded, use its normalize method. 443 | normalizedName = pluginModule.normalize(name, function (name) { 444 | return normalize(name, parentName, applyMap); 445 | }); 446 | } else { 447 | normalizedName = normalize(name, parentName, applyMap); 448 | } 449 | } else { 450 | //A regular module. 451 | normalizedName = normalize(name, parentName, applyMap); 452 | 453 | //Normalized name may be a plugin ID due to map config 454 | //application in normalize. The map config values must 455 | //already be normalized, so do not need to redo that part. 456 | nameParts = splitPrefix(normalizedName); 457 | prefix = nameParts[0]; 458 | normalizedName = nameParts[1]; 459 | isNormalized = true; 460 | 461 | url = context.nameToUrl(normalizedName); 462 | } 463 | } 464 | 465 | //If the id is a plugin id that cannot be determined if it needs 466 | //normalization, stamp it with a unique ID so two matching relative 467 | //ids that may conflict can be separate. 468 | suffix = prefix && !pluginModule && !isNormalized ? 469 | '_unnormalized' + (unnormalizedCounter += 1) : 470 | ''; 471 | 472 | return { 473 | prefix: prefix, 474 | name: normalizedName, 475 | parentMap: parentModuleMap, 476 | unnormalized: !!suffix, 477 | url: url, 478 | originalName: originalName, 479 | isDefine: isDefine, 480 | id: (prefix ? 481 | prefix + '!' + normalizedName : 482 | normalizedName) + suffix 483 | }; 484 | } 485 | 486 | function getModule(depMap) { 487 | var id = depMap.id, 488 | mod = getOwn(registry, id); 489 | 490 | if (!mod) { 491 | mod = registry[id] = new context.Module(depMap); 492 | } 493 | 494 | return mod; 495 | } 496 | 497 | function on(depMap, name, fn) { 498 | var id = depMap.id, 499 | mod = getOwn(registry, id); 500 | 501 | if (hasProp(defined, id) && 502 | (!mod || mod.defineEmitComplete)) { 503 | if (name === 'defined') { 504 | fn(defined[id]); 505 | } 506 | } else { 507 | mod = getModule(depMap); 508 | if (mod.error && name === 'error') { 509 | fn(mod.error); 510 | } else { 511 | mod.on(name, fn); 512 | } 513 | } 514 | } 515 | 516 | function onError(err, errback) { 517 | var ids = err.requireModules, 518 | notified = false; 519 | 520 | if (errback) { 521 | errback(err); 522 | } else { 523 | each(ids, function (id) { 524 | var mod = getOwn(registry, id); 525 | if (mod) { 526 | //Set error on module, so it skips timeout checks. 527 | mod.error = err; 528 | if (mod.events.error) { 529 | notified = true; 530 | mod.emit('error', err); 531 | } 532 | } 533 | }); 534 | 535 | if (!notified) { 536 | req.onError(err); 537 | } 538 | } 539 | } 540 | 541 | /** 542 | * Internal method to transfer globalQueue items to this context's 543 | * defQueue. 544 | */ 545 | function takeGlobalQueue() { 546 | //Push all the globalDefQueue items into the context's defQueue 547 | if (globalDefQueue.length) { 548 | //Array splice in the values since the context code has a 549 | //local var ref to defQueue, so cannot just reassign the one 550 | //on context. 551 | apsp.apply(defQueue, 552 | [defQueue.length - 1, 0].concat(globalDefQueue)); 553 | globalDefQueue = []; 554 | } 555 | } 556 | 557 | handlers = { 558 | 'require': function (mod) { 559 | if (mod.require) { 560 | return mod.require; 561 | } else { 562 | return (mod.require = context.makeRequire(mod.map)); 563 | } 564 | }, 565 | 'exports': function (mod) { 566 | mod.usingExports = true; 567 | if (mod.map.isDefine) { 568 | if (mod.exports) { 569 | return mod.exports; 570 | } else { 571 | return (mod.exports = defined[mod.map.id] = {}); 572 | } 573 | } 574 | }, 575 | 'module': function (mod) { 576 | if (mod.module) { 577 | return mod.module; 578 | } else { 579 | return (mod.module = { 580 | id: mod.map.id, 581 | uri: mod.map.url, 582 | config: function () { 583 | var c, 584 | pkg = getOwn(config.pkgs, mod.map.id); 585 | // For packages, only support config targeted 586 | // at the main module. 587 | c = pkg ? getOwn(config.config, mod.map.id + '/' + pkg.main) : 588 | getOwn(config.config, mod.map.id); 589 | return c || {}; 590 | }, 591 | exports: defined[mod.map.id] 592 | }); 593 | } 594 | } 595 | }; 596 | 597 | function cleanRegistry(id) { 598 | //Clean up machinery used for waiting modules. 599 | delete registry[id]; 600 | delete enabledRegistry[id]; 601 | } 602 | 603 | function breakCycle(mod, traced, processed) { 604 | var id = mod.map.id; 605 | 606 | if (mod.error) { 607 | mod.emit('error', mod.error); 608 | } else { 609 | traced[id] = true; 610 | each(mod.depMaps, function (depMap, i) { 611 | var depId = depMap.id, 612 | dep = getOwn(registry, depId); 613 | 614 | //Only force things that have not completed 615 | //being defined, so still in the registry, 616 | //and only if it has not been matched up 617 | //in the module already. 618 | if (dep && !mod.depMatched[i] && !processed[depId]) { 619 | if (getOwn(traced, depId)) { 620 | mod.defineDep(i, defined[depId]); 621 | mod.check(); //pass false? 622 | } else { 623 | breakCycle(dep, traced, processed); 624 | } 625 | } 626 | }); 627 | processed[id] = true; 628 | } 629 | } 630 | 631 | function checkLoaded() { 632 | var map, modId, err, usingPathFallback, 633 | waitInterval = config.waitSeconds * 1000, 634 | //It is possible to disable the wait interval by using waitSeconds of 0. 635 | expired = waitInterval && (context.startTime + waitInterval) < new Date().getTime(), 636 | noLoads = [], 637 | reqCalls = [], 638 | stillLoading = false, 639 | needCycleCheck = true; 640 | 641 | //Do not bother if this call was a result of a cycle break. 642 | if (inCheckLoaded) { 643 | return; 644 | } 645 | 646 | inCheckLoaded = true; 647 | 648 | //Figure out the state of all the modules. 649 | eachProp(enabledRegistry, function (mod) { 650 | map = mod.map; 651 | modId = map.id; 652 | 653 | //Skip things that are not enabled or in error state. 654 | if (!mod.enabled) { 655 | return; 656 | } 657 | 658 | if (!map.isDefine) { 659 | reqCalls.push(mod); 660 | } 661 | 662 | if (!mod.error) { 663 | //If the module should be executed, and it has not 664 | //been inited and time is up, remember it. 665 | if (!mod.inited && expired) { 666 | if (hasPathFallback(modId)) { 667 | usingPathFallback = true; 668 | stillLoading = true; 669 | } else { 670 | noLoads.push(modId); 671 | removeScript(modId); 672 | } 673 | } else if (!mod.inited && mod.fetched && map.isDefine) { 674 | stillLoading = true; 675 | if (!map.prefix) { 676 | //No reason to keep looking for unfinished 677 | //loading. If the only stillLoading is a 678 | //plugin resource though, keep going, 679 | //because it may be that a plugin resource 680 | //is waiting on a non-plugin cycle. 681 | return (needCycleCheck = false); 682 | } 683 | } 684 | } 685 | }); 686 | 687 | if (expired && noLoads.length) { 688 | //If wait time expired, throw error of unloaded modules. 689 | err = makeError('timeout', 'Load timeout for modules: ' + noLoads, null, noLoads); 690 | err.contextName = context.contextName; 691 | return onError(err); 692 | } 693 | 694 | //Not expired, check for a cycle. 695 | if (needCycleCheck) { 696 | each(reqCalls, function (mod) { 697 | breakCycle(mod, {}, {}); 698 | }); 699 | } 700 | 701 | //If still waiting on loads, and the waiting load is something 702 | //other than a plugin resource, or there are still outstanding 703 | //scripts, then just try back later. 704 | if ((!expired || usingPathFallback) && stillLoading) { 705 | //Something is still waiting to load. Wait for it, but only 706 | //if a timeout is not already in effect. 707 | if ((isBrowser || isWebWorker) && !checkLoadedTimeoutId) { 708 | checkLoadedTimeoutId = setTimeout(function () { 709 | checkLoadedTimeoutId = 0; 710 | checkLoaded(); 711 | }, 50); 712 | } 713 | } 714 | 715 | inCheckLoaded = false; 716 | } 717 | 718 | Module = function (map) { 719 | this.events = getOwn(undefEvents, map.id) || {}; 720 | this.map = map; 721 | this.shim = getOwn(config.shim, map.id); 722 | this.depExports = []; 723 | this.depMaps = []; 724 | this.depMatched = []; 725 | this.pluginMaps = {}; 726 | this.depCount = 0; 727 | 728 | /* this.exports this.factory 729 | this.depMaps = [], 730 | this.enabled, this.fetched 731 | */ 732 | }; 733 | 734 | Module.prototype = { 735 | init: function (depMaps, factory, errback, options) { 736 | options = options || {}; 737 | 738 | //Do not do more inits if already done. Can happen if there 739 | //are multiple define calls for the same module. That is not 740 | //a normal, common case, but it is also not unexpected. 741 | if (this.inited) { 742 | return; 743 | } 744 | 745 | this.factory = factory; 746 | 747 | if (errback) { 748 | //Register for errors on this module. 749 | this.on('error', errback); 750 | } else if (this.events.error) { 751 | //If no errback already, but there are error listeners 752 | //on this module, set up an errback to pass to the deps. 753 | errback = bind(this, function (err) { 754 | this.emit('error', err); 755 | }); 756 | } 757 | 758 | //Do a copy of the dependency array, so that 759 | //source inputs are not modified. For example 760 | //"shim" deps are passed in here directly, and 761 | //doing a direct modification of the depMaps array 762 | //would affect that config. 763 | this.depMaps = depMaps && depMaps.slice(0); 764 | 765 | this.errback = errback; 766 | 767 | //Indicate this module has be initialized 768 | this.inited = true; 769 | 770 | this.ignore = options.ignore; 771 | 772 | //Could have option to init this module in enabled mode, 773 | //or could have been previously marked as enabled. However, 774 | //the dependencies are not known until init is called. So 775 | //if enabled previously, now trigger dependencies as enabled. 776 | if (options.enabled || this.enabled) { 777 | //Enable this module and dependencies. 778 | //Will call this.check() 779 | this.enable(); 780 | } else { 781 | this.check(); 782 | } 783 | }, 784 | 785 | defineDep: function (i, depExports) { 786 | //Because of cycles, defined callback for a given 787 | //export can be called more than once. 788 | if (!this.depMatched[i]) { 789 | this.depMatched[i] = true; 790 | this.depCount -= 1; 791 | this.depExports[i] = depExports; 792 | } 793 | }, 794 | 795 | fetch: function () { 796 | if (this.fetched) { 797 | return; 798 | } 799 | this.fetched = true; 800 | 801 | context.startTime = (new Date()).getTime(); 802 | 803 | var map = this.map; 804 | 805 | //If the manager is for a plugin managed resource, 806 | //ask the plugin to load it now. 807 | if (this.shim) { 808 | context.makeRequire(this.map, { 809 | enableBuildCallback: true 810 | })(this.shim.deps || [], bind(this, function () { 811 | return map.prefix ? this.callPlugin() : this.load(); 812 | })); 813 | } else { 814 | //Regular dependency. 815 | return map.prefix ? this.callPlugin() : this.load(); 816 | } 817 | }, 818 | 819 | load: function () { 820 | var url = this.map.url; 821 | 822 | //Regular dependency. 823 | if (!urlFetched[url]) { 824 | urlFetched[url] = true; 825 | context.load(this.map.id, url); 826 | } 827 | }, 828 | 829 | /** 830 | * Checks if the module is ready to define itself, and if so, 831 | * define it. 832 | */ 833 | check: function () { 834 | if (!this.enabled || this.enabling) { 835 | return; 836 | } 837 | 838 | var err, cjsModule, 839 | id = this.map.id, 840 | depExports = this.depExports, 841 | exports = this.exports, 842 | factory = this.factory; 843 | 844 | if (!this.inited) { 845 | this.fetch(); 846 | } else if (this.error) { 847 | this.emit('error', this.error); 848 | } else if (!this.defining) { 849 | //The factory could trigger another require call 850 | //that would result in checking this module to 851 | //define itself again. If already in the process 852 | //of doing that, skip this work. 853 | this.defining = true; 854 | 855 | if (this.depCount < 1 && !this.defined) { 856 | if (isFunction(factory)) { 857 | //If there is an error listener, favor passing 858 | //to that instead of throwing an error. However, 859 | //only do it for define()'d modules. require 860 | //errbacks should not be called for failures in 861 | //their callbacks (#699). However if a global 862 | //onError is set, use that. 863 | if ((this.events.error && this.map.isDefine) || 864 | req.onError !== defaultOnError) { 865 | try { 866 | exports = context.execCb(id, factory, depExports, exports); 867 | } catch (e) { 868 | err = e; 869 | } 870 | } else { 871 | exports = context.execCb(id, factory, depExports, exports); 872 | } 873 | 874 | if (this.map.isDefine) { 875 | //If setting exports via 'module' is in play, 876 | //favor that over return value and exports. After that, 877 | //favor a non-undefined return value over exports use. 878 | cjsModule = this.module; 879 | if (cjsModule && 880 | cjsModule.exports !== undefined && 881 | //Make sure it is not already the exports value 882 | cjsModule.exports !== this.exports) { 883 | exports = cjsModule.exports; 884 | } else if (exports === undefined && this.usingExports) { 885 | //exports already set the defined value. 886 | exports = this.exports; 887 | } 888 | } 889 | 890 | if (err) { 891 | err.requireMap = this.map; 892 | err.requireModules = this.map.isDefine ? [this.map.id] : null; 893 | err.requireType = this.map.isDefine ? 'define' : 'require'; 894 | return onError((this.error = err)); 895 | } 896 | 897 | } else { 898 | //Just a literal value 899 | exports = factory; 900 | } 901 | 902 | this.exports = exports; 903 | 904 | if (this.map.isDefine && !this.ignore) { 905 | defined[id] = exports; 906 | 907 | if (req.onResourceLoad) { 908 | req.onResourceLoad(context, this.map, this.depMaps); 909 | } 910 | } 911 | 912 | //Clean up 913 | cleanRegistry(id); 914 | 915 | this.defined = true; 916 | } 917 | 918 | //Finished the define stage. Allow calling check again 919 | //to allow define notifications below in the case of a 920 | //cycle. 921 | this.defining = false; 922 | 923 | if (this.defined && !this.defineEmitted) { 924 | this.defineEmitted = true; 925 | this.emit('defined', this.exports); 926 | this.defineEmitComplete = true; 927 | } 928 | 929 | } 930 | }, 931 | 932 | callPlugin: function () { 933 | var map = this.map, 934 | id = map.id, 935 | //Map already normalized the prefix. 936 | pluginMap = makeModuleMap(map.prefix); 937 | 938 | //Mark this as a dependency for this plugin, so it 939 | //can be traced for cycles. 940 | this.depMaps.push(pluginMap); 941 | 942 | on(pluginMap, 'defined', bind(this, function (plugin) { 943 | var load, normalizedMap, normalizedMod, 944 | name = this.map.name, 945 | parentName = this.map.parentMap ? this.map.parentMap.name : null, 946 | localRequire = context.makeRequire(map.parentMap, { 947 | enableBuildCallback: true 948 | }); 949 | 950 | //If current map is not normalized, wait for that 951 | //normalized name to load instead of continuing. 952 | if (this.map.unnormalized) { 953 | //Normalize the ID if the plugin allows it. 954 | if (plugin.normalize) { 955 | name = plugin.normalize(name, function (name) { 956 | return normalize(name, parentName, true); 957 | }) || ''; 958 | } 959 | 960 | //prefix and name should already be normalized, no need 961 | //for applying map config again either. 962 | normalizedMap = makeModuleMap(map.prefix + '!' + name, 963 | this.map.parentMap); 964 | on(normalizedMap, 965 | 'defined', bind(this, function (value) { 966 | this.init([], function () { return value; }, null, { 967 | enabled: true, 968 | ignore: true 969 | }); 970 | })); 971 | 972 | normalizedMod = getOwn(registry, normalizedMap.id); 973 | if (normalizedMod) { 974 | //Mark this as a dependency for this plugin, so it 975 | //can be traced for cycles. 976 | this.depMaps.push(normalizedMap); 977 | 978 | if (this.events.error) { 979 | normalizedMod.on('error', bind(this, function (err) { 980 | this.emit('error', err); 981 | })); 982 | } 983 | normalizedMod.enable(); 984 | } 985 | 986 | return; 987 | } 988 | 989 | load = bind(this, function (value) { 990 | this.init([], function () { return value; }, null, { 991 | enabled: true 992 | }); 993 | }); 994 | 995 | load.error = bind(this, function (err) { 996 | this.inited = true; 997 | this.error = err; 998 | err.requireModules = [id]; 999 | 1000 | //Remove temp unnormalized modules for this module, 1001 | //since they will never be resolved otherwise now. 1002 | eachProp(registry, function (mod) { 1003 | if (mod.map.id.indexOf(id + '_unnormalized') === 0) { 1004 | cleanRegistry(mod.map.id); 1005 | } 1006 | }); 1007 | 1008 | onError(err); 1009 | }); 1010 | 1011 | //Allow plugins to load other code without having to know the 1012 | //context or how to 'complete' the load. 1013 | load.fromText = bind(this, function (text, textAlt) { 1014 | /*jslint evil: true */ 1015 | var moduleName = map.name, 1016 | moduleMap = makeModuleMap(moduleName), 1017 | hasInteractive = useInteractive; 1018 | 1019 | //As of 2.1.0, support just passing the text, to reinforce 1020 | //fromText only being called once per resource. Still 1021 | //support old style of passing moduleName but discard 1022 | //that moduleName in favor of the internal ref. 1023 | if (textAlt) { 1024 | text = textAlt; 1025 | } 1026 | 1027 | //Turn off interactive script matching for IE for any define 1028 | //calls in the text, then turn it back on at the end. 1029 | if (hasInteractive) { 1030 | useInteractive = false; 1031 | } 1032 | 1033 | //Prime the system by creating a module instance for 1034 | //it. 1035 | getModule(moduleMap); 1036 | 1037 | //Transfer any config to this other module. 1038 | if (hasProp(config.config, id)) { 1039 | config.config[moduleName] = config.config[id]; 1040 | } 1041 | 1042 | try { 1043 | req.exec(text); 1044 | } catch (e) { 1045 | return onError(makeError('fromtexteval', 1046 | 'fromText eval for ' + id + 1047 | ' failed: ' + e, 1048 | e, 1049 | [id])); 1050 | } 1051 | 1052 | if (hasInteractive) { 1053 | useInteractive = true; 1054 | } 1055 | 1056 | //Mark this as a dependency for the plugin 1057 | //resource 1058 | this.depMaps.push(moduleMap); 1059 | 1060 | //Support anonymous modules. 1061 | context.completeLoad(moduleName); 1062 | 1063 | //Bind the value of that module to the value for this 1064 | //resource ID. 1065 | localRequire([moduleName], load); 1066 | }); 1067 | 1068 | //Use parentName here since the plugin's name is not reliable, 1069 | //could be some weird string with no path that actually wants to 1070 | //reference the parentName's path. 1071 | plugin.load(map.name, localRequire, load, config); 1072 | })); 1073 | 1074 | context.enable(pluginMap, this); 1075 | this.pluginMaps[pluginMap.id] = pluginMap; 1076 | }, 1077 | 1078 | enable: function () { 1079 | enabledRegistry[this.map.id] = this; 1080 | this.enabled = true; 1081 | 1082 | //Set flag mentioning that the module is enabling, 1083 | //so that immediate calls to the defined callbacks 1084 | //for dependencies do not trigger inadvertent load 1085 | //with the depCount still being zero. 1086 | this.enabling = true; 1087 | 1088 | //Enable each dependency 1089 | each(this.depMaps, bind(this, function (depMap, i) { 1090 | var id, mod, handler; 1091 | 1092 | if (typeof depMap === 'string') { 1093 | //Dependency needs to be converted to a depMap 1094 | //and wired up to this module. 1095 | depMap = makeModuleMap(depMap, 1096 | (this.map.isDefine ? this.map : this.map.parentMap), 1097 | false, 1098 | !this.skipMap); 1099 | this.depMaps[i] = depMap; 1100 | 1101 | handler = getOwn(handlers, depMap.id); 1102 | 1103 | if (handler) { 1104 | this.depExports[i] = handler(this); 1105 | return; 1106 | } 1107 | 1108 | this.depCount += 1; 1109 | 1110 | on(depMap, 'defined', bind(this, function (depExports) { 1111 | this.defineDep(i, depExports); 1112 | this.check(); 1113 | })); 1114 | 1115 | if (this.errback) { 1116 | on(depMap, 'error', bind(this, this.errback)); 1117 | } 1118 | } 1119 | 1120 | id = depMap.id; 1121 | mod = registry[id]; 1122 | 1123 | //Skip special modules like 'require', 'exports', 'module' 1124 | //Also, don't call enable if it is already enabled, 1125 | //important in circular dependency cases. 1126 | if (!hasProp(handlers, id) && mod && !mod.enabled) { 1127 | context.enable(depMap, this); 1128 | } 1129 | })); 1130 | 1131 | //Enable each plugin that is used in 1132 | //a dependency 1133 | eachProp(this.pluginMaps, bind(this, function (pluginMap) { 1134 | var mod = getOwn(registry, pluginMap.id); 1135 | if (mod && !mod.enabled) { 1136 | context.enable(pluginMap, this); 1137 | } 1138 | })); 1139 | 1140 | this.enabling = false; 1141 | 1142 | this.check(); 1143 | }, 1144 | 1145 | on: function (name, cb) { 1146 | var cbs = this.events[name]; 1147 | if (!cbs) { 1148 | cbs = this.events[name] = []; 1149 | } 1150 | cbs.push(cb); 1151 | }, 1152 | 1153 | emit: function (name, evt) { 1154 | each(this.events[name], function (cb) { 1155 | cb(evt); 1156 | }); 1157 | if (name === 'error') { 1158 | //Now that the error handler was triggered, remove 1159 | //the listeners, since this broken Module instance 1160 | //can stay around for a while in the registry. 1161 | delete this.events[name]; 1162 | } 1163 | } 1164 | }; 1165 | 1166 | function callGetModule(args) { 1167 | //Skip modules already defined. 1168 | if (!hasProp(defined, args[0])) { 1169 | getModule(makeModuleMap(args[0], null, true)).init(args[1], args[2]); 1170 | } 1171 | } 1172 | 1173 | function removeListener(node, func, name, ieName) { 1174 | //Favor detachEvent because of IE9 1175 | //issue, see attachEvent/addEventListener comment elsewhere 1176 | //in this file. 1177 | if (node.detachEvent && !isOpera) { 1178 | //Probably IE. If not it will throw an error, which will be 1179 | //useful to know. 1180 | if (ieName) { 1181 | node.detachEvent(ieName, func); 1182 | } 1183 | } else { 1184 | node.removeEventListener(name, func, false); 1185 | } 1186 | } 1187 | 1188 | /** 1189 | * Given an event from a script node, get the requirejs info from it, 1190 | * and then removes the event listeners on the node. 1191 | * @param {Event} evt 1192 | * @returns {Object} 1193 | */ 1194 | function getScriptData(evt) { 1195 | //Using currentTarget instead of target for Firefox 2.0's sake. Not 1196 | //all old browsers will be supported, but this one was easy enough 1197 | //to support and still makes sense. 1198 | var node = evt.currentTarget || evt.srcElement; 1199 | 1200 | //Remove the listeners once here. 1201 | removeListener(node, context.onScriptLoad, 'load', 'onreadystatechange'); 1202 | removeListener(node, context.onScriptError, 'error'); 1203 | 1204 | return { 1205 | node: node, 1206 | id: node && node.getAttribute('data-requiremodule') 1207 | }; 1208 | } 1209 | 1210 | function intakeDefines() { 1211 | var args; 1212 | 1213 | //Any defined modules in the global queue, intake them now. 1214 | takeGlobalQueue(); 1215 | 1216 | //Make sure any remaining defQueue items get properly processed. 1217 | while (defQueue.length) { 1218 | args = defQueue.shift(); 1219 | if (args[0] === null) { 1220 | return onError(makeError('mismatch', 'Mismatched anonymous define() module: ' + args[args.length - 1])); 1221 | } else { 1222 | //args are id, deps, factory. Should be normalized by the 1223 | //define() function. 1224 | callGetModule(args); 1225 | } 1226 | } 1227 | } 1228 | 1229 | context = { 1230 | config: config, 1231 | contextName: contextName, 1232 | registry: registry, 1233 | defined: defined, 1234 | urlFetched: urlFetched, 1235 | defQueue: defQueue, 1236 | Module: Module, 1237 | makeModuleMap: makeModuleMap, 1238 | nextTick: req.nextTick, 1239 | onError: onError, 1240 | 1241 | /** 1242 | * Set a configuration for the context. 1243 | * @param {Object} cfg config object to integrate. 1244 | */ 1245 | configure: function (cfg) { 1246 | //Make sure the baseUrl ends in a slash. 1247 | if (cfg.baseUrl) { 1248 | if (cfg.baseUrl.charAt(cfg.baseUrl.length - 1) !== '/') { 1249 | cfg.baseUrl += '/'; 1250 | } 1251 | } 1252 | 1253 | //Save off the paths and packages since they require special processing, 1254 | //they are additive. 1255 | var pkgs = config.pkgs, 1256 | shim = config.shim, 1257 | objs = { 1258 | paths: true, 1259 | config: true, 1260 | map: true 1261 | }; 1262 | 1263 | eachProp(cfg, function (value, prop) { 1264 | if (objs[prop]) { 1265 | if (prop === 'map') { 1266 | if (!config.map) { 1267 | config.map = {}; 1268 | } 1269 | mixin(config[prop], value, true, true); 1270 | } else { 1271 | mixin(config[prop], value, true); 1272 | } 1273 | } else { 1274 | config[prop] = value; 1275 | } 1276 | }); 1277 | 1278 | //Merge shim 1279 | if (cfg.shim) { 1280 | eachProp(cfg.shim, function (value, id) { 1281 | //Normalize the structure 1282 | if (isArray(value)) { 1283 | value = { 1284 | deps: value 1285 | }; 1286 | } 1287 | if ((value.exports || value.init) && !value.exportsFn) { 1288 | value.exportsFn = context.makeShimExports(value); 1289 | } 1290 | shim[id] = value; 1291 | }); 1292 | config.shim = shim; 1293 | } 1294 | 1295 | //Adjust packages if necessary. 1296 | if (cfg.packages) { 1297 | each(cfg.packages, function (pkgObj) { 1298 | var location; 1299 | 1300 | pkgObj = typeof pkgObj === 'string' ? { name: pkgObj } : pkgObj; 1301 | location = pkgObj.location; 1302 | 1303 | //Create a brand new object on pkgs, since currentPackages can 1304 | //be passed in again, and config.pkgs is the internal transformed 1305 | //state for all package configs. 1306 | pkgs[pkgObj.name] = { 1307 | name: pkgObj.name, 1308 | location: location || pkgObj.name, 1309 | //Remove leading dot in main, so main paths are normalized, 1310 | //and remove any trailing .js, since different package 1311 | //envs have different conventions: some use a module name, 1312 | //some use a file name. 1313 | main: (pkgObj.main || 'main') 1314 | .replace(currDirRegExp, '') 1315 | .replace(jsSuffixRegExp, '') 1316 | }; 1317 | }); 1318 | 1319 | //Done with modifications, assing packages back to context config 1320 | config.pkgs = pkgs; 1321 | } 1322 | 1323 | //If there are any "waiting to execute" modules in the registry, 1324 | //update the maps for them, since their info, like URLs to load, 1325 | //may have changed. 1326 | eachProp(registry, function (mod, id) { 1327 | //If module already has init called, since it is too 1328 | //late to modify them, and ignore unnormalized ones 1329 | //since they are transient. 1330 | if (!mod.inited && !mod.map.unnormalized) { 1331 | mod.map = makeModuleMap(id); 1332 | } 1333 | }); 1334 | 1335 | //If a deps array or a config callback is specified, then call 1336 | //require with those args. This is useful when require is defined as a 1337 | //config object before require.js is loaded. 1338 | if (cfg.deps || cfg.callback) { 1339 | context.require(cfg.deps || [], cfg.callback); 1340 | } 1341 | }, 1342 | 1343 | makeShimExports: function (value) { 1344 | function fn() { 1345 | var ret; 1346 | if (value.init) { 1347 | ret = value.init.apply(global, arguments); 1348 | } 1349 | return ret || (value.exports && getGlobal(value.exports)); 1350 | } 1351 | return fn; 1352 | }, 1353 | 1354 | makeRequire: function (relMap, options) { 1355 | options = options || {}; 1356 | 1357 | function localRequire(deps, callback, errback) { 1358 | var id, map, requireMod; 1359 | 1360 | if (options.enableBuildCallback && callback && isFunction(callback)) { 1361 | callback.__requireJsBuild = true; 1362 | } 1363 | 1364 | if (typeof deps === 'string') { 1365 | if (isFunction(callback)) { 1366 | //Invalid call 1367 | return onError(makeError('requireargs', 'Invalid require call'), errback); 1368 | } 1369 | 1370 | //If require|exports|module are requested, get the 1371 | //value for them from the special handlers. Caveat: 1372 | //this only works while module is being defined. 1373 | if (relMap && hasProp(handlers, deps)) { 1374 | return handlers[deps](registry[relMap.id]); 1375 | } 1376 | 1377 | //Synchronous access to one module. If require.get is 1378 | //available (as in the Node adapter), prefer that. 1379 | if (req.get) { 1380 | return req.get(context, deps, relMap, localRequire); 1381 | } 1382 | 1383 | //Normalize module name, if it contains . or .. 1384 | map = makeModuleMap(deps, relMap, false, true); 1385 | id = map.id; 1386 | 1387 | if (!hasProp(defined, id)) { 1388 | return onError(makeError('notloaded', 'Module name "' + 1389 | id + 1390 | '" has not been loaded yet for context: ' + 1391 | contextName + 1392 | (relMap ? '' : '. Use require([])'))); 1393 | } 1394 | return defined[id]; 1395 | } 1396 | 1397 | //Grab defines waiting in the global queue. 1398 | intakeDefines(); 1399 | 1400 | //Mark all the dependencies as needing to be loaded. 1401 | context.nextTick(function () { 1402 | //Some defines could have been added since the 1403 | //require call, collect them. 1404 | intakeDefines(); 1405 | 1406 | requireMod = getModule(makeModuleMap(null, relMap)); 1407 | 1408 | //Store if map config should be applied to this require 1409 | //call for dependencies. 1410 | requireMod.skipMap = options.skipMap; 1411 | 1412 | requireMod.init(deps, callback, errback, { 1413 | enabled: true 1414 | }); 1415 | 1416 | checkLoaded(); 1417 | }); 1418 | 1419 | return localRequire; 1420 | } 1421 | 1422 | mixin(localRequire, { 1423 | isBrowser: isBrowser, 1424 | 1425 | /** 1426 | * Converts a module name + .extension into an URL path. 1427 | * *Requires* the use of a module name. It does not support using 1428 | * plain URLs like nameToUrl. 1429 | */ 1430 | toUrl: function (moduleNamePlusExt) { 1431 | var ext, 1432 | index = moduleNamePlusExt.lastIndexOf('.'), 1433 | segment = moduleNamePlusExt.split('/')[0], 1434 | isRelative = segment === '.' || segment === '..'; 1435 | 1436 | //Have a file extension alias, and it is not the 1437 | //dots from a relative path. 1438 | if (index !== -1 && (!isRelative || index > 1)) { 1439 | ext = moduleNamePlusExt.substring(index, moduleNamePlusExt.length); 1440 | moduleNamePlusExt = moduleNamePlusExt.substring(0, index); 1441 | } 1442 | 1443 | return context.nameToUrl(normalize(moduleNamePlusExt, 1444 | relMap && relMap.id, true), ext, true); 1445 | }, 1446 | 1447 | defined: function (id) { 1448 | return hasProp(defined, makeModuleMap(id, relMap, false, true).id); 1449 | }, 1450 | 1451 | specified: function (id) { 1452 | id = makeModuleMap(id, relMap, false, true).id; 1453 | return hasProp(defined, id) || hasProp(registry, id); 1454 | } 1455 | }); 1456 | 1457 | //Only allow undef on top level require calls 1458 | if (!relMap) { 1459 | localRequire.undef = function (id) { 1460 | //Bind any waiting define() calls to this context, 1461 | //fix for #408 1462 | takeGlobalQueue(); 1463 | 1464 | var map = makeModuleMap(id, relMap, true), 1465 | mod = getOwn(registry, id); 1466 | 1467 | delete defined[id]; 1468 | delete urlFetched[map.url]; 1469 | delete undefEvents[id]; 1470 | 1471 | if (mod) { 1472 | //Hold on to listeners in case the 1473 | //module will be attempted to be reloaded 1474 | //using a different config. 1475 | if (mod.events.defined) { 1476 | undefEvents[id] = mod.events; 1477 | } 1478 | 1479 | cleanRegistry(id); 1480 | } 1481 | }; 1482 | } 1483 | 1484 | return localRequire; 1485 | }, 1486 | 1487 | /** 1488 | * Called to enable a module if it is still in the registry 1489 | * awaiting enablement. A second arg, parent, the parent module, 1490 | * is passed in for context, when this method is overriden by 1491 | * the optimizer. Not shown here to keep code compact. 1492 | */ 1493 | enable: function (depMap) { 1494 | var mod = getOwn(registry, depMap.id); 1495 | if (mod) { 1496 | getModule(depMap).enable(); 1497 | } 1498 | }, 1499 | 1500 | /** 1501 | * Internal method used by environment adapters to complete a load event. 1502 | * A load event could be a script load or just a load pass from a synchronous 1503 | * load call. 1504 | * @param {String} moduleName the name of the module to potentially complete. 1505 | */ 1506 | completeLoad: function (moduleName) { 1507 | var found, args, mod, 1508 | shim = getOwn(config.shim, moduleName) || {}, 1509 | shExports = shim.exports; 1510 | 1511 | takeGlobalQueue(); 1512 | 1513 | while (defQueue.length) { 1514 | args = defQueue.shift(); 1515 | if (args[0] === null) { 1516 | args[0] = moduleName; 1517 | //If already found an anonymous module and bound it 1518 | //to this name, then this is some other anon module 1519 | //waiting for its completeLoad to fire. 1520 | if (found) { 1521 | break; 1522 | } 1523 | found = true; 1524 | } else if (args[0] === moduleName) { 1525 | //Found matching define call for this script! 1526 | found = true; 1527 | } 1528 | 1529 | callGetModule(args); 1530 | } 1531 | 1532 | //Do this after the cycle of callGetModule in case the result 1533 | //of those calls/init calls changes the registry. 1534 | mod = getOwn(registry, moduleName); 1535 | 1536 | if (!found && !hasProp(defined, moduleName) && mod && !mod.inited) { 1537 | if (config.enforceDefine && (!shExports || !getGlobal(shExports))) { 1538 | if (hasPathFallback(moduleName)) { 1539 | return; 1540 | } else { 1541 | return onError(makeError('nodefine', 1542 | 'No define call for ' + moduleName, 1543 | null, 1544 | [moduleName])); 1545 | } 1546 | } else { 1547 | //A script that does not call define(), so just simulate 1548 | //the call for it. 1549 | callGetModule([moduleName, (shim.deps || []), shim.exportsFn]); 1550 | } 1551 | } 1552 | 1553 | checkLoaded(); 1554 | }, 1555 | 1556 | /** 1557 | * Converts a module name to a file path. Supports cases where 1558 | * moduleName may actually be just an URL. 1559 | * Note that it **does not** call normalize on the moduleName, 1560 | * it is assumed to have already been normalized. This is an 1561 | * internal API, not a public one. Use toUrl for the public API. 1562 | */ 1563 | nameToUrl: function (moduleName, ext, skipExt) { 1564 | var paths, pkgs, pkg, pkgPath, syms, i, parentModule, url, 1565 | parentPath; 1566 | 1567 | //If a colon is in the URL, it indicates a protocol is used and it is just 1568 | //an URL to a file, or if it starts with a slash, contains a query arg (i.e. ?) 1569 | //or ends with .js, then assume the user meant to use an url and not a module id. 1570 | //The slash is important for protocol-less URLs as well as full paths. 1571 | if (req.jsExtRegExp.test(moduleName)) { 1572 | //Just a plain path, not module name lookup, so just return it. 1573 | //Add extension if it is included. This is a bit wonky, only non-.js things pass 1574 | //an extension, this method probably needs to be reworked. 1575 | url = moduleName + (ext || ''); 1576 | } else { 1577 | //A module that needs to be converted to a path. 1578 | paths = config.paths; 1579 | pkgs = config.pkgs; 1580 | 1581 | syms = moduleName.split('/'); 1582 | //For each module name segment, see if there is a path 1583 | //registered for it. Start with most specific name 1584 | //and work up from it. 1585 | for (i = syms.length; i > 0; i -= 1) { 1586 | parentModule = syms.slice(0, i).join('/'); 1587 | pkg = getOwn(pkgs, parentModule); 1588 | parentPath = getOwn(paths, parentModule); 1589 | if (parentPath) { 1590 | //If an array, it means there are a few choices, 1591 | //Choose the one that is desired 1592 | if (isArray(parentPath)) { 1593 | parentPath = parentPath[0]; 1594 | } 1595 | syms.splice(0, i, parentPath); 1596 | break; 1597 | } else if (pkg) { 1598 | //If module name is just the package name, then looking 1599 | //for the main module. 1600 | if (moduleName === pkg.name) { 1601 | pkgPath = pkg.location + '/' + pkg.main; 1602 | } else { 1603 | pkgPath = pkg.location; 1604 | } 1605 | syms.splice(0, i, pkgPath); 1606 | break; 1607 | } 1608 | } 1609 | 1610 | //Join the path parts together, then figure out if baseUrl is needed. 1611 | url = syms.join('/'); 1612 | url += (ext || (/\?/.test(url) || skipExt ? '' : '.js')); 1613 | url = (url.charAt(0) === '/' || url.match(/^[\w\+\.\-]+:/) ? '' : config.baseUrl) + url; 1614 | } 1615 | 1616 | return config.urlArgs ? url + 1617 | ((url.indexOf('?') === -1 ? '?' : '&') + 1618 | config.urlArgs) : url; 1619 | }, 1620 | 1621 | //Delegates to req.load. Broken out as a separate function to 1622 | //allow overriding in the optimizer. 1623 | load: function (id, url) { 1624 | req.load(context, id, url); 1625 | }, 1626 | 1627 | /** 1628 | * Executes a module callback function. Broken out as a separate function 1629 | * solely to allow the build system to sequence the files in the built 1630 | * layer in the right sequence. 1631 | * 1632 | * @private 1633 | */ 1634 | execCb: function (name, callback, args, exports) { 1635 | return callback.apply(exports, args); 1636 | }, 1637 | 1638 | /** 1639 | * callback for script loads, used to check status of loading. 1640 | * 1641 | * @param {Event} evt the event from the browser for the script 1642 | * that was loaded. 1643 | */ 1644 | onScriptLoad: function (evt) { 1645 | //Using currentTarget instead of target for Firefox 2.0's sake. Not 1646 | //all old browsers will be supported, but this one was easy enough 1647 | //to support and still makes sense. 1648 | if (evt.type === 'load' || 1649 | (readyRegExp.test((evt.currentTarget || evt.srcElement).readyState))) { 1650 | //Reset interactive script so a script node is not held onto for 1651 | //to long. 1652 | interactiveScript = null; 1653 | 1654 | //Pull out the name of the module and the context. 1655 | var data = getScriptData(evt); 1656 | context.completeLoad(data.id); 1657 | } 1658 | }, 1659 | 1660 | /** 1661 | * Callback for script errors. 1662 | */ 1663 | onScriptError: function (evt) { 1664 | var data = getScriptData(evt); 1665 | if (!hasPathFallback(data.id)) { 1666 | return onError(makeError('scripterror', 'Script error for: ' + data.id, evt, [data.id])); 1667 | } 1668 | } 1669 | }; 1670 | 1671 | context.require = context.makeRequire(); 1672 | return context; 1673 | } 1674 | 1675 | /** 1676 | * Main entry point. 1677 | * 1678 | * If the only argument to require is a string, then the module that 1679 | * is represented by that string is fetched for the appropriate context. 1680 | * 1681 | * If the first argument is an array, then it will be treated as an array 1682 | * of dependency string names to fetch. An optional function callback can 1683 | * be specified to execute when all of those dependencies are available. 1684 | * 1685 | * Make a local req variable to help Caja compliance (it assumes things 1686 | * on a require that are not standardized), and to give a short 1687 | * name for minification/local scope use. 1688 | */ 1689 | req = requirejs = function (deps, callback, errback, optional) { 1690 | 1691 | //Find the right context, use default 1692 | var context, config, 1693 | contextName = defContextName; 1694 | 1695 | // Determine if have config object in the call. 1696 | if (!isArray(deps) && typeof deps !== 'string') { 1697 | // deps is a config object 1698 | config = deps; 1699 | if (isArray(callback)) { 1700 | // Adjust args if there are dependencies 1701 | deps = callback; 1702 | callback = errback; 1703 | errback = optional; 1704 | } else { 1705 | deps = []; 1706 | } 1707 | } 1708 | 1709 | if (config && config.context) { 1710 | contextName = config.context; 1711 | } 1712 | 1713 | context = getOwn(contexts, contextName); 1714 | if (!context) { 1715 | context = contexts[contextName] = req.s.newContext(contextName); 1716 | } 1717 | 1718 | if (config) { 1719 | context.configure(config); 1720 | } 1721 | 1722 | return context.require(deps, callback, errback); 1723 | }; 1724 | 1725 | /** 1726 | * Support require.config() to make it easier to cooperate with other 1727 | * AMD loaders on globally agreed names. 1728 | */ 1729 | req.config = function (config) { 1730 | return req(config); 1731 | }; 1732 | 1733 | /** 1734 | * Execute something after the current tick 1735 | * of the event loop. Override for other envs 1736 | * that have a better solution than setTimeout. 1737 | * @param {Function} fn function to execute later. 1738 | */ 1739 | req.nextTick = typeof setTimeout !== 'undefined' ? function (fn) { 1740 | setTimeout(fn, 4); 1741 | } : function (fn) { fn(); }; 1742 | 1743 | /** 1744 | * Export require as a global, but only if it does not already exist. 1745 | */ 1746 | if (!require) { 1747 | require = req; 1748 | } 1749 | 1750 | req.version = version; 1751 | 1752 | //Used to filter out dependencies that are already paths. 1753 | req.jsExtRegExp = /^\/|:|\?|\.js$/; 1754 | req.isBrowser = isBrowser; 1755 | s = req.s = { 1756 | contexts: contexts, 1757 | newContext: newContext 1758 | }; 1759 | 1760 | //Create default context. 1761 | req({}); 1762 | 1763 | //Exports some context-sensitive methods on global require. 1764 | each([ 1765 | 'toUrl', 1766 | 'undef', 1767 | 'defined', 1768 | 'specified' 1769 | ], function (prop) { 1770 | //Reference from contexts instead of early binding to default context, 1771 | //so that during builds, the latest instance of the default context 1772 | //with its config gets used. 1773 | req[prop] = function () { 1774 | var ctx = contexts[defContextName]; 1775 | return ctx.require[prop].apply(ctx, arguments); 1776 | }; 1777 | }); 1778 | 1779 | if (isBrowser) { 1780 | head = s.head = document.getElementsByTagName('head')[0]; 1781 | //If BASE tag is in play, using appendChild is a problem for IE6. 1782 | //When that browser dies, this can be removed. Details in this jQuery bug: 1783 | //http://dev.jquery.com/ticket/2709 1784 | baseElement = document.getElementsByTagName('base')[0]; 1785 | if (baseElement) { 1786 | head = s.head = baseElement.parentNode; 1787 | } 1788 | } 1789 | 1790 | /** 1791 | * Any errors that require explicitly generates will be passed to this 1792 | * function. Intercept/override it if you want custom error handling. 1793 | * @param {Error} err the error object. 1794 | */ 1795 | req.onError = defaultOnError; 1796 | 1797 | /** 1798 | * Creates the node for the load command. Only used in browser envs. 1799 | */ 1800 | req.createNode = function (config, moduleName, url) { 1801 | var node = config.xhtml ? 1802 | document.createElementNS('http://www.w3.org/1999/xhtml', 'html:script') : 1803 | document.createElement('script'); 1804 | node.type = config.scriptType || 'text/javascript'; 1805 | node.charset = 'utf-8'; 1806 | node.async = true; 1807 | return node; 1808 | }; 1809 | 1810 | /** 1811 | * Does the request to load a module for the browser case. 1812 | * Make this a separate function to allow other environments 1813 | * to override it. 1814 | * 1815 | * @param {Object} context the require context to find state. 1816 | * @param {String} moduleName the name of the module. 1817 | * @param {Object} url the URL to the module. 1818 | */ 1819 | req.load = function (context, moduleName, url) { 1820 | var config = (context && context.config) || {}, 1821 | node; 1822 | if (isBrowser) { 1823 | //In the browser so use a script tag 1824 | node = req.createNode(config, moduleName, url); 1825 | 1826 | node.setAttribute('data-requirecontext', context.contextName); 1827 | node.setAttribute('data-requiremodule', moduleName); 1828 | 1829 | //Set up load listener. Test attachEvent first because IE9 has 1830 | //a subtle issue in its addEventListener and script onload firings 1831 | //that do not match the behavior of all other browsers with 1832 | //addEventListener support, which fire the onload event for a 1833 | //script right after the script execution. See: 1834 | //https://connect.microsoft.com/IE/feedback/details/648057/script-onload-event-is-not-fired-immediately-after-script-execution 1835 | //UNFORTUNATELY Opera implements attachEvent but does not follow the script 1836 | //script execution mode. 1837 | if (node.attachEvent && 1838 | //Check if node.attachEvent is artificially added by custom script or 1839 | //natively supported by browser 1840 | //read https://github.com/jrburke/requirejs/issues/187 1841 | //if we can NOT find [native code] then it must NOT natively supported. 1842 | //in IE8, node.attachEvent does not have toString() 1843 | //Note the test for "[native code" with no closing brace, see: 1844 | //https://github.com/jrburke/requirejs/issues/273 1845 | !(node.attachEvent.toString && node.attachEvent.toString().indexOf('[native code') < 0) && 1846 | !isOpera) { 1847 | //Probably IE. IE (at least 6-8) do not fire 1848 | //script onload right after executing the script, so 1849 | //we cannot tie the anonymous define call to a name. 1850 | //However, IE reports the script as being in 'interactive' 1851 | //readyState at the time of the define call. 1852 | useInteractive = true; 1853 | 1854 | node.attachEvent('onreadystatechange', context.onScriptLoad); 1855 | //It would be great to add an error handler here to catch 1856 | //404s in IE9+. However, onreadystatechange will fire before 1857 | //the error handler, so that does not help. If addEventListener 1858 | //is used, then IE will fire error before load, but we cannot 1859 | //use that pathway given the connect.microsoft.com issue 1860 | //mentioned above about not doing the 'script execute, 1861 | //then fire the script load event listener before execute 1862 | //next script' that other browsers do. 1863 | //Best hope: IE10 fixes the issues, 1864 | //and then destroys all installs of IE 6-9. 1865 | //node.attachEvent('onerror', context.onScriptError); 1866 | } else { 1867 | node.addEventListener('load', context.onScriptLoad, false); 1868 | node.addEventListener('error', context.onScriptError, false); 1869 | } 1870 | node.src = url; 1871 | 1872 | //For some cache cases in IE 6-8, the script executes before the end 1873 | //of the appendChild execution, so to tie an anonymous define 1874 | //call to the module name (which is stored on the node), hold on 1875 | //to a reference to this node, but clear after the DOM insertion. 1876 | currentlyAddingScript = node; 1877 | if (baseElement) { 1878 | head.insertBefore(node, baseElement); 1879 | } else { 1880 | head.appendChild(node); 1881 | } 1882 | currentlyAddingScript = null; 1883 | 1884 | return node; 1885 | } else if (isWebWorker) { 1886 | try { 1887 | //In a web worker, use importScripts. This is not a very 1888 | //efficient use of importScripts, importScripts will block until 1889 | //its script is downloaded and evaluated. However, if web workers 1890 | //are in play, the expectation that a build has been done so that 1891 | //only one script needs to be loaded anyway. This may need to be 1892 | //reevaluated if other use cases become common. 1893 | importScripts(url); 1894 | 1895 | //Account for anonymous modules 1896 | context.completeLoad(moduleName); 1897 | } catch (e) { 1898 | context.onError(makeError('importscripts', 1899 | 'importScripts failed for ' + 1900 | moduleName + ' at ' + url, 1901 | e, 1902 | [moduleName])); 1903 | } 1904 | } 1905 | }; 1906 | 1907 | function getInteractiveScript() { 1908 | if (interactiveScript && interactiveScript.readyState === 'interactive') { 1909 | return interactiveScript; 1910 | } 1911 | 1912 | eachReverse(scripts(), function (script) { 1913 | if (script.readyState === 'interactive') { 1914 | return (interactiveScript = script); 1915 | } 1916 | }); 1917 | return interactiveScript; 1918 | } 1919 | 1920 | //Look for a data-main script attribute, which could also adjust the baseUrl. 1921 | if (isBrowser) { 1922 | //Figure out baseUrl. Get it from the script tag with require.js in it. 1923 | eachReverse(scripts(), function (script) { 1924 | //Set the 'head' where we can append children by 1925 | //using the script's parent. 1926 | if (!head) { 1927 | head = script.parentNode; 1928 | } 1929 | 1930 | //Look for a data-main attribute to set main script for the page 1931 | //to load. If it is there, the path to data main becomes the 1932 | //baseUrl, if it is not already set. 1933 | dataMain = script.getAttribute('data-main'); 1934 | if (dataMain) { 1935 | //Preserve dataMain in case it is a path (i.e. contains '?') 1936 | mainScript = dataMain; 1937 | 1938 | //Set final baseUrl if there is not already an explicit one. 1939 | if (!cfg.baseUrl) { 1940 | //Pull off the directory of data-main for use as the 1941 | //baseUrl. 1942 | src = mainScript.split('/'); 1943 | mainScript = src.pop(); 1944 | subPath = src.length ? src.join('/') + '/' : './'; 1945 | 1946 | cfg.baseUrl = subPath; 1947 | } 1948 | 1949 | //Strip off any trailing .js since mainScript is now 1950 | //like a module name. 1951 | mainScript = mainScript.replace(jsSuffixRegExp, ''); 1952 | 1953 | //If mainScript is still a path, fall back to dataMain 1954 | if (req.jsExtRegExp.test(mainScript)) { 1955 | mainScript = dataMain; 1956 | } 1957 | 1958 | //Put the data-main script in the files to load. 1959 | cfg.deps = cfg.deps ? cfg.deps.concat(mainScript) : [mainScript]; 1960 | 1961 | return true; 1962 | } 1963 | }); 1964 | } 1965 | 1966 | /** 1967 | * The function that handles definitions of modules. Differs from 1968 | * require() in that a string for the module should be the first argument, 1969 | * and the function to execute after dependencies are loaded should 1970 | * return a value to define the module corresponding to the first argument's 1971 | * name. 1972 | */ 1973 | define = function (name, deps, callback) { 1974 | var node, context; 1975 | 1976 | //Allow for anonymous modules 1977 | if (typeof name !== 'string') { 1978 | //Adjust args appropriately 1979 | callback = deps; 1980 | deps = name; 1981 | name = null; 1982 | } 1983 | 1984 | //This module may not have dependencies 1985 | if (!isArray(deps)) { 1986 | callback = deps; 1987 | deps = null; 1988 | } 1989 | 1990 | //If no name, and callback is a function, then figure out if it a 1991 | //CommonJS thing with dependencies. 1992 | if (!deps && isFunction(callback)) { 1993 | deps = []; 1994 | //Remove comments from the callback string, 1995 | //look for require calls, and pull them into the dependencies, 1996 | //but only if there are function args. 1997 | if (callback.length) { 1998 | callback 1999 | .toString() 2000 | .replace(commentRegExp, '') 2001 | .replace(cjsRequireRegExp, function (match, dep) { 2002 | deps.push(dep); 2003 | }); 2004 | 2005 | //May be a CommonJS thing even without require calls, but still 2006 | //could use exports, and module. Avoid doing exports and module 2007 | //work though if it just needs require. 2008 | //REQUIRES the function to expect the CommonJS variables in the 2009 | //order listed below. 2010 | deps = (callback.length === 1 ? ['require'] : ['require', 'exports', 'module']).concat(deps); 2011 | } 2012 | } 2013 | 2014 | //If in IE 6-8 and hit an anonymous define() call, do the interactive 2015 | //work. 2016 | if (useInteractive) { 2017 | node = currentlyAddingScript || getInteractiveScript(); 2018 | if (node) { 2019 | if (!name) { 2020 | name = node.getAttribute('data-requiremodule'); 2021 | } 2022 | context = contexts[node.getAttribute('data-requirecontext')]; 2023 | } 2024 | } 2025 | 2026 | //Always save off evaluating the def call until the script onload handler. 2027 | //This allows multiple modules to be in a file without prematurely 2028 | //tracing dependencies, and allows for anonymous module support, 2029 | //where the module name is not known until the script onload event 2030 | //occurs. If no context, use the global queue, and get it processed 2031 | //in the onscript load callback. 2032 | (context ? context.defQueue : globalDefQueue).push([name, deps, callback]); 2033 | }; 2034 | 2035 | define.amd = { 2036 | jQuery: true 2037 | }; 2038 | 2039 | 2040 | /** 2041 | * Executes the text. Normally just uses eval, but can be modified 2042 | * to use a better, environment-specific call. Only used for transpiling 2043 | * loader plugins, not for plain JS modules. 2044 | * @param {String} text the text to execute/evaluate. 2045 | */ 2046 | req.exec = function (text) { 2047 | /*jslint evil: true */ 2048 | return eval(text); 2049 | }; 2050 | 2051 | //Set up with config info. 2052 | req(cfg); 2053 | }(this)); --------------------------------------------------------------------------------