GoonCord/api/storage/index.ts
2025-12-10 21:43:06 +01:00

190 lines
5.8 KiB
TypeScript

import { Emitter } from "@core/vendetta/Emitter";
import { Observable, ObserverOptions } from "@gullerya/object-observer";
import { fileExists, readFile, removeFile, writeFile } from "@lib/api/native/fs";
import { debounce } from "es-toolkit";
const storageInitErrorSymbol = Symbol.for("bunny.storage.initError");
const storagePromiseSymbol = Symbol.for("bunny.storage.promise");
const _loadedStorage = {} as Record<string, any>;
function createFileBackend<T extends object>(filePath: string) {
const write = debounce((data: T) => {
writeFile(filePath, JSON.stringify(data));
}, 500);
return {
get: async () => {
try {
return JSON.parse(await readFile(filePath)) as T;
} catch (e) {
throw new Error(`Failed to parse storage from '${filePath}'`, { cause: e });
}
},
set: async (data: T) => {
if (!data || typeof data !== "object") {
throw new Error("data needs to be an object");
}
write(data);
},
exists: async () => {
return await fileExists(filePath);
}
};
}
export function useObservable(observables: Observable[], opts?: ObserverOptions) {
if (observables.some((o: any) => o?.[storageInitErrorSymbol])) throw new Error(
"An error occured while initializing the storage",
);
if (observables.some(o => !Observable.isObservable(o))) {
throw new Error("Argument passed isn't an Observable");
}
const [, forceUpdate] = React.useReducer(n => ~n, 0);
React.useEffect(() => {
const listener = () => forceUpdate();
observables.forEach(o => Observable.observe(o, listener, opts));
return () => {
observables.forEach(o => Observable.unobserve(o, listener));
};
}, []);
}
export async function updateStorage<T extends object = {}>(path: string, value: T): Promise<void> {
_loadedStorage[path] = value;
createFileBackend<T>(path).set(value);
}
export function createStorageAndCallback<T extends object = {}>(
path: string,
cb: (proxy: T) => void,
{
dflt = {} as T,
nullIfEmpty = false
} = {}
) {
let emitter: Emitter;
const callback = (data: any) => {
const proxy = new Proxy(Observable.from(data), {
get(target, prop, receiver) {
if (prop === Symbol.for("vendetta.storage.emitter")) {
if (emitter) return emitter;
emitter = new Emitter();
Observable.observe(target, changes => {
for (const change of changes) {
emitter.emit(change.type !== "delete" ? "SET" : "DEL", {
path: change.path,
value: change.value
});
}
});
return emitter;
}
return Reflect.get(target, prop, receiver);
},
});
const handler = () => backend.set(proxy);
Observable.observe(proxy, handler);
cb(proxy);
};
const backend = createFileBackend<T>(path);
if (_loadedStorage[path]) {
callback(_loadedStorage[path]);
} else {
backend.exists().then(async exists => {
if (!exists) {
if (nullIfEmpty) {
callback(_loadedStorage[path] = null);
} else {
_loadedStorage[path] = dflt;
await backend.set(dflt);
callback(dflt);
}
} else {
callback(_loadedStorage[path] = await backend.get());
}
});
}
}
type StorageOptions<T extends object> = Parameters<typeof createStorageAndCallback<T>>[2];
export async function createStorageAsync<T extends object = {}>(
path: string,
opts: StorageOptions<T> = {}
): Promise<T> {
return new Promise(r => createStorageAndCallback(path, r, opts));
}
export const createStorage = <T extends object = {}>(
path: string,
opts: StorageOptions<T> = {}
): T => {
const promise = new Promise(r => resolvePromise = r);
let awaited: any, resolved: boolean, error: any, resolvePromise: (val?: unknown) => void;
createStorageAndCallback(path, proxy => {
awaited = proxy;
resolved = true;
resolvePromise();
}, opts);
const check = () => {
if (resolved) return true;
throw new Error(`Attempted to access storage without initializing: ${path}`);
};
return new Proxy({} as any, {
...Object.fromEntries(
Object.getOwnPropertyNames(Reflect)
.map(k => [k, (t: T, ...a: any[]) => {
// @ts-expect-error
return check() && Reflect[k](awaited, ...a);
}])
),
get(target, prop, recv) {
if (prop === storageInitErrorSymbol) return error;
if (prop === storagePromiseSymbol) return promise;
return check() && Reflect.get(awaited ?? target, prop, recv);
},
});
};
export async function preloadStorageIfExists(path: string): Promise<boolean> {
if (_loadedStorage[path]) return true;
const backend = createFileBackend(path);
if (await backend.exists()) {
_loadedStorage[path] = await backend.get();
return true;
}
return false;
}
export async function purgeStorage(path: string) {
await removeFile(path);
delete _loadedStorage[path];
}
export function awaitStorage(...proxies: any[]) {
return Promise.all(proxies.map(proxy => proxy[storagePromiseSymbol]));
}
/** @internal */
export function getPreloadedStorage<T extends object = {}>(path: string): T | undefined {
return _loadedStorage[path];
}