var WriteQueue = require("./write-queue"); var Util = require("./common-util"); /* This module provides implements a FIFO scheduler which assumes the existence of three types of async tasks: 1. ordered tasks which must be executed sequentially 2. unordered tasks which can be executed in parallel 3. blocking tasks which must block the execution of all other tasks The scheduler assumes there will be many resources identified by strings, and that the constraints described above will only apply in the context of identical string ids. Many blocking tasks may be executed in parallel so long as they concern resources identified by different ids. USAGE: const schedule = require("./schedule")(); // schedule two sequential tasks using the resource 'pewpew' schedule.ordered('pewpew', function (next) { appendToFile('beep\n', next); }); schedule.ordered('pewpew', function (next) { appendToFile('boop\n', next); }); // schedule a task that can happen whenever schedule.unordered('pewpew', function (next) { displayFileSize(next); }); // schedule a blocking task which will wait // until the all unordered tasks have completed before commencing schedule.blocking('pewpew', function (next) { deleteFile(next); }); // this will be queued for after the blocking task schedule.ordered('pewpew', function (next) { appendFile('boom', next); }); */ // return a uid which is not already in a map var unusedUid = function (set) { var uid = Util.uid(); if (set[uid]) { return unusedUid(); } return uid; }; // return an existing session, creating one if it does not already exist var lookup = function (map, id) { return (map[id] = map[id] || { //blocking: [], active: {}, blocked: {}, }); }; var isEmpty = function (map) { for (var key in map) { if (map.hasOwnProperty(key)) { return false; } } return true; }; // XXX enforce asynchrony everywhere module.exports = function () { // every scheduler instance has its own queue var queue = WriteQueue(); // ordered tasks don't require any extra logic var Ordered = function (id, task) { queue(id, task); }; // unordered and blocking tasks need a little extra state var map = {}; // regular garbage collection keeps memory consumption low var collectGarbage = function (id) { // avoid using 'lookup' since it creates a session implicitly var local = map[id]; // bail out if no session if (!local) { return; } // bail out if there are blocking or active tasks if (local.lock) { return; } if (!isEmpty(local.active)) { return; } // if there are no pending actions then delete the session delete map[id]; }; // unordered tasks run immediately if there are no blocking tasks scheduled // or immediately after blocking tasks finish var runImmediately = function (local, task) { // set a flag in the map of active unordered tasks // to prevent blocking tasks from running until you finish var uid = unusedUid(local.active); local.active[uid] = true; task(function () { // remove the flag you set to indicate that your task completed delete local.active[uid]; // don't do anything if other unordered tasks are still running if (!isEmpty(local.active)) { return; } // bail out if there are no blocking tasks scheduled or ready if (typeof(local.waiting) !== 'function') { return void collectGarbage(); } local.waiting(); }); }; var runOnceUnblocked = function (local, task) { var uid = unusedUid(local.blocked); local.blocked[uid] = function () { runImmediately(local, task); }; }; // 'unordered' tasks are scheduled to run in after the most recently received blocking task // or immediately and in parallel if there are no blocking tasks scheduled. var Unordered = function (id, task) { var local = lookup(map, id); if (local.lock) { return runOnceUnblocked(local, task); } runImmediately(local, task); }; var runBlocked = function (local) { for (var task in local.blocked) { runImmediately(local, local.blocked[task]); } }; // 'blocking' tasks must be run alone. // They are queued alongside ordered tasks, // and wait until any running 'unordered' tasks complete before commencing. var Blocking = function (id, task) { var local = lookup(map, id); queue(id, function (next) { // start right away if there are no running unordered tasks if (isEmpty(local.active)) { local.lock = true; return void task(function () { delete local.lock; runBlocked(local); next(); }); } // otherwise wait until the running tasks have completed local.waiting = function () { local.lock = true; task(function () { delete local.lock; delete local.waiting; runBlocked(local); next(); }); }; }); }; return { ordered: Ordered, unordered: Unordered, blocking: Blocking, }; };