UNPKG

@statezero/core

Version:

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

227 lines (226 loc) 9.25 kB
import { getAllEventReceivers } from "../core/eventReceivers"; import { operationRegistry } from "./stores/operation"; import { initializeAllEventReceivers } from "../config"; import { getEventReceiver } from "../core/eventReceivers"; import { querysetStoreRegistry, QuerysetStoreRegistry } from "./registries/querysetStoreRegistry"; import { modelStoreRegistry, ModelStoreRegistry } from "./registries/modelStoreRegistry"; import { metricRegistry, MetricRegistry } from "./registries/metricRegistry"; import { getModelClass, getConfig } from "../config"; import { isNil } from "lodash-es"; import { QuerysetStore } from "./stores/querysetStore"; export class EventPayload { constructor(data) { this.event = data.event; this.model = data.model; this.operation_id = data.operation_id; this.pk_field_name = data.pk_field_name; this.configKey = data.configKey; this.instances = data.instances; this._cachedInstances = null; } get modelClass() { return getModelClass(this.model, this.configKey); } async getFullInstances() { if (this.event === 'delete') { throw new Error("Cannot fetch full instances for delete operation bozo..."); } if (isNil(this._cachedInstances)) { this._cachedInstances = await this.modelClass.objects .filter({ [`${this.modelClass.primaryKeyField}__in`]: this.instances.map((instance) => instance[this.modelClass.primaryKeyField]), }).fetch(); } return this._cachedInstances; } } export class SyncManager { constructor() { this.handleEvent = (event) => { let payload = new EventPayload(event); let isLocalOperation = operationRegistry.has(payload.operation_id); if (this.registries.has(MetricRegistry)) { this.processMetrics(payload); } if (isLocalOperation) { // This is a local operation, so don't resync querysets or models return; } if (this.registries.has(QuerysetStoreRegistry)) { this.processQuerysets(payload); } if (this.registries.has(ModelStoreRegistry)) { this.processModels(payload); } }; this.registries = new Map(); // Map of registries to model sets this.followedModels = new Map(); // Map of querysets to keep synced this.followAllQuerysets = true; this.followedQuerysets = new Map(); this.periodicSyncTimer = null; } /** * Initialize event handlers for all event receivers */ initialize() { // Initialize all event receivers initializeAllEventReceivers(); // Get all registered event receivers const eventReceivers = getAllEventReceivers(); // Register the event handlers with each receiver eventReceivers.forEach((receiver, configKey) => { if (receiver) { // Model events go to handleEvent receiver.addModelEventHandler(this.handleEvent.bind(this)); } }); this.startPeriodicSync(); } startPeriodicSync() { if (this.periodicSyncTimer) return; try { const config = getConfig(); const intervalSeconds = config.periodicSyncIntervalSeconds; // If null or undefined, don't start periodic sync if (!intervalSeconds) { console.log("[SyncManager] Periodic sync disabled (set to null)"); return; } const intervalMs = intervalSeconds * 1000; this.periodicSyncTimer = setInterval(() => { this.syncStaleQuerysets(); }, intervalMs); console.log(`[SyncManager] Periodic sync started: ${intervalSeconds}s intervals`); } catch (error) { // If no config, don't start periodic sync by default console.log("[SyncManager] No config found, periodic sync disabled by default"); } } syncStaleQuerysets() { let syncedCount = 0; // Sync all followed querysets - keep it simple const querysetRegistry = this.registries.get(QuerysetStoreRegistry); if (querysetRegistry) { for (const [semanticKey, store] of querysetRegistry._stores.entries()) { // Only sync if this store is actually being followed const isFollowed = this.isStoreFollowed(querysetRegistry, semanticKey); if (this.followAllQuerysets || isFollowed) { store.sync(); syncedCount++; } } } if (syncedCount > 0) { console.log(`[SyncManager] Periodic sync: ${syncedCount} stores synced`); } } isStoreFollowed(registry, semanticKey) { const followingQuerysets = registry.followingQuerysets.get(semanticKey); if (!followingQuerysets) return false; return [...followingQuerysets].some((queryset) => { return this.isQuerysetFollowed(queryset); }); } cleanup() { if (this.periodicSyncTimer) { clearInterval(this.periodicSyncTimer); this.periodicSyncTimer = null; } } followModel(registry, modelClass) { const models = this.followedModels.get(registry) || new Set(); this.followedModels.set(registry, models); if (models.has(modelClass)) return; const alreadyFollowed = [...this.followedModels.values()].some((set) => set.has(modelClass)); models.add(modelClass); if (!alreadyFollowed) { getEventReceiver(modelClass.configKey)?.subscribe(modelClass.modelName, this.handleEvent); } } unfollowModel(registry, modelClass) { const models = this.followedModels.get(registry); if (!models) return; models.delete(modelClass); const stillFollowed = [...this.followedModels.values()].some((set) => set.has(modelClass)); if (!stillFollowed) { getEventReceiver(modelClass.configKey)?.unsubscribe(modelClass.modelName, this.handleEvent); } } manageRegistry(registry) { this.registries.set(registry.constructor, registry); registry.setSyncManager(this); } removeRegistry(registry) { this.registries.delete(registry.constructor); } isQuerysetFollowed(queryset) { const activeSemanticKeys = new Set([...this.followedQuerysets].map((qs) => qs.semanticKey)); let current = queryset; while (current) { if (activeSemanticKeys.has(current.semanticKey)) { return true; } current = current.__parent; } return false; } processQuerysets(event) { const registry = this.registries.get(QuerysetStoreRegistry); for (const [semanticKey, store] of registry._stores.entries()) { if (store.modelClass.modelName === event.model && store.modelClass.configKey === event.configKey) { if (this.followAllQuerysets) { store.sync(); continue; } // Get the set of querysets following this semantic key const followingQuerysets = registry.followingQuerysets.get(semanticKey); if (followingQuerysets) { // Use some() to break early when we find a match const shouldSync = [...followingQuerysets].some((queryset) => { return this.isQuerysetFollowed(queryset); }); if (shouldSync) { console.log(`syncing store for semantic key: ${semanticKey}`); store.sync(); } } } } } processMetrics(event) { const registry = this.registries.get(MetricRegistry); for (const [key, entry] of registry._stores.entries()) { // Check if the store has a queryset with the model class AND correct backend if (entry.queryset && entry.queryset.modelClass.modelName === event.model && entry.queryset.modelClass.configKey === event.configKey) { if (this.followAllQuerysets) { entry.store.sync(); continue; } // Check if this queryset (or any parent) is being followed if (this.isQuerysetFollowed(entry.queryset)) { console.log(`syncing metric store for key: ${key}`); entry.store.sync(); } } } } processModels(event) { // No need to sync models, because everything that is live, is synced return; } } const syncManager = new SyncManager(); syncManager.manageRegistry(querysetStoreRegistry); syncManager.manageRegistry(modelStoreRegistry); syncManager.manageRegistry(metricRegistry); export { syncManager };