update doc
This commit is contained in:
+108
-10
@@ -408,23 +408,121 @@ let event = list::build_list_event(&my_keys, &mine)?; // kind 13302, NIP-44
|
||||
strictly newer join outruns it. `fits()` is the write gate — 50 memberships and
|
||||
the NIP-44 size cap, both protocol constants.
|
||||
|
||||
## GPUI conventions
|
||||
## GPUI integration
|
||||
|
||||
- Wrap, decrypt, verify, fold and every database or relay call go in
|
||||
`cx.background_spawn`. A secp256k1 verification per edition is far too
|
||||
expensive for the foreground thread.
|
||||
- Hold entities foreground: `cx.spawn` with `this.update(cx, |this, cx| …)` and
|
||||
the inner `cx`, keeping the returned `Task` in a field so it is cancelled with
|
||||
the view.
|
||||
- In tests, use `cx.background_executor().timer(..)` for delays, never
|
||||
`smol::Timer`, or `run_until_parked()` will find nothing left to run.
|
||||
`crates/concord` stays GPUI-free. The UI layer adds a registry global and one
|
||||
entity per community, and moves every decrypt, verification, fold and I/O off
|
||||
the foreground thread.
|
||||
|
||||
### Entities
|
||||
|
||||
Same shape as `ChatRegistry`:
|
||||
|
||||
```rust
|
||||
pub fn init(window: &mut Window, cx: &mut App) {
|
||||
ConcordRegistry::set_global(cx.new(|cx| ConcordRegistry::new(window, cx)), cx);
|
||||
}
|
||||
|
||||
impl ConcordRegistry {
|
||||
pub fn global(cx: &App) -> Entity<Self> {
|
||||
cx.global::<GlobalConcordRegistry>().0.clone()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Call it after `chat::init` in `desktop/src/main.rs` and `web/src/lib.rs`, and
|
||||
subscribe to `NostrRegistry` for `SignerChanged` so the communities reset with
|
||||
the account.
|
||||
|
||||
- `ConcordRegistry` holds `communities: Vec<Entity<Community>>`, an index by
|
||||
`CommunityId`, and `tasks: SmallVec<[Task<Result<(), Error>>; 2]>`.
|
||||
- `Community` owns one `CommunityState`, the last `ControlFold`, the member list
|
||||
and the channel list. Views render `Entity<Community>`; no protocol state
|
||||
lives in a view.
|
||||
- `CommunityState::apply_fold` is one assignment: run it in the task that
|
||||
produced the fold and send only the result to the foreground.
|
||||
- Emit an event on every fold so dependents re-read.
|
||||
|
||||
### Foreground and background
|
||||
|
||||
A background task never touches an entity. It sends results through a bounded
|
||||
`flume` channel that a foreground `cx.spawn` drains with `this.update(...)`.
|
||||
|
||||
```rust
|
||||
let (signal_tx, signal_rx) = flume::bounded::<Signal>(256);
|
||||
let database = client.database().clone();
|
||||
|
||||
// Background: open, verify, fold — no entities.
|
||||
self.ingress = Some(cx.background_spawn(async move {
|
||||
for wrap in &wraps {
|
||||
let Some(plane) = planes.iter().find(|plane| plane.group.pk() == wrap.pubkey) else {
|
||||
continue;
|
||||
};
|
||||
let (opened, rumor) = chat::open(wrap, &plane.group, &plane.channel, plane.epoch)?;
|
||||
store::cache_rumor(database.as_ref(), &plane.channel, &opened).await?;
|
||||
signal_tx.send_async(Signal::Chat { channel: plane.channel, rumor }).await?;
|
||||
}
|
||||
Ok(())
|
||||
}));
|
||||
|
||||
// Foreground: the only place entities change.
|
||||
self.consumer = Some(cx.spawn(async move |this, cx| {
|
||||
while let Ok(signal) = signal_rx.recv_async().await {
|
||||
this.update(cx, |this, cx| this.apply(signal, cx))?;
|
||||
}
|
||||
Ok(())
|
||||
}));
|
||||
```
|
||||
|
||||
- `client.database()` is a `&Arc<dyn NostrDatabase>` and `store::save_state`
|
||||
wants `&dyn NostrDatabase`, so clone the `Arc` and pass `database.as_ref()`.
|
||||
- Keep long-lived tasks in fields — dropping a `Task` cancels it. Assign `None`
|
||||
to an `Option<Task<_>>` before respawning it; a signer change replaces both
|
||||
the listener and the consumer.
|
||||
- `cx.spawn` when the work updates an entity after awaiting, and
|
||||
`cx.background_spawn` when it only produces a value. A query the foreground awaits can be returned straight out: `fn messages(&self, cx: &App) -> Task<Result<Vec<ChatMessage>, Error>>`.
|
||||
- Do the first load in `cx.defer_in(window, ...)` so `init` returns before the
|
||||
first relay request.
|
||||
- NIP-46 signing is async: call `signer.get_public_key_async()` /
|
||||
`sign_event_async` inside the background task. The builders still take
|
||||
`&Keys`, so run them where device keys are available.
|
||||
|
||||
### Subscriptions
|
||||
|
||||
A wrap is addressed to a plane, so the plane's public key is the routing key and
|
||||
one `Filter` per held plane is enough:
|
||||
|
||||
```rust
|
||||
let filter = Filter::new()
|
||||
.kinds([Kind::from(KIND_WRAP), Kind::from(KIND_WRAP_EPHEMERAL)])
|
||||
.pubkey(plane.group.pk())
|
||||
.since(joined_at);
|
||||
client.subscribe(filter).with_id(sub_id).await?;
|
||||
```
|
||||
|
||||
- `pubkeys([...])` carries every plane of a community on one subscription. Call
|
||||
`subscribe` again with the new address whenever a join, a channel add or a
|
||||
rekey fold changes it.
|
||||
- Route inbound events by `subscription_id` from `RelayMessage::Event`, never by
|
||||
kind.
|
||||
- Watch one epoch ahead: while holding `root_N`, subscribe to
|
||||
`base_rekey_group_key(&root_N, &community_id, Epoch(N + 1))` and to
|
||||
`channel_rekey_group_key(&root_N, &channel, Epoch(N + 1))` for each private
|
||||
channel. A second epoch ahead is not derivable until the new root arrives.
|
||||
|
||||
### Tests
|
||||
|
||||
`cx.background_executor().timer(..)` for delays, never `smol::Timer`, or
|
||||
`run_until_parked()` finds nothing left to run. Push a wrap into the channel and
|
||||
`run_until_parked()` to drive the foreground consumer.
|
||||
|
||||
## Not wired up yet
|
||||
|
||||
- **No registry and no sync engine.** `crates/concord` has no subscriptions, no
|
||||
`init`, and no `Entity<Community>`; the UI owns subscribing, routing a wrap to
|
||||
the plane whose address it carries, and rebuilding a subscription when a plane's
|
||||
address changes (join, channel added, rekey folded).
|
||||
address changes (join, channel added, rekey folded). GPUI integration above is
|
||||
the shape to build, not code that exists.
|
||||
- **Every writer takes `&Keys`, not a `NostrSigner`.** NIP-46 is one deliberate
|
||||
pass over the builders, not a per-call patch.
|
||||
- **`crates/chat/src/lib.rs::handle_notifications` treats every kind 1059 event as
|
||||
|
||||
Reference in New Issue
Block a user