1
0
Fork 0
tsgames/src/common/storage.ts

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