UNPKG

@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
/** * @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 {};