Skip to content

feat: ambient per-call options (the caller-side half of IpcContext) - #156

Open
eduard-dumitru wants to merge 3 commits into
masterfrom
feat/ambient-call-options
Open

eduard-dumitru wants to merge 3 commits into
masterfrom
feat/ambient-call-options

Conversation

@eduard-dumitru

@eduard-dumitru eduard-dumitru commented Sep 14, 2026

Copy link
Copy Markdown
Collaborator

Follow-up to #127. That PR gave the receiver a way to reach the peer without a Message parameter (IpcContext.Current), so a contracts assembly need not reference UiPath.Ipc. The caller side had no equivalent: the only way to set a per-call deadline is to pass a Message argument — which puts the transport type back into the signature and costs a wire slot.

This adds the missing half.

using (IpcCallOptions.With(TimeSpan.FromSeconds(30)))
    await proxy.SendLogBatch(jobId, logs);      // POCO contract, no Message
with call_options(request_timeout=30):
    await proxy.SendLogBatch(job_id, batch)

Precedence

client default → ambient → explicit Message argument (still wins).

The wire is unchanged

Request.TimeoutInSeconds already carries the value and peers already honour it (Request.GetTimeout). This is purely a client-API addition — no protocol change, no version boundary between peers, only a package bump.

Why now

uipath-python streams job logs to the handler over a shared contract. Bounding those sends currently requires a trailing Message, which forced UiPath.Ipc into GenericExecutors.Contracts — exactly the dependency #127 existed to avoid.

Tests

  • .NET: 5 new (nesting, restore-on-throw, flow isolation, and a real round trip proving a POCO contract call is bounded — and that the same call is unbounded without the scope). Full suite green: 103 net6.0 / 101 net461.
  • Python: 8 new (contextvar semantics + wire-level assertions that TimeoutInSeconds carries the ambient, and that an explicit Message overrides it). Full client suite: 107 passed.
  • Both wirings mutation-checked: disabling the ambient fails exactly the new round-trip/wire test and nothing else.

TypeScript — now included, Node only

Added in a second commit. Same shape as the other two halves:

withRequestTimeout(TimeSpan.fromSeconds(30), async () => {
    await proxy.SendLogBatch(jobId, logs);   // POCO contract, no Message
});

Deliberately Node-only. Tracking "the current asynchronous flow" needs AsyncLocalStorage, and browsers have no equivalent. So:

  • callOptions / withRequestTimeout are exported from the node entry point only — importing them from the web build is a compile error, not a silent no-op.
  • The read side (AmbientCallOptions) has to live in std, because RpcRequestFactory is shared code and cannot import from a platform entry point. It is a one-function seam with a comment saying so; with no provider installed it returns undefined and the web build behaves exactly as before. Both tsconfig.node.json and tsconfig.web.json typecheck clean.

We have no web end-users today, so the asymmetry costs nothing; if that ever changes, the seam is the place to add a browser implementation (TC39 AsyncContext, when it lands).

Verification: the new spec (test/node/core/CallOptions.test.ts) passes — 5 of 5 — and is mutation-checked: making AmbientCallOptions.current() always return undefined fails 3 of them.

It has to be run with the spec list narrowed, though, because the jasmine suite does not compile on master, independent of this PR:

test/std/bcl/AggregateDisposable.test.ts(73,63): error TS2345: 'number | boolean | Mock | {}' is not assignable to 'IDisposable'
test/std/bcl/AggregateDisposable.test.ts(153,34): error TS7006: Parameter 'x' implicitly has an 'any' type

Reproduced on a pristine origin/master worktree after a clean npm ci — same two errors, same lines. tsconfig.jasmine.json sets no include, so it compiles test/ as well, which tsconfig.node.json never does; that is why the build is green while the test run is not. Not fixed here — flagging it as a separate problem worth its own ticket.

IpcContext (#127) let a contract reach the peer without a Message parameter, so
that a contracts assembly need not reference UiPath.Ipc. The caller side had no
equivalent: a per-call deadline could only be set by passing a Message, which
puts the transport type back in the signature and costs a wire slot.

IpcCallOptions.With(timeout) in .NET and call_options(timeout) in Python apply a
deadline to every call on the current async flow. Precedence is client default,
then ambient, then an explicit Message argument, which still wins.

The wire is unchanged: Request.TimeoutInSeconds already carries the value and
peers already honour it, so this is a client API addition, not a protocol change.

TypeScript is not included -- see the PR description for the open question.
Same shape as the .NET and Python halves: a scope that applies a deadline to
every call made inside it, so a contract that takes no Message can still be
bounded. Precedence is explicit Message, then ambient, then the client default.

Node only, deliberately. Tracking "the current asynchronous flow" needs
AsyncLocalStorage and browsers have no equivalent, so `callOptions` is exported
from the node entry point alone -- importing it from the web build is a compile
error rather than a silent no-op. The read side sits in std because
RpcRequestFactory is shared code and cannot import from a platform entry point;
with no provider installed it returns undefined and the web build behaves
exactly as before.
The two new tests read the written frame after one `sleep(0)`, which is enough
on a fast machine and not on a CI agent -- both failed on Linux py310/py311 with
an IndexError off `frames[0]`. Poll until the frame lands, as the sibling test
in this file already does, and fail with a message that says what happened.
@eduard-dumitru
eduard-dumitru force-pushed the feat/ambient-call-options branch from fa658dd to 9c9161a Compare September 15, 2026 22:49
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