Improved CustomStorage, performance+++

This commit is contained in:
Ewout Stortenbeker 2020-04-25 11:30:28 +02:00
parent 51e63f2bdc
commit 79ac2fda4d
10 changed files with 2332 additions and 1548 deletions

111
README.md
View file

@ -863,60 +863,92 @@ const db = AceBase.WithIndexedDB('dbname');
It is now possible to store data in your own custom storage backend. To do this, you only have to provide a couple of methods to get, set and remove data and you're done.
The example below shows how to implement a ```CustomStorage``` class that uses the browser's localStorage (NOTE: you can also use ```LocalStorageSettings``` described above to do the same):
The example below shows how to implement a ```CustomStorage``` class that uses the browser's localStorage (NOTE: you can also use ```AceBase.WithLocalStorage``` described above to do the same):
```javascript
const { AceBase, CustomStorageSettings, CustomStorageHelpers } = require('acebase');
const { AceBase, CustomStorageSettings, CustomStorageTransaction, CustomStorageHelpers } = require('acebase');
// Helper functions to prefix all localStorage keys with dbname
// to allows multiple db's in localStorage:
const dbname = 'test';
const storageKeysPrefix = `${dbname}.acebase::`;
function getPathFromStorageKey(key) {
return key.slice(storageKeysPrefix.length);
}
function getStorageKeyForPath(path) {
return `${storageKeysPrefix}${path}`;
}
// Setup our CustomStorageSettings
const customStorageSettings = new CustomStorageSettings({
info: 'Custom LocalStorage',
const storageSettings = new CustomStorageSettings({
name: 'LocalStorage',
locking: true, // Let AceBase handle resource locking to prevent multiple simultanious updates to the same data. NOTE: This does not prevent multiple tabs from doing this!!
ready() {
// LocalStorage is always ready
return Promise.resolve();
},
getTransaction(target) {
// Create an instance of our transaction class
const context = {
debug: true,
dbname
}
const transaction = new LocalStorageTransaction(context, target);
return Promise.resolve(transaction);
}
});
// Setup CustomStorageTransaction for browser's LocalStorage
class LocalStorageTransaction extends CustomStorageTransaction {
/**
* @param {{debug: boolean, dbname: string}} context
* @param {{path: string, write: boolean}} target
*/
constructor(context, target) {
super(target);
this.context = context;
this._storageKeysPrefix = `${this.context.dbname}.acebase::`;
}
commit() {
// All changes have already been committed
return Promise.resolve();
}
rollback(err) {
// Not able to rollback changes, because we did not keep track
return Promise.resolve();
}
get(path) {
// Gets value from localStorage, wrapped in Promise
return new Promise(resolve => {
const json = localStorage.getItem(getStorageKeyForPath(path));
const json = localStorage.getItem(this.getStorageKeyForPath(path));
const val = JSON.parse(json);
resolve(val);
});
},
}
set(path, val) {
// Sets value in localStorage, wrapped in Promise
return new Promise(resolve => {
const json = JSON.stringify(val);
localStorage.setItem(getStorageKeyForPath(path), json);
localStorage.setItem(this.getStorageKeyForPath(path), json);
resolve();
});
},
}
remove(path) {
// Removes a value from localStorage, wrapped in Promise
return new Promise(resolve => {
localStorage.removeItem(getStorageKeyForPath(path));
localStorage.removeItem(this.getStorageKeyForPath(path));
resolve();
});
},
}
childrenOf(path, include, checkCallback, addCallback) {
// Gets all child paths
// Streams all child paths
// Cannot query localStorage, so loop through all stored keys to find children
return new Promise(resolve => {
const pathInfo = CustomStorageHelpers.PathInfo.get(path);
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (!key.startsWith(storageKeysPrefix)) { continue; }
let otherPath = getPathFromStorageKey(key);
if (!key.startsWith(this._storageKeysPrefix)) { continue; }
let otherPath = this.getPathFromStorageKey(key);
if (pathInfo.isParentOf(otherPath) && checkCallback(otherPath)) {
let node;
if (include.metadata || include.value) {
@ -929,16 +961,17 @@ const customStorageSettings = new CustomStorageSettings({
}
resolve();
});
},
}
descendantsOf(path, include, checkCallback, addCallback) {
// Gets all descendant paths
// Streams all descendant paths
// Cannot query localStorage, so loop through all stored keys to find descendants
return new Promise(resolve => {
const pathInfo = CustomStorageHelpers.PathInfo.get(path);
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (!key.startsWith(storageKeysPrefix)) { continue; }
let otherPath = getPathFromStorageKey(key);
if (!key.startsWith(this._storageKeysPrefix)) { continue; }
let otherPath = this.getPathFromStorageKey(key);
if (pathInfo.isAncestorOf(otherPath) && checkCallback(otherPath)) {
let node;
if (include.metadata || include.value) {
@ -952,11 +985,29 @@ const customStorageSettings = new CustomStorageSettings({
resolve();
});
}
});
// Now create AceBase instance using our custom storage:
const db = new AceBase(dbname, { logLevel: 'log', storage: customStorageSettings });
// That's all!
/**
* Helper function to get the path from a localStorage key
* @param {string} key
*/
getPathFromStorageKey(key) {
return key.slice(this._storageKeysPrefix.length);
}
/**
* Helper function to get the localStorage key for a path
* @param {string} path
*/
getStorageKeyForPath(path) {
return `${this._storageKeysPrefix}${path}`;
}
}
// Now, create the database
const db = new AceBase(dbname, { logLevel: settings.logLevel, storage: storageSettings });
db.ready(ready => {
// That's it!
})
```
## Reflect API

