Commit Graph

12644 Commits

Author SHA1 Message Date
Claude 897e036b3a fix(nests): only gate room re-entry through lobby when card reads closed
NestFeedCard previously routed every tap on a joinable MeetingSpaceEvent
through NestLobbyScreen. The lobby exists to keep an accidental tap on
a stale room from booting the audio pipeline (and the host-side
kind-30312 republish path) — once the badge says the room is live with
fresh presence, that gating is just extra friction. Mirror the badge's
liveness heuristic in the click handler and launch NestActivity
directly for cards rendering as LIVE / PLANNED / PRIVATE; only EndedFlag
cards (status = ENDED, status = LIVE with stale presence, or unknown
status) keep flowing through the read-only lobby.
2026-04-30 22:25:39 +00:00
Vitor Pamplona 3dceb194dc Removes the disappearing bar from the TwoPane messages 2026-04-30 17:59:28 -04:00
Vitor Pamplona fa8892b6a4 Merge pull request #2673 from vitorpamplona/claude/fix-chatroom-list-reuse-Wkz10
Fix double padding in two-pane chat layout
2026-04-30 17:46:45 -04:00
Vitor Pamplona 4c4a28dbe9 Removing unnecessary label 2026-04-30 17:42:44 -04:00
Vitor Pamplona 4003b735dd Adds an isNewThread to the TextNote 2026-04-30 17:40:16 -04:00
Vitor Pamplona 2f9c6bddeb simplifying the readme 2026-04-30 17:37:29 -04:00
Claude 089261420c refactor(chats): apply scaffold padding to two-pane left pane
Move the workaround out of ChatroomList: use the DisappearingScaffold's
padding lambda parameter (previously unused) to position the left pane
below the top bar and above the bottom bar. Reset
LocalDisappearingScaffoldPadding to zero only at that boundary so the
inner LazyColumn inside ChatroomListFeedView doesn't double-count the
bar inset and produce a gap below the TabRow.

The right pane keeps systemBarsPadding because ChatroomView /
PublicChatChannelView bring their own DisappearingScaffold; layering the
outer padding there would stack the chat header below the outer search
bar.

ChatroomList stays a flat Column with no scaffold awareness.
2026-04-30 21:32:10 +00:00
Vitor Pamplona 479f14cbd4 additional info 2026-04-30 17:28:20 -04:00
Vitor Pamplona acb36adfa3 Merge branch 'main' of https://github.com/vitorpamplona/amethyst
# Conflicts:
#	quartz/README.md
2026-04-30 17:26:28 -04:00
Vitor Pamplona 43dc925b74 Updates readme to look more like a guide 2026-04-30 17:22:58 -04:00
Claude 2db8dc99a8 fix(chats): drop scaffold top inset for inner feed in two-pane
In MessagesSinglePane the MessagesTabHeader sits inside the
DisappearingScaffold's topBar slot, so LocalDisappearingScaffoldPadding's
top component already covers the bar + tabs and the inner LazyColumn's
contentPadding lines the first row up flush below the tabs.

In MessagesTwoPane the tabs are part of the column content rather than
the scaffold's top slot. The scaffold still publishes its top-app-bar
height as the top inset, so rememberFeedContentPadding inside the feed
double-counts it and renders a top-app-bar-tall gap between the TabRow
and the first chatroom row.

Override LocalDisappearingScaffoldPadding around MessagesPager to zero
the top component while preserving start/end/bottom, so the feed still
clears the bottom bar.
2026-04-30 21:15:54 +00:00
Vitor Pamplona 4c9ccbadbd Merge pull request #2672 from vitorpamplona/claude/quartz-docs-update
Refactor relay integration docs: EventCollector pattern and lifecycle management
2026-04-30 16:57:10 -04:00
Vitor Pamplona 6aa24c5ef2 Merge pull request #2671 from vitorpamplona/claude/fix-nestlobby-layout-ofVC0
Refactor NestLobbyScreen layout: move header outside LazyColumn
2026-04-30 16:56:35 -04:00
Claude 61bed279fc fix: lift NestLobby header out of the chat LazyColumn
The room metadata (RoomHeader) and live listener count were items
inside the reverse-layout LazyColumn, so they scrolled with the chat
and forced the chat to share its viewport. Move them to a regular
Column above the LazyColumn so the chat list owns most of the screen
and the header sits as a fixed row on top.

