state-management-utilities
Version:
State management utilities
260 lines (259 loc) • 7.82 kB
JavaScript
import { produce } from "immer";
/**
* The `StateManagerCenter` class is a singleton that manages multiple state managers,
* logs state changes, and provides methods for state hydration and dehydration.
*/
class StateManagerCenter {
_cacheRefs = {};
/**
* A record of state managers identified by unique IDs.
*/
_stateManagers = {};
/**
* An array of state manager center records.
*/
_records = [];
/**
* A record of the current states identified by unique IDs.
*/
_currentStates = {};
/**
* A flag indicating whether logging is enabled.
*/
_isEnabledLog = true;
/**
* A flag indicating whether cloning is disabled.
*/
_disableCloning = false;
/**
* An optional callback function for logging.
*/
_onLog;
/**
* A flag indicating whether hydration is in progress.
*/
_isHydration = false;
/**
* Gets the hydration status.
*/
get isHydration() {
return this._isHydration;
}
/**
* The singleton instance of `StateManagerCenter`.
*/
static _instance;
/**
* Private constructor to prevent direct instantiation.
*/
constructor() { }
/**
* Gets the cloning disable status.
*/
get disableCloning() {
return this._disableCloning;
}
/**
* Sets the cloning disable status.
*/
set disableCloning(value) {
this._disableCloning = value;
}
/**
* Gets the singleton instance of `StateManagerCenter`.
*/
static get instance() {
if (!StateManagerCenter._instance) {
StateManagerCenter._instance = new StateManagerCenter();
}
return StateManagerCenter._instance;
}
/**
* Gets the current states.
*/
get currentStates() {
return this._currentStates;
}
/**
* Gets the logging enable status.
*/
get enableLog() {
return this._isEnabledLog;
}
/**
* Sets the logging enable status.
*/
set enableLog(value) {
if (value === this._isEnabledLog)
return;
this._isEnabledLog = value;
if (!value) {
this._records = [];
}
this._onLog?.({ uid: "TOGGLED-STATUS", state: { enable: value } }).catch(console.error);
}
/**
* Returns the state manager center records.
* These records can be sent to the server for monitoring and debugging purposes.
*
* @returns The state manager center records.
*/
get records() {
return this._records;
}
/**
* Sets the current records to the provided records.
* This method can be used for monitoring and debugging purposes.
*
* @param records - The records to be set.
*/
set records(records) {
this._records = records;
this._onLog?.({ uid: "IMPORTED", state: undefined }).catch(console.error);
}
/**
* Gets the state manager center records in reverse order.
*/
async getReverseRecords() {
return this._records?.slice().reverse() ?? [];
}
/**
* Logs a state change.
*/
_log({ uid, state }) {
if (!this.enableLog)
return this;
this._currentStates[uid] = state;
const timestamp = Date.now();
const number = ++counter;
(async () => {
this._records = produce(this._records, (draft) => {
draft.push({
updatedUID: uid,
states: { ...this._currentStates },
timestamp,
number,
});
});
this._onLog?.({ uid, state })?.catch(console.error);
})().catch(console.error);
return this;
}
/**
* Registers a state manager with a unique ID.
*/
_register({ uid, stateManager, }) {
if (this._stateManagers[uid])
throw new Error(`UID "${uid}" already is registered in the "State Manager Center".`);
this._stateManagers[uid] = stateManager;
return this;
}
/**
* Applies the given states to the related state managers.
*
* @param {CenterRecordType} record - An object containing the states to be applied.
* @returns {this} The current instance of the class.
*/
apply({ states }) {
try {
for (const uid in states) {
if (this._stateManagers[uid])
this._stateManagers[uid].value = states[uid];
}
}
catch (error) {
/* istanbul ignore next */
console.error(`Error occurred while applying the states in the "State Manager Center".\n`, error);
}
return this;
}
/**
* Initializes the hydration process.
*/
_initializeHydration() {
this._isHydration = true;
this.enableLog = true;
this._records = [];
this._currentStates = {};
return this;
}
/**
* Hydrates the state managers with the provided entities.
* @param entities - The entities to be hydrated.
* @returns The hydrated states.
* @throws An error if the entity does not have a `hydrated` property.
*/
get hydrated() {
return {
generate: async (...entries) => await this._generateHydrated({ entries }),
config: ({ initial }) => ({
generate: async (...entries) => await this._generateHydrated({ entries, initial }),
}),
};
}
async _generateHydrated({ entries, initial, }) {
this._initializeHydration();
const updaters = await Promise.all(entries);
const timestamp = Date.now();
const hydrated = initial ?? {
data: {},
timestamp,
id: `${timestamp}`,
};
const values = [];
updaters.forEach(({ update, value }) => {
update(hydrated.data);
values.push(value);
});
return Object.freeze({ hydrated, values });
}
_registerCacheRef({ uid, ref }) {
this._cacheRefs[uid] = ref;
return this;
}
/**
* Dehydrates the state managers with the provided states.
* @param states - The states to be dehydrated.
*/
async dehydrate(hydrated) {
if (!hydrated)
return;
for (const uid in hydrated.data) {
const data = hydrated.data[uid];
data.timestamp = data.timestamp ? data.timestamp : hydrated.timestamp;
this._updateCache(data);
}
}
async _updateCache({ value, hash, cacheRef, timestamp, }) {
if (!hash || !cacheRef || !timestamp)
return;
const ref = this._cacheRefs[cacheRef];
const { updatedAt } = (await ref?._getCache?.(hash)) ?? {};
if (updatedAt && updatedAt >= timestamp)
return;
ref?._setCache?.(hash, { data: value, updatedAt: timestamp });
}
/**
* Registers a callback function for logging.
* If a callback is already registered, an error will be thrown.
* @param callback - The callback function to be registered.
* @returns The `StateManagerCenter` instance.
* @throws An error if a callback is already registered.
*/
onLog(callback) {
if (this._onLog && callback)
throw new Error(`"onLog" callback is already registered in the "State Manager Center".`);
this._onLog = callback;
return this;
}
/**
* Clears the state manager center records.
*/
clearRecords() {
this._records = [];
this._onLog?.({ uid: "CLEARED", state: undefined }).catch(console.error);
return this;
}
}
let counter = 0;
export const center = StateManagerCenter.instance;