/** * @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 { MigrationError } from "./Errors.ts"; import type { DataStoreEngine } from "./DataStoreEngine.ts"; import type { LooseUnion, Prettify } from "./types.ts"; import { NanoEmitter, type NanoEmitterOptions } from "./NanoEmitter.ts"; /** 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; /** Dictionary of format version numbers and the function that migrates to them from the previous whole integer */ export type DataMigrationsDict = Record; /** Tuple of a compression format identifier and a function to use to encode the data */ export type EncodeTuple = [format: LooseUnion | null, encode: (data: string) => string | Promise]; /** Tuple of a compression format identifier and a function to use to decode the data */ export type DecodeTuple = [format: LooseUnion | null, decode: (data: string) => string | Promise]; /** Options for the DataStore instance */ export type DataStoreOptions = 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) | DataStoreEngine; /** * 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 (smallest number) to the newest (biggest number) 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[]; /** * Whether to keep a copy of the data in memory for synchronous read access. Defaults to `true`. * * - ⚠️ If turned off, {@linkcode DataStore.getData()} will be unavailable at the type level and only {@linkcode DataStore.loadData()} can be used to access the data. * This may be useful if multiple sources are modifying the data, or the data is very large and you want to save memory, but it will make accessing the data slower, especially when combined with compression. */ memoryCache?: TMemCache; /** * Allows overriding the default prefix (`__ds-`) that is prepended to all keys used for persistent storage. * Note: this will break backwards compatibility with any previously saved data, so only change this if you know what you're doing and ideally before any non-volatile data is saved by the end user. */ keyPrefix?: string; /** Options for the internal NanoEmitter instance. */ nanoEmitterOptions?: NanoEmitterOptions; } & ({ 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 a combination of the 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: EncodeTuple; /** * 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: DecodeTuple; compressionFormat?: never; })>; /** * Generic type that represents the serializable data structure saved in a {@linkcode DataStore} instance. * - ⚠️ Uses `object` instead of an index signature so that interfaces without an explicit index signature can be used as `TData`. * However, this also means that the type system won't prevent you from using non-serializable data structures like functions or symbols in the data, which will cause errors at runtime. * Make sure to only use types that are compatible with `JSON.stringify()`, and use `null` instead of `undefined` when you need to preserve the key of an empty value. */ export type DataStoreData = object; /** Map of event names and their corresponding listener function signatures for the {@linkcode DataStore} class. */ export type DataStoreEventMap = { /** Emitted whenever the data is loaded from persistent storage with {@linkcode DataStore.loadData()}. */ loadData: (data: TData) => void; /** Emitted when the data is updated with {@linkcode DataStore.setData()} or {@linkcode DataStore.runMigrations()} */ updateData: (newData: TData) => void; /** Emitted when the memory cache was updated with {@linkcode DataStore.setData()}, before the data is saved to persistent storage. Not emitted if `memoryCache` is set to `false`. */ updateDataSync: (newData: TData) => void; /** Emitted for every called migration function with the resulting data. */ migrateData: (migratedTo: number, migratedData: unknown, isFinalMigration: boolean) => void; /** Emitted for every successfully migrated old ID. Gets passed the old and new ID. */ migrateId: (oldId: string, newId: string) => void; /** Emitted whenever the data is reset to the default value with {@linkcode DataStore.saveDefaultData()} (will not be called on the initial population of persistent storage with the default data in {@linkcode DataStore.loadData()}). */ setDefaultData: (defaultData: TData) => void; /** Emitted after the data was deleted from persistent storage with {@linkcode DataStore.deleteData()}. */ deleteData: () => void; /** Emitted when an error occurs at any point. */ error: (error: Error) => void; /** Emitted only when an error occurs during a migration function. */ migrationError: (migratingTo: number, error: MigrationError) => void; }; /** * 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 */ export declare class DataStore extends NanoEmitter> { readonly id: string; readonly formatVersion: number; readonly defaultData: TData; readonly encodeData: DataStoreOptions["encodeData"]; readonly decodeData: DataStoreOptions["decodeData"]; readonly compressionFormat: Exclude["compressionFormat"], undefined>; readonly memoryCache: TMemCache; readonly engine: DataStoreEngine; readonly keyPrefix: string; options: DataStoreOptions; /** * 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. */ protected cachedData: TData; protected migrations?: DataMigrationsDict; protected migrateIds: string[]; /** * 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); /** * 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; /** * 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). * ⚠️ Only available when `memoryCache` is `true` (default). When set to `false`, this produces a type and runtime error - use {@linkcode loadData()} instead. */ getData(this: DataStore): TData; /** Saves the data synchronously to the in-memory cache and asynchronously to the persistent storage */ setData(data: TData): Promise; /** * Saves the default data passed in the constructor synchronously to the in-memory cache and asynchronously to persistent storage. * @param emitEvent Whether to emit the `setDefaultData` event - set to `false` to prevent event emission (used internally during initial population in {@linkcode loadData()}) */ saveDefaultData(emitEvent?: boolean): Promise; /** * 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; /** Returns whether encoding and decoding are enabled for this DataStore instance */ encodingEnabled(): this is Required, "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; /** * 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; } export {};