Skip to content

Commit 5dfa92f

Browse files
committed
fix: onerror handler for css files to still unload old themes
refactor: Replace theme numbers with theme uuids for better cache avoidance
1 parent ee68c4c commit 5dfa92f

10 files changed

Lines changed: 49 additions & 85 deletions

File tree

package-lock.json

Lines changed: 9 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,6 @@
6969
"@types/react-router-dom": "5.1.7",
7070
"@types/react-virtualized": "9.22.2",
7171
"@types/tail": "2.0.0",
72-
"@types/uuid": "3.4.5",
7372
"@types/uuid-validate": "0.0.1",
7473
"@types/which": "1.3.2",
7574
"@types/ws": "8.18.1",
@@ -128,7 +127,7 @@
128127
"typesafe-actions": "4.4.2",
129128
"typescript": "^5.5.3",
130129
"typescript-eslint": "8.31.0",
131-
"uuid": "3.3.2",
130+
"uuid": "^13.0.0",
132131
"uuid-validate": "0.0.3",
133132
"vitest": "3.2.4",
134133
"which": "1.3.1",

src/back/extensions/ApiImplementation.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
runService,
2222
setStatus
2323
} from '@back/util/misc';
24+
import { uuid } from '@back/util/uuid';
2425
import { ExtSearchable } from '@fparchive/flashpoint-archive';
2526
import { BrowsePageLayout, ScreenshotPreviewMode } from '@shared/BrowsePageLayout';
2627
import { LogLevel } from '@shared/Log/interface';
@@ -39,7 +40,6 @@ import * as fsExtra from 'fs-extra';
3940
import * as fs from 'node:fs';
4041
import * as path from 'node:path';
4142
import * as stream from 'stream';
42-
import uuid from 'uuid';
4343
import { fpDatabase, loadCurationArchive } from '..';
4444
import { addPlaylistGame, deletePlaylist, deletePlaylistGame, filterPlaylists, findPlaylist, findPlaylistByName, getPlaylistGame, savePlaylistGame, updatePlaylist } from '../playlist';
4545
import { newExtLog } from './ExtensionUtils';

src/back/util/uuid.ts

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,5 @@
1-
import { randomBytes } from 'crypto';
2-
import * as guid from 'uuid/v4';
1+
import { v4 as uuidv4 } from 'uuid';
32

4-
/**
5-
* Wrapper function over uuid's v4 method that attempts to source
6-
* entropy using the window Crypto instance rather than through
7-
* Node.JS.
8-
*/
93
export function uuid() {
10-
return guid({ random: bufferToNumbers(randomBytes(16)) });
11-
}
12-
13-
function bufferToNumbers(buffer: Buffer): number[] {
14-
const array: number[] = [];
15-
for (let i = 0; i < buffer.length; i++) {
16-
array[i] = buffer[i];
17-
}
18-
return array;
4+
return uuidv4();
195
}

src/renderer/components/ThemeProvider.test.tsx

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { initialMainState, updateSystemThemeCss, updateThemeCss } from '@renderer/store/main/slice';
22
import { initialPreferencesState, updatePreferences } from '@renderer/store/preferences/slice';
33
import { getFileServerURL } from '@shared/Util';
4+
import { uuid } from '@shared/utils/uuid';
45
import { renderWithProviders } from '@test/redux';
56
import { useTestServer } from '@test/useTestServer';
67
import { ITheme } from 'flashpoint-launcher';
78
import { act } from 'react';
8-
import uuid from 'uuid';
99
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
1010
import { ThemeProvider } from './ThemeProvider';
1111

@@ -69,8 +69,9 @@ describe('ThemeProvider', () => {
6969
}
7070
});
7171

72+
let themeKey = store.getState().main.themeVersion;
7273
// Check theme element was created
73-
const expectedUrl = `${getFileServerURL()}/Themes/${themeList[0].id}/${themeList[0].entryPath}?v=0`;
74+
const expectedUrl = `${getFileServerURL()}/Themes/${themeList[0].id}/${themeList[0].entryPath}?v=${themeKey}`;
7475
const themeElement = container.ownerDocument.querySelector(`[data-theme="true"][href="${expectedUrl}"]`) as HTMLLinkElement;
7576
expect(themeElement).toBeInTheDocument();
7677
expect(themeElement.tagName).toBe('LINK');
@@ -85,7 +86,8 @@ describe('ThemeProvider', () => {
8586
}));
8687
});
8788

88-
const newExpectedUrl = `${getFileServerURL()}/Themes/${themeList[1].id}/${themeList[1].entryPath}?v=0`;
89+
themeKey = store.getState().main.themeVersion;
90+
const newExpectedUrl = `${getFileServerURL()}/Themes/${themeList[1].id}/${themeList[1].entryPath}?v=${themeKey}`;
8991
const updatedElement = container.ownerDocument.querySelector(`[data-theme="true"][href="${newExpectedUrl}"]`) as HTMLLinkElement;
9092
expect(updatedElement.getAttribute('href')).toBe(newExpectedUrl);
9193

