Skip to content

Every generated client carries a private copy of the runtime, so a second module in one app duplicates it — and in Dart the two cannot be imported together at all #110

Description

@jryannel

Each sqlb generate run emits a self-contained client: the row types and operations for that project's tables, preceded by a full private copy of the runtime — Collection, Page, Problem, ProblemDetail, ApiRequest, Transport, the filter-grammar encoder, isProblem/allowedFor.

That is exactly right for one module. A modular monolith has many, all serving one frontend, and the second module is where it breaks.

Measured, on the second module in one app

Two modules in one app (family, then tutor), emitting into the same web/src/api and mobile/lib/api:

file A file B identical leading lines
TypeScript client.gen.ts 457 tutor.client.gen.ts 587 287
Dart client.gen.dart 1563 tutor.client.gen.dart 1893 1187

The runtime is byte-identical in both — it is the same generator emitting the same template. A third module makes it three copies.

I had to set TSClientFile / TSQueriesFile / DartFile on the second module, because the defaults (client.gen.ts, queries.gen.ts, client.gen.dart) are per-project constants and the second project silently overwrites the first's files in a shared output directory. Those options exist, so this part has an answer — but the answer is a naming convention the adopter has to invent, and it does not address what follows.

In Dart it is a compile error, not duplication

Dart is nominally typed, so the two Page<T> are two unrelated classes. One file cannot consume both clients:

import 'client.gen.dart';
import 'tutor.client.gen.dart';

Future<void> probe(Transport t) async {
  final Page<Child> kids = await listChildren(t);
  final Page<Session> sessions = await listSessions(t);
}
error - The name 'Transport' is defined in the libraries 'client.gen.dart' and
        'tutor.client.gen.dart'. Try using 'as prefix' for one of the import
        directives, or hiding the name from all but one of the imports. - ambiguous_import
error - The name 'Page' is defined in the libraries ... - ambiguous_import

as prefix makes it compile and does not make it work:

  • family.Page<T> and tutor.Page<T> are distinct types, so no shared widget, pager, or error boundary can accept both. An infinite-scroll list widget — the thing the Dart cursor pager exists to make possible — has to be written once per module.
  • The hand-written http_transport.dart returns Transport. There are now two Transport typedefs, so the app's single transport (base URL, bearer token, 401 policy, ApiException) has to be duplicated per module, and each copy catches only its own module's Problem.

TypeScript escapes the compile error because it is structurally typed — the duplicate Page/Problem interfaces interoperate. It still ships N copies of the encoder and the runtime to the browser, and still asks the app to wire one Transport per module.

Why a convention cannot fix it

The generated file's own header says it imports nothing, and that is a real design property — no pub package, no bundler assumption. But "imports nothing" and "there are several of these" are only compatible if the runtime is emitted once, separately, and the per-module files import that.

Suggested shape

Emit the runtime as its own file, once per output directory, and have each module's client import it:

web/src/api/runtime.gen.ts        ← Page, Problem, Transport, encodeListQuery, …
web/src/api/family.gen.ts         ← imports './runtime.gen'
web/src/api/tutor.gen.ts          ← imports './runtime.gen'
mobile/lib/api/runtime.gen.dart
mobile/lib/api/family.gen.dart    ← export 'runtime.gen.dart' or import it
mobile/lib/api/tutor.gen.dart

The runtime is derived from nothing module-specific, so a second module writing it is a no-op and codegen.Check stays meaningful. A RuntimeFile/RuntimeImport option would let an adopter point several projects at one shared file without the generator needing to know they are related.

The alternative — one generator invocation over a registry containing every module's tables — is not available to this codebase: the whole point of the per-module registry is that modules do not import each other, so nothing is entitled to hold all of them at once.

Found taking a second module schema-first in the multi-app adoption (the subject-mono of docs/review-adoption-multi-app.md). Related: #20 (Dart emitter).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions