Cleanup of old code, moved Node class, many improvements on file handling
This commit is contained in:
parent
65f8cd14a7
commit
16992db6b1
19 changed files with 485 additions and 3281 deletions
|
|
@ -1,12 +1,218 @@
|
|||
import { Node } from 'node'
|
||||
|
||||
export const ROOT_NODE = '00000000-0000-0000-0000-000000000000'
|
||||
export const ORPHANED_NODE = '00000000-0000-0000-0000-000000000001'
|
||||
export const DELETED_NODE = '00000000-0000-0000-0000-000000000002'
|
||||
|
||||
export class Node {
|
||||
static sort(a, b) {//{{{
|
||||
// Nodes with children ("folders") are sorted first.
|
||||
if (a._has_children && !b._has_children) return -1
|
||||
if (!a._has_children && b._has_children) return 1
|
||||
|
||||
// Otherwise sort by lowercased name.
|
||||
const an = a.data.Name.toLowerCase()
|
||||
const bn = b.data.Name.toLowerCase()
|
||||
if (an < bn) return -1
|
||||
if (an > bn) return 1
|
||||
return 0
|
||||
}//}}}
|
||||
static create(name, parentUUID) {// {{{
|
||||
const node = new Node({
|
||||
UUID: uuidv7(),
|
||||
Created: (new Date()).toISOString(),
|
||||
Content: '',
|
||||
Name: name,
|
||||
ParentUUID: parentUUID,
|
||||
Markdown: false,
|
||||
})
|
||||
|
||||
// Newly created node (not constructed from existing data) is considered modified
|
||||
// since node.save returns early if it isn't modified.
|
||||
node._modified = true
|
||||
|
||||
return node
|
||||
}// }}}
|
||||
|
||||
constructor(nodeData, level) {//{{{
|
||||
this.Level = level
|
||||
this.data = nodeData
|
||||
this.UUID = nodeData.UUID
|
||||
|
||||
// Toplevel nodes are normalized to have the ROOT_NODE as parent.
|
||||
if (nodeData.UUID !== ROOT_NODE && nodeData.ParentUUID === '') {
|
||||
this.ParentUUID = ROOT_NODE
|
||||
this.data.ParentUUID = ROOT_NODE
|
||||
} else
|
||||
this.ParentUUID = nodeData.ParentUUID
|
||||
|
||||
this._children_fetched = false
|
||||
this._has_children = null // this will be set by nodeStore.getTreeNodes
|
||||
this.Children = []
|
||||
this.Ancestors = []
|
||||
|
||||
this._sibling_before = null
|
||||
this._sibling_after = null
|
||||
this._parent = null
|
||||
|
||||
this.reset()
|
||||
}//}}}
|
||||
|
||||
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
|
||||
}// }}}
|
||||
get(prop) {//{{{
|
||||
return this.data[prop]
|
||||
}//}}}
|
||||
updated() {//{{{
|
||||
// '2024-12-17T17:33:48.85939Z
|
||||
return new Date(Date.parse(this.data.Updated))
|
||||
}//}}}
|
||||
isModified() {// {{{
|
||||
return this._modified
|
||||
}// }}}
|
||||
hasFetchedChildren() {//{{{
|
||||
return this._children_fetched
|
||||
}//}}}
|
||||
async fetchChildren() {//{{{
|
||||
this.Children = await globalThis.nodeStore.getTreeNodes(this.UUID, this.Level + 1)
|
||||
this._children_fetched = true
|
||||
|
||||
// Children are sorted to allow for storing siblings befare and after.
|
||||
// These are used with keyboard navigation in the tree.
|
||||
this.Children.sort(Node.sort)
|
||||
|
||||
const numChildren = this.Children.length
|
||||
this.setHasChildren(numChildren > 0)
|
||||
for (let i = 0; i < numChildren; i++) {
|
||||
if (i > 0)
|
||||
this.Children[i]._sibling_before = this.Children[i - 1]
|
||||
if (i < numChildren - 1)
|
||||
this.Children[i]._sibling_after = this.Children[i + 1]
|
||||
this.Children[i]._parent = this
|
||||
}
|
||||
|
||||
return this.Children
|
||||
}//}}}
|
||||
setHasChildren(v) {// {{{
|
||||
this._has_children = v
|
||||
}// }}}
|
||||
hasChildren() {//{{{
|
||||
return this._has_children
|
||||
}//}}}
|
||||
getSiblingBefore() {// {{{
|
||||
return this._sibling_before
|
||||
}// }}}
|
||||
getSiblingAfter() {// {{{
|
||||
return this._sibling_after
|
||||
}// }}}
|
||||
getParent() {//{{{
|
||||
return this._parent
|
||||
}//}}}
|
||||
moveToParent(newParentUUID) {// {{{
|
||||
if (this.UUID === newParentUUID)
|
||||
throw new Error("New parent UUID is the same as node UUID. Can't be your own parent.")
|
||||
|
||||
this.ParentUUID = newParentUUID
|
||||
this.data.ParentUUID = newParentUUID
|
||||
this._modified = true
|
||||
}// }}}
|
||||
isLastSibling() {//{{{
|
||||
return this._sibling_after === null
|
||||
}//}}}
|
||||
isFirstSibling() {//{{{
|
||||
return this._sibling_before === null
|
||||
}//}}}
|
||||
isSpecial() {// {{{
|
||||
return this.data.Special
|
||||
}// }}}
|
||||
content() {//{{{
|
||||
// TODO - implement crypto
|
||||
return this._content
|
||||
}//}}}
|
||||
setContent(new_content) {//{{{
|
||||
this._content = new_content
|
||||
this._modified = true
|
||||
_mbus.dispatch('NODE_MODIFIED', { node: this })
|
||||
}//}}}
|
||||
setName(new_name) {// {{{
|
||||
if (new_name.trim() === '')
|
||||
throw new Error(`The name can't be empty`)
|
||||
|
||||
this.data.Name = new_name
|
||||
this._modified = true
|
||||
_mbus.dispatch('NODE_MODIFIED', { node: this })
|
||||
}// }}}
|
||||
async save() {//{{{
|
||||
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
|
||||
|
||||
// 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
|
||||
|
||||
// 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)
|
||||
|
||||
await globalThis.nodeStore.filesNodelink.removeFileUUIDs(this.UUID, removed)
|
||||
await globalThis.nodeStore.filesNodelink.addFileUUIDs(this.UUID, added)
|
||||
|
||||
// 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.
|
||||
|
||||
_mbus.dispatch('NODE_UNMODIFIED')
|
||||
this._modified = false
|
||||
|
||||
// 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. */
|
||||
|
||||
// Current node is added to history. It will be duplicated with the "nodes" store
|
||||
// for simplicity, to hopefully avoid bugs.
|
||||
const history = globalThis.nodeStore.nodesHistory.add(this)
|
||||
|
||||
// Updated node is added to the send queue to be stored on server.
|
||||
|
||||
const sendQueue = globalThis.nodeStore.sendQueue.add(this)
|
||||
|
||||
// Updated node is saved to the primary node store.
|
||||
const nodeStoreAdding = globalThis.nodeStore.add([this])
|
||||
|
||||
await Promise.all([history, sendQueue, nodeStoreAdding])
|
||||
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
alert(err.message)
|
||||
}
|
||||
|
||||
return
|
||||
}//}}}
|
||||
}
|
||||
|
||||
export class NodeStore {
|
||||
constructor() {//{{{
|
||||
if (!('indexedDB' in window)) {
|
||||
if (!('indexedDB' in self)) {
|
||||
throw 'Missing IndexedDB'
|
||||
}
|
||||
|
||||
|
|
@ -21,7 +227,7 @@ export class NodeStore {
|
|||
}//}}}
|
||||
initializeDB() {//{{{
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = indexedDB.open('notes', 12)
|
||||
const req = indexedDB.open('notes', 14)
|
||||
|
||||
// Schema upgrades for IndexedDB.
|
||||
// These can start from different points depending on updates to Notes2 since a device was online.
|
||||
|
|
@ -31,7 +237,6 @@ export class NodeStore {
|
|||
let sendQueue
|
||||
let nodesHistory
|
||||
let files
|
||||
let filesNodelink
|
||||
const db = event.target.result
|
||||
const trx = event.target.transaction
|
||||
|
||||
|
|
@ -89,6 +294,14 @@ export class NodeStore {
|
|||
case 12:
|
||||
trx.objectStore('files_nodelink').createIndex('byNodeCount', 'NodeCount', { unique: false })
|
||||
break
|
||||
|
||||
case 13:
|
||||
db.createObjectStore('file_segments', { keyPath: ['UUID', 'Sequence'] })
|
||||
break
|
||||
|
||||
case 14:
|
||||
db.deleteObjectStore('files')
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -98,6 +311,7 @@ export class NodeStore {
|
|||
this.sendQueue = new SimpleNodeStore(this.db, 'send_queue')
|
||||
this.nodesHistory = new NodeHistoryStore(this.db, 'nodes_history')
|
||||
this.files = new SimpleNodeStore(this.db, 'files')
|
||||
this.fileSegments = new FileSegmentsStore(this.db, 'file_segments')
|
||||
this.filesMetadata = new FilesMetadataStore(this.db, 'files_metadata')
|
||||
this.filesNodelink = new FileNodesLinkStore(this.db, 'files_nodelink')
|
||||
resolve()
|
||||
|
|
@ -353,9 +567,38 @@ export class NodeStore {
|
|||
}//}}}
|
||||
|
||||
async storeFile(file) {// {{{
|
||||
const metadata = new FileMetadata(file, {})
|
||||
const metadata = new FileMetadata(file)
|
||||
|
||||
try {
|
||||
// Do potentially larger blocks.
|
||||
const BLOCKSIZE = 1048576
|
||||
const stream = file.stream()
|
||||
const reader = stream.getReader({ mode: 'byob' })
|
||||
let buffer = new Uint8Array(BLOCKSIZE)
|
||||
|
||||
let seq = 0
|
||||
while (true) {
|
||||
const { done, value } = await reader.read(buffer)
|
||||
if (done) break
|
||||
|
||||
// Block is added as a file segment.
|
||||
await this.fileSegments.add({
|
||||
data: {
|
||||
UUID: metadata.UUID,
|
||||
Sequence: seq,
|
||||
Data: value,
|
||||
}
|
||||
})
|
||||
buffer = new Uint8Array(value.buffer)
|
||||
seq++
|
||||
}
|
||||
|
||||
reader.releaseLock()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
alert(e.message)
|
||||
}
|
||||
|
||||
await this.files.add({ data: { UUID: metadata.UUID, file } })
|
||||
await this.filesMetadata.add({ data: metadata })
|
||||
|
||||
return metadata
|
||||
|
|
@ -374,30 +617,11 @@ export class NodeStore {
|
|||
|
||||
// No more files to sync.
|
||||
if (data === undefined) {
|
||||
resolve({ file: null, metadata: null })
|
||||
resolve(null)
|
||||
return
|
||||
}
|
||||
|
||||
// Retrieve file as well.
|
||||
const filereq = this.db.
|
||||
transaction('files', 'readonly')
|
||||
.objectStore('files')
|
||||
.get(data.UUID)
|
||||
|
||||
filereq.onsuccess = (e) => {
|
||||
const f = e.target.result
|
||||
|
||||
// File should always be in IndexedDB when metadata is.
|
||||
if (f === undefined) {
|
||||
reject(new Error(`File with UUID '${data.UUID}' is missing.`))
|
||||
return
|
||||
}
|
||||
|
||||
resolve({
|
||||
file: f.file,
|
||||
metadata: new FileMetadata(f.file, data),
|
||||
})
|
||||
}
|
||||
resolve(new FileMetadata(data))
|
||||
}
|
||||
})
|
||||
}// }}}
|
||||
|
|
@ -528,7 +752,7 @@ class NodeHistoryStore extends SimpleNodeStore {
|
|||
}
|
||||
|
||||
req.onerror = (event) => {
|
||||
console.log(event.target.error)
|
||||
console.error(event.target.error)
|
||||
reject(event.target.error)
|
||||
}
|
||||
})
|
||||
|
|
@ -581,6 +805,37 @@ class NodeHistoryStore extends SimpleNodeStore {
|
|||
}// }}}
|
||||
}
|
||||
|
||||
class FileSegmentsStore extends SimpleNodeStore {
|
||||
constructor(db, storeName) {//{{{
|
||||
super(db, storeName)
|
||||
}//}}}
|
||||
async getStream(uuid) {// {{{
|
||||
let seq = 0
|
||||
const meself = this
|
||||
|
||||
return new ReadableStream({
|
||||
type: 'bytes',
|
||||
|
||||
async pull(controller) {
|
||||
try {
|
||||
while (true) {
|
||||
const chunk = await meself.get([uuid, seq])
|
||||
if (!chunk) {
|
||||
controller.close()
|
||||
return
|
||||
}
|
||||
|
||||
controller.enqueue(chunk.Data)
|
||||
seq++
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
})
|
||||
}// }}}
|
||||
}
|
||||
|
||||
class FilesMetadataStore extends SimpleNodeStore {
|
||||
countToUpload() {// {{{
|
||||
const store = this.db
|
||||
|
|
@ -638,6 +893,32 @@ class FileNodesLinkStore extends SimpleNodeStore {
|
|||
}// }}}
|
||||
}
|
||||
|
||||
class FileMetadata {
|
||||
constructor(data) {// {{{
|
||||
console.log('md', data)
|
||||
this.UUID = data.UUID || uuidv7()
|
||||
this.uploaded = data.uploaded || 'only_on_device'
|
||||
|
||||
this.name = data.name
|
||||
this.size = data.size
|
||||
this.type = data.type || 'application/octet-stream'
|
||||
this.modified = data.modified || data.lastModified || Date.now()
|
||||
}// }}}
|
||||
setUploaded() {// {{{
|
||||
this.uploaded = 'on_server'
|
||||
}// }}}
|
||||
get data() {// {{{
|
||||
return {
|
||||
UUID: this.UUID,
|
||||
uploaded: this.uploaded,
|
||||
name: this.name,
|
||||
size: this.size,
|
||||
type: this.type,
|
||||
modified: this.modified,
|
||||
}
|
||||
}// }}}
|
||||
}
|
||||
|
||||
export function uuidv7() {// {{{
|
||||
// random bytes
|
||||
const value = new Uint8Array(16)
|
||||
|
|
@ -664,29 +945,4 @@ export function uuidv7() {// {{{
|
|||
return `${str.slice(0, 8)}-${str.slice(8, 12)}-${str.slice(12, 16)}-${str.slice(16, 20)}-${str.slice(20)}`
|
||||
}// }}}
|
||||
|
||||
class FileMetadata {
|
||||
constructor(file, data) {// {{{
|
||||
this.UUID = data.UUID || uuidv7()
|
||||
this.uploaded = data.uploaded || 'only_on_device'
|
||||
|
||||
this.name = file.name
|
||||
this.size = file.size
|
||||
this.type = file.type || 'application/octet-stream'
|
||||
this.modified = file.lastModified
|
||||
}// }}}
|
||||
setUploaded() {// {{{
|
||||
this.uploaded = 'on_server'
|
||||
}// }}}
|
||||
get data() {// {{{
|
||||
return {
|
||||
UUID: this.UUID,
|
||||
uploaded: this.uploaded,
|
||||
name: this.name,
|
||||
size: this.size,
|
||||
type: this.type,
|
||||
modified: this.modified,
|
||||
}
|
||||
}// }}}
|
||||
}
|
||||
|
||||
// vim: foldmethod=marker
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue