Source: org/terraswarm/accessor/accessors/web/hosts/common/modules/syncAtom.js

//
// Copyright (c) 2017 The Regents of the University of California.
// All rights reserved.
//
// Permission is hereby granted, without written agreement and without
// license or royalty fees, to use, copy, modify, and distribute this
// software and its documentation for any purpose, provided that the above
// copyright notice and the following two paragraphs appear in all copies
// of this software.
//
// IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY
// FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
// ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF
// THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF
// SUCH DAMAGE.
//
// THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,
// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE
// PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF
// CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES,
// ENHANCEMENTS, OR MODIFICATIONS.
//
/**
 * Timed events are delayed callbacks, scheduled using setTimeout or setInterval.
 * This module offers variants of setTimeout, clearTimeout, setInterval, 
 * clearInterval, and reset that enforce a synchronous semantics for timed events. 
 * Synchronization between timed events entails the following two properties: 
 * (1) A timed event that is released from within another timed event that occurred
 * at T=tau, will occur precisely at T=tau+timeout.
 * (2) Any two events t1, t2, with intervals i1, i2, that are released from within 
 * the same function will occur precisely at T=tau+i2 and T=tau+i2, respectively. 
 * The value of tau will approximate the wallclock time of the first-released event.
 * At common multiples of i1 and i2, t1 and t2 will execute simultenously i.e., 
 * in order of their release, and with no logical time elapsing until both have 
 * executed.
 * In the rare case the same function happens to execute multiple times consequtively,
 * without some timed event occurring in between, all releases in that series will
 * be scheduled with respect to the same tau. Note that this situation can only 
 * arise trough rapidly succeeding asynchronous callbacks. When precise timing
 * is desired, this scenario must be avoided.
 * 
 * WARNING: this implementation will only work correctly if the JavaScript execution
 * environment provides a working implementation of Function.caller. Also see:
 * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/caller
 * Function.caller is supported by: Chrome, Firefox Gecko 1.0, IE 8, Opera, Safari,
 * which includes the mobile version of these respective browsers. Function.caller
 * will not work in strict mode.
 * 
 * @module @accessors-hosts/common/sync-atom
 * @author Marten Lohstroh
 * @version $Id$
 */

/**
 * Construct a new calendar queue.
 */
function CalendarQ() {
    this.q = [];
}

/**
 * Pop an element from the queue. FIXME: change signature to function(event, time)
 */
CalendarQ.prototype.enqueue = function(e) {
    for (var i = 0; i < this.q.length && e.time >= this.q[i].time; i++);
    this.q.splice(i, 0, e);
}

/**
 * Push an element onto the queue.
 */
CalendarQ.prototype.dequeue = function() {
    return this.q.shift();
}

/**
 * See what is at the start of the queue.
 */
CalendarQ.prototype.peek = function() {
    return this.q[0];
}

/**
 * Report the size of the queue.
 */
CalendarQ.prototype.size = function() {
    return this.q.length;
}

// Events.
var callbacks = {};

// Time schedule.
var calendar = new CalendarQ();

// The current time.
var time = Date.now();

// Start of simulation time.
var startTime = time;

// The next wake up time.
var alarm = Infinity;

// Handle for the alarm.
var handle;

// Counter for callback IDs.
var counter = 0;

// The last caller that scheduled a timed event.
var lastCaller = null;

// Synchronization point for releases from the same caller.
var release; 

/**
 * Construct a new Callback object.
 * @param fun
 * @param offset
 * @param period
 */
function Callback(fun, offset, period) {
    this.fun = fun;
    this.offset = offset;
    this.period = period;
}

/**
 * Construct a new Event object.
 * @param id
 * @param time
 */
function Event(id, time) {
    this.id = id;
    this.time = time;
}

/**
 * Wake up and process enabled events.
 */
var tick = function() {
    console.log("Tick at T=" + time);

    // Process enabled events.
    while (calendar.peek() != undefined && Date.now() >= calendar.peek().time) {
        var e = calendar.dequeue();
        console.log("Event: " + JSON.stringify(e));
        // Update the global time.
        time = e.time;
        if (e.id in callbacks) {
            var cb = callbacks[e.id];
            console.log("Callback: " + JSON.stringify(cb));
            console.log(e.time - startTime);
            cb.fun.call();
            if (cb.period >= 0) {
                calendar.enqueue(e.id, e.time + events[e.id].period);
            }
        }
    }
    
    // Set a timer to wake up for the next tick.
    if (calendar.peek() != undefined) {
        handle = setTimeout(tick, Math.max(calendar.peek().time - Date.now(), 0)); 
    }
    
    // Clear the last caller.
    lastCaller = null;   
};

