chrono-forge
Version:
A comprehensive framework for building resilient Temporal workflows, advanced state management, and real-time streaming activities in TypeScript. Designed for a seamless developer experience with powerful abstractions, dynamic orchestration, and full cont
230 lines (229 loc) • 9.29 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.StateManager = void 0;
const eventemitter3_1 = __importDefault(require("eventemitter3"));
const entities_1 = require("./entities");
const actions_1 = require("./actions");
const deep_object_diff_1 = require("deep-object-diff");
const lodash_1 = require("lodash");
const utils_1 = require("../utils");
const mnemonist_1 = require("mnemonist");
const EntityProxyManager_1 = require("./EntityProxyManager");
const SchemaManager_1 = require("./SchemaManager");
const typescript_algos_1 = require("typescript-algos");
class StateManager extends eventemitter3_1.default {
_instanceId;
get instanceId() {
return this._instanceId;
}
constructor(_instanceId) {
super();
this._instanceId = _instanceId;
this._instanceId = _instanceId;
this._state = {};
if (!EntityProxyManager_1.EntityProxyManager['proxyStateTree']) {
EntityProxyManager_1.EntityProxyManager.initialize();
}
}
static getInstance(instanceId) {
if (!instanceId) {
throw new Error(`You must provide a instanceId ${instanceId}!`);
}
if (!EntityProxyManager_1.EntityProxyManager['proxyStateTree']) {
EntityProxyManager_1.EntityProxyManager.initialize();
}
if (!this.instances[instanceId]) {
this.instances[instanceId] = new StateManager(instanceId);
}
return this.instances[instanceId];
}
static instances = {};
_processing = false;
get processing() {
return this._processing;
}
_state;
get state() {
return this._state ?? {};
}
set state(state) {
this._state = state;
}
async handleStateChange(newState, previousState, origins) {
const differences = (0, deep_object_diff_1.detailedDiff)(previousState, newState);
if (!(0, lodash_1.isEmpty)(differences.added) || !(0, lodash_1.isEmpty)(differences.updated) || !(0, lodash_1.isEmpty)(differences.deleted)) {
this._state = newState;
this.cache.clear();
EntityProxyManager_1.EntityProxyManager.clearCache();
await this.emitStateChangeEvents(differences, previousState, newState, Array.from(origins));
}
}
async setState(newState) {
if (this._state === newState)
return;
await this.dispatch((0, actions_1.setState)(newState), true, this.instanceId);
}
cache = new mnemonist_1.LRUCacheWithDelete(1000);
_queue = new typescript_algos_1.Queue();
get queue() {
return this._queue;
}
async dispatch(actions, sync = true, origin) {
const actionArray = Array.isArray(actions) ? actions : [actions];
const queueItems = actionArray.map((action) => ({ action, origin }));
if (sync && !this._processing) {
await this.processChanges(queueItems);
}
else {
queueItems.forEach((item) => this._queue.enqueue(item));
}
}
async processChanges(items) {
const origins = new Set();
const pendingChanges = items ?? this._queue;
const previousState = this._state;
this._processing = true;
let newState;
let itemsProcessed = 0;
const startTime = Date.now();
const MAX_PROCESSING_TIME = 30000;
while (pendingChanges.length > 0 && itemsProcessed < 100 && Date.now() - startTime < MAX_PROCESSING_TIME) {
const item = Array.isArray(pendingChanges) ? pendingChanges.shift() : pendingChanges.dequeue();
if (!item)
break;
const { action, origin } = item;
newState = (0, entities_1.reducer)(newState || this._state, action);
if (origin) {
origins.add(origin);
}
itemsProcessed++;
}
if (newState && newState !== this._state) {
await this.handleStateChange(newState, previousState, origins);
}
this._processing = false;
}
async emitStateChangeEvents(differences, previousState, newState, origins) {
const changedPaths = ['added', 'updated', 'deleted'];
const emitMatchingEvents = async (event, details) => {
if (this.listenerCount(event) > 0) {
await this.emitAsync(event, details);
}
const [entityName, eventIdAndType] = event.split('.');
const [eventId, eventType] = eventIdAndType.split(':');
const wildcardPatterns = [
`${entityName}.*:${eventType}`,
`*.*:${eventType}`,
`${entityName}.${eventId}:*`,
`${entityName}.*:*`,
'*.*:*'
];
for (const pattern of wildcardPatterns) {
if (this.listenerCount(pattern) > 0) {
await this.emitAsync(pattern, details);
}
}
};
for (const changeType of changedPaths) {
const entities = differences[changeType];
if (!entities || typeof entities !== 'object')
continue;
for (const [entityName, entityChanges] of Object.entries(entities)) {
if (!entityChanges || typeof entityChanges !== 'object')
continue;
for (const entityId of Object.keys(entityChanges)) {
const eventName = `${entityName}.${entityId}:${changeType}`;
if (origins.includes(this.instanceId)) {
continue;
}
const eventDetails = { newState, previousState, changeType, changes: entityChanges[entityId], origins };
await emitMatchingEvents(eventName, eventDetails);
}
}
}
await this.emitAsync('stateChange', { newState, previousState, differences, changeOrigins: origins });
}
query(entityName, id, denormalizeData = true) {
if (!entityName || !id) {
return null;
}
const entity = this._state[entityName]?.[id];
if (!entity) {
return null;
}
if (!denormalizeData) {
return entity;
}
const denormalized = (0, utils_1.limitRecursion)(id, entityName, this._state, this);
if (denormalized === id) {
return entity;
}
return EntityProxyManager_1.EntityProxyManager.createEntityProxy(entityName, id, denormalized, this);
}
clear() {
this.dispatch((0, actions_1.clearEntities)(), true, this.instanceId);
}
async emitAsync(event, ...args) {
const listeners = this.listeners(event);
for (const listener of listeners) {
try {
await Promise.resolve().then(() => listener(...args));
}
catch (error) {
console.error(`Error in listener for event '${event}':`, error);
}
}
return listeners.length > 0;
}
isEntityReferenced(entityName, entityId, ignoreReference, checkedEntities = new Set()) {
const entityKey = `${entityName}:${entityId}`;
if (checkedEntities.has(entityKey)) {
return false;
}
checkedEntities.add(entityKey);
const referenceMap = SchemaManager_1.SchemaManager.relationshipMap[entityName]?._referencedBy;
if (!referenceMap)
return false;
return Object.entries(referenceMap).some(([referencingEntityName, referencingEntity]) => {
if (ignoreReference &&
ignoreReference.entityName === referencingEntityName &&
ignoreReference.fieldName === referencingEntity.fieldName) {
return false;
}
const entities = this.state[referencingEntityName];
if (!entities)
return false;
return Object.keys(entities).some((id) => {
const entity = entities[id];
if (!entity || entity[referencingEntity.fieldName] === undefined)
return false;
const hasReference = referencingEntity.isMany
? Array.isArray(entity[referencingEntity.fieldName]) && entity[referencingEntity.fieldName].includes(entityId)
: entity[referencingEntity.fieldName] === entityId;
if (hasReference) {
return this.isEntityReferenced(referencingEntityName, id, ignoreReference, checkedEntities);
}
return false;
});
});
}
cleanup() {
this.removeAllListeners();
this._state = {};
this._processing = false;
while (this._queue.length > 0) {
this._queue.dequeue();
}
this.cache.clear();
EntityProxyManager_1.EntityProxyManager.clearCache();
delete StateManager.instances[this.instanceId];
}
static clearInstances() {
this.instances = {};
}
}
exports.StateManager = StateManager;
exports.default = StateManager;