diff --git a/web/package.json b/web/package.json index 7e2294b..2076c5c 100644 --- a/web/package.json +++ b/web/package.json @@ -15,6 +15,7 @@ "axios": "^1.7.4", "element-tiptap": "1.27.1", "element-ui": "^2.15.13", + "idb": "^8.0.1", "lodash": "^4.17.21", "moment": "^2.29.4", "prosemirror-tables": "^1.1.1", diff --git a/web/src/components/DiaryEditor.vue b/web/src/components/DiaryEditor.vue index 6f0dec9..59298d4 100644 --- a/web/src/components/DiaryEditor.vue +++ b/web/src/components/DiaryEditor.vue @@ -1,15 +1,16 @@ diff --git a/web/src/services/note.ts b/web/src/services/note.ts new file mode 100644 index 0000000..049c80a --- /dev/null +++ b/web/src/services/note.ts @@ -0,0 +1,132 @@ +// services/api.ts +import axios from '../axiosConfig.js'; + +import { openDB, DBSchema, IDBPDatabase } from 'idb'; +import type { DiaryEntry, QueuedRequest } from '../types.ts'; + +interface MyDB extends DBSchema { + notes: { + key: string; + value: DiaryEntry; + indexes: { 'noteId': string } + } +} + +let db: IDBPDatabase | null = null; + +const DB_NAME = 'logbook-db'; + +const openDatabase = async () => { + if (db) return db; + db = await openDB(DB_NAME, 1, { + upgrade(db) { + db.createObjectStore('notes', { keyPath: 'noteId' }) + } + }); + return db; +} + +const requestQueue: QueuedRequest[] = []; +let isSyncing = false; + +// Function to process request queue +const processQueue = async () => { + if (isSyncing || !navigator.onLine) { + return; // syncing process or offline, wait + } + + isSyncing = true; + + while (requestQueue.length > 0) { + const request = requestQueue.shift(); + if (!request) continue; + + try { + const response = await axios({ + url: request.url, + method: request.method, + data: request.data + }) + if (response.status >= 200 && response.status < 300) { + request.resolve && request.resolve(response.data) + } else { + console.error('request failed with status: ' + response.status) + requestQueue.unshift(request); // push back into the queue + request.reject && request.reject('request failed'); + break; + } + } + catch (error) { + console.error("request failed", error) + requestQueue.unshift(request); + request.reject && request.reject('request failed'); + break; + } + } + + isSyncing = false; + if (requestQueue.length > 0) { + // if there are requests left, retry after a short delay + setTimeout(processQueue, 1000) + } +}; + + +const enqueueRequest = (url: string, method: 'PUT' | 'GET', data: any): Promise => { + return new Promise((resolve, reject) => { + requestQueue.push({ url, method, data, resolve, reject }); + processQueue(); + }); + +}; + +const apiRequest = async (url: string, method: 'PUT' | 'GET', data: any) => { + if (navigator.onLine) { + // if online go straight to the network + try { + const response = await axios({ url, method, data }); + return response.data + } + catch (error) { + throw error + } + } + else { + return enqueueRequest(url, method, data); + } +}; + + +const saveNote = async (note: DiaryEntry) => { + const db = await openDatabase() + console.log("saving note", note); + await db.put('notes', note); + + // if user online, immediately save to the server + return apiRequest(`/api/diary/${note.noteId}`, 'PUT', note); +}; + +const fetchNote = async (noteId: string): Promise => { + console.log("fetching note", noteId); + const db = await openDatabase(); + let cachedNote = await db.get('notes', noteId); + if (navigator.onLine) { + try { + const response = await apiRequest(`/api/diary/${noteId}`, 'GET', null); + if (response) { + cachedNote = response; + db.put('notes', response); + } + } catch (error) { + console.log("fetch note failed, using local", error); + } + } + return cachedNote +}; + +window.addEventListener('online', () => { + processQueue(); //process the queue on network status change +}); + +export { saveNote, fetchNote }; + diff --git a/web/src/types.ts b/web/src/types.ts new file mode 100644 index 0000000..80eb131 --- /dev/null +++ b/web/src/types.ts @@ -0,0 +1,12 @@ +// types.ts +export interface DiaryEntry { + noteId: string; + note: string; +} +export interface QueuedRequest { + url: string; + method: 'PUT' | 'GET'; + data: any; + resolve?: (value: any) => void; + reject?: (reason?: any) => void; +} diff --git a/web/yarn.lock b/web/yarn.lock index f1af09c..dfa4b8b 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -1449,6 +1449,11 @@ iconv-lite@^0.4.24: dependencies: safer-buffer ">= 2.1.2 < 3" +idb@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/idb/-/idb-8.0.1.tgz#15e8be673413d6caf4beefacf086c8902d785e1e" + integrity sha512-EkBCzUZSdhJV8PxMSbeEV//xguVKZu9hZZulM+2gHXI0t2hGVU3eYE6/XnH77DS6FM2FY8wl17aDcu9vXpvLWQ== + ignore@^4.0.6: version "4.0.6" resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..08377cd --- /dev/null +++ b/yarn.lock @@ -0,0 +1,8 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +idb@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/idb/-/idb-8.0.1.tgz#15e8be673413d6caf4beefacf086c8902d785e1e" + integrity sha512-EkBCzUZSdhJV8PxMSbeEV//xguVKZu9hZZulM+2gHXI0t2hGVU3eYE6/XnH77DS6FM2FY8wl17aDcu9vXpvLWQ==