{
"$type": "site.standard.document",
"content": {
"$type": "blog.pckt.content",
"items": [
{
"$type": "blog.pckt.block.text",
"plaintext": "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."
},
{
"$type": "blog.pckt.block.heading",
"level": 2,
"plaintext": "Refresh mode"
},
{
"$type": "blog.pckt.block.text",
"plaintext": "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."
},
{
"$type": "blog.pckt.block.text",
"plaintext": "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."
},
{
"$type": "blog.pckt.block.text",
"plaintext": "My new setFeed implementation respects a mode now:"
},
{
"$type": "blog.pckt.block.codeBlock",
"attrs": {
"language": "typescript"
},
"plaintext": " setFeed((prev) => {\n if (mode === \"initial\") {\n // Merge fresh posts with cached: fresh posts win on overlap\n if (prev.length === 0) return allNewPosts;\n const freshKeys = new Set(allNewPosts.map((p) => `${p.protocol}:${p.uri}`));\n const cachedOnly = prev.filter((p) => !freshKeys.has(`${p.protocol}:${p.uri}`));\n // Fresh first (newest), then remaining cached (older)\n const merged = [...allNewPosts, ...cachedOnly];\n merged.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());\n return merged;\n }\n\n // Merge: deduplicate by URI\n const seen = new Set(prev.map((p) => `${p.protocol}:${p.uri}`));\n const unique = allNewPosts.filter(\n (p) => !seen.has(`${p.protocol}:${p.uri}`)\n );\n\n if (mode === \"refresh\") {\n // Prepend new posts\n return [...unique, ...prev];\n }\n // mode === \"more\": append\n return [...prev, ...unique];\n });"
},
{
"$type": "blog.pckt.block.text",
"plaintext": "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)."
},
{
"$type": "blog.pckt.block.text",
"plaintext": "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."
},
{
"$type": "blog.pckt.block.heading",
"facets": [
{
"features": [
{
"$type": "blog.pckt.richtext.facet#bold"
}
],
"index": {
"byteEnd": 15,
"byteStart": 0
}
}
],
"level": 2,
"plaintext": "oEmbed endpoint"
},
{
"$type": "blog.pckt.block.text",
"plaintext": "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."
},
{
"$type": "blog.pckt.block.text",
"facets": [
{
"features": [
{
"$type": "blog.pckt.richtext.facet#link",
"uri": "https://podcasts.apple.com/de/podcast/sound-memes-der-k%C3%BCrzeste-comedy-podcast-der-welt/id1729757656"
},
{
"$type": "blog.pckt.richtext.facet#underline"
}
],
"index": {
"byteEnd": 155,
"byteStart": 51
}
}
],
"plaintext": "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:"
},
{
"$type": "blog.pckt.block.codeBlock",
"attrs": {
"language": "html"
},
"plaintext": "<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\">"
},
{
"$type": "blog.pckt.block.text",
"plaintext": "and noticed my url was completely wrong. It is just:"
},
{
"$type": "blog.pckt.block.codeBlock",
"attrs": [],
"plaintext": "https://podcasts.apple.com/api/oembed?url="
},
{
"$type": "blog.pckt.block.text",
"plaintext": "and this one works. The one before looked promising, too. But it was wrong."
},
{
"$type": "blog.pckt.block.heading",
"level": 2,
"plaintext": "4x useEffect or one state"
},
{
"$type": "blog.pckt.block.text",
"plaintext": "The embed component has 4 async steps:"
},
{
"$type": "blog.pckt.block.orderedList",
"attrs": {
"start": 1
},
"content": [
{
"$type": "blog.pckt.block.listItem",
"content": [
{
"$type": "blog.pckt.block.text",
"plaintext": "detect provider"
}
]
},
{
"$type": "blog.pckt.block.listItem",
"content": [
{
"$type": "blog.pckt.block.text",
"plaintext": "check consent"
}
]
},
{
"$type": "blog.pckt.block.listItem",
"content": [
{
"$type": "blog.pckt.block.text",
"plaintext": "fetch oEmbed"
}
]
},
{
"$type": "blog.pckt.block.listItem",
"content": [
{
"$type": "blog.pckt.block.text",
"plaintext": "render player as webview"
}
]
}
]
},
{
"$type": "blog.pckt.block.text",
"facets": [
{
"features": [
{
"$type": "blog.pckt.richtext.facet#code"
}
],
"index": {
"byteEnd": 54,
"byteStart": 45
}
}
],
"plaintext": "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."
},
{
"$type": "blog.pckt.block.text",
"facets": [
{
"features": [
{
"$type": "blog.pckt.richtext.facet#code"
}
],
"index": {
"byteEnd": 61,
"byteStart": 51
}
}
],
"plaintext": "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": "blog.pckt.block.codeBlock",
"attrs": [],
"plaintext": "type EmbedState =\n | { status: \"loading\" }\n | { status: \"no-provider\" }\n | { status: \"needs-consent\"; provider: ProviderConfig }\n | { status: \"fetching\"; provider: ProviderConfig }\n | { status: \"oembed-ready\"; provider: ProviderConfig; html: string; width?: number; height?: number }\n | { status: \"direct-embed\"; provider: ProviderConfig; embedUrl: string }\n | { status: \"error\"; provider: ProviderConfig | null };"
},
{
"$type": "blog.pckt.block.text",
"facets": [
{
"features": [
{
"$type": "blog.pckt.richtext.facet#code"
}
],
"index": {
"byteEnd": 17,
"byteStart": 8
}
}
],
"plaintext": "and one useEffect, which knows what it's used for:"
},
{
"$type": "blog.pckt.block.codeBlock",
"attrs": {
"language": "typescript"
},
"plaintext": " useEffect(() => {\n const init = async () => {\n const provider = getProviderForUrl(uri);\n if (!provider || (!provider.oembedEndpoint && !provider.buildEmbedUrl)) {\n setState({ status: \"no-provider\" });\n return;\n }\n\n const consented = await hasConsent(provider.id);\n if (!consented) {\n setState({ status: \"needs-consent\", provider });\n return;\n }\n\n loadEmbed(provider);\n };\n init();\n }, [uri]);"
},
{
"$type": "blog.pckt.block.text",
"facets": [
{
"features": [
{
"$type": "blog.pckt.richtext.facet#code"
}
],
"index": {
"byteEnd": 34,
"byteStart": 31
}
}
],
"plaintext": "And it has just one dependency uri. Looks nice? ;)"
},
{
"$type": "blog.pckt.block.text",
"plaintext": "See you in the next dev day post!"
}
]
},
"description": "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. 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 re...",
"path": "/it-works-was-a-lie-qd559a9",
"publishedAt": "2026-05-25T18:49:22+00:00",
"site": "at://did:plc:ks6l7qs37543awud4jyfl7o3/site.standard.publication/3mka7ezi7cjmw",
"tags": [],
"textContent": "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.\nRefresh mode\nThe 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.\nThe 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.\nMy new setFeed implementation respects a mode now:\n setFeed((prev) => {\n if (mode === \"initial\") {\n // Merge fresh posts with cached: fresh posts win on overlap\n if (prev.length === 0) return allNewPosts;\n const freshKeys = new Set(allNewPosts.map((p) => `${p.protocol}:${p.uri}`));\n const cachedOnly = prev.filter((p) => !freshKeys.has(`${p.protocol}:${p.uri}`));\n // Fresh first (newest), then remaining cached (older)\n const merged = [...allNewPosts, ...cachedOnly];\n merged.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());\n return merged;\n }\n\n // Merge: deduplicate by URI\n const seen = new Set(prev.map((p) => `${p.protocol}:${p.uri}`));\n const unique = allNewPosts.filter(\n (p) => !seen.has(`${p.protocol}:${p.uri}`)\n );\n\n if (mode === \"refresh\") {\n // Prepend new posts\n return [...unique, ...prev];\n }\n // mode === \"more\": append\n return [...prev, ...unique];\n });\nand 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).\nSince 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.\noEmbed endpoint\nFor 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.\nAfter 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:\n<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\">\nand noticed my url was completely wrong. It is just:\nhttps://podcasts.apple.com/api/oembed?url=\nand this one works. The one before looked promising, too. But it was wrong.\n4x useEffect or one state\nThe embed component has 4 async steps:\ndetect provider\ncheck consent\nfetch oEmbed\nrender player as webview\nMy 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.\nTo make it less complex, we had to introduce a new EmbedState which holds the overall state for the implementation of the embed component:\ntype EmbedState =\n | { status: \"loading\" }\n | { status: \"no-provider\" }\n | { status: \"needs-consent\"; provider: ProviderConfig }\n | { status: \"fetching\"; provider: ProviderConfig }\n | { status: \"oembed-ready\"; provider: ProviderConfig; html: string; width?: number; height?: number }\n | { status: \"direct-embed\"; provider: ProviderConfig; embedUrl: string }\n | { status: \"error\"; provider: ProviderConfig | null };\nand one useEffect, which knows what it's used for:\n useEffect(() => {\n const init = async () => {\n const provider = getProviderForUrl(uri);\n if (!provider || (!provider.oembedEndpoint && !provider.buildEmbedUrl)) {\n setState({ status: \"no-provider\" });\n return;\n }\n\n const consented = await hasConsent(provider.id);\n if (!consented) {\n setState({ status: \"needs-consent\", provider });\n return;\n }\n\n loadEmbed(provider);\n };\n init();\n }, [uri]);\nAnd it has just one dependency uri. Looks nice? ;)\nSee you in the next dev day post!",
"title": "\"It works\" was a lie",
"updatedAt": "2026-05-25T18:52:07+00:00"
}