hyperstack-react 0.3.2 → 0.3.3

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