1847
dist/browser.js vendored

File diff suppressed because it is too large Load diff

2
dist/browser.min.js vendored

File diff suppressed because one or more lines are too long

71
index.d.ts vendored
View file

@ -100,6 +100,56 @@ export interface ICustomStorageNodeValue {
}
export interface ICustomStorageNode extends ICustomStorageNodeMetaData, ICustomStorageNodeValue {}
export abstract class CustomStorageTransaction {
/**
* @param target Which path the transaction is taking place on, and whether it is a read or read/write lock. If your storage backend does not support transactions, is synchronous, or if you are able to lock resources based on path: use storage.nodeLocker to ensure threadsafe transactions
*/
constructor(target: { path: string, write: boolean });
readonly target: { path: string, readonly originalPath: string, readonly write: boolean };
/** Function that gets the node with given path from your custom data store, must return null if it doesn't exist */
abstract get(path: string): Promise<ICustomStorageNode|null>;
/** Function that inserts or updates a node with given path in your custom data store */
abstract set(path: string, value: ICustomStorageNode): Promise<void>;
/** Function that removes the node with given path from your custom data store */
abstract remove(path: string): Promise<void>;
/**
* Function that streams all stored nodes that are direct children of the given path. For path "parent/path", results must include paths such as "parent/path/key" AND "parent/path[0]". 👉🏻 You can use CustomStorageHelpers for logic. Keep calling the add callback for each node until it returns false.
* @param path Parent path to load children of
* @param include
* @param include.metadata Whether metadata needs to be loaded
* @param include.value Whether value needs to be loaded
* @param checkCallback callback method to precheck if child needs to be added, perform before loading metadata/value if possible
* @param addCallback callback method that adds the child node. Returns whether or not to keep calling with more children
* @returns Returns a promise that resolves when there are no more children to be streamed
*/
abstract childrenOf(path: string, include: { value: boolean, metadata: boolean }, checkCallback: (childPath: string) => boolean, addCallback: (childPath: string, node?: ICustomStorageNodeMetaData|ICustomStorageNode) => boolean): Promise<any>;
/**
* Function that streams all stored nodes that are descendants of the given path. For path "parent/path", results must include paths such as "parent/path/key", "parent/path/key/subkey", "parent/path[0]", "parent/path[12]/key" etc. 👉🏻 You can use CustomStorageHelpers for logic. Keep calling the add callback for each node until it returns false.
* @param path Parent path to load descendants of
* @param include
* @param include.metadata Whether metadata needs to be loaded
* @param include.value Whether value needs to be loaded
* @param checkCallback callback method to precheck if descendant needs to be added, perform before loading metadata/value if possible
* @param addCallback callback method that adds the descendant node. Returns whether or not to keep calling with more children
* @returns Returns a promise that resolves when there are no more descendants to be streamed
*/
abstract descendantsOf(path: string, include: { value: boolean, metadata: boolean }, checkCallback: (childPath: string) => boolean, addCallback: (descPath: string, node?: ICustomStorageNodeMetaData|ICustomStorageNode) => boolean): Promise<any>;
/** (optional, not used yet) Function that gets multiple nodes (metadata AND value) from your custom data store at once. Must return a Promise that resolves with Map<path,value> */
getMultiple?(paths: string[]): Promise<Map<string, ICustomStorageNode|null>>;
/** (optional, not used yet) Function that sets multiple nodes at once */
setMultiple?(nodes: Array<{ path: string, node: ICustomStorageNode }>): Promise<void>;
/** (optional) Function that removes multiple nodes from your custom data store at once */
removeMultiple?(paths: string[]): Promise<void>;
abstract commit(): Promise<void>;
abstract rollback(reason: Error): Promise<void>;
}
/**
* Allows data to be stored in a custom storage backend of your choice! Simply provide a couple of functions
* to get, set and remove data and you're done.
@ -108,22 +158,15 @@ export class CustomStorageSettings extends StorageSettings {
constructor(settings: CustomStorageSettings);
/** Name of the custom storage adapter */
name?: string;
/** Whether default node locking should be used (default). Set to false if your storage backend disallows multiple simultanious write transactions (eg IndexedDB). Set to true if your storage backend does not support transactions (eg LocalStorage) or allows multiple simultanious write transactions (eg AceBase binary). */
locking?: boolean;
/** Function that returns a Promise that resolves once your data store backend is ready for use */
ready(): Promise<any>;
/** Function that gets the node with given path from your custom data store, must return null if it doesn't exist */
get(path: string): Promise<ICustomStorageNode|null>;
/** Function that inserts or updates a node with given path in your custom data store */
set(path: string, value: ICustomStorageNode): Promise<void>;
/** Function that removes the node with given path from your custom data store */
remove(path: string): Promise<void>;
/** Function that streams all stored nodes that are direct children of the given path. For path "parent/path", results must include paths such as "parent/path/key" AND "parent/path[0]". 👉🏻 You can use CustomStorageHelpers for logic. Keep calling the add callback for each node until it returns false. */
childrenOf(path: string, include: { value: boolean, metadata: boolean }, checkCallback: (childPath: string) => boolean, addCallback: (childPath: string, node?: ICustomStorageNodeMetaData|ICustomStorageNode) => boolean): Promise<any>;
/** Function that streams all stored nodes that are descendants of the given path. For path "parent/path", results must include paths such as "parent/path/key", "parent/path/key/subkey", "parent/path[0]", "parent/path[12]/key" etc. 👉🏻 You can use CustomStorageHelpers for logic. Keep calling the add callback for each node until it returns false. */
descendantsOf(path: string, include: { value: boolean, metadata: boolean }, checkCallback: (childPath: string) => boolean, addCallback: (descPath: string, node?: ICustomStorageNodeMetaData|ICustomStorageNode) => boolean): Promise<any>;
// /** (optional, not used yet) Function that gets multiple nodes (metadata AND value) from your custom data store at once. Must return a Promise that resolves with Map<path,value> */
// getMultiple?(paths: string[]): Promise<Map<string, ICustomStorageNode|null>>;
/** (optional) Function that removes multiple nodes from your custom data store at once */
removeMultiple?(paths: string[]): Promise<void>;
/**
* Function that starts a transaction for read/write operations on a specific path and/or child paths
* @param target target path and mode to start transaction on
*/
getTransaction(target: { path: string, write: boolean }): Promise<CustomStorageTransaction>
}
export class CustomStorageHelpers {

View file

@ -1,6 +1,6 @@
{
"name": "acebase",
"version": "0.9.25",
"version": "0.9.26",
"description": "AceBase realtime database engine",
"main": "./src/index.js",
"browser": "./dist/browser.js",
@ -49,7 +49,7 @@
"author": "Ewout Stortenbeker <me@appy.one> (http://appy.one)",
"license": "MIT",
"dependencies": {
"acebase-core": "~0.9.8",
"acebase-core": "~0.9.9",
"colors": "^1.3.2",
"unidecode": "^0.1.8"
}

View file

@ -1,5 +1,5 @@
const { AceBase, AceBaseLocalSettings } = require('./acebase-local');
const { CustomStorageSettings, CustomStorageHelpers, ICustomStorageNode, ICustomStorageNodeMetaData } = require('./storage-custom');
const { CustomStorageSettings, CustomStorageTransaction, CustomStorageHelpers, ICustomStorageNode, ICustomStorageNodeMetaData } = require('./storage-custom');
/**
* @typedef {Object} IIndexedDBNodeData
@ -71,211 +71,27 @@ class BrowserAceBase extends AceBase {
request.onsuccess = e => {
db = request.result;
readyResolve();
}
};
request.onerror = e => {
readyReject(e);
};
const storageSettings = new CustomStorageSettings({
name: 'IndexedDB',
locking: false,
ready() {
return readyPromise;
},
get(path) {
// Get metadata from "nodes" object store
const tx = db.transaction(['nodes', 'content'], 'readonly');
const request = tx.objectStore('nodes').get(path);
return new Promise((resolve, reject) => {
request.onsuccess = event => {
/** @type {IIndexedDBNodeData} */
const data = request.result;
if (!data) {
return resolve(null);
}
/** @type {ICustomStorageNode} */
const node = data.metadata;
// Node exists, get content from "content" object store
const contentReq = tx.objectStore('content').get(path);
contentReq.onsuccess = e => {
node.value = contentReq.result;
resolve(node);
};
contentReq.onerror = e => reject(e);
}
request.onerror = e => {
console.error(`IndexedDB get error`, e);
reject(e);
}
});
},
set(path, node) {
/** @type {ICustomStorageNode} */
const copy = {};
const value = node.value;
Object.assign(copy, node);
delete copy.value;
/** @type {ICustomStorageNodeMetaData} */
const metadata = copy;
/** @type {IIndexedDBNodeData} */
const obj = {
path,
metadata
async getTransaction(target) {
const context = {
debug: true,
db
}
return new Promise((resolve, reject) => {
const tx = db.transaction(['nodes', 'content'], 'readwrite');
// Insert into "nodes" object store first
const request = tx.objectStore('nodes').put(obj);
request.onerror = e => reject(e);
request.onsuccess = e => {
// Now add to "content" object store
const contentReq = tx.objectStore('content').put(value, path);
contentReq.onsuccess = e => resolve();
contentReq.onerror = e => {
tx.abort(); // rollback transaction
reject(e);
}
};
});
},
remove(path) {
const tx = db.transaction(['nodes','content'], 'readwrite');
return new Promise((resolve, reject) => {
// First, remove from "content" object store
const r1 = tx.objectStore('content').delete(path);
r1.onerror = e => reject(e);
r1.onsuccess = e => {
// Now, remove from "nodes" data store
const r2 = tx.objectStore('nodes').delete(path);
r2.onerror = e => {
tx.abort(); // rollback transaction
reject(e);
};
r2.onsuccess = e => resolve();
}
});
},
childrenOf(path, include, checkCallback, addCallback) {
// Use cursor to loop from path on
const pathInfo = CustomStorageHelpers.PathInfo.get(path);
const lockStores = include.value ? ['nodes','content'] : 'nodes';
const tx = db.transaction(lockStores, 'readonly');
const store = tx.objectStore('nodes');
const query = IDBKeyRange.lowerBound(path, true);
return new Promise((resolve, reject) => {
/** @type {IDBRequest<IDBCursorWithValue>|IDBRequest<IDBCursor>} */
const cursor = include.metadata ? store.openCursor(query) : store.openKeyCursor(query);
cursor.onerror = e => reject(e);
cursor.onsuccess = async e => {
/** type {string} */
const otherPath = cursor.result ? cursor.result.key : null;
let keepGoing = true;
if (otherPath === null) {
// No more results
keepGoing = false;
}
else if (!pathInfo.isAncestorOf(otherPath)) {
// Paths are sorted, no more children to be expected!
keepGoing = false;
}
else if (pathInfo.isParentOf(otherPath) && checkCallback(otherPath)) {
/** @type {ICustomStorageNode|ICustomStorageNodeMetaData} */
let node;
if (include.metadata) {
/** @type {IDBRequest<IDBCursorWithValue>} */
const valueCursor = cursor;
/** @type {IIndexedDBNodeData} */
const data = valueCursor.result.value;
node = data.metadata;
if (include.value) {
// Load value!
const req = tx.objectStore('content').get(otherPath);
await new Promise((resolve, reject) => {
req.onerror = e => reject(e);
req.onsuccess = e => {
node.value = req.result;
resolve();
}
});
}
}
keepGoing = addCallback(otherPath, node);
}
if (keepGoing) {
try { cursor.result.continue(); }
catch(err) {
// We reached the end of the cursor?
keepGoing = false;
}
}
if (!keepGoing) {
resolve();
}
};
});
},
descendantsOf(path, include, checkCallback, addCallback) {
// Use cursor to loop from path on
// NOTE: Implementation is almost identical to childrenOf, consider merging them
const pathInfo = CustomStorageHelpers.PathInfo.get(path);
const lockStores = include.value ? ['nodes','content'] : 'nodes';
const tx = db.transaction(lockStores, 'readonly');
const store = tx.objectStore('nodes');
const query = IDBKeyRange.lowerBound(path, true);
return new Promise((resolve, reject) => {
/** @type {IDBRequest<IDBCursorWithValue>|IDBRequest<IDBCursor>} */
const cursor = include.metadata ? store.openCursor(query) : store.openKeyCursor(query);
cursor.onerror = e => reject(e);
cursor.onsuccess = async e => {
/** @type {string} */
const otherPath = cursor.result ? cursor.result.key : null;
let keepGoing = true;
if (otherPath === null) {
// No more results
keepGoing = false;
}
else if (!pathInfo.isAncestorOf(otherPath)) {
// Paths are sorted, no more ancestors to be expected!
keepGoing = false;
}
else if (checkCallback(otherPath)) {
/** @type {ICustomStorageNode|ICustomStorageNodeMetaData} */
let node;
if (include.metadata) {
/** @type {IDBRequest<IDBCursorWithValue>} */
const valueCursor = cursor;
/** @type {IIndexedDBNodeData} */
const data = valueCursor.result.value;
node = data.metadata;
if (include.value) {
// Load value!
const req = tx.objectStore('content').get(otherPath);
await new Promise((resolve, reject) => {
req.onerror = e => reject(e);
req.onsuccess = e => {
node.value = req.result;
resolve();
}
});
}
}
keepGoing = addCallback(otherPath, node);
}
if (keepGoing) {
try { cursor.result.continue(); }
catch(err) {
// We reached the end of the cursor?
keepGoing = false;
}
}
if (!keepGoing) {
resolve();
}
};
});
}
const transaction = new IndexedDBStorageTransaction(context, target);
await transaction.start();
return transaction;
}
});
return new AceBase(dbname, { logLevel: settings.logLevel, storage: storageSettings });
}
@ -295,92 +111,367 @@ class BrowserAceBase extends AceBase {
// Determine whether to use localStorage or sessionStorage
const localStorage = settings.provider ? settings.provider : settings.temp ? window.localStorage : window.sessionStorage;
// Helper functions to prefix all localStorage keys with dbname
// to allows multiple db's in localStorage
const storageKeysPrefix = `${dbname}.acebase::`; // Prefix all localStorage keys with dbname
function getPathFromStorageKey(key) {
return key.slice(storageKeysPrefix.length);
}
function getStorageKeyForPath(path) {
return `${storageKeysPrefix}${path}`;
}
// Setup our CustomStorageSettings
const storageSettings = new CustomStorageSettings({
name: 'LocalStorage',
locking: true,
ready() {
// LocalStorage is always ready
return Promise.resolve();
},
get(path) {
// Gets value from localStorage, wrapped in Promise
return new Promise(resolve => {
const json = localStorage.getItem(getStorageKeyForPath(path));
const val = JSON.parse(json);
resolve(val);
});
},
set(path, val) {
// Sets value in localStorage, wrapped in Promise
return new Promise(resolve => {
const json = JSON.stringify(val);
localStorage.setItem(getStorageKeyForPath(path), json);
resolve();
});
},
remove(path) {
// Removes a value from localStorage, wrapped in Promise
return new Promise(resolve => {
localStorage.removeItem(getStorageKeyForPath(path));
resolve();
});
},
childrenOf(path, include, checkCallback, addCallback) {
// Gets all child paths
// Cannot query localStorage, so loop through all stored keys to find children
return new Promise(resolve => {
const pathInfo = CustomStorageHelpers.PathInfo.get(path);
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (!key.startsWith(storageKeysPrefix)) { continue; }
let otherPath = getPathFromStorageKey(key);
if (pathInfo.isParentOf(otherPath) && checkCallback(otherPath)) {
let node;
if (include.metadata || include.value) {
const json = localStorage.getItem(key);
node = JSON.parse(json);
}
const keepGoing = addCallback(otherPath, node);
if (!keepGoing) { break; }
}
}
resolve();
});
},
descendantsOf(path, include, checkCallback, addCallback) {
// Gets all descendant paths
// Cannot query localStorage, so loop through all stored keys to find descendants
return new Promise(resolve => {
const pathInfo = CustomStorageHelpers.PathInfo.get(path);
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (!key.startsWith(storageKeysPrefix)) { continue; }
let otherPath = getPathFromStorageKey(key);
if (pathInfo.isAncestorOf(otherPath) && checkCallback(otherPath)) {
let node;
if (include.metadata || include.value) {
const json = localStorage.getItem(key);
node = JSON.parse(json);
}
const keepGoing = addCallback(otherPath, node);
if (!keepGoing) { break; }
}
}
resolve();
});
getTransaction(target) {
// Create an instance of our transaction class
const context = {
debug: true,
dbname,
localStorage
}
const transaction = new LocalStorageTransaction(context, target);
return Promise.resolve(transaction);
}
});
return new AceBase(dbname, { logLevel: settings.logLevel, storage: storageSettings });
}
}
class IndexedDBStorageTransaction extends CustomStorageTransaction {
/**
* @param {{debug: boolean, db: typeof IndexedDB }} context
* @param {{path: string, write: boolean}} target
* @param {NodeLocker} nodeLocker
*/
constructor(context, target) {
super(target);
this.context = context;
}
start() {
// Start transaction
return new Promise((resolve, reject) => {
this._tx = this.context.db.transaction(['nodes', 'content'], this.target.write ? 'readwrite' : 'readonly');
resolve();
})
}
commit() {
const tx = this._tx;
tx.commit && tx.commit();
}
rollback(err) {
const tx = this._tx;
tx.abort && tx.abort();
}
get(path) {
// Get metadata from "nodes" object store
const tx = this._tx;
const request = tx.objectStore('nodes').get(path);
return new Promise((resolve, reject) => {
request.onsuccess = event => {
/** @type {IIndexedDBNodeData} */
const data = request.result;
if (!data) {
return resolve(null);
}
/** @type {ICustomStorageNode} */
const node = data.metadata;
// Node exists, get content from "content" object store
const contentReq = tx.objectStore('content').get(path);
contentReq.onsuccess = e => {
node.value = contentReq.result;
resolve(node);
};
contentReq.onerror = e => reject(e);
}
request.onerror = e => {
console.error(`IndexedDB get error`, e);
reject(e);
}
});
}
set(path, node) {
/** @type {ICustomStorageNode} */
const copy = {};
const value = node.value;
Object.assign(copy, node);
delete copy.value;
/** @type {ICustomStorageNodeMetaData} */
const metadata = copy;
/** @type {IIndexedDBNodeData} */
const obj = {
path,
metadata
}
return new Promise((resolve, reject) => {
const tx = this._tx;
// Insert into "nodes" object store first
const request = tx.objectStore('nodes').put(obj);
request.onerror = e => reject(e);
request.onsuccess = e => {
// Now add to "content" object store
const contentReq = tx.objectStore('content').put(value, path);
contentReq.onsuccess = e => resolve();
contentReq.onerror = e => {
tx.abort(); // rollback transaction
reject(e);
}
};
});
}
remove(path) {
const tx = this._tx;
return new Promise((resolve, reject) => {
// First, remove from "content" object store
const r1 = tx.objectStore('content').delete(path);
r1.onerror = e => reject(e);
r1.onsuccess = e => {
// Now, remove from "nodes" data store
const r2 = tx.objectStore('nodes').delete(path);
r2.onerror = e => {
tx.abort(); // rollback transaction
reject(e);
};
r2.onsuccess = e => resolve();
}
});
}
childrenOf(path, include, checkCallback, addCallback) {
// Use cursor to loop from path on
const pathInfo = CustomStorageHelpers.PathInfo.get(path);
const tx = this._tx;
const store = tx.objectStore('nodes');
const query = IDBKeyRange.lowerBound(path, true);
return new Promise((resolve, reject) => {
/** @type {IDBRequest<IDBCursorWithValue>|IDBRequest<IDBCursor>} */
const cursor = include.metadata ? store.openCursor(query) : store.openKeyCursor(query);
cursor.onerror = e => reject(e);
cursor.onsuccess = async e => {
/** type {string} */
const otherPath = cursor.result ? cursor.result.key : null;
let keepGoing = true;
if (otherPath === null) {
// No more results
keepGoing = false;
}
else if (!pathInfo.isAncestorOf(otherPath)) {
// Paths are sorted, no more children to be expected!
keepGoing = false;
}
else if (pathInfo.isParentOf(otherPath) && checkCallback(otherPath)) {
/** @type {ICustomStorageNode|ICustomStorageNodeMetaData} */
let node;
if (include.metadata) {
/** @type {IDBRequest<IDBCursorWithValue>} */
const valueCursor = cursor;
/** @type {IIndexedDBNodeData} */
const data = valueCursor.result.value;
node = data.metadata;
if (include.value) {
// Load value!
const req = tx.objectStore('content').get(otherPath);
await new Promise((resolve, reject) => {
req.onerror = e => reject(e);
req.onsuccess = e => {
node.value = req.result;
resolve();
}
});
}
}
keepGoing = addCallback(otherPath, node);
}
if (keepGoing) {
try { cursor.result.continue(); }
catch(err) {
// We reached the end of the cursor?
keepGoing = false;
}
}
if (!keepGoing) {
resolve();
}
};
});
}
descendantsOf(path, include, checkCallback, addCallback) {
// Use cursor to loop from path on
// NOTE: Implementation is almost identical to childrenOf, consider merging them
const pathInfo = CustomStorageHelpers.PathInfo.get(path);
const tx = this._tx;
const store = tx.objectStore('nodes');
const query = IDBKeyRange.lowerBound(path, true);
return new Promise((resolve, reject) => {
/** @type {IDBRequest<IDBCursorWithValue>|IDBRequest<IDBCursor>} */
const cursor = include.metadata ? store.openCursor(query) : store.openKeyCursor(query);
cursor.onerror = e => reject(e);
cursor.onsuccess = async e => {
/** @type {string} */
const otherPath = cursor.result ? cursor.result.key : null;
let keepGoing = true;
if (otherPath === null) {
// No more results
keepGoing = false;
}
else if (!pathInfo.isAncestorOf(otherPath)) {
// Paths are sorted, no more ancestors to be expected!
keepGoing = false;
}
else if (checkCallback(otherPath)) {
/** @type {ICustomStorageNode|ICustomStorageNodeMetaData} */
let node;
if (include.metadata) {
/** @type {IDBRequest<IDBCursorWithValue>} */
const valueCursor = cursor;
/** @type {IIndexedDBNodeData} */
const data = valueCursor.result.value;
node = data.metadata;
if (include.value) {
// Load value!
const req = tx.objectStore('content').get(otherPath);
await new Promise((resolve, reject) => {
req.onerror = e => reject(e);
req.onsuccess = e => {
node.value = req.result;
resolve();
}
});
}
}
keepGoing = addCallback(otherPath, node);
}
if (keepGoing) {
try { cursor.result.continue(); }
catch(err) {
// We reached the end of the cursor?
keepGoing = false;
}
}
if (!keepGoing) {
resolve();
}
};
});
}
}
// Setup CustomStorageTransaction for browser's LocalStorage
class LocalStorageTransaction extends CustomStorageTransaction {
/**
* @param {{debug: boolean, dbname: string, localStorage: typeof window.localStorage}} context
* @param {{path: string, write: boolean}} target
*/
constructor(context, target) {
super(target);
this.context = context;
this._storageKeysPrefix = `${this.context.dbname}.acebase::`;
}
commit() {
// All changes have already been committed
return Promise.resolve();
}
rollback(err) {
// Not able to rollback changes, because we did not keep track
return Promise.resolve();
}
get(path) {
// Gets value from localStorage, wrapped in Promise
return new Promise(resolve => {
const json = this.context.localStorage.getItem(this.getStorageKeyForPath(path));
const val = JSON.parse(json);
resolve(val);
});
}
set(path, val) {
// Sets value in localStorage, wrapped in Promise
return new Promise(resolve => {
const json = JSON.stringify(val);
this.context.localStorage.setItem(this.getStorageKeyForPath(path), json);
resolve();
});
}
remove(path) {
// Removes a value from localStorage, wrapped in Promise
return new Promise(resolve => {
this.context.localStorage.removeItem(this.getStorageKeyForPath(path));
resolve();
});
}
childrenOf(path, include, checkCallback, addCallback) {
// Streams all child paths
// Cannot query localStorage, so loop through all stored keys to find children
return new Promise(resolve => {
const pathInfo = CustomStorageHelpers.PathInfo.get(path);
for (let i = 0; i < this.context.localStorage.length; i++) {
const key = this.context.localStorage.key(i);
if (!key.startsWith(this._storageKeysPrefix)) { continue; }
let otherPath = this.getPathFromStorageKey(key);
if (pathInfo.isParentOf(otherPath) && checkCallback(otherPath)) {
let node;
if (include.metadata || include.value) {
const json = this.context.localStorage.getItem(key);
node = JSON.parse(json);
}
const keepGoing = addCallback(otherPath, node);
if (!keepGoing) { break; }
}
}
resolve();
});
}
descendantsOf(path, include, checkCallback, addCallback) {
// Streams all descendant paths
// Cannot query localStorage, so loop through all stored keys to find descendants
return new Promise(resolve => {
const pathInfo = CustomStorageHelpers.PathInfo.get(path);
for (let i = 0; i < this.context.localStorage.length; i++) {
const key = this.context.localStorage.key(i);
if (!key.startsWith(this._storageKeysPrefix)) { continue; }
let otherPath = this.getPathFromStorageKey(key);
if (pathInfo.isAncestorOf(otherPath) && checkCallback(otherPath)) {
let node;
if (include.metadata || include.value) {
const json = this.context.localStorage.getItem(key);
node = JSON.parse(json);
}
const keepGoing = addCallback(otherPath, node);
if (!keepGoing) { break; }
}
}
resolve();
});
}
/**
* Helper function to get the path from a localStorage key
* @param {string} key
*/
getPathFromStorageKey(key) {
return key.slice(this._storageKeysPrefix.length);
}
/**
* Helper function to get the localStorage key for a path
* @param {string} path
*/
getStorageKeyForPath(path) {
return `${this._storageKeysPrefix}${path}`;
}
}
module.exports = { BrowserAceBase };

