Sign in

Tom Sherman

@tom.sherman.is
8.5K followers 485 following 4.4K posts

Software Engineer in Norwich. he/him Working on @frontpage.fyi tom-sherman.com

PostsRepliesMedia
Tom Sherman @tom.sherman.is · 01/10/2026
So I just learned that in @typescriptlang.org any JSX element is allowed arbitrary attributes as long as they contain a `-` 🤔 It's documented as an exemption for invalid JS identifiers (why?) but I checked the source and literally only checks for hyphens. Insane behaviour
function Foo() {
  return null;
}

const jsx = <Foo foo foo-bar />;

the `foo` attribute on the component is underlined in red showing an error.Note: If an attribute name is not a valid JS identifier (like a data-* attribute), it is not considered to be an error if it is not found in the element attributes type.
170
Tom Sherman @tom.sherman.is · 19/08/2026
they can never convince me that sin and cosine are the same. sin is a wave and cos is literally just a v like what do you mean
Graph of f(x) = cos x and f(x) = sin x
140
Tom Sherman @tom.sherman.is · 10/07/2026
🪟✨ New in Glide v0.2.24 I added the ability to always float windows by matching on their app bundle id, title, or accessibility role
```toml
# Always float macos preferences
[[window_rules]]
if.app_id = "com.apple.systempreferences"
float = true
```
350
Tom Sherman @tom.sherman.is · 24/06/2026
I'm a guy with a small rack now
10" minirack inside of an IKEA wheeled trolley
3400
Tom Sherman @tom.sherman.is · 19/05/2026
fail whale
0130
Tom Sherman @tom.sherman.is · 30/04/2026
I'm doing my part 🫡
https://x.com/tomus_sherman/status/2049237186575306915?s=20
050
Tom Sherman @tom.sherman.is · 21/04/2026
excuse me???? codex just said it needs to rest
I think I might need some rest
190
Tom Sherman @tom.sherman.is · 07/04/2026
Is anyone else experiencing incredibly slow `playwright install --with-deps` or generally slow `apt-get` times in github actions? > Fetched 21.5 MB in 12min 52s (27.9 kB/s)
Fetched 21.5 MB in 12min 52s (27.9 kB/s)
020
Tom Sherman @tom.sherman.is · 07/04/2026
After updating to vite 8 99% of the build time is now bundling .d.ts files and running the react compiler Looking forward to TypeScript 7.0 and react compiler rust rewrite (and native rolldown integration) 😅
[PLUGIN_TIMINGS] Warning: Your build spent significant time in plugins. Here is a breakdown:
  - vite:dts (81%)
  - @rolldown/plugin-babel (18%)
See https://rolldown.rs/options/checks#plugintimings for more details.
180
Tom Sherman @tom.sherman.is · 27/03/2026
Better diagram (thank you @excalidraw.com text to diagram)
Visual representation of the flow described in https://nextjs.org/docs/app/api-reference/config/next-config-js/adapterPath#2-runtime-flow-serve-cached-shell-and-resume-in-background
010
Tom Sherman @tom.sherman.is · 27/03/2026
The Next.js Adapters API docs have probably the most succinct explanation of how PPR works under the hood nextjs.org/docs/app/api-ref…
https://nextjs.org/docs/app/api-reference/config/next-config-js/adapterPath#2-runtime-flow-serve-cached-shell-and-resume-in-background
110
Tom Sherman @tom.sherman.is · 25/02/2026
it has gulp too wow
same UI but shows gulp and grunt together
020
Tom Sherman @tom.sherman.is · 25/02/2026
rip king 🫡
a screenshot of vscode showing "select the task to run" command pallette UI with grunt task options below
140
Tom Sherman @tom.sherman.is · 08/01/2026
This came to me in a dream last night
What's the matter promptboy, afraid you might code something?
510011
Tom Sherman @tom.sherman.is · 31/12/2025
I do enjoy this graph generated by studio.outerbase.com Looking at it is the only form of enjoyment I have had on this task
Query plan visualized as a flow chart
000
Tom Sherman @tom.sherman.is · 20/12/2025
Me when bugs
Dog in hoodie tucked into bed
070
Tom Sherman @tom.sherman.is · 16/12/2025
Thoughts on this atproto login UI? Play with it on the link below but you need a Vercel account so i attached some screenshots too frontpage-git-rework-login-form-frontpagefyi.vercel.app/login?_vercel_sh…
Login UILogin with PDS modalLogin UI with help articles expanded
6202
Tom Sherman @tom.sherman.is · 06/12/2025
@bnewbold.net I don't seem to have any of the new `goat lex` commands after installing v0.2.0. Any idea why?
tom@TomusPC ~/c/unravel (main)> goat --version
goat version v0.2.0
tom@TomusPC ~/c/unravel (main)> goat lex check-dns --help
error: No help topic for 'check-dns'
tom@TomusPC ~/c/unravel (main) [255]> goat lex breaking --help
error: No help topic for 'breaking'
tom@TomusPC ~/c/unravel (main) [255]>
120
Tom Sherman @tom.sherman.is · 02/12/2025
I cleared cache/data and logged in again and now when I open the app I'm stuck with this UI
Screenshot of Bsky app
110
Tom Sherman @tom.sherman.is · 22/11/2025
I added lexicon linting and compatibility checks to @frontpage.fyi's CI. It ensures we don't accidentally introduce breaking changes to the lexicons by comparing against the published versions Uses @bnewbold.net's glot tool tangled.org/bnewbold.net... Workflow here: github.com/frontpagefyi...
Screenshot of https://github.com/frontpagefyi/frontpage/actions/runs/19571183678
0131
Tom Sherman @tom.sherman.is · 20/11/2025
Ah, opening a single image is quite snappy but opening an image from a post with multiple has a bad delay
451
Tom Sherman @tom.sherman.is · 18/11/2025
pov you want to remove a default value from a sqlite table
screenshot of a db migration showing a bunch of indexes being dropped, the column being altered, and then those same indexes being added again

https://github.com/frontpagefyi/frontpage/blob/64cece7619b6aeff709742c245f6f53c596e86f3/packages/frontpage/drizzle/0009_soft_strong_guy.sql
21423
Tom Sherman @tom.sherman.is · 17/11/2025
Triple grind this time
010
Tom Sherman @tom.sherman.is · 15/11/2025
I love this game
060
Tom Sherman @tom.sherman.is · 14/11/2025
I think about these people a lot
Screenshot of bsky.app search with the query "from:me". Below are three user accounts:

- from:me (slut.lexicon) @lexiconslut.bsky.social
- squeaky from:me @skeet.best
- from:me @lamp.bsky.social
0260
Tom Sherman @tom.sherman.is · 04/11/2025
I'm actually on the other panel
Character from "the garden of earthly delights" eating a giant strawberry
120
Tom Sherman @tom.sherman.is · 31/10/2025
It's been real
My wife (left) dressed as Lord Farquad, myself (right) dressed as Fiona (ogre version)
0110
Tom Sherman @tom.sherman.is · 22/10/2025
woah discover runs on graze????
Screenshot of the parent post displayed on the "Discover" feed in the Bluesky app
130
Tom Sherman @tom.sherman.is · 20/10/2025
My top 10 if anyone was asking @jakearchibald.com
1. JavaScript top-level await
2. CSS text-box and related longhand properties
3. Federated credential management (FedCM)
4. Async iterable ReadableStreams
5. HTML <link rel="prefetch">
6. JavaScript Temporal
7. CSS line-clamp property
8. HTML blocking="render" attribute on <link>, <script> and <style>
9. WebTransport API
10. Web Share API
020
Tom Sherman @tom.sherman.is · 15/10/2025
This is a worse failure mode than anything that exists in React today. I can't even compose things without worrying about when they call update
https://x.com/rickhanlonii/status/1978250938616828171?
170
Tom Sherman @tom.sherman.is · 14/10/2025
On the other hand this is potentially the worst UX in all of Bluesky right now
Screenshot of "select moderation service" step in the Bluesky report flow
161
Tom Sherman @tom.sherman.is · 14/10/2025
Yeah I wasn't creating a new abort controller and setting it in the ref, I also wasn't discarding the result of getCities when the transition was aborted Still left with a TODO which made me realise you can't totally abstract this pattern away, what you show on abort is specific to each use case
      <select
        onChange={(e) => {
          controllerRef.current?.abort();
          const controller = new AbortController();
          controllerRef.current = controller;

          const state = e.currentTarget.value;
          startTransition(async () => {
            const cities = await getCities(state, controller.signal);
            startTransition(() => {
              if (controller.signal.aborted) {
                // TODO: Show an aborted state here, maybe just clear the cities
                return;
              }
              setCities(cities);
            });
          });
        }}
      >
