Files
bitvid/js/nostr/replaceableCreatedAt.js
T
thePR0M3TH3ANandClaude Opus 4.8 2dc7397535 [hashtags] Monotonic created_at so a rapid second edit doesn't false-error (#57)
Adding a second hashtag interest/disinterest quickly showed "didn't work" yet the
change persisted after a refresh. Cause: the replaceable preferences event used
created_at = floor(now/1000), so two edits in the same wall-clock second produced
the SAME created_at. Relays reject the second as "not newer" (client shows an
error) while relays that break created_at ties by id still store it (so it appears
after a refresh) — an inconsistent false-negative.

Fix: nextReplaceableCreatedAt(lastCreatedAt) (js/nostr/replaceableCreatedAt.js) —
always advance to max(now, lastCreatedAt + 1), so each publish is strictly newer
than the last and every relay accepts it unambiguously. Reusable for the other
replaceable lists (subscriptions/blocks) that have the same latent collision.

Tests: tests/replaceable-created-at.test.mjs (5 — same-second bump, clock-ahead,
no-prior, rapid successive strictly-increasing, floors fractional). Lint + build green.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-01 19:41:00 -04:00

22 lines
1.2 KiB
JavaScript

// Monotonic created_at for replaceable events (NIP-01: kind 30000 subscriptions,
// 30015/30078 preference notes, 10000 blocks, etc). Relays keep only the newest
// event per (kind, pubkey, d-tag) and reject/ignore an update whose created_at is
// not STRICTLY greater than the stored one. Two quick edits within the same
// wall-clock second otherwise collide: the second is rejected as "not newer" (the
// client shows an error) yet lands on relays that break same-created_at ties by id
// (so it "failed but works after refresh"). Always advance past the last-published
// created_at to remove the collision.
//
// @param {number} lastCreatedAt - created_at of the last event we published (0 if none).
// @param {number} [nowSeconds] - current unix seconds (injectable for tests).
// @returns {number} A created_at strictly greater than lastCreatedAt.
export function nextReplaceableCreatedAt(lastCreatedAt, nowSeconds) {
const now = Number.isFinite(nowSeconds)
? Math.floor(nowSeconds)
: Math.floor(Date.now() / 1000);
const last = Number.isFinite(lastCreatedAt) ? Math.floor(lastCreatedAt) : 0;
return Math.max(now, last + 1);
}
export default nextReplaceableCreatedAt;