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:
237
.gitignore
vendored
Normal file
237
.gitignore
vendored
Normal 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
233
README.md
Normal 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.
|
||||
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;
|
||||
}
|
||||
298
implementations/SettingsService.ts
Normal file
298
implementations/SettingsService.ts
Normal 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);
|
||||
496
implementations/SettingsUI.tsx
Normal file
496
implementations/SettingsUI.tsx
Normal 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
42
index.ts
Normal 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';
|
||||
87
interfaces/ILibp2pComponent.ts
Normal file
87
interfaces/ILibp2pComponent.ts
Normal 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
6
interfaces/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Export all interfaces
|
||||
*/
|
||||
|
||||
export * from './ILibp2pComponent';
|
||||
export * from './types';
|
||||
20
interfaces/types.ts
Normal file
20
interfaces/types.ts
Normal 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
1
native/libp2p.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
// This file is no longer needed as types have been moved to libp2p.ts
|
||||
36
package.json
Normal file
36
package.json
Normal 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
45
utils/constants.ts
Normal 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
103
utils/eventHelpers.ts
Normal 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
54
utils/types.ts
Normal 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[];
|
||||
}
|
||||
Reference in New Issue
Block a user