@fuman/net 0.0.1
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/LICENSE +8 -0
- package/errors.cjs +14 -0
- package/errors.d.ts +6 -0
- package/errors.js +14 -0
- package/fake.cjs +42 -0
- package/fake.d.ts +16 -0
- package/fake.js +42 -0
- package/index.cjs +30 -0
- package/index.d.ts +7 -0
- package/index.js +30 -0
- package/ip/bundle.cjs +19 -0
- package/ip/bundle.d.ts +3 -0
- package/ip/bundle.js +19 -0
- package/ip/index.d.ts +8 -0
- package/ip/parse.cjs +34 -0
- package/ip/parse.d.ts +5 -0
- package/ip/parse.js +34 -0
- package/ip/types.d.ts +10 -0
- package/ip/v4.bench.d.ts +1 -0
- package/ip/v4.cjs +55 -0
- package/ip/v4.d.ts +4 -0
- package/ip/v4.js +55 -0
- package/ip/v6.bench.d.ts +1 -0
- package/ip/v6.cjs +217 -0
- package/ip/v6.d.ts +18 -0
- package/ip/v6.js +217 -0
- package/package.json +30 -0
- package/proxy/http/_protocol.cjs +31 -0
- package/proxy/http/_protocol.d.ts +3 -0
- package/proxy/http/_protocol.js +31 -0
- package/proxy/http/connect.cjs +34 -0
- package/proxy/http/connect.d.ts +4 -0
- package/proxy/http/connect.js +34 -0
- package/proxy/http/index.cjs +15 -0
- package/proxy/http/index.d.ts +5 -0
- package/proxy/http/index.js +15 -0
- package/proxy/http/types.cjs +10 -0
- package/proxy/http/types.d.ts +32 -0
- package/proxy/http/types.js +10 -0
- package/proxy/index.d.ts +2 -0
- package/proxy/socks/_protocol.cjs +111 -0
- package/proxy/socks/_protocol.d.ts +7 -0
- package/proxy/socks/_protocol.js +111 -0
- package/proxy/socks/connect.cjs +78 -0
- package/proxy/socks/connect.d.ts +4 -0
- package/proxy/socks/connect.js +78 -0
- package/proxy/socks/index.cjs +15 -0
- package/proxy/socks/index.d.ts +5 -0
- package/proxy/socks/index.js +15 -0
- package/proxy/socks/types.cjs +10 -0
- package/proxy/socks/types.d.ts +34 -0
- package/proxy/socks/types.js +10 -0
- package/reconnection.cjs +151 -0
- package/reconnection.d.ts +82 -0
- package/reconnection.js +151 -0
- package/types.d.ts +41 -0
- package/websocket.cjs +157 -0
- package/websocket.d.ts +44 -0
- package/websocket.js +157 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { MaybePromise } from '@fuman/utils';
|
|
2
|
+
import { ConnectFunction, IConnection } from './types.js';
|
|
3
|
+
interface ReconnectionState {
|
|
4
|
+
readonly previousWait: number | null;
|
|
5
|
+
readonly consequentFails: number;
|
|
6
|
+
readonly lastError: Error | null;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Declares a strategy to handle reconnection.
|
|
10
|
+
* When a number is returned, that number of MS will be waited before trying to reconnect.
|
|
11
|
+
* When `false` is returned, connection is not reconnected
|
|
12
|
+
*/
|
|
13
|
+
export type ReconnectionStrategy = (state: ReconnectionState) => number | false;
|
|
14
|
+
/**
|
|
15
|
+
* Declares an action to take when an error occurs.
|
|
16
|
+
*
|
|
17
|
+
* - `reconnect` - reconnect using the current strategy
|
|
18
|
+
* - `reconnect-now` - reconnect immediately, ignoring the current strategy
|
|
19
|
+
* - `close` - close the connection
|
|
20
|
+
*/
|
|
21
|
+
export type OnErrorAction = 'reconnect' | 'reconnect-now' | 'close';
|
|
22
|
+
/**
|
|
23
|
+
* default reconnection strategy: first - immediate reconnection,
|
|
24
|
+
* then 1s with linear increase up to 5s (with 1s step)
|
|
25
|
+
*/
|
|
26
|
+
export declare const defaultReconnectionStrategy: ReconnectionStrategy;
|
|
27
|
+
export declare class PersistentConnection<ConnectAddress, Connection extends IConnection<unknown>> {
|
|
28
|
+
#private;
|
|
29
|
+
readonly params: {
|
|
30
|
+
connect: ConnectFunction<ConnectAddress, Connection>;
|
|
31
|
+
strategy?: ReconnectionStrategy;
|
|
32
|
+
/**
|
|
33
|
+
* Function to call once the connection is open
|
|
34
|
+
*
|
|
35
|
+
* As soon as the promise is resolved the connection will be closed (and will *not*
|
|
36
|
+
* be re-opened automatically), so this is the best place to put your recv loop
|
|
37
|
+
*/
|
|
38
|
+
onOpen: (connection: Connection) => Promise<void>;
|
|
39
|
+
onClose?: () => MaybePromise<void>;
|
|
40
|
+
onWait?: (wait: number) => void;
|
|
41
|
+
/**
|
|
42
|
+
* Function that will be called whenever an error happens while connecting
|
|
43
|
+
* (in which case `connection` will be null) or inside `onOpen`.
|
|
44
|
+
*
|
|
45
|
+
* @default `(err) => err instanceof ConnectionClosedError ? 'reconnect' : 'close'`
|
|
46
|
+
*/
|
|
47
|
+
onError?: (error: Error, connection: Connection | null, state: ReconnectionState) => MaybePromise<OnErrorAction>;
|
|
48
|
+
};
|
|
49
|
+
constructor(params: {
|
|
50
|
+
connect: ConnectFunction<ConnectAddress, Connection>;
|
|
51
|
+
strategy?: ReconnectionStrategy;
|
|
52
|
+
/**
|
|
53
|
+
* Function to call once the connection is open
|
|
54
|
+
*
|
|
55
|
+
* As soon as the promise is resolved the connection will be closed (and will *not*
|
|
56
|
+
* be re-opened automatically), so this is the best place to put your recv loop
|
|
57
|
+
*/
|
|
58
|
+
onOpen: (connection: Connection) => Promise<void>;
|
|
59
|
+
onClose?: () => MaybePromise<void>;
|
|
60
|
+
onWait?: (wait: number) => void;
|
|
61
|
+
/**
|
|
62
|
+
* Function that will be called whenever an error happens while connecting
|
|
63
|
+
* (in which case `connection` will be null) or inside `onOpen`.
|
|
64
|
+
*
|
|
65
|
+
* @default `(err) => err instanceof ConnectionClosedError ? 'reconnect' : 'close'`
|
|
66
|
+
*/
|
|
67
|
+
onError?: (error: Error, connection: Connection | null, state: ReconnectionState) => MaybePromise<OnErrorAction>;
|
|
68
|
+
});
|
|
69
|
+
get isConnected(): boolean;
|
|
70
|
+
get isConnecting(): boolean;
|
|
71
|
+
get isWaiting(): boolean;
|
|
72
|
+
get connection(): Connection | null;
|
|
73
|
+
get state(): ReconnectionState;
|
|
74
|
+
connect(address: ConnectAddress): void;
|
|
75
|
+
/**
|
|
76
|
+
* @param force Whether to close the existing connection if there is one
|
|
77
|
+
*/
|
|
78
|
+
reconnect(force: boolean): void;
|
|
79
|
+
close(): Promise<void>;
|
|
80
|
+
changeTransport(connect: ConnectFunction<ConnectAddress, Connection>): Promise<void>;
|
|
81
|
+
}
|
|
82
|
+
export {};
|
package/reconnection.js
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { Deferred, timers } from "@fuman/utils";
|
|
2
|
+
import { ConnectionClosedError } from "./errors.js";
|
|
3
|
+
const defaultReconnectionStrategy = ({ previousWait }) => {
|
|
4
|
+
if (previousWait === null) return 0;
|
|
5
|
+
if (previousWait === 0) return 1e3;
|
|
6
|
+
if (previousWait >= 5e3) return 5e3;
|
|
7
|
+
return Math.min(5e3, previousWait + 1e3);
|
|
8
|
+
};
|
|
9
|
+
function defaultOnError(err) {
|
|
10
|
+
return err instanceof ConnectionClosedError ? "reconnect" : "close";
|
|
11
|
+
}
|
|
12
|
+
class PersistentConnection {
|
|
13
|
+
constructor(params) {
|
|
14
|
+
this.params = params;
|
|
15
|
+
this.#strategy = params.strategy ?? defaultReconnectionStrategy;
|
|
16
|
+
this.#connect = params.connect;
|
|
17
|
+
this.#onError = params.onError ?? defaultOnError;
|
|
18
|
+
}
|
|
19
|
+
#state = {
|
|
20
|
+
previousWait: null,
|
|
21
|
+
lastError: null,
|
|
22
|
+
consequentFails: 0
|
|
23
|
+
};
|
|
24
|
+
#connect;
|
|
25
|
+
#lastAddress;
|
|
26
|
+
#connection;
|
|
27
|
+
#connecting = false;
|
|
28
|
+
#strategy;
|
|
29
|
+
#onError;
|
|
30
|
+
// boolean represents whether the timer is clean
|
|
31
|
+
// true - resolved because timer is up
|
|
32
|
+
// false - resolved because .close()/.reconnect() was called
|
|
33
|
+
#sleep;
|
|
34
|
+
#closed;
|
|
35
|
+
get isConnected() {
|
|
36
|
+
return this.#connection !== void 0;
|
|
37
|
+
}
|
|
38
|
+
get isConnecting() {
|
|
39
|
+
return this.#connection === void 0 && this.#connecting;
|
|
40
|
+
}
|
|
41
|
+
get isWaiting() {
|
|
42
|
+
return this.#connection === void 0 && this.#lastAddress !== void 0 && !this.#connecting;
|
|
43
|
+
}
|
|
44
|
+
get connection() {
|
|
45
|
+
return this.#connection ?? null;
|
|
46
|
+
}
|
|
47
|
+
get state() {
|
|
48
|
+
return this.#state;
|
|
49
|
+
}
|
|
50
|
+
#resetState() {
|
|
51
|
+
this.#state.previousWait = null;
|
|
52
|
+
this.#state.lastError = null;
|
|
53
|
+
this.#state.consequentFails = 0;
|
|
54
|
+
this.#connecting = false;
|
|
55
|
+
}
|
|
56
|
+
async #loop() {
|
|
57
|
+
while (true) {
|
|
58
|
+
try {
|
|
59
|
+
this.#connecting = true;
|
|
60
|
+
this.#connection = await this.#connect(this.#lastAddress);
|
|
61
|
+
if (this.#closed) {
|
|
62
|
+
this.#closed.resolve();
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
this.#resetState();
|
|
66
|
+
await this.params.onOpen?.(this.#connection);
|
|
67
|
+
this.#connection.close();
|
|
68
|
+
this.#connection = void 0;
|
|
69
|
+
break;
|
|
70
|
+
} catch (err) {
|
|
71
|
+
const oldConnection = this.#connection;
|
|
72
|
+
this.#connection = void 0;
|
|
73
|
+
await this.params.onClose?.();
|
|
74
|
+
if (this.#closed) {
|
|
75
|
+
this.#closed.resolve();
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
const action = await this.#onError(err, oldConnection ?? null, this.#state);
|
|
79
|
+
if (action === "close") {
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
const wait = action === "reconnect-now" ? 0 : this.#strategy(this.#state);
|
|
83
|
+
if (wait === false) {
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
this.params.onWait?.(wait);
|
|
87
|
+
if (wait !== 0) {
|
|
88
|
+
this.#sleep = new Deferred();
|
|
89
|
+
const timer = timers.setTimeout(this.#sleep.resolve, wait, true);
|
|
90
|
+
const sleepResult = await this.#sleep.promise;
|
|
91
|
+
this.#sleep = void 0;
|
|
92
|
+
if (!sleepResult) {
|
|
93
|
+
timers.clearTimeout(timer);
|
|
94
|
+
if (this.#closed != null) {
|
|
95
|
+
this.#closed.resolve();
|
|
96
|
+
break;
|
|
97
|
+
} else {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
this.#state.previousWait = wait;
|
|
103
|
+
this.#state.consequentFails = 1;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
this.#lastAddress = void 0;
|
|
108
|
+
this.#resetState();
|
|
109
|
+
}
|
|
110
|
+
connect(address) {
|
|
111
|
+
if (this.#lastAddress !== void 0 && this.#lastAddress !== address) {
|
|
112
|
+
throw new Error("Connection is already open to another address");
|
|
113
|
+
}
|
|
114
|
+
this.#closed = void 0;
|
|
115
|
+
this.#lastAddress = address;
|
|
116
|
+
void this.#loop();
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* @param force Whether to close the existing connection if there is one
|
|
120
|
+
*/
|
|
121
|
+
reconnect(force) {
|
|
122
|
+
if (this.#sleep) {
|
|
123
|
+
this.#sleep.resolve(false);
|
|
124
|
+
} else if (this.#connection && force) {
|
|
125
|
+
this.#connection.close();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
async close() {
|
|
129
|
+
if (this.#closed) return this.#closed.promise;
|
|
130
|
+
if (this.#lastAddress == null) return;
|
|
131
|
+
this.#closed = new Deferred();
|
|
132
|
+
if (this.#sleep) {
|
|
133
|
+
this.#sleep.resolve(false);
|
|
134
|
+
} else if (this.#connection) {
|
|
135
|
+
this.#connection.close();
|
|
136
|
+
} else if (this.#connecting) ;
|
|
137
|
+
return this.#closed.promise;
|
|
138
|
+
}
|
|
139
|
+
async changeTransport(connect) {
|
|
140
|
+
this.#connect = connect;
|
|
141
|
+
const addr = this.#lastAddress;
|
|
142
|
+
await this.close();
|
|
143
|
+
if (addr != null) {
|
|
144
|
+
this.connect(addr);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
export {
|
|
149
|
+
PersistentConnection,
|
|
150
|
+
defaultReconnectionStrategy
|
|
151
|
+
};
|
package/types.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { IClosable, IReadable, IWritable } from '@fuman/io';
|
|
2
|
+
export interface IConnection<Address, LocalAddress = Address> extends IReadable, IWritable, IClosable {
|
|
3
|
+
readonly localAddress: LocalAddress | null;
|
|
4
|
+
readonly remoteAddress: Address | null;
|
|
5
|
+
}
|
|
6
|
+
export interface TcpEndpoint {
|
|
7
|
+
readonly address: string;
|
|
8
|
+
readonly port: number;
|
|
9
|
+
}
|
|
10
|
+
export interface TlsOptions {
|
|
11
|
+
/**
|
|
12
|
+
* List of CA certificates to use.
|
|
13
|
+
* Will replace whatever is in the default platform CA store.
|
|
14
|
+
*/
|
|
15
|
+
readonly caCerts?: string[];
|
|
16
|
+
/** List of ALPN protocols to accept/offer */
|
|
17
|
+
readonly alpnProtocols?: string[];
|
|
18
|
+
/** Hostname to use for SNI */
|
|
19
|
+
readonly sni?: string;
|
|
20
|
+
}
|
|
21
|
+
export interface TlsConnectOptions extends TcpEndpoint, TlsOptions {
|
|
22
|
+
}
|
|
23
|
+
export interface TlsListenOptions extends TcpEndpoint, TlsOptions {
|
|
24
|
+
readonly key?: string;
|
|
25
|
+
readonly cert?: string;
|
|
26
|
+
readonly hosts?: Omit<this, 'hosts' | 'address' | 'port'>[];
|
|
27
|
+
}
|
|
28
|
+
export interface ITcpConnection extends IConnection<TcpEndpoint> {
|
|
29
|
+
setNoDelay: (noDelay: boolean) => void;
|
|
30
|
+
setKeepAlive: (keepAlive: boolean) => void;
|
|
31
|
+
}
|
|
32
|
+
export interface ITlsConnection extends ITcpConnection {
|
|
33
|
+
getAlpnProtocol: () => string | null;
|
|
34
|
+
}
|
|
35
|
+
export interface IListener<Address, Connection extends IConnection<Address> = IConnection<Address>> extends IClosable {
|
|
36
|
+
readonly address: Address | null;
|
|
37
|
+
accept: () => Promise<Connection>;
|
|
38
|
+
}
|
|
39
|
+
export type ListenFunction<Options, Listener extends IListener<unknown>> = (options: Options) => Promise<Listener>;
|
|
40
|
+
export type ConnectFunction<Options, Connection extends IConnection<unknown>> = (options: Options) => Promise<Connection>;
|
|
41
|
+
export type TlsUpgradeFunction<Options, TcpConnection extends ITcpConnection, TlsConnection extends ITlsConnection> = (tcpConn: TcpConnection, options: Options) => Promise<TlsConnection>;
|
package/websocket.cjs
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const io = require("@fuman/io");
|
|
4
|
+
const utils = require("@fuman/utils");
|
|
5
|
+
const errors = require("./errors.cjs");
|
|
6
|
+
class WebSocketConnectionClosedError extends errors.ConnectionClosedError {
|
|
7
|
+
constructor(code, reason) {
|
|
8
|
+
super(`code ${code} (${reason})`);
|
|
9
|
+
this.code = code;
|
|
10
|
+
this.reason = reason;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function eventToError(event) {
|
|
14
|
+
if (event instanceof Error) {
|
|
15
|
+
return event;
|
|
16
|
+
}
|
|
17
|
+
return "error" in event ? event.error : new Error("unknown WebSocket error", { cause: event });
|
|
18
|
+
}
|
|
19
|
+
class WebSocketConnectionBase {
|
|
20
|
+
constructor(socket) {
|
|
21
|
+
this.socket = socket;
|
|
22
|
+
this._cv = new utils.ConditionVariable();
|
|
23
|
+
socket.addEventListener("message", (event) => {
|
|
24
|
+
this.onMessage(event);
|
|
25
|
+
this._cv.notify();
|
|
26
|
+
});
|
|
27
|
+
socket.addEventListener("close", (event) => {
|
|
28
|
+
this._error = new WebSocketConnectionClosedError(event.code, event.reason);
|
|
29
|
+
this._cv.notify();
|
|
30
|
+
});
|
|
31
|
+
socket.addEventListener("error", (event) => {
|
|
32
|
+
this._error = eventToError(event);
|
|
33
|
+
this._cv.notify();
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
_error = null;
|
|
37
|
+
_cv;
|
|
38
|
+
get remoteAddress() {
|
|
39
|
+
return this.socket.url;
|
|
40
|
+
}
|
|
41
|
+
get localAddress() {
|
|
42
|
+
throw new Error(".localAddress is not available for WebSockets");
|
|
43
|
+
}
|
|
44
|
+
close() {
|
|
45
|
+
this.socket?.close();
|
|
46
|
+
this._error = new errors.ConnectionClosedError();
|
|
47
|
+
}
|
|
48
|
+
closeWithCode(code, reason) {
|
|
49
|
+
this.socket.close(code, reason);
|
|
50
|
+
this._error = new errors.ConnectionClosedError();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
class WebSocketConnection extends WebSocketConnectionBase {
|
|
54
|
+
#buffer;
|
|
55
|
+
constructor(socket) {
|
|
56
|
+
super(socket);
|
|
57
|
+
this.#buffer = io.Bytes.alloc(0);
|
|
58
|
+
}
|
|
59
|
+
onMessage(event) {
|
|
60
|
+
if (typeof event.data === "string") {
|
|
61
|
+
const buf = this.#buffer.writeSync(utils.utf8.encodedLength(event.data));
|
|
62
|
+
utils.utf8.encoder.encodeInto(event.data, buf);
|
|
63
|
+
} else {
|
|
64
|
+
const u8 = new Uint8Array(event.data);
|
|
65
|
+
this.#buffer.writeSync(u8.length).set(u8);
|
|
66
|
+
}
|
|
67
|
+
this.#buffer.disposeWriteSync();
|
|
68
|
+
}
|
|
69
|
+
async read(into) {
|
|
70
|
+
if (this.#buffer.available > 0) {
|
|
71
|
+
const size2 = Math.min(this.#buffer.available, into.length);
|
|
72
|
+
into.set(this.#buffer.readSync(size2));
|
|
73
|
+
this.#buffer.reclaim();
|
|
74
|
+
return size2;
|
|
75
|
+
}
|
|
76
|
+
if (this._error !== null) throw this._error;
|
|
77
|
+
await this._cv.wait();
|
|
78
|
+
if (this._error !== null) throw this._error;
|
|
79
|
+
const size = Math.min(this.#buffer.available, into.length);
|
|
80
|
+
into.set(this.#buffer.readSync(size));
|
|
81
|
+
this.#buffer.reclaim();
|
|
82
|
+
return size;
|
|
83
|
+
}
|
|
84
|
+
async write(bytes) {
|
|
85
|
+
if (this._error) throw this._error;
|
|
86
|
+
if (!bytes.length) return;
|
|
87
|
+
this.socket.send(bytes);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
class WebSocketConnectionFramed extends WebSocketConnectionBase {
|
|
91
|
+
#buffer = new utils.Deque();
|
|
92
|
+
onMessage(event) {
|
|
93
|
+
if (typeof event.data === "string") {
|
|
94
|
+
this.#buffer.pushBack(event.data);
|
|
95
|
+
} else {
|
|
96
|
+
this.#buffer.pushBack(new Uint8Array(event.data));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
async readFrame() {
|
|
100
|
+
if (!this.#buffer.isEmpty()) {
|
|
101
|
+
return this.#buffer.popFront();
|
|
102
|
+
}
|
|
103
|
+
if (this._error !== null) throw this._error;
|
|
104
|
+
await this._cv.wait();
|
|
105
|
+
if (this._error !== null) throw this._error;
|
|
106
|
+
return this.#buffer.popFront();
|
|
107
|
+
}
|
|
108
|
+
async writeFrame(data) {
|
|
109
|
+
if (this._error) throw this._error;
|
|
110
|
+
this.socket.send(data);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const connectWs = (endpoint) => {
|
|
114
|
+
const {
|
|
115
|
+
url,
|
|
116
|
+
implementation: WebSocketImpl = WebSocket,
|
|
117
|
+
protocols
|
|
118
|
+
} = endpoint;
|
|
119
|
+
return new Promise((resolve, reject) => {
|
|
120
|
+
const socket = new WebSocketImpl(url, protocols);
|
|
121
|
+
socket.binaryType = "arraybuffer";
|
|
122
|
+
const onError = (event) => {
|
|
123
|
+
socket.removeEventListener("error", onError);
|
|
124
|
+
reject(eventToError(event));
|
|
125
|
+
};
|
|
126
|
+
socket.addEventListener("error", onError);
|
|
127
|
+
socket.addEventListener("open", () => {
|
|
128
|
+
socket.removeEventListener("error", onError);
|
|
129
|
+
resolve(new WebSocketConnection(socket));
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
};
|
|
133
|
+
async function connectWsFramed(endpoint) {
|
|
134
|
+
const {
|
|
135
|
+
url,
|
|
136
|
+
implementation: WebSocketImpl = WebSocket,
|
|
137
|
+
protocols
|
|
138
|
+
} = endpoint;
|
|
139
|
+
return new Promise((resolve, reject) => {
|
|
140
|
+
const socket = new WebSocketImpl(url, protocols);
|
|
141
|
+
socket.binaryType = "arraybuffer";
|
|
142
|
+
const onError = (event) => {
|
|
143
|
+
socket.removeEventListener("error", onError);
|
|
144
|
+
reject(eventToError(event));
|
|
145
|
+
};
|
|
146
|
+
socket.addEventListener("error", onError);
|
|
147
|
+
socket.addEventListener("open", () => {
|
|
148
|
+
socket.removeEventListener("error", onError);
|
|
149
|
+
resolve(new WebSocketConnectionFramed(socket));
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
exports.WebSocketConnection = WebSocketConnection;
|
|
154
|
+
exports.WebSocketConnectionClosedError = WebSocketConnectionClosedError;
|
|
155
|
+
exports.WebSocketConnectionFramed = WebSocketConnectionFramed;
|
|
156
|
+
exports.connectWs = connectWs;
|
|
157
|
+
exports.connectWsFramed = connectWsFramed;
|
package/websocket.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { IClosable } from '@fuman/io';
|
|
2
|
+
import { ConnectFunction, IConnection } from './types.js';
|
|
3
|
+
import { ConditionVariable } from '@fuman/utils';
|
|
4
|
+
import { ConnectionClosedError } from './errors.js';
|
|
5
|
+
export declare class WebSocketConnectionClosedError extends ConnectionClosedError {
|
|
6
|
+
readonly code: number;
|
|
7
|
+
readonly reason: string;
|
|
8
|
+
constructor(code: number, reason: string);
|
|
9
|
+
}
|
|
10
|
+
declare abstract class WebSocketConnectionBase implements IClosable {
|
|
11
|
+
readonly socket: WebSocket;
|
|
12
|
+
protected _error: Error | null;
|
|
13
|
+
protected _cv: ConditionVariable;
|
|
14
|
+
constructor(socket: WebSocket);
|
|
15
|
+
get remoteAddress(): string | null;
|
|
16
|
+
get localAddress(): never;
|
|
17
|
+
close(): void;
|
|
18
|
+
closeWithCode(code: number, reason?: string): void;
|
|
19
|
+
abstract onMessage(event: MessageEvent): void;
|
|
20
|
+
}
|
|
21
|
+
export declare class WebSocketConnection extends WebSocketConnectionBase implements IConnection<string, never> {
|
|
22
|
+
#private;
|
|
23
|
+
constructor(socket: WebSocket);
|
|
24
|
+
onMessage(event: MessageEvent): void;
|
|
25
|
+
read(into: Uint8Array): Promise<number>;
|
|
26
|
+
write(bytes: Uint8Array): Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
export declare class WebSocketConnectionFramed extends WebSocketConnectionBase {
|
|
29
|
+
#private;
|
|
30
|
+
onMessage(event: MessageEvent): void;
|
|
31
|
+
readFrame(): Promise<Uint8Array | string>;
|
|
32
|
+
writeFrame(data: Uint8Array | string): Promise<void>;
|
|
33
|
+
}
|
|
34
|
+
export interface WebSocketConstructor {
|
|
35
|
+
new (url: string | URL, protocols?: string | string[]): WebSocket;
|
|
36
|
+
}
|
|
37
|
+
export interface WebSocketEndpoint {
|
|
38
|
+
readonly url: string | URL;
|
|
39
|
+
readonly implementation?: WebSocketConstructor;
|
|
40
|
+
readonly protocols?: string | string[];
|
|
41
|
+
}
|
|
42
|
+
export declare const connectWs: ConnectFunction<WebSocketEndpoint, WebSocketConnection>;
|
|
43
|
+
export declare function connectWsFramed(endpoint: WebSocketEndpoint): Promise<WebSocketConnectionFramed>;
|
|
44
|
+
export {};
|
package/websocket.js
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { Bytes } from "@fuman/io";
|
|
2
|
+
import { utf8, Deque, ConditionVariable } from "@fuman/utils";
|
|
3
|
+
import { ConnectionClosedError } from "./errors.js";
|
|
4
|
+
class WebSocketConnectionClosedError extends ConnectionClosedError {
|
|
5
|
+
constructor(code, reason) {
|
|
6
|
+
super(`code ${code} (${reason})`);
|
|
7
|
+
this.code = code;
|
|
8
|
+
this.reason = reason;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function eventToError(event) {
|
|
12
|
+
if (event instanceof Error) {
|
|
13
|
+
return event;
|
|
14
|
+
}
|
|
15
|
+
return "error" in event ? event.error : new Error("unknown WebSocket error", { cause: event });
|
|
16
|
+
}
|
|
17
|
+
class WebSocketConnectionBase {
|
|
18
|
+
constructor(socket) {
|
|
19
|
+
this.socket = socket;
|
|
20
|
+
this._cv = new ConditionVariable();
|
|
21
|
+
socket.addEventListener("message", (event) => {
|
|
22
|
+
this.onMessage(event);
|
|
23
|
+
this._cv.notify();
|
|
24
|
+
});
|
|
25
|
+
socket.addEventListener("close", (event) => {
|
|
26
|
+
this._error = new WebSocketConnectionClosedError(event.code, event.reason);
|
|
27
|
+
this._cv.notify();
|
|
28
|
+
});
|
|
29
|
+
socket.addEventListener("error", (event) => {
|
|
30
|
+
this._error = eventToError(event);
|
|
31
|
+
this._cv.notify();
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
_error = null;
|
|
35
|
+
_cv;
|
|
36
|
+
get remoteAddress() {
|
|
37
|
+
return this.socket.url;
|
|
38
|
+
}
|
|
39
|
+
get localAddress() {
|
|
40
|
+
throw new Error(".localAddress is not available for WebSockets");
|
|
41
|
+
}
|
|
42
|
+
close() {
|
|
43
|
+
this.socket?.close();
|
|
44
|
+
this._error = new ConnectionClosedError();
|
|
45
|
+
}
|
|
46
|
+
closeWithCode(code, reason) {
|
|
47
|
+
this.socket.close(code, reason);
|
|
48
|
+
this._error = new ConnectionClosedError();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
class WebSocketConnection extends WebSocketConnectionBase {
|
|
52
|
+
#buffer;
|
|
53
|
+
constructor(socket) {
|
|
54
|
+
super(socket);
|
|
55
|
+
this.#buffer = Bytes.alloc(0);
|
|
56
|
+
}
|
|
57
|
+
onMessage(event) {
|
|
58
|
+
if (typeof event.data === "string") {
|
|
59
|
+
const buf = this.#buffer.writeSync(utf8.encodedLength(event.data));
|
|
60
|
+
utf8.encoder.encodeInto(event.data, buf);
|
|
61
|
+
} else {
|
|
62
|
+
const u8 = new Uint8Array(event.data);
|
|
63
|
+
this.#buffer.writeSync(u8.length).set(u8);
|
|
64
|
+
}
|
|
65
|
+
this.#buffer.disposeWriteSync();
|
|
66
|
+
}
|
|
67
|
+
async read(into) {
|
|
68
|
+
if (this.#buffer.available > 0) {
|
|
69
|
+
const size2 = Math.min(this.#buffer.available, into.length);
|
|
70
|
+
into.set(this.#buffer.readSync(size2));
|
|
71
|
+
this.#buffer.reclaim();
|
|
72
|
+
return size2;
|
|
73
|
+
}
|
|
74
|
+
if (this._error !== null) throw this._error;
|
|
75
|
+
await this._cv.wait();
|
|
76
|
+
if (this._error !== null) throw this._error;
|
|
77
|
+
const size = Math.min(this.#buffer.available, into.length);
|
|
78
|
+
into.set(this.#buffer.readSync(size));
|
|
79
|
+
this.#buffer.reclaim();
|
|
80
|
+
return size;
|
|
81
|
+
}
|
|
82
|
+
async write(bytes) {
|
|
83
|
+
if (this._error) throw this._error;
|
|
84
|
+
if (!bytes.length) return;
|
|
85
|
+
this.socket.send(bytes);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
class WebSocketConnectionFramed extends WebSocketConnectionBase {
|
|
89
|
+
#buffer = new Deque();
|
|
90
|
+
onMessage(event) {
|
|
91
|
+
if (typeof event.data === "string") {
|
|
92
|
+
this.#buffer.pushBack(event.data);
|
|
93
|
+
} else {
|
|
94
|
+
this.#buffer.pushBack(new Uint8Array(event.data));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
async readFrame() {
|
|
98
|
+
if (!this.#buffer.isEmpty()) {
|
|
99
|
+
return this.#buffer.popFront();
|
|
100
|
+
}
|
|
101
|
+
if (this._error !== null) throw this._error;
|
|
102
|
+
await this._cv.wait();
|
|
103
|
+
if (this._error !== null) throw this._error;
|
|
104
|
+
return this.#buffer.popFront();
|
|
105
|
+
}
|
|
106
|
+
async writeFrame(data) {
|
|
107
|
+
if (this._error) throw this._error;
|
|
108
|
+
this.socket.send(data);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const connectWs = (endpoint) => {
|
|
112
|
+
const {
|
|
113
|
+
url,
|
|
114
|
+
implementation: WebSocketImpl = WebSocket,
|
|
115
|
+
protocols
|
|
116
|
+
} = endpoint;
|
|
117
|
+
return new Promise((resolve, reject) => {
|
|
118
|
+
const socket = new WebSocketImpl(url, protocols);
|
|
119
|
+
socket.binaryType = "arraybuffer";
|
|
120
|
+
const onError = (event) => {
|
|
121
|
+
socket.removeEventListener("error", onError);
|
|
122
|
+
reject(eventToError(event));
|
|
123
|
+
};
|
|
124
|
+
socket.addEventListener("error", onError);
|
|
125
|
+
socket.addEventListener("open", () => {
|
|
126
|
+
socket.removeEventListener("error", onError);
|
|
127
|
+
resolve(new WebSocketConnection(socket));
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
};
|
|
131
|
+
async function connectWsFramed(endpoint) {
|
|
132
|
+
const {
|
|
133
|
+
url,
|
|
134
|
+
implementation: WebSocketImpl = WebSocket,
|
|
135
|
+
protocols
|
|
136
|
+
} = endpoint;
|
|
137
|
+
return new Promise((resolve, reject) => {
|
|
138
|
+
const socket = new WebSocketImpl(url, protocols);
|
|
139
|
+
socket.binaryType = "arraybuffer";
|
|
140
|
+
const onError = (event) => {
|
|
141
|
+
socket.removeEventListener("error", onError);
|
|
142
|
+
reject(eventToError(event));
|
|
143
|
+
};
|
|
144
|
+
socket.addEventListener("error", onError);
|
|
145
|
+
socket.addEventListener("open", () => {
|
|
146
|
+
socket.removeEventListener("error", onError);
|
|
147
|
+
resolve(new WebSocketConnectionFramed(socket));
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
export {
|
|
152
|
+
WebSocketConnection,
|
|
153
|
+
WebSocketConnectionClosedError,
|
|
154
|
+
WebSocketConnectionFramed,
|
|
155
|
+
connectWs,
|
|
156
|
+
connectWsFramed
|
|
157
|
+
};
|