diff --git a/static/js/node_store.mjs b/static/js/node_store.mjs index 96b71ca..fa65a27 100644 --- a/static/js/node_store.mjs +++ b/static/js/node_store.mjs @@ -21,7 +21,7 @@ export class NodeStore { }//}}} initializeDB() {//{{{ return new Promise((resolve, reject) => { - const req = indexedDB.open('notes', 10) + const req = indexedDB.open('notes', 12) // Schema upgrades for IndexedDB. // These can start from different points depending on updates to Notes2 since a device was online. @@ -31,6 +31,7 @@ export class NodeStore { let sendQueue let nodesHistory let files + let filesNodelink const db = event.target.result const trx = event.target.transaction @@ -80,6 +81,14 @@ export class NodeStore { case 10: trx.objectStore('files_metadata').createIndex('byUploaded', 'uploaded', { unique: false }) break + + case 11: + db.createObjectStore('files_nodelink', { keyPath: 'UUID' }) + break + + case 12: + trx.objectStore('files_nodelink').createIndex('byNodeCount', 'NodeCount', { unique: false }) + break } } } @@ -90,6 +99,7 @@ export class NodeStore { this.nodesHistory = new NodeHistoryStore(this.db, 'nodes_history') this.files = new SimpleNodeStore(this.db, 'files') this.filesMetadata = new SimpleNodeStore(this.db, 'files_metadata') + this.filesNodelink = new FileNodesLinkStore(this.db, 'files_nodelink') resolve() } @@ -231,7 +241,6 @@ export class NodeStore { nodeStore = t.objectStore('nodes') t.oncomplete = (_event) => { - console.log('complete') resolve() } @@ -572,6 +581,51 @@ class NodeHistoryStore extends SimpleNodeStore { }// }}} } +class FileNodesLinkStore extends SimpleNodeStore { + async removeFileUUIDs(nodeUUID, fileUUIDs) {// {{{ + for (const fileUUID of fileUUIDs) { + let nodelink = await this.get(fileUUID) + + // Nothing to remove from. + if (nodelink === undefined) + continue + + const idx = nodelink.Nodes.indexOf(nodeUUID) + if (idx > -1) + nodelink.Nodes.splice(idx, 1) + + // Maintains an index to quickly find orphaned files. + nodelink.NodeCount = nodelink.Nodes.length + + console.log('removing', nodelink) + await this.add({data: nodelink}) + } + }// }}} + async addFileUUIDs(nodeUUID, fileUUIDs) {// {{{ + for (const fileUUID of fileUUIDs) { + let nodelink = await this.get(fileUUID) + + // Needs to be initialized if not existing already. + if (nodelink === undefined) + nodelink = { + UUID: fileUUID, + Nodes: [nodeUUID], + } + + // Can get called with the same UUID as already there. + // No duplicates, only append if missing. + if (!nodelink.Nodes.includes(nodeUUID)) + nodelink.Nodes.push(nodeUUID) + + // Maintains an index to quickly find orphaned files. + nodelink.NodeCount = nodelink.Nodes.length + + console.log('pushing', nodelink) + await this.add({data: nodelink}) + } + }// }}} +} + export function uuidv7() {// {{{ // random bytes const value = new Uint8Array(16) diff --git a/static/js/page_node.mjs b/static/js/page_node.mjs index b0d0a00..eb717fd 100644 --- a/static/js/page_node.mjs +++ b/static/js/page_node.mjs @@ -453,6 +453,8 @@ export class Node { }//}}} reset() {// {{{ + // this._content is the runtime content in the editor. + // this.data.Content is the original data. this._content = this.data.Content this._modified = false }// }}} @@ -522,10 +524,7 @@ export class Node { return this.data.Special }// }}} content() {//{{{ - /* TODO - implement crypto - if (this.CryptoKeyID != 0 && !this._decrypted) - this.#decrypt() - */ + // TODO - implement crypto return this._content }//}}} setContent(new_content) {//{{{ @@ -542,48 +541,67 @@ export class Node { _mbus.dispatch('NODE_MODIFIED', { node: this }) }// }}} async save() {//{{{ - // Just safeguarding not using the root node, - // which sort of exist but isn't supposed to communicate to server. - if (this.UUID == ROOT_NODE) - return + try { + const dblink = /db:\/\/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/gi - this.data.Content = this._content - this.data.Updated = new Date().toISOString() - this.data.HistoryUUID = uuidv7() // every time the node is saved a new history UUID identifies the changed node. - this._modified = false + // Just safeguarding not using the root node, + // which sort of exist but isn't supposed to communicate to server. + if (this.UUID == ROOT_NODE) + return - _mbus.dispatch('NODE_UNMODIFIED') - // When stored into database and ancestry was changed, - // the ancestry path could be interesting. - /* - const ancestors = await nodeStore.getNodeAncestry(this) - this.data.Ancestors = ancestors.map(a => a.get('Name')).reverse() - */ - /* The node history is a local store for node history. - * This could be provisioned from the server or cleared if - * deemed unnecessary. - * - * The send queue is what will be sent back to the server - * to have a recorded history of the notes. - * - * A setting to be implemented in the future could be to - * not save the history locally at all. */ + // File links from the original, unmodified content are found to compare to links from the new content. + // All files (images) needs to be indexed to nodes in order to track orphaned files. + const before = new Set([...this.data.Content.matchAll(dblink)].map(m => m[1])) + const after = new Set([...this._content.matchAll(dblink)].map(m => m[1])) + const removed = before.difference(after) + const added = after.difference(before) - // Current node is added to history. It will be duplicated with the "nodes" store - // for simplicity, to hopefully avoid bugs. - const history = nodeStore.nodesHistory.add(this) + await nodeStore.filesNodelink.removeFileUUIDs(this.UUID, removed) + await nodeStore.filesNodelink.addFileUUIDs(this.UUID, added) - // Updated node is added to the send queue to be stored on server. + // Authoritative content and metadata is set to prepare it for IndexedDB update. + this.data.Content = this._content + this.data.Updated = new Date().toISOString() + this.data.HistoryUUID = uuidv7() // every time the node is saved a new history UUID identifies the changed node. - const sendQueue = nodeStore.sendQueue.add(this) + _mbus.dispatch('NODE_UNMODIFIED') + this._modified = false - // Updated node is saved to the primary node store. - const nodeStoreAdding = nodeStore.add([this]) + // When stored into database and ancestry was changed, + // the ancestry path could be interesting. + /* + const ancestors = await nodeStore.getNodeAncestry(this) + this.data.Ancestors = ancestors.map(a => a.get('Name')).reverse() + */ - console.log('waiting') - await Promise.all([history, sendQueue, nodeStoreAdding]) - console.log('waiting done') + /* The node history is a local store for node history. + * This could be provisioned from the server or cleared if + * deemed unnecessary. + * + * The send queue is what will be sent back to the server + * to have a recorded history of the notes. + * + * A setting to be implemented in the future could be to + * not save the history locally at all. */ + + // Current node is added to history. It will be duplicated with the "nodes" store + // for simplicity, to hopefully avoid bugs. + const history = nodeStore.nodesHistory.add(this) + + // Updated node is added to the send queue to be stored on server. + + const sendQueue = nodeStore.sendQueue.add(this) + + // Updated node is saved to the primary node store. + const nodeStoreAdding = nodeStore.add([this]) + + await Promise.all([history, sendQueue, nodeStoreAdding]) + + } catch (err) { + console.error(err) + alert(err.message) + } return }//}}} diff --git a/static/js/sync.mjs b/static/js/sync.mjs index 9c64caf..7991644 100644 --- a/static/js/sync.mjs +++ b/static/js/sync.mjs @@ -164,9 +164,9 @@ export class Sync { } } }//}}} - async filesToServer() { + async filesToServer() {// {{{ try { - const BLOCKSIZE = 128*1024 + const BLOCKSIZE = 128 * 1024 let sentBytes = 0 const { file, metadata } = await nodeStore.nextFileToSync() @@ -193,7 +193,7 @@ export class Sync { console.error(e) alert(e.message) } - } + }// }}} } export class N2SyncProgress extends CustomHTMLElement {