├── .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 |