@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.
- package/dist/adaptors/react/composables.d.ts +1 -0
- package/dist/adaptors/react/composables.js +54 -2
- package/dist/adaptors/react/index.d.ts +2 -1
- package/dist/adaptors/react/index.js +5 -1
- package/dist/adaptors/react/reactivity.d.ts +22 -0
- package/dist/adaptors/react/reactivity.js +145 -0
- package/dist/syncEngine/registries/metricRegistry.d.ts +5 -0
- package/dist/syncEngine/registries/metricRegistry.js +8 -0
- package/dist/syncEngine/registries/querysetStoreRegistry.d.ts +5 -0
- package/dist/syncEngine/registries/querysetStoreRegistry.js +32 -8
- package/package.json +1 -1
|
@@ -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
|
-
|
|
3
|
-
|
|
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
|
|
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 ===
|
|
44
|
+
if (prop === "touch") {
|
|
45
45
|
return () => this.touch();
|
|
46
46
|
}
|
|
47
|
-
if (prop ===
|
|
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 ===
|
|
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 ===
|
|
66
|
+
else if (prop === "length") {
|
|
58
67
|
return this.getCurrentItems().length;
|
|
59
68
|
}
|
|
60
|
-
else if (typeof 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
|
|
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