@statezero/core 0.1.68 → 0.1.70
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -128,8 +128,15 @@ export class ModelStore {
|
|
|
128
128
|
return;
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
|
-
item
|
|
132
|
-
|
|
131
|
+
if (item && typeof item.serialize === "function") {
|
|
132
|
+
const serializedItem = item.serialize();
|
|
133
|
+
serializedItem[pkField] = pk;
|
|
134
|
+
nonTempPkItems.push(serializedItem);
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
item[pkField] = pk;
|
|
138
|
+
nonTempPkItems.push(item);
|
|
139
|
+
}
|
|
133
140
|
});
|
|
134
141
|
this.modelCache.set(this.cacheKey, nonTempPkItems);
|
|
135
142
|
}
|
|
@@ -16,6 +16,12 @@ export class SyncManager {
|
|
|
16
16
|
followAllQuerysets: boolean;
|
|
17
17
|
followedQuerysets: Map<any, any>;
|
|
18
18
|
periodicSyncTimer: NodeJS.Timeout | null;
|
|
19
|
+
eventBatch: Map<any, any>;
|
|
20
|
+
debounceTimer: NodeJS.Timeout | null;
|
|
21
|
+
maxWaitTimer: NodeJS.Timeout | null;
|
|
22
|
+
debounceMs: number;
|
|
23
|
+
maxWaitMs: number;
|
|
24
|
+
batchStartTime: number | null;
|
|
19
25
|
/**
|
|
20
26
|
* Initialize event handlers for all event receivers
|
|
21
27
|
*/
|
|
@@ -29,8 +35,9 @@ export class SyncManager {
|
|
|
29
35
|
manageRegistry(registry: any): void;
|
|
30
36
|
removeRegistry(registry: any): void;
|
|
31
37
|
handleEvent: (event: any) => void;
|
|
38
|
+
processBatch(reason?: string): void;
|
|
32
39
|
isQuerysetFollowed(queryset: any): boolean;
|
|
33
|
-
|
|
40
|
+
processQuerysetsBatch(representativeEvent: any, allEvents: any): void;
|
|
34
41
|
processMetrics(event: any): void;
|
|
35
42
|
processModels(event: any): void;
|
|
36
43
|
}
|
package/dist/syncEngine/sync.js
CHANGED
|
@@ -2,8 +2,8 @@ import { getAllEventReceivers } from "../core/eventReceivers";
|
|
|
2
2
|
import { operationRegistry } from "./stores/operation";
|
|
3
3
|
import { initializeAllEventReceivers } from "../config";
|
|
4
4
|
import { getEventReceiver } from "../core/eventReceivers";
|
|
5
|
-
import { querysetStoreRegistry, QuerysetStoreRegistry } from "./registries/querysetStoreRegistry";
|
|
6
|
-
import { modelStoreRegistry, ModelStoreRegistry } from "./registries/modelStoreRegistry";
|
|
5
|
+
import { querysetStoreRegistry, QuerysetStoreRegistry, } from "./registries/querysetStoreRegistry";
|
|
6
|
+
import { modelStoreRegistry, ModelStoreRegistry, } from "./registries/modelStoreRegistry";
|
|
7
7
|
import { metricRegistry, MetricRegistry } from "./registries/metricRegistry";
|
|
8
8
|
import { getModelClass, getConfig } from "../config";
|
|
9
9
|
import { isNil } from "lodash-es";
|
|
@@ -22,14 +22,15 @@ export class EventPayload {
|
|
|
22
22
|
return getModelClass(this.model, this.configKey);
|
|
23
23
|
}
|
|
24
24
|
async getFullInstances() {
|
|
25
|
-
if (this.event ===
|
|
25
|
+
if (this.event === "delete") {
|
|
26
26
|
throw new Error("Cannot fetch full instances for delete operation bozo...");
|
|
27
27
|
}
|
|
28
28
|
if (isNil(this._cachedInstances)) {
|
|
29
29
|
this._cachedInstances = await this.modelClass.objects
|
|
30
30
|
.filter({
|
|
31
31
|
[`${this.modelClass.primaryKeyField}__in`]: this.instances.map((instance) => instance[this.modelClass.primaryKeyField]),
|
|
32
|
-
})
|
|
32
|
+
})
|
|
33
|
+
.fetch();
|
|
33
34
|
}
|
|
34
35
|
return this._cachedInstances;
|
|
35
36
|
}
|
|
@@ -39,19 +40,36 @@ export class SyncManager {
|
|
|
39
40
|
this.handleEvent = (event) => {
|
|
40
41
|
let payload = new EventPayload(event);
|
|
41
42
|
let isLocalOperation = operationRegistry.has(payload.operation_id);
|
|
43
|
+
// Always process metrics immediately (they're lightweight)
|
|
42
44
|
if (this.registries.has(MetricRegistry)) {
|
|
43
45
|
this.processMetrics(payload);
|
|
44
46
|
}
|
|
45
47
|
if (isLocalOperation) {
|
|
46
|
-
// This is a local operation, so don't resync querysets or models
|
|
47
48
|
return;
|
|
48
49
|
}
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
// Add to batch for queryset/model processing
|
|
51
|
+
const key = `${event.model}::${event.configKey}`;
|
|
52
|
+
const now = Date.now();
|
|
53
|
+
// If this is the first event in the batch, start max wait timer
|
|
54
|
+
if (this.eventBatch.size === 0) {
|
|
55
|
+
this.batchStartTime = now;
|
|
56
|
+
this.maxWaitTimer = setTimeout(() => {
|
|
57
|
+
this.processBatch("maxWait");
|
|
58
|
+
}, this.maxWaitMs);
|
|
51
59
|
}
|
|
52
|
-
|
|
53
|
-
|
|
60
|
+
this.eventBatch.set(key, {
|
|
61
|
+
event: payload,
|
|
62
|
+
firstSeen: this.eventBatch.has(key)
|
|
63
|
+
? this.eventBatch.get(key).firstSeen
|
|
64
|
+
: now,
|
|
65
|
+
});
|
|
66
|
+
// Reset debounce timer
|
|
67
|
+
if (this.debounceTimer) {
|
|
68
|
+
clearTimeout(this.debounceTimer);
|
|
54
69
|
}
|
|
70
|
+
this.debounceTimer = setTimeout(() => {
|
|
71
|
+
this.processBatch("debounce");
|
|
72
|
+
}, this.debounceMs);
|
|
55
73
|
};
|
|
56
74
|
this.registries = new Map();
|
|
57
75
|
// Map of registries to model sets
|
|
@@ -60,6 +78,13 @@ export class SyncManager {
|
|
|
60
78
|
this.followAllQuerysets = true;
|
|
61
79
|
this.followedQuerysets = new Map();
|
|
62
80
|
this.periodicSyncTimer = null;
|
|
81
|
+
// Batching for event processing
|
|
82
|
+
this.eventBatch = new Map(); // model::configKey -> { event, firstSeen }
|
|
83
|
+
this.debounceTimer = null;
|
|
84
|
+
this.maxWaitTimer = null;
|
|
85
|
+
this.debounceMs = 100; // Wait for rapid events to settle
|
|
86
|
+
this.maxWaitMs = 2000; // Maximum time to hold events
|
|
87
|
+
this.batchStartTime = null;
|
|
63
88
|
}
|
|
64
89
|
/**
|
|
65
90
|
* Initialize event handlers for all event receivers
|
|
@@ -131,6 +156,15 @@ export class SyncManager {
|
|
|
131
156
|
clearInterval(this.periodicSyncTimer);
|
|
132
157
|
this.periodicSyncTimer = null;
|
|
133
158
|
}
|
|
159
|
+
// Clean up batch timers
|
|
160
|
+
if (this.debounceTimer) {
|
|
161
|
+
clearTimeout(this.debounceTimer);
|
|
162
|
+
this.debounceTimer = null;
|
|
163
|
+
}
|
|
164
|
+
if (this.maxWaitTimer) {
|
|
165
|
+
clearTimeout(this.maxWaitTimer);
|
|
166
|
+
this.maxWaitTimer = null;
|
|
167
|
+
}
|
|
134
168
|
}
|
|
135
169
|
followModel(registry, modelClass) {
|
|
136
170
|
const models = this.followedModels.get(registry) || new Set();
|
|
@@ -160,6 +194,43 @@ export class SyncManager {
|
|
|
160
194
|
removeRegistry(registry) {
|
|
161
195
|
this.registries.delete(registry.constructor);
|
|
162
196
|
}
|
|
197
|
+
processBatch(reason = "unknown") {
|
|
198
|
+
if (this.eventBatch.size === 0)
|
|
199
|
+
return;
|
|
200
|
+
// Clear timers
|
|
201
|
+
if (this.debounceTimer) {
|
|
202
|
+
clearTimeout(this.debounceTimer);
|
|
203
|
+
this.debounceTimer = null;
|
|
204
|
+
}
|
|
205
|
+
if (this.maxWaitTimer) {
|
|
206
|
+
clearTimeout(this.maxWaitTimer);
|
|
207
|
+
this.maxWaitTimer = null;
|
|
208
|
+
}
|
|
209
|
+
const events = Array.from(this.eventBatch.values()).map((item) => item.event);
|
|
210
|
+
const waitTime = Date.now() - this.batchStartTime;
|
|
211
|
+
this.eventBatch.clear();
|
|
212
|
+
this.batchStartTime = null;
|
|
213
|
+
console.log(`[SyncManager] Processing batch of ${events.length} events (reason: ${reason}, waited: ${waitTime}ms)`);
|
|
214
|
+
// Group events by model for efficient processing
|
|
215
|
+
const eventsByModel = new Map();
|
|
216
|
+
events.forEach((event) => {
|
|
217
|
+
const key = `${event.model}::${event.configKey}`;
|
|
218
|
+
if (!eventsByModel.has(key)) {
|
|
219
|
+
eventsByModel.set(key, []);
|
|
220
|
+
}
|
|
221
|
+
eventsByModel.get(key).push(event);
|
|
222
|
+
});
|
|
223
|
+
// Process each model's events as a batch
|
|
224
|
+
eventsByModel.forEach((modelEvents, modelKey) => {
|
|
225
|
+
const event = modelEvents[0]; // Use first event as representative
|
|
226
|
+
if (this.registries.has(QuerysetStoreRegistry)) {
|
|
227
|
+
this.processQuerysetsBatch(event, modelEvents);
|
|
228
|
+
}
|
|
229
|
+
if (this.registries.has(ModelStoreRegistry)) {
|
|
230
|
+
this.processModels(event);
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
}
|
|
163
234
|
isQuerysetFollowed(queryset) {
|
|
164
235
|
const activeSemanticKeys = new Set([...this.followedQuerysets].map((qs) => qs.semanticKey));
|
|
165
236
|
let current = queryset;
|
|
@@ -171,29 +242,36 @@ export class SyncManager {
|
|
|
171
242
|
}
|
|
172
243
|
return false;
|
|
173
244
|
}
|
|
174
|
-
|
|
245
|
+
processQuerysetsBatch(representativeEvent, allEvents) {
|
|
175
246
|
const registry = this.registries.get(QuerysetStoreRegistry);
|
|
247
|
+
// Collect all stores that need syncing for this model
|
|
248
|
+
const storesToSync = [];
|
|
176
249
|
for (const [semanticKey, store] of registry._stores.entries()) {
|
|
177
|
-
if (store.modelClass.modelName ===
|
|
178
|
-
store.modelClass.configKey ===
|
|
250
|
+
if (store.modelClass.modelName === representativeEvent.model &&
|
|
251
|
+
store.modelClass.configKey === representativeEvent.configKey) {
|
|
179
252
|
if (this.followAllQuerysets) {
|
|
180
|
-
|
|
253
|
+
storesToSync.push(store);
|
|
181
254
|
continue;
|
|
182
255
|
}
|
|
183
|
-
// Get the set of querysets following this semantic key
|
|
184
256
|
const followingQuerysets = registry.followingQuerysets.get(semanticKey);
|
|
185
257
|
if (followingQuerysets) {
|
|
186
|
-
// Use some() to break early when we find a match
|
|
187
258
|
const shouldSync = [...followingQuerysets].some((queryset) => {
|
|
188
259
|
return this.isQuerysetFollowed(queryset);
|
|
189
260
|
});
|
|
190
261
|
if (shouldSync) {
|
|
191
|
-
|
|
192
|
-
store.sync();
|
|
262
|
+
storesToSync.push(store);
|
|
193
263
|
}
|
|
194
264
|
}
|
|
195
265
|
}
|
|
196
266
|
}
|
|
267
|
+
// Sync all relevant stores for this model
|
|
268
|
+
console.log(`[SyncManager] Syncing ${storesToSync.length} queryset stores for ${representativeEvent.model}`);
|
|
269
|
+
storesToSync.forEach((store) => {
|
|
270
|
+
// Don't await - let them run in parallel
|
|
271
|
+
store.sync().catch((error) => {
|
|
272
|
+
console.error(`[SyncManager] Failed to sync queryset store:`, error);
|
|
273
|
+
});
|
|
274
|
+
});
|
|
197
275
|
}
|
|
198
276
|
processMetrics(event) {
|
|
199
277
|
const registry = this.registries.get(MetricRegistry);
|
|
@@ -215,8 +293,32 @@ export class SyncManager {
|
|
|
215
293
|
}
|
|
216
294
|
}
|
|
217
295
|
processModels(event) {
|
|
218
|
-
|
|
219
|
-
|
|
296
|
+
const registry = this.registries.get(ModelStoreRegistry);
|
|
297
|
+
if (!registry)
|
|
298
|
+
return;
|
|
299
|
+
// Get the model class for this event
|
|
300
|
+
const modelClass = event.modelClass;
|
|
301
|
+
if (!modelClass)
|
|
302
|
+
return;
|
|
303
|
+
// Get the model store for this model class
|
|
304
|
+
const modelStore = registry.getStore(modelClass);
|
|
305
|
+
if (!modelStore)
|
|
306
|
+
return;
|
|
307
|
+
// Event instances are just PKs - find which ones we have locally
|
|
308
|
+
const eventPks = event.instances || [];
|
|
309
|
+
// Get all currently rendered instances (includes ground truth + operations)
|
|
310
|
+
const renderedInstances = modelStore.render();
|
|
311
|
+
const localPks = new Set(renderedInstances.map((instance) => instance[modelClass.primaryKeyField]));
|
|
312
|
+
const pksToSync = eventPks.filter((pk) => localPks.has(pk));
|
|
313
|
+
if (pksToSync.length === 0) {
|
|
314
|
+
console.log(`[SyncManager] No locally cached instances to sync for ${event.model}`);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
// Sync only the PKs that are both in the event and locally cached
|
|
318
|
+
console.log(`[SyncManager] Syncing ${pksToSync.length} model instances for ${event.model}`);
|
|
319
|
+
modelStore.sync(pksToSync).catch((error) => {
|
|
320
|
+
console.error(`[SyncManager] Failed to sync model store for ${event.model}:`, error);
|
|
321
|
+
});
|
|
220
322
|
}
|
|
221
323
|
}
|
|
222
324
|
const syncManager = new SyncManager();
|
package/package.json
CHANGED