1185 строки
61 KiB
TypeScript
1185 строки
61 KiB
TypeScript
///<reference path='refs.ts'/>
|
|
|
|
|
|
module TDev {
|
|
export module World {
|
|
export function log(s: string) { Util.log("World: " + s); }
|
|
// Filled in from [editor/default.ts]; expects [s] to be a
|
|
// *touchdevelop* script text, not an external editor.
|
|
export var getScriptMeta : (s:string) => any;
|
|
export var sanitizeScriptTextForCloud : (s:string) => string;
|
|
export var waitForUpdate = (id:string) => false;
|
|
|
|
// for now disable merge on sync in the lite cloud
|
|
export var disableMerge = true;
|
|
|
|
// this is so that the Editor can react to changes made by sync
|
|
// state is:
|
|
// "uploaded" - when a new version was sent to the cloud, and we got a new snapshotId
|
|
// "published" - after a script is published
|
|
// "downloaded" - after a merge, or just plain download
|
|
// "skippedMerge" - like "downloaded", but we kept the local version
|
|
export var newHeaderCallbackAsync = (h:Cloud.Header, state:string) => Promise.as();
|
|
// this is called before we attempt a merge; the editor should save the state if the guid matches and display
|
|
// a progress overlay until newHeaderCallbackAsync({ guid: guid }, "downloaded" or "skippedMerge") is called
|
|
export var incomingHeaderAsync = (guid:string) => Promise.as();
|
|
|
|
var currentUserInfo:any = null;
|
|
var currentUserPromise = new PromiseInv();
|
|
var localStorage = window.localStorage;
|
|
var getIndexTablePromise = () => Storage.getTableAsync("Index");
|
|
var getScriptsTablePromise = () => Storage.getTableAsync("Scripts");
|
|
var getTracesTablePromise = () => Storage.getTableAsync("Traces");
|
|
|
|
interface SyncData {
|
|
indexTable: Storage.Table;
|
|
scriptsTable: Storage.Table;
|
|
installedHeaders: Cloud.InstalledHeaders;
|
|
recentUses: any;
|
|
downloaded: any[];
|
|
removed: any[];
|
|
uptodates: any[];
|
|
uploaded: any[];
|
|
keys: string[];
|
|
scriptVersionsInCloudItems: any;
|
|
items: any;
|
|
progress: any;
|
|
}
|
|
|
|
export interface ScriptStub {
|
|
// Either "touchdevelop", or another one (external). This is unlike
|
|
// [Cloud.Header] where [editor] is either undefined, or a string
|
|
// (meaning external editor).
|
|
editorName: string;
|
|
// When the editor is "touchdevelop", these are the same value that
|
|
// can be obtained by running [getScriptMeta] on [scriptText].
|
|
scriptName: string;
|
|
scriptComment?: string;
|
|
// When the editor is "touchdevelop", initially contains a template, then
|
|
// gets mutated by [newScriptAsync] with extra meta information before
|
|
// being saved to storage. When the editor is external, remains blank.
|
|
scriptText: string;
|
|
}
|
|
|
|
|
|
function getHeader(body: Cloud.Body) : Cloud.Header {
|
|
var x = JSON.parse(JSON.stringify(body));
|
|
delete x.script;
|
|
return x;
|
|
}
|
|
function removeInstalledAsync(indexTable: Storage.Table, scriptsTable: Storage.Table, guid: string) : Promise {
|
|
var headerItem = {}
|
|
headerItem[guid] = undefined;
|
|
var bodyItem = {}
|
|
bodyItem[guid + "-script"] = undefined;
|
|
bodyItem[guid + "-scriptState"] = undefined;
|
|
bodyItem[guid + "-scriptVersionInCloud"] = undefined;
|
|
return Promise.join([indexTable.setItemsAsync(headerItem), scriptsTable.setItemsAsync(bodyItem)]);
|
|
}
|
|
|
|
var pendingMetaItems = [];
|
|
|
|
function setUnInstalledAsync(
|
|
indexTable: Storage.Table,
|
|
scriptsTable: Storage.Table,
|
|
header: Cloud.Header)
|
|
{
|
|
return setInstalledAsync(indexTable, scriptsTable, uninstall(header), <any>0, <any>0, <any>0, <any>0)
|
|
}
|
|
|
|
|
|
function setInstalledAsync(
|
|
indexTable: Storage.Table,
|
|
scriptsTable: Storage.Table,
|
|
header: Cloud.Header,
|
|
script: string,
|
|
editorState: string,
|
|
scriptState: string,
|
|
cloudScriptVersion: string
|
|
) : Promise {
|
|
var headerItem = {}
|
|
// In the case of a regular script, we can recover the metadata from
|
|
// the script body. In the case of an external editor, we demand
|
|
// that the caller properly set the metadata.
|
|
if (script && !header.editor && (!header.meta || header.meta.comment === undefined)) {
|
|
header.meta = getScriptMeta(script);
|
|
if (Cloud.lite) {
|
|
// will try to store the meta
|
|
pendingMetaItems.push({
|
|
guid: header.guid,
|
|
meta: header.meta
|
|
})
|
|
}
|
|
}
|
|
if (header.editor && (!header.meta || !header.meta.name)) {
|
|
Util.log("ERROR pre-condition not met for [setInstalledAsync]; bailing");
|
|
debugger;
|
|
return Promise.as();
|
|
}
|
|
if (header.meta)
|
|
header.name = header.meta.name;
|
|
headerItem[header.guid] = JSON.stringify(header);
|
|
var bodyItem = {}
|
|
// protz: I believe we can get rid of this assert now that we have
|
|
// external scripts that may start out null...?
|
|
// Util.assert(script !== "")
|
|
if (script != null)
|
|
bodyItem[header.guid + "-script"] = typeof script === "string" ? script : undefined;
|
|
if (editorState != null)
|
|
bodyItem[header.guid + "-editorState"] = typeof editorState === "string" ? editorState : undefined;
|
|
if (scriptState !== null)
|
|
bodyItem[header.guid + "-scriptState"] = typeof scriptState === "string" ? scriptState : undefined;
|
|
if (cloudScriptVersion != null)
|
|
bodyItem[header.guid + "-scriptVersionInCloud"] = typeof cloudScriptVersion === "string" ? cloudScriptVersion : undefined;
|
|
|
|
log(header.guid + "/" + header.scriptId + ": " + header.name + " save with base " + header.scriptVersion.baseSnapshot);
|
|
|
|
return Promise.join([indexTable.setItemsAsync(headerItem), scriptsTable.setItemsAsync(bodyItem)]);
|
|
}
|
|
|
|
function setCloudScriptVersionAsync(scriptsTable: Storage.Table, guid: string, cloudScriptVersion: string) : Promise {
|
|
var bodyItem = {}
|
|
bodyItem[guid + "-scriptVersionInCloud"] = typeof cloudScriptVersion === "string" ? cloudScriptVersion : undefined;
|
|
return scriptsTable.setItemsAsync(bodyItem);
|
|
}
|
|
|
|
export function mergeJSON(base:any, local:any, server:any)
|
|
{
|
|
Object.keys(server).forEach(k => {
|
|
if (server[k] && typeof server[k] === "object" && local[k] && typeof local[k] == "object")
|
|
local[k] = mergeJSON(base[k] || {}, local[k], server[k])
|
|
else if (!local.hasOwnProperty(k) || base[k] === local[k])
|
|
local[k] = server[k]
|
|
})
|
|
|
|
return local
|
|
}
|
|
|
|
function mergeEditorStates(base:string, local:string, server:string)
|
|
{
|
|
return JSON.stringify(mergeJSON(JSON.parse(base || "{}"), JSON.parse(local || "{}"), JSON.parse(server || "{}")))
|
|
}
|
|
|
|
export var mergeScripts = (base:string, local:string, server:string) => local;
|
|
|
|
export function getScriptBlobAsync(snapshotId:string)
|
|
{
|
|
return Util.httpGetJsonAsync(Cloud.config.workspaceUrl + snapshotId)
|
|
}
|
|
|
|
export interface ScriptBlob
|
|
{
|
|
script: string;
|
|
editorState: string;
|
|
extra?: any;
|
|
}
|
|
|
|
// [header] is coming from the cloud; we need to update our local
|
|
// storage to merge data from the cloud
|
|
function downloadInstalledAsync(indexTable: Storage.Table, scriptsTable: Storage.Table, header: Cloud.Header) : Promise {
|
|
log(header.guid + "/" + header.scriptId + ": " + header.name + " is newer");
|
|
|
|
if (Cloud.lite) {
|
|
var theirs:ScriptBlob;
|
|
var baseVer:ScriptBlob;
|
|
var currVer:ScriptBlob;
|
|
var hd:Cloud.Header; // local header
|
|
var skipMsg = false
|
|
|
|
return getScriptBlobAsync(header.scriptVersion.baseSnapshot)
|
|
.then(v => theirs = v)
|
|
.then(() => incomingHeaderAsync(header.guid))
|
|
.then(() => indexTable.getValueAsync(header.guid))
|
|
.then(str => hd = str ? JSON.parse(str) : null)
|
|
.then(() => {
|
|
// [hd] is the local header; if the [instanceId] is "cloud", then no local modifications were performed, i.e. the
|
|
// local header is *exactly* baseSnapshot
|
|
var touch = () => {
|
|
header.scriptVersion.instanceId = Cloud.getWorldId()
|
|
header.scriptVersion.time = getCurrentTime();
|
|
header.scriptVersion.version++;
|
|
header.status = "unpublished";
|
|
}
|
|
if (hd && hd.scriptVersion.instanceId != "cloud" && hd.scriptVersion.baseSnapshot && hd.scriptVersion.baseSnapshot != header.scriptVersion.baseSnapshot) {
|
|
if (disableMerge) {
|
|
touch()
|
|
skipMsg = true
|
|
// setInstalledAsync() will not update to null
|
|
return <ScriptBlob>{ script: null, editorState: null }
|
|
}
|
|
// We need to merge, because there's been a fork. The base header is [hd.scriptVersion.baseSnapshot], "theirs" is
|
|
// [header], and "mine" is [hd].
|
|
log(header.guid + "/" + header.scriptId + ": " + header.name + " merging based on " + hd.scriptVersion.baseSnapshot);
|
|
return getScriptBlobAsync(hd.scriptVersion.baseSnapshot)
|
|
.then(r => { baseVer = r })
|
|
// Note: the [guid] is the same for both [header] and [hd]. The line below is getting the local script.
|
|
.then(() => incomingHeaderAsync(header.guid))
|
|
.then(() => scriptsTable.getItemsAsync([header.guid + "-script", header.guid + "-editorState"]))
|
|
.then(r => { currVer = { script: r[header.guid + "-script"], editorState: r[header.guid + "-editorState"] } })
|
|
.then(() => {
|
|
if (header.editor) {
|
|
var ret:ScriptBlob = {
|
|
script: currVer.script,
|
|
editorState: currVer.editorState,
|
|
// This must be exactly an <External.PendingMerge>
|
|
extra: {
|
|
theirs: {
|
|
scriptText: theirs.script,
|
|
editorState: theirs.editorState,
|
|
baseSnapshot: header.scriptVersion.baseSnapshot,
|
|
metadata: header.meta,
|
|
},
|
|
base: {
|
|
scriptText: baseVer.script,
|
|
editorState: baseVer.editorState,
|
|
baseSnapshot: hd.scriptVersion.baseSnapshot,
|
|
metadata: hd.meta,
|
|
},
|
|
}
|
|
};
|
|
// Don't update the header: merely record the fact that we've seen a new version go by from the cloud,
|
|
// and record in the extra field the contents of that version (so that we don't have to hit the cloud
|
|
// again to get it later on).
|
|
var newVersion = header.scriptVersion.baseSnapshot;
|
|
header = hd; // FIXME properly pass a value instead of updating in-place, so that we don't have to bind ret before
|
|
header.pendingMerge = newVersion;
|
|
return ret;
|
|
} else {
|
|
// Our new header is the one that we took in from the cloud, except that some modifications were
|
|
// performed. Hence, we modify the [instanceId] so that it no longer says "cloud". Since the
|
|
// [baseSnapshot] is still the one from the cloud header, this means that we've been creating a new
|
|
// version *on top of* the cloud header. This new version has not been synced to the cloud, and
|
|
// therefore does not have a [baseSnapshot] yet.
|
|
touch()
|
|
return <ScriptBlob>{
|
|
script: mergeScripts(baseVer.script, currVer.script, theirs.script),
|
|
editorState: mergeEditorStates(baseVer.editorState, currVer.editorState, theirs.editorState),
|
|
}
|
|
}
|
|
})
|
|
} else {
|
|
return theirs
|
|
}
|
|
})
|
|
.then(resp =>
|
|
setInstalledAsync(indexTable, scriptsTable, header, resp.script, resp.editorState, null, JSON.stringify(resp.extra || {})))
|
|
.then(() => newHeaderCallbackAsync(header, skipMsg ? "skippedMerge" : "downloaded"))
|
|
.then(() => header.scriptVersion.instanceId == "cloud" ? Promise.as() : uploadInstalledAsync(indexTable, scriptsTable, header))
|
|
}
|
|
|
|
return Cloud.getUserInstalledBodyAsync(header.guid).then(function (installedBodies: Cloud.InstalledBodies) {
|
|
var body = <Cloud.Body>undefined;
|
|
installedBodies.bodies.forEach(function (b) { if (b.guid == header.guid) body = b; });
|
|
if (body) {
|
|
var cloudScriptVersion = JSON.stringify(header.scriptVersion);
|
|
if (body.status == "published")
|
|
return ScriptCache.getScriptAsync(body.scriptId)
|
|
.then((script) => script == null // transient download error?
|
|
? Promise.as() // ignore
|
|
: script == "" // published script deleted in cloud? (rare, but possible)
|
|
? setUnInstalledAsync(indexTable, scriptsTable, getHeader(body))
|
|
: setInstalledAsync(indexTable, scriptsTable, getHeader(body), script, body.editorState, null, cloudScriptVersion));
|
|
else if (body.script == "") // unpublished script deleted in cloud? (not sure how possible, but observed in practice)
|
|
return setUnInstalledAsync(indexTable, scriptsTable, getHeader(body));
|
|
else
|
|
return setInstalledAsync(indexTable, scriptsTable, getHeader(body), body.script, body.editorState, null, cloudScriptVersion);
|
|
}
|
|
else
|
|
return removeInstalledAsync(indexTable, scriptsTable, header.guid);
|
|
});
|
|
}
|
|
|
|
function publishInstalledAsync(indexTable: Storage.Table, scriptsTable: Storage.Table, header: Cloud.Header) : Promise {
|
|
log(header.guid + "/" + header.scriptId + ": " + header.name + " is to be published");
|
|
var hidden = header.publishAsHidden
|
|
return indexTable.getValueAsync(header.guid)
|
|
.then(resp => { header = JSON.parse(resp) })
|
|
.then(() => Cloud.postUserInstalledPublishAsync(header.guid, hidden, JSON.stringify(header.scriptVersion), header.meta))
|
|
.then(function (installedBodies: Cloud.InstalledBodies) {
|
|
var body = <Cloud.Body>undefined;
|
|
installedBodies.bodies.forEach(function (b) { if (b.guid == header.guid) body = b; });
|
|
if (!body) return undefined;
|
|
var cloudScriptVersion = JSON.stringify(header.scriptVersion);
|
|
// do not delete state on publication; make sure we don't override body with ""
|
|
var hd = getHeader(body)
|
|
return setInstalledAsync(indexTable, scriptsTable, hd, body.script || null, body.editorState || null, null, cloudScriptVersion)
|
|
.then(() => newHeaderCallbackAsync(hd, "published"))
|
|
.then(() => []) // non-null result
|
|
})
|
|
.then((r) => r, (e) => {
|
|
if (e.status == 400) {
|
|
ModalDialog.info("cannot publish",
|
|
lf("Your script '{0}' cannot be published. Error message: {1:not available}", header.name, e.errorMessage))
|
|
getInstalledHeaderAsync(header.guid).then((header:Cloud.Header) => {
|
|
if (header.status == "tobepublished") {
|
|
header.status = "unpublished";
|
|
return setInstalledAsync(indexTable, scriptsTable, header, null, null, null, null)
|
|
} else return Promise.as();
|
|
}).done();
|
|
}
|
|
throw e;
|
|
});
|
|
}
|
|
|
|
function uploadInstalledAsync(indexTable: Storage.Table, scriptsTable: Storage.Table, header: Cloud.Header): Promise { // of PostUserInstalledResponse
|
|
// A conservative estimate of the version we are saving. We compare all three fields at
|
|
// the same time. (It may be the case that in-between the various asynchronous steps
|
|
// below, a newer version gets written and it's innocuous, but we err on the safe side.)
|
|
var conservativeVersion = JSON.stringify(header.scriptVersion);
|
|
log(header.guid + "/" + header.scriptId + ": " + header.name + " is dirty, attempting to save version " + conservativeVersion);
|
|
if (header.pendingMerge) {
|
|
log(header.guid + "/" + header.scriptId + ": " + header.name + " is pending merge resolution, skipping");
|
|
return Promise.as();
|
|
}
|
|
return Promise.join({
|
|
script: scriptsTable.getValueAsync(header.guid + "-script"),
|
|
editorState: scriptsTable.getValueAsync(header.guid + "-editorState")
|
|
}).then(function (data) {
|
|
var body = <Cloud.Body>JSON.parse(JSON.stringify(header));
|
|
if (!Cloud.lite && body.status == "published")
|
|
body.script = "";
|
|
else if ((Cloud.lite && body.status == "published") || body.status == "unpublished" || body.status == "tobepublished") {
|
|
body.script = sanitizeScriptTextForCloud(data.script);
|
|
if (body.status == "tobepublished")
|
|
body.status = "unpublished";
|
|
}
|
|
else
|
|
body.script = undefined;
|
|
body.editorState = data.editorState;
|
|
if (Cloud.lite && disableMerge) {
|
|
body.scriptVersion.baseSnapshot = "*"
|
|
}
|
|
return Cloud.postUserInstalledAsync(<Cloud.InstalledBodies>{ bodies: [body] })
|
|
.then(resp => {
|
|
if (Cloud.lite && !resp.numErrors) {
|
|
var header = resp.headers[0]
|
|
if (!header.editor && body.script)
|
|
header.meta = getScriptMeta(body.script)
|
|
// [setInstalledAsync] is not interrupted until it performs the
|
|
// actual call to [setItemsAsync], so that's the right time to check
|
|
// whether the version has changed in the meanwhile. This check
|
|
// assumes that all clients of the [World] module are well-behaved
|
|
// and always call [updateInstalledAsync], which takes care of
|
|
// bumping the version number in a monotonic fashion.
|
|
return getInstalledHeaderAsync(header.guid).then((h: Cloud.Header) => {
|
|
var currentVersion = JSON.stringify(h.scriptVersion);
|
|
// This should be equal or greater than currentVersion. Anything
|
|
// else means I've missed something!
|
|
log("actually saving? version is now "+currentVersion);
|
|
if (currentVersion != conservativeVersion) {
|
|
// Someone wrote a new version in local storage; so all we
|
|
// remember is that this local version now needs to be saved on
|
|
// top of the newer version that's in the cloud. Client code
|
|
// must retry to save.
|
|
h.scriptVersion.baseSnapshot = resp.headers[0].scriptVersion.baseSnapshot;
|
|
resp.retry = true;
|
|
return setInstalledAsync(indexTable, scriptsTable, h, null, null, null, null)
|
|
.then(() => resp)
|
|
} else {
|
|
// That header in the cloud is fine, that's our new header.
|
|
return setInstalledAsync(indexTable, scriptsTable, header, null, null, null, null)
|
|
.then(() => resp)
|
|
}
|
|
})
|
|
.then(resp => newHeaderCallbackAsync(resp.headers[0], "uploaded").then(() => resp));
|
|
}
|
|
return Promise.as(resp)
|
|
})
|
|
});
|
|
}
|
|
|
|
function recentUsesInstalledAsync(headers: Cloud.Header[]): Promise { // of PostUserInstalledResponse
|
|
return Cloud.postUserInstalledAsync(<Cloud.InstalledBodies>{ recentUses: headers.map(h => <Cloud.RecentUse>{ guid: h.guid, recentUse: h.recentUse }) });
|
|
}
|
|
var syncVersion = undefined;
|
|
var continuouslySyncVersion = undefined;
|
|
var syncCount = 0;
|
|
export function cancelSync() {
|
|
syncVersion = undefined;
|
|
}
|
|
export function syncIsActive()
|
|
{
|
|
return !!syncVersion;
|
|
}
|
|
export function cancelContinuouslySync() {
|
|
if (continuouslySyncVersion) {
|
|
continuouslySyncVersion = undefined;
|
|
cancelSync();
|
|
}
|
|
}
|
|
|
|
var updateCache:any = null
|
|
var addUpdates:any = {}
|
|
export var onNewNotificationChanged: (newNotifications: number) => void = undefined;
|
|
|
|
export var _askEmail: boolean = false;
|
|
export var _askToEnableEmailNewsletter: boolean = false;
|
|
export var _askToEnableEmailNotifications: boolean = false;
|
|
export var _askToEnableNotifications: boolean = false;
|
|
export var _profileIndex: number = 0;
|
|
export var _profileCount: number = 0;
|
|
|
|
export function continuouslySyncAsync(m: boolean, onSyncedAsync: () => Promise = undefined) {
|
|
if (continuouslySyncVersion) return Promise.as(); // continuouslySync still going on
|
|
return internalContinuouslySyncAsync(m, onSyncedAsync, continuouslySyncVersion = new Object());
|
|
}
|
|
function internalContinuouslySyncAsync(m: boolean, onSyncedAsync: () => Promise , myContinuouslySyncVersion: any) {
|
|
var v = lastV;
|
|
if (myContinuouslySyncVersion == continuouslySyncVersion && v)
|
|
return syncAsync(false, v, m).then(() => {
|
|
var p = Promise.as();
|
|
if (myContinuouslySyncVersion == continuouslySyncVersion) {
|
|
if (onSyncedAsync && v != lastV) p = p.then(() => onSyncedAsync());
|
|
p = p.then(() => Promise.delay(2000, () =>
|
|
internalContinuouslySyncAsync(m, onSyncedAsync, myContinuouslySyncVersion)));
|
|
}
|
|
return p;
|
|
});
|
|
else
|
|
return Promise.as();
|
|
}
|
|
|
|
export function getCurrentUserInfoAsync()
|
|
{
|
|
if (currentUserInfo) return Promise.as(currentUserInfo)
|
|
else return currentUserPromise;
|
|
}
|
|
|
|
var lastV;
|
|
export function syncAsync(uploadRecentUses: boolean = true, v: number = undefined, m: boolean = false,
|
|
onNotLoggedIn: () => void = undefined,
|
|
onBadTime: (number) => void = undefined,
|
|
onAskBeta: () => void = undefined,
|
|
onAskSomething: (AskSomething) => void = undefined,
|
|
onNoOtherAsk: () => void = undefined): Promise // of string --- undefined: success; some text: we hit an error (no internet, not yet logged in, too much posted...); you can try again later
|
|
{
|
|
var totalCounter = TDev.RT.Perf.start("syncasync", true);
|
|
var last = TDev.RT.Perf.start("startsync");
|
|
function time(name: string) {
|
|
if (!v) TDev.RT.Perf.stop(last);
|
|
last = TDev.RT.Perf.start(name);
|
|
}
|
|
if (syncCount > 0 && window.applicationCache.status == window.applicationCache.IDLE) {
|
|
try {
|
|
window.applicationCache.update();
|
|
} catch (e) {
|
|
}
|
|
}
|
|
time("appcache");
|
|
|
|
syncCount++;
|
|
|
|
if (Cloud.isOffline()) {
|
|
var message = lf("cannot sync - you appear to be offline");
|
|
HTML.showProgressNotification(v ? undefined : message);
|
|
return Promise.as(message);
|
|
}
|
|
time("isonline");
|
|
|
|
var mySyncVersion = new Object();
|
|
syncVersion = mySyncVersion;
|
|
var canceled = Promise.wrapError("canceled");
|
|
log("starting sync");
|
|
|
|
localStorage.removeItem("editorScriptToSaveDirty");
|
|
|
|
var pendingDownloads = 0;
|
|
var pendingUploads = 0;
|
|
var pendingPublishs = 0;
|
|
var progress = function (deltaDownloads: number, deltaUploads: number, deltaPublishs: number) {
|
|
if (syncVersion != mySyncVersion) return;
|
|
pendingDownloads += deltaDownloads;
|
|
pendingUploads += deltaUploads;
|
|
pendingPublishs += deltaPublishs;
|
|
var a = []
|
|
if (pendingDownloads) a.push(lf("{0} down", pendingDownloads));
|
|
if (pendingUploads) a.push(lf("{0} up", pendingUploads));
|
|
if (pendingPublishs) a.push(lf("{0} publish", pendingPublishs));
|
|
var s = lf("syncing...");
|
|
if (a.length > 0) {
|
|
s += " (" + a.join(", ") + ")";
|
|
HTML.showProgressNotification(s, false);
|
|
}
|
|
};
|
|
progress(0, 0, 0);
|
|
|
|
var tobepublished = []
|
|
var newerHeaders = []
|
|
var deletedHeaders = []
|
|
var uptodates = []
|
|
var dirtyHeaders = [];
|
|
|
|
var askBeta = false;
|
|
var askSomething = null;
|
|
return (v ? Cloud.getUserInstalledLongAsync(v, m) : Cloud.getUserInstalledAsync()).then(installedHeaders =>
|
|
{
|
|
if (syncVersion != mySyncVersion) return canceled;
|
|
return Promise.join({
|
|
installedHeaders: installedHeaders,
|
|
indexTable: getIndexTablePromise(),
|
|
scriptsTable: getScriptsTablePromise()
|
|
});
|
|
}, e =>
|
|
{
|
|
if (e.status == 204 || // NoContent
|
|
false)
|
|
// This triggers randomly on flaky connections and such
|
|
// We're very rarely really not logged in nowadays
|
|
// v && Cloud.isOnline() && /localhost/.test(document.URL) && e.status == 0 // because of CORS on localhost when not logged in yet
|
|
{
|
|
mySyncVersion = new Object();
|
|
return canceled;
|
|
}
|
|
throw e;
|
|
}).then(function (data: SyncData) {
|
|
time("opendb+user/installed");
|
|
if (syncVersion != mySyncVersion) return canceled;
|
|
var user = data.installedHeaders.user;
|
|
if (user) {
|
|
currentUserInfo = user;
|
|
if (currentUserPromise) {
|
|
currentUserPromise.success(user);
|
|
currentUserPromise = null;
|
|
}
|
|
}
|
|
var min = data.installedHeaders.minimum;
|
|
lastV = data.installedHeaders.v;
|
|
if (min && Cloud.currentReleaseId && min < Cloud.currentReleaseId) {
|
|
if (waitForUpdate(min)) {
|
|
syncVersion = new Object()
|
|
return;
|
|
}
|
|
}
|
|
if (onNewNotificationChanged) onNewNotificationChanged(data.installedHeaders.newNotifications);
|
|
if (Runtime.offerNotifications()) {
|
|
var notifications = data.installedHeaders.notifications;
|
|
if (Runtime.refreshNotifications) Runtime.refreshNotifications(notifications);
|
|
_askToEnableNotifications = !notifications;
|
|
}
|
|
_askEmail = !data.installedHeaders.email;
|
|
_askToEnableEmailNewsletter = !data.installedHeaders.emailNewsletter;
|
|
_askToEnableEmailNotifications = !data.installedHeaders.emailNotifications;
|
|
_profileIndex = data.installedHeaders.profileIndex || 0;
|
|
_profileCount = data.installedHeaders.profileCount || 0;
|
|
if (data.installedHeaders.blobcontainer)
|
|
Cloud.config.workspaceUrl = data.installedHeaders.blobcontainer
|
|
Random.addCloudEntropy(data.installedHeaders.random)
|
|
if (!Cloud.lite && data.installedHeaders.time) {
|
|
var now = new Date().getTime();
|
|
var seconds = (now - data.installedHeaders.time * 1000) / 1000;
|
|
if (Math.abs(seconds) > 180) {
|
|
HTML.showProgressNotification(v ? undefined : "syncing canceled.");
|
|
syncVersion = undefined;
|
|
if (onBadTime) onBadTime(seconds);
|
|
return;
|
|
}
|
|
}
|
|
if (data.installedHeaders.askBeta && World.switchToChannel) askBeta = true;
|
|
askSomething = data.installedHeaders.askSomething;
|
|
updateCache = {}
|
|
data.installedHeaders.headers.forEach((hd) => {
|
|
if (hd.updateId && hd.scriptId != hd.updateId && hd.updateTime > hd.scriptTime)
|
|
updateCache[hd.scriptId] = hd.updateId;
|
|
});
|
|
localStorage["updateCacheForInstalled"] = JSON.stringify(updateCache);
|
|
data.keys = <any>data.indexTable.getKeysAsync();
|
|
return Promise.join(data);
|
|
}).then(function (data: SyncData) {
|
|
time("readdb1");
|
|
if (syncVersion != mySyncVersion) return canceled;
|
|
data.items = data.indexTable.getItemsAsync(data.keys)
|
|
if (!Cloud.lite)
|
|
data.scriptVersionsInCloudItems = data.scriptsTable.getItemsAsync(data.keys.map((guid) => guid + "-scriptVersionInCloud"));
|
|
return Promise.join(data);
|
|
}).then(function (data/*: SyncData*/) {
|
|
time("readdb2");
|
|
if (syncVersion != mySyncVersion) return canceled;
|
|
var recentUses = []
|
|
var newerOrDeletedGuids: any = {};
|
|
(<SyncData>data).installedHeaders.headers.forEach(function (header) {
|
|
var existingItem = data.items[header.guid];
|
|
var isNewer = true;
|
|
if (existingItem) {
|
|
var existingHeader = <Cloud.Header>JSON.parse(existingItem);
|
|
if (Cloud.lite) {
|
|
isNewer = header.scriptVersion.baseSnapshot != existingHeader.scriptVersion.baseSnapshot
|
|
&& header.scriptVersion.baseSnapshot != existingHeader.pendingMerge;
|
|
if (existingHeader.status == "deleted")
|
|
isNewer = false
|
|
if (!isNewer && existingHeader.scriptVersion.instanceId == "cloud" &&
|
|
header.status == "published" && existingHeader.status == "unpublished")
|
|
isNewer = true;
|
|
} else
|
|
isNewer = Cloud.isVersionNewer(header.scriptVersion, existingHeader.scriptVersion);
|
|
if (header.recentUse < existingHeader.recentUse)
|
|
recentUses.push(existingHeader);
|
|
}
|
|
if (isNewer) {
|
|
newerOrDeletedGuids[header.guid] = true;
|
|
if (header.status === "deleted") {
|
|
if (existingItem)
|
|
deletedHeaders.push(header);
|
|
} else {
|
|
newerHeaders.push(header);
|
|
}
|
|
}
|
|
else {
|
|
var cloudScriptVersion = JSON.stringify(header.scriptVersion);
|
|
var uptodate = Promise.as();
|
|
if (!Cloud.lite && data.scriptVersionsInCloudItems[header.guid + "-scriptVersionInCloud"] !== cloudScriptVersion)
|
|
uptodate = uptodate.then(() => setCloudScriptVersionAsync(data.scriptsTable, header.guid, cloudScriptVersion));
|
|
if (header.recentUse > existingHeader.recentUse)
|
|
uptodate = uptodate.then(() => recentUseAsync(data.indexTable, header.guid, header.recentUse));
|
|
uptodates.push(uptodate)
|
|
}
|
|
});
|
|
(<SyncData>data).keys.forEach(function (key) {
|
|
var header = <Cloud.Header>JSON.parse(data.items[key]);
|
|
if (newerOrDeletedGuids[header.guid]) return;
|
|
if (header.status === "tobepublished") tobepublished.push(header);
|
|
var isDirty = true;
|
|
if (Cloud.lite) {
|
|
isDirty = header.scriptVersion.instanceId != "cloud"
|
|
} else {
|
|
var s = data.scriptVersionsInCloudItems[key + "-scriptVersionInCloud"];
|
|
if (s) {
|
|
var cloudScriptVersion = <Cloud.Version>JSON.parse(s);
|
|
if (!Cloud.isVersionNewer(header.scriptVersion, cloudScriptVersion)) isDirty = false;
|
|
}
|
|
}
|
|
if (isDirty) dirtyHeaders.push(header);
|
|
});
|
|
log(recentUses.length + " items with newer recentUses");
|
|
log(newerHeaders.length + " newer items to download, " + deletedHeaders.length + " deleted items");
|
|
log(dirtyHeaders.length + " items to upload");
|
|
|
|
progress(newerHeaders.length, dirtyHeaders.length, tobepublished.length);
|
|
if (recentUses.length > 0 && uploadRecentUses)
|
|
data.recentUses = recentUsesInstalledAsync(recentUses);
|
|
return Promise.join(data);
|
|
}).then(function (data/*: SyncData*/) {
|
|
// It's unclear how [data] is used from then on, because it is
|
|
// just discarded two steps below. Perhaps we assign the
|
|
// properties to prevent some promises from being
|
|
// garbage-collected until we move on to the next step?
|
|
time("diff");
|
|
pendingMetaItems = [];
|
|
data.downloaded = Promise.thenEach(newerHeaders, (h: Cloud.Header) => {
|
|
if (syncVersion != mySyncVersion) return canceled;
|
|
return downloadInstalledAsync(data.indexTable, data.scriptsTable, h).then(() => { progress(-1, 0, 0) });
|
|
});
|
|
data.removed = Promise.thenEach(deletedHeaders, (h: Cloud.Header) => {
|
|
if (syncVersion != mySyncVersion) return canceled;
|
|
return removeInstalledAsync(data.indexTable, data.scriptsTable, h.guid);
|
|
});
|
|
data.uploaded = Promise.thenEach(dirtyHeaders, (h: Cloud.Header) => {
|
|
if (syncVersion != mySyncVersion) return canceled;
|
|
return uploadInstalledAsync(data.indexTable, data.scriptsTable, h).then(() => { progress(0, -1, 0) });
|
|
});
|
|
data.uptodates = Promise.join(uptodates);
|
|
return Promise.join(data);
|
|
}).then(function (data/*: SyncData*/) {
|
|
time("download+upload");
|
|
if (syncVersion != mySyncVersion) return canceled;
|
|
data.tobepublished = Promise.thenEach(tobepublished, (header) => publishInstalledAsync(data.indexTable, data.scriptsTable, header).then(result => {
|
|
progress(0, 0, -1);
|
|
if (!result)
|
|
ModalDialog.info(lf("publishing failed"), lf("There was a versioning mismatch between your local state and the cloud. Please check the content of the script you want to publish and then try again."));
|
|
}));
|
|
data.progress = Cloud.postPendingProgressAsync();
|
|
// This is performance optimization - after import from the old cloud script entries have no meta and thus meta
|
|
// has to be re-generated on every sync on a new device. Here, we'll push meta to the cloud.
|
|
if (Cloud.lite && pendingMetaItems.length > 0) {
|
|
data.postmeta = Cloud.postUserInstalledAsync(<any>{ metas: pendingMetaItems })
|
|
}
|
|
return Promise.join(data);
|
|
}).then(() => {
|
|
time("publish");
|
|
if (!v) TDev.RT.Perf.stop(totalCounter);
|
|
if (syncVersion != mySyncVersion) return;
|
|
syncVersion = undefined;
|
|
HTML.showProgressNotification(v ? undefined : "syncing done", true, 0, 1000);
|
|
if (askBeta && onAskBeta && !/localhost/.test(window.location.href)) onAskBeta();
|
|
else if (askSomething && onAskSomething) onAskSomething(askSomething);
|
|
else if (onNoOtherAsk) onNoOtherAsk();
|
|
return undefined;
|
|
}, function (e) {
|
|
if (syncVersion != mySyncVersion) return;
|
|
syncVersion = undefined;
|
|
var status = e.status
|
|
var errorMessage = e.errorMessage
|
|
if (!status) Object.keys(e).forEach(k => {
|
|
var f = e[k];
|
|
if (!f) return;
|
|
if (f.status) status = f.status;
|
|
if (f.errorMessage) errorMessage = f.errorMessage;
|
|
if (typeof f == "object")
|
|
Object.keys(f).forEach(l => {
|
|
var g = f[l];
|
|
if (!g) return;
|
|
if (g.status) status = g.status;
|
|
if (g.errorMessage) errorMessage = g.errorMessage;
|
|
});
|
|
});
|
|
var info = "";
|
|
if (status || errorMessage)
|
|
info = " (code " + status + (errorMessage ? (": " + errorMessage) : "") + ")";
|
|
Util.log('nosync: ' + info);
|
|
if (Util.navigatingAway) {
|
|
HTML.showProgressNotification(undefined);
|
|
return undefined;
|
|
} else if (status == 442 && Cloud._migrate) {
|
|
Cloud._migrate();
|
|
return undefined;
|
|
} else if (status == 400) {
|
|
var message = lf("Cloud precondition violated") + info;
|
|
HTML.showProgressNotification(message)
|
|
return message;
|
|
}
|
|
else if (status == 503) {
|
|
var message = lf("Did you post a lot recently? You must wait for one hour before you can post more.") + info;
|
|
HTML.showProgressNotification(message);
|
|
return message;
|
|
}
|
|
else if (status == 403)
|
|
//(Cloud.isOnline() && /localhost/.test(document.URL)) // because of CORS on localhost when not logged in yet
|
|
{
|
|
var message = status == 403
|
|
? Cloud.hasAccessToken()
|
|
? onNotLoggedIn
|
|
? lf("cannot sync - your access token has expired and will renew automatically") + info
|
|
: lf("cannot sync - your access token has expired") + info
|
|
: lf("cannot sync - you are not signed in") + info
|
|
: lf("cannot sync") + info;
|
|
HTML.showProgressNotification(message)
|
|
|
|
if (status == 403) {
|
|
if (!onNotLoggedIn)
|
|
Cloud.handlePostingError(e, lf("sync"))
|
|
else
|
|
Cloud.accessTokenExpired();
|
|
}
|
|
if (onNotLoggedIn) onNotLoggedIn();
|
|
World.cancelContinuouslySync(); // stop continuously script
|
|
return message;
|
|
}
|
|
else if (!Cloud.isTouchDevelopOnline()) {
|
|
var message = lf("cannot sync - you are in offline mode");
|
|
HTML.showProgressNotification(v ? undefined : message);
|
|
return message;
|
|
} else {
|
|
var message = lf("cannot sync - are you offline?") + info;
|
|
HTML.showProgressNotification(v ? undefined : message);
|
|
return message;
|
|
}
|
|
// TDev.World.log("ERROR" + (!!e ? JSON.stringify(e) : "undefined"));
|
|
});
|
|
}
|
|
export function saveAsync(guid: string, onNotLoggedIn: () => void = undefined, onBadTime: (number) => void = undefined): Promise // of PostUserInstalledResponse
|
|
{
|
|
if (!Cloud.getUserId() || Cloud.isOffline()) {
|
|
Util.log('save skipped: not auth or offline');
|
|
return Promise.as();
|
|
}
|
|
|
|
var mySyncVersion = new Object();
|
|
syncVersion = mySyncVersion;
|
|
var canceled = Promise.wrapError("canceled");
|
|
log("starting save");
|
|
|
|
return Promise.join({
|
|
indexTable: getIndexTablePromise(),
|
|
scriptsTable: getScriptsTablePromise(),
|
|
header: getInstalledHeaderAsync(guid)
|
|
}).then(function (data) {
|
|
if (syncVersion != mySyncVersion) return;
|
|
return uploadInstalledAsync(data.indexTable, data.scriptsTable, data.header);
|
|
}).then(function (result) {
|
|
if (syncVersion != mySyncVersion) return;
|
|
syncVersion = undefined;
|
|
HTML.showSaveNotification(result.numErrors ? lf("problems saving!") : lf("saved"));
|
|
return result;
|
|
}, function (e) {
|
|
if (syncVersion != mySyncVersion) return;
|
|
syncVersion = undefined;
|
|
var status = e.status
|
|
var errorMessage = e.errorMessage
|
|
var info = "";
|
|
if (status || errorMessage)
|
|
info = " (code " + status + (errorMessage ? (": " + errorMessage) : "") + ")";
|
|
if (status == 400)
|
|
throw new Error("Cloud precondition violated" + info);
|
|
else if (status == 403 ||
|
|
(!Cloud.lite && Cloud.isOnline() && /localhost/.test(document.URL))) // because of CORS on localhost when not logged in yet
|
|
{
|
|
HTML.showSaveNotification(lf("could not save - you are not signed in ({0:500})",status));
|
|
if (status == 403)
|
|
Cloud.accessTokenExpired();
|
|
if (onNotLoggedIn) onNotLoggedIn();
|
|
}
|
|
else
|
|
HTML.showSaveNotification(lf("cannot back up to cloud - you appear to be offline"));
|
|
});
|
|
}
|
|
function uninstall(header: Cloud.Header) {
|
|
header.scriptVersion = <Cloud.Version>{ instanceId: Cloud.getWorldId(), version: 2147483647, time: 253402300799, baseSnapshot: header.scriptVersion.baseSnapshot };
|
|
header.status = "deleted";
|
|
return header;
|
|
}
|
|
export function uninstallAsync(guid: string) : Promise // of void
|
|
{
|
|
if (!Util.check(!!guid)) return Promise.as(undefined);
|
|
log("starting uninstall of " + guid);
|
|
return Promise.join({
|
|
indexTable: getIndexTablePromise(),
|
|
scriptsTable: getScriptsTablePromise(),
|
|
}).then(function (data/*: SyncData*/) {
|
|
data.items = data.indexTable.getItemsAsync([guid]);
|
|
return Promise.join(data);
|
|
}).then(function (data/*: SyncData*/) {
|
|
var h = data.items[guid];
|
|
if (!h) return undefined; // already uninstalled?
|
|
var header = <Cloud.Header>JSON.parse(h);
|
|
return setUnInstalledAsync(data.indexTable, data.scriptsTable, header);
|
|
});
|
|
}
|
|
export function publishAsync(guid: string, hidden:boolean) : Promise // of void
|
|
{
|
|
if (!Util.check(!!guid)) return Promise.as(undefined);
|
|
log("starting publishing of " + guid);
|
|
return Promise.join({
|
|
indexTable: getIndexTablePromise()
|
|
}).then(function (data/*: SyncData*/) {
|
|
data.items = data.indexTable.getItemsAsync([guid]);
|
|
return Promise.join(data);
|
|
}).then(function (data/*: SyncData*/) {
|
|
var h = data.items[guid];
|
|
if (!h) return undefined; // uninstalled?
|
|
var header = <Cloud.Header>JSON.parse(h);
|
|
if (header.status !== "unpublished") return undefined;
|
|
header.status = "tobepublished";
|
|
header.publishAsHidden = hidden;
|
|
var headerItem = {};
|
|
headerItem[header.guid] = JSON.stringify(header);
|
|
return data.indexTable.setItemsAsync(headerItem);
|
|
});
|
|
}
|
|
export function recentUseAsync(indexTable: any, guid: string, recentUse: number) : Promise // of void
|
|
{
|
|
if (!Util.check(!!guid)) return Promise.as(undefined);
|
|
return Promise.join({
|
|
items: indexTable.getItemsAsync([guid])
|
|
}).then(function (data/*: SyncData*/) {
|
|
var h = data.items[guid];
|
|
if (!h) return undefined; // uninstalled?
|
|
var header = <Cloud.Header>JSON.parse(h);
|
|
if (header.recentUse < recentUse) header.recentUse = recentUse;
|
|
var headerItem = {};
|
|
headerItem[header.guid] = JSON.stringify(header);
|
|
return indexTable.setItemsAsync(headerItem);
|
|
});
|
|
}
|
|
export function getCurrentTime() {
|
|
return Math.floor(new Date().getTime()/1000);
|
|
}
|
|
|
|
export function stripHeaderForSave(hd:Cloud.Header): Cloud.Header
|
|
{
|
|
return <any>{
|
|
cloudId: Cloud.config.cloudId,
|
|
scriptId: hd.scriptId,
|
|
userId: Cloud.isRestricted() ? undefined : hd.userId,
|
|
status: hd.status,
|
|
// store date of last modification, not most recent use
|
|
lastUse: hd.scriptVersion ? hd.scriptVersion.time : getCurrentTime(),
|
|
editor: hd.editor || "touchdevelop",
|
|
name: hd.meta.name,
|
|
comment: hd.meta.comment,
|
|
}
|
|
}
|
|
|
|
export function installFromSaveAsync(hd:Cloud.Header, scriptText: string): Promise // of Cloud.Header
|
|
{
|
|
var tm = (<any>hd).lastUse || hd.recentUse || getCurrentTime();
|
|
tm = Util.intBetween(1350000000, tm, getCurrentTime());
|
|
if (!hd.meta) hd.meta = {}
|
|
var name = hd.name || hd.meta.name || "no name"
|
|
var comment = (<any>hd).comment || hd.meta.comment || ""
|
|
var h = <Cloud.Header>(<any>{
|
|
status: hd.status === "published" ? "published" : "unpublished",
|
|
scriptId: hd.scriptId || "",
|
|
userId: hd.userId || (hd.scriptId ? "bogususerid" : ""),
|
|
name: name,
|
|
meta: {
|
|
name: name,
|
|
comment: comment,
|
|
},
|
|
scriptVersion: <Cloud.Version>{
|
|
instanceId: Cloud.getWorldId(),
|
|
version: 0,
|
|
time: tm,
|
|
baseSnapshot: ""
|
|
},
|
|
guid: Util.guidGen(),
|
|
editor: hd.editor == "touchdevelop" ? "" : hd.editor,
|
|
recentUse: getCurrentTime(),
|
|
});
|
|
if (!/^\w{1,50}$/.test(h.editor))
|
|
h.editor = "";
|
|
if (!/^[a-z]{1,20}$/.test(h.scriptId)) {
|
|
h.scriptId = "";
|
|
h.userId = "";
|
|
}
|
|
if (h.userId && !/^[a-z]{1,20}$/.test(h.userId))
|
|
h.userId = "bogususerid";
|
|
|
|
if (!h.editor) h.meta = null; // force recompute of meta
|
|
return Promise.join({
|
|
indexTable: getIndexTablePromise(),
|
|
scriptsTable: getScriptsTablePromise(),
|
|
}).then(function (data/*: SyncData*/) {
|
|
return setInstalledAsync(data.indexTable, data.scriptsTable, h, scriptText, null, null, null).then(() => h);
|
|
});
|
|
}
|
|
|
|
function installAsync(status: string, scriptId: string, userId: string, stub: ScriptStub) : Promise // of Cloud.Header
|
|
{
|
|
var meta;
|
|
if (stub.editorName == "touchdevelop") {
|
|
meta = getScriptMeta(stub.scriptText);
|
|
// This is mandatory: since [setInstalledAsync] uses the
|
|
// [scriptText] to save data, there's no way we can switch to a
|
|
// different name at this stage.
|
|
Util.assert(meta.name.trim() == stub.scriptName.trim());
|
|
// For compatibility with old cloud entries, we now switch to
|
|
// the semantics "falsy [editor] field for [Cloud.Header] means
|
|
// TouchDevelop editor".
|
|
stub.editorName = "";
|
|
} else {
|
|
meta = {
|
|
localGuid: Util.guidGen(),
|
|
name: stub.scriptName,
|
|
comment: stub.scriptComment,
|
|
};
|
|
}
|
|
var h = <Cloud.Header>(<any>{
|
|
status: status,
|
|
scriptId: scriptId,
|
|
userId: userId,
|
|
meta: meta,
|
|
name: meta.name,
|
|
scriptVersion: <Cloud.Version>{instanceId: Cloud.getWorldId(), version: 0, time: getCurrentTime(), baseSnapshot: "" },
|
|
guid: meta.localGuid,
|
|
editor: stub.editorName,
|
|
});
|
|
Util.assert(!!h.guid);
|
|
return Promise.join({
|
|
indexTable: getIndexTablePromise(),
|
|
scriptsTable: getScriptsTablePromise(),
|
|
}).then(function (data/*: SyncData*/) {
|
|
return setInstalledAsync(data.indexTable, data.scriptsTable, h, stub.scriptText, null, null, null).then(() => h);
|
|
});
|
|
}
|
|
export function installPublishedAsync(scriptId: string, userId: string) : Promise // of Cloud.Header
|
|
{
|
|
if (!Util.check(!!scriptId)) return Promise.as(undefined);
|
|
return getInstalledAsync().then(function (items) {
|
|
var guids = Object.keys(items);
|
|
var matchingGuids = guids.filter(function (guid) {
|
|
var item = items[guid];
|
|
return item.status == "published" && item.scriptId == scriptId;
|
|
});
|
|
if (matchingGuids.length > 0)
|
|
return items[matchingGuids[0]];
|
|
return Promise.join({
|
|
text: ScriptCache.getScriptAsync(scriptId),
|
|
json: (<any>Browser).TheApiCacheMgr.getAsync(scriptId, true),
|
|
}).then(data => {
|
|
var text: string = data.text;
|
|
var json = data.json;
|
|
if (!text) {
|
|
HTML.showErrorNotification("cannot get script /" + scriptId);
|
|
return new PromiseInv(); // stall
|
|
} else {
|
|
return installAsync("published", scriptId, userId, {
|
|
scriptText: text,
|
|
// This is a script stub that uses the different
|
|
// convention
|
|
editorName: json.editor || "touchdevelop",
|
|
scriptName: json.name,
|
|
scriptComment: json.description,
|
|
});
|
|
}
|
|
});
|
|
});
|
|
}
|
|
export function installUnpublishedAsync(baseScriptId: string, baseUserId: string, stub: ScriptStub) : Promise // of Cloud.Header
|
|
{
|
|
return installAsync("unpublished", baseScriptId, baseUserId, stub);
|
|
}
|
|
export function getInstalledAsync() : Promise // yields object whose keys are guids, and the values are Headers
|
|
{
|
|
return Promise.join({
|
|
indexTable: getIndexTablePromise(),
|
|
scriptsTable: getScriptsTablePromise(),
|
|
}).then(function (data/*: SyncData*/) {
|
|
data.keys = data.indexTable.getKeysAsync();
|
|
return Promise.join(data);
|
|
}).then(function (data: SyncData) {
|
|
return data.indexTable.getItemsAsync(data.keys).then((items) => Promise.thenEach(items, (v) => JSON.parse(v)));
|
|
});
|
|
}
|
|
export function getInstalledHeaderAsync(guid: string) : Promise // of Cloud.Header
|
|
{
|
|
if (!Util.check(!!guid)) return Promise.as(undefined);
|
|
return getIndexTablePromise().then((indexTable) => indexTable.getValueAsync(guid)).then((s) => s ? JSON.parse(s) : undefined);
|
|
}
|
|
export function getInstalledScriptAsync(guid: string) : Promise // of string (script text)
|
|
{
|
|
if (!Util.check(!!guid)) return Promise.as(undefined);
|
|
return getScriptsTablePromise().then((scriptsTable) => scriptsTable.getValueAsync(guid + "-script"));
|
|
}
|
|
export function getInstalledEditorStateAsync(guid: string) : Promise // of string (script text)
|
|
{
|
|
if (!Util.check(!!guid)) return Promise.as(undefined);
|
|
return getScriptsTablePromise().then((scriptsTable) => scriptsTable.getValueAsync(guid + "-editorState"));
|
|
}
|
|
export function getInstalledScriptVersionInCloud(guid: string) : Promise // of string
|
|
{
|
|
if (!Util.check(!!guid)) return Promise.as(undefined);
|
|
return getScriptsTablePromise().then((scriptsTable) => scriptsTable.getValueAsync(guid + "-scriptVersionInCloud"));
|
|
}
|
|
export function getAnyScriptAsync(guid: string) : Promise // of string (script text)
|
|
{
|
|
if (/-/.test(guid)) return getInstalledScriptAsync(guid);
|
|
else return ScriptCache.getScriptAsync(guid);
|
|
}
|
|
export function getInstalledScriptsAsync(guids: string[]) : Promise // of guid => string (script text)
|
|
{
|
|
return getScriptsTablePromise().then((scriptsTable) =>
|
|
scriptsTable.getItemsAsync(guids.map((g) => g + "-script")).then((map) => {
|
|
var r = {}
|
|
guids.forEach((g) => {
|
|
r[g] = map[g + "-script"]
|
|
})
|
|
return r;
|
|
})
|
|
);
|
|
}
|
|
|
|
export function getScriptRestoreAsync(guid:string) : Promise
|
|
{
|
|
var d = null
|
|
|
|
return Promise.join({
|
|
indexTable: getIndexTablePromise(),
|
|
scriptsTable: getScriptsTablePromise(),
|
|
}).then(function (data/*: SyncData*/) {
|
|
d = data
|
|
return Promise.join([
|
|
data.indexTable.getValueAsync(guid),
|
|
data.scriptsTable.getItemsAsync([
|
|
guid + "-scriptVersionInCloud",
|
|
guid + "-editorState",
|
|
guid + "-script"
|
|
])])
|
|
}).then(resp => {
|
|
var hd = {}
|
|
hd[guid] = resp[0]
|
|
var entries = resp[1]
|
|
return () => Promise.join([d.indexTable.setItemsAsync(hd), d.scriptsTable.setItemsAsync(entries)])
|
|
})
|
|
}
|
|
|
|
export function setInstalledScriptAsync(
|
|
header: Cloud.Header,
|
|
script: string,
|
|
editorState: string,
|
|
scriptState: string = null,
|
|
scriptVersionInCloud = null
|
|
) : Promise // of void
|
|
{
|
|
if (!Util.check(!!header)) return Promise.as(undefined);
|
|
log("setting " + header.guid);
|
|
return Promise.join({
|
|
indexTable: getIndexTablePromise(),
|
|
scriptsTable: getScriptsTablePromise(),
|
|
}).then(function (data/*: SyncData*/) {
|
|
return setInstalledAsync(data.indexTable, data.scriptsTable, header, script, editorState, scriptState, scriptVersionInCloud);
|
|
});
|
|
}
|
|
|
|
/*
|
|
export function triggerCrash(id:string)
|
|
{
|
|
getScriptsTablePromise().then(tbl => {
|
|
var s = {}
|
|
s[id + "-script"] = ""
|
|
return tbl.setItemsAsync(s);
|
|
}).done();
|
|
}
|
|
*/
|
|
|
|
function initUpdateCache()
|
|
{
|
|
if (!updateCache) {
|
|
var s = localStorage["updateCacheForInstalled"]
|
|
updateCache = s ? JSON.parse(s) : {}
|
|
}
|
|
}
|
|
|
|
export function rememberUpdate(id:string, update:string)
|
|
{
|
|
addUpdates[id] = update;
|
|
}
|
|
|
|
export function updateFor(h:Cloud.Header)
|
|
{
|
|
initUpdateCache();
|
|
if (h && h.status == "published") return updateCache[h.scriptId] || addUpdates[h.scriptId] || null;
|
|
return null;
|
|
}
|
|
|
|
export function updateAsync(guid:string)
|
|
{
|
|
var id = "";
|
|
return getInstalledHeaderAsync(guid).then((h:Cloud.Header) => {
|
|
id = updateFor(h);
|
|
if (!id) return Promise.as();
|
|
return ScriptCache.getScriptAsync(id);
|
|
}).then((text) => {
|
|
if (!text) return Promise.as();
|
|
return getInstalledHeaderAsync(guid).then((h:Cloud.Header) => {
|
|
if (h.status != "published") return Promise.as();
|
|
h.status = "published";
|
|
h.scriptId = id;
|
|
h.meta = null // recompute
|
|
return updateInstalledScriptAsync(h, text, null, true)
|
|
})
|
|
})
|
|
}
|
|
|
|
export function updateInstalledScriptAsync(hd:Cloud.Header, script:string, state:string, background = false, scriptVersionInCloud = "")
|
|
{
|
|
if (!background) {
|
|
hd.status = "unpublished";
|
|
hd.recentUse = getCurrentTime();
|
|
}
|
|
hd.scriptVersion.instanceId = Cloud.getWorldId()
|
|
hd.scriptVersion.time = getCurrentTime();
|
|
hd.scriptVersion.version++;
|
|
if (!hd.editor)
|
|
hd.meta = null // recompute
|
|
return World.setInstalledScriptAsync(hd, script, state, "", scriptVersionInCloud)
|
|
}
|
|
|
|
export var switchToChannel = (ch:string) => {
|
|
if (ch == "beta")
|
|
Util.navigateInWindow(Cloud.getServiceUrl() + "/app/beta?nocache=" + Util.guidGen());
|
|
else
|
|
Util.navigateInWindow(Cloud.getServiceUrl() + "/app/");
|
|
};
|
|
}
|
|
|
|
}
|