UNPKG

@statezero/core

Version:

The type-safe frontend client for StateZero - connect directly to your backend models with zero boilerplate

172 lines (171 loc) 6.01 kB
import { MetricStore } from '../stores/metricStore'; import { isNil, isEmpty } from 'lodash-es'; import { querysetStoreRegistry } from './querysetStoreRegistry'; import { QueryExecutor } from '../../flavours/django/queryExecutor'; import { wrapReactiveMetric } from '../../reactiveAdaptor.js'; import hash from 'object-hash'; /** * LiveMetric, a simple wrapper that always returns the latest metric value * using a getter for the value property */ export class LiveMetric { constructor(queryset, metricType, field = null) { this.queryset = queryset; this.metricType = metricType; this.field = field; } get lqs() { return querysetStoreRegistry.getEntity(this.queryset); } /** * Refresh the metric data from the database * Delegates to the underlying store's sync method */ refreshFromDb() { const store = metricRegistry.getStore(this.metricType, this.queryset, this.field); return store.sync(true); } /** * Getter that always returns the current value from the store */ get value() { // Get the latest store from the registry const store = metricRegistry.getStore(this.metricType, this.queryset, this.field); if (!store) { return null; } // Render the current value return store.render(); } } /** * Registry to manage metric stores */ export class MetricRegistry { constructor() { // Store both the store and a reference to the queryset as a tuple this._stores = new Map(); this.syncManager = null; } clear() { for (const entry of this._stores.values()) { this.syncManager.unfollowModel(this, entry.store.modelClass); } this._stores = new Map(); } setSyncManager(syncManager) { this.syncManager = syncManager; } /** * Get all metric stores that match a specific queryset * @param {Queryset} queryset - The queryset to match * @returns {Array} Array of metric stores */ getAllStoresForQueryset(queryset) { if (isNil(queryset) || isNil(queryset.ModelClass)) { return []; } const stores = []; const modelClass = queryset.ModelClass; for (const [key, entry] of this._stores.entries()) { const store = entry.store; // First check if model class matches if (store.modelClass !== modelClass) { continue; } if (store.queryset.semanticKey != queryset.semanticKey) { continue; } stores.push(store); } return stores; } _makeKey(metricType, queryset, field = null) { const modelClass = queryset.ModelClass; const ast = queryset.build(); return `${metricType}::${modelClass.configKey}::${modelClass.modelName}::${field || 'null'}::${hash(ast)}`; } /** * Get a metric store for a specific queryset and metric type */ getStore(metricType, queryset, field = null) { if (isNil(metricType) || isNil(queryset) || isNil(queryset.ModelClass)) { return null; } const modelClass = queryset.ModelClass; const ast = queryset.build(); const key = this._makeKey(metricType, queryset, field); if (!this._stores.has(key)) { // Create fetch function for this metric const fetchMetricFn = async ({ metricType, modelClass, field, ast }) => { const qs = queryset.clone(); return await QueryExecutor.executeAgg(qs, metricType, { field }); }; // Create new store const store = new MetricStore(metricType, modelClass, queryset, field, ast, fetchMetricFn); // Store both the store and a reference to the queryset this._stores.set(key, { store, queryset // This keeps the queryset alive }); this.syncManager.followModel(this, store.modelClass); } return this._stores.get(key).store; } /** * Get a metric value for a specific queryset */ getEntity(metricType, queryset, field = null) { // defensive checks if (isNil(metricType) || isNil(queryset) || isNil(queryset.ModelClass)) { return null; } // Get the store (creates if doesn't exist) const store = this.getStore(metricType, queryset, field); // Create a LiveMetric that will always get the latest store and value const liveMetric = new LiveMetric(queryset, metricType, field); // If we're in a reactive environment, wrap the metric return wrapReactiveMetric(liveMetric); } /** * Set metric ground truth value */ setEntity(metricType, queryset, field, value) { // defensive checks if (isNil(metricType) || isNil(queryset) || isNil(queryset.ModelClass)) { return null; } const store = this.getStore(metricType, queryset, field); if (!store) { return null; } // check if the store already has a ground truth or operations if (!isNil(store.groundTruthValue) || !isEmpty(store.operations)) { store.reset(); } store.setGroundTruth(value); return value; } /** * Sync a specific metric with the server */ syncMetric(metricType, queryset, field = null) { const store = this.getStore(metricType, queryset, field); if (store) { return store.sync(); } return null; } /** * Get the queryset for a specific key * Useful for debugging or auditing */ getQuerysetForKey(key) { if (this._stores.has(key)) { return this._stores.get(key).queryset; } return null; } } // Export singleton instance export const metricRegistry = new MetricRegistry();