hyperstack-react 0.3.2 → 0.3.4

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