-
-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathstorage.ts
More file actions
96 lines (84 loc) · 3.62 KB
/
Copy pathstorage.ts
File metadata and controls
96 lines (84 loc) · 3.62 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
import { storage, type StorageItemKey, type WxtStorageItem } from '#imports';
import { DEFAULT_SETTINGS, DEFAULT_UI_PREFS } from '../model/defaults';
import type {
EqPreset,
FavoriteEntry,
HistoryEntry,
Settings,
TrackData,
UiPrefs,
} from '../model/types';
/** Rebuild a value as plain arrays/objects, stripping any Svelte `$state`
* proxies on the way.
*
* Chrome serializes storage writes to JSON and reads straight through a proxy;
* Firefox structured-clones them and throws DataCloneError, rejecting the write
* with nothing persisted. Panel stores are expected to `$state.snapshot` before
* writing, but one missed call is an invisible, browser-specific data-loss bug —
* so every write goes through here as well.
*
* A rebuild, not `structuredClone`: that throws on a proxy, which is the very
* case being defended against. Safe because this schema is JSON-shaped
* throughout (numbers, strings, booleans, arrays, plain objects); a Date, Map or
* typed array added later would need handling here first.
*
* Plain function, not the `$state.snapshot` rune: background.ts imports this
* module and runes only compile inside Svelte files. */
function toPlain<T>(value: T): T {
if (Array.isArray(value)) return value.map(toPlain) as T;
if (value === null || typeof value !== 'object') return value;
const out: Record<string, unknown> = {};
for (const [k, v] of Object.entries(value)) out[k] = toPlain(v);
return out as T;
}
/** `storage.defineItem` with proxy-stripping on every write. Annotated rather
* than inferred: `storage.defineItem` is overloaded five ways, so deriving this
* signature from it makes the inference circular. */
function defineItem<T>(
key: StorageItemKey,
options: { fallback: T },
): WxtStorageItem<T, Record<string, unknown>> {
const item = storage.defineItem<T>(key, options);
const setValue = item.setValue.bind(item);
item.setValue = (value: T) => setValue(toPlain(value));
return item;
}
export const settingsItem = defineItem<Settings>('local:settings', {
fallback: DEFAULT_SETTINGS,
});
export const uiPrefsItem = defineItem<UiPrefs>('local:uiPrefs', {
fallback: DEFAULT_UI_PREFS,
});
/** Recent history (Auto Save), newest first. */
export const historyItem = defineItem<HistoryEntry[]>('local:history', {
fallback: [],
});
/** Starred songs (History → Favorites). Array order = manual sort order. */
export const favoritesItem = defineItem<FavoriteEntry[]>('local:favorites', {
fallback: [],
});
/** EQ curves the user saved (Equalizer → preset row). Array order = save order.
* Kept out of `settings` so Reset Settings can't wipe them. */
export const eqPresetsItem = defineItem<EqPreset[]>('local:eqPresets', {
fallback: [],
});
/** Origins the user has granted host permission for (mirrors permissions API,
* used to show/revoke the list without a permissions query round-trip). */
export const grantedOriginsItem = defineItem<string[]>('local:grantedOrigins', {
fallback: [],
});
/** Per-track markers/snippets, keyed by TrackIdentity.key. */
export function trackDataKey(key: string) {
return `local:track:${key}` as const;
}
export async function loadTrackData(key: string): Promise<TrackData | null> {
return (await storage.getItem<TrackData>(trackDataKey(key))) ?? null;
}
export async function saveTrackData(data: TrackData): Promise<void> {
await storage.setItem(trackDataKey(data.identity.key), toPlain(data));
}
export async function removeAllTrackData(): Promise<void> {
const snapshot = await browser.storage.local.get(null);
const keys = Object.keys(snapshot).filter((k) => k.startsWith('track:'));
if (keys.length) await browser.storage.local.remove(keys);
}