Fixed tree reset after sync, optimized sync with IndexedDB

This commit is contained in:
Magnus Åhall 2026-05-03 19:45:39 +02:00
parent 454d065baa
commit 26ca510785
5 changed files with 72 additions and 52 deletions

View file

@ -6,13 +6,13 @@ import { Node } from 'node'
export class App { export class App {
constructor() {// {{{ constructor() {// {{{
this.currentNode = null this.currentNode = null
this.treeNative = new N2Tree() this.tree = new N2Tree()
this.crumbs = new N2Crumbs() this.crumbs = new N2Crumbs()
this.crumbsElement = document.getElementById('crumbs') this.crumbsElement = document.getElementById('crumbs')
this.nodeUI = document.getElementById('note') this.nodeUI = document.getElementById('note')
_mbus.subscribe('TREE_TRUNK_FETCHED', async () => { _mbus.subscribe('TREE_TRUNK_FETCHED', async () => {
document.getElementById('tree').append(this.treeNative.render()) document.getElementById('tree').append(this.tree.render())
document.getElementById('tree-nodes')?.focus() document.getElementById('tree-nodes')?.focus()
const startNode = await this.getStartNode() const startNode = await this.getStartNode()
@ -188,7 +188,7 @@ export class App {
node.reset() // any modifications are discarded. node.reset() // any modifications are discarded.
this.currentNode = node this.currentNode = node
this.treeNative.setSelected(node, dontExpand) this.tree.setSelected(node, dontExpand)
const ancestors = await nodeStore.getNodeAncestry(node) const ancestors = await nodeStore.getNodeAncestry(node)
_mbus.dispatch('CRUMBS_SET', ancestors, () => this.crumbsElement.replaceChildren(this.crumbs.render())) _mbus.dispatch('CRUMBS_SET', ancestors, () => this.crumbsElement.replaceChildren(this.crumbs.render()))
@ -196,7 +196,7 @@ export class App {
_mbus.dispatch('NODE_UNMODIFIED') _mbus.dispatch('NODE_UNMODIFIED')
// Scrolls node into view. // Scrolls node into view.
this.treeNative.makeVisible(node) this.tree.makeVisible(node)
}//}}} }//}}}
} }

View file

@ -54,7 +54,7 @@ export class Node {
if (a.data.Name > b.data.Name) return 0 if (a.data.Name > b.data.Name) return 0
return 0 return 0
}//}}} }//}}}
static create(name, parentUUID) { static create(name, parentUUID) {// {{{
return new Node({ return new Node({
UUID: uuidv7(), UUID: uuidv7(),
Created: (new Date()).toISOString(), Created: (new Date()).toISOString(),
@ -64,7 +64,7 @@ export class Node {
Markdown: false, Markdown: false,
History: false, History: false,
}) })
} }// }}}
constructor(nodeData, level) {//{{{ constructor(nodeData, level) {//{{{
@ -123,9 +123,6 @@ export class Node {
return this._children_fetched return this._children_fetched
}//}}} }//}}}
async fetchChildren() {//{{{ async fetchChildren() {//{{{
if (this._children_fetched)
return this.Children
this.Children = await nodeStore.getTreeNodes(this.UUID, this.Level + 1) this.Children = await nodeStore.getTreeNodes(this.UUID, this.Level + 1)
this._children_fetched = true this._children_fetched = true

View file

@ -159,6 +159,7 @@ export class NodeStore {
}) })
}//}}} }//}}}
/*
upsertNodeRecords(records) {//{{{ upsertNodeRecords(records) {//{{{
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const t = this.db.transaction('nodes', 'readwrite') const t = this.db.transaction('nodes', 'readwrite')
@ -187,16 +188,10 @@ export class NodeStore {
record.modified = 0 record.modified = 0
addReq = nodeStore.put(record) 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)
}
} }
}) })
}//}}} }//}}}
*/
getTreeNodes(parent, newLevel) {//{{{ getTreeNodes(parent, newLevel) {//{{{
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// Parent of toplevel nodes is ROOT_NODE in indexedDB. // Parent of toplevel nodes is ROOT_NODE in indexedDB.
@ -219,7 +214,7 @@ export class NodeStore {
req.onerror = (event) => reject(event.target.error) req.onerror = (event) => reject(event.target.error)
}) })
}//}}} }//}}}
async search(searchfor, parent) {//{{{ search(searchfor, parent) {//{{{
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const trx = this.db.transaction('nodes', 'readonly') const trx = this.db.transaction('nodes', 'readonly')
const nodeStore = trx.objectStore('nodes') const nodeStore = trx.objectStore('nodes')
@ -249,43 +244,55 @@ export class NodeStore {
}) })
}//}}} }//}}}
add(records) {//{{{ add(records, objstore) {//{{{
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
try { try {
const t = this.db.transaction('nodes', 'readwrite') // A nodestore can be provided in order to
const nodeStore = t.objectStore('nodes') // avoid creating new transactions.
let nodeStore = objstore
let t
if (nodeStore === undefined) {
t = this.db.transaction('nodes', 'readwrite')
nodeStore = t.objectStore('nodes')
t.oncomplete = (_event) => {
resolve()
}
t.onerror = (event) => { t.onerror = (event) => {
console.error('transaction error', event.target.error) console.error('transaction error', event.target.error)
reject(event.target.error) reject(event.target.error)
} }
}
// records is an object, not an array. // records is an object, not an array.
const promises = []
for (const recordIdx in records) { for (const recordIdx in records) {
const record = records[recordIdx] const record = records[recordIdx]
const addReq = nodeStore.put(record.data) nodeStore.put(record.data)
const promise = new Promise((resolve, reject) => {
addReq.onsuccess = () => resolve()
addReq.onerror = (event) => {
console.error('Error!', event.target.error, record.ID)
reject(event.target.error)
}
})
promises.push(promise)
} }
Promise.all(promises).then(() => resolve()) resolve()
} catch (e) { } catch (e) {
console.log(e) console.error(e)
reject(e)
} }
}) })
}//}}} }//}}}
get(uuid) {//{{{ get(uuid, suppliedNodestore) {//{{{
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const trx = this.db.transaction('nodes', 'readonly') // A nodestore can be provided in order to
const nodeStore = trx.objectStore('nodes') // avoid creating new transactions.
let trx
let nodeStore = suppliedNodestore
if (nodeStore === undefined) {
trx = this.db.transaction('nodes', 'readonly')
nodeStore = trx.objectStore('nodes')
}
const getRequest = nodeStore.get(uuid) const getRequest = nodeStore.get(uuid)
getRequest.onsuccess = (event) => { getRequest.onsuccess = (event) => {
// Node not found in IndexedDB. // Node not found in IndexedDB.
if (event.target.result === undefined) { if (event.target.result === undefined) {
@ -328,6 +335,9 @@ export class NodeStore {
}) })
}//}}} }//}}}
newTransaction(objectStore, mode) {// {{{
return this.db.transaction(objectStore, mode)
}// }}}
nodeCount() {//{{{ nodeCount() {//{{{
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {

View file

@ -65,9 +65,16 @@ export class Sync {
* sync be preserved in the backend. */ * sync be preserved in the backend. */
let backendNode = null let backendNode = null
// Create a single transaction to be used in the chain of
// this sync. Otherwise it would take more time to create
// transactions for each node.
const trx = nodeStore.newTransaction('nodes', 'readwrite')
const objstore = trx.objectStore('nodes')
for (const i in res.Nodes) { for (const i in res.Nodes) {
backendNode = new Node(res.Nodes[i], -1) backendNode = new Node(res.Nodes[i], -1)
await window._sync.handleNode(backendNode) await this.handleNode(backendNode, objstore)
handled++ handled++
if (handled % 100 === 0) if (handled % 100 === 0)
@ -88,16 +95,16 @@ export class Sync {
} }
return (syncEnd - syncStart) return (syncEnd - syncStart)
}//}}} }//}}}
async handleNode(backendNode) {//{{{ async handleNode(backendNode, objstore) {//{{{
try { try {
/* Retrieving the local copy of this node from IndexedDB. /* Retrieving the local copy of this node from IndexedDB.
* The backend node can be discarded if it is older than * The backend node can be discarded if it is older than
* the local copy since it is considered history preserved * the local copy since it is considered history preserved
* in the backend. */ * in the backend. */
return nodeStore.get(backendNode.UUID) return nodeStore.get(backendNode.UUID, objstore)
.then(async localNode => { .then(localNode => {
if (localNode.updated() >= backendNode.updated()) { if (localNode.updated() >= backendNode.updated()) {
console.log(`History from backend: ${backendNode.UUID}`) console.debug(`History from backend: ${backendNode.UUID}`)
return return
} }
@ -107,12 +114,12 @@ export class Sync {
* *
* If the local node has seen change, the change is already * If the local node has seen change, the change is already
* placed into the send_queue anyway. */ * placed into the send_queue anyway. */
return nodeStore.add([backendNode]) return nodeStore.add([backendNode], objstore)
}) })
.catch(async () => { .catch(() => {
// Not found in IndexedDB - OK to just insert since it only exists in backend. // Not found in IndexedDB - OK to just insert since it only exists in backend.
return nodeStore.add([backendNode]) return nodeStore.add([backendNode], objstore)
}) })
} catch (e) { } catch (e) {
console.error(e) console.error(e)
@ -198,10 +205,7 @@ export class N2SyncProgress extends CustomHTMLElement {
break break
// Reload the tree nodes to reflect the new/updated nodes. // Reload the tree nodes to reflect the new/updated nodes.
if (window._notes2?.current?.reloadTree.value !== null) { window._app.tree.reset()
nodeStore.purgeCache()
window._notes2.current.reloadTree.value = window._notes2.current.reloadTree.value + 1
}
break break
} }
this.render() this.render()

View file

@ -10,6 +10,7 @@ export class N2Tree extends CustomHTMLElement {
<img data-el="search" class='search' src="/images/${_VERSION}/icon_search.svg" style="height: 22px" /> <img data-el="search" class='search' src="/images/${_VERSION}/icon_search.svg" style="height: 22px" />
<img data-el="sync" class='sync' src="/images/${_VERSION}/icon_refresh.svg" /> <img data-el="sync" class='sync' src="/images/${_VERSION}/icon_refresh.svg" />
</div> </div>
<div data-el="treenodes"></div>
` `
}// }}} }// }}}
@ -39,12 +40,20 @@ export class N2Tree extends CustomHTMLElement {
for (const node of this.treeTrunk) { for (const node of this.treeTrunk) {
const treenode = new N2TreeNode(this, node) const treenode = new N2TreeNode(this, node)
this.treeNodeComponents[node.UUID] = treenode this.treeNodeComponents[node.UUID] = treenode
this.appendChild(treenode.render()) this.elTreenodes.appendChild(treenode.render())
} }
this.rendered = true this.rendered = true
return this return this
}// }}} }// }}}
reset() {
console.log('tree reset')
this.treeNodeComponents = {}
this.treeTrunk = []
this.rendered = false
this.elTreenodes.replaceChildren()
this.populateFirstLevel()
}
populateFirstLevel() {//{{{ populateFirstLevel() {//{{{
nodeStore.get(ROOT_NODE) nodeStore.get(ROOT_NODE)
.then(node => node.fetchChildren()) .then(node => node.fetchChildren())