Initial commit: libp2p-native-bridge package
- Extracted libp2p component from main app - Created modular package structure with interfaces and implementations - Added dependency injection for NativeModules - Configured for IOR loading from Gitea - Added comprehensive README and documentation
This commit is contained in:
402
implementations/Libp2pComponent.ts
Normal file
402
implementations/Libp2pComponent.ts
Normal file
@@ -0,0 +1,402 @@
|
||||
/**
|
||||
* Native libp2p bridge component for React Native
|
||||
*/
|
||||
|
||||
import { NativeEventEmitter, NativeModules } from 'react-native';
|
||||
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';
|
||||
|
||||
// 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 cachedConnections: Connection[] = [];
|
||||
private options: Libp2pOptions;
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
get peerId(): PeerId | null {
|
||||
return this._peerId || null;
|
||||
}
|
||||
|
||||
get multiaddrs(): Multiaddr[] {
|
||||
return this._multiaddrs;
|
||||
}
|
||||
|
||||
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 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> {
|
||||
// Register protocols if any
|
||||
if (this.options.protocols && this.nativeModule.registerProtocolHandler) {
|
||||
for (const protocol of this.options.protocols) {
|
||||
await this.nativeModule.registerProtocolHandler(protocol.protocolId);
|
||||
}
|
||||
}
|
||||
|
||||
// Pass configuration options to native module including keypair
|
||||
const config = {
|
||||
tcpPort: this.options.config?.tcpPort,
|
||||
wsPort: this.options.config?.wsPort,
|
||||
// Convert Uint8Array to base64 for passing to native module
|
||||
keypair: this.options.keypair
|
||||
? {
|
||||
privateKey: btoa(String.fromCharCode(...this.options.keypair.privateKey)),
|
||||
publicKey: btoa(String.fromCharCode(...this.options.keypair.publicKey)),
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
|
||||
await this.nativeModule.startLibp2p(config);
|
||||
this._started = true;
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
try {
|
||||
await this.nativeModule.stopLibp2p();
|
||||
this._started = false;
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async dial(multiaddr: string): Promise<Connection> {
|
||||
await this.nativeModule.connectToPeer(multiaddr);
|
||||
|
||||
// Create connection object
|
||||
const peerId = multiaddr.match(/\/p2p\/([^/]+)/)?.[1] || '';
|
||||
return {
|
||||
id: `${peerId}-${Date.now()}`,
|
||||
remotePeer: new SimplePeerId(peerId),
|
||||
remoteAddr: new SimpleMultiaddr(multiaddr),
|
||||
stat: {
|
||||
direction: 'outbound',
|
||||
status: 'open',
|
||||
timeline: {
|
||||
open: Date.now(),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
Reference in New Issue
Block a user