@forgeax/engine-net-websocket 0.1.3 → 0.1.6
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 +131 -40
- package/dist/.tsbuildinfo +1 -1
- package/dist/browser.d.ts +14 -1
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.mjs +57 -10
- package/dist/browser.mjs.map +1 -1
- package/dist/node.d.ts +16 -0
- package/dist/node.d.ts.map +1 -1
- package/dist/node.mjs +58 -11
- package/dist/node.mjs.map +1 -1
- package/dist/websocket-client-core.d.ts +3 -1
- package/dist/websocket-client-core.d.ts.map +1 -1
- package/dist/websocket-connector.d.ts +11 -0
- package/dist/websocket-connector.d.ts.map +1 -0
- package/package.json +3 -3
- package/src/browser.ts +29 -7
- package/src/node.ts +31 -8
- package/src/websocket-client-core.ts +38 -4
- package/src/websocket-connector.ts +35 -0
package/src/node.ts
CHANGED
|
@@ -9,7 +9,8 @@ import {
|
|
|
9
9
|
import { err, ok, type Result } from '@forgeax/engine-types';
|
|
10
10
|
import WebSocket, { WebSocketServer } from 'ws';
|
|
11
11
|
import { BoundedEventQueue, DEFAULT_MAX_QUEUED_EVENTS } from './event-queue';
|
|
12
|
-
import {
|
|
12
|
+
import type { WebSocketConstructor } from './websocket-client-core';
|
|
13
|
+
import { createWebSocketConnectorAdapter } from './websocket-connector';
|
|
13
14
|
|
|
14
15
|
export interface ListenWebSocketEndpointOptions {
|
|
15
16
|
readonly port: number;
|
|
@@ -22,20 +23,42 @@ export interface ConnectWebSocketClientEndpointOptions {
|
|
|
22
23
|
readonly maxQueuedEvents?: number | undefined;
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Creates the Node WebSocket adapter for the public NetEndpointConnector.
|
|
28
|
+
* Each connect call accepts an AbortSignal and creates one replacement-capable
|
|
29
|
+
* NetEndpoint. Transport lifecycle and EndpointError results stay here;
|
|
30
|
+
* authoritative resync and replication policy stay with NetSession.
|
|
31
|
+
*/
|
|
32
|
+
export function createWebSocketConnector(
|
|
26
33
|
url: string,
|
|
27
34
|
options: ConnectWebSocketClientEndpointOptions = {},
|
|
28
|
-
):
|
|
29
|
-
return
|
|
30
|
-
WebSocket as unknown as import('./websocket-client-core').WebSocketConstructor,
|
|
35
|
+
): import('@forgeax/engine-net').NetEndpointConnector {
|
|
36
|
+
return createWebSocketConnectorAdapter(
|
|
31
37
|
{
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
toBytes: toBytes,
|
|
38
|
+
WebSocket: WebSocket as unknown as WebSocketConstructor,
|
|
39
|
+
toBytes,
|
|
35
40
|
},
|
|
41
|
+
url,
|
|
42
|
+
options,
|
|
36
43
|
);
|
|
37
44
|
}
|
|
38
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Connects one Node WebSocket with the default one-shot AbortSignal.
|
|
48
|
+
* Use createWebSocketConnector when the caller must cancel or replace an
|
|
49
|
+
* endpoint through an explicit signal.
|
|
50
|
+
*/
|
|
51
|
+
export function connectWebSocketClientEndpoint(
|
|
52
|
+
url: string,
|
|
53
|
+
options: ConnectWebSocketClientEndpointOptions = {},
|
|
54
|
+
): Promise<Result<NetEndpoint, EndpointError>> {
|
|
55
|
+
return createWebSocketConnector(url, options).connect(new AbortController().signal);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Starts a Node WebSocket listener that exposes NetEndpoint peer events and
|
|
60
|
+
* binary messages without owning NetSession or replication policy.
|
|
61
|
+
*/
|
|
39
62
|
export function listenWebSocketEndpoint(
|
|
40
63
|
options: ListenWebSocketEndpointOptions,
|
|
41
64
|
): Promise<Result<NetEndpoint, EndpointError>> {
|
|
@@ -31,6 +31,8 @@ export interface WebSocketConstructor {
|
|
|
31
31
|
export interface WebSocketClientCoreOptions {
|
|
32
32
|
readonly url: string;
|
|
33
33
|
readonly maxQueuedEvents?: number | undefined;
|
|
34
|
+
readonly peerId?: PeerId;
|
|
35
|
+
readonly signal?: AbortSignal;
|
|
34
36
|
readonly toBytes: (data: unknown) => Uint8Array | Promise<Uint8Array | undefined> | undefined;
|
|
35
37
|
}
|
|
36
38
|
|
|
@@ -41,6 +43,13 @@ export function createWebSocketClientEndpoint(
|
|
|
41
43
|
options: WebSocketClientCoreOptions,
|
|
42
44
|
): Promise<Result<NetEndpoint, EndpointError>> {
|
|
43
45
|
return new Promise((resolve) => {
|
|
46
|
+
const clientPeerId = options.peerId ?? CLIENT_PEER_ID;
|
|
47
|
+
const signal = options.signal ?? new AbortController().signal;
|
|
48
|
+
if (signal.aborted) {
|
|
49
|
+
resolve(connectionFailed(options.url, 'WebSocket connection aborted.'));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
44
53
|
let queue: BoundedEventQueue;
|
|
45
54
|
try {
|
|
46
55
|
queue = new BoundedEventQueue(options.maxQueuedEvents ?? DEFAULT_MAX_QUEUED_EVENTS);
|
|
@@ -65,18 +74,40 @@ export function createWebSocketClientEndpoint(
|
|
|
65
74
|
let locallyClosed = false;
|
|
66
75
|
let messageTail = Promise.resolve();
|
|
67
76
|
|
|
77
|
+
const removeAbortListener = (): void => {
|
|
78
|
+
signal.removeEventListener('abort', abortPendingConnection);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const abortPendingConnection = (): void => {
|
|
82
|
+
if (opened || settled) return;
|
|
83
|
+
settled = true;
|
|
84
|
+
try {
|
|
85
|
+
socket.close();
|
|
86
|
+
} catch {
|
|
87
|
+
// Closing a partially opened platform socket is best effort.
|
|
88
|
+
}
|
|
89
|
+
removeAbortListener();
|
|
90
|
+
resolve(connectionFailed(options.url, 'WebSocket connection aborted.'));
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
signal.addEventListener('abort', abortPendingConnection, { once: true });
|
|
94
|
+
if (signal.aborted) {
|
|
95
|
+
abortPendingConnection();
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
68
99
|
const disconnect = (reason: string): void => {
|
|
69
100
|
if (closed) return;
|
|
70
101
|
closed = true;
|
|
71
102
|
queue.close(reason);
|
|
72
|
-
terminalEvents.push({ kind: 'peer-disconnected', peerId:
|
|
103
|
+
terminalEvents.push({ kind: 'peer-disconnected', peerId: clientPeerId });
|
|
73
104
|
};
|
|
74
105
|
|
|
75
106
|
const endpoint: NetEndpoint = {
|
|
76
107
|
poll: () => [...queue.drain(), ...terminalEvents.splice(0)],
|
|
77
108
|
send: (peerId, data) => {
|
|
78
109
|
if (closed) return locallyClosed ? alreadyClosed() : connectionClosed(peerId);
|
|
79
|
-
if (peerId !==
|
|
110
|
+
if (peerId !== clientPeerId)
|
|
80
111
|
return err(
|
|
81
112
|
new EndpointError({
|
|
82
113
|
code: 'peer-not-found',
|
|
@@ -127,9 +158,10 @@ export function createWebSocketClientEndpoint(
|
|
|
127
158
|
|
|
128
159
|
socket.onopen = () => {
|
|
129
160
|
if (settled) return;
|
|
161
|
+
removeAbortListener();
|
|
130
162
|
opened = true;
|
|
131
163
|
settled = true;
|
|
132
|
-
queue.enqueue({ kind: 'peer-connected', peerId:
|
|
164
|
+
queue.enqueue({ kind: 'peer-connected', peerId: clientPeerId });
|
|
133
165
|
resolve(ok(endpoint));
|
|
134
166
|
};
|
|
135
167
|
socket.onmessage = ({ data }) => {
|
|
@@ -139,7 +171,7 @@ export function createWebSocketClientEndpoint(
|
|
|
139
171
|
.then(async () => {
|
|
140
172
|
const bytes = await options.toBytes(data);
|
|
141
173
|
if (!bytes || closed) return;
|
|
142
|
-
if (!queue.enqueue({ kind: 'message', peerId:
|
|
174
|
+
if (!queue.enqueue({ kind: 'message', peerId: clientPeerId, data: bytes })) {
|
|
143
175
|
socket.close();
|
|
144
176
|
}
|
|
145
177
|
})
|
|
@@ -149,12 +181,14 @@ export function createWebSocketClientEndpoint(
|
|
|
149
181
|
if (opened) disconnect(`WebSocket error: ${normalizeCause(cause)}`);
|
|
150
182
|
else if (!settled) {
|
|
151
183
|
settled = true;
|
|
184
|
+
removeAbortListener();
|
|
152
185
|
resolve(connectionFailed(options.url, cause));
|
|
153
186
|
}
|
|
154
187
|
};
|
|
155
188
|
socket.onclose = (cause) => {
|
|
156
189
|
if (!opened && !settled) {
|
|
157
190
|
settled = true;
|
|
191
|
+
removeAbortListener();
|
|
158
192
|
resolve(connectionFailed(options.url, cause));
|
|
159
193
|
return;
|
|
160
194
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { NetEndpointConnector, PeerId } from '@forgeax/engine-net';
|
|
2
|
+
import {
|
|
3
|
+
createWebSocketClientEndpoint,
|
|
4
|
+
type WebSocketClientCoreOptions,
|
|
5
|
+
type WebSocketConstructor,
|
|
6
|
+
} from './websocket-client-core';
|
|
7
|
+
|
|
8
|
+
export interface WebSocketConnectorOptions {
|
|
9
|
+
readonly maxQueuedEvents?: number | undefined;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface WebSocketConnectorRuntime {
|
|
13
|
+
readonly WebSocket: WebSocketConstructor;
|
|
14
|
+
readonly toBytes: WebSocketClientCoreOptions['toBytes'];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function createWebSocketConnectorAdapter(
|
|
18
|
+
runtime: WebSocketConnectorRuntime,
|
|
19
|
+
url: string,
|
|
20
|
+
options: WebSocketConnectorOptions = {},
|
|
21
|
+
): NetEndpointConnector {
|
|
22
|
+
let nextPeerId = 1;
|
|
23
|
+
return {
|
|
24
|
+
connect: (signal) => {
|
|
25
|
+
const peerId = nextPeerId++ as PeerId;
|
|
26
|
+
return createWebSocketClientEndpoint(runtime.WebSocket, {
|
|
27
|
+
url,
|
|
28
|
+
maxQueuedEvents: options.maxQueuedEvents,
|
|
29
|
+
peerId,
|
|
30
|
+
signal,
|
|
31
|
+
toBytes: runtime.toBytes,
|
|
32
|
+
});
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|