Files
coracle-rust/book/research/kinds.md
T
2026-04-16 17:37:21 -07:00

257 lines
10 KiB
Markdown

# 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.