mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-09-16 18:06:55 +00:00
4.79.0
* Added: Extension Storage Provider so that extension users can store their settings in a way that won't be reset when they clear their browsing data and that works across multiple sub-domains, embeds, pop-outs, etc. * Fixed: Additional issue with FFZ failing to load completely on Firefox.
This commit is contained in:
parent
3551d5c650
commit
fb2857a63c
13 changed files with 871 additions and 51 deletions
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "frankerfacez",
|
||||
"author": "Dan Salvato LLC",
|
||||
"version": "4.78.1",
|
||||
"version": "4.79.0",
|
||||
"description": "FrankerFaceZ is a Twitch enhancement suite.",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
|
|
|
@ -6,6 +6,8 @@ import Module from 'utilities/module';
|
|||
import {DEBUG} from 'utilities/constants';
|
||||
import {serializeBlob, deserializeBlob} from 'utilities/blobs';
|
||||
|
||||
import { installPort } from './utilities/extension_port';
|
||||
|
||||
import SettingsManager from './settings/index';
|
||||
|
||||
class FFZBridge extends Module {
|
||||
|
@ -38,6 +40,9 @@ class FFZBridge extends Module {
|
|||
// Core Systems
|
||||
// ========================================================================
|
||||
|
||||
if (!! document.body.dataset.ffzExtension)
|
||||
installPort(this);
|
||||
|
||||
this.inject('settings', SettingsManager);
|
||||
|
||||
|
||||
|
|
|
@ -9,6 +9,8 @@ import Module from 'utilities/module';
|
|||
import {DEBUG} from 'utilities/constants';
|
||||
import {timeout} from 'utilities/object';
|
||||
|
||||
import { installPort } from './utilities/extension_port';
|
||||
|
||||
import SettingsManager from './settings/index';
|
||||
import AddonManager from './addons';
|
||||
import ExperimentManager from './experiments';
|
||||
|
@ -55,6 +57,9 @@ class FrankerFaceZ extends Module {
|
|||
// Core Systems
|
||||
// ========================================================================
|
||||
|
||||
if (!! document.body.dataset.ffzExtension)
|
||||
installPort(this);
|
||||
|
||||
this.inject('settings', SettingsManager);
|
||||
this.inject('experiments', ExperimentManager);
|
||||
this.inject('i18n', TranslationManager);
|
||||
|
|
|
@ -129,7 +129,7 @@ class FrankerFaceZ extends Module {
|
|||
// Core Systems
|
||||
// ========================================================================
|
||||
|
||||
if (Utility_Constants.EXTENSION)
|
||||
if (!! document.body.dataset.ffzExtension)
|
||||
installPort(this);
|
||||
|
||||
this.inject('settings', SettingsManager);
|
||||
|
|
|
@ -737,8 +737,17 @@ export default class MainMenu extends Module {
|
|||
terms.push(this.i18n.t(tok.desc_i18n_key, tok.description, null));
|
||||
}
|
||||
|
||||
if ( tok.getExtraTerms )
|
||||
terms = terms.concat(tok.getExtraTerms());
|
||||
if ( tok.getExtraTerms ) {
|
||||
let extras;
|
||||
try {
|
||||
extras = tok.getExtraTerms();
|
||||
} catch(err) {
|
||||
this.log.warn('Error getting extra terms for setting', setting_key, err);
|
||||
}
|
||||
|
||||
if (extras)
|
||||
terms = terms.concat(extras);
|
||||
}
|
||||
|
||||
tok.search_terms = terms.map(format_term).join('\n');
|
||||
|
||||
|
|
|
@ -9,6 +9,8 @@ import Module from 'utilities/module';
|
|||
import {DEBUG} from 'utilities/constants';
|
||||
import {timeout} from 'utilities/object';
|
||||
|
||||
import { installPort } from './utilities/extension_port';
|
||||
|
||||
import SettingsManager from './settings/index';
|
||||
import AddonManager from './addons';
|
||||
import ExperimentManager from './experiments';
|
||||
|
@ -49,6 +51,9 @@ class FrankerFaceZ extends Module {
|
|||
// Core Systems
|
||||
// ========================================================================
|
||||
|
||||
if (!! document.body.dataset.ffzExtension)
|
||||
installPort(this);
|
||||
|
||||
this.inject('settings', SettingsManager);
|
||||
this.inject('experiments', ExperimentManager);
|
||||
this.inject('i18n', TranslationManager);
|
||||
|
|
|
@ -167,33 +167,7 @@ export default class SettingsManager extends Module<'settings', SettingsEvents>
|
|||
this.providers[key] = provider;
|
||||
}
|
||||
|
||||
// Load any dynamic providers that have been registered.
|
||||
// Now that we're here, no further providers can be registered, so seal them.
|
||||
window.ffz_providers = window.ffz_providers || [];
|
||||
try {
|
||||
Object.seal(window.ffz_providers);
|
||||
} catch(err) {
|
||||
this.log.warn('Unable to seal window.ffz_providers:', err);
|
||||
}
|
||||
if ( window.ffz_providers.length > 0 ) {
|
||||
const evt = {
|
||||
settings: this,
|
||||
Provider: SettingsProvider,
|
||||
AdvancedProvider: AdvancedSettingsProvider,
|
||||
IGNORE_CONTENT_KEYS: IGNORE_CONTENT_KEYS,
|
||||
registerProvider: (key: string, provider: typeof SettingsProvider) => {
|
||||
if ( ! this.providers[key] && provider.supported(this) )
|
||||
this.providers[key] = provider;
|
||||
}
|
||||
};
|
||||
|
||||
for(const p of window.ffz_providers)
|
||||
try {
|
||||
p(evt);
|
||||
} catch(err) {
|
||||
this.log.error('Error while registering external settings provider:', err);
|
||||
}
|
||||
}
|
||||
this.loadDynamicProviders();
|
||||
|
||||
// This cannot be modified at a future time, as providers NEED
|
||||
// to be ready very early in FFZ intitialization. Seal it.
|
||||
|
@ -308,6 +282,38 @@ export default class SettingsManager extends Module<'settings', SettingsEvents>
|
|||
this.enable();
|
||||
}
|
||||
|
||||
loadDynamicProviders() {
|
||||
// Load any dynamic providers that have been registered.
|
||||
// Now that we're here, no further providers can be registered, so seal them.
|
||||
try {
|
||||
window.ffz_providers = window.ffz_providers || [];
|
||||
Object.seal(window.ffz_providers);
|
||||
if (window.ffz_providers.length === 0)
|
||||
return;
|
||||
} catch(err) {
|
||||
this.log.warn('Unable to seal window.ffz_providers. Dynamic settings providers will be skipped. Details:', err);
|
||||
return;
|
||||
}
|
||||
|
||||
const evt = {
|
||||
settings: this,
|
||||
Provider: SettingsProvider,
|
||||
AdvancedProvider: AdvancedSettingsProvider,
|
||||
IGNORE_CONTENT_KEYS: IGNORE_CONTENT_KEYS,
|
||||
registerProvider: (key: string, provider: typeof SettingsProvider) => {
|
||||
if ( ! this.providers[key] && provider.supported(this) )
|
||||
this.providers[key] = provider;
|
||||
}
|
||||
};
|
||||
|
||||
for(const p of window.ffz_providers)
|
||||
try {
|
||||
p(evt);
|
||||
} catch(err) {
|
||||
this.log.error('Error while registering dynamic settings provider:', err);
|
||||
}
|
||||
}
|
||||
|
||||
_updateContextProxies(proxy?: MessageEventSource) {
|
||||
if ( ! proxy && ! this._context_proxies.size )
|
||||
return;
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
'use strict';
|
||||
|
||||
import { isValidBlob, deserializeBlob, serializeBlob, BlobLike, SerializedBlobLike } from 'utilities/blobs';
|
||||
import { EXTENSION } from 'utilities/constants';
|
||||
import { isValidBlob, deserializeBlob, serializeBlob, BlobLike, SerializedBlobLike, JsonSerialized, jsonSerialize, jsonDeserialize } from 'utilities/blobs';
|
||||
import {EventEmitter} from 'utilities/events';
|
||||
import {TicketLock, has, once} from 'utilities/object';
|
||||
import type { OptionalArray, OptionalPromise, ProviderTypeMap } from '../utilities/types';
|
||||
|
@ -163,6 +162,8 @@ export abstract class AdvancedSettingsProvider extends SettingsProvider {
|
|||
|
||||
export abstract class RemoteSettingsProvider extends AdvancedSettingsProvider {
|
||||
|
||||
static needJsonBlobs = false;
|
||||
|
||||
// State and Storage
|
||||
private _start_time: number;
|
||||
private _cached: Map<string, any>;
|
||||
|
@ -300,15 +301,22 @@ export abstract class RemoteSettingsProvider extends AdvancedSettingsProvider {
|
|||
// Provider Methods: Blobs
|
||||
|
||||
async getBlob(key: string) {
|
||||
const msg = await this.rpc({ffz_type: 'get-blob', key});
|
||||
return msg ? deserializeBlob(msg) : null;
|
||||
let msg = await this.rpc({ffz_type: 'get-blob', key});
|
||||
if (msg && typeof msg.buffer === 'string')
|
||||
msg = jsonDeserialize(msg as JsonSerialized<SerializedBlobLike>);
|
||||
|
||||
return msg ? deserializeBlob(msg as SerializedBlobLike) : null;
|
||||
}
|
||||
|
||||
async setBlob(key: string, value: BlobLike) {
|
||||
let serialized: SerializedBlobLike | JsonSerialized<SerializedBlobLike> | null = await serializeBlob(value);
|
||||
if (serialized && (this.constructor as typeof RemoteSettingsProvider).needJsonBlobs)
|
||||
serialized = jsonSerialize(serialized);
|
||||
|
||||
await this.rpc({
|
||||
ffz_type: 'set-blob',
|
||||
key,
|
||||
value: await serializeBlob(value)
|
||||
value: serialized
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1543,7 +1551,7 @@ export class ExtensionProvider extends RemoteSettingsProvider {
|
|||
|
||||
// Static Stuff
|
||||
|
||||
static supported() { return EXTENSION }
|
||||
static supported() { return !! document.body.dataset.ffzExtension; }
|
||||
|
||||
static hasContent(manager: SettingsManager) {
|
||||
if ( ! ExtensionProvider.supported() )
|
||||
|
@ -1588,9 +1596,14 @@ export class ExtensionProvider extends RemoteSettingsProvider {
|
|||
static title = 'Browser Extension Storage';
|
||||
static description = 'This provider uses a browser extension service worker to store settings in a location that should not suffer from issues due to storage partitioning or cache clearing.';
|
||||
|
||||
static crossOrigin() { return true; }
|
||||
static canSupportBlobs() { return true; }
|
||||
|
||||
static allowTransfer = true;
|
||||
static shouldUpdate = true;
|
||||
|
||||
static needJsonBlobs = true;
|
||||
|
||||
// State and Storage
|
||||
|
||||
constructor(manager: SettingsManager) {
|
||||
|
@ -1616,7 +1629,7 @@ export class ExtensionProvider extends RemoteSettingsProvider {
|
|||
if ( typeof msg === 'string' )
|
||||
msg = {ffz_type: msg} as any;
|
||||
|
||||
this.manager.emit('ext:message', msg);
|
||||
this.manager.emit('ext:post-message', msg);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1676,13 +1689,13 @@ type CorsRpcTypes = {
|
|||
input: {
|
||||
key: string;
|
||||
};
|
||||
output: SerializedBlobLike | null;
|
||||
output: JsonSerialized<SerializedBlobLike> | SerializedBlobLike | null;
|
||||
};
|
||||
|
||||
'set-blob': {
|
||||
input: {
|
||||
key: string;
|
||||
value: SerializedBlobLike | null;
|
||||
value: JsonSerialized<SerializedBlobLike> | SerializedBlobLike | null;
|
||||
};
|
||||
output: void;
|
||||
};
|
||||
|
@ -1760,6 +1773,6 @@ export const Providers: Record<string, typeof SettingsProvider> = {
|
|||
local: LocalStorageProvider,
|
||||
idb: IndexedDBProvider,
|
||||
cosb: CrossOriginStorageBridge,
|
||||
//ext: ExtensionProvider
|
||||
ext: ExtensionProvider
|
||||
|
||||
};
|
||||
|
|
|
@ -427,9 +427,9 @@ export default class ChatHook extends Module {
|
|||
ui: {
|
||||
path: 'Chat > Filtering > Block >> Message Types @{"description":"This filter allows you to remove all messages of a certain type from Twitch chat. It can be used to filter system messages, such as Hosts or Raids. Some types, such as moderation actions, cannot be blocked to prevent chat functionality from breaking."}',
|
||||
component: 'blocked-types',
|
||||
getExtraTerms: () => Object.keys(this.chat_types).filter(key => ! UNBLOCKABLE_TYPES.includes(key) && ! /^\d+$/.test(key)),
|
||||
getExtraTerms: () => Object.keys(this.chat_types ?? CHAT_TYPES).filter(key => ! UNBLOCKABLE_TYPES.includes(key) && ! /^\d+$/.test(key)),
|
||||
data: () => Object
|
||||
.keys(this.chat_types)
|
||||
.keys(this.chat_types ?? CHAT_TYPES)
|
||||
.filter(key => ! UNBLOCKABLE_TYPES.includes(key) && ! /^\d+$/.test(key))
|
||||
.sort()
|
||||
}
|
||||
|
|
|
@ -33,6 +33,11 @@ export type SerializedUint8Array = {
|
|||
buffer: ArrayBuffer
|
||||
};
|
||||
|
||||
export type JsonSerialized<T> = Omit<T, "buffer"> & {
|
||||
buffer: string;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Determine if the provided object is a valid Blob that can be serialized
|
||||
* for transmission via a messaging API.
|
||||
|
@ -102,3 +107,38 @@ export function deserializeBlob(data: SerializedBlobLike): BlobLike | null {
|
|||
|
||||
throw new TypeError('Invalid type');
|
||||
}
|
||||
|
||||
|
||||
export function arrayBufferToBase64(buffer: ArrayBuffer): string {
|
||||
let binary = '';
|
||||
const bytes = new Uint8Array(buffer),
|
||||
len = bytes.byteLength;
|
||||
for (let i = 0; i < len; i++) {
|
||||
binary += String.fromCharCode(bytes[i]);
|
||||
}
|
||||
return btoa(binary);
|
||||
}
|
||||
|
||||
export function base64ToArrayBuffer(base64: string): ArrayBuffer {
|
||||
const binary_string = atob(base64),
|
||||
len = binary_string.length;
|
||||
const bytes = new Uint8Array(len);
|
||||
for (let i = 0; i < len; i++) {
|
||||
bytes[i] = binary_string.charCodeAt(i);
|
||||
}
|
||||
return bytes.buffer;
|
||||
}
|
||||
|
||||
export function jsonSerialize<T extends SerializedBlobLike>(data: T): JsonSerialized<T> {
|
||||
return {
|
||||
...data,
|
||||
buffer: arrayBufferToBase64(data.buffer)
|
||||
};
|
||||
}
|
||||
|
||||
export function jsonDeserialize<T extends SerializedBlobLike>(data: JsonSerialized<T>): T {
|
||||
return {
|
||||
...data,
|
||||
buffer: base64ToArrayBuffer(data.buffer)
|
||||
} as T;
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ type PortType = {
|
|||
|
||||
export function installPort(module: Module) {
|
||||
let port: PortType | null = null;
|
||||
let count = 0;
|
||||
|
||||
function initialize() {
|
||||
try {
|
||||
|
@ -15,21 +16,25 @@ export function installPort(module: Module) {
|
|||
});
|
||||
|
||||
cp.onMessage.addListener(msg => {
|
||||
count = 0;
|
||||
module.emit('ext:message', msg);
|
||||
});
|
||||
|
||||
cp.onDisconnect.addListener(p => {
|
||||
module.log.warn('Extension port disconnected.', (p as any)?.error ?? chrome.runtime.lastError);
|
||||
port = null;
|
||||
count++;
|
||||
if ( count < 10 )
|
||||
initialize();
|
||||
});
|
||||
|
||||
return;
|
||||
} catch(err) {
|
||||
module.log.info('Unable to connect using externally_connectable, falling back to bridge.');
|
||||
module.log.warn('Unable to connect using externally_connectable, falling back to bridge.');
|
||||
}
|
||||
|
||||
window.addEventListener('message', evt => {
|
||||
if ( evt.source !== window || ! evt.data || evt.data.ffz_from_worker )
|
||||
if ( evt.source !== window || ! evt.data?.ffz_from_worker )
|
||||
return;
|
||||
|
||||
module.emit('ext:message', evt.data);
|
||||
|
@ -49,6 +54,20 @@ export function installPort(module: Module) {
|
|||
if ( ! port )
|
||||
initialize();
|
||||
|
||||
try {
|
||||
port!.postMessage(msg);
|
||||
});
|
||||
} catch(err) {
|
||||
// Try re-initializing once.
|
||||
port = null;
|
||||
initialize();
|
||||
|
||||
try {
|
||||
port!.postMessage(msg);
|
||||
} catch(err) {
|
||||
module.log.error('Error posting message to extension port.', err);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
initialize();
|
||||
}
|
||||
|
|
722
src/worker.ts
722
src/worker.ts
|
@ -1,5 +1,6 @@
|
|||
const browser = ((globalThis as any).browser ?? globalThis.chrome) as typeof globalThis.chrome;
|
||||
|
||||
// First, the toolbar action handler.
|
||||
browser.runtime.onInstalled.addListener(() => {
|
||||
browser.action.disable();
|
||||
});
|
||||
|
@ -17,14 +18,727 @@ browser.action.onClicked.addListener(tab => {
|
|||
});
|
||||
|
||||
browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
const type = message?.type;
|
||||
if ( ! type || ! sender?.tab?.id )
|
||||
const type = message?.type,
|
||||
tab_id = sender?.tab?.id;
|
||||
|
||||
if ( ! type || ! tab_id )
|
||||
return;
|
||||
|
||||
if ( type === 'ffz_not_supported' )
|
||||
browser.action.disable(sender.tab.id);
|
||||
browser.action.disable(tab_id);
|
||||
|
||||
else if ( type === 'ffz_injecting' )
|
||||
browser.action.enable(sender.tab.id);
|
||||
browser.action.enable(tab_id);
|
||||
|
||||
else if ( type === 'ffz_mute_tab' ) {
|
||||
if ( typeof message.muted === 'boolean' )
|
||||
setMuted(tab_id, message.muted);
|
||||
}
|
||||
});
|
||||
|
||||
async function setMuted(tab_id: number, muted: boolean) {
|
||||
await browser.tabs.update(tab_id, {muted});
|
||||
}
|
||||
|
||||
|
||||
// Now, the settings proxy.
|
||||
const connections: Set<chrome.runtime.Port> = new Set;
|
||||
(globalThis as any).connections = connections;
|
||||
|
||||
function newPort(port: chrome.runtime.Port) {
|
||||
console.log('new connection from extension:', port);
|
||||
connections.add(port);
|
||||
|
||||
// Start opening the database now, so it's faster to open when we start
|
||||
// receiving messages from the extension.
|
||||
openDatabase();
|
||||
|
||||
port.onDisconnect.addListener(() => {
|
||||
connections.delete(port);
|
||||
});
|
||||
|
||||
port.onMessage.addListener(msg => {
|
||||
const type = msg?.ffz_type as keyof CorsRpcTypes;
|
||||
const id = msg?.id as number | undefined;
|
||||
if ( ! type )
|
||||
return;
|
||||
|
||||
if ( type === 'ready' ) {
|
||||
// Echo back that we're ready.
|
||||
port.postMessage({ffz_type: 'ready'});
|
||||
return;
|
||||
|
||||
} else if ( type === 'init-load' ) {
|
||||
initializeCache().then(() => {
|
||||
port.postMessage({
|
||||
ffz_type: 'reply',
|
||||
id: msg.id,
|
||||
reply: {
|
||||
blobs: true,
|
||||
values: Object.fromEntries(cache!)
|
||||
}
|
||||
});
|
||||
}).catch(err => {
|
||||
console.error('Error while initializing cache for init-load:', err);
|
||||
port.postMessage({
|
||||
ffz_type: 'reply-error',
|
||||
id: msg.id
|
||||
});
|
||||
});
|
||||
|
||||
} else if ( type === 'set' ) {
|
||||
reply(msg, port, setValue(msg.key, msg.value, port));
|
||||
|
||||
} else if ( type === 'delete' ) {
|
||||
reply(msg, port, deleteValue(msg.key, port));
|
||||
|
||||
} else if ( type === 'clear' ) {
|
||||
reply(msg, port, clearValues(port));
|
||||
|
||||
} else if ( type === 'blob-keys' ) {
|
||||
reply(msg, port, blobKeys());
|
||||
|
||||
} else if ( type === 'get-blob' ) {
|
||||
reply(msg, port, getBlob(msg.key));
|
||||
|
||||
} else if ( type === 'set-blob' ) {
|
||||
reply(msg, port, setBlob(msg.key, msg.value, port));
|
||||
|
||||
} else if ( type === 'delete-blob' ) {
|
||||
reply(msg, port, deleteBlob(msg.key, port));
|
||||
|
||||
} else if ( type === 'clear-blobs' ) {
|
||||
reply(msg, port, clearBlobs(port));
|
||||
|
||||
} else if ( type === 'flush' ) {
|
||||
reply(msg, port, flush());
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
function reply<K extends keyof CorsRpcTypes>(msg: RPCInputMessage<K>, port: chrome.runtime.Port, reply: Promise<CorsOutput<K>>) {
|
||||
reply.then(result => {
|
||||
port.postMessage({
|
||||
ffz_type: 'reply',
|
||||
id: msg.id,
|
||||
reply: result
|
||||
});
|
||||
}).catch(err => {
|
||||
console.error('Error while replying to message:', err);
|
||||
port.postMessage({
|
||||
ffz_type: 'reply-error',
|
||||
id: msg.id
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
browser.runtime.onConnect.addListener(newPort);
|
||||
|
||||
browser.runtime.onConnectExternal.addListener(newPort);
|
||||
browser.runtime.onConnect.addListener(newPort);
|
||||
|
||||
function broadcast(msg: any, exclude?: chrome.runtime.Port) {
|
||||
for(const port of connections)
|
||||
if (port !== exclude)
|
||||
port.postMessage(msg);
|
||||
}
|
||||
|
||||
|
||||
// IndexedDB Operations
|
||||
let cache: Map<string, any> | null = null;
|
||||
|
||||
const DB_VERSION = 1,
|
||||
_db_handle = new Map<string, IDBDatabase>,
|
||||
_db_waiters = new Map<string, Promise<IDBDatabase>>;
|
||||
|
||||
function openDatabase(name: string = 'FFZ', attempt = 0) {
|
||||
let db = _db_handle.get(name);
|
||||
if ( db )
|
||||
return Promise.resolve(db);
|
||||
|
||||
let waiter = _db_waiters.get(name);
|
||||
if ( waiter )
|
||||
return waiter;
|
||||
|
||||
let start = performance.now();
|
||||
|
||||
waiter = new Promise<IDBDatabase>((resolve, reject) => {
|
||||
|
||||
const request = indexedDB.open(name, DB_VERSION);
|
||||
|
||||
request.onerror = event => {
|
||||
console.error('Error opening database:', name, event);
|
||||
reject(event);
|
||||
}
|
||||
|
||||
request.onupgradeneeded = event => {
|
||||
console.log(`Upgrading database from version ${event.oldVersion} to ${DB_VERSION}`)
|
||||
|
||||
const db = request.result;
|
||||
|
||||
db.createObjectStore('settings', {keyPath: 'k'});
|
||||
db.createObjectStore('blobs');
|
||||
}
|
||||
|
||||
request.onsuccess = () => {
|
||||
const db = request.result;
|
||||
|
||||
// TODO: Check that the database isn't in an invalid state.
|
||||
|
||||
console.log(`Database "${name}" opened. (After: ${performance.now() - start}ms)`);
|
||||
_db_handle.set(name, db);
|
||||
resolve(db);
|
||||
}
|
||||
|
||||
}).finally(() => {
|
||||
if ( _db_waiters.get(name) === waiter )
|
||||
_db_waiters.delete(name);
|
||||
});
|
||||
|
||||
_db_waiters.set(name, waiter);
|
||||
return waiter;
|
||||
|
||||
}
|
||||
|
||||
|
||||
// Access Methods
|
||||
|
||||
let _flush_wait: Promise<void> | null = null;
|
||||
let _flush_wait_resolve: (() => void) | null = null;
|
||||
let _pending: Set<unknown> | null = null;
|
||||
let _last_tx = 0;
|
||||
|
||||
function _onStart(req: unknown) {
|
||||
if ( ! _pending )
|
||||
_pending = new Set;
|
||||
|
||||
_pending.add(req);
|
||||
}
|
||||
|
||||
function _onFinish(req: unknown) {
|
||||
if ( _pending ) {
|
||||
_pending.delete(req);
|
||||
if ( _pending.size )
|
||||
return;
|
||||
}
|
||||
|
||||
if ( _flush_wait_resolve )
|
||||
_flush_wait_resolve();
|
||||
}
|
||||
|
||||
function flush() {
|
||||
if ( _flush_wait )
|
||||
return _flush_wait;
|
||||
|
||||
if ( ! _pending || ! _pending.size )
|
||||
return Promise.resolve();
|
||||
|
||||
return _flush_wait = new Promise<void>(resolve => {
|
||||
_flush_wait_resolve = resolve;
|
||||
}).finally(() => {
|
||||
_flush_wait = null;
|
||||
_flush_wait_resolve = null;
|
||||
});
|
||||
}
|
||||
|
||||
// Normal Values
|
||||
async function initializeCache() {
|
||||
if ( cache )
|
||||
return;
|
||||
|
||||
cache = new Map<string, any>();
|
||||
|
||||
const db = await openDatabase(),
|
||||
trx = db.transaction(['settings'], 'readonly'),
|
||||
store = trx.objectStore('settings'),
|
||||
id = _last_tx++;
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
|
||||
trx.onabort = err => {
|
||||
console.error('Transaction aborted while initializing cache.', err);
|
||||
_onFinish(id);
|
||||
reject();
|
||||
};
|
||||
|
||||
_onStart(id);
|
||||
const request = store.getAll();
|
||||
|
||||
request.onerror = err => {
|
||||
console.error('Error while initializing cache.', err);
|
||||
_onFinish(id);
|
||||
reject();
|
||||
};
|
||||
|
||||
request.onsuccess = () => {
|
||||
const result = request.result;
|
||||
for(const entry of result) {
|
||||
cache!.set(entry.k, entry.v);
|
||||
}
|
||||
|
||||
_onFinish(id);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
async function hasValue(key: string) {
|
||||
if ( cache == null )
|
||||
await initializeCache();
|
||||
|
||||
return cache!.has(key);
|
||||
}
|
||||
|
||||
async function setValue(key: string, value: any, source?: chrome.runtime.Port) {
|
||||
if ( cache == null )
|
||||
await initializeCache();
|
||||
|
||||
if ( value === undefined ) {
|
||||
if ( cache!.has(key) )
|
||||
return deleteValue(key);
|
||||
return;
|
||||
}
|
||||
|
||||
if ( cache!.get(key) === value )
|
||||
return;
|
||||
|
||||
cache!.set(key, value);
|
||||
|
||||
const db = await openDatabase(),
|
||||
trx = db.transaction(['settings'], 'readwrite'),
|
||||
store = trx.objectStore('settings'),
|
||||
id = _last_tx++;
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
trx.onabort = err => {
|
||||
console.error('Transaction aborted while setting value.', err);
|
||||
_onFinish(id);
|
||||
reject();
|
||||
};
|
||||
|
||||
_onStart(id);
|
||||
const request = store.put({
|
||||
k: key,
|
||||
v: value
|
||||
});
|
||||
|
||||
request.onerror = err => {
|
||||
console.error('Error while setting value.', err);
|
||||
_onFinish(id);
|
||||
reject();
|
||||
}
|
||||
|
||||
request.onsuccess = () => {
|
||||
_onFinish(id);
|
||||
resolve();
|
||||
}
|
||||
}).then(() => broadcast({
|
||||
ffz_type: 'change',
|
||||
key,
|
||||
value,
|
||||
deleted: false
|
||||
}, source));
|
||||
}
|
||||
|
||||
async function deleteValue(key: string, source?: chrome.runtime.Port) {
|
||||
if ( cache == null )
|
||||
await initializeCache();
|
||||
|
||||
if ( ! cache!.has(key) )
|
||||
return;
|
||||
|
||||
cache!.delete(key);
|
||||
|
||||
const db = await openDatabase(),
|
||||
trx = db.transaction(['settings'], 'readwrite'),
|
||||
store = trx.objectStore('settings'),
|
||||
id = _last_tx++;
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
trx.onabort = err => {
|
||||
console.error('Transaction aborted while deleting value.', err);
|
||||
_onFinish(id);
|
||||
reject();
|
||||
};
|
||||
|
||||
_onStart(id);
|
||||
const request = store.delete(key);
|
||||
|
||||
request.onerror = err => {
|
||||
console.error('Error while deleting value.', err);
|
||||
_onFinish(id);
|
||||
reject();
|
||||
}
|
||||
|
||||
request.onsuccess = () => {
|
||||
_onFinish(id);
|
||||
resolve();
|
||||
}
|
||||
}).then(() => broadcast({
|
||||
ffz_type: 'change',
|
||||
key,
|
||||
value: undefined,
|
||||
deleted: true
|
||||
}, source));
|
||||
}
|
||||
|
||||
async function clearValues(source?: chrome.runtime.Port) {
|
||||
cache = new Map;
|
||||
|
||||
const db = await openDatabase(),
|
||||
trx = db.transaction(['settings'], 'readwrite'),
|
||||
store = trx.objectStore('settings'),
|
||||
id = _last_tx++;
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
trx.onabort = err => {
|
||||
console.error('Transaction aborted while clearing data.', err);
|
||||
_onFinish(id);
|
||||
reject();
|
||||
};
|
||||
|
||||
_onStart(id);
|
||||
const request = store.clear();
|
||||
|
||||
request.onerror = err => {
|
||||
console.error('Error while clearing data.', err);
|
||||
_onFinish(id);
|
||||
reject();
|
||||
}
|
||||
|
||||
request.onsuccess = () => {
|
||||
_onFinish(id);
|
||||
resolve();
|
||||
}
|
||||
}).then(() => broadcast({
|
||||
ffz_type: 'clear',
|
||||
}, source));
|
||||
}
|
||||
|
||||
|
||||
// Blobs
|
||||
|
||||
async function getBlob(key: string) {
|
||||
const db = await openDatabase(),
|
||||
trx = db.transaction(['blobs'], 'readonly'),
|
||||
store = trx.objectStore('blobs'),
|
||||
id = _last_tx++;
|
||||
|
||||
return new Promise<SerializedBlobLike | null>((resolve, reject) => {
|
||||
trx.onabort = err => {
|
||||
console.error('Transaction aborted while getting blob.', err);
|
||||
_onFinish(id);
|
||||
reject();
|
||||
};
|
||||
|
||||
_onStart(id);
|
||||
const request = store.get(key);
|
||||
|
||||
request.onerror = err => {
|
||||
console.error('Error while getting blob.', err);
|
||||
_onFinish(id);
|
||||
reject();
|
||||
}
|
||||
|
||||
request.onsuccess = () => {
|
||||
_onFinish(id);
|
||||
resolve(request.result ?? null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function setBlob(key: string, value: SerializedBlobLike | null, source?: chrome.runtime.Port) {
|
||||
if ( value === null ) {
|
||||
return deleteBlob(key, source);
|
||||
}
|
||||
|
||||
const db = await openDatabase(),
|
||||
trx = db.transaction(['blobs'], 'readwrite'),
|
||||
store = trx.objectStore('blobs'),
|
||||
id = _last_tx++;
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
trx.onabort = err => {
|
||||
console.error('Transaction aborted while setting blob.', err);
|
||||
_onFinish(id);
|
||||
reject();
|
||||
};
|
||||
|
||||
_onStart(id);
|
||||
const request = store.put(value, key);
|
||||
|
||||
request.onerror = err => {
|
||||
console.error('Error while setting blob.', err);
|
||||
_onFinish(id);
|
||||
reject();
|
||||
}
|
||||
|
||||
request.onsuccess = () => {
|
||||
_onFinish(id);
|
||||
resolve();
|
||||
}
|
||||
}).then(() => broadcast({
|
||||
ffz_type: 'change-blob',
|
||||
key,
|
||||
deleted: false
|
||||
}, source));
|
||||
}
|
||||
|
||||
async function deleteBlob(key: string, source?: chrome.runtime.Port) {
|
||||
const db = await openDatabase(),
|
||||
trx = db.transaction(['blobs'], 'readwrite'),
|
||||
store = trx.objectStore('blobs'),
|
||||
id = _last_tx++;
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
trx.onabort = err => {
|
||||
console.error('Transaction aborted while deleting blob.', err);
|
||||
_onFinish(id);
|
||||
reject();
|
||||
};
|
||||
|
||||
_onStart(id);
|
||||
const request = store.delete(key);
|
||||
|
||||
request.onerror = err => {
|
||||
console.error('Error while deleting blob.', err);
|
||||
_onFinish(id);
|
||||
reject();
|
||||
}
|
||||
|
||||
request.onsuccess = () => {
|
||||
_onFinish(id);
|
||||
resolve();
|
||||
}
|
||||
}).then(() => broadcast({
|
||||
ffz_type: 'change-blob',
|
||||
key,
|
||||
deleted: true
|
||||
}, source));
|
||||
}
|
||||
|
||||
async function blobKeys() {
|
||||
const db = await openDatabase(),
|
||||
trx = db.transaction(['blobs'], 'readonly'),
|
||||
store = trx.objectStore('blobs'),
|
||||
id = _last_tx++;
|
||||
|
||||
return new Promise<string[]>((resolve, reject) => {
|
||||
trx.onabort = err => {
|
||||
console.error('Transaction aborted while getting blob keys.', err);
|
||||
_onFinish(id);
|
||||
reject();
|
||||
};
|
||||
|
||||
_onStart(id);
|
||||
const request = store.getAllKeys();
|
||||
|
||||
request.onerror = err => {
|
||||
console.error('Error while getting blob keys.', err);
|
||||
_onFinish(id);
|
||||
reject();
|
||||
}
|
||||
|
||||
request.onsuccess = () => {
|
||||
_onFinish(id);
|
||||
resolve(request.result as string[]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function hasBlob(key: string) {
|
||||
const keys = await blobKeys();
|
||||
return keys.includes(key);
|
||||
}
|
||||
|
||||
async function clearBlobs(source?: chrome.runtime.Port) {
|
||||
const db = await openDatabase(),
|
||||
trx = db.transaction(['blobs'], 'readwrite'),
|
||||
store = trx.objectStore('blobs'),
|
||||
id = _last_tx++;
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
|
||||
trx.onabort = err => {
|
||||
console.error('Transaction aborted while clearing blobs.', err);
|
||||
_onFinish(id);
|
||||
reject();
|
||||
}
|
||||
|
||||
_onStart(id);
|
||||
const req = store.clear();
|
||||
|
||||
req.onerror = () => {
|
||||
reject();
|
||||
_onFinish(id);
|
||||
}
|
||||
|
||||
req.onsuccess = () => {
|
||||
resolve();
|
||||
broadcast({ffz_type: 'clear-blobs'}, source);
|
||||
_onFinish(id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// Storage Types
|
||||
|
||||
type CorsRpcTypes = {
|
||||
|
||||
'ready': {
|
||||
input: void;
|
||||
output: void;
|
||||
};
|
||||
|
||||
'change': {
|
||||
input: {
|
||||
key: string;
|
||||
value: any;
|
||||
deleted: boolean;
|
||||
};
|
||||
output: void;
|
||||
};
|
||||
|
||||
'init-load': {
|
||||
input: void;
|
||||
output: {
|
||||
blobs: boolean;
|
||||
values: Record<string, any>;
|
||||
}
|
||||
};
|
||||
|
||||
'set': {
|
||||
input: {
|
||||
key: string;
|
||||
value: any;
|
||||
};
|
||||
output: void;
|
||||
};
|
||||
|
||||
'delete': {
|
||||
input: {
|
||||
key: string;
|
||||
};
|
||||
output: void;
|
||||
};
|
||||
|
||||
'clear': {
|
||||
input: void;
|
||||
output: void;
|
||||
};
|
||||
|
||||
'get-blob': {
|
||||
input: {
|
||||
key: string;
|
||||
};
|
||||
output: SerializedBlobLike | null;
|
||||
};
|
||||
|
||||
'set-blob': {
|
||||
input: {
|
||||
key: string;
|
||||
value: SerializedBlobLike | null;
|
||||
};
|
||||
output: void;
|
||||
};
|
||||
|
||||
'change-blob': {
|
||||
input: {
|
||||
key: string;
|
||||
deleted: boolean;
|
||||
};
|
||||
output: void;
|
||||
}
|
||||
|
||||
'delete-blob': {
|
||||
input: {
|
||||
key: string;
|
||||
};
|
||||
output: void;
|
||||
};
|
||||
|
||||
'has-blob': {
|
||||
input: {
|
||||
key: string;
|
||||
};
|
||||
output: boolean;
|
||||
};
|
||||
|
||||
'clear-blobs': {
|
||||
input: void;
|
||||
output: void;
|
||||
};
|
||||
|
||||
'blob-keys': {
|
||||
input: void;
|
||||
output: string[];
|
||||
};
|
||||
|
||||
'flush': {
|
||||
input: void;
|
||||
output: void;
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
type CorsInput<K extends keyof CorsRpcTypes> = CorsRpcTypes[K] extends { input: infer U } ? U : void;
|
||||
type CorsOutput<K extends keyof CorsRpcTypes> = CorsRpcTypes[K] extends { output: infer U } ? U : void;
|
||||
|
||||
type RPCInputMessage<K extends keyof CorsRpcTypes> = {
|
||||
ffz_type: K;
|
||||
id?: number;
|
||||
} & CorsInput<K>;
|
||||
|
||||
type CorsReplyMessage = {
|
||||
ffz_type: 'reply';
|
||||
id: number;
|
||||
reply: any;
|
||||
};
|
||||
|
||||
type CorsReplyErrorMessage = {
|
||||
ffz_type: 'reply-error';
|
||||
id: number;
|
||||
};
|
||||
|
||||
type CorsMessage = CorsReplyMessage | CorsReplyErrorMessage | {
|
||||
[K in keyof CorsRpcTypes]: RPCInputMessage<K>
|
||||
}[keyof CorsRpcTypes];
|
||||
|
||||
/** A union of the various Blob types that are supported. */
|
||||
type BlobLike = Blob | File | ArrayBuffer | Uint8Array;
|
||||
|
||||
/** A union of the various serialized blob types. */
|
||||
type SerializedBlobLike = SerializedBlob | SerializedFile | SerializedArrayBuffer | SerializedUint8Array;
|
||||
|
||||
/** A serialized {@link Blob} representation. */
|
||||
type SerializedBlob = {
|
||||
type: 'blob';
|
||||
mime: string;
|
||||
buffer: ArrayBuffer
|
||||
};
|
||||
|
||||
/** A serialized {@link File} representation. */
|
||||
type SerializedFile = {
|
||||
type: 'file';
|
||||
mime: string;
|
||||
name: string;
|
||||
modified: number;
|
||||
buffer: ArrayBuffer
|
||||
};
|
||||
|
||||
/** A serialized {@link ArrayBuffer} representation. */
|
||||
type SerializedArrayBuffer = {
|
||||
type: 'ab';
|
||||
buffer: ArrayBuffer;
|
||||
};
|
||||
|
||||
/** A serialized {@link Uint8Array} representation. */
|
||||
type SerializedUint8Array = {
|
||||
type: 'u8',
|
||||
buffer: ArrayBuffer;
|
||||
};
|
||||
|
|
|
@ -351,6 +351,10 @@ if ( DEV_SERVER )
|
|||
|
||||
setupMiddlewares: (middlewares, devServer) => {
|
||||
|
||||
devServer.app.get('/script/script.min.js', (req, res) => {
|
||||
res.redirect('/script/script.js');
|
||||
});
|
||||
|
||||
devServer.app.get('/update_font', (req, res) => {
|
||||
const proc = exec('npm run font:save');
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue