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:
Chris Daßler
2025-08-29 11:18:37 +02:00
commit 6f1d6ec37b
14 changed files with 2060 additions and 0 deletions

237
.gitignore vendored Normal file
View File

@@ -0,0 +1,237 @@
# Created by https://www.toptal.com/developers/gitignore/api/macos,windows,linux,visualstudiocode,node
# Edit at https://www.toptal.com/developers/gitignore?templates=macos,windows,linux,visualstudiocode,node
### Linux ###
*~
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
# .nfs files are created when an open file is removed but is still being accessed
.nfs*
### macOS ###
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### macOS Patch ###
# iCloud generated files
*.icloud
### Node ###
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
### Node Patch ###
# Serverless Webpack directories
.webpack/
# Optional stylelint cache
# SvelteKit build / generate output
.svelte-kit
### VisualStudioCode ###
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
# Local History for Visual Studio Code
.history/
# Built Visual Studio Code Extensions
*.vsix
### VisualStudioCode Patch ###
# Ignore all local history of files
.history
.ionide
### Windows ###
# Windows thumbnail cache files
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
# End of https://www.toptal.com/developers/gitignore/api/macos,windows,linux,visualstudiocode,node

233
README.md Normal file
View File

@@ -0,0 +1,233 @@
# libp2p-native-bridge
Native libp2p bridge for React Native applications, providing a unified interface for iOS and Android libp2p implementations.
## Installation
### Via IOR (Interoperable Object Reference)
```typescript
import { Libp2pComponent } from 'ior:gitea:gitea.metatrom.net:universal-components/libp2p-native-bridge@1.0.0';
```
### Via npm/yarn (if published)
```bash
npm install @metatrom/libp2p-native-bridge
# or
yarn add @metatrom/libp2p-native-bridge
```
## Features
- 🌐 Unified libp2p interface for React Native
- 📱 iOS and Android native module support
- 🔧 Configurable settings management
- 🎛️ Settings UI component
- 🔌 Protocol handler support
- 🔍 Peer discovery and connection management
- 🚀 TypeScript support with full type definitions
## Usage
### Basic Setup
```typescript
import { Libp2pComponent } from '@metatrom/libp2p-native-bridge';
// Create libp2p instance
const libp2p = new Libp2pComponent({
config: {
tcpPort: 10000,
wsPort: 10005,
},
protocols: [
{
protocolId: '/my-protocol/1.0.0',
handler: async ({ peerId, data }) => {
console.log(`Received data from ${peerId}:`, data);
},
},
],
});
// Start the node
await libp2p.start();
// Listen for peer discovery
libp2p.addEventListener('peer:discovery', (evt) => {
console.log('Discovered peer:', evt.detail.id.toString());
});
// Connect to a peer
await libp2p.dial('/ip4/192.168.1.2/tcp/10000/p2p/12D3KooW...');
```
### Using with React Hook
```typescript
import { useLibp2p } from './hooks/useLibp2p';
import { Libp2pComponent } from '@metatrom/libp2p-native-bridge';
function MyComponent() {
const { isStarted, peerId, discoveredPeers, start, stop } = useLibp2p({
config: {
tcpPort: 10000,
wsPort: 10005,
},
});
return (
<View>
<Text>Status: {isStarted ? 'Started' : 'Stopped'}</Text>
<Text>Peer ID: {peerId?.toString()}</Text>
<Button title="Start" onPress={start} />
<Button title="Stop" onPress={stop} />
</View>
);
}
```
### Settings Management
```typescript
import { getSettingsService, SettingsUI } from '@metatrom/libp2p-native-bridge';
// Get settings service instance
const settingsService = getSettingsService();
// Load settings
const settings = await settingsService.loadSettings();
// Update settings
await settingsService.updateSetting('tcpPort', 10100);
// Use the Settings UI component
<SettingsUI
initialSettings={settings}
onSettingsChange={(newSettings) => {
console.log('Settings changed:', newSettings);
}}
/>
```
## API Reference
### Libp2pComponent
Main class for interacting with the native libp2p implementation.
#### Constructor Options
```typescript
interface Libp2pOptions {
config?: {
tcpPort?: number; // Default: 10000
wsPort?: number; // Default: 10005
};
protocols?: ProtocolHandler[];
keypair?: {
privateKey: Uint8Array;
publicKey: Uint8Array;
};
}
```
#### Methods
- `start()`: Start the libp2p node
- `stop()`: Stop the libp2p node
- `dial(multiaddr: string)`: Connect to a peer
- `hangUp(peerId: string)`: Disconnect from a peer
- `getConnections(peerId?: string)`: Get active connections
- `sendProtocolData(peerId: string, protocolId: string, data: Uint8Array)`: Send data via protocol
- `refreshDiscovery()`: Refresh peer discovery
- `pingPeer(peerId: string)`: Ping a peer
#### Events
- `peer:discovery`: New peer discovered
- `peer:lost`: Peer lost
- `peer:connect`: Peer connected
- `peer:disconnect`: Peer disconnected
- `connection:open`: Connection opened
- `connection:close`: Connection closed
- `self:peer:update`: Own peer info updated
### SettingsService
Singleton service for managing application settings.
#### Methods
- `getInstance()`: Get singleton instance
- `initialize()`: Initialize settings
- `loadSettings()`: Load settings from storage
- `saveSettings(settings)`: Save settings
- `getSettings()`: Get current settings
- `updateSetting(key, value)`: Update specific setting
- `resetAllData()`: Reset all application data
## Native Module Requirements
This package requires the following native modules to be implemented in your React Native project:
### iOS
- `Libp2pModule`: Swift implementation of libp2p
- `DiscoveryModule`: mDNS/Bonjour discovery
- `SecureStorageModule`: Keychain storage (optional)
### Android
- `Libp2pModule`: Kotlin implementation of libp2p
- `DiscoveryModule`: Network Service Discovery
- `SecureStorageModule`: Keystore storage (optional)
## Configuration
### Default Settings
```typescript
{
tcpPort: 10000,
wsPort: 10005,
discoveryTimeout: 30000,
enableDebugLogs: false,
autoDiscovery: false,
useCustomPorts: false,
dhtServerUrl: 'ws://192.168.188.40:3000/ws'
}
```
## Platform Considerations
### iOS
- Uses Swift libp2p packages
- Native Bonjour/mDNS discovery
- Compile-time protocol registration
### Android
- Uses Kotlin with JVM libp2p
- Network Service Discovery (NSD)
- Dynamic protocol registration
## Dependencies
### Peer Dependencies
- `react`: >=18.0.0
- `react-native`: >=0.72.0
- `@react-native-async-storage/async-storage`: *
### External IOR Dependencies
- `ior:gitea:gitea.metatrom.net:universal-components/logger@1.0.0`
## License
MIT
## Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
## Support
For issues and questions, please open an issue on the repository.

