Skip to content

Conversation

schiller-manuel
Copy link
Contributor

@schiller-manuel schiller-manuel commented Sep 30, 2025

Summary by CodeRabbit

  • New Features

    • Added a new SSR route at /ssr/nested.
    • Enhanced state serialization to support nested/extended adapters for accurate SSR hydration.
  • Refactor

    • Improved adapter handling to avoid duplicates and ensure reliable nested serialization.
  • Chores

    • Simplified server response headers for more predictable output.
  • Tests

    • Added end-to-end tests validating nested state (shout/whisper) is preserved across SSR/CSR.

Copy link
Contributor

coderabbitai bot commented Sep 30, 2025

Walkthrough

Adds nested serialization adapters and a new SSR route (/ssr/nested) in the e2e app, updates tests, deduplicates adapter registration, and extends the core serializer to support adapter “extends” chains. Adjusts start-server response header assembly and performs a small type cleanup in start-client-core.

Changes

Cohort / File(s) Summary
Core serializer: adapter extends support
packages/router-core/src/ssr/serializer/transformer.ts
Generalizes serialization adapter types to support extends chains, introduces UnionizeSerializationAdaptersInput, updates createSerializationAdapter and plugins to accept AnySerializationAdapter and recursively process extends.
Start client-core tweaks
packages/start-client-core/src/createStart.ts, packages/start-client-core/src/createMiddleware.ts
Adds dedupeSerializationAdapters and applies it to options.serializationAdapters; minor TypeScript type cleanup in AssignAllServerRequestContext (comment/alias removal, no runtime change).
Start server-core headers
packages/start-server-core/src/createStartHandler.ts
Removes getResponseHeaders usage; constructs start response headers from Content-Type and accumulated route headers only.
E2E demo data + adapter registration
e2e/react-start/serialization-adapters/src/data.tsx, e2e/react-start/serialization-adapters/src/start.tsx
Adds NestedInner/NestedOuter classes, nestedInnerAdapter and nestedOuterAdapter (outer extends inner), and makeNested(); registers nestedOuterAdapter in serializationAdapters.
E2E new SSR route
e2e/react-start/serialization-adapters/src/routes/ssr/nested.tsx, e2e/react-start/serialization-adapters/src/routeTree.gen.ts
Introduces SSR route /ssr/nested; integrates it into generated route typings/tree with beforeLoad data provisioning and component verifying serialized shout/whisper.
E2E tests
e2e/react-start/serialization-adapters/tests/app.spec.ts
Adds tests for /ssr/nested validating expected vs actual shout/whisper across SSR and data-only/stream paths.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant Router
  participant Route(/ssr/nested)
  participant Serializer
  participant Client

  User->>Router: Request /ssr/nested
  Router->>Route(/ssr/nested): beforeLoad()
  Route(/ssr/nested)->>Route(/ssr/nested): makeNested() -> NestedOuter(NestedInner)
  Route(/ssr/nested)->>Serializer: serialize loader/context data
  Note right of Serializer: Uses adapters with extends chain
  Serializer-->>Router: Serialized payload
  Router-->>User: SSR HTML + serialized data
  User->>Client: Hydrate
  Client->>Serializer: deserialize payload
  Client->>Route(/ssr/nested): component() reads loader data
  Route(/ssr/nested)-->>User: Render expected vs actual shout/whisper
Loading
sequenceDiagram
  autonumber
  participant Start as Start.createStart
  participant Opts as getOptions()
  participant Dedupe as dedupeSerializationAdapters
  participant Adapter as nestedOuterAdapter
  participant Ext as nestedInnerAdapter

  Start->>Opts: initialize with options
  Opts->>Dedupe: dedupe(options.serializationAdapters)
  Dedupe->>Adapter: visit
  Adapter->>Ext: visit extends[]
  Ext-->>Dedupe: add
  Adapter-->>Dedupe: add
  Dedupe-->>Opts: deduped list
  Opts-->>Start: options with deduped adapters
