The storage object provides an interface to device and cloud-based storage and backup for user input. Device storage allows the application to restart with the same settings when it was last active. Device storage is implemented using the browser's localStorage. The user's saved input is stored in localStorage under the key ${ctl.appId}_input. A new set of user input is saved after an input state variable change causes the Controller to synchronize the Model and View.
Detecting localStorage changes made by another tab of the same app is not implemented. The code to do so is commented out in storage.init(), so each tab works from its own state and the last one to save wins.
storage is not exported by mvcs/export.js. The Controller imports it internally, and other modules that need it import it directly:
import {storage} from '../mvcs/storage.js';
(adjust the relative path to the module's location).
storage.init() is called by ctl._init() and does the following:
localStorage keys ${ctl.appId}_guid and ${ctl.appId}_input.${ctl.appId}Input to the ${ctl.appId}_input key.storage.guid, creating it if necessary.storage page, which holds the state variables storage_stateMsg (a text status message), storage_state ('enabled' or 'disabled'), storage_cloudState (see Cloud state), and storage_isOnline.storage.setCloudData().On an installed iOS app, storage.migrateLegacyLocalStorage() runs before ctl._init().
The storage_cloudState state variable records the state of cloud backup and Cloud Sync:
'inactive': Cloud storage is disabled.'active': Cloud Sync is active with an email address and password.'active:backup': Anonymous cloud backup is active, associated with the GUID only.'active:cloudSync': Retrieving data written by another device.'checking': Checking the server for data associated with a new email address and password.'user': Waiting for the user to respond to a dialog.'serverOffline': The device is connected but the server is not responding.'offline': The device is disconnected from the Internet.'incompatible': The cloud data was saved by a newer app version. Cloud backup is suspended until the app is updated.'error': A network or server error occurred.Cloud saving is only attempted when storage is enabled, the device is online, server.isEnabled is true, and the cloud state is none of 'inactive', 'incompatible', 'user', or 'checking'.
When an application starts up on a device for the first time, it creates a globally unique ID (GUID) by storing the string GUID-${Date.now()} in localStorage under the key ${ctl.appId}_guid.
Cloud storage saves the current data and takes backup snapshots of the data that the user can request in case local storage becomes corrupt or they make an unrecoverable mistake when entering data. When the server detects it is storing new data that is more than a day newer than the previous data, it saves the previous data as a backup snapshot. Up to four backup snapshots will be preserved. When a new snapshot is created, the oldest snapshot is discarded after four snapshots are present. If the user has not entered an email address and password, all cloud storage is associated with the GUID.
The storage.setState() method saves user input data locally, and if online, a copy is sent to the cloud. The storage.getState() method only gets the local state.
Saving data can be disabled and re-enabled using the storage.disable() and storage.enable() methods, respectively. Storage can be disabled for many reasons, such as running a self-test or loading a trouble report. A subsystem that disables and enables saving can use a unique id string so that competing requests can be matched. Storage is only re-enabled when all the disabled IDs have been re-enabled. When storage becomes enabled, the current app state is saved using ctl.save().
Cloud Sync allows user input data to be transferred between devices and saved input to be recovered when a device is lost. It is enabled when the user enters an email address and a password. Once Cloud Sync is enabled, data stored on the server is associated with the email address and password. Any data previously associated with the GUID (including backup snapshots) is also associated with the email address and password.
By design, this procedure does not require any user registration process. The user may choose any combination of email address and password. Setting a new password will create an independent set of data from the old password. The user can deliberately use this to keep, for example, a different set of aircraft data for testing or to keep aircraft associated with different companies separated.
The server will accept email addresses and passwords with the following characters:
/[\w\-+!#$%&'()*+,.;=@\[\]\^_`{}~]/
The email must have the form of an email address with any illegal characters stripped out but is otherwise unverified. The password must have at least two characters after any illegal character are stripped out. The character restrictions allow the server to use raw files to store user data.
When new user input is storage to device storage, a copy is also sent to the cloud using the server.setSavedInput() method. The server will return an error if the GUID in the passed id argument does not match the GUID in the previously saved input data. This tells the app that another device with a different GUID but the same email and password has written new data since the last time is wrote. The app then asks the user whether they want to use this device's current data or transfer the other device's data from the cloud.
If the user wants to use the cloud data, the data is retrieved using the server.getSavedInput() method, and the data is used to set the app's inputs. In either case, the app's state is then saved to the cloud using storage.setCloudData() method with the takeover argument set to true. This forces the server to overwrite the other device's data with this device's data, which includes this device's GUID.
The storage object also contain interfaces to the indexedDB browser service to store larger objects using a key. In the POH Performance apps, this is primarily used to store airport database updates. The object storage interfaces are:
storage.getObjStoreVal()storage.setObjStoreVal()storage.hasObjStoreVal()storage.deleteObjStoreVal()storage methodsdeleteObjStoreVal()storage.deleteObjStoreVal(key)
Deletes the value associated with key.
keyReturns a Promise that resolves if the operation is successful and rejects with the event errorCode when it is unsuccessful.
disable()storage.disable()
storage.disable(id)
Disables storage saving. An id string that identifies the subsystem that will re-enable storage should be provided.
id (optional)enable()storage.enable()
storage.enable(id)
Enables storage saving if no other subsystems have disabled storage. An id string that identifies the subsystem that disabled storage should be provided. If id is provided but did not previously disable storage, the call is ignored. When storage becomes enabled, enable() calls ctl.save() to save the current app state.
id (optional)getCloudData()storage.getCloudData()
Gets the cloud data associated with server.id. Returns a Promise that resolves with a input state object, if the operation is successful. The method calls the server.getSavedInput() to make the request. The result is decoded from JSON and upgraded to the current app version using ctl.upgradeSavedState()) before being returned. Any state variable values whose save property is 'local' are then replaced with this device's current values, so they are preserved rather than overwritten by the cloud data.
If ctl.upgradeSavedState() returns an error string, then the Promise is rejected with the error string.
Returns a Promise that resolves if the operation is successful and rejects with one of the following error strings:
'corrupt''incompatible''nodata'server.id,'offline''serverOffline'getObjStoreVal()storage.getObjStoreVal(key)
Returns a Promise that resolves with the value associated with key.
keyReturns a Promise that resolves with the value associated with key if the operation is successful. The Promise rejects with the event errorCode when it is unsuccessful.
getState()storage.getState()
Get and decode locally stored user input data. Returns the result of fetching and decoding the JSON string stored in localStorage under the ${ctl.appId}_input key. If successful, the decoded results are upgraded to the current app version (see ctl.upgradeSavedState()) before being returned.
One of the following:
storage.setState() and upgraded to be compatible with the current app version.'corrupt' if it cannot be decoded, or the error string from ctl.upgradeSavedState() (such as 'incompatible').undefined if nothing is stored.ctl.restore() resets the app values if the result is not an object.
hasObjStoreVal()storage.hasObjStoreVal(key)
Returns a Promise that resolves with true if there is a value associated with key.
keyReturns a Promise that resolves with true if there is a value associated with key, if the operation is successful. The Promise rejects with the event errorCode when it is unsuccessful.
isValidCloudId()storage.isValidCloudId(email, password)
Returns a true value if both email and password are valid. To be valid, the email string must match the form x@y.z (any characters, with @ and a following . separated by at least one character) when illegal characters are stripped. To be valid, the password string must be at least two characters in length when illegal characters are stripped. Empty strings are not valid.
emailserver.id.passwordserver.id.A true value if both parameters meet the validity requirements. Otherwise a false value, which can be null (the result of a failed email match) rather than false.
migrateLegacyLocalStorage()storage.migrateLegacyLocalStorage()
On an installed iOS Cordova app, migrates saved input (and the GUID) from the legacy file:// origin to the current app://localhost origin using the legacyLocalStorage Cordova plugin. It never overwrites data already present at the current origin, and all failures fail open so that normal startup continues. This method is called by the Controller before ctl._init().
Returns a Promise that always resolves with one of the following strings:
'current''unsupported''nodata''migrated''error'newCloudId()storage.newCloudId(email, password)
Sets a new cloud sync email and password. If the server has data associated with this email/password, then use it to set the app state. If not, then ask the user whether they want to create a new data store on the server. This prevents accidentally creating a new store for a mis-typed email or password.
If email or password is empty, the request is accepted as an anonymous backup: the cloud state is set to 'active:backup' and the Promise resolves with 'success' without contacting the server.
If both are non-empty and storage.isValidCloudId() is false, the Promise is rejected with 'badSaveId'. If the device is offline or server.isEnabled is false, the new ID is accepted without checking the server.
If email and password are valid (see storage.isValidCloudId()), then newCloudId() composes a new JSON-encoded ID object containing email, password, and guid elements and set server.id.
If the retrieved state is from a newer version of the app, then it's considered incompatible and Cloud Sync is disabled until the app is upgraded.
emailserver.id.passwordserver.id.Returns a Promise that resolves with 'success' if the new ID is accepted and is rejected if the user cancels email/password change or if it's invalid. Rejection returns one of the following strings:
'badSaveId''cancel'server.id is unchanged.'incompatible'setCloudData()storage.setCloudData()
storage.setCloudData(input)
storage.setCloudData(input, ask)
Sets the cloud data associated with server.id to input. Returns a Promise that resolves if the operation is successful. The method calls the server.setSavedInput() to make the request. If cloud backup is not ready (see Cloud state), the Promise resolves with 'success' immediately without contacting the server.
Writes are serialized: each write is queued behind the previous one and starts after it settles, whatever its result. The returned Promise is the end of this queue.
If the server responds with a status code of 409 (Conflict), it indicates that the GUID element in server.id did not match the GUID in the previously stored input data because another device last wrote it. In that case, if ask is omitted or falsey, then storage.setCloudData() will retrieve the cloud data using the server.getSavedInput() method and use it to set the app's input state. If ask is truthy, then storage.setCloudData() will issue a dialog.choose() that ask the user to choose between the current app state and the cloud data from the other device. If the cloud data is chosen then the storage.setCloudData() will retrieve the cloud data using server.getSavedInput() and use it to set the app's input state. Finally, once the previous steps are done, storage.setCloudData() calls the server.setSavedInput() method one more time with the latest app state and the takeover argument set to true to store take ownership of the cloud data.
input (optional)storage.setState() in localStorage is used.ask (optional)ask is omitted or falsey, then storage.setCloudData() will retrieve the cloud data and use it to set the app's input state. Otherwise, if ask is truthy, then storage.setCloudData() will ask the user to choose between the current app state and the cloud data from the other device.Returns a Promise that resolves with 'success' if the operation is successful. It rejects with the numeric server.setSavedInput() response status code, or with the string 'offline' or 'serverOffline' (for example when the cloud data must be retrieved after a 409 and that retrieval fails), when it is unsuccessful.
startCloudSync()storage.startCloudSync(email, password)
Starts Cloud Sync with an initial email and password. Sets storage.email to email. If both email and password are valid (see storage.isValidCloudId(), then it sets storage.password to password. Otherwise, it sets it to ''. It sets server.id to a JSON string containing email, password, and guid. If cloud backup is ready, it sets storage_cloudState to 'active' when storage.email and storage.password are both set, and to 'active:backup' otherwise.
Called from the initialization routing of the page that contains the email and password.
emailserver.id.passwordserver.id.setObjStoreVal()storage.setObjStoreVal(key, value)
Sets the value associated with key.
keyvaluekey.Returns a Promise that resolves if the operation is successful and rejects with the event errorCode when it is unsuccessful.
setState()storage.setState(input)
Set the locally stored saved input to input. If storage is disabled (see Enable and disable), nothing is saved. Otherwise, input is JSON-encoded and saved to localStorage, and if cloud backup is ready (see Cloud state), a copy is saved to the cloud as well (see storage.setCloudData()). The first call does not ask the user about ownership conflicts; later calls do.
inputctl.getSavedState().Returns a Promise that resolves with 'noop' if storage is disabled, and with 'success' when the local and cloud operations are complete. If the cloud operation fails, the Promise rejects as described for storage.setCloudData().
storage propertiesemailThe current email address to use in server.id.
guidThe current GUID to use in server.id.
passwordThe current password to use in server.id.