Add expiration chapter
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
# Plan: Expiring Events
|
||||
|
||||
## Topic Summary
|
||||
|
||||
NIP-40 defines an optional `expiration` tag:
|
||||
`["expiration", "<unix_timestamp_seconds>"]`. It marks an event as no
|
||||
longer valid after the given time. Relays SHOULD stop serving expired
|
||||
events and MAY drop them; clients SHOULD hide them. Expiration is a
|
||||
cooperative hint, not a cryptographic guarantee.
|
||||
|
||||
This chapter adds tag-layer support: a parser, a predicate, a
|
||||
constructor, and the usual method facades on `HashedEvent` and `Event`.
|
||||
Enforcement (storage sweeps, UI hiding) is out of scope.
|
||||
|
||||
## Chapter Outline
|
||||
|
||||
1. **Framing** — expiration as a behavior tag; why it lives on `Tags`
|
||||
rather than on kind; cooperative nature of the feature.
|
||||
2. **The module declaration** — `pub mod expiration;` in `lib.rs` plus
|
||||
the module header.
|
||||
3. **Reading the tag** — `get_expiration(&Tags) -> Option<u64>`.
|
||||
Explain missing-vs-malformed both collapsing to `None`.
|
||||
4. **Testable predicate** — `is_expired_at(&Tags, now: u64) -> bool`.
|
||||
Explain the strict-`<` comparison.
|
||||
5. **System-clock predicate** — `is_expired(&Tags) -> bool`, plus the
|
||||
private `now_seconds()` helper.
|
||||
6. **Constructor** — `Tag::expiration(ts: u64) -> Tag`. Explain why we
|
||||
bother: a chapter's worth of call sites read better with a named
|
||||
constructor than with `Tag::new("expiration", [ts.to_string()])`.
|
||||
7. **Methods on event types** — `get_expiration`, `is_expired`,
|
||||
`is_expired_at` on both `HashedEvent` and `Event`.
|
||||
8. **Usage patterns** — the expected call sites (a relay filter, a UI
|
||||
check, a builder attaching a tag), very briefly.
|
||||
9. **What's next** — bridge to protected events (chapter 10).
|
||||
|
||||
## API Design
|
||||
|
||||
### Module `coracle-lib/src/expiration.rs`
|
||||
|
||||
```rust
|
||||
pub fn get_expiration(tags: &Tags) -> Option<u64>;
|
||||
pub fn is_expired_at(tags: &Tags, now: u64) -> bool;
|
||||
pub fn is_expired(tags: &Tags) -> bool;
|
||||
|
||||
impl HashedEvent {
|
||||
pub fn get_expiration(&self) -> Option<u64>;
|
||||
pub fn is_expired_at(&self, now: u64) -> bool;
|
||||
pub fn is_expired(&self) -> bool;
|
||||
}
|
||||
|
||||
impl Event {
|
||||
pub fn get_expiration(&self) -> Option<u64>;
|
||||
pub fn is_expired_at(&self, now: u64) -> bool;
|
||||
pub fn is_expired(&self) -> bool;
|
||||
}
|
||||
```
|
||||
|
||||
### Tag constructor in `coracle-lib/src/tags.rs`
|
||||
|
||||
```rust
|
||||
impl Tag {
|
||||
pub fn expiration(timestamp: u64) -> Tag;
|
||||
}
|
||||
```
|
||||
|
||||
Rationale for placement: the constructor lives with `Tag` (not in
|
||||
`expiration.rs`) because it belongs to the tag-construction surface,
|
||||
which callers already import when they build tags. This matches how the
|
||||
nonce tag is handled — built inline at its mining site — but gives
|
||||
`expiration` a nicer call site because building this tag is common.
|
||||
|
||||
## Code Organization
|
||||
|
||||
- **`coracle-lib/src/expiration.rs`** — new module for the free
|
||||
functions and the method impls.
|
||||
- **`coracle-lib/src/lib.rs`** — add `pub mod expiration;`.
|
||||
- **`coracle-lib/src/tags.rs`** — add the `Tag::expiration` constructor
|
||||
inside an existing `impl Tag` block, tangled from the expiration
|
||||
chapter (code blocks targeting the same file concatenate; the new
|
||||
block adds another `impl Tag` block appended at the end, which is
|
||||
valid Rust).
|
||||
- **`coracle-lib/tests/expiration.rs`** — integration test (hand-
|
||||
written, not tangled).
|
||||
|
||||
## Dependencies
|
||||
|
||||
None. `std::time::{SystemTime, UNIX_EPOCH}` is already in std. No new
|
||||
crate dependencies, no new workspace entries.
|
||||
|
||||
## Narrative Notes
|
||||
|
||||
- Lead with the behavior-tag framing. A reader who has just finished
|
||||
proof of work will recognize the pattern: a tag that *requests*
|
||||
downstream cooperation. Expiration is the cleaner example.
|
||||
- Make the "hint, not guarantee" point explicitly. The chapter is not
|
||||
asserting that expired events are gone — it's giving callers the
|
||||
primitive they need to act on the hint.
|
||||
- Strict-`<` deserves a sentence. An event whose expiration equals
|
||||
`now` is still valid; it becomes expired the moment `now` advances.
|
||||
- The `get_expiration` / `is_expired` split mirrors the pattern from
|
||||
`get_pow` in the previous chapter: a free function that takes tags,
|
||||
plus method sugar on the event types. Call this out briefly — the
|
||||
consistency is the point.
|
||||
- Don't over-design `is_expired`. The `SystemTime::now()` fallback to
|
||||
`0` on a clock error means an unreachable case maps to "never
|
||||
expired," which is the safer default than "always expired".
|
||||
- Keep the constructor section short. It's one line of code; the
|
||||
narrative justifies adding it at all.
|
||||
|
||||
## Design Decisions
|
||||
|
||||
- **No `Timestamp` newtype.** `created_at` is `u64` everywhere; this
|
||||
stays consistent.
|
||||
- **No clock injection.** The testable `is_expired_at(now)` is enough;
|
||||
a `Clock` trait would be ceremony in a library this size.
|
||||
- **Strict `<` not `<=`.** Matches rust-nostr, welshman, applesauce.
|
||||
"Expires at T" reads as "invalid after T".
|
||||
- **Free functions take `&Tags` only.** The predicate doesn't need the
|
||||
event id or pubkey. Keeping it at the `Tags` level makes it reusable
|
||||
for any caller holding a tag slice (e.g., a relay streaming raw JSON
|
||||
can check without reconstructing an `Event`).
|
||||
- **No background sweeper, no storage integration.** That belongs in
|
||||
a later storage chapter, if at all. This chapter is the parse layer.
|
||||
- **Tag constructor in `tags.rs`.** Discoverable next to
|
||||
`Tag::new`; avoids an import from `expiration` just to build a tag.
|
||||
- **No `set_expiration(ts)` helper on event builders.** Callers can
|
||||
write `.tags(tags.add_tag(Tag::expiration(ts)))` or the equivalent;
|
||||
the builder surface is already chainable enough.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Whether to document the interaction with NIP-09 (deletion requests)
|
||||
in this chapter. Keep it out — deletion is its own chapter's problem,
|
||||
and entangling them here dilutes the focus.
|
||||
- Whether to add a convenience `is_expired_now` on `Tags` directly.
|
||||
Skip it; the free function is already named clearly, and adding
|
||||
methods on `Tags` blurs the "tags are a generic container" line.
|
||||
Reference in New Issue
Block a user