Skip to content

refactor!: apply Dart 3 class modifiers to the core public types - #1683

Open
spydon wants to merge 1 commit into
mainfrom
lukasklingsbo/sdk-819-breakingsdk-apply-dart-3-class-modifiers
Open

refactor!: apply Dart 3 class modifiers to the core public types#1683
spydon wants to merge 1 commit into
mainfrom
lukasklingsbo/sdk-819-breakingsdk-apply-dart-3-class-modifiers

Conversation

@spydon

@spydon spydon commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Closes SDK-819.

Most public classes in the SDK were plain class, so consumers could extend or implement any of them, including internal plumbing. This declares the intent explicitly with Dart 3 class modifiers.

What changed

interface class — implementable, not extendable

The client entry points and the request builder base. interface class closes off extends while leaving implements open, so the standard mocking idiom keeps compiling:

// Still compiles
class MockSupabaseClient extends Mock implements SupabaseClient {}

// No longer compiles
class MyClient extends SupabaseClient { /* ... */ }

SupabaseClient, GoTrueClient, PostgrestClient, PostgrestBuilder, RealtimeClient, FunctionsClient, SupabaseStorageClient, StorageFileApi.

The ticket proposed final class for these. final also blocks implements, which would break every consumer test suite that mocks a client with no escape hatch, so interface is the narrower break that still delivers "not designed for extension".

abstract interface class — the two consumer extension points

LocalStorage and GotrueAsyncStorage. Both declare only abstract members, so callers change one word:

- class MyLocalStorage extends LocalStorage { /* ... */ }
+ class MyLocalStorage implements LocalStorage { /* ... */ }

final class — fully closed

AuthState, plus the internal GotrueFetch, Fetch and SupabaseAuth helpers.

Notes on the rest of the ticket's proposal

  • The AuthException hierarchy is deliberately untouched. The ticket asks for base class AuthException with final subclasses, but the exceptions are being restructured by a separate chain of PRs, so the modifiers belong there rather than here where they would just conflict.
  • AuthChangeEvent is already an enum, so the proposed sealed class treatment is moot; exhaustive switches over auth events are unchanged. AuthState is a single class rather than a hierarchy, so it is final rather than sealed.
  • StorageBucketApi is not exported from package:storage_client, so consumers cannot name it and its modifier would have no public effect. Closing it would also force SupabaseStorageClient to be base/final and lose mockability, so it stays a plain class.
  • The concrete postgrest builders (PostgrestQueryBuilder and friends) stay open. PostgrestQueryBuilder is extended across package boundaries by SupabaseQueryBuilder, and marking the chain base/final would make the whole from(...) chain unmockable.

Test changes

Two realtime test stubs extended RealtimeClient and had to go:

  • The setAuth error-handling tests in channel_test.dart now use a mocktail mock of the socket, which is exactly the escape hatch interface class preserves.
  • The remove and setAuth tests in socket_test.dart register their mock channels on RealtimeClient.channels directly rather than overriding channel() to hand them back.

The LocalStorage and GotrueAsyncStorage test stubs switched from extends to implements.

Verification

  • dart analyze packages: no issues
  • dart format: no changes
  • Full test suites pass for gotrue (479), postgrest (196), realtime_client (205), storage_client (210), supabase (134), functions_client (48) and supabase_flutter (65), with the local Supabase stack running for the backend-dependent ones
  • The extracted public symbol set is byte-identical to main (1754 symbols, no additions or removals), so sdk-compliance.yaml needs no changes

MIGRATION.md documents each modifier under the v2 to v3 section.

Summary by CodeRabbit

  • Documentation

    • Added migration guidance for Dart 3 class modifiers and updated inheritance examples.
  • API Updates

    • Clarified which public classes can be extended, implemented, or neither through interface, abstract interface, and final declarations.
    • Preserved existing members, method signatures, and runtime behavior.
  • Tests

    • Updated storage and realtime test implementations to follow the new class-modifier rules.
    • Refined realtime authentication and channel test coverage using mocks.

@spydon
spydon requested a review from a team as a code owner August 10, 2026 14:51
@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

The PR applies Dart 3 class modifiers to public clients, builders, storage abstractions, authentication types, and utility classes. Production and test storage implementations now use implements. Realtime tests replace subclass-based helpers with mocks and direct channel setup. The migration guide documents the updated inheritance rules.

Dart 3 class modifier migration

