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 = {} }//}}} 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: ['UUID', 'Updated'] }) 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.initializeRootNode() .then(() => this.initializeClientUUID()) .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) }) }//}}} async initializeClientUUID() {//{{{ let clientUUID = await this.getAppState('client_uuid') if (clientUUID !== null) return clientUUID = crypto.randomUUID() return this.setAppState('client_uuid', clientUUID) }//}}} 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 moveToSendQueue(nodeToMove, replaceWithNode) {//{{{ return new Promise((resolve, reject) => { const t = this.db.transaction(['nodes', 'send_queue'], 'readwrite') const nodeStore = t.objectStore('nodes') const sendQueue = t.objectStore('send_queue') t.onerror = (event) => { console.log('transaction error', event.target.error) reject(event.target.error) } t.oncomplete = () => { resolve() } // Node to be moved is first stored in the new queue. const queueReq = sendQueue.put(nodeToMove.data) queueReq.onsuccess = () => { // When added to the send queue, the node is either deleted // or replaced with a new node. console.debug(`Queueing ${nodeToMove.UUID} (${nodeToMove.get('Name')})`) let nodeReq if (replaceWithNode) nodeReq = nodeStore.put(replaceWithNode.data) else nodeReq = nodeStore.delete(nodeToMove.UUID) nodeReq.onsuccess = () => { resolve() } nodeReq.onerror = (event) => { console.log(`Error moving ${nodeToMove.UUID}`, event.target.error) reject(event.target.error) } } queueReq.onerror = (event) => { console.log(`Error queueing ${nodeToMove.UUID}`, event.target.error) reject(event.target.error) } }) }//}}} async copyToNodesHistory(nodeToCopy) {//{{{ return new Promise((resolve, reject) => { const t = this.db.transaction('nodes_history', 'readwrite') const nodesHistory = t.objectStore('nodes_history') t.oncomplete = () => { resolve() } t.onerror = (event) => { console.log('transaction error', event.target.error) reject(event.target.error) } const historyReq = nodesHistory.put(nodeToCopy.data) historyReq.onerror = (event) => { console.log(`Error copying ${nodeToCopy.UUID}`, event.target.error) reject(event.target.error) } }) }//}}} 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) } }) }//}}} } // vim: foldmethod=marker