Skip to content

web: phoenix-relay-idiom — every page on @connection + idiomatic Relay#2

Merged
cansirin merged 4 commits into
mainfrom
umut/phoenix-relay-idiom
May 15, 2026
Merged

web: phoenix-relay-idiom — every page on @connection + idiomatic Relay#2
cansirin merged 4 commits into
mainfrom
umut/phoenix-relay-idiom

Conversation

@usirin
Copy link
Copy Markdown
Member

@usirin usirin commented May 11, 2026

Summary

  • Migrates every page (pano feed, pano post detail, sozluk home, sozluk term page, profile) from flat-array list fields + setFetchKey-driven refetches to idiomatic Relay: @connection-shaped fields with usePaginationFragment, fragment composition via useFragment, hand-written updater + optimisticResponse for adds, @deleteRecord for deletes, and commitLocalUpdate for live updates (via the new useLiveAgent v2).
  • Adds the GraphQL Node interface (Term / Post / Comment / Definition / User / Profile all implements Node), a top-level node(id) resolver, and global base64 ids; mutations stay lenient via extractLocalId so callers can hand back either shape during the transition.
  • Cleanup: drops the parallel flat-array list fields (the last survivor was Query.postComments), collapses useLiveAgent v1/v2 into one file, and strikes through the obsolete Suspense-double-mount lessons in phoenix-product-mvp/operator.md with a pointer to this work. The 12-sozluk-vote.spec.ts:46 page.reload() — the literal artifact behind this whole feature — is gone.

Test plan

  • pnpm typecheck clean
  • pnpm build clean
  • pnpm test 124/124 across 19 vitest files (3 new connection tests for posts / comments / terms)
  • pnpm relay clean compile (43 reader / 25 normalization / 47 operation text)
  • Scoped Playwright (specs 11- through 23-): 19 / 24 passing. The 5 failures (15-pano-vote-post, 19-pano-edit-delete-comment, 20-profile-page, 22-live-updates × 2) are pre-existing on tasks 3 / live-updates and unrelated to the cleanup — triaged in progress/task_7.md.
  • Local smoke against pnpm dev worth 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)

  • WS-borne new-comment edge insertion stub exists in panoPostDetailUpdater.ts but isn't wired (the WS payload carries counts, not new-comment ids today).
  • Brand-new-slug addDefinition still calls window.location.reload() because the payload doesn't return the auto-created Term — needs an AddDefinitionPayload { definition, term } follow-up.

🤖 Generated with Claude Code

usirin and others added 4 commits May 10, 2026 14:56
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>
@cansirin cansirin merged commit fcec685 into main May 15, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants