87 lines
3.4 KiB
TypeScript
87 lines
3.4 KiB
TypeScript
// src/main/config-effects.ts
|
|
// 설정 키별 부수효과 레지스트리 — 어느 경로로 바뀌든(범용 config:set, 전용 IPC, 원격 동기화)
|
|
// 같은 효과가 한 번씩 적용되게 한다.
|
|
//
|
|
// 예전엔 효과가 입력 경로에 붙어 있었다: 테마 팝업 다시 칠하기는 SET_THEME 와 원격 동기화 경로에만,
|
|
// OS 로그인 항목 갱신은 SET_AUTO_LAUNCH 에만 있었다. 설정 화면은 범용 config:set 을 써서 두 효과가
|
|
// 모두 빠졌다(자동 실행이 재부팅 뒤 동작하지 않고, 미리 로드된 팝업이 이전 테마로 남았다).
|
|
// 이제 효과는 설정 변경 이벤트 하나를 구독하는 이 레지스트리에 등록한다(OCP — 새 키는 등록만 한다).
|
|
// Electron 에 의존하지 않는다: 구독 함수와 효과는 주입받는다(테스트는 가짜를 넘긴다).
|
|
|
|
export interface ConfigChange {
|
|
key: string
|
|
value: unknown
|
|
previousValue?: unknown
|
|
}
|
|
|
|
export type ConfigEffect = (value: unknown, previousValue: unknown) => void
|
|
|
|
export interface ConfigEffectsDeps {
|
|
/** 설정 변경 구독 (ConfigService.onConfigChanged) — 해지 함수를 돌려준다 */
|
|
subscribe: (listener: (change: ConfigChange) => void) => () => void
|
|
/** 효과가 던진 오류 — 다른 효과와 설정 저장은 계속된다 */
|
|
onError?: (key: string, error: unknown) => void
|
|
}
|
|
|
|
function isPrimitive(value: unknown): boolean {
|
|
return value === null || (typeof value !== 'object' && typeof value !== 'function')
|
|
}
|
|
|
|
/**
|
|
* 값이 실제로 바뀌었는가. 원시값은 같으면 건너뛴다 — 효과 안에서 같은 값을 다시 저장해도
|
|
* (AutoLaunchService.setEnabled 는 적용 후 configSet 을 부른다) 재귀하지 않는다.
|
|
* 객체·배열은 비교하지 않고 적용한다(효과는 멱등이어야 한다).
|
|
*/
|
|
export function isEffectiveChange(change: ConfigChange): boolean {
|
|
if (isPrimitive(change.value) && isPrimitive(change.previousValue)) {
|
|
return !Object.is(change.value, change.previousValue)
|
|
}
|
|
return true
|
|
}
|
|
|
|
export class ConfigEffects {
|
|
private readonly _effects = new Map<string, ConfigEffect[]>()
|
|
private _unsubscribe: (() => void) | null = null
|
|
|
|
constructor(private readonly _deps: ConfigEffectsDeps) {}
|
|
|
|
/** 키에 효과를 등록한다. 돌려받은 함수로 등록을 푼다. */
|
|
register(key: string, effect: ConfigEffect): () => void {
|
|
const list = this._effects.get(key) ?? []
|
|
list.push(effect)
|
|
this._effects.set(key, list)
|
|
return () => {
|
|
const current = this._effects.get(key)
|
|
if (!current) return
|
|
const next = current.filter((e) => e !== effect)
|
|
if (next.length > 0) this._effects.set(key, next)
|
|
else this._effects.delete(key)
|
|
}
|
|
}
|
|
|
|
/** 설정 변경 구독을 시작한다(멱등). */
|
|
start(): void {
|
|
if (this._unsubscribe) return
|
|
this._unsubscribe = this._deps.subscribe((change) => this.dispatch(change))
|
|
}
|
|
|
|
dispose(): void {
|
|
this._unsubscribe?.()
|
|
this._unsubscribe = null
|
|
this._effects.clear()
|
|
}
|
|
|
|
/** 변경 하나를 등록된 효과에 전달한다(테스트·직접 호출용). */
|
|
dispatch(change: ConfigChange): void {
|
|
const effects = this._effects.get(change.key)
|
|
if (!effects || effects.length === 0) return
|
|
if (!isEffectiveChange(change)) return
|
|
for (const effect of [...effects]) {
|
|
try {
|
|
effect(change.value, change.previousValue)
|
|
} catch (err) {
|
|
this._deps.onError?.(change.key, err)
|
|
}
|
|
}
|
|
}
|
|
}
|