@statezero/core 0.1.2 → 0.1.3-9.1
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/LICENSE +3 -2
- package/dist/cli/commands/sync.d.ts +6 -0
- package/dist/cli/commands/sync.js +30 -0
- package/dist/cli/commands/syncActions.d.ts +46 -0
- package/dist/cli/commands/syncActions.js +623 -0
- package/dist/cli/commands/syncModels.js +17 -35
- package/dist/cli/index.js +18 -10
- package/dist/config.d.ts +5 -0
- package/dist/config.js +40 -10
- package/dist/flavours/django/dates.js +3 -3
- package/dist/flavours/django/files.d.ts +8 -7
- package/dist/flavours/django/files.js +36 -2
- package/dist/flavours/django/model.d.ts +15 -0
- package/dist/flavours/django/model.js +143 -24
- package/dist/setup.js +11 -0
- package/dist/syncEngine/registries/metricRegistry.d.ts +5 -0
- package/dist/syncEngine/registries/metricRegistry.js +8 -0
- package/dist/syncEngine/registries/querysetStoreGraph.d.ts +21 -0
- package/dist/syncEngine/registries/querysetStoreGraph.js +95 -0
- package/dist/syncEngine/registries/querysetStoreRegistry.d.ts +14 -0
- package/dist/syncEngine/registries/querysetStoreRegistry.js +64 -16
- package/dist/syncEngine/stores/modelStore.d.ts +1 -0
- package/dist/syncEngine/stores/modelStore.js +23 -12
- package/dist/syncEngine/stores/querysetStore.d.ts +18 -0
- package/dist/syncEngine/stores/querysetStore.js +129 -18
- package/dist/syncEngine/sync.d.ts +5 -0
- package/dist/syncEngine/sync.js +61 -5
- package/package.json +126 -123
- package/readme.md +1 -1
package/dist/setup.js
CHANGED
|
@@ -2,6 +2,9 @@ import { configInstance } from "./config.js";
|
|
|
2
2
|
import { setAdapters } from "./reactiveAdaptor.js";
|
|
3
3
|
import { syncManager } from "./syncEngine/sync.js";
|
|
4
4
|
import { initEventHandler } from "./syncEngine/stores/operationEventHandlers.js";
|
|
5
|
+
import { querysetStoreRegistry } from "./syncEngine/registries/querysetStoreRegistry.js";
|
|
6
|
+
import { modelStoreRegistry } from "./syncEngine/registries/modelStoreRegistry.js";
|
|
7
|
+
import { metricRegistry } from "./syncEngine/registries/metricRegistry.js";
|
|
5
8
|
/**
|
|
6
9
|
* Initialize StateZero with the provided configuration
|
|
7
10
|
*
|
|
@@ -19,4 +22,12 @@ export function setupStateZero(config, getModelClass, adapters) {
|
|
|
19
22
|
setAdapters(adapters.ModelAdaptor, adapters.QuerySetAdaptor, adapters.MetricAdaptor);
|
|
20
23
|
initEventHandler();
|
|
21
24
|
syncManager.initialize();
|
|
25
|
+
// Expose registries and sync manager for devtools de
|
|
26
|
+
if (typeof window !== "undefined") {
|
|
27
|
+
window.__STATEZERO_DEVTOOLS__ = {
|
|
28
|
+
querysetStoreRegistry,
|
|
29
|
+
modelStoreRegistry,
|
|
30
|
+
metricRegistry,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
22
33
|
}
|
|
@@ -8,6 +8,11 @@ export class LiveMetric {
|
|
|
8
8
|
metricType: any;
|
|
9
9
|
field: any;
|
|
10
10
|
get lqs(): import("./querysetStoreRegistry").LiveQueryset;
|
|
11
|
+
/**
|
|
12
|
+
* Refresh the metric data from the database
|
|
13
|
+
* Delegates to the underlying store's sync method
|
|
14
|
+
*/
|
|
15
|
+
refreshFromDb(): any;
|
|
11
16
|
/**
|
|
12
17
|
* Getter that always returns the current value from the store
|
|
13
18
|
*/
|
|
@@ -17,6 +17,14 @@ export class LiveMetric {
|
|
|
17
17
|
get lqs() {
|
|
18
18
|
return querysetStoreRegistry.getEntity(this.queryset);
|
|
19
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Refresh the metric data from the database
|
|
22
|
+
* Delegates to the underlying store's sync method
|
|
23
|
+
*/
|
|
24
|
+
refreshFromDb() {
|
|
25
|
+
const store = metricRegistry.getStore(this.metricType, this.queryset, this.field);
|
|
26
|
+
return store.sync();
|
|
27
|
+
}
|
|
20
28
|
/**
|
|
21
29
|
* Getter that always returns the current value from the store
|
|
22
30
|
*/
|
|
@@ -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,95 @@
|
|
|
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
|
+
if (currentKey !== parentKey) {
|
|
33
|
+
this.graph.setEdge(currentKey, parentKey);
|
|
34
|
+
}
|
|
35
|
+
current = current.__parent;
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Find the root store for a queryset
|
|
44
|
+
* @param {Object} queryset - The queryset to analyze
|
|
45
|
+
* @returns {Object} { isRoot: boolean, root: semanticKey|null }
|
|
46
|
+
*/
|
|
47
|
+
findRoot(queryset) {
|
|
48
|
+
// Validate input - null/undefined is a programming error
|
|
49
|
+
if (!queryset) {
|
|
50
|
+
throw new Error("findRoot was called with a null object, instead of a queryset");
|
|
51
|
+
}
|
|
52
|
+
// Handle queryset without semanticKey
|
|
53
|
+
if (!queryset.semanticKey) {
|
|
54
|
+
throw new Error("findRoot was called on an object without a semanticKey, which means its not a queryset. findRoot only works on querysets");
|
|
55
|
+
}
|
|
56
|
+
const semanticKey = queryset.semanticKey;
|
|
57
|
+
if (!this.graph.hasNode(semanticKey)) {
|
|
58
|
+
this.addQueryset(queryset);
|
|
59
|
+
}
|
|
60
|
+
// Traverse ALL the way up to find the HIGHEST ancestor with a store
|
|
61
|
+
const visited = new Set();
|
|
62
|
+
let current = semanticKey;
|
|
63
|
+
let highestAncestorWithStore = null;
|
|
64
|
+
while (current && !visited.has(current)) {
|
|
65
|
+
visited.add(current);
|
|
66
|
+
// Check if current node has a store
|
|
67
|
+
if (this.hasStoreFn(current)) {
|
|
68
|
+
highestAncestorWithStore = current;
|
|
69
|
+
}
|
|
70
|
+
// Move to parent
|
|
71
|
+
const parents = this.graph.successors(current) || [];
|
|
72
|
+
if (parents.length > 0) {
|
|
73
|
+
current = parents[0]; // Follow the parent chain
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
break; // No more parents
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (highestAncestorWithStore) {
|
|
80
|
+
if (highestAncestorWithStore === semanticKey) {
|
|
81
|
+
// This queryset itself is the highest with a store
|
|
82
|
+
return { isRoot: true, root: semanticKey };
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
// Found a higher ancestor with a store
|
|
86
|
+
return { isRoot: false, root: highestAncestorWithStore };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// No stores found anywhere in the chain
|
|
90
|
+
return { isRoot: true, root: null };
|
|
91
|
+
}
|
|
92
|
+
clear() {
|
|
93
|
+
this.graph = new Graph({ directed: true });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -9,6 +9,11 @@ export class LiveQueryset {
|
|
|
9
9
|
* Serializes the lqs as a simple array of objects, for freezing e.g in the metric stores
|
|
10
10
|
*/
|
|
11
11
|
serialize(): any;
|
|
12
|
+
/**
|
|
13
|
+
* Refresh the queryset data from the database
|
|
14
|
+
* Delegates to the underlying store's sync method
|
|
15
|
+
*/
|
|
16
|
+
refreshFromDb(): any;
|
|
12
17
|
/**
|
|
13
18
|
* Get the current items from the store
|
|
14
19
|
* @private
|
|
@@ -23,6 +28,7 @@ declare class QuerysetStoreRegistry {
|
|
|
23
28
|
_tempStores: WeakMap<object, any>;
|
|
24
29
|
followingQuerysets: Map<any, any>;
|
|
25
30
|
syncManager: () => void;
|
|
31
|
+
querysetStoreGraph: QuerysetStoreGraph;
|
|
26
32
|
clear(): void;
|
|
27
33
|
setSyncManager(syncManager: any): void;
|
|
28
34
|
/**
|
|
@@ -30,6 +36,13 @@ declare class QuerysetStoreRegistry {
|
|
|
30
36
|
*/
|
|
31
37
|
addFollowingQueryset(semanticKey: any, queryset: any): void;
|
|
32
38
|
getStore(queryset: any, seed?: boolean): any;
|
|
39
|
+
/**
|
|
40
|
+
* Function to return the root store for a queryset
|
|
41
|
+
*/
|
|
42
|
+
getRootStore(queryset: any): {
|
|
43
|
+
isRoot: boolean;
|
|
44
|
+
rootStore: any;
|
|
45
|
+
};
|
|
33
46
|
/**
|
|
34
47
|
* Get the current state of the queryset, wrapped in a LiveQueryset
|
|
35
48
|
* @param {Object} queryset - The queryset
|
|
@@ -52,4 +65,5 @@ declare class QuerysetStoreRegistry {
|
|
|
52
65
|
*/
|
|
53
66
|
getAllStoresForModel(ModelClass: any): any[];
|
|
54
67
|
}
|
|
68
|
+
import { QuerysetStoreGraph } from './querysetStoreGraph.js';
|
|
55
69
|
export {};
|
|
@@ -16,6 +16,7 @@ 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';
|
|
@@ -40,28 +41,37 @@ export class LiveQueryset {
|
|
|
40
41
|
__classPrivateFieldSet(this, _LiveQueryset_proxy, new Proxy(__classPrivateFieldGet(this, _LiveQueryset_array, "f"), {
|
|
41
42
|
get: (target, prop, receiver) => {
|
|
42
43
|
// Expose the touch method through the proxy
|
|
43
|
-
if (prop ===
|
|
44
|
+
if (prop === "touch") {
|
|
44
45
|
return () => this.touch();
|
|
45
46
|
}
|
|
46
|
-
if (prop ===
|
|
47
|
+
if (prop === "serialize") {
|
|
47
48
|
return () => this.serialize();
|
|
48
49
|
}
|
|
49
50
|
// Special handling for iterators and common array methods
|
|
50
51
|
if (prop === Symbol.iterator) {
|
|
51
52
|
return () => this.getCurrentItems()[Symbol.iterator]();
|
|
52
53
|
}
|
|
53
|
-
else if (typeof prop ===
|
|
54
|
+
else if (typeof prop === "string" &&
|
|
55
|
+
[
|
|
56
|
+
"forEach",
|
|
57
|
+
"map",
|
|
58
|
+
"filter",
|
|
59
|
+
"reduce",
|
|
60
|
+
"some",
|
|
61
|
+
"every",
|
|
62
|
+
"find",
|
|
63
|
+
].includes(prop)) {
|
|
54
64
|
return (...args) => this.getCurrentItems()[prop](...args);
|
|
55
65
|
}
|
|
56
|
-
else if (prop ===
|
|
66
|
+
else if (prop === "length") {
|
|
57
67
|
return this.getCurrentItems().length;
|
|
58
68
|
}
|
|
59
|
-
else if (typeof prop ===
|
|
69
|
+
else if (typeof prop === "string" && !isNaN(parseInt(prop))) {
|
|
60
70
|
// Handle numeric indices
|
|
61
71
|
return this.getCurrentItems()[prop];
|
|
62
72
|
}
|
|
63
73
|
return target[prop];
|
|
64
|
-
}
|
|
74
|
+
},
|
|
65
75
|
}), "f");
|
|
66
76
|
return __classPrivateFieldGet(this, _LiveQueryset_proxy, "f");
|
|
67
77
|
}
|
|
@@ -73,30 +83,37 @@ export class LiveQueryset {
|
|
|
73
83
|
// Get the current primary keys from the store
|
|
74
84
|
const pks = store.render();
|
|
75
85
|
// Map primary keys to full model objects
|
|
76
|
-
return pks.map(pk => {
|
|
86
|
+
return pks.map((pk) => {
|
|
77
87
|
// Get the full model instance from the model store
|
|
78
88
|
const pkField = __classPrivateFieldGet(this, _LiveQueryset_ModelClass, "f").primaryKeyField;
|
|
79
89
|
return __classPrivateFieldGet(this, _LiveQueryset_ModelClass, "f").fromPk(pk, __classPrivateFieldGet(this, _LiveQueryset_queryset, "f")).serialize();
|
|
80
90
|
});
|
|
81
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* Refresh the queryset data from the database
|
|
94
|
+
* Delegates to the underlying store's sync method
|
|
95
|
+
*/
|
|
96
|
+
refreshFromDb() {
|
|
97
|
+
const store = querysetStoreRegistry.getStore(__classPrivateFieldGet(this, _LiveQueryset_queryset, "f"));
|
|
98
|
+
return store.sync();
|
|
99
|
+
}
|
|
82
100
|
/**
|
|
83
101
|
* Get the current items from the store
|
|
84
102
|
* @private
|
|
85
103
|
* @returns {Array} The current items in the queryset
|
|
86
104
|
*/
|
|
87
|
-
getCurrentItems(
|
|
105
|
+
getCurrentItems() {
|
|
88
106
|
const store = querysetStoreRegistry.getStore(__classPrivateFieldGet(this, _LiveQueryset_queryset, "f"));
|
|
89
107
|
// Get the current primary keys from the store
|
|
90
108
|
const pks = store.render();
|
|
91
109
|
// Map primary keys to full model objects
|
|
92
|
-
const instances = pks
|
|
110
|
+
const instances = pks
|
|
111
|
+
.map((pk) => {
|
|
93
112
|
// Get the full model instance from the model store
|
|
94
113
|
const pkField = __classPrivateFieldGet(this, _LiveQueryset_ModelClass, "f").primaryKeyField;
|
|
95
114
|
return __classPrivateFieldGet(this, _LiveQueryset_ModelClass, "f").fromPk(pk, __classPrivateFieldGet(this, _LiveQueryset_queryset, "f"));
|
|
96
115
|
});
|
|
97
|
-
|
|
98
|
-
return instances;
|
|
99
|
-
return filter(instances, __classPrivateFieldGet(this, _LiveQueryset_queryset, "f").build(), __classPrivateFieldGet(this, _LiveQueryset_ModelClass, "f"), true);
|
|
116
|
+
return instances;
|
|
100
117
|
}
|
|
101
118
|
}
|
|
102
119
|
_LiveQueryset_queryset = new WeakMap(), _LiveQueryset_ModelClass = new WeakMap(), _LiveQueryset_proxy = new WeakMap(), _LiveQueryset_array = new WeakMap();
|
|
@@ -106,6 +123,9 @@ class QuerysetStoreRegistry {
|
|
|
106
123
|
this._tempStores = new WeakMap(); // WeakMap<Queryset, Store>
|
|
107
124
|
this.followingQuerysets = new Map(); // Map<semanticKey, Set<queryset>>
|
|
108
125
|
this.syncManager = () => { console.warn("SyncManager not set for QuerysetStoreRegistry"); };
|
|
126
|
+
this.querysetStoreGraph = new QuerysetStoreGraph((semanticKey) => {
|
|
127
|
+
return this._stores.has(semanticKey);
|
|
128
|
+
});
|
|
109
129
|
}
|
|
110
130
|
clear() {
|
|
111
131
|
this._stores.forEach((store) => {
|
|
@@ -113,6 +133,7 @@ class QuerysetStoreRegistry {
|
|
|
113
133
|
});
|
|
114
134
|
this._stores = new Map();
|
|
115
135
|
this.followingQuerysets = new Map();
|
|
136
|
+
this.querysetStoreGraph.clear();
|
|
116
137
|
}
|
|
117
138
|
setSyncManager(syncManager) {
|
|
118
139
|
this.syncManager = syncManager;
|
|
@@ -130,6 +151,7 @@ class QuerysetStoreRegistry {
|
|
|
130
151
|
if (isNil(queryset) || isNil(queryset.ModelClass)) {
|
|
131
152
|
throw new Error("QuerysetStoreRegistry.getStore requires a valid queryset");
|
|
132
153
|
}
|
|
154
|
+
this.querysetStoreGraph.addQueryset(queryset);
|
|
133
155
|
// Check if we already have a temporary store for this exact QuerySet instance
|
|
134
156
|
if (this._tempStores.has(queryset)) {
|
|
135
157
|
return this._tempStores.get(queryset);
|
|
@@ -156,17 +178,39 @@ class QuerysetStoreRegistry {
|
|
|
156
178
|
let ast = queryset.build();
|
|
157
179
|
let ModelClass = queryset.ModelClass;
|
|
158
180
|
if (queryset.__parent && seed) {
|
|
159
|
-
|
|
160
|
-
|
|
181
|
+
const parentKey = queryset.__parent.semanticKey;
|
|
182
|
+
if (this._stores.has(parentKey)) {
|
|
183
|
+
let parentLiveQuerySet = this.getEntity(queryset.__parent);
|
|
184
|
+
initialGroundTruthPks = filter(parentLiveQuerySet, ast, ModelClass, false);
|
|
185
|
+
}
|
|
161
186
|
}
|
|
162
187
|
// Get the parent registry
|
|
163
188
|
const store = new QuerysetStore(ModelClass, fetchQueryset, queryset, initialGroundTruthPks, // Initial ground truth PKs
|
|
164
|
-
null // Initial operations
|
|
165
|
-
|
|
189
|
+
null, // Initial operations
|
|
190
|
+
{
|
|
191
|
+
getRootStore: this.getRootStore.bind(this),
|
|
192
|
+
isTemp: true,
|
|
193
|
+
});
|
|
166
194
|
// Store it in the temp store map
|
|
167
195
|
this._tempStores.set(queryset, store);
|
|
168
196
|
return store;
|
|
169
197
|
}
|
|
198
|
+
/**
|
|
199
|
+
* Function to return the root store for a queryset
|
|
200
|
+
*/
|
|
201
|
+
getRootStore(queryset) {
|
|
202
|
+
if (isNil(queryset)) {
|
|
203
|
+
throw new Error("QuerysetStoreRegistry.getRootStore requires a valid queryset");
|
|
204
|
+
}
|
|
205
|
+
const { isRoot, root } = this.querysetStoreGraph.findRoot(queryset);
|
|
206
|
+
const rootStore = this._stores.get(root);
|
|
207
|
+
if (!isRoot && rootStore) {
|
|
208
|
+
return { isRoot: false, rootStore: rootStore };
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
return { isRoot: true, rootStore: null };
|
|
212
|
+
}
|
|
213
|
+
}
|
|
170
214
|
/**
|
|
171
215
|
* Get the current state of the queryset, wrapped in a LiveQueryset
|
|
172
216
|
* @param {Object} queryset - The queryset
|
|
@@ -183,12 +227,14 @@ class QuerysetStoreRegistry {
|
|
|
183
227
|
// If we have a temporary store, promote it
|
|
184
228
|
if (this._tempStores.has(queryset)) {
|
|
185
229
|
store = this._tempStores.get(queryset);
|
|
230
|
+
store.isTemp = false; // Promote to permanent store
|
|
186
231
|
this._stores.set(semanticKey, store);
|
|
187
232
|
this.syncManager.followModel(this, queryset.ModelClass);
|
|
188
233
|
}
|
|
189
234
|
// Otherwise, ensure we have a permanent store
|
|
190
235
|
else if (!this._stores.has(semanticKey)) {
|
|
191
236
|
store = this.getStore(queryset, seed);
|
|
237
|
+
store.isTemp = false;
|
|
192
238
|
this._stores.set(semanticKey, store);
|
|
193
239
|
this.syncManager.followModel(this, queryset.ModelClass);
|
|
194
240
|
}
|
|
@@ -219,11 +265,13 @@ class QuerysetStoreRegistry {
|
|
|
219
265
|
// If we have a temp store, promote it
|
|
220
266
|
if (this._tempStores.has(queryset)) {
|
|
221
267
|
store = this._tempStores.get(queryset);
|
|
268
|
+
store.isTemp = false; // Promote to permanent store
|
|
222
269
|
this._stores.set(semanticKey, store);
|
|
223
270
|
}
|
|
224
271
|
else {
|
|
225
272
|
// Create a new permanent store
|
|
226
273
|
store = this.getStore(queryset);
|
|
274
|
+
store.isTemp = false;
|
|
227
275
|
this._stores.set(semanticKey, store);
|
|
228
276
|
}
|
|
229
277
|
}
|
|
@@ -7,6 +7,7 @@ export class ModelStore {
|
|
|
7
7
|
isSyncing: boolean;
|
|
8
8
|
pruneThreshold: any;
|
|
9
9
|
modelCache: Cache;
|
|
10
|
+
_lastRenderedData: Map<any, any>;
|
|
10
11
|
/**
|
|
11
12
|
* Load operations from data and add them to the operations map,
|
|
12
13
|
* reusing existing operations from the registry if they exist
|
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
import { Operation, Status, Type, operationRegistry } from './operation.js';
|
|
2
|
-
import { isNil, isEmpty, trim } from 'lodash-es';
|
|
2
|
+
import { isNil, isEmpty, trim, isEqual } from 'lodash-es';
|
|
3
3
|
import { modelEventEmitter } from './reactivity.js';
|
|
4
4
|
import { Cache } from '../cache/cache.js';
|
|
5
5
|
import { replaceTempPks, containsTempPk } from '../../flavours/django/tempPk.js';
|
|
6
|
-
const emitEvents = (
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
const emitEvents = (store, events) => {
|
|
7
|
+
if (!Array.isArray(events))
|
|
8
|
+
return;
|
|
9
|
+
events.forEach((event) => {
|
|
10
|
+
const pk = event.pk;
|
|
11
|
+
if (isNil(pk))
|
|
12
|
+
return;
|
|
13
|
+
const newRenderedDataArray = store.render([pk], true);
|
|
14
|
+
const newRenderedData = newRenderedDataArray.length > 0 ? newRenderedDataArray[0] : null;
|
|
15
|
+
const lastRenderedData = store._lastRenderedData.get(pk);
|
|
16
|
+
if (!isEqual(newRenderedData, lastRenderedData)) {
|
|
17
|
+
store._lastRenderedData.set(pk, newRenderedData);
|
|
18
|
+
modelEventEmitter.emit(`${store.modelClass.configKey}::${store.modelClass.modelName}::render`, event);
|
|
19
|
+
}
|
|
10
20
|
});
|
|
11
21
|
};
|
|
12
22
|
class EventData {
|
|
@@ -62,6 +72,7 @@ export class ModelStore {
|
|
|
62
72
|
this._loadOperations(initialOperations);
|
|
63
73
|
}
|
|
64
74
|
this.modelCache = new Cache('model-cache', {}, this.onHydrated.bind(this));
|
|
75
|
+
this._lastRenderedData = new Map();
|
|
65
76
|
}
|
|
66
77
|
/**
|
|
67
78
|
* Load operations from data and add them to the operations map,
|
|
@@ -164,26 +175,26 @@ export class ModelStore {
|
|
|
164
175
|
if (this.operationsMap.size > this.pruneThreshold) {
|
|
165
176
|
this.prune();
|
|
166
177
|
}
|
|
167
|
-
emitEvents(this
|
|
178
|
+
emitEvents(this, EventData.fromOperation(operation));
|
|
168
179
|
}
|
|
169
180
|
updateOperation(operation) {
|
|
170
181
|
if (!this.operationsMap.has(operation.operationId))
|
|
171
182
|
return false;
|
|
172
183
|
this.operationsMap.set(operation.operationId, operation);
|
|
173
|
-
emitEvents(this
|
|
184
|
+
emitEvents(this, EventData.fromOperation(operation));
|
|
174
185
|
return true;
|
|
175
186
|
}
|
|
176
187
|
confirm(operation) {
|
|
177
188
|
if (!this.operationsMap.has(operation.operationId))
|
|
178
189
|
return;
|
|
179
190
|
this.operationsMap.set(operation.operationId, operation);
|
|
180
|
-
emitEvents(this
|
|
191
|
+
emitEvents(this, EventData.fromOperation(operation));
|
|
181
192
|
}
|
|
182
193
|
reject(operation) {
|
|
183
194
|
if (!this.operationsMap.has(operation.operationId))
|
|
184
195
|
return;
|
|
185
196
|
this.operationsMap.set(operation.operationId, operation);
|
|
186
|
-
emitEvents(this
|
|
197
|
+
emitEvents(this, EventData.fromOperation(operation));
|
|
187
198
|
}
|
|
188
199
|
setOperations(operations = []) {
|
|
189
200
|
const prevOps = this.operations;
|
|
@@ -192,7 +203,7 @@ export class ModelStore {
|
|
|
192
203
|
this.operationsMap.set(op.operationId, op);
|
|
193
204
|
});
|
|
194
205
|
const allOps = [...prevOps, ...this.operations];
|
|
195
|
-
emitEvents(this
|
|
206
|
+
emitEvents(this, EventData.fromOperations(allOps));
|
|
196
207
|
}
|
|
197
208
|
// Ground truth data methods
|
|
198
209
|
setGroundTruth(groundTruth) {
|
|
@@ -200,7 +211,7 @@ export class ModelStore {
|
|
|
200
211
|
this.groundTruthArray = Array.isArray(groundTruth) ? groundTruth : [];
|
|
201
212
|
// reactivity - gather all ops
|
|
202
213
|
const allOps = [...prevGroundTruth, ...this.groundTruthArray];
|
|
203
|
-
emitEvents(this
|
|
214
|
+
emitEvents(this, EventData.fromInstances(allOps, this.modelClass));
|
|
204
215
|
}
|
|
205
216
|
getGroundTruth() {
|
|
206
217
|
return this.groundTruthArray;
|
|
@@ -263,7 +274,7 @@ export class ModelStore {
|
|
|
263
274
|
console.log(`[ModelStore ${this.modelClass.modelName}] Created CHECKPOINT operation for ${checkpointInstances.length} existing instances`);
|
|
264
275
|
}
|
|
265
276
|
// reactivity - use all the newly added instances (both new and updated)
|
|
266
|
-
emitEvents(this
|
|
277
|
+
emitEvents(this, EventData.fromInstances([...checkpointInstances, ...Array.from(pkMap.values())], this.modelClass));
|
|
267
278
|
}
|
|
268
279
|
_filteredOperations(pks, operations) {
|
|
269
280
|
if (!pks)
|
|
@@ -6,8 +6,16 @@ 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;
|
|
15
|
+
_lastRenderedPks: any[] | null;
|
|
16
|
+
renderCallbacks: Set<any>;
|
|
17
|
+
_rootUnregister: any;
|
|
18
|
+
_currentRootStore: any;
|
|
11
19
|
get cacheKey(): any;
|
|
12
20
|
onHydrated(hydratedData: any): void;
|
|
13
21
|
setCache(result: any): void;
|
|
@@ -25,7 +33,17 @@ export class QuerysetStore {
|
|
|
25
33
|
getTrimmedOperations(): any[];
|
|
26
34
|
getInflightOperations(): any[];
|
|
27
35
|
prune(): void;
|
|
36
|
+
registerRenderCallback(callback: any): () => boolean;
|
|
37
|
+
_ensureRootRegistration(): void;
|
|
38
|
+
/**
|
|
39
|
+
* Helper to validate PKs against the model store and apply local filtering/sorting.
|
|
40
|
+
* This is the core of the rendering logic.
|
|
41
|
+
* @private
|
|
42
|
+
*/
|
|
43
|
+
private _getValidatedAndFilteredPks;
|
|
28
44
|
render(optimistic?: boolean, fromCache?: boolean): any[];
|
|
45
|
+
renderFromRoot(optimistic: boolean | undefined, rootStore: any): any[];
|
|
46
|
+
renderFromData(optimistic?: boolean): any[];
|
|
29
47
|
applyOperation(operation: any, currentPks: any): any;
|
|
30
48
|
sync(): Promise<void>;
|
|
31
49
|
}
|