@0xobelisk/react 1.2.0-pre.64

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,341 @@
1
+ /**
2
+ * Dubhe Provider - useRef Pattern for Client Management
3
+ *
4
+ * Features:
5
+ * - 🎯 Single client instances across application lifecycle
6
+ * - ⚡ useRef-based storage (no re-initialization on re-renders)
7
+ * - 🔧 Provider pattern for dependency injection
8
+ * - 🛡️ Complete type safety with strict TypeScript
9
+ * - 📦 Context-based client sharing
10
+ */
11
+
12
+ import React, { createContext, useContext, useRef, ReactNode } from 'react';
13
+ import { Dubhe } from '@0xobelisk/sui-client';
14
+ import { createDubheGraphqlClient } from '@0xobelisk/graphql-client';
15
+ import { createECSWorld } from '@0xobelisk/ecs';
16
+ import { useDubheConfig } from './config';
17
+ import type { DubheConfig, DubheReturn } from './types';
18
+
19
+ /**
20
+ * Context interface for Dubhe client instances
21
+ * All clients are stored using useRef to ensure single initialization
22
+ */
23
+ interface DubheContextValue {
24
+ getContract: () => Dubhe;
25
+ getGraphqlClient: () => any | null;
26
+ getEcsWorld: () => any | null;
27
+ getAddress: () => string;
28
+ getMetrics: () => {
29
+ initTime: number;
30
+ requestCount: number;
31
+ lastActivity: number;
32
+ };
33
+ config: DubheConfig;
34
+ }
35
+
36
+ /**
37
+ * Context for sharing Dubhe clients across the application
38
+ * Uses useRef pattern to ensure clients are created only once
39
+ */
40
+ const DubheContext = createContext<DubheContextValue | null>(null);
41
+
42
+ /**
43
+ * Props interface for DubheProvider component
44
+ */
45
+ interface DubheProviderProps {
46
+ /** Configuration for Dubhe initialization */
47
+ config: Partial<DubheConfig>;
48
+ /** Child components that will have access to Dubhe clients */
49
+ children: ReactNode;
50
+ }
51
+
52
+ /**
53
+ * DubheProvider Component - useRef Pattern Implementation
54
+ *
55
+ * This Provider uses useRef to store client instances, ensuring they are:
56
+ * 1. Created only once during component lifecycle
57
+ * 2. Persisted across re-renders without re-initialization
58
+ * 3. Shared efficiently via React Context
59
+ *
60
+ * Key advantages over useMemo:
61
+ * - useRef guarantees single initialization (useMemo can re-run on dependency changes)
62
+ * - No dependency array needed (eliminates potential re-initialization bugs)
63
+ * - Better performance for heavy client objects
64
+ * - Clearer separation of concerns via Provider pattern
65
+ *
66
+ * @param props - Provider props containing config and children
67
+ * @returns Provider component wrapping children with Dubhe context
68
+ *
69
+ * @example
70
+ * ```typescript
71
+ * // App root setup
72
+ * function App() {
73
+ * const dubheConfig = {
74
+ * network: 'devnet',
75
+ * packageId: '0x123...',
76
+ * metadata: contractMetadata,
77
+ * credentials: {
78
+ * secretKey: process.env.NEXT_PUBLIC_PRIVATE_KEY
79
+ * }
80
+ * };
81
+ *
82
+ * return (
83
+ * <DubheProvider config={dubheConfig}>
84
+ * <MyApplication />
85
+ * </DubheProvider>
86
+ * );
87
+ * }
88
+ * ```
89
+ */
90
+ export function DubheProvider({ config, children }: DubheProviderProps) {
91
+ // Merge configuration with defaults (only runs once)
92
+ const finalConfig = useDubheConfig(config);
93
+
94
+ // Track initialization start time (useRef ensures single timestamp)
95
+ const startTimeRef = useRef<number>(performance.now());
96
+
97
+ // useRef for contract instance - guarantees single initialization
98
+ // Unlike useMemo, useRef.current is never re-calculated
99
+ const contractRef = useRef<Dubhe | undefined>(undefined);
100
+ const getContract = (): Dubhe => {
101
+ if (!contractRef.current) {
102
+ try {
103
+ console.log('Initializing Dubhe contract instance (one-time)');
104
+ contractRef.current = new Dubhe({
105
+ networkType: finalConfig.network,
106
+ packageId: finalConfig.packageId,
107
+ metadata: finalConfig.metadata,
108
+ secretKey: finalConfig.credentials?.secretKey
109
+ });
110
+ } catch (error) {
111
+ console.error('Contract initialization failed:', error);
112
+ throw error;
113
+ }
114
+ }
115
+ return contractRef.current;
116
+ };
117
+
118
+ // useRef for GraphQL client instance - single initialization guaranteed
119
+ const graphqlClientRef = useRef<any | null>(null);
120
+ const hasInitializedGraphql = useRef(false);
121
+ const getGraphqlClient = (): any | null => {
122
+ if (!hasInitializedGraphql.current && finalConfig.dubheMetadata) {
123
+ try {
124
+ console.log('Initializing GraphQL client instance (one-time)');
125
+ graphqlClientRef.current = createDubheGraphqlClient({
126
+ endpoint: finalConfig.endpoints?.graphql || 'http://localhost:4000/graphql',
127
+ subscriptionEndpoint: finalConfig.endpoints?.websocket || 'ws://localhost:4000/graphql',
128
+ dubheMetadata: finalConfig.dubheMetadata
129
+ });
130
+ hasInitializedGraphql.current = true;
131
+ } catch (error) {
132
+ console.error('GraphQL client initialization failed:', error);
133
+ throw error;
134
+ }
135
+ }
136
+ return graphqlClientRef.current;
137
+ };
138
+
139
+ // useRef for ECS World instance - depends on GraphQL client
140
+ const ecsWorldRef = useRef<any | null>(null);
141
+ const hasInitializedEcs = useRef(false);
142
+ const getEcsWorld = (): any | null => {
143
+ const graphqlClient = getGraphqlClient();
144
+ if (!hasInitializedEcs.current && graphqlClient) {
145
+ try {
146
+ console.log('Initializing ECS World instance (one-time)');
147
+ ecsWorldRef.current = createECSWorld(graphqlClient, {
148
+ queryConfig: {
149
+ enableBatchOptimization: finalConfig.options?.enableBatchOptimization ?? true,
150
+ defaultCacheTimeout: finalConfig.options?.cacheTimeout ?? 5000
151
+ },
152
+ subscriptionConfig: {
153
+ defaultDebounceMs: finalConfig.options?.debounceMs ?? 100,
154
+ reconnectOnError: finalConfig.options?.reconnectOnError ?? true
155
+ }
156
+ });
157
+ hasInitializedEcs.current = true;
158
+ } catch (error) {
159
+ console.error('ECS World initialization failed:', error);
160
+ throw error;
161
+ }
162
+ }
163
+ return ecsWorldRef.current;
164
+ };
165
+
166
+ // Address getter - calculated from contract
167
+ const getAddress = (): string => {
168
+ return getContract().getAddress();
169
+ };
170
+
171
+ // Metrics getter - performance tracking
172
+ const getMetrics = () => ({
173
+ initTime: performance.now() - (startTimeRef.current || 0),
174
+ requestCount: 0, // Can be enhanced with actual tracking
175
+ lastActivity: Date.now()
176
+ });
177
+
178
+ // Context value - stable reference (no re-renders for consumers)
179
+ const contextValue: DubheContextValue = {
180
+ getContract,
181
+ getGraphqlClient,
182
+ getEcsWorld,
183
+ getAddress,
184
+ getMetrics,
185
+ config: finalConfig
186
+ };
187
+
188
+ return <DubheContext.Provider value={contextValue}>{children}</DubheContext.Provider>;
189
+ }
190
+
191
+ /**
192
+ * Custom hook to access Dubhe context
193
+ * Provides type-safe access to all Dubhe client instances
194
+ *
195
+ * @returns DubheContextValue with all client getters and config
196
+ * @throws Error if used outside of DubheProvider
197
+ *
198
+ * @example
199
+ * ```typescript
200
+ * function MyComponent() {
201
+ * const dubheContext = useDubheContext();
202
+ *
203
+ * const contract = dubheContext.getContract();
204
+ * const graphqlClient = dubheContext.getGraphqlClient();
205
+ * const ecsWorld = dubheContext.getEcsWorld();
206
+ * const address = dubheContext.getAddress();
207
+ *
208
+ * return <div>Connected as {address}</div>;
209
+ * }
210
+ * ```
211
+ */
212
+ export function useDubheContext(): DubheContextValue {
213
+ const context = useContext(DubheContext);
214
+
215
+ if (!context) {
216
+ throw new Error(
217
+ 'useDubheContext must be used within a DubheProvider. ' +
218
+ 'Make sure to wrap your app with <DubheProvider config={...}>'
219
+ );
220
+ }
221
+
222
+ return context;
223
+ }
224
+
225
+ /**
226
+ * Enhanced hook that mimics the original useDubhe API
227
+ * Uses the Provider pattern internally but maintains backward compatibility
228
+ *
229
+ * @returns DubheReturn object with all instances and metadata
230
+ *
231
+ * @example
232
+ * ```typescript
233
+ * function MyComponent() {
234
+ * const { contract, graphqlClient, ecsWorld, address } = useDubheFromProvider();
235
+ *
236
+ * const handleTransaction = async () => {
237
+ * const tx = new Transaction();
238
+ * await contract.tx.my_system.my_method({ tx });
239
+ * };
240
+ *
241
+ * return <button onClick={handleTransaction}>Execute</button>;
242
+ * }
243
+ * ```
244
+ */
245
+ export function useDubheFromProvider(): DubheReturn {
246
+ const context = useDubheContext();
247
+
248
+ // Get instances (lazy initialization via getters)
249
+ const contract = context.getContract();
250
+ const graphqlClient = context.getGraphqlClient();
251
+ const ecsWorld = context.getEcsWorld();
252
+ const address = context.getAddress();
253
+ const metrics = context.getMetrics();
254
+
255
+ // Enhanced contract with additional methods (similar to original implementation)
256
+ const enhancedContract = contract as any;
257
+
258
+ // Add transaction methods with error handling (if not already added)
259
+ if (!enhancedContract.txWithOptions) {
260
+ enhancedContract.txWithOptions = (system: string, method: string, options: any = {}) => {
261
+ return async (params: any) => {
262
+ try {
263
+ const startTime = performance.now();
264
+ const result = await contract.tx[system][method](params);
265
+ const executionTime = performance.now() - startTime;
266
+
267
+ if (process.env.NODE_ENV === 'development') {
268
+ console.log(
269
+ `Transaction ${system}.${method} completed in ${executionTime.toFixed(2)}ms`
270
+ );
271
+ }
272
+
273
+ options.onSuccess?.(result);
274
+ return result;
275
+ } catch (error) {
276
+ options.onError?.(error);
277
+ throw error;
278
+ }
279
+ };
280
+ };
281
+ }
282
+
283
+ // Add query methods with performance tracking (if not already added)
284
+ if (!enhancedContract.queryWithOptions) {
285
+ enhancedContract.queryWithOptions = (system: string, method: string, options: any = {}) => {
286
+ return async (params: any) => {
287
+ const startTime = performance.now();
288
+ const result = await contract.query[system][method](params);
289
+ const executionTime = performance.now() - startTime;
290
+
291
+ if (process.env.NODE_ENV === 'development') {
292
+ console.log(`Query ${system}.${method} completed in ${executionTime.toFixed(2)}ms`);
293
+ }
294
+
295
+ return result;
296
+ };
297
+ };
298
+ }
299
+
300
+ return {
301
+ contract: enhancedContract,
302
+ graphqlClient,
303
+ ecsWorld,
304
+ metadata: context.config.metadata,
305
+ network: context.config.network,
306
+ packageId: context.config.packageId,
307
+ dubheSchemaId: context.config.dubheSchemaId,
308
+ address,
309
+ options: context.config.options,
310
+ metrics
311
+ };
312
+ }
313
+
314
+ /**
315
+ * Individual client hooks for components that only need specific instances
316
+ * These are more efficient than useDubheFromProvider for single-client usage
317
+ */
318
+
319
+ /**
320
+ * Hook for accessing only the Dubhe contract instance
321
+ */
322
+ export function useDubheContractFromProvider(): Dubhe {
323
+ const { contract } = useDubheFromProvider();
324
+ return contract;
325
+ }
326
+
327
+ /**
328
+ * Hook for accessing only the GraphQL client instance
329
+ */
330
+ export function useDubheGraphQLFromProvider(): any | null {
331
+ const { getGraphqlClient } = useDubheContext();
332
+ return getGraphqlClient();
333
+ }
334
+
335
+ /**
336
+ * Hook for accessing only the ECS World instance
337
+ */
338
+ export function useDubheECSFromProvider(): any | null {
339
+ const { getEcsWorld } = useDubheContext();
340
+ return getEcsWorld();
341
+ }
@@ -0,0 +1,99 @@
1
+ import { SuiMoveNormalizedModules, Dubhe } from '@0xobelisk/sui-client';
2
+ import type { DubheGraphqlClient } from '@0xobelisk/graphql-client';
3
+ import type { DubheECSWorld } from '@0xobelisk/ecs';
4
+
5
+ /**
6
+ * Network type
7
+ */
8
+ export type NetworkType = 'mainnet' | 'testnet' | 'devnet' | 'localnet';
9
+
10
+ /**
11
+ * Modern Dubhe client configuration for auto-initialization
12
+ */
13
+ export interface DubheConfig {
14
+ /** Network type */
15
+ network: NetworkType;
16
+ /** Contract package ID */
17
+ packageId: string;
18
+ /** Dubhe Schema ID (optional, for enhanced features) */
19
+ dubheSchemaId: string;
20
+ /** Contract metadata (required for contract instantiation) */
21
+ metadata: SuiMoveNormalizedModules;
22
+ /** Dubhe metadata (enables GraphQL/ECS features) */
23
+ dubheMetadata?: any;
24
+ /** Authentication credentials */
25
+ credentials?: {
26
+ secretKey?: string;
27
+ mnemonics?: string;
28
+ };
29
+ /** Service endpoints configuration */
30
+ endpoints?: {
31
+ graphql?: string;
32
+ websocket?: string;
33
+ };
34
+ /** Performance and behavior options */
35
+ options?: {
36
+ /** Enable batch query optimization */
37
+ enableBatchOptimization?: boolean;
38
+ /** Default cache timeout (milliseconds) */
39
+ cacheTimeout?: number;
40
+ /** Request debounce delay (milliseconds) */
41
+ debounceMs?: number;
42
+ /** Auto-reconnect on WebSocket errors */
43
+ reconnectOnError?: boolean;
44
+ };
45
+ }
46
+
47
+ /**
48
+ * Return type for the main useDubhe hook
49
+ */
50
+ export interface DubheReturn {
51
+ /** Dubhe contract instance with enhanced methods */
52
+ contract: Dubhe & {
53
+ /** Enhanced transaction methods with options */
54
+ txWithOptions?: (
55
+ system: string,
56
+ method: string,
57
+ options?: any
58
+ ) => (params: any) => Promise<any>;
59
+ /** Enhanced query methods with options */
60
+ queryWithOptions?: (
61
+ system: string,
62
+ method: string,
63
+ options?: any
64
+ ) => (params: any) => Promise<any>;
65
+ };
66
+ /** GraphQL client (null if dubheMetadata not provided) */
67
+ graphqlClient: DubheGraphqlClient | null;
68
+ /** ECS World instance (null if GraphQL not available) */
69
+ ecsWorld: DubheECSWorld | null;
70
+ /** Contract metadata */
71
+ metadata: SuiMoveNormalizedModules;
72
+ /** Network type */
73
+ network: NetworkType;
74
+ /** Package ID */
75
+ packageId: string;
76
+ /** Dubhe Schema ID (if provided) */
77
+ dubheSchemaId?: string;
78
+ /** User address */
79
+ address: string;
80
+ /** Configuration options used */
81
+ options?: {
82
+ enableBatchOptimization?: boolean;
83
+ cacheTimeout?: number;
84
+ debounceMs?: number;
85
+ reconnectOnError?: boolean;
86
+ };
87
+ /** Performance metrics */
88
+ metrics?: {
89
+ initTime?: number;
90
+ requestCount?: number;
91
+ lastActivity?: number;
92
+ };
93
+ }
94
+
95
+ /**
96
+ * Compatibility alias for DubheReturn
97
+ * @deprecated Use DubheReturn instead for better consistency
98
+ */
99
+ export type ContractReturn = DubheReturn;
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Utility Functions for Dubhe Configuration Management
3
+ *
4
+ * Features:
5
+ * - Configuration validation and error handling
6
+ * - Smart configuration merging with proper type safety
7
+ * - Type-safe configuration validation
8
+ */
9
+
10
+ import type { DubheConfig, NetworkType } from './types';
11
+
12
+ /**
13
+ * Merge multiple configuration objects with proper deep merging
14
+ * Later configurations override earlier ones
15
+ *
16
+ * @param baseConfig - Base configuration (usually defaults)
17
+ * @param overrideConfig - Override configuration (user provided)
18
+ * @returns Merged configuration
19
+ */
20
+ export function mergeConfigurations(
21
+ baseConfig: Partial<DubheConfig>,
22
+ overrideConfig?: Partial<DubheConfig>
23
+ ): Partial<DubheConfig> {
24
+ if (!overrideConfig) {
25
+ return { ...baseConfig };
26
+ }
27
+
28
+ const result: Partial<DubheConfig> = { ...baseConfig };
29
+
30
+ // Merge top-level properties
31
+ Object.assign(result, overrideConfig);
32
+
33
+ // Deep merge nested objects
34
+ if (overrideConfig.credentials || baseConfig.credentials) {
35
+ result.credentials = {
36
+ ...baseConfig.credentials,
37
+ ...overrideConfig.credentials
38
+ };
39
+ }
40
+
41
+ if (overrideConfig.endpoints || baseConfig.endpoints) {
42
+ result.endpoints = {
43
+ ...baseConfig.endpoints,
44
+ ...overrideConfig.endpoints
45
+ };
46
+ }
47
+
48
+ if (overrideConfig.options || baseConfig.options) {
49
+ result.options = {
50
+ ...baseConfig.options,
51
+ ...overrideConfig.options
52
+ };
53
+ }
54
+
55
+ return result;
56
+ }
57
+
58
+ /**
59
+ * Validate configuration and ensure required fields are present
60
+ * Throws descriptive errors for missing required fields
61
+ *
62
+ * @param config - Configuration to validate
63
+ * @returns Validated and typed configuration
64
+ * @throws Error if required fields are missing or invalid
65
+ */
66
+ export function validateConfig(config: Partial<DubheConfig>): DubheConfig {
67
+ const errors: string[] = [];
68
+
69
+ // Check required fields
70
+ if (!config.network) {
71
+ errors.push('network is required');
72
+ }
73
+
74
+ if (!config.packageId) {
75
+ errors.push('packageId is required');
76
+ }
77
+
78
+ if (!config.metadata) {
79
+ errors.push('metadata is required');
80
+ } else {
81
+ // Basic metadata validation
82
+ if (typeof config.metadata !== 'object') {
83
+ errors.push('metadata must be an object');
84
+ } else if (Object.keys(config.metadata).length === 0) {
85
+ errors.push('metadata cannot be empty');
86
+ }
87
+ }
88
+
89
+ // Validate network type
90
+ if (config.network && !['mainnet', 'testnet', 'devnet', 'localnet'].includes(config.network)) {
91
+ errors.push(
92
+ `invalid network: ${config.network}. Must be one of: mainnet, testnet, devnet, localnet`
93
+ );
94
+ }
95
+
96
+ // Validate package ID format (enhanced check)
97
+ if (config.packageId) {
98
+ if (!config.packageId.startsWith('0x')) {
99
+ errors.push('packageId must start with 0x');
100
+ } else if (config.packageId.length < 3) {
101
+ errors.push('packageId must be longer than 0x');
102
+ } else if (!/^0x[a-fA-F0-9]+$/.test(config.packageId)) {
103
+ errors.push('packageId must contain only hexadecimal characters after 0x');
104
+ }
105
+ }
106
+
107
+ // Validate dubheMetadata if provided
108
+ if (config.dubheMetadata !== undefined) {
109
+ if (typeof config.dubheMetadata !== 'object' || config.dubheMetadata === null) {
110
+ errors.push('dubheMetadata must be an object');
111
+ } else if (!config.dubheMetadata.components && !config.dubheMetadata.resources) {
112
+ errors.push('dubheMetadata must contain components or resources');
113
+ }
114
+ }
115
+
116
+ // Validate credentials if provided
117
+ if (config.credentials) {
118
+ if (config.credentials.secretKey && typeof config.credentials.secretKey !== 'string') {
119
+ errors.push('credentials.secretKey must be a string');
120
+ }
121
+ if (config.credentials.mnemonics && typeof config.credentials.mnemonics !== 'string') {
122
+ errors.push('credentials.mnemonics must be a string');
123
+ }
124
+ }
125
+
126
+ // Validate URLs if provided
127
+ if (config.endpoints?.graphql && !isValidUrl(config.endpoints.graphql)) {
128
+ errors.push('endpoints.graphql must be a valid URL');
129
+ }
130
+
131
+ if (config.endpoints?.websocket && !isValidUrl(config.endpoints.websocket)) {
132
+ errors.push('endpoints.websocket must be a valid URL');
133
+ }
134
+
135
+ // Validate numeric options
136
+ if (
137
+ config.options?.cacheTimeout !== undefined &&
138
+ (typeof config.options.cacheTimeout !== 'number' || config.options.cacheTimeout < 0)
139
+ ) {
140
+ errors.push('options.cacheTimeout must be a non-negative number');
141
+ }
142
+
143
+ if (
144
+ config.options?.debounceMs !== undefined &&
145
+ (typeof config.options.debounceMs !== 'number' || config.options.debounceMs < 0)
146
+ ) {
147
+ errors.push('options.debounceMs must be a non-negative number');
148
+ }
149
+
150
+ if (errors.length > 0) {
151
+ const errorMessage = `Invalid Dubhe configuration (${errors.length} error${errors.length > 1 ? 's' : ''}):\n${errors.map((e) => `- ${e}`).join('\n')}`;
152
+ console.error('Configuration validation failed:', { errors, config });
153
+ throw new Error(errorMessage);
154
+ }
155
+
156
+ return config as DubheConfig;
157
+ }
158
+
159
+ /**
160
+ * Simple URL validation helper
161
+ *
162
+ * @param url - URL string to validate
163
+ * @returns true if URL is valid, false otherwise
164
+ */
165
+ function isValidUrl(url: string): boolean {
166
+ try {
167
+ new URL(url);
168
+ return true;
169
+ } catch {
170
+ return false;
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Generate a configuration summary for debugging
176
+ * Hides sensitive information like private keys
177
+ *
178
+ * @param config - Configuration to summarize
179
+ * @returns Safe configuration summary
180
+ */
181
+ export function getConfigSummary(config: DubheConfig): object {
182
+ return {
183
+ network: config.network,
184
+ packageId: config.packageId,
185
+ dubheSchemaId: config.dubheSchemaId,
186
+ hasMetadata: !!config.metadata,
187
+ hasDubheMetadata: !!config.dubheMetadata,
188
+ hasCredentials: !!config.credentials?.secretKey,
189
+ endpoints: config.endpoints,
190
+ options: config.options
191
+ };
192
+ }