diff --git a/authentication/pkg.go b/authentication/pkg.go index 9eb6245..bcd84ed 100644 --- a/authentication/pkg.go +++ b/authentication/pkg.go @@ -2,9 +2,8 @@ package authentication import ( // External - werr "git.gibonuddevalla.se/go/wrappederror" + _ "git.gibonuddevalla.se/go/wrappederror" "github.com/golang-jwt/jwt/v5" - "github.com/google/uuid" "github.com/jmoiron/sqlx" "github.com/lib/pq" @@ -147,14 +146,6 @@ func (mngr *Manager) AuthenticationHandler(w http.ResponseWriter, r *http.Reques data["uid"] = user.ID data["login"] = user.Username data["name"] = user.Name - - data["cid"], err = mngr.NewClientUUID(user) - if err != nil { - mngr.log.Error("authentication", "error", err) - httpError(w, err) - return - } - token, err = mngr.GenerateToken(data) if err != nil { mngr.log.Error("authentication", "error", err) @@ -278,31 +269,3 @@ func (mngr *Manager) ChangePassword(username, currentPassword, newPassword strin changed = (rowsAffected == 1) return } // }}} -func (mngr *Manager) NewClientUUID(user User) (clientUUID string, err error) { // {{{ - // Each client session has its own UUID. - // Loop through until a unique one is established. - var proposedClientUUID string - var numSessions int - for { - proposedClientUUID = uuid.NewString() - row := mngr.db.QueryRow("SELECT COUNT(id) FROM public.client WHERE client_uuid = $1", proposedClientUUID) - err = row.Scan(&numSessions) - if err != nil { - err = werr.Wrap(err).WithData(proposedClientUUID) - return - } - - if numSessions > 0 { - continue - } - - _, err = mngr.db.Exec(`INSERT INTO public.client(user_id, client_uuid) VALUES($1, $2)`, user.ID, proposedClientUUID) - if err != nil { - err = werr.Wrap(err).WithData(proposedClientUUID) - return - } - clientUUID = proposedClientUUID - break - } - return -} // }}} diff --git a/main.go b/main.go index 3c17136..34545e6 100644 --- a/main.go +++ b/main.go @@ -124,9 +124,7 @@ func main() { // {{{ http.HandleFunc("/user/authenticate", AuthManager.AuthenticationHandler) - 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)) + http.HandleFunc("/sync/node/{sequence}/{offset}", authenticated(actionSyncNode)) http.HandleFunc("/node/retrieve/{uuid}", authenticated(actionNodeRetrieve)) @@ -168,7 +166,7 @@ func authenticated(fn func(http.ResponseWriter, *http.Request)) func(http.Respon user := NewUser(claims) r = r.WithContext(context.WithValue(r.Context(), CONTEXT_USER, user)) - Log.Debug("webserver", "op", "request", "method", r.Method, "url", r.URL.String(), "username", user.Username, "client", user.ClientUUID) + Log.Info("webserver", "op", "request", "method", r.Method, "url", r.URL.String(), "username", user.Username) fn(w, r) } } // }}} @@ -243,22 +241,33 @@ func pageSync(w http.ResponseWriter, r *http.Request) { // {{{ } } // }}} -func actionSyncFromServer(w http.ResponseWriter, r *http.Request) { // {{{ +func actionSyncNode(w http.ResponseWriter, r *http.Request) { // {{{ // The purpose of the Client UUID is to avoid // sending nodes back once again to a client that // just created or modified it. - user := getUser(r) - changedFrom, _ := strconv.Atoi(r.PathValue("sequence")) - offset, _ := strconv.Atoi(r.PathValue("offset")) - - nodes, maxSeq, moreRowsExist, err := Nodes(user.UserID, offset, uint64(changedFrom), user.ClientUUID) + request := struct { + ClientUUID string + }{} + body, _ := io.ReadAll(r.Body) + err := json.Unmarshal(body, &request) if err != nil { - Log.Error("/sync/from_server", "error", err) + Log.Error("/node/tree", "error", err) httpError(w, err) return } - Log.Debug("/sync/from_server", "num_nodes", len(nodes), "maxSeq", maxSeq) + user := getUser(r) + changedFrom, _ := strconv.Atoi(r.PathValue("sequence")) + offset, _ := strconv.Atoi(r.PathValue("offset")) + + nodes, maxSeq, moreRowsExist, err := Nodes(user.ID, offset, uint64(changedFrom), request.ClientUUID) + if err != nil { + Log.Error("/node/tree", "error", err) + httpError(w, err) + return + } + + Log.Debug("/node/tree", "num_nodes", len(nodes), "maxSeq", maxSeq) foo, _ := json.Marshal(nodes) os.WriteFile(fmt.Sprintf("/tmp/nodes-%d.json", offset), foo, 0644) @@ -270,36 +279,12 @@ func actionSyncFromServer(w http.ResponseWriter, r *http.Request) { // {{{ }{true, nodes, maxSeq, moreRowsExist}) w.Write(j) } // }}} -func actionSyncFromServerCount(w http.ResponseWriter, r *http.Request) { // {{{ - // The purpose of the Client UUID is to avoid - // sending nodes back once again to a client that - // just created or modified it. - 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) - httpError(w, err) - return - } - - j, _ := json.Marshal(struct { - OK bool - Count int - }{ - true, - count, - }) - w.Write(j) -} // }}} func actionNodeRetrieve(w http.ResponseWriter, r *http.Request) { // {{{ user := getUser(r) var err error uuid := r.PathValue("uuid") - node, err := RetrieveNode(user.UserID, uuid) + node, err := RetrieveNode(user.ID, uuid) if err != nil { responseError(w, err) return @@ -310,25 +295,6 @@ func actionNodeRetrieve(w http.ResponseWriter, r *http.Request) { // {{{ "Node": node, }) } // }}} -func actionSyncToServer(w http.ResponseWriter, r *http.Request) { // {{{ - user := getUser(r) - - body, _ := io.ReadAll(r.Body) - var request struct { - NodeData string - } - err := json.Unmarshal(body, &request) - if err != nil { - httpError(w, err) - return - } - - db.Exec(`CALL add_nodes($1, $2, $3::jsonb)`, user.UserID, user.ClientUUID, request.NodeData) - - responseData(w, map[string]interface{}{ - "OK": true, - }) -} // }}} func createNewUser(username string) { // {{{ reader := bufio.NewReader(os.Stdin) @@ -372,7 +338,7 @@ func changePassword(username string) { // {{{ fmt.Printf("\nPassword changed\n") } // }}} -func getUser(r *http.Request) UserSession { // {{{ - user, _ := r.Context().Value(CONTEXT_USER).(UserSession) +func getUser(r *http.Request) User { // {{{ + user, _ := r.Context().Value(CONTEXT_USER).(User) return user } // }}} diff --git a/node.go b/node.go index c33a513..83b1c5a 100644 --- a/node.go +++ b/node.go @@ -2,7 +2,6 @@ package main import ( // External - werr "git.gibonuddevalla.se/go/wrappederror" "github.com/jmoiron/sqlx" // Standard @@ -184,39 +183,6 @@ func Nodes(userID, offset int, synced uint64, clientUUID string) (nodes []Node, return } // }}} -func NodesCount(userID int, synced uint64, clientUUID string) (count int, err error) { // {{{ - row := db.QueryRow(` - SELECT - COUNT(*) - FROM - public.node - WHERE - user_id = $1 AND - client != $3 AND - NOT history AND ( - created_seq > $2 OR - updated_seq > $2 OR - deleted_seq > $2 - ) - `, - userID, - synced, - clientUUID, - ) - err = row.Scan(&count) - if err != nil { - err = werr.Wrap(err).WithData( - struct { - UserID int - Synced uint64 - ClientUUID string - }{ - userID, synced, clientUUID, - }, - ) - } - return -} // }}} func RetrieveNode(userID int, nodeUUID string) (node Node, err error) { // {{{ var rows *sqlx.Row rows = db.QueryRowx(` @@ -262,7 +228,7 @@ func NodeCrumbs(nodeUUID string) (nodes []Node, err error) { // {{{ SELECT n.uuid, - COALESCE(n.parent_uuid, '') AS parent_uuid, + COALESCE(n.parent_uuid, 0) AS parent_uuid, n.name FROM node n INNER JOIN nodes nr ON n.uuid = nr.parent_uuid @@ -286,13 +252,13 @@ func NodeCrumbs(nodeUUID string) (nodes []Node, err error) { // {{{ } // }}} func TestData() (err error) { - for range 8 { + for range 10 { hash1, name1, _ := generateOneTestNode("", "G") - for range 8 { + for range 10 { hash2, name2, _ := generateOneTestNode(hash1, name1) - for range 8 { + for range 10 { hash3, name3, _ := generateOneTestNode(hash2, name2) - for range 8 { + for range 10 { generateOneTestNode(hash3, name3) } } diff --git a/sql/00006.sql b/sql/00006.sql deleted file mode 100644 index 6b0ea9b..0000000 --- a/sql/00006.sql +++ /dev/null @@ -1,16 +0,0 @@ -CREATE TABLE public.node_history ( - id serial4 NOT NULL, - user_id int4 NOT NULL, - uuid bpchar(36) NOT NULL, - parents varchar[] NULL, - created timestamptz NOT NULL, - updated timestamptz NOT NULL, - name varchar(256) NOT NULL, - "content" text NOT NULL, - content_encrypted text NOT NULL, - markdown bool DEFAULT false NOT NULL, - client bpchar(36) DEFAULT ''::bpchar NOT NULL, - CONSTRAINT node_history_pk PRIMARY KEY (id), - CONSTRAINT node_history_user_fk FOREIGN KEY (user_id) REFERENCES public."user"(id) ON DELETE RESTRICT ON UPDATE RESTRICT -); -CREATE INDEX node_history_uuid_idx ON public.node USING btree (uuid); diff --git a/sql/00007.sql b/sql/00007.sql deleted file mode 100644 index 40ce48e..0000000 --- a/sql/00007.sql +++ /dev/null @@ -1,162 +0,0 @@ -CREATE TYPE json_ancestor_array as ("Ancestors" varchar[]); - - -CREATE OR REPLACE PROCEDURE add_nodes(p_user_id int4, p_client_uuid varchar, p_nodes jsonb) -LANGUAGE PLPGSQL AS $$ - -DECLARE - node_data jsonb; - node_updated timestamptz; - db_updated timestamptz; - db_uuid bpchar; - db_client bpchar; - db_client_seq int; - node_uuid bpchar; - -BEGIN - RAISE NOTICE '--------------------------'; - FOR node_data IN SELECT * FROM jsonb_array_elements(p_nodes) - LOOP - node_uuid = (node_data->>'UUID')::bpchar; - node_updated = (node_data->>'Updated')::timestamptz; - - /* Retrieve the current modified timestamp for this node from the database. */ - SELECT - uuid, updated, client, client_sequence - INTO - db_uuid, db_updated, db_client, db_client_seq - FROM public."node" - WHERE - user_id = p_user_id AND - uuid = node_uuid; - - /* Is the node not in database? It needs to be created. */ - IF db_uuid IS NULL THEN - RAISE NOTICE '01 New node %', node_uuid; - INSERT INTO public."node" ( - user_id, "uuid", parent_uuid, created, updated, - "name", "content", markdown, "content_encrypted", - client, client_sequence - ) - VALUES( - p_user_id, - node_uuid, - (node_data->>'ParentUUID')::bpchar, - (node_data->>'Created')::timestamptz, - (node_data->>'Updated')::timestamptz, - (node_data->>'Name')::varchar, - (node_data->>'Content')::text, - (node_data->>'Markdown')::bool, - '', /* content_encrypted */ - p_client_uuid, - (node_data->>'ClientSequence')::int - ); - CONTINUE; - END IF; - - - /* The client could send a specific node again if it didn't receive the OK from this procedure before. */ - IF db_updated = node_updated AND db_client = p_client_uuid AND db_client_seq = (node_data->>'ClientSequence')::int THEN - RAISE NOTICE '04, already recorded, %, %', db_client, db_client_seq; - CONTINUE; - END IF; - - /* Determine if the incoming node data is to go into history or replace the current node. */ - IF db_updated > node_updated THEN - RAISE NOTICE '02 DB newer, % > % (%))', db_updated, node_updated, node_uuid; - /* Incoming node is going straight to history since it is older than the current node. */ - INSERT INTO node_history( - user_id, "uuid", parents, created, updated, - "name", "content", markdown, "content_encrypted", - client, client_sequence - ) - VALUES( - p_user_id, - node_uuid, - (jsonb_populate_record(null::json_ancestor_array, node_data))."Ancestors", - (node_data->>'Created')::timestamptz, - (node_data->>'Updated')::timestamptz, - (node_data->>'Name')::varchar, - (node_data->>'Content')::text, - (node_data->>'Markdown')::bool, - '', /* content_encrypted */ - p_client_uuid, - (node_data->>'ClientSequence')::int - ) - ON CONFLICT (client, client_sequence) - DO NOTHING; - ELSE - RAISE NOTICE '03 Client newer, % > % (%, %)', node_updated, db_updated, node_uuid, (node_data->>'ClientSequence'); - /* Incoming node is newer and will replace the current node. - * - * The current node is copied to the node_history table and then modified in place - * with the incoming data. */ - INSERT INTO node_history( - user_id, "uuid", parents, - created, updated, "name", "content", markdown, "content_encrypted", - client, client_sequence - ) - SELECT - user_id, - "uuid", - ( - WITH RECURSIVE nodes AS ( - SELECT - uuid, - COALESCE(parent_uuid, '') AS parent_uuid, - name, - 0 AS depth - FROM node - WHERE - uuid = node_uuid - - UNION - - SELECT - n.uuid, - COALESCE(n.parent_uuid, '') AS parent_uuid, - n.name, - nr.depth+1 AS depth - FROM node n - INNER JOIN nodes nr ON n.uuid = nr.parent_uuid - ) - SELECT ARRAY ( - SELECT name - FROM nodes - ORDER BY depth DESC - OFFSET 1 /* discard itself */ - ) - ), - created, - updated, - name, - content, - markdown, - content_encrypted, - client, - client_sequence - FROM public."node" - WHERE - user_id = p_user_id AND - uuid = node_uuid - ON CONFLICT (client, client_sequence) - DO NOTHING; - - /* Current node in database is updated with incoming data. */ - UPDATE public."node" - SET - updated = (node_data->>'Updated')::timestamptz, - updated_seq = nextval('node_updates'), - name = (node_data->>'Name')::varchar, - content = (node_data->>'Content')::text, - markdown = (node_data->>'Markdown')::bool, - client = p_client_uuid, - client_sequence = (node_data->>'ClientSequence')::int - WHERE - user_id = p_user_id AND - uuid = node_uuid; - END IF; - - END LOOP; -END -$$; diff --git a/sql/00008.sql b/sql/00008.sql deleted file mode 100644 index a91d54c..0000000 --- a/sql/00008.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE node ADD COLUMN Client_sequence int NULL; -ALTER TABLE node_history ADD COLUMN Client_sequence int NULL; diff --git a/sql/00009.sql b/sql/00009.sql deleted file mode 100644 index 332af3a..0000000 --- a/sql/00009.sql +++ /dev/null @@ -1 +0,0 @@ -CREATE UNIQUE INDEX node_history_client_idx ON public.node_history (client,client_sequence); diff --git a/sql/00010.sql b/sql/00010.sql deleted file mode 100644 index c0f14ee..0000000 --- a/sql/00010.sql +++ /dev/null @@ -1,10 +0,0 @@ -CREATE TABLE public.client ( - id serial NOT NULL, - user_id int4 NOT NULL, - client_uuid bpchar(36) DEFAULT '' NOT NULL, - created timestamptz DEFAULT NOW() NOT NULL, - description varchar DEFAULT '' NOT NULL, - CONSTRAINT client_pk PRIMARY KEY (id) -); - -CREATE UNIQUE INDEX client_uuid_idx ON public.client (client_uuid); diff --git a/static/css/notes2.css b/static/css/notes2.css index 4be2968..30f207a 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 sync" "tree name" "tree content" "tree blank"; + grid-template-areas: "tree crumbs" "tree name" "tree content" "tree blank"; grid-template-columns: min-content 1fr; } @media only screen and (max-width: 600px) { #notes2 { - grid-template-areas: "crumbs" "sync" "name" "content" "blank"; + grid-template-areas: "crumbs" "name" "content" "blank"; grid-template-columns: 1fr; } #notes2 #tree { @@ -75,52 +75,6 @@ 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; @@ -155,11 +109,11 @@ html { margin-left: 0px; } #name { - color: #333; + color: #666; font-weight: bold; text-align: center; font-size: 1.15em; - margin-top: 0px; + margin-top: 32px; margin-bottom: 16px; } /* ============================================================= * diff --git a/static/js/node.mjs b/static/js/node.mjs index e0b3201..d234789 100644 --- a/static/js/node.mjs +++ b/static/js/node.mjs @@ -2,7 +2,6 @@ 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 { @@ -16,7 +15,6 @@ 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) @@ -39,7 +37,7 @@ export class NodeUI extends Component { const crumbDivs = [ html`
_notes2.current.goToNode(ROOT_NODE)}>Start
` ] - for (let i = this.crumbs.length - 1; i >= 0; i--) { + for (let i = this.crumbs.length-1; i >= 0; i--) { const crumbNode = this.crumbs[i] crumbDivs.push(html`
_notes2.current.goToNode(crumbNode.UUID)}>${crumbNode.get('Name')}
`) } @@ -54,7 +52,6 @@ export class NodeUI extends Component { ${crumbDivs} - <${SyncProgress} ref=${this.syncProgress} />
${node.get('Name')}
<${NodeContent} key=${node.UUID} node=${node} ref=${this.nodeContent} />
@@ -170,32 +167,13 @@ export class NodeUI extends Component { if (!this.nodeModified.value) return - /* 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. */ - const node = this.node.value - - // The node is still in its old state and will present - // the unmodified content to the node store. - const history = nodeStore.nodesHistory.add(node) + await nodeStore.copyToNodesHistory(this.node.value) // Prepares the node object for saving. // Sets Updated value to current date and time. - await node.save() - - // Updated node is added to the send queue to be stored on server. - const sendQueue = nodeStore.sendQueue.add(this.node.value) - - // Updated node is saved to the primary node store. - const nodeStoreAdding = nodeStore.add([node]) - - await Promise.all([history, sendQueue, nodeStoreAdding]) + const node = this.node.value + node.save() + await nodeStore.add([node]) this.nodeModified.value = false }//}}} @@ -337,7 +315,6 @@ export class Node { this._children_fetched = false this.Children = [] - this.Ancestors = [] this._content = this.data.Content this._modified = false @@ -395,15 +372,10 @@ export class Node { this._decrypted = true */ }//}}} - async save() {//{{{ + save() {//{{{ this.data.Content = this._content this.data.Updated = new Date().toISOString() 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() }//}}} } diff --git a/static/js/node_store.mjs b/static/js/node_store.mjs index 8b6dea5..a1a8a1a 100644 --- a/static/js/node_store.mjs +++ b/static/js/node_store.mjs @@ -10,8 +10,6 @@ export class NodeStore { this.db = null this.nodes = {} - this.sendQueue = null - this.nodesHistory = null }//}}} async initializeDB() {//{{{ return new Promise((resolve, reject) => { @@ -50,7 +48,7 @@ export class NodeStore { break case 5: - sendQueue = db.createObjectStore('send_queue', { keyPath: 'ClientSequence', autoIncrement: true }) + sendQueue = db.createObjectStore('send_queue', { keyPath: ['UUID', 'Updated'] }) sendQueue.createIndex('updated', 'Updated', { unique: false }) break @@ -67,9 +65,8 @@ export class NodeStore { req.onsuccess = (event) => { this.db = event.target.result - this.sendQueue = new SimpleNodeStore(this.db, 'send_queue') - this.nodesHistory = new SimpleNodeStore(this.db, 'nodes_history') this.initializeRootNode() + .then(() => this.initializeClientUUID()) .then(() => resolve()) } @@ -109,6 +106,13 @@ export class NodeStore { getRequest.onerror = (event) => reject(event.target.error) }) }//}}} + async initializeClientUUID() {//{{{ + let clientUUID = await this.getAppState('client_uuid') + if (clientUUID !== null) + return + clientUUID = crypto.randomUUID() + return this.setAppState('client_uuid', clientUUID) + }//}}} node(uuid, dataIfUndefined, newLevel) {//{{{ let n = this.nodes[uuid] @@ -156,6 +160,64 @@ export class NodeStore { }) }//}}} + async moveToSendQueue(nodeToMove, replaceWithNode) {//{{{ + return new Promise((resolve, reject) => { + const t = this.db.transaction(['nodes', 'send_queue'], 'readwrite') + const nodeStore = t.objectStore('nodes') + const sendQueue = t.objectStore('send_queue') + t.onerror = (event) => { + console.log('transaction error', event.target.error) + reject(event.target.error) + } + t.oncomplete = () => { + resolve() + } + + // Node to be moved is first stored in the new queue. + const queueReq = sendQueue.put(nodeToMove.data) + queueReq.onsuccess = () => { + // When added to the send queue, the node is either deleted + // or replaced with a new node. + console.debug(`Queueing ${nodeToMove.UUID} (${nodeToMove.get('Name')})`) + let nodeReq + if (replaceWithNode) + nodeReq = nodeStore.put(replaceWithNode.data) + else + nodeReq = nodeStore.delete(nodeToMove.UUID) + nodeReq.onsuccess = () => { + resolve() + } + nodeReq.onerror = (event) => { + console.log(`Error moving ${nodeToMove.UUID}`, event.target.error) + reject(event.target.error) + } + + } + queueReq.onerror = (event) => { + console.log(`Error queueing ${nodeToMove.UUID}`, event.target.error) + reject(event.target.error) + } + }) + }//}}} + async copyToNodesHistory(nodeToCopy) {//{{{ + return new Promise((resolve, reject) => { + const t = this.db.transaction('nodes_history', 'readwrite') + const nodesHistory = t.objectStore('nodes_history') + t.oncomplete = () => { + resolve() + } + t.onerror = (event) => { + console.log('transaction error', event.target.error) + reject(event.target.error) + } + + const historyReq = nodesHistory.put(nodeToCopy.data) + historyReq.onerror = (event) => { + console.log(`Error copying ${nodeToCopy.UUID}`, event.target.error) + reject(event.target.error) + } + }) + }//}}} async storeNode(node) {//{{{ return new Promise((resolve, reject) => { const t = this.db.transaction('nodes', 'readwrite') @@ -246,6 +308,9 @@ 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 = [] @@ -332,86 +397,4 @@ export class NodeStore { }//}}} } -class SimpleNodeStore { - constructor(db, storeName) {//{{{ - this.db = db - this.storeName = storeName - }//}}} - async add(node) {//{{{ - return new Promise((resolve, reject) => { - const t = this.db.transaction(['nodes', this.storeName], 'readwrite') - const store = t.objectStore(this.storeName) - t.onerror = (event) => { - console.log('transaction error', event.target.error) - reject(event.target.error) - } - - // Node to be moved is first stored in the new queue. - const req = store.put(node.data) - req.onsuccess = () => { - resolve() - } - req.onerror = (event) => { - console.log(`Error adding ${node.UUID}`, event.target.error) - reject(event.target.error) - } - }) - }//}}} - async retrieve(limit) {//{{{ - return new Promise((resolve, reject) => { - const cursorReq = this.db - .transaction(['nodes', this.storeName], 'readonly') - .objectStore(this.storeName) - .index('updated') - .openCursor() - - let retrieved = 0 - const nodes = [] - - cursorReq.onsuccess = (event) => { - const cursor = event.target.result - if (!cursor) { - resolve(nodes) - return - } - retrieved++ - nodes.push(cursor.value) - if (retrieved === limit) { - resolve(nodes) - return - } - - cursor.continue() - } - }) - }//}}} - async delete(keys) {//{{{ - const store = this.db - .transaction(['nodes', this.storeName], 'readwrite') - .objectStore(this.storeName) - - const promises = [] - for (const key of keys) { - 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) - }) - 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 4da700a..5e62ffa 100644 --- a/static/js/notes2.mjs +++ b/static/js/notes2.mjs @@ -13,8 +13,9 @@ export class Notes2 extends Component { startNode: null, } - window._sync = new Sync() - window._sync.run() + Sync.nodes().then(durationNodes => + console.log(`Total time: ${Math.round(100 * durationNodes) / 100}s`) + ) this.getStartNode() }//}}} diff --git a/static/js/sync.mjs b/static/js/sync.mjs index 293bb42..2814fe4 100644 --- a/static/js/sync.mjs +++ b/static/js/sync.mjs @@ -1,73 +1,28 @@ 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.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) - }//}}} + constructor() { + this.foo = '' + } - async run() {//{{{ + static async nodes() { + let duration = 0 + const syncStart = Date.now() 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 clientUUID = await nodeStore.getAppState('client_uuid') const oldMax = (state?.value ? state.value : 0) - - 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 do { batch++ - res = await API.query('POST', `/sync/from_server/${oldMax}/${offset}`) + res = await API.query('POST', `/sync/node/${oldMax}/${offset}`, { ClientUUID: clientUUID.value }) if (res.Nodes.length > 0) console.log(`Node sync batch #${batch}`) offset += res.Nodes.length @@ -85,7 +40,7 @@ export class Sync { let backendNode = null for (const i in res.Nodes) { backendNode = new Node(res.Nodes[i], -1) - await window._sync.handleNode(backendNode) + await Sync.handleNode(backendNode) } } while (res.Continue) @@ -94,14 +49,14 @@ export class Sync { } catch (e) { console.log('sync node tree', e) } finally { - syncEnd = Date.now() - const duration = (syncEnd - syncStart) / 1000 + const syncEnd = Date.now() + duration = (syncEnd - syncStart) / 1000 const count = await nodeStore.nodeCount() console.log(`Node sync took ${duration}s`, count) } - return (syncEnd - syncStart) - }//}}} - async handleNode(backendNode) {//{{{ + return duration + } + static async handleNode(backendNode) { try { /* Retrieving the local copy of this node from IndexedDB. * The backend node can be discarded if it is older than @@ -114,117 +69,16 @@ export class Sync { return } - /* 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]) - + // local node is older than the backend node + // and moved into the send_queue table for later sync to backend. + return nodeStore.moveToSendQueue(localNode, backendNode) }) - .catch(async () => { + .catch(async e => { // Not found in IndexedDB - OK to just insert since it only exists in backend. return nodeStore.add([backendNode]) }) } catch (e) { console.error(e) - } finally { - this.pushMessage({ op: SYNC_HANDLED, count: 1 }) } - }//}}} - 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(BATCH_SIZE) - if (nodesToSend.length === 0) - break - console.debug(`Sending ${nodesToSend.length} node(s) to server`) - - 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) - await nodeStore.sendQueue.delete(keys) - this.pushMessage({ op: SYNC_HANDLED, count: nodesToSend.length }) - - } catch (e) { - console.trace(e) - alert(e) - return - } - } - }//}}} -} - -export class SyncProgress extends Component { - constructor() {//{{{ - super() - - this.forceUpdateRequest = null - - 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) - }//}}} - componentDidUpdate() {//{{{ - if (!this.state.syncedDone) { - if (this.forceUpdateRequest !== null) - clearTimeout(this.forceUpdateRequest) - this.forceUpdateRequest = setTimeout( - () => { - this.forceUpdateRequest = null - this.forceUpdate() - }, - 50 - ) - } - }//}}} - progressHandler(msg) {//{{{ - switch (msg.op) { - case SYNC_COUNT: - this.setState({ nodesToSync: msg.count }) - break - - case SYNC_HANDLED: - 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 c39d7af..f3abdc5 100644 --- a/static/less/notes2.less +++ b/static/less/notes2.less @@ -10,7 +10,6 @@ html { display: grid; grid-template-areas: "tree crumbs" - "tree sync" "tree name" "tree content" //"tree checklist" @@ -23,7 +22,6 @@ html { @media only screen and (max-width: 600px) { grid-template-areas: "crumbs" - "sync" "name" "content" //"checklist" @@ -112,61 +110,6 @@ 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; @@ -208,11 +151,11 @@ html { } #name { - color: #333; + color: @color3; font-weight: bold; text-align: center; font-size: 1.15em; - margin-top: 0px; + margin-top: 32px; margin-bottom: 16px; } diff --git a/user.go b/user.go index b1c2abf..fcd1cb9 100644 --- a/user.go +++ b/user.go @@ -5,23 +5,20 @@ import ( "github.com/golang-jwt/jwt/v5" ) -type UserSession struct { - UserID int - Username string - Password string - Name string - ClientUUID string +type User struct { + ID int + Username string + Password string + Name string } -func NewUser(claims jwt.MapClaims) (u UserSession) { +func NewUser(claims jwt.MapClaims) (u User) { uid, _ := claims["uid"].(float64) name, _ := claims["name"].(string) username, _ := claims["login"].(string) - clientUUID, _ := claims["cid"].(string) - u.UserID = int(uid) + u.ID = int(uid) u.Username = username u.Name = name - u.ClientUUID = clientUUID return }