View file

@ -22,7 +22,7 @@ const { DataReference, DataSnapshot, EventSubscription, PathReference, TypeMappi
const { AceBaseLocalSettings } = require('./acebase-local');
const { BrowserAceBase } = require('./acebase-browser');
const { LocalStorageSettings } = require('./storage-localstorage');
const { CustomStorageSettings, CustomStorageHelpers } = require('./storage-custom');
const { CustomStorageSettings, CustomStorageTransaction, CustomStorageHelpers } = require('./storage-custom');
const acebase = {
AceBase: BrowserAceBase,
@ -35,6 +35,7 @@ const acebase = {
TypeMappingOptions,
LocalStorageSettings,
CustomStorageSettings,
CustomStorageTransaction,
CustomStorageHelpers
};

View file

@ -4,7 +4,7 @@ const { AceBaseStorageSettings } = require('./storage-acebase');
const { SQLiteStorageSettings } = require('./storage-sqlite');
const { MSSQLStorageSettings } = require('./storage-mssql');
const { LocalStorageSettings } = require('./storage-localstorage');
const { CustomStorageSettings, CustomStorageHelpers } = require('./storage-custom');
const { CustomStorageTransaction, CustomStorageSettings, CustomStorageHelpers } = require('./storage-custom');
module.exports = {
AceBase,
@ -19,6 +19,7 @@ module.exports = {
SQLiteStorageSettings,
MSSQLStorageSettings,
LocalStorageSettings,
CustomStorageTransaction,
CustomStorageSettings,
CustomStorageHelpers
};

File diff suppressed because it is too large Load diff

View file

@ -606,10 +606,12 @@ class Storage extends EventEmitter {
* @param {object} [options]
* @returns {Promise<void>}
*/
_writeNodeWithTracking(path, value, options = { merge: false, tid: undefined, _customWriteFunction: undefined, waitForIndexUpdates: true }) {
if (!options || !options.tid) { throw new Error(`_writeNodeWithTracking MUST be executed with a tid!`); }
_writeNodeWithTracking(path, value, options = { merge: false, transaction: undefined, tid: undefined, _customWriteFunction: undefined, waitForIndexUpdates: true }) {
options = options || {};
if (!options.tid && !options.transaction) { throw new Error(`_writeNodeWithTracking MUST be executed with a tid OR transaction!`); }
options.merge = options.merge === true;
const tid = options.tid;
const transaction = options.transaction;
// Is anyone interested in the values changing on this path?
let topEventData = null;
@ -708,13 +710,13 @@ class Storage extends EventEmitter {
}
}
return this.getNodeInfo(topEventPath, { tid })
return this.getNodeInfo(topEventPath, { transaction, tid })
.then(eventNodeInfo => {
if (!eventNodeInfo.exists) {
// Node doesn't exist
return null;
}
let valueOptions = { tid };
let valueOptions = { transaction, tid };
// if (!hasValueSubscribers && options.merge === true) {
// // Only load current value for properties being updated
// valueOptions.include = Object.keys(value);