273 lines
9.3 KiB
Markdown
273 lines
9.3 KiB
Markdown
# Kinds
|
||
|
||
Every event carries a `kind` field — a 16-bit integer that says what the
|
||
event means. Kind 0 is a profile. Kind 1 is a short text note. Kind 5 is
|
||
a deletion request. Kind 1984 is a content report. The number is the only
|
||
thing that distinguishes a follow list from a zap receipt; the rest of the
|
||
event structure is identical.
|
||
|
||
Using integers instead of human-readable names is deliberate. In a protocol
|
||
built on signed, immutable data, names can never change once chosen. Numbers
|
||
carry no inherent meaning, which lets every implementer assign their own
|
||
terminology based on their understanding of a kind's purpose. The protocol
|
||
says "kind 0"; whether your library calls that `Metadata`, `Profile`, or
|
||
`UserInfo` is your business.
|
||
|
||
This chapter does not catalog individual kinds — a future series of chapters
|
||
will do that. Instead it builds the infrastructure that those chapters will use:
|
||
a trait for attaching behavior to a kind number, free functions for
|
||
classifying raw kind values, and a concrete example to prove the pattern
|
||
works.
|
||
|
||
## The module
|
||
|
||
```rust {file=coracle-lib/src/lib.rs}
|
||
pub mod kinds;
|
||
```
|
||
|
||
```rust {file=coracle-lib/src/kinds.rs}
|
||
//! Event kind classification and the [`Kind`] trait for attaching
|
||
//! domain-specific behavior to kind numbers.
|
||
|
||
use std::fmt;
|
||
|
||
use crate::events::{Event, EventContent, EventTemplate};
|
||
```
|
||
|
||
## Classifying a kind number
|
||
|
||
The nostr protocol divides the 16-bit kind space into four ranges, each
|
||
with different storage and replacement semantics:
|
||
|
||
| Range | Name | Behavior |
|
||
|-------|------|----------|
|
||
| 0–9999 | Regular | Stored by relays, no replacement logic |
|
||
| 10000–19999 | Replaceable | Relay keeps only the latest event per author and kind |
|
||
| 20000–29999 | Ephemeral | Relays broadcast but do not store |
|
||
| 30000–39999 | Addressable | Like replaceable, but partitioned by a `d` tag |
|
||
|
||
Two exceptions break the pattern: kinds 0 and 3 sit below 10000 but are
|
||
replaceable. A user's profile (kind 0) and follow list (kind 3) predate
|
||
the range system and were grandfathered in.
|
||
|
||
These four functions encode the rules so that the rest of the library doesn't
|
||
have to remember them:
|
||
|
||
```rust {file=coracle-lib/src/kinds.rs}
|
||
/// A regular event: stored by relays with no replacement logic.
|
||
pub fn is_regular(kind: u16) -> bool {
|
||
kind != 0 && kind != 3 && kind < 10_000
|
||
}
|
||
|
||
/// A replaceable event: relays keep only the latest per (author, kind).
|
||
/// Kinds 0 and 3 are replaceable despite falling below 10000.
|
||
pub fn is_replaceable(kind: u16) -> bool {
|
||
kind == 0 || kind == 3 || (10_000 <= kind && kind < 20_000)
|
||
}
|
||
|
||
/// An ephemeral event: relays broadcast it but do not store it.
|
||
pub fn is_ephemeral(kind: u16) -> bool {
|
||
20_000 <= kind && kind < 30_000
|
||
}
|
||
|
||
/// An addressable event: like replaceable, but the relay partitions by
|
||
/// the event's `d` tag in addition to author and kind.
|
||
pub fn is_addressable(kind: u16) -> bool {
|
||
30_000 <= kind && kind < 40_000
|
||
}
|
||
```
|
||
|
||
These are free functions rather than methods on a type because plenty of
|
||
code needs to classify a raw `u16` without having a `Kind` implementor
|
||
in hand. Relay logic, filter construction, and storage layers all work
|
||
with kind numbers directly.
|
||
|
||
## Errors
|
||
|
||
Parsing an event into a kind-specific type can fail for two reasons: the
|
||
event's kind doesn't match what the parser expects, or the content and
|
||
tags don't conform to the kind's format.
|
||
|
||
```rust {file=coracle-lib/src/kinds.rs}
|
||
/// Errors that can occur when parsing an event into a kind-specific
|
||
/// domain type.
|
||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||
pub enum KindError {
|
||
/// The event's kind field does not match the expected kind number.
|
||
WrongKind {
|
||
expected: u16,
|
||
got: u16,
|
||
},
|
||
/// The event's content or tags could not be parsed for this kind.
|
||
InvalidContent(String),
|
||
}
|
||
|
||
impl fmt::Display for KindError {
|
||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||
match self {
|
||
KindError::WrongKind { expected, got } => {
|
||
write!(f, "expected kind {expected}, got {got}")
|
||
}
|
||
KindError::InvalidContent(msg) => {
|
||
write!(f, "invalid content for kind: {msg}")
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
impl std::error::Error for KindError {}
|
||
```
|
||
|
||
## The `Kind` trait
|
||
|
||
A kind is more than a number — it's a contract about what an event's
|
||
content and tags mean and how to work with them. The `Kind` trait captures
|
||
that contract. Implement it for a domain type and you get:
|
||
|
||
- A kind number constant that ties the type to a specific integer, plus
|
||
a `kind()` method to access it from an instance.
|
||
- A `from_event` entry point that checks the kind and delegates to your
|
||
parser.
|
||
- A `to_template` method that serializes your type back into an event.
|
||
|
||
The set of kinds is open-ended — every NIP can introduce new ones — so
|
||
a trait is the right abstraction. Each module that handles a specific kind
|
||
implements the trait for its own type without touching a central enum or
|
||
registry.
|
||
|
||
```rust {file=coracle-lib/src/kinds.rs}
|
||
/// A domain type that corresponds to a specific event kind.
|
||
///
|
||
/// Implementors provide a kind number, a parser that reads an event's
|
||
/// content and tags into the domain type, and a serializer that
|
||
/// converts it back into an event template. The trait provides a
|
||
/// `from_event` entry point that checks the kind number before parsing.
|
||
pub trait Kind: Sized {
|
||
/// The kind number this type handles.
|
||
const KIND: u16;
|
||
|
||
/// Parse an event's content and tags into this domain type.
|
||
///
|
||
/// This is called by [`from_event`](Kind::from_event) after the kind
|
||
/// number check passes, so implementations can assume the kind is
|
||
/// correct.
|
||
fn parse(event: &Event) -> Result<Self, KindError>;
|
||
|
||
/// Convert this domain type into an event's content and tags.
|
||
fn to_content(&self) -> EventContent;
|
||
|
||
/// The kind number for this instance.
|
||
fn kind(&self) -> u16 {
|
||
Self::KIND
|
||
}
|
||
|
||
/// Convert this domain type into an event template.
|
||
///
|
||
/// The default implementation calls [`to_content`](Kind::to_content)
|
||
/// and attaches the kind number. Override this only if you need
|
||
/// non-standard behavior.
|
||
fn to_template(&self) -> EventTemplate {
|
||
self.to_content().kind(Self::KIND)
|
||
}
|
||
|
||
/// Parse an event into this domain type, checking the kind number
|
||
/// first.
|
||
fn from_event(event: &Event) -> Result<Self, KindError> {
|
||
if event.kind != Self::KIND {
|
||
return Err(KindError::WrongKind {
|
||
expected: Self::KIND,
|
||
got: event.kind,
|
||
});
|
||
}
|
||
Self::parse(event)
|
||
}
|
||
}
|
||
```
|
||
|
||
The trait has one associated constant and two required methods.
|
||
Implementors provide `parse` (event to domain type) and `to_content`
|
||
(domain type to event content). The trait provides `kind()` for
|
||
instance access, `to_template()` to attach the kind number, and
|
||
`from_event()` to check the kind before parsing.
|
||
|
||
Classification lives in the free functions from earlier in the chapter.
|
||
Given any `Kind` implementor, `is_replaceable(deletion.kind())` tells
|
||
you what you need to know.
|
||
|
||
The `to_content` / `to_template` split keeps implementors from having to
|
||
repeat the kind number. `to_content` returns the content and tags;
|
||
`to_template` attaches the kind. Since the kind is already known from
|
||
the constant, there's no reason to ask every implementor to specify it
|
||
again.
|
||
|
||
## A concrete example
|
||
|
||
Kind 5 is a deletion request. Its content is an optional human-readable
|
||
reason, and its tags reference the events or addresses being deleted. It
|
||
makes a good first implementation of the `Kind` trait: simple enough to
|
||
fit in a few lines, real enough to exercise every part of the interface.
|
||
|
||
```rust
|
||
use coracle_lib::kinds::{Kind, KindError};
|
||
use coracle_lib::events::{Event, EventContent};
|
||
use coracle_lib::tags::Tags;
|
||
|
||
/// A deletion request (kind 5). The content is an optional reason
|
||
/// string, and the tags reference the events or addresses to delete.
|
||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||
pub struct Deletion {
|
||
/// An optional human-readable reason for the deletion.
|
||
pub reason: String,
|
||
/// The tags from the original event, which reference the events
|
||
/// or addresses being deleted.
|
||
pub tags: Tags,
|
||
}
|
||
|
||
impl Kind for Deletion {
|
||
const KIND: u16 = 5;
|
||
|
||
fn parse(event: &Event) -> Result<Self, KindError> {
|
||
Ok(Deletion {
|
||
reason: event.content.clone(),
|
||
tags: event.tags.clone(),
|
||
})
|
||
}
|
||
|
||
fn to_content(&self) -> EventContent {
|
||
EventContent::new(self.reason.clone(), self.tags.clone())
|
||
}
|
||
}
|
||
```
|
||
|
||
With this implementation, parsing a deletion event is:
|
||
|
||
```rust
|
||
let deletion = Deletion::from_event(&event)?;
|
||
```
|
||
|
||
And creating an event template from a `Deletion` value is:
|
||
|
||
```rust
|
||
let template = deletion.to_template();
|
||
assert_eq!(template.kind, 5);
|
||
```
|
||
|
||
Classification uses the free functions with the instance's kind number:
|
||
|
||
```rust
|
||
use coracle_lib::kinds;
|
||
|
||
assert!(kinds::is_regular(deletion.kind()));
|
||
assert!(!kinds::is_replaceable(deletion.kind()));
|
||
```
|
||
|
||
Later chapters will follow this same pattern for profiles, follow lists,
|
||
reactions, zaps, and everything else. The shape is always the same:
|
||
define a struct, implement `Kind`, and the trait provides the rest.
|
||
|
||
## What's next
|
||
|
||
The next chapter looks more closely at kind ranges — the system that
|
||
ties storage behavior to kind numbers — and what that means for relay
|
||
queries and event addressing.
|