docs: catalog INostrClient relay-client extensions so they're discoverable

The one-shot/high-level relay ops (fetchAll, fetchFirst, fetchAllPages,
publishAndConfirm, count, negentropy sync/reconcile, …) are INostrClient
extension functions spread across ~8 files with no index, so they don't surface
under "usages of NostrClient" or in completion — easy to miss and re-implement
(as just happened with a bespoke fetchRaw duplicating fetchAll).

- Add accessories/README.md cataloging each public extension with a one-line
  "use when".
- CLAUDE.md (Feature Workflow): point at that package/README before hand-rolling
  a subscribe/REQ/publish loop.
- relay-client skill: add a Related note steering headless/one-shot callers to
  the accessories instead of Subscribable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JgL1WTV4Hkp2uuXcUHCHGt
This commit is contained in:
Claude
2026-07-08 13:25:13 +00:00
parent 0af65b4296
commit c57681b3e9
3 changed files with 78 additions and 0 deletions
+11
View File
@@ -160,6 +160,17 @@ Summarize the survey in your plan: for each component, note whether it's
reused as-is, extracted from `amethyst/` to `commons/`, genuinely new
(platform-specific only), or a duplicate of an existing pattern to avoid.
**Relay client ops already exist — don't hand-roll subscribe/REQ/publish loops.**
One-shot and high-level relay operations (fetch a set, fetch one, page past the
relay cap, publish-and-confirm, NIP-45 count, NIP-77 sync/reconcile) are
`INostrClient` **extension functions** in
`quartz/…/nip01Core/relay/client/accessories/` (+ `…/reqs/` for the flow/subscribe
helpers). Because they're extensions, they don't surface under "usages of
`NostrClient`" or in completion — grep that package (or read its `README.md`, which
catalogs them) before writing a new subscription/collect loop. Reuse `fetchAll`,
`fetchFirst`, `fetchAllPages`, `publishAndConfirm`, `count`, `negentropyReconcile`,
etc. instead of re-implementing them.
**Share vs keep platform-native:**
- **Share** → `quartz/commonMain/` (business logic, data models, protocol) and
+6
View File
@@ -123,6 +123,12 @@ Each subscription tracks "End of Stored Events" per relay. The eose manager in `
## Related
- **Headless / one-shot client ops** (CLI, geode, tests, non-compose code): don't go
through `Subscribable` — use the `INostrClient` extension functions in
`quartz/…/nip01Core/relay/client/accessories/` (`fetchAll`, `fetchFirst`,
`fetchAllPages`, `publishAndConfirm`, `count`, `negentropyReconcile`/`negentropySync`,
…). They're extensions, so they don't show up under "usages of `NostrClient`" — see
that package's `README.md` for the catalog before writing a raw subscribe/collect loop.
- `nostr-expert/references/tag-patterns.md` — how tags inform what a filter needs to look for.
- `kotlin-coroutines/references/relay-patterns.md` — relay pool internals (sibling layer beneath assemblers).
- `feed-patterns` skill — feeds compose several Subscribables (content + metadata + reactions).
@@ -0,0 +1,61 @@
# `INostrClient` accessories
One-shot / high-level relay operations, written as **extension functions** on
`INostrClient`. They live here (and in `../reqs/`) rather than on the client class,
so they don't show up under "usages of `NostrClient`" or in method completion — you
only find them by knowing this package exists.
**Before writing a new subscribe / REQ / publish loop, look here first.** Most of what
a caller needs (fetch a set, fetch one, page past the relay cap, publish-and-confirm,
count, negentropy sync/reconcile) already exists.
Import as `com.vitorpamplona.quartz.nip01Core.relay.client.accessories.<name>` (or
`...client.reqs.<name>` for the flow/subscribe helpers).
## One-shot reads (subscribe → collect → return)
| Function | File | Use when |
| --- | --- | --- |
| `fetchAll(relay, filter, timeoutMs)` | `NostrClientFetchAllExt` | Get every event matching a filter in one REQ, deduped by id, until EOSE or timeout. **No verify, no store** — just the events. |
| `fetchFirst(relay, filter, timeoutMs)` | `NostrClientFetchFirstExt` | Get the first matching event and stop (returns `null` on none/timeout). |
| `fetchAllPages(relay, filters, timeoutMs)` | `NostrClientFetchAllPagesExt` | Fully retrieve a result set larger than the relay's per-REQ cap (strfry `limit`, ~500) by walking a `created_at` cursor. Bound it with the filter's `limit`. |
| `fetchAllPagesFromPool(filters, ...)` | `NostrClientFetchAllPagesPoolExt` | Same paging, across several relays at once, deduped across them. |
## Streaming (`Flow`)
| Function | File | Use when |
| --- | --- | --- |
| `fetchAsFlow(relay, filter)` | `../reqs/NostrClientFetchAsFlowExt` | Emit the accumulating list on each arrival; completes on EOSE. One-shot query as a flow. |
| `subscribeAsFlow(relay, filter)` | `../reqs/NostrClientSubscribeAsFlowExt` | Live subscription as a flow (stays open past EOSE; re-sends the REQ on reconnect). |
| `subscribe(subId, filters, listener)` | `../reqs/StaticSubscription`, `DynamicSubscription` | Raw live subscription with a `SubscriptionListener`. The lowest-level primitive the above build on. |
## Publish
| Function | File | Use when |
| --- | --- | --- |
| `publishAndConfirm(event, relays, timeout)` | `NostrClientPublishExt` | Send an EVENT and wait for `OK`; returns whether any relay accepted it. |
| `publishAndConfirmDetailed(event, relays, timeout)` | `NostrClientPublishExt` | Same, but returns the per-relay accepted/rejected map. |
## Count (NIP-45)
| Function | File | Use when |
| --- | --- | --- |
| `count(relay, filter, timeoutMs)` | `NostrClientCountExt` | NIP-45 `COUNT` against one relay (`null` on timeout / no support). |
| `countMerged(relays, filter, ...)` | `NostrClientCountExt` | Merged count across relays. |
## Negentropy (NIP-77)
| Function | File | Use when |
| --- | --- | --- |
| `negentropySync(relay, filter, ...)` | `NostrClientNegentropySyncExt` | Download everything a relay holds for a filter, diffing against `localEntries` and by-id downloading only the diff. Throws `NegentropySyncException` if the relay can't reconcile (no fallback). |
| `negentropySyncOrFetch(relay, filter, ...)` | `NostrClientNegentropySyncExt` | Same, but transparently falls back to `fetchAllPages` when the relay can't reconcile. The "just get the events" combinator. |
| `negentropySyncEvents` / `negentropySyncOrFetchEvents` | `NostrClientNegentropySyncEventsExt` | The two above as an O(1)-memory `Flow<Event>`. |
| `negentropyReconcile(relay, filter, localEntries, onNeedIds, onHaveIds)` | `NostrClientNegentropySyncExt` | **Pure diff, no I/O** — streams the two directions (`need` = relay has & we lack; `have` = we have & relay lacks) to callbacks. Compose your own download/upload on top. |
| `negentropyReconcileIds(relay, filter, localEntries)` | `NostrClientNegentropySyncExt` | Same diff, materialized into `needIds` / `haveIds` lists (small sets only). |
`fetchByIds`, `reconcileStreaming`, `syncPipeline` in `NostrClientNegentropySyncExt`
are `internal` implementation details — not part of the public surface.
---
_Keep this table in sync when you add a public `INostrClient` extension here._