200
Tom Sherman @tom.sherman.is · 14/10/2025
Hmmmm, yeah you're right I don't think uAS is doing anything for me here I reached for it to solve the out of order completion bug but the abort controller itself solves that I think. Just using startTransition and useRef(AbortController) is much simpler
import { useRef, useState, useTransition } from 'react';
import './App.css';

function App() {
  const [isPending, startTransition] = useTransition();
  const controllerRef = useRef<null | AbortController>(null);
  const [cities, setCities] = useState<string[]>([]);

  return (
    <>
      <select
        onChange={(e) => {
          controllerRef.current?.abort();
          const state = e.currentTarget.value;

          startTransition(async () => {
            const cities = await getCities(state);
            startTransition(() => {
              setCities(cities);
            });
          });
        }}
      >
        <option />
        {Object.keys(stateCitites).map((state) => (
          <option key={state}>{state}</option>
        ))}
      </select>

      <select disabled={isPending}>
        {isPending ? (
          <option>Loading... </option>
        ) : (
          cities.map((city) => <option key={city}>{city}</option>)
        )}
      </select>
    </>
  );
}

const stateCitites: Record<string, string[]> = {
  Utah: ['Salt Lake City', 'Provo', 'West Valley City'],
  California: ['Los Angeles', 'San Francisco', 'San Diego'],
  Texas: ['Houston', 'Dallas', 'Austin'],
  Florida: ['Miami', 'Orlando', 'Tampa'],
  'New York': ['New York City', 'Buffalo', 'Rochester'],
};

async function getCities(state: string) {
  await new Promise((res) => setTimeout(res, Math.random() * 1500 + 100));
  return stateCitites[state] ?? [];
}

export default App;
110
Tom Sherman @tom.sherman.is · 10/10/2025
@lakshikag.bsky.social FYI your profile appears to be entirely broken, might be some messed up timestamps in some of your posts?
Screenshot of https://bsky.app/profile/did:plc:wnnem3ed7dqgdbljxq47onjq showing an internal server error in the author's posts feed
210
Tom Sherman @tom.sherman.is · 09/10/2025
@ricky.fm what are you thoughts on this useAbortableAction hook? useActionState runs the actions in sequence, this allows you to call abort syncronously before calling the action in a transition
function useAbortableActionState(action, initialState, permalink) {
  const controllerRef = (useRef < AbortController) | (null > null)

  function abort() {
    controllerRef.current?.abort()
  }

  function wrappedAction(state, payload) {
    const controller = new AbortController()
    controllerRef.current = controller

    return Promise.race([
      new Promise() <
        never >
        ((resolve) => {
          controller.signal.addEventListener('abort', () => {
            console.log('abort', state)
            // This will never actually get rendered by react
            // Rejecting here however will trigger an error boundary so we just resolve with a junk value
            resolve(undefined)
          })
        }),
      action(state, payload),
    ])
  }

  return [...useActionState(wrappedAction, initialState, permalink), abort]
}

// Usage:

function App() {
  const [cities, abortableAction, isPending, abort] =
    useAbortableActionState(action)

  return (
    <select
      onChange={(e) => {
        abort()
        const state = e.currentTarget.value

        startTransition(() => {
          updateCitiesAction(state)
        })
      }}
    />
  )
}
140
Tom Sherman @tom.sherman.is · 06/10/2025
40ms per poast
Screenshot of timing panel in chrome devtools

Queuing: 0.83ms
Stalled: 0.27ms
Requet sent: 0.15ms
Waiting for server response: 38.24ms
Content download: 0.56ms

