docs(commons): correct what blocks the event cache from commonMain

The earlier note said the NIP-95 `java.io.File` spill was the only thing
keeping the cache out of `commonMain`. That was wrong. The binding blocker is
that the cache's own storage — `LargeSoftCache`, holding `users`, `notes` and
`addressables` — is itself `jvmAndroid`, as are both `*ListMatchingFilter`
observables, `MintDirectoryIndex` and `NwcPaymentTracker`. `EventCache` cannot
go without them.

The audit also found most of it is already answered in the tree:
`commons.util.WeakReference` is an expect/actual that already anticipates iOS,
quartz's commonMain ships `ConcurrentMap`/`ConcurrentSet`/`LargeCache`, and
`LargeSoftCache`'s skip-list ordering is not load-bearing — its surface has no
ordered read, so the skip list is there for concurrency alone.

That leaves two pieces of real work: an okio or `expect` blob sink, and
getting `java.util.SortedSet` out of `EventCache.filter`'s signature, which
has no common equivalent and ripples into `CacheSearch`, both observables and
the parity test.

Recorded with a bottom-up order, and noted that none of it is on part B's
critical path — Desktop is JVM.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KkULS5SVq4GHDdoCzajKi8
This commit is contained in:
Claude
2026-09-21 02:23:40 +00:00
parent 5165dc3110
commit 8f38d4a0b9
2 changed files with 75 additions and 4 deletions
+12 -4
View File
@@ -253,10 +253,18 @@ These are intentionally *documented*, not silently tolerated. Fix opportunistica
namespaces expected to grow; do not fold them into `util` just for size.
- **`onchain`** (on-chain zap splitting) is `quartz`-adjacent but un-numbered;
leave readable unless a clear NIP number lands.
- **`model/cache/LocalCache` is `jvmAndroid`, not `commonMain`**, because it
spills NIP-95 blobs through `java.io.File`. Promoting it needs an okio (or
`expect`) sink behind `LocalCacheHost.nip95BlobDir`; nothing else in the
move-group blocks iOS.
- **`model/cache/EventCache` is `jvmAndroid`, not `commonMain`.** The NIP-95
`java.io.File` spill is the obvious blocker but not the binding one: the
cache's own storage, `LargeSoftCache`, is `jvmAndroid`
(`WeakReference` + `ConcurrentSkipListMap`), as are the two
`*ListMatchingFilter` observables, `MintDirectoryIndex` and
`NwcPaymentTracker`. Promoting the cache means promoting those first.
Most of it has answers already in the tree — `commons.util.WeakReference`
is an expect/actual, and quartz's `commonMain` has `ConcurrentMap`,
`ConcurrentSet` and `LargeCache` — so the genuinely new work is an okio (or
`expect`) blob sink and getting `java.util.SortedSet` out of
`EventCache.filter`'s signature, which has no common equivalent. See the
audit in `commons/plans/2026-08-30-commons-migration-sweep.md`.
---
@@ -1000,3 +1000,66 @@ GC-eligible on Desktop, as they already are on Android.
**The test suite is not sufficient for this step.** 62 tests cover the consume
path well, but nothing covers a stale render. Run the desktop app against a
real relay before calling part B done.
### Can `EventCache` go to `commonMain`? — audit (2026-09-21)
Short answer: yes in principle, but it is a **port, not a move**, and the
earlier note in this file ("needs an okio sink … nothing else in the
move-group blocks iOS") was wrong. The file sink is the visible blocker; the
binding one is that the cache's own storage is `jvmAndroid`.
`commons` really does build `iosArm64`/`iosSimulatorArm64`, so this is a real
constraint rather than a hypothetical one.
**What must move with it** (`EventCache` cannot go alone):
| Dependency | Why it is `jvmAndroid` |
|---|---|
| `LargeSoftCache` — `users`, `notes`, `addressables` | `java.lang.ref.WeakReference`, `ConcurrentSkipListMap`, `BiConsumer` |
| `EventListMatchingFilter`, `NoteListMatchingFilter` | `SortedSet`, `ConcurrentHashMap`, `ConcurrentSkipListSet` |
| `MintDirectoryIndex` | `ConcurrentHashMap` |
| `NwcPaymentTracker` | `ConcurrentHashMap`, `AtomicInteger` |
| `DvmHeartbeatRegistry`, `OnchainZapResolver` | `ConcurrentHashMap` |
| `LocalCacheHost` | `java.io.File` |
**Most of it already has an answer in the tree:**
- `commons.util.WeakReference` is already an `expect`/`actual`, and its KDoc
already anticipates `kotlin.native.ref.WeakReference` for iOS.
- quartz `commonMain` already ships `ConcurrentMap`, `ConcurrentSet`,
`ConcurrentHashCache` and `LargeCache` — so every `ConcurrentHashMap` /
`ConcurrentSkipListSet` above is a swap, not a design problem.
- `LargeSoftCache`'s skip-list ordering is **not** load-bearing: its whole
surface is `get`/`put`/`remove`/`size`/`containsKey`/`keys`/`forEach`, with
no ordered read. `ConcurrentSkipListMap` is there for lock-free concurrency,
so quartz's `ConcurrentMap` should substitute — worth confirming nothing
iterates expecting order before relying on that.
- `AtomicInteger` → `kotlin.concurrent.atomics`; `BiConsumer` → a function
type; the `dateFormatter` call is one log line and can go.
- `androidx.collection.LruCache` in `AntiSpamFilter` is already KMP —
`commonMain` uses it in `blurhash/CosineCache` and `relays/EOSE`.
**The two pieces of genuinely new work:**
1. **NIP-95 blob sink.** `EventCache.consume(FileStorageEvent)` writes through
`java.io.File`/`FileOutputStream`. Needs okio or an `expect` sink behind
`LocalCacheHost.nip95BlobDir`.
2. **`java.util.SortedSet` in the public API.** `EventCache.filter(Filter)`
returns one, and both `*ListMatchingFilter` observables take
`(Filter) -> SortedSet<Note>`. Kotlin has no common `SortedSet`, so this is
a signature change rippling into `CacheSearch`, both observables and
`LocalCacheSearchParityTest`.
**Recommended order** (bottom-up; each step is independently shippable):
1. `LargeSoftCache` → `commonMain` on the existing `WeakReference` expect plus
quartz's `ConcurrentMap`. This is the keystone — everything else is behind it.
2. The `ConcurrentHashMap`/`ConcurrentSkipListSet` holders, mechanically.
3. `SortedSet` out of `EventCache.filter`'s signature.
4. The NIP-95 sink.
5. `EventCache` + `LocalCacheHost` themselves.
**Not on part B's critical path.** Desktop is JVM, so retiring
`DesktopLocalCache` needs none of this. The payoff here is an iOS front end
later, not anything queued now.