diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 2a019ff766..0d3293a869 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -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 diff --git a/.claude/skills/relay-client/SKILL.md b/.claude/skills/relay-client/SKILL.md index 0bdea82309..c1019cde8f 100644 --- a/.claude/skills/relay-client/SKILL.md +++ b/.claude/skills/relay-client/SKILL.md @@ -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). diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/README.md b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/README.md new file mode 100644 index 0000000000..dbf9fbcf63 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/README.md @@ -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.` (or +`...client.reqs.` 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`. | +| `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._