View 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;
}

View File

@@ -0,0 +1,298 @@
/**
* Settings Service
*
* Manages application settings and provides methods to:
* - Load/save settings
* - Apply settings to native modules
* - Reset application data
*/
import { LoggerComponent } from 'ior:gitea:gitea.metatrom.net:universal-components/logger@1.0.0';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { NativeModules, Platform } from 'react-native';
// Storage keys
const SETTINGS_KEY = '@libp2p_settings';
// Default settings
export const DEFAULT_SETTINGS = {
tcpPort: 10000,
wsPort: 10005,
discoveryTimeout: 30000,
enableDebugLogs: false,
autoDiscovery: false,
useCustomPorts: false,
dhtServerUrl: 'ws://192.168.188.40:3000/ws', // This is the default/fallback DHT server URL
};
export interface AppSettings {
tcpPort: number;
wsPort: number;
discoveryTimeout: number;
enableDebugLogs: boolean;
autoDiscovery: boolean;
useCustomPorts: boolean;
dhtServerUrl: string;
}
export interface INativeModules {
Libp2pModule?: any;
DiscoveryModule?: any;
SecureStorageModule?: any;
}
export class SettingsService {
private static instance: SettingsService | null = null;
private currentSettings: AppSettings = DEFAULT_SETTINGS;
private logger = new LoggerComponent('SettingsService');
private nativeModules: INativeModules;
private constructor(nativeModules?: INativeModules) {
// Allow dependency injection of native modules
this.nativeModules = nativeModules || NativeModules;
}
public static getInstance(nativeModules?: INativeModules): SettingsService {
if (!SettingsService.instance) {
SettingsService.instance = new SettingsService(nativeModules);
}
return SettingsService.instance;
}
/**
* Reset the singleton instance (useful for testing)
*/
public static resetInstance(): void {
SettingsService.instance = null;
}
/**
* Initialize settings service
*/
async initialize(): Promise<AppSettings> {
try {
const settings = await this.loadSettings();
this.currentSettings = settings;
return settings;
} catch (error) {
this.logger.error('[SettingsService] Failed to initialize:', error);
return DEFAULT_SETTINGS;
}
}
/**
* Load settings from storage
*/
async loadSettings(): Promise<AppSettings> {
try {
const stored = await AsyncStorage.getItem(SETTINGS_KEY);
if (stored) {
const parsed = JSON.parse(stored);
return { ...DEFAULT_SETTINGS, ...parsed };
}
} catch (error) {
this.logger.error('[SettingsService] Failed to load settings:', error);
}
return DEFAULT_SETTINGS;
}
/**
* Save settings to storage
*/
async saveSettings(settings: AppSettings): Promise<void> {
try {
await AsyncStorage.setItem(SETTINGS_KEY, JSON.stringify(settings));
this.currentSettings = settings;
// Apply settings to native modules if needed
await this.applySettings(settings);
} catch (error) {
this.logger.error('[SettingsService] Failed to save settings:', error);
throw error;
}
}
/**
* Apply settings to native modules
*/
private async applySettings(settings: AppSettings): Promise<void> {
const { Libp2pModule, DiscoveryModule } = this.nativeModules;
// Apply port settings if custom ports are enabled
if (settings.useCustomPorts) {
// This would need to be passed to the native modules
// when starting the libp2p node
this.logger.debug('[SettingsService] Custom ports:', {
tcp: settings.tcpPort,
ws: settings.wsPort,
});
}
// Apply debug logging
if (Libp2pModule?.setDebugConsoleOutput) {
Libp2pModule.setDebugConsoleOutput(settings.enableDebugLogs);
}
// Apply discovery timeout
if (DiscoveryModule?.setDiscoveryTimeout) {
DiscoveryModule.setDiscoveryTimeout(settings.discoveryTimeout);
}
}
/**
* Get current settings
*/
getSettings(): AppSettings {
return this.currentSettings;
}
/**
* Get specific setting value
*/
getSetting<K extends keyof AppSettings>(key: K): AppSettings[K] {
return this.currentSettings[key];
}
/**
* Update specific setting
*/
async updateSetting<K extends keyof AppSettings>(key: K, value: AppSettings[K]): Promise<void> {
const newSettings = { ...this.currentSettings, [key]: value };
await this.saveSettings(newSettings);
}
/**
* Reset all application data
*/
async resetAllData(): Promise<void> {
try {
this.logger.info('[SettingsService] Starting app reset...');
// Clear all AsyncStorage (this includes dht_discovered_users)
const allKeys = await AsyncStorage.getAllKeys();
this.logger.debug('[SettingsService] Clearing AsyncStorage keys:', allKeys);
await AsyncStorage.multiRemove(allKeys);
// Clear native module data
await this.clearNativeData();
// Reset settings to defaults
this.currentSettings = DEFAULT_SETTINGS;
this.logger.info('[SettingsService] App reset complete');
} catch (error) {
this.logger.error('[SettingsService] Failed to reset app data:', error);
throw error;
}
}
/**
* Clear native module stored data
*/
private async clearNativeData(): Promise<void> {
const { Libp2pModule, DiscoveryModule, SecureStorageModule } = this.nativeModules;
try {
if (Platform.OS === 'ios') {
// Clear iOS UserDefaults
if (Libp2pModule?.clearStoredData) {
await Libp2pModule.clearStoredData();
}
if (DiscoveryModule?.clearStoredData) {
await DiscoveryModule.clearStoredData();
}
if (SecureStorageModule?.clearAll) {
await SecureStorageModule.clearAll();
}
} else if (Platform.OS === 'android') {
// Clear Android SharedPreferences
if (Libp2pModule?.clearStoredData) {
await Libp2pModule.clearStoredData();
}
if (DiscoveryModule?.clearStoredData) {
await DiscoveryModule.clearStoredData();
}
if (SecureStorageModule?.clearAll) {
await SecureStorageModule.clearAll();
}
}
} catch (error) {
this.logger.error('[SettingsService] Failed to clear native data:', error);
// Continue even if native clear fails
}
}
/**
* Export all app data for debugging
*/
async exportDebugData(): Promise<object> {
try {
const allKeys = await AsyncStorage.getAllKeys();
const allData: Record<string, unknown> = {};
for (const key of allKeys) {
try {
const value = await AsyncStorage.getItem(key);
allData[key] = value ? JSON.parse(value) : null;
} catch {
// If JSON parse fails, store as string
allData[key] = await AsyncStorage.getItem(key);
}
}
return {
settings: this.currentSettings,
storedData: allData,
platform: Platform.OS,
timestamp: new Date().toISOString(),
};
} catch (error) {
this.logger.error('[SettingsService] Failed to export debug data:', error);
throw error;
}
}
/**
* Check if this is first app launch
*/
async isFirstLaunch(): Promise<boolean> {
try {
const hasSettings = await AsyncStorage.getItem(SETTINGS_KEY);
return !hasSettings;
} catch {
return true;
}
}
/**
* Get storage info
*/
async getStorageInfo(): Promise<{
keys: string[];
totalSize: number;
}> {
try {
const allKeys = await AsyncStorage.getAllKeys();
let totalSize = 0;
for (const key of allKeys) {
const value = await AsyncStorage.getItem(key);
if (value) {
totalSize += value.length;
}
}
return {
keys: [...allKeys], // Convert readonly array to mutable array
totalSize,
};
} catch (error) {
this.logger.error('[SettingsService] Failed to get storage info:', error);
return { keys: [], totalSize: 0 };
}
}
}
// Export singleton getter function instead of instance
export const getSettingsService = (nativeModules?: INativeModules) =>
SettingsService.getInstance(nativeModules);

View File

@@ -0,0 +1,496 @@
/**
* Settings UI Component
*
* Provides configuration options for the libp2p node including:
* - Port configuration
* - Network settings
* - Data management (reset app)
* - Debug options
*/
import { LoggerComponent } from 'ior:gitea:gitea.metatrom.net:universal-components/logger@1.0.0';
import AsyncStorage from '@react-native-async-storage/async-storage';
import type React from 'react';
import { useEffect, useRef, useState } from 'react';
import {
Alert,
KeyboardAvoidingView,
NativeModules,
Platform,
ScrollView,
StyleSheet,
Switch,
Text,
TextInput,
TouchableOpacity,
View,
} from 'react-native';
// SecureStorage removed - not needed in external package
import { DEFAULT_SETTINGS, type AppSettings } from './SettingsService';
const { Libp2pModule, DiscoveryModule, SecureStorageModule } = NativeModules;
// Settings storage keys
const SETTINGS_STORAGE_KEY = '@libp2p_settings';
// Use AppSettings from SettingsService
// The LibP2PSettings type is now replaced by AppSettings
interface SettingsUIProps {
initialSettings?: AppSettings;
onSettingsChange?: (settings: AppSettings) => void;
onPortsSaved?: () => void;
onReset?: () => void;
}
export const SettingsUI: React.FC<SettingsUIProps> = ({
initialSettings,
onSettingsChange,
onPortsSaved,
onReset,
}) => {
const loggerRef = useRef<LoggerComponent | null>(null);
if (!loggerRef.current) {
loggerRef.current = new LoggerComponent('SettingsUI');
}
const logger = loggerRef.current;
const [settings, setSettings] = useState<AppSettings>(initialSettings || DEFAULT_SETTINGS);
const [isDirty, setIsDirty] = useState(false);
const [tcpPortInput, setTcpPortInput] = useState(
String((initialSettings || DEFAULT_SETTINGS).tcpPort),
);
const [wsPortInput, setWsPortInput] = useState(
String((initialSettings || DEFAULT_SETTINGS).wsPort),
);
const [discoveryTimeoutInput, setDiscoveryTimeoutInput] = useState(
String((initialSettings || DEFAULT_SETTINGS).discoveryTimeout / 1000),
);
const [dhtServerUrlInput, setDhtServerUrlInput] = useState(
(initialSettings || DEFAULT_SETTINGS).dhtServerUrl,
);
// Update state when initialSettings changes
useEffect(() => {
if (initialSettings) {
setSettings(initialSettings);
setTcpPortInput(String(initialSettings.tcpPort));
setWsPortInput(String(initialSettings.wsPort));
setDiscoveryTimeoutInput(String(initialSettings.discoveryTimeout / 1000));
setDhtServerUrlInput(initialSettings.dhtServerUrl);
} else {
loadSettings();
}
}, [initialSettings]);
/**
* Load settings from AsyncStorage
*/
const loadSettings = async () => {
try {
const stored = await AsyncStorage.getItem(SETTINGS_STORAGE_KEY);
let loadedSettings: LibP2PSettings;
if (stored) {
const parsed = JSON.parse(stored);
loadedSettings = { ...DEFAULT_SETTINGS, ...parsed };
} else {
// No stored settings, use defaults
loadedSettings = DEFAULT_SETTINGS;
}
setSettings(loadedSettings);
setTcpPortInput(String(loadedSettings.tcpPort));
setWsPortInput(String(loadedSettings.wsPort));
setDiscoveryTimeoutInput(String(loadedSettings.discoveryTimeout / 1000));
setDhtServerUrlInput(loadedSettings.dhtServerUrl);
// Don't call onSettingsChange during initial load
// Parent component should handle initial settings
} catch (error) {
logger.error('[Settings] Failed to load settings:', error);
// On error, still load defaults
setSettings(DEFAULT_SETTINGS);
setTcpPortInput(String(DEFAULT_SETTINGS.tcpPort));
setWsPortInput(String(DEFAULT_SETTINGS.wsPort));
setDiscoveryTimeoutInput(String(DEFAULT_SETTINGS.discoveryTimeout / 1000));
setDhtServerUrlInput(DEFAULT_SETTINGS.dhtServerUrl);
}
};
/**
* Save settings to AsyncStorage
*/
const saveSettings = async () => {
try {
// Validate ports
const tcpPort = Number.parseInt(tcpPortInput, 10);
const wsPort = Number.parseInt(wsPortInput, 10);
const discoveryTimeout = Number.parseInt(discoveryTimeoutInput, 10) * 1000;
if (Number.isNaN(tcpPort) || tcpPort < 1024 || tcpPort > 65535) {
Alert.alert('Invalid TCP Port', 'Please enter a valid port number (1024-65535)');
return;
}
if (Number.isNaN(wsPort) || wsPort < 1024 || wsPort > 65535) {
Alert.alert('Invalid WebSocket Port', 'Please enter a valid port number (1024-65535)');
return;
}
if (Number.isNaN(discoveryTimeout) || discoveryTimeout < 5000 || discoveryTimeout > 300000) {
Alert.alert('Invalid Discovery Timeout', 'Please enter a value between 5 and 300 seconds');
return;
}
const newSettings = {
...settings,
tcpPort,
wsPort,
discoveryTimeout,
dhtServerUrl: dhtServerUrlInput,
};
await AsyncStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(newSettings));
setSettings(newSettings);
setIsDirty(false);
if (onSettingsChange) {
onSettingsChange(newSettings);
}
// Call the onPortsSaved callback to trigger libp2p restart
if (onPortsSaved) {
onPortsSaved();
}
} catch (error) {
logger.error('[Settings] Failed to save settings:', error);
Alert.alert('Error', 'Failed to save settings');
}
};
/**
* Reset all app data
*/
const resetApp = () => {
Alert.alert(
'Reset App Data',
'This will delete all stored data including:\n\n• Your peer identity\n• All settings\n• Stored devices\n• Chat history\n\nThe app will be like freshly installed. Continue?',
[
{ text: 'Cancel', style: 'cancel' },
{
text: 'Reset',
style: 'destructive',
onPress: async () => {
try {
// Show progress alert
Alert.alert('Resetting...', 'Please wait while we clear all data');
// Clear all AsyncStorage
await AsyncStorage.clear();
// Clear all secure storage (identities, paired devices, etc.)
try {
// Clear secure storage if available
if (SecureStorageModule?.clearAll) {
await SecureStorageModule.clearAll();
}
logger.info('[SettingsUI] Cleared all secure storage data');
} catch (error) {
logger.error('[SettingsUI] Failed to clear secure storage:', error);
}
// Clear native module stored data
if (Platform.OS === 'ios') {
// iOS uses UserDefaults
if (Libp2pModule.clearStoredData) {
await Libp2pModule.clearStoredData();
}
if (DiscoveryModule.clearStoredData) {
await DiscoveryModule.clearStoredData();
}
// SecureStorage handled above
} else if (Platform.OS === 'android') {
// Android uses SharedPreferences
if (Libp2pModule.clearStoredData) {
await Libp2pModule.clearStoredData();
}
if (DiscoveryModule.clearStoredData) {
await DiscoveryModule.clearStoredData();
}
if (SecureStorageModule?.clearAll) {
await SecureStorageModule.clearAll();
}
}
// Reset settings to defaults
setSettings(DEFAULT_SETTINGS);
setTcpPortInput(String(DEFAULT_SETTINGS.tcpPort));
setWsPortInput(String(DEFAULT_SETTINGS.wsPort));
setDiscoveryTimeoutInput(String(DEFAULT_SETTINGS.discoveryTimeout / 1000));
setDhtServerUrlInput(DEFAULT_SETTINGS.dhtServerUrl);
if (onReset) {
onReset();
}
Alert.alert(
'Reset Complete',
'All app data has been cleared. Please restart the app for a fresh start.',
[
{
text: 'OK',
onPress: () => {
// Optionally restart the app
// RNRestart.Restart(); // Requires react-native-restart package
},
},
],
);
} catch (error) {
logger.error('[Settings] Failed to reset app:', error);
Alert.alert('Error', 'Failed to reset app data');
}
},
},
],
);
};
const handleToggleCustomPorts = async () => {
const newSettings = { ...settings, useCustomPorts: !settings.useCustomPorts };
setSettings(newSettings);
// Auto-save switch changes immediately
try {
await AsyncStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(newSettings));
if (onSettingsChange) {
onSettingsChange(newSettings);
}
} catch (error) {
logger.error('[Settings] Failed to save setting:', error);
}
};
return (
<KeyboardAvoidingView
style={styles.container}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
keyboardVerticalOffset={Platform.OS === 'ios' ? 0 : 0}
>
<ScrollView
style={styles.scrollView}
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={true}
keyboardShouldPersistTaps="handled"
>
<View style={styles.section}>
<Text style={styles.sectionTitle}>Network Configuration</Text>
<View style={styles.setting}>
<Text style={styles.settingLabel}>Use Custom Ports</Text>
<Switch
value={settings.useCustomPorts}
onValueChange={handleToggleCustomPorts}
trackColor={{ false: '#767577', true: '#2196F3' }}
thumbColor={settings.useCustomPorts ? '#fff' : '#f4f3f4'}
ios_backgroundColor="#3e3e3e"
/>
</View>
{settings.useCustomPorts && (
<>
<View style={styles.inputSetting}>
<Text style={styles.settingLabel}>TCP Port</Text>
<TextInput
style={styles.input}
value={tcpPortInput}
onChangeText={(text) => {
setTcpPortInput(text);
setIsDirty(true);
}}
keyboardType="number-pad"
placeholder="10000"
placeholderTextColor="#666"
maxLength={5}
/>
</View>
<View style={styles.inputSetting}>
<Text style={styles.settingLabel}>WebSocket Port</Text>
<TextInput
style={styles.input}
value={wsPortInput}
onChangeText={(text) => {
setWsPortInput(text);
setIsDirty(true);
}}
keyboardType="number-pad"
placeholder="10005"
placeholderTextColor="#666"
maxLength={5}
/>
</View>
</>
)}
</View>
<View style={styles.section}>
<Text style={styles.sectionTitle}>Discovery Configuration</Text>
<View style={styles.inputSetting}>
<Text style={styles.settingLabel}>Timeout (seconds)</Text>
<TextInput
style={styles.input}
value={discoveryTimeoutInput}
onChangeText={(text) => {
setDiscoveryTimeoutInput(text);
setIsDirty(true);
}}
keyboardType="number-pad"
placeholder="30"
placeholderTextColor="#666"
maxLength={3}
/>
</View>
<View style={styles.inputSetting}>
<Text style={styles.settingLabel}>DHT Server URL</Text>
<TextInput
style={styles.input}
value={dhtServerUrlInput}
onChangeText={(text) => {
setDhtServerUrlInput(text);
setIsDirty(true);
}}
placeholder="ws://192.168.188.40:3000/ws"
placeholderTextColor="#666"
selectionColor="#2196F3"
autoCapitalize="none"
autoCorrect={false}
underlineColorAndroid="transparent"
/>
<Text style={styles.settingDescription}>
WebSocket URL of the DHT server for global peer discovery
</Text>
</View>
</View>
{isDirty && (
<View style={styles.section}>
<TouchableOpacity style={styles.saveButton} onPress={saveSettings}>
<Text style={styles.saveButtonText}>Save Settings</Text>
</TouchableOpacity>
</View>
)}
<View style={[styles.section, styles.lastSection]}>
<TouchableOpacity style={styles.dangerButton} onPress={resetApp}>
<Text style={styles.dangerButtonText}>🗑 Reset All App Data</Text>
</TouchableOpacity>
<Text style={styles.warningText}>
This will delete all stored data and reset the app to its initial state
</Text>
</View>
</ScrollView>
</KeyboardAvoidingView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: 'transparent',
},
scrollView: {
flex: 1,
},
scrollContent: {
paddingVertical: 20,
paddingBottom: 100, // Increased to ensure all content is visible
},
section: {
paddingHorizontal: 20,
marginBottom: 24,
},
sectionTitle: {
fontSize: 16,
fontWeight: '600',
color: '#fff',
marginBottom: 16,
},
setting: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'flex-start',
paddingVertical: 16,
},
settingInfo: {
flex: 1,
marginRight: 12,
},
settingLabel: {
fontSize: 16,
fontWeight: '600',
color: '#fff',
marginBottom: 4,
},
settingDescription: {
fontSize: 12,
color: '#999',
marginTop: 2,
},
inputSetting: {
paddingVertical: 12,
},
input: {
marginTop: 8,
paddingHorizontal: 12,
paddingVertical: Platform.OS === 'ios' ? 10 : 8,
backgroundColor: '#2a2a2a',
borderRadius: 8,
borderWidth: 1,
borderColor: '#333',
fontSize: 14,
color: '#fff',
textAlignVertical: 'center',
minHeight: Platform.OS === 'android' ? 40 : undefined,
},
dangerButton: {
marginTop: 12,
paddingVertical: 12,
paddingHorizontal: 20,
backgroundColor: '#5c1e1e',
borderRadius: 8,
alignItems: 'center',
},
dangerButtonText: {
fontSize: 16,
fontWeight: '600',
color: '#ff9999',
},
saveButton: {
marginTop: 16,
paddingVertical: 12,
paddingHorizontal: 20,
backgroundColor: '#2563eb',
borderRadius: 8,
alignItems: 'center',
},
saveButtonText: {
fontSize: 16,
fontWeight: '600',
color: '#fff',
},
warningText: {
fontSize: 12,
color: '#999',
textAlign: 'center',
marginTop: 8,
fontStyle: 'italic',
},
lastSection: {
marginBottom: 40,
},
});
export default SettingsUI;

