feat(waves): publish to hive.flow first, fall back to ecency.waves - #1007
Conversation
Mirror mobile's container preference on the web write path. New waves from the standard composer resolve their container by walking ECENCY_WAVES_HOSTS ([hive.flow, ecency.waves]) and posting into the first host that has a live anchor post. hive.flow is pre-provisioned and inert today, so this resolves to ecency.waves with no behaviour change now, then switches over automatically once hive.flow starts posting. The unified feed already reads every container; only Ecency's own containers are written to (third-party deck hosts like leothreads / liketu.moments are still used as requested). - wave-hosts: add hive.flow + ECENCY_WAVES_HOSTS + isEcencyWavesHost - use-wave-create: resolve the preferred live container at submit time - use-waves-api: hive.flow waves get the dedicated wave-* permlink too - use-wave-submit: reflect the resolved container on the created item - add unit test for the host priority + predicate
📝 WalkthroughWalkthroughAdds ChangesEcency Wave Host Resolution
Sequence Diagram(s)sequenceDiagram
participant WaveForm
participant useWaveSubmit
participant useWaveCreate as mutationFn (use-wave-create)
participant ECENCY_WAVES_HOSTS
participant HiveAPI
WaveForm->>useWaveSubmit: submit wave
useWaveSubmit->>useWaveCreate: create({ host, body, ... })
useWaveCreate->>ECENCY_WAVES_HOSTS: iterate candidates [hive.flow, ecency.waves]
loop each candidate
useWaveCreate->>HiveAPI: fetchLatestPost(candidate)
HiveAPI-->>useWaveCreate: entries[] or error
end
useWaveCreate->>HiveAPI: generalApiRequest(resolvedHost, entry)
HiveAPI-->>useWaveCreate: WaveEntry
useWaveCreate-->>useWaveSubmit: { host: resolvedHost, entry }
useWaveSubmit->>useWaveSubmit: threadItem.host = created.host ?? host
useWaveSubmit-->>WaveForm: updated threadItem
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested labels
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a304a586bc
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| threadItem = created.entry; | ||
| // create() resolves the actual container (hive.flow vs ecency.waves); | ||
| // reflect that on the item rather than the requested host. | ||
| threadItem.host = created.host ?? host; |
There was a problem hiding this comment.
Preserve the original host when saving edits
When this path is saving an existing wave (editingEntry is set), create() can now return the resolved write host instead of the entry's actual host. Once hive.flow has a live anchor, editing an older ecency.waves wave still broadcasts against the original parent from editingEntry.parent_author/parent_permlink, but this assignment relabels the returned object as hive.flow; use-waves-api has already placed that same object in the entry cache via updateEntryQueryData, so the cached edited wave can end up under the wrong host. Only overwrite with created.host for new waves, or preserve editingEntry.host for edits.
Useful? React with 👍 / 👎.
- use-wave-create: don't swallow the fallback (ecency.waves) container's errors. Tolerate hive.flow (pre-provisioned/inert) failures but remember the last error and rethrow it when nothing resolves, so a real RPC outage shows the underlying error instead of the generic "no host" message. - use-wave-submit: only relabel the item's host with the resolved container for NEW waves. Edits keep their existing host (created.entry is the edited entry), so an edited ecency.waves wave isn't mislabeled hive.flow once it goes live.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/web/src/features/waves/components/wave-form/api/use-wave-create.ts`:
- Around line 56-86: Clear the stale fallback error state in use-wave-create
after a successful fetch probe. In the isEcencyWavesHost(host) loop, if
queryClient.fetchQuery succeeds but returns no entries, reset lastError so a
previous hive.flow failure is not rethrown later. Keep the retry logic around
ECENCY_WAVES_HOSTS, but only throw lastError when the final attempted host
actually failed; otherwise fall back to the no-threads-host error.
In `@apps/web/src/features/waves/hooks/use-wave-submit.ts`:
- Around line 106-108: The reply host is being taken from the composer default
instead of the parent thread, so optimistic reply state can be assigned to the
wrong host-scoped cache. Update use-wave-submit’s reply-building logic to derive
host from the parent thread or existing reply context in threadItem, and only
fall back to the composer host for new top-level waves; also review wave-form’s
threadHost passthrough so replies don’t override the parent host.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: a9c7fd60-b702-4a54-a38d-537585d19b84
📒 Files selected for processing (6)
apps/web/src/features/waves/components/wave-form/api/use-wave-create.tsapps/web/src/features/waves/components/wave-form/api/use-waves-api.tsapps/web/src/features/waves/components/wave-form/index.tsxapps/web/src/features/waves/enums/wave-hosts.tsapps/web/src/features/waves/hooks/use-wave-submit.tsapps/web/src/specs/features/waves/wave-hosts.spec.ts
| if (isEcencyWavesHost(host)) { | ||
| for (const candidate of ECENCY_WAVES_HOSTS) { | ||
| try { | ||
| const entries = await queryClient.fetchQuery( | ||
| getAccountPostsQueryOptions(candidate, ProfileFilter.posts) | ||
| ); | ||
| if (entries && entries.length > 0) { | ||
| resolvedHost = candidate; | ||
| entry = entries[0]; | ||
| break; | ||
| } | ||
| } catch (e) { | ||
| // hive.flow is pre-provisioned and may not resolve yet, so tolerate | ||
| // its failure and try the next preferred container. Remember the | ||
| // error so a genuine outage on the fallback (ecency.waves) is | ||
| // surfaced rather than masked as a missing host. | ||
| lastError = e; | ||
| } | ||
| } | ||
| } else { | ||
| const hostEntries = await queryClient.fetchQuery( | ||
| getAccountPostsQueryOptions(host, ProfileFilter.posts) | ||
| ); | ||
| entry = hostEntries?.[0]; | ||
| } | ||
|
|
||
| if (!entry) { | ||
| if (lastError) { | ||
| throw lastError; | ||
| } | ||
| throw new Error(i18next.t("decks.threads-form.no-threads-host")); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Clear stale fallback errors after a successful probe.
If hive.flow throws and a later candidate fetch succeeds but returns no entries, Line 83 still rethrows the old hive.flow error. That turns a real “no live host” case into a misleading RPC failure. Reset lastError on any successful fetch that returns an empty list, or only rethrow when the final attempted host actually failed.
Suggested fix
if (isEcencyWavesHost(host)) {
for (const candidate of ECENCY_WAVES_HOSTS) {
try {
const entries = await queryClient.fetchQuery(
getAccountPostsQueryOptions(candidate, ProfileFilter.posts)
);
if (entries && entries.length > 0) {
resolvedHost = candidate;
entry = entries[0];
break;
}
+ lastError = undefined;
} catch (e) {
// hive.flow is pre-provisioned and may not resolve yet, so tolerate
// its failure and try the next preferred container. Remember the
// error so a genuine outage on the fallback (ecency.waves) is
// surfaced rather than masked as a missing host.
lastError = e;
}
}
} else {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (isEcencyWavesHost(host)) { | |
| for (const candidate of ECENCY_WAVES_HOSTS) { | |
| try { | |
| const entries = await queryClient.fetchQuery( | |
| getAccountPostsQueryOptions(candidate, ProfileFilter.posts) | |
| ); | |
| if (entries && entries.length > 0) { | |
| resolvedHost = candidate; | |
| entry = entries[0]; | |
| break; | |
| } | |
| } catch (e) { | |
| // hive.flow is pre-provisioned and may not resolve yet, so tolerate | |
| // its failure and try the next preferred container. Remember the | |
| // error so a genuine outage on the fallback (ecency.waves) is | |
| // surfaced rather than masked as a missing host. | |
| lastError = e; | |
| } | |
| } | |
| } else { | |
| const hostEntries = await queryClient.fetchQuery( | |
| getAccountPostsQueryOptions(host, ProfileFilter.posts) | |
| ); | |
| entry = hostEntries?.[0]; | |
| } | |
| if (!entry) { | |
| if (lastError) { | |
| throw lastError; | |
| } | |
| throw new Error(i18next.t("decks.threads-form.no-threads-host")); | |
| if (isEcencyWavesHost(host)) { | |
| for (const candidate of ECENCY_WAVES_HOSTS) { | |
| try { | |
| const entries = await queryClient.fetchQuery( | |
| getAccountPostsQueryOptions(candidate, ProfileFilter.posts) | |
| ); | |
| if (entries && entries.length > 0) { | |
| resolvedHost = candidate; | |
| entry = entries[0]; | |
| break; | |
| } | |
| lastError = undefined; | |
| } catch (e) { | |
| // hive.flow is pre-provisioned and may not resolve yet, so tolerate | |
| // its failure and try the next preferred container. Remember the | |
| // error so a genuine outage on the fallback (ecency.waves) is | |
| // surfaced rather than masked as a missing host. | |
| lastError = e; | |
| } | |
| } | |
| } else { | |
| const hostEntries = await queryClient.fetchQuery( | |
| getAccountPostsQueryOptions(host, ProfileFilter.posts) | |
| ); | |
| entry = hostEntries?.[0]; | |
| } | |
| if (!entry) { | |
| if (lastError) { | |
| throw lastError; | |
| } | |
| throw new Error(i18next.t("decks.threads-form.no-threads-host")); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/web/src/features/waves/components/wave-form/api/use-wave-create.ts`
around lines 56 - 86, Clear the stale fallback error state in use-wave-create
after a successful fetch probe. In the isEcencyWavesHost(host) loop, if
queryClient.fetchQuery succeeds but returns no entries, reset lastError so a
previous hive.flow failure is not rethrown later. Keep the retry logic around
ECENCY_WAVES_HOSTS, but only throw lastError when the final attempted host
actually failed; otherwise fall back to the no-threads-host error.
| if (host) { | ||
| threadItem.host = host; | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Derive reply host from the parent thread, not the composer default.
Line 106 overwrites every reply with the requested threadHost, but apps/web/src/features/waves/components/wave-form/index.tsx Lines 282-289 pass that local-storage value even for replies. Once new waves start resolving to hive.flow, replies to those threads can still be labeled ecency.waves here, which risks pushing optimistic reply state into the wrong host-scoped caches/UI. Use the reply’s existing/parent host as the source of truth and only fall back to the composer host for new top-level waves.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/web/src/features/waves/hooks/use-wave-submit.ts` around lines 106 - 108,
The reply host is being taken from the composer default instead of the parent
thread, so optimistic reply state can be assigned to the wrong host-scoped
cache. Update use-wave-submit’s reply-building logic to derive host from the
parent thread or existing reply context in threadItem, and only fall back to the
composer host for new top-level waves; also review wave-form’s threadHost
passthrough so replies don’t override the parent host.
Summary
Aligns the web wave write path with mobile: new waves prefer the
hive.flowcontainer and fall back toecency.waves.Previously the composer always published into
ecency.waves. Nowuse-wave-createresolves the container at submit time by walkingECENCY_WAVES_HOSTS([hive.flow, ecency.waves]) and posting into the first one that currently has a live anchor post.hive.flowis pre-provisioned and inert today, so there is no behaviour change now (it resolves toecency.waves); it switches over automatically oncehive.flowstarts posting. The unified feed already reads every container; only Ecency's own containers are written to. Explicit deck hosts (leothreads / liketu.moments / ...) are unchanged.Changes
wave-hosts: addhive.flow, the orderedECENCY_WAVES_HOSTSlist, andisEcencyWavesHost.use-wave-create: resolve the preferred live container; the hive.flow lookup is wrapped so its absence is a clean no-op fallback.use-waves-api:hive.flowwaves use the dedicatedwave-*permlink scheme, likeecency.waves.use-wave-submit: reflect the resolved container on the created item.Full web test suite passes locally (1673 tests); typecheck + lint clean.
Summary by CodeRabbit
New Features
Bug Fixes
Tests