fix(events): publish bookmarks with monotonic created_at (#122) #126

Merged
padreug merged 2 commits from fix/monotonic-created-at into dev 2026-06-18 12:03:59 +00:00
Owner

Closes the gap from the nostr-patterns review — and the same root cause found while debugging the live ticket count. Fixes #122.

Problem

Relays only push a replaceable-event update to open subscriptions when its created_at is strictly newer than the held version. created_at is second-resolution, so useBookmarks' Math.floor(Date.now()/1000) lets two rapid toggles collide in the same second — the second is treated as not-newer and never reaches live subscribers (only a reload surfaces it).

Fix

  • Add monotonicCreatedAt(lastCreatedAt, now?) = max(now, last+1) in src/lib/nostr/timestamp.ts — reusable for any replaceable-event publisher.
  • Use it in toggleBookmark; track lastCreatedAt as a typed field on BookmarkState (drops the (state as any) casts).

Tests

src/lib/nostr/timestamp.spec.ts: no-prior, same-second bump, wall-clock tracking, future-dated prior (clock skew), and a strictly-increasing same-second burst. Run with pnpm test.

Follow-up (not in this PR)

The aiolabs/events extension's nostr_publisher uses int(time.time()) the same way — flagged in #122 for a backend follow-up.

Merge order

Branches off chore/vitest-setup (the vitest PR) — merge that first; this PR's diff drops the vitest files once it lands.

🤖 Generated with Claude Code

Closes the gap from the nostr-patterns review — and the same root cause found while debugging the live ticket count. Fixes #122. ## Problem Relays only push a replaceable-event update to **open** subscriptions when its `created_at` is **strictly newer** than the held version. `created_at` is second-resolution, so `useBookmarks`' `Math.floor(Date.now()/1000)` lets two rapid toggles collide in the same second — the second is treated as not-newer and never reaches live subscribers (only a reload surfaces it). ## Fix - Add `monotonicCreatedAt(lastCreatedAt, now?)` = `max(now, last+1)` in `src/lib/nostr/timestamp.ts` — reusable for any replaceable-event publisher. - Use it in `toggleBookmark`; track `lastCreatedAt` as a typed field on `BookmarkState` (drops the `(state as any)` casts). ## Tests `src/lib/nostr/timestamp.spec.ts`: no-prior, same-second bump, wall-clock tracking, future-dated prior (clock skew), and a strictly-increasing same-second burst. Run with `pnpm test`. ## Follow-up (not in this PR) The `aiolabs/events` extension's `nostr_publisher` uses `int(time.time())` the same way — flagged in #122 for a backend follow-up. ## Merge order Branches off `chore/vitest-setup` (the vitest PR) — **merge that first**; this PR's diff drops the vitest files once it lands. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
No test runner existed in the repo. Add vitest (node env, *.spec.ts
discovery) with a minimal config mirroring only the `@`→src alias, plus
`test`/`test:watch` scripts and a smoke test as a known-good baseline.

Precursor for the nostr-patterns review fixes (events store coordinate
keying #121, monotonic created_at #122), which ship with unit tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Relays only push a replaceable-event update to OPEN subscriptions when
its created_at is strictly newer than the held version. created_at is
second-resolution, so useBookmarks' `Math.floor(Date.now()/1000)` lets
two rapid toggles collide in the same second — the second is treated as
not-newer and never reaches live subscribers (only a reload shows it).
This is the same root cause found while debugging the live ticket count.

- Add `monotonicCreatedAt(lastCreatedAt, now?)` = max(now, last+1), a
  reusable helper for any replaceable-event publisher.
- Use it in `toggleBookmark`; track `lastCreatedAt` as a typed field on
  BookmarkState (drops the `(state as any)` casts).

Unit tests cover no-prior, same-second bump, wall-clock tracking,
future-dated prior, and a strictly-increasing same-second burst.

The aiolabs/events extension's nostr_publisher uses int(time.time()) the
same way — flagged in #122 for a follow-up on the backend.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The "strictly-monotonic created_at per coord" section named useRSVP.ts as
canonical, but that file no longer exists. monotonicCreatedAt() in
src/lib/nostr/timestamp.ts is now the single implementation — make the
doc reference it and show both the per-coord-Map and single-field
tracking shapes. Keeps doc and code aligned per the docs discipline.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
padreug deleted branch fix/monotonic-created-at 2026-06-18 12:03:59 +00:00
Sign in to join this conversation.
No description provided.