@statezero/core 0.1.30 → 0.1.32

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 +1,2 @@
1
1
  export function useQueryset(querysetFactory: any): any;
2
+ export const querysets: Map<any, any>;
@@ -1,4 +1,56 @@
1
+ import { useState, useEffect, useRef } from "react";
2
+ import { syncManager } from "../../syncEngine/sync";
3
+ import { v7 } from "uuid";
4
+ syncManager.followAllQuerysets = false;
5
+ export const querysets = new Map(); // Map of composableId -> queryset
6
+ function updateSyncManager() {
7
+ // Get unique querysets from all active composables
8
+ const uniqueQuerysets = new Set(querysets.values());
9
+ syncManager.followedQuerysets = uniqueQuerysets;
10
+ }
1
11
  export function useQueryset(querysetFactory) {
2
- console.warn("React environments are not currently supported");
3
- return querysetFactory;
12
+ const composableIdRef = useRef(v7());
13
+ const lastQuerysetRef = useRef(null);
14
+ const lastSemanticKeyRef = useRef(null);
15
+ const [, forceUpdate] = useState(0);
16
+ // Generate the queryset
17
+ const result = querysetFactory();
18
+ const original = result?.original || result;
19
+ const queryset = original?.queryset || original;
20
+ // Update tracking if queryset changed (compare by semantic key for stability)
21
+ const semanticKey = queryset?.semanticKey;
22
+ const lastSemanticKeyRef = useRef(null);
23
+ useEffect(() => {
24
+ if (lastSemanticKeyRef.current !== semanticKey) {
25
+ querysets.set(composableIdRef.current, queryset);
26
+ updateSyncManager();
27
+ lastSemanticKeyRef.current = semanticKey;
28
+ lastQuerysetRef.current = queryset;
29
+ }
30
+ }, [semanticKey, queryset]);
31
+ // Cleanup on unmount
32
+ useEffect(() => {
33
+ const composableId = composableIdRef.current;
34
+ return () => {
35
+ querysets.delete(composableId);
36
+ updateSyncManager();
37
+ };
38
+ }, []);
39
+ // Force re-render when queryset updates
40
+ useEffect(() => {
41
+ if (!result)
42
+ return;
43
+ // This will be triggered by the QuerySetAdaptor when data changes
44
+ const handleUpdate = () => {
45
+ forceUpdate((x) => x + 1);
46
+ };
47
+ // Attach update listener to the wrapped queryset (unconditionally)
48
+ result._reactUpdateHandler = handleUpdate;
49
+ return () => {
50
+ if (result) {
51
+ result._reactUpdateHandler = null;
52
+ }
53
+ };
54
+ }, [result]);
55
+ return result;
4
56
  }
