Merge feat/logger (#1)
Fixed encoding issue: changed from btoa(String.fromCharCode(...)) to Buffer.from().toString('base64')
Maintains persistent identity through private key storage
Provides better state management and error tracking
Supports connection acceptance/rejection flow
Properly handles node lifecycle events
Can be cleanly shut down with resource cleanup
Co-authored-by: Chris Daßler <chris.dassler@me.com>
Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
@@ -3,6 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { NativeEventEmitter, NativeModules } from 'react-native';
|
import { NativeEventEmitter, NativeModules } from 'react-native';
|
||||||
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
import { LIBP2P_CONFIG } from '../utils/constants';
|
import { LIBP2P_CONFIG } from '../utils/constants';
|
||||||
import type { ConnectionStatusEvent, PeerDiscoveredEvent, PeerInfoEvent } from '../utils/types';
|
import type { ConnectionStatusEvent, PeerDiscoveredEvent, PeerInfoEvent } from '../utils/types';
|
||||||
import type {
|
import type {
|
||||||
@@ -74,8 +75,13 @@ export class Libp2pComponent implements ILibp2pComponent {
|
|||||||
private _peerId?: PeerId;
|
private _peerId?: PeerId;
|
||||||
private _multiaddrs: Multiaddr[] = [];
|
private _multiaddrs: Multiaddr[] = [];
|
||||||
private _started: boolean = false;
|
private _started: boolean = false;
|
||||||
|
private _starting: boolean = false;
|
||||||
|
private _error: string | null = null;
|
||||||
private cachedConnections: Connection[] = [];
|
private cachedConnections: Connection[] = [];
|
||||||
private options: Libp2pOptions;
|
private options: Libp2pOptions;
|
||||||
|
private nodeStartedListener: any = null;
|
||||||
|
private nodeStoppedListener: any = null;
|
||||||
|
private errorListener: any = null;
|
||||||
|
|
||||||
constructor(options?: Libp2pOptions, nativeModules?: typeof NativeModules) {
|
constructor(options?: Libp2pOptions, nativeModules?: typeof NativeModules) {
|
||||||
// Allow dependency injection of NativeModules for testing
|
// Allow dependency injection of NativeModules for testing
|
||||||
@@ -90,6 +96,7 @@ export class Libp2pComponent implements ILibp2pComponent {
|
|||||||
|
|
||||||
this.setupNativeEventListeners();
|
this.setupNativeEventListeners();
|
||||||
this.setupProtocolHandlers();
|
this.setupProtocolHandlers();
|
||||||
|
this.setupStateListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
get peerId(): PeerId | null {
|
get peerId(): PeerId | null {
|
||||||
@@ -100,6 +107,14 @@ export class Libp2pComponent implements ILibp2pComponent {
|
|||||||
return this._multiaddrs;
|
return this._multiaddrs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get isStarting(): boolean {
|
||||||
|
return this._starting;
|
||||||
|
}
|
||||||
|
|
||||||
|
get error(): string | null {
|
||||||
|
return this._error;
|
||||||
|
}
|
||||||
|
|
||||||
private setupNativeEventListeners(): void {
|
private setupNativeEventListeners(): void {
|
||||||
// Map native events to js-libp2p style events
|
// Map native events to js-libp2p style events
|
||||||
|
|
||||||
@@ -195,6 +210,27 @@ export class Libp2pComponent implements ILibp2pComponent {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
private emit<K extends keyof Libp2pEvents>(event: K, detail: unknown): void {
|
||||||
const handlers = this.eventHandlers.get(event);
|
const handlers = this.eventHandlers.get(event);
|
||||||
if (handlers) {
|
if (handlers) {
|
||||||
@@ -206,6 +242,14 @@ export class Libp2pComponent implements ILibp2pComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async start(): Promise<void> {
|
async start(): Promise<void> {
|
||||||
|
if (this._started || this._starting) {
|
||||||
|
return; // Already started or starting
|
||||||
|
}
|
||||||
|
|
||||||
|
this._starting = true;
|
||||||
|
this._error = null;
|
||||||
|
|
||||||
|
try {
|
||||||
// Register protocols if any
|
// Register protocols if any
|
||||||
if (this.options.protocols && this.nativeModule.registerProtocolHandler) {
|
if (this.options.protocols && this.nativeModule.registerProtocolHandler) {
|
||||||
for (const protocol of this.options.protocols) {
|
for (const protocol of this.options.protocols) {
|
||||||
@@ -213,27 +257,65 @@ export class Libp2pComponent implements ILibp2pComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
// Pass configuration options to native module including keypair
|
||||||
const config = {
|
const config: any = {
|
||||||
tcpPort: this.options.config?.tcpPort,
|
tcpPort: this.options.config?.tcpPort,
|
||||||
wsPort: this.options.config?.wsPort,
|
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);
|
if (privateKeyBytes) {
|
||||||
|
// Convert Uint8Array to base64 for passing to native module
|
||||||
|
config.privateKey = Buffer.from(privateKeyBytes).toString('base64');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await this.nativeModule.startLibp2p(config);
|
||||||
|
|
||||||
|
// 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._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> {
|
async stop(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await this.nativeModule.stopLibp2p();
|
await this.nativeModule.stopLibp2p();
|
||||||
this._started = false;
|
this._started = false;
|
||||||
|
this._peerId = undefined;
|
||||||
|
this._multiaddrs = [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// If libp2p wasn't running, that's okay - just update our state
|
// If libp2p wasn't running, that's okay - just update our state
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
@@ -247,6 +329,24 @@ export class Libp2pComponent implements ILibp2pComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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> {
|
async dial(multiaddr: string): Promise<Connection> {
|
||||||
await this.nativeModule.connectToPeer(multiaddr);
|
await this.nativeModule.connectToPeer(multiaddr);
|
||||||
|
|
||||||
@@ -370,6 +470,20 @@ export class Libp2pComponent implements ILibp2pComponent {
|
|||||||
throw new Error('Protocol sending not supported on this platform');
|
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
|
// Type definition for CustomEventInit
|
||||||
|
|||||||
@@ -43,6 +43,9 @@ export interface ProtocolHandler {
|
|||||||
export interface ILibp2pComponent {
|
export interface ILibp2pComponent {
|
||||||
peerId: PeerId | null;
|
peerId: PeerId | null;
|
||||||
multiaddrs: Multiaddr[];
|
multiaddrs: Multiaddr[];
|
||||||
|
isStarted: boolean;
|
||||||
|
isStarting: boolean;
|
||||||
|
error: string | null;
|
||||||
|
|
||||||
start(): Promise<void>;
|
start(): Promise<void>;
|
||||||
stop(): Promise<void>;
|
stop(): Promise<void>;
|
||||||
@@ -52,6 +55,9 @@ export interface ILibp2pComponent {
|
|||||||
sendProtocolData(peerId: string, protocolId: string, data: Uint8Array): Promise<void>;
|
sendProtocolData(peerId: string, protocolId: string, data: Uint8Array): Promise<void>;
|
||||||
refreshDiscovery(): Promise<void>;
|
refreshDiscovery(): Promise<void>;
|
||||||
pingPeer(peerId: string): Promise<{ success: boolean; rtt?: number; peerId: string }>;
|
pingPeer(peerId: string): Promise<{ success: boolean; rtt?: number; peerId: string }>;
|
||||||
|
acceptConnection(peerId: string): Promise<void>;
|
||||||
|
rejectConnection(peerId: string): Promise<void>;
|
||||||
|
cleanup(): void;
|
||||||
|
|
||||||
addEventListener<K extends keyof Libp2pEvents>(
|
addEventListener<K extends keyof Libp2pEvents>(
|
||||||
event: K,
|
event: K,
|
||||||
|
|||||||
Reference in New Issue
Block a user