@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.
- package/dist/actions/backend1/django_app/calculate-hash.js +1 -1
- package/dist/actions/backend1/django_app/calculate-hash.schema.json +1 -1
- package/dist/actions/backend1/django_app/get-current-username.js +1 -1
- package/dist/actions/backend1/django_app/get-current-username.schema.json +1 -1
- package/dist/actions/backend1/django_app/get-user-info.js +1 -1
- package/dist/actions/backend1/django_app/get-user-info.schema.json +1 -1
- package/dist/actions/backend1/django_app/process-data.js +1 -1
- package/dist/actions/backend1/django_app/process-data.schema.json +1 -1
- package/dist/actions/backend1/django_app/send-notification.js +1 -1
- package/dist/actions/backend1/django_app/send-notification.schema.json +1 -1
- package/dist/actions/default/django_app/calculate-hash.js +1 -1
- package/dist/actions/default/django_app/calculate-hash.schema.json +1 -1
- package/dist/actions/default/django_app/get-current-username.js +1 -1
- package/dist/actions/default/django_app/get-current-username.schema.json +1 -1
- package/dist/actions/default/django_app/get-user-info.js +1 -1
- package/dist/actions/default/django_app/get-user-info.schema.json +1 -1
- package/dist/actions/default/django_app/process-data.js +1 -1
- package/dist/actions/default/django_app/process-data.schema.json +1 -1
- package/dist/actions/default/django_app/send-notification.js +1 -1
- package/dist/actions/default/django_app/send-notification.schema.json +1 -1
- package/dist/flavours/django/makeApiCall.d.ts +14 -1
- package/dist/flavours/django/makeApiCall.js +31 -3
- package/dist/models/backend1/django_app/comprehensivemodel.schema.json +1 -1
- package/dist/models/backend1/django_app/custompkmodel.schema.json +4 -4
- package/dist/models/backend1/django_app/dailyrate.schema.json +8 -8
- package/dist/models/backend1/django_app/dummymodel.schema.json +2 -2
- package/dist/models/backend1/django_app/m2mdepthtestlevel1.schema.json +2 -2
- package/dist/models/backend1/django_app/m2mdepthtestlevel2.schema.json +1 -1
- package/dist/models/backend1/django_app/m2mdepthtestlevel3.schema.json +5 -5
- package/dist/models/backend1/django_app/modelwithrestrictedfields.schema.json +1 -1
- package/dist/models/backend1/django_app/namefiltercustompkmodel.schema.json +4 -4
- package/dist/models/backend1/django_app/order.schema.json +8 -8
- package/dist/models/backend1/django_app/orderitem.schema.json +1 -1
- package/dist/models/backend1/django_app/product.schema.json +9 -9
- package/dist/models/backend1/django_app/productcategory.schema.json +2 -2
- package/dist/models/backend1/django_app/rateplan.schema.json +2 -2
- package/dist/models/backend1/django_app/restrictedfieldrelatedmodel.schema.json +2 -2
- package/dist/models/default/django_app/comprehensivemodel.schema.json +1 -1
- package/dist/models/default/django_app/custompkmodel.schema.json +4 -4
- package/dist/models/default/django_app/dailyrate.schema.json +8 -8
- package/dist/models/default/django_app/dummymodel.schema.json +2 -2
- package/dist/models/default/django_app/m2mdepthtestlevel1.schema.json +2 -2
- package/dist/models/default/django_app/m2mdepthtestlevel2.schema.json +1 -1
- package/dist/models/default/django_app/m2mdepthtestlevel3.schema.json +5 -5
- package/dist/models/default/django_app/modelwithrestrictedfields.schema.json +1 -1
- package/dist/models/default/django_app/namefiltercustompkmodel.schema.json +4 -4
- package/dist/models/default/django_app/order.schema.json +8 -8
- package/dist/models/default/django_app/orderitem.schema.json +1 -1
- package/dist/models/default/django_app/product.schema.json +9 -9
- package/dist/models/default/django_app/productcategory.schema.json +2 -2
- package/dist/models/default/django_app/rateplan.schema.json +2 -2
- package/dist/models/default/django_app/restrictedfieldrelatedmodel.schema.json +2 -2
- package/dist/syncEngine/registries/querysetStoreGraph.d.ts +15 -5
- package/dist/syncEngine/registries/querysetStoreGraph.js +64 -22
- package/dist/syncEngine/registries/querysetStoreRegistry.d.ts +14 -10
- package/dist/syncEngine/registries/querysetStoreRegistry.js +66 -40
- package/dist/syncEngine/stores/operationEventHandlers.js +12 -20
- package/dist/syncEngine/stores/querysetStore.d.ts +9 -11
- package/dist/syncEngine/stores/querysetStore.js +34 -100
- package/dist/syncEngine/sync.d.ts +1 -4
- package/dist/syncEngine/sync.js +27 -21
- 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
|
-
"
|
|
11
|
-
"
|
|
9
|
+
"value",
|
|
10
|
+
"name",
|
|
11
|
+
"category"
|
|
12
12
|
],
|
|
13
13
|
"searchable_fields": [
|
|
14
14
|
"name"
|
|
15
15
|
],
|
|
16
16
|
"ordering_fields": [
|
|
17
|
-
"
|
|
18
|
-
"
|
|
17
|
+
"value",
|
|
18
|
+
"name"
|
|
19
19
|
],
|
|
20
20
|
"properties": {
|
|
21
21
|
"id": {
|
|
@@ -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
|
-
"
|
|
9
|
-
"
|
|
8
|
+
"custom_pk",
|
|
9
|
+
"name"
|
|
10
10
|
],
|
|
11
11
|
"searchable_fields": [
|
|
12
12
|
"name"
|
|
13
13
|
],
|
|
14
14
|
"ordering_fields": [
|
|
15
|
-
"
|
|
16
|
-
"
|
|
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
|
-
"
|
|
11
|
+
"created_at",
|
|
10
12
|
"customer_name",
|
|
11
|
-
"
|
|
12
|
-
"order_number",
|
|
13
|
+
"customer_email",
|
|
13
14
|
"last_updated",
|
|
14
|
-
"
|
|
15
|
-
"total"
|
|
15
|
+
"order_number"
|
|
16
16
|
],
|
|
17
17
|
"searchable_fields": [
|
|
18
|
-
"
|
|
19
|
-
"
|
|
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-
|
|
137
|
+
"default": "2026-01-29T14:56:44.889107+00:00",
|
|
138
138
|
"validators": [],
|
|
139
139
|
"max_digits": null,
|
|
140
140
|
"decimal_places": null,
|
|
@@ -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
|
-
"
|
|
9
|
+
"id",
|
|
11
10
|
"price",
|
|
12
|
-
"category",
|
|
13
11
|
"created_at",
|
|
14
|
-
"
|
|
15
|
-
"in_stock"
|
|
12
|
+
"category",
|
|
13
|
+
"in_stock",
|
|
14
|
+
"description",
|
|
15
|
+
"created_by"
|
|
16
16
|
],
|
|
17
17
|
"searchable_fields": [
|
|
18
|
-
"
|
|
19
|
-
"
|
|
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-
|
|
132
|
+
"default": "2026-01-29T14:56:44.816661+00:00",
|
|
133
133
|
"validators": [],
|
|
134
134
|
"max_digits": null,
|
|
135
135
|
"decimal_places": null,
|
|
@@ -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
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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
|
|
69
|
+
let highestInSubset = null;
|
|
64
70
|
while (current && !visited.has(current)) {
|
|
65
71
|
visited.add(current);
|
|
66
|
-
// Check if current node
|
|
67
|
-
if (
|
|
68
|
-
|
|
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];
|
|
79
|
+
current = parents[0];
|
|
74
80
|
}
|
|
75
81
|
else {
|
|
76
|
-
break;
|
|
82
|
+
break;
|
|
77
83
|
}
|
|
78
84
|
}
|
|
79
|
-
if (
|
|
80
|
-
if (
|
|
81
|
-
// This queryset itself is the highest
|
|
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
|
|
86
|
-
return { isRoot: false, root:
|
|
91
|
+
// Found a higher ancestor in subset
|
|
92
|
+
return { isRoot: false, root: highestInSubset };
|
|
87
93
|
}
|
|
88
94
|
}
|
|
89
|
-
// No
|
|
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
|
|
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,
|
|
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(
|
|
127
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
|
11
|
-
*
|
|
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
|
|
15
|
+
function getAllQuerysets(queryset) {
|
|
18
16
|
const modelClass = queryset.ModelClass;
|
|
19
17
|
const result = new Map();
|
|
20
|
-
// Route
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
95
|
-
querysetStoreMap =
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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,
|
|
110
|
-
querysetStoreMap =
|
|
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);
|