Emoji: Fix path resolution for emoji worker (#36897)

pull/36941/head
Echo 1 week ago committed by Claire
parent 44d45e5705
commit c08cd6d62a

@ -1,6 +1,7 @@
import { initialState } from '@/mastodon/initial_state'; import { initialState } from '@/mastodon/initial_state';
import { toSupportedLocale } from './locale'; import { toSupportedLocale } from './locale';
import type { LocaleOrCustom } from './types';
import { emojiLogger } from './utils'; import { emojiLogger } from './utils';
// eslint-disable-next-line import/default -- Importing via worker loader. // eslint-disable-next-line import/default -- Importing via worker loader.
import EmojiWorker from './worker?worker&inline'; import EmojiWorker from './worker?worker&inline';
@ -24,19 +25,17 @@ export function initializeEmoji() {
} }
if (worker) { if (worker) {
// Assign worker to const to make TS happy inside the event listener.
const thisWorker = worker;
const timeoutId = setTimeout(() => { const timeoutId = setTimeout(() => {
log('worker is not ready after timeout'); log('worker is not ready after timeout');
worker = null; worker = null;
void fallbackLoad(); void fallbackLoad();
}, WORKER_TIMEOUT); }, WORKER_TIMEOUT);
thisWorker.addEventListener('message', (event: MessageEvent<string>) => { worker.addEventListener('message', (event: MessageEvent<string>) => {
const { data: message } = event; const { data: message } = event;
if (message === 'ready') { if (message === 'ready') {
log('worker ready, loading data'); log('worker ready, loading data');
clearTimeout(timeoutId); clearTimeout(timeoutId);
thisWorker.postMessage('custom'); messageWorker('custom');
void loadEmojiLocale(userLocale); void loadEmojiLocale(userLocale);
// Load English locale as well, because people are still used to // Load English locale as well, because people are still used to
// using it from before we supported other locales. // using it from before we supported other locales.
@ -55,20 +54,35 @@ export function initializeEmoji() {
async function fallbackLoad() { async function fallbackLoad() {
log('falling back to main thread for loading'); log('falling back to main thread for loading');
const { importCustomEmojiData } = await import('./loader'); const { importCustomEmojiData } = await import('./loader');
await importCustomEmojiData(); const emojis = await importCustomEmojiData();
if (emojis) {
log('loaded %d custom emojis', emojis.length);
}
await loadEmojiLocale(userLocale); await loadEmojiLocale(userLocale);
if (userLocale !== 'en') { if (userLocale !== 'en') {
await loadEmojiLocale('en'); await loadEmojiLocale('en');
} }
} }
export async function loadEmojiLocale(localeString: string) { async function loadEmojiLocale(localeString: string) {
const locale = toSupportedLocale(localeString); const locale = toSupportedLocale(localeString);
const { importEmojiData, localeToPath } = await import('./loader');
if (worker) { if (worker) {
worker.postMessage(locale); const path = await localeToPath(locale);
log('asking worker to load locale %s from %s', locale, path);
messageWorker(locale, path);
} else { } else {
const { importEmojiData } = await import('./loader'); const emojis = await importEmojiData(locale);
await importEmojiData(locale); if (emojis) {
log('loaded %d emojis to locale %s', emojis.length, locale);
}
}
}
function messageWorker(locale: LocaleOrCustom, path?: string) {
if (!worker) {
return;
} }
worker.postMessage({ locale, path });
} }

@ -8,44 +8,64 @@ import {
putLatestEtag, putLatestEtag,
} from './database'; } from './database';
import { toSupportedLocale, toSupportedLocaleOrCustom } from './locale'; import { toSupportedLocale, toSupportedLocaleOrCustom } from './locale';
import type { CustomEmojiData, LocaleOrCustom } from './types'; import type { CustomEmojiData } from './types';
import { emojiLogger } from './utils';
const log = emojiLogger('loader'); export async function importEmojiData(localeString: string, path?: string) {
export async function importEmojiData(localeString: string) {
const locale = toSupportedLocale(localeString); const locale = toSupportedLocale(localeString);
const emojis = await fetchAndCheckEtag<CompactEmoji[]>(locale);
// Validate the provided path.
if (path && !/^[/a-z]*\/packs\/assets\/compact-\w+\.json$/.test(path)) {
throw new Error('Invalid path for emoji data');
} else {
// Otherwise get the path if not provided.
path ??= await localeToPath(locale);
}
const emojis = await fetchAndCheckEtag<CompactEmoji[]>(locale, path);
if (!emojis) { if (!emojis) {
return; return;
} }
const flattenedEmojis: FlatCompactEmoji[] = flattenEmojiData(emojis); const flattenedEmojis: FlatCompactEmoji[] = flattenEmojiData(emojis);
log('loaded %d for %s locale', flattenedEmojis.length, locale);
await putEmojiData(flattenedEmojis, locale); await putEmojiData(flattenedEmojis, locale);
return flattenedEmojis;
} }
export async function importCustomEmojiData() { export async function importCustomEmojiData() {
const emojis = await fetchAndCheckEtag<CustomEmojiData[]>('custom'); const emojis = await fetchAndCheckEtag<CustomEmojiData[]>(
'custom',
'/api/v1/custom_emojis',
);
if (!emojis) { if (!emojis) {
return; return;
} }
log('loaded %d custom emojis', emojis.length);
await putCustomEmojiData(emojis); await putCustomEmojiData(emojis);
return emojis;
}
const modules = import.meta.glob<string>(
'../../../../../node_modules/emojibase-data/**/compact.json',
{
query: '?url',
import: 'default',
},
);
export function localeToPath(locale: Locale) {
const key = `../../../../../node_modules/emojibase-data/${locale}/compact.json`;
if (!modules[key] || typeof modules[key] !== 'function') {
throw new Error(`Unsupported locale: ${locale}`);
}
return modules[key]();
} }
async function fetchAndCheckEtag<ResultType extends object[]>( export async function fetchAndCheckEtag<ResultType extends object[]>(
localeOrCustom: LocaleOrCustom, localeString: string,
path: string,
): Promise<ResultType | null> { ): Promise<ResultType | null> {
const locale = toSupportedLocaleOrCustom(localeOrCustom); const locale = toSupportedLocaleOrCustom(localeString);
// Use location.origin as this script may be loaded from a CDN domain. // Use location.origin as this script may be loaded from a CDN domain.
const url = new URL(location.origin); const url = new URL(path, location.origin);
if (locale === 'custom') {
url.pathname = '/api/v1/custom_emojis';
} else {
const modulePath = await localeToPath(locale);
url.pathname = modulePath;
}
const oldEtag = await loadLatestEtag(locale); const oldEtag = await loadLatestEtag(locale);
const response = await fetch(url, { const response = await fetch(url, {
@ -60,38 +80,20 @@ async function fetchAndCheckEtag<ResultType extends object[]>(
} }
if (!response.ok) { if (!response.ok) {
throw new Error( throw new Error(
`Failed to fetch emoji data for ${localeOrCustom}: ${response.statusText}`, `Failed to fetch emoji data for ${locale}: ${response.statusText}`,
); );
} }
const data = (await response.json()) as ResultType; const data = (await response.json()) as ResultType;
if (!Array.isArray(data)) { if (!Array.isArray(data)) {
throw new Error( throw new Error(`Unexpected data format for ${locale}: expected an array`);
`Unexpected data format for ${localeOrCustom}: expected an array`,
);
} }
// Store the ETag for future requests // Store the ETag for future requests
const etag = response.headers.get('ETag'); const etag = response.headers.get('ETag');
if (etag) { if (etag) {
await putLatestEtag(etag, localeOrCustom); await putLatestEtag(etag, localeString);
} }
return data; return data;
} }
const modules = import.meta.glob<string>(
'../../../../../node_modules/emojibase-data/**/compact.json',
{
query: '?url',
import: 'default',
},
);
function localeToPath(locale: Locale) {
const key = `../../../../../node_modules/emojibase-data/${locale}/compact.json`;
if (!modules[key] || typeof modules[key] !== 'function') {
throw new Error(`Unsupported locale: ${locale}`);
}
return modules[key]();
}

@ -162,7 +162,7 @@ describe('loadEmojiDataToState', () => {
const dbCall = vi const dbCall = vi
.spyOn(db, 'loadEmojiByHexcode') .spyOn(db, 'loadEmojiByHexcode')
.mockRejectedValue(new db.LocaleNotLoadedError('en')); .mockRejectedValue(new db.LocaleNotLoadedError('en'));
vi.spyOn(loader, 'importEmojiData').mockResolvedValueOnce(); vi.spyOn(loader, 'importEmojiData').mockResolvedValueOnce(undefined);
const consoleCall = vi const consoleCall = vi
.spyOn(console, 'warn') .spyOn(console, 'warn')
.mockImplementationOnce(() => null); .mockImplementationOnce(() => null);

@ -1,18 +1,25 @@
import { importEmojiData, importCustomEmojiData } from './loader'; import { importCustomEmojiData, importEmojiData } from './loader';
addEventListener('message', handleMessage); addEventListener('message', handleMessage);
self.postMessage('ready'); // After the worker is ready, notify the main thread self.postMessage('ready'); // After the worker is ready, notify the main thread
function handleMessage(event: MessageEvent<string>) { function handleMessage(event: MessageEvent<{ locale: string; path?: string }>) {
const { data: locale } = event; const {
void loadData(locale); data: { locale, path },
} = event;
void loadData(locale, path);
} }
async function loadData(locale: string) { async function loadData(locale: string, path?: string) {
if (locale !== 'custom') { let importCount: number | undefined;
await importEmojiData(locale); if (locale === 'custom') {
importCount = (await importCustomEmojiData())?.length;
} else if (path) {
importCount = (await importEmojiData(locale, path))?.length;
} else { } else {
await importCustomEmojiData(); throw new Error('Path is required for loading locale emoji data');
}
if (importCount) {
self.postMessage(`loaded ${importCount} emojis into ${locale}`);
} }
self.postMessage(`loaded ${locale}`);
} }

@ -9,7 +9,6 @@ import { me, reduceMotion } from 'mastodon/initial_state';
import ready from 'mastodon/ready'; import ready from 'mastodon/ready';
import { store } from 'mastodon/store'; import { store } from 'mastodon/store';
import { initializeEmoji } from './features/emoji';
import { isProduction, isDevelopment } from './utils/environment'; import { isProduction, isDevelopment } from './utils/environment';
function main() { function main() {
@ -30,6 +29,7 @@ function main() {
}); });
} }
const { initializeEmoji } = await import('./features/emoji/index');
initializeEmoji(); initializeEmoji();
const root = createRoot(mountNode); const root = createRoot(mountNode);

Loading…
Cancel
Save