mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2025-12-10 17:52:36 +00:00
moves clientside storage to an iframe instead of byondstorage (#93044)
## About The Pull Request this converts the dependency on byondstorage, which is laggy (due to being a large json file written to disk every 10 seconds), to using indexeddb, like we did prior to 516. this is achieved by using an iframe to give us a persistent origin, as the web is evil and has invented same-origin policy https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy. this also hosts the iframe page on the github pages site for a secondary repository (see tgstation/byond-client-storage) so it works OOTB without requiring server operators to setup the webroot cdn (which i don't believe is configured on the tg servers at the moment) however, if a server is using the webroot cdn, it will use that instead of github pages you could also host the iframe.html page on a separate host from your cdn or github pages if you wanted to if we can't access the configured cdn at all, it failovers to use byondstorage anyway, if the internet stops working and you still want your chat history to save, i guess ## Why It's Good For The Game saving this enormous json file is laggy, and this solution would fix https://github.com/tgstation/tgstation/issues/89988 and fix https://github.com/tgstation/tgstation/issues/92035 i am open to other solutions, but this does seem to require the least amount of external dependencies of posed solutions ## Changelog 🆑 fix: you should experience less stutters every 10 seconds server: server operators can now configure an alternative storage domain for clientside data storage, read the example configuration for more /🆑 --------- Co-authored-by: harryob <55142896+harryob@users.noreply.github.com>
This commit is contained in:
33
.github/workflows/generate_client_storage.yml
vendored
Normal file
33
.github/workflows/generate_client_storage.yml
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
name: "Generate Client Storage"
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- tgui/public/*
|
||||
|
||||
jobs:
|
||||
dispatch_repo:
|
||||
if: ( !contains(github.event.head_commit.message, '[ci skip]') )
|
||||
name: Repository Dispatch
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Generate App Token
|
||||
id: app-token-generation
|
||||
uses: actions/create-github-app-token@v2
|
||||
if: env.APP_PRIVATE_KEY != '' && env.APP_ID != ''
|
||||
with:
|
||||
app-id: ${{ secrets.APP_ID }}
|
||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||
owner: tgstation
|
||||
env:
|
||||
APP_PRIVATE_KEY: ${{ secrets.APP_PRIVATE_KEY }}
|
||||
APP_ID: ${{ secrets.APP_ID }}
|
||||
|
||||
- name: Send Repository Dispatch
|
||||
if: success()
|
||||
uses: peter-evans/repository-dispatch@v4
|
||||
with:
|
||||
token: ${{ steps.app-token-generation.outputs.token }}
|
||||
repository: tgstation/byond-client-storage
|
||||
event-type: on_master_push
|
||||
@@ -28,3 +28,7 @@
|
||||
if (str_var && str_var[length(str_var)] != "/")
|
||||
str_var += "/"
|
||||
return ..(str_var)
|
||||
|
||||
/datum/config_entry/string/storage_cdn_iframe
|
||||
protection = CONFIG_ENTRY_LOCKED
|
||||
default = "https://tgstation.github.io/byond-client-storage/iframe.html"
|
||||
|
||||
@@ -39,6 +39,25 @@ SUBSYSTEM_DEF(tgui)
|
||||
|
||||
basehtml = replacetextEx(basehtml, "<!-- tgui:nt-copyright -->", "Nanotrasen (c) 2525-[CURRENT_STATION_YEAR]")
|
||||
|
||||
/datum/controller/subsystem/tgui/OnConfigLoad()
|
||||
var/storage_iframe = CONFIG_GET(string/storage_cdn_iframe)
|
||||
|
||||
if(storage_iframe && storage_iframe != /datum/config_entry/string/storage_cdn_iframe::default)
|
||||
basehtml = replacetext(basehtml, "\[tgui:storagecdn\]", storage_iframe)
|
||||
return
|
||||
|
||||
if(CONFIG_GET(string/asset_transport) == "webroot")
|
||||
var/datum/asset_transport/webroot/webroot = SSassets.transport
|
||||
|
||||
var/datum/asset_cache_item/item = webroot.register_asset("iframe.html", file("tgui/public/iframe.html"))
|
||||
basehtml = replacetext(basehtml, "\[tgui:storagecdn\]", webroot.get_asset_url("iframe.html", item))
|
||||
return
|
||||
|
||||
if(!storage_iframe)
|
||||
return
|
||||
|
||||
basehtml = replacetext(basehtml, "\[tgui:storagecdn\]", storage_iframe)
|
||||
|
||||
|
||||
/datum/controller/subsystem/tgui/Shutdown()
|
||||
close_all_uis()
|
||||
|
||||
@@ -268,8 +268,7 @@ GLOBAL_LIST_INIT(unrecommended_builds, list(
|
||||
persistent_client = new(ckey)
|
||||
persistent_client.set_client(src)
|
||||
|
||||
if(byond_version >= 516)
|
||||
winset(src, null, list("browser-options" = "find,refresh,byondstorage"))
|
||||
winset(src, null, list("browser-options" = "find,refresh"))
|
||||
|
||||
// Instantiate stat panel
|
||||
stat_panel = new(src, "statbrowser")
|
||||
|
||||
@@ -44,3 +44,12 @@ EXTERNAL_RSC_URLS http://tgstation13.download/byond/tgstationv2.zip
|
||||
# <add name="Vary" value="Origin" />
|
||||
# <add name="Access-Control-Allow-Methods" value="GET, POST, OPTIONS" />
|
||||
# <add name="Access-Control-Max-Age" value="0" />
|
||||
|
||||
# If configured, this allows server operators to define the persistent origin used for clientside
|
||||
# storage. This must host the same file as available in tgui/public/iframe.html. This is also hosted
|
||||
# on the GitHub Pages site for the /tg/station repository, so does not need to be configured.
|
||||
# If multiple servers use the same domain name, clientside features such as message saving
|
||||
# and chat tabs will be persistent across both.
|
||||
# If this setting is not configured, but the webroot CDN is, that will be used instead of GitHub Pages.
|
||||
# If this setting is mpty, and the webroot CDN is disabled, byondstorage will be used.
|
||||
# STORAGE_CDN_IFRAME https://tgstation.github.io/byond-client-storage/iframe.html
|
||||
|
||||
5
tgui/global.d.ts
vendored
5
tgui/global.d.ts
vendored
@@ -63,6 +63,11 @@ type ByondType = {
|
||||
*/
|
||||
strictMode: boolean;
|
||||
|
||||
/**
|
||||
* The external URL for the IndexedDB IFrame to use as the origin
|
||||
*/
|
||||
storageCdn: string;
|
||||
|
||||
/**
|
||||
* Makes a BYOND call.
|
||||
*
|
||||
|
||||
@@ -8,8 +8,12 @@
|
||||
|
||||
export const IMPL_MEMORY = 0;
|
||||
export const IMPL_HUB_STORAGE = 1;
|
||||
export const IMPL_IFRAME_INDEXED_DB = 2;
|
||||
|
||||
type StorageImplementation = typeof IMPL_MEMORY | typeof IMPL_HUB_STORAGE;
|
||||
type StorageImplementation =
|
||||
| typeof IMPL_MEMORY
|
||||
| typeof IMPL_HUB_STORAGE
|
||||
| typeof IMPL_IFRAME_INDEXED_DB;
|
||||
|
||||
type StorageBackend = {
|
||||
impl: StorageImplementation;
|
||||
@@ -85,6 +89,80 @@ class HubStorageBackend implements StorageBackend {
|
||||
}
|
||||
}
|
||||
|
||||
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');
|
||||
iframe.style.display = 'none';
|
||||
iframe.src = Byond.storageCdn;
|
||||
|
||||
const completePromise: Promise<boolean> = new Promise((resolve) => {
|
||||
iframe.onload = () => resolve(true);
|
||||
});
|
||||
|
||||
this.documentElement = document.body.appendChild(iframe);
|
||||
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 ping(): Promise<boolean> {
|
||||
const promise: Promise<boolean> = new Promise((resolve) => {
|
||||
window.addEventListener('message', (message) => {
|
||||
if (message.data === true) {
|
||||
resolve(true);
|
||||
}
|
||||
});
|
||||
|
||||
setTimeout(() => resolve(false), 100);
|
||||
});
|
||||
|
||||
this.iframeWindow.postMessage({ type: 'ping' }, '*');
|
||||
return promise;
|
||||
}
|
||||
|
||||
async destroy(): Promise<void> {
|
||||
document.body.removeChild(this.documentElement);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Web Storage Proxy object, which selects the best backend available
|
||||
* depending on the environment.
|
||||
@@ -95,10 +173,53 @@ class StorageProxy implements StorageBackend {
|
||||
|
||||
constructor() {
|
||||
this.backendPromise = (async () => {
|
||||
if (testHubStorage()) {
|
||||
if (Byond.storageCdn && !window.hubStorage) {
|
||||
const iframe = new IFrameIndexedDbBackend();
|
||||
await iframe.ready();
|
||||
|
||||
if ((await iframe.ping()) === 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();
|
||||
|
||||
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();
|
||||
|
||||
if (!testHubStorage()) {
|
||||
Byond.winset(null, 'browser-options', '+byondstorage');
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const listener = () => {
|
||||
document.removeEventListener('byondstorageupdated', listener);
|
||||
resolve(new HubStorageBackend());
|
||||
};
|
||||
|
||||
document.addEventListener('byondstorageupdated', listener);
|
||||
});
|
||||
}
|
||||
return new HubStorageBackend();
|
||||
}
|
||||
|
||||
console.warn(
|
||||
'No supported storage backend found. Using in-memory storage.',
|
||||
);
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
|
||||
// Expose inlined metadata
|
||||
Byond.windowId = parseMetaTag('tgui:windowId');
|
||||
Byond.storageCdn = parseMetaTag('tgui:storagecdn');
|
||||
|
||||
// Backwards compatibility
|
||||
window.__windowId__ = Byond.windowId;
|
||||
|
||||
2
tgui/public/helpers.min.js
vendored
2
tgui/public/helpers.min.js
vendored
File diff suppressed because one or more lines are too long
98
tgui/public/iframe.html
Normal file
98
tgui/public/iframe.html
Normal file
@@ -0,0 +1,98 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta charset="utf-8" />
|
||||
|
||||
<script type="text/javascript">
|
||||
const INDEXED_DB_VERSION = 1;
|
||||
const INDEXED_DB_NAME = 'tgui';
|
||||
const INDEXED_DB_STORE_NAME = 'storage';
|
||||
|
||||
const READ_ONLY = 'readonly';
|
||||
const READ_WRITE = 'readwrite';
|
||||
|
||||
const MAX_MESSAGES = 1000;
|
||||
|
||||
const dbPromise = new Promise((resolve, reject) => {
|
||||
const indexedDB = window.indexedDB;
|
||||
const req = indexedDB.open(INDEXED_DB_NAME, INDEXED_DB_VERSION);
|
||||
req.onupgradeneeded = (event) => {
|
||||
try {
|
||||
if (event.oldVersion < 1) {
|
||||
req.result.createObjectStore(INDEXED_DB_STORE_NAME);
|
||||
}
|
||||
} catch (err) {
|
||||
reject(new Error('Failed to upgrade IDB: ' + req.error));
|
||||
}
|
||||
};
|
||||
|
||||
req.onsuccess = () => resolve(req.result);
|
||||
|
||||
req.onerror = () => {
|
||||
reject(new Error('Failed to open IDB: ' + req.error));
|
||||
};
|
||||
});
|
||||
|
||||
window.addEventListener('message', (messageEvent) => {
|
||||
switch (messageEvent.data.type) {
|
||||
case 'ping':
|
||||
messageEvent.source.postMessage(true, '*');
|
||||
break;
|
||||
case 'get':
|
||||
get(messageEvent.data.key).then((value) => {
|
||||
messageEvent.source.postMessage(
|
||||
{ key: messageEvent.data.key, value: value },
|
||||
'*',
|
||||
);
|
||||
});
|
||||
break;
|
||||
case 'set':
|
||||
set(messageEvent.data.key, messageEvent.data.value);
|
||||
break;
|
||||
case 'remove':
|
||||
remove(messageEvent.data.key);
|
||||
break;
|
||||
case 'clear':
|
||||
clear();
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
const getStore = async (mode) => {
|
||||
return dbPromise.then((db) =>
|
||||
db
|
||||
.transaction(INDEXED_DB_STORE_NAME, mode)
|
||||
.objectStore(INDEXED_DB_STORE_NAME),
|
||||
);
|
||||
};
|
||||
|
||||
const get = async (key) => {
|
||||
const store = await getStore(READ_ONLY);
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = store.get(key);
|
||||
req.onsuccess = () => resolve(req.result);
|
||||
req.onerror = () => reject(req.error);
|
||||
});
|
||||
};
|
||||
|
||||
const set = async (key, value) => {
|
||||
const store = await getStore(READ_WRITE);
|
||||
store.put(value, key);
|
||||
};
|
||||
|
||||
const remove = async (key) => {
|
||||
const store = await getStore(READ_WRITE);
|
||||
store.delete(key);
|
||||
};
|
||||
|
||||
const clear = async () => {
|
||||
const store = await getStore(READ_WRITE);
|
||||
store.clear();
|
||||
};
|
||||
</script>
|
||||
</head>
|
||||
</html>
|
||||
@@ -6,6 +6,7 @@
|
||||
<!-- Inlined metadata -->
|
||||
<meta id="tgui:windowId" content="[tgui:windowId]" />
|
||||
<meta id="tgui:strictMode" content="[tgui:strictMode]" />
|
||||
<meta id="tgui:storagecdn" content="[tgui:storagecdn]" />
|
||||
|
||||
<!-- Early setup -->
|
||||
<!-- tgui:helpers -->
|
||||
|
||||
Reference in New Issue
Block a user