@@ -94,7 +96,8 @@ describe('ThemeProvider', () => {
9496
store.dispatch(updateThemeCss());
9597
});
9698

97-
const newExpectedVerUrl = `${getFileServerURL()}/Themes/${themeList[1].id}/${themeList[1].entryPath}?v=1`;
99+
themeKey = store.getState().main.themeVersion;
100+
const newExpectedVerUrl = `${getFileServerURL()}/Themes/${themeList[1].id}/${themeList[1].entryPath}?v=${themeKey}`;
98101
const updatedVerElement = container.ownerDocument.querySelector(`[data-theme="true"][href="${newExpectedVerUrl}"]`) as HTMLLinkElement;
99102
expect(updatedVerElement.getAttribute('href')).toBe(newExpectedVerUrl);
100103

@@ -103,8 +106,9 @@ describe('ThemeProvider', () => {
103106
store.dispatch(updateSystemThemeCss());
104107
});
105108

106-
const coreHrefNew = `${coreHref}?v=1`;
107-
const fancyHrefNew = `${fancyHref}?v=1`;
109+
const systemThemeKey = store.getState().main.systemThemeVersion;
110+
const coreHrefNew = `${coreHref}?v=${systemThemeKey}`;
111+
const fancyHrefNew = `${fancyHref}?v=${systemThemeKey}`;
108112
const coreVerElement = container.ownerDocument.querySelector(`[data-corecss="true"][href="${coreHrefNew}"]`) as HTMLLinkElement;
109113
const fancyVerElement = container.ownerDocument.querySelector(`[data-fancycss="true"][href="${fancyHrefNew}"]`) as HTMLLinkElement;
110114
expect(coreVerElement).toBeInTheDocument();

src/renderer/components/ThemeProvider.tsx

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,19 @@ import { getFileServerURL } from '@shared/Util';
33
import { ITheme } from 'flashpoint-launcher';
44
import { PropsWithChildren, useEffect, useRef } from 'react';
55

6-
const globalThemeAttribute = 'data-theme';
7-
86
type ThemeProviderProps = PropsWithChildren;
97

