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;
}