/// module TDev { export module Collab { // ------------------ revisionservice collaboration API export interface CollaborationInfo { owner: string; ownerScriptguid: string; group: string; session: string; meta: string; } export function getSessionOwner(session: string): string { return session.substr(0, session.indexOf("0")); } export function getCollaborationsAsync(group: string): Promise { // CollaborationInfo[] var userid = Cloud.getUserId(); if (!userid) return undefined; return Revisions.getRevisionServiceTokenAsync().then((token) => { if (Cloud.isOffline()) return Promise.wrapError("Cloud is offline"); var url = Revisions.revisionservice_http() + "/collaborations/" + group + "?user=" + userid + "&access_token=" + encodeURIComponent(token); return Util.httpRequestAsync(url, "GET", undefined).then((s) => { var a = JSON.parse(s); if (Array.isArray(a)) return a.filter(j => (typeof j === "object")); }); }); } /*export function getCollaborationAsync(owner: string, scriptguid: string): Promise { // CollaborationInfo var sessionid = Revisions.astsessionid(owner, scriptguid); return Revisions.getRevisionServiceTokenAsync().then((token) => { if (Cloud.isOffline()) return Promise.wrapError("Cloud is offline"); var url = Revisions.revisionservice_http() + "/" + sessionid + "/collaboration" + "?user=" + owner + "&access_token=" + encodeURIComponent(token); return Util.httpRequestAsync(url, "GET", undefined).then((s) => { var x = JSON.parse(s); if (typeof x === "object") return x; }); }); }*/ export function startCollaborationAsync(scriptGuid: string, script: string, group: string): Promise { // session id TDev.tick(Ticks.collabStartCollaboration); var userid = Cloud.getUserId(); if (!userid) return undefined; var sessionid = Revisions.make_astsessionid(userid); var ci = {}; ci.owner = userid; ci.group = group; ci.ownerScriptguid = scriptGuid; ci.session = sessionid; ci["script"] = script; return Revisions.getRevisionServiceTokenAsync().then((token) => { if (Cloud.isOffline()) return Promise.wrapError("Cloud is offline"); var url = Revisions.revisionservice_http() + "/" + sessionid + "/collaboration" + "?user=" + userid + "&access_token=" + encodeURIComponent(token); return Util.httpRequestAsync(url, "POST", JSON.stringify(ci)).then( (s) => Editor.updateEditorStateAsync(scriptGuid, (st) => { st.collabSessionId = sessionid; st.groupId = this.publicId; }), (e) => { if (typeof (e) == "object" && e.errorMessage.indexOf("already in group") != -1) ModalDialog.info(lf("can't add twice"), lf("this script has already been added to a group.")); else if(typeof (e) == "object" && e.errorMessage.indexOf("invalid group") != -1) ModalDialog.info(lf("cannot add script to group"), lf("could not access this group.")); else if (typeof (e) == "object" && e.errorMessage.indexOf("not a group member") != -1) ModalDialog.info(lf("cannot add script to group"), lf("you are not a member of this group.")); else throw e; }); }); } export function stopCollaborationAsync(sessionid: string): Promise { // void TDev.tick(Ticks.collabStopCollaboration); var userid = Cloud.getUserId(); if (!userid) return undefined; var owner = Collab.getSessionOwner(sessionid); return Revisions.getRevisionServiceTokenAsync().then((token) => { if (Cloud.isOffline()) return Promise.wrapError("Cloud is offline"); var url = Revisions.revisionservice_http() + "/" + sessionid + "/collaboration" + "?user=" + userid + "&access_token=" + encodeURIComponent(token); return Util.httpRequestAsync(url, "DELETE", undefined).then((s) => { return; }); }); } /// -------- push and pull enable/disable // temporary pull suppression (initially false, controlled from editor while editing in calculator or other buffers) var pullIsTemporarilySuppressed: boolean = false; export function getTemporaryPullSuppression(): boolean { return pullIsTemporarilySuppressed; } export function setTemporaryPullSuppression(val: boolean) { if (pullIsTemporarilySuppressed == val) return; pullIsTemporarilySuppressed = val; if (ready && !val && delayed_pull) { // when suppression is over, pull now processAstTable(); } } // automatic push (initially true, setting is persisted on disk) var enable_automatic_push; export function getAutomaticPushEnabled(): boolean { return enable_automatic_push; } export function setAutomaticPushEnabled(val: boolean) { if (enable_automatic_push == val) return; enable_automatic_push = val; if (ready && val && delayed_push) // when turned on, push now pushAstToCloud(); } // automatic pull (initially true, setting is persisted on disk) var enable_automatic_pull; export function getAutomaticPullEnabled(): boolean { return enable_automatic_pull; } export function setAutomaticPullEnabled(val: boolean) { if (enable_automatic_pull == val) return; enable_automatic_pull = val; if (ready && val && delayed_pull) { // when turned on, pull now processAstTable(); } } /// ---------- chat & presence interface export function registerChangeHandler(handler: () => void) { changehandler = handler; } var changehandler: () => void; export function getConnectedUsers(): Revisions.Participant[]{ if (!AstSession || !AstSession.loaded || AstSession.faulted) return []; return AstSession.user_get_presence(); } export function postMessage(message: string) { TDev.tick(Ticks.collabPostChatMessage); if (!AstSession || !AstSession.loaded || AstSession.faulted) return; var uid = AstSession.user_create_item(ct_chatentry, [], []).uid; var userfield = AstSession.user_get_lval(ct_chattable_user, [uid], []) var timestampfield = AstSession.user_get_lval(ct_chattable_timestamp, [uid], []) var contentfield = AstSession.user_get_lval(ct_chattable_content, [uid], []) AstSession.user_modify_lval(userfield, Cloud.getUserId()); AstSession.user_modify_lval(timestampfield, new Date().toISOString()); AstSession.user_modify_lval(contentfield, message); AstSession.user_push(); } // This function is called by ast.ts, who notifies us when a new // statement becomes active. There's nothing to do if collaboration // isn't active. export function onActivation(stmt: AST.IStableNameEntry) { if (!AstSession || !AstSession.loaded || AstSession.faulted || !getAutomaticPushEnabled()) return; var action = TheEditor.currentAction(); // Happens when editing, say, a record definition, which _is_ an // [AST.Stmt] but isn't an action per se. if (!action) return; var stmtName: string = stmt ? stmt.getStableName() : ""; var actionName: string = action.getStableName(); var myNumber = AstSession.getMemberNumber(); if (myNumber === -1) { Util.log("Session not active yet! Not pushing info"); return; } var ct_lastedit = AstSession.user_get_lval(ct_participantindex_lastedit, [], [myNumber.toString()]); var ct_stmtname = AstSession.user_get_lval(ct_participantindex_stmtname, [], [myNumber.toString()]); var ct_actionname = AstSession.user_get_lval(ct_participantindex_actionname, [], [myNumber.toString()]); AstSession.user_modify_lval(ct_lastedit, new Date().toISOString()); AstSession.user_modify_lval(ct_stmtname, stmtName); AstSession.user_modify_lval(ct_actionname, actionName); AstSession.user_push(); } export interface IMessage { uid: string; user: string; timestamp: Date; content: string; confirmed: boolean; } export interface IParticipantInfo { lastEdit: Date; stmtName: string; actionName: string; sessionId: number; } var msg_expiration_msec = 15 * 60 * 1000; export function getLastTenMessages(): IMessage[]{ if (!AstSession || !AstSession.loaded || AstSession.faulted) return []; var items = AstSession.user_get_items_in_domain(ct_chatentry).sort((a, b) => a.compareTo(b)); var msgarray = []; var currenttime = new Date().getTime(); for (var i = 0; i < items.length; i++) { if (i < items.length - 10) AstSession.user_delete_item(items[i]); // delete all but 10 last messages else { var msg = {}; msg.uid = items[i].uid; var ukeys = [items[i].uid]; var lkeys = []; var timestampfield = AstSession.user_get_lval(ct_chattable_timestamp, ukeys, lkeys); var userfield = AstSession.user_get_lval(ct_chattable_user, ukeys, lkeys); var contentfield = AstSession.user_get_lval(ct_chattable_content, ukeys, lkeys); msg.user = AstSession.user_get_value(userfield); msg.timestamp = new Date(AstSession.user_get_value(timestampfield)); msg.content = AstSession.user_get_value(contentfield); msg.confirmed = AstSession.user_is_datum_confirmed(items[i]); if (currenttime - msg.timestamp.getTime() > msg_expiration_msec) AstSession.user_delete_item(items[i]); else msgarray.push(msg); } } return msgarray; } function getParticipantInfo(aSessionId): IParticipantInfo { var lastEditLval = AstSession.user_get_lval(ct_participantindex_lastedit, [], [aSessionId.toString()]); var stmtNameLval = AstSession.user_get_lval(ct_participantindex_stmtname, [], [aSessionId.toString()]); var actionNameLval = AstSession.user_get_lval(ct_participantindex_actionname, [], [aSessionId.toString()]); // Abort if this is an empty entry. var d = AstSession.user_is_defaultvalue; if (d(lastEditLval) && d(stmtNameLval) && d(actionNameLval)) return null; var lastEdit = AstSession.user_is_defaultvalue(lastEditLval) ? null : new Date(AstSession.user_get_value(lastEditLval)); var stmtName = AstSession.user_get_value(stmtNameLval); var actionName = AstSession.user_get_value(actionNameLval); return { lastEdit: lastEdit, stmtName: stmtName, actionName: actionName, sessionId: aSessionId } } function clearParticipantInfo(aSessionId) { var lastEditLval = AstSession.user_get_lval(ct_participantindex_lastedit, [], [aSessionId.toString()]); var stmtNameLval = AstSession.user_get_lval(ct_participantindex_stmtname, [], [aSessionId.toString()]); var actionNameLval = AstSession.user_get_lval(ct_participantindex_actionname, [], [aSessionId.toString()]); AstSession.user_modify_lval(lastEditLval, ""); AstSession.user_modify_lval(stmtNameLval, ""); AstSession.user_modify_lval(actionNameLval, ""); } export function getActiveParticipants(): IParticipantInfo[] { var connectedUserSet: StringMap = {}; getConnectedUsers().forEach((x: Revisions.Participant) => connectedUserSet[x.sessionId] = true); var r: IParticipantInfo[] = []; var entries = AstSession.user_get_entries_in_indexdomain(ct_participantindex); entries.forEach(function (e) { var sessionId = parseInt(e.lkeys[0]); if (!(sessionId in connectedUserSet)) return; var info = getParticipantInfo(sessionId); if (!info) return; if (!info.lastEdit || (Date.now() - info.lastEdit.getTime()) > msg_expiration_msec) { clearParticipantInfo(sessionId); } else { r.push(info); } }); // Don't forget to push our changes (i.e. outdated messages that we // removed from the index!). AstSession.user_push(); return r; } export function getLastActivity(aUserId): IParticipantInfo { var mostRecent = null; this.getConnectedUsers().forEach(u => { if (u.userId == aUserId) { var info = getParticipantInfo(u.sessionId); if (mostRecent == null || info.lastEdit && mostRecent.lastEdit < info.lastEdit) mostRecent = info; } }); return mostRecent; } // cloud types for snap chat var ct_chatentry = Revisions.Parser.MakeDomain("chat", Revisions.Parser.DOMAIN_DYNAMIC, []); var ct_chattable = Revisions.Parser.MakeDomain("chattable", Revisions.Parser.DOMAIN_STATIC, [ct_chatentry]); var ct_chattable_user = Revisions.Parser.MakeProperty("user", ct_chattable, "string"); var ct_chattable_timestamp = Revisions.Parser.MakeProperty("timestamp", ct_chattable, "string"); var ct_chattable_content = Revisions.Parser.MakeProperty("content", ct_chattable, "string"); // cloud types for participant var ct_participantindex_key = Revisions.Parser.MakeDomain("participant", Revisions.Parser.DOMAIN_BUILTIN, []); var ct_participantindex = Revisions.Parser.MakeDomain("participantindex", Revisions.Parser.DOMAIN_STATIC, [ct_participantindex_key]); var ct_participantindex_lastedit = Revisions.Parser.MakeProperty("lastedit", ct_participantindex, "string"); var ct_participantindex_stmtname = Revisions.Parser.MakeProperty("stmtname", ct_participantindex, "string"); var ct_participantindex_actionname = Revisions.Parser.MakeProperty("actionname", ct_participantindex, "string"); // cloud types for AST merging var cloudtype_delta = Revisions.Parser.MakeDomain("delta", Revisions.Parser.DOMAIN_DYNAMIC, []); var cloudtype_deltatable = Revisions.Parser.MakeDomain("deltatable", Revisions.Parser.DOMAIN_STATIC, [cloudtype_delta]); var cloudtype_pre = Revisions.Parser.MakeProperty("pre", cloudtype_deltatable, "ast"); var cloudtype_post = Revisions.Parser.MakeProperty("post", cloudtype_deltatable, "ast"); var cloudtype_merge = Revisions.Parser.MakeProperty("merge", cloudtype_deltatable, "ast"); var cloudtype_desc = Revisions.Parser.MakeProperty("desc", cloudtype_deltatable, "string"); var cloudtype_stats = Revisions.Parser.MakeProperty("stats", cloudtype_deltatable, "string"); /// ---------------- hooks that are called from editor // called when a new Script is loaded into the Editor export function setCollab(astsession: string) { if (!astsession) { Util.log(">>> Stop Collab! " + Script); astSessionSlot.disconnect(false, "collab turned off"); ready = false; loadPromise = undefined; readyPromise = undefined; } else { var userid = Cloud.getUserId(); // TODO prompt sign in? if (userid) { Util.log(">>> Start Collab! " + astsession); var desc = getAstSessionDescriptor(astsession); ready = false; var p = readyPromise = new PromiseInv(); loadPromise = new PromiseInv(); prevCloudAst = currentCloudAst = undefined; enable_automatic_pull = true; enable_automatic_push = true; astSessionSlot.connect(desc); // calls afterLoad() once file is loaded, before it connects } } } export var readyPromise: PromiseInv; export var loadPromise: PromiseInv; // called immediately after the file is loaded function afterload(): Promise { if (AstSession.faulted) { // skip rest of loading - just be done with it, so the automatic reload can trigger readyPromise.success(undefined); loadPromise.success(false); } else { if (loaduserdata()) { TDev.tick(Ticks.collabResume); Util.log(">>> Resuming collab from file " + astdesc(currentCloudAst)); ready = true; // we are resuming from saved state TDev.TheEditor.undoMgr.pullIntoEditor().then(() => { readyPromise.success(undefined); loadPromise.success(false); }); } else { TDev.tick(Ticks.collabFirstLoad); // need to get initial version from revision server loadPromise.success(true); } } return Promise.as(); } function loaduserdata(): boolean { var pc = Collab.AstSession.user_get_userdata("asts"); if (!pc) return false; prevCloudAst = pc[0]; currentCloudAst = (pc.length > 1) ? pc[1] : pc[0]; enable_automatic_pull = Collab.AstSession.user_get_userdata("enable_automatic_pull"); enable_automatic_push = Collab.AstSession.user_get_userdata("enable_automatic_push"); return true; } function saveuserdata() { Collab.AstSession.user_set_userdata("asts", astEquals(prevCloudAst,currentCloudAst) ? [currentCloudAst] : [prevCloudAst, currentCloudAst], (pc, newpc) => (pc && newpc && pc.length == newpc.length && astEquals(pc[0], newpc[0]) && (!pc[1] || astEquals(pc[1], newpc[1]))) ); Collab.AstSession.user_set_userdata("enable_automatic_pull", enable_automatic_pull); Collab.AstSession.user_set_userdata("enable_automatic_push", enable_automatic_push); } //// -------------- global state export var enableUndo = false; export var AstSession: TDev.Revisions.ClientSession = undefined; // sync control export var ready = false; export var currentCloudAst: string[]; var prevCloudAst: string[]; var numberUnconfirmedDeltas = 0; // flags indicating presence of suppressed pushes or pulls var delayed_pull = false; var delayed_push = false; export function astEquals(ast1: string[], ast2: string[]):boolean { return ast1[0] == ast2[0] || ast1[1] === ast2[1]; } function randomsuffix(): string { var d = new Date(); var ms = d.getMilliseconds(); return String.fromCharCode("a".charCodeAt(0) + ms % 26) + String.fromCharCode("a".charCodeAt(0) + Math.floor(ms / 26) % 26); } export function astdesc(ast: string[]): string { return ast[0] + "(" + ast[1].length + ")"; } function onDoorBell() { if (!AstSession) { return; } if (AstSession.marooned) { var s = AstSession; AstSession.log("discard cache because it is marooned"); Script.editorState.collabSessionId = undefined; ModalDialog.infoAsync("Project Discontinued", "This project has been discontinued. You can continue to edit the script, but it will no longer synchronize with other team members.") .thenalways(() => TDev.TheEditor.goToHubAsync()).done(); astSessionSlot.disconnect(true, "project discontinued"); // deletes the file from disk } else if (AstSession.faulted) { AstSession.log("local cache is corrupted - deleting"); ModalDialog.infoAsync("Cache Corrupted", "Sorry... we encountered a problem with the stored project state. Please try again to get the latest state from the server.") .thenalways(() => TDev.TheEditor.goToHubAsync()).done(); return astSessionSlot.disconnect(true, "cache corrupted"); // deletes the file from disk } else { //TODO detect permission problems as well if (!AstSession.user_yield() && ready) return; processAstTable(); if (changehandler) changehandler(); } } export function recordAst(ast: string) : any { Util.assert(ready); TDev.tick(Ticks.collabRecordAst); var newast = [randomsuffix(), ast]; if (astEquals(currentCloudAst, newast)) return currentCloudAst; Util.log(">>> recordAst " + astdesc(newast)); currentCloudAst = newast; saveuserdata(); var wentout = pushAstToCloud(); if (!wentout) { //TODO : make pending changes visible //AstSession.user_push(); // AstSession.user_modify_lval(AstSession.user_get_lval(ct_participantindex_blockedpushes, [], [AstSession.getMemberNumber().toString()]), "A1"); } else { // clear unsaved changes } return newast; } export function pushAstToCloud(): boolean { Util.assert(ready); if (!enable_automatic_push || numberUnconfirmedDeltas >= 2) { delayed_push = true; return false; } delayed_push = false; numberUnconfirmedDeltas++; if (!astEquals(prevCloudAst, currentCloudAst)) { var desc = astdesc(prevCloudAst) + ", " + astdesc(currentCloudAst); Util.log(">>> pushAstToCloud: " + desc); var item = AstSession.user_create_item(cloudtype_delta, [], []); AstSession.user_modify_lval(AstSession.user_get_lval(cloudtype_pre, [item.uid], []), prevCloudAst); AstSession.user_modify_lval(AstSession.user_get_lval(cloudtype_post, [item.uid], []), currentCloudAst); AstSession.user_modify_lval(AstSession.user_get_lval(cloudtype_desc, [item.uid], []), desc); prevCloudAst = currentCloudAst; saveuserdata(); AstSession.user_push(); } return true; } export function processAstTable() { var items = AstSession.user_get_items_in_domain(cloudtype_delta).sort((a, b) => a.compareTo(b)); if (items.length < 1) { Util.assert(!ready); Util.log(">>> processAstTable (not ready)"); return; // have not received initial prefix from server yet } else if (!ready) { Util.log(">>> processAstTable first time, (" + items.length + ")"); } else Util.log(">>> processAstTable (" + items.length + ")"); var pos = 0; var cur = items[pos]; var cloud_ast = AstSession.user_get_value(AstSession.user_get_lval(cloudtype_merge, [cur.uid], [])); Util.assert(cloud_ast); var lkeys = []; // go through unmerged confirmed delta entries; delete all but last, and enter merge results var pos = 1; while (pos < items.length && AstSession.user_is_datum_confirmed(items[pos])) { AstSession.user_delete_item(items[pos - 1]); var ukeys = [items[pos].uid]; var merge_lval = AstSession.user_get_lval(cloudtype_merge, ukeys, lkeys); var stats_lval = AstSession.user_get_lval(cloudtype_stats, ukeys, lkeys); Util.assert(!AstSession.user_get_value(merge_lval)); var pre = AstSession.user_get_value(AstSession.user_get_lval(cloudtype_pre, ukeys, lkeys)); var post = AstSession.user_get_value(AstSession.user_get_lval(cloudtype_post, ukeys, lkeys)); cloud_ast = mergeAsts(pre, post, cloud_ast, (data) => AstSession.user_modify_lval(stats_lval, JSON.stringify(data))); AstSession.user_modify_lval(merge_lval, cloud_ast); pos++; } numberUnconfirmedDeltas = items.length - pos; // if automatic pull is off, this is all and we stop here if (ready && (!enable_automatic_pull || pullIsTemporarilySuppressed)) { delayed_pull = true; return; } else delayed_pull = false; // go through unconfirmed delta entries while (pos < items.length) { Util.assert(!AstSession.user_is_datum_confirmed(items[pos])); var ukeys = [items[pos].uid]; var pre = AstSession.user_get_value(AstSession.user_get_lval(cloudtype_pre, ukeys, lkeys)); var post = AstSession.user_get_value(AstSession.user_get_lval(cloudtype_post, ukeys, lkeys)); cloud_ast = mergeAsts(pre, post, cloud_ast); pos++; } if (!ready) { // first time currentCloudAst = cloud_ast; prevCloudAst = cloud_ast; saveuserdata(); ready = true; Util.log(">>> collab is ready " + astdesc(currentCloudAst)); TDev.TheEditor.undoMgr.pullIntoEditor().then(() => { readyPromise.success(undefined); }); } else { // merge with local delta (prev,current) var m = mergeAsts(prevCloudAst, currentCloudAst, cloud_ast); if (!astEquals(prevCloudAst, cloud_ast)) { prevCloudAst = cloud_ast; saveuserdata(); } if (!astEquals(currentCloudAst, m)) { currentCloudAst = m; saveuserdata(); } TDev.TheEditor.undoMgr.pullIntoEditor(); } // potentially push things that were delayed earlier if (delayed_push) pushAstToCloud(); } /// ------------------- the actual merge function export var testMode = true; function versionname(ast: string[]) { var full = ast[0]; var pos = full.indexOf("="); return (pos != -1) ? full.substr(0, pos) : full; } function mergeAsts(o_ast: string[], a_ast: string[], b_ast: string[], datacollector?: (IMergeData) => void) { // take shortcuts based on merge function equivalences if (astEquals(o_ast, b_ast) // easy merge: deltas are consecutive edits || astEquals(b_ast, a_ast)) // easy merge: identical change { return a_ast; } var os = o_ast[1]; var bs = b_ast[1]; var as = a_ast[1]; TDev.tick(Ticks.collabRealMerge); var name = randomsuffix(); var mergedesc = "m(" + versionname(o_ast) + "," + versionname(a_ast) + "," + versionname(b_ast) + ")"; var timer1 = Util.perfNow(); var b = (TDev).AST.Parser.parseScript(bs); var o = (TDev).AST.Parser.parseScript(os); var a = (TDev).AST.Parser.parseScript(as); // (TDev).AST.TypeChecker.tcApp(t1); // (TDev).AST.TypeChecker.tcApp(t2); // (TDev).AST.TypeChecker.tcApp(t3); // (TDev).AST.TypeChecker.tcApp(t4); //var bss = b.serialize(); //var oss = o.serialize(); //var ass = a.serialize(); //if (t1ss !== t1s) debugger; //if (t2ss !== t2s) debugger; //if( t3ss !== t3s) debugger; //if( t4ss !== t4s) debugger; (TDev).TheEditor.initIds(b); (TDev).TheEditor.initIds(o); (TDev).TheEditor.initIds(a); var timer2 = Util.perfNow(); //console.log(">> merging: \n" + t3.serialize() + "\n---------\n" + t4.serialize() + "\n-----------\n" + t2.serialize()); var merged = (TDev).AST.Merge.merge3(o, a, b, datacollector); var mergeds = merged.serialize(); Util.assert(merged.things.length > 0 || a.things.length == 0 || b.things.length == 0); // TODO XXX - do we need to update the ancestors somehow? //console.log(">> merging: \n" + t3.serialize() + "\n---------\n" + t4.serialize() + "\n-----------\n" + t2.serialize()); // if we are in testing mode, record results and test equivalences if (testMode) var record = JSON.stringify({ "O": os, "A": as, "B": bs, "actual": mergeds }); return [name + "=" + mergedesc, mergeds]; } // session context functions var astSessionSlot = new Revisions.Slot( { url_ws: () => Revisions.revisionservice_http().replace("http", "ws"), url_http: Revisions.revisionservice_http, tokensource: Revisions.getRevisionServiceTokenAsync, clearCachedData: clearCachedData, updateStatus: updateStatus, createSession: createSession, onDoorBell: onDoorBell, afterload: afterload }, () => AstSession, (cs?: TDev.Revisions.ClientSession) => { AstSession = cs; ready = false; currentCloudAst = undefined; prevCloudAst = undefined; } ); function updateStatus() { } function clearCachedData() { } function createSession(original: Revisions.ISessionParams): Revisions.ClientSession { var si = new Revisions.ClientSession(original.servername, original.localname, original.user); si.permissions = ""; si.title = ""; si.script = ""; si.readonly = false; si.user = original.user; return si; } function getAstSessionDescriptor(session: string): Revisions.ISessionParams { var desc = {}; desc.servername = session; desc.localname = session; desc.permissions = ""; desc.readonly = false; desc.title = ""; desc.user = Cloud.getUserId(); desc.nodeserver = ""; desc.script = ""; return desc; } } }