@sv443-network/coreutils 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,159 @@
1
+ /**
2
+ * @module DataStore
3
+ * This module contains the DataStore class, which is a general purpose, sync and async persistent database for JSON-serializable data - [see the documentation for more info](https://github.com/Sv443-Network/CoreUtils/blob/main/docs.md#class-datastore)
4
+ */
5
+ import type { DataStoreEngine } from "./DataStoreEngine.js";
6
+ import type { LooseUnion, Prettify, SerializableVal } from "./types.js";
7
+ /** Function that takes the data in the old format and returns the data in the new format. Also supports an asynchronous migration. */
8
+ type MigrationFunc = (oldData: any) => any | Promise<any>;
9
+ /** Dictionary of format version numbers and the function that migrates to them from the previous whole integer */
10
+ export type DataMigrationsDict = Record<number, MigrationFunc>;
11
+ /** Options for the DataStore instance */
12
+ export type DataStoreOptions<TData extends DataStoreData> = Prettify<{
13
+ /**
14
+ * A unique internal ID for this data store.
15
+ * To avoid conflicts with other scripts, it is recommended to use a prefix that is unique to your script.
16
+ * If you want to change the ID, you should make use of the {@linkcode DataStore.migrateId()} method.
17
+ */
18
+ id: string;
19
+ /**
20
+ * The default data object to use if no data is saved in persistent storage yet.
21
+ * Until the data is loaded from persistent storage with {@linkcode DataStore.loadData()}, this will be the data returned by {@linkcode DataStore.getData()}.
22
+ *
23
+ * - ⚠️ This has to be an object that can be serialized to JSON using `JSON.stringify()`, so no functions or circular references are allowed, they will cause unexpected behavior.
24
+ */
25
+ defaultData: TData;
26
+ /**
27
+ * An incremental, whole integer version number of the current format of data.
28
+ * If the format of the data is changed in any way, this number should be incremented, in which case all necessary functions of the migrations dictionary will be run consecutively.
29
+ *
30
+ * - ⚠️ Never decrement this number and optimally don't skip any numbers either!
31
+ */
32
+ formatVersion: number;
33
+ /**
34
+ * The engine middleware to use for persistent storage.
35
+ * Create an instance of {@linkcode FileStorageEngine} (Node.js), {@linkcode BrowserStorageEngine} (DOM) or your own engine class that extends {@linkcode DataStoreEngine} and pass it here.
36
+ *
37
+ * ⚠️ Don't reuse the same engine instance for multiple DataStores, unless it explicitly supports it!
38
+ */
39
+ engine: (() => DataStoreEngine<TData>) | DataStoreEngine<TData>;
40
+ /**
41
+ * A dictionary of functions that can be used to migrate data from older versions to newer ones.
42
+ * The keys of the dictionary should be the format version that the functions can migrate to, from the previous whole integer value.
43
+ * The values should be functions that take the data in the old format and return the data in the new format.
44
+ * The functions will be run in order from the oldest to the newest version.
45
+ * If the current format version is not in the dictionary, no migrations will be run.
46
+ */
47
+ migrations?: DataMigrationsDict;
48
+ /**
49
+ * If an ID or multiple IDs are passed here, the data will be migrated from the old ID(s) to the current ID.
50
+ * This will happen once per page load, when {@linkcode DataStore.loadData()} is called.
51
+ * All future calls to {@linkcode DataStore.loadData()} in the session will not check for the old ID(s) anymore.
52
+ * To migrate IDs manually, use the method {@linkcode DataStore.migrateId()} instead.
53
+ */
54
+ migrateIds?: string | string[];
55
+ } & ({
56
+ encodeData?: never;
57
+ decodeData?: never;
58
+ /**
59
+ * The format to use for compressing the data. Defaults to `deflate-raw`. Explicitly set to `null` to store data uncompressed.
60
+ * ⚠️ Use either this, or `encodeData` and `decodeData`, but not all three at a time!
61
+ */
62
+ compressionFormat?: CompressionFormat | null;
63
+ } | {
64
+ /**
65
+ * Tuple of a compression format identifier and a function to use to encode the data prior to saving it in persistent storage.
66
+ * If this is specified, `compressionFormat` can't be used. Also make sure to declare {@linkcode decodeData()} as well.
67
+ *
68
+ * You can make use of the [`compress()` function](https://github.com/Sv443-Network/CoreUtils/blob/main/docs.md#function-compress) here to make the data use up less space at the cost of a little bit of performance.
69
+ * @param data The input data as a serialized object (JSON string)
70
+ */
71
+ encodeData: [format: LooseUnion<CompressionFormat>, encode: (data: string) => string | Promise<string>];
72
+ /**
73
+ * Tuple of a compression format identifier and a function to use to decode the data after reading it from persistent storage.
74
+ * If this is specified, `compressionFormat` can't be used. Also make sure to declare {@linkcode encodeData()} as well.
75
+ *
76
+ * You can make use of the [`decompress()` function](https://github.com/Sv443-Network/CoreUtils/blob/main/docs.md#function-decompress) here to make the data use up less space at the cost of a little bit of performance.
77
+ * @returns The resulting data as a valid serialized object (JSON string)
78
+ */
79
+ decodeData: [format: LooseUnion<CompressionFormat>, decode: (data: string) => string | Promise<string>];
80
+ compressionFormat?: never;
81
+ })>;
82
+ /** Generic type that represents the serializable data structure saved in a {@linkcode DataStore} instance. */
83
+ export type DataStoreData<TData extends SerializableVal = SerializableVal> = Record<string, SerializableVal | TData>;
84
+ /**
85
+ * Manages a hybrid synchronous & asynchronous persistent JSON database that is cached in memory and persistently saved across sessions using one of the preset DataStoreEngines or your own one.
86
+ * Supports migrating data from older format versions to newer ones and populating the cache with default data if no persistent data is found.
87
+ * Can be overridden to implement any other storage method.
88
+ *
89
+ * All methods are `protected` or `public`, so you can easily extend this class and overwrite them to use a different storage method or to add other functionality.
90
+ * Remember that you can use `super.methodName()` in the subclass to call the original method if needed.
91
+ *
92
+ * - ⚠️ The data is stored as a JSON string, so only data compatible with [`JSON.stringify()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify) can be used. Circular structures and complex objects (containing functions, symbols, etc.) will either throw an error on load and save or cause otherwise unexpected behavior. Properties with a value of `undefined` will be removed from the data prior to saving it, so use `null` instead.
93
+ * - ⚠️ Make sure to call {@linkcode loadData()} at least once after creating an instance, or the returned data will be the same as `options.defaultData`
94
+ *
95
+ * @template TData The type of the data that is saved in persistent storage for the currently set format version (FIXME:will be automatically inferred from `defaultData` if not provided)
96
+ */
97
+ export declare class DataStore<TData extends DataStoreData> {
98
+ readonly id: string;
99
+ readonly formatVersion: number;
100
+ readonly defaultData: TData;
101
+ readonly encodeData: DataStoreOptions<TData>["encodeData"];
102
+ readonly decodeData: DataStoreOptions<TData>["decodeData"];
103
+ readonly compressionFormat = "deflate-raw";
104
+ readonly engine: DataStoreEngine<TData>;
105
+ protected firstInit: boolean;
106
+ private cachedData;
107
+ private migrations?;
108
+ private migrateIds;
109
+ /**
110
+ * Creates an instance of DataStore to manage a sync & async database that is cached in memory and persistently saved across sessions.
111
+ * Supports migrating data from older versions to newer ones and populating the cache with default data if no persistent data is found.
112
+ *
113
+ * - ⚠️ Requires the directives `@grant GM.getValue` and `@grant GM.setValue` if the storageMethod is left as the default of `"GM"`
114
+ * - ⚠️ Make sure to call {@linkcode loadData()} at least once after creating an instance, or the returned data will be the same as `options.defaultData`
115
+ *
116
+ * @template TData The type of the data that is saved in persistent storage for the currently set format version (will be automatically inferred from `defaultData` if not provided) - **This has to be a JSON-compatible object!** (no undefined, circular references, etc.)
117
+ * @param opts The options for this DataStore instance
118
+ */
119
+ constructor(opts: DataStoreOptions<TData>);
120
+ /**
121
+ * Loads the data saved in persistent storage into the in-memory cache and also returns a copy of it.
122
+ * Automatically populates persistent storage with default data if it doesn't contain any data yet.
123
+ * Also runs all necessary migration functions if the data format has changed since the last time the data was saved.
124
+ */
125
+ loadData(): Promise<TData>;
126
+ /**
127
+ * Returns a copy of the data from the in-memory cache.
128
+ * Use {@linkcode loadData()} to get fresh data from persistent storage (usually not necessary since the cache should always exactly reflect persistent storage).
129
+ */
130
+ getData(): TData;
131
+ /** Saves the data synchronously to the in-memory cache and asynchronously to the persistent storage */
132
+ setData(data: TData): Promise<void>;
133
+ /** Saves the default data passed in the constructor synchronously to the in-memory cache and asynchronously to persistent storage */
134
+ saveDefaultData(): Promise<void>;
135
+ /**
136
+ * Call this method to clear all persistently stored data associated with this DataStore instance.
137
+ * The in-memory cache will be left untouched, so you may still access the data with {@linkcode getData()}
138
+ * Calling {@linkcode loadData()} or {@linkcode setData()} after this method was called will recreate persistent storage with the cached or default data.
139
+ *
140
+ * - ⚠️ This requires the additional directive `@grant GM.deleteValue` if the storageMethod is left as the default of `"GM"`
141
+ */
142
+ deleteData(): Promise<void>;
143
+ /** Returns whether encoding and decoding are enabled for this DataStore instance */
144
+ encodingEnabled(): this is Required<Pick<DataStoreOptions<TData>, "encodeData" | "decodeData">>;
145
+ /**
146
+ * Runs all necessary migration functions consecutively and saves the result to the in-memory cache and persistent storage and also returns it.
147
+ * This method is automatically called by {@linkcode loadData()} if the data format has changed since the last time the data was saved.
148
+ * Though calling this method manually is not necessary, it can be useful if you want to run migrations for special occasions like a user importing potentially outdated data that has been previously exported.
149
+ *
150
+ * If one of the migrations fails, the data will be reset to the default value if `resetOnError` is set to `true` (default). Otherwise, an error will be thrown and no data will be saved.
151
+ */
152
+ runMigrations(oldData: unknown, oldFmtVer: number, resetOnError?: boolean): Promise<TData>;
153
+ /**
154
+ * Tries to migrate the currently saved persistent data from one or more old IDs to the ID set in the constructor.
155
+ * If no data exist for the old ID(s), nothing will be done, but some time may still pass trying to fetch the non-existent data.
156
+ */
157
+ migrateId(oldIds: string | string[]): Promise<void>;
158
+ }
159
+ export {};
@@ -0,0 +1,89 @@
1
+ /**
2
+ * @module DataStoreEngine
3
+ * This module contains the `DataStoreEngine` class and some of its subclasses like `FileStorageEngine` and `BrowserStorageEngine`.
4
+ * [See the documentation for more info.](https://github.com/Sv443-Network/CoreUtils/blob/main/docs.md#class-datastoreengine)
5
+ */
6
+ import type { DataStoreData, DataStoreOptions } from "./DataStore.js";
7
+ import type { SerializableVal } from "./types.js";
8
+ /**
9
+ * Base class for creating {@linkcode DataStore} storage engines.
10
+ * This acts as an interchangeable API for writing and reading persistent data in various environments.
11
+ */
12
+ export declare abstract class DataStoreEngine<TData extends DataStoreData> {
13
+ protected dataStoreOptions: DataStoreOptions<TData>;
14
+ /** Called by DataStore on creation, to pass its options */
15
+ setDataStoreOptions(dataStoreOptions: DataStoreOptions<TData>): void;
16
+ /** Fetches a value from persistent storage */
17
+ abstract getValue<TValue extends SerializableVal = string>(name: string, defaultValue: TValue): Promise<string | TValue>;
18
+ /** Sets a value in persistent storage */
19
+ abstract setValue(name: string, value: SerializableVal): Promise<void>;
20
+ /** Deletes a value from persistent storage */
21
+ abstract deleteValue(name: string): Promise<void>;
22
+ /** Serializes the given object to a string, optionally encoded with `options.encodeData` if {@linkcode useEncoding} is set to true */
23
+ serializeData(data: TData, useEncoding?: boolean): Promise<string>;
24
+ /** Deserializes the given string to a JSON object, optionally decoded with `options.decodeData` if {@linkcode useEncoding} is set to true */
25
+ deserializeData(data: string, useEncoding?: boolean): Promise<TData>;
26
+ /**
27
+ * Copies a JSON-compatible object and loses all its internal references in the process.
28
+ * Uses [`structuredClone()`](https://developer.mozilla.org/en-US/docs/Web/API/structuredClone) if available, otherwise falls back to `JSON.parse(JSON.stringify(obj))`.
29
+ */
30
+ deepCopy<T>(obj: T): T;
31
+ }
32
+ /** Options for the {@linkcode BrowserStorageEngine} class */
33
+ export type BrowserStorageEngineOptions = {
34
+ /** Whether to store the data in LocalStorage (default) or SessionStorage */
35
+ type?: "localStorage" | "sessionStorage";
36
+ };
37
+ /**
38
+ * Storage engine for the {@linkcode DataStore} class that uses the browser's LocalStorage or SessionStorage to store data.
39
+ *
40
+ * ⚠️ Requires a DOM environment
41
+ * ⚠️ Don't reuse this engine across multiple {@linkcode DataStore} instances
42
+ */
43
+ export declare class BrowserStorageEngine<TData extends DataStoreData> extends DataStoreEngine<TData> {
44
+ protected options: Required<BrowserStorageEngineOptions>;
45
+ /**
46
+ * Creates an instance of `BrowserStorageEngine`.
47
+ *
48
+ * ⚠️ Requires a DOM environment
49
+ * ⚠️ Don't reuse this engine across multiple {@linkcode DataStore} instances
50
+ */
51
+ constructor(options?: BrowserStorageEngineOptions);
52
+ /** Fetches a value from persistent storage */
53
+ getValue<TValue extends SerializableVal = string>(name: string, defaultValue: TValue): Promise<string | TValue>;
54
+ /** Sets a value in persistent storage */
55
+ setValue(name: string, value: SerializableVal): Promise<void>;
56
+ /** Deletes a value from persistent storage */
57
+ deleteValue(name: string): Promise<void>;
58
+ }
59
+ /** Options for the {@linkcode FileStorageEngine} class */
60
+ export type FileStorageEngineOptions = {
61
+ /** Function that returns a string or a plain string that is the data file path, including name and extension. Defaults to `.ds-${dataStoreID}` */
62
+ filePath?: ((dataStoreID: string) => string) | string;
63
+ };
64
+ /**
65
+ * Storage engine for the {@linkcode DataStore} class that uses a JSON file to store data.
66
+ *
67
+ * ⚠️ Requires Node.js or Deno with Node compatibility
68
+ * ⚠️ Don't reuse this engine across multiple {@linkcode DataStore} instances
69
+ */
70
+ export declare class FileStorageEngine<TData extends DataStoreData> extends DataStoreEngine<TData> {
71
+ protected options: Required<FileStorageEngineOptions>;
72
+ /**
73
+ * Creates an instance of `FileStorageEngine`.
74
+ *
75
+ * ⚠️ Requires Node.js or Deno with Node compatibility
76
+ * ⚠️ Don't reuse this engine across multiple {@linkcode DataStore} instances
77
+ */
78
+ constructor(options?: FileStorageEngineOptions);
79
+ /** Reads the file contents */
80
+ protected readFile(): Promise<TData | undefined>;
81
+ /** Overwrites the file contents */
82
+ protected writeFile(data: TData): Promise<void>;
83
+ /** Fetches a value from persistent storage */
84
+ getValue<TValue extends SerializableVal = string>(name: string, defaultValue: TValue): Promise<string | TValue>;
85
+ /** Sets a value in persistent storage */
86
+ setValue<TValue extends SerializableVal = string>(name: string, value: TValue): Promise<void>;
87
+ /** Deletes a value from persistent storage */
88
+ deleteValue(name: string): Promise<void>;
89
+ }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * @module DataStoreSerializer
3
+ * This module contains the DataStoreSerializer class, which allows you to import and export serialized DataStore data - [see the documentation for more info](https://github.com/Sv443-Network/UserUtils/blob/main/docs.md#datastoreserializer)
4
+ */
5
+ import type { DataStore, DataStoreData } from "./DataStore.js";
6
+ /** Options for the DataStoreSerializer class */
7
+ export type DataStoreSerializerOptions = {
8
+ /** Whether to add a checksum to the exported data. Defaults to `true` */
9
+ addChecksum?: boolean;
10
+ /** Whether to ensure the integrity of the data when importing it by throwing an error (doesn't throw when the checksum property doesn't exist). Defaults to `true` */
11
+ ensureIntegrity?: boolean;
12
+ };
13
+ /** Meta object and serialized data of a DataStore instance */
14
+ export type SerializedDataStore = {
15
+ /** The ID of the DataStore instance */
16
+ id: string;
17
+ /** The serialized data */
18
+ data: string;
19
+ /** The format version of the data */
20
+ formatVersion: number;
21
+ /** Whether the data is encoded */
22
+ encoded: boolean;
23
+ /** The checksum of the data - key is not present when `addChecksum` is `false` */
24
+ checksum?: string;
25
+ };
26
+ /** Result of {@linkcode DataStoreSerializer.loadStoresData()} */
27
+ export type LoadStoresDataResult = {
28
+ /** The ID of the DataStore instance */
29
+ id: string;
30
+ /** The in-memory data object */
31
+ data: object;
32
+ };
33
+ /** Filter for selecting data stores */
34
+ export type StoreFilter = string[] | ((id: string) => boolean);
35
+ /**
36
+ * Allows for easy serialization and deserialization of multiple DataStore instances.
37
+ *
38
+ * All methods are at least `protected`, so you can easily extend this class and overwrite them to use a different storage method or to add additional functionality.
39
+ * Remember that you can call `super.methodName()` in the subclass to access the original method.
40
+ *
41
+ * - ⚠️ Needs to run in a secure context (HTTPS) due to the use of the SubtleCrypto API if checksumming is enabled.
42
+ */
43
+ export declare class DataStoreSerializer<TData extends DataStoreData> {
44
+ protected stores: DataStore<TData>[];
45
+ protected options: Required<DataStoreSerializerOptions>;
46
+ constructor(stores: DataStore<TData>[], options?: DataStoreSerializerOptions);
47
+ /** Calculates the checksum of a string */
48
+ protected calcChecksum(input: string): Promise<string>;
49
+ /**
50
+ * Serializes only a subset of the data stores into a string.
51
+ * @param stores An array of store IDs or functions that take a store ID and return a boolean
52
+ * @param useEncoding Whether to encode the data using each DataStore's `encodeData()` method
53
+ * @param stringified Whether to return the result as a string or as an array of `SerializedDataStore` objects
54
+ */
55
+ serializePartial(stores: StoreFilter, useEncoding?: boolean, stringified?: true): Promise<string>;
56
+ /**
57
+ * Serializes only a subset of the data stores into a string.
58
+ * @param stores An array of store IDs or functions that take a store ID and return a boolean
59
+ * @param useEncoding Whether to encode the data using each DataStore's `encodeData()` method
60
+ * @param stringified Whether to return the result as a string or as an array of `SerializedDataStore` objects
61
+ */
62
+ serializePartial(stores: StoreFilter, useEncoding?: boolean, stringified?: false): Promise<SerializedDataStore[]>;
63
+ /**
64
+ * Serializes only a subset of the data stores into a string.
65
+ * @param stores An array of store IDs or functions that take a store ID and return a boolean
66
+ * @param useEncoding Whether to encode the data using each DataStore's `encodeData()` method
67
+ * @param stringified Whether to return the result as a string or as an array of `SerializedDataStore` objects
68
+ */
69
+ serializePartial(stores: StoreFilter, useEncoding?: boolean, stringified?: boolean): Promise<string | SerializedDataStore[]>;
70
+ /**
71
+ * Serializes the data stores into a string.
72
+ * @param useEncoding Whether to encode the data using each DataStore's `encodeData()` method
73
+ * @param stringified Whether to return the result as a string or as an array of `SerializedDataStore` objects
74
+ */
75
+ serialize(useEncoding?: boolean, stringified?: true): Promise<string>;
76
+ /**
77
+ * Serializes the data stores into a string.
78
+ * @param useEncoding Whether to encode the data using each DataStore's `encodeData()` method
79
+ * @param stringified Whether to return the result as a string or as an array of `SerializedDataStore` objects
80
+ */
81
+ serialize(useEncoding?: boolean, stringified?: false): Promise<SerializedDataStore[]>;
82
+ /**
83
+ * Deserializes the data exported via {@linkcode serialize()} and imports only a subset into the DataStore instances.
84
+ * Also triggers the migration process if the data format has changed.
85
+ */
86
+ deserializePartial(stores: StoreFilter, data: string | SerializedDataStore[]): Promise<void>;
87
+ /**
88
+ * Deserializes the data exported via {@linkcode serialize()} and imports the data into all matching DataStore instances.
89
+ * Also triggers the migration process if the data format has changed.
90
+ */
91
+ deserialize(data: string | SerializedDataStore[]): Promise<void>;
92
+ /**
93
+ * Loads the persistent data of the DataStore instances into the in-memory cache.
94
+ * Also triggers the migration process if the data format has changed.
95
+ * @param stores An array of store IDs or a function that takes the store IDs and returns a boolean - if omitted, all stores will be loaded
96
+ * @returns Returns a PromiseSettledResult array with the results of each DataStore instance in the format `{ id: string, data: object }`
97
+ */
98
+ loadStoresData(stores?: StoreFilter): Promise<PromiseSettledResult<LoadStoresDataResult>[]>;
99
+ /**
100
+ * Resets the persistent and in-memory data of the DataStore instances to their default values.
101
+ * @param stores An array of store IDs or a function that takes the store IDs and returns a boolean - if omitted, all stores will be affected
102
+ */
103
+ resetStoresData(stores?: StoreFilter): Promise<PromiseSettledResult<void>[]>;
104
+ /**
105
+ * Deletes the persistent data of the DataStore instances.
106
+ * Leaves the in-memory data untouched.
107
+ * @param stores An array of store IDs or a function that takes the store IDs and returns a boolean - if omitted, all stores will be affected
108
+ */
109
+ deleteStoresData(stores?: StoreFilter): Promise<PromiseSettledResult<void>[]>;
110
+ /** Checks if a given value is an array of SerializedDataStore objects */
111
+ static isSerializedDataStoreObjArray(obj: unknown): obj is SerializedDataStore[];
112
+ /** Checks if a given value is a SerializedDataStore object */
113
+ static isSerializedDataStoreObj(obj: unknown): obj is SerializedDataStore;
114
+ /** Returns the DataStore instances whose IDs match the provided array or function */
115
+ protected getStoresFiltered(stores?: StoreFilter): DataStore<TData>[];
116
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * @module Debouncer
3
+ * This module contains the Debouncer class and debounce function that allow you to reduce the amount of calls in rapidly firing event listeners and such - [see the documentation for more info](https://github.com/Sv443-Network/UserUtils/blob/main/docs.md#debouncer)
4
+ */
5
+ import { NanoEmitter } from "./NanoEmitter.js";
6
+ /**
7
+ * The type of edge to use for the debouncer - [see the docs for a diagram and explanation.](https://github.com/Sv443-Network/UserUtils/blob/main/docs.md#debouncer)
8
+ * - `immediate` - (default & recommended) - calls the listeners at the very first call ("rising" edge) and queues the latest call until the timeout expires
9
+ * - Pros:
10
+ * - First call is let through immediately
11
+ * - Cons:
12
+ * - After all calls stop, the JS engine's event loop will continue to run until the last timeout expires (doesn't really matter on the web, but could cause a process exit delay in Node.js)
13
+ * - `idle` - queues all calls until there are no more calls in the given timeout duration ("falling" edge), and only then executes the very last call
14
+ * - Pros:
15
+ * - Makes sure there are zero calls in the given `timeoutDuration` before executing the last call
16
+ * - Cons:
17
+ * - Calls are always delayed by at least `1 * timeoutDuration`
18
+ * - Calls could get stuck in the queue indefinitely if there is no downtime between calls that is greater than the `timeoutDuration`
19
+ */
20
+ export type DebouncerType = "immediate" | "idle";
21
+ type AnyFn = (...args: any) => any;
22
+ /** The debounced function type that is returned by the {@linkcode debounce} function */
23
+ export type DebouncedFunction<TFunc extends AnyFn> = ((...args: Parameters<TFunc>) => ReturnType<TFunc>) & {
24
+ debouncer: Debouncer<TFunc>;
25
+ };
26
+ /** Event map for the {@linkcode Debouncer} */
27
+ export type DebouncerEventMap<TFunc extends AnyFn> = {
28
+ /** Emitted when the debouncer calls all registered listeners, as a pub-sub alternative */
29
+ call: TFunc;
30
+ /** Emitted when the timeout or edge type is changed after the instance was created */
31
+ change: (timeout: number, type: DebouncerType) => void;
32
+ };
33
+ /**
34
+ * A debouncer that calls all listeners after a specified timeout, discarding all calls in-between.
35
+ * It is very useful for event listeners that fire quickly, like `input` or `mousemove`, to prevent the listeners from being called too often and hogging resources.
36
+ * The exact behavior can be customized with the `type` parameter.
37
+ *
38
+ * The instance inherits from {@linkcode NanoEmitter} and emits the following events:
39
+ * - `call` - emitted when the debouncer calls all listeners - use this as a pub-sub alternative to the default callback-style listeners
40
+ * - `change` - emitted when the timeout or edge type is changed after the instance was created
41
+ */
42
+ export declare class Debouncer<TFunc extends AnyFn> extends NanoEmitter<DebouncerEventMap<TFunc>> {
43
+ protected timeout: number;
44
+ protected type: DebouncerType;
45
+ /** All registered listener functions and the time they were attached */
46
+ protected listeners: TFunc[];
47
+ /** The currently active timeout */
48
+ protected activeTimeout: ReturnType<typeof setTimeout> | undefined;
49
+ /** The latest queued call */
50
+ protected queuedCall: (() => void) | undefined;
51
+ /**
52
+ * Creates a new debouncer with the specified timeout and edge type.
53
+ * @param timeout Timeout in milliseconds between letting through calls - defaults to 200
54
+ * @param type The edge type to use for the debouncer - see {@linkcode DebouncerType} for details or [the documentation for an explanation and diagram](https://github.com/Sv443-Network/UserUtils/blob/main/docs.md#debouncer) - defaults to "immediate"
55
+ */
56
+ constructor(timeout?: number, type?: DebouncerType);
57
+ /** Adds a listener function that will be called on timeout */
58
+ addListener(fn: TFunc): void;
59
+ /** Removes the listener with the specified function reference */
60
+ removeListener(fn: TFunc): void;
61
+ /** Removes all listeners */
62
+ removeAllListeners(): void;
63
+ /** Returns all registered listeners */
64
+ getListeners(): TFunc[];
65
+ /** Sets the timeout for the debouncer */
66
+ setTimeout(timeout: number): void;
67
+ /** Returns the current timeout */
68
+ getTimeout(): number;
69
+ /** Whether the timeout is currently active, meaning any latest call to the {@linkcode call()} method will be queued */
70
+ isTimeoutActive(): boolean;
71
+ /** Sets the edge type for the debouncer */
72
+ setType(type: DebouncerType): void;
73
+ /** Returns the current edge type */
74
+ getType(): DebouncerType;
75
+ /** Use this to call the debouncer with the specified arguments that will be passed to all listener functions registered with {@linkcode addListener()} */
76
+ call(...args: Parameters<TFunc>): void;
77
+ }
78
+ /**
79
+ * Creates a {@linkcode Debouncer} instance with the specified timeout and edge type and attaches the passed function as a listener.
80
+ * The returned function can be called with any arguments and will execute the `call()` method of the debouncer.
81
+ * The debouncer instance is accessible via the `debouncer` property of the returned function.
82
+ *
83
+ * Refer to the {@linkcode Debouncer} class definition or the [Debouncer documentation](https://github.com/Sv443-Network/UserUtils/blob/main/docs.md#debouncer) for more information.
84
+ */
85
+ export declare function debounce<TFunc extends (...args: any[]) => any>(fn: TFunc, timeout?: number, type?: DebouncerType): DebouncedFunction<TFunc>;
86
+ export {};
@@ -0,0 +1,21 @@
1
+ /**
2
+ * @module Errors
3
+ * Contains custom error classes that all have a `date` property set to the time of the error throw
4
+ */
5
+ /** Base class for all custom error classes - adds a `date` prop set to the time when the error was thrown */
6
+ export declare class DatedError extends Error {
7
+ readonly date: Date;
8
+ constructor(message: string, options?: ErrorOptions);
9
+ }
10
+ /** Error while validating checksum */
11
+ export declare class ChecksumMismatchError extends DatedError {
12
+ constructor(message: string, options?: ErrorOptions);
13
+ }
14
+ /** Error while migrating data */
15
+ export declare class MigrationError extends DatedError {
16
+ constructor(message: string, options?: ErrorOptions);
17
+ }
18
+ /** Error while validating data */
19
+ export declare class ValidationError extends DatedError {
20
+ constructor(message: string, options?: ErrorOptions);
21
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * @module NanoEmitter
3
+ * This module contains the NanoEmitter class, which is a tiny event emitter powered by [nanoevents](https://www.npmjs.com/package/nanoevents) - [see the documentation for more info](https://github.com/Sv443-Network/UserUtils/blob/main/docs.md#nanoemitter)
4
+ */
5
+ import { type DefaultEvents, type Emitter, type EventsMap, type Unsubscribe } from "nanoevents";
6
+ export interface NanoEmitterOptions {
7
+ /** If set to true, allows emitting events through the public method emit() */
8
+ publicEmit: boolean;
9
+ }
10
+ /**
11
+ * Class that can be extended or instantiated by itself to create a lightweight event emitter with helper methods and a strongly typed event map.
12
+ * If extended from, you can use `this.events.emit()` to emit events, even if the `emit()` method doesn't work because `publicEmit` is not set to true in the constructor.
13
+ */
14
+ export declare class NanoEmitter<TEvtMap extends EventsMap = DefaultEvents> {
15
+ protected readonly events: Emitter<TEvtMap>;
16
+ protected eventUnsubscribes: Unsubscribe[];
17
+ protected emitterOptions: NanoEmitterOptions;
18
+ /** Creates a new instance of NanoEmitter - a lightweight event emitter with helper methods and a strongly typed event map */
19
+ constructor(options?: Partial<NanoEmitterOptions>);
20
+ /**
21
+ * Subscribes to an event and calls the callback when it's emitted.
22
+ * @param event The event to subscribe to. Use `as "_"` in case your event names aren't thoroughly typed (like when using a template literal, e.g. \`event-${val}\` as "_")
23
+ * @returns Returns a function that can be called to unsubscribe the event listener
24
+ * @example ```ts
25
+ * const emitter = new NanoEmitter<{
26
+ * foo: (bar: string) => void;
27
+ * }>({
28
+ * publicEmit: true,
29
+ * });
30
+ *
31
+ * let i = 0;
32
+ * const unsub = emitter.on("foo", (bar) => {
33
+ * // unsubscribe after 10 events:
34
+ * if(++i === 10) unsub();
35
+ * console.log(bar);
36
+ * });
37
+ *
38
+ * emitter.emit("foo", "bar");
39
+ * ```
40
+ */
41
+ on<TKey extends keyof TEvtMap>(event: TKey | "_", cb: TEvtMap[TKey]): () => void;
42
+ /**
43
+ * Subscribes to an event and calls the callback or resolves the Promise only once when it's emitted.
44
+ * @param event The event to subscribe to. Use `as "_"` in case your event names aren't thoroughly typed (like when using a template literal, e.g. \`event-${val}\` as "_")
45
+ * @param cb The callback to call when the event is emitted - if provided or not, the returned Promise will resolve with the event arguments
46
+ * @returns Returns a Promise that resolves with the event arguments when the event is emitted
47
+ * @example ```ts
48
+ * const emitter = new NanoEmitter<{
49
+ * foo: (bar: string) => void;
50
+ * }>();
51
+ *
52
+ * // Promise syntax:
53
+ * const [bar] = await emitter.once("foo");
54
+ * console.log(bar);
55
+ *
56
+ * // Callback syntax:
57
+ * emitter.once("foo", (bar) => console.log(bar));
58
+ * ```
59
+ */
60
+ once<TKey extends keyof TEvtMap>(event: TKey | "_", cb?: TEvtMap[TKey]): Promise<Parameters<TEvtMap[TKey]>>;
61
+ /**
62
+ * Emits an event on this instance.
63
+ * ⚠️ Needs `publicEmit` to be set to true in the NanoEmitter constructor or super() call!
64
+ * @param event The event to emit
65
+ * @param args The arguments to pass to the event listeners
66
+ * @returns Returns true if `publicEmit` is true and the event was emitted successfully
67
+ */
68
+ emit<TKey extends keyof TEvtMap>(event: TKey, ...args: Parameters<TEvtMap[TKey]>): boolean;
69
+ /** Unsubscribes all event listeners from this instance */
70
+ unsubscribeAll(): void;
71
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * @module TieredCache
3
+ * This module contains the TieredCache class, which is a cache that can have multiple tiers with different max TTLs, with data being moved between tiers based on what is fetched the most.
4
+ */
5
+ import { DataStore, type DataStoreData } from "./DataStore.js";
6
+ import { NanoEmitter, type NanoEmitterOptions } from "./NanoEmitter.js";
7
+ import type { DataStoreEngine } from "./DataStoreEngine.js";
8
+ import type { Prettify } from "./types.js";
9
+ /** Options for the {@linkcode TieredCache} class. */
10
+ export type TieredCacheOptions<TData extends DataStoreData> = Prettify<{
11
+ /** Unique identifier for this cache. */
12
+ id: string;
13
+ /** The available cache tiers. */
14
+ tiers: TieredCacheTierOptions<TData>[];
15
+ /** Optional options to pass to the {@linkcode NanoEmitter} constructor. */
16
+ nanoEmitterOptions?: NanoEmitterOptions;
17
+ }>;
18
+ /** Options object as resolved by the {@linkcode TieredCache.resolveTierOpts()} method. */
19
+ type TieredCacheResolvedTierOpts<TData extends DataStoreData> = Prettify<TieredCacheTierOptions<TData> & Pick<Required<TieredCacheTierOptions<TData>>, "compressionFormat">>;
20
+ /** Options for when a {@linkcode TieredCache} entry goes stale. */
21
+ export type TieredCacheStaleOptions = Prettify<{
22
+ /** The method to use for determining which entries are stale. `recency` = least recently used, `frequency` = least frequently used, `relevance` = combination of both (default). */
23
+ method?: "relevance" | "recency" | "frequency";
24
+ /** Maximum time to live for the data in this tier in seconds. */
25
+ ttl?: number;
26
+ /** Maximum amount of entries to keep in this tier. */
27
+ amount?: number;
28
+ /** The index of the cache tier to send the entry to when it goes stale. Defaults to the next available tier, or deletes the entry if there is none. */
29
+ sendToTier?: number;
30
+ }>;
31
+ /** Options for entry propagation between {@linkcode TieredCache} tiers. */
32
+ export type TieredCachePropagateTierOptions = Prettify<{
33
+ /** The index of the cache tier to propagate the entry to. Use negative numbers for accessing from the end, just like `Array.prototype.at()`. Use `1` for the second tier, use `-1` for the last. */
34
+ index: number;
35
+ /** Whether to propagate created entries to the cache with this index. Defaults to true. */
36
+ created?: boolean;
37
+ /** Whether to propagate updated entries to the cache with this index. Defaults to true. */
38
+ updated?: boolean;
39
+ /** Whether to propagate deleted entries to the cache with this index. Defaults to true. */
40
+ deleted?: boolean;
41
+ /** Whether to propagate accessed entries to the cache with this index. Defaults to true. */
42
+ accessed?: boolean;
43
+ }>;
44
+ /** Options for each {@linkcode TieredCache} tier. */
45
+ export type TieredCacheTierOptions<TData extends DataStoreData> = Prettify<{
46
+ /**
47
+ * Engine used for persistent storage. Can be a function that returns a DataStoreEngine or a DataStoreEngine instance.
48
+ * If this property is not set, this tier will not persist data and only keeps it in memory.
49
+ * ⚠️ **Don't reuse instances in multiple tiers and make sure the ID is always unique!**
50
+ */
51
+ engine?: (() => DataStoreEngine<TData>) | DataStoreEngine<TData>;
52
+ /** Which compression format to use for this tier's persistent storage. Defaults to `deflate-raw` - set to `null` to disable compression. */
53
+ compressionFormat?: CompressionFormat | null;
54
+ /** Options for when an entry goes stale, making it move to a lower tier or get fully deleted. */
55
+ staleOptions?: TieredCacheStaleOptions;
56
+ /** To which tiers to propagate created and updated entries. Defaults to the next tier and all properties set to their default. */
57
+ propagateTiers?: TieredCachePropagateTierOptions[];
58
+ }>;
59
+ /** Events that can be emitted by the {@linkcode TieredCache} class. */
60
+ export type TieredCacheEventMap = {};
61
+ /**
62
+ * Cache class that can have multiple tiers with different max TTLs, with data being moved between tiers based on what is fetched the most.
63
+ * Persists data using DataStore and DataStoreEngines.
64
+ * The zeroth tier contains the most accessed data, and the last tier contains the least accessed data, so it is recommended to use slower storage engines for the last tier(s).
65
+ */
66
+ export declare class TieredCache<TData extends DataStoreData> extends NanoEmitter<TieredCacheEventMap> {
67
+ protected options: TieredCacheOptions<TData>;
68
+ protected stores: Map<number, DataStore<TData>>;
69
+ /**
70
+ * Creates a new TieredCache instance.
71
+ * It is a cache that can have multiple tiers with different max TTLs, with data being moved between tiers based on what is fetched the most.
72
+ * It persists data using DataStore and DataStoreEngines.
73
+ * The zeroth tier contains the most accessed data, and the last tier contains the least accessed data, so it is recommended to use slower storage engines for the last tier(s).
74
+ * If the given {@linkcode options} are invalid, a {@linkcode ValidationError} is thrown.
75
+ */
76
+ constructor(options: TieredCacheOptions<TData>);
77
+ /** Validates the options for this cache and throws an Error containing all problems if they're invalid. Should be called once in the constructor. */
78
+ protected validateOptions(opts: TieredCacheOptions<TData>): void;
79
+ get(matching: string | ((tier: TieredCacheTierOptions<TData>) => boolean)): Promise<TData | undefined>;
80
+ upsert(matching: string | ((tier: TieredCacheTierOptions<TData>) => boolean), data: TData): Promise<boolean>;
81
+ /** Loads all persistent data from all tiers and initializes the DataStore instances. */
82
+ loadData(): Promise<PromiseSettledResult<TData>[]>;
83
+ /** Returns the options for the specified tier, after filling in all defaults. */
84
+ protected resolveTierOpts(index: number): Prettify<TieredCacheResolvedTierOpts<TData>> | undefined;
85
+ }
86
+ export {};