"It works" was a lie

By
dracoblue
May 25, 2026

My next #notai generated post is about another day of developing the kiesel app. This dev day was a bit sad. The happy path of each implementation worked, but as soon as I tried it in real usage: fail.

Refresh mode

The list view of kiesel has support for pull-to-refresh. The last changes somehow made the list after pull-to-refresh: empty. Each of the tabs: empty. That means everything needs to be reloaded and the cache didn't work.

The reason was that the refresh mode was implemented in such a way that it replaced the array with: page 1. And if the cache filled many other pages - pull to refresh made it empty.

My new setFeed implementation respects a mode now:

      setFeed((prev) => {
        if (mode === "initial") {
          // Merge fresh posts with cached: fresh posts win on overlap
          if (prev.length === 0) return allNewPosts;
          const freshKeys = new Set(allNewPosts.map((p) => `${p.protocol}:${p.uri}`));
          const cachedOnly = prev.filter((p) => !freshKeys.has(`${p.protocol}:${p.uri}`));
          // Fresh first (newest), then remaining cached (older)
          const merged = [...allNewPosts, ...cachedOnly];
          merged.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
          return merged;
        }

        // Merge: deduplicate by URI
        const seen = new Set(prev.map((p) => `${p.protocol}:${p.uri}`));
        const unique = allNewPosts.filter(
          (p) => !seen.has(`${p.protocol}:${p.uri}`)
        );

        if (mode === "refresh") {
            // Prepend new posts
          return [...unique, ...prev];
        }
        // mode === "more": append
        return [...prev, ...unique];
      });

and this way it can take care of initial loading, refreshing when prepending (posts happening in the future) new posts or more when appending old posts (scroll further).

Since I want to keep the cursors for pagination API valid, I need to update those. If this does not happen, the pagination starts again at page 1.

oEmbed endpoint

For the Apple Podcasts oEmbed endpoint I found one stating something like embed podcasts apple com (I won't post it here for you to not copy it wrong) and the result looked good. I started the app and embedded Apple Podcasts entry and it looked ugly. And the JSON was HTML, no JSON at all. I was confused.

<link rel="alternate" type="application/json+oembed" href="https://podcasts.apple.com/api/oembed?url=https%3A%2F%2Fpodcasts.apple.com%2Fde%2Fpodcast%2Fsound-memes-der-k%25C3%25BCrzeste-comedy-podcast-der-welt%2Fid1729757656" title="„Sound Memes - Der kürzeste Comedy-Podcast der Welt“-Podcast – Apple Podcasts">

and noticed my url was completely wrong. It is just:

https://podcasts.apple.com/api/oembed?url=

and this one works. The one before looked promising, too. But it was wrong.

4x useEffect or one state

The embed component has 4 async steps:

  1. detect provider
  2. check consent
  3. fetch oEmbed
  4. render player as webview

My first implementation included 4 different useEffect calls, each of them checking for one variable for state change. That looks slim at the beginning. But I ran into race conditions in multiple ways. Duplicated entries and such things.

To make it less complex, we had to introduce a new EmbedState which holds the overall state for the implementation of the embed component:

type EmbedState =
  | { status: "loading" }
  | { status: "no-provider" }
  | { status: "needs-consent"; provider: ProviderConfig }
  | { status: "fetching"; provider: ProviderConfig }
  | { status: "oembed-ready"; provider: ProviderConfig; html: string; width?: number; height?: number }
  | { status: "direct-embed"; provider: ProviderConfig; embedUrl: string }
  | { status: "error"; provider: ProviderConfig | null };

and one useEffect, which knows what it's used for:

  useEffect(() => {
    const init = async () => {
      const provider = getProviderForUrl(uri);
      if (!provider || (!provider.oembedEndpoint && !provider.buildEmbedUrl)) {
        setState({ status: "no-provider" });
        return;
      }

      const consented = await hasConsent(provider.id);
      if (!consented) {
        setState({ status: "needs-consent", provider });
        return;
      }

      loadEmbed(provider);
    };
    init();
  }, [uri]);

And it has just one dependency uri. Looks nice? ;)

See you in the next dev day post!