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