94 lines
3.1 KiB
TypeScript
94 lines
3.1 KiB
TypeScript
const SAVE_THROTTLE_MS = 2000;
|
|
const TOKEN_KEY = '__storage_token__';
|
|
const BASE_URL = '/storage/api';
|
|
|
|
const pendingSaves = new Map<string, ReturnType<typeof setTimeout>>();
|
|
|
|
// ─── Auth ────────────────────────────────────────────────────
|
|
|
|
let token: string | null = localStorage.getItem(TOKEN_KEY);
|
|
|
|
export const isLoggedIn = () => token !== null;
|
|
|
|
export const login = async (username: string, password: string): Promise<void> => {
|
|
const res = await fetch(`${BASE_URL}/login`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ username, password }),
|
|
});
|
|
const json = await res.json();
|
|
if (!res.ok) throw new Error(json.error ?? 'Login failed');
|
|
token = json.token;
|
|
localStorage.setItem(TOKEN_KEY, json.token);
|
|
};
|
|
|
|
export const logout = () => {
|
|
token = null;
|
|
localStorage.removeItem(TOKEN_KEY);
|
|
};
|
|
|
|
// ─── Storage ─────────────────────────────────────────────────
|
|
|
|
export const loadObject = async <T>(key: string, defaultObject: T): Promise<T> => {
|
|
let localObject: Partial<T> = {};
|
|
try {
|
|
const json = localStorage.getItem(key);
|
|
if (json) localObject = JSON.parse(json);
|
|
} catch { }
|
|
|
|
let remoteObject: Partial<T> = {};
|
|
if (token) {
|
|
try {
|
|
const res = await fetch(`${BASE_URL}/storage/${key}`, {
|
|
headers: { 'Authorization': `Bearer ${token}` },
|
|
});
|
|
if (res.ok) remoteObject = await res.json();
|
|
} catch { }
|
|
}
|
|
|
|
return { ...defaultObject, ...localObject, ...remoteObject };
|
|
}
|
|
|
|
export const saveObject = <T>(key: string, obj: T) => {
|
|
const saveData = JSON.stringify(obj);
|
|
|
|
try {
|
|
localStorage.setItem(key, saveData);
|
|
} catch { }
|
|
|
|
if (token) {
|
|
const existing = pendingSaves.get(key);
|
|
if (existing) clearTimeout(existing);
|
|
pendingSaves.set(key, setTimeout(() => {
|
|
pendingSaves.delete(key);
|
|
saveRemote(key, saveData);
|
|
}, SAVE_THROTTLE_MS));
|
|
}
|
|
};
|
|
|
|
const saveRemote = async (key: string, saveData: string) => {
|
|
try {
|
|
await fetch(`${BASE_URL}/storage/${key}`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: saveData,
|
|
});
|
|
} catch { }
|
|
};
|
|
|
|
// ─── Compression utils ───────────────────────────────────────
|
|
|
|
export const compressBlob = async (blob: Blob | string): Promise<Blob> => {
|
|
if (typeof blob === 'string') blob = new Blob([blob]);
|
|
const cs = new CompressionStream("gzip");
|
|
return await new Response(blob.stream().pipeThrough(cs)).blob();
|
|
}
|
|
|
|
export const decompressBlob = async (blob: Blob): Promise<Blob> => {
|
|
const ds = new DecompressionStream("gzip");
|
|
return await new Response(blob.stream().pipeThrough(ds)).blob();
|
|
}
|