feat: initial work on stores
Some checks failed
TrafficCue CI / check (push) Failing after 1m5s
TrafficCue CI / build (push) Successful in 1m48s
TrafficCue CI / build-android (push) Successful in 14m3s

This commit is contained in:
2025-09-22 20:14:28 +02:00
parent c071b5d7a9
commit acd6dc9682
3 changed files with 165 additions and 1 deletions

View File

@ -6,7 +6,7 @@ export const SEARCH_SERVER = "https://photon.komoot.io/";
export const OVERPASS_SERVER = "https://overpass-api.de/api/interpreter"; export const OVERPASS_SERVER = "https://overpass-api.de/api/interpreter";
export const LNV_SERVER = export const LNV_SERVER =
location.hostname == "localhost" && location.protocol == "http:" location.hostname == "localhost" && location.protocol == "http:"
? "https://staging-trafficcue-api.picoscratch.de/api" ? "http://localhost:3000/api"
: location.hostname.includes("staging") : location.hostname.includes("staging")
? "https://staging-trafficcue-api.picoscratch.de/api" ? "https://staging-trafficcue-api.picoscratch.de/api"
: "https://trafficcue-api.picoscratch.de/api"; : "https://trafficcue-api.picoscratch.de/api";

View File

@ -1,5 +1,6 @@
import { LNV_SERVER } from "./hosts"; import { LNV_SERVER } from "./hosts";
import type { OIDCUser } from "./oidc"; import type { OIDCUser } from "./oidc";
import type { Store } from "./stores";
export type Capabilities = ( export type Capabilities = (
| "auth" | "auth"
@ -273,3 +274,32 @@ export async function postHazard(location: WorldLocation, type: string) {
} }
return await res.json(); return await res.json();
} }
export function getStores(): Promise<Store[]> {
return authFetch(LNV_SERVER + "/stores").then((res) => res.json());
}
export function getStore(name: string): Promise<Store | null> {
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());
}

134
src/lib/services/stores.ts Normal file
View File

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