42
index.ts Normal file
View File

@@ -0,0 +1,42 @@
/**
* @metatrom/libp2p-native-bridge
*
* Native libp2p bridge for React Native applications
* Provides a unified interface for iOS and Android libp2p implementations
*/
// Main component export
export { Libp2pComponent, SimplePeerId, SimpleMultiaddr } from './implementations/Libp2pComponent';
// Settings exports
export {
SettingsService,
getSettingsService,
DEFAULT_SETTINGS,
type AppSettings,
type INativeModules
} from './implementations/SettingsService';
// UI Components
export { SettingsUI } from './implementations/SettingsUI';
// Interface exports
export * from './interfaces/ILibp2pComponent';
export * from './interfaces/types';
// Utility exports
export * from './utils/constants';
export * from './utils/eventHelpers';
export * from './utils/types';
// Re-export commonly used types for convenience
export type {
Connection,
ILibp2pComponent,
Libp2pEvents,
Libp2pOptions,
Multiaddr,
PeerId,
PeerInfo,
ProtocolHandler,
} from './interfaces/ILibp2pComponent';

View File

@@ -0,0 +1,87 @@
/**
* Core libp2p component interfaces
*/
export interface PeerId {
toString(): string;
toBytes(): Uint8Array;
equals(other: PeerId): boolean;
}
export interface Multiaddr {
toString(): string;
bytes: Uint8Array;
protos(): Array<{ code: number; name: string }>;
getPeerId(): string | null;
}
export interface Connection {
id: string;
remotePeer: PeerId;
remoteAddr: Multiaddr;
stat: {
direction: 'inbound' | 'outbound';
status: 'pending' | 'open' | 'closing' | 'closed';
timeline: {
open: number;
upgraded?: number;
close?: number;
};
};
}
export interface PeerInfo {
id: PeerId;
multiaddrs: Multiaddr[];
}
export interface ProtocolHandler {
protocolId: string;
handler: (data: { peerId: string; data?: Uint8Array }) => void | Promise<void>;
}
export interface ILibp2pComponent {
peerId: PeerId | null;
multiaddrs: Multiaddr[];
start(): Promise<void>;
stop(): Promise<void>;
dial(multiaddr: string): Promise<Connection>;
hangUp(peerId: string): Promise<void>;
getConnections(peerId?: string): Promise<Connection[]>;
sendProtocolData(peerId: string, protocolId: string, data: Uint8Array): Promise<void>;
refreshDiscovery(): Promise<void>;
pingPeer(peerId: string): Promise<{ success: boolean; rtt?: number; peerId: string }>;
addEventListener<K extends keyof Libp2pEvents>(
event: K,
handler: (evt: Libp2pEvents[K]) => void,
): void;
removeEventListener<K extends keyof Libp2pEvents>(
event: K,
handler: (evt: Libp2pEvents[K]) => void,
): void;
}
export interface Libp2pEvents {
'peer:discovery': CustomEvent<PeerInfo>;
'peer:lost': CustomEvent<{ id: PeerId }>;
'peer:connect': CustomEvent<Connection>;
'peer:disconnect': CustomEvent<Connection>;
'connection:open': CustomEvent<Connection>;
'connection:close': CustomEvent<Connection>;
'self:peer:update': CustomEvent<{ peerId: PeerId; multiaddrs: Multiaddr[] }>;
}
export interface Libp2pOptions {
config?: {
tcpPort?: number;
wsPort?: number;
};
protocols?: ProtocolHandler[];
keypair?: {
privateKey: Uint8Array;
publicKey: Uint8Array;
};
}

