401 lines
11 KiB
JavaScript
401 lines
11 KiB
JavaScript
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
|