diff --git a/src/lib/services/hosts.ts b/src/lib/services/hosts.ts index 4d4008d..ab1f60c 100644 --- a/src/lib/services/hosts.ts +++ b/src/lib/services/hosts.ts @@ -6,7 +6,7 @@ export const SEARCH_SERVER = "https://photon.komoot.io/"; export const OVERPASS_SERVER = "https://overpass-api.de/api/interpreter"; export const LNV_SERVER = location.hostname == "localhost" && location.protocol == "http:" - ? "https://staging-trafficcue-api.picoscratch.de/api" + ? "http://localhost:3000/api" : location.hostname.includes("staging") ? "https://staging-trafficcue-api.picoscratch.de/api" : "https://trafficcue-api.picoscratch.de/api"; diff --git a/src/lib/services/lnv.ts b/src/lib/services/lnv.ts index d197fb5..e9439f6 100644 --- a/src/lib/services/lnv.ts +++ b/src/lib/services/lnv.ts @@ -1,5 +1,6 @@ import { LNV_SERVER } from "./hosts"; import type { OIDCUser } from "./oidc"; +import type { Store } from "./stores"; export type Capabilities = ( | "auth" @@ -273,3 +274,32 @@ export async function postHazard(location: WorldLocation, type: string) { } return await res.json(); } + +export function getStores(): Promise { + return authFetch(LNV_SERVER + "/stores").then((res) => res.json()); +} + +export function getStore(name: string): Promise { + return authFetch(LNV_SERVER + "/store/" + name).then((res) => res.json()); +} + +export function putStore(name: string, data: object, type: string, privacy: string) { + return authFetch(LNV_SERVER + "/store", { + method: "PUT", + body: JSON.stringify({ + name, + data: JSON.stringify(data), + type, + privacy, + }), + }); +} + +export function deleteStore(name: string) { + return authFetch(LNV_SERVER + "/store", { + method: "DELETE", + body: JSON.stringify({ + name, + }), + }).then((res) => res.text()); +} diff --git a/src/lib/services/stores.ts b/src/lib/services/stores.ts new file mode 100644 index 0000000..aa78b5e --- /dev/null +++ b/src/lib/services/stores.ts @@ -0,0 +1,134 @@ +import { getStores, putStore } from "./lnv"; + +export interface Store { + name: string; + data: string; + type: StoreType; + privacy: StorePrivacy; + modified_at: string; +} +export type StoreType = "route" | "location" | "vehicle"; +export type StorePrivacy = "public" | "friends" | "private"; +export interface StoreInfo { + name: string; + type: StoreType; + privacy: StorePrivacy; +} + +export async function syncStore(remoteStore: Store) { + if(!localStorage.getItem("lnv-token")) { + console.warn(`[STORES] [syncStore] No token, skipping sync of ${remoteStore.name}`); + 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; + } 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; + } + } + */ + 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); + } +}