@forgeax/engine-net-websocket 0.1.4 → 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/dist/browser.mjs
CHANGED
|
@@ -45,6 +45,12 @@ var BoundedEventQueue = class {
|
|
|
45
45
|
var CLIENT_PEER_ID = 1;
|
|
46
46
|
function createWebSocketClientEndpoint(WebSocket2, options) {
|
|
47
47
|
return new Promise((resolve) => {
|
|
48
|
+
const clientPeerId = options.peerId ?? CLIENT_PEER_ID;
|
|
49
|
+
const signal = options.signal ?? new AbortController().signal;
|
|
50
|
+
if (signal.aborted) {
|
|
51
|
+
resolve(connectionFailed(options.url, "WebSocket connection aborted."));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
48
54
|
let queue;
|
|
49
55
|
try {
|
|
50
56
|
queue = new BoundedEventQueue(options.maxQueuedEvents ?? DEFAULT_MAX_QUEUED_EVENTS);
|
|
@@ -66,17 +72,35 @@ function createWebSocketClientEndpoint(WebSocket2, options) {
|
|
|
66
72
|
let closed = false;
|
|
67
73
|
let locallyClosed = false;
|
|
68
74
|
let messageTail = Promise.resolve();
|
|
75
|
+
const removeAbortListener = () => {
|
|
76
|
+
signal.removeEventListener("abort", abortPendingConnection);
|
|
77
|
+
};
|
|
78
|
+
const abortPendingConnection = () => {
|
|
79
|
+
if (opened || settled) return;
|
|
80
|
+
settled = true;
|
|
81
|
+
try {
|
|
82
|
+
socket.close();
|
|
83
|
+
} catch {
|
|
84
|
+
}
|
|
85
|
+
removeAbortListener();
|
|
86
|
+
resolve(connectionFailed(options.url, "WebSocket connection aborted."));
|
|
87
|
+
};
|
|
88
|
+
signal.addEventListener("abort", abortPendingConnection, { once: true });
|
|
89
|
+
if (signal.aborted) {
|
|
90
|
+
abortPendingConnection();
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
69
93
|
const disconnect = (reason) => {
|
|
70
94
|
if (closed) return;
|
|
71
95
|
closed = true;
|
|
72
96
|
queue.close(reason);
|
|
73
|
-
terminalEvents.push({ kind: "peer-disconnected", peerId:
|
|
97
|
+
terminalEvents.push({ kind: "peer-disconnected", peerId: clientPeerId });
|
|
74
98
|
};
|
|
75
99
|
const endpoint = {
|
|
76
100
|
poll: () => [...queue.drain(), ...terminalEvents.splice(0)],
|
|
77
101
|
send: (peerId, data) => {
|
|
78
102
|
if (closed) return locallyClosed ? alreadyClosed() : connectionClosed(peerId);
|
|
79
|
-
if (peerId !==
|
|
103
|
+
if (peerId !== clientPeerId)
|
|
80
104
|
return err(
|
|
81
105
|
new EndpointError({
|
|
82
106
|
code: "peer-not-found",
|
|
@@ -126,16 +150,17 @@ function createWebSocketClientEndpoint(WebSocket2, options) {
|
|
|
126
150
|
};
|
|
127
151
|
socket.onopen = () => {
|
|
128
152
|
if (settled) return;
|
|
153
|
+
removeAbortListener();
|
|
129
154
|
opened = true;
|
|
130
155
|
settled = true;
|
|
131
|
-
queue.enqueue({ kind: "peer-connected", peerId:
|
|
156
|
+
queue.enqueue({ kind: "peer-connected", peerId: clientPeerId });
|
|
132
157
|
resolve(ok(endpoint));
|
|
133
158
|
};
|
|
134
159
|
socket.onmessage = ({ data }) => {
|
|
135
160
|
messageTail = messageTail.then(async () => {
|
|
136
161
|
const bytes = await options.toBytes(data);
|
|
137
162
|
if (!bytes || closed) return;
|
|
138
|
-
if (!queue.enqueue({ kind: "message", peerId:
|
|
163
|
+
if (!queue.enqueue({ kind: "message", peerId: clientPeerId, data: bytes })) {
|
|
139
164
|
socket.close();
|
|
140
165
|
}
|
|
141
166
|
}).catch(() => void 0);
|
|
@@ -144,12 +169,14 @@ function createWebSocketClientEndpoint(WebSocket2, options) {
|
|
|
144
169
|
if (opened) disconnect(`WebSocket error: ${normalizeCause(cause)}`);
|
|
145
170
|
else if (!settled) {
|
|
146
171
|
settled = true;
|
|
172
|
+
removeAbortListener();
|
|
147
173
|
resolve(connectionFailed(options.url, cause));
|
|
148
174
|
}
|
|
149
175
|
};
|
|
150
176
|
socket.onclose = (cause) => {
|
|
151
177
|
if (!opened && !settled) {
|
|
152
178
|
settled = true;
|
|
179
|
+
removeAbortListener();
|
|
153
180
|
resolve(connectionFailed(options.url, cause));
|
|
154
181
|
return;
|
|
155
182
|
}
|
|
@@ -193,13 +220,33 @@ function normalizeCause(cause) {
|
|
|
193
220
|
return "WebSocket operation failed without a platform error message.";
|
|
194
221
|
}
|
|
195
222
|
|
|
223
|
+
// src/websocket-connector.ts
|
|
224
|
+
function createWebSocketConnectorAdapter(runtime, url, options = {}) {
|
|
225
|
+
let nextPeerId = 1;
|
|
226
|
+
return {
|
|
227
|
+
connect: (signal) => {
|
|
228
|
+
const peerId = nextPeerId++;
|
|
229
|
+
return createWebSocketClientEndpoint(runtime.WebSocket, {
|
|
230
|
+
url,
|
|
231
|
+
maxQueuedEvents: options.maxQueuedEvents,
|
|
232
|
+
peerId,
|
|
233
|
+
signal,
|
|
234
|
+
toBytes: runtime.toBytes
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
196
240
|
// src/browser.ts
|
|
197
|
-
function
|
|
198
|
-
return
|
|
241
|
+
function createWebSocketConnector(url, options = {}) {
|
|
242
|
+
return createWebSocketConnectorAdapter(
|
|
243
|
+
{ WebSocket, toBytes },
|
|
199
244
|
url,
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
245
|
+
options
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
function connectWebSocketClientEndpoint(url, options = {}) {
|
|
249
|
+
return createWebSocketConnector(url, options).connect(new AbortController().signal);
|
|
203
250
|
}
|
|
204
251
|
function toBytes(data) {
|
|
205
252
|
if (data instanceof ArrayBuffer) return new Uint8Array(data);
|
|
@@ -209,6 +256,6 @@ function toBytes(data) {
|
|
|
209
256
|
return void 0;
|
|
210
257
|
}
|
|
211
258
|
|
|
212
|
-
export { connectWebSocketClientEndpoint };
|
|
259
|
+
export { connectWebSocketClientEndpoint, createWebSocketConnector };
|
|
213
260
|
//# sourceMappingURL=browser.mjs.map
|
|
214
261
|
//# sourceMappingURL=browser.mjs.map
|
package/dist/browser.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/event-queue.ts","../src/websocket-client-core.ts","../src/browser.ts"],"names":["WebSocket"],"mappings":"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","file":"browser.mjs","sourcesContent":["import type { EndpointEvent } from '@forgeax/engine-net';\n\nexport const DEFAULT_MAX_QUEUED_EVENTS = 1024;\n\nexport class BoundedEventQueue {\n readonly #events: EndpointEvent[] = [];\n #closed = false;\n #disconnectReason: string | undefined;\n\n constructor(readonly maxQueuedEvents: number) {\n if (!Number.isInteger(maxQueuedEvents) || maxQueuedEvents < 1) {\n throw new RangeError('maxQueuedEvents must be a positive integer');\n }\n }\n\n get closed(): boolean {\n return this.#closed;\n }\n\n get disconnectReason(): string | undefined {\n return this.#disconnectReason;\n }\n\n enqueue(event: EndpointEvent): boolean {\n if (this.#closed) return false;\n if (this.#events.length === this.maxQueuedEvents) {\n this.close(`event queue overflow (maxQueuedEvents=${this.maxQueuedEvents})`);\n return false;\n }\n this.#events.push(event);\n return true;\n }\n\n close(reason: string): void {\n if (this.#closed) return;\n this.#closed = true;\n this.#disconnectReason = reason;\n }\n\n drain(): EndpointEvent[] {\n return this.#events.splice(0);\n }\n}\n","import {\n ENDPOINT_ERROR_HINTS,\n ENDPOINT_EXPECTED,\n EndpointError,\n type EndpointEvent,\n type NetEndpoint,\n type PeerId,\n} from '@forgeax/engine-net';\nimport { err, ok, type Result } from '@forgeax/engine-types';\nimport { BoundedEventQueue, DEFAULT_MAX_QUEUED_EVENTS } from './event-queue';\n\nexport interface WebSocketLike {\n readonly CONNECTING: number;\n readonly OPEN: number;\n readonly CLOSING: number;\n readonly CLOSED: number;\n readonly readyState: number;\n binaryType?: string;\n onopen: ((event: unknown) => void) | null;\n onmessage: ((event: { data: unknown }) => void) | null;\n onerror: ((event: unknown) => void) | null;\n onclose: ((event: unknown) => void) | null;\n send(data: Uint8Array): void;\n close(): void;\n}\n\nexport interface WebSocketConstructor {\n new (url: string): WebSocketLike;\n}\n\nexport interface WebSocketClientCoreOptions {\n readonly url: string;\n readonly maxQueuedEvents?: number | undefined;\n readonly toBytes: (data: unknown) => Uint8Array | Promise<Uint8Array | undefined> | undefined;\n}\n\nconst CLIENT_PEER_ID = 1 as PeerId;\n\nexport function createWebSocketClientEndpoint(\n WebSocket: WebSocketConstructor,\n options: WebSocketClientCoreOptions,\n): Promise<Result<NetEndpoint, EndpointError>> {\n return new Promise((resolve) => {\n let queue: BoundedEventQueue;\n try {\n queue = new BoundedEventQueue(options.maxQueuedEvents ?? DEFAULT_MAX_QUEUED_EVENTS);\n } catch (cause) {\n resolve(connectionFailed(options.url, cause));\n return;\n }\n\n let socket: WebSocketLike;\n try {\n socket = new WebSocket(options.url);\n socket.binaryType = 'arraybuffer';\n } catch (cause) {\n resolve(connectionFailed(options.url, cause));\n return;\n }\n\n const terminalEvents: EndpointEvent[] = [];\n let opened = false;\n let settled = false;\n let closed = false;\n let locallyClosed = false;\n let messageTail = Promise.resolve();\n\n const disconnect = (reason: string): void => {\n if (closed) return;\n closed = true;\n queue.close(reason);\n terminalEvents.push({ kind: 'peer-disconnected', peerId: CLIENT_PEER_ID });\n };\n\n const endpoint: NetEndpoint = {\n poll: () => [...queue.drain(), ...terminalEvents.splice(0)],\n send: (peerId, data) => {\n if (closed) return locallyClosed ? alreadyClosed() : connectionClosed(peerId);\n if (peerId !== CLIENT_PEER_ID)\n return err(\n new EndpointError({\n code: 'peer-not-found',\n expected: ENDPOINT_EXPECTED['peer-not-found'],\n hint: ENDPOINT_ERROR_HINTS['peer-not-found'],\n detail: { peerId },\n }),\n );\n if (socket.readyState !== socket.OPEN)\n return err(\n new EndpointError({\n code: 'connection-closed',\n expected: ENDPOINT_EXPECTED['connection-closed'],\n hint: ENDPOINT_ERROR_HINTS['connection-closed'],\n detail: { peerId },\n }),\n );\n try {\n socket.send(data);\n return ok(undefined);\n } catch (cause) {\n return err(\n new EndpointError({\n code: 'send-failed',\n expected: ENDPOINT_EXPECTED['send-failed'],\n hint: ENDPOINT_ERROR_HINTS['send-failed'],\n detail: { peerId, cause: normalizeCause(cause) },\n }),\n );\n }\n },\n close: () => {\n if (closed)\n return err(\n new EndpointError({\n code: 'already-closed',\n expected: ENDPOINT_EXPECTED['already-closed'],\n hint: ENDPOINT_ERROR_HINTS['already-closed'],\n detail: { cause: 'The WebSocket endpoint is already closed.' },\n }),\n );\n locallyClosed = true;\n disconnect('Endpoint close requested.');\n socket.close();\n return ok(undefined);\n },\n };\n\n socket.onopen = () => {\n if (settled) return;\n opened = true;\n settled = true;\n queue.enqueue({ kind: 'peer-connected', peerId: CLIENT_PEER_ID });\n resolve(ok(endpoint));\n };\n socket.onmessage = ({ data }) => {\n // Blob.arrayBuffer() is asynchronous. Serialize conversion so that\n // ordered WebSocket messages retain their wire order after decoding.\n messageTail = messageTail\n .then(async () => {\n const bytes = await options.toBytes(data);\n if (!bytes || closed) return;\n if (!queue.enqueue({ kind: 'message', peerId: CLIENT_PEER_ID, data: bytes })) {\n socket.close();\n }\n })\n .catch(() => undefined);\n };\n socket.onerror = (cause) => {\n if (opened) disconnect(`WebSocket error: ${normalizeCause(cause)}`);\n else if (!settled) {\n settled = true;\n resolve(connectionFailed(options.url, cause));\n }\n };\n socket.onclose = (cause) => {\n if (!opened && !settled) {\n settled = true;\n resolve(connectionFailed(options.url, cause));\n return;\n }\n disconnect(`WebSocket closed: ${normalizeCause(cause)}`);\n };\n });\n}\n\nfunction connectionFailed(address: string, cause: unknown): Result<never, EndpointError> {\n return err(\n new EndpointError({\n code: 'connection-failed',\n expected: ENDPOINT_EXPECTED['connection-failed'],\n hint: ENDPOINT_ERROR_HINTS['connection-failed'],\n detail: { address, cause: normalizeCause(cause) },\n }),\n );\n}\n\nfunction alreadyClosed(): Result<never, EndpointError> {\n return err(\n new EndpointError({\n code: 'already-closed',\n expected: ENDPOINT_EXPECTED['already-closed'],\n hint: ENDPOINT_ERROR_HINTS['already-closed'],\n detail: { cause: 'The WebSocket endpoint is closed.' },\n }),\n );\n}\n\nfunction connectionClosed(peerId: PeerId): Result<never, EndpointError> {\n return err(\n new EndpointError({\n code: 'connection-closed',\n expected: ENDPOINT_EXPECTED['connection-closed'],\n hint: ENDPOINT_ERROR_HINTS['connection-closed'],\n detail: { peerId },\n }),\n );\n}\n\nfunction normalizeCause(cause: unknown): string {\n if (cause instanceof Error) return cause.message;\n if (typeof cause === 'string') return cause;\n return 'WebSocket operation failed without a platform error message.';\n}\n","import type { EndpointError, NetEndpoint } from '@forgeax/engine-net';\nimport type { Result } from '@forgeax/engine-types';\nimport { createWebSocketClientEndpoint, type WebSocketConstructor } from './websocket-client-core';\n\nexport interface ConnectWebSocketClientEndpointOptions {\n readonly maxQueuedEvents?: number | undefined;\n}\n\nexport function connectWebSocketClientEndpoint(\n url: string,\n options: ConnectWebSocketClientEndpointOptions = {},\n): Promise<Result<NetEndpoint, EndpointError>> {\n return createWebSocketClientEndpoint(WebSocket as unknown as WebSocketConstructor, {\n url,\n maxQueuedEvents: options.maxQueuedEvents,\n toBytes,\n });\n}\n\nfunction toBytes(data: unknown): Uint8Array | Promise<Uint8Array | undefined> | undefined {\n if (data instanceof ArrayBuffer) return new Uint8Array(data);\n if (typeof Blob !== 'undefined' && data instanceof Blob) {\n return data.arrayBuffer().then((buffer) => new Uint8Array(buffer));\n }\n return undefined;\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/event-queue.ts","../src/websocket-client-core.ts","../src/websocket-connector.ts","../src/browser.ts"],"names":["WebSocket"],"mappings":"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","file":"browser.mjs","sourcesContent":["import type { EndpointEvent } from '@forgeax/engine-net';\n\nexport const DEFAULT_MAX_QUEUED_EVENTS = 1024;\n\nexport class BoundedEventQueue {\n readonly #events: EndpointEvent[] = [];\n #closed = false;\n #disconnectReason: string | undefined;\n\n constructor(readonly maxQueuedEvents: number) {\n if (!Number.isInteger(maxQueuedEvents) || maxQueuedEvents < 1) {\n throw new RangeError('maxQueuedEvents must be a positive integer');\n }\n }\n\n get closed(): boolean {\n return this.#closed;\n }\n\n get disconnectReason(): string | undefined {\n return this.#disconnectReason;\n }\n\n enqueue(event: EndpointEvent): boolean {\n if (this.#closed) return false;\n if (this.#events.length === this.maxQueuedEvents) {\n this.close(`event queue overflow (maxQueuedEvents=${this.maxQueuedEvents})`);\n return false;\n }\n this.#events.push(event);\n return true;\n }\n\n close(reason: string): void {\n if (this.#closed) return;\n this.#closed = true;\n this.#disconnectReason = reason;\n }\n\n drain(): EndpointEvent[] {\n return this.#events.splice(0);\n }\n}\n","import {\n ENDPOINT_ERROR_HINTS,\n ENDPOINT_EXPECTED,\n EndpointError,\n type EndpointEvent,\n type NetEndpoint,\n type PeerId,\n} from '@forgeax/engine-net';\nimport { err, ok, type Result } from '@forgeax/engine-types';\nimport { BoundedEventQueue, DEFAULT_MAX_QUEUED_EVENTS } from './event-queue';\n\nexport interface WebSocketLike {\n readonly CONNECTING: number;\n readonly OPEN: number;\n readonly CLOSING: number;\n readonly CLOSED: number;\n readonly readyState: number;\n binaryType?: string;\n onopen: ((event: unknown) => void) | null;\n onmessage: ((event: { data: unknown }) => void) | null;\n onerror: ((event: unknown) => void) | null;\n onclose: ((event: unknown) => void) | null;\n send(data: Uint8Array): void;\n close(): void;\n}\n\nexport interface WebSocketConstructor {\n new (url: string): WebSocketLike;\n}\n\nexport interface WebSocketClientCoreOptions {\n readonly url: string;\n readonly maxQueuedEvents?: number | undefined;\n readonly peerId?: PeerId;\n readonly signal?: AbortSignal;\n readonly toBytes: (data: unknown) => Uint8Array | Promise<Uint8Array | undefined> | undefined;\n}\n\nconst CLIENT_PEER_ID = 1 as PeerId;\n\nexport function createWebSocketClientEndpoint(\n WebSocket: WebSocketConstructor,\n options: WebSocketClientCoreOptions,\n): Promise<Result<NetEndpoint, EndpointError>> {\n return new Promise((resolve) => {\n const clientPeerId = options.peerId ?? CLIENT_PEER_ID;\n const signal = options.signal ?? new AbortController().signal;\n if (signal.aborted) {\n resolve(connectionFailed(options.url, 'WebSocket connection aborted.'));\n return;\n }\n\n let queue: BoundedEventQueue;\n try {\n queue = new BoundedEventQueue(options.maxQueuedEvents ?? DEFAULT_MAX_QUEUED_EVENTS);\n } catch (cause) {\n resolve(connectionFailed(options.url, cause));\n return;\n }\n\n let socket: WebSocketLike;\n try {\n socket = new WebSocket(options.url);\n socket.binaryType = 'arraybuffer';\n } catch (cause) {\n resolve(connectionFailed(options.url, cause));\n return;\n }\n\n const terminalEvents: EndpointEvent[] = [];\n let opened = false;\n let settled = false;\n let closed = false;\n let locallyClosed = false;\n let messageTail = Promise.resolve();\n\n const removeAbortListener = (): void => {\n signal.removeEventListener('abort', abortPendingConnection);\n };\n\n const abortPendingConnection = (): void => {\n if (opened || settled) return;\n settled = true;\n try {\n socket.close();\n } catch {\n // Closing a partially opened platform socket is best effort.\n }\n removeAbortListener();\n resolve(connectionFailed(options.url, 'WebSocket connection aborted.'));\n };\n\n signal.addEventListener('abort', abortPendingConnection, { once: true });\n if (signal.aborted) {\n abortPendingConnection();\n return;\n }\n\n const disconnect = (reason: string): void => {\n if (closed) return;\n closed = true;\n queue.close(reason);\n terminalEvents.push({ kind: 'peer-disconnected', peerId: clientPeerId });\n };\n\n const endpoint: NetEndpoint = {\n poll: () => [...queue.drain(), ...terminalEvents.splice(0)],\n send: (peerId, data) => {\n if (closed) return locallyClosed ? alreadyClosed() : connectionClosed(peerId);\n if (peerId !== clientPeerId)\n return err(\n new EndpointError({\n code: 'peer-not-found',\n expected: ENDPOINT_EXPECTED['peer-not-found'],\n hint: ENDPOINT_ERROR_HINTS['peer-not-found'],\n detail: { peerId },\n }),\n );\n if (socket.readyState !== socket.OPEN)\n return err(\n new EndpointError({\n code: 'connection-closed',\n expected: ENDPOINT_EXPECTED['connection-closed'],\n hint: ENDPOINT_ERROR_HINTS['connection-closed'],\n detail: { peerId },\n }),\n );\n try {\n socket.send(data);\n return ok(undefined);\n } catch (cause) {\n return err(\n new EndpointError({\n code: 'send-failed',\n expected: ENDPOINT_EXPECTED['send-failed'],\n hint: ENDPOINT_ERROR_HINTS['send-failed'],\n detail: { peerId, cause: normalizeCause(cause) },\n }),\n );\n }\n },\n close: () => {\n if (closed)\n return err(\n new EndpointError({\n code: 'already-closed',\n expected: ENDPOINT_EXPECTED['already-closed'],\n hint: ENDPOINT_ERROR_HINTS['already-closed'],\n detail: { cause: 'The WebSocket endpoint is already closed.' },\n }),\n );\n locallyClosed = true;\n disconnect('Endpoint close requested.');\n socket.close();\n return ok(undefined);\n },\n };\n\n socket.onopen = () => {\n if (settled) return;\n removeAbortListener();\n opened = true;\n settled = true;\n queue.enqueue({ kind: 'peer-connected', peerId: clientPeerId });\n resolve(ok(endpoint));\n };\n socket.onmessage = ({ data }) => {\n // Blob.arrayBuffer() is asynchronous. Serialize conversion so that\n // ordered WebSocket messages retain their wire order after decoding.\n messageTail = messageTail\n .then(async () => {\n const bytes = await options.toBytes(data);\n if (!bytes || closed) return;\n if (!queue.enqueue({ kind: 'message', peerId: clientPeerId, data: bytes })) {\n socket.close();\n }\n })\n .catch(() => undefined);\n };\n socket.onerror = (cause) => {\n if (opened) disconnect(`WebSocket error: ${normalizeCause(cause)}`);\n else if (!settled) {\n settled = true;\n removeAbortListener();\n resolve(connectionFailed(options.url, cause));\n }\n };\n socket.onclose = (cause) => {\n if (!opened && !settled) {\n settled = true;\n removeAbortListener();\n resolve(connectionFailed(options.url, cause));\n return;\n }\n disconnect(`WebSocket closed: ${normalizeCause(cause)}`);\n };\n });\n}\n\nfunction connectionFailed(address: string, cause: unknown): Result<never, EndpointError> {\n return err(\n new EndpointError({\n code: 'connection-failed',\n expected: ENDPOINT_EXPECTED['connection-failed'],\n hint: ENDPOINT_ERROR_HINTS['connection-failed'],\n detail: { address, cause: normalizeCause(cause) },\n }),\n );\n}\n\nfunction alreadyClosed(): Result<never, EndpointError> {\n return err(\n new EndpointError({\n code: 'already-closed',\n expected: ENDPOINT_EXPECTED['already-closed'],\n hint: ENDPOINT_ERROR_HINTS['already-closed'],\n detail: { cause: 'The WebSocket endpoint is closed.' },\n }),\n );\n}\n\nfunction connectionClosed(peerId: PeerId): Result<never, EndpointError> {\n return err(\n new EndpointError({\n code: 'connection-closed',\n expected: ENDPOINT_EXPECTED['connection-closed'],\n hint: ENDPOINT_ERROR_HINTS['connection-closed'],\n detail: { peerId },\n }),\n );\n}\n\nfunction normalizeCause(cause: unknown): string {\n if (cause instanceof Error) return cause.message;\n if (typeof cause === 'string') return cause;\n return 'WebSocket operation failed without a platform error message.';\n}\n","import type { NetEndpointConnector, PeerId } from '@forgeax/engine-net';\nimport {\n createWebSocketClientEndpoint,\n type WebSocketClientCoreOptions,\n type WebSocketConstructor,\n} from './websocket-client-core';\n\nexport interface WebSocketConnectorOptions {\n readonly maxQueuedEvents?: number | undefined;\n}\n\nexport interface WebSocketConnectorRuntime {\n readonly WebSocket: WebSocketConstructor;\n readonly toBytes: WebSocketClientCoreOptions['toBytes'];\n}\n\nexport function createWebSocketConnectorAdapter(\n runtime: WebSocketConnectorRuntime,\n url: string,\n options: WebSocketConnectorOptions = {},\n): NetEndpointConnector {\n let nextPeerId = 1;\n return {\n connect: (signal) => {\n const peerId = nextPeerId++ as PeerId;\n return createWebSocketClientEndpoint(runtime.WebSocket, {\n url,\n maxQueuedEvents: options.maxQueuedEvents,\n peerId,\n signal,\n toBytes: runtime.toBytes,\n });\n },\n };\n}\n","import type { EndpointError, NetEndpoint, NetEndpointConnector } from '@forgeax/engine-net';\nimport type { Result } from '@forgeax/engine-types';\nimport type { WebSocketConstructor } from './websocket-client-core';\nimport {\n createWebSocketConnectorAdapter,\n type WebSocketConnectorOptions,\n} from './websocket-connector';\n\nexport interface ConnectWebSocketClientEndpointOptions {\n readonly maxQueuedEvents?: number | undefined;\n}\n\n/**\n * Creates the browser WebSocket adapter for the public NetEndpointConnector.\n * Each connect call accepts an AbortSignal and creates one replacement-capable\n * NetEndpoint. Transport lifecycle and EndpointError results stay here;\n * authoritative resync and replication policy stay with NetSession.\n */\nexport function createWebSocketConnector(\n url: string,\n options: WebSocketConnectorOptions = {},\n): NetEndpointConnector {\n return createWebSocketConnectorAdapter(\n { WebSocket: WebSocket as unknown as WebSocketConstructor, toBytes },\n url,\n options,\n );\n}\n\n/**\n * Connects one browser WebSocket with the default one-shot AbortSignal.\n * Use createWebSocketConnector when the caller must cancel or replace an\n * endpoint through an explicit signal.\n */\nexport function connectWebSocketClientEndpoint(\n url: string,\n options: ConnectWebSocketClientEndpointOptions = {},\n): Promise<Result<NetEndpoint, EndpointError>> {\n return createWebSocketConnector(url, options).connect(new AbortController().signal);\n}\n\nfunction toBytes(data: unknown): Uint8Array | Promise<Uint8Array | undefined> | undefined {\n if (data instanceof ArrayBuffer) return new Uint8Array(data);\n if (typeof Blob !== 'undefined' && data instanceof Blob) {\n return data.arrayBuffer().then((buffer) => new Uint8Array(buffer));\n }\n return undefined;\n}\n"]}
|
package/dist/node.d.ts
CHANGED
|
@@ -9,6 +9,22 @@ export interface ListenWebSocketEndpointOptions {
|
|
|
9
9
|
export interface ConnectWebSocketClientEndpointOptions {
|
|
10
10
|
readonly maxQueuedEvents?: number | undefined;
|
|
11
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* Creates the Node WebSocket adapter for the public NetEndpointConnector.
|
|
14
|
+
* Each connect call accepts an AbortSignal and creates one replacement-capable
|
|
15
|
+
* NetEndpoint. Transport lifecycle and EndpointError results stay here;
|
|
16
|
+
* authoritative resync and replication policy stay with NetSession.
|
|
17
|
+
*/
|
|
18
|
+
export declare function createWebSocketConnector(url: string, options?: ConnectWebSocketClientEndpointOptions): import('@forgeax/engine-net').NetEndpointConnector;
|
|
19
|
+
/**
|
|
20
|
+
* Connects one Node WebSocket with the default one-shot AbortSignal.
|
|
21
|
+
* Use createWebSocketConnector when the caller must cancel or replace an
|
|
22
|
+
* endpoint through an explicit signal.
|
|
23
|
+
*/
|
|
12
24
|
export declare function connectWebSocketClientEndpoint(url: string, options?: ConnectWebSocketClientEndpointOptions): Promise<Result<NetEndpoint, EndpointError>>;
|
|
25
|
+
/**
|
|
26
|
+
* Starts a Node WebSocket listener that exposes NetEndpoint peer events and
|
|
27
|
+
* binary messages without owning NetSession or replication policy.
|
|
28
|
+
*/
|
|
13
29
|
export declare function listenWebSocketEndpoint(options: ListenWebSocketEndpointOptions): Promise<Result<NetEndpoint, EndpointError>>;
|
|
14
30
|
//# sourceMappingURL=node.d.ts.map
|
package/dist/node.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"node.d.ts","sourceRoot":"","sources":["../src/node.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,aAAa,EAEb,KAAK,WAAW,EAEjB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAW,KAAK,MAAM,EAAE,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"node.d.ts","sourceRoot":"","sources":["../src/node.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,aAAa,EAEb,KAAK,WAAW,EAEjB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAW,KAAK,MAAM,EAAE,MAAM,uBAAuB,CAAC;AAM7D,MAAM,WAAW,8BAA8B;IAC7C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC/C;AAED,MAAM,WAAW,qCAAqC;IACpD,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC/C;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CACtC,GAAG,EAAE,MAAM,EACX,OAAO,GAAE,qCAA0C,GAClD,OAAO,qBAAqB,EAAE,oBAAoB,CASpD;AAED;;;;GAIG;AACH,wBAAgB,8BAA8B,CAC5C,GAAG,EAAE,MAAM,EACX,OAAO,GAAE,qCAA0C,GAClD,OAAO,CAAC,MAAM,CAAC,WAAW,EAAE,aAAa,CAAC,CAAC,CAE7C;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,8BAA8B,GACtC,OAAO,CAAC,MAAM,CAAC,WAAW,EAAE,aAAa,CAAC,CAAC,CA6H7C"}
|
package/dist/node.mjs
CHANGED
|
@@ -44,6 +44,12 @@ var BoundedEventQueue = class {
|
|
|
44
44
|
var CLIENT_PEER_ID = 1;
|
|
45
45
|
function createWebSocketClientEndpoint(WebSocket2, options) {
|
|
46
46
|
return new Promise((resolve) => {
|
|
47
|
+
const clientPeerId = options.peerId ?? CLIENT_PEER_ID;
|
|
48
|
+
const signal = options.signal ?? new AbortController().signal;
|
|
49
|
+
if (signal.aborted) {
|
|
50
|
+
resolve(connectionFailed(options.url, "WebSocket connection aborted."));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
47
53
|
let queue;
|
|
48
54
|
try {
|
|
49
55
|
queue = new BoundedEventQueue(options.maxQueuedEvents ?? DEFAULT_MAX_QUEUED_EVENTS);
|
|
@@ -65,17 +71,35 @@ function createWebSocketClientEndpoint(WebSocket2, options) {
|
|
|
65
71
|
let closed = false;
|
|
66
72
|
let locallyClosed = false;
|
|
67
73
|
let messageTail = Promise.resolve();
|
|
74
|
+
const removeAbortListener = () => {
|
|
75
|
+
signal.removeEventListener("abort", abortPendingConnection);
|
|
76
|
+
};
|
|
77
|
+
const abortPendingConnection = () => {
|
|
78
|
+
if (opened || settled) return;
|
|
79
|
+
settled = true;
|
|
80
|
+
try {
|
|
81
|
+
socket.close();
|
|
82
|
+
} catch {
|
|
83
|
+
}
|
|
84
|
+
removeAbortListener();
|
|
85
|
+
resolve(connectionFailed(options.url, "WebSocket connection aborted."));
|
|
86
|
+
};
|
|
87
|
+
signal.addEventListener("abort", abortPendingConnection, { once: true });
|
|
88
|
+
if (signal.aborted) {
|
|
89
|
+
abortPendingConnection();
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
68
92
|
const disconnect = (reason) => {
|
|
69
93
|
if (closed) return;
|
|
70
94
|
closed = true;
|
|
71
95
|
queue.close(reason);
|
|
72
|
-
terminalEvents.push({ kind: "peer-disconnected", peerId:
|
|
96
|
+
terminalEvents.push({ kind: "peer-disconnected", peerId: clientPeerId });
|
|
73
97
|
};
|
|
74
98
|
const endpoint = {
|
|
75
99
|
poll: () => [...queue.drain(), ...terminalEvents.splice(0)],
|
|
76
100
|
send: (peerId, data) => {
|
|
77
101
|
if (closed) return locallyClosed ? alreadyClosed() : connectionClosed(peerId);
|
|
78
|
-
if (peerId !==
|
|
102
|
+
if (peerId !== clientPeerId)
|
|
79
103
|
return err(
|
|
80
104
|
new EndpointError({
|
|
81
105
|
code: "peer-not-found",
|
|
@@ -125,16 +149,17 @@ function createWebSocketClientEndpoint(WebSocket2, options) {
|
|
|
125
149
|
};
|
|
126
150
|
socket.onopen = () => {
|
|
127
151
|
if (settled) return;
|
|
152
|
+
removeAbortListener();
|
|
128
153
|
opened = true;
|
|
129
154
|
settled = true;
|
|
130
|
-
queue.enqueue({ kind: "peer-connected", peerId:
|
|
155
|
+
queue.enqueue({ kind: "peer-connected", peerId: clientPeerId });
|
|
131
156
|
resolve(ok(endpoint));
|
|
132
157
|
};
|
|
133
158
|
socket.onmessage = ({ data }) => {
|
|
134
159
|
messageTail = messageTail.then(async () => {
|
|
135
160
|
const bytes = await options.toBytes(data);
|
|
136
161
|
if (!bytes || closed) return;
|
|
137
|
-
if (!queue.enqueue({ kind: "message", peerId:
|
|
162
|
+
if (!queue.enqueue({ kind: "message", peerId: clientPeerId, data: bytes })) {
|
|
138
163
|
socket.close();
|
|
139
164
|
}
|
|
140
165
|
}).catch(() => void 0);
|
|
@@ -143,12 +168,14 @@ function createWebSocketClientEndpoint(WebSocket2, options) {
|
|
|
143
168
|
if (opened) disconnect(`WebSocket error: ${normalizeCause(cause)}`);
|
|
144
169
|
else if (!settled) {
|
|
145
170
|
settled = true;
|
|
171
|
+
removeAbortListener();
|
|
146
172
|
resolve(connectionFailed(options.url, cause));
|
|
147
173
|
}
|
|
148
174
|
};
|
|
149
175
|
socket.onclose = (cause) => {
|
|
150
176
|
if (!opened && !settled) {
|
|
151
177
|
settled = true;
|
|
178
|
+
removeAbortListener();
|
|
152
179
|
resolve(connectionFailed(options.url, cause));
|
|
153
180
|
return;
|
|
154
181
|
}
|
|
@@ -192,17 +219,37 @@ function normalizeCause(cause) {
|
|
|
192
219
|
return "WebSocket operation failed without a platform error message.";
|
|
193
220
|
}
|
|
194
221
|
|
|
222
|
+
// src/websocket-connector.ts
|
|
223
|
+
function createWebSocketConnectorAdapter(runtime, url, options = {}) {
|
|
224
|
+
let nextPeerId = 1;
|
|
225
|
+
return {
|
|
226
|
+
connect: (signal) => {
|
|
227
|
+
const peerId = nextPeerId++;
|
|
228
|
+
return createWebSocketClientEndpoint(runtime.WebSocket, {
|
|
229
|
+
url,
|
|
230
|
+
maxQueuedEvents: options.maxQueuedEvents,
|
|
231
|
+
peerId,
|
|
232
|
+
signal,
|
|
233
|
+
toBytes: runtime.toBytes
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
195
239
|
// src/node.ts
|
|
196
|
-
function
|
|
197
|
-
return
|
|
198
|
-
WebSocket,
|
|
240
|
+
function createWebSocketConnector(url, options = {}) {
|
|
241
|
+
return createWebSocketConnectorAdapter(
|
|
199
242
|
{
|
|
200
|
-
|
|
201
|
-
maxQueuedEvents: options.maxQueuedEvents,
|
|
243
|
+
WebSocket,
|
|
202
244
|
toBytes
|
|
203
|
-
}
|
|
245
|
+
},
|
|
246
|
+
url,
|
|
247
|
+
options
|
|
204
248
|
);
|
|
205
249
|
}
|
|
250
|
+
function connectWebSocketClientEndpoint(url, options = {}) {
|
|
251
|
+
return createWebSocketConnector(url, options).connect(new AbortController().signal);
|
|
252
|
+
}
|
|
206
253
|
function listenWebSocketEndpoint(options) {
|
|
207
254
|
return new Promise((resolve) => {
|
|
208
255
|
const host = options.host ?? "127.0.0.1";
|
|
@@ -357,6 +404,6 @@ function normalizeCause2(cause) {
|
|
|
357
404
|
return "WebSocket operation failed without a platform error message.";
|
|
358
405
|
}
|
|
359
406
|
|
|
360
|
-
export { connectWebSocketClientEndpoint, listenWebSocketEndpoint };
|
|
407
|
+
export { connectWebSocketClientEndpoint, createWebSocketConnector, listenWebSocketEndpoint };
|
|
361
408
|
//# sourceMappingURL=node.mjs.map
|
|
362
409
|
//# sourceMappingURL=node.mjs.map
|
package/dist/node.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/event-queue.ts","../src/websocket-client-core.ts","../src/node.ts"],"names":["WebSocket","connectionFailed","alreadyClosed","err","EndpointError","ENDPOINT_EXPECTED","ENDPOINT_ERROR_HINTS","ok","normalizeCause"],"mappings":"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","file":"node.mjs","sourcesContent":["import type { EndpointEvent } from '@forgeax/engine-net';\n\nexport const DEFAULT_MAX_QUEUED_EVENTS = 1024;\n\nexport class BoundedEventQueue {\n readonly #events: EndpointEvent[] = [];\n #closed = false;\n #disconnectReason: string | undefined;\n\n constructor(readonly maxQueuedEvents: number) {\n if (!Number.isInteger(maxQueuedEvents) || maxQueuedEvents < 1) {\n throw new RangeError('maxQueuedEvents must be a positive integer');\n }\n }\n\n get closed(): boolean {\n return this.#closed;\n }\n\n get disconnectReason(): string | undefined {\n return this.#disconnectReason;\n }\n\n enqueue(event: EndpointEvent): boolean {\n if (this.#closed) return false;\n if (this.#events.length === this.maxQueuedEvents) {\n this.close(`event queue overflow (maxQueuedEvents=${this.maxQueuedEvents})`);\n return false;\n }\n this.#events.push(event);\n return true;\n }\n\n close(reason: string): void {\n if (this.#closed) return;\n this.#closed = true;\n this.#disconnectReason = reason;\n }\n\n drain(): EndpointEvent[] {\n return this.#events.splice(0);\n }\n}\n","import {\n ENDPOINT_ERROR_HINTS,\n ENDPOINT_EXPECTED,\n EndpointError,\n type EndpointEvent,\n type NetEndpoint,\n type PeerId,\n} from '@forgeax/engine-net';\nimport { err, ok, type Result } from '@forgeax/engine-types';\nimport { BoundedEventQueue, DEFAULT_MAX_QUEUED_EVENTS } from './event-queue';\n\nexport interface WebSocketLike {\n readonly CONNECTING: number;\n readonly OPEN: number;\n readonly CLOSING: number;\n readonly CLOSED: number;\n readonly readyState: number;\n binaryType?: string;\n onopen: ((event: unknown) => void) | null;\n onmessage: ((event: { data: unknown }) => void) | null;\n onerror: ((event: unknown) => void) | null;\n onclose: ((event: unknown) => void) | null;\n send(data: Uint8Array): void;\n close(): void;\n}\n\nexport interface WebSocketConstructor {\n new (url: string): WebSocketLike;\n}\n\nexport interface WebSocketClientCoreOptions {\n readonly url: string;\n readonly maxQueuedEvents?: number | undefined;\n readonly toBytes: (data: unknown) => Uint8Array | Promise<Uint8Array | undefined> | undefined;\n}\n\nconst CLIENT_PEER_ID = 1 as PeerId;\n\nexport function createWebSocketClientEndpoint(\n WebSocket: WebSocketConstructor,\n options: WebSocketClientCoreOptions,\n): Promise<Result<NetEndpoint, EndpointError>> {\n return new Promise((resolve) => {\n let queue: BoundedEventQueue;\n try {\n queue = new BoundedEventQueue(options.maxQueuedEvents ?? DEFAULT_MAX_QUEUED_EVENTS);\n } catch (cause) {\n resolve(connectionFailed(options.url, cause));\n return;\n }\n\n let socket: WebSocketLike;\n try {\n socket = new WebSocket(options.url);\n socket.binaryType = 'arraybuffer';\n } catch (cause) {\n resolve(connectionFailed(options.url, cause));\n return;\n }\n\n const terminalEvents: EndpointEvent[] = [];\n let opened = false;\n let settled = false;\n let closed = false;\n let locallyClosed = false;\n let messageTail = Promise.resolve();\n\n const disconnect = (reason: string): void => {\n if (closed) return;\n closed = true;\n queue.close(reason);\n terminalEvents.push({ kind: 'peer-disconnected', peerId: CLIENT_PEER_ID });\n };\n\n const endpoint: NetEndpoint = {\n poll: () => [...queue.drain(), ...terminalEvents.splice(0)],\n send: (peerId, data) => {\n if (closed) return locallyClosed ? alreadyClosed() : connectionClosed(peerId);\n if (peerId !== CLIENT_PEER_ID)\n return err(\n new EndpointError({\n code: 'peer-not-found',\n expected: ENDPOINT_EXPECTED['peer-not-found'],\n hint: ENDPOINT_ERROR_HINTS['peer-not-found'],\n detail: { peerId },\n }),\n );\n if (socket.readyState !== socket.OPEN)\n return err(\n new EndpointError({\n code: 'connection-closed',\n expected: ENDPOINT_EXPECTED['connection-closed'],\n hint: ENDPOINT_ERROR_HINTS['connection-closed'],\n detail: { peerId },\n }),\n );\n try {\n socket.send(data);\n return ok(undefined);\n } catch (cause) {\n return err(\n new EndpointError({\n code: 'send-failed',\n expected: ENDPOINT_EXPECTED['send-failed'],\n hint: ENDPOINT_ERROR_HINTS['send-failed'],\n detail: { peerId, cause: normalizeCause(cause) },\n }),\n );\n }\n },\n close: () => {\n if (closed)\n return err(\n new EndpointError({\n code: 'already-closed',\n expected: ENDPOINT_EXPECTED['already-closed'],\n hint: ENDPOINT_ERROR_HINTS['already-closed'],\n detail: { cause: 'The WebSocket endpoint is already closed.' },\n }),\n );\n locallyClosed = true;\n disconnect('Endpoint close requested.');\n socket.close();\n return ok(undefined);\n },\n };\n\n socket.onopen = () => {\n if (settled) return;\n opened = true;\n settled = true;\n queue.enqueue({ kind: 'peer-connected', peerId: CLIENT_PEER_ID });\n resolve(ok(endpoint));\n };\n socket.onmessage = ({ data }) => {\n // Blob.arrayBuffer() is asynchronous. Serialize conversion so that\n // ordered WebSocket messages retain their wire order after decoding.\n messageTail = messageTail\n .then(async () => {\n const bytes = await options.toBytes(data);\n if (!bytes || closed) return;\n if (!queue.enqueue({ kind: 'message', peerId: CLIENT_PEER_ID, data: bytes })) {\n socket.close();\n }\n })\n .catch(() => undefined);\n };\n socket.onerror = (cause) => {\n if (opened) disconnect(`WebSocket error: ${normalizeCause(cause)}`);\n else if (!settled) {\n settled = true;\n resolve(connectionFailed(options.url, cause));\n }\n };\n socket.onclose = (cause) => {\n if (!opened && !settled) {\n settled = true;\n resolve(connectionFailed(options.url, cause));\n return;\n }\n disconnect(`WebSocket closed: ${normalizeCause(cause)}`);\n };\n });\n}\n\nfunction connectionFailed(address: string, cause: unknown): Result<never, EndpointError> {\n return err(\n new EndpointError({\n code: 'connection-failed',\n expected: ENDPOINT_EXPECTED['connection-failed'],\n hint: ENDPOINT_ERROR_HINTS['connection-failed'],\n detail: { address, cause: normalizeCause(cause) },\n }),\n );\n}\n\nfunction alreadyClosed(): Result<never, EndpointError> {\n return err(\n new EndpointError({\n code: 'already-closed',\n expected: ENDPOINT_EXPECTED['already-closed'],\n hint: ENDPOINT_ERROR_HINTS['already-closed'],\n detail: { cause: 'The WebSocket endpoint is closed.' },\n }),\n );\n}\n\nfunction connectionClosed(peerId: PeerId): Result<never, EndpointError> {\n return err(\n new EndpointError({\n code: 'connection-closed',\n expected: ENDPOINT_EXPECTED['connection-closed'],\n hint: ENDPOINT_ERROR_HINTS['connection-closed'],\n detail: { peerId },\n }),\n );\n}\n\nfunction normalizeCause(cause: unknown): string {\n if (cause instanceof Error) return cause.message;\n if (typeof cause === 'string') return cause;\n return 'WebSocket operation failed without a platform error message.';\n}\n","import {\n ENDPOINT_ERROR_HINTS,\n ENDPOINT_EXPECTED,\n EndpointError,\n type EndpointEvent,\n type NetEndpoint,\n type PeerId,\n} from '@forgeax/engine-net';\nimport { err, ok, type Result } from '@forgeax/engine-types';\nimport WebSocket, { WebSocketServer } from 'ws';\nimport { BoundedEventQueue, DEFAULT_MAX_QUEUED_EVENTS } from './event-queue';\nimport { createWebSocketClientEndpoint } from './websocket-client-core';\n\nexport interface ListenWebSocketEndpointOptions {\n readonly port: number;\n readonly host?: string;\n readonly maxPeers?: number;\n readonly maxQueuedEvents?: number | undefined;\n}\n\nexport interface ConnectWebSocketClientEndpointOptions {\n readonly maxQueuedEvents?: number | undefined;\n}\n\nexport function connectWebSocketClientEndpoint(\n url: string,\n options: ConnectWebSocketClientEndpointOptions = {},\n): Promise<Result<NetEndpoint, EndpointError>> {\n return createWebSocketClientEndpoint(\n WebSocket as unknown as import('./websocket-client-core').WebSocketConstructor,\n {\n url,\n maxQueuedEvents: options.maxQueuedEvents,\n toBytes: toBytes,\n },\n );\n}\n\nexport function listenWebSocketEndpoint(\n options: ListenWebSocketEndpointOptions,\n): Promise<Result<NetEndpoint, EndpointError>> {\n return new Promise((resolve) => {\n const host = options.host ?? '127.0.0.1';\n const address = `ws://${host}:${options.port}`;\n let queue: BoundedEventQueue;\n try {\n queue = new BoundedEventQueue(options.maxQueuedEvents ?? DEFAULT_MAX_QUEUED_EVENTS);\n } catch (cause) {\n resolve(connectionFailed(address, cause));\n return;\n }\n\n const terminalEvents: EndpointEvent[] = [];\n const peers = new Map<PeerId, WebSocket>();\n const disconnectedPeers = new Set<PeerId>();\n const sockets = new Map<WebSocket, PeerId>();\n const maxPeers = options.maxPeers ?? Number.POSITIVE_INFINITY;\n let nextPeerId = 1;\n let settled = false;\n let closed = false;\n const server = new WebSocketServer({ host, port: options.port, perMessageDeflate: false });\n\n const endpoint: NetEndpoint = {\n poll: () => [...queue.drain(), ...terminalEvents.splice(0)],\n send: (peerId, data) => {\n if (closed) return alreadyClosed('The WebSocket listener endpoint is closed.');\n const socket = peers.get(peerId);\n if (!socket && disconnectedPeers.has(peerId))\n return err(\n new EndpointError({\n code: 'connection-closed',\n expected: ENDPOINT_EXPECTED['connection-closed'],\n hint: ENDPOINT_ERROR_HINTS['connection-closed'],\n detail: { peerId },\n }),\n );\n if (!socket)\n return err(\n new EndpointError({\n code: 'peer-not-found',\n expected: ENDPOINT_EXPECTED['peer-not-found'],\n hint: ENDPOINT_ERROR_HINTS['peer-not-found'],\n detail: { peerId },\n }),\n );\n if (socket.readyState !== socket.OPEN)\n return err(\n new EndpointError({\n code: 'connection-closed',\n expected: ENDPOINT_EXPECTED['connection-closed'],\n hint: ENDPOINT_ERROR_HINTS['connection-closed'],\n detail: { peerId },\n }),\n );\n try {\n socket.send(data, { binary: true });\n return ok(undefined);\n } catch (cause) {\n return err(\n new EndpointError({\n code: 'send-failed',\n expected: ENDPOINT_EXPECTED['send-failed'],\n hint: ENDPOINT_ERROR_HINTS['send-failed'],\n detail: { peerId, cause: normalizeCause(cause) },\n }),\n );\n }\n },\n close: () => {\n if (closed) return alreadyClosed('The WebSocket listener endpoint is already closed.');\n closed = true;\n for (const socket of peers.values()) socket.close();\n server.close();\n return ok(undefined);\n },\n };\n\n const enqueue = (event: EndpointEvent, socket?: WebSocket): void => {\n if (queue.enqueue(event)) return;\n if (event.kind !== 'peer-disconnected') {\n terminalEvents.push({ kind: 'peer-disconnected', peerId: event.peerId });\n }\n socket?.close();\n };\n\n server.on('connection', (socket) => {\n if (closed || peers.size >= maxPeers) {\n socket.close();\n return;\n }\n const peerId = nextPeerId++ as PeerId;\n peers.set(peerId, socket);\n sockets.set(socket, peerId);\n enqueue({ kind: 'peer-connected', peerId }, socket);\n socket.on('message', (data, isBinary) => {\n if (!isBinary) {\n socket.close();\n return;\n }\n const bytes = toBytes(data);\n if (!bytes) {\n socket.close();\n return;\n }\n enqueue({ kind: 'message', peerId, data: bytes }, socket);\n });\n socket.on('close', () => {\n if (!peers.delete(peerId)) return;\n disconnectedPeers.add(peerId);\n sockets.delete(socket);\n enqueue({ kind: 'peer-disconnected', peerId });\n });\n socket.on('error', () => socket.close());\n });\n server.on('error', (cause) => {\n if (settled) return;\n settled = true;\n resolve(connectionFailed(address, cause));\n });\n server.on('listening', () => {\n if (settled) return;\n settled = true;\n resolve(ok(endpoint));\n });\n });\n}\n\nfunction toBytes(data: unknown): Uint8Array | undefined {\n if (data instanceof Uint8Array)\n return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);\n if (data instanceof ArrayBuffer) return new Uint8Array(data);\n return undefined;\n}\n\nfunction connectionFailed(address: string, cause: unknown): Result<never, EndpointError> {\n return err(\n new EndpointError({\n code: 'connection-failed',\n expected: ENDPOINT_EXPECTED['connection-failed'],\n hint: ENDPOINT_ERROR_HINTS['connection-failed'],\n detail: { address, cause: normalizeCause(cause) },\n }),\n );\n}\n\nfunction alreadyClosed(cause: string): Result<never, EndpointError> {\n return err(\n new EndpointError({\n code: 'already-closed',\n expected: ENDPOINT_EXPECTED['already-closed'],\n hint: ENDPOINT_ERROR_HINTS['already-closed'],\n detail: { cause },\n }),\n );\n}\n\nfunction normalizeCause(cause: unknown): string {\n if (cause instanceof Error) return cause.message;\n if (typeof cause === 'string') return cause;\n return 'WebSocket operation failed without a platform error message.';\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/event-queue.ts","../src/websocket-client-core.ts","../src/websocket-connector.ts","../src/node.ts"],"names":["WebSocket","connectionFailed","alreadyClosed","err","EndpointError","ENDPOINT_EXPECTED","ENDPOINT_ERROR_HINTS","ok","normalizeCause"],"mappings":"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","file":"node.mjs","sourcesContent":["import type { EndpointEvent } from '@forgeax/engine-net';\n\nexport const DEFAULT_MAX_QUEUED_EVENTS = 1024;\n\nexport class BoundedEventQueue {\n readonly #events: EndpointEvent[] = [];\n #closed = false;\n #disconnectReason: string | undefined;\n\n constructor(readonly maxQueuedEvents: number) {\n if (!Number.isInteger(maxQueuedEvents) || maxQueuedEvents < 1) {\n throw new RangeError('maxQueuedEvents must be a positive integer');\n }\n }\n\n get closed(): boolean {\n return this.#closed;\n }\n\n get disconnectReason(): string | undefined {\n return this.#disconnectReason;\n }\n\n enqueue(event: EndpointEvent): boolean {\n if (this.#closed) return false;\n if (this.#events.length === this.maxQueuedEvents) {\n this.close(`event queue overflow (maxQueuedEvents=${this.maxQueuedEvents})`);\n return false;\n }\n this.#events.push(event);\n return true;\n }\n\n close(reason: string): void {\n if (this.#closed) return;\n this.#closed = true;\n this.#disconnectReason = reason;\n }\n\n drain(): EndpointEvent[] {\n return this.#events.splice(0);\n }\n}\n","import {\n ENDPOINT_ERROR_HINTS,\n ENDPOINT_EXPECTED,\n EndpointError,\n type EndpointEvent,\n type NetEndpoint,\n type PeerId,\n} from '@forgeax/engine-net';\nimport { err, ok, type Result } from '@forgeax/engine-types';\nimport { BoundedEventQueue, DEFAULT_MAX_QUEUED_EVENTS } from './event-queue';\n\nexport interface WebSocketLike {\n readonly CONNECTING: number;\n readonly OPEN: number;\n readonly CLOSING: number;\n readonly CLOSED: number;\n readonly readyState: number;\n binaryType?: string;\n onopen: ((event: unknown) => void) | null;\n onmessage: ((event: { data: unknown }) => void) | null;\n onerror: ((event: unknown) => void) | null;\n onclose: ((event: unknown) => void) | null;\n send(data: Uint8Array): void;\n close(): void;\n}\n\nexport interface WebSocketConstructor {\n new (url: string): WebSocketLike;\n}\n\nexport interface WebSocketClientCoreOptions {\n readonly url: string;\n readonly maxQueuedEvents?: number | undefined;\n readonly peerId?: PeerId;\n readonly signal?: AbortSignal;\n readonly toBytes: (data: unknown) => Uint8Array | Promise<Uint8Array | undefined> | undefined;\n}\n\nconst CLIENT_PEER_ID = 1 as PeerId;\n\nexport function createWebSocketClientEndpoint(\n WebSocket: WebSocketConstructor,\n options: WebSocketClientCoreOptions,\n): Promise<Result<NetEndpoint, EndpointError>> {\n return new Promise((resolve) => {\n const clientPeerId = options.peerId ?? CLIENT_PEER_ID;\n const signal = options.signal ?? new AbortController().signal;\n if (signal.aborted) {\n resolve(connectionFailed(options.url, 'WebSocket connection aborted.'));\n return;\n }\n\n let queue: BoundedEventQueue;\n try {\n queue = new BoundedEventQueue(options.maxQueuedEvents ?? DEFAULT_MAX_QUEUED_EVENTS);\n } catch (cause) {\n resolve(connectionFailed(options.url, cause));\n return;\n }\n\n let socket: WebSocketLike;\n try {\n socket = new WebSocket(options.url);\n socket.binaryType = 'arraybuffer';\n } catch (cause) {\n resolve(connectionFailed(options.url, cause));\n return;\n }\n\n const terminalEvents: EndpointEvent[] = [];\n let opened = false;\n let settled = false;\n let closed = false;\n let locallyClosed = false;\n let messageTail = Promise.resolve();\n\n const removeAbortListener = (): void => {\n signal.removeEventListener('abort', abortPendingConnection);\n };\n\n const abortPendingConnection = (): void => {\n if (opened || settled) return;\n settled = true;\n try {\n socket.close();\n } catch {\n // Closing a partially opened platform socket is best effort.\n }\n removeAbortListener();\n resolve(connectionFailed(options.url, 'WebSocket connection aborted.'));\n };\n\n signal.addEventListener('abort', abortPendingConnection, { once: true });\n if (signal.aborted) {\n abortPendingConnection();\n return;\n }\n\n const disconnect = (reason: string): void => {\n if (closed) return;\n closed = true;\n queue.close(reason);\n terminalEvents.push({ kind: 'peer-disconnected', peerId: clientPeerId });\n };\n\n const endpoint: NetEndpoint = {\n poll: () => [...queue.drain(), ...terminalEvents.splice(0)],\n send: (peerId, data) => {\n if (closed) return locallyClosed ? alreadyClosed() : connectionClosed(peerId);\n if (peerId !== clientPeerId)\n return err(\n new EndpointError({\n code: 'peer-not-found',\n expected: ENDPOINT_EXPECTED['peer-not-found'],\n hint: ENDPOINT_ERROR_HINTS['peer-not-found'],\n detail: { peerId },\n }),\n );\n if (socket.readyState !== socket.OPEN)\n return err(\n new EndpointError({\n code: 'connection-closed',\n expected: ENDPOINT_EXPECTED['connection-closed'],\n hint: ENDPOINT_ERROR_HINTS['connection-closed'],\n detail: { peerId },\n }),\n );\n try {\n socket.send(data);\n return ok(undefined);\n } catch (cause) {\n return err(\n new EndpointError({\n code: 'send-failed',\n expected: ENDPOINT_EXPECTED['send-failed'],\n hint: ENDPOINT_ERROR_HINTS['send-failed'],\n detail: { peerId, cause: normalizeCause(cause) },\n }),\n );\n }\n },\n close: () => {\n if (closed)\n return err(\n new EndpointError({\n code: 'already-closed',\n expected: ENDPOINT_EXPECTED['already-closed'],\n hint: ENDPOINT_ERROR_HINTS['already-closed'],\n detail: { cause: 'The WebSocket endpoint is already closed.' },\n }),\n );\n locallyClosed = true;\n disconnect('Endpoint close requested.');\n socket.close();\n return ok(undefined);\n },\n };\n\n socket.onopen = () => {\n if (settled) return;\n removeAbortListener();\n opened = true;\n settled = true;\n queue.enqueue({ kind: 'peer-connected', peerId: clientPeerId });\n resolve(ok(endpoint));\n };\n socket.onmessage = ({ data }) => {\n // Blob.arrayBuffer() is asynchronous. Serialize conversion so that\n // ordered WebSocket messages retain their wire order after decoding.\n messageTail = messageTail\n .then(async () => {\n const bytes = await options.toBytes(data);\n if (!bytes || closed) return;\n if (!queue.enqueue({ kind: 'message', peerId: clientPeerId, data: bytes })) {\n socket.close();\n }\n })\n .catch(() => undefined);\n };\n socket.onerror = (cause) => {\n if (opened) disconnect(`WebSocket error: ${normalizeCause(cause)}`);\n else if (!settled) {\n settled = true;\n removeAbortListener();\n resolve(connectionFailed(options.url, cause));\n }\n };\n socket.onclose = (cause) => {\n if (!opened && !settled) {\n settled = true;\n removeAbortListener();\n resolve(connectionFailed(options.url, cause));\n return;\n }\n disconnect(`WebSocket closed: ${normalizeCause(cause)}`);\n };\n });\n}\n\nfunction connectionFailed(address: string, cause: unknown): Result<never, EndpointError> {\n return err(\n new EndpointError({\n code: 'connection-failed',\n expected: ENDPOINT_EXPECTED['connection-failed'],\n hint: ENDPOINT_ERROR_HINTS['connection-failed'],\n detail: { address, cause: normalizeCause(cause) },\n }),\n );\n}\n\nfunction alreadyClosed(): Result<never, EndpointError> {\n return err(\n new EndpointError({\n code: 'already-closed',\n expected: ENDPOINT_EXPECTED['already-closed'],\n hint: ENDPOINT_ERROR_HINTS['already-closed'],\n detail: { cause: 'The WebSocket endpoint is closed.' },\n }),\n );\n}\n\nfunction connectionClosed(peerId: PeerId): Result<never, EndpointError> {\n return err(\n new EndpointError({\n code: 'connection-closed',\n expected: ENDPOINT_EXPECTED['connection-closed'],\n hint: ENDPOINT_ERROR_HINTS['connection-closed'],\n detail: { peerId },\n }),\n );\n}\n\nfunction normalizeCause(cause: unknown): string {\n if (cause instanceof Error) return cause.message;\n if (typeof cause === 'string') return cause;\n return 'WebSocket operation failed without a platform error message.';\n}\n","import type { NetEndpointConnector, PeerId } from '@forgeax/engine-net';\nimport {\n createWebSocketClientEndpoint,\n type WebSocketClientCoreOptions,\n type WebSocketConstructor,\n} from './websocket-client-core';\n\nexport interface WebSocketConnectorOptions {\n readonly maxQueuedEvents?: number | undefined;\n}\n\nexport interface WebSocketConnectorRuntime {\n readonly WebSocket: WebSocketConstructor;\n readonly toBytes: WebSocketClientCoreOptions['toBytes'];\n}\n\nexport function createWebSocketConnectorAdapter(\n runtime: WebSocketConnectorRuntime,\n url: string,\n options: WebSocketConnectorOptions = {},\n): NetEndpointConnector {\n let nextPeerId = 1;\n return {\n connect: (signal) => {\n const peerId = nextPeerId++ as PeerId;\n return createWebSocketClientEndpoint(runtime.WebSocket, {\n url,\n maxQueuedEvents: options.maxQueuedEvents,\n peerId,\n signal,\n toBytes: runtime.toBytes,\n });\n },\n };\n}\n","import {\n ENDPOINT_ERROR_HINTS,\n ENDPOINT_EXPECTED,\n EndpointError,\n type EndpointEvent,\n type NetEndpoint,\n type PeerId,\n} from '@forgeax/engine-net';\nimport { err, ok, type Result } from '@forgeax/engine-types';\nimport WebSocket, { WebSocketServer } from 'ws';\nimport { BoundedEventQueue, DEFAULT_MAX_QUEUED_EVENTS } from './event-queue';\nimport type { WebSocketConstructor } from './websocket-client-core';\nimport { createWebSocketConnectorAdapter } from './websocket-connector';\n\nexport interface ListenWebSocketEndpointOptions {\n readonly port: number;\n readonly host?: string;\n readonly maxPeers?: number;\n readonly maxQueuedEvents?: number | undefined;\n}\n\nexport interface ConnectWebSocketClientEndpointOptions {\n readonly maxQueuedEvents?: number | undefined;\n}\n\n/**\n * Creates the Node WebSocket adapter for the public NetEndpointConnector.\n * Each connect call accepts an AbortSignal and creates one replacement-capable\n * NetEndpoint. Transport lifecycle and EndpointError results stay here;\n * authoritative resync and replication policy stay with NetSession.\n */\nexport function createWebSocketConnector(\n url: string,\n options: ConnectWebSocketClientEndpointOptions = {},\n): import('@forgeax/engine-net').NetEndpointConnector {\n return createWebSocketConnectorAdapter(\n {\n WebSocket: WebSocket as unknown as WebSocketConstructor,\n toBytes,\n },\n url,\n options,\n );\n}\n\n/**\n * Connects one Node WebSocket with the default one-shot AbortSignal.\n * Use createWebSocketConnector when the caller must cancel or replace an\n * endpoint through an explicit signal.\n */\nexport function connectWebSocketClientEndpoint(\n url: string,\n options: ConnectWebSocketClientEndpointOptions = {},\n): Promise<Result<NetEndpoint, EndpointError>> {\n return createWebSocketConnector(url, options).connect(new AbortController().signal);\n}\n\n/**\n * Starts a Node WebSocket listener that exposes NetEndpoint peer events and\n * binary messages without owning NetSession or replication policy.\n */\nexport function listenWebSocketEndpoint(\n options: ListenWebSocketEndpointOptions,\n): Promise<Result<NetEndpoint, EndpointError>> {\n return new Promise((resolve) => {\n const host = options.host ?? '127.0.0.1';\n const address = `ws://${host}:${options.port}`;\n let queue: BoundedEventQueue;\n try {\n queue = new BoundedEventQueue(options.maxQueuedEvents ?? DEFAULT_MAX_QUEUED_EVENTS);\n } catch (cause) {\n resolve(connectionFailed(address, cause));\n return;\n }\n\n const terminalEvents: EndpointEvent[] = [];\n const peers = new Map<PeerId, WebSocket>();\n const disconnectedPeers = new Set<PeerId>();\n const sockets = new Map<WebSocket, PeerId>();\n const maxPeers = options.maxPeers ?? Number.POSITIVE_INFINITY;\n let nextPeerId = 1;\n let settled = false;\n let closed = false;\n const server = new WebSocketServer({ host, port: options.port, perMessageDeflate: false });\n\n const endpoint: NetEndpoint = {\n poll: () => [...queue.drain(), ...terminalEvents.splice(0)],\n send: (peerId, data) => {\n if (closed) return alreadyClosed('The WebSocket listener endpoint is closed.');\n const socket = peers.get(peerId);\n if (!socket && disconnectedPeers.has(peerId))\n return err(\n new EndpointError({\n code: 'connection-closed',\n expected: ENDPOINT_EXPECTED['connection-closed'],\n hint: ENDPOINT_ERROR_HINTS['connection-closed'],\n detail: { peerId },\n }),\n );\n if (!socket)\n return err(\n new EndpointError({\n code: 'peer-not-found',\n expected: ENDPOINT_EXPECTED['peer-not-found'],\n hint: ENDPOINT_ERROR_HINTS['peer-not-found'],\n detail: { peerId },\n }),\n );\n if (socket.readyState !== socket.OPEN)\n return err(\n new EndpointError({\n code: 'connection-closed',\n expected: ENDPOINT_EXPECTED['connection-closed'],\n hint: ENDPOINT_ERROR_HINTS['connection-closed'],\n detail: { peerId },\n }),\n );\n try {\n socket.send(data, { binary: true });\n return ok(undefined);\n } catch (cause) {\n return err(\n new EndpointError({\n code: 'send-failed',\n expected: ENDPOINT_EXPECTED['send-failed'],\n hint: ENDPOINT_ERROR_HINTS['send-failed'],\n detail: { peerId, cause: normalizeCause(cause) },\n }),\n );\n }\n },\n close: () => {\n if (closed) return alreadyClosed('The WebSocket listener endpoint is already closed.');\n closed = true;\n for (const socket of peers.values()) socket.close();\n server.close();\n return ok(undefined);\n },\n };\n\n const enqueue = (event: EndpointEvent, socket?: WebSocket): void => {\n if (queue.enqueue(event)) return;\n if (event.kind !== 'peer-disconnected') {\n terminalEvents.push({ kind: 'peer-disconnected', peerId: event.peerId });\n }\n socket?.close();\n };\n\n server.on('connection', (socket) => {\n if (closed || peers.size >= maxPeers) {\n socket.close();\n return;\n }\n const peerId = nextPeerId++ as PeerId;\n peers.set(peerId, socket);\n sockets.set(socket, peerId);\n enqueue({ kind: 'peer-connected', peerId }, socket);\n socket.on('message', (data, isBinary) => {\n if (!isBinary) {\n socket.close();\n return;\n }\n const bytes = toBytes(data);\n if (!bytes) {\n socket.close();\n return;\n }\n enqueue({ kind: 'message', peerId, data: bytes }, socket);\n });\n socket.on('close', () => {\n if (!peers.delete(peerId)) return;\n disconnectedPeers.add(peerId);\n sockets.delete(socket);\n enqueue({ kind: 'peer-disconnected', peerId });\n });\n socket.on('error', () => socket.close());\n });\n server.on('error', (cause) => {\n if (settled) return;\n settled = true;\n resolve(connectionFailed(address, cause));\n });\n server.on('listening', () => {\n if (settled) return;\n settled = true;\n resolve(ok(endpoint));\n });\n });\n}\n\nfunction toBytes(data: unknown): Uint8Array | undefined {\n if (data instanceof Uint8Array)\n return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);\n if (data instanceof ArrayBuffer) return new Uint8Array(data);\n return undefined;\n}\n\nfunction connectionFailed(address: string, cause: unknown): Result<never, EndpointError> {\n return err(\n new EndpointError({\n code: 'connection-failed',\n expected: ENDPOINT_EXPECTED['connection-failed'],\n hint: ENDPOINT_ERROR_HINTS['connection-failed'],\n detail: { address, cause: normalizeCause(cause) },\n }),\n );\n}\n\nfunction alreadyClosed(cause: string): Result<never, EndpointError> {\n return err(\n new EndpointError({\n code: 'already-closed',\n expected: ENDPOINT_EXPECTED['already-closed'],\n hint: ENDPOINT_ERROR_HINTS['already-closed'],\n detail: { cause },\n }),\n );\n}\n\nfunction normalizeCause(cause: unknown): string {\n if (cause instanceof Error) return cause.message;\n if (typeof cause === 'string') return cause;\n return 'WebSocket operation failed without a platform error message.';\n}\n"]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EndpointError, type NetEndpoint } from '@forgeax/engine-net';
|
|
1
|
+
import { EndpointError, type NetEndpoint, type PeerId } from '@forgeax/engine-net';
|
|
2
2
|
import { type Result } from '@forgeax/engine-types';
|
|
3
3
|
export interface WebSocketLike {
|
|
4
4
|
readonly CONNECTING: number;
|
|
@@ -22,6 +22,8 @@ export interface WebSocketConstructor {
|
|
|
22
22
|
export interface WebSocketClientCoreOptions {
|
|
23
23
|
readonly url: string;
|
|
24
24
|
readonly maxQueuedEvents?: number | undefined;
|
|
25
|
+
readonly peerId?: PeerId;
|
|
26
|
+
readonly signal?: AbortSignal;
|
|
25
27
|
readonly toBytes: (data: unknown) => Uint8Array | Promise<Uint8Array | undefined> | undefined;
|
|
26
28
|
}
|
|
27
29
|
export declare function createWebSocketClientEndpoint(WebSocket: WebSocketConstructor, options: WebSocketClientCoreOptions): Promise<Result<NetEndpoint, EndpointError>>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"websocket-client-core.d.ts","sourceRoot":"","sources":["../src/websocket-client-core.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,aAAa,EAEb,KAAK,WAAW,
|
|
1
|
+
{"version":3,"file":"websocket-client-core.d.ts","sourceRoot":"","sources":["../src/websocket-client-core.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,aAAa,EAEb,KAAK,WAAW,EAChB,KAAK,MAAM,EACZ,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAW,KAAK,MAAM,EAAE,MAAM,uBAAuB,CAAC;AAG7D,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,CAAC,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC,GAAG,IAAI,CAAC;IAC1C,SAAS,EAAE,CAAC,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,CAAC,GAAG,IAAI,CAAC;IACvD,OAAO,EAAE,CAAC,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC,GAAG,IAAI,CAAC;IAC3C,OAAO,EAAE,CAAC,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC,GAAG,IAAI,CAAC;IAC3C,IAAI,CAAC,IAAI,EAAE,UAAU,GAAG,IAAI,CAAC;IAC7B,KAAK,IAAI,IAAI,CAAC;CACf;AAED,MAAM,WAAW,oBAAoB;IACnC,KAAK,GAAG,EAAE,MAAM,GAAG,aAAa,CAAC;CAClC;AAED,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9C,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;IAC9B,QAAQ,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,UAAU,GAAG,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC,GAAG,SAAS,CAAC;CAC/F;AAID,wBAAgB,6BAA6B,CAC3C,SAAS,EAAE,oBAAoB,EAC/B,OAAO,EAAE,0BAA0B,GAClC,OAAO,CAAC,MAAM,CAAC,WAAW,EAAE,aAAa,CAAC,CAAC,CA0J7C"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { NetEndpointConnector } from '@forgeax/engine-net';
|
|
2
|
+
import { type WebSocketClientCoreOptions, type WebSocketConstructor } from './websocket-client-core';
|
|
3
|
+
export interface WebSocketConnectorOptions {
|
|
4
|
+
readonly maxQueuedEvents?: number | undefined;
|
|
5
|
+
}
|
|
6
|
+
export interface WebSocketConnectorRuntime {
|
|
7
|
+
readonly WebSocket: WebSocketConstructor;
|
|
8
|
+
readonly toBytes: WebSocketClientCoreOptions['toBytes'];
|
|
9
|
+
}
|
|
10
|
+
export declare function createWebSocketConnectorAdapter(runtime: WebSocketConnectorRuntime, url: string, options?: WebSocketConnectorOptions): NetEndpointConnector;
|
|
11
|
+
//# sourceMappingURL=websocket-connector.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"websocket-connector.d.ts","sourceRoot":"","sources":["../src/websocket-connector.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAU,MAAM,qBAAqB,CAAC;AACxE,OAAO,EAEL,KAAK,0BAA0B,EAC/B,KAAK,oBAAoB,EAC1B,MAAM,yBAAyB,CAAC;AAEjC,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC/C;AAED,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,SAAS,EAAE,oBAAoB,CAAC;IACzC,QAAQ,CAAC,OAAO,EAAE,0BAA0B,CAAC,SAAS,CAAC,CAAC;CACzD;AAED,wBAAgB,+BAA+B,CAC7C,OAAO,EAAE,yBAAyB,EAClC,GAAG,EAAE,MAAM,EACX,OAAO,GAAE,yBAA8B,GACtC,oBAAoB,CActB"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forgeax/engine-net-websocket",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -50,8 +50,8 @@
|
|
|
50
50
|
}
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@forgeax/engine-net": "0.1.
|
|
54
|
-
"@forgeax/engine-types": "0.1.
|
|
53
|
+
"@forgeax/engine-net": "0.1.6",
|
|
54
|
+
"@forgeax/engine-types": "0.1.6",
|
|
55
55
|
"ws": "^8.20.0"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
package/src/browser.ts
CHANGED
|
@@ -1,20 +1,42 @@
|
|
|
1
|
-
import type { EndpointError, NetEndpoint } from '@forgeax/engine-net';
|
|
1
|
+
import type { EndpointError, NetEndpoint, NetEndpointConnector } from '@forgeax/engine-net';
|
|
2
2
|
import type { Result } from '@forgeax/engine-types';
|
|
3
|
-
import {
|
|
3
|
+
import type { WebSocketConstructor } from './websocket-client-core';
|
|
4
|
+
import {
|
|
5
|
+
createWebSocketConnectorAdapter,
|
|
6
|
+
type WebSocketConnectorOptions,
|
|
7
|
+
} from './websocket-connector';
|
|
4
8
|
|
|
5
9
|
export interface ConnectWebSocketClientEndpointOptions {
|
|
6
10
|
readonly maxQueuedEvents?: number | undefined;
|
|
7
11
|
}
|
|
8
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Creates the browser WebSocket adapter for the public NetEndpointConnector.
|
|
15
|
+
* Each connect call accepts an AbortSignal and creates one replacement-capable
|
|
16
|
+
* NetEndpoint. Transport lifecycle and EndpointError results stay here;
|
|
17
|
+
* authoritative resync and replication policy stay with NetSession.
|
|
18
|
+
*/
|
|
19
|
+
export function createWebSocketConnector(
|
|
20
|
+
url: string,
|
|
21
|
+
options: WebSocketConnectorOptions = {},
|
|
22
|
+
): NetEndpointConnector {
|
|
23
|
+
return createWebSocketConnectorAdapter(
|
|
24
|
+
{ WebSocket: WebSocket as unknown as WebSocketConstructor, toBytes },
|
|
25
|
+
url,
|
|
26
|
+
options,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Connects one browser WebSocket with the default one-shot AbortSignal.
|
|
32
|
+
* Use createWebSocketConnector when the caller must cancel or replace an
|
|
33
|
+
* endpoint through an explicit signal.
|
|
34
|
+
*/
|
|
9
35
|
export function connectWebSocketClientEndpoint(
|
|
10
36
|
url: string,
|
|
11
37
|
options: ConnectWebSocketClientEndpointOptions = {},
|
|
12
38
|
): Promise<Result<NetEndpoint, EndpointError>> {
|
|
13
|
-
return
|
|
14
|
-
url,
|
|
15
|
-
maxQueuedEvents: options.maxQueuedEvents,
|
|
16
|
-
toBytes,
|
|
17
|
-
});
|
|
39
|
+
return createWebSocketConnector(url, options).connect(new AbortController().signal);
|
|
18
40
|
}
|
|
19
41
|
|
|
20
42
|
function toBytes(data: unknown): Uint8Array | Promise<Uint8Array | undefined> | undefined {
|