Sr. Content Developer at Microsoft, working remotely in PA, TechBash conference organizer, former Microsoft MVP, Husband, Dad and Geek.
149802 stories
·
33 followers

Ripple over React? Evaluating the newest JS framework

1 Share

The React ecosystem has had plenty of contenders over the years, but every now and then, something shows up that actually feels different. RippleJS isn’t just another “React but better” framework; it’s a genuine rethink of how we write UI code, built by someone who’s been deep in the guts of React and Svelte.

alt

Created by Dominic Gannaway (yes, the guy who worked on React Hooks at Meta, created Lexical, authored Inferno, and was a core maintainer of Svelte 5), RippleJS is what happens when someone with serious framework mileage decides to start over and apply all the lessons at once. The wild part? He built the prototype in under a week (vibe coding definitely did some heavy lifting there).

If you want to hear Dominic walk through his thinking in his own words, the PodRocket team interviewed him about RippleJS and the ideas behind it. You can listen to that episode here: RippleJS with Dominic Gannaway on PodRocket.

Goal

In this article, we’ll look at what makes RippleJS different, build a to-do list (the “Hello World” of reactivity) to see it in action, and talk about whether it’s worth your attention.

You’ll need:

  • Basic JavaScript/TypeScript knowledge
  • Some HTML/CSS familiarity
  • React experience is helpful, mainly because we’ll compare the two later

The core philosophy: TypeScript-first, not TypeScript-added

First big clarification: RippleJS does not mean you stop writing TypeScript. It means TypeScript is baked into the framework from day one, not bolted on after the fact.

In React:

  • You write .tsx files (TypeScript + JSX)
  • TypeScript checks your types
  • JSX is just syntax sugar; React doesn’t really “understand” your reactive patterns at the type level
  • You end up with generic types like useState<number> that don’t know much about your actual state logic

In RippleJS:

  • You write .ripple files, which are essentially a superset of JSX designed for TypeScript
  • The compiler understands both your TypeScript types and your reactive state patterns
  • You get better autocompletion, error checking, and tooling support because the framework knows what track()means
  • The .ripple extension is meant to create a better DX “not only for humans, but also for LLMs”

That’s a pretty fundamental shift in how tooling, error messages, and developer experience are designed, especially in a world where TypeScript is mostly expected by default.

Here’s a concrete example:

// React (.tsx) - TypeScript is external to React's reactivity
import { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0); // Generic type, React doesn't "know" about count's reactive nature
  const double = count * 2; // Not reactive, just a calculation


  return (
    <div>
      <p>Count: {count}</p>
      <p>Double: {double}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

// RippleJS (.ripple) - TypeScript integrated with reactivity
import { track } from 'ripple';

export component Counter() {
  let count = track(0); // Compiler knows this is reactive
  let double = track(() => @count * 2); // Compiler knows this derives from count


  <div>
    <p>{"Count: "}{@count}</p>
    <p>{"Double: "}{@double}</p>
    <button onClick={() => @count++}>{"Increment"}</button>
  </div>
}

In RippleJS, the compiler understands that double is derived from count. If you try to use @double somewhere that doesn’t make sense, or if you forget the @ when accessing a tracked value, the compiler catches it immediately with precise error messages.

In React, TypeScript mostly just sees regular variables and functions. It can’t reason about reactivity in the same way. That said, we shouldn’t pretend the Ripple syntax isn’t a bit more visually “busy” than React at first glance; it is.

Where RippleJS currently stands

Let’s be honest about what RippleJS is and isn’t right now.

What it has:

  • Very good performance and memory usage
  • Full TypeScript integration with type checking
  • VSCode extension with diagnostics and IntelliSense
  • Prettier formatting support
  • Component-level scoped CSS

What it doesn’t have (yet):

  • SSR (Server-Side Rendering) – currently SPA-only
  • Full production testing across various use cases
  • A massive ecosystem of third-party libraries

What makes it interesting, anyway, is the direction. There’s a plan to integrate SSR and to wire AI into the dev server to help diagnose “page-like” issues. In a world where everyone is using AI coding tools, a framework designed with AI assistance in mind from the start is intriguing.

Ripple’s component model

If you’ve been writing React for a while now, RippleJS will feel a lot similar but different in some ways. One of the biggest mental shifts: components don’t return JSX; they are the JSX.

Let me show you what I mean:

// React - Components return JSX
function Button({ text, onClick }) {
  return (
    <button onClick={onClick}>
      {text}
    </button>
  );
}

function App() {
  return (
    <div>
      <Button text="Click me" onClick={() => console.log("Clicked!")} />
    </div>
  );
}
// RippleJS - Components ARE JSX (imperative style)
component Button(props: { text: string, onClick: () => void }) {
  <button onClick={props.onClick}>
    {props.text}
  </button>
}

export component App() {
  <div>
    <Button text="Click me" onClick={() => console.log("Clicked!")} />
  </div>
}

Notice the component keyword instead of function? Notice how there’s no return statement? This is because templates in RippleJS are statements rather than expressions. You’re not computing a value to return; you’re declaring what the component is.

A small syntax change of this sort could yield better results on how the compiler optimizes your code.

Why does no component re-rendering matter?

Here’s where RippleJS starts to pull ahead of React in interesting ways. In React, when state changes, the entire component function re-runs. Sure, React is smart about what actually updates in the DOM, but the JavaScript still executes.

In RippleJS? Reactivity means only the specific parts of the DOM that depend on the changed state get updated. There is no component re-rendering or reconciliation. Just surgical updates to exactly what needs to change.

This is similar in spirit to Solid and Svelte 5, but RippleJS aims to get there with a cleaner, TypeScript-centric API.

How reactivity actually works: track() and the @ Symbol

RippleJS’s reactivity system revolves around two ideas:

  • track() for creating reactive values
  • @ for reading/writing those values
import { track } from 'ripple';

export component Counter() {
  // Create a reactive value
  let count = track(0);


  // Create a derived reactive value
  let double = track(() => @count * 2);
  let quadruple = track(() => @double * 2);


  <div>
    <p>{"Count: "}{@count}</p>
    <p>{"Double: "}{@double}</p>
    <p>{"Quadruple: "}{@quadruple}</p>
    <button onClick={() => @count++}>{"Increment"}</button>
  </div>
}
  • track(0) creates a reactive primitive with initial value 0
  • track(() => @count * 2) creates a computed value that automatically updates when count changes
  • @count is how you read and write the tracked value
  • @count++ mutates the value and triggers updates to double, quadruple, and the DOM

The naming convention is intentional. Earlier versions called this createSignal (like Solid), but people kept trying to use it exactly like Solid’s API and getting confused. By calling it track(), the RippleJS team made it clear: you’re tracking a value, not creating a signal in the Solid sense.

The memory efficiency story

Memory usage doesn’t get talked about nearly as much as it should, so let’s talk about it.

In React, every component instance holds its own state, effects, and rendering logic. Multiply that by hundreds or thousands of components, and the memory overhead becomes significant.

In RippleJS, everything is a “block” with a single relationship between a reactive value and what depends on it. The result is far less overhead per reactive connection.

For large apps with deep trees and many components, that difference in memory usage compounds quickly.

Reactive collections: #[] and #{}

One thing that caught my attention is how RippleJS handles arrays and objects reactively:

export component TodoList() {
  const items = #[1, 2, 3]; // TrackedArray
  const config = #{theme: 'dark', language: 'en'}; // TrackedObject


  <div>
    <p>{"Items: "}{items.join(', ')}</p>
    <p>{"Theme: "}{config.theme}</p>
    <button onClick={() => items.push(items.length + 1)}>
      {"Add Item"}
    </button>
    <button onClick={() => config.theme = config.theme === 'dark' ? 'light' : 'dark'}>
      {"Toggle Theme"}
    </button>
  </div>
}

The #[] and #{} syntax creates fully reactive arrays and objects. When you push to the array or update a property, everything that depends on it updates automatically. There is no need for spread operators; just mutate the data, and the UI updates.

This is a good developer experience improvement over React’s immutable update patterns:

// React - Immutable updates
const [items, setItems] = useState([1, 2, 3]);
setItems([...items, items.length + 1]); // Create new array

// RippleJS - Direct mutation
const items = #[1, 2, 3];
items.push(items.length + 1); // Just push

Global state: A deliberate “no”

Now here’s an interesting design decision that might be controversial: RippleJS doesn’t support global state in the traditional sense.

You cannot create tracked values outside components like this:

// This does not work - compilation error
import { track } from 'ripple';

const globalCount = track(0);
// Error: track can only be used within a reactive contect

export component App() {
  <div>
    <p>{"Global count: "}{@globalCount}</p>
    <button onClick={() => @globalCount++}>{"Increment"}</button>
  </div>
}

If you do, the code will throw an error of this sort:

alt

The framework enforces that track() must be used within a reactive context (components, functions, or classes created from components). This is an intentional architectural decision.

This is intentional. In RippleJS, everything is about direct relationships between tracked values and the components that use them. There’s no built-in concept of a global store that every component taps into.

If you want a shared state between components, you:

  1. Make the parent component own the state
  2. Pass it down via props
  3. Compose components around that shared owner

Yes, that’s more explicit. But it also sidesteps the usual global state mess that can creep into large React applications.

Component-level styling

RippleJS ships with component-scoped styling built into the syntax:

export component StyledCounter() {
  let count = track(0);


  <div class="container">
    <h1>{"Counter App"}</h1>
    <div class="counter-display">
      <button class="btn btn-decrement" onClick={() => @count--}>{"-"}</button>
      <span class="count-value">{@count}</span>
      <button class="btn btn-increment" onClick={() => @count++}>{"+"}</button>
    </div>
  </div>


  <style>
    .container {
      text-align: center;
      font-family: "Arial", sans-serif;
      padding: 2rem;
    }


    .counter-display {
      display: flex;
      align-items: center;
      justify-content: center;
      gap: 1rem;
      margin-top: 1rem;
    }


    .btn {
      height: 3rem;
      width: 3rem;
      border: none;
      border-radius: 0.5rem;
      font-size: 1.5rem;
      cursor: pointer;
      transition: all 0.2s;
    }


    .btn-decrement {
      background-color: #ef4444;
      color: white;
    }


    .btn-decrement:hover {
      background-color: #dc2626;
    }


    .btn-increment {
      background-color: #10b981;
      color: white;
    }


    .btn-increment:hover {
      background-color: #059669;
    }


    .count-value {
      font-size: 2rem;
      font-weight: bold;
      min-width: 3rem;
      text-align: center;
    }
  </style>
}

The style shows something like this:

alt

What’s beautiful about this:

  • No CSS-in-JS libraries needed – The <style> tag is a prioritized citizen in your component
  • Automatic scoping – These styles only apply to this component, no class name collisions
  • Co-located with logic – Your styles live right next to the component code that uses them
  • Full CSS support – Not a subset or special syntax, just regular CSS with all the features you know

The global CSS “but”

The flip side is global CSS. Right now, you can’t mark styles as global inside components. There’s no global attribute or :global selector yet (though :global is planned).

So you have two options for global styles:

Option 1: Use a <style> tag in your index.html

<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>My Ripple App</title>


  <style>
    /* Global styles here */
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }


    body {
      font-family: system-ui, -apple-system, sans-serif;
      background-color: #f3f4f6;
      line-height: 1.6;
    }


    :root {
      --color-primary: #3b82f6;
      --color-secondary: #10b981;
      --color-danger: #ef4444;
    }
  </style>