Loading
sequenceDiagram
  autonumber
  participant Core as router-core serializer
  participant Plugin as makeSerovalPlugin()
  participant OA as OuterAdapter
  participant IA as InnerAdapter

  Core->>Plugin: create plugin for OA
  Plugin->>OA: test(value)
  OA->>OA: toSerializable(value) -> inner instance
  Plugin->>Plugin: recurse into extends[]
  Plugin->>IA: test(inner)
  IA->>IA: toSerializable(inner) -> string
  IA-->>Plugin: fromSerializable(string) -> inner
  Plugin-->>OA: fromSerializable(inner) -> outer
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

package: router-core, package: start-client-core, package: react-start

Suggested reviewers

  • chorobin

Poem

I nibbled some types, then hopped through the tree,
Nested on nested, a whisper to me.
Shout in the server, echo on client—
Adapters in line, their extends compliant.
Headers trimmed tidy, routes freshly dressed—
Carrot-approve! This build is blessed. 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The pull request title clearly and concisely describes the primary change of enabling dependencies between serialization adapters by adding an “extends” relationship and corresponding generic support, which aligns with the extensive updates to createSerializationAdapter, the SerializationAdapter interface, transformer plumbing, and related usage in nested adapter definitions and tests.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch serialization-adapter-extends

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

nx-cloud bot commented Sep 30, 2025

View your CI Pipeline Execution ↗ for commit f770758

