@statezero/core 0.1.19 → 0.1.21
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/syncEngine/registries/querysetStoreGraph.d.ts +21 -0
- package/dist/syncEngine/registries/querysetStoreGraph.js +93 -0
- package/dist/syncEngine/registries/querysetStoreRegistry.d.ts +9 -0
- package/dist/syncEngine/registries/querysetStoreRegistry.js +36 -6
- package/dist/syncEngine/stores/operationEventHandlers.js +15 -10
- package/dist/syncEngine/stores/querysetStore.d.ts +6 -0
- package/dist/syncEngine/stores/querysetStore.js +57 -18
- package/package.json +1 -1
- package/dist/syncEngine/stores/querysetSemanticGraph.d.ts +0 -21
- package/dist/syncEngine/stores/querysetSemanticGraph.js +0 -58
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple graph for tracking queryset store ancestry
|
|
3
|
+
*/
|
|
4
|
+
export class QuerysetStoreGraph {
|
|
5
|
+
constructor(hasStoreFn?: null);
|
|
6
|
+
graph: any;
|
|
7
|
+
hasStoreFn: () => boolean;
|
|
8
|
+
processedQuerysets: Set<any>;
|
|
9
|
+
setHasStoreFn(hasStoreFn: any): void;
|
|
10
|
+
/**
|
|
11
|
+
* Add a queryset and its parent relationship to the graph
|
|
12
|
+
*/
|
|
13
|
+
addQueryset(queryset: any): void;
|
|
14
|
+
/**
|
|
15
|
+
* Find the root store for a queryset
|
|
16
|
+
* @param {Object} queryset - The queryset to analyze
|
|
17
|
+
* @returns {Object} { isRoot: boolean, root: semanticKey|null }
|
|
18
|
+
*/
|
|
19
|
+
findRoot(queryset: Object): Object;
|
|
20
|
+
clear(): void;
|
|
21
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { Graph } from "graphlib";
|
|
2
|
+
/**
|
|
3
|
+
* Simple graph for tracking queryset store ancestry
|
|
4
|
+
*/
|
|
5
|
+
export class QuerysetStoreGraph {
|
|
6
|
+
constructor(hasStoreFn = null) {
|
|
7
|
+
this.graph = new Graph({ directed: true });
|
|
8
|
+
this.hasStoreFn = hasStoreFn || (() => false);
|
|
9
|
+
this.processedQuerysets = new Set(); // Track UUIDs of processed querysets
|
|
10
|
+
}
|
|
11
|
+
setHasStoreFn(hasStoreFn) {
|
|
12
|
+
this.hasStoreFn = hasStoreFn;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Add a queryset and its parent relationship to the graph
|
|
16
|
+
*/
|
|
17
|
+
addQueryset(queryset) {
|
|
18
|
+
if (!queryset)
|
|
19
|
+
return;
|
|
20
|
+
if (this.processedQuerysets.has(queryset.key)) {
|
|
21
|
+
return; // Already processed, skip
|
|
22
|
+
}
|
|
23
|
+
let current = queryset;
|
|
24
|
+
while (current && !this.processedQuerysets.has(current.key)) {
|
|
25
|
+
const currentKey = current.semanticKey;
|
|
26
|
+
const currentUuid = current.key;
|
|
27
|
+
this.processedQuerysets.add(currentUuid);
|
|
28
|
+
this.graph.setNode(currentKey);
|
|
29
|
+
if (current.__parent) {
|
|
30
|
+
const parentKey = current.__parent.semanticKey;
|
|
31
|
+
this.graph.setNode(parentKey);
|
|
32
|
+
this.graph.setEdge(currentKey, parentKey); // child -> parent
|
|
33
|
+
current = current.__parent;
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Find the root store for a queryset
|
|
42
|
+
* @param {Object} queryset - The queryset to analyze
|
|
43
|
+
* @returns {Object} { isRoot: boolean, root: semanticKey|null }
|
|
44
|
+
*/
|
|
45
|
+
findRoot(queryset) {
|
|
46
|
+
// Validate input - null/undefined is a programming error
|
|
47
|
+
if (!queryset) {
|
|
48
|
+
throw new Error("findRoot was called with a null object, instead of a queryset");
|
|
49
|
+
}
|
|
50
|
+
// Handle queryset without semanticKey
|
|
51
|
+
if (!queryset.semanticKey) {
|
|
52
|
+
throw new Error("findRoot was called on an object without a semanticKey, which means its not a queryset. findRoot only works on querysets");
|
|
53
|
+
}
|
|
54
|
+
const semanticKey = queryset.semanticKey;
|
|
55
|
+
if (!this.graph.hasNode(semanticKey)) {
|
|
56
|
+
this.addQueryset(queryset);
|
|
57
|
+
}
|
|
58
|
+
// Traverse ALL the way up to find the HIGHEST ancestor with a store
|
|
59
|
+
const visited = new Set();
|
|
60
|
+
let current = semanticKey;
|
|
61
|
+
let highestAncestorWithStore = null;
|
|
62
|
+
while (current && !visited.has(current)) {
|
|
63
|
+
visited.add(current);
|
|
64
|
+
// Check if current node has a store
|
|
65
|
+
if (this.hasStoreFn(current)) {
|
|
66
|
+
highestAncestorWithStore = current;
|
|
67
|
+
}
|
|
68
|
+
// Move to parent
|
|
69
|
+
const parents = this.graph.successors(current) || [];
|
|
70
|
+
if (parents.length > 0) {
|
|
71
|
+
current = parents[0]; // Follow the parent chain
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
break; // No more parents
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (highestAncestorWithStore) {
|
|
78
|
+
if (highestAncestorWithStore === semanticKey) {
|
|
79
|
+
// This queryset itself is the highest with a store
|
|
80
|
+
return { isRoot: true, root: semanticKey };
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
// Found a higher ancestor with a store
|
|
84
|
+
return { isRoot: false, root: highestAncestorWithStore };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// No stores found anywhere in the chain
|
|
88
|
+
return { isRoot: true, root: null };
|
|
89
|
+
}
|
|
90
|
+
clear() {
|
|
91
|
+
this.graph = new Graph({ directed: true });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -23,6 +23,7 @@ declare class QuerysetStoreRegistry {
|
|
|
23
23
|
_tempStores: WeakMap<object, any>;
|
|
24
24
|
followingQuerysets: Map<any, any>;
|
|
25
25
|
syncManager: () => void;
|
|
26
|
+
querysetStoreGraph: QuerysetStoreGraph;
|
|
26
27
|
clear(): void;
|
|
27
28
|
setSyncManager(syncManager: any): void;
|
|
28
29
|
/**
|
|
@@ -30,6 +31,13 @@ declare class QuerysetStoreRegistry {
|
|
|
30
31
|
*/
|
|
31
32
|
addFollowingQueryset(semanticKey: any, queryset: any): void;
|
|
32
33
|
getStore(queryset: any, seed?: boolean): any;
|
|
34
|
+
/**
|
|
35
|
+
* Function to return the root store for a queryset
|
|
36
|
+
*/
|
|
37
|
+
getRootStore(queryset: any): {
|
|
38
|
+
isRoot: boolean;
|
|
39
|
+
rootStore: any;
|
|
40
|
+
};
|
|
33
41
|
/**
|
|
34
42
|
* Get the current state of the queryset, wrapped in a LiveQueryset
|
|
35
43
|
* @param {Object} queryset - The queryset
|
|
@@ -52,4 +60,5 @@ declare class QuerysetStoreRegistry {
|
|
|
52
60
|
*/
|
|
53
61
|
getAllStoresForModel(ModelClass: any): any[];
|
|
54
62
|
}
|
|
63
|
+
import { QuerysetStoreGraph } from './querysetStoreGraph.js';
|
|
55
64
|
export {};
|
|
@@ -16,11 +16,11 @@ import { wrapReactiveQuerySet } from '../../reactiveAdaptor.js';
|
|
|
16
16
|
import { processQuery, getRequiredFields, pickRequiredFields } from '../../filtering/localFiltering.js';
|
|
17
17
|
import { filter } from '../../filtering/localFiltering.js';
|
|
18
18
|
import { makeApiCall } from '../../flavours/django/makeApiCall.js';
|
|
19
|
+
import { QuerysetStoreGraph } from './querysetStoreGraph.js';
|
|
19
20
|
import { isNil, pick } from 'lodash-es';
|
|
20
21
|
import hash from 'object-hash';
|
|
21
22
|
import { Operation } from '../stores/operation.js';
|
|
22
23
|
import { Cache } from '../cache/cache.js';
|
|
23
|
-
import { querysetGraph } from '../stores/querysetSemanticGraph.js';
|
|
24
24
|
/**
|
|
25
25
|
* A dynamic wrapper that always returns the latest queryset results
|
|
26
26
|
* This class proxies array operations to always reflect the current state
|
|
@@ -107,6 +107,9 @@ class QuerysetStoreRegistry {
|
|
|
107
107
|
this._tempStores = new WeakMap(); // WeakMap<Queryset, Store>
|
|
108
108
|
this.followingQuerysets = new Map(); // Map<semanticKey, Set<queryset>>
|
|
109
109
|
this.syncManager = () => { console.warn("SyncManager not set for QuerysetStoreRegistry"); };
|
|
110
|
+
this.querysetStoreGraph = new QuerysetStoreGraph((semanticKey) => {
|
|
111
|
+
return this._stores.has(semanticKey);
|
|
112
|
+
});
|
|
110
113
|
}
|
|
111
114
|
clear() {
|
|
112
115
|
this._stores.forEach((store) => {
|
|
@@ -114,7 +117,7 @@ class QuerysetStoreRegistry {
|
|
|
114
117
|
});
|
|
115
118
|
this._stores = new Map();
|
|
116
119
|
this.followingQuerysets = new Map();
|
|
117
|
-
|
|
120
|
+
this.querysetStoreGraph.clear();
|
|
118
121
|
}
|
|
119
122
|
setSyncManager(syncManager) {
|
|
120
123
|
this.syncManager = syncManager;
|
|
@@ -132,6 +135,7 @@ class QuerysetStoreRegistry {
|
|
|
132
135
|
if (isNil(queryset) || isNil(queryset.ModelClass)) {
|
|
133
136
|
throw new Error("QuerysetStoreRegistry.getStore requires a valid queryset");
|
|
134
137
|
}
|
|
138
|
+
this.querysetStoreGraph.addQueryset(queryset);
|
|
135
139
|
// Check if we already have a temporary store for this exact QuerySet instance
|
|
136
140
|
if (this._tempStores.has(queryset)) {
|
|
137
141
|
return this._tempStores.get(queryset);
|
|
@@ -158,17 +162,39 @@ class QuerysetStoreRegistry {
|
|
|
158
162
|
let ast = queryset.build();
|
|
159
163
|
let ModelClass = queryset.ModelClass;
|
|
160
164
|
if (queryset.__parent && seed) {
|
|
161
|
-
|
|
162
|
-
|
|
165
|
+
const parentKey = queryset.__parent.semanticKey;
|
|
166
|
+
if (this._stores.has(parentKey)) {
|
|
167
|
+
let parentLiveQuerySet = this.getEntity(queryset.__parent);
|
|
168
|
+
initialGroundTruthPks = filter(parentLiveQuerySet, ast, ModelClass, false);
|
|
169
|
+
}
|
|
163
170
|
}
|
|
164
171
|
// Get the parent registry
|
|
165
172
|
const store = new QuerysetStore(ModelClass, fetchQueryset, queryset, initialGroundTruthPks, // Initial ground truth PKs
|
|
166
|
-
null // Initial operations
|
|
167
|
-
|
|
173
|
+
null, // Initial operations
|
|
174
|
+
{
|
|
175
|
+
getRootStore: this.getRootStore.bind(this),
|
|
176
|
+
isTemp: true,
|
|
177
|
+
});
|
|
168
178
|
// Store it in the temp store map
|
|
169
179
|
this._tempStores.set(queryset, store);
|
|
170
180
|
return store;
|
|
171
181
|
}
|
|
182
|
+
/**
|
|
183
|
+
* Function to return the root store for a queryset
|
|
184
|
+
*/
|
|
185
|
+
getRootStore(queryset) {
|
|
186
|
+
if (isNil(queryset)) {
|
|
187
|
+
throw new Error("QuerysetStoreRegistry.getRootStore requires a valid queryset");
|
|
188
|
+
}
|
|
189
|
+
const { isRoot, root } = this.querysetStoreGraph.findRoot(queryset);
|
|
190
|
+
const rootStore = this._stores.get(root);
|
|
191
|
+
if (!isRoot && rootStore) {
|
|
192
|
+
return { isRoot: false, rootStore: rootStore };
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
return { isRoot: true, rootStore: null };
|
|
196
|
+
}
|
|
197
|
+
}
|
|
172
198
|
/**
|
|
173
199
|
* Get the current state of the queryset, wrapped in a LiveQueryset
|
|
174
200
|
* @param {Object} queryset - The queryset
|
|
@@ -185,12 +211,14 @@ class QuerysetStoreRegistry {
|
|
|
185
211
|
// If we have a temporary store, promote it
|
|
186
212
|
if (this._tempStores.has(queryset)) {
|
|
187
213
|
store = this._tempStores.get(queryset);
|
|
214
|
+
store.isTemp = false; // Promote to permanent store
|
|
188
215
|
this._stores.set(semanticKey, store);
|
|
189
216
|
this.syncManager.followModel(this, queryset.ModelClass);
|
|
190
217
|
}
|
|
191
218
|
// Otherwise, ensure we have a permanent store
|
|
192
219
|
else if (!this._stores.has(semanticKey)) {
|
|
193
220
|
store = this.getStore(queryset, seed);
|
|
221
|
+
store.isTemp = false;
|
|
194
222
|
this._stores.set(semanticKey, store);
|
|
195
223
|
this.syncManager.followModel(this, queryset.ModelClass);
|
|
196
224
|
}
|
|
@@ -221,11 +249,13 @@ class QuerysetStoreRegistry {
|
|
|
221
249
|
// If we have a temp store, promote it
|
|
222
250
|
if (this._tempStores.has(queryset)) {
|
|
223
251
|
store = this._tempStores.get(queryset);
|
|
252
|
+
store.isTemp = false; // Promote to permanent store
|
|
224
253
|
this._stores.set(semanticKey, store);
|
|
225
254
|
}
|
|
226
255
|
else {
|
|
227
256
|
// Create a new permanent store
|
|
228
257
|
store = this.getStore(queryset);
|
|
258
|
+
store.isTemp = false;
|
|
229
259
|
this._stores.set(semanticKey, store);
|
|
230
260
|
}
|
|
231
261
|
}
|
|
@@ -4,7 +4,6 @@ import { querysetStoreRegistry } from '../registries/querysetStoreRegistry.js';
|
|
|
4
4
|
import { metricRegistry } from '../registries/metricRegistry.js';
|
|
5
5
|
import { getFingerprint } from './utils.js';
|
|
6
6
|
import { QuerySet } from '../../flavours/django/querySet.js';
|
|
7
|
-
import { querysetGraph } from './querysetSemanticGraph.js';
|
|
8
7
|
import { isEqual, isNil } from 'lodash-es';
|
|
9
8
|
import hash from 'object-hash';
|
|
10
9
|
/**
|
|
@@ -13,21 +12,27 @@ import hash from 'object-hash';
|
|
|
13
12
|
* @returns {Map<QuerySet, Store>}
|
|
14
13
|
*/
|
|
15
14
|
function relatedQuerysets(queryset) {
|
|
15
|
+
// Collect ancestor nodes for comparison
|
|
16
|
+
let ancestorNodes = [];
|
|
17
|
+
let current = queryset;
|
|
18
|
+
while (current) {
|
|
19
|
+
ancestorNodes.push(current.nodes);
|
|
20
|
+
current = current.__parent;
|
|
21
|
+
}
|
|
22
|
+
const modelClass = queryset.ModelClass;
|
|
16
23
|
const result = new Map();
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
for (const qs of semanticAncestors) {
|
|
24
|
+
Array.from(querysetStoreRegistry._stores.entries()).forEach(([queryset, store]) => {
|
|
25
|
+
if (store.modelClass !== modelClass)
|
|
26
|
+
return;
|
|
21
27
|
try {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
result.set(qs, store);
|
|
28
|
+
if (ancestorNodes.some(nodes => isEqual(nodes, store.queryset.nodes))) {
|
|
29
|
+
result.set(store.queryset, store);
|
|
25
30
|
}
|
|
26
31
|
}
|
|
27
32
|
catch (e) {
|
|
28
|
-
console.warn('Error
|
|
33
|
+
console.warn('Error comparing nodes for related querysets', e);
|
|
29
34
|
}
|
|
30
|
-
}
|
|
35
|
+
});
|
|
31
36
|
return result;
|
|
32
37
|
}
|
|
33
38
|
/**
|
|
@@ -6,7 +6,11 @@ export class QuerysetStore {
|
|
|
6
6
|
operationsMap: Map<any, any>;
|
|
7
7
|
groundTruthPks: never[];
|
|
8
8
|
isSyncing: boolean;
|
|
9
|
+
lastSync: number | null;
|
|
10
|
+
needsSync: boolean;
|
|
11
|
+
isTemp: any;
|
|
9
12
|
pruneThreshold: any;
|
|
13
|
+
getRootStore: any;
|
|
10
14
|
qsCache: Cache;
|
|
11
15
|
get cacheKey(): any;
|
|
12
16
|
onHydrated(hydratedData: any): void;
|
|
@@ -26,6 +30,8 @@ export class QuerysetStore {
|
|
|
26
30
|
getInflightOperations(): any[];
|
|
27
31
|
prune(): void;
|
|
28
32
|
render(optimistic?: boolean, fromCache?: boolean): any[];
|
|
33
|
+
renderFromRoot(optimistic: boolean | undefined, rootStore: any): any[];
|
|
34
|
+
renderFromData(optimistic?: boolean): any[];
|
|
29
35
|
applyOperation(operation: any, currentPks: any): any;
|
|
30
36
|
sync(): Promise<void>;
|
|
31
37
|
}
|
|
@@ -4,16 +4,21 @@ import { isNil, isEmpty, trim } 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 { querysetGraph } from "./querysetSemanticGraph.js";
|
|
8
7
|
import hash from 'object-hash';
|
|
9
8
|
import { Cache } from '../cache/cache.js';
|
|
9
|
+
import { filter } from "../../filtering/localFiltering.js";
|
|
10
|
+
import { mod } from 'mathjs';
|
|
10
11
|
export class QuerysetStore {
|
|
11
12
|
constructor(modelClass, fetchFn, queryset, initialGroundTruthPks = null, initialOperations = null, options = {}) {
|
|
12
13
|
this.modelClass = modelClass;
|
|
13
14
|
this.fetchFn = fetchFn;
|
|
14
15
|
this.queryset = queryset;
|
|
15
16
|
this.isSyncing = false;
|
|
17
|
+
this.lastSync = null;
|
|
18
|
+
this.needsSync = false;
|
|
19
|
+
this.isTemp = options.isTemp || false;
|
|
16
20
|
this.pruneThreshold = options.pruneThreshold || 10;
|
|
21
|
+
this.getRootStore = options.getRootStore || null;
|
|
17
22
|
this.groundTruthPks = initialGroundTruthPks || [];
|
|
18
23
|
this.operationsMap = new Map();
|
|
19
24
|
if (Array.isArray(initialOperations)) {
|
|
@@ -23,8 +28,7 @@ export class QuerysetStore {
|
|
|
23
28
|
this.operationsMap.set(op.operationId, op);
|
|
24
29
|
}
|
|
25
30
|
}
|
|
26
|
-
this.qsCache = new Cache(
|
|
27
|
-
querysetGraph.add(queryset);
|
|
31
|
+
this.qsCache = new Cache("queryset-cache", {}, this.onHydrated.bind(this));
|
|
28
32
|
}
|
|
29
33
|
// Caching
|
|
30
34
|
get cacheKey() {
|
|
@@ -42,7 +46,7 @@ export class QuerysetStore {
|
|
|
42
46
|
setCache(result) {
|
|
43
47
|
let nonTempPks = [];
|
|
44
48
|
result.forEach((pk) => {
|
|
45
|
-
if (typeof pk ===
|
|
49
|
+
if (typeof pk === "string" && containsTempPk(pk)) {
|
|
46
50
|
pk = replaceTempPks(pk);
|
|
47
51
|
if (isNil(pk) || isEmpty(trim(pk))) {
|
|
48
52
|
return;
|
|
@@ -95,9 +99,7 @@ export class QuerysetStore {
|
|
|
95
99
|
this._emitRenderEvent();
|
|
96
100
|
}
|
|
97
101
|
async setGroundTruth(groundTruthPks) {
|
|
98
|
-
this.groundTruthPks = Array.isArray(groundTruthPks)
|
|
99
|
-
? groundTruthPks
|
|
100
|
-
: [];
|
|
102
|
+
this.groundTruthPks = Array.isArray(groundTruthPks) ? groundTruthPks : [];
|
|
101
103
|
this._emitRenderEvent();
|
|
102
104
|
}
|
|
103
105
|
async setOperations(operations) {
|
|
@@ -111,10 +113,10 @@ export class QuerysetStore {
|
|
|
111
113
|
}
|
|
112
114
|
getTrimmedOperations() {
|
|
113
115
|
const cutoff = Date.now() - 1000 * 60 * 2;
|
|
114
|
-
return this.operations.filter(op => op.timestamp > cutoff);
|
|
116
|
+
return this.operations.filter((op) => op.timestamp > cutoff);
|
|
115
117
|
}
|
|
116
118
|
getInflightOperations() {
|
|
117
|
-
return this.operations.filter(operation => operation.status != Status.CONFIRMED &&
|
|
119
|
+
return this.operations.filter((operation) => operation.status != Status.CONFIRMED &&
|
|
118
120
|
operation.status != Status.REJECTED);
|
|
119
121
|
}
|
|
120
122
|
prune() {
|
|
@@ -129,13 +131,16 @@ export class QuerysetStore {
|
|
|
129
131
|
return cachedResult;
|
|
130
132
|
}
|
|
131
133
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
134
|
+
let result;
|
|
135
|
+
if (this.getRootStore && typeof this.getRootStore === "function" && !this.isTemp) {
|
|
136
|
+
const { isRoot, rootStore } = this.getRootStore(this.queryset);
|
|
137
|
+
if (!isRoot && rootStore) {
|
|
138
|
+
result = this.renderFromRoot(optimistic, rootStore);
|
|
136
139
|
}
|
|
137
140
|
}
|
|
138
|
-
|
|
141
|
+
if (isNil(result)) {
|
|
142
|
+
result = this.renderFromData(optimistic);
|
|
143
|
+
}
|
|
139
144
|
let limit = this.queryset.build().serializerOptions?.limit;
|
|
140
145
|
if (limit) {
|
|
141
146
|
result = result.slice(0, limit);
|
|
@@ -143,12 +148,30 @@ export class QuerysetStore {
|
|
|
143
148
|
this.setCache(result);
|
|
144
149
|
return result;
|
|
145
150
|
}
|
|
151
|
+
renderFromRoot(optimistic = true, rootStore) {
|
|
152
|
+
let renderedPks = rootStore.render(optimistic);
|
|
153
|
+
let renderedData = renderedPks.map((pk) => {
|
|
154
|
+
return this.modelClass.fromPk(pk, this.queryset);
|
|
155
|
+
});
|
|
156
|
+
let ast = this.queryset.build();
|
|
157
|
+
let result = filter(renderedData, ast, this.modelClass, false);
|
|
158
|
+
return result;
|
|
159
|
+
}
|
|
160
|
+
renderFromData(optimistic = true) {
|
|
161
|
+
const renderedPks = this.groundTruthSet;
|
|
162
|
+
for (const op of this.operations) {
|
|
163
|
+
if (op.status !== Status.REJECTED &&
|
|
164
|
+
(optimistic || op.status === Status.CONFIRMED)) {
|
|
165
|
+
this.applyOperation(op, renderedPks);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
let result = Array.from(renderedPks);
|
|
169
|
+
return result;
|
|
170
|
+
}
|
|
146
171
|
applyOperation(operation, currentPks) {
|
|
147
172
|
const pkField = this.pkField;
|
|
148
173
|
for (const instance of operation.instances) {
|
|
149
|
-
if (!instance ||
|
|
150
|
-
typeof instance !== 'object' ||
|
|
151
|
-
!(pkField in instance)) {
|
|
174
|
+
if (!instance || typeof instance !== "object" || !(pkField in instance)) {
|
|
152
175
|
console.warn(`[QuerysetStore ${this.modelClass.modelName}] Skipping instance in operation ${operation.operationId} due to missing PK '${String(pkField)}' or invalid format.`);
|
|
153
176
|
continue;
|
|
154
177
|
}
|
|
@@ -177,12 +200,25 @@ export class QuerysetStore {
|
|
|
177
200
|
console.warn(`[QuerysetStore ${id}] Already syncing, request ignored.`);
|
|
178
201
|
return;
|
|
179
202
|
}
|
|
203
|
+
// Check if we're delegating to a root store
|
|
204
|
+
if (this.getRootStore && typeof this.getRootStore === "function" && !this.isTemp) {
|
|
205
|
+
const { isRoot, rootStore } = this.getRootStore(this.queryset);
|
|
206
|
+
if (!isRoot && rootStore) {
|
|
207
|
+
// We're delegating to a root store - don't sync, just mark as needing sync
|
|
208
|
+
console.log(`[${id}] Delegating to root store, marking sync needed.`);
|
|
209
|
+
this.needsSync = true;
|
|
210
|
+
this.lastSync = null; // Clear last sync since we're not actually syncing
|
|
211
|
+
this.setOperations(this.getInflightOperations());
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
// We're in independent mode - proceed with normal sync
|
|
180
216
|
this.isSyncing = true;
|
|
181
217
|
console.log(`[${id}] Starting sync...`);
|
|
182
218
|
try {
|
|
183
219
|
const response = await this.fetchFn({
|
|
184
220
|
ast: this.queryset.build(),
|
|
185
|
-
modelClass: this.modelClass
|
|
221
|
+
modelClass: this.modelClass,
|
|
186
222
|
});
|
|
187
223
|
const { data, included } = response;
|
|
188
224
|
console.log(`[${id}] Sync fetch completed. Received: ${JSON.stringify(data)}.`);
|
|
@@ -190,10 +226,13 @@ export class QuerysetStore {
|
|
|
190
226
|
processIncludedEntities(modelStoreRegistry, included, this.modelClass);
|
|
191
227
|
this.setGroundTruth(data);
|
|
192
228
|
this.setOperations(this.getInflightOperations());
|
|
229
|
+
this.lastSync = Date.now();
|
|
230
|
+
this.needsSync = false;
|
|
193
231
|
console.log(`[${id}] Sync completed.`);
|
|
194
232
|
}
|
|
195
233
|
catch (e) {
|
|
196
234
|
console.error(`[${id}] Failed to sync ground truth:`, e);
|
|
235
|
+
this.needsSync = true; // Mark as needing sync on error
|
|
197
236
|
}
|
|
198
237
|
finally {
|
|
199
238
|
this.isSyncing = false;
|
package/package.json
CHANGED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
export const querysetGraph: QuerysetGraph;
|
|
2
|
-
/**
|
|
3
|
-
* Simple graph using graphlib: AST nodes with parent edges
|
|
4
|
-
*/
|
|
5
|
-
declare class QuerysetGraph {
|
|
6
|
-
graph: any;
|
|
7
|
-
clear(): void;
|
|
8
|
-
add(queryset: any): void;
|
|
9
|
-
/**
|
|
10
|
-
* Gets all querysets that share semantic equivalence with ancestors of the given queryset.
|
|
11
|
-
*
|
|
12
|
-
* This traverses up the parent chain to find all ancestor nodes (based on semanticId),
|
|
13
|
-
* then returns all querysets stored in those nodes that match the same model class.
|
|
14
|
-
* Querysets with the same semanticId are considered semantically equivalent (same AST structure).
|
|
15
|
-
*
|
|
16
|
-
* @param {QuerySet} queryset - The queryset to find semantic ancestors for
|
|
17
|
-
* @returns {QuerySet[]} Array of querysets that are semantically equivalent to ancestors
|
|
18
|
-
*/
|
|
19
|
-
getSemanticAncestors(queryset: QuerySet): QuerySet[];
|
|
20
|
-
}
|
|
21
|
-
export {};
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { Graph, alg } from "graphlib";
|
|
2
|
-
/**
|
|
3
|
-
* Simple graph using graphlib: AST nodes with parent edges
|
|
4
|
-
*/
|
|
5
|
-
class QuerysetGraph {
|
|
6
|
-
constructor() {
|
|
7
|
-
this.graph = new Graph({ directed: true });
|
|
8
|
-
}
|
|
9
|
-
clear() {
|
|
10
|
-
this.graph = new Graph({ directed: true });
|
|
11
|
-
}
|
|
12
|
-
add(queryset) {
|
|
13
|
-
const astId = queryset.semanticId;
|
|
14
|
-
// Get or create node data with Set
|
|
15
|
-
let nodeData = this.graph.node(astId);
|
|
16
|
-
if (!nodeData) {
|
|
17
|
-
nodeData = { querysets: new Set() };
|
|
18
|
-
this.graph.setNode(astId, nodeData);
|
|
19
|
-
}
|
|
20
|
-
// Add queryset to Set (automatically handles duplicates)
|
|
21
|
-
nodeData.querysets.add(queryset);
|
|
22
|
-
// Add parent edge if exists
|
|
23
|
-
if (queryset.__parent) {
|
|
24
|
-
const parentAstId = queryset.__parent.semanticId;
|
|
25
|
-
this.graph.setEdge(astId, parentAstId);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* Gets all querysets that share semantic equivalence with ancestors of the given queryset.
|
|
30
|
-
*
|
|
31
|
-
* This traverses up the parent chain to find all ancestor nodes (based on semanticId),
|
|
32
|
-
* then returns all querysets stored in those nodes that match the same model class.
|
|
33
|
-
* Querysets with the same semanticId are considered semantically equivalent (same AST structure).
|
|
34
|
-
*
|
|
35
|
-
* @param {QuerySet} queryset - The queryset to find semantic ancestors for
|
|
36
|
-
* @returns {QuerySet[]} Array of querysets that are semantically equivalent to ancestors
|
|
37
|
-
*/
|
|
38
|
-
getSemanticAncestors(queryset) {
|
|
39
|
-
const modelClass = queryset.ModelClass;
|
|
40
|
-
const result = [];
|
|
41
|
-
// Use graphlib to get all ancestors (including the node itself)
|
|
42
|
-
const ancestorNodeIds = alg.postorder(this.graph, [queryset.semanticId]);
|
|
43
|
-
// Collect all querysets from ancestor nodes
|
|
44
|
-
for (const nodeId of ancestorNodeIds) {
|
|
45
|
-
const nodeData = this.graph.node(nodeId);
|
|
46
|
-
if (!nodeData || !nodeData.querysets)
|
|
47
|
-
continue;
|
|
48
|
-
// Add all querysets from this node that match the model class
|
|
49
|
-
for (const qs of nodeData.querysets) {
|
|
50
|
-
if (qs.ModelClass === modelClass) {
|
|
51
|
-
result.push(qs);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
return result;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
export const querysetGraph = new QuerysetGraph();
|