diff --git a/src/App.tsx b/src/App.tsx index 2866d529..3fd2429e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -55,7 +55,7 @@ import * as toolbox from './toolbox/toolbox'; //import { testAllBlocksInToolbox } from './toolbox/toolbox_tests'; import ToolboxSettingsModal from './toolbox/settings'; -import * as storage from './storage/client_side_storage'; +import * as clientSideStorage from './storage/client_side_storage'; import * as commonStorage from './storage/common_storage'; import * as ChangeFramework from './blocks/utils/change_framework' @@ -238,10 +238,9 @@ const App: React.FC = () => { const [messageApi, contextHolder] = message.useMessage(); const [alertErrorMessage, setAlertErrorMessage] = useState(''); const [alertErrorVisible, setAlertErrorVisible] = useState(false); + const [storage, setStorage] = useState(null); const [mostRecentModulePath, setMostRecentModulePath] = useState(null); const [shownPythonToolboxCategories, setShownPythonToolboxCategories] = useState>(new Set()); - const [triggerListModules, setTriggerListModules] = useState(0); - const afterListModulesSuccess = useRef<() => void>(() => {}); const [modules, setModules] = useState([]); const [treeData, setTreeData] = useState([]); const [treeExpandedKeys, setTreeExpandedKeys] = useState([]); @@ -286,42 +285,26 @@ const App: React.FC = () => { return false; } - // When the app is loaded, initialize the blocks we provide. + // When the app is loaded, open storage and initialize the blocks we provide. useEffect(() => { renderCounter.current = renderCounter.current + 1; - if (ignoreEffect()) { return; } - fetchMostRecentModulePath(); - initializeShownPythonToolboxCategories(); + openStorage(); initializeBlocks(); //testAllBlocksInToolbox(toolbox.getToolboxJSON([], []).contents); }, []); - const fetchMostRecentModulePath = () => { - storage.fetchEntry( - 'mostRecentModulePath', '', - (value: string | null, errorMessage: string) => { - if (value) { - setMostRecentModulePath(value); - } else { - // There is no most recent module path. - setMostRecentModulePath(''); - } - }); - }; - - const initializeShownPythonToolboxCategories = () => { - storage.fetchEntry( - 'shownPythonToolboxCategories', '[]', - (value: string | null, errorMessage: string) => { - if (value) { - const shownCategories: string[] = JSON.parse(value); - setShownPythonToolboxCategories(new Set(shownCategories)); - } - }); + const openStorage = async () => { + try { + const c = await clientSideStorage.openClientSideStorage(); + setStorage(c); + } catch (e) { + console.log('Failed to open client side storage. Caught the following error...'); + console.log(e); + } }; const initializeBlocks = () => { @@ -333,40 +316,64 @@ const App: React.FC = () => { initializeGeneratedBlocks(); }; - // Fetch the list of modules from storage. useEffect(() => { if (ignoreEffect()) { return; } - // mostRecentModulePath hasn't been fetched yet. Try agagin in a bit. - if (mostRecentModulePath == null) { - setTimeout(() => { - setTriggerListModules(Date.now()); - }, 50); + if (!storage) { return; } + fetchMostRecentModulePath(); + initializeShownPythonToolboxCategories(); + initializeModules(); + }, [storage]); + + const fetchMostRecentModulePath = async () => { + try { + const value = await storage.fetchEntry('mostRecentModulePath', ''); + setMostRecentModulePath(value); + } catch (e) { + console.log('Failed to fetch mostRecentModulePath. Caught the following error...'); + console.log(e); + } + }; - storage.listModules((array: commonStorage.Project[] | null, errorMessage: string) => { - if (errorMessage) { - setAlertErrorMessage('Unable to load the list of modules: ' + errorMessage); - setAlertErrorVisible(true); - return; - } - if (array != null) { - setModules(array) - const callback = afterListModulesSuccess.current; - afterListModulesSuccess.current = () => {}; - callback(); + const initializeShownPythonToolboxCategories = async () => { + try { + const value = await storage.fetchEntry('shownPythonToolboxCategories', '[]'); + const shownCategories: string[] = JSON.parse(value); + setShownPythonToolboxCategories(new Set(shownCategories)); + } catch (e) { + console.log('Failed to fetch shownPythonToolboxCategories. Caught the following error...'); + console.log(e); + } + }; - if (array.length === 0) { - setNewProjectNameModalPurpose(PURPOSE_NEW_PROJECT); - setNewProjectNameModalInitialValue(''); - setNewProjectNameModalTitle('Welcome to WPILib Blocks!'); - setNewProjectNameModalIsOpen(true); - } + const initializeModules = async () => { + const array = await fetchListOfModules(); + if (array.length === 0) { + setNewProjectNameModalPurpose(PURPOSE_NEW_PROJECT); + setNewProjectNameModalInitialValue(''); + setNewProjectNameModalTitle('Welcome to WPILib Blocks!'); + setNewProjectNameModalIsOpen(true); + } + }; + + const fetchListOfModules = async (): Promise => { + return new Promise(async (resolve, reject) => { + try { + const array = await storage.listModules(); + setModules(array) + resolve(array); + } catch (e) { + console.log('Failed to load the list of modules. Caught the following error...'); + console.log(e); + setAlertErrorMessage('Failed to load the list of modules.'); + setAlertErrorVisible(true); + reject(new Error('Failed to load the list of modules.')); } }); - }, [triggerListModules]); + }; // When the list of modules is set, update the treeData and treeExpandedKeys. useEffect(() => { @@ -447,6 +454,9 @@ const App: React.FC = () => { if (ignoreEffect()) { return; } + if (!storage) { + return; + } const module = (modules.length > 0 && currentModulePath) ? commonStorage.findModule(modules, currentModulePath) : null; @@ -508,19 +518,17 @@ const App: React.FC = () => { if (ignoreEffect()) { return; } - if (blocklyComponent.current) { - const blocklyWorkspace = blocklyComponent.current.getBlocklyWorkspace(); - if (blocklyWorkspace) { - ChangeFramework.setup(blocklyWorkspace); - blocklyWorkspace.addChangeListener(mutatorOpenListener); - - // Show generated python code. - blocklyWorkspace.addChangeListener(handleBlocksChanged); - } - - blocksEditor.current = new editor.Editor(blocklyWorkspace); + if (!blocklyComponent.current || !storage) { + return; } - }, [blocklyComponent]); + const blocklyWorkspace = blocklyComponent.current.getBlocklyWorkspace(); + if (blocklyWorkspace) { + ChangeFramework.setup(blocklyWorkspace); + blocklyWorkspace.addChangeListener(mutatorOpenListener); + blocklyWorkspace.addChangeListener(handleBlocksChanged); + } + blocksEditor.current = new editor.Editor(blocklyWorkspace, storage); + }, [blocklyComponent, storage]); const handleBlocksChanged = (event: Blockly.Events.Abstract) => { if (event.isUiEvent) { @@ -565,13 +573,12 @@ const App: React.FC = () => { // Set the function to be executed if the user clicks 'ok'. afterPopconfirmOk.current = () => { setPopconfirmLoading(true); - saveModule((success) => { - setOpenPopconfirm(false); - setPopconfirmLoading(false); - if (success) { - callback(); - } - }); + const success = saveBlocks(); + setOpenPopconfirm(false); + setPopconfirmLoading(false); + if (success) { + callback(); + } }; setOpenPopconfirm(true); } @@ -594,53 +601,47 @@ const App: React.FC = () => { return projectNames; }; - const handleNewProjectNameOk = (newProjectName: string) => { + const handleNewProjectNameOk = async (newProjectName: string) => { const newProjectPath = commonStorage.makeProjectPath(newProjectName); if (newProjectNameModalPurpose === PURPOSE_NEW_PROJECT) { const projectContent = commonStorage.newProjectContent(newProjectName); - storage.createModule( - commonStorage.MODULE_TYPE_PROJECT, newProjectPath, projectContent, - (success: boolean, errorMessage: string) => { - if (success) { - afterListModulesSuccess.current = () => { - setCurrentModulePath(newProjectPath); - }; - setTriggerListModules(Date.now()); - } else if (errorMessage) { - setAlertErrorMessage('Failed to create a new Project: ' + errorMessage); - setAlertErrorVisible(true); - } - }); + try { + await storage.createModule( + commonStorage.MODULE_TYPE_PROJECT, newProjectPath, projectContent); + await fetchListOfModules(); + setCurrentModulePath(newProjectPath); + } catch (e) { + console.log('Failed to create a new project. Caught the following error...'); + console.log(e); + setAlertErrorMessage('Failed to create a new project.'); + setAlertErrorVisible(true); + } } else if (newProjectNameModalPurpose === PURPOSE_RENAME_PROJECT) { - storage.renameModule( - currentModule.moduleType, currentModule.projectName, - currentModule.moduleName, newProjectName, - (success: boolean, errorMessage: string) => { - if (success) { - afterListModulesSuccess.current = () => { - setCurrentModulePath(newProjectPath); - }; - setTriggerListModules(Date.now()); - } else if (errorMessage) { - setAlertErrorMessage('Failed to rename the Project: ' + errorMessage); - setAlertErrorVisible(true); - } - }); + try { + await storage.renameModule( + currentModule.moduleType, currentModule.projectName, + currentModule.moduleName, newProjectName); + await fetchListOfModules(); + setCurrentModulePath(newProjectPath); + } catch (e) { + console.log('Failed to rename the project. Caught the following error...'); + console.log(e); + setAlertErrorMessage('Failed to rename the project.'); + setAlertErrorVisible(true); + } } else if (newProjectNameModalPurpose === PURPOSE_COPY_PROJECT) { - storage.copyModule( + try { + await storage.copyModule( currentModule.moduleType, currentModule.projectName, - currentModule.moduleName, newProjectName, - (success: boolean, errorMessage: string) => { - if (success) { - afterListModulesSuccess.current = () => { - setCurrentModulePath(newProjectPath); - }; - setTriggerListModules(Date.now()); - } else if (errorMessage) { - setAlertErrorMessage('Failed to copy the Project: ' + errorMessage); - setAlertErrorVisible(true); - } - }); + currentModule.moduleName, newProjectName); + await fetchListOfModules(); + setCurrentModulePath(newProjectPath); + } catch (e) { + console.log('Failed to copy the project. Caught the following error...'); + console.log(e); + setAlertErrorMessage('Failed to copy the project.'); + setAlertErrorVisible(true); + } } }; @@ -686,90 +687,85 @@ const App: React.FC = () => { return moduleNames; }; - const handleNewModuleNameOk = (newModuleName: string) => { + const handleNewModuleNameOk = async (newModuleName: string) => { const newModulePath = commonStorage.makeModulePath(currentModule.projectName, newModuleName); if (newModuleNameModalPurpose === PURPOSE_NEW_MECHANISM) { - const mechanismContent = commonStorage.newMechanismContent(currentModule.projectName, newModuleName); - storage.createModule( - commonStorage.MODULE_TYPE_MECHANISM, newModulePath, mechanismContent, - (success: boolean, errorMessage: string) => { - if (success) { - afterListModulesSuccess.current = () => { - setCurrentModulePath(newModulePath); - }; - setTriggerListModules(Date.now()); - } else if (errorMessage) { - setAlertErrorMessage('Failed to create a new Mechanism: ' + errorMessage); - setAlertErrorVisible(true); - } - }); + const mechanismContent = commonStorage.newMechanismContent( + currentModule.projectName, newModuleName); + try { + await storage.createModule( + commonStorage.MODULE_TYPE_MECHANISM, newModulePath, mechanismContent); + await fetchListOfModules(); + setCurrentModulePath(newModulePath); + } catch (e) { + console.log('Failed to create a new mechanism. Caught the following error...'); + console.log(e); + setAlertErrorMessage('Failed to create a new mechanism.'); + setAlertErrorVisible(true); + } } else if (newModuleNameModalPurpose === PURPOSE_NEW_OPMODE) { const opModeContent = commonStorage.newOpModeContent(currentModule.projectName, newModuleName); - storage.createModule( - commonStorage.MODULE_TYPE_OPMODE, newModulePath, opModeContent, - (success: boolean, errorMessage: string) => { - if (success) { - afterListModulesSuccess.current = () => { - setCurrentModulePath(newModulePath); - }; - setTriggerListModules(Date.now()); - } else if (errorMessage) { - setAlertErrorMessage('Failed to create a new OpMode: ' + errorMessage); - setAlertErrorVisible(true); - } - }); + try { + await storage.createModule( + commonStorage.MODULE_TYPE_OPMODE, newModulePath, opModeContent); + await fetchListOfModules(); + setCurrentModulePath(newModulePath); + } catch (e) { + console.log('Failed to create a new OpMode. Caught the following error...'); + console.log(e); + setAlertErrorMessage('Failed to create a new OpMode'); + setAlertErrorVisible(true); + } } else if (newModuleNameModalPurpose === PURPOSE_RENAME_MODULE) { - storage.renameModule( - currentModule.moduleType, currentModule.projectName, - currentModule.moduleName, newModuleName, - (success: boolean, errorMessage: string) => { - if (success) { - afterListModulesSuccess.current = () => { - setCurrentModulePath(newModulePath); - }; - setTriggerListModules(Date.now()); - } else if (errorMessage) { - setAlertErrorMessage('Failed to rename the module: ' + errorMessage); - setAlertErrorVisible(true); - } - }); + try { + await storage.renameModule( + currentModule.moduleType, currentModule.projectName, + currentModule.moduleName, newModuleName); + await fetchListOfModules(); + setCurrentModulePath(newModulePath); + } catch (e) { + console.log('Failed to rename the module. Caught the following error...'); + console.log(e); + setAlertErrorMessage('Failed to rename the module.'); + setAlertErrorVisible(true); + } } else if (newModuleNameModalPurpose === PURPOSE_COPY_MODULE) { - storage.copyModule( - currentModule.moduleType, currentModule.projectName, - currentModule.moduleName, newModuleName, - (success: boolean, errorMessage: string) => { - if (success) { - afterListModulesSuccess.current = () => { - setCurrentModulePath(newModulePath); - }; - setTriggerListModules(Date.now()); - } else if (errorMessage) { - setAlertErrorMessage('Failed to copy the module: ' + errorMessage); - setAlertErrorVisible(true); - } - }); + try { + await storage.copyModule( + currentModule.moduleType, currentModule.projectName, + currentModule.moduleName, newModuleName); + await fetchListOfModules(); + setCurrentModulePath(newModulePath); + } catch (e) { + console.log('Failed to copy the module. Caught the following error...'); + console.log(e); + setAlertErrorMessage('Failed to copy the module.'); + setAlertErrorVisible(true); + } } }; - const handleSaveClicked = () => { - saveModule((success) => {}); + const handleSaveClicked = async () => { + saveBlocks((success) => {}); }; - const saveModule = (callback: (success: boolean) => void) => { + const saveBlocks = async (): boolean => { if (blocksEditor.current && currentModulePath) { - blocksEditor.current.saveModule((success, errorMessage) => { - if (errorMessage) { - setAlertErrorMessage(errorMessage); - setAlertErrorVisible(true); - } else { - messageApi.open({ - type: 'success', - content: 'Save completed successfully.', - }); - } - callback(success); - }); + try { + await blocksEditor.current.saveBlocks(); + messageApi.open({ + type: 'success', + content: 'Save completed successfully.', + }); + return true; + } catch (e: Error) { + console.log('Failed to save the blocks. Caught the following error...'); + console.log(e); + setAlertErrorMessage('Failed to save the blocks.'); + setAlertErrorVisible(true); + } } + return false; }; const handleRenameClicked = () => { @@ -842,7 +838,7 @@ const App: React.FC = () => { // Set the function to be executed if the user clicks 'ok'. afterPopconfirmOk.current = () => { setOpenPopconfirm(false); - checkIfBlocksWereModified(() => { + checkIfBlocksWereModified(async () => { if (!currentModule) { return; } @@ -863,15 +859,15 @@ const App: React.FC = () => { if (!foundAnotherProject) { setCurrentModulePath(''); } - storage.deleteModule(moduleTypeToDelete, modulePathToDelete, - (success: boolean, errorMessage: string) => { - if (success) { - setTriggerListModules(Date.now()); - } else if (errorMessage) { - setAlertErrorMessage('Failed to delete the Project: ' + errorMessage); - setAlertErrorVisible(true); - } - }); + try { + await storage.deleteModule(moduleTypeToDelete, modulePathToDelete); + await fetchListOfModules(); + } catch (e) { + console.log('Failed to delete the project. Caught the following error...'); + console.log(e); + setAlertErrorMessage('Failed to delete the project.'); + setAlertErrorVisible(true); + } } else if (currentModule.moduleType == commonStorage.MODULE_TYPE_MECHANISM || currentModule.moduleType == commonStorage.MODULE_TYPE_OPMODE) { // This is a Mechanism or an OpMode. @@ -881,15 +877,15 @@ const App: React.FC = () => { const modulePathToDelete = currentModulePath; const projectPath = commonStorage.makeProjectPath(currentModule.projectName); setCurrentModulePath(projectPath); - storage.deleteModule(moduleTypeToDelete, modulePathToDelete, - (success: boolean, errorMessage: string) => { - if (success) { - setTriggerListModules(Date.now()); - } else if (errorMessage) { - setAlertErrorMessage('Failed to delete the module: ' + errorMessage); - setAlertErrorVisible(true); - } - }); + try { + await storage.deleteModule(moduleTypeToDelete, modulePathToDelete); + await fetchListOfModules(); + } catch (e) { + console.log('Failed to delete the module. Caught the following error...'); + console.log(e); + setAlertErrorMessage('Failed to delete the module.'); + setAlertErrorVisible(true); + } } }); }; @@ -911,29 +907,28 @@ const App: React.FC = () => { }, customRequest: ({ file, onSuccess, onError }) => { const reader = new FileReader(); - reader.onload = (event) => { + reader.onload = async (event) => { const dataUrl = event.target.result; const uploadProjectName = commonStorage.makeUploadProjectName(file.name, getProjectNames()); - storage.uploadProject( - uploadProjectName, dataUrl, - (success: boolean, errorMessage: string) => { - if (success) { - onSuccess('Upload successful'); - afterListModulesSuccess.current = () => { - const uploadProjectPath = commonStorage.makeProjectPath(uploadProjectName); - setCurrentModulePath(uploadProjectPath); - }; - setTriggerListModules(!triggerListModules); - } else { - onError(errorMessage); - setAlertErrorMessage('Unable to upload the project'); - setAlertErrorVisible(true); - } - }); + try { + await storage.uploadProject(uploadProjectName, dataUrl); + onSuccess('Upload successful'); + await fetchListOfModules(); + const uploadProjectPath = commonStorage.makeProjectPath(uploadProjectName); + setCurrentModulePath(uploadProjectPath); + } catch (e) { + console.log('Failed to upload the project. Caught the following error...'); + console.log(e); + onError('Failed to upload the project.'); + setAlertErrorMessage('Failed to upload the project'); + setAlertErrorVisible(true); + } }; reader.onerror = (error) => { - onError(error); - setAlertErrorMessage('Unable to upload the project'); + console.log('Failed to upload the project. reader.onerror called with the following error...'); + console.log(error); + onError('Failed to upload the project.'); + setAlertErrorMessage('Failed to upload the project'); setAlertErrorVisible(true); }; reader.readAsDataURL(file); @@ -941,20 +936,19 @@ const App: React.FC = () => { }; const handleDownloadClicked = () => { - checkIfBlocksWereModified(() => { - storage.downloadProject( - currentModule.projectName, - (url: string | null, errorMessage: string) => { - if (errorMessage) { - setAlertErrorMessage('Unable to download the project: ' + errorMessage); - setAlertErrorVisible(true); - return; - } - const link = document.createElement('a'); - link.href = url; - link.download = currentModule.projectName + commonStorage.UPLOAD_DOWNLOAD_FILE_EXTENSION; - link.click(); - }); + checkIfBlocksWereModified(async () => { + try { + const url = await storage.downloadProject(currentModule.projectName); + const link = document.createElement('a'); + link.href = url; + link.download = currentModule.projectName + commonStorage.UPLOAD_DOWNLOAD_FILE_EXTENSION; + link.click(); + } catch (e) { + console.log('Failed to download the project. Caught the following error...'); + console.log(e); + setAlertErrorMessage('Failed to download the project.'); + setAlertErrorVisible(true); + } }); }; @@ -962,7 +956,7 @@ const App: React.FC = () => { setToolboxSettingsModalIsOpen(true); }; - const handleToolboxSettingsOk = (updatedShownCategories: Set) => { + const handleToolboxSettingsOk = async (updatedShownCategories: Set) => { setShownPythonToolboxCategories(updatedShownCategories); const array = Array.from(updatedShownCategories); array.sort(); diff --git a/src/editor/editor.ts b/src/editor/editor.ts index 34fdf708..6530e35f 100644 --- a/src/editor/editor.ts +++ b/src/editor/editor.ts @@ -22,7 +22,6 @@ import * as Blockly from 'blockly/core'; import { extendedPythonGenerator } from './extended_python_generator'; -import * as storage from '../storage/client_side_storage'; import * as commonStorage from '../storage/common_storage'; import { getToolboxJSON } from '../toolbox/toolbox'; @@ -34,6 +33,7 @@ const EMPTY_TOOLBOX: Blockly.utils.toolbox.ToolboxDefinition = { export class Editor { private blocklyWorkspace: Blockly.WorkspaceSvg; + private storage: commonStorage.Storage; private currentModule: commonStorage.Module | null = null; private modulePath: string = ''; private projectPath: string = ''; @@ -42,8 +42,9 @@ export class Editor { private bindedOnChange: any = null; private toolbox: Blockly.utils.toolbox.ToolboxDefinition = EMPTY_TOOLBOX; - constructor(blocklyWorkspace: Blockly.WorkspaceSvg) { + constructor(blocklyWorkspace: Blockly.WorkspaceSvg, storage: commonStorage.Storage) { this.blocklyWorkspace = blocklyWorkspace; + this.storage = storage; } private onChangeWhileLoading(event: Blockly.Events.Abstract) { @@ -102,7 +103,7 @@ export class Editor { // TODO(lizlooney): do we need to do anything here? } - public loadModuleBlocks(currentModule: commonStorage.Module | null) { + public async loadModuleBlocks(currentModule: commonStorage.Module | null) { this.currentModule = currentModule; if (currentModule) { this.modulePath = currentModule.modulePath; @@ -116,47 +117,26 @@ export class Editor { this.clearBlocklyWorkspace(); if (currentModule) { - storage.fetchModuleContent( - this.modulePath, - (moduleContent: string | null, errorMessage: string) => { - if (errorMessage) { - alert(errorMessage); - return; - } - if (moduleContent) { - this.moduleContent = moduleContent; - if (this.projectPath === this.modulePath) { - this.projectContent = moduleContent - } - - // If both the project content and the module content have been - // loaded, load the blocks into the blockly workspace. - if (this.projectContent) { - this.loadBlocksIntoBlocklyWorkspace(); - } - } - } - ); + const promises: {[key: string]: Promise} = {}; // key is module path, value is promise of module content. + promises[this.modulePath] = this.storage.fetchModuleContent(this.modulePath); if (this.projectPath !== this.modulePath) { - storage.fetchModuleContent( - this.projectPath, - (projectContent: string | null, errorMessage: string) => { - if (errorMessage) { - alert(errorMessage); - return; - } - if (projectContent) { - this.projectContent = projectContent; - - // If both the project and the module have been loaded, load the - // blocks into the blockly workspace. - if (this.moduleContent) { - this.loadBlocksIntoBlocklyWorkspace(); - } - } - } - ); + // Also fetch the project module content. It contains exported blocks that can be used. + promises[this.projectPath] = this.storage.fetchModuleContent(this.projectPath) + } + + const moduleContents: {[key: string]: string} = {}; // key is module path, value is module content + await Promise.all( + Object.entries(promises).map(async ([modulePath, promise]) => { + moduleContents[modulePath] = await promise; + }) + ); + this.moduleContent = moduleContents[this.modulePath]; + if (this.projectPath === this.modulePath) { + this.projectContent = this.moduleContent + } else { + this.projectContent = moduleContents[this.projectPath]; } + this.loadBlocksIntoBlocklyWorkspace(); } } @@ -210,6 +190,15 @@ export class Editor { } public isModified(): boolean { + /* + // This code is helpful for debugging issues where the editor says + // 'Blocks have been modified!'. + if (this.getModuleContent() !== this.moduleContent) { + console.log('isModified will return true'); + console.log('this.getModuleContent() is ' + this.getModuleContent()); + console.log('this.moduleContent is ' + this.moduleContent); + } + */ return this.getModuleContent() !== this.moduleContent; } @@ -221,13 +210,13 @@ export class Editor { return commonStorage.makeModuleContent(this.currentModule, pythonCode, exportedBlocks, blocksContent); } - public saveModule(callback: storage.BooleanCallback): void { + public async saveBlocks(): void { const moduleContent = this.getModuleContent(); - storage.saveModule(this.modulePath, moduleContent, (success, errorMessage) => { - if (success) { - this.moduleContent = moduleContent; - } - callback(success, errorMessage); - }); + try { + await this.storage.saveModule(this.modulePath, moduleContent); + this.moduleContent = moduleContent; + } catch (e: Error) { + throw e; + } } } diff --git a/src/storage/client_side_storage.ts b/src/storage/client_side_storage.ts index 2a571bad..c768bcb7 100644 --- a/src/storage/client_side_storage.ts +++ b/src/storage/client_side_storage.ts @@ -23,702 +23,590 @@ import * as commonStorage from './common_storage'; // Functions for saving blocks modules to client side storage. -export type BooleanCallback = (success: boolean, error: string) => void; -export type StringCallback = (value: string | null, error: string) => void; -export type ModulesCallback = (modules: commonStorage.Project[] | null, error: string) => void; - - -const databaseName = 'systemcore-blocks-interface'; -let db: IDBDatabase | null = null; - -function _openDatabase(callback: BooleanCallback): void { - const openRequest = window.indexedDB.open(databaseName, 1); - openRequest.onerror = (event: Event) => { - console.log('IndexedDB open request failed:'); - console.log(openRequest.error); - callback(false, 'openRequest error'); - }; - openRequest.onupgradeneeded = (event: Event) => { - const db1 = openRequest.result; - - var stores = db1.objectStoreNames; +const DATABASE_NAME = 'systemcore-blocks-interface'; + +export async function openClientSideStorage(): Promise { + return new Promise((resolve, reject) => { + const openRequest = window.indexedDB.open(DATABASE_NAME, 1); + openRequest.onerror = (event: Event) => { + console.log('IndexedDB open request failed. openRequest.error is...'); + console.log(openRequest.error); + reject(new Error('IndexedDB open request failed.')); + }; + openRequest.onupgradeneeded = (event: Event) => { + const db = openRequest.result; - if (!stores.contains('modules')) { - // Create the object store for modules. - db1.createObjectStore('modules', { keyPath: 'path' }); - } + var stores = db.objectStoreNames; - if (!stores.contains('entries')) { - // Create an object store for key/value entries. - db1.createObjectStore('entries', { keyPath: 'key' }); - } - }; - openRequest.onsuccess = (event: Event) => { - db = openRequest.result; - callback(true, ''); - }; -} + if (!stores.contains('entries')) { + // Create an object store for key/value entries. + db.createObjectStore('entries', { keyPath: 'key' }); + } -export function saveEntry( - entryKey: string, entryValue: string, - callback: BooleanCallback | null = null): void { - if (!db) { - _openDatabase((success: boolean, errorReason: string) => { - if (success) { - saveEntry(entryKey, entryValue, callback); - } else { - if (callback) { - callback(false, 'Save entry failed. (' + errorReason + ')'); - } + if (!stores.contains('modules')) { + // Create the object store for modules. + db.createObjectStore('modules', { keyPath: 'path' }); } - }); - return; - } - const transaction = db.transaction(['entries'], 'readwrite'); - transaction.oncomplete = (event: Event) => { - if (callback) { - callback(true, ''); - } - }; - transaction.onabort = () => { - if (callback) { - callback(false, 'Save entry failed.'); - } - }; - const entriesObjectStore = transaction.objectStore('entries'); - const getRequest = entriesObjectStore.get(entryKey); - getRequest.onerror = (event: Event) => { - console.log('IndexedDB get request failed:'); - console.log(getRequest.error); - throw new Error('getRequest error'); - }; - getRequest.onsuccess = (event: Event) => { - let value; - if (getRequest.result === undefined) { - value = Object.create(null); - value.key = entryKey; - } else { - value = getRequest.result; - } - value.value = entryValue; - const putRequest = entriesObjectStore.put(value); - putRequest.onerror = (event: Event) => { - console.log('IndexedDB put request failed:'); - console.log(putRequest.error); - throw new Error('putRequest error'); }; - }; + openRequest.onsuccess = (event: Event) => { + const db = openRequest.result; + resolve(new ClientSideStorage(db)); + }; + }); } -export function fetchEntry(entryKey: string, defaultValue: string, callback: StringCallback): void { - if (!db) { - _openDatabase((success: boolean, errorReason: string) => { - if (success) { - fetchEntry(entryKey, defaultValue, callback); - } else { - callback(null, 'Fetch entry failed. (' + errorReason + ')'); - } +class ClientSideStorage implements commonStorage.Storage { + db: IDBDatabase; + + private constructor(db: IDBDatabase) { + this.db = db; + } + + async saveEntry(entryKey: string, entryValue: string): Promise { + return new Promise((resolve, reject) => { + const transaction = this.db.transaction(['entries'], 'readwrite'); + transaction.oncomplete = (event: Event) => { + resolve(); + }; + transaction.onabort = () => { + console.log('IndexedDB transaction aborted.'); + reject(new Error('IndexedDB transaction aborted.')); + }; + const entriesObjectStore = transaction.objectStore('entries'); + const getRequest = entriesObjectStore.get(entryKey); + getRequest.onerror = (event: Event) => { + console.log('IndexedDB get request failed. getRequest.error is...'); + console.log(getRequest.error); + throw new Error('IndexedDB get request failed.'); + }; + getRequest.onsuccess = (event: Event) => { + let value; + if (getRequest.result === undefined) { + value = Object.create(null); + value.key = entryKey; + } else { + value = getRequest.result; + } + value.value = entryValue; + const putRequest = entriesObjectStore.put(value); + putRequest.onerror = (event: Event) => { + console.log('IndexedDB put request failed. putRequest.error is...'); + console.log(putRequest.error); + throw new Error('IndexedDB put request failed.'); + }; + }; }); - return; } - const getRequest = db.transaction(['entries'], 'readonly') - .objectStore('entries').get(entryKey); - getRequest.onerror = (event: Event) => { - console.log('IndexedDB get request failed:'); - console.log(getRequest.error); - callback(null, 'Fetch entry failed. (getRequest error)'); - }; - getRequest.onsuccess = (event: Event) => { - if (getRequest.result === undefined) { - // Entry does not exist. - callback(defaultValue, ''); - return; - } - const value = getRequest.result; - callback(value.value, ''); - }; -} -export function listModules(callback: ModulesCallback): void { - if (!db) { - _openDatabase((success: boolean, errorReason: string) => { - if (success) { - listModules(callback); - } else { - callback(null, 'List modules failed. (' + errorReason + ')'); - } + async fetchEntry(entryKey: string, defaultValue: string): Promise { + return new Promise((resolve, reject) => { + const getRequest = this.db.transaction(['entries'], 'readonly') + .objectStore('entries').get(entryKey); + getRequest.onerror = (event: Event) => { + console.log('IndexedDB get request failed. getRequest.error is...'); + console.log(getRequest.error); + reject(new Error('IndexedDB get request failed.')); + }; + getRequest.onsuccess = (event: Event) => { + const value = (getRequest.result === undefined) ? defaultValue : getRequest.result.value; + resolve(value); + }; }); - return; } - const projects: {[key: string]: commonStorage.Project} = {}; // key is project name, value is Project - // The mechanisms and opModes variables hold any Mechanisms and OpModes that - // are read before the Project to which they belong is read. - const mechanisms: {[key: string]: commonStorage.Mechanism[]} = {}; // key is project name, value is list of Mechanisms - const opModes: {[key: string]: commonStorage.OpMode[]} = {}; // key is project name, value is list of OpModes - const openCursorRequest = db.transaction(['modules'], 'readonly') - .objectStore('modules') - .openCursor(); - openCursorRequest.onerror = (event: Event) => { - console.log('IndexedDB openCursor request failed:'); - console.log(openCursorRequest.error); - callback(null, 'List modules failed. Could not open cursor.'); - }; - openCursorRequest.onsuccess = (event: Event) => { - const cursor = openCursorRequest.result; - if (cursor) { - const value = cursor.value; - const path = value.path; - const moduleType = value.type; - const module: commonStorage.Module = { - modulePath: path, - moduleType: moduleType, - projectName: commonStorage.getProjectName(path), - moduleName: commonStorage.getModuleName(path), - dateModifiedMillis: value.dateModifiedMillis, - } - if (moduleType === commonStorage.MODULE_TYPE_PROJECT) { - const project: commonStorage.Project = { - ...module, - mechanisms: [], - opModes: [], - }; - projects[project.projectName] = project; - // Add any Mechanisms that belong to this project that have already - // been read. - if (project.projectName in mechanisms) { - project.mechanisms = mechanisms[project.projectName]; - delete mechanisms[project.projectName]; - } - // Add any OpModes that belong to this project that have already been - // read. - if (project.projectName in opModes) { - project.opModes = opModes[project.projectName]; - delete opModes[project.projectName]; - } - } else if (moduleType === commonStorage.MODULE_TYPE_MECHANISM) { - const mechanism: commonStorage.Mechanism = { - ...module, - }; - if (mechanism.projectName in projects) { - // If the Project to which this Mechanism belongs has already been read, - // add this Mechanism to it. - projects[mechanism.projectName].mechanisms.push(mechanism); - } else { - // Otherwise, add this Mechanism to the mechanisms local variable. - if (mechanism.projectName in mechanisms) { - mechanisms[mechanism.projectName].push(mechanism); - } else { - mechanisms[mechanism.projectName] = [mechanism]; + + async listModules(): Promise { + return new Promise((resolve, reject) => { + const projects: {[key: string]: commonStorage.Project} = {}; // key is project name, value is Project + // The mechanisms and opModes variables hold any Mechanisms and OpModes that + // are read before the Project to which they belong is read. + const mechanisms: {[key: string]: commonStorage.Mechanism[]} = {}; // key is project name, value is list of Mechanisms + const opModes: {[key: string]: commonStorage.OpMode[]} = {}; // key is project name, value is list of OpModes + const openCursorRequest = this.db.transaction(['modules'], 'readonly') + .objectStore('modules') + .openCursor(); + openCursorRequest.onerror = (event: Event) => { + console.log('IndexedDB openCursor request failed. openCursorRequest.error is...'); + console.log(openCursorRequest.error); + reject(new Error('IndexedDB openCursor request failed.')); + }; + openCursorRequest.onsuccess = (event: Event) => { + const cursor = openCursorRequest.result; + if (cursor) { + const value = cursor.value; + const path = value.path; + const moduleType = value.type; + const module: commonStorage.Module = { + modulePath: path, + moduleType: moduleType, + projectName: commonStorage.getProjectName(path), + moduleName: commonStorage.getModuleName(path), + dateModifiedMillis: value.dateModifiedMillis, } - } - } else if (moduleType === commonStorage.MODULE_TYPE_OPMODE) { - const opMode: commonStorage.OpMode = { - ...module, - }; - if (opMode.projectName in projects) { - // If the Project to which this OpMode belongs has already been read, - // add this OpMode to it. - projects[opMode.projectName].opModes.push(opMode); - } else { - // Otherwise, add this OpMode to the opModes local variable. - if (opMode.projectName in opModes) { - opModes[opMode.projectName].push(opMode); - } else { - opModes[opMode.projectName] = [opMode]; + if (moduleType === commonStorage.MODULE_TYPE_PROJECT) { + const project: commonStorage.Project = { + ...module, + mechanisms: [], + opModes: [], + }; + projects[project.projectName] = project; + // Add any Mechanisms that belong to this project that have already + // been read. + if (project.projectName in mechanisms) { + project.mechanisms = mechanisms[project.projectName]; + delete mechanisms[project.projectName]; + } + // Add any OpModes that belong to this project that have already been + // read. + if (project.projectName in opModes) { + project.opModes = opModes[project.projectName]; + delete opModes[project.projectName]; + } + } else if (moduleType === commonStorage.MODULE_TYPE_MECHANISM) { + const mechanism: commonStorage.Mechanism = { + ...module, + }; + if (mechanism.projectName in projects) { + // If the Project to which this Mechanism belongs has already been read, + // add this Mechanism to it. + projects[mechanism.projectName].mechanisms.push(mechanism); + } else { + // Otherwise, add this Mechanism to the mechanisms local variable. + if (mechanism.projectName in mechanisms) { + mechanisms[mechanism.projectName].push(mechanism); + } else { + mechanisms[mechanism.projectName] = [mechanism]; + } + } + } else if (moduleType === commonStorage.MODULE_TYPE_OPMODE) { + const opMode: commonStorage.OpMode = { + ...module, + }; + if (opMode.projectName in projects) { + // If the Project to which this OpMode belongs has already been read, + // add this OpMode to it. + projects[opMode.projectName].opModes.push(opMode); + } else { + // Otherwise, add this OpMode to the opModes local variable. + if (opMode.projectName in opModes) { + opModes[opMode.projectName].push(opMode); + } else { + opModes[opMode.projectName] = [opMode]; + } + } } + cursor.continue(); + } else { + // The cursor is done. We have finished reading all the modules. + const modules: commonStorage.Project[] = []; + const sortedProjectNames = Object.keys(projects).sort(); + sortedProjectNames.forEach((projectName) => { + modules.push(projects[projectName]); + }); + resolve(modules); } - } - cursor.continue(); - } else { - // The cursor is done. We have finished reading all the modules. - const modules: commonStorage.Project[] = []; - const sortedProjectNames = Object.keys(projects).sort(); - sortedProjectNames.forEach((projectName) => { - modules.push(projects[projectName]); - }); - callback(modules, ''); - } - }; -} - -export function fetchModuleContent( - modulePath: string, - callback : (content: string | null, error: string) => void): void { - if (!db) { - _openDatabase((success: boolean, errorReason: string) => { - if (success) { - fetchModuleContent(modulePath, callback); - } else { - callback(null, 'Fetch module failed. (' + errorReason + ')'); - } + }; }); - return; } - const getRequest = db.transaction(['modules'], 'readonly') - .objectStore('modules').get(modulePath); - getRequest.onerror = (event: Event) => { - console.log('IndexedDB get request failed:'); - console.log(getRequest.error); - callback(null, 'Fetch module failed. (getRequest error)'); - }; - getRequest.onsuccess = (event: Event) => { - if (getRequest.result === undefined) { - // Module does not exist. - callback(null, 'Module does not exist'); - return; - } - const value = getRequest.result; - callback(value.content, ''); - }; -} - -export function createModule( - moduleType: string, modulePath: string, moduleContent: string, - callback: BooleanCallback): void { - _saveModule(moduleType, modulePath, moduleContent, callback); -} - -export function saveModule( - modulePath: string, moduleContent: string, - callback: BooleanCallback): void { - _saveModule('', modulePath, moduleContent, callback); -} -function _saveModule( - moduleType: string, modulePath: string, moduleContent: string, - callback: BooleanCallback): void { - // When creating a new module, moduleType must be truthy. - // When saving an existing module, the moduleType must be falsy. - if (!db) { - _openDatabase((success: boolean, errorReason: string) => { - if (success) { - _saveModule(moduleType, modulePath, moduleContent, callback); - } else { - callback(false, 'Save module failed. (' + errorReason + ')'); - } + async fetchModuleContent(modulePath: string): Promise { + return new Promise((resolve, reject) => { + const getRequest = this.db.transaction(['modules'], 'readonly') + .objectStore('modules').get(modulePath); + getRequest.onerror = (event: Event) => { + console.log('IndexedDB get request failed. getRequest.error is...'); + console.log(getRequest.error); + reject(new Error('IndexedDB get request failed.')); + }; + getRequest.onsuccess = (event: Event) => { + if (getRequest.result === undefined) { + // Module does not exist. + reject(new Error('IndexedDB get request succeeded, but the module does not exist.')); + return; + } + resolve(getRequest.result.content); + }; }); - return; } - const transaction = db.transaction(['modules'], 'readwrite'); - transaction.oncomplete = (event: Event) => { - callback(true, ''); - }; - transaction.onabort = () => { - callback(false, 'Save module failed.'); - }; - const modulesObjectStore = transaction.objectStore('modules'); - const getRequest = modulesObjectStore.get(modulePath); - getRequest.onerror = (event: Event) => { - console.log('IndexedDB get request failed:'); - console.log(getRequest.error); - throw new Error('getRequest error'); - }; - getRequest.onsuccess = (event: Event) => { - let value; - if (getRequest.result === undefined) { - // The module does not exist. - // Let's make sure that's what we expected. - if (!moduleType) { - // If moduleType is not truthy, we are trying to save an existing module. - // It is unexpected that the module does not exist. - throw new Error('Unable to save module ' + modulePath + ' because the module does not exist.'); - } - value = Object.create(null); - value.path = modulePath; - value.type = moduleType; - } else { - // The module already exists. - // Let's make sure if that's what we expected. - if (moduleType) { - // Since moduleType is truthy, we are trying to create a new module. - // It is unexpected that the module already exists. - throw new Error('Unable to create module ' + modulePath + ' because the module already exists.'); - } - value = getRequest.result; - } - value.content = moduleContent; - value.dateModifiedMillis = Date.now(); - const putRequest = modulesObjectStore.put(value); - putRequest.onerror = (event: Event) => { - console.log('IndexedDB put request failed:'); - console.log(putRequest.error); - throw new Error('putRequest error'); - }; - }; -} -function _renameOrCopyProject( - oldProjectName: string, newProjectName: string, - callback: BooleanCallback, copy: boolean): void { - const errorMessage = copy - ? 'Copy Project failed.' - : 'Rename Project failed.' - if (!db) { - _openDatabase((success: boolean, errorReason: string) => { - if (success) { - _renameOrCopyProject(oldProjectName, newProjectName, callback, copy); - } else { - callback(false, errorMessage + ' (' + errorReason + ')'); - } - }); - return; + async createModule(moduleType: string, modulePath: string, moduleContent: string): Promise { + return this._saveModule(moduleType, modulePath, moduleContent); } - const transaction = db.transaction(['modules'], 'readwrite'); - transaction.oncomplete = (event: Event) => { - callback(true, ''); - }; - transaction.onabort = () => { - callback(false, errorMessage); - }; - const modulesObjectStore = transaction.objectStore('modules'); - // First get the list of modules in the project. - const oldToNewModulePaths: {[key: string]: string} = {}; - const openCursorRequest = modulesObjectStore.openCursor(); - openCursorRequest.onerror = (event: Event) => { - console.log('IndexedDB openCursor request failed:'); - console.log(openCursorRequest.error); - throw new Error('openCursorRequest error'); - }; - openCursorRequest.onsuccess = (event: Event) => { - const cursor = openCursorRequest.result; - if (cursor) { - const value = cursor.value; - const path = value.path; - const moduleType = value.type; - if (commonStorage.getProjectName(path) === oldProjectName) { - let newPath; - if (moduleType === commonStorage.MODULE_TYPE_PROJECT) { - newPath = commonStorage.makeProjectPath(newProjectName); + async saveModule(modulePath: string, moduleContent: string): Promise { + return this._saveModule('', modulePath, moduleContent); + } + + private async _saveModule(moduleType: string, modulePath: string, moduleContent: string) + : Promise { + // When creating a new module, moduleType must be truthy. + // When saving an existing module, the moduleType must be falsy. + return new Promise((resolve, reject) => { + const transaction = this.db.transaction(['modules'], 'readwrite'); + transaction.oncomplete = (event: Event) => { + resolve(); + }; + transaction.onabort = () => { + console.log('IndexedDB transaction aborted.'); + reject(new Error('IndexedDB transaction aborted.')); + }; + const modulesObjectStore = transaction.objectStore('modules'); + const getRequest = modulesObjectStore.get(modulePath); + getRequest.onerror = (event: Event) => { + console.log('IndexedDB get request failed. getRequest.error is...'); + console.log(getRequest.error); + throw new Error('IndexedDB get request failed.'); + }; + getRequest.onsuccess = (event: Event) => { + let value; + if (getRequest.result === undefined) { + // The module does not exist. + // Let's make sure that's what we expected. + if (!moduleType) { + // If moduleType is not truthy, we are trying to save an existing module. + // It is unexpected that the module does not exist. + console.log('IndexedDB get request succeeded, but the module does not exist.'); + throw new Error('IndexedDB get request succeeded, but the module does not exist.'); + } + value = Object.create(null); + value.path = modulePath; + value.type = moduleType; } else { - const moduleName = commonStorage.getModuleName(path); - newPath = commonStorage.makeModulePath(newProjectName, moduleName); + // The module already exists. + // Let's make sure if that's what we expected. + if (moduleType) { + // Since moduleType is truthy, we are trying to create a new module. + // It is unexpected that the module already exists. + console.log('IndexedDB get request succeeded, but the module already exist.'); + throw new Error('IndexedDB get request succeeded, but the module already exists.'); + } + value = getRequest.result; } - oldToNewModulePaths[path] = newPath; - } - cursor.continue(); - } else { - // Now rename the project for each of the modules. - Object.entries(oldToNewModulePaths).forEach(([oldModulePath, newModulePath]) => { - const getRequest = modulesObjectStore.get(oldModulePath); - getRequest.onerror = (event: Event) => { - console.log('IndexedDB get request failed:'); - console.log(getRequest.error); - throw new Error('getRequest error'); + value.content = moduleContent; + value.dateModifiedMillis = Date.now(); + const putRequest = modulesObjectStore.put(value); + putRequest.onerror = (event: Event) => { + console.log('IndexedDB put request failed. putRequest.error is...'); + console.log(putRequest.error); + throw new Error('IndexedDB put request failed.'); }; - getRequest.onsuccess = (event: Event) => { - if (getRequest.result === undefined) { - throw new Error('project not found'); + }; + }); + } + + private async _renameOrCopyProject(oldProjectName: string, newProjectName: string, copy: boolean): Promise { + return new Promise((resolve, reject) => { + const errorMessage = copy + ? 'Copy Project failed.' + : 'Rename Project failed.' + + const transaction = this.db.transaction(['modules'], 'readwrite'); + transaction.oncomplete = (event: Event) => { + resolve(); + }; + transaction.onabort = () => { + console.log('IndexedDB transaction aborted.'); + reject(new Error('IndexedDB transaction aborted.')); + }; + const modulesObjectStore = transaction.objectStore('modules'); + // First get the list of modules in the project. + const oldToNewModulePaths: {[key: string]: string} = {}; + const openCursorRequest = modulesObjectStore.openCursor(); + openCursorRequest.onerror = (event: Event) => { + console.log('IndexedDB openCursor request failed. openCursorRequest.error is...'); + console.log(openCursorRequest.error); + throw new Error('IndexedDB openCursor request failed.'); + }; + openCursorRequest.onsuccess = (event: Event) => { + const cursor = openCursorRequest.result; + if (cursor) { + const value = cursor.value; + const path = value.path; + const moduleType = value.type; + if (commonStorage.getProjectName(path) === oldProjectName) { + let newPath; + if (moduleType === commonStorage.MODULE_TYPE_PROJECT) { + newPath = commonStorage.makeProjectPath(newProjectName); + } else { + const moduleName = commonStorage.getModuleName(path); + newPath = commonStorage.makeModulePath(newProjectName, moduleName); + } + oldToNewModulePaths[path] = newPath; } - const value = getRequest.result; - value.path = newModulePath; - value.dateModifiedMillis = Date.now(); - const putRequest = modulesObjectStore.put(value); - putRequest.onerror = (event: Event) => { - console.log('IndexedDB put request failed:'); - console.log(putRequest.error); - throw new Error('putRequest error'); - }; - putRequest.onsuccess = (event: Event) => { - if (!copy) { - const deleteRequest = modulesObjectStore.delete(oldModulePath); - deleteRequest.onerror = (event: Event) => { - console.log('IndexedDB delete request failed:'); - console.log(deleteRequest.error); - throw new Error('deleteRequest error'); + cursor.continue(); + } else { + // Now rename the project for each of the modules. + Object.entries(oldToNewModulePaths).forEach(([oldModulePath, newModulePath]) => { + const getRequest = modulesObjectStore.get(oldModulePath); + getRequest.onerror = (event: Event) => { + console.log('IndexedDB get request failed. getRequest.error is...'); + console.log(getRequest.error); + throw new Error('IndexedDB get request failed.'); + }; + getRequest.onsuccess = (event: Event) => { + if (getRequest.result === undefined) { + console.log('IndexedDB get request succeeded, but the module does not exist.'); + throw new Error('IndexedDB get request succeeded, but the module does not exist.'); + } + const value = getRequest.result; + value.path = newModulePath; + value.dateModifiedMillis = Date.now(); + const putRequest = modulesObjectStore.put(value); + putRequest.onerror = (event: Event) => { + console.log('IndexedDB put request failed. putRequest.error is...'); + console.log(putRequest.error); + throw new Error('IndexedDB put request failed.'); }; - deleteRequest.onsuccess = (event: Event) => { + putRequest.onsuccess = (event: Event) => { + if (!copy) { + const deleteRequest = modulesObjectStore.delete(oldModulePath); + deleteRequest.onerror = (event: Event) => { + console.log('IndexedDB delete request failed. deleteRequest.error is...'); + console.log(deleteRequest.error); + throw new Error('IndexedDB delete request failed.'); + }; + deleteRequest.onsuccess = (event: Event) => { + }; + } }; - } - }; - }; - }); - } - }; -} - -export function renameModule( - moduleType: string, projectName: string, - oldModuleName: string, newModuleName: string, - callback: BooleanCallback): void { - _renameOrCopyModule( - moduleType, projectName, oldModuleName, newModuleName, - callback, false); -} - -export function copyModule( - moduleType: string, projectName: string, - oldModuleName: string, newModuleName: string, - callback: BooleanCallback): void { - _renameOrCopyModule( - moduleType, projectName, oldModuleName, newModuleName, - callback, true); -} - -function _renameOrCopyModule( - moduleType: string, projectName: string, - oldModuleName: string, newModuleName: string, - callback: BooleanCallback, copy: boolean): void { - - if (moduleType == commonStorage.MODULE_TYPE_PROJECT) { - _renameOrCopyProject(oldModuleName, newModuleName, callback, copy); - return; + }; + }); + } + }; + }); } - const errorMessage = copy - ? 'Copy module failed.' - : 'Rename module failed.' + async renameModule( + moduleType: string, projectName: string, + oldModuleName: string, newModuleName: string): Promise { + return this._renameOrCopyModule( + moduleType, projectName, oldModuleName, newModuleName, false); + } - if (!db) { - _openDatabase((success: boolean, errorReason: string) => { - if (success) { - _renameOrCopyModule( - moduleType, projectName, oldModuleName, newModuleName, - callback, copy); - } else { - callback(false, errorMessage + '(' + errorReason + ')'); - } - }); - return; + async copyModule( + moduleType: string, projectName: string, + oldModuleName: string, newModuleName: string): Promise { + return this._renameOrCopyModule( + moduleType, projectName, oldModuleName, newModuleName, true); } - const transaction = db.transaction(['modules'], 'readwrite'); - transaction.oncomplete = (event: Event) => { - callback(true, ''); - }; - transaction.onabort = () => { - callback(false, errorMessage); - }; - const modulesObjectStore = transaction.objectStore('modules'); - const oldModulePath = commonStorage.makeModulePath(projectName, oldModuleName); - const newModulePath = commonStorage.makeModulePath(projectName, newModuleName); - const getRequest = modulesObjectStore.get(oldModulePath); - getRequest.onerror = (event: Event) => { - console.log('IndexedDB get request failed:'); - console.log(getRequest.error); - throw new Error('getRequest error'); - }; - getRequest.onsuccess = (event: Event) => { - if (getRequest.result === undefined) { - throw new Error('module not found'); - return; + private async _renameOrCopyModule( + moduleType: string, projectName: string, + oldModuleName: string, newModuleName: string, copy: boolean): Promise { + + if (moduleType == commonStorage.MODULE_TYPE_PROJECT) { + return this._renameOrCopyProject(oldModuleName, newModuleName, copy); } - const value = getRequest.result; - value.path = newModulePath; - value.dateModifiedMillis = Date.now(); - const putRequest = modulesObjectStore.put(value); - putRequest.onerror = (event: Event) => { - console.log('IndexedDB put request failed:'); - console.log(putRequest.error); - throw new Error('putRequest error'); - }; - putRequest.onsuccess = (event: Event) => { - if (!copy) { - const deleteRequest = modulesObjectStore.delete(oldModulePath); - deleteRequest.onerror = (event: Event) => { - console.log('IndexedDB delete request failed:'); - console.log(deleteRequest.error); - throw new Error('deleteRequest error'); - }; - deleteRequest.onsuccess = (event: Event) => { - }; - } - }; - }; -} -function _deleteProject( - projectName: string, callback: BooleanCallback): void { - if (!db) { - _openDatabase((success: boolean, errorReason: string) => { - if (success) { - deleteProject(projectName, callback); - } else { - callback(false, 'Delete project failed. (' + errorReason + ')'); - } - }); - return; - } + return new Promise((resolve, reject) => { + const errorMessage = copy + ? 'Copy module failed.' + : 'Rename module failed.' - const transaction = db.transaction(['modules'], 'readwrite'); - transaction.oncomplete = (event: Event) => { - callback(true, ''); - }; - transaction.onabort = () => { - callback(false, 'Delete project failed.'); - }; - const modulesObjectStore = transaction.objectStore('modules'); - // First get the list of modulePaths in the project. - const modulePaths: string[] = []; - const openCursorRequest = modulesObjectStore.openCursor(); - openCursorRequest.onerror = (event: Event) => { - console.log('IndexedDB openCursor request failed:'); - console.log(openCursorRequest.error); - throw new Error('openCursorRequest error'); - }; - openCursorRequest.onsuccess = (event: Event) => { - const cursor = openCursorRequest.result; - if (cursor) { - const value = cursor.value; - const path = value.path; - if (commonStorage.getProjectName(path) === projectName) { - modulePaths.push(path); - } - cursor.continue(); - } else { - // Now delete each of the modules. - modulePaths.forEach((modulePath) => { - const deleteRequest = modulesObjectStore.delete(modulePath); - deleteRequest.onerror = (event: Event) => { - console.log('IndexedDB delete request failed:'); - console.log(deleteRequest.error); - throw new Error('deleteRequest error'); + const transaction = this.db.transaction(['modules'], 'readwrite'); + transaction.oncomplete = (event: Event) => { + resolve(); + }; + transaction.onabort = () => { + console.log('IndexedDB transaction aborted.'); + reject(new Error('IndexedDB transaction aborted.')); + }; + const modulesObjectStore = transaction.objectStore('modules'); + const oldModulePath = commonStorage.makeModulePath(projectName, oldModuleName); + const newModulePath = commonStorage.makeModulePath(projectName, newModuleName); + const getRequest = modulesObjectStore.get(oldModulePath); + getRequest.onerror = (event: Event) => { + console.log('IndexedDB get request failed. getRequest.error is...'); + console.log(getRequest.error); + throw new Error('IndexedDB get request failed.'); + }; + getRequest.onsuccess = (event: Event) => { + if (getRequest.result === undefined) { + console.log('IndexedDB get request succeeded, but the module does not exist.'); + throw new Error('IndexedDB get request succeeded, but the module does not exist.'); + return; + } + const value = getRequest.result; + value.path = newModulePath; + value.dateModifiedMillis = Date.now(); + const putRequest = modulesObjectStore.put(value); + putRequest.onerror = (event: Event) => { + console.log('IndexedDB put request failed. putRequest.error is...'); + console.log(putRequest.error); + throw new Error('IndexedDB put request failed.'); }; - deleteRequest.onsuccess = (event: Event) => { + putRequest.onsuccess = (event: Event) => { + if (!copy) { + const deleteRequest = modulesObjectStore.delete(oldModulePath); + deleteRequest.onerror = (event: Event) => { + console.log('IndexedDB delete request failed. deleteRequest.error is...'); + console.log(deleteRequest.error); + throw new Error('IndexedDB delete request failed.'); + }; + deleteRequest.onsuccess = (event: Event) => { + }; + } }; - }); - } - }; -} - -export function deleteModule( - moduleType: string, modulePath: string, - callback: BooleanCallback): void { - - if (moduleType == commonStorage.MODULE_TYPE_PROJECT) { - const projectName = commonStorage.getProjectName(modulePath); - _deleteProject(projectName, callback); - return; - } - - if (!db) { - _openDatabase((success: boolean, errorReason: string) => { - if (success) { - deleteModule(modulePath, callback); - } else { - callback(false, 'Delete module failed. (' + errorReason + ')'); - } + }; }); - return; } - const transaction = db.transaction(['modules'], 'readwrite'); - transaction.oncomplete = (event: Event) => { - callback(true, ''); - }; - transaction.onabort = () => { - callback(false, 'Delete module failed.'); - }; - const modulesObjectStore = transaction.objectStore('modules'); - const deleteRequest = modulesObjectStore.delete(modulePath); - deleteRequest.onerror = (event: Event) => { - console.log('IndexedDB delete request failed:'); - console.log(deleteRequest.error); - throw new Error('deleteRequest error'); - }; - deleteRequest.onsuccess = (event: Event) => { - }; -} - -export async function downloadProject(projectName: string, callback: StringCallback): void { - if (!db) { - _openDatabase((success: boolean, errorReason: string) => { - if (success) { - downloadProject(projectName, callback); - } else { - callback(null, 'Download project failed. (' + errorReason + ')'); - } + private async _deleteProject(projectName: string): Promise { + return new Promise((resolve, reject) => { + const transaction = this.db.transaction(['modules'], 'readwrite'); + transaction.oncomplete = (event: Event) => { + resolve(); + }; + transaction.onabort = () => { + console.log('IndexedDB transaction aborted.'); + reject(new Error('IndexedDB transaction aborted.')); + }; + const modulesObjectStore = transaction.objectStore('modules'); + // First get the list of modulePaths in the project. + const modulePaths: string[] = []; + const openCursorRequest = modulesObjectStore.openCursor(); + openCursorRequest.onerror = (event: Event) => { + console.log('IndexedDB openCursor request failed. openCursorRequest.error is...'); + console.log(openCursorRequest.error); + throw new Error('IndexedDB openCursor request failed.'); + }; + openCursorRequest.onsuccess = (event: Event) => { + const cursor = openCursorRequest.result; + if (cursor) { + const value = cursor.value; + const path = value.path; + if (commonStorage.getProjectName(path) === projectName) { + modulePaths.push(path); + } + cursor.continue(); + } else { + // Now delete each of the modules. + modulePaths.forEach((modulePath) => { + const deleteRequest = modulesObjectStore.delete(modulePath); + deleteRequest.onerror = (event: Event) => { + console.log('IndexedDB delete request failed. deleteRequest.error is...'); + console.log(deleteRequest.error); + throw new Error('IndexedDB delete request failed.'); + }; + deleteRequest.onsuccess = (event: Event) => { + }; + }); + } + }; }); - return; } - // Collect all the modules in the project. - const moduleContents: {[key: string]: string} = {}; // key is module name, value is module content - const openCursorRequest = db.transaction(['modules'], 'readonly') - .objectStore('modules') - .openCursor(); - openCursorRequest.onerror = (event: Event) => { - console.log('IndexedDB openCursor request failed:'); - console.log(openCursorRequest.error); - callback(null, 'Download project failed. Could not open cursor.'); - }; - openCursorRequest.onsuccess = async (event: Event) => { - const cursor = openCursorRequest.result; - if (cursor) { - const value = cursor.value; - if (commonStorage.getProjectName(value.path) === projectName) { - const moduleName = commonStorage.getModuleName(value.path); - moduleContents[moduleName] = value.content; - } - cursor.continue(); - } else { - // The cursor is done. We have finished collecting all the modules in the project. - // Now create the blob for download. - const blobUrl = await commonStorage.produceDownloadProjectBlob(projectName, moduleContents); - callback(blobUrl, ''); + async deleteModule(moduleType: string, modulePath: string): Promise { + if (moduleType == commonStorage.MODULE_TYPE_PROJECT) { + const projectName = commonStorage.getProjectName(modulePath); + return this._deleteProject(projectName); } - }; -} -export async function uploadProject(projectName: string, blobUrl: string, callback: BooleanCallback): void { - if (!db) { - _openDatabase((success: boolean, errorReason: string) => { - if (success) { - uploadProject(projectName, url, callback); - } else { - callback(false, 'Upload project failed. (' + errorReason + ')'); - } + return new Promise((resolve, reject) => { + const transaction = this.db.transaction(['modules'], 'readwrite'); + transaction.oncomplete = (event: Event) => { + resolve(); + }; + transaction.onabort = () => { + console.log('IndexedDB transaction aborted.'); + reject(new Error('IndexedDB transaction aborted.')); + }; + const modulesObjectStore = transaction.objectStore('modules'); + const deleteRequest = modulesObjectStore.delete(modulePath); + deleteRequest.onerror = (event: Event) => { + console.log('IndexedDB delete request failed. deleteRequest.error is...'); + console.log(deleteRequest.error); + throw new Error('IndexedDB delete request failed.'); + }; + deleteRequest.onsuccess = (event: Event) => { + }; }); - return; } - // Process the uploaded blob to get the module types and contents. - let moduleTypes: {[key: string]: string}; // key is module name, value is module content - let moduleContents: {[key: string]: string}; // key is module name, value is module content - try { - [moduleTypes, moduleContents] = await commonStorage.processUploadedBlob( - projectName, blobUrl); - } catch (e) { - callback(false, 'Upload project failed. (' + e + ')'); + async downloadProject(projectName: string): Promise { + return new Promise((resolve, reject) => { + // Collect all the modules in the project. + const moduleContents: {[key: string]: string} = {}; // key is module name, value is module content + const openCursorRequest = this.db.transaction(['modules'], 'readonly') + .objectStore('modules') + .openCursor(); + openCursorRequest.onerror = (event: Event) => { + console.log('IndexedDB openCursor request failed. openCursorRequest.error is...'); + console.log(openCursorRequest.error); + reject(new Error('IndexedDB openCursor request failed.')); + }; + openCursorRequest.onsuccess = async (event: Event) => { + const cursor = openCursorRequest.result; + if (cursor) { + const value = cursor.value; + if (commonStorage.getProjectName(value.path) === projectName) { + const moduleName = commonStorage.getModuleName(value.path); + moduleContents[moduleName] = value.content; + } + cursor.continue(); + } else { + // The cursor is done. We have finished collecting all the modules in the project. + // Now create the blob for download. + const blobUrl = await commonStorage.produceDownloadProjectBlob(projectName, moduleContents); + resolve(blobUrl); + } + }; + }); } - // Save each module. - const transaction = db.transaction(['modules'], 'readwrite'); - transaction.oncomplete = (event: Event) => { - callback(true, ''); - }; - transaction.onabort = () => { - callback(false, 'Upload project failed.'); - }; - const modulesObjectStore = transaction.objectStore('modules'); - - for (let moduleName in moduleTypes) { - const moduleType = moduleTypes[moduleName]; - const moduleContent = moduleContents[moduleName]; - const modulePath = commonStorage.makeModulePath(projectName, moduleName); - const getRequest = modulesObjectStore.get(modulePath); - getRequest.onerror = (event: Event) => { - console.log('IndexedDB get request failed:'); - console.log(getRequest.error); - throw new Error('Unable to create module ' + modulePath - + '. (getRequest error)'); - }; - getRequest.onsuccess = (event: Event) => { - if (getRequest.result !== undefined) { - // The module already exists. That is not expected! - throw new Error('Unable to create module ' + modulePath + ' because the module already exists.'); + async uploadProject(projectName: string, blobUrl: string): Promise { + return new Promise(async (resolve, reject) => { + // Process the uploaded blob to get the module types and contents. + let moduleTypes: {[key: string]: string}; // key is module name, value is module content + let moduleContents: {[key: string]: string}; // key is module name, value is module content + try { + [moduleTypes, moduleContents] = await commonStorage.processUploadedBlob( + projectName, blobUrl); + } catch (e) { + console.log('commonStorage.processUploadedBlob failed.'); + reject(new Error('commonStorage.processUploadedBlob failed.')); + return; } - const value = Object.create(null); - value.path = modulePath; - value.type = moduleType; - value.content = moduleContent; - value.dateModifiedMillis = Date.now(); - const putRequest = modulesObjectStore.put(value); - putRequest.onerror = (event: Event) => { - console.log('IndexedDB put request failed:'); - console.log(putRequest.error); - throw new Error('Unable to create module. (putRequest error)'); + + // Save each module. + const transaction = this.db.transaction(['modules'], 'readwrite'); + transaction.oncomplete = (event: Event) => { + resolve(); }; - }; + transaction.onabort = () => { + console.log('IndexedDB transaction aborted.'); + reject(new Error('IndexedDB transaction aborted.')); + }; + const modulesObjectStore = transaction.objectStore('modules'); + + for (let moduleName in moduleTypes) { + const moduleType = moduleTypes[moduleName]; + const moduleContent = moduleContents[moduleName]; + const modulePath = commonStorage.makeModulePath(projectName, moduleName); + const getRequest = modulesObjectStore.get(modulePath); + getRequest.onerror = (event: Event) => { + console.log('IndexedDB get request failed. getRequest.error is...'); + console.log(getRequest.error); + throw new Error('IndexedDB get request failed.'); + }; + getRequest.onsuccess = (event: Event) => { + if (getRequest.result !== undefined) { + // The module already exists. That is not expected! + console.log('IndexedDB get request succeeded, but the module already exists.'); + throw new Error('IndexedDB get request succeeded, but the module already exists.'); + } + const value = Object.create(null); + value.path = modulePath; + value.type = moduleType; + value.content = moduleContent; + value.dateModifiedMillis = Date.now(); + const putRequest = modulesObjectStore.put(value); + putRequest.onerror = (event: Event) => { + console.log('IndexedDB put request failed. putRequest.error is...'); + console.log(putRequest.error); + throw new Error('IndexedDB put request failed.'); + }; + }; + } + }); } } diff --git a/src/storage/common_storage.ts b/src/storage/common_storage.ts index 66342aed..4d13bf6d 100644 --- a/src/storage/common_storage.ts +++ b/src/storage/common_storage.ts @@ -66,6 +66,20 @@ const NUMBER_OF_PARTS = 3; export const UPLOAD_DOWNLOAD_FILE_EXTENSION = '.blocks'; +export interface Storage { + async saveEntry(entryKey: string, entryValue: string): Promise; + async fetchEntry(entryKey: string, defaultValue: string): Promise; + async listModules(): Promise; + async fetchModuleContent(modulePath: string): Promise; + async createModule(moduleType: string, modulePath: string, moduleContent: string): Promise; + async saveModule(modulePath: string, moduleContent: string): Promise; + async renameModule(moduleType: string, projectName: string, oldModuleName: string, newModuleName: string): Promise; + async copyModule(moduleType: string, projectName: string, oldModuleName: string, newModuleName: string): Promise; + async deleteModule(moduleType: string, modulePath: string): Promise; + async downloadProject(projectName: string): Promise; + async uploadProject(projectName: string, blobUrl: string): Promise; +} + /** * Returns the module with the given module path, or null if it is not found. */