/** 
 * Unschedule a periodic callback.
 * @param cbId Handle returned by setInterval().
 */
function clearIntervalSync(cbId){
    if (callbacks[cbId].period >= 0) {
        delete callbacks[cbId];
    }
}

/**
 * Unschedule a delayed callback.
 * @param cbId Handle returned by setTimeout().
 */
function clearTimeoutSync(cbId){
    if (callbacks[cbId].period < 0) {
        delete callbacks[cbId];
    }
}

/**
 * Schedule a new timed event. This entails adding to the callbacks dictonary,
 * as well as adding it to the calendar queue, and if needed, (re)setting a 
 * timer to wake up in time to process enabled events.
 * @param callback The callback to be scheduled.
 * @param timeout The interval with respect to the current time.
 * @param caller The function from which this event was released.
 * @param repeat Whether this timed event is periodic or not.
 * @return the id of the event.
 */
function schedule(callback, timeout, caller, repeat) {
    var id = counter++;
    var offset;

    // Use logical time if the caller was a sync function.
    // Use wallclock time otherwise.
    if (caller.includes("setInterval") || caller.includes("setTimeout")) {
        console.log("Synchronous release at T=" + time);
        offset = time;
    } else {
        console.log("Asynchronous release at T=" + time);
        // The same caller released a timed event, and no other timed events have
        // occurred since (lastCaller would have been cleared). 
        // These releases ought to have the _same_ offset.
        if (caller != lastCaller) {
            release = Date.now();
            lastCaller = caller;
        }
        offset = release;
    }

    // FIXME: make this function idempotent, don't schedule duplicate events.

    if (repeat) {
        callbacks[id] = new Callback(callback, offset, timeout);
    } else {
        callbacks[id] = new Callback(callback, offset, -1);
    }

    var newTime = offset + timeout;
    calendar.enqueue(new Event(id, newTime));
    
    // Reset the alarm, if necessary.
    if (newTime < alarm) {
        clearTimeout(handle);
        alarm = newTime;
        handle = setTimeout(tick, Math.max(newTime - Date.now(), 0))
    }
    return id;
}

/**
 * Schedule a periodic timed event.
 * @param callback The function to be executed.
 * @param timeout The interval with respect to the (re)current time.
 * @return the unique id of the setInterval call.
 */
function setIntervalSync(callback, timeout) {
    if (timeout >= 0) {
        return schedule(callback, timeout, setIntervalSync.caller.toString(), true);
    }
}

/**
 * Schedule a timed event.  
 * @param callback The function to be executed.
 * @param timeout The interval with respect to the (re)current time.
 * @return the unique id of setTimeout call
 */
function setTimeoutSync(callback, timeout) {
    if (timeout >= 0) {
        return schedule(callback, timeout, setTimeoutSync.caller.toString(), false);
    }
}

/** 
 * Clear all (period) timed events. 
 */
function reset() {
    // Clear timer for the next tick.
    clearTimeout(handle);
    
    // Empty calendar queue, reset all kept state.
    calendar = new CalendarQ();

    callbacks = {};
    alarm = Infinity;
    time = Date.now();
    
}

///////////////////////////////////////////////////////////////////
//// Exports

exports.setTimeoutSync = setTimeoutSync;
exports.clearTimeoutSync = clearTimeoutSync;
exports.setIntervalSync = setIntervalSync;
exports.clearIntervalSync = clearIntervalSync;
exports.reset = reset;

// Some test code...

// function g() {
//     console.log("3@4000.")
//     //console.log("3@3200.")
// }

// function f() {
//     console.log("1@1200.")
//     //setTimeoutSync(g, 2000);
// }

// function h() {
//     console.log("3@2000.")
//     setTimeoutSync(g, 2000);
// }

// setTimeoutSync(f, 1200);
// setTimeoutSync(h, 2000);