Most recent blog posts

Or you can click on the categories to filter by topic

Adding client-to-server sync to PissueTracker

March 20, 2025
0 comments React, Bun, JavaScript

Last week I started a side project called PissueTracker which is a web app that lets you create issues/bugs. What's cool about it is that it uses an offline database, namely IndexedDB in the browser. That means that every time you create or edit an issue, it's stored in a database in your browser. The problem with this is that inevitably, you might reset your entire browser. Or, when auth is working, you might want to access your data from an entirely different browser. So I'm adding a client-to-server sync. (It'll become two-way sync later)

How it works is demonstrated with this code:


db.issues
  .update(issue.id, { state: newState, syncedAt: null })
  .then((updated) => {
    if (updated) {
      notifications.show(...);
      mutate(issue.id);

The payload sent to the .update() is first the ID, and then it's an object. No matter what the payload changes are, state: newState in this case, there's always syncedAt: null tacked on.
Then, after it has successfully updated the local database, it triggers that mutate function. Let's dig into that next.

The mutate function is based on the useMutation hook in TanStack Query which is a wrapper for making a fetch XHR POST request. It looks like this:


const issue = await db.issues.get({ id });
const response = await upfetch(`/api/sync/issue/${tracker.uuid}`, {
  method: "POST",
  body: { id, issue },
});
if (response.ok) {
  db.issues.update(id, { syncedAt: new Date() });
  return response.json();
}
throw new Error("Failed to sync issue");

Get it?
First, we set syncedAt: null into the local browser database.
Then, we send the issue object to the server. Once that's done, we update the browser database, again, but this time set syncedAt to a valid Date.

Once all of this is done, you have a copy of the same issue payload in the server and in the client. The only thing that is different is that the syncedAt is null on the server.

The server is an Hono server that uses PostgreSQL. All records are stored as JSONB in PostgreSQL. That means we don't need to update the columns in PostgreSQL if the columns change in the Dexie.js definition.

UPDATE

This idea is flawed. The problem is the use of incrementing integer IDs.
Suppose you have 2 distinct clients. I.e. two different people. One on a train (in a tunnel) and one on an airplane. If they both create a new row, they'd be sending:

id: 2
title: "Trains are noisy"
body: ...

and

id: 2
title: "Airplanes are loud"
body: ...

That would override each other's data when synchronized with the central server, later.

The only solution is UUIDs.

Starting a side project: PissueTracker

March 16, 2025
2 comments React, JavaScript

I've started a new side project called PissueTracker. It's an issue tracker, but adding the "P" because my name is "Peter" and it makes the name a bit more unique. Also, the name isn't important. What this project can do is important.

In a sense, it's a competitor to GitHub Issues. An issue can be anything such as a feature or bug report or just something that needs to be addressed. In another sense, it's a competitor to a web product I built 24 years ago called IssueTrackerProduct. That was a huge success and it was so much fun to build.

What's special about this issue tracker is that it's all about offline data, with server sync. All the issues you create and edit are stored in your browser. And if you don't want to lose all your data if/when your browser resets, you can have all that data constantly synchronized to the server which will either store it in PostgreSQL or the file system of a server. Since there's a shared data store, you can also authorize yourself amongst others in the system and this way it can become a collaborative experience.

I'm building it by using a React framework called Mantine, which I'm a huge fan of. The browser storage is done with Dexie.js which uses IndexedDB and allows you to store data in relational ways.

So far, I've only spent 1 weekend day on it but I now have a foundation. It doesn't support comments or auth, yet. And I haven't even started on the server sync. But it supports creating multiple trackers, creating/editing issues, fast filtering, and safe Markdown rendering.

The front end is handled by Vite, React Router, and Bun, I don't know if that's important. Right now, the most important thing is to have fun and build something that feels intuitive and so fast that it feels instantly responsive.

More to come!

Listing issues
List of issues in a tracker

Viewing an issue
Viewing an issue

UPDATE (Mar 20, 2025)

Blogged about the client-to-server sync here: "Adding client-to-server sync to PissueTracker."

Announcing: Spot the Difference

February 23, 2025
0 comments React, Bun, JavaScript

Spot the Difference is a web app where you're shown two snippets of code (programming, config files, style sheets, etc) and you're supposed to find the one difference. If you get it right, you get showered in confetti.

This started as an excuse to try out some relevant patterns in React that I was working on. I was also curious about writing a SPA (Single Page App) using Bun and React Router v7 (as a library). It started as a quick prototype, with no intention to keep it. But then I found it was quite fun to play and I liked the foundation of the prototype code; so I decided to clean it up and keep it. After all, it's free.

There's a goose on the home page because when I first showed my son the prototype, I said: "It's a silly goose game. Wanna try it?". So it stuck.

Game play in light mode

The technology behind it

Truncated! Read the rest by clicking the link below.

get in JavaScript is the same as property in Python

February 13, 2025
0 comments Python, JavaScript

Almost embarrassing that I didn't realize that it's the same thing!


class MyClass {
  get something() {
  }
}

is the same as Python's property decorator:


class MyClass
    @property
    def something:

They both make an attribute on a class automatically "callable".
These two are doing the same functionality:


class Foo {
  get greeting() {
    return "hello";
  }
  place() {
    return "world";
  }
}

const f = new Foo();
console.log(f.greeting, f.place());
// prints 'hello world'

and


class Foo:
    @property
    def greeting(self):
        return "hello"

    def place(self):
        return "world"

f = Foo()
print(f.greeting, f.place())
# prints 'hello word'

Use 'key' in React components to reset them

February 12, 2025
0 comments React

I'm sure you've seen this React code:


<ul>
  {users.map(user => <li key={user.id}>{user.name}</li>)}
</ul>

The key prop is necessary so each element is identifiable.

But did you know you can pass key to a component even though there's no .map(...) or other array of React elements.

Consider this simplified app:

Truncated! Read the rest by clicking the link below.

How to send custom headers in a loader in react-router v7

February 7, 2025
0 comments React, JavaScript

tl;dr; Use data() in your loader function and make your headers function pass it on to get headers be dependent on what's happening in the loader function.

I recently rewrote the front end of this website from Remix to react-router v7. A route is a page, which can be something like /about or have a parameter in it like /blog/:slug.

The way react-router v7 (the "framework mode") works is that your route looks like this:


import type { Route } from "./+types/post"

export async function loader({ params }: Route.LoaderArgs) {
  const post = await fetchPost(params.slug)
  return { post }
}

export default function Component({loaderData}: Route.ComponentProps) {
  return <h1>{loaderData.post.title}</h1>
}

So good for so far. But suppose you want this page to have a certain header, depending on the value of the post object. To set headers, you have to add an exported function called, surprise surprise; headers. For example:

Truncated! Read the rest by clicking the link below.

TypeScript enums without enums

January 29, 2025
0 comments JavaScript, TypeScript

My colleague @mattcosta7 demonstrated something that feels obvious in hindsight.

Instead of enums, use a regular object. So instead of


enum State {
  SOLID,
  LIQUID,
  GAS,
}

(playground here - note how "complex" the transpiled JavaScript becomes)

...use an object. Objects have the advantage that when the TypeScript is converted to JavaScript, it looks pretty much identical. TypeScript 5.8 makes it possible to disallow "non-erasable syntax" which means you can set up your tsconfig.json to avoid enum.

The alternative is an object. It's a bit more verbose but it has advantages:


const State = {
  SOLID: "solid",
  LIQUID: "liquid",
  GAS: "gas"
} as const

type State = typeof State[keyof typeof State]

(playground here - note how simple the transpiled JavaScript is)

In the above code, if you hover the mouse over State it'll say

'solid' | 'liquid' | 'gas'

Truncated! Read the rest by clicking the link below.

How I run standalone Python in 2025

January 14, 2025
1 comment Python

I don't do as much Python as I used to do. The few projects I still maintain, in Python, have a pyproject.toml and uv.lock. I.e. I'm using uv for getting the right executable version of Python and for installing dependencies. No more pip install ... and no more requirements.(in|txt). And definitely no poetry.lock.

And pyenv stopped working entirely when Python 3.12 came out. I used to use pyenv instead of Homebrew to get different versions of Python for different projects. uv is just that much better. I still use virtual envs, in the form of uv sync && source .venv/bin/activate when working inside a project and want to be able to type python ... and that referring to the exact version of Python with the relevant dependencies (from the pyproject.toml) installed.

However, there's a problem how: Outside of projects (that have a pyproject.toml and uv.lock) I no longer have a valid python executable. There's still a python3 executable that comes from /opt/homebrew/bin/python3 but that one I can't add dependencies to.
And many times I just want to whip up a quick script or start a repl, but with some certain dependencies installed. For example, to run...


import requests
print(requests.get('https://www.peterbe.com').headers['content-type'])
# prints 'text/html; charset=utf-8'

Again, uv to the rescue! I created ~/bin/python (plus chmod +x ~/bin/python) which now looks like this:


#!/bin/bash
set -x
uv run --python 3.12 --with requests python $@

Now I can quickly start a repl. Or if I create a /tmp/test-something.py I can just run that with


python /tmp/test-something.py

Truncated! Read the rest by clicking the link below.

My 2024 golf goals

December 30, 2024
0 comments Golf

When I started with an instructor at the start of this year (2024), I texted her about my goals. They were:

  • Eliminate big tee-off mistakes (slices, pulls)
  • Break 80
  • Handicap 6
  • Predictable positioning at setup

Admittedly, only "Break 80" and "Handicap 6" are measurable.

Text message about goals
Text message about goals

So how did I do?

Truncated! Read the rest by clicking the link below.

Previous page
Next page