@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.
- package/AGENTS.md +176 -0
- package/README.md +699 -0
- package/dist/__tests__/p2p-protocols.test.d.ts +10 -0
- package/dist/chunk-EEZWRIUI.js +160 -0
- package/dist/chunk-OJEETLZQ.js +7087 -0
- package/dist/client.d.ts +107 -0
- package/dist/contracts/ContentRegistry.d.ts +1 -0
- package/dist/discovery.d.ts +28 -0
- package/dist/index.cjs +7291 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +50 -0
- package/dist/p2p-protocols.d.ts +114 -0
- package/dist/protocol-handler.cjs +185 -0
- package/dist/protocol-handler.d.ts +57 -0
- package/dist/protocol-handler.js +18 -0
- package/dist/provider.d.ts +59 -0
- package/dist/react/components.d.ts +90 -0
- package/dist/react/hooks.d.ts +67 -0
- package/dist/react/index.cjs +6344 -0
- package/dist/react/index.d.ts +8 -0
- package/dist/react/index.js +23 -0
- package/dist/react/useHashdUrl.d.ts +15 -0
- package/dist/types.d.ts +53 -0
- package/package.json +77 -0
- package/src/__tests__/p2p-protocols.test.ts +292 -0
- package/src/client.ts +876 -0
- package/src/contracts/ContentRegistry.ts +6 -0
- package/src/discovery.ts +79 -0
- package/src/index.ts +59 -0
- package/src/p2p-protocols.ts +451 -0
- package/src/protocol-handler.ts +271 -0
- package/src/provider.tsx +275 -0
- package/src/react/components.tsx +177 -0
- package/src/react/hooks.ts +253 -0
- package/src/react/index.ts +9 -0
- package/src/react/useHashdUrl.ts +68 -0
- package/src/types.ts +60 -0
- package/tsconfig.json +19 -0
- package/tsup.config.ts +17 -0
|
@@ -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,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
|
+
});
|