@statezero/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/dist/adaptors/react/composables.d.ts +1 -0
  2. package/dist/adaptors/react/composables.js +4 -0
  3. package/dist/adaptors/react/index.d.ts +1 -0
  4. package/dist/adaptors/react/index.js +1 -0
  5. package/dist/adaptors/vue/composables.d.ts +2 -0
  6. package/dist/adaptors/vue/composables.js +36 -0
  7. package/dist/adaptors/vue/index.d.ts +2 -0
  8. package/dist/adaptors/vue/index.js +2 -0
  9. package/dist/adaptors/vue/reactivity.d.ts +18 -0
  10. package/dist/adaptors/vue/reactivity.js +125 -0
  11. package/dist/cli/commands/syncModels.d.ts +132 -0
  12. package/dist/cli/commands/syncModels.js +1040 -0
  13. package/dist/cli/configFileLoader.d.ts +10 -0
  14. package/dist/cli/configFileLoader.js +85 -0
  15. package/dist/cli/index.d.ts +2 -0
  16. package/dist/cli/index.js +14 -0
  17. package/dist/config.d.ts +52 -0
  18. package/dist/config.js +242 -0
  19. package/dist/core/eventReceivers.d.ts +179 -0
  20. package/dist/core/eventReceivers.js +210 -0
  21. package/dist/core/utils.d.ts +8 -0
  22. package/dist/core/utils.js +62 -0
  23. package/dist/filtering/localFiltering.d.ts +116 -0
  24. package/dist/filtering/localFiltering.js +834 -0
  25. package/dist/flavours/django/dates.d.ts +33 -0
  26. package/dist/flavours/django/dates.js +99 -0
  27. package/dist/flavours/django/errors.d.ts +138 -0
  28. package/dist/flavours/django/errors.js +187 -0
  29. package/dist/flavours/django/f.d.ts +6 -0
  30. package/dist/flavours/django/f.js +91 -0
  31. package/dist/flavours/django/files.d.ts +76 -0
  32. package/dist/flavours/django/files.js +338 -0
  33. package/dist/flavours/django/makeApiCall.d.ts +20 -0
  34. package/dist/flavours/django/makeApiCall.js +169 -0
  35. package/dist/flavours/django/manager.d.ts +197 -0
  36. package/dist/flavours/django/manager.js +222 -0
  37. package/dist/flavours/django/model.d.ts +112 -0
  38. package/dist/flavours/django/model.js +253 -0
  39. package/dist/flavours/django/operationFactory.d.ts +65 -0
  40. package/dist/flavours/django/operationFactory.js +216 -0
  41. package/dist/flavours/django/q.d.ts +70 -0
  42. package/dist/flavours/django/q.js +43 -0
  43. package/dist/flavours/django/queryExecutor.d.ts +131 -0
  44. package/dist/flavours/django/queryExecutor.js +468 -0
  45. package/dist/flavours/django/querySet.d.ts +412 -0
  46. package/dist/flavours/django/querySet.js +601 -0
  47. package/dist/flavours/django/tempPk.d.ts +19 -0
  48. package/dist/flavours/django/tempPk.js +48 -0
  49. package/dist/flavours/django/utils.d.ts +19 -0
  50. package/dist/flavours/django/utils.js +29 -0
  51. package/dist/index.d.ts +38 -0
  52. package/dist/index.js +38 -0
  53. package/dist/react-entry.d.ts +2 -0
  54. package/dist/react-entry.js +2 -0
  55. package/dist/reactiveAdaptor.d.ts +24 -0
  56. package/dist/reactiveAdaptor.js +38 -0
  57. package/dist/setup.d.ts +15 -0
  58. package/dist/setup.js +22 -0
  59. package/dist/syncEngine/cache/cache.d.ts +75 -0
  60. package/dist/syncEngine/cache/cache.js +341 -0
  61. package/dist/syncEngine/metrics/metricOptCalcs.d.ts +79 -0
  62. package/dist/syncEngine/metrics/metricOptCalcs.js +284 -0
  63. package/dist/syncEngine/registries/metricRegistry.d.ts +53 -0
  64. package/dist/syncEngine/registries/metricRegistry.js +162 -0
  65. package/dist/syncEngine/registries/modelStoreRegistry.d.ts +11 -0
  66. package/dist/syncEngine/registries/modelStoreRegistry.js +56 -0
  67. package/dist/syncEngine/registries/querysetStoreRegistry.d.ts +55 -0
  68. package/dist/syncEngine/registries/querysetStoreRegistry.js +244 -0
  69. package/dist/syncEngine/stores/metricStore.d.ts +55 -0
  70. package/dist/syncEngine/stores/metricStore.js +222 -0
  71. package/dist/syncEngine/stores/modelStore.d.ts +40 -0
  72. package/dist/syncEngine/stores/modelStore.js +405 -0
  73. package/dist/syncEngine/stores/operation.d.ts +99 -0
  74. package/dist/syncEngine/stores/operation.js +224 -0
  75. package/dist/syncEngine/stores/operationEventHandlers.d.ts +8 -0
  76. package/dist/syncEngine/stores/operationEventHandlers.js +239 -0
  77. package/dist/syncEngine/stores/querysetStore.d.ts +32 -0
  78. package/dist/syncEngine/stores/querysetStore.js +200 -0
  79. package/dist/syncEngine/stores/reactivity.d.ts +3 -0
  80. package/dist/syncEngine/stores/reactivity.js +4 -0
  81. package/dist/syncEngine/stores/utils.d.ts +14 -0
  82. package/dist/syncEngine/stores/utils.js +32 -0
  83. package/dist/syncEngine/sync.d.ts +32 -0
  84. package/dist/syncEngine/sync.js +169 -0
  85. package/dist/vue-entry.d.ts +6 -0
  86. package/dist/vue-entry.js +2 -0
  87. package/license.md +116 -0
  88. package/package.json +123 -0
  89. package/readme.md +222 -0
