parent
34726e6096
commit
3aa3d40451
@ -0,0 +1,154 @@ |
||||
/* |
||||
* SPDX-License-Identifier: GPL-3.0 |
||||
* Aerocord, a vesktop fork for older microsoft NT releases such as NT 6.0, 6.1, 6.2 and 6.3.
|
||||
* Credits to vendicated and the rest of the vesktop contribuitors for making Vesktop! |
||||
*/ |
||||
|
||||
import { LiteralUnion } from "type-fest"; |
||||
|
||||
// Resolves a possibly nested prop in the form of "some.nested.prop" to type of T.some.nested.prop
|
||||
type ResolvePropDeep<T, P> = P extends `${infer Pre}.${infer Suf}` |
||||
? Pre extends keyof T |
||||
? ResolvePropDeep<T[Pre], Suf> |
||||
: any |
||||
: P extends keyof T |
||||
? T[P] |
||||
: any; |
||||
|
||||
/** |
||||
* The SettingsStore allows you to easily create a mutable store that |
||||
* has support for global and path-based change listeners. |
||||
*/ |
||||
export class SettingsStore<T extends object> { |
||||
private pathListeners = new Map<string, Set<(newData: any) => void>>(); |
||||
private globalListeners = new Set<(newData: T, path: string) => void>(); |
||||
|
||||
/** |
||||
* The store object. Making changes to this object will trigger the applicable change listeners |
||||
*/ |
||||
public declare store: T; |
||||
/** |
||||
* The plain data. Changes to this object will not trigger any change listeners |
||||
*/ |
||||
public declare plain: T; |
||||
|
||||
public constructor(plain: T) { |
||||
this.plain = plain; |
||||
this.store = this.makeProxy(plain); |
||||
} |
||||
|
||||
private makeProxy(object: any, root: T = object, path: string = "") { |
||||
const self = this; |
||||
|
||||
return new Proxy(object, { |
||||
get(target, key: string) { |
||||
const v = target[key]; |
||||
|
||||
if (typeof v === "object" && v !== null && !Array.isArray(v)) |
||||
return self.makeProxy(v, root, `${path}${path && "."}${key}`); |
||||
|
||||
return v; |
||||
}, |
||||
set(target, key: string, value) { |
||||
if (target[key] === value) return true; |
||||
|
||||
Reflect.set(target, key, value); |
||||
const setPath = `${path}${path && "."}${key}`; |
||||
|
||||
self.globalListeners.forEach(cb => cb(root, setPath)); |
||||
self.pathListeners.get(setPath)?.forEach(cb => cb(value)); |
||||
|
||||
return true; |
||||
} |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* Set the data of the store. |
||||
* This will update this.store and this.plain (and old references to them will be stale! Avoid storing them in variables) |
||||
* |
||||
* Additionally, all global listeners (and those for pathToNotify, if specified) will be called with the new data |
||||
* @param value New data |
||||
* @param pathToNotify Optional path to notify instead of globally. Used to transfer path via ipc |
||||
*/ |
||||
public setData(value: T, pathToNotify?: string) { |
||||
this.plain = value; |
||||
this.store = this.makeProxy(value); |
||||
|
||||
if (pathToNotify) { |
||||
let v = value; |
||||
|
||||
const path = pathToNotify.split("."); |
||||
for (const p of path) { |
||||
if (!v) { |
||||
console.warn( |
||||
`Settings#setData: Path ${pathToNotify} does not exist in new data. Not dispatching update` |
||||
); |
||||
return; |
||||
} |
||||
v = v[p]; |
||||
} |
||||
|
||||
this.pathListeners.get(pathToNotify)?.forEach(cb => cb(v)); |
||||
} |
||||
|
||||
this.globalListeners.forEach(cb => cb(value, "")); |
||||
} |
||||
|
||||
/** |
||||
* Add a global change listener, that will fire whenever any setting is changed |
||||
*/ |
||||
public addGlobalChangeListener(cb: (data: T, path: string) => void) { |
||||
this.globalListeners.add(cb); |
||||
} |
||||
|
||||
/** |
||||
* Add a scoped change listener that will fire whenever a setting matching the specified path is changed. |
||||
* |
||||
* For example if path is `"foo.bar"`, the listener will fire on |
||||
* ```js
|
||||
* Setting.store.foo.bar = "hi" |
||||
* ``` |
||||
* but not on |
||||
* ```js
|
||||
* Setting.store.foo.baz = "hi" |
||||
* ``` |
||||
* @param path |
||||
* @param cb |
||||
*/ |
||||
public addChangeListener<P extends LiteralUnion<keyof T, string>>( |
||||
path: P, |
||||
cb: (data: ResolvePropDeep<T, P>) => void |
||||
) { |
||||
const listeners = this.pathListeners.get(path as string) ?? new Set(); |
||||
listeners.add(cb); |
||||
this.pathListeners.set(path as string, listeners); |
||||
} |
||||
|
||||
/** |
||||
* Remove a global listener |
||||
* @see {@link addGlobalChangeListener} |
||||
*/ |
||||
public removeGlobalChangeListener(cb: (data: T, path: string) => void) { |
||||
this.globalListeners.delete(cb); |
||||
} |
||||
|
||||
/** |
||||
* Remove a scoped listener |
||||
* @see {@link addChangeListener} |
||||
*/ |
||||
public removeChangeListener(path: LiteralUnion<keyof T, string>, cb: (data: any) => void) { |
||||
const listeners = this.pathListeners.get(path as string); |
||||
if (!listeners) return; |
||||
|
||||
listeners.delete(cb); |
||||
if (!listeners.size) this.pathListeners.delete(path as string); |
||||
} |
||||
|
||||
/** |
||||
* Call all global change listeners |
||||
*/ |
||||
public markAsChanged() { |
||||
this.globalListeners.forEach(cb => cb(this.plain, "")); |
||||
} |
||||
} |
@ -0,0 +1,21 @@ |
||||
/* |
||||
* SPDX-License-Identifier: GPL-3.0 |
||||
* Aerocord, a vesktop fork for older microsoft NT releases such as NT 6.0, 6.1, 6.2 and 6.3.
|
||||
* Credits to vendicated and the rest of the vesktop contribuitors for making Vesktop! |
||||
*/ |
||||
|
||||
/** |
||||
* Returns a new function that will only be called after the given delay. |
||||
* Subsequent calls will cancel the previous timeout and start a new one from 0 |
||||
* |
||||
* Useful for grouping multiple calls into one |
||||
*/ |
||||
export function debounce<T extends Function>(func: T, delay = 300): T { |
||||
let timeout: NodeJS.Timeout; |
||||
return function (...args: any[]) { |
||||
clearTimeout(timeout); |
||||
timeout = setTimeout(() => { |
||||
func(...args); |
||||
}, delay); |
||||
} as any; |
||||
} |
@ -0,0 +1,13 @@ |
||||
/* |
||||
* SPDX-License-Identifier: GPL-3.0 |
||||
* Aerocord, a vesktop fork for older microsoft NT releases such as NT 6.0, 6.1, 6.2 and 6.3.
|
||||
* Credits to vendicated and the rest of the vesktop contribuitors for making Vesktop! |
||||
*/ |
||||
|
||||
export function isTruthy<T>(item: T): item is Exclude<T, 0 | "" | false | null | undefined> { |
||||
return Boolean(item); |
||||
} |
||||
|
||||
export function isNonNullish<T>(item: T): item is Exclude<T, null | undefined> { |
||||
return item != null; |
||||
} |
@ -0,0 +1,19 @@ |
||||
/* |
||||
* SPDX-License-Identifier: GPL-3.0 |
||||
* Aerocord, a vesktop fork for older microsoft NT releases such as NT 6.0, 6.1, 6.2 and 6.3.
|
||||
* Credits to vendicated and the rest of the vesktop contribuitors for making Vesktop! |
||||
*/ |
||||
|
||||
/** |
||||
* Wraps the given function so that it can only be called once |
||||
* @param fn Function to wrap |
||||
* @returns New function that can only be called once |
||||
*/ |
||||
export function once<T extends Function>(fn: T): T { |
||||
let called = false; |
||||
return function (this: any, ...args: any[]) { |
||||
if (called) return; |
||||
called = true; |
||||
return fn.apply(this, args); |
||||
} as any; |
||||
} |
@ -0,0 +1,9 @@ |
||||
/* |
||||
* SPDX-License-Identifier: GPL-3.0 |
||||
* Aerocord, a vesktop fork for older microsoft NT releases such as NT 6.0, 6.1, 6.2 and 6.3.
|
||||
* Credits to vendicated and the rest of the vesktop contribuitors for making Vesktop! |
||||
*/ |
||||
|
||||
export function sleep(ms: number): Promise<void> { |
||||
return new Promise(r => setTimeout(r, ms)); |
||||
} |
Loading…
Reference in new issue