hyperstack-react 0.3.1 → 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 +442 -168
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +442 -168
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.esm.js
CHANGED
|
@@ -6777,40 +6777,29 @@ class HyperStackError extends Error {
|
|
|
6777
6777
|
}
|
|
6778
6778
|
}
|
|
6779
6779
|
|
|
6780
|
-
|
|
6781
|
-
|
|
6782
|
-
|
|
6783
|
-
|
|
6784
|
-
typeof obj.data === 'string');
|
|
6785
|
-
}
|
|
6786
|
-
function decompressGzip(base64Data) {
|
|
6787
|
-
const binaryString = atob(base64Data);
|
|
6788
|
-
const bytes = new Uint8Array(binaryString.length);
|
|
6789
|
-
for (let i = 0; i < binaryString.length; i++) {
|
|
6790
|
-
bytes[i] = binaryString.charCodeAt(i);
|
|
6791
|
-
}
|
|
6792
|
-
const decompressed = inflate_1(bytes);
|
|
6793
|
-
return new TextDecoder().decode(decompressed);
|
|
6794
|
-
}
|
|
6795
|
-
function parseAndDecompress(jsonString) {
|
|
6796
|
-
const parsed = JSON.parse(jsonString);
|
|
6797
|
-
if (isCompressedFrame(parsed)) {
|
|
6798
|
-
const decompressedJson = decompressGzip(parsed.data);
|
|
6799
|
-
const frame = JSON.parse(decompressedJson);
|
|
6800
|
-
return frame;
|
|
6801
|
-
}
|
|
6802
|
-
return parsed;
|
|
6780
|
+
const GZIP_MAGIC_0 = 0x1f;
|
|
6781
|
+
const GZIP_MAGIC_1 = 0x8b;
|
|
6782
|
+
function isGzipData(data) {
|
|
6783
|
+
return data.length >= 2 && data[0] === GZIP_MAGIC_0 && data[1] === GZIP_MAGIC_1;
|
|
6803
6784
|
}
|
|
6804
6785
|
function isSnapshotFrame(frame) {
|
|
6805
6786
|
return frame.op === 'snapshot';
|
|
6806
6787
|
}
|
|
6788
|
+
function isSubscribedFrame(frame) {
|
|
6789
|
+
return frame.op === 'subscribed';
|
|
6790
|
+
}
|
|
6807
6791
|
function parseFrame(data) {
|
|
6808
6792
|
if (typeof data === 'string') {
|
|
6809
|
-
return
|
|
6793
|
+
return JSON.parse(data);
|
|
6810
6794
|
}
|
|
6811
|
-
const
|
|
6812
|
-
|
|
6813
|
-
|
|
6795
|
+
const bytes = new Uint8Array(data);
|
|
6796
|
+
if (isGzipData(bytes)) {
|
|
6797
|
+
const decompressed = inflate_1(bytes);
|
|
6798
|
+
const jsonString = new TextDecoder().decode(decompressed);
|
|
6799
|
+
return JSON.parse(jsonString);
|
|
6800
|
+
}
|
|
6801
|
+
const jsonString = new TextDecoder('utf-8').decode(data);
|
|
6802
|
+
return JSON.parse(jsonString);
|
|
6814
6803
|
}
|
|
6815
6804
|
async function parseFrameFromBlob(blob) {
|
|
6816
6805
|
const arrayBuffer = await blob.arrayBuffer();
|
|
@@ -6885,8 +6874,8 @@ class ConnectionManager {
|
|
|
6885
6874
|
this.reconnectAttempts = 0;
|
|
6886
6875
|
this.updateState('connected');
|
|
6887
6876
|
this.startPingInterval();
|
|
6888
|
-
this.flushSubscriptionQueue();
|
|
6889
6877
|
this.resubscribeActive();
|
|
6878
|
+
this.flushSubscriptionQueue();
|
|
6890
6879
|
resolve();
|
|
6891
6880
|
};
|
|
6892
6881
|
this.ws.onmessage = async (event) => {
|
|
@@ -6907,7 +6896,6 @@ class ConnectionManager {
|
|
|
6907
6896
|
this.notifyFrameHandlers(frame);
|
|
6908
6897
|
}
|
|
6909
6898
|
catch (error) {
|
|
6910
|
-
console.error('[hyperstack] Error parsing frame:', error);
|
|
6911
6899
|
this.updateState('error', 'Failed to parse frame from server');
|
|
6912
6900
|
}
|
|
6913
6901
|
};
|
|
@@ -6945,12 +6933,18 @@ class ConnectionManager {
|
|
|
6945
6933
|
subscribe(subscription) {
|
|
6946
6934
|
const subKey = this.makeSubKey(subscription);
|
|
6947
6935
|
if (this.currentState === 'connected' && this.ws?.readyState === WebSocket.OPEN) {
|
|
6936
|
+
if (this.activeSubscriptions.has(subKey)) {
|
|
6937
|
+
return;
|
|
6938
|
+
}
|
|
6948
6939
|
const subMsg = { type: 'subscribe', ...subscription };
|
|
6949
6940
|
this.ws.send(JSON.stringify(subMsg));
|
|
6950
6941
|
this.activeSubscriptions.add(subKey);
|
|
6951
6942
|
}
|
|
6952
6943
|
else {
|
|
6953
|
-
this.subscriptionQueue.
|
|
6944
|
+
const alreadyQueued = this.subscriptionQueue.some((s) => this.makeSubKey(s) === subKey);
|
|
6945
|
+
if (!alreadyQueued) {
|
|
6946
|
+
this.subscriptionQueue.push(subscription);
|
|
6947
|
+
}
|
|
6954
6948
|
}
|
|
6955
6949
|
}
|
|
6956
6950
|
unsubscribe(view, key) {
|
|
@@ -7040,11 +7034,11 @@ class ConnectionManager {
|
|
|
7040
7034
|
}
|
|
7041
7035
|
}
|
|
7042
7036
|
|
|
7043
|
-
function isObject(item) {
|
|
7037
|
+
function isObject$1(item) {
|
|
7044
7038
|
return item !== null && typeof item === 'object' && !Array.isArray(item);
|
|
7045
7039
|
}
|
|
7046
|
-
function deepMergeWithAppend(target, source, appendPaths, currentPath = '') {
|
|
7047
|
-
if (!isObject(target) || !isObject(source)) {
|
|
7040
|
+
function deepMergeWithAppend$1(target, source, appendPaths, currentPath = '') {
|
|
7041
|
+
if (!isObject$1(target) || !isObject$1(source)) {
|
|
7048
7042
|
return source;
|
|
7049
7043
|
}
|
|
7050
7044
|
const result = { ...target };
|
|
@@ -7060,8 +7054,8 @@ function deepMergeWithAppend(target, source, appendPaths, currentPath = '') {
|
|
|
7060
7054
|
result[key] = sourceValue;
|
|
7061
7055
|
}
|
|
7062
7056
|
}
|
|
7063
|
-
else if (isObject(sourceValue) && isObject(targetValue)) {
|
|
7064
|
-
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);
|
|
7065
7059
|
}
|
|
7066
7060
|
else {
|
|
7067
7061
|
result[key] = sourceValue;
|
|
@@ -7071,20 +7065,107 @@ function deepMergeWithAppend(target, source, appendPaths, currentPath = '') {
|
|
|
7071
7065
|
}
|
|
7072
7066
|
class FrameProcessor {
|
|
7073
7067
|
constructor(storage, config = {}) {
|
|
7068
|
+
this.pendingUpdates = [];
|
|
7069
|
+
this.flushTimer = null;
|
|
7070
|
+
this.isProcessing = false;
|
|
7074
7071
|
this.storage = storage;
|
|
7075
7072
|
this.maxEntriesPerView = config.maxEntriesPerView === undefined
|
|
7076
7073
|
? DEFAULT_MAX_ENTRIES_PER_VIEW
|
|
7077
7074
|
: config.maxEntriesPerView;
|
|
7075
|
+
this.flushIntervalMs = config.flushIntervalMs ?? 0;
|
|
7078
7076
|
}
|
|
7079
7077
|
handleFrame(frame) {
|
|
7080
|
-
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)) {
|
|
7081
7139
|
this.handleSnapshotFrame(frame);
|
|
7082
7140
|
}
|
|
7083
7141
|
else {
|
|
7084
7142
|
this.handleEntityFrame(frame);
|
|
7085
7143
|
}
|
|
7086
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
|
+
}
|
|
7087
7164
|
handleSnapshotFrame(frame) {
|
|
7165
|
+
this.handleSnapshotFrameWithoutEnforce(frame);
|
|
7166
|
+
this.enforceMaxEntries(frame.entity);
|
|
7167
|
+
}
|
|
7168
|
+
handleSnapshotFrameWithoutEnforce(frame) {
|
|
7088
7169
|
const viewPath = frame.entity;
|
|
7089
7170
|
for (const entity of frame.data) {
|
|
7090
7171
|
const previousValue = this.storage.get(viewPath, entity.key);
|
|
@@ -7096,16 +7177,18 @@ class FrameProcessor {
|
|
|
7096
7177
|
});
|
|
7097
7178
|
this.emitRichUpdate(viewPath, entity.key, previousValue, entity.data, 'upsert');
|
|
7098
7179
|
}
|
|
7099
|
-
this.enforceMaxEntries(viewPath);
|
|
7100
7180
|
}
|
|
7101
7181
|
handleEntityFrame(frame) {
|
|
7182
|
+
this.handleEntityFrameWithoutEnforce(frame);
|
|
7183
|
+
this.enforceMaxEntries(frame.entity);
|
|
7184
|
+
}
|
|
7185
|
+
handleEntityFrameWithoutEnforce(frame) {
|
|
7102
7186
|
const viewPath = frame.entity;
|
|
7103
7187
|
const previousValue = this.storage.get(viewPath, frame.key);
|
|
7104
7188
|
switch (frame.op) {
|
|
7105
7189
|
case 'create':
|
|
7106
7190
|
case 'upsert':
|
|
7107
7191
|
this.storage.set(viewPath, frame.key, frame.data);
|
|
7108
|
-
this.enforceMaxEntries(viewPath);
|
|
7109
7192
|
this.storage.notifyUpdate(viewPath, frame.key, {
|
|
7110
7193
|
type: 'upsert',
|
|
7111
7194
|
key: frame.key,
|
|
@@ -7117,10 +7200,9 @@ class FrameProcessor {
|
|
|
7117
7200
|
const existing = this.storage.get(viewPath, frame.key);
|
|
7118
7201
|
const appendPaths = frame.append ?? [];
|
|
7119
7202
|
const merged = existing
|
|
7120
|
-
? deepMergeWithAppend(existing, frame.data, appendPaths)
|
|
7203
|
+
? deepMergeWithAppend$1(existing, frame.data, appendPaths)
|
|
7121
7204
|
: frame.data;
|
|
7122
7205
|
this.storage.set(viewPath, frame.key, merged);
|
|
7123
|
-
this.enforceMaxEntries(viewPath);
|
|
7124
7206
|
this.storage.notifyUpdate(viewPath, frame.key, {
|
|
7125
7207
|
type: 'patch',
|
|
7126
7208
|
key: frame.key,
|
|
@@ -7159,7 +7241,7 @@ class FrameProcessor {
|
|
|
7159
7241
|
}
|
|
7160
7242
|
}
|
|
7161
7243
|
|
|
7162
|
-
class ViewData {
|
|
7244
|
+
let ViewData$1 = class ViewData {
|
|
7163
7245
|
constructor() {
|
|
7164
7246
|
this.entities = new Map();
|
|
7165
7247
|
this.accessOrder = [];
|
|
@@ -7213,7 +7295,7 @@ class ViewData {
|
|
|
7213
7295
|
this.entities.clear();
|
|
7214
7296
|
this.accessOrder = [];
|
|
7215
7297
|
}
|
|
7216
|
-
}
|
|
7298
|
+
};
|
|
7217
7299
|
class MemoryAdapter {
|
|
7218
7300
|
constructor(_config = {}) {
|
|
7219
7301
|
this.views = new Map();
|
|
@@ -7261,7 +7343,7 @@ class MemoryAdapter {
|
|
|
7261
7343
|
set(viewPath, key, data) {
|
|
7262
7344
|
let view = this.views.get(viewPath);
|
|
7263
7345
|
if (!view) {
|
|
7264
|
-
view = new ViewData();
|
|
7346
|
+
view = new ViewData$1();
|
|
7265
7347
|
this.views.set(viewPath, view);
|
|
7266
7348
|
}
|
|
7267
7349
|
view.set(key, data);
|
|
@@ -7602,6 +7684,60 @@ class HyperStack {
|
|
|
7602
7684
|
}
|
|
7603
7685
|
}
|
|
7604
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
|
+
}
|
|
7605
7741
|
class ZustandAdapter {
|
|
7606
7742
|
constructor(_config = {}) {
|
|
7607
7743
|
this.updateCallbacks = new Set();
|
|
@@ -7609,6 +7745,8 @@ class ZustandAdapter {
|
|
|
7609
7745
|
this.accessOrder = new Map();
|
|
7610
7746
|
this.store = create((set) => ({
|
|
7611
7747
|
entities: new Map(),
|
|
7748
|
+
sortedKeys: new Map(),
|
|
7749
|
+
viewConfigs: new Map(),
|
|
7612
7750
|
connectionState: 'disconnected',
|
|
7613
7751
|
lastError: undefined,
|
|
7614
7752
|
_set: (viewPath, key, data) => {
|
|
@@ -7637,14 +7775,30 @@ class ZustandAdapter {
|
|
|
7637
7775
|
if (viewPath) {
|
|
7638
7776
|
const newEntities = new Map(state.entities);
|
|
7639
7777
|
newEntities.delete(viewPath);
|
|
7640
|
-
|
|
7778
|
+
const newSortedKeys = new Map(state.sortedKeys);
|
|
7779
|
+
newSortedKeys.delete(viewPath);
|
|
7780
|
+
return { entities: newEntities, sortedKeys: newSortedKeys };
|
|
7641
7781
|
}
|
|
7642
|
-
return { entities: new Map() };
|
|
7782
|
+
return { entities: new Map(), sortedKeys: new Map() };
|
|
7643
7783
|
});
|
|
7644
7784
|
},
|
|
7645
7785
|
_setConnectionState: (connectionState, lastError) => {
|
|
7646
7786
|
set({ connectionState, lastError });
|
|
7647
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
|
+
},
|
|
7648
7802
|
}));
|
|
7649
7803
|
}
|
|
7650
7804
|
get(viewPath, key) {
|
|
@@ -7656,17 +7810,25 @@ class ZustandAdapter {
|
|
|
7656
7810
|
return value !== undefined ? value : null;
|
|
7657
7811
|
}
|
|
7658
7812
|
getAll(viewPath) {
|
|
7659
|
-
const
|
|
7660
|
-
const viewMap = entities.get(viewPath);
|
|
7813
|
+
const state = this.store.getState();
|
|
7814
|
+
const viewMap = state.entities.get(viewPath);
|
|
7661
7815
|
if (!viewMap)
|
|
7662
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
|
+
}
|
|
7663
7821
|
return Array.from(viewMap.values());
|
|
7664
7822
|
}
|
|
7665
7823
|
getAllSync(viewPath) {
|
|
7666
|
-
const
|
|
7667
|
-
const viewMap = entities.get(viewPath);
|
|
7824
|
+
const state = this.store.getState();
|
|
7825
|
+
const viewMap = state.entities.get(viewPath);
|
|
7668
7826
|
if (!viewMap)
|
|
7669
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
|
+
}
|
|
7670
7832
|
return Array.from(viewMap.values());
|
|
7671
7833
|
}
|
|
7672
7834
|
getSync(viewPath, key) {
|
|
@@ -7690,27 +7852,56 @@ class ZustandAdapter {
|
|
|
7690
7852
|
return this.store.getState().entities.get(viewPath)?.size ?? 0;
|
|
7691
7853
|
}
|
|
7692
7854
|
set(viewPath, key, data) {
|
|
7693
|
-
|
|
7694
|
-
|
|
7695
|
-
|
|
7696
|
-
|
|
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);
|
|
7697
7870
|
}
|
|
7698
|
-
|
|
7699
|
-
|
|
7700
|
-
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);
|
|
7701
7883
|
}
|
|
7702
|
-
order.push(key);
|
|
7703
|
-
this.store.getState()._set(viewPath, key, data);
|
|
7704
7884
|
}
|
|
7705
7885
|
delete(viewPath, key) {
|
|
7706
|
-
const
|
|
7707
|
-
|
|
7708
|
-
|
|
7709
|
-
|
|
7710
|
-
|
|
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);
|
|
7711
7893
|
}
|
|
7712
7894
|
}
|
|
7713
|
-
|
|
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
|
+
}
|
|
7902
|
+
}
|
|
7903
|
+
}
|
|
7904
|
+
state._delete(viewPath, key);
|
|
7714
7905
|
}
|
|
7715
7906
|
clear(viewPath) {
|
|
7716
7907
|
if (viewPath) {
|
|
@@ -7752,12 +7943,49 @@ class ZustandAdapter {
|
|
|
7752
7943
|
setConnectionState(state, error) {
|
|
7753
7944
|
this.store.getState()._setConnectionState(state, error);
|
|
7754
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
|
+
}
|
|
7755
7980
|
}
|
|
7756
7981
|
|
|
7982
|
+
const DEFAULT_FLUSH_INTERVAL_MS = 16;
|
|
7983
|
+
|
|
7757
7984
|
function createRuntime(config) {
|
|
7758
7985
|
const adapter = new ZustandAdapter();
|
|
7759
7986
|
const processor = new FrameProcessor(adapter, {
|
|
7760
7987
|
maxEntriesPerView: config.maxEntriesPerView,
|
|
7988
|
+
flushIntervalMs: config.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS,
|
|
7761
7989
|
});
|
|
7762
7990
|
const connection = new ConnectionManager({
|
|
7763
7991
|
websocketUrl: config.websocketUrl,
|
|
@@ -7777,13 +8005,15 @@ function createRuntime(config) {
|
|
|
7777
8005
|
connection,
|
|
7778
8006
|
subscriptionRegistry,
|
|
7779
8007
|
wallet: config.wallet,
|
|
7780
|
-
subscribe(view, key, filters) {
|
|
7781
|
-
const subscription = { view, key, filters };
|
|
8008
|
+
subscribe(view, key, filters, take, skip) {
|
|
8009
|
+
const subscription = { view, key, filters, take, skip };
|
|
7782
8010
|
const unsubscribe = subscriptionRegistry.subscribe(subscription);
|
|
7783
8011
|
return {
|
|
7784
8012
|
view,
|
|
7785
8013
|
key,
|
|
7786
8014
|
filters,
|
|
8015
|
+
take,
|
|
8016
|
+
skip,
|
|
7787
8017
|
unsubscribe,
|
|
7788
8018
|
};
|
|
7789
8019
|
},
|
|
@@ -7920,7 +8150,7 @@ function createStateViewHook(viewDef, runtime) {
|
|
|
7920
8150
|
use: (key, options) => {
|
|
7921
8151
|
const [isLoading, setIsLoading] = useState(!options?.initialData);
|
|
7922
8152
|
const [error, setError] = useState();
|
|
7923
|
-
const keyString = Object.values(key)[0];
|
|
8153
|
+
const keyString = key ? Object.values(key)[0] : undefined;
|
|
7924
8154
|
const enabled = options?.enabled !== false;
|
|
7925
8155
|
useEffect(() => {
|
|
7926
8156
|
if (!enabled)
|
|
@@ -7967,8 +8197,14 @@ function createStateViewHook(viewDef, runtime) {
|
|
|
7967
8197
|
const unsubscribe = runtime.zustandStore.subscribe(callback);
|
|
7968
8198
|
return unsubscribe;
|
|
7969
8199
|
}, () => {
|
|
7970
|
-
const
|
|
7971
|
-
|
|
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;
|
|
7972
8208
|
});
|
|
7973
8209
|
useEffect(() => {
|
|
7974
8210
|
if (data && isLoading) {
|
|
@@ -7984,113 +8220,147 @@ function createStateViewHook(viewDef, runtime) {
|
|
|
7984
8220
|
}
|
|
7985
8221
|
};
|
|
7986
8222
|
}
|
|
7987
|
-
function
|
|
7988
|
-
|
|
7989
|
-
|
|
7990
|
-
|
|
7991
|
-
|
|
7992
|
-
|
|
7993
|
-
|
|
7994
|
-
|
|
7995
|
-
|
|
7996
|
-
|
|
7997
|
-
|
|
7998
|
-
|
|
7999
|
-
|
|
8000
|
-
|
|
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 () => {
|
|
8001
8242
|
try {
|
|
8002
|
-
|
|
8003
|
-
setIsLoading(true);
|
|
8004
|
-
return () => {
|
|
8005
|
-
try {
|
|
8006
|
-
handle.unsubscribe();
|
|
8007
|
-
}
|
|
8008
|
-
catch (err) {
|
|
8009
|
-
console.error('[Hyperstack] Error unsubscribing from list view:', err);
|
|
8010
|
-
}
|
|
8011
|
-
};
|
|
8243
|
+
handle.unsubscribe();
|
|
8012
8244
|
}
|
|
8013
8245
|
catch (err) {
|
|
8014
|
-
|
|
8015
|
-
setIsLoading(false);
|
|
8016
|
-
return undefined;
|
|
8246
|
+
console.error('[Hyperstack] Error unsubscribing from list view:', err);
|
|
8017
8247
|
}
|
|
8018
|
-
}
|
|
8019
|
-
|
|
8020
|
-
|
|
8021
|
-
|
|
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(() => {
|
|
8022
8263
|
try {
|
|
8023
|
-
|
|
8024
|
-
setIsLoading(true);
|
|
8025
|
-
setTimeout(() => {
|
|
8026
|
-
try {
|
|
8027
|
-
handle.unsubscribe();
|
|
8028
|
-
}
|
|
8029
|
-
catch (err) {
|
|
8030
|
-
console.error('[Hyperstack] Error during list refresh unsubscribe:', err);
|
|
8031
|
-
}
|
|
8032
|
-
}, 0);
|
|
8264
|
+
handle.unsubscribe();
|
|
8033
8265
|
}
|
|
8034
8266
|
catch (err) {
|
|
8035
|
-
|
|
8036
|
-
setIsLoading(false);
|
|
8267
|
+
console.error('[Hyperstack] Error during list refresh unsubscribe:', err);
|
|
8037
8268
|
}
|
|
8038
|
-
},
|
|
8039
|
-
|
|
8040
|
-
|
|
8041
|
-
|
|
8042
|
-
|
|
8043
|
-
|
|
8044
|
-
|
|
8045
|
-
|
|
8046
|
-
|
|
8047
|
-
|
|
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;
|
|
8048
8315
|
}
|
|
8049
|
-
return
|
|
8050
|
-
}
|
|
8051
|
-
if (lastMapRef.current === baseMap && cachedDataRef.current !== undefined) {
|
|
8052
|
-
return cachedDataRef.current;
|
|
8053
|
-
}
|
|
8054
|
-
let items = Array.from(baseMap.values());
|
|
8055
|
-
if (params?.where) {
|
|
8056
|
-
items = items.filter((item) => {
|
|
8057
|
-
return Object.entries(params.where).every(([fieldKey, condition]) => {
|
|
8058
|
-
const value = item[fieldKey];
|
|
8059
|
-
if (typeof condition === 'object' && condition !== null) {
|
|
8060
|
-
const cond = condition;
|
|
8061
|
-
if ('gte' in cond)
|
|
8062
|
-
return value >= cond.gte;
|
|
8063
|
-
if ('lte' in cond)
|
|
8064
|
-
return value <= cond.lte;
|
|
8065
|
-
if ('gt' in cond)
|
|
8066
|
-
return value > cond.gt;
|
|
8067
|
-
if ('lt' in cond)
|
|
8068
|
-
return value < cond.lt;
|
|
8069
|
-
}
|
|
8070
|
-
return value === condition;
|
|
8071
|
-
});
|
|
8072
|
-
});
|
|
8073
|
-
}
|
|
8074
|
-
if (params?.limit) {
|
|
8075
|
-
items = items.slice(0, params.limit);
|
|
8076
|
-
}
|
|
8077
|
-
lastMapRef.current = runtime.zustandStore.getState().entities.get(viewDef.view);
|
|
8078
|
-
cachedDataRef.current = items;
|
|
8079
|
-
return items;
|
|
8316
|
+
return value === condition;
|
|
8317
|
+
});
|
|
8080
8318
|
});
|
|
8081
|
-
|
|
8082
|
-
|
|
8083
|
-
|
|
8084
|
-
|
|
8085
|
-
|
|
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) {
|
|
8086
8344
|
return {
|
|
8087
|
-
data:
|
|
8088
|
-
isLoading,
|
|
8089
|
-
error,
|
|
8090
|
-
refresh
|
|
8345
|
+
data: result.data?.[0],
|
|
8346
|
+
isLoading: result.isLoading,
|
|
8347
|
+
error: result.error,
|
|
8348
|
+
refresh: result.refresh
|
|
8091
8349
|
};
|
|
8092
8350
|
}
|
|
8093
|
-
|
|
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 };
|
|
8094
8364
|
}
|
|
8095
8365
|
|
|
8096
8366
|
function createTxMutationHook(runtime, transactions) {
|
|
@@ -8179,11 +8449,15 @@ function useHyperstack(stack) {
|
|
|
8179
8449
|
views[viewName] = {};
|
|
8180
8450
|
if (typeof viewGroup === 'object' && viewGroup !== null) {
|
|
8181
8451
|
const group = viewGroup;
|
|
8182
|
-
|
|
8183
|
-
|
|
8184
|
-
|
|
8185
|
-
|
|
8186
|
-
|
|
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
|
+
}
|
|
8187
8461
|
}
|
|
8188
8462
|
}
|
|
8189
8463
|
}
|