hyperstack-react 0.1.2

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.
@@ -0,0 +1,834 @@
1
+ import React, { createContext, useMemo, useRef, useEffect, useContext, useState, useCallback, useSyncExternalStore } from 'react';
2
+ import { create } from 'zustand';
3
+
4
+ // Sensible defaults - can be overridden per ConnectionManager instance
5
+ const DEFAULT_CONFIG = {
6
+ websocketUrl: 'ws://localhost:8080', // default local development server
7
+ reconnectIntervals: [1000, 2000, 4000, 8000, 16000], // exponential backoff: 1s, 2s, 4s, 8s, 16s
8
+ maxReconnectAttempts: 5, // retry 5 times before giving up (matches array length)
9
+ initialSubscriptions: [], // no global subscriptions by default
10
+ supportsUnsubscribe: false, // conservative default - many servers don't support this
11
+ autoSubscribeDefault: true, // hooks auto-subscribe by default
12
+ };
13
+ // Custom error class for SDK-specific errors with structured details
14
+ class HyperStreamError extends Error {
15
+ constructor(message, // human-readable error message
16
+ code, // machine-readable error code (e.g., 'CONNECTION_FAILED')
17
+ details // additional context (original error, frame data, etc.)
18
+ ) {
19
+ super(message);
20
+ this.code = code;
21
+ this.details = details;
22
+ this.name = 'HyperStreamError'; // Proper error name for debugging/logging
23
+ }
24
+ }
25
+
26
+ // Manages WebSocket connection lifecycle and subscription queuing
27
+ class ConnectionManager {
28
+ constructor(config = {}) {
29
+ this.ws = null;
30
+ this.reconnectAttempts = 0;
31
+ this.reconnectTimeout = null;
32
+ this.pingInterval = null;
33
+ this.currentState = 'disconnected';
34
+ this.subscriptionQueue = [];
35
+ this.config = { ...DEFAULT_CONFIG, ...config };
36
+ }
37
+ // set callbacks
38
+ setHandlers(handlers) {
39
+ this.onFrame = handlers.onFrame;
40
+ this.onStateChange = handlers.onStateChange;
41
+ }
42
+ // public getters for external state inspection
43
+ getState() {
44
+ return this.currentState;
45
+ }
46
+ getConfig() {
47
+ return this.config;
48
+ }
49
+ // Initiate WebSocket connection with subscription restoration
50
+ connect() {
51
+ // prevent duplicate connections
52
+ if (this.ws?.readyState === WebSocket.OPEN ||
53
+ this.ws?.readyState === WebSocket.CONNECTING ||
54
+ this.currentState === 'connecting') {
55
+ console.log('Connection already exists or in progress, skipping connect');
56
+ return;
57
+ }
58
+ console.log('[Hyperstack] Connecting to WebSocket...');
59
+ this.updateState('connecting'); // update UI state immediately
60
+ try {
61
+ this.ws = new WebSocket(this.config.websocketUrl);
62
+ this.ws.onopen = () => {
63
+ console.log('[Hyperstack] WebSocket connected');
64
+ this.reconnectAttempts = 0; // reset retry counter on successful connect
65
+ this.updateState('connected'); // notify store/UI of successful connection
66
+ this.startPingInterval(); // start keep-alive ping
67
+ // send global subscriptions first (from config)
68
+ if (this.config.initialSubscriptions) {
69
+ for (const sub of this.config.initialSubscriptions) {
70
+ this.subscribe(sub);
71
+ }
72
+ }
73
+ // flush queued subscriptions from when we were offline
74
+ while (this.subscriptionQueue.length > 0) {
75
+ const sub = this.subscriptionQueue.shift();
76
+ this.subscribe(sub);
77
+ }
78
+ };
79
+ // core message handler - parses incoming data into EntityFrames
80
+ this.ws.onmessage = async (event) => {
81
+ try {
82
+ let frame;
83
+ if (event.data instanceof ArrayBuffer) {
84
+ // binary data as ArrayBuffer
85
+ frame = this.parseBinaryFrame(event.data);
86
+ }
87
+ else if (event.data instanceof Blob) {
88
+ // binary data as Blob - convert to ArrayBuffer
89
+ const arrayBuffer = await event.data.arrayBuffer();
90
+ frame = this.parseBinaryFrame(arrayBuffer);
91
+ }
92
+ else if (typeof event.data === 'string') {
93
+ // JSON text data
94
+ frame = JSON.parse(event.data);
95
+ }
96
+ else {
97
+ throw new Error(`Unsupported message type: ${typeof event.data}`);
98
+ }
99
+ // fw parsed frame to store for entity updates
100
+ this.onFrame?.(frame);
101
+ }
102
+ catch (error) {
103
+ console.error('Failed to parse frame:', error);
104
+ this.updateState('error', 'Failed to parse frame from server');
105
+ }
106
+ };
107
+ // WebSocket error handling
108
+ this.ws.onerror = (error) => {
109
+ console.error('WebSocket error:', error);
110
+ this.updateState('error', 'WebSocket connection error'); // Notify store/UI of errors
111
+ };
112
+ // connection lost handler with auto-reconnection
113
+ this.ws.onclose = () => {
114
+ console.log('WebSocket disconnected');
115
+ this.stopPingInterval();
116
+ this.ws = null;
117
+ // only auto-reconnect if we didn't explicitly disconnect
118
+ // this preserves subscriptions across temporary network issues
119
+ if (this.currentState !== 'disconnected') {
120
+ this.handleReconnect();
121
+ }
122
+ };
123
+ }
124
+ catch (error) {
125
+ console.error('Failed to create WebSocket:', error);
126
+ this.updateState('error', 'Failed to create WebSocket connection');
127
+ }
128
+ }
129
+ disconnect() {
130
+ this.clearReconnectTimeout();
131
+ this.stopPingInterval();
132
+ if (this.ws) {
133
+ this.ws.close();
134
+ this.ws = null;
135
+ }
136
+ this.updateState('disconnected');
137
+ }
138
+ updateConfig(newConfig) {
139
+ this.config = { ...this.config, ...newConfig };
140
+ }
141
+ subscribe(subscription) {
142
+ if (this.currentState === 'connected' && this.ws && this.ws.readyState === WebSocket.OPEN) {
143
+ console.log('[Hyperstack] Subscribing to:', subscription.view);
144
+ const message = JSON.stringify(subscription);
145
+ this.ws.send(message);
146
+ }
147
+ else {
148
+ this.subscriptionQueue.push(subscription);
149
+ }
150
+ }
151
+ // Unsubscribe support (feature-gated by server capabilities)
152
+ unsubscribe(_view, _key) {
153
+ if (this.config.supportsUnsubscribe && this.currentState === 'connected' && this.ws) {
154
+ // only send unsubscribe if server supports it - TO DO
155
+ console.warn('Unsubscribe not yet implemented on server side');
156
+ }
157
+ }
158
+ // binary frame parser - converts WebSocket binary data to EntityFrame
159
+ parseBinaryFrame(data) {
160
+ // server sends JSON Frame serialized as binary bytes
161
+ // convert binary data back to JSON string and parse
162
+ const decoder = new TextDecoder('utf-8');
163
+ const jsonString = decoder.decode(data);
164
+ // parse the Frame JSON sent by projector
165
+ const frame = JSON.parse(jsonString);
166
+ // convert to EntityFrame format expected by SDK
167
+ return {
168
+ mode: frame.mode,
169
+ entity: frame.entity, // Backend serializes view path as 'entity' field
170
+ op: frame.op,
171
+ key: frame.key,
172
+ data: frame.data
173
+ };
174
+ }
175
+ // Internal state change handler - notifies store and triggers UI re-renders
176
+ updateState(state, error) {
177
+ this.currentState = state;
178
+ this.onStateChange?.(state, error);
179
+ }
180
+ // Auto-reconnection with true exponential backoff protection
181
+ handleReconnect() {
182
+ const intervals = this.config.reconnectIntervals || [1000, 2000, 4000, 8000, 16000];
183
+ const maxAttempts = this.config.maxReconnectAttempts || intervals.length;
184
+ if (this.reconnectAttempts >= maxAttempts) {
185
+ // give up after max attempts to avoid infinite retry loops
186
+ this.updateState('error', `Max reconnection attempts (${this.reconnectAttempts}) reached`);
187
+ return;
188
+ }
189
+ this.updateState('reconnecting'); // update store/UI to show reconnection status
190
+ // get delay for current attempt (use last interval if we exceed array length)
191
+ const attemptIndex = Math.min(this.reconnectAttempts, intervals.length - 1);
192
+ const delay = intervals[attemptIndex];
193
+ this.reconnectAttempts++;
194
+ // delayed reconnection with exponential backoff
195
+ this.reconnectTimeout = setTimeout(() => {
196
+ console.log(`Reconnect attempt ${this.reconnectAttempts} after ${delay}ms delay`);
197
+ this.connect(); // recursive call - will restore subscriptions on success
198
+ }, delay);
199
+ }
200
+ // Cleanup helper for reconnection timer
201
+ clearReconnectTimeout() {
202
+ if (this.reconnectTimeout) {
203
+ clearTimeout(this.reconnectTimeout);
204
+ this.reconnectTimeout = null;
205
+ }
206
+ }
207
+ startPingInterval() {
208
+ this.stopPingInterval();
209
+ this.pingInterval = setInterval(() => {
210
+ if (this.ws?.readyState === WebSocket.OPEN) {
211
+ this.ws.send(JSON.stringify({ type: 'ping' }));
212
+ }
213
+ }, 15000);
214
+ }
215
+ stopPingInterval() {
216
+ if (this.pingInterval) {
217
+ clearInterval(this.pingInterval);
218
+ this.pingInterval = null;
219
+ }
220
+ }
221
+ }
222
+
223
+ function deepMerge(target, source) {
224
+ if (!isObject(target) || !isObject(source)) {
225
+ return source;
226
+ }
227
+ const result = { ...target };
228
+ for (const key in source) {
229
+ const sourceValue = source[key];
230
+ const targetValue = result[key];
231
+ if (isObject(sourceValue) && isObject(targetValue)) {
232
+ result[key] = deepMerge(targetValue, sourceValue);
233
+ }
234
+ else {
235
+ result[key] = sourceValue;
236
+ }
237
+ }
238
+ return result;
239
+ }
240
+ function isObject(item) {
241
+ return item && typeof item === 'object' && !Array.isArray(item);
242
+ }
243
+ function createHyperStore(config = {}) {
244
+ return create((set, get) => {
245
+ const connectionManager = new ConnectionManager({ ...DEFAULT_CONFIG, ...config });
246
+ const makeSubKey = (subscription) => `${subscription.view}:${subscription.key ?? '*'}:${subscription.partition ?? ''}:${JSON.stringify(subscription.filters ?? {})}`;
247
+ connectionManager.setHandlers({
248
+ onFrame: (frame) => {
249
+ get().handleFrame(frame);
250
+ },
251
+ onStateChange: (connectionState, error) => {
252
+ set({ connectionState, lastError: error });
253
+ }
254
+ });
255
+ return {
256
+ connectionState: 'disconnected',
257
+ lastError: undefined,
258
+ entities: new Map(),
259
+ recentFrames: [],
260
+ subscriptionRefs: new Map(),
261
+ connectionManager,
262
+ viewCache: {},
263
+ handleFrame: (frame) => {
264
+ set((state) => {
265
+ const newEntities = new Map(state.entities);
266
+ const viewPath = frame.entity;
267
+ const entityMap = new Map(newEntities.get(viewPath) || new Map());
268
+ switch (frame.op) {
269
+ case 'upsert':
270
+ entityMap.set(frame.key, frame.data);
271
+ break;
272
+ case 'patch':
273
+ const existing = entityMap.get(frame.key);
274
+ if (existing && typeof existing === 'object' && typeof frame.data === 'object') {
275
+ entityMap.set(frame.key, deepMerge(existing, frame.data));
276
+ }
277
+ else {
278
+ entityMap.set(frame.key, frame.data);
279
+ }
280
+ break;
281
+ case 'delete':
282
+ entityMap.delete(frame.key);
283
+ break;
284
+ }
285
+ newEntities.set(viewPath, entityMap);
286
+ const newViewCache = { ...state.viewCache };
287
+ const currentMetadata = newViewCache[viewPath];
288
+ const keys = Array.from(entityMap.keys());
289
+ newViewCache[viewPath] = {
290
+ mode: (frame.mode === 'state' || frame.mode === 'list') ? frame.mode : 'list',
291
+ keys,
292
+ lastUpdatedAt: Date.now(),
293
+ lastArgs: currentMetadata?.lastArgs
294
+ };
295
+ return {
296
+ ...state,
297
+ entities: newEntities,
298
+ recentFrames: [frame, ...state.recentFrames],
299
+ viewCache: newViewCache
300
+ };
301
+ });
302
+ },
303
+ _incRef: (subscription) => {
304
+ const subKey = makeSubKey(subscription);
305
+ const { subscriptionRefs } = get();
306
+ const existing = subscriptionRefs.get(subKey);
307
+ if (existing) {
308
+ existing.refCount++;
309
+ }
310
+ else {
311
+ subscriptionRefs.set(subKey, {
312
+ subscription,
313
+ refCount: 1
314
+ });
315
+ connectionManager.subscribe(subscription);
316
+ }
317
+ },
318
+ _decRef: (subscription) => {
319
+ const subKey = makeSubKey(subscription);
320
+ const { subscriptionRefs } = get();
321
+ const existing = subscriptionRefs.get(subKey);
322
+ if (existing) {
323
+ existing.refCount--;
324
+ if (existing.refCount <= 0) {
325
+ subscriptionRefs.delete(subKey);
326
+ connectionManager.unsubscribe(subscription.view, subscription.key);
327
+ }
328
+ }
329
+ },
330
+ _getRefCount: (subscription) => {
331
+ const subKey = makeSubKey(subscription);
332
+ return get().subscriptionRefs.get(subKey)?.refCount ?? 0;
333
+ },
334
+ subscribe: (subscription) => {
335
+ connectionManager.subscribe(subscription);
336
+ },
337
+ unsubscribe: (view, key) => {
338
+ connectionManager.unsubscribe(view, key);
339
+ },
340
+ connect: () => {
341
+ connectionManager.connect();
342
+ },
343
+ disconnect: () => {
344
+ connectionManager.disconnect();
345
+ },
346
+ updateConfig: (newConfig) => {
347
+ connectionManager.updateConfig(newConfig);
348
+ }
349
+ };
350
+ });
351
+ }
352
+
353
+ function createRuntime(config) {
354
+ const store = createHyperStore({
355
+ websocketUrl: config.websocketUrl
356
+ });
357
+ const connection = store.getState().connectionManager;
358
+ return {
359
+ store,
360
+ connection,
361
+ wallet: config.wallet,
362
+ subscribe(view, key, filters) {
363
+ const subscription = { view, key, filters };
364
+ store.getState()._incRef(subscription);
365
+ return {
366
+ view,
367
+ key,
368
+ filters,
369
+ unsubscribe: () => store.getState()._decRef(subscription)
370
+ };
371
+ },
372
+ unsubscribe(handle) {
373
+ handle.unsubscribe();
374
+ }
375
+ };
376
+ }
377
+
378
+ const HyperstackContext = createContext(null);
379
+ function resolveNetworkConfig(network, websocketUrl) {
380
+ if (websocketUrl) {
381
+ return {
382
+ name: 'custom',
383
+ websocketUrl
384
+ };
385
+ }
386
+ if (typeof network === 'object') {
387
+ return network;
388
+ }
389
+ if (network === 'mainnet') {
390
+ return {
391
+ name: 'mainnet',
392
+ websocketUrl: 'wss://mainnet.hyperstack.xyz',
393
+ };
394
+ }
395
+ if (network === 'devnet') {
396
+ return {
397
+ name: 'devnet',
398
+ websocketUrl: 'ws://localhost:8080',
399
+ };
400
+ }
401
+ if (network === 'localnet') {
402
+ return {
403
+ name: 'localnet',
404
+ websocketUrl: 'ws://localhost:8080',
405
+ };
406
+ }
407
+ throw new Error('Must provide either network or websocketUrl');
408
+ }
409
+ function HyperstackProvider({ children, ...config }) {
410
+ const networkConfig = useMemo(() => {
411
+ try {
412
+ return resolveNetworkConfig(config.network, config.websocketUrl);
413
+ }
414
+ catch (error) {
415
+ console.error('[Hyperstack] Invalid network configuration:', error);
416
+ throw error;
417
+ }
418
+ }, [config.network, config.websocketUrl]);
419
+ const runtimeRef = useRef(null);
420
+ if (!runtimeRef.current) {
421
+ try {
422
+ runtimeRef.current = createRuntime({
423
+ ...config,
424
+ websocketUrl: networkConfig.websocketUrl,
425
+ network: networkConfig
426
+ });
427
+ }
428
+ catch (error) {
429
+ console.error('[Hyperstack] Failed to create runtime:', error);
430
+ throw error;
431
+ }
432
+ }
433
+ const runtime = runtimeRef.current;
434
+ const isMountedRef = useRef(true);
435
+ useEffect(() => {
436
+ isMountedRef.current = true;
437
+ if (config.autoConnect !== false) {
438
+ try {
439
+ runtime.connection.connect();
440
+ }
441
+ catch (error) {
442
+ console.error('[Hyperstack] Failed to auto-connect:', error);
443
+ }
444
+ }
445
+ return () => {
446
+ isMountedRef.current = false;
447
+ setTimeout(() => {
448
+ if (!isMountedRef.current) {
449
+ try {
450
+ runtime.connection.disconnect();
451
+ }
452
+ catch (error) {
453
+ console.error('[Hyperstack] Failed to disconnect:', error);
454
+ }
455
+ }
456
+ }, 100);
457
+ };
458
+ }, [runtime, config.autoConnect]);
459
+ const value = {
460
+ runtime,
461
+ config: { ...config, network: networkConfig }
462
+ };
463
+ return (React.createElement(HyperstackContext.Provider, { value: value }, children));
464
+ }
465
+ function useHyperstackContext() {
466
+ const context = useContext(HyperstackContext);
467
+ if (!context) {
468
+ throw new Error('useHyperstackContext must be used within HyperstackProvider');
469
+ }
470
+ return context;
471
+ }
472
+
473
+ function createStateViewHook(viewDef, runtime) {
474
+ return {
475
+ use: (key, options) => {
476
+ const [isLoading, setIsLoading] = useState(!options?.initialData);
477
+ const [error, setError] = useState();
478
+ const keyString = Object.values(key)[0];
479
+ const enabled = options?.enabled !== false;
480
+ useEffect(() => {
481
+ if (!enabled)
482
+ return undefined;
483
+ try {
484
+ const handle = runtime.subscribe(viewDef.view, keyString);
485
+ setIsLoading(true);
486
+ return () => {
487
+ try {
488
+ handle.unsubscribe();
489
+ }
490
+ catch (err) {
491
+ console.error('[Hyperstack] Error unsubscribing from view:', err);
492
+ }
493
+ };
494
+ }
495
+ catch (err) {
496
+ setError(err instanceof Error ? err : new Error('Subscription failed'));
497
+ setIsLoading(false);
498
+ return undefined;
499
+ }
500
+ }, [keyString, enabled]);
501
+ const refresh = useCallback(() => {
502
+ if (!enabled)
503
+ return;
504
+ try {
505
+ const handle = runtime.subscribe(viewDef.view, keyString);
506
+ setIsLoading(true);
507
+ setTimeout(() => {
508
+ try {
509
+ handle.unsubscribe();
510
+ }
511
+ catch (err) {
512
+ console.error('[Hyperstack] Error during refresh unsubscribe:', err);
513
+ }
514
+ }, 0);
515
+ }
516
+ catch (err) {
517
+ setError(err instanceof Error ? err : new Error('Refresh failed'));
518
+ setIsLoading(false);
519
+ }
520
+ }, [keyString, enabled]);
521
+ const data = useSyncExternalStore((callback) => {
522
+ const unsubscribe = runtime.store.subscribe(() => {
523
+ callback();
524
+ });
525
+ return unsubscribe;
526
+ }, () => {
527
+ const rawData = runtime.store.getState().entities.get(viewDef.view)?.get(keyString);
528
+ if (rawData && viewDef.transform) {
529
+ try {
530
+ return viewDef.transform(rawData);
531
+ }
532
+ catch (err) {
533
+ return undefined;
534
+ }
535
+ }
536
+ return rawData;
537
+ });
538
+ useEffect(() => {
539
+ if (data && isLoading) {
540
+ setIsLoading(false);
541
+ }
542
+ }, [data, isLoading]);
543
+ return {
544
+ data: options?.initialData ?? data,
545
+ isLoading,
546
+ error,
547
+ refresh
548
+ };
549
+ }
550
+ };
551
+ }
552
+ function createListViewHook(viewDef, runtime) {
553
+ return {
554
+ use: (params, options) => {
555
+ const [isLoading, setIsLoading] = useState(!options?.initialData);
556
+ const [error, setError] = useState();
557
+ const cachedDataRef = useRef(undefined);
558
+ const lastMapRef = useRef(undefined);
559
+ const enabled = options?.enabled !== false;
560
+ const key = params?.key;
561
+ // Stabilize filters object to prevent unnecessary re-subscriptions
562
+ // We use a JSON string as the dependency to detect actual value changes
563
+ const filtersJson = params?.filters ? JSON.stringify(params.filters) : undefined;
564
+ const filters = useMemo(() => params?.filters, [filtersJson]);
565
+ useEffect(() => {
566
+ if (!enabled)
567
+ return undefined;
568
+ try {
569
+ const handle = runtime.subscribe(viewDef.view, key, filters);
570
+ setIsLoading(true);
571
+ return () => {
572
+ try {
573
+ handle.unsubscribe();
574
+ }
575
+ catch (err) {
576
+ console.error('[Hyperstack] Error unsubscribing from list view:', err);
577
+ }
578
+ };
579
+ }
580
+ catch (err) {
581
+ setError(err instanceof Error ? err : new Error('Subscription failed'));
582
+ setIsLoading(false);
583
+ return undefined;
584
+ }
585
+ }, [enabled, key, filtersJson]);
586
+ const refresh = useCallback(() => {
587
+ if (!enabled)
588
+ return;
589
+ try {
590
+ const handle = runtime.subscribe(viewDef.view, key, filters);
591
+ setIsLoading(true);
592
+ setTimeout(() => {
593
+ try {
594
+ handle.unsubscribe();
595
+ }
596
+ catch (err) {
597
+ console.error('[Hyperstack] Error during list refresh unsubscribe:', err);
598
+ }
599
+ }, 0);
600
+ }
601
+ catch (err) {
602
+ setError(err instanceof Error ? err : new Error('Refresh failed'));
603
+ setIsLoading(false);
604
+ }
605
+ }, [enabled, key, filtersJson]);
606
+ const data = useSyncExternalStore((callback) => {
607
+ const unsubscribe = runtime.store.subscribe(() => {
608
+ callback();
609
+ });
610
+ return unsubscribe;
611
+ }, () => {
612
+ let baseMap = runtime.store.getState().entities.get(viewDef.view);
613
+ if (!baseMap) {
614
+ if (cachedDataRef.current !== undefined) {
615
+ cachedDataRef.current = undefined;
616
+ lastMapRef.current = undefined;
617
+ }
618
+ return undefined;
619
+ }
620
+ if (lastMapRef.current === baseMap && cachedDataRef.current !== undefined) {
621
+ return cachedDataRef.current;
622
+ }
623
+ let items = Array.from(baseMap.values()).map((value) => {
624
+ const item = value?.item ?? value;
625
+ if (viewDef.transform) {
626
+ try {
627
+ return viewDef.transform(item);
628
+ }
629
+ catch (err) {
630
+ console.log("Error transforming", err);
631
+ return item;
632
+ }
633
+ }
634
+ return item;
635
+ });
636
+ if (params?.where) {
637
+ items = items.filter((item) => {
638
+ return Object.entries(params.where).every(([key, condition]) => {
639
+ const value = item[key];
640
+ if (typeof condition === 'object' && condition !== null) {
641
+ if ('gte' in condition)
642
+ return value >= condition.gte;
643
+ if ('lte' in condition)
644
+ return value <= condition.lte;
645
+ if ('gt' in condition)
646
+ return value > condition.gt;
647
+ if ('lt' in condition)
648
+ return value < condition.lt;
649
+ }
650
+ return value === condition;
651
+ });
652
+ });
653
+ }
654
+ if (params?.limit) {
655
+ items = items.slice(0, params.limit);
656
+ }
657
+ lastMapRef.current = runtime.store.getState().entities.get(viewDef.view);
658
+ cachedDataRef.current = items;
659
+ return items;
660
+ });
661
+ useEffect(() => {
662
+ if (data && isLoading) {
663
+ setIsLoading(false);
664
+ }
665
+ }, [data, isLoading]);
666
+ return {
667
+ data: options?.initialData ?? data,
668
+ isLoading,
669
+ error,
670
+ refresh
671
+ };
672
+ }
673
+ };
674
+ }
675
+
676
+ function createTxMutationHook(runtime, transactions) {
677
+ return function useMutation() {
678
+ const [status, setStatus] = useState('idle');
679
+ const [error, setError] = useState();
680
+ const [signature, setSignature] = useState();
681
+ const submit = async (instructionOrTx) => {
682
+ setStatus('pending');
683
+ setError(undefined);
684
+ setSignature(undefined);
685
+ try {
686
+ if (!instructionOrTx) {
687
+ throw new Error('Transaction instruction or transaction object is required');
688
+ }
689
+ if (!runtime.wallet) {
690
+ throw new Error('Wallet not connected. Please provide a wallet adapter to HyperstackProvider.');
691
+ }
692
+ console.log('[Hyperstack] Submitting transaction:', instructionOrTx);
693
+ let txSignature;
694
+ let instructionsToRefresh = [];
695
+ if (Array.isArray(instructionOrTx)) {
696
+ console.log('[Hyperstack] Processing multiple instructions');
697
+ txSignature = await runtime.wallet.signAndSend(instructionOrTx);
698
+ instructionsToRefresh = instructionOrTx.filter(inst => inst && typeof inst === 'object' && inst.instruction && inst.params !== undefined);
699
+ }
700
+ else if (typeof instructionOrTx === 'object' &&
701
+ instructionOrTx.instruction &&
702
+ instructionOrTx.params !== undefined) {
703
+ console.log('[Hyperstack] Processing single instruction');
704
+ txSignature = await runtime.wallet.signAndSend(instructionOrTx);
705
+ instructionsToRefresh = [instructionOrTx];
706
+ }
707
+ else {
708
+ console.log('[Hyperstack] Processing raw transaction');
709
+ txSignature = await runtime.wallet.signAndSend(instructionOrTx);
710
+ }
711
+ setSignature(txSignature);
712
+ setStatus('success');
713
+ if (transactions && instructionsToRefresh.length > 0) {
714
+ for (const inst of instructionsToRefresh) {
715
+ const txDef = transactions[inst.instruction];
716
+ if (txDef?.refresh) {
717
+ for (const refreshTarget of txDef.refresh) {
718
+ try {
719
+ const key = typeof refreshTarget.key === 'function'
720
+ ? refreshTarget.key(inst.params)
721
+ : refreshTarget.key;
722
+ runtime.subscribe(refreshTarget.view, key);
723
+ }
724
+ catch (err) {
725
+ console.error('[Hyperstack] Error refreshing view after transaction:', err);
726
+ }
727
+ }
728
+ }
729
+ }
730
+ }
731
+ return txSignature;
732
+ }
733
+ catch (err) {
734
+ const errorMessage = err instanceof Error ? err.message : 'Transaction failed';
735
+ console.error('[Hyperstack] Transaction error:', errorMessage, err);
736
+ setStatus('error');
737
+ setError(errorMessage);
738
+ throw err;
739
+ }
740
+ };
741
+ const reset = () => {
742
+ setStatus('idle');
743
+ setError(undefined);
744
+ setSignature(undefined);
745
+ };
746
+ return {
747
+ submit,
748
+ status,
749
+ error,
750
+ signature,
751
+ reset
752
+ };
753
+ };
754
+ }
755
+
756
+ function defineStack(definition) {
757
+ if (!definition.name) {
758
+ throw new Error('[Hyperstack] Stack definition must have a name');
759
+ }
760
+ if (!definition.views || typeof definition.views !== 'object') {
761
+ throw new Error('[Hyperstack] Stack definition must have views');
762
+ }
763
+ return definition;
764
+ }
765
+ function useHyperstack(stack) {
766
+ if (!stack) {
767
+ throw new Error('[Hyperstack] Stack definition is required');
768
+ }
769
+ const { runtime } = useHyperstackContext();
770
+ const views = {};
771
+ try {
772
+ for (const [viewName, viewGroup] of Object.entries(stack.views)) {
773
+ views[viewName] = {};
774
+ if (typeof viewGroup === 'object' && viewGroup !== null) {
775
+ if ('state' in viewGroup && viewGroup.state) {
776
+ views[viewName].state = createStateViewHook(viewGroup.state, runtime);
777
+ }
778
+ if ('list' in viewGroup && viewGroup.list) {
779
+ views[viewName].list = createListViewHook(viewGroup.list, runtime);
780
+ }
781
+ }
782
+ }
783
+ }
784
+ catch (err) {
785
+ console.error('[Hyperstack] Error creating view hooks:', err);
786
+ throw err;
787
+ }
788
+ const tx = {};
789
+ try {
790
+ if (stack.transactions) {
791
+ for (const [txName, txDef] of Object.entries(stack.transactions)) {
792
+ tx[txName] = txDef.build;
793
+ }
794
+ }
795
+ tx.useMutation = createTxMutationHook(runtime, stack.transactions);
796
+ }
797
+ catch (err) {
798
+ console.error('[Hyperstack] Error creating transaction hooks:', err);
799
+ tx.useMutation = () => ({
800
+ submit: async () => { },
801
+ status: 'idle',
802
+ error: 'Failed to initialize transaction hooks',
803
+ signature: undefined,
804
+ reset: () => { }
805
+ });
806
+ }
807
+ return {
808
+ views,
809
+ tx,
810
+ helpers: stack.helpers || {},
811
+ store: runtime.store,
812
+ runtime
813
+ };
814
+ }
815
+
816
+ function createStateView(viewPath, options) {
817
+ return {
818
+ mode: 'state',
819
+ view: viewPath,
820
+ type: {},
821
+ transform: options?.transform
822
+ };
823
+ }
824
+ function createListView(viewPath, options) {
825
+ return {
826
+ mode: 'list',
827
+ view: viewPath,
828
+ type: {},
829
+ transform: options?.transform
830
+ };
831
+ }
832
+
833
+ export { ConnectionManager, DEFAULT_CONFIG, HyperStreamError, HyperstackProvider, createListView, createRuntime, createStateView, defineStack, useHyperstack, useHyperstackContext };
834
+ //# sourceMappingURL=index.esm.js.map