Drop the redundant "Recent chat" label that was only there to
separate the in-list header from messages.
2026-04-30 20:50:53 +00:00
Claude c4a38bb899 docs(quartz): align tutorials with the layered patterns demoApp uses
The README's "Wiring relays into the store" tutorial used a
per-subscription SubscriptionListener.onEvent callback to feed the
store. EventCollector landed exactly to remove that boilerplate, so
update the example to register one connection-level collector at app
init and stop wiring listeners per subscribe() call.

The "Building a reactive feed UI" example showed project() ->
stateIn(WhileSubscribed) but never opened the relay subscription.
Bind it to the projection's collection lifecycle with .onStart {
client.subscribe } / .onCompletion { client.unsubscribe }, so the
relay subscription's lifetime equals the UI collector's. Also
demonstrate ProjectionState.filterItems for narrowing without a
re-query.

Drop the redundant client.connect() call from the wiring example
and add an explicit note (in both README and CLIENT) that
NostrClient connects on demand from subscribe()/publish() — connect()
is only useful after disconnect().

Other small fixes:
  - client.send(...) -> client.publish(...) (the actual API name).
  - Rename the variable from `observable` to `db` to match the demoApp
    code and the conceptual "this is the local database" framing.
  - Wrap the publish path in a viewModelScope.launch so the snippet
    is copy-pasteable into a ViewModel.
2026-04-30 20:50:00 +00:00
Vitor Pamplona 276b25a448 Removes the stop broadcast button, user must leave the stage to stop 2026-04-30 16:38:47 -04:00
Vitor Pamplona 2a0ea98df2 Merge pull request #2670 from vitorpamplona/claude/update-nestlobby-chat-layout-JimqW
Claude/update nestlobby chat layout jimq w
2026-04-30 16:22:38 -04:00
Claude 8f4474c8a7 fix(nests): make Open lobby action a prominent filled button
A TextButton in the AppBar actions slot read as too subtle and small
next to the title. Switch to a filled Button so the primary CTA is
unmistakable. The actions Row already centers children vertically;
trim the trailing modifier padding so the button sits flush with
the bar's edge.
2026-04-30 20:13:28 +00:00
Claude 8f7c0eac47 refactor(nests): lobby chat reuses NestFullScreen list shape
Replace the static Column-based CachedChatList with the same
LazyColumn(reverseLayout = true) the in-room screen uses
(NestChatPanel.NestChatMessageList). Items are keyed by note id,
and the room metadata header / listener count / "Recent chat"
label are stacked above the oldest message so the user can scroll
up through history into the room info.

Wire onSendNewMessage to animateScrollToItem(0) so a freshly sent
message snaps the list back to the newest entry — same behaviour
the in-room composer has.
2026-04-30 20:12:45 +00:00
Vitor Pamplona c249765c51 Merge pull request #2669 from vitorpamplona/claude/update-nestlobby-chat-layout-JimqW
Add chat composer to nest lobby screen
2026-04-30 16:04:02 -04:00
Vitor Pamplona 12ed2af028 Moves java.util.SortedSet to an arrayList. 2026-04-30 15:57:26 -04:00
Claude 02d34ed74f feat: active chat composer on NestLobby with Open action in top bar
- Replace bottom Open Room button with a TextButton "Open" action in
  the top app bar, freeing the screen bottom for the chat composer.
- Mount NestEditFieldRow + NestNewMessageViewModel on the lobby so
  users can post kind-1311 messages without joining the audio plane;
  in-room composer features (mentions, file upload, drafts, emojis)
  carry over.
- Switch the Scaffold to contentWindowInsets=0 and use imePadding +
  navigation-bar inset on the composer so the chat list draws behind
  the 3-button system navigation while the composer stays reachable
  above it (and rises above the IME when the keyboard opens).
- Drop the now-unused OpenNestRoomButton helper and its string.
2026-04-30 19:53:41 +00:00
Vitor Pamplona 728a396ff2 Merge pull request #2667 from vitorpamplona/claude/fix-mute-button-height-6Vfip
Refactor NestActionBar UI layout and state handling
2026-04-30 15:24:13 -04:00
Claude 951f6c37f0 refactor(nests): extract action bar affordances and refresh KDoc
NestActionBar.kt had grown to ~470 lines with most of the volume in
OnStageControls, where four broadcast-state branches each inlined a
56dp filled icon button, a tonal toggle button, and an outlined
"Leave the Stage" button. The result was hard to scan for state
coverage, and the top-level KDoc still described the pre-cleanup
layout.

Changes:

* Rewrote the top-level KDoc as a state-by-state table matching the
  current UI.
* Extracted reusable affordances:
  ConnectButton, StatusChip, LeaveStageButton, TalkButton,
  StopBroadcastButton, MicMuteToggle, HandRaiseToggle,
  LeaveRoomButton.
* Merged the duplicate Connect button branches in StartCluster
  (Idle / Closed / Failed all share one). The failure reason already
  appears in the status strip above.
* Split OnStageControls: kept the dispatcher thin and moved the
  permission state + Settings deep-link into OnStageIdleControls,
  which is the only state that needs them.
* Replaced the inline qualified Settings Intent with a
  Context.openAppSettings() helper, plus a Context.hasMicPermission()
  helper for the two RECORD_AUDIO checks.
* Pulled the status-strip text resolution into a small
  NestUiState.statusStripText() so ActionBarStatusStrip is one Text.

No behavior changes — just structure, naming, and docs.
2026-04-30 19:22:52 +00:00
Claude 7be8ac5106 fix(nests): drop redundant Live chip while broadcasting
The red Mic-on FilledIconButton already conveys "you are live, tap
to stop" — the AssistChip labelled "Live" beside it was duplicate
visual noise. Removed the chip and its now-orphan string. Mute toggle
+ red Mic + Leave the Stage are sufficient to express the broadcasting
state.
2026-04-30 19:14:17 +00:00
Claude 640e7904e4 fix(nests): tighten action bar state coverage
Several gaps in the action bar were causing dead-end UI states or
making non-actionable controls visible:

* Hand-raise was rendered while disconnected / connecting / failed.
  Raising can't be delivered to the room until we're Connected, so
  hide it until the connection state agrees. Threaded `isConnected`
  into `EndCluster` from the root composable.
* Audience listen-mute toggle (Volume Up/Off) removed. System volume
  keys cover local volume; the on-screen control was redundant and
  shared the same speaker glyph as the broadcasting mic-mute toggle,
  which made the two affordances easy to confuse.
* On-stage user without `canBroadcast` had no way to step down without
  leaving the whole room. Show a "Leave the Stage" outlined button
  in that branch so the speaker slot can be released.
* Broadcast `Connecting` and `Failed` substates also had no
  step-down affordance — forcing the user to either retry the mic or
  leave the room. Added "Leave the Stage" to both. `stopBroadcast()`
  cancels the in-flight `speakerConnectJob` (NestViewModel
  `teardownBroadcast`), so cancelling mid-handshake is safe.
* `BroadcastUiState.Broadcasting.muteError` was tracked in state but
  never surfaced. Route it through `ActionBarStatusStrip`.
* Permission denial pill said "Open settings"; renamed to
  "No permissions" so the label communicates the *state* (the tap
  still deep-links to the system settings page).
2026-04-30 19:02:25 +00:00
Vitor Pamplona 48523c2903 Merge pull request #2666 from vitorpamplona/claude/fix-nest-room-status-3Mmq9
Add NestLobbyScreen for read-only room preview before joining
2026-04-30 14:04:35 -04:00
Vitor Pamplona 2b4064df96 Merge pull request #2665 from vitorpamplona/claude/tor-connection-fallback-uJM2E
Add Tor connection timeout with graceful fallback to regular connection
2026-04-30 14:03:10 -04:00
Claude b86219b1e1 fix(nests): match mute toggle height with action bar buttons
Material3 expressive defaults the FilledTonalIconToggleButton container
shorter than ButtonDefaults.MinHeight, leaving the audience listen-mute
and on-stage mic-mute toggles visibly shorter than the neighboring
"Leave the Stage" / "Leave" outlined buttons. Pin the toggle height to
ButtonDefaults.MinHeight so the row aligns.
2026-04-30 17:57:45 +00:00
Claude 786864c311 feat(tor): fall back to clearnet when bootstrap is stuck
When Tor stays in Connecting for >60s, prompt the user to drop the proxy
for the rest of the session. The choice is persisted as a 1-hour window
so subsequent failures (across cold starts, network changes) silently
fall back without re-prompting; after the hour the dialog returns.

Bypass clears on user-initiated TorType changes and on network identity
changes so Tor gets a fresh attempt under new conditions.
2026-04-30 17:47:47 +00:00
Vitor Pamplona 1e4829626b Merge pull request #2664 from vitorpamplona/claude/quartz-eventstore-observer-o2f1O
Add event projection and interning for reactive event store queries
2026-04-30 13:46:40 -04:00
Claude 4e5a69da66 feat(nests): in-app lobby route to gate room re-entry
Adds Route.NestLobby — a regular Compose destination — between every
Nests entry point (feed card, channel-view card, in-feed CTA, naddr
deep links) and NestActivity. The lobby is read-only: it warms cached
chat, host info, listener count, and recent messages via the existing
NestRoomFilterAssemblerSubscription, but never opens a NestViewModel,
publishes kind-10312 presence, or launches MoQ. Only the new
OpenNestRoomButton inside the lobby launches NestActivity, so a user
coming back to peek at an old room no longer triggers the audio
pipeline (or the host-side kind-30312 republish path) by accident.

JoinNestButton now navigates to the lobby instead of launching the
activity; OpenNestRoomButton owns the actual launch and lives only
on the lobby screen.
2026-04-30 17:41:58 +00:00
Claude c873385fd6 docs(quartz): add module-level README with layer overview + tutorials
Tutorial-style README covering the full pipeline:
relay → NostrClient → ObservableEventStore → InterningEventStore →
EventStore → project() → ViewModel → Compose.

Two worked examples:
1. Wiring NostrClient subscriptions into the store via
   SubscriptionListener.onEvent.
2. Building a reactive feed UI with project().stateIn(...) and
   Compose, showing how the three reactivity layers (Loading/Loaded
   state, list membership, per-event handles) map to Compose's
   recomposition model.

Cross-links the existing CLIENT.md, RELAY.md, and store/sqlite
README. Pointers to per-class KDoc for projection internals.

https://claude.ai/code/session_01Jny85MTu1ynKgFBgysfWu5
2026-04-30 17:28:17 +00:00
Claude 4cfd56da36 perf(quartz): cheaper hot paths in EventStoreProjection
Three small wins on paths the projection runs every event arrival
or every snapshot publish:

1. Cache `address` on Slot. Previously `removeIndexes(slot)` called
   `addressOf(slot.flow.value)` on every drop — for replaceables
   that's a fresh Address allocation every time. Now the address is
   computed once at slot construction and reused on removal.

2. Single-filter snapshot fast path. The deduped sorted union via
   TreeSet was unnecessary when there's only one per-filter set —
   that set is already sorted in the slot comparator's order. The
   common UI case (one filter per projection) now skips the
   TreeSet allocation + log-n inserts entirely.

3. Comparator singleton. `slotComparator()` was allocating a fresh
   Comparator lambda on every call (ctor + every snapshot).
   Replaced with a single `Comparator<Slot<*>>` cast at the use
   site — the comparator only reads sort keys that don't depend on
   the type parameter.

Also trimmed a verbose comment block in `project()` (3 lines → 1).
All 18 projection tests + 240 store + interner tests still pass.

https://claude.ai/code/session_01Jny85MTu1ynKgFBgysfWu5
2026-04-30 16:24:15 +00:00
Vitor Pamplona 3d46dfcfe1 Merge pull request #2663 from vitorpamplona/claude/fix-nest-room-crash-KJwBQ
Support Android's hostname-aware certificate validation
2026-04-30 12:16:25 -04:00
Claude 058c56dc21 feat(quartz): re-evaluate filter membership on addressable supersession
When a replaceable / addressable update arrives in handleInsert, the
projection now re-runs Filter.match against the new event for every
filter. A v2 that no longer matches a filter (e.g. tag list changed)
is removed from that filter's set; a v2 that newly matches a filter
joins it. If no filter retains the slot afterwards, it's fully
dropped from byId / byAddress.

Closes the previous gap where v2 of an addressable kept a stale
filter membership inherited from v1. The slot's MutableStateFlow is
still updated in place — collectors of that handle still see the
content change before the membership update emits.

The cap-eviction-with-cleanup logic was extracted into a small
`admit` helper since the supersession path and the new-slot path
both need it.

For the common case (filter on `kinds + authors`, no tag/time
constraints), v2 always still matches — the new branches are no-ops
and the list reference stays stable, preserving the in-place update
guarantee. The behaviour change kicks in for tag, time-window, or
id-list filters.

New test addressableUpdateDropsSlotWhenFilterStopsMatching: v1 has
hashtag "nostr" and matches a `t = nostr` filter; v2 changes to
"bitcoin"; the projection drops the slot. 18/18 projection tests
pass.

https://claude.ai/code/session_01Jny85MTu1ynKgFBgysfWu5
2026-04-30 16:10:47 +00:00
Claude a6605e2792 fix(quic): use hostname-aware trust manager on Android
Android's RootTrustManager throws when an app has Network Security Config
domain-specific entries and the 2-arg checkServerTrusted overload is used,
crashing Nest room joins on relays covered by such config. Discover the
3-arg checkServerTrusted(chain, authType, hostname) overload via reflection
and invoke it with the SNI host; fall back to the standard 2-arg form on
plain JVM where that overload doesn't exist.
2026-04-30 16:10:00 +00:00
Claude f759f44eea docs(quartz): trim verbose KDoc on cache + observable layers
Reviews before merge:

- EventInterner: drop stale "used by Event.fromJson" reference (we
  reverted that earlier). Tighten the class doc.
- ObservableEventStore: collapse the 30+ line class KDoc + duplicated
  StoreChange description into one focused doc each.
- EventStoreProjection: trim the 60+ line class doc; reactive
  semantics live on the project() extension. Add explicit caveat
  about in-place addressable updates not re-evaluating filter
  membership.

No behaviour change. All cache + store + projection tests pass.

https://claude.ai/code/session_01Jny85MTu1ynKgFBgysfWu5
2026-04-30 16:03:50 +00:00
Vitor Pamplona d91ffb6f32 Merge pull request #2662 from vitorpamplona/claude/fix-expiration-test-sqlite-6Vv10
Fix expiration timing in ExpirationTest
2026-04-30 11:57:15 -04:00
Claude f4b94d6c6a feat(quartz): InterningEventStore now interns accepted events on insert
After the inner store accepts an event (insert succeeds), register
the caller's instance with the interner so subsequent reads of the
same id return the same `===` reference. This means an event that
was just inserted and is then queried back resolves to the original
in-memory object, not a freshly-deserialized clone.

Same in transaction { }: every accepted event is interned after the
inner transaction commits. If the body throws or inner rolls back,
nothing is interned (accepted list is discarded).

The decorator still does not *substitute* the caller's event with a
previously-cached one — same-id-different-sig collisions resolve
the new event into the cache without overwriting (intern() is
first-seen-wins). The caller keeps the instance they passed in.

All cache + store + projection tests pass.

https://claude.ai/code/session_01Jny85MTu1ynKgFBgysfWu5
2026-04-30 15:52:21 +00:00
Claude 5755ab90b2 refactor(quartz): switch project() builder from channelFlow to flow
channelFlow was overkill for a single-producer chain. The cost was
an extra Channel hop between every projection.snapshot() and the
downstream collector — pure overhead with no concurrency benefit.

flow { } needs the outer collector captured (because inside
changes.onSubscription { ... } and changes.collect { ... } the
implicit `this` is FlowCollector<StoreChange>, not
FlowCollector<ProjectionState<T>>). One `val outer = this` fixes
that.

Backpressure now flows directly: a slow collector suspends emit,
which suspends our changes.collect, which suspends the SharedFlow's
buffer drain — natural propagation, no decoupling Channel in the
middle.

17/17 projection tests pass.

https://claude.ai/code/session_01Jny85MTu1ynKgFBgysfWu5
2026-04-30 15:48:36 +00:00
Claude 8a349d0f2e fix(quartz): widen ExpirationTest insert window to dodge isExpired race
The pre-check in SQLiteEventStore.insertEvent rejects events whose
expiration is <= TimeUtils.now(). With expiration = time + 1, the
wall clock can tick past time + 1 between sampling `time` and the
insert call, intermittently failing testDeletingExpiredEvents.

https://claude.ai/code/session_01GoqXtcFnwgyjBict1EURyA
2026-04-30 15:36:05 +00:00
Claude f789fe4f53 refactor(quartz): EventStoreProjection becomes pure state machine; ObservableEventStore.project returns cold Flow
Removes scope ownership from EventStoreProjection. The class is now a
pure state machine — no Job, no AutoCloseable, no embedded
StateFlow:

  class EventStoreProjection(store, filters, nowProvider) {
      suspend fun seed()                         // run the seed query
      fun apply(change: StoreChange): Boolean    // apply one mutation
      fun snapshot(): ProjectionState.Loaded     // current view
  }

ObservableEventStore.observe(filter, scope) is renamed to
ObservableEventStore.project(filter) and now returns a cold
Flow<ProjectionState<T>>. Each collection allocates its own state
machine, runs its own seed, and unsubscribes when the collector
cancels. The flow is built using channelFlow + the state machine —
the class stays as the load-bearing primitive.

Standard caller pattern:

  val state = observable.project<TextNoteEvent>(filter)
      .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000),
               ProjectionState.Loading)

Tradeoffs:
- Cold flow → multiple collectors each re-seed. ViewModel pattern
  with stateIn shares one collection across observers (the standard
  Android approach).
- close() goes away. Cancelling the collecting scope tears down the
  projection.
- closeStopsListening test renamed to cancellingScopeStopsListening
  and now verifies the StateFlow's value freezes after its scope is
  cancelled.

17/17 projection + 240/240 store tests pass.

https://claude.ai/code/session_01Jny85MTu1ynKgFBgysfWu5
2026-04-30 15:35:30 +00:00
Claude 8517bae7a4 feat(quartz): replace items + ready with sealed ProjectionState
EventStoreProjection now exposes `state: StateFlow<ProjectionState<T>>`
instead of separate `items` + `ready` signals. ProjectionState is a
sealed interface with two cases:

  data object Loading                                  // initial state
  data class Loaded(items: List<MutableStateFlow<T>>)  // post-seed view

This lets the UI distinguish "still seeding" from "seeded but empty"
— the previous `emptyList()` initial value conflated the two. Future
extension to `Failed(throwable)` is a single case addition.

The `ready: CompletableDeferred<Unit>` signal is gone; callers use
`state.first { it is Loaded }` (or the test `awaitReady()` helper).

Test fixtures gain two private extensions on EventStoreProjection<T>:
- val items: List<MutableStateFlow<T>> — terse access to the loaded
  list (returns empty if still seeding).
- suspend awaitReady() — replaces ready.await().

awaitItems(predicate) now matches against the Loaded state. 17/17
projection + 240/240 store + interner tests pass.

https://claude.ai/code/session_01Jny85MTu1ynKgFBgysfWu5
2026-04-30 15:20:45 +00:00
Claude f23537a49c refactor(quartz): rename InternedEventStore → InterningEventStore
Past-participle "Interned" suggested an attribute of the events
flowing through; present-participle "Interning" describes what the
decorator actively does. Read clearer.

File renamed, no consumers needed updating (this commit is the only
external reference, since callers compose the decorator inline).
All 240 store + projection + interner tests still pass.

https://claude.ai/code/session_01Jny85MTu1ynKgFBgysfWu5
2026-04-30 15:02:30 +00:00
Claude ecbb7ea10c refactor(quartz): tighten projection package layout + relay flows from store
Three changes wrapped together:

1. relay moves up to IEventStore. The interface gains an abstract
   `val relay: NormalizedRelayUrl?` and SQLiteEventStore, EventStore,
   FsEventStore, InternedEventStore, and ObservableEventStore all
   override it (decorators forward inner.relay). EventStoreProjection
   loses its `relay` ctor param and uses `store.relay` for NIP-62
   vanish scoping; ObservableEventStore.observe overloads lose their
   relay arg. The relay was always a property of the store anyway —
   threading it through projection construction was redundant.

2. StoreChange inlines into ObservableEventStore. The sealed type is
   now ObservableEventStore.StoreChange (with Insert / DeleteByFilter /
   DeleteExpired as nested cases) since it's strictly a contract of
   the change stream, never used independently. StoreChange.kt deleted.

3. Package reshuffle:
     store/projection/ObservableEventStore.kt → store/ObservableEventStore.kt
     store/projection/EventStoreProjection.kt → cache/projection/EventStoreProjection.kt
     store/projection/StoreChange.kt → inlined into ObservableEventStore

   ObservableEventStore is a store decorator and lives next to
   IEventStore. EventStoreProjection is a cache primitive (alongside
   EventInterner / InternedEventStore) and lives under cache/. The
   `observe(filters, scope)` convenience moved from a method on
   ObservableEventStore to a top-level extension function in
   cache/projection/, which avoids the otherwise-circular
   store → cache/projection → store dependency.

7/7 interner + 17/17 projection + all other store tests pass.

https://claude.ai/code/session_01Jny85MTu1ynKgFBgysfWu5
2026-04-30 14:51:16 +00:00
Claude e126ca679a refactor(quartz): move interning classes into nip01Core/cache/interning
Groups EventInterner + InternedEventStore + the platform actuals + the
test under a focused interning sub-package. Leaves nip01Core/cache/ as
the namespace for cache primitives generally; future cache types
(LargeWeakCache, etc.) get sibling sub-packages.

Files moved:
  cache/EventInterner.kt          → cache/interning/EventInterner.kt
  cache/InternedEventStore.kt     → cache/interning/InternedEventStore.kt
  cache/EventInterner.jvmAndroid  → cache/interning/EventInterner.jvmAndroid
  cache/EventInterner.apple       → cache/interning/EventInterner.apple
  cache/EventInterner.linux       → cache/interning/EventInterner.linux
  cache/EventInternerTest.kt      → cache/interning/EventInternerTest.kt

No consumers to update — the previous decorator refactor already
removed the interner from all the store ctors. 7/7 interner +
240/240 store + projection tests still pass.

https://claude.ai/code/session_01Jny85MTu1ynKgFBgysfWu5
2026-04-30 14:40:15 +00:00
Vitor Pamplona 7a9896a788 Merge pull request #2661 from vitorpamplona/claude/fix-nests-speaker-test-601LZ
Fix race condition in ReconnectingNestsSpeakerTest broadcast handle access
2026-04-30 10:38:50 -04:00
Claude 3792c98ec5 refactor(quartz): rename StoreEvent → StoreChange, events flow → changes
The "Event" name was overloaded — the store-mutation type lived
right next to Nostr Event, so StoreEvent.Insert(event: Event) read
awkwardly. Renaming to StoreChange disambiguates and matches
reactive vocabulary ("store changes").

  - StoreEvent → StoreChange (file, sealed type, all references).
  - ObservableEventStore.events → ObservableEventStore.changes.
  - Internal field _events → _changes.
  - KDoc cross-references updated.

Case names (Insert, DeleteByFilter, DeleteExpired) unchanged. All
240 store + projection + interner tests still pass.

https://claude.ai/code/session_01Jny85MTu1ynKgFBgysfWu5
2026-04-30 14:38:23 +00:00
Claude dd9a530305 fix(nests): publish handles list under AtomicInteger barrier in speaker test
ScriptedSpeaker mutated `handles` AFTER incrementing `_startCount`,
so a reader that polled startCount on a different thread (the test
runs on the runBlocking thread while the broadcast pump runs on
Dispatchers.Default) could see startCount==1 via the volatile
AtomicInteger read before the subsequent list write became visible,
then fail `assertEquals(1, first.handles.size)` with a stale 0.

Also `mutableListOf` itself is not thread-safe — concurrent reads
during a write are undefined.

Reorder so the list append happens BEFORE the increment (the
volatile write now publishes the list mutation under the same
happens-before edge tests already rely on for startCount), and
swap the backing store for CopyOnWriteArrayList so concurrent
reads of `handles[0]` are well-defined.

Observed as a flake on Ubuntu CI; passes locally.
2026-04-30 14:34:53 +00:00