Notes2/static/js/sync.mjs

231 lines
6.3 KiB
JavaScript
Raw Normal View History

2024-12-03 06:53:31 +01:00
import { API } from 'api'
2024-12-18 19:12:10 +01:00
import { Node } from 'node'
2025-01-21 18:20:50 +01:00
import { h, Component, createRef } from 'preact'
import htm from 'htm'
const html = htm.bind(h)
const SYNC_COUNT = 1
const SYNC_HANDLED = 2
const SYNC_DONE = 3
2024-12-03 06:53:31 +01:00
export class Sync {
2025-01-21 18:20:50 +01:00
constructor() {//{{{
this.listeners = []
this.messagesReceived = []
}//}}}
addListener(fn, runMessageQueue) {//{{{
// Some handlers won't be added until a time after sync messages have been added to the queue.
// This is an opportunity for the handler to receive the old messages in order.
if (runMessageQueue)
for (const msg of this.messagesReceived)
fn(msg)
this.listeners.push(fn)
}//}}}
pushMessage(msg) {//{{{
this.messagesReceived.push(msg)
for (const fn of this.listeners)
fn(msg)
}//}}}
async run() {//{{{
2024-12-03 13:56:38 +01:00
try {
2025-01-21 18:20:50 +01:00
let duration = 0 // in ms
2024-12-18 19:12:10 +01:00
// The latest sync node value is used to retrieve the changes
// from the backend.
const state = await nodeStore.getAppState('latest_sync_node')
2024-12-03 22:08:45 +01:00
const oldMax = (state?.value ? state.value : 0)
2024-12-03 13:56:38 +01:00
2025-01-21 18:20:50 +01:00
let nodeCount = await this.getNodeCount(oldMax)
nodeCount += await nodeStore.sendQueue.count()
const msg = { op: SYNC_COUNT, count: nodeCount }
this.pushMessage(msg)
await this.nodesFromServer(oldMax)
.then(durationNodes => {
duration = durationNodes // in ms
console.log(`Total time: ${Math.round(1000 * durationNodes) / 1000}s`)
})
await this.nodesToServer()
} finally {
2025-01-21 18:46:00 +01:00
this.pushMessage({ op: SYNC_DONE })
2025-01-21 18:20:50 +01:00
}
}//}}}
async getNodeCount(oldMax) {//{{{
// Retrieve the amount of values the server will send us.
const res = await API.query('POST', `/sync/from_server/count/${oldMax}`)
return res?.Count
}//}}}
async nodesFromServer(oldMax) {//{{{
const syncStart = Date.now()
let syncEnd
try {
let currMax = oldMax
2024-12-03 13:56:38 +01:00
let offset = 0
let res = { Continue: false }
let batch = 0
do {
batch++
2025-01-12 17:35:29 +01:00
res = await API.query('POST', `/sync/from_server/${oldMax}/${offset}`)
2024-12-18 19:12:10 +01:00
if (res.Nodes.length > 0)
console.log(`Node sync batch #${batch}`)
2024-12-03 13:56:38 +01:00
offset += res.Nodes.length
2024-12-18 19:12:10 +01:00
currMax = Math.max(currMax, res.MaxSeq)
/* Go through each node and determine if they are older than
* the node in IndexedDB. If they are, they are just history
* and can be ignored since history is currently not stored
* in the browser.
*
* If the backed node is newer, the local node is stored in
* a separate table in IndexedDB to at a later stage in the
* sync be preserved in the backend. */
let backendNode = null
for (const i in res.Nodes) {
backendNode = new Node(res.Nodes[i], -1)
2025-01-21 18:20:50 +01:00
await window._sync.handleNode(backendNode)
2024-12-18 19:12:10 +01:00
}
2024-12-03 13:56:38 +01:00
} while (res.Continue)
2024-12-18 19:12:10 +01:00
nodeStore.setAppState('latest_sync_node', currMax)
2024-12-03 13:56:38 +01:00
} catch (e) {
console.log('sync node tree', e)
2024-12-18 19:12:10 +01:00
} finally {
2025-01-21 18:20:50 +01:00
syncEnd = Date.now()
const duration = (syncEnd - syncStart) / 1000
2024-12-18 19:12:10 +01:00
const count = await nodeStore.nodeCount()
console.log(`Node sync took ${duration}s`, count)
}
2025-01-21 18:20:50 +01:00
return (syncEnd - syncStart)
2025-01-12 12:21:49 +01:00
}//}}}
2025-01-21 18:20:50 +01:00
async handleNode(backendNode) {//{{{
2024-12-18 19:12:10 +01:00
try {
/* Retrieving the local copy of this node from IndexedDB.
* The backend node can be discarded if it is older than
* the local copy since it is considered history preserved
* in the backend. */
return nodeStore.get(backendNode.UUID)
.then(async localNode => {
if (localNode.updated() >= backendNode.updated()) {
console.log(`History from backend: ${backendNode.UUID}`)
return
}
2025-01-12 12:21:49 +01:00
/* If the local node hasn't seen unsynchronized change,
* it can be replaced without anything else being done
* since it is already on the backend server.
*
* If the local node has seen change, the change is already
* placed into the send_queue anyway. */
return nodeStore.add([backendNode])
2024-12-18 19:12:10 +01:00
})
2025-01-12 12:21:49 +01:00
.catch(async () => {
2024-12-18 19:12:10 +01:00
// Not found in IndexedDB - OK to just insert since it only exists in backend.
return nodeStore.add([backendNode])
})
} catch (e) {
console.error(e)
2025-01-21 18:20:50 +01:00
} finally {
this.pushMessage({ op: SYNC_HANDLED, count: 1 })
2024-12-03 13:56:38 +01:00
}
2025-01-12 12:21:49 +01:00
}//}}}
2025-01-21 18:20:50 +01:00
async nodesToServer() {//{{{
const BATCH_SIZE = 32
while (true) {
2025-01-12 16:54:21 +01:00
try {
// Send nodes in batches until everything is sent, or an error has occured.
2025-01-21 18:20:50 +01:00
const nodesToSend = await nodeStore.sendQueue.retrieve(BATCH_SIZE)
2025-01-12 16:54:21 +01:00
if (nodesToSend.length === 0)
break
console.debug(`Sending ${nodesToSend.length} node(s) to server`)
2025-01-12 12:21:49 +01:00
2025-01-12 16:54:21 +01:00
const request = {
NodeData: JSON.stringify(nodesToSend),
}
const res = await API.query('POST', '/sync/to_server', request)
if (!res.OK) {
// TODO - implement better error management here.
console.log(res)
alert(res)
return
}
// Nodes are archived on server and can now be deleted from the send queue.
const keys = nodesToSend.map(node => node.ClientSequence)
2025-01-21 18:46:00 +01:00
await nodeStore.sendQueue.delete(keys)
2025-01-21 18:20:50 +01:00
this.pushMessage({ op: SYNC_HANDLED, count: nodesToSend.length })
2025-01-12 16:54:21 +01:00
} catch (e) {
console.trace(e)
alert(e)
return
}
2025-01-12 12:21:49 +01:00
}
}//}}}
2024-12-03 06:53:31 +01:00
}
2025-01-21 18:20:50 +01:00
export class SyncProgress extends Component {
constructor() {//{{{
super()
2025-01-21 18:46:00 +01:00
this.forceUpdateRequest = null
2025-01-21 18:20:50 +01:00
this.state = {
nodesToSync: 0,
nodesSynced: 0,
syncedDone: false,
}
}//}}}
componentDidMount() {//{{{
window._sync.addListener(msg => this.progressHandler(msg), true)
}//}}}
getSnapshotBeforeUpdate(_, prevState) {//{{{
if (!prevState.syncedDone && this.state.syncedDone)
setTimeout(() => document.getElementById('sync-progress')?.classList.add('hidden'), 750)
}//}}}
2025-01-21 18:46:00 +01:00
componentDidUpdate() {//{{{
if (!this.state.syncedDone) {
if (this.forceUpdateRequest !== null)
clearTimeout(this.forceUpdateRequest)
this.forceUpdateRequest = setTimeout(
() => {
this.forceUpdateRequest = null
this.forceUpdate()
},
50
)
}
}//}}}
2025-01-21 18:20:50 +01:00
progressHandler(msg) {//{{{
switch (msg.op) {
case SYNC_COUNT:
this.setState({ nodesToSync: msg.count })
break
case SYNC_HANDLED:
2025-01-21 18:46:00 +01:00
this.state.nodesSynced += msg.count
2025-01-21 18:20:50 +01:00
break
case SYNC_DONE:
this.setState({ syncedDone: true })
break
}
}//}}}
render(_, { nodesToSync, nodesSynced }) {//{{{
if (nodesToSync === 0)
return html`<div id="sync-progress"></div>`
return html`
<div id="sync-progress">
<progress min=0 max=${nodesToSync} value=${nodesSynced}></progress>
<div class="count">${nodesSynced} / ${nodesToSync}</div>
</div>
`
}//}}}
}