Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 39
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiSseController
0.00% covered (danger)
0.00%
0 / 39
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
90
0.00% covered (danger)
0.00%
0 / 1
 sse
0.00% covered (danger)
0.00%
0 / 39
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
90
1<?php
2
3declare(strict_types=1);
4
5namespace Drupal\display_builder\Controller;
6
7use Drupal\display_builder\Event\DisplayBuilderEvents;
8use Drupal\display_builder\InstanceInterface;
9use Symfony\Component\HttpFoundation\EventStreamResponse;
10use Symfony\Component\HttpFoundation\ServerEvent;
11
12/**
13 * Returns responses for Display builder routes.
14 */
15class ApiSseController extends ApiControllerBase {
16
17  /**
18   * Check if the builder needs to be refreshed after this period, in seconds.
19   */
20  public const int REFRESH_WINDOW = 2;
21
22  /**
23   * Refresh builder only if the last edit is older than this window.
24   */
25  public const int STALE = (self::REFRESH_WINDOW * 2) - 1;
26
27  /**
28   * Stream server side events for real-time collaboration.
29   *
30   * Update islands every time another user is triggering a state altering
31   * event: ON_UPDATE, ON_ATTACH_TO_ROOT, ON_ATTACH_TO_SLOT, ON_MOVE,
32   * ON_DELETE, ON_PRESET_SAVE, ON_SAVE and ON_HISTORY_CHANGE.
33   * Skip ON_ACTIVE.
34   *
35   * @param \Drupal\display_builder\InstanceInterface $display_builder_instance
36   *   Display builder instance.
37   *
38   * @return \Symfony\Component\HttpFoundation\EventStreamResponse
39   *   The event stream response.
40   *
41   * @see https://v1.htmx.org/extensions/server-sent-events/
42   * @see https://symfony.com/blog/new-in-symfony-7-3-simpler-server-event-streaming
43   */
44  public function sse(InstanceInterface $display_builder_instance): EventStreamResponse {
45    $builder_id = (string) $display_builder_instance->id();
46
47    return new EventStreamResponse(function () use ($builder_id) {
48      $sessionId = $this->session->getId();
49      $collection = $this->sharedTempStoreFactory->get($this::SSE_COLLECTION);
50
51      // Infinite loop because we keep the HTTP transaction open until it is
52      // closed by the client.
53      // @phpstan-ignore-next-line
54      while (TRUE) {
55        // We start by sending an empty event to avoid HTTP timeout. We need
56        // at least the HTTP response headers to be sent back to client without
57        // waiting for a real event to occur.
58        yield new ServerEvent('');
59        $latest = $collection->get("{$builder_id}_latest");
60
61        if (!$latest) {
62          \sleep($this::REFRESH_WINDOW);
63
64          continue;
65        }
66
67        // The current session is the last one editing the builder. Do nothing.
68        // Use session ID to handle:
69        // - different users
70        // - the same user in different browsers
71        // Not handled: multiple tabs with the same user in the same browser.
72        if ($latest['sessionId'] === $sessionId) {
73          \sleep($this::REFRESH_WINDOW);
74
75          continue;
76        }
77
78        // If the last edit is older than STALE, do nothing
79        // as we consider that the builder has already been refreshed.
80        if ($latest['timestamp'] < $this->time->getCurrentTime() - $this::STALE) {
81          \sleep($this::REFRESH_WINDOW);
82
83          continue;
84        }
85
86        // Reload the instance to ensure we get the builder's latest
87        // state.
88        $builder = $this->entityTypeManager()->getStorage('display_builder_instance')
89          ->loadUnchanged($builder_id);
90
91        if (!$builder instanceof InstanceInterface) {
92          \sleep($this::REFRESH_WINDOW);
93
94          continue;
95        }
96        $this->builder = $builder;
97
98        // Recompute islands regarding the current user.
99        // Use ON_HISTORY_CHANGE because it is the event where most islands are
100        // updated.
101        $event = $this->createEventWithEnabledIsland(
102          DisplayBuilderEvents::ON_HISTORY_CHANGE,
103          $builder->getCurrentState(),
104          NULL,
105          NULL,
106        );
107
108        foreach ($event->getResult() as $island_id => $result) {
109          // Do nothing if the island is not updated.
110          if (empty($result)) {
111            continue;
112          }
113
114          // Do nothing if the island is empty.
115          if (!isset($result['content'])) {
116            continue;
117          }
118
119          $sse_target = "island-{$builder->id()}-{$island_id}";
120          $data = $this->renderer->renderInIsolation($result['content']);
121
122          // Need to send the data back as one line.
123          // @see https://www.reddit.com/r/htmx/comments/1mjhzl1/comment/n7bdf0s
124          $data = \str_replace(["\r\n", "\r", "\n"], ' ', (string) $data);
125
126          yield new ServerEvent($data, type: $sse_target);
127        }
128
129        \sleep($this::REFRESH_WINDOW);
130      }
131    });
132  }
133
134}

Paths

Below are the source code lines that represent each code path as identified by Xdebug. Please note a path is not necessarily coterminous with a line, a line may contain multiple paths and therefore show up more than once. Please also be aware that some paths may include implicit rather than explicit branches, e.g. an if statement always has an else as part of its logical flow even if you didn't write one.

