Permalink
Join GitHub today
GitHub is home to over 28 million developers working together to host and review code, manage projects, and build software together.
Sign up| /* globals setImmediate, clearImmediate, setTimeout, clearTimeout */ | |
| /* jshint -W083, -W098, -W003 */ | |
| "use strict"; | |
| var Aff = function () { | |
| // A unique value for empty. | |
| var EMPTY = {}; | |
| /* | |
| An awkward approximation. We elide evidence we would otherwise need in PS for | |
| efficiency sake. | |
| data Aff eff a | |
| = Pure a | |
| | Throw Error | |
| | Catch (Aff eff a) (Error -> Aff eff a) | |
| | Sync (Eff eff a) | |
| | Async ((Either Error a -> Eff eff Unit) -> Eff eff (Canceler eff)) | |
| | forall b. Bind (Aff eff b) (b -> Aff eff a) | |
| | forall b. Bracket (Aff eff b) (BracketConditions eff b) (b -> Aff eff a) | |
| | forall b. Fork Boolean (Aff eff b) ?(Fiber eff b -> a) | |
| | Sequential (ParAff aff a) | |
| */ | |
| var PURE = "Pure"; | |
| var THROW = "Throw"; | |
| var CATCH = "Catch"; | |
| var SYNC = "Sync"; | |
| var ASYNC = "Async"; | |
| var BIND = "Bind"; | |
| var BRACKET = "Bracket"; | |
| var FORK = "Fork"; | |
| var SEQ = "Sequential"; | |
| /* | |
| data ParAff eff a | |
| = forall b. Map (b -> a) (ParAff eff b) | |
| | forall b. Apply (ParAff eff (b -> a)) (ParAff eff b) | |
| | Alt (ParAff eff a) (ParAff eff a) | |
| | ?Par (Aff eff a) | |
| */ | |
| var MAP = "Map"; | |
| var APPLY = "Apply"; | |
| var ALT = "Alt"; | |
| // Various constructors used in interpretation | |
| var CONS = "Cons"; // Cons-list, for stacks | |
| var RESUME = "Resume"; // Continue indiscriminately | |
| var RELEASE = "Release"; // Continue with bracket finalizers | |
| var FINALIZER = "Finalizer"; // A non-interruptible effect | |
| var FINALIZED = "Finalized"; // Marker for finalization | |
| var FORKED = "Forked"; // Reference to a forked fiber, with resumption stack | |
| var FIBER = "Fiber"; // Actual fiber reference | |
| var THUNK = "Thunk"; // Primed effect, ready to invoke | |
| function Aff(tag, _1, _2, _3) { | |
| this.tag = tag; | |
| this._1 = _1; | |
| this._2 = _2; | |
| this._3 = _3; | |
| } | |
| function AffCtr(tag) { | |
| var fn = function (_1, _2, _3) { | |
| return new Aff(tag, _1, _2, _3); | |
| }; | |
| fn.tag = tag; | |
| return fn; | |
| } | |
| function nonCanceler(error) { | |
| return new Aff(PURE, void 0); | |
| } | |
| function runEff(eff) { | |
| try { | |
| eff(); | |
| } catch (error) { | |
| setTimeout(function () { | |
| throw error; | |
| }, 0); | |
| } | |
| } | |
| function runSync(left, right, eff) { | |
| try { | |
| return right(eff()); | |
| } catch (error) { | |
| return left(error); | |
| } | |
| } | |
| function runAsync(left, eff, k) { | |
| try { | |
| return eff(k)(); | |
| } catch (error) { | |
| k(left(error))(); | |
| return nonCanceler; | |
| } | |
| } | |
| var Scheduler = function () { | |
| var limit = 1024; | |
| var size = 0; | |
| var ix = 0; | |
| var queue = new Array(limit); | |
| var draining = false; | |
| function drain() { | |
| var thunk; | |
| draining = true; | |
| while (size !== 0) { | |
| size--; | |
| thunk = queue[ix]; | |
| queue[ix] = void 0; | |
| ix = (ix + 1) % limit; | |
| thunk(); | |
| } | |
| draining = false; | |
| } | |
| return { | |
| isDraining: function () { | |
| return draining; | |
| }, | |
| enqueue: function (cb) { | |
| var i, tmp; | |
| if (size === limit) { | |
| tmp = draining; | |
| drain(); | |
| draining = tmp; | |
| } | |
| queue[(ix + size) % limit] = cb; | |
| size++; | |
| if (!draining) { | |
| drain(); | |
| } | |
| } | |
| }; | |
| }(); | |
| function Supervisor(util) { | |
| var fibers = {}; | |
| var fiberId = 0; | |
| var count = 0; | |
| return { | |
| register: function (fiber) { | |
| var fid = fiberId++; | |
| fiber.onComplete({ | |
| rethrow: true, | |
| handler: function (result) { | |
| return function () { | |
| count--; | |
| delete fibers[fid]; | |
| }; | |
| } | |
| }); | |
| fibers[fid] = fiber; | |
| count++; | |
| }, | |
| isEmpty: function () { | |
| return count === 0; | |
| }, | |
| killAll: function (killError, cb) { | |
| return function () { | |
| var killCount = 0; | |
| var kills = {}; | |
| function kill(fid) { | |
| kills[fid] = fibers[fid].kill(killError, function (result) { | |
| return function () { | |
| delete kills[fid]; | |
| killCount--; | |
| if (util.isLeft(result) && util.fromLeft(result)) { | |
| setTimeout(function () { | |
| throw util.fromLeft(result); | |
| }, 0); | |
| } | |
| if (killCount === 0) { | |
| cb(); | |
| } | |
| }; | |
| })(); | |
| } | |
| for (var k in fibers) { | |
| if (fibers.hasOwnProperty(k)) { | |
| killCount++; | |
| kill(k); | |
| } | |
| } | |
| fibers = {}; | |
| fiberId = 0; | |
| count = 0; | |
| return function (error) { | |
| return new Aff(SYNC, function () { | |
| for (var k in kills) { | |
| if (kills.hasOwnProperty(k)) { | |
| kills[k](); | |
| } | |
| } | |
| }); | |
| }; | |
| }; | |
| } | |
| }; | |
| } | |
| // Fiber state machine | |
| var SUSPENDED = 0; // Suspended, pending a join. | |
| var CONTINUE = 1; // Interpret the next instruction. | |
| var STEP_BIND = 2; // Apply the next bind. | |
| var STEP_RESULT = 3; // Handle potential failure from a result. | |
| var PENDING = 4; // An async effect is running. | |
| var RETURN = 5; // The current stack has returned. | |
| var COMPLETED = 6; // The entire fiber has completed. | |
| function Fiber(util, supervisor, aff) { | |
| // Monotonically increasing tick, increased on each asynchronous turn. | |
| var runTick = 0; | |
| // The current branch of the state machine. | |
| var status = SUSPENDED; | |
| // The current point of interest for the state machine branch. | |
| var step = aff; // Successful step | |
| var fail = null; // Failure step | |
| var interrupt = null; // Asynchronous interrupt | |
| // Stack of continuations for the current fiber. | |
| var bhead = null; | |
| var btail = null; | |
| // Stack of attempts and finalizers for error recovery. Every `Cons` is also | |
| // tagged with current `interrupt` state. We use this to track which items | |
| // should be ignored or evaluated as a result of a kill. | |
| var attempts = null; | |
| // A special state is needed for Bracket, because it cannot be killed. When | |
| // we enter a bracket acquisition or finalizer, we increment the counter, | |
| // and then decrement once complete. | |
| var bracketCount = 0; | |
| // Each join gets a new id so they can be revoked. | |
| var joinId = 0; | |
| var joins = null; | |
| var rethrow = true; | |
| // Each invocation of `run` requires a tick. When an asynchronous effect is | |
| // resolved, we must check that the local tick coincides with the fiber | |
| // tick before resuming. This prevents multiple async continuations from | |
| // accidentally resuming the same fiber. A common example may be invoking | |
| // the provided callback in `makeAff` more than once, but it may also be an | |
| // async effect resuming after the fiber was already cancelled. | |
| function run(localRunTick) { | |
| var tmp, result, attempt; | |
| while (true) { | |
| tmp = null; | |
| result = null; | |
| attempt = null; | |
| switch (status) { | |
| case STEP_BIND: | |
| status = CONTINUE; | |
| step = bhead(step); | |
| if (btail === null) { | |
| bhead = null; | |
| } else { | |
| bhead = btail._1; | |
| btail = btail._2; | |
| } | |
| break; | |
| case STEP_RESULT: | |
| if (util.isLeft(step)) { | |
| status = RETURN; | |
| fail = step; | |
| step = null; | |
| } else if (bhead === null) { | |
| status = RETURN; | |
| } else { | |
| status = STEP_BIND; | |
| step = util.fromRight(step); | |
| } | |
| break; | |
| case CONTINUE: | |
| switch (step.tag) { | |
| case BIND: | |
| if (bhead) { | |
| btail = new Aff(CONS, bhead, btail); | |
| } | |
| bhead = step._2; | |
| status = CONTINUE; | |
| step = step._1; | |
| break; | |
| case PURE: | |
| if (bhead === null) { | |
| status = RETURN; | |
| step = util.right(step._1); | |
| } else { | |
| status = STEP_BIND; | |
| step = step._1; | |
| } | |
| break; | |
| case SYNC: | |
| status = STEP_RESULT; | |
| step = runSync(util.left, util.right, step._1); | |
| break; | |
| case ASYNC: | |
| status = PENDING; | |
| step = runAsync(util.left, step._1, function (result) { | |
| return function () { | |
| if (runTick !== localRunTick) { | |
| return; | |
| } | |
| runTick++; | |
| Scheduler.enqueue(function () { | |
| // It's possible to interrupt the fiber between enqueuing and | |
| // resuming, so we need to check that the runTick is still | |
| // valid. | |
| if (runTick !== localRunTick + 1) { | |
| return; | |
| } | |
| status = STEP_RESULT; | |
| step = result; | |
| run(runTick); | |
| }); | |
| }; | |
| }); | |
| return; | |
| case THROW: | |
| status = RETURN; | |
| fail = util.left(step._1); | |
| step = null; | |
| break; | |
| // Enqueue the Catch so that we can call the error handler later on | |
| // in case of an exception. | |
| case CATCH: | |
| if (bhead === null) { | |
| attempts = new Aff(CONS, step, attempts, interrupt); | |
| } else { | |
| attempts = new Aff(CONS, step, new Aff(CONS, new Aff(RESUME, bhead, btail), attempts, interrupt), interrupt); | |
| } | |
| bhead = null; | |
| btail = null; | |
| status = CONTINUE; | |
| step = step._1; | |
| break; | |
| // Enqueue the Bracket so that we can call the appropriate handlers | |
| // after resource acquisition. | |
| case BRACKET: | |
| bracketCount++; | |
| if (bhead === null) { | |
| attempts = new Aff(CONS, step, attempts, interrupt); | |
| } else { | |
| attempts = new Aff(CONS, step, new Aff(CONS, new Aff(RESUME, bhead, btail), attempts, interrupt), interrupt); | |
| } | |
| bhead = null; | |
| btail = null; | |
| status = CONTINUE; | |
| step = step._1; | |
| break; | |
| case FORK: | |
| status = STEP_RESULT; | |
| tmp = Fiber(util, supervisor, step._2); | |
| if (supervisor) { | |
| supervisor.register(tmp); | |
| } | |
| if (step._1) { | |
| tmp.run(); | |
| } | |
| step = util.right(tmp); | |
| break; | |
| case SEQ: | |
| status = CONTINUE; | |
| step = sequential(util, supervisor, step._1); | |
| break; | |
| } | |
| break; | |
| case RETURN: | |
| bhead = null; | |
| btail = null; | |
| // If the current stack has returned, and we have no other stacks to | |
| // resume or finalizers to run, the fiber has halted and we can | |
| // invoke all join callbacks. Otherwise we need to resume. | |
| if (attempts === null) { | |
| status = COMPLETED; | |
| step = interrupt || fail || step; | |
| } else { | |
| // The interrupt status for the enqueued item. | |
| tmp = attempts._3; | |
| attempt = attempts._1; | |
| attempts = attempts._2; | |
| switch (attempt.tag) { | |
| // We cannot recover from an interrupt. Otherwise we should | |
| // continue stepping, or run the exception handler if an exception | |
| // was raised. | |
| case CATCH: | |
| // We should compare the interrupt status as well because we | |
| // only want it to apply if there has been an interrupt since | |
| // enqueuing the catch. | |
| if (interrupt && interrupt !== tmp) { | |
| status = RETURN; | |
| } else if (fail) { | |
| status = CONTINUE; | |
| step = attempt._2(util.fromLeft(fail)); | |
| fail = null; | |
| } | |
| break; | |
| // We cannot resume from an interrupt or exception. | |
| case RESUME: | |
| // As with Catch, we only want to ignore in the case of an | |
| // interrupt since enqueing the item. | |
| if (interrupt && interrupt !== tmp || fail) { | |
| status = RETURN; | |
| } else { | |
| bhead = attempt._1; | |
| btail = attempt._2; | |
| status = STEP_BIND; | |
| step = util.fromRight(step); | |
| } | |
| break; | |
| // If we have a bracket, we should enqueue the handlers, | |
| // and continue with the success branch only if the fiber has | |
| // not been interrupted. If the bracket acquisition failed, we | |
| // should not run either. | |
| case BRACKET: | |
| bracketCount--; | |
| if (fail === null) { | |
| result = util.fromRight(step); | |
| // We need to enqueue the Release with the same interrupt | |
| // status as the Bracket that is initiating it. | |
| attempts = new Aff(CONS, new Aff(RELEASE, attempt._2, result), attempts, tmp); | |
| // We should only coninue as long as the interrupt status has not changed or | |
| // we are currently within a non-interruptable finalizer. | |
| if (interrupt === tmp || bracketCount > 0) { | |
| status = CONTINUE; | |
| step = attempt._3(result); | |
| } | |
| } | |
| break; | |
| // Enqueue the appropriate handler. We increase the bracket count | |
| // because it should not be cancelled. | |
| case RELEASE: | |
| bracketCount++; | |
| attempts = new Aff(CONS, new Aff(FINALIZED, step, fail), attempts, interrupt); | |
| status = CONTINUE; | |
| // It has only been killed if the interrupt status has changed | |
| // since we enqueued the item. | |
| if (interrupt && interrupt !== tmp) { | |
| step = attempt._1.killed(util.fromLeft(interrupt))(attempt._2); | |
| } else if (fail) { | |
| step = attempt._1.failed(util.fromLeft(fail))(attempt._2); | |
| } else { | |
| step = attempt._1.completed(util.fromRight(step))(attempt._2); | |
| } | |
| fail = null; | |
| break; | |
| case FINALIZER: | |
| bracketCount++; | |
| attempts = new Aff(CONS, new Aff(FINALIZED, step, fail), attempts, interrupt); | |
| status = CONTINUE; | |
| step = attempt._1; | |
| break; | |
| case FINALIZED: | |
| bracketCount--; | |
| status = RETURN; | |
| step = attempt._1; | |
| fail = attempt._2; | |
| break; | |
| } | |
| } | |
| break; | |
| case COMPLETED: | |
| for (var k in joins) { | |
| if (joins.hasOwnProperty(k)) { | |
| rethrow = rethrow && joins[k].rethrow; | |
| runEff(joins[k].handler(step)); | |
| } | |
| } | |
| joins = null; | |
| // If we have an interrupt and a fail, then the thread threw while | |
| // running finalizers. This should always rethrow in a fresh stack. | |
| if (interrupt && fail) { | |
| setTimeout(function () { | |
| throw util.fromLeft(fail); | |
| }, 0); | |
| // If we have an unhandled exception, and no other fiber has joined | |
| // then we need to throw the exception in a fresh stack. | |
| } else if (util.isLeft(step) && rethrow) { | |
| setTimeout(function () { | |
| // Guard on reathrow because a completely synchronous fiber can | |
| // still have an observer which was added after-the-fact. | |
| if (rethrow) { | |
| throw util.fromLeft(step); | |
| } | |
| }, 0); | |
| } | |
| return; | |
| case SUSPENDED: | |
| status = CONTINUE; | |
| break; | |
| case PENDING: return; | |
| } | |
| } | |
| } | |
| function onComplete(join) { | |
| return function () { | |
| if (status === COMPLETED) { | |
| rethrow = rethrow && join.rethrow; | |
| join.handler(step)(); | |
| return function () {}; | |
| } | |
| var jid = joinId++; | |
| joins = joins || {}; | |
| joins[jid] = join; | |
| return function() { | |
| if (joins !== null) { | |
| delete joins[jid]; | |
| } | |
| }; | |
| }; | |
| } | |
| function kill(error, cb) { | |
| return function () { | |
| if (status === COMPLETED) { | |
| cb(util.right(void 0))(); | |
| return function () {}; | |
| } | |
| var canceler = onComplete({ | |
| rethrow: false, | |
| handler: function (/* unused */) { | |
| return cb(util.right(void 0)); | |
| } | |
| })(); | |
| switch (status) { | |
| case SUSPENDED: | |
| interrupt = util.left(error); | |
| status = COMPLETED; | |
| step = interrupt; | |
| run(runTick); | |
| break; | |
| case PENDING: | |
| if (interrupt === null) { | |
| interrupt = util.left(error); | |
| } | |
| if (bracketCount === 0) { | |
| if (status === PENDING) { | |
| attempts = new Aff(CONS, new Aff(FINALIZER, step(error)), attempts, interrupt); | |
| } | |
| status = RETURN; | |
| step = null; | |
| fail = null; | |
| run(++runTick); | |
| } | |
| break; | |
| default: | |
| if (interrupt === null) { | |
| interrupt = util.left(error); | |
| } | |
| if (bracketCount === 0) { | |
| status = RETURN; | |
| step = null; | |
| fail = null; | |
| } | |
| } | |
| return canceler; | |
| }; | |
| } | |
| function join(cb) { | |
| return function () { | |
| var canceler = onComplete({ | |
| rethrow: false, | |
| handler: cb | |
| })(); | |
| if (status === SUSPENDED) { | |
| run(runTick); | |
| } | |
| return canceler; | |
| }; | |
| } | |
| return { | |
| kill: kill, | |
| join: join, | |
| onComplete: onComplete, | |
| isSuspended: function () { | |
| return status === SUSPENDED; | |
| }, | |
| run: function () { | |
| if (status === SUSPENDED) { | |
| if (!Scheduler.isDraining()) { | |
| Scheduler.enqueue(function () { | |
| run(runTick); | |
| }); | |
| } else { | |
| run(runTick); | |
| } | |
| } | |
| } | |
| }; | |
| } | |
| function runPar(util, supervisor, par, cb) { | |
| // Table of all forked fibers. | |
| var fiberId = 0; | |
| var fibers = {}; | |
| // Table of currently running cancelers, as a product of `Alt` behavior. | |
| var killId = 0; | |
| var kills = {}; | |
| // Error used for early cancelation on Alt branches. | |
| var early = new Error("[ParAff] Early exit"); | |
| // Error used to kill the entire tree. | |
| var interrupt = null; | |
| // The root pointer of the tree. | |
| var root = EMPTY; | |
| // Walks a tree, invoking all the cancelers. Returns the table of pending | |
| // cancellation fibers. | |
| function kill(error, par, cb) { | |
| var step = par; | |
| var head = null; | |
| var tail = null; | |
| var count = 0; | |
| var kills = {}; | |
| var tmp, kid; | |
| loop: while (true) { | |
| tmp = null; | |
| switch (step.tag) { | |
| case FORKED: | |
| if (step._3 === EMPTY) { | |
| tmp = fibers[step._1]; | |
| kills[count++] = tmp.kill(error, function (result) { | |
| return function () { | |
| count--; | |
| if (count === 0) { | |
| cb(result)(); | |
| } | |
| }; | |
| }); | |
| } | |
| // Terminal case. | |
| if (head === null) { | |
| break loop; | |
| } | |
| // Go down the right side of the tree. | |
| step = head._2; | |
| if (tail === null) { | |
| head = null; | |
| } else { | |
| head = tail._1; | |
| tail = tail._2; | |
| } | |
| break; | |
| case MAP: | |
| step = step._2; | |
| break; | |
| case APPLY: | |
| case ALT: | |
| if (head) { | |
| tail = new Aff(CONS, head, tail); | |
| } | |
| head = step; | |
| step = step._1; | |
| break; | |
| } | |
| } | |
| if (count === 0) { | |
| cb(util.right(void 0))(); | |
| } else { | |
| // Run the cancelation effects. We alias `count` because it's mutable. | |
| kid = 0; | |
| tmp = count; | |
| for (; kid < tmp; kid++) { | |
| kills[kid] = kills[kid](); | |
| } | |
| } | |
| return kills; | |
| } | |
| // When a fiber resolves, we need to bubble back up the tree with the | |
| // result, computing the applicative nodes. | |
| function join(result, head, tail) { | |
| var fail, step, lhs, rhs, tmp, kid; | |
| if (util.isLeft(result)) { | |
| fail = result; | |
| step = null; | |
| } else { | |
| step = result; | |
| fail = null; | |
| } | |
| loop: while (true) { | |
| lhs = null; | |
| rhs = null; | |
| tmp = null; | |
| kid = null; | |
| // We should never continue if the entire tree has been interrupted. | |
| if (interrupt !== null) { | |
| return; | |
| } | |
| // We've made it all the way to the root of the tree, which means | |
| // the tree has fully evaluated. | |
| if (head === null) { | |
| cb(fail || step)(); | |
| return; | |
| } | |
| // The tree has already been computed, so we shouldn't try to do it | |
| // again. This should never happen. | |
| // TODO: Remove this? | |
| if (head._3 !== EMPTY) { | |
| return; | |
| } | |
| switch (head.tag) { | |
| case MAP: | |
| if (fail === null) { | |
| head._3 = util.right(head._1(util.fromRight(step))); | |
| step = head._3; | |
| } else { | |
| head._3 = fail; | |
| } | |
| break; | |
| case APPLY: | |
| lhs = head._1._3; | |
| rhs = head._2._3; | |
| // If we have a failure we should kill the other side because we | |
| // can't possible yield a result anymore. | |
| if (fail) { | |
| head._3 = fail; | |
| tmp = true; | |
| kid = killId++; | |
| kills[kid] = kill(early, fail === lhs ? head._2 : head._1, function (/* unused */) { | |
| return function () { | |
| delete kills[kid]; | |
| if (tmp) { | |
| tmp = false; | |
| } else if (tail === null) { | |
| join(fail, null, null); | |
| } else { | |
| join(fail, tail._1, tail._2); | |
| } | |
| }; | |
| }); | |
| if (tmp) { | |
| tmp = false; | |
| return; | |
| } | |
| } else if (lhs === EMPTY || rhs === EMPTY) { | |
| // We can only proceed if both sides have resolved. | |
| return; | |
| } else { | |
| step = util.right(util.fromRight(lhs)(util.fromRight(rhs))); | |
| head._3 = step; | |
| } | |
| break; | |
| case ALT: | |
| lhs = head._1._3; | |
| rhs = head._2._3; | |
| // We can only proceed if both have resolved or we have a success | |
| if (lhs === EMPTY && util.isLeft(rhs) || rhs === EMPTY && util.isLeft(lhs)) { | |
| return; | |
| } | |
| // If both sides resolve with an error, we should continue with the | |
| // first error | |
| if (lhs !== EMPTY && util.isLeft(lhs) && rhs !== EMPTY && util.isLeft(rhs)) { | |
| fail = step === lhs ? rhs : lhs; | |
| step = null; | |
| head._3 = fail; | |
| } else { | |
| head._3 = step; | |
| tmp = true; | |
| kid = killId++; | |
| // Once a side has resolved, we need to cancel the side that is still | |
| // pending before we can continue. | |
| kills[kid] = kill(early, step === lhs ? head._2 : head._1, function (/* unused */) { | |
| return function () { | |
| delete kills[kid]; | |
| if (tmp) { | |
| tmp = false; | |
| } else if (tail === null) { | |
| join(step, null, null); | |
| } else { | |
| join(step, tail._1, tail._2); | |
| } | |
| }; | |
| }); | |
| if (tmp) { | |
| tmp = false; | |
| return; | |
| } | |
| } | |
| break; | |
| } | |
| if (tail === null) { | |
| head = null; | |
| } else { | |
| head = tail._1; | |
| tail = tail._2; | |
| } | |
| } | |
| } | |
| function resolve(fiber) { | |
| return function (result) { | |
| return function () { | |
| delete fibers[fiber._1]; | |
| fiber._3 = result; | |
| join(result, fiber._2._1, fiber._2._2); | |
| }; | |
| }; | |
| } | |
| // Walks the applicative tree, substituting non-applicative nodes with | |
| // `FORKED` nodes. In this tree, all applicative nodes use the `_3` slot | |
| // as a mutable slot for memoization. In an unresolved state, the `_3` | |
| // slot is `EMPTY`. In the cases of `ALT` and `APPLY`, we always walk | |
| // the left side first, because both operations are left-associative. As | |
| // we `RETURN` from those branches, we then walk the right side. | |
| function run() { | |
| var status = CONTINUE; | |
| var step = par; | |
| var head = null; | |
| var tail = null; | |
| var tmp, fid; | |
| loop: while (true) { | |
| tmp = null; | |
| fid = null; | |
| switch (status) { | |
| case CONTINUE: | |
| switch (step.tag) { | |
| case MAP: | |
| if (head) { | |
| tail = new Aff(CONS, head, tail); | |
| } | |
| head = new Aff(MAP, step._1, EMPTY, EMPTY); | |
| step = step._2; | |
| break; | |
| case APPLY: | |
| if (head) { | |
| tail = new Aff(CONS, head, tail); | |
| } | |
| head = new Aff(APPLY, EMPTY, step._2, EMPTY); | |
| step = step._1; | |
| break; | |
| case ALT: | |
| if (head) { | |
| tail = new Aff(CONS, head, tail); | |
| } | |
| head = new Aff(ALT, EMPTY, step._2, EMPTY); | |
| step = step._1; | |
| break; | |
| default: | |
| // When we hit a leaf value, we suspend the stack in the `FORKED`. | |
| // When the fiber resolves, it can bubble back up the tree. | |
| fid = fiberId++; | |
| status = RETURN; | |
| tmp = step; | |
| step = new Aff(FORKED, fid, new Aff(CONS, head, tail), EMPTY); | |
| tmp = Fiber(util, supervisor, tmp); | |
| tmp.onComplete({ | |
| rethrow: false, | |
| handler: resolve(step) | |
| })(); | |
| fibers[fid] = tmp; | |
| if (supervisor) { | |
| supervisor.register(tmp); | |
| } | |
| } | |
| break; | |
| case RETURN: | |
| // Terminal case, we are back at the root. | |
| if (head === null) { | |
| break loop; | |
| } | |
| // If we are done with the right side, we need to continue down the | |
| // left. Otherwise we should continue up the stack. | |
| if (head._1 === EMPTY) { | |
| head._1 = step; | |
| status = CONTINUE; | |
| step = head._2; | |
| head._2 = EMPTY; | |
| } else { | |
| head._2 = step; | |
| step = head; | |
| if (tail === null) { | |
| head = null; | |
| } else { | |
| head = tail._1; | |
| tail = tail._2; | |
| } | |
| } | |
| } | |
| } | |
| // Keep a reference to the tree root so it can be cancelled. | |
| root = step; | |
| for (fid = 0; fid < fiberId; fid++) { | |
| fibers[fid].run(); | |
| } | |
| } | |
| // Cancels the entire tree. If there are already subtrees being canceled, | |
| // we need to first cancel those joins. We will then add fresh joins for | |
| // all pending branches including those that were in the process of being | |
| // canceled. | |
| function cancel(error, cb) { | |
| interrupt = util.left(error); | |
| var innerKills; | |
| for (var kid in kills) { | |
| if (kills.hasOwnProperty(kid)) { | |
| innerKills = kills[kid]; | |
| for (kid in innerKills) { | |
| if (innerKills.hasOwnProperty(kid)) { | |
| innerKills[kid](); | |
| } | |
| } | |
| } | |
| } | |
| kills = null; | |
| var newKills = kill(error, root, cb); | |
| return function (killError) { | |
| return new Aff(ASYNC, function (killCb) { | |
| return function () { | |
| for (var kid in newKills) { | |
| if (newKills.hasOwnProperty(kid)) { | |
| newKills[kid](); | |
| } | |
| } | |
| return nonCanceler; | |
| }; | |
| }); | |
| }; | |
| } | |
| run(); | |
| return function (killError) { | |
| return new Aff(ASYNC, function (killCb) { | |
| return function () { | |
| return cancel(killError, killCb); | |
| }; | |
| }); | |
| }; | |
| } | |
| function sequential(util, supervisor, par) { | |
| return new Aff(ASYNC, function (cb) { | |
| return function () { | |
| return runPar(util, supervisor, par, cb); | |
| }; | |
| }); | |
| } | |
| Aff.EMPTY = EMPTY; | |
| Aff.Pure = AffCtr(PURE); | |
| Aff.Throw = AffCtr(THROW); | |
| Aff.Catch = AffCtr(CATCH); | |
| Aff.Sync = AffCtr(SYNC); | |
| Aff.Async = AffCtr(ASYNC); | |
| Aff.Bind = AffCtr(BIND); | |
| Aff.Bracket = AffCtr(BRACKET); | |
| Aff.Fork = AffCtr(FORK); | |
| Aff.Seq = AffCtr(SEQ); | |
| Aff.ParMap = AffCtr(MAP); | |
| Aff.ParApply = AffCtr(APPLY); | |
| Aff.ParAlt = AffCtr(ALT); | |
| Aff.Fiber = Fiber; | |
| Aff.Supervisor = Supervisor; | |
| Aff.Scheduler = Scheduler; | |
| Aff.nonCanceler = nonCanceler; | |
| return Aff; | |
| }(); | |
| exports._pure = Aff.Pure; | |
| exports._throwError = Aff.Throw; | |
| exports._catchError = function (aff) { | |
| return function (k) { | |
| return Aff.Catch(aff, k); | |
| }; | |
| }; | |
| exports._map = function (f) { | |
| return function (aff) { | |
| if (aff.tag === Aff.Pure.tag) { | |
| return Aff.Pure(f(aff._1)); | |
| } else { | |
| return Aff.Bind(aff, function (value) { | |
| return Aff.Pure(f(value)); | |
| }); | |
| } | |
| }; | |
| }; | |
| exports._bind = function (aff) { | |
| return function (k) { | |
| return Aff.Bind(aff, k); | |
| }; | |
| }; | |
| exports._fork = function (immediate) { | |
| return function (aff) { | |
| return Aff.Fork(immediate, aff); | |
| }; | |
| }; | |
| exports._liftEffect = Aff.Sync; | |
| exports._parAffMap = function (f) { | |
| return function (aff) { | |
| return Aff.ParMap(f, aff); | |
| }; | |
| }; | |
| exports._parAffApply = function (aff1) { | |
| return function (aff2) { | |
| return Aff.ParApply(aff1, aff2); | |
| }; | |
| }; | |
| exports._parAffAlt = function (aff1) { | |
| return function (aff2) { | |
| return Aff.ParAlt(aff1, aff2); | |
| }; | |
| }; | |
| exports.makeAff = Aff.Async; | |
| exports.generalBracket = function (acquire) { | |
| return function (options) { | |
| return function (k) { | |
| return Aff.Bracket(acquire, options, k); | |
| }; | |
| }; | |
| }; | |
| exports._makeFiber = function (util, aff) { | |
| return function () { | |
| return Aff.Fiber(util, null, aff); | |
| }; | |
| }; | |
| exports._makeSupervisedFiber = function (util, aff) { | |
| return function () { | |
| var supervisor = Aff.Supervisor(util); | |
| return { | |
| fiber: Aff.Fiber(util, supervisor, aff), | |
| supervisor: supervisor | |
| }; | |
| }; | |
| }; | |
| exports._killAll = function (error, supervisor, cb) { | |
| return supervisor.killAll(error, cb); | |
| }; | |
| exports._delay = function () { | |
| function setDelay(n, k) { | |
| if (n === 0 && typeof setImmediate !== "undefined") { | |
| return setImmediate(k); | |
| } else { | |
| return setTimeout(k, n); | |
| } | |
| } | |
| function clearDelay(n, t) { | |
| if (n === 0 && typeof clearImmediate !== "undefined") { | |
| return clearImmediate(t); | |
| } else { | |
| return clearTimeout(t); | |
| } | |
| } | |
| return function (right, ms) { | |
| return Aff.Async(function (cb) { | |
| return function () { | |
| var timer = setDelay(ms, cb(right())); | |
| return function () { | |
| return Aff.Sync(function () { | |
| return right(clearDelay(ms, timer)); | |
| }); | |
| }; | |
| }; | |
| }); | |
| }; | |
| }(); | |
| exports._sequential = Aff.Seq; |