From 3aa3d40451c074c4ae53e00e3acbd616c9d5f832 Mon Sep 17 00:00:00 2001 From: aiek Date: Fri, 26 Jul 2024 19:18:17 +0300 Subject: [PATCH] Upload files to 'src/shared/utils' --- src/shared/utils/SettingsStore.ts | 154 ++++++++++++++++++++++++++++++ src/shared/utils/debounce.ts | 21 ++++ src/shared/utils/guards.ts | 13 +++ src/shared/utils/once.ts | 19 ++++ src/shared/utils/sleep.ts | 9 ++ 5 files changed, 216 insertions(+) create mode 100644 src/shared/utils/SettingsStore.ts create mode 100644 src/shared/utils/debounce.ts create mode 100644 src/shared/utils/guards.ts create mode 100644 src/shared/utils/once.ts create mode 100644 src/shared/utils/sleep.ts diff --git a/src/shared/utils/SettingsStore.ts b/src/shared/utils/SettingsStore.ts new file mode 100644 index 0000000..75ea565 --- /dev/null +++ b/src/shared/utils/SettingsStore.ts @@ -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 = P extends `${infer Pre}.${infer Suf}` + ? Pre extends keyof T + ? ResolvePropDeep + : 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 { + private pathListeners = new Map 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

>( + path: P, + cb: (data: ResolvePropDeep) => 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, 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, "")); + } +} diff --git a/src/shared/utils/debounce.ts b/src/shared/utils/debounce.ts new file mode 100644 index 0000000..4404561 --- /dev/null +++ b/src/shared/utils/debounce.ts @@ -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(func: T, delay = 300): T { + let timeout: NodeJS.Timeout; + return function (...args: any[]) { + clearTimeout(timeout); + timeout = setTimeout(() => { + func(...args); + }, delay); + } as any; +} diff --git a/src/shared/utils/guards.ts b/src/shared/utils/guards.ts new file mode 100644 index 0000000..cfce673 --- /dev/null +++ b/src/shared/utils/guards.ts @@ -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(item: T): item is Exclude { + return Boolean(item); +} + +export function isNonNullish(item: T): item is Exclude { + return item != null; +} diff --git a/src/shared/utils/once.ts b/src/shared/utils/once.ts new file mode 100644 index 0000000..1afcafd --- /dev/null +++ b/src/shared/utils/once.ts @@ -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(fn: T): T { + let called = false; + return function (this: any, ...args: any[]) { + if (called) return; + called = true; + return fn.apply(this, args); + } as any; +} diff --git a/src/shared/utils/sleep.ts b/src/shared/utils/sleep.ts new file mode 100644 index 0000000..7db3607 --- /dev/null +++ b/src/shared/utils/sleep.ts @@ -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 { + return new Promise(r => setTimeout(r, ms)); +}