diff --git a/bun.lock b/bun.lock index 4853422..99089d5 100644 --- a/bun.lock +++ b/bun.lock @@ -13,6 +13,7 @@ "eslint": "^9.29.0", "eslint-plugin-svelte": "^3.9.3", "globals": "^16.2.0", + "idb": "^8.0.3", "jsonwebtoken": "^9.0.2", "libsodium-wrappers": "^0.7.15", "opening_hours": "^3.8.0", @@ -586,6 +587,8 @@ "i18next-browser-languagedetector": ["i18next-browser-languagedetector@6.1.8", "", { "dependencies": { "@babel/runtime": "^7.19.0" } }, "sha512-Svm+MduCElO0Meqpj1kJAriTC6OhI41VhlT/A0UPjGoPZBhAHIaGE5EfsHlTpgdH09UVX7rcc72pSDDBeKSQQA=="], + "idb": ["idb@8.0.3", "", {}, "sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg=="], + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], diff --git a/package.json b/package.json index 47fe1e8..91abfe9 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "eslint": "^9.29.0", "eslint-plugin-svelte": "^3.9.3", "globals": "^16.2.0", + "idb": "^8.0.3", "jsonwebtoken": "^9.0.2", "libsodium-wrappers": "^0.7.15", "opening_hours": "^3.8.0", diff --git a/src/lib/components/lnv/sidebar/settings/DeveloperSidebar.svelte b/src/lib/components/lnv/sidebar/settings/DeveloperSidebar.svelte index 9acb1ab..f4f87ff 100644 --- a/src/lib/components/lnv/sidebar/settings/DeveloperSidebar.svelte +++ b/src/lib/components/lnv/sidebar/settings/DeveloperSidebar.svelte @@ -1,7 +1,9 @@ @@ -63,6 +66,33 @@ /> +
+

Stores

+ { + syncStores(); + }} + /> + { + const name = prompt("Store Name?"); + if (!name) return; + const type = prompt("Store Type? (route, location, vehicle)"); + if (type !== "route" && type !== "location" && type !== "vehicle") { + alert("Invalid type"); + return; + } + const data = prompt("Data? (JSON)"); + if (!data) return; + await updateStore({ name, type }, JSON.parse(data)); + }} + /> +
+

Other

