update backend
This commit is contained in:
+42
-15
@@ -29,11 +29,16 @@ vocabulary are shared substrate — every document calls them — so they live o
|
||||
| `cord05` | Invite bundles, links, the Direct Invite, the Invite List |
|
||||
| `cord06` | Key rotations, refounding, compaction, dissolution |
|
||||
| `derive` | Every frozen HKDF derivation and coordinate |
|
||||
| `store` | Local rumor cache, the community state document, relay paging |
|
||||
| `state` | The community state document and its pure folds — no I/O |
|
||||
|
||||
`CommunityId`, `ChannelId`, `RoleId`, `Epoch` and `Extra` (crate-internal) come from
|
||||
the private `types` module and are re-exported at the crate root.
|
||||
|
||||
Nothing in `concord` touches a relay or a database. The local rumor cache, the
|
||||
state documents and the relay paging walk live one crate up, in
|
||||
`community::cache` and `community::history`, and are what a client actually calls
|
||||
(read on below).
|
||||
|
||||
CORD-07 (audio/video) is unimplemented. CORD-08's timer has no file of its own: it
|
||||
lives in the metadata it reads (`cord02`) and the fold it filters (`cord03`).
|
||||
|
||||
@@ -44,7 +49,8 @@ Read `CommunityId` as "this community", `ChannelId` as "this channel", `Epoch` a
|
||||
|
||||
```rust
|
||||
use concord::cord02::{self, CommunityMetadata};
|
||||
use concord::store::{self, CommunityState, save_state};
|
||||
use concord::state::CommunityState;
|
||||
use community::cache::save_state;
|
||||
|
||||
let metadata = CommunityMetadata { name: "Room".into(), ..Default::default() };
|
||||
let minted = cord02::genesis(&owner_keys, &metadata, now_secs).await?;
|
||||
@@ -187,6 +193,7 @@ about an existing `EventId` rather than a mutation.
|
||||
|
||||
```rust
|
||||
use concord::cord03::{self, fold, plane_keys};
|
||||
use community::cache;
|
||||
|
||||
let planes = plane_keys(&held, &channel)?; // &[(Epoch, secret)]
|
||||
let mut rumors = Vec::new();
|
||||
@@ -198,7 +205,7 @@ for wrap in &wraps {
|
||||
let Ok((opened, rumor)) = cord03::open(wrap, group, &channel, *epoch) else {
|
||||
continue;
|
||||
};
|
||||
store::cache_rumor(&client, &channel, &opened).await?;
|
||||
cache::cache_rumor(&client, &channel, &opened).await?;
|
||||
rumors.push(rumor);
|
||||
}
|
||||
|
||||
@@ -215,15 +222,35 @@ let messages = fold(&rumors, Timestamp::now(), |actor, citation, author| {
|
||||
Relay history pages through the local cache:
|
||||
|
||||
```rust
|
||||
let page = store::backfill(client, &channel, &held, until, 50).await?;
|
||||
let cached = store::query_rumors(&client, &channel, None, 50).await?;
|
||||
use community::history::{self, Window};
|
||||
|
||||
let page = history::page(client, &channel, &held, &relays, Window::newest(), 20, 50).await?;
|
||||
let cached = cache::query_rumors(&client, &channel, None, 50, Some(&cord03::ROW_KINDS)).await?;
|
||||
```
|
||||
|
||||
`backfill` walks newest-first across every held epoch, caches what it opens, and
|
||||
stops on a short page. `query_rumors` is the read path when the group keys are
|
||||
gone. Run `store::purge_expired(client, &channel, now)` on the same cadence as
|
||||
any other local sweep — the timer is cooperative, so the local store is the
|
||||
artifact that has to forget.
|
||||
A relay is only ever *asked*: `history::page` subscribes each one to the page's
|
||||
filter (which is what makes the client verify a wrap, deduplicate it and persist
|
||||
it), waits for EOSE, and then reads the page back out of the local database —
|
||||
no wrap is ever consumed straight off the wire, and `fetch_events` is not used
|
||||
anywhere. The wrap lands in the client's shared event store through that
|
||||
ordinary ingest path; `cache_rumor` then puts the decrypted rumor beside it, and
|
||||
only the rumor is ever served to a reader.
|
||||
|
||||
A page ends on EOSE. A CLOSED ends it as unanswered, *except* NIP-42's
|
||||
`auth-required`: the SDK re-issues that REQ under the same subscription id once
|
||||
the handshake completes, so the page waits for the resubscribed answer rather
|
||||
than writing off a relay that only wanted to authenticate.
|
||||
|
||||
`history::page` walks newest-first across every held epoch, caches what it opens, and
|
||||
reports what it saw: `oldest_ms`/`newest_ms` feed the caller's `ChannelCursor`,
|
||||
`exhausted` is earned only by a short page *after* history was seen, and an
|
||||
all-empty answer sets `failed` so a later round re-asks instead of sealing the
|
||||
channel at "no more history". Page down with `Window::older_than(seen.oldest_ms)`,
|
||||
and read the region between two cursors with `Window::between(..)`. `query_rumors`
|
||||
is the read path when the group keys are gone; pass `kinds` to budget rows apart
|
||||
from the events that only decorate them. Run `cache::purge_expired(client, &channel, now)`
|
||||
on the same cadence as any other local sweep — the timer is cooperative, so the
|
||||
local store is the artifact that has to forget.
|
||||
|
||||
`ChatAction::TimerNotice { seconds }` is a policy notice, not a message: render it
|
||||
as an inline row only when its author passes
|
||||
@@ -419,7 +446,7 @@ A member's own memberships, synced across their devices:
|
||||
```rust
|
||||
use concord::cord02::list;
|
||||
|
||||
let entry = concord::store::list_entry(&state, &metadata.name); // state → §8 material
|
||||
let entry = concord::state::list_entry(&state, &metadata.name); // state → §8 material
|
||||
let held = list::parse_list_event(&my_keys, &event).await?; // validates the d tag
|
||||
let mine = held.joined(entry); // community_id-keyed union
|
||||
let event = list::build_list_event(&my_keys, &mine, 0, now_secs).await?; // kind 33302, d = 0
|
||||
@@ -431,7 +458,7 @@ locally *before* it resolves targets, so the fragment is available to the
|
||||
`concord/list` read path even if every relay is unreachable.
|
||||
|
||||
`join_material(&invite, control_root)` makes the §8 material from a CORD-05
|
||||
invite; `store::list_entry(state, name)` makes it from a `CommunityState`, and is
|
||||
invite; `concord::state::list_entry(state, name)` makes it from a `CommunityState`, and is
|
||||
what a write path uses after a create, join or rename. `joined` and `tombstoned`
|
||||
are the two mutations: both are `community_id`-keyed unions, so neither an append
|
||||
nor a leave can lose a membership the other writer has.
|
||||
@@ -538,7 +565,7 @@ self.ingress = Some(cx.background_spawn(async move {
|
||||
continue;
|
||||
};
|
||||
let (opened, rumor) = cord03::open(wrap, &plane.group, &plane.channel, plane.epoch)?;
|
||||
store::cache_rumor(&client, &plane.channel, &opened).await?;
|
||||
cache::cache_rumor(&client, &plane.channel, &opened).await?;
|
||||
signal_tx.send_async(Signal::Chat { channel: plane.channel, rumor }).await?;
|
||||
}
|
||||
Ok(())
|
||||
@@ -553,7 +580,7 @@ self.consumer = Some(cx.spawn(async move |this, cx| {
|
||||
}));
|
||||
```
|
||||
|
||||
- Every store function takes the `&Client` and reaches the database through
|
||||
- Every cache function takes the `&Client` and reaches the database through
|
||||
`client.database()`, so clone the `Client` into the background task.
|
||||
- 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
|
||||
@@ -627,7 +654,7 @@ client.subscribe(filter).with_id(sub_id).await?;
|
||||
app `UniversalSigner` both work. The NIP-59 paths (`build_direct_invite`,
|
||||
`unwrap_direct_invite`) stay `Sized` because the SDK's gift-wrap helpers are.
|
||||
Group-key and locally-held-secret writers (`cord01` wrap functions,
|
||||
`cord05::build_bundle_event`, `store`) still take the raw key material they
|
||||
`cord05::build_bundle_event`, `community::cache`) still take the raw key material they
|
||||
genuinely need.
|
||||
- **`crates/chat/src/lib.rs::handle_notifications` treats every kind 1059 event as
|
||||
a NIP-59 gift wrap for the current user.** Concord wraps are kind 1059 too, so
|
||||
|
||||
Reference in New Issue
Block a user