Sync progress bar somewhat working
This commit is contained in:
parent
f33e5d54af
commit
3453dffb53
8 changed files with 250 additions and 42 deletions
3
main.go
3
main.go
|
@ -124,7 +124,7 @@ func main() { // {{{
|
|||
|
||||
http.HandleFunc("/user/authenticate", AuthManager.AuthenticationHandler)
|
||||
|
||||
http.HandleFunc("/sync/from_server/count", authenticated(actionSyncFromServerCount))
|
||||
http.HandleFunc("/sync/from_server/count/{sequence}", authenticated(actionSyncFromServerCount))
|
||||
http.HandleFunc("/sync/from_server/{sequence}/{offset}", authenticated(actionSyncFromServer))
|
||||
http.HandleFunc("/sync/to_server", authenticated(actionSyncToServer))
|
||||
|
||||
|
@ -277,6 +277,7 @@ func actionSyncFromServerCount(w http.ResponseWriter, r *http.Request) { // {{{
|
|||
user := getUser(r)
|
||||
changedFrom, _ := strconv.Atoi(r.PathValue("sequence"))
|
||||
|
||||
Log.Debug("FOO", "UUID", user.ClientUUID, "changedFrom", changedFrom)
|
||||
count, err := NodesCount(user.UserID, uint64(changedFrom), user.ClientUUID)
|
||||
if err != nil {
|
||||
Log.Error("/sync/from_server/count", "error", err)
|
||||
|
|
20
node.go
20
node.go
|
@ -192,22 +192,22 @@ func NodesCount(userID int, synced uint64, clientUUID string) (count int, err er
|
|||
public.node
|
||||
WHERE
|
||||
user_id = $1 AND
|
||||
client != $5 AND
|
||||
client != $3 AND
|
||||
NOT history AND (
|
||||
created_seq > $4 OR
|
||||
updated_seq > $4 OR
|
||||
deleted_seq > $4
|
||||
created_seq > $2 OR
|
||||
updated_seq > $2 OR
|
||||
deleted_seq > $2
|
||||
)
|
||||
`,
|
||||
userID,
|
||||
synced,
|
||||
clientUUID,
|
||||
)
|
||||
row.Scan(&row)
|
||||
err = row.Scan(&count)
|
||||
if err != nil {
|
||||
err = werr.Wrap(err).WithData(
|
||||
struct {
|
||||
UserID uint64
|
||||
UserID int
|
||||
Synced uint64
|
||||
ClientUUID string
|
||||
}{
|
||||
|
@ -286,13 +286,13 @@ func NodeCrumbs(nodeUUID string) (nodes []Node, err error) { // {{{
|
|||
} // }}}
|
||||
|
||||
func TestData() (err error) {
|
||||
for range 10 {
|
||||
for range 8 {
|
||||
hash1, name1, _ := generateOneTestNode("", "G")
|
||||
for range 10 {
|
||||
for range 8 {
|
||||
hash2, name2, _ := generateOneTestNode(hash1, name1)
|
||||
for range 10 {
|
||||
for range 8 {
|
||||
hash3, name3, _ := generateOneTestNode(hash2, name2)
|
||||
for range 10 {
|
||||
for range 8 {
|
||||
generateOneTestNode(hash3, name3)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,12 +4,12 @@ html {
|
|||
#notes2 {
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
grid-template-areas: "tree crumbs" "tree name" "tree content" "tree blank";
|
||||
grid-template-areas: "tree crumbs" "tree sync" "tree name" "tree content" "tree blank";
|
||||
grid-template-columns: min-content 1fr;
|
||||
}
|
||||
@media only screen and (max-width: 600px) {
|
||||
#notes2 {
|
||||
grid-template-areas: "crumbs" "name" "content" "blank";
|
||||
grid-template-areas: "crumbs" "sync" "name" "content" "blank";
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
#notes2 #tree {
|
||||
|
@ -75,6 +75,52 @@ html {
|
|||
justify-items: center;
|
||||
margin: 16px;
|
||||
}
|
||||
#sync-progress {
|
||||
grid-area: sync;
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
justify-self: center;
|
||||
width: 100%;
|
||||
max-width: 900px;
|
||||
height: 56px;
|
||||
position: relative;
|
||||
}
|
||||
#sync-progress.hidden {
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
transition: visibility 0s 500ms, opacity 500ms linear;
|
||||
}
|
||||
#sync-progress progress {
|
||||
width: calc(100% - 16px);
|
||||
height: 16px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
#sync-progress progress[value]::-webkit-progress-bar {
|
||||
background-color: #eee;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.25) inset;
|
||||
border-radius: 4px;
|
||||
}
|
||||
#sync-progress progress[value]::-moz-progress-bar {
|
||||
background-color: #eee;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.25) inset;
|
||||
border-radius: 4px;
|
||||
}
|
||||
#sync-progress progress[value]::-webkit-progress-value {
|
||||
background: #ba5f59;
|
||||
background: linear-gradient(180deg, #ba5f59 0%, #fe5f55 50%, #ba5f59 100%);
|
||||
border-radius: 4px;
|
||||
}
|
||||
#sync-progress progress[value]::-moz-progress-value {
|
||||
background: #ba5f59;
|
||||
background: linear-gradient(180deg, #ba5f59 0%, #fe5f55 50%, #ba5f59 100%);
|
||||
border-radius: 4px;
|
||||
}
|
||||
#sync-progress .count {
|
||||
margin-top: 0px;
|
||||
color: #888;
|
||||
position: absolute;
|
||||
top: 22px;
|
||||
}
|
||||
.crumbs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
@ -109,11 +155,11 @@ html {
|
|||
margin-left: 0px;
|
||||
}
|
||||
#name {
|
||||
color: #666;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
font-size: 1.15em;
|
||||
margin-top: 32px;
|
||||
margin-top: 0px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
/* ============================================================= *
|
||||
|
|
|
@ -2,6 +2,7 @@ import { h, Component, createRef } from 'preact'
|
|||
import htm from 'htm'
|
||||
import { signal } from 'preact/signals'
|
||||
import { ROOT_NODE } from 'node_store'
|
||||
import { SyncProgress } from 'sync'
|
||||
const html = htm.bind(h)
|
||||
|
||||
export class NodeUI extends Component {
|
||||
|
@ -15,6 +16,7 @@ export class NodeUI extends Component {
|
|||
this.keys = signal([])
|
||||
this.page = signal('node')
|
||||
this.crumbs = []
|
||||
this.syncProgress = createRef()
|
||||
window.addEventListener('popstate', evt => {
|
||||
if (evt.state?.hasOwnProperty('nodeUUID'))
|
||||
_notes2.current.goToNode(evt.state.nodeUUID, true)
|
||||
|
@ -52,6 +54,7 @@ export class NodeUI extends Component {
|
|||
${crumbDivs}
|
||||
</div>
|
||||
</div>
|
||||
<${SyncProgress} ref=${this.syncProgress} />
|
||||
<div id="name">${node.get('Name')}</div>
|
||||
<${NodeContent} key=${node.UUID} node=${node} ref=${this.nodeContent} />
|
||||
<div id="blank"></div>
|
||||
|
|
|
@ -246,9 +246,6 @@ export class NodeStore {
|
|||
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 = []
|
||||
|
@ -395,16 +392,26 @@ class SimpleNodeStore {
|
|||
|
||||
const promises = []
|
||||
for (const key of keys) {
|
||||
const p = new Promise((resolve, reject)=>{
|
||||
const p = new Promise((resolve, reject) => {
|
||||
// TODO - implement a way to add an error to a page-global error log.
|
||||
const request = store.delete(key)
|
||||
request.onsuccess = (event)=>resolve(event)
|
||||
request.onerror = (event)=>reject(event)
|
||||
request.onsuccess = (event) => resolve(event)
|
||||
request.onerror = (event) => reject(event)
|
||||
})
|
||||
promises.push(p)
|
||||
}
|
||||
return Promise.all(promises)
|
||||
}//}}}
|
||||
async count() {//{{{
|
||||
const store = this.db
|
||||
.transaction(['nodes', this.storeName], 'readonly')
|
||||
.objectStore(this.storeName)
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = store.count()
|
||||
request.onsuccess = (event) => resolve(event.target.result)
|
||||
request.onerror = (event) => reject(event.target.error)
|
||||
})
|
||||
}//}}}
|
||||
}
|
||||
|
||||
// vim: foldmethod=marker
|
||||
|
|
|
@ -13,9 +13,8 @@ export class Notes2 extends Component {
|
|||
startNode: null,
|
||||
}
|
||||
|
||||
Sync.nodesFromServer().then(durationNodes =>
|
||||
console.log(`Total time: ${Math.round(100 * durationNodes) / 100}s`)
|
||||
)
|
||||
window._sync = new Sync()
|
||||
window._sync.run()
|
||||
|
||||
this.getStartNode()
|
||||
}//}}}
|
||||
|
|
|
@ -1,21 +1,68 @@
|
|||
import { API } from 'api'
|
||||
import { Node } from 'node'
|
||||
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
|
||||
|
||||
export class Sync {
|
||||
constructor() {
|
||||
this.foo = ''
|
||||
}
|
||||
|
||||
static async nodesFromServer() {//{{{
|
||||
let duration = 0
|
||||
const syncStart = Date.now()
|
||||
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() {//{{{
|
||||
try {
|
||||
let duration = 0 // in ms
|
||||
|
||||
// The latest sync node value is used to retrieve the changes
|
||||
// from the backend.
|
||||
const state = await nodeStore.getAppState('latest_sync_node')
|
||||
const oldMax = (state?.value ? state.value : 0)
|
||||
let currMax = oldMax
|
||||
|
||||
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 {
|
||||
this.pushMessage({ op: SYNC_DONE, })
|
||||
}
|
||||
}//}}}
|
||||
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
|
||||
let offset = 0
|
||||
let res = { Continue: false }
|
||||
let batch = 0
|
||||
|
@ -39,7 +86,7 @@ export class Sync {
|
|||
let backendNode = null
|
||||
for (const i in res.Nodes) {
|
||||
backendNode = new Node(res.Nodes[i], -1)
|
||||
await Sync.handleNode(backendNode)
|
||||
await window._sync.handleNode(backendNode)
|
||||
}
|
||||
|
||||
} while (res.Continue)
|
||||
|
@ -48,14 +95,14 @@ export class Sync {
|
|||
} catch (e) {
|
||||
console.log('sync node tree', e)
|
||||
} finally {
|
||||
const syncEnd = Date.now()
|
||||
duration = (syncEnd - syncStart) / 1000
|
||||
syncEnd = Date.now()
|
||||
const duration = (syncEnd - syncStart) / 1000
|
||||
const count = await nodeStore.nodeCount()
|
||||
console.log(`Node sync took ${duration}s`, count)
|
||||
}
|
||||
return duration
|
||||
return (syncEnd - syncStart)
|
||||
}//}}}
|
||||
static async handleNode(backendNode) {//{{{
|
||||
async handleNode(backendNode) {//{{{
|
||||
try {
|
||||
/* Retrieving the local copy of this node from IndexedDB.
|
||||
* The backend node can be discarded if it is older than
|
||||
|
@ -83,14 +130,16 @@ export class Sync {
|
|||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally {
|
||||
this.pushMessage({ op: SYNC_HANDLED, count: 1 })
|
||||
}
|
||||
}//}}}
|
||||
|
||||
static async nodesToServer() {//{{{
|
||||
while(true) {
|
||||
async nodesToServer() {//{{{
|
||||
const BATCH_SIZE = 32
|
||||
while (true) {
|
||||
try {
|
||||
// Send nodes in batches until everything is sent, or an error has occured.
|
||||
const nodesToSend = await nodeStore.sendQueue.retrieve(2)
|
||||
const nodesToSend = await nodeStore.sendQueue.retrieve(BATCH_SIZE)
|
||||
if (nodesToSend.length === 0)
|
||||
break
|
||||
console.debug(`Sending ${nodesToSend.length} node(s) to server`)
|
||||
|
@ -109,6 +158,7 @@ export class Sync {
|
|||
// Nodes are archived on server and can now be deleted from the send queue.
|
||||
const keys = nodesToSend.map(node => node.ClientSequence)
|
||||
console.log(await nodeStore.sendQueue.delete(keys))
|
||||
this.pushMessage({ op: SYNC_HANDLED, count: nodesToSend.length })
|
||||
|
||||
} catch (e) {
|
||||
console.trace(e)
|
||||
|
@ -118,3 +168,48 @@ export class Sync {
|
|||
}
|
||||
}//}}}
|
||||
}
|
||||
|
||||
export class SyncProgress extends Component {
|
||||
constructor() {//{{{
|
||||
super()
|
||||
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)
|
||||
}//}}}
|
||||
progressHandler(msg) {//{{{
|
||||
switch (msg.op) {
|
||||
case SYNC_COUNT:
|
||||
this.setState({ nodesToSync: msg.count })
|
||||
break
|
||||
|
||||
case SYNC_HANDLED:
|
||||
this.setState({ nodesSynced: this.state.nodesSynced + msg.count })
|
||||
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>
|
||||
`
|
||||
}//}}}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ html {
|
|||
display: grid;
|
||||
grid-template-areas:
|
||||
"tree crumbs"
|
||||
"tree sync"
|
||||
"tree name"
|
||||
"tree content"
|
||||
//"tree checklist"
|
||||
|
@ -22,6 +23,7 @@ html {
|
|||
@media only screen and (max-width: 600px) {
|
||||
grid-template-areas:
|
||||
"crumbs"
|
||||
"sync"
|
||||
"name"
|
||||
"content"
|
||||
//"checklist"
|
||||
|
@ -110,6 +112,61 @@ html {
|
|||
margin: 16px;
|
||||
}
|
||||
|
||||
#sync-progress {
|
||||
grid-area: sync;
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
justify-self: center;
|
||||
width: 100%;
|
||||
max-width: 900px;
|
||||
height: 56px;
|
||||
position: relative;
|
||||
|
||||
&.hidden {
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
transition: visibility 0s 500ms, opacity 500ms linear;
|
||||
}
|
||||
|
||||
progress {
|
||||
width: calc(100% - 16px);
|
||||
height: 16px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
progress[value]::-webkit-progress-bar {
|
||||
background-color: #eee;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.25) inset;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
progress[value]::-moz-progress-bar {
|
||||
background-color: #eee;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.25) inset;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
progress[value]::-webkit-progress-value {
|
||||
background: rgb(186,95,89);
|
||||
background: linear-gradient(180deg, rgba(186,95,89,1) 0%, rgba(254,95,85,1) 50%, rgba(186,95,89,1) 100%);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
// TODO: style the progress value for Firefox
|
||||
progress[value]::-moz-progress-value {
|
||||
background: rgb(186,95,89);
|
||||
background: linear-gradient(180deg, rgba(186,95,89,1) 0%, rgba(254,95,85,1) 50%, rgba(186,95,89,1) 100%);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.count {
|
||||
margin-top: 0px;
|
||||
color: #888;
|
||||
position: absolute;
|
||||
top: 22px;
|
||||
}
|
||||
}
|
||||
|
||||
.crumbs {
|
||||
background: #e4e4e4;
|
||||
display: flex;
|
||||
|
@ -151,11 +208,11 @@ html {
|
|||
}
|
||||
|
||||
#name {
|
||||
color: @color3;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
font-size: 1.15em;
|
||||
margin-top: 32px;
|
||||
margin-top: 0px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue