@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
JavaScript
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();