Total: 40.05ms
060
Tom Sherman @tom.sherman.is · 26/09/2025
Certified loser just ordered the loser shoe
NikeCraft Pre-Order: General Purpose Participation Shoe
110
Tom Sherman @tom.sherman.is · 26/09/2025
DIDs don't have multiple identities, they can have multiple handles. The DID is the identity, a handle is like an alias. Sorry this is kinda a nitpick! As for your question, the spec basically says that apps should ignore all handles except the first one lists in the DID doc.
The current handle for the DID is found in the alsoKnownAs array. Each element of this array is a URI. Handles will have the URI scheme at://, followed by the handle, with no path or other URI parts. The current primary handle is the first valid handle URI found in the ordered list. Any other handle URIs should be ignored.
130
Tom Sherman @tom.sherman.is · 26/09/2025
Hmm, maybe I'm holding wrong but this just gives me "RPC session was shut down by disposing the main stub" when i do a second RPC on the client Or maybe it's a deno-specific issue 🤔
Screenshot of some code
100
Tom Sherman @tom.sherman.is · 25/09/2025
So instead, could the gateway multiplex many sessions over a single socket connection to the downstream services? I have the thing on the left running, could the right design be possible?
A diagram comparing two gateway connection models. The left shows a gateway opening a new socket to backend services for each client session. The right shows a gateway multiplexing multiple client sessions over a single, shared socket per backend service to reduce connection overhead.
100
Tom Sherman @tom.sherman.is · 25/09/2025
Looks like something like that can work! The types don't like it but it works at runtime Requires spinning up many websockets which doesn't feel ideal, especially as they need to be created per client Between the gateway and services it'd be better to have a single websocket per service somehow
Vscode screenshot of 4 code files: client, customer service, order service, and gateway
110
Tom Sherman @tom.sherman.is · 25/09/2025
It's incredibly easy to wire up in a deno server
import {
  RpcTarget,
  newHttpBatchRpcResponse,
  newWebSocketRpcSession,
} from "capnweb";

class MyApiServer extends RpcTarget {
  hello(name: string) {
    return `Hello, ${name}!`;
  }
}

Deno.serve({ port: 8000 }, (req) => {
  if (req.headers.get("upgrade") != "websocket") {
    return newHttpBatchRpcResponse(req, new MyApiServer());
  }

  const { socket, response } = Deno.upgradeWebSocket(req);

  newWebSocketRpcSession(socket, new MyApiServer());

  return response;
});
020
Tom Sherman @tom.sherman.is · 25/09/2025
imagine not supporting these types in the big 2025. common react flight w
Screenshot from the capnweb docs:

The following types are not supported as of this writing, but may be added in the future:

Map and Set
ArrayBuffer and typed arrays other than Uint8Array
RegExp
ReadableStream and WritableStream, with automatic flow control.
210
Tom Sherman @tom.sherman.is · 23/09/2025
Might just start posting all of my skate clips here, this game is so good
160
Tom Sherman @tom.sherman.is · 22/07/2025
Does anyone know of a JS library that can parse the package.json#repository field and give me a URL? The thing shown under "repository" in the npm UI
Repository UI in npm showing github.com/facebook/react
100
Tom Sherman @tom.sherman.is · 17/07/2025
This is not normal???
Sign in to Bluesky emails in my email inbox
240
Tom Sherman @tom.sherman.is · 06/06/2025
@graze.social Pretty sure you can ask the bsky API for this now, think it requires oauth
A popup modal titled: Please ass your email to your account
2120
Tom Sherman @tom.sherman.is · 17/05/2025
If you weren't aware of FedCM, IMO this would be such a good UX for atproto login. No entering handles (after the first time)
191
Tom Sherman @tom.sherman.is · 30/04/2025
I seem to use this pattern a lot with React form actions so thought I'd suggest adding it to the docs github.com/reactjs/react.de…
You can override the default reset behaviour of the form by additionally passing an onSubmit prop to the <form> component calls preventDefault on the event and calling the action yourself. Passing onSubmit alongside action ensures the form can be progressively enhanced if this component is rendered to HTML on the server.
190
Tom Sherman @tom.sherman.is · 28/04/2025
Just shipped some UX improvements to @frontpage.fyi login ✨ Those in the mycosphere can login without typing anything Self hosters can login by entering their PDS URL Also allows signups! You can signup to any (open) PDS (including bsky.social) without creating a Bluesky profile
A screenshot of the new Frontpage login form found at https://frontpage.fyi/login
5354