@@ -1 +1,2 @@
1
- export { useQueryset } from "./composables.js";
1
+ export { useQueryset, querysets } from "./composables.js";
2
+ export { ModelAdaptor, QuerySetAdaptor, MetricAdaptor } from "./reactivity.js";
@@ -1 +1,5 @@
1
- export { useQueryset } from './composables.js';
1
+ export { useQueryset, querysets } from "./composables.js";
2
+ export { ModelAdaptor, QuerySetAdaptor, MetricAdaptor } from "./reactivity.js";
3
+ // src/react-entry.js
4
+ import { ModelAdaptor, QuerySetAdaptor, MetricAdaptor, useQueryset, querysets, } from "./adaptors/react/index.js";
5
+ export { ModelAdaptor, QuerySetAdaptor, MetricAdaptor, useQueryset, querysets };
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Adapts a model instance for React by wrapping it with event listeners
3
+ * that trigger React re-renders when the model updates.
4
+ *
5
+ * @param {Object} modelInstance - An instance of a model class with static modelName and primaryKeyField
6
+ * @returns {Object} The model instance with React update capabilities
7
+ */
8
+ export function ModelAdaptor(modelInstance: Object): Object;
9
+ /**
10
+ * Adapts a queryset for React and sets up event handling for queryset updates
11
+ *
12
+ * @param {Object} liveQuerySet - A LiveQueryset instance
13
+ * @returns {Array} The reactive queryset array
14
+ */
15
+ export function QuerySetAdaptor(liveQuerySet: Object): any[];
16
+ /**
17
+ * Adapts a metric for React and sets up event handling for metric updates
18
+ *
19
+ * @param {Object} metric - A metric instance
20
+ * @returns {Object} The reactive metric wrapper
21
+ */
22
+ export function MetricAdaptor(metric: Object): Object;
@@ -0,0 +1,145 @@
1
+ import { modelEventEmitter, querysetEventEmitter, metricEventEmitter, } from "../../syncEngine/stores/reactivity.js";
2
+ import { initEventHandler } from "../../syncEngine/stores/operationEventHandlers.js";
3
+ import { isEqual, isNil } from "lodash-es";
4
+ import hash from "object-hash";
5
+ initEventHandler(); // Initialize event handler for model events
6
+ const wrappedQuerysetCache = new Map();
7
+ const wrappedMetricCache = new Map();
8
+ /**
9
+ * Adapts a model instance for React by wrapping it with event listeners
10
+ * that trigger React re-renders when the model updates.
11
+ *
12
+ * @param {Object} modelInstance - An instance of a model class with static modelName and primaryKeyField
13
+ * @returns {Object} The model instance with React update capabilities
14
+ */
15
+ export function ModelAdaptor(modelInstance) {
16
+ const modelClass = modelInstance.constructor;
17
+ const modelName = modelClass.modelName;
18
+ const configKey = modelClass.configKey;
19
+ const pkField = modelClass.primaryKeyField;
20
+ // Create a wrapper that maintains the original instance
21
+ const wrapper = Object.create(modelInstance);
22
+ wrapper._reactUpdateHandler = null;
23
+ wrapper._reactVersion = 0;
24
+ const eventName = `${configKey}::${modelName}::render`;
25
+ // Handler triggers React update when this instance updates
26
+ const renderHandler = (eventData) => {
27
+ if (eventData.pk === modelInstance[pkField]) {
28
+ wrapper._reactVersion++;
29
+ if (wrapper._reactUpdateHandler) {
30
+ wrapper._reactUpdateHandler();
31
+ }
32
+ }
33
+ };
34
+ // Subscribe to model events indefinitely
35
+ modelEventEmitter.on(eventName, renderHandler);
36
+ // Store the cleanup function
37
+ wrapper._cleanup = () => {
38
+ modelEventEmitter.off(eventName, renderHandler);
39
+ };
40
+ return wrapper;
41
+ }
42
+ /**
43
+ * Adapts a queryset for React and sets up event handling for queryset updates
44
+ *
45
+ * @param {Object} liveQuerySet - A LiveQueryset instance
46
+ * @returns {Array} The reactive queryset array
47
+ */
48
+ export function QuerySetAdaptor(liveQuerySet) {
49
+ const queryset = liveQuerySet?.queryset;
50
+ const modelName = queryset?.ModelClass?.modelName;
51
+ const configKey = queryset?.ModelClass?.configKey;
52
+ if (isNil(queryset) || isNil(modelName)) {
53
+ throw new Error(`liveQuerySet ${JSON.stringify(liveQuerySet)} had null qs: ${queryset} or model: ${modelName}`);
54
+ }
55
+ // Use the semantic key if available
56
+ const cacheKey = queryset.semanticKey;
57
+ // Check the cache first
58
+ if (cacheKey && wrappedQuerysetCache.has(cacheKey)) {
59
+ return wrappedQuerysetCache.get(cacheKey);
60
+ }
61
+ const querysetAst = queryset.build();
62
+ // Create wrapper array that extends the live queryset
63
+ const wrapper = [...liveQuerySet];
64
+ wrapper.original = liveQuerySet;
65
+ wrapper._reactUpdateHandler = null;
66
+ wrapper._reactVersion = 0;
67
+ const eventName = `${configKey}::${modelName}::queryset::render`;
68
+ // Handler updates array and triggers React update when this queryset updates
69
+ const renderHandler = (eventData) => {
70
+ if (eventData && eventData.ast && isEqual(querysetAst, eventData.ast)) {
71
+ // Update the wrapper array
72
+ wrapper.length = 0;
73
+ wrapper.push(...liveQuerySet);
74
+ wrapper._reactVersion++;
75
+ // Trigger React update if handler is set
76
+ if (wrapper._reactUpdateHandler) {
77
+ wrapper._reactUpdateHandler();
78
+ }
79
+ }
80
+ };
81
+ // Subscribe to queryset events indefinitely
82
+ querysetEventEmitter.on(eventName, renderHandler);
83
+ // Store cleanup function
84
+ wrapper._cleanup = () => {
85
+ querysetEventEmitter.off(eventName, renderHandler);
86
+ };
87
+ // Cache if not empty (following Vue's pattern for stability)
88
+ if (cacheKey && liveQuerySet && liveQuerySet.length > 0) {
89
+ wrappedQuerysetCache.set(cacheKey, wrapper);
90
+ }
91
+ return wrapper;
92
+ }
93
+ /**
94
+ * Adapts a metric for React and sets up event handling for metric updates
95
+ *
96
+ * @param {Object} metric - A metric instance
97
+ * @returns {Object} The reactive metric wrapper
98
+ */
99
+ export function MetricAdaptor(metric) {
100
+ const queryset = metric.queryset;
101
+ const modelName = queryset?.ModelClass?.modelName;
102
+ const configKey = queryset?.ModelClass?.configKey;
103
+ const querysetAst = queryset.build();
104
+ // Create a cache key based on metric properties
105
+ const cacheKey = `${configKey}::${modelName}::${metric.metricType}::${metric.field}::${hash(querysetAst)}`;
106
+ // Check the cache first
107
+ if (cacheKey && wrappedMetricCache.has(cacheKey)) {
108
+ return wrappedMetricCache.get(cacheKey);
109
+ }
110
+ // Create a wrapper object that maintains the metric value
111
+ const wrapper = {
112
+ value: metric.value,
113
+ original: metric,
114
+ _reactUpdateHandler: null,
115
+ _reactVersion: 0,
116
+ };
117
+ // Single handler for metric render events
118
+ const metricRenderHandler = (eventData) => {
119
+ // Only update if this event is for our metric
120
+ if (eventData.metricType === metric.metricType &&
121
+ eventData.ModelClass === metric.queryset.ModelClass &&
122
+ eventData.field === metric.field &&
123
+ eventData.ast === hash(querysetAst) &&
124
+ eventData.valueChanged === true) {
125
+ // Update the wrapper value with the latest metric value
126
+ wrapper.value = metric.value;
127
+ wrapper._reactVersion++;
128
+ // Trigger React update if handler is set
129
+ if (wrapper._reactUpdateHandler) {
130
+ wrapper._reactUpdateHandler();
131
+ }
132
+ }
133
+ };
134
+ // Only listen for metric render events
135
+ metricEventEmitter.on("metric::render", metricRenderHandler);
136
+ // Store cleanup function
137
+ wrapper._cleanup = () => {
138
+ metricEventEmitter.off("metric::render", metricRenderHandler);
139
+ };
140
+ // Store in cache
141
+ if (cacheKey) {
142
+ wrappedMetricCache.set(cacheKey, wrapper);
143
+ }
144
+ return wrapper;
145
+ }
@@ -8,6 +8,11 @@ export class LiveMetric {
8
8
  metricType: any;
9
9
  field: any;
10
10
  get lqs(): import("./querysetStoreRegistry").LiveQueryset;
11
+ /**
12
+ * Refresh the metric data from the database
13
+ * Delegates to the underlying store's sync method
14
+ */
15
+ refreshFromDb(): any;
11
16
  /**
12
17
  * Getter that always returns the current value from the store
13
18
  */
@@ -17,6 +17,14 @@ export class LiveMetric {
17
17
  get lqs() {
18
18
  return querysetStoreRegistry.getEntity(this.queryset);
19
19
  }
20
+ /**
21
+ * Refresh the metric data from the database
22
+ * Delegates to the underlying store's sync method
23
+ */
24
+ refreshFromDb() {
25
+ const store = metricRegistry.getStore(this.metricType, this.queryset, this.field);
26
+ return store.sync();
27
+ }
20
28
  /**
21
29
  * Getter that always returns the current value from the store
22
30
  */
@@ -9,6 +9,11 @@ export class LiveQueryset {
9
9
  * Serializes the lqs as a simple array of objects, for freezing e.g in the metric stores
10
10
  */
11
11
  serialize(): any;
12
+ /**
13
+ * Refresh the queryset data from the database
14
+ * Delegates to the underlying store's sync method
15
+ */
16
+ refreshFromDb(): any;
12
17
  /**
13
18
  * Get the current items from the store
14
19
  * @private
@@ -41,28 +41,37 @@ export class LiveQueryset {
41
41
  __classPrivateFieldSet(this, _LiveQueryset_proxy, new Proxy(__classPrivateFieldGet(this, _LiveQueryset_array, "f"), {
42
42
  get: (target, prop, receiver) => {
43
43
  // Expose the touch method through the proxy
44
- if (prop === 'touch') {
44
+ if (prop === "touch") {
45
45
  return () => this.touch();
46
46
  }
47
- if (prop === 'serialize') {
47
+ if (prop === "serialize") {
48
48
  return () => this.serialize();
49
49
  }
50
50
  // Special handling for iterators and common array methods
51
51
  if (prop === Symbol.iterator) {
52
52
  return () => this.getCurrentItems()[Symbol.iterator]();
53
53
  }
54
- else if (typeof prop === 'string' && ['forEach', 'map', 'filter', 'reduce', 'some', 'every', 'find'].includes(prop)) {
54
+ else if (typeof prop === "string" &&
55
+ [
56
+ "forEach",
57
+ "map",
58
+ "filter",
59
+ "reduce",
60
+ "some",
61
+ "every",
62
+ "find",
63
+ ].includes(prop)) {
55
64
  return (...args) => this.getCurrentItems()[prop](...args);
56
65
  }
57
- else if (prop === 'length') {
66
+ else if (prop === "length") {
58
67
  return this.getCurrentItems().length;
59
68
  }
60
- else if (typeof prop === 'string' && !isNaN(parseInt(prop))) {
69
+ else if (typeof prop === "string" && !isNaN(parseInt(prop))) {
61
70
  // Handle numeric indices
62
71
  return this.getCurrentItems()[prop];
63
72
  }
64
73
  return target[prop];
65
- }
74
+ },
66
75
  }), "f");
67
76
  return __classPrivateFieldGet(this, _LiveQueryset_proxy, "f");
68
77
  }
@@ -74,12 +83,20 @@ export class LiveQueryset {
74
83
  // Get the current primary keys from the store
75
84
  const pks = store.render();
76
85
  // Map primary keys to full model objects
77
- return pks.map(pk => {
86
+ return pks.map((pk) => {
78
87
  // Get the full model instance from the model store
79
88
  const pkField = __classPrivateFieldGet(this, _LiveQueryset_ModelClass, "f").primaryKeyField;
80
89
  return __classPrivateFieldGet(this, _LiveQueryset_ModelClass, "f").fromPk(pk, __classPrivateFieldGet(this, _LiveQueryset_queryset, "f")).serialize();
81
90
  });
82
91
  }
92
+ /**
93
+ * Refresh the queryset data from the database
94
+ * Delegates to the underlying store's sync method
95
+ */
96
+ refreshFromDb() {
97
+ const store = querysetStoreRegistry.getStore(__classPrivateFieldGet(this, _LiveQueryset_queryset, "f"));
98
+ return store.sync();
99
+ }
83
100
  /**
84
101
  * Get the current items from the store
85
102
  * @private
@@ -90,10 +107,17 @@ export class LiveQueryset {
90
107
  // Get the current primary keys from the store
91
108
  const pks = store.render();
92
109
  // Map primary keys to full model objects
93
- const instances = pks.map(pk => {
110
+ const instances = pks
111
+ .map((pk) => {
94
112
  // Get the full model instance from the model store
95
113
  const pkField = __classPrivateFieldGet(this, _LiveQueryset_ModelClass, "f").primaryKeyField;
96
114
  return __classPrivateFieldGet(this, _LiveQueryset_ModelClass, "f").fromPk(pk, __classPrivateFieldGet(this, _LiveQueryset_queryset, "f"));
115
+ })
116
+ .filter((instance) => {
117
+ // Filter out ghost entries - instances where the model data doesn't exist i.e because it has
118
+ // been deleted on the backend but the event hasn't been propagated to this queryset
119
+ const storedData = modelStoreRegistry.getEntity(__classPrivateFieldGet(this, _LiveQueryset_ModelClass, "f"), instance.pk);
120
+ return storedData !== null && storedData !== undefined;
97
121
  });
98
122
  if (!sortAndFilter)
99
123
  return instances;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@statezero/core",
3
- "version": "0.1.30",
3
+ "version": "0.1.32",
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",