diff --git a/constants.php b/constants.php
index 5130f729519d9fe0320208c5889a8807e81ab1f6..8478d6e0d00034ecada9e22ec4b80127e0af0940 100644
--- a/constants.php
+++ b/constants.php
@@ -5,6 +5,7 @@ declare(strict_types=1);
 define('DM_APP_ROOT', __DIR__.'/app');
 define('DM_BATCH_PROMISE_TIMEOUT', 0.3);
 define('DM_GRAPHQL_PROMISE_TIMEOUT', 0.2);
+define('DM_POOL_CONNECTION_TIMEOUT', 0.1);
 define('DM_PUBLIC_ROOT', __DIR__.'/public');
 define('DM_RESONANCE_ROOT', __DIR__.'/src');
 define('DM_ROOT', __DIR__);
diff --git a/docs/pages/docs/features/ai/index.md b/docs/pages/docs/features/ai/index.md
index 4413ef18602e3fa244958042d655bcbbb9e45ebc..747e70a4ea36f5da626a6fcc89fe66176daa05e8 100644
--- a/docs/pages/docs/features/ai/index.md
+++ b/docs/pages/docs/features/ai/index.md
@@ -3,9 +3,11 @@ collections:
     - documents
 layout: dm:document
 parent: docs/features/index
-title: AI
+title: Artificial Intelligence
 description: >
     Use integration features to serve or use AI models.
 ---
 
+# Artificial Intelligence
+
 {{docs/features/ai/*/index}}
diff --git a/docs/pages/docs/features/ai/llama-cpp/index.md b/docs/pages/docs/features/ai/llama-cpp/index.md
index faa10672922c9a73b8be4af5ce235fd8fab03393..49ecacbb27ea9d33e974ad3a7c0e476ce2142938 100644
--- a/docs/pages/docs/features/ai/llama-cpp/index.md
+++ b/docs/pages/docs/features/ai/llama-cpp/index.md
@@ -20,7 +20,7 @@ You can use Resonance to connect with it and process LLM responses.
 
 # Usage
 
-You can also check the tutorial: {{tutorials/connect-to-llama-cpp/index}}
+You can also check the tutorial: {{tutorials/how-to-serve-llm-completions/index}}
 
 ## Configuration
 
@@ -71,3 +71,30 @@ class LlamaCppGenerate
     }
 }
 ```
+
+## Stopping Completion Generator
+
+:::danger
+Using just the `break` keyword does not stop the completion request. You need
+to use `$completion->send(...)`. 
+
+See also: [Generator::send(...)](https://www.php.net/manual/en/generator.send.php)
+:::
+
+For example, stops after generating 10 tokens:
+
+```php
+use Distantmagic\Resonance\LlamaCppCompletionCommand;
+
+$i = 0;
+
+foreach ($completion as $token) {
+    if ($i > 9) {
+        $completion->send(LlamaCppCompletionCommand::Stop);
+    } else {
+        // do something
+
+        $i += 1;
+    }
+}
+```
diff --git a/docs/pages/docs/features/configuration/index.md b/docs/pages/docs/features/configuration/index.md
index a3b7d1108c22adb67fbbffa6df13596a720638c7..3f581383e34eb2adf642cefcc84b98a37ea2a407 100644
--- a/docs/pages/docs/features/configuration/index.md
+++ b/docs/pages/docs/features/configuration/index.md
@@ -37,6 +37,7 @@ define('DM_ROOT', __DIR__);
 // Coroutine timeouts
 define('DM_BATCH_PROMISE_TIMEOUT', 0.3);
 define('DM_GRAPHQL_PROMISE_TIMEOUT', 0.2);
+define('DM_POOL_CONNECTION_TIMEOUT', 0.1);
 ```
 
 ```json file:composer.json
diff --git a/docs/pages/docs/features/websockets/protocols.md b/docs/pages/docs/features/websockets/protocols.md
index ef966e66c9f8f5b7972d50c8099e8728792e655c..4af04e3689b22c14be67e428fed45b9a4e6061e3 100644
--- a/docs/pages/docs/features/websockets/protocols.md
+++ b/docs/pages/docs/features/websockets/protocols.md
@@ -25,8 +25,32 @@ digraph {
 
 ### Establishing the Connection
 
+Besides connecting to the server and choosing a protocol, you also have to
+add {{docs/features/security/csrf-protection/index}} token. Since we cannot
+use `POST` to establish connection, we need to add it as a `GET` variable.
+
 ```typescript
-const webSocket = new WebSocket('wss://localhost:9501', ['dm-rpc']);
+const serverUrl = new URL('wss://localhost:9501');
+
+serverUrl.searchParams.append('csrf', /* obtain CSRF token */);
+
+const webSocket = new WebSocket(serverUrl, ['dm-rpc']);
+```
+
+For example, if you are using {{docs/features/templating/twig/index}}, you can
+put CSRF token into a meta tag:
+
+```twig
+<meta name="csrf-token" content="{{ csrf_token(request, response) }}">
+```
+
+Then, in JavaScript:
+
+```typescript
+serverUrl.searchParams.append(
+    'csrf', 
+    document.querySelector("meta[name=csrf-token]").attributes.content.value,
+);
 ```
 
 ### Writing RPC Responders
@@ -39,6 +63,7 @@ namespace App\WebSocketRPCResponder;
 use App\RPCMethod;
 use Distantmagic\Resonance\Attribute\RespondsToWebSocketRPC;
 use Distantmagic\Resonance\Attribute\Singleton;
+use Distantmagic\Resonance\Feature;
 use Distantmagic\Resonance\RPCRequest;
 use Distantmagic\Resonance\RPCResponse;
 use Distantmagic\Resonance\SingletonCollection;
@@ -47,7 +72,10 @@ use Distantmagic\Resonance\WebSocketConnection;
 use Distantmagic\Resonance\WebSocketRPCResponder;
 
 #[RespondsToWebSocketRPC(RPCMethod::Echo)]
-#[Singleton(collection: SingletonCollection::WebSocketRPCResponder)]
+#[Singleton(
+    collection: SingletonCollection::WebSocketRPCResponder,
+    wantsFeature: Feature::WebSocket,
+)]
 final readonly class EchoResponder extends WebSocketRPCResponder
 {
     protected function onRequest(
@@ -62,3 +90,56 @@ final readonly class EchoResponder extends WebSocketRPCResponder
     }
 }
 ```
+
+### RPC Connection Controller (Optional)
+
+In case you want not only respond to RPC messages, but also be able to push
+notifications to the client at any moment, you can implement 
+`Distantmagic\Resonance\WebSocketRPCConnectionControllerInterface` and 
+register in in the {{docs/features/dependency-injection/index}} container.
+
+For example:
+
+```php file:app/WebSocketRPCConnectionController.php
+<?php
+
+namespace App;
+
+use Distantmagic\Resonance\Attribute\Singleton;
+use Distantmagic\Resonance\Feature;
+use Distantmagic\Resonance\RPCNotification;
+use Distantmagic\Resonance\WebSocketAuthResolution;
+use Distantmagic\Resonance\WebSocketConnection;
+use Distantmagic\Resonance\WebSocketRPCConnectionControllerInterface;
+
+#[Singleton(
+    provides: WebSocketRPCConnectionControllerInterface::class,
+    wantsFeature: Feature::WebSocket,
+)]
+readonly class WebSocketRPCConnectionController implements WebSocketRPCConnectionControllerInterface
+{
+    public function onClose(
+        WebSocketAuthResolution $webSocketAuthResolution,
+        WebSocketConnection $webSocketConnection,
+    ): void {
+        // called when connection is closed
+    }
+
+    public function onOpen(
+        WebSocketAuthResolution $webSocketAuthResolution,
+        WebSocketConnection $webSocketConnection,
+    ): void
+    {
+        if ($webSocketConnection->status === WebSocketConnectionStatus::Closed) {
+            // connection is closed
+        }
+
+        $webSocketConnection->push(new RPCNotification(
+            RPCMethod::YourMethod,
+            [
+                // your payload
+            ]
+        ));
+    }
+}
+```
diff --git a/docs/pages/index.md b/docs/pages/index.md
index 804ee2d7a4226cbdbcffff05b4c04184d264322e..837817b6e02286b82dfeef3ae45ef90a4d4b9d44 100644
--- a/docs/pages/index.md
+++ b/docs/pages/index.md
@@ -17,7 +17,7 @@ description: >
     <div class="homepage__content">
         <hgroup class="homepage__title">
             <h1>Resonance</h1>
-            <h2>PHP SaaS Framework</h2>
+            <h2>SaaS Framework That Solves Real-Life Issues</h2>
             <p>
                 Designed from the ground up to facilitate interoperability
                 and messaging between services in your infrastructure and 
@@ -37,6 +37,47 @@ description: >
             </a>
         </hgroup>
         <ul class="homepage__examples">
+            <li class="formatted-content homepage__example">
+                <h2 class="homepage__example__title">
+                    Artificial Intelligence
+                </h2>
+                <div class="homepage__example__description">
+                    <p>
+                        Integrate your application with self-hosted open-source 
+                        LLMs.
+                    </p>
+                    <p>
+                        Use your own Machine Learning models in production.
+                    </p>
+                    <a 
+                        class="homepage__cta homepage__cta--example"
+                        href="/docs/features/ai/"
+                    >
+                        Learn More
+                    </a>
+                </div>
+                <pre class="homepage__example__code fenced-code"><code 
+                        class="language-php"
+                        data-controller="hljs"
+                        data-hljs-language-value="php"
+                    >class LlamaCppGenerate 
+{
+    public function __construct(protected LlamaCppClient $llamaCppClient) 
+    {
+    }
+
+    public function doSomething(): void
+    {
+        $request = new LlamaCppCompletionRequest('How to make a cat happy?');
+
+        $completion = $this->llamaCppClient->generateCompletion($request);
+
+        foreach ($completion as $token) {
+            // ...
+        }
+    }
+}</code></pre>
+            </li>
             <li class="formatted-content homepage__example">
                 <h2 class="homepage__example__title">
                     Asynchronous Where it Matters
@@ -141,8 +182,7 @@ readonly class Homepage implements HttpResponderInterface
                         class="language-php"
                         data-controller="hljs"
                         data-hljs-language-value="php"
-                    >
-#[ListensTo(HttpServerStarted::class)]
+                    >#[ListensTo(HttpServerStarted::class)]
 #[Singleton(collection: SingletonCollection::EventListener)]
 final readonly class InitializeErrorReporting extends EventListener
 {
diff --git a/docs/pages/tutorials/how-to-create-llm-chat-with-websocket-and-llama-cpp/index.md b/docs/pages/tutorials/how-to-create-llm-chat-with-websocket-and-llama-cpp/index.md
new file mode 100644
index 0000000000000000000000000000000000000000..8ea6c3fd4aec40a749fc0e1937d5783031101afc
--- /dev/null
+++ b/docs/pages/tutorials/how-to-create-llm-chat-with-websocket-and-llama-cpp/index.md
@@ -0,0 +1,126 @@
+---
+collections:
+  - tutorials
+draft: true
+layout: dm:tutorial
+parent: tutorials/index
+title: How to Create LLM Chat with WebSocket and llama.cpp?
+description: >
+    Learn how to setup a WebSocket server and create basic chat with an open
+    source LLM.
+---
+
+## Preparations
+
+Before starting this tutorial, it might be useful for you to familiarize with
+the other tutorials and documentation pages first:
+
+* {{tutorials/how-to-serve-llm-completions/index}} - to setup 
+    [llama.cpp](https://github.com/ggerganov/llama.cpp) server and configure
+    Resonance to use it
+* {{docs/features/websockets/index}} - to learn how WebSocket featres are
+    implemented in Resonance
+
+In this tutorial, we will also use [Stimulus](https://stimulus.hotwired.dev/)
+for the front-end code.
+
+Once you have both Resonance and 
+[llama.cpp](https://github.com/ggerganov/llama.cpp) runing, we can continue.
+
+## Http Responder
+
+```php file:app/RPCMethod.php
+<?php
+
+namespace App;
+
+use Distantmagic\Resonance\EnumValuesTrait;
+use Distantmagic\Resonance\NameableEnumTrait;
+use Distantmagic\Resonance\RPCMethodInterface;
+
+enum RPCMethod: string implements RPCMethodInterface
+{
+    use EnumValuesTrait;
+    use NameableEnumTrait;
+
+    case LlmChatReady = 'llm_chat_ready';
+}
+```
+
+```php file:app/HttpResponder/LlmChat.php
+<?php
+
+declare(strict_types=1);
+
+namespace App\HttpResponder\Webapp;
+
+use App\HttpRouteSymbol;
+use Distantmagic\Resonance\Attribute\Can;
+use Distantmagic\Resonance\Attribute\RespondsToHttp;
+use Distantmagic\Resonance\Attribute\Singleton;
+use Distantmagic\Resonance\HttpInterceptableInterface;
+use Distantmagic\Resonance\HttpResponder;
+use Distantmagic\Resonance\RequestMethod;
+use Distantmagic\Resonance\SingletonCollection;
+use Distantmagic\Resonance\SiteAction;
+use Distantmagic\Resonance\TwigTemplate;
+use Swoole\Http\Request;
+use Swoole\Http\Response;
+
+#[Can(SiteAction::StartWebSocketRPCConnection)]
+#[RespondsToHttp(
+    method: RequestMethod::GET,
+    pattern: '/chat',
+    routeSymbol: HttpRouteSymbol::LlmChat,
+)]
+#[Singleton(collection: SingletonCollection::HttpResponder)]
+final readonly class LlmChat extends HttpResponder
+{
+    public function respond(Request $request, Response $response): HttpInterceptableInterface
+    {
+        return new TwigTemplate('llmchat.twig');
+    }
+}
+```
+
+```twig file:app/views/llamachat.twig
+turbo/llmchat/index.twig
+```
+
+## Front-end
+
+We will use {{docs/features/asset-bundling-esbuild/index}} to bundle front-end
+TypeScript code.
+
+```shell
+$ ./node_modules/.bin/esbuild \
+    --bundle \
+    --define:global=globalThis \
+    --entry-names="./[name]_$(BUILD_ID)" \
+    --format=esm \
+    --log-limit=0 \
+    --metafile=esbuild-meta-app.json \
+    --minify \
+    --outdir=./$(BUILD_TARGET_DIRECTORY) \
+    --platform=browser \
+    --sourcemap \
+    --target=es2022,safari16 \
+    --tree-shaking=true \
+    --tsconfig=tsconfig.json \
+    resources/ts/controller_llmchat.ts \
+;
+```
+
+```typescript file:resources/ts/controller_llmchat.ts
+import { Controller } from "@hotwired/stimulus";
+
+import { stimulus } from "../stimulus";
+
+@stimulus("llmchat")
+export class controller_llmchat extends Controller<HTMLElement> {
+  public static targets = ["textarea", "textareaGrow"];
+
+  private declare readonly textareaGrowTarget: HTMLElement;
+  private declare readonly textareaTarget: HTMLTextAreaElement;
+}
+```
diff --git a/docs/pages/tutorials/connect-to-llama-cpp/index.md b/docs/pages/tutorials/how-to-serve-llm-completions/index.md
similarity index 98%
rename from docs/pages/tutorials/connect-to-llama-cpp/index.md
rename to docs/pages/tutorials/how-to-serve-llm-completions/index.md
index 2604136537d7217e76eedb18cbc44b5453503a37..166820e4b10c8205f95e65e807bbbf59179a0eb8 100644
--- a/docs/pages/tutorials/connect-to-llama-cpp/index.md
+++ b/docs/pages/tutorials/how-to-serve-llm-completions/index.md
@@ -3,7 +3,7 @@ collections:
   - tutorials
 layout: dm:tutorial
 parent: tutorials/index
-title: How to Serve LLM Completions (With llama.cpp)
+title: How to Serve LLM Completions (With llama.cpp)?
 description: >
     How to connect with llama.cpp and issue parallel requests for LLM 
     completions and embeddings with Resonance.
diff --git a/docs/pages/tutorials/session-based-authentication/index.md b/docs/pages/tutorials/session-based-authentication/index.md
index cf61a5cca292bed622701cee9787f8a1fdaf2faf..0559023dedaa3692485c7a42cc448aa796482729 100644
--- a/docs/pages/tutorials/session-based-authentication/index.md
+++ b/docs/pages/tutorials/session-based-authentication/index.md
@@ -153,11 +153,9 @@ declare(strict_types=1);
 namespace App;
 
 use Distantmagic\Resonance\Attribute\Singleton;
-use Distantmagic\Resonance\DatabaseConnectionPoolRepository;
 use Distantmagic\Resonance\DoctrineEntityManagerRepository;
 use Distantmagic\Resonance\UserInterface;
 use Distantmagic\Resonance\UserRepositoryInterface;
-use LogicException;
 use Swoole\Http\Request;
 
 #[Singleton(provides: UserRepositoryInterface::class)]
@@ -167,13 +165,19 @@ readonly class UserRepository implements UserRepositoryInterface
         private DoctrineEntityManagerRepository $doctrineEntityManagerRepository,
     ) {}
 
-    public function findUserById(Request $request, int|string $userId): ?UserInterface
+    public function findUserById(int|string $userId): ?UserInterface
     {
         return $this
             ->doctrineEntityManagerRepository
-            ->getEntityManager($request)
-            ->getRepository(User::class)
-            ->find($userId)
+            ->withRepository(User::class, function (
+                EntityManagerInterface $entityManager,
+                EntityRepository $entityRepository,
+            ) use ($userId) {
+                /**
+                 * @var null|UserInterface
+                 */
+                return $entityRepository->find($userId);
+            })
         ;
     }
 }
diff --git a/resources/css/docs-page-homepage.css b/resources/css/docs-page-homepage.css
index 99ad27395049fe94794fe0cc1ead2ae283ca49d5..4422f7dc1e9539baf031e5e66545e55bed0f839f 100644
--- a/resources/css/docs-page-homepage.css
+++ b/resources/css/docs-page-homepage.css
@@ -108,9 +108,12 @@ h2.homepage__example__title {
     background-repeat: no-repeat;
     background-size: cover;
   }
-  @media screen and (min-width: 1024px) {
+  @media screen and (min-width: 1024px) and (max-width: 1649px) {
     padding: 160px 0;
   }
+  @media screen and (min-width: 1650px) {
+    padding: 210px 0;
+  }
 }
 
 .homepage__title h1 {
@@ -126,7 +129,7 @@ h2.homepage__example__title {
 }
 
 .homepage__title h2 {
-  margin: 0px 0 100px 0;
+  margin: 0px 0 40px 0;
   text-align: center;
 
   @media screen and (max-width: 1023px) {
diff --git a/src/Command/Watch.php b/src/Command/Watch.php
index fce35a4bab429f3da03d53f00fe6a60cb4822e84..e7a941a6cbf8ea20c057a69c4ed12b9f69f4be0b 100644
--- a/src/Command/Watch.php
+++ b/src/Command/Watch.php
@@ -4,6 +4,7 @@ declare(strict_types=1);
 
 namespace Distantmagic\Resonance\Command;
 
+use Distantmagic\Resonance\ApplicationConfiguration;
 use Distantmagic\Resonance\Attribute\ConsoleCommand;
 use Distantmagic\Resonance\Command;
 use Distantmagic\Resonance\InotifyIterator;
@@ -20,9 +21,12 @@ use Symfony\Component\Console\Output\OutputInterface;
 )]
 final class Watch extends Command
 {
+    private const THROTTLE_TIME_MS = 10;
+
     private ?Process $process = null;
 
     public function __construct(
+        private ApplicationConfiguration $applicationConfiguration,
         private LoggerInterface $logger,
     ) {
         parent::__construct();
@@ -47,7 +51,7 @@ final class Watch extends Command
         $directories = [
             DM_APP_ROOT,
             DM_APP_ROOT.'/../config.ini',
-            DM_PUBLIC_ROOT,
+            $this->applicationConfiguration->esbuildMetafile,
             DM_RESONANCE_ROOT,
         ];
 
diff --git a/src/DatabaseConnectionPoolRepository.php b/src/DatabaseConnectionPoolRepository.php
index 1d678ece5912af1763c16342d11b4b1901b6fb66..c4ee1a367a96e148908a73468b457ee8152b311e 100644
--- a/src/DatabaseConnectionPoolRepository.php
+++ b/src/DatabaseConnectionPoolRepository.php
@@ -7,6 +7,7 @@ namespace Distantmagic\Resonance;
 use Ds\Map;
 use OutOfBoundsException;
 use PDO;
+use RuntimeException;
 use Swoole\Database\PDOPool;
 use Swoole\Database\PDOProxy;
 
@@ -33,9 +34,15 @@ readonly class DatabaseConnectionPoolRepository
         }
 
         /**
-         * @var PDO|PDOProxy
+         * @var false|PDO|PDOProxy
          */
-        return $this->databaseConnectionPool->get($name)->get();
+        $pdo = $this->databaseConnectionPool->get($name)->get(DM_POOL_CONNECTION_TIMEOUT);
+
+        if (!$pdo) {
+            throw new RuntimeException('Database connection timed out');
+        }
+
+        return $pdo;
     }
 
     public function putConnection(string $name, PDO|PDOProxy $pdo): void
diff --git a/src/DoctrineEntityManagerRepository.php b/src/DoctrineEntityManagerRepository.php
index eefbd356edd1e022d8e6151969b05fefb0e60158..759ee6c03bd0d38a98da72a286f5678ae87f9234 100644
--- a/src/DoctrineEntityManagerRepository.php
+++ b/src/DoctrineEntityManagerRepository.php
@@ -40,6 +40,11 @@ readonly class DoctrineEntityManagerRepository
         );
     }
 
+    public function createContextKey(string $name): string
+    {
+        return sprintf('%s.%s', __CLASS__, $name);
+    }
+
     public function getEntityManager(Request $request, string $name = 'default'): EntityManagerInterface
     {
         if (!$this->entityManagers->offsetExists($request)) {
@@ -52,9 +57,23 @@ readonly class DoctrineEntityManagerRepository
             return $entityManagers->get($name);
         }
 
+        /**
+         * @var null|Context $context
+         */
+        $context = Coroutine::getContext();
+        $contextKey = $this->createContextKey($name);
+
+        if ($context && isset($context[$contextKey]) && $context[$contextKey] instanceof EntityManagerWeakReference) {
+            return $context[$contextKey]->getEntityManager();
+        }
+
         $conn = $this->doctrineConnectionRepository->getConnection($request, $name);
         $entityManager = new EntityManager($conn, $this->configuration);
 
+        if ($context) {
+            $context[$contextKey] = new EntityManagerWeakReference($entityManager);
+        }
+
         $entityManagers->put($name, $entityManager);
 
         return $entityManager;
@@ -73,8 +92,7 @@ readonly class DoctrineEntityManagerRepository
          * @var null|Context $context
          */
         $context = Coroutine::getContext();
-
-        $contextKey = sprintf('%s.entityManager.%s', __METHOD__, $name);
+        $contextKey = $this->createContextKey($name);
 
         if ($context && isset($context[$contextKey])) {
             $entityManagerWeakReference = $context[$contextKey];
diff --git a/src/InputValidator/RPCMessageValidator.php b/src/InputValidator/RPCMessageValidator.php
index 3acf1f71c576a53ce51fd848e9e4d14fed086920..7716c6870de02f7324c9a4aa49b081e3d521280e 100644
--- a/src/InputValidator/RPCMessageValidator.php
+++ b/src/InputValidator/RPCMessageValidator.php
@@ -11,6 +11,7 @@ use Distantmagic\Resonance\InputValidator;
 use Distantmagic\Resonance\JsonSchema;
 use Distantmagic\Resonance\JsonSchemaValidator;
 use Distantmagic\Resonance\RPCMethodValidatorInterface;
+use stdClass;
 
 /**
  * @extends InputValidator<RPCMessage, array{
@@ -42,17 +43,16 @@ readonly class RPCMessageValidator extends InputValidator
     {
         return new JsonSchema([
             'type' => 'array',
-            'items' => [
+            'items' => false,
+            'prefixItems' => [
                 [
                     'type' => 'string',
-                    'enum' => $this->rpcMethodValidator->names(),
+                    'enum' => $this->rpcMethodValidator->values(),
                 ],
+                new stdClass(),
                 [
-                ],
-                [
-                    'type' => 'string',
+                    'type' => ['null', 'string'],
                     'format' => 'uuid',
-                    'nullable' => true,
                 ],
             ],
         ]);
diff --git a/src/LlamaCppClient.php b/src/LlamaCppClient.php
index b0f8834595c8f32f763a7c660ac7218fc2727aa4..5905bdd946194776e7110921193f337d71219702 100644
--- a/src/LlamaCppClient.php
+++ b/src/LlamaCppClient.php
@@ -8,6 +8,7 @@ use CurlHandle;
 use Distantmagic\Resonance\Attribute\Singleton;
 use Generator;
 use JsonSerializable;
+use Psr\Log\LoggerInterface;
 use RuntimeException;
 use Swoole\Coroutine\Channel;
 
@@ -15,24 +16,21 @@ use Swoole\Coroutine\Channel;
 readonly class LlamaCppClient
 {
     // strlen('data: ')
-    public const COMPLETION_CHUNKED_DATA_PREFIX_LENGTH = 6;
+    private const COMPLETION_CHUNKED_DATA_PREFIX_LENGTH = 6;
 
     public function __construct(
         private JsonSerializer $jsonSerializer,
+        private LoggerInterface $logger,
         private LlamaCppConfiguration $llamaCppConfiguration,
         private LlamaCppLinkBuilder $llamaCppLinkBuilder,
     ) {}
 
     /**
-     * @return Generator<LlamaCppCompletionToken>
+     * @return Generator<int,LlamaCppCompletionToken,null|LlamaCppCompletionCommand>
      */
     public function generateCompletion(LlamaCppCompletionRequest $request): Generator
     {
-        $curlHandle = $this->createCurlHandle();
-
-        curl_setopt($curlHandle, CURLOPT_POST, true);
-
-        $responseChunks = $this->streamResponse($curlHandle, $request, '/completion');
+        $responseChunks = $this->streamResponse($request, '/completion');
 
         /**
          * @var null|string
@@ -52,11 +50,19 @@ readonly class LlamaCppClient
             );
 
             if (is_string($previousContent)) {
-                yield new LlamaCppCompletionToken(
+                $shouldContinue = yield new LlamaCppCompletionToken(
                     content: $previousContent,
                     isLast: $unserializedToken->stop,
                 );
 
+                if (LlamaCppCompletionCommand::Stop === $shouldContinue) {
+                    if (!$responseChunks->channel->close()) {
+                        throw new RuntimeException('Unable to close coroutine channel');
+                    }
+
+                    break;
+                }
+
                 $previousContent = null;
             }
 
@@ -104,11 +110,7 @@ readonly class LlamaCppClient
      */
     public function generateInfill(LlamaCppInfillRequest $request): Generator
     {
-        $curlHandle = $this->createCurlHandle();
-
-        curl_setopt($curlHandle, CURLOPT_POST, true);
-
-        $responseChunks = $this->streamResponse($curlHandle, $request, '/infill');
+        $responseChunks = $this->streamResponse($request, '/infill');
 
         foreach ($responseChunks as $responseChunk) {
             /**
@@ -185,6 +187,8 @@ readonly class LlamaCppClient
             $headers[] = sprintf('Authorization: Bearer %s', $this->llamaCppConfiguration->apiKey);
         }
 
+        curl_setopt($curlHandle, CURLOPT_FORBID_REUSE, true);
+        curl_setopt($curlHandle, CURLOPT_FRESH_CONNECT, true);
         curl_setopt($curlHandle, CURLOPT_HTTPHEADER, $headers);
 
         return $curlHandle;
@@ -193,29 +197,37 @@ readonly class LlamaCppClient
     /**
      * @return SwooleChannelIterator<string>
      */
-    private function streamResponse(CurlHandle $curlHandle, JsonSerializable $request, string $path): SwooleChannelIterator
+    private function streamResponse(JsonSerializable $request, string $path): SwooleChannelIterator
     {
         $channel = new Channel(1);
         $requestData = json_encode($request);
 
-        $cid = go(function () use ($channel, $curlHandle, $path, $requestData) {
+        $cid = go(function () use ($channel, $path, $requestData) {
+            $curlHandle = $this->createCurlHandle();
+
             try {
+                curl_setopt($curlHandle, CURLOPT_POST, true);
                 curl_setopt($curlHandle, CURLOPT_POSTFIELDS, $requestData);
                 curl_setopt($curlHandle, CURLOPT_RETURNTRANSFER, false);
                 curl_setopt($curlHandle, CURLOPT_URL, $this->llamaCppLinkBuilder->build($path));
                 curl_setopt($curlHandle, CURLOPT_WRITEFUNCTION, static function (CurlHandle $curlHandle, string $data) use ($channel) {
-                    $channel->push($data);
+                    if ($channel->push($data, DM_POOL_CONNECTION_TIMEOUT)) {
+                        return strlen($data);
+                    }
 
-                    return strlen($data);
+                    return 0;
                 });
-
                 if (!curl_exec($curlHandle)) {
-                    throw new CurlException($curlHandle);
-                }
+                    $curlErrno = curl_errno($curlHandle);
 
-                $this->assertStatusCode($curlHandle, 200);
+                    if (CURLE_WRITE_ERROR !== $curlErrno) {
+                        throw new CurlException($curlHandle);
+                    }
+                } else {
+                    $this->assertStatusCode($curlHandle, 200);
+                }
             } finally {
-                curl_setopt($curlHandle, CURLOPT_WRITEFUNCTION, null);
+                curl_close($curlHandle);
 
                 $channel->close();
             }
diff --git a/src/WebSocketConnectionController.php b/src/LlamaCppCompletionCommand.php
similarity index 54%
rename from src/WebSocketConnectionController.php
rename to src/LlamaCppCompletionCommand.php
index e45efe54d16f0f58bc49d67ed9b96462e963edcd..99dbf1f67ddcb539cfaca9c15db4b11575450624 100644
--- a/src/WebSocketConnectionController.php
+++ b/src/LlamaCppCompletionCommand.php
@@ -4,4 +4,7 @@ declare(strict_types=1);
 
 namespace Distantmagic\Resonance;
 
-abstract readonly class WebSocketConnectionController {}
+enum LlamaCppCompletionCommand
+{
+    case Stop;
+}
diff --git a/src/OAuth2ClaimReader.php b/src/OAuth2ClaimReader.php
index 42895365e0d371c774bd15333059f402cc5724dc..0d51b0260b65afe28457047be146d17c65b42144 100644
--- a/src/OAuth2ClaimReader.php
+++ b/src/OAuth2ClaimReader.php
@@ -63,7 +63,7 @@ readonly class OAuth2ClaimReader
 
         return $this
             ->doctrineEntityManagerRepository
-            ->withEntityManager(function (EntityManagerInterface $entityManager) use ($request, $serverRequest) {
+            ->withEntityManager(function (EntityManagerInterface $entityManager) use ($serverRequest) {
                 $accessTokenId = $serverRequest->getAttribute('oauth_access_token_id');
 
                 if (!is_string($accessTokenId)) {
@@ -100,7 +100,7 @@ readonly class OAuth2ClaimReader
                     throw OAuthServerException::invalidRequest('oauth_user_id');
                 }
 
-                $user = $this->userRepository->findUserById($request, $userId);
+                $user = $this->userRepository->findUserById($userId);
 
                 if (!$user) {
                     throw OAuthServerException::invalidRequest('oauth_user_id');
diff --git a/src/RPCMethodInterface.php b/src/RPCMethodInterface.php
index e15559ad7c9ec81e82bb7a58cc164af199c2fac7..2e1deb24bbd32c49a87c9cb71118275918e770fd 100644
--- a/src/RPCMethodInterface.php
+++ b/src/RPCMethodInterface.php
@@ -4,4 +4,7 @@ declare(strict_types=1);
 
 namespace Distantmagic\Resonance;
 
-interface RPCMethodInterface extends NameableInterface {}
+interface RPCMethodInterface
+{
+    public function getValue(): string;
+}
diff --git a/src/RPCMethodValidatorInterface.php b/src/RPCMethodValidatorInterface.php
index a9db4daacd4b6ef7f4e6c5fa3c8a74d9663188e8..1e8a3ea289436cc905c4c6edec5e57062718c3b2 100644
--- a/src/RPCMethodValidatorInterface.php
+++ b/src/RPCMethodValidatorInterface.php
@@ -16,5 +16,5 @@ interface RPCMethodValidatorInterface
     /**
      * @return array<string>
      */
-    public function names(): array;
+    public function values(): array;
 }
diff --git a/src/RPCNotification.php b/src/RPCNotification.php
index c3367420f16acfafd2ac988ca14b1aa98fa38118..7ca2c4aa0d4f4c587a7816cf2056d502e0ca0ee8 100644
--- a/src/RPCNotification.php
+++ b/src/RPCNotification.php
@@ -4,15 +4,26 @@ declare(strict_types=1);
 
 namespace Distantmagic\Resonance;
 
+use Stringable;
+
 /**
  * @psalm-suppress PossiblyUnusedProperty
  *
  * @template TPayload
  */
-readonly class RPCNotification
+readonly class RPCNotification implements Stringable
 {
     public function __construct(
         public RPCMethodInterface $method,
         public mixed $payload,
     ) {}
+
+    public function __toString(): string
+    {
+        return json_encode([
+            $this->method->getValue(),
+            $this->payload,
+            null,
+        ]);
+    }
 }
diff --git a/src/RPCResponse.php b/src/RPCResponse.php
index d0758a0709fa1f777b99987ac81901c4fd47914d..a25f63fb2be3ae606623c2575c1ef731bced407c 100644
--- a/src/RPCResponse.php
+++ b/src/RPCResponse.php
@@ -10,14 +10,14 @@ readonly class RPCResponse implements Stringable
 {
     public function __construct(
         private string $requestId,
-        private string|Stringable $content,
+        private mixed $content,
     ) {}
 
     public function __toString(): string
     {
         return json_encode([
             $this->requestId,
-            (string) $this->content,
+            $this->content,
         ]);
     }
 }
diff --git a/src/SessionAuthentication.php b/src/SessionAuthentication.php
index 3850c35c698a4a5c4e2c9fdd99136c8f9e0f53fc..51e03212c86d500df4fc35e9b163009052e0247f 100644
--- a/src/SessionAuthentication.php
+++ b/src/SessionAuthentication.php
@@ -71,6 +71,6 @@ final readonly class SessionAuthentication
             return null;
         }
 
-        return $this->userRepository->findUserById($request, $userId);
+        return $this->userRepository->findUserById($userId);
     }
 }
diff --git a/src/SingletonProvider/ConfigurationProvider/LlamaCppConfigurationProvider.php b/src/SingletonProvider/ConfigurationProvider/LlamaCppConfigurationProvider.php
index 7aebce60642e033296e171e8b797e196b310697e..3999e62ee00c874f352e7c62c041058555b11eba 100644
--- a/src/SingletonProvider/ConfigurationProvider/LlamaCppConfigurationProvider.php
+++ b/src/SingletonProvider/ConfigurationProvider/LlamaCppConfigurationProvider.php
@@ -31,9 +31,8 @@ final readonly class LlamaCppConfigurationProvider extends ConfigurationProvider
             'type' => 'object',
             'properties' => [
                 'apiKey' => [
-                    'type' => 'string',
+                    'type' => ['null', 'string'],
                     'minLength' => 1,
-                    'nullable' => true,
                     'default' => null,
                 ],
                 'host' => [
diff --git a/src/SwooleChannelIterator.php b/src/SwooleChannelIterator.php
index 8e16302456aa8e6e19801cb256a3d59192d3f7a5..91b26b48e44838da6da9e4e2ecc34c7cd1427c8f 100644
--- a/src/SwooleChannelIterator.php
+++ b/src/SwooleChannelIterator.php
@@ -16,18 +16,27 @@ use Swoole\Coroutine\Channel;
  */
 readonly class SwooleChannelIterator implements IteratorAggregate
 {
-    public function __construct(private Channel $channel) {}
+    public function __construct(public Channel $channel) {}
+
+    public function close(): void
+    {
+        $this->channel->close();
+    }
 
     /**
-     * @return Generator<TData>
+     * @return Generator<int,TData,bool>
      */
     public function getIterator(): Generator
     {
         do {
+            if (SWOOLE_CHANNEL_CLOSED === $this->channel->errCode) {
+                return;
+            }
+
             /**
              * @var mixed $data explicitly mixed for typechecks
              */
-            $data = $this->channel->pop();
+            $data = $this->channel->pop(DM_POOL_CONNECTION_TIMEOUT);
 
             if (false === $data) {
                 switch ($this->channel->errCode) {
diff --git a/src/UserRepositoryInterface.php b/src/UserRepositoryInterface.php
index 731de6dcf4196180b187b2b271b6d9d10b0b8fe0..d36c940a4d351373fa9a9cea5ff46701502e988b 100644
--- a/src/UserRepositoryInterface.php
+++ b/src/UserRepositoryInterface.php
@@ -4,9 +4,7 @@ declare(strict_types=1);
 
 namespace Distantmagic\Resonance;
 
-use Swoole\Http\Request;
-
 interface UserRepositoryInterface
 {
-    public function findUserById(Request $request, int|string $userId): ?UserInterface;
+    public function findUserById(int|string $userId): ?UserInterface;
 }
diff --git a/src/WebSocketConnection.php b/src/WebSocketConnection.php
index 1473665749ea0c502ea374a81b3205437e757aed..4123e92e7373d808af35b4b51d3b6ec17ca823f9 100644
--- a/src/WebSocketConnection.php
+++ b/src/WebSocketConnection.php
@@ -7,11 +7,13 @@ namespace Distantmagic\Resonance;
 use Stringable;
 use Swoole\WebSocket\Server;
 
-readonly class WebSocketConnection
+class WebSocketConnection
 {
+    public WebSocketConnectionStatus $status = WebSocketConnectionStatus::Open;
+
     public function __construct(
-        public Server $server,
-        public int $fd,
+        public readonly Server $server,
+        public readonly int $fd,
     ) {}
 
     public function push(string|Stringable $response): void
diff --git a/src/WebSocketConnectionController/RPCConnectionController.php b/src/WebSocketConnectionController/RPCConnectionController.php
deleted file mode 100644
index 8b8efe6cbca0e0252c684b572feb75d2e53fbad0..0000000000000000000000000000000000000000
--- a/src/WebSocketConnectionController/RPCConnectionController.php
+++ /dev/null
@@ -1,32 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Distantmagic\Resonance\WebSocketConnectionController;
-
-use Distantmagic\Resonance\InputValidatedData\RPCMessage;
-use Distantmagic\Resonance\WebSocketAuthResolution;
-use Distantmagic\Resonance\WebSocketConnection;
-use Distantmagic\Resonance\WebSocketConnectionController;
-use Distantmagic\Resonance\WebSocketRPCResponderAggregate;
-
-readonly class RPCConnectionController extends WebSocketConnectionController
-{
-    public function __construct(
-        private WebSocketRPCResponderAggregate $webSocketRPCResponderAggregate,
-        private WebSocketAuthResolution $webSocketAuthResolution,
-        private WebSocketConnection $webSocketConnection,
-    ) {}
-
-    public function onRPCMessage(RPCMessage $rpcMessage): void
-    {
-        $this
-            ->webSocketRPCResponderAggregate
-            ->respond(
-                $this->webSocketAuthResolution,
-                $this->webSocketConnection,
-                $rpcMessage
-            )
-        ;
-    }
-}
diff --git a/src/WebSocketConnectionStatus.php b/src/WebSocketConnectionStatus.php
new file mode 100644
index 0000000000000000000000000000000000000000..fd37d59b3ddbe7ba5283a2a790266af2bd2c6d7c
--- /dev/null
+++ b/src/WebSocketConnectionStatus.php
@@ -0,0 +1,21 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Distantmagic\Resonance;
+
+enum WebSocketConnectionStatus
+{
+    case Closed;
+    case Open;
+
+    public function isClosed(): bool
+    {
+        return self::Closed === $this;
+    }
+
+    public function isOpen(): bool
+    {
+        return self::Open === $this;
+    }
+}
diff --git a/src/WebSocketProtocolController/RPCProtocolController.php b/src/WebSocketProtocolController/RPCProtocolController.php
index b9545ae9dece654f69c7a71f0a38234696a26eb5..5b9e1908b0a54fcad684c049754eee60fd7d85b7 100644
--- a/src/WebSocketProtocolController/RPCProtocolController.php
+++ b/src/WebSocketProtocolController/RPCProtocolController.php
@@ -16,10 +16,12 @@ use Distantmagic\Resonance\SingletonCollection;
 use Distantmagic\Resonance\SiteAction;
 use Distantmagic\Resonance\WebSocketAuthResolution;
 use Distantmagic\Resonance\WebSocketConnection;
-use Distantmagic\Resonance\WebSocketConnectionController\RPCConnectionController;
+use Distantmagic\Resonance\WebSocketConnectionStatus;
 use Distantmagic\Resonance\WebSocketProtocol;
 use Distantmagic\Resonance\WebSocketProtocolController;
 use Distantmagic\Resonance\WebSocketProtocolException;
+use Distantmagic\Resonance\WebSocketRPCConnectionControllerInterface;
+use Distantmagic\Resonance\WebSocketRPCConnectionHandle;
 use Distantmagic\Resonance\WebSocketRPCResponderAggregate;
 use Ds\Map;
 use JsonException;
@@ -38,9 +40,9 @@ use Throwable;
 final readonly class RPCProtocolController extends WebSocketProtocolController
 {
     /**
-     * @var Map<int, RPCConnectionController>
+     * @var Map<int, WebSocketRPCConnectionHandle>
      */
-    private Map $connectionControllers;
+    private Map $connectionHandles;
 
     public function __construct(
         private CSRFManager $csrfManager,
@@ -50,16 +52,19 @@ final readonly class RPCProtocolController extends WebSocketProtocolController
         private LoggerInterface $logger,
         private RPCMessageValidator $rpcMessageValidator,
         private WebSocketRPCResponderAggregate $webSocketRPCResponderAggregate,
+        private ?WebSocketRPCConnectionControllerInterface $webSocketRPCConnectionController = null,
     ) {
         /**
-         * @var Map<int, RPCConnectionController>
+         * @var Map<int, WebSocketRPCConnectionHandle>
          */
-        $this->connectionControllers = new Map();
+        $this->connectionHandles = new Map();
     }
 
     public function isAuthorizedToConnect(Request $request): WebSocketAuthResolution
     {
         if (!is_array($request->get) || !$this->csrfManager->checkToken($request, $request->get)) {
+            $this->logger->debug('WebSocket: Invalid CSRF token');
+
             return new WebSocketAuthResolution(false);
         }
 
@@ -73,14 +78,23 @@ final readonly class RPCProtocolController extends WebSocketProtocolController
 
     public function onClose(Server $server, int $fd): void
     {
-        if (!$this->connectionControllers->hasKey($fd)) {
+        $connectionHandle = $this->connectionHandles->get($fd, null);
+
+        if (!$connectionHandle) {
             throw new RuntimeException(sprintf(
                 'RPC connection controller is not set and therefore it cannot be removed: %s',
                 $fd,
             ));
         }
 
-        $this->connectionControllers->remove($fd);
+        $connectionHandle->webSocketConnection->status = WebSocketConnectionStatus::Closed;
+
+        $this->webSocketRPCConnectionController?->onClose(
+            $connectionHandle->webSocketAuthResolution,
+            $connectionHandle->webSocketConnection,
+        );
+
+        $this->connectionHandles->remove($fd);
     }
 
     public function onMessage(Server $server, Frame $frame): void
@@ -103,25 +117,31 @@ final readonly class RPCProtocolController extends WebSocketProtocolController
 
     public function onOpen(Server $server, int $fd, WebSocketAuthResolution $webSocketAuthResolution): void
     {
-        $connectionController = new RPCConnectionController(
+        $webSocketConnection = new WebSocketConnection($server, $fd);
+        $connectionHandle = new WebSocketRPCConnectionHandle(
             $this->webSocketRPCResponderAggregate,
             $webSocketAuthResolution,
-            new WebSocketConnection($server, $fd),
+            $webSocketConnection,
+        );
+
+        $this->webSocketRPCConnectionController?->onOpen(
+            $webSocketAuthResolution,
+            $webSocketConnection,
         );
 
-        $this->connectionControllers->put($fd, $connectionController);
+        $this->connectionHandles->put($fd, $connectionHandle);
     }
 
-    private function getFrameController(Frame $frame): RPCConnectionController
+    private function getFrameController(Frame $frame): WebSocketRPCConnectionHandle
     {
-        if (!$this->connectionControllers->hasKey($frame->fd)) {
+        if (!$this->connectionHandles->hasKey($frame->fd)) {
             throw new RuntimeException(sprintf(
                 'RPC connection controller is not set and therefore it cannot handle a message: %s',
                 $frame->fd,
             ));
         }
 
-        return $this->connectionControllers->get($frame->fd);
+        return $this->connectionHandles->get($frame->fd);
     }
 
     private function onException(Server $server, Frame $frame, Throwable $exception): void
@@ -145,6 +165,7 @@ final readonly class RPCProtocolController extends WebSocketProtocolController
 
     private function onProtocolError(Server $server, Frame $frame, string $reason): void
     {
+        $this->logger->debug(sprintf('WebSocket Protocol Error: %s', $reason));
         $server->disconnect($frame->fd, SWOOLE_WEBSOCKET_CLOSE_PROTOCOL_ERROR, $reason);
     }
 }
diff --git a/src/WebSocketProtocolException/UnexpectedNotification.php b/src/WebSocketProtocolException/UnexpectedNotification.php
index 509dddf83b6b85891cb4764ee457b97df54edccb..e6f1f39ae7dc63cabfd38f82cd6041a9338da643 100644
--- a/src/WebSocketProtocolException/UnexpectedNotification.php
+++ b/src/WebSocketProtocolException/UnexpectedNotification.php
@@ -11,6 +11,6 @@ class UnexpectedNotification extends WebSocketProtocolException
 {
     public function __construct(RPCMethodInterface $rpcMethod)
     {
-        parent::__construct('RPC method must expect a response: '.$rpcMethod->getName());
+        parent::__construct('RPC method must expect a notification: '.$rpcMethod->getValue());
     }
 }
diff --git a/src/WebSocketProtocolException/UnexpectedRequest.php b/src/WebSocketProtocolException/UnexpectedRequest.php
index 82bbb079de23ab79b2579e3b38c6f278cffd3e8d..4e3d64f8be53d88b1d8969efb972db53ea6cc327 100644
--- a/src/WebSocketProtocolException/UnexpectedRequest.php
+++ b/src/WebSocketProtocolException/UnexpectedRequest.php
@@ -11,6 +11,6 @@ class UnexpectedRequest extends WebSocketProtocolException
 {
     public function __construct(RPCMethodInterface $rpcMethod)
     {
-        parent::__construct('RPC method must not expect a response: '.$rpcMethod->getName());
+        parent::__construct('RPC method must not expect a response: '.$rpcMethod->getValue());
     }
 }
diff --git a/src/WebSocketRPCConnectionController.php b/src/WebSocketRPCConnectionController.php
new file mode 100644
index 0000000000000000000000000000000000000000..d4a55383bca53d975b5e9e84cbf8465d6ff13efd
--- /dev/null
+++ b/src/WebSocketRPCConnectionController.php
@@ -0,0 +1,10 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Distantmagic\Resonance;
+
+abstract readonly class WebSocketRPCConnectionController implements WebSocketRPCConnectionControllerInterface
+{
+    public function onClose(WebSocketAuthResolution $webSocketAuthResolution, WebSocketConnection $webSocketConnection): void {}
+}
diff --git a/src/WebSocketRPCConnectionControllerInterface.php b/src/WebSocketRPCConnectionControllerInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..3b2ac58922a355a181050b5d8a1219729e292f89
--- /dev/null
+++ b/src/WebSocketRPCConnectionControllerInterface.php
@@ -0,0 +1,18 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Distantmagic\Resonance;
+
+interface WebSocketRPCConnectionControllerInterface
+{
+    public function onClose(
+        WebSocketAuthResolution $webSocketAuthResolution,
+        WebSocketConnection $webSocketConnection,
+    ): void;
+
+    public function onOpen(
+        WebSocketAuthResolution $webSocketAuthResolution,
+        WebSocketConnection $webSocketConnection,
+    ): void;
+}
diff --git a/src/WebSocketRPCConnectionHandle.php b/src/WebSocketRPCConnectionHandle.php
new file mode 100644
index 0000000000000000000000000000000000000000..06ac51d0c507ea8262462e65708a958b73285734
--- /dev/null
+++ b/src/WebSocketRPCConnectionHandle.php
@@ -0,0 +1,28 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Distantmagic\Resonance;
+
+use Distantmagic\Resonance\InputValidatedData\RPCMessage;
+
+readonly class WebSocketRPCConnectionHandle
+{
+    public function __construct(
+        public WebSocketRPCResponderAggregate $webSocketRPCResponderAggregate,
+        public WebSocketAuthResolution $webSocketAuthResolution,
+        public WebSocketConnection $webSocketConnection,
+    ) {}
+
+    public function onRPCMessage(RPCMessage $rpcMessage): void
+    {
+        $this
+            ->webSocketRPCResponderAggregate
+            ->respond(
+                $this->webSocketAuthResolution,
+                $this->webSocketConnection,
+                $rpcMessage
+            )
+        ;
+    }
+}
diff --git a/src/WebSocketRPCResponderAggregate.php b/src/WebSocketRPCResponderAggregate.php
index e6476f401065d2c9d8fe8d7cf548a97b5e4f66ee..9241dcaef3f288d215e7113a97135af01c093040 100644
--- a/src/WebSocketRPCResponderAggregate.php
+++ b/src/WebSocketRPCResponderAggregate.php
@@ -34,7 +34,7 @@ readonly class WebSocketRPCResponderAggregate
     private function selectResponder(RPCMessage $rpcMessage): WebSocketRPCResponderInterface
     {
         if (!$this->rpcResponders->hasKey($rpcMessage->method)) {
-            throw new DomainException('Unsupported RPC method: '.$rpcMessage->method->getName());
+            throw new DomainException('Unsupported RPC method: '.$rpcMessage->method->getValue());
         }
 
         return $this->rpcResponders->get($rpcMessage->method);
diff --git a/src/WebSocketServerController.php b/src/WebSocketServerController.php
index a9d8b72333b68c6b388d4e7020e3a5ec5e51a8f4..4abeead7c5945f55e934905917589ecc7e487699 100644
--- a/src/WebSocketServerController.php
+++ b/src/WebSocketServerController.php
@@ -7,7 +7,6 @@ namespace Distantmagic\Resonance;
 use Distantmagic\Resonance\Attribute\Singleton;
 use Ds\Map;
 use Psr\Log\LoggerInterface;
-use Swoole\Event;
 use Swoole\Http\Request;
 use Swoole\Http\Response;
 use Swoole\WebSocket\Frame;
@@ -23,6 +22,14 @@ final readonly class WebSocketServerController
      */
     private const HANDSHAKE_MAGIC_GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
 
+    private const MESSAGE_INVALID_HANDSHAKE = 'WebSocket invalid handshake';
+    private const MESSAGE_INVALID_SEC_WEBSOCKET = 'Invalid sec-websocket-key';
+    private const MESSAGE_NO_REQUEST_HEADERS = 'No request headers';
+    private const MESSAGE_NO_SEC_WEBSOCKET = 'Missing sec-websocket-key';
+    private const MESSAGE_NO_WEBSOCKET_CONTROLLER = 'WebSocket controller is not set for connection';
+    private const MESSAGE_NOT_AUTHORIZED = 'Not authorized to open WebSocket connection';
+    private const MESSAGE_PROTOCOL_NOT_SUPPORTED = 'None of the requested protocols is supported';
+    private const MESSAGE_UNEXPECTED_ONOPEN = 'Websocket open event fired despite implementing custom handshake';
     private const SEC_WEBSOCKET_KEY_BASE64_DECODED_BYTES_STRLEN = 16;
     private const SEC_WEBSOCKET_KEY_BASE64_STRLEN = 24;
 
@@ -51,13 +58,13 @@ final readonly class WebSocketServerController
     public function onHandshake(Server $server, Request $request, Response $response): void
     {
         if (!is_array($request->header)) {
-            $this->onInvalidHandshake($response, 'No request headers');
+            $this->onInvalidHandshake($response, self::MESSAGE_NO_REQUEST_HEADERS);
 
             return;
         }
 
         if (!isset($request->header['sec-websocket-key'])) {
-            $this->onInvalidHandshake($response, 'Missing sec-websocket-key');
+            $this->onInvalidHandshake($response, self::MESSAGE_NO_SEC_WEBSOCKET);
 
             return;
         }
@@ -68,7 +75,7 @@ final readonly class WebSocketServerController
         $secWebSocketKey = $request->header['sec-websocket-key'];
 
         if (!$this->isSecWebSocketKeyValid($secWebSocketKey)) {
-            $this->onInvalidHandshake($response, 'Invalid sec-websocket-key');
+            $this->onInvalidHandshake($response, self::MESSAGE_INVALID_SEC_WEBSOCKET);
 
             return;
         }
@@ -76,7 +83,7 @@ final readonly class WebSocketServerController
         $controllerResolution = $this->protocolControllerAggregate->resolveController($request);
 
         if (!$controllerResolution) {
-            $this->onInvalidHandshake($response, 'None of the requested protocols is supported');
+            $this->onInvalidHandshake($response, self::MESSAGE_PROTOCOL_NOT_SUPPORTED);
 
             return;
         }
@@ -84,7 +91,8 @@ final readonly class WebSocketServerController
         $authResolution = $controllerResolution->controller->isAuthorizedToConnect($request);
 
         if (!$authResolution->isAuthorizedToConnect) {
-            $response->status(403, 'Not authorized to open connection');
+            $this->logger->debug(self::MESSAGE_NOT_AUTHORIZED);
+            $response->status(403, self::MESSAGE_NOT_AUTHORIZED);
             $response->end();
 
             return;
@@ -93,10 +101,6 @@ final readonly class WebSocketServerController
         $fd = $request->fd;
         $this->protocolControllerAssignments->put($fd, $controllerResolution->controller);
 
-        Event::defer(static function () use ($authResolution, $controllerResolution, $fd, $server) {
-            $controllerResolution->controller->onOpen($server, $fd, $authResolution);
-        });
-
         $secWebSocketAccept = base64_encode(sha1($secWebSocketKey.self::HANDSHAKE_MAGIC_GUID, true));
 
         $response->header('connection', 'Upgrade');
@@ -107,12 +111,14 @@ final readonly class WebSocketServerController
 
         $response->status(101);
         $response->end();
+
+        $controllerResolution->controller->onOpen($server, $fd, $authResolution);
     }
 
     public function onMessage(Server $server, Frame $frame): void
     {
         if (!$this->protocolControllerAssignments->hasKey($frame->fd)) {
-            $this->logger->error('websocket controller is not set for connection: '.(string) $frame->fd);
+            $this->logger->error(self::MESSAGE_NO_WEBSOCKET_CONTROLLER);
             $server->disconnect($frame->fd, SWOOLE_WEBSOCKET_CLOSE_SERVER_ERROR);
 
             return;
@@ -123,7 +129,7 @@ final readonly class WebSocketServerController
 
     public function onOpen(Server $server, Request $request): void
     {
-        $this->logger->error('websocket open event fired despite implementing custom handshake');
+        $this->logger->error(self::MESSAGE_UNEXPECTED_ONOPEN);
         $server->disconnect($request->fd, SWOOLE_WEBSOCKET_CLOSE_SERVER_ERROR);
     }
 
@@ -144,7 +150,7 @@ final readonly class WebSocketServerController
 
     private function onInvalidHandshake(Response $response, string $reason): void
     {
-        $this->logger->debug('websocket invalid handshake: '.$reason);
+        $this->logger->debug(sprintf('%s: %s', self::MESSAGE_INVALID_HANDSHAKE, $reason));
 
         $response->status(400, $reason);
         $response->end();