hyperstack-react 0.3.2 → 0.3.3
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/README.md +7 -0
- package/dist/index.d.ts +51 -28
- package/dist/index.esm.js +422 -149
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +422 -149
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.esm.js
CHANGED
|
@@ -6785,6 +6785,9 @@ function isGzipData(data) {
|
|
|
6785
6785
|
function isSnapshotFrame(frame) {
|
|
6786
6786
|
return frame.op === 'snapshot';
|
|
6787
6787
|
}
|
|
6788
|
+
function isSubscribedFrame(frame) {
|
|
6789
|
+
return frame.op === 'subscribed';
|
|
6790
|
+
}
|
|
6788
6791
|
function parseFrame(data) {
|
|
6789
6792
|
if (typeof data === 'string') {
|
|
6790
6793
|
return JSON.parse(data);
|
|
@@ -6893,7 +6896,6 @@ class ConnectionManager {
|
|
|
6893
6896
|
this.notifyFrameHandlers(frame);
|
|
6894
6897
|
}
|
|
6895
6898
|
catch (error) {
|
|
6896
|
-
console.error('[hyperstack] Error parsing frame:', error);
|
|
6897
6899
|
this.updateState('error', 'Failed to parse frame from server');
|
|
6898
6900
|
}
|
|
6899
6901
|
};
|
|
@@ -6932,21 +6934,15 @@ class ConnectionManager {
|
|
|
6932
6934
|
const subKey = this.makeSubKey(subscription);
|
|
6933
6935
|
if (this.currentState === 'connected' && this.ws?.readyState === WebSocket.OPEN) {
|
|
6934
6936
|
if (this.activeSubscriptions.has(subKey)) {
|
|
6935
|
-
console.log('[hyperstack] Skipping already active subscription:', subKey);
|
|
6936
6937
|
return;
|
|
6937
6938
|
}
|
|
6938
6939
|
const subMsg = { type: 'subscribe', ...subscription };
|
|
6939
|
-
console.log('[hyperstack] Sending subscribe:', subKey);
|
|
6940
6940
|
this.ws.send(JSON.stringify(subMsg));
|
|
6941
6941
|
this.activeSubscriptions.add(subKey);
|
|
6942
6942
|
}
|
|
6943
6943
|
else {
|
|
6944
6944
|
const alreadyQueued = this.subscriptionQueue.some((s) => this.makeSubKey(s) === subKey);
|
|
6945
|
-
if (alreadyQueued) {
|
|
6946
|
-
console.log('[hyperstack] Skipping duplicate queue entry:', subKey);
|
|
6947
|
-
}
|
|
6948
|
-
else {
|
|
6949
|
-
console.log('[hyperstack] Queuing subscription:', subKey, '| Queue:', this.subscriptionQueue.map(s => this.makeSubKey(s)));
|
|
6945
|
+
if (!alreadyQueued) {
|
|
6950
6946
|
this.subscriptionQueue.push(subscription);
|
|
6951
6947
|
}
|
|
6952
6948
|
}
|
|
@@ -6969,7 +6965,6 @@ class ConnectionManager {
|
|
|
6969
6965
|
return `${subscription.view}:${subscription.key ?? '*'}:${subscription.partition ?? ''}`;
|
|
6970
6966
|
}
|
|
6971
6967
|
flushSubscriptionQueue() {
|
|
6972
|
-
console.log('[hyperstack] Flushing subscription queue:', this.subscriptionQueue.map(s => this.makeSubKey(s)));
|
|
6973
6968
|
while (this.subscriptionQueue.length > 0) {
|
|
6974
6969
|
const sub = this.subscriptionQueue.shift();
|
|
6975
6970
|
if (sub) {
|
|
@@ -6978,7 +6973,6 @@ class ConnectionManager {
|
|
|
6978
6973
|
}
|
|
6979
6974
|
}
|
|
6980
6975
|
resubscribeActive() {
|
|
6981
|
-
console.log('[hyperstack] Resubscribing active:', Array.from(this.activeSubscriptions));
|
|
6982
6976
|
for (const subKey of this.activeSubscriptions) {
|
|
6983
6977
|
const [view, key, partition] = subKey.split(':');
|
|
6984
6978
|
const subscription = {
|
|
@@ -6988,7 +6982,6 @@ class ConnectionManager {
|
|
|
6988
6982
|
};
|
|
6989
6983
|
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
6990
6984
|
const subMsg = { type: 'subscribe', ...subscription };
|
|
6991
|
-
console.log('[hyperstack] Resubscribe sending:', subKey);
|
|
6992
6985
|
this.ws.send(JSON.stringify(subMsg));
|
|
6993
6986
|
}
|
|
6994
6987
|
}
|
|
@@ -7041,11 +7034,11 @@ class ConnectionManager {
|
|
|
7041
7034
|
}
|
|
7042
7035
|
}
|
|
7043
7036
|
|
|
7044
|
-
function isObject(item) {
|
|
7037
|
+
function isObject$1(item) {
|
|
7045
7038
|
return item !== null && typeof item === 'object' && !Array.isArray(item);
|
|
7046
7039
|
}
|
|
7047
|
-
function deepMergeWithAppend(target, source, appendPaths, currentPath = '') {
|
|
7048
|
-
if (!isObject(target) || !isObject(source)) {
|
|
7040
|
+
function deepMergeWithAppend$1(target, source, appendPaths, currentPath = '') {
|
|
7041
|
+
if (!isObject$1(target) || !isObject$1(source)) {
|
|
7049
7042
|
return source;
|
|
7050
7043
|
}
|
|
7051
7044
|
const result = { ...target };
|
|
@@ -7061,8 +7054,8 @@ function deepMergeWithAppend(target, source, appendPaths, currentPath = '') {
|
|
|
7061
7054
|
result[key] = sourceValue;
|
|
7062
7055
|
}
|
|
7063
7056
|
}
|
|
7064
|
-
else if (isObject(sourceValue) && isObject(targetValue)) {
|
|
7065
|
-
result[key] = deepMergeWithAppend(targetValue, sourceValue, appendPaths, fieldPath);
|
|
7057
|
+
else if (isObject$1(sourceValue) && isObject$1(targetValue)) {
|
|
7058
|
+
result[key] = deepMergeWithAppend$1(targetValue, sourceValue, appendPaths, fieldPath);
|
|
7066
7059
|
}
|
|
7067
7060
|
else {
|
|
7068
7061
|
result[key] = sourceValue;
|
|
@@ -7072,20 +7065,107 @@ function deepMergeWithAppend(target, source, appendPaths, currentPath = '') {
|
|
|
7072
7065
|
}
|
|
7073
7066
|
class FrameProcessor {
|
|
7074
7067
|
constructor(storage, config = {}) {
|
|
7068
|
+
this.pendingUpdates = [];
|
|
7069
|
+
this.flushTimer = null;
|
|
7070
|
+
this.isProcessing = false;
|
|
7075
7071
|
this.storage = storage;
|
|
7076
7072
|
this.maxEntriesPerView = config.maxEntriesPerView === undefined
|
|
7077
7073
|
? DEFAULT_MAX_ENTRIES_PER_VIEW
|
|
7078
7074
|
: config.maxEntriesPerView;
|
|
7075
|
+
this.flushIntervalMs = config.flushIntervalMs ?? 0;
|
|
7079
7076
|
}
|
|
7080
7077
|
handleFrame(frame) {
|
|
7081
|
-
if (
|
|
7078
|
+
if (this.flushIntervalMs === 0) {
|
|
7079
|
+
this.processFrame(frame);
|
|
7080
|
+
return;
|
|
7081
|
+
}
|
|
7082
|
+
this.pendingUpdates.push({ frame });
|
|
7083
|
+
this.scheduleFlush();
|
|
7084
|
+
}
|
|
7085
|
+
/**
|
|
7086
|
+
* Immediately flush all pending updates.
|
|
7087
|
+
* Useful for ensuring all updates are processed before reading state.
|
|
7088
|
+
*/
|
|
7089
|
+
flush() {
|
|
7090
|
+
if (this.flushTimer !== null) {
|
|
7091
|
+
clearTimeout(this.flushTimer);
|
|
7092
|
+
this.flushTimer = null;
|
|
7093
|
+
}
|
|
7094
|
+
this.flushPendingUpdates();
|
|
7095
|
+
}
|
|
7096
|
+
/**
|
|
7097
|
+
* Clean up any pending timers. Call when disposing the processor.
|
|
7098
|
+
*/
|
|
7099
|
+
dispose() {
|
|
7100
|
+
if (this.flushTimer !== null) {
|
|
7101
|
+
clearTimeout(this.flushTimer);
|
|
7102
|
+
this.flushTimer = null;
|
|
7103
|
+
}
|
|
7104
|
+
this.pendingUpdates = [];
|
|
7105
|
+
}
|
|
7106
|
+
scheduleFlush() {
|
|
7107
|
+
if (this.flushTimer !== null) {
|
|
7108
|
+
return;
|
|
7109
|
+
}
|
|
7110
|
+
this.flushTimer = setTimeout(() => {
|
|
7111
|
+
this.flushTimer = null;
|
|
7112
|
+
this.flushPendingUpdates();
|
|
7113
|
+
}, this.flushIntervalMs);
|
|
7114
|
+
}
|
|
7115
|
+
flushPendingUpdates() {
|
|
7116
|
+
if (this.isProcessing || this.pendingUpdates.length === 0) {
|
|
7117
|
+
return;
|
|
7118
|
+
}
|
|
7119
|
+
this.isProcessing = true;
|
|
7120
|
+
const batch = this.pendingUpdates;
|
|
7121
|
+
this.pendingUpdates = [];
|
|
7122
|
+
const viewsToEnforce = new Set();
|
|
7123
|
+
for (const { frame } of batch) {
|
|
7124
|
+
const viewPath = this.processFrameWithoutEnforce(frame);
|
|
7125
|
+
if (viewPath) {
|
|
7126
|
+
viewsToEnforce.add(viewPath);
|
|
7127
|
+
}
|
|
7128
|
+
}
|
|
7129
|
+
viewsToEnforce.forEach((viewPath) => {
|
|
7130
|
+
this.enforceMaxEntries(viewPath);
|
|
7131
|
+
});
|
|
7132
|
+
this.isProcessing = false;
|
|
7133
|
+
}
|
|
7134
|
+
processFrame(frame) {
|
|
7135
|
+
if (isSubscribedFrame(frame)) {
|
|
7136
|
+
this.handleSubscribedFrame(frame);
|
|
7137
|
+
}
|
|
7138
|
+
else if (isSnapshotFrame(frame)) {
|
|
7082
7139
|
this.handleSnapshotFrame(frame);
|
|
7083
7140
|
}
|
|
7084
7141
|
else {
|
|
7085
7142
|
this.handleEntityFrame(frame);
|
|
7086
7143
|
}
|
|
7087
7144
|
}
|
|
7145
|
+
processFrameWithoutEnforce(frame) {
|
|
7146
|
+
if (isSubscribedFrame(frame)) {
|
|
7147
|
+
this.handleSubscribedFrame(frame);
|
|
7148
|
+
return null;
|
|
7149
|
+
}
|
|
7150
|
+
else if (isSnapshotFrame(frame)) {
|
|
7151
|
+
this.handleSnapshotFrameWithoutEnforce(frame);
|
|
7152
|
+
return frame.entity;
|
|
7153
|
+
}
|
|
7154
|
+
else {
|
|
7155
|
+
this.handleEntityFrameWithoutEnforce(frame);
|
|
7156
|
+
return frame.entity;
|
|
7157
|
+
}
|
|
7158
|
+
}
|
|
7159
|
+
handleSubscribedFrame(frame) {
|
|
7160
|
+
if (this.storage.setViewConfig && frame.sort) {
|
|
7161
|
+
this.storage.setViewConfig(frame.view, { sort: frame.sort });
|
|
7162
|
+
}
|
|
7163
|
+
}
|
|
7088
7164
|
handleSnapshotFrame(frame) {
|
|
7165
|
+
this.handleSnapshotFrameWithoutEnforce(frame);
|
|
7166
|
+
this.enforceMaxEntries(frame.entity);
|
|
7167
|
+
}
|
|
7168
|
+
handleSnapshotFrameWithoutEnforce(frame) {
|
|
7089
7169
|
const viewPath = frame.entity;
|
|
7090
7170
|
for (const entity of frame.data) {
|
|
7091
7171
|
const previousValue = this.storage.get(viewPath, entity.key);
|
|
@@ -7097,16 +7177,18 @@ class FrameProcessor {
|
|
|
7097
7177
|
});
|
|
7098
7178
|
this.emitRichUpdate(viewPath, entity.key, previousValue, entity.data, 'upsert');
|
|
7099
7179
|
}
|
|
7100
|
-
this.enforceMaxEntries(viewPath);
|
|
7101
7180
|
}
|
|
7102
7181
|
handleEntityFrame(frame) {
|
|
7182
|
+
this.handleEntityFrameWithoutEnforce(frame);
|
|
7183
|
+
this.enforceMaxEntries(frame.entity);
|
|
7184
|
+
}
|
|
7185
|
+
handleEntityFrameWithoutEnforce(frame) {
|
|
7103
7186
|
const viewPath = frame.entity;
|
|
7104
7187
|
const previousValue = this.storage.get(viewPath, frame.key);
|
|
7105
7188
|
switch (frame.op) {
|
|
7106
7189
|
case 'create':
|
|
7107
7190
|
case 'upsert':
|
|
7108
7191
|
this.storage.set(viewPath, frame.key, frame.data);
|
|
7109
|
-
this.enforceMaxEntries(viewPath);
|
|
7110
7192
|
this.storage.notifyUpdate(viewPath, frame.key, {
|
|
7111
7193
|
type: 'upsert',
|
|
7112
7194
|
key: frame.key,
|
|
@@ -7118,10 +7200,9 @@ class FrameProcessor {
|
|
|
7118
7200
|
const existing = this.storage.get(viewPath, frame.key);
|
|
7119
7201
|
const appendPaths = frame.append ?? [];
|
|
7120
7202
|
const merged = existing
|
|
7121
|
-
? deepMergeWithAppend(existing, frame.data, appendPaths)
|
|
7203
|
+
? deepMergeWithAppend$1(existing, frame.data, appendPaths)
|
|
7122
7204
|
: frame.data;
|
|
7123
7205
|
this.storage.set(viewPath, frame.key, merged);
|
|
7124
|
-
this.enforceMaxEntries(viewPath);
|
|
7125
7206
|
this.storage.notifyUpdate(viewPath, frame.key, {
|
|
7126
7207
|
type: 'patch',
|
|
7127
7208
|
key: frame.key,
|
|
@@ -7160,7 +7241,7 @@ class FrameProcessor {
|
|
|
7160
7241
|
}
|
|
7161
7242
|
}
|
|
7162
7243
|
|
|
7163
|
-
class ViewData {
|
|
7244
|
+
let ViewData$1 = class ViewData {
|
|
7164
7245
|
constructor() {
|
|
7165
7246
|
this.entities = new Map();
|
|
7166
7247
|
this.accessOrder = [];
|
|
@@ -7214,7 +7295,7 @@ class ViewData {
|
|
|
7214
7295
|
this.entities.clear();
|
|
7215
7296
|
this.accessOrder = [];
|
|
7216
7297
|
}
|
|
7217
|
-
}
|
|
7298
|
+
};
|
|
7218
7299
|
class MemoryAdapter {
|
|
7219
7300
|
constructor(_config = {}) {
|
|
7220
7301
|
this.views = new Map();
|
|
@@ -7262,7 +7343,7 @@ class MemoryAdapter {
|
|
|
7262
7343
|
set(viewPath, key, data) {
|
|
7263
7344
|
let view = this.views.get(viewPath);
|
|
7264
7345
|
if (!view) {
|
|
7265
|
-
view = new ViewData();
|
|
7346
|
+
view = new ViewData$1();
|
|
7266
7347
|
this.views.set(viewPath, view);
|
|
7267
7348
|
}
|
|
7268
7349
|
view.set(key, data);
|
|
@@ -7603,6 +7684,60 @@ class HyperStack {
|
|
|
7603
7684
|
}
|
|
7604
7685
|
}
|
|
7605
7686
|
|
|
7687
|
+
function getNestedValue(obj, path) {
|
|
7688
|
+
let current = obj;
|
|
7689
|
+
for (const segment of path) {
|
|
7690
|
+
if (current === null || current === undefined)
|
|
7691
|
+
return undefined;
|
|
7692
|
+
if (typeof current !== 'object')
|
|
7693
|
+
return undefined;
|
|
7694
|
+
current = current[segment];
|
|
7695
|
+
}
|
|
7696
|
+
return current;
|
|
7697
|
+
}
|
|
7698
|
+
function compareSortValues(a, b) {
|
|
7699
|
+
if (a === b)
|
|
7700
|
+
return 0;
|
|
7701
|
+
if (a === undefined || a === null)
|
|
7702
|
+
return -1;
|
|
7703
|
+
if (b === undefined || b === null)
|
|
7704
|
+
return 1;
|
|
7705
|
+
if (typeof a === 'number' && typeof b === 'number') {
|
|
7706
|
+
return a - b;
|
|
7707
|
+
}
|
|
7708
|
+
if (typeof a === 'string' && typeof b === 'string') {
|
|
7709
|
+
return a.localeCompare(b);
|
|
7710
|
+
}
|
|
7711
|
+
if (typeof a === 'boolean' && typeof b === 'boolean') {
|
|
7712
|
+
return (a ? 1 : 0) - (b ? 1 : 0);
|
|
7713
|
+
}
|
|
7714
|
+
return String(a).localeCompare(String(b));
|
|
7715
|
+
}
|
|
7716
|
+
function binarySearchInsertPosition(sortedKeys, entities, sortConfig, newKey, newValue) {
|
|
7717
|
+
const newSortValue = getNestedValue(newValue, sortConfig.field);
|
|
7718
|
+
const isDesc = sortConfig.order === 'desc';
|
|
7719
|
+
let low = 0;
|
|
7720
|
+
let high = sortedKeys.length;
|
|
7721
|
+
while (low < high) {
|
|
7722
|
+
const mid = Math.floor((low + high) / 2);
|
|
7723
|
+
const midKey = sortedKeys[mid];
|
|
7724
|
+
const midEntity = entities.get(midKey);
|
|
7725
|
+
const midValue = getNestedValue(midEntity, sortConfig.field);
|
|
7726
|
+
let cmp = compareSortValues(newSortValue, midValue);
|
|
7727
|
+
if (isDesc)
|
|
7728
|
+
cmp = -cmp;
|
|
7729
|
+
if (cmp === 0) {
|
|
7730
|
+
cmp = newKey.localeCompare(midKey);
|
|
7731
|
+
}
|
|
7732
|
+
if (cmp < 0) {
|
|
7733
|
+
high = mid;
|
|
7734
|
+
}
|
|
7735
|
+
else {
|
|
7736
|
+
low = mid + 1;
|
|
7737
|
+
}
|
|
7738
|
+
}
|
|
7739
|
+
return low;
|
|
7740
|
+
}
|
|
7606
7741
|
class ZustandAdapter {
|
|
7607
7742
|
constructor(_config = {}) {
|
|
7608
7743
|
this.updateCallbacks = new Set();
|
|
@@ -7610,6 +7745,8 @@ class ZustandAdapter {
|
|
|
7610
7745
|
this.accessOrder = new Map();
|
|
7611
7746
|
this.store = create((set) => ({
|
|
7612
7747
|
entities: new Map(),
|
|
7748
|
+
sortedKeys: new Map(),
|
|
7749
|
+
viewConfigs: new Map(),
|
|
7613
7750
|
connectionState: 'disconnected',
|
|
7614
7751
|
lastError: undefined,
|
|
7615
7752
|
_set: (viewPath, key, data) => {
|
|
@@ -7638,14 +7775,30 @@ class ZustandAdapter {
|
|
|
7638
7775
|
if (viewPath) {
|
|
7639
7776
|
const newEntities = new Map(state.entities);
|
|
7640
7777
|
newEntities.delete(viewPath);
|
|
7641
|
-
|
|
7778
|
+
const newSortedKeys = new Map(state.sortedKeys);
|
|
7779
|
+
newSortedKeys.delete(viewPath);
|
|
7780
|
+
return { entities: newEntities, sortedKeys: newSortedKeys };
|
|
7642
7781
|
}
|
|
7643
|
-
return { entities: new Map() };
|
|
7782
|
+
return { entities: new Map(), sortedKeys: new Map() };
|
|
7644
7783
|
});
|
|
7645
7784
|
},
|
|
7646
7785
|
_setConnectionState: (connectionState, lastError) => {
|
|
7647
7786
|
set({ connectionState, lastError });
|
|
7648
7787
|
},
|
|
7788
|
+
_setViewConfig: (viewPath, config) => {
|
|
7789
|
+
set((state) => {
|
|
7790
|
+
const newConfigs = new Map(state.viewConfigs);
|
|
7791
|
+
newConfigs.set(viewPath, config);
|
|
7792
|
+
return { viewConfigs: newConfigs };
|
|
7793
|
+
});
|
|
7794
|
+
},
|
|
7795
|
+
_updateSortedKeys: (viewPath, newSortedKeys) => {
|
|
7796
|
+
set((state) => {
|
|
7797
|
+
const newSortedKeysMap = new Map(state.sortedKeys);
|
|
7798
|
+
newSortedKeysMap.set(viewPath, newSortedKeys);
|
|
7799
|
+
return { sortedKeys: newSortedKeysMap };
|
|
7800
|
+
});
|
|
7801
|
+
},
|
|
7649
7802
|
}));
|
|
7650
7803
|
}
|
|
7651
7804
|
get(viewPath, key) {
|
|
@@ -7657,17 +7810,25 @@ class ZustandAdapter {
|
|
|
7657
7810
|
return value !== undefined ? value : null;
|
|
7658
7811
|
}
|
|
7659
7812
|
getAll(viewPath) {
|
|
7660
|
-
const
|
|
7661
|
-
const viewMap = entities.get(viewPath);
|
|
7813
|
+
const state = this.store.getState();
|
|
7814
|
+
const viewMap = state.entities.get(viewPath);
|
|
7662
7815
|
if (!viewMap)
|
|
7663
7816
|
return [];
|
|
7817
|
+
const sortedKeys = state.sortedKeys.get(viewPath);
|
|
7818
|
+
if (sortedKeys && sortedKeys.length > 0) {
|
|
7819
|
+
return sortedKeys.map(k => viewMap.get(k)).filter(v => v !== undefined);
|
|
7820
|
+
}
|
|
7664
7821
|
return Array.from(viewMap.values());
|
|
7665
7822
|
}
|
|
7666
7823
|
getAllSync(viewPath) {
|
|
7667
|
-
const
|
|
7668
|
-
const viewMap = entities.get(viewPath);
|
|
7824
|
+
const state = this.store.getState();
|
|
7825
|
+
const viewMap = state.entities.get(viewPath);
|
|
7669
7826
|
if (!viewMap)
|
|
7670
7827
|
return undefined;
|
|
7828
|
+
const sortedKeys = state.sortedKeys.get(viewPath);
|
|
7829
|
+
if (sortedKeys && sortedKeys.length > 0) {
|
|
7830
|
+
return sortedKeys.map(k => viewMap.get(k)).filter(v => v !== undefined);
|
|
7831
|
+
}
|
|
7671
7832
|
return Array.from(viewMap.values());
|
|
7672
7833
|
}
|
|
7673
7834
|
getSync(viewPath, key) {
|
|
@@ -7691,27 +7852,56 @@ class ZustandAdapter {
|
|
|
7691
7852
|
return this.store.getState().entities.get(viewPath)?.size ?? 0;
|
|
7692
7853
|
}
|
|
7693
7854
|
set(viewPath, key, data) {
|
|
7694
|
-
|
|
7695
|
-
|
|
7696
|
-
|
|
7697
|
-
|
|
7855
|
+
const state = this.store.getState();
|
|
7856
|
+
const viewConfig = state.viewConfigs.get(viewPath);
|
|
7857
|
+
if (viewConfig?.sort) {
|
|
7858
|
+
const viewMap = state.entities.get(viewPath) ?? new Map();
|
|
7859
|
+
const currentSortedKeys = [...(state.sortedKeys.get(viewPath) ?? [])];
|
|
7860
|
+
const existingIdx = currentSortedKeys.indexOf(key);
|
|
7861
|
+
if (existingIdx !== -1) {
|
|
7862
|
+
currentSortedKeys.splice(existingIdx, 1);
|
|
7863
|
+
}
|
|
7864
|
+
const tempMap = new Map(viewMap);
|
|
7865
|
+
tempMap.set(key, data);
|
|
7866
|
+
const insertIdx = binarySearchInsertPosition(currentSortedKeys, tempMap, viewConfig.sort, key, data);
|
|
7867
|
+
currentSortedKeys.splice(insertIdx, 0, key);
|
|
7868
|
+
state._set(viewPath, key, data);
|
|
7869
|
+
state._updateSortedKeys(viewPath, currentSortedKeys);
|
|
7698
7870
|
}
|
|
7699
|
-
|
|
7700
|
-
|
|
7701
|
-
order
|
|
7871
|
+
else {
|
|
7872
|
+
let order = this.accessOrder.get(viewPath);
|
|
7873
|
+
if (!order) {
|
|
7874
|
+
order = [];
|
|
7875
|
+
this.accessOrder.set(viewPath, order);
|
|
7876
|
+
}
|
|
7877
|
+
const existingIdx = order.indexOf(key);
|
|
7878
|
+
if (existingIdx !== -1) {
|
|
7879
|
+
order.splice(existingIdx, 1);
|
|
7880
|
+
}
|
|
7881
|
+
order.push(key);
|
|
7882
|
+
state._set(viewPath, key, data);
|
|
7702
7883
|
}
|
|
7703
|
-
order.push(key);
|
|
7704
|
-
this.store.getState()._set(viewPath, key, data);
|
|
7705
7884
|
}
|
|
7706
7885
|
delete(viewPath, key) {
|
|
7707
|
-
const
|
|
7708
|
-
|
|
7709
|
-
|
|
7710
|
-
|
|
7711
|
-
|
|
7886
|
+
const state = this.store.getState();
|
|
7887
|
+
const viewConfig = state.viewConfigs.get(viewPath);
|
|
7888
|
+
if (viewConfig?.sort) {
|
|
7889
|
+
const currentSortedKeys = state.sortedKeys.get(viewPath);
|
|
7890
|
+
if (currentSortedKeys) {
|
|
7891
|
+
const newSortedKeys = currentSortedKeys.filter(k => k !== key);
|
|
7892
|
+
state._updateSortedKeys(viewPath, newSortedKeys);
|
|
7893
|
+
}
|
|
7894
|
+
}
|
|
7895
|
+
else {
|
|
7896
|
+
const order = this.accessOrder.get(viewPath);
|
|
7897
|
+
if (order) {
|
|
7898
|
+
const idx = order.indexOf(key);
|
|
7899
|
+
if (idx !== -1) {
|
|
7900
|
+
order.splice(idx, 1);
|
|
7901
|
+
}
|
|
7712
7902
|
}
|
|
7713
7903
|
}
|
|
7714
|
-
|
|
7904
|
+
state._delete(viewPath, key);
|
|
7715
7905
|
}
|
|
7716
7906
|
clear(viewPath) {
|
|
7717
7907
|
if (viewPath) {
|
|
@@ -7753,12 +7943,49 @@ class ZustandAdapter {
|
|
|
7753
7943
|
setConnectionState(state, error) {
|
|
7754
7944
|
this.store.getState()._setConnectionState(state, error);
|
|
7755
7945
|
}
|
|
7946
|
+
setViewConfig(viewPath, config) {
|
|
7947
|
+
const state = this.store.getState();
|
|
7948
|
+
const existingConfig = state.viewConfigs.get(viewPath);
|
|
7949
|
+
if (existingConfig?.sort)
|
|
7950
|
+
return;
|
|
7951
|
+
state._setViewConfig(viewPath, config);
|
|
7952
|
+
if (config.sort) {
|
|
7953
|
+
this.rebuildSortedKeys(viewPath, config.sort);
|
|
7954
|
+
}
|
|
7955
|
+
}
|
|
7956
|
+
getViewConfig(viewPath) {
|
|
7957
|
+
return this.store.getState().viewConfigs.get(viewPath);
|
|
7958
|
+
}
|
|
7959
|
+
rebuildSortedKeys(viewPath, sortConfig) {
|
|
7960
|
+
const state = this.store.getState();
|
|
7961
|
+
const viewMap = state.entities.get(viewPath);
|
|
7962
|
+
if (!viewMap || viewMap.size === 0)
|
|
7963
|
+
return;
|
|
7964
|
+
const isDesc = sortConfig.order === 'desc';
|
|
7965
|
+
const entries = Array.from(viewMap.entries());
|
|
7966
|
+
entries.sort((a, b) => {
|
|
7967
|
+
const aValue = getNestedValue(a[1], sortConfig.field);
|
|
7968
|
+
const bValue = getNestedValue(b[1], sortConfig.field);
|
|
7969
|
+
let cmp = compareSortValues(aValue, bValue);
|
|
7970
|
+
if (isDesc)
|
|
7971
|
+
cmp = -cmp;
|
|
7972
|
+
if (cmp === 0) {
|
|
7973
|
+
cmp = a[0].localeCompare(b[0]);
|
|
7974
|
+
}
|
|
7975
|
+
return cmp;
|
|
7976
|
+
});
|
|
7977
|
+
const sortedKeys = entries.map(([k]) => k);
|
|
7978
|
+
state._updateSortedKeys(viewPath, sortedKeys);
|
|
7979
|
+
}
|
|
7756
7980
|
}
|
|
7757
7981
|
|
|
7982
|
+
const DEFAULT_FLUSH_INTERVAL_MS = 16;
|
|
7983
|
+
|
|
7758
7984
|
function createRuntime(config) {
|
|
7759
7985
|
const adapter = new ZustandAdapter();
|
|
7760
7986
|
const processor = new FrameProcessor(adapter, {
|
|
7761
7987
|
maxEntriesPerView: config.maxEntriesPerView,
|
|
7988
|
+
flushIntervalMs: config.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS,
|
|
7762
7989
|
});
|
|
7763
7990
|
const connection = new ConnectionManager({
|
|
7764
7991
|
websocketUrl: config.websocketUrl,
|
|
@@ -7778,13 +8005,15 @@ function createRuntime(config) {
|
|
|
7778
8005
|
connection,
|
|
7779
8006
|
subscriptionRegistry,
|
|
7780
8007
|
wallet: config.wallet,
|
|
7781
|
-
subscribe(view, key, filters) {
|
|
7782
|
-
const subscription = { view, key, filters };
|
|
8008
|
+
subscribe(view, key, filters, take, skip) {
|
|
8009
|
+
const subscription = { view, key, filters, take, skip };
|
|
7783
8010
|
const unsubscribe = subscriptionRegistry.subscribe(subscription);
|
|
7784
8011
|
return {
|
|
7785
8012
|
view,
|
|
7786
8013
|
key,
|
|
7787
8014
|
filters,
|
|
8015
|
+
take,
|
|
8016
|
+
skip,
|
|
7788
8017
|
unsubscribe,
|
|
7789
8018
|
};
|
|
7790
8019
|
},
|
|
@@ -7921,7 +8150,7 @@ function createStateViewHook(viewDef, runtime) {
|
|
|
7921
8150
|
use: (key, options) => {
|
|
7922
8151
|
const [isLoading, setIsLoading] = useState(!options?.initialData);
|
|
7923
8152
|
const [error, setError] = useState();
|
|
7924
|
-
const keyString = Object.values(key)[0];
|
|
8153
|
+
const keyString = key ? Object.values(key)[0] : undefined;
|
|
7925
8154
|
const enabled = options?.enabled !== false;
|
|
7926
8155
|
useEffect(() => {
|
|
7927
8156
|
if (!enabled)
|
|
@@ -7968,8 +8197,14 @@ function createStateViewHook(viewDef, runtime) {
|
|
|
7968
8197
|
const unsubscribe = runtime.zustandStore.subscribe(callback);
|
|
7969
8198
|
return unsubscribe;
|
|
7970
8199
|
}, () => {
|
|
7971
|
-
const
|
|
7972
|
-
|
|
8200
|
+
const viewMap = runtime.zustandStore.getState().entities.get(viewDef.view);
|
|
8201
|
+
if (!viewMap)
|
|
8202
|
+
return undefined;
|
|
8203
|
+
if (keyString) {
|
|
8204
|
+
return viewMap.get(keyString);
|
|
8205
|
+
}
|
|
8206
|
+
const firstEntry = viewMap.values().next();
|
|
8207
|
+
return firstEntry.done ? undefined : firstEntry.value;
|
|
7973
8208
|
});
|
|
7974
8209
|
useEffect(() => {
|
|
7975
8210
|
if (data && isLoading) {
|
|
@@ -7985,113 +8220,147 @@ function createStateViewHook(viewDef, runtime) {
|
|
|
7985
8220
|
}
|
|
7986
8221
|
};
|
|
7987
8222
|
}
|
|
7988
|
-
function
|
|
7989
|
-
|
|
7990
|
-
|
|
7991
|
-
|
|
7992
|
-
|
|
7993
|
-
|
|
7994
|
-
|
|
7995
|
-
|
|
7996
|
-
|
|
7997
|
-
|
|
7998
|
-
|
|
7999
|
-
|
|
8000
|
-
|
|
8001
|
-
|
|
8223
|
+
function useListViewInternal(viewDef, runtime, params, options) {
|
|
8224
|
+
const [isLoading, setIsLoading] = useState(!options?.initialData);
|
|
8225
|
+
const [error, setError] = useState();
|
|
8226
|
+
const cachedDataRef = useRef(undefined);
|
|
8227
|
+
const lastMapRef = useRef(undefined);
|
|
8228
|
+
const lastSortedKeysRef = useRef(undefined);
|
|
8229
|
+
const enabled = options?.enabled !== false;
|
|
8230
|
+
const key = params?.key;
|
|
8231
|
+
const take = params?.take;
|
|
8232
|
+
const skip = params?.skip;
|
|
8233
|
+
const filtersJson = params?.filters ? JSON.stringify(params.filters) : undefined;
|
|
8234
|
+
const filters = useMemo(() => params?.filters, [filtersJson]);
|
|
8235
|
+
useEffect(() => {
|
|
8236
|
+
if (!enabled)
|
|
8237
|
+
return undefined;
|
|
8238
|
+
try {
|
|
8239
|
+
const handle = runtime.subscribe(viewDef.view, key, filters, take, skip);
|
|
8240
|
+
setIsLoading(true);
|
|
8241
|
+
return () => {
|
|
8002
8242
|
try {
|
|
8003
|
-
|
|
8004
|
-
setIsLoading(true);
|
|
8005
|
-
return () => {
|
|
8006
|
-
try {
|
|
8007
|
-
handle.unsubscribe();
|
|
8008
|
-
}
|
|
8009
|
-
catch (err) {
|
|
8010
|
-
console.error('[Hyperstack] Error unsubscribing from list view:', err);
|
|
8011
|
-
}
|
|
8012
|
-
};
|
|
8243
|
+
handle.unsubscribe();
|
|
8013
8244
|
}
|
|
8014
8245
|
catch (err) {
|
|
8015
|
-
|
|
8016
|
-
setIsLoading(false);
|
|
8017
|
-
return undefined;
|
|
8246
|
+
console.error('[Hyperstack] Error unsubscribing from list view:', err);
|
|
8018
8247
|
}
|
|
8019
|
-
}
|
|
8020
|
-
|
|
8021
|
-
|
|
8022
|
-
|
|
8248
|
+
};
|
|
8249
|
+
}
|
|
8250
|
+
catch (err) {
|
|
8251
|
+
setError(err instanceof Error ? err : new Error('Subscription failed'));
|
|
8252
|
+
setIsLoading(false);
|
|
8253
|
+
return undefined;
|
|
8254
|
+
}
|
|
8255
|
+
}, [enabled, key, filtersJson, take, skip]);
|
|
8256
|
+
const refresh = useCallback(() => {
|
|
8257
|
+
if (!enabled)
|
|
8258
|
+
return;
|
|
8259
|
+
try {
|
|
8260
|
+
const handle = runtime.subscribe(viewDef.view, key, filters, take, skip);
|
|
8261
|
+
setIsLoading(true);
|
|
8262
|
+
setTimeout(() => {
|
|
8023
8263
|
try {
|
|
8024
|
-
|
|
8025
|
-
setIsLoading(true);
|
|
8026
|
-
setTimeout(() => {
|
|
8027
|
-
try {
|
|
8028
|
-
handle.unsubscribe();
|
|
8029
|
-
}
|
|
8030
|
-
catch (err) {
|
|
8031
|
-
console.error('[Hyperstack] Error during list refresh unsubscribe:', err);
|
|
8032
|
-
}
|
|
8033
|
-
}, 0);
|
|
8264
|
+
handle.unsubscribe();
|
|
8034
8265
|
}
|
|
8035
8266
|
catch (err) {
|
|
8036
|
-
|
|
8037
|
-
setIsLoading(false);
|
|
8267
|
+
console.error('[Hyperstack] Error during list refresh unsubscribe:', err);
|
|
8038
8268
|
}
|
|
8039
|
-
},
|
|
8040
|
-
|
|
8041
|
-
|
|
8042
|
-
|
|
8043
|
-
|
|
8044
|
-
|
|
8045
|
-
|
|
8046
|
-
|
|
8047
|
-
|
|
8048
|
-
|
|
8269
|
+
}, 0);
|
|
8270
|
+
}
|
|
8271
|
+
catch (err) {
|
|
8272
|
+
setError(err instanceof Error ? err : new Error('Refresh failed'));
|
|
8273
|
+
setIsLoading(false);
|
|
8274
|
+
}
|
|
8275
|
+
}, [enabled, key, filtersJson, take, skip]);
|
|
8276
|
+
const data = useSyncExternalStore((callback) => {
|
|
8277
|
+
const unsubscribe = runtime.zustandStore.subscribe(callback);
|
|
8278
|
+
return unsubscribe;
|
|
8279
|
+
}, () => {
|
|
8280
|
+
const state = runtime.zustandStore.getState();
|
|
8281
|
+
const baseMap = state.entities.get(viewDef.view);
|
|
8282
|
+
const sortedKeys = state.sortedKeys.get(viewDef.view);
|
|
8283
|
+
if (!baseMap) {
|
|
8284
|
+
if (cachedDataRef.current !== undefined) {
|
|
8285
|
+
cachedDataRef.current = undefined;
|
|
8286
|
+
lastMapRef.current = undefined;
|
|
8287
|
+
lastSortedKeysRef.current = undefined;
|
|
8288
|
+
}
|
|
8289
|
+
return undefined;
|
|
8290
|
+
}
|
|
8291
|
+
if (lastMapRef.current === baseMap && lastSortedKeysRef.current === sortedKeys && cachedDataRef.current !== undefined) {
|
|
8292
|
+
return cachedDataRef.current;
|
|
8293
|
+
}
|
|
8294
|
+
let items;
|
|
8295
|
+
if (sortedKeys && sortedKeys.length > 0) {
|
|
8296
|
+
items = sortedKeys.map(k => baseMap.get(k)).filter(v => v !== undefined);
|
|
8297
|
+
}
|
|
8298
|
+
else {
|
|
8299
|
+
items = Array.from(baseMap.values());
|
|
8300
|
+
}
|
|
8301
|
+
if (params?.where) {
|
|
8302
|
+
items = items.filter((item) => {
|
|
8303
|
+
return Object.entries(params.where).every(([fieldKey, condition]) => {
|
|
8304
|
+
const value = item[fieldKey];
|
|
8305
|
+
if (typeof condition === 'object' && condition !== null) {
|
|
8306
|
+
const cond = condition;
|
|
8307
|
+
if ('gte' in cond)
|
|
8308
|
+
return value >= cond.gte;
|
|
8309
|
+
if ('lte' in cond)
|
|
8310
|
+
return value <= cond.lte;
|
|
8311
|
+
if ('gt' in cond)
|
|
8312
|
+
return value > cond.gt;
|
|
8313
|
+
if ('lt' in cond)
|
|
8314
|
+
return value < cond.lt;
|
|
8049
8315
|
}
|
|
8050
|
-
return
|
|
8051
|
-
}
|
|
8052
|
-
if (lastMapRef.current === baseMap && cachedDataRef.current !== undefined) {
|
|
8053
|
-
return cachedDataRef.current;
|
|
8054
|
-
}
|
|
8055
|
-
let items = Array.from(baseMap.values());
|
|
8056
|
-
if (params?.where) {
|
|
8057
|
-
items = items.filter((item) => {
|
|
8058
|
-
return Object.entries(params.where).every(([fieldKey, condition]) => {
|
|
8059
|
-
const value = item[fieldKey];
|
|
8060
|
-
if (typeof condition === 'object' && condition !== null) {
|
|
8061
|
-
const cond = condition;
|
|
8062
|
-
if ('gte' in cond)
|
|
8063
|
-
return value >= cond.gte;
|
|
8064
|
-
if ('lte' in cond)
|
|
8065
|
-
return value <= cond.lte;
|
|
8066
|
-
if ('gt' in cond)
|
|
8067
|
-
return value > cond.gt;
|
|
8068
|
-
if ('lt' in cond)
|
|
8069
|
-
return value < cond.lt;
|
|
8070
|
-
}
|
|
8071
|
-
return value === condition;
|
|
8072
|
-
});
|
|
8073
|
-
});
|
|
8074
|
-
}
|
|
8075
|
-
if (params?.limit) {
|
|
8076
|
-
items = items.slice(0, params.limit);
|
|
8077
|
-
}
|
|
8078
|
-
lastMapRef.current = runtime.zustandStore.getState().entities.get(viewDef.view);
|
|
8079
|
-
cachedDataRef.current = items;
|
|
8080
|
-
return items;
|
|
8316
|
+
return value === condition;
|
|
8317
|
+
});
|
|
8081
8318
|
});
|
|
8082
|
-
|
|
8083
|
-
|
|
8084
|
-
|
|
8085
|
-
|
|
8086
|
-
|
|
8319
|
+
}
|
|
8320
|
+
if (params?.limit) {
|
|
8321
|
+
items = items.slice(0, params.limit);
|
|
8322
|
+
}
|
|
8323
|
+
lastMapRef.current = baseMap;
|
|
8324
|
+
lastSortedKeysRef.current = sortedKeys;
|
|
8325
|
+
cachedDataRef.current = items;
|
|
8326
|
+
return items;
|
|
8327
|
+
});
|
|
8328
|
+
useEffect(() => {
|
|
8329
|
+
if (data && isLoading) {
|
|
8330
|
+
setIsLoading(false);
|
|
8331
|
+
}
|
|
8332
|
+
}, [data, isLoading]);
|
|
8333
|
+
return {
|
|
8334
|
+
data: (options?.initialData ?? data),
|
|
8335
|
+
isLoading,
|
|
8336
|
+
error,
|
|
8337
|
+
refresh
|
|
8338
|
+
};
|
|
8339
|
+
}
|
|
8340
|
+
function createListViewHook(viewDef, runtime) {
|
|
8341
|
+
function use(params, options) {
|
|
8342
|
+
const result = useListViewInternal(viewDef, runtime, params, options);
|
|
8343
|
+
if (params?.take === 1) {
|
|
8087
8344
|
return {
|
|
8088
|
-
data:
|
|
8089
|
-
isLoading,
|
|
8090
|
-
error,
|
|
8091
|
-
refresh
|
|
8345
|
+
data: result.data?.[0],
|
|
8346
|
+
isLoading: result.isLoading,
|
|
8347
|
+
error: result.error,
|
|
8348
|
+
refresh: result.refresh
|
|
8092
8349
|
};
|
|
8093
8350
|
}
|
|
8094
|
-
|
|
8351
|
+
return result;
|
|
8352
|
+
}
|
|
8353
|
+
function useOne(params, options) {
|
|
8354
|
+
const paramsWithTake = params ? { ...params, take: 1 } : { take: 1 };
|
|
8355
|
+
const result = useListViewInternal(viewDef, runtime, paramsWithTake, options);
|
|
8356
|
+
return {
|
|
8357
|
+
data: result.data?.[0],
|
|
8358
|
+
isLoading: result.isLoading,
|
|
8359
|
+
error: result.error,
|
|
8360
|
+
refresh: result.refresh
|
|
8361
|
+
};
|
|
8362
|
+
}
|
|
8363
|
+
return { use, useOne };
|
|
8095
8364
|
}
|
|
8096
8365
|
|
|
8097
8366
|
function createTxMutationHook(runtime, transactions) {
|
|
@@ -8180,11 +8449,15 @@ function useHyperstack(stack) {
|
|
|
8180
8449
|
views[viewName] = {};
|
|
8181
8450
|
if (typeof viewGroup === 'object' && viewGroup !== null) {
|
|
8182
8451
|
const group = viewGroup;
|
|
8183
|
-
|
|
8184
|
-
|
|
8185
|
-
|
|
8186
|
-
|
|
8187
|
-
|
|
8452
|
+
for (const [subViewName, viewDef] of Object.entries(group)) {
|
|
8453
|
+
if (!viewDef || typeof viewDef !== 'object' || !('mode' in viewDef))
|
|
8454
|
+
continue;
|
|
8455
|
+
if (viewDef.mode === 'state') {
|
|
8456
|
+
views[viewName][subViewName] = createStateViewHook(viewDef, runtime);
|
|
8457
|
+
}
|
|
8458
|
+
else if (viewDef.mode === 'list') {
|
|
8459
|
+
views[viewName][subViewName] = createListViewHook(viewDef, runtime);
|
|
8460
|
+
}
|
|
8188
8461
|
}
|
|
8189
8462
|
}
|
|
8190
8463
|
}
|