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/dist/index.js CHANGED
@@ -6779,40 +6779,29 @@ class HyperStackError extends Error {
6779
6779
  }
6780
6780
  }
6781
6781
 
6782
- function isCompressedFrame(obj) {
6783
- return (typeof obj === 'object' &&
6784
- obj !== null &&
6785
- obj.compressed === 'gzip' &&
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 parseAndDecompress(data);
6795
+ return JSON.parse(data);
6812
6796
  }
6813
- const decoder = new TextDecoder('utf-8');
6814
- const jsonString = decoder.decode(data);
6815
- return parseAndDecompress(jsonString);
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.push(subscription);
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 (isSnapshotFrame(frame)) {
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
- return { entities: newEntities };
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 entities = this.store.getState().entities;
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 entities = this.store.getState().entities;
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
- let order = this.accessOrder.get(viewPath);
7696
- if (!order) {
7697
- order = [];
7698
- this.accessOrder.set(viewPath, order);
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
- const existingIdx = order.indexOf(key);
7701
- if (existingIdx !== -1) {
7702
- order.splice(existingIdx, 1);
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 order = this.accessOrder.get(viewPath);
7709
- if (order) {
7710
- const idx = order.indexOf(key);
7711
- if (idx !== -1) {
7712
- order.splice(idx, 1);
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
- this.store.getState()._delete(viewPath, key);
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 rawData = runtime.zustandStore.getState().entities.get(viewDef.view)?.get(keyString);
7973
- return rawData;
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 createListViewHook(viewDef, runtime) {
7990
- return {
7991
- use: (params, options) => {
7992
- const [isLoading, setIsLoading] = React.useState(!options?.initialData);
7993
- const [error, setError] = React.useState();
7994
- const cachedDataRef = React.useRef(undefined);
7995
- const lastMapRef = React.useRef(undefined);
7996
- const enabled = options?.enabled !== false;
7997
- const key = params?.key;
7998
- const filtersJson = params?.filters ? JSON.stringify(params.filters) : undefined;
7999
- const filters = React.useMemo(() => params?.filters, [filtersJson]);
8000
- React.useEffect(() => {
8001
- if (!enabled)
8002
- return undefined;
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
- const handle = runtime.subscribe(viewDef.view, key, filters);
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
- setError(err instanceof Error ? err : new Error('Subscription failed'));
8017
- setIsLoading(false);
8018
- return undefined;
8248
+ console.error('[Hyperstack] Error unsubscribing from list view:', err);
8019
8249
  }
8020
- }, [enabled, key, filtersJson]);
8021
- const refresh = React.useCallback(() => {
8022
- if (!enabled)
8023
- return;
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
- const handle = runtime.subscribe(viewDef.view, key, filters);
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
- setError(err instanceof Error ? err : new Error('Refresh failed'));
8038
- setIsLoading(false);
8269
+ console.error('[Hyperstack] Error during list refresh unsubscribe:', err);
8039
8270
  }
8040
- }, [enabled, key, filtersJson]);
8041
- const data = React.useSyncExternalStore((callback) => {
8042
- const unsubscribe = runtime.zustandStore.subscribe(callback);
8043
- return unsubscribe;
8044
- }, () => {
8045
- const baseMap = runtime.zustandStore.getState().entities.get(viewDef.view);
8046
- if (!baseMap) {
8047
- if (cachedDataRef.current !== undefined) {
8048
- cachedDataRef.current = undefined;
8049
- lastMapRef.current = undefined;
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 undefined;
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
- React.useEffect(() => {
8084
- if (data && isLoading) {
8085
- setIsLoading(false);
8086
- }
8087
- }, [data, isLoading]);
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: (options?.initialData ?? 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
- if (group.state) {
8185
- views[viewName].state = createStateViewHook(group.state, runtime);
8186
- }
8187
- if (group.list) {
8188
- views[viewName].list = createListViewHook(group.list, runtime);
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
  }