ApiSseController->sse
44  public function sse(InstanceInterface $display_builder_instance): EventStreamResponse {
45    $builder_id = (string) $display_builder_instance->id();
46
47    return new EventStreamResponse(function () use ($builder_id) {
48      $sessionId = $this->session->getId();
49      $collection = $this->sharedTempStoreFactory->get($this::SSE_COLLECTION);
50
51      // Infinite loop because we keep the HTTP transaction open until it is
52      // closed by the client.
53      // @phpstan-ignore-next-line
54      while (TRUE) {
55        // We start by sending an empty event to avoid HTTP timeout. We need
56        // at least the HTTP response headers to be sent back to client without
57        // waiting for a real event to occur.
58        yield new ServerEvent('');
59        $latest = $collection->get("{$builder_id}_latest");
60
61        if (!$latest) {
62          \sleep($this::REFRESH_WINDOW);
63
64          continue;
65        }
66
67        // The current session is the last one editing the builder. Do nothing.
68        // Use session ID to handle:
69        // - different users
70        // - the same user in different browsers
71        // Not handled: multiple tabs with the same user in the same browser.
72        if ($latest['sessionId'] === $sessionId) {
73          \sleep($this::REFRESH_WINDOW);
74
75          continue;
76        }
77
78        // If the last edit is older than STALE, do nothing
79        // as we consider that the builder has already been refreshed.
80        if ($latest['timestamp'] < $this->time->getCurrentTime() - $this::STALE) {
81          \sleep($this::REFRESH_WINDOW);
82
83          continue;
84        }
85
86        // Reload the instance to ensure we get the builder's latest
87        // state.
88        $builder = $this->entityTypeManager()->getStorage('display_builder_instance')
89          ->loadUnchanged($builder_id);
90
91        if (!$builder instanceof InstanceInterface) {
92          \sleep($this::REFRESH_WINDOW);
93
94          continue;
95        }
96        $this->builder = $builder;
97
98        // Recompute islands regarding the current user.
99        // Use ON_HISTORY_CHANGE because it is the event where most islands are
100        // updated.
101        $event = $this->createEventWithEnabledIsland(
102          DisplayBuilderEvents::ON_HISTORY_CHANGE,
103          $builder->getCurrentState(),
104          NULL,
105          NULL,
106        );
107
108        foreach ($event->getResult() as $island_id => $result) {
109          // Do nothing if the island is not updated.
110          if (empty($result)) {
111            continue;
112          }
113
114          // Do nothing if the island is empty.
115          if (!isset($result['content'])) {
116            continue;
117          }
118
119          $sse_target = "island-{$builder->id()}-{$island_id}";
120          $data = $this->renderer->renderInIsolation($result['content']);
121
122          // Need to send the data back as one line.
123          // @see https://www.reddit.com/r/htmx/comments/1mjhzl1/comment/n7bdf0s
124          $data = \str_replace(["\r\n", "\r", "\n"], ' ', (string) $data);
125
126          yield new ServerEvent($data, type: $sse_target);
127        }
128
129        \sleep($this::REFRESH_WINDOW);
130      }
131    });
{closure:/var/www/html/web/modules/custom/display_builder/src/Controller/ApiSseController.php:47-131}
47    return new EventStreamResponse(function () use ($builder_id) {
48      $sessionId = $this->session->getId();
49      $collection = $this->sharedTempStoreFactory->get($this::SSE_COLLECTION);
50
51      // Infinite loop because we keep the HTTP transaction open until it is
52      // closed by the client.
53      // @phpstan-ignore-next-line
54      while (TRUE) {
55        // We start by sending an empty event to avoid HTTP timeout. We need
56        // at least the HTTP response headers to be sent back to client without
57        // waiting for a real event to occur.
58        yield new ServerEvent('');
 
58        yield new ServerEvent('');
59        $latest = $collection->get("{$builder_id}_latest");
60
61        if (!$latest) {
 
62          \sleep($this::REFRESH_WINDOW);
63
64          continue;
 
58        yield new ServerEvent('');
59        $latest = $collection->get("{$builder_id}_latest");
60
61        if (!$latest) {
 
72        if ($latest['sessionId'] === $sessionId) {
 
73          \sleep($this::REFRESH_WINDOW);
74
75          continue;
 
58        yield new ServerEvent('');
59        $latest = $collection->get("{$builder_id}_latest");
60
61        if (!$latest) {
47    return new EventStreamResponse(function () use ($builder_id) {
48      $sessionId = $this->session->getId();
49      $collection = $this->sharedTempStoreFactory->get($this::SSE_COLLECTION);
50
51      // Infinite loop because we keep the HTTP transaction open until it is
52      // closed by the client.
53      // @phpstan-ignore-next-line
54      while (TRUE) {
55        // We start by sending an empty event to avoid HTTP timeout. We need
56        // at least the HTTP response headers to be sent back to client without
57        // waiting for a real event to occur.
58        yield new ServerEvent('');
 
58        yield new ServerEvent('');
59        $latest = $collection->get("{$builder_id}_latest");
60
61        if (!$latest) {
 
62          \sleep($this::REFRESH_WINDOW);
63
64          continue;
 
58        yield new ServerEvent('');
59        $latest = $collection->get("{$builder_id}_latest");
60
61        if (!$latest) {
 
72        if ($latest['sessionId'] === $sessionId) {
 
80        if ($latest['timestamp'] < $this->time->getCurrentTime() - $this::STALE) {
 
81          \sleep($this::REFRESH_WINDOW);
82
83          continue;
 
58        yield new ServerEvent('');
59        $latest = $collection->get("{$builder_id}_latest");
60
61        if (!$latest) {
47    return new EventStreamResponse(function () use ($builder_id) {
48      $sessionId = $this->session->getId();
49      $collection = $this->sharedTempStoreFactory->get($this::SSE_COLLECTION);
50
51      // Infinite loop because we keep the HTTP transaction open until it is
52      // closed by the client.
53      // @phpstan-ignore-next-line
54      while (TRUE) {
55        // We start by sending an empty event to avoid HTTP timeout. We need
56        // at least the HTTP response headers to be sent back to client without
57        // waiting for a real event to occur.
58        yield new ServerEvent('');
 
58        yield new ServerEvent('');
59        $latest = $collection->get("{$builder_id}_latest");
60
61        if (!$latest) {
 
62          \sleep($this::REFRESH_WINDOW);
63
64          continue;
 
58        yield new ServerEvent('');
59        $latest = $collection->get("{$builder_id}_latest");
60
61        if (!$latest) {
 
72        if ($latest['sessionId'] === $sessionId) {
 
80        if ($latest['timestamp'] < $this->time->getCurrentTime() - $this::STALE) {
 
88        $builder = $this->entityTypeManager()->getStorage('display_builder_instance')
89          ->loadUnchanged($builder_id);
90
91        if (!$builder instanceof InstanceInterface) {
 
92          \sleep($this::REFRESH_WINDOW);
93
94          continue;
 
58        yield new ServerEvent('');
59        $latest = $collection->get("{$builder_id}_latest");
60
61        if (!$latest) {
47    return new EventStreamResponse(function () use ($builder_id) {
48      $sessionId = $this->session->getId();
49      $collection = $this->sharedTempStoreFactory->get($this::SSE_COLLECTION);
50
51      // Infinite loop because we keep the HTTP transaction open until it is
52      // closed by the client.
53      // @phpstan-ignore-next-line
54      while (TRUE) {
55        // We start by sending an empty event to avoid HTTP timeout. We need
56        // at least the HTTP response headers to be sent back to client without
57        // waiting for a real event to occur.
58        yield new ServerEvent('');
 
58        yield new ServerEvent('');
59        $latest = $collection->get("{$builder_id}_latest");
60
61        if (!$latest) {
 
62          \sleep($this::REFRESH_WINDOW);
63
64          continue;
 
58        yield new ServerEvent('');
59        $latest = $collection->get("{$builder_id}_latest");
60
61        if (!$latest) {
 
72        if ($latest['sessionId'] === $sessionId) {
 
80        if ($latest['timestamp'] < $this->time->getCurrentTime() - $this::STALE) {
 
88        $builder = $this->entityTypeManager()->getStorage('display_builder_instance')
89          ->loadUnchanged($builder_id);
90
91        if (!$builder instanceof InstanceInterface) {
 
96        $this->builder = $builder;
97
98        // Recompute islands regarding the current user.
99        // Use ON_HISTORY_CHANGE because it is the event where most islands are
100        // updated.
101        $event = $this->createEventWithEnabledIsland(
102          DisplayBuilderEvents::ON_HISTORY_CHANGE,
103          $builder->getCurrentState(),
104          NULL,
105          NULL,
106        );
107
108        foreach ($event->getResult() as $island_id => $result) {
 
108        foreach ($event->getResult() as $island_id => $result) {
 
108        foreach ($event->getResult() as $island_id => $result) {
109          // Do nothing if the island is not updated.
110          if (empty($result)) {
 
111            continue;
 
108        foreach ($event->getResult() as $island_id => $result) {
 
54      while (TRUE) {
55        // We start by sending an empty event to avoid HTTP timeout. We need
56        // at least the HTTP response headers to be sent back to client without
57        // waiting for a real event to occur.
58        yield new ServerEvent('');
59        $latest = $collection->get("{$builder_id}_latest");
60
61        if (!$latest) {
62          \sleep($this::REFRESH_WINDOW);
63
64          continue;
65        }
66
67        // The current session is the last one editing the builder. Do nothing.
68        // Use session ID to handle:
69        // - different users
70        // - the same user in different browsers
71        // Not handled: multiple tabs with the same user in the same browser.
72        if ($latest['sessionId'] === $sessionId) {
73          \sleep($this::REFRESH_WINDOW);
74
75          continue;
76        }
77
78        // If the last edit is older than STALE, do nothing
79        // as we consider that the builder has already been refreshed.
80        if ($latest['timestamp'] < $this->time->getCurrentTime() - $this::STALE) {
81          \sleep($this::REFRESH_WINDOW);
82
83          continue;
84        }
85
86        // Reload the instance to ensure we get the builder's latest
87        // state.
88        $builder = $this->entityTypeManager()->getStorage('display_builder_instance')
89          ->loadUnchanged($builder_id);
90
91        if (!$builder instanceof InstanceInterface) {
92          \sleep($this::REFRESH_WINDOW);
93
94          continue;
95        }
96        $this->builder = $builder;
97
98        // Recompute islands regarding the current user.
99        // Use ON_HISTORY_CHANGE because it is the event where most islands are
100        // updated.
101        $event = $this->createEventWithEnabledIsland(
102          DisplayBuilderEvents::ON_HISTORY_CHANGE,
103          $builder->getCurrentState(),
104          NULL,
105          NULL,
106        );
107
108        foreach ($event->getResult() as $island_id => $result) {
 
58        yield new ServerEvent('');
59        $latest = $collection->get("{$builder_id}_latest");
60
61        if (!$latest) {
47    return new EventStreamResponse(function () use ($builder_id) {
48      $sessionId = $this->session->getId();
49      $collection = $this->sharedTempStoreFactory->get($this::SSE_COLLECTION);
50
51      // Infinite loop because we keep the HTTP transaction open until it is
52      // closed by the client.
53      // @phpstan-ignore-next-line
54      while (TRUE) {
55        // We start by sending an empty event to avoid HTTP timeout. We need
56        // at least the HTTP response headers to be sent back to client without
57        // waiting for a real event to occur.
58        yield new ServerEvent('');
 
58        yield new ServerEvent('');
59        $latest = $collection->get("{$builder_id}_latest");
60
61        if (!$latest) {
 
62          \sleep($this::REFRESH_WINDOW);
63
64          continue;
 
58        yield new ServerEvent('');
59        $latest = $collection->get("{$builder_id}_latest");
60
61        if (!$latest) {
 
72        if ($latest['sessionId'] === $sessionId) {
 
80        if ($latest['timestamp'] < $this->time->getCurrentTime() - $this::STALE) {
 
88        $builder = $this->entityTypeManager()->getStorage('display_builder_instance')
89          ->loadUnchanged($builder_id);
90
91        if (!$builder instanceof InstanceInterface) {
 
96        $this->builder = $builder;
97
98        // Recompute islands regarding the current user.
99        // Use ON_HISTORY_CHANGE because it is the event where most islands are
100        // updated.
101        $event = $this->createEventWithEnabledIsland(
102          DisplayBuilderEvents::ON_HISTORY_CHANGE,
103          $builder->getCurrentState(),
104          NULL,
105          NULL,
106        );
107
108        foreach ($event->getResult() as $island_id => $result) {
 
108        foreach ($event->getResult() as $island_id => $result) {
 
108        foreach ($event->getResult() as $island_id => $result) {
109          // Do nothing if the island is not updated.
110          if (empty($result)) {
 
115          if (!isset($result['content'])) {
 
116            continue;
 
108        foreach ($event->getResult() as $island_id => $result) {
 
54      while (TRUE) {
55        // We start by sending an empty event to avoid HTTP timeout. We need
56        // at least the HTTP response headers to be sent back to client without
57        // waiting for a real event to occur.
58        yield new ServerEvent('');
59        $latest = $collection->get("{$builder_id}_latest");
60
61        if (!$latest) {
62          \sleep($this::REFRESH_WINDOW);
63
64          continue;
65        }
66
67        // The current session is the last one editing the builder. Do nothing.
68        // Use session ID to handle:
69        // - different users
70        // - the same user in different browsers
71        // Not handled: multiple tabs with the same user in the same browser.
72        if ($latest['sessionId'] === $sessionId) {
73          \sleep($this::REFRESH_WINDOW);
74
75          continue;
76        }
77
78        // If the last edit is older than STALE, do nothing
79        // as we consider that the builder has already been refreshed.
80        if ($latest['timestamp'] < $this->time->getCurrentTime() - $this::STALE) {
81          \sleep($this::REFRESH_WINDOW);
82
83          continue;
84        }
85
86        // Reload the instance to ensure we get the builder's latest
87        // state.
88        $builder = $this->entityTypeManager()->getStorage('display_builder_instance')
89          ->loadUnchanged($builder_id);
90
91        if (!$builder instanceof InstanceInterface) {
92          \sleep($this::REFRESH_WINDOW);
93
94          continue;
95        }
96        $this->builder = $builder;
97
98        // Recompute islands regarding the current user.
99        // Use ON_HISTORY_CHANGE because it is the event where most islands are
100        // updated.
101        $event = $this->createEventWithEnabledIsland(
102          DisplayBuilderEvents::ON_HISTORY_CHANGE,
103          $builder->getCurrentState(),
104          NULL,
105          NULL,
106        );
107
108        foreach ($event->getResult() as $island_id => $result) {
 
58        yield new ServerEvent('');
59        $latest = $collection->get("{$builder_id}_latest");
60
61        if (!$latest) {
47    return new EventStreamResponse(function () use ($builder_id) {
48      $sessionId = $this->session->getId();
49      $collection = $this->sharedTempStoreFactory->get($this::SSE_COLLECTION);
50
51      // Infinite loop because we keep the HTTP transaction open until it is
52      // closed by the client.
53      // @phpstan-ignore-next-line
54      while (TRUE) {
55        // We start by sending an empty event to avoid HTTP timeout. We need
56        // at least the HTTP response headers to be sent back to client without
57        // waiting for a real event to occur.
58        yield new ServerEvent('');
 
58        yield new ServerEvent('');
59        $latest = $collection->get("{$builder_id}_latest");
60
61        if (!$latest) {
 
62          \sleep($this::REFRESH_WINDOW);
63
64          continue;
 
58        yield new ServerEvent('');
59        $latest = $collection->get("{$builder_id}_latest");
60
61        if (!$latest) {
 
72        if ($latest['sessionId'] === $sessionId) {
 
80        if ($latest['timestamp'] < $this->time->getCurrentTime() - $this::STALE) {
 
88        $builder = $this->entityTypeManager()->getStorage('display_builder_instance')
89          ->loadUnchanged($builder_id);
90
91        if (!$builder instanceof InstanceInterface) {
 
96        $this->builder = $builder;
97
98        // Recompute islands regarding the current user.
99        // Use ON_HISTORY_CHANGE because it is the event where most islands are
100        // updated.
101        $event = $this->createEventWithEnabledIsland(
102          DisplayBuilderEvents::ON_HISTORY_CHANGE,
103          $builder->getCurrentState(),
104          NULL,
105          NULL,
106        );
107
108        foreach ($event->getResult() as $island_id => $result) {
 
108        foreach ($event->getResult() as $island_id => $result) {
 
108        foreach ($event->getResult() as $island_id => $result) {
109          // Do nothing if the island is not updated.
110          if (empty($result)) {
 
115          if (!isset($result['content'])) {
 
108        foreach ($event->getResult() as $island_id => $result) {
109          // Do nothing if the island is not updated.
110          if (empty($result)) {
111            continue;
112          }
113
114          // Do nothing if the island is empty.
115          if (!isset($result['content'])) {
116            continue;
117          }
118
119          $sse_target = "island-{$builder->id()}-{$island_id}";
 
108        foreach ($event->getResult() as $island_id => $result) {
 
54      while (TRUE) {
55        // We start by sending an empty event to avoid HTTP timeout. We need
56        // at least the HTTP response headers to be sent back to client without
57        // waiting for a real event to occur.
58        yield new ServerEvent('');
59        $latest = $collection->get("{$builder_id}_latest");
60
61        if (!$latest) {
62          \sleep($this::REFRESH_WINDOW);
63
64          continue;
65        }
66
67        // The current session is the last one editing the builder. Do nothing.
68        // Use session ID to handle:
69        // - different users
70        // - the same user in different browsers
71        // Not handled: multiple tabs with the same user in the same browser.
72        if ($latest['sessionId'] === $sessionId) {
73          \sleep($this::REFRESH_WINDOW);
74
75          continue;
76        }
77
78        // If the last edit is older than STALE, do nothing
79        // as we consider that the builder has already been refreshed.
80        if ($latest['timestamp'] < $this->time->getCurrentTime() - $this::STALE) {
81          \sleep($this::REFRESH_WINDOW);
82
83          continue;
84        }
85
86        // Reload the instance to ensure we get the builder's latest
87        // state.
88        $builder = $this->entityTypeManager()->getStorage('display_builder_instance')
89          ->loadUnchanged($builder_id);
90
91        if (!$builder instanceof InstanceInterface) {
92          \sleep($this::REFRESH_WINDOW);
93
94          continue;
95        }
96        $this->builder = $builder;
97
98        // Recompute islands regarding the current user.
99        // Use ON_HISTORY_CHANGE because it is the event where most islands are
100        // updated.
101        $event = $this->createEventWithEnabledIsland(
102          DisplayBuilderEvents::ON_HISTORY_CHANGE,
103          $builder->getCurrentState(),
104          NULL,
105          NULL,
106        );
107
108        foreach ($event->getResult() as $island_id => $result) {
 
58        yield new ServerEvent('');
59        $latest = $collection->get("{$builder_id}_latest");
60
61        if (!$latest) {
47    return new EventStreamResponse(function () use ($builder_id) {
48      $sessionId = $this->session->getId();
49      $collection = $this->sharedTempStoreFactory->get($this::SSE_COLLECTION);
50
51      // Infinite loop because we keep the HTTP transaction open until it is
52      // closed by the client.
53      // @phpstan-ignore-next-line
54      while (TRUE) {
55        // We start by sending an empty event to avoid HTTP timeout. We need
56        // at least the HTTP response headers to be sent back to client without
57        // waiting for a real event to occur.
58        yield new ServerEvent('');
 
58        yield new ServerEvent('');
59        $latest = $collection->get("{$builder_id}_latest");
60
61        if (!$latest) {
 
62          \sleep($this::REFRESH_WINDOW);
63
64          continue;
 
58        yield new ServerEvent('');
59        $latest = $collection->get("{$builder_id}_latest");
60
61        if (!$latest) {
 
72        if ($latest['sessionId'] === $sessionId) {
 
80        if ($latest['timestamp'] < $this->time->getCurrentTime() - $this::STALE) {
 
88        $builder = $this->entityTypeManager()->getStorage('display_builder_instance')
89          ->loadUnchanged($builder_id);
90
91        if (!$builder instanceof InstanceInterface) {
 
96        $this->builder = $builder;
97
98        // Recompute islands regarding the current user.
99        // Use ON_HISTORY_CHANGE because it is the event where most islands are
100        // updated.
101        $event = $this->createEventWithEnabledIsland(
102          DisplayBuilderEvents::ON_HISTORY_CHANGE,
103          $builder->getCurrentState(),
104          NULL,
105          NULL,
106        );
107
108        foreach ($event->getResult() as $island_id => $result) {
 
108        foreach ($event->getResult() as $island_id => $result) {
 
54      while (TRUE) {
55        // We start by sending an empty event to avoid HTTP timeout. We need
56        // at least the HTTP response headers to be sent back to client without
57        // waiting for a real event to occur.
58        yield new ServerEvent('');
59        $latest = $collection->get("{$builder_id}_latest");
60
61        if (!$latest) {
62          \sleep($this::REFRESH_WINDOW);
63
64          continue;
65        }
66
67        // The current session is the last one editing the builder. Do nothing.
68        // Use session ID to handle:
69        // - different users
70        // - the same user in different browsers
71        // Not handled: multiple tabs with the same user in the same browser.
72        if ($latest['sessionId'] === $sessionId) {
73          \sleep($this::REFRESH_WINDOW);
74
75          continue;
76        }
77
78        // If the last edit is older than STALE, do nothing
79        // as we consider that the builder has already been refreshed.
80        if ($latest['timestamp'] < $this->time->getCurrentTime() - $this::STALE) {
81          \sleep($this::REFRESH_WINDOW);
82
83          continue;
84        }
85
86        // Reload the instance to ensure we get the builder's latest
87        // state.
88        $builder = $this->entityTypeManager()->getStorage('display_builder_instance')
89          ->loadUnchanged($builder_id);
90
91        if (!$builder instanceof InstanceInterface) {
92          \sleep($this::REFRESH_WINDOW);
93
94          continue;
95        }
96        $this->builder = $builder;
97
98        // Recompute islands regarding the current user.
99        // Use ON_HISTORY_CHANGE because it is the event where most islands are
100        // updated.
101        $event = $this->createEventWithEnabledIsland(
102          DisplayBuilderEvents::ON_HISTORY_CHANGE,
103          $builder->getCurrentState(),
104          NULL,
105          NULL,
106        );
107
108        foreach ($event->getResult() as $island_id => $result) {
 
58        yield new ServerEvent('');
59        $latest = $collection->get("{$builder_id}_latest");
60
61        if (!$latest) {
47    return new EventStreamResponse(function () use ($builder_id) {
48      $sessionId = $this->session->getId();
49      $collection = $this->sharedTempStoreFactory->get($this::SSE_COLLECTION);
50
51      // Infinite loop because we keep the HTTP transaction open until it is
52      // closed by the client.
53      // @phpstan-ignore-next-line
54      while (TRUE) {
55        // We start by sending an empty event to avoid HTTP timeout. We need
56        // at least the HTTP response headers to be sent back to client without
57        // waiting for a real event to occur.
58        yield new ServerEvent('');
 
58        yield new ServerEvent('');
59        $latest = $collection->get("{$builder_id}_latest");
60
61        if (!$latest) {
 
62          \sleep($this::REFRESH_WINDOW);
63
64          continue;
 
58        yield new ServerEvent('');
59        $latest = $collection->get("{$builder_id}_latest");
60
61        if (!$latest) {
 
72        if ($latest['sessionId'] === $sessionId) {
 
80        if ($latest['timestamp'] < $this->time->getCurrentTime() - $this::STALE) {
 
88        $builder = $this->entityTypeManager()->getStorage('display_builder_instance')
89          ->loadUnchanged($builder_id);
90
91        if (!$builder instanceof InstanceInterface) {
 
96        $this->builder = $builder;
97
98        // Recompute islands regarding the current user.
99        // Use ON_HISTORY_CHANGE because it is the event where most islands are
100        // updated.
101        $event = $this->createEventWithEnabledIsland(
102          DisplayBuilderEvents::ON_HISTORY_CHANGE,
103          $builder->getCurrentState(),
104          NULL,
105          NULL,
106        );
107
108        foreach ($event->getResult() as $island_id => $result) {
 
54      while (TRUE) {
55        // We start by sending an empty event to avoid HTTP timeout. We need
56        // at least the HTTP response headers to be sent back to client without
57        // waiting for a real event to occur.
58        yield new ServerEvent('');
59        $latest = $collection->get("{$builder_id}_latest");
60
61        if (!$latest) {
62          \sleep($this::REFRESH_WINDOW);
63
64          continue;
65        }
66
67        // The current session is the last one editing the builder. Do nothing.
68        // Use session ID to handle:
69        // - different users
70        // - the same user in different browsers
71        // Not handled: multiple tabs with the same user in the same browser.
72        if ($latest['sessionId'] === $sessionId) {
73          \sleep($this::REFRESH_WINDOW);
74
75          continue;
76        }
77
78        // If the last edit is older than STALE, do nothing
79        // as we consider that the builder has already been refreshed.
80        if ($latest['timestamp'] < $this->time->getCurrentTime() - $this::STALE) {
81          \sleep($this::REFRESH_WINDOW);
82
83          continue;
84        }
85
86        // Reload the instance to ensure we get the builder's latest
87        // state.
88        $builder = $this->entityTypeManager()->getStorage('display_builder_instance')
89          ->loadUnchanged($builder_id);
90
91        if (!$builder instanceof InstanceInterface) {
92          \sleep($this::REFRESH_WINDOW);
93
94          continue;
95        }
96        $this->builder = $builder;
97
98        // Recompute islands regarding the current user.
99        // Use ON_HISTORY_CHANGE because it is the event where most islands are
100        // updated.
101        $event = $this->createEventWithEnabledIsland(
102          DisplayBuilderEvents::ON_HISTORY_CHANGE,
103          $builder->getCurrentState(),
104          NULL,
105          NULL,
106        );
107
108        foreach ($event->getResult() as $island_id => $result) {
 
58        yield new ServerEvent('');
59        $latest = $collection->get("{$builder_id}_latest");
60
61        if (!$latest) {
47    return new EventStreamResponse(function () use ($builder_id) {
48      $sessionId = $this->session->getId();
49      $collection = $this->sharedTempStoreFactory->get($this::SSE_COLLECTION);
50
51      // Infinite loop because we keep the HTTP transaction open until it is
52      // closed by the client.
53      // @phpstan-ignore-next-line
54      while (TRUE) {
55        // We start by sending an empty event to avoid HTTP timeout. We need
56        // at least the HTTP response headers to be sent back to client without
57        // waiting for a real event to occur.
58        yield new ServerEvent('');
 
58        yield new ServerEvent('');
59        $latest = $collection->get("{$builder_id}_latest");
60
61        if (!$latest) {
 
72        if ($latest['sessionId'] === $sessionId) {
 
73          \sleep($this::REFRESH_WINDOW);
74
75          continue;
 
58        yield new ServerEvent('');
59        $latest = $collection->get("{$builder_id}_latest");
60
61        if (!$latest) {
 
62          \sleep($this::REFRESH_WINDOW);
63
64          continue;
 
58        yield new ServerEvent('');
59        $latest = $collection->get("{$builder_id}_latest");
60
61        if (!$latest) {
47    return new EventStreamResponse(function () use ($builder_id) {
48      $sessionId = $this->session->getId();
49      $collection = $this->sharedTempStoreFactory->get($this::SSE_COLLECTION);
50
51      // Infinite loop because we keep the HTTP transaction open until it is
52      // closed by the client.
53      // @phpstan-ignore-next-line
54      while (TRUE) {
55        // We start by sending an empty event to avoid HTTP timeout. We need
56        // at least the HTTP response headers to be sent back to client without
57        // waiting for a real event to occur.
58        yield new ServerEvent('');
 
58        yield new ServerEvent('');
59        $latest = $collection->get("{$builder_id}_latest");
60
61        if (!$latest) {
 
72        if ($latest['sessionId'] === $sessionId) {
 
80        if ($latest['timestamp'] < $this->time->getCurrentTime() - $this::STALE) {
 
81          \sleep($this::REFRESH_WINDOW);
82
83          continue;
 
58        yield new ServerEvent('');
59        $latest = $collection->get("{$builder_id}_latest");
60
61        if (!$latest) {
 
62          \sleep($this::REFRESH_WINDOW);
63
64          continue;
 
58        yield new ServerEvent('');
59        $latest = $collection->get("{$builder_id}_latest");
60
61        if (!$latest) {
47    return new EventStreamResponse(function () use ($builder_id) {
48      $sessionId = $this->session->getId();
49      $collection = $this->sharedTempStoreFactory->get($this::SSE_COLLECTION);
50
51      // Infinite loop because we keep the HTTP transaction open until it is
52      // closed by the client.
53      // @phpstan-ignore-next-line
54      while (TRUE) {
55        // We start by sending an empty event to avoid HTTP timeout. We need
56        // at least the HTTP response headers to be sent back to client without
57        // waiting for a real event to occur.
58        yield new ServerEvent('');
 
58        yield new ServerEvent('');
59        $latest = $collection->get("{$builder_id}_latest");
60
61        if (!$latest) {
 
72        if ($latest['sessionId'] === $sessionId) {
 
80        if ($latest['timestamp'] < $this->time->getCurrentTime() - $this::STALE) {
 
88        $builder = $this->entityTypeManager()->getStorage('display_builder_instance')
89          ->loadUnchanged($builder_id);
90
91        if (!$builder instanceof InstanceInterface) {
 
92          \sleep($this::REFRESH_WINDOW);
93
94          continue;
 
58        yield new ServerEvent('');
59        $latest = $collection->get("{$builder_id}_latest");
60
61        if (!$latest) {
 
62          \sleep($this::REFRESH_WINDOW);
63
64          continue;
 
58        yield new ServerEvent('');
59        $latest = $collection->get("{$builder_id}_latest");
60
61        if (!$latest) {
47    return new EventStreamResponse(function () use ($builder_id) {
48      $sessionId = $this->session->getId();
49      $collection = $this->sharedTempStoreFactory->get($this::SSE_COLLECTION);
50
51      // Infinite loop because we keep the HTTP transaction open until it is
52      // closed by the client.
53      // @phpstan-ignore-next-line
54      while (TRUE) {
55        // We start by sending an empty event to avoid HTTP timeout. We need
56        // at least the HTTP response headers to be sent back to client without
57        // waiting for a real event to occur.
58        yield new ServerEvent('');
 
58        yield new ServerEvent('');
59        $latest = $collection->get("{$builder_id}_latest");
60
61        if (!$latest) {
 
72        if ($latest['sessionId'] === $sessionId) {
 
80        if ($latest['timestamp'] < $this->time->getCurrentTime() - $this::STALE) {
 
88        $builder = $this->entityTypeManager()->getStorage('display_builder_instance')
89          ->loadUnchanged($builder_id);
90
91        if (!$builder instanceof InstanceInterface) {
 
96        $this->builder = $builder;
97
98        // Recompute islands regarding the current user.
99        // Use ON_HISTORY_CHANGE because it is the event where most islands are
100        // updated.
101        $event = $this->createEventWithEnabledIsland(
102          DisplayBuilderEvents::ON_HISTORY_CHANGE,
103          $builder->getCurrentState(),
104          NULL,
105          NULL,
106        );
107
108        foreach ($event->getResult() as $island_id => $result) {
 
108        foreach ($event->getResult() as $island_id => $result) {
 
108        foreach ($event->getResult() as $island_id => $result) {
109          // Do nothing if the island is not updated.
110          if (empty($result)) {
 
111            continue;
 
108        foreach ($event->getResult() as $island_id => $result) {
 
54      while (TRUE) {
55        // We start by sending an empty event to avoid HTTP timeout. We need
56        // at least the HTTP response headers to be sent back to client without
57        // waiting for a real event to occur.
58        yield new ServerEvent('');
59        $latest = $collection->get("{$builder_id}_latest");
60
61        if (!$latest) {
62          \sleep($this::REFRESH_WINDOW);
63
64          continue;
65        }
66
67        // The current session is the last one editing the builder. Do nothing.
68        // Use session ID to handle:
69        // - different users
70        // - the same user in different browsers
71        // Not handled: multiple tabs with the same user in the same browser.
72        if ($latest['sessionId'] === $sessionId) {
73          \sleep($this::REFRESH_WINDOW);
74
75          continue;
76        }
77
78        // If the last edit is older than STALE, do nothing
79        // as we consider that the builder has already been refreshed.
80        if ($latest['timestamp'] < $this->time->getCurrentTime() - $this::STALE) {
81          \sleep($this::REFRESH_WINDOW);
82
83          continue;
84        }
85
86        // Reload the instance to ensure we get the builder's latest
87        // state.
88        $builder = $this->entityTypeManager()->getStorage('display_builder_instance')
89          ->loadUnchanged($builder_id);
90
91        if (!$builder instanceof InstanceInterface) {
92          \sleep($this::REFRESH_WINDOW);
93
94          continue;
95        }
96        $this->builder = $builder;
97
98        // Recompute islands regarding the current user.
99        // Use ON_HISTORY_CHANGE because it is the event where most islands are
100        // updated.
101        $event = $this->createEventWithEnabledIsland(
102          DisplayBuilderEvents::ON_HISTORY_CHANGE,
103          $builder->getCurrentState(),
104          NULL,
105          NULL,
106        );
107
108        foreach ($event->getResult() as $island_id => $result) {
 
58        yield new ServerEvent('');
59        $latest = $collection->get("{$builder_id}_latest");
60
61        if (!$latest) {
 
62          \sleep($this::REFRESH_WINDOW);
63
64          continue;
 
58        yield new ServerEvent('');
59        $latest = $collection->get("{$builder_id}_latest");
60
61        if (!$latest) {
47    return new EventStreamResponse(function () use ($builder_id) {
48      $sessionId = $this->session->getId();
49      $collection = $this->sharedTempStoreFactory->get($this::SSE_COLLECTION);
50
51      // Infinite loop because we keep the HTTP transaction open until it is
52      // closed by the client.
53      // @phpstan-ignore-next-line
54      while (TRUE) {
55        // We start by sending an empty event to avoid HTTP timeout. We need
56        // at least the HTTP response headers to be sent back to client without
57        // waiting for a real event to occur.
58        yield new ServerEvent('');
 
58        yield new ServerEvent('');
59        $latest = $collection->get("{$builder_id}_latest");
60
61        if (!$latest) {
 
72        if ($latest['sessionId'] === $sessionId) {
 
80        if ($latest['timestamp'] < $this->time->getCurrentTime() - $this::STALE) {
 
88        $builder = $this->entityTypeManager()->getStorage('display_builder_instance')
89          ->loadUnchanged($builder_id);
90
91        if (!$builder instanceof InstanceInterface) {
 
96        $this->builder = $builder;
97
98        // Recompute islands regarding the current user.
99        // Use ON_HISTORY_CHANGE because it is the event where most islands are
100        // updated.
101        $event = $this->createEventWithEnabledIsland(
102          DisplayBuilderEvents::ON_HISTORY_CHANGE,
103          $builder->getCurrentState(),
104          NULL,
105          NULL,
106        );
107
108        foreach ($event->getResult() as $island_id => $result) {
 
108        foreach ($event->getResult() as $island_id => $result) {
 
108        foreach ($event->getResult() as $island_id => $result) {
109          // Do nothing if the island is not updated.
110          if (empty($result)) {
 
115          if (!isset($result['content'])) {
 
116            continue;
 
108        foreach ($event->getResult() as $island_id => $result) {
 
54      while (TRUE) {
55        // We start by sending an empty event to avoid HTTP timeout. We need
56        // at least the HTTP response headers to be sent back to client without
57        // waiting for a real event to occur.
58        yield new ServerEvent('');
59        $latest = $collection->get("{$builder_id}_latest");
60
61        if (!$latest) {
62          \sleep($this::REFRESH_WINDOW);
63
64          continue;
65        }
66
67        // The current session is the last one editing the builder. Do nothing.
68        // Use session ID to handle:
69        // - different users
70        // - the same user in different browsers
71        // Not handled: multiple tabs with the same user in the same browser.
72        if ($latest['sessionId'] === $sessionId) {
73          \sleep($this::REFRESH_WINDOW);
74
75          continue;
76        }
77
78        // If the last edit is older than STALE, do nothing
79        // as we consider that the builder has already been refreshed.
80        if ($latest['timestamp'] < $this->time->getCurrentTime() - $this::STALE) {
81          \sleep($this::REFRESH_WINDOW);
82
83          continue;
84        }
85
86        // Reload the instance to ensure we get the builder's latest
87        // state.
88        $builder = $this->entityTypeManager()->getStorage('display_builder_instance')
89          ->loadUnchanged($builder_id);
90
91        if (!$builder instanceof InstanceInterface) {
92          \sleep($this::REFRESH_WINDOW);
93
94          continue;
95        }
96        $this->builder = $builder;
97
98        // Recompute islands regarding the current user.
99        // Use ON_HISTORY_CHANGE because it is the event where most islands are
100        // updated.
101        $event = $this->createEventWithEnabledIsland(
102          DisplayBuilderEvents::ON_HISTORY_CHANGE,
103          $builder->getCurrentState(),
104          NULL,
105          NULL,
106        );
107
108        foreach ($event->getResult() as $island_id => $result) {
 
58        yield new ServerEvent('');
59        $latest = $collection->get("{$builder_id}_latest");
60
61        if (!$latest) {
 
62          \sleep($this::REFRESH_WINDOW);
63
64          continue;
 
58        yield new ServerEvent('');
59        $latest = $collection->get("{$builder_id}_latest");
60
61        if (!$latest) {
47    return new EventStreamResponse(function () use ($builder_id) {
48      $sessionId = $this->session->getId();
49      $collection = $this->sharedTempStoreFactory->get($this::SSE_COLLECTION);
50
51      // Infinite loop because we keep the HTTP transaction open until it is
52      // closed by the client.
53      // @phpstan-ignore-next-line
54      while (TRUE) {
55        // We start by sending an empty event to avoid HTTP timeout. We need
56        // at least the HTTP response headers to be sent back to client without
57        // waiting for a real event to occur.
58        yield new ServerEvent('');
 
58        yield new ServerEvent('');
59        $latest = $collection->get("{$builder_id}_latest");
60
61        if (!$latest) {
 
72        if ($latest['sessionId'] === $sessionId) {
 
80        if ($latest['timestamp'] < $this->time->getCurrentTime() - $this::STALE) {
 
88        $builder = $this->entityTypeManager()->getStorage('display_builder_instance')
89          ->loadUnchanged($builder_id);
90
91        if (!$builder instanceof InstanceInterface) {
 
96        $this->builder = $builder;
97
98        // Recompute islands regarding the current user.
99        // Use ON_HISTORY_CHANGE because it is the event where most islands are
100        // updated.
101        $event = $this->createEventWithEnabledIsland(
102          DisplayBuilderEvents::ON_HISTORY_CHANGE,
103          $builder->getCurrentState(),
104          NULL,
105          NULL,
106        );
107
108        foreach ($event->getResult() as $island_id => $result) {
 
108        foreach ($event->getResult() as $island_id => $result) {
 
108        foreach ($event->getResult() as $island_id => $result) {
109          // Do nothing if the island is not updated.
110          if (empty($result)) {
 
115          if (!isset($result['content'])) {
 
108        foreach ($event->getResult() as $island_id => $result) {
109          // Do nothing if the island is not updated.
110          if (empty($result)) {
111            continue;
112          }
113
114          // Do nothing if the island is empty.
115          if (!isset($result['content'])) {
116            continue;
117          }
118
119          $sse_target = "island-{$builder->id()}-{$island_id}";
 
108        foreach ($event->getResult() as $island_id => $result) {
 
54      while (TRUE) {
55        // We start by sending an empty event to avoid HTTP timeout. We need
56        // at least the HTTP response headers to be sent back to client without
57        // waiting for a real event to occur.
58        yield new ServerEvent('');
59        $latest = $collection->get("{$builder_id}_latest");
60
61        if (!$latest) {
62          \sleep($this::REFRESH_WINDOW);
63
64          continue;
65        }
66
67        // The current session is the last one editing the builder. Do nothing.
68        // Use session ID to handle:
69        // - different users
70        // - the same user in different browsers
71        // Not handled: multiple tabs with the same user in the same browser.
72        if ($latest['sessionId'] === $sessionId) {
73          \sleep($this::REFRESH_WINDOW);
74
75          continue;
76        }
77
78        // If the last edit is older than STALE, do nothing
79        // as we consider that the builder has already been refreshed.
80        if ($latest['timestamp'] < $this->time->getCurrentTime() - $this::STALE) {
81          \sleep($this::REFRESH_WINDOW);
82
83          continue;
84        }
85
86        // Reload the instance to ensure we get the builder's latest
87        // state.
88        $builder = $this->entityTypeManager()->getStorage('display_builder_instance')
89          ->loadUnchanged($builder_id);
90
91        if (!$builder instanceof InstanceInterface) {
92          \sleep($this::REFRESH_WINDOW);
93
94          continue;
95        }
96        $this->builder = $builder;
97
98        // Recompute islands regarding the current user.
99        // Use ON_HISTORY_CHANGE because it is the event where most islands are
100        // updated.
101        $event = $this->createEventWithEnabledIsland(
102          DisplayBuilderEvents::ON_HISTORY_CHANGE,
103          $builder->getCurrentState(),
104          NULL,
105          NULL,
106        );
107
108        foreach ($event->getResult() as $island_id => $result) {
 
58        yield new ServerEvent('');
59        $latest = $collection->get("{$builder_id}_latest");
60
61        if (!$latest) {
 
62          \sleep($this::REFRESH_WINDOW);
63
64          continue;
 
58        yield new ServerEvent('');
59        $latest = $collection->get("{$builder_id}_latest");
60
61        if (!$latest) {
47    return new EventStreamResponse(function () use ($builder_id) {
48      $sessionId = $this->session->getId();
49      $collection = $this->sharedTempStoreFactory->get($this::SSE_COLLECTION);
50
51      // Infinite loop because we keep the HTTP transaction open until it is
52      // closed by the client.
53      // @phpstan-ignore-next-line
54      while (TRUE) {
55        // We start by sending an empty event to avoid HTTP timeout. We need
56        // at least the HTTP response headers to be sent back to client without
57        // waiting for a real event to occur.
58        yield new ServerEvent('');
 
58        yield new ServerEvent('');
59        $latest = $collection->get("{$builder_id}_latest");
60
61        if (!$latest) {
 
72        if ($latest['sessionId'] === $sessionId) {
 
80        if ($latest['timestamp'] < $this->time->getCurrentTime() - $this::STALE) {
 
88        $builder = $this->entityTypeManager()->getStorage('display_builder_instance')
89          ->loadUnchanged($builder_id);
90
91        if (!$builder instanceof InstanceInterface) {
 
96        $this->builder = $builder;
97
98        // Recompute islands regarding the current user.
99        // Use ON_HISTORY_CHANGE because it is the event where most islands are
100        // updated.
101        $event = $this->createEventWithEnabledIsland(
102          DisplayBuilderEvents::ON_HISTORY_CHANGE,
103          $builder->getCurrentState(),
104          NULL,
105          NULL,
106        );
107
108        foreach ($event->getResult() as $island_id => $result) {
 
108        foreach ($event->getResult() as $island_id => $result) {
 
54      while (TRUE) {
55        // We start by sending an empty event to avoid HTTP timeout. We need
56        // at least the HTTP response headers to be sent back to client without
57        // waiting for a real event to occur.
58        yield new ServerEvent('');
59        $latest = $collection->get("{$builder_id}_latest");
60
61        if (!$latest) {
62          \sleep($this::REFRESH_WINDOW);
63
64          continue;
65        }
66
67        // The current session is the last one editing the builder. Do nothing.
68        // Use session ID to handle:
69        // - different users
70        // - the same user in different browsers
71        // Not handled: multiple tabs with the same user in the same browser.
72        if ($latest['sessionId'] === $sessionId) {
73          \sleep($this::REFRESH_WINDOW);
74
75          continue;
76        }
77
78        // If the last edit is older than STALE, do nothing
79        // as we consider that the builder has already been refreshed.
80        if ($latest['timestamp'] < $this->time->getCurrentTime() - $this::STALE) {
81          \sleep($this::REFRESH_WINDOW);
82
83          continue;
84        }
85
86        // Reload the instance to ensure we get the builder's latest
87        // state.
88        $builder = $this->entityTypeManager()->getStorage('display_builder_instance')
89          ->loadUnchanged($builder_id);
90
91        if (!$builder instanceof InstanceInterface) {
92          \sleep($this::REFRESH_WINDOW);
93
94          continue;
95        }
96        $this->builder = $builder;
97
98        // Recompute islands regarding the current user.
99        // Use ON_HISTORY_CHANGE because it is the event where most islands are
100        // updated.
101        $event = $this->createEventWithEnabledIsland(
102          DisplayBuilderEvents::ON_HISTORY_CHANGE,
103          $builder->getCurrentState(),
104          NULL,
105          NULL,
106        );
107
108        foreach ($event->getResult() as $island_id => $result) {
 
58        yield new ServerEvent('');
59        $latest = $collection->get("{$builder_id}_latest");
60
61        if (!$latest) {
 
62          \sleep($this::REFRESH_WINDOW);
63
64          continue;
 
58        yield new ServerEvent('');
59        $latest = $collection->get("{$builder_id}_latest");
60
61        if (!$latest) {
47    return new EventStreamResponse(function () use ($builder_id) {
48      $sessionId = $this->session->getId();
49      $collection = $this->sharedTempStoreFactory->get($this::SSE_COLLECTION);
50
51      // Infinite loop because we keep the HTTP transaction open until it is
52      // closed by the client.
53      // @phpstan-ignore-next-line
54      while (TRUE) {
55        // We start by sending an empty event to avoid HTTP timeout. We need
56        // at least the HTTP response headers to be sent back to client without
57        // waiting for a real event to occur.
58        yield new ServerEvent('');
 
58        yield new ServerEvent('');
59        $latest = $collection->get("{$builder_id}_latest");
60
61        if (!$latest) {
 
72        if ($latest['sessionId'] === $sessionId) {
 
80        if ($latest['timestamp'] < $this->time->getCurrentTime() - $this::STALE) {
 
88        $builder = $this->entityTypeManager()->getStorage('display_builder_instance')
89          ->loadUnchanged($builder_id);
90
91        if (!$builder instanceof InstanceInterface) {
 
96        $this->builder = $builder;
97
98        // Recompute islands regarding the current user.
99        // Use ON_HISTORY_CHANGE because it is the event where most islands are
100        // updated.
101        $event = $this->createEventWithEnabledIsland(
102          DisplayBuilderEvents::ON_HISTORY_CHANGE,
103          $builder->getCurrentState(),
104          NULL,
105          NULL,
106        );
107
108        foreach ($event->getResult() as $island_id => $result) {
 
54      while (TRUE) {
55        // We start by sending an empty event to avoid HTTP timeout. We need
56        // at least the HTTP response headers to be sent back to client without
57        // waiting for a real event to occur.
58        yield new ServerEvent('');
59        $latest = $collection->get("{$builder_id}_latest");
60
61        if (!$latest) {
62          \sleep($this::REFRESH_WINDOW);
63
64          continue;
65        }
66
67        // The current session is the last one editing the builder. Do nothing.
68        // Use session ID to handle:
69        // - different users
70        // - the same user in different browsers
71        // Not handled: multiple tabs with the same user in the same browser.
72        if ($latest['sessionId'] === $sessionId) {
73          \sleep($this::REFRESH_WINDOW);
74
75          continue;
76        }
77
78        // If the last edit is older than STALE, do nothing
79        // as we consider that the builder has already been refreshed.
80        if ($latest['timestamp'] < $this->time->getCurrentTime() - $this::STALE) {
81          \sleep($this::REFRESH_WINDOW);
82
83          continue;
84        }
85
86        // Reload the instance to ensure we get the builder's latest
87        // state.
88        $builder = $this->entityTypeManager()->getStorage('display_builder_instance')
89          ->loadUnchanged($builder_id);
90
91        if (!$builder instanceof InstanceInterface) {
92          \sleep($this::REFRESH_WINDOW);
93
94          continue;
95        }
96        $this->builder = $builder;
97
98        // Recompute islands regarding the current user.
99        // Use ON_HISTORY_CHANGE because it is the event where most islands are
100        // updated.
101        $event = $this->createEventWithEnabledIsland(
102          DisplayBuilderEvents::ON_HISTORY_CHANGE,
103          $builder->getCurrentState(),
104          NULL,
105          NULL,
106        );
107
108        foreach ($event->getResult() as $island_id => $result) {
 
58        yield new ServerEvent('');
59        $latest = $collection->get("{$builder_id}_latest");
60
61        if (!$latest) {
 
62          \sleep($this::REFRESH_WINDOW);
63
64          continue;
 
58        yield new ServerEvent('');
59        $latest = $collection->get("{$builder_id}_latest");
60
61        if (!$latest) {