6
interfaces/index.ts Normal file
View File

@@ -0,0 +1,6 @@
/**
* Export all interfaces
*/
export * from './ILibp2pComponent';
export * from './types';

20
interfaces/types.ts Normal file
View File

@@ -0,0 +1,20 @@
/**
* Type definitions for libp2p native bridge
*/
export type {
ConnectionStatusEvent,
PeerDiscoveredEvent,
PeerInfoEvent
} from '../utils/types';
// Re-export common types from ILibp2pComponent for convenience
export type {
PeerId,
Multiaddr,
Connection,
PeerInfo,
ProtocolHandler,
Libp2pEvents,
Libp2pOptions
} from './ILibp2pComponent';

1
native/libp2p.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
// This file is no longer needed as types have been moved to libp2p.ts

36
package.json Normal file
View File

@@ -0,0 +1,36 @@
{
"name": "@metatrom/libp2p-native-bridge",
"version": "1.0.0",
"main": "index.ts",
"type": "module",
"description": "Native libp2p bridge for React Native applications",
"metatrom": {
"ior": "com.metatrom.libp2p-native-bridge@1.0.0",
"capabilities": {
"p2p": true,
"contracts": false,
"viewer": false,
"sync": true
}
},
"exports": {
".": "./index.ts",
"./settings": "./implementations/SettingsService.ts",
"./ui": "./implementations/SettingsUI.tsx",
"./interfaces": "./interfaces/index.ts"
},
"peerDependencies": {
"react": ">=18.0.0",
"react-native": ">=0.72.0",
"@react-native-async-storage/async-storage": "*"
},
"keywords": [
"libp2p",
"react-native",
"p2p",
"networking",
"metatrom"
],
"author": "Metatrom",
"license": "MIT"
}

