import { Node } from 'node' export const ROOT_NODE = '00000000-0000-0000-0000-000000000000' export class NodeStore { constructor() {//{{{ if (!('indexedDB' in window)) { throw 'Missing IndexedDB' } this.db = null this.nodes = {} this.sendQueue = null this.nodesHistory = null }//}}} async initializeDB() {//{{{ return new Promise((resolve, reject) => { const req = indexedDB.open('notes', 7) // Schema upgrades for IndexedDB. // These can start from different points depending on updates to Notes2 since a device was online. req.onupgradeneeded = (event) => { let nodes let appState let sendQueue let nodesHistory const db = event.target.result const trx = event.target.transaction for (let i = event.oldVersion + 1; i <= event.newVersion; i++) { console.log(`Upgrade to schema ${i}`) // The schema transformations. switch (i) { case 1: nodes = db.createObjectStore('nodes', { keyPath: 'UUID' }) nodes.createIndex('byName', 'Name', { unique: false }) break case 2: trx.objectStore('nodes').createIndex('byParent', 'ParentUUID', { unique: false }) break case 3: appState = db.createObjectStore('app_state', { keyPath: 'key' }) break case 4: trx.objectStore('nodes').createIndex('byModified', 'modified', { unique: false }) break case 5: sendQueue = db.createObjectStore('send_queue', { keyPath: 'ClientSequence', autoIncrement: true }) sendQueue.createIndex('updated', 'Updated', { unique: false }) break case 6: nodesHistory = db.createObjectStore('nodes_history', { keyPath: ['UUID', 'Updated'] }) break case 7: trx.objectStore('nodes_history').createIndex('byUUID', 'UUID', { unique: false }) break } } } req.onsuccess = (event) => { this.db = event.target.result this.sendQueue = new SimpleNodeStore(this.db, 'send_queue') this.nodesHistory = new SimpleNodeStore(this.db, 'nodes_history') this.initializeRootNode() .then(() => resolve()) } req.onerror = (event) => { reject(event.target.error) } }) }//}}} async initializeRootNode() {//{{{ return new Promise((resolve, reject) => { // The root node is a magical node which displays as the first node if none is specified. // If not already existing, it will be created. const trx = this.db.transaction('nodes', 'readwrite') const nodes = trx.objectStore('nodes') const getRequest = nodes.get(ROOT_NODE) getRequest.onsuccess = (event) => { // Root node exists - nice! if (event.target.result !== undefined) { resolve(event.target.result) return } const putRequest = nodes.put({ UUID: ROOT_NODE, Name: 'Notes2', Content: 'Hello, World!', Updated: new Date().toISOString(), ParentUUID: '', }) putRequest.onsuccess = (event) => { resolve(event.target.result) } putRequest.onerror = (event) => { reject(event.target.error) } } getRequest.onerror = (event) => reject(event.target.error) }) }//}}} node(uuid, dataIfUndefined, newLevel) {//{{{ let n = this.nodes[uuid] if (n === undefined && dataIfUndefined !== undefined) n = this.nodes[uuid] = new Node(dataIfUndefined, newLevel) return n }//}}} async getAppState(key) {//{{{ return new Promise((resolve, reject) => { const trx = this.db.transaction('app_state', 'readonly') const appState = trx.objectStore('app_state') const getRequest = appState.get(key) getRequest.onsuccess = (event) => { if (event.target.result !== undefined) { resolve(event.target.result) } else { resolve(null) } } getRequest.onerror = (event) => reject(event.target.error) }) }//}}} async setAppState(key, value) {//{{{ return new Promise((resolve, reject) => { try { const t = this.db.transaction('app_state', 'readwrite') const appState = t.objectStore('app_state') t.onerror = (event) => { console.log('transaction error', event.target.error) reject(event.target.error) } t.oncomplete = () => { resolve() } const record = { key, value } const addReq = appState.put(record) addReq.onerror = (event) => { console.log('Error!', event.target.error, key, value) } } catch (e) { reject(e) } }) }//}}} async storeNode(node) {//{{{ return new Promise((resolve, reject) => { const t = this.db.transaction('nodes', 'readwrite') const nodeStore = t.objectStore('nodes') t.onerror = (event) => { console.log('transaction error', event.target.error) reject(event.target.error) } t.oncomplete = () => { resolve() } const nodeReq = nodeStore.put(node.data) nodeReq.onsuccess = () => { console.debug(`Storing ${node.UUID} (${node.get('Name')})`) } queueReq.onerror = (event) => { console.log(`Error storing ${node.UUID}`, event.target.error) reject(event.target.error) } }) }//}}} async upsertNodeRecords(records) {//{{{ return new Promise((resolve, reject) => { const t = this.db.transaction('nodes', 'readwrite') const nodeStore = t.objectStore('nodes') t.onerror = (event) => { console.log('transaction error', event.target.error) reject(event.target.error) } t.oncomplete = () => { resolve() } // records is an object, not an array. for (const i in records) { const record = records[i] let addReq let op if (record.Deleted) { op = 'deleting' addReq = nodeStore.delete(record.UUID) } else { op = 'upserting' // 'modified' is a local property for tracking // nodes needing to be synced to backend. record.modified = 0 addReq = nodeStore.put(record) } addReq.onsuccess = () => { console.debug(`${op} ${record.UUID} (${record.Name})`) } addReq.onerror = (event) => { console.log(`error ${op} ${record.UUID}`, event.target.error) reject(event.target.error) } } }) }//}}} async getTreeNodes(parent, newLevel) {//{{{ return new Promise((resolve, reject) => { const trx = this.db.transaction('nodes', 'readonly') const nodeStore = trx.objectStore('nodes') const index = nodeStore.index('byParent') const req = index.getAll(parent) req.onsuccess = (event) => { const nodes = [] for (const i in event.target.result) { const nodeData = event.target.result[i] const node = this.node(nodeData.UUID, nodeData, newLevel) nodes.push(node) } resolve(nodes) } req.onerror = (event) => reject(event.target.error) }) }//}}} async add(records) {//{{{ return new Promise((resolve, reject) => { try { const t = this.db.transaction('nodes', 'readwrite') const nodeStore = t.objectStore('nodes') t.onerror = (event) => { console.log('transaction error', event.target.error) reject(event.target.error) } t.oncomplete = () => { console.log('OK') } // records is an object, not an array. const promises = [] for (const recordIdx in records) { const record = records[recordIdx] const addReq = nodeStore.put(record.data) const promise = new Promise((resolve, reject) => { addReq.onsuccess = () => { console.debug('OK!', record.ID, record.Name) resolve() } addReq.onerror = (event) => { console.log('Error!', event.target.error, record.ID) reject(event.target.error) } }) promises.push(promise) } Promise.all(promises).then(() => resolve()) } catch (e) { console.log(e) } }) }//}}} async get(uuid) {//{{{ return new Promise((resolve, reject) => { const trx = this.db.transaction('nodes', 'readonly') const nodeStore = trx.objectStore('nodes') const getRequest = nodeStore.get(uuid) getRequest.onsuccess = (event) => { // Node not found in IndexedDB. if (event.target.result === undefined) { reject("No such node") return } const node = this.node(uuid, event.target.result, -1) resolve(node) } }) }//}}} async getNodeAncestry(node, accumulated) {//{{{ return new Promise((resolve, reject) => { if (accumulated === undefined) accumulated = [] const nodeParentIndex = this.db .transaction('nodes', 'readonly') .objectStore('nodes') if (node.ParentUUID === '') { resolve(accumulated) return } const getRequest = nodeParentIndex.get(node.ParentUUID) getRequest.onsuccess = (event) => { // Node not found in IndexedDB. // Not expected to happen. const parentNodeData = event.target.result if (parentNodeData === undefined) { reject("No such node") return } const parentNode = this.node(parentNodeData.UUID, parentNodeData, -1) this.getNodeAncestry(parentNode, accumulated.concat(parentNode)) .then(accumulated => resolve(accumulated)) } }) }//}}} async nodeCount() {//{{{ return new Promise((resolve, reject) => { const t = this.db.transaction('nodes', 'readwrite') const nodeStore = t.objectStore('nodes') const countReq = nodeStore.count() countReq.onsuccess = event => { resolve(event.target.result) } }) }//}}} } class SimpleNodeStore { constructor(db, storeName) {//{{{ this.db = db this.storeName = storeName }//}}} async add(node) {//{{{ return new Promise((resolve, reject) => { const t = this.db.transaction(['nodes', this.storeName], 'readwrite') const store = t.objectStore(this.storeName) t.onerror = (event) => { console.log('transaction error', event.target.error) reject(event.target.error) } // Node to be moved is first stored in the new queue. const req = store.put(node.data) req.onsuccess = () => { resolve() } req.onerror = (event) => { console.log(`Error adding ${node.UUID}`, event.target.error) reject(event.target.error) } }) }//}}} async retrieve(limit) {//{{{ return new Promise((resolve, reject) => { const cursorReq = this.db .transaction(['nodes', this.storeName], 'readonly') .objectStore(this.storeName) .index('updated') .openCursor() let retrieved = 0 const nodes = [] cursorReq.onsuccess = (event) => { const cursor = event.target.result if (!cursor) { resolve(nodes) return } retrieved++ nodes.push(cursor.value) if (retrieved === limit) { resolve(nodes) return } cursor.continue() } }) }//}}} async delete(keys) {//{{{ const store = this.db .transaction(['nodes', this.storeName], 'readwrite') .objectStore(this.storeName) const promises = [] for (const key of keys) { const p = new Promise((resolve, reject)=>{ // TODO - implement a way to add an error to a page-global error log. const request = store.delete(key) request.onsuccess = (event)=>resolve(event) request.onerror = (event)=>reject(event) }) promises.push(p) } return Promise.all(promises) }//}}} } // vim: foldmethod=marker