@statezero/core
Version:
The type-safe frontend client for StateZero - connect directly to your backend models with zero boilerplate
208 lines (207 loc) • 8.09 kB
JavaScript
import { operationEvents, Status, Type } from './operation.js';
import { modelStoreRegistry } from '../registries/modelStoreRegistry.js';
import { querysetStoreRegistry } from '../registries/querysetStoreRegistry.js';
import { metricRegistry } from '../registries/metricRegistry.js';
import { getFingerprint } from './utils.js';
import { QuerySet } from '../../flavours/django/querySet.js';
import { isEqual, isNil } from 'lodash-es';
import hash from 'object-hash';
/**
* Returns querysets that have the same nodes as any ancestor of the specific queryset
* @param {QuerySet} queryset
* @returns {Map<QuerySet, Store>}
*/
function relatedQuerysets(queryset) {
// Collect ancestor nodes for comparison
let ancestorNodes = [];
let current = queryset;
while (current) {
ancestorNodes.push(current.nodes);
current = current.__parent;
}
const modelClass = queryset.ModelClass;
const result = new Map();
Array.from(querysetStoreRegistry._stores.entries()).forEach(([queryset, store]) => {
if (store.modelClass !== modelClass)
return;
try {
if (ancestorNodes.some(nodes => isEqual(nodes, store.queryset.nodes))) {
result.set(store.queryset, store);
}
}
catch (e) {
console.warn('Error comparing nodes for related querysets', e);
}
});
return result;
}
/**
* Process an operation in the model store
*
* @param {Operation} operation - The operation to process
* @param {string} actionType - The action to perform ('add', 'update', 'confirm', 'reject')
*/
function processModelStore(operation, actionType) {
const ModelClass = operation.queryset.ModelClass;
const modelStore = modelStoreRegistry.getStore(ModelClass);
if (!modelStore)
return;
switch (actionType) {
case 'add':
modelStore.addOperation(operation);
break;
case 'update':
modelStore.updateOperation(operation);
break;
case 'confirm':
modelStore.confirm(operation);
break;
case 'reject':
modelStore.reject(operation);
break;
}
}
/**
* Process an operation in the queryset stores based on operation type
* Uses different routing strategies based on the operation type
*
* @param {Operation} operation - The operation to process
* @param {string} actionType - The action to perform ('add', 'update', 'confirm', 'reject')
*/
function processQuerysetStores(operation, actionType) {
const ModelClass = operation.queryset.ModelClass;
const queryset = operation.queryset;
// Apply the appropriate action to a single queryset store
const applyAction = (store) => {
switch (actionType) {
case 'add':
store.addOperation(operation);
break;
case 'update':
store.updateOperation(operation);
break;
case 'confirm':
store.confirm(operation);
break;
case 'reject':
store.reject(operation);
break;
}
};
let querysetStoreMap;
// Different routing strategies based on operation type
switch (operation.type) {
case Type.CREATE:
case Type.GET_OR_CREATE:
case Type.UPDATE_OR_CREATE:
// For creates, route to related querysets (they might want to include the new item)
querysetStoreMap = relatedQuerysets(queryset);
break;
case Type.UPDATE:
case Type.UPDATE_INSTANCE:
case Type.DELETE:
case Type.DELETE_INSTANCE:
// No need to do anything, the model will change the queryset local filtering will handle it
querysetStoreMap = new Map();
break;
case Type.CHECKPOINT:
// No need to do anything, the model will change the queryset local filtering will handle it
querysetStoreMap = new Map();
break;
default:
// For other operation types, use the existing related querysets logic
querysetStoreMap = relatedQuerysets(queryset);
break;
}
Array.from(querysetStoreMap.values()).forEach(applyAction);
}
/**
* Process an operation in the metric stores based on operation type
*
* @param {Operation} operation - The operation to process
* @param {string} actionType - The action to perform ('add', 'update', 'confirm', 'reject')
*/
function processMetricStores(operation, actionType) {
const queryset = operation.queryset;
const ModelClass = queryset.ModelClass;
// For metrics, we can use a similar strategy but might be more conservative
// and always use related querysets since metrics are aggregations
const allQuerysets = Array.from(relatedQuerysets(queryset).keys());
let allMetricStores = new Set();
allQuerysets.forEach(qs => {
const stores = metricRegistry.getAllStoresForQueryset(qs);
if (stores && stores.length > 0) {
stores.forEach(store => allMetricStores.add(store));
}
});
if (!allMetricStores || allMetricStores.size === 0) {
return;
}
// Apply the action to each matching metric store
allMetricStores.forEach(store => {
switch (actionType) {
case 'add':
store.addOperation(operation);
break;
case 'update':
store.updateOperation(operation);
break;
case 'confirm':
store.confirm(operation);
break;
case 'reject':
store.reject(operation);
break;
}
});
}
/**
* Common processing logic for operations, handling validation and routing
* to the appropriate store processors
*
* @param {Operation} operation - The operation to process
* @param {string} actionType - The action to perform ('add', 'update', 'confirm', 'reject')
*/
function processOperation(operation, actionType) {
if (!operation || !operation.queryset || !operation.queryset.ModelClass) {
console.warn(`Received invalid operation in processOperation (${actionType})`, operation);
return;
}
if (operation.doNotPropagate) {
return;
}
// Process model store first
processModelStore(operation, actionType);
// Then process queryset stores with improved routing
processQuerysetStores(operation, actionType);
// Finally process metric stores
processMetricStores(operation, actionType);
}
// Define handlers as named arrow functions at the top level
const handleOperationCreated = operation => processOperation(operation, 'add');
const handleOperationUpdated = operation => processOperation(operation, 'update');
const handleOperationMutated = operation => processOperation(operation, 'update');
const handleOperationConfirmed = operation => processOperation(operation, 'confirm');
const handleOperationRejected = operation => processOperation(operation, 'reject');
/**
* Initialize the operation event handler system by setting up event listeners
*/
export function initEventHandler() {
operationEvents.on(Status.CREATED, handleOperationCreated);
operationEvents.on(Status.UPDATED, handleOperationUpdated);
operationEvents.on(Status.CONFIRMED, handleOperationConfirmed);
operationEvents.on(Status.REJECTED, handleOperationRejected);
operationEvents.on(Status.MUTATED, handleOperationMutated);
console.log('Operation event handler initialized');
}
/**
* Clean up by removing all event listeners
*/
export function cleanupEventHandler() {
operationEvents.off(Status.CREATED, handleOperationCreated);
operationEvents.off(Status.UPDATED, handleOperationUpdated);
operationEvents.off(Status.CONFIRMED, handleOperationConfirmed);
operationEvents.off(Status.REJECTED, handleOperationRejected);
operationEvents.off(Status.MUTATED, handleOperationMutated);
console.log('Operation event handler cleaned up');
}