Skip to content

Commit 35da446

Browse files
authored
Merge pull request #18 from PrismarineJS/update
Updates
2 parents e303592 + af5bb85 commit 35da446

14 files changed

Lines changed: 1248 additions & 154 deletions

‎.github/workflows/ci.yml‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@ on:
99
jobs:
1010
build:
1111

12-
runs-on: ubuntu-latest
12+
runs-on: ${{ matrix.os }}
13+
timeout-minutes: 10
1314

1415
strategy:
1516
matrix:
1617
node-version: [24.x]
18+
os: [ubuntu-latest, macos-latest, windows-latest]
1719

1820
steps:
1921
- uses: actions/checkout@v2

‎README.md‎

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# node-nethernet
2-
[![NPM version](https://img.shields.io/npm/v/node-nethernet.svg?logo=npm)](http://npmjs.com/package/node-nethernet)
2+
[![NPM version](https://img.shields.io/npm/v/nethernet.svg?logo=npm)](https://npmjs.com/package/nethernet)
33
[![Build Status](https://img.shields.io/github/actions/workflow/status/PrismarineJS/node-nethernet/ci.yml.svg?label=CI&logo=github)](https://github.com/PrismarineJS/node-nethernet/actions?query=workflow%3A%22CI%22)
44
[![Try it on gitpod](https://img.shields.io/static/v1.svg?label=try&message=on%20gitpod&color=brightgreen&logo=gitpod)](https://gitpod.io/#https://github.com/PrismarineJS/node-nethernet)
55
[![GitHub Sponsors](https://img.shields.io/github/sponsors/PrismarineJS)](https://github.com/sponsors/PrismarineJS)
@@ -18,7 +18,7 @@ npm install nethernet
1818
## Example
1919

2020
```ts
21-
const { Client, Server } = require('node-nethernet')
21+
const { Client, Server } = require('nethernet')
2222

2323
const server = new Server()
2424
// Client sends request to the broadcast address and server responds with a message
@@ -38,3 +38,59 @@ server.listen()
3838
client.connect()
3939

4040
```
41+
42+
## Connection options and lifecycle
43+
44+
`new Client(serverNetworkId, broadcastAddress?, options?)` starts UDP discovery.
45+
The default broadcast address is `255.255.255.255`; use `127.0.0.1` for local tests.
46+
Call `connect()` to initiate a connection and listen for `connected`, `disconnect`,
47+
and `error`. `connect()` returns immediately; it is not a promise for connection readiness.
48+
49+
Client options:
50+
51+
- `networkId`, `connectionId`: optional bigint IDs. A retry uses a fresh connection ID.
52+
- `iceServers` (or `credentials`, which takes precedence): string STUN/TURN URLs or
53+
objects such as `{ urls: 'turn:relay.example.com:3478', username: 'user', credential: 'secret' }`.
54+
- `responseTimeoutMs`: defaults to 15000; bounds discovery and then waiting for an answer.
55+
- `inactivityTimeoutMs`: defaults to 5000; bounds connection establishment after the answer,
56+
not application inactivity once connected.
57+
58+
`new Server(options?)` accepts `networkId`, the same ICE options, and
59+
`acceptTimeoutMs` (default 5000) for waiting for connection establishment after an answer.
60+
A timeout value of zero or less disables that timeout.
61+
62+
Negotiation failures produce a `disconnect` reason of `connecterror:<ErrorCode>`.
63+
Local offer/configuration errors also reach an attached `error` listener; without
64+
one they are logged via `DEBUG=nethernet`. Register listeners before calling `connect()`.
65+
After failure or remote disconnect, call `connect()` again to retry. Repeated calls
66+
while connecting or connected are ignored. Call `close()` when finished to release
67+
discovery sockets, timers, and peer resources, including after a failed attempt.
68+
`close()` is idempotent and works before `connect()`/`listen()`. Explicitly closed
69+
clients and servers cannot be reused; create a new instance.
70+
71+
## External signalling
72+
73+
Assign `client.signalHandler` before `connect()` to send signals through your own
74+
transport instead of UDP discovery. Feed incoming signals to `client.handleSignal()`.
75+
`SignalStructure.networkId` identifies the destination on outgoing signals; set it
76+
to the sender's network ID when delivering an incoming signal. Preserve `connectionId`.
77+
78+
On a server, route `ConnectRequest` to `server.handleOffer(signal, respond)` and
79+
`CandidateAdd` to `server.handleCandidate(signal, respond)`. Route `ConnectError` to
80+
`server.connections.get(signal.connectionId)?.close('connecterror:' + signal.data)`.
81+
The `respond` callback must deliver responses to the client through your transport.
82+
Await these asynchronous handlers and handle transport errors. Deliver the SDP
83+
offer/answer before their ICE candidates; arbitrary signalling reordering is not
84+
currently supported. A server used only for external signalling need not call `listen()`.
85+
86+
## WebRTC API and validation
87+
88+
Peer connections and data channels use `@roamhq/wrtc` and expose the standard
89+
WebRTC API (for example `connectionState`, `getStats()`, and `bufferedAmount`).
90+
Type declarations use TypeScript's DOM interfaces; backend-specific extensions are
91+
not declared. The former `node-datachannel` methods are no longer supported.
92+
93+
`npm test` runs lint, strict TypeScript checks, and runtime tests without forcing
94+
process exit. Tests cover local transport, signalling errors, cancellation, and
95+
resource cleanup. Live Minecraft/Realms and authenticated TURN interoperability
96+
still require testing against a real environment before release.

‎index.d.ts‎

Lines changed: 78 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,21 @@
1+
/// <reference lib="dom" />
12
import EventEmitter from 'node:events'
23
import { RemoteInfo, Socket } from 'node:dgram'
3-
import { PeerConnection, DataChannel, IceServer } from 'node-datachannel'
44

55
declare module 'nethernet' {
66

7+
// wrtc implements the standard WebRTC API. Use its standard interfaces without
8+
// importing wrtc's upstream declarations, which currently fail strict checking.
9+
export interface RTCPeerConnectionLike extends RTCPeerConnection {}
10+
11+
export interface RTCDataChannelLike extends RTCDataChannel {}
12+
13+
export interface IceServer {
14+
urls: string | string[]
15+
username?: string
16+
credential?: string
17+
}
18+
719
export interface ResponsePacket {
820
binary: number[]
921
buffer: Buffer
@@ -18,24 +30,29 @@ declare module 'nethernet' {
1830
export class Connection {
1931
nethernet: Client | Server;
2032
address: bigint;
21-
rtcConnection: PeerConnection;
22-
reliable: DataChannel | null;
23-
unreliable: DataChannel | null;
33+
rtcConnection: RTCPeerConnectionLike;
34+
reliable: RTCDataChannelLike | null;
35+
unreliable: RTCDataChannelLike | null;
2436
promisedSegments: number;
2537
buf: Buffer | null;
26-
sendQueue: Buffer[];
38+
sendQueue: Buffer[];
2739

28-
constructor(nethernet: Client | Server, address: bigint, rtcConnection: PeerConnection);
29-
setChannels(reliable?: DataChannel | null, unreliable?: DataChannel | null): void;
40+
closed: boolean;
41+
42+
constructor(nethernet: Client | Server, address: bigint, rtcConnection: RTCPeerConnectionLike);
43+
setChannels(reliable?: RTCDataChannelLike | null, unreliable?: RTCDataChannelLike | null): void;
3044
handleMessage(data: Buffer | string | ArrayBuffer): void;
3145
send(data: Buffer | string): number;
3246
sendNow(data: Buffer): number;
3347
flushQueue(): void;
34-
close(): void;
48+
close(reason?: string): void;
3549
}
3650

3751
export interface ServerOptions {
3852
networkId?: bigint;
53+
credentials?: (string | IceServer)[];
54+
iceServers?: (string | IceServer)[];
55+
acceptTimeoutMs?: number;
3956
}
4057

4158
export interface ServerEvents {
@@ -47,15 +64,18 @@ declare module 'nethernet' {
4764

4865
export class Server extends EventEmitter {
4966
options: ServerOptions;
67+
credentials: IceServer[];
68+
acceptTimeoutMs: number;
5069
networkId: bigint;
5170
connections: Map<bigint, Connection>;
71+
acceptTimeouts: Map<bigint, NodeJS.Timeout>;
5272
advertisement?: Buffer;
5373
socket: Socket;
5474
serializer: any;
5575
deserializer: any;
5676

5777
constructor(options?: ServerOptions);
58-
handleCandidate(signal: SignalStructure): Promise<void>;
78+
handleCandidate(signal: SignalStructure, respond?: (signal: SignalStructure) => void): Promise<void>;
5979
handleOffer(signal: SignalStructure, respond: (signal: SignalStructure) => void, credentials?: (string | IceServer)[]): Promise<void>;
6080
processPacket(buffer: Buffer, rinfo: RemoteInfo): void;
6181
setAdvertisement(buffer: Buffer): void;
@@ -73,7 +93,17 @@ declare module 'nethernet' {
7393
disconnect: (connectionId: bigint, reason: string) => void;
7494
encapsulated: (data: Buffer, connectionId: bigint) => void;
7595
pong: (packet: any) => void;
76-
}
96+
error: (error: Error) => void;
97+
}
98+
99+
export interface ClientOptions {
100+
networkId?: bigint;
101+
connectionId?: bigint;
102+
credentials?: (string | IceServer)[];
103+
iceServers?: (string | IceServer)[];
104+
responseTimeoutMs?: number;
105+
inactivityTimeoutMs?: number;
106+
}
77107

78108
export class Client extends EventEmitter {
79109
serverNetworkId: bigint;
@@ -85,14 +115,17 @@ declare module 'nethernet' {
85115
deserializer: any;
86116
responses: Map<bigint, any>;
87117
addresses: Map<bigint, RemoteInfo>;
88-
credentials: (string | IceServer)[];
118+
credentials: IceServer[];
119+
responseTimeoutMs: number;
120+
inactivityTimeoutMs: number;
89121
signalHandler: (signal: SignalStructure) => void;
90-
connection?: Connection;
91-
rtcConnection?: PeerConnection;
122+
connection?: Connection | null;
123+
rtcConnection?: RTCPeerConnectionLike | null;
92124
pingInterval?: NodeJS.Timeout;
93125
running: boolean;
94-
126+
95127
constructor(networkId: bigint, broadcastAddress?: string);
128+
constructor(networkId: bigint, broadcastAddress: string | undefined, options: ClientOptions);
96129
handleCandidate(signal: SignalStructure): Promise<void>;
97130
handleAnswer(signal: SignalStructure): Promise<void>;
98131
createOffer(): Promise<void>;
@@ -102,7 +135,7 @@ declare module 'nethernet' {
102135
handleSignal(signal: SignalStructure): void;
103136
sendDiscoveryRequest(): void;
104137
sendDiscoveryMessage(signal: SignalStructure): void;
105-
connect(): Promise<void>;
138+
connect(): void;
106139
send(buffer: Buffer): void;
107140
ping(): void;
108141
close(reason?: string): void;
@@ -118,6 +151,36 @@ declare module 'nethernet' {
118151
ConnectError = 'CONNECTERROR'
119152
}
120153

154+
export enum ErrorCode {
155+
None = 0,
156+
DestinationNotLoggedIn = 1,
157+
NegotiationTimeout = 2,
158+
WrongTransportVersion = 3,
159+
FailedToCreatePeerConnection = 4,
160+
ICE = 5,
161+
ConnectRequest = 6,
162+
ConnectResponse = 7,
163+
CandidateAdd = 8,
164+
InactivityTimeout = 9,
165+
FailedToCreateOffer = 10,
166+
FailedToCreateAnswer = 11,
167+
FailedToSetLocalDescription = 12,
168+
FailedToSetRemoteDescription = 13,
169+
NegotiationTimeoutWaitingForResponse = 14,
170+
NegotiationTimeoutWaitingForAccept = 15,
171+
IncomingConnectionIgnored = 16,
172+
SignalingParsingFailure = 17,
173+
SignalingUnknownError = 18,
174+
SignalingUnicastMessageDeliveryFailed = 19,
175+
SignalingBroadcastDeliveryFailed = 20,
176+
SignalingMessageDeliveryFailed = 21,
177+
SignalingTurnAuthFailed = 22,
178+
SignalingFallbackToBestEffortDelivery = 23,
179+
NoSignalingChannel = 24,
180+
NotLoggedIn = 25,
181+
SignalingFailedToSend = 26
182+
}
183+
121184
export class SignalStructure {
122185
type: SignalType;
123186
connectionId: bigint;

‎index.js‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
const { Client } = require('./src/client')
22
const { Server } = require('./src/server')
3-
const { SignalStructure } = require('./src/signalling')
3+
const { ErrorCode, SignalStructure } = require('./src/signalling')
44

55
const SignalType = {
66
ConnectRequest: 'CONNECTREQUEST',
@@ -11,6 +11,7 @@ const SignalType = {
1111

1212
module.exports = {
1313
Client,
14+
ErrorCode,
1415
Server,
1516
SignalType,
1617
SignalStructure

‎package.json‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44
"description": "(WIP) Minecraft Bedrock nethernet protocol",
55
"main": "index.js",
66
"scripts": {
7-
"test": "mocha --reporter spec --exit",
8-
"pretest": "npm run lint",
7+
"test": "mocha --reporter spec",
8+
"pretest": "npm run lint && npm run typecheck",
99
"lint": "standard",
10+
"typecheck": "tsc --noEmit --strict --types node --target ES2022 --module Node16 --moduleResolution Node16 test/types.test.ts",
1011
"fix": "standard --fix"
1112
},
1213
"repository": {
@@ -24,13 +25,15 @@
2425
},
2526
"homepage": "https://github.com/PrismarineJS/node-nethernet#readme",
2627
"devDependencies": {
28+
"@types/node": "^24.13.6",
2729
"debug": "^4.4.0",
2830
"mocha": "^11.7.5",
2931
"nethernet": "file:.",
30-
"standard": "^17.0.0"
32+
"standard": "^17.0.0",
33+
"typescript": "^7.0.2"
3134
},
3235
"dependencies": {
33-
"node-datachannel": "^0.26.0",
36+
"@roamhq/wrtc": "^0.10.0",
3437
"protodef": "^1.19.0"
3538
}
3639
}

0 commit comments

Comments
 (0)