257 lines
10 KiB
Markdown
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.
|