45
utils/constants.ts Normal file
View File

@@ -0,0 +1,45 @@
/**
* Shared constants for libp2p configuration across platforms
*/
export const LIBP2P_CONFIG = {
// Default ports
DEFAULT_TCP_PORT: 10000,
DEFAULT_WS_PORT: 10001,
// Connection timeouts (in milliseconds)
CONNECTION_PENDING_TIMEOUT: 30000, // 30 seconds before removing pending connections
CONNECTION_FAILED_VISIBILITY: 5000, // 5 seconds to keep failed connections visible
CONNECTION_ACCEPTANCE_CHECK_DELAY: 5000, // 5 seconds before checking acceptance
// Monitoring intervals (in milliseconds)
CONNECTION_MONITOR_INTERVAL: 3000, // Check connections every 3 seconds
CONNECTION_SYNC_INTERVAL: 5000, // Sync connections every 5 seconds
// Protocol IDs
PROTOCOLS: {
CONNECTION_ACCEPT: '/metatrom/connection-accept/1.0.0',
CONNECTION_ACCEPTED: '/metatrom/connection-accepted/1.0.0',
},
// Connection states
CONNECTION_STATUS: {
PENDING: 'pending',
CONNECTED: 'connected',
DISCONNECTED: 'disconnected',
FAILED: 'failed',
CLOSING: 'closing',
CLOSED: 'closed',
} as const,
// Connection directions
CONNECTION_DIRECTION: {
INBOUND: 'inbound',
OUTBOUND: 'outbound',
} as const,
} as const;
export type ConnectionStatus =
(typeof LIBP2P_CONFIG.CONNECTION_STATUS)[keyof typeof LIBP2P_CONFIG.CONNECTION_STATUS];
export type ConnectionDirection =
(typeof LIBP2P_CONFIG.CONNECTION_DIRECTION)[keyof typeof LIBP2P_CONFIG.CONNECTION_DIRECTION];

