Add kinds chapter
This commit is contained in:
@@ -0,0 +1,272 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,175 @@
|
||||
# Plan: Kinds
|
||||
|
||||
## Topic Summary
|
||||
|
||||
The kinds chapter explains what event kinds are — 16-bit integers that determine how an
|
||||
event's content and tags should be interpreted — without going into detail on any particular
|
||||
kind. It introduces a `Kind` trait that domain chapters will implement to attach behavior
|
||||
(validation, parsing, creation) to specific kind numbers. Classification methods like
|
||||
`is_replaceable()` get default implementations derived from the kind number.
|
||||
|
||||
## Chapter Outline
|
||||
|
||||
1. **Introduction** — What kinds are and why they're integers not names. Draw on
|
||||
building-nostr philosophy: numbers are meaningless, which allows subjective naming while
|
||||
keeping technical meaning intact. Mention that this chapter builds infrastructure; the
|
||||
domain chapters fill it in.
|
||||
|
||||
2. **The module** — Add `pub mod kinds;` to lib.rs, set up the file.
|
||||
|
||||
3. **The `Kind` trait** — The core abstraction. One required method: `fn kind_number() -> u16`.
|
||||
This is an associated function (not `&self`) since kind number is a property of the type,
|
||||
not an instance. Default methods for classification derive from it.
|
||||
|
||||
4. **Classification methods** — Default implementations for `is_regular()`,
|
||||
`is_replaceable()`, `is_ephemeral()`, `is_addressable()`. Explain the ranges and the
|
||||
special cases (kinds 0 and 3 are replaceable despite being < 10000). Mention the
|
||||
building-nostr critique that coupling storage behavior to kind ranges is a design flaw,
|
||||
but we implement the protocol as-is.
|
||||
|
||||
5. **Parsing events** — The `from_event` associated function on the trait. Takes an `&Event`,
|
||||
checks `event.kind == Self::kind_number()`, then delegates to a required `parse` method.
|
||||
Returns `Result<Self, KindError>`. This gives domain types a uniform entry point:
|
||||
`Profile::from_event(&event)`.
|
||||
|
||||
6. **Creating events** — A `to_template` method that converts a domain struct back into an
|
||||
`EventTemplate`. Required, no default — each kind defines its own serialization.
|
||||
|
||||
7. **A concrete example** — A trivial `Deletion` kind (kind 5) as a proof-of-concept
|
||||
implementation, showing how domain chapters will use the trait. Simple enough to not
|
||||
steal thunder from the domain chapters but real enough to prove the pattern works.
|
||||
|
||||
8. **What's next** — Tease kind ranges chapter (deeper dive into the range system and its
|
||||
implications for relay storage and query planning).
|
||||
|
||||
## API Design
|
||||
|
||||
```rust
|
||||
/// Errors when parsing an event into a domain type.
|
||||
pub enum KindError {
|
||||
/// The event's kind field doesn't match the expected kind number.
|
||||
WrongKind { expected: u16, got: u16 },
|
||||
/// The event's content or tags couldn't be parsed for this kind.
|
||||
InvalidContent(String),
|
||||
}
|
||||
|
||||
/// The core trait for attaching behavior to a kind number.
|
||||
pub trait Kind: Sized {
|
||||
/// The kind number this type handles.
|
||||
fn kind_number() -> u16;
|
||||
|
||||
/// Parse an event's content and tags into this domain type.
|
||||
/// Called by `from_event` after the kind number check passes.
|
||||
fn parse(event: &Event) -> Result<Self, KindError>;
|
||||
|
||||
/// Convert this domain type back into an event template.
|
||||
fn to_template(&self) -> EventTemplate;
|
||||
|
||||
/// Parse an event into this type, checking the kind number first.
|
||||
fn from_event(event: &Event) -> Result<Self, KindError> {
|
||||
if event.kind != Self::kind_number() {
|
||||
return Err(KindError::WrongKind {
|
||||
expected: Self::kind_number(),
|
||||
got: event.kind,
|
||||
});
|
||||
}
|
||||
Self::parse(event)
|
||||
}
|
||||
|
||||
// --- Classification defaults ---
|
||||
|
||||
fn is_regular() -> bool {
|
||||
let k = Self::kind_number();
|
||||
k != 0 && k != 3 && k < 10_000
|
||||
}
|
||||
|
||||
fn is_replaceable() -> bool {
|
||||
let k = Self::kind_number();
|
||||
k == 0 || k == 3 || (10_000 <= k && k < 20_000)
|
||||
}
|
||||
|
||||
fn is_ephemeral() -> bool {
|
||||
let k = Self::kind_number();
|
||||
20_000 <= k && k < 30_000
|
||||
}
|
||||
|
||||
fn is_addressable() -> bool {
|
||||
let k = Self::kind_number();
|
||||
30_000 <= k && k < 40_000
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Also provide free functions for the same classification on raw `u16` values, for code that
|
||||
doesn't have a `Kind` impl handy (e.g. relay logic, filters):
|
||||
|
||||
```rust
|
||||
pub fn is_regular(kind: u16) -> bool { ... }
|
||||
pub fn is_replaceable(kind: u16) -> bool { ... }
|
||||
pub fn is_ephemeral(kind: u16) -> bool { ... }
|
||||
pub fn is_addressable(kind: u16) -> bool { ... }
|
||||
```
|
||||
|
||||
## Code Organization
|
||||
|
||||
- All code in `coracle-lib/src/kinds.rs`
|
||||
- Module declared in `coracle-lib/src/lib.rs`
|
||||
- Depends on `events` module (uses `Event`, `EventTemplate`, `EventContent`)
|
||||
- No dependency on any domain crate
|
||||
|
||||
## Dependencies
|
||||
|
||||
No new external crates needed. Only uses std and types from earlier chapters.
|
||||
|
||||
## Narrative Notes
|
||||
|
||||
- Lead with the philosophy of why kinds are numbers. The building-nostr quote about
|
||||
meaningless integers allowing subjective naming is compelling.
|
||||
- Explain the trait design choice: why a trait instead of an enum. The set of kinds is
|
||||
open-ended and grows with every NIP. An enum would require updating a central file for
|
||||
every new kind. A trait lets domain chapters add kinds independently.
|
||||
- The classification defaults are a nice teaching moment: "implement one method, get four
|
||||
for free" demonstrates Rust's default method pattern.
|
||||
- The `from_event`/`parse` split mirrors applesauce's factory/cast pattern but in Rust
|
||||
idiom: `from_event` is the public API that does the kind check, `parse` is the
|
||||
kind-specific logic implementors provide.
|
||||
- Keep the Deletion example minimal — just enough to show the trait in action.
|
||||
- Note the building-nostr critique of kind ranges coupling content type with storage
|
||||
behavior, but don't dwell on it. We implement the protocol.
|
||||
|
||||
## Design Decisions
|
||||
|
||||
1. **Trait, not enum** — The set of kinds is open and grows with each NIP. An enum would
|
||||
centralize all kind definitions. A trait lets each domain module add its own kinds
|
||||
independently. This follows the distributed pattern seen in applesauce (no centralized
|
||||
registry) and is idiomatic Rust.
|
||||
|
||||
2. **`kind_number()` as associated function, not const** — An associated const
|
||||
(`const KIND: u16`) would also work but associated functions compose better with
|
||||
default methods. Either would be fine; the chapter can use whichever reads better.
|
||||
Actually, a const may be cleaner since it can't vary per call. Decide during writing.
|
||||
|
||||
3. **Events keep `u16`** — Per user preference. The `Kind` trait lives alongside events,
|
||||
not inside them. Events are protocol-level; kinds are application-level.
|
||||
|
||||
4. **Free functions mirror trait methods** — Not all code has a `Kind` impl. Relay logic,
|
||||
filter building, and storage layers need classification from a raw `u16`. The free
|
||||
functions are the source of truth; the trait defaults call them.
|
||||
|
||||
5. **`from_event` with kind check as default** — Implementors only write `parse`, which
|
||||
can assume the kind is correct. The trait provides the boilerplate.
|
||||
|
||||
6. **Deletion as example** — Kind 5 is simple (content is a reason string, tags are
|
||||
references to deleted events) and universally understood. It won't conflict with
|
||||
domain chapter content since deletion is a protocol-level concept.
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. **Associated const vs function** — Should it be `const KIND: u16 = 5;` or
|
||||
`fn kind_number() -> u16 { 5 }`? Const is more idiomatic for a fixed value.
|
||||
Try const during writing and see if defaults can reference it cleanly.
|
||||
|
||||
2. **Should `to_template` return `EventTemplate` or `EventContent`?** — EventTemplate
|
||||
includes the kind, which the trait already knows. Returning EventContent and having
|
||||
a default `to_template` that adds the kind could reduce boilerplate. Explore during
|
||||
writing.
|
||||
@@ -0,0 +1,256 @@
|
||||
# Research: Kinds
|
||||
|
||||
## Topic Summary
|
||||
|
||||
The kinds chapter should explain what kinds are without going into detail on any kind in
|
||||
particular (that's saved for domain chapters). Instead, the chapter should create some kind
|
||||
of factory/registry/enum that can be used to add behavior to a given kind — validation,
|
||||
event template factories, interfaces for decryption, methods for converting an event of a
|
||||
particular kind into a domain-specific struct, etc. Applesauce's factory pattern is preferred
|
||||
over welshman's ad-hoc utilities approach.
|
||||
|
||||
## Philosophy
|
||||
|
||||
From building-nostr, several key principles inform this chapter:
|
||||
|
||||
**Numbers Over Names:** Event kinds are 16-bit integers. Using numbers instead of names is
|
||||
deliberate — in a distributed system built on signed data, names can't change. Numbers are
|
||||
meaningless, which allows subjective interpretations while keeping meaning intact. This frees
|
||||
implementers to assign their own terms based on understanding.
|
||||
|
||||
**Kind Ranges Encode Behavior:** The protocol defines ranges with storage/replacement semantics:
|
||||
- Regular (1-9999, with exceptions): standard events stored by relays
|
||||
- Replaceable (0, 3, 10000-19999): only latest per (pubkey, kind) kept
|
||||
- Ephemeral (20000-29999): not stored, just broadcast
|
||||
- Addressable (30000-39999): latest per (pubkey, kind, d-tag) kept
|
||||
|
||||
The author criticizes this design as coupling kind numbers with behavior — these policies are
|
||||
orthogonal to content type and "should have belonged in behavior tags."
|
||||
|
||||
**More Kinds, Less Ambiguity:** Prefer creating specific event kinds over overloading existing
|
||||
ones. "Overloading a single kind for multiple purposes is always a recipe for disaster."
|
||||
|
||||
**Implementation-First:** Nostr takes an implementation-first approach. It's only through
|
||||
implementation that what is actually needed can be understood.
|
||||
|
||||
## Reference Implementation Analysis
|
||||
|
||||
### applesauce
|
||||
|
||||
The most sophisticated factory pattern found. Uses a layered architecture:
|
||||
|
||||
**EventFactory base class** extends `Promise<T>` for fluent chaining. Core capabilities:
|
||||
- `EventFactory.fromKind<K>(kind)` — create from kind number
|
||||
- `EventFactory.fromEvent<K>(event)` — create from existing event (modify)
|
||||
- `.content()`, `.created()`, `.meta()` — base operations
|
||||
- `.modifyPublicTags()`, `.modifyHiddenTags()` — tag manipulation
|
||||
- `.encryptedContent()` — NIP-44 encryption
|
||||
- `.as(signer)` — set signer, propagated through chain
|
||||
- `.stamp()`, `.sign()` — finalize event
|
||||
|
||||
**Typed Factory subclasses** per kind:
|
||||
```typescript
|
||||
export class NoteFactory extends EventFactory<kinds.ShortTextNote, NoteTemplate> {
|
||||
static create(content?: string): NoteFactory
|
||||
static reply(parent: NostrEvent, content?: string): NoteFactory
|
||||
text(content, options?)
|
||||
mention(pubkey)
|
||||
subject(subject)
|
||||
addHashtag(hashtag)
|
||||
}
|
||||
```
|
||||
|
||||
**Dual system for create vs consume:**
|
||||
- **Factories** — for creating/modifying events (builder pattern)
|
||||
- **Casts** (`EventCast<KnownEvent<KIND>>`) — for consuming events, providing typed
|
||||
properties, validation, and observable streams for relationships
|
||||
|
||||
**List factory hierarchy** for NIP-51:
|
||||
- `ListFactory` → `NIP51RelayListFactory` / `NIP51UserListFactory` / `NIP51ItemListFactory`
|
||||
- Concrete: `BookmarkSetFactory extends NIP51ItemListFactory`
|
||||
|
||||
**Validation** via type guard functions:
|
||||
```typescript
|
||||
function isValidComment(event): event is CommentEvent {
|
||||
return event.kind === COMMENT_KIND && hasRoot(event) && hasReply(event);
|
||||
}
|
||||
```
|
||||
|
||||
**No centralized registry** — factories distributed across modules, aggregated via index.ts exports.
|
||||
|
||||
### ndk
|
||||
|
||||
Uses class-based kind wrappers with inheritance:
|
||||
|
||||
**NDKKind enum** with 200+ named variants. Each kind gets a dedicated wrapper class extending
|
||||
`NDKEvent`:
|
||||
|
||||
```typescript
|
||||
export class NDKArticle extends NDKEvent {
|
||||
static kind = NDKKind.Article;
|
||||
static kinds = [NDKKind.Article]; // supports multi-kind classes
|
||||
static from(event: NDKEvent): NDKArticle
|
||||
get title(): string | undefined
|
||||
set title(title: string | undefined)
|
||||
}
|
||||
```
|
||||
|
||||
**Multi-kind classes:** `NDKVideo` handles kinds 21, 34235, 34236, 22. `NDKRepost` handles 6
|
||||
and 16.
|
||||
|
||||
**40+ kind wrapper files** in `core/src/events/kinds/`.
|
||||
|
||||
Kind-aware behavior methods on base `NDKEvent`: `isReplaceable()`, `isEphemeral()`,
|
||||
`isParamReplaceable()`.
|
||||
|
||||
### nostr-gadgets
|
||||
|
||||
Functional/storage-oriented approach:
|
||||
|
||||
**Higher-order factory functions:**
|
||||
```typescript
|
||||
export const loadFollowsList = makeListFetcher<string>(3, RELAYS, itemsFromTags(...))
|
||||
export const loadFollowSets = makeSetFetcher<string>(30000, itemsFromTags(...))
|
||||
```
|
||||
|
||||
**Generic fetcher factories** take kind number + processor function, return typed fetchers.
|
||||
Storage interface returns different shapes based on kind classification (addressable vs
|
||||
replaceable).
|
||||
|
||||
### nostrlib
|
||||
|
||||
Go library with multi-layered kind awareness:
|
||||
|
||||
**`Kind` type** is `uint16` with categorization methods: `IsRegular()`, `IsReplaceable()`,
|
||||
`IsEphemeral()`, `IsAddressable()`.
|
||||
|
||||
**Factory map pattern** for domain objects:
|
||||
```go
|
||||
var moderationActionFactories = map[nostr.Kind]func(nostr.Event) (Action, error){
|
||||
nostr.KindSimpleGroupPutUser: func(evt nostr.Event) (Action, error) { ... },
|
||||
}
|
||||
func PrepareModerationAction(evt nostr.Event) (Action, error) {
|
||||
factory, ok := moderationActionFactories[evt.Kind]
|
||||
if !ok { return nil, fmt.Errorf("unsupported kind %d", evt.Kind) }
|
||||
return factory(evt)
|
||||
}
|
||||
```
|
||||
|
||||
**Schema-based validation** from YAML specs per kind (content format, required tags, d-tag rules).
|
||||
|
||||
**Generic list/set patterns** with `fetchGenericList[V, I](kind, index, parseTag, cache)`.
|
||||
|
||||
**Dataloader registries** — index-based arrays for 18 replaceable and 4 addressable kinds.
|
||||
|
||||
### nostr-tools
|
||||
|
||||
Foundational kind system:
|
||||
|
||||
**Const + typeof pattern:**
|
||||
```typescript
|
||||
export const ShortTextNote = 1;
|
||||
export type ShortTextNote = typeof ShortTextNote;
|
||||
```
|
||||
|
||||
**Classification functions:** `classifyKind()`, `isRegularKind()`, `isReplaceableKind()`,
|
||||
`isEphemeralKind()`, `isAddressableKind()`.
|
||||
|
||||
**Type guard:** `isKind<T>(event, kind): event is NostrEvent & { kind: T }`.
|
||||
|
||||
65+ kind constants. No factory pattern — this is the lowest-level library.
|
||||
|
||||
### rust-nostr
|
||||
|
||||
Most comprehensive Rust implementation:
|
||||
|
||||
**Macro-generated `Kind` enum** with 130+ named variants plus `Custom(u16)` fallback:
|
||||
```rust
|
||||
pub enum Kind { Metadata, TextNote, ContactList, ..., Custom(u16) }
|
||||
```
|
||||
|
||||
**Kind range constants:**
|
||||
```rust
|
||||
pub const REPLACEABLE_RANGE: Range<u16> = 10_000..20_000;
|
||||
pub const EPHEMERAL_RANGE: Range<u16> = 20_000..30_000;
|
||||
pub const ADDRESSABLE_RANGE: Range<u16> = 30_000..40_000;
|
||||
```
|
||||
|
||||
**Behavior methods:** `is_regular()`, `is_replaceable()`, `is_ephemeral()`, `is_addressable()`.
|
||||
|
||||
**EventBuilder** with 140+ kind-specific factory methods:
|
||||
```rust
|
||||
pub fn text_note(content) -> Self
|
||||
pub fn metadata(metadata) -> Self
|
||||
pub fn job_request(kind) -> Result<Self, Error> // validates kind range
|
||||
```
|
||||
|
||||
**Coordinate validation** enforces kind constraints (replaceable vs addressable, d-tag rules).
|
||||
|
||||
**No central registry** — behavior statically encoded in kind ranges and builder methods.
|
||||
|
||||
### welshman
|
||||
|
||||
Ad-hoc utilities approach:
|
||||
|
||||
**Kind constants** in centralized `Kinds.ts` (60+ constants).
|
||||
|
||||
**Classification predicates:** `isRegularKind()`, `isPlainReplaceableKind()`,
|
||||
`isParameterizedReplaceableKind()`, `isEphemeralKind()`, `isDVMKind()`.
|
||||
|
||||
**Domain types per kind** with read/create/edit functions:
|
||||
- `readProfile(event) -> PublishedProfile`
|
||||
- `createProfile(profile) -> EventTemplate`
|
||||
- `editProfile(profile) -> EventTemplate`
|
||||
- `readRoomMeta(event) -> PublishedRoomMeta` (validates kind in reader)
|
||||
- `readList(event) -> PublishedList` (generic across list kinds)
|
||||
|
||||
**Store integration:** `deriveItemsByKey({ eventToItem: readProfile, filters: [{kinds: [PROFILE]}] })`
|
||||
|
||||
**Encryptable wrapper** for events with encrypted content (lists with private tags).
|
||||
|
||||
Pattern: each kind gets constants + types + read/create/edit functions + store wiring. Scattered
|
||||
across files rather than unified in a registry.
|
||||
|
||||
## Common Patterns
|
||||
|
||||
1. **Kind as integer wrapper** — all implementations use u16/number with named constants
|
||||
2. **Range-based classification** — universal: regular/replaceable/ephemeral/addressable predicates
|
||||
3. **Factory methods for creation** — builder/factory pattern to construct events of specific kinds
|
||||
4. **Parser functions for consumption** — event → domain struct conversion
|
||||
5. **Validation at boundaries** — kind checks when parsing events, kind range validation for coordinates
|
||||
6. **No centralized registry** — most implementations distribute behavior across modules rather
|
||||
than maintaining a central kind→behavior map
|
||||
|
||||
**Divergences:**
|
||||
- applesauce uses class hierarchy with fluent builder; welshman uses standalone functions
|
||||
- rust-nostr uses macro-generated enum; Go uses typed constants
|
||||
- NDK couples kind behavior to event subclasses; others keep them separate
|
||||
- Only nostrlib uses external schema-based validation
|
||||
|
||||
## Considerations for Our Implementation
|
||||
|
||||
1. **Trait-based factory pattern** — Rust traits map well to the factory/cast dual pattern from
|
||||
applesauce. A `KindHandler` trait could define validation, parsing, and creation methods.
|
||||
|
||||
2. **Kind enum with Custom fallback** — Follow rust-nostr's `Kind` enum pattern with named
|
||||
variants + `Custom(u16)`. This gives compile-time safety for known kinds while remaining
|
||||
extensible.
|
||||
|
||||
3. **Range classification as methods** — `Kind::is_replaceable()` etc. are universally needed
|
||||
and straightforward to implement.
|
||||
|
||||
4. **Separate create vs parse** — The applesauce pattern of factories (create) vs casts (parse)
|
||||
maps to Rust traits: one trait for building `EventTemplate` from domain data, another for
|
||||
extracting domain data from `Event`.
|
||||
|
||||
5. **Registry for extensibility** — A `HashMap<Kind, Box<dyn KindHandler>>` or similar registry
|
||||
would let domain chapters register their handlers without modifying the kinds chapter.
|
||||
Alternatively, use static dispatch with an enum if the set of kinds is known at compile time.
|
||||
|
||||
6. **Literate programming flow** — The chapter should introduce kinds conceptually, define the
|
||||
`Kind` type, implement classification methods, then build the trait/registry infrastructure
|
||||
that later chapters will use. Keep actual kind implementations (profiles, lists, etc.) for
|
||||
domain chapters.
|
||||
|
||||
7. **Dependencies** — This chapter depends on the events chapter (for `Event`, `EventTemplate`
|
||||
types). It should not depend on any domain-specific types.
|
||||
@@ -0,0 +1,77 @@
|
||||
use coracle_lib::kinds::{is_addressable, is_ephemeral, is_regular, is_replaceable};
|
||||
use coracle_lib::kinds::KindError;
|
||||
|
||||
// --- Free function classification tests ---
|
||||
|
||||
#[test]
|
||||
fn regular_kind_1() {
|
||||
assert!(is_regular(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn regular_kind_9999() {
|
||||
assert!(is_regular(9999));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kind_0_is_replaceable_not_regular() {
|
||||
assert!(!is_regular(0));
|
||||
assert!(is_replaceable(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kind_3_is_replaceable_not_regular() {
|
||||
assert!(!is_regular(3));
|
||||
assert!(is_replaceable(3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replaceable_range() {
|
||||
assert!(is_replaceable(10_000));
|
||||
assert!(is_replaceable(19_999));
|
||||
assert!(!is_replaceable(20_000));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ephemeral_range() {
|
||||
assert!(is_ephemeral(20_000));
|
||||
assert!(is_ephemeral(29_999));
|
||||
assert!(!is_ephemeral(30_000));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn addressable_range() {
|
||||
assert!(is_addressable(30_000));
|
||||
assert!(is_addressable(39_999));
|
||||
assert!(!is_addressable(40_000));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ranges_are_mutually_exclusive_for_regular() {
|
||||
for k in [1, 100, 5000, 9999] {
|
||||
assert!(is_regular(k));
|
||||
assert!(!is_replaceable(k));
|
||||
assert!(!is_ephemeral(k));
|
||||
assert!(!is_addressable(k));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn high_kinds_are_none_of_the_above() {
|
||||
assert!(!is_regular(40_000));
|
||||
assert!(!is_replaceable(40_000));
|
||||
assert!(!is_ephemeral(40_000));
|
||||
assert!(!is_addressable(40_000));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kind_error_display() {
|
||||
let err = KindError::WrongKind {
|
||||
expected: 5,
|
||||
got: 1,
|
||||
};
|
||||
assert_eq!(err.to_string(), "expected kind 5, got 1");
|
||||
|
||||
let err = KindError::InvalidContent("bad json".into());
|
||||
assert_eq!(err.to_string(), "invalid content for kind: bad json");
|
||||
}
|
||||
Reference in New Issue
Block a user