<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title>defn.io</title><link>https://defn.io</link><description>Bogdan Popa's website.</description><language>en-US</language><lastBuildDate>Tue, 31 Mar 2026 11:25:11 +0300</lastBuildDate><atom:link rel="self" href="https://defn.io/index.xml" type="application/rss+xml"></atom:link><item><title>Announcing Ruckus</title><link>https://defn.io/2026/03/31/ann-ruckus</link><guid>https://defn.io/2026/03/31/ann-ruckus</guid><pubDate>Tue, 31 Mar 2026 11:21:00 +0300</pubDate><description>&lt;article&gt;&lt;p&gt;I'm happy to announce the public release of &lt;a href="https://ruckus.defn.io"&gt;Ruckus&lt;/a&gt;, a Racket IDE for
iPhone and iPad. Check it out and let me know what you think!&lt;/p&gt;&lt;p&gt;Ruckus is &lt;a href="https://github.com/Bogdanp/Ruckus"&gt;open source&lt;/a&gt; and based on &lt;a href="https://github.com/Bogdanp/Noise"&gt;Noise&lt;/a&gt;. The frontend was coded
almost entirely by Claude Code, with a lot of architecture guidance and
code review from me. The project was on the back of my mind for a while,
but I probably would never have started it were it not for CC becoming
quite good at (what I find to be) mundane tasks like this.&lt;/p&gt;&lt;p&gt;My workflow was&lt;sup&gt;&lt;a href="#fn_1" id="fnref_1_1"&gt;1&lt;/a&gt;&lt;/sup&gt; pretty simple. I would come up with a task and
tell CC to expand on it and write a plan to disk. Then I would ask it
to perform the task. It would summarize what it was about to do and
I'd correct it or tell it to go ahead. After it finished a first pass
at a task, I'd review the changes and concurrently ask it to review
them itself, either by running the &lt;code&gt;/simplify&lt;/code&gt; command or by telling
it to "Ultrathink&lt;sup&gt;&lt;a href="#fn_2" id="fnref_2_1"&gt;2&lt;/a&gt;&lt;/sup&gt; and review the pending changes for issues and
refactoring opportunities." While I don't have a counterexample to
compare with, my instinct is that this took overall less time than it
would've had I done everything manually -- which is not something I
would've felt even a few months ago with the state of the art at the
time.&lt;/p&gt;&lt;p&gt;I realize this is sounding like an ad for Anthropic, but I'm actually
ambivalent about these tools. On one hand, I love the act of programming
and I feel similarly to &lt;a href="https://www.eod.com/blog/2026/02/lose-myself/"&gt;Greg Knauss&lt;/a&gt;. On the other hand, they are
becoming genuinely useful, so I guess I'll continue to use them for work
I don't feel like doing for now.&lt;/p&gt;&lt;section class="footnotes"&gt;&lt;ol&gt;&lt;li id="fn_1"&gt;&lt;p&gt;It has since evolved to something more complicated that I'll
write about someday. But the version released today was built almost
entirely following this workflow. &lt;a class="footnote-backref" href="#fnref_1_1" title="Jump to reference" aria-label="Jump to reference"&gt;↩&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;&lt;li id="fn_2"&gt;&lt;p&gt;&lt;code&gt;ultrathink&lt;/code&gt; is a keyword that temporarily turns on maximum
thinking effort. It only works interactively, so I would keep repeating
more or less the same phrasing after each task. At one point, I even
considered adding a keyboard macro for it. &lt;a class="footnote-backref" href="#fnref_2_1" title="Jump to reference" aria-label="Jump to reference"&gt;↩&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;&lt;/ol&gt;&lt;/section&gt;&lt;/article&gt;</description></item><item><title>SO_KEEPALIVE Slow (?) on macOS</title><link>https://defn.io/2025/07/31/so-keepalive-slow-on-macos</link><guid>https://defn.io/2025/07/31/so-keepalive-slow-on-macos</guid><pubDate>Thu, 31 Jul 2025 08:00:00 +0300</pubDate><description>&lt;article&gt;&lt;p&gt;&lt;i&gt;Update: Mystery solved!&lt;/i&gt;&lt;/p&gt;&lt;p&gt;&lt;a href="https://github.com/pkazmier"&gt;Pete Kazmier&lt;/a&gt; reached out over e-mail asking if I could share some
packet captures of the problem. I sent them to him, and he figured out
the issue.&lt;/p&gt;&lt;p&gt;The Racket implementation was passing the wrong &lt;code&gt;level&lt;/code&gt; to &lt;code&gt;setsockopt&lt;/code&gt;,
namely &lt;code&gt;IPPROTO_TCP&lt;/code&gt; instead of &lt;code&gt;SOL_SOCKET&lt;/code&gt;. The particular combination
of the values of &lt;code&gt;IPPROTO_TCP&lt;/code&gt; and &lt;code&gt;SO_KEEPALIVE&lt;/code&gt; on macOS happens
to map to a configuration where the use of TCP options is disabled
altogether. See &lt;a href="https://github.com/racket/racket/commit/1c9468f029b37c5bb6ea140f6001a9a4639c216b#commitcomment-163229892"&gt;Pete's explanation&lt;/a&gt; on GitHub.&lt;/p&gt;&lt;p&gt;When I made my reproduction I, of course, didn't think to double check
that the original &lt;code&gt;setsockopt&lt;/code&gt; call was correct in the first place 🤦🏻‍♂️.&lt;/p&gt;&lt;p&gt;Original post below.&lt;/p&gt;&lt;p&gt;&lt;hr/&gt;&lt;/p&gt;&lt;p&gt;[Tested on macOS 14, 15 and 26.]&lt;/p&gt;&lt;p&gt;It turns out that enabling &lt;code&gt;SO_KEEPALIVE&lt;/code&gt; on a socket on macOS slows
operations through that socket to a crawl.&lt;/p&gt;&lt;p&gt;I noticed this while looking into a performance issue with downloads for
&lt;a href="https://podcatcher.net/"&gt;Podcatcher&lt;/a&gt;. I set up a remote server&lt;sup&gt;&lt;a href="#fn_1" id="fnref_1_1"&gt;1&lt;/a&gt;&lt;/sup&gt; to download a 1GB file, and
saw it was much slower than &lt;code&gt;curl&lt;/code&gt; or even Python's &lt;code&gt;requests&lt;/code&gt; library.
Then, I minimized the test down to a plain Racket TCP client:&lt;/p&gt;&lt;pre&gt;&lt;code info="language-racket"&gt;#lang racket/base

(require racket/port
         racket/tcp)

(define-values (in out)
  (tcp-connect "&amp;lt;HOST&amp;gt;" 8000))

(fprintf out "GET /1gb.bin HTTP/1.1\r\n")
(fprintf out "Connection: close\r\n")
(fprintf out "\r\n")
(tcp-abandon-port out)
(read-line in)
(time (copy-port in (open-output-nowhere)))
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Which, to my surprise, was also slow. Eventually, I tested it on a
Linux Docker container, and that was performing as expected. Then, I
tested it on a macOS machine running Racket 8.15, and that was also
performing well.&lt;/p&gt;&lt;p&gt;In version 8.17, Racket enabled &lt;code&gt;SO_KEEPALIVE&lt;/code&gt; for all TCP sockets by
default, and that turns out to be the culprit. For whatever reason,
only on macOS, if you turn on &lt;code&gt;SO_KEEPALIVE&lt;/code&gt; on a socket (client or
server), operations on that socket slow down significantly (2x-4x
between machines on the same WiFi network and a lot more when more hops
are involved&lt;sup&gt;&lt;a href="#fn_2" id="fnref_2_1"&gt;2&lt;/a&gt;&lt;/sup&gt;).&lt;/p&gt;&lt;p&gt;Here's a minimal C client program that reproduces the problem. To test
it, on a remote server, generate a large file by running &lt;code&gt;dd&lt;/code&gt;:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;dd if=/dev/zero of=1gb.bin bs=1MB count=1024
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then, serve it using Python:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;python -m http.server --bind 0.0.0.0 8000
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;On a Mac, compile and execute the following code, replacing the &lt;code&gt;HOST&lt;/code&gt;
and &lt;code&gt;PORT&lt;/code&gt; strings with appropriate values for your test:&lt;/p&gt;&lt;pre&gt;&lt;code info="language-c"&gt;#include &amp;lt;arpa/inet.h&amp;gt;
#include &amp;lt;netinet/tcp.h&amp;gt;
#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;
#include &amp;lt;string.h&amp;gt;
#include &amp;lt;sys/socket.h&amp;gt;
#include &amp;lt;unistd.h&amp;gt;

const char *HOST = "&amp;lt;HOST&amp;gt;";
const char *PORT = "&amp;lt;PORT&amp;gt;";

int sendstr(int sock, const char *str) {
  return send(sock, str, strlen(str), 0);
}

