@statezero/core
Version:
The type-safe frontend client for StateZero - connect directly to your backend models with zero boilerplate
133 lines (132 loc) • 6.17 kB
JavaScript
import { reactive, ref, nextTick } from "vue";
import { modelEventEmitter, querysetEventEmitter, metricEventEmitter } from "../../syncEngine/stores/reactivity.js";
import { initEventHandler } from "../../syncEngine/stores/operationEventHandlers.js";
import { isEqual, isNil } from 'lodash-es';
import hash from 'object-hash';
import { registerAdapterReset } from "../../reset.js";
initEventHandler();
const wrappedQuerysetCache = new Map();
const wrappedMetricCache = new Map();
/**
* Adapts a model instance to a Vue reactive object by directly wrapping
* the instance and incrementing an internal version on relevant events.
*
* @param {Object} modelInstance - An instance of a model class with static modelName and primaryKeyField
* @param {Function} [reactivityFn=reactive] - Which Vue reactivity function to use (reactive or ref)
* @returns {import('vue').Reactive|import('vue').Ref} The reactive model instance, augmented with __version
*/
export function ModelAdaptor(modelInstance, reactivityFn = reactive) {
const modelClass = modelInstance.constructor;
const modelName = modelClass.modelName;
const configKey = modelClass.configKey;
const pkField = modelClass.primaryKeyField;
// Make the model instance reactive using the specified function
const wrapper = reactivityFn(modelInstance);
const eventName = `${configKey}::${modelName}::render`;
// Handler bumps version to trigger Vue reactivity when this instance updates
const renderHandler = (eventData) => {
const isRef = reactivityFn === ref;
const model = isRef ? wrapper.value : wrapper;
const modelPk = model[pkField];
// Check if this model's pk is in the event's pks array
if (eventData.pks && eventData.pks.includes(modelPk)) {
if (isRef) {
wrapper.value.touch();
}
else {
wrapper.touch();
}
}
};
// Subscribe to model events indefinitely
modelEventEmitter.on(eventName, renderHandler);
return wrapper;
}
/**
* Adapts a queryset to a Vue reactive object and sets up event handling for queryset updates
*
* @param {Object} liveQuerySet - A LiveQueryset instance
* @param {Function} [reactivityFn=reactive] - Which Vue reactivity function to use (reactive or ref)
* @returns {import('vue').Reactive|import('vue').Ref} The reactive queryset
*/
export function QuerySetAdaptor(liveQuerySet, reactivityFn = reactive) {
const queryset = liveQuerySet?.queryset;
const modelName = queryset?.ModelClass?.modelName;
const configKey = queryset?.ModelClass?.configKey;
if (isNil(queryset) || isNil(modelName)) {
throw new Error(`liveQuerySet ${JSON.stringify(liveQuerySet)} had null qs: ${queryset} or model: ${modelName}`);
}
// Use the semantic key if available
const cacheKey = queryset.semanticKey;
// Check the cache first
if (cacheKey && wrappedQuerysetCache.has(cacheKey)) {
return wrappedQuerysetCache.get(cacheKey);
}
const querysetAst = queryset.build();
// Make the queryset reactive using the specified function
const wrapper = reactivityFn([...liveQuerySet]);
wrapper.original = liveQuerySet;
const eventName = `${configKey}::${modelName}::queryset::render`;
// Handler bumps version to trigger Vue reactivity when this queryset updates
const renderHandler = (eventData) => {
if (eventData && eventData.ast && isEqual(querysetAst, eventData.ast)) {
if (reactivityFn === ref) {
wrapper.value = [...liveQuerySet];
}
else {
wrapper.splice(0, wrapper.length);
wrapper.push(...liveQuerySet);
}
}
};
// Subscribe to queryset events indefinitely
querysetEventEmitter.on(eventName, renderHandler);
/* Dont delete the innocuous looking queryset.length check. There is some weird interaction
with vue, where when we load an empty queryset from the cache, the reactivity completely breaks.
I wasted over 2 days on this bug, and it won't show up in the e2e tests because it just impacts the
vue reactivity. If this causes performance issues, and needs to be refactored. Make sure you understand
the vue reactivity interaction correctly and find a different way to fix the broken reactivity for empty querysets */
if (cacheKey && liveQuerySet && liveQuerySet.length > 0) {
wrappedQuerysetCache.set(cacheKey, wrapper);
}
return wrapper;
}
export function MetricAdaptor(metric) {
const queryset = metric.queryset;
const modelName = queryset?.ModelClass?.modelName;
const configKey = queryset?.ModelClass?.configKey;
const querysetAst = queryset.build();
// Create a cache key based on metric properties
// This combines model, metric type, field, and the queryset AST hash to ensure uniqueness
const cacheKey = `${configKey}::${modelName}::${metric.metricType}::${metric.field}::${hash(querysetAst)}`;
// Check the cache first
if (cacheKey && wrappedMetricCache.has(cacheKey)) {
return wrappedMetricCache.get(cacheKey);
}
// Create a reactive reference with the initial value
const wrapper = ref(metric.value);
wrapper.original = metric;
// Single handler for metric render events
const metricRenderHandler = (eventData) => {
// Only update if this event is for our metric
if (eventData.metricType === metric.metricType &&
eventData.ModelClass === metric.queryset.ModelClass &&
eventData.field === metric.field &&
eventData.ast === hash(querysetAst) &&
eventData.valueChanged === true) {
// Update the wrapper value with the latest metric value
wrapper.value = metric.value;
}
};
// Only listen for metric render events
metricEventEmitter.on('metric::render', metricRenderHandler);
// Store in cache
if (cacheKey) {
wrappedMetricCache.set(cacheKey, wrapper);
}
return wrapper;
}
registerAdapterReset(() => {
wrappedQuerysetCache.clear();
wrappedMetricCache.clear();
});