@statezero/core 0.2.51 → 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.
Files changed (177) hide show
  1. package/dist/adaptors/vue/components/StateZeroDebugPanel.js +586 -559
  2. package/dist/adaptors/vue/composables.d.ts +1 -1
  3. package/dist/adaptors/vue/composables.js +8 -2
  4. package/dist/adaptors/vue/reactivity.d.ts +1 -0
  5. package/dist/adaptors/vue/reactivity.js +20 -8
  6. package/dist/cli/commands/syncActions.js +33 -1
  7. package/dist/cli/commands/syncModels.js +44 -3
  8. package/dist/filtering/localFiltering.js +3 -2
  9. package/dist/flavours/django/errors.d.ts +12 -0
  10. package/dist/flavours/django/errors.js +20 -0
  11. package/dist/flavours/django/f.js +16 -15
  12. package/dist/index.d.ts +2 -1
  13. package/dist/index.js +2 -2
  14. package/dist/models/backend1/django_app/author.d.ts +284 -0
  15. package/dist/models/backend1/django_app/author.js +69 -0
  16. package/dist/models/backend1/django_app/author.schema.json +281 -0
  17. package/dist/models/backend1/django_app/book.d.ts +262 -0
  18. package/dist/models/backend1/django_app/book.js +71 -0
  19. package/dist/models/backend1/django_app/book.schema.json +254 -0
  20. package/dist/models/backend1/django_app/composeditem.d.ts +140 -0
  21. package/dist/models/backend1/django_app/composeditem.js +69 -0
  22. package/dist/models/backend1/django_app/composeditem.schema.json +122 -0
  23. package/dist/models/backend1/django_app/comprehensivemodel.schema.json +3 -3
  24. package/dist/models/backend1/django_app/dailyrate.schema.json +7 -7
  25. package/dist/models/backend1/django_app/deepmodellevel1.schema.json +1 -1
  26. package/dist/models/backend1/django_app/dummymodel.schema.json +3 -3
  27. package/dist/models/backend1/django_app/errortestcompoundunique.d.ts +124 -0
  28. package/dist/models/backend1/django_app/errortestcompoundunique.js +69 -0
  29. package/dist/models/backend1/django_app/errortestcompoundunique.schema.json +106 -0
  30. package/dist/models/backend1/django_app/errortestonetoonemodel.d.ts +118 -0
  31. package/dist/models/backend1/django_app/errortestonetoonemodel.js +71 -0
  32. package/dist/models/backend1/django_app/errortestonetoonemodel.schema.json +90 -0
  33. package/dist/models/backend1/django_app/errortestparent.d.ts +92 -0
  34. package/dist/models/backend1/django_app/errortestparent.js +69 -0
  35. package/dist/models/backend1/django_app/errortestparent.schema.json +70 -0
  36. package/dist/models/backend1/django_app/errortestprotectedchild.d.ts +118 -0
  37. package/dist/models/backend1/django_app/errortestprotectedchild.js +71 -0
  38. package/dist/models/backend1/django_app/errortestprotectedchild.schema.json +94 -0
  39. package/dist/models/backend1/django_app/errortestuniquemodel.d.ts +108 -0
  40. package/dist/models/backend1/django_app/errortestuniquemodel.js +69 -0
  41. package/dist/models/backend1/django_app/errortestuniquemodel.schema.json +88 -0
  42. package/dist/models/backend1/django_app/excludeditem.d.ts +124 -0
  43. package/dist/models/backend1/django_app/excludeditem.js +69 -0
  44. package/dist/models/backend1/django_app/excludeditem.schema.json +105 -0
  45. package/dist/models/backend1/django_app/hfchild.d.ts +118 -0
  46. package/dist/models/backend1/django_app/hfchild.js +71 -0
  47. package/dist/models/backend1/django_app/hfchild.schema.json +94 -0
  48. package/dist/models/backend1/django_app/hfparent.d.ts +124 -0
  49. package/dist/models/backend1/django_app/hfparent.js +69 -0
  50. package/dist/models/backend1/django_app/hfparent.schema.json +105 -0
  51. package/dist/models/backend1/django_app/index.js +19 -0
  52. package/dist/models/backend1/django_app/m2mdepthtestlevel1.schema.json +3 -3
  53. package/dist/models/backend1/django_app/m2mdepthtestlevel2.schema.json +3 -3
  54. package/dist/models/backend1/django_app/m2mdepthtestlevel3.schema.json +5 -5
  55. package/dist/models/backend1/django_app/modelwithrestrictedfields.schema.json +2 -2
  56. package/dist/models/backend1/django_app/nodeleteitem.d.ts +124 -0
  57. package/dist/models/backend1/django_app/nodeleteitem.js +69 -0
  58. package/dist/models/backend1/django_app/nodeleteitem.schema.json +105 -0
  59. package/dist/models/backend1/django_app/objectlevelitem.d.ts +124 -0
  60. package/dist/models/backend1/django_app/objectlevelitem.js +69 -0
  61. package/dist/models/backend1/django_app/objectlevelitem.schema.json +105 -0
  62. package/dist/models/backend1/django_app/order.schema.json +7 -7
  63. package/dist/models/backend1/django_app/orderitem.schema.json +5 -5
  64. package/dist/models/backend1/django_app/product.schema.json +11 -11
  65. package/dist/models/backend1/django_app/productcategory.schema.json +2 -2
  66. package/dist/models/backend1/django_app/rateplan.schema.json +2 -2
  67. package/dist/models/backend1/django_app/readonlyitem.d.ts +124 -0
  68. package/dist/models/backend1/django_app/readonlyitem.js +69 -0
  69. package/dist/models/backend1/django_app/readonlyitem.schema.json +105 -0
  70. package/dist/models/backend1/django_app/restrictedcreateitem.d.ts +124 -0
  71. package/dist/models/backend1/django_app/restrictedcreateitem.js +69 -0
  72. package/dist/models/backend1/django_app/restrictedcreateitem.schema.json +105 -0
  73. package/dist/models/backend1/django_app/restrictededititem.d.ts +124 -0
  74. package/dist/models/backend1/django_app/restrictededititem.js +69 -0
  75. package/dist/models/backend1/django_app/restrictededititem.schema.json +105 -0
  76. package/dist/models/backend1/django_app/restrictedfieldrelatedmodel.schema.json +1 -1
  77. package/dist/models/backend1/django_app/rowfiltereditem.d.ts +124 -0
  78. package/dist/models/backend1/django_app/rowfiltereditem.js +69 -0
  79. package/dist/models/backend1/django_app/rowfiltereditem.schema.json +105 -0
  80. package/dist/models/backend1/django_app/tag.d.ts +554 -0
  81. package/dist/models/backend1/django_app/tag.js +71 -0
  82. package/dist/models/backend1/django_app/tag.schema.json +541 -0
  83. package/dist/models/backend1/django_app/updateonlyitem.d.ts +108 -0
  84. package/dist/models/backend1/django_app/updateonlyitem.js +69 -0
  85. package/dist/models/backend1/django_app/updateonlyitem.schema.json +88 -0
  86. package/dist/models/default/django_app/author.d.ts +284 -0
  87. package/dist/models/default/django_app/author.js +69 -0
  88. package/dist/models/default/django_app/author.schema.json +281 -0
  89. package/dist/models/default/django_app/book.d.ts +262 -0
  90. package/dist/models/default/django_app/book.js +71 -0
  91. package/dist/models/default/django_app/book.schema.json +254 -0
  92. package/dist/models/default/django_app/composeditem.d.ts +140 -0
  93. package/dist/models/default/django_app/composeditem.js +69 -0
  94. package/dist/models/default/django_app/composeditem.schema.json +122 -0
  95. package/dist/models/default/django_app/comprehensivemodel.schema.json +3 -3
  96. package/dist/models/default/django_app/dailyrate.schema.json +7 -7
  97. package/dist/models/default/django_app/deepmodellevel1.d.ts +1 -13
  98. package/dist/models/default/django_app/deepmodellevel1.schema.json +2 -14
  99. package/dist/models/default/django_app/deepmodellevel2.d.ts +1 -13
  100. package/dist/models/default/django_app/deepmodellevel2.schema.json +1 -13
  101. package/dist/models/default/django_app/deepmodellevel3.d.ts +1 -13
  102. package/dist/models/default/django_app/deepmodellevel3.schema.json +1 -13
  103. package/dist/models/default/django_app/dummymodel.d.ts +1 -13
  104. package/dist/models/default/django_app/dummymodel.schema.json +4 -16
  105. package/dist/models/default/django_app/dummyrelatedmodel.d.ts +1 -13
  106. package/dist/models/default/django_app/dummyrelatedmodel.schema.json +1 -13
  107. package/dist/models/default/django_app/errortestcompoundunique.d.ts +124 -0
  108. package/dist/models/default/django_app/errortestcompoundunique.js +69 -0
  109. package/dist/models/default/django_app/errortestcompoundunique.schema.json +106 -0
  110. package/dist/models/default/django_app/errortestonetoonemodel.d.ts +118 -0
  111. package/dist/models/default/django_app/errortestonetoonemodel.js +71 -0
  112. package/dist/models/default/django_app/errortestonetoonemodel.schema.json +90 -0
  113. package/dist/models/default/django_app/errortestparent.d.ts +92 -0
  114. package/dist/models/default/django_app/errortestparent.js +69 -0
  115. package/dist/models/default/django_app/errortestparent.schema.json +70 -0
  116. package/dist/models/default/django_app/errortestprotectedchild.d.ts +118 -0
  117. package/dist/models/default/django_app/errortestprotectedchild.js +71 -0
  118. package/dist/models/default/django_app/errortestprotectedchild.schema.json +94 -0
  119. package/dist/models/default/django_app/errortestuniquemodel.d.ts +108 -0
  120. package/dist/models/default/django_app/errortestuniquemodel.js +69 -0
  121. package/dist/models/default/django_app/errortestuniquemodel.schema.json +88 -0
  122. package/dist/models/default/django_app/excludeditem.d.ts +124 -0
  123. package/dist/models/default/django_app/excludeditem.js +69 -0
  124. package/dist/models/default/django_app/excludeditem.schema.json +105 -0
  125. package/dist/models/default/django_app/filetest.d.ts +1 -13
  126. package/dist/models/default/django_app/filetest.schema.json +1 -13
  127. package/dist/models/default/django_app/hfchild.d.ts +118 -0
  128. package/dist/models/default/django_app/hfchild.js +71 -0
  129. package/dist/models/default/django_app/hfchild.schema.json +94 -0
  130. package/dist/models/default/django_app/hfparent.d.ts +124 -0
  131. package/dist/models/default/django_app/hfparent.js +69 -0
  132. package/dist/models/default/django_app/hfparent.schema.json +105 -0
  133. package/dist/models/default/django_app/index.js +19 -0
  134. package/dist/models/default/django_app/m2mdepthtestlevel1.schema.json +3 -3
  135. package/dist/models/default/django_app/m2mdepthtestlevel2.schema.json +3 -3
  136. package/dist/models/default/django_app/m2mdepthtestlevel3.schema.json +5 -5
  137. package/dist/models/default/django_app/modelwithrestrictedfields.schema.json +2 -2
  138. package/dist/models/default/django_app/nodeleteitem.d.ts +124 -0
  139. package/dist/models/default/django_app/nodeleteitem.js +69 -0
  140. package/dist/models/default/django_app/nodeleteitem.schema.json +105 -0
  141. package/dist/models/default/django_app/objectlevelitem.d.ts +124 -0
  142. package/dist/models/default/django_app/objectlevelitem.js +69 -0
  143. package/dist/models/default/django_app/objectlevelitem.schema.json +105 -0
  144. package/dist/models/default/django_app/order.schema.json +7 -7
  145. package/dist/models/default/django_app/orderitem.schema.json +5 -5
  146. package/dist/models/default/django_app/product.schema.json +11 -11
  147. package/dist/models/default/django_app/productcategory.schema.json +2 -2
  148. package/dist/models/default/django_app/rateplan.schema.json +2 -2
  149. package/dist/models/default/django_app/readonlyitem.d.ts +124 -0
  150. package/dist/models/default/django_app/readonlyitem.js +69 -0
  151. package/dist/models/default/django_app/readonlyitem.schema.json +105 -0
  152. package/dist/models/default/django_app/restrictedcreateitem.d.ts +124 -0
  153. package/dist/models/default/django_app/restrictedcreateitem.js +69 -0
  154. package/dist/models/default/django_app/restrictedcreateitem.schema.json +105 -0
  155. package/dist/models/default/django_app/restrictededititem.d.ts +124 -0
  156. package/dist/models/default/django_app/restrictededititem.js +69 -0
  157. package/dist/models/default/django_app/restrictededititem.schema.json +105 -0
  158. package/dist/models/default/django_app/restrictedfieldrelatedmodel.schema.json +1 -1
  159. package/dist/models/default/django_app/rowfiltereditem.d.ts +124 -0
  160. package/dist/models/default/django_app/rowfiltereditem.js +69 -0
  161. package/dist/models/default/django_app/rowfiltereditem.schema.json +105 -0
  162. package/dist/models/default/django_app/tag.d.ts +554 -0
  163. package/dist/models/default/django_app/tag.js +71 -0
  164. package/dist/models/default/django_app/tag.schema.json +541 -0
  165. package/dist/models/default/django_app/updateonlyitem.d.ts +108 -0
  166. package/dist/models/default/django_app/updateonlyitem.js +69 -0
  167. package/dist/models/default/django_app/updateonlyitem.schema.json +88 -0
  168. package/dist/schemaIntegrity.d.ts +14 -0
  169. package/dist/schemaIntegrity.js +29 -0
  170. package/dist/setup.js +6 -0
  171. package/dist/syncEngine/cache/cache.d.ts +1 -1
  172. package/dist/syncEngine/cache/cache.js +8 -1
  173. package/dist/syncEngine/registries/metricRegistry.d.ts +14 -3
  174. package/dist/syncEngine/registries/metricRegistry.js +20 -6
  175. package/dist/syncEngine/stores/operationEventHandlers.js +10 -8
  176. package/dist/syncEngine/stores/querysetStore.js +27 -6
  177. package/package.json +1 -1