Layer / File(s) Summary
Public class modifier declarations
packages/functions_client/lib/src/functions_client.dart, packages/gotrue/lib/src/..., packages/postgrest/lib/src/..., packages/realtime_client/lib/src/..., packages/storage_client/lib/src/..., packages/supabase/lib/src/..., packages/supabase_flutter/lib/src/..., MIGRATION.md
Public clients and builders are declared as interface class. AuthState, SupabaseAuth, GotrueFetch, and Fetch are declared as final class. The migration guide documents the new restrictions and examples.
Storage interface adoption
packages/gotrue/lib/src/types/gotrue_async_storage.dart, packages/gotrue/test/utils.dart, packages/supabase_flutter/lib/src/local_storage.dart, packages/supabase_flutter/test/widget_test_stubs.dart
GotrueAsyncStorage and LocalStorage are declared as abstract interfaces. Production and test storage classes implement them instead of extending them.
Realtime test adaptation
packages/realtime_client/test/channel_test.dart, packages/realtime_client/test/socket_test.dart, packages/realtime_client/test/socket_test_stubs.dart
Realtime tests use mock clients, direct channel lists, and mocktail verification. The custom socket subclass and channel lookup helper are removed.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

Suggested labels: v3, realtime

Suggested reviewers: tr00d

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: applying Dart 3 class modifiers to core public types.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch lukasklingsbo/sdk-819-breakingsdk-apply-dart-3-class-modifiers

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.

❤️ Share

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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Applies Dart 3 class modifiers across public SDK types to formalize extension and implementation boundaries.

Changes:

  • Adds interface, abstract interface, base, and final modifiers.
  • Updates affected test doubles and realtime tests.
  • Documents migration requirements.

Reviewed changes

Copilot reviewed 21 out of 21 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
MIGRATION.md Documents breaking modifier changes.
packages/supabase/lib/src/supabase_client.dart Makes the client an interface class.
packages/supabase_flutter/lib/src/local_storage.dart Defines storage extension interfaces.
packages/supabase_flutter/lib/src/supabase_auth.dart Finalizes the internal auth helper.
packages/supabase_flutter/test/widget_test_stubs.dart Updates storage test implementations.
packages/storage_client/lib/src/storage_file_api.dart Makes file API implementable only.
packages/storage_client/lib/src/storage_client.dart Makes storage client implementable only.
packages/storage_client/lib/src/fetch.dart Finalizes the internal fetch helper.
packages/realtime_client/lib/src/realtime_client.dart Makes realtime client implementable only.
packages/realtime_client/test/channel_test.dart Replaces subclass stubs with mocks.
packages/realtime_client/test/socket_test.dart Registers mock channels directly.
packages/realtime_client/test/socket_test_stubs.dart Removes the subclass-based socket stub.
packages/postgrest/lib/src/postgrest.dart Makes PostgREST client implementable only.
packages/postgrest/lib/src/postgrest_builder.dart Restricts external builder extension.
packages/gotrue/lib/src/gotrue_client.dart Makes auth client implementable only.
packages/gotrue/lib/src/fetch.dart Finalizes the internal fetch helper.
packages/gotrue/lib/src/types/auth_exception.dart Establishes the exception hierarchy modifiers.
packages/gotrue/lib/src/types/auth_state.dart Finalizes auth state.
packages/gotrue/lib/src/types/gotrue_async_storage.dart Defines an async storage interface.
packages/gotrue/test/utils.dart Updates the storage test implementation.
packages/functions_client/lib/src/functions_client.dart Makes functions client implementable only.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

