Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/bright-tags-recognize.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"effect": patch
---

Recognize tagged Config and RPC errors across duplicated `effect` package copies.
4 changes: 3 additions & 1 deletion packages/effect/src/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = <A, E, R>(
self: Effect.Effect<A, E, R>,
hasInput: boolean
): Effect.Effect<A, E | EvaluationFailure, R> =>
self.pipe(
Effect.catchDefect((defect) =>
defect instanceof ConfigProvider.SourceError
isSourceError(defect)
? Effect.fail(evaluationFailure(new ConfigError(defect), hasInput))
: Effect.die(defect)
)
Expand Down
5 changes: 4 additions & 1 deletion packages/effect/src/unstable/rpc/RpcClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand Down
19 changes: 19 additions & 0 deletions packages/effect/test/Config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {
SchemaIssue,
SchemaTransformation
} from "effect"
import { vi } from "vitest"
import type * as ConfigProviderModule from "../src/ConfigProvider.ts"

async function assertSuccess<T>(config: Config.Config<T>, provider: ConfigProvider.ConfigProvider, expected: T) {
const r = await config.parse(provider).pipe(
Expand All @@ -31,6 +33,23 @@ async function assertFailure<T>(config: Config.Config<T>, provider: ConfigProvid
}

describe("Config", () => {
it("recognizes SourceError defects from a reloaded module copy", async () => {
vi.resetModules()
const ForeignConfigProvider = await vi.importActual<typeof ConfigProviderModule>(
"../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" } })
Expand Down
38 changes: 35 additions & 3 deletions packages/effect/test/rpc/RpcClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }),
Expand All @@ -22,15 +24,20 @@ const makeHttpClient = (body: string): HttpClient.HttpClient =>
)
)

const makeProtocolLayer = (
const makeProtocolLayerWithClient = (
serializationLayer: Layer.Layer<RpcSerialization.RpcSerialization>,
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<RpcSerialization.RpcSerialization>,
body: string
) => makeProtocolLayerWithClient(serializationLayer, makeHttpClient(body))

const assertEmptyResponseFailsRequest = (
serializationLayer: Layer.Layer<RpcSerialization.RpcSerialization>,
body: string
Expand All @@ -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<typeof RpcClientErrorModule>(
"../../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, "[]"))

Expand Down
4 changes: 2 additions & 2 deletions packages/tools/api-diff/src/Annotations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 })
)
Expand Down
4 changes: 2 additions & 2 deletions packages/tools/api-diff/src/ApiDiff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -151,7 +151,7 @@ export class ApiDiff extends Context.Service<ApiDiff, {
const run = (options: ApiDiffOptions): Effect.Effect<void, ApiDiffError> =>
runInternal(options).pipe(
Effect.mapError((cause) =>
cause instanceof ApiDiffError
isApiDiffError(cause)
? cause
: new ApiDiffError({
message: "API diff failed",
Expand Down
3 changes: 3 additions & 0 deletions packages/tools/api-diff/src/Error.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import * as Predicate from "effect/Predicate"
import * as Schema from "effect/Schema"

export class ApiDiffError extends Schema.TaggedError<ApiDiffError>()("ApiDiffError", {
message: Schema.String,
cause: Schema.optional(Schema.Defect())
}) {}

export const isApiDiffError = (u: unknown): u is ApiDiffError => Predicate.isTagged(u, "ApiDiffError")
6 changes: 5 additions & 1 deletion packages/tools/api-diff/src/Snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -709,6 +710,9 @@ export class SnapshotExtractionError extends Schema.TaggedError<SnapshotExtracti
}
) {}

export const isSnapshotExtractionError = (u: unknown): u is SnapshotExtractionError =>
Predicate.isTagged(u, "SnapshotExtractionError")

const snapshotExtractionError = (diagnostics: ReadonlyArray<SnapshotDiagnostic>): SnapshotExtractionError =>
new SnapshotExtractionError({
message: diagnostics.map((diagnostic) => `${diagnostic.code}: ${diagnostic.message}`).join("\n"),
Expand Down Expand Up @@ -861,7 +865,7 @@ export class Snapshotter extends Context.Service<Snapshotter, {
return yield* Effect.try({
try: () => extractSnapshot(options, discovered, path),
catch: (cause) =>
cause instanceof SnapshotExtractionError
isSnapshotExtractionError(cause)
? cause
: new ApiDiffError({
message: `Could not extract the API snapshot for ${options.ref}`,
Expand Down
6 changes: 3 additions & 3 deletions packages/tools/api-diff/src/Worktrees.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -186,7 +186,7 @@ export class Worktrees extends Context.Service<Worktrees, {
...(options.modules === undefined ? {} : { modules: options.modules })
}).pipe(
Effect.mapError((cause) =>
cause instanceof ApiDiffError
isApiDiffError(cause)
? cause
: new ApiDiffError({
message: `Could not extract the ${options.name} API snapshot`,
Expand Down Expand Up @@ -221,7 +221,7 @@ export class Worktrees extends Context.Service<Worktrees, {
const prepareSnapshot = (options: PrepareSnapshotOptions): Effect.Effect<ApiSnapshot, ApiDiffError> =>
prepareSnapshotInternal(options).pipe(
Effect.mapError((cause) =>
cause instanceof ApiDiffError
isApiDiffError(cause)
? cause
: new ApiDiffError({
message: `Could not prepare the ${options.name} API snapshot`,
Expand Down
25 changes: 25 additions & 0 deletions packages/tools/api-diff/test/Error.test.ts
Original file line number Diff line number Diff line change
@@ -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<typeof ApiDiffErrorModule>("../src/Error.ts")
const ForeignSnapshot = await vi.importActual<typeof SnapshotModule>("../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))
})
})
Loading