this repo has no description

add docs

+748 -3
+10 -1
Package.resolved
··· 1 1 { 2 - "originHash" : "29d06dd4d3fcf924c37d6591ec44fb6bc18d5097d9235a86a34df3d343359b60", 2 + "originHash" : "c9e9e2667901cc2af4981bac3286deec3f228fc94d6e54128de7bb4da4d8d9af", 3 3 "pins" : [ 4 + { 5 + "identity" : "coreatprotocol", 6 + "kind" : "remoteSourceControl", 7 + "location" : "https://tangled.org/@sparrowtek.com/CoreATProtocol", 8 + "state" : { 9 + "branch" : "main", 10 + "revision" : "d50e4c6a1c92a5e777fe5c642173be03b14116e5" 11 + } 12 + }, 4 13 { 5 14 "identity" : "jwt-kit", 6 15 "kind" : "remoteSourceControl",
+2 -2
Package.swift
··· 18 18 ), 19 19 ], 20 20 dependencies: [ 21 - .package(path: "../../CoreATProtocol"), 22 - // .package(url: "https://tangled.org/@sparrowtek.com/CoreATProtocol", branch: "main"), 21 + // .package(path: "../../CoreATProtocol"), 22 + .package(url: "https://tangled.org/@sparrowtek.com/CoreATProtocol", branch: "main"), 23 23 ], 24 24 targets: [ 25 25 .target(
+131
README.md
··· 1 + # EffemKit 2 + 3 + Swift client for the Effem AT Protocol AppView. Provides social podcasting features — subscriptions, comments, recommendations, bookmarks, curated lists, and profiles — built on the AT Protocol. 4 + 5 + ## Requirements 6 + 7 + - Swift 6.2+ 8 + - iOS 26+ / macOS 26+ / watchOS 26+ / tvOS 26+ 9 + 10 + ## Dependencies 11 + 12 + - [CoreATProtocol](https://tangled.org/@sparrowtek.com/CoreATProtocol) — AT Protocol primitives, OAuth, networking 13 + 14 + ## Installation 15 + 16 + Add EffemKit as a dependency in your `Package.swift`: 17 + 18 + ```swift 19 + dependencies: [ 20 + .package(url: "https://tangled.org/@sparrowtek.com/EffemKit", branch: "main"), 21 + ] 22 + ``` 23 + 24 + Or add it as a local package in Xcode. 25 + 26 + ## Quick Start 27 + 28 + ### 1. Configure the AppView 29 + 30 + ```swift 31 + import EffemKit 32 + import CoreATProtocol 33 + 34 + // At app launch 35 + Task { @APActor in 36 + setup(appViewHost: "https://appview.effem.xyz") 37 + } 38 + ``` 39 + 40 + ### 2. Read Data 41 + 42 + ```swift 43 + @APActor 44 + func loadTrending() async throws -> [PodcastResult] { 45 + let service = EffemService() 46 + let response = try await service.getTrending(max: 20) 47 + return response.feeds 48 + } 49 + ``` 50 + 51 + ### 3. Write Data (requires authentication) 52 + 53 + ```swift 54 + @APActor 55 + func subscribe(feedId: Int, userDID: String) async throws { 56 + let repoService = EffemRepoService() 57 + let podcast = PodcastRef(feedId: feedId) 58 + _ = try await repoService.subscribe(to: podcast, repo: userDID) 59 + } 60 + ``` 61 + 62 + ## Architecture 63 + 64 + EffemKit uses a two-router architecture: 65 + 66 + | Path | Service | Target | Auth Required | 67 + |------|---------|--------|--------------| 68 + | Read | `EffemService` | Effem AppView | No | 69 + | Write | `EffemRepoService` | User's PDS | Yes | 70 + 71 + **Reads** go to the Effem AppView, which indexes social records from the AT Protocol firehose and proxies Podcast Index metadata enriched with social overlay data. 72 + 73 + **Writes** go to the authenticated user's PDS using standard `com.atproto.repo.createRecord` / `deleteRecord` calls. The AppView picks up new records asynchronously via the firehose. 74 + 75 + All public API is isolated to `@APActor` for thread safety. 76 + 77 + ## API Overview 78 + 79 + ### EffemService (Read) 80 + 81 + | Category | Methods | 82 + |----------|---------| 83 + | Subscriptions | `getSubscriptions`, `getSubscribers` | 84 + | Comments | `getComments`, `getCommentThread` | 85 + | Recommendations | `getRecommendations`, `getPopular` | 86 + | Lists | `getList`, `getLists` | 87 + | Bookmarks | `getBookmarks` | 88 + | Inbox | `getInbox` | 89 + | Profiles | `getProfile` | 90 + | Podcast Search | `searchPodcasts`, `searchEpisodes` | 91 + | Podcast Metadata | `getPodcast`, `getEpisodes`, `getEpisode`, `getTrending`, `getCategories` | 92 + 93 + ### EffemRepoService (Write) 94 + 95 + | Category | Methods | 96 + |----------|---------| 97 + | Subscriptions | `subscribe`, `unsubscribe` | 98 + | Comments | `postComment`, `deleteComment` | 99 + | Recommendations | `recommend`, `unrecommend` | 100 + | Bookmarks | `bookmark`, `removeBookmark` | 101 + | Lists | `createList`, `deleteList` | 102 + | Profile | `updateProfile` | 103 + 104 + ## Lexicon Namespace 105 + 106 + All Effem records use the `xyz.effem.*` namespace: 107 + 108 + - `xyz.effem.feed.subscription` 109 + - `xyz.effem.feed.comment` 110 + - `xyz.effem.feed.recommendation` 111 + - `xyz.effem.feed.bookmark` 112 + - `xyz.effem.feed.list` 113 + - `xyz.effem.actor.profile` 114 + 115 + ## Documentation 116 + 117 + Build the DocC documentation: 118 + 119 + ```bash 120 + swift package generate-documentation 121 + ``` 122 + 123 + Or in Xcode: Product > Build Documentation. 124 + 125 + ## Testing 126 + 127 + ```bash 128 + swift test 129 + ``` 130 + 131 + 15 tests across 8 suites covering model decoding, round-tripping, serialization, and AT URI generation.
+81
Sources/EffemKit/EffemKit.docc/EffemKit.md
··· 1 + # ``EffemKit`` 2 + 3 + Swift client for the Effem AT Protocol AppView — social podcasting on the AT Protocol. 4 + 5 + ## Overview 6 + 7 + EffemKit provides an iOS client for interacting with the Effem AppView, a custom AT Protocol service that adds social features to podcast listening. The package handles two distinct communication paths: 8 + 9 + - **Read path** — ``EffemService`` queries the Effem AppView for social data (comments, subscriptions, recommendations) and Podcast Index metadata enriched with social overlays. 10 + - **Write path** — ``EffemRepoService`` writes records to the user's AT Protocol PDS (Personal Data Server), which the AppView then indexes asynchronously via the AT Protocol firehose. 11 + 12 + All public API is isolated to the `@APActor` global actor from CoreATProtocol, providing thread-safe access without manual synchronization. 13 + 14 + ## Topics 15 + 16 + ### Essentials 17 + 18 + - <doc:GettingStarted> 19 + - ``setup(appViewHost:)`` 20 + - ``EffemEnvironment`` 21 + 22 + ### Reading Data 23 + 24 + - <doc:ReadingData> 25 + - ``EffemService`` 26 + 27 + ### Writing Data 28 + 29 + - <doc:WritingData> 30 + - ``EffemRepoService`` 31 + - ``CommentReplyRef`` 32 + 33 + ### Models — AT Protocol Records 34 + 35 + - ``Subscription`` 36 + - ``Comment`` 37 + - ``CommentAuthor`` 38 + - ``Recommendation`` 39 + - ``Bookmark`` 40 + - ``PodcastList`` 41 + - ``EffemProfile`` 42 + 43 + ### Models — Podcast Index 44 + 45 + - ``PodcastRef`` 46 + - ``EpisodeRef`` 47 + - ``PodcastResult`` 48 + - ``EpisodeResult`` 49 + - ``PodcastCategory`` 50 + - ``SocialOverlay`` 51 + - ``InboxItem`` 52 + 53 + ### Models — Responses 54 + 55 + - ``SubscriptionsResponse`` 56 + - ``SubscribersResponse`` 57 + - ``SubscriberInfo`` 58 + - ``CommentsResponse`` 59 + - ``CommentThreadResponse`` 60 + - ``CommentThreadNode`` 61 + - ``RecommendationsResponse`` 62 + - ``PopularResponse`` 63 + - ``PopularEpisode`` 64 + - ``ListsResponse`` 65 + - ``ListResponse`` 66 + - ``BookmarksResponse`` 67 + - ``ProfileResponse`` 68 + - ``PodcastSearchResponse`` 69 + - ``PodcastDetailResponse`` 70 + - ``EpisodeSearchResponse`` 71 + - ``EpisodesResponse`` 72 + - ``EpisodeDetailResponse`` 73 + - ``TrendingResponse`` 74 + - ``CategoriesResponse`` 75 + - ``InboxResponse`` 76 + - ``CreateRecordResponse`` 77 + 78 + ### Errors 79 + 80 + - ``EffemKitConfigurationError`` 81 + - ``EffemRepoError``
+134
Sources/EffemKit/EffemKit.docc/GettingStarted.md
··· 1 + # Getting Started with EffemKit 2 + 3 + Configure EffemKit and make your first API calls. 4 + 5 + ## Overview 6 + 7 + EffemKit requires two pieces of configuration before use: 8 + 9 + 1. **AppView host** — The URL of your Effem AppView server (for reading social and podcast data). 10 + 2. **PDS authentication** — An authenticated CoreATProtocol session (for writing records to the user's repository). 11 + 12 + Read-only features (browsing podcasts, viewing comments) only need the AppView host. Write features (subscribing, commenting, recommending) additionally require PDS authentication. 13 + 14 + ## Configure the AppView 15 + 16 + Call ``setup(appViewHost:)`` once at app launch, typically in your `App` initializer or an early scene phase handler: 17 + 18 + ```swift 19 + import EffemKit 20 + import CoreATProtocol 21 + 22 + @main 23 + struct EffemApp: App { 24 + init() { 25 + Task { @APActor in 26 + setup(appViewHost: "https://appview.effem.xyz") 27 + } 28 + } 29 + 30 + var body: some Scene { 31 + WindowGroup { 32 + ContentView() 33 + } 34 + } 35 + } 36 + ``` 37 + 38 + > Important: All EffemKit API is isolated to `@APActor`. Call EffemKit methods from within an `@APActor`-isolated context or use `Task { @APActor in ... }`. 39 + 40 + ## Authenticate the User 41 + 42 + EffemKit relies on CoreATProtocol for OAuth authentication. Once the user signs in through CoreATProtocol, the PDS host is automatically available and ``EffemRepoService`` can write records: 43 + 44 + ```swift 45 + // After CoreATProtocol OAuth flow completes, 46 + // APEnvironment.current.host is set to the user's PDS. 47 + // No additional EffemKit configuration is needed for writes. 48 + ``` 49 + 50 + ## Read Data with EffemService 51 + 52 + ``EffemService`` provides all read-only queries. Create an instance and call any method: 53 + 54 + ```swift 55 + @APActor 56 + func loadTrending() async throws -> [PodcastResult] { 57 + let service = EffemService() 58 + let response = try await service.getTrending(max: 20) 59 + return response.feeds 60 + } 61 + ``` 62 + 63 + ```swift 64 + @APActor 65 + func loadComments(feedId: Int, episodeId: Int) async throws -> [Comment] { 66 + let service = EffemService() 67 + let response = try await service.getComments( 68 + feedId: feedId, 69 + episodeId: episodeId 70 + ) 71 + return response.comments 72 + } 73 + ``` 74 + 75 + ## Write Data with EffemRepoService 76 + 77 + ``EffemRepoService`` writes records to the user's PDS. The `repo` parameter is the user's DID: 78 + 79 + ```swift 80 + @APActor 81 + func subscribeToPodcast(feedId: Int, userDID: String) async throws { 82 + let repoService = EffemRepoService() 83 + let podcast = PodcastRef(feedId: feedId) 84 + let response = try await repoService.subscribe(to: podcast, repo: userDID) 85 + // response.uri contains the AT URI of the created record 86 + // response.cid contains the content hash 87 + } 88 + ``` 89 + 90 + ## Handle Pagination 91 + 92 + Most list endpoints support cursor-based pagination: 93 + 94 + ```swift 95 + @APActor 96 + func loadAllSubscriptions(did: String) async throws -> [Subscription] { 97 + let service = EffemService() 98 + var allSubscriptions: [Subscription] = [] 99 + var cursor: String? 100 + 101 + repeat { 102 + let response = try await service.getSubscriptions( 103 + did: did, 104 + cursor: cursor, 105 + limit: 50 106 + ) 107 + allSubscriptions.append(contentsOf: response.subscriptions) 108 + cursor = response.cursor 109 + } while cursor != nil 110 + 111 + return allSubscriptions 112 + } 113 + ``` 114 + 115 + ## Error Handling 116 + 117 + EffemKit throws ``EffemKitConfigurationError`` when the AppView or PDS isn't configured: 118 + 119 + ```swift 120 + @APActor 121 + func safeFetch() async { 122 + let service = EffemService() 123 + do { 124 + let trending = try await service.getTrending() 125 + // use trending.feeds 126 + } catch let error as EffemKitConfigurationError { 127 + // Handle missing configuration 128 + print(error.localizedDescription) 129 + } catch { 130 + // Handle network or decoding errors 131 + print(error.localizedDescription) 132 + } 133 + } 134 + ```
+182
Sources/EffemKit/EffemKit.docc/ReadingData.md
··· 1 + # Reading Data from the AppView 2 + 3 + Query social data, podcast metadata, and search results through the Effem AppView. 4 + 5 + ## Overview 6 + 7 + ``EffemService`` is the single entry point for all read operations. Every method maps to an XRPC endpoint on the Effem AppView, returning strongly-typed response models. 8 + 9 + The AppView serves two categories of data: 10 + 11 + - **Social data** — Records written by users (subscriptions, comments, recommendations, bookmarks, lists) that the AppView indexed from the AT Protocol firehose. 12 + - **Podcast metadata** — Podcast Index data proxied and cached by the AppView, enriched with ``SocialOverlay`` counts. 13 + 14 + ## Discover Podcasts 15 + 16 + Search for podcasts and episodes, browse trending content, or look up specific metadata: 17 + 18 + ```swift 19 + @APActor 20 + func discoverPodcasts() async throws { 21 + let service = EffemService() 22 + 23 + // Search 24 + let searchResults = try await service.searchPodcasts(query: "swift") 25 + for podcast in searchResults.feeds { 26 + print("\(podcast.title ?? "Untitled") — \(podcast.social?.subscriberCount ?? 0) subscribers") 27 + } 28 + 29 + // Trending 30 + let trending = try await service.getTrending(max: 10, lang: "en") 31 + 32 + // By category 33 + let categories = try await service.getCategories() 34 + 35 + // Podcast detail with social overlay 36 + let detail = try await service.getPodcast(feedId: 75075) 37 + print("Subscribers: \(detail.feed.social?.subscriberCount ?? 0)") 38 + } 39 + ``` 40 + 41 + ## Browse Episodes 42 + 43 + ```swift 44 + @APActor 45 + func browseEpisodes(feedId: Int) async throws { 46 + let service = EffemService() 47 + 48 + // List episodes for a podcast 49 + let episodes = try await service.getEpisodes(feedId: feedId, max: 20) 50 + 51 + // Single episode detail 52 + if let first = episodes.items.first { 53 + let detail = try await service.getEpisode(episodeId: first.episodeId) 54 + } 55 + 56 + // Search episodes 57 + let results = try await service.searchEpisodes(query: "WWDC") 58 + } 59 + ``` 60 + 61 + ## Social Features 62 + 63 + ### Subscriptions 64 + 65 + ```swift 66 + @APActor 67 + func viewSubscriptions(userDID: String) async throws { 68 + let service = EffemService() 69 + 70 + // A user's subscriptions 71 + let subs = try await service.getSubscriptions(did: userDID) 72 + for sub in subs.subscriptions { 73 + print("Subscribed to feed \(sub.podcast.feedId) on \(sub.createdAt)") 74 + } 75 + 76 + // Who subscribes to a specific podcast 77 + let subscribers = try await service.getSubscribers(feedId: 75075) 78 + for subscriber in subscribers.subscribers { 79 + print("\(subscriber.displayName ?? subscriber.did)") 80 + } 81 + } 82 + ``` 83 + 84 + ### Comments 85 + 86 + ```swift 87 + @APActor 88 + func viewComments(feedId: Int, episodeId: Int) async throws { 89 + let service = EffemService() 90 + 91 + // Flat list of comments for an episode 92 + let comments = try await service.getComments( 93 + feedId: feedId, 94 + episodeId: episodeId 95 + ) 96 + 97 + // Threaded view of a specific comment 98 + if let first = comments.comments.first { 99 + let thread = try await service.getCommentThread(uri: first.uri) 100 + // thread.thread.comment is the root 101 + // thread.thread.replies contains nested replies 102 + } 103 + } 104 + ``` 105 + 106 + ### Recommendations and Popular Episodes 107 + 108 + ```swift 109 + @APActor 110 + func viewRecommendations(feedId: Int, episodeId: Int) async throws { 111 + let service = EffemService() 112 + 113 + // Recommendations for a specific episode 114 + let recs = try await service.getRecommendations( 115 + feedId: feedId, 116 + episodeId: episodeId 117 + ) 118 + 119 + // Most recommended episodes this week 120 + let popular = try await service.getPopular(period: "week", limit: 10) 121 + for ep in popular.episodes { 122 + print("\(ep.title ?? "Untitled") — \(ep.recommendationCount) recs") 123 + } 124 + } 125 + ``` 126 + 127 + ### Bookmarks, Lists, and Inbox 128 + 129 + ```swift 130 + @APActor 131 + func viewUserContent(userDID: String) async throws { 132 + let service = EffemService() 133 + 134 + // Bookmarks (optionally filtered by podcast or episode) 135 + let bookmarks = try await service.getBookmarks(did: userDID) 136 + 137 + // Curated podcast lists 138 + let lists = try await service.getLists(did: userDID) 139 + if let first = lists.lists.first { 140 + let detail = try await service.getList(uri: first.uri) 141 + } 142 + 143 + // New episodes from subscribed podcasts 144 + let inbox = try await service.getInbox(did: userDID) 145 + for item in inbox.items { 146 + print("New: \(item.episode.title ?? "Untitled") from \(item.podcastTitle ?? "Unknown")") 147 + } 148 + } 149 + ``` 150 + 151 + ### Profiles 152 + 153 + ```swift 154 + @APActor 155 + func viewProfile(userDID: String) async throws { 156 + let service = EffemService() 157 + let profile = try await service.getProfile(did: userDID) 158 + print(""" 159 + \(profile.profile.displayName ?? profile.profile.did) 160 + Subscriptions: \(profile.profile.subscriptionCount ?? 0) 161 + Comments: \(profile.profile.commentCount ?? 0) 162 + Genres: \(profile.profile.favoriteGenres?.joined(separator: ", ") ?? "none") 163 + """) 164 + } 165 + ``` 166 + 167 + ## Social Overlay 168 + 169 + Many podcast and episode responses include a ``SocialOverlay`` with engagement counts. This data is computed by the AppView from indexed AT Protocol records: 170 + 171 + ```swift 172 + if let social = podcastResult.social { 173 + // How many Effem users subscribe to this podcast 174 + let subscribers = social.subscriberCount ?? 0 175 + 176 + // How many comments exist across all episodes 177 + let comments = social.commentCount ?? 0 178 + 179 + // Which of the current user's follows also subscribe 180 + let followingWhoSubscribe = social.subscribedByFollowing ?? [] 181 + } 182 + ```
+208
Sources/EffemKit/EffemKit.docc/WritingData.md
··· 1 + # Writing Data to the AT Protocol 2 + 3 + Create and delete records in the user's AT Protocol repository. 4 + 5 + ## Overview 6 + 7 + ``EffemRepoService`` writes Effem records to the authenticated user's PDS (Personal Data Server) using standard AT Protocol repository operations (`com.atproto.repo.createRecord` and `com.atproto.repo.deleteRecord`). 8 + 9 + Records are written to the user's PDS, not the AppView. The AppView discovers new records asynchronously by subscribing to the AT Protocol firehose. There may be a brief delay (typically under a second) between writing a record and it appearing in AppView query results. 10 + 11 + > Important: The user must be authenticated via CoreATProtocol before calling any ``EffemRepoService`` method. If the PDS host isn't configured, methods throw ``EffemKitConfigurationError/pdsHostNotConfigured``. 12 + 13 + ## Subscribe to a Podcast 14 + 15 + ```swift 16 + @APActor 17 + func subscribe(feedId: Int, feedUrl: String?, userDID: String) async throws { 18 + let repoService = EffemRepoService() 19 + let podcast = PodcastRef(feedId: feedId, feedUrl: feedUrl) 20 + 21 + let response = try await repoService.subscribe(to: podcast, repo: userDID) 22 + // response.uri = "at://did:plc:xxx/xyz.effem.feed.subscription/3jm2..." 23 + // response.cid = content hash of the record 24 + 25 + // Extract the rkey from the URI for later deletion 26 + let rkey = response.uri.split(separator: "/").last.map(String.init) ?? "" 27 + } 28 + ``` 29 + 30 + To unsubscribe, pass the record key from the original subscription: 31 + 32 + ```swift 33 + @APActor 34 + func unsubscribe(rkey: String, userDID: String) async throws { 35 + let repoService = EffemRepoService() 36 + try await repoService.unsubscribe(rkey: rkey, repo: userDID) 37 + } 38 + ``` 39 + 40 + ## Post a Comment 41 + 42 + Comments are attached to a specific episode. You can optionally include a playback timestamp and a reply reference for threading: 43 + 44 + ```swift 45 + @APActor 46 + func postComment( 47 + feedId: Int, 48 + episodeId: Int, 49 + text: String, 50 + timestamp: Int?, 51 + userDID: String 52 + ) async throws { 53 + let repoService = EffemRepoService() 54 + let episode = EpisodeRef(feedId: feedId, episodeId: episodeId) 55 + 56 + let response = try await repoService.postComment( 57 + episode: episode, 58 + text: text, 59 + timestamp: timestamp, 60 + repo: userDID 61 + ) 62 + } 63 + ``` 64 + 65 + ### Reply to a Comment 66 + 67 + To reply to an existing comment, construct a ``CommentReplyRef`` with the root and parent comment URIs and CIDs: 68 + 69 + ```swift 70 + @APActor 71 + func replyToComment( 72 + episode: EpisodeRef, 73 + text: String, 74 + parentComment: Comment, 75 + rootComment: Comment, 76 + rootCID: String, 77 + parentCID: String, 78 + userDID: String 79 + ) async throws { 80 + let repoService = EffemRepoService() 81 + 82 + let reply = CommentReplyRef( 83 + rootURI: rootComment.uri, 84 + rootCID: rootCID, 85 + parentURI: parentComment.uri, 86 + parentCID: parentCID 87 + ) 88 + 89 + let response = try await repoService.postComment( 90 + episode: episode, 91 + text: text, 92 + reply: reply, 93 + repo: userDID 94 + ) 95 + } 96 + ``` 97 + 98 + ## Recommend an Episode 99 + 100 + ```swift 101 + @APActor 102 + func recommend(feedId: Int, episodeId: Int, text: String?, userDID: String) async throws { 103 + let repoService = EffemRepoService() 104 + let episode = EpisodeRef(feedId: feedId, episodeId: episodeId) 105 + 106 + let response = try await repoService.recommend( 107 + episode: episode, 108 + text: text, 109 + repo: userDID 110 + ) 111 + } 112 + ``` 113 + 114 + ## Bookmark an Episode 115 + 116 + Bookmarks can include a playback timestamp so the user can return to a specific moment: 117 + 118 + ```swift 119 + @APActor 120 + func bookmarkCurrentPosition( 121 + feedId: Int, 122 + episodeId: Int, 123 + playbackSeconds: Int, 124 + userDID: String 125 + ) async throws { 126 + let repoService = EffemRepoService() 127 + let episode = EpisodeRef(feedId: feedId, episodeId: episodeId) 128 + 129 + let response = try await repoService.bookmark( 130 + episode: episode, 131 + timestamp: playbackSeconds, 132 + repo: userDID 133 + ) 134 + } 135 + ``` 136 + 137 + ## Create a Podcast List 138 + 139 + Curated lists group multiple podcasts under a name and optional description: 140 + 141 + ```swift 142 + @APActor 143 + func createList(userDID: String) async throws { 144 + let repoService = EffemRepoService() 145 + 146 + let podcasts = [ 147 + PodcastRef(feedId: 75075, feedUrl: "https://example.com/feed.xml"), 148 + PodcastRef(feedId: 920666), 149 + ] 150 + 151 + let response = try await repoService.createList( 152 + name: "My Favorite Tech Pods", 153 + description: "The best podcasts about Swift and iOS development", 154 + podcasts: podcasts, 155 + repo: userDID 156 + ) 157 + } 158 + ``` 159 + 160 + ## Update User Profile 161 + 162 + The Effem profile is stored as a single record with the fixed key `"self"`. Calling ``EffemRepoService/updateProfile(displayName:description:favoriteGenres:repo:)`` creates or overwrites it: 163 + 164 + ```swift 165 + @APActor 166 + func updateProfile(userDID: String) async throws { 167 + let repoService = EffemRepoService() 168 + 169 + let response = try await repoService.updateProfile( 170 + displayName: "Alice", 171 + description: "Podcast enthusiast and Swift developer", 172 + favoriteGenres: ["Technology", "Science", "Comedy"], 173 + repo: userDID 174 + ) 175 + } 176 + ``` 177 + 178 + ## Delete Any Record 179 + 180 + Every write method that creates a record has a corresponding delete method. All delete methods take an `rkey` (record key) and the user's DID: 181 + 182 + ```swift 183 + @APActor 184 + func deleteOperations(userDID: String) async throws { 185 + let repoService = EffemRepoService() 186 + 187 + try await repoService.unsubscribe(rkey: "3jm2abc", repo: userDID) 188 + try await repoService.deleteComment(rkey: "3jm2def", repo: userDID) 189 + try await repoService.unrecommend(rkey: "3jm2ghi", repo: userDID) 190 + try await repoService.removeBookmark(rkey: "3jm2jkl", repo: userDID) 191 + try await repoService.deleteList(rkey: "3jm2mno", repo: userDID) 192 + } 193 + ``` 194 + 195 + ## Understanding Record Keys 196 + 197 + When you create a record, the PDS assigns a TID (timestamp-based identifier) as the record key. The ``CreateRecordResponse`` contains the full AT URI: 198 + 199 + ``` 200 + at://did:plc:abc123/xyz.effem.feed.subscription/3jm2szx5c47mo 201 + │ │ │ │ 202 + │ │ │ └─ rkey (record key) 203 + │ │ └─ collection (lexicon ID) 204 + │ └─ repo (user's DID) 205 + └─ AT URI scheme 206 + ``` 207 + 208 + Extract the `rkey` from the URI's last path component to use with delete methods. Store record keys locally (e.g., in SwiftData) so you can delete records later without querying the AppView.