@@ -0,0 +1,69 @@
1
+ /**
2
+ * This file was auto-generated. Do not make direct changes to the file.
3
+ */
4
+ import { Model, Manager, QuerySet, getModelClass } from '../../../../src';
5
+ import { wrapReactiveModel } from '../../../../src';
6
+ import schemaData from './updateonlyitem.schema.json';
7
+ /**
8
+ * Model-specific QuerySet implementation
9
+ */
10
+ export class UpdateOnlyItemQuerySet extends QuerySet {
11
+ }
12
+ /**
13
+ * Model-specific Manager implementation
14
+ */
15
+ export class UpdateOnlyItemManager extends Manager {
16
+ constructor(ModelClass) {
17
+ super(ModelClass, UpdateOnlyItemQuerySet);
18
+ }
19
+ newQuerySet() {
20
+ return new UpdateOnlyItemQuerySet(this.ModelClass);
21
+ }
22
+ }
23
+ /**
24
+ * Implementation of the UpdateOnlyItem model
25
+ */
26
+ export class UpdateOnlyItem extends Model {
27
+ constructor(data) {
28
+ UpdateOnlyItem.validateFields(data);
29
+ super(data);
30
+ // Define getters and setters for all fields
31
+ this._defineProperties();
32
+ return wrapReactiveModel(this);
33
+ }
34
+ /**
35
+ * Define property getters and setters for all model fields
36
+ * @private
37
+ */
38
+ _defineProperties() {
39
+ // For each field, define a property that gets/sets from internal storage
40
+ UpdateOnlyItem.fields.forEach(field => {
41
+ Object.defineProperty(this, field, {
42
+ get: function () {
43
+ return this.getField(field);
44
+ },
45
+ set: function (value) {
46
+ this.setField(field, value);
47
+ },
48
+ enumerable: true, // Make sure fields are enumerable for serialization
49
+ configurable: true
50
+ });
51
+ });
52
+ // Add a special read-only getter for the repr field
53
+ Object.defineProperty(this, 'repr', {
54
+ get: function () {
55
+ return this.getField('repr');
56
+ },
57
+ enumerable: true, // Make sure repr is enumerable
58
+ configurable: true
59
+ });
60
+ }
61
+ }
62
+ // Bind this model to its backend
63
+ UpdateOnlyItem.configKey = 'default';
64
+ UpdateOnlyItem.modelName = 'django_app.updateonlyitem';
65
+ UpdateOnlyItem.primaryKeyField = 'id';
66
+ UpdateOnlyItem.objects = new UpdateOnlyItemManager(UpdateOnlyItem);
67
+ UpdateOnlyItem.fields = ['id', 'name', 'value'];
68
+ UpdateOnlyItem.schema = schemaData;
69
+ UpdateOnlyItem.relationshipFields = new Map([]);
@@ -0,0 +1,88 @@
1
+ {
2
+ "model_name": "django_app.updateonlyitem",
3
+ "title": "Update Only Item",
4
+ "class_name": "UpdateOnlyItem",
5
+ "plural_title": "Update Only Items",
6
+ "primary_key_field": "id",
7
+ "filterable_fields": [
8
+ "value",
9
+ "id",
10
+ "name"
11
+ ],
12
+ "searchable_fields": [
13
+ "name"
14
+ ],
15
+ "ordering_fields": [
16
+ "value",
17
+ "name"
18
+ ],
19
+ "properties": {
20
+ "id": {
21
+ "type": "integer",
22
+ "title": "Id",
23
+ "required": true,
24
+ "description": null,
25
+ "nullable": false,
26
+ "format": "id",
27
+ "max_length": null,
28
+ "choices": null,
29
+ "default": null,
30
+ "validators": [],
31
+ "max_digits": null,
32
+ "decimal_places": null,
33
+ "read_only": true,
34
+ "ref": null
35
+ },
36
+ "name": {
37
+ "type": "string",
38
+ "title": "Name",
39
+ "required": true,
40
+ "description": null,
41
+ "nullable": false,
42
+ "format": null,
43
+ "max_length": 100,
44
+ "choices": null,
45
+ "default": null,
46
+ "validators": [],
47
+ "max_digits": null,
48
+ "decimal_places": null,
49
+ "read_only": false,
50
+ "ref": null
51
+ },
52
+ "value": {
53
+ "type": "integer",
54
+ "title": "Value",
55
+ "required": false,
56
+ "description": null,
57
+ "nullable": false,
58
+ "format": null,
59
+ "max_length": null,
60
+ "choices": null,
61
+ "default": 0,
62
+ "validators": [],
63
+ "max_digits": null,
64
+ "decimal_places": null,
65
+ "read_only": false,
66
+ "ref": null
67
+ }
68
+ },
69
+ "relationships": {},
70
+ "default_ordering": null,
71
+ "definitions": {
72
+ "MoneyField": {
73
+ "type": "object",
74
+ "properties": {
75
+ "amount": {
76
+ "type": "number"
77
+ },
78
+ "currency": {
79
+ "type": "string"
80
+ }
81
+ }
82
+ }
83
+ },
84
+ "datetime_format": "iso-8601",
85
+ "date_format": "iso-8601",
86
+ "time_format": "iso-8601",
87
+ "display": null
88
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Background check that compares stored schema checksums (from sync time)
3
+ * against the current checksums returned by the backend.
4
+ *
5
+ * Warns via console if schemas have drifted. Silently skips on network error.
6
+ *
7
+ * @param {Object.<string, Object>} backendConfigs - Backend configuration map from configInstance
8
+ * @param {Object.<string, string>} checksums - Map of backend key to checksum from sync time
9
+ */
10
+ export function checkSchemaIntegrity(backendConfigs: {
11
+ [x: string]: Object;
12
+ }, checksums: {
13
+ [x: string]: string;
14
+ }): Promise<void>;
@@ -0,0 +1,29 @@
1
+ import axios from "axios";
2
+ /**
3
+ * Background check that compares stored schema checksums (from sync time)
4
+ * against the current checksums returned by the backend.
5
+ *
6
+ * Warns via console if schemas have drifted. Silently skips on network error.
7
+ *
8
+ * @param {Object.<string, Object>} backendConfigs - Backend configuration map from configInstance
9
+ * @param {Object.<string, string>} checksums - Map of backend key to checksum from sync time
10
+ */
11
+ export async function checkSchemaIntegrity(backendConfigs, checksums) {
12
+ for (const [backendKey, expectedChecksum] of Object.entries(checksums)) {
13
+ const backend = backendConfigs[backendKey];
14
+ if (!backend)
15
+ continue;
16
+ try {
17
+ const headers = backend.getAuthHeaders ? backend.getAuthHeaders() : {};
18
+ const response = await axios.get(`${backend.API_URL}/models/`, { headers });
19
+ const currentChecksum = response.headers['x-statezero-schema-checksum'];
20
+ if (currentChecksum && currentChecksum !== expectedChecksum) {
21
+ console.warn(`[StateZero] Schema drift detected for backend '${backendKey}'. ` +
22
+ `Run 'npx statezero sync' to update your generated types.`);
23
+ }
24
+ }
25
+ catch {
26
+ // Silently skip — this is a non-blocking background check
27
+ }
28
+ }
29
+ }
package/dist/setup.js CHANGED
@@ -5,6 +5,7 @@ import { initEventHandler } from "./syncEngine/stores/operationEventHandlers.js"
5
5
  import { querysetStoreRegistry } from "./syncEngine/registries/querysetStoreRegistry.js";
6
6
  import { modelStoreRegistry } from "./syncEngine/registries/modelStoreRegistry.js";
7
7
  import { metricRegistry } from "./syncEngine/registries/metricRegistry.js";
8
+ import { checkSchemaIntegrity } from "./schemaIntegrity.js";
8
9
  /**
9
10
  * Initialize StateZero with the provided configuration
10
11
  *
@@ -30,4 +31,9 @@ export function setupStateZero(config, getModelClass, adapters) {
30
31
  metricRegistry,
31
32
  };
32
33
  }
34
+ // Background schema freshness check (fire-and-forget)
35
+ if (getModelClass.SYNC_CHECKSUMS) {
36
+ const cfg = configInstance.getConfig();
37
+ checkSchemaIntegrity(cfg.backendConfigs, getModelClass.SYNC_CHECKSUMS);
38
+ }
33
39
  }
@@ -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
@@ -203,7 +203,7 @@ function routeCreateOperation(operation, applyAction) {
203
203
  *
204
204
  * @param {Operation} operation - The UPDATE operation
205
205
  */
206
- function routeUpdateOperation(operation) {
206
+ function routeUpdateOperation(operation, applyAction) {
207
207
  const modelClass = operation.queryset.ModelClass;
208
208
  // frozenInstances is already serialized with proper FK structure
209
209
  const beforeInstances = operation.frozenInstances;
@@ -230,6 +230,8 @@ function routeUpdateOperation(operation) {
230
230
  return;
231
231
  }
232
232
  // Item matched before OR matches after, no offset - definitely affected
233
+ // Apply the operation so the queryset can add/remove PKs from its candidate set
234
+ applyAction(store);
233
235
  operationRegistry.setQuerysetState(operation.operationId, semanticKey, createMembershipState(true, false));
234
236
  });
235
237
  }
