/// // openDatabase("TDev", "1.0", "TouchDevelop database", 5 * 1024 * 1024).transaction(function (tx) { tx.executeSql("DELETE FROM TABLESessions;"); }); module TDev { export module Storage { function fatalError(e: any, op: string = undefined) { Util.navigateInWindow((window).errorUrl + "#storageinit," + encodeURIComponent(op)); } function versionError(e: any) { Util.navigateInWindow((window).errorUrl + "#storageversion"); } function tagError(e: any, origin: string, willRetry: boolean = false) { e.isDatabaseError = true; e.databaseOrigin = origin; Util.log("database error" + (willRetry ? " (will retry)" : " (no retry)") + ": " + Util.getErrorInfo(e)); return e; } export interface Table { getValueAsync(key: string): Promise; // of string getItemsAsync(keys: string[]): Promise; // of Object getKeysAsync(): Promise; // of string[] setItemsAsync(items: any): Promise; // of void } // indicates if the database initialization failed and the changes are only stored in memory // this mode should only be allowed if no database was ever opened in this browser export var temporary = false; var temporaryRequestedSignin = false; export function showTemporaryWarning() { if (temporary) { if (Cloud.isAccessTokenExpired()) HTML.showWarningNotification(lf("Web site data not available, sign in to back up your scripts!"), lf("Your browser does not allow TouchDevelop to store data. This usually happens if run in Private Mode (Safari), in InPrivate mode (Internet Explorer) or your security settings prevent data storage. Sign in or change your browser settings to avoid loosing your work. All the changes not saved to the cloud will be lost when leaving this page.")); else if (Cloud.isOffline()) HTML.showWarningNotification(lf("Web site data not available, connect to internet to back up your scripts!"), lf("Your browser does not allow TouchDevelop to store data. This usually happens if run in Private Mode (Safari), in InPrivate mode (Internet Explorer) or your security settings prevent data storage. Sign in or change your browser settings to avoid loosing your work. All the changes not saved to the cloud will be lost when leaving this page.")); return true; } else { return false; } } // uses an in-memory object or localstorage var memoryStorage: any = {}; class MemoryTable implements Table { public getValueAsync(key: string): Promise { var v = memoryStorage[this.tableName + "-" + key]; if (typeof v !== "string") v = undefined; return Promise.as(v); } public getItemsAsync(keys: string[]): Promise { var items = {}; keys.forEach((k) => { var v = memoryStorage[this.tableName + "-" + k]; if (typeof v !== "string") v = undefined; items[k] = v; }); return Promise.as(items); } public getKeysAsync(): Promise { var prefix = this.tableName + "-"; var results = [] Object.keys(memoryStorage).forEach(k => { if (k.length > prefix.length && k.slice(0, prefix.length) === prefix) results.push(k.slice(prefix.length)); }); return Promise.as(results); } public setItemsAsync(items: any): Promise { Object.keys(items).forEach((k) => { var v = items[k]; if (typeof v === "string") memoryStorage[this.tableName + "-" + k] = v; else memoryStorage.removeItem(this.tableName + "-" + k); }); return Promise.as(); } constructor(public tableName: string) { Storage.temporary = true; } } export function createMemoryTable(tableName: string): Table { return new MemoryTable(tableName); } /* class StorageTable implements Table { static localStorage = window.localStorage; public getValueAsync(key: string): Promise { var v = localStorage[this.tableName + "-" + key]; if (typeof v !== "string") v = undefined; return Promise.as(v); } public getItemsAsync(keys: string[]): Promise { var items = {}; keys.forEach((k) => { var v = localStorage[this.tableName + "-" + k]; if (typeof v !== "string") v = undefined; items[k] = v; }); return Promise.as(items); } public getKeysAsync(): Promise { var len = localStorage.length; var prefix = this.tableName + "-"; var results = [] for (var i = 0; i < len; ++i) { var k = localStorage.key(i); if (k.length > prefix.length && k.slice(0, prefix.length) === prefix) results.push(k.slice(prefix.length)); } return Promise.as(results); } public setItemsAsync(items: any): Promise { Object.keys(items).forEach((k) => { var v = items[k]; if (typeof v === "string") localStorage[this.tableName + "-" + k] = v; else localStorage.removeItem(this.tableName + "-" + k); }); return Promise.as(); } constructor (public tableName: string) { } } */ export var tableNames = ["Editor", "Index", "Scripts", "ScriptCache", "Traces", "ApiCache", "ArtCache", "Sessions"]; var indexedDBDeleting: boolean = false var indexedDBPromise: Promise var indexedDB: IDBDatabase class IndexedDBTable implements Table { public getValueAsync(key: string): Promise { return this.getItemsAsync([key]).then((items) => items[key]); } public getItemsAsync(keys: string[]): Promise { if (keys.length == 0) return Promise.as({}) return new Promise((onSuccess: (v: any) => any, onError: (v: any) => any, onProgress: (v: any) => any) => { try { var trans = indexedDB.transaction([this.tableName], 'readonly'); var store = trans.objectStore(this.tableName); var requests = keys.map((k) => store.get(k)); var items = {} var missing = keys.length requests.forEach((r: any, i: number) => { // TSBUG: remove type annotations and see it crash r.onsuccess = (e: any) => { var result = e.target.result; items[keys[i]] = result === undefined ? undefined : result.value; if (--missing == 0) onSuccess(items); }; r.onerror = (e: any) => { if (missing >= 0) { missing = -1; onError(tagError(e, "IndexedDBTable.getItemsAsync/0", false)); }; }; }); } catch (e) { onError(tagError(e, "IndexedDBTable.getItemsAsync/1", false)); } }); } public getKeysAsync(): Promise { return new Promise((onSuccess: (v: any) => any, onError: (v: any) => any, onProgress: (v: any) => any) => { try { var trans = indexedDB.transaction([this.tableName], 'readonly'); var store = trans.objectStore(this.tableName); var request = store.openCursor(); var results = []; request.onsuccess = (e) => { var cursor = ((e.target).result); if (!!cursor) { results.push(cursor.key); cursor["continue"](); } else onSuccess(results); }; request.onerror = onError; } catch (e) { onError(tagError(e, "IndexedDBTable.getKeysAsync", false)); } }); } public setItemsAsync(items: any): Promise { var list = [] Object.keys(items).forEach((k) => { list.push({ key: k, value: items[k] }); }); if (list.length == 0) return Promise.as(); return new Promise((onSuccess: (v: any) => any, onError: (v: any) => any, onProgress: (v: any) => any) => { try { var requests = list.map((kvp) => { var trans = indexedDB.transaction([this.tableName], 'readwrite'); var store = trans.objectStore(this.tableName); if (typeof kvp.value === "string") return store.put(kvp); else return store["delete"](kvp.key); }); var missing = requests.length requests.forEach((r: any, i: number) => { r.onsuccess = (e: any) => { if (--missing == 0) onSuccess(undefined); }; r.onerror = (e: any) => { if (missing >= 0) { missing = -1; onError(tagError(e, "IndexedDBTable.setItemsAsync/0", false)); }; }; }); } catch (e) { onError(tagError(e, "IndexedDBTable.setItemsAsync/1", false)); } }); } constructor (public tableName: string) { } } function indexedDBupgrade(indexedDB: IDBDatabase): void { try { var names = indexedDB.objectStoreNames; var exisiting: any = {} for (var i = 0; i < names.length; i++) { if (tableNames.indexOf(names[i]) < 0) indexedDB.deleteObjectStore(names[i]); else exisiting[names[i]] = true; } tableNames.forEach(function (tableName) { if (!exisiting[tableName]) indexedDB.createObjectStore(tableName, { keyPath: "key" }); }); } catch (e) { Util.reportError("indexedDBupgrade", e, true); } } interface WebSqlTransactionResultsRows { length: number; item(index: number): any; } interface WebSqlTransactionResults { rows: WebSqlTransactionResultsRows; } interface WebSqlTransaction { executeSql(query: string); executeSql(query: string, parameters: string[]); executeSql(query: string, parameters: string[], callback: (tx: WebSqlTransaction, results: WebSqlTransactionResults) => void ); } interface WebSql { transaction(callback: (tx: WebSqlTransaction) => void , onError: (error: any) => void , onSuccess: () => void ): void; } var webSqlDeleting: boolean = false var webSqlPromise: Promise var webSql: WebSql class WebSqlTable implements Table { public getValueAsync(key: string): Promise { return this.getItemsAsync([key]).then((items) => items[key]); } internalGetItemsAsync(items: any, keys: string[], start: number, retries: number = 3): Promise { if (start >= keys.length) return Promise.as(items); var end = start + 256; // splitting up as WebSQL seems to impose a limit of 1000 items in a single query if (end > keys.length) end = keys.length; return new Promise((onSuccess: (v: any) => any, onError: (v: any) => any, onProgress: (v: any) => any) => { var propagated = false; var errorHandler = (e, origin) => { if (!propagated) { propagated = true; tagError(e, "WebSqlTable.internalGetItemsAsync/" + origin, retries > 0); if (retries > 0) delayCreateWebSqlAsync().then(() => this.internalGetItemsAsync(items, keys, start, retries - 3)).done(onSuccess, onError); else onError(e); } }; try { webSql.transaction((tx) => { var query = "SELECT * FROM TABLE" + this.tableName + " WHERE "; var values = []; for (var i = start; i < end; i++) { if (i > start) query += " OR "; query += "id=?"; values.push(keys[i]); } try { tx.executeSql(query, values, (tx, results) => { var len = results.rows.length; for (var i = 0; i < len; i++) { var item = results.rows.item(i); items[item.id] = item.text; } }); } catch (e) { errorHandler(e, 0); } }, (e) => errorHandler(e, 1), () => { if (!propagated) { propagated = true; onSuccess(items); } }); } catch (e) { errorHandler(e, 2); } }).then(() => this.internalGetItemsAsync(items, keys, end)); } public getItemsAsync(keys: string[]): Promise { return this.internalGetItemsAsync({}, keys, 0); } public getKeysAsync(retries: number = 3): Promise { return new Promise((onSuccess: (v: any) => any, onError: (v: any) => any, onProgress: (v: any) => any) => { var keys = [] var propagated = false; var errorHandler = (e, origin) => { if (!propagated) { propagated = true; tagError(e, "WebSqlTable.getKeysAsync/" + origin, retries > 0) if (retries > 0) delayCreateWebSqlAsync().then(() => this.getKeysAsync(retries - 1)).done(onSuccess, onError); else onError(e); } } try { webSql.transaction((tx) => { try { tx.executeSql("SELECT id FROM TABLE" + this.tableName, [], (tx, results) => { var len = results.rows.length for (var i = 0; i < len; i++) { var item = results.rows.item(i); keys.push(item.id); } }); } catch (e) { errorHandler(e, 0); } }, (e) => errorHandler(e, 1), () => { if (!propagated) { propagated = true; onSuccess(keys); } }); } catch (e) { errorHandler(e, 2); } }); } public setItemsAsync(items: any, retries: number = 3): Promise { var list = [] Object.keys(items).forEach((k) => { list.push({ key: k, value: items[k] }); }); if (list.length == 0) return Promise.as(); return new Promise((onSuccess: (v: any) => any, onError: (v: any) => any, onProgress: (v: any) => any) => { var propagated = false; var errorHandler = (e, origin) => { if (!propagated) { propagated = true; tagError(e, "WebSqlTable.setItemsAsync/" + origin, retries > 0) if (retries > 0) delayCreateWebSqlAsync().then(() => this.setItemsAsync(items, retries - 1)).done(onSuccess, onError); else onError(e); } }; try { webSql.transaction((tx) => { list.forEach((kvp) => { try { if (typeof kvp.value === "string") tx.executeSql("INSERT OR REPLACE INTO TABLE" + this.tableName + " (id, text) VALUES (?, ?)", [kvp.key, kvp.value]); else tx.executeSql("DELETE FROM TABLE" + this.tableName + " WHERE id=?", [kvp.key]); } catch (e) { errorHandler(e, 0); } }); }, (e) => errorHandler(e, 1), () => { if (!propagated) { propagated = true; onSuccess(undefined); } }); } catch (e) { errorHandler(e, 2); } }); } constructor (public tableName: string) { } } function webSqlInit(webSql: WebSql, onSuccess: (webSql: WebSql) => void , onError: (error: any) => void , retries = 3): void { var propagated = false; var errorHandler = (e, origin) => { if (!propagated) { propagated = true; tagError(e, "WebSqlTable.webSqlInit/" + origin, retries > 0) if (retries > 0) delayCreateWebSqlAsync(retries - 1).done(onSuccess, onError); else onError(e); } }; try { webSql.transaction(function (tx) { tableNames.forEach(function (tableName) { try { tx.executeSql("CREATE TABLE IF NOT EXISTS TABLE" + tableName + " (id unique, text)"); } catch (e) { errorHandler(e, 0); } }); }, (e) => errorHandler(e, 1), () => { if (!propagated) { propagated = true; onSuccess(webSql); } }); } catch (e) { errorHandler(e, 2); } } function getWebSqlAsync(retries: number = 3): Promise // of WebSql { if (webSql) return Promise.as(webSql); if (!webSqlPromise) { if (webSqlDeleting) return Promise.delay(1000, () => getWebSqlAsync(retries)); webSqlPromise = internalGetWebSqlAsync(retries); } return webSqlPromise; } function internalGetWebSqlAsync(retries: number = 3): Promise { return new Promise((onSuccess: (v: any) => any, onError: (v: any) => any, onProgress: (v: any) => any) => { var propagated = false; var errorHandler = (e, origin) => { if (!propagated) { propagated = true; tagError(e, "WebSqlTable.internalGetWebSqlAsync/" + origin, retries > 0) if (retries > 0) Promise.delay(2000, () => internalGetWebSqlAsync(retries - 1)).done(onSuccess, onError); else { Util.log("error during openDatabase " + Util.getErrorInfo(e)); if (Browser.canMemoryTable) onSuccess(undefined); else fatalError(e, "during openDatabase: " + Util.getErrorInfo(e)); } } }; try { webSqlInit((window).openDatabase("TDev", "1.0", "TouchDevelop database", 5 * 1024 * 1024), function (webSqlInitialized) { webSql = webSqlInitialized; onSuccess(webSql); }, e => errorHandler(e, 0), retries); } catch (e) { errorHandler(e, 1); } }); } function delayCreateWebSqlAsync(retries: number = 3): Promise // of WebSql { webSql = undefined; webSqlPromise = undefined; return Promise.delay(2000, () => getWebSqlAsync(retries)); } function getIndexedDBFactory(): IDBFactory { var w = window; return w.indexedDB || w.mozIndexedDB || w.msIndexedDB; // w.webkitIndexedDB } function getIndexedDBAsync(): Promise // of IDBDatabase { if (temporary) return Promise.as(undefined); if (indexedDB) return Promise.as(indexedDB); if (!indexedDBPromise) { if (indexedDBDeleting) return Promise.delay(1000, () => getIndexedDBAsync()); indexedDBPromise = new Promise((onSuccess: (v: any) => any, onError: (v: any) => any, onProgress: (v: any) => any) => { var version = 26; try { var request = getIndexedDBFactory().open("TDev", version); request.onupgradeneeded = function (e) { indexedDBupgrade(request.result); }; request.onsuccess = function (e) { indexedDB = request.result; onSuccess(indexedDB); }; request.onblocked = function (e) { fatalError(e, "blocked during open " + Util.getErrorInfo(request.error)); } request.onerror = function (e) { if (request.error && request.error.name == "VersionError") versionError(e); else { Util.log("error during openDatabase " + Util.getErrorInfo(request.error)); if (Browser.canMemoryTable) onSuccess(undefined); else fatalError(e, "error during openDatabase " + Util.getErrorInfo(request.error)); } } } catch (e) { Util.log("error during openDatabase " + Util.getErrorInfo(request.error)); if (Browser.canMemoryTable) onSuccess(undefined); else fatalError(e, "during openDatabase " + Util.getErrorInfo(e)); } }); } return indexedDBPromise; } export var getTableAsync = (tableName: string): Promise => // of Table { if (!temporary && Browser.webAppImplicit) { Util.log('implicit web app: disable database storage'); temporary = true; Browser.canMemoryTable = true; } if (!temporary) { if (Browser.canWebSql) return getWebSqlAsync().then((myWebSql) => { if (!myWebSql) { Util.log('webSql openDatabase failed'); temporary = true; return Storage.getTableAsync(tableName); } else { Browser.supportMemoryTable(false); // remember that a resilient database is available return Promise.as(new WebSqlTable(tableName)); } }); if (Browser.canIndexedDB) { return getIndexedDBAsync().then((database) => { if (!database) { Util.log('indexedDB openDatabase failed'); temporary = true; return Storage.getTableAsync(tableName); } else { Browser.supportMemoryTable(false); // remember that a resilient database is available return Promise.as(new IndexedDBTable(tableName)); } }); } } if (Browser.canMemoryTable) { Util.log('storage: opening in-memory table ' + tableName); return Promise.as(new MemoryTable(tableName)); } else throw tagError(new Error("no database available"), "getTableAsync", false); } function webSqlClearAsync(retries: number = 3): Promise // of void { return getWebSqlAsync().then(myWebSql => { webSql = undefined; webSqlPromise = undefined; webSqlDeleting = true; return new Promise((onSuccess: (v: any) => any, onError: (v: any) => any, onProgress: (v: any) => any) => { var propagated = false; var errorHandler = (e, origin) => { webSql = undefined; if (!propagated) { propagated = true; webSqlDeleting = false; tagError(e, origin, retries > 0); if (retries > 0) delayCreateWebSqlAsync().then(() => webSqlClearAsync(retries - 1)).done(onSuccess, onError); else fatalError(e, "during drop " + Util.getErrorInfo(e)); } }; try { myWebSql.transaction(function (tx) { tableNames.forEach(function (tableName) { try { tx.executeSql("DROP TABLE TABLE" + tableName); } catch (e) { errorHandler(e, "0"); } }); }, (e) => errorHandler(e, 1), () => { webSql = undefined; if (!propagated) { propagated = true; webSqlDeleting = false; onSuccess(undefined); } }); } catch (e) { errorHandler(e, 2); } }); }); } function indexedDBClearAsync() { if (indexedDB) { try { indexedDB.close(); } catch (e) { } indexedDB = null; } indexedDBPromise = undefined; indexedDBDeleting = true; return new Promise((onSuccess: (v: any) => any, onError: (v: any) => any, onProgress: (v: any) => any) => { try { var request = getIndexedDBFactory().deleteDatabase("TDev"); request.onsuccess = function (e) { if (!indexedDBDeleting) return indexedDBDeleting = false; onSuccess(undefined); }; request.onblocked = request.onerror = function (e) { if (!indexedDBDeleting) return indexedDBDeleting = false; onError(e); } } catch (e) { indexedDBDeleting = false; onError(e); } }); } export var clearPreAsync = () => Promise.as(); export function clearAsync(): Promise // of void { // TODO: race --- Promises that are still ongoing will probably result in errors var p = clearPreAsync(); if (Browser.canWebSql) p = p.then(() => { Util.log("clearing WebSql"); return getWebSqlAsync(); }).then((wsql) => { if (!wsql) return Promise.as(undefined); return webSqlClearAsync().then(() => getWebSqlAsync(), () => { }); }); if (Browser.canIndexedDB) p = p.then(() => { Util.log("clearing IndexedDB"); return indexedDBClearAsync(); }).then(_ => _, _ => { }); // swallow errors p = p.then(() => { Util.log("clearing localStorage"); var oauth_states = window.localStorage["oauth_states"]; window.localStorage.clear(); if (oauth_states) window.localStorage["oauth_states"] = oauth_states; Browser.supportMemoryTable(true); }); return p; } export function logContentsAsync(details: boolean): Promise { var formatBytes = n => n < 1024 ? (n + " B") : n < 1024 * 1024 ? (Math.floor(n / 1024) + " KB") : (Math.floor(n / 1024 / 1024) + " MB"); return Promise.sequentialMap(tableNames, tableName => getTableAsync(tableName).then(table => table.getKeysAsync().then(keys => Promise.sequentialMap(keys, key => table.getValueAsync(key).then(value => { if (details) { var escapedKey = (key + '').replace(/[\\"']/g, '\\$&').replace(/\u0000/g, '\\0'); Util.log("DB table " +tableName + '["' +escapedKey + '"].length=' +formatBytes(value.length)); } return keys.length + value.length; })).then(lengths => { var sum = lengths.reduce((a, b) => a + b, 0); Util.log("DB table " + tableName + " size: " + formatBytes(sum)); return sum; })))).then(lengths => { var sum = lengths.reduce((a, b) => a + b, 0); Util.log("DB total size: " + formatBytes(sum)); }); } } }