diff --git a/.changeset/bright-apricots-sit.md b/.changeset/bright-apricots-sit.md new file mode 100644 index 0000000000..d5dc12b8ce --- /dev/null +++ b/.changeset/bright-apricots-sit.md @@ -0,0 +1,5 @@ +--- +"effect": minor +--- + +Add Stream.toReadableStreamEffect / .toReadableStreamRuntime diff --git a/.changeset/cyan-kiwis-juggle.md b/.changeset/cyan-kiwis-juggle.md new file mode 100644 index 0000000000..9a766d4fe4 --- /dev/null +++ b/.changeset/cyan-kiwis-juggle.md @@ -0,0 +1,7 @@ +--- +"effect": minor +--- + +add Cause.prettyErrors api + +You can use this to extract `Error` instances from a `Cause`, that have clean stack traces and have had span information added to them. diff --git a/.changeset/few-bikes-cheer.md b/.changeset/few-bikes-cheer.md new file mode 100644 index 0000000000..e1361cecc7 --- /dev/null +++ b/.changeset/few-bikes-cheer.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +add span stack trace to rendered causes diff --git a/.changeset/four-garlics-wonder.md b/.changeset/four-garlics-wonder.md new file mode 100644 index 0000000000..184d460ae4 --- /dev/null +++ b/.changeset/four-garlics-wonder.md @@ -0,0 +1,5 @@ +--- +"effect": minor +--- + +add Chunk.difference & Chunk.differenceWith diff --git a/.changeset/gorgeous-students-jog.md b/.changeset/gorgeous-students-jog.md new file mode 100644 index 0000000000..6f6a94b074 --- /dev/null +++ b/.changeset/gorgeous-students-jog.md @@ -0,0 +1,6 @@ +--- +"effect": minor +"@effect/vitest": minor +--- + +Improve causal rendering in vitest by rethrowing pretty errors diff --git a/.changeset/lovely-frogs-scream.md b/.changeset/lovely-frogs-scream.md new file mode 100644 index 0000000000..c82b348496 --- /dev/null +++ b/.changeset/lovely-frogs-scream.md @@ -0,0 +1,19 @@ +--- +"effect": minor +--- + +add Effect.functionWithSpan + +Allows you to define an effectful function that is wrapped with a span. + +```ts +import { Effect } from "effect" + +const getTodo = Effect.functionWithSpan({ + body: (id: number) => Effect.succeed(`Got todo ${id}!`), + options: (id) => ({ + name: `getTodo-${id}`, + attributes: { id } + }) +}) +``` diff --git a/.changeset/metal-balloons-play.md b/.changeset/metal-balloons-play.md new file mode 100644 index 0000000000..796ca9febe --- /dev/null +++ b/.changeset/metal-balloons-play.md @@ -0,0 +1,5 @@ +--- +"effect": minor +--- + +Add do notation for Array diff --git a/.changeset/quiet-ladybugs-fly.md b/.changeset/quiet-ladybugs-fly.md new file mode 100644 index 0000000000..d6561e7394 --- /dev/null +++ b/.changeset/quiet-ladybugs-fly.md @@ -0,0 +1,5 @@ +--- +"effect": minor +--- + +support $is & $match for Data.TaggedEnum with generics diff --git a/.changeset/seven-cougars-jump.md b/.changeset/seven-cougars-jump.md new file mode 100644 index 0000000000..5848a53702 --- /dev/null +++ b/.changeset/seven-cougars-jump.md @@ -0,0 +1,5 @@ +--- +"@effect/platform": patch +--- + +Run client request stream with a current runtime. diff --git a/.changeset/soft-moons-raise.md b/.changeset/soft-moons-raise.md new file mode 100644 index 0000000000..f5d565c1ed --- /dev/null +++ b/.changeset/soft-moons-raise.md @@ -0,0 +1,5 @@ +--- +"@effect/vitest": patch +--- + +Throw plain error object derived from fiber failure diff --git a/.changeset/tiny-zebras-work.md b/.changeset/tiny-zebras-work.md new file mode 100644 index 0000000000..b67f80da2a --- /dev/null +++ b/.changeset/tiny-zebras-work.md @@ -0,0 +1,9 @@ +--- +"effect": minor +"@effect/experimental": patch +"@effect/platform": patch +"@effect/rpc": patch +"@effect/sql": patch +--- + +capture stack trace for tracing spans diff --git a/.changeset/tough-buses-brush.md b/.changeset/tough-buses-brush.md new file mode 100644 index 0000000000..eaf28d5156 --- /dev/null +++ b/.changeset/tough-buses-brush.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Consider Generator.next a cutpoint diff --git a/.changeset/warm-clouds-matter.md b/.changeset/warm-clouds-matter.md new file mode 100644 index 0000000000..b514686d55 --- /dev/null +++ b/.changeset/warm-clouds-matter.md @@ -0,0 +1,5 @@ +--- +"@effect/opentelemetry": patch +--- + +properly record exceptions in otel spans diff --git a/packages/effect/src/Array.ts b/packages/effect/src/Array.ts index 5b5ded67e8..b3085ffa5a 100644 --- a/packages/effect/src/Array.ts +++ b/packages/effect/src/Array.ts @@ -12,6 +12,7 @@ import type { LazyArg } from "./Function.js" import { dual, identity } from "./Function.js" import type { TypeLambda } from "./HKT.js" import * as readonlyArray from "./internal/array.js" +import * as doNotation from "./internal/doNotation.js" import * as EffectIterable from "./Iterable.js" import type { Option } from "./Option.js" import * as O from "./Option.js" @@ -2111,3 +2112,216 @@ export const cartesian: { 2, (self: ReadonlyArray, that: ReadonlyArray): Array<[A, B]> => cartesianWith(self, that, (a, b) => [a, b]) ) + +// ------------------------------------------------------------------------------------- +// do notation +// ------------------------------------------------------------------------------------- + +/** + * The "do simulation" for array allows you to sequentially apply operations to the elements of arrays, just as nested loops allow you to go through all combinations of elements in an arrays. + * + * It can be used to simulate "array comprehension". + * It's a technique that allows you to create new arrays by iterating over existing ones and applying specific **conditions** or **transformations** to the elements. It's like assembling a new collection from pieces of other collections based on certain rules. + * + * Here's how the do simulation works: + * + * 1. Start the do simulation using the `Do` value + * 2. Within the do simulation scope, you can use the `bind` function to define variables and bind them to `Array` values + * 3. You can accumulate multiple `bind` statements to define multiple variables within the scope + * 4. Inside the do simulation scope, you can also use the `let` function to define variables and bind them to simple values + * 5. Regular `Option` functions like `map` and `filter` can still be used within the do simulation. These functions will receive the accumulated variables as arguments within the scope + * + * @see {@link bindTo} + * @see {@link bind} + * @see {@link let_ let} + * + * @example + * import { Array as Arr, pipe } from "effect" + * const doResult = pipe( + * Arr.Do, + * Arr.bind("x", () => [1, 3, 5]), + * Arr.bind("y", () => [2, 4, 6]), + * Arr.filter(({ x, y }) => x < y), // condition + * Arr.map(({ x, y }) => [x, y] as const) // transformation + * ) + * assert.deepStrictEqual(doResult, [[1, 2], [1, 4], [1, 6], [3, 4], [3, 6], [5, 6]]) + * + * // equivalent + * const x = [1, 3, 5], + * y = [2, 4, 6], + * result = []; + * for(let i = 0; i < x.length; i++) { + * for(let j = 0; j < y.length; j++) { + * const _x = x[i], _y = y[j]; + * if(_x < _y) result.push([_x, _y] as const) + * } + * } + * + * @category do notation + * @since 3.2.0 + */ +export const Do: ReadonlyArray<{}> = of({}) + +/** + * The "do simulation" for array allows you to sequentially apply operations to the elements of arrays, just as nested loops allow you to go through all combinations of elements in an arrays. + * + * It can be used to simulate "array comprehension". + * It's a technique that allows you to create new arrays by iterating over existing ones and applying specific **conditions** or **transformations** to the elements. It's like assembling a new collection from pieces of other collections based on certain rules. + * + * Here's how the do simulation works: + * + * 1. Start the do simulation using the `Do` value + * 2. Within the do simulation scope, you can use the `bind` function to define variables and bind them to `Array` values + * 3. You can accumulate multiple `bind` statements to define multiple variables within the scope + * 4. Inside the do simulation scope, you can also use the `let` function to define variables and bind them to simple values + * 5. Regular `Option` functions like `map` and `filter` can still be used within the do simulation. These functions will receive the accumulated variables as arguments within the scope + * + * @see {@link bindTo} + * @see {@link Do} + * @see {@link let_ let} + * + * @example + * import { Array as Arr, pipe } from "effect" + * const doResult = pipe( + * Arr.Do, + * Arr.bind("x", () => [1, 3, 5]), + * Arr.bind("y", () => [2, 4, 6]), + * Arr.filter(({ x, y }) => x < y), // condition + * Arr.map(({ x, y }) => [x, y] as const) // transformation + * ) + * assert.deepStrictEqual(doResult, [[1, 2], [1, 4], [1, 6], [3, 4], [3, 6], [5, 6]]) + * + * // equivalent + * const x = [1, 3, 5], + * y = [2, 4, 6], + * result = []; + * for(let i = 0; i < x.length; i++) { + * for(let j = 0; j < y.length; j++) { + * const _x = x[i], _y = y[j]; + * if(_x < _y) result.push([_x, _y] as const) + * } + * } + * + * @category do notation + * @since 3.2.0 + */ +export const bind: { + ( + tag: Exclude, + f: (a: A) => ReadonlyArray + ): ( + self: ReadonlyArray + ) => Array<{ [K in N | keyof A]: K extends keyof A ? A[K] : B }> + ( + self: ReadonlyArray, + tag: Exclude, + f: (a: A) => ReadonlyArray + ): Array<{ [K in N | keyof A]: K extends keyof A ? A[K] : B }> +} = doNotation.bind(map, flatMap) as any + +/** + * The "do simulation" for array allows you to sequentially apply operations to the elements of arrays, just as nested loops allow you to go through all combinations of elements in an arrays. + * + * It can be used to simulate "array comprehension". + * It's a technique that allows you to create new arrays by iterating over existing ones and applying specific **conditions** or **transformations** to the elements. It's like assembling a new collection from pieces of other collections based on certain rules. + * + * Here's how the do simulation works: + * + * 1. Start the do simulation using the `Do` value + * 2. Within the do simulation scope, you can use the `bind` function to define variables and bind them to `Array` values + * 3. You can accumulate multiple `bind` statements to define multiple variables within the scope + * 4. Inside the do simulation scope, you can also use the `let` function to define variables and bind them to simple values + * 5. Regular `Option` functions like `map` and `filter` can still be used within the do simulation. These functions will receive the accumulated variables as arguments within the scope + * + * @see {@link bindTo} + * @see {@link Do} + * @see {@link let_ let} + * + * @example + * import { Array as Arr, pipe } from "effect" + * const doResult = pipe( + * Arr.Do, + * Arr.bind("x", () => [1, 3, 5]), + * Arr.bind("y", () => [2, 4, 6]), + * Arr.filter(({ x, y }) => x < y), // condition + * Arr.map(({ x, y }) => [x, y] as const) // transformation + * ) + * assert.deepStrictEqual(doResult, [[1, 2], [1, 4], [1, 6], [3, 4], [3, 6], [5, 6]]) + * + * // equivalent + * const x = [1, 3, 5], + * y = [2, 4, 6], + * result = []; + * for(let i = 0; i < x.length; i++) { + * for(let j = 0; j < y.length; j++) { + * const _x = x[i], _y = y[j]; + * if(_x < _y) result.push([_x, _y] as const) + * } + * } + * + * @category do notation + * @since 3.2.0 + */ +export const bindTo: { + (tag: N): (self: ReadonlyArray) => Array<{ [K in N]: A }> + (self: ReadonlyArray, tag: N): Array<{ [K in N]: A }> +} = doNotation.bindTo(map) as any + +const let_: { + ( + tag: Exclude, + f: (a: A) => B + ): (self: ReadonlyArray) => Array<{ [K in N | keyof A]: K extends keyof A ? A[K] : B }> + ( + self: ReadonlyArray, + tag: Exclude, + f: (a: A) => B + ): Array<{ [K in N | keyof A]: K extends keyof A ? A[K] : B }> +} = doNotation.let_(map) as any + +export { + /** + * The "do simulation" for array allows you to sequentially apply operations to the elements of arrays, just as nested loops allow you to go through all combinations of elements in an arrays. + * + * It can be used to simulate "array comprehension". + * It's a technique that allows you to create new arrays by iterating over existing ones and applying specific **conditions** or **transformations** to the elements. It's like assembling a new collection from pieces of other collections based on certain rules. + * + * Here's how the do simulation works: + * + * 1. Start the do simulation using the `Do` value + * 2. Within the do simulation scope, you can use the `bind` function to define variables and bind them to `Array` values + * 3. You can accumulate multiple `bind` statements to define multiple variables within the scope + * 4. Inside the do simulation scope, you can also use the `let` function to define variables and bind them to simple values + * 5. Regular `Option` functions like `map` and `filter` can still be used within the do simulation. These functions will receive the accumulated variables as arguments within the scope + * + * @see {@link bindTo} + * @see {@link bind} + * @see {@link Do} + * + * @example + * import { Array as Arr, pipe } from "effect" + * const doResult = pipe( + * Arr.Do, + * Arr.bind("x", () => [1, 3, 5]), + * Arr.bind("y", () => [2, 4, 6]), + * Arr.filter(({ x, y }) => x < y), // condition + * Arr.map(({ x, y }) => [x, y] as const) // transformation + * ) + * assert.deepStrictEqual(doResult, [[1, 2], [1, 4], [1, 6], [3, 4], [3, 6], [5, 6]]) + * + * // equivalent + * const x = [1, 3, 5], + * y = [2, 4, 6], + * result = []; + * for(let i = 0; i < x.length; i++) { + * for(let j = 0; j < y.length; j++) { + * const _x = x[i], _y = y[j]; + * if(_x < _y) result.push([_x, _y] as const) + * } + * } + * + * @category do notation + * @since 3.2.0 + */ + let_ as let +} diff --git a/packages/effect/src/Cause.ts b/packages/effect/src/Cause.ts index 9e90fb0b3f..a57557c00c 100644 --- a/packages/effect/src/Cause.ts +++ b/packages/effect/src/Cause.ts @@ -36,6 +36,7 @@ import type { Pipeable } from "./Pipeable.js" import type { Predicate, Refinement } from "./Predicate.js" import type * as Sink from "./Sink.js" import type * as Stream from "./Stream.js" +import type { Span } from "./Tracer.js" import type { Covariant, NoInfer } from "./Types.js" /** @@ -914,6 +915,22 @@ export const isUnknownException: (u: unknown) => u is UnknownException = core.is */ export const pretty: (cause: Cause) => string = internal.pretty +/** + * @since 3.2.0 + * @category models + */ +export interface PrettyError extends Error { + readonly span: Span | undefined +} + +/** + * Returns the specified `Cause` as a pretty-printed string. + * + * @since 3.2.0 + * @category rendering + */ +export const prettyErrors: (cause: Cause) => Array = internal.prettyErrors + /** * Returns the original, unproxied, instance of a thrown error * diff --git a/packages/effect/src/Chunk.ts b/packages/effect/src/Chunk.ts index 1ee6cbd18d..cb6ccff319 100644 --- a/packages/effect/src/Chunk.ts +++ b/packages/effect/src/Chunk.ts @@ -1387,3 +1387,33 @@ export const reduceRight: { (b: B, f: (b: B, a: A, i: number) => B): (self: Chunk) => B (self: Chunk, b: B, f: (b: B, a: A, i: number) => B): B } = RA.reduceRight + +/** + * Creates a `Chunk` of values not included in the other given `Chunk` using the provided `isEquivalent` function. + * The order and references of result values are determined by the first `Chunk`. + * + * @since 3.2.0 + */ +export const differenceWith = (isEquivalent: (self: A, that: A) => boolean): { + (that: Chunk): (self: Chunk) => Chunk + (self: Chunk, that: Chunk): Chunk +} => { + return dual( + 2, + (self: Chunk, that: Chunk): Chunk => unsafeFromArray(RA.differenceWith(isEquivalent)(that, self)) + ) +} + +/** + * Creates a `Chunk` of values not included in the other given `Chunk`. + * The order and references of result values are determined by the first `Chunk`. + * + * @since 3.2.0 + */ +export const difference: { + (that: Chunk): (self: Chunk) => Chunk + (self: Chunk, that: Chunk): Chunk +} = dual( + 2, + (self: Chunk, that: Chunk): Chunk => unsafeFromArray(RA.difference(that, self)) +) diff --git a/packages/effect/src/Data.ts b/packages/effect/src/Data.ts index 1554b33ca6..dacabf0590 100644 --- a/packages/effect/src/Data.ts +++ b/packages/effect/src/Data.ts @@ -7,6 +7,7 @@ import * as internal from "./internal/data.js" import { StructuralPrototype } from "./internal/effectable.js" import * as Predicate from "./Predicate.js" import type * as Types from "./Types.js" +import type { Unify } from "./Unify.js" /** * @since 2.0.0 @@ -336,13 +337,63 @@ export declare namespace TaggedEnum { } & { readonly $is: (tag: Tag) => (u: unknown) => u is Extract - readonly $match: < + readonly $match: { + < + Cases extends { + readonly [Tag in A["_tag"]]: (args: Extract) => any + } + >(cases: Cases): (value: A) => Unify> + < + Cases extends { + readonly [Tag in A["_tag"]]: (args: Extract) => any + } + >(value: A, cases: Cases): Unify> + } + } + > + + /** + * @since 3.2.0 + */ + export interface GenericMatchers> { + readonly $is: ( + tag: Tag + ) => { + >( + u: T + ): u is T & { readonly _tag: Tag } + (u: unknown): u is Extract, { readonly _tag: Tag }> + } + readonly $match: { + < + A, + B, + C, + D, + Cases extends { + readonly [Tag in Z["taggedEnum"]["_tag"]]: ( + args: Extract, { readonly _tag: Tag }> + ) => any + } + >( + cases: Cases + ): (self: TaggedEnum.Kind) => Unify> + < + A, + B, + C, + D, Cases extends { - readonly [Tag in A["_tag"]]: (args: Extract) => any + readonly [Tag in Z["taggedEnum"]["_tag"]]: ( + args: Extract, { readonly _tag: Tag }> + ) => any } - >(cases: Cases) => (value: A) => ReturnType + >( + self: TaggedEnum.Kind, + cases: Cases + ): Unify> } - > + } } /** @@ -379,52 +430,60 @@ export declare namespace TaggedEnum { * @since 2.0.0 */ export const taggedEnum: { - >(): { - readonly [Tag in Z["taggedEnum"]["_tag"]]: ( - args: TaggedEnum.Args< - TaggedEnum.Kind, - Tag, - Extract, { readonly _tag: Tag }> - > - ) => TaggedEnum.Value, Tag> - } + >(): Types.Simplify< + { + readonly [Tag in Z["taggedEnum"]["_tag"]]: ( + args: TaggedEnum.Args< + TaggedEnum.Kind, + Tag, + Extract, { readonly _tag: Tag }> + > + ) => TaggedEnum.Value, Tag> + } & TaggedEnum.GenericMatchers + > - >(): { - readonly [Tag in Z["taggedEnum"]["_tag"]]: ( - args: TaggedEnum.Args< - TaggedEnum.Kind, - Tag, - Extract, { readonly _tag: Tag }> - > - ) => TaggedEnum.Value, Tag> - } + >(): Types.Simplify< + { + readonly [Tag in Z["taggedEnum"]["_tag"]]: ( + args: TaggedEnum.Args< + TaggedEnum.Kind, + Tag, + Extract, { readonly _tag: Tag }> + > + ) => TaggedEnum.Value, Tag> + } & TaggedEnum.GenericMatchers + > - >(): { - readonly [Tag in Z["taggedEnum"]["_tag"]]: ( - args: TaggedEnum.Args< - TaggedEnum.Kind, - Tag, - Extract, { readonly _tag: Tag }> - > - ) => TaggedEnum.Value, Tag> - } + >(): Types.Simplify< + { + readonly [Tag in Z["taggedEnum"]["_tag"]]: ( + args: TaggedEnum.Args< + TaggedEnum.Kind, + Tag, + Extract, { readonly _tag: Tag }> + > + ) => TaggedEnum.Value, Tag> + } & TaggedEnum.GenericMatchers + > - >(): { - readonly [Tag in Z["taggedEnum"]["_tag"]]: ( - args: TaggedEnum.Args< - TaggedEnum.Kind, - Tag, - Extract, { readonly _tag: Tag }> - > - ) => TaggedEnum.Value, Tag> - } + >(): Types.Simplify< + { + readonly [Tag in Z["taggedEnum"]["_tag"]]: ( + args: TaggedEnum.Args< + TaggedEnum.Kind, + Tag, + Extract, { readonly _tag: Tag }> + > + ) => TaggedEnum.Value, Tag> + } & TaggedEnum.GenericMatchers + > (): TaggedEnum.Constructor } = () => new Proxy({}, { get(_target, tag, _receiver) { if (tag === "$is") { - return taggedIs + return Predicate.isTagged } else if (tag === "$match") { return taggedMatch } @@ -432,19 +491,33 @@ export const taggedEnum: { } }) as any -function taggedIs(tag: Tag) { - return Predicate.isTagged(tag) -} - function taggedMatch< A extends { readonly _tag: string }, Cases extends { readonly [K in A["_tag"]]: (args: Extract) => any } ->(cases: Cases) { - return function(value: A): ReturnType { - return cases[value._tag as A["_tag"]](value as any) +>(self: A, cases: Cases): ReturnType +function taggedMatch< + A extends { readonly _tag: string }, + Cases extends { + readonly [K in A["_tag"]]: (args: Extract) => any + } +>(cases: Cases): (value: A) => ReturnType +function taggedMatch< + A extends { readonly _tag: string }, + Cases extends { + readonly [K in A["_tag"]]: (args: Extract) => any + } +>(): any { + if (arguments.length === 1) { + const cases = arguments[0] as Cases + return function(value: A): ReturnType { + return cases[value._tag as A["_tag"]](value as any) + } } + const value = arguments[0] as A + const cases = arguments[1] as Cases + return cases[value._tag as A["_tag"]](value as any) } /** diff --git a/packages/effect/src/Effect.ts b/packages/effect/src/Effect.ts index e5048fda26..b2bbb06868 100644 --- a/packages/effect/src/Effect.ts +++ b/packages/effect/src/Effect.ts @@ -5435,6 +5435,46 @@ export const withSpan: { ): Effect> } = effect.withSpan +/** + * Wraps a function that returns an effect with a new span for tracing. + * + * @since 3.2.0 + * @category models + */ +export interface FunctionWithSpanOptions { + readonly name: string + readonly attributes?: Record | undefined + readonly links?: ReadonlyArray | undefined + readonly parent?: Tracer.AnySpan | undefined + readonly root?: boolean | undefined + readonly context?: Context.Context | undefined + readonly kind?: Tracer.SpanKind | undefined +} + +/** + * Wraps a function that returns an effect with a new span for tracing. + * + * @since 3.2.0 + * @category tracing + * @example + * import { Effect } from "effect" + * + * const getTodo = Effect.functionWithSpan({ + * body: (id: number) => Effect.succeed(`Got todo ${id}!`), + * options: (id) => ({ + * name: `getTodo-${id}`, + * attributes: { id } + * }) + * }) + */ +export const functionWithSpan: , Ret extends Effect>( + options: { + readonly body: (...args: Args) => Ret + readonly options: FunctionWithSpanOptions | ((...args: Args) => FunctionWithSpanOptions) + readonly captureStackTrace?: boolean | undefined + } +) => (...args: Args) => Unify.Unify = effect.functionWithSpan + /** * Wraps the effect with a new span for tracing. * diff --git a/packages/effect/src/Stream.ts b/packages/effect/src/Stream.ts index 83d0564512..ea1e2b8c12 100644 --- a/packages/effect/src/Stream.ts +++ b/packages/effect/src/Stream.ts @@ -22,6 +22,7 @@ import type { Pipeable } from "./Pipeable.js" import type { Predicate, Refinement } from "./Predicate.js" import type * as PubSub from "./PubSub.js" import type * as Queue from "./Queue.js" +import type { Runtime } from "./Runtime.js" import type * as Schedule from "./Schedule.js" import type * as Scope from "./Scope.js" import type * as Sink from "./Sink.js" @@ -3859,7 +3860,36 @@ export const toQueueOfElements: { * @since 2.0.0 * @category destructors */ -export const toReadableStream: (source: Stream) => ReadableStream = internal.toReadableStream +export const toReadableStream: (self: Stream) => ReadableStream = internal.toReadableStream + +/** + * Converts the stream to a `Effect`. + * + * See https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream. + * + * @since 2.0.0 + * @category destructors + */ +export const toReadableStreamEffect: (self: Stream) => Effect.Effect, never, R> = + internal.toReadableStreamEffect + +/** + * Converts the stream to a `ReadableStream` using the provided runtime. + * + * See https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream. + * + * @since 2.0.0 + * @category destructors + */ +export const toReadableStreamRuntime: { + ( + runtime: Runtime + ): (self: Stream) => ReadableStream + ( + self: Stream, + runtime: Runtime + ): ReadableStream +} = internal.toReadableStreamRuntime /** * Applies the transducer to the stream and emits its outputs. diff --git a/packages/effect/src/Tracer.ts b/packages/effect/src/Tracer.ts index 37996a5852..69a33e4f69 100644 --- a/packages/effect/src/Tracer.ts +++ b/packages/effect/src/Tracer.ts @@ -92,6 +92,7 @@ export interface SpanOptions { readonly root?: boolean | undefined readonly context?: Context.Context | undefined readonly kind?: SpanKind | undefined + readonly captureStackTrace?: boolean | string | undefined } /** diff --git a/packages/effect/src/internal/cause.ts b/packages/effect/src/internal/cause.ts index b7bc12babf..d4714d8841 100644 --- a/packages/effect/src/internal/cause.ts +++ b/packages/effect/src/internal/cause.ts @@ -10,8 +10,8 @@ import * as HashSet from "../HashSet.js" import { NodeInspectSymbol, toJSON } from "../Inspectable.js" import * as Option from "../Option.js" import { pipeArguments } from "../Pipeable.js" -import { hasProperty, isFunction } from "../Predicate.js" import type { Predicate, Refinement } from "../Predicate.js" +import { hasProperty, isFunction } from "../Predicate.js" import type { AnySpan, Span } from "../Tracer.js" import type { NoInfer } from "../Types.js" import { getBugErrorMessage } from "./errors.js" @@ -968,53 +968,45 @@ export const reduceWithContext = dual< // Pretty Printing // ----------------------------------------------------------------------------- -const filterStack = (stack: string) => { - const lines = stack.split("\n") - const out: Array = [] - for (let i = 0; i < lines.length; i++) { - out.push(lines[i].replace(/at .*effect_instruction_i.*\((.*)\)/, "at $1")) - if (lines[i].includes("effect_instruction_i")) { - return out.join("\n") - } - } - return out.join("\n") -} - /** @internal */ export const pretty = (cause: Cause.Cause): string => { if (isInterruptedOnly(cause)) { return "All fibers interrupted without errors." } - const final = prettyErrors(cause).map((e) => { - let message = e.message - if (e.stack) { - message += `\r\n${filterStack(e.stack)}` - } - if (e.span) { - let current: Span | AnySpan | undefined = e.span - let i = 0 - while (current && current._tag === "Span" && i < 10) { - message += `\r\n at ${current.name}` - current = Option.getOrUndefined(current.parent) - i++ + return prettyErrors(cause).map((e) => e.stack).join("\n") +} + +class PrettyError extends globalThis.Error implements Cause.PrettyError { + span: undefined | Span = undefined + constructor(originalError: unknown) { + const prevLimit = Error.stackTraceLimit + Error.stackTraceLimit = 0 + super(prettyErrorMessage(originalError)) + Error.stackTraceLimit = prevLimit + + this.name = originalError instanceof Error ? originalError.name : "Error" + if (typeof originalError === "object" && originalError !== null) { + if (spanSymbol in originalError) { + this.span = originalError[spanSymbol] as Span } + Object.keys(originalError).forEach((key) => { + if (!(key in this)) { + // @ts-expect-error + this[key] = originalError[key] + } + }) } - return message - }).join("\r\n") - return final -} + this.stack = prettyErrorStack( + this.message, + originalError instanceof Error && originalError.stack + ? originalError.stack + : "", + this.span + ) + } -class PrettyError { - constructor( - readonly message: string, - readonly stack: string | undefined, - readonly span: Span | undefined - ) {} toJSON() { - const out: any = { message: this.message } - if (this.stack) { - out.stack = this.stack - } + const out: any = { message: this.message, stack: this.stack } if (this.span) { out.span = this.span } @@ -1058,29 +1050,55 @@ export const prettyErrorMessage = (u: unknown): string => { return `Error: ${JSON.stringify(u)}` } -const spanSymbol = Symbol.for("effect/SpanAnnotation") +const locationRegex = /\((.*)\)/ + +const prettyErrorStack = (message: string, stack: string, span?: Span | undefined): string => { + const out: Array = [message] + const lines = stack.split("\n") -const defaultRenderError = (error: unknown): PrettyError => { - const span: any = hasProperty(error, spanSymbol) && error[spanSymbol] - if (error instanceof Error) { - return new PrettyError( - prettyErrorMessage(error), - error.stack?.split("\n").filter((_) => _.match(/at (.*)/)).join("\n"), - span + for (let i = 1; i < lines.length; i++) { + if (lines[i].includes("effect_cutpoint") || lines[i].includes("Generator.next")) { + break + } + out.push( + lines[i].replace(/at .*effect_instruction_i.*\((.*)\)/, "at $1").replace(/EffectPrimitive\.\w+/, "") ) + if (lines[i].includes("effect_instruction_i")) { + break + } } - return new PrettyError(prettyErrorMessage(error), void 0, span) + + if (span) { + let current: Span | AnySpan | undefined = span + let i = 0 + while (current && current._tag === "Span" && i < 10) { + const stack = current.attributes.get("code.stacktrace") + if (typeof stack === "string") { + const locationMatch = stack.match(locationRegex) + const location = locationMatch ? locationMatch[1] : stack.replace(/^at /, "") + out.push(` at ${current.name} (${location})`) + } else { + out.push(` at ${current.name}`) + } + current = Option.getOrUndefined(current.parent) + i++ + } + } + + return out.join("\n") } +const spanSymbol = Symbol.for("effect/SpanAnnotation") + /** @internal */ -export const prettyErrors = (cause: Cause.Cause): ReadonlyArray => +export const prettyErrors = (cause: Cause.Cause): Array => reduceWithContext(cause, void 0, { - emptyCase: (): ReadonlyArray => [], + emptyCase: (): Array => [], dieCase: (_, unknownError) => { - return [defaultRenderError(unknownError)] + return [new PrettyError(unknownError)] }, failCase: (_, error) => { - return [defaultRenderError(error)] + return [new PrettyError(error)] }, interruptCase: () => [], parallelCase: (_, l, r) => [...l, ...r], diff --git a/packages/effect/src/internal/channel.ts b/packages/effect/src/internal/channel.ts index d5c33048b1..fe36316738 100644 --- a/packages/effect/src/internal/channel.ts +++ b/packages/effect/src/internal/channel.ts @@ -2310,29 +2310,47 @@ export const updateService = dual< ))) /** @internal */ -export const withSpan = dual< +export const withSpan: { ( name: string, options?: Tracer.SpanOptions - ) => ( + ): ( self: Channel.Channel - ) => Channel.Channel>, + ) => Channel.Channel> ( self: Channel.Channel, name: string, options?: Tracer.SpanOptions - ) => Channel.Channel> ->(3, (self, name, options) => - unwrapScoped( - Effect.flatMap( - Effect.context(), - (context) => - Effect.map( - Effect.makeSpanScoped(name, options), - (span) => core.provideContext(self, Context.add(context, tracer.spanTag, span)) - ) + ): Channel.Channel> +} = function() { + const dataFirst = typeof arguments[0] !== "string" + const name = dataFirst ? arguments[1] : arguments[0] + const options = tracer.addSpanStackTrace(dataFirst ? arguments[2] : arguments[1]) + if (dataFirst) { + const self = arguments[0] + return unwrapScoped( + Effect.flatMap( + Effect.context(), + (context) => + Effect.map( + Effect.makeSpanScoped(name, options), + (span) => core.provideContext(self, Context.add(context, tracer.spanTag, span)) + ) + ) + ) + } + return (self: Effect.Effect) => + unwrapScoped( + Effect.flatMap( + Effect.context(), + (context) => + Effect.map( + Effect.makeSpanScoped(name, options), + (span) => core.provideContext(self, Context.add(context, tracer.spanTag, span)) + ) + ) ) - ) as any) +} as any /** @internal */ export const writeAll = ( diff --git a/packages/effect/src/internal/core-effect.ts b/packages/effect/src/internal/core-effect.ts index a20595a956..d8fed1c20a 100644 --- a/packages/effect/src/internal/core-effect.ts +++ b/packages/effect/src/internal/core-effect.ts @@ -26,6 +26,7 @@ import * as Ref from "../Ref.js" import type * as runtimeFlagsPatch from "../RuntimeFlagsPatch.js" import * as Tracer from "../Tracer.js" import type { NoInfer } from "../Types.js" +import type { Unify } from "../Unify.js" import { yieldWrapGet } from "../Utils.js" import * as internalCause from "./cause.js" import { clockTag } from "./clock.js" @@ -2011,7 +2012,7 @@ const bigint0 = BigInt(0) export const unsafeMakeSpan = ( fiber: FiberRuntime, name: string, - options?: Tracer.SpanOptions + options: Tracer.SpanOptions ) => { const enabled = fiber.getFiberRef(core.currentTracerEnabled) if (enabled === false) { @@ -2029,34 +2030,38 @@ export const unsafeMakeSpan = ( const annotationsFromEnv = FiberRefs.get(fiberRefs, core.currentTracerSpanAnnotations) const linksFromEnv = FiberRefs.get(fiberRefs, core.currentTracerSpanLinks) - const parent = options?.parent + const parent = options.parent ? Option.some(options.parent) - : options?.root + : options.root ? Option.none() : Context.getOption(context, internalTracer.spanTag) const links = linksFromEnv._tag === "Some" ? - options?.links !== undefined ? + options.links !== undefined ? [ ...Chunk.toReadonlyArray(linksFromEnv.value), - ...(options?.links ?? []) + ...(options.links ?? []) ] : Chunk.toReadonlyArray(linksFromEnv.value) : - options?.links ?? Arr.empty() + options.links ?? Arr.empty() const span = tracer.span( name, parent, - options?.context ?? Context.empty(), + options.context ?? Context.empty(), links, timingEnabled ? clock.unsafeCurrentTimeNanos() : bigint0, - options?.kind ?? "internal" + options.kind ?? "internal" ) + if (typeof options.captureStackTrace === "string") { + span.attribute("code.stacktrace", options.captureStackTrace) + } + if (annotationsFromEnv._tag === "Some") { HashMap.forEach(annotationsFromEnv.value, (value, key) => span.attribute(key, value)) } - if (options?.attributes !== undefined) { + if (options.attributes !== undefined) { Object.entries(options.attributes).forEach(([k, v]) => span.attribute(k, v)) } @@ -2067,7 +2072,10 @@ export const unsafeMakeSpan = ( export const makeSpan = ( name: string, options?: Tracer.SpanOptions -): Effect.Effect => core.withFiberRuntime((fiber) => core.succeed(unsafeMakeSpan(fiber, name, options))) +): Effect.Effect => { + options = internalTracer.addSpanStackTrace(options) + return core.withFiberRuntime((fiber) => core.succeed(unsafeMakeSpan(fiber, name, options))) +} /* @internal */ export const spanAnnotations: Effect.Effect> = core @@ -2092,7 +2100,7 @@ export const useSpan: { evaluate: (span: Tracer.Span) => Effect.Effect ] ) => { - const options: Tracer.SpanOptions | undefined = args.length === 1 ? undefined : args[0] + const options = internalTracer.addSpanStackTrace(args.length === 1 ? undefined : args[0]) const evaluate: (span: Tracer.Span) => Effect.Effect = args[args.length - 1] return core.withFiberRuntime((fiber) => { @@ -2118,25 +2126,66 @@ export const withParentSpan = dual< >(2, (self, span) => provideService(self, internalTracer.spanTag, span)) /** @internal */ -export const withSpan = dual< +export const withSpan: { ( name: string, - options?: Tracer.SpanOptions - ) => (self: Effect.Effect) => Effect.Effect>, + options?: Tracer.SpanOptions | undefined + ): (self: Effect.Effect) => Effect.Effect> ( self: Effect.Effect, name: string, - options?: Tracer.SpanOptions - ) => Effect.Effect> ->( - (args) => typeof args[0] !== "string", - (self, name, options) => - useSpan( - name, - options ?? {}, - (span) => withParentSpan(self, span) - ) -) + options?: Tracer.SpanOptions | undefined + ): Effect.Effect> +} = function() { + const dataFirst = typeof arguments[0] !== "string" + const name = dataFirst ? arguments[1] : arguments[0] + const options = internalTracer.addSpanStackTrace(dataFirst ? arguments[2] : arguments[1]) + if (dataFirst) { + const self = arguments[0] + return useSpan(name, options, (span) => withParentSpan(self, span)) + } + return (self: Effect.Effect) => useSpan(name, options, (span) => withParentSpan(self, span)) +} as any + +export const functionWithSpan = , Ret extends Effect.Effect>( + options: { + readonly body: (...args: Args) => Ret + readonly options: Effect.FunctionWithSpanOptions | ((...args: Args) => Effect.FunctionWithSpanOptions) + readonly captureStackTrace?: boolean | undefined + } +): (...args: Args) => Unify => + (function(this: any) { + let captureStackTrace: string | boolean = options.captureStackTrace ?? false + if (options.captureStackTrace !== false) { + const limit = Error.stackTraceLimit + Error.stackTraceLimit = 2 + const error = new Error() + Error.stackTraceLimit = limit + if (error.stack !== undefined) { + const stack = error.stack.trim().split("\n") + captureStackTrace = stack.slice(2).join("\n").trim() + } + } + // eslint-disable-next-line @typescript-eslint/no-this-alias + const self = this + const args = arguments + return core.suspend(() => { + const opts = typeof options.options === "function" + ? options.options.apply(null, arguments as any) + : options.options + + return withSpan( + core.custom(options.body, function() { + return this.effect_instruction_i0.apply(self, args as any) + }), + opts.name, + { + ...opts, + captureStackTrace + } + ) + }) + }) as any // ------------------------------------------------------------------------------------- // optionality diff --git a/packages/effect/src/internal/core.ts b/packages/effect/src/internal/core.ts index 2147a2b57b..0924417510 100644 --- a/packages/effect/src/internal/core.ts +++ b/packages/effect/src/internal/core.ts @@ -459,6 +459,10 @@ export const as: { /* @internal */ export const asVoid = (self: Effect.Effect): Effect.Effect => as(self, void 0) +function commitCallCutpoint(this: any) { + return this.effect_cutpoint() +} + /* @internal */ export const custom: { (i0: X, body: (this: { effect_instruction_i0: X }) => Effect.Effect): Effect.Effect @@ -477,23 +481,24 @@ export const custom: { ): Effect.Effect } = function() { const wrapper = new EffectPrimitive(OpCodes.OP_COMMIT) as any + wrapper.commit = commitCallCutpoint switch (arguments.length) { case 2: { wrapper.effect_instruction_i0 = arguments[0] - wrapper.commit = arguments[1] + wrapper.effect_cutpoint = arguments[1] break } case 3: { wrapper.effect_instruction_i0 = arguments[0] wrapper.effect_instruction_i1 = arguments[1] - wrapper.commit = arguments[2] + wrapper.effect_cutpoint = arguments[2] break } case 4: { wrapper.effect_instruction_i0 = arguments[0] wrapper.effect_instruction_i1 = arguments[1] wrapper.effect_instruction_i2 = arguments[2] - wrapper.commit = arguments[3] + wrapper.effect_cutpoint = arguments[3] break } default: { diff --git a/packages/effect/src/internal/fiberRuntime.ts b/packages/effect/src/internal/fiberRuntime.ts index 15fd069f90..f61565e808 100644 --- a/packages/effect/src/internal/fiberRuntime.ts +++ b/packages/effect/src/internal/fiberRuntime.ts @@ -1335,7 +1335,7 @@ export class FiberRuntime implements Fiber.RuntimeFi internalCause.sequential(internalCause.die(e), internalCause.interrupt(FiberId.none)) ) } else { - cur = core.exitFailCause(internalCause.die(e)) + cur = core.die(e) } } } @@ -3622,8 +3622,9 @@ export const interruptWhenPossible = dual< export const makeSpanScoped = ( name: string, options?: Tracer.SpanOptions | undefined -): Effect.Effect => - core.uninterruptible( +): Effect.Effect => { + options = tracer.addSpanStackTrace(options) + return core.uninterruptible( core.withFiberRuntime((fiber) => { const scope = Context.unsafeGet(fiber.getFiberRef(core.currentContext), scopeTag) const span = internalEffect.unsafeMakeSpan(fiber, name, options) @@ -3641,27 +3642,37 @@ export const makeSpanScoped = ( ) }) ) +} /* @internal */ export const withTracerScoped = (value: Tracer.Tracer): Effect.Effect => fiberRefLocallyScopedWith(defaultServices.currentServices, Context.add(tracer.tracerTag, value)) /** @internal */ -export const withSpanScoped = dual< +export const withSpanScoped: { ( name: string, options?: Tracer.SpanOptions - ) => (self: Effect.Effect) => Effect.Effect | Scope.Scope>, + ): (self: Effect.Effect) => Effect.Effect> ( self: Effect.Effect, name: string, options?: Tracer.SpanOptions - ) => Effect.Effect | Scope.Scope> ->( - (args) => typeof args[0] !== "string", - (self, name, options) => + ): Effect.Effect> +} = function() { + const dataFirst = typeof arguments[0] !== "string" + const name = dataFirst ? arguments[1] : arguments[0] + const options = tracer.addSpanStackTrace(dataFirst ? arguments[2] : arguments[1]) + if (dataFirst) { + const self = arguments[0] + return core.flatMap( + makeSpanScoped(name, tracer.addSpanStackTrace(options)), + (span) => internalEffect.provideService(self, tracer.spanTag, span) + ) + } + return (self: Effect.Effect) => core.flatMap( - makeSpanScoped(name, options), + makeSpanScoped(name, tracer.addSpanStackTrace(options)), (span) => internalEffect.provideService(self, tracer.spanTag, span) ) -) +} as any diff --git a/packages/effect/src/internal/layer.ts b/packages/effect/src/internal/layer.ts index bb88a424c7..749ab4ccff 100644 --- a/packages/effect/src/internal/layer.ts +++ b/packages/effect/src/internal/layer.ts @@ -1113,7 +1113,7 @@ export const unwrapScoped = ( // ----------------------------------------------------------------------------- /** @internal */ -export const withSpan = dual< +export const withSpan: { ( name: string, options?: Tracer.SpanOptions & { @@ -1121,7 +1121,7 @@ export const withSpan = dual< | ((span: Tracer.Span, exit: Exit.Exit) => Effect.Effect) | undefined } - ) => (self: Layer.Layer) => Layer.Layer>, + ): (self: Layer.Layer) => Layer.Layer> ( self: Layer.Layer, name: string, @@ -1130,19 +1130,42 @@ export const withSpan = dual< | ((span: Tracer.Span, exit: Exit.Exit) => Effect.Effect) | undefined } - ) => Layer.Layer> ->((args) => isLayer(args[0]), (self, name, options) => - unwrapScoped( - core.map( - options?.onEnd - ? core.tap( - fiberRuntime.makeSpanScoped(name, options), - (span) => fiberRuntime.addFinalizer((exit) => options.onEnd!(span, exit)) - ) - : fiberRuntime.makeSpanScoped(name, options), - (span) => withParentSpan(self, span) + ): Layer.Layer> +} = function() { + const dataFirst = typeof arguments[0] !== "string" + const name = dataFirst ? arguments[1] : arguments[0] + const options = tracer.addSpanStackTrace(dataFirst ? arguments[2] : arguments[1]) as Tracer.SpanOptions & { + readonly onEnd?: + | ((span: Tracer.Span, exit: Exit.Exit) => Effect.Effect) + | undefined + } + if (dataFirst) { + const self = arguments[0] + return unwrapScoped( + core.map( + options?.onEnd + ? core.tap( + fiberRuntime.makeSpanScoped(name, options), + (span) => fiberRuntime.addFinalizer((exit) => options.onEnd!(span, exit)) + ) + : fiberRuntime.makeSpanScoped(name, options), + (span) => withParentSpan(self, span) + ) ) - )) + } + return (self: Layer.Layer) => + unwrapScoped( + core.map( + options?.onEnd + ? core.tap( + fiberRuntime.makeSpanScoped(name, options), + (span) => fiberRuntime.addFinalizer((exit) => options.onEnd!(span, exit)) + ) + : fiberRuntime.makeSpanScoped(name, options), + (span) => withParentSpan(self, span) + ) + ) +} as any /** @internal */ export const withParentSpan = dual< diff --git a/packages/effect/src/internal/layer/circular.ts b/packages/effect/src/internal/layer/circular.ts index be46420d4d..791b143fcd 100644 --- a/packages/effect/src/internal/layer/circular.ts +++ b/packages/effect/src/internal/layer/circular.ts @@ -196,8 +196,9 @@ export const span = ( | ((span: Tracer.Span, exit: Exit.Exit) => Effect.Effect) | undefined } -): Layer.Layer => - layer.scoped( +): Layer.Layer => { + options = tracer.addSpanStackTrace(options) as any + return layer.scoped( tracer.spanTag, options?.onEnd ? core.tap( @@ -206,6 +207,7 @@ export const span = ( ) : fiberRuntime.makeSpanScoped(name, options) ) +} /** @internal */ export const setTracer = (tracer: Tracer.Tracer): Layer.Layer => diff --git a/packages/effect/src/internal/runtime.ts b/packages/effect/src/internal/runtime.ts index d7fa606af0..bf93d5b7eb 100644 --- a/packages/effect/src/internal/runtime.ts +++ b/packages/effect/src/internal/runtime.ts @@ -173,12 +173,16 @@ class FiberFailureImpl extends Error implements Runtime.FiberFailure { const prettyErrors = InternalCause.prettyErrors(cause) if (prettyErrors.length > 0) { const head = prettyErrors[0] - this.name = head.message.split(":")[0] - this.message = head.message.substring(this.name.length + 2) - this.stack = InternalCause.pretty(cause) + this.name = head.name + this.message = head.message + this.stack = head.stack! } this.name = `(FiberFailure) ${this.name}` + + if (this.message === undefined || this.message.length === 0) { + this.message = "An error has occurred" + } } toJSON(): unknown { @@ -187,8 +191,9 @@ class FiberFailureImpl extends Error implements Runtime.FiberFailure { cause: this[FiberFailureCauseId].toJSON() } } + toString(): string { - return "(FiberFailure) " + InternalCause.pretty(this[FiberFailureCauseId]) + return "(FiberFailure) " + (this.stack ?? this.message) } [Inspectable.NodeInspectSymbol](): unknown { return this.toString() diff --git a/packages/effect/src/internal/stream.ts b/packages/effect/src/internal/stream.ts index f095b2b7d1..8d84d49427 100644 --- a/packages/effect/src/internal/stream.ts +++ b/packages/effect/src/internal/stream.ts @@ -50,6 +50,7 @@ import * as SinkEndReason from "./stream/sinkEndReason.js" import * as ZipAllState from "./stream/zipAllState.js" import * as ZipChunksState from "./stream/zipChunksState.js" import * as InternalTake from "./take.js" +import * as InternalTracer from "./tracer.js" /** @internal */ const StreamSymbolKey = "effect/Stream" @@ -6541,16 +6542,30 @@ export const toQueueOfElements = dual< )) /** @internal */ -export const toReadableStream = (source: Stream.Stream) => { - let pull: Effect.Effect +export const toReadableStream = (self: Stream.Stream) => + toReadableStreamRuntime(self, Runtime.defaultRuntime) + +/** @internal */ +export const toReadableStreamEffect = (self: Stream.Stream) => + Effect.map(Effect.runtime(), (runtime) => toReadableStreamRuntime(self, runtime)) + +/** @internal */ +export const toReadableStreamRuntime = dual< + (runtime: Runtime.Runtime) => (self: Stream.Stream) => ReadableStream, + (self: Stream.Stream, runtime: Runtime.Runtime) => ReadableStream +>(2, (self: Stream.Stream, runtime: Runtime.Runtime): ReadableStream => { + const runSync = Runtime.runSync(runtime) + const runPromise = Runtime.runPromise(runtime) + + let pull: Effect.Effect let scope: Scope.CloseableScope return new ReadableStream({ start(controller) { - scope = Effect.runSync(Scope.make()) + scope = runSync(Scope.make()) pull = pipe( - toPull(source), - Scope.use(scope), - Effect.runSync, + toPull(self), + Scope.extend(scope), + runSync, Effect.tap((chunk) => Effect.sync(() => { Chunk.map(chunk, (a) => { @@ -6573,13 +6588,13 @@ export const toReadableStream = (source: Stream.Stream) => { ) }, pull() { - return Effect.runPromise(pull) + return runPromise(pull) }, cancel() { - return Effect.runPromise(Scope.close(scope, Exit.void)) + return runPromise(Scope.close(scope, Exit.void)) } }) -} +}) /** @internal */ export const transduce = dual< @@ -6801,17 +6816,26 @@ export const whenEffect = dual< ) /** @internal */ -export const withSpan = dual< +export const withSpan: { ( name: string, options?: Tracer.SpanOptions - ) => (self: Stream.Stream) => Stream.Stream>, + ): (self: Stream.Stream) => Stream.Stream> ( self: Stream.Stream, name: string, options?: Tracer.SpanOptions - ) => Stream.Stream> ->(3, (self, name, options) => new StreamImpl(channel.withSpan(toChannel(self), name, options))) + ): Stream.Stream> +} = function() { + const dataFirst = typeof arguments[0] !== "string" + const name = dataFirst ? arguments[1] : arguments[0] + const options = InternalTracer.addSpanStackTrace(dataFirst ? arguments[2] : arguments[1]) + if (dataFirst) { + const self = arguments[0] + return new StreamImpl(channel.withSpan(toChannel(self), name, options)) + } + return (self: Stream.Stream) => new StreamImpl(channel.withSpan(toChannel(self), name, options)) +} as any /** @internal */ export const zip = dual< diff --git a/packages/effect/src/internal/tracer.ts b/packages/effect/src/internal/tracer.ts index 7fa29fb9b0..a4c8ac4887 100644 --- a/packages/effect/src/internal/tracer.ts +++ b/packages/effect/src/internal/tracer.ts @@ -106,3 +106,24 @@ export const externalSpan = (options: { sampled: options.sampled ?? true, context: options.context ?? Context.empty() }) + +/** @internal */ +export const addSpanStackTrace = (options: Tracer.SpanOptions | undefined): Tracer.SpanOptions => { + if (options?.captureStackTrace === false) { + return options + } else if (options?.captureStackTrace !== undefined && typeof options.captureStackTrace !== "boolean") { + return options + } + const limit = Error.stackTraceLimit + Error.stackTraceLimit = 3 + const traceError = new Error() + Error.stackTraceLimit = limit + if (traceError.stack === undefined) { + return { ...options, captureStackTrace: false } + } + const stack = traceError.stack.split("\n") + if (!stack[3]) { + return { ...options, captureStackTrace: false } + } + return { ...options, captureStackTrace: stack[3].trim() } +} diff --git a/packages/effect/test/Array.test.ts b/packages/effect/test/Array.test.ts index 2f3ac90513..bb08ca7b3a 100644 --- a/packages/effect/test/Array.test.ts +++ b/packages/effect/test/Array.test.ts @@ -7,6 +7,7 @@ import * as Order from "effect/Order" import type { Predicate } from "effect/Predicate" import * as String from "effect/String" import { deepStrictEqual, double, strictEqual } from "effect/test/util" +import * as Util from "effect/test/util" import * as fc from "fast-check" import { assert, describe, expect, it } from "vitest" @@ -1222,4 +1223,26 @@ describe("ReadonlyArray", () => { const arr: ReadonlyArray = [{ a: "a", b: 2 }, { a: "b", b: 1 }] expect(RA.sortWith(arr, (x) => x.b, Order.number)).toEqual([{ a: "b", b: 1 }, { a: "a", b: 2 }]) }) + + it("Do notation", () => { + const _do = RA.Do + Util.deepStrictEqual(_do, RA.of({})) + + const doA = RA.bind(_do, "a", () => ["a"]) + Util.deepStrictEqual(doA, RA.of({ a: "a" })) + + const doAB = RA.bind(doA, "b", (x) => ["b", x.a + "b"]) + Util.deepStrictEqual(doAB, [ + { a: "a", b: "b" }, + { a: "a", b: "ab" } + ]) + const doABC = RA.let(doAB, "c", (x) => [x.a, x.b, x.a + x.b]) + Util.deepStrictEqual(doABC, [ + { a: "a", b: "b", c: ["a", "b", "ab"] }, + { a: "a", b: "ab", c: ["a", "ab", "aab"] } + ]) + + const doABCD = RA.bind(doABC, "d", () => RA.empty()) + Util.deepStrictEqual(doABCD, []) + }) }) diff --git a/packages/effect/test/Cause.test.ts b/packages/effect/test/Cause.test.ts index c2ecbd48d7..f37be7072d 100644 --- a/packages/effect/test/Cause.test.ts +++ b/packages/effect/test/Cause.test.ts @@ -223,13 +223,13 @@ describe("Cause", () => { it("Sequential", () => { expect(String(Cause.sequential(Cause.fail("failure 1"), Cause.fail("failure 2")))).toEqual( - `Error: failure 1\r\nError: failure 2` + `Error: failure 1\nError: failure 2` ) }) it("Parallel", () => { expect(String(Cause.parallel(Cause.fail("failure 1"), Cause.fail("failure 2")))).toEqual( - `Error: failure 1\r\nError: failure 2` + `Error: failure 1\nError: failure 2` ) }) }) diff --git a/packages/effect/test/Chunk.test.ts b/packages/effect/test/Chunk.test.ts index c8d37e669d..ad08940b02 100644 --- a/packages/effect/test/Chunk.test.ts +++ b/packages/effect/test/Chunk.test.ts @@ -17,6 +17,8 @@ describe("Chunk", () => { expect(Chunk.unsafeFromNonEmptyArray).exist expect(Chunk.contains).exist expect(Chunk.containsWith).exist + expect(Chunk.difference).exist + expect(Chunk.differenceWith).exist expect(Chunk.findFirst).exist expect(Chunk.findFirstIndex).exist expect(Chunk.findLast).exist @@ -874,4 +876,25 @@ describe("Chunk", () => { expect(equivalence(Chunk.make(1, 2, 3), Chunk.make(1, 2))).toBe(false) expect(equivalence(Chunk.make(1, 2, 3), Chunk.make(1, 2, 4))).toBe(false) }) + + it("differenceWith", () => { + const eq = (a: E, b: E) => a.id === b.id + const diffW = pipe(eq, Chunk.differenceWith) + + const curr = Chunk.make({ id: 1 }, { id: 2 }, { id: 3 }) + + expect(diffW(Chunk.make({ id: 1 }, { id: 2 }), curr)).toEqual(Chunk.make({ id: 3 })) + expect(diffW(Chunk.empty(), curr)).toEqual(curr) + expect(diffW(curr, Chunk.empty())).toEqual(Chunk.empty()) + expect(diffW(curr, curr)).toEqual(Chunk.empty()) + }) + + it("difference", () => { + const curr = Chunk.make(1, 3, 5, 7, 9) + + expect(Chunk.difference(Chunk.make(1, 2, 3, 4, 5), curr)).toEqual(Chunk.make(7, 9)) + expect(Chunk.difference(Chunk.empty(), curr)).toEqual(curr) + expect(Chunk.difference(curr, Chunk.empty())).toEqual(Chunk.empty()) + expect(Chunk.difference(curr, curr)).toEqual(Chunk.empty()) + }) }) diff --git a/packages/effect/test/Data.test.ts b/packages/effect/test/Data.test.ts index 974c91bfe7..e7a1f551c5 100644 --- a/packages/effect/test/Data.test.ts +++ b/packages/effect/test/Data.test.ts @@ -1,6 +1,7 @@ import * as Data from "effect/Data" import * as Equal from "effect/Equal" -import { describe, expect, it } from "vitest" +import { pipe } from "effect/Function" +import { assert, describe, expect, it } from "vitest" describe("Data", () => { it("struct", () => { @@ -218,7 +219,7 @@ describe("Data", () => { interface ResultDefinition extends Data.TaggedEnum.WithGenerics<2> { readonly taggedEnum: Result } - const { Failure, Success } = Data.taggedEnum() + const { $is, $match, Failure, Success } = Data.taggedEnum() const a = Success({ value: 1 }) satisfies Result const b = Failure({ error: "test" }) satisfies Result @@ -233,6 +234,34 @@ describe("Data", () => { expect(Equal.equals(a, b)).toBe(false) expect(Equal.equals(a, c)).toBe(true) + + const aResult = Success({ value: 1 }) as Result + const bResult = Failure({ error: "boom" }) as Result + + assert.strictEqual( + $match(aResult, { + Success: (_) => 1, + Failure: (_) => 2 + }), + 1 + ) + const result = pipe( + bResult, + $match({ + Success: (_) => _.value, + Failure: (_) => _.error + }) + ) + result satisfies string | number + assert.strictEqual(result, "boom") + + assert($is("Success")(aResult)) + aResult satisfies { readonly _tag: "Success"; readonly value: number } + assert.strictEqual(aResult.value, 1) + + assert($is("Failure")(bResult)) + bResult satisfies { readonly _tag: "Failure"; readonly error: string } + assert.strictEqual(bResult.error, "boom") }) describe("Error", () => { diff --git a/packages/effect/test/Effect/cause-rendering.test.ts b/packages/effect/test/Effect/cause-rendering.test.ts index b7663ee64c..b003d2c4cd 100644 --- a/packages/effect/test/Effect/cause-rendering.test.ts +++ b/packages/effect/test/Effect/cause-rendering.test.ts @@ -16,7 +16,9 @@ describe("Effect", () => { ))) const rendered = Cause.pretty(cause) assert.include(rendered, "spanA") + assert.include(rendered, "cause-rendering.test.ts:12") assert.include(rendered, "spanB") + assert.include(rendered, "cause-rendering.test.ts:11") })) it.effect("catchTag should not invalidate traces", () => Effect.gen(function*($) { @@ -98,4 +100,38 @@ describe("Effect", () => { const pretty = Cause.pretty(cause) assert.include(pretty, "cause-rendering.test.ts") })) + + it.effect("functionWithSpan PrettyError stack", () => + Effect.gen(function*() { + const fail = Effect.functionWithSpan({ + body: (_id: number) => Effect.fail(new Error("boom")), + options: (id) => ({ name: `span-${id}` }) + }) + const cause = yield* fail(123).pipe(Effect.sandbox, Effect.flip) + const prettyErrors = Cause.prettyErrors(cause) + assert.strictEqual(prettyErrors.length, 1) + const error = prettyErrors[0] + assert.strictEqual(error.name, "Error") + assert.notInclude(error.stack, "/internal/") + assert.include(error.stack, "cause-rendering.test.ts:107") + assert.include(error.stack, "span-123") + assert.include(error.stack, "cause-rendering.test.ts:110") + })) + + it.effect("includes span name in stack", () => + Effect.gen(function*() { + const fn = Effect.functionWithSpan({ + options: (n) => ({ name: `fn-${n}` }), + body: (a: number) => + Effect.sync(() => { + assert.strictEqual(a, 2) + }) + }) + const cause = yield* fn(0).pipe( + Effect.sandbox, + Effect.flip + ) + const prettyErrors = Cause.prettyErrors(cause) + assert.include(prettyErrors[0].stack ?? "", "at fn-0 ") + })) }) diff --git a/packages/effect/test/Tracer.test.ts b/packages/effect/test/Tracer.test.ts index 3067db9ddf..c3c2c4ae2f 100644 --- a/packages/effect/test/Tracer.test.ts +++ b/packages/effect/test/Tracer.test.ts @@ -14,13 +14,11 @@ import { assert, describe } from "vitest" describe("Tracer", () => { describe("withSpan", () => { it.effect("no parent", () => - Effect.gen(function*($) { - const span = yield* $( - Effect.withSpan("A")(Effect.currentSpan) - ) - + Effect.gen(function*() { + const span = yield* Effect.withSpan("A")(Effect.currentSpan) assert.deepEqual(span.name, "A") assert.deepEqual(span.parent, Option.none()) + assert.include(span.attributes.get("code.stacktrace"), "Tracer.test.ts:18") })) it.effect("parent", () => @@ -77,203 +75,226 @@ describe("Tracer", () => { assert.deepEqual((span.status as any)["endTime"], 1000000000n) assert.deepEqual(span.status._tag, "Ended") })) + }) - it.effect("annotateSpans", () => - Effect.gen(function*($) { - const span = yield* $( - Effect.annotateSpans( - Effect.withSpan("A")(Effect.currentSpan), - "key", - "value" - ) + it.effect("annotateSpans", () => + Effect.gen(function*($) { + const span = yield* $( + Effect.annotateSpans( + Effect.withSpan("A")(Effect.currentSpan), + "key", + "value" ) + ) - assert.deepEqual(span.name, "A") - assert.deepEqual(span.parent, Option.none()) - assert.deepEqual(span.attributes.get("key"), "value") - })) + assert.deepEqual(span.name, "A") + assert.deepEqual(span.parent, Option.none()) + assert.deepEqual(span.attributes.get("key"), "value") + })) - it.effect("annotateSpans record", () => - Effect.gen(function*($) { - const span = yield* $( - Effect.annotateSpans( - Effect.withSpan("A")(Effect.currentSpan), - { key: "value", key2: "value2" } - ) + it.effect("annotateSpans record", () => + Effect.gen(function*($) { + const span = yield* $( + Effect.annotateSpans( + Effect.withSpan("A")(Effect.currentSpan), + { key: "value", key2: "value2" } ) + ) - assert.deepEqual(span.attributes.get("key"), "value") - assert.deepEqual(span.attributes.get("key2"), "value2") - })) - - it.effect("logger", () => - Effect.gen(function*($) { - yield* $(TestClock.adjust(millis(0.01))) - - const [span, fiberId] = yield* $( - Effect.log("event"), - Effect.zipRight(Effect.all([Effect.currentSpan, Effect.fiberId])), - Effect.withSpan("A") - ) + assert.deepEqual(span.attributes.get("key"), "value") + assert.deepEqual(span.attributes.get("key2"), "value2") + })) - assert.deepEqual(span.name, "A") - assert.deepEqual(span.parent, Option.none()) - assert.deepEqual((span as NativeSpan).events, [["event", 10000n, { - "effect.fiberId": FiberId.threadName(fiberId), - "effect.logLevel": "INFO" - }]]) - })) + it.effect("logger", () => + Effect.gen(function*($) { + yield* $(TestClock.adjust(millis(0.01))) - it.effect("withTracerTiming false", () => - Effect.gen(function*($) { - yield* $(TestClock.adjust(millis(1))) + const [span, fiberId] = yield* $( + Effect.log("event"), + Effect.zipRight(Effect.all([Effect.currentSpan, Effect.fiberId])), + Effect.withSpan("A") + ) - const span = yield* $( - Effect.withSpan("A")(Effect.currentSpan), - Effect.withTracerTiming(false) - ) + assert.deepEqual(span.name, "A") + assert.deepEqual(span.parent, Option.none()) + assert.deepEqual((span as NativeSpan).events, [["event", 10000n, { + "effect.fiberId": FiberId.threadName(fiberId), + "effect.logLevel": "INFO" + }]]) + })) - assert.deepEqual(span.status.startTime, 0n) - })) + it.effect("withTracerTiming false", () => + Effect.gen(function*($) { + yield* $(TestClock.adjust(millis(1))) - it.effect("useSpanScoped", () => - Effect.gen(function*(_) { - const span = yield* _(Effect.scoped(Effect.makeSpanScoped("A"))) - assert.deepEqual(span.status._tag, "Ended") - })) + const span = yield* $( + Effect.withSpan("A")(Effect.currentSpan), + Effect.withTracerTiming(false) + ) - it.effect("annotateCurrentSpan", () => - Effect.gen(function*(_) { - yield* _(Effect.annotateCurrentSpan("key", "value")) - const span = yield* _(Effect.currentSpan) - assert.deepEqual(span.attributes.get("key"), "value") - }).pipe( - Effect.withSpan("A") - )) + assert.deepEqual(span.status.startTime, 0n) + })) - it.effect("withParentSpan", () => - Effect.gen(function*(_) { - const span = yield* _(Effect.currentSpan) - assert.deepEqual( - span.parent.pipe( - Option.map((_) => _.spanId) - ), - Option.some("456") - ) - }).pipe( - Effect.withSpan("A"), - Effect.withParentSpan({ - _tag: "ExternalSpan", - traceId: "123", - spanId: "456", - sampled: true, - context: Context.empty() - }) - )) + it.effect("useSpanScoped", () => + Effect.gen(function*() { + const span = yield* Effect.scoped(Effect.makeSpanScoped("A")) + assert.deepEqual(span.status._tag, "Ended") + assert.include(span.attributes.get("code.stacktrace"), "Tracer.test.ts:140") + })) - it.effect("Layer.parentSpan", () => - Effect.gen(function*(_) { - const span = yield* _(Effect.makeSpan("child")) - assert.deepEqual( - span.parent.pipe( - Option.filter((span): span is Span => span._tag === "Span"), - Option.map((span) => span.name) - ), - Option.some("parent") + it.effect("annotateCurrentSpan", () => + Effect.gen(function*(_) { + yield* _(Effect.annotateCurrentSpan("key", "value")) + const span = yield* _(Effect.currentSpan) + assert.deepEqual(span.attributes.get("key"), "value") + }).pipe( + Effect.withSpan("A") + )) + + it.effect("withParentSpan", () => + Effect.gen(function*(_) { + const span = yield* _(Effect.currentSpan) + assert.deepEqual( + span.parent.pipe( + Option.map((_) => _.spanId) + ), + Option.some("456") + ) + }).pipe( + Effect.withSpan("A"), + Effect.withParentSpan({ + _tag: "ExternalSpan", + traceId: "123", + spanId: "456", + sampled: true, + context: Context.empty() + }) + )) + + it.effect("Layer.parentSpan", () => + Effect.gen(function*() { + const span = yield* Effect.makeSpan("child") + const parent = yield* Option.filter(span.parent, (span): span is Span => span._tag === "Span") + assert.deepEqual(parent.name, "parent") + assert.include(span.attributes.get("code.stacktrace"), "Tracer.test.ts:176") + assert.include(parent.attributes.get("code.stacktrace"), "Tracer.test.ts:184") + }).pipe( + Effect.provide(Layer.unwrapScoped( + Effect.map( + Effect.makeSpanScoped("parent"), + (span) => Layer.parentSpan(span) ) - }).pipe( - Effect.provide(Layer.unwrapScoped( - Effect.map( - Effect.makeSpanScoped("parent"), - (span) => Layer.parentSpan(span) - ) - )) )) + )) + + it.effect("Layer.span", () => + Effect.gen(function*() { + const span = yield* Effect.makeSpan("child") + const parent = span.parent.pipe( + Option.filter((span): span is Span => span._tag === "Span"), + Option.getOrThrow + ) + assert.strictEqual(parent.name, "parent") + assert.include(parent.attributes.get("code.stacktrace"), "Tracer.test.ts:200") + }).pipe( + Effect.provide(Layer.span("parent")) + )) + + it.effect("Layer.span onEnd", () => + Effect.gen(function*(_) { + let onEndCalled = false + const span = yield* _( + Effect.currentSpan, + Effect.provide(Layer.span("span", { + onEnd: (span, _exit) => + Effect.sync(() => { + assert.strictEqual(span.name, "span") + onEndCalled = true + }) + })) + ) + assert.strictEqual(span.name, "span") + assert.strictEqual(onEndCalled, true) + })) - it.effect("Layer.span", () => - Effect.gen(function*(_) { - const span = yield* _(Effect.makeSpan("child")) - assert.deepEqual( - span.parent.pipe( - Option.filter((span): span is Span => span._tag === "Span"), - Option.map((span) => span.name) - ), - Option.some("parent") - ) - }).pipe( - Effect.provide(Layer.span("parent")) - )) + it.effect("linkSpans", () => + Effect.gen(function*(_) { + const childA = yield* _(Effect.makeSpan("childA")) + const childB = yield* _(Effect.makeSpan("childB")) + const currentSpan = yield* _( + Effect.currentSpan, + Effect.withSpan("A", { links: [{ _tag: "SpanLink", span: childB, attributes: {} }] }), + Effect.linkSpans(childA) + ) + assert.includeMembers( + currentSpan.links.map((_) => _.span), + [childB, childA] + ) + })) - it.effect("Layer.span onEnd", () => - Effect.gen(function*(_) { - let onEndCalled = false - const span = yield* _( - Effect.currentSpan, - Effect.provide(Layer.span("span", { - onEnd: (span, _exit) => - Effect.sync(() => { - assert.strictEqual(span.name, "span") - onEndCalled = true - }) - })) - ) + it.effect("Layer.withSpan", () => + Effect.gen(function*(_) { + let onEndCalled = false + const layer = Layer.effectDiscard(Effect.gen(function*() { + const span = yield* Effect.currentSpan assert.strictEqual(span.name, "span") - assert.strictEqual(onEndCalled, true) - })) - - it.effect("linkSpans", () => - Effect.gen(function*(_) { - const childA = yield* _(Effect.makeSpan("childA")) - const childB = yield* _(Effect.makeSpan("childB")) - const currentSpan = yield* _( - Effect.currentSpan, - Effect.withSpan("A", { links: [{ _tag: "SpanLink", span: childB, attributes: {} }] }), - Effect.linkSpans(childA) - ) - assert.includeMembers( - currentSpan.links.map((_) => _.span), - [childB, childA] - ) - })) + assert.include(span.attributes.get("code.stacktrace"), "Tracer.test.ts:243") + })).pipe( + Layer.withSpan("span", { + onEnd: (span, _exit) => + Effect.sync(() => { + assert.strictEqual(span.name, "span") + onEndCalled = true + }) + }) + ) - it.effect("Layer.withSpan", () => - Effect.gen(function*(_) { - let onEndCalled = false - const layer = Layer.effectDiscard(Effect.gen(function*(_) { - const span = yield* _(Effect.currentSpan) - assert.strictEqual(span.name, "span") - })).pipe( - Layer.withSpan("span", { - onEnd: (span, _exit) => - Effect.sync(() => { - assert.strictEqual(span.name, "span") - onEndCalled = true - }) - }) - ) + const span = yield* _(Effect.currentSpan, Effect.provide(layer), Effect.option) - const span = yield* _(Effect.currentSpan, Effect.provide(layer), Effect.option) + assert.deepEqual(span, Option.none()) + assert.strictEqual(onEndCalled, true) + })) +}) - assert.deepEqual(span, Option.none()) - assert.strictEqual(onEndCalled, true) - })) +it.effect("withTracerEnabled", () => + Effect.gen(function*($) { + const span = yield* $( + Effect.currentSpan, + Effect.withSpan("A"), + Effect.withTracerEnabled(false) + ) + const spanB = yield* $( + Effect.currentSpan, + Effect.withSpan("B"), + Effect.withTracerEnabled(true) + ) + + assert.deepEqual(span.name, "A") + assert.deepEqual(span.spanId, "noop") + assert.deepEqual(spanB.name, "B") + })) + +describe("functionWithSpan", () => { + const getSpan = Effect.functionWithSpan({ + body: (_id: string) => Effect.currentSpan, + options: (id) => ({ + name: `span-${id}`, + attributes: { id } + }) }) - it.effect("withTracerEnabled", () => - Effect.gen(function*($) { - const span = yield* $( - Effect.currentSpan, - Effect.withSpan("A"), - Effect.withTracerEnabled(false) - ) - const spanB = yield* $( - Effect.currentSpan, - Effect.withSpan("B"), - Effect.withTracerEnabled(true) - ) + it.effect("no parent", () => + Effect.gen(function*() { + const span = yield* getSpan("A") + assert.deepEqual(span.name, "span-A") + assert.deepEqual(span.parent, Option.none()) + assert.include(span.attributes.get("code.stacktrace"), "Tracer.test.ts:288") + })) - assert.deepEqual(span.name, "A") - assert.deepEqual(span.spanId, "noop") - assert.deepEqual(spanB.name, "B") + it.effect("parent", () => + Effect.gen(function*() { + const span = yield* Effect.withSpan("B")(getSpan("A")) + assert.deepEqual(span.name, "span-A") + assert.deepEqual(Option.map(span.parent, (span) => (span as Span).name), Option.some("B")) })) }) diff --git a/packages/experimental/src/Machine.ts b/packages/experimental/src/Machine.ts index 53f1f3642f..5ac3517b96 100644 --- a/packages/experimental/src/Machine.ts +++ b/packages/experimental/src/Machine.ts @@ -542,7 +542,8 @@ export const boot = < "effect.machine": runState.identifier, ...request }, - kind: "client" + kind: "client", + captureStackTrace: false }, (span) => Queue.offer(requests, [request, deferred, span, true]).pipe( Effect.zipRight(Deferred.await(deferred)), @@ -565,7 +566,8 @@ export const boot = < "effect.machine": runState.identifier, ...request }, - kind: "client" + kind: "client", + captureStackTrace: false }, (span) => Queue.offer(requests, [request, deferred, span, true])) } ) @@ -762,7 +764,8 @@ export const boot = < parent: span, attributes: { "effect.machine": runState.identifier - } + }, + captureStackTrace: false }) } else if (span !== undefined) { handler = Effect.provideService(handler, Tracer.ParentSpan, span) diff --git a/packages/opentelemetry/src/internal/tracer.ts b/packages/opentelemetry/src/internal/tracer.ts index ecccf7d953..2e0d13d340 100644 --- a/packages/opentelemetry/src/internal/tracer.ts +++ b/packages/opentelemetry/src/internal/tracer.ts @@ -75,6 +75,7 @@ export class OtelSpan implements EffectTracer.Span { } end(endTime: bigint, exit: Exit) { + const hrTime = nanosToHrTime(endTime) this.status = { _tag: "Ended", endTime, @@ -83,9 +84,7 @@ export class OtelSpan implements EffectTracer.Span { } if (exit._tag === "Success") { - this.span.setStatus({ - code: OtelApi.SpanStatusCode.OK - }) + this.span.setStatus({ code: OtelApi.SpanStatusCode.OK }) } else { if (Cause.isInterruptedOnly(exit.cause)) { this.span.setStatus({ @@ -95,13 +94,22 @@ export class OtelSpan implements EffectTracer.Span { this.span.setAttribute("span.label", "⚠︎ Interrupted") this.span.setAttribute("status.interrupted", true) } else { - this.span.setStatus({ - code: OtelApi.SpanStatusCode.ERROR, - message: Cause.pretty(exit.cause) - }) + const errors = Cause.prettyErrors(exit.cause) + if (errors.length > 0) { + for (const error of errors) { + this.span.recordException(error, hrTime) + } + this.span.setStatus({ + code: OtelApi.SpanStatusCode.ERROR, + message: errors[0].message + }) + } else { + // empty cause means no error + this.span.setStatus({ code: OtelApi.SpanStatusCode.OK }) + } } } - this.span.end(nanosToHrTime(endTime)) + this.span.end(hrTime) } event(name: string, startTime: bigint, attributes?: Record) { diff --git a/packages/platform-bun/src/internal/http/server.ts b/packages/platform-bun/src/internal/http/server.ts index ea81da0ef2..3626460eb8 100644 --- a/packages/platform-bun/src/internal/http/server.ts +++ b/packages/platform-bun/src/internal/http/server.ts @@ -27,6 +27,7 @@ import * as Inspectable from "effect/Inspectable" import * as Layer from "effect/Layer" import * as Option from "effect/Option" import type { ReadonlyRecord } from "effect/Record" +import type * as Runtime from "effect/Runtime" import type * as Scope from "effect/Scope" import * as Stream from "effect/Stream" import { Readable } from "node:stream" @@ -73,25 +74,27 @@ export const make = ( return Server.make({ address: { _tag: "TcpAddress", port: server.port, hostname: server.hostname }, serve(httpApp, middleware) { - const app = App.toHandled(httpApp, (request, exit) => - Effect.sync(() => { - const impl = request as ServerRequestImpl - if (exit._tag === "Success") { - impl.resolve(makeResponse(request, exit.value)) - } else if (Cause.isInterruptedOnly(exit.cause)) { - impl.resolve( - new Response(undefined, { - status: impl.source.signal.aborted ? 499 : 503 - }) - ) - } else { - impl.reject(Cause.pretty(exit.cause)) - } - }), middleware) - return pipe( FiberSet.makeRuntime(), - Effect.flatMap((runFork) => + Effect.bindTo("runFork"), + Effect.bind("runtime", () => Effect.runtime()), + Effect.let("app", ({ runtime }) => + App.toHandled(httpApp, (request, exit) => + Effect.sync(() => { + const impl = request as ServerRequestImpl + if (exit._tag === "Success") { + impl.resolve(makeResponse(request, exit.value, runtime)) + } else if (Cause.isInterruptedOnly(exit.cause)) { + impl.resolve( + new Response(undefined, { + status: impl.source.signal.aborted ? 499 : 503 + }) + ) + } else { + impl.reject(Cause.pretty(exit.cause)) + } + }), middleware)), + Effect.flatMap(({ app, runFork }) => Effect.async((_) => { function handler(request: Request, server: BunServer) { return new Promise((resolve, reject) => { @@ -121,7 +124,11 @@ export const make = ( }) }) -const makeResponse = (request: ServerRequest.ServerRequest, response: ServerResponse.ServerResponse): Response => { +const makeResponse = ( + request: ServerRequest.ServerRequest, + response: ServerResponse.ServerResponse, + runtime: Runtime.Runtime +): Response => { const fields: { headers: globalThis.Headers status?: number @@ -157,7 +164,10 @@ const makeResponse = (request: ServerRequest.ServerRequest, response: ServerResp return new Response(body.formData as any, fields) } case "Stream": { - return new Response(Stream.toReadableStream(body.stream), fields) + return new Response( + Stream.toReadableStreamRuntime(body.stream, runtime), + fields + ) } } } diff --git a/packages/platform/src/Http/ServerResponse.ts b/packages/platform/src/Http/ServerResponse.ts index 126b209a47..a84c3b06a8 100644 --- a/packages/platform/src/Http/ServerResponse.ts +++ b/packages/platform/src/Http/ServerResponse.ts @@ -165,7 +165,7 @@ export const formData: (body: FormData, options?: Options.WithContent | undefine * @since 1.0.0 * @category constructors */ -export const stream: (body: Stream.Stream, options?: Options | undefined) => ServerResponse = +export const stream: (body: Stream.Stream, options?: Options | undefined) => ServerResponse = internal.stream /** diff --git a/packages/platform/src/internal/http/client.ts b/packages/platform/src/internal/http/client.ts index 2be03beb81..b92311c448 100644 --- a/packages/platform/src/internal/http/client.ts +++ b/packages/platform/src/internal/http/client.ts @@ -14,7 +14,6 @@ import * as Ref from "effect/Ref" import type * as Schedule from "effect/Schedule" import * as Scope from "effect/Scope" import * as Stream from "effect/Stream" -import type * as Body from "../../Http/Body.js" import type * as Client from "../../Http/Client.js" import * as Error from "../../Http/ClientError.js" import type * as ClientRequest from "../../Http/ClientRequest.js" @@ -144,7 +143,7 @@ export const makeDefault = ( addAbort, Effect.useSpan( `http.client ${request.method}`, - { kind: "client" }, + { kind: "client", captureStackTrace: false }, (span) => { span.attribute("http.request.method", request.method) span.attribute("server.address", url.origin) @@ -222,26 +221,19 @@ export const fetch: Client.Client.Default = makeDefault((request, url, signal, f (response) => internalResponse.fromWeb(request, response) ) if (Method.hasBody(request.method)) { - return send(convertBody(request.body)) + switch (request.body._tag) { + case "Raw": + case "Uint8Array": + return send(request.body.body as any) + case "FormData": + return send(request.body.formData) + case "Stream": + return Effect.flatMap(Stream.toReadableStreamEffect(request.body.stream), send) + } } return send(undefined) }) -const convertBody = (body: Body.Body): BodyInit | undefined => { - switch (body._tag) { - case "Empty": - return undefined - case "Raw": - return body.body as any - case "Uint8Array": - return body.body - case "FormData": - return body.formData - case "Stream": - return Stream.toReadableStream(body.stream) - } -} - /** @internal */ export const transform = dual< ( diff --git a/packages/platform/src/internal/http/middleware.ts b/packages/platform/src/internal/http/middleware.ts index 36df4374c6..c4cff7920e 100644 --- a/packages/platform/src/internal/http/middleware.ts +++ b/packages/platform/src/internal/http/middleware.ts @@ -125,7 +125,11 @@ export const tracer = make((httpApp) => const redactedHeaders = Headers.redact(request.headers, redactedHeaderNames) return Effect.useSpan( `http.server ${request.method}`, - { parent: Option.getOrUndefined(TraceContext.fromHeaders(request.headers)), kind: "server" }, + { + parent: Option.getOrUndefined(TraceContext.fromHeaders(request.headers)), + kind: "server", + captureStackTrace: false + }, (span) => { span.attribute("http.request.method", request.method) if (url !== undefined) { diff --git a/packages/platform/src/internal/http/serverResponse.ts b/packages/platform/src/internal/http/serverResponse.ts index e576c4ca96..3834e6f550 100644 --- a/packages/platform/src/internal/http/serverResponse.ts +++ b/packages/platform/src/internal/http/serverResponse.ts @@ -258,8 +258,8 @@ export const formData = ( ) /** @internal */ -export const stream = ( - body: Stream.Stream, +export const stream = ( + body: Stream.Stream, options?: ServerResponse.Options | undefined ): ServerResponse.ServerResponse => new ServerResponseImpl( diff --git a/packages/platform/test/HttpClient.test.ts b/packages/platform/test/HttpClient.test.ts index b513e990d4..15d02327d2 100644 --- a/packages/platform/test/HttpClient.test.ts +++ b/packages/platform/test/HttpClient.test.ts @@ -4,6 +4,7 @@ import { Ref } from "effect" import * as Context from "effect/Context" import * as Effect from "effect/Effect" import * as Layer from "effect/Layer" +import * as Logger from "effect/Logger" import * as Stream from "effect/Stream" import { assert, describe, expect, it } from "vitest" @@ -129,4 +130,26 @@ describe("HttpClient", () => { const response = yield* _(Http.request.get("/todos/1"), todoClient) expect(response.id).toBe(1) }).pipe(Effect.provide(Http.client.layer), Effect.runPromise)) + + it("streamBody accesses the current runtime", () => + Effect.gen(function*(_) { + const defaultClient = yield* _(Http.client.Client) + + const requestStream = Stream.fromIterable(["hello", "world"]).pipe( + Stream.tap((_) => Effect.log(_)), + Stream.encodeText + ) + + const logs: Array = [] + const logger = Logger.make(({ message }) => logs.push(message)) + + yield* Http.request.post("https://jsonplaceholder.typicode.com").pipe( + Http.request.streamBody(requestStream), + defaultClient, + Effect.provide(Logger.replace(Logger.defaultLogger, logger)), + Effect.scoped + ) + + expect(logs).toEqual(["hello", "world"]) + }).pipe(Effect.provide(Http.client.layer), Effect.runPromise)) }) diff --git a/packages/rpc/src/Router.ts b/packages/rpc/src/Router.ts index 1b671c3b48..0db28d8809 100644 --- a/packages/rpc/src/Router.ts +++ b/packages/rpc/src/Router.ts @@ -244,7 +244,8 @@ export const toHandler = >(router: R, options?: { spanId: req.spanId, sampled: req.sampled, context: Context.empty() - } + }, + captureStackTrace: false }) ) } @@ -276,7 +277,8 @@ export const toHandler = >(router: R, options?: { spanId: req.spanId, sampled: req.sampled, context: Context.empty() - } + }, + captureStackTrace: false }) ) }, { concurrency: "unbounded", discard: true }), @@ -332,7 +334,8 @@ export const toHandlerEffect = >(router: R, options?: spanId: req.spanId, sampled: req.sampled, context: Context.empty() - } + }, + captureStackTrace: false }) ) } @@ -352,7 +355,8 @@ export const toHandlerEffect = >(router: R, options?: spanId: req.spanId, sampled: req.sampled, context: Context.empty() - } + }, + captureStackTrace: false }) ) }, { concurrency: "unbounded" }) diff --git a/packages/rpc/src/Rpc.ts b/packages/rpc/src/Rpc.ts index 61d71e626c..943885afd6 100644 --- a/packages/rpc/src/Rpc.ts +++ b/packages/rpc/src/Rpc.ts @@ -329,7 +329,8 @@ export const request = ( ): Effect.Effect, never, Scope> => pipe( Effect.makeSpanScoped(`${options?.spanPrefix ?? "Rpc.request "}${request._tag}`, { - kind: "client" + kind: "client", + captureStackTrace: false }), Effect.zip(FiberRef.get(currentHeaders)), Effect.map(([span, headers]) => diff --git a/packages/sql/src/Resolver.ts b/packages/sql/src/Resolver.ts index c3faef4b9f..e96dedce91 100644 --- a/packages/sql/src/Resolver.ts +++ b/packages/sql/src/Resolver.ts @@ -118,7 +118,7 @@ const makeResolver = ( return function(input: I) { return Effect.useSpan( `sql.Resolver.execute ${tag}`, - { kind: "client" }, + { kind: "client", captureStackTrace: false }, (span) => Effect.withFiberRuntime((fiber) => { span.attribute("request.input", input) @@ -247,7 +247,8 @@ export const ordered = } @@ -326,7 +327,8 @@ export const grouped = } @@ -404,7 +406,8 @@ export const findById = } @@ -450,7 +453,8 @@ const void_ = ( Effect.withSpan(`sql.Resolver.batch ${tag}`, { kind: "client", links: spanLinks, - attributes: { "request.count": inputs.length } + attributes: { "request.count": inputs.length }, + captureStackTrace: false }) ) as Effect.Effect } diff --git a/packages/sql/src/internal/client.ts b/packages/sql/src/internal/client.ts index fdc38138af..5ca7c04bc3 100644 --- a/packages/sql/src/internal/client.ts +++ b/packages/sql/src/internal/client.ts @@ -56,54 +56,58 @@ export function make({ effect: Effect.Effect ): Effect.Effect => Effect.uninterruptibleMask((restore) => - Effect.useSpan("sql.transaction", (span) => - Effect.withFiberRuntime((fiber) => { - for (const [key, value] of spanAttributes) { - span.attribute(key, value) - } - const context = fiber.getFiberRef(FiberRef.currentContext) - const clock = Context.get(fiber.getFiberRef(DefaultServices.currentServices), Clock.Clock) - const connOption = Context.getOption(context, TransactionConnection) - const conn = connOption._tag === "Some" - ? Effect.succeed([undefined, connOption.value[0]] as const) - : getTxConn - const id = connOption._tag === "Some" ? connOption.value[1] + 1 : 0 - return Effect.flatMap( - conn, - ( - [scope, conn] - ) => - conn.executeRaw(id === 0 ? beginTransaction : savepoint(`effect_sql_${id}`)).pipe( - Effect.zipRight(Effect.locally( - restore(effect), - FiberRef.currentContext, - Context.add(context, TransactionConnection, [conn, id]).pipe( - Context.add(Tracer.ParentSpan, span) - ) - )), - Effect.exit, - Effect.flatMap((exit) => { - let effect: Effect.Effect - if (Exit.isSuccess(exit)) { - if (id === 0) { - span.event("db.transaction.commit", clock.unsafeCurrentTimeNanos()) - effect = Effect.orDie(conn.executeRaw(commit)) + Effect.useSpan( + "sql.transaction", + { kind: "client", captureStackTrace: false }, + (span) => + Effect.withFiberRuntime((fiber) => { + for (const [key, value] of spanAttributes) { + span.attribute(key, value) + } + const context = fiber.getFiberRef(FiberRef.currentContext) + const clock = Context.get(fiber.getFiberRef(DefaultServices.currentServices), Clock.Clock) + const connOption = Context.getOption(context, TransactionConnection) + const conn = connOption._tag === "Some" + ? Effect.succeed([undefined, connOption.value[0]] as const) + : getTxConn + const id = connOption._tag === "Some" ? connOption.value[1] + 1 : 0 + return Effect.flatMap( + conn, + ( + [scope, conn] + ) => + conn.executeRaw(id === 0 ? beginTransaction : savepoint(`effect_sql_${id}`)).pipe( + Effect.zipRight(Effect.locally( + restore(effect), + FiberRef.currentContext, + Context.add(context, TransactionConnection, [conn, id]).pipe( + Context.add(Tracer.ParentSpan, span) + ) + )), + Effect.exit, + Effect.flatMap((exit) => { + let effect: Effect.Effect + if (Exit.isSuccess(exit)) { + if (id === 0) { + span.event("db.transaction.commit", clock.unsafeCurrentTimeNanos()) + effect = Effect.orDie(conn.executeRaw(commit)) + } else { + span.event("db.transaction.savepoint", clock.unsafeCurrentTimeNanos()) + effect = Effect.void + } } else { - span.event("db.transaction.savepoint", clock.unsafeCurrentTimeNanos()) - effect = Effect.void + span.event("db.transaction.rollback", clock.unsafeCurrentTimeNanos()) + effect = Effect.orDie( + id > 0 ? conn.executeRaw(rollbackSavepoint(`effect_sql_${id}`)) : conn.executeRaw(rollback) + ) } - } else { - span.event("db.transaction.rollback", clock.unsafeCurrentTimeNanos()) - effect = Effect.orDie( - id > 0 ? conn.executeRaw(rollbackSavepoint(`effect_sql_${id}`)) : conn.executeRaw(rollback) - ) - } - const withScope = scope !== undefined ? Effect.ensuring(effect, Scope.close(scope, exit)) : effect - return Effect.zipRight(withScope, exit) - }) - ) - ) - })) + const withScope = scope !== undefined ? Effect.ensuring(effect, Scope.close(scope, exit)) : effect + return Effect.zipRight(withScope, exit) + }) + ) + ) + }) + ) ) const client: Client.Client = Object.assign( diff --git a/packages/sql/src/internal/statement.ts b/packages/sql/src/internal/statement.ts index 6666581f3e..c5e02751e7 100644 --- a/packages/sql/src/internal/statement.ts +++ b/packages/sql/src/internal/statement.ts @@ -90,7 +90,7 @@ export class StatementPrimitive extends Effectable.Class, Er ): Effect.Effect { return Effect.useSpan( "sql.execute", - { kind: "client" }, + { kind: "client", captureStackTrace: false }, (span) => Effect.withFiberRuntime((fiber) => { const transform = fiber.getFiberRef(currentTransformer) @@ -118,7 +118,7 @@ export class StatementPrimitive extends Effectable.Class, Er get stream(): Stream.Stream { return Stream.unwrapScoped(Effect.flatMap( - Effect.makeSpanScoped("sql.execute", { kind: "client" }), + Effect.makeSpanScoped("sql.execute", { kind: "client", captureStackTrace: false }), (span) => Effect.withFiberRuntime, Error.SqlError, Scope>((fiber) => { const transform = fiber.getFiberRef(currentTransformer) diff --git a/packages/vitest/src/index.ts b/packages/vitest/src/index.ts index 562ba6f4e8..dea0a8c96f 100644 --- a/packages/vitest/src/index.ts +++ b/packages/vitest/src/index.ts @@ -2,9 +2,11 @@ * @since 1.0.0 */ import type { Tester, TesterContext } from "@vitest/expect" +import * as Cause from "effect/Cause" import * as Duration from "effect/Duration" import * as Effect from "effect/Effect" import * as Equal from "effect/Equal" +import * as Exit from "effect/Exit" import { pipe } from "effect/Function" import * as Layer from "effect/Layer" import * as Logger from "effect/Logger" @@ -16,6 +18,22 @@ import * as Utils from "effect/Utils" import type { TestAPI } from "vitest" import * as V from "vitest" +const runTest = (effect: Effect.Effect) => + Effect.gen(function*() { + const exit: Exit.Exit = yield* Effect.exit(effect) + if (Exit.isSuccess(exit)) { + return () => {} + } else { + const errors = Cause.prettyErrors(exit.cause) + for (let i = 1; i < errors.length; i++) { + yield* Effect.logError(errors[i]) + } + return () => { + throw errors[0] + } + } + }).pipe(Effect.runPromise).then((f) => f()) + /** * @since 1.0.0 */ @@ -58,7 +76,7 @@ export const effect = (() => { pipe( Effect.suspend(() => self(c)), Effect.provide(TestEnv), - Effect.runPromise + runTest ), timeout ) @@ -74,7 +92,7 @@ export const effect = (() => { pipe( Effect.suspend(() => self(c)), Effect.provide(TestEnv), - Effect.runPromise + runTest ), timeout ), @@ -89,7 +107,7 @@ export const effect = (() => { pipe( Effect.suspend(() => self(c)), Effect.provide(TestEnv), - Effect.runPromise + runTest ), timeout ) @@ -109,7 +127,7 @@ export const live = ( (c) => pipe( Effect.suspend(() => self(c)), - Effect.runPromise + runTest ), timeout ) @@ -150,7 +168,7 @@ export const scoped = ( Effect.suspend(() => self(c)), Effect.scoped, Effect.provide(TestEnv), - Effect.runPromise + runTest ), timeout ) @@ -169,7 +187,7 @@ export const scopedLive = ( pipe( Effect.suspend(() => self(c)), Effect.scoped, - Effect.runPromise + runTest ), timeout )