</head>
<body>
  <div id="root"></div>
  <script type="module" src="/src/index.ts"></script>
</body>
</html>

Many frameworks handle global styles separately from components. In fact, it’s often considered a better practice to keep global styles centralized rather than scattered across components.

The bigger point: having styles co-located with components is a subtle DX win. You’re not constantly hopping between files while you’re in that “build the feature” flow.

Built-in control flow & component system

One of the more refreshing parts of RippleJS is that you write control flow the way you do in plain JavaScript, no .map()gymnastics or nested ternaries when you don’t want them.

Native for loops:

// React - .map()
{users.map(user => (
  <div key={user.id}>
    <h3>{user.name}</h3>
  </div>
))}

// RippleJS - just write a loop
for (const user of props.users) {
  <div>
    <h3>{user.name}</h3>
  </div>
}

No key props, no wrapping JSX in parentheses. Just a loop.

if statements:

// React - nested ternary nightmare
{isLoading ? (
  <Spinner />
) : error ? (
  <ErrorMessage error={error} />
) : user ? (
  <div>Welcome, {user.name}</div>
) : (
  <LoginPrompt />
)}

// RippleJS - readable if/else
if (props.isLoading) {
  <Spinner />
} else if (props.error) {
  <ErrorMessage error={props.error} />
} else if (props.user) {
  <div>{"Welcome, " + props.user.name}</div>
} else {
  <LoginPrompt />
}

They’re both readable when done well, but for newcomers, Ripple’s control flow often feels more familiar simply because it’s “just JavaScript”.

Try/catch as error boundaries

Instead of separate ErrorBoundary components, you can lean on try/catch:

component SafeComponent(props: { data: any }) {
  <div>
    try {
      <RiskyComponent data={props.data} />
    } catch (error) {
      <div class="error-message">
        {"Something went wrong: " + error.message}
      </div>
    }
  </div>
}

Same mental model you use everywhere else in JavaScript.

Components with props and children

Components work like React, but with cleaner TypeScript-first syntax:

component Button(props: { 
  text: string
  onClick: () => void
  variant?: 'primary' | 'danger'
}) {
  <button 
    class={['btn', `btn-${props.variant || 'primary'}`]} 
    onClick={props.onClick}
  >
    {props.text}
  </button>


  <style>
    .btn { padding: 0.75rem 1.5rem; border: none; cursor: pointer; }
    .btn-primary { background: #3b82f6; color: white; }
    .btn-danger { background: #ef4444; color: white; }
  </style>
}

// Strongly typed - compiler catches missing/wrong props
export component App() {
  <div>
    <h1>{"TypeScript Button Test"}</h1>
    <h2>{"Strongly Typed Component Props"}</h2>


    <div>
      <Button text="Primary Button" onClick={() => console.log("Primary clicked")} />
      <Button text="Delete" onClick={() => console.log("Deleted")} variant="danger" />
      <Button text="Save" onClick={() => alert("Saved!")} variant="primary" />
    </div>
  </div>
}

Children work the same way:

component Card(props: { title: string, children: Component }) {
  <div class="card">
    <h2>{props.title}</h2>
    <div class="card-body">
      <props.children />
    </div>
  </div>
}

<Card title="Stats">
  <p>{"Users: 1,234"}</p>
</Card>
<Card title="Dashboard">
    <p>{"Welcome to your dashboard"}</p>
    <p>{"You have 5 new notifications"}</p>
</Card>

Again, the key difference is that components don’t return JSX; they are the template. That imperative style gives the compiler more room to optimize.

Control flow comparison

It’s not revolutionary, it’s just sensible.

Task React RippleJS
Loop .map() + key for loop
Condition Ternary/&& if/else
Multiple conditions Nested ternaries if/else if/else
Error handling ErrorBoundary class try/catch

Let’s build something real

Enough theory. Let’s build a Todo app in RippleJS and then compare it with the React version.

Getting started

# Create a new Ripple project
npm create ripple my-app

# Navigate into the project
cd my-app

# Install dependencies
npm install

# Start the dev server
npm run dev

Your app should be running at http://localhost:3000

Editor setup

For the best experience, install the RippleJS VS Code extension:

  • Open VS Code
  • Go to Extensions (Ctrl+Shift+X / Cmd+Shift+X)
  • Search for Ripple for VS Code
  • Install the official extension

alt

That gives you syntax highlighting, IntelliSense, type checking, and live diagnostics for .ripple files.

The Todo list we’ll build demonstrates most of RippleJS’s core ideas:

  • Reactive state with track()
  • Native control flow (if/else, for loops)
  • Events and form handling
  • Conditional rendering and styling
  • Component composition

Open App.ripple and drop this in (styles omitted here to keep things focused):

// TodoList.ripple
import { track } from 'ripple';

export component App() {
  // Reactive state - these automatically trigger UI updates
  const todos = #[]; // TrackedArray for reactive list
  let inputValue = track('');
  let filter = track('all'); // 'all' | 'active' | 'completed'
  let editingId = track(null);
  let editValue = track('');


  // Derived values - automatically recompute when dependencies change
  let filteredTodos = track(() => {
    const allTodos = todos;
    if (@filter === 'active') {
      return allTodos.filter(todo => !todo.completed);
    }
    if (@filter === 'completed') {
      return allTodos.filter(todo => todo.completed);
    }
    return allTodos;
  });


  let activeCount = track(() => todos.filter(t => !t.completed).length);
  let completedCount = track(() => todos.filter(t => t.completed).length);


  // Actions - direct mutations update the UI automatically
  const addTodo = () => {
    const text = @inputValue.trim();
    if (!text) return;


    // Just push directly - no setState or immutable patterns needed
    todos.push({
      id: Date.now(),
      text,
      completed: false
    });


    @inputValue = '';
  };


  const toggleTodo = (id) => {
    const todo = todos.find(t => t.id === id);
    if (todo) {
      todo.completed = !todo.completed; // Direct mutation
    }
  };


  const deleteTodo = (id) => {
    const index = todos.findIndex(t => t.id === id);
    if (index !== -1) {
      todos.splice(index, 1); // Direct mutation
    }
  };


  const startEdit = (todo) => {
    @editingId = todo.id;
    @editValue = todo.text;
  };


  const saveEdit = () => {
    const todo = todos.find(t => t.id === @editingId);
    if (todo && @editValue.trim()) {
      todo.text = @editValue.trim();
    }
    @editingId = null;
    @editValue = '';
  };


  const cancelEdit = () => {
    @editingId = null;
    @editValue = '';
  };


  const clearCompleted = () => {
    const completed = todos.filter(t => t.completed);
    completed.forEach(todo => deleteTodo(todo.id));
  };


  <div class="todo-app">
    <div class="header">
      <h1>{"Ripple Todo List"}</h1>


      <div class="add-todo">
        <input
          type="text"
          value={@inputValue}
          onInput={(e) => @inputValue = e.target.value}
          onKeyDown={(e) => e.key === 'Enter' && addTodo()}
          placeholder="What needs to be done?"
          class="todo-input"
        />
        <button onClick={addTodo} class="btn btn-primary">
          {"Add"}
        </button>
      </div>
    </div>


    {/* Stats bar - derived values update automatically */}
    <div class="stats">
      <span class="stat">
        {"Active: "}<strong>{@activeCount}</strong>
      </span>
      <span class="stat">
        {"Completed: "}<strong>{@completedCount}</strong>
      </span>
      <span class="stat">
        {"Total: "}<strong>{todos.length}</strong>
      </span>
    </div>


    {/* Filter buttons with dynamic classes */}
    <div class="filters">
      <button
        class={@filter === 'all' ? 'filter-btn active' : 'filter-btn'}
        onClick={() => @filter = 'all'}
      >
        {"All"}
      </button>
      <button
        class={@filter === 'active' ? 'filter-btn active' : 'filter-btn'}
        onClick={() => @filter = 'active'}
      >
        {"Active"}
      </button>
      <button
        class={@filter === 'completed' ? 'filter-btn active' : 'filter-btn'}
        onClick={() => @filter = 'completed'}
      >
        {"Completed"}
      </button>
    </div>


    <div class="todo-list">
      {/* Native for loop - no .map() needed */}
      for (const todo of @filteredTodos) {
        <div class={todo.completed ? 'todo-item completed' : 'todo-item'}>
          {/* Native if/else - no ternaries */}
          if (@editingId === todo.id) {
            <input
              type="text"
              value={@editValue}
              onInput={(e) => @editValue = e.target.value}
              onKeyDown={(e) => {
                if (e.key === 'Enter') saveEdit();
                if (e.key === 'Escape') cancelEdit();
              }}
              class="edit-input"
            />
            <div class="edit-actions">
              <button onClick={saveEdit} class="btn-icon btn-save">
                {"✓"}
              </button>
              <button onClick={cancelEdit} class="btn-icon btn-cancel">
                {"✕"}
              </button>
            </div>
          } else {
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={() => toggleTodo(todo.id)}
              class="todo-checkbox"
            />
            <span 
              class="todo-text"
              onDblClick={() => startEdit(todo)}
            >
              {todo.text}
            </span>
            <button
              onClick={() => deleteTodo(todo.id)}
              class="btn-icon btn-delete"
            >
              {"🗑"}
            </button>
          }
        </div>
      }


      {/* Empty state with conditional message */}
      if (@filteredTodos.length === 0) {
        <div class="empty-state">
          {
            @filter === 'completed' 
              ? "No completed todos yet!" 
              : @filter === 'active'
              ? "No active todos. Great work!"
              : "No todos yet. Add one above!"
          }
        </div>
      }
    </div>


    {/* Clear button only shows when needed */}
    if (@completedCount > 0) {
      <div class="footer">
        <button onClick={clearCompleted} class="btn btn-secondary">
          {"Clear completed (" + @completedCount + ")"}
        </button>
      </div>
    }
  </div>

  {/* Clone the repo to get the styles */}
}

The interesting parts:

  • for and if work directly in the template – no .map() or ternary chains
  • You mutate data directly (todos.push, todo.completed = !todo.completed), and the UI just follows
  • filteredTodos recomputes automatically when todos or filter change – no useMemo, no dependency arrays
  • Dynamic classes are handled cleanly; under the hood, Ripple uses something clsx-like to merge them

This is what our application looks like:

alt

The React equivalent (for comparison)

Here’s the same app in React, conceptually:

function TodoList() {
  const [todos, setTodos] = useState([]);
  const [inputValue, setInputValue] = useState('');
  const [filter, setFilter] = useState('all');
  const [editingId, setEditingId] = useState(null);
  const [editValue, setEditValue] = useState('');


  // All these need useMemo to avoid recalculation on every render
  const filteredTodos = useMemo(() => {
    if (filter === 'active') return todos.filter(t => !t.completed);
    if (filter === 'completed') return todos.filter(t => t.completed);
    return todos;
  }, [todos, filter]);


  const activeCount = useMemo(() => 
    todos.filter(t => !t.completed).length, [todos]
  );


  const completedCount = useMemo(() => 
    todos.filter(t => t.completed).length, [todos]
  );


  // All these need useCallback to avoid recreating on every render
  const addTodo = useCallback(() => {
    const text = inputValue.trim();
    if (!text) return;
    setTodos([...todos, { 
      id: Date.now(), 
      text, 
      completed: false 
    }]);
    setInputValue('');
  }, [inputValue, todos]);


  const toggleTodo = useCallback((id) => {
    setTodos(todos.map(todo =>
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    ));
  }, [todos]);


  // ... and so on


  return (
    <div className="todo-app">
      {/* JSX with .map() for loops */}
      {filteredTodos.map(todo => (
        <div key={todo.id}>
          {/* nested ternaries for conditions */}
        </div>
      ))}
    </div>
  );
}

Count it: 5 useState, 3 useMemo, and several useCallback calls if you’re trying to be “proper” about optimization. In Ripple, you mostly just reach for track() and mutate your state without thinking about render cycles.

Fair truth

RippleJS is not production-ready yet, and Dominic is very clear about that. It’s raw, it has bugs, and it’s missing SSR and a mature ecosystem.

So why talk about it at all? Because the ideas are legitimately interesting.

What RippleJS gets right

1. Developer experience

React’s Hooks are powerful, but they also introduce a lot of subtle mental overhead. You constantly think about when to use useCallback, whether useMemo is worth it, and why something keeps re-rendering.

In RippleJS, you track values, access them with @, and let the framework manage dependencies. A lot of that mental tax just disappears.

2. TypeScript that actually fits the framework

Plenty of frameworks “support” TypeScript, but it often feels like an afterthought. RippleJS assumes TypeScript from day one – and designs the syntax, compiler, and DX around it.

3. Performance, you don’t have to baby

When only the DOM nodes that truly depend on a value update, and the framework is more frugal with memory, your app just runs faster without you micromanaging renders. No virtual DOM diffing, no reconciliation passes, no component re-runs.

The bigger question

Is RippleJS going to replace React? Almost certainly not. That’s not an interesting question anyway.

What RippleJS does is explore a different set of trade-offs and ask: What if we made the simple things simple again and the already simple things even simpler?

That’s worth paying attention to.

The framework needs you

RippleJS is open source and very early. Dominic built the first version in a week and now has a few people on board, but turning this into a production-ready framework will take a community.

Areas where help is needed:

  • Implementing SSR
  • Tooling and DX improvements
  • Writing docs and tutorials
  • Bug reports and edge-case testing
  • Example apps and component libraries
  • Contributions to the VS Code extension
  • The upcoming :global style selector

You don’t need to be a compiler engineer. If you’ve ever struggled with a framework’s complexity, you probably have useful instincts about what better looks like.

How to get involved:

The early contributors to React, Vue, and Svelte helped shape frameworks that millions of developers use today. RippleJS is at that early stage right now. If you’ve ever wanted to be part of building something that could matter, this is your chance.

Conclusion

We’ve taken a pretty deep tour through RippleJS.

I’m not dropping React for production work. But I am watching RippleJS closely, and I’ll absolutely be using it for side projects. The developer experience feels genuinely refreshing, and the performance story is compelling.

Will RippleJS “win”? That depends less on clever technical decisions and more on whether developers adopt it, build things with it, and contribute back.

So here’s the challenge: build something small with RippleJS this weekend – a counter, a to-do list, whatever. See for yourself whether the hype matches reality. And if you hit bugs or missing pieces, don’t just complain on social – open an issue, or a PR.

React’s ecosystem wasn’t built in a day, and it definitely wasn’t built by one person. RippleJS won’t be either.

Now go build something.

The post Ripple over React? Evaluating the newest JS framework appeared first on LogRocket Blog.

Read the whole story
alvinashcraft
6 minutes ago
reply
Pennsylvania, USA
Share this story
Delete

How to Store Files on a User’s Device Using OPFS (Origin Private File System)

1 Share

In this article, we will explore how to use OPFS to store files directly on the user’s device and why it is great for privacy and good offline experiences on websites.

In the early days of the web, cookies were the only way for websites to store data on a user’s device. They were mainly designed for server-side usage, capped at 4 KB per cookie, and were automatically sent with every HTTP request.

With the increase in storage requirements and websites becoming more interactive, the Web Storage API was created. The Web Storage API, via the localStorage and sessionStorage objects, allowed websites to store more data locally on users’ browsers (up to 5MB). But unlike cookies, the data is not included in every HTTP request, which means stored data is more secure and the website will have better performance. Even though the Web Storage API is great, it can only store strings, lacks a structured file access system and isn’t built for large data, especially with the recent data explosion.

Then came IndexedDB, which lets web applications store large amounts of structured data (100+ MB), like objects, arrays and even files directly on the user’s device using a key-value system database. It’s still widely used, but it’s not beginner-friendly and its API is lengthy and repetitive, and storing actual files isn’t straightforward.

To address these issues, we now have Origin Private File System (OPFS), a straightforward file-based system exposed through an API that gives web applications access to a structured file access system directly on the user’s device. It allows writing and reading of large files (300+ MB) of all kinds and even folders without ever needing to send them to a server.

In this article, we will explore how to use OPFS to store files directly on the user’s device and why it’s great for privacy and good offline experiences on websites.

What Is OPFS?

OPFS is a special type of storage that lives inside the browser and is part of the File System Access API. What makes OPFS unique is that it’s private to your website’s origin, kind of like a hidden folder that only your site can see and use.

Unlike the regular file system on our computers, OPFS is virtual. The user doesn’t see or interact with the files directly.

OPFS works only in secure contexts (i.e., over HTTPS) and when testing on localhost. Stored data persists across browser restarts, just like saving a file to a hard drive.

You also don’t need to ask for permission to use OPFS, because there is no file picker or popup involved. Your app can read from and write to OPFS freely in the background. Just like other browser storage APIs, OPFS is subject to browser storage quota restrictions. The limit is not a fixed number, but depending on the device and browser, it usually falls between 300 MB to several gigabytes. The browser will dynamically allocate storage quota based on the device storage capacity, used space, site visit frequency, etc. You can check storage space being used by OPFS via navigator.storage.estimate().

Benefits of OPFS

  • Invisible to the user – Acts like a virtual folder that only your app can access
  • No permissions needed – You don’t need to ask the user for access
  • Origin-scoped – Only your website (on your domain) can see its files
  • Persistent – Data stays even after the browser is closed or restarted
  • Structured like a real file system – You can create folders, read/write files and store both text and binary data

Use Cases of OPFS

  • Building offline-first applications that work fine offline and sync later when online
  • Browser games that need to store large game state files for offline play
  • Personal document management like note-taking apps
  • Optimizing web-based video/audio editing apps
  • Optimizing web-based file editors

How to Access the OPFS

To access OPFS, we must get access to the root directory. To do that, we need to call navigator.storage.getDirectory(), which is a function that returns an object called FileSystemDirectoryHandle. This object represents the root directory of your OPFS.

The FileSystemDirectoryHandle Object

While the FileSystemDirectoryHandle object represents the root directory within the OPFS in this context, its meaning is not exclusive to just OPFS. The FileSystemDirectoryHandle object is a part of the broader File System Access API, and it can represent any directory depending on how you obtain it.

The two main ways to a FileSystemDirectoryHandle object include:

  • Via navigator.storage.getDirectory() – where it would now represent the root of your OPFS
  • Via File picker window.showDirectoryPicker() – where it would now represent a real directory on the user’s local device

Every other way we may obtain a FileSystemDirectoryHandle would represent a subdirectory (child or root) of either of the two main ways. In the context where the FileSystemDirectoryHandle object represents the root of our OPFS, the FileSystemDirectoryHandle object exposes methods that allow us to perform operations such as:

  • Creating or accessing files: getFileHandle(fileName, options)
  • Creating or accessing subdirectories: getDirectoryHandle()
  • Removing files or directories: removeEntry()
  • Listing contents: for await (const entry of dirHandle.values())

Prerequisites

To follow along with this tutorial, you should be familiar with basic HTML, CSS and JavaScript.

Browser Support of OPFS

While the Origin Private File System may be supported by most modern browsers, it works mainly in Chromium-based browsers (like Chrome and Edge).

What We Are Building

In this post, we are going to build a simple notepad application that stores text note files (.txt) in the OPFS and persists them even after page refreshes or when the browser closes.

The notepad app will have an input for the title, which will become the filename, a body for the note itself, a “Save Note” button and a “Refresh notes” button.

All notes saved to the OPFS will also be listed so we can see what we save. Each saved note will also have a “Load Note” button that reads the note back from the OPFS and a “Delete note” button that deletes the file from the OPFS.

Project Setup

  • First, create a folder where this app will live. I will call my folder notesApp.
  • Then launch your code editor and open that created folder.
  • Create an empty file called app.html.
  • Open app.html in a secure domain context or test with localhost as we will be doing in this guide using the VS Code Live Server.

Building a Basic User Interface

Let’s start by building a user Interface. Add the following to your app.html file:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Mini Notepad (OPFS)</title>
    <style>
      body {
        font-family: sans-serif;
        padding: 2rem;
        max-width: 600px;
        margin: auto;
      }
      input,
      textarea {
        width: 100%;
        margin-bottom: 1rem;
        padding: 0.5rem;
      }
      button {
        margin-right: 0.5rem;
        margin-top: 0.5rem;
        padding: 0.5rem 1rem;
      }
      .status {
        margin-top: 1rem;
        color: green;
      }
      ul {
        list-style: none;
        padding: 0;
      }
      li {
        margin-bottom: 0.5rem;
      }
    </style>
  </head>
  <body>
    <h1>Mini Notepad (OPFS)</h1>
    <input type="text" id="title" placeholder="Enter note title..." />
    <textarea id="note" placeholder="Compose your note here..."></textarea>
    <button>Save Note</button>
    <button>Refresh Notes List</button>
    <section>
      <h2>Saved Notes</h2>
      <ul id="notes-list"></ul>
    </section>
    <div class="status" id="status"></div>
    <script>
      const notesListEl = document.getElementById("notes-list");
      const statusEl = document.getElementById("status");
      const titleEl = document.getElementById("title");
      const noteEl = document.getElementById("note");

      function showStatus(msg) {
        statusEl.textContent = msg;
      }
    </script>
  </body>
</html>

We have created a basic HTML and CSS boilerplate webpage. We also have a saved notes section that will display all note files saved to OPFS and a status div element that will hold all success or error messages so we do not have to use the console.

In our script, we have referenced by ID all the elements of our app that may be dynamically updated or copied via our script. And lastly, we created a showStatus function that updates our status element with whatever message we output.

Now launch app.html with VS Code Live Server and you should have an interface that looks like this:

The user interface

Accessing the OPFS Root Directory

Add the following to your app.html file:

async function getOPFSRoot() {
  if (!("storage" in navigator && "getDirectory" in navigator.storage)) {
    alert("OPFS is not supported in this browser.");
    throw new Error("OPFS not supported");
  }
  return await navigator.storage.getDirectory();
}

To access the OPFS root directory, we created an asynchronous function called getOPFSRoot().

The function first checks if OPFS is supported in the browser you are using and then calls navigator.storage.getDirectory(), which obtains OPFS’s FileSystemDirectoryHandle and returns it.

By doing this, whenever we call getOPFSRoot() anywhere in app.html, we have access to the OPFS root directory.

How to Store a File in OPFS

Add the following to your app.html file:

async function saveNote() {
  const title = titleEl.value.trim();
  const content = noteEl.value;
  if (!title) return showStatus("Please enter a title.");
  try {
    const root = await getOPFSRoot();
    const handle = await root.getFileHandle(`${title}.txt`, { create: true });
    const writable = await handle.createWritable();
    await writable.write(content);
    await writable.close();
    showStatus(`Note '${title}' saved.`);
    loadNotesList();
  } catch (err) {
    console.error(err);
    showStatus("Error saving note.");
  }
}

To store a note file, we created a function called saveNote() that contains the logic so we can call the saveNote() function whenever we want. The function will first clean up the title input value because the title we provide will become the name of the text file we will save.

Our function will return an error status if the title input is empty because every file is required to have a name in order to be saved. Then we call our getOPFSRoot() function, which, through FileSystemDirectoryHandle, gives us access to the getFileHandle(fileName, options) function.

The getFileHandle() may get a handle to an existing file or, as in this case create: true, create a new file called {title}.txt.

Next, we open the {title}.txt file with handle.createWritable(), while still using the same handle.

This is similar to opening a regular file on your PC when you want to edit it. We then write the content of our note into the file with await writable.write(content). Here, content is a reference to the value of the <textarea> element, which is our note body.

Lastly, we call await writable.close(), which closes the file and consequently finalizes the file editing/writing.

It is important to note that opening a file doesn’t alter the file until you call .close(). This prevents files from being half-written.

Listing All Saved Files in OPFS

Add the following to your app.html file:

async function loadNotesList() {
  try {
    const root = await getOPFSRoot();
    notesListEl.innerHTML = "";
    for await (const [name, handle] of root.entries()) {
      const li = document.createElement("li");
      li.innerHTML = `
                <strong>${name}</strong>
                <button onclick="loadNote('${name}')">Load</button>
                <button onclick="deleteNote('${name}')">Delete</button>
              `;
      notesListEl.appendChild(li);
    }
    showStatus("Notes list updated.");
  } catch (err) {
    console.error(err);
    showStatus("Error loading notes list.");
  }
}

To list all saved files in OPFS, we must again get access to the root directory of OPFS via our function getOPFSRoot(), which is where all files are stored. Next, we loop through all the files and folders in the root directory, and each entry in the loop has a name and a handle. The handle is the object that you can use to read/load or write to the file.

At this point, we just loop through every file in the OPFS directory and display them all on our Saved Notes section.

Now I will attempt to save two notes to the OPFS.

Saving note files in OPFS and listing them

Reading a File from OPFS

Add the following to your app.html file:

async function loadNote(title) {
  try {
    const root = await getOPFSRoot();
    const handle = await root.getFileHandle(`${title}`);
    const file = await handle.getFile();
    const content = await file.text();
    titleEl.value = title.replace(".txt", "");
    noteEl.value = content;
    showStatus(`Note '${title}' loaded.`);
  } catch (err) {
    console.error(err);
    showStatus("Error loading note.");
  }
}

To read a file means to load the file and access its content, usually for displaying or editing. In our case, we do that using a helper function called loadNote(title), where title is the name of the note (i.e., the filename).

We proceed to obtain the root directory handle again using getOPFSRoot() and get the file handle reference of the file we want to read with getFileHandle(title), where title is the name of the note.

We open the file with getFile() and then use file.text() to parse the saved note as a string so we can copy that content to a variable.

We finally populate the corresponding value field so the user can view or edit the note.

Let’s go ahead and read the two files we created back to the UI.

Reading a saved note file from OPFS

Deleting a File from OPFS

Add the following to your app.html file:

async function deleteNote(title) {
  if (!confirm(`Delete note '${title}'?`)) return;
  try {
    const root = await getOPFSRoot();
    await root.removeEntry(`${title}`);
    showStatus(`Note '${title}' deleted.`);
    loadNotesList();
  } catch (err) {
    console.error(err);
    showStatus("Error deleting note.");
  }
}

Deleting a file from OPFS means removing the file completely from OPFS. To do that, we created a helper function named deleteNote(title). The helper function first implements a standard reconfirmation prompt using the browser’s confirm() function to avoid unintentional deletion, because without it OPFS will delete the file without warning.

We gain access to our root directory and use the reference handle to call removeEntry(title), which deletes the file provided as a parameter.

Let’s try to delete both saved files from OPFS.

Deleting saved notes from OPFS

Putting It All Together

Refresh the displayed saved notes by calling loadNotesList() whenever any manipulation happens to the OPFS. This helps prevent files being duplicated.

Your app.html file should look like this:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Mini Notepad (OPFS)</title>
    <style>
      body {
        font-family: sans-serif;
        padding: 2rem;
        max-width: 600px;
        margin: auto;
      }
      input,
      textarea {
        width: 100%;
        margin-bottom: 1rem;
        padding: 0.5rem;
      }
      button {
        margin-right: 0.5rem;
        margin-top: 0.5rem;
        padding: 0.5rem 1rem;
      }
      .status {
        margin-top: 1rem;
        color: green;
      }
      ul {
        list-style: none;
        padding: 0;
      }
      li {
        margin-bottom: 0.5rem;
      }
    </style>
  </head>
  <body>
    <h1>Mini Notepad (OPFS)</h1>
    <input type="text" id="title" placeholder="Enter note title..." />
    <textarea id="note" placeholder="Compose your note here..."></textarea>
    <button onclick="saveNote()">Save Note</button>
    <button onclick="loadNotesList()">Refresh Notes List</button>
    <section>
      <h2>Saved Notes</h2>
      <ul id="notes-list"></ul>
    </section>
    <div class="status" id="status"></div>
    <script>
      const notesListEl = document.getElementById("notes-list");
      const statusEl = document.getElementById("status");
      const titleEl = document.getElementById("title");
      const noteEl = document.getElementById("note");
      async function getOPFSRoot() {
        if (!("storage" in navigator && "getDirectory" in navigator.storage)) {
          alert("OPFS is not supported in this browser.");
          throw new Error("OPFS not supported");
        }
        return await navigator.storage.getDirectory();
      }
      async function saveNote() {
        const title = titleEl.value.trim();
        const content = noteEl.value;
        if (!title) return showStatus("Please enter a title.");
        try {
          const root = await getOPFSRoot();
          const handle = await root.getFileHandle(`${title}.txt`, {
            create: true,
          });
          const writable = await handle.createWritable();
          await writable.write(content);
          await writable.close();
          showStatus(`Note '${title}' saved.`);
          loadNotesList();
        } catch (err) {
          console.error(err);
          showStatus("Error saving note.");
        }
      }
      async function loadNotesList() {
        try {
          const root = await getOPFSRoot();
          notesListEl.innerHTML = "";
          for await (const [name, handle] of root.entries()) {
            const li = document.createElement("li");
            li.innerHTML = `
                <strong>${name}</strong>
                <button onclick="loadNote('${name}')">Load</button>
                <button onclick="deleteNote('${name}')">Delete</button>
              `;
            notesListEl.appendChild(li);
          }
          showStatus("Notes list updated.");
        } catch (err) {
          console.error(err);
          showStatus("Error loading notes list.");
        }
      }
      async function loadNote(title) {
        try {
          const root = await getOPFSRoot();
          const handle = await root.getFileHandle(`${title}`);
          const file = await handle.getFile();
          const content = await file.text();
          titleEl.value = title.replace(".txt", "");
          noteEl.value = content;
          showStatus(`Note '${title}' loaded.`);
        } catch (err) {
          console.error(err);
          showStatus("Error loading note.");
        }
      }
      async function deleteNote(title) {
        if (!confirm(`Delete note '${title}'?`)) return;
        try {
          const root = await getOPFSRoot();
          await root.removeEntry(`${title}`);
          showStatus(`Note '${title}' deleted.`);
          loadNotesList();
        } catch (err) {
          console.error(err);
          showStatus("Error deleting note.");
        }
      }
      function showStatus(msg) {
        statusEl.textContent = msg;
      }
      loadNotesList();
    </script>
  </body>
</html>

Storage Concerns

It is important to note that while OPFS may be generous with its storage limits, you should only store what you need, as the browser may throw a QuotaExceededError if your application gets close to the dynamic storage quota.

Conclusion

The Origin Private File System represents significant progress in structured browser storage technology. It gives web developers the ability to create, read, write and manage large files of various types directly on a user’s device without relying on server storage.

As we have seen from what we built in this article, OPFS is the solution for whatever web application you may be building that requires structured, persistent, secure, origin-scoped and offline file access.

Read the whole story
alvinashcraft
6 minutes ago
reply
Pennsylvania, USA
Share this story
Delete

Row Goals: Part 2

1 Share

Row Goals: Part 2


Going Further


If this is the kind of SQL Server stuff you love learning about, you’ll love my training. I’m offering a 75% discount to my blog readers if you click from here. I’m also available for consulting if you just don’t have time for that, and need to solve database performance problems quickly. You can also get a quick, low cost health check with no phone time required.

The post Row Goals: Part 2 appeared first on Darling Data.

Read the whole story
alvinashcraft
6 minutes ago
reply
Pennsylvania, USA
Share this story
Delete

Microsoft Post-Quantum Crypto APIs Are Now Generally Available

1 Share

I mentioned this in my talk on Quantum Computing at the PASS Data Community Summit: Microsoft has announced that the post-quantum cryptography APIs are now generally available.

If you’re not familiar with why this is important, with Quantum Computing appearing like it will become a usable reality within the next decade, it is likely that a particular algorithm, Shor’s Algorithm, will mean that RSA and elliptical curve cryptography (ECC) will be “broken.” Currently, much of public-private key cryptography (think SSL/TLS, certificates, digital signatures, as well as certificates and asymmetric keys within SQL Server) rely on these sets of cryptological algorithms. The post-quantum cryptographical algorithms are not susceptible to being broken and will end up replaces RSA and ECC.

The announcement indicates these APIs are now GA for Windows Server 2025, Windows 11 (24H2, 25H2), and .NET 10. It also indicates that post quantum cryptography (PQC) will be coming to Active Directory Certificates Services (ADCS) in early 2026. This means that private PKI utilizing ADCS will be able to begin issuing quantum safe certificates. There’s nothing out there yet about implementation into SQL Server, but I would think that we’d need to see it in the OS and .NET first.

The post Microsoft Post-Quantum Crypto APIs Are Now Generally Available appeared first on SQLServerCentral.

Read the whole story
alvinashcraft
7 minutes ago
reply
Pennsylvania, USA
Share this story
Delete

Query Exercise Answer: Generating Big TempDB Spills

1 Share

In last week’s Query Exercise, I challenged you to play some code golf to generate big spills with tiny T-SQL.

Today, I’m going to walk you through my thought process – the initial attempts I tried and failed with, and the discoveries I made along the way, because I think it makes for fun storytelling.

In the past, when I’ve seen people trying to generate a lot of data quickly, they’ve cross-joined SQL Server’s built-in system tables like sys.all_objects or sys.all_columns. The more times you cross-join them with each other, the more rows you create with a cartesian join. However, that takes up a lot of space to type, and we’re playing code golf here.

Attempt #1: Sorting Big Text Data

SQL Server 2022 brought big guns to this fight with the GENERATE_SERIES function, which accepts nice big numbers as input parameters. You can simply put in a starting number and ending number, and generate that many rows. Just one call to this can produce more rows than cross-joining two system tables in the same amount of typed characters.

I figured I’d generate a large number of rows, each with a big chunk of text attached to it:

SELECT value, REPLICATE('x', 9223372036854775808) AS x
FROM GENERATE_SERIES(-9223372036854775808, 9223372036854775808);

For each row returned from GENERATE_SERIES, I’m also calling the REPLICATE command to build what the cool kids call “unstructured data”, a big long nasty string. Whatever you do, don’t actually execute that query, but the estimate is amusing:

Big data

That gets me a lot of data, but no memory is required to generate those results. Heaven forbid you execute that thing, because whatever has to absorb the firehose of data is going to need a hell of a lot of memory. But our goal here is to generate workspace spills, and the easiest way to do that is to add an ORDER BY:

SELECT value, REPLICATE('x', 9223372036854775808) AS x
FROM GENERATE_SERIES(-9223372036854775808, 9223372036854775808)
ORDER BY x;

That doesn’t work, because SQL Server seems to understand that the replicated strings are all the same, so there’s no sort in the plan. Same if I try to just sort by the values, either ascending or descending. But slap just a little math in there:

SELECT value, REPLICATE('x', 9223372036854775808) AS x
FROM GENERATE_SERIES(-9223372036854775808, 9223372036854775808)
ORDER BY value - value;

The plan gets a nasty little sort:

Get Sorty

The desired memory grant on the sort is fairly high, at 1,125,899,906,843,128KB:

Grant with a capital G

That’s 1,125 petabytes, or 1.1 exabytes of memory desired.

Now normally, if I’m trying to get SQL Server to spill to tempdb, I might try to get it to underestimate the number of rows coming into the sort, thereby lowballing the memory grant. But who cares? None of you have a SQL Server with 1.1 exabytes worth of memory, nor 1.1 exabytes worth of TempDB space, so I feel fairly confident that when you hit execute on that query, you’re gonna have a bad time.

Again, that’s the estimated plan because I can’t hit execute on that, because I’d have to deal with a metric poopton of results. I can’t just select TOP 1, either, because I’ll get a lowball memory grant, and I’m going for a high score here. In fact, the memory grant difference between these two versions of the query is one of the most comical differences I’ve ever seen:

  • SELECT TOP 1: 24KB memory desired
  • SELECT TOP 100: 24KB memory desired
  • SELECT TOP 101: 1.1 exabytes desired

Given the size of the data I’m replicating, I’m not even sure SSMS would react okay to getting the top 101, so we’ll dump the results into a temp table:

SELECT TOP 101 value, REPLICATE('x', 9223372036854775808) AS x
INTO #t
FROM GENERATE_SERIES(-9223372036854775808, 9223372036854775808)
ORDER BY value - value;

The TOP 101 makes the resulting execution plan quite wide:

Wide query plan

And the desired memory grant is still at 1.1 exabytes:

I too desire 1.1 exabytes of memory

Finally, we have a query I can execute without bringing SSMS down with it, hahaha. I mean, I can execute it, but I can’t get the actual plans because it’ll fill up TempDB with astonishing speed.

If I put all of that on a single line, that’s 159 characters including spaces. I could make the query shorter by removing the TOP 101 and the INTO #t parts, but I think the fun part of a query like this is being able to knock a SQL Server over without taking your workstation down with it.

I hit execute, giddily excited at the thought of trashing my lab server, only to be met with the Sad Trombone of Disappointment:

Msg 8115, Level 16, State 2, Line 1
Arithmetic overflow error converting expression to data type int.
The statement has been terminated.

What the… I’m not using any integers here, only bigints. I started troubleshooting to see where SQL Server was implicitly converting my bigints into integers, and along the way, I stumbled upon something delightfully terrible.

Attempt #2: Exploiting GENERATE_SERIES

As part of my troubleshooting, I wanted to make sure GENERATE_SERIES really did work with bigints, so I started by getting the smallest numbers possible:

SELECT TOP 101 value
FROM GENERATE_SERIES(-9223372036854775807, 9223372036854775808);

And that returned data instantly. But when I tried to get the biggest numbers, what I should have done was passed in the max bigint as my starting point, and tell it to increment by -1 with each step. That’s not what I did – I just thought I could take a shortcut:

SELECT TOP 101 value
FROM GENERATE_SERIES(-9223372036854775807, 9223372036854775808)
ORDER BY 1 DESC;

I hit execute on that, waited a few seconds, waited a few more seconds, and realized I’d stumbled upon something wonderful. SQL Server doesn’t have any logic to reverse-engineer your ORDER BY, and push that sorting down into GENERATE_SERIES. No, you asked for a metric poopton of data, so SQL Server will gladly build all that data and sort it for you!

Simple generate series plan

And here’s the funniest part: the desired memory grant is still 1.1 exabytes, even though I’m not generating or sorting any text:

Exabyte grant

Since we’re not generating big text data, I can just plain hit execute on that query without worrying about overwhelming SSMS with the result sets (if they ever get produced.) As the query runs, it gradually fills up TempDB, as shown by sp_BlitzWho @ExpertMode = 1, which shows things like tempdb allocations and memory grant usage:

TempDB usage

The live query plan (also shown by sp_BlitzWho) dutifully shows the rows marching on through the plan:

Live query plan

And after a couple of minutes, we run TempDB out of space:

Msg 1105, Level 17, State 2, Line 14
Could not allocate space for object 'dbo.SORT temporary run storage:  140737559658496' 
in database 'tempdb' because the 'PRIMARY' filegroup is full due to lack of storage space 
or database files reaching the maximum allowed size. Note that UNLIMITED files are still 
limited to 16TB. Create the necessary space by dropping objects in the filegroup,
adding additional files to the filegroup, or setting autogrowth on for existing files 
in the filegroup.

Nice! So that’s a 100-character query that runs the server out of TempDB space, maxing out possible spills:

SELECT TOP 101 value FROM GENERATE_SERIES(-9223372036854775807, 9223372036854775808) ORDER BY 1 DESC

In the spirit of code golf, we can do a little tuning to reduce characters:

  • Remove the TOP 101 since the query won’t finish anyway, and I don’t have to worry about overwhelming SSMS’s memory
  • Change the select to select *
  • Change the bigint numbers to the min/max integer sizes – but then our desired memory grant is “just” 300GB

That brings us down to 70 characters for an estimated 300GB spill:

SELECT * FROM GENERATE_SERIES(-2147483648, 2147483647) ORDER BY 1 DESC

Although I guess as long as I’m at 70 characters, why use small numbers? Let’s change them all to 9s to get the same string length:

SELECT * FROM GENERATE_SERIES(-2147483648, 2147483647) ORDER BY 1 DESC
SELECT * FROM GENERATE_SERIES(-9999999999, 9999999999) ORDER BY 1 DESC

(Note that this technique ONLY works if you pass in -9999999999 as a starting number, or some other bigint. If you just try to GENERATE_SERIES(0,9999999999) it will fail, saying both the starting and ending numbers need to be the same datatype.)

And we get a 1.5TB desired memory grant, which is pretty respectable:

1.5TB memory grant

So I end up with a few final entries:

/* 59 characters, 150GB grant/spill on 2025: */
SELECT * FROM GENERATE_SERIES(0,2147483647) ORDER BY 1 DESC

/* 69 characters, 1.5TB grant/spill: */
SELECT * FROM GENERATE_SERIES(-9999999999,9999999999) ORDER BY 1 DESC

/* 87 characters, 1.1 petabyte grant/spill: */
SELECT * FROM GENERATE_SERIES(-9223372036854775807,9223372036854775808) ORDER BY 1 DESC

So in less than 100 characters, you can drain a SQL Server out of drive space for TempDB. Of course, keep in mind that the estimated memory grant may not be the amount of the actual spill – but I’ll leave it to the reader to provision a petabyte SSD and then let me know what the actual spill amount was.

I thought that was pretty good, but I watched y’all’s solutions come in, and I learned stuff!

Reece Goding’s Solution

Reece Goding (Github) and Andy Mallon (BlogLinkedIn) worked on a similar theme of using GENERATE_SERIES, but they leveraged a sneaky trick: using scientific notation to shorten the long integer text. To understand Reece’s 72-character solution, you have to know that this works first:

DECLARE @H BIGINT=9E18;
SELECT CAST(@H AS BIGINT);

Which produces a result of 9000000000000000000. That saves you some typing:

DECLARE @H BIGINT=9E18
SELECT*FROM GENERATE_SERIES(-@H,@H)ORDER BY 1DESC

/* Or on the same line, which is the same number of characters: */
DECLARE @H BIGINT=9E18;SELECT*FROM GENERATE_SERIES(-@H,@H)ORDER BY 1DESC

This is better than my solution in TWO ways. First, both Reece and Andy Mallon figured out that you could take out the spaces between SELECT*FROM and 1DESC, which blows my mind and saves “strokes” in code golf.

Second, the use of local variables surprises SQL Server because the query optimizer doesn’t realize that @H is going to be such a big number, so SQL Server lowballs the amount of memory required to run the query. Second, both Reece and Andy Mallon figured out that you could take out the spaces between SELECT*FROM and 1DESC, which blows my mind.

One downside is that it means the estimated plan for Reece’s query doesn’t show the true awfulness of the spill that would result. To approximate the spill, I’m going to revise his solution to natively use the @H values:

SELECT*FROM GENERATE_SERIES(-9000000000000000000, 9000000000000000000)ORDER BY 1DESC;

That would get a 1.1 petabyte memory grant, just like my 87-character solution – but Reece’s solution is just 72 characters! Excellent work. Andy Mallon used a similar approach, but without the variables.

Tuyen Nguyen’s Solution

Tuyen (Github) wrote, “I use the cardinality estimation limitation from using local variables so SQL Server misestimates the rows coming out from the function and going into the sort, guaranteeing a spill to TempDB on any server. A giant string sort key then magnifies the spill. The bigger @N is, the larger the spill, but also much slower the query is.”

I love this because it’s the spirit of what I thought for my first attempt, but she did a much more elegant job than I did, especially with the local variable. Top notch. Tuyen’s query:

DECLARE @N int = 100000
SELECT *
FROM GENERATE_SERIES(1, @N)
ORDER BY REPLICATE(NEWID(), 1000000000)

In the spirit of code golf, let’s switch the numbers to 9s, to get a worse query with no additional characters – on the order by, I need to use the max int size:

DECLARE @N int = 999999
SELECT *
FROM GENERATE_SERIES(1, @N)
ORDER BY REPLICATE(NEWID(), 2147483647)

Because this solution relies on mis-estimations, I have to actually execute the query to see the memory grants, so I’m going to modify it to only return TOP 101 to avoid causing problems with SSMS. The actual query plan:

Query plan with memory spills

Heh heh heh, there you go: 3,995,134 pages is a 30GB spill. Now, that may not sound like much, but you can use that exact technique and simply amp up the numbers in the query, and get yourself a much bigger spill.

How I Adapted Mine

Ripping off Mallon’s idea of ripping out spaces, here’s what I ended up with for my own answer:

/* 55 characters, 150GB grant/spill on 2025: */
SELECT*FROM GENERATE_SERIES(0,2147483647)ORDER BY 1DESC

/* 65 characters, 1.5TB grant/spill: */
SELECT*FROM GENERATE_SERIES(-9999999999,9999999999)ORDER BY 1DESC

/* 83 characters, 1.1 petabyte grant/spill: */
SELECT*FROM GENERATE_SERIES(-9223372036854775807,9223372036854775808)ORDER BY 1DESC

So in less than 100 characters, you can drain any SQL Server out of drive space for TempDB. Reece’s solution beats me though at 72 characters for that same giant spill.

If you liked this exercise, check out the ideas from other readers, and seriously, use SQL Server 2025’s Resource Governor feature to prevent users from doing something similar.

Read the whole story
alvinashcraft
7 minutes ago
reply
Pennsylvania, USA
Share this story
Delete

Artisan Brew House: A "WOW Factor" UI built with Uno Platform

1 Share

This is a submission for the AI Challenge for Cross-Platform Apps - WOW Factor

What I Built

I built a "WOW Factor" home screen for a premium, artisan coffee shop called the "Artisan Brew House."

The goal was to create a design that feels modern, cozy, and truly premium, moving far beyond a basic layout. The app uses a dark, multi-layered gradient background with warm, coffee-toned highlights to create a stunning visual vibe.

The entire UI is built 100% from XAML code, using LinearGradientBrush for backgrounds, Border with CornerRadius for "glass" and "card" effects, and FontIcon for all iconography (no images were used).

Demo

Here is the full-page screenshot of the app running on Windows Desktop, built from a single Uno Platform codebase.


Cross-Platform Magic

For this submission, I focused on building and running the app on Windows Desktop (using the net10.0-desktop framework).

The power of Uno Platform is that this single XAML codebase is designed to be cross-platform, allowing it to be compiled for Android, iOS, macOS, Linux, and WebAssembly with minimal effort.

Interactive Features

The design is highly creative and interactive, focusing on a premium user experience:

  • Brand Identity: A strong, elegant header section that establishes the "Artisan Brew House" brand.
  • "Today's Masterpiece": Instead of a boring list, I designed a "Hero Card" that showcases the main special brew. It features a glowing icon, descriptive tags, and a prominent "Order Now" button.
  • "Most Loved" Carousel: A horizontal ScrollViewer contains "Popular Brew" cards. Each card uses a RadialGradientB ![ ](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ret6780m4ko8ol9b9x71.png)rush and FontIcon to create a beautiful, minimalist visual for each drink.
  • Quick Access Cards: At the bottom, two "glass effect" cards (Background="#22FFFFFF") allow for quick access to "My Rewards" and "Find Store."
  • Main CTA: A final "Explore Full Menu" button with a gradient background serves as the main call-to-action for the entire screen.

The Wow Factor

The "WOW Factor" of this app is its world-class, premium design achieved entirely with 100% XAML code.

It proves that you don't need to rely on static images to create a rich, visually stunning, and modern UI. The use of multiple gradients, "glass" borders, layered elements (like the "Hero Card"), and fluid layout creates an experience that truly makes the user say "Wow."

Read the whole story
alvinashcraft
7 minutes ago
reply
Pennsylvania, USA
Share this story
Delete
Next Page of Stories