108
export function ThemeProvider({ children }: ThemeProviderProps) {
119
const systemThemeVersion = useAppSelector(state => state.main.systemThemeVersion);
12-
const themeVersion = useAppSelector(state => state.main.themeVersion) + systemThemeVersion;
10+
const themeVersion = useAppSelector(state => state.main.themeVersion) + (systemThemeVersion || '');
1311
const availableThemes = useAppSelector(state => state.main.themeList);
1412
const currentTheme = useAppSelector(state => state.preferences.currentTheme);
1513
const coreHref = useRef(document.querySelector('[data-corecss="true"]')?.getAttribute('href') || undefined);
1614
const fancyHref = useRef(document.querySelector('[data-fancycss="true"]')?.getAttribute('href') || undefined);
1715

1816
// Update system theme when needed
1917
useEffect(() => {
20-
if (systemThemeVersion > 0) {
18+
if (systemThemeVersion) {
2119
// Don't need to update unless theme has incremented, first links are in raw HTML
2220
updateSystemThemeDom(systemThemeVersion, coreHref.current, fancyHref.current);
2321
}
@@ -33,7 +31,7 @@ export function ThemeProvider({ children }: ThemeProviderProps) {
3331
}
3432

3533
// Updates the System css links to force them to update with new version
36-
function updateSystemThemeDom(version: number, coreHref?: string, fancyHref?: string): void {
34+
function updateSystemThemeDom(version: string, coreHref?: string, fancyHref?: string): void {
3735
if (coreHref) {
3836
const existingElements = document.querySelectorAll('[data-corecss="true"]');
3937
const newElement = createThemeElement(`${coreHref}?v=${version}`);
@@ -69,17 +67,17 @@ function updateSystemThemeDom(version: number, coreHref?: string, fancyHref?: st
6967

7068

7169
// Updates the Theme css links to force them to update with new version, or with the newly selected them
72-
function updateThemeDom(version: number, theme?: ITheme): void {
70+
function updateThemeDom(version: string, theme?: ITheme): void {
7371
const url = theme ? `${getFileServerURL()}/Themes/${theme.id}/${theme.entryPath}?v=${version}` : undefined;
7472
replaceThemeElement(url);
7573
}
7674

7775
function replaceThemeElement(url?: string) {
7876
// Get list of old theme elems to remove after loading new theme elem
79-
const existingElements = document.head.querySelectorAll(`[${globalThemeAttribute}="true"]`);
77+
const existingElements = document.head.querySelectorAll('[data-theme="true"]');
8078
if (url) {
8179
const newElement = createThemeElement(url);
82-
newElement.setAttribute(globalThemeAttribute, 'true');
80+
newElement.setAttribute('data-theme', 'true');
8381
newElement.onload = () => {
8482
existingElements.forEach((elem) => {
8583
try {
@@ -89,6 +87,16 @@ function replaceThemeElement(url?: string) {
8987
}
9088
});
9189
};
90+
newElement.onerror = () => {
91+
console.error('Failed to load theme:', url);
92+
existingElements.forEach((elem) => {
93+
try {
94+
elem.remove();
95+
} catch {
96+
// Ignore, may have been removed earlier
97+
}
98+
});
99+
};
92100
if (document.head) { document.head.appendChild(newElement); }
93101
} else {
94102
existingElements.forEach((elem) => {

src/renderer/store/main/slice.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit';
22
import { BackIn, BackInit } from '@shared/back/types';
33
import { createLangContainer } from '@shared/lang';
44
import { deepCopy, recursiveReplace } from '@shared/Util';
5+
import { uuid } from '@shared/utils/uuid';
56
import { CreditsData, DialogFieldProps, DialogState, Game, GameMetadataSource, IExtensionDescription, IService, Playlist } from 'flashpoint-launcher';
67
import { CustomRoute, DisplaySettings, DisplaySettingsGameSidebarAction, DynamicPageProps, ExtConfigValueAction, ExtOrderable, MainState, UnrecoverableError } from 'flashpoint-launcher-renderer';
78

@@ -123,8 +124,8 @@ export function initialMainState(): MainState {
123124
},
124125
loadedAll: false,
125126
themeList: [],
126-
themeVersion: 0,
127-
systemThemeVersion: 0,
127+
themeVersion: uuid(),
128+
systemThemeVersion: null,
128129
logoSets: [],
129130
logoVersion: 0,
130131
gamesTotal: -1,
@@ -411,10 +412,10 @@ const mainSlice = createSlice({
411412
state.unrecoverableError = payload;
412413
},
413414
updateThemeCss(state: MainState) {
414-
state.themeVersion += 1;
415+
state.themeVersion = uuid();
415416
},
416417
updateSystemThemeCss(state: MainState) {
417-
state.systemThemeVersion += 1;
418+
state.systemThemeVersion = uuid();
418419
},
419420
setExtConfigValue(state: MainState, { payload }: PayloadAction<ExtConfigValueAction>) {
420421
state.extConfig[payload.key] = payload.value;

src/shared/utils/uuid.ts

Lines changed: 2 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,7 @@
1-
// Work around synchronously seeding of random buffer in the v1
2-
// version of uuid by explicitly only requiring v4. As far as I'm
3-
// aware we cannot use an import statement here without causing webpack
4-
// to load the v1 version as well.
5-
//
6-
// See
7-
// https://github.com/kelektiv/node-uuid/issues/189
8-
import * as guid from 'uuid/v4';
1+
import { v4 as uuidv4 } from 'uuid';
92

10-
/**
11-
* Fills a buffer with the required number of random bytes.
12-
*
13-
* Attempt to use the Chromium-provided crypto library rather than
14-
* Node.JS. For some reason the Node.JS randomBytes function adds
15-
* _considerable_ (1s+) synchronous load time to the start up.
16-
*
17-
* See
18-
* https://developer.mozilla.org/en-US/docs/Web/API/Window/crypto
19-
* https://github.com/kelektiv/node-uuid/issues/189
20-
*
21-
* @param count Number of bytes to return
22-
*/
23-
function getRandomBytes(count: number): Buffer {
24-
const rndBuf = new Uint8Array(count);
25-
crypto.getRandomValues(rndBuf);
26-
return Buffer.from(rndBuf.buffer);
27-
}
28-
29-
/**
30-
* Wrapper function over uuid's v4 method that attempts to source
31-
* entropy using the window Crypto instance rather than through
32-
* Node.JS.
33-
*/
343
export function uuid() {
35-
return (guid as any as (options?: { random?: Buffer }) => string)({ random: getRandomBytes(16) });
4+
return uuidv4();
365
}
376

387
/**

src/test/mocks/curate.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1+
import { uuid } from '@shared/utils/uuid';
12
import { CurationState } from 'flashpoint-launcher';
2-
import uuid from 'uuid';
33

44
export function mockCuration(): CurationState {
55
const id = uuid();

typings/flashpoint-launcher.d.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3268,8 +3268,8 @@ declare module 'flashpoint-launcher-renderer' {
32683268
loadedAll: boolean;
32693269
extensions: IExtensionDescription[];
32703270
themeList: ITheme[];
3271-
systemThemeVersion: number;
3272-
themeVersion: number;
3271+
systemThemeVersion: string | null;
3272+
themeVersion: string;
32733273
logoSets: ILogoSet[];
32743274
logoVersion: number; // Increase to force cache clear
32753275
gamesTotal: number;

0 commit comments

Comments
 (0)