@statezero/core 0.2.53 → 0.2.54
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/adaptors/vue/components/StateZeroDebugPanel.js +1025 -1159
- package/dist/adaptors/vue/composables.js +5 -0
- package/dist/core.css +1 -1
- package/dist/syncEngine/metrics/metricOptCalcs.d.ts +9 -78
- package/dist/syncEngine/metrics/metricOptCalcs.js +24 -277
- package/dist/syncEngine/registries/metricRegistry.d.ts +3 -0
- package/dist/syncEngine/registries/metricRegistry.js +10 -0
- package/dist/syncEngine/stores/metricStore.d.ts +18 -18
- package/dist/syncEngine/stores/metricStore.js +52 -91
- package/dist/syncEngine/stores/modelStore.js +28 -12
- package/dist/syncEngine/stores/operationEventHandlers.js +83 -349
- package/dist/syncEngine/stores/querysetStore.d.ts +10 -5
- package/dist/syncEngine/stores/querysetStore.js +134 -52
- package/dist/syncEngine/sync.js +2 -2
- package/package.json +1 -1
|
@@ -3,6 +3,7 @@ import { querysetEventEmitter } from './reactivity.js';
|
|
|
3
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
|
+
import { querysetStoreRegistry } from '../registries/querysetStoreRegistry.js';
|
|
6
7
|
import { processIncludedEntities } from '../../flavours/django/makeApiCall.js';
|
|
7
8
|
import { Cache } from '../cache/cache.js';
|
|
8
9
|
import { filter } from "../../filtering/localFiltering.js";
|
|
@@ -14,6 +15,7 @@ export class QuerysetStore {
|
|
|
14
15
|
this.queryset = queryset;
|
|
15
16
|
this.isSyncing = false;
|
|
16
17
|
this.lastSync = null;
|
|
18
|
+
this._createdAt = Date.now();
|
|
17
19
|
this.isTemp = options.isTemp || false;
|
|
18
20
|
this.pruneThreshold = options.pruneThreshold || 10;
|
|
19
21
|
this.groundTruthPks = initialGroundTruthPks || [];
|
|
@@ -191,6 +193,56 @@ export class QuerysetStore {
|
|
|
191
193
|
const finalPks = filter(instances, ast, this.modelClass, false); // false = return PKs
|
|
192
194
|
return finalPks;
|
|
193
195
|
}
|
|
196
|
+
/**
|
|
197
|
+
* For offset pages that aren't full, expand with creates from the model store
|
|
198
|
+
* that sort after the first item on the page (direction-adjusted).
|
|
199
|
+
*/
|
|
200
|
+
_fillOffsetPage(currentPks, optimistic) {
|
|
201
|
+
const modelStore = modelStoreRegistry.getStore(this.modelClass);
|
|
202
|
+
if (!modelStore)
|
|
203
|
+
return null;
|
|
204
|
+
const orderBy = this.queryset._orderBy;
|
|
205
|
+
if (!orderBy || orderBy.length === 0)
|
|
206
|
+
return null;
|
|
207
|
+
// Get sort field and direction from first ordering field
|
|
208
|
+
const field = orderBy[0].replace(/^-/, '');
|
|
209
|
+
const isDesc = orderBy[0].startsWith('-');
|
|
210
|
+
// Get first item's sort value as the page boundary
|
|
211
|
+
const firstInstance = this.modelClass.fromPk(currentPks[0], this.queryset);
|
|
212
|
+
if (!firstInstance)
|
|
213
|
+
return null;
|
|
214
|
+
const boundary = firstInstance[field];
|
|
215
|
+
if (boundary == null)
|
|
216
|
+
return null;
|
|
217
|
+
// Find CREATE-type instances that sort into this page
|
|
218
|
+
const pkField = this.pkField;
|
|
219
|
+
const currentSet = new Set(currentPks);
|
|
220
|
+
const candidates = new Set(currentPks);
|
|
221
|
+
for (const op of modelStore.operations) {
|
|
222
|
+
if (op.status === Status.REJECTED)
|
|
223
|
+
continue;
|
|
224
|
+
if (!optimistic && op.status !== Status.CONFIRMED)
|
|
225
|
+
continue;
|
|
226
|
+
if (op.type !== Type.CREATE && op.type !== Type.BULK_CREATE &&
|
|
227
|
+
op.type !== Type.GET_OR_CREATE && op.type !== Type.UPDATE_OR_CREATE)
|
|
228
|
+
continue;
|
|
229
|
+
for (const inst of op.instances) {
|
|
230
|
+
if (!inst || inst[pkField] == null)
|
|
231
|
+
continue;
|
|
232
|
+
if (currentSet.has(inst[pkField]))
|
|
233
|
+
continue;
|
|
234
|
+
const val = inst[field];
|
|
235
|
+
if (val == null)
|
|
236
|
+
continue;
|
|
237
|
+
const sortsAfter = isDesc ? val <= boundary : val >= boundary;
|
|
238
|
+
if (sortsAfter)
|
|
239
|
+
candidates.add(inst[pkField]);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (candidates.size === currentSet.size)
|
|
243
|
+
return null;
|
|
244
|
+
return this._getValidatedAndFilteredPks(Array.from(candidates));
|
|
245
|
+
}
|
|
194
246
|
render(optimistic = true, fromCache = false) {
|
|
195
247
|
// Check cache first if requested
|
|
196
248
|
if (fromCache) {
|
|
@@ -210,32 +262,26 @@ export class QuerysetStore {
|
|
|
210
262
|
return cachedResult;
|
|
211
263
|
}
|
|
212
264
|
}
|
|
213
|
-
|
|
214
|
-
// This handles chained optimistic filters, newly created stores, etc.
|
|
215
|
-
// (If synced with empty results, that's valid ground truth)
|
|
216
|
-
// Skip model store seeding for paginated querysets (offset > 0) — the model
|
|
217
|
-
// store contains items from other pages, so seeding would show wrong results.
|
|
218
|
-
const offset = this.queryset.build().serializerOptions?.offset;
|
|
219
|
-
const canSeedFromModelStore = this.groundTruthPks.length === 0
|
|
220
|
-
&& this.lastSync === null
|
|
221
|
-
&& !(offset != null && offset > 0);
|
|
222
|
-
const pks = canSeedFromModelStore
|
|
223
|
-
? this.renderFromModelStore()
|
|
224
|
-
: this.renderFromData(optimistic);
|
|
265
|
+
const pks = this.renderFromData(optimistic);
|
|
225
266
|
// Validate against model store and apply local filtering/sorting
|
|
226
267
|
let result = this._getValidatedAndFilteredPks(pks);
|
|
227
268
|
const preLimitCount = result.length;
|
|
228
269
|
// Apply pagination limit
|
|
229
270
|
const limit = this.queryset.build().serializerOptions?.limit;
|
|
271
|
+
const hasOffset = (this.queryset._serializerOptions?.offset ?? 0) > 0;
|
|
272
|
+
// Offset page not full: fill with creates from model store that sort into this page
|
|
273
|
+
if (hasOffset && limit && result.length > 0 && result.length < limit) {
|
|
274
|
+
const expanded = this._fillOffsetPage(result, optimistic);
|
|
275
|
+
if (expanded)
|
|
276
|
+
result = expanded;
|
|
277
|
+
}
|
|
230
278
|
if (limit) {
|
|
231
279
|
result = result.slice(0, limit);
|
|
232
280
|
}
|
|
233
281
|
this.setCache(result);
|
|
234
282
|
recordDebugEvent({
|
|
235
283
|
type: "render",
|
|
236
|
-
source:
|
|
237
|
-
? "modelStore"
|
|
238
|
-
: "groundTruth",
|
|
284
|
+
source: "groundTruth",
|
|
239
285
|
semanticKey: this.queryset.semanticKey,
|
|
240
286
|
modelName: this.modelClass.modelName,
|
|
241
287
|
configKey: this.modelClass.configKey,
|
|
@@ -250,26 +296,79 @@ export class QuerysetStore {
|
|
|
250
296
|
return result;
|
|
251
297
|
}
|
|
252
298
|
renderFromData(optimistic = true) {
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
299
|
+
const modelStore = modelStoreRegistry.getStore(this.modelClass);
|
|
300
|
+
// Temp stores: greedy seed from all model data (for op targeting, ignores pagination)
|
|
301
|
+
if (this.isTemp && this.lastSync === null && modelStore) {
|
|
302
|
+
const pkField = this.pkField;
|
|
303
|
+
return modelStore.render().map(inst => inst[pkField]).filter(pk => pk != null);
|
|
304
|
+
}
|
|
305
|
+
// Permanent never-synced: seed from local data
|
|
306
|
+
if (!this.isTemp && this.lastSync === null && modelStore) {
|
|
307
|
+
const hasOffset = (this.queryset._serializerOptions?.offset ?? 0) > 0;
|
|
308
|
+
if (!hasOffset) {
|
|
309
|
+
// No offset: seed from all model store data, filter handles membership
|
|
310
|
+
const pkField = this.pkField;
|
|
311
|
+
return modelStore.render().map(inst => inst[pkField]).filter(pk => pk != null);
|
|
312
|
+
}
|
|
313
|
+
// Offset: seed from root store if it's been synced
|
|
314
|
+
const { root: rootKey } = querysetStoreRegistry.querysetStoreGraph.findRoot(this.queryset);
|
|
315
|
+
const rootStore = rootKey ? querysetStoreRegistry._stores.get(rootKey) : null;
|
|
316
|
+
if (rootStore && rootStore !== this && rootStore.lastSync !== null) {
|
|
317
|
+
return rootStore.render();
|
|
258
318
|
}
|
|
259
319
|
}
|
|
260
|
-
|
|
261
|
-
|
|
320
|
+
const hasOffset = (this.queryset._serializerOptions?.offset ?? 0) > 0;
|
|
321
|
+
// Offset querysets after sync: just ground truth.
|
|
322
|
+
// Filled pages shouldn't mix in local ops (makes mess).
|
|
323
|
+
// Last-page fill is handled by _fillOffsetPage in render().
|
|
324
|
+
if (hasOffset) {
|
|
325
|
+
return Array.from(this.groundTruthSet);
|
|
326
|
+
}
|
|
327
|
+
// No-offset: greedy — filtering is perfect, so include all fresh ops.
|
|
328
|
+
if (!modelStore || !this._hasRecentOps(modelStore)) {
|
|
329
|
+
return Array.from(this.groundTruthSet);
|
|
330
|
+
}
|
|
331
|
+
const freshPks = this._buildFreshPks(modelStore, optimistic);
|
|
332
|
+
const basePks = this.groundTruthSet;
|
|
333
|
+
for (const pk of freshPks)
|
|
334
|
+
basePks.add(pk);
|
|
335
|
+
return Array.from(basePks);
|
|
262
336
|
}
|
|
263
|
-
/**
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
337
|
+
/** Are there any inflight or recent confirmed ops in the model store? */
|
|
338
|
+
_hasRecentOps(modelStore) {
|
|
339
|
+
const ops = modelStore.operations;
|
|
340
|
+
const cutoff = this.lastSync ?? this._createdAt;
|
|
341
|
+
for (let i = ops.length - 1; i >= 0; i--) {
|
|
342
|
+
if (ops[i].status === Status.REJECTED)
|
|
343
|
+
continue;
|
|
344
|
+
// Inflight ops are always relevant (not yet in ground truth)
|
|
345
|
+
if (ops[i].status !== Status.CONFIRMED)
|
|
346
|
+
return true;
|
|
347
|
+
// Confirmed ops: only relevant if newer than last sync
|
|
348
|
+
return ops[i].timestamp >= cutoff;
|
|
349
|
+
}
|
|
350
|
+
return false;
|
|
351
|
+
}
|
|
352
|
+
/** Build set of PKs from ops newer than lastSync. Only used for no-offset querysets. */
|
|
353
|
+
_buildFreshPks(modelStore, optimistic) {
|
|
354
|
+
const freshPks = new Set();
|
|
355
|
+
const pkField = this.pkField;
|
|
356
|
+
const cutoff = this.lastSync ?? this._createdAt;
|
|
357
|
+
for (const op of modelStore.operations) {
|
|
358
|
+
if (op.status === Status.REJECTED)
|
|
359
|
+
continue;
|
|
360
|
+
if (!optimistic && op.status !== Status.CONFIRMED)
|
|
361
|
+
continue;
|
|
362
|
+
// Inflight ops always included (not yet in ground truth).
|
|
363
|
+
// Confirmed ops: skip if older than last sync (already in ground truth).
|
|
364
|
+
if (op.status === Status.CONFIRMED && op.timestamp < cutoff)
|
|
365
|
+
continue;
|
|
366
|
+
for (const inst of op.instances) {
|
|
367
|
+
if (inst && inst[pkField] != null)
|
|
368
|
+
freshPks.add(inst[pkField]);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return freshPks;
|
|
273
372
|
}
|
|
274
373
|
applyOperation(operation, currentPks) {
|
|
275
374
|
const pkField = this.pkField;
|
|
@@ -279,26 +378,9 @@ export class QuerysetStore {
|
|
|
279
378
|
continue;
|
|
280
379
|
}
|
|
281
380
|
let pk = instance[pkField];
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
currentPks.add(pk);
|
|
286
|
-
break;
|
|
287
|
-
case Type.CHECKPOINT:
|
|
288
|
-
break;
|
|
289
|
-
case Type.UPDATE:
|
|
290
|
-
case Type.UPDATE_INSTANCE:
|
|
291
|
-
// Add PKs as candidates so items can move into this queryset
|
|
292
|
-
// when an update makes them match the filter.
|
|
293
|
-
// _getValidatedAndFilteredPks will filter out non-matching items.
|
|
294
|
-
currentPks.add(pk);
|
|
295
|
-
break;
|
|
296
|
-
case Type.DELETE:
|
|
297
|
-
case Type.DELETE_INSTANCE:
|
|
298
|
-
currentPks.delete(pk);
|
|
299
|
-
break;
|
|
300
|
-
default:
|
|
301
|
-
console.error(`[QuerysetStore ${this.modelClass.modelName}] Unknown operation type: ${operation.type}`);
|
|
381
|
+
// Only CREATE types add PKs. UPDATE/DELETE are handled by model store + refiltering.
|
|
382
|
+
if (CREATE_TYPES.has(operation.type)) {
|
|
383
|
+
currentPks.add(pk);
|
|
302
384
|
}
|
|
303
385
|
}
|
|
304
386
|
return currentPks;
|
package/dist/syncEngine/sync.js
CHANGED
|
@@ -377,8 +377,8 @@ export class SyncManager {
|
|
|
377
377
|
entry.store.sync();
|
|
378
378
|
continue;
|
|
379
379
|
}
|
|
380
|
-
//
|
|
381
|
-
if (this.isQuerysetFollowed(entry.queryset)) {
|
|
380
|
+
// Sync if the metric itself is followed, or its queryset is followed
|
|
381
|
+
if (registry.followedMetrics.has(key) || this.isQuerysetFollowed(entry.queryset)) {
|
|
382
382
|
entry.store.sync();
|
|
383
383
|
}
|
|
384
384
|
}
|
package/package.json
CHANGED