/// * [SharedPreferencesLocalStorage], that implements SharedPreferences as
/// storage method
abstract class LocalStorage {
abstract interface class LocalStorage {
The core public types now declare their subtyping intent instead of
leaving every class open by default.

The clients and the request builders are `interface class`, so they can
no longer be extended but can still be implemented, which is what
mockito and mocktail need: `SupabaseClient`, `GoTrueClient`,
`PostgrestClient`, `PostgrestBuilder`, `RealtimeClient`,
`FunctionsClient`, `SupabaseStorageClient` and `StorageFileApi`.

`LocalStorage` and `GotrueAsyncStorage`, the two abstractions consumers
plug their own storage into, are `abstract interface class`.

`AuthState` is a `final class`, and the internal `GotrueFetch`, `Fetch`
and `SupabaseAuth` helpers are `final class`.

The `AuthException` hierarchy is deliberately left alone; it is being
restructured separately.

The realtime test stubs that extended `RealtimeClient` are gone. The
setAuth channel tests use a mocktail mock of the socket, and the socket
tests register their mock channels on `RealtimeClient.channels` directly
instead of intercepting `channel()`.
@spydon
spydon force-pushed the lukasklingsbo/sdk-819-breakingsdk-apply-dart-3-class-modifiers branch from f0d71b7 to e61c3dc Compare August 11, 2026 07:42

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
MIGRATION.md (1)

330-334: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Document the AuthException hierarchy change

MIGRATION.md does not mention the base class change for AuthException or the final class changes for its subclasses. Add the breaking-change guidance and migration path.

🤖 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 `@MIGRATION.md` around lines 330 - 334, Update the class-modifier migration
section in MIGRATION.md to explicitly document that AuthException is now a base
class and its subclasses are final classes. Describe the resulting restriction
on extending or implementing these types and provide the migration path for
consumers that currently subtype them.
🤖 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 `@MIGRATION.md`:
- Around line 350-367: Clarify the LoggingSupabaseClient example as partial
composition that cannot substitute for SupabaseClient, since it only forwards
from. State that callers requiring SupabaseClient must use a complete delegating
wrapper or retain the original client type.

---

Nitpick comments:
In `@MIGRATION.md`:
- Around line 330-334: Update the class-modifier migration section in
MIGRATION.md to explicitly document that AuthException is now a base class and
its subclasses are final classes. Describe the resulting restriction on
extending or implementing these types and provide the migration path for
consumers that currently subtype them.
🪄 Autofix

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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 93366a1b-45bb-4ee8-a878-4f4cb53f2a8a

📥 Commits

Reviewing files that changed from the base of the PR and between f0d71b7 and e61c3dc.

📒 Files selected for processing (1)
  • MIGRATION.md

Comment thread MIGRATION.md
Comment on lines +350 to +367
If you were extending one of these to change its behaviour, wrap it instead:

```dart
// Before
class LoggingSupabaseClient extends SupabaseClient {
LoggingSupabaseClient(super.url, super.key);
}

// After
class LoggingSupabaseClient {
LoggingSupabaseClient(this._inner);

final SupabaseClient _inner;

SupabaseQueryBuilder from(String table) {
print('from($table)');
return _inner.from(table);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- MIGRATION.md excerpt ---'
sed -n '320,390p' MIGRATION.md
printf '%s\n' '--- SupabaseClient declarations/usages ---'
rg -n --glob '*.dart' 'class SupabaseClient|abstract class SupabaseClient|interface class SupabaseClient|final class SupabaseClient|SupabaseClient\(' .
printf '%s\n' '--- wrapper example references ---'
rg -n 'LoggingSupabaseClient|wrap it instead|SupabaseClient' MIGRATION.md

Repository: supabase/supabase-flutter

Length of output: 5784


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- SupabaseClient public API ---'
sed -n '45,190p' packages/supabase/lib/src/supabase_client.dart
printf '%s\n' '--- Static contract check for the migration example ---'
python3 - <<'PY'
from pathlib import Path
import re

doc = Path("MIGRATION.md").read_text()
match = re.search(
    r"class LoggingSupabaseClient \{\n(?P<body>.*?)\n\}",
    doc,
    re.S,
)
assert match, "LoggingSupabaseClient wrapper not found"
body = match.group("body")
methods = re.findall(r"^\s*(?:[A-Za-z_<>,?]+\s+)+([A-Za-z_]\w*)\s*\(", body, re.M)
fields = re.findall(r"^\s*final\s+[^;]+;", body, re.M)
print("wrapper_has_implements:", bool(re.search(r"\bimplements\s+SupabaseClient\b", match.group(0))))
print("wrapper_methods:", methods)
print("wrapper_fields:", fields)
PY

Repository: supabase/supabase-flutter

Length of output: 5094


Clarify the wrapper’s type contract.

LoggingSupabaseClient does not implement SupabaseClient and forwards only from, so it cannot replace a SupabaseClient where that type is required. State that this is a partial composition example, or delegate the complete public API.

🤖 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 `@MIGRATION.md` around lines 350 - 367, Clarify the LoggingSupabaseClient
example as partial composition that cannot substitute for SupabaseClient, since
it only forwards from. State that callers requiring SupabaseClient must use a
complete delegating wrapper or retain the original client type.

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.

3 participants