<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://jpomykala.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://jpomykala.com/" rel="alternate" type="text/html" /><updated>2026-05-29T08:33:22+00:00</updated><id>https://jpomykala.com/feed.xml</id><title type="html">Jakub Pomykała</title><subtitle>My personal website, blog and place in the internet.</subtitle><author><name>GitHub User</name><email>your-email@domain.com</email></author><entry><title type="html">Glance dashboard - pre-cached data</title><link href="https://jpomykala.com/2026/01/02/glance-lazy-load-and-cache/" rel="alternate" type="text/html" title="Glance dashboard - pre-cached data" /><published>2026-01-02T00:00:00+00:00</published><updated>2026-01-02T00:00:00+00:00</updated><id>https://jpomykala.com/2026/01/02/glance-lazy-load-and-cache</id><content type="html" xml:base="https://jpomykala.com/2026/01/02/glance-lazy-load-and-cache/"><![CDATA[<p><a href="https://github.com/glanceapp/glance">Glance</a> is a popular open-source dashboard for monitoring self-hosted services. It provides a clean and user-friendly interface to visualize various metrics and data from different sources.</p>

<p><img src="/assets/2026-01-02/glance-dashboard.png" alt="My Glance dashboard" /></p>

<p>I’m using Glance for over a year now, and never had any issue with it. However, as the number of widgets and data sources increased, I noticed that the dashboard started to load slowly, especially on initial load.
I have to resign with many widgets just to keep the load time fast. I was also trying to split the dashboard into multiple pages, but it was not an ideal solution, especially when I wanted to see all the data at once.
One of the widgets that I wanted to have was <a href="https://github.com/glanceapp/community-widgets/blob/main/widgets/synology-disk-station/README.md">Synology Disk Station Widget</a>, but it was increasing the load time significantly, as each authorization request was taking 2-3 seconds.</p>

<p>Recently, <a href="https://www.reddit.com/r/selfhosted/comments/1q1epik/comment/nx5cddi/">I discovered that Glance can load data from JSON files</a> that are hosted via <code class="language-plaintext highlighter-rouge">glance-assets</code> directory. Knowing that, I thought that I could use <code class="language-plaintext highlighter-rouge">crontab</code> to periodically fetch data from my services and store them as JSON files in the <code class="language-plaintext highlighter-rouge">glance-assets</code> directory.</p>

<h3 id="docker-compose-setup">Docker compose setup</h3>

<p>Here is my docker-compose snippet for Glance with the necessary volume mappings:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">glance</span><span class="pi">:</span>
  <span class="na">image</span><span class="pi">:</span> <span class="s">glanceapp/glance:v0.8.4</span>
  <span class="na">volumes</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">./glance:/app/config</span>
    <span class="pi">-</span> <span class="s">./glance-assets:/app/assets</span> <span class="c1"># &lt;&lt; directory for JSON files</span>
    <span class="pi">-</span> <span class="s">/etc/timezone:/etc/timezone:ro</span>
    <span class="pi">-</span> <span class="s">/etc/localtime:/etc/localtime:ro</span>
    <span class="pi">-</span> <span class="s">/var/run/docker.sock:/var/run/docker.sock</span>
</code></pre></div></div>

<h2 id="monitoring-synology-nas">Monitoring Synology NAS</h2>

<p>To monitor my Synology NAS, I created a bash script that uses the Synology API to fetch CPU, RAM, and storage utilization data. The script then writes this data to <code class="language-plaintext highlighter-rouge">synology_stats.json</code> in the <code class="language-plaintext highlighter-rouge">glance-assets</code> directory.</p>

<p>The original code that I used comes from the Glance community widgets repository, which you can find <a href="https://github.com/glanceapp/community-widgets/blob/main/widgets/synology-disk-station/README.md">here</a>.</p>

<p>You need to have <code class="language-plaintext highlighter-rouge">jq</code> installed on your system to parse JSON responses.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/bash</span>
<span class="nb">export </span><span class="nv">PATH</span><span class="o">=</span>/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

<span class="nv">NAS_IP</span><span class="o">=</span><span class="s2">"[YOUR_NAS_IP]"</span>
<span class="nv">NAS_PORT</span><span class="o">=</span><span class="s2">"[YOUR_NAS_PORT]"</span> <span class="c"># usually 5000 or 5001 for HTTPS</span>
<span class="nv">USERNAME</span><span class="o">=</span><span class="s2">"[YOUR_NAS_USERNAME]"</span>
<span class="nv">PASSWORD</span><span class="o">=</span><span class="s2">"[YOUR_NAS_PASSWORD]"</span>
<span class="nv">BASE_URL</span><span class="o">=</span><span class="s2">"http://</span><span class="k">${</span><span class="nv">NAS_IP</span><span class="k">}</span><span class="s2">:</span><span class="k">${</span><span class="nv">NAS_PORT</span><span class="k">}</span><span class="s2">/webapi/entry.cgi"</span>

<span class="c"># Authenticate and get SID</span>
<span class="nv">AUTH_RESPONSE</span><span class="o">=</span><span class="si">$(</span>curl <span class="nt">-s</span> <span class="s2">"</span><span class="k">${</span><span class="nv">BASE_URL</span><span class="k">}</span><span class="s2">?api=SYNO.API.Auth&amp;version=6&amp;method=login&amp;account=</span><span class="k">${</span><span class="nv">USERNAME</span><span class="k">}</span><span class="s2">&amp;passwd=</span><span class="k">${</span><span class="nv">PASSWORD</span><span class="k">}</span><span class="s2">&amp;session=FileStation&amp;format=sid"</span><span class="si">)</span>
<span class="nv">SID</span><span class="o">=</span><span class="si">$(</span><span class="nb">echo</span> <span class="s2">"</span><span class="nv">$AUTH_RESPONSE</span><span class="s2">"</span> | jq <span class="nt">-r</span> <span class="s1">'.data.sid'</span><span class="si">)</span>

<span class="k">if</span> <span class="o">[[</span> <span class="nt">-z</span> <span class="s2">"</span><span class="nv">$SID</span><span class="s2">"</span> <span class="o">||</span> <span class="s2">"</span><span class="nv">$SID</span><span class="s2">"</span> <span class="o">==</span> <span class="s2">"null"</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then
  </span><span class="nb">echo</span> <span class="s2">"Failed to authenticate with Synology NAS"</span> <span class="o">&gt;</span>&amp;2
  <span class="nb">exit </span>1
<span class="k">fi</span>

<span class="c"># Fetch storage info</span>
<span class="nv">STORAGE_RESPONSE</span><span class="o">=</span><span class="si">$(</span>curl <span class="nt">-s</span> <span class="s2">"</span><span class="k">${</span><span class="nv">BASE_URL</span><span class="k">}</span><span class="s2">?api=SYNO.Core.System&amp;method=info&amp;type=storage&amp;version=1&amp;_sid=</span><span class="k">${</span><span class="nv">SID</span><span class="k">}</span><span class="s2">"</span><span class="si">)</span>

<span class="c"># Fetch CPU and RAM utilization</span>
<span class="nv">CPU_RESPONSE</span><span class="o">=</span><span class="si">$(</span>curl <span class="nt">-s</span> <span class="s2">"</span><span class="k">${</span><span class="nv">BASE_URL</span><span class="k">}</span><span class="s2">?api=SYNO.Core.System.Utilization&amp;method=get&amp;version=1&amp;_sid=</span><span class="k">${</span><span class="nv">SID</span><span class="k">}</span><span class="s2">"</span><span class="si">)</span>

<span class="c"># Parse storage data</span>
<span class="nv">TOTAL_SIZE</span><span class="o">=</span><span class="si">$(</span><span class="nb">echo</span> <span class="s2">"</span><span class="nv">$STORAGE_RESPONSE</span><span class="s2">"</span> | jq <span class="nt">-r</span> <span class="s1">'.data.vol_info[0].total_size'</span><span class="si">)</span>
<span class="nv">USED_SIZE</span><span class="o">=</span><span class="si">$(</span><span class="nb">echo</span> <span class="s2">"</span><span class="nv">$STORAGE_RESPONSE</span><span class="s2">"</span> | jq <span class="nt">-r</span> <span class="s1">'.data.vol_info[0].used_size'</span><span class="si">)</span>
<span class="nv">STORAGE_PERCENT</span><span class="o">=</span><span class="si">$(</span><span class="nb">awk</span> <span class="s2">"BEGIN {printf </span><span class="se">\"</span><span class="s2">%.0f</span><span class="se">\"</span><span class="s2">, (</span><span class="nv">$USED_SIZE</span><span class="s2"> / </span><span class="nv">$TOTAL_SIZE</span><span class="s2">) * 100}"</span><span class="si">)</span>

<span class="c"># Parse CPU and RAM data</span>
<span class="nv">CPU_PERCENT</span><span class="o">=</span><span class="si">$(</span><span class="nb">echo</span> <span class="s2">"</span><span class="nv">$CPU_RESPONSE</span><span class="s2">"</span> | jq <span class="nt">-r</span> <span class="s1">'.data.cpu.user_load'</span><span class="si">)</span>
<span class="nv">RAM_PERCENT</span><span class="o">=</span><span class="si">$(</span><span class="nb">echo</span> <span class="s2">"</span><span class="nv">$CPU_RESPONSE</span><span class="s2">"</span> | jq <span class="nt">-r</span> <span class="s1">'.data.memory.real_usage'</span><span class="si">)</span>

<span class="c"># Generate timestamp</span>
<span class="nv">TIMESTAMP</span><span class="o">=</span><span class="si">$(</span><span class="nb">date</span> <span class="nt">-u</span> +<span class="s2">"%Y-%m-%dT%H:%M:%SZ"</span><span class="si">)</span>

<span class="c"># Write JSON output</span>
<span class="nb">cat</span> <span class="o">&gt;</span> /home/jpomykala/Compose/glance-assets/synology_stats.json <span class="o">&lt;&lt;</span><span class="no">EOF</span><span class="sh">
{
    "timestamp": "</span><span class="k">${</span><span class="nv">TIMESTAMP</span><span class="k">}</span><span class="sh">",
    "cpu_percent": </span><span class="k">${</span><span class="nv">CPU_PERCENT</span><span class="k">}</span><span class="sh">,
    "ram_percent": </span><span class="k">${</span><span class="nv">RAM_PERCENT</span><span class="k">}</span><span class="sh">,
    "storage_percent": </span><span class="k">${</span><span class="nv">STORAGE_PERCENT</span><span class="k">}</span><span class="sh">
}
</span><span class="no">EOF

</span><span class="nb">echo</span> <span class="s2">"Synology stats updated successfully"</span>
</code></pre></div></div>

<p>Make sure to replace all placeholders with credentials and <strong>adjust the output file path</strong>. 
Save the script as <code class="language-plaintext highlighter-rouge">monitor-synology.sh</code>. Don’t forget to give it execute permissions:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">chmod</span> +x monitor-synology.sh
</code></pre></div></div>

<p>I set up a cron job to run this script every 15 minutes:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>crontab <span class="nt">-e</span>
</code></pre></div></div>

<p>and add the following line:</p>

<pre><code class="language-cron">*/15 * * * * /home/YOUR_PATH/scripts/monitor-synology.sh
</code></pre>

<p>After few minutes, you should see a file created in <code class="language-plaintext highlighter-rouge">glance-assets</code> with:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
    </span><span class="nl">"timestamp"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2026-01-02T10:30:04Z"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"cpu_percent"</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w">
    </span><span class="nl">"ram_percent"</span><span class="p">:</span><span class="w"> </span><span class="mi">44</span><span class="p">,</span><span class="w">
    </span><span class="nl">"storage_percent"</span><span class="p">:</span><span class="w"> </span><span class="mi">51</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<h2 id="glance-widget-configuration">Glance widget configuration</h2>

<p>Here is the Glance widget configuration to display the Synology NAS stats:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">custom-api</span>
  <span class="na">cache</span><span class="pi">:</span> <span class="s">1m</span>
  <span class="na">title</span><span class="pi">:</span> <span class="s">Synology</span>
  <span class="na">url</span><span class="pi">:</span> <span class="s">http://glance.home/assets/synology_stats.json</span>
  <span class="na">template</span><span class="pi">:</span> <span class="pi">|</span>
    <span class="s">&lt;div class="flex justify-between text-center"&gt;</span>
      <span class="s">&lt;div class="flex-1"&gt;</span>
        <span class="s">&lt;div class="size-h4 color-highlight"&gt;%&lt;/div&gt;</span>
        <span class="s">&lt;div class="size-h6"&gt;RAM&lt;/div&gt;</span>
      <span class="s">&lt;/div&gt;</span>
      <span class="s">&lt;div class="flex-1"&gt;</span>
        <span class="s">&lt;div class="size-h4 color-highlight"&gt;%&lt;/div&gt;</span>
        <span class="s">&lt;div class="size-h6"&gt;STORAGE&lt;/div&gt;</span>
      <span class="s">&lt;/div&gt;</span>
      <span class="s">&lt;div class="flex-1"&gt;</span>
        <span class="s">&lt;div class="size-h4 color-highlight" &gt;&lt;/div&gt;</span>
        <span class="s">&lt;div class="size-h6"&gt;LAST UPDATE&lt;/div&gt;</span>
      <span class="s">&lt;/div&gt;</span>
    <span class="s">&lt;/div&gt;</span>
</code></pre></div></div>

<p>This configuration uses the <a href="https://github.com/glanceapp/glance/blob/main/docs/custom-api.md"><code class="language-plaintext highlighter-rouge">custom-api</code> widget type</a> to fetch data from the JSON file that will be updated by the cron job. In my case, the URL is:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>http://glance.home/assets/synology_stats.json
</code></pre></div></div>

<p>but you should adjust the domain or IP address according to your setup.</p>

<h2 id="result">Result</h2>

<p>After setting everything up, I was able to add the Synology NAS widget to my Glance dashboard without any noticeable delay in loading time. The data is updated every 15 minutes, and the dashboard remains responsive and fast.</p>

<p><img src="/assets/2026-01-02/glance-synology.png" alt="My Glance dashboard" /></p>

<p>In my case, I also created similar scripts for monitoring other things like, systemctl services, and backups status, all using the same approach of pre-caching data in JSON files.</p>

<h2 id="support">Support</h2>

<p>If you think Glance is as cool as I do, please consider supporting the developer on <a href="https://github.com/sponsors/glanceapp">GitHub Sponsors</a>.
I think even a small contribution will make the developer smile. :)</p>]]></content><author><name>GitHub User</name><email>your-email@domain.com</email></author><category term="selfhosted" /><summary type="html"><![CDATA[How to implement lazy loading and caching in Glance dashboard.]]></summary></entry><entry><title type="html">Icon Generator for Alfred</title><link href="https://jpomykala.com/2024/12/21/simple-icon-generator-for-alfred/" rel="alternate" type="text/html" title="Icon Generator for Alfred" /><published>2024-12-21T00:00:00+00:00</published><updated>2024-12-21T00:00:00+00:00</updated><id>https://jpomykala.com/2024/12/21/simple-icon-generator-for-alfred</id><content type="html" xml:base="https://jpomykala.com/2024/12/21/simple-icon-generator-for-alfred/"><![CDATA[<p><img src="/assets/2024-12-21/super-simple-icon-generator-for-alfred.webp" alt="Icon Generator for Alfred" /></p>

