first commmit
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* Local-storage facade used by:
|
||||
* - Level unlock state (req 17.1)
|
||||
* - Floating control layout (req 17.2)
|
||||
* - BGM / SFX volume (req 17.3, 16.4)
|
||||
* - Tutorial completion flags (req 17.4)
|
||||
* - Story-intro seen flag (req 17.5 / 19.5)
|
||||
*
|
||||
* Rationale for the thin facade:
|
||||
* - The WeChat Mini Game runtime exposes `wx.setStorageSync` while the
|
||||
* in-editor / browser preview exposes `sys.localStorage`. We isolate
|
||||
* both behind a single `IStorageDriver` interface so that switching
|
||||
* platforms is a single line.
|
||||
* - On read, a failure never throws: it returns the provided default value
|
||||
* (req 17.6 — "must not crash if local storage is unreadable").
|
||||
* - All values go through JSON serialisation so that structured objects
|
||||
* round-trip without callers having to remember to stringify.
|
||||
*/
|
||||
|
||||
export interface IStorageDriver {
|
||||
getItem(key: string): string | null;
|
||||
setItem(key: string, value: string): void;
|
||||
removeItem(key: string): void;
|
||||
}
|
||||
|
||||
/** Selects the best available driver at runtime. */
|
||||
function detectDriver(): IStorageDriver {
|
||||
// 1. WeChat Mini Game global
|
||||
const wxGlobal = (globalThis as any).wx;
|
||||
if (wxGlobal && typeof wxGlobal.setStorageSync === 'function') {
|
||||
return {
|
||||
getItem(key) {
|
||||
try {
|
||||
const v = wxGlobal.getStorageSync(key);
|
||||
return v === '' ? null : (v as string);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
setItem(key, value) {
|
||||
try {
|
||||
wxGlobal.setStorageSync(key, value);
|
||||
} catch {
|
||||
// swallow — req 17.6
|
||||
}
|
||||
},
|
||||
removeItem(key) {
|
||||
try {
|
||||
wxGlobal.removeStorageSync(key);
|
||||
} catch {
|
||||
// swallow
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// 2. Browser localStorage
|
||||
if (typeof globalThis !== 'undefined' && (globalThis as any).localStorage) {
|
||||
const ls = (globalThis as any).localStorage as Storage;
|
||||
return {
|
||||
getItem: (k) => ls.getItem(k),
|
||||
setItem: (k, v) => ls.setItem(k, v),
|
||||
removeItem: (k) => ls.removeItem(k),
|
||||
};
|
||||
}
|
||||
|
||||
// 3. In-memory fallback (Jest, Node-only unit tests).
|
||||
const mem = new Map<string, string>();
|
||||
return {
|
||||
getItem: (k) => (mem.has(k) ? (mem.get(k) as string) : null),
|
||||
setItem: (k, v) => {
|
||||
mem.set(k, v);
|
||||
},
|
||||
removeItem: (k) => {
|
||||
mem.delete(k);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export class StorageMgr {
|
||||
private driver: IStorageDriver;
|
||||
|
||||
constructor(driver?: IStorageDriver) {
|
||||
this.driver = driver ?? detectDriver();
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a JSON-serialisable value. Returns `defaultValue` if the key is
|
||||
* missing, unparseable, or the underlying driver throws (req 17.6).
|
||||
*/
|
||||
public get<T>(key: string, defaultValue: T): T {
|
||||
try {
|
||||
const raw = this.driver.getItem(key);
|
||||
if (raw == null) {
|
||||
return defaultValue;
|
||||
}
|
||||
return JSON.parse(raw) as T;
|
||||
} catch {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
/** Write a JSON-serialisable value. Silently ignores driver errors. */
|
||||
public set<T>(key: string, value: T): void {
|
||||
try {
|
||||
this.driver.setItem(key, JSON.stringify(value));
|
||||
} catch {
|
||||
// req 17.6
|
||||
}
|
||||
}
|
||||
|
||||
public remove(key: string): void {
|
||||
this.driver.removeItem(key);
|
||||
}
|
||||
|
||||
/** Swap the driver at runtime. Used in unit tests and platform ports. */
|
||||
public setDriver(driver: IStorageDriver): void {
|
||||
this.driver = driver;
|
||||
}
|
||||
}
|
||||
|
||||
/** Shared project-wide storage manager. */
|
||||
export const globalStorageMgr = new StorageMgr();
|
||||
Reference in New Issue
Block a user