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.js
CHANGED
|
@@ -6779,40 +6779,29 @@ class HyperStackError extends Error {
|
|
|
6779
6779
|
}
|
|
6780
6780
|
}
|
|
6781
6781
|
|
|
6782
|
-
|
|
6783
|
-
|
|
6784
|
-
|
|
6785
|
-
|
|
6786
|
-
typeof obj.data === 'string');
|
|
6787
|
-
}
|
|
6788
|
-
function decompressGzip(base64Data) {
|
|
6789
|
-
const binaryString = atob(base64Data);
|
|
6790
|
-
const bytes = new Uint8Array(binaryString.length);
|
|
6791
|
-
for (let i = 0; i < binaryString.length; i++) {
|
|
6792
|
-
bytes[i] = binaryString.charCodeAt(i);
|
|
6793
|
-
}
|
|
6794
|
-
const decompressed = inflate_1(bytes);
|
|
6795
|
-
return new TextDecoder().decode(decompressed);
|
|
6796
|
-
}
|
|
6797
|
-
function parseAndDecompress(jsonString) {
|
|
6798
|
-
const parsed = JSON.parse(jsonString);
|
|
6799
|
-
if (isCompressedFrame(parsed)) {
|
|
6800
|
-
const decompressedJson = decompressGzip(parsed.data);
|
|
6801
|
-
const frame = JSON.parse(decompressedJson);
|
|
6802
|
-
return frame;
|
|
6803
|
-
}
|
|
6804
|
-
return parsed;
|
|
6782
|
+
const GZIP_MAGIC_0 = 0x1f;
|
|
6783
|
+
const GZIP_MAGIC_1 = 0x8b;
|
|
6784
|
+
function isGzipData(data) {
|
|
6785
|
+
return data.length >= 2 && data[0] === GZIP_MAGIC_0 && data[1] === GZIP_MAGIC_1;
|
|
6805
6786
|
}
|
|
6806
6787
|
function isSnapshotFrame(frame) {
|
|
6807
6788
|
return frame.op === 'snapshot';
|
|
6808
6789
|
}
|
|
6790
|
+
function isSubscribedFrame(frame) {
|
|
6791
|
+
return frame.op === 'subscribed';
|
|
6792
|
+
}
|
|
6809
6793
|
function parseFrame(data) {
|
|
6810
6794
|
if (typeof data === 'string') {
|
|
6811
|
-
return
|
|
6795
|
+
return JSON.parse(data);
|
|
6812
6796
|
}
|
|
6813
|
-
const
|
|
6814
|
-
|
|
6815
|
-
|
|
6797
|
+
const bytes = new Uint8Array(data);
|
|
6798
|
+
if (isGzipData(bytes)) {
|
|
6799
|
+
const decompressed = inflate_1(bytes);
|
|
6800
|
+
const jsonString = new TextDecoder().decode(decompressed);
|
|
6801
|
+
return JSON.parse(jsonString);
|
|
6802
|
+
}
|
|
6803
|
+
const jsonString = new TextDecoder('utf-8').decode(data);
|
|
6804
|
+
return JSON.parse(jsonString);
|
|
6816
6805
|
}
|
|
6817
6806
|
async function parseFrameFromBlob(blob) {
|
|
6818
6807
|
const arrayBuffer = await blob.arrayBuffer();
|
|
@@ -6887,8 +6876,8 @@ class ConnectionManager {
|
|
|
6887
6876
|
this.reconnectAttempts = 0;
|
|
6888
6877
|
this.updateState('connected');
|
|
6889
6878
|
this.startPingInterval();
|
|
6890
|
-
this.flushSubscriptionQueue();
|
|
6891
6879
|
this.resubscribeActive();
|
|
6880
|
+
this.flushSubscriptionQueue();
|
|
6892
6881
|
resolve();
|
|
6893
6882
|
};
|
|
6894
6883
|
this.ws.onmessage = async (event) => {
|
|
@@ -6909,7 +6898,6 @@ class ConnectionManager {
|
|
|
6909
6898
|
this.notifyFrameHandlers(frame);
|
|
6910
6899
|
}
|
|
6911
6900
|
catch (error) {
|
|
6912
|
-
console.error('[hyperstack] Error parsing frame:', error);
|
|
6913
6901
|
this.updateState('error', 'Failed to parse frame from server');
|
|
6914
6902
|
}
|
|
6915
6903
|
};
|
|
@@ -6947,12 +6935,18 @@ class ConnectionManager {
|
|
|
6947
6935
|
subscribe(subscription) {
|
|
6948
6936
|
const subKey = this.makeSubKey(subscription);
|
|
6949
6937
|
if (this.currentState === 'connected' && this.ws?.readyState === WebSocket.OPEN) {
|
|
6938
|
+
if (this.activeSubscriptions.has(subKey)) {
|
|
6939
|
+
return;
|
|
6940
|
+
}
|
|
6950
6941
|
const subMsg = { type: 'subscribe', ...subscription };
|
|
6951
6942
|
this.ws.send(JSON.stringify(subMsg));
|
|
6952
6943
|
this.activeSubscriptions.add(subKey);
|
|
6953
6944
|
}
|
|
6954
6945
|
else {
|
|
6955
|
-
this.subscriptionQueue.
|
|
6946
|
+
const alreadyQueued = this.subscriptionQueue.some((s) => this.makeSubKey(s) === subKey);
|
|
6947
|
+
if (!alreadyQueued) {
|
|
6948
|
+
this.subscriptionQueue.push(subscription);
|
|
6949
|
+
}
|
|
6956
6950
|
}
|
|
6957
6951
|
}
|
|
6958
6952
|
unsubscribe(view, key) {
|
|
@@ -7042,11 +7036,11 @@ class ConnectionManager {
|
|
|
7042
7036
|
}
|
|
7043
7037
|
}
|
|
7044
7038
|
|
|
7045
|
-
function isObject(item) {
|
|
7039
|
+
function isObject$1(item) {
|
|
7046
7040
|
return item !== null && typeof item === 'object' && !Array.isArray(item);
|
|
7047
7041
|
}
|
|
7048
|
-
function deepMergeWithAppend(target, source, appendPaths, currentPath = '') {
|
|
7049
|
-
if (!isObject(target) || !isObject(source)) {
|
|
7042
|
+
function deepMergeWithAppend$1(target, source, appendPaths, currentPath = '') {
|
|
7043
|
+
if (!isObject$1(target) || !isObject$1(source)) {
|
|
7050
7044
|
return source;
|
|
7051
7045
|
}
|
|
7052
7046
|
const result = { ...target };
|
|
@@ -7062,8 +7056,8 @@ function deepMergeWithAppend(target, source, appendPaths, currentPath = '') {
|
|
|
7062
7056
|
result[key] = sourceValue;
|
|
7063
7057
|
}
|
|
7064
7058
|
}
|
|
7065
|
-
else if (isObject(sourceValue) && isObject(targetValue)) {
|
|
7066
|
-
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);
|
|
7067
7061
|
}
|
|
7068
7062
|
else {
|
|
7069
7063
|
result[key] = sourceValue;
|
|
@@ -7073,20 +7067,107 @@ function deepMergeWithAppend(target, source, appendPaths, currentPath = '') {
|
|
|
7073
7067
|
}
|
|
7074
7068
|
class FrameProcessor {
|
|
7075
7069
|
constructor(storage, config = {}) {
|
|
7070
|
+
this.pendingUpdates = [];
|
|
7071
|
+
this.flushTimer = null;
|
|
7072
|
+
this.isProcessing = false;
|
|
7076
7073
|
this.storage = storage;
|
|
7077
7074
|
this.maxEntriesPerView = config.maxEntriesPerView === undefined
|
|
7078
7075
|
? DEFAULT_MAX_ENTRIES_PER_VIEW
|
|
7079
7076
|
: config.maxEntriesPerView;
|
|
7077
|
+
this.flushIntervalMs = config.flushIntervalMs ?? 0;
|
|
7080
7078
|
}
|
|
7081
7079
|
handleFrame(frame) {
|
|
7082
|
-
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)) {
|
|
7083
7141
|
this.handleSnapshotFrame(frame);
|
|
7084
7142
|
}
|
|
7085
7143
|
else {
|
|
7086
7144
|
this.handleEntityFrame(frame);
|
|
7087
7145
|
}
|
|
7088
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
|
+
}
|
|
7089
7166
|
handleSnapshotFrame(frame) {
|
|
7167
|
+
this.handleSnapshotFrameWithoutEnforce(frame);
|
|
7168
|
+
this.enforceMaxEntries(frame.entity);
|
|
7169
|
+
}
|
|
7170
|
+
handleSnapshotFrameWithoutEnforce(frame) {
|
|
7090
7171
|
const viewPath = frame.entity;
|
|
7091
7172
|
for (const entity of frame.data) {
|
|
7092
7173
|
const previousValue = this.storage.get(viewPath, entity.key);
|
|
@@ -7098,16 +7179,18 @@ class FrameProcessor {
|
|
|
7098
7179
|
});
|
|
7099
7180
|
this.emitRichUpdate(viewPath, entity.key, previousValue, entity.data, 'upsert');
|
|
7100
7181
|
}
|
|
7101
|
-
this.enforceMaxEntries(viewPath);
|
|
7102
7182
|
}
|
|
7103
7183
|
handleEntityFrame(frame) {
|
|
7184
|
+
this.handleEntityFrameWithoutEnforce(frame);
|
|
7185
|
+
this.enforceMaxEntries(frame.entity);
|
|
7186
|
+
}
|
|
7187
|
+
handleEntityFrameWithoutEnforce(frame) {
|
|
7104
7188
|
const viewPath = frame.entity;
|
|
7105
7189
|
const previousValue = this.storage.get(viewPath, frame.key);
|
|
7106
7190
|
switch (frame.op) {
|
|
7107
7191
|
case 'create':
|
|
7108
7192
|
case 'upsert':
|
|
7109
7193
|
this.storage.set(viewPath, frame.key, frame.data);
|
|
7110
|
-
this.enforceMaxEntries(viewPath);
|
|
7111
7194
|
this.storage.notifyUpdate(viewPath, frame.key, {
|
|
7112
7195
|
type: 'upsert',
|
|
7113
7196
|
key: frame.key,
|
|
@@ -7119,10 +7202,9 @@ class FrameProcessor {
|
|
|
7119
7202
|
const existing = this.storage.get(viewPath, frame.key);
|
|
7120
7203
|
const appendPaths = frame.append ?? [];
|
|
7121
7204
|
const merged = existing
|
|
7122
|
-
? deepMergeWithAppend(existing, frame.data, appendPaths)
|
|
7205
|
+
? deepMergeWithAppend$1(existing, frame.data, appendPaths)
|
|
7123
7206
|
: frame.data;
|
|
7124
7207
|
this.storage.set(viewPath, frame.key, merged);
|
|
7125
|
-
this.enforceMaxEntries(viewPath);
|
|
7126
7208
|
this.storage.notifyUpdate(viewPath, frame.key, {
|
|
7127
7209
|
type: 'patch',
|
|
7128
7210
|
key: frame.key,
|
|
@@ -7161,7 +7243,7 @@ class FrameProcessor {
|
|
|
7161
7243
|
}
|
|
7162
7244
|
}
|
|
7163
7245
|
|
|
7164
|
-
class ViewData {
|
|
7246
|
+
let ViewData$1 = class ViewData {
|
|
7165
7247
|
constructor() {
|
|
7166
7248
|
this.entities = new Map();
|
|
7167
7249
|
this.accessOrder = [];
|
|
@@ -7215,7 +7297,7 @@ class ViewData {
|
|
|
7215
7297
|
this.entities.clear();
|
|
7216
7298
|
this.accessOrder = [];
|
|
7217
7299
|
}
|
|
7218
|
-
}
|
|
7300
|
+
};
|
|
7219
7301
|
class MemoryAdapter {
|
|
7220
7302
|
constructor(_config = {}) {
|
|
7221
7303
|
this.views = new Map();
|
|
@@ -7263,7 +7345,7 @@ class MemoryAdapter {
|
|
|
7263
7345
|
set(viewPath, key, data) {
|
|
7264
7346
|
let view = this.views.get(viewPath);
|
|
7265
7347
|
if (!view) {
|
|
7266
|
-
view = new ViewData();
|
|
7348
|
+
view = new ViewData$1();
|
|
7267
7349
|
this.views.set(viewPath, view);
|
|
7268
7350
|
}
|
|
7269
7351
|
view.set(key, data);
|
|
@@ -7604,6 +7686,60 @@ class HyperStack {
|
|
|
7604
7686
|
}
|
|
7605
7687
|
}
|
|
7606
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
|
+
}
|
|
7607
7743
|
class ZustandAdapter {
|
|
7608
7744
|
constructor(_config = {}) {
|
|
7609
7745
|
this.updateCallbacks = new Set();
|
|
@@ -7611,6 +7747,8 @@ class ZustandAdapter {
|
|
|
7611
7747
|
this.accessOrder = new Map();
|
|
7612
7748
|
this.store = zustand.create((set) => ({
|
|
7613
7749
|
entities: new Map(),
|
|
7750
|
+
sortedKeys: new Map(),
|
|
7751
|
+
viewConfigs: new Map(),
|
|
7614
7752
|
connectionState: 'disconnected',
|
|
7615
7753
|
lastError: undefined,
|
|
7616
7754
|
_set: (viewPath, key, data) => {
|
|
@@ -7639,14 +7777,30 @@ class ZustandAdapter {
|
|
|
7639
7777
|
if (viewPath) {
|
|
7640
7778
|
const newEntities = new Map(state.entities);
|
|
7641
7779
|
newEntities.delete(viewPath);
|
|
7642
|
-
|
|
7780
|
+
const newSortedKeys = new Map(state.sortedKeys);
|
|
7781
|
+
newSortedKeys.delete(viewPath);
|
|
7782
|
+
return { entities: newEntities, sortedKeys: newSortedKeys };
|
|
7643
7783
|
}
|
|
7644
|
-
return { entities: new Map() };
|
|
7784
|
+
return { entities: new Map(), sortedKeys: new Map() };
|
|
7645
7785
|
});
|
|
7646
7786
|
},
|
|
7647
7787
|
_setConnectionState: (connectionState, lastError) => {
|
|
7648
7788
|
set({ connectionState, lastError });
|
|
7649
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
|
+
},
|
|
7650
7804
|
}));
|
|
7651
7805
|
}
|
|
7652
7806
|
get(viewPath, key) {
|
|
@@ -7658,17 +7812,25 @@ class ZustandAdapter {
|
|
|
7658
7812
|
return value !== undefined ? value : null;
|
|
7659
7813
|
}
|
|
7660
7814
|
getAll(viewPath) {
|
|
7661
|
-
const
|
|
7662
|
-
const viewMap = entities.get(viewPath);
|
|
7815
|
+
const state = this.store.getState();
|
|
7816
|
+
const viewMap = state.entities.get(viewPath);
|
|
7663
7817
|
if (!viewMap)
|
|
7664
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
|
+
}
|
|
7665
7823
|
return Array.from(viewMap.values());
|
|
7666
7824
|
}
|
|
7667
7825
|
getAllSync(viewPath) {
|
|
7668
|
-
const
|
|
7669
|
-
const viewMap = entities.get(viewPath);
|
|
7826
|
+
const state = this.store.getState();
|
|
7827
|
+
const viewMap = state.entities.get(viewPath);
|
|
7670
7828
|
if (!viewMap)
|
|
7671
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
|
+
}
|
|
7672
7834
|
return Array.from(viewMap.values());
|
|
7673
7835
|
}
|
|
7674
7836
|
getSync(viewPath, key) {
|
|
@@ -7692,27 +7854,56 @@ class ZustandAdapter {
|
|
|
7692
7854
|
return this.store.getState().entities.get(viewPath)?.size ?? 0;
|
|
7693
7855
|
}
|
|
7694
7856
|
set(viewPath, key, data) {
|
|
7695
|
-
|
|
7696
|
-
|
|
7697
|
-
|
|
7698
|
-
|
|
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);
|
|
7699
7872
|
}
|
|
7700
|
-
|
|
7701
|
-
|
|
7702
|
-
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);
|
|
7703
7885
|
}
|
|
7704
|
-
order.push(key);
|
|
7705
|
-
this.store.getState()._set(viewPath, key, data);
|
|
7706
7886
|
}
|
|
7707
7887
|
delete(viewPath, key) {
|
|
7708
|
-
const
|
|
7709
|
-
|
|
7710
|
-
|
|
7711
|
-
|
|
7712
|
-
|
|
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);
|
|
7713
7895
|
}
|
|
7714
7896
|
}
|
|
7715
|
-
|
|
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
|
+
}
|
|
7904
|
+
}
|
|
7905
|
+
}
|
|
7906
|
+
state._delete(viewPath, key);
|
|
7716
7907
|
}
|
|
7717
7908
|
clear(viewPath) {
|
|
7718
7909
|
if (viewPath) {
|
|
@@ -7754,12 +7945,49 @@ class ZustandAdapter {
|
|
|
7754
7945
|
setConnectionState(state, error) {
|
|
7755
7946
|
this.store.getState()._setConnectionState(state, error);
|
|
7756
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
|
+
}
|
|
7757
7982
|
}
|
|
7758
7983
|
|
|
7984
|
+
const DEFAULT_FLUSH_INTERVAL_MS = 16;
|
|
7985
|
+
|
|
7759
7986
|
function createRuntime(config) {
|
|
7760
7987
|
const adapter = new ZustandAdapter();
|
|
7761
7988
|
const processor = new FrameProcessor(adapter, {
|
|
7762
7989
|
maxEntriesPerView: config.maxEntriesPerView,
|
|
7990
|
+
flushIntervalMs: config.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS,
|
|
7763
7991
|
});
|
|
7764
7992
|
const connection = new ConnectionManager({
|
|
7765
7993
|
websocketUrl: config.websocketUrl,
|
|
@@ -7779,13 +8007,15 @@ function createRuntime(config) {
|
|
|
7779
8007
|
connection,
|
|
7780
8008
|
subscriptionRegistry,
|
|
7781
8009
|
wallet: config.wallet,
|
|
7782
|
-
subscribe(view, key, filters) {
|
|
7783
|
-
const subscription = { view, key, filters };
|
|
8010
|
+
subscribe(view, key, filters, take, skip) {
|
|
8011
|
+
const subscription = { view, key, filters, take, skip };
|
|
7784
8012
|
const unsubscribe = subscriptionRegistry.subscribe(subscription);
|
|
7785
8013
|
return {
|
|
7786
8014
|
view,
|
|
7787
8015
|
key,
|
|
7788
8016
|
filters,
|
|
8017
|
+
take,
|
|
8018
|
+
skip,
|
|
7789
8019
|
unsubscribe,
|
|
7790
8020
|
};
|
|
7791
8021
|
},
|
|
@@ -7922,7 +8152,7 @@ function createStateViewHook(viewDef, runtime) {
|
|
|
7922
8152
|
use: (key, options) => {
|
|
7923
8153
|
const [isLoading, setIsLoading] = React.useState(!options?.initialData);
|
|
7924
8154
|
const [error, setError] = React.useState();
|
|
7925
|
-
const keyString = Object.values(key)[0];
|
|
8155
|
+
const keyString = key ? Object.values(key)[0] : undefined;
|
|
7926
8156
|
const enabled = options?.enabled !== false;
|
|
7927
8157
|
React.useEffect(() => {
|
|
7928
8158
|
if (!enabled)
|
|
@@ -7969,8 +8199,14 @@ function createStateViewHook(viewDef, runtime) {
|
|
|
7969
8199
|
const unsubscribe = runtime.zustandStore.subscribe(callback);
|
|
7970
8200
|
return unsubscribe;
|
|
7971
8201
|
}, () => {
|
|
7972
|
-
const
|
|
7973
|
-
|
|
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;
|
|
7974
8210
|
});
|
|
7975
8211
|
React.useEffect(() => {
|
|
7976
8212
|
if (data && isLoading) {
|
|
@@ -7986,113 +8222,147 @@ function createStateViewHook(viewDef, runtime) {
|
|
|
7986
8222
|
}
|
|
7987
8223
|
};
|
|
7988
8224
|
}
|
|
7989
|
-
function
|
|
7990
|
-
|
|
7991
|
-
|
|
7992
|
-
|
|
7993
|
-
|
|
7994
|
-
|
|
7995
|
-
|
|
7996
|
-
|
|
7997
|
-
|
|
7998
|
-
|
|
7999
|
-
|
|
8000
|
-
|
|
8001
|
-
|
|
8002
|
-
|
|
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 () => {
|
|
8003
8244
|
try {
|
|
8004
|
-
|
|
8005
|
-
setIsLoading(true);
|
|
8006
|
-
return () => {
|
|
8007
|
-
try {
|
|
8008
|
-
handle.unsubscribe();
|
|
8009
|
-
}
|
|
8010
|
-
catch (err) {
|
|
8011
|
-
console.error('[Hyperstack] Error unsubscribing from list view:', err);
|
|
8012
|
-
}
|
|
8013
|
-
};
|
|
8245
|
+
handle.unsubscribe();
|
|
8014
8246
|
}
|
|
8015
8247
|
catch (err) {
|
|
8016
|
-
|
|
8017
|
-
setIsLoading(false);
|
|
8018
|
-
return undefined;
|
|
8248
|
+
console.error('[Hyperstack] Error unsubscribing from list view:', err);
|
|
8019
8249
|
}
|
|
8020
|
-
}
|
|
8021
|
-
|
|
8022
|
-
|
|
8023
|
-
|
|
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(() => {
|
|
8024
8265
|
try {
|
|
8025
|
-
|
|
8026
|
-
setIsLoading(true);
|
|
8027
|
-
setTimeout(() => {
|
|
8028
|
-
try {
|
|
8029
|
-
handle.unsubscribe();
|
|
8030
|
-
}
|
|
8031
|
-
catch (err) {
|
|
8032
|
-
console.error('[Hyperstack] Error during list refresh unsubscribe:', err);
|
|
8033
|
-
}
|
|
8034
|
-
}, 0);
|
|
8266
|
+
handle.unsubscribe();
|
|
8035
8267
|
}
|
|
8036
8268
|
catch (err) {
|
|
8037
|
-
|
|
8038
|
-
setIsLoading(false);
|
|
8269
|
+
console.error('[Hyperstack] Error during list refresh unsubscribe:', err);
|
|
8039
8270
|
}
|
|
8040
|
-
},
|
|
8041
|
-
|
|
8042
|
-
|
|
8043
|
-
|
|
8044
|
-
|
|
8045
|
-
|
|
8046
|
-
|
|
8047
|
-
|
|
8048
|
-
|
|
8049
|
-
|
|
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;
|
|
8050
8317
|
}
|
|
8051
|
-
return
|
|
8052
|
-
}
|
|
8053
|
-
if (lastMapRef.current === baseMap && cachedDataRef.current !== undefined) {
|
|
8054
|
-
return cachedDataRef.current;
|
|
8055
|
-
}
|
|
8056
|
-
let items = Array.from(baseMap.values());
|
|
8057
|
-
if (params?.where) {
|
|
8058
|
-
items = items.filter((item) => {
|
|
8059
|
-
return Object.entries(params.where).every(([fieldKey, condition]) => {
|
|
8060
|
-
const value = item[fieldKey];
|
|
8061
|
-
if (typeof condition === 'object' && condition !== null) {
|
|
8062
|
-
const cond = condition;
|
|
8063
|
-
if ('gte' in cond)
|
|
8064
|
-
return value >= cond.gte;
|
|
8065
|
-
if ('lte' in cond)
|
|
8066
|
-
return value <= cond.lte;
|
|
8067
|
-
if ('gt' in cond)
|
|
8068
|
-
return value > cond.gt;
|
|
8069
|
-
if ('lt' in cond)
|
|
8070
|
-
return value < cond.lt;
|
|
8071
|
-
}
|
|
8072
|
-
return value === condition;
|
|
8073
|
-
});
|
|
8074
|
-
});
|
|
8075
|
-
}
|
|
8076
|
-
if (params?.limit) {
|
|
8077
|
-
items = items.slice(0, params.limit);
|
|
8078
|
-
}
|
|
8079
|
-
lastMapRef.current = runtime.zustandStore.getState().entities.get(viewDef.view);
|
|
8080
|
-
cachedDataRef.current = items;
|
|
8081
|
-
return items;
|
|
8318
|
+
return value === condition;
|
|
8319
|
+
});
|
|
8082
8320
|
});
|
|
8083
|
-
|
|
8084
|
-
|
|
8085
|
-
|
|
8086
|
-
|
|
8087
|
-
|
|
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) {
|
|
8088
8346
|
return {
|
|
8089
|
-
data:
|
|
8090
|
-
isLoading,
|
|
8091
|
-
error,
|
|
8092
|
-
refresh
|
|
8347
|
+
data: result.data?.[0],
|
|
8348
|
+
isLoading: result.isLoading,
|
|
8349
|
+
error: result.error,
|
|
8350
|
+
refresh: result.refresh
|
|
8093
8351
|
};
|
|
8094
8352
|
}
|
|
8095
|
-
|
|
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 };
|
|
8096
8366
|
}
|
|
8097
8367
|
|
|
8098
8368
|
function createTxMutationHook(runtime, transactions) {
|
|
@@ -8181,11 +8451,15 @@ function useHyperstack(stack) {
|
|
|
8181
8451
|
views[viewName] = {};
|
|
8182
8452
|
if (typeof viewGroup === 'object' && viewGroup !== null) {
|
|
8183
8453
|
const group = viewGroup;
|
|
8184
|
-
|
|
8185
|
-
|
|
8186
|
-
|
|
8187
|
-
|
|
8188
|
-
|
|
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
|
+
}
|
|
8189
8463
|
}
|
|
8190
8464
|
}
|
|
8191
8465
|
}
|