@statezero/core 0.1.2 → 0.1.3-9.2

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/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 === 'touch') {
44
+ if (prop === "touch") {
44
45
  return () => this.touch();
45
46
  }
46
- if (prop === 'serialize') {
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 === 'string' && ['forEach', 'map', 'filter', 'reduce', 'some', 'every', 'find'].includes(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 === 'length') {
66
+ else if (prop === "length") {
57
67
  return this.getCurrentItems().length;
58
68
  }
59
- else if (typeof prop === 'string' && !isNaN(parseInt(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(sortAndFilter = true) {
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.map(pk => {
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
- if (!sortAndFilter)
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
- let parentLiveQuerySet = this.getEntity(queryset.__parent);
160
- initialGroundTruthPks = filter(parentLiveQuerySet, ast, ModelClass, false);
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,9 @@ export class ModelStore {
7
7
  isSyncing: boolean;
8
8
  pruneThreshold: any;
9
9
  modelCache: Cache;
10
+ _lastRenderedData: Map<any, any>;
11
+ renderCallbacks: Set<any>;
12
+ registerRenderCallback(callback: any): () => boolean;
10
13
  /**
11
14
  * Load operations from data and add them to the operations map,
12
15
  * reusing existing operations from the registry if they exist