264 lines
6.8 KiB
TypeScript
264 lines
6.8 KiB
TypeScript
// import { getStores, putStore } from "./lnv";
|
|
|
|
import { openDB, type DBSchema, type IDBPDatabase } from "idb";
|
|
import { authFetch, hasCapability, ping } from "./lnv";
|
|
import { LNV_SERVER } from "./hosts";
|
|
|
|
export interface Store {
|
|
id: string; // UUID
|
|
name: string;
|
|
type: StoreType;
|
|
owner_id: string; // User ID
|
|
data: string; // JSON string
|
|
modified_at: string; // ISO date string
|
|
}
|
|
export type StoreType = "route" | "location" | "vehicle";
|
|
export interface StoreInfo {
|
|
name: string;
|
|
type: StoreType;
|
|
}
|
|
export interface WrappedValue<T> {
|
|
current: T;
|
|
}
|
|
export type StoreValue<T> = {
|
|
data: T;
|
|
} & Omit<Store, "data">;
|
|
|
|
interface TCDB extends DBSchema {
|
|
stores: {
|
|
key: string;
|
|
value: Store;
|
|
indexes: {
|
|
"by-name-and-type": [string, StoreType];
|
|
"by-type": StoreType;
|
|
"by-owner": string;
|
|
"by-type-and-owner": [StoreType, string];
|
|
};
|
|
};
|
|
changes: {
|
|
key: string;
|
|
value: {
|
|
id: string; // UUID of store
|
|
operation: "create" | "update" | "delete";
|
|
};
|
|
};
|
|
}
|
|
|
|
export async function getDB(): Promise<IDBPDatabase<TCDB>> {
|
|
if (_db) return _db;
|
|
_db = await openDB<TCDB>("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" });
|
|
}
|
|
},
|
|
});
|
|
return _db;
|
|
}
|
|
|
|
let _db: IDBPDatabase<TCDB>;
|
|
|
|
const eventTarget = new EventTarget();
|
|
|
|
export async function trySync() {
|
|
const pingResult = await ping();
|
|
if (!pingResult) {
|
|
console.warn(
|
|
"[STORES] [trySync] LNV server is not reachable, skipping sync",
|
|
);
|
|
return { success: false, message: "LNV server is not reachable" };
|
|
}
|
|
await syncStores();
|
|
return { success: true };
|
|
}
|
|
|
|
export async function syncStores() {
|
|
if (!(await hasCapability("stores"))) {
|
|
return;
|
|
}
|
|
if (!(localStorage.getItem("lnv-token"))) {
|
|
return;
|
|
}
|
|
const db = await getDB();
|
|
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(),
|
|
type: storeData?.type || "location",
|
|
name: storeData?.name || "",
|
|
};
|
|
}),
|
|
),
|
|
);
|
|
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;
|
|
}
|
|
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 {
|
|
await tx.objectStore("stores").put(store);
|
|
}
|
|
eventTarget.dispatchEvent(
|
|
new CustomEvent("store-updated", { detail: store }),
|
|
);
|
|
}
|
|
// Delete all changes
|
|
await tx.objectStore("changes").clear();
|
|
await tx.done;
|
|
}
|
|
|
|
async function createStore(info: StoreInfo, data: object) {
|
|
const db = await getDB();
|
|
const id = crypto.randomUUID();
|
|
const store: Store = {
|
|
id,
|
|
name: info.name, // TODO: do we need a 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;
|
|
eventTarget.dispatchEvent(
|
|
new CustomEvent("store-updated", { detail: store }),
|
|
);
|
|
await trySync();
|
|
return store;
|
|
}
|
|
|
|
export async function updateStore(info: StoreInfo, data: object | null) {
|
|
const db = await getDB();
|
|
const store = await db.getFromIndex("stores", "by-name-and-type", [
|
|
info.name,
|
|
info.type,
|
|
]);
|
|
if (!store) {
|
|
if (data === null) {
|
|
console.warn(
|
|
"[STORES] [updateStore] Tried to delete non-existing store",
|
|
info,
|
|
);
|
|
return;
|
|
}
|
|
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;
|
|
eventTarget.dispatchEvent(
|
|
new CustomEvent("store-updated", { detail: store }),
|
|
);
|
|
await trySync();
|
|
return store;
|
|
}
|
|
|
|
export async function hasStore(info: StoreInfo) {
|
|
const db = await getDB();
|
|
const store = await db.getFromIndex("stores", "by-name-and-type", [
|
|
info.name,
|
|
info.type,
|
|
]);
|
|
return store !== undefined;
|
|
}
|
|
|
|
// export async function store<T extends object>(info: StoreInfo) {
|
|
// const store = await db.getFromIndex("stores", "by-name-and-type", [info.name, info.type]);
|
|
// if (!store) {
|
|
// return null;
|
|
// }
|
|
// const state = $state<T>(JSON.parse(store.data) as T);
|
|
// $effect(() => {
|
|
// updateStore(info, state);
|
|
// })
|
|
// eventTarget.addEventListener("store-updated", (event) => {
|
|
// const customEvent = event as CustomEvent;
|
|
// if(customEvent.detail.id === store.id) {
|
|
// const updatedStore = customEvent.detail as Store;
|
|
// Object.assign(state, JSON.parse(updatedStore.data));
|
|
// }
|
|
// });
|
|
// return state;
|
|
// }
|
|
|
|
export function stores<T extends object>(
|
|
type: StoreType,
|
|
): WrappedValue<StoreValue<T>[]> {
|
|
const state = $state<StoreValue<T>[]>([]);
|
|
eventTarget.addEventListener("store-updated", async (event) => {
|
|
const customEvent = event as CustomEvent;
|
|
const updatedStore = customEvent.detail as Store;
|
|
if (updatedStore.type === type) {
|
|
const db = await getDB();
|
|
const stores = await db.getAllFromIndex("stores", "by-type", type);
|
|
state.splice(
|
|
0,
|
|
state.length,
|
|
...stores
|
|
.map((store) => ({ ...store, data: JSON.parse(store.data) as T }))
|
|
.filter((store) => store.data !== null),
|
|
);
|
|
}
|
|
});
|
|
(async () => {
|
|
const db = await getDB();
|
|
const stores = await db.getAllFromIndex("stores", "by-type", type);
|
|
state.splice(
|
|
0,
|
|
state.length,
|
|
...stores
|
|
.map((store) => ({ ...store, data: JSON.parse(store.data) as T }))
|
|
.filter((store) => store.data !== null),
|
|
);
|
|
})();
|
|
return {
|
|
get current() {
|
|
return state;
|
|
},
|
|
set current(newValue: StoreValue<T>[]) {
|
|
state.splice(0, state.length, ...newValue);
|
|
},
|
|
};
|
|
}
|