e9e994fff4
- isExpired (#10): NIP-40 says an event is expired *once* `expiration` is reached, and the SQL trigger uses `<= unixepoch()`. The Kotlin pre-check used `<` (strict) so an event with `expiration == now` passed the Kotlin check then failed in the trigger. Both layers now use `<=`. Also applies to `isExpirationBefore` for consistency. - transaction extension (#7): if the body throws *and* ROLLBACK also throws, we now attach the rollback failure as a suppressed exception instead of letting it mask the original cause. COMMIT is moved outside the catch so a commit failure doesn't trigger a second ROLLBACK on already-finalized transaction state. - SeedModule.hasher (#8): the cache field is now a `kotlin.concurrent. atomics.AtomicReference` (matches the pattern used in BleChunkAssembler and BasicRelayClient) so the hasher publication is visible across threads. The race itself is benign — the seed is stable, so two concurrent computations produce identical hashers — but the prior plain `var` had no visibility guarantee. - delete(Filter()) (#12): documents the intentional asymmetry — `query` on an empty filter returns everything, but `delete` on an empty filter is a no-op (safe-by-default). New test pins the contract. Tests: - testInsertingEventExpiringExactlyNow: events with `expiration == now` are rejected by both Kotlin and the trigger. - testTransactionRollsBackOnException: a user transaction whose body throws leaves the DB unchanged and still accepts new writes. - testDeleteWithEmptyFilterIsSafe: empty-filter delete is a no-op. https://claude.ai/code/session_01X163Nr31vGkvAXoJ3JgMov