@forgeax/engine-net-websocket 0.0.0-dev.8d955ade1c79

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.
@@ -0,0 +1,261 @@
1
+ import { EndpointError, ENDPOINT_ERROR_HINTS, ENDPOINT_EXPECTED } from '@forgeax/engine-net';
2
+ import { ok, err } from '@forgeax/engine-types';
3
+
4
+ // src/websocket-client-core.ts
5
+
6
+ // src/event-queue.ts
7
+ var DEFAULT_MAX_QUEUED_EVENTS = 1024;
8
+ var BoundedEventQueue = class {
9
+ constructor(maxQueuedEvents) {
10
+ this.maxQueuedEvents = maxQueuedEvents;
11
+ if (!Number.isInteger(maxQueuedEvents) || maxQueuedEvents < 1) {
12
+ throw new RangeError("maxQueuedEvents must be a positive integer");
13
+ }
14
+ }
15
+ maxQueuedEvents;
16
+ #events = [];
17
+ #closed = false;
18
+ #disconnectReason;
19
+ get closed() {
20
+ return this.#closed;
21
+ }
22
+ get disconnectReason() {
23
+ return this.#disconnectReason;
24
+ }
25
+ enqueue(event) {
26
+ if (this.#closed) return false;
27
+ if (this.#events.length === this.maxQueuedEvents) {
28
+ this.close(`event queue overflow (maxQueuedEvents=${this.maxQueuedEvents})`);
29
+ return false;
30
+ }
31
+ this.#events.push(event);
32
+ return true;
33
+ }
34
+ close(reason) {
35
+ if (this.#closed) return;
36
+ this.#closed = true;
37
+ this.#disconnectReason = reason;
38
+ }
39
+ drain() {
40
+ return this.#events.splice(0);
41
+ }
42
+ };
43
+
44
+ // src/websocket-client-core.ts
45
+ var CLIENT_PEER_ID = 1;
46
+ function createWebSocketClientEndpoint(WebSocket2, options) {
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
+ }
54
+ let queue;
55
+ try {
56
+ queue = new BoundedEventQueue(options.maxQueuedEvents ?? DEFAULT_MAX_QUEUED_EVENTS);
57
+ } catch (cause) {
58
+ resolve(connectionFailed(options.url, cause));
59
+ return;
60
+ }
61
+ let socket;
62
+ try {
63
+ socket = new WebSocket2(options.url);
64
+ socket.binaryType = "arraybuffer";
65
+ } catch (cause) {
66
+ resolve(connectionFailed(options.url, cause));
67
+ return;
68
+ }
69
+ const terminalEvents = [];
70
+ let opened = false;
71
+ let settled = false;
72
+ let closed = false;
73
+ let locallyClosed = false;
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
+ }
93
+ const disconnect = (reason) => {
94
+ if (closed) return;
95
+ closed = true;
96
+ queue.close(reason);
97
+ terminalEvents.push({ kind: "peer-disconnected", peerId: clientPeerId });
98
+ };
99
+ const endpoint = {
100
+ poll: () => [...queue.drain(), ...terminalEvents.splice(0)],
101
+ send: (peerId, data) => {
102
+ if (closed) return locallyClosed ? alreadyClosed() : connectionClosed(peerId);
103
+ if (peerId !== clientPeerId)
104
+ return err(
105
+ new EndpointError({
106
+ code: "peer-not-found",
107
+ expected: ENDPOINT_EXPECTED["peer-not-found"],
108
+ hint: ENDPOINT_ERROR_HINTS["peer-not-found"],
109
+ detail: { peerId }
110
+ })
111
+ );
112
+ if (socket.readyState !== socket.OPEN)
113
+ return err(
114
+ new EndpointError({
115
+ code: "connection-closed",
116
+ expected: ENDPOINT_EXPECTED["connection-closed"],
117
+ hint: ENDPOINT_ERROR_HINTS["connection-closed"],
118
+ detail: { peerId }
119
+ })
120
+ );
121
+ try {
122
+ socket.send(data);
123
+ return ok(void 0);
124
+ } catch (cause) {
125
+ return err(
126
+ new EndpointError({
127
+ code: "send-failed",
128
+ expected: ENDPOINT_EXPECTED["send-failed"],
129
+ hint: ENDPOINT_ERROR_HINTS["send-failed"],
130
+ detail: { peerId, cause: normalizeCause(cause) }
131
+ })
132
+ );
133
+ }
134
+ },
135
+ close: () => {
136
+ if (closed)
137
+ return err(
138
+ new EndpointError({
139
+ code: "already-closed",
140
+ expected: ENDPOINT_EXPECTED["already-closed"],
141
+ hint: ENDPOINT_ERROR_HINTS["already-closed"],
142
+ detail: { cause: "The WebSocket endpoint is already closed." }
143
+ })
144
+ );
145
+ locallyClosed = true;
146
+ disconnect("Endpoint close requested.");
147
+ socket.close();
148
+ return ok(void 0);
149
+ }
150
+ };
151
+ socket.onopen = () => {
152
+ if (settled) return;
153
+ removeAbortListener();
154
+ opened = true;
155
+ settled = true;
156
+ queue.enqueue({ kind: "peer-connected", peerId: clientPeerId });
157
+ resolve(ok(endpoint));
158
+ };
159
+ socket.onmessage = ({ data }) => {
160
+ messageTail = messageTail.then(async () => {
161
+ const bytes = await options.toBytes(data);
162
+ if (!bytes || closed) return;
163
+ if (!queue.enqueue({ kind: "message", peerId: clientPeerId, data: bytes })) {
164
+ socket.close();
165
+ }
166
+ }).catch(() => void 0);
167
+ };
168
+ socket.onerror = (cause) => {
169
+ if (opened) disconnect(`WebSocket error: ${normalizeCause(cause)}`);
170
+ else if (!settled) {
171
+ settled = true;
172
+ removeAbortListener();
173
+ resolve(connectionFailed(options.url, cause));
174
+ }
175
+ };
176
+ socket.onclose = (cause) => {
177
+ if (!opened && !settled) {
178
+ settled = true;
179
+ removeAbortListener();
180
+ resolve(connectionFailed(options.url, cause));
181
+ return;
182
+ }
183
+ disconnect(`WebSocket closed: ${normalizeCause(cause)}`);
184
+ };
185
+ });
186
+ }
187
+ function connectionFailed(address, cause) {
188
+ return err(
189
+ new EndpointError({
190
+ code: "connection-failed",
191
+ expected: ENDPOINT_EXPECTED["connection-failed"],
192
+ hint: ENDPOINT_ERROR_HINTS["connection-failed"],
193
+ detail: { address, cause: normalizeCause(cause) }
194
+ })
195
+ );
196
+ }
197
+ function alreadyClosed() {
198
+ return err(
199
+ new EndpointError({
200
+ code: "already-closed",
201
+ expected: ENDPOINT_EXPECTED["already-closed"],
202
+ hint: ENDPOINT_ERROR_HINTS["already-closed"],
203
+ detail: { cause: "The WebSocket endpoint is closed." }
204
+ })
205
+ );
206
+ }
207
+ function connectionClosed(peerId) {
208
+ return err(
209
+ new EndpointError({
210
+ code: "connection-closed",
211
+ expected: ENDPOINT_EXPECTED["connection-closed"],
212
+ hint: ENDPOINT_ERROR_HINTS["connection-closed"],
213
+ detail: { peerId }
214
+ })
215
+ );
216
+ }
217
+ function normalizeCause(cause) {
218
+ if (cause instanceof Error) return cause.message;
219
+ if (typeof cause === "string") return cause;
220
+ return "WebSocket operation failed without a platform error message.";
221
+ }
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
+
240
+ // src/browser.ts
241
+ function createWebSocketConnector(url, options = {}) {
242
+ return createWebSocketConnectorAdapter(
243
+ { WebSocket, toBytes },
244
+ url,
245
+ options
246
+ );
247
+ }
248
+ function connectWebSocketClientEndpoint(url, options = {}) {
249
+ return createWebSocketConnector(url, options).connect(new AbortController().signal);
250
+ }
251
+ function toBytes(data) {
252
+ if (data instanceof ArrayBuffer) return new Uint8Array(data);
253
+ if (typeof Blob !== "undefined" && data instanceof Blob) {
254
+ return data.arrayBuffer().then((buffer) => new Uint8Array(buffer));
255
+ }
256
+ return void 0;
257
+ }
258
+
259
+ export { connectWebSocketClientEndpoint, createWebSocketConnector };
260
+ //# sourceMappingURL=browser.mjs.map
261
+ //# sourceMappingURL=browser.mjs.map
@@ -0,0 +1 @@
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"]}
@@ -0,0 +1,13 @@
1
+ import type { EndpointEvent } from '@forgeax/engine-net';
2
+ export declare const DEFAULT_MAX_QUEUED_EVENTS = 1024;
3
+ export declare class BoundedEventQueue {
4
+ #private;
5
+ readonly maxQueuedEvents: number;
6
+ constructor(maxQueuedEvents: number);
7
+ get closed(): boolean;
8
+ get disconnectReason(): string | undefined;
9
+ enqueue(event: EndpointEvent): boolean;
10
+ close(reason: string): void;
11
+ drain(): EndpointEvent[];
12
+ }
13
+ //# sourceMappingURL=event-queue.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"event-queue.d.ts","sourceRoot":"","sources":["../src/event-queue.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD,eAAO,MAAM,yBAAyB,OAAO,CAAC;AAE9C,qBAAa,iBAAiB;;IAKhB,QAAQ,CAAC,eAAe,EAAE,MAAM;gBAAvB,eAAe,EAAE,MAAM;IAM5C,IAAI,MAAM,IAAI,OAAO,CAEpB;IAED,IAAI,gBAAgB,IAAI,MAAM,GAAG,SAAS,CAEzC;IAED,OAAO,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO;IAUtC,KAAK,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAM3B,KAAK,IAAI,aAAa,EAAE;CAGzB"}
package/dist/node.d.ts ADDED
@@ -0,0 +1,30 @@
1
+ import { EndpointError, type NetEndpoint } from '@forgeax/engine-net';
2
+ import { type Result } from '@forgeax/engine-types';
3
+ export interface ListenWebSocketEndpointOptions {
4
+ readonly port: number;
5
+ readonly host?: string;
6
+ readonly maxPeers?: number;
7
+ readonly maxQueuedEvents?: number | undefined;
8
+ }
9
+ export interface ConnectWebSocketClientEndpointOptions {
10
+ readonly maxQueuedEvents?: number | undefined;
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
+ */
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
+ */
29
+ export declare function listenWebSocketEndpoint(options: ListenWebSocketEndpointOptions): Promise<Result<NetEndpoint, EndpointError>>;
30
+ //# sourceMappingURL=node.d.ts.map
@@ -0,0 +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;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"}