refactor(core): replace Rc-based Context with a synchronous, Send-able Core#123
refactor(core): replace Rc-based Context with a synchronous, Send-able Core#123osmaczko wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
Pull request overview
Refactors the conversations core to remove Rc<RefCell<_>>-based service sharing, renaming Context to Session and making it own its services outright. Inbox/Conversation primitives become non-generic transient structs that take services and storage as &mut parameters per call. A compile-time assertion pins the Send invariant when injected services are Send. The trait objects Convo/GroupConvo/Id are removed; Session dispatches by persisted ConversationKind and exposes group operations directly.
Changes:
- Rename
Context→Session; dropRc/RefCell-shared services and dispatch byConversationKindinSession; add aSendstatic-assert. - De-generic
Inbox,InboxV2,PrivateV1Convo,GroupV1Convoand removeConvo/GroupConvo/Idtraits; pass identity/store/delivery/registry per call. CausalHistoryStorekeeps aRefCellbut dropsRc; updated call sites in client, FFI, and integration tests.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| Cargo.toml | Workspace member list edits (formatting regression: two entries on one line). |
| core/conversations/src/lib.rs | Replace context module/exports with session::{Session, ConversationId, Introduction}. |
| core/conversations/src/session.rs | New Session owning services directly; per-kind dispatch; Send assertion. |
| core/conversations/src/conversation.rs | Remove Id/Convo/GroupConvo traits, keep only re-exports. |
| core/conversations/src/conversation/privatev1.rs | PrivateV1Convo becomes non-generic; store passed per-op. |
| core/conversations/src/conversation/group_v1.rs | GroupV1Convo becomes non-generic; ctx/ds/causal/keypkg passed per-op. |
| core/conversations/src/inbox/handler.rs | Inbox becomes non-generic; identity/store passed per-op. |
| core/conversations/src/inbox_v2.rs | InboxV2 becomes non-generic; adds create_group/group_send_content/group_add_member/handle_group_message. |
| core/conversations/src/causal_history.rs | CausalHistoryStore drops Rc, keeps RefCell. |
| crates/client/src/client.rs | Use Session and &mut DS via ctx.ds(). |
| core/integration_tests_core/tests/private_integration.rs | Migrate from Context to Session. |
| core/integration_tests_core/tests/mls_integration.rs | Use Session and new direct group methods; drop convo() helper. |
| core/integration_tests_core/tests/causal_history.rs | Use Session and group_send_content. |
Comments suppressed due to low confidence (1)
core/conversations/src/session.rs:53
- Minor: the
let mut delivery = delivery;/let mut store = store;rebindings can be avoided by declaring the parameters asmut delivery: DSandmut chat_store: CS(andmut registration: RSinnew_with_name) in the function signature. Same pattern appears innew_from_storeat lines 52–53 andnew_with_nameat lines 93–95. Purely a readability nit.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| "core/double-ratchets", | ||
| "core/integration_tests_core", | ||
| "core/sqlite", | ||
| "core/integration_tests_core", "core/sqlite", |
| "core/double-ratchets", | ||
| "core/integration_tests_core", | ||
| "core/sqlite", | ||
| "core/integration_tests_core", "core/sqlite", |
4f7a9c8 to
d02d508
Compare
d02d508 to
cab032a
Compare
|
@osmaczko can you link to the project issue that this PR belongs to? |
This is part of Client Event System, as per ADR's 0001:
This PR is a prerequisite for enabling the client to perform background, threaded transport polling. I split the changes into two PRs (this + followup with background processing) to make the review easier. Do you think it makes sense to close Client Event System issue and open new one to track that? For instance: Threaded Client? |
…e Core Make the core Send so the upcoming threaded client can own it behind an Arc<Mutex<Core>>: a background worker polls the transport and handles inbound payloads while the application's own thread issues outbound calls (send, create conversation). Sharing the core across those two threads means moving it into the spawned worker, which is only legal if it is Send. Access stays serialized by the client's Mutex (one thread at a time), so the core needs Send but not Sync, and carries no lock of its own. See docs/adr/0001-client-event-system.md for the background-poller design. Dissolve the Rc<RefCell> service-sharing that made the conversations core !Send. Context is de-Rc'd and renamed Core: it owns its services outright (identity, delivery, store, registry) and drives the inbox/conversation primitives with plain &mut self. - Inbox, InboxV2, PrivateV1Convo, GroupV1Convo become non-generic and take services as &mut/& parameters; no Rc or RefCell-as-shared-state remains. - CausalHistoryStore drops its Rc (keeps a Send RefCell). - The Convo/GroupConvo/Id trait objects are removed: Core dispatches on ConversationKind and exposes group operations directly, so conversations never escape the orchestrator. - A Send static-assert pins the invariant: the core is Send whenever its services are, with no thread-affinity of its own.
cab032a to
a803bb0
Compare
jazzz
left a comment
There was a problem hiding this comment.
In general. I like this direction.
- Removing run time borrow checks is absolutely needed.
- Renaming Context is a welcome change.
I'm not entirely convinced that removing dynamic dispatch is needed/helpful in the long term. But if it simplifies things now, I don't see any blockers.
| // primitives with plain `&mut self`. | ||
| pub struct Core<DS: DeliveryService, RS: RegistrationService, CS: ChatStore> { | ||
| identity: Identity, | ||
| ds: DS, |
There was a problem hiding this comment.
[!] Longterm Its uncertain if the core can be the owner of the DS. There are possible usecases where the client, or the app may require access to the DS.
There was a problem hiding this comment.
Legitimate forward-looking flag 👍 If that ever becomes the case, shared ownership should be introduced. Let’s KISS for now.
| let (convo_id, envelopes) = self.ctx.create_private_convo(&intro, initial_content)?; | ||
| self.dispatch_all(envelopes)?; | ||
| Ok(convo_id) | ||
| self.core | ||
| .create_private_convo(&intro, initial_content) | ||
| .map_err(Into::into) | ||
| } |
There was a problem hiding this comment.
This is much cleaner. Love this direction.
| pub fn send_message(&mut self, convo_id: &str, content: &[u8]) -> Result<(), ClientError> { | ||
| self.core | ||
| .send_content(convo_id, content) | ||
| .map_err(Into::into) |
There was a problem hiding this comment.
[Dust] The map_error can be avoided, by invoking the conversion directly.
as Chat(#[from] ChatError), is defined; You can use the try operator with an explicit Ok of unit
self.core.send_content(convo_id, content)?;
Ok(())
| pub fn group_add_member( | ||
| &mut self, | ||
| convo_id: &str, | ||
| members: &[&AccountId], | ||
| ) -> Result<(), ChatError> { | ||
| let mut convo = self.load_mls_convo(convo_id)?; | ||
| convo.add_member(members, &self.mls_ctx, &mut self.ds, &self.contact_registry) | ||
| } |
There was a problem hiding this comment.
[Sand] By removing the GroupConvoTrait, this now becomes a runtime error (rather than compile time) when account_id is not of the correct type.
Not critical for V0.2 however less than Ideal.
There was a problem hiding this comment.
[?] What was the issue that led to the removal of the ConvoTraits Traits
| ConversationKind::PrivateV1 => self.private_send_content(record, content), | ||
| ConversationKind::GroupV1 => self.group_send_content(&record.local_convo_id, content), | ||
| ConversationKind::Unknown(_) => Err(ChatError::BadBundleValue(format!( | ||
| "unsupported conversation type: {}", | ||
| record.kind.as_str() | ||
| ))), |
There was a problem hiding this comment.
[Dust] Harder to manage over dynamic dispatch
| { | ||
| fn is_send<T: Send>() {} | ||
| is_send::<Core<DS, RS, CS>>(); | ||
| } |
There was a problem hiding this comment.
[Sand] This is cute - However It doesn't seem needed.
-
If you want to enforce that DS, RS, CS are Send so that Send can be auto implemented on Core
- Add the Send requirement to service_traits::DeliveryService. That way its enforced everywhere cleanly
-
Better yet. Core does not require Send to operate. It's only needed if Client requires Send. Move this constraint to the Client. Have the client enforce that all types that are passed int are Send compatible. This enforce the compile time check when building Client, and allows test cases or other use cases to use !Send Services if there is no conflict.
| ctx: &MlsCtx, | ||
| ds: &mut DS, | ||
| keypkg_provider: &KP, | ||
| ) -> Result<(), ChatError> { |
There was a problem hiding this comment.
Moving services to be passed in on invocation is a positive change; rather than references being retained which complicates ownership.
Specifically as this PR removes dynamic dispatch, there are no requirements for functions to be similar across Conversations of the the same class.
However
[Sand] Consider combining Services into a common provider object. There are more services required to facilitate DeMLS. Adding them all as parameters will be harder to manage at various callsites, and implementations.
Here is an example usage: group_v1::add_client
With Core mapping to core_client::InnerClient
If desirable to constrain which services a function has access to, the ExternalServices bounds can be updated per function.
Make the core Send so the upcoming threaded client can own it behind an
Arc<Mutex>: a background worker polls the transport and handles
inbound payloads while the application's own thread issues outbound calls
(send, create conversation). Sharing the core across those two threads means
moving it into the spawned worker, which is only legal if it is Send. Access
stays serialized by the client's Mutex (one thread at a time), so the core
needs Send but not Sync, and carries no lock of its own. See
docs/adr/0001-client-event-system.md for the background-poller design.
Dissolve the Rc service-sharing that made the conversations core
!Send. Context is de-Rc'd and renamed Core: it owns its services outright
(identity, delivery, store, registry) and drives the inbox/conversation
primitives with plain &mut self.
services as &mut/& parameters; no Rc or RefCell-as-shared-state remains.
ConversationKind and exposes group operations directly, so conversations
never escape the orchestrator.
services are, with no thread-affinity of its own.