fix: list admin plugins when the browser can't cache their code #1327

This commit is contained in:
Linus Rath
2026-10-24 22:37:16 +02:00
parent a49ab079ec
commit af07bb3cd3
2 changed files with 33 additions and 2 deletions
+18
View File
@@ -189,6 +189,24 @@ describe('usePluginStore', () => {
expect(p).toMatchObject({ managed: true, enabled: true, bundleHash: 'hash-v1' });
});
it('lists every server plugin when IndexedDB cannot be written (#1327)', async () => {
mocks.apiFetch.mockResolvedValue({
ok: true,
json: async () => ({ plugins: [serverPlugin, { ...serverPlugin, id: 'optional-plugin', forceEnabled: false }], themes: [] }),
});
vi.mocked(pluginStorage.getCode).mockRejectedValue(new Error('IndexedDB unavailable'));
vi.mocked(pluginStorage.saveCode).mockRejectedValueOnce(new Error('IndexedDB unavailable'));
vi.mocked(pluginStorage.saveCode).mockRejectedValueOnce(new Error('IndexedDB unavailable'));
vi.spyOn(console, 'warn').mockImplementation(() => {});
await usePluginStore.getState().initializePlugins();
const plugins = usePluginStore.getState().plugins;
expect(plugins.map(p => p.id)).toEqual(['managed-plugin', 'optional-plugin']);
expect(plugins[0]).toMatchObject({ managed: true, enabled: true });
expect(plugins[1]).toMatchObject({ managed: true, enabled: false });
});
it('re-downloads the bundle when the record is current but IndexedDB has no copy (#636)', async () => {
usePluginStore.setState({ plugins: [syncedRecord()] });
vi.mocked(pluginStorage.getCode).mockResolvedValue(null);
+15 -2
View File
@@ -524,7 +524,7 @@ async function syncServerPlugins(
// New server plugin - download bundle and install.
const code = await downloadPluginBundle(sp.id, sp.bundleHash, sp.tier);
if (!code) continue;
await pluginStorage.saveCode(sp.id, code);
await cacheManagedBundle(sp.id, code);
const plugin: InstalledPlugin = {
id: sp.id,
@@ -569,7 +569,7 @@ async function syncServerPlugins(
if (needsBundle) {
const code = await downloadPluginBundle(sp.id, sp.bundleHash, sp.tier);
if (!code) continue;
await pluginStorage.saveCode(sp.id, code);
await cacheManagedBundle(sp.id, code);
}
// Force-enable in the same pass when the server flips it on, so the user
@@ -625,6 +625,19 @@ async function downloadPluginBundle(pluginId: string, bundleHash?: string, tier?
}
}
/**
* Caching is best effort. Where IndexedDB can't be written (some private
* windows, storage disabled) the plugin must still be listed: the loader
* fetches a managed bundle again when no local copy exists (#1327).
*/
async function cacheManagedBundle(pluginId: string, code: string): Promise<void> {
try {
await pluginStorage.saveCode(pluginId, code);
} catch (err) {
console.warn(`[plugin-store] Could not cache bundle for "${pluginId}":`, err);
}
}
// ─── Server-side admin-approval helpers ───────────────────────
async function checkServerApproval(pluginId: string, bundleHash: string): Promise<{ status: 'pending' | 'approved' | 'denied' | 'not-requested' } | null> {