/** * 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; set(key: string, value: any): Promise; remove(key: string): Promise; clear(): Promise; }; 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 { 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 { window.hubStorage.setItem(`${KEY_NAME}-${key}`, JSON.stringify(value)); } async remove(key: string): Promise { window.hubStorage.removeItem(`${KEY_NAME}-${key}`); } async clear(): Promise { 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 { 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 = 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 { 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 { this.iframeWindow.postMessage({ type: 'set', key: key, value: value }, '*'); } async remove(key: string): Promise { this.iframeWindow.postMessage({ type: 'remove', key: key }, '*'); } async clear(): Promise { this.iframeWindow.postMessage({ type: 'clear' }, '*'); } async destroy(): Promise { 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; 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((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 { const backend = await this.backendPromise; return backend.get(key); } async set(key: string, value: any): Promise { const backend = await this.backendPromise; return backend.set(key, value); } async remove(key: string): Promise { const backend = await this.backendPromise; return backend.remove(key); } async clear(): Promise { const backend = await this.backendPromise; return backend.clear(); } } export const storage = new StorageProxy();