<p>Recently, after 7 years of using <a href="https://www.alfredapp.com/powerpack/">Alfred PowerPack</a>
, I decided to create some workflows for my daily tasks.  Unfortunately, I couldn’t find a quick way to get nice-looking icons for them, so I made an app that allows me to upload an SVG icon and generate a nice background for it.</p>

<p>I had to generate ~8 icons, so instead opening Sketch I decided to make an app for it. It took me an hour to make it. 😄</p>

<p>I primarily use <a href="https://fontawesome.com/icons">FontAwesome</a> as my source for icons
, but I believe there are more icon projects out there.  I made it primarily for myself. It took me less than an hour to make it.</p>

<p>Creating workflows is now super easy with ChatGPT, but please beware of that ChatGPT not always knows everything regarding the latests updates, eg. Automation Tasks that are super cool. 😎</p>

<p>Here’s the link to the icon generator: <a href="https://renderform.io/tools/icon-generator/">Icon Generator</a></p>

<p>Feel free to ping me with suggestions! I’m open to adding more advanced options, but for now, I wanted to keep it simple.</p>]]></content><author><name>GitHub User</name><email>your-email@domain.com</email></author><summary type="html"><![CDATA[Generate icons for your Alfred workflows with ease.]]></summary></entry><entry><title type="html">Websockets in Spring Boot made simple</title><link href="https://jpomykala.com/2024/07/15/spring-boot-simple-websockets/" rel="alternate" type="text/html" title="Websockets in Spring Boot made simple" /><published>2024-07-15T00:00:00+00:00</published><updated>2024-07-15T00:00:00+00:00</updated><id>https://jpomykala.com/2024/07/15/spring-boot-simple-websockets</id><content type="html" xml:base="https://jpomykala.com/2024/07/15/spring-boot-simple-websockets/"><![CDATA[<p><a href="https://www.reddit.com/r/SpringBoot/comments/1dzpfa7/comment/lcheh4w/">Recently, I asked Spring Boot developers on Reddit</a>
if anyone uses plain WebSockets in their applications without <a href="https://stomp-js.github.io">STOMP</a>
and/or <a href="https://github.com/sockjs/sockjs-client">SockJS</a>. I was curious about the answers
because in all the resources I found on the internet, people use STOMP and SockJS to implement WebSockets in Spring Boot
apps, but those technologies are not so popular on the frontend side. I started wondering, do I really need them? And
why is there no simple example of using WebSockets in Spring Boot without STOMP and SockJS? I guess the reason is that
Spring Boot is mostly used in enterprise applications where STOMP and SockJS are more suitable or documentation is just
outdated in this matter or configuration of WebSockets without these technologies is that easy that it doesn’t require
any explanation. However, I decided to write this post to show you how to configure WebSockets in Spring Boot without
it.</p>

<blockquote>
  <p>I won’t cover how to send messages from the client to the server, as it didn’t fit into the context of the project
presented in this post.</p>
</blockquote>

<h2 id="prerequisites">Prerequisites</h2>

<p>To follow this tutorial, it’s good to know the basics of Java, Spring Boot, and Spring Security, and have a general
understanding of WebSockets and ReactJS, as we will use ReactJS to communicate with the server using WebSockets.</p>

<h2 id="why-websockets-and-not-server-sent-events">Why WebSockets and not Server-Sent Events</h2>

<p>A few months back, I started working on a workflow automation engine at <a href="https://simplelocalize.io">SimpleLocalize</a>. The
idea was very simple: I
wanted to create a feature where you can create workflows for translations. For example, when you modify a source
translation, it would auto-translate other translations for all other languages.</p>

<p>I knew that building an automation engine handling all the rules and logic would be a challenge, but I also knew that
some automations created by users wouldn’t run fast enough to return results in a few seconds. I needed a way to notify
users browsing the content that the translations were being updated.</p>

<p><img src="/assets/2024-07-15/translations-automation.mp4" alt="Auto-translation workflow in SimpleLocalize" /></p>

<p>After a few hours of research, I decided to
use <a href="https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events">server-side events (SSE)</a> to send live updates
to the client. SSE works
similarly to a regular HTTP request but allows the server to send updates to the client without the client having to
send a request. I didn’t need duplex communication; it was enough to send updates from the server to the client.</p>