Command Status Duration Result
nx affected --targets=test:eslint,test:unit,tes... ✅ Succeeded 5m 9s View ↗
nx run-many --target=build --exclude=examples/*... ✅ Succeeded 1m 21s View ↗

☁️ Nx Cloud last updated this comment at 2025-09-30 18:32:43 UTC

Copy link

pkg-pr-new bot commented Sep 30, 2025

More templates

@tanstack/arktype-adapter

npm i https://pkg.pr.new/TanStack/router/@tanstack/arktype-adapter@5314

@tanstack/directive-functions-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/directive-functions-plugin@5314

@tanstack/eslint-plugin-router

npm i https://pkg.pr.new/TanStack/router/@tanstack/eslint-plugin-router@5314

@tanstack/history

npm i https://pkg.pr.new/TanStack/router/@tanstack/history@5314

@tanstack/nitro-v2-vite-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/nitro-v2-vite-plugin@5314

@tanstack/react-router

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-router@5314

@tanstack/react-router-devtools

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-router-devtools@5314

@tanstack/react-router-ssr-query

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-router-ssr-query@5314

@tanstack/react-start

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-start@5314

@tanstack/react-start-client

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-start-client@5314

@tanstack/react-start-server

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-start-server@5314

@tanstack/router-cli

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-cli@5314

@tanstack/router-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-core@5314

@tanstack/router-devtools

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-devtools@5314

@tanstack/router-devtools-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-devtools-core@5314

@tanstack/router-generator

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-generator@5314

@tanstack/router-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-plugin@5314

@tanstack/router-ssr-query-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-ssr-query-core@5314

@tanstack/router-utils

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-utils@5314

@tanstack/router-vite-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-vite-plugin@5314

@tanstack/server-functions-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/server-functions-plugin@5314

@tanstack/solid-router

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-router@5314

@tanstack/solid-router-devtools

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-router-devtools@5314

@tanstack/solid-start

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-start@5314

@tanstack/solid-start-client

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-start-client@5314

@tanstack/solid-start-server

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-start-server@5314

@tanstack/start-client-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-client-core@5314

@tanstack/start-plugin-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-plugin-core@5314

@tanstack/start-server-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-server-core@5314

@tanstack/start-static-server-functions

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-static-server-functions@5314

@tanstack/start-storage-context

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-storage-context@5314

@tanstack/valibot-adapter

npm i https://pkg.pr.new/TanStack/router/@tanstack/valibot-adapter@5314

@tanstack/virtual-file-routes

npm i https://pkg.pr.new/TanStack/router/@tanstack/virtual-file-routes@5314

@tanstack/zod-adapter

npm i https://pkg.pr.new/TanStack/router/@tanstack/zod-adapter@5314

commit: f770758

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/start-server-core/src/createStartHandler.ts (1)

41-51: Don’t drop headers coming from the shared response context.

By removing getResponseHeaders() we no longer flush headers that middleware/routes push via setResponseHeaders/setResponseHeader (e.g. cookies, CORS). Only merging match.headers means those headers disappear from the final SSR response, which breaks auth flows and any route-level header customization. Please restore the merge with the shared response headers (or otherwise propagate them) before release.

🧹 Nitpick comments (4)
packages/router-core/src/ssr/serializer/transformer.ts (2)

32-44: createSerializationAdapter: good API shape; consider avoiding double cast

The function shape with const generic for TExtendsAdapters is solid. Minor: the opts as unknown as SerializationAdapter<...> cast sidelines type errors. If feasible, tighten the interface so opts already satisfies SerializationAdapter<...> (e.g., by intersecting CreateSerializationAdapterOptions with the required function types), removing the double cast.

-export function createSerializationAdapter<...>(opts: CreateSerializationAdapterOptions<...>): SerializationAdapter<...> {
-  return opts as unknown as SerializationAdapter<...>
-}
+export function createSerializationAdapter<...>(
+  opts: CreateSerializationAdapterOptions<...>,
+): SerializationAdapter<...> {
+  return opts
+}

139-170: Plugin ‘extends’ mapping works; avoid mutable Array cast

Runtime wiring is correct. Minor nit: prefer not casting to mutable Array—use ReadonlyArray to reflect the declared type and avoid unnecessary as:

-extends: serializationAdapter.extends
-  ? (serializationAdapter.extends as Array<AnySerializationAdapter>).map(
-      (ext) => makeSsrSerovalPlugin(ext, options),
-    )
-  : undefined,
+extends: serializationAdapter.extends
+  ? (serializationAdapter.extends as ReadonlyArray<AnySerializationAdapter>).map(
+      (ext) => makeSsrSerovalPlugin(ext, options),
+    )
+  : undefined,
packages/start-client-core/src/createStart.ts (1)

120-127: Dedupe invocation: prefer ReadonlyArray and avoid double cast

The new dedupe is valuable. Minor type hygiene: accept ReadonlyArray<AnySerializationAdapter> in the helper to remove as unknown as Array<...> and keep options immutable.

-const deduped = new Set<AnySerializationAdapter>()
-dedupeSerializationAdapters(
-  deduped,
-  options.serializationAdapters as unknown as Array<AnySerializationAdapter>,
-)
-options.serializationAdapters = Array.from(deduped) as any
+const deduped = new Map<string, AnySerializationAdapter>()
+dedupeSerializationAdapters(deduped, options.serializationAdapters as ReadonlyArray<AnySerializationAdapter>)
+options.serializationAdapters = Array.from(deduped.values()) as any
e2e/react-start/serialization-adapters/src/data.tsx (1)

112-118: Consider adding a type guard for consistency.

The adapter correctly demonstrates the new extends feature by depending on nestedInnerAdapter. However, for consistency with nestedInnerAdapter (line 107), consider adding an explicit type guard to the test function.

Apply this diff:

 export const nestedOuterAdapter = createSerializationAdapter({
   key: 'nestedOuter',
   extends: [nestedInnerAdapter],
-  test: (value) => value instanceof NestedOuter,
+  test: (value): value is NestedOuter => value instanceof NestedOuter,
   toSerializable: (outer) => outer.inner,
   fromSerializable: (value) => new NestedOuter(value),
 })
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 581941a and f770758.

📒 Files selected for processing (9)
  • e2e/react-start/serialization-adapters/src/data.tsx (1 hunks)
  • e2e/react-start/serialization-adapters/src/routeTree.gen.ts (6 hunks)
  • e2e/react-start/serialization-adapters/src/routes/ssr/nested.tsx (1 hunks)
  • e2e/react-start/serialization-adapters/src/start.tsx (1 hunks)
  • e2e/react-start/serialization-adapters/tests/app.spec.ts (1 hunks)
  • packages/router-core/src/ssr/serializer/transformer.ts (4 hunks)
  • packages/start-client-core/src/createMiddleware.ts (1 hunks)
  • packages/start-client-core/src/createStart.ts (2 hunks)
  • packages/start-server-core/src/createStartHandler.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use TypeScript in strict mode with extensive type safety across the codebase

Files:

  • e2e/react-start/serialization-adapters/src/routeTree.gen.ts
  • e2e/react-start/serialization-adapters/src/routes/ssr/nested.tsx
  • packages/start-client-core/src/createStart.ts
  • e2e/react-start/serialization-adapters/src/start.tsx
  • e2e/react-start/serialization-adapters/tests/app.spec.ts
  • e2e/react-start/serialization-adapters/src/data.tsx
  • packages/start-server-core/src/createStartHandler.ts
  • packages/router-core/src/ssr/serializer/transformer.ts
  • packages/start-client-core/src/createMiddleware.ts
e2e/**

📄 CodeRabbit inference engine (AGENTS.md)

Store end-to-end tests under the e2e/ directory

Files:

  • e2e/react-start/serialization-adapters/src/routeTree.gen.ts
  • e2e/react-start/serialization-adapters/src/routes/ssr/nested.tsx
  • e2e/react-start/serialization-adapters/src/start.tsx
  • e2e/react-start/serialization-adapters/tests/app.spec.ts
  • e2e/react-start/serialization-adapters/src/data.tsx
**/src/routes/**

📄 CodeRabbit inference engine (AGENTS.md)

Place file-based routes under src/routes/ directories

Files:

  • e2e/react-start/serialization-adapters/src/routes/ssr/nested.tsx
packages/{*-start,start-*}/**

📄 CodeRabbit inference engine (AGENTS.md)

Name and place Start framework packages under packages/-start/ or packages/start-/

Files:

  • packages/start-client-core/src/createStart.ts
  • packages/start-server-core/src/createStartHandler.ts
  • packages/start-client-core/src/createMiddleware.ts
packages/router-core/**

📄 CodeRabbit inference engine (AGENTS.md)

Keep framework-agnostic core router logic in packages/router-core/

Files:

  • packages/router-core/src/ssr/serializer/transformer.ts
🧬 Code graph analysis (6)
e2e/react-start/serialization-adapters/src/routeTree.gen.ts (2)
e2e/react-start/basic/src/routeTree.gen.ts (3)
  • FileRoutesByTo (300-332)
  • FileRoutesById (333-375)
  • RootRouteChildren (493-511)
e2e/react-router/js-only-file-based/src/routeTree.gen.js (1)
  • IndexRoute (30-34)
e2e/react-start/serialization-adapters/src/routes/ssr/nested.tsx (1)
e2e/react-start/serialization-adapters/src/data.tsx (1)
  • makeNested (120-122)
packages/start-client-core/src/createStart.ts (2)
packages/router-core/src/ssr/serializer/transformer.ts (1)
  • AnySerializationAdapter (137-137)
packages/router-core/src/index.ts (1)
  • AnySerializationAdapter (416-416)
e2e/react-start/serialization-adapters/src/start.tsx (2)
packages/start-client-core/src/createStart.ts (1)
  • createStart (83-137)
e2e/react-start/serialization-adapters/src/data.tsx (1)
  • nestedOuterAdapter (112-118)
e2e/react-start/serialization-adapters/src/data.tsx (1)
packages/router-core/src/ssr/serializer/transformer.ts (1)
  • createSerializationAdapter (30-44)
packages/router-core/src/ssr/serializer/transformer.ts (1)
packages/router-core/src/index.ts (7)
  • AnySerializationAdapter (416-416)
  • createSerializationAdapter (429-429)
  • SerializationAdapter (417-417)
  • ValidateSerializable (421-421)
  • Serializable (425-425)
  • makeSsrSerovalPlugin (431-431)
  • makeSerovalPlugin (430-430)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Preview
  • GitHub Check: Test
🔇 Additional comments (25)
packages/start-client-core/src/createMiddleware.ts (1)

292-298: Safer context merge without widening

Switching the inner merge to Assign<GlobalServerRequestContext<TRegister>, …> keeps the registered server request context narrow instead of immediately widening to AnyContext, so downstream middleware gets the proper inference without losing the existing fallback semantics. Looks great.

packages/router-core/src/ssr/serializer/transformer.ts (6)

26-29: Type-level union of extended adapters’ inputs looks correct

UnionizeSerializationAdaptersInput cleanly unions ['~types']['input'] from the extends chain; with never it collapses as intended. LGTM.


46-61: toSerializable typing: nice expansion to accept extended adapters’ inputs

Widening ValidateSerializable to Serializable | Unionize... is the right move to enable chaining. No issues spotted.


114-125: SerializationAdapter ‘extends’ typing is precise; good use of never to forbid property

The extends?: TExtendsAdapters with TExtendsAdapters = never elegantly prevents accidental assignment. LGTM.


127-135: ‘~types’.input includes parent and extended inputs

Including UnionizeSerializationAdaptersInput<TExtendsAdapters> in input enables downstream composition. Looks good.


137-137: AnySerializationAdapter now parameterizes all three generics

This aligns consumers (e.g., start-client-core) with the new shape. LGTM.


172-201: Client plugin deserialization path looks correct

fromSerializable(ctx.deserialize(node)) matches the chaining model; parse variants cover sync/async/stream. LGTM.

If we ever allow deep (>1) extends chains, consider adding an e2e that nests 3+ adapters to validate multi-hop parse/deserialize.

e2e/react-start/serialization-adapters/src/start.tsx (2)

2-2: Import of nestedOuterAdapter only is appropriate

Matches the intent to register nestedInnerAdapter via extends on the outer adapter. LGTM.


8-14: Unable to automatically verify that all createSerializationAdapter keys are unique. Please manually review every adapter definition (including those brought in via extends) to ensure no duplicate keys exist.

packages/start-client-core/src/createStart.ts (1)

83-137: Public type imports align with router-core changes

AnySerializationAdapter usage matches the updated alias in router-core. LGTM.

Please confirm the repository’s TypeScript version is ≥ 5.3 (required for const type parameters).

e2e/react-start/serialization-adapters/tests/app.spec.ts (1)

53-72: LGTM!

The test case correctly validates nested serialization adapter functionality by comparing expected (locally generated) vs actual (server-serialized) state for both shout and whisper methods. The structure follows the established testing pattern.

e2e/react-start/serialization-adapters/src/routes/ssr/nested.tsx (3)

5-10: LGTM!

The beforeLoad hook correctly seeds the context with nested data, and the loader propagates the entire context for serialization testing. This pattern is appropriate for validating SSR serialization round-trips.


11-19: LGTM!

The component logic correctly compares locally-generated expected values against loader-provided actual values, validating that serialization/deserialization preserves both the object structure and method behavior.


20-54: LGTM!

The render output correctly exposes expected and actual state with test IDs that match the test assertions in app.spec.ts. The structure facilitates both visual debugging and automated testing.

e2e/react-start/serialization-adapters/src/data.tsx (4)

98-103: LGTM!

The NestedInner class provides a simple string wrapper with an shout() method for testing. The implementation is clean and correct.


91-96: LGTM!

The NestedOuter class correctly wraps NestedInner and provides a whisper() method. This nested structure is appropriate for testing serialization adapter dependency chains.


105-110: LGTM!

The nestedInnerAdapter correctly serializes NestedInner instances to their string values and reconstructs them during deserialization. The type guard is properly specified.


120-122: LGTM!

The makeNested() factory correctly instantiates the nested structure for testing. The fixed seed value ('Hello World') ensures consistent test results.

e2e/react-start/serialization-adapters/src/routeTree.gen.ts (7)

14-14: LGTM: Import statement follows existing pattern.

The import for the new nested route is consistent with other SSR route imports in this auto-generated file.


28-32: LGTM: Route definition is consistent with existing routes.

The SsrNestedRoute definition correctly follows the established pattern with proper id, path, and parent route configuration.


49-49: LGTM: Type interface additions are correct.

The new route is properly added to all three route mapping interfaces (FileRoutesByFullPath, FileRoutesByTo, and FileRoutesById) with consistent typing.

Also applies to: 56-56, 64-64


73-73: LGTM: Union type additions are correct.

The new route path '/ssr/nested' is properly added to the fullPaths, to, and id union types in FileRouteTypes. The reformatting of the to union type (lines 76-81) maintains consistency while adding the new route.

Also applies to: 76-81, 87-87


95-95: LGTM: RootRouteChildren interface correctly updated.

The SsrNestedRoute is properly added to the RootRouteChildren interface with correct typing.


115-121: LGTM: Module augmentation is complete and correct.

The module augmentation for '@tanstack/react-router' properly declares the '/ssr/nested' route with all required metadata (id, path, fullPath, preLoaderRoute, parentRoute).


143-143: LGTM: Route tree construction properly includes the new route.

The rootRouteChildren object correctly includes the SsrNestedRoute, completing the integration of the new '/ssr/nested' route into the route tree.

Comment on lines +68 to +81
function dedupeSerializationAdapters(
deduped: Set<AnySerializationAdapter>,
plugins: Array<AnySerializationAdapter>,
): void {
for (let i = 0, len = plugins.length; i < len; i++) {
const current = plugins[i]!
if (!deduped.has(current)) {
deduped.add(current)
if (current.extends) {
dedupeSerializationAdapters(deduped, current.extends)
}
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Dedupe by identity only; also dedupe by key to prevent registry collisions

Current Set dedupes by object identity. Two distinct adapter instances with the same key can still both be retained, leading to ambiguous registration (last one wins) and hard-to-debug behavior.

Consider deduping by key (in addition to identity) so only the first occurrence is kept:

-function dedupeSerializationAdapters(
-  deduped: Set<AnySerializationAdapter>,
-  plugins: Array<AnySerializationAdapter>,
-): void {
-  for (let i = 0, len = plugins.length; i < len; i++) {
-    const current = plugins[i]!
-    if (!deduped.has(current)) {
-      deduped.add(current)
-      if (current.extends) {
-        dedupeSerializationAdapters(deduped, current.extends)
-      }
-    }
-  }
-}
+function dedupeSerializationAdapters(
+  seenByKey: Map<string, AnySerializationAdapter>,
+  plugins: ReadonlyArray<AnySerializationAdapter>,
+): void {
+  for (let i = 0, len = plugins.length; i < len; i++) {
+    const current = plugins[i]!
+    if (!seenByKey.has(current.key)) {
+      seenByKey.set(current.key, current)
+      if (current.extends) {
+        dedupeSerializationAdapters(seenByKey, current.extends as ReadonlyArray<AnySerializationAdapter>)
+      }
+    }
+  }
+}

And update the call site to use Array.from(map.values()) to preserve first-seen order.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In packages/start-client-core/src/createStart.ts around lines 68 to 81, the
current dedupeSerializationAdapters function only dedupes by object identity
which allows multiple distinct adapter objects with the same key to remain and
cause registry collisions; change the implementation to dedupe by adapter.key in
addition to identity (for example, track seen keys in a Set or use a Map keyed
by adapter.key to keep the first-seen instance and ignore subsequent ones),
ensure recursion over current.extends uses the same dedupe logic, and update the
call site that collects final adapters to use Array.from(map.values()) (or
otherwise preserve first-seen order) so only the initial adapter per key is kept
and ordering is stable.

@schiller-manuel schiller-manuel merged commit 545ff3f into main Sep 30, 2025
6 checks passed
@schiller-manuel schiller-manuel deleted the serialization-adapter-extends branch September 30, 2025 18:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant