Skip to content

feat: add @nexus.internal for interfaces that only an SDK calls - #97

Closed
moetemp wants to merge 32 commits into
moe/up/error-enumsfrom
moe/up/internal-interfaces
Closed

moetemp wants to merge 32 commits into
moe/up/error-enumsfrom
moe/up/internal-interfaces

Conversation

@moetemp

@moetemp moetemp commented Oct 28, 2026 •

Copy link
Copy Markdown
Owner

What changed?

This PR adds @nexus.internal, which marks an interface that only an SDK calls, so its package exports nothing.

  • Python's __init__.py exports nothing, but keeps the operation registry for serialization contexts.
  • TypeScript's package has no index.ts, and each top-level declaration is @internal, so stripInternal removes it from the published types.
  • Each .NET type is internal. Go output doesn't change, since a caller writes it under internal/, and Java ignores the directive with a warning.

A new internal-service sample shows it in the four languages.

It's based on #95, and it's on the fork for review before an upstream PR.

Why?

A service of an SDK's runtime isn't for its users, but the generated packages export it next to the user API. The Python module names stay as they are, since the generated imports between packages name them.

How did you test it?

Link to a test plan if any -

  • Unit Tests
  • Staging
  • End to End Tests

The internal-service tests check the exports in each language.

The sample now generates what Brook generates, byte for byte from its WIT. No SDK has the Stream
protos yet, so the sample projects ship their bindings. Today only .NET generates the service,
and each fix turns its part on.
Python and TypeScript panic on it, so their outputs keep their previous content. .NET types each
case as the raw proto message, not as its record.
Variant planning kept such a case as the raw proto message. Python and TypeScript then
panicked on a message conversion that wasn't model-shaped, and .NET typed the case as the
proto class. The case now plans as the record, as a record field already does. Brook's stream
service now generates for TypeScript, and its .NET read item holds the records.
nexgen maps every temporal/... proto to a temporalio module. The Stream protos aren't in
temporalio yet, so the service's imports don't resolve, and basedpyright skips it.
`@nexus.proto` takes `python-import`, the Python module of the bound message or enum, as
`typescript-import` names the TypeScript module. A proto that the installed temporalio doesn't
have yet needs it. The stream sample imports the Stream protos from its own streamprotos package,
so the Python checks now cover it.
A proto-backed request holds a 64-bit integer as a `Long`, but a resource method took a number.
A Brook offset doesn't fit in a number. A resource field, method param or result is now a `Long`
when the method's request is proto-backed, so it goes into the request as it is. Brook's stream
service now type-checks.
The setup of a @nexus.flatten-in-api record always took the indent of a module
function. In a method of the client of a service with no endpoint, the code
then left the method body, and services.py didn't parse.
Before planning, a proto-backed record is named by its proto type, so the walk
for a resource's type parameters stopped there. nexgen then wrote generic models
for a resource class that isn't generic, and the code didn't compile.
…output.

The request's email field has the name of the user's own email, with another type. Today the
user's email fills it.
A resource method filled each request field from the resource field or param of the same
name, whatever its type. So the user's own email went into the notify request's email flag.
A source now binds only when its type is the field's type.
Python's caller method renders a bare `*` with no keyword parameter after it, so its services.py
doesn't parse and the Python output keeps its previous content.
Python refuses a bare `*` with no parameter after it. An unpacked operation with an empty request
rendered one, in the caller of a service with no endpoint and in an operation function. The
user-service caller now generates its `notify_all` method.
Python's caller method takes the docstring indent of a module function, so its services.py
doesn't parse and the Python output keeps its previous content.
The caller of a service with no endpoint took the docstring indent of a module function, so a
documented operation broke the module. The user-service caller now generates with the doc of
`notify_all`.
Proto3 enums are open. Python fails the whole message on a Stream status that it doesn't know.
TypeScript and .NET pass the number through, and their tests pin it.
Proto3 enums are open, but a generated Python enum raised `ValueError` on a number that it
didn't know, so the whole message failed. A newer server's Producer kind stopped an old reader's
poll loop. The enum now makes a member named `Unknown<number>` with that value, as TypeScript and
.NET pass the number through.
Its System Nexus interceptors name the generic models without type arguments. basedpyright
refuses the Python one. The TypeScript one names a type parameter that isn't in scope, so `tsc`
fails.
An interceptor serves every type argument of an operation's models. Python named a generic model
with no type arguments, which basedpyright refuses. TypeScript named the operation's own type
parameter, which isn't in scope. Both now erase them, as the service definition does.
The operation renderers take a `PythonTarget` and start each line at its indent. The client target
can then render the same operations as methods, with no thread-local flag and no re-indented text.
Only services with an endpoint reach these renderers, so their branches for a service with no
endpoint are gone. The output doesn't change.
`--targets client` writes `client.py`, with a client extension for each service. Its methods start
each operation through `_create_nexus_client` and return the result. One resource class serves
both targets, through an operations object that each target supplies.
An operation with `@nexus.poll` gets an `iter_<name>` iterator in each generated target, and so
does each resource method that binds it. A support helper turns a request and its response into
the next request, or into `None` to stop. `target` limits the iterator to one target, and nexgen
refuses the directive where no iterator can exist.
A resource with `@nexus.type-parameter "<alias>"` is generic in Python, TypeScript and .NET. Its
operations take a runtime type where the request can't give one, so a response decodes its generic
values.
`@nexus.definition` names an SDK type, such as a Stream definition, that gives the type arguments
of a generic resource. An operation that returns the resource takes it in place of runtime types.
Each client call and resource method takes `rpc_metadata` and `rpc_timeout`, and Workflow code
refuses them. An `async func` operation also gets `start_<name>`, which returns the handle.
A field takes a source or an api-omit for the Workflow or the client target. Target code runs a
targeted source, so a model built outside a Workflow never calls a Workflow helper.
`@nexus.flatten-in-api fields="..." fill="<field>=<sibling>"` on a field makes the call take the
named fields of its record, and copies sibling fields into the others. The Stream create takes the
backend of its ref this way, and the ref copies the id.
Each resource gets a factory on the client and in the Workflow package, which builds a handle with
no call. A resource with one proto-backed record field passes as that record, so a handle can be an
argument or a result.
A resource method can take the items of its last list as arguments, take a one-field record as its
field, and return one field of the response. The Stream's publish takes values and returns the
last offset.
`@nexus.poll items=` makes the iterator yield the items of each page, and `resume=` names the helper
that resumes after one item. The iterator's `next_request` lets a caller save its place.
`@nexus.close` marks the method that ends a resource. Python closes it at a block's end with the
block's error, TypeScript and .NET close it at the end of `await using`.
`@nexus.error` on an enum makes a class per case of a failed call, under one base. Python's
operations objects raise the class of a failed call's reason from the original error.
The package of an internal service exports nothing. Python's `__init__.py` has no exports,
TypeScript has no `index.ts` and marks each declaration `@internal`, and .NET types are `internal`.
@moetemp
moetemp force-pushed the moe/up/error-enums branch from dbd78c4 to f867378 Compare October 29, 2026 04:00
@moetemp
moetemp force-pushed the moe/up/internal-interfaces branch from c88f90e to bd8e2ef Compare October 29, 2026 04:00
@moetemp
moetemp force-pushed the moe/up/error-enums branch from f867378 to 12e53e5 Compare October 31, 2026 13:08
@moetemp

moetemp commented Oct 31, 2026

Copy link
Copy Markdown
Owner Author

Closing. The Stream writer stays hand-written in each SDK, so nothing uses @nexus.internal.

@moetemp moetemp closed this Oct 31, 2026
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.

1 participant