@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
JavaScript
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 };