feat(storage): port storage api from dev
move old storage api as legacy
This commit is contained in:
parent
f2f1f6ac73
commit
122c3a11e9
30 changed files with 433 additions and 446 deletions
|
|
@ -32,6 +32,7 @@
|
|||
"yargs-parser": "^21.1.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@gullerya/object-observer": "^6.1.3",
|
||||
"@shopify/react-native-skia": "^1.3.8",
|
||||
"@tanstack/react-query": "^5.51.16",
|
||||
"@types/chroma-js": "~2.4.4",
|
||||
|
|
|
|||
8
pnpm-lock.yaml
generated
8
pnpm-lock.yaml
generated
|
|
@ -8,6 +8,9 @@ importers:
|
|||
|
||||
.:
|
||||
dependencies:
|
||||
'@gullerya/object-observer':
|
||||
specifier: ^6.1.3
|
||||
version: 6.1.3
|
||||
'@shopify/react-native-skia':
|
||||
specifier: ^1.3.8
|
||||
version: 1.3.13(react-native-reanimated@3.15.3(@babel/core@7.25.2)(react-native@0.75.3(@babel/core@7.25.2)(@babel/preset-env@7.25.4(@babel/core@7.25.2))(@types/react@18.2.60)(react@18.3.1)(typescript@5.6.2))(react@18.3.1))(react-native@0.75.3(@babel/core@7.25.2)(@babel/preset-env@7.25.4(@babel/core@7.25.2))(@types/react@18.2.60)(react@18.3.1)(typescript@5.6.2))(react@18.3.1)
|
||||
|
|
@ -960,6 +963,9 @@ packages:
|
|||
'@formatjs/intl-localematcher@0.5.4':
|
||||
resolution: {integrity: sha512-zTwEpWOzZ2CiKcB93BLngUX59hQkuZjT2+SAQEscSm52peDW/getsawMcWF1rGRpMCX6D7nSJA3CzJ8gn13N/g==}
|
||||
|
||||
'@gullerya/object-observer@6.1.3':
|
||||
resolution: {integrity: sha512-omJ9YpdZf/QhrzPDGV+75Gbu1Aw3Q6wP8aqNqg1Hvr3BYFh22I43Pf3MOmQHG9qayDZYFiyhZs1LJbUaO1A/YA==}
|
||||
|
||||
'@hapi/hoek@9.3.0':
|
||||
resolution: {integrity: sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==}
|
||||
|
||||
|
|
@ -4493,6 +4499,8 @@ snapshots:
|
|||
dependencies:
|
||||
tslib: 2.7.0
|
||||
|
||||
'@gullerya/object-observer@6.1.3': {}
|
||||
|
||||
'@hapi/hoek@9.3.0': {}
|
||||
|
||||
'@hapi/topo@5.1.0':
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { VdPluginManager } from "@core/vendetta/plugins";
|
|||
import { findAssetId } from "@lib/api/assets";
|
||||
import { isThemeSupported } from "@lib/api/native/loader";
|
||||
import { after } from "@lib/api/patcher";
|
||||
import { useProxy } from "@lib/api/storage";
|
||||
import { useProxy } from "@core/vendetta/storage";
|
||||
import { installTheme, removeTheme, themes } from "@lib/addons/themes";
|
||||
import { VD_DISCORD_SERVER_ID, HTTP_REGEX_MULTI, VD_PLUGINS_CHANNEL_ID, VD_THEMES_CHANNEL_ID, VD_PROXY_PREFIX } from "@lib/utils/constants";
|
||||
import { lazyDestructure } from "@lib/utils/lazy";
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { CardWrapper } from "@core/ui/components/AddonCard";
|
||||
import { findAssetId } from "@lib/api/assets";
|
||||
import { settings } from "@lib/api/settings";
|
||||
import { useProxy } from "@lib/api/storage";
|
||||
import { useProxy } from "@core/vendetta/storage";
|
||||
import AlertModal, { AlertActionButton } from "@lib/ui/components/wrappers/AlertModal";
|
||||
import isValidHttpUrl from "@lib/utils/isValidHttpUrl";
|
||||
import { lazyDestructure } from "@lib/utils/lazy";
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { Strings } from "@core/i18n";
|
|||
import { findAssetId } from "@lib/api/assets";
|
||||
import { isFontSupported, isThemeSupported } from "@lib/api/native/loader";
|
||||
import { settings } from "@lib/api/settings";
|
||||
import { useProxy } from "@lib/api/storage";
|
||||
import { useProxy } from "@core/vendetta/storage";
|
||||
import { registerSection } from "@ui/settings";
|
||||
import { version } from "bunny-build-info";
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { findAssetId } from "@lib/api/assets";
|
|||
import { connectToDebugger } from "@lib/api/debug";
|
||||
import { getReactDevToolsProp, getReactDevToolsVersion, isLoaderConfigSupported, isReactDevToolsPreloaded, isVendettaLoader } from "@lib/api/native/loader";
|
||||
import { loaderConfig, settings } from "@lib/api/settings";
|
||||
import { useProxy } from "@lib/api/storage";
|
||||
import { useProxy } from "@core/vendetta/storage";
|
||||
import { lazyDestructure } from "@lib/utils/lazy";
|
||||
import { NavigationNative } from "@metro/common";
|
||||
import { Button, LegacyFormText, Stack, TableRow, TableRowGroup, TableSwitchRow, TextInput } from "@metro/common/components";
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { Strings } from "@core/i18n";
|
|||
import { CardWrapper } from "@core/ui/components/AddonCard";
|
||||
import { findAssetId } from "@lib/api/assets";
|
||||
import { BundleUpdaterManager } from "@lib/api/native/modules";
|
||||
import { useProxy } from "@lib/api/storage";
|
||||
import { useProxy } from "@core/vendetta/storage";
|
||||
import { FontDefinition, fonts, selectFont } from "@lib/addons/fonts";
|
||||
import { lazyDestructure } from "@lib/utils/lazy";
|
||||
import { findByProps } from "@metro";
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { formatString, Strings } from "@core/i18n";
|
||||
import { findAssetId } from "@lib/api/assets";
|
||||
import { createProxy, useProxy } from "@lib/api/storage";
|
||||
import { createProxy, useProxy } from "@core/vendetta/storage";
|
||||
import { FontDefinition, fonts, removeFont, saveFont, validateFont } from "@lib/addons/fonts";
|
||||
import { getCurrentTheme } from "@lib/addons/themes";
|
||||
import { safeFetch } from "@lib/utils";
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { Strings } from "@core/i18n";
|
|||
import AddonPage from "@core/ui/components/AddonPage";
|
||||
import FontEditor from "@core/ui/settings/pages/Fonts/FontEditor";
|
||||
import { settings } from "@lib/api/settings";
|
||||
import { useProxy } from "@lib/api/storage";
|
||||
import { useProxy } from "@core/vendetta/storage";
|
||||
import { FontDefinition, fonts } from "@lib/addons/fonts";
|
||||
import { NavigationNative } from "@metro/common";
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { Strings } from "@core/i18n";
|
|||
import Version from "@core/ui/settings/pages/General/Version";
|
||||
import { getDebugInfo } from "@lib/api/debug";
|
||||
import { settings } from "@lib/api/settings";
|
||||
import { useProxy } from "@lib/api/storage";
|
||||
import { useProxy } from "@core/vendetta/storage";
|
||||
import { Stack, TableRowGroup } from "@metro/common/components";
|
||||
import { Platform, ScrollView } from "react-native";
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import About from "@core/ui/settings/pages/General/About";
|
|||
import { findAssetId } from "@lib/api/assets";
|
||||
import { getDebugInfo, toggleSafeMode } from "@lib/api/debug";
|
||||
import { settings } from "@lib/api/settings";
|
||||
import { useProxy } from "@lib/api/storage";
|
||||
import { useProxy } from "@core/vendetta/storage";
|
||||
import { DISCORD_SERVER, GITHUB } from "@lib/utils/constants";
|
||||
import { NavigationNative, url } from "@metro/common";
|
||||
import { Stack, TableRow, TableRowGroup, TableSwitchRow } from "@metro/common/components";
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ import PluginCard from "@core/ui/settings/pages/Plugins/components/PluginCard";
|
|||
import { VdPluginManager } from "@core/vendetta/plugins";
|
||||
import { findAssetId } from "@lib/api/assets";
|
||||
import { settings } from "@lib/api/settings";
|
||||
import { useProxy } from "@lib/api/storage";
|
||||
import { useProxy as useNewProxy } from "@lib/api/storage/new";
|
||||
import { useProxy } from "@core/vendetta/storage";
|
||||
import { useObservable } from "@lib/api/storage";
|
||||
import { showToast } from "@lib/ui/toasts";
|
||||
import { BUNNY_PROXY_PREFIX, VD_PROXY_PREFIX } from "@lib/utils/constants";
|
||||
import { lazyDestructure } from "@lib/utils/lazy";
|
||||
|
|
@ -33,7 +33,7 @@ export interface UnifiedPluginModel {
|
|||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
authors?: Author;
|
||||
authors?: Author[];
|
||||
icon?: string;
|
||||
|
||||
isEnabled(): boolean;
|
||||
|
|
@ -81,7 +81,7 @@ export default function Plugins() {
|
|||
return <PluginPage
|
||||
useItems={() => {
|
||||
useProxy(VdPluginManager.plugins);
|
||||
useNewProxy(pluginSettings);
|
||||
useObservable([pluginSettings]);
|
||||
|
||||
const vdPlugins = Object.values(VdPluginManager.plugins).map(unifyVdPlugin);
|
||||
const bnPlugins = [...registeredPlugins.values()].filter(p => isPluginInstalled(p.id) && !isCorePlugin(p.id)).map(unifyBunnyPlugin);
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { useProxy } from "@lib/api/storage/new";
|
||||
import { disablePlugin, enablePlugin, getId, getPluginSettingsComponent, isPluginEnabled, pluginSettings } from "@lib/addons/plugins";
|
||||
import { BunnyPluginManifest } from "@lib/addons/plugins/types";
|
||||
|
||||
import { UnifiedPluginModel } from "..";
|
||||
import { useObservable } from "@lib/api/storage";
|
||||
|
||||
export default function unifyBunnyPlugin(manifest: BunnyPluginManifest): UnifiedPluginModel {
|
||||
return {
|
||||
|
|
@ -13,7 +13,7 @@ export default function unifyBunnyPlugin(manifest: BunnyPluginManifest): Unified
|
|||
isEnabled: () => isPluginEnabled(getId(manifest)),
|
||||
isInstalled: () => manifest.id in pluginSettings,
|
||||
usePluginState() {
|
||||
useProxy(pluginSettings);
|
||||
useObservable([pluginSettings]);
|
||||
},
|
||||
toggle(start: boolean) {
|
||||
start
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { VdPluginManager, VendettaPlugin } from "@core/vendetta/plugins";
|
||||
import { useProxy } from "@lib/api/storage";
|
||||
import { useProxy } from "@core/vendetta/storage";
|
||||
|
||||
import { UnifiedPluginModel } from "..";
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { formatString, Strings } from "@core/i18n";
|
||||
import { VdPluginManager } from "@core/vendetta/plugins";
|
||||
import { findAssetId } from "@lib/api/assets";
|
||||
import { purgeStorage } from "@lib/api/storage";
|
||||
import { purgeStorage } from "@core/vendetta/storage";
|
||||
import { clipboard } from "@metro/common";
|
||||
import { ActionSheet, ActionSheetRow, Button, TableRow, Text } from "@metro/common/components";
|
||||
import { showConfirmationAlert } from "@core/vendetta/alerts";
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { formatString, Strings } from "@core/i18n";
|
|||
import AddonCard, { CardWrapper } from "@core/ui/components/AddonCard";
|
||||
import { findAssetId } from "@lib/api/assets";
|
||||
import { settings } from "@lib/api/settings";
|
||||
import { useProxy } from "@lib/api/storage";
|
||||
import { useProxy } from "@core/vendetta/storage";
|
||||
import { applyTheme, fetchTheme, removeTheme, selectTheme, Theme, themes } from "@lib/addons/themes";
|
||||
import { clipboard } from "@metro/common";
|
||||
import { showConfirmationAlert } from "@core/vendetta/alerts";
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { formatString, Strings } from "@core/i18n";
|
|||
import AddonPage from "@core/ui/components/AddonPage";
|
||||
import ThemeCard from "@core/ui/settings/pages/Themes/ThemeCard";
|
||||
import { settings } from "@lib/api/settings";
|
||||
import { useProxy } from "@lib/api/storage";
|
||||
import { useProxy } from "@core/vendetta/storage";
|
||||
import { installTheme, Theme, themes } from "@lib/addons/themes";
|
||||
import { Button } from "@metro/common/components";
|
||||
import { Author } from "@lib/addons/types";
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ import * as debug from "@lib/api/debug";
|
|||
import { getVendettaLoaderIdentity, isPyonLoader } from "@lib/api/native/loader";
|
||||
import patcher from "@lib/api/patcher";
|
||||
import { loaderConfig, settings } from "@lib/api/settings";
|
||||
import * as storage from "@lib/api/storage";
|
||||
import { createStorage } from "@lib/api/storage";
|
||||
import * as storage from "@core/vendetta/storage";
|
||||
import { createStorage } from "@core/vendetta/storage";
|
||||
import * as themes from "@lib/addons/themes";
|
||||
import * as utils from "@lib/utils";
|
||||
import { cyrb64Hash } from "@lib/utils/cyrb64";
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Author } from "@lib/addons/types";
|
||||
import { settings } from "@lib/api/settings";
|
||||
import { awaitStorage, createMMKVBackend, createStorage, purgeStorage, wrapSync } from "@lib/api/storage";
|
||||
import { awaitStorage, createMMKVBackend, createStorage, purgeStorage, wrapSync } from "@core/vendetta/storage";
|
||||
import { safeFetch } from "@lib/utils";
|
||||
import { BUNNY_PROXY_PREFIX, VD_PROXY_PREFIX } from "@lib/utils/constants";
|
||||
import { LoggerClass, logger } from "@lib/utils/logger";
|
||||
|
|
|
|||
238
src/core/vendetta/storage.ts
Normal file
238
src/core/vendetta/storage.ts
Normal file
|
|
@ -0,0 +1,238 @@
|
|||
import { Emitter, EmitterEvent, EmitterListener, EmitterListenerData } from "@core/vendetta/Emitter";
|
||||
import { FileManager, MMKVManager } from "@lib/api/native/modules";
|
||||
import { Platform } from "react-native";
|
||||
|
||||
const emitterSymbol = Symbol.for("vendetta.storage.emitter");
|
||||
const syncAwaitSymbol = Symbol.for("vendetta.storage.accessor");
|
||||
|
||||
export function createProxy(target: any = {}): { proxy: any; emitter: Emitter; } {
|
||||
const emitter = new Emitter();
|
||||
|
||||
const childrens = new WeakMap<any, any>();
|
||||
const proxiedChildrenSet = new WeakSet<any>();
|
||||
|
||||
function createProxy(target: any, path: string[]): any {
|
||||
return new Proxy(target, {
|
||||
get(target, prop: string) {
|
||||
if ((prop as unknown) === emitterSymbol) return emitter;
|
||||
|
||||
const newPath = [...path, prop];
|
||||
const value: any = target[prop];
|
||||
|
||||
if (value !== undefined && value !== null) {
|
||||
emitter.emit("GET", {
|
||||
path: newPath,
|
||||
value,
|
||||
});
|
||||
|
||||
if (typeof value === "object") {
|
||||
if (proxiedChildrenSet.has(value)) return value;
|
||||
if (childrens.has(value)) return childrens.get(value);
|
||||
|
||||
const childrenProxy = createProxy(value, newPath);
|
||||
childrens.set(value, childrenProxy);
|
||||
return childrenProxy;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
return value;
|
||||
},
|
||||
|
||||
set(target, prop: string, value) {
|
||||
if (typeof value === "object") {
|
||||
if (childrens.has(value)) {
|
||||
target[prop] = childrens.get(value);
|
||||
} else {
|
||||
const childrenProxy = createProxy(value, [...path, prop]);
|
||||
childrens.set(value, childrenProxy);
|
||||
proxiedChildrenSet.add(value);
|
||||
target[prop] = childrenProxy;
|
||||
}
|
||||
} else {
|
||||
target[prop] = value;
|
||||
}
|
||||
|
||||
emitter.emit("SET", {
|
||||
path: [...path, prop],
|
||||
value: target[prop],
|
||||
});
|
||||
// we do not care about success, if this actually does fail we have other problems
|
||||
return true;
|
||||
},
|
||||
|
||||
deleteProperty(target, prop: string) {
|
||||
const value = typeof target[prop] === "object" ? childrens.get(target[prop])! : target[prop];
|
||||
const success = delete target[prop];
|
||||
if (success)
|
||||
emitter.emit("DEL", {
|
||||
value,
|
||||
path: [...path, prop],
|
||||
});
|
||||
return success;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
proxy: createProxy(target, []),
|
||||
emitter,
|
||||
};
|
||||
}
|
||||
|
||||
export function useProxy<T>(storage: T): T {
|
||||
const emitter = (storage as any)?.[emitterSymbol] as Emitter;
|
||||
if (!emitter) throw new Error("storage?.[emitterSymbol] is undefined");
|
||||
|
||||
const [, forceUpdate] = React.useReducer(n => ~n, 0);
|
||||
|
||||
React.useEffect(() => {
|
||||
const listener: EmitterListener = (event: EmitterEvent, data: EmitterListenerData) => {
|
||||
if (event === "DEL" && data.value === storage) return;
|
||||
forceUpdate();
|
||||
};
|
||||
|
||||
emitter.on("SET", listener);
|
||||
emitter.on("DEL", listener);
|
||||
|
||||
return () => {
|
||||
emitter.off("SET", listener);
|
||||
emitter.off("DEL", listener);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return storage;
|
||||
}
|
||||
|
||||
export async function createStorage<T>(backend: StorageBackend): Promise<Awaited<T>> {
|
||||
const data = await backend.get();
|
||||
const { proxy, emitter } = createProxy(data);
|
||||
|
||||
const handler = () => backend.set(proxy);
|
||||
emitter.on("SET", handler);
|
||||
emitter.on("DEL", handler);
|
||||
|
||||
return proxy;
|
||||
}
|
||||
|
||||
export function wrapSync<T extends Promise<any>>(store: T): Awaited<T> {
|
||||
let awaited: any = undefined;
|
||||
|
||||
const awaitQueue: (() => void)[] = [];
|
||||
const awaitInit = (cb: () => void) => (awaited ? cb() : awaitQueue.push(cb));
|
||||
|
||||
store.then(v => {
|
||||
awaited = v;
|
||||
awaitQueue.forEach(cb => cb());
|
||||
});
|
||||
|
||||
return new Proxy({} as Awaited<T>, {
|
||||
...Object.fromEntries(
|
||||
Object.getOwnPropertyNames(Reflect)
|
||||
// @ts-expect-error
|
||||
.map(k => [k, (t: T, ...a: any[]) => Reflect[k](awaited ?? t, ...a)])
|
||||
),
|
||||
get(target, prop, recv) {
|
||||
if (prop === syncAwaitSymbol) return awaitInit;
|
||||
return Reflect.get(awaited ?? target, prop, recv);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function awaitStorage(...stores: any[]) {
|
||||
return Promise.all(
|
||||
stores.map(store => new Promise<void>(res => store[syncAwaitSymbol](res)))
|
||||
);
|
||||
}
|
||||
|
||||
export interface StorageBackend {
|
||||
get: () => unknown | Promise<unknown>;
|
||||
set: (data: unknown) => void | Promise<void>;
|
||||
}
|
||||
const ILLEGAL_CHARS_REGEX = /[<>:"/\\|?*]/g;
|
||||
const filePathFixer = (file: string): string => Platform.select({
|
||||
default: file,
|
||||
ios: FileManager.saveFileToGallery ? file : `Documents/${file}`,
|
||||
});
|
||||
const getMMKVPath = (name: string): string => {
|
||||
if (ILLEGAL_CHARS_REGEX.test(name)) {
|
||||
// Replace forbidden characters with hyphens
|
||||
name = name.replace(ILLEGAL_CHARS_REGEX, "-").replace(/-+/g, "-");
|
||||
}
|
||||
|
||||
return `vd_mmkv/${name}`;
|
||||
};
|
||||
|
||||
export const purgeStorage = async (store: string) => {
|
||||
if (await MMKVManager.getItem(store)) {
|
||||
MMKVManager.removeItem(store);
|
||||
}
|
||||
|
||||
const mmkvPath = getMMKVPath(store);
|
||||
if (await FileManager.fileExists(`${FileManager.getConstants().DocumentsDirPath}/${mmkvPath}`)) {
|
||||
await FileManager.removeFile?.("documents", mmkvPath);
|
||||
}
|
||||
};
|
||||
|
||||
export const createMMKVBackend = (store: string, defaultData = {}) => {
|
||||
const mmkvPath = getMMKVPath(store);
|
||||
const defaultStr = JSON.stringify(defaultData);
|
||||
|
||||
return createFileBackend(mmkvPath, defaultData, (async () => {
|
||||
const path = `${FileManager.getConstants().DocumentsDirPath}/${mmkvPath}`;
|
||||
if (await FileManager.fileExists(path)) return;
|
||||
|
||||
let oldData = await MMKVManager.getItem(store) ?? defaultStr;
|
||||
|
||||
// From the testing on Android, it seems to return this if the data is too large
|
||||
if (oldData === "!!LARGE_VALUE!!") {
|
||||
const cachePath = `${FileManager.getConstants().CacheDirPath}/mmkv/${store}`;
|
||||
if (await FileManager.fileExists(cachePath)) {
|
||||
oldData = await FileManager.readFile(cachePath, "utf8");
|
||||
} else {
|
||||
console.log(`${store}: Experienced data loss :(`);
|
||||
oldData = defaultStr;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
JSON.parse(oldData);
|
||||
} catch {
|
||||
console.error(`${store} had an unparseable data while migrating`);
|
||||
oldData = defaultStr;
|
||||
}
|
||||
|
||||
await FileManager.writeFile("documents", filePathFixer(mmkvPath), oldData, "utf8");
|
||||
if (await MMKVManager.getItem(store) !== null) {
|
||||
MMKVManager.removeItem(store);
|
||||
console.log(`Successfully migrated ${store} store from MMKV storage to fs`);
|
||||
}
|
||||
})());
|
||||
};
|
||||
|
||||
export const createFileBackend = (file: string, defaultData = {}, migratePromise?: Promise<void>): StorageBackend => {
|
||||
return {
|
||||
get: async () => {
|
||||
await migratePromise;
|
||||
const path = `${FileManager.getConstants().DocumentsDirPath}/${file}`;
|
||||
|
||||
if (await FileManager.fileExists(path)) {
|
||||
const content = await FileManager.readFile(path, "utf8");
|
||||
try {
|
||||
return JSON.parse(content);
|
||||
} catch {
|
||||
// Corrupted content, ignore
|
||||
}
|
||||
}
|
||||
|
||||
await FileManager.writeFile("documents", filePathFixer(file), JSON.stringify(defaultData), "utf8");
|
||||
return JSON.parse(await FileManager.readFile(path, "utf8"));
|
||||
},
|
||||
set: async (data) => {
|
||||
await migratePromise;
|
||||
await FileManager.writeFile("documents", filePathFixer(file), JSON.stringify(data), "utf8");
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { clearFolder, downloadFile, fileExists, removeFile, writeFile } from "@lib/api/native/fs";
|
||||
import { awaitStorage, createMMKVBackend, createStorage, wrapSync } from "@lib/api/storage";
|
||||
import { awaitStorage, createMMKVBackend, createStorage, wrapSync } from "@core/vendetta/storage";
|
||||
import { safeFetch } from "@lib/utils";
|
||||
|
||||
type FontMap = Record<string, string>;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { patcher } from "@lib/api";
|
||||
import { registerCommand } from "@lib/api/commands";
|
||||
import { createStorage } from "@lib/api/storage/new";
|
||||
import { createStorage } from "@lib/api/storage";
|
||||
import { logger } from "@lib/utils/logger";
|
||||
|
||||
import { registeredPlugins } from ".";
|
||||
|
|
@ -39,7 +39,7 @@ export function createBunnyPluginAPI(id: string) {
|
|||
},
|
||||
// Added something in here? Make sure to also update BunnyPluginProperty in ./types
|
||||
plugin: {
|
||||
createStorage: () => createStorage(`plugins/storage/${id}.json`),
|
||||
createStorage: <T extends object = any>() => createStorage<T>(`plugins/storage/${id}.json`),
|
||||
manifest: registeredPlugins.get(id),
|
||||
logger
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { getCorePlugins } from "@core/plugins";
|
||||
import { readFile, removeFile, writeFile } from "@lib/api/native/fs";
|
||||
import { awaitStorage, createStorage, getPreloadedStorage, preloadStorageIfExists, updateStorageAsync } from "@lib/api/storage/new";
|
||||
import { awaitStorage, createStorage, getPreloadedStorage, preloadStorageIfExists, updateStorage } from "@lib/api/storage";
|
||||
import { safeFetch } from "@lib/utils";
|
||||
import { OFFICIAL_PLUGINS_REPO_URL } from "@lib/utils/constants";
|
||||
import { semver } from "@metro/common";
|
||||
|
|
@ -97,7 +97,7 @@ export async function updateAndWritePlugin(repoUrl: string, id: string, fetchScr
|
|||
await writeFile(manifest.jsPath, js);
|
||||
}
|
||||
|
||||
await updateStorageAsync(`plugins/manifests/${id}.json`, manifest);
|
||||
await updateStorage(`plugins/manifests/${id}.json`, manifest);
|
||||
|
||||
if (registeredPlugins.has(id)) {
|
||||
const existingManifest = registeredPlugins.get(id)!;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { createStorage } from "@lib/api/storage";
|
||||
import { createStorage } from "@core/vendetta/storage";
|
||||
import { Logger } from "@lib/utils/logger";
|
||||
import { Author } from "../types";
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
import { getStoredTheme, getThemeFilePath } from "@lib/api/native/loader";
|
||||
import { ThemeManager } from "@lib/api/native/modules";
|
||||
import { after, before, instead } from "@lib/api/patcher";
|
||||
import { awaitStorage, createFileBackend, createMMKVBackend, createStorage, wrapSync } from "@lib/api/storage";
|
||||
import { awaitStorage, createFileBackend, createMMKVBackend, createStorage, wrapSync } from "@core/vendetta/storage";
|
||||
import { findInReactTree, safeFetch } from "@lib/utils";
|
||||
import { lazyDestructure, proxyLazy } from "@lib/utils/lazy";
|
||||
import { byMutableProp } from "@metro/filters";
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { getLoaderConfigPath } from "@lib/api/native/loader";
|
||||
import { createFileBackend, createMMKVBackend, createStorage, wrapSync } from "@lib/api/storage";
|
||||
import { createFileBackend, createMMKVBackend, createStorage, wrapSync } from "@core/vendetta/storage";
|
||||
|
||||
export interface Settings {
|
||||
debuggerUrl: string;
|
||||
|
|
|
|||
|
|
@ -1,95 +0,0 @@
|
|||
import { FileManager, MMKVManager } from "@lib/api/native/modules";
|
||||
import { Platform } from "react-native";
|
||||
|
||||
export interface StorageBackend {
|
||||
get: () => unknown | Promise<unknown>;
|
||||
set: (data: unknown) => void | Promise<void>;
|
||||
}
|
||||
|
||||
const ILLEGAL_CHARS_REGEX = /[<>:"/\\|?*]/g;
|
||||
|
||||
const filePathFixer = (file: string): string => Platform.select({
|
||||
default: file,
|
||||
ios: FileManager.saveFileToGallery ? file : `Documents/${file}`,
|
||||
});
|
||||
|
||||
const getMMKVPath = (name: string): string => {
|
||||
if (ILLEGAL_CHARS_REGEX.test(name)) {
|
||||
// Replace forbidden characters with hyphens
|
||||
name = name.replace(ILLEGAL_CHARS_REGEX, "-").replace(/-+/g, "-");
|
||||
}
|
||||
|
||||
return `vd_mmkv/${name}`;
|
||||
};
|
||||
|
||||
export const purgeStorage = async (store: string) => {
|
||||
if (await MMKVManager.getItem(store)) {
|
||||
MMKVManager.removeItem(store);
|
||||
}
|
||||
|
||||
const mmkvPath = getMMKVPath(store);
|
||||
if (await FileManager.fileExists(`${FileManager.getConstants().DocumentsDirPath}/${mmkvPath}`)) {
|
||||
await FileManager.removeFile?.("documents", mmkvPath);
|
||||
}
|
||||
};
|
||||
|
||||
export const createMMKVBackend = (store: string, defaultData = {}) => {
|
||||
const mmkvPath = getMMKVPath(store);
|
||||
const defaultStr = JSON.stringify(defaultData);
|
||||
|
||||
return createFileBackend(mmkvPath, defaultData, (async () => {
|
||||
const path = `${FileManager.getConstants().DocumentsDirPath}/${mmkvPath}`;
|
||||
if (await FileManager.fileExists(path)) return;
|
||||
|
||||
let oldData = await MMKVManager.getItem(store) ?? defaultStr;
|
||||
|
||||
// From the testing on Android, it seems to return this if the data is too large
|
||||
if (oldData === "!!LARGE_VALUE!!") {
|
||||
const cachePath = `${FileManager.getConstants().CacheDirPath}/mmkv/${store}`;
|
||||
if (await FileManager.fileExists(cachePath)) {
|
||||
oldData = await FileManager.readFile(cachePath, "utf8");
|
||||
} else {
|
||||
console.log(`${store}: Experienced data loss :(`);
|
||||
oldData = defaultStr;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
JSON.parse(oldData);
|
||||
} catch {
|
||||
console.error(`${store} had an unparseable data while migrating`);
|
||||
oldData = defaultStr;
|
||||
}
|
||||
|
||||
await FileManager.writeFile("documents", filePathFixer(mmkvPath), oldData, "utf8");
|
||||
if (await MMKVManager.getItem(store) !== null) {
|
||||
MMKVManager.removeItem(store);
|
||||
console.log(`Successfully migrated ${store} store from MMKV storage to fs`);
|
||||
}
|
||||
})());
|
||||
};
|
||||
|
||||
export const createFileBackend = (file: string, defaultData = {}, migratePromise?: Promise<void>): StorageBackend => {
|
||||
return {
|
||||
get: async () => {
|
||||
await migratePromise;
|
||||
const path = `${FileManager.getConstants().DocumentsDirPath}/${file}`;
|
||||
|
||||
if (await FileManager.fileExists(path)) {
|
||||
const content = await FileManager.readFile(path, "utf8");
|
||||
try {
|
||||
return JSON.parse(content);
|
||||
} catch {
|
||||
// Corrupted content, ignore
|
||||
}
|
||||
}
|
||||
|
||||
await FileManager.writeFile("documents", filePathFixer(file), JSON.stringify(defaultData), "utf8");
|
||||
return JSON.parse(await FileManager.readFile(path, "utf8"));
|
||||
},
|
||||
set: async data => {
|
||||
await migratePromise;
|
||||
await FileManager.writeFile("documents", filePathFixer(file), JSON.stringify(data), "utf8");
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
@ -1,152 +1,190 @@
|
|||
import { StorageBackend } from "@lib/api/storage/backends";
|
||||
import { Emitter, EmitterEvent, EmitterListener, EmitterListenerData } from "@lib/utils/Emitter";
|
||||
import { Observable, ObserverOptions } from "@gullerya/object-observer";
|
||||
import { fileExists, readFile, removeFile, writeFile } from "@lib/api/native/fs";
|
||||
import { Emitter } from "@core/vendetta/Emitter";
|
||||
import { debounce } from "es-toolkit";
|
||||
|
||||
const emitterSymbol = Symbol.for("vendetta.storage.emitter");
|
||||
const syncAwaitSymbol = Symbol.for("vendetta.storage.accessor");
|
||||
const storageInitErrorSymbol = Symbol.for("bunny.storage.initError");
|
||||
const storagePromiseSymbol = Symbol.for("bunny.storage.promise");
|
||||
|
||||
export function createProxy(target: any = {}): { proxy: any; emitter: Emitter; } {
|
||||
const emitter = new Emitter();
|
||||
const _loadedStorage = {} as Record<string, any>;
|
||||
|
||||
const childrens = new WeakMap<any, any>();
|
||||
const proxiedChildrenSet = new WeakSet<any>();
|
||||
|
||||
function createProxy(target: any, path: string[]): any {
|
||||
return new Proxy(target, {
|
||||
get(target, prop: string) {
|
||||
if ((prop as unknown) === emitterSymbol) return emitter;
|
||||
|
||||
const newPath = [...path, prop];
|
||||
const value: any = target[prop];
|
||||
|
||||
if (value !== undefined && value !== null) {
|
||||
emitter.emit("GET", {
|
||||
path: newPath,
|
||||
value,
|
||||
});
|
||||
|
||||
if (typeof value === "object") {
|
||||
if (proxiedChildrenSet.has(value)) return value;
|
||||
if (childrens.has(value)) return childrens.get(value);
|
||||
|
||||
const childrenProxy = createProxy(value, newPath);
|
||||
childrens.set(value, childrenProxy);
|
||||
return childrenProxy;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
return value;
|
||||
},
|
||||
|
||||
set(target, prop: string, value) {
|
||||
if (typeof value === "object") {
|
||||
if (childrens.has(value)) {
|
||||
target[prop] = childrens.get(value);
|
||||
} else {
|
||||
const childrenProxy = createProxy(value, [...path, prop]);
|
||||
childrens.set(value, childrenProxy);
|
||||
proxiedChildrenSet.add(value);
|
||||
target[prop] = childrenProxy;
|
||||
}
|
||||
} else {
|
||||
target[prop] = value;
|
||||
}
|
||||
|
||||
emitter.emit("SET", {
|
||||
path: [...path, prop],
|
||||
value: target[prop],
|
||||
});
|
||||
// we do not care about success, if this actually does fail we have other problems
|
||||
return true;
|
||||
},
|
||||
|
||||
deleteProperty(target, prop: string) {
|
||||
const value = typeof target[prop] === "object" ? childrens.get(target[prop])! : target[prop];
|
||||
const success = delete target[prop];
|
||||
if (success)
|
||||
emitter.emit("DEL", {
|
||||
value,
|
||||
path: [...path, prop],
|
||||
});
|
||||
return success;
|
||||
},
|
||||
});
|
||||
}
|
||||
function createFileBackend<T extends object>(filePath: string) {
|
||||
const write = debounce((data: T) => {
|
||||
writeFile(filePath, JSON.stringify(data));
|
||||
}, 500);
|
||||
|
||||
return {
|
||||
proxy: createProxy(target, []),
|
||||
emitter,
|
||||
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 useProxy<T>(storage: T): T {
|
||||
const emitter = (storage as any)?.[emitterSymbol] as Emitter;
|
||||
if (!emitter) throw new Error("storage?.[emitterSymbol] is undefined");
|
||||
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: EmitterListener = (event: EmitterEvent, data: EmitterListenerData) => {
|
||||
if (event === "DEL" && data.value === storage) return;
|
||||
forceUpdate();
|
||||
};
|
||||
const listener = () => forceUpdate();
|
||||
|
||||
emitter.on("SET", listener);
|
||||
emitter.on("DEL", listener);
|
||||
observables.forEach(o => Observable.observe(o, listener, opts));
|
||||
|
||||
return () => {
|
||||
emitter.off("SET", listener);
|
||||
emitter.off("DEL", listener);
|
||||
observables.forEach(o => Observable.unobserve(o, listener));
|
||||
};
|
||||
}, []);
|
||||
|
||||
return storage;
|
||||
}
|
||||
|
||||
export async function createStorage<T>(backend: StorageBackend): Promise<Awaited<T>> {
|
||||
const data = await backend.get();
|
||||
const { proxy, emitter } = createProxy(data);
|
||||
|
||||
const handler = () => backend.set(proxy);
|
||||
emitter.on("SET", handler);
|
||||
emitter.on("DEL", handler);
|
||||
|
||||
return proxy;
|
||||
export async function updateStorage<T extends object = {}>(path: string, value: T): Promise<void> {
|
||||
_loadedStorage[path] = value;
|
||||
createFileBackend<T>(path).set(value);
|
||||
}
|
||||
|
||||
export function wrapSync<T extends Promise<any>>(store: T): Awaited<T> {
|
||||
let awaited: any = undefined;
|
||||
export function createStorageAndCallback<T extends object = {}>(
|
||||
path: string,
|
||||
cb: (proxy: T) => void,
|
||||
{
|
||||
dflt = {} as T,
|
||||
nullIfEmpty = false
|
||||
} = {}
|
||||
) {
|
||||
let emitter: Emitter;
|
||||
|
||||
const awaitQueue: (() => void)[] = [];
|
||||
const awaitInit = (cb: () => void) => (awaited ? cb() : awaitQueue.push(cb));
|
||||
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();
|
||||
|
||||
store.then(v => {
|
||||
awaited = v;
|
||||
awaitQueue.forEach(cb => cb());
|
||||
});
|
||||
Observable.observe(target, changes => {
|
||||
for (const change of changes) {
|
||||
emitter.emit(change.type !== "delete" ? "SET" : "DEL", {
|
||||
path: change.path,
|
||||
value: change.value
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return new Proxy({} as Awaited<T>, {
|
||||
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)
|
||||
// @ts-expect-error
|
||||
.map(k => [k, (t: T, ...a: any[]) => Reflect[k](awaited ?? t, ...a)])
|
||||
.map(k => [k, (t: T, ...a: any[]) => {
|
||||
// @ts-expect-error
|
||||
return check() && Reflect[k](awaited, ...a);
|
||||
}])
|
||||
),
|
||||
get(target, prop, recv) {
|
||||
if (prop === syncAwaitSymbol) return awaitInit;
|
||||
return Reflect.get(awaited ?? 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 false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function awaitStorage(...stores: any[]) {
|
||||
return Promise.all(
|
||||
stores.map(store => new Promise<void>(res => store[syncAwaitSymbol](res)))
|
||||
);
|
||||
export async function purgeStorage(path: string) {
|
||||
await removeFile(path);
|
||||
delete _loadedStorage[path];
|
||||
}
|
||||
|
||||
export {
|
||||
createFileBackend,
|
||||
createMMKVBackend,
|
||||
purgeStorage
|
||||
} from "@lib/api/storage/backends";
|
||||
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];
|
||||
}
|
||||
|
|
@ -1,203 +0,0 @@
|
|||
// New iteration of storage API, mostly yoinked from unreleased pyoncord (and sunrise?)
|
||||
import { fileExists, readFile, writeFile } from "@lib/api/native/fs";
|
||||
import { Emitter } from "@lib/utils/Emitter";
|
||||
|
||||
interface StorageBackend<T = unknown> {
|
||||
get: () => Promise<T>;
|
||||
set: (data: T) => Promise<void>;
|
||||
exists: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
const emitterSymbol = Symbol.for("bunny.storage.emitter");
|
||||
const storageInitErrorSymbol = Symbol.for("bunny.storage.initError");
|
||||
const storagePromiseSymbol = Symbol.for("bunny.storage.promise");
|
||||
|
||||
const _loadedPath = {} as Record<string, any>;
|
||||
|
||||
function createFileBackend<T = any>(filePath: string): StorageBackend<T> {
|
||||
return {
|
||||
get: async () => {
|
||||
try {
|
||||
return JSON.parse(await readFile(filePath));
|
||||
} catch (e) {
|
||||
throw new Error(`Failed to parse storage from '${filePath}'`, { cause: e });
|
||||
}
|
||||
},
|
||||
set: async data => {
|
||||
if (!data || typeof data !== "object") throw new Error("data needs to be an object");
|
||||
await writeFile(filePath, JSON.stringify(data));
|
||||
},
|
||||
exists: async () => {
|
||||
return await fileExists(filePath);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function _createProxy(target: any, path: string[], emitter: Emitter): any {
|
||||
// cache the children proxies so proxy.child === proxy.child (yields the same value)
|
||||
const objChildrens = new WeakMap<object, object>();
|
||||
|
||||
return new Proxy(target, {
|
||||
get(target, prop: string) {
|
||||
if ((prop as unknown) === emitterSymbol) return emitter;
|
||||
|
||||
const newPath = [...path, prop];
|
||||
let value: any = target[prop];
|
||||
|
||||
if (value && typeof value === "object") {
|
||||
const origValue = value;
|
||||
value = objChildrens.get(origValue);
|
||||
if (!value) {
|
||||
value = _createProxy(origValue, newPath, emitter);
|
||||
objChildrens.set(origValue, value!);
|
||||
}
|
||||
}
|
||||
|
||||
if (value != null) {
|
||||
emitter.emit("GET", {
|
||||
path: newPath,
|
||||
value,
|
||||
});
|
||||
}
|
||||
|
||||
return value;
|
||||
},
|
||||
|
||||
set(target, prop: string, value) {
|
||||
target[prop] = value;
|
||||
emitter.emit("SET", {
|
||||
path: [...path, prop],
|
||||
value,
|
||||
});
|
||||
|
||||
// we do not care about success, if this actually does fail we have other problems
|
||||
return true;
|
||||
},
|
||||
|
||||
deleteProperty(target, prop: string) {
|
||||
const success = delete target[prop];
|
||||
if (success)
|
||||
emitter.emit("DEL", {
|
||||
path: [...path, prop],
|
||||
});
|
||||
return success;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function createProxy(target: any = {}): { proxy: any; emitter: Emitter; } {
|
||||
const emitter = new Emitter();
|
||||
|
||||
return {
|
||||
proxy: _createProxy(target, [], emitter),
|
||||
emitter,
|
||||
};
|
||||
}
|
||||
|
||||
export function useProxy<T>(storage: T & { [key: string | symbol]: any; }) {
|
||||
if (storage[storageInitErrorSymbol]) throw new Error(
|
||||
"An error occured while initializing the storage",
|
||||
{ cause: storage[storageInitErrorSymbol] }
|
||||
);
|
||||
|
||||
const emitter = storage[emitterSymbol] as Emitter;
|
||||
|
||||
if (emitter == null) {
|
||||
throw new Error(`InvalidArgumentException - storage[emitterSymbol] is ${typeof emitter}`);
|
||||
}
|
||||
|
||||
const [, forceUpdate] = React.useReducer(n => ~n, 0);
|
||||
|
||||
React.useEffect(() => {
|
||||
const listener = () => forceUpdate();
|
||||
|
||||
emitter.on("SET", listener);
|
||||
emitter.on("DEL", listener);
|
||||
|
||||
return () => {
|
||||
emitter.off("SET", listener);
|
||||
emitter.off("DEL", listener);
|
||||
};
|
||||
}, []);
|
||||
}
|
||||
|
||||
export async function updateStorageAsync<T>(path: string, value: T): Promise<void> {
|
||||
_loadedPath[path] = value;
|
||||
await createFileBackend<T>(path).set(value);
|
||||
}
|
||||
|
||||
export function createStorageAndCallback<T>(path: string, dflt = {} as T, cb: (proxy: T) => void) {
|
||||
const callback = (data: any) => {
|
||||
const { proxy, emitter } = createProxy(data);
|
||||
|
||||
const handler = () => backend.set(proxy);
|
||||
emitter.on("SET", handler);
|
||||
emitter.on("DEL", handler);
|
||||
|
||||
cb(proxy);
|
||||
};
|
||||
|
||||
const backend = createFileBackend<T>(path);
|
||||
if (_loadedPath[path]) callback(_loadedPath[path]);
|
||||
else {
|
||||
backend.exists().then(async exists => {
|
||||
if (!exists) {
|
||||
await backend.set(dflt);
|
||||
callback(dflt);
|
||||
} else {
|
||||
callback(await backend.get());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function createStorageAsync<T>(path: string, dflt = {} as T): Promise<T> {
|
||||
return new Promise(r => createStorageAndCallback(path, dflt, r));
|
||||
}
|
||||
|
||||
export const createStorage = <T>(path: string, dflt = {} as T): T & { [key: symbol]: any; } => {
|
||||
const promise = new Promise(r => resolvePromise = r);
|
||||
let awaited: any, resolved: boolean, error: any, resolvePromise: (val?: unknown) => void;
|
||||
|
||||
createStorageAndCallback(path, dflt, proxy => {
|
||||
awaited = proxy;
|
||||
resolved = true;
|
||||
resolvePromise();
|
||||
});
|
||||
|
||||
const check = () => {
|
||||
if (resolved) return true;
|
||||
throw new Error("Attempted to access storage without initializing");
|
||||
};
|
||||
|
||||
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) {
|
||||
if (_loadedPath[path]) return _loadedPath[path];
|
||||
|
||||
const backend = createFileBackend(path);
|
||||
if (await backend.exists()) {
|
||||
return _loadedPath[path] = await backend.get();
|
||||
}
|
||||
}
|
||||
|
||||
export function getPreloadedStorage<T>(path: string): T {
|
||||
return _loadedPath[path];
|
||||
}
|
||||
export function awaitStorage(...proxies: any[]) {
|
||||
return Promise.all(proxies.map(proxy => proxy[storagePromiseSymbol]));
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue