Commit 67e5e954b4

67e5e954b48c3203eac43ca31e975332b6439a49

parent: 00360ca116

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-20 21:20 UTC

docs: preflight corrections to the push plan

UserDefaults has no Int64 overload, so the stored device id round
trips through a conditional NSNumber bridge; store it as Int. And a
test closure stored on the session captured it strongly.

Ref krz/gitbay#89

Layout: unified · split

docs/superpowers/plans/2026-09-20-push-notifications.md +14 −4
@@ -572,8 +572,11 @@ private func twoAccounts() async throws -> (SessionStore, StubProtocol.Box) {
572 let (session, _) = try await twoAccounts() 572 let (session, _) = try await twoAccounts()
573 let victim = try #require(session.accounts.first) 573 let victim = try #require(session.accounts.first)
574 var sawTokenDuringHook: Bool? 574 var sawTokenDuringHook: Bool?
575 session.willRemoveAccount = { account in 575 // [weak session] because this closure is stored ON session; the
576 sawTokenDuringHook = session.client(for: account) != nil 576 // production wiring in gitbayApp captures the registrar weakly for
577 // the same reason.
578 session.willRemoveAccount = { [weak session] account in
579 sawTokenDuringHook = session?.client(for: account) != nil
577 } 580 }
578 await session.remove(victim) 581 await session.remove(victim)
579 #expect(sawTokenDuringHook == true) 582 #expect(sawTokenDuringHook == true)
@@ -753,7 +756,12 @@ final class PushRegistrar {
753 ["notifications", "device", "add", "--label", Self.deviceLabel], 756 ["notifications", "device", "add", "--label", Self.deviceLabel],
754 stdin: token, as: Registered.self) 757 stdin: token, as: Registered.self)
755 if let id = out?.id { 758 if let id = out?.id {
756 defaults.set(id, forKey: Self.idKey(account)) 759 // Stored as Int, which UserDefaults handles natively —
760 // it has no Int64 overload, so an Int64 goes in as a
761 // boxed NSNumber and comes back out through a
762 // conditional bridge. Int is 64-bit on every device
763 // this ships to.
764 defaults.set(Int(id), forKey: Self.idKey(account))
757 } 765 }
758 } catch { 766 } catch {
759 // A side channel. The account still works and the next 767 // A side channel. The account still works and the next
@@ -767,7 +775,9 @@ final class PushRegistrar {
767 /// wired, and that hook is awaited before the token is discarded. 775 /// wired, and that hook is awaited before the token is discarded.
768 func deregister(_ account: Account) async { 776 func deregister(_ account: Account) async {
769 let key = Self.idKey(account) 777 let key = Self.idKey(account)
770 guard let id = defaults.object(forKey: key) as? Int64, 778 // object(forKey:) rather than integer(forKey:), which cannot
779 // tell a stored 0 from an absent key.
780 guard let id = defaults.object(forKey: key) as? Int,
771 let client = session.client(for: account) else { return } 781 let client = session.client(for: account) else { return }
772 try? await client.run(["notifications", "device", "remove", String(id)]) 782 try? await client.run(["notifications", "device", "remove", String(id)])
773 defaults.removeObject(forKey: key) 783 defaults.removeObject(forKey: key)