diff --git a/main.go b/main.go
index 54b9ab7..3c17136 100644
--- a/main.go
+++ b/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)
diff --git a/node.go b/node.go
index f92d524..c33a513 100644
--- a/node.go
+++ b/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)
}
}
diff --git a/static/css/notes2.css b/static/css/notes2.css
index 30f207a..4be2968 100644
--- a/static/css/notes2.css
+++ b/static/css/notes2.css
@@ -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;
}
/* ============================================================= *
diff --git a/static/js/node.mjs b/static/js/node.mjs
index 0374be9..e0b3201 100644
--- a/static/js/node.mjs
+++ b/static/js/node.mjs
@@ -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}
+ <${SyncProgress} ref=${this.syncProgress} />
${node.get('Name')}
<${NodeContent} key=${node.UUID} node=${node} ref=${this.nodeContent} />
diff --git a/static/js/node_store.mjs b/static/js/node_store.mjs
index e1253a4..8b6dea5 100644
--- a/static/js/node_store.mjs
+++ b/static/js/node_store.mjs
@@ -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
diff --git a/static/js/notes2.mjs b/static/js/notes2.mjs
index 3a91a29..4da700a 100644
--- a/static/js/notes2.mjs
+++ b/static/js/notes2.mjs
@@ -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()
}//}}}
diff --git a/static/js/sync.mjs b/static/js/sync.mjs
index 6321db6..6bfe1c8 100644
--- a/static/js/sync.mjs
+++ b/static/js/sync.mjs
@@ -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``
+
+ return html`
+
+
+
${nodesSynced} / ${nodesToSync}
+
+ `
+ }//}}}
+}
diff --git a/static/less/notes2.less b/static/less/notes2.less
index f3abdc5..c39d7af 100644
--- a/static/less/notes2.less
+++ b/static/less/notes2.less
@@ -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;
}