bitchat-node 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +223 -0
- package/dist/bin/bitchat.d.ts +7 -0
- package/dist/bin/bitchat.d.ts.map +1 -0
- package/dist/bin/bitchat.js +69 -0
- package/dist/bin/bitchat.js.map +1 -0
- package/dist/client.d.ts +77 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +411 -0
- package/dist/client.js.map +1 -0
- package/dist/crypto/index.d.ts +6 -0
- package/dist/crypto/index.d.ts.map +1 -0
- package/dist/crypto/index.js +6 -0
- package/dist/crypto/index.js.map +1 -0
- package/dist/crypto/noise.d.ts +72 -0
- package/dist/crypto/noise.d.ts.map +1 -0
- package/dist/crypto/noise.js +470 -0
- package/dist/crypto/noise.js.map +1 -0
- package/dist/crypto/signing.d.ts +34 -0
- package/dist/crypto/signing.d.ts.map +1 -0
- package/dist/crypto/signing.js +56 -0
- package/dist/crypto/signing.js.map +1 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +48 -0
- package/dist/index.js.map +1 -0
- package/dist/mesh/deduplicator.d.ts +48 -0
- package/dist/mesh/deduplicator.d.ts.map +1 -0
- package/dist/mesh/deduplicator.js +107 -0
- package/dist/mesh/deduplicator.js.map +1 -0
- package/dist/mesh/index.d.ts +6 -0
- package/dist/mesh/index.d.ts.map +1 -0
- package/dist/mesh/index.js +6 -0
- package/dist/mesh/index.js.map +1 -0
- package/dist/mesh/router.d.ts +90 -0
- package/dist/mesh/router.d.ts.map +1 -0
- package/dist/mesh/router.js +204 -0
- package/dist/mesh/router.js.map +1 -0
- package/dist/protocol/binary.d.ts +37 -0
- package/dist/protocol/binary.d.ts.map +1 -0
- package/dist/protocol/binary.js +310 -0
- package/dist/protocol/binary.js.map +1 -0
- package/dist/protocol/constants.d.ts +30 -0
- package/dist/protocol/constants.d.ts.map +1 -0
- package/dist/protocol/constants.js +37 -0
- package/dist/protocol/constants.js.map +1 -0
- package/dist/protocol/index.d.ts +8 -0
- package/dist/protocol/index.d.ts.map +1 -0
- package/dist/protocol/index.js +8 -0
- package/dist/protocol/index.js.map +1 -0
- package/dist/protocol/packets.d.ts +38 -0
- package/dist/protocol/packets.d.ts.map +1 -0
- package/dist/protocol/packets.js +177 -0
- package/dist/protocol/packets.js.map +1 -0
- package/dist/protocol/types.d.ts +134 -0
- package/dist/protocol/types.d.ts.map +1 -0
- package/dist/protocol/types.js +108 -0
- package/dist/protocol/types.js.map +1 -0
- package/dist/session/index.d.ts +5 -0
- package/dist/session/index.d.ts.map +1 -0
- package/dist/session/index.js +5 -0
- package/dist/session/index.js.map +1 -0
- package/dist/session/manager.d.ts +113 -0
- package/dist/session/manager.d.ts.map +1 -0
- package/dist/session/manager.js +371 -0
- package/dist/session/manager.js.map +1 -0
- package/dist/transport/ble.d.ts +92 -0
- package/dist/transport/ble.d.ts.map +1 -0
- package/dist/transport/ble.js +434 -0
- package/dist/transport/ble.js.map +1 -0
- package/dist/transport/index.d.ts +5 -0
- package/dist/transport/index.d.ts.map +1 -0
- package/dist/transport/index.js +5 -0
- package/dist/transport/index.js.map +1 -0
- package/dist/ui/index.d.ts +2 -0
- package/dist/ui/index.d.ts.map +1 -0
- package/dist/ui/index.js +2 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/server.d.ts +16 -0
- package/dist/ui/server.d.ts.map +1 -0
- package/dist/ui/server.js +510 -0
- package/dist/ui/server.js.map +1 -0
- package/package.json +79 -0
- package/src/bin/bitchat.ts +87 -0
- package/src/client.ts +519 -0
- package/src/crypto/index.ts +22 -0
- package/src/crypto/noise.ts +574 -0
- package/src/crypto/signing.ts +66 -0
- package/src/index.ts +95 -0
- package/src/mesh/deduplicator.ts +129 -0
- package/src/mesh/index.ts +6 -0
- package/src/mesh/router.ts +258 -0
- package/src/protocol/binary.ts +345 -0
- package/src/protocol/constants.ts +43 -0
- package/src/protocol/index.ts +15 -0
- package/src/protocol/packets.ts +223 -0
- package/src/protocol/types.ts +182 -0
- package/src/session/index.ts +9 -0
- package/src/session/manager.ts +476 -0
- package/src/transport/ble.ts +553 -0
- package/src/transport/index.ts +10 -0
- package/src/ui/index.ts +1 -0
- package/src/ui/server.ts +569 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bitchat-node
|
|
3
|
+
* Node.js implementation of the Bitchat BLE mesh protocol
|
|
4
|
+
*
|
|
5
|
+
* Compatible with Jack Dorsey's Bitchat app.
|
|
6
|
+
* @see https://github.com/permissionlesstech/bitchat
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* import { BitchatClient } from 'bitchat-node';
|
|
11
|
+
*
|
|
12
|
+
* const client = new BitchatClient({ nickname: 'my-node' });
|
|
13
|
+
*
|
|
14
|
+
* client.on('message', (message) => {
|
|
15
|
+
* console.log(`${message.senderNickname}: ${message.content}`);
|
|
16
|
+
* });
|
|
17
|
+
*
|
|
18
|
+
* client.on('peer:connected', (peer) => {
|
|
19
|
+
* console.log(`Connected to ${peer.nickname}`);
|
|
20
|
+
* });
|
|
21
|
+
*
|
|
22
|
+
* await client.start();
|
|
23
|
+
* await client.sendPublicMessage('Hello mesh!');
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
// Main client
|
|
28
|
+
export { BitchatClient, type BitchatClientConfig, type BitchatClientEvents } from './client.js';
|
|
29
|
+
// Crypto
|
|
30
|
+
export {
|
|
31
|
+
// Noise protocol
|
|
32
|
+
CipherState,
|
|
33
|
+
fingerprint,
|
|
34
|
+
formatFingerprint,
|
|
35
|
+
generateKeyPair,
|
|
36
|
+
// Signing
|
|
37
|
+
generateSigningKeyPair,
|
|
38
|
+
HandshakeState as NoiseHandshakeState,
|
|
39
|
+
type NoiseKeyPair,
|
|
40
|
+
type NoiseRole,
|
|
41
|
+
NoiseSession,
|
|
42
|
+
type SigningKeyPair,
|
|
43
|
+
sha256,
|
|
44
|
+
sign,
|
|
45
|
+
verify,
|
|
46
|
+
} from './crypto/index.js';
|
|
47
|
+
// Mesh
|
|
48
|
+
export {
|
|
49
|
+
type DeduplicatorConfig,
|
|
50
|
+
type Link,
|
|
51
|
+
MeshRouter,
|
|
52
|
+
MessageDeduplicator,
|
|
53
|
+
type RouterConfig,
|
|
54
|
+
} from './mesh/index.js';
|
|
55
|
+
// Protocol
|
|
56
|
+
export {
|
|
57
|
+
type BitchatPacket,
|
|
58
|
+
CHARACTERISTIC_UUID,
|
|
59
|
+
type ChatMessage,
|
|
60
|
+
DEFAULT_TTL,
|
|
61
|
+
type DeliveryStatus,
|
|
62
|
+
decode,
|
|
63
|
+
// Binary encoding
|
|
64
|
+
encode,
|
|
65
|
+
encodeForSigning,
|
|
66
|
+
HandshakeState,
|
|
67
|
+
MAX_MESSAGE_LENGTH,
|
|
68
|
+
MAX_TTL,
|
|
69
|
+
// Types
|
|
70
|
+
MessageType,
|
|
71
|
+
makeDeduplicationID,
|
|
72
|
+
NoisePayloadType,
|
|
73
|
+
PacketFlags,
|
|
74
|
+
PeerID,
|
|
75
|
+
type PeerInfo,
|
|
76
|
+
type ReadReceipt,
|
|
77
|
+
// Constants
|
|
78
|
+
SERVICE_UUID,
|
|
79
|
+
SERVICE_UUID_TESTNET,
|
|
80
|
+
} from './protocol/index.js';
|
|
81
|
+
|
|
82
|
+
// Sessions
|
|
83
|
+
export {
|
|
84
|
+
SessionManager,
|
|
85
|
+
type SessionManagerConfig,
|
|
86
|
+
type SessionManagerEvents,
|
|
87
|
+
} from './session/index.js';
|
|
88
|
+
|
|
89
|
+
// Transport
|
|
90
|
+
export {
|
|
91
|
+
BLELink,
|
|
92
|
+
BLETransport,
|
|
93
|
+
type BLETransportConfig,
|
|
94
|
+
type BLETransportEvents,
|
|
95
|
+
} from './transport/index.js';
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message Deduplicator
|
|
3
|
+
* Prevents processing the same message multiple times in the mesh
|
|
4
|
+
* Uses a simple bloom-filter-like approach with time-based expiry
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface DeduplicatorConfig {
|
|
8
|
+
maxSize: number;
|
|
9
|
+
maxAgeMs: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const DEFAULT_CONFIG: DeduplicatorConfig = {
|
|
13
|
+
maxSize: 10000,
|
|
14
|
+
maxAgeMs: 5 * 60 * 1000, // 5 minutes
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Efficient message deduplication for mesh networks
|
|
19
|
+
*/
|
|
20
|
+
export class MessageDeduplicator {
|
|
21
|
+
private seen = new Map<string, number>();
|
|
22
|
+
private readonly config: DeduplicatorConfig;
|
|
23
|
+
private cleanupTimer?: ReturnType<typeof setInterval>;
|
|
24
|
+
|
|
25
|
+
constructor(config: Partial<DeduplicatorConfig> = {}) {
|
|
26
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
27
|
+
|
|
28
|
+
// Periodic cleanup
|
|
29
|
+
this.cleanupTimer = setInterval(() => this.cleanup(), 60000);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Check if message was seen and mark it as seen
|
|
34
|
+
* Returns true if this is a NEW message (not seen before)
|
|
35
|
+
*/
|
|
36
|
+
markSeen(id: string): boolean {
|
|
37
|
+
const now = Date.now();
|
|
38
|
+
|
|
39
|
+
// Check if already seen
|
|
40
|
+
const existingTimestamp = this.seen.get(id);
|
|
41
|
+
if (existingTimestamp !== undefined) {
|
|
42
|
+
// Already seen - check if expired
|
|
43
|
+
if (now - existingTimestamp > this.config.maxAgeMs) {
|
|
44
|
+
// Expired, treat as new
|
|
45
|
+
this.seen.set(id, now);
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// New message - add to seen set
|
|
52
|
+
this.seen.set(id, now);
|
|
53
|
+
|
|
54
|
+
// Trim if too large
|
|
55
|
+
if (this.seen.size > this.config.maxSize) {
|
|
56
|
+
this.trimOldest();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Check if message was seen (without marking)
|
|
64
|
+
*/
|
|
65
|
+
hasSeen(id: string): boolean {
|
|
66
|
+
const timestamp = this.seen.get(id);
|
|
67
|
+
if (timestamp === undefined) return false;
|
|
68
|
+
|
|
69
|
+
// Check expiry
|
|
70
|
+
if (Date.now() - timestamp > this.config.maxAgeMs) {
|
|
71
|
+
this.seen.delete(id);
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Remove expired entries
|
|
80
|
+
*/
|
|
81
|
+
private cleanup(): void {
|
|
82
|
+
const now = Date.now();
|
|
83
|
+
const maxAge = this.config.maxAgeMs;
|
|
84
|
+
|
|
85
|
+
for (const [id, timestamp] of this.seen) {
|
|
86
|
+
if (now - timestamp > maxAge) {
|
|
87
|
+
this.seen.delete(id);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Trim oldest entries when size limit exceeded
|
|
94
|
+
*/
|
|
95
|
+
private trimOldest(): void {
|
|
96
|
+
// Convert to array, sort by timestamp, keep newest half
|
|
97
|
+
const entries = Array.from(this.seen.entries()).sort((a, b) => a[1] - b[1]);
|
|
98
|
+
|
|
99
|
+
const toRemove = Math.floor(entries.length / 2);
|
|
100
|
+
for (let i = 0; i < toRemove; i++) {
|
|
101
|
+
this.seen.delete(entries[i][0]);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Clear all entries
|
|
107
|
+
*/
|
|
108
|
+
reset(): void {
|
|
109
|
+
this.seen.clear();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Stop cleanup timer
|
|
114
|
+
*/
|
|
115
|
+
destroy(): void {
|
|
116
|
+
if (this.cleanupTimer) {
|
|
117
|
+
clearInterval(this.cleanupTimer);
|
|
118
|
+
this.cleanupTimer = undefined;
|
|
119
|
+
}
|
|
120
|
+
this.seen.clear();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Get current size
|
|
125
|
+
*/
|
|
126
|
+
get size(): number {
|
|
127
|
+
return this.seen.size;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mesh Router
|
|
3
|
+
* Handles TTL-based message flooding and routing decisions
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { EventEmitter } from 'node:events';
|
|
7
|
+
import { makeDeduplicationID } from '../protocol/binary.js';
|
|
8
|
+
import { DEFAULT_TTL, MAX_TTL } from '../protocol/constants.js';
|
|
9
|
+
import { type BitchatPacket, MessageType, type PeerID } from '../protocol/types.js';
|
|
10
|
+
import { MessageDeduplicator } from './deduplicator.js';
|
|
11
|
+
|
|
12
|
+
export interface RouterConfig {
|
|
13
|
+
maxTTL: number;
|
|
14
|
+
defaultTTL: number;
|
|
15
|
+
probabilisticRelay: boolean;
|
|
16
|
+
relayProbability: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const DEFAULT_ROUTER_CONFIG: RouterConfig = {
|
|
20
|
+
maxTTL: MAX_TTL,
|
|
21
|
+
defaultTTL: DEFAULT_TTL,
|
|
22
|
+
probabilisticRelay: false,
|
|
23
|
+
relayProbability: 0.8,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export interface Link {
|
|
27
|
+
id: string;
|
|
28
|
+
send(data: Uint8Array): Promise<void>;
|
|
29
|
+
peerID?: PeerID;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface RouterEvents {
|
|
33
|
+
packet: (packet: BitchatPacket, fromLink: Link | null) => void;
|
|
34
|
+
forward: (packet: BitchatPacket, toLinks: Link[]) => void;
|
|
35
|
+
drop: (packet: BitchatPacket, reason: string) => void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Mesh message router with TTL-based flooding
|
|
40
|
+
*/
|
|
41
|
+
export class MeshRouter extends EventEmitter {
|
|
42
|
+
private readonly config: RouterConfig;
|
|
43
|
+
private readonly deduplicator: MessageDeduplicator;
|
|
44
|
+
private readonly links = new Map<string, Link>();
|
|
45
|
+
private myPeerID: PeerID | null = null;
|
|
46
|
+
|
|
47
|
+
// Track ingress link for each message to avoid echo
|
|
48
|
+
private ingressLinks = new Map<string, string>();
|
|
49
|
+
|
|
50
|
+
constructor(config: Partial<RouterConfig> = {}) {
|
|
51
|
+
super();
|
|
52
|
+
this.config = { ...DEFAULT_ROUTER_CONFIG, ...config };
|
|
53
|
+
this.deduplicator = new MessageDeduplicator();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Set our own peer ID (to avoid processing our own messages)
|
|
58
|
+
*/
|
|
59
|
+
setMyPeerID(peerID: PeerID): void {
|
|
60
|
+
this.myPeerID = peerID;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Get our own peer ID
|
|
65
|
+
*/
|
|
66
|
+
getMyPeerID(): PeerID | null {
|
|
67
|
+
return this.myPeerID;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Add a link (connection to another peer)
|
|
72
|
+
*/
|
|
73
|
+
addLink(link: Link): void {
|
|
74
|
+
this.links.set(link.id, link);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Remove a link
|
|
79
|
+
*/
|
|
80
|
+
removeLink(linkId: string): void {
|
|
81
|
+
this.links.delete(linkId);
|
|
82
|
+
// Clean up any ingress tracking
|
|
83
|
+
for (const [msgId, ingressLinkId] of this.ingressLinks) {
|
|
84
|
+
if (ingressLinkId === linkId) {
|
|
85
|
+
this.ingressLinks.delete(msgId);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Get all active links
|
|
92
|
+
*/
|
|
93
|
+
getLinks(): Link[] {
|
|
94
|
+
return Array.from(this.links.values());
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Get peer IDs of all connected links
|
|
99
|
+
*/
|
|
100
|
+
getConnectedPeerIDs(): PeerID[] {
|
|
101
|
+
return Array.from(this.links.values())
|
|
102
|
+
.filter((link): link is Link & { peerID: PeerID } => link.peerID !== undefined)
|
|
103
|
+
.map((link) => link.peerID);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Handle an incoming packet
|
|
108
|
+
*/
|
|
109
|
+
async handlePacket(packet: BitchatPacket, fromLink: Link | null): Promise<boolean> {
|
|
110
|
+
const dedupID = makeDeduplicationID(packet);
|
|
111
|
+
|
|
112
|
+
// Check deduplication
|
|
113
|
+
if (!this.deduplicator.markSeen(dedupID)) {
|
|
114
|
+
this.emit('drop', packet, 'duplicate');
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Track ingress link
|
|
119
|
+
if (fromLink) {
|
|
120
|
+
this.ingressLinks.set(dedupID, fromLink.id);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Validate TTL
|
|
124
|
+
if (packet.ttl > this.config.maxTTL) {
|
|
125
|
+
this.emit('drop', packet, 'ttl_exceeded');
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Emit packet for processing
|
|
130
|
+
this.emit('packet', packet, fromLink);
|
|
131
|
+
|
|
132
|
+
// Decide whether to forward
|
|
133
|
+
if (this.shouldForward(packet)) {
|
|
134
|
+
await this.forward(packet, fromLink?.id);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Send a new packet (originated by us)
|
|
142
|
+
*/
|
|
143
|
+
async sendPacket(packet: BitchatPacket): Promise<void> {
|
|
144
|
+
const dedupID = makeDeduplicationID(packet);
|
|
145
|
+
|
|
146
|
+
// Mark as seen to avoid processing our own relayed copy
|
|
147
|
+
this.deduplicator.markSeen(dedupID);
|
|
148
|
+
|
|
149
|
+
// Broadcast to all links
|
|
150
|
+
await this.broadcast(packet);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Check if packet should be forwarded
|
|
155
|
+
*/
|
|
156
|
+
private shouldForward(packet: BitchatPacket): boolean {
|
|
157
|
+
// Don't forward if TTL exhausted
|
|
158
|
+
if (packet.ttl <= 0) return false;
|
|
159
|
+
|
|
160
|
+
// Don't forward certain message types
|
|
161
|
+
switch (packet.type) {
|
|
162
|
+
case MessageType.REQUEST_SYNC:
|
|
163
|
+
// Sync requests are local-only
|
|
164
|
+
return false;
|
|
165
|
+
default:
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Probabilistic relay under high load
|
|
170
|
+
if (this.config.probabilisticRelay) {
|
|
171
|
+
if (Math.random() > this.config.relayProbability) {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Forward a packet (decrement TTL, send to other links)
|
|
181
|
+
*/
|
|
182
|
+
private async forward(packet: BitchatPacket, excludeLinkId?: string): Promise<void> {
|
|
183
|
+
// Decrement TTL
|
|
184
|
+
const forwardPacket: BitchatPacket = {
|
|
185
|
+
...packet,
|
|
186
|
+
ttl: packet.ttl - 1,
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
// Get links to forward to (excluding ingress)
|
|
190
|
+
const dedupID = makeDeduplicationID(packet);
|
|
191
|
+
const ingressLinkId = excludeLinkId ?? this.ingressLinks.get(dedupID);
|
|
192
|
+
|
|
193
|
+
const targetLinks = Array.from(this.links.values()).filter((link) => link.id !== ingressLinkId);
|
|
194
|
+
|
|
195
|
+
if (targetLinks.length === 0) return;
|
|
196
|
+
|
|
197
|
+
this.emit('forward', forwardPacket, targetLinks);
|
|
198
|
+
|
|
199
|
+
// Send to all target links
|
|
200
|
+
await this.sendToLinks(forwardPacket, targetLinks);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Broadcast to all links
|
|
205
|
+
*/
|
|
206
|
+
private async broadcast(packet: BitchatPacket): Promise<void> {
|
|
207
|
+
const targetLinks = Array.from(this.links.values());
|
|
208
|
+
await this.sendToLinks(packet, targetLinks);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Send packet to specific links
|
|
213
|
+
*/
|
|
214
|
+
private async sendToLinks(packet: BitchatPacket, links: Link[]): Promise<void> {
|
|
215
|
+
const { encode } = await import('../protocol/binary.js');
|
|
216
|
+
const data = encode(packet);
|
|
217
|
+
|
|
218
|
+
const sendPromises = links.map(async (link) => {
|
|
219
|
+
try {
|
|
220
|
+
await link.send(data);
|
|
221
|
+
} catch (error) {
|
|
222
|
+
// Log error but don't fail the whole broadcast
|
|
223
|
+
console.error(`Failed to send to link ${link.id}:`, error);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
await Promise.allSettled(sendPromises);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Send to a specific peer (if we have a direct link)
|
|
232
|
+
*/
|
|
233
|
+
async sendToPeer(packet: BitchatPacket, peerID: PeerID): Promise<boolean> {
|
|
234
|
+
// Find link for this peer
|
|
235
|
+
const link = Array.from(this.links.values()).find((l) => l.peerID?.equals(peerID));
|
|
236
|
+
|
|
237
|
+
if (!link) {
|
|
238
|
+
// No direct link - broadcast and hope for relay
|
|
239
|
+
await this.sendPacket(packet);
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const { encode } = await import('../protocol/binary.js');
|
|
244
|
+
const data = encode(packet);
|
|
245
|
+
await link.send(data);
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Clean up
|
|
251
|
+
*/
|
|
252
|
+
destroy(): void {
|
|
253
|
+
this.deduplicator.destroy();
|
|
254
|
+
this.links.clear();
|
|
255
|
+
this.ingressLinks.clear();
|
|
256
|
+
this.removeAllListeners();
|
|
257
|
+
}
|
|
258
|
+
}
|