Files
CHOMPStation2/tgui/packages/common/storage.ts
CHOMPStation2StaffMirrorBot a42bdce47b [MIRROR] Iframe fix (#12067)
Co-authored-by: Selis <12716288+ItsSelis@users.noreply.github.com>
2025-12-05 02:18:52 +01:00

237 lines
6.6 KiB
TypeScript

/**
* Browser-agnostic abstraction of key-value web storage.
*
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
export const IMPL_HUB_STORAGE = 1;
export const IMPL_IFRAME_INDEXED_DB = 2;
const KEY_NAME = 'chomp'; // CHOMPEdit - CHOMPStation Localstore
type StorageImplementation =
| typeof IMPL_HUB_STORAGE
| typeof IMPL_IFRAME_INDEXED_DB;
type StorageBackend = {
impl: StorageImplementation;
get(key: string): Promise<any>;
set(key: string, value: any): Promise<void>;
remove(key: string): Promise<void>;
clear(): Promise<void>;
};
const testGeneric = (testFn: () => boolean) => (): boolean => {
try {
return Boolean(testFn());
} catch {
return false;
}
};
const testHubStorage = testGeneric(
() => window.hubStorage && !!window.hubStorage.getItem,
);
class HubStorageBackend implements StorageBackend {
public impl: StorageImplementation;
constructor() {
this.impl = IMPL_HUB_STORAGE;
}
async get(key: string): Promise<any> {
const value = await window.hubStorage.getItem(`${KEY_NAME}-${key}`);
if (typeof value === 'string') {
return JSON.parse(value);
}
return undefined;
}
async set(key: string, value: any): Promise<void> {
window.hubStorage.setItem(`${KEY_NAME}-${key}`, JSON.stringify(value));
}
async remove(key: string): Promise<void> {
window.hubStorage.removeItem(`${KEY_NAME}-${key}`);
}
async clear(): Promise<void> {
window.hubStorage.clear();
}
}
class IFrameIndexedDbBackend implements StorageBackend {
public impl: StorageImplementation;
private documentElement: HTMLIFrameElement;
private iframeWindow: Window;
constructor() {
this.impl = IMPL_IFRAME_INDEXED_DB;
}
async ready(): Promise<boolean | null> {
const iframe = document.createElement('iframe');
const iframeStore = `${Byond.storageCdn}?store=${KEY_NAME}`;
iframe.style.display = 'none';
this.documentElement = document.body.appendChild(iframe);
iframe.src = iframeStore;
const completePromise: Promise<boolean> = new Promise((resolve) => {
fetch(iframeStore, { method: 'HEAD' })
.then((response) => {
if (response.status !== 200) {
resolve(false);
}
})
.catch(() => {
resolve(false);
});
window.addEventListener('message', (message) => {
if (message.data === 'ready') {
resolve(true);
}
});
});
if (!this.documentElement.contentWindow) {
return new Promise((res) => res(false));
}
this.iframeWindow = this.documentElement.contentWindow;
return completePromise;
}
async get(key: string): Promise<any> {
const promise = new Promise((resolve) => {
window.addEventListener('message', (message) => {
if (message.data.key && message.data.key === key) {
resolve(message.data.value);
}
});
});
this.iframeWindow.postMessage({ type: 'get', key: key }, '*');
return promise;
}
async set(key: string, value: any): Promise<void> {
this.iframeWindow.postMessage({ type: 'set', key: key, value: value }, '*');
}
async remove(key: string): Promise<void> {
this.iframeWindow.postMessage({ type: 'remove', key: key }, '*');
}
async clear(): Promise<void> {
this.iframeWindow.postMessage({ type: 'clear' }, '*');
}
async destroy(): Promise<void> {
document.body.removeChild(this.documentElement);
}
}
/**
* Web Storage Proxy object, which selects the best backend available
* depending on the environment.
*/
class StorageProxy implements StorageBackend {
private backendPromise: Promise<StorageBackend>;
public impl: StorageImplementation = IMPL_IFRAME_INDEXED_DB;
constructor() {
this.backendPromise = (async () => {
// If we have not enabled byondstorage yet, we need to check
// if we can use the IFrame, or if we need to enable byondstorage
console.log(`testHubStorage ${testHubStorage()}`);
if (!testHubStorage()) {
// If we have an IFrame URL we can use, and we haven't already enabled
// byondstorage, we should use the IFrame backend
console.log(`storageCdn: ${Byond.storageCdn}`);
if (Byond.storageCdn) {
const iframe = new IFrameIndexedDbBackend();
if ((await iframe.ready()) === true) {
if (await iframe.get('byondstorage-migrated')) return iframe;
Byond.winset(null, 'browser-options', '+byondstorage');
await new Promise<void>((resolve) => {
document.addEventListener('byondstorageupdated', async () => {
setTimeout(() => {
const hub = new HubStorageBackend();
// Migrate these existing settings from byondstorage to the IFrame
for (const setting of [
'panel-settings',
'chat-state',
'chat-messages',
]) {
hub
.get(setting)
.then((settings) => iframe.set(setting, settings));
}
iframe.set('byondstorage-migrated', true);
Byond.winset(null, 'browser-options', '-byondstorage');
resolve();
}, 1);
});
});
return iframe;
}
iframe.destroy();
}
// IFrame hasn't worked out for us, we'll need to enable byondstorage
Byond.winset(null, 'browser-options', '+byondstorage');
return new Promise((resolve) => {
const listener = () => {
document.removeEventListener('byondstorageupdated', listener);
// This event is emitted *before* byondstorage is actually created
// so we have to wait a little bit before we can use it
setTimeout(() => resolve(new HubStorageBackend()), 1);
};
document.addEventListener('byondstorageupdated', listener);
});
}
// byondstorage is already enabled, we can use it straight away
return new HubStorageBackend();
})();
}
async get(key: string): Promise<any> {
const backend = await this.backendPromise;
return backend.get(key);
}
async set(key: string, value: any): Promise<void> {
const backend = await this.backendPromise;
return backend.set(key, value);
}
async remove(key: string): Promise<void> {
const backend = await this.backendPromise;
return backend.remove(key);
}
async clear(): Promise<void> {
const backend = await this.backendPromise;
return backend.clear();
}
}
export const storage = new StorageProxy();