@@ -0,0 +1,55 @@
1
+ /**
2
+ * A dynamic wrapper that always returns the latest queryset results
3
+ * This class proxies array operations to always reflect the current state
4
+ * of the underlying QuerysetStore.
5
+ */
6
+ export class LiveQueryset {
7
+ constructor(queryset: any);
8
+ /**
9
+ * Serializes the lqs as a simple array of objects, for freezing e.g in the metric stores
10
+ */
11
+ serialize(): any;
12
+ /**
13
+ * Get the current items from the store
14
+ * @private
15
+ * @returns {Array} The current items in the queryset
16
+ */
17
+ private getCurrentItems;
18
+ #private;
19
+ }
20
+ export const querysetStoreRegistry: QuerysetStoreRegistry;
21
+ declare class QuerysetStoreRegistry {
22
+ _stores: Map<any, any>;
23
+ _tempStores: WeakMap<object, any>;
24
+ followingQuerysets: Map<any, any>;
25
+ syncManager: () => void;
26
+ clear(): void;
27
+ setSyncManager(syncManager: any): void;
28
+ /**
29
+ * Add a queryset to the following set for a semantic key
30
+ */
31
+ addFollowingQueryset(semanticKey: any, queryset: any): void;
32
+ getStore(queryset: any, seed?: boolean): any;
33
+ /**
34
+ * Get the current state of the queryset, wrapped in a LiveQueryset
35
+ * @param {Object} queryset - The queryset
36
+ * @param {Boolean} seed - Should we optimistically seed the queryset with relevant items from the parent?
37
+ * @param {Boolean} sync - Schedule a sync of the queryset with the backend
38
+ * @returns {LiveQueryset} - A live view of the queryset
39
+ */
40
+ getEntity(queryset: Object, seed?: boolean, sync?: boolean): LiveQueryset;
41
+ /**
42
+ * Set ground truth for a queryset
43
+ * @param {Object} queryset - The queryset
44
+ * @param {Array} instances - Array of instances to set as ground truth
45
+ * @returns {Array} - The set instances
46
+ */
47
+ setEntity(queryset: Object, instances: any[]): any[];
48
+ /**
49
+ * Get all queryset stores for a specific model class
50
+ * @param {ModelClass} ModelClass - The model class to get stores for
51
+ * @returns {Array} - Array of queryset stores for this model
52
+ */
53
+ getAllStoresForModel(ModelClass: any): any[];
54
+ }
55
+ export {};
@@ -0,0 +1,244 @@
1
+ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
2
+ if (kind === "m") throw new TypeError("Private method is not writable");
3
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
4
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
5
+ return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
6
+ };
7
+ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
8
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
9
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
10
+ return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
11
+ };
12
+ var _LiveQueryset_queryset, _LiveQueryset_ModelClass, _LiveQueryset_proxy, _LiveQueryset_array;
13
+ import { QuerysetStore } from '../stores/querysetStore.js';
14
+ import { modelStoreRegistry } from '../registries/modelStoreRegistry.js';
15
+ import { wrapReactiveQuerySet } from '../../reactiveAdaptor.js';
16
+ import { processQuery, getRequiredFields, pickRequiredFields } from '../../filtering/localFiltering.js';
17
+ import { filter } from '../../filtering/localFiltering.js';
18
+ import { makeApiCall } from '../../flavours/django/makeApiCall.js';
19
+ import { isNil, pick } from 'lodash-es';
20
+ import hash from 'object-hash';
21
+ import { Operation } from '../stores/operation.js';
22
+ import { Cache } from '../cache/cache.js';
23
+ /**
24
+ * A dynamic wrapper that always returns the latest queryset results
25
+ * This class proxies array operations to always reflect the current state
26
+ * of the underlying QuerysetStore.
27
+ */
28
+ export class LiveQueryset {
29
+ constructor(queryset) {
30
+ _LiveQueryset_queryset.set(this, void 0);
31
+ _LiveQueryset_ModelClass.set(this, void 0);
32
+ _LiveQueryset_proxy.set(this, void 0);
33
+ _LiveQueryset_array.set(this, []);
34
+ // used internally
35
+ __classPrivateFieldSet(this, _LiveQueryset_queryset, queryset, "f");
36
+ __classPrivateFieldSet(this, _LiveQueryset_ModelClass, queryset.ModelClass, "f");
37
+ __classPrivateFieldGet(this, _LiveQueryset_array, "f").queryset = queryset;
38
+ __classPrivateFieldGet(this, _LiveQueryset_array, "f").ModelClass = queryset.ModelClass;
39
+ // Create a proxy that intercepts all array access
40
+ __classPrivateFieldSet(this, _LiveQueryset_proxy, new Proxy(__classPrivateFieldGet(this, _LiveQueryset_array, "f"), {
41
+ get: (target, prop, receiver) => {
42
+ // Expose the touch method through the proxy
43
+ if (prop === 'touch') {
44
+ return () => this.touch();
45
+ }
46
+ if (prop === 'serialize') {
47
+ return () => this.serialize();
48
+ }
49
+ // Special handling for iterators and common array methods
50
+ if (prop === Symbol.iterator) {
51
+ return () => this.getCurrentItems()[Symbol.iterator]();
52
+ }
53
+ else if (typeof prop === 'string' && ['forEach', 'map', 'filter', 'reduce', 'some', 'every', 'find'].includes(prop)) {
54
+ return (...args) => this.getCurrentItems()[prop](...args);
55
+ }
56
+ else if (prop === 'length') {
57
+ return this.getCurrentItems().length;
58
+ }
59
+ else if (typeof prop === 'string' && !isNaN(parseInt(prop))) {
60
+ // Handle numeric indices
61
+ return this.getCurrentItems()[prop];
62
+ }
63
+ return target[prop];
64
+ }
65
+ }), "f");
66
+ return __classPrivateFieldGet(this, _LiveQueryset_proxy, "f");
67
+ }
68
+ /**
69
+ * Serializes the lqs as a simple array of objects, for freezing e.g in the metric stores
70
+ */
71
+ serialize() {
72
+ const store = querysetStoreRegistry.getStore(__classPrivateFieldGet(this, _LiveQueryset_queryset, "f"));
73
+ // Get the current primary keys from the store
74
+ const pks = store.render();
75
+ // Map primary keys to full model objects
76
+ return pks.map(pk => {
77
+ // Get the full model instance from the model store
78
+ const pkField = __classPrivateFieldGet(this, _LiveQueryset_ModelClass, "f").primaryKeyField;
79
+ return __classPrivateFieldGet(this, _LiveQueryset_ModelClass, "f").fromPk(pk, __classPrivateFieldGet(this, _LiveQueryset_queryset, "f")).serialize();
80
+ });
81
+ }
82
+ /**
83
+ * Get the current items from the store
84
+ * @private
85
+ * @returns {Array} The current items in the queryset
86
+ */
87
+ getCurrentItems(sortAndFilter = true) {
88
+ const store = querysetStoreRegistry.getStore(__classPrivateFieldGet(this, _LiveQueryset_queryset, "f"));
89
+ // Get the current primary keys from the store
90
+ const pks = store.render();
91
+ // Map primary keys to full model objects
92
+ const instances = pks.map(pk => {
93
+ // Get the full model instance from the model store
94
+ const pkField = __classPrivateFieldGet(this, _LiveQueryset_ModelClass, "f").primaryKeyField;
95
+ return __classPrivateFieldGet(this, _LiveQueryset_ModelClass, "f").fromPk(pk, __classPrivateFieldGet(this, _LiveQueryset_queryset, "f"));
96
+ });
97
+ if (!sortAndFilter)
98
+ return instances;
99
+ return filter(instances, __classPrivateFieldGet(this, _LiveQueryset_queryset, "f").build(), __classPrivateFieldGet(this, _LiveQueryset_ModelClass, "f"), true);
100
+ }
101
+ }
102
+ _LiveQueryset_queryset = new WeakMap(), _LiveQueryset_ModelClass = new WeakMap(), _LiveQueryset_proxy = new WeakMap(), _LiveQueryset_array = new WeakMap();
103
+ class QuerysetStoreRegistry {
104
+ constructor() {
105
+ this._stores = new Map(); // Map<semanticKey, Store>
106
+ this._tempStores = new WeakMap(); // WeakMap<Queryset, Store>
107
+ this.followingQuerysets = new Map(); // Map<semanticKey, Set<queryset>>
108
+ this.syncManager = () => { console.warn("SyncManager not set for QuerysetStoreRegistry"); };
109
+ }
110
+ clear() {
111
+ this._stores.forEach((store) => {
112
+ this.syncManager.unfollowModel(this, store.modelClass);
113
+ });
114
+ this._stores = new Map();
115
+ this.followingQuerysets = new Map();
116
+ }
117
+ setSyncManager(syncManager) {
118
+ this.syncManager = syncManager;
119
+ }
120
+ /**
121
+ * Add a queryset to the following set for a semantic key
122
+ */
123
+ addFollowingQueryset(semanticKey, queryset) {
124
+ if (!this.followingQuerysets.has(semanticKey)) {
125
+ this.followingQuerysets.set(semanticKey, new Set());
126
+ }
127
+ this.followingQuerysets.get(semanticKey).add(queryset);
128
+ }
129
+ getStore(queryset, seed = false) {
130
+ if (isNil(queryset) || isNil(queryset.ModelClass)) {
131
+ throw new Error("QuerysetStoreRegistry.getStore requires a valid queryset");
132
+ }
133
+ // Check if we already have a temporary store for this exact QuerySet instance
134
+ if (this._tempStores.has(queryset)) {
135
+ return this._tempStores.get(queryset);
136
+ }
137
+ // Get the semanticKey
138
+ const semanticKey = queryset.semanticKey;
139
+ // Check if we have a permanent store with this semanticKey
140
+ if (this._stores.has(semanticKey)) {
141
+ this.addFollowingQueryset(semanticKey, queryset);
142
+ return this._stores.get(semanticKey);
143
+ }
144
+ // Create a new temporary store
145
+ const fetchQueryset = async ({ ast, modelClass }) => {
146
+ // Directly assemble the request and call the API to avoid recursive logic from the
147
+ // queryset back to the registry / store
148
+ const payload = {
149
+ ...ast,
150
+ type: 'list'
151
+ };
152
+ const response = await makeApiCall(queryset, 'list', payload);
153
+ return response.data;
154
+ };
155
+ let initialGroundTruthPks = null;
156
+ let ast = queryset.build();
157
+ let ModelClass = queryset.ModelClass;
158
+ if (queryset.__parent && seed) {
159
+ let parentLiveQuerySet = this.getEntity(queryset.__parent);
160
+ initialGroundTruthPks = filter(parentLiveQuerySet, ast, ModelClass, false);
161
+ }
162
+ // Get the parent registry
163
+ const store = new QuerysetStore(ModelClass, fetchQueryset, queryset, initialGroundTruthPks, // Initial ground truth PKs
164
+ null // Initial operations
165
+ );
166
+ // Store it in the temp store map
167
+ this._tempStores.set(queryset, store);
168
+ return store;
169
+ }
170
+ /**
171
+ * Get the current state of the queryset, wrapped in a LiveQueryset
172
+ * @param {Object} queryset - The queryset
173
+ * @param {Boolean} seed - Should we optimistically seed the queryset with relevant items from the parent?
174
+ * @param {Boolean} sync - Schedule a sync of the queryset with the backend
175
+ * @returns {LiveQueryset} - A live view of the queryset
176
+ */
177
+ getEntity(queryset, seed = true, sync = false) {
178
+ if (isNil(queryset))
179
+ throw new Error(`qsStoreRegistry: getEntity cannot be called without a queryset`);
180
+ const semanticKey = queryset.semanticKey;
181
+ this.addFollowingQueryset(semanticKey, queryset);
182
+ let store;
183
+ // If we have a temporary store, promote it
184
+ if (this._tempStores.has(queryset)) {
185
+ store = this._tempStores.get(queryset);
186
+ this._stores.set(semanticKey, store);
187
+ this.syncManager.followModel(this, queryset.ModelClass);
188
+ }
189
+ // Otherwise, ensure we have a permanent store
190
+ else if (!this._stores.has(semanticKey)) {
191
+ store = this.getStore(queryset, seed);
192
+ this._stores.set(semanticKey, store);
193
+ this.syncManager.followModel(this, queryset.ModelClass);
194
+ }
195
+ else {
196
+ store = this._stores.get(semanticKey);
197
+ }
198
+ const liveQueryset = new LiveQueryset(queryset);
199
+ if (sync)
200
+ store.sync();
201
+ return wrapReactiveQuerySet(liveQueryset);
202
+ }
203
+ /**
204
+ * Set ground truth for a queryset
205
+ * @param {Object} queryset - The queryset
206
+ * @param {Array} instances - Array of instances to set as ground truth
207
+ * @returns {Array} - The set instances
208
+ */
209
+ setEntity(queryset, instances) {
210
+ if (isNil(queryset) || isNil(instances))
211
+ return [];
212
+ const semanticKey = queryset.semanticKey;
213
+ this.addFollowingQueryset(semanticKey, queryset);
214
+ let store;
215
+ if (this._stores.has(semanticKey)) {
216
+ store = this._stores.get(semanticKey);
217
+ }
218
+ else {
219
+ // If we have a temp store, promote it
220
+ if (this._tempStores.has(queryset)) {
221
+ store = this._tempStores.get(queryset);
222
+ this._stores.set(semanticKey, store);
223
+ }
224
+ else {
225
+ // Create a new permanent store
226
+ store = this.getStore(queryset);
227
+ this._stores.set(semanticKey, store);
228
+ }
229
+ }
230
+ store.setGroundTruth(instances.map(instance => instance[queryset.ModelClass.primaryKeyField] || instance));
231
+ return instances;
232
+ }
233
+ /**
234
+ * Get all queryset stores for a specific model class
235
+ * @param {ModelClass} ModelClass - The model class to get stores for
236
+ * @returns {Array} - Array of queryset stores for this model
237
+ */
238
+ getAllStoresForModel(ModelClass) {
239
+ if (!ModelClass)
240
+ return [];
241
+ return Array.from(this._stores.values()).filter(store => store.modelClass === ModelClass);
242
+ }
243
+ }
244
+ export const querysetStoreRegistry = new QuerysetStoreRegistry();
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Store for managing a single metric with optimistic updates
3
+ */
4
+ export class MetricStore {
5
+ constructor(metricType: any, modelClass: any, queryset: any, field: null | undefined, ast: null | undefined, fetchFn: any);
6
+ metricType: any;
7
+ modelClass: any;
8
+ queryset: any;
9
+ field: any;
10
+ ast: any;
11
+ fetchFn: any;
12
+ groundTruthValue: any;
13
+ isSyncing: boolean;
14
+ strategy: import("../metrics/metricOptCalcs").MetricCalculationStrategy;
15
+ operations: any[];
16
+ confirmedOps: Set<any>;
17
+ metricCache: Cache;
18
+ _lastCalculatedValue: any;
19
+ reset(): void;
20
+ get cacheKey(): string;
21
+ /**
22
+ * Add an operation to this metric store
23
+ * @param {Operation} operation - The operation to add
24
+ */
25
+ addOperation(operation: Operation): void;
26
+ /**
27
+ * Update an operation in this metric store
28
+ * @param {Operation} operation - The operation to update
29
+ */
30
+ updateOperation(operation: Operation): void;
31
+ /**
32
+ * Confirm an operation in this metric store
33
+ * @param {Operation} operation - The operation to confirm
34
+ */
35
+ confirm(operation: Operation): void;
36
+ /**
37
+ * Reject an operation in this metric store
38
+ * @param {Operation} operation - The operation to reject
39
+ */
40
+ reject(operation: Operation): void;
41
+ onHydrated(): void;
42
+ setCache(): void;
43
+ clearCache(): void;
44
+ setGroundTruth(value: any): void;
45
+ /**
46
+ * Render the metric with current operations
47
+ * @returns {any} Calculated metric value
48
+ */
49
+ render(): any;
50
+ /**
51
+ * Sync metric with server
52
+ */
53
+ sync(): Promise<any>;
54
+ }
55
+ import { Cache } from '../cache/cache.js';
@@ -0,0 +1,222 @@
1
+ import { Cache } from '../cache/cache.js';
2
+ import { MetricStrategyFactory } from "../metrics/metricOptCalcs";
3
+ import hash from 'object-hash';
4
+ import { isNil, isEmpty, isEqual } from 'lodash-es';
5
+ import { metricEventEmitter } from './reactivity.js';
6
+ import { Status } from './operation.js';
7
+ /**
8
+ * Store for managing a single metric with optimistic updates
9
+ */
10
+ export class MetricStore {
11
+ constructor(metricType, modelClass, queryset, field = null, ast = null, fetchFn) {
12
+ this.metricType = metricType;
13
+ this.modelClass = modelClass;
14
+ this.queryset = queryset;
15
+ this.field = field;
16
+ this.ast = ast;
17
+ this.fetchFn = fetchFn;
18
+ this.groundTruthValue = null;
19
+ this.isSyncing = false;
20
+ this.strategy = MetricStrategyFactory.getStrategy(metricType, modelClass);
21
+ // Store operations related to this metric
22
+ this.operations = [];
23
+ // Keep track of which operations have been confirmed
24
+ this.confirmedOps = new Set();
25
+ // Initialize cache with AST-specific key
26
+ this.metricCache = new Cache("metric-store-cache", {}, this.onHydrated.bind(this));
27
+ // Initialize _lastCalculatedValue
28
+ this._lastCalculatedValue = null;
29
+ }
30
+ reset() {
31
+ this.groundTruthValue = null;
32
+ this._lastCalculatedValue = null;
33
+ this.operations = [];
34
+ this.confirmedOps = new Set();
35
+ this.isSyncing = false;
36
+ this.clearCache();
37
+ }
38
+ get cacheKey() {
39
+ return `${this.modelClass.configKey}::${this.modelClass.modelName}::metric::${this.metricType}::${this.field || "null"}::${this.ast ? hash(this.ast) : "global"}`;
40
+ }
41
+ /**
42
+ * Add an operation to this metric store
43
+ * @param {Operation} operation - The operation to add
44
+ */
45
+ addOperation(operation) {
46
+ // Only track operations for this model
47
+ if (operation.queryset.ModelClass !== this.modelClass) {
48
+ return;
49
+ }
50
+ // Check if operation already exists to avoid duplicates
51
+ const existingIndex = this.operations.findIndex((op) => op.operationId === operation.operationId);
52
+ if (existingIndex !== -1) {
53
+ // Update existing operation instead of adding a duplicate
54
+ this.operations[existingIndex] = operation;
55
+ }
56
+ else {
57
+ // Add to our operations list
58
+ this.operations.push(operation);
59
+ }
60
+ // Trigger a render to update the metric value
61
+ if (!isNil(this.groundTruthValue)) {
62
+ this.render();
63
+ }
64
+ }
65
+ /**
66
+ * Update an operation in this metric store
67
+ * @param {Operation} operation - The operation to update
68
+ */
69
+ updateOperation(operation) {
70
+ // Only track operations for this model
71
+ if (operation.queryset.ModelClass !== this.modelClass) {
72
+ return;
73
+ }
74
+ // Find and update the operation - use operationId not id
75
+ const index = this.operations.findIndex((op) => op.operationId === operation.operationId);
76
+ if (index !== -1) {
77
+ this.operations[index] = operation;
78
+ }
79
+ else {
80
+ // If not found, add it
81
+ this.operations.push(operation);
82
+ }
83
+ // Trigger a render to update the metric value
84
+ if (!isNil(this.groundTruthValue)) {
85
+ this.render();
86
+ }
87
+ }
88
+ /**
89
+ * Confirm an operation in this metric store
90
+ * @param {Operation} operation - The operation to confirm
91
+ */
92
+ confirm(operation) {
93
+ // Only track operations for this model
94
+ if (operation.queryset.ModelClass !== this.modelClass) {
95
+ return;
96
+ }
97
+ // Update operation status in our list and mark as confirmed - use operationId not id
98
+ const index = this.operations.findIndex((op) => op.operationId === operation.operationId);
99
+ if (index !== -1) {
100
+ this.operations[index] = operation;
101
+ this.confirmedOps.add(operation.operationId);
102
+ }
103
+ // Trigger a sync to update ground truth
104
+ this.render();
105
+ }
106
+ /**
107
+ * Reject an operation in this metric store
108
+ * @param {Operation} operation - The operation to reject
109
+ */
110
+ reject(operation) {
111
+ // Only track operations for this model
112
+ if (operation.queryset.ModelClass !== this.modelClass) {
113
+ return;
114
+ }
115
+ // Remove the operation as it's now rejected - use operationId not id
116
+ this.operations = this.operations.filter((op) => op.operationId !== operation.operationId);
117
+ this.confirmedOps.delete(operation.operationId);
118
+ // Trigger a render to update the metric value
119
+ if (!isNil(this.groundTruthValue)) {
120
+ this.render();
121
+ }
122
+ }
123
+ onHydrated() {
124
+ if (this.groundTruthValue === null) {
125
+ const cached = this.metricCache.get(this.cacheKey);
126
+ if (!isNil(cached) && !isEmpty(cached)) {
127
+ console.log(`[MetricStore] Hydrated ${this.metricType} metric for ${this.modelClass.modelName} from cache`);
128
+ this.setGroundTruth(cached?.value);
129
+ }
130
+ }
131
+ }
132
+ setCache() {
133
+ let groundTruthValue = this.groundTruthValue;
134
+ let serializedGroundTruth = groundTruthValue?.value
135
+ ? groundTruthValue.value
136
+ : groundTruthValue;
137
+ this.metricCache.set(this.cacheKey, {
138
+ value: serializedGroundTruth,
139
+ });
140
+ }
141
+ clearCache() {
142
+ this.metricCache.delete(this.cacheKey);
143
+ }
144
+ setGroundTruth(value) {
145
+ // Check if the value has actually changed
146
+ const valueChanged = !isEqual(this.groundTruthValue, value);
147
+ this.groundTruthValue = value;
148
+ this.setCache();
149
+ // Only emit event if the value changed
150
+ if (valueChanged) {
151
+ metricEventEmitter.emit("metric::render", {
152
+ metricType: this.metricType,
153
+ ModelClass: this.modelClass,
154
+ field: this.field,
155
+ ast: hash(this.ast),
156
+ valueChanged: true,
157
+ });
158
+ }
159
+ }
160
+ /**
161
+ * Render the metric with current operations
162
+ * @returns {any} Calculated metric value
163
+ */
164
+ render() {
165
+ // Check if ground truth value is null
166
+ if (isNil(this.groundTruthValue)) {
167
+ console.log(`groundTruthValue is null, returning null`);
168
+ return null;
169
+ }
170
+ // Calculate the new value using the operations-based approach
171
+ const newValue = this.strategy.calculateWithOperations(this.groundTruthValue, this.operations, this.field, this.modelClass);
172
+ // Check if the value has actually changed
173
+ if (!isEqual(this._lastCalculatedValue, newValue)) {
174
+ this._lastCalculatedValue = newValue;
175
+ // Only emit event if the value changed
176
+ metricEventEmitter.emit("metric::render", {
177
+ metricType: this.metricType,
178
+ ModelClass: this.modelClass,
179
+ field: this.field,
180
+ ast: hash(this.ast),
181
+ valueChanged: true,
182
+ });
183
+ }
184
+ return newValue;
185
+ }
186
+ /**
187
+ * Sync metric with server
188
+ */
189
+ async sync() {
190
+ if (this.isSyncing) {
191
+ console.warn(`[MetricStore] Already syncing ${this.metricType} for ${this.modelClass.modelName}`);
192
+ return;
193
+ }
194
+ this.isSyncing = true;
195
+ try {
196
+ console.log(`[MetricStore] Syncing ${this.metricType} metric for ${this.modelClass.modelName}`);
197
+ // Use fetchFn to get server metric value
198
+ const result = await this.fetchFn({
199
+ metricType: this.metricType,
200
+ modelClass: this.modelClass,
201
+ field: this.field,
202
+ ast: this.ast,
203
+ });
204
+ // After syncing, remove all confirmed operations
205
+ if (this.confirmedOps.size > 0) {
206
+ this.operations = this.operations.filter((op) => !this.confirmedOps.has(op.operationId));
207
+ this.confirmedOps.clear();
208
+ }
209
+ // Update ground truth
210
+ this.setGroundTruth(result);
211
+ console.log(`[MetricStore] Synced ${this.metricType} metric with value:`, result);
212
+ return result;
213
+ }
214
+ catch (error) {
215
+ console.error(`[MetricStore] Failed to sync ${this.metricType} metric:`, error);
216
+ return null;
217
+ }
218
+ finally {
219
+ this.isSyncing = false;
220
+ }
221
+ }
222
+ }
@@ -0,0 +1,40 @@
1
+ export class ModelStore {
2
+ constructor(modelClass: any, fetchFn: any, initialGroundTruth?: null, initialOperations?: null, options?: {});
3
+ modelClass: any;
4
+ fetchFn: any;
5
+ groundTruthArray: never[];
6
+ operationsMap: Map<any, any>;
7
+ isSyncing: boolean;
8
+ pruneThreshold: any;
9
+ modelCache: Cache;
10
+ /**
11
+ * Load operations from data and add them to the operations map,
12
+ * reusing existing operations from the registry if they exist
13
+ */
14
+ _loadOperations(operationsData: any): void;
15
+ get cacheKey(): string;
16
+ onHydrated(): void;
17
+ setCache(result: any): void;
18
+ clearCache(): void;
19
+ updateCache(items: any, requestedPks: any): void;
20
+ get operations(): any[];
21
+ get pkField(): any;
22
+ addOperation(operation: any): void;
23
+ updateOperation(operation: any): boolean;
24
+ confirm(operation: any): void;
25
+ reject(operation: any): void;
26
+ setOperations(operations?: any[]): void;
27
+ setGroundTruth(groundTruth: any): void;
28
+ getGroundTruth(): never[];
29
+ get groundTruthPks(): never[];
30
+ addToGroundTruth(instances: any): void;
31
+ _filteredOperations(pks: any, operations: any): any;
32
+ _filteredGroundTruth(pks: any, groundTruthArray: any): Map<any, any>;
33
+ applyOperation(operation: any, currentInstances: any): any;
34
+ getTrimmedOperations(): any[];
35
+ getInflightOperations(): any[];
36
+ prune(): void;
37
+ render(pks?: null, optimistic?: boolean): any[];
38
+ sync(pks?: null): Promise<void>;
39
+ }
40
+ import { Cache } from '../cache/cache.js';