Add expiration chapter
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
# Expiring Events
|
||||
|
||||
Not every event is meant to last. A chat message, a transient status, a
|
||||
time-limited invitation — these are events whose value decays to zero
|
||||
at a known point in the future. NIP-40 gives them a way to say so: an
|
||||
`expiration` tag carrying a Unix timestamp in seconds. After that
|
||||
timestamp, the event is considered no longer valid.
|
||||
|
||||
The tag is a **behavior tag** — orthogonal to the event's kind. A kind
|
||||
1 note with an expiration is still a note; the expiration is a request
|
||||
that relays stop serving it and clients stop displaying it once the
|
||||
time is up. The distinction matters. Nostr clients and relays already
|
||||
agreed, per kind, on what the payload *means*. The behavior tag layer
|
||||
lets authors add a separate instruction about what the network should
|
||||
*do* with the event, without reopening the kind's semantics.
|
||||
|
||||
Expiration is a cooperative hint, not a cryptographic guarantee.
|
||||
Anyone who has a copy of the event keeps that copy; the signature
|
||||
remains valid forever. What the tag buys is a shared convention:
|
||||
well-behaved relays will refuse to serve the event after the
|
||||
timestamp, well-behaved clients will hide it, and storage layers that
|
||||
care about disk space can sweep it. The library's job at this level is
|
||||
only to parse the tag and answer "is this event expired?" — deciding
|
||||
what to *do* about that answer belongs to code further up the stack.
|
||||
|
||||
## The module
|
||||
|
||||
```rust {file=coracle-lib/src/lib.rs}
|
||||
pub mod expiration;
|
||||
```
|
||||
|
||||
```rust {file=coracle-lib/src/expiration.rs}
|
||||
//! NIP-40 expiring events: reading the `expiration` tag and checking
|
||||
//! whether an event has expired.
|
||||
//!
|
||||
//! The free functions in this module operate on [`Tags`] so they can
|
||||
//! be used wherever a tag slice is in hand. Method versions on
|
||||
//! [`HashedEvent`] and [`Event`] delegate to them for a readable call
|
||||
//! site. The tag itself is built with [`Tag::expiration`] from the
|
||||
//! tags module.
|
||||
|
||||
use crate::events::{Event, HashedEvent};
|
||||
use crate::tags::Tags;
|
||||
use crate::util::now;
|
||||
```
|
||||
|
||||
## Reading the tag
|
||||
|
||||
The shape on the wire is `["expiration", "<unix_seconds>"]`. Parsing it
|
||||
means finding the first tag with that name and interpreting its value
|
||||
as a `u64`. Two ways to read this can go wrong — the tag may be absent,
|
||||
or its value may not parse as a number — and both collapse to the same
|
||||
answer: `None`. A malformed tag is indistinguishable from no tag at
|
||||
all; neither tells us when the event should be considered expired, so
|
||||
neither should produce a different downstream behavior.
|
||||
|
||||
```rust {file=coracle-lib/src/expiration.rs}
|
||||
/// Return the expiration timestamp from a tag set, if present.
|
||||
///
|
||||
/// Reads the first `expiration` tag and parses its value as a Unix
|
||||
/// timestamp in seconds. Returns `None` if the tag is missing or its
|
||||
/// value does not parse as a `u64` — treating "malformed" the same as
|
||||
/// "absent" avoids forcing every caller to distinguish a case they
|
||||
/// cannot usefully act on.
|
||||
pub fn get_expiration(tags: &Tags) -> Option<u64> {
|
||||
tags.value("expiration").and_then(|v| v.parse().ok())
|
||||
}
|
||||
```
|
||||
|
||||
## A testable predicate
|
||||
|
||||
A predicate that reads the system clock is awkward to test. The
|
||||
standard fix — take the clock as a parameter — costs nothing here
|
||||
because the caller usually has a `now` value on hand anyway (a relay
|
||||
checking a batch of incoming events picks a timestamp once and reuses
|
||||
it, a UI rendering a timeline pins "now" at render time). The
|
||||
two-function split keeps both audiences happy: callers who already
|
||||
have a timestamp pass it in, and callers who just want to check
|
||||
against wall-clock time call the shorter form that fills it in for
|
||||
them.
|
||||
|
||||
The comparison is strict: `expiration < now`. An event whose
|
||||
expiration equals the current time is still valid; it becomes expired
|
||||
the moment `now` advances past it. "Expires at T" reads naturally as
|
||||
"invalid after T," and that reading matches what every reference
|
||||
implementation does.
|
||||
|
||||
```rust {file=coracle-lib/src/expiration.rs}
|
||||
/// Test whether the tags declare an expiration that has already
|
||||
/// passed at the given `now` timestamp.
|
||||
///
|
||||
/// Returns `false` if no expiration tag is present. The comparison is
|
||||
/// strict: an event with expiration equal to `now` is *not* yet
|
||||
/// expired.
|
||||
pub fn is_expired_at(tags: &Tags, now: u64) -> bool {
|
||||
matches!(get_expiration(tags), Some(t) if t < now)
|
||||
}
|
||||
```
|
||||
|
||||
## Against the system clock
|
||||
|
||||
The wall-clock form is a one-liner over `is_expired_at`, filling in the
|
||||
timestamp from `crate::util::now`.
|
||||
|
||||
```rust {file=coracle-lib/src/expiration.rs}
|
||||
/// Test whether the tags declare an expiration that has already
|
||||
/// passed, relative to the system clock.
|
||||
///
|
||||
/// For batch checks or testable callers, prefer [`is_expired_at`] and
|
||||
/// pass an explicit timestamp.
|
||||
pub fn is_expired(tags: &Tags) -> bool {
|
||||
is_expired_at(tags, now())
|
||||
}
|
||||
```
|
||||
|
||||
## Building the tag
|
||||
|
||||
We can add a small convenience method for building this tag:
|
||||
|
||||
```rust {file=coracle-lib/src/tags.rs}
|
||||
impl Tag {
|
||||
/// Build a NIP-40 `expiration` tag with the given Unix timestamp in seconds.
|
||||
pub fn expiration(timestamp: u64) -> Tag {
|
||||
Tag::new("expiration", [timestamp.to_string()])
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Methods on event types
|
||||
|
||||
Both `HashedEvent` and `Event` carry a `Tags` field, and callers who
|
||||
have an event in hand almost always want to ask questions of its tags
|
||||
without a separate import. The method impls delegate to the free
|
||||
functions verbatim.
|
||||
|
||||
```rust {file=coracle-lib/src/expiration.rs}
|
||||
impl HashedEvent {
|
||||
/// Return the expiration timestamp of this event, if any.
|
||||
pub fn get_expiration(&self) -> Option<u64> {
|
||||
get_expiration(&self.tags)
|
||||
}
|
||||
|
||||
/// Whether this event has expired relative to the given timestamp.
|
||||
pub fn is_expired_at(&self, now: u64) -> bool {
|
||||
is_expired_at(&self.tags, now)
|
||||
}
|
||||
|
||||
/// Whether this event has expired relative to the system clock.
|
||||
pub fn is_expired(&self) -> bool {
|
||||
is_expired(&self.tags)
|
||||
}
|
||||
}
|
||||
|
||||
impl Event {
|
||||
/// Return the expiration timestamp of this event, if any.
|
||||
pub fn get_expiration(&self) -> Option<u64> {
|
||||
get_expiration(&self.tags)
|
||||
}
|
||||
|
||||
/// Whether this event has expired relative to the given timestamp.
|
||||
pub fn is_expired_at(&self, now: u64) -> bool {
|
||||
is_expired_at(&self.tags, now)
|
||||
}
|
||||
|
||||
/// Whether this event has expired relative to the system clock.
|
||||
pub fn is_expired(&self) -> bool {
|
||||
is_expired(&self.tags)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Usage patterns
|
||||
|
||||
**Building an expiring event.** Attach the tag at template
|
||||
construction:
|
||||
|
||||
```rust
|
||||
let event = EventContent::new()
|
||||
.content("this link is good for an hour")
|
||||
.tags(Tags::new().add_tag(Tag::expiration(now + 3600)))
|
||||
.kind(1)
|
||||
.stamp(now)
|
||||
.own(pubkey);
|
||||
```
|
||||
|
||||
**Filtering out expired events.** Relays and clients both might need to
|
||||
remove expired events from a collection. Using `is_expired_at` avoids the
|
||||
need to re-generate the current timestamp repeatedly:
|
||||
|
||||
```rust
|
||||
let now = util::now();
|
||||
incoming.retain(|e| !e.is_expired_at(now));
|
||||
```
|
||||
|
||||
## What's next
|
||||
|
||||
Expiration is one kind of behavior tag. The next chapter introduces
|
||||
another — NIP-70's `protected` tag, which tells relays not to accept
|
||||
an event from anyone but its author.
|
||||
Reference in New Issue
Block a user