feat(records)!: record and custom-XRPC ergonomics, Bluesky helpers on client.Bsky #122

Closed
opened 2026-09-24 12:39:05 +00:00 by Grandiras · 0 comments
Owner

Part of the v0.7.0 update cycle (wave 4). Custom records, custom XRPC and the facade.

Records

  • interface IAtProtoRecord { static abstract Nsid Collection { get; } } so client.GetCollection<T>() no longer restates the NSID. GetCollection<T>(Nsid) remains for dynamic cases and throws if T declares a different collection.
  • Deserializing a record without createdAt no longer fabricates "now", which a later PutAsync would persist. CreateAsync fills it only when unset.
  • One RecordRef(AtUri Uri, Cid Cid, CommitMeta? Commit) and one RecordView<T>(AtUri Uri, Cid? Cid, T Value), returned by every write and every typed read. This replaces the three "record reference" shapes and two "typed record" shapes and removes the name clash with Embed.RecordView.
  • RecordCollection<T> deserializes each record once, not JsonElement then T. Add FindAsync (returns null on RecordNotFound); ExistsAsync becomes FindAsync(...) is not null.

Custom XRPC and extensibility

  • Add QueryAsync<TOut>(Nsid, XrpcParams?, XrpcCallOptions?, ct), ProcedureAsync<TIn, TOut>(…), ProcedureAsync<TIn>(…) and ProcedureAsync(Nsid, …). Mark the anonymous-object-parameters overload [RequiresUnreferencedCode], or drop it.
  • Expose a transport seam (for example IXrpcTransport Transport) so third-party lexicon packages can build sub-clients that share the client's transport and session.
  • EnsureAuthenticated throws XrpcAuthenticationException with a neutral message, since it also covers OAuth users.

Facade

  • Move the Bluesky helpers to client.Bsky with typed arguments:

    • PostAsync(RichText, PostOptions?), where RichText is a record with implicit conversion from string;
    • LikeAsync(StrongRef) / RepostAsync(StrongRef) / FollowAsync(Did);
    • one DeleteRecordAsync(AtUri) in place of Unlike/UndoRepost/Unfollow/DeletePost, validating that the collection and rkey are present;
    • UpdateProfileAsync, as reworked in #115.

    RichTextBuilder.Build() returns RichText.

  • Take firehose construction off AtProtoClient: remove CreateFirehoseClient / CreateFirehoseConsumer / RelayUrl. Streaming clients are built independently of a user-session client.

  • Remove the forwarders nothing uses, now covered by XrpcCallOptions. Consolidate AtProtoClientBuilder against the options and constructors, keeping one obvious construction path.

  • Merge duplicate definitions:

    • SessionResponse vs Session;
    • CreatePdsAccountRequest ⊂ CreateAccountRequest;
    • BlobLink/CidLink;
    • TeamMemberProfile → ProfileViewDetailed;
    • the three report-subject copies → one union;
    • the non-generic GetRecordResponse.

CHANGELOG Breaking changes with a migration note per moved or renamed member.

Part of the v0.7.0 update cycle (wave 4). Custom records, custom XRPC and the facade. ## Records - [ ] `interface IAtProtoRecord { static abstract Nsid Collection { get; } }` so `client.GetCollection<T>()` no longer restates the NSID. `GetCollection<T>(Nsid)` remains for dynamic cases and throws if `T` declares a different collection. - [ ] Deserializing a record without `createdAt` no longer fabricates "now", which a later `PutAsync` would persist. `CreateAsync` fills it only when unset. - [ ] One `RecordRef(AtUri Uri, Cid Cid, CommitMeta? Commit)` and one `RecordView<T>(AtUri Uri, Cid? Cid, T Value)`, returned by every write and every typed read. This replaces the three "record reference" shapes and two "typed record" shapes and removes the name clash with `Embed.RecordView`. - [ ] `RecordCollection<T>` deserializes each record once, not `JsonElement` then `T`. Add `FindAsync` (returns `null` on `RecordNotFound`); `ExistsAsync` becomes `FindAsync(...) is not null`. ## Custom XRPC and extensibility - [ ] Add `QueryAsync<TOut>(Nsid, XrpcParams?, XrpcCallOptions?, ct)`, `ProcedureAsync<TIn, TOut>(…)`, `ProcedureAsync<TIn>(…)` and `ProcedureAsync(Nsid, …)`. Mark the anonymous-object-parameters overload `[RequiresUnreferencedCode]`, or drop it. - [ ] Expose a transport seam (for example `IXrpcTransport Transport`) so third-party lexicon packages can build sub-clients that share the client's transport and session. - [ ] `EnsureAuthenticated` throws `XrpcAuthenticationException` with a neutral message, since it also covers OAuth users. ## Facade - [ ] Move the Bluesky helpers to `client.Bsky` with typed arguments: - `PostAsync(RichText, PostOptions?)`, where `RichText` is a record with implicit conversion from `string`; - `LikeAsync(StrongRef)` / `RepostAsync(StrongRef)` / `FollowAsync(Did)`; - one `DeleteRecordAsync(AtUri)` in place of Unlike/UndoRepost/Unfollow/DeletePost, validating that the collection and rkey are present; - `UpdateProfileAsync`, as reworked in #115. `RichTextBuilder.Build()` returns `RichText`. - [ ] Take firehose construction off `AtProtoClient`: remove `CreateFirehoseClient` / `CreateFirehoseConsumer` / `RelayUrl`. Streaming clients are built independently of a user-session client. - [ ] Remove the forwarders nothing uses, now covered by `XrpcCallOptions`. Consolidate `AtProtoClientBuilder` against the options and constructors, keeping one obvious construction path. - [ ] Merge duplicate definitions: - `SessionResponse` vs `Session`; - `CreatePdsAccountRequest` ⊂ `CreateAccountRequest`; - `BlobLink`/`CidLink`; - `TeamMemberProfile` → `ProfileViewDetailed`; - the three report-subject copies → one union; - the non-generic `GetRecordResponse`. CHANGELOG `Breaking changes` with a migration note per moved or renamed member.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Grandiras/ATProto.NET#122
No description provided.