UNPKG

@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
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'); }