@statezero/core 0.1.38 → 0.1.41

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.
@@ -1,9 +1,8 @@
1
- import { computed, getCurrentInstance, onBeforeUnmount } from "vue";
2
- import { querysetStoreRegistry } from "../../syncEngine/registries/querysetStoreRegistry";
3
- import { metricRegistry } from "../../syncEngine/registries/metricRegistry";
4
- import { syncManager } from "../../syncEngine/sync";
5
- import { wrappedQuerysetCache } from "./reactivity.js";
6
- import { v7 } from "uuid";
1
+ import { computed, getCurrentInstance, onBeforeUnmount } from 'vue';
2
+ import { querysetStoreRegistry } from '../../syncEngine/registries/querysetStoreRegistry';
3
+ import { metricRegistry } from '../../syncEngine/registries/metricRegistry';
4
+ import { syncManager } from '../../syncEngine/sync';
5
+ import { v7 } from 'uuid';
7
6
  syncManager.followAllQuerysets = false;
8
7
  export const querysets = new Map(); // Map of composableId -> queryset
9
8
  function updateSyncManager() {
@@ -14,16 +13,11 @@ function updateSyncManager() {
14
13
  export function useQueryset(querysetFactory) {
15
14
  const instance = getCurrentInstance();
16
15
  if (!instance) {
17
- throw new Error("useQueryset must be called within a component setup function");
16
+ throw new Error('useQueryset must be called within a component setup function');
18
17
  }
19
18
  const composableId = v7();
20
19
  let lastQueryset = null;
21
20
  onBeforeUnmount(() => {
22
- // Clear cache when component unmounts
23
- if (lastQueryset?.semanticKey &&
24
- wrappedQuerysetCache.has(lastQueryset.semanticKey)) {
25
- wrappedQuerysetCache.delete(lastQueryset.semanticKey);
26
- }
27
21
  querysets.delete(composableId);
28
22
  updateSyncManager();
29
23
  });
@@ -33,12 +27,6 @@ export function useQueryset(querysetFactory) {
33
27
  const queryset = original?.queryset || original;
34
28
  // Only update if queryset actually changed
35
29
  if (lastQueryset !== queryset) {
36
- // Clear cache for previous queryset if it changed
37
- if (lastQueryset?.semanticKey &&
38
- lastQueryset !== queryset &&
39
- wrappedQuerysetCache.has(lastQueryset.semanticKey)) {
40
- wrappedQuerysetCache.delete(lastQueryset.semanticKey);
41
- }
42
30
  querysets.set(composableId, queryset);
43
31
  updateSyncManager();
44
32
  lastQueryset = queryset;
@@ -16,4 +16,3 @@ export function ModelAdaptor(modelInstance: Object, reactivityFn?: Function): an
16
16
  */
17
17
  export function QuerySetAdaptor(liveQuerySet: Object, reactivityFn?: Function): any | import("vue").Ref;
18
18
  export function MetricAdaptor(metric: any): any;
19
- export const wrappedQuerysetCache: Map<any, any>;
@@ -123,4 +123,3 @@ export function MetricAdaptor(metric) {
123
123
  }
124
124
  return wrapper;
125
125
  }
126
- export { wrappedQuerysetCache };
@@ -102,7 +102,7 @@ export class LiveQueryset {
102
102
  * @private
103
103
  * @returns {Array} The current items in the queryset
104
104
  */
105
- getCurrentItems(sortAndFilter = true) {
105
+ getCurrentItems() {
106
106
  const store = querysetStoreRegistry.getStore(__classPrivateFieldGet(this, _LiveQueryset_queryset, "f"));
107
107
  // Get the current primary keys from the store
108
108
  const pks = store.render();
@@ -113,9 +113,7 @@ export class LiveQueryset {
113
113
  const pkField = __classPrivateFieldGet(this, _LiveQueryset_ModelClass, "f").primaryKeyField;
114
114
  return __classPrivateFieldGet(this, _LiveQueryset_ModelClass, "f").fromPk(pk, __classPrivateFieldGet(this, _LiveQueryset_queryset, "f"));
115
115
  });
116
- if (!sortAndFilter)
117
- return instances;
118
- return filter(instances, __classPrivateFieldGet(this, _LiveQueryset_queryset, "f").build(), __classPrivateFieldGet(this, _LiveQueryset_ModelClass, "f"), true);
116
+ return instances;
119
117
  }
120
118
  }
121
119
  _LiveQueryset_queryset = new WeakMap(), _LiveQueryset_ModelClass = new WeakMap(), _LiveQueryset_proxy = new WeakMap(), _LiveQueryset_array = new WeakMap();
@@ -8,6 +8,8 @@ export class ModelStore {
8
8
  pruneThreshold: any;
9
9
  modelCache: Cache;
10
10
  _lastRenderedData: Map<any, any>;
11
+ renderCallbacks: Set<any>;
12
+ registerRenderCallback(callback: any): () => boolean;
11
13
  /**
12
14
  * Load operations from data and add them to the operations map,
13
15
  * reusing existing operations from the registry if they exist
@@ -16,6 +16,14 @@ const emitEvents = (store, events) => {
16
16
  if (!isEqual(newRenderedData, lastRenderedData)) {
17
17
  store._lastRenderedData.set(pk, newRenderedData);
18
18
  modelEventEmitter.emit(`${store.modelClass.configKey}::${store.modelClass.modelName}::render`, event);
19
+ store.renderCallbacks.forEach((callback) => {
20
+ try {
21
+ callback();
22
+ }
23
+ catch (error) {
24
+ console.warn("Error in model store render callback:", error);
25
+ }
26
+ });
19
27
  }
20
28
  });
21
29
  };
@@ -71,15 +79,20 @@ export class ModelStore {
71
79
  if (initialOperations && initialOperations.length > 0) {
72
80
  this._loadOperations(initialOperations);
73
81
  }
74
- this.modelCache = new Cache('model-cache', {}, this.onHydrated.bind(this));
82
+ this.modelCache = new Cache("model-cache", {}, this.onHydrated.bind(this));
75
83
  this._lastRenderedData = new Map();
84
+ this.renderCallbacks = new Set();
85
+ }
86
+ registerRenderCallback(callback) {
87
+ this.renderCallbacks.add(callback);
88
+ return () => this.renderCallbacks.delete(callback);
76
89
  }
77
90
  /**
78
91
  * Load operations from data and add them to the operations map,
79
92
  * reusing existing operations from the registry if they exist
80
93
  */
81
94
  _loadOperations(operationsData) {
82
- operationsData.forEach(opData => {
95
+ operationsData.forEach((opData) => {
83
96
  const existingOp = operationRegistry.get(opData.operationId);
84
97
  if (existingOp) {
85
98
  // If the operation exists in the registry, use it
@@ -109,7 +122,7 @@ export class ModelStore {
109
122
  let nonTempPkItems = [];
110
123
  result.forEach((item) => {
111
124
  let pk = item[pkField];
112
- if (typeof pk === 'string' && containsTempPk(pk)) {
125
+ if (typeof pk === "string" && containsTempPk(pk)) {
113
126
  pk = replaceTempPks(item[pkField]);
114
127
  if (isNil(pk) || isEmpty(trim(pk))) {
115
128
  return;
@@ -128,7 +141,7 @@ export class ModelStore {
128
141
  let nonTempPkItems = [];
129
142
  items.forEach((item) => {
130
143
  let pk = item[pkField];
131
- if (typeof pk === 'string' && containsTempPk(pk)) {
144
+ if (typeof pk === "string" && containsTempPk(pk)) {
132
145
  pk = replaceTempPks(item[pkField]);
133
146
  if (isNil(pk) || isEmpty(trim(pk))) {
134
147
  return;
@@ -145,16 +158,16 @@ export class ModelStore {
145
158
  // Otherwise, we're rendering specific items - update only those items
146
159
  const currentCache = this.modelCache.get(this.cacheKey) || [];
147
160
  // Filter out items that were requested but not in the result (they were deleted)
148
- const filteredCache = currentCache.filter(item => item &&
149
- typeof item === 'object' &&
161
+ const filteredCache = currentCache.filter((item) => item &&
162
+ typeof item === "object" &&
150
163
  pkField in item &&
151
164
  (!requestedPks.has(item[pkField]) ||
152
- nonTempPkItems.some(newItem => newItem[pkField] === item[pkField])));
165
+ nonTempPkItems.some((newItem) => newItem[pkField] === item[pkField])));
153
166
  // Create a map for faster lookups
154
- const cacheMap = new Map(filteredCache.map(item => [item[pkField], item]));
167
+ const cacheMap = new Map(filteredCache.map((item) => [item[pkField], item]));
155
168
  // Add or update items from the result
156
169
  for (const item of nonTempPkItems) {
157
- if (item && typeof item === 'object' && pkField in item) {
170
+ if (item && typeof item === "object" && pkField in item) {
158
171
  cacheMap.set(item[pkField], item);
159
172
  }
160
173
  }
@@ -199,7 +212,7 @@ export class ModelStore {
199
212
  setOperations(operations = []) {
200
213
  const prevOps = this.operations;
201
214
  this.operationsMap.clear();
202
- operations.forEach(op => {
215
+ operations.forEach((op) => {
203
216
  this.operationsMap.set(op.operationId, op);
204
217
  });
205
218
  const allOps = [...prevOps, ...this.operations];
@@ -219,16 +232,16 @@ export class ModelStore {
219
232
  get groundTruthPks() {
220
233
  const pk = this.pkField;
221
234
  return this.groundTruthArray
222
- .filter(instance => instance && typeof instance === 'object' && pk in instance)
223
- .map(instance => instance[pk]);
235
+ .filter((instance) => instance && typeof instance === "object" && pk in instance)
236
+ .map((instance) => instance[pk]);
224
237
  }
225
238
  addToGroundTruth(instances) {
226
239
  if (!Array.isArray(instances) || instances.length === 0)
227
240
  return;
228
241
  const pkField = this.pkField;
229
242
  const pkMap = new Map();
230
- instances.forEach(inst => {
231
- if (inst && typeof inst === 'object' && pkField in inst) {
243
+ instances.forEach((inst) => {
244
+ if (inst && typeof inst === "object" && pkField in inst) {
232
245
  pkMap.set(inst[pkField], inst);
233
246
  }
234
247
  else {
@@ -241,7 +254,9 @@ export class ModelStore {
241
254
  const processedPks = new Set();
242
255
  const checkpointInstances = []; // Track instances that need CHECKPOINT operations
243
256
  for (const existingItem of this.groundTruthArray) {
244
- if (!existingItem || typeof existingItem !== 'object' || !(pkField in existingItem)) {
257
+ if (!existingItem ||
258
+ typeof existingItem !== "object" ||
259
+ !(pkField in existingItem)) {
245
260
  continue;
246
261
  }
247
262
  const pk = existingItem[pkField];
@@ -263,12 +278,14 @@ export class ModelStore {
263
278
  // Create CHECKPOINT operation for instances that already existed
264
279
  if (checkpointInstances.length > 0) {
265
280
  const checkpointOperation = new Operation({
266
- operationId: `checkpoint_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
281
+ operationId: `checkpoint_${Date.now()}_${Math.random()
282
+ .toString(36)
283
+ .substr(2, 9)}`,
267
284
  type: Type.CHECKPOINT,
268
285
  instances: checkpointInstances,
269
286
  status: Status.CONFIRMED,
270
287
  timestamp: Date.now(),
271
- queryset: this.modelClass.objects.all()
288
+ queryset: this.modelClass.objects.all(),
272
289
  });
273
290
  this.operationsMap.set(checkpointOperation.operationId, checkpointOperation);
274
291
  console.log(`[ModelStore ${this.modelClass.modelName}] Created CHECKPOINT operation for ${checkpointInstances.length} existing instances`);
@@ -282,7 +299,7 @@ export class ModelStore {
282
299
  const pkField = this.pkField;
283
300
  let filteredOps = [];
284
301
  for (const op of operations) {
285
- let relevantInstances = op.instances.filter(instance => pks.has(instance[pkField] || instance));
302
+ let relevantInstances = op.instances.filter((instance) => pks.has(instance[pkField] || instance));
286
303
  if (relevantInstances.length > 0) {
287
304
  filteredOps.push({
288
305
  operationId: op.operationId,
@@ -291,7 +308,7 @@ export class ModelStore {
291
308
  queryset: op.queryset,
292
309
  type: op.type,
293
310
  status: op.status,
294
- args: op.args
311
+ args: op.args,
295
312
  });
296
313
  }
297
314
  }
@@ -301,7 +318,7 @@ export class ModelStore {
301
318
  const pkField = this.pkField;
302
319
  let groundTruthMap = new Map();
303
320
  for (const instance of groundTruthArray) {
304
- if (!instance || typeof instance !== 'object' || !(pkField in instance)) {
321
+ if (!instance || typeof instance !== "object" || !(pkField in instance)) {
305
322
  continue;
306
323
  }
307
324
  const pk = instance[pkField];
@@ -314,7 +331,7 @@ export class ModelStore {
314
331
  applyOperation(operation, currentInstances) {
315
332
  const pkField = this.pkField;
316
333
  for (const instance of operation.instances) {
317
- if (!instance || typeof instance !== 'object' || !(pkField in instance)) {
334
+ if (!instance || typeof instance !== "object" || !(pkField in instance)) {
318
335
  console.warn(`[ModelStore ${this.modelClass.modelName}] Skipping instance ${instance} in operation ${operation.operationId} during applyOperation due to missing PK field '${String(pkField)}' or invalid format.`);
319
336
  continue;
320
337
  }
@@ -333,9 +350,9 @@ export class ModelStore {
333
350
  currentInstances.set(pk, { ...existing, ...instance });
334
351
  }
335
352
  else {
336
- const wasDeletedLocally = this.operations.some(op => op.type === Type.DELETE &&
353
+ const wasDeletedLocally = this.operations.some((op) => op.type === Type.DELETE &&
337
354
  op.status !== Status.REJECTED &&
338
- op.instances.some(inst => inst && inst[pkField] === pk));
355
+ op.instances.some((inst) => inst && inst[pkField] === pk));
339
356
  if (!wasDeletedLocally) {
340
357
  currentInstances.set(pk, instance);
341
358
  }
@@ -354,10 +371,10 @@ export class ModelStore {
354
371
  }
355
372
  getTrimmedOperations() {
356
373
  const twoMinutesAgo = Date.now() - 1000 * 60 * 2;
357
- return this.operations.filter(operation => operation.timestamp > twoMinutesAgo);
374
+ return this.operations.filter((operation) => operation.timestamp > twoMinutesAgo);
358
375
  }
359
376
  getInflightOperations() {
360
- return this.operations.filter(operation => operation.status != Status.CONFIRMED &&
377
+ return this.operations.filter((operation) => operation.status != Status.CONFIRMED &&
361
378
  operation.status != Status.REJECTED);
362
379
  }
363
380
  // Pruning
@@ -369,12 +386,16 @@ export class ModelStore {
369
386
  }
370
387
  // Render methods
371
388
  render(pks = null, optimistic = true) {
372
- const pksSet = pks === null ? null :
373
- (pks instanceof Set ? pks : new Set(Array.isArray(pks) ? pks : [pks]));
389
+ const pksSet = pks === null
390
+ ? null
391
+ : pks instanceof Set
392
+ ? pks
393
+ : new Set(Array.isArray(pks) ? pks : [pks]);
374
394
  const renderedInstancesMap = this._filteredGroundTruth(pksSet, this.groundTruthArray);
375
395
  const relevantOperations = this._filteredOperations(pksSet, this.operations);
376
396
  for (const op of relevantOperations) {
377
- if (op.status !== Status.REJECTED && (optimistic || op.status === Status.CONFIRMED)) {
397
+ if (op.status !== Status.REJECTED &&
398
+ (optimistic || op.status === Status.CONFIRMED)) {
378
399
  this.applyOperation(op, renderedInstancesMap);
379
400
  }
380
401
  }
@@ -397,7 +418,10 @@ export class ModelStore {
397
418
  this.setOperations(trimmedOps);
398
419
  return;
399
420
  }
400
- const newGroundTruth = await this.fetchFn({ pks: currentPks, modelClass: this.modelClass });
421
+ const newGroundTruth = await this.fetchFn({
422
+ pks: currentPks,
423
+ modelClass: this.modelClass,
424
+ });
401
425
  if (pks) {
402
426
  this.addToGroundTruth(newGroundTruth);
403
427
  return;
@@ -35,38 +35,6 @@ function relatedQuerysets(queryset) {
35
35
  });
36
36
  return result;
37
37
  }
38
- /**
39
- * Returns querysets that contain any of the specified instances
40
- * @param {Operation} operation - The operation containing instances to check
41
- * @returns {Map<QuerySet, Store>}
42
- */
43
- function querysetsContainingInstances(operation) {
44
- const ModelClass = operation.queryset.ModelClass;
45
- const pkField = ModelClass.primaryKeyField;
46
- const instancePks = new Set(operation.instances
47
- .filter(instance => instance && typeof instance === 'object' && pkField in instance)
48
- .map(instance => instance[pkField]));
49
- if (instancePks.size === 0) {
50
- return new Map();
51
- }
52
- const result = new Map();
53
- Array.from(querysetStoreRegistry._stores.entries()).forEach(([queryset, store]) => {
54
- if (store.modelClass !== ModelClass)
55
- return;
56
- try {
57
- // Check if this queryset contains any of the instances
58
- const renderedPks = new Set(store.render(false)); // Get without optimistic updates
59
- const hasIntersection = [...instancePks].some(pk => renderedPks.has(pk));
60
- if (hasIntersection) {
61
- result.set(store.queryset, store);
62
- }
63
- }
64
- catch (e) {
65
- console.warn('Error checking queryset for instances', e);
66
- }
67
- });
68
- return result;
69
- }
70
38
  /**
71
39
  * Process an operation in the model store
72
40
  *
@@ -133,12 +101,12 @@ function processQuerysetStores(operation, actionType) {
133
101
  case Type.UPDATE_INSTANCE:
134
102
  case Type.DELETE:
135
103
  case Type.DELETE_INSTANCE:
136
- // For updates and deletes, only route to querysets that actually contain the instances
137
- querysetStoreMap = querysetsContainingInstances(operation);
104
+ // No need to do anything, the model will change the queryset local filtering will handle it
105
+ querysetStoreMap = new Map();
138
106
  break;
139
107
  case Type.CHECKPOINT:
140
- // For checkpoints, route to querysets that contain the instances
141
- querysetStoreMap = querysetsContainingInstances(operation);
108
+ // No need to do anything, the model will change the queryset local filtering will handle it
109
+ querysetStoreMap = new Map();
142
110
  break;
143
111
  default:
144
112
  // For other operation types, use the existing related querysets logic
@@ -16,6 +16,7 @@ export class QuerysetStore {
16
16
  renderCallbacks: Set<any>;
17
17
  _rootUnregister: any;
18
18
  _currentRootStore: any;
19
+ _modelStoreUnregister: any;
19
20
  get cacheKey(): any;
20
21
  onHydrated(hydratedData: any): void;
21
22
  setCache(result: any): void;
@@ -35,6 +36,12 @@ export class QuerysetStore {
35
36
  prune(): void;
36
37
  registerRenderCallback(callback: any): () => boolean;
37
38
  _ensureRootRegistration(): void;
39
+ /**
40
+ * Helper to validate PKs against the model store and apply local filtering/sorting.
41
+ * This is the core of the rendering logic.
42
+ * @private
43
+ */
44
+ private _getValidatedAndFilteredPks;
38
45
  render(optimistic?: boolean, fromCache?: boolean): any[];
39
46
  renderFromRoot(optimistic: boolean | undefined, rootStore: any): any[];
40
47
  renderFromData(optimistic?: boolean): any[];
@@ -34,6 +34,10 @@ export class QuerysetStore {
34
34
  this._rootUnregister = null;
35
35
  this._currentRootStore = null;
36
36
  this._ensureRootRegistration();
37
+ const modelStore = modelStoreRegistry.getStore(this.modelClass);
38
+ this._modelStoreUnregister = modelStore.registerRenderCallback(() => {
39
+ this._emitRenderEvent();
40
+ });
37
41
  }
38
42
  // Caching
39
43
  get cacheKey() {
@@ -170,6 +174,21 @@ export class QuerysetStore {
170
174
  // Update current root store reference (could be null now)
171
175
  this._currentRootStore = rootStore;
172
176
  }
177
+ /**
178
+ * Helper to validate PKs against the model store and apply local filtering/sorting.
179
+ * This is the core of the rendering logic.
180
+ * @private
181
+ */
182
+ _getValidatedAndFilteredPks(pks) {
183
+ // 1. Convert PKs to instances, filtering out any that are null (deleted).
184
+ const instances = Array.from(pks)
185
+ .map((pk) => this.modelClass.fromPk(pk, this.queryset))
186
+ .filter((instance) => modelStoreRegistry.getEntity(this.modelClass, instance.pk) !== null);
187
+ // 2. Apply the queryset's AST (filters, ordering) to the validated instances.
188
+ const ast = this.queryset.build();
189
+ const finalPks = filter(instances, ast, this.modelClass, false); // false = return PKs
190
+ return finalPks;
191
+ }
173
192
  render(optimistic = true, fromCache = false) {
174
193
  this._ensureRootRegistration();
175
194
  if (fromCache) {
@@ -178,18 +197,19 @@ export class QuerysetStore {
178
197
  return cachedResult;
179
198
  }
180
199
  }
181
- let result;
200
+ let pks;
182
201
  if (this.getRootStore &&
183
202
  typeof this.getRootStore === "function" &&
184
203
  !this.isTemp) {
185
204
  const { isRoot, rootStore } = this.getRootStore(this.queryset);
186
205
  if (!isRoot && rootStore) {
187
- result = this.renderFromRoot(optimistic, rootStore);
206
+ pks = this.renderFromRoot(optimistic, rootStore);
188
207
  }
189
208
  }
190
- if (isNil(result)) {
191
- result = this.renderFromData(optimistic);
209
+ if (isNil(pks)) {
210
+ pks = this.renderFromData(optimistic);
192
211
  }
212
+ let result = this._getValidatedAndFilteredPks(pks);
193
213
  let limit = this.queryset.build().serializerOptions?.limit;
194
214
  if (limit) {
195
215
  result = result.slice(0, limit);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@statezero/core",
3
- "version": "0.1.38",
3
+ "version": "0.1.41",
4
4
  "type": "module",
5
5
  "module": "ESNext",
6
6
  "description": "The type-safe frontend client for StateZero - connect directly to your backend models with zero boilerplate",