@@ -243,7 +245,7 @@ function routeUpdateOperation(operation) {
243
245
  *
244
246
  * @param {Operation} operation - The DELETE operation
245
247
  */
246
- function routeDeleteOperation(operation) {
248
+ function routeDeleteOperation(operation, applyAction) {
247
249
  const modelClass = operation.queryset.ModelClass;
248
250
  // frozenInstances is already serialized with proper FK structure
249
251
  const beforeInstances = operation.frozenInstances;
@@ -267,6 +269,7 @@ function routeDeleteOperation(operation) {
267
269
  return;
268
270
  }
269
271
  // Item matched before, no offset - definitely affected
272
+ applyAction(store);
270
273
  operationRegistry.setQuerysetState(operation.operationId, semanticKey, createMembershipState(true, false));
271
274
  });
272
275
  }
@@ -339,15 +342,14 @@ function processQuerysetStores(operation, actionType) {
339
342
  return;
340
343
  case Type.UPDATE:
341
344
  case Type.UPDATE_INSTANCE:
342
- // Track membership for sync optimization
343
- // Check both before (frozenInstances) and after (instances) state
344
- routeUpdateOperation(operation);
345
+ // Route update to affected querysets so items can move between filtered sets
346
+ // (e.g., an item changing name from 'beta' to 'alpha' needs to appear in the alpha queryset)
347
+ routeUpdateOperation(operation, applyAction);
345
348
  return;
346
349
  case Type.DELETE:
347
350
  case Type.DELETE_INSTANCE:
348
- // Track membership for sync optimization
349
- // Check before state only (item being deleted)
350
- routeDeleteOperation(operation);
351
+ // Add delete to matching queryset stores for optimistic removal
352
+ routeDeleteOperation(operation, applyAction);
351
353
  return;
352
354
  case Type.CHECKPOINT:
353
355
  // Model store handles the change, querysets re-render via local filtering
@@ -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) {
@@ -125,6 +131,10 @@ export class QuerysetStore {
125
131
  }
126
132
  async setGroundTruth(groundTruthPks) {
127
133
  this.groundTruthPks = Array.isArray(groundTruthPks) ? groundTruthPks : [];
134
+ if (this.groundTruthPks.length > 500) {
135
+ const limit = this.queryset?._serializerOptions?.limit;
136
+ console.warn(`[statezero] ⚠️ ${this.modelClass.modelName} (${this.queryset.semanticKey}) received ${this.groundTruthPks.length} items${limit ? ` (limit: ${limit})` : ' with NO limit'}. Live querysets re-render on every change — at this size, this WILL cause UI freezes on mobile. Add .fetch({ limit }) or use narrower filters.`);
137
+ }
128
138
  this.lastSync = Date.now();
129
139
  this._emitRenderEvent();
130
140
  }
@@ -203,7 +213,13 @@ export class QuerysetStore {
203
213
  // If no ground truth AND hasn't been synced, render from model store
204
214
  // This handles chained optimistic filters, newly created stores, etc.
205
215
  // (If synced with empty results, that's valid ground truth)
206
- 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
207
223
  ? this.renderFromModelStore()
208
224
  : this.renderFromData(optimistic);
209
225
  // Validate against model store and apply local filtering/sorting
@@ -269,8 +285,13 @@ export class QuerysetStore {
269
285
  currentPks.add(pk);
270
286
  break;
271
287
  case Type.CHECKPOINT:
288
+ break;
272
289
  case Type.UPDATE:
273
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);
274
295
  break;
275
296
  case Type.DELETE:
276
297
  case Type.DELETE_INSTANCE:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@statezero/core",
3
- "version": "0.2.51",
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",