@flighthq/socket 0.5.0-next.1944.cd6b6e7 → 0.5.1-edge.638.d4dbd4e
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 +2 -2
- package/dist/enableSocketGuards.js +1 -1
- package/dist/enableSocketGuards.js.map +1 -1
- package/dist/socket.d.ts +2 -4
- package/dist/socket.d.ts.map +1 -1
- package/dist/socket.js +10 -16
- package/dist/socket.js.map +1 -1
- package/package.json +4 -4
- package/src/enableSocketGuards.test.ts +16 -20
- package/src/explainSocketSendFailure.test.ts +18 -14
- package/src/socket.test.ts +56 -79
- package/src/socketHost.test.ts +57 -0
package/README.md
CHANGED
|
@@ -13,5 +13,5 @@ Import the supported application-facing API from `@flighthq/socket`. The `@fligh
|
|
|
13
13
|
This package is part of the locked-version Flight SDK graph. Applications may instead install and import `@flighthq/sdk` when package-level tree shaking is sufficient.
|
|
14
14
|
|
|
15
15
|
- [Flight project](https://github.com/flighthq/flight)
|
|
16
|
-
- [Source for @flighthq/socket@0.5.
|
|
17
|
-
- [License](https://github.com/flighthq/flight/blob/
|
|
16
|
+
- [Source for @flighthq/socket@0.5.1-edge.638.d4dbd4e](https://github.com/flighthq/flight/tree/d4dbd4e347dc8be10b180c85a766ab50333dcb51/packages/socket)
|
|
17
|
+
- [License](https://github.com/flighthq/flight/blob/d4dbd4e347dc8be10b180c85a766ab50333dcb51/LICENSE.md)
|
|
@@ -17,7 +17,7 @@ export function enableSocketGuards() {
|
|
|
17
17
|
function warnOnSocketMisuse(notice) {
|
|
18
18
|
const url = notice.socket.url;
|
|
19
19
|
const message = notice.reason === 'no-connection'
|
|
20
|
-
? 'createSocket:
|
|
20
|
+
? 'createSocket: the host carries no socket provider for this transport, or its provider returned no connection — pass a host whose net.socket supports it'
|
|
21
21
|
: `${notice.operation}: socket is already disposed — call createSocket(...) to create a new socket`;
|
|
22
22
|
logOnce(`socket:${notice.operation}:${notice.reason}`, LogLevel.Warn, { message, operation: notice.operation, reason: notice.reason, url }, 'socket');
|
|
23
23
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"enableSocketGuards.js","sourceRoot":"","sources":["../src/enableSocketGuards.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC;AAEjD,OAAO,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AAEpD,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAE1C,MAAM,UAAU,sBAAsB;IACpC,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,MAAM,UAAU,mBAAmB;IACjC,cAAc,CAAC,IAAI,CAAC,CAAC;IACrB,QAAQ,GAAG,KAAK,CAAC;AACnB,CAAC;AAED,qGAAqG;AACrG,0FAA0F;AAC1F,MAAM,UAAU,kBAAkB;IAChC,cAAc,CAAC,kBAAkB,CAAC,CAAC;IACnC,QAAQ,GAAG,IAAI,CAAC;AAClB,CAAC;AAED,SAAS,kBAAkB,CAAC,MAAmC;IAC7D,MAAM,GAAG,GAAG,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC;IAC9B,MAAM,OAAO,GACX,MAAM,CAAC,MAAM,KAAK,eAAe;QAC/B,CAAC,CAAC,
|
|
1
|
+
{"version":3,"file":"enableSocketGuards.js","sourceRoot":"","sources":["../src/enableSocketGuards.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC;AAEjD,OAAO,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AAEpD,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAE1C,MAAM,UAAU,sBAAsB;IACpC,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,MAAM,UAAU,mBAAmB;IACjC,cAAc,CAAC,IAAI,CAAC,CAAC;IACrB,QAAQ,GAAG,KAAK,CAAC;AACnB,CAAC;AAED,qGAAqG;AACrG,0FAA0F;AAC1F,MAAM,UAAU,kBAAkB;IAChC,cAAc,CAAC,kBAAkB,CAAC,CAAC;IACnC,QAAQ,GAAG,IAAI,CAAC;AAClB,CAAC;AAED,SAAS,kBAAkB,CAAC,MAAmC;IAC7D,MAAM,GAAG,GAAG,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC;IAC9B,MAAM,OAAO,GACX,MAAM,CAAC,MAAM,KAAK,eAAe;QAC/B,CAAC,CAAC,yJAAyJ;QAC3J,CAAC,CAAC,GAAG,MAAM,CAAC,SAAS,8EAA8E,CAAC;IACxG,OAAO,CACL,UAAU,MAAM,CAAC,SAAS,IAAI,MAAM,CAAC,MAAM,EAAE,EAC7C,QAAQ,CAAC,IAAI,EACb,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,EAAE,EACpE,QAAQ,CACT,CAAC;AACJ,CAAC;AAED,IAAI,QAAQ,GAAG,KAAK,CAAC"}
|
package/dist/socket.d.ts
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
|
-
import type { Socket, SocketBackend, SocketGuard, SocketOptions, SocketReadyState, SocketSignals } from '@flighthq/types/contract';
|
|
1
|
+
import type { HasNetSocket, Socket, SocketBackend, SocketGuard, SocketOptions, SocketReadyState, SocketSignals } from '@flighthq/types/contract';
|
|
2
2
|
export declare function attachSocket(socket: Socket): void;
|
|
3
3
|
export declare function closeSocket(socket: Socket, code?: number, reason?: string): void;
|
|
4
|
-
export declare function createSocket(options: Readonly<SocketOptions>): Socket;
|
|
4
|
+
export declare function createSocket(host: HasNetSocket, options: Readonly<SocketOptions>): Socket;
|
|
5
5
|
export declare function createWebSocketBackend(): SocketBackend;
|
|
6
6
|
export declare function detachSocket(socket: Socket): void;
|
|
7
7
|
export declare function disposeSocket(socket: Socket): void;
|
|
8
8
|
export declare function enableSocketSignals(socket: Socket): SocketSignals;
|
|
9
|
-
export declare function getSocketBackend(): SocketBackend;
|
|
10
9
|
export declare function getSocketReadyState(socket: Readonly<Socket>): SocketReadyState;
|
|
11
10
|
export declare function sendSocketMessage(socket: Readonly<Socket>, data: string | ArrayBuffer): boolean;
|
|
12
|
-
export declare function setSocketBackend(backend: SocketBackend | null): void;
|
|
13
11
|
export declare function setSocketGuard(guard: SocketGuard | null): void;
|
|
14
12
|
//# sourceMappingURL=socket.d.ts.map
|
package/dist/socket.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"socket.d.ts","sourceRoot":"","sources":["../src/socket.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,MAAM,EACN,aAAa,EAIb,WAAW,EAEX,aAAa,EACb,gBAAgB,EAEhB,aAAa,EACd,MAAM,0BAA0B,CAAC;AAKlC,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAGjD;AAKD,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAShF;
|
|
1
|
+
{"version":3,"file":"socket.d.ts","sourceRoot":"","sources":["../src/socket.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,YAAY,EACZ,MAAM,EACN,aAAa,EAIb,WAAW,EAEX,aAAa,EACb,gBAAgB,EAEhB,aAAa,EACd,MAAM,0BAA0B,CAAC;AAKlC,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAGjD;AAKD,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAShF;AAQD,wBAAgB,YAAY,CAAC,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,QAAQ,CAAC,aAAa,CAAC,GAAG,MAAM,CAgBzF;AAMD,wBAAgB,sBAAsB,IAAI,aAAa,CA0BtD;AAID,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAEjD;AAKD,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CASlD;AAMD,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,aAAa,CAYjE;AAGD,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,gBAAgB,CAE9E;AAKD,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,OAAO,CAQ/F;AAID,wBAAgB,cAAc,CAAC,KAAK,EAAE,WAAW,GAAG,IAAI,GAAG,IAAI,CAE9D"}
|
package/dist/socket.js
CHANGED
|
@@ -21,12 +21,13 @@ export function closeSocket(socket, code, reason) {
|
|
|
21
21
|
runtime.readyState = 'closing';
|
|
22
22
|
runtime.connection?.closeSocketConnection(code, reason);
|
|
23
23
|
}
|
|
24
|
-
// Allocates a Socket entity plus its runtime and opens the connection through the
|
|
24
|
+
// Allocates a Socket entity plus its runtime and opens the connection through the host's socket
|
|
25
|
+
// provider,
|
|
25
26
|
// wiring the backend's open/message/close/error into the socket's (still inert) signal group. The
|
|
26
27
|
// socket starts in 'connecting' and is left attached (delivering). Enable signals with
|
|
27
28
|
// enableSocketSignals to observe events. A backend that does not support the transport yields a null
|
|
28
29
|
// connection and the socket stays in 'connecting' until closed.
|
|
29
|
-
export function createSocket(options) {
|
|
30
|
+
export function createSocket(host, options) {
|
|
30
31
|
const runtime = {
|
|
31
32
|
connection: null,
|
|
32
33
|
signals: null,
|
|
@@ -35,13 +36,17 @@ export function createSocket(options) {
|
|
|
35
36
|
disposed: false,
|
|
36
37
|
};
|
|
37
38
|
const socket = { url: options.url, runtime };
|
|
38
|
-
|
|
39
|
+
// The provider comes from the host the caller passed. A host that carries no socket transport
|
|
40
|
+
// yields no connection rather than reaching a process-global fallback: absence is an answer here,
|
|
41
|
+
// and the guard reports it.
|
|
42
|
+
const backend = host.net.socket;
|
|
43
|
+
runtime.connection = backend === undefined ? null : backend.openSocket(options, makeSocketEventSink(runtime));
|
|
39
44
|
if (runtime.connection === null)
|
|
40
45
|
_guard?.({ operation: 'createSocket', reason: 'no-connection', socket });
|
|
41
46
|
return socket;
|
|
42
47
|
}
|
|
43
|
-
// Builds the
|
|
44
|
-
//
|
|
48
|
+
// Builds the web backend over the DOM WebSocket. Nothing constructs one at import time, so importing
|
|
49
|
+
// the package has no side effect; a host composes this value into its own net group. Returns a
|
|
45
50
|
// null connection when WebSocket is unavailable (non-browser host) rather than throwing; raw TCP/UDP
|
|
46
51
|
// is likewise unsupported here and only reachable through a native backend.
|
|
47
52
|
export function createWebSocketBackend() {
|
|
@@ -108,12 +113,6 @@ export function enableSocketSignals(socket) {
|
|
|
108
113
|
}
|
|
109
114
|
return runtime.signals;
|
|
110
115
|
}
|
|
111
|
-
// The active socket backend, or a lazily-created web default. There is always a backend.
|
|
112
|
-
export function getSocketBackend() {
|
|
113
|
-
if (_backend === null)
|
|
114
|
-
_backend = createWebSocketBackend();
|
|
115
|
-
return _backend;
|
|
116
|
-
}
|
|
117
116
|
// The socket's current connection phase, tracked on the runtime from backend events and closeSocket.
|
|
118
117
|
export function getSocketReadyState(socket) {
|
|
119
118
|
return socket.runtime.readyState;
|
|
@@ -131,16 +130,11 @@ export function sendSocketMessage(socket, data) {
|
|
|
131
130
|
return false;
|
|
132
131
|
return runtime.connection.sendSocketFrame(data);
|
|
133
132
|
}
|
|
134
|
-
// Installs a native host socket backend (adding TCP/UDP); pass null to fall back to the web default.
|
|
135
|
-
export function setSocketBackend(backend) {
|
|
136
|
-
_backend = backend;
|
|
137
|
-
}
|
|
138
133
|
// Installs the diagnostics hook used by the separately imported enableSocketGuards module. Null is
|
|
139
134
|
// the production default and removes all guard-path work except the branch.
|
|
140
135
|
export function setSocketGuard(guard) {
|
|
141
136
|
_guard = guard;
|
|
142
137
|
}
|
|
143
|
-
let _backend = null;
|
|
144
138
|
let _guard = null;
|
|
145
139
|
// Builds the backend→entity sink bound to one socket's runtime: it updates readyState and emits the
|
|
146
140
|
// opt-in signals. Every handler is a no-op once the runtime stops delivering (detach/dispose), so a
|
package/dist/socket.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"socket.js","sourceRoot":"","sources":["../src/socket.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAC;
|
|
1
|
+
{"version":3,"file":"socket.js","sourceRoot":"","sources":["../src/socket.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flighthq/socket",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1-edge.638.d4dbd4e",
|
|
4
4
|
"author": "Joshua Granick and other contributors",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -38,9 +38,9 @@
|
|
|
38
38
|
"clean:dist": "tsx ../../scripts/clean-package-dist.ts"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@flighthq/log": "0.5.
|
|
42
|
-
"@flighthq/signals": "0.5.
|
|
43
|
-
"@flighthq/types": "0.5.
|
|
41
|
+
"@flighthq/log": "0.5.1-edge.638.d4dbd4e",
|
|
42
|
+
"@flighthq/signals": "0.5.1-edge.638.d4dbd4e",
|
|
43
|
+
"@flighthq/types": "0.5.1-edge.638.d4dbd4e"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"typescript": "^5.3.0"
|
|
@@ -1,15 +1,8 @@
|
|
|
1
1
|
import { addLogSink, createMemoryLogSink, getMemoryLogSinkEntries, removeLogSink } from '@flighthq/log/contract';
|
|
2
|
-
import type { LogEntry, SocketBackend } from '@flighthq/types/contract';
|
|
2
|
+
import type { HasNetSocket, LogEntry, SocketBackend } from '@flighthq/types/contract';
|
|
3
3
|
|
|
4
4
|
import { areSocketGuardsEnabled, disableSocketGuards, enableSocketGuards } from './enableSocketGuards';
|
|
5
|
-
import {
|
|
6
|
-
closeSocket,
|
|
7
|
-
createSocket,
|
|
8
|
-
disposeSocket,
|
|
9
|
-
enableSocketSignals,
|
|
10
|
-
sendSocketMessage,
|
|
11
|
-
setSocketBackend,
|
|
12
|
-
} from './socket';
|
|
5
|
+
import { closeSocket, createSocket, disposeSocket, enableSocketSignals, sendSocketMessage } from './socket';
|
|
13
6
|
|
|
14
7
|
function captureLog(run: () => void): readonly LogEntry[] {
|
|
15
8
|
const sink = createMemoryLogSink(8);
|
|
@@ -33,9 +26,12 @@ function backend(hasConnection: boolean): SocketBackend {
|
|
|
33
26
|
|
|
34
27
|
afterEach(() => {
|
|
35
28
|
disableSocketGuards();
|
|
36
|
-
setSocketBackend(null);
|
|
37
29
|
});
|
|
38
30
|
|
|
31
|
+
function hostOf(backend: SocketBackend): HasNetSocket {
|
|
32
|
+
return { net: { socket: backend } } as HasNetSocket;
|
|
33
|
+
}
|
|
34
|
+
|
|
39
35
|
describe('areSocketGuardsEnabled', () => {
|
|
40
36
|
it('reports whether the global guard hook is installed', () => {
|
|
41
37
|
enableSocketGuards();
|
|
@@ -49,18 +45,18 @@ describe('disableSocketGuards', () => {
|
|
|
49
45
|
it('restores silent core behavior', () => {
|
|
50
46
|
enableSocketGuards();
|
|
51
47
|
disableSocketGuards();
|
|
52
|
-
|
|
53
|
-
expect(captureLog(() => createSocket({ url: 'tcp://silent' }))).toEqual([]);
|
|
48
|
+
const host = hostOf(backend(false));
|
|
49
|
+
expect(captureLog(() => createSocket(host, { url: 'tcp://silent' }))).toEqual([]);
|
|
54
50
|
});
|
|
55
51
|
});
|
|
56
52
|
|
|
57
53
|
describe('enableSocketGuards', () => {
|
|
58
54
|
it('warns once when socket creation produces no connection', () => {
|
|
59
55
|
enableSocketGuards();
|
|
60
|
-
|
|
56
|
+
const host = hostOf(backend(false));
|
|
61
57
|
const entries = captureLog(() => {
|
|
62
|
-
createSocket({ url: 'tcp://unsupported' });
|
|
63
|
-
createSocket({ url: 'tcp://unsupported-again' });
|
|
58
|
+
createSocket(host, { url: 'tcp://unsupported' });
|
|
59
|
+
createSocket(host, { url: 'tcp://unsupported-again' });
|
|
64
60
|
});
|
|
65
61
|
expect(entries).toHaveLength(1);
|
|
66
62
|
expect(entries[0]).toMatchObject({ channel: 'socket' });
|
|
@@ -69,13 +65,13 @@ describe('enableSocketGuards', () => {
|
|
|
69
65
|
reason: 'no-connection',
|
|
70
66
|
url: 'tcp://unsupported',
|
|
71
67
|
});
|
|
72
|
-
expect(String((entries[0].data as Record<string, unknown>).message)).toContain('
|
|
68
|
+
expect(String((entries[0].data as Record<string, unknown>).message)).toContain('net.socket');
|
|
73
69
|
});
|
|
74
70
|
|
|
75
71
|
it('warns once per command issued after terminal disposal', () => {
|
|
76
72
|
enableSocketGuards();
|
|
77
|
-
|
|
78
|
-
const socket = createSocket({ url: 'ws://disposed' });
|
|
73
|
+
const host = hostOf(backend(true));
|
|
74
|
+
const socket = createSocket(host, { url: 'ws://disposed' });
|
|
79
75
|
disposeSocket(socket);
|
|
80
76
|
const entries = captureLog(() => {
|
|
81
77
|
closeSocket(socket);
|
|
@@ -97,8 +93,8 @@ describe('enableSocketGuards', () => {
|
|
|
97
93
|
|
|
98
94
|
it('stays silent for ordinary pre-open sends and closes', () => {
|
|
99
95
|
enableSocketGuards();
|
|
100
|
-
|
|
101
|
-
const socket = createSocket({ url: 'ws://connecting' });
|
|
96
|
+
const host = hostOf(backend(true));
|
|
97
|
+
const socket = createSocket(host, { url: 'ws://connecting' });
|
|
102
98
|
const entries = captureLog(() => {
|
|
103
99
|
sendSocketMessage(socket, 'early');
|
|
104
100
|
closeSocket(socket);
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import type { SocketBackend, SocketEventSink } from '@flighthq/types/contract';
|
|
1
|
+
import type { HasNetSocket, SocketBackend, SocketEventSink } from '@flighthq/types/contract';
|
|
2
2
|
|
|
3
3
|
import { explainSocketSendFailure } from './explainSocketSendFailure';
|
|
4
|
-
import { createSocket, disposeSocket, sendSocketMessage
|
|
4
|
+
import { createSocket, disposeSocket, sendSocketMessage } from './socket';
|
|
5
5
|
|
|
6
|
-
function installBackend(
|
|
6
|
+
function installBackend(
|
|
7
|
+
hasConnection: boolean,
|
|
8
|
+
sendResult = true,
|
|
9
|
+
): { host: HasNetSocket; sink: () => SocketEventSink } {
|
|
7
10
|
let sink!: SocketEventSink;
|
|
8
11
|
const backend: SocketBackend = {
|
|
9
12
|
openSocket(_options, events) {
|
|
@@ -15,16 +18,17 @@ function installBackend(hasConnection: boolean, sendResult = true): () => Socket
|
|
|
15
18
|
};
|
|
16
19
|
},
|
|
17
20
|
};
|
|
18
|
-
|
|
19
|
-
return () => sink;
|
|
21
|
+
return { host: hostOf(backend), sink: () => sink };
|
|
20
22
|
}
|
|
21
23
|
|
|
22
|
-
|
|
24
|
+
function hostOf(backend: SocketBackend): HasNetSocket {
|
|
25
|
+
return { net: { socket: backend } } as HasNetSocket;
|
|
26
|
+
}
|
|
23
27
|
|
|
24
28
|
describe('explainSocketSendFailure', () => {
|
|
25
29
|
it('identifies an unsupported backend with no connection', () => {
|
|
26
|
-
installBackend(false);
|
|
27
|
-
const socket = createSocket({ url: 'tcp://host' });
|
|
30
|
+
const { host } = installBackend(false);
|
|
31
|
+
const socket = createSocket(host, { url: 'tcp://host' });
|
|
28
32
|
expect(explainSocketSendFailure(socket)).toEqual({
|
|
29
33
|
reason: 'no-connection',
|
|
30
34
|
readyState: 'connecting',
|
|
@@ -33,8 +37,8 @@ describe('explainSocketSendFailure', () => {
|
|
|
33
37
|
});
|
|
34
38
|
|
|
35
39
|
it('reports the current non-open phase when a connection exists', () => {
|
|
36
|
-
installBackend(true);
|
|
37
|
-
const socket = createSocket({ url: 'ws://host' });
|
|
40
|
+
const { host } = installBackend(true);
|
|
41
|
+
const socket = createSocket(host, { url: 'ws://host' });
|
|
38
42
|
expect(explainSocketSendFailure(socket)).toEqual({
|
|
39
43
|
reason: 'not-open',
|
|
40
44
|
readyState: 'connecting',
|
|
@@ -43,16 +47,16 @@ describe('explainSocketSendFailure', () => {
|
|
|
43
47
|
});
|
|
44
48
|
|
|
45
49
|
it('returns null once the socket can reach its backend, even when that backend returns false', () => {
|
|
46
|
-
const getSink = installBackend(true, false);
|
|
47
|
-
const socket = createSocket({ url: 'ws://host' });
|
|
50
|
+
const { host, sink: getSink } = installBackend(true, false);
|
|
51
|
+
const socket = createSocket(host, { url: 'ws://host' });
|
|
48
52
|
getSink().handleSocketOpen();
|
|
49
53
|
expect(sendSocketMessage(socket, 'frame')).toBe(false);
|
|
50
54
|
expect(explainSocketSendFailure(socket)).toBeNull();
|
|
51
55
|
});
|
|
52
56
|
|
|
53
57
|
it('distinguishes terminal disposal from other closed or disconnected states', () => {
|
|
54
|
-
installBackend(true);
|
|
55
|
-
const socket = createSocket({ url: 'ws://host' });
|
|
58
|
+
const { host } = installBackend(true);
|
|
59
|
+
const socket = createSocket(host, { url: 'ws://host' });
|
|
56
60
|
disposeSocket(socket);
|
|
57
61
|
expect(explainSocketSendFailure(socket)).toEqual({
|
|
58
62
|
reason: 'disposed',
|
package/src/socket.test.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { connectSignal } from '@flighthq/signals/contract';
|
|
2
2
|
import type {
|
|
3
|
+
HasNetSocket,
|
|
3
4
|
SocketBackend,
|
|
4
5
|
SocketCloseInfo,
|
|
5
6
|
SocketConnection,
|
|
@@ -15,10 +16,8 @@ import {
|
|
|
15
16
|
detachSocket,
|
|
16
17
|
disposeSocket,
|
|
17
18
|
enableSocketSignals,
|
|
18
|
-
getSocketBackend,
|
|
19
19
|
getSocketReadyState,
|
|
20
20
|
sendSocketMessage,
|
|
21
|
-
setSocketBackend,
|
|
22
21
|
setSocketGuard,
|
|
23
22
|
} from './socket';
|
|
24
23
|
|
|
@@ -65,15 +64,18 @@ function fakeBackend(): FakeSocket {
|
|
|
65
64
|
}
|
|
66
65
|
|
|
67
66
|
afterEach(() => {
|
|
68
|
-
setSocketBackend(null);
|
|
69
67
|
setSocketGuard(null);
|
|
70
68
|
});
|
|
71
69
|
|
|
70
|
+
function hostOf(backend: SocketBackend): HasNetSocket {
|
|
71
|
+
return { net: { socket: backend } } as HasNetSocket;
|
|
72
|
+
}
|
|
73
|
+
|
|
72
74
|
describe('attachSocket', () => {
|
|
73
75
|
it('resumes delivery after a detach', () => {
|
|
74
76
|
const fake = fakeBackend();
|
|
75
|
-
|
|
76
|
-
const socket = createSocket({ url: 'ws://x' });
|
|
77
|
+
const host = hostOf(fake.backend);
|
|
78
|
+
const socket = createSocket(host, { url: 'ws://x' });
|
|
77
79
|
const signals = enableSocketSignals(socket);
|
|
78
80
|
let opens = 0;
|
|
79
81
|
connectSignal(signals.onSocketOpen, () => opens++);
|
|
@@ -85,8 +87,8 @@ describe('attachSocket', () => {
|
|
|
85
87
|
|
|
86
88
|
it('cannot resume delivery after terminal disposal', () => {
|
|
87
89
|
const fake = fakeBackend();
|
|
88
|
-
|
|
89
|
-
const socket = createSocket({ url: 'ws://x' });
|
|
90
|
+
const host = hostOf(fake.backend);
|
|
91
|
+
const socket = createSocket(host, { url: 'ws://x' });
|
|
90
92
|
const signals = enableSocketSignals(socket);
|
|
91
93
|
let opens = 0;
|
|
92
94
|
connectSignal(signals.onSocketOpen, () => opens++);
|
|
@@ -101,8 +103,8 @@ describe('attachSocket', () => {
|
|
|
101
103
|
describe('closeSocket', () => {
|
|
102
104
|
it('transitions to closing and forwards code/reason to the connection', () => {
|
|
103
105
|
const fake = fakeBackend();
|
|
104
|
-
|
|
105
|
-
const socket = createSocket({ url: 'ws://x' });
|
|
106
|
+
const host = hostOf(fake.backend);
|
|
107
|
+
const socket = createSocket(host, { url: 'ws://x' });
|
|
106
108
|
fake.sink.handleSocketOpen();
|
|
107
109
|
closeSocket(socket, 1000, 'bye');
|
|
108
110
|
expect(getSocketReadyState(socket)).toBe('closing');
|
|
@@ -111,8 +113,8 @@ describe('closeSocket', () => {
|
|
|
111
113
|
|
|
112
114
|
it('reaches closed once the backend close event arrives', () => {
|
|
113
115
|
const fake = fakeBackend();
|
|
114
|
-
|
|
115
|
-
const socket = createSocket({ url: 'ws://x' });
|
|
116
|
+
const host = hostOf(fake.backend);
|
|
117
|
+
const socket = createSocket(host, { url: 'ws://x' });
|
|
116
118
|
fake.sink.handleSocketOpen();
|
|
117
119
|
closeSocket(socket);
|
|
118
120
|
fake.sink.handleSocketClose({ code: 1000, reason: '', wasClean: true });
|
|
@@ -121,8 +123,8 @@ describe('closeSocket', () => {
|
|
|
121
123
|
|
|
122
124
|
it('is a no-op when already closed', () => {
|
|
123
125
|
const fake = fakeBackend();
|
|
124
|
-
|
|
125
|
-
const socket = createSocket({ url: 'ws://x' });
|
|
126
|
+
const host = hostOf(fake.backend);
|
|
127
|
+
const socket = createSocket(host, { url: 'ws://x' });
|
|
126
128
|
fake.sink.handleSocketOpen();
|
|
127
129
|
closeSocket(socket);
|
|
128
130
|
fake.sink.handleSocketClose({ code: 1000, reason: '', wasClean: true });
|
|
@@ -132,8 +134,8 @@ describe('closeSocket', () => {
|
|
|
132
134
|
|
|
133
135
|
it('stays closed and does not close the connection again after disposal', () => {
|
|
134
136
|
const fake = fakeBackend();
|
|
135
|
-
|
|
136
|
-
const socket = createSocket({ url: 'ws://x' });
|
|
137
|
+
const host = hostOf(fake.backend);
|
|
138
|
+
const socket = createSocket(host, { url: 'ws://x' });
|
|
137
139
|
disposeSocket(socket);
|
|
138
140
|
closeSocket(socket);
|
|
139
141
|
expect(getSocketReadyState(socket)).toBe('closed');
|
|
@@ -144,8 +146,8 @@ describe('closeSocket', () => {
|
|
|
144
146
|
describe('createSocket', () => {
|
|
145
147
|
it('opens through the backend in the connecting state and records the url', () => {
|
|
146
148
|
const fake = fakeBackend();
|
|
147
|
-
|
|
148
|
-
const socket = createSocket({ url: 'ws://host/path' });
|
|
149
|
+
const host = hostOf(fake.backend);
|
|
150
|
+
const socket = createSocket(host, { url: 'ws://host/path' });
|
|
149
151
|
expect(socket.url).toBe('ws://host/path');
|
|
150
152
|
expect(getSocketReadyState(socket)).toBe('connecting');
|
|
151
153
|
expect(fake.lastOptions?.url).toBe('ws://host/path');
|
|
@@ -153,8 +155,8 @@ describe('createSocket', () => {
|
|
|
153
155
|
|
|
154
156
|
it('passes protocols and binaryType through to the backend', () => {
|
|
155
157
|
const fake = fakeBackend();
|
|
156
|
-
|
|
157
|
-
createSocket({ url: 'ws://x', protocols: ['a', 'b'], binaryType: 'arraybuffer' });
|
|
158
|
+
const host = hostOf(fake.backend);
|
|
159
|
+
createSocket(host, { url: 'ws://x', protocols: ['a', 'b'], binaryType: 'arraybuffer' });
|
|
158
160
|
expect(fake.lastOptions?.protocols).toEqual(['a', 'b']);
|
|
159
161
|
expect(fake.lastOptions?.binaryType).toBe('arraybuffer');
|
|
160
162
|
});
|
|
@@ -162,16 +164,16 @@ describe('createSocket', () => {
|
|
|
162
164
|
it('tolerates a null connection from an unsupported transport', () => {
|
|
163
165
|
const fake = fakeBackend();
|
|
164
166
|
fake.openReturnsNull = true;
|
|
165
|
-
|
|
166
|
-
const socket = createSocket({ url: 'tcp://x' });
|
|
167
|
+
const host = hostOf(fake.backend);
|
|
168
|
+
const socket = createSocket(host, { url: 'tcp://x' });
|
|
167
169
|
expect(getSocketReadyState(socket)).toBe('connecting');
|
|
168
170
|
expect(sendSocketMessage(socket, 'x')).toBe(false);
|
|
169
171
|
});
|
|
170
172
|
|
|
171
173
|
it('emits a text message with binary false and a binary message with binary true', () => {
|
|
172
174
|
const fake = fakeBackend();
|
|
173
|
-
|
|
174
|
-
const socket = createSocket({ url: 'ws://x' });
|
|
175
|
+
const host = hostOf(fake.backend);
|
|
176
|
+
const socket = createSocket(host, { url: 'ws://x' });
|
|
175
177
|
const signals = enableSocketSignals(socket);
|
|
176
178
|
const received: SocketMessage[] = [];
|
|
177
179
|
connectSignal(signals.onSocketMessage, (m) => received.push(m));
|
|
@@ -187,8 +189,8 @@ describe('createSocket', () => {
|
|
|
187
189
|
|
|
188
190
|
it('emits close info with code, reason, and wasClean', () => {
|
|
189
191
|
const fake = fakeBackend();
|
|
190
|
-
|
|
191
|
-
const socket = createSocket({ url: 'ws://x' });
|
|
192
|
+
const host = hostOf(fake.backend);
|
|
193
|
+
const socket = createSocket(host, { url: 'ws://x' });
|
|
192
194
|
const signals = enableSocketSignals(socket);
|
|
193
195
|
const infos: SocketCloseInfo[] = [];
|
|
194
196
|
connectSignal(signals.onSocketClose, (i) => infos.push(i));
|
|
@@ -198,8 +200,8 @@ describe('createSocket', () => {
|
|
|
198
200
|
|
|
199
201
|
it('emits onSocketError', () => {
|
|
200
202
|
const fake = fakeBackend();
|
|
201
|
-
|
|
202
|
-
const socket = createSocket({ url: 'ws://x' });
|
|
203
|
+
const host = hostOf(fake.backend);
|
|
204
|
+
const socket = createSocket(host, { url: 'ws://x' });
|
|
203
205
|
const signals = enableSocketSignals(socket);
|
|
204
206
|
let errors = 0;
|
|
205
207
|
connectSignal(signals.onSocketError, () => errors++);
|
|
@@ -302,8 +304,8 @@ describe('createWebSocketBackend', () => {
|
|
|
302
304
|
describe('detachSocket', () => {
|
|
303
305
|
it('stops backend events from reaching the signals', () => {
|
|
304
306
|
const fake = fakeBackend();
|
|
305
|
-
|
|
306
|
-
const socket = createSocket({ url: 'ws://x' });
|
|
307
|
+
const host = hostOf(fake.backend);
|
|
308
|
+
const socket = createSocket(host, { url: 'ws://x' });
|
|
307
309
|
const signals = enableSocketSignals(socket);
|
|
308
310
|
let opens = 0;
|
|
309
311
|
connectSignal(signals.onSocketOpen, () => opens++);
|
|
@@ -316,8 +318,8 @@ describe('detachSocket', () => {
|
|
|
316
318
|
describe('disposeSocket', () => {
|
|
317
319
|
it('closes an open connection and detaches so later events fire no signal', () => {
|
|
318
320
|
const fake = fakeBackend();
|
|
319
|
-
|
|
320
|
-
const socket = createSocket({ url: 'ws://x' });
|
|
321
|
+
const host = hostOf(fake.backend);
|
|
322
|
+
const socket = createSocket(host, { url: 'ws://x' });
|
|
321
323
|
const signals = enableSocketSignals(socket);
|
|
322
324
|
let messages = 0;
|
|
323
325
|
connectSignal(signals.onSocketMessage, () => messages++);
|
|
@@ -332,8 +334,8 @@ describe('disposeSocket', () => {
|
|
|
332
334
|
|
|
333
335
|
it('is safe to call on a fresh socket', () => {
|
|
334
336
|
const fake = fakeBackend();
|
|
335
|
-
|
|
336
|
-
const socket = createSocket({ url: 'ws://x' });
|
|
337
|
+
const host = hostOf(fake.backend);
|
|
338
|
+
const socket = createSocket(host, { url: 'ws://x' });
|
|
337
339
|
expect(() => disposeSocket(socket)).not.toThrow();
|
|
338
340
|
});
|
|
339
341
|
});
|
|
@@ -341,22 +343,22 @@ describe('disposeSocket', () => {
|
|
|
341
343
|
describe('enableSocketSignals', () => {
|
|
342
344
|
it('returns the same group on repeated calls', () => {
|
|
343
345
|
const fake = fakeBackend();
|
|
344
|
-
|
|
345
|
-
const socket = createSocket({ url: 'ws://x' });
|
|
346
|
+
const host = hostOf(fake.backend);
|
|
347
|
+
const socket = createSocket(host, { url: 'ws://x' });
|
|
346
348
|
expect(enableSocketSignals(socket)).toBe(enableSocketSignals(socket));
|
|
347
349
|
});
|
|
348
350
|
|
|
349
351
|
it('leaves a bare socket without signals', () => {
|
|
350
352
|
const fake = fakeBackend();
|
|
351
|
-
|
|
352
|
-
const socket = createSocket({ url: 'ws://x' });
|
|
353
|
+
const host = hostOf(fake.backend);
|
|
354
|
+
const socket = createSocket(host, { url: 'ws://x' });
|
|
353
355
|
expect(socket.runtime.signals).toBeNull();
|
|
354
356
|
});
|
|
355
357
|
|
|
356
358
|
it('returns an inert stable group without reviving a disposed socket', () => {
|
|
357
359
|
const fake = fakeBackend();
|
|
358
|
-
|
|
359
|
-
const socket = createSocket({ url: 'ws://x' });
|
|
360
|
+
const host = hostOf(fake.backend);
|
|
361
|
+
const socket = createSocket(host, { url: 'ws://x' });
|
|
360
362
|
disposeSocket(socket);
|
|
361
363
|
const first = enableSocketSignals(socket);
|
|
362
364
|
expect(enableSocketSignals(socket)).toBe(first);
|
|
@@ -365,23 +367,11 @@ describe('enableSocketSignals', () => {
|
|
|
365
367
|
});
|
|
366
368
|
});
|
|
367
369
|
|
|
368
|
-
describe('getSocketBackend', () => {
|
|
369
|
-
it('lazily returns a web backend by default', () => {
|
|
370
|
-
expect(typeof getSocketBackend().openSocket).toBe('function');
|
|
371
|
-
});
|
|
372
|
-
|
|
373
|
-
it('returns the installed backend', () => {
|
|
374
|
-
const fake = fakeBackend();
|
|
375
|
-
setSocketBackend(fake.backend);
|
|
376
|
-
expect(getSocketBackend()).toBe(fake.backend);
|
|
377
|
-
});
|
|
378
|
-
});
|
|
379
|
-
|
|
380
370
|
describe('getSocketReadyState', () => {
|
|
381
371
|
it('reflects connecting → open → closing → closed transitions', () => {
|
|
382
372
|
const fake = fakeBackend();
|
|
383
|
-
|
|
384
|
-
const socket = createSocket({ url: 'ws://x' });
|
|
373
|
+
const host = hostOf(fake.backend);
|
|
374
|
+
const socket = createSocket(host, { url: 'ws://x' });
|
|
385
375
|
expect(getSocketReadyState(socket)).toBe('connecting');
|
|
386
376
|
fake.sink.handleSocketOpen();
|
|
387
377
|
expect(getSocketReadyState(socket)).toBe('open');
|
|
@@ -395,8 +385,8 @@ describe('getSocketReadyState', () => {
|
|
|
395
385
|
describe('sendSocketMessage', () => {
|
|
396
386
|
it('sends through the connection and returns true when open', () => {
|
|
397
387
|
const fake = fakeBackend();
|
|
398
|
-
|
|
399
|
-
const socket = createSocket({ url: 'ws://x' });
|
|
388
|
+
const host = hostOf(fake.backend);
|
|
389
|
+
const socket = createSocket(host, { url: 'ws://x' });
|
|
400
390
|
fake.sink.handleSocketOpen();
|
|
401
391
|
expect(sendSocketMessage(socket, 'ping')).toBe(true);
|
|
402
392
|
expect(fake.sent).toEqual(['ping']);
|
|
@@ -404,16 +394,16 @@ describe('sendSocketMessage', () => {
|
|
|
404
394
|
|
|
405
395
|
it('returns false without throwing when not open', () => {
|
|
406
396
|
const fake = fakeBackend();
|
|
407
|
-
|
|
408
|
-
const socket = createSocket({ url: 'ws://x' });
|
|
397
|
+
const host = hostOf(fake.backend);
|
|
398
|
+
const socket = createSocket(host, { url: 'ws://x' });
|
|
409
399
|
expect(sendSocketMessage(socket, 'ping')).toBe(false);
|
|
410
400
|
expect(fake.sent).toEqual([]);
|
|
411
401
|
});
|
|
412
402
|
|
|
413
403
|
it('returns false after disposal without reaching the released connection', () => {
|
|
414
404
|
const fake = fakeBackend();
|
|
415
|
-
|
|
416
|
-
const socket = createSocket({ url: 'ws://x' });
|
|
405
|
+
const host = hostOf(fake.backend);
|
|
406
|
+
const socket = createSocket(host, { url: 'ws://x' });
|
|
417
407
|
fake.sink.handleSocketOpen();
|
|
418
408
|
disposeSocket(socket);
|
|
419
409
|
expect(sendSocketMessage(socket, 'ping')).toBe(false);
|
|
@@ -423,8 +413,8 @@ describe('sendSocketMessage', () => {
|
|
|
423
413
|
it('returns false for an open state whose backend supplied no connection', () => {
|
|
424
414
|
const fake = fakeBackend();
|
|
425
415
|
fake.openReturnsNull = true;
|
|
426
|
-
|
|
427
|
-
const socket = createSocket({ url: 'tcp://x' });
|
|
416
|
+
const host = hostOf(fake.backend);
|
|
417
|
+
const socket = createSocket(host, { url: 'tcp://x' });
|
|
428
418
|
fake.sink.handleSocketOpen();
|
|
429
419
|
expect(getSocketReadyState(socket)).toBe('open');
|
|
430
420
|
expect(sendSocketMessage(socket, 'ping')).toBe(false);
|
|
@@ -433,8 +423,8 @@ describe('sendSocketMessage', () => {
|
|
|
433
423
|
it('forwards binary data by identity and propagates false without mutating the readonly socket', () => {
|
|
434
424
|
const fake = fakeBackend();
|
|
435
425
|
fake.sendReturns = false;
|
|
436
|
-
|
|
437
|
-
const socket = createSocket({ url: 'ws://x' });
|
|
426
|
+
const host = hostOf(fake.backend);
|
|
427
|
+
const socket = createSocket(host, { url: 'ws://x' });
|
|
438
428
|
fake.sink.handleSocketOpen();
|
|
439
429
|
Object.freeze(socket.runtime);
|
|
440
430
|
Object.freeze(socket);
|
|
@@ -444,30 +434,17 @@ describe('sendSocketMessage', () => {
|
|
|
444
434
|
});
|
|
445
435
|
});
|
|
446
436
|
|
|
447
|
-
describe('setSocketBackend', () => {
|
|
448
|
-
it('restores the lazy web default when passed null', () => {
|
|
449
|
-
const fake = fakeBackend();
|
|
450
|
-
setSocketBackend(fake.backend);
|
|
451
|
-
expect(getSocketBackend()).toBe(fake.backend);
|
|
452
|
-
setSocketBackend(null);
|
|
453
|
-
const web = getSocketBackend();
|
|
454
|
-
expect(web).not.toBe(fake.backend);
|
|
455
|
-
expect(typeof web.openSocket).toBe('function');
|
|
456
|
-
});
|
|
457
|
-
});
|
|
458
|
-
|
|
459
437
|
describe('setSocketGuard', () => {
|
|
460
438
|
it('installs and removes the core notice hook', () => {
|
|
461
439
|
const notices: string[] = [];
|
|
462
440
|
setSocketGuard((notice) => notices.push(`${notice.operation}:${notice.reason}`));
|
|
463
441
|
const unsupported = fakeBackend();
|
|
464
442
|
unsupported.openReturnsNull = true;
|
|
465
|
-
|
|
466
|
-
createSocket({ url: 'tcp://x' });
|
|
443
|
+
const host = hostOf(unsupported.backend);
|
|
444
|
+
createSocket(host, { url: 'tcp://x' });
|
|
467
445
|
|
|
468
446
|
const supported = fakeBackend();
|
|
469
|
-
|
|
470
|
-
const socket = createSocket({ url: 'ws://x' });
|
|
447
|
+
const socket = createSocket(hostOf(supported.backend), { url: 'ws://x' });
|
|
471
448
|
disposeSocket(socket);
|
|
472
449
|
closeSocket(socket);
|
|
473
450
|
sendSocketMessage(socket, 'x');
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { HasNetSocket, SocketBackend, SocketConnection, SocketEventSink } from '@flighthq/types/contract';
|
|
2
|
+
|
|
3
|
+
import { createSocket, getSocketReadyState } from './socket';
|
|
4
|
+
|
|
5
|
+
function recordingBackend(label: string, opened: string[]): SocketBackend {
|
|
6
|
+
return {
|
|
7
|
+
openSocket(_options, events: SocketEventSink): SocketConnection | null {
|
|
8
|
+
opened.push(label);
|
|
9
|
+
return {
|
|
10
|
+
closeSocketConnection(): void {},
|
|
11
|
+
sendSocketFrame(): boolean {
|
|
12
|
+
events.handleSocketOpen();
|
|
13
|
+
return true;
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function hostWith(backend: SocketBackend | undefined): HasNetSocket {
|
|
21
|
+
return { net: backend === undefined ? {} : { socket: backend } } as HasNetSocket;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe('createSocket', () => {
|
|
25
|
+
it('opens through the provider carried by the host it is given', () => {
|
|
26
|
+
const opened: string[] = [];
|
|
27
|
+
createSocket(hostWith(recordingBackend('a', opened)), { url: 'wss://example.test' });
|
|
28
|
+
expect(opened).toEqual(['a']);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// ★ HOST ISOLATION. Two hosts, two providers, two answers. Under the ambient resolver one
|
|
32
|
+
// process-wide backend answered for every caller, so the second host was unreachable no matter what
|
|
33
|
+
// it carried — this is the property the migration exists to create.
|
|
34
|
+
it('keeps two hosts independent', () => {
|
|
35
|
+
const opened: string[] = [];
|
|
36
|
+
createSocket(hostWith(recordingBackend('first', opened)), { url: 'wss://one.test' });
|
|
37
|
+
createSocket(hostWith(recordingBackend('second', opened)), { url: 'wss://two.test' });
|
|
38
|
+
expect(opened).toEqual(['first', 'second']);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// ★ NO AMBIENT FALLBACK. A host that carries no socket provider must yield no connection rather than
|
|
42
|
+
// quietly reaching a process-global web backend. The socket stays 'connecting', which is the
|
|
43
|
+
// documented shape for a backend that cannot open the transport.
|
|
44
|
+
it('yields no connection when the host carries no socket provider', () => {
|
|
45
|
+
const socket = createSocket(hostWith(undefined), { url: 'wss://absent.test' });
|
|
46
|
+
expect(socket.runtime.connection).toBeNull();
|
|
47
|
+
expect(getSocketReadyState(socket)).toBe('connecting');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('does not consult one host provider when another host is passed', () => {
|
|
51
|
+
const opened: string[] = [];
|
|
52
|
+
const unused = recordingBackend('unused', opened);
|
|
53
|
+
createSocket(hostWith(recordingBackend('used', opened)), { url: 'wss://x.test' });
|
|
54
|
+
expect(opened).not.toContain('unused');
|
|
55
|
+
expect(unused).toBeDefined();
|
|
56
|
+
});
|
|
57
|
+
});
|