103
utils/eventHelpers.ts Normal file
View File

@@ -0,0 +1,103 @@
/**
* Helper functions for standardized event emission across platforms
* Import this file in native modules to ensure consistent event structures
*/
import type {
ConnectionStatusEvent,
ErrorEvent,
LogEvent,
PeerDiscoveredEvent,
PeerInfoEvent,
PeerLostEvent,
} from './types';
/**
* Creates a standardized connection status event payload
*/
export function createConnectionStatusEvent(
peerId: string,
status: string,
direction?: string,
multiaddr?: string,
error?: string,
reason?: string,
): ConnectionStatusEvent {
return {
peerId,
status: status as ConnectionStatus,
direction: direction as ConnectionDirection,
multiaddr,
error,
reason,
};
}
/**
* Creates a standardized peer discovered event payload
*/
export function createPeerDiscoveredEvent(
peerId: string,
addresses: string[],
): PeerDiscoveredEvent {
return {
peerId,
addresses,
multiaddrs: addresses, // Use same array for both for compatibility
};
}
/**
* Creates a standardized peer lost event payload
*/
export function createPeerLostEvent(peerId: string): PeerLostEvent {
return {
peerId,
};
}
/**
* Creates a standardized log event payload
*/
export function createLogEvent(
message: string,
level: 'debug' | 'info' | 'warn' | 'error' = 'info',
): LogEvent {
return {
message,
level,
};
}
/**
* Creates a standardized error event payload
*/
export function createErrorEvent(message: string, code?: string): ErrorEvent {
return {
message,
code,
};
}
/**
* Creates a standardized peer info event payload
*/
export function createPeerInfoEvent(peerId: string, multiaddrs: string[]): PeerInfoEvent {
return {
peerId,
multiaddrs,
};
}
/**
* Event names used across platforms
*/
export const EVENT_NAMES = {
PEER_INFO: 'onPeerInfo',
PEER_DISCOVERED: 'onPeerDiscovered',
PEER_LOST: 'onPeerLost',
CONNECTION_STATUS: 'onConnectionStatus',
INCOMING_CONNECTION: 'onIncomingConnection',
LOG: 'onLog',
ERROR: 'onError',
} as const;

54
utils/types.ts Normal file
View File

@@ -0,0 +1,54 @@
/**
* Shared types for libp2p implementation across platforms
*/
import type { ConnectionDirection, ConnectionStatus } from './constants';
/**
* Unified connection information structure used by both iOS and Android
*/
export interface ConnectionInfo {
peerId: string;
status: ConnectionStatus;
direction: ConnectionDirection;
multiaddr?: string;
timestamp: number; // Unix timestamp in seconds
error?: string;
}
/**
* Event payloads for consistent event handling across platforms
*/
export interface ConnectionStatusEvent {
peerId: string;
status: ConnectionStatus;
direction?: ConnectionDirection;
multiaddr?: string;
error?: string;
reason?: string;
}
export interface PeerDiscoveredEvent {
peerId: string;
addresses?: string[];
multiaddrs?: string[];
}
export interface PeerLostEvent {
peerId: string;
}
export interface LogEvent {
message: string;
level?: 'debug' | 'info' | 'warn' | 'error';
}
export interface ErrorEvent {
message: string;
code?: string;
}
export interface PeerInfoEvent {
peerId: string;
multiaddrs: string[];
}