diff --git a/.changeset/bright-tags-recognize.md b/.changeset/bright-tags-recognize.md
new file mode 100644
index 00000000000..edead0f682b
--- /dev/null
+++ b/.changeset/bright-tags-recognize.md
@@ -0,0 +1,5 @@
+---
+"effect": patch
+---
+
+Recognize tagged Config and RPC errors across duplicated `effect` package copies.
diff --git a/packages/effect/src/Config.ts b/packages/effect/src/Config.ts
index 5caddba3a43..68f21037e9f 100644
--- a/packages/effect/src/Config.ts
+++ b/packages/effect/src/Config.ts
@@ -194,13 +194,15 @@ const evaluationFailure = (error: ConfigError, hasInput: boolean): EvaluationFai
hasInput
})
+const isSourceError = (u: unknown): u is ConfigProvider.SourceError => Predicate.isTagged(u, "SourceError")
+
const catchSourceError = (
self: Effect.Effect,
hasInput: boolean
): Effect.Effect =>
self.pipe(
Effect.catchDefect((defect) =>
- defect instanceof ConfigProvider.SourceError
+ isSourceError(defect)
? Effect.fail(evaluationFailure(new ConfigError(defect), hasInput))
: Effect.die(defect)
)
diff --git a/packages/effect/src/unstable/rpc/RpcClient.ts b/packages/effect/src/unstable/rpc/RpcClient.ts
index 6a9483fc48a..8f028a5716d 100644
--- a/packages/effect/src/unstable/rpc/RpcClient.ts
+++ b/packages/effect/src/unstable/rpc/RpcClient.ts
@@ -23,6 +23,7 @@ import * as Latch from "../../Latch.ts"
import * as Layer from "../../Layer.ts"
import * as Option from "../../Option.ts"
import * as Pool from "../../Pool.ts"
+import * as Predicate from "../../Predicate.ts"
import * as Queue from "../../Queue.ts"
import * as Result from "../../Result.ts"
import * as Schedule from "../../Schedule.ts"
@@ -51,6 +52,8 @@ import * as RpcSerialization from "./RpcSerialization.ts"
import * as RpcWorker from "./RpcWorker.ts"
import { withRunClient } from "./Utils.ts"
+const isRpcClientError = (u: unknown): u is RpcClientError => Predicate.isTagged(u, "RpcClientError")
+
/**
* The object-shaped client generated from a union of RPC definitions, with one
* method per RPC tag.
@@ -960,7 +963,7 @@ export const makeProtocolHttp = (client: HttpClient.HttpClient): Effect.Effect<
})
})
)).pipe(
- Effect.mapError((cause) => cause instanceof RpcClientError ? cause : httpClientError(cause))
+ Effect.mapError((cause) => isRpcClientError(cause) ? cause : httpClientError(cause))
)
if (!hasResponse) {
return yield* emptyResponseError(request)
diff --git a/packages/effect/test/Config.test.ts b/packages/effect/test/Config.test.ts
index ddccd151da0..e2f7c6a96da 100644
--- a/packages/effect/test/Config.test.ts
+++ b/packages/effect/test/Config.test.ts
@@ -12,6 +12,8 @@ import {
SchemaIssue,
SchemaTransformation
} from "effect"
+import { vi } from "vitest"
+import type * as ConfigProviderModule from "../src/ConfigProvider.ts"
async function assertSuccess(config: Config.Config, provider: ConfigProvider.ConfigProvider, expected: T) {
const r = await config.parse(provider).pipe(
@@ -31,6 +33,23 @@ async function assertFailure(config: Config.Config, provider: ConfigProvid
}
describe("Config", () => {
+ it("recognizes SourceError defects from a reloaded module copy", async () => {
+ vi.resetModules()
+ const ForeignConfigProvider = await vi.importActual(
+ "../src/ConfigProvider.ts"
+ )
+ const sourceError = new ForeignConfigProvider.SourceError({ message: "source unavailable" })
+ assert.isFalse(sourceError instanceof ConfigProvider.SourceError)
+
+ const provider = ConfigProvider.make(() => Effect.die(sourceError))
+ const error = await Config.string("value").parse(provider).pipe(
+ Effect.flip,
+ Effect.runPromise
+ )
+
+ assert.strictEqual(error.cause, sourceError)
+ })
+
it.effect("uses the current ConfigProvider when yielded as an Effect", () =>
Effect.gen(function*() {
const provider = ConfigProvider.fromEnv({ env: { STRING: "value" } })
diff --git a/packages/effect/test/rpc/RpcClient.test.ts b/packages/effect/test/rpc/RpcClient.test.ts
index 52474a5f622..995825d6b71 100644
--- a/packages/effect/test/rpc/RpcClient.test.ts
+++ b/packages/effect/test/rpc/RpcClient.test.ts
@@ -6,6 +6,8 @@ import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse"
import { Rpc, RpcClient, RpcGroup, RpcMessage, RpcSchema, RpcSerialization } from "effect/unstable/rpc"
import { RpcClientError } from "effect/unstable/rpc/RpcClientError"
import * as Socket from "effect/unstable/socket/Socket"
+import { vi } from "vitest"
+import type * as RpcClientErrorModule from "../../src/unstable/rpc/RpcClientError.ts"
const TestGroup = RpcGroup.make(
Rpc.make("Ping", { success: Schema.String }),
@@ -22,15 +24,20 @@ const makeHttpClient = (body: string): HttpClient.HttpClient =>
)
)
-const makeProtocolLayer = (
+const makeProtocolLayerWithClient = (
serializationLayer: Layer.Layer,
- body: string
+ client: HttpClient.HttpClient
) =>
RpcClient.layerProtocolHttp({ url: "http://localhost/rpc" }).pipe(
Layer.provideMerge(serializationLayer),
- Layer.provideMerge(Layer.succeed(HttpClient.HttpClient, makeHttpClient(body)))
+ Layer.provideMerge(Layer.succeed(HttpClient.HttpClient, client))
)
+const makeProtocolLayer = (
+ serializationLayer: Layer.Layer,
+ body: string
+) => makeProtocolLayerWithClient(serializationLayer, makeHttpClient(body))
+
const assertEmptyResponseFailsRequest = (
serializationLayer: Layer.Layer,
body: string
@@ -53,6 +60,31 @@ const assertEmptyResponseFailsRequest = (
})
describe("RpcClient", () => {
+ it("preserves RpcClientError failures from a reloaded module copy", async () => {
+ vi.resetModules()
+ const ForeignRpcClientError = await vi.importActual(
+ "../../src/unstable/rpc/RpcClientError.ts"
+ )
+ const rpcClientError = new ForeignRpcClientError.RpcClientError({
+ reason: new ForeignRpcClientError.RpcClientDefect({ message: "boom", cause: undefined })
+ })
+ assert.isFalse(rpcClientError instanceof RpcClientError)
+
+ const httpClient = HttpClient.make((request) => {
+ const response = HttpClientResponse.fromWeb(request, new Response("", { status: 200 }))
+ Object.defineProperty(response, "stream", { value: Stream.fail(rpcClientError) })
+ return Effect.succeed(response)
+ })
+ const error = await Effect.gen(function*() {
+ const client = yield* RpcClient.make(TestGroup).pipe(
+ Effect.provide(makeProtocolLayerWithClient(RpcSerialization.layerNdjson, httpClient))
+ )
+ return yield* client.Ping().pipe(Effect.flip)
+ }).pipe(Effect.scoped, Effect.runPromise)
+
+ assert.strictEqual(error, rpcClientError)
+ })
+
it.effect("fails request on empty HTTP response for unframed serialization", () =>
assertEmptyResponseFailsRequest(RpcSerialization.layerJson, "[]"))
diff --git a/packages/tools/api-diff/src/Annotations.ts b/packages/tools/api-diff/src/Annotations.ts
index a6e9a8bb201..86bcbc096e1 100644
--- a/packages/tools/api-diff/src/Annotations.ts
+++ b/packages/tools/api-diff/src/Annotations.ts
@@ -2,7 +2,7 @@ import * as Effect from "effect/Effect"
import * as FileSystem from "effect/FileSystem"
import * as Path from "effect/Path"
import * as Yaml from "yaml"
-import { ApiDiffError } from "./Error.ts"
+import { ApiDiffError, isApiDiffError } from "./Error.ts"
export interface MigrationAnnotation {
readonly replacement: string
@@ -65,7 +65,7 @@ const loadAnnotationsInternal = Effect.fnUntraced(function*(directory: string) {
export const loadAnnotations = (directory: string) =>
loadAnnotationsInternal(directory).pipe(
Effect.mapError((cause) =>
- cause instanceof ApiDiffError
+ isApiDiffError(cause)
? cause
: new ApiDiffError({ message: `Could not load annotations from ${directory}`, cause })
)
diff --git a/packages/tools/api-diff/src/ApiDiff.ts b/packages/tools/api-diff/src/ApiDiff.ts
index f7a4953f2f3..3a05ebb30ef 100644
--- a/packages/tools/api-diff/src/ApiDiff.ts
+++ b/packages/tools/api-diff/src/ApiDiff.ts
@@ -6,7 +6,7 @@ import * as Layer from "effect/Layer"
import * as Path from "effect/Path"
import { loadAnnotations } from "./Annotations.ts"
import { diffSnapshots } from "./Diff.ts"
-import { ApiDiffError } from "./Error.ts"
+import { ApiDiffError, isApiDiffError } from "./Error.ts"
import { prettyJson } from "./Json.ts"
import {
extractImportMapSections,
@@ -151,7 +151,7 @@ export class ApiDiff extends Context.Service =>
runInternal(options).pipe(
Effect.mapError((cause) =>
- cause instanceof ApiDiffError
+ isApiDiffError(cause)
? cause
: new ApiDiffError({
message: "API diff failed",
diff --git a/packages/tools/api-diff/src/Error.ts b/packages/tools/api-diff/src/Error.ts
index 5762592c118..d2d4444c408 100644
--- a/packages/tools/api-diff/src/Error.ts
+++ b/packages/tools/api-diff/src/Error.ts
@@ -1,6 +1,9 @@
+import * as Predicate from "effect/Predicate"
import * as Schema from "effect/Schema"
export class ApiDiffError extends Schema.TaggedError()("ApiDiffError", {
message: Schema.String,
cause: Schema.optional(Schema.Defect())
}) {}
+
+export const isApiDiffError = (u: unknown): u is ApiDiffError => Predicate.isTagged(u, "ApiDiffError")
diff --git a/packages/tools/api-diff/src/Snapshot.ts b/packages/tools/api-diff/src/Snapshot.ts
index 9770d45ef47..445d5809703 100644
--- a/packages/tools/api-diff/src/Snapshot.ts
+++ b/packages/tools/api-diff/src/Snapshot.ts
@@ -2,6 +2,7 @@ import * as Context from "effect/Context"
import * as Effect from "effect/Effect"
import * as Layer from "effect/Layer"
import * as Path from "effect/Path"
+import * as Predicate from "effect/Predicate"
import * as Schema from "effect/Schema"
import ts from "typescript-compiler"
import { Discovery, type DiscoveryResult } from "./Discovery.ts"
@@ -709,6 +710,9 @@ export class SnapshotExtractionError extends Schema.TaggedError
+ Predicate.isTagged(u, "SnapshotExtractionError")
+
const snapshotExtractionError = (diagnostics: ReadonlyArray): SnapshotExtractionError =>
new SnapshotExtractionError({
message: diagnostics.map((diagnostic) => `${diagnostic.code}: ${diagnostic.message}`).join("\n"),
@@ -861,7 +865,7 @@ export class Snapshotter extends Context.Service extractSnapshot(options, discovered, path),
catch: (cause) =>
- cause instanceof SnapshotExtractionError
+ isSnapshotExtractionError(cause)
? cause
: new ApiDiffError({
message: `Could not extract the API snapshot for ${options.ref}`,
diff --git a/packages/tools/api-diff/src/Worktrees.ts b/packages/tools/api-diff/src/Worktrees.ts
index 149a9046972..837cc985391 100644
--- a/packages/tools/api-diff/src/Worktrees.ts
+++ b/packages/tools/api-diff/src/Worktrees.ts
@@ -7,7 +7,7 @@ import * as Path from "effect/Path"
import * as Stream from "effect/Stream"
import * as ChildProcess from "effect/unstable/process/ChildProcess"
import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner"
-import { ApiDiffError } from "./Error.ts"
+import { ApiDiffError, isApiDiffError } from "./Error.ts"
import { decodeJson } from "./Json.ts"
import type { ApiSnapshot } from "./Model.ts"
import { snapshotCacheKey, Snapshotter } from "./Snapshot.ts"
@@ -186,7 +186,7 @@ export class Worktrees extends Context.Service
- cause instanceof ApiDiffError
+ isApiDiffError(cause)
? cause
: new ApiDiffError({
message: `Could not extract the ${options.name} API snapshot`,
@@ -221,7 +221,7 @@ export class Worktrees extends Context.Service =>
prepareSnapshotInternal(options).pipe(
Effect.mapError((cause) =>
- cause instanceof ApiDiffError
+ isApiDiffError(cause)
? cause
: new ApiDiffError({
message: `Could not prepare the ${options.name} API snapshot`,
diff --git a/packages/tools/api-diff/test/Error.test.ts b/packages/tools/api-diff/test/Error.test.ts
new file mode 100644
index 00000000000..d9b5e6433a2
--- /dev/null
+++ b/packages/tools/api-diff/test/Error.test.ts
@@ -0,0 +1,25 @@
+import { ApiDiffError, isApiDiffError } from "@effect/api-diff/Error"
+import { isSnapshotExtractionError, SnapshotExtractionError } from "@effect/api-diff/Snapshot"
+import { assert, describe, it } from "@effect/vitest"
+import { vi } from "vitest"
+import type * as ApiDiffErrorModule from "../src/Error.ts"
+import type * as SnapshotModule from "../src/Snapshot.ts"
+
+describe("tagged errors", () => {
+ it("recognizes errors from a reloaded module copy", async () => {
+ vi.resetModules()
+ const ForeignApiDiffError = await vi.importActual("../src/Error.ts")
+ const ForeignSnapshot = await vi.importActual("../src/Snapshot.ts")
+
+ const apiDiffError = new ForeignApiDiffError.ApiDiffError({ message: "boom" })
+ assert.isFalse(apiDiffError instanceof ApiDiffError)
+ assert.isTrue(isApiDiffError(apiDiffError))
+
+ const snapshotError = new ForeignSnapshot.SnapshotExtractionError({
+ message: "boom",
+ diagnostics: []
+ })
+ assert.isFalse(snapshotError instanceof SnapshotExtractionError)
+ assert.isTrue(isSnapshotExtractionError(snapshotError))
+ })
+})