@statezero/core 0.2.52 → 0.2.53

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,2 +1,2 @@
1
- export function useQueryset(querysetFactory: any): import("vue").ComputedRef<any>;
1
+ export function useQueryset(querysetFactory: any): import("vue").Ref<any, any> | import("vue").ComputedRef<any>;
2
2
  export const querysets: Map<any, any>;
@@ -1,6 +1,6 @@
1
- import { computed, getCurrentInstance, onBeforeUnmount } from 'vue';
1
+ import { computed, isRef, getCurrentInstance, onBeforeUnmount } from 'vue';
2
2
  import { querysetStoreRegistry } from '../../syncEngine/registries/querysetStoreRegistry.js';
3
- import { metricRegistry } from '../../syncEngine/registries/metricRegistry.js';
3
+ import { metricRegistry, LiveMetric } from '../../syncEngine/registries/metricRegistry.js';
4
4
  import { syncManager } from '../../syncEngine/sync.js';
5
5
  import { registerAdapterReset } from '../../reset.js';
6
6
  import { v7 } from 'uuid';
@@ -22,6 +22,12 @@ export function useQueryset(querysetFactory) {
22
22
  querysets.delete(composableId);
23
23
  updateSyncManager();
24
24
  });
25
+ const result = querysetFactory();
26
+ // Metrics: the adaptor already returns a ref(scalar) — return it directly.
27
+ // Wrapping in another computed would cause .value.value in templates.
28
+ if (isRef(result) && result.original instanceof LiveMetric) {
29
+ return result;
30
+ }
25
31
  return computed(() => {
26
32
  const result = querysetFactory();
27
33
  const original = result?.original || result;
@@ -173,7 +173,7 @@ export function MetricAdaptor(metric) {
173
173
  return wrappedMetricCache.get(cacheKey);
174
174
  }
175
175
  // Create a reactive reference with the initial value
176
- const wrapper = ref(metric.value);
176
+ const wrapper = ref(metric.valueOf());
177
177
  wrapper.original = metric;
178
178
  // Single handler for metric render events
179
179
  const metricRenderHandler = (eventData) => {
@@ -183,9 +183,9 @@ export function MetricAdaptor(metric) {
183
183
  eventData.field === metric.field &&
184
184
  eventData.ast === hash(querysetAst) &&
185
185
  eventData.valueChanged === true) {
186
- console.log(`[sz] metric update: ${modelName}.${metric.field} (${metric.metricType}) =`, metric.value);
186
+ console.log(`[sz] metric update: ${modelName}.${metric.field} (${metric.metricType}) =`, metric.valueOf());
187
187
  // Update the wrapper value with the latest metric value
188
- wrapper.value = metric.value;
188
+ wrapper.value = metric.valueOf();
189
189
  }
190
190
  };
191
191
  // Only listen for metric render events
@@ -60,7 +60,7 @@ export class IndexedDBStore {
60
60
  }
61
61
  export class Cache {
62
62
  constructor(dbName: any, options?: {}, onHydrated?: null);
63
- store: IndexedDBStore;
63
+ store: any;
64
64
  localMap: Map<any, any>;
65
65
  hydrate(): Promise<void>;
66
66
  get(key: any): any;
@@ -288,7 +288,14 @@ export class IndexedDBStore {
288
288
  }
289
289
  export class Cache {
290
290
  constructor(dbName, options = {}, onHydrated = null) {
291
- this.store = new IndexedDBStore(dbName, options);
291
+ // Share IndexedDBStore instances across Cache objects with the same dbName
292
+ if (dbConnections.has(dbName)) {
293
+ this.store = dbConnections.get(dbName);
294
+ }
295
+ else {
296
+ this.store = new IndexedDBStore(dbName, options);
297
+ dbConnections.set(dbName, this.store);
298
+ }
292
299
  this.localMap = new Map();
293
300
  // don't await - will hydrate during app setup
294
301
  this.hydrate()
@@ -1,6 +1,13 @@
1
1
  /**
2
- * LiveMetric, a simple wrapper that always returns the latest metric value
3
- * using a getter for the value property
2
+ * LiveMetric wraps a metric scalar (count, sum, etc.) and stays live.
3
+ *
4
+ * Uses valueOf()/toString() so it coerces to the scalar naturally:
5
+ * count + 0 // → 5
6
+ * `${count}` // → "5"
7
+ * count > 3 // → true
8
+ *
9
+ * Note: typeof returns "object" and === compares identity, not value.
10
+ * Use == for loose comparison, or +metric for explicit coercion.
4
11
  */
5
12
  export class LiveMetric {
6
13
  constructor(queryset: any, metricType: any, field?: null);
@@ -14,8 +21,12 @@ export class LiveMetric {
14
21
  */
15
22
  refreshFromDb(): any;
16
23
  /**
17
- * Getter that always returns the current value from the store
24
+ * Returns the current metric value from the store.
25
+ * Called implicitly by JS when coercing to a primitive (arithmetic, template literals, etc.)
18
26
  */
27
+ valueOf(): any;
28
+ toString(): string;
29
+ /** @deprecated Use valueOf() coercion instead (e.g. +metric, `${metric}`, metric + 0) */
19
30
  get value(): any;
20
31
  }
21
32
  /**
@@ -5,8 +5,15 @@ import { QueryExecutor } from '../../flavours/django/queryExecutor.js';
5
5
  import { wrapReactiveMetric } from '../../reactiveAdaptor.js';
6
6
  import hash from 'object-hash';
7
7
  /**
8
- * LiveMetric, a simple wrapper that always returns the latest metric value
9
- * using a getter for the value property
8
+ * LiveMetric wraps a metric scalar (count, sum, etc.) and stays live.
9
+ *
10
+ * Uses valueOf()/toString() so it coerces to the scalar naturally:
11
+ * count + 0 // → 5
12
+ * `${count}` // → "5"
13
+ * count > 3 // → true
14
+ *
15
+ * Note: typeof returns "object" and === compares identity, not value.
16
+ * Use == for loose comparison, or +metric for explicit coercion.
10
17
  */
11
18
  export class LiveMetric {
12
19
  constructor(queryset, metricType, field = null) {
@@ -26,17 +33,24 @@ export class LiveMetric {
26
33
  return store.sync(true);
27
34
  }
28
35
  /**
29
- * Getter that always returns the current value from the store
36
+ * Returns the current metric value from the store.
37
+ * Called implicitly by JS when coercing to a primitive (arithmetic, template literals, etc.)
30
38
  */
31
- get value() {
32
- // Get the latest store from the registry
39
+ valueOf() {
33
40
  const store = metricRegistry.getStore(this.metricType, this.queryset, this.field);
34
41
  if (!store) {
35
42
  return null;
36
43
  }
37
- // Render the current value
38
44
  return store.render();
39
45
  }
46
+ toString() {
47
+ const val = this.valueOf();
48
+ return val === null ? '' : String(val);
49
+ }
50
+ /** @deprecated Use valueOf() coercion instead (e.g. +metric, `${metric}`, metric + 0) */
51
+ get value() {
52
+ return this.valueOf();
53
+ }
40
54
  }
41
55
  /**
42
56
  * Registry to manage metric stores
@@ -46,11 +46,17 @@ export class QuerysetStore {
46
46
  return this.queryset.semanticKey;
47
47
  }
48
48
  onHydrated(hydratedData) {
49
- if (this.groundTruthPks.length === 0 && this.operationsMap.size === 0) {
50
- const cached = this.qsCache.get(this.cacheKey);
51
- if (!isNil(cached) && !isEmpty(cached)) {
52
- this.setGroundTruth(cached);
53
- }
49
+ // Only use cached data if we haven't synced yet and have no ground truth.
50
+ // This prevents stale cache from overwriting real server data.
51
+ if (this.lastSync !== null)
52
+ return;
53
+ if (this.groundTruthPks.length > 0 || this.operationsMap.size > 0)
54
+ return;
55
+ const cached = this.qsCache.get(this.cacheKey);
56
+ if (!isNil(cached) && !isEmpty(cached)) {
57
+ // Set ground truth WITHOUT updating lastSync — hydration is not a real sync
58
+ this.groundTruthPks = Array.isArray(cached) ? cached : [];
59
+ this._emitRenderEvent();
54
60
  }
55
61
  }
56
62
  setCache(result) {
@@ -207,7 +213,13 @@ export class QuerysetStore {
207
213
  // If no ground truth AND hasn't been synced, render from model store
208
214
  // This handles chained optimistic filters, newly created stores, etc.
209
215
  // (If synced with empty results, that's valid ground truth)
210
- const pks = this.groundTruthPks.length === 0 && this.lastSync === null
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
211
223
  ? this.renderFromModelStore()
212
224
  : this.renderFromData(optimistic);
213
225
  // Validate against model store and apply local filtering/sorting
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@statezero/core",
3
- "version": "0.2.52",
3
+ "version": "0.2.53",
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",