Add missing LoggerComponent import that was causing ReferenceError when trying to use logger in start() method
559 lines
18 KiB
TypeScript
559 lines
18 KiB
TypeScript
/**
|
|
* Native libp2p bridge component for React Native
|
|
*/
|
|
|
|
import { NativeEventEmitter, NativeModules } from 'react-native';
|
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
import { LoggerComponent } from 'ior:gitea:gitea.metatrom.net:universal-components/logger@1.0.0';
|
|
import { LIBP2P_CONFIG } from '../utils/constants';
|
|
import type { ConnectionStatusEvent, PeerDiscoveredEvent, PeerInfoEvent } from '../utils/types';
|
|
import type {
|
|
Connection,
|
|
ILibp2pComponent,
|
|
Libp2pEvents,
|
|
Libp2pOptions,
|
|
Multiaddr,
|
|
PeerId,
|
|
PeerInfo,
|
|
} from '../interfaces/ILibp2pComponent';
|
|
|
|
// Create logger instance for this module
|
|
const logger = new LoggerComponent('Libp2pComponent');
|
|
|
|
// Helper class to create PeerId-like objects from strings
|
|
export class SimplePeerId implements PeerId {
|
|
constructor(private id: string) {}
|
|
|
|
toString(): string {
|
|
return this.id;
|
|
}
|
|
|
|
toBytes(): Uint8Array {
|
|
return new TextEncoder().encode(this.id);
|
|
}
|
|
|
|
equals(other: PeerId): boolean {
|
|
return this.toString() === other.toString();
|
|
}
|
|
}
|
|
|
|
// Helper class to create Multiaddr-like objects from strings
|
|
export class SimpleMultiaddr implements Multiaddr {
|
|
bytes: Uint8Array;
|
|
|
|
constructor(private addr: string) {
|
|
this.bytes = new TextEncoder().encode(addr);
|
|
}
|
|
|
|
toString(): string {
|
|
return this.addr;
|
|
}
|
|
|
|
protos(): Array<{ code: number; name: string }> {
|
|
// Simple parsing of multiaddr components
|
|
const parts = this.addr.split('/').filter((p) => p);
|
|
const protos = [];
|
|
|
|
for (let i = 0; i < parts.length; i += 2) {
|
|
const name = parts[i];
|
|
protos.push({ code: 0, name }); // Simplified - real implementation would have proper codes
|
|
}
|
|
|
|
return protos;
|
|
}
|
|
|
|
getPeerId(): string | null {
|
|
const match = this.addr.match(/\/p2p\/([^/]+)/);
|
|
return match ? match[1] : null;
|
|
}
|
|
}
|
|
|
|
// Type for event handler functions
|
|
type EventHandler<T = unknown> = (evt: T) => void;
|
|
|
|
// Native implementation that wraps our iOS/Android modules
|
|
export class Libp2pComponent implements ILibp2pComponent {
|
|
private nativeModule: any;
|
|
private eventEmitter: NativeEventEmitter;
|
|
private eventHandlers: Map<string, Set<EventHandler>> = new Map();
|
|
private _peerId?: PeerId;
|
|
private _multiaddrs: Multiaddr[] = [];
|
|
private _started: boolean = false;
|
|
private _starting: boolean = false;
|
|
private _error: string | null = null;
|
|
private cachedConnections: Connection[] = [];
|
|
private options: Libp2pOptions;
|
|
private nodeStartedListener: any = null;
|
|
private nodeStoppedListener: any = null;
|
|
private errorListener: any = null;
|
|
|
|
constructor(options?: Libp2pOptions, nativeModules?: typeof NativeModules) {
|
|
// Allow dependency injection of NativeModules for testing
|
|
const modules = nativeModules || NativeModules;
|
|
this.nativeModule = modules.Libp2pModule;
|
|
this.eventEmitter = new NativeEventEmitter(this.nativeModule);
|
|
this.options = options || {};
|
|
|
|
if (!this.nativeModule) {
|
|
throw new Error('Libp2p native module not found');
|
|
}
|
|
|
|
this.setupNativeEventListeners();
|
|
this.setupProtocolHandlers();
|
|
this.setupStateListeners();
|
|
}
|
|
|
|
get peerId(): PeerId | null {
|
|
return this._peerId || null;
|
|
}
|
|
|
|
get multiaddrs(): Multiaddr[] {
|
|
return this._multiaddrs;
|
|
}
|
|
|
|
get isStarting(): boolean {
|
|
return this._starting;
|
|
}
|
|
|
|
get error(): string | null {
|
|
return this._error;
|
|
}
|
|
|
|
private setupNativeEventListeners(): void {
|
|
// Map native events to js-libp2p style events
|
|
|
|
// Peer info update
|
|
this.eventEmitter.addListener('onPeerInfo', ({ peerId, multiaddrs }: PeerInfoEvent) => {
|
|
this._peerId = new SimplePeerId(peerId);
|
|
this._multiaddrs = multiaddrs.map((addr: string) => new SimpleMultiaddr(addr));
|
|
this.emit('self:peer:update', {
|
|
peerId: this._peerId,
|
|
multiaddrs: this._multiaddrs,
|
|
});
|
|
});
|
|
|
|
// Peer discovery
|
|
this.eventEmitter.addListener(
|
|
'onPeerDiscovered',
|
|
({ peerId, addresses, multiaddrs }: PeerDiscoveredEvent) => {
|
|
const addrs = (multiaddrs || addresses || []).map(
|
|
(addr: string) => new SimpleMultiaddr(addr),
|
|
);
|
|
this.emit('peer:discovery', {
|
|
id: new SimplePeerId(peerId),
|
|
multiaddrs: addrs,
|
|
});
|
|
},
|
|
);
|
|
|
|
// Peer lost (for mDNS service lost events)
|
|
this.eventEmitter.addListener('onPeerLost', ({ peerId }: { peerId: string }) => {
|
|
this.emit('peer:lost', {
|
|
id: new SimplePeerId(peerId),
|
|
});
|
|
});
|
|
|
|
// Connection events
|
|
this.eventEmitter.addListener(
|
|
'onConnectionStatus',
|
|
({ peerId, status, direction, multiaddr }: ConnectionStatusEvent) => {
|
|
const connection: Connection = {
|
|
id: `${peerId}-${Date.now()}`,
|
|
remotePeer: new SimplePeerId(peerId),
|
|
remoteAddr: new SimpleMultiaddr(multiaddr || ''),
|
|
stat: {
|
|
direction: direction || 'outbound',
|
|
status:
|
|
status === LIBP2P_CONFIG.CONNECTION_STATUS.CONNECTED
|
|
? 'open'
|
|
: status === LIBP2P_CONFIG.CONNECTION_STATUS.PENDING
|
|
? 'pending'
|
|
: status === LIBP2P_CONFIG.CONNECTION_STATUS.DISCONNECTED
|
|
? 'closed'
|
|
: 'closing',
|
|
timeline: {
|
|
open: Date.now(),
|
|
},
|
|
},
|
|
};
|
|
|
|
if (
|
|
status === LIBP2P_CONFIG.CONNECTION_STATUS.CONNECTED ||
|
|
status === LIBP2P_CONFIG.CONNECTION_STATUS.PENDING
|
|
) {
|
|
// Emit peer:connect for both pending and connected states
|
|
// The UI will differentiate based on connection.stat.status
|
|
this.emit('peer:connect', connection);
|
|
this.emit('connection:open', connection);
|
|
} else if (
|
|
status === LIBP2P_CONFIG.CONNECTION_STATUS.DISCONNECTED ||
|
|
status === LIBP2P_CONFIG.CONNECTION_STATUS.FAILED
|
|
) {
|
|
connection.stat.timeline.close = Date.now();
|
|
connection.stat.status = 'closed';
|
|
this.emit('peer:disconnect', connection);
|
|
this.emit('connection:close', connection);
|
|
}
|
|
},
|
|
);
|
|
}
|
|
|
|
private setupProtocolHandlers(): void {
|
|
// Listen for protocol data events
|
|
this.eventEmitter.addListener(
|
|
'onProtocolData',
|
|
(data: { protocolId: string; peerId: string; data?: number[] }) => {
|
|
// Find matching protocol handler
|
|
const handler = this.options.protocols?.find((p) => p.protocolId === data.protocolId);
|
|
if (handler) {
|
|
// Convert number array to Uint8Array if needed
|
|
const uint8Data = data.data ? new Uint8Array(data.data) : undefined;
|
|
handler.handler({ peerId: data.peerId, data: uint8Data });
|
|
}
|
|
},
|
|
);
|
|
}
|
|
|
|
private setupStateListeners(): void {
|
|
// Listen for node started event
|
|
this.nodeStartedListener = this.eventEmitter.addListener('onNodeStarted', () => {
|
|
this._started = true;
|
|
this._starting = false;
|
|
this._error = null;
|
|
});
|
|
|
|
// Listen for node stopped event
|
|
this.nodeStoppedListener = this.eventEmitter.addListener('onNodeStopped', () => {
|
|
this._started = false;
|
|
this._starting = false;
|
|
});
|
|
|
|
// Listen for error events
|
|
this.errorListener = this.eventEmitter.addListener('onError', (event: any) => {
|
|
this._error = event.error || 'Unknown error';
|
|
this._starting = false;
|
|
});
|
|
}
|
|
|
|
private emit<K extends keyof Libp2pEvents>(event: K, detail: unknown): void {
|
|
const handlers = this.eventHandlers.get(event);
|
|
if (handlers) {
|
|
const customEvent = new CustomEvent(event, { detail });
|
|
for (const handler of handlers) {
|
|
handler(customEvent as Libp2pEvents[K]);
|
|
}
|
|
}
|
|
}
|
|
|
|
async start(): Promise<void> {
|
|
logger.info('[TypeScript] start() called, _started:', this._started, '_starting:', this._starting);
|
|
|
|
if (this._started || this._starting) {
|
|
logger.info('[TypeScript] Already started or starting, returning early');
|
|
return; // Already started or starting
|
|
}
|
|
|
|
this._starting = true;
|
|
this._error = null;
|
|
logger.info('[TypeScript] Setting _starting to true, proceeding with start');
|
|
|
|
try {
|
|
// Register protocols if any
|
|
if (this.options.protocols && this.nativeModule.registerProtocolHandler) {
|
|
for (const protocol of this.options.protocols) {
|
|
await this.nativeModule.registerProtocolHandler(protocol.protocolId);
|
|
}
|
|
}
|
|
|
|
// Load stored private key if available and not provided
|
|
let privateKeyBytes = this.options.keypair?.privateKey;
|
|
if (!privateKeyBytes) {
|
|
try {
|
|
const storedKey = await AsyncStorage.getItem('libp2p_private_key');
|
|
if (storedKey) {
|
|
privateKeyBytes = new Uint8Array(JSON.parse(storedKey));
|
|
}
|
|
} catch (err) {
|
|
// Failed to load stored key, will generate new one
|
|
}
|
|
}
|
|
|
|
// Pass configuration options to native module including keypair
|
|
const config: any = {
|
|
tcpPort: this.options.config?.tcpPort,
|
|
wsPort: this.options.config?.wsPort,
|
|
};
|
|
|
|
if (privateKeyBytes) {
|
|
// Convert Uint8Array to base64 for passing to native module
|
|
config.privateKey = Buffer.from(privateKeyBytes).toString('base64');
|
|
}
|
|
|
|
logger.debug('[Libp2pComponent] About to call nativeModule.startLibp2p with config:', config);
|
|
logger.debug('[Libp2pComponent] nativeModule exists:', !!this.nativeModule);
|
|
logger.debug('[Libp2pComponent] nativeModule.startLibp2p exists:', !!this.nativeModule?.startLibp2p);
|
|
|
|
let result;
|
|
try {
|
|
result = await this.nativeModule.startLibp2p(config);
|
|
logger.info('[Libp2pComponent] startLibp2p returned:', result);
|
|
} catch (error) {
|
|
logger.error('[Libp2pComponent] startLibp2p failed:', error);
|
|
throw error;
|
|
}
|
|
|
|
// Update internal state from result
|
|
if (result.peerId) {
|
|
this._peerId = new SimplePeerId(result.peerId);
|
|
}
|
|
if (result.multiaddrs) {
|
|
this._multiaddrs = result.multiaddrs.map((addr: string) => new SimpleMultiaddr(addr));
|
|
}
|
|
|
|
// Store the private key if we generated a new one
|
|
if (result.privateKey && !privateKeyBytes) {
|
|
try {
|
|
const keyBytes = Buffer.from(result.privateKey, 'base64');
|
|
await AsyncStorage.setItem('libp2p_private_key', JSON.stringify(Array.from(keyBytes)));
|
|
} catch (err) {
|
|
// Failed to store key, but continue
|
|
}
|
|
}
|
|
|
|
this._started = true;
|
|
this._starting = false;
|
|
} catch (error) {
|
|
this._starting = false;
|
|
this._error = error instanceof Error ? error.message : 'Failed to start';
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async stop(): Promise<void> {
|
|
try {
|
|
await this.nativeModule.stopLibp2p();
|
|
this._started = false;
|
|
this._peerId = undefined;
|
|
this._multiaddrs = [];
|
|
} catch (error) {
|
|
// If libp2p wasn't running, that's okay - just update our state
|
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
const errorCode = (error as { code?: string })?.code;
|
|
if (errorMessage.includes('not running') || errorCode === 'LIBP2P_ERROR') {
|
|
this._started = false;
|
|
} else {
|
|
// Re-throw other errors
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
|
|
cleanup(): void {
|
|
// Remove state listeners
|
|
if (this.nodeStartedListener) {
|
|
this.nodeStartedListener.remove();
|
|
this.nodeStartedListener = null;
|
|
}
|
|
if (this.nodeStoppedListener) {
|
|
this.nodeStoppedListener.remove();
|
|
this.nodeStoppedListener = null;
|
|
}
|
|
if (this.errorListener) {
|
|
this.errorListener.remove();
|
|
this.errorListener = null;
|
|
}
|
|
// Clear event handlers
|
|
this.eventHandlers.clear();
|
|
}
|
|
|
|
async dial(multiaddr: string): Promise<Connection> {
|
|
console.log(`[Libp2pComponent] dial() called with multiaddr: ${multiaddr}`);
|
|
|
|
try {
|
|
const result = await this.nativeModule.connectToPeer(multiaddr);
|
|
console.log('[Libp2pComponent] connectToPeer result:', result);
|
|
|
|
// Create connection object
|
|
// Try to extract peer ID from result or multiaddr
|
|
let peerId = result?.peer_id || result?.peerId || '';
|
|
if (!peerId) {
|
|
// Try to extract from multiaddr if not in result
|
|
peerId = multiaddr.match(/\/p2p\/([^/]+)/)?.[1] || '';
|
|
}
|
|
|
|
// If still no peer ID, this might be a connection without known peer ID
|
|
// The real peer ID will be determined during handshake
|
|
if (!peerId || peerId === 'pending') {
|
|
console.log('[Libp2pComponent] Dialing without known peer ID, will be determined during handshake');
|
|
peerId = 'pending';
|
|
}
|
|
|
|
const connection = {
|
|
id: `${peerId}-${Date.now()}`,
|
|
remotePeer: new SimplePeerId(peerId),
|
|
remoteAddr: new SimpleMultiaddr(multiaddr),
|
|
stat: {
|
|
direction: 'outbound' as const,
|
|
status: 'open' as const,
|
|
timeline: {
|
|
open: Date.now(),
|
|
},
|
|
},
|
|
};
|
|
|
|
console.log('[Libp2pComponent] Returning connection:', connection);
|
|
return connection;
|
|
} catch (error) {
|
|
console.error('[Libp2pComponent] dial error:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async hangUp(peerId: string): Promise<void> {
|
|
const result = await this.nativeModule.hangUp(peerId);
|
|
|
|
if (!result?.success) {
|
|
// No active connections to peer - this is okay
|
|
}
|
|
}
|
|
|
|
async getConnections(peerId?: string): Promise<Connection[]> {
|
|
try {
|
|
const rawConnections = await this.nativeModule.getConnections();
|
|
|
|
// Transform native connections to match the Connection interface
|
|
this.cachedConnections = (rawConnections || []).map((conn: any) => {
|
|
// Ensure the connection has the expected structure
|
|
// Handle different formats from native modules
|
|
let peerIdStr = conn.peerId || 'unknown';
|
|
if (conn.remotePeer) {
|
|
// If remotePeer is an object with toString property (from native module)
|
|
if (typeof conn.remotePeer.toString === 'string') {
|
|
peerIdStr = conn.remotePeer.toString;
|
|
}
|
|
// If remotePeer is a PeerId object with toString method
|
|
else if (typeof conn.remotePeer.toString === 'function') {
|
|
peerIdStr = conn.remotePeer.toString();
|
|
}
|
|
}
|
|
|
|
const connection: Connection = {
|
|
id: conn.id || `${peerIdStr}-${Date.now()}`,
|
|
remotePeer: new SimplePeerId(peerIdStr),
|
|
remoteAddr: conn.remoteAddr?.toString
|
|
? new SimpleMultiaddr(
|
|
typeof conn.remoteAddr.toString === 'string'
|
|
? conn.remoteAddr.toString
|
|
: conn.remoteAddr.toString(),
|
|
)
|
|
: new SimpleMultiaddr('/unknown'),
|
|
stat: conn.stat || {
|
|
direction: conn.direction || 'outbound',
|
|
status: conn.status || 'open',
|
|
timeline: conn.timeline || {
|
|
open: Date.now(),
|
|
},
|
|
},
|
|
};
|
|
return connection;
|
|
});
|
|
|
|
// Filter by peerId if provided
|
|
if (peerId) {
|
|
return this.cachedConnections.filter((conn) => conn.remotePeer.toString() === peerId);
|
|
}
|
|
|
|
return this.cachedConnections;
|
|
} catch (_error) {
|
|
// Failed to get connections
|
|
return [];
|
|
}
|
|
}
|
|
|
|
addEventListener<K extends keyof Libp2pEvents>(
|
|
event: K,
|
|
handler: (evt: Libp2pEvents[K]) => void,
|
|
): void {
|
|
if (!this.eventHandlers.has(event)) {
|
|
this.eventHandlers.set(event, new Set());
|
|
}
|
|
this.eventHandlers.get(event)?.add(handler as EventHandler);
|
|
}
|
|
|
|
removeEventListener<K extends keyof Libp2pEvents>(
|
|
event: K,
|
|
handler: (evt: Libp2pEvents[K]) => void,
|
|
): void {
|
|
const handlers = this.eventHandlers.get(event);
|
|
if (handlers) {
|
|
handlers.delete(handler as EventHandler);
|
|
}
|
|
}
|
|
|
|
async refreshDiscovery(): Promise<void> {
|
|
if (this.nativeModule.refreshDiscovery) {
|
|
await this.nativeModule.refreshDiscovery();
|
|
}
|
|
}
|
|
|
|
async pingPeer(peerId: string): Promise<{ success: boolean; rtt?: number; peerId: string }> {
|
|
if (this.nativeModule.pingPeer) {
|
|
return await this.nativeModule.pingPeer(peerId);
|
|
} else {
|
|
throw new Error('Ping not supported on this platform');
|
|
}
|
|
}
|
|
|
|
async sendProtocolData(peerId: string, protocolId: string, data: Uint8Array): Promise<void> {
|
|
if (this.nativeModule.sendProtocolData) {
|
|
// Convert Uint8Array to regular array for native module
|
|
const dataArray = Array.from(data);
|
|
await this.nativeModule.sendProtocolData(peerId, protocolId, dataArray);
|
|
} else {
|
|
throw new Error('Protocol sending not supported on this platform');
|
|
}
|
|
}
|
|
|
|
async acceptConnection(peerId: string): Promise<void> {
|
|
if (!this.nativeModule.acceptConnection) {
|
|
throw new Error('acceptConnection not supported by native module');
|
|
}
|
|
await this.nativeModule.acceptConnection(peerId);
|
|
}
|
|
|
|
async rejectConnection(peerId: string): Promise<void> {
|
|
if (!this.nativeModule.rejectConnection) {
|
|
throw new Error('rejectConnection not supported by native module');
|
|
}
|
|
await this.nativeModule.rejectConnection(peerId);
|
|
}
|
|
}
|
|
|
|
// Type definition for CustomEventInit
|
|
interface CustomEventInit<T = unknown> {
|
|
detail?: T;
|
|
bubbles?: boolean;
|
|
cancelable?: boolean;
|
|
}
|
|
|
|
// Polyfill CustomEvent for React Native
|
|
if (typeof CustomEvent === 'undefined') {
|
|
const globalObj = global as typeof globalThis & { CustomEvent: typeof CustomEvent };
|
|
globalObj.CustomEvent = class CustomEvent<T = unknown> {
|
|
readonly type: string;
|
|
readonly detail: T;
|
|
readonly bubbles: boolean;
|
|
readonly cancelable: boolean;
|
|
|
|
constructor(type: string, eventInitDict?: CustomEventInit<T>) {
|
|
this.type = type;
|
|
this.detail = eventInitDict?.detail as T;
|
|
this.bubbles = eventInitDict?.bubbles || false;
|
|
this.cancelable = eventInitDict?.cancelable || false;
|
|
}
|
|
|
|
preventDefault() {}
|
|
stopPropagation() {}
|
|
stopImmediatePropagation() {}
|
|
} as any;
|
|
} |