Repository: symfony/ux (php)

Issue:
[Turbo] Add `mercure()` to `turbo_stream_listen()`
Hi,

For generate the URL of the Hub and for automatically set the cookie required to use the authorization mechanism, we need Twig helper `mercure()` (PR: https://github.com/symfony/mercure/pull/62)

But how to use it with `turbo_stream_listen()`?


Pull request description:
[Turbo] Pass EventSource options to `turbo_stream_listen`
| Q             | A
| ------------- | ---
| Bug fix?      | no
| New feature?  | yes
| Issues        | Fix #1860 I think
| License       | MIT

This allows to pass options to the EventSource, for example
`{{ turbo_stream_listen('a_topic', 'default', { withCredentials: true }) }}`
Or create specific auth cookie 
`{{ turbo_stream_listen('a_topic', 'default', { subscribe: '*' }) }}`
This functionality is similar to how `mercure()` works 


Patch:
diff --git a/src/Turbo/CHANGELOG.md b/src/Turbo/CHANGELOG.md
index 9bf757ac..fef22f69 100644
--- a/src/Turbo/CHANGELOG.md
+++ b/src/Turbo/CHANGELOG.md
@@ -3,6 +3,7 @@
 ## 2.24.0
 
 -   Add Twig Extensions for `meta` tags
+-   Add support for authentication to the EventSource via `turbo_stream_listen`
 
 ## 2.22.0
 
diff --git a/src/Turbo/assets/dist/turbo_stream_controller.d.ts b/src/Turbo/assets/dist/turbo_stream_controller.d.ts
index 2806afea..cc4db88a 100644
--- a/src/Turbo/assets/dist/turbo_stream_controller.d.ts
+++ b/src/Turbo/assets/dist/turbo_stream_controller.d.ts
@@ -4,11 +4,13 @@ export default class extends Controller {
         topic: StringConstructor;
         topics: ArrayConstructor;
         hub: StringConstructor;
+        withCredentials: BooleanConstructor;
     };
     es: EventSource | undefined;
     url: string | undefined;
     readonly topicValue: string;
     readonly topicsValue: string[];
+    readonly withCredentialsValue: boolean;
     readonly hubValue: string;
     readonly hasHubValue: boolean;
     readonly hasTopicValue: boolean;
diff --git a/src/Turbo/assets/dist/turbo_stream_controller.js b/src/Turbo/assets/dist/turbo_stream_controller.js
index 3d55567c..d5962232 100644
--- a/src/Turbo/assets/dist/turbo_stream_controller.js
+++ b/src/Turbo/assets/dist/turbo_stream_controller.js
@@ -23,7 +23,7 @@ class default_1 extends Controller {
     }
     connect() {
         if (this.url) {
-            this.es = new EventSource(this.url);
+            this.es = new EventSource(this.url, { withCredentials: this.withCredentialsValue });
             connectStreamSource(this.es);
         }
     }
@@ -38,6 +38,7 @@ default_1.values = {
     topic: String,
     topics: Array,
     hub: String,
+    withCredentials: Boolean,
 };
 
 export { default_1 as default };
diff --git a/src/Turbo/assets/src/turbo_stream_controller.ts b/src/Turbo/assets/src/turbo_stream_controller.ts
index 4c8fd4d9..aaa19c78 100644
--- a/src/Turbo/assets/src/turbo_stream_controller.ts
+++ b/src/Turbo/assets/src/turbo_stream_controller.ts
@@ -18,12 +18,14 @@ export default class extends Controller {
         topic: String,
         topics: Array,
         hub: String,
+        withCredentials: Boolean,
     };
     es: EventSource | undefined;
     url: string | undefined;
 
     declare readonly topicValue: string;
     declare readonly topicsValue: string[];
+    declare readonly withCredentialsValue: boolean;
     declare readonly hubValue: string;
     declare readonly hasHubValue: boolean;
     declare readonly hasTopicValue: boolean;
@@ -50,7 +52,7 @@ export default class extends Controller {
 
     connect() {
         if (this.url) {
-            this.es = new EventSource(this.url);
+            this.es = new EventSource(this.url, { withCredentials: this.withCredentialsValue });
             connectStreamSource(this.es);
         }
     }
diff --git a/src/Turbo/config/services.php b/src/Turbo/config/services.php
index 0344e95a..e3a3fabc 100644
--- a/src/Turbo/config/services.php
+++ b/src/Turbo/config/services.php
@@ -18,6 +18,7 @@ use Symfony\UX\Turbo\Broadcaster\ImuxBroadcaster;
 use Symfony\UX\Turbo\Broadcaster\TwigBroadcaster;
 use Symfony\UX\Turbo\Doctrine\BroadcastListener;
 use Symfony\UX\Turbo\Request\RequestListener;
+use Symfony\UX\Turbo\Twig\TurboRuntime;
 use Symfony\UX\Turbo\Twig\TwigExtension;
 
 /*
@@ -47,9 +48,15 @@ return static function (ContainerConfigurator $container): void {
             ->decorate('turbo.broadcaster.imux')
 
         ->set('turbo.twig.extension', TwigExtension::class)
-            ->args([tagged_locator('turbo.renderer.stream_listen', 'transport'), abstract_arg('default')])
             ->tag('twig.extension')
 
+        ->set('turbo.twig.runtime', TurboRuntime::class)
+            ->args([
+                tagged_locator('turbo.renderer.stream_listen', 'transport'),
+                abstract_arg('default_transport'),
+            ])
+            ->tag('twig.runtime')
+
         ->set('turbo.doctrine.event_listener', BroadcastListener::class)
             ->args([
                 service('turbo.broadcaster.imux'),
diff --git a/src/Turbo/doc/index.rst b/src/Turbo/doc/index.rst
index 9f5a5b7a..d215aaaa 100644
--- a/src/Turbo/doc/index.rst
+++ b/src/Turbo/doc/index.rst
@@ -754,6 +754,9 @@ Let's create our chat::
         </turbo-frame>
     {% endblock %}
 
+If you're using a private hub, you can add ``{ withCredentials: true }``
+as ``turbo_stream_listen()`` third argument to authenticate with the hub
+
 .. code-block:: html+twig
 
     {# chat/message.stream.html.twig #}
diff --git a/src/Turbo/src/Bridge/Mercure/TurboStreamListenRenderer.php b/src/Turbo/src/Bridge/Mercure/TurboStreamListenRenderer.php
index 68eadd82..a5e511ca 100644
--- a/src/Turbo/src/Bridge/Mercure/TurboStreamListenRenderer.php
+++ b/src/Turbo/src/Bridge/Mercure/TurboStreamListenRenderer.php
@@ -12,18 +12,20 @@
 namespace Symfony\UX\Turbo\Bridge\Mercure;
 
 use Symfony\Component\Mercure\HubInterface;
+use Symfony\Component\Mercure\Twig\MercureExtension;
 use Symfony\UX\StimulusBundle\Helper\StimulusHelper;
 use Symfony\UX\Turbo\Broadcaster\IdAccessor;
-use Symfony\UX\Turbo\Twig\TurboStreamListenRendererInterface;
+use Symfony\UX\Turbo\Twig\TurboStreamListenRendererWithOptionsInterface;
 use Symfony\WebpackEncoreBundle\Twig\StimulusTwigExtension;
 use Twig\Environment;
+use Twig\Error\RuntimeError;
 
 /**
  * Renders the attributes to load the "mercure-turbo-stream" controller.
  *
  * @author Kévin Dunglas <kevin@dunglas.fr>
  */
-final class TurboStreamListenRenderer implements TurboStreamListenRendererInterface
+final class TurboStreamListenRenderer implements TurboStreamListenRendererWithOptionsInterface
 {
     private StimulusHelper $stimulusHelper;
 
@@ -31,6 +33,7 @@ final class TurboStreamListenRenderer implements TurboStreamListenRendererInterf
         private HubInterface $hub,
         StimulusHelper|StimulusTwigExtension $stimulus,
         private IdAccessor $idAccessor,
+        private Environment $twig,
     ) {
         if ($stimulus instanceof StimulusTwigExtension) {
             trigger_deprecation('symfony/ux-turbo', '2.9', 'Passing an instance of "%s" as second argument of "%s" is deprecated, pass an instance of "%s" instead.', StimulusTwigExtension::class, __CLASS__, StimulusHelper::class);
@@ -42,8 +45,12 @@ final class TurboStreamListenRenderer implements TurboStreamListenRendererInterf
         $this->stimulusHelper = $stimulus;
     }
 
-    public function renderTurboStreamListen(Environment $env, $topic): string
+    public function renderTurboStreamListen(Environment $env, $topic /* array $eventSourceOptions = [] */): string
     {
+        if (\func_num_args() > 2) {
+            $eventSourceOptions = func_get_arg(2);
+        }
+
         $topics = $topic instanceof TopicSet
             ? array_map($this->resolveTopic(...), $topic->getTopics())
             : [$this->resolveTopic($topic)];
@@ -55,6 +62,18 @@ final class TurboStreamListenRenderer implements TurboStreamListenRendererInterf
             $controllerAttributes['topic'] = current($topics);
         }
 
+        if (isset($eventSourceOptions)) {
+            try {
+                $mercure = $this->twig->getExtension(MercureExtension::class);
+                $mercure->mercure($topics, $eventSourceOptions);
+
+                if (isset($eventSourceOptions['withCredentials'])) {
+                    $controllerAttributes['withCredentials'] = $eventSourceOptions['withCredentials'];
+                }
+            } catch (RuntimeError $e) {
+            }
+        }
+
         $stimulusAttributes = $this->stimulusHelper->createStimulusAttributes();
         $stimulusAttributes->addController(
             'symfony/ux-turbo/mercure-turbo-stream',
diff --git a/src/Turbo/src/DependencyInjection/Compiler/RegisterMercureHubsPass.php b/src/Turbo/src/DependencyInjection/Compiler/RegisterMercureHubsPass.php
new file mode 100644
index 00000000..ebe97b68
--- /dev/null
+++ b/src/Turbo/src/DependencyInjection/Compiler/RegisterMercureHubsPass.php
@@ -0,0 +1,53 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\UX\Turbo\DependencyInjection\Compiler;
+
+use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\DependencyInjection\Reference;
+use Symfony\UX\Turbo\Bridge\Mercure\Broadcaster;
+use Symfony\UX\Turbo\Bridge\Mercure\TurboStreamListenRenderer;
+
+/**
+ * This compiler pass ensures that TurboStreamListenRenderer
+ * and Broadcast are registered per Mercure hub.
+ *
+ * @author Pierre Ambroise<pierre27.ambroise@gmail.com>
+ */
+final class RegisterMercureHubsPass implements CompilerPassInterface
+{
+    public function process(ContainerBuilder $container)
+    {
+        foreach ($container->findTaggedServiceIds('mercure.hub') as $hubId => $tags) {
+            $name = str_replace('mercure.hub.', '', $hubId);
+
+            $container->register("turbo.mercure.$name.renderer", TurboStreamListenRenderer::class)
+                ->addArgument(new Reference($hubId))
+                ->addArgument(new Reference('turbo.mercure.stimulus_helper'))
+                ->addArgument(new Reference('turbo.id_accessor'))
+                ->addArgument(new Reference('twig'))
+                ->addTag('turbo.renderer.stream_listen', ['transport' => $name]);
+
+            foreach ($tags as $tag) {
+                if (isset($tag['default']) && $tag['default'] && 'default' !== $name) {
+                    $container->getDefinition("turbo.mercure.$name.renderer")
+                        ->addTag('turbo.renderer.stream_listen', ['transport' => 'default']);
+                }
+            }
+
+            $container->register("turbo.mercure.$name.broadcaster", Broadcaster::class)
+                ->addArgument($name)
+                ->addArgument(new Reference($hubId))
+                ->addTag('turbo.broadcaster');
+        }
+    }
+}
diff --git a/src/Turbo/src/DependencyInjection/TurboExtension.php b/src/Turbo/src/DependencyInjection/TurboExtension.php
index ca53f6d2..761d8e4b 100644
--- a/src/Turbo/src/DependencyInjection/TurboExtension.php
+++ b/src/Turbo/src/DependencyInjection/TurboExtension.php
@@ -37,7 +37,7 @@ final class TurboExtension extends Extension implements PrependExtensionInterfac
 
         $loader = (new PhpFileLoader($container, new FileLocator(\dirname(__DIR__).'/../config')));
         $loader->load('services.php');
-        $container->getDefinition('turbo.twig.extension')->replaceArgument(1, $config['default_transport']);
+        $container->getDefinition('turbo.twig.runtime')->replaceArgument(1, $config['default_transport']);
 
         $this->registerTwig($config, $container);
         $this->registerBroadcast($config, $container, $loader);
diff --git a/src/Turbo/src/TurboBundle.php b/src/Turbo/src/TurboBundle.php
index e1524e4b..ca149e8f 100644
--- a/src/Turbo/src/TurboBundle.php
+++ b/src/Turbo/src/TurboBundle.php
@@ -15,6 +15,7 @@ use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
 use Symfony\Component\DependencyInjection\Compiler\PassConfig;
 use Symfony\Component\DependencyInjection\ContainerBuilder;
 use Symfony\Component\HttpKernel\Bundle\Bundle;
+use Symfony\UX\Turbo\DependencyInjection\Compiler\RegisterMercureHubsPass;
 
 /**
  * @author Kévin Dunglas <kevin@dunglas.fr>
@@ -28,6 +29,8 @@ final class TurboBundle extends Bundle
     {
         parent::build($container);
 
+        $container->addCompilerPass(new RegisterMercureHubsPass());
+
         $container->addCompilerPass(new class implements CompilerPassInterface {
             public function process(ContainerBuilder $container): void
             {
diff --git a/src/Turbo/src/Twig/TurboRuntime.php b/src/Turbo/src/Twig/TurboRuntime.php
new file mode 100644
index 00000000..30238c64
--- /dev/null
+++ b/src/Turbo/src/Twig/TurboRuntime.php
@@ -0,0 +1,55 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\UX\Turbo\Twig;
+
+use Psr\Container\ContainerInterface;
+use Symfony\UX\Turbo\Bridge\Mercure\TopicSet;
+use Twig\Environment;
+use Twig\Extension\RuntimeExtensionInterface;
+
+/**
+ * @author Kévin Dunglas <kevin@dunglas.fr>
+ * @author Pierre Ambroise <pierre27.ambroise@gmail.com>
+ *
+ * @internal
+ */
+final class TurboRuntime implements RuntimeExtensionInterface
+{
+    public function __construct(
+        private ContainerInterface $turboStreamListenRenderers,
+        private readonly string $defaultTransport,
+    ) {
+    }
+
+    /**
+     * @param object|string|array<object|string> $topic
+     * @param array<string, mixed>               $options
+     */
+    public function renderTurboStreamListen(Environment $env, $topic, ?string $transport = null, array $options = []): string
+    {
+        $options['transport'] = $transport ??= $this->defaultTransport;
+
+        if (!$this->turboStreamListenRenderers->has($transport)) {
+            throw new \InvalidArgumentException(\sprintf('The Turbo stream transport "%s" does not exist.', $transport));
+        }
+
+        if (\is_array($topic)) {
+            $topic = new TopicSet($topic);
+        }
+
+        $renderer = $this->turboStreamListenRenderers->get($transport);
+
+        return $renderer instanceof TurboStreamListenRendererWithOptionsInterface
+            ? $renderer->renderTurboStreamListen($env, $topic, $options) // @phpstan-ignore-line
+            : $renderer->renderTurboStreamListen($env, $topic);
+    }
+}
diff --git a/src/Turbo/src/Twig/TurboStreamListenRendererInterface.php b/src/Turbo/src/Twig/TurboStreamListenRendererInterface.php
index 3670e40b..24072131 100644
--- a/src/Turbo/src/Twig/TurboStreamListenRendererInterface.php
+++ b/src/Turbo/src/Twig/TurboStreamListenRendererInterface.php
@@ -23,5 +23,5 @@ interface TurboStreamListenRendererInterface
     /**
      * @param string|object $topic
      */
-    public function renderTurboStreamListen(Environment $env, $topic): string;
+    public function renderTurboStreamListen(Environment $env, $topic /* , array $eventSourceOptions = [] */): string;
 }
diff --git a/src/Turbo/src/Twig/TurboStreamListenRendererWithOptionsInterface.php b/src/Turbo/src/Twig/TurboStreamListenRendererWithOptionsInterface.php
new file mode 100644
index 00000000..6364fe3b
--- /dev/null
+++ b/src/Turbo/src/Twig/TurboStreamListenRendererWithOptionsInterface.php
@@ -0,0 +1,19 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\UX\Turbo\Twig;
+
+/**
+ * @internal
+ */
+interface TurboStreamListenRendererWithOptionsInterface extends TurboStreamListenRendererInterface
+{
+}
diff --git a/src/Turbo/src/Twig/TwigExtension.php b/src/Turbo/src/Twig/TwigExtension.php
index eb7aedfe..f3e992ce 100644
--- a/src/Turbo/src/Twig/TwigExtension.php
+++ b/src/Turbo/src/Twig/TwigExtension.php
@@ -11,9 +11,6 @@
 
 namespace Symfony\UX\Turbo\Twig;
 
-use Psr\Container\ContainerInterface;
-use Symfony\UX\Turbo\Bridge\Mercure\TopicSet;
-use Twig\Environment;
 use Twig\Extension\AbstractExtension;
 use Twig\TwigFunction;
 
@@ -28,16 +25,10 @@ final class TwigExtension extends AbstractExtension
     private const REFRESH_SCROLL_RESET = 'reset';
     private const REFRESH_SCROLL_PRESERVE = 'preserve';
 
-    public function __construct(
-        private ContainerInterface $turboStreamListenRenderers,
-        private string $default,
-    ) {
-    }
-
     public function getFunctions(): array
     {
         return [
-            new TwigFunction('turbo_stream_listen', $this->turboStreamListen(...), ['needs_environment' => true, 'is_safe' => ['html']]),
+            new TwigFunction('turbo_stream_listen', [TurboRuntime::class, 'renderTurboStreamListen'], ['needs_environment' => true, 'is_safe' => ['html']]),
             new TwigFunction('turbo_exempts_page_from_cache', $this->turboExemptsPageFromCache(...), ['is_safe' => ['html']]),
             new TwigFunction('turbo_exempts_page_from_preview', $this->turboExemptsPageFromPreview(...), ['is_safe' => ['html']]),
             new TwigFunction('turbo_page_requires_reload', $this->turboPageRequiresReload(...), ['is_safe' => ['html']]),
@@ -47,24 +38,6 @@ final class TwigExtension extends AbstractExtension
         ];
     }
 
-    /**
-     * @param object|string|array<object|string> $topic
-     */
-    public function turboStreamListen(Environment $env, $topic, ?string $transport = null): string
-    {
-        $transport ??= $this->default;
-
-        if (!$this->turboStreamListenRenderers->has($transport)) {
-            throw new \InvalidArgumentException(\sprintf('The Turbo stream transport "%s" does not exist.', $transport));
-        }
-
-        if (\is_array($topic)) {
-            $topic = new TopicSet($topic);
-        }
-
-        return $this->turboStreamListenRenderers->get($transport)->renderTurboStreamListen($env, $topic);
-    }
-
     /**
      * Generates a <meta> tag to disable caching of a page.
      *


Test patch:
diff --git a/src/Turbo/tests/Bridge/Mercure/TurboStreamListenRendererTest.php b/src/Turbo/tests/Bridge/Mercure/TurboStreamListenRendererTest.php
index 9b19ba4d..108715b0 100644
--- a/src/Turbo/tests/Bridge/Mercure/TurboStreamListenRendererTest.php
+++ b/src/Turbo/tests/Bridge/Mercure/TurboStreamListenRendererTest.php
@@ -71,5 +71,13 @@ final class TurboStreamListenRendererTest extends KernelTestCase
                 ? 'data-controller="symfony--ux-turbo--mercure-turbo-stream" data-symfony--ux-turbo--mercure-turbo-stream-hub-value="http://127.0.0.1:3000/.well-known/mercure" data-symfony--ux-turbo--mercure-turbo-stream-topics-value="[&quot;a_topic&quot;,&quot;AppEntityBook&quot;,&quot;https:\/\/symfony.com\/ux-turbo\/App%5CEntity%5CBook\/123&quot;]"'
                 : 'data-controller="symfony--ux-turbo--mercure-turbo-stream" data-symfony--ux-turbo--mercure-turbo-stream-hub-value="http&#x3A;&#x2F;&#x2F;127.0.0.1&#x3A;3000&#x2F;.well-known&#x2F;mercure" data-symfony--ux-turbo--mercure-turbo-stream-topics-value="&#x5B;&quot;a_topic&quot;,&quot;AppEntityBook&quot;,&quot;https&#x3A;&#x5C;&#x2F;&#x5C;&#x2F;symfony.com&#x5C;&#x2F;ux-turbo&#x5C;&#x2F;App&#x25;5CEntity&#x25;5CBook&#x5C;&#x2F;123&quot;&#x5D;"',
         ];
+
+        yield [
+            "{{ turbo_stream_listen('a_topic', 'default', { withCredentials: true }) }}",
+            [],
+            $newEscape
+                ? 'data-controller="symfony--ux-turbo--mercure-turbo-stream" data-symfony--ux-turbo--mercure-turbo-stream-hub-value="http://127.0.0.1:3000/.well-known/mercure" data-symfony--ux-turbo--mercure-turbo-stream-topic-value="a_topic" data-symfony--ux-turbo--mercure-turbo-stream-with-credentials-value="true"'
+                : 'data-controller="symfony--ux-turbo--mercure-turbo-stream" data-symfony--ux-turbo--mercure-turbo-stream-hub-value="http&#x3A;&#x2F;&#x2F;127.0.0.1&#x3A;3000&#x2F;.well-known&#x2F;mercure" data-symfony--ux-turbo--mercure-turbo-stream-topic-value="a_topic" data-symfony--ux-turbo--mercure-turbo-stream-with-credentials-value="true"',
+        ];
     }
 }
diff --git a/src/Turbo/tests/Compiler/RegisterMercureHubsPassTest.php b/src/Turbo/tests/Compiler/RegisterMercureHubsPassTest.php
new file mode 100644
index 00000000..5d369ab8
--- /dev/null
+++ b/src/Turbo/tests/Compiler/RegisterMercureHubsPassTest.php
@@ -0,0 +1,51 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace src\Turbo\tests\Compiler;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\UX\Turbo\DependencyInjection\Compiler\RegisterMercureHubsPass;
+
+final class RegisterMercureHubsPassTest extends TestCase
+{
+    public function testProcess(): void
+    {
+        $pass = new RegisterMercureHubsPass();
+
+        $container = new ContainerBuilder();
+        $container->register('hub')
+            ->addTag('mercure.hub');
+
+        $pass->process($container);
+
+        $this->assertTrue($container->has('turbo.mercure.hub.renderer'));
+        $this->assertTrue($container->has('turbo.mercure.hub.broadcaster'));
+    }
+
+    public function testProcessWithDefault(): void
+    {
+        $pass = new RegisterMercureHubsPass();
+
+        $container = new ContainerBuilder();
+        $container->register('hub1')
+            ->addTag('mercure.hub');
+
+        $container->register('default_hub')
+            ->addTag('mercure.hub', ['default' => true]);
+
+        $pass->process($container);
+
+        $this->assertSame([
+            'transport' => 'default',
+        ], $container->getDefinition('turbo.mercure.default_hub.renderer')->getTag('turbo.renderer.stream_listen')[1]);
+    }
+}