{
  "$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"
}