import { Jsonify, Opaque } from "type-fest"; import { UserId } from "../../types/guid"; import { Utils } from "../misc/utils"; import { StateDefinition } from "./state-definition"; /** * A set of options for customizing the behavior of a {@link KeyDefinition} */ type KeyDefinitionOptions = { /** * A function to use to safely convert your type from json to your expected type. * * **Important:** Your data may be serialized/deserialized at any time and this * callback needs to be able to faithfully re-initialize from the JSON object representation of your type. * * @param jsonValue The JSON object representation of your state. * @returns The fully typed version of your state. */ readonly deserializer: (jsonValue: Jsonify) => T; }; /** * KeyDefinitions describe the precise location to store data for a given piece of state. * The StateDefinition is used to describe the domain of the state, and the KeyDefinition * sub-divides that domain into specific keys. */ export class KeyDefinition { /** * Creates a new instance of a KeyDefinition * @param stateDefinition The state definition for which this key belongs to. * @param key The name of the key, this should be unique per domain. * @param options A set of options to customize the behavior of {@link KeyDefinition}. All options are required. * @param options.deserializer A function to use to safely convert your type from json to your expected type. * Your data may be serialized/deserialized at any time and this needs callback needs to be able to faithfully re-initialize * from the JSON object representation of your type. */ constructor( readonly stateDefinition: StateDefinition, readonly key: string, private readonly options: KeyDefinitionOptions ) { if (options.deserializer == null) { throw new Error( `'deserializer' is a required property on key ${stateDefinition.name} > ${key}` ); } } /** * Gets the deserializer configured for this {@link KeyDefinition} */ get deserializer() { return this.options.deserializer; } /** * Creates a {@link KeyDefinition} for state that is an array. * @param stateDefinition The state definition to be added to the KeyDefinition * @param key The key to be added to the KeyDefinition * @param options The options to customize the final {@link KeyDefinition}. * @returns A {@link KeyDefinition} initialized for arrays, the options run * the deserializer on the provided options for each element of an array * **unless that array is null, in which case it will return an empty list.** * * @example * ```typescript * const MY_KEY = KeyDefinition.array(MY_STATE, "key", { * deserializer: (myJsonElement) => convertToElement(myJsonElement), * }); * ``` */ static array( stateDefinition: StateDefinition, key: string, // We have them provide options for the element of the array, depending on future options we add, this could get a little weird. options: KeyDefinitionOptions // The array helper forces an initialValue of an empty array ) { return new KeyDefinition(stateDefinition, key, { ...options, deserializer: (jsonValue) => { if (jsonValue == null) { return null; } return jsonValue.map((v) => options.deserializer(v)); }, }); } /** * Creates a {@link KeyDefinition} for state that is a record. * @param stateDefinition The state definition to be added to the KeyDefinition * @param key The key to be added to the KeyDefinition * @param options The options to customize the final {@link KeyDefinition}. * @returns A {@link KeyDefinition} that contains a serializer that will run the provided deserializer for each * value in a record and returns every key as a string **unless that record is null, in which case it will return an record.** * * @example * ```typescript * const MY_KEY = KeyDefinition.record(MY_STATE, "key", { * deserializer: (myJsonValue) => convertToValue(myJsonValue), * }); * ``` */ static record( stateDefinition: StateDefinition, key: string, // We have them provide options for the value of the record, depending on future options we add, this could get a little weird. options: KeyDefinitionOptions // The array helper forces an initialValue of an empty record ) { return new KeyDefinition>(stateDefinition, key, { ...options, deserializer: (jsonValue) => { if (jsonValue == null) { return null; } const output: Record = {}; for (const key in jsonValue) { output[key] = options.deserializer((jsonValue as Record>)[key]); } return output; }, }); } /** * Create a string that should be unique across the entire application. * @returns A string that can be used to cache instances created via this key. */ buildCacheKey(): string { return `${this.stateDefinition.storageLocation}_${this.stateDefinition.name}_${this.key}`; } } export type StorageKey = Opaque; /** * Creates a {@link StorageKey} that points to the data at the given key definition for the specified user. * @param userId The userId of the user you want the key to be for. * @param keyDefinition The key definition of which data the key should point to. * @returns A key that is ready to be used in a storage service to get data. */ export function userKeyBuilder(userId: UserId, keyDefinition: KeyDefinition): StorageKey { if (!Utils.isGuid(userId)) { throw new Error("You cannot build a user key without a valid UserId"); } return `user_${userId}_${keyDefinition.stateDefinition.name}_${keyDefinition.key}` as StorageKey; } /** * Creates a {@link StorageKey} * @param keyDefinition The key definition of which data the key should point to. * @returns A key that is ready to be used in a storage service to get data. */ export function globalKeyBuilder(keyDefinition: KeyDefinition): StorageKey { return `global_${keyDefinition.stateDefinition.name}_${keyDefinition.key}` as StorageKey; }