<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:media="http://search.yahoo.com/mrss/"><channel><title>Andrew Usher&apos;s Blog</title><description>Thoughts on web development, JavaScript, TypeScript, and software engineering</description><link>https://andrewusher.dev/</link><language>en-us</language><item><title>Handling Multiple Promises: Promise.all vs Promise.allSettled</title><link>https://andrewusher.dev/blog/promise-all-vs-promise-allsettled/</link><guid isPermaLink="true">https://andrewusher.dev/blog/promise-all-vs-promise-allsettled/</guid><description>When you&apos;re pulling data from multiple sources at once, JavaScript gives you two tools:  and . Pick the wrong one and things get ugly fast—one failed...</description><pubDate>Wed, 21 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;When you&amp;#39;re pulling data from multiple sources at once, JavaScript gives you two tools: &lt;code&gt;Promise.all&lt;/code&gt; and &lt;code&gt;Promise.allSettled&lt;/code&gt;. Pick the wrong one and things get ugly fast—one failed request takes down everything.&lt;/p&gt;
&lt;p&gt;I&amp;#39;ll break down when to use each.&lt;/p&gt;
&lt;h2&gt;Promise.all: Everything or Nothing&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Promise.all&lt;/code&gt; stops dead the moment anything fails. If you&amp;#39;re fetching three endpoints and one returns a 500 error, you get nothing back. Zilch.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const fetchUserData = async () =&amp;gt; {
  const endpoints = [
    &amp;#39;https://api.example.com/users/1&amp;#39;,
    &amp;#39;https://api.example.com/users/2&amp;#39;,
    &amp;#39;https://api.example.com/users/3&amp;#39;,
  ]

  try {
    const responses = await Promise.all(
      endpoints.map((url) =&amp;gt; fetch(url).then((res) =&amp;gt; res.json()))
    )
    console.log(&amp;#39;All data fetched:&amp;#39;, responses)
    return responses
  } catch (error) {
    console.error(&amp;#39;One request failed:&amp;#39;, error)
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This makes sense when your operations depend on each other. Picture a form submission where validations chain together, or a dashboard where missing user data means you can&amp;#39;t render anything at all. If one piece missing breaks the whole flow, &lt;code&gt;Promise.all&lt;/code&gt; is your friend.&lt;/p&gt;
&lt;h2&gt;Promise.allSettled: Let Everything Finish&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;allSettled&lt;/code&gt; doesn&amp;#39;t care about failure—it waits for all promises to finish and reports back on each one individually.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const fetchUserProfiles = async () =&amp;gt; {
  const requests = [
    fetch(&amp;#39;/api/user/1&amp;#39;),
    fetch(&amp;#39;/api/user/2&amp;#39;),
    fetch(&amp;#39;/api/avatar/3&amp;#39;),
  ]

  const results = await Promise.allSettled(requests)

  results.forEach((result, index) =&amp;gt; {
    if (result.status === &amp;#39;fulfilled&amp;#39;) {
      console.log(`Request ${index} succeeded:`, result.value)
    } else {
      console.log(`Request ${index} failed:`, result.reason)
    }
  })

  return results
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each result tells you whether it succeeded (&lt;code&gt;status: &amp;quot;fulfilled&amp;quot;&lt;/code&gt;) or failed (&lt;code&gt;status: &amp;quot;rejected&amp;quot;&lt;/code&gt;). This is perfect for independent operations where partial data still has value.&lt;/p&gt;
&lt;h2&gt;When to Use Which&lt;/h2&gt;
&lt;p&gt;Go with &lt;code&gt;Promise.all&lt;/code&gt; when partial results are useless. If you&amp;#39;re loading critical data and can&amp;#39;t proceed without all of it, this is the right call.&lt;/p&gt;
&lt;p&gt;Go with &lt;code&gt;Promise.allSettled&lt;/code&gt; when operations are independent and some data beats no data. Analytics, optional metadata, secondary UI elements—these don&amp;#39;t need to bring down the whole page if one source is down.&lt;/p&gt;
&lt;p&gt;Ask yourself: &amp;quot;If half these requests fail, is the result still useful?&amp;quot; Answer no? &lt;code&gt;Promise.all&lt;/code&gt;. Answer yes? &lt;code&gt;allSettled&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Quick Win: Extract Just the Successes&lt;/h2&gt;
&lt;p&gt;After &lt;code&gt;allSettled&lt;/code&gt;, you often want the working values only:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const results = await Promise.allSettled(promises)

const successfulValues = results
  .filter((result) =&amp;gt; result.status === &amp;#39;fulfilled&amp;#39;)
  .map((result) =&amp;gt; result.value)

const errors = results
  .filter((result) =&amp;gt; result.status === &amp;#39;rejected&amp;#39;)
  .map((result) =&amp;gt; result.reason)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Clean data, full error picture, zero surprises.&lt;/p&gt;
&lt;h2&gt;The Bottom Line&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Promise.all&lt;/code&gt; is all-or-nothing. &lt;code&gt;Promise.allSettled&lt;/code&gt; plays the long game. The right choice keeps your app running when things go wrong—which they always do.&lt;/p&gt;
</content:encoded><media:content type="image/png" width="1200" height="630" medium="image" url="https://andrewusher.dev/open-graph/promise-all-vs-promise-allsettled.png"/><category>javascript</category><category>async</category><category>promises</category></item><item><title>Cookie Store API: The Modern Way to Handle Cookies</title><link>https://andrewusher.dev/blog/cookie-store-api-the-modern-way-to-handle-cookies/</link><guid isPermaLink="true">https://andrewusher.dev/blog/cookie-store-api-the-modern-way-to-handle-cookies/</guid><description>Introduction The Cookie Store API makes all of this possible. It&apos;s a modern, Promise-based alternative to the legacy  API that solves many of the prob...</description><pubDate>Sun, 11 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;The &lt;strong&gt;Cookie Store API&lt;/strong&gt; makes all of this possible. It&amp;#39;s a modern, Promise-based alternative to the legacy &lt;code&gt;document.cookie&lt;/code&gt; API that solves many of the problems developers have struggled with for years. In this article, we&amp;#39;ll explore Cookie Store API from the ground up. We&amp;#39;ll start with basics, progressively build up to advanced patterns, and create interactive demos you can play with. By the end, you&amp;#39;ll have all the tools you need to modernize your cookie handling.&lt;/p&gt;
&lt;h2&gt;The Problem with &lt;code&gt;document.cookie&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;Before diving into the Cookie Store API, let&amp;#39;s understand why we need a new API. The traditional &lt;code&gt;document.cookie&lt;/code&gt; approach has several limitations:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. Synchronous and Blocking&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// This blocks the main thread!
const cookies = document.cookie;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When you read &lt;code&gt;document.cookie&lt;/code&gt;, the browser must serialize all cookies for the current domain into a single string. This is slow and blocks your JavaScript execution.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. String Parsing is Error-Prone&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// You have to manually parse this mess:
&amp;quot;session=abc123; user=john; theme=dark; analytics=enabled&amp;quot;

// Parse logic is tedious:
const cookies = document.cookie.split(&amp;#39;;&amp;#39;).reduce((acc, cookie) =&amp;gt; {
  const [name, value] = cookie.trim().split(&amp;#39;=&amp;#39;);
  acc[name] = value;
  return acc;
}, {});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This parsing code is duplicated across projects and prone to bugs.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. No Service Worker Access&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Service workers can&amp;#39;t access &lt;code&gt;document.cookie&lt;/code&gt; because they don&amp;#39;t have a DOM. This makes it impossible to read cookies in:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Offline-first apps&lt;/li&gt;
&lt;li&gt;Background sync scenarios&lt;/li&gt;
&lt;li&gt;Push notification handlers&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;4. No Type Safety&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Returns a string - what is this?
const value = document.cookie.split(&amp;#39;=&amp;#39;)[1];
// What if the cookie has an equals sign in its value?
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No validation, no structure, just a string.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;5. No Change Notifications&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;You can&amp;#39;t listen for cookie changes. You have to poll (inefficient) or reload the page to see updates.&lt;/p&gt;
&lt;p&gt;The Cookie Store API solves all of these problems.&lt;/p&gt;
&lt;h2&gt;Quick Start: Your First Cookie&lt;/h2&gt;
&lt;p&gt;The Cookie Store API is accessed via the &lt;code&gt;cookieStore&lt;/code&gt; object, available in both windows and service workers.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Basic Usage:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Get a cookie
const cookie = await cookieStore.get(&amp;#39;session&amp;#39;);
console.log(cookie); // { name: &amp;#39;session&amp;#39;, value: &amp;#39;abc123&amp;#39;, ... }

// Set a cookie
await cookieStore.set(&amp;#39;theme&amp;#39;, &amp;#39;dark&amp;#39;);

// Delete a cookie
await cookieStore.delete(&amp;#39;session&amp;#39;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That&amp;#39;s it! Three simple methods. But let&amp;#39;s try it yourself:&lt;/p&gt;
&lt;CookiePlayground client:only /&gt;

&lt;p&gt;Notice how much easier this is than &lt;code&gt;document.cookie&lt;/code&gt;? No parsing, no string manipulation, just clean, promise-based operations.&lt;/p&gt;
&lt;h2&gt;Cookie Properties Deep Dive&lt;/h2&gt;
&lt;p&gt;The real power of the Cookie Store API is in the options you can pass when setting cookies. Let&amp;#39;s explore each property:&lt;/p&gt;
&lt;h3&gt;expires vs maxAge&lt;/h3&gt;
&lt;p&gt;Two ways to control cookie lifetime:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Using expires (absolute timestamp)
await cookieStore.set({
  name: &amp;#39;session&amp;#39;,
  value: &amp;#39;abc123&amp;#39;,
  expires: Date.now() + 3600000 // 1 hour from now
});

// Session cookie (no expiration)
await cookieStore.set({
  name: &amp;#39;theme&amp;#39;,
  value: &amp;#39;dark&amp;#39;
  // No expires = session cookie, deleted when browser closes
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;domain&lt;/h3&gt;
&lt;p&gt;Controls which domains can receive the cookie:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Only this exact domain
await cookieStore.set({
  name: &amp;#39;cookie&amp;#39;,
  value: &amp;#39;value&amp;#39;,
  domain: &amp;#39;example.com&amp;#39;
});

// Subdomains too (note the leading dot)
await cookieStore.set({
  name: &amp;#39;cookie&amp;#39;,
  value: &amp;#39;value&amp;#39;,
  domain: &amp;#39;.example.com&amp;#39; // example.com, www.example.com, api.example.com
});

// Current domain only (default)
await cookieStore.set({
  name: &amp;#39;cookie&amp;#39;,
  value: &amp;#39;value&amp;#39;
  // No domain = only set for current domain
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;path&lt;/h3&gt;
&lt;p&gt;Restricts cookie to a specific path:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Only accessible from /dashboard and subpaths
await cookieStore.set({
  name: &amp;#39;preferences&amp;#39;,
  value: &amp;#39;value&amp;#39;,
  path: &amp;#39;/dashboard&amp;#39;
});

// Site-wide (default)
await cookieStore.set({
  name: &amp;#39;session&amp;#39;,
  value: &amp;#39;value&amp;#39;,
  path: &amp;#39;/&amp;#39;
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Secure&lt;/h3&gt;
&lt;p&gt;Only send over HTTPS connections:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;await cookieStore.set({
  name: &amp;#39;session&amp;#39;,
  value: &amp;#39;abc123&amp;#39;,
  secure: true // Only sent over HTTPS
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Always set &lt;code&gt;secure: true&lt;/code&gt; for authentication cookies and sensitive data.&lt;/p&gt;
&lt;h3&gt;SameSite&lt;/h3&gt;
&lt;p&gt;Controls when cookies are sent with cross-site requests:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Strict: Only same-site requests
await cookieStore.set({
  name: &amp;#39;token&amp;#39;,
  value: &amp;#39;abc123&amp;#39;,
  sameSite: &amp;#39;Strict&amp;#39;
});

// Lax: Allows some cross-site (navigation)
await cookieStore.set({
  name: &amp;#39;token&amp;#39;,
  value: &amp;#39;abc123&amp;#39;,
  sameSite: &amp;#39;Lax&amp;#39; // Default for modern browsers
});

// None: Allows all cross-site (requires Secure)
await cookieStore.set({
  name: &amp;#39;token&amp;#39;,
  value: &amp;#39;abc123&amp;#39;,
  sameSite: &amp;#39;None&amp;#39;,
  secure: true // Required with SameSite=None
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Partitioned&lt;/h3&gt;
&lt;p&gt;Creates a privacy-preserving partitioned cookie (more on this later):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;await cookieStore.set({
  name: &amp;#39;analytics&amp;#39;,
  value: &amp;#39;123&amp;#39;,
  domain: &amp;#39;.tracker.com&amp;#39;,
  partitioned: true // Scoped to top-level site
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Try configuring all these properties in the interactive builder:&lt;/p&gt;
&lt;PropertyBuilder /&gt;

&lt;h2&gt;Getting Cookies&lt;/h2&gt;
&lt;p&gt;The Cookie Store API provides two methods for reading cookies:&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;get()&lt;/code&gt; - Single Cookie&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Get by name
const theme = await cookieStore.get(&amp;#39;theme&amp;#39;);
console.log(theme); // { name: &amp;#39;theme&amp;#39;, value: &amp;#39;dark&amp;#39;, ... }

// Get by options
const cookie = await cookieStore.get({
  name: &amp;#39;session&amp;#39;,
  domain: &amp;#39;example.com&amp;#39;,
  path: &amp;#39;/api&amp;#39;
});

// Returns null if not found
const missing = await cookieStore.get(&amp;#39;does-not-exist&amp;#39;);
console.log(missing); // null
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;getAll()&lt;/code&gt; - Multiple Cookies&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Get all cookies
const allCookies = await cookieStore.getAll();
console.log(allCookies); // [{ name: &amp;#39;a&amp;#39;, value: &amp;#39;1&amp;#39; }, { name: &amp;#39;b&amp;#39;, value: &amp;#39;2&amp;#39; }]

// Filter by name
const sessionCookies = await cookieStore.getAll({ name: &amp;#39;session&amp;#39; });

// Filter by path
const apiCookies = await cookieStore.getAll({ path: &amp;#39;/api&amp;#39; });

// Chain filters
const secureSessionCookies = await cookieStore.getAll({
  name: &amp;#39;session&amp;#39;,
  path: &amp;#39;/api&amp;#39;,
  secure: true
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Unlike &lt;code&gt;document.cookie&lt;/code&gt; (which returns a string), these methods return structured objects with all cookie properties.&lt;/p&gt;
&lt;h2&gt;Cookie Change Events&lt;/h2&gt;
&lt;p&gt;One of the most powerful features of the Cookie Store API is the ability to subscribe to cookie changes. This means you can react to cookies being added, modified, or deleted in real-time.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Basic Event Listener:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;cookieStore.addEventListener(&amp;#39;change&amp;#39;, (event) =&amp;gt; {
  console.log(&amp;#39;Cookies changed:&amp;#39;, event);

  // event.type: &amp;#39;changed&amp;#39; | &amp;#39;deleted&amp;#39;
  // event.changed: Array of cookies that were added or modified
  // event.deleted: Array of cookies that were deleted
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Real-World Example: Auto-Logout on Token Deletion:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;cookieStore.addEventListener(&amp;#39;change&amp;#39;, (event) =&amp;gt; {
  // Check if auth cookie was deleted
  const deletedSession = event.deleted.find(c =&amp;gt; c.name === &amp;#39;session&amp;#39;);

  if (deletedSession) {
    // Redirect to login page
    window.location.href = &amp;#39;/login&amp;#39;;
    // Clear app state
    logout();
  }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Sync State Across Tabs:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;cookieStore.addEventListener(&amp;#39;change&amp;#39;, async (event) =&amp;gt; {
  // Update theme when cookie changes
  const themeCookie = event.changed.find(c =&amp;gt; c.name === &amp;#39;theme&amp;#39;);

  if (themeCookie) {
    // Update UI in all tabs
    document.documentElement.setAttribute(&amp;#39;data-theme&amp;#39;, themeCookie.value);
    // Update local state
    setTheme(themeCookie.value);
  }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Try it out in the interactive demo:&lt;/p&gt;
&lt;ChangeEventDemo client:only /&gt;

&lt;h2&gt;Partitioned Cookies (CHIPS)&lt;/h2&gt;
&lt;p&gt;Cookies Having Independent Partitioned State (CHIPS) is a privacy-preserving feature that&amp;#39;s rapidly becoming essential for modern web development.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Problem: Third-Party Cookies&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Traditional third-party cookies can track users across sites:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;User visits &lt;code&gt;site-a.com&lt;/code&gt; → tracker.com cookie set&lt;/li&gt;
&lt;li&gt;User visits &lt;code&gt;site-b.com&lt;/code&gt; → Same tracker.com cookie read&lt;/li&gt;
&lt;li&gt;Tracker knows user visited both sites&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This is why browsers increasingly block third-party cookies (ITP, ETP, etc.).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Solution: Partitioned Cookies&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Partitioned cookies are scoped to the top-level site:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;User visits &lt;code&gt;site-a.com&lt;/code&gt; → tracker.com sets &lt;strong&gt;partitioned&lt;/strong&gt; cookie&lt;/li&gt;
&lt;li&gt;User visits &lt;code&gt;site-b.com&lt;/code&gt; → Different tracker.com cookie (different partition!)&lt;/li&gt;
&lt;li&gt;Each site gets its own tracker.com cookie → No cross-site tracking&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Legitimate Use Cases:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Partitioned cookies still enable legitimate third-party functionality:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Embedded login buttons&lt;/strong&gt; (Google, Facebook, Twitter OAuth)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Embedded payments&lt;/strong&gt; (Stripe, PayPal)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Embedded services&lt;/strong&gt; (maps, analytics)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each embedding site gets its own isolated cookie.&lt;/p&gt;
&lt;PartitionedCookieDemo /&gt;

&lt;h2&gt;Service Worker Integration&lt;/h2&gt;
&lt;p&gt;One of the biggest advantages of the Cookie Store API is that it works in service workers. This opens up powerful new patterns:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Background Sync with Cookies:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// In service worker
self.addEventListener(&amp;#39;sync&amp;#39;, async (event) =&amp;gt; {
  const session = await cookieStore.get(&amp;#39;session&amp;#39;);

  if (session) {
    // Sync user data to server
    await fetch(&amp;#39;/api/sync&amp;#39;, {
      headers: {
        &amp;#39;Authorization&amp;#39;: `Bearer ${session.value}`
      }
    });
  }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Push Notifications with Personalization:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// In service worker
self.addEventListener(&amp;#39;push&amp;#39;, async (event) =&amp;gt; {
  const preferences = await cookieStore.get(&amp;#39;notifications&amp;#39;);

  if (preferences) {
    // Personalize based on user preferences
    const data = await event.data.json();
    data.theme = JSON.parse(preferences.value).theme;

    await self.registration.showNotification(&amp;#39;Update&amp;#39;, data);
  }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Offline-First Authentication:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// In service worker
self.addEventListener(&amp;#39;fetch&amp;#39;, async (event) =&amp;gt; {
  if (event.request.url.includes(&amp;#39;/api/&amp;#39;)) {
    const session = await cookieStore.get(&amp;#39;session&amp;#39;);

    if (session) {
      // Add auth header to all API requests
      const modifiedRequest = new Request(event.request, {
        headers: {
          ...event.request.headers,
          &amp;#39;Authorization&amp;#39;: `Bearer ${session.value}`
        }
      });

      return fetch(modifiedRequest);
    }
  }

  return event.respondWith(fetch(event.request));
});
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Service workers can only access cookies via &lt;code&gt;self.cookieStore&lt;/code&gt;, not &lt;code&gt;window.cookieStore&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Async Patterns &amp;amp; Error Handling&lt;/h2&gt;
&lt;h3&gt;Promise Chaining&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Chain multiple operations
await cookieStore.set(&amp;#39;step1&amp;#39;, &amp;#39;value1&amp;#39;);
await cookieStore.set(&amp;#39;step2&amp;#39;, &amp;#39;value2&amp;#39;);
await cookieStore.set(&amp;#39;step3&amp;#39;, &amp;#39;value3&amp;#39;);

// Parallel operations (faster!)
await Promise.all([
  cookieStore.set(&amp;#39;cookie1&amp;#39;, &amp;#39;value1&amp;#39;),
  cookieStore.set(&amp;#39;cookie2&amp;#39;, &amp;#39;value2&amp;#39;),
  cookieStore.set(&amp;#39;cookie3&amp;#39;, &amp;#39;value3&amp;#39;)
]);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Error Handling&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;try {
  await cookieStore.set({
    name: &amp;#39;session&amp;#39;,
    value: token,
    secure: true,
    sameSite: &amp;#39;None&amp;#39; // ❌ Will throw without secure!
  });
} catch (error) {
  console.error(&amp;#39;Failed to set cookie:&amp;#39;, error);
  // Handle error (show message to user, fallback, etc.)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Common errors:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Secure context required&lt;/strong&gt;: Only works on HTTPS (or localhost)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Invalid SameSite&lt;/strong&gt;: &lt;code&gt;SameSite=&amp;#39;None&amp;#39;&lt;/code&gt; requires &lt;code&gt;Secure=true&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cookie size exceeded&lt;/strong&gt;: Cookies limited to ~4KB&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Invalid domain&lt;/strong&gt;: Can&amp;#39;t set cookies for other domains&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Retry Logic&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;async function setCookieWithRetry(name, value, options, maxRetries = 3) {
  for (let i = 0; i &amp;lt; maxRetries; i++) {
    try {
      await cookieStore.set({ name, value, ...options });
      return true;
    } catch (error) {
      if (i === maxRetries - 1) throw error;
      // Exponential backoff
      await new Promise(resolve =&amp;gt; setTimeout(resolve, Math.pow(2, i) * 100));
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Migration Guide: From &lt;code&gt;document.cookie&lt;/code&gt; to Cookie Store API&lt;/h2&gt;
&lt;h3&gt;Reading Cookies&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Old way:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const cookies = document.cookie.split(&amp;#39;;&amp;#39;).reduce((acc, cookie) =&amp;gt; {
  const [name, value] = cookie.trim().split(&amp;#39;=&amp;#39;);
  acc[name] = value;
  return acc;
}, {});
console.log(cookies.theme);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;New way:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const theme = await cookieStore.get(&amp;#39;theme&amp;#39;);
console.log(theme.value);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Setting Cookies&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Old way:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;document.cookie = &amp;#39;session=abc123; Path=/; Secure; SameSite=Strict&amp;#39;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;New way:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;await cookieStore.set({
  name: &amp;#39;session&amp;#39;,
  value: &amp;#39;abc123&amp;#39;,
  path: &amp;#39;/&amp;#39;,
  secure: true,
  sameSite: &amp;#39;Strict&amp;#39;
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Deleting Cookies&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Old way:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;document.cookie = &amp;#39;session=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT&amp;#39;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;New way:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;await cookieStore.delete(&amp;#39;session&amp;#39;);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Conditional Implementation&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Feature detection
if (&amp;#39;cookieStore&amp;#39; in window) {
  // Use Cookie Store API
  const cookie = await cookieStore.get(&amp;#39;session&amp;#39;);
} else {
  // Fallback to document.cookie
  const cookies = document.cookie;
  // Parse cookie string...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;BrowserCompatibility client:only /&gt;

&lt;h2&gt;Performance Considerations&lt;/h2&gt;
&lt;h3&gt;Batch Operations&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Slow (multiple network round-trips)
for (const [name, value] of Object.entries(cookies)) {
  await cookieStore.set(name, value);
}

// Fast (batched)
await Promise.all(
  Object.entries(cookies).map(([name, value]) =&amp;gt;
    cookieStore.set(name, value)
  )
);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Debounce Change Handlers&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let timeoutId: number;

cookieStore.addEventListener(&amp;#39;change&amp;#39;, () =&amp;gt; {
  clearTimeout(timeoutId);
  timeoutId = window.setTimeout(() =&amp;gt; {
    // Debounced logic (e.g., re-render UI)
    updateCookieUI();
  }, 100);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Cache Frequently Accessed Cookies&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const cookieCache = new Map&amp;lt;string, ExtendedCookieListItem&amp;gt;();

async function getCachedCookie(name: string) {
  // Check cache first
  if (cookieCache.has(name)) {
    return cookieCache.get(name);
  }

  // Fetch from cookie store
  const cookie = await cookieStore.get(name);
  if (cookie) {
    cookieCache.set(name, cookie);
  }

  return cookie;
}

// Invalidate cache on changes
cookieStore.addEventListener(&amp;#39;change&amp;#39;, (event) =&amp;gt; {
  event.changed.forEach(cookie =&amp;gt; cookieCache.set(cookie.name, cookie));
  event.deleted.forEach(cookie =&amp;gt; cookieCache.delete(cookie.name));
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Real-World Use Cases&lt;/h2&gt;
&lt;h3&gt;1. Session Management&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Set session on login
async function login(credentials) {
  const response = await fetch(&amp;#39;/api/login&amp;#39;, {
    method: &amp;#39;POST&amp;#39;,
    body: JSON.stringify(credentials)
  });

  const { token } = await response.json();

  await cookieStore.set({
    name: &amp;#39;session&amp;#39;,
    value: token,
    expires: Date.now() + 3600000 * 24 * 7, // 7 days
    secure: true,
    sameSite: &amp;#39;Strict&amp;#39;,
    httpOnly: false // Can&amp;#39;t set via JavaScript for server-only cookies
  });
}

// Check session on load
async function checkAuth() {
  const session = await cookieStore.get(&amp;#39;session&amp;#39;);

  if (!session) {
    window.location.href = &amp;#39;/login&amp;#39;;
    return false;
  }

  return true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. A/B Testing&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;async function getVariant() {
  let variant = await cookieStore.get(&amp;#39;ab_variant&amp;#39;);

  if (!variant) {
    // Assign random variant
    variant = Math.random() &amp;lt; 0.5 ? &amp;#39;A&amp;#39; : &amp;#39;B&amp;#39;;

    await cookieStore.set({
      name: &amp;#39;ab_variant&amp;#39;,
      value: variant,
      expires: Date.now() + 3600000 * 24 * 30 // 30 days
    });
  }

  return variant.value;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. User Preferences&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;async function setTheme(theme: &amp;#39;light&amp;#39; | &amp;#39;dark&amp;#39;) {
  await cookieStore.set({
    name: &amp;#39;theme&amp;#39;,
    value: theme,
    expires: Date.now() + 3600000 * 24 * 365 // 1 year
  });

  document.documentElement.setAttribute(&amp;#39;data-theme&amp;#39;, theme);
}

async function loadTheme() {
  const theme = await cookieStore.get(&amp;#39;theme&amp;#39;);
  if (theme) {
    document.documentElement.setAttribute(&amp;#39;data-theme&amp;#39;, theme.value);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. Cross-Domain Authentication&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Set SSO token across domains (with CORS)
async function setSSOToken(token: string, domains: string[]) {
  await Promise.all(
    domains.map(domain =&amp;gt;
      fetch(`https://${domain}/api/set-token`, {
        method: &amp;#39;POST&amp;#39;,
        body: JSON.stringify({ token }),
        credentials: &amp;#39;include&amp;#39;
      })
    )
  );
}

// In server endpoint (not shown):
// await cookieStore.set({ name: &amp;#39;sso_token&amp;#39;, value: token, domain });
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Best Practices&lt;/h2&gt;
&lt;h3&gt;1. Always Use Secure Cookies&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// ❌ Bad
await cookieStore.set(&amp;#39;session&amp;#39;, &amp;#39;token&amp;#39;);

// ✅ Good
await cookieStore.set({
  name: &amp;#39;session&amp;#39;,
  value: &amp;#39;token&amp;#39;,
  secure: true
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. Set Expiration&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// ❌ Bad (session cookie when not needed)
await cookieStore.set(&amp;#39;preferences&amp;#39;, &amp;#39;value&amp;#39;);

// ✅ Good (explicit expiration)
await cookieStore.set({
  name: &amp;#39;preferences&amp;#39;,
  value: &amp;#39;value&amp;#39;,
  expires: Date.now() + 3600000 * 24 * 365 // 1 year
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. Use SameSite Appropriately&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Authentication: Strict
await cookieStore.set({
  name: &amp;#39;session&amp;#39;,
  value: token,
  sameSite: &amp;#39;Strict&amp;#39;,
  secure: true
});

// Analytics: Lax
await cookieStore.set({
  name: &amp;#39;analytics_id&amp;#39;,
  value: &amp;#39;abc123&amp;#39;,
  sameSite: &amp;#39;Lax&amp;#39;
});

// Third-party: None (with Secure)
await cookieStore.set({
  name: &amp;#39;third_party&amp;#39;,
  value: &amp;#39;value&amp;#39;,
  sameSite: &amp;#39;None&amp;#39;,
  secure: true
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. Handle Errors Gracefully&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;async function setCookieSafe(options) {
  try {
    await cookieStore.set(options);
    return true;
  } catch (error) {
    console.warn(&amp;#39;Failed to set cookie:&amp;#39;, error);

    // Fallback or alternative
    localStorage.setItem(options.name, options.value);
    return false;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5. Use Partitioned Cookies for Third-Party Contexts&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Embedded service
await cookieStore.set({
  name: &amp;#39;analytics&amp;#39;,
  value: &amp;#39;id123&amp;#39;,
  domain: &amp;#39;.analytics.com&amp;#39;,
  partitioned: true // Privacy-preserving
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Wrapping Up&lt;/h2&gt;
&lt;p&gt;We&amp;#39;ve covered a lot of ground! Here&amp;#39;s what we explored:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The Problem&lt;/strong&gt;: &lt;code&gt;document.cookie&lt;/code&gt; is synchronous, error-prone, and unavailable in service workers&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The Solution&lt;/strong&gt;: Cookie Store API is async, Promise-based, and works everywhere&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Core Methods&lt;/strong&gt;: &lt;code&gt;get()&lt;/code&gt;, &lt;code&gt;getAll()&lt;/code&gt;, &lt;code&gt;set()&lt;/code&gt;, &lt;code&gt;delete()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Advanced Features&lt;/strong&gt;: Change events, partitioned cookies, service worker integration&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Best Practices&lt;/strong&gt;: Security, performance, error handling&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The Cookie Store API represents a significant improvement in how we handle cookies on the web. It&amp;#39;s:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Faster&lt;/strong&gt;: Asynchronous, non-blocking operations&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Safer&lt;/strong&gt;: Better security controls (partitioned cookies, SameSite)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;More Powerful&lt;/strong&gt;: Works in service workers, supports change events&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Easier&lt;/strong&gt;: No string parsing, promise-based, type-safe&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Resources&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Cookie_Store_API&quot;&gt;MDN: Cookie Store API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://cookiestore.spec.whatwg.org/&quot;&gt;Specification&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://caniuse.com/cookie-store-api&quot;&gt;Can I Use: Cookie Store API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/privacycg/CHIPS&quot;&gt;Explainer: Partitioned Cookies&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The code for all interactive demos in this article is available in this repository. Feel free to explore, fork, and build your own cookie-powered experiences!&lt;/p&gt;
</content:encoded><media:content type="image/png" width="1200" height="630" medium="image" url="https://andrewusher.dev/open-graph/cookie-store-api-the-modern-way-to-handle-cookies.png"/><category>web-api</category><category>cookies</category><category>javascript</category><category>interactive</category><category>async</category></item><item><title>Building Real-Time Dashboards with Cloudflare Durable Objects</title><link>https://andrewusher.dev/blog/building-realtime-dashboards-with-durable-objects/</link><guid isPermaLink="true">https://andrewusher.dev/blog/building-realtime-dashboards-with-durable-objects/</guid><description>Introduction What if you could build a real-time analytics dashboard that scales to millions of concurrent users, handles persistent WebSocket connect...</description><pubDate>Sun, 21 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;What if you could build a real-time analytics dashboard that scales to millions of concurrent users, handles persistent WebSocket connections, and costs almost nothing when idle? That&amp;#39;s the promise of Cloudflare Durable Objects with Hibernation.&lt;/p&gt;
&lt;p&gt;Traditional real-time architectures require dedicated servers that consume resources 24/7, even when mostly idle. Connection management becomes a nightmare at scale. Durable Objects flip this model on its head: they&amp;#39;re serverless, globally distributed, and automatically hibernate when inactive - consuming zero CPU time while preserving all state.&lt;/p&gt;
&lt;p&gt;In this article, we&amp;#39;ll explore how to build real-time systems using Durable Objects. We&amp;#39;ll understand the fundamentals, compare different connection types, visualize the architecture, and see how hibernation saves costs. Every concept comes with interactive demos you can experiment with right here in your browser.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What You&amp;#39;ll Learn&lt;/strong&gt;: By the end of this article, you&amp;#39;ll understand Durable Objects, WebSocket vs SSE vs polling trade-offs, how hibernation works, and how to architect production-ready real-time dashboards.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;What Are Durable Objects?&lt;/h2&gt;
&lt;p&gt;Durable Objects are Cloudflare&amp;#39;s answer to stateful serverless computing. Think of them as single-threaded, globally distributed mini-servers that live at the edge. Unlike traditional serverless functions that are stateless and ephemeral, each Durable Object:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Has persistent state&lt;/strong&gt; - In-memory data that survives between requests&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lives in one location&lt;/strong&gt; - Provides strong consistency guarantees&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Handles concurrent connections&lt;/strong&gt; - Perfect for WebSockets and real-time data&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hibernates when idle&lt;/strong&gt; - Automatically freezes to save costs&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Routes by ID&lt;/strong&gt; - Multiple clients can connect to the same object instance&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The killer feature? They can maintain WebSocket connections while hibernating between messages. Your object wakes up when a message arrives, processes it, then goes back to sleep. No CPU billing during idle time.&lt;/p&gt;
&lt;h3&gt;The Perfect Use Case: Live Dashboards&lt;/h3&gt;
&lt;p&gt;Imagine building a live metrics dashboard where:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Multiple browser clients connect to see real-time data&lt;/li&gt;
&lt;li&gt;Updates are broadcast to all connected clients instantly&lt;/li&gt;
&lt;li&gt;The system scales from 10 to 10,000 concurrent viewers&lt;/li&gt;
&lt;li&gt;You only pay for actual processing time, not idle connections&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is exactly what Durable Objects excel at. Let&amp;#39;s build it.&lt;/p&gt;
&lt;h2&gt;Connection Types: Choosing Your Strategy&lt;/h2&gt;
&lt;p&gt;Before diving into Durable Objects, we need to understand the three main ways to stream data to browsers. Each has different trade-offs for real-time dashboards.&lt;/p&gt;
&lt;ConnectionTypesDemo client:load /&gt;

&lt;h3&gt;WebSockets: True Bidirectional Communication&lt;/h3&gt;
&lt;p&gt;WebSockets create a persistent, full-duplex connection between client and server. Both sides can send messages at any time.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How it works:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Client-side
const ws = new WebSocket(&amp;#39;wss://api.example.com/metrics/room-123&amp;#39;);

ws.onopen = () =&amp;gt; {
  ws.send(JSON.stringify({ type: &amp;#39;subscribe&amp;#39;, metric: &amp;#39;cpu&amp;#39; }));
};

ws.onmessage = (event) =&amp;gt; {
  const data = JSON.parse(event.data);
  updateDashboard(data);
};

// Durable Object can send at any time
ws.send(JSON.stringify({ cpu: 85.3, timestamp: Date.now() }));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Lowest latency for bidirectional communication&lt;/li&gt;
&lt;li&gt;Efficient for high-frequency updates&lt;/li&gt;
&lt;li&gt;Native browser support&lt;/li&gt;
&lt;li&gt;Can send from both client and server&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;More complex than HTTP&lt;/li&gt;
&lt;li&gt;Requires sticky routing (perfect for Durable Objects)&lt;/li&gt;
&lt;li&gt;May need protocol upgrade negotiation&lt;/li&gt;
&lt;li&gt;Some corporate firewalls block WebSockets&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Real-time dashboards, multiplayer games, collaborative editing, chat applications&lt;/p&gt;
&lt;h3&gt;Server-Sent Events (SSE): Simple Streaming&lt;/h3&gt;
&lt;p&gt;SSE is a simpler, HTTP-based protocol where the server streams events to the client. It&amp;#39;s unidirectional - only the server can push data.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How it works:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Client-side
const eventSource = new EventSource(&amp;#39;/api/metrics/stream&amp;#39;);

eventSource.onmessage = (event) =&amp;gt; {
  const data = JSON.parse(event.data);
  updateDashboard(data);
};

// Server streams events
eventSource.addEventListener(&amp;#39;metric-update&amp;#39;, (event) =&amp;gt; {
  console.log(&amp;#39;Received:&amp;#39;, event.data);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Simpler than WebSockets&lt;/li&gt;
&lt;li&gt;Built on HTTP (better firewall compatibility)&lt;/li&gt;
&lt;li&gt;Automatic reconnection handling&lt;/li&gt;
&lt;li&gt;Native browser support&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Unidirectional only (server → client)&lt;/li&gt;
&lt;li&gt;Limit of 6 concurrent connections per browser domain&lt;/li&gt;
&lt;li&gt;Less efficient encoding than WebSockets&lt;/li&gt;
&lt;li&gt;Client must use separate HTTP requests to send data&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; News feeds, stock tickers, server logs, one-way notifications&lt;/p&gt;
&lt;h3&gt;HTTP Polling: The Fallback&lt;/h3&gt;
&lt;p&gt;Polling is the simplest approach: the client repeatedly requests updates at a fixed interval.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How it works:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Client polls every 2 seconds
setInterval(async () =&amp;gt; {
  const response = await fetch(&amp;#39;/api/metrics/latest&amp;#39;);
  const data = await response.json();
  updateDashboard(data);
}, 2000);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Extremely simple to implement&lt;/li&gt;
&lt;li&gt;Works everywhere (just HTTP)&lt;/li&gt;
&lt;li&gt;Easy to debug and understand&lt;/li&gt;
&lt;li&gt;Stateless server architecture&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;High latency (interval-dependent)&lt;/li&gt;
&lt;li&gt;Wasteful (many empty responses)&lt;/li&gt;
&lt;li&gt;Scales poorly (constant server requests)&lt;/li&gt;
&lt;li&gt;Battery drain on mobile devices&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Infrequent updates, simple MVPs, maximum compatibility, systems with unpredictable update timing&lt;/p&gt;
&lt;h3&gt;The Verdict for Dashboards&lt;/h3&gt;
&lt;p&gt;For real-time dashboards with Durable Objects, &lt;strong&gt;WebSockets are the clear winner&lt;/strong&gt;. They provide:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Instant bidirectional updates&lt;/li&gt;
&lt;li&gt;Efficient binary or JSON encoding&lt;/li&gt;
&lt;li&gt;Perfect pairing with Durable Objects (they handle persistent connections natively)&lt;/li&gt;
&lt;li&gt;Low overhead once connected&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We&amp;#39;ll use WebSockets for the rest of this article, but the concepts apply to SSE as well.&lt;/p&gt;
&lt;h2&gt;Architecture: How It All Fits Together&lt;/h2&gt;
&lt;p&gt;Now let&amp;#39;s visualize how Durable Objects orchestrate real-time connections. This interactive demo simulates the full architecture:&lt;/p&gt;
&lt;DashboardArchitectureDemo client:load /&gt;

&lt;h3&gt;Understanding the Flow&lt;/h3&gt;
&lt;p&gt;In this architecture:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Clients connect&lt;/strong&gt; - Each browser establishes a WebSocket connection to the Durable Object&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Durable Object coordinates&lt;/strong&gt; - A single object instance manages all connections for a specific dashboard/room&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Messages flow bidirectionally&lt;/strong&gt; - Clients send metrics, server broadcasts updates&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;State persists&lt;/strong&gt; - The object maintains connection state, message history, and application data&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hibernation kicks in&lt;/strong&gt; - When no messages arrive for a while, the object freezes&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Try adding multiple clients in the demo above. Notice how:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Each client connects through the Durable Object&lt;/li&gt;
&lt;li&gt;When you send a message, it routes through the DO&lt;/li&gt;
&lt;li&gt;Broadcasting reaches all connected clients simultaneously&lt;/li&gt;
&lt;li&gt;The event log tracks every connection, message, and state change&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;The Code Structure&lt;/h3&gt;
&lt;p&gt;Here&amp;#39;s how you&amp;#39;d structure a Durable Object for a metrics dashboard:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// worker.ts - Cloudflare Worker entrypoint

  async fetch(request: Request, env: Env) {
    const url = new URL(request.url);

    // Extract dashboard ID from URL
    const dashboardId = url.pathname.split(&amp;#39;/&amp;#39;)[2];

    // Get the Durable Object instance for this dashboard
    const id = env.METRICS_DASHBOARD.idFromName(dashboardId);
    const stub = env.METRICS_DASHBOARD.get(id);

    // Forward the request to the Durable Object
    return stub.fetch(request);
  }
}

// metrics-dashboard.ts - Durable Object class

  private sessions: Map&amp;lt;WebSocket, { id: string; name: string }&amp;gt;;
  private metrics: Map&amp;lt;string, any&amp;gt;;

  constructor(private state: DurableObjectState, private env: Env) {
    this.sessions = new Map();
    this.metrics = new Map();

    // Enable WebSocket hibernation
    this.state.blockConcurrencyWhile(async () =&amp;gt; {
      // Load persisted state if needed
      const stored = await this.state.storage.get(&amp;#39;metrics&amp;#39;);
      if (stored) this.metrics = new Map(stored);
    });
  }

  async fetch(request: Request): Promise&amp;lt;Response&amp;gt; {
    // Upgrade to WebSocket
    const upgradeHeader = request.headers.get(&amp;#39;Upgrade&amp;#39;);
    if (upgradeHeader !== &amp;#39;websocket&amp;#39;) {
      return new Response(&amp;#39;Expected WebSocket&amp;#39;, { status: 426 });
    }

    const pair = new WebSocketPair();
    const [client, server] = Object.values(pair);

    // Accept the WebSocket connection
    this.state.acceptWebSocket(server);

    // Store session info
    const sessionId = crypto.randomUUID();
    this.sessions.set(server, { id: sessionId, name: `Client-${sessionId.slice(0, 8)}` });

    // Send welcome message
    server.send(JSON.stringify({
      type: &amp;#39;connected&amp;#39;,
      sessionId,
      currentMetrics: Object.fromEntries(this.metrics)
    }));

    // Notify others
    this.broadcast({
      type: &amp;#39;user-joined&amp;#39;,
      sessionId
    }, server);

    return new Response(null, { status: 101, webSocket: client });
  }

  // This method is called when a WebSocket message arrives
  async webSocketMessage(ws: WebSocket, message: string | ArrayBuffer) {
    const session = this.sessions.get(ws);
    if (!session) return;

    try {
      const data = JSON.parse(message as string);

      // Handle different message types
      switch (data.type) {
        case &amp;#39;metric-update&amp;#39;:
          // Store the metric
          this.metrics.set(data.name, data.value);

          // Broadcast to all connected clients
          this.broadcast({
            type: &amp;#39;metric-updated&amp;#39;,
            name: data.name,
            value: data.value,
            timestamp: Date.now()
          });
          break;

        case &amp;#39;subscribe&amp;#39;:
          // Send current metrics to this client
          ws.send(JSON.stringify({
            type: &amp;#39;snapshot&amp;#39;,
            metrics: Object.fromEntries(this.metrics)
          }));
          break;
      }
    } catch (error) {
      console.error(&amp;#39;Error processing message:&amp;#39;, error);
      ws.send(JSON.stringify({ type: &amp;#39;error&amp;#39;, message: &amp;#39;Invalid message format&amp;#39; }));
    }
  }

  // Called when a WebSocket closes
  async webSocketClose(ws: WebSocket, code: number, reason: string) {
    const session = this.sessions.get(ws);
    if (session) {
      this.sessions.delete(ws);

      // Notify others
      this.broadcast({
        type: &amp;#39;user-left&amp;#39;,
        sessionId: session.id
      });
    }

    // Clean up
    ws.close(code, reason);
  }

  // Helper to broadcast to all connected clients
  private broadcast(message: any, exclude?: WebSocket) {
    const payload = JSON.stringify(message);

    for (const [ws] of this.sessions) {
      if (ws !== exclude) {
        try {
          ws.send(payload);
        } catch (error) {
          console.error(&amp;#39;Error broadcasting to client:&amp;#39;, error);
        }
      }
    }
  }

  // Called when an error occurs on a WebSocket
  async webSocketError(ws: WebSocket, error: unknown) {
    console.error(&amp;#39;WebSocket error:&amp;#39;, error);
    ws.close(1011, &amp;#39;Internal error&amp;#39;);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Key Patterns to Notice&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;1. Routing by ID&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Multiple clients connecting to &amp;quot;dashboard-123&amp;quot; get the SAME object instance
const id = env.METRICS_DASHBOARD.idFromName(&amp;#39;dashboard-123&amp;#39;);
const stub = env.METRICS_DASHBOARD.get(id);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;2. WebSocket Lifecycle Management&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Accept connection
this.state.acceptWebSocket(server);

// Durable Objects automatically handle:
// - webSocketMessage() when messages arrive
// - webSocketClose() when connections close
// - webSocketError() when errors occur
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;3. Broadcasting Pattern&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Send to all connected clients except the sender
private broadcast(message: any, exclude?: WebSocket) {
  for (const [ws] of this.sessions) {
    if (ws !== exclude) ws.send(JSON.stringify(message));
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;4. State Persistence&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Store in Durable Object storage (persists across hibernation)
await this.state.storage.put(&amp;#39;metrics&amp;#39;, Object.fromEntries(this.metrics));

// Load on wake-up
const stored = await this.state.storage.get(&amp;#39;metrics&amp;#39;);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;The Magic of Hibernation&lt;/h2&gt;
&lt;p&gt;Here&amp;#39;s where Durable Objects get really interesting. Traditional WebSocket servers consume CPU time continuously while connections are open. Durable Objects can hibernate - they freeze execution while keeping connections alive.&lt;/p&gt;
&lt;HibernationDemo client:load /&gt;

&lt;h3&gt;How Hibernation Works&lt;/h3&gt;
&lt;p&gt;The lifecycle has four states:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. Active&lt;/strong&gt; - Processing messages, executing code&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Full CPU billing&lt;/li&gt;
&lt;li&gt;Handling WebSocket messages, HTTP requests, or alarms&lt;/li&gt;
&lt;li&gt;Object stays active for a timeout period after the last activity (default: ~30 seconds)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;2. Idle&lt;/strong&gt; - No active requests, waiting for hibernation timeout&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Still billing for CPU (object is running)&lt;/li&gt;
&lt;li&gt;Hibernation timer has started&lt;/li&gt;
&lt;li&gt;If new activity arrives, resets to Active&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;3. Hibernating&lt;/strong&gt; - Frozen state, consuming no CPU&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;✨ &lt;strong&gt;No CPU billing&lt;/strong&gt; - This is where you save money&lt;/li&gt;
&lt;li&gt;All WebSocket connections remain open&lt;/li&gt;
&lt;li&gt;In-memory state is preserved&lt;/li&gt;
&lt;li&gt;Waiting for wake event (message, alarm, HTTP request)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;4. Resuming&lt;/strong&gt; - Waking from hibernation&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Brief CPU time for state restoration (~milliseconds)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;webSocketMessage()&lt;/code&gt; or &lt;code&gt;fetch()&lt;/code&gt; handlers execute&lt;/li&gt;
&lt;li&gt;Returns to Active state&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;The Cost Savings&lt;/h3&gt;
&lt;p&gt;Let&amp;#39;s do the math for a real-time dashboard with 1,000 connected users:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Traditional Server (always-on):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1,000 connections × 24 hours/day = 24,000 connection-hours&lt;/li&gt;
&lt;li&gt;Assumes constant CPU usage even when idle&lt;/li&gt;
&lt;li&gt;Estimated cost: ~$50-100/month for dedicated server&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Durable Object with Hibernation:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Active time: ~1 second per message × 10 messages/hour = 10 seconds/hour active&lt;/li&gt;
&lt;li&gt;Hibernating: 59 minutes 50 seconds per hour&lt;/li&gt;
&lt;li&gt;CPU billing: Only the ~10 seconds of active time&lt;/li&gt;
&lt;li&gt;Estimated cost: ~$0.50-2/month&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That&amp;#39;s &lt;strong&gt;95%+ cost reduction&lt;/strong&gt; for applications with bursty traffic.&lt;/p&gt;
&lt;h3&gt;Hibernation Best Practices&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;1. Design for resumption&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Don&amp;#39;t rely on timers or intervals - they stop during hibernation
// ❌ Bad
setInterval(() =&amp;gt; this.cleanup(), 60000);

// ✅ Good - use alarms instead
this.state.storage.setAlarm(Date.now() + 60000);

async alarm() {
  this.cleanup();
  // Schedule next alarm
  this.state.storage.setAlarm(Date.now() + 60000);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;2. Keep in-memory state minimal&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Hibernation preserves in-memory state, but large objects slow wake-up
// ✅ Good - lean state
private sessions: Map&amp;lt;WebSocket, SessionInfo&amp;gt;;
private metrics: Map&amp;lt;string, number&amp;gt;;

// ❌ Bad - huge state slows resumption
private fullHistory: Array&amp;lt;{ timestamp: number; data: LargeObject }&amp;gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;3. Use storage for critical data&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// In-memory state can be lost on rare occasions (object migration, errors)
// Persist critical data to Durable Object storage
async updateMetric(name: string, value: number) {
  this.metrics.set(name, value);

  // Persist to storage
  await this.state.storage.put(`metric:${name}`, value);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;4. Handle wake-up gracefully&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// The first message after hibernation takes slightly longer
// Don&amp;#39;t assume instant processing
webSocketMessage(ws: WebSocket, message: string) {
  // Objects wake up automatically - no special handling needed
  // But avoid complex initialization in hot paths
  this.processMessage(message);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Monitoring Hibernation&lt;/h3&gt;
&lt;p&gt;You can track hibernation metrics in your Durable Object:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;
  private stats = {
    totalMessages: 0,
    hibernationCount: 0,
    lastActivityTime: Date.now()
  };

  async webSocketMessage(ws: WebSocket, message: string) {
    const now = Date.now();
    const timeSinceLastActivity = now - this.stats.lastActivityTime;

    // If more than 30 seconds passed, we probably hibernated
    if (timeSinceLastActivity &amp;gt; 30000) {
      this.stats.hibernationCount++;
      console.log(`Resumed from hibernation after ${timeSinceLastActivity}ms`);
    }

    this.stats.lastActivityTime = now;
    this.stats.totalMessages++;

    // Process the message
    this.handleMessage(ws, message);
  }

  // Expose stats via HTTP endpoint
  async fetch(request: Request) {
    const url = new URL(request.url);

    if (url.pathname === &amp;#39;/stats&amp;#39;) {
      return new Response(JSON.stringify(this.stats), {
        headers: { &amp;#39;Content-Type&amp;#39;: &amp;#39;application/json&amp;#39; }
      });
    }

    // ... WebSocket upgrade logic
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Stream Backpressure and Buffering&lt;/h2&gt;
&lt;p&gt;Real-time systems need to handle scenarios where data arrives faster than clients can consume it. This is where backpressure and buffering come in.&lt;/p&gt;
&lt;h3&gt;The Problem&lt;/h3&gt;
&lt;p&gt;Imagine your dashboard receives 1,000 metric updates per second, but a client on a slow connection can only process 10/second. What happens to the other 990 messages?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Option 1: Drop them&lt;/strong&gt; - Client sees stale data
&lt;strong&gt;Option 2: Buffer them&lt;/strong&gt; - Risk memory overflow
&lt;strong&gt;Option 3: Apply backpressure&lt;/strong&gt; - Slow down the sender&lt;/p&gt;
&lt;p&gt;Durable Objects support all three strategies.&lt;/p&gt;
&lt;h3&gt;Implementing Smart Buffering&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;
  private buffers: Map&amp;lt;WebSocket, Array&amp;lt;any&amp;gt;&amp;gt;;
  private readonly MAX_BUFFER_SIZE = 100;

  constructor(state: DurableObjectState, env: Env) {
    this.buffers = new Map();
  }

  async webSocketMessage(ws: WebSocket, message: string) {
    const data = JSON.parse(message);

    // Broadcast with buffering
    this.broadcastWithBackpressure({
      type: &amp;#39;metric-update&amp;#39;,
      ...data
    });
  }

  private broadcastWithBackpressure(message: any) {
    const payload = JSON.stringify(message);

    for (const [ws] of this.sessions) {
      try {
        // Check WebSocket ready state
        if (ws.readyState === WebSocket.OPEN) {
          // Check if buffer exists for this socket
          const buffer = this.buffers.get(ws) || [];

          if (buffer.length &amp;gt;= this.MAX_BUFFER_SIZE) {
            // Buffer full - drop oldest messages (or apply other strategy)
            console.warn(`Buffer full for client, dropping old messages`);
            buffer.shift(); // Remove oldest
          }

          // Add to buffer
          buffer.push(payload);
          this.buffers.set(ws, buffer);

          // Try to flush buffer
          this.flushBuffer(ws);
        }
      } catch (error) {
        console.error(&amp;#39;Error buffering message:&amp;#39;, error);
      }
    }
  }

  private flushBuffer(ws: WebSocket) {
    const buffer = this.buffers.get(ws);
    if (!buffer || buffer.length === 0) return;

    try {
      // Send all buffered messages
      while (buffer.length &amp;gt; 0) {
        const message = buffer.shift();
        ws.send(message);
      }

      // Clear empty buffer
      if (buffer.length === 0) {
        this.buffers.delete(ws);
      }
    } catch (error) {
      console.error(&amp;#39;Error flushing buffer:&amp;#39;, error);
      // Keep messages in buffer for retry
    }
  }

  async webSocketClose(ws: WebSocket, code: number, reason: string) {
    // Clean up buffer
    this.buffers.delete(ws);
    this.sessions.delete(ws);
    ws.close(code, reason);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sampling Strategy&lt;/h3&gt;
&lt;p&gt;For high-frequency metrics, you might want to sample instead of sending everything:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;
  private lastBroadcast: Map&amp;lt;string, number&amp;gt;;
  private readonly MIN_BROADCAST_INTERVAL = 100; // ms

  constructor(state: DurableObjectState, env: Env) {
    this.lastBroadcast = new Map();
  }

  async webSocketMessage(ws: WebSocket, message: string) {
    const data = JSON.parse(message);

    if (data.type === &amp;#39;metric-update&amp;#39;) {
      const metricKey = data.name;
      const now = Date.now();
      const lastTime = this.lastBroadcast.get(metricKey) || 0;

      // Only broadcast if enough time has passed
      if (now - lastTime &amp;gt;= this.MIN_BROADCAST_INTERVAL) {
        this.broadcast({
          type: &amp;#39;metric-update&amp;#39;,
          name: data.name,
          value: data.value,
          timestamp: now
        });

        this.lastBroadcast.set(metricKey, now);
      } else {
        // Update internal state but don&amp;#39;t broadcast
        this.metrics.set(data.name, data.value);
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This ensures clients receive updates at most once per 100ms per metric, preventing overwhelming slow clients.&lt;/p&gt;
&lt;h2&gt;Production Considerations&lt;/h2&gt;
&lt;p&gt;Building a production-ready real-time dashboard requires attention to several key areas.&lt;/p&gt;
&lt;h3&gt;Error Handling&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;
  async webSocketMessage(ws: WebSocket, message: string | ArrayBuffer) {
    try {
      // Always validate input
      if (typeof message !== &amp;#39;string&amp;#39;) {
        throw new Error(&amp;#39;Expected string message&amp;#39;);
      }

      const data = JSON.parse(message);

      // Validate message structure
      if (!data.type) {
        throw new Error(&amp;#39;Message missing type field&amp;#39;);
      }

      // Process message
      await this.handleMessage(ws, data);

    } catch (error) {
      console.error(&amp;#39;Error processing WebSocket message:&amp;#39;, error);

      // Send error to client
      try {
        ws.send(JSON.stringify({
          type: &amp;#39;error&amp;#39;,
          message: error instanceof Error ? error.message : &amp;#39;Unknown error&amp;#39;,
          code: &amp;#39;PROCESSING_ERROR&amp;#39;
        }));
      } catch (sendError) {
        // Failed to send error - close connection
        console.error(&amp;#39;Failed to send error to client:&amp;#39;, sendError);
        ws.close(1011, &amp;#39;Internal error&amp;#39;);
      }
    }
  }

  async webSocketError(ws: WebSocket, error: unknown) {
    console.error(&amp;#39;WebSocket error:&amp;#39;, error);

    // Clean up session
    this.sessions.delete(ws);

    // Close with appropriate code
    try {
      ws.close(1011, &amp;#39;Unexpected error&amp;#39;);
    } catch (closeError) {
      console.error(&amp;#39;Error closing WebSocket:&amp;#39;, closeError);
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Rate Limiting&lt;/h3&gt;
&lt;p&gt;Protect your Durable Object from abusive clients:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;
  private rateLimits: Map&amp;lt;WebSocket, { count: number; resetTime: number }&amp;gt;;
  private readonly MAX_MESSAGES_PER_MINUTE = 60;

  constructor(state: DurableObjectState, env: Env) {
    this.rateLimits = new Map();
  }

  async webSocketMessage(ws: WebSocket, message: string) {
    // Check rate limit
    if (!this.checkRateLimit(ws)) {
      ws.send(JSON.stringify({
        type: &amp;#39;error&amp;#39;,
        message: &amp;#39;Rate limit exceeded&amp;#39;,
        code: &amp;#39;RATE_LIMIT&amp;#39;
      }));
      return;
    }

    // Process message normally
    this.handleMessage(ws, message);
  }

  private checkRateLimit(ws: WebSocket): boolean {
    const now = Date.now();
    let limit = this.rateLimits.get(ws);

    if (!limit || now &amp;gt; limit.resetTime) {
      // Create or reset limit
      limit = {
        count: 0,
        resetTime: now + 60000 // 1 minute window
      };
      this.rateLimits.set(ws, limit);
    }

    limit.count++;

    if (limit.count &amp;gt; this.MAX_MESSAGES_PER_MINUTE) {
      return false; // Rate limit exceeded
    }

    return true;
  }

  async webSocketClose(ws: WebSocket, code: number, reason: string) {
    // Clean up rate limit tracking
    this.rateLimits.delete(ws);
    this.sessions.delete(ws);
    ws.close(code, reason);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Authentication&lt;/h3&gt;
&lt;p&gt;Secure your WebSocket connections:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// worker.ts - Validate auth before routing to Durable Object

  async fetch(request: Request, env: Env) {
    const url = new URL(request.url);

    // Extract and verify auth token
    const token = url.searchParams.get(&amp;#39;token&amp;#39;);
    if (!token) {
      return new Response(&amp;#39;Unauthorized&amp;#39;, { status: 401 });
    }

    // Verify JWT or session token
    const userId = await verifyAuthToken(token, env);
    if (!userId) {
      return new Response(&amp;#39;Invalid token&amp;#39;, { status: 403 });
    }

    // Check authorization for this dashboard
    const dashboardId = url.pathname.split(&amp;#39;/&amp;#39;)[2];
    if (!await canAccessDashboard(userId, dashboardId, env)) {
      return new Response(&amp;#39;Forbidden&amp;#39;, { status: 403 });
    }

    // Get Durable Object
    const id = env.METRICS_DASHBOARD.idFromName(dashboardId);
    const stub = env.METRICS_DASHBOARD.get(id);

    // Forward request with user context
    const modifiedRequest = new Request(request);
    modifiedRequest.headers.set(&amp;#39;X-User-ID&amp;#39;, userId);

    return stub.fetch(modifiedRequest);
  }
}

// metrics-dashboard.ts

  async fetch(request: Request): Promise&amp;lt;Response&amp;gt; {
    // Extract authenticated user
    const userId = request.headers.get(&amp;#39;X-User-ID&amp;#39;);
    if (!userId) {
      return new Response(&amp;#39;Unauthorized&amp;#39;, { status: 401 });
    }

    // Upgrade to WebSocket
    const pair = new WebSocketPair();
    const [client, server] = Object.values(pair);

    this.state.acceptWebSocket(server);

    // Store user ID with session
    this.sessions.set(server, {
      id: crypto.randomUUID(),
      userId,
      name: `User-${userId}`
    });

    return new Response(null, { status: 101, webSocket: client });
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Monitoring and Observability&lt;/h3&gt;
&lt;p&gt;Track key metrics:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;
  private metrics = {
    totalConnections: 0,
    currentConnections: 0,
    messagesProcessed: 0,
    errorCount: 0,
    hibernationEvents: 0
  };

  async fetch(request: Request): Promise&amp;lt;Response&amp;gt; {
    const url = new URL(request.url);

    // Expose metrics endpoint
    if (url.pathname === &amp;#39;/metrics&amp;#39;) {
      return new Response(JSON.stringify(this.metrics), {
        headers: { &amp;#39;Content-Type&amp;#39;: &amp;#39;application/json&amp;#39; }
      });
    }

    // WebSocket upgrade
    if (request.headers.get(&amp;#39;Upgrade&amp;#39;) === &amp;#39;websocket&amp;#39;) {
      this.metrics.totalConnections++;
      this.metrics.currentConnections++;

      // ... handle WebSocket upgrade
    }
  }

  async webSocketClose(ws: WebSocket, code: number, reason: string) {
    this.metrics.currentConnections--;
    // ... cleanup
  }

  async webSocketMessage(ws: WebSocket, message: string) {
    this.metrics.messagesProcessed++;

    try {
      // ... process message
    } catch (error) {
      this.metrics.errorCount++;
      throw error;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can then poll these metrics or integrate with Cloudflare Analytics Engine.&lt;/p&gt;
&lt;h2&gt;Wrapping Up&lt;/h2&gt;
&lt;p&gt;We&amp;#39;ve covered a lot of ground! From understanding Durable Objects to building production-ready real-time dashboards. Here&amp;#39;s what we explored:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Durable Objects fundamentals&lt;/strong&gt; - Stateful serverless computing at the edge&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Connection strategies&lt;/strong&gt; - WebSockets vs SSE vs polling trade-offs&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Architecture patterns&lt;/strong&gt; - How Durable Objects coordinate real-time connections&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hibernation magic&lt;/strong&gt; - How to save 95%+ on costs with automatic sleep/wake&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Production concerns&lt;/strong&gt; - Error handling, rate limiting, auth, and monitoring&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Durable Objects with WebSocket hibernation represent a paradigm shift in real-time architecture. You get:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Global distribution without infrastructure management&lt;/li&gt;
&lt;li&gt;Strong consistency without coordination overhead&lt;/li&gt;
&lt;li&gt;Persistent connections without constant CPU billing&lt;/li&gt;
&lt;li&gt;Automatic scaling without capacity planning&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The result? Real-time systems that are simpler to build, cheaper to run, and easier to scale than traditional approaches.&lt;/p&gt;
&lt;h3&gt;Next Steps&lt;/h3&gt;
&lt;p&gt;Ready to build your own? Here are some ideas:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Live Collaboration&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Collaborative text editor (like Google Docs)&lt;/li&gt;
&lt;li&gt;Shared whiteboard or drawing canvas&lt;/li&gt;
&lt;li&gt;Multiplayer game state coordination&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Real-Time Analytics&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Server metrics dashboard (CPU, memory, requests)&lt;/li&gt;
&lt;li&gt;Application performance monitoring&lt;/li&gt;
&lt;li&gt;Live user activity tracking&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Communication&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Chat rooms with presence indicators&lt;/li&gt;
&lt;li&gt;Live notifications and alerts&lt;/li&gt;
&lt;li&gt;Streaming log viewers&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Creative Projects&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Live music jam sessions&lt;/li&gt;
&lt;li&gt;Shared particle simulations&lt;/li&gt;
&lt;li&gt;Multiplayer interactive art&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The code for all the interactive demos in this article is &lt;a href=&quot;https://github.com/andrewusher/andrewusher.dev&quot;&gt;available on GitHub&lt;/a&gt;. Feel free to explore, fork, and build your own real-time experiences!&lt;/p&gt;
</content:encoded><media:content type="image/png" width="1200" height="630" medium="image" url="https://andrewusher.dev/open-graph/building-realtime-dashboards-with-durable-objects.png"/><category>cloudflare</category><category>websockets</category><category>durable-objects</category><category>real-time</category><category>interactive</category></item><item><title>Give Your Web App a Voice</title><link>https://andrewusher.dev/blog/give-your-web-app-a-voice/</link><guid isPermaLink="true">https://andrewusher.dev/blog/give-your-web-app-a-voice/</guid><description>Introduction What if your website could literally talk to your users? Not through pre-recorded audio files or complicated audio libraries, but using t...</description><pubDate>Fri, 12 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;What if your website could literally talk to your users? Not through pre-recorded audio files or complicated audio libraries, but using the voices already built into their browser?&lt;/p&gt;
&lt;p&gt;The Web Speech API makes this possible. With just a few lines of JavaScript, you can transform any text into spoken words, customize the voice, pitch, and speed, and even track which word is being spoken in real-time. Try it out:&lt;/p&gt;
&lt;BasicPlayground client:load /&gt;

&lt;p&gt;Pretty cool, right? This technology opens up incredible possibilities: accessibility tools that read content aloud, language learning apps with pronunciation practice, interactive storytelling with character voices, and creative experiments you haven&amp;#39;t even imagined yet.&lt;/p&gt;
&lt;p&gt;In this article, we&amp;#39;ll explore the Speech Synthesis API from the ground up. We&amp;#39;ll start with the basics, progressively build up to advanced patterns, and create plenty of interactive demos you can play with along the way. By the end, you&amp;#39;ll have all the tools you need to give your web apps a voice.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Browser Compatibility Note&lt;/strong&gt;: The Speech Synthesis API is supported in modern browsers (Chrome, Safari, Edge, Firefox), but voice availability and behavior can vary significantly across platforms. iOS Safari, for example, has more limited voice options than desktop Chrome. We&amp;#39;ll explore these differences throughout this article.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Your First Words: The Basics&lt;/h2&gt;
&lt;p&gt;The Speech Synthesis API consists of two main pieces: the &lt;code&gt;speechSynthesis&lt;/code&gt; object (the controller) and &lt;code&gt;SpeechSynthesisUtterance&lt;/code&gt; (the thing being spoken). Think of it like a music player: &lt;code&gt;speechSynthesis&lt;/code&gt; is the play/pause/stop controls, while &lt;code&gt;SpeechSynthesisUtterance&lt;/code&gt; is the track you want to play.&lt;/p&gt;
&lt;p&gt;Here&amp;#39;s the absolute simplest example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Your browser&amp;#39;s built-in text-to-speech
const utterance = new SpeechSynthesisUtterance(&amp;quot;Hello, world!&amp;quot;);
speechSynthesis.speak(utterance);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That&amp;#39;s it! Just two lines of code. Let&amp;#39;s break down what&amp;#39;s happening:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Create an utterance&lt;/strong&gt;: &lt;code&gt;new SpeechSynthesisUtterance(&amp;quot;Hello, world!&amp;quot;)&lt;/code&gt; creates a speech request containing the text you want spoken.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Speak it&lt;/strong&gt;: &lt;code&gt;speechSynthesis.speak(utterance)&lt;/code&gt; adds your utterance to the speech queue and starts speaking it.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The speech synthesis system uses a queue, which means if you call &lt;code&gt;speak()&lt;/code&gt; multiple times, each utterance will be spoken in order. Think of it like a playlist - one finishes, then the next begins.&lt;/p&gt;
&lt;BasicPlayground client:load /&gt;

&lt;p&gt;Go ahead, modify the text in the playground above and hear how it sounds. The default voice varies by platform, but we&amp;#39;ll learn how to choose specific voices later.&lt;/p&gt;
&lt;h2&gt;Customizing the Voice: Parameters&lt;/h2&gt;
&lt;p&gt;The default voice is fine, but what if you want to make speech faster, slower, higher, or lower? The &lt;code&gt;SpeechSynthesisUtterance&lt;/code&gt; object has three properties you can adjust to customize how the speech sounds.&lt;/p&gt;
&lt;h3&gt;Pitch&lt;/h3&gt;
&lt;p&gt;Controls how high or low the voice sounds. The value ranges from &lt;code&gt;0&lt;/code&gt; to &lt;code&gt;2&lt;/code&gt;, with &lt;code&gt;1&lt;/code&gt; being the default.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;0.5&lt;/code&gt; = Deep, low-pitched voice&lt;/li&gt;
&lt;li&gt;&lt;code&gt;1.0&lt;/code&gt; = Normal pitch (default)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;1.5&lt;/code&gt; = Higher-pitched, squeakier voice&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Rate&lt;/h3&gt;
&lt;p&gt;Controls the speed of speech. Values range from &lt;code&gt;0.1&lt;/code&gt; to &lt;code&gt;10&lt;/code&gt;, though most useful values are between &lt;code&gt;0.5&lt;/code&gt; and &lt;code&gt;2&lt;/code&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;0.5&lt;/code&gt; = Half speed (good for language learning)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;1.0&lt;/code&gt; = Normal speed (default)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;1.5&lt;/code&gt; = 1.5x speed (like a podcast on fast-forward)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Volume&lt;/h3&gt;
&lt;p&gt;Controls how loud the voice is. Values range from &lt;code&gt;0&lt;/code&gt; (silent) to &lt;code&gt;1&lt;/code&gt; (full volume).&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;0&lt;/code&gt; = Silent&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0.5&lt;/code&gt; = Half volume&lt;/li&gt;
&lt;li&gt;&lt;code&gt;1.0&lt;/code&gt; = Full volume (default)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Play with these parameters in the interactive demo below. Notice how different combinations can create wildly different effects:&lt;/p&gt;
&lt;ParameterPlayground client:load /&gt;

&lt;h3&gt;Using Parameters in Code&lt;/h3&gt;
&lt;p&gt;Here&amp;#39;s how you set these parameters in vanilla JavaScript:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const utterance = new SpeechSynthesisUtterance(&amp;quot;I sound different now!&amp;quot;);
utterance.pitch = 1.5;  // Higher pitched
utterance.rate = 0.8;   // Slower
utterance.volume = 0.9; // Slightly quieter
speechSynthesis.speak(utterance);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And here&amp;#39;s a React component that lets users control these parameters:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;
function SpeechDemo() {
  const [pitch, setPitch] = useState(1);
  const [rate, setRate] = useState(1);
  const [volume, setVolume] = useState(1);

  const speak = (text: string) =&amp;gt; {
    const utterance = new SpeechSynthesisUtterance(text);
    utterance.pitch = pitch;
    utterance.rate = rate;
    utterance.volume = volume;
    speechSynthesis.speak(utterance);
  };

  // UI controls here...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Browser Note&lt;/strong&gt;: Most browsers support the full range of pitch and rate values, but some mobile browsers may clamp these values more aggressively. Safari on iOS, for example, limits how high or low the pitch can go compared to desktop Chrome.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Choosing Voices: The Voice Gallery&lt;/h2&gt;
&lt;p&gt;So far we&amp;#39;ve been using the browser&amp;#39;s default voice. But most browsers actually provide multiple voices across different languages. Some sound robotic, some sound surprisingly natural, and the selection varies wildly depending on your operating system and browser.&lt;/p&gt;
&lt;p&gt;You can get all available voices using &lt;code&gt;speechSynthesis.getVoices()&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Get all available voices
const voices = speechSynthesis.getVoices();

voices.forEach(voice =&amp;gt; {
  console.log(voice.name, voice.lang);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Try browsing through all the voices available on your device. The number and quality will vary - I get 80+ voices on macOS Chrome, but only a handful on iOS Safari:&lt;/p&gt;
&lt;VoiceShowcase client:load /&gt;

&lt;p&gt;Each &lt;code&gt;SpeechSynthesisVoice&lt;/code&gt; object has several properties:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;name&lt;/code&gt;&lt;/strong&gt;: The voice&amp;#39;s name (e.g., &amp;quot;Alex&amp;quot;, &amp;quot;Samantha&amp;quot;, &amp;quot;Google US English&amp;quot;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;lang&lt;/code&gt;&lt;/strong&gt;: The language code (e.g., &amp;quot;en-US&amp;quot;, &amp;quot;es-ES&amp;quot;, &amp;quot;ja-JP&amp;quot;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;localService&lt;/code&gt;&lt;/strong&gt;: Boolean indicating if the voice is on-device (&lt;code&gt;true&lt;/code&gt;) or requires network (&lt;code&gt;false&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;default&lt;/code&gt;&lt;/strong&gt;: Boolean indicating if this is the system&amp;#39;s default voice&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;The voiceschanged Gotcha&lt;/h3&gt;
&lt;p&gt;Here&amp;#39;s something that trips up a lot of developers: voices load asynchronously in most browsers. If you try to get voices immediately when your page loads, you&amp;#39;ll often get an empty array:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Wrong: voices might not be loaded yet!
const voices = speechSynthesis.getVoices(); // Often returns []
console.log(voices.length); // 0 😢
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The solution is to wait for the &lt;code&gt;voiceschanged&lt;/code&gt; event:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Right: wait for the event
speechSynthesis.addEventListener(&amp;#39;voiceschanged&amp;#39;, () =&amp;gt; {
  const voices = speechSynthesis.getVoices();
  console.log(&amp;#39;Voices loaded:&amp;#39;, voices.length); // 80 🎉
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In React, you&amp;#39;d typically handle this in a &lt;code&gt;useEffect&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;useEffect(() =&amp;gt; {
  const loadVoices = () =&amp;gt; {
    const availableVoices = speechSynthesis.getVoices();
    setVoices(availableVoices);
  };

  // Load voices immediately (works in some browsers)
  loadVoices();

  // Also listen for the voiceschanged event (required in others)
  speechSynthesis.addEventListener(&amp;#39;voiceschanged&amp;#39;, loadVoices);

  return () =&amp;gt; {
    speechSynthesis.removeEventListener(&amp;#39;voiceschanged&amp;#39;, loadVoices);
  };
}, []);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This approach covers both bases: it tries to load voices immediately (works in Firefox and Safari) and also listens for the event (required in Chrome and Edge).&lt;/p&gt;
&lt;h2&gt;Lifecycle Events: Controlling Playback&lt;/h2&gt;
&lt;p&gt;Speech isn&amp;#39;t just fire-and-forget. The &lt;code&gt;SpeechSynthesisUtterance&lt;/code&gt; object fires events throughout its lifecycle, allowing you to track when speech starts, ends, encounters errors, or even which word is currently being spoken.&lt;/p&gt;
&lt;h3&gt;The Event Lifecycle&lt;/h3&gt;
&lt;p&gt;Each utterance can emit several events:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;onstart&lt;/code&gt;&lt;/strong&gt; - Fired when speech begins&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;onend&lt;/code&gt;&lt;/strong&gt; - Fired when speech completes&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;onerror&lt;/code&gt;&lt;/strong&gt; - Fired if something goes wrong&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;onpause&lt;/code&gt;&lt;/strong&gt; - Fired when speech is paused&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;onresume&lt;/code&gt;&lt;/strong&gt; - Fired when speech resumes after pause&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;onboundary&lt;/code&gt;&lt;/strong&gt; - Fired at word/sentence boundaries (not supported in all browsers)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here&amp;#39;s a basic example in vanilla JavaScript:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const utterance = new SpeechSynthesisUtterance(&amp;quot;Track my lifecycle!&amp;quot;);

utterance.onstart = () =&amp;gt; console.log(&amp;#39;Started speaking&amp;#39;);
utterance.onend = () =&amp;gt; console.log(&amp;#39;Finished speaking&amp;#39;);
utterance.onerror = (event) =&amp;gt; console.error(&amp;#39;Error:&amp;#39;, event.error);

speechSynthesis.speak(utterance);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Interactive Demo&lt;/h3&gt;
&lt;p&gt;Try the interactive demo below. Click &amp;quot;Speak&amp;quot; and watch the event log populate in real-time. Try pausing and resuming to see how those events fire:&lt;/p&gt;
&lt;EventsDemo client:load /&gt;

&lt;p&gt;The &lt;code&gt;onboundary&lt;/code&gt; event is particularly interesting - it fires at word boundaries, giving you the character index and length of each word. You can use this to highlight words as they&amp;#39;re spoken, create karaoke-style effects, or track reading progress. Unfortunately, not all browsers support it (Firefox and Safari notably don&amp;#39;t).&lt;/p&gt;
&lt;h3&gt;Building a Reusable Hook&lt;/h3&gt;
&lt;p&gt;Rather than wiring up all these events every time, let&amp;#39;s create a reusable React hook. This is exactly what all the interactive demos in this article use:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// useSpeechSynthesis.ts

  const [voices, setVoices] = useState&amp;lt;SpeechSynthesisVoice[]&amp;gt;([]);
  const [speaking, setSpeaking] = useState(false);
  const [paused, setPaused] = useState(false);

  // Load voices (handling the async gotcha)
  useEffect(() =&amp;gt; {
    const loadVoices = () =&amp;gt; {
      setVoices(speechSynthesis.getVoices());
    };
    loadVoices();
    speechSynthesis.addEventListener(&amp;#39;voiceschanged&amp;#39;, loadVoices);
    return () =&amp;gt; {
      speechSynthesis.removeEventListener(&amp;#39;voiceschanged&amp;#39;, loadVoices);
    };
  }, []);

  const speak = useCallback((text: string, options = {}) =&amp;gt; {
    speechSynthesis.cancel(); // Clear queue
    const utterance = new SpeechSynthesisUtterance(text);

    // Apply options
    if (options.voice) utterance.voice = options.voice;
    if (options.pitch) utterance.pitch = options.pitch;
    if (options.rate) utterance.rate = options.rate;
    if (options.volume) utterance.volume = options.volume;

    // Attach event handlers
    utterance.onstart = () =&amp;gt; {
      setSpeaking(true);
      options.onStart?.();
    };
    utterance.onend = () =&amp;gt; {
      setSpeaking(false);
      options.onEnd?.();
    };
    utterance.onerror = (event) =&amp;gt; {
      console.error(&amp;#39;Speech error:&amp;#39;, event);
      setSpeaking(false);
      options.onError?.(event);
    };

    speechSynthesis.speak(utterance);
  }, []);

  const cancel = useCallback(() =&amp;gt; {
    speechSynthesis.cancel();
    setSpeaking(false);
  }, []);

  return { speak, cancel, voices, speaking, paused };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now using speech synthesis becomes much simpler:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// In your component
const { speak, voices, speaking, cancel } = useSpeechSynthesis();

// Speak with custom options
speak(&amp;quot;Hello!&amp;quot;, {
  voice: voices[0],
  pitch: 1.2,
  rate: 1.0,
  onEnd: () =&amp;gt; console.log(&amp;#39;Done!&amp;#39;)
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This hook handles voice loading, state management, and provides a clean API for all our needs. All the interactive demos in this article use this same hook - we&amp;#39;re not reinventing the wheel for each one!&lt;/p&gt;
&lt;h2&gt;Creative Use Cases&lt;/h2&gt;
&lt;p&gt;Now that you understand the fundamentals, let&amp;#39;s explore some creative applications. The Speech Synthesis API opens up possibilities that go far beyond simple text-to-speech.&lt;/p&gt;
&lt;h3&gt;Interactive Storytelling&lt;/h3&gt;
&lt;p&gt;Imagine a choose-your-own-adventure story where different characters have distinct voices. By switching between voices and adjusting parameters, you can create immersive, dynamic narratives:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Character voice switching example
const narrator = voices.find(v =&amp;gt; v.name.includes(&amp;#39;Alex&amp;#39;));
const character = voices.find(v =&amp;gt; v.name.includes(&amp;#39;Samantha&amp;#39;));

function speakDialogue(text, isNarrator) {
  const utterance = new SpeechSynthesisUtterance(text);
  utterance.voice = isNarrator ? narrator : character;
  utterance.pitch = isNarrator ? 1.0 : 1.3;
  speechSynthesis.speak(utterance);
}

// Usage
speakDialogue(&amp;quot;Once upon a time...&amp;quot;, true);
speakDialogue(&amp;quot;Help! A dragon!&amp;quot;, false);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You could take this further by:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Using the &lt;code&gt;onboundary&lt;/code&gt; event to highlight text as it&amp;#39;s spoken&lt;/li&gt;
&lt;li&gt;Synchronizing animations with speech events&lt;/li&gt;
&lt;li&gt;Letting users skip ahead by canceling the current utterance&lt;/li&gt;
&lt;li&gt;Creating voice-activated choices using the Speech Recognition API&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Language Learning&lt;/h3&gt;
&lt;p&gt;The Speech Synthesis API is perfect for language learning applications. By controlling the rate and selecting native voices for different languages, you can create pronunciation practice tools:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Language learning helper
function pronunciationPractice(word, language = &amp;#39;es-ES&amp;#39;) {
  const voice = voices.find(v =&amp;gt; v.lang === language);

  // Slow version for learning
  const slow = new SpeechSynthesisUtterance(word);
  slow.voice = voice;
  slow.rate = 0.6;

  // Normal speed version
  const normal = new SpeechSynthesisUtterance(word);
  normal.voice = voice;
  normal.rate = 1.0;

  // Speak slow first, then normal
  speechSynthesis.speak(slow);
  slow.onend = () =&amp;gt; speechSynthesis.speak(normal);
}

// Try it
pronunciationPractice(&amp;quot;¡Hola! ¿Cómo estás?&amp;quot;, &amp;quot;es-ES&amp;quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This pattern works great for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Vocabulary flashcards with audio&lt;/li&gt;
&lt;li&gt;Accent comparison (compare English voice saying Spanish words vs native Spanish voice)&lt;/li&gt;
&lt;li&gt;Pronunciation drills that repeat words at adjustable speeds&lt;/li&gt;
&lt;li&gt;Interactive lessons that respond to user progress&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Creative &amp;amp; Experimental&lt;/h3&gt;
&lt;p&gt;Speech synthesis can be an artistic medium. By randomizing parameters and using the event system creatively, you can create generative audio art:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Generative poetry reader with random parameters
function readPoetically(text) {
  const lines = text.split(&amp;#39;\n&amp;#39;);

  lines.forEach((line, i) =&amp;gt; {
    const utterance = new SpeechSynthesisUtterance(line);

    // Random voice parameters for artistic effect
    utterance.pitch = 0.8 + Math.random() * 0.8;  // 0.8-1.6
    utterance.rate = 0.7 + Math.random() * 0.6;   // 0.7-1.3

    // Add delay between lines
    setTimeout(() =&amp;gt; {
      speechSynthesis.speak(utterance);
    }, i * 2000);
  });
}

// Read a poem with varying voice characteristics
const poem = `Roses are red
Violets are blue
This poem sounds weird
Because pitch is askew`;

readPoetically(poem);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Other creative ideas:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Voice-based games&lt;/strong&gt;: Speak clues in a mystery game, or have enemies taunt players&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Data sonification&lt;/strong&gt;: &amp;quot;Speak&amp;quot; numbers from charts to make data more accessible&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Generative music&lt;/strong&gt;: Use speech as a rhythmic or melodic element&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Interactive art&lt;/strong&gt;: Create installations that respond to user input with synthesized speech&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The key is experimentation. Try combining speech with other web APIs - like the Web Audio API for effects, Canvas for visualizations, or Gamepad API for voice-controlled games.&lt;/p&gt;
&lt;h2&gt;Browser Compatibility &amp;amp; Gotchas&lt;/h2&gt;
&lt;p&gt;The Speech Synthesis API is widely supported, but with significant differences in implementation quality and available features. Let&amp;#39;s dig into the details so you know what to expect.&lt;/p&gt;
&lt;h3&gt;Support Matrix&lt;/h3&gt;
&lt;p&gt;Here&amp;#39;s a breakdown of feature support across major browsers:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Chrome&lt;/th&gt;
&lt;th&gt;Firefox&lt;/th&gt;
&lt;th&gt;Safari&lt;/th&gt;
&lt;th&gt;Edge&lt;/th&gt;
&lt;th&gt;iOS Safari&lt;/th&gt;
&lt;th&gt;Android Chrome&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Basic synthesis&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Voice selection&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;⚠️ Limited&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;⚠️ Very Limited&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full pitch/rate range&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;⚠️ Clamped&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;⚠️ Clamped&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;onboundary&lt;/code&gt; event&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pause()&lt;/code&gt;/&lt;code&gt;resume()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;⚠️ Buggy&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;✅ = Fully supported
⚠️ = Partially supported or has quirks
❌ = Not supported&lt;/p&gt;
&lt;h3&gt;Platform-Specific Quirks&lt;/h3&gt;
&lt;h4&gt;iOS Safari&lt;/h4&gt;
&lt;p&gt;iOS Safari has the most limitations:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Very few voices&lt;/strong&gt;: Often just 2-3 voices available (compared to 80+ on desktop)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Requires user interaction&lt;/strong&gt;: Speech won&amp;#39;t work until the user has interacted with the page (click, tap, etc.)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No background playback&lt;/strong&gt;: Speech stops when the app is backgrounded&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Parameter clamping&lt;/strong&gt;: Pitch and rate values are more restricted than on desktop&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pause/resume issues&lt;/strong&gt;: The pause() and resume() methods can be unreliable&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// iOS-friendly pattern: trigger speech from a user event
button.addEventListener(&amp;#39;click&amp;#39;, () =&amp;gt; {
  const utterance = new SpeechSynthesisUtterance(&amp;quot;Hello!&amp;quot;);
  speechSynthesis.speak(utterance);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Android&lt;/h4&gt;
&lt;p&gt;Android&amp;#39;s implementation varies based on the system TTS engine:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;System-dependent voices&lt;/strong&gt;: Voice quality and selection depend on what TTS engines the user has installed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Google voices are common&lt;/strong&gt;: Most Android devices have Google TTS pre-installed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Generally good support&lt;/strong&gt;: Most features work as expected on modern Android versions&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Desktop Browsers&lt;/h4&gt;
&lt;p&gt;Desktop browsers generally have the best support:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Chrome/Edge&lt;/strong&gt;: Excellent support, extensive voice libraries (Google voices + system voices)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Firefox&lt;/strong&gt;: Good support, but lacks &lt;code&gt;onboundary&lt;/code&gt; event&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Safari&lt;/strong&gt;: Good support, but limited to system voices (high quality, but fewer options)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Common Gotchas&lt;/h3&gt;
&lt;h4&gt;1. Voice Loading Timing&lt;/h4&gt;
&lt;p&gt;We covered this earlier, but it&amp;#39;s worth repeating: voices load asynchronously in most browsers. Always use the &lt;code&gt;voiceschanged&lt;/code&gt; event:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;speechSynthesis.addEventListener(&amp;#39;voiceschanged&amp;#39;, () =&amp;gt; {
  const voices = speechSynthesis.getVoices();
  // Now you can use voices
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2. Queue Behavior&lt;/h4&gt;
&lt;p&gt;The speech queue can sometimes get stuck, especially when rapidly calling &lt;code&gt;speak()&lt;/code&gt; multiple times. Always cancel before speaking new text:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Clear the queue if things get stuck
speechSynthesis.cancel();

// Then speak your new text
const utterance = new SpeechSynthesisUtterance(&amp;quot;New text&amp;quot;);
speechSynthesis.speak(utterance);
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3. User Interaction Requirements&lt;/h4&gt;
&lt;p&gt;Many mobile browsers (especially iOS Safari) require user interaction before allowing speech synthesis. This is similar to autoplay restrictions for video and audio:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// This might not work on page load
speechSynthesis.speak(new SpeechSynthesisUtterance(&amp;quot;Hello!&amp;quot;));

// This will work after a user click
button.addEventListener(&amp;#39;click&amp;#39;, () =&amp;gt; {
  speechSynthesis.speak(new SpeechSynthesisUtterance(&amp;quot;Hello!&amp;quot;));
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;4. Long Text Truncation&lt;/h4&gt;
&lt;p&gt;Some browsers (notably Chrome on some platforms) may cut off text after ~200-300 characters. The workaround is to chunk your text:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Workaround: chunk long text
function speakLongText(text) {
  // Split into ~200 character chunks at sentence boundaries
  const chunks = text.match(/.{1,200}/g) || [text];

  chunks.forEach((chunk, i) =&amp;gt; {
    const utterance = new SpeechSynthesisUtterance(chunk);
    if (i === chunks.length - 1) {
      utterance.onend = () =&amp;gt; console.log(&amp;#39;Complete!&amp;#39;);
    }
    speechSynthesis.speak(utterance);
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;5. Rate Limits&lt;/h4&gt;
&lt;p&gt;Some browsers may rate-limit or restrict speech synthesis if called too frequently. Be mindful of how often you&amp;#39;re triggering speech, especially in response to user input.&lt;/p&gt;
&lt;h3&gt;Feature Detection &amp;amp; Fallbacks&lt;/h3&gt;
&lt;p&gt;Always check for browser support before using the API:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;if (&amp;#39;speechSynthesis&amp;#39; in window) {
  // Use speech synthesis
  const utterance = new SpeechSynthesisUtterance(text);
  speechSynthesis.speak(utterance);
} else {
  // Fallback: show text in a modal, use audio files, etc.
  showTextFallback(text);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can also check for specific features:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Check if onboundary is supported
const utterance = new SpeechSynthesisUtterance();
const hasBoundary = &amp;#39;onboundary&amp;#39; in utterance;

if (hasBoundary) {
  // Use word highlighting features
} else {
  // Skip word-by-word tracking
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The key takeaway: test your implementation across different platforms, especially if you&amp;#39;re targeting mobile users. What works perfectly on desktop Chrome might need adjustments for iOS Safari.&lt;/p&gt;
&lt;h2&gt;Wrapping Up&lt;/h2&gt;
&lt;p&gt;We&amp;#39;ve covered a lot of ground! From the basics of creating your first utterance to advanced patterns with event handling, voice selection, and creative applications. Here&amp;#39;s what we explored:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The fundamentals&lt;/strong&gt;: How &lt;code&gt;speechSynthesis&lt;/code&gt; and &lt;code&gt;SpeechSynthesisUtterance&lt;/code&gt; work together&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Customization&lt;/strong&gt;: Adjusting pitch, rate, and volume to create different effects&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Voice selection&lt;/strong&gt;: Browsing and choosing from available voices (and handling the async loading quirk)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Events&lt;/strong&gt;: Tracking speech lifecycle with &lt;code&gt;onstart&lt;/code&gt;, &lt;code&gt;onend&lt;/code&gt;, &lt;code&gt;onboundary&lt;/code&gt;, and other events&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Creative applications&lt;/strong&gt;: Interactive storytelling, language learning, and experimental uses&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Browser quirks&lt;/strong&gt;: Platform-specific limitations and workarounds&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The Speech Synthesis API is just one half of the Web Speech API. The other half is the &lt;strong&gt;Speech Recognition API&lt;/strong&gt;, which does the opposite - it listens to spoken words and converts them to text. Combine both, and you can create fully voice-interactive applications.&lt;/p&gt;
&lt;p&gt;This technology is mature enough for production use, but remember to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Test across multiple browsers and devices&lt;/li&gt;
&lt;li&gt;Provide fallbacks for unsupported browsers&lt;/li&gt;
&lt;li&gt;Consider accessibility implications&lt;/li&gt;
&lt;li&gt;Respect user preferences (some users may find unexpected speech jarring)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Resources&lt;/h3&gt;
&lt;p&gt;Want to dive deeper? Here are some helpful resources:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/SpeechSynthesis&quot;&gt;MDN: SpeechSynthesis API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/SpeechSynthesisUtterance&quot;&gt;MDN: SpeechSynthesisUtterance&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://webaudio.github.io/web-speech-api/&quot;&gt;Web Speech API Specification&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://caniuse.com/speech-synthesis&quot;&gt;Can I Use: Speech Synthesis&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><media:content type="image/png" width="1200" height="630" medium="image" url="https://andrewusher.dev/open-graph/give-your-web-app-a-voice.png"/><category>javascript</category><category>web-api</category><category>speech-synthesis</category><category>interactive</category></item><item><title>Adding Estimated Reading Time to an Astro Blog</title><link>https://andrewusher.dev/blog/adding-reading-time-to-astro-blog/</link><guid isPermaLink="true">https://andrewusher.dev/blog/adding-reading-time-to-astro-blog/</guid><description>Reading time estimates have become a standard UX feature on modern blogs. They set reader expectations, help people decide if they have time to read a...</description><pubDate>Mon, 08 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Reading time estimates have become a standard UX feature on modern blogs. They set reader expectations, help people decide if they have time to read an article, and improve engagement. Adding this feature to an Astro blog is straightforward and can be done with a simple utility function and a reusable component.&lt;/p&gt;
&lt;h2&gt;The Core Algorithm&lt;/h2&gt;
&lt;p&gt;The foundation of reading time estimation is a utility function that counts words and calculates reading duration. The standard approach assumes readers process about 200 words per minute.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;/**
 * Calculate estimated reading time for text content
 * @param content - Raw markdown or text content
 * @param wordsPerMinute - Reading speed (default: 200 wpm)
 * @returns Object with minutes and display string
 */

  content: string,
  wordsPerMinute = 200
): { minutes: number; display: string } {
  // Remove markdown syntax, code blocks, and extra whitespace
  const cleanContent = content
    .replace(/```[\s\S]*?```/g, &amp;#39;&amp;#39;) // Remove code blocks
    .replace(/`[^`]*`/g, &amp;#39;&amp;#39;) // Remove inline code
    .replace(/[#*_~[\]()]/g, &amp;#39;&amp;#39;) // Remove markdown symbols
    .replace(/\s+/g, &amp;#39; &amp;#39;) // Normalize whitespace
    .trim()

  // Count words
  const wordCount = cleanContent.split(/\s+/).length

  // Calculate reading time
  const minutes = Math.ceil(wordCount / wordsPerMinute)

  // Format display string
  const display = minutes &amp;lt; 1 ? &amp;#39;&amp;lt; 1 min read&amp;#39; : `${minutes} min read`

  return { minutes, display }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The function handles several important considerations:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Stripping Markdown&lt;/strong&gt;: Code blocks and inline code shouldn&amp;#39;t count toward word count since they&amp;#39;re not prose. Similarly, markdown syntax characters are removed to get accurate word counts.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Whitespace Normalization&lt;/strong&gt;: Multiple spaces or newlines are collapsed into single spaces to ensure consistent word splitting.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Rounding&lt;/strong&gt;: Using &lt;code&gt;Math.ceil&lt;/code&gt; rounds up, so even a 201-word article shows &amp;quot;2 min read&amp;quot; rather than &amp;quot;1 min read&amp;quot;. This errs on the side of giving readers slightly more time than needed.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Return Format&lt;/strong&gt;: The function returns both the numeric minutes and a formatted display string for flexibility in how you present the information.&lt;/p&gt;
&lt;h2&gt;The Display Component&lt;/h2&gt;
&lt;p&gt;With the calculation logic in place, a small Astro component handles rendering the reading time consistently across your blog:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-astro&quot;&gt;---
interface Props {
  minutes: number
  display?: string
  className?: string
}

const { minutes, display, className = &amp;#39;text-sm tracking-wide text-slate-700 dark:text-slate-500&amp;#39; } = Astro.props

// Generate display string if not provided
const readingTimeText = display ?? (minutes &amp;lt; 1 ? &amp;#39;&amp;lt; 1 min read&amp;#39; : `${minutes} min read`)
---

&amp;lt;span class={className}&amp;gt;{readingTimeText}&amp;lt;/span&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This component is intentionally simple. It accepts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;minutes&lt;/code&gt;: The calculated reading time&lt;/li&gt;
&lt;li&gt;&lt;code&gt;display&lt;/code&gt;: Optional pre-formatted display string (useful if you want custom text)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;className&lt;/code&gt;: Optional custom Tailwind classes for styling flexibility&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;By keeping the component minimal and reusable, you can drop it into any location in your blog layout without duplication.&lt;/p&gt;
&lt;h2&gt;Integration with Your Blog&lt;/h2&gt;
&lt;p&gt;The real benefit comes from integrating this into your Astro Content Collections. When you fetch blog posts using &lt;code&gt;getCollection(&amp;#39;blogPosts&amp;#39;)&lt;/code&gt;, each entry includes the raw markdown content in its &lt;code&gt;body&lt;/code&gt; property.&lt;/p&gt;
&lt;p&gt;In your blog listing page, calculate reading time for each post:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const blogPostsWithReadingTime = blogPosts.map(post =&amp;gt; ({
  ...post,
  readingTime: calculateReadingTime(post.body)
}))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then display it alongside the publish date. On individual post pages, add the reading time to the hero header where you show the title and date.&lt;/p&gt;
&lt;h2&gt;The Benefits&lt;/h2&gt;
&lt;p&gt;This approach offers several advantages:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Build-time Calculation&lt;/strong&gt;: Reading time is calculated once during the build, not on every page load. There&amp;#39;s zero runtime overhead.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Accurate for Your Content&lt;/strong&gt;: You can adjust the words-per-minute parameter for your audience if needed. Technical audiences might read slower, while shorter content might skew differently.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Flexible Presentation&lt;/strong&gt;: The component accepts custom classes, so you can style reading time differently in various locations (subtle on listings, prominent on individual posts).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Accessible&lt;/strong&gt;: The calculation is based on actual word count from your markdown, not arbitrary estimates, making it reliable and consistent across your blog.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Adding reading time to your blog is a small feature that makes a meaningful difference in reader experience. With Astro&amp;#39;s content collections and a simple utility function, it requires surprisingly little code.&lt;/p&gt;
</content:encoded><media:content type="image/png" width="1200" height="630" medium="image" url="https://andrewusher.dev/open-graph/adding-reading-time-to-astro-blog.png"/><category>astro</category><category>typescript</category><category>web-development</category><category>tutorial</category></item><item><title>Advanced VSCode Search: Finding Function Calls While Excluding Variable Assignments</title><link>https://andrewusher.dev/blog/vscode-search/</link><guid isPermaLink="true">https://andrewusher.dev/blog/vscode-search/</guid><description>As developers, we often need to search through our codebase for specific patterns. One common scenario is finding function calls while filtering out v...</description><pubDate>Thu, 06 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;As developers, we often need to search through our codebase for specific patterns. One common scenario is finding function calls while filtering out variable assignments. In this post, I&amp;#39;ll walk you through creating a powerful VSCode search query that finds &lt;code&gt;prefetch&lt;/code&gt; function calls while excluding lines that contain assignments.&lt;/p&gt;
&lt;h2&gt;The Challenge&lt;/h2&gt;
&lt;p&gt;Imagine you&amp;#39;re working on a large codebase and need to find all the places where &lt;code&gt;prefetch&lt;/code&gt; functions are being called directly, but you want to exclude cases where the result is being assigned to a variable or an arrow function declaration.. &lt;/p&gt;
&lt;p&gt;You want to find lines like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;prefetchData()
await prefetchUser(userId)
this.prefetchCache()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But exclude lines like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const data = prefetchData()
let user = await prefetchUser(userId)
this.cache = prefetchCache()
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;The Solution&lt;/h2&gt;
&lt;p&gt;Here&amp;#39;s the regex pattern that accomplishes this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;^(?!.*=).*prefetch.*\(
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Breaking Down the Pattern&lt;/h2&gt;
&lt;p&gt;Let&amp;#39;s dissect this regex step by step:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;^&lt;/code&gt; - Anchors the pattern to the start of the line&lt;/li&gt;
&lt;li&gt;&lt;code&gt;(?!.*=)&lt;/code&gt; - This is a &lt;strong&gt;negative lookahead&lt;/strong&gt; that ensures there&amp;#39;s no equal sign (&lt;code&gt;=&lt;/code&gt;) anywhere on the line&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.*&lt;/code&gt; - Matches any characters (except newline) before &amp;quot;prefetch&amp;quot;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;prefetch&lt;/code&gt; - Matches the literal text &amp;quot;prefetch&amp;quot;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.*&lt;/code&gt; - Matches any characters between &amp;quot;prefetch&amp;quot; and the opening parenthesis&lt;/li&gt;
&lt;li&gt;&lt;code&gt;\(&lt;/code&gt; - Matches a literal open parenthesis (escaped because &lt;code&gt;(&lt;/code&gt; has special meaning in regex)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;How to Use It in VSCode&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Open the search panel&lt;/strong&gt;: Press &lt;code&gt;Ctrl+Shift+F&lt;/code&gt; (Windows/Linux) or &lt;code&gt;Cmd+Shift+F&lt;/code&gt; (Mac)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Enable regex mode&lt;/strong&gt;: Click the regex icon (&lt;code&gt;.*&lt;/code&gt;) in the search box or press &lt;code&gt;Alt+R&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Enter the pattern&lt;/strong&gt;: Type &lt;code&gt;^(?!.*=).*prefetch.*\(&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Search&lt;/strong&gt;: Press Enter to find all matches&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Understanding Negative Lookahead&lt;/h2&gt;
&lt;p&gt;The key component here is the negative lookahead &lt;code&gt;(?!.*=)&lt;/code&gt;. This is a powerful regex feature that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Looks ahead&lt;/strong&gt; in the string without consuming characters&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Asserts that what follows doesn&amp;#39;t match&lt;/strong&gt; the specified pattern&lt;/li&gt;
&lt;li&gt;In our case, it ensures that nowhere on the current line (&lt;code&gt;.*=&lt;/code&gt;) there&amp;#39;s an equal sign&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Real-World Examples&lt;/h2&gt;
&lt;p&gt;This pattern will match:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// ✅ Direct function calls
prefetchData()
await prefetchUser(123)
component.prefetchResources()
this.prefetchCache()

// ✅ Function calls in conditions
if (prefetchData()) {
    // ...
}

// ✅ Function calls as arguments
doSomething(prefetchUser(id))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But will exclude:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// ❌ Variable assignments
const result = prefetchData()
let user = await prefetchUser(123)
this.data = prefetchCache()

// ❌ Object property assignments
obj.prop = prefetchSomething()
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Extending the Pattern&lt;/h2&gt;
&lt;p&gt;You can easily modify this pattern for other use cases:&lt;/p&gt;
&lt;h3&gt;Find any function call (not just prefetch):&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;^(?!.*=).*\w+.*\(
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Exclude multiple operators (assignment and comparison):&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;^(?!.*[=&amp;lt;&amp;gt;]).*prefetch.*\(
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Case-insensitive search:&lt;/h3&gt;
&lt;p&gt;Add the &lt;code&gt;i&lt;/code&gt; flag or use &lt;code&gt;(?i)&lt;/code&gt; at the beginning:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;^(?!.*=).*(?i)prefetch.*\(
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Tips for Complex Searches&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Test your regex&lt;/strong&gt;: Use online regex testers like regex101.com to validate your patterns&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use word boundaries&lt;/strong&gt;: Add &lt;code&gt;\b&lt;/code&gt; around words to avoid partial matches&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Escape special characters&lt;/strong&gt;: Remember to escape characters like &lt;code&gt;(&lt;/code&gt;, &lt;code&gt;)&lt;/code&gt;, &lt;code&gt;[&lt;/code&gt;, &lt;code&gt;]&lt;/code&gt;, etc.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Save frequent searches&lt;/strong&gt;: VSCode allows you to save search queries for reuse&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Have you used similar search patterns in your development workflow? Share your favorite VSCode search tips in the comments below!&lt;/em&gt;&lt;/p&gt;
</content:encoded><media:content type="image/png" width="1200" height="630" medium="image" url="https://andrewusher.dev/open-graph/vscode-search.png"/><category>vscode</category><category>productivity</category><category>tutorial</category></item><item><title>Simplifying VSCode</title><link>https://andrewusher.dev/blog/simplifying-vscode/</link><guid isPermaLink="true">https://andrewusher.dev/blog/simplifying-vscode/</guid><description>Introduction As a long-time user of Visual Studio Code (VSCode), I&apos;ve accumulated a vast array of extensions, settings, and files that have made my wo...</description><pubDate>Sun, 11 Jun 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;As a long-time user of Visual Studio Code (VSCode), I&amp;#39;ve accumulated a vast array of extensions, settings, and files that have made my workspace cluttered and overwhelming. I found myself spending more time searching for files and navigating through menus than actually coding. It was time for a change.&lt;/p&gt;
&lt;p&gt;In this blog post, I&amp;#39;ll share my experience of cleaning up and simplifying my VSCode workspace. I&amp;#39;ll discuss the steps I took to remove unused extensions, organize my files, customize settings, and use keyboard shortcuts and snippets to streamline my workflow. By the end of this post, you&amp;#39;ll have a better understanding of how to optimize your workspace and improve your coding experience. So, let&amp;#39;s dive in!&lt;/p&gt;
&lt;h2&gt;Extensions I Removed&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://marketplace.visualstudio.com/items?itemName=codecrumbs.codecrumbs-vs-code&quot;&gt;codecrumbs-vs-code&lt;/a&gt; - Never used it after the first few weeks of installing&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://marketplace.visualstudio.com/items?itemName=vsls-contrib.codetour&quot;&gt;Codetour&lt;/a&gt; - Same as above&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://marketplace.visualstudio.com/items?itemName=wix.vscode-import-cost&quot;&gt;Import Cost&lt;/a&gt; - I use Bundlephobia for this instead&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://marketplace.visualstudio.com/items?itemName=ms-playwright.playwright&quot;&gt;Playwright Test for VSCode&lt;/a&gt; - I use &lt;a href=&quot;https://playwright.dev/docs/test-ui-mode&quot;&gt;Playwright&amp;#39;s UI mode&lt;/a&gt; for this functionality now&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://marketplace.visualstudio.com/items?itemName=pnp.polacode&quot;&gt;Polacode&lt;/a&gt; - I use &lt;a href=&quot;https://ray.so/&quot;&gt;ray.so&lt;/a&gt; instead now&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://marketplace.visualstudio.com/items?itemName=ReacTreeDev.reactree&quot;&gt;ReacTree&lt;/a&gt; - Seemed cool when I looked at it, but I&amp;#39;ve never had a use case for it&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://marketplace.visualstudio.com/items?itemName=SamuelCharpentier.remove-non-ascii-chars&quot;&gt;Remove Non ASCII Chars&lt;/a&gt; - The linting tools I use already handles this&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Status Bar Sections Removed&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Workspace Trust&lt;/li&gt;
&lt;li&gt;GitLens Commit Graph&lt;/li&gt;
&lt;li&gt;Problems&lt;/li&gt;
&lt;li&gt;Editor Selection&lt;/li&gt;
&lt;li&gt;Editor Indentation&lt;/li&gt;
&lt;li&gt;Editor Encoding&lt;/li&gt;
&lt;li&gt;Editor End of Line&lt;/li&gt;
&lt;li&gt;Editor Language Status&lt;/li&gt;
&lt;li&gt;Editor Language&lt;/li&gt;
&lt;li&gt;Prettier&lt;/li&gt;
&lt;li&gt;Feedback&lt;/li&gt;
&lt;li&gt;Notifications&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Sidebar Icons Removed&lt;/h2&gt;
&lt;p&gt;After years of use, the sidebar in VSCode can become cluttered with icons, making it difficult to navigate and find what you need. To simplify my workspace, I recently removed the below sidebar icons:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Run and Debug&lt;/li&gt;
&lt;li&gt;Extensions&lt;/li&gt;
&lt;li&gt;GitLab Workflow&lt;/li&gt;
&lt;li&gt;GitLens&lt;/li&gt;
&lt;li&gt;TODOs&lt;/li&gt;
&lt;li&gt;Bookmarks&lt;/li&gt;
&lt;li&gt;Accounts&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;In conclusion, by removing unused extensions, status bar sections, and sidebar icons, as well as organizing files, customizing settings, and using keyboard shortcuts and snippets, I was able to simplify my VSCode workspace and improve my coding experience. It&amp;#39;s important to regularly review and optimize your workspace to ensure that it remains efficient and clutter-free.&lt;/p&gt;
</content:encoded><media:content type="image/png" width="1200" height="630" medium="image" url="https://andrewusher.dev/open-graph/simplifying-vscode.png"/><category>vscode</category><category>productivity</category></item><item><title>Updating from @sveltejs/kit@1.0.0-next.350 to @sveltejs/kit@1.0.0-next.403</title><link>https://andrewusher.dev/blog/updating-vite-to-patch-403/</link><guid isPermaLink="true">https://andrewusher.dev/blog/updating-vite-to-patch-403/</guid><description>TLDR: Here&apos;s the commit that I made all code changes in I recently updated the version of sveltekit I&apos;m using for qr-gen, and it was quite the experie...</description><pubDate>Sat, 06 Aug 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;TLDR: &lt;a href=&quot;https://github.com/AndrewUsher/qr-gen/commit/612b7c76710e662f8c9adab22f3753f459f2edb5&quot;&gt;Here&amp;#39;s the commit that I made all code changes in&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I recently updated the version of sveltekit I&amp;#39;m using for &lt;a href=&quot;https://qr-gen-murex.vercel.app/&quot;&gt;qr-gen&lt;/a&gt;, and it was quite the experience :). There&amp;#39;s not much documentation out there (at least that I could find), so I&amp;#39;m documenting what I needed to change here.&lt;/p&gt;
&lt;h2&gt;Update dependencies&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;yarn add @sveltejs/kit@latest @sveltejs/adapter-auto@latest --exact
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note: if you&amp;#39;re using any other packages under the &lt;code&gt;@sveltejs&lt;/code&gt; org, you&amp;#39;ll likely need to update those as well.&lt;/p&gt;
&lt;h2&gt;Add vite.config.js&lt;/h2&gt;
&lt;p&gt;Next, I needed to create &lt;code&gt;vite.config.js&lt;/code&gt; in the root of the repo and add the svelte-kit plugin to the config:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;
/** @type {import(&amp;#39;vite&amp;#39;).UserConfig} */
const config = {
  plugins: [sveltekit()]
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You&amp;#39;ll also need to remove any configuration you have under the &lt;code&gt;kit.vite&lt;/code&gt; property in &lt;code&gt;svelte.config.js&lt;/code&gt;. &lt;a href=&quot;https://vitejs.dev/config/&quot;&gt;Consulting the vite configuration docs&lt;/a&gt; should be helpful here.&lt;/p&gt;
&lt;h2&gt;Update API Routes to Use Uppercase Method Names&lt;/h2&gt;
&lt;p&gt;Updating &lt;code&gt;export const post&lt;/code&gt; to &lt;code&gt;export const POST&lt;/code&gt; (and so forth for other request methods)&lt;/p&gt;
</content:encoded><media:content type="image/png" width="1200" height="630" medium="image" url="https://andrewusher.dev/open-graph/updating-vite-to-patch-403.png"/><category>javascript</category><category>web-development</category><category>tutorial</category></item><item><title>Resolving ERR_IMPORT_ASSERTION_TYPE_MISSING in Node.js</title><link>https://andrewusher.dev/blog/node-err-import-assertion-type-missing/</link><guid isPermaLink="true">https://andrewusher.dev/blog/node-err-import-assertion-type-missing/</guid><description>&lt;p&gt; While working on a CLI, I ran into the below error when trying to import  to read the package name and version: &lt;/p&gt; &lt;p&gt; To resolve the issue, we...</description><pubDate>Sat, 04 Jun 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;
While working on a CLI, I ran into the below error when trying to import `package.json` to read the package name and version:
&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;TypeError [ERR_IMPORT_ASSERTION_TYPE_MISSING]: Module &amp;quot;file:///Users/Andrew.Usher/code/gh/andrewusher/andrewusher.dev/package.json&amp;quot; needs an import assertion of type &amp;quot;json&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;
const { name, version } = pkgInfo


  name,
  version
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;
To resolve the issue, we need to use the [import assertions syntax](https://github.com/tc39/proposal-import-assertions) to tell Node that this is a JSON module:
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;import pkgInfo from &amp;#39;../package.json&amp;#39; assert { type: &amp;#39;json&amp;#39; }

const { name, version } = pkgInfo


  name,
  version
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><media:content type="image/png" width="1200" height="630" medium="image" url="https://andrewusher.dev/open-graph/node-err-import-assertion-type-missing.png"/><category>node</category><category>javascript</category><category>tutorial</category></item><item><title>offsetHeight/scrollHeight/clientHeight: What&apos;s The Difference?</title><link>https://andrewusher.dev/blog/offset-client-scroll-height-difference/</link><guid isPermaLink="true">https://andrewusher.dev/blog/offset-client-scroll-height-difference/</guid><description>While working on a scrolling progress bar for my portfolio site, I ran into some issues trying to find out the small differences between , , and  on a...</description><pubDate>Sun, 22 May 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;While working on a scrolling progress bar for my portfolio site, I ran into some issues trying to find out the small differences between &lt;code&gt;offsetHeight&lt;/code&gt;, &lt;code&gt;clientHeight&lt;/code&gt;, and &lt;code&gt;scrollHeight&lt;/code&gt; on a given element. Here&amp;#39;s a brief description of each one:&lt;/p&gt;
&lt;h2&gt;clientHeight&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;clientHeight&lt;/code&gt; can be calculated as: CSS height + CSS padding - height of horizontal scrollbar (if present)&lt;/p&gt;
&lt;h2&gt;scrollHeight&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;scrollHeight&lt;/code&gt; value is equal to the minimum height the element would require in order to fit all the content in the viewport without using a vertical scrollbar. The height is measured in the same way as clientHeight: it includes the element&amp;#39;s padding, but not its border, margin or horizontal scrollbar (if present)&lt;/p&gt;
&lt;h2&gt;offsetHeight&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;offsetHeight&lt;/code&gt; is a measurement in pixels of the element&amp;#39;s CSS height, including any borders, padding, and horizontal scrollbars (if rendered). It does not include the height of pseudo-elements such as ::before or ::after&lt;/p&gt;
</content:encoded><media:content type="image/png" width="1200" height="630" medium="image" url="https://andrewusher.dev/open-graph/offset-client-scroll-height-difference.png"/><category>javascript</category><category>web-development</category><category>tutorial</category></item><item><title>How To Use Default Function Parameter Values In JS</title><link>https://andrewusher.dev/blog/js-default-function-params/</link><guid isPermaLink="true">https://andrewusher.dev/blog/js-default-function-params/</guid><description>Function parameters are undefined by default in JavaScript. Sometimes, you want to define a default parameter in this case. Before ES6 (also known as...</description><pubDate>Sat, 07 May 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Function parameters are undefined by default in JavaScript. Sometimes, you want to define a default parameter in this case. Before &lt;a href=&quot;http://es6-features.org/#Constants&quot;&gt;ES6&lt;/a&gt; (also known as ES2015), creating default paramaters was a little tedious:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;function createName(firstName, lastName) {
  firstName = typeof firstName === &amp;#39;undefined&amp;#39; ? &amp;#39;Jane&amp;#39; : firstName;
  secondName = typeof secondName === &amp;#39;undefined&amp;#39; ? &amp;#39;Doe&amp;#39; : secondName;

  return firstName + &amp;#39; &amp;#39; + secondName;
}

console.log(createName()); // Jane Doe
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With the introduction of default parameter values in ES6, the above could be simplified to:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;function createName(firstName = &amp;#39;Jane&amp;#39;, lastName = &amp;#39;Doe&amp;#39;) {  
  return firstName + &amp;#39; &amp;#39; + secondName;
}

console.log(createName()); // Jane Doe
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><media:content type="image/png" width="1200" height="630" medium="image" url="https://andrewusher.dev/open-graph/js-default-function-params.png"/><category>javascript</category><category>tutorial</category></item><item><title>NPM Needs: snarkdown</title><link>https://andrewusher.dev/blog/npm-needs-snarkdown/</link><guid isPermaLink="true">https://andrewusher.dev/blog/npm-needs-snarkdown/</guid><description>What Is It?  is a barebones markdown parser with the sole purpose of converting markdown to HTML. This could be used in cases where you don&apos;t need a f...</description><pubDate>Sun, 10 Apr 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;What Is It?&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;snarkdown&lt;/code&gt; is a barebones markdown parser with the sole purpose of converting markdown to HTML. This could be used in cases where you don&amp;#39;t need a full fledged tool like &lt;a href=&quot;https://github.com/markedjs/marked&quot;&gt;&lt;code&gt;marked&lt;/code&gt;&lt;/a&gt; or &lt;a href=&quot;https://github.com/remarkjs/remark&quot;&gt;&lt;code&gt;remark&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Usage Examples&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;
console.log(snarkdown(&amp;#39;# Hello World&amp;#39;))
// &amp;#39;&amp;lt;h1&amp;gt;Hello World&amp;lt;/h1&amp;gt;&amp;#39;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;More Info&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Github Repo: &lt;a href=&quot;https://github.com/developit/snarkdown&quot;&gt;developit/snarkdown&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Author: &lt;a href=&quot;https://github.com/developit&quot;&gt;Jason Miller&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><media:content type="image/png" width="1200" height="630" medium="image" url="https://andrewusher.dev/open-graph/npm-needs-snarkdown.png"/><category>npm</category><category>javascript</category></item><item><title>NPM Needs: pretty-bytes</title><link>https://andrewusher.dev/blog/npm-needs-pretty-bytes/</link><guid isPermaLink="true">https://andrewusher.dev/blog/npm-needs-pretty-bytes/</guid><description>What Is It?  is a utility used to convert an integer form of bytes to a more human readable form (bytes to kilobytes, for example).  is a command line...</description><pubDate>Sat, 09 Apr 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;What Is It?&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;pretty-bytes&lt;/code&gt; is a utility used to convert an integer form of bytes to a more human readable form (bytes to kilobytes, for example). &lt;code&gt;pretty-bytes-cli&lt;/code&gt; is a command line wrapper for the same package.&lt;/p&gt;
&lt;h2&gt;Usage Examples&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;
prettyBytes(100)
// &amp;#39;100 B&amp;#39;
prettyBytes(1650)
// &amp;#39;1.65 kB&amp;#39;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;More Info&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Github Repo: &lt;a href=&quot;https://github.com/sindresorhus/pretty-bytes&quot;&gt;sindresorhus/pretty-bytes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Author: &lt;a href=&quot;https://sindresorhus.com/&quot;&gt;Sindre Sorhus&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><media:content type="image/png" width="1200" height="630" medium="image" url="https://andrewusher.dev/open-graph/npm-needs-pretty-bytes.png"/><category>npm</category><category>javascript</category></item><item><title>A Fastify Quickstart</title><link>https://andrewusher.dev/blog/fastify-quickstart/</link><guid isPermaLink="true">https://andrewusher.dev/blog/fastify-quickstart/</guid><description>This post will cover instantiating a basic server using [](https://github.com/fastify/fastify).  Getting Started First, we&apos;ll need to setup a new fold...</description><pubDate>Wed, 05 Jan 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;This post will cover instantiating a basic server using &lt;a href=&quot;https://github.com/fastify/fastify&quot;&gt;&lt;code&gt;fastify&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Getting Started&lt;/h2&gt;
&lt;p&gt;First, we&amp;#39;ll need to setup a new folder for this project. Open up a terminal, and type these commands to get setup.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;mkdir fastify-quickstart
cd fastify-quickstart
npm init -y
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Installing Fastify&lt;/h2&gt;
&lt;p&gt;After that, we need to install &lt;code&gt;fastify&lt;/code&gt; with the below command:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;npm i fastify
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;TLDR: The Code&lt;/h2&gt;
&lt;p&gt;For some quick code to copy and paste, the snippet below will be enough to get you started:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// server.js
const fastify = require(&amp;#39;fastify&amp;#39;)({ logger: true })

fastify.get(&amp;#39;/&amp;#39;, async (request, reply) =&amp;gt; {
  return {
    message: &amp;#39;Hello World!&amp;#39;
  }
})

const startServer = async () =&amp;gt; {
  try {
    await fastify.listen(3000)
  } catch (err) {
    fastify.log.error(err)
    process.exit(1)
  }
}

startServer()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To run the server now, type &lt;code&gt;node server.js&lt;/code&gt; in your terminal.&lt;/p&gt;
&lt;p&gt;You can test that things are working as expected by running &lt;code&gt;curl&lt;/code&gt; in your terminal.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; curl http://localhost:3000
{&amp;quot;message&amp;quot;:&amp;quot;Hello World!&amp;quot;} 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For more info on additional methods, configuration, etc., &lt;a href=&quot;https://www.fastify.io/docs/latest/&quot;&gt;take a look at the Fastify documentation!&lt;/a&gt;&lt;/p&gt;
</content:encoded><media:content type="image/png" width="1200" height="630" medium="image" url="https://andrewusher.dev/open-graph/fastify-quickstart.png"/><category>node</category><category>javascript</category><category>web-development</category><category>tutorial</category></item><item><title>Web Pointer Events</title><link>https://andrewusher.dev/blog/web-pointer-events/</link><guid isPermaLink="true">https://andrewusher.dev/blog/web-pointer-events/</guid><description>Pointing at things on the web used to be simple. You had a mouse, you moved it around, sometimes you pushed buttons, and that was it. But this, doesn&apos;...</description><pubDate>Sat, 14 Aug 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Pointing at things on the web used to be simple. You had a mouse, you moved it around, sometimes you pushed buttons, and that was it. But this, doesn&amp;#39;t work so well on here.&lt;/p&gt;
&lt;p&gt;Touch events are good, but to support &lt;a href=&quot;https://www.w3.org/TR/touch-events/&quot;&gt;touch&lt;/a&gt; and &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent&quot;&gt;mouse&lt;/a&gt;, you had to support two event models:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;elem.addEventListener(&amp;#39;mousemove&amp;#39;, mouseMoveEvent);
elem.addEventListener(&amp;#39;touchmove&amp;#39;, touchMoveEvent);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Chrome now enables unified input handling by dispatching &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent&quot;&gt;PointerEvents&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;elem.addEventListener(&amp;#39;pointermove&amp;#39;, pointerMoveEvent);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Pointer events unify the pointer input model for the browser, bringing touch, pens, and mice together into a single set of events. They&amp;#39;re supported in &lt;a href=&quot;https://caniuse.com/pointer&quot;&gt;IE11, Edge, Chrome, Opera and partially supported in Firefox&lt;/a&gt;.&lt;/p&gt;
&lt;picture&gt;
&lt;source type=&quot;image/webp&quot; srcset=&quot;https://caniuse.bitsofco.de/image/pointer.webp&quot;&gt;
&lt;source type=&quot;image/png&quot; srcset=&quot;https://caniuse.bitsofco.de/image/pointer.png&quot;&gt;
&lt;img src=&quot;https://caniuse.bitsofco.de/image/pointer.jpg&quot; alt=&quot;Data on support for the pointer feature across the major browsers from caniuse.com&quot;&gt;
&lt;/picture&gt;</content:encoded><media:content type="image/png" width="1200" height="630" medium="image" url="https://andrewusher.dev/open-graph/web-pointer-events.png"/><category>javascript</category><category>web-development</category></item><item><title>useWindowSize</title><link>https://andrewusher.dev/blog/use-window-size/</link><guid isPermaLink="true">https://andrewusher.dev/blog/use-window-size/</guid><description>A really common need is to get the current size of the browser window. This hook returns an object containing the window&apos;s width and height. If execut...</description><pubDate>Sat, 06 Feb 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A really common need is to get the current size of the browser window. This hook returns an object containing the window&amp;#39;s width and height. If executed server-side (no window object) the value of width and height will be undefined.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;
const Example = () =&amp;gt; {
  const { height, width } = useWindowSize()

  return (
    &amp;lt;div&amp;gt;
      {width}px / {height}px
    &amp;lt;/div&amp;gt;
  )
}

// Hook
function useWindowSize() {
  const isBrowser = typeof window !== &amp;#39;undefined&amp;#39;

  const getSize = () =&amp;gt; ({
    width: isBrowser ? window.innerWidth : undefined,
    height: isBrowser ? window.innerHeight : undefined,
  })

  const handleResize = () =&amp;gt; {
    setWindowSize(getSize())
  }

  const [windowSize, setWindowSize] = useState(getSize)

  useEffect(() =&amp;gt; {
    if (!isBrowser) return
    window.addEventListener(&amp;#39;resize&amp;#39;, handleResize)
    return () =&amp;gt; window.removeEventListener(&amp;#39;resize&amp;#39;, handleResize)
  }, [])

  return windowSize
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><media:content type="image/png" width="1200" height="630" medium="image" url="https://andrewusher.dev/open-graph/use-window-size.png"/><category>react</category><category>javascript</category><category>tutorial</category></item><item><title>Lost Some Commits I Know I Made</title><link>https://andrewusher.dev/blog/lost-some-commits-i-know-i-made/</link><guid isPermaLink="true">https://andrewusher.dev/blog/lost-some-commits-i-know-i-made/</guid><description>First make sure that it was not on a different branch. Try  where &quot;bar&quot; is replaced with something unique in the commits you made. You can also search...</description><pubDate>Sat, 23 Feb 2019 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;First make sure that it was not on a different branch. Try &lt;code&gt;git log -Sbar --all&lt;/code&gt; where &amp;quot;bar&amp;quot; is replaced with something unique in the commits you made. You can also search with &lt;code&gt;gitk --all --date-order&lt;/code&gt; to see if anything looks likely.Check your stashes, &lt;code&gt;git stash list&lt;/code&gt;, to see if you might have stashed instead of committing. You can also visualize what the stashes might be associated with via:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;gitk --all --date-order $(git stash list | awk -F: &amp;#39;{print $1};&amp;#39;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, you should probably look in other repositories you have lying around including ones on other hosts and in testing environments, and in your backups.Once you are fully convinced that it is well and truly lost, you can start looking elsewhere in git. Specifically, you should first look at the reflog which contains the history of what happened to the tip of your branches for the past two weeks or so. You can of course say &lt;code&gt;git log -g&lt;/code&gt; or &lt;code&gt;git reflog&lt;/code&gt; to view it, but it may be best visualized with:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;gitk --all --date-order $(git reflog --pretty=%H)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next you can look in git&amp;#39;s lost and found. Dangling commits get generated for many good reasons including resets and rebases. Still those activities might have mislaid the commits you were interested in. These might be best visualized with &lt;code&gt;gitk --all --date-order $(git fsck | grep &amp;quot;dangling commit&amp;quot; | awk &amp;#39;{print $3;}&amp;#39;)&lt;/code&gt;The last place you can look is in dangling blobs. These are files which have been &lt;code&gt;git add&lt;/code&gt;ed but not attached to a commit for some (usually innocuous) reason. To look at the files, one at a time, run:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;git fsck | grep &amp;quot;dangling blob&amp;quot; | while read x x s; do git show $s | less; done
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once you find the changes you are interested in, there are several ways you can proceed. You can &lt;code&gt;git reset --hard SHA&lt;/code&gt; your current branch to the history and current state of that SHA (probably not recommended for stashes), you can &lt;code&gt;git branch newbranch SHA&lt;/code&gt; to link the old history to a new branch name (also not recommended for stashes), you can &lt;code&gt;git stash apply SHA&lt;/code&gt; (for the non-index commit in a git-stash), you can &lt;code&gt;git stash merge SHA&lt;/code&gt; or &lt;code&gt;git cherry-pick SHA&lt;/code&gt; (for either part of a stash or non-stashes), etc.&lt;/p&gt;
</content:encoded><media:content type="image/png" width="1200" height="630" medium="image" url="https://andrewusher.dev/open-graph/lost-some-commits-i-know-i-made.png"/><category>git</category><category>tutorial</category></item><item><title>Edit Styles Inline On A Page</title><link>https://andrewusher.dev/blog/edit-styles-inline-on-a-page/</link><guid isPermaLink="true">https://andrewusher.dev/blog/edit-styles-inline-on-a-page/</guid><pubDate>Tue, 19 Feb 2019 00:00:00 GMT</pubDate><content:encoded>&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;style contenteditable style=&amp;quot;display: block; white-space: pre;&amp;quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><media:content type="image/png" width="1200" height="630" medium="image" url="https://andrewusher.dev/open-graph/edit-styles-inline-on-a-page.png"/><category>css</category><category>html</category><category>web-development</category></item><item><title>Reverting A Merge Commit</title><link>https://andrewusher.dev/blog/reverting-a-merge-commit/</link><guid isPermaLink="true">https://andrewusher.dev/blog/reverting-a-merge-commit/</guid><description>Oh dear. This is going to get complicated.To create an positive commit to remove the effects of a merge commit, you must first identify the SHA of the...</description><pubDate>Wed, 23 Jan 2019 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Oh dear. This is going to get complicated.To create an positive commit to remove the effects of a merge commit, you must first identify the SHA of the commit you want to revert. You can do this using &lt;code&gt;gitk --date-order&lt;/code&gt; or using &lt;code&gt;git log --graph --decorate --oneline&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;You are looking for the 40 character SHA-1 hash ID (or the 7 character abbreviation). Yes, if you know the &amp;quot;^&amp;quot; or &amp;quot;~&amp;quot; shortcuts you may use those.Undoing the file modifications caused by the merge is about as simple as you might hope. &lt;code&gt;git revert -m 1 SHA&lt;/code&gt;. (replace &amp;quot;SHA&amp;quot; with the reference you want to revert; &lt;code&gt;-m 1&lt;/code&gt; will revert changes from all but the first parent, which is almost always what you want.) Unfortunately, this is just the tip of the iceberg.&lt;/p&gt;
&lt;p&gt;The problem is, what happens months later, long after you have exiled this problem from your memory, when you try again to merge these branches (or any other branches they have been merged into)? Because git has it tracked in history that a merge occurred, it is not going to attempt to remerge what it has already merged, and even worse, if you merge &lt;em&gt;from&lt;/em&gt; the branch where you did the revert you will undo the changes on the branch where they were made. (Imagine you revert a premature merge of a long-lived topic branch into master and later merge master into the topic branch to get other changes for testing.)One option is actually to do this reverse merge immediately, annihilating any changes before the bad merge, and then to &amp;quot;revert the revert&amp;quot; to restore them. This leaves the changes removed from the branch you mistakenly merged to, but present on their original branch, and allows merges in either direction without loss. This is the simplest option, and in many cases, can be the best.A disadvantage of this approach is that &lt;code&gt;git blame&lt;/code&gt; output is not as useful (all the changes will be attributed to the revert of the revert) and &lt;code&gt;git bisect&lt;/code&gt; is similarly impaired. Another disadvantage is that you must merge all current changes in the target of the bad merge back into the source; if your development style is to keep branches clean, this may be undesirable, and if you rebase your branches (e.g. with &lt;code&gt;git pull --rebase&lt;/code&gt;), it could cause complications unless you are careful to use &lt;code&gt;git rebase -p&lt;/code&gt; to preserve merges.In the following example please replace $destination with the name of the branch that was the destination of the bad merge, $source with the name of the branch that was the source of the bad merge, and $sha with the SHA-1 hash ID of the bad merge itself.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;git checkout $destination
git revert $sha
# save the SHA-1 of the revert commit to un-revert it later
revert=`git rev-parse HEAD`
git checkout $source
git merge $destination
git revert $revert
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Another option is to abandon the branch you merged from, recreate it from the previous merge-base with the commits since then rebased or cherry-picked over, and use the recreated branch from now on. Then the new branch is unrelated and will merge properly. Of course, if you have pushed the donor branch you cannot use the same name (that would be rewriting public history and is bad) so everyone needs to remember to use the new branch. Hopefully you have something like &lt;a href=&quot;https://github.com/sitaramc/gitolite&quot;&gt;gitolite&lt;/a&gt; where you can close the old branch name.&lt;/p&gt;
&lt;p&gt;This approach has the advantage that the recreated donor branch will have cleaner history, but especially if there have been many commits (and especially merges) to the branch, it can be a lot of work. At this time, I will not walk you through the process of recreating the donor branch. Given sufficient demand I can try to add that. However, if you look at &lt;a href=&quot;https://www.kernel.org/pub/software/scm/git/docs/howto/revert-a-faulty-merge.txt&quot;&gt;howto/revert-a-faulty-merge.txt&lt;/a&gt; (also shipped as part of the git distribution) it will provide more words than you can shake a stick at.&lt;/p&gt;
</content:encoded><media:content type="image/png" width="1200" height="630" medium="image" url="https://andrewusher.dev/open-graph/reverting-a-merge-commit.png"/><category>git</category><category>tutorial</category></item><item><title>React Aha Moments</title><link>https://andrewusher.dev/blog/react-aha-moments/</link><guid isPermaLink="true">https://andrewusher.dev/blog/react-aha-moments/</guid><description>- How using immutable objects as props can make  super fast for pure components, even when you need complicated hierarchical objects - That  function...</description><pubDate>Tue, 22 Jan 2019 00:00:00 GMT</pubDate><content:encoded>&lt;ul&gt;
&lt;li&gt;How using immutable objects as props can make &lt;code&gt;shouldComponentUpdate&lt;/code&gt; super fast for pure components, even when you need complicated hierarchical objects&lt;/li&gt;
&lt;li&gt;That &lt;code&gt;render&lt;/code&gt; function is called before &lt;code&gt;componentDidMount&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;You don&amp;#39;t need to have any data-flow package (flux, redux, mobx, etc) unless your components need to share state&lt;/li&gt;
&lt;li&gt;Your UI is a function of your state&lt;/li&gt;
&lt;li&gt;That when two components need to share state I need to lift it up instead of trying to keep their states in sync&lt;/li&gt;
&lt;li&gt;Components don’t necessarily have to correspond to DOM nodes&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><media:content type="image/png" width="1200" height="630" medium="image" url="https://andrewusher.dev/open-graph/react-aha-moments.png"/><category>react</category><category>javascript</category><category>web-development</category></item><item><title>Recovering From A Bad Rebase</title><link>https://andrewusher.dev/blog/recovering-from-a-bad-rebase/</link><guid isPermaLink="true">https://andrewusher.dev/blog/recovering-from-a-bad-rebase/</guid><description>So, you were in the middle of a rebase, have encountered one or more conflicts, and you have now decided that it was a big mistake and want to get out...</description><pubDate>Thu, 29 Nov 2018 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;So, you were in the middle of a rebase, have encountered one or more conflicts, and you have now decided that it was a big mistake and want to get out of the merge.The fastest way out of the merge is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;git rebase --abort
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><media:content type="image/png" width="1200" height="630" medium="image" url="https://andrewusher.dev/open-graph/recovering-from-a-bad-rebase.png"/><category>git</category><category>tutorial</category></item><item><title>setState: a function??</title><link>https://andrewusher.dev/blog/set-state-a-function/</link><guid isPermaLink="true">https://andrewusher.dev/blog/set-state-a-function/</guid><description>Components in React are independent and reusable pieces of code that often contain their own state. They return React elements that make up the UI of...</description><pubDate>Fri, 19 Oct 2018 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Components in React are independent and reusable pieces of code that often contain their own state. They return React elements that make up the UI of an application. Components that contain local state have a property called state When we want to change our how application looks or behaves, we need to change our component’s state. So, how do we update the state of our component? React components have a method available to them called setState Calling this.setState causes React to re-render your application and update the DOM.&lt;/p&gt;
&lt;p&gt;Normally, when we want to update our component we just call setState with a new value by passing in an object to the setState function:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;this.setState({ someField: someValue })
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But, often there is a need to update our component’s state using the current state of the component. Directly accessing this.state to update our component is not a reliable way to update our component’s next state. From the React documentation:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Because this.props and this.state may be updated asynchronously, you should not rely on their values for calculating the next state.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The key word from that documentation is &lt;strong&gt;asynchronously&lt;/strong&gt;. Updates to the DOM don’t happen immediately when &lt;code&gt;this.setState&lt;/code&gt; is called. React batches updates so elements are re-rendered to the DOM efficiently.&lt;/p&gt;
&lt;h3&gt;Function in setState!&lt;/h3&gt;
&lt;p&gt;Instead of passing in an object to this.setState we can pass in a function and reliably get the value of the current state of our component:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;this.setState((prevState) =&amp;gt; ({
  someBool: !prevState.someBool,
}))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Passing in a function into setState instead of an object will give you a reliable value for your component’s state and props. If you know you’re going to use setState to update your component and you know you’re going to need the current state or the current props of your component to calculate the next state, passing in a function as the first parameter of this.setState instead of an object is the recommended solution.&lt;/p&gt;
</content:encoded><media:content type="image/png" width="1200" height="630" medium="image" url="https://andrewusher.dev/open-graph/set-state-a-function.png"/><category>react</category><category>javascript</category><category>tutorial</category></item><item><title>Bucket List</title><link>https://andrewusher.dev/blog/bucket-list/</link><guid isPermaLink="true">https://andrewusher.dev/blog/bucket-list/</guid><description>- South Park Escape Room - See the Mona Lisa - Visit a Castle - Go On A Cruise - Fly In A Helicopter - Meet Dan Abramov - Fly First Class - Live In A...</description><pubDate>Thu, 20 Sep 2018 00:00:00 GMT</pubDate><content:encoded>&lt;ul&gt;
&lt;li&gt;South Park Escape Room&lt;/li&gt;
&lt;li&gt;See the Mona Lisa&lt;/li&gt;
&lt;li&gt;Visit a Castle&lt;/li&gt;
&lt;li&gt;Go On A Cruise&lt;/li&gt;
&lt;li&gt;Fly In A Helicopter&lt;/li&gt;
&lt;li&gt;Meet Dan Abramov&lt;/li&gt;
&lt;li&gt;&lt;del&gt;Fly First Class&lt;/del&gt;&lt;/li&gt;
&lt;li&gt;Live In A Different Country&lt;/li&gt;
&lt;li&gt;Fly a Kite&lt;/li&gt;
&lt;li&gt;Grow A Tree&lt;/li&gt;
&lt;li&gt;Backpack to 10 Places&lt;/li&gt;
&lt;li&gt;Run A Marathon&lt;/li&gt;
&lt;li&gt;Take Part in Triathlon&lt;/li&gt;
&lt;li&gt;Go Scuba Diving&lt;/li&gt;
&lt;li&gt;Walk the Inca Trail&lt;/li&gt;
&lt;li&gt;Climb a Mountain&lt;/li&gt;
&lt;li&gt;Fly in A Hot Air Balloon&lt;/li&gt;
&lt;li&gt;Volcano Boarding&lt;/li&gt;
&lt;li&gt;Go to Google Headquarters&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><media:content type="image/png" width="1200" height="630" medium="image" url="https://andrewusher.dev/open-graph/bucket-list.png"/><category>opinion</category></item><item><title>Advice For Me 10 Years Ago</title><link>https://andrewusher.dev/blog/advice-for-me-10-years-ago/</link><guid isPermaLink="true">https://andrewusher.dev/blog/advice-for-me-10-years-ago/</guid><description>Just follow your passion. I think working in different environments is very important. It helps you to gain experience and find your interests and pas...</description><pubDate>Tue, 29 May 2018 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Just follow your passion. I think working in different environments is very important. It helps you to gain experience and find your interests and passions. Looking back, it makes a lot of sense, but I would have never imagined where I would end up.&lt;/p&gt;
</content:encoded><media:content type="image/png" width="1200" height="630" medium="image" url="https://andrewusher.dev/open-graph/advice-for-me-10-years-ago.png"/><category>career</category><category>opinion</category></item><item><title>Regular Expression Additions in ES 2018</title><link>https://andrewusher.dev/blog/regular-expression-additions-in-es-2018/</link><guid isPermaLink="true">https://andrewusher.dev/blog/regular-expression-additions-in-es-2018/</guid><description>Javascript has really come a long way in the past couple of years. With the now yearly updates of the language, there is always a lot of new things to...</description><pubDate>Thu, 24 May 2018 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Javascript has really come a long way in the past couple of years. With the now yearly updates of the language, there is always a lot of new things to learn. Something that is being added in 2018 is major improvements to regular expressions in JavaScript.&lt;/p&gt;
&lt;p&gt;If you are a complete beginner to regular expressions, this article can widen your understanding of the practical use of regular expressions. If, however, you do have at least an average knowledge of regular expressions, this article can get you caught up with new capabilities of regular expressions in JS.&lt;/p&gt;
&lt;h2&gt;Is Regex Finally Powerful In JS?&lt;/h2&gt;
&lt;p&gt;It doesn&amp;#39;t have everything under the regex sun now, but JS has significantly closed the gap between its own engine and other PCRE-based regex engines. The new updates are geared towards practical use cases:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Dotall Flag&lt;/li&gt;
&lt;li&gt;Named Capture Groups&lt;/li&gt;
&lt;li&gt;Unicode Escapes&lt;/li&gt;
&lt;li&gt;Lookbehinds&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Dotall Flag&lt;/h3&gt;
&lt;p&gt;This is a pretty simple update that was added. In Javascript and many other PCRE regular expressions, newline characters like &lt;code&gt;\n&lt;/code&gt; does not match the dot.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// Newline character does not match dot
const regex = /./.regex.test(&amp;#39;\n&amp;#39;)
// &amp;gt; false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When we test it, the test returns false. However, adding an s flag in ES2018 matches the newline character in addition to the carriage return, line seperator, and paragraph seperator characters:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// Dotall flag
let regex = /./s
regex.test(&amp;#39;\n&amp;#39;)
// &amp;gt; true
regex.test(&amp;#39;\r&amp;#39;)
// &amp;gt; true
regex.test(&amp;#39;\u2028&amp;#39;)
// &amp;gt; true
regex.test(&amp;#39;\u2029&amp;#39;)
// &amp;gt; true
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Named Capture Groups&lt;/h3&gt;
&lt;p&gt;Suppose you want to get the currency, numeric price value, and total currency in a string of format:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Total: $60.00
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The regular matching this string would look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;;/^Total: [€\$]\d\d\.\d\d$/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The dolloar sign and dot are escaped because they are metasyntax characters. After adding parentheses to capture the needed values, the regular expression lookis like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;;/^Total: (([€\$])(\d\d\.\d\d))$/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We now have three capture groups to access the data:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1: price with currency&lt;/li&gt;
&lt;li&gt;2: currency symbol&lt;/li&gt;
&lt;li&gt;3: numeric price&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Using the indices 1, 2, and 3 to refer to these capture groups isn&amp;#39;t very maintainable. For example, what if price is multilingual, and you have to capture the Total as well:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;;/^(Total|Totaal): (([€\$])(\d\d\.\d\d))$/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As a result, capture groups 1, 2, and 3 have now become 2, 3, and 4. Code processing these values now have to be rewritten. Enter capture groups:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&amp;lt;priceWithSymbol&amp;gt;: price with symbol&lt;/li&gt;
&lt;li&gt;&amp;lt;symbol&amp;gt;: currency symbol&lt;/li&gt;
&lt;li&gt;&amp;lt;price&amp;gt;: numeric price&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The syntax &lt;code&gt;&amp;lt;captureGroupName&amp;gt;content&lt;/code&gt; matches &lt;code&gt;content&lt;/code&gt; in &lt;code&gt;&amp;lt;captureGroupName&amp;gt;&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// Capture groups example
const regex = /^Total: (?&amp;lt;priceWithSymbol&amp;gt;(?&amp;lt;symbol&amp;gt;[€\$])(?&amp;lt;price&amp;gt;\d\d\.\d\d))$/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In order to create a named capture, all that&amp;#39;s needed is to write a question mark after the start of the parentheses, then the capture group name inside greater than and less than symbols.&lt;/p&gt;
&lt;p&gt;Now we&amp;#39;re done. Let&amp;#39;s see the results:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// Named capture groups example
const regex = /^Total: (?&amp;lt;priceWithSymbol&amp;gt;(?&amp;lt;symbol&amp;gt;[€\$])(?&amp;lt;price&amp;gt;\d\d\.\d\d))$/
regex.exec(&amp;#39;Total: $60.00&amp;#39;).groups
// &amp;gt; {priceWithSymbol: $60.00, symbol: &amp;#39;$&amp;#39;, price: &amp;#39;60.00&amp;#39;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Named capture groups make regular expressions more maintainable.&lt;/p&gt;
&lt;h3&gt;Unicode Escapes&lt;/h3&gt;
&lt;p&gt;This new feature is very documentation-heavy since the docs themselves discuss every minute detail in this update. You can use the &lt;a href=&quot;https://github.com/tc39/proposal-regexp-unicode-property-escapes&quot;&gt;documentation&lt;/a&gt; as a reference. The documentation entails how you can match certain unicode character groups with some expressions without third party libraries. In this section, I&amp;#39;ll concentrate on some practical use cases of the addition instead of a thorough walkthrough of all unicode groups.&lt;/p&gt;
&lt;p&gt;One use case is matching greek characters. Before ES2018, you would have to create character sets:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// Greek lowercase character set
const greekChars = /[θωερτψυιοπασδφγηςκλζχξωβνμάέήίϊΐόύϋΰώ]/u
greekChars.test(&amp;#39;λ&amp;#39;)
// &amp;gt; true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That doesn&amp;#39;t even include uppercase letters:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const uppercaseGreekChars = /[ΘΩΕΡΤΨΥΙΟΠΑΣΔΦΓΗςΚΛΖΧΞΩΒΝΜΆΈΉΊΪΐΌΎΫΰΏ]/u
uppercaseGreekChars.test(&amp;#39;Λ&amp;#39;)
// &amp;gt; true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In ES2018, it&amp;#39;s a lot simpler:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// Old way
const greekChars = /[θωερτψυιοπασδφγηςκλζχξωβνμάέήίϊΐόύϋΰώ]/u
const uppercaseGreekChars = /[ΘΩΕΡΤΨΥΙΟΠΑΣΔΦΓΗςΚΛΖΧΞΩΒΝΜΆΈΉΊΪΐΌΎΫΰΏ]/u
// New way
const unicodeEscape = /\p{Script=Greek}/u
unicodeEscape.test(&amp;#39;π&amp;#39;)
// &amp;gt; true
unicodeEscape.test(&amp;#39;ω&amp;#39;)
// &amp;gt; true
unicodeEscape.test(&amp;#39;Ϋ&amp;#39;)
// &amp;gt; true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;/\p{Script=Greek}/u&lt;/code&gt; only matches Greek characters, and this is great semantic shorthand. If you think about it, Greek is very much like English in terms of the limited number of characters. In addition, you would have a have a difficult time working in Chinese or Japanese, where you would have to concatenate another endlees pool of symbols. This problem is solved with Unicode escapes in ES2018.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/\p{Alphabetic}/u&lt;/code&gt; matches any alphabetical character of any language:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// Alphabetic unicode escape
const regex = /\p{Alphabetic}/u
regex.test(&amp;#39;á&amp;#39;)
// &amp;gt; true
regex.test(&amp;#39;が&amp;#39;)
// &amp;gt; true
regex.test(&amp;#39;6&amp;#39;)
// &amp;gt; false
regex.test(&amp;#39;π&amp;#39;)
// &amp;gt; true
regex.test(&amp;#39;ω&amp;#39;)
// &amp;gt; true
regex.test(&amp;#39;Ϋ&amp;#39;)
// &amp;gt; true
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Lookbehinds&lt;/h3&gt;
&lt;p&gt;A lookbehind is the opposite of a lookahead. It walks backwards in the regular expression and checks if the given pattern matches the string before the current position. If a lookbehind match is succesful, the match is reverted.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Negative lookbehind: (?&amp;lt;!pattern)&lt;/li&gt;
&lt;li&gt;Positive lookbehind: (?&amp;lt;=pattern)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;An implicit lookbehind does exist before ES2018, &lt;strong&gt;\b&lt;/strong&gt;, the word boundary anchor. So, in practice, you could use word boundaries without lookbehinds. Here are some examples that show this.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Example 1&lt;/strong&gt;: Determine if the string contains a character sequence starting with whimper or whisper:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const regex = /\bwhi[ms]per/
regex.test(string)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the example above, we&amp;#39;re looking for the pattern &lt;code&gt;whi[ms]per&lt;/code&gt; in the string provided. Before the &lt;code&gt;w&lt;/code&gt; character, the &lt;code&gt;\b&lt;/code&gt; word boundary anchor filters our results by ensuring a non-whitespace character cannot stand in front of the &lt;code&gt;w&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Example 2&lt;/strong&gt;: Here are some examples for negative and positive lookbehinds. &lt;code&gt;/(?&amp;lt;=e)i/&lt;/code&gt; matches an &lt;code&gt;i&lt;/code&gt; character if it comes right after an &lt;code&gt;e&lt;/code&gt; character. The &lt;code&gt;e&lt;/code&gt; character is not included in the match.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// Positive lookbehind syntax
const regex = /(?&amp;lt;=e)i/
regex.exec(&amp;#39;oi&amp;#39;)
// &amp;gt; null
regex.exec(&amp;#39;ei&amp;#39;)
// &amp;gt; {&amp;#39;i&amp;#39;, index: 1, input: &amp;#39;ei&amp;#39;, groups: undefined}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;/(?&amp;lt;=e)i/&lt;/code&gt; matches an &lt;code&gt;i&lt;/code&gt; character if it does not come right after an &lt;code&gt;e&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// Negative lookbehind syntax
const regex = /(?&amp;lt;=e)i/
regex.exec(&amp;#39;oi&amp;#39;)
// &amp;gt; {&amp;#39;i&amp;#39;, index: 1, input: &amp;#39;ei&amp;#39;, groups: undefined}
regex.exec(&amp;#39;ei&amp;#39;)
// &amp;gt; null
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><media:content type="image/png" width="1200" height="630" medium="image" url="https://andrewusher.dev/open-graph/regular-expression-additions-in-es-2018.png"/><category>javascript</category><category>web-development</category></item><item><title>Want to Learn Faster? Double Your Rate of Failure</title><link>https://andrewusher.dev/blog/want-to-learn-faster-double-your-rate-of-failure/</link><guid isPermaLink="true">https://andrewusher.dev/blog/want-to-learn-faster-double-your-rate-of-failure/</guid><description>I&apos;ve been doing web development for around a year now, but I&apos;ve had some rough patches throughout this time. When I used to fail at creating projects,...</description><pubDate>Sat, 24 Feb 2018 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I&amp;#39;ve been doing web development for around a year now, but I&amp;#39;ve had some rough patches throughout this time. When I used to fail at creating projects, I would just delete everything and start over, or just stop coding altogether. These coding droughts occurred from time periods of a couple days all the way up to a month or two.&lt;/p&gt;
&lt;p&gt;I used to see failure as a bad thing. However, as a developer, I have learned that I need to &lt;strong&gt;embrace failure and discomfort&lt;/strong&gt;. Now, I don&amp;#39;t look as failure as having to start over: it&amp;#39;s a chance to do something better this time.&lt;/p&gt;
&lt;p&gt;For example, let&amp;#39;s say you want to create a full stack app in a day. If you&amp;#39;re just starting out coding, that&amp;#39;s not a realistic goal. It&amp;#39;s not really specific either. However, if you set a unrealistic goal and fail, &lt;strong&gt;learn from it&lt;/strong&gt;. Try again, fail again, and keep going. Even the best make mistakes: it&amp;#39;s what you do after the mistake that counts!&lt;/p&gt;
&lt;p&gt;What happens is we, as people and developers, fall into a cycle of discouragement. We mess up, then we beat ourselves up. As a result, we lose our motivation, and the cycle just repeats itself. &lt;strong&gt;Break out of that cycle&lt;/strong&gt;. Look at every failure as an opportunity to start over and do things even better.&lt;/p&gt;
</content:encoded><media:content type="image/png" width="1200" height="630" medium="image" url="https://andrewusher.dev/open-graph/want-to-learn-faster-double-your-rate-of-failure.png"/><category>career</category><category>opinion</category></item><item><title>Difference Between Git Stash and Git Commit</title><link>https://andrewusher.dev/blog/difference-between-git-stash-and-git-commit/</link><guid isPermaLink="true">https://andrewusher.dev/blog/difference-between-git-stash-and-git-commit/</guid><description>git stash The command saves your local modifications away and reverts the working directory to match the HEAD commit. It allows you to store your unco...</description><pubDate>Wed, 19 Apr 2017 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;git stash&lt;/h2&gt;
&lt;p&gt;The command saves your local modifications away and reverts the working directory to match the HEAD commit. It allows you to store your uncommited modifications into a buffer area called stash, and deletes it from the branch you are working on. You may later retreive them by applying the stash.&lt;/p&gt;
&lt;h2&gt;git commit&lt;/h2&gt;
&lt;p&gt;This records your change for next push and this event is recorded in history too.&lt;/p&gt;
</content:encoded><media:content type="image/png" width="1200" height="630" medium="image" url="https://andrewusher.dev/open-graph/difference-between-git-stash-and-git-commit.png"/><category>git</category><category>tutorial</category></item></channel></rss>