@statezero/core
Version:
The type-safe frontend client for StateZero - connect directly to your backend models with zero boilerplate
207 lines (206 loc) • 8.34 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 are in root mode (materialized with no materialized parent)
* Since filtered querysets render by filtering their parent's data, we only need
* to route operations to root querysets. Filtered children will see the operations
* when they filter their parent's rendered data.
* @param {QuerySet} queryset
* @returns {Map<QuerySet, Store>}
*/
function getRootQuerysets(queryset) {
const modelClass = queryset.ModelClass;
const result = new Map();
// Route only to querysets that are in root mode
// Note: _stores is Map<semanticKey, Store>, so we get the queryset from store.queryset
Array.from(querysetStoreRegistry._stores.entries()).forEach(([semanticKey, store]) => {
if (store.modelClass !== modelClass)
return;
// Use the graph to determine if this store is in root mode
const { isRoot, root } = querysetStoreRegistry.querysetStoreGraph.findRoot(store.queryset);
// A queryset is in root mode if isRoot=true and root is its own semantic key
if (isRoot && root === store.queryset.semanticKey) {
result.set(store.queryset, store);
}
});
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.BULK_CREATE:
case Type.GET_OR_CREATE:
case Type.UPDATE_OR_CREATE:
// For creates, route to root querysets (they might want to include the new item)
querysetStoreMap = getRootQuerysets(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 root querysets logic
querysetStoreMap = getRootQuerysets(queryset);
break;
}
Array.from(querysetStoreMap.values()).forEach(applyAction);
}
/**
* Process an operation in the metric stores
*
* For metrics, we route operations UP the family tree - any metric on an ancestor
* queryset should receive the operation so it can check if it affects the metric.
*
* @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 allMetricStores = new Set();
// Walk up the queryset family tree and collect all metrics
let current = queryset;
while (current) {
const stores = metricRegistry.getAllStoresForQueryset(current);
if (stores && stores.length > 0) {
stores.forEach(store => allMetricStores.add(store));
}
current = current.__parent;
}
if (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');
}