TouchDevelop/rt/rt.ts

3115 строки
111 KiB
TypeScript

///<reference path='refs.ts'/>
module TDev
{
export interface CoverageData {
compilerversion: string;
astnodes: string[];
}
export interface IStackFrame
{
previous:IStackFrame;
rt:Runtime;
d:any; // data
libs:any; // libraries
returnAddr:IContinuationFunction;
entryAddr:IContinuationFunction;
isLibProxy:boolean;
stackDepth:number;
name:string;
pc:string;
// this is used in the asyncStack
continueAt?:IContinuationFunction;
currentHandler?:RT.EventBinding;
serverRequest?: RT.ServerRequest;
result?:any;
results?:any[];
pauseValue?:any;
rendermode?:any;
errorHandler?:(err:any,th:IStackFrame)=>void;
isDetached?:boolean;
isAsync?:boolean;
}
export interface IContinuationFunction
{
(s:IStackFrame): IContinuationFunction;
}
// Cloud runner interface
export interface ExecutionRequest
{
script:string;
actionName:string;
state:any;
}
export interface ExecutionResult
{
crashReport?:BugReport;
wallMessages:string[];
state:any;
}
export class StackFrameBase
implements IStackFrame
{
public d:any;
public libs:any;
public returnAddr:IContinuationFunction = null;
public entryAddr:IContinuationFunction = null;
public previous:IStackFrame = null;
public isLibProxy:boolean = false;
public stackDepth:number;
public name:string;
public pc:string;
public serverRequest:RT.ServerRequest;
public errorHandler:(err:any,th:IStackFrame)=>void;
public isAsync:boolean;
public isDetached:boolean;
constructor(public rt:Runtime) {
}
}
export class StackBottom
extends StackFrameBase
{
public needsPicker = false;
constructor(rt:Runtime) {
super(rt)
this.pc = ""
this.stackDepth = 0
this.name = "<entry-point>";
this.d = this.rt.datas["this"];
this.libs = this.rt.compiled.libs;
}
}
export class LibProxy
extends StackFrameBase
{
public isLibProxy = true;
constructor(libs:any, previous:IStackFrame, public libRefName:string, public libActionName:string, public invoke:()=>any) {
super(previous.rt)
this.pc = "";
this.libs = libs;
this.previous = previous;
this.d = this.rt.datas[this.libRefName];
this.name = this.libActionName;
this.stackDepth = previous.stackDepth;
}
}
export class ResumeCtx
{
private shownProgress:boolean;
private used = false;
public rt: Runtime;
public isBlocking = false;
public versionNumber:number;
constructor(public stackframe:IStackFrame)
{
this.rt = stackframe.rt;
stackframe.rendermode = stackframe.rt.rendermode;
}
public isTaskCtx() { return false }
private clearProgress()
{
if (this.shownProgress) {
this.shownProgress = false
HTML.showProgressNotification("")
}
}
public resumeVal(v:any)
{
// valid only once
if (this.used) return;
this.used = true;
this.clearProgress()
this.resumeCore(v)
}
public resumeCore(v:any)
{
this.rt._resumeVal(v, this)
}
public resume() { return this.resumeVal(undefined); }
public progress(msg:string)
{
this.shownProgress = true
HTML.showProgressNotification(msg, false)
}
}
export enum RtState
{
Stopped,
Running,
Paused,
AtAwait,
BreakpointHit,
}
export interface RuntimeHost
{
getWall(): HTMLElement;
init(rt: Runtime): void;
notifyStopAsync(): Promise;
notifyHideWall(): void;
notifyPagePush(): void;
notifyPagePop(p: WallPage): void;
notifyPageButtonUpdate(): void;
notifyRunState(): void;
// debugger notifiers
notifyBreakpointHit(bp: string): void;
notifyBreakpointContinue(): void;
initApiKeysAsync(): Promise;
agreeTermsOfUseAsync(): Promise;
liveMode(): boolean;
dontWaitForEvents(): boolean;
exceptionHandler(exn: any): void;
applyPageAttributes(wp: WallPage): void;
isFullScreen(): boolean;
setFullScreenElement(elmt: HTMLElement): void;
setTransform3d(trans: string, origin: string, perspective: string): void;
attachProfilingInfo(profile: any): void;
attachCoverageInfo(coverage: any, showCoverage: boolean): void;
isHeadless(): boolean;
notifyTutorial(cmd: string);
askSourceAccessAsync(source: string, description: string, secondchance: boolean, critical?:boolean): Promise; // of boolean
toScreenshotCanvas(): HTMLCanvasElement;
wallOrientation: number;
wallHeight: number;
wallWidth: number;
fullWallWidth(): number;
fullWallHeight(): number;
userWallHeight(): number;
astOfAsync(id: string): Promise;
pickScriptAsync(mode: string, message: string): Promise;
saveAstAsync(id: string, ast: any): Promise;
deploymentSettingsAsync(id: string): Promise;
packageScriptAsync(id : string, options: any) : Promise; // json object
currentGuid: string;
keyboard: TDev.RT.RuntimeKeyboard;
updateCloudState(hasCloudState: boolean, type: string, status: string);
isServer?: boolean;
localProxyAsync?: (path: string, data: any) => Promise; // of any
}
export interface IPageButton
{
icon():string;
getElement():HTMLElement;
// ...
}
export module RuntimeSettings {
export var readSetting = (key : string): any =>
{
return window.localStorage[key];
}
export var storeSetting = (key : string, value : any) =>
{
window.localStorage[key] = value;
}
export function location() : boolean {
return !readSetting("rtnolocation");
}
export function setLocation(value : boolean) {
storeSetting("rtnolocation", value ? value : undefined);
}
export function sounds() : boolean {
return !readSetting("rtsnosounds");
}
export function setSounds(value: boolean) : void {
storeSetting("rtsnosounds", value ? value : undefined);
if (!value)
TDev.RT.Player.pause();
}
export var askSourceAccess = true;
}
export interface TutorialState
{
validated?:boolean;
}
export class Runtime
{
// shell/package.ts depends on the exact format of the next line
static shellVersion = 24;
// this is not to be set from the editor - only in the exported app
static initialUrl: string;
public host: RuntimeHost;
private handlingException = false;
public current: IStackFrame;
public errorPC: string;
private returnedFrom: IStackFrame;
public validatorAction: string;
public validatorActionFlags: string;
public datas: any = {};
public liveMode() { return this.host.liveMode(); }
public testMode = false;
private getWall() { return this.host.getWall(); }
public compiled: CompiledScript;
public devMode = true;
public eventQ: EventQueue = null;
private recordTypesRegistered = false;
private resumePointOverride = null;
private pageStack: WallPage[]; // Instantiated at #initFrom(CompiledScript)
public versionNumber = 1;
public pluginSlotId: string;
private restartQueued = false;
public tutorialState: TutorialState = null;
public editorObj: RT.Editor;
public sessions: Revisions.Sessions;
public authValidator: RT.StringConverter<string>;
public runtimeKind() {
return this.devMode ? "editor" : "website"
}
public requiresAuth(): boolean {
return this.compiled.hasCloudData && this.sessions.getCurrentSession().requiresAuth;
}
public getUserId() {
if (this.sessions.isNodeClient()) {
return (<Revisions.NodeSession>this.sessions.getCurrentSession()).clientUserId;
}
return Cloud.getUserId()
}
// state for various singletons
public webState: RT.Web.State = <any>{};
private state: RtState = RtState.Stopped;
// when an event is executing, no other event can start
private eventExecuting = false;
// used to prevent recursive invocations of mainLoop
private mainLoopRunning = false;
// after the user hits the pause button: state==Stopped && resumeAllowed
private resumeAllowed = false;
public runningPluginOn = "";
public headlessPluginMode = false;
public tutorialObject = "";
public pageTransitionStyle = "slide";
public currentScriptId: string = undefined; // current script id, if any, is needed for the leaderboards
public currentAuthorId: string = undefined;
public baseScriptId: string = undefined;
public getScriptGuid(): string { return this.host.currentGuid; }
public getScriptName(): string { return this.compiled.scriptTitle; }
public getScriptColor(): string { return this.compiled.scriptColor; }
public disposables: RT.RTDisposableValue[] = [];
// Session related getters
//public getUserId() { return this.sessions.current_userid; }
//public getScriptAuthor() { return this.sessions.current_scriptauthor; }
//public getScript() { return this.sessions.current_script; }
//public checkSignedIn(specific_script: boolean) { return this.sessions.checkSignedIn(specific_script, this); }
constructor(sessions?: Revisions.Sessions) {
this.sessions = sessions || new Revisions.Sessions();
this.sessions.rt = this;
}
private asyncStack: IStackFrame[] = [];
private asyncTasks: any[] = [];
static theRuntime: Runtime; // there can be only one running!
static maxBoxLength: number = 1000;
// debugging stuff
public runMap: RunBitMap = new RunBitMap(); // runMap is essentially just a set of visited stuff for now
public beenHere(id: string) {
this.runMap.push(id);
}
public resetRunMap() { this.runMap.clear(); }
private breakpoints: Hashtable;
public initBreakpoints(h: Hashtable) { this.breakpoints = h; this.updateScriptBreakpoints(); }
private hitBreakpoint(id: string) {
this.debuggerLastState = this.state;
this.setState(RtState.BreakpointHit, "breakpoint");
this.host.notifyBreakpointHit(id);
}
public updateScriptBreakpoints() {
if (!this.compiled) return;
var binds = this.compiled.breakpointBindings;
Object.keys(binds).forEach(k => {
var bind = binds[k];
bind.setter(this.breakpoints.get(k));
});
}
private debuggerCC: IContinuationFunction;
private debuggerLastState: RtState = null;
public debuggerContinue() {
if (!this.debuggerStopped()) return;
if (this.debuggerLastState !== null) this.setState(this.debuggerLastState, "debugger last state")
if (this.debuggerCC) this.mainLoop(this.debuggerCC, "resume debugger");
}
public debuggerStopped(): boolean {
return this.state === RtState.BreakpointHit;
}
public debuggerQueryGlobalValue(stableName: string) {
Util.log("Runtime.debuggerQueryGlobalValue: " + stableName);
if (!this.compiled || !this.current) return;
return this.current.d[stableName];
}
public debuggerQueryLocalValue(actionId: string, name: string, stackFrame?: IStackFrame) {
Util.log("Runtime.debuggerQueryLocalValue: " + name);
if (!this.compiled || !this.current) return;
var actionBindings: { [name: string]: string; };
this.compiled.forEachLib(l => {
if (!actionBindings && l.localNamesBindings)
actionBindings = l.localNamesBindings[actionId];
})
if (!actionBindings) return;
name = actionBindings[name];
Util.log("Runtime.debuggerQueryLocalValue resolved to: " + name);
var frame = stackFrame ? stackFrame : this.current;
return frame && frame["$" + name];
}
public debuggerQueryOutValue(ix: number, stackFrame?: IStackFrame) {
Util.log("Runtime.debuggerQueryOutValue: " + ix);
if (!this.compiled || !this.current) return;
var frame = stackFrame ? stackFrame : this.current;
if (ix > 0) return frame.results[ix];
else return (<any>frame).orig_result || frame.result || (frame.results && frame.results[0]);
}
public saveAndCloseAllSessionsAsync(): Promise {
return this.sessions.clearScriptContext(true);
}
public permissionsAsync(): Promise {
return this.sessions.getLocalSessionAttributeAsync("____source_access", this).then(s => JSON.parse(s || "{}"));
}
public savePermissionsAsync(perm: any): Promise {
return this.sessions.setLocalSessionAttributeAsync("____source_access", JSON.stringify(perm), this);
}
static lockOrientation: (portraitAllowed: boolean, landscapeAllowed: boolean, showClock: boolean) => void = () => { };
static rateTouchDevelop: () => void = null;
static refreshNotifications: (enable: boolean) => void;
static offerNotifications() { return !!Runtime.refreshNotifications || !!localStorage["gcm"]; }
static legalNotice: string = "";
static appName = "TouchDevelop Web App";
public getActionResults() {
var r = this.returnedFrom;
if (!r) return null;
if (r.results) return r.results.slice(0);
else return [r.result];
}
private isReplaying = false;
private eventCategory: string = null;
private eventVariable: string = null;
public setNextEvent(c: string, v: string) {
this.eventCategory = c;
this.eventVariable = v;
}
public resetNextEvent() {
this.eventCategory = null;
this.eventVariable = null;
}
private replayStartTime: number;
private currentOffset: number;
public startReplay() {
this.isReplaying = true;
this.replayStartTime = new Date().getTime();
}
public stopReplayAsync() {
var p = this.stopAsync();
this.isReplaying = false;
return p;
}
public currentTime() {
return Util.perfNow();
}
public setHost(h: RuntimeHost) {
this.host = h;
this.host.init(this);
}
// cloud service
public inCloudCall: boolean = false;
public inQuery: boolean = false;
// should only be called for top-level cloud operation call
public startCloudCall(libName: string, actionName: string, paramNames: string[], returnNames: string[], args: any, isQuery: boolean) {
Util.assert(!this.inCloudCall);
Util.assert(!this.inQuery);
this.inCloudCall = true;
if (!isQuery) {
(<Revisions.NodeSession>this.sessions.getCurrentSession()).user_start_cloud_operation(libName, actionName, paramNames, returnNames, args, Revisions.CloudOperationType.OFFLINE);
} else {
this.inQuery = true;
}
}
public endCloudCall(libName: string, actionName: string, paramNames: string[], returnNames: string[], args: any, isQuery: boolean) {
Util.assert(this.inCloudCall);
this.inCloudCall = false;
if (isQuery) {
Util.assert(this.inQuery);
this.inQuery = false;
} else {
(<Revisions.NodeSession>this.sessions.getCurrentSession()).user_stop_cloud_operation(libName, actionName, paramNames, returnNames, args);
}
}
public log(s: string) {
Util.log(s);
}
////////////////////////////////////////////////////////////////////////
// Wall methods
////////////////////////////////////////////////////////////////////////
public useModalWallDialogs(): boolean {
if (this.rendermode)
Util.userError(lf("cannot ask user in page display code"));
return this.host.isFullScreen() ||
!this.mayPostToWall(this.getCurrentPage());
}
public mayPostToWall(p: WallPage): boolean {
return !this.headlessPluginMode && (!p.isAuto() || this.rendermode || p.crashed)
}
public clearWall() {
var p = this.getCurrentPage();
if (p.isAuto())
Util.userError(lf("cannot clear wall on pages"));
p.clear();
p.render(this.host);
}
public setWallDirection(topDown: boolean) {
var p = this.getCurrentPage();
if (p.isAuto())
Util.userError(lf("cannot set wall direction on pages"));
p.setReversed(topDown);
}
//private postHtmlWithTap(e:HTMLElement, rtV:RT.RTValue)
//{
// var box = this.postBoxedHtml(e)
// this.addTapEvent(e, rtV.rtType(), box, rtV);
//}
//public postTextWithTap(s:string, rtV:RT.RTValue)
//{
// this.postHtmlWithTap(div("wall-text", s), rtV);
//}
public postHtml(e: HTMLElement, pc: string): void {
this.postBoxedHtml(e, pc);
}
public postText(s: string, pc: string): void {
this.postHtml(div("wall-text", s), pc);
}
public postException(e: HTMLElement): void {
var p = this.getCurrentPage();
if (this.rendermode)
this.abortRender();
else if (p.isAuto())
p.clear();
p.crashed = true;
this.postBoxedHtml(e, "");
}
public addTapEvent(e: HTMLElement, tp: string, box: WallBox, v: any) {
if (this.eventEnabled("tap wall " + tp)) {
if (!box || !this.getCurrentPage().isAuto()) {
e.style.cursor = "pointer";
e.withClick(() => {
this.eventQ.add("tap wall " + tp, null, [v]);
});
}
else {
box.withClick(() => {
this.eventQ.add("tap wall " + tp, null, [v]);
});
}
}
}
////////////////////////////////////////////////////////////////////////
// Page methods
////////////////////////////////////////////////////////////////////////
public getPageCount(): number {
return !this.pageStack ? 0 : this.pageStack.length < 1 ? 1 : this.pageStack.length;
}
public pushPage(auto = false): WallPage {
// Hide the current page.
var currentPage = this.getCurrentPage();
currentPage.deactivate();
// Create a new page.
var page = new WallPage(this, auto);
if (auto && this.pageStack.length == 1 && !this.pageStack[0].isAuto()
&& this.pageStack[0].lastChildCount < 0 && !(this.pageStack[0].fullScreenElement))
this.pageStack[0] = page; // special case: discard startup empty legacy wall page.
else
this.pageStack.push(page);
// Append the page element to the wall.
var wall = this.getWall();
wall.appendChild(page.getElement());
if (this.pageTransitionStyle == "slide")
Util.coreAnim("showPageRight", 400, page.getElement())
else if (this.pageTransitionStyle == "fade")
Util.coreAnim("fadeIn", 400, page.getElement())
// ensure render code is called
this.forcePageRefresh();
// Notify this event to the runtime host.
this.host.notifyPagePush();
this.applyPageAttributes();
return page;
}
public popPagesIncluding(p: WallPage) {
while (this.pageStack.indexOf(p) >= 0) {
if (!this.popPage()) return;
}
}
public popPage(transition: string = null): boolean {
// Return false if current page is the default one.
if (this.pageStack.length <= 1) return false;
// Remove the topmost page element.
var currentPage = this.pageStack.pop();
var prevPage = currentPage;
var currentElement = currentPage.getElement();
// Show the previous page element.
currentPage = this.getCurrentPage();
currentPage.activate();
var hideStyle = transition;
var hideAnim = null;
if (hideStyle == "slide" || hideStyle == "slide right")
hideAnim = "hidePageLeft 0.2";
else if (hideStyle == "slide up")
hideAnim = "hidePageUp 0.7";
else if (hideStyle == "slide down")
hideAnim = "hidePageDown 0.7";
else if (hideStyle == "fade")
hideAnim = "fadeOut 0.3";
else if (hideStyle == "none")
hideAnim = "fadeOut 0.01";
if (!hideAnim && this.pageTransitionStyle == "slide")
hideAnim = "hidePageLeft 0.2";
if (hideAnim) {
currentPage.getElement().style.opacity = "0";
var parts = hideAnim.split(/ /)
var hideDuration = parseFloat(parts[1]) * 1000
hideAnim = parts[0]
Util.coreAnim(hideAnim, hideDuration, currentElement, () => {
currentElement.removeSelf()
currentPage.getElement().style.opacity = null;
if (this.pageTransitionStyle == "slide")
Util.coreAnim("showPageLeft", 300, currentPage.getElement())
else if (this.pageTransitionStyle == "fade")
Util.coreAnim("fadeIn", 400, currentPage.getElement())
else { }
});
} else if (this.pageTransitionStyle == "fade") {
Util.coreAnim("fadeOut", 400, currentElement, () => currentElement.removeSelf())
Util.coreAnim("fadeIn", 400, currentPage.getElement())
} else {
currentElement.removeSelf();
}
if (this.eventEnabled("page navigated from"))
this.eventQ.add("page navigated from", null, [prevPage.rtPage()]);
if (prevPage.onNavigatedFrom.handlers)
this.queueLocalEvent(prevPage.onNavigatedFrom);
// Notify this event to the runtime host.
this.host.notifyPagePop(prevPage);
this.applyPageAttributes();
// ensure render code is called
if (currentPage.isAuto())
this.forcePageRefresh();
else
currentPage.render(this.host);
return true;
}
public getPageAt(idx: number): WallPage {
if (idx == 0) return this.getCurrentPage();
else return this.pageStack[idx];
}
public initPageStack() {
var page = new WallPage(this, false);
this.pageStack = [page];
var wall = this.getWall();
if (wall)
wall.setChildren([page.getElement()]);
this.sessions.scriptRestarted();
// defensive programming: reset mode to avoid mode errors when restarting after crashes
this.resetRender();
}
private refreshPageStackForNewScript() {
this.pageStack.forEach((p) => p.refreshForNewScript())
}
public getCurrentPage(): WallPage {
if (!this.pageStack) return new WallPage(this, false)
return this.pageStack.peek();
}
public onCssPage(): boolean {
if (!this.pageStack) return false;
var pg = this.pageStack.peek();
return pg ? pg.csslayout : false;
}
public addPageButton(pageButton: IPageButton): void {
this.forceNonRender("You may not add a page button here");
var currentPage = this.getCurrentPage();
currentPage.buttons.push(pageButton);
this.host.notifyPageButtonUpdate();
this.addTapEvent(pageButton.getElement(), "Page Button", null, pageButton);
}
public clearPageButtons(): void {
this.forceNonRender("You may not remove a page button here");
var currentPage = this.getCurrentPage();
currentPage.buttons = [];
this.host.notifyPageButtonUpdate();
}
public getPageButtons(): IPageButton[] { return this.getCurrentPage().buttons; }
public applyPageAttributes(renderwall = false) {
var p = this.getCurrentPage();
this.host.applyPageAttributes(p);
if (renderwall && !p.isAuto())
p.render(this.host);
}
// libName, pageName, args
public postAutoPage(...args: any[]) {
this.eventQ.add("page", null, args);
}
public forceNonRender(msg = "You may not perform this operation here") {
if (this.rendermode) {
Util.userError(msg + ". Only side-effect-free operations are allowed in page display code.");
}
}
public mkLibObject(libId:string, objectName:string)
{
var singl = this.getLibRecordSingleton(libId, objectName)
var obj = <RT.ObjectEntry> new (<any>singl.entryCtor)(this);
obj.on_render_heap = this.rendermode;
return obj;
}
public getLibRecordSingleton(libId:string, objectName:string):RT.RecordSingleton
{
var indir = this.current.libs[libId + "$lib"]
if (indir) libId = indir
var d = this.datas[libId]
var getsingl = this.compiled.libScripts[libId].objectSingletons[objectName]
return getsingl(d)
}
public logDataWrite(renderheap = false) {
if (!renderheap)
this.forceNonRender("You may not modify global variables here");
if (this.inQuery)
this.forceNonRender("You may not change data in a query function");
this.forcePageRefresh();
}
public logObjectMutation(value: RT.RTValue): void {
if (value) {
value.versioncounter++;
if (!value.on_render_heap) {
this.forcePageRefresh();
this.forceNonRender();
}
} else {
this.forceNonRender();
this.forcePageRefresh();
}
}
public forcePageRefresh() {
if (!Browser.isNodeJS) {
if (this.eventQ)
this.eventQ.queuePageUpdate();
}
}
public yield_when_possible() {
if (this.eventQ)
this.eventQ.queueYield();
}
public yield_now() {
var changes = this.sessions.yieldSession();
if (this.eventQ) {
this.eventQ.finishYield(changes, this.eventEnabled("cloud data updated"));
}
}
public registerTimeDependency() {
if (this.rendermode && this.eventQ)
this.eventQ.registerTimeDependency();
}
public canPause() {
return this.pageStack && this.pageStack.length && this.pageStack[this.pageStack.length - 1].isAuto();
}
public canResume() {
return this.canPause() && this.resumeAllowed;
}
public liveViewSupported() {
return this.canResume() && this.getCurrentPage().isAuto();
}
////////////////////////////////////////////////////////////////////////
// Render Mode
////////////////////////////////////////////////////////////////////////
public rendermode = false;
// called when excecution enters display code
public enter_render() {
this.rendermode = true;
LayoutMgr.SetRenderExecutionMode(true);
var page = this.getCurrentPage();
page.startrender();
LayoutMgr.setCurrentRenderBox(page.getCurrentBox());
Util.log("Enter Render Mode");
}
// called when excecution exits display code
public leave_render() {
Util.log("Leave Render Mode");
this.render();
LayoutMgr.SetRenderExecutionMode(false);
this.rendermode = false;
if (this.eventQ)
this.eventQ.finishPageUpdate(); // we just recomputed the view
//this.render();
}
// called on exceptions
public abortRender() {
LayoutMgr.SetRenderExecutionMode(false);
this.rendermode = false;
// clear page so we can display an exception message
this.getCurrentPage().clear();
this.host.setFullScreenElement(undefined);
}
// called when starting the app (defensively)
private resetRender() {
LayoutMgr.SetRenderExecutionMode(false);
this.rendermode = false;
this.host.setFullScreenElement(undefined);
}
public markAllocated(obj: any) {
if (this.rendermode && obj) obj.on_render_heap = true;
}
////////////////////////////////////////////////////////////////////////
// Box-related method
////////////////////////////////////////////////////////////////////////
public getCurrentBoxBase(nonRenderOk = false): BoxBase {
if (this.rendermode) {
// get current box in layout mgr
return LayoutMgr.getCurrentRenderBox();
}
else {
if (!nonRenderOk)
Util.userError(lf("'box' can only be accessed in page display code"));
// get current box on current page
return this.getCurrentPage().getCurrentBox();
}
}
public getCurrentBox(): WallBox {
var box = this.getCurrentBoxBase();
if (! (box instanceof WallBox))
Util.userError(lf("'box' cannot be accessed in HTML layout mode"));
return <WallBox> box;
}
public getCurrentHtmlBox(): HtmlBox {
var box = this.getCurrentBoxBase();
if (!(box instanceof HtmlBox))
Util.userError(lf("'html' can only be accessed in HTML layout mode"));
return <HtmlBox> box;
}
public render(popCount: number = 0): void {
Contract.Requires(popCount >= 0);
this.getCurrentPage().render(this.host, popCount);
}
public renderBox(box: BoxBase): void {
var p = this.getCurrentPage();
if ((p.crashed || !p.isAuto()) && (<WallBox>box).getDepth() === 1) {
var popCount = 0;
// avoid infinite wall
var parent = <WallBox> box.parent;
Util.assert(parent instanceof WallBox);
if (parent.size() > Runtime.maxBoxLength) {
parent.shift();
popCount++;
}
this.render(popCount);
}
}
public postBoxedHtml(e: HTMLElement, pc: string, reusekey= null): BoxBase {
if (!this.mayPostToWall(this.getCurrentPage()))
Util.userError(lf("cannot post to the wall here"));
var box = WallBox.CreateOrRecycleLeafBox(this, reusekey); // null key means we never recycle
box.setContent(e);
this.renderBox(box);
return box;
}
/*
public postBoxedHtmlWithTap(e:HTMLElement, type:string, rtV:any) : WallBox
{
if (this.suppressWallPosts(this.getCurrentPage())) return;
var box = WallBox.CreateOrRecycleLeafBox(this, null); // null key means we never recycle
box.setContent(e);
this.renderBox(box);
this.addTapEvent(box.getContent(), type, rtV, box);
return box;
}
*/
public postBoxedTextWithTap(s: string, rtV: any, pc: string): BoxBase {
if (!this.mayPostToWall(this.getCurrentPage()))
Util.userError(lf("cannot post to the wall here"));
var box = WallBox.CreateOrRecycleLeafBox(this, rtV);
if (!box.getContent()) {
box.setContent(s);
this.renderBox(box);
var type;
switch (typeof rtV) {
case "boolean":
type = "Bool";
break;
case "string":
type = "String";
break;
case "number":
type = "Number";
break;
default:
type = rtV.rtType();
break;
}
if (box instanceof WallBox)
this.addTapEvent(box.getContent(), type, <WallBox>box, rtV);
}
return box;
}
public postBoxedText(s: string, pc: string): BoxBase {
if (!this.mayPostToWall(this.getCurrentPage()))
Util.userError(lf("cannot post to the wall here"));
var box = WallBox.CreateOrRecycleLeafBox(this, s);
if (!box.getContent()) {
box.setContent(s);
this.renderBox(box);
}
return box;
}
public postUnboxedText(s: string, pc: string) {
if (!this.mayPostToWall(this.getCurrentPage()))
Util.userError(lf("cannot post to the wall here"));
var box = WallBox.CreateOrRecycleLeafBox(this, s);
if (!box.getContent()) {
box.setContent(text(s));
this.renderBox(box);
}
return box;
}
static inputboxstylemap = { textline: "text", password: "password", number: "number" };
public postEditableText(style: string, s: string, handler: any /* RT.TextAction or Ref<string> */, pc: string): WallBox {
if (!this.mayPostToWall(this.getCurrentPage()))
Util.userError(lf("cannot post to the wall here"));
var box = <WallBox> WallBox.CreateOrRecycleLeafBox(this, style);
var current = box.getContent();
if (!current) {
if (style === "textarea") {
box.textarea = true;
var elt = HTML.mkTextArea();
elt.id = "i" + box.getId();
box.setContent(elt);
}
else {
box.textarea = false;
style = (Runtime.inputboxstylemap[style] || "text");
var elt2 = HTML.mkTextInput(style, lf("edit"));
elt2.id = "i" + box.getId();
box.setContent(elt2);
}
box.bindEditableText(s, handler, pc);
this.renderBox(box);
} else {
box.textarea = (style === "textarea");
box.bindEditableText(s, handler, pc);
}
return box;
}
////////////////////////////////////////////////////////////////////////
// Hooks for API
////////////////////////////////////////////////////////////////////////
public nextHitCount(current: number) {
if (current >= 200)
return Math.round(200 + (Math.random() * 50));
else
return Math.round(current * (Math.random() + 1) + 2);
}
public isHeadless() {
return this.host.isHeadless()
}
public restartAfterException() {
this.current = null
this.asyncStack = []
this.setState(RtState.AtAwait, "restart after exception");
this.queueRestart()
}
public stopAsync(isPause = false): Promise {
var p = Promise.as();
if (!this.isHeadless()) {
HistoryMgr.instance.clearModalStack();
}
if (this.state != RtState.Stopped) {
if (!isPause) {
this.versionNumber++;
if (this.eventQ) this.eventQ.clear();
}
this.eventExecuting = false;
this.resumeAllowed = isPause;
if (this.headlessPluginMode)
ProgressOverlay.hide()
this.asyncStack = [];
this.asyncTasks = [];
this.setState(RtState.Stopped, "stopAsync");
this.compiled.stopFn(this);
if (!isPause && !this.resumeAllowed && !this.handlingException) {
var profilingData = this.compiled._getProfilingResults();
this.host.attachProfilingInfo(profilingData);
var runMap = this.getRunMap();
if (runMap)
this.host.attachCoverageInfo(<CoverageData>{
compilerversion: this.compiled._compilerVersion,
astnodes: runMap.toJSON()
}, this.compiled._showCoverage);
}
if (!this.resumeAllowed) {
this.killDisposables();
this.killTempState();
}
p = this.host.notifyStopAsync();
p = p.then(() => this.sessions.stopAsync());
if (!isPause)
VisibilityManager.attachToVisibilityChange(null);
}
return p;
}
public stopAndHideAsync(): Promise {
var p = this.stopAsync();
this.host.notifyHideWall();
return p;
}
public isStopped() { return this.state == RtState.Stopped; }
public queueEvent(category: string, varValue: any, args: any[], ignore = true) {
if (this.eventQ == null) return;
this.eventQ.add(category, varValue, args, ignore);
}
public queueBoardEvent(categories: string[], valueStack: any[], args: any[], ignore = true, matchAll = false) {
if (this.eventQ == null) return;
this.eventQ.addBoardEvent(categories, valueStack, args, ignore, matchAll);
}
public queueLocalEvent(e: RT.Event_, args: any[]= [], ignore = true, skipIfInQueue = false, filter: (b: RT.EventBinding) => boolean = undefined) {
if (this.eventQ == null) return;
this.eventQ.addLocalEvent(e, args, ignore, skipIfInQueue, filter);
}
public queueAsyncEvent(f: () => any) {
this.asyncTasks.push(f);
this.queueRestart();
}
public queueRestart() {
if (this.restartQueued || this.state != RtState.AtAwait) return;
this.restartQueued = true;
Util.setTimeout(0, () => {
this.restartQueued = false;
if (this.state != RtState.AtAwait) return;
var f = this.pumpEventsCore();
if (f) {
this.mainLoop(f, "queue restart");
}
})
}
public eventEnabled(category: string) { return this.eventQ != null && !!this.eventQ.eventsByCategory[category]; }
////////////////////////////////////////////////////////////////////////
// Hooks for compiler
////////////////////////////////////////////////////////////////////////
public enterAsync(t: RT.Task<any>, s: IStackFrame) {
this.current.continueAt = s.returnAddr;
this.asyncStack.push(this.current)
s.isAsync = true
s.returnAddr = (s: IStackFrame) => {
var prev = s.rt.returnedFrom
t.resume(prev.results || prev.result)
this.setState(RtState.AtAwait, "enterAsync stop");
return null
};
return this.enter(s)
}
private last_topscript_pc: string;
private isTopScriptFrame(s: IStackFrame): boolean { return s.libs.scriptId === s.libs.topScriptId; }
public getTopScriptPc() {
return this.current ?
((!this.rendermode || this.isTopScriptFrame(this.current)) ? this.current.pc : this.last_topscript_pc)
: "";
}
public enter(s: IStackFrame): IContinuationFunction {
this.current = s;
if (s.previous) {
if (this.rendermode && s.previous.isLibProxy && this.isTopScriptFrame(s.previous.previous)) {
this.last_topscript_pc = s.previous.previous.pc; s.previous.previous.libs
}
s.stackDepth = s.previous.stackDepth + 1;
if (s.stackDepth > 1000) {
Util.userError("stack overflow");
}
}
return s.entryAddr;
}
public leave() {
var c = this.current;
this.current = c.previous;
if (this.current.isLibProxy)
this.current = this.current.previous;
if (c.serverRequest)
c.serverRequest.response().sendNow()
this.returnedFrom = c;
var ret = c.returnAddr;
//Util.dbglog("leave, " + c.name +" ret " + ret )
c.returnAddr = Runtime.pumpEvents;
return ret
}
static toRestArgument(v: any, s: IStackFrame): any {
if (typeof v == "undefined" ||
typeof v == "string" ||
typeof v == "number" ||
typeof v == "boolean") {
// OK
return v
} else if (v.exportJson) {
try {
var ctx = new JsonExportCtx(s)
var v0 = v
ctx.push(v0)
v = v.exportJson(ctx)
ctx.pop(v0)
return v
} catch (e) {
Util.userError("JSON export failed on " + v.toString())
}
} else {
Util.userError("unsupported value in JSON cloud call: " + v.toString())
}
}
public getRuntimeType(libname: string, typename: string) {
var type = this.datas[libname];
var result;
Object.keys(type).forEach((t) => {
if (type[t].name === typename) {
result = type[t]
}
});
return result;
}
static stringToBoolean(s: string): boolean {
if (!s) return false
if (/^(false|no|0)$/i.test(s)) return false
return true
}
static fromRestArgument(v: any, tp: any, s: IStackFrame): any {
//if (typeof tp === "Object") {
// tp.fromRest(v, s);
//}
//if (tp.indexOf("Collection of") !== -1) {
// var subtype = tp.slice(14);
// var rtt = s.rt.datas.all[subtype];
// return TDev.RT.Collection.fromArray(v.map((v) => rtt.fromRest(v)));
//}
var singleton = s.d["$" + tp];
if (singleton !== undefined) {
return singleton.fromRest(v);
}
var a = tp.indexOf("→");
if (a !== -1) {
var lib = tp.slice(0, a);
var tab = tp.slice(a + 1);
if (lib.indexOf("Collection of") !== -1) {
lib = lib.slice(14);
var typ = s.rt.getRuntimeType(lib, tab);
return TDev.RT.Collection.fromArray(v.map((v) => typ.fromRest(v)), typ);
} else {
return s.rt.getRuntimeType(lib, tab).fromRest(v);
}
}
switch (tp) {
case "Number":
return parseFloat(v);
case "String":
return v + "";
case "Boolean":
return Runtime.stringToBoolean(v);
case "Json Object":
return RT.JsonObject.wrap(v);
case "Json Builder":
if (typeof v == "object")
return RT.JsonBuilder.wrap(Util.jsonClone(v))
return undefined;
default:
TDev.RT.App.logEvent(TDev.RT.App.WARNING, "rest", lf("unsupported type {0}", tp), undefined);
return undefined;
}
}
// Service call that is tagged as offline available
public callServiceOffline(isQuery: boolean, site: string, service: string, libName: string, actionName: string, paramNames: string[], returnNames: string[],
returnTypes: string[], prev, ret, ...args: any[]) {
var rt = this;
var action = prev.libs[libName][actionName](prev);
var next = ret;
if (this.rendermode && !isQuery) {
this.forceNonRender("Can not call a cloud action that is not \"read-only\" in display code");
}
// setup recording if outer cloud service call
if (!rt.inCloudCall && !rt.host.isServer) {
// Compose the parameters object
var req = {};
for (var i = 0; i < args.length; i++) {
req[paramNames[i]] = Runtime.toRestArgument(args[i], prev);
}
// 1) start recording operation
prev.rt.startCloudCall(service, actionName, paramNames, returnNames, req, isQuery);
// executed after outer cloud service action
next = (s: IStackFrame) => {
// 3) stop recording operation
s.rt.endCloudCall(service, actionName, paramNames, returnNames, req, isQuery);
// 4) continue after action call
return ret;
};
}
// 2) execute action locally
return action.invoke.apply(action, [action, next].concat(args));
}
// Remote service call -- action not tagged as "offline available"
public callService(isQuery: boolean, site: string, service: string, libName: string, actionName: string, paramNames: string[], returnNames: string[],
returnTypes: string[], prev, ret, ...args: any[]) {
var rt = this;
var run = (s: IStackFrame) => {
if (this.rendermode) {
this.forceNonRender("Can not call a remote cloud action in display code (only \"offline available\" \"read-only\" are allowed)");
}
// Start await
var ctx = this.getAwaitResumeCtx((s) => s.rt.leave());
// Compose the parameters object
var req = {};
for (var i = 0; i < args.length; i++) {
req[paramNames[i]] = Runtime.toRestArgument(args[i], prev);
}
var ses = (<Revisions.NodeSession>rt.sessions.getCurrentSession());
if (!ses.hasNodeConnection()) {
var m = new ModalDialog();
m.add([
div("wall-dialog-header", lf("Trying to reach server")),
div("wall-dialog-body", lf("Please wait..."))
]);
m.show();
}
ses.user_rpc_cloud_operation(service, actionName, paramNames, returnNames, req)
.then((resp) => {
var results = returnNames.map((n) => resp[n]);
results = results.map((v, i) => Runtime.fromRestArgument(v, returnTypes[i], s))
if (results.length == 1)
s.result = results[0];
else
s.results = results;
if (m) m.dismiss();
// Resume await
ctx.resume();
}, (err: any) => {
TDev.Runtime.theRuntime.handleException(err);
});
};
return {
previous: prev,
returnAddr: ret,
d: prev.d,
rt: this,
libs: prev.libs,
entryAddr: run,
name: actionName
};
}
////////////////////////////////////////////////////////////////////////
// Parameter picking
////////////////////////////////////////////////////////////////////////
public pickParameters(cont: (s: IStackFrame) => any, ...parms: RT.IFullPicker[]) {
var ch: any[] = parms.map((p) => [div("picker-name", p.userName + ":"), div("picker-input", p.html)]);
var ctx = this.getAwaitResumeCtx(cont);
ch.push(div("wall-dialog-buttons", HTML.mkButton(lf("ok"), () => {
var allOk = true;
var s = this.current;
parms.forEach((p) => {
var ok = p.validate();
allOk = allOk && ok;
p.html.setFlag("invalid", !ok);
if (ok) {
s[p.quotedName] = p.get();
}
});
if (allOk)
ctx.resume();
})));
this.postHtml(div("picker-form", ch), "");
}
public displayResult(name: string, val: any) {
var e = div("picker-form");
var pc = this.current.pc;
var box = new WallBox(this, <WallBox> this.getCurrentBoxBase(true), pc);
box.setContent(e);
this.getCurrentPage().setCurrentBox(box);
var dual = (str: string) => {
this.postHtml(div("picker-name", name + ":"), pc)
this.postHtml(div("picker-input", str), pc)
}
try {
switch (typeof val) {
case "string": dual(val); break;
case "number": dual(val + ""); break;
case "boolean": dual(val ? "True" : "False"); break;
case "undefined": dual("[invalid]"); break;
default:
if (val === null)
dual("[null]"); // shouldn't really happen
else {
if (val.postResult)
val.postResult(this.current);
else {
this.postText(name + ":", pc);
val.post_to_wall(this.current);
}
}
break;
}
} finally {
this.getCurrentPage().setCurrentBox(box.parent)
}
box.forEachChild((c) => {
e.appendChild(c.getContent())
});
this.renderBox(box);
}
////////////////////////////////////////////////////////////////////////
// Execution loop
////////////////////////////////////////////////////////////////////////
public setState(s: RtState, msg: string) {
// Util.log("state: {0} -> {1}, {2}", this.state, s, msg)
if (this.state == RtState.Stopped || s == RtState.Stopped)
Util.log("runtime state: {0} -> {1}, {2}", this.state, s, msg)
this.state = s;
}
private getResumeCtxCore(isBlocking: boolean, cont: IContinuationFunction) {
//this.forceNonRender();
//this.forcePageRefresh();
Util.assert(this.state == RtState.Running)
if (isBlocking)
this.setState(RtState.Paused, "getBlockingResumeCtx");
else
this.setState(RtState.AtAwait, "getAwaitResumeCtx");
this.current.continueAt = cont
var r = new ResumeCtx(this.current);
r.isBlocking = isBlocking;
r.versionNumber = this.versionNumber;
return r
}
public getBlockingResumeCtx(cont: IContinuationFunction) { return this.getResumeCtxCore(true, cont) }
public getAwaitResumeCtx(cont: IContinuationFunction) {
if (this.rendermode)
Util.userError("non-atomic APIs cannot be called in page display code");
return this.getResumeCtxCore(false, cont)
}
public getAsyncResumeCtx() {
Util.assert(this.state == RtState.Running)
if (this.rendermode)
Util.userError("'async' cannot be used in page display code");
var t = new (<any>RT.Task)(); // TS9
var r = new RT.TaskResumeCtx(t, this.current);
r.versionNumber = this.versionNumber;
return r
}
public mkActionTask() {
var t = new (<any>RT.Task)(); // TS9
return t;
}
public _resumeVal(v: any, r: ResumeCtx) {
var frame = r.stackframe
// a stale continuation coming to hunt us?
if (r.versionNumber != this.versionNumber) return;
//Util.dbglog("resuming, " + this.state + " v: " + v)
if (this.state == RtState.Paused) {
if (r.isBlocking) {
this._resumeValCore(v, frame)
this.mainLoop(frame.continueAt, "_resumeValBlocking");
} else {
// we're waiting for a blocking await, but another thing has finished; put it in the queue
this.queueAsyncEvent(() => this.continueStackFrame(v, frame))
}
} else if (this.state == RtState.AtAwait) {
Util.assert(!r.isBlocking, "blocking await")
this._resumeValCore(v, frame)
this.mainLoop(frame.continueAt, "_resumeValAwait");
} else {
Util.oops("wrong resume state: " + this.state)
}
}
public continueStackFrame(v: any, frame: IStackFrame): IContinuationFunction {
this._resumeValCore(v, frame)
return frame.continueAt
}
private _resumeValCore(v: any, frame: IStackFrame) {
this.current = frame;
if (frame.rendermode !== undefined)
this.rendermode = frame.rendermode;
frame.pauseValue = v;
}
public initFrom(cs: CompiledScript) {
this.compiled = cs;
if (cs.authorId) this.currentAuthorId = cs.authorId;
if (cs.scriptId) { this.currentScriptId = this.baseScriptId = cs.scriptId; }
cs.initPages();
EventQueue.init(this);
this.sessions.scriptStarted(cs.authorId);
}
private nextAsyncTask(): IContinuationFunction {
if (this.asyncStack.length > 0) {
// isAsync should be always true here
if (this.current.isAsync)
this.current.isDetached = true
var f = this.asyncStack.pop()
this.current = f;
this.setState(RtState.Running, "async stack");
return f.continueAt;
} else if (this.asyncTasks.length > 0) {
var q = this.asyncTasks.shift()
this.setState(RtState.Running, "async task");
return q();
} else {
return null
}
}
public wrap(s: IStackFrame, f: any): any {
if (this.isStopped())
return () => { };
if (!s) s == this.current
var rt = s.rt
Util.assert(rt == this)
return function () {
try {
if (rt.isStopped())
return undefined
Runtime.theRuntime = rt
rt.current = s
f.apply(this, arguments)
} catch (e) {
rt.handleException(e)
e.rtProtectHandled = true
throw e
}
}
}
public pumpEventsCore(): IContinuationFunction {
this.yield_now();
Util.assert(!this.isStopped(), "pump-stopped")
var r = this.nextAsyncTask()
if (r) return r
if (this.eventQ == null) {
this.stopAsync().done()
return null;
} else if (this.eventExecuting) {
this.setState(RtState.AtAwait, "event executing");
return null
} else {
var fn = this.eventQ.maybeRunPageRefresh();
if (!fn)
fn = this.eventQ.process();
if (!fn && this.host.dontWaitForEvents())
this.stopAsync(true).done();
else if (fn)
this.setState(RtState.Running, "got event");
else {
if (this.liveMode())
this.stopAsync(true).done()
else if (this.headlessPluginMode)
this.stopAsync().done()
else
this.setState(RtState.AtAwait, "no event");
}
return fn;
}
}
static pumpEvents(s: IStackFrame) {
s.rt.eventExecuting = false
return s.rt.pumpEventsCore();
}
public pauseExecution() {
this.queueEvent("pause", null, [])
}
public resumeExecution(once: boolean = false) {
this.eventQ.clearPause()
if (this.state != RtState.Stopped || !this.resumeAllowed) return;
VisibilityManager.attachToVisibilityChange(this);
this.refreshPageStackForNewScript();
this.eventQ.clear();
if (!this.liveMode())
this.getWall().setChildrenIfNeeded(this.pageStack.map((p) => p.getElement()))
try {
this.eventQ.blockEvents = false;
this.logDataWrite();
this.eventQ.blockEvents = once;
var bot = new StackBottom(this);
bot.entryAddr = Runtime.pumpEvents;
bot.returnAddr = Runtime.pumpEvents;
var frame = this.enter(bot);
if (!once)
VisibilityManager.attachToVisibilityChange(this);
this.mainLoop(frame, "resume execution");
} catch (e) {
this.handleException(e)
}
}
static mkStackFrame(prev: IStackFrame, ret: any) {
return <IStackFrame>{
previous: prev,
d: prev.d,
rt: prev.rt,
libs: prev.libs,
returnAddr: ret
};
}
static syntheticFrame(f: (s: IStackFrame) => void) {
return (prev: IStackFrame, ret: any) => {
var s = Runtime.mkStackFrame(prev, ret);
s.name = "__synthetic";
s.entryAddr = (s) => {
s.rt.forcePageRefresh();
f(s);
return s.rt.leave();
};
return s;
}
}
public getActionFrame(mk: any, args: any[], isBlocking = true): IContinuationFunction {
var bot = new StackBottom(this);
if (args == null) {
bot.needsPicker = true;
args = [];
}
var fnObj: any;
if (isBlocking)
this.eventExecuting = true;
if (args.length == 0)
fnObj = mk(bot, Runtime.pumpEvents);
else
fnObj = mk.apply(null, (<any[]>[bot, Runtime.pumpEvents]).concat(args));
// Special event processing work-around
this.resetNextEvent();
return this.enter(fnObj);
}
public queueEventCallback(cb: (rt: Runtime, args: any[]) => void) {
var ev = new RT.Event_();
ev.addHandler(new RT.PseudoAction((rt: Runtime, args: any[]) => {
cb(rt, args)
}));
this.queueLocalEvent(ev)
}
public getEventFrame(mk: any, args: any[], isBlocking: boolean = true): IContinuationFunction {
try {
if (this.state != RtState.Running) {
Util.assert(this.state == RtState.AtAwait)
this.setState(RtState.Running, "getEventFrame dispatch");
}
return this.getActionFrame(mk, args, isBlocking);
} catch (e) {
return () => { throw e; return null };
}
}
static handleUserError(err: any) {
if (Runtime.theRuntime && Runtime.theRuntime.state != RtState.Stopped && !Runtime.theRuntime.handlingException) {
if (err.isUserError || err.wabStatus) {
Runtime.theRuntime.handleException(err);
return true;
}
}
return false;
}
static stopPendingScriptsAsync() {
if (Runtime.theRuntime && Runtime.theRuntime.state != RtState.Stopped) {
return Runtime.theRuntime.stopAsync();
} else {
return Promise.as();
}
}
private startMk: any;
private startArgs: any[];
public rerun() {
Util.assert(this.isStopped(), "rerun-isStopped")
this.initPageStack();
this.run(this.startMk, this.startArgs);
}
public run(mk: any, args: any[]) {
Util.assert(this.isStopped(), "run-isStopped")
this.startMk = mk;
this.startArgs = args;
this.tutorialState = null
Runtime.stopPendingScriptsAsync().done(() => {
this.errorPC = undefined;
Runtime.theRuntime = this;
if (this.eventQ)
this.eventQ.blockEvents = false;
this.resetRunMap();
this.killTempState();
ProgressOverlay.setMessage("loading...");
RT.ArtCache.resetProgress();
this.initDataAsync().done(() => {
try {
if (this.headlessPluginMode) {
ProgressOverlay.setMessage("running...")
ProgressOverlay.unblockKeyboard()
ProgressOverlay.showLog()
}
else
ProgressOverlay.hide();
Util.assert(this.isStopped())
VisibilityManager.attachToVisibilityChange(this);
// missing must be setup after the loading dialog is gone
this.host.initApiKeysAsync()
.then(() => this.host.agreeTermsOfUseAsync())
.done(() => {
try {
var entryPt = this.getActionFrame(mk, args);
if (this.validatorAction) {
var act = this.current.libs["tutorialLib"][this.validatorAction]
if (!act) {
Util.userError(lf("problem with tutorial: validator action '{0}' not found", this.validatorAction))
}
var libcall = act(this.current)
this.tutorialObject = libcall.libs.topScriptId
if (/norun/i.test(this.validatorActionFlags))
entryPt = Runtime.pumpEvents
entryPt = this.enter(libcall.invoke(libcall, entryPt, this.editorObj))
}
this.mainLoop(entryPt, "Runtime.run");
} catch (e) {
this.handleException(e)
}
});
} catch (e) {
this.handleException(e)
}
}, (e) => {
this.handleException(e);
});
});
}
public resyncData()
{
this.datas = {};
}
public initDataAsync() : Promise
{
this.compiled.startFn(this);
var loadSession = this.sessions.ensureSessionLoaded().thenalways(() => { this.sessions.yieldSession(); });
this.compiled.initGlobals(this.datas, this);
// let NodeJS skip initArtAsync
//if (Browser.isNodeJS) return loadSession;
//else
return Promise.join([this.compiled.initArtAsync(this.datas), loadSession]);
}
public killDisposables() {
// dispose more data
this.disposables.forEach(d => {
try {
d.dispose();
}
catch (e) {
Util.reportError('', e, false);
}
});
this.disposables = [];
}
public killTempState() {
this.sessions.unlink(); // sessions can have backlinks
this.compiled.resetData(this.datas); // all globals need to be cleared
}
// called from editor on data definition changes
public resetData() {
if (this.state == RtState.Stopped && this.resumeAllowed) {
this.resumeAllowed = false;
this.killTempState();
this.initPageStack(); // pages can reference temporary data
this.host.notifyRunState();
}
}
public quietlyHandleError(e:any)
{
for (var s = this.current; s; s = s.previous) {
if (s.isDetached) break
if (s.errorHandler) (() => {
var s0 = s
Util.setTimeout(0, () => {
try {
s0.errorHandler(e, s0)
} catch (exn) {
this.host.exceptionHandler(exn);
}
})
})()
}
return false
}
public handleException(e:any)
{
if (this.quietlyHandleError(e))
return
this.handlingException = true;
if (e.programCounter)
this.errorPC = e.programCounter;
this.compiled.extractAllRunMaps(this);
this.host.exceptionHandler(e);
this.stopAsync().done()
}
public getStackTrace()
{
var locs:IStackFrame[] = []
if (this.errorPC)
locs.push(<any>{ pc: this.errorPC })
for (var s = this.current; s; s = s.previous) {
locs.push(s)
}
return locs;
}
public getRunMap() {
this.runMap.clear();
if (this.compiled.extractAllRunMaps(this))
return this.runMap;
else
return undefined;
}
private lastBreak = 0;
private quickLoopDepth = 0;
public wrapFromHandler(f:()=>void)
{
var prevState = this.state
var prevCurr = this.current
this.current = new StackBottom(this);
this.setState(RtState.Running, "runUserAction from handler")
try {
f()
this.setState(prevState, "runUserAction from handler restore")
this.current = prevCurr
} catch (e) {
this.handleException(e)
}
}
public runUserAction(f:RT.ActionBase, args:any[])
{
args.unshift((s:IStackFrame) => {
this.state = RtState.Paused
return null
})
args.unshift(this.current)
var fn = this.enter((<any>f).apply(null, args))
return this.quickLoop(fn)
}
public runValidUserAction(f:RT.ActionBase, args:any[])
{
var r = this.runUserAction(f, args)
if (r === undefined)
Util.userError("user action passed to library returned invalid")
return r
}
private quickLoop(fn:IContinuationFunction) : any
{
if (this.quickLoopDepth > 20) {
Util.userError("stack overflow (more than 20 runtime/user code switches)")
}
Util.assert(this.state == RtState.Running);
this.quickLoopDepth++;
var prevFrom = this.returnedFrom
this.returnedFrom = null
try {
while (true) {
var newFn = fn(this.current);
if (this.state != RtState.Running) {
if (this.state == RtState.Paused) {
this.state = RtState.Running
break
}
if (this.state == RtState.BreakpointHit)
Util.userError("breakpoints in library callbacks are not supported")
Util.oops("wrong state in quick Loop " + this.state);
}
if (!newFn) Util.oops("no newFn: " + fn);
fn = newFn;
}
} finally {
this.quickLoopDepth--;
}
var res = this.returnedFrom ? this.returnedFrom.result : undefined
this.returnedFrom = prevFrom
return res
}
public mainLoop(fn:IContinuationFunction, comment:string) : void
{
if (!Runtime.continueAfter)
Runtime.continueAfter = Util.setTimeout;
this.handlingException = false;
// this happens when resumeVal() is called from the API method, not from an event handler later
if (this.mainLoopRunning) {
//Util.dbglog("saving resume point override")
this.setState(RtState.Running, "resume point override");
this.resumePointOverride = fn;
return;
}
var prevState = this.state
this.setState(RtState.Running, comment);
if (prevState == RtState.Stopped)
this.host.notifyRunState();
// check that cloud state is valid before proceeding, abort execution if necessary
if (!this.sessions.readyForExecution()) {
this.stopAsync().done();
return;
}
// var lastBreak = Date.now();
var continueLater = false;
var numCheck = 0;
this.mainLoopRunning = true;
try {
while (true) {
var newFn = fn(this.current);
if (this.state != RtState.Running) {
if (this.state == RtState.BreakpointHit) {
this.debuggerCC = newFn;
break;
}
if (this.state == RtState.Stopped) {
break;
}
if (this.debuggerCC) {
this.debuggerCC = null;
this.host.notifyBreakpointContinue();
}
if (this.state == RtState.AtAwait) {
newFn = this.pumpEventsCore();
if (!newFn) break;
Util.check(!this.resumePointOverride);
this.resumePointOverride = null;
}
if (this.state != RtState.Running) {
break;
}
}
if (!newFn) Util.oops("no newFn: " + fn);
if (!!this.resumePointOverride) {
fn = this.resumePointOverride;
this.resumePointOverride = null;
} else {
fn = newFn;
}
if (numCheck++ > 10) {
var now = Date.now();
if (now - this.lastBreak > 50) {
continueLater = true;
break;
}
numCheck = 0;
}
}
} catch (e) {
this.handleException(e)
}
this.mainLoopRunning = false;
if (continueLater) {
this.setState(RtState.Paused, "continue later");
var ver = this.versionNumber;
var curr = this.current
Runtime.continueAfter(1, () => {
if (ver != this.versionNumber)
return;
this.lastBreak = Date.now();
this.current = curr
this.mainLoop(fn, "continue later - run")
});
}
Util.assert(this.state != RtState.Running)
}
static continueAfter:(ms:number,f:()=>void)=>void;
public saveDataAsync() : Promise
{
this.sessions.yieldSession();
return Promise.as();
}
public getRestRequest():RT.ServerRequest
{
var frame = <any>this.current
while (frame && !frame.serverRequest)
frame = frame.previous
return frame ? frame.serverRequest : undefined
}
public getRestArgument(name:string, tp:string, s:IStackFrame):any
{
var r = this.getRestRequest().getRestArgument(name, tp, s);
//console.log("get rest : " + name + " -> " + r)
return r
}
}
export class EventHandlerDesc
{
constructor(public varId:string, public entry:any) {
}
}
export interface IEventEntry {
category: string;
dispatch(rt: Runtime, eventsByCategory: any): IContinuationFunction
isGameLoop(): boolean;
isPause(): boolean;
isPageEvent():boolean;
clear():void;
}
export class EventEntry implements IEventEntry {
public category = "local";
private done = false;
constructor(private binding : RT.EventBinding, private args : any[], private isLast:boolean)
{ }
public isPageEvent() { return this.binding._event.isPageEvent }
public dispatch(rt: Runtime, eventsByCategory: any): IContinuationFunction
{
if (this.done) return null;
this.done = true;
var ev = this.binding._event;
ev.pendinghandlers--;
this.binding.inQueue = false;
var f = this.binding._handler;
if (!f) return null;
if (f instanceof RT.PseudoAction) {
(<RT.PseudoAction>f).run(rt, this.args);
return null;
}
var res = rt.getEventFrame(f, this.args, ev.isBlocking);
rt.current.currentHandler = this.binding;
if (ev.finalCallback) {
var cb = ev.finalCallback
ev.finalCallback = null
rt.current.returnAddr = s => {
cb(s);
return Runtime.pumpEvents(s)
}
}
if (ev.errorHandler) {
rt.current.errorHandler = ev.errorHandler
}
return res
}
public clear()
{
var ev = this.binding._event;
ev.pendinghandlers = 0;
}
public isPause() { return false; }
public isGameLoop() { return false; }
}
export class GlobalEventEntry implements IEventEntry
{
private evts:EventHandlerDesc[];
public isPageEvent() { return false; }
constructor(public category:string, private varValue:any, private args:any[], private idx = 0) {
Util.assert(category != "local");
}
public dispatch(rt: Runtime, eventsByCategory: any): IContinuationFunction
{
if (this.evts === undefined)
this.evts = eventsByCategory[this.category];
if (this.evts === undefined)
return null;
while (this.idx < this.evts.length) {
var handler = this.evts[this.idx++];
var match = this.handlerMatch(handler, rt);
if (match.matches) {
rt.setNextEvent(this.category, handler.varId);
return rt.getEventFrame(handler.entry, match.args);
}
}
return null;
}
private handlerMatch(desc:EventHandlerDesc, rt:Runtime)
{
if (desc.varId == null) return { matches:true, args:this.args };
if (this.category.search(/ in /) >= 0) {
var set : ObjSet = rt.datas["this"][desc.varId];
if (!!set) {
var idx = set.index_of_obj(this.varValue);
if (idx >= 0) {
return {matches:true, args:[this.varValue, idx].concat(this.args)}
}
}
}
else {
if (rt.datas["this"][desc.varId] === this.varValue) {
return {matches:true, args:this.args};
}
}
return {matches:false, args:[]};
}
public isGameLoop() { return this.category == "gameloop" && !this.evts; }
public isPause() { return this.category == "pause" && !this.evts; }
public clear() {}
}
export class BoardEventEntry implements IEventEntry
{
public category = "board";
private categoryHandlers:EventHandlerDesc[][];
private matchedHandlers: { category: string; varid: string; handler: any; args: any[]; }[];
constructor(private categories:string[], private valueStack:any[], private args:any[], private matchAll:boolean) {
}
///
/// We need to potentially match a value against many sets/locals, not just the first handler that is satisfied
/// However, once we picked a value that matches, not other values should be picked and matched!
///
public dispatch(rt:Runtime, eventsByCategory:any)
{
if (this.categoryHandlers === undefined)
this.categoryHandlers = <any>this.categories.map((c) => eventsByCategory[c]);
var valueIndex = 0;
while (!this.matchedHandlers && valueIndex < this.valueStack.length) {
var value = this.valueStack[valueIndex++];
var categoryIndex = 0;
while (categoryIndex < this.categories.length) {
var category = this.categories[categoryIndex];
var handlers = this.categoryHandlers[categoryIndex++];
if (!handlers) {
continue;
}
var handlerIndex = 0;
while (handlerIndex < handlers.length) {
var handler = handlers[handlerIndex++];
var match = this.handlerMatch(handler, category, value, rt);
if (match.matches) {
if (!this.matchedHandlers) {
this.matchedHandlers = [];
}
this.matchedHandlers.push({ category: category, varid: handler.varId, handler: handler.entry, args: match.args });
}
}
}
if (!this.matchAll && !!this.matchedHandlers) {
// stop matching lower z-index values
break;
}
}
if (!!this.matchedHandlers && this.matchedHandlers.length > 0) {
var matchedHandler = this.matchedHandlers.shift();
rt.setNextEvent(matchedHandler.category, matchedHandler.varid);
return rt.getEventFrame(matchedHandler.handler, matchedHandler.args);
}
return null;
}
private handlerMatch(desc:EventHandlerDesc, category:string, varValue:any, rt:Runtime)
{
if (category.search(/ sprite in /) >= 0) {
var set : ObjSet = rt.datas["this"][desc.varId];
if (!!set) {
var idx = set.index_of_obj(varValue);
if (idx >= 0) {
return {matches:true, args:[varValue, idx].concat(this.args)}
}
}
}
if (category.search(/touch over /) >= 0) {
var set : ObjSet = rt.datas["this"][desc.varId];
if (!!set) {
var idx = set.index_of_obj(varValue);
if (idx >= 0) {
return {matches:true, args:[varValue, idx].concat(this.args)}
}
}
}
else if (category.search(/ sprite:/) >= 0) {
if (rt.datas["this"][desc.varId] == varValue) {
return { matches: true, args: [varValue].concat(this.args) }
}
}
else {
if (rt.datas["this"][desc.varId] === varValue) {
return { matches: true, args: this.args };
}
}
return {matches:false, args:[]};
}
public isGameLoop() { return false; }
public isPause() { return false; }
public isPageEvent() { return false }
public clear() {}
}
export interface ObjSet
{
index_of_obj(v: any): number;
}
export class EventQueue {
private queue: IEventEntry[] = [];
private needPageRefresh = false;
private needYield = false;
private needCloudstateRefresh = false;
public eventsByCategory: any = null;
public hasGameLoop = false;
public needsGameLoopTimer = false;
public blockEvents = false;
public eps = 0; // gameloop events per second
public minimumEps = 0;
public maximumEps = 0;
public averageEps = 0;
public epsHistory: number[] = [];
public profiling = false;
public numPageEvents = 0;
constructor(public rt: Runtime) {
}
public add(category: string, varValue: any, args: any[], ignore = true) {
if (this.blockEvents) return;
Util.assert(!(varValue instanceof RT.Event_));
if (this.eventsByCategory[category] !== undefined) {
var ev = new GlobalEventEntry(category, varValue, args);
this.queue.push(ev);
this.rt.queueRestart();
}
}
public addLocalEvent(ev: RT.Event_, args: any[], ignore = true, skipIfInQueue = false, filter : (b : RT.EventBinding) => boolean = undefined) {
if (!this.blockEvents && ev.handlers) {
var anyPushed = false;
for (var i = 0; i < ev.handlers.length; ++i) {
var binding = ev.handlers[i];
if (filter && !filter(binding)) continue;
if (!(skipIfInQueue && binding.inQueue)) {
var ee = new EventEntry(binding, args, i == ev.handlers.length - 1);
if (ee.isPageEvent()) this.numPageEvents++;
ev.pendinghandlers++;
binding.inQueue = true;
this.queue.push(ee);
anyPushed = true;
}
}
if (anyPushed)
this.rt.queueRestart()
}
ev.runAwaiters(args)
}
public addBoardEvent(categories: string[], valueStack: any[], args: any[], ignore = true, matchAll = false) {
if (this.blockEvents) return;
this.queue.push(new BoardEventEntry(categories, valueStack, args, matchAll));
this.rt.queueRestart()
}
public clear() {
this.queue.forEach(e => e.clear())
this.queue = []
}
public clearPause() {
this.queue = this.queue.filter(q => !q.isPause());
}
public calculateEpsInfo() {
if (this.epsHistory.length > 2) {
// first and last measurements are inaccurate
var myEpsHistory = this.epsHistory.slice(1);
myEpsHistory.pop();
var minimum = Number.MAX_VALUE;
var maximum = 0;
var cumulative = 0;
for (var i = 0; i < myEpsHistory.length; ++i) {
if (myEpsHistory[i] > maximum)
maximum = myEpsHistory[i];
if (myEpsHistory[i] < minimum)
minimum = myEpsHistory[i];
cumulative += myEpsHistory[i];
}
if (TDev.dbg) {
Util.log("Gameloop Events per Second statistics: minimum " + minimum + ", maximum "
+ maximum + ", average " + (cumulative / myEpsHistory.length).toFixed(2)
+ ". Ran for " + myEpsHistory.length + "s.");
}
this.minimumEps = minimum;
this.maximumEps = maximum;
this.averageEps = cumulative / myEpsHistory.length;
this.epsHistory = [];
}
}
private setupGameLoopTimer() {
var stopLogEPS = false;
// Log gameloop events per second
var logEPS = (): void => {
var eps = this.eps;
this.eps = 0;
if (!stopLogEPS)
Util.setTimeout(1000, logEPS);
if (eps > 0)
this.epsHistory.push(eps);
}
var gameLoop = () =>
{
if (this.rt.isStopped()) {
this.needsGameLoopTimer = true;
stopLogEPS = true;
return;
}
var hasIt = false;
for (var i = 0; i < this.queue.length; ++i)
if (this.queue[i].isGameLoop()) { hasIt = true; break; }
if (!hasIt)
this.add("gameloop", null, []);
Util.setTimeout(20, gameLoop);
}
this.needsGameLoopTimer = false;
gameLoop();
if (this.profiling)
logEPS();
}
// queue a page refresh
public queuePageUpdate() {
this.needPageRefresh = true;
this.rt.queueRestart();
}
public viewIsCurrent():boolean {
return !this.needPageRefresh;
}
public registerTimeDependency() {
this.pageIsTimeDependent = true;
}
private pageIsTimeDependent = false;
private refreshtimerpending = false;
public finishPageUpdate() {
this.needPageRefresh = false;
if (this.pageIsTimeDependent) {
this.pageIsTimeDependent = false;
if (!this.refreshtimerpending) {
this.refreshtimerpending = true;
Util.setTimeout(100, () => {
if (this.refreshtimerpending) {
this.refreshtimerpending = false;
this.queuePageUpdate();
}
});
}
}
else
this.refreshtimerpending = false;
}
// queue yield
public queueYield() {
this.needYield = true;
this.rt.queueRestart();
}
public finishYield(changes: boolean, fireevent: boolean) {
this.needYield = false;
if (changes)
{
if (fireevent)
this.add("cloud data updated", null, []);
this.queuePageUpdate();
}
}
// queue events if nothing else is in the queue
public maybeRunPageRefresh():IContinuationFunction {
if (this.rt.isHeadless())
return null
if (!this.needPageRefresh && !this.needYield)
return null
if (this.numPageEvents > 0)
return null
// for both page refresh and yield, we use the same event
var p = this.rt.getCurrentPage();
if (p.isAuto() && !p.crashed) {
return () => {
var page = this.rt.getCurrentPage();
return this.rt.getEventFrame((p, b) => page.getFrame(p, b), [])
};
}
else {
this.needPageRefresh = false;
this.needYield = false;
return null
}
}
public process() : any
{
if (this.needsGameLoopTimer)
this.setupGameLoopTimer();
while (this.queue.length > 0) {
var e = this.queue[0];
if (this.profiling && this.hasGameLoop && e.isGameLoop()) {
++this.eps;
}
var fn = e.dispatch(this.rt, this.eventsByCategory);
if (fn != null) return fn;
this.queue.shift();
if (e.isPageEvent()) this.numPageEvents--;
Util.assert(this.numPageEvents >= 0);
}
return null;
}
static init(rt: Runtime) {
var s = rt.compiled;
var q = new EventQueue(rt);
rt.eventQ = q;
if (!s.eventsByCategory || !s.eventsByCategory["local"]) {
// will never get called; handled specially
s.registerEventHandler("local", null, null);
}
q.hasGameLoop = (s.eventsByCategory && s.eventsByCategory["gameloop"] !== undefined);
q.eps = 0;
q.needsGameLoopTimer = q.hasGameLoop;
q.eventsByCategory = s.eventsByCategory;
}
}
export interface PackageResource {
kind: string;
type: string;
id: string;
packageUrl: string;
url?: string;
content?: string;
sourceName?: string;
usageLevel?: number;
}
export interface ApiKey {
url: string;
value: string;
}
export interface BreakpointBinding {
setter: (v: boolean) => void;
getter: () => boolean;
}
export interface BreakpointBindings {
[pc: string] : BreakpointBinding;
}
export class BreakpointCollection {
constructor(private cs: CompiledScript) { }
public init(bps: Hashtable) {
bps.forEach((k, v) => this.cs.breakpointBindings[k].setter(true));
}
public set(bp: string, val: boolean) {
this.cs.breakpointBindings[bp].setter(val);
}
public get(bp: string) {
return this.cs.breakpointBindings[bp].getter();
}
}
export class CompilerOptStatistics {
constructor(public inlinedFunctions = 0, public inlinedCalls = 0, public eliminatedOks = 0,
public termsReused = 0, public constantsPropagated = 0,
public reachingDefsTime = 0, public inlineAnalysisTime = 0, public dominatorsTime = 0,
public usedAnalysisTime = 0, public availableExpressionsTime = 0,
public constantPropagationTime = 0,
public compileTime = 0, public numActions = 0, public numStatements = 0) {
}
}
export class CompiledScript
{
private steps = [];
public actionsByName:any = {};
public actionsByStableName:any = {};
public pagesByName:any = {};
public code:string;
public objectSingletons:any;
public additionalCode:string;
public eventsByCategory:any = null;
public reflectionInfo:StringMap<any> = {};
private artInitializers = [];
private artPromises: Promise[] = [];
public missingApis: string[] = [];
private apiKeys: any = {};
public globals:string[] = [];
public libScripts:any;
public libs:any;
public libBindings:any;
public mainActionName:string;
public packageResources : PackageResource[] = [];
public npmModules: StringMap<string> = {};
public cordovaPlugins: StringMap<string> = {};
public pipPackages: StringMap<string> = {};
public authorId: string;
public scriptId: string;
public baseScriptId: string;
public hasCloudData: boolean;
public hasLocalData: boolean;
public hasPartialData: boolean;
public azureSite: string;
public scriptGuid: string;
public allApiKeys(): ApiKey[] {
var r = {};
Object.keys(this.libScripts).forEach(cs => {
var keys = this.libScripts[cs].apiKeys;
Object.keys(keys).forEach(key => r[key] = keys[key]);
});
return Object.keys(r).map(k => {
return { url: k, value: r[k] };
});
}
public scriptTitle: string = "";
public scriptColor: string = "";
public primaryName:string;
public showAd = false;
public startFn:(rt:Runtime)=>void = (rt) => {};
public stopFn: (rt: Runtime) => void = (rt) => { };
public setupRestRoutes: (rt:Runtime)=>void = (rt) => {};
public extractRunMap: (rt: Runtime) => void = undefined;
public _resetGlobals:(data:any)=>any = (dt) => {};
public _initGlobals:(data:any,rt:Runtime)=>any = (dt,rt) => {};
public _initGlobals2: (data: any) => any = (dt) => { };
public _importJson: (data: any, ctx:JsonImportCtx, json:any) => any = (dt,ctx,json) => { };
public _exportJson: (data: any, ctx: JsonExportCtx) => any = (dt, ctx) => { };
public _getProfilingResults: () => any = () => null;
public _showCoverage = false;
public _compilerVersion: string;
public breakpointBindings: BreakpointBindings = {};
public breakpoints: BreakpointCollection;
public initBreakpoints: Hashtable = null;
public localNamesBindings: { [action: string]: { [name: string]: string; }; } = {};
public optStatistics = new CompilerOptStatistics();
constructor()
{
this.libScripts = { "this": this };
this.breakpoints = new BreakpointCollection(this);
}
public forEachLib(f:(cs:CompiledScript) => void)
{
Object.keys(this.libScripts).forEach((k) => {
f(this.libScripts[k])
});
}
public extractAllRunMaps(rt: Runtime) {
var defined = true;
Object.keys(this.libScripts).forEach((k) => {
var extractRunMap = this.libScripts[k].extractRunMap;
if (extractRunMap)
extractRunMap(rt);
else
defined = false;
});
return defined;
}
public registerAction(name:string, stName:string, entry:any, isAsync:boolean)
{
this.actionsByName[name] = entry;
this.actionsByStableName[stName] = entry;
if (isAsync && !this.eventsByCategory)
this.registerEventHandler("async", null, null);
}
public initPages()
{
var hasPages = Object.keys(this.libScripts).some((k) => Object.keys(this.libScripts[k].pagesByName).length > 0);
if (!hasPages) return;
this.registerEventHandler("page", null,
(prev:IStackFrame, ret:(s:IStackFrame)=>any, libName:string, pageName:string, ...args:any[]) => {
var p = prev.rt.pushPage(true);
p.libName = libName;
p.pageName = pageName;
p.drawArgs = args;
return p.getFrame(prev, ret);
});
}
public getCompiledCode()
{
Object.keys(this.libScripts).forEach((name:string) => { this.libScripts[name].primaryName = null })
var res = "var TDev;\nif (!TDev) TDev = {};\nTDev.precompiledScript = {\n"
var first = true;
Object.keys(this.libScripts).forEach((name:string) => {
if (!first)
res += ",\n\n// **************************************************************\n"
first = false;
res += Util.jsStringLiteral(name) + ": ";
var cs = <CompiledScript>this.libScripts[name];
if (cs.primaryName) res += Util.jsStringLiteral(cs.primaryName);
else {
cs.primaryName = name;
res += cs.code
}
})
res += "}\n"
return res;
}
public initFromPrecompiled(script:any = null)
{
if (!script)
script = (<any>TDev).precompiledScript;
Object.keys(script).forEach((name:string) => {
if (name == "this") return;
var f = script[name]
var cs = this;
if (typeof f == "string") {
cs = this.libScripts[f]
} else {
cs = new CompiledScript();
f(cs);
}
this.registerLibRef(name, cs);
});
script["this"](this);
}
public registerPage(name:string, stName:string, entry:any)
{
this.actionsByName[name] = entry;
this.actionsByStableName[stName] = entry;
this.pagesByName[name] = entry;
}
public registerLambda(name:string, stName:string, entry:any)
{
this.actionsByName[name] = entry;
this.actionsByStableName[stName] = entry;
}
private forEachData(datas:any, f:(d:any, cs:CompiledScript, libRef:string)=>any)
{
var res = [];
Object.keys(this.libScripts).forEach((lr) => {
if (!datas[lr]) datas[lr] = {};
datas[lr]["libName"] = lr;
res.push(f(datas[lr], this.libScripts[lr], lr));
});
return res;
}
public resetData(datas:any)
{
this.forEachData(datas, (d, cs) => { cs._resetGlobals(d); });
}
public initGlobals(datas:any, rt:Runtime)
{
this.forEachData(datas, (d, cs) => { cs._initGlobals(d, rt); cs._initGlobals2(d); });
//datas.all = {};
// this.forEachData(datas, (d, cs) => {
// cs._initGlobals(datas.all);
// cs._initGlobals2(datas.all);
// });
}
public registerArtResource(clsName:string, id:string, url:string)
{
this.artInitializers.push((data:any) => {
if (!!data[id]) {
// detect API keys
if (clsName === "String_") {
var key = TDev.RT.String_.valueFromKeyUrl(url);
if (key) this.apiKeys[key] = <string>data[id];
}
return;
}
var f = (<any>TDev).RT[clsName].fromArtUrl;
if (!!f)
this.artPromises.push(
f(url).then((v: any) => {
// detect API Keys
if (clsName === "String_") {
var key = TDev.RT.String_.valueFromKeyUrl(url);
if (key) {
// user might not have a key already,
// or might not have an internet connection to load it
this.apiKeys[key] = v;
}
}
// load missing data
if (v === undefined) {
switch (clsName) {
case "Picture":
v = TDev.RT.Picture.fromArtUrl("data:image/jpeg;base64," +
"iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAW7SURBVHhe7ZxbTBxVGMc/7gssdGG5LSIUC6UKTQu1Rg19EDVqmja2SWNTo1HTxMTESLw90PhkUk29PZgYo6kPTXyqDbGJPhgDKW1tLRWsWhrFyp0F2kUuy7VcnG/2bGcG+sAuifufM+eXbPg+CAT2x3fmfDPnnLji423LpIAhXnxUgKCEgKGEgKGEgKGEgKGEgKGEgKGEgKGEgKGEgKGEgKGEgKGEgKGEgKGEgKGEgOFoIdmuxNuv9ESMt8KxQg5X5lL7oa3669LBKirzuMRXYosjhTyQ76aGnXeJjOjopQG6cnNaZLHFcUKK3Mn05WOllBAfp+c/9IzRVx039BgBRwlJ0Bx89kgpeVIS9bx/cp7eONurxyg4SsjLW/NpW26aHi8tL9Ob53poYn5Rz1FwjJCKLBe9tr1AZERf/xmgC/6gyHBwhBAeqj6oLSGXmNoOBOfp2OVBPUbDEUL2bcq+PVQxH7f54YaqMNILSdWq4q0dhSIj6p6Yo8broyLDQ3ohz9+bQwXpSSIj+vTXIVoEXjwrtRCuDp5ZhUGvDkZqIXvvySKvK9RzMMevjkBXByO1kGcrckRENL2wRN9e/1dkuEgrpNKbaplZfd81RuOgMyszcEKe2uihLx4tpVO7y+mZzV7x2cg5UGb93pOdARFhAyXkUIWXPq8rpSdKPHR/vpuO1RZTfbXRXUfC4yUbREQUmF2gi0N4XfmdgBLyYmWeiAwORlEl5R6Xflc3THPfhIjwgRJinhGFcScliGjtPKlVmJmmvnER4QMlpKV/9X9y63DkQ01tYYaIiOYWl6jpDj8XFSghR1sH6MyA8eZ1BGboyE99IlsbyfFxVJ1nzK7aR6ZpRpvy2gXIXbh5qYmUk5pEf43N0sJSZL9ejTbVbdxTITKiE9du0jsXIpMaS6AqJMzIzAJ1jM5ELIOpyjGqg7nonxSRPYAUsh6qc9NFFIKrzE5IJ2RzlrGcZ1LrzDuVkNhSkpEiIqJRrSG0G1IJ4RlWRrLRt3RPzonIPkglxGd6EMX4g7dEZB8kE2LcLmHUkBVjvFr/YsY/PS8i+yDdRd3uSCXEnWT9c3jaazekEpKSYP1z5qPo9GONGrLAUELAkEoIP/sww42i3ZBKSPCWVYi5a7cLasgCQyoh/qC1EfSlWTt3OyCVEF7uY8a8yNouyFUhU9abiRszjVvxdkEqIdwI+qeMYWvl3V87IN1F3Txs8QkNdpv6SiekbcQ4AIBvpdTkWZ+xoyOhEOvCOiUkxvwRmBFRiCqvdVkQOtIJ4VUm5tvuD/rcIrIH0glhWgaMxXG8gJtXw9sFOCE7tDG/YWchfbirWN+8Ew0rV7vX3Z0pInyghPAmzW92l+s7Zw+UhzbvvHRfrvjq2mnun6BF08OpuiJj8w46UEJe3V5A8XHWvuGFKIRwL9I6PCUy0lfDr3y8iwrUb+lLW91Zc3MXDd91GztuuR/ZX5YtMmyghJwdXL1S/fdAdCe9newc1bdCh3luS+SVFgughLz7c7++SSdM1/gcNZyPbm8Hb9I5/Y9RJbwIm/eOoAO3YYePUtqSnaqf+vbLcJBm13H0wracNDq919i80/j3KNW39IgME7grHb//V7UqOa8NX+uRwfDBlk2mHbg8iyvJwH5oZY+pxzr4pN0vIq364uP0mRwy0gv5bUWV7N+UDV0l0gthVlbJ6zU+keHhCCFcJSeuGWfzPq1VyUOgNx0dIYR5v3WQhkzP3N97uJhcPKUDwzFCprS+5Ihpv3rphhQ6XLX6bJVY4xghzI+943RK60XC1Ff79F4FCUcJYd4+10tXboRuxyRpF3g+ejwTaMmp44Tw6RCvNHfRyHToelKkTYE/2lWsxwhAnnXyf1CTm077yrJEFjoTBeGQAccKQcVxQxY6SggYSggYSggYSggYSggYSggYSggYSggYSggYSggYSggYSggYSggYSggYSggURP8B901+vZyEn/4AAAAASUVORK5CYII="
)._value;
break;
case "Sound":
v = TDev.RT.Sound.mk("https://az31353.vo.msecnd.net/pub/pxiraczt");
break;
default:
break;
}
}
data[id] = v
}));
});
}
public registerGlobal(id:string)
{
this.globals.push(id);
}
public initArtAsync(datas:any) : Promise
{
this.apiKeys = {};
return Promise.join(this.forEachData(datas, (d, cs) => cs.initArtCoreAsync(d)));
}
public initArtCoreAsync(data:any) : Promise
{
this.artPromises = [];
this.artInitializers.forEach((f) => { f(data) });
return Promise.join(this.artPromises);
}
public registerEventHandler(category:string, varId:string, entry:any)
{
if (this.eventsByCategory == null)
this.eventsByCategory = {};
var curr = this.eventsByCategory[category];
if (curr === undefined) {
curr = [];
this.eventsByCategory[category] = curr;
}
curr.push(new EventHandlerDesc(varId, entry));
if (!this.eventsByCategory["pause"])
this.registerEventHandler("pause", null,
(prev:IStackFrame, ret:(s:IStackFrame)=>any) => {
var frame:IStackFrame = <any>{};
frame.previous = prev;
frame.rt = prev.rt;
frame.returnAddr = ret;
frame.entryAddr = (s) => {
s.rt.stopAsync(true).done();
return null
};
return frame;
});
if (!this.eventsByCategory["async"])
// will never get called; handled specially
this.registerEventHandler("async", null, null);
}
public registerStep(step:any, name:string)
{
step.idx = this.steps.length;
step.theName = name;
this.steps.push(step);
}
public registerLibRef(libRefName:string, cs:CompiledScript)
{
if (this.libScripts.hasOwnProperty(libRefName))
Util.oops("redefinition of libref " + libRefName);
this.libScripts[libRefName] = cs;
}
public mkLambdaProxy(libs:any, libRefName:string)
{
return (s:IStackFrame) => new LibProxy(libs, s, libRefName, "inline action", null);
}
public mkLibProxyFactory(libs:any, libRefName:string, actionName:string) : (s:IStackFrame) => IStackFrame
{
var f = this.libScripts[libRefName].actionsByName[actionName];
Util.assert(f);
return (s:IStackFrame) => new LibProxy(libs, s, libRefName, actionName, f);
}
public lookupLibPage(libRefName:string, actionName:string) : (s:IStackFrame) => LibProxy
{
var f = this.libScripts[libRefName].actionsByName[actionName];
Util.assert(f);
return (s:IStackFrame) => new LibProxy(this.libBindings[libRefName], s, libRefName, actionName, f);
}
public lookupAction(libName:string, actName:string) { return (<CompiledScript>this.libScripts[libName]).actionsByStableName[actName]; }
static additionalScriptStateFields = ["leaderboard_score", "apikeys_consent", "source_access"];
public init(code:string, missingApis:string[], packageResources : PackageResource[], safe:boolean)
{
this.code = code;
this.missingApis.pushRange(missingApis);
this.packageResources.pushRange(packageResources);
if (safe) {
var f = eval(code);
f(this);
}
}
public reinit(code:string)
{
this.additionalCode = code;
var f = eval(code);
f(this);
}
}
module VisibilityManager {
var hiddenProp = null;
var rt: Runtime = null;
function getHiddenProp(): string {
if (Browser.isNodeJS) return null;
var prefixes = ['webkit', 'moz', 'ms', 'o'];
// if 'hidden' is natively supported just return it
if ('hidden' in document) return 'hidden';
// otherwise loop over all the known prefixes until we find one
for (var i = 0; i < prefixes.length; i++) {
if ((prefixes[i] + 'Hidden') in document)
return prefixes[i] + 'Hidden';
}
// otherwise it's not supported
return null;
}
export function attachToVisibilityChange(runtime: Runtime) {
if (runtime && runtime.testMode)
return;
rt = runtime;
if(!hiddenProp)
hiddenProp = getHiddenProp();
if (hiddenProp) {
Util.log('visibility manager: ' + (!!runtime ? "attach" : "detach"));
var evName = hiddenProp.replace(/[H|h]idden/, '') + 'visibilitychange';
if (rt) document.addEventListener(evName, visibilityChanged, false);
else document.removeEventListener(evName, visibilityChanged, false);
}
}
function visibilityChanged() {
var rt = Runtime.theRuntime;
if (rt && hiddenProp) {
var hidden = document[hiddenProp];
if (hidden) {
Util.log('visibility manager: pausing');
rt.pauseExecution()
}
else {
Util.log('visibility manager: resuming');
rt.resumeExecution();
}
}
}
}
export module RT {
export function unwrapJson(o: JsonObject): any {
return o ? o.value() : undefined;
}
export function wrapJson(o : any) : JsonObject {
return JsonObject.wrap(o);
}
export function queueAction(s: IStackFrame, a: ActionBase, args: any[],
whenDone:(s:IStackFrame)=>void = null,
errorHandler: (err:any, s:IStackFrame)=>void = null) {
if (a) {
var ev = new Event_();
ev.isBlocking = false;
ev.finalCallback = whenDone;
ev.errorHandler = errorHandler;
ev.addHandler(a);
s.rt.queueLocalEvent(ev, args);
}
}
export function protect(s:IStackFrame, f:any):any
{
return s.rt.wrap(s, f)
}
export function userError(msg:string):any
{
Util.userError(msg)
}
export function logError(err: any, meta?: any) {
App.logException(err, meta);
}
export function checkAndLog(err:any, meta?: any):boolean
{
if (!err) return true
logError(err, meta);
return false
}
export function checkAndThrow(e:any)
{
if (!e) return
Util.userError(e + "")
}
export function checkAndResume(s:IStackFrame)
{
return protect(s, function (err) {
checkAndThrow(err);
(<any>s).localResume()
})
}
}
}