Strict Types and expo OTA

By
dracoblue
May 29, 2026

My next #notai generated post is about another day of developing the kiesel app. I really like the workflow of using expo.dev and its features to iterate fast on developing kiesel, but this time it confused me and made my debugging sessions longer. The strict types for typescript made my life very easy, but didn't match what I expected. And for atproto and Mastodon character limits I found a gentle way to make it possible to respect both.

Expo OTA JS Bundle

On this day I finally added expo-updates package from expo to kiesel. That makes it possible to push updates to the test devices, without expensive build times on expo's eas or even app store review.

expo-updates does just push JS bundle changes. It does not push any native code (Swift or Kotlin). I knew it, but I failed multiple times. When I introduced expo-sqlite for caching or expo-image-picker when posting new posts.

My new routine is, if I add a new native module: I increment the appVersion in package.json, then I run eas build and thus I also have a new runtime available. That makes sure that my app does not crash because of missing module. Two early mistakes: the auto-incremented build went somewhere it shouldn't have, and the developmentClient bundle ended up in the preview channel. My current eas.json separates the three cleanly:

{
  "build": {
    "development": {
      "developmentClient": true,
      "distribution": "internal"
    },
    "preview": {
      "distribution": "internal",
      "channel": "preview"
    },
    "production": {
      "autoIncrement": true,
      "channel": "production"
    }
  }
}

ATproto parent structure

When introducing reply parents (important if you want to make it clickable) you might guess, that reply.parent looks the same as a top level feed item parent.post.author.

But that's not the case. And my typescript code did think it was the case (I just copied it) and thus -- my code thought everything was fine and no replies were visible.

The fix:

-item.reply.parent.post.author.handle
+item.reply.parent.author.handle

Character Limits

For Mastodon 500 characters are allowed. For ATproto it's just up to 300 characters. I tried different versions of handling it. If Mastodon is active: we want 500 characters. If it is ATproto, then we want to use 300 characters. If both: we want to use 300 characters. The straight forward solution is Math.min!

But now it is just a matter of:

const CHAR_LIMITS: Record<ProtocolType, number> = {
  atproto: 300,
  mastodon: 500,
};

and (targets is a set of selected protocols):

const activeLimit = Math.min(
  ...Array.from(targets).map((t) => CHAR_LIMITS[t])
);
const remaining = activeLimit - text.length;

to calculate the remaining limit. Simple as such, but nice to avoid people being unhappy.

See you in the next dev day post!