@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
@@ -5,17 +5,17 @@
5
5
  "plural_title": "M2M Depth Test Level3S",
6
6
  "primary_key_field": "id",
7
7
  "filterable_fields": [
8
- "name",
9
8
  "id",
10
- "category",
11
- "value"
9
+ "value",
10
+ "name",
11
+ "category"
12
12
  ],
13
13
  "searchable_fields": [
14
14
  "name"
15
15
  ],
16
16
  "ordering_fields": [
17
- "name",
18
- "value"
17
+ "value",
18
+ "name"
19
19
  ],
20
20
  "properties": {
21
21
  "id": {
@@ -5,9 +5,9 @@
5
5
  "plural_title": "Model With Restricted Fieldss",
6
6
  "primary_key_field": "id",
7
7
  "filterable_fields": [
8
- "name",
9
8
  "id",
10
9
  "admin_only_field",
10
+ "name",
11
11
  "restricted_related"
12
12
  ],
13
13
  "searchable_fields": [
@@ -5,15 +5,15 @@
5
5
  "plural_title": "Name Filter Custom Pk Models",
6
6
  "primary_key_field": "custom_pk",
7
7
  "filterable_fields": [
8
- "name",
9
- "custom_pk"
8
+ "custom_pk",
9
+ "name"
10
10
  ],
11
11
  "searchable_fields": [
12
12
  "name"
13
13
  ],
14
14
  "ordering_fields": [
15
- "name",
16
- "custom_pk"
15
+ "custom_pk",
16
+ "name"
17
17
  ],
18
18
  "properties": {
19
19
  "custom_pk": {
@@ -5,18 +5,18 @@
5
5
  "plural_title": "Orders",
6
6
  "primary_key_field": "id",
7
7
  "filterable_fields": [
8
+ "status",
9
+ "total",
8
10
  "id",
9
- "customer_email",
11
+ "created_at",
10
12
  "customer_name",
11
- "status",
12
- "order_number",
13
+ "customer_email",
13
14
  "last_updated",
14
- "created_at",
15
- "total"
15
+ "order_number"
16
16
  ],
17
17
  "searchable_fields": [
18
- "order_number",
19
- "customer_name"
18
+ "customer_name",
19
+ "order_number"
20
20
  ],
21
21
  "ordering_fields": [
22
22
  "created_at",
@@ -134,7 +134,7 @@
134
134
  "format": "date-time",
135
135
  "max_length": null,
136
136
  "choices": null,
137
- "default": "2026-01-18T17:05:59.982046+00:00",
137
+ "default": "2026-01-29T14:56:44.889107+00:00",
138
138
  "validators": [],
139
139
  "max_digits": null,
140
140
  "decimal_places": null,
@@ -7,8 +7,8 @@
7
7
  "filterable_fields": [
8
8
  "id",
9
9
  "order",
10
- "price",
11
10
  "product",
11
+ "price",
12
12
  "quantity"
13
13
  ],
14
14
  "searchable_fields": [],
@@ -5,22 +5,22 @@
5
5
  "plural_title": "Products",
6
6
  "primary_key_field": "id",
7
7
  "filterable_fields": [
8
- "id",
9
8
  "name",
10
- "description",
9
+ "id",
11
10
  "price",
12
- "category",
13
11
  "created_at",
14
- "created_by",
15
- "in_stock"
12
+ "category",
13
+ "in_stock",
14
+ "description",
15
+ "created_by"
16
16
  ],
17
17
  "searchable_fields": [
18
- "name",
19
- "description"
18
+ "description",
19
+ "name"
20
20
  ],
21
21
  "ordering_fields": [
22
- "name",
23
22
  "price",
23
+ "name",
24
24
  "created_at"
25
25
  ],
26
26
  "properties": {
@@ -129,7 +129,7 @@
129
129
  "format": "date-time",
130
130
  "max_length": null,
131
131
  "choices": null,
132
- "default": "2026-01-18T17:05:59.911294+00:00",
132
+ "default": "2026-01-29T14:56:44.816661+00:00",
133
133
  "validators": [],
134
134
  "max_digits": null,
135
135
  "decimal_places": null,
@@ -5,8 +5,8 @@
5
5
  "plural_title": "Product Categorys",
6
6
  "primary_key_field": "id",
7
7
  "filterable_fields": [
8
- "name",
9
- "id"
8
+ "id",
9
+ "name"
10
10
  ],
11
11
  "searchable_fields": [
12
12
  "name"
@@ -5,8 +5,8 @@
5
5
  "plural_title": "Rate Plans",
6
6
  "primary_key_field": "id",
7
7
  "filterable_fields": [
8
- "name",
9
- "id"
8
+ "id",
9
+ "name"
10
10
  ],
11
11
  "searchable_fields": [
12
12
  "name"
@@ -5,9 +5,9 @@
5
5
  "plural_title": "Restricted Field Related Models",
6
6
  "primary_key_field": "id",
7
7
  "filterable_fields": [
8
- "name",
9
8
  "id",
10
- "admin_only_field"
9
+ "admin_only_field",
10
+ "name"
11
11
  ],
12
12
  "searchable_fields": [
13
13
  "name"
@@ -2,20 +2,30 @@
2
2
  * Simple graph for tracking queryset store ancestry
3
3
  */
4
4
  export class QuerysetStoreGraph {
5
- constructor(hasStoreFn?: null);
6
5
  graph: any;
7
- hasStoreFn: () => boolean;
8
6
  processedQuerysets: Set<any>;
9
- setHasStoreFn(hasStoreFn: any): void;
10
7
  /**
11
8
  * Add a queryset and its parent relationship to the graph
12
9
  */
13
10
  addQueryset(queryset: any): void;
14
11
  /**
15
- * Find the root store for a queryset
12
+ * Find the root (topmost ancestor) of a queryset chain within a subset.
13
+ * Traverses up the graph but only considers nodes that are in the subset.
14
+ * Uses the graph to "jump" through nodes not in subset to find connections.
15
+ *
16
16
  * @param {Object} queryset - The queryset to analyze
17
+ * @param {Set|null} subset - Set of semanticKeys to consider. If null, considers all nodes in graph.
17
18
  * @returns {Object} { isRoot: boolean, root: semanticKey|null }
18
19
  */
19
- findRoot(queryset: Object): Object;
20
+ findRoot(queryset: Object, subset?: Set<any> | null): Object;
21
+ /**
22
+ * Check if parent queryset is a valid data source for creating an edge.
23
+ * Parent must have data that is a superset of what child needs.
24
+ *
25
+ * @param {Object} parentOpts - Parent's serializerOptions
26
+ * @param {Object} childOpts - Child's serializerOptions
27
+ * @returns {boolean} - True if parent is valid for edge creation
28
+ */
29
+ _isValidParentForEdge(parentOpts?: Object, childOpts?: Object): boolean;
20
30
  clear(): void;
21
31
  }
@@ -3,14 +3,10 @@ import { Graph } from "graphlib";
3
3
  * Simple graph for tracking queryset store ancestry
4
4
  */
5
5
  export class QuerysetStoreGraph {
6
- constructor(hasStoreFn = null) {
6
+ constructor() {
7
7
  this.graph = new Graph({ directed: true });
8
- this.hasStoreFn = hasStoreFn || (() => false);
9
8
  this.processedQuerysets = new Set(); // Track UUIDs of processed querysets
10
9
  }
11
- setHasStoreFn(hasStoreFn) {
12
- this.hasStoreFn = hasStoreFn;
13
- }
14
10
  /**
15
11
  * Add a queryset and its parent relationship to the graph
16
12
  */
@@ -29,7 +25,11 @@ export class QuerysetStoreGraph {
29
25
  if (current.__parent) {
30
26
  const parentKey = current.__parent.semanticKey;
31
27
  this.graph.setNode(parentKey);
32
- if (currentKey !== parentKey) {
28
+ // Determine if we can create an edge to parent
29
+ // Parent must be a valid data source (superset of child's data needs)
30
+ const canLinkToParent = currentKey !== parentKey &&
31
+ this._isValidParentForEdge(current.__parent._serializerOptions, current._serializerOptions);
32
+ if (canLinkToParent) {
33
33
  this.graph.setEdge(currentKey, parentKey);
34
34
  }
35
35
  current = current.__parent;
@@ -40,11 +40,15 @@ export class QuerysetStoreGraph {
40
40
  }
41
41
  }
42
42
  /**
43
- * Find the root store for a queryset
43
+ * Find the root (topmost ancestor) of a queryset chain within a subset.
44
+ * Traverses up the graph but only considers nodes that are in the subset.
45
+ * Uses the graph to "jump" through nodes not in subset to find connections.
46
+ *
44
47
  * @param {Object} queryset - The queryset to analyze
48
+ * @param {Set|null} subset - Set of semanticKeys to consider. If null, considers all nodes in graph.
45
49
  * @returns {Object} { isRoot: boolean, root: semanticKey|null }
46
50
  */
47
- findRoot(queryset) {
51
+ findRoot(queryset, subset = null) {
48
52
  // Validate input - null/undefined is a programming error
49
53
  if (!queryset) {
50
54
  throw new Error("findRoot was called with a null object, instead of a queryset");
@@ -57,39 +61,77 @@ export class QuerysetStoreGraph {
57
61
  if (!this.graph.hasNode(semanticKey)) {
58
62
  this.addQueryset(queryset);
59
63
  }
60
- // Traverse ALL the way up to find the HIGHEST ancestor with a store
64
+ // If no subset provided, consider all nodes in the graph
65
+ subset = subset || new Set(this.graph.nodes());
66
+ // Traverse ALL the way up to find the HIGHEST ancestor in the subset
61
67
  const visited = new Set();
62
68
  let current = semanticKey;
63
- let highestAncestorWithStore = null;
69
+ let highestInSubset = null;
64
70
  while (current && !visited.has(current)) {
65
71
  visited.add(current);
66
- // Check if current node has a store
67
- if (this.hasStoreFn(current)) {
68
- highestAncestorWithStore = current;
72
+ // Check if current node is in subset
73
+ if (subset.has(current)) {
74
+ highestInSubset = current;
69
75
  }
70
- // Move to parent
76
+ // Move to parent (continue jumping even if current not in subset)
71
77
  const parents = this.graph.successors(current) || [];
72
78
  if (parents.length > 0) {
73
- current = parents[0]; // Follow the parent chain
79
+ current = parents[0];
74
80
  }
75
81
  else {
76
- break; // No more parents
82
+ break;
77
83
  }
78
84
  }
79
- if (highestAncestorWithStore) {
80
- if (highestAncestorWithStore === semanticKey) {
81
- // This queryset itself is the highest with a store
85
+ if (highestInSubset) {
86
+ if (highestInSubset === semanticKey) {
87
+ // This queryset itself is the highest in subset
82
88
  return { isRoot: true, root: semanticKey };
83
89
  }
84
90
  else {
85
- // Found a higher ancestor with a store
86
- return { isRoot: false, root: highestAncestorWithStore };
91
+ // Found a higher ancestor in subset
92
+ return { isRoot: false, root: highestInSubset };
87
93
  }
88
94
  }
89
- // No stores found anywhere in the chain
95
+ // No nodes found in subset
90
96
  return { isRoot: true, root: null };
91
97
  }
98
+ /**
99
+ * Check if parent queryset is a valid data source for creating an edge.
100
+ * Parent must have data that is a superset of what child needs.
101
+ *
102
+ * @param {Object} parentOpts - Parent's serializerOptions
103
+ * @param {Object} childOpts - Child's serializerOptions
104
+ * @returns {boolean} - True if parent is valid for edge creation
105
+ */
106
+ _isValidParentForEdge(parentOpts = {}, childOpts = {}) {
107
+ // Cannot link if parent has pagination (limit/offset)
108
+ // Paginated parent only has a subset of data
109
+ if (parentOpts.limit != null || parentOpts.offset != null) {
110
+ return false;
111
+ }
112
+ // Cannot link if parent has different depth
113
+ // Different depth means different nested data structure
114
+ if (parentOpts.depth != null && parentOpts.depth !== childOpts.depth) {
115
+ return false;
116
+ }
117
+ // Cannot link if parent has fields that are not a superset of child's fields
118
+ // Parent must have all fields that child needs
119
+ if (parentOpts.fields != null) {
120
+ // If child needs all fields (null) but parent restricts fields, cannot link
121
+ if (childOpts.fields == null) {
122
+ return false;
123
+ }
124
+ // Check if parent's fields contain all of child's fields
125
+ const parentFields = new Set(parentOpts.fields);
126
+ const childHasFieldsNotInParent = childOpts.fields.some(f => !parentFields.has(f));
127
+ if (childHasFieldsNotInParent) {
128
+ return false;
129
+ }
130
+ }
131
+ return true;
132
+ }
92
133
  clear() {
93
134
  this.graph = new Graph({ directed: true });
135
+ this.processedQuerysets = new Set();
94
136
  }
95
137
  }
@@ -28,28 +28,21 @@ export class QuerysetStoreRegistry {
28
28
  followingQuerysets: Map<any, any>;
29
29
  syncManager: () => void;
30
30
  querysetStoreGraph: QuerysetStoreGraph;
31
+ _groupSyncCache: Map<any, any>;
31
32
  clear(): void;
32
33
  setSyncManager(syncManager: any): void;
33
34
  /**
34
35
  * Add a queryset to the following set for a semantic key
35
36
  */
36
37
  addFollowingQueryset(semanticKey: any, queryset: any): void;
37
- getStore(queryset: any, seed?: boolean): any;
38
- /**
39
- * Function to return the root store for a queryset
40
- */
41
- getRootStore(queryset: any): {
42
- isRoot: boolean;
43
- rootStore: any;
44
- };
38
+ getStore(queryset: any): any;
45
39
  /**
46
40
  * Get the current state of the queryset, wrapped in a LiveQueryset
47
41
  * @param {Object} queryset - The queryset
48
- * @param {Boolean} seed - Should we optimistically seed the queryset with relevant items from the parent?
49
42
  * @param {Boolean} sync - Schedule a sync of the queryset with the backend
50
43
  * @returns {LiveQueryset} - A live view of the queryset
51
44
  */
52
- getEntity(queryset: Object, seed?: boolean, sync?: boolean): LiveQueryset;
45
+ getEntity(queryset: Object, sync?: boolean): LiveQueryset;
53
46
  /**
54
47
  * Set ground truth for a queryset
55
48
  * @param {Object} queryset - The queryset
@@ -63,6 +56,17 @@ export class QuerysetStoreRegistry {
63
56
  * @returns {Array} - Array of queryset stores for this model
64
57
  */
65
58
  getAllStoresForModel(ModelClass: any): any[];
59
+ /**
60
+ * Sync a queryset, coordinating with its chain to minimize DB calls.
61
+ * The root fetches from DB, children filter from cached results.
62
+ * Uses operationId to coordinate - whoever arrives first creates the promise,
63
+ * the root takes over and resolves it.
64
+ *
65
+ * @param {Object} queryset - The queryset to sync
66
+ * @param {string} operationId - Unique ID for this sync operation (for coordination)
67
+ * @param {Set} dbSyncedKeys - Set of semanticKeys that are dbSynced (followedQuerysets)
68
+ */
69
+ groupSync(queryset: Object, operationId: string, dbSyncedKeys: Set<any>): Promise<void>;
66
70
  }
67
71
  export const querysetStoreRegistry: QuerysetStoreRegistry;
68
72
  import { QuerysetStoreGraph } from './querysetStoreGraph.js';
@@ -123,9 +123,10 @@ export class QuerysetStoreRegistry {
123
123
  this._tempStores = new WeakMap(); // WeakMap<Queryset, Store>
124
124
  this.followingQuerysets = new Map(); // Map<semanticKey, Set<queryset>>
125
125
  this.syncManager = () => { console.warn("SyncManager not set for QuerysetStoreRegistry"); };
126
- this.querysetStoreGraph = new QuerysetStoreGraph((semanticKey) => {
127
- return this._stores.has(semanticKey);
128
- });
126
+ this.querysetStoreGraph = new QuerysetStoreGraph();
127
+ // Cache for groupSync coordination
128
+ // Map<operationId, { promise, resolve, rootKey, pks, ModelClass }>
129
+ this._groupSyncCache = new Map();
129
130
  }
130
131
  clear() {
131
132
  this._stores.forEach((store) => {
@@ -147,7 +148,7 @@ export class QuerysetStoreRegistry {
147
148
  }
148
149
  this.followingQuerysets.get(semanticKey).add(queryset);
149
150
  }
150
- getStore(queryset, seed = false) {
151
+ getStore(queryset) {
151
152
  if (isNil(queryset) || isNil(queryset.ModelClass)) {
152
153
  throw new Error("QuerysetStoreRegistry.getStore requires a valid queryset");
153
154
  }
@@ -183,55 +184,25 @@ export class QuerysetStoreRegistry {
183
184
  };
184
185
  const response = await makeApiCall(queryset, 'list', payload, null, // operationId
185
186
  null, // beforeExit
186
- canonical_id // canonical_id for caching
187
+ canonical_id, // canonical_id for caching
188
+ { namespace: 'sync', timeout: 30000 } // Sync ops on separate queue
187
189
  );
188
190
  return response.data;
189
191
  };
190
- let initialGroundTruthPks = null;
191
- let ast = queryset.build();
192
- let ModelClass = queryset.ModelClass;
193
- if (queryset.__parent && seed) {
194
- const parentKey = queryset.__parent.semanticKey;
195
- if (this._stores.has(parentKey)) {
196
- let parentLiveQuerySet = this.getEntity(queryset.__parent);
197
- initialGroundTruthPks = filter(parentLiveQuerySet, ast, ModelClass, false);
198
- }
199
- }
200
- // Get the parent registry
201
- const store = new QuerysetStore(ModelClass, fetchQueryset, queryset, initialGroundTruthPks, // Initial ground truth PKs
192
+ const store = new QuerysetStore(queryset.ModelClass, fetchQueryset, queryset, null, // No initial ground truth - will render from model store if needed
202
193
  null, // Initial operations
203
- {
204
- getRootStore: this.getRootStore.bind(this),
205
- isTemp: true,
206
- });
194
+ { isTemp: true });
207
195
  // Store it in the temp store map
208
196
  this._tempStores.set(queryset, store);
209
197
  return store;
210
198
  }
211
- /**
212
- * Function to return the root store for a queryset
213
- */
214
- getRootStore(queryset) {
215
- if (isNil(queryset)) {
216
- throw new Error("QuerysetStoreRegistry.getRootStore requires a valid queryset");
217
- }
218
- const { isRoot, root } = this.querysetStoreGraph.findRoot(queryset);
219
- const rootStore = this._stores.get(root);
220
- if (!isRoot && rootStore) {
221
- return { isRoot: false, rootStore: rootStore };
222
- }
223
- else {
224
- return { isRoot: true, rootStore: null };
225
- }
226
- }
227
199
  /**
228
200
  * Get the current state of the queryset, wrapped in a LiveQueryset
229
201
  * @param {Object} queryset - The queryset
230
- * @param {Boolean} seed - Should we optimistically seed the queryset with relevant items from the parent?
231
202
  * @param {Boolean} sync - Schedule a sync of the queryset with the backend
232
203
  * @returns {LiveQueryset} - A live view of the queryset
233
204
  */
234
- getEntity(queryset, seed = true, sync = false) {
205
+ getEntity(queryset, sync = false) {
235
206
  if (isNil(queryset))
236
207
  throw new Error(`qsStoreRegistry: getEntity cannot be called without a queryset`);
237
208
  const semanticKey = queryset.semanticKey;
@@ -246,7 +217,7 @@ export class QuerysetStoreRegistry {
246
217
  }
247
218
  // Otherwise, ensure we have a permanent store
248
219
  else if (!this._stores.has(semanticKey)) {
249
- store = this.getStore(queryset, seed);
220
+ store = this.getStore(queryset);
250
221
  store.isTemp = false;
251
222
  this._stores.set(semanticKey, store);
252
223
  this.syncManager.followModel(this, queryset.ModelClass);
@@ -301,5 +272,60 @@ export class QuerysetStoreRegistry {
301
272
  return [];
302
273
  return Array.from(this._stores.values()).filter(store => store.modelClass === ModelClass);
303
274
  }
275
+ /**
276
+ * Sync a queryset, coordinating with its chain to minimize DB calls.
277
+ * The root fetches from DB, children filter from cached results.
278
+ * Uses operationId to coordinate - whoever arrives first creates the promise,
279
+ * the root takes over and resolves it.
280
+ *
281
+ * @param {Object} queryset - The queryset to sync
282
+ * @param {string} operationId - Unique ID for this sync operation (for coordination)
283
+ * @param {Set} dbSyncedKeys - Set of semanticKeys that are dbSynced (followedQuerysets)
284
+ */
285
+ async groupSync(queryset, operationId, dbSyncedKeys) {
286
+ if (isNil(queryset))
287
+ return;
288
+ const semanticKey = queryset.semanticKey;
289
+ const ModelClass = queryset.ModelClass;
290
+ // Convert dbSyncedKeys to semanticKeys if needed
291
+ const subset = new Set();
292
+ for (const item of dbSyncedKeys) {
293
+ subset.add(typeof item === 'string' ? item : item?.semanticKey);
294
+ }
295
+ // Find the dbSynced root
296
+ const { isRoot, root: rootKey } = this.querysetStoreGraph.findRoot(queryset, subset);
297
+ const iAmRoot = isRoot || rootKey === semanticKey;
298
+ // Get or create cache entry - whoever arrives first creates it
299
+ if (!this._groupSyncCache.has(operationId)) {
300
+ let resolve;
301
+ const promise = new Promise(r => { resolve = r; });
302
+ this._groupSyncCache.set(operationId, { promise, resolve, pks: null, ModelClass });
303
+ setTimeout(() => this._groupSyncCache.delete(operationId), 5000);
304
+ }
305
+ const cached = this._groupSyncCache.get(operationId);
306
+ const store = this._stores.get(semanticKey);
307
+ if (!store) {
308
+ console.warn(`[groupSync] No store found for queryset: ${semanticKey}`);
309
+ return;
310
+ }
311
+ if (iAmRoot) {
312
+ // I'm the root - sync from DB (store handles everything)
313
+ await store.sync();
314
+ cached.pks = store.groundTruthPks;
315
+ cached.resolve();
316
+ }
317
+ else {
318
+ // Wait for root to finish
319
+ await cached.promise;
320
+ // Filter from cached root data
321
+ const rootInstances = cached.pks.map(pk => ModelClass.fromPk(pk, queryset));
322
+ const ast = queryset.build();
323
+ const filteredPks = filter(rootInstances, ast, ModelClass, false);
324
+ // Set ground truth and clean up inflight ops (like sync does)
325
+ store.setGroundTruth(filteredPks);
326
+ store.setOperations(store.getInflightOperations());
327
+ store.lastSync = Date.now();
328
+ }
329
+ }
304
330
  }
305
331
  export const querysetStoreRegistry = new QuerysetStoreRegistry();
@@ -7,27 +7,19 @@ import { QuerySet } from '../../flavours/django/querySet.js';
7
7
  import { isEqual, isNil } from 'lodash-es';
8
8
  import hash from 'object-hash';
9
9
  /**
10
- * Returns querysets that are in root mode (materialized with no materialized parent)
11
- * Since filtered querysets render by filtering their parent's data, we only need
12
- * to route operations to root querysets. Filtered children will see the operations
13
- * when they filter their parent's rendered data.
10
+ * Returns all querysets (stores) for the same model.
11
+ * Each queryset has its own ground truth and applies local filtering.
14
12
  * @param {QuerySet} queryset
15
13
  * @returns {Map<QuerySet, Store>}
16
14
  */
17
- function getRootQuerysets(queryset) {
15
+ function getAllQuerysets(queryset) {
18
16
  const modelClass = queryset.ModelClass;
19
17
  const result = new Map();
20
- // Route only to querysets that are in root mode
21
- // Note: _stores is Map<semanticKey, Store>, so we get the queryset from store.queryset
18
+ // Route to all stores for this model
22
19
  Array.from(querysetStoreRegistry._stores.entries()).forEach(([semanticKey, store]) => {
23
20
  if (store.modelClass !== modelClass)
24
21
  return;
25
- // Use the graph to determine if this store is in root mode
26
- const { isRoot, root } = querysetStoreRegistry.querysetStoreGraph.findRoot(store.queryset);
27
- // A queryset is in root mode if isRoot=true and root is its own semantic key
28
- if (isRoot && root === store.queryset.semanticKey) {
29
- result.set(store.queryset, store);
30
- }
22
+ result.set(store.queryset, store);
31
23
  });
32
24
  return result;
33
25
  }
@@ -85,29 +77,29 @@ function processQuerysetStores(operation, actionType) {
85
77
  }
86
78
  };
87
79
  let querysetStoreMap;
88
- // Different routing strategies based on operation type
80
+ // Route to all querysets for the model - each applies local filtering
89
81
  switch (operation.type) {
90
82
  case Type.CREATE:
91
83
  case Type.BULK_CREATE:
92
84
  case Type.GET_OR_CREATE:
93
85
  case Type.UPDATE_OR_CREATE:
94
- // For creates, route to root querysets (they might want to include the new item)
95
- querysetStoreMap = getRootQuerysets(queryset);
86
+ // For creates, route to all querysets (each checks if new item matches its filter)
87
+ querysetStoreMap = getAllQuerysets(queryset);
96
88
  break;
97
89
  case Type.UPDATE:
98
90
  case Type.UPDATE_INSTANCE:
99
91
  case Type.DELETE:
100
92
  case Type.DELETE_INSTANCE:
101
- // No need to do anything, the model will change the queryset local filtering will handle it
93
+ // Model store handles the change, querysets re-render via local filtering
102
94
  querysetStoreMap = new Map();
103
95
  break;
104
96
  case Type.CHECKPOINT:
105
- // No need to do anything, the model will change the queryset local filtering will handle it
97
+ // Model store handles the change, querysets re-render via local filtering
106
98
  querysetStoreMap = new Map();
107
99
  break;
108
100
  default:
109
- // For other operation types, use the existing root querysets logic
110
- querysetStoreMap = getRootQuerysets(queryset);
101
+ // For other operation types, route to all querysets
102
+ querysetStoreMap = getAllQuerysets(queryset);
111
103
  break;
112
104
  }
113
105
  Array.from(querysetStoreMap.values()).forEach(applyAction);