"It works" was a lie
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.
After some debugging and checking podcasts at e.g. https://podcasts.apple.com/de/podcast/sound-memes-der-k%C3%BCrzeste-comedy-podcast-der-welt/id1729757656 I found:
<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:
- detect provider
- check consent
- fetch oEmbed
- 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!