138 lines
5.8 KiB
Markdown
138 lines
5.8 KiB
Markdown
# 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.
|