@gethashd/bytecave-browser 1.0.1

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,253 @@
1
+ /**
2
+ * React Hooks for HASHD Protocol
3
+ *
4
+ * Provides hooks for loading content from ByteCave network using hashd:// URLs
5
+ */
6
+
7
+ import { useState, useEffect, useCallback, useRef } from 'react';
8
+ import type { ByteCaveClient } from '../client.js';
9
+ import { fetchHashdContent, parseHashdUrl, type HashdUrl } from '../protocol-handler.js';
10
+
11
+ export interface UseHashdContentOptions {
12
+ client: ByteCaveClient | null;
13
+ enabled?: boolean;
14
+ onSuccess?: (blobUrl: string) => void;
15
+ onError?: (error: Error) => void;
16
+ }
17
+
18
+ export interface UseHashdContentResult {
19
+ blobUrl: string | null;
20
+ data: Uint8Array | null;
21
+ mimeType: string | null;
22
+ loading: boolean;
23
+ error: Error | null;
24
+ cached: boolean;
25
+ refetch: () => void;
26
+ }
27
+
28
+ /**
29
+ * Hook for loading content from hashd:// URLs
30
+ *
31
+ * @example
32
+ * const { blobUrl, loading, error } = useHashdContent('hashd://abc123...', { client });
33
+ * if (loading) return <Spinner />;
34
+ * if (error) return <Error message={error.message} />;
35
+ * return <img src={blobUrl} />;
36
+ */
37
+ export function useHashdContent(
38
+ url: string | null | undefined,
39
+ options: UseHashdContentOptions
40
+ ): UseHashdContentResult {
41
+ const { client, enabled = true, onSuccess, onError } = options;
42
+
43
+ const [blobUrl, setBlobUrl] = useState<string | null>(null);
44
+ const [data, setData] = useState<Uint8Array | null>(null);
45
+ const [mimeType, setMimeType] = useState<string | null>(null);
46
+ const [loading, setLoading] = useState(false);
47
+ const [error, setError] = useState<Error | null>(null);
48
+ const [cached, setCached] = useState(false);
49
+ const [refetchTrigger, setRefetchTrigger] = useState(0);
50
+
51
+ const mountedRef = useRef(true);
52
+ const abortControllerRef = useRef<AbortController | null>(null);
53
+
54
+ const refetch = useCallback(() => {
55
+ setRefetchTrigger((prev: number) => prev + 1);
56
+ }, []);
57
+
58
+ useEffect(() => {
59
+ mountedRef.current = true;
60
+ return () => {
61
+ mountedRef.current = false;
62
+ // Abort any pending fetch
63
+ if (abortControllerRef.current) {
64
+ abortControllerRef.current.abort();
65
+ }
66
+ };
67
+ }, []);
68
+
69
+ useEffect(() => {
70
+ // Reset state when URL changes
71
+ if (!url || !enabled || !client) {
72
+ setBlobUrl(null);
73
+ setData(null);
74
+ setMimeType(null);
75
+ setError(null);
76
+ setCached(false);
77
+ setLoading(false);
78
+ return;
79
+ }
80
+
81
+ // Validate URL
82
+ let parsed: HashdUrl;
83
+ try {
84
+ parsed = parseHashdUrl(url);
85
+ } catch (err) {
86
+ setError(err as Error);
87
+ setLoading(false);
88
+ return;
89
+ }
90
+
91
+ // Abort previous fetch
92
+ if (abortControllerRef.current) {
93
+ abortControllerRef.current.abort();
94
+ }
95
+
96
+ const abortController = new AbortController();
97
+ abortControllerRef.current = abortController;
98
+
99
+ setLoading(true);
100
+ setError(null);
101
+
102
+ fetchHashdContent(parsed, client, { signal: abortController.signal })
103
+ .then(result => {
104
+ if (!mountedRef.current || abortController.signal.aborted) {
105
+ return;
106
+ }
107
+
108
+ setBlobUrl(result.blobUrl);
109
+ setData(result.data);
110
+ setMimeType(result.mimeType);
111
+ setCached(result.cached);
112
+ setLoading(false);
113
+
114
+ if (onSuccess) {
115
+ onSuccess(result.blobUrl);
116
+ }
117
+ })
118
+ .catch(err => {
119
+ if (!mountedRef.current || abortController.signal.aborted) {
120
+ return;
121
+ }
122
+
123
+ const error = err instanceof Error ? err : new Error(String(err));
124
+ setError(error);
125
+ setLoading(false);
126
+
127
+ if (onError) {
128
+ onError(error);
129
+ }
130
+ });
131
+
132
+ return () => {
133
+ abortController.abort();
134
+ };
135
+ }, [url, client, enabled, refetchTrigger, onSuccess, onError]);
136
+
137
+ return {
138
+ blobUrl,
139
+ data,
140
+ mimeType,
141
+ loading,
142
+ error,
143
+ cached,
144
+ refetch
145
+ };
146
+ }
147
+
148
+ /**
149
+ * Hook specifically for loading images from hashd:// URLs
150
+ * Includes image-specific optimizations and error handling
151
+ *
152
+ * @example
153
+ * const { src, loading, error } = useHashdImage('hashd://abc123...', { client });
154
+ * return <img src={src || placeholderImage} alt="..." />;
155
+ */
156
+ export function useHashdImage(
157
+ url: string | null | undefined,
158
+ options: UseHashdContentOptions & { placeholder?: string }
159
+ ): UseHashdContentResult & { src: string } {
160
+ const result = useHashdContent(url, options);
161
+
162
+ return {
163
+ ...result,
164
+ src: result.blobUrl || options.placeholder || ''
165
+ };
166
+ }
167
+
168
+ /**
169
+ * Hook for loading video/audio content from hashd:// URLs
170
+ * Optimized for media playback
171
+ *
172
+ * @example
173
+ * const { src, loading } = useHashdMedia('hashd://abc123...', { client });
174
+ * return <video src={src} controls />;
175
+ */
176
+ export function useHashdMedia(
177
+ url: string | null | undefined,
178
+ options: UseHashdContentOptions
179
+ ): UseHashdContentResult & { src: string } {
180
+ const result = useHashdContent(url, options);
181
+
182
+ return {
183
+ ...result,
184
+ src: result.blobUrl || ''
185
+ };
186
+ }
187
+
188
+ /**
189
+ * Hook for batch loading multiple hashd:// URLs
190
+ * Useful for galleries or lists
191
+ *
192
+ * @example
193
+ * const { results, loading, errors } = useHashdBatch(urls, { client });
194
+ */
195
+ export function useHashdBatch(
196
+ urls: (string | null | undefined)[],
197
+ options: UseHashdContentOptions
198
+ ): {
199
+ results: Map<string, UseHashdContentResult>;
200
+ loading: boolean;
201
+ errors: Map<string, Error>;
202
+ } {
203
+ const [results, setResults] = useState<Map<string, UseHashdContentResult>>(new Map());
204
+ const [loading, setLoading] = useState(false);
205
+ const [errors, setErrors] = useState<Map<string, Error>>(new Map());
206
+
207
+ useEffect(() => {
208
+ if (!options.client || !options.enabled) {
209
+ return;
210
+ }
211
+
212
+ const validUrls = urls.filter((url): url is string => !!url);
213
+
214
+ if (validUrls.length === 0) {
215
+ setResults(new Map());
216
+ setErrors(new Map());
217
+ setLoading(false);
218
+ return;
219
+ }
220
+
221
+ setLoading(true);
222
+ const newResults = new Map<string, UseHashdContentResult>();
223
+ const newErrors = new Map<string, Error>();
224
+
225
+ Promise.all(
226
+ validUrls.map(async (url) => {
227
+ try {
228
+ const parsed = parseHashdUrl(url);
229
+ const result = await fetchHashdContent(parsed, options.client!);
230
+
231
+ newResults.set(url, {
232
+ blobUrl: result.blobUrl,
233
+ data: result.data,
234
+ mimeType: result.mimeType,
235
+ loading: false,
236
+ error: null,
237
+ cached: result.cached,
238
+ refetch: () => {}
239
+ });
240
+ } catch (err) {
241
+ const error = err instanceof Error ? err : new Error(String(err));
242
+ newErrors.set(url, error);
243
+ }
244
+ })
245
+ ).finally(() => {
246
+ setResults(newResults);
247
+ setErrors(newErrors);
248
+ setLoading(false);
249
+ });
250
+ }, [urls.join(','), options.client, options.enabled]);
251
+
252
+ return { results, loading, errors };
253
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * React integration for ByteCave Browser
3
+ *
4
+ * Export all React hooks and components
5
+ */
6
+
7
+ export * from './hooks.js';
8
+ export * from './components.js';
9
+ export { useHashdUrl } from './useHashdUrl.js';
@@ -0,0 +1,68 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { useByteCaveContext } from '../provider.js';
3
+
4
+ interface UseHashdUrlResult {
5
+ blobUrl: string | null;
6
+ loading: boolean;
7
+ error: string | null;
8
+ }
9
+
10
+ /**
11
+ * Hook to convert hashd:// URLs to blob URLs
12
+ * Must be used within ByteCaveProvider
13
+ *
14
+ * @example
15
+ * const { blobUrl, loading, error } = useHashdUrl('hashd://abc123...');
16
+ * return <img src={blobUrl || ''} alt="..." />;
17
+ */
18
+ export function useHashdUrl(hashdUrl: string | null | undefined): UseHashdUrlResult {
19
+ const { retrieve } = useByteCaveContext();
20
+ const [blobUrl, setBlobUrl] = useState<string | null>(null);
21
+ const [loading, setLoading] = useState(false);
22
+ const [error, setError] = useState<string | null>(null);
23
+
24
+ useEffect(() => {
25
+ if (!hashdUrl || !hashdUrl.startsWith('hashd://')) {
26
+ setBlobUrl(null);
27
+ setLoading(false);
28
+ setError(null);
29
+ return;
30
+ }
31
+
32
+ const cid = hashdUrl.replace('hashd://', '').split('?')[0];
33
+
34
+ let mounted = true;
35
+ setLoading(true);
36
+ setError(null);
37
+
38
+ retrieve(cid)
39
+ .then(result => {
40
+ if (!mounted) return;
41
+
42
+ if (result.success && result.data) {
43
+ const dataCopy = new Uint8Array(result.data);
44
+ const blob = new Blob([dataCopy]);
45
+ const url = URL.createObjectURL(blob);
46
+ setBlobUrl(url);
47
+ setLoading(false);
48
+ } else {
49
+ setError(result.error || 'Failed to retrieve content');
50
+ setLoading(false);
51
+ }
52
+ })
53
+ .catch(err => {
54
+ if (!mounted) return;
55
+ setError(err.message || 'Failed to retrieve content');
56
+ setLoading(false);
57
+ });
58
+
59
+ return () => {
60
+ mounted = false;
61
+ if (blobUrl) {
62
+ URL.revokeObjectURL(blobUrl);
63
+ }
64
+ };
65
+ }, [hashdUrl, retrieve]);
66
+
67
+ return { blobUrl, loading, error };
68
+ }
package/src/types.ts ADDED
@@ -0,0 +1,60 @@
1
+ /**
2
+ * ByteCave Browser Client Types
3
+ */
4
+
5
+ export interface ByteCaveConfig {
6
+ vaultNodeRegistryAddress?: string; // Optional - VaultNodeRegistry contract address for node verification
7
+ contentRegistryAddress?: string; // Optional - ContentRegistry contract address for on-chain registration
8
+ rpcUrl?: string; // Optional - required if vaultNodeRegistryAddress is provided
9
+ appId: string; // Application identifier for storage authorization
10
+ directNodeAddrs?: string[]; // Direct node multiaddrs for WebRTC connections (no relay)
11
+ relayPeers?: string[]; // Relay node multiaddrs for circuit relay fallback
12
+ maxPeers?: number;
13
+ connectionTimeout?: number;
14
+ }
15
+
16
+ export interface PeerInfo {
17
+ peerId: string;
18
+ publicKey: string;
19
+ contentTypes: string[] | 'all';
20
+ connected: boolean;
21
+ latency?: number;
22
+ nodeId?: string;
23
+ isRegistered?: boolean;
24
+ owner?: string;
25
+ }
26
+
27
+ export interface StoreResult {
28
+ success: boolean;
29
+ cid?: string;
30
+ peerId?: string;
31
+ error?: string;
32
+ }
33
+
34
+ export interface RetrieveResult {
35
+ success: boolean;
36
+ data?: Uint8Array;
37
+ peerId?: string;
38
+ error?: string;
39
+ }
40
+
41
+ export type ConnectionState = 'disconnected' | 'connecting' | 'connected' | 'error';
42
+
43
+ export interface SignalingMessage {
44
+ type: 'offer' | 'answer' | 'ice-candidate';
45
+ from: string;
46
+ sdp?: string;
47
+ candidate?: {
48
+ candidate: string;
49
+ sdpMid?: string;
50
+ sdpMLineIndex?: number;
51
+ };
52
+ }
53
+
54
+ export interface NodeRegistryEntry {
55
+ nodeId: string;
56
+ owner: string;
57
+ publicKey: string;
58
+ url: string;
59
+ active: boolean;
60
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "lib": ["ES2020", "DOM"],
7
+ "jsx": "react",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "declaration": true,
13
+ "declarationMap": true,
14
+ "outDir": "./dist",
15
+ "rootDir": "./src"
16
+ },
17
+ "include": ["src/**/*"],
18
+ "exclude": ["node_modules", "dist"]
19
+ }
package/tsup.config.ts ADDED
@@ -0,0 +1,17 @@
1
+ import { defineConfig } from 'tsup';
2
+
3
+ export default defineConfig({
4
+ entry: {
5
+ index: 'src/index.ts',
6
+ 'protocol-handler': 'src/protocol-handler.ts',
7
+ 'react/index': 'src/react/index.ts'
8
+ },
9
+ format: ['cjs', 'esm'],
10
+ dts: false, // Will use tsc for type definitions
11
+ clean: true,
12
+ sourcemap: false,
13
+ external: ['react'],
14
+ esbuildOptions(options) {
15
+ options.jsx = 'automatic';
16
+ }
17
+ });