@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.
@@ -1,12 +1,30 @@
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 = (modelClass, events) => {
7
- // helper method to emit every event
8
- events.forEach(event => {
9
- modelEventEmitter.emit(`${modelClass.configKey}::${modelClass.modelName}::render`, event);
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
+ store.renderCallbacks.forEach((callback) => {
20
+ try {
21
+ callback();
22
+ }
23
+ catch (error) {
24
+ console.warn("Error in model store render callback:", error);
25
+ }
26
+ });
27
+ }
10
28
  });
11
29
  };
12
30
  class EventData {
@@ -61,14 +79,20 @@ export class ModelStore {
61
79
  if (initialOperations && initialOperations.length > 0) {
62
80
  this._loadOperations(initialOperations);
63
81
  }
64
- this.modelCache = new Cache('model-cache', {}, this.onHydrated.bind(this));
82
+ this.modelCache = new Cache("model-cache", {}, this.onHydrated.bind(this));
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);
65
89
  }
66
90
  /**
67
91
  * Load operations from data and add them to the operations map,
68
92
  * reusing existing operations from the registry if they exist
69
93
  */
70
94
  _loadOperations(operationsData) {
71
- operationsData.forEach(opData => {
95
+ operationsData.forEach((opData) => {
72
96
  const existingOp = operationRegistry.get(opData.operationId);
73
97
  if (existingOp) {
74
98
  // If the operation exists in the registry, use it
@@ -98,7 +122,7 @@ export class ModelStore {
98
122
  let nonTempPkItems = [];
99
123
  result.forEach((item) => {
100
124
  let pk = item[pkField];
101
- if (typeof pk === 'string' && containsTempPk(pk)) {
125
+ if (typeof pk === "string" && containsTempPk(pk)) {
102
126
  pk = replaceTempPks(item[pkField]);
103
127
  if (isNil(pk) || isEmpty(trim(pk))) {
104
128
  return;
@@ -117,7 +141,7 @@ export class ModelStore {
117
141
  let nonTempPkItems = [];
118
142
  items.forEach((item) => {
119
143
  let pk = item[pkField];
120
- if (typeof pk === 'string' && containsTempPk(pk)) {
144
+ if (typeof pk === "string" && containsTempPk(pk)) {
121
145
  pk = replaceTempPks(item[pkField]);
122
146
  if (isNil(pk) || isEmpty(trim(pk))) {
123
147
  return;
@@ -134,16 +158,16 @@ export class ModelStore {
134
158
  // Otherwise, we're rendering specific items - update only those items
135
159
  const currentCache = this.modelCache.get(this.cacheKey) || [];
136
160
  // Filter out items that were requested but not in the result (they were deleted)
137
- const filteredCache = currentCache.filter(item => item &&
138
- typeof item === 'object' &&
161
+ const filteredCache = currentCache.filter((item) => item &&
162
+ typeof item === "object" &&
139
163
  pkField in item &&
140
164
  (!requestedPks.has(item[pkField]) ||
141
- nonTempPkItems.some(newItem => newItem[pkField] === item[pkField])));
165
+ nonTempPkItems.some((newItem) => newItem[pkField] === item[pkField])));
142
166
  // Create a map for faster lookups
143
- const cacheMap = new Map(filteredCache.map(item => [item[pkField], item]));
167
+ const cacheMap = new Map(filteredCache.map((item) => [item[pkField], item]));
144
168
  // Add or update items from the result
145
169
  for (const item of nonTempPkItems) {
146
- if (item && typeof item === 'object' && pkField in item) {
170
+ if (item && typeof item === "object" && pkField in item) {
147
171
  cacheMap.set(item[pkField], item);
148
172
  }
149
173
  }
@@ -164,35 +188,35 @@ export class ModelStore {
164
188
  if (this.operationsMap.size > this.pruneThreshold) {
165
189
  this.prune();
166
190
  }
167
- emitEvents(this.modelClass, EventData.fromOperation(operation));
191
+ emitEvents(this, EventData.fromOperation(operation));
168
192
  }
169
193
  updateOperation(operation) {
170
194
  if (!this.operationsMap.has(operation.operationId))
171
195
  return false;
172
196
  this.operationsMap.set(operation.operationId, operation);
173
- emitEvents(this.modelClass, EventData.fromOperation(operation));
197
+ emitEvents(this, EventData.fromOperation(operation));
174
198
  return true;
175
199
  }
176
200
  confirm(operation) {
177
201
  if (!this.operationsMap.has(operation.operationId))
178
202
  return;
179
203
  this.operationsMap.set(operation.operationId, operation);
180
- emitEvents(this.modelClass, EventData.fromOperation(operation));
204
+ emitEvents(this, EventData.fromOperation(operation));
181
205
  }
182
206
  reject(operation) {
183
207
  if (!this.operationsMap.has(operation.operationId))
184
208
  return;
185
209
  this.operationsMap.set(operation.operationId, operation);
186
- emitEvents(this.modelClass, EventData.fromOperation(operation));
210
+ emitEvents(this, EventData.fromOperation(operation));
187
211
  }
188
212
  setOperations(operations = []) {
189
213
  const prevOps = this.operations;
190
214
  this.operationsMap.clear();
191
- operations.forEach(op => {
215
+ operations.forEach((op) => {
192
216
  this.operationsMap.set(op.operationId, op);
193
217
  });
194
218
  const allOps = [...prevOps, ...this.operations];
195
- emitEvents(this.modelClass, EventData.fromOperations(allOps));
219
+ emitEvents(this, EventData.fromOperations(allOps));
196
220
  }
197
221
  // Ground truth data methods
198
222
  setGroundTruth(groundTruth) {
@@ -200,7 +224,7 @@ export class ModelStore {
200
224
  this.groundTruthArray = Array.isArray(groundTruth) ? groundTruth : [];
201
225
  // reactivity - gather all ops
202
226
  const allOps = [...prevGroundTruth, ...this.groundTruthArray];
203
- emitEvents(this.modelClass, EventData.fromInstances(allOps, this.modelClass));
227
+ emitEvents(this, EventData.fromInstances(allOps, this.modelClass));
204
228
  }
205
229
  getGroundTruth() {
206
230
  return this.groundTruthArray;
@@ -208,16 +232,16 @@ export class ModelStore {
208
232
  get groundTruthPks() {
209
233
  const pk = this.pkField;
210
234
  return this.groundTruthArray
211
- .filter(instance => instance && typeof instance === 'object' && pk in instance)
212
- .map(instance => instance[pk]);
235
+ .filter((instance) => instance && typeof instance === "object" && pk in instance)
236
+ .map((instance) => instance[pk]);
213
237
  }
214
238
  addToGroundTruth(instances) {
215
239
  if (!Array.isArray(instances) || instances.length === 0)
216
240
  return;
217
241
  const pkField = this.pkField;
218
242
  const pkMap = new Map();
219
- instances.forEach(inst => {
220
- if (inst && typeof inst === 'object' && pkField in inst) {
243
+ instances.forEach((inst) => {
244
+ if (inst && typeof inst === "object" && pkField in inst) {
221
245
  pkMap.set(inst[pkField], inst);
222
246
  }
223
247
  else {
@@ -230,7 +254,9 @@ export class ModelStore {
230
254
  const processedPks = new Set();
231
255
  const checkpointInstances = []; // Track instances that need CHECKPOINT operations
232
256
  for (const existingItem of this.groundTruthArray) {
233
- if (!existingItem || typeof existingItem !== 'object' || !(pkField in existingItem)) {
257
+ if (!existingItem ||
258
+ typeof existingItem !== "object" ||
259
+ !(pkField in existingItem)) {
234
260
  continue;
235
261
  }
236
262
  const pk = existingItem[pkField];
@@ -252,18 +278,20 @@ export class ModelStore {
252
278
  // Create CHECKPOINT operation for instances that already existed
253
279
  if (checkpointInstances.length > 0) {
254
280
  const checkpointOperation = new Operation({
255
- 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)}`,
256
284
  type: Type.CHECKPOINT,
257
285
  instances: checkpointInstances,
258
286
  status: Status.CONFIRMED,
259
287
  timestamp: Date.now(),
260
- queryset: this.modelClass.objects.all()
288
+ queryset: this.modelClass.objects.all(),
261
289
  });
262
290
  this.operationsMap.set(checkpointOperation.operationId, checkpointOperation);
263
291
  console.log(`[ModelStore ${this.modelClass.modelName}] Created CHECKPOINT operation for ${checkpointInstances.length} existing instances`);
264
292
  }
265
293
  // reactivity - use all the newly added instances (both new and updated)
266
- emitEvents(this.modelClass, EventData.fromInstances([...checkpointInstances, ...Array.from(pkMap.values())], this.modelClass));
294
+ emitEvents(this, EventData.fromInstances([...checkpointInstances, ...Array.from(pkMap.values())], this.modelClass));
267
295
  }
268
296
  _filteredOperations(pks, operations) {
269
297
  if (!pks)
@@ -271,7 +299,7 @@ export class ModelStore {
271
299
  const pkField = this.pkField;
272
300
  let filteredOps = [];
273
301
  for (const op of operations) {
274
- let relevantInstances = op.instances.filter(instance => pks.has(instance[pkField] || instance));
302
+ let relevantInstances = op.instances.filter((instance) => pks.has(instance[pkField] || instance));
275
303
  if (relevantInstances.length > 0) {
276
304
  filteredOps.push({
277
305
  operationId: op.operationId,
@@ -280,7 +308,7 @@ export class ModelStore {
280
308
  queryset: op.queryset,
281
309
  type: op.type,
282
310
  status: op.status,
283
- args: op.args
311
+ args: op.args,
284
312
  });
285
313
  }
286
314
  }
@@ -290,7 +318,7 @@ export class ModelStore {
290
318
  const pkField = this.pkField;
291
319
  let groundTruthMap = new Map();
292
320
  for (const instance of groundTruthArray) {
293
- if (!instance || typeof instance !== 'object' || !(pkField in instance)) {
321
+ if (!instance || typeof instance !== "object" || !(pkField in instance)) {
294
322
  continue;
295
323
  }
296
324
  const pk = instance[pkField];
@@ -303,7 +331,7 @@ export class ModelStore {
303
331
  applyOperation(operation, currentInstances) {
304
332
  const pkField = this.pkField;
305
333
  for (const instance of operation.instances) {
306
- if (!instance || typeof instance !== 'object' || !(pkField in instance)) {
334
+ if (!instance || typeof instance !== "object" || !(pkField in instance)) {
307
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.`);
308
336
  continue;
309
337
  }
@@ -322,9 +350,9 @@ export class ModelStore {
322
350
  currentInstances.set(pk, { ...existing, ...instance });
323
351
  }
324
352
  else {
325
- const wasDeletedLocally = this.operations.some(op => op.type === Type.DELETE &&
353
+ const wasDeletedLocally = this.operations.some((op) => op.type === Type.DELETE &&
326
354
  op.status !== Status.REJECTED &&
327
- op.instances.some(inst => inst && inst[pkField] === pk));
355
+ op.instances.some((inst) => inst && inst[pkField] === pk));
328
356
  if (!wasDeletedLocally) {
329
357
  currentInstances.set(pk, instance);
330
358
  }
@@ -343,10 +371,10 @@ export class ModelStore {
343
371
  }
344
372
  getTrimmedOperations() {
345
373
  const twoMinutesAgo = Date.now() - 1000 * 60 * 2;
346
- return this.operations.filter(operation => operation.timestamp > twoMinutesAgo);
374
+ return this.operations.filter((operation) => operation.timestamp > twoMinutesAgo);
347
375
  }
348
376
  getInflightOperations() {
349
- return this.operations.filter(operation => operation.status != Status.CONFIRMED &&
377
+ return this.operations.filter((operation) => operation.status != Status.CONFIRMED &&
350
378
  operation.status != Status.REJECTED);
351
379
  }
352
380
  // Pruning
@@ -358,12 +386,16 @@ export class ModelStore {
358
386
  }
359
387
  // Render methods
360
388
  render(pks = null, optimistic = true) {
361
- const pksSet = pks === null ? null :
362
- (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]);
363
394
  const renderedInstancesMap = this._filteredGroundTruth(pksSet, this.groundTruthArray);
364
395
  const relevantOperations = this._filteredOperations(pksSet, this.operations);
365
396
  for (const op of relevantOperations) {
366
- if (op.status !== Status.REJECTED && (optimistic || op.status === Status.CONFIRMED)) {
397
+ if (op.status !== Status.REJECTED &&
398
+ (optimistic || op.status === Status.CONFIRMED)) {
367
399
  this.applyOperation(op, renderedInstancesMap);
368
400
  }
369
401
  }
@@ -386,7 +418,10 @@ export class ModelStore {
386
418
  this.setOperations(trimmedOps);
387
419
  return;
388
420
  }
389
- const newGroundTruth = await this.fetchFn({ pks: currentPks, modelClass: this.modelClass });
421
+ const newGroundTruth = await this.fetchFn({
422
+ pks: currentPks,
423
+ modelClass: this.modelClass,
424
+ });
390
425
  if (pks) {
391
426
  this.addToGroundTruth(newGroundTruth);
392
427
  return;
@@ -6,8 +6,17 @@ 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;
19
+ _modelStoreUnregister: any;
11
20
  get cacheKey(): any;
12
21
  onHydrated(hydratedData: any): void;
13
22
  setCache(result: any): void;
@@ -25,7 +34,17 @@ export class QuerysetStore {
25
34
  getTrimmedOperations(): any[];
26
35
  getInflightOperations(): any[];
27
36
  prune(): void;
37
+ registerRenderCallback(callback: any): () => boolean;
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;
28
45
  render(optimistic?: boolean, fromCache?: boolean): any[];
46
+ renderFromRoot(optimistic: boolean | undefined, rootStore: any): any[];
47
+ renderFromData(optimistic?: boolean): any[];
29
48
  applyOperation(operation: any, currentPks: any): any;
30
49
  sync(): Promise<void>;
31
50
  }
@@ -1,18 +1,24 @@
1
1
  import { Operation, Status, Type, operationRegistry } from './operation.js';
2
2
  import { querysetEventEmitter } from './reactivity.js';
3
- import { isNil, isEmpty, trim } from 'lodash-es';
3
+ import { isNil, isEmpty, trim, isEqual } from 'lodash-es';
4
4
  import { replaceTempPks, containsTempPk } from '../../flavours/django/tempPk.js';
5
5
  import { modelStoreRegistry } from '../registries/modelStoreRegistry.js';
6
6
  import { processIncludedEntities } from '../../flavours/django/makeApiCall.js';
7
7
  import hash from 'object-hash';
8
8
  import { Cache } from '../cache/cache.js';
9
+ import { filter } from "../../filtering/localFiltering.js";
10
+ import { mod } from 'mathjs';
9
11
  export class QuerysetStore {
10
12
  constructor(modelClass, fetchFn, queryset, initialGroundTruthPks = null, initialOperations = null, options = {}) {
11
13
  this.modelClass = modelClass;
12
14
  this.fetchFn = fetchFn;
13
15
  this.queryset = queryset;
14
16
  this.isSyncing = false;
17
+ this.lastSync = null;
18
+ this.needsSync = false;
19
+ this.isTemp = options.isTemp || false;
15
20
  this.pruneThreshold = options.pruneThreshold || 10;
21
+ this.getRootStore = options.getRootStore || null;
16
22
  this.groundTruthPks = initialGroundTruthPks || [];
17
23
  this.operationsMap = new Map();
18
24
  if (Array.isArray(initialOperations)) {
@@ -22,7 +28,16 @@ export class QuerysetStore {
22
28
  this.operationsMap.set(op.operationId, op);
23
29
  }
24
30
  }
25
- this.qsCache = new Cache('queryset-cache', {}, this.onHydrated.bind(this));
31
+ this.qsCache = new Cache("queryset-cache", {}, this.onHydrated.bind(this));
32
+ this._lastRenderedPks = null;
33
+ this.renderCallbacks = new Set();
34
+ this._rootUnregister = null;
35
+ this._currentRootStore = null;
36
+ this._ensureRootRegistration();
37
+ const modelStore = modelStoreRegistry.getStore(this.modelClass);
38
+ this._modelStoreUnregister = modelStore.registerRenderCallback(() => {
39
+ this._emitRenderEvent();
40
+ });
26
41
  }
27
42
  // Caching
28
43
  get cacheKey() {
@@ -40,7 +55,7 @@ export class QuerysetStore {
40
55
  setCache(result) {
41
56
  let nonTempPks = [];
42
57
  result.forEach((pk) => {
43
- if (typeof pk === 'string' && containsTempPk(pk)) {
58
+ if (typeof pk === "string" && containsTempPk(pk)) {
44
59
  pk = replaceTempPks(pk);
45
60
  if (isNil(pk) || isEmpty(trim(pk))) {
46
61
  return;
@@ -64,7 +79,22 @@ export class QuerysetStore {
64
79
  return new Set(this.groundTruthPks);
65
80
  }
66
81
  _emitRenderEvent() {
67
- querysetEventEmitter.emit(`${this.modelClass.configKey}::${this.modelClass.modelName}::queryset::render`, { ast: this.queryset.build(), ModelClass: this.modelClass });
82
+ const newPks = this.render(true, false);
83
+ // 1. Always notify direct child stores to trigger their own re-evaluation.
84
+ // They will perform their own check to see if their own results have changed.
85
+ this.renderCallbacks.forEach((callback) => {
86
+ try {
87
+ callback();
88
+ }
89
+ catch (error) {
90
+ console.warn("Error in render callback:", error);
91
+ }
92
+ });
93
+ // 2. Only emit the global event for UI components if the final list of PKs has actually changed.
94
+ if (!isEqual(newPks, this._lastRenderedPks)) {
95
+ this._lastRenderedPks = newPks; // Update the cache with the new state
96
+ querysetEventEmitter.emit(`${this.modelClass.configKey}::${this.modelClass.modelName}::queryset::render`, { ast: this.queryset.build(), ModelClass: this.modelClass });
97
+ }
68
98
  }
69
99
  async addOperation(operation) {
70
100
  this.operationsMap.set(operation.operationId, operation);
@@ -93,9 +123,7 @@ export class QuerysetStore {
93
123
  this._emitRenderEvent();
94
124
  }
95
125
  async setGroundTruth(groundTruthPks) {
96
- this.groundTruthPks = Array.isArray(groundTruthPks)
97
- ? groundTruthPks
98
- : [];
126
+ this.groundTruthPks = Array.isArray(groundTruthPks) ? groundTruthPks : [];
99
127
  this._emitRenderEvent();
100
128
  }
101
129
  async setOperations(operations) {
@@ -109,10 +137,10 @@ export class QuerysetStore {
109
137
  }
110
138
  getTrimmedOperations() {
111
139
  const cutoff = Date.now() - 1000 * 60 * 2;
112
- return this.operations.filter(op => op.timestamp > cutoff);
140
+ return this.operations.filter((op) => op.timestamp > cutoff);
113
141
  }
114
142
  getInflightOperations() {
115
- return this.operations.filter(operation => operation.status != Status.CONFIRMED &&
143
+ return this.operations.filter((operation) => operation.status != Status.CONFIRMED &&
116
144
  operation.status != Status.REJECTED);
117
145
  }
118
146
  prune() {
@@ -120,20 +148,68 @@ export class QuerysetStore {
120
148
  this.setGroundTruth(renderedPks);
121
149
  this.setOperations(this.getInflightOperations());
122
150
  }
151
+ registerRenderCallback(callback) {
152
+ this.renderCallbacks.add(callback);
153
+ return () => this.renderCallbacks.delete(callback);
154
+ }
155
+ _ensureRootRegistration() {
156
+ if (this.isTemp)
157
+ return;
158
+ const { isRoot, rootStore } = this.getRootStore(this.queryset);
159
+ // If the root store hasn't changed, nothing to do
160
+ if (this._currentRootStore === rootStore) {
161
+ return;
162
+ }
163
+ // Root store changed - clean up old registration if it exists
164
+ if (this._rootUnregister) {
165
+ this._rootUnregister();
166
+ this._rootUnregister = null;
167
+ }
168
+ // Set up new registration if we're derived and have a root store
169
+ if (!isRoot && rootStore) {
170
+ this._rootUnregister = rootStore.registerRenderCallback(() => {
171
+ this._emitRenderEvent();
172
+ });
173
+ }
174
+ // Update current root store reference (could be null now)
175
+ this._currentRootStore = rootStore;
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
+ }
123
192
  render(optimistic = true, fromCache = false) {
193
+ this._ensureRootRegistration();
124
194
  if (fromCache) {
125
195
  const cachedResult = this.qsCache.get(this.cacheKey);
126
196
  if (Array.isArray(cachedResult)) {
127
197
  return cachedResult;
128
198
  }
129
199
  }
130
- const renderedPks = this.groundTruthSet;
131
- for (const op of this.operations) {
132
- if (op.status !== Status.REJECTED && (optimistic || op.status === Status.CONFIRMED)) {
133
- this.applyOperation(op, renderedPks);
200
+ let pks;
201
+ if (this.getRootStore &&
202
+ typeof this.getRootStore === "function" &&
203
+ !this.isTemp) {
204
+ const { isRoot, rootStore } = this.getRootStore(this.queryset);
205
+ if (!isRoot && rootStore) {
206
+ pks = this.renderFromRoot(optimistic, rootStore);
134
207
  }
135
208
  }
136
- let result = Array.from(renderedPks);
209
+ if (isNil(pks)) {
210
+ pks = this.renderFromData(optimistic);
211
+ }
212
+ let result = this._getValidatedAndFilteredPks(pks);
137
213
  let limit = this.queryset.build().serializerOptions?.limit;
138
214
  if (limit) {
139
215
  result = result.slice(0, limit);
@@ -141,12 +217,30 @@ export class QuerysetStore {
141
217
  this.setCache(result);
142
218
  return result;
143
219
  }
220
+ renderFromRoot(optimistic = true, rootStore) {
221
+ let renderedPks = rootStore.render(optimistic);
222
+ let renderedData = renderedPks.map((pk) => {
223
+ return this.modelClass.fromPk(pk, this.queryset);
224
+ });
225
+ let ast = this.queryset.build();
226
+ let result = filter(renderedData, ast, this.modelClass, false);
227
+ return result;
228
+ }
229
+ renderFromData(optimistic = true) {
230
+ const renderedPks = this.groundTruthSet;
231
+ for (const op of this.operations) {
232
+ if (op.status !== Status.REJECTED &&
233
+ (optimistic || op.status === Status.CONFIRMED)) {
234
+ this.applyOperation(op, renderedPks);
235
+ }
236
+ }
237
+ let result = Array.from(renderedPks);
238
+ return result;
239
+ }
144
240
  applyOperation(operation, currentPks) {
145
241
  const pkField = this.pkField;
146
242
  for (const instance of operation.instances) {
147
- if (!instance ||
148
- typeof instance !== 'object' ||
149
- !(pkField in instance)) {
243
+ if (!instance || typeof instance !== "object" || !(pkField in instance)) {
150
244
  console.warn(`[QuerysetStore ${this.modelClass.modelName}] Skipping instance in operation ${operation.operationId} due to missing PK '${String(pkField)}' or invalid format.`);
151
245
  continue;
152
246
  }
@@ -175,23 +269,44 @@ export class QuerysetStore {
175
269
  console.warn(`[QuerysetStore ${id}] Already syncing, request ignored.`);
176
270
  return;
177
271
  }
272
+ // Check if we're delegating to a root store
273
+ if (this.getRootStore &&
274
+ typeof this.getRootStore === "function" &&
275
+ !this.isTemp) {
276
+ const { isRoot, rootStore } = this.getRootStore(this.queryset);
277
+ if (!isRoot && rootStore) {
278
+ // We're delegating to a root store - don't sync, just mark as needing sync
279
+ console.log(`[${id}] Delegating to root store, marking sync needed.`);
280
+ this.needsSync = true;
281
+ this.lastSync = null; // Clear last sync since we're not actually syncing
282
+ this.setOperations(this.getInflightOperations());
283
+ return;
284
+ }
285
+ }
286
+ // We're in independent mode - proceed with normal sync
178
287
  this.isSyncing = true;
179
288
  console.log(`[${id}] Starting sync...`);
180
289
  try {
181
290
  const response = await this.fetchFn({
182
291
  ast: this.queryset.build(),
183
- modelClass: this.modelClass
292
+ modelClass: this.modelClass,
184
293
  });
185
294
  const { data, included } = response;
295
+ if (isNil(data)) {
296
+ return;
297
+ }
186
298
  console.log(`[${id}] Sync fetch completed. Received: ${JSON.stringify(data)}.`);
187
299
  // Persists all the instances (including nested instances) to the model store
188
300
  processIncludedEntities(modelStoreRegistry, included, this.modelClass);
189
301
  this.setGroundTruth(data);
190
302
  this.setOperations(this.getInflightOperations());
303
+ this.lastSync = Date.now();
304
+ this.needsSync = false;
191
305
  console.log(`[${id}] Sync completed.`);
192
306
  }
193
307
  catch (e) {
194
308
  console.error(`[${id}] Failed to sync ground truth:`, e);
309
+ this.needsSync = true; // Mark as needing sync on error
195
310
  }
196
311
  finally {
197
312
  this.isSyncing = false;
@@ -15,10 +15,15 @@ export class SyncManager {
15
15
  followedModels: Map<any, any>;
16
16
  followAllQuerysets: boolean;
17
17
  followedQuerysets: Map<any, any>;
18
+ periodicSyncTimer: NodeJS.Timeout | null;
18
19
  /**
19
20
  * Initialize event handlers for all event receivers
20
21
  */
21
22
  initialize(): void;
23
+ startPeriodicSync(): void;
24
+ syncStaleQuerysets(): void;
25
+ isStoreFollowed(registry: any, semanticKey: any): boolean;
26
+ cleanup(): void;
22
27
  followModel(registry: any, modelClass: any): void;
23
28
  unfollowModel(registry: any, modelClass: any): void;
24
29
  manageRegistry(registry: any): void;