1
0
mirror of https://github.com/pkimpel/retro-b5500.git synced 2026-02-28 01:35:46 +00:00
Files
pkimpel.retro-b5500/webUI/B5500SetCallback.js
paul.kimpel@digm.com dd227d8cc7 Tag release 0.13.
2013-11-16 12:37:23 +00:00

167 lines
7.0 KiB
JavaScript

/***********************************************************************
* retro-b5500/webUI B5500SetCallback.js
************************************************************************
* Copyright (c) 2013, Nigel Williams and Paul Kimpel.
* Licensed under the MIT License, see
* http://www.opensource.org/licenses/mit-license.php
************************************************************************
* B5500 universal function call-back module.
*
* Implements a combination setTimeout() and setImmediate() facility for the
* B5500 emulator web-based user interface. setCallback() is used the same way
* that setTimeout() is used, except that for low values of the timeout parameter,
* it merely yields control to any other pending events and timers before calling
* the call-back function.
*
* This facility is needed because modern browsers implement a minimum delay
* when calling setTimeout(). HTML5 specs require 4ms, but on Microsoft Windows
* systems (at least through Win7), the minimum precision of setTimeout() is
* about 15ms, unless you are running Google Chrome. This module will use
* setTimeout() if the requested delay time is above a certain threshold, and
* a setImmediate()-like mechanism (based on window.postMessage) if the requested
* delay is above that threshold.
*
* Even though this mechanism may execute the call-back function sooner than the
* requested delay specifies, the timing and throttling mechanisms in the
* emulator will correct for that in subsequent delay cycles. We are going for
* good average behavior, and quick call-backs are better than consistently
* too-long callbacks in this environment, so that I/Os can be initiated and
* their finish detected in finer-grained time increments.
*
* The SetCallback mechanism defines two functions, which become members of the
* global (window) object:
*
* cookie = setCallback(fcn, context, delay, args...)
*
* Requests that the function "fcn" be called after "delay" milliseconds.
* The function will be called as a method of "context", passing the
* list of arguments "args...". The call-back "fcn" may be called
* earlier or later than the specified delay. setCallBack returns a
* numeric token identifying the call-back event, which can be used
* with clearCallback(). Note that passing a string in lieu of a function
* object is not permitted.
*
* clearCallBack(cookie)
*
* Cancels a pending call-back event, if in fact it is still pending.
* The "cookie" parameter is a value returned from setCallback().
*
* This implementation has been inspired by Domenic Denicola's shim for the
* setImmediate() API at https://github.com/NobleJS/setImmediate, and
* David Baron's setZeroTimeout() implemenmentation described in his blog
* at http://dbaron.org/log/20100309-faster-timeouts.
*
* Stole a little of their code, too.
*
************************************************************************
* 2013-08-04 P.Kimpel
* Original version, cloned from B5500DiskUnit.js.
***********************************************************************/
"use strict";
(function (global) {
/* Define a closure for the setCallback() mechanism */
var minTimeout = 4; // minimum setTimeout() threshold, milliseconds
var nextCookieNr = 1; // next setCallback cookie return value
var pendingCallbacks = {}; // hash of pending callbacks, indexed by cookie as a string
var secretPrefix = "com.google.code.p.retro-b5500.webUI." + new Date().getTime().toString(16);
/**************************************/
function activateCallback(cookieName) {
/* Activates a callback after its delay period has expired */
var thisCallback;
if (cookieName in pendingCallbacks) {
thisCallback = pendingCallbacks[cookieName];
delete pendingCallbacks[cookieName];
try {
thisCallback.fcn.apply(thisCallback.context, thisCallback.args);
} catch (err) {
console.log("B5500SetCallback.activateCallback: " + err);
}
}
}
/**************************************/
function clearCallback(cookie) {
/* Disables a pending callback, if it still exists and is still pending */
var cookieName = cookie.toString();
var thisCallback;
if (cookieName in pendingCallbacks) {
thisCallback = pendingCallbacks[cookieName];
delete pendingCallbacks[cookieName];
if (thisCallback.cancelToken) {
if (thisCallback.type == 2) {
global.clearTimeout(thisCallback.cancelToken);
}
}
}
}
/**************************************/
function setCallback(fcn, context, callbackDelay, args) {
/* Sets up and schedules a callback for function "fcn", called with context
"context", after a delay of "delay" ms. Any "args" will be passed to "fcn".
If the delay is less than "minTimeout", a setImmediate-like mechanism based on
window.postsMessage() will be used; otherwise the environment's standard
setTimeout mechanism will be used */
var delay = callbackDelay || 0;
var cookie = nextCookieNr++;
var cookieName = cookie.toString();
var thisCallback = {
args: null,
fcn: fcn,
context: context || this,
};
pendingCallbacks[cookieName] = thisCallback;
if (arguments.length > 3) {
thisCallback.args = Array.slice(arguments, 3);
}
if (delay < minTimeout) {
thisCallback.type = 1;
global.postMessage(secretPrefix + cookieName, "*");
} else {
thisCallback.type = 2;
thisCallback.cancelToken = global.setTimeout(activateCallback, delay, cookieName);
}
return cookie;
}
/**************************************/
function onMessage(ev) {
/* Handler for the global.onmessage event. Activates the callback */
var cookieName;
var payload;
if (ev.source === global) {
payload = ev.data.toString();
if (payload.substring(0, secretPrefix.length) === secretPrefix) {
cookieName = payload.substring(secretPrefix.length);
activateCallback(cookieName);
}
}
}
/********** Outer block of anonymous closure **********/
if (!global.setCallback && global.postMessage && !global.importScripts) {
// Attach to the prototype of global, if possible, otherwise to global itself
var attachee = global;
/*****
if (typeof Object.getPrototypeOf === "function") {
if ("setTimeout" in Object.getPrototypeOf(global)) {
attachee = Object.getPrototypeOf(global);
}
}
*****/
global.addEventListener("message", onMessage, false);
attachee.setCallback = setCallback;
attachee.clearCallback = clearCallback;
}
}(typeof global === "object" && global ? global : this));