[codex] Refactor client runtime Effect services#3200
Conversation
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
|
🚀 Expo continuous deployment is ready!
|
ApprovabilityVerdict: Approved Mechanical refactoring that converts to namespace exports and restructures error types for better type safety. All changes update import patterns and type references without altering runtime behavior. No code changes detected at You can customize Macroscope's approvability policy. Learn more. |
c7e149a to
7b181b2
Compare
Dismissing prior approval to re-evaluate 7b181b2
7b181b2 to
9052877
Compare
Dismissing prior approval to re-evaluate 9052877
9052877 to
5aea238
Compare
Dismissing prior approval to re-evaluate 5aea238
5aea238 to
80378d3
Compare
80378d3 to
92ac971
Compare
Dismissing prior approval to re-evaluate 92ac971
721f98f to
5360201
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Relay config errors mapped retryable
- Replaced the default fallthrough branch in mapManagedRelayError with explicit case arms that map ManagedRelayUrlInvalidError to ConnectionBlockedError(configuration) and all DPoP/token errors to ConnectionBlockedError(authentication), preventing permanent failures from being treated as retryable transient errors.
Or push these changes by commenting:
@cursor push 2ffc3bc303
Preview (2ffc3bc303)
diff --git a/apps/mobile/src/connection/runtime.ts b/apps/mobile/src/connection/runtime.ts
--- a/apps/mobile/src/connection/runtime.ts
+++ b/apps/mobile/src/connection/runtime.ts
@@ -1,4 +1,4 @@
-import { connectionLayer as clientConnectionLayer } from "@t3tools/client-runtime/connection";
+import { Connection } from "@t3tools/client-runtime/connection";
import * as Layer from "effect/Layer";
import { Atom } from "effect/unstable/reactivity";
@@ -9,8 +9,19 @@
Layer.provide(runtimeContextLayer),
);
-export const connectionLayer = clientConnectionLayer.pipe(
+type ConnectionLayerSource =
+ | typeof Connection.layer
+ | typeof runtimeContextLayer
+ | typeof providedConnectionPlatformLayer;
+
+export const connectionLayer: Layer.Layer<
+ Layer.Success<ConnectionLayerSource>,
+ Layer.Error<ConnectionLayerSource>
+> = Connection.layer.pipe(
Layer.provideMerge(Layer.mergeAll(runtimeContextLayer, providedConnectionPlatformLayer)),
);
-export const connectionAtomRuntime = Atom.runtime(connectionLayer);
+export const connectionAtomRuntime: Atom.AtomRuntime<
+ Layer.Success<typeof connectionLayer>,
+ Layer.Error<typeof connectionLayer>
+> = Atom.runtime(connectionLayer);
diff --git a/apps/mobile/src/connection/storage.ts b/apps/mobile/src/connection/storage.ts
--- a/apps/mobile/src/connection/storage.ts
+++ b/apps/mobile/src/connection/storage.ts
@@ -1,5 +1,6 @@
import {
ConnectionPersistenceError,
+ type ConnectionPersistenceOperation,
ConnectionRegistrationStore,
ConnectionTargetStore,
EnvironmentCacheStore,
@@ -62,19 +63,10 @@
});
}
-function shellPersistenceError(
- operation:
- | "load-shell"
- | "save-shell"
- | "load-thread"
- | "save-thread"
- | "remove-thread"
- | "clear-environment",
- cause: unknown,
-) {
+function shellPersistenceError(operation: ConnectionPersistenceOperation, cause: unknown) {
return new ConnectionPersistenceError({
operation,
- message: `Could not ${operation.replaceAll("-", " ")}: ${String(cause)}`,
+ cause,
});
}
@@ -118,12 +110,12 @@
});
function targetPersistenceError(
- operation: "list-targets" | "register-connection" | "remove-connection",
+ operation: ConnectionPersistenceOperation,
error: ConnectionTransientError,
) {
return new ConnectionPersistenceError({
operation,
- message: error.message,
+ cause: error,
});
}
diff --git a/apps/mobile/src/features/agent-awareness/liveActivityPreferences.test.ts b/apps/mobile/src/features/agent-awareness/liveActivityPreferences.test.ts
--- a/apps/mobile/src/features/agent-awareness/liveActivityPreferences.test.ts
+++ b/apps/mobile/src/features/agent-awareness/liveActivityPreferences.test.ts
@@ -2,7 +2,7 @@
import { describe, expect, it } from "@effect/vitest";
import * as Effect from "effect/Effect";
import type { EnvironmentId } from "@t3tools/contracts";
-import { ManagedRelayClient } from "@t3tools/client-runtime/relay";
+import { ManagedRelay } from "@t3tools/client-runtime/relay";
import * as Layer from "effect/Layer";
import { HttpClient } from "effect/unstable/http";
@@ -35,7 +35,7 @@
};
const testLayer = Layer.mergeAll(
- Layer.succeed(ManagedRelayClient, null as never),
+ Layer.succeed(ManagedRelay.ManagedRelayClient, null as never),
Layer.succeed(
HttpClient.HttpClient,
HttpClient.make(() => Effect.die("unexpected HTTP request")),
diff --git a/apps/mobile/src/features/agent-awareness/liveActivityPreferences.ts b/apps/mobile/src/features/agent-awareness/liveActivityPreferences.ts
--- a/apps/mobile/src/features/agent-awareness/liveActivityPreferences.ts
+++ b/apps/mobile/src/features/agent-awareness/liveActivityPreferences.ts
@@ -1,6 +1,6 @@
import * as Effect from "effect/Effect";
import { HttpClient } from "effect/unstable/http";
-import { ManagedRelayClient } from "@t3tools/client-runtime/relay";
+import { ManagedRelay } from "@t3tools/client-runtime/relay";
import type { SavedRemoteConnection } from "../../lib/connection";
import { savePreferencesPatch } from "../../lib/storage";
@@ -11,7 +11,7 @@
readonly enabled: boolean;
readonly clerkToken: string | null;
readonly connections: ReadonlyArray<SavedRemoteConnection>;
-}): Effect.Effect<void, unknown, HttpClient.HttpClient | ManagedRelayClient> {
+}): Effect.Effect<void, unknown, HttpClient.HttpClient | ManagedRelay.ManagedRelayClient> {
return Effect.gen(function* () {
yield* Effect.tryPromise({
try: () => savePreferencesPatch({ liveActivitiesEnabled: input.enabled }),
diff --git a/apps/mobile/src/features/agent-awareness/remoteRegistration.test.ts b/apps/mobile/src/features/agent-awareness/remoteRegistration.test.ts
--- a/apps/mobile/src/features/agent-awareness/remoteRegistration.test.ts
+++ b/apps/mobile/src/features/agent-awareness/remoteRegistration.test.ts
@@ -9,7 +9,7 @@
import * as Exit from "effect/Exit";
import * as Layer from "effect/Layer";
import { FetchHttpClient } from "effect/unstable/http";
-import { type ManagedRelayClient } from "@t3tools/client-runtime/relay";
+import { ManagedRelay } from "@t3tools/client-runtime/relay";
import type { EnvironmentId } from "@t3tools/contracts";
import { verifyDpopProof } from "@t3tools/shared/dpop";
@@ -158,7 +158,7 @@
}
idlePasses = 0;
const exit = yield* Effect.exit(
- pending.operation as Effect.Effect<unknown, unknown, ManagedRelayClient>,
+ pending.operation as Effect.Effect<unknown, unknown, ManagedRelay.ManagedRelayClient>,
);
yield* Effect.sync(() => {
pending.resolve(exit);
diff --git a/apps/mobile/src/features/agent-awareness/remoteRegistration.ts b/apps/mobile/src/features/agent-awareness/remoteRegistration.ts
--- a/apps/mobile/src/features/agent-awareness/remoteRegistration.ts
+++ b/apps/mobile/src/features/agent-awareness/remoteRegistration.ts
@@ -9,7 +9,7 @@
type RelayLiveActivityRegistrationRequest,
} from "@t3tools/contracts/relay";
import { findErrorTraceId } from "@t3tools/client-runtime/errors";
-import { ManagedRelayClient } from "@t3tools/client-runtime/relay";
+import { ManagedRelay } from "@t3tools/client-runtime/relay";
import {
isAtomCommandInterrupted,
settleAsyncResult,
@@ -175,7 +175,7 @@
function registerDeviceWithRelay(
body: RelayDeviceRegistrationRequest,
expectedGeneration: number,
-): Effect.Effect<void, unknown, ManagedRelayClient> {
+): Effect.Effect<void, unknown, ManagedRelay.ManagedRelayClient> {
return Effect.gen(function* () {
if (expectedGeneration !== deviceRegistrationGeneration) {
logRegistrationDebug("device registration cancelled before relay request", {
@@ -198,7 +198,7 @@
return;
}
- const client = yield* ManagedRelayClient;
+ const client = yield* ManagedRelay.ManagedRelayClient;
logRegistrationDebug("relay device registration request started", {
expectedGeneration,
});
@@ -215,7 +215,7 @@
function unregisterDeviceWithRelay(input: {
readonly deviceId: string;
readonly tokenProvider: () => Promise<string | null>;
-}): Effect.Effect<void, unknown, ManagedRelayClient> {
+}): Effect.Effect<void, unknown, ManagedRelay.ManagedRelayClient> {
return Effect.gen(function* () {
if (!readRelayConfig()) return;
const token = yield* Effect.tryPromise({
@@ -227,7 +227,7 @@
return;
}
- const client = yield* ManagedRelayClient;
+ const client = yield* ManagedRelay.ManagedRelayClient;
yield* client.unregisterDevice({
clerkToken: token,
deviceId: input.deviceId,
@@ -237,7 +237,7 @@
function registerLiveActivityWithRelay(
body: RelayLiveActivityRegistrationRequest,
-): Effect.Effect<boolean, unknown, ManagedRelayClient> {
+): Effect.Effect<boolean, unknown, ManagedRelay.ManagedRelayClient> {
return Effect.gen(function* () {
if (!readRelayConfig()) return false;
const token = yield* relayToken;
@@ -246,7 +246,7 @@
return false;
}
- const client = yield* ManagedRelayClient;
+ const client = yield* ManagedRelay.ManagedRelayClient;
yield* client.registerLiveActivity({
clerkToken: token,
payload: body,
@@ -274,7 +274,7 @@
}
function runRegistrationInBackground(
- operation: Effect.Effect<unknown, unknown, ManagedRelayClient>,
+ operation: Effect.Effect<unknown, unknown, ManagedRelay.ManagedRelayClient>,
context: string,
): void {
void (async () => {
@@ -370,7 +370,7 @@
function registerDevice(
input: DeviceRegistrationInput = {},
expectedGeneration = deviceRegistrationGeneration,
-): Effect.Effect<void, unknown, ManagedRelayClient> {
+): Effect.Effect<void, unknown, ManagedRelay.ManagedRelayClient> {
return Effect.gen(function* () {
if (!canRegisterRemoteLiveActivities()) {
logRegistrationDebug("device registration skipped; platform does not support it");
@@ -411,7 +411,7 @@
function registerDeviceForCurrentUser(
pushToStartToken?: string,
-): Effect.Effect<void, unknown, ManagedRelayClient> {
+): Effect.Effect<void, unknown, ManagedRelay.ManagedRelayClient> {
return registerDevice(pushToStartToken ? { pushToStartToken } : undefined);
}
@@ -485,7 +485,7 @@
export function refreshAgentAwarenessRegistration(): Effect.Effect<
void,
never,
- ManagedRelayClient
+ ManagedRelay.ManagedRelayClient
> {
return registerDeviceForCurrentUser().pipe(
Effect.catch((error) =>
@@ -515,7 +515,7 @@
export function unregisterAgentAwarenessDeviceForCurrentUser(
tokenProvider: () => Promise<string | null>,
-): Effect.Effect<void, never, ManagedRelayClient> {
+): Effect.Effect<void, never, ManagedRelay.ManagedRelayClient> {
return Effect.gen(function* () {
const deviceId = yield* Effect.tryPromise({
try: () => loadAgentAwarenessDeviceId(),
@@ -536,7 +536,7 @@
export function registerLiveActivityPushToken(input: {
readonly activity: LiveActivity<AgentActivityProps>;
-}): Effect.Effect<boolean, unknown, ManagedRelayClient> {
+}): Effect.Effect<boolean, unknown, ManagedRelay.ManagedRelayClient> {
return Effect.gen(function* () {
if (!canRegisterRemoteLiveActivities()) {
return false;
@@ -588,7 +588,7 @@
function registerLiveActivityPushTokenValue(input: {
readonly activityPushToken: string;
-}): Effect.Effect<boolean, unknown, ManagedRelayClient> {
+}): Effect.Effect<boolean, unknown, ManagedRelay.ManagedRelayClient> {
return Effect.gen(function* () {
const deviceId = yield* Effect.tryPromise({
try: () => loadOrCreateAgentAwarenessDeviceId(),
@@ -624,7 +624,7 @@
export function refreshActiveLiveActivityRemoteRegistration(): Effect.Effect<
void,
never,
- ManagedRelayClient
+ ManagedRelay.ManagedRelayClient
> {
return Effect.gen(function* () {
if (!canRegisterRemoteLiveActivities() || !relayTokenProvider) {
diff --git a/apps/mobile/src/features/cloud/CloudAuthProvider.tsx b/apps/mobile/src/features/cloud/CloudAuthProvider.tsx
--- a/apps/mobile/src/features/cloud/CloudAuthProvider.tsx
+++ b/apps/mobile/src/features/cloud/CloudAuthProvider.tsx
@@ -1,6 +1,6 @@
import { ClerkProvider, useAuth } from "@clerk/expo";
import { tokenCache } from "@clerk/expo/token-cache";
-import { ManagedRelayClient, setManagedRelaySession } from "@t3tools/client-runtime/relay";
+import { ManagedRelay, setManagedRelaySession } from "@t3tools/client-runtime/relay";
import {
reportAtomCommandResult,
settleAsyncResult,
@@ -22,7 +22,7 @@
function resetManagedRelayTokenCache() {
return settleAsyncResult(() =>
runtime.runPromiseExit(
- ManagedRelayClient.pipe(Effect.flatMap((client) => client.resetTokenCache)),
+ ManagedRelay.ManagedRelayClient.pipe(Effect.flatMap((client) => client.resetTokenCache)),
),
);
}
diff --git a/apps/mobile/src/features/cloud/linkEnvironment.test.ts b/apps/mobile/src/features/cloud/linkEnvironment.test.ts
--- a/apps/mobile/src/features/cloud/linkEnvironment.test.ts
+++ b/apps/mobile/src/features/cloud/linkEnvironment.test.ts
@@ -4,11 +4,7 @@
import * as Layer from "effect/Layer";
import { EnvironmentId } from "@t3tools/contracts";
import { RelayMobileClientId } from "@t3tools/contracts/relay";
-import {
- managedRelayClientLayer,
- ManagedRelayClient,
- ManagedRelayDpopSigner,
-} from "@t3tools/client-runtime/relay";
+import { ManagedRelay } from "@t3tools/client-runtime/relay";
import { remoteHttpClientLayer } from "@t3tools/client-runtime/rpc";
import { HttpClient } from "effect/unstable/http";
@@ -62,8 +58,8 @@
Effect.succeed(`dpop:${input.method}:${input.url}`),
);
const testDpopSignerLayer = Layer.succeed(
- ManagedRelayDpopSigner,
- ManagedRelayDpopSigner.of({
+ ManagedRelay.ManagedRelayDpopSigner,
+ ManagedRelay.ManagedRelayDpopSigner.of({
thumbprint: Effect.succeed("client-proof-key-thumbprint"),
createProof: (input) => createProofMock(input),
}),
@@ -73,7 +69,7 @@
const httpClientLayer = remoteHttpClientLayer((input, init) => globalThis.fetch(input, init));
return Layer.mergeAll(
httpClientLayer,
- managedRelayClientLayer({
+ ManagedRelay.layer({
relayUrl: "https://relay.example.test",
clientId: RelayMobileClientId,
}).pipe(Layer.provideMerge(testDpopSignerLayer), Layer.provide(httpClientLayer)),
@@ -81,7 +77,11 @@
}
const withCloudServices = <A, E>(
- effect: Effect.Effect<A, E, HttpClient.HttpClient | ManagedRelayClient | ManagedRelayDpopSigner>,
+ effect: Effect.Effect<
+ A,
+ E,
+ HttpClient.HttpClient | ManagedRelay.ManagedRelayClient | ManagedRelay.ManagedRelayDpopSigner
+ >,
) => effect.pipe(Effect.provide(cloudClientLayer()));
function validLinkProof() {
diff --git a/apps/mobile/src/features/cloud/linkEnvironment.ts b/apps/mobile/src/features/cloud/linkEnvironment.ts
--- a/apps/mobile/src/features/cloud/linkEnvironment.ts
+++ b/apps/mobile/src/features/cloud/linkEnvironment.ts
@@ -25,11 +25,7 @@
import { exchangeRemoteDpopAccessToken } from "@t3tools/client-runtime/authorization";
import { fetchRemoteEnvironmentDescriptor } from "@t3tools/client-runtime/environment";
import { findErrorTraceId } from "@t3tools/client-runtime/errors";
-import {
- ManagedRelayClient,
- type ManagedRelayClientError,
- ManagedRelayDpopSigner,
-} from "@t3tools/client-runtime/relay";
+import { ManagedRelay } from "@t3tools/client-runtime/relay";
import { makeEnvironmentHttpApiClient } from "@t3tools/client-runtime/rpc";
import { authClientMetadata } from "../../lib/authClientMetadata";
@@ -156,13 +152,15 @@
}
function decodedRelayClientError(message: string) {
- return (cause: ManagedRelayClientError) => {
- const relayError = cause.relayError;
+ return (cause: ManagedRelay.ManagedRelayClientError) => {
+ const relayError =
+ cause._tag === "ManagedRelayRequestFailedError" ? cause.relayError : undefined;
+ const traceId = cause._tag === "ManagedRelayRequestFailedError" ? cause.traceId : undefined;
const detail = relayError ? relayProtectedErrorMessage(relayError) : null;
return new CloudEnvironmentLinkError({
message: detail ? `${message}: ${detail}` : message,
cause,
- ...(cause.traceId ? { traceId: cause.traceId } : {}),
+ ...(traceId ? { traceId } : {}),
});
};
}
@@ -261,7 +259,11 @@
export function linkEnvironmentToCloud(input: {
readonly connection: SavedRemoteConnection;
readonly clerkToken: string;
-}): Effect.Effect<void, CloudEnvironmentLinkError, HttpClient.HttpClient | ManagedRelayClient> {
+}): Effect.Effect<
+ void,
+ CloudEnvironmentLinkError,
+ HttpClient.HttpClient | ManagedRelay.ManagedRelayClient
+> {
return Effect.gen(function* () {
if (!input.connection.bearerToken) {
return yield* new CloudEnvironmentLinkError({
@@ -270,7 +272,7 @@
}
const localBearerToken = input.connection.bearerToken;
const relayUrl = yield* requireRelayUrl();
- const relayClient = yield* ManagedRelayClient;
+ const relayClient = yield* ManagedRelay.ManagedRelayClient;
const deviceId = yield* Effect.tryPromise({
try: () => loadOrCreateAgentAwarenessDeviceId(),
catch: cloudEnvironmentLinkError("Could not load the mobile device id."),
@@ -353,11 +355,11 @@
}): Effect.Effect<
ReadonlyArray<RelayClientEnvironmentRecord>,
CloudEnvironmentLinkError,
- ManagedRelayClient
+ ManagedRelay.ManagedRelayClient
> {
return Effect.gen(function* () {
const relayUrl = yield* requireRelayUrl();
- const relayClient = yield* ManagedRelayClient;
+ const relayClient = yield* ManagedRelay.ManagedRelayClient;
return yield* relayClient
.listEnvironments({
@@ -374,11 +376,11 @@
}): Effect.Effect<
RelayEnvironmentStatusResponseType,
CloudEnvironmentLinkError,
- ManagedRelayClient
+ ManagedRelay.ManagedRelayClient
> {
return Effect.gen(function* () {
const relayUrl = yield* requireRelayUrl();
- const relayClient = yield* ManagedRelayClient;
+ const relayClient = yield* ManagedRelay.ManagedRelayClient;
const status = yield* relayClient
.getEnvironmentStatus({
clerkToken: input.clerkToken,
@@ -413,7 +415,7 @@
}): Effect.Effect<
ReadonlyArray<CloudEnvironmentRecordWithStatus>,
CloudEnvironmentLinkError,
- ManagedRelayClient
+ ManagedRelay.ManagedRelayClient
> {
return Effect.forEach(
input.environments,
@@ -445,7 +447,7 @@
}): Effect.Effect<
ReadonlyArray<CloudEnvironmentRecordWithStatus>,
CloudEnvironmentLinkError,
- ManagedRelayClient
+ ManagedRelay.ManagedRelayClient
> {
return Effect.gen(function* () {
const environments = yield* listCloudEnvironments(input);
@@ -473,7 +475,7 @@
}) {
yield* Effect.annotateCurrentSpan({ "environment.id": input.environmentId });
const relayUrl = yield* requireRelayUrl();
- const relayClient = yield* ManagedRelayClient;
+ const relayClient = yield* ManagedRelay.ManagedRelayClient;
const deviceId = yield* loadAgentAwarenessDeviceId();
const connect = yield* relayClient
@@ -514,7 +516,7 @@
message: "Connected endpoint descriptor does not match the selected environment.",
});
}
- const signer = yield* ManagedRelayDpopSigner;
+ const signer = yield* ManagedRelay.ManagedRelayDpopSigner;
const bootstrapDpop = yield* signer
.createProof({
method: "POST",
@@ -555,7 +557,7 @@
}): Effect.Effect<
SavedRemoteConnection,
CloudEnvironmentLinkError,
- HttpClient.HttpClient | ManagedRelayClient | ManagedRelayDpopSigner
+ HttpClient.HttpClient | ManagedRelay.ManagedRelayClient | ManagedRelay.ManagedRelayDpopSigner
> {
return connectRelayManagedEnvironment({
clerkToken: input.clerkToken,
@@ -570,7 +572,7 @@
}): Effect.Effect<
SavedRemoteConnection,
CloudEnvironmentLinkError,
- HttpClient.HttpClient | ManagedRelayClient | ManagedRelayDpopSigner
+ HttpClient.HttpClient | ManagedRelay.ManagedRelayClient | ManagedRelay.ManagedRelayDpopSigner
> {
return connectRelayManagedEnvironment({
clerkToken: input.clerkToken,
diff --git a/apps/mobile/src/features/cloud/managedRelayLayer.ts b/apps/mobile/src/features/cloud/managedRelayLayer.ts
--- a/apps/mobile/src/features/cloud/managedRelayLayer.ts
+++ b/apps/mobile/src/features/cloud/managedRelayLayer.ts
@@ -1,8 +1,4 @@
-import {
- managedRelayClientLayer as makeManagedRelayClientLayer,
- ManagedRelayDpopSigner,
- ManagedRelayDpopSignerError,
-} from "@t3tools/client-runtime/relay";
+import { ManagedRelay } from "@t3tools/client-runtime/relay";
import { RelayMobileClientId } from "@t3tools/contracts/relay";
import * as Crypto from "effect/Crypto";
import * as Effect from "effect/Effect";
@@ -12,16 +8,16 @@
import { managedRelayAccessTokenStore } from "./managedRelayTokenStore";
const relayDpopSignerLayer = Layer.effect(
- ManagedRelayDpopSigner,
+ ManagedRelay.ManagedRelayDpopSigner,
Effect.gen(function* () {
const crypto = yield* Crypto.Crypto;
const loadProofKey = yield* Effect.cached(
loadOrCreateDpopProofKeyPair().pipe(Effect.provideService(Crypto.Crypto, crypto)),
);
- return ManagedRelayDpopSigner.of({
+ return ManagedRelay.ManagedRelayDpopSigner.of({
thumbprint: loadProofKey.pipe(
Effect.map((proofKey) => proofKey.thumbprint),
- Effect.mapError((cause) => new ManagedRelayDpopSignerError({ cause })),
+ Effect.mapError((cause) => new ManagedRelay.ManagedRelayDpopKeyLoadError({ cause })),
Effect.withSpan("mobile.managedRelayDpopSigner.loadThumbprint"),
),
createProof: Effect.fn("mobile.managedRelayDpopSigner.createProof")(
@@ -32,14 +28,14 @@
Effect.map((proof) => proof.proof),
);
},
- Effect.mapError((cause) => new ManagedRelayDpopSignerError({ cause })),
+ Effect.mapError((cause) => new ManagedRelay.ManagedRelayDpopProofCreationError({ cause })),
),
});
}),
);
export const managedRelayClientLayer = (relayUrl: string) =>
- makeManagedRelayClientLayer({
+ ManagedRelay.layer({
relayUrl,
clientId: RelayMobileClientId,
accessTokenStore: managedRelayAccessTokenStore,
diff --git a/apps/mobile/src/features/cloud/managedRelayTokenStore.ts b/apps/mobile/src/features/cloud/managedRelayTokenStore.ts
--- a/apps/mobile/src/features/cloud/managedRelayTokenStore.ts
+++ b/apps/mobile/src/features/cloud/managedRelayTokenStore.ts
@@ -1,7 +1,4 @@
-import {
- type ManagedRelayAccessTokenCacheEntry,
- type ManagedRelayAccessTokenStore,
-} from "@t3tools/client-runtime/relay";
+import { ManagedRelay } from "@t3tools/client-runtime/relay";
import * as Data from "effect/Data";
import * as Effect from "effect/Effect";
import * as Schema from "effect/Schema";
@@ -60,7 +57,7 @@
}).pipe(
Effect.flatMap((encoded) =>
encoded === null
- ? Effect.succeed<ReadonlyArray<ManagedRelayAccessTokenCacheEntry>>([])
+ ? Effect.succeed<ReadonlyArray<ManagedRelay.ManagedRelayAccessTokenCacheEntry>>([])
: decodeManagedRelayAccessTokenCache(encoded).pipe(
Effect.map((cache) => cache.entries),
Effect.mapError(storeError("Persisted relay access tokens are invalid.")),
@@ -68,7 +65,9 @@
),
);
-const saveManagedRelayAccessTokens = (entries: ReadonlyArray<ManagedRelayAccessTokenCacheEntry>) =>
+const saveManagedRelayAccessTokens = (
+ entries: ReadonlyArray<ManagedRelay.ManagedRelayAccessTokenCacheEntry>,
+) =>
encodeManagedRelayAccessTokenCache({
version: MANAGED_RELAY_TOKEN_CACHE_VERSION,
entries,
@@ -87,7 +86,7 @@
catch: storeError("Could not clear persisted relay access tokens."),
});
-export const managedRelayAccessTokenStore: ManagedRelayAccessTokenStore = {
+export const managedRelayAccessTokenStore: ManagedRelay.ManagedRelayAccessTokenStore = {
load: loadManagedRelayAccessTokens.pipe(
Effect.tapError(logStoreFailure("load")),
Effect.orElseSucceed(() => []),
diff --git a/apps/mobile/src/lib/runtime.ts b/apps/mobile/src/lib/runtime.ts
--- a/apps/mobile/src/lib/runtime.ts
+++ b/apps/mobile/src/lib/runtime.ts
@@ -15,7 +15,17 @@
const httpClientLayer = remoteHttpClientLayer(fetch);
-export const runtimeLayer = Layer.merge(
+type RuntimeLayerSource =
+ | ReturnType<typeof managedRelayClientLayer>
+ | typeof Socket.layerWebSocketConstructorGlobal
+ | typeof cryptoLayer
+ | typeof httpClientLayer
+ | typeof tracingLayer;
+
+export const runtimeLayer: Layer.Layer<
+ Layer.Success<RuntimeLayerSource>,
+ Layer.Error<RuntimeLayerSource>
+> = Layer.merge(
managedRelayClientLayer(configuredRelayUrl()),
Socket.layerWebSocketConstructorGlobal,
).pipe(
@@ -24,6 +34,12 @@
Layer.provideMerge(tracingLayer.pipe(Layer.provide(httpClientLayer))),
);
-export const runtime = ManagedRuntime.make(runtimeLayer);
+export const runtime: ManagedRuntime.ManagedRuntime<
+ Layer.Success<typeof runtimeLayer>,
+ Layer.Error<typeof runtimeLayer>
+> = ManagedRuntime.make(runtimeLayer);
-export const runtimeContextLayer = Layer.effectContext(runtime.contextEffect);
+export const runtimeContextLayer: Layer.Layer<
+ Layer.Success<typeof runtimeLayer>,
+ Layer.Error<typeof runtimeLayer>
+> = Layer.effectContext(runtime.contextEffect);
diff --git a/apps/mobile/src/state/relay.ts b/apps/mobile/src/state/relay.ts
--- a/apps/mobile/src/state/relay.ts
+++ b/apps/mobile/src/state/relay.ts
@@ -2,5 +2,5 @@
import { connectionAtomRuntime } from "../connection/runtime";
-export const relayEnvironmentDiscovery =
+export const relayEnvironmentDiscovery: ReturnType<typeof createRelayEnvironmentDiscoveryAtoms> =
createRelayEnvironmentDiscoveryAtoms(connectionAtomRuntime);
diff --git a/apps/web/src/cloud/linkEnvironment.test.ts b/apps/web/src/cloud/linkEnvironment.test.ts
--- a/apps/web/src/cloud/linkEnvironment.test.ts
+++ b/apps/web/src/cloud/linkEnvironment.test.ts
@@ -23,11 +23,7 @@
} from "@t3tools/client-runtime/connection";
import { type RpcSession } from "@t3tools/client-runtime/rpc";
import { EnvironmentRegistry } from "@t3tools/client-runtime/connection";
-import {
- managedRelayClientLayer,
- ManagedRelayClient,
- ManagedRelayDpopSigner,
-} from "@t3tools/client-runtime/relay";
+import { ManagedRelay } from "@t3tools/client-runtime/relay";
import { remoteHttpClientLayer } from "@t3tools/client-runtime/rpc";
import { __resetDesktopPrimaryAuthForTests } from "../environments/primary/desktopAuth";
@@ -62,8 +58,8 @@
const createProof = vi.fn(() => Effect.succeed("dpop-proof"));
const dpopSignerLayer = Layer.succeed(
- ManagedRelayDpopSigner,
- ManagedRelayDpopSigner.of({
+ ManagedRelay.ManagedRelayDpopSigner,
+ ManagedRelay.ManagedRelayDpopSigner.of({
thumbprint: Effect.succeed("thumbprint"),
createProof,
}),
@@ -73,7 +69,7 @@
const http = remoteHttpClientLayer(globalThis.fetch);
return Layer.mergeAll(
http,
- managedRelayClientLayer({
+ ManagedRelay.layer({
relayUrl: "https://relay.example.test",
clientId: RelayWebClientId,
}).pipe(Layer.provideMerge(dpopSignerLayer), Layer.provide(http)),
@@ -131,7 +127,11 @@
}
function withServices<A, E>(
- effect: Effect.Effect<A, E, HttpClient.HttpClient | ManagedRelayClient | EnvironmentRegistry>,
+ effect: Effect.Effect<
+ A,
+ E,
+ HttpClient.HttpClient | ManagedRelay.ManagedRelayClient | EnvironmentRegistry
+ >,
options?: Parameters<typeof registryLayer>[0],
) {
return effect.pipe(Effect.provide(services(options)));
diff --git a/apps/web/src/cloud/linkEnvironment.ts b/apps/web/src/cloud/linkEnvironment.ts
--- a/apps/web/src/cloud/linkEnvironment.ts
+++ b/apps/web/src/cloud/linkEnvironment.ts
@@ -25,7 +25,7 @@
import { EnvironmentRegistry } from "@t3tools/client-runtime/connection";
import { request, runStream } from "@t3tools/client-runtime/rpc";
import { makeEnvironmentHttpApiClient } from "@t3tools/client-runtime/rpc";
-import { ManagedRelayClient, type ManagedRelayClientError } from "@t3tools/client-runtime/relay";
+import { ManagedRelay } from "@t3tools/client-runtime/relay";
import {
readPrimaryEnvironmentDescriptor,
@@ -164,13 +164,15 @@
}
function decodedRelayClientError(message: string) {
- return (cause: ManagedRelayClientError) => {
- const relayError = cause.relayError;
+ return (cause: ManagedRelay.ManagedRelayClientError) => {
+ const relayError =
+ cause._tag === "ManagedRelayRequestFailedError" ? cause.relayError : undefined;
+ const traceId = cause._tag === "ManagedRelayRequestFailedError" ? cause.traceId : undefined;
const detail = relayError ? relayProtectedErrorMessage(relayError) : null;
return new CloudEnvironmentLinkError({
message: detail ? `${message}: ${detail}` : message,
cause,
- ...(cause.traceId ? { traceId: cause.traceId } : {}),
+ ...(traceId ? { traceId } : {}),
});
};
}
@@ -268,7 +270,7 @@
}): Effect.Effect<
ReadonlyArray<RelayClientEnvironmentRecord>,
CloudEnvironmentLinkError,
- ManagedRelayClient
+ ManagedRelay.ManagedRelayClient
> {
return Effect.gen(function* () {
const configuredRelayUrl = relayUrl();
@@ -277,7 +279,7 @@
message: "T3CODE_RELAY_URL is not configured.",
});
}
- const relayClient = yield* ManagedRelayClient;
+ const relayClient = yield* ManagedRelay.ManagedRelayClient;
return yield* relayClient
.listEnvironments({
clerkToken: input.clerkToken,
@@ -299,7 +301,7 @@
}): Effect.Effect<
ReadonlyArray<RelayClientDeviceRecord>,
CloudEnvironmentLinkError,
- ManagedRelayClient
+ ManagedRelay.ManagedRelayClient
> {
return Effect.gen(function* () {
if (!relayUrl()) {
@@ -307,7 +309,7 @@
message: "T3CODE_RELAY_URL is not configured.",
});
}
- const relayClient = yield* ManagedRelayClient;
+ const relayClient = yield* ManagedRelay.ManagedRelayClient;
return yield* relayClient.listDevices({ clerkToken: input.clerkToken }).pipe(
Effect.mapError(
(cause) =>
@@ -351,7 +353,11 @@
export function unlinkPrimaryEnvironmentFromCloud(input: {
readonly target: CloudLinkTarget;
readonly clerkToken: string | null;
-}): Effect.Effect<void, CloudEnvironmentLinkError, HttpClient.HttpClient | ManagedRelayClient> {
+}): Effect.Effect<
+ void,
+ CloudEnvironmentLinkError,
+ HttpClient.HttpClient | ManagedRelay.ManagedRelayClient
+> {
return Effect.gen(function* () {
const client = yield* makeEnvironmentHttpApiClient(input.target.httpBaseUrl);
yield* client.connect
@@ -360,7 +366,7 @@
const configuredRelayUrl = relayUrl();
if (configuredRelayUrl && input.clerkToken) {
- const relayClient = yield* ManagedRelayClient;
+ const relayClient = yield* ManagedRelay.ManagedRelayClient;
yield* relayClient
.unlinkEnvironment({
clerkToken: input.clerkToken,
@@ -383,7 +389,7 @@
}): Effect.Effect<
void,
CloudEnvironmentLinkError,
- EnvironmentRegistry | HttpClient.HttpClient | ManagedRelayClient
+ EnvironmentRegistry | HttpClient.HttpClient | ManagedRelay.ManagedRelayClient
> {
return Effect.gen(function* () {
const configuredRelayUrl = relayUrl();
@@ -392,7 +398,7 @@
message: "T3CODE_RELAY_URL is not configured.",
});
}
- const relayClient = yield* ManagedRelayClient;
+ const relayClient = yield* ManagedRelay.ManagedRelayClient;
const environmentClient = yield* makeEnvironmentHttpApiClient(input.target.httpBaseUrl);
yield* ensureRelayClientAvailable(EnvironmentId.make(input.target.environmentId));
diff --git a/apps/web/src/cloud/managedAuth.tsx b/apps/web/src/cloud/managedAuth.tsx
--- a/apps/web/src/cloud/managedAuth.tsx
+++ b/apps/web/src/cloud/managedAuth.tsx
@@ -1,5 +1,5 @@
import { useAuth } from "@clerk/react";
... diff truncated: showing 800 of 4105 linesYou can send follow-ups to the cloud agent here.
Reviewed by Cursor Bugbot for commit 5360201. Configure here.
5360201 to
5b314a7
Compare
5b314a7 to
d3fe354
Compare
Dismissing prior approval to re-evaluate d3fe354
d3fe354 to
119ebe3
Compare
119ebe3 to
fdde198
Compare
Dismissing prior approval to re-evaluate 604691c
bea99c6 to
c0eceff
Compare
Dismissing prior approval to re-evaluate c0eceff
1aa32d8 to
20301d4
Compare
20301d4 to
8a64adf
Compare
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
8a64adf to
f3a36b4
Compare


Summary
Context.Servicecontracts with canonicalmakeandlayerexportsSchema.TaggedErrorClassbranches, retaining category-levelSchema.Unionexports and structured request diagnosticsDiscoveryandManagedRelaynamespaces and update web, mobile, and client-runtime consumers to use qualified membersConnection.layerand give the concrete resolver canonicalmakeandlayerexportsDesign notes
Layer.effectis not a requirement: each concrete service exports a canonicallayerusing the constructor appropriate to its implementation. The managed relay client remains a parameterized layer because its URL, public client ID, and optional token store are runtime inputs. Platform capability, persistence, connection-source, and DPoP signer tags remain abstract ports whose implementations and layers are platform-specific; no syntheticmakeorlayeris added for those ports. Orchestration modules are intentionally excluded because they are being reworked separately in #2829.Validation
vp check: passed with 0 errors and 20 existing repository warningsvp run typecheck: passedvp run lint:mobile: passedgit diff --check: passedNote
Refactor client-runtime relay exports to use
ManagedRelayandDiscoverynamespacespackages/client-runtime/src/relay/index.tsto exportmanagedRelay.tsanddiscovery.tssymbols underManagedRelay.*andDiscovery.*namespaces instead of flat top-level exports, requiring all consumers to update their import paths.ManagedRelayClientErrortagged error with a discriminated union of typed errors (ManagedRelayRequestFailedError,ManagedRelayRequestTimeoutError,ManagedRelayUrlInvalidError,ManagedRelayAccessTokenScopesUnexpectedError,ManagedRelayDpopKeyLoadError, etc.), each carrying structured context fields.mapManagedRelayErrorinpackages/client-runtime/src/connection/errors.tsto classify relay errors into specificConnectionAttemptErrortypes by switching on error tags rather than inspecting causes generically.ManagedRelayDpopKeyLoadErrorandManagedRelayDpopProofCreationErrorwithkeyStore,method, andurlcontext instead of a singleManagedRelayDpopSignerError.decodedRelayClientErrorin both mobile and weblinkEnvironment.tsnow only extractsrelayErrorandtraceIdwhen the cause tag isManagedRelayRequestFailedError; other error variants produce messages without relay-protected detail.Macroscope summarized f3a36b4.
Note
Medium Risk
Touches relay auth, DPoP, token cache, and cloud link/connect paths across client-runtime, web, and mobile; error-tag changes alter how relay failures are classified and surfaced to users.
Overview
Refactors how client-runtime exposes relay services and how failures surface through the stack.
Relay module shape:
packages/client-runtime/src/relay/index.tsnow exportsDiscoveryandManagedRelaynamespaces instead of flat symbols. Web, mobile, and tests importManagedRelay.ManagedRelayClient,ManagedRelay.layer,ManagedRelay.ManagedRelayDpopSigner, and similar qualified members. Relay environment discovery atoms pull discovery state from theDiscoverynamespace.Managed relay client API: The parameterized client layer is
ManagedRelay.layerwith a canonicalmakefactory (replacingmanagedRelayClientLayer). Service types stay on inlineContext.Servicecontracts.Error model: The single
ManagedRelayClientErrorwrapper is replaced by a discriminated union ofSchema.TaggedErrorClassvariants (ManagedRelayRequestFailedError,ManagedRelayRequestTimeoutError,ManagedRelayUrlInvalidError, DPoP key/proof errors, scope mismatch, etc.) with structured fields (activity,action,relayUrl,keyStore, …). Timeouts and invalid URLs are first-class tags rather than nested causes.Downstream mapping:
mapManagedRelayErrorswitches on_tagto map timeouts, config, permission, and auth failures into connection attempt errors. Mobile/webdecodedRelayClientErroronly attachesrelayErrorandtraceIdwhen the cause isManagedRelayRequestFailedError; other tags no longer expose relay-protected detail. Platform DPoP signer layers emitManagedRelayDpopKeyLoadError/ManagedRelayDpopProofCreationError(withexpo-secure-storevsindexed-db) instead of a generic signer error.Typing: Mobile/web
runtimeLayerandruntimegain explicitLayer/ManagedRuntimecomposition types where generated declarations need them.Reviewed by Cursor Bugbot for commit f3a36b4. Bugbot is set up for automated code reviews on this repo. Configure here.