snowl/content/strands.js

477 строки
17 KiB
JavaScript

/**
Strands - JavaScript Cooperative Threading and Coroutine support
Copyright (C) 2007 Xucia Incorporation
Author - Kris Zyp - kriszyp@xucia.com
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
* ***** END LICENSE BLOCK ***** */
// Suppress JS strict warnings by declaring all global strands variables.
var Future, sleep, strands, _S, strand, _frm, _gen, returns;
function temp() {
var standardPush = Array.prototype.push;
try {
var debugMode =document.location.search.indexOf("debug=true") > -1;
}
catch(e) { debugMode=true}
var push = function(obj,value) {
return standardPush.call(obj,value); // preserve default push behavoir
}
var suspendedFrame = null;
var currentThread = {};
var Suspend = { // returned value when a coroutine is suspended, and state of the top frame when suspended
toString: function() { return "SUSPENDED" }
}
/**
* This is wrap the given function in a try catch when debug mode is off. If an error is thrown, the error message will be displayed with an alert
*/
var tryCatch = function(func) {
return strands.errorWrapper(func)();
}
var specificNotify = {};
Future = function(func,args,thisObj,callback) {
this.topFrames = [];
var self = this;
this.fulfill = function(retValue) {
if (retValue == specificNotify) {
var targetFrames = retValue.targetFrames;
retValue = retValue.retValue;
}
else
self.value = retValue;
var frame;
while (frame = (targetFrames || self.topFrames).shift()) { // iterate through all the resulting threads/frames
// This is the beginning of a resume
currentThread = {};
// checkRestState();
// if (!frame._r)
// throw new Error("Future called without being in a result state");
frame.retValue = retValue;
if (frame.NRY)
frame.thread = currentThread;
while (frame.parentFrame) {
frame = frame.parentFrame; // now the bottom frame
frame.thread = currentThread;
}
if (frame.args) {
suspendedFrame = frame; // the .parents indicates it was a NotReadyYet function, so there is no suspended frames above it
tryCatch(function() {
frame.args.callee.call(frame.frameThis); // now resume
});
}
else {
//A thread was resumed with no call stack
suspendedFrame = null;
}
}
}
if (func) {
if (callback)
this.addListener(callback);
(function() {
var f = _frm(arguments);
var value = func.apply(thisObj || window,args||[]);
if (value===_S) return f.sus();
self.fulfill(value);
})();
}
}
Future.prototype = {
addListener : function(func) {
push(this.topFrames,func);
},
interrupt : function () {
this.fulfill(strands.INTERRUPTED);
},
isDone : function() {
return this.hasOwnProperty('value');
},
result : function(timeout) {
if (this.hasOwnProperty('value') || (suspendedFrame && suspendedFrame.retValue)) { // The future has been called, we can resume
var value = (suspendedFrame ? suspendedFrame.retValue : 0) || this.value;
suspendedFrame = null; // make sure we don't get any callers picking this up, we may not need this
if (value == strands.TIMEOUT || value== strands.INTERRUPTED)
throw value;
return value;// the future has already been fulfilled so we can just return the result
}
var topFrame = {}
push(this.topFrames,topFrame);
var self = this;
topFrame.args = [];
topFrame.args.callee=function() {return self.result()};
suspendedFrame = topFrame;
if (timeout) {
setTimeout(function() {
self.fulfill(specificNotify = {retValue:strands.TIMEOUT,targetFrames:[topFrame]});
},timeout);
}
return Suspend;
}
}
var CallFrame = function() {
}
var Construct = function() {};
var defaultScope = CallFrame.prototype = {
sus : function() { // this handles exceptions as well as suspensions
for (var i = 0; i < arguments.length-2;i++) // record all the vars and params
this[i] = arguments[i];
this.cp = arguments[arguments.length-2]; // record the code pointer
this.frameThis = arguments[arguments.length-1]; // record the code pointer
if (suspendedFrame == this)
same;
if (!suspendedFrame)
NoSuspendedFrame; // This can be caused by returning Suspend without actually being in a suspension, or if _S ends up in a variable
suspendedFrame.parentFrame = this;
this.childFrame = suspendedFrame; // it needs to be reexecuted
suspendedFrame = this;
if (this.thread == currentThread) // Assertion
SuspensionFrameShouldNotBeCurrentThread;
return Suspend;
},
exc : function(exception) {
if (this.ecp == null)
throw exception;
this.thr = true;
this.ex = exception;
return this.ecp;
},
clsr : function(func) {
if (this.scp)
this.inner = func;
return this.inner;
},
scp : 2,
keys : function(obj) {
var keys = [];
for(var n in obj)
push(keys,n);
return keys;
},
_new : function(value,args) { // create a new instance of an object or constructor
if (value === Suspend)
return value;
var frame = _frm(arguments);
if (!frame._cp) {
frame._cp=1;
frame.Const= value;
Construct.prototype = value.prototype;
if (value === String || value === Number) // these classes must not have a target this
return args.length?new value(args[0]):new value;
if (value !== Date) { // date does not have to directly instantiated, but it does need an undefined scope, it also needs to be able to handle variable arguements
frame.newThis = new Construct();
}
}
if ((value = frame.Const.apply(frame.newThis,args?args:[])) == Suspend) return frame.sus();
if (value instanceof Object) return value; // if the new returns a different value than "this"
return frame.newThis;
}
}
/**
* Suspend execution for the given amount time
* @param time milliseconds to pause
*/
sleep = function(time) {
var frame = _frm(arguments);
if (!frame._cp) { // if it is zero
frame._cp = 1;
setTimeout((frame.future = new Future).fulfill,time);
frame.future.result();
return frame.sus();
}
frame.future.result(); // this is the result operation to resume
}
strands = {
defaultScope : defaultScope,
loadScope : function(){},
/**
* This function that will be called to return a function that should execute the provided function in order to initialize the stack
* Any time a new stack is created, the returned function will be used. This provides a mechanism to wrap all processing
* within a try catch.
*/
errorWrapper : function(func) {
var newFunc = function() {
if (debugMode)
return func.apply(this,arguments);
try {
return func.apply(this,arguments);
}
catch (e) {
Components.utils.reportError(e.message || e);
}
}
newFunc.noTryCatch = func;
return newFunc;
},
TIMEOUT : {toString:function(){return "Thread timeout"}},
INTERRUPTED : {toString:function(){return "Thread interrupted"}},
sleep : sleep,
/**
* This is a constant that is returned from functions to indicate that the code execution is suspending
*/
Suspension : _S = Suspend,
/**
* Makes an XHR (Ajax) request using the given url, method, and post data (for POST requests) and
returns contents that came from the server.
* @param {Object} url
* @param {Object} method
* @param {Object} postData
*/
request : function(url, method, postData) {
var frame = _frm(arguments);
if (!frame._cp) {
var getXMLHttpRequest = function () {
if (parent.XMLHttpRequest)
return new parent.XMLHttpRequest();
else if (window.ActiveXObject)
return new ActiveXObject("Microsoft.XMLHTTP");
}
var xhr = getXMLHttpRequest();
frame.future = new Future();
var ajaxDataReader = function () {
if (xhr.readyState == 4) {
// only if "OK"
var loaded;
try {
var status = xhr.status;
loaded = xhr.responseText.length > 0;//firefox can throw an exception right here
} catch(e) {}
if (loaded)
frame.future.fulfill(xhr.responseText);
else
frame.future.interrupt();
xhr = null; // This is to correct for IE memory leak
}
}
frame._cp = 1;
xhr.open(method || "POST", url, true);
xhr.onreadystatechange = ajaxDataReader;
xhr.send(postData);
}
var result = frame.future.result();
if (result == _S) frame.sus();
return result;
},
compilerUrl : 'js/compiler.js',
js17 : (function() { try {return Iterator}catch(e){}})(), // something better would be good?
/**
* Loads the given script. It will compile the script if necessary for coroutine support.
* You can set strands.precompiled = true if the scripts have
* been precompiled on the server. It was HIGHLY recommended
* that you compiled the scripts on the server to reduce the overhead
* on the client.
* @param {Object} url
*/
loadScript : function(url) {
var frame = _frm(arguments);
switch(frame._cp) {
case undefined:frame.future = new Future();
frame.url = url;
frame._cp = 1;
case 1: case 2:case 3:url = frame.url;
function load() {
if (strands.js17) {
if (frame._cp != 2) {
var scriptTag = document.createElement("script"); // must use the document that supports events, not his one
scriptTag.onload = frame.future.fulfill;
scriptTag.type= 'application/javascript;version=1.7';
scriptTag.setAttribute("src", url);
document.body.appendChild(scriptTag); // This is supposed to be asynchronous, but sometimes it will get its request before continuing
}
frame._cp = 2;
return frame.future.result();
}
else {
var source = strands.request(url,'GET');
if (source == Suspend) return Suspend; //we can only do this because it is an inner function
eval(source);
}
}
var compOptions,shorten;
if (url.match(/\.jss$/)) {
compOptions = {};
shorten = 1;
}
else if (url.match(/\.js17$/)) {
if (strands.js17)
shorten = 0;
else {
compOptions = {source:'js17'};
shorten = 2;
}
}
if (strands.precompiled)
url = url.substring(0,url.length-shorten);
else
{
if (compOptions) {
if (!strands.compiler || frame._cp == 2) {
frame._cp = 2;
var compiler;
if ((compiler = strands.request(strands.compilerUrl,'GET'))==Suspend)
return frame.sus();
eval(compiler);
frame._cp = 3;
strands.compiler = new StrandsCompiler(compOptions);
}
var source;
if ((source = strands.request(url,'GET'))==Suspend)
return frame.sus();
source = strands.compiler.compile(source);
eval(source);
return;
}
}
if (load() == Suspend) return frame.sus();
}
}
}
var coreturn;
strand = strands.js17 ? eval("(function(func) {\r\n"+
"return function() {"+
" if (suspendedFrame && suspendedFrame.thread == currentThread && suspendedFrame.args) {"+
" var frame = suspendedFrame;"+
" suspendedFrame = frame.childFrame;"+ // if this is undefined it means we are at the top of the resume stack
" delete frame.thread;"+
" delete frame.childFrame;"+// for assertions
" var result = suspendedFrame.args.callee.apply(this,suspendedFrame.args);"+ // resume the stack
" if (result == Suspend) return CallFrame.prototype.sus.call(frame,this);"+ // if we have to suspend again right away
" }"+
" else {"+ //fresh entry
" var frame = func.apply(this,arguments);"+
" if (!(frame != null && typeof(frame) == 'object' &&"+
" typeof(frame.next) == 'function' && typeof(frame.send) == 'function'))"+
" return frame;"+
" frame.args = arguments;"+
" var result;"+
" }"+
" try{"+
" while(1){"+
" coreturn = undefined;"+ // global return value
" result = frame.send(result);"+
" if (result == Suspend) return CallFrame.prototype.sus.call(frame,this);"+
" } "+
" } catch(e if e == StopIteration) {"+
" return coreturn;"+
" }"+
" }"+
"})") : function() {throw new Error("can not call strand except as a function wrapper");}; /**
* This creates a new Strands call frame. It becomes the scope for the function maintains variables and parameters across suspensions and resumes. It should only be used in compiled code
*/
_frm = function(args) {
if (args.caller) args.caller=0; // this fixes a big memory leak;
if (suspendedFrame) {
// if it is loading we start a new frame
if (suspendedFrame.thread == currentThread && suspendedFrame.args && !suspendedFrame.NRY) {// this means we are resuming
var frame = suspendedFrame;
//TODO: Implement this:
if (frame.args.callee != args.callee && frame.args.callee.toString() != args.callee.toString()) {// this means the function that is being called has changed, the function has been replaced, so we need to call the orginal one
// if (this!=frame.frameThis) {
suspendedFrame = null;
StackAlterationError("The function has changed, the means for handling this right now is not complete");
// }
/* var retValue = frame.args.callee.call(frame.frameThis);
if (retValue == _S){
// this is the tricky part, we need to call the next function and have it be guaranteed to return a _S
}
else {// we need to come up with a way to ensure that we have the right rv#
frame["rv" + frame.cp++] = retValue; //make sure we increment the cp so we are the next part
}
return frame;*/
}
delete frame.thread;
suspendedFrame = frame.childFrame; // if this is undefined it means we are at the top of the resume stack
delete frame.childFrame; // for assertions
if (suspendedFrame && suspendedFrame._r) {//Assertion stuff
if (! suspendedFrame.parentFrame)
SuspendedFrameHasNoParentFrame;
else
delete suspendedFrame.parentFrame;
}
for (var i =0;i<frame.args.length;i++)
args[i] = frame.args[i];
frame.scp=0; // start in the suspend segment
return frame;
}
else { // this case means that there is a suspendedFrame variable left over from a previous resume/suspension
// It should only be a left over from a suspension, and it should be the bottom frame. A resume should null out suspendedFrame
suspendedFrame = null; // a suspension took place somewhere else, so now we can get rid of this
}
}
frame = new CallFrame;
frame.scp = 1; // Why is this needed for opera to work? somewhere the prototype _cp is getting set, need to figure out why
frame.args = args;
return frame;
}
function Generator(frame){
this.frame = frame;
};
Generator.prototype = {
next : function() {
return this.send();
},
send : function(value) {
suspendedFrame = this.frame;
suspendedFrame.thread = currentThread;
_receive = value;
return this.frame.args.callee.call(this.frame.frameThis);
}
}
_gen = function(frame) {
return new Generator(frame);
}
returns = function(value) {
coreturn = value;
throw StopIteration;
}
}
temp();
if (!setTimeout) {
/**
* Set a timer, simulating the API for the window.setTimeout call.
* This only simulates the API for the version of the call that accepts
* a function as its first argument and no additional parameters,
* and it doesn't return the timeout ID.
*
* @param func {Function}
* the function to call after the delay
* @param delay {Number}
* the number of milliseconds to wait
*/
var setTimeout = function(func, delay) {
let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
let callback = {
_func: func,
notify: function(timer) {
// We call the function without "this." in front of it so that "this"
// inside the function is the global object, just as it would be
// with window.setTimeout.
let func = this._func;
func();
}
}
timer.initWithCallback(callback, delay, Ci.nsITimer.TYPE_ONE_SHOT);
}
}