@sv443-network/coreutils
Version:
Cross-platform, general-purpose, JavaScript core library for Node, Deno and the browser. Intended to be used in conjunction with `@sv443-network/userutils` and `@sv443-network/djsutils`, but can be used independently as well.
168 lines (167 loc) • 12.2 kB
TypeScript
/**
* @module DataStore
* 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)
*/
import type { DataStoreEngine } from "./DataStoreEngine.js";
import type { LooseUnion, Prettify, SerializableVal } from "./types.js";
/** Function that takes the data in the old format and returns the data in the new format. Also supports an asynchronous migration. */
type MigrationFunc = (oldData: any) => any | Promise<any>;
/** Dictionary of format version numbers and the function that migrates to them from the previous whole integer */
export type DataMigrationsDict = Record<number, MigrationFunc>;
/** Options for the DataStore instance */
export type DataStoreOptions<TData extends DataStoreData> = Prettify<{
/**
* A unique internal ID for this data store.
* To avoid conflicts with other scripts, it is recommended to use a prefix that is unique to your script.
* If you want to change the ID, you should make use of the {@linkcode DataStore.migrateId()} method.
*/
id: string;
/**
* The default data object to use if no data is saved in persistent storage yet.
* Until the data is loaded from persistent storage with {@linkcode DataStore.loadData()}, this will be the data returned by {@linkcode DataStore.getData()}.
*
* - ⚠️ 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.
*/
defaultData: TData;
/**
* An incremental, whole integer version number of the current format of data.
* 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.
*
* - ⚠️ Never decrement this number and optimally don't skip any numbers either!
*/
formatVersion: number;
/**
* The engine middleware to use for persistent storage.
* Create an instance of {@linkcode FileStorageEngine} (Node.js), {@linkcode BrowserStorageEngine} (DOM) or your own engine class that extends {@linkcode DataStoreEngine} and pass it here.
*
* - ⚠️ Don't reuse the same engine instance for multiple DataStores, unless it explicitly supports it!
*/
engine: (() => DataStoreEngine<TData>) | DataStoreEngine<TData>;
/**
* A dictionary of functions that can be used to migrate data from older versions to newer ones.
* The keys of the dictionary should be the format version that the functions can migrate to, from the previous whole integer value.
* The values should be functions that take the data in the old format and return the data in the new format.
* The functions will be run in order from the oldest to the newest version.
* If the current format version is not in the dictionary, no migrations will be run.
*/
migrations?: DataMigrationsDict;
/**
* If an ID or multiple IDs are passed here, the data will be migrated from the old ID(s) to the current ID.
* This will happen once per page load, when {@linkcode DataStore.loadData()} is called.
* All future calls to {@linkcode DataStore.loadData()} in the session will not check for the old ID(s) anymore.
* To migrate IDs manually, use the method {@linkcode DataStore.migrateId()} instead.
*/
migrateIds?: string | string[];
} & ({
encodeData?: never;
decodeData?: never;
/**
* The format to use for compressing the data. Defaults to `deflate-raw`. Explicitly set to `null` to store data uncompressed.
* - ⚠️ Use either this property, or both `encodeData` and `decodeData`, but not all three!
*/
compressionFormat?: CompressionFormat | null;
} | {
/**
* Tuple of a compression format identifier and a function to use to encode the data prior to saving it in persistent storage.
* Set the identifier to `null` or `"identity"` to indicate that no traditional compression is used.
*
* - ⚠️ If this is specified, `compressionFormat` can't be used. Also make sure to declare {@linkcode decodeData()} as well.
*
* 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.
* @param data The input data as a serialized object (JSON string)
*/
encodeData: [format: LooseUnion<CompressionFormat> | null, encode: (data: string) => string | Promise<string>];
/**
* Tuple of a compression format identifier and a function to use to decode the data after reading it from persistent storage.
* Set the identifier to `null` or `"identity"` to indicate that no traditional compression is used.
*
* - ⚠️ If this is specified, `compressionFormat` can't be used. Also make sure to declare {@linkcode encodeData()} as well.
*
* 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.
* @returns The resulting data as a valid serialized object (JSON string)
*/
decodeData: [format: LooseUnion<CompressionFormat> | null, decode: (data: string) => string | Promise<string>];
compressionFormat?: never;
})>;
/** Generic type that represents the serializable data structure saved in a {@linkcode DataStore} instance. */
export type DataStoreData<TData extends SerializableVal = SerializableVal> = Record<string, SerializableVal | TData>;
/**
* 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.
* Supports migrating data from older format versions to newer ones and populating the cache with default data if no persistent data is found.
* Can be overridden to implement any other storage method.
*
* 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.
* Remember that you can use `super.methodName()` in the subclass to call the original method if needed.
*
* - ⚠️ 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.
* - ⚠️ Make sure to call {@linkcode loadData()} at least once after creating an instance, or the returned data will be the same as `options.defaultData`
*
* @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)
*/
export declare class DataStore<TData extends DataStoreData> {
readonly id: string;
readonly formatVersion: number;
readonly defaultData: TData;
readonly encodeData: DataStoreOptions<TData>["encodeData"];
readonly decodeData: DataStoreOptions<TData>["decodeData"];
readonly compressionFormat: Exclude<DataStoreOptions<TData>["compressionFormat"], undefined>;
readonly engine: DataStoreEngine<TData>;
options: DataStoreOptions<TData>;
/**
* Whether all first-init checks should be done.
* This includes migrating the internal DataStore format, migrating data from the UserUtils format, and anything similar.
* This is set to `true` by default. Create a subclass and set it to `false` before calling {@linkcode loadData()} if you want to explicitly skip these checks.
*/
protected firstInit: boolean;
/** In-memory cached copy of the data that is saved in persistent storage used for synchronous read access. */
private cachedData;
private migrations?;
private migrateIds;
/**
* Creates an instance of DataStore to manage a sync & async database that is cached in memory and persistently saved across sessions.
* Supports migrating data from older versions to newer ones and populating the cache with default data if no persistent data is found.
*
* - ⚠️ Make sure to call {@linkcode loadData()} at least once after creating an instance, or the returned data will be the same as `options.defaultData`
*
* @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.)
* @param opts The options for this DataStore instance
*/
constructor(opts: DataStoreOptions<TData>);
/**
* Loads the data saved in persistent storage into the in-memory cache and also returns a copy of it.
* Automatically populates persistent storage with default data if it doesn't contain any data yet.
* Also runs all necessary migration functions if the data format has changed since the last time the data was saved.
*/
loadData(): Promise<TData>;
/**
* Returns a copy of the data from the in-memory cache.
* Use {@linkcode loadData()} to get fresh data from persistent storage (usually not necessary since the cache should always exactly reflect persistent storage).
*/
getData(): TData;
/** Saves the data synchronously to the in-memory cache and asynchronously to the persistent storage */
setData(data: TData): Promise<void>;
/** Saves the default data passed in the constructor synchronously to the in-memory cache and asynchronously to persistent storage */
saveDefaultData(): Promise<void>;
/**
* Call this method to clear all persistently stored data associated with this DataStore instance, including the storage container (if supported by the DataStoreEngine).
* The in-memory cache will be left untouched, so you may still access the data with {@linkcode getData()}
* Calling {@linkcode loadData()} or {@linkcode setData()} after this method was called will recreate persistent storage with the cached or default data.
*/
deleteData(): Promise<void>;
/** Returns whether encoding and decoding are enabled for this DataStore instance */
encodingEnabled(): this is Required<Pick<DataStoreOptions<TData>, "encodeData" | "decodeData">>;
/**
* Runs all necessary migration functions consecutively and saves the result to the in-memory cache and persistent storage and also returns it.
* This method is automatically called by {@linkcode loadData()} if the data format has changed since the last time the data was saved.
* 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.
*
* 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.
*/
runMigrations(oldData: unknown, oldFmtVer: number, resetOnError?: boolean): Promise<TData>;
/**
* Tries to migrate the currently saved persistent data from one or more old IDs to the ID set in the constructor.
* 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.
*/
migrateId(oldIds: string | string[]): Promise<void>;
}
export {};