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