<p>Unfortunately, the solution didn’t work well with the reverse proxy we used, which struggled with many long-running
connections. At some point, when specific conditions were met, the frontend received malformed JSON responses,
like <code class="language-plaintext highlighter-rouge">{ message: 'OK' } { mes</code> (yiiikes!). The short-term solution was to set an SSE timeout to 15 seconds and reconnect
after that time, but it wasn’t a good solution.</p>

<p>Shortly after that, I started looking into WebSockets. WebSockets were a buzzword in the web development world a few
years back, and they are still very popular and widely used. They are well-supported by all modern browsers and
infrastructure middlewares. I hoped that WebSockets would solve my problem with live updates, and they did!</p>

<p>Currently, we have over 100 live users simultaneously connected to the server without any hiccups, and we are gradually
increasing
this number by switching from SSE to WebSockets.</p>

<h2 id="why-i-dont-want-to-use-stomp-and-sockjs">Why I don’t want to use STOMP and SockJS</h2>

<p><a href="https://spring.io/guides/gs/messaging-stomp-websocket">Most of the tutorials (including the Spring Framework docs)</a> on
the internet use STOMP and SockJS to implement WebSockets in Spring Boot applications. It’s worth explaining what STOMP
and SockJS are:</p>

<ul>
  <li>
    <p><strong>STOMP</strong> provides a higher-level API to work with WebSockets, such as subscribing to topics, sending messages, etc.
However, it adds complexity to the application, which I don’t need, at least for now.</p>
  </li>
  <li>
    <p><strong>SockJS</strong> provides fallback support for browsers that don’t support WebSockets. Since all modern browsers support
WebSockets, fallback support is generally unnecessary. While SockJS can be useful if WebSockets are blocked in a
user’s network, such cases are rare these days, and I haven’t encountered any so far.</p>
  </li>
</ul>

<p>I’ve decided to use just the Spring Boot WebSocket dependency and the native WebSocket API provided by the browser to
avoid dependence on any external libraries and Spring Boot magic.</p>

<h2 id="authentication-in-websockets">Authentication in WebSockets</h2>

<p>In a real-world application, you would need to authenticate and authorize users before they can connect to the
WebSocket. You can use Spring Security to handle this process. Nearly seven years ago, I wrote a post on how to use
Spring Security with WebSockets; you can check it out <a href="/2017/12/19/spring-react-websockets-with-auth">here</a>. However,
after gaining more experience, I’d prefer to use less Spring magic and keep things simple.</p>

<p>I’ve decided to use a straightforward ticket-based authentication mechanism. The server generates a ticket for the user
when they connect to the WebSocket, and the client uses this ticket to authenticate with the server.</p>

<h2 id="how-to-configure-websockets-in-spring-boot">How to configure WebSockets in Spring Boot</h2>

<p>Let’s dive into the code and see how we can configure WebSockets in Spring Boot 3.
I’ve started by creating a service class called <code class="language-plaintext highlighter-rouge">WssConnectorService</code> that extends <code class="language-plaintext highlighter-rouge">TextWebSocketHandler</code> class provided
by Spring Boot. To keep it easy to read for the purpose of this tutorial, the <code class="language-plaintext highlighter-rouge">WssConnectorService</code> realizes a few
functionalities:</p>

<ul>
  <li>It keeps track of all the connected users and their sessions.</li>
  <li>It creates a ticket for a user when they connect to the WebSocket.</li>
  <li>It removes unused tickets every 5 minutes.</li>
  <li>It sends a message to a user.</li>
  <li>It pings all the connected users every 10 seconds to check if the connection is still alive.</li>
</ul>

<p>All tickets and sessions are stored in-memory, but in a real-world application, you could store them in a database or a
cache like Redis.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="nd">@Component</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">WssConnectorService</span> <span class="kd">extends</span> <span class="nc">TextWebSocketHandler</span>
<span class="o">{</span>
  <span class="kd">private</span> <span class="kd">final</span> <span class="nc">ObjectMapper</span> <span class="n">objectMapper</span><span class="o">;</span>
  <span class="kd">private</span> <span class="kd">final</span> <span class="nc">List</span><span class="o">&lt;</span><span class="nc">WssUser</span><span class="o">&gt;</span> <span class="n">wssUsers</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">CopyOnWriteArrayList</span><span class="o">&lt;&gt;();</span>
  <span class="kd">private</span> <span class="kd">final</span> <span class="nc">Map</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">WssTicketDetails</span><span class="o">&gt;</span> <span class="n">tickets</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ConcurrentHashMap</span><span class="o">&lt;&gt;();</span>

  <span class="c1">// Connection closed by client</span>
  <span class="nd">@Override</span>
  <span class="kd">public</span> <span class="kt">void</span> <span class="nf">afterConnectionClosed</span><span class="o">(</span><span class="nd">@NotNull</span> <span class="nc">WebSocketSession</span> <span class="n">session</span><span class="o">,</span> <span class="nd">@NotNull</span> <span class="nc">CloseStatus</span> <span class="n">status</span><span class="o">)</span>
  <span class="o">{</span>
    <span class="n">wssUsers</span><span class="o">.</span><span class="na">removeIf</span><span class="o">(</span><span class="n">wssUser</span> <span class="o">-&gt;</span> <span class="n">wssUser</span><span class="o">.</span><span class="na">session</span><span class="o">().</span><span class="na">equals</span><span class="o">(</span><span class="n">session</span><span class="o">));</span>
  <span class="o">}</span>

  <span class="c1">// Connection established by client</span>
  <span class="nd">@Override</span>
  <span class="kd">public</span> <span class="kt">void</span> <span class="nf">afterConnectionEstablished</span><span class="o">(</span><span class="nd">@NotNull</span> <span class="nc">WebSocketSession</span> <span class="n">session</span><span class="o">)</span>
  <span class="o">{</span>
    <span class="c1">// Example uri: /wss?ticket=123</span>
    <span class="nc">String</span> <span class="n">ticket</span> <span class="o">=</span> <span class="nc">Optional</span><span class="o">.</span><span class="na">ofNullable</span><span class="o">(</span><span class="n">session</span><span class="o">.</span><span class="na">getUri</span><span class="o">())</span>
            <span class="o">.</span><span class="na">map</span><span class="o">(</span><span class="nl">URI:</span><span class="o">:</span><span class="n">toString</span><span class="o">)</span>
            <span class="o">.</span><span class="na">map</span><span class="o">(</span><span class="nl">UriComponentsBuilder:</span><span class="o">:</span><span class="n">fromUriString</span><span class="o">)</span>
            <span class="o">.</span><span class="na">map</span><span class="o">(</span><span class="n">builder</span> <span class="o">-&gt;</span> <span class="n">builder</span><span class="o">.</span><span class="na">build</span><span class="o">().</span><span class="na">getQueryParams</span><span class="o">().</span><span class="na">getFirst</span><span class="o">(</span><span class="s">"ticket"</span><span class="o">))</span>
            <span class="o">.</span><span class="na">filter</span><span class="o">(</span><span class="nl">StringUtils:</span><span class="o">:</span><span class="n">hasText</span><span class="o">)</span>
            <span class="o">.</span><span class="na">orElseThrow</span><span class="o">(()</span> <span class="o">-&gt;</span> <span class="k">new</span> <span class="nc">BadRequestException</span><span class="o">(</span><span class="s">"Ticket not found in query"</span><span class="o">));</span>

    <span class="nc">WssTicketDetails</span> <span class="n">ticketDetails</span> <span class="o">=</span> <span class="n">tickets</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="n">ticket</span><span class="o">);</span>
    <span class="nc">WssUser</span> <span class="n">wssUser</span> <span class="o">=</span> <span class="nc">WssUser</span><span class="o">.</span><span class="na">builder</span><span class="o">()</span>
            <span class="o">.</span><span class="na">userId</span><span class="o">(</span><span class="n">ticketDetails</span><span class="o">.</span><span class="na">userId</span><span class="o">())</span>
            <span class="o">.</span><span class="na">projectToken</span><span class="o">(</span><span class="n">ticketDetails</span><span class="o">.</span><span class="na">project</span><span class="o">().</span><span class="na">projectToken</span><span class="o">())</span>
            <span class="o">.</span><span class="na">session</span><span class="o">(</span><span class="n">session</span><span class="o">)</span>
            <span class="o">.</span><span class="na">build</span><span class="o">();</span>
    <span class="n">tickets</span><span class="o">.</span><span class="na">remove</span><span class="o">(</span><span class="n">ticket</span><span class="o">);</span>
    <span class="n">wssUsers</span><span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="n">wssUser</span><span class="o">);</span>
  <span class="o">}</span>

  <span class="kd">public</span> <span class="kt">void</span> <span class="nf">sendEvent</span><span class="o">(</span><span class="nc">WssUser</span> <span class="n">wssUser</span><span class="o">,</span> <span class="nc">LiveEvent</span> <span class="n">event</span><span class="o">)</span>
  <span class="o">{</span>
    <span class="k">try</span>
    <span class="o">{</span>
      <span class="nc">String</span> <span class="n">json</span> <span class="o">=</span> <span class="n">objectMapper</span><span class="o">.</span><span class="na">writeValueAsString</span><span class="o">(</span><span class="n">event</span><span class="o">);</span>
      <span class="nc">WebSocketSession</span> <span class="n">session</span> <span class="o">=</span> <span class="n">wssUser</span><span class="o">.</span><span class="na">session</span><span class="o">();</span>
      <span class="n">session</span><span class="o">.</span><span class="na">sendMessage</span><span class="o">(</span><span class="k">new</span> <span class="nc">TextMessage</span><span class="o">(</span><span class="n">json</span><span class="o">));</span>
    <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span>
            <span class="nc">Exception</span> <span class="n">e</span><span class="o">)</span> <span class="c1">// you can catch more specific exception here and handle it in a different ways, e.g.: when the session is closed unexpectedly</span>
    <span class="o">{</span>
      <span class="n">wssUsers</span><span class="o">.</span><span class="na">remove</span><span class="o">(</span><span class="n">wssUser</span><span class="o">);</span>
    <span class="o">}</span>
  <span class="o">}</span>

  <span class="kd">public</span> <span class="nc">String</span> <span class="nf">createTicketForUser</span><span class="o">()</span>
  <span class="o">{</span>
    <span class="nc">User</span> <span class="n">user</span> <span class="o">=</span> <span class="c1">// e.g.: get current user</span>
            <span class="nc">Project</span> <span class="n">project</span> <span class="o">=</span> <span class="c1">// e.g.: get project for the user</span>
          <span class="nc">WssTicketDetails</span> <span class="n">ticketDetails</span> <span class="o">=</span> <span class="nc">WssTicketDetails</span><span class="o">.</span><span class="na">builder</span><span class="o">()</span>
          <span class="o">.</span><span class="na">project</span><span class="o">(</span><span class="n">project</span><span class="o">)</span>
          <span class="o">.</span><span class="na">userId</span><span class="o">(</span><span class="n">userId</span><span class="o">)</span>
          <span class="o">.</span><span class="na">createdAt</span><span class="o">(</span><span class="nc">Instant</span><span class="o">.</span><span class="na">now</span><span class="o">())</span>
          <span class="o">.</span><span class="na">build</span><span class="o">();</span>
    <span class="nc">String</span> <span class="n">ticket</span> <span class="o">=</span> <span class="c1">// create a unique ticket, e.g. UUID, SecureRandom, or JWT token with expiration time</span>
            <span class="n">tickets</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="n">ticket</span><span class="o">,</span> <span class="n">ticketDetails</span><span class="o">);</span>
    <span class="k">return</span> <span class="n">ticket</span><span class="o">;</span>
  <span class="o">}</span>

  <span class="nd">@Scheduled</span><span class="o">(</span><span class="n">fixedDelay</span> <span class="o">=</span> <span class="mi">5</span><span class="o">,</span> <span class="n">initialDelay</span> <span class="o">=</span> <span class="mi">5</span><span class="o">,</span> <span class="n">timeUnit</span> <span class="o">=</span> <span class="nc">TimeUnit</span><span class="o">.</span><span class="na">MINUTES</span><span class="o">)</span>
  <span class="kd">public</span> <span class="kt">void</span> <span class="nf">removeUnusedTickets</span><span class="o">()</span>
  <span class="o">{</span>
    <span class="n">tickets</span><span class="o">.</span><span class="na">entrySet</span><span class="o">().</span><span class="na">removeIf</span><span class="o">(</span><span class="k">this</span><span class="o">::</span><span class="n">isTicketExpired</span><span class="o">);</span>
  <span class="o">}</span>

  <span class="kd">private</span> <span class="kt">boolean</span> <span class="nf">isTicketExpired</span><span class="o">(</span><span class="nc">Map</span><span class="o">.</span><span class="na">Entry</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">WssTicketDetails</span><span class="o">&gt;</span> <span class="n">entry</span><span class="o">)</span>
  <span class="o">{</span>
    <span class="k">return</span> <span class="n">entry</span><span class="o">.</span><span class="na">getValue</span><span class="o">().</span><span class="na">createdAt</span><span class="o">().</span><span class="na">toEpochMilli</span><span class="o">()</span> <span class="o">&lt;</span> <span class="nc">System</span><span class="o">.</span><span class="na">currentTimeMillis</span><span class="o">()</span> <span class="o">-</span> <span class="nc">TimeUnit</span><span class="o">.</span><span class="na">MINUTES</span><span class="o">.</span><span class="na">toMillis</span><span class="o">(</span><span class="mi">5</span><span class="o">);</span>
  <span class="o">}</span>

  <span class="nd">@Scheduled</span><span class="o">(</span><span class="n">fixedDelay</span> <span class="o">=</span> <span class="mi">10</span><span class="o">,</span> <span class="n">initialDelay</span> <span class="o">=</span> <span class="mi">10</span><span class="o">,</span> <span class="n">timeUnit</span> <span class="o">=</span> <span class="nc">TimeUnit</span><span class="o">.</span><span class="na">SECONDS</span><span class="o">)</span>
  <span class="kd">public</span> <span class="kt">void</span> <span class="nf">pingSessions</span><span class="o">()</span>
  <span class="o">{</span>
    <span class="k">for</span> <span class="o">(</span><span class="nc">WssUser</span> <span class="n">wssUser</span> <span class="o">:</span> <span class="n">wssUsers</span><span class="o">)</span>
    <span class="o">{</span>
      <span class="n">sendEvent</span><span class="o">(</span><span class="n">wssUser</span><span class="o">,</span> <span class="nc">LivePingEvent</span><span class="o">.</span><span class="na">ping</span><span class="o">());</span>
    <span class="o">}</span>
  <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">LiveEvent</code> class is a simple interface that represents an event that can be sent to the client. We have two types of
events: <code class="language-plaintext highlighter-rouge">PING</code> and <code class="language-plaintext highlighter-rouge">CONTENT_CHANGE</code>. Here is the implementation of the <code class="language-plaintext highlighter-rouge">LivePingEvent</code> class:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">interface</span> <span class="nc">LiveEvent</span>
<span class="o">{</span>
  <span class="nc">LiveEventType</span> <span class="nf">type</span><span class="o">();</span>
<span class="o">}</span>

<span class="kd">public</span> <span class="kd">enum</span> <span class="nc">LiveEventType</span>
<span class="o">{</span>
  <span class="no">PING</span><span class="o">,</span>
  <span class="no">CONTENT_CHANGE</span>
<span class="o">}</span>

<span class="nd">@Builder</span>
<span class="kd">public</span> <span class="kd">record</span> <span class="nf">LivePingEvent</span><span class="o">(</span><span class="nc">LiveEventType</span> <span class="n">type</span><span class="o">,</span> <span class="nc">String</span> <span class="n">message</span><span class="o">)</span> <span class="kd">implements</span> <span class="nc">LiveEvent</span>
<span class="o">{</span>
  <span class="kd">public</span> <span class="kd">static</span> <span class="nc">LivePingEvent</span> <span class="nf">ping</span><span class="o">()</span>
  <span class="o">{</span>
    <span class="k">return</span> <span class="nc">LivePingEvent</span><span class="o">.</span><span class="na">builder</span><span class="o">().</span><span class="na">type</span><span class="o">(</span><span class="nc">LiveEventType</span><span class="o">.</span><span class="na">PING</span><span class="o">).</span><span class="na">message</span><span class="o">(</span><span class="s">"ping"</span><span class="o">).</span><span class="na">build</span><span class="o">();</span>
  <span class="o">}</span>

  <span class="nd">@Override</span>
  <span class="kd">public</span> <span class="nc">LiveEventType</span> <span class="nf">getType</span><span class="o">()</span>
  <span class="o">{</span>
    <span class="k">return</span> <span class="n">type</span><span class="o">;</span>
  <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>Isn’t it simple? We have a service class that extends <code class="language-plaintext highlighter-rouge">TextWebSocketHandler</code> and keeps track of all the connected users
and their sessions. Here is the controller class that we will use to create a ticket for a user when they connect to the
WebSocket, and send the ticket to the client:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="nd">@RestController</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">WssConnectorController</span>
<span class="o">{</span>
  <span class="kd">private</span> <span class="kd">final</span> <span class="nc">WssConnectorService</span> <span class="n">wssConnectorService</span><span class="o">;</span>

  <span class="nd">@GetMapping</span><span class="o">(</span><span class="s">"/wss/ticket"</span><span class="o">)</span>
  <span class="kd">public</span> <span class="nc">WssTicketResponse</span> <span class="nf">getTicket</span><span class="o">()</span>
  <span class="o">{</span>
    <span class="nc">String</span> <span class="n">ticket</span> <span class="o">=</span> <span class="n">wssConnectorService</span><span class="o">.</span><span class="na">createTicket</span><span class="o">();</span>
    <span class="k">return</span> <span class="nc">WssTicketResponse</span><span class="o">.</span><span class="na">builder</span><span class="o">().</span><span class="na">ticket</span><span class="o">(</span><span class="n">ticket</span><span class="o">).</span><span class="na">build</span><span class="o">();</span>
  <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>Solid and dead simple, not very scalable as we used in-memory storage to keep everything, but it’s a good starting
point.</p>

<p>Here is the configuration class that we will use to configure WebSockets in our Spring Boot 3 application. I’ve
created a class called <code class="language-plaintext highlighter-rouge">WssConfig</code> that implements the <code class="language-plaintext highlighter-rouge">WebSocketConfigurer</code> interface, and added <code class="language-plaintext highlighter-rouge">@EnableWebSocket</code>
annotation to enable WebSockets in our application.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">org.springframework.beans.factory.annotation.Autowired</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.context.annotation.Configuration</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.web.socket.config.annotation.EnableWebSocket</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.web.socket.config.annotation.WebSocketConfigurer</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry</span><span class="o">;</span>

<span class="nd">@Configuration</span>
<span class="nd">@EnableWebSocket</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">WssConfig</span> <span class="kd">implements</span> <span class="nc">WebSocketConfigurer</span>
<span class="o">{</span>

  <span class="kd">public</span> <span class="nf">WssConfig</span><span class="o">(</span><span class="nc">WssConnectorService</span> <span class="n">wssConnectorService</span><span class="o">)</span>
  <span class="o">{</span>
    <span class="k">this</span><span class="o">.</span><span class="na">wssConnectorService</span> <span class="o">=</span> <span class="n">wssConnectorService</span><span class="o">;</span>
  <span class="o">}</span>

  <span class="nd">@Override</span>
  <span class="kd">public</span> <span class="kt">void</span> <span class="nf">registerWebSocketHandlers</span><span class="o">(</span><span class="nc">WebSocketHandlerRegistry</span> <span class="n">registry</span><span class="o">)</span>
  <span class="o">{</span>
    <span class="n">registry</span><span class="o">.</span><span class="na">addHandler</span><span class="o">(</span><span class="n">wssConnectorService</span><span class="o">,</span> <span class="s">"/wss"</span><span class="o">).</span><span class="na">setAllowedOrigins</span><span class="o">(</span><span class="s">"*"</span><span class="o">);</span>
  <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<blockquote>
  <p>Please also remember that you have to adjust your Spring Security configuration to allow access to the <code class="language-plaintext highlighter-rouge">/wss</code>
endpoint.</p>
</blockquote>

<h2 id="how-to-connect-to-the-websocket-from-the-client">How to connect to the WebSocket from the client</h2>

<p>Now, that we have configured WebSockets in our Spring Boot 3 application, let’s see how we can connect to the WebSocket
from the client. This part is a most satisfying one, as we will use a native APIs provided by the browser to connect to
the WebSocket.</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">useEffect</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="kd">let</span> <span class="na">wss</span><span class="p">:</span> <span class="nx">WebSocket</span><span class="p">;</span>

  <span class="kd">const</span> <span class="nx">connectToServer</span> <span class="o">=</span> <span class="k">async </span><span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">openWebSocket</span> <span class="o">=</span> <span class="p">(</span><span class="na">ticket</span><span class="p">:</span> <span class="kr">string</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
      <span class="nx">wss</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">WebSocket</span><span class="p">(</span><span class="dl">"</span><span class="s2">ws://localhost:8080/wss?ticket=</span><span class="dl">"</span> <span class="o">+</span> <span class="nx">ticket</span><span class="p">);</span>
      <span class="nx">wss</span><span class="p">.</span><span class="nx">onopen</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">Connection opened</span><span class="dl">"</span><span class="p">);</span>
      <span class="nx">wss</span><span class="p">.</span><span class="nx">onmessage</span> <span class="o">=</span> <span class="p">(</span><span class="na">message</span><span class="p">:</span> <span class="kr">any</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
        <span class="kd">const</span> <span class="nx">response</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nf">parse</span><span class="p">(</span><span class="nx">message</span><span class="p">?.</span><span class="nx">data</span><span class="p">);</span>
        <span class="k">if </span><span class="p">(</span><span class="nx">response</span><span class="p">.</span><span class="kd">type</span> <span class="o">===</span> <span class="nx">SseEventType</span><span class="p">.</span><span class="nx">CONTENT_CHANGE</span><span class="p">)</span> <span class="p">{</span>
          <span class="c1">// update the UI with the new content</span>
        <span class="p">}</span>
      <span class="p">};</span>
      <span class="nx">wss</span><span class="p">.</span><span class="nx">onclose</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">Connection closed</span><span class="dl">"</span><span class="p">);</span> <span class="c1">// you can try to reconnect here</span>
    <span class="p">};</span>
    <span class="nf">fetch</span><span class="p">(</span><span class="dl">"</span><span class="s2">/wss/ticket</span><span class="dl">"</span><span class="p">)</span>
      <span class="p">.</span><span class="nf">then</span><span class="p">((</span><span class="nx">response</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">response</span><span class="p">.</span><span class="nf">json</span><span class="p">())</span>
      <span class="p">.</span><span class="nf">then</span><span class="p">((</span><span class="nx">data</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">data</span><span class="p">.</span><span class="nx">ticket</span><span class="p">)</span>
      <span class="p">.</span><span class="nf">then</span><span class="p">((</span><span class="nx">ticket</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nf">openWebSocket</span><span class="p">(</span><span class="nx">ticket</span><span class="p">))</span>
      <span class="p">.</span><span class="k">catch</span><span class="p">((</span><span class="nx">error</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">console</span><span class="p">.</span><span class="nf">error</span><span class="p">(</span><span class="dl">"</span><span class="s2">Error getting ticket</span><span class="dl">"</span><span class="p">,</span> <span class="nx">error</span><span class="p">));</span>
  <span class="p">};</span>

  <span class="nf">connectToServer</span><span class="p">().</span><span class="k">catch</span><span class="p">((</span><span class="nx">error</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">console</span><span class="p">.</span><span class="nf">error</span><span class="p">(</span><span class="dl">"</span><span class="s2">Error while initial connection</span><span class="dl">"</span><span class="p">,</span> <span class="nx">error</span><span class="p">));</span>
  <span class="k">return </span><span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">Gracefully closing the connection</span><span class="dl">"</span><span class="p">);</span>
    <span class="nx">wss</span><span class="p">.</span><span class="nf">close</span><span class="p">();</span>
  <span class="p">};</span>
<span class="p">},</span> <span class="p">[]);</span>
</code></pre></div></div>

<p>In this code snippet, we are using the <code class="language-plaintext highlighter-rouge">WebSocket</code> and <code class="language-plaintext highlighter-rouge">fetch</code> APIs provided by the browser to connect to the WebSocket
from the client. We are first fetching a ticket from the server using the <code class="language-plaintext highlighter-rouge">/wss/ticket</code> endpoint, and then we are
connecting to the WebSocket using the ticket.</p>

<p>We are also handling the <code class="language-plaintext highlighter-rouge">onopen</code> and <code class="language-plaintext highlighter-rouge">onmessage</code> events of the WebSocket to log the connection status and process the
messages received from the server.</p>

<p>Once you connect to the WebSocket, you can also periodically check if the connection is closed and reconnect if
necessary, e.g.:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">setInterval</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="k">if </span><span class="p">(</span><span class="nx">wss</span><span class="p">?.</span><span class="nx">readyState</span> <span class="o">===</span> <span class="nx">WebSocket</span><span class="p">.</span><span class="nx">CLOSED</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">Connection closed, reconnecting...</span><span class="dl">"</span><span class="p">);</span>
    <span class="nf">connectToServer</span><span class="p">().</span><span class="k">catch</span><span class="p">((</span><span class="nx">error</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">console</span><span class="p">.</span><span class="nf">error</span><span class="p">(</span><span class="dl">"</span><span class="s2">Error while reconnecting</span><span class="dl">"</span><span class="p">,</span> <span class="nx">error</span><span class="p">));</span>
  <span class="p">}</span>
<span class="p">},</span> <span class="mi">2000</span><span class="p">);</span>

<span class="c1">// cleanup the interval when the component is unmounted</span>
<span class="k">return </span><span class="p">()</span> <span class="o">=&gt;</span> <span class="nf">clearInterval</span><span class="p">(</span><span class="nx">intervalId</span><span class="p">);</span>
</code></pre></div></div>

<h2 id="conclusion">Conclusion</h2>

<p>That’s all! In this post, we have set up the most basic implementation of WebSockets with Spring Boot 3 to send live
updates from the server to the client.</p>

<p>The biggest advantage of this approach (and disadvantage at the same time) is that it’s very simple and doesn’t require
any additional libraries like STOMP or SockJS. This simplicity makes the setup lightweight and easy to understand,
reducing the overhead of learning and maintaining extra dependencies. However, it also means that you miss out on the
additional features and abstractions provided by these libraries, such as automatic fallback mechanisms and higher-level
messaging protocols.</p>

<p>If your application requirements are straightforward and you prefer to minimize dependencies, this approach is ideal.
However, if you need more advanced features like message brokering, topic subscriptions, or support for older browsers
and network environments, you might want to consider using mentioned technologies.</p>

<p>Below is an example of how the auto-translation feature works using a Context Menu in <a href="https://simplelocalize.io">SimpleLocalize</a>:</p>

<p><img src="/assets/2024-07-15/context-menu-auto-translation.mp4" alt="Auto-translation via Context Menu in SimpleLocalize" /></p>]]></content><author><name>GitHub User</name><email>your-email@domain.com</email></author><category term="java" /><category term="spring boot" /><category term="frontend" /><category term="websocket" /><summary type="html"><![CDATA[How to configure WebSockets in Spring Boot without STOMP and SockJS]]></summary></entry><entry><title type="html">Raspberry Pi 5 - Game Streaming Console</title><link href="https://jpomykala.com/2024/05/19/raspberry-pi-5-streaming-machine/" rel="alternate" type="text/html" title="Raspberry Pi 5 - Game Streaming Console" /><published>2024-05-19T00:00:00+00:00</published><updated>2024-05-19T00:00:00+00:00</updated><id>https://jpomykala.com/2024/05/19/raspberry-pi-5-streaming-machine</id><content type="html" xml:base="https://jpomykala.com/2024/05/19/raspberry-pi-5-streaming-machine/"><![CDATA[<p>If you are as excited as I’m when you think about streaming games from your PC to your TV and getting a console-like experience at 4K HDR and 60FPS, then… you better get an Apple TV 4K or Nvidia Shield, and save your time. 😉</p>

<p>I started realising my idea from upgrading my network, by getting a better Wi-Fi PCIe card for my PC and router with WiFi 6. 
Previous setup wasn’t able to keep stable 80MB/s transfers, mostly due to weak Wi-Fi signal at my PC. I also had to switch from Raspberry Pi 3b+ to Raspberry Pi 5, because RPI3 doesn’t support 4K resolution.</p>

<p>To make sure everything will go smoothing from that point I’ve also took a case with active cooling, official microHDMI 2.1 cable, and official 27W power adapter.</p>

<p>…and that was just a start of my troubles. At time of writing this post (May 2024) <a href="https://github.com/moonlight-stream/moonlight-qt/issues/1133">Raspberry Pi 5 doesn’t work nice with moonlight-qt</a> and there is no official Steam Link app for ARM64, so I took me a few days of digging to get the setup working. I’m writing this post so you don’t have to waste your time.</p>

<p>Here you can find a repository with all the scripts: <a href="https://github.com/jpomykala/moonlight-pi">https://github.com/jpomykala/moonlight-pi</a></p>

<h2 id="before-we-start">Before we start</h2>

<p>I assume that you have already done the followings:</p>

<p>Gaming PC side:</p>

<ul>
  <li>Installed Steam</li>
  <li>Installed Sunshine</li>
  <li>Configured a static address on your router</li>
  <li>Configured Wake-On-Lan (WOL)</li>
</ul>

<p>Raspberry Pi 5 side:</p>

<ul>
  <li>Installed Raspbian Bookworm Lite (x64)</li>
  <li>Installed moonlight-qt on Raspberry Pi</li>
</ul>

<p>I won’t cover those topics in this post, as they are straightforward to do. I also assume you have some
basic IT skills to find information on your own, like finding a MAC address, using a command line, bash etc.</p>

<h2 id="moonlight-and-raspberry-pi-5">Moonlight and Raspberry Pi 5</h2>

<p>Unfortunately, at time I’m writing the post the default configuration that comes with moonlight-qt doesn’t play nicely
with Raspberry Pi 5, but way to make it work by creating a custom <code class="language-plaintext highlighter-rouge">egls.json</code> configuration file.</p>

<p>I’ve a 4K TV, so in my case the <code class="language-plaintext highlighter-rouge">egls.json</code> file looks like this:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{
  "device": "/dev/dri/card1",
  "outputs": [
    {
      "name": "HDMI1",
      "mode": "3840x2160"
    }
  ]
}
</code></pre></div></div>
<p>For different resolutions you should just change the <code class="language-plaintext highlighter-rouge">mode</code> value.</p>

<p><strong>Important:</strong> Raspberry Pi 5 has 2 microHDMI ports, and the above <code class="language-plaintext highlighter-rouge">egls.json</code> assumes that you connect your TV to the microHDMI port next to power input.</p>

<p>Let’s run it:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>QT_QPA_EGLFS_KMS_CONFIG=eglfs.json moonlight-qt
</code></pre></div></div>

<p>Now, you should see a moonlight app on your TV screen, and if you PC is running it should be there. You can use a small cog icon in the top-right corner to adjust the streaming settings to your liking. In my case I’ve selected 4K resolution and 60FPS with ~80MBs of bitrate. I’ve set encoders and decoder settings to “Automatic” and I checked “HDR (Experimental)”</p>

<h2 id="connecting-xbox-controller">Connecting Xbox Controller</h2>

<p>For connecting an Xbox Controller I’ve used an official Xbox dongle and <a href="https://github.com/medusalix/xone">‘xone’ driver</a>
that worked like a charm on a first shot. Please follow the instructions on the GitHub page to install the driver as they might change in the future.</p>

<p>Unfortunately, my previous attempt of connecting Xbox Controller via Bluetooth didn’t work for some reason, 
so I just turned off built-in Bluetooth and Wi-Fi, just to save power and decrease the interference using <code class="language-plaintext highlighter-rouge">rfkill block wlan</code> and <code class="language-plaintext highlighter-rouge">rfkill block bluetooth</code>.</p>

<h2 id="streaming-steam-big-picture">Streaming Steam Big Picture</h2>

<p>By default, moonlight opens a window where we can select our PC and change the settings. Once we choose a PC then we have to choose an app.
Fortunately, moonlight-qt developers covered a case if we would like to automate the connection. 
Let’s start from listing apps that are available on your PC using <code class="language-plaintext highlighter-rouge">moonlight-qt list &lt;YOUR_PC_IP_ADDRESS&gt;</code></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>jpomykala@raspberry:~ $ moonlight-qt list 192.168.0.120
00:00:00 - Qt Info: Unable to detect Wayland or X11, so EGLFS will be used by default. Set QT_QPA_PLATFORM to override this.
00:00:00 - Qt Info: Setting display mode by default. Set QT_QPA_EGLFS_ALWAYS_SET_MODE=0 to override this.
00:00:00 - Qt Critical: drmModeGetResources failed (Operation not supported)
Desktop
Steam Big Picture
jpomykala@raspberry:~ $ 
</code></pre></div></div>
<p>By default, moonlight have 2 apps, <code class="language-plaintext highlighter-rouge">Desktop</code> and <code class="language-plaintext highlighter-rouge">Steam Big Picture</code>, with this information we create a command that will look like this:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>QT_QPA_EGLFS_KMS_CONFIG=eglfs.json moonlight-qt stream 192.168.0.120 'Steam Big Picture'
</code></pre></div></div>

<p>to start streaming Steam Big Picture automatically without needing any interaction.</p>

<h2 id="waking-up-the-pc">Waking up the PC</h2>

<p>Waking up the PC is a nice part of this post. I’ve installed <code class="language-plaintext highlighter-rouge">etherwake</code> package that allows me to send a Magic Packet</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo apt install etherwake
</code></pre></div></div>

<p>Using it is simple as that:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>etherwake &lt;PC_MAC_ADDRESS&gt;
</code></pre></div></div>

<p>You don’t need to add any IP address, mask or anything. It should just work. Here is copy and paste installation command:</p>

<h2 id="listening-for-controller-events">Listening for controller events</h2>

<p>This is the most exciting part of this post. Whenever, I turn on my Xbox Controller I wanted to wake up my PC,
and run moonlight to start streaming the Steam Big Picture. For this I’ve used <code class="language-plaintext highlighter-rouge">udev</code> and <code class="language-plaintext highlighter-rouge">udev rules</code>. <code class="language-plaintext highlighter-rouge">Udev</code> allows 
me to detect whenever my Xbox Controller is connected or disconnected and run some custom scripts.</p>

<h3 id="getting-controller-details">Getting controller details</h3>

<p>Lets start about getting some details about the controller as we need to know what to look for in <code class="language-plaintext highlighter-rouge">udev</code> rules. 
Make sure that your controller is connected and run the following command: <code class="language-plaintext highlighter-rouge">ls -l /dev/input</code></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ ls -l /dev/input/
total 0
drwxr-xr-x 2 root root     100 May 19 14:53 by-path
crw-rw---- 1 root input 13, 64 May 19 13:17 event0
crw-rw---- 1 root input 13, 65 May 19 14:10 event1
crw-rw---- 1 root input 13, 66 May 19 14:10 event2
crw-rw---- 1 root input 13, 67 May 19 14:10 event3
crw-rw---- 1 root input 13, 68 May 19 14:10 event4
crw-rw---- 1 root input 13, 68 May 19 14:10 event5
crw-rw---- 1 root input 13, 63 May 19 13:17 mice
</code></pre></div></div>

<p>This will list all input devices connected to your Raspberry Pi and you should see a few <code class="language-plaintext highlighter-rouge">eventX</code> files. 
In my case, the controller was the last device, which is <code class="language-plaintext highlighter-rouge">event5</code>, so let’s get more information to see if I was right.
Run the following command:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>udevadm info --name /dev/eventX --attribute-walk
</code></pre></div></div>

<p>and replace <code class="language-plaintext highlighter-rouge">eventX</code> with the number you’ve found in the previous step. In my case it was <code class="language-plaintext highlighter-rouge">event5</code>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ udevadm info --name /dev/event5 --attribute-walk
looking at parent device '/devices/platform/axi/1000120000.pcie/1f00300000.usb/xhci-hcd.1/usb3/3-1/3-1:1.0/gip0/gip0.0/input/input8':
    KERNELS=="input8"
    SUBSYSTEMS=="input"
    DRIVERS==""
    ATTRS{capabilities/abs}=="3003f"
    ATTRS{capabilities/ev}=="20000b"
    ATTRS{capabilities/ff}=="107030000 0"
    ATTRS{capabilities/key}=="7cdb000000000000 0 0 0 0"
    ATTRS{capabilities/led}=="0"
    ATTRS{capabilities/msc}=="0"
    ATTRS{capabilities/rel}=="0"
    ATTRS{capabilities/snd}=="0"
    ATTRS{capabilities/sw}=="0"
    ATTRS{id/bustype}=="0006"
    ATTRS{id/product}=="02ea"
    ATTRS{id/vendor}=="045e"
    ATTRS{id/version}=="0408"
    ATTRS{inhibited}=="0"
    ATTRS{name}=="Microsoft Xbox Controller"
</code></pre></div></div>

<p>Look at line <code class="language-plaintext highlighter-rouge">ATTRS{name}=="Microsoft Xbox Controller"</code> is what we are looking for, this is the name of the controller that we are going to use in our <code class="language-plaintext highlighter-rouge">udev</code> rule.</p>

<h3 id="writing-udev-rules">Writing <code class="language-plaintext highlighter-rouge">udev</code> rules</h3>

<p>Now, we are going to create a rule that will execute a script whenever the controller is connected or disconnected. Create a file for a new rules:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo vim /etc/udev/rules.d/99-xbox-controller.rules
</code></pre></div></div>

<p>and add the following content:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ACTION=="add", SUBSYSTEM=="input", ATTRS{name}=="Microsoft Xbox Controller", RUN+="/home/jpomykala/moonlight/controller-connected.sh"
ACTION=="remove", SUBSYSTEM=="input", ATTRS{name}=="Microsoft Xbox Controller", RUN+="/home/jpomykala/moonlight/controller-disconnected.sh"
</code></pre></div></div>

<p>The first rule, will execute script at <code class="language-plaintext highlighter-rouge">/home/jpomykala/moonlight/controller-connected.sh</code> whenever the controller is connected, and it’s name is <code class="language-plaintext highlighter-rouge">Microsoft Xbox Controller</code>.
The second rule will execute <code class="language-plaintext highlighter-rouge">/home/jpomykala/moonlight/controller-disconnected.sh</code> whenever the controller is disconnected.</p>

<p>The rules won’t work until we reload them, but before we do this, let’s create those scripts.</p>

<h3 id="logging-and-debugging">Logging and debugging</h3>

<p>Before we start writing scripts, lets create a place where we can store logs, in my case I’ve chosen: <code class="language-plaintext highlighter-rouge">/var/log/xbox-controller.log</code>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>touch /var/log/xbox-controller.log
chmod +0777 /var/log/xbox-controller.log
</code></pre></div></div>

<p>I’ve set the permissions to 0777, so I can do anything with this file, but you can set it to 0644 if you want to be more restrictive.</p>

<h3 id="when-controller-connects">When controller connects</h3>

<p><a href="https://github.com/jpomykala/moonlight-pi/blob/main/controller-connected.sh"><code class="language-plaintext highlighter-rouge">controller-connected.sh</code> script</a> does a few things:</p>

<ul>
  <li>creates a lockfile, because <code class="language-plaintext highlighter-rouge">udev</code> event fires 4 times, when my controller is connecting for some reason</li>
  <li>wakes my PC</li>
  <li>waits until I get a ping from my PC</li>
  <li>checks if the <code class="language-plaintext highlighter-rouge">moonlight-qt</code> is already running, and if not start it</li>
  <li>deletes a lockfile</li>
</ul>

<h3 id="when-controller-disconnects">When controller disconnects</h3>

<p><a href="https://github.com/jpomykala/moonlight-pi/blob/main/controller-disconnected.sh"><code class="language-plaintext highlighter-rouge">controller-disconnected.sh</code> script</a> is much shorter as it only force kills the moonlight. It also requires a lockfile as the rule we’ve created fires 2 times.</p>

<h3 id="wrapping-up">Wrapping up</h3>

<p>Remember to make all files executable via:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>chmod +x controller-connected.sh
chmod +x controller-disconnected.sh
</code></pre></div></div>

<p>Now, let’s wrap this up and enable the rule by reloading them:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo udevadm control --reload-rules
</code></pre></div></div>

<p>To browse the logs use:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>tail -f /var/log/xbox-controller.log
</code></pre></div></div>

<p>You can also check logs for <code class="language-plaintext highlighter-rouge">udev</code> via journalctl:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo journalctl -u systemd-udevd -f
</code></pre></div></div>

<p>Now your Raspberry Pi should wake up your PC and start streaming Steam Big Picture whenever you connect your Xbox Controller
and stop the streaming whenever you disconnect it. 🥳</p>

<h2 id="further-work">Further work</h2>

<p>I plan to improve the final solution in a few ways:</p>

<ul>
  <li>Wake on Lan part got naive assumptions; Whenever I’ve got ping from the PC I just wait a couple of seconds to make sure the Sunshine is ready to stream, this could be improved.</li>
  <li>Turning off PC; The current solution doesn’t turn off the PC at all, so this could be added. I didn’t find a way yet to do this without installing additional software on Windows.</li>
  <li>CEC and TV integration, I plan to use ‘cec-utils’ to automatically switch the TV output to the Raspberry Pi. For some reason, the CEC commands do nothing when I was executing them against my TV, so I give up for a moment.</li>
</ul>

<p>Additionally, I wanted to add a small speaker to the Raspberry Pi to play a sound when I launch ‘moonlight-qt’. Furthermore, I would like to start some boot animation to make the startup process more visually appealing. This will add a more console feeling.</p>]]></content><author><name>GitHub User</name><email>your-email@domain.com</email></author><category term="gaming" /><summary type="html"><![CDATA[My journey to create a game streaming console using Raspberry Pi 5 and Moonlight.]]></summary></entry><entry><title type="html">Custom built search for docs with Algolia</title><link href="https://jpomykala.com/2022/04/12/next-js-algolia-markdown-search/" rel="alternate" type="text/html" title="Custom built search for docs with Algolia" /><published>2022-04-12T00:00:00+00:00</published><updated>2022-04-12T00:00:00+00:00</updated><id>https://jpomykala.com/2022/04/12/next-js-algolia-markdown-search</id><content type="html" xml:base="https://jpomykala.com/2022/04/12/next-js-algolia-markdown-search/"><![CDATA[<p>Since 2019 I’m building a <a href="https://simplelocalize.io">SaaS for translation management</a> as a solo developer. Since the very beginning,
I needed a place where I could post code samples, and explain to people how to integrate different frameworks or how to use SimpleLocalize CLI to upload and download translation files. That’s why I built my project documentation. Once the app and docs started to grow, it turned out that I need a search option. After some research, I decided to implement Algolia.
See Algolia search in action on <a href="https://simplelocalize.io/docs/">SimpleLocalize documentation</a>.</p>

<p><img src="/assets/2022-04-12/example-search-results.png" alt="algolia nextjs markdown example" /></p>

<p>At the very beginning of my journey with documentation, I was testing <a href="https://www.gitbook.com">GitBook</a> software because it was shining and trendy at the time. It was a good move,
because when you start something new, you want to avoid starting from scratch on every level. The main goal then is to deliver good quality code and value to the customers, all other things you can (or should) buy to save time.</p>

<p>Long story short, I wasn’t thrilled with GitBook after a couple of months because it didn’t fit to my needs. I didn’t like the style, and I was spending too much time on adjusting and fixing links and so on. I decided to go with my custom documentation, and 
surprisingly, everything went smoothly! The only concern was lack of good search option, which is available in all documentation solutions, 
or <a href="https://github.com/facebook/docusaurus">open-source Docusaurus</a>. While searching for the best options and considering implementing my own search, I came across Algolia.</p>

<h2 id="installation">Installation</h2>

<p>Start with Algolia search components and Algolia client installation:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm <span class="nb">install </span>react-instantsearch-dom algoliasearch <span class="nt">--save</span>
</code></pre></div></div>

<p>Then install <a href="https://github.com/jonschlinkert/gray-matter">gray-matter</a>, a Markdown parser, and <a href="https://github.com/sindresorhus/globby">globby</a> for finding files:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm <span class="nb">install </span>gray-matter globby <span class="nt">--saveDev</span>
</code></pre></div></div>

<h2 id="configuration">Configuration</h2>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">algoliasearch</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">algoliasearch/lite</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span><span class="nx">connectStateResults</span><span class="p">,</span> <span class="nx">Hits</span><span class="p">,</span> <span class="nx">InstantSearch</span><span class="p">,</span> <span class="nx">SearchBox</span><span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react-instantsearch-dom</span><span class="dl">'</span><span class="p">;</span>

<span class="kd">const</span> <span class="nx">searchClient</span> <span class="o">=</span> <span class="nf">algoliasearch</span><span class="p">(</span>
  <span class="dl">'</span><span class="s1">APP_ID</span><span class="dl">'</span><span class="p">,</span>
  <span class="dl">'</span><span class="s1">SEARCH_API_KEY</span><span class="dl">'</span>
<span class="p">);</span>
</code></pre></div></div>

<h3 id="results-component">Results component</h3>

<p>By default, Algolia <code class="language-plaintext highlighter-rouge">&lt;Hits/&gt;</code> component shows all results when search query is empty. I adjusted the code to show results only if there are any results and data are loaded.</p>

<div class="language-tsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">Results</span> <span class="o">=</span> <span class="nf">connectStateResults</span><span class="p">(({</span><span class="nx">searchState</span><span class="p">,</span> <span class="nx">searchResults</span><span class="p">,</span> <span class="nx">searching</span><span class="p">})</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">hasQuery</span> <span class="o">=</span> <span class="nx">searchState</span> <span class="o">&amp;&amp;</span> <span class="nx">searchState</span><span class="p">.</span><span class="nx">query</span><span class="p">;</span>
  <span class="kd">const</span> <span class="nx">hasResults</span> <span class="o">=</span> <span class="p">(</span><span class="nx">searchResults</span><span class="p">?.</span><span class="nx">hits</span> <span class="o">??</span> <span class="p">[]).</span><span class="nx">length</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">;</span>
  <span class="kd">const</span> <span class="nx">isSearching</span> <span class="o">=</span> <span class="nx">searching</span><span class="p">;</span>
  <span class="k">if </span><span class="p">(</span><span class="nx">hasQuery</span> <span class="o">&amp;&amp;</span> <span class="nx">hasResults</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span> <span class="p">&lt;</span><span class="nc">Hits</span> <span class="na">hitComponent</span><span class="p">=</span><span class="si">{</span><span class="nx">Hit</span><span class="si">}</span><span class="p">/&gt;;</span>
  <span class="p">}</span>
  <span class="k">if </span><span class="p">(</span><span class="nx">hasQuery</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="nx">hasResults</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="nx">isSearching</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span> <span class="p">&lt;</span><span class="nt">div</span><span class="p">&gt;</span>No results 😔<span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
  <span class="p">}</span>
  <span class="k">return</span> <span class="kc">null</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">);</span>
</code></pre></div></div>

<h3 id="hit-component">Hit component</h3>

<p>Hit component is nothing but just one search result. I stripped my styling to make it easier to copy-paste. 😄</p>

<div class="language-tsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">React</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">react</span><span class="dl">"</span><span class="p">;</span>

<span class="kd">function</span> <span class="nf">Hit</span><span class="p">(</span><span class="nx">props</span><span class="p">:</span> <span class="kr">any</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">content</span> <span class="o">=</span> <span class="nx">props</span><span class="p">?.</span><span class="nx">hit</span><span class="p">?.</span><span class="nx">content</span> <span class="o">??</span> <span class="dl">""</span><span class="p">;</span>
  <span class="kd">const</span> <span class="nx">words</span> <span class="o">=</span> <span class="nx">content</span><span class="p">.</span><span class="nf">split</span><span class="p">(</span><span class="dl">"</span><span class="s2"> </span><span class="dl">"</span><span class="p">).</span><span class="nx">length</span><span class="p">;</span>
  <span class="k">return </span><span class="p">(&lt;</span><span class="nt">a</span> <span class="na">href</span><span class="p">=</span><span class="si">{</span><span class="nx">props</span><span class="p">.</span><span class="nx">hit</span><span class="p">.</span><span class="nx">slug</span><span class="si">}</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">div</span><span class="p">&gt;</span>
      <span class="p">&lt;</span><span class="nt">h3</span><span class="p">&gt;</span><span class="si">{</span><span class="nx">props</span><span class="p">?.</span><span class="nx">hit</span><span class="p">?.</span><span class="nx">frontmatter</span><span class="p">?.</span><span class="nx">title</span> <span class="o">??</span> <span class="dl">"</span><span class="s2">no title</span><span class="dl">"</span><span class="si">}</span><span class="p">&lt;/</span><span class="nt">h3</span><span class="p">&gt;</span>
    <span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span><span class="si">{</span><span class="nx">props</span><span class="p">?.</span><span class="nx">hit</span><span class="p">?.</span><span class="nx">frontmatter</span><span class="p">?.</span><span class="nx">excerpt</span> <span class="o">??</span> <span class="dl">""</span><span class="si">}</span><span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
  <span class="p">&lt;/</span><span class="nt">a</span><span class="p">&gt;)</span>
<span class="p">}</span>

<span class="k">export</span> <span class="k">default</span> <span class="nx">Hit</span><span class="p">;</span>
</code></pre></div></div>

<h3 id="search-component">Search component</h3>

<p>Use <code class="language-plaintext highlighter-rouge">InstantSearch</code> component and search client, which you configured in the previous step. Put your index name and use Algolia <code class="language-plaintext highlighter-rouge">SearchBox</code> component and my custom <code class="language-plaintext highlighter-rouge">Results</code> component we created previously to change empty state behavior.</p>

<div class="language-tsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">&lt;</span><span class="nc">InstantSearch</span> <span class="na">indexName</span><span class="p">=</span><span class="s">"simplelocalize-docs"</span> <span class="na">searchClient</span><span class="p">=</span><span class="si">{</span><span class="nx">searchClient</span><span class="si">}</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nc">SearchBox</span><span class="p">/&gt;</span>
    <span class="p">&lt;</span><span class="nc">Results</span><span class="p">/&gt;</span>
<span class="p">&lt;/</span><span class="nc">InstantSearch</span><span class="p">&gt;</span>
</code></pre></div></div>
<p>I felt it would be better not to make it more complex than it needs to be, so I overwrite some CSS properties in my stylesheets to hide the search and reset buttons. I also added Bootstrap styling to the search box using SCSS <code class="language-plaintext highlighter-rouge">@extend</code> property. It’s simple and it does the job.</p>

<div class="language-scss highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">.ais-SearchBox-input</span> <span class="p">{</span>
  <span class="k">@extend</span> <span class="nc">.form-control</span><span class="p">;</span>
<span class="p">}</span>

<span class="nc">.ais-SearchBox-submit</span> <span class="p">{</span>
  <span class="nl">display</span><span class="p">:</span> <span class="nb">none</span><span class="p">;</span>
<span class="p">}</span>

<span class="nc">.ais-SearchBox-reset</span> <span class="p">{</span>
  <span class="nl">display</span><span class="p">:</span> <span class="nb">none</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="get-markdown-files-to-index">Get markdown files to index</h2>

<p>Now, it’s time to get all files that should be indexed and visible in the search results. I created a new <code class="language-plaintext highlighter-rouge">index-docs.js</code> file which will be executed after every successful build on CI/CD server. All my documentation pages are in <code class="language-plaintext highlighter-rouge">/docs/</code> directory. So, I needed to write a script for converting it into an array of objects to populate Algolia search index.</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span><span class="nx">globby</span><span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">globby</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="nx">fs</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">fs</span><span class="dl">"</span><span class="p">;</span>
<span class="k">import</span> <span class="nx">matter</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">gray-matter</span><span class="dl">"</span><span class="p">;</span>
<span class="k">import</span> <span class="nx">algoliasearch</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">algoliasearch</span><span class="dl">'</span><span class="p">;</span>

<span class="kd">const</span> <span class="nx">pages</span> <span class="o">=</span> <span class="k">await</span> <span class="nf">globby</span><span class="p">([</span>
  <span class="dl">'</span><span class="s1">docs/</span><span class="dl">'</span><span class="p">,</span>
<span class="p">]);</span>

<span class="kd">const</span> <span class="nx">objects</span> <span class="o">=</span> <span class="nx">pages</span><span class="p">.</span><span class="nf">map</span><span class="p">(</span><span class="nx">page</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">fileContents</span> <span class="o">=</span> <span class="nx">fs</span><span class="p">.</span><span class="nf">readFileSync</span><span class="p">(</span><span class="nx">page</span><span class="p">,</span> <span class="dl">'</span><span class="s1">utf8</span><span class="dl">'</span><span class="p">)</span>
  <span class="kd">const</span> <span class="p">{</span><span class="nx">data</span><span class="p">,</span> <span class="nx">content</span><span class="p">}</span> <span class="o">=</span> <span class="nf">matter</span><span class="p">(</span><span class="nx">fileContents</span><span class="p">)</span>
  <span class="kd">const</span> <span class="nx">path</span> <span class="o">=</span> <span class="nx">page</span><span class="p">.</span><span class="nf">replace</span><span class="p">(</span><span class="dl">'</span><span class="s1">.md</span><span class="dl">'</span><span class="p">,</span> <span class="dl">''</span><span class="p">);</span>
  <span class="kd">let</span> <span class="nx">slug</span> <span class="o">=</span> <span class="nx">path</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">docs/index</span><span class="dl">'</span> <span class="p">?</span> <span class="dl">'</span><span class="s1">docs</span><span class="dl">'</span> <span class="p">:</span> <span class="nx">path</span><span class="p">;</span>
  <span class="nx">slug</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">/</span><span class="dl">"</span> <span class="o">+</span> <span class="nx">slug</span> <span class="o">+</span> <span class="dl">"</span><span class="s2">/</span><span class="dl">"</span>
  <span class="k">return</span> <span class="p">{</span>
    <span class="nx">slug</span><span class="p">,</span>
    <span class="nx">content</span><span class="p">,</span>
    <span class="na">frontmatter</span><span class="p">:</span> <span class="p">{</span>
      <span class="p">...</span><span class="nx">data</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">})</span>

<span class="c1">//algolia update index code</span>
</code></pre></div></div>

<ul>
  <li>I’m getting all markdown files from <code class="language-plaintext highlighter-rouge">/docs/</code> directory with <code class="language-plaintext highlighter-rouge">globby</code>,</li>
  <li>reading content of the files with <code class="language-plaintext highlighter-rouge">fs</code>,</li>
  <li>parsing files with <code class="language-plaintext highlighter-rouge">gray-matter</code> for Markdown,</li>
  <li>and doing some magic tricks to convert file path to slugs which are used.</li>
</ul>

<p>In my case, slugs are the just file paths, where the file path looks like this <code class="language-plaintext highlighter-rouge">/docs/{category}/{title}.md</code>, for example:
<code class="language-plaintext highlighter-rouge">/docs/integrations/next-translate.md</code>, so slug will look like this: <code class="language-plaintext highlighter-rouge">/docs/integrations/next-translate/</code>.</p>

<p>In the end I’m getting an array of objects which looks like this:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="w">
    </span><span class="p">{</span><span class="w">
      </span><span class="nl">"slug"</span><span class="p">:</span><span class="w"> </span><span class="s2">"/docs/integrations/next-translate/"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"content"</span><span class="p">:</span><span class="w"> </span><span class="s2">"My article content for indexing"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"frontmatter"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"category"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Integrations"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"date"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2022-01-20"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"some-other-properties"</span><span class="p">:</span><span class="w"> </span><span class="s2">"properties"</span><span class="w">
      </span><span class="p">}</span><span class="w">
    </span><span class="p">}</span><span class="w">
</span><span class="p">]</span><span class="w">
</span></code></pre></div></div>

<h3 id="update-algolia-index">Update Algolia index</h3>

<p>Algolia provides a really simple and easy to use JavaScript client, so there is no magic here. We are acquiring our index, and saving objects which we created in the previous step.</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">client</span> <span class="o">=</span> <span class="nf">algoliasearch</span><span class="p">(</span>
  <span class="dl">'</span><span class="s1">APP_ID</span><span class="dl">'</span><span class="p">,</span>
  <span class="dl">'</span><span class="s1">ADMIN_API_KEY</span><span class="dl">'</span>
<span class="p">);</span>
<span class="kd">const</span> <span class="nx">index</span> <span class="o">=</span> <span class="nx">client</span><span class="p">.</span><span class="nf">initIndex</span><span class="p">(</span><span class="dl">"</span><span class="s2">simplelocalize-docs</span><span class="dl">"</span><span class="p">)</span>
<span class="nx">index</span><span class="p">.</span><span class="nf">saveObjects</span><span class="p">(</span><span class="nx">objects</span><span class="p">,</span> <span class="p">{</span>
  <span class="na">autoGenerateObjectIDIfNotExist</span><span class="p">:</span> <span class="kc">true</span>
<span class="p">});</span>
</code></pre></div></div>

<h3 id="include-index-update-in-packagejson">Include index update in package.json</h3>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"simplelocalize-docs"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"version"</span><span class="p">:</span><span class="w"> </span><span class="s2">"1.0.0"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"private"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
  </span><span class="nl">"scripts"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"dev"</span><span class="p">:</span><span class="w"> </span><span class="s2">"next dev"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"build"</span><span class="p">:</span><span class="w"> </span><span class="s2">"next build &amp;&amp; next export -o build/ &amp;&amp; i18n:download &amp;&amp; index:docs"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"index:docs"</span><span class="p">:</span><span class="w"> </span><span class="s2">"npx ts-node --skip-project index-docs.mjs"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"i18n:upload"</span><span class="p">:</span><span class="w"> </span><span class="s2">"simplelocalize upload"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"i18n:download"</span><span class="p">:</span><span class="w"> </span><span class="s2">"simplelocalize download"</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>You can execute the script manually by running <code class="language-plaintext highlighter-rouge">npm run index:docs</code>.</p>

<p><img src="/assets/2022-04-12/algolia-docs-search-index-overview.png" alt="algolia search index overview" /></p>

<p>And that’s it! See the search in action on SimpleLocalize <a href="https://simplelocalize.io/docs">documentation page</a>.</p>]]></content><author><name>GitHub User</name><email>your-email@domain.com</email></author><category term="frontend" /><summary type="html"><![CDATA[About implementing Algolia search in my documentation built with NextJS and Markdown.]]></summary></entry><entry><title type="html">Custom CodeBuild environment with Amazon ECR</title><link href="https://jpomykala.com/2018/11/10/setup-custom-codebuild-environment-with-amazon-ecr/" rel="alternate" type="text/html" title="Custom CodeBuild environment with Amazon ECR" /><published>2018-11-10T00:00:00+00:00</published><updated>2018-11-10T00:00:00+00:00</updated><id>https://jpomykala.com/2018/11/10/setup-custom-codebuild-environment-with-amazon-ecr</id><content type="html" xml:base="https://jpomykala.com/2018/11/10/setup-custom-codebuild-environment-with-amazon-ecr/"><![CDATA[<p>Since faster Java development by Oracle, every tool we use these days 
need to follow up for every Java release or just support only LTS versions like Amazon.</p>

<p>CodeBuild is AWS-hosted tool for building software applications and 3 months after stable 
Java 11 release still it does not support anything newer than Java 9. Time is up, now we will build 
our own build environment which will support Java 11 and Java 12 in future right after stable release.</p>

<h1 id="requirements">Requirements</h1>

<ul>
  <li>AWS CLI</li>
  <li>Docker</li>
  <li>Amazon ECR</li>
</ul>

<p><img src="/assets/2018-11-10/codebuild-environment.png" alt="codebuild environment" /></p>

<p>We are going to need nothing special for this. AWS CLI with AWS account obliviously. Installed docker on 
our local machine to build image, and some basic knowledge how docker works and how proper Dockerfile should looks like.
Basically we will create build environment which will be used during CodeBuild process using docker images.
After that we will push created image to Amazon ECR. ECR is container registry like docker hub but managed by Amazon.
Eventually we will use our image as build environment. 
Now it’s time to start “fixing” CodeBuild.</p>

<h1 id="build-environment">Build environment</h1>

<p>First of all we need to should create our own <code class="language-plaintext highlighter-rouge">Dockerfile</code> with environment. 
I used official <code class="language-plaintext highlighter-rouge">maven</code> image from Docker Hub, we can choose there maven and Java version.
[https://hub.docker.com/<em>/maven/](https://hub.docker.com/</em>/maven/)
I added few environment variables and own <code class="language-plaintext highlighter-rouge">CMD</code> to show <code class="language-plaintext highlighter-rouge">mvn --version</code> on start instead running just <code class="language-plaintext highlighter-rouge">mvn</code>, 
because this leads to failing CodeBuild process.</p>

<figure class="highlight"><pre><code class="language-dockerfile" data-lang="dockerfile"><span class="k">FROM</span><span class="s"> maven:3.6-jdk-11</span>
<span class="k">MAINTAINER</span><span class="s"> Jakub Pomykała &lt;hello@jpomykala.me&gt;</span>

<span class="k">ENV</span><span class="s"> PATH                   $PATH:$JAVA_HOME/bin</span>
<span class="k">ENV</span><span class="s"> JAVA_OPTS              "-server -XX:+UseG1GC -XX:+UseStringDeduplication -XX:+OptimizeStringConcat -Dsun.net.inetaddr.ttl=60"</span>
<span class="k">ENV</span><span class="s"> HEAP_SPACE             "-Xms512m -Xmx2g"</span>

<span class="k">CMD</span><span class="s"> ["mvn", "--version"]</span></code></pre></figure>

<h1 id="pushing-to-ecr">Pushing to ECR</h1>

<p>Now we can create new repository on <a href="https://eu-west-1.console.aws.amazon.com/ecr/repositories?region=eu-west-1">Amazon ECR</a> 
with name <code class="language-plaintext highlighter-rouge">java11-codebuild-environemnt</code>.</p>

<p>Sign in to ECR with</p>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="si">$(</span>aws ecr get-login <span class="nt">--no-include-email</span> <span class="nt">--</span>&lt;YOUR REGION&gt;<span class="si">)</span></code></pre></figure>

<p>Build docker image with:</p>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash">docker build <span class="nt">-t</span> java11-codebuild-environemnt .</code></pre></figure>

<p>and try to run it using:</p>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash">docker run java11-codebuild-environemnt:latest</code></pre></figure>

<p>and ensure that maven displays installed version.</p>

<p><img src="/assets/2018-11-10/docker-run-mvn.png" alt="mvn version" class="img-responsive" /></p>

<p>If everything work as expected, we can tag and push our image to ECR service.</p>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash">docker tag java11-codebuild-environment:latest &lt;YOUR_ECR_URL&gt;/java11-codebuild-environment:latest
docker push &lt;YOUR_ECR_URL&gt;/java11-codebuild-environment:latest</code></pre></figure>

<p><img src="/assets/2018-11-10/pushing-to-ecr.png" alt="pushing to ecr" /></p>

<p>Before we start using our container we need to setup proper access policy for CodeBuild service.
This need to be done using internal ECR policy management (not the standard AWS roles/policies!)
<code class="language-plaintext highlighter-rouge">Amazon ECR &gt; Repositories &gt; Permissions &gt; Edit policy JSON</code></p>

<figure class="highlight"><pre><code class="language-json" data-lang="json"><span class="p">{</span><span class="w">
  </span><span class="nl">"Version"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2012-10-17"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"Statement"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
    </span><span class="p">{</span><span class="w">
      </span><span class="nl">"Sid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"CodeBuildAccess"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"Effect"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Allow"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"Principal"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"Service"</span><span class="p">:</span><span class="w"> </span><span class="s2">"codebuild.amazonaws.com"</span><span class="w">
      </span><span class="p">},</span><span class="w">
      </span><span class="nl">"Action"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
        </span><span class="s2">"ecr:BatchCheckLayerAvailability"</span><span class="p">,</span><span class="w">
        </span><span class="s2">"ecr:BatchGetImage"</span><span class="p">,</span><span class="w">
        </span><span class="s2">"ecr:GetDownloadUrlForLayer"</span><span class="w">
      </span><span class="p">]</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">]</span><span class="w">
</span><span class="p">}</span></code></pre></figure>

<p><img src="/assets/2018-11-10/ecr-permissions.png" alt="pushing to ecr" /></p>

<p>We allow the CodeBuild service to get docker images, download them and check their availability.</p>

<h1 id="codebuild-configuration">CodeBuild configuration</h1>
<p>The last step is to configure CodeBuild environment, like bellow.</p>

<p><img src="/assets/2018-11-10/codebuild-environment.png" alt="pushing to ecr" /></p>

<p>And here we go! We can finally use the latest Java features and push it to the production right away!
Next version is scheduled to March 2019, so we will go through this steps again but on <code class="language-plaintext highlighter-rouge">Dockerfile</code> 
we will just change <code class="language-plaintext highlighter-rouge">maven:3.6-jdk-11</code> to <code class="language-plaintext highlighter-rouge">maven:3.6-jdk-12</code>. Even now we can do this because we can find docker images
with beta releases of Java 12.</p>]]></content><author><name>GitHub User</name><email>your-email@domain.com</email></author><category term="aws" /><category term="java" /><category term="docker" /><summary type="html"><![CDATA[Configuring CodeBuild environment with custom Docker image hosted on Amazon ECR.]]></summary></entry><entry><title type="html">Contact form on a static page with Amazon Lambda</title><link href="https://jpomykala.com/2018/08/04/serverless-contact-form-on-static-page/" rel="alternate" type="text/html" title="Contact form on a static page with Amazon Lambda" /><published>2018-08-04T00:00:00+00:00</published><updated>2018-08-04T00:00:00+00:00</updated><id>https://jpomykala.com/2018/08/04/serverless-contact-form-on-static-page</id><content type="html" xml:base="https://jpomykala.com/2018/08/04/serverless-contact-form-on-static-page/"><![CDATA[<p>If you are here, then I assume that you probably don’t know PHP nor Wordpress, and so you decided to build a static web page or use some static 
page generator like Jekyll, Grav or GatsbyJS. Right now your obvious option to provide ability to create a 
contact from is using a <a href="https://formspree.io">formspree.io</a> or something similar. I’m going to show you how to write your own contact form ‘backend’ in very short time.</p>

<p><img src="/assets/2018-08-04/lambda.png" alt="lambda" /></p>

<p>Please remember that this is not a step-by-step tutorial, I’m describing overall architecture with code (<em>copy-paste ready</em> certification). 
If you have any remarks, then <a href="https://twitter.com/jakub_pomykala">let me know</a> or <a href="https://github.com/jpomykala/jpomykala.github.io/blob/master/_posts/2018-08-04-serverless-contact-form-on-static-page.md">create a Github issue</a>. 😉</p>

<h3 id="requirements">Requirements</h3>

<ul>
  <li>minimal knowledge about <a href="https://aws.amazon.com">Amazon Web Services</a></li>
  <li>basic JavaScript skills</li>
</ul>

<h3 id="used-technologies">Used technologies</h3>

<ul>
  <li><a href="https://aws.amazon.com/lambda/">Amazon Lambda</a></li>
  <li><a href="https://aws.amazon.com/ses/">Amazon SES</a></li>
  <li><a href="https://aws.amazon.com/api-gateway/">Amazon API Gateway</a></li>
</ul>

<h3 id="build-environment">Build environment</h3>

<p>If you are familiar with used technologies the diagram should be pretty straightforward for you.</p>

<p><img src="/assets/2018-08-04/architecture-diagram.png" alt="aws-lambda-function" /></p>

<p>Static web page should be gathering data from contact form, validate and send them using XHR Fetch, jQuery or in 
other way to API Gateway by POST method. API Gateway will invoke Lambda function, and the Lambda function will invoke 
our JavaScript code where we parse POST request, do some custom logic and call <code class="language-plaintext highlighter-rouge">sendEmail(...)</code> on SES service.</p>

<h3 id="0-verify-your-e-mail-address">0. Verify your e-mail address</h3>

<p>SES e-mail address verification can be done <a href="https://eu-west-1.console.aws.amazon.com/ses/home?region=eu-west-1#verified-senders-email:">here (eu-west-1)</a></p>

<p><img src="/assets/2018-08-04/ses-verification.png" alt="ses-verification" /></p>

<h3 id="1-lambda-function">1. Lambda Function</h3>

<p>We will start with creating <strong>AWS Lambda function</strong> and choosing a NodeJS 8.1 environment.</p>

<p><a href="https://gist.github.com/jpomykala/a3548903e3454f7d65443053ec412b65">The full code can be found on GitHub Gist</a></p>

<h6 id="import-aws-sdk">import <code class="language-plaintext highlighter-rouge">aws-sdk</code></h6>

<p>Once we have prepared environment, then we can start to implement the function. A first important thing is that we need to import <code class="language-plaintext highlighter-rouge">aws-sdk</code> to use SES and other Amazon services.</p>

<figure class="highlight"><pre><code class="language-javascript" data-lang="javascript"><span class="kd">var</span> <span class="nx">aws</span> <span class="o">=</span> <span class="nf">require</span><span class="p">(</span><span class="dl">"</span><span class="s2">aws-sdk</span><span class="dl">"</span><span class="p">);</span></code></pre></figure>

<p><a href="https://docs.aws.amazon.com/sdk-for-javascript/index.html">Link to <code class="language-plaintext highlighter-rouge">aws-sdk</code> documentation</a></p>

<h6 id="aws-lambda-responses">AWS Lambda responses</h6>
<p>Success and error responses. This part is more important than you think. <strong>If we return wrong JSON from lambda function to API Gateway, 
the client (contact form in this case) will get HTTP 500 status.</strong> Code will be invoked and email sent anyway, but it’s just a good practise to 
follow the documentation and just don’t return 500 status codes intentionally.</p>

<figure class="highlight"><pre><code class="language-javascript" data-lang="javascript"><span class="kd">const</span> <span class="nx">successResponse</span> <span class="o">=</span> <span class="p">{</span>
    <span class="dl">"</span><span class="s2">statusCode</span><span class="dl">"</span><span class="p">:</span> <span class="mi">200</span><span class="p">,</span>
    <span class="dl">"</span><span class="s2">headers</span><span class="dl">"</span><span class="p">:</span> <span class="p">{</span>
        <span class="dl">"</span><span class="s2">Content-Type</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">application/json</span><span class="dl">"</span><span class="p">,</span>
    <span class="p">},</span>
    <span class="dl">"</span><span class="s2">body</span><span class="dl">"</span><span class="p">:</span> <span class="nx">JSON</span><span class="p">.</span><span class="nf">stringify</span><span class="p">({</span> <span class="na">message</span><span class="p">:</span> <span class="dl">"</span><span class="s2">:)</span><span class="dl">"</span> <span class="p">}),</span>
    <span class="dl">"</span><span class="s2">isBase64Encoded</span><span class="dl">"</span><span class="p">:</span> <span class="kc">false</span>
<span class="p">};</span>

<span class="kd">const</span> <span class="nx">errorResponse</span> <span class="o">=</span> <span class="p">{</span>
    <span class="dl">"</span><span class="s2">statusCode</span><span class="dl">"</span><span class="p">:</span> <span class="mi">500</span><span class="p">,</span>
    <span class="dl">"</span><span class="s2">headers</span><span class="dl">"</span><span class="p">:</span> <span class="p">{</span>
        <span class="dl">"</span><span class="s2">Content-Type</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">application/json</span><span class="dl">"</span><span class="p">,</span>
    <span class="p">},</span>
    <span class="dl">"</span><span class="s2">body</span><span class="dl">"</span><span class="p">:</span> <span class="nx">JSON</span><span class="p">.</span><span class="nf">stringify</span><span class="p">({</span> <span class="na">message</span><span class="p">:</span> <span class="dl">"</span><span class="s2">something bad happened, check logs</span><span class="dl">"</span> <span class="p">}),</span>
    <span class="dl">"</span><span class="s2">isBase64Encoded</span><span class="dl">"</span><span class="p">:</span> <span class="kc">false</span>
<span class="p">};</span></code></pre></figure>

<h6 id="check-is-domain-allowed">Check is domain allowed</h6>
<p>By using this function we can easily turn on and off e-mail sending from certain domains.</p>

<figure class="highlight"><pre><code class="language-javascript" data-lang="javascript"><span class="kd">const</span> <span class="nx">extractDomain</span> <span class="o">=</span> <span class="p">(</span><span class="nx">emailAddress</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">emailSplit</span> <span class="o">=</span> <span class="nx">emailAddress</span><span class="p">.</span><span class="nf">split</span><span class="p">(</span><span class="dl">'</span><span class="s1">@</span><span class="dl">'</span><span class="p">);</span>
    <span class="kd">const</span> <span class="nx">arraySize</span> <span class="o">=</span> <span class="nx">emailSplit</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span>
    <span class="k">if</span><span class="p">(</span><span class="nx">arraySize</span> <span class="o">&lt;</span> <span class="mi">2</span><span class="p">){</span>
        <span class="nx">console</span><span class="p">.</span><span class="nf">warn</span><span class="p">(</span><span class="dl">"</span><span class="s2">Domain not found for email:</span><span class="dl">"</span><span class="p">,</span> <span class="nx">emailAddress</span><span class="p">);</span>
        <span class="k">return</span> <span class="dl">""</span><span class="p">;</span>
    <span class="p">}</span>
    
    <span class="k">return</span> <span class="nx">emailSplit</span><span class="p">[</span><span class="nx">arraySize</span> <span class="o">-</span> <span class="mi">1</span><span class="p">];</span>
<span class="p">}</span>

 <span class="kd">const</span> <span class="nx">allowedDomains</span> <span class="o">=</span> <span class="p">[</span><span class="dl">'</span><span class="s1">simplelocalize.io</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">jpomykala.com</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">yourdomain.com</span><span class="dl">'</span><span class="p">];</span>
 <span class="kd">const</span> <span class="nx">isDomainAllowed</span> <span class="o">=</span> <span class="p">(</span><span class="nx">domain</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">allowedDomains</span><span class="p">.</span><span class="nf">includes</span><span class="p">(</span><span class="nx">domain</span><span class="p">);</span></code></pre></figure>

<h6 id="e-mail-message">E-mail message</h6>

<p>This is a true strength of this solution. We are passing whole Javascript object from contact form to e-mail formatted
with <code class="language-plaintext highlighter-rouge">&lt;pre&gt;&lt;/pre&gt;</code> tags and <code class="language-plaintext highlighter-rouge">JSON.stringify</code>.</p>

<figure class="highlight"><pre><code class="language-javascript" data-lang="javascript"><span class="kd">const</span> <span class="nx">getEmailMessage</span> <span class="o">=</span> <span class="p">(</span><span class="nx">request</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="k">return</span> <span class="p">{</span>
        <span class="na">Body</span><span class="p">:</span> <span class="p">{</span>
            <span class="na">Html</span><span class="p">:</span> <span class="p">{</span>
                <span class="na">Charset</span><span class="p">:</span> <span class="dl">"</span><span class="s2">UTF-8</span><span class="dl">"</span><span class="p">,</span>
                <span class="na">Data</span><span class="p">:</span> <span class="s2">`
                    &lt;body&gt;
                    &lt;p&gt;</span><span class="p">${</span><span class="nx">request</span><span class="p">.</span><span class="nx">message</span><span class="p">}</span><span class="s2">&lt;/p&gt;
                    &lt;pre&gt;</span><span class="p">${</span><span class="nx">JSON</span><span class="p">.</span><span class="nf">stringify</span><span class="p">(</span><span class="nx">request</span><span class="p">,</span> <span class="kc">undefined</span><span class="p">,</span> <span class="mi">2</span><span class="p">)}</span><span class="s2">&lt;/pre&gt;
                    &lt;/body&gt;
                    `</span>
            <span class="p">}</span>
        <span class="p">},</span>
        <span class="na">Subject</span><span class="p">:</span> <span class="p">{</span>
            <span class="na">Charset</span><span class="p">:</span> <span class="dl">"</span><span class="s2">UTF-8</span><span class="dl">"</span><span class="p">,</span>
            <span class="na">Data</span><span class="p">:</span> <span class="s2">`New submission`</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span></code></pre></figure>

<h6 id="send-e-mail-with-ses-service">Send e-mail with SES service</h6>
<p>The most important part of this function is of course sending email by SES. Let’s create a JSON params object with a message, 
subject and all other <a href="https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/SES.html">SES options which can be found here.</a></p>

<figure class="highlight"><pre><code class="language-javascript" data-lang="javascript"> <span class="kd">const</span> <span class="nx">params</span> <span class="o">=</span> <span class="p">{</span>
        <span class="na">Destination</span><span class="p">:</span> <span class="p">{</span>
            <span class="na">ToAddresses</span><span class="p">:</span> <span class="p">[</span><span class="nx">sendToEmail</span><span class="p">]</span>
        <span class="p">},</span>
        <span class="na">Message</span><span class="p">:</span> <span class="nx">emailMessage</span><span class="p">,</span>
        <span class="na">Source</span><span class="p">:</span> <span class="s2">`</span><span class="p">${</span><span class="nx">request</span><span class="p">.</span><span class="nx">name</span> <span class="o">||</span> <span class="dl">"</span><span class="s2">Unknown</span><span class="dl">"</span><span class="p">}</span><span class="s2"> &lt;your_verified_email_in_ses@gmail.com&gt;`</span><span class="p">,</span>
        <span class="na">ReplyToAddresses</span><span class="p">:</span> <span class="p">[</span><span class="nx">request</span><span class="p">.</span><span class="nx">_replyTo</span><span class="p">]</span>
    <span class="p">};</span>


    <span class="kd">const</span> <span class="nx">sendPromise</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">aws</span><span class="p">.</span><span class="nc">SES</span><span class="p">()</span>
        <span class="p">.</span><span class="nf">sendEmail</span><span class="p">(</span><span class="nx">params</span><span class="p">)</span>
        <span class="p">.</span><span class="nf">promise</span><span class="p">();</span>

    <span class="k">await</span> <span class="nx">sendPromise</span>
        <span class="p">.</span><span class="nf">then</span><span class="p">(</span><span class="nx">data</span> <span class="o">=&gt;</span> <span class="p">{</span>
            <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="s2">`E-mail sent to </span><span class="p">${</span><span class="nx">sendToEmail</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
            <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="nx">successResponse</span><span class="p">);</span>
            <span class="nf">callback</span><span class="p">(</span><span class="kc">null</span><span class="p">,</span> <span class="nx">successResponse</span><span class="p">);</span>
        <span class="p">})</span>
        <span class="p">.</span><span class="k">catch</span><span class="p">(</span><span class="nx">err</span> <span class="o">=&gt;</span> <span class="p">{</span>
            <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">E-mail NOT sent</span><span class="dl">"</span><span class="p">,</span> <span class="nx">err</span><span class="p">);</span>
            <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="nx">errorResponse</span><span class="p">);</span>
            <span class="nf">callback</span><span class="p">(</span><span class="nx">errorResponse</span><span class="p">);</span>
        <span class="p">});</span></code></pre></figure>

<p>Now we can deploy our function and move to the API Gateway.</p>

<p><img src="/assets/2018-08-04/send-mail-fuction-aws.png" alt="aws-lambda-function" /></p>

<h3 id="2-set-up-api-gateway">2. Set up API Gateway</h3>
<p>Setting up API Gateway for Lambda functions, should be straightforward. There is no need to code anything, just click-and-play configuration.
In this case I set up my endpoint to receive any HTTP method. For working contact form you will need only <code class="language-plaintext highlighter-rouge">http/post</code> method.</p>

<p><img src="/assets/2018-08-04/api-gateway.png" alt="api-gateway" /></p>

<h6 id="things-to-remember">Things to remember</h6>
<ul>
  <li>Every time we change something on endpoint configuration we need deploy API again to see changes. Use: <code class="language-plaintext highlighter-rouge">Actions -&gt; Deploy API</code></li>
  <li>Remember about setting up CORS while using API Gateway. Use: <code class="language-plaintext highlighter-rouge">Actions -&gt; Enable CORS</code></li>
</ul>

<h3 id="3-contact-form-example">3. Contact form example</h3>

<h6 id="html-form">HTML form</h6>

<figure class="highlight"><pre><code class="language-html" data-lang="html"><span class="nt">&lt;form</span> <span class="na">action=</span><span class="s">"#"</span> <span class="na">id=</span><span class="s">"callbackForm"</span> <span class="na">class=</span><span class="s">"contact-form"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"form-group"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;label</span> <span class="na">for=</span><span class="s">"email"</span><span class="nt">&gt;</span>Email<span class="nt">&lt;/label&gt;</span>
        <span class="nt">&lt;input</span> <span class="na">type=</span><span class="s">"email"</span> <span class="na">required</span> <span class="na">id=</span><span class="s">"email"</span> <span class="na">class=</span><span class="s">"form-control"</span> <span class="na">placeholder=</span><span class="s">""</span> <span class="na">autocomplete=</span><span class="s">"email"</span> <span class="na">name=</span><span class="s">"email"</span> <span class="nt">/&gt;</span>
    <span class="nt">&lt;/div&gt;</span>
    <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"form-group"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;label</span> <span class="na">for=</span><span class="s">"name"</span><span class="nt">&gt;</span>Message<span class="nt">&lt;/label&gt;</span>
        <span class="nt">&lt;input</span> <span class="na">id=</span><span class="s">"message"</span> <span class="na">type=</span><span class="s">"text"</span> <span class="na">class=</span><span class="s">"form-control"</span> <span class="na">placeholder=</span><span class="s">""</span> <span class="na">name=</span><span class="s">"message"</span> <span class="nt">/&gt;</span>
    <span class="nt">&lt;/div&gt;</span>
    <span class="nt">&lt;button</span> <span class="na">type=</span><span class="s">"submit"</span> <span class="na">id=</span><span class="s">"sendMessageButton"</span> <span class="na">class=</span><span class="s">"btn btn-primary btn-block"</span><span class="nt">&gt;</span>
        Send message
    <span class="nt">&lt;/button&gt;</span>
<span class="nt">&lt;/form&gt;</span></code></pre></figure>

<h6 id="javascript-to-submit-the-form">JavaScript to submit the form</h6>

<figure class="highlight"><pre><code class="language-html" data-lang="html"><span class="nt">&lt;script&gt;</span>
        <span class="nf">$</span><span class="p">(</span><span class="dl">"</span><span class="s2">#callbackForm</span><span class="dl">"</span><span class="p">).</span><span class="nf">submit</span><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span>
            <span class="nx">e</span><span class="p">.</span><span class="nf">preventDefault</span><span class="p">();</span>
            <span class="kd">var</span> <span class="nx">replyTo</span> <span class="o">=</span> <span class="nf">$</span><span class="p">(</span><span class="dl">"</span><span class="s2">#email</span><span class="dl">"</span><span class="p">);</span>
            <span class="kd">var</span> <span class="nx">message</span> <span class="o">=</span> <span class="nf">$</span><span class="p">(</span><span class="dl">"</span><span class="s2">#message</span><span class="dl">"</span><span class="p">);</span>
            <span class="kd">var</span> <span class="nx">data</span> <span class="o">=</span> <span class="p">{</span>
                <span class="dl">"</span><span class="s2">_sendTo</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">&lt;your_email&gt;</span><span class="dl">"</span><span class="p">,</span>
                <span class="dl">"</span><span class="s2">_replyTo</span><span class="dl">"</span><span class="p">:</span> <span class="nx">replyTo</span><span class="p">.</span><span class="nf">val</span><span class="p">(),</span>
                <span class="dl">"</span><span class="s2">message</span><span class="dl">"</span><span class="p">:</span> <span class="nx">message</span><span class="p">.</span><span class="nf">val</span><span class="p">()</span>
            <span class="p">};</span>
            <span class="kd">var</span> <span class="nx">url</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">&lt;API_GATEWAY_URL&gt;</span><span class="dl">"</span><span class="p">;</span>
            <span class="nx">$</span><span class="p">.</span><span class="nf">ajax</span><span class="p">({</span>
                <span class="na">url</span><span class="p">:</span> <span class="nx">url</span><span class="p">,</span>
                <span class="na">type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">POST</span><span class="dl">'</span><span class="p">,</span>
                <span class="na">crossDomain</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
                <span class="na">data</span><span class="p">:</span> <span class="nx">JSON</span><span class="p">.</span><span class="nf">stringify</span><span class="p">(</span><span class="nx">data</span><span class="p">),</span>
                <span class="na">dataType</span><span class="p">:</span> <span class="dl">'</span><span class="s1">json</span><span class="dl">'</span><span class="p">,</span>
                <span class="na">contentType</span><span class="p">:</span> <span class="dl">"</span><span class="s2">application/json</span><span class="dl">"</span>
            <span class="p">});</span>
        <span class="p">});</span>
<span class="nt">&lt;/script&gt;</span></code></pre></figure>

<h3 id="conclusion">Conclusion</h3>

<p>We can scale this technique to multiple web pages with ease, but this <strong>solution in current form has few downsides.</strong>
For now the only one protection against DDoS or some similar attack is <strong>rate limiter included in Lambda function</strong>.
Right now there is no bot protection, no captcha or something like that. We can add Google re-captcha on the contact 
form and setup rate limiting on both API Gateway and Lambda function, to avoid unnecessary costs.</p>

<h3 id="update-2021-02-23">Update 2021-02-23</h3>
<p>So far I didn’t have any issues with this solution. I’ve never received any SPAM or message from a bot. 😄</p>]]></content><author><name>GitHub User</name><email>your-email@domain.com</email></author><category term="aws" /><category term="serverless" /><summary type="html"><![CDATA[How to create a contact form on a static page with Amazon Lambda Function.]]></summary></entry><entry><title type="html">Authenticated websockets in Spring Boot and ReactJS</title><link href="https://jpomykala.com/2017/12/19/spring-react-websockets-with-auth/" rel="alternate" type="text/html" title="Authenticated websockets in Spring Boot and ReactJS" /><published>2017-12-19T00:00:00+00:00</published><updated>2017-12-19T00:00:00+00:00</updated><id>https://jpomykala.com/2017/12/19/spring-react-websockets-with-auth</id><content type="html" xml:base="https://jpomykala.com/2017/12/19/spring-react-websockets-with-auth/"><![CDATA[<p>Websockets are an easy way to update data on clients side without making request to server where there is no new data. 
It gives “wow effect” for clients and lower server costs for you.</p>

<p><img src="/assets/2017-12-19/spring-react.jpg" alt="spring boot react websocket connection" /></p>

<h2 id="server-side---spring-framework">Server side - Spring Framework</h2>
<p>We will start from adding proper dependency in <code class="language-plaintext highlighter-rouge">pom.xml</code> on backend side. In my case it the latest stable version was <code class="language-plaintext highlighter-rouge">2.0.2.RELEASE</code>.</p>

<figure class="highlight"><pre><code class="language-xml" data-lang="xml"><span class="nt">&lt;dependency&gt;</span>
    <span class="nt">&lt;groupId&gt;</span>org.springframework.boot<span class="nt">&lt;/groupId&gt;</span>
    <span class="nt">&lt;artifactId&gt;</span>spring-boot-starter-websocket<span class="nt">&lt;/artifactId&gt;</span>
<span class="nt">&lt;/dependency&gt;</span></code></pre></figure>

<p>Basic websockets configuration in Spring is easy as copy-paste configuration files and handle connection on client side.
Create new configuration class annotated with <code class="language-plaintext highlighter-rouge">@Configuration</code> and <code class="language-plaintext highlighter-rouge">@EnableWebSocketMessageBroker</code> and extend it with 
<code class="language-plaintext highlighter-rouge">AbstractWebSocketMessageBrokerConfigurer</code>.</p>

<figure class="highlight"><pre><code class="language-java" data-lang="java"><span class="nd">@Autowired</span>
<span class="kd">private</span> <span class="nc">ThreadPoolTaskScheduler</span> <span class="n">threadPoolTaskScheduler</span><span class="o">;</span>

<span class="nd">@Override</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">configureMessageBroker</span><span class="o">(</span><span class="nc">MessageBrokerRegistry</span> <span class="n">config</span><span class="o">)</span> <span class="o">{</span>
  <span class="n">config</span>
      <span class="o">.</span><span class="na">enableSimpleBroker</span><span class="o">(</span><span class="s">"/queue"</span><span class="o">)</span>
      <span class="o">.</span><span class="na">setTaskScheduler</span><span class="o">(</span><span class="n">threadPoolTaskScheduler</span><span class="o">);</span>
<span class="o">}</span>

<span class="nd">@Override</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">registerStompEndpoints</span><span class="o">(</span><span class="nc">StompEndpointRegistry</span> <span class="n">registry</span><span class="o">)</span> <span class="o">{</span>
  <span class="n">registry</span>
      <span class="o">.</span><span class="na">addEndpoint</span><span class="o">(</span><span class="s">"/ws"</span><span class="o">)</span>
      <span class="o">.</span><span class="na">setAllowedOrigins</span><span class="o">(</span><span class="no">ALLOWED_ORIGINS</span><span class="o">)</span>
      <span class="o">.</span><span class="na">withSockJS</span><span class="o">()</span>
      <span class="o">.</span><span class="na">setTaskScheduler</span><span class="o">(</span><span class="n">threadPoolTaskScheduler</span><span class="o">);</span>
<span class="o">}</span></code></pre></figure>

<p>Remember to provide <code class="language-plaintext highlighter-rouge">TaskScheduler</code> which is required to sending messages. 
In above example I also configured CORS by using list of allowed origins from <code class="language-plaintext highlighter-rouge">*.yml</code> configuration.</p>

<h2 id="support-for-websocket-authentication">Support for websocket authentication</h2>
<p>Unfortunately as far as I know Spring websockets does not support authentication, so we need to implement it on our own. 
I came up with very simple idea, I’m authenticating user on <code class="language-plaintext highlighter-rouge">SessionSubscribeEvent</code>.</p>

<figure class="highlight"><pre><code class="language-java" data-lang="java"><span class="nd">@EventListener</span><span class="o">(</span><span class="nc">SessionSubscribeEvent</span><span class="o">.</span><span class="na">class</span><span class="o">)</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">onWebSocketSessionsConnected</span><span class="o">(</span><span class="nc">SessionSubscribeEvent</span> <span class="n">event</span><span class="o">)</span> <span class="o">{</span>
  <span class="nc">Message</span><span class="o">&lt;</span><span class="kt">byte</span><span class="o">[]&gt;</span> <span class="n">eventMessage</span> <span class="o">=</span> <span class="n">event</span><span class="o">.</span><span class="na">getMessage</span><span class="o">();</span>
  <span class="nc">String</span> <span class="n">token</span> <span class="o">=</span> <span class="n">getAuthorizationToken</span><span class="o">(</span><span class="n">eventMessage</span><span class="o">);</span>
  <span class="c1">// Bearer xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx</span>
  <span class="c1">// do whatever you need with user, throw exception if user should not be connected</span>
  <span class="c1">// ...</span>
<span class="o">}</span>

<span class="kd">private</span> <span class="nc">String</span> <span class="nf">getAuthorizationToken</span><span class="o">(</span><span class="nc">Message</span><span class="o">&lt;</span><span class="kt">byte</span><span class="o">[]&gt;</span> <span class="n">message</span><span class="o">)</span> <span class="o">{</span>
  <span class="nc">StompHeaderAccessor</span> <span class="n">headerAccessor</span> <span class="o">=</span> <span class="nc">StompHeaderAccessor</span><span class="o">.</span><span class="na">wrap</span><span class="o">(</span><span class="n">message</span><span class="o">);</span>

  <span class="nc">List</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;</span> <span class="n">authorization</span> <span class="o">=</span> <span class="nc">Optional</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="n">headerAccessor</span><span class="o">)</span>
      <span class="o">.</span><span class="na">map</span><span class="o">(</span><span class="err">$</span> <span class="o">-&gt;</span> <span class="err">$</span><span class="o">.</span><span class="na">getNativeHeader</span><span class="o">(</span><span class="nc">WebSocketHttpHeaders</span><span class="o">.</span><span class="na">AUTHORIZATION</span><span class="o">))</span>
      <span class="o">.</span><span class="na">orElse</span><span class="o">(</span><span class="nc">Collections</span><span class="o">.</span><span class="na">emptyList</span><span class="o">());</span>
  <span class="c1">// if header does not exists returns null instead empty list :/</span>

  <span class="k">return</span> <span class="n">authorization</span><span class="o">.</span><span class="na">stream</span><span class="o">()</span>
      <span class="o">.</span><span class="na">findFirst</span><span class="o">()</span>
      <span class="o">.</span><span class="na">orElseThrow</span><span class="o">(()</span> <span class="o">-&gt;</span> <span class="k">new</span> <span class="nc">IllegalArgumentException</span><span class="o">(</span><span class="s">"Missing access token in Stomp message headers"</span><span class="o">));</span>
<span class="o">}</span></code></pre></figure>

<p>Now we are ready to send data to connected clients! In my application I’m using application events
to send updates with ease to connected companies from any place in code. 
In my case I’m sending update to dashboard page on every new transaction for company.
Every user subscribe his company message channel and get update on every transaction or alert if occurred.</p>

<figure class="highlight"><pre><code class="language-java" data-lang="java"><span class="nd">@Autowired</span>
<span class="kd">private</span> <span class="nc">SimpMessagingTemplate</span> <span class="n">webSocket</span><span class="o">;</span>
	
<span class="nd">@EventListener</span><span class="o">(</span><span class="nc">UpdateDashboardRequestEvent</span><span class="o">.</span><span class="na">class</span><span class="o">)</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">onClientUpdate</span><span class="o">(</span><span class="nc">UpdateDashboardRequestEvent</span> <span class="n">request</span><span class="o">)</span> <span class="o">{</span>
  <span class="nc">String</span> <span class="n">companyName</span> <span class="o">=</span> <span class="n">request</span><span class="o">.</span><span class="na">getCompanyName</span><span class="o">();</span>
  <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"Sending dashboard update to {}"</span><span class="o">,</span> <span class="n">companyName</span><span class="o">);</span>
  <span class="nc">List</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;</span> <span class="n">connectedCompanies</span> <span class="o">=</span> <span class="n">connectedUsersService</span><span class="o">.</span><span class="na">getConnectedCompanies</span><span class="o">();</span>
  <span class="c1">// when user/companies successfully connects to server I add him to list of connected users/companies</span>
  
  <span class="kt">boolean</span> <span class="n">isConnected</span> <span class="o">=</span> <span class="n">connectedCompanies</span><span class="o">.</span><span class="na">contains</span><span class="o">(</span><span class="n">companyName</span><span class="o">);</span>

  <span class="k">if</span> <span class="o">(!</span><span class="n">isConnected</span><span class="o">)</span> <span class="o">{</span>
    <span class="n">log</span><span class="o">.</span><span class="na">warn</span><span class="o">(</span><span class="s">"Company is not on connection list"</span><span class="o">);</span>
    <span class="k">return</span><span class="o">;</span>
  <span class="o">}</span>
  <span class="nc">String</span> <span class="n">companyDestinationUrl</span> <span class="o">=</span> <span class="s">"/queue/"</span> <span class="o">+</span> <span class="n">updateForCompany</span> <span class="o">+</span> <span class="s">"/company"</span><span class="o">;</span>
  <span class="nc">Response</span> <span class="n">response</span> <span class="o">=</span> <span class="o">...</span> 
  <span class="c1">//response will be converted using message converters like on regular class annotated with @RestController</span>
  <span class="n">webSocket</span><span class="o">.</span><span class="na">convertAndSend</span><span class="o">(</span><span class="n">companyDestinationUrl</span><span class="o">,</span> <span class="n">response</span><span class="o">);</span>
<span class="o">}</span></code></pre></figure>

<h2 id="client-side---reactjs">Client side - ReactJS</h2>

<p>On client side I’m using two additional dependencies, one for SockJS and second for webstomp of course.</p>

<p><a href="https://github.com/sockjs/sockjs-client">sockjs-client</a> - official SockJS client</p>

<p><a href="https://github.com/JSteunou/webstomp-client">webstomp-client</a> - community developed webstomp client</p>

<figure class="highlight"><pre><code class="language-javascript" data-lang="javascript"><span class="k">import</span> <span class="nx">React</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">react</span><span class="dl">"</span><span class="p">;</span>
<span class="k">import</span> <span class="nx">SockJS</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">sockjs-client</span><span class="dl">"</span><span class="p">;</span>
<span class="k">import</span> <span class="nx">webstomp</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">webstomp-client</span><span class="dl">"</span><span class="p">;</span>

<span class="c1">// types of Props &amp; State</span>

<span class="kd">class</span> <span class="nc">Dashboard</span> <span class="kd">extends</span> <span class="nc">React</span><span class="p">.</span><span class="nx">Component</span><span class="o">&lt;</span><span class="nx">Props</span><span class="p">,</span> <span class="nx">State</span><span class="o">&gt;</span> <span class="p">{</span>
  <span class="nx">subscribeUpdates</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="p">{</span><span class="nx">companyName</span><span class="p">}</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">props</span><span class="p">;</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">topicSubscription</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">client</span><span class="p">.</span><span class="nf">subscribe</span><span class="p">(</span>
        <span class="s2">`/queue/</span><span class="p">${</span><span class="nx">companyName</span><span class="p">}</span><span class="s2">/company`</span><span class="p">,</span> <span class="k">this</span><span class="p">.</span><span class="nx">onUpdate</span><span class="p">,</span>
        <span class="p">{</span><span class="na">Authorization</span><span class="p">:</span> <span class="s2">`Bearer </span><span class="p">${</span><span class="nx">localStorage</span><span class="p">.</span><span class="nf">getItem</span><span class="p">(</span><span class="nx">ACCESS_TOKEN_KEY</span><span class="p">)}</span><span class="s2">`</span><span class="p">},</span>
    <span class="p">);</span>
  <span class="p">};</span>

  <span class="nx">connectSocket</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">token</span> <span class="o">=</span> <span class="nx">localStorage</span><span class="p">.</span><span class="nf">getItem</span><span class="p">(</span><span class="nx">ACCESS_TOKEN_KEY</span><span class="p">);</span>
    <span class="c1">//pure accessToken without 'Bearer' part</span>
    <span class="kd">const</span> <span class="nx">sockjs</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">SockJS</span><span class="p">(</span>
        <span class="s2">`</span><span class="p">${</span><span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">REACT_APP_API_URL</span><span class="p">}</span><span class="s2">/ws`</span><span class="p">,</span> <span class="kc">null</span><span class="p">,</span>
        <span class="p">{</span> <span class="na">headers</span><span class="p">:</span> <span class="p">{</span><span class="na">Authorization</span><span class="p">:</span> <span class="s2">`Bearer </span><span class="p">${</span><span class="nx">token</span><span class="p">}</span><span class="s2">`</span> <span class="p">}},</span>
    <span class="p">);</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">client</span> <span class="o">=</span> <span class="nx">webstomp</span><span class="p">.</span><span class="nf">over</span><span class="p">(</span><span class="nx">sockjs</span><span class="p">,</span> <span class="p">{</span> <span class="na">debug</span><span class="p">:</span> <span class="kc">false</span> <span class="p">});</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">client</span><span class="p">.</span><span class="nf">connect</span><span class="p">({</span><span class="na">Authorization</span><span class="p">:</span> <span class="s2">`Bearer </span><span class="p">${</span><span class="nx">token</span><span class="p">}</span><span class="s2">`</span><span class="p">},</span> <span class="k">this</span><span class="p">.</span><span class="nx">subscribeUpdates</span><span class="p">);</span>
  <span class="p">};</span>

  <span class="nf">componentDidMount</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">this</span><span class="p">.</span><span class="nf">connectSocket</span><span class="p">();</span>
  <span class="p">}</span>
  
  <span class="nx">onUpdate</span> <span class="o">=</span> <span class="p">({</span><span class="nx">body</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">{}</span><span class="dl">"</span><span class="p">})</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">message</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nf">parse</span><span class="p">(</span><span class="nx">body</span><span class="p">)</span>
    <span class="c1">// do whatever you want, eg. setState to update view</span>
  <span class="p">}</span></code></pre></figure>

<h2 id="conclusions">Conclusions</h2>

<p>Done! 🌱 Below is an example how it looks like in my application. 
In terminal, we see logs from the server for test demo company with a vending machines. 
Every time machine sold a product data are sent to our server, and then transaction is 
validated and eventually inserted to database. In the end I’m sending event 
<code class="language-plaintext highlighter-rouge">UpdateDashboardRequestEvent</code> to update the dashboard with WebSockets.</p>]]></content><author><name>GitHub User</name><email>your-email@domain.com</email></author><category term="java" /><category term="spring boot" /><category term="frontend" /><category term="websocket" /><summary type="html"><![CDATA[Authenticating websocket connection with Spring Boot and ReactJS.]]></summary></entry></feed>