("tc", 1, { + upgrade(db, _oldVersion, _newVersion, _transaction, _event) { + if(!db.objectStoreNames.contains("stores")) { + const store = db.createObjectStore("stores", { keyPath: "id" }); + store.createIndex("by-type", "type"); + store.createIndex("by-owner", "owner_id"); + store.createIndex("by-type-and-owner", ["type", "owner_id"]); + store.createIndex("by-name-and-type", ["name", "type"]); + } + if(!db.objectStoreNames.contains("changes")) { + db.createObjectStore("changes", { keyPath: "id" }); + } + } +}) + +export async function syncStores() { + const changes = await Promise.all(await db.getAll("changes").then(changes => changes.map(async change => { + const storeData = await db.get("stores", change.id); + return { id: change.id, operation: change.operation, data: storeData?.data || null, modified_at: storeData?.modified_at || new Date().toISOString() }; + }))); + const stores = await db.getAll("stores").then(stores => stores.map(store => ({ id: store.id, modified_at: store.modified_at }))); + const res = await authFetch(LNV_SERVER + "/stores/sync", { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ + changes, + stores + }) + }) + if(!res.ok) { + console.error("[STORES] [syncStores] Failed to sync stores:", await res.text()); return; } - // Figure out which is newer - const localModified = new Date((JSON.parse(localStorage.getItem(remoteStore.type + "/" + remoteStore.name + "-meta") ?? "{\"modified_at\":\"1970-01-01\"}")).modified_at); - const remoteModified = new Date(remoteStore.modified_at); - if(localModified < remoteModified) { - // Remote is newer - if(remoteStore.data == null) { - // Remote was deleted, delete local copy too - localStorage.removeItem(remoteStore.type + "/" + remoteStore.name); - localStorage.removeItem(remoteStore.type + "/" + remoteStore.name + "-meta"); - return; + const data = await res.json() as { changes: Store[]; }; + const tx = db.transaction(["stores", "changes"], "readwrite"); + // Apply all changes the server sent us + for(const store of data.changes) { + if(store.data === null) { + await tx.objectStore("stores").delete(store.id); } else { - localStorage.setItem(remoteStore.type + "/" + remoteStore.name, remoteStore.data); - localStorage.setItem(remoteStore.type + "/" + remoteStore.name + "-meta", JSON.stringify({ - name: remoteStore.name, - type: remoteStore.type, - privacy: remoteStore.privacy, - modified_at: remoteStore.modified_at - })); - } - } else if(localModified > remoteModified) { - // Local is newer, upload it - const localStore = JSON.parse(localStorage.getItem(remoteStore.type + "/" + remoteStore.name) ?? "null"); - if(localStore != null) { - await putStore(remoteStore.name, localStore, remoteStore.type, remoteStore.privacy); - } - } else { - // Same timestamp, do nothing - } -} - -export async function deleteStore(info: StoreInfo) { - const store = await serverStore(info); - store.current = null; -} - -export async function serverStore(info: StoreInfo) { - // await syncStore(await getStore(info.name)); - const value = localStorage.getItem(info.type + "/" + info.name); - const state = $state({ - current: value ? JSON.parse(value) : null, - }); - - $effect(() => { - if(state.current) { - localStorage.setItem(info.type + "/" + info.name, JSON.stringify(state.current)); - localStorage.setItem(info.type + "/" + info.name + "-meta", JSON.stringify({ - name: info.name, - type: info.type, - privacy: info.privacy, - modified_at: new Date().toISOString() - })); - putStore(info.name, state.current, info.type, info.privacy); - } - }); - - return state; -} - -export async function serverStores(type: StoreType) { - const stores = await getStores(); - const filtered = stores.filter(store => store.type === type); - - const state = $state({ - list: await Promise.all(filtered.map(async store => ({ - name: store.name, - privacy: store.privacy, - modified_at: store.modified_at, - ...await serverStore({ name: store.name, type: store.type, privacy: store.privacy }) - }))), - }); - - // TODO: add new stores when they are created - - return state; -} - -export async function syncAllStores() { - const stores = await getStores(); - // Upload local stores that don't exist remotely - /* - - if(remoteStore == null) { - // Remote has never heard of this store, upload local version - const localStore = JSON.parse(localStorage.getItem(remoteStore.type + "/" + remoteStore.name) ?? "null"); - if(localStore != null) { - await putStore(remoteStore.name, localStore, remoteStore.type, remoteStore.privacy); - return; - } else { - // We don't have it locally either, nothing to do - return; + await tx.objectStore("stores").put(store); } } - */ - const localKeys = Object.keys(localStorage).filter(key => key.endsWith("-meta")).map(key => key.slice(0, -5)); - for(const key of localKeys) { - const [type, name] = key.split("/"); - if(!stores.find(store => store.name === name && store.type === type)) { - // This local store doesn't exist remotely, upload it - const localStore = JSON.parse(localStorage.getItem(key) ?? "null"); - const localMeta = JSON.parse(localStorage.getItem(key + "-meta") ?? "{\"privacy\":\"private\"}"); - if(localStore != null) { - console.log(`[STORES] [syncAllStores] Uploading local store ${name} of type ${type}, privacy ${localMeta.privacy} to server`); - await putStore(name, localStore, type as StoreType, localMeta.privacy as StorePrivacy); - } - } - } - - for(const store of stores) { - console.log(`[STORES] [syncAllStores] Syncing store ${store.name} of type ${store.type}`); - await syncStore(store); - } + // Delete all changes + await tx.objectStore("changes").clear(); + await tx.done; +} + +async function createStore(info: StoreInfo, data: object) { + const id = crypto.randomUUID(); + const store: Store = { + id, + name: info.name, + type: info.type, + owner_id: "", // TODO + data: JSON.stringify(data), + modified_at: new Date().toISOString() + }; + const tx = db.transaction(["stores", "changes"], "readwrite"); + await tx.objectStore("stores").add(store); + await tx.objectStore("changes").add({ id, operation: "create" }); + await tx.done; + return store; +} + +export async function updateStore(info: StoreInfo, data: object) { + const store = await db.getFromIndex("stores", "by-name-and-type", [info.name, info.type]); + if (!store) { + return await createStore(info, data); + } + // Update the store data + store.data = JSON.stringify(data); + store.modified_at = new Date().toISOString(); + const tx = db.transaction(["stores", "changes"], "readwrite"); + await tx.objectStore("stores").put(store); + await tx.objectStore("changes").add({ id: store.id, operation: "update" }); + await tx.done; + return store; }