@statezero/core 0.2.28 → 0.2.30

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.
Files changed (62) hide show
  1. package/dist/actions/backend1/django_app/calculate-hash.js +1 -1
  2. package/dist/actions/backend1/django_app/calculate-hash.schema.json +1 -1
  3. package/dist/actions/backend1/django_app/get-current-username.js +1 -1
  4. package/dist/actions/backend1/django_app/get-current-username.schema.json +1 -1
  5. package/dist/actions/backend1/django_app/get-user-info.js +1 -1
  6. package/dist/actions/backend1/django_app/get-user-info.schema.json +1 -1
  7. package/dist/actions/backend1/django_app/process-data.js +1 -1
  8. package/dist/actions/backend1/django_app/process-data.schema.json +1 -1
  9. package/dist/actions/backend1/django_app/send-notification.js +1 -1
  10. package/dist/actions/backend1/django_app/send-notification.schema.json +1 -1
  11. package/dist/actions/default/django_app/calculate-hash.js +1 -1
  12. package/dist/actions/default/django_app/calculate-hash.schema.json +1 -1
  13. package/dist/actions/default/django_app/get-current-username.js +1 -1
  14. package/dist/actions/default/django_app/get-current-username.schema.json +1 -1
  15. package/dist/actions/default/django_app/get-user-info.js +1 -1
  16. package/dist/actions/default/django_app/get-user-info.schema.json +1 -1
  17. package/dist/actions/default/django_app/process-data.js +1 -1
  18. package/dist/actions/default/django_app/process-data.schema.json +1 -1
  19. package/dist/actions/default/django_app/send-notification.js +1 -1
  20. package/dist/actions/default/django_app/send-notification.schema.json +1 -1
  21. package/dist/flavours/django/makeApiCall.d.ts +14 -1
  22. package/dist/flavours/django/makeApiCall.js +31 -3
  23. package/dist/models/backend1/django_app/comprehensivemodel.schema.json +1 -1
  24. package/dist/models/backend1/django_app/custompkmodel.schema.json +4 -4
  25. package/dist/models/backend1/django_app/dailyrate.schema.json +8 -8
  26. package/dist/models/backend1/django_app/dummymodel.schema.json +2 -2
  27. package/dist/models/backend1/django_app/m2mdepthtestlevel1.schema.json +2 -2
  28. package/dist/models/backend1/django_app/m2mdepthtestlevel2.schema.json +1 -1
  29. package/dist/models/backend1/django_app/m2mdepthtestlevel3.schema.json +5 -5
  30. package/dist/models/backend1/django_app/modelwithrestrictedfields.schema.json +1 -1
  31. package/dist/models/backend1/django_app/namefiltercustompkmodel.schema.json +4 -4
  32. package/dist/models/backend1/django_app/order.schema.json +8 -8
  33. package/dist/models/backend1/django_app/orderitem.schema.json +1 -1
  34. package/dist/models/backend1/django_app/product.schema.json +9 -9
  35. package/dist/models/backend1/django_app/productcategory.schema.json +2 -2
  36. package/dist/models/backend1/django_app/rateplan.schema.json +2 -2
  37. package/dist/models/backend1/django_app/restrictedfieldrelatedmodel.schema.json +2 -2
  38. package/dist/models/default/django_app/comprehensivemodel.schema.json +1 -1
  39. package/dist/models/default/django_app/custompkmodel.schema.json +4 -4
  40. package/dist/models/default/django_app/dailyrate.schema.json +8 -8
  41. package/dist/models/default/django_app/dummymodel.schema.json +2 -2
  42. package/dist/models/default/django_app/m2mdepthtestlevel1.schema.json +2 -2
  43. package/dist/models/default/django_app/m2mdepthtestlevel2.schema.json +1 -1
  44. package/dist/models/default/django_app/m2mdepthtestlevel3.schema.json +5 -5
  45. package/dist/models/default/django_app/modelwithrestrictedfields.schema.json +1 -1
  46. package/dist/models/default/django_app/namefiltercustompkmodel.schema.json +4 -4
  47. package/dist/models/default/django_app/order.schema.json +8 -8
  48. package/dist/models/default/django_app/orderitem.schema.json +1 -1
  49. package/dist/models/default/django_app/product.schema.json +9 -9
  50. package/dist/models/default/django_app/productcategory.schema.json +2 -2
  51. package/dist/models/default/django_app/rateplan.schema.json +2 -2
  52. package/dist/models/default/django_app/restrictedfieldrelatedmodel.schema.json +2 -2
  53. package/dist/syncEngine/registries/querysetStoreGraph.d.ts +15 -5
  54. package/dist/syncEngine/registries/querysetStoreGraph.js +64 -22
  55. package/dist/syncEngine/registries/querysetStoreRegistry.d.ts +14 -10
  56. package/dist/syncEngine/registries/querysetStoreRegistry.js +66 -40
  57. package/dist/syncEngine/stores/operationEventHandlers.js +12 -20
  58. package/dist/syncEngine/stores/querysetStore.d.ts +9 -11
  59. package/dist/syncEngine/stores/querysetStore.js +34 -100
  60. package/dist/syncEngine/sync.d.ts +1 -4
  61. package/dist/syncEngine/sync.js +27 -21
  62. package/package.json +1 -1
@@ -9,13 +9,10 @@ export class QuerysetStore {
9
9
  lastSync: number | null;
10
10
  isTemp: any;
11
11
  pruneThreshold: any;
12
- getRootStore: any;
13
12
  includedPks: Map<any, any>;
14
13
  qsCache: Cache;
15
14
  _lastRenderedPks: any[] | null;
16
15
  renderCallbacks: Set<any>;
17
- _rootUnregister: any;
18
- _currentRootStore: any;
19
16
  _modelStoreUnregister: any;
20
17
  get cacheKey(): any;
21
18
  onHydrated(hydratedData: any): void;
@@ -35,7 +32,6 @@ export class QuerysetStore {
35
32
  getInflightOperations(): any[];
36
33
  prune(): void;
37
34
  registerRenderCallback(callback: any): () => boolean;
38
- _ensureRootRegistration(): void;
39
35
  /**
40
36
  * Helper to validate PKs against the model store and apply local filtering/sorting.
41
37
  * This is the core of the rendering logic.
@@ -43,15 +39,17 @@ export class QuerysetStore {
43
39
  */
44
40
  private _getValidatedAndFilteredPks;
45
41
  render(optimistic?: boolean, fromCache?: boolean): any[];
46
- renderFromRoot(optimistic: boolean | undefined, rootStore: any): any[];
42
+ renderFromData(optimistic?: boolean): any[];
47
43
  /**
48
- * Render by getting ALL instances from the model store and applying
49
- * the queryset's filters locally. Used for temp stores (e.g., optimistic
50
- * chained filters) that don't have their own ground truth.
44
+ * Render by getting all instances from the model store and filtering locally.
45
+ * Used when a queryset has no ground truth (temp stores, newly created stores, etc.)
51
46
  */
52
- renderFromModelStore(optimistic?: boolean): any[];
53
- renderFromData(optimistic?: boolean): any[];
47
+ renderFromModelStore(): any[];
54
48
  applyOperation(operation: any, currentPks: any): any;
55
- sync(forceFromDb?: boolean): Promise<void>;
49
+ /**
50
+ * Sync this queryset with the database.
51
+ * Fetches from DB and sets ground truth.
52
+ */
53
+ sync(): Promise<void>;
56
54
  }
57
55
  import { Cache } from '../cache/cache.js';
@@ -4,10 +4,8 @@ import { isNil, isEmpty, trim, isEqual } from 'lodash-es';
4
4
  import { replaceTempPks, containsTempPk } from '../../flavours/django/tempPk.js';
5
5
  import { modelStoreRegistry } from '../registries/modelStoreRegistry.js';
6
6
  import { processIncludedEntities } from '../../flavours/django/makeApiCall.js';
7
- import hash from 'object-hash';
8
7
  import { Cache } from '../cache/cache.js';
9
8
  import { filter } from "../../filtering/localFiltering.js";
10
- import { mod } from 'mathjs';
11
9
  export class QuerysetStore {
12
10
  constructor(modelClass, fetchFn, queryset, initialGroundTruthPks = null, initialOperations = null, options = {}) {
13
11
  this.modelClass = modelClass;
@@ -17,7 +15,6 @@ export class QuerysetStore {
17
15
  this.lastSync = null;
18
16
  this.isTemp = options.isTemp || false;
19
17
  this.pruneThreshold = options.pruneThreshold || 10;
20
- this.getRootStore = options.getRootStore || null;
21
18
  this.groundTruthPks = initialGroundTruthPks || [];
22
19
  this.operationsMap = new Map();
23
20
  // Track which model PKs are in this queryset's included data
@@ -33,9 +30,7 @@ export class QuerysetStore {
33
30
  this.qsCache = new Cache("queryset-cache", {}, this.onHydrated.bind(this));
34
31
  this._lastRenderedPks = null;
35
32
  this.renderCallbacks = new Set();
36
- this._rootUnregister = null;
37
- this._currentRootStore = null;
38
- this._ensureRootRegistration();
33
+ // Register for model store changes to re-render when model data changes
39
34
  const modelStore = modelStoreRegistry.getStore(this.modelClass);
40
35
  this._modelStoreUnregister = modelStore.registerRenderCallback(() => {
41
36
  this._emitRenderEvent();
@@ -155,28 +150,6 @@ export class QuerysetStore {
155
150
  this.renderCallbacks.add(callback);
156
151
  return () => this.renderCallbacks.delete(callback);
157
152
  }
158
- _ensureRootRegistration() {
159
- if (this.isTemp)
160
- return;
161
- const { isRoot, rootStore } = this.getRootStore(this.queryset);
162
- // If the root store hasn't changed, nothing to do
163
- if (this._currentRootStore === rootStore) {
164
- return;
165
- }
166
- // Root store changed - clean up old registration if it exists
167
- if (this._rootUnregister) {
168
- this._rootUnregister();
169
- this._rootUnregister = null;
170
- }
171
- // Set up new registration if we're derived and have a root store
172
- if (!isRoot && rootStore) {
173
- this._rootUnregister = rootStore.registerRenderCallback(() => {
174
- this._emitRenderEvent();
175
- });
176
- }
177
- // Update current root store reference (could be null now)
178
- this._currentRootStore = rootStore;
179
- }
180
153
  /**
181
154
  * Helper to validate PKs against the model store and apply local filtering/sorting.
182
155
  * This is the core of the rendering logic.
@@ -193,67 +166,29 @@ export class QuerysetStore {
193
166
  return finalPks;
194
167
  }
195
168
  render(optimistic = true, fromCache = false) {
196
- this._ensureRootRegistration();
169
+ // Check cache first if requested
197
170
  if (fromCache) {
198
171
  const cachedResult = this.qsCache.get(this.cacheKey);
199
172
  if (Array.isArray(cachedResult)) {
200
173
  return cachedResult;
201
174
  }
202
175
  }
203
- let pks;
204
- if (this.getRootStore &&
205
- typeof this.getRootStore === "function" &&
206
- !this.isTemp) {
207
- const { isRoot, rootStore } = this.getRootStore(this.queryset);
208
- // Only render from root if the root has been synced at least once
209
- // This prevents child stores from getting empty data on first render
210
- if (!isRoot && rootStore && rootStore.lastSync !== null) {
211
- pks = this.renderFromRoot(optimistic, rootStore);
212
- }
213
- }
214
- // For temp stores with no ground truth (e.g., chained optimistic filters),
215
- // render from the model store instead of empty ground truth
216
- if (isNil(pks) && this.isTemp && this.groundTruthPks.length === 0) {
217
- pks = this.renderFromModelStore(optimistic);
218
- }
219
- if (isNil(pks)) {
220
- pks = this.renderFromData(optimistic);
221
- }
176
+ // If no ground truth AND hasn't been synced, render from model store
177
+ // This handles chained optimistic filters, newly created stores, etc.
178
+ // (If synced with empty results, that's valid ground truth)
179
+ const pks = this.groundTruthPks.length === 0 && this.lastSync === null
180
+ ? this.renderFromModelStore()
181
+ : this.renderFromData(optimistic);
182
+ // Validate against model store and apply local filtering/sorting
222
183
  let result = this._getValidatedAndFilteredPks(pks);
223
- let limit = this.queryset.build().serializerOptions?.limit;
184
+ // Apply pagination limit
185
+ const limit = this.queryset.build().serializerOptions?.limit;
224
186
  if (limit) {
225
187
  result = result.slice(0, limit);
226
188
  }
227
189
  this.setCache(result);
228
190
  return result;
229
191
  }
230
- renderFromRoot(optimistic = true, rootStore) {
231
- let renderedPks = rootStore.render(optimistic);
232
- let renderedData = renderedPks.map((pk) => {
233
- return this.modelClass.fromPk(pk, this.queryset);
234
- });
235
- let ast = this.queryset.build();
236
- let result = filter(renderedData, ast, this.modelClass, false);
237
- return result;
238
- }
239
- /**
240
- * Render by getting ALL instances from the model store and applying
241
- * the queryset's filters locally. Used for temp stores (e.g., optimistic
242
- * chained filters) that don't have their own ground truth.
243
- */
244
- renderFromModelStore(optimistic = true) {
245
- const modelStore = modelStoreRegistry.getStore(this.modelClass);
246
- // Get all PKs from the model store
247
- const allPks = modelStore.groundTruthPks;
248
- // Convert to model instances (like renderFromRoot does)
249
- const allInstances = allPks.map((pk) => {
250
- return this.modelClass.fromPk(pk, this.queryset);
251
- });
252
- // Apply the queryset's AST filters locally
253
- const ast = this.queryset.build();
254
- const result = filter(allInstances, ast, this.modelClass, false);
255
- return result;
256
- }
257
192
  renderFromData(optimistic = true) {
258
193
  const renderedPks = this.groundTruthSet;
259
194
  for (const op of this.operations) {
@@ -265,6 +200,17 @@ export class QuerysetStore {
265
200
  let result = Array.from(renderedPks);
266
201
  return result;
267
202
  }
203
+ /**
204
+ * Render by getting all instances from the model store and filtering locally.
205
+ * Used when a queryset has no ground truth (temp stores, newly created stores, etc.)
206
+ */
207
+ renderFromModelStore() {
208
+ const modelStore = modelStoreRegistry.getStore(this.modelClass);
209
+ const allPks = modelStore.groundTruthPks;
210
+ const allInstances = allPks.map((pk) => this.modelClass.fromPk(pk, this.queryset));
211
+ const ast = this.queryset.build();
212
+ return filter(allInstances, ast, this.modelClass, false);
213
+ }
268
214
  applyOperation(operation, currentPks) {
269
215
  const pkField = this.pkField;
270
216
  for (const instance of operation.instances) {
@@ -292,26 +238,16 @@ export class QuerysetStore {
292
238
  }
293
239
  return currentPks;
294
240
  }
295
- async sync(forceFromDb = false) {
241
+ /**
242
+ * Sync this queryset with the database.
243
+ * Fetches from DB and sets ground truth.
244
+ */
245
+ async sync() {
296
246
  const id = this.modelClass.modelName;
297
247
  if (this.isSyncing) {
298
248
  console.warn(`[QuerysetStore ${id}] Already syncing, request ignored.`);
299
249
  return;
300
250
  }
301
- // Check if we're delegating to a root store
302
- if (!forceFromDb &&
303
- this.getRootStore &&
304
- typeof this.getRootStore === "function" &&
305
- !this.isTemp) {
306
- const { isRoot, rootStore } = this.getRootStore(this.queryset);
307
- if (!isRoot && rootStore) {
308
- // We're delegating to a root store - don't sync, just mark as needing sync
309
- console.log(`[${id}] Delegating to root store.`);
310
- this.setOperations(this.getInflightOperations());
311
- return;
312
- }
313
- }
314
- // We're in independent mode - proceed with normal sync
315
251
  this.isSyncing = true;
316
252
  console.log(`[${id}] Starting sync...`);
317
253
  try {
@@ -320,16 +256,14 @@ export class QuerysetStore {
320
256
  modelClass: this.modelClass,
321
257
  });
322
258
  const { data, included } = response;
323
- if (isNil(data)) {
324
- return;
259
+ if (!isNil(data)) {
260
+ console.log(`[${id}] Sync fetch completed. Received: ${JSON.stringify(data)}.`);
261
+ // Clear previous included PKs tracking before processing new data
262
+ this.includedPks.clear();
263
+ // Persist all instances (including nested) to the model store
264
+ processIncludedEntities(modelStoreRegistry, included, this.modelClass, this.queryset);
265
+ this.setGroundTruth(data);
325
266
  }
326
- console.log(`[${id}] Sync fetch completed. Received: ${JSON.stringify(data)}.`);
327
- // Clear previous included PKs tracking before processing new data
328
- this.includedPks.clear();
329
- // Persists all the instances (including nested instances) to the model store
330
- // Pass this queryset to track which PKs are in the included data
331
- processIncludedEntities(modelStoreRegistry, included, this.modelClass, this.queryset);
332
- this.setGroundTruth(data);
333
267
  this.setOperations(this.getInflightOperations());
334
268
  this.lastSync = Date.now();
335
269
  console.log(`[${id}] Sync completed.`);
@@ -22,15 +22,13 @@ export class SyncManager {
22
22
  debounceMs: number;
23
23
  maxWaitMs: number;
24
24
  batchStartTime: number | null;
25
- /** @type {PQueue} */
26
- syncQueue: PQueue;
27
25
  withTimeout(promise: any, ms: any): Promise<any>;
28
26
  /**
29
27
  * Initialize event handlers for all event receivers
30
28
  */
31
29
  initialize(): void;
32
30
  startPeriodicSync(): void;
33
- syncStaleQuerysets(): void;
31
+ syncStaleQuerysets(): Promise<void>;
34
32
  pruneUnreferencedModels(): void;
35
33
  isStoreFollowed(registry: any, semanticKey: any): boolean;
36
34
  cleanup(): void;
@@ -45,5 +43,4 @@ export class SyncManager {
45
43
  processMetrics(event: any): void;
46
44
  processModels(event: any): void;
47
45
  }
48
- import PQueue from "p-queue";
49
46
  export const syncManager: SyncManager;
@@ -8,7 +8,7 @@ import { metricRegistry, MetricRegistry } from "./registries/metricRegistry.js";
8
8
  import { getModelClass, getConfig } from "../config.js";
9
9
  import { isNil } from "lodash-es";
10
10
  import { QuerysetStore } from "./stores/querysetStore.js";
11
- import PQueue from "p-queue";
11
+ import { v7 as uuidv7 } from "uuid";
12
12
  export class EventPayload {
13
13
  constructor(data) {
14
14
  this.event = data.event;
@@ -95,9 +95,6 @@ export class SyncManager {
95
95
  this.debounceMs = 100; // Wait for rapid events to settle
96
96
  this.maxWaitMs = 2000; // Maximum time to hold events
97
97
  this.batchStartTime = null;
98
- // SyncQueue
99
- /** @type {PQueue} */
100
- this.syncQueue = new PQueue({ concurrency: 1 });
101
98
  }
102
99
  withTimeout(promise, ms) {
103
100
  // If no timeout specified, use 2x the periodic sync interval, or 30s as fallback
@@ -155,22 +152,26 @@ export class SyncManager {
155
152
  console.log("[SyncManager] No config found, periodic sync disabled by default");
156
153
  }
157
154
  }
158
- syncStaleQuerysets() {
159
- let syncedCount = 0;
160
- // Sync all followed querysets - keep it simple
155
+ async syncStaleQuerysets() {
161
156
  const querysetRegistry = this.registries.get(QuerysetStoreRegistry);
162
- if (querysetRegistry) {
163
- for (const [semanticKey, store] of querysetRegistry._stores.entries()) {
164
- // Only sync if this store is actually being followed
165
- const isFollowed = this.isStoreFollowed(querysetRegistry, semanticKey);
166
- if (this.followAllQuerysets || isFollowed) {
167
- this.syncQueue.add(() => this.withTimeout(store.sync()));
168
- syncedCount++;
169
- }
157
+ if (!querysetRegistry)
158
+ return;
159
+ // Generate operationId for this sync batch - querysets in same chain will coordinate
160
+ const operationId = `periodic-sync-${uuidv7()}`;
161
+ // Get dbSynced keys (followed querysets)
162
+ const dbSyncedKeys = new Set([...this.followedQuerysets].map(qs => qs.semanticKey));
163
+ // Collect all stores to sync
164
+ const storesToSync = [];
165
+ for (const [semanticKey, store] of querysetRegistry._stores.entries()) {
166
+ const isFollowed = this.isStoreFollowed(querysetRegistry, semanticKey);
167
+ if (this.followAllQuerysets || isFollowed) {
168
+ storesToSync.push(store);
170
169
  }
171
170
  }
172
- if (syncedCount > 0) {
173
- console.log(`[SyncManager] Periodic sync: ${syncedCount} stores pushed to the sync queue`);
171
+ if (storesToSync.length > 0) {
172
+ console.log(`[SyncManager] Periodic sync: syncing ${storesToSync.length} stores`);
173
+ // Run all groupSync calls in parallel - they coordinate via shared promise cache
174
+ await Promise.all(storesToSync.map(store => querysetRegistry.groupSync(store.queryset, operationId, dbSyncedKeys)));
174
175
  }
175
176
  // Prune unreferenced model instances
176
177
  this.pruneUnreferencedModels();
@@ -306,11 +307,16 @@ export class SyncManager {
306
307
  }
307
308
  }
308
309
  }
310
+ if (storesToSync.length === 0)
311
+ return;
309
312
  // Sync all relevant stores for this model
310
313
  console.log(`[SyncManager] Syncing ${storesToSync.length} queryset stores for ${representativeEvent.model}`);
311
- storesToSync.forEach((store) => {
312
- this.syncQueue.add(() => this.withTimeout(store.sync()));
313
- });
314
+ // Generate operationId for this batch - querysets in same chain will coordinate
315
+ const operationId = `remote-event-${uuidv7()}`;
316
+ // Get dbSynced keys (followed querysets)
317
+ const dbSyncedKeys = new Set([...this.followedQuerysets].map(qs => qs.semanticKey));
318
+ // Run all groupSync calls in parallel - they coordinate via shared promise cache
319
+ Promise.all(storesToSync.map(store => registry.groupSync(store.queryset, operationId, dbSyncedKeys)));
314
320
  }
315
321
  processMetrics(event) {
316
322
  const registry = this.registries.get(MetricRegistry);
@@ -372,7 +378,7 @@ export class SyncManager {
372
378
  });
373
379
  if (pksToSync.length > 0) {
374
380
  console.log(`[SyncManager] Syncing ${pksToSync.length} nested-only PKs for ${event.model}: ${pksToSync}`);
375
- this.syncQueue.add(() => this.withTimeout(modelStore.sync(pksToSync)));
381
+ modelStore.sync(pksToSync);
376
382
  }
377
383
  }
378
384
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@statezero/core",
3
- "version": "0.2.28",
3
+ "version": "0.2.30",
4
4
  "type": "module",
5
5
  "module": "ESNext",
6
6
  "description": "The type-safe frontend client for StateZero - connect directly to your backend models with zero boilerplate",