(function (global, factory) {
  typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
  typeof define === 'function' && define.amd ? define(['exports'], factory) :
  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.troika_worker_utils = {}));
}(this, (function (exports) { 'use strict';

  /**
   * Lightweight thenable implementation that is entirely self-contained within a single
   * function with no external dependencies so it can be easily shipped across to a WorkerModule.
   *
   * This implementation conforms fully to the Promises/A+ spec so it can safely interoperate
   * with other thenable implementations. https://github.com/promises-aplus/promises-spec
   *
   * *However*, it is _not_ a full implementation of ES2015 Promises, e.g. it does not
   * have the same constructor signature and does not expose a `catch` method or the static
   * `resolve`/`reject`/`all`/`race` initializer methods. If you need to hand a Thenable
   * instance off to consuming code that may expect a true Promise, you'll want to wrap it
   * in a native-or-polyfilled Promise first.
   *
   * (Why yet another Promises/A+ implementation? Great question. We needed a polyfill-like
   * thing that was (a) wrapped in a single function for easy serialization across to a Worker,
   * and (b) was as small as possible -- at ~900B minified (~500B gzipped) this is the smallest
   * implementation I've found. And also, exercises like this are challenging and fun.)
   */
  function BespokeThenable() {
    var state = 0; // 0=pending, 1=fulfilled, -1=rejected
    var queue = [];
    var value;
    var scheduled = 0;
    var completeCalled = 0;

    function then(onResolve, onReject) {
      var nextThenable = BespokeThenable();

      function handleNext() {
        var cb = state > 0 ? onResolve : onReject;
        if (isFn(cb)) {
          try {
            var result = cb(value);
            if (result === nextThenable) {
              recursiveError();
            }
            var resultThen = getThenableThen(result);
            if (resultThen) {
              resultThen.call(result, nextThenable.resolve, nextThenable.reject);
            } else {
              nextThenable.resolve(result);
            }
          } catch (err) {
            nextThenable.reject(err);
          }
        } else {
          nextThenable[state > 0 ? 'resolve' : 'reject'](value);
        }
      }

      queue.push(handleNext);
      if (state) {
        scheduleQueueFlush();
      }
      return nextThenable
    }

    var resolve = oneTime(function (val) {
      if (!completeCalled) {
        complete(1, val);
      }
    });

    var reject = oneTime(function (reason) {
      if (!completeCalled) {
        complete(-1, reason);
      }
    });

    function complete(st, val) {
      completeCalled++;
      var ignoreThrow = 0;
      try {
        if (val === thenableObj) {
          recursiveError();
        }
        var valThen = st > 0 && getThenableThen(val);
        if (valThen) {
          valThen.call(val, oneTime(function (v) {
            ignoreThrow++;
            complete(1, v);
          }), oneTime(function (v) {
            ignoreThrow++;
            complete(-1, v);
          }));
        } else {
          state = st;
          value = val;
          scheduleQueueFlush();
        }
      } catch(e) {
        if (!state && !ignoreThrow) {
          complete(-1, e);
        }
      }
    }

    function scheduleQueueFlush() {
      if (!scheduled) {
        setTimeout(flushQueue, 0); //TODO setImmediate or postMessage approach if available?
        scheduled = 1;
      }
    }

    function flushQueue() {
      var q = queue;
      scheduled = 0;
      queue = [];
      q.forEach(callIt);
    }

    function callIt(fn) {
      fn();
    }

    function getThenableThen(val) {
      var valThen = val && (isFn(val) || typeof val === 'object') && val.then;
      return isFn(valThen) && valThen
    }

    function oneTime(fn) {
      var called = 0;
      return function() {
        var args = [], len = arguments.length;
        while ( len-- ) args[ len ] = arguments[ len ];

        if (!called++) {
          fn.apply(this, args);
        }
      }
    }

    function recursiveError() {
      throw new TypeError('Chaining cycle detected')
    }

    var isFn = function (v) { return typeof v === 'function'; };

    var thenableObj = {
      then: then,
      resolve: resolve,
      reject: reject
    };
    return thenableObj
  }


  /**
   * Thenable implementation that uses a native Promise under the covers. This implementation
   * is preferred if Promise is available, for better performance and dev tools integration.
   * @constructor
   */
  function NativePromiseThenable() {
    var resolve, reject;
    var promise = new Promise(function (res, rej) {
      resolve = res;
      reject = rej;
    });
    return {
      then: promise.then.bind(promise),
      resolve: resolve,
      reject: reject
    }
  }

  /**
   * Promise.all() impl:
   */
  BespokeThenable.all = NativePromiseThenable.all = function(items) {
    var resultCount = 0;
    var results = [];
    var out = DefaultThenable();
    if (items.length === 0) {
      out.resolve([]);
    } else {
      items.forEach(function (item, i) {
        var itemThenable = DefaultThenable();
        itemThenable.resolve(item);
        itemThenable.then(function (res) {
          resultCount++;
          results[i] = res;
          if (resultCount === items.length) {
            out.resolve(results);
          }
        }, out.reject);
      });
    }
    return out
  };


  /**
   * Choose the best Thenable implementation and export it as the default.
   */
  var DefaultThenable = typeof Promise === 'function' ? NativePromiseThenable : BespokeThenable;

  /**
   * Main content for the worker that handles the loading and execution of
   * modules within it.
   */
  function workerBootstrap() {
    var modules = Object.create(null);

    // Handle messages for registering a module
    function registerModule(ref, callback) {
      var id = ref.id;
      var name = ref.name;
      var dependencies = ref.dependencies; if ( dependencies === void 0 ) dependencies = [];
      var init = ref.init; if ( init === void 0 ) init = function(){};
      var getTransferables = ref.getTransferables; if ( getTransferables === void 0 ) getTransferables = null;

      // Only register once
      if (modules[id]) { return }

      try {
        // If any dependencies are modules, ensure they're registered and grab their value
        dependencies = dependencies.map(function (dep) {
          if (dep && dep.isWorkerModule) {
            registerModule(dep, function (depResult) {
              if (depResult instanceof Error) { throw depResult }
            });
            dep = modules[dep.id].value;
          }
          return dep
        });

        // Rehydrate functions
        init = rehydrate(("<" + name + ">.init"), init);
        if (getTransferables) {
          getTransferables = rehydrate(("<" + name + ">.getTransferables"), getTransferables);
        }

        // Initialize the module and store its value
        var value = null;
        if (typeof init === 'function') {
          value = init.apply(void 0, dependencies);
        } else {
          console.error('worker module init function failed to rehydrate');
        }
        modules[id] = {
          id: id,
          value: value,
          getTransferables: getTransferables
        };
        callback(value);
      } catch(err) {
        if (!(err && err.noLog)) {
          console.error(err);
        }
        callback(err);
      }
    }

    // Handle messages for calling a registered module's result function
    function callModule(ref, callback) {
      var ref$1;

      var id = ref.id;
      var args = ref.args;
      if (!modules[id] || typeof modules[id].value !== 'function') {
        callback(new Error(("Worker module " + id + ": not found or its 'init' did not return a function")));
      }
      try {
        var result = (ref$1 = modules[id]).value.apply(ref$1, args);
        if (result && typeof result.then === 'function') {
          result.then(handleResult, function (rej) { return callback(rej instanceof Error ? rej : new Error('' + rej)); });
        } else {
          handleResult(result);
        }
      } catch(err) {
        callback(err);
      }
      function handleResult(result) {
        try {
          var tx = modules[id].getTransferables && modules[id].getTransferables(result);
          if (!tx || !Array.isArray(tx) || !tx.length) {
            tx = undefined; //postMessage is very picky about not passing null or empty transferables
          }
          callback(result, tx);
        } catch(err) {
          console.error(err);
          callback(err);
        }
      }
    }

    function rehydrate(name, str) {
      var result = void 0;
      self.troikaDefine = function (r) { return result = r; };
      var url = URL.createObjectURL(
        new Blob(
          [("/** " + (name.replace(/\*/g, '')) + " **/\n\ntroikaDefine(\n" + str + "\n)")],
          {type: 'application/javascript'}
        )
      );
      try {
        importScripts(url);
      } catch(err) {
        console.error(err);
      }
      URL.revokeObjectURL(url);
      delete self.troikaDefine;
      return result
    }

    // Handler for all messages within the worker
    self.addEventListener('message', function (e) {
      var ref = e.data;
      var messageId = ref.messageId;
      var action = ref.action;
      var data = ref.data;
      try {
        // Module registration
        if (action === 'registerModule') {
          registerModule(data, function (result) {
            if (result instanceof Error) {
              postMessage({
                messageId: messageId,
                success: false,
                error: result.message
              });
            } else {
              postMessage({
                messageId: messageId,
                success: true,
                result: {isCallable: typeof result === 'function'}
              });
            }
          });
        }
        // Invocation
        if (action === 'callModule') {
          callModule(data, function (result, transferables) {
            if (result instanceof Error) {
              postMessage({
                messageId: messageId,
                success: false,
                error: result.message
              });
            } else {
              postMessage({
                messageId: messageId,
                success: true,
                result: result
              }, transferables || undefined);
            }
          });
        }
      } catch(err) {
        postMessage({
          messageId: messageId,
          success: false,
          error: err.stack
        });
      }
    });
  }

  /**
   * Fallback for `defineWorkerModule` that behaves identically but runs in the main
   * thread, for when the execution environment doesn't support web workers or they
   * are disallowed due to e.g. CSP security restrictions.
   */
  function defineMainThreadModule(options) {
    var moduleFunc = function() {
      var args = [], len = arguments.length;
      while ( len-- ) args[ len ] = arguments[ len ];

      return moduleFunc._getInitResult().then(function (initResult) {
        if (typeof initResult === 'function') {
          return initResult.apply(void 0, args)
        } else {
          throw new Error('Worker module function was called but `init` did not return a callable function')
        }
      })
    };
    moduleFunc._getInitResult = function() {
      // We can ignore getTransferables in main thread. TODO workerId?
      var dependencies = options.dependencies;
      var init = options.init;

      // Resolve dependencies
      dependencies = Array.isArray(dependencies) ? dependencies.map(function (dep) { return dep && dep._getInitResult ? dep._getInitResult() : dep; }
      ) : [];

      // Invoke init with the resolved dependencies
      var initThenable = DefaultThenable.all(dependencies).then(function (deps) {
        return init.apply(null, deps)
      });

      // Cache the resolved promise for subsequent calls
      moduleFunc._getInitResult = function () { return initThenable; };

      return initThenable
    };
    return moduleFunc
  }

  var supportsWorkers = function () {
    var supported = false;

    // Only attempt worker initialization in browsers; elsewhere it would just be
    // noise e.g. loading into a Node environment for SSR.
    if (typeof window !== 'undefined' && typeof window.document !== 'undefined') {
      try {
        // TODO additional checks for things like importScripts within the worker?
        //  Would need to be an async check.
        var worker = new Worker(
          URL.createObjectURL(new Blob([''], { type: 'application/javascript' }))
        );
        worker.terminate();
        supported = true;
      } catch (err) {
        if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') ; else {
          console.log(
            ("Troika createWorkerModule: web workers not allowed; falling back to main thread execution. Cause: [" + (err.message) + "]")
          );
        }
      }
    }

    // Cached result
    supportsWorkers = function () { return supported; };
    return supported
  };

  var _workerModuleId = 0;
  var _messageId = 0;
  var _allowInitAsString = false;
  var workers = Object.create(null);
  var registeredModules = Object.create(null); //workerId -> Set<unregisterFn>
  var openRequests = Object.create(null);


  /**
   * Define a module of code that will be executed with a web worker. This provides a simple
   * interface for moving chunks of logic off the main thread, and managing their dependencies
   * among one another.
   *
   * @param {object} options
   * @param {function} options.init
   * @param {array} [options.dependencies]
   * @param {function} [options.getTransferables]
   * @param {string} [options.name]
   * @param {string} [options.workerId]
   * @return {function(...[*]): {then}}
   */
  function defineWorkerModule(options) {
    if ((!options || typeof options.init !== 'function') && !_allowInitAsString) {
      throw new Error('requires `options.init` function')
    }
    var dependencies = options.dependencies;
    var init = options.init;
    var getTransferables = options.getTransferables;
    var workerId = options.workerId;

    if (!supportsWorkers()) {
      return defineMainThreadModule(options)
    }

    if (workerId == null) {
      workerId = '#default';
    }
    var id = "workerModule" + (++_workerModuleId);
    var name = options.name || id;
    var registrationThenable = null;

    dependencies = dependencies && dependencies.map(function (dep) {
      // Wrap raw functions as worker modules with no dependencies
      if (typeof dep === 'function' && !dep.workerModuleData) {
        _allowInitAsString = true;
        dep = defineWorkerModule({
          workerId: workerId,
          name: ("<" + name + "> function dependency: " + (dep.name)),
          init: ("function(){return (\n" + (stringifyFunction(dep)) + "\n)}")
        });
        _allowInitAsString = false;
      }
      // Grab postable data for worker modules
      if (dep && dep.workerModuleData) {
        dep = dep.workerModuleData;
      }
      return dep
    });

    function moduleFunc() {
      var args = [], len = arguments.length;
      while ( len-- ) args[ len ] = arguments[ len ];

      // Register this module if needed
      if (!registrationThenable) {
        registrationThenable = callWorker(workerId,'registerModule', moduleFunc.workerModuleData);
        var unregister = function () {
          registrationThenable = null;
          registeredModules[workerId].delete(unregister);
        }
        ;(registeredModules[workerId] || (registeredModules[workerId] = new Set())).add(unregister);
      }

      // Invoke the module, returning a thenable
      return registrationThenable.then(function (ref) {
        var isCallable = ref.isCallable;

        if (isCallable) {
          return callWorker(workerId,'callModule', {id: id, args: args})
        } else {
          throw new Error('Worker module function was called but `init` did not return a callable function')
        }
      })
    }
    moduleFunc.workerModuleData = {
      isWorkerModule: true,
      id: id,
      name: name,
      dependencies: dependencies,
      init: stringifyFunction(init),
      getTransferables: getTransferables && stringifyFunction(getTransferables)
    };
    return moduleFunc
  }

  /**
   * Terminate an active Worker by a workerId that was passed to defineWorkerModule.
   * This only terminates the Worker itself; the worker module will remain available
   * and if you call it again its Worker will be respawned.
   * @param {string} workerId
   */
  function terminateWorker(workerId) {
    // Unregister all modules that were registered in that worker
    if (registeredModules[workerId]) {
      registeredModules[workerId].forEach(function (unregister) {
        unregister();
      });
    }
    // Terminate the Worker object
    if (workers[workerId]) {
      workers[workerId].terminate();
      delete workers[workerId];
    }
  }

  /**
   * Stringifies a function into a form that can be deserialized in the worker
   * @param fn
   */
  function stringifyFunction(fn) {
    var str = fn.toString();
    // If it was defined in object method/property format, it needs to be modified
    if (!/^function/.test(str) && /^\w+\s*\(/.test(str)) {
      str = 'function ' + str;
    }
    return str
  }


  function getWorker(workerId) {
    var worker = workers[workerId];
    if (!worker) {
      // Bootstrap the worker's content
      var bootstrap = stringifyFunction(workerBootstrap);

      // Create the worker from the bootstrap function content
      worker = workers[workerId] = new Worker(
        URL.createObjectURL(
          new Blob(
            [("/** Worker Module Bootstrap: " + (workerId.replace(/\*/g, '')) + " **/\n\n;(" + bootstrap + ")()")],
            {type: 'application/javascript'}
          )
        )
      );

      // Single handler for response messages from the worker
      worker.onmessage = function (e) {
        var response = e.data;
        var msgId = response.messageId;
        var callback = openRequests[msgId];
        if (!callback) {
          throw new Error('WorkerModule response with empty or unknown messageId')
        }
        delete openRequests[msgId];
        callback(response);
      };
    }
    return worker
  }

  // Issue a call to the worker with a callback to handle the response
  function callWorker(workerId, action, data) {
    var thenable = DefaultThenable();
    var messageId = ++_messageId;
    openRequests[messageId] = function (response) {
      if (response.success) {
        thenable.resolve(response.result);
      } else {
        thenable.reject(new Error(("Error in worker " + action + " call: " + (response.error))));
      }
    };
    getWorker(workerId).postMessage({
      messageId: messageId,
      action: action,
      data: data
    });
    return thenable
  }

  /**
   * Just the {@link Thenable} function wrapped as a worker module. If another worker
   * module needs Thenable as a dependency, it's better to pass this module rather than
   * the raw function in its `dependencies` array so it only gets registered once.
   */
  var ThenableWorkerModule = /*#__PURE__*/defineWorkerModule({
    name: 'Thenable',
    dependencies: [DefaultThenable],
    init: function(Thenable) {
      return Thenable
    }
  });

  exports.Thenable = DefaultThenable;
  exports.ThenableWorkerModule = ThenableWorkerModule;
  exports.defineWorkerModule = defineWorkerModule;
  exports.stringifyFunction = stringifyFunction;
  exports.terminateWorker = terminateWorker;

  Object.defineProperty(exports, '__esModule', { value: true });

})));
