Emoji Rendering Efficiency (#35568)
This commit is contained in:
23 files changed
+950
-329
No files matched your search
@@ -9,6 +9,7 @@ import type {
|
||||
UnicodeEmojiData,
|
||||
LocaleOrCustom,
|
||||
} from './types';
|
||||
import { emojiLogger } from './utils';
|
||||
|
||||
interface EmojiDB extends LocaleTables, DBSchema {
|
||||
custom: {
|
||||
@@ -36,40 +37,63 @@ interface LocaleTable {
|
||||
}
|
||||
type LocaleTables = Record<Locale, LocaleTable>;
|
||||
|
||||
type Database = IDBPDatabase<EmojiDB>;
|
||||
|
||||
const SCHEMA_VERSION = 1;
|
||||
|
||||
let db: IDBPDatabase<EmojiDB> | null = null;
|
||||
const loadedLocales = new Set<Locale>();
|
||||
|
||||
async function loadDB() {
|
||||
if (db) {
|
||||
return db;
|
||||
}
|
||||
db = await openDB<EmojiDB>('mastodon-emoji', SCHEMA_VERSION, {
|
||||
upgrade(database) {
|
||||
const customTable = database.createObjectStore('custom', {
|
||||
keyPath: 'shortcode',
|
||||
autoIncrement: false,
|
||||
});
|
||||
customTable.createIndex('category', 'category');
|
||||
const log = emojiLogger('database');
|
||||
|
||||
database.createObjectStore('etags');
|
||||
// Loads the database in a way that ensures it's only loaded once.
|
||||
const loadDB = (() => {
|
||||
let dbPromise: Promise<Database> | null = null;
|
||||
|
||||
for (const locale of SUPPORTED_LOCALES) {
|
||||
const localeTable = database.createObjectStore(locale, {
|
||||
keyPath: 'hexcode',
|
||||
// Actually load the DB.
|
||||
async function initDB() {
|
||||
const db = await openDB<EmojiDB>('mastodon-emoji', SCHEMA_VERSION, {
|
||||
upgrade(database) {
|
||||
const customTable = database.createObjectStore('custom', {
|
||||
keyPath: 'shortcode',
|
||||
autoIncrement: false,
|
||||
});
|
||||
localeTable.createIndex('group', 'group');
|
||||
localeTable.createIndex('label', 'label');
|
||||
localeTable.createIndex('order', 'order');
|
||||
localeTable.createIndex('tags', 'tags', { multiEntry: true });
|
||||
}
|
||||
},
|
||||
});
|
||||
return db;
|
||||
}
|
||||
customTable.createIndex('category', 'category');
|
||||
|
||||
database.createObjectStore('etags');
|
||||
|
||||
for (const locale of SUPPORTED_LOCALES) {
|
||||
const localeTable = database.createObjectStore(locale, {
|
||||
keyPath: 'hexcode',
|
||||
autoIncrement: false,
|
||||
});
|
||||
localeTable.createIndex('group', 'group');
|
||||
localeTable.createIndex('label', 'label');
|
||||
localeTable.createIndex('order', 'order');
|
||||
localeTable.createIndex('tags', 'tags', { multiEntry: true });
|
||||
}
|
||||
},
|
||||
});
|
||||
await syncLocales(db);
|
||||
return db;
|
||||
}
|
||||
|
||||
// Loads the database, or returns the existing promise if it hasn't resolved yet.
|
||||
const loadPromise = async (): Promise<Database> => {
|
||||
if (dbPromise) {
|
||||
return dbPromise;
|
||||
}
|
||||
dbPromise = initDB();
|
||||
return dbPromise;
|
||||
};
|
||||
// Special way to reset the database, used for unit testing.
|
||||
loadPromise.reset = () => {
|
||||
dbPromise = null;
|
||||
};
|
||||
return loadPromise;
|
||||
})();
|
||||
|
||||
export async function putEmojiData(emojis: UnicodeEmojiData[], locale: Locale) {
|
||||
loadedLocales.add(locale);
|
||||
const db = await loadDB();
|
||||
const trx = db.transaction(locale, 'readwrite');
|
||||
await Promise.all(emojis.map((emoji) => trx.store.put(emoji)));
|
||||
@@ -86,15 +110,15 @@ export async function putCustomEmojiData(emojis: CustomEmojiData[]) {
|
||||
export async function putLatestEtag(etag: string, localeString: string) {
|
||||
const locale = toSupportedLocaleOrCustom(localeString);
|
||||
const db = await loadDB();
|
||||
return db.put('etags', etag, locale);
|
||||
await db.put('etags', etag, locale);
|
||||
}
|
||||
|
||||
export async function searchEmojiByHexcode(
|
||||
export async function loadEmojiByHexcode(
|
||||
hexcode: string,
|
||||
localeString: string,
|
||||
) {
|
||||
const locale = toSupportedLocale(localeString);
|
||||
const db = await loadDB();
|
||||
const locale = toLoadedLocale(localeString);
|
||||
return db.get(locale, hexcode);
|
||||
}
|
||||
|
||||
@@ -102,45 +126,39 @@ export async function searchEmojisByHexcodes(
|
||||
hexcodes: string[],
|
||||
localeString: string,
|
||||
) {
|
||||
const locale = toSupportedLocale(localeString);
|
||||
const db = await loadDB();
|
||||
return db.getAll(
|
||||
const locale = toLoadedLocale(localeString);
|
||||
const sortedCodes = hexcodes.toSorted();
|
||||
const results = await db.getAll(
|
||||
locale,
|
||||
IDBKeyRange.bound(hexcodes[0], hexcodes[hexcodes.length - 1]),
|
||||
IDBKeyRange.bound(sortedCodes.at(0), sortedCodes.at(-1)),
|
||||
);
|
||||
return results.filter((emoji) => hexcodes.includes(emoji.hexcode));
|
||||
}
|
||||
|
||||
export async function searchEmojiByTag(tag: string, localeString: string) {
|
||||
const locale = toSupportedLocale(localeString);
|
||||
const range = IDBKeyRange.only(tag.toLowerCase());
|
||||
export async function searchEmojisByTag(tag: string, localeString: string) {
|
||||
const db = await loadDB();
|
||||
const locale = toLoadedLocale(localeString);
|
||||
const range = IDBKeyRange.bound(
|
||||
tag.toLowerCase(),
|
||||
`${tag.toLowerCase()}\uffff`,
|
||||
);
|
||||
return db.getAllFromIndex(locale, 'tags', range);
|
||||
}
|
||||
|
||||
export async function searchCustomEmojiByShortcode(shortcode: string) {
|
||||
export async function loadCustomEmojiByShortcode(shortcode: string) {
|
||||
const db = await loadDB();
|
||||
return db.get('custom', shortcode);
|
||||
}
|
||||
|
||||
export async function searchCustomEmojisByShortcodes(shortcodes: string[]) {
|
||||
const db = await loadDB();
|
||||
return db.getAll(
|
||||
const sortedCodes = shortcodes.toSorted();
|
||||
const results = await db.getAll(
|
||||
'custom',
|
||||
IDBKeyRange.bound(shortcodes[0], shortcodes[shortcodes.length - 1]),
|
||||
IDBKeyRange.bound(sortedCodes.at(0), sortedCodes.at(-1)),
|
||||
);
|
||||
}
|
||||
|
||||
export async function findMissingLocales(localeStrings: string[]) {
|
||||
const locales = new Set(localeStrings.map(toSupportedLocale));
|
||||
const missingLocales: Locale[] = [];
|
||||
const db = await loadDB();
|
||||
for (const locale of locales) {
|
||||
const rowCount = await db.count(locale);
|
||||
if (!rowCount) {
|
||||
missingLocales.push(locale);
|
||||
}
|
||||
}
|
||||
return missingLocales;
|
||||
return results.filter((emoji) => shortcodes.includes(emoji.shortcode));
|
||||
}
|
||||
|
||||
export async function loadLatestEtag(localeString: string) {
|
||||
@@ -153,3 +171,51 @@ export async function loadLatestEtag(localeString: string) {
|
||||
const etag = await db.get('etags', locale);
|
||||
return etag ?? null;
|
||||
}
|
||||
|
||||
// Private functions
|
||||
|
||||
async function syncLocales(db: Database) {
|
||||
const locales = await Promise.all(
|
||||
SUPPORTED_LOCALES.map(
|
||||
async (locale) =>
|
||||
[locale, await hasLocale(locale, db)] satisfies [Locale, boolean],
|
||||
),
|
||||
);
|
||||
for (const [locale, loaded] of locales) {
|
||||
if (loaded) {
|
||||
loadedLocales.add(locale);
|
||||
} else {
|
||||
loadedLocales.delete(locale);
|
||||
}
|
||||
}
|
||||
log('Loaded %d locales: %o', loadedLocales.size, loadedLocales);
|
||||
}
|
||||
|
||||
function toLoadedLocale(localeString: string) {
|
||||
const locale = toSupportedLocale(localeString);
|
||||
if (localeString !== locale) {
|
||||
log(`Locale ${locale} is different from provided ${localeString}`);
|
||||
}
|
||||
if (!loadedLocales.has(locale)) {
|
||||
throw new Error(`Locale ${locale} is not loaded in emoji database`);
|
||||
}
|
||||
return locale;
|
||||
}
|
||||
|
||||
async function hasLocale(locale: Locale, db: Database): Promise<boolean> {
|
||||
if (loadedLocales.has(locale)) {
|
||||
return true;
|
||||
}
|
||||
const rowCount = await db.count(locale);
|
||||
return !!rowCount;
|
||||
}
|
||||
|
||||
// Testing helpers
|
||||
export async function testGet() {
|
||||
const db = await loadDB();
|
||||
return { db, loadedLocales };
|
||||
}
|
||||
export function testClear() {
|
||||
loadedLocales.clear();
|
||||
loadDB.reset();
|
||||
}
|
||||
Reference in new issue
Block a user