int main(void) {
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock &amp;lt; 0) {
        perror("socket");
        return 1;
    }

    int enable = 0; // Set to 1 to slow to a crawl
    if (setsockopt(sock, IPPROTO_TCP, SO_KEEPALIVE, &amp;amp;enable, sizeof(enable))) {
      perror("setsockopt");
      close(sock);
      return 1;
    }

    struct sockaddr_in server_addr;
    memset(&amp;amp;server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(atoi(PORT));

    if (inet_pton(AF_INET, HOST, &amp;amp;server_addr.sin_addr) &amp;lt;= 0) {
        perror("inet_pton");
        close(sock);
        return 1;
    }

    if (connect(sock, (struct sockaddr *)&amp;amp;server_addr, sizeof(server_addr)) &amp;lt; 0) {
        perror("connect");
        close(sock);
        return 1;
    }

    sendstr(sock, "GET /1gb.bin HTTP/1.1\r\n");
    sendstr(sock, "Connection: close\r\n");
    sendstr(sock, "\r\n");

    char buf[65536];
    size_t nread, total = 0;
    do {
      nread = recv(sock, buf, sizeof(buf), 0);
      total += nread;
      printf("%ldMiB\r", total/1024/1024);
    } while (nread &amp;gt; 0);
    printf("%ldMiB\n", total/1024/1024);

    close(sock);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Change the &lt;code&gt;enable&lt;/code&gt; flag to &lt;code&gt;1&lt;/code&gt; and then run it again to see the
difference.&lt;/p&gt;&lt;p&gt;For Racket, the fix is going to be to turn off &lt;code&gt;SO_KEEPALIVE&lt;/code&gt; on macOS.
I've sent a report to Apple (FB19250856) about this problem. Maybe
they'll have an idea about what's going wrong here.&lt;/p&gt;&lt;section class="footnotes"&gt;&lt;ol&gt;&lt;li id="fn_1"&gt;&lt;p&gt;The issue doesn't occur over loopback. &lt;a class="footnote-backref" href="#fnref_1_1" title="Jump to reference" aria-label="Jump to reference"&gt;↩&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;&lt;li id="fn_2"&gt;&lt;p&gt;In my test with the 1gb file on a remote server, the slowdown
was between 20x and 40x. &lt;a class="footnote-backref" href="#fnref_2_1" title="Jump to reference" aria-label="Jump to reference"&gt;↩&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;&lt;/ol&gt;&lt;/section&gt;&lt;/article&gt;</description></item><item><title>What is Racket DOING???</title><link>https://defn.io/2025/05/30/racket-thread-stack-dumps</link><guid>https://defn.io/2025/05/30/racket-thread-stack-dumps</guid><pubDate>Fri, 30 May 2025 07:35:00 +0300</pubDate><description>&lt;article&gt;&lt;p&gt;A neat feature of the JVM is that, out of the box, you can send
a running JVM process a &lt;code&gt;SIGQUIT&lt;/code&gt; signal and it'll dump stack
traces for all running threads to &lt;code&gt;stdout&lt;/code&gt;. The output looks like
&lt;a href="https://gist.github.com/Bogdanp/9d4a2c6d9a36243ff8acf81ac9a99696"&gt;this&lt;/a&gt;. It can be really handy when you're trying to debug a
live system.&lt;/p&gt;&lt;p&gt;Racket doesn't have this feature, but you can build something like it
yourself by combining some of the introspection tools the runtime system
provides to you. Given a Racket thread, you can get its &lt;a href="https://docs.racket-lang.org/reference/contmarks.html"&gt;continuation
marks&lt;/a&gt;, using the &lt;a href="https://docs.racket-lang.org/reference/contmarks.html#%28def._%28%28quote._~23~25kernel%29._continuation-marks%29%29"&gt;&lt;code&gt;continuation-marks&lt;/code&gt;&lt;/a&gt; procedure:&lt;/p&gt;&lt;pre&gt;&lt;code info="language-racket"&gt;&amp;gt; (continuation-marks (thread void))
#&amp;lt;continuation-mark-set&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;With a set of marks in hand, you can get an approximate stack trace by
calling &lt;a href="https://docs.racket-lang.org/reference/contmarks.html#%28def._%28%28quote._~23~25kernel%29._continuation-mark-set-~3econtext%29%29"&gt;&lt;code&gt;continuation-mark-set-&amp;gt;context&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;&lt;pre&gt;&lt;code info="language-racket"&gt;&amp;gt; (continuation-mark-set-&amp;gt;context
   (let ([thd (thread (λ () (let loop () (sleep 5) (loop))))])
     ;; Give the thread a chance to activate.
     (sync (system-idle-evt))
     (continuation-marks thd)))
(list (cons #f (srcloc 'string 2 20 53 42)))
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The result is a list of pairs of procedure names (or &lt;code&gt;#f&lt;/code&gt; if a procedure
name is not available, as above) and source locations. Converting that
list to a textual stack trace is straightforward.&lt;/p&gt;&lt;p&gt;The next step is to get a list of all running threads.  All threads in
Racket are managed by a &lt;a href="https://docs.racket-lang.org/reference/eval-model.html#%28tech._custodian%29"&gt;custodian&lt;/a&gt;. If you have access to a custodian
and its parent, you can ask for all of the objects managed by that
custodian by calling &lt;a href="https://docs.racket-lang.org/reference/custodians.html#%28def._%28%28quote._~23~25kernel%29._custodian-managed-list%29%29"&gt;&lt;code&gt;custodian-managed-list&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;&lt;pre&gt;&lt;code info="language-racket"&gt;&amp;gt; (define root (current-custodian))
&amp;gt; (current-custodian (make-custodian root))
&amp;gt; (define thd (thread (lambda () (let loop () (sleep 5) (loop)))))
&amp;gt; (custodian-managed-list (current-custodian) root)
'(#&amp;lt;thread&amp;gt;)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The result may include custodians subordinate to the custodian you're
querying:&lt;/p&gt;&lt;pre&gt;&lt;code info="language-racket"&gt;;; ... continued from above
&amp;gt; (define child (make-custodian))
&amp;gt; (define thd-of-child
    (parameterize ([current-custodian child])
      (thread (lambda () (let loop () (sleep 5) (loop))))))
&amp;gt; (custodian-managed-list (current-custodian) root)
'(#&amp;lt;thread&amp;gt; #&amp;lt;custodian&amp;gt;)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;So, you have to collect the list of threads recursively by dispatching
on the types of the values in the list:&lt;/p&gt;&lt;pre&gt;&lt;code info="language-racket"&gt;;; ...continued from above
&amp;gt; (define thds
    (let loop ([v (current-custodian)])
      (cond
        [(thread? v) (list v)]
        [(custodian? v) (loop (custodian-managed-list v root))]
        [(list? v) (apply append (map loop v))]
        [else null])))
&amp;gt; thds
'(#&amp;lt;thread&amp;gt; #&amp;lt;thread&amp;gt;)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;With that, you can print stack traces for all threads reachable from
the topmost custodian your program or library has access to.&lt;/p&gt;&lt;p&gt;This is now a &lt;a href="https://github.com/Bogdanp/racket-dbg/blob/f1f91f74b440b795bf858fa5d596d80db25072c5/dbg/private/stackdump.rkt"&gt;a built-in feature&lt;/a&gt; of &lt;a href="https://docs.racket-lang.org/dbg-manual/index.html"&gt;dbg&lt;/a&gt;. The client has a
new &lt;a href="https://docs.racket-lang.org/dbg-manual/index.html#%28def._%28%28lib._debugging%2Fclient..rkt%29._dump-threads%29%29"&gt;&lt;code&gt;dump-threads&lt;/code&gt;&lt;/a&gt; procedure that returns a string representing the
stack traces of all the threads accessible by the debugging server in
a process&lt;sup&gt;&lt;a href="#fn_1" id="fnref_1_1"&gt;1&lt;/a&gt;&lt;/sup&gt; and the GUI displays that same information under a new
"Threads" tab.&lt;/p&gt;&lt;section class="footnotes"&gt;&lt;ol&gt;&lt;li id="fn_1"&gt;&lt;p&gt;More specifically, in a &lt;a href="https://docs.racket-lang.org/reference/places.html"&gt;place&lt;/a&gt;, since each place has its own custodian tree. &lt;a class="footnote-backref" href="#fnref_1_1" title="Jump to reference" aria-label="Jump to reference"&gt;↩&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;&lt;/ol&gt;&lt;/section&gt;&lt;/article&gt;</description></item><item><title>Sharing Data Between Widgets and iOS Apps</title><link>https://defn.io/2025/04/13/communicating-with-widgets-on-ios</link><guid>https://defn.io/2025/04/13/communicating-with-widgets-on-ios</guid><pubDate>Sun, 13 Apr 2025 09:31:00 +0300</pubDate><description>&lt;article&gt;&lt;p&gt;Following up on the &lt;a href="/2025/04/13/performing-widget-intents-in-ios-app/"&gt;previous post&lt;/a&gt;, another thing that's not
well-documented when it comes to implementing widgets on iOS is how to
share data between the widget and the main app.&lt;/p&gt;&lt;p&gt;The widget is supposed to be small and efficient so loading all your
models in there seems wrong (and the extension probably(?) can't even
access the app's sandboxed database).&lt;/p&gt;&lt;p&gt;The documentation mentions using network requests a bunch, presumably
because a lot of these widgets are expected to be used to display remote
data, but what about if you want to keep everything local?&lt;/p&gt;&lt;p&gt;The best solution I've found so far is to use &lt;code&gt;UserDefaults&lt;/code&gt; with a
shared &lt;a href="https://developer.apple.com/documentation/xcode/configuring-app-groups"&gt;app group&lt;/a&gt;. I created a new app group (&lt;code&gt;Target Settings -&amp;gt; Signing &amp;amp; Capabilities -&amp;gt; Add Capability -&amp;gt; App Groups&lt;/code&gt;) and added both
the main app target and the widget extension target to it.&lt;/p&gt;&lt;p&gt;Then, I made some &lt;code&gt;Codable&lt;/code&gt; structs that are shared between the two
targets and a coordinator, also shared between the targets, that reads
and writes those structs as JSON through a &lt;code&gt;UserDefaults(suiteName: "app-group-id")&lt;/code&gt; instance.&lt;/p&gt;&lt;p&gt;Finally, whenever something relevant to the widgets happens in the app,
an event listener updates the shared data and calls &lt;a href="https://developer.apple.com/documentation/widgetkit/widgetcenter"&gt;&lt;code&gt;WidgetCenter&lt;/code&gt;&lt;/a&gt;'s
&lt;code&gt;reloadAllTimelines&lt;/code&gt; method to have the system instruct the widgets to
reload the next time they're rendered. As far as I can tell, reloading
the timelines is always deferred, so calling &lt;code&gt;reloadAllTimelines&lt;/code&gt;
multiple times in a row won't cause issues.&lt;/p&gt;&lt;/article&gt;</description></item><item><title>Performing Widget Intents in-app on iOS</title><link>https://defn.io/2025/04/13/performing-widget-intents-in-ios-app</link><guid>https://defn.io/2025/04/13/performing-widget-intents-in-ios-app</guid><pubDate>Sun, 13 Apr 2025 09:09:00 +0300</pubDate><description>&lt;article&gt;&lt;p&gt;I'm currently adding widgets to &lt;a href="https://apps.apple.com/app/podcatcher-podcast-player/id6736467324"&gt;Podcatcher&lt;/a&gt; and one issue I've run into
that's not very well documented is how to trigger an App Intent — for
example, to start playing a Podcast — from a widget, ensuring that the
intent is performed in the app.&lt;/p&gt;&lt;p&gt;There are two problems that need to be solved:&lt;/p&gt;&lt;ol class="loose" start="1"&gt;&lt;li&gt;&lt;p&gt;The intent has to be a part of both the main app target and the
widget extension target. How do we avoid including all of the app's
dependencies in the widget extension?&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Since the intent is going to be included in both targets, after we
solve 1), how do we ensure that the intent gets run inside the main app
process.&lt;/p&gt;&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;To solve the first problem, I added an Active Compilation Condition
(&lt;code&gt;Build Settings -&amp;gt; Swift Compiler - Custom Flags -&amp;gt; Active Compilation Conditions&lt;/code&gt;) to the main app target for both Debug and Release builds.
I called it &lt;code&gt;MAIN_APP&lt;/code&gt;. With the flag in place, I can conditionally
compile the &lt;code&gt;perform&lt;/code&gt; method of the intent so that it only does stuff
when invoked within the app:&lt;/p&gt;&lt;pre&gt;&lt;code info="language-swift"&gt;struct TogglePlaybackIntent: AppIntent {
  nonisolated static let title: LocalizedStringResource = "Toggle Playback"
  nonisolated static let description = IntentDescription("Plays/pauses the Podcatcher queue.")

  @MainActor
  func perform() async throws -&amp;gt; some IntentResult {
#if MAIN_APP
    // actual playback code here
#endif
    return .result()
  }
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This way, I can include the intent in the extension target and refer
to it in a button without bringing in all the other deps (models,
audio engine, etc.) that are needed to actually play a podcast.&lt;/p&gt;&lt;p&gt;To solve the second problem, you have to either set the &lt;code&gt;openAppWhenRun&lt;/code&gt;
member to &lt;code&gt;true&lt;/code&gt; within your intent implementation, or have the intent
implement the &lt;code&gt;AudioPlaybackIntent&lt;/code&gt; instead of &lt;code&gt;AppIntent&lt;/code&gt;. The latter
was more appropriate for this particular intent, so that's what I did:&lt;/p&gt;&lt;pre&gt;&lt;code info="language-diff"&gt;- struct TogglePlaybackIntent: AppIntent {
+ struct TogglePlaybackIntent: AudioPlaybackIntent {
&lt;/code&gt;&lt;/pre&gt;&lt;/article&gt;</description></item><item><title>DSLs for Safe iOS/watchOS Communication</title><link>https://defn.io/2025/02/16/type-safe-watchos-communication</link><guid>https://defn.io/2025/02/16/type-safe-watchos-communication</guid><pubDate>Sun, 16 Feb 2025 08:00:00 +0200</pubDate><description>&lt;article&gt;&lt;p&gt;I'm currently writing an Apple Watch counterpart app for &lt;a href="https://apps.apple.com/us/app/podcatcher-podcast-app/id6736467324"&gt;Podcatcher&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;The &lt;a href="https://developer.apple.com/documentation/watchconnectivity"&gt;Watch Connectivity&lt;/a&gt; framework that iOS apps use to communicate with
watchOS apps offers a limited API for communication: you can either send
untyped dictionaries or arbitrary byte strings between the two.&lt;/p&gt;&lt;p&gt;In a large app, you want more structure than that framework offers. One
approach you can take is to encode shared structs as JSON and pass them
around as byte strings, manually writing all the boilerplate code to
ensure that a response to a certain message decodes to the right type,
and so on. A better approach is to write a little DSL to declare all the
message and response types and use that information to generate code to
handle the encoding/decoding boilerplate and to ensure that the right
message handlers are implemented on either side. In Podcatcher, that
currently looks like this:&lt;/p&gt;&lt;pre&gt;&lt;code info="language-racket"&gt;(define-enum AppPlaybackState
  [empty]
  [paused
   {item : (Delay WatchQueueItem)}
   {progress : UVarint}
   {duration : UVarint}]
  [playing
   {item : (Delay WatchQueueItem)}
   {progress : UVarint}
   {duration : UVarint}])

(define-record WatchQueueItem
  [podcast-title : String]
  [episode-id : UVarint]
  [episode-title : String]
  [episode-progress : UVarint]
  [episode-duration : (Optional UVarint)]
  [enclosure-path : (Optional String)]
  [completed? : Bool]
  [order : UVarint])

(define-record WatchQueue
  [items : (Listof WatchQueueItem)])

(define-watch-rpcs
  WatchMessage ;; watch -&amp;gt; app messages
  [get-playback-state : AppPlaybackState]
  [go-backward : Bool]
  [go-forward : Bool]
  [pause : Bool]
  [play {episode-id : UVarint} : Bool]
  [resume : Bool]
  [sync-queue {local-items : (Listof WatchQueueItem)} : WatchQueue]
  [want-files {episode-ids : (Listof UVarint)} : Bool])
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This takes advantage of &lt;a href="https://github.com/Bogdanp/Noise"&gt;Noise&lt;/a&gt;'s &lt;a href="https://docs.racket-lang.org/noise-manual/index.html#%28form._%28%28lib._noise%2Fserde..rkt%29._define-enum%29%29"&gt;define-enum&lt;/a&gt; and &lt;a href="https://docs.racket-lang.org/noise-manual/index.html#%28form._%28%28lib._noise%2Fserde..rkt%29._define-record%29%29"&gt;define-record&lt;/a&gt;
to generate Swift enums and structs that can be serialized and
deserialized to and from byte strings. On top of that functionality, the
&lt;code&gt;define-watch-rpcs&lt;/code&gt; macro declares what all the watchOS to iOS messages
are and generates:&lt;/p&gt;&lt;ol class="tight" start="1"&gt;&lt;li&gt;an enum representing the messages,&lt;/li&gt;&lt;li&gt;code to send a message from the watch app to the phone app,&lt;/li&gt;&lt;li&gt;a protocol for handling those messages in the phone app and&lt;/li&gt;&lt;li&gt;code to wire up the message-receiving side to the protocol implementation.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;The generated &lt;code&gt;WatchMessage&lt;/code&gt; enum looks like this:&lt;/p&gt;&lt;pre&gt;&lt;code info="language-swift"&gt;public enum WatchMessage: Readable, Sendable, Writable {
  case getPlaybackState
  case goBackward
  case goForward
  case pause
  case play(UVarint)
  case resume
  case syncQueue([WatchQueueItem])
  case wantFiles([UVarint])

  // ser/de code elided
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The generated code for sending these messages from the watch app to the
phone app looks like this:&lt;/p&gt;&lt;pre&gt;&lt;code info="language-swift"&gt;extension WCSessionManager {
  func getPlaybackState() async throws -&amp;gt; AppPlaybackState {
    return try await send(message: WatchMessage.getPlaybackState)
  }

  func goBackward() async throws -&amp;gt; Bool {
    return try await send(message: WatchMessage.goBackward)
  }

  func goForward() async throws -&amp;gt; Bool {
    return try await send(message: WatchMessage.goForward)
  }

  func pause() async throws -&amp;gt; Bool {
    return try await send(message: WatchMessage.pause)
  }

  func play(episodeId: UVarint) async throws -&amp;gt; Bool {
    return try await send(message: WatchMessage.play(episodeId))
  }

  func resume() async throws -&amp;gt; Bool {
    return try await send(message: WatchMessage.resume)
  }

  func syncQueue(localItems: [WatchQueueItem]) async throws -&amp;gt; WatchQueue {
    return try await send(message: WatchMessage.syncQueue(localItems))
  }

  func wantFiles(episodeIds: [UVarint]) async throws -&amp;gt; Bool {
    return try await send(message: WatchMessage.wantFiles(episodeIds))
  }
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The generated protocol for handling these messages in the phone app
looks like this:&lt;/p&gt;&lt;pre&gt;&lt;code info="language-swift"&gt;protocol WatchMessageHandler {
  func getPlaybackState(session: WCSession) -&amp;gt; AppPlaybackState
  func goBackward(session: WCSession) -&amp;gt; Bool
  func goForward(session: WCSession) -&amp;gt; Bool
  func pause(session: WCSession) -&amp;gt; Bool
  func play(session: WCSession, episodeId: UVarint) -&amp;gt; Bool
  func resume(session: WCSession) -&amp;gt; Bool
  func syncQueue(session: WCSession, localItems: [WatchQueueItem]) -&amp;gt; WatchQueue
  func wantFiles(session: WCSession, episodeIds: [UVarint]) -&amp;gt; Bool
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Finally, the generated code to wire receiving the messages to an
implementation of the protocol looks like this:&lt;/p&gt;&lt;pre&gt;&lt;code info="language-swift"&gt;extension AppDelegate: WCSessionManagerDelegate {
  nonisolated func handle(session: WCSession, watchMessage message: WatchMessage) -&amp;gt; any Writable {
    switch message {
    case .getPlaybackState:
      return getPlaybackState(session: session)
    case .goBackward:
      return goBackward(session: session)
    case .goForward:
      return goForward(session: session)
    case .pause:
      return pause(session: session)
    case .play(let episodeId):
      return play(session: session, episodeId: episodeId)
    case .resume:
      return resume(session: session)
    case .syncQueue(let localItems):
      return syncQueue(session: session, localItems: localItems)
    case .wantFiles(let episodeIds):
      return wantFiles(session: session, episodeIds: episodeIds)
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The watch app sends the phone app a message by calling one of the
methods defined in the &lt;code&gt;WCSessionManager&lt;/code&gt; extension. The phone app
handles the message in its implementation of the &lt;code&gt;WatchMessageHandler&lt;/code&gt;
protocol and returns a response.&lt;/p&gt;&lt;p&gt;That short &lt;code&gt;define-watch-rpcs&lt;/code&gt; declaration from the first code snippet
saves me a lot of manual typing and error-prone wiring up of things.
When I add a new message case to the &lt;code&gt;WatchMessage&lt;/code&gt; enum, all I have to
do is implement its associated handler. If I forget to do that, the app
doesn't compile.&lt;/p&gt;&lt;p&gt;You can find the full implementation of the &lt;code&gt;define-watch-rpcs&lt;/code&gt; macro
and its associated codegen procedures &lt;a href="https://gist.github.com/Bogdanp/6d800c1064c60ff5d7579e2caed0ca51"&gt;in this gist&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;Now that Swift also has macros in the language, you could probably write
a DSL like this directly in Swift, but I just used what I know, and
Swift macros look somewhat clunky compared to what Racket offers.&lt;/p&gt;&lt;/article&gt;</description></item><item><title>Batch Inserts in PostgreSQL</title><link>https://defn.io/2025/02/15/postgres-batch-inserts</link><guid>https://defn.io/2025/02/15/postgres-batch-inserts</guid><pubDate>Sat, 15 Feb 2025 11:40:00 +0200</pubDate><description>&lt;article&gt;&lt;p&gt;I recently added &lt;a href="https://docs.racket-lang.org/koyo/database.html#%28part._database-batch%29"&gt;support for batch inserts&lt;/a&gt; to &lt;a href="https://docs.racket-lang.org/koyo/"&gt;koyo&lt;/a&gt; and thought
I'd make a quick post about it. Here's what it looks like to use this
feature:&lt;/p&gt;&lt;pre&gt;&lt;code info="language-racket"&gt;(define ib
  (make-insert-batcher
   #:on-conflict '(do-nothing (ticker))
   'tickers
   '([isin "TEXT"]
     [ticker "TEXT"]
     [added_at "TIMESTAMPTZ"])))
(with-database-connection [conn db]
  (for ([(isin ticker added-at) (in-sequence datasource)])
    (ib-push! ib conn isin ticker added-at))
  (ib-flush! ib conn))
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;You create a batcher, tell it what table to insert the data into and
what columns to insert, then push data into it and flush it at the
end. Pushing may trigger a flush when too many rows have accumulated,
according to an optional &lt;code&gt;#:batch-size&lt;/code&gt; argument.&lt;/p&gt;&lt;p&gt;In the past, when I built a batcher like this, I did it by accumulating
the row data into an array and, on flush, generating an &lt;code&gt;INSERT&lt;/code&gt;
statement with a row-wise set of placeholder parameters. Like this:&lt;/p&gt;&lt;pre&gt;&lt;code info="language-sql"&gt;INSERT INTO tickers(
  isin, ticker, added_at
) VALUES
  ($1, $2, $3),
  ($4, $5, $6),
  ...
  ($(n*3+1), $(n*3+2), $(n*3+3))
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This works fine for the most part, but it has a couple of problems.
First, the maximum number of parameters to an insert statement in
Postgres is 65536, so at most &lt;code&gt;65k/n-columns&lt;/code&gt; rows may be batched in
memory before a flush is required. Second, this has the obvious problem
that every flush requires sending a new, long query to the database,
so this approach can't easily leverage prepared statements. The latter
doesn't seem to have a huge impact, but it's still some unnecessary
inefficiency.&lt;/p&gt;&lt;p&gt;This time around, I decided to buffer the values in column-wise arrays.
On flush, those arrays are passed to the insert statement directly and
I use &lt;code&gt;UNNEST&lt;/code&gt; to turn them into a virtual table to insert from. That
looks like this:&lt;/p&gt;&lt;pre&gt;&lt;code info="language-sql"&gt;INSERT INTO tickers(
  isin, ticker, added_at
) SELECT * FROM UNNEST(
  $1::TEXT[],
  $2::TEXT[],
  $3::TIMESTAMPTZ[]
) AS t(isin, ticker, added_at)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;So, the end result is a much shorter query that can be prepared ahead of
time and reused between flushes. It also means we can buffer more rows
in memory before flushing. The only disadvantage is that the batcher
needs to know what the individual column types are ahead of time, hence
the last positional argument to &lt;code&gt;make-insert-batcher&lt;/code&gt;.&lt;/p&gt;&lt;/article&gt;</description></item><item><title>iOS Media Center Progress Jank</title><link>https://defn.io/2025/01/26/ios-media-center-progress-jank</link><guid>https://defn.io/2025/01/26/ios-media-center-progress-jank</guid><pubDate>Sun, 26 Jan 2025 15:00:00 +0200</pubDate><description>&lt;article&gt;&lt;p&gt;If you listen to podcasts on iOS, chances are you've noticed an issue
some apps have when displaying the playing episode's progress in the
media center. For example, notice how in the video below, playback
pauses at 0:39, then skips forward in real time to 0:45 when I hit
resume, before the app finally resets the elapsed time back to 0:38.&lt;/p&gt;&lt;p&gt;&lt;div style="float: right; padding: 0 0 1rem 1rem"&gt;&lt;video controls="" height="480px" muted="" src="/img/media-center-demo.mp4" width="221px"&gt;&lt;/video&gt;&lt;/div&gt;&lt;/p&gt;&lt;p&gt;Pictured is one of the most popular iOS apps&lt;sup&gt;&lt;a href="#fn_1" id="fnref_1_1"&gt;1&lt;/a&gt;&lt;/sup&gt; for playing podcasts,
but others I've tested have this issue as well. &lt;a href="https://podcatcher.net"&gt;Podcatcher&lt;/a&gt; used to
also have this problem until a couple months ago.&lt;/p&gt;&lt;p&gt;The reason this happens is because the iOS Now Playing view calculates
the playback position automatically according to the last &lt;a href="https://developer.apple.com/documentation/mediaplayer/mpnowplayinginfopropertyelapsedplaybacktime"&gt;elapsed
time&lt;/a&gt; and &lt;a href="https://developer.apple.com/documentation/mediaplayer/mpnowplayinginfopropertyplaybackrate"&gt;playback rate&lt;/a&gt; values it was given. That seems sensible as a
performance optimization, and it would normally be fine, but it appears
that the media center doesn't take into account the time that the item
spends being paused.&lt;/p&gt;&lt;p&gt;One workaround is to set the playback rate to &lt;code&gt;0&lt;/code&gt; before pausing and
to reset it and the elapsed time before resuming. That works fine for
most apps, but isn't quite right in Podcatcher's case since the playback
rate may be constantly varying when the Trim Silence feature is turned
on. Instead, I always set the playback rate to &lt;code&gt;0&lt;/code&gt; and manually update
the elapsed time as part of the audio engine tap that keeps track of
playback.&lt;/p&gt;&lt;pre&gt;&lt;code info="language-swift"&gt;internal func updateMediaCenterProgress() {
  let center = MPNowPlayingInfoCenter.default()
  var info = center.nowPlayingInfo ?? [String: Any]()
  info[MPMediaItemPropertyPlaybackDuration] = duration
  info[MPNowPlayingInfoPropertyElapsedPlaybackTime] = progress
  // The info center computes its own elapsed time based on the
  // playback rate. So, to avoid visual discrepancies between it and
  // our own progress tracking, always set the playback rate to 0.
  info[MPNowPlayingInfoPropertyPlaybackRate] = 0.0
  center.nowPlayingInfo = info
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Since the tap was already running in the background during playback,
this approach doesn't seem to have had any measurable impact on power
consumption&lt;sup&gt;&lt;a href="#fn_2" id="fnref_2_1"&gt;2&lt;/a&gt;&lt;/sup&gt; and the accuracy improvement is well worth it to me,
even though it feels somewhat gross to be working against the intent of
the media center API.&lt;/p&gt;&lt;p&gt;&lt;div style="clear: both"&gt;&lt;/div&gt;&lt;/p&gt;&lt;section class="footnotes"&gt;&lt;ol&gt;&lt;li id="fn_1"&gt;&lt;p&gt;Identity elided to protect the innocent. &lt;a class="footnote-backref" href="#fnref_1_1" title="Jump to reference" aria-label="Jump to reference"&gt;↩&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;&lt;li id="fn_2"&gt;&lt;p&gt;And Podcatcher is already much better in this area than other apps in the space. &lt;a class="footnote-backref" href="#fnref_2_1" title="Jump to reference" aria-label="Jump to reference"&gt;↩&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;&lt;/ol&gt;&lt;/section&gt;&lt;/article&gt;</description></item><item><title>How Podcatcher Does Transcriptions</title><link>https://defn.io/2025/01/04/podcast-transcriptions-in-podcatcher</link><guid>https://defn.io/2025/01/04/podcast-transcriptions-in-podcatcher</guid><pubDate>Sat, 4 Jan 2025 16:25:00 +0200</pubDate><description>&lt;article&gt;&lt;p&gt;About a month ago, I added transcription support to &lt;a href="https://apps.apple.com/us/app/podcatcher-podcast-player/id6736467324"&gt;Podcatcher&lt;/a&gt;. For
example, &lt;a href="https://podcatcher.net/podcasts/https%3A%2F%2Ffeeds.megaphone.fm%2Fvergecast/dd3b9ebc-bbe3-11ef-b3b0-9f7e98aa1a24?tab=transcript"&gt;here's a transcript&lt;/a&gt; of the latest
episode of the Vergecast, and &lt;a href="https://podcatcher.net/podcasts/https%3A%2F%2Fappstories.net%2Fepisodes%2Ffeed%2F/36d30050-c2bf-4037-a901-52d13658fa6f?tab=transcript"&gt;another one&lt;/a&gt; from AppStories.&lt;/p&gt;&lt;p&gt;Years ago, I bought an M1 Mac Mini and, in the intervening time, I
haven't done much with it besides keep it on my desk. So, I figured
I'd put it to work for this purpose. I initially reached for Apple's
&lt;a href="https://developer.apple.com/documentation/speech"&gt;Speech&lt;/a&gt; Framework, but &lt;a href="https://developer.apple.com/documentation/speech/sfspeechrecognizer"&gt;SFSpeechRecognizer&lt;/a&gt; turned out to be both
surprisingly slow – on that particular machine, it transcribes
at about a 1-to-1 ratio of audio time to wall clock time –, and
pretty inaccurate. I spent some time evaluating other alternatives
and eventually settled on OpenAI's open source &lt;a href="https://github.com/openai/whisper"&gt;Whisper&lt;/a&gt; model. In
particular, since I'm not yet at a point where it makes sense to spend
money on a machine with a powerful GPU to perform these transcriptions,
I'm currently using the &lt;code&gt;tiny.en&lt;/code&gt; variant of the model. Running two
concurrent transcriptions at a time, on the CPU, I can get the model to
transcribe about 30-60 minutes of podcast time for every 5 minutes of
wall clock time. The accuracy is acceptable, though far from perfect.
Eventually, I'll probably run one of the larger models to redo them if
the app gets traction.&lt;/p&gt;&lt;p&gt;The actual guts of the transcriber are fairly simple. At its core, it's
a small Racket script that shells out to Python to run the Whisper
model. It runs in a loop where it leases transcriptions-to-be-done from
the server, downloads the podcast enclosure, runs Whisper on it to
produce an &lt;a href="https://en.wikipedia.org/wiki/SubRip"&gt;SRT&lt;/a&gt; file and then uploads the resulting output back to the
server using the lease token it receives at the beginning. If the lease
expires before the transcriber has a chance to upload it, the server
just ignores the request.&lt;/p&gt;&lt;p&gt;The server prioritizes recent English-language podcasts and keeps track
of leases in a Postgres table. A cron job removes leases from the table
after 24 hours in case one of the transcriber processes gets stuck or
crashes in the middle of transcribing. It hasn't actually crashed yet,
but we did have a brief power outage recently, so I suppose that counts.&lt;/p&gt;&lt;p&gt;On the client side, the app and the website request the SRT data from
the server, parse it (SRT is a simple line-oriented text format) and
display it. The iOS app uses the timing information in the file to sync
transcripts to playback, which works in general, but can go off the
rails when a podcast uses dynamic ad insertion.&lt;/p&gt;&lt;/article&gt;</description></item><item><title>Platform-Specific Resources in SwiftPM</title><link>https://defn.io/2024/11/24/swiftpm-platform-specific-resources</link><guid>https://defn.io/2024/11/24/swiftpm-platform-specific-resources</guid><pubDate>Sun, 24 Nov 2024 09:00:00 +0200</pubDate><description>&lt;article&gt;&lt;p&gt;&lt;a href="https://github.com/Bogdanp/Noise"&gt;Noise&lt;/a&gt; packages Racket &amp;amp; Chez Scheme boot files&lt;sup&gt;&lt;a href="#fn_1" id="fnref_1_1"&gt;1&lt;/a&gt;&lt;/sup&gt; for all the
platforms it supports. Originally, that was just &lt;code&gt;x86-64&lt;/code&gt; and &lt;code&gt;arm64&lt;/code&gt;
macOS, but when I added iOS support, that extended to include &lt;code&gt;arm64&lt;/code&gt;
iOS. These boot files take up about 45MB for each &lt;code&gt;arch+os&lt;/code&gt; pair and
the way I originally distributed them was placing them all in a &lt;code&gt;boot&lt;/code&gt;
folder and adding them to the &lt;code&gt;resources&lt;/code&gt; list for the core &lt;code&gt;Noise&lt;/code&gt;
&lt;a href="https://github.com/Bogdanp/Noise/blob/0581556c6977948d85839c589a1079e53f2368f5/Package.swift#L31-L33"&gt;target&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;That worked fine, but it seemed like a waste to lug around an extra 90MB
of data that would never be used inside my iOS apps. Looking at the
&lt;a href="https://developer.apple.com/documentation/packagedescription"&gt;&lt;code&gt;PackageDescription&lt;/code&gt;&lt;/a&gt; docs, there doesn't appear to be a way to filter
resources by platform. There is an &lt;code&gt;exclude&lt;/code&gt; property on &lt;code&gt;Target&lt;/code&gt;s,
which I tried to use by making the &lt;code&gt;Package.swift&lt;/code&gt; script manipulate the
target at runtime, but I couldn't figure out a way to get that working
with cross-compilation.&lt;/p&gt;&lt;p&gt;Next, I tried writing a SwiftPM &lt;a href="https://github.com/swiftlang/swift-package-manager/blob/dca0cc27b9d5f08a9c9a38101e322d0f3ab1ba03/Documentation/Plugins.md#implementing-the-build-tool-plugin-script"&gt;build tool plugin&lt;/a&gt; to remove files
based on the target platform, but plugin execution is sandboxed and I
couldn't figure out a way to move the boot files out of the package
context&lt;sup&gt;&lt;a href="#fn_2" id="fnref_2_1"&gt;2&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;&lt;p&gt;Finally, I &lt;a href="https://github.com/Bogdanp/Noise/commit/4fb9fccc84a583b0bf8536063d63ad3aed84bb6c#diff-f913940c58e8744a2af1c68b909bb6383e49007e6c5a12fb03104a9006ae677eR25-R32"&gt;settled on&lt;/a&gt; just making separate targets for macOS
and iOS. Each target contains its respective boot files and the main
target conditionally depends on the platform-specific targets. I really
wanted to avoid this approach because it's somewhat ugly and it means I
have to manually wire things around that the build system should be able
to do for me, but, for now, this seems like the most sensible approach.&lt;/p&gt;&lt;section class="footnotes"&gt;&lt;ol&gt;&lt;li id="fn_1"&gt;&lt;p&gt;Object files that contain the Racket and Chez Scheme runtime. &lt;a class="footnote-backref" href="#fnref_1_1" title="Jump to reference" aria-label="Jump to reference"&gt;↩&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;&lt;li id="fn_2"&gt;&lt;p&gt;As I'm writing this, I wonder if I could've combined the &lt;code&gt;exclude&lt;/code&gt;
property and a build tool plugin to define a folder into which I
could've moved the unneeded boot files during the build. &lt;a class="footnote-backref" href="#fnref_2_1" title="Jump to reference" aria-label="Jump to reference"&gt;↩&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;&lt;/ol&gt;&lt;/section&gt;&lt;/article&gt;</description></item></channel></rss>