web: phoenix-relay-idiom — every page on @connection + idiomatic Relay#2
Merged
Conversation
Lay the server-side foundation for phoenix-relay-idiom (task_1). Six entity types (Term, Post, Comment, Definition, User, Profile) implement the new `Node` interface and expose globally-unique base64-encoded ids. A top-level `node(id): Node` resolver decodes the global id and dispatches to the right per-atom Agent or D1 reader. PageInfo grows the missing Relay fields (`hasPreviousPage`, `startCursor`) with safe defaults so existing connection consumers keep working without having to manufacture them. Mutations that take an `ID!` for one of these entities now run their input through `extractLocalId(input, expectedTypename)` before dispatch — lenient during the MVP-frontend migration window: a global id decodes; a raw local id passes through. Page-migration tasks (2-6) will switch the FE to send global ids; raw ids drop out in cleanup (task 7). Helpers: - `apps/web/src/relay/encodeNodeId.ts` — `encodeNodeId` / `decodeNodeId` / `extractLocalId`. UTF-8-safe base64 (Turkish slugs round-trip). Source-of-truth here, included in both app and worker tsconfigs. - `apps/web/src/lib/useLiveAgent.v2.ts` — skeleton hook that swaps the v1 refetch-on-signal pattern for `commitLocalUpdate`. Consumer pages provide an `applyToStore` callback. v1 untouched; both coexist during the migration. Cleanup task drops v1 and renames v2. Non-goals from the PRD held: no shared `updateConnectionCount` helper (accept totalCount drift), no shared `LoadMoreButton` (define inline per page). The existing flat-array list fields (`posts`, `terms`, `Term.definitions`, `Post.comments`) still resolve — page migrations 2-6 add their connection counterparts alongside. Refs: phoenix-relay-idiom task_1
…tions
Wave 1 of phoenix-relay-idiom — pano surface only. Two task slices land
together because their schema changes overlap on the Pano types:
- Feed (task_2): `posts(sort, host, first, after): PostConnection!` +
`PanoFeedPostsFragment` (`@connection(filters: ["sort","host"])` +
`@refetchable`) + `PanoPostCardFragment on Post` + hand-written
`submitPost` updater (prepends across every cached filter combo) +
`optimisticResponse` for the immediate flip + `deletePost` returns
`ID! @deleteRecord`.
- Post detail (task_3): `Post.comments(first, after): CommentConnection!`
via `listCommentsConnection` on PanoPost (forward keyset on comment id;
reply-aware totalCount) + `PanoPostHeaderFragment`,
`PanoPostDetailCommentsFragment`, `CommentTreeNodeFragment` +
`useLazyLoadQuery + usePaginationFragment` + tree builder from edges +
`addComment` `appendCommentToPostConnection` updater +
`optimisticResponse` + `deleteComment` returns
`DeleteCommentPayload { deletedCommentId @deleteRecord, comment }` so
the leaf path removes the record and the parent-with-replies path
ships the `[silindi]` placeholder back through Relay's normal store
update.
Live updates flip from `useLiveAgent` v1 (refetch on signal) to
`useLiveAgentV2` (`commitLocalUpdate`). The page tree no longer
unmounts on a Pano live event; `LivePill` connection-state UX is
preserved verbatim.
Comment.deletedAt added to the schema so the SPA can detect the
`[silindi]` placeholder via a typed null-check instead of the body-string
match.
Test changes:
- New: `pano-post-connection.test.ts`, `pano-comments-connection.test.ts`
cover both new readers (totalCount, hasNextPage, stale cursor, reply-
aware reply count, leaf vs parent-with-replies delete payload).
- Stripped page.reload() Suspense workarounds from 15/16/17/18/19; the
reasoning lives in updated doc comments referencing this feature.
PanoComment.tsx renamed to CommentTreeNode.tsx per task spec.
…fragment composition Migrates UserProfilePage to idiomatic Relay (phoenix-relay-idiom task_6). Server: ContributionConnection now exposes totalCount — listContributions issues a parallel three-table COUNT(*) so the profile header can display the absolute contribution count alongside the per-kind counters. Client: split UserProfilePage into a connection-shaped pagination fragment plus three sub-fragments. UserProfileHeader reads the profile aggregates; ContributionsList runs usePaginationFragment over the contributions connection and renders a LoadMoreButton when hasNext. ContributionRow does an inline __typename switch over the ProfileContribution union and spreads ContributionRow_definition / _post / _comment per variant. PostContribution's bodyExcerpt is aliased postBodyExcerpt because Post's variant is nullable where the other two variants are String! — Relay refuses to merge selections of differing nullability under one name. Fragment naming follows the relay-compiler module-name rule: fragments must start with the source module's basename. The pagination fragment is therefore UserProfilePageContributionsFragment (not UserProfileContributionsFragment as the AC text reads); connection key is UserProfile__contributions, matching the <SomeName>__<fieldName> form Relay enforces. Strips the page.reload() Suspense workaround from 20-profile-page.spec.ts — with connection-shaped fragments the page tree no longer unmounts on mutation, so the workaround is obsolete.
… home + term to relay-idiom Closes the migration: every page now goes through @connection-shaped fields with usePaginationFragment + fragment composition. Drops the parallel flat-array list fields (postComments was the last survivor) and collapses useLiveAgent v1/v2 into a single commitLocalUpdate-flavor file. Adds the sozluk home + term-page migrations (TermConnection / DefinitionConnection with prepend updater + @deleteRecord) that complete tasks 4-7 of phoenix-relay-idiom. The 12-sozluk-vote.spec.ts:46 reload — the literal artifact behind this feature — is gone. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 tasks
usirin
added a commit
that referenced
this pull request
Jun 1, 2026
…, Cause Option) #1 Type resolver/source generator bodies with Effect.gen.Return<A, never, R> (pins only the env slot R) so Effect.gen infers R structurally — deletes the `Effect.gen(body) as Effect.Effect<A, unknown, R>` cast in genEffect and the per-wrapper R generic moves outward (default FateEnv / WorkerFateServices). #2 Pass {signal: ctx.request.signal} to runPromiseExit so a disconnected fate client interrupts the resolver fiber (matches HttpEffect run-with-signal). #5 Replace Cause.findError + `_tag === "Success"` with Cause.findErrorOption + Option.match (no Result tag leaks into boundary code). Rewrite the stale .patterns/fate-effect-bridge.md to the shipped ManagedRuntime model. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
usirin
added a commit
that referenced
this pull request
Jun 1, 2026
Idiomatic review (#2,#5) landed; #1 (removing the genEffect cast via Effect.gen.Return) reverted — pinning R in the generator yield is contravariant and cascades into fate's QueryDefinition<FateContext<WorkerFateServices>> server constraint, so the single contained boundary cast stays. runEffect now wires {signal: ctx.request.signal} (client-abort interrupts the resolver fiber, matching HttpEffect) and unwinds the Cause via Cause.findErrorOption + Option.match. .patterns/fate-effect-bridge.md updated to match. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
setFetchKey-driven refetches to idiomatic Relay:@connection-shaped fields withusePaginationFragment, fragment composition viauseFragment, hand-writtenupdater+optimisticResponsefor adds,@deleteRecordfor deletes, andcommitLocalUpdatefor live updates (via the newuseLiveAgentv2).Nodeinterface (Term/Post/Comment/Definition/User/Profileallimplements Node), a top-levelnode(id)resolver, and global base64 ids; mutations stay lenient viaextractLocalIdso callers can hand back either shape during the transition.Query.postComments), collapsesuseLiveAgentv1/v2 into one file, and strikes through the obsolete Suspense-double-mount lessons inphoenix-product-mvp/operator.mdwith a pointer to this work. The12-sozluk-vote.spec.ts:46page.reload()— the literal artifact behind this whole feature — is gone.Test plan
pnpm typecheckcleanpnpm buildcleanpnpm test124/124 across 19 vitest files (3 new connection tests for posts / comments / terms)pnpm relayclean compile (43 reader / 25 normalization / 47 operation text)progress/task_7.md.pnpm devworth doing on each migrated page (pano feed submit / vote / delete; pano post detail add comment / vote / delete; sozluk home / term page; profile pagination) to confirm no UI regression vs the MVP.Carry-forwards (documented in feature progress logs, not blocking)
panoPostDetailUpdater.tsbut isn't wired (the WS payload carries counts, not new-comment ids today).addDefinitionstill callswindow.location.reload()because the payload doesn't return the auto-createdTerm— needs anAddDefinitionPayload